Ponieważ typy podstawowe są zdefiniowane jako część rdzenia języka C++, są one dostępne do natychmiastowego użycia. Na przykład, jeśli chcemy zdefiniować zmienną typu int lub double, możemy to po prostu zrobić:
int x; // define variable of fundamental type 'int'
double d; // define variable of fundamental type 'double'Odnosi się to również do typów złożonych, które są prostymi rozszerzeniami typów podstawowych (w tym funkcji, wskaźników, referencji i tablic):
void fcn(int) {}; // define a function of type void(int)
int* ptr; // define variable of compound type 'pointer to int'
int& ref { x }; // define variable of compound type 'reference to int' (initialized with x)
int arr[5]; // define an array of 5 integers of type int[5] (we'll cover this in a future chapter)To działa, ponieważ język C++ już wie, co oznaczają nazwy typów (i symbole) dla tych typów - nie musimy podawać ani importować żadnych definicje.
Rozważmy jednak przypadek aliasu typu (przedstawiony w lekcji 10.7 -- Typedefs i aliasy typów), który pozwala nam zdefiniować nową nazwę dla istniejącego typu. Ponieważ alias typu wprowadza do programu nowy identyfikator, alias typu musi zostać zdefiniowany, zanim będzie można go użyć:
#include <iostream>
using Length = int; // define a type alias with identifier 'Length'
int main()
{
Length x { 5 }; // we can use 'length' here since we defined it above
std::cout << x << '\n';
return 0;
}Gdybyśmy pominęli definicję Length, kompilator nie wiedziałby, czym jest a Length i narzekałby, gdy próbowaliśmy zdefiniować zmienną przy użyciu tego typu. Definicja for Length nie tworzy obiektu - po prostu mówi kompilatorowi, czym jest Length a, aby można było jej później użyć.
Co to są typy zdefiniowane przez użytkownika/programowo?
Wracając do poprzedniego rozdziału (12.1 — Wprowadzenie do złożonych typów danych), przedstawiliśmy wyzwanie polegające na konieczności przechowywania ułamka, który ma licznik i mianownik które są ze sobą koncepcyjnie powiązane. Podczas tej lekcji omówiliśmy niektóre wyzwania związane z użyciem dwóch oddzielnych liczb całkowitych do niezależnego przechowywania licznika i mianownika ułamka.
Gdyby C++ miał wbudowany typ ułamka, byłoby idealnie — ale tak nie jest. Istnieją setki innych potencjalnie przydatnych typów, których C++ nie zawiera, ponieważ po prostu nie można przewidzieć wszystkiego, czego ktoś może potrzebować (nie mówiąc już o zaimplementowaniu i przetestowaniu tych rzeczy).
Zamiast tego C++ rozwiązuje takie problemy w inny sposób: umożliwiając tworzenie zupełnie nowych, niestandardowych typów, których możemy używać w naszych programach! Typy takie nazywane są typami zdefiniowanymi przez użytkownika. Jednakże, jak omówimy w dalszej części tej lekcji, będziemy preferować termin typy zdefiniowane przez program dla wszelkich typów, które tworzymy do użytku w naszych własnych programach.
C++ ma dwie różne kategorie typów złożonych, których można używać do tworzenia typów zdefiniowanych przez program:
- Typy wyliczeniowe (w tym wyliczenia o ograniczonym i ograniczonym zakresie)
- Klasa typy (w tym struktury, klasy i związki).
Definiowanie typów zdefiniowanych przez program
Podobnie jak aliasy typów, typy zdefiniowane przez program również muszą zostać zdefiniowane i nadać im nazwę, zanim będą mogły być użyte. Definicja typu zdefiniowanego przez program nazywa się definicją typu.
Kluczowa informacja
Typ zdefiniowany przez program musi mieć nazwę i definicję, zanim będzie można go użyć. Inne typy złożone nie wymagają żadnego z nich.
Funkcje nie są uważane za typy zdefiniowane przez użytkownika (nawet jeśli wymagają nazwy i definicji, zanim będą mogły zostać użyte), ponieważ to sama funkcja otrzymuje nazwę i definicję, a nie typ funkcji. Funkcje, które sami definiujemy, nazywane są funkcjami zdefiniowanymi przez użytkownika.
Chociaż nie omówiliśmy jeszcze, czym jest struktura, oto przykład pokazujący definicję niestandardowego typu Fraction i utworzenie obiektu przy użyciu tego typu:
// Define a program-defined type named Fraction so the compiler understands what a Fraction is
// (we'll explain what a struct is and how to use them later in this chapter)
// This only defines what a Fraction type looks like, it doesn't create one
struct Fraction
{
int numerator {};
int denominator {};
};
// Now we can make use of our Fraction type
int main()
{
Fraction f { 3, 4 }; // this actually instantiates a Fraction object named f
return 0;
}W tym przykładzie używamy słowa kluczowego struct do zdefiniowania nowego typu zdefiniowanego w programie o nazwie Fraction (w zakresie globalnym, więc można użyć w dowolnym miejscu w pozostałej części pliku). Nie przydziela to żadnej pamięci - po prostu informuje kompilator, jak wygląda a Fraction , dzięki czemu możemy później przydzielić obiekty typu a Fraction . Następnie wewnątrz main() tworzymy instancję (i inicjujemy) zmienną typu Fraction nazwany f.
Definicje typów zdefiniowanych w programie muszą kończyć się średnikiem. Brak średnika na końcu definicji typu jest częstym błędem programisty i może być trudny do debugowania, ponieważ kompilator może popełnić błąd w wierszu po definicji typu.
Ostrzeżenie
Nie zapomnij zakończyć definicji typów średnikiem.
W następnej lekcji (13.2 — Wyliczenia bez zakresu) pokażemy więcej przykładów definiowania i używania typów zdefiniowanych przez program, a omówimy rozpoczynające się struktury na lekcji 13.7 — Wprowadzenie do struktur, elementów i wyboru elementów.
Nazywanie typów zdefiniowanych przez program
Zgodnie z konwencją, typy zdefiniowane w programie nazywane są wielką literą i nie używają przyrostka (np. Fraction, nie fraction, fraction_t lub Fraction_t).
Najlepsza praktyka
Nazywaj typy zdefiniowane przez program zaczynając od dużej litery i nie używaj przyrostka.
Nowi programiści czasami uważają definicje zmiennych, takie jak poniższe, za mylące ze względu na podobieństwo między nazwą typu a zmienną nazwa:
Fraction fraction {}; // Instantiates a variable named fraction of type FractionNie różni się to od innych definicji zmiennej: typ (Fraction) jest pierwszy (a ponieważ Ułamek jest pisany wielką literą, wiemy, że jest to typ zdefiniowany w programie), następnie nazwa zmiennej (fraction), a na końcu opcjonalny inicjator. Ponieważ w C++ rozróżniana jest wielkość liter, nie ma tutaj konfliktu nazewnictwa!
Korzystanie z typów zdefiniowanych przez program w wielu plikach. program
Każdy plik kodu, który używa typu zdefiniowanego przez program, musi zobaczyć pełną definicję typu, zanim zostanie użyty. Deklaracja forward nie jest wystarczająca. Jest to wymagane, aby kompilator wiedział, ile pamięci przydzielić dla obiektów tego typu.
Aby propagować definicje typów do plików kodu, które ich potrzebują, typy zdefiniowane przez program są zwykle definiowane w plikach nagłówkowych, a następnie #dołączane do dowolnego pliku kodu, który wymaga tej definicji typu mając taką samą nazwę jak typ zdefiniowany przez program (np. typ zdefiniowany przez program o nazwie Fraction zostałby zdefiniowany w Fraction.h)
Najlepsza praktyka
Typ zdefiniowany przez program używany tylko w jednym pliku kodu powinien być zdefiniowany w tym pliku kodu jak najbliżej pierwszego punktu użycia.
Typ zdefiniowany przez program używany w wielu plikach kodu powinien być zdefiniowany w pliku nagłówkowym o tej samej nazwie, co typ zdefiniowany przez program, a następnie #included do każdego pliku kodu jako potrzebne.
Oto przykład, jak wyglądałby nasz typ ułamkowy, gdybyśmy przenieśli go do pliku nagłówkowego (o nazwie Fraction.h), aby można go było włączyć do wielu plików z kodem:
Fraction.h:
#ifndef FRACTION_H
#define FRACTION_H
// Define a new type named Fraction
// This only defines what a Fraction looks like, it doesn't create one
// Note that this is a full definition, not a forward declaration
struct Fraction
{
int numerator {};
int denominator {};
};
#endifFraction.cpp:
#include "Fraction.h" // include our Fraction definition in this code file
// Now we can make use of our Fraction type
int main()
{
Fraction f{ 3, 4 }; // this actually creates a Fraction object named f
return 0;
}Definicje typów są częściowo wyłączone z reguły jednej definicji (ODR)
W lekcji 2.7 -- Deklaracje przesyłania dalej i definicje, omówiliśmy, jak reguła jednej definicji wymaga, aby każda funkcja i zmienna globalna miała tylko jedną definicję na program. Aby użyć danej funkcji lub zmiennej globalnej w pliku, który nie zawiera definicji, potrzebujemy deklaracji forward (którą zwykle propagujemy poprzez plik nagłówkowy. Działa to, ponieważ deklaracje wystarczą, aby zadowolić kompilator, jeśli chodzi o funkcje i zmienne niebędące constexpr, a linker może wtedy wszystko połączyć). up.
Jednak używanie deklaracji forward w podobny sposób nie działa w przypadku typów, ponieważ kompilator zazwyczaj musi zobaczyć pełną definicję, aby użyć danego typu. Musimy być w stanie propagować pełną definicję typu do każdego pliku kodu, który jej potrzebuje.
Aby to umożliwić, typy są częściowo zwolnione z reguły jednej definicji: dany typ może być zdefiniowany w wielu plikach kodu.
Już to zrobiłeś. skorzystałeś z tej możliwości (prawdopodobnie nie zdając sobie z tego sprawy): jeśli twój program ma dwa pliki z kodem, #include <iostream>, importujesz wszystkie definicje typów wejścia/wyjścia do obu plików.
Istnieją dwa zastrzeżenia, o których warto wiedzieć. Po pierwsze, nadal możesz mieć tylko jedną definicję typu na plik kodu (zwykle nie stanowi to problemu, ponieważ zapobiegają temu zabezpieczenia nagłówka). Po drugie, wszystkie definicje typów dla danego typu muszą być identyczne, w przeciwnym razie nastąpi niezdefiniowane zachowanie.
Nomenklatura: typy zdefiniowane przez użytkownika vs typy zdefiniowane przez program
Termin „typ zdefiniowany przez użytkownika” czasami pojawia się w zwykłych rozmowach, a także jest wspomniany (ale nie zdefiniowany) w standardzie języka C++. W zwykłej rozmowie termin ten zwykle oznacza „typ zdefiniowany w twoich własnych programach” (taki jak powyższy przykład typu ułamkowego).
Standard języka C++ używa terminu „typ zdefiniowany przez użytkownika” w niekonwencjonalny sposób. W standardzie językowym „typ zdefiniowany przez użytkownika” to dowolny typ klasy lub typ wyliczeniowy zdefiniowany przez Ciebie, bibliotekę standardową lub implementację (np. typy zdefiniowane przez kompilator w celu obsługi rozszerzeń języka). Być może wbrew intuicji oznacza to, że std::string (typ klasy zdefiniowany w bibliotece standardowej) jest uważany za typ zdefiniowany przez użytkownika!
Aby zapewnić dodatkowe rozróżnienie, standard języka C++20 w przydatny sposób definiuje termin „typ zdefiniowany przez program” oznaczający typy klas i typy wyliczeniowe, które nie są zdefiniowane jako część standardowej biblioteki, implementacji lub języka podstawowego. Innymi słowy, „typy zdefiniowane przez program” obejmują tylko typy klas i typy wyliczeniowe, które są zdefiniowane przez nas (lub przez bibliotekę strony trzeciej).
W związku z tym, mówiąc tylko o typach klas i typach wyliczeniowych, które definiujemy do użycia w naszych własnych programach, wolimy termin „zdefiniowany w programie”, ponieważ ma on bardziej precyzyjną definicję.
| Typ | Znaczenie | Przykłady |
|---|---|---|
| Podstawowy | Typ podstawowy wbudowany w rdzeń C++ język | int, std::nullptr_t |
| Złożony | Typ zdefiniowany w kategoriach innych typów | int&, double*, std::string, Fraction |
| Zdefiniowany przez użytkownika | Typ klasowy lub typ wyliczeniowy (Zawiera te zdefiniowany w standardowej bibliotece lub implementacji) (W codziennym użyciu, zwykle oznacza typy zdefiniowane w programie) | std::string, Fraction |
| Zdefiniowane w programie | Typ klasowy lub typ wyliczeniowy (Nie obejmuje tych zdefiniowanych w standardowej bibliotece lub implementacja) | Ułamek |

