Struktury w C++

Struktury w C++

Tablica to kolekcja elementów tego samego typu. Natomiast najprostsza struktura to kolekcja elementów dowolnego typu. Na przykład:

struct Address {
    const char*name; // "Jim Dandy"
    int number; // 61
    const char*street; // "South St"
    const char*town; // "New Providence"
    char state[2]; // 'N' 'J'
    const char*zip; // "07974"
};

Jest to definicja typu o nazwie Address zawierającego elementy potrzebne do przechowywania informacji umożliwiających wysyłanie poczty do osoby mieszkającej w USA. Zwróć uwagę na znajdujący się na końcu średnik. Zmienne typu Address można deklarować dokładnie tak samo jak inne zmienne, a dostęp do składowych można uzyskać przy użyciu operatora kropki, np.:

void f()
{
    Address jd;
    jd.name = "Jim Dandy";
    jd.number = 61;
}

Zmienne typu strukturalnego można inicjować przy użyciu notacji {} (6.3.5), np.:

Address jd = {
    "Jim Dandy",
    61, "South St",
    "New Providence",
    {'N','J'}, "07974"
};

Należy zauważyć, że składowej jd.state nie można zainicjować łańcuchem "NJ". Koniec łańcucha jest oznaczany znakiem '', w związku z czym łańcuch "NJ" tak naprawdę zawiera trzy znaki, a więc o jeden za dużo jak dla składowej jd.state. Celowo użyłem niskopoziomowych typów dla składowych, aby pokazać, jak się ich używa, oraz aby wskazać, jakie problemy mogą wyniknąć z ich używania. Dostęp do struktur często uzyskuje się poprzez wskaźniki, przy użyciu operatora -> (dereferencji wskaźnika struktury). Na przykład:

void print_addr(Address*p)
{
    cout << p−>name << ' '
    << p−>number << ' ' << p−>street << ' '
    << p−>town << ' '
    << p−>state[0] << p−>state[1] << ' ' << p−>zip << ' ';
}

Gdy p jest wskaźnikiem, zapis p->m jest równoważny z (*p).m. Strukturę można też przekazać przez referencję i wówczas dostęp do jej składowych uzyskuje się przy użyciu operatora . (dostępu do składowych struktury):

void print_addr2(const Address& r)
{
    cout << r.name << ' '
        << r.number << ' ' << r.street << ' '
        << r.town << ' '
        << r.state[0] << r.state[1] << ' ' << r.zip << ' ';
}

Obiektom typów strukturalnych można przypisywać wartości oraz można je przekazywać jako argumenty funkcji i zwracać jako wynik działania funkcji. Na przykład: 

Address current;
Address set_current(Address next)
{
    address prev = current;
    current = next;
    return prev;
}

Inne operacje, które mogłyby być przydatne, takie jak porównywanie (== i !=), nie są domyślnie dostępne. Jednakże użytkownik może sam zdefiniować potrzebne mu operatory.

Układ struktur

Składowe w obiekcie strukturalnym są przechowywane w kolejności deklaracji. Na przykład odczyt prostej aparatury można przechowywać w następującej strukturze:

struct Readout {
    char hour; // [0:23]
    int value;
    char seq; // znak oznaczający sekwencję ['a':'z']
};

Składowe obiektu Readout w pamięci można sobie wyobrazić w następujący sposób:

Składowe są zapisywane w pamięci w porządku odpowiadającym kolejności deklaracji, więc adres składowej hour musi być mniejszy od adresu value. Jednak rozmiar obiektu strukturalnego nie musi być równy sumie rozmiarów jego składowych. Jest to spowodowane tym, że wiele komputerów ma określone wymagania architekturalne dotyczące sposobu alokacji różnych typów obiektów, dzięki czemu mogą znacznie wydajniej pracować. Na przykład liczby całkowite mogą być alokowane w granicach słów i wówczas mówi się, że obiekty w określonym typie komputera są właściwie wyrównane (ang. properly aligned). To sprawia, że w strukturach mogą powstawać dziury. Bardziej realny sposób rozmieszczenia składowych obiektu typu Readout w komputerze z 4-bajtowym typem int wygląda tak:

W tym przypadku operacja sizeof(Readout) na wielu komputerach zwróci wartość 12, a nie 6, czego można by było się spodziewać, tylko dodając rozmiary poszczególnych składowych. Marnotrawstwo pamięci można zminimalizować, ustawiając składowe od największej do najmniejszej. Na przykład:

struct Readout {
    int value;
    char hour; // [0:23]
    char seq; // znak oznaczający sekwencję ['a':'z']
};

Teraz otrzymamy coś takiego:


Nadal pozostaje dwubajtowa dziura nieużywanej pamięci, ale teraz sizeof(Readout) wynosi 8. Jest tak dzięki temu, że gdy dwa obiekty znajdują się obok siebie, muszą być prawidłowo wyrównane w pamięci. Rozmiar tablicy zawierającej 10 obiektów typu Readout wynosiłby 10*sizeof(Readout). Zazwyczaj najlepiej jest porządkować składowe tak, aby struktura była jak najbardziej czytelna, a sortowanie wg rozmiaru stosować tylko wówczas, gdy konieczna jest optymalizacja. Na układ mogą mieć też wpływ specyfikatory dostępu (np. public, private i protected).

Nazwy struktur

Nazwa typu staje się dostępna do użytku natychmiast po jej wystąpieniu, a nie dopiero za całą
deklaracją struktury. Na przykład:

struct Link {
    Link*previous;
    Link*successor;
};

Jednak obiekty struktury można deklarować dopiero po zakończeniu jej deklaracji. Na przykład:

struct No_good {
    No_good member; // błąd: rekurencyjna definicja
};

Jest to błąd, bo kompilator nie może określić rozmiaru typu No_good. Aby dwie struktury (lub więcej) mogły odnosić się do siebie wzajemnie, można zadeklarować tylko nazwę struktury. Na przykład:

struct List; // deklaracja nazwy struktury List, która zostanie zdefiniowana później
struct Link {
    Link*pre;
    Link*suc;
    List*member_of;
    int data;
};
struct List {
    Link*head;
};

Gdyby nie było pierwszej deklaracji struktury List, użycie wskaźnika typu List* w deklaracji struktury Link powodowałoby błąd składniowy. Nazwy struktury można użyć, zanim sam typ zostanie zdefiniowany, pod warunkiem że nie zostanie użyta składowa tego typu albo nie będzie potrzebny jego rozmiar. Jednak dopóki struktura nie zostanie w całości zadeklarowana, jest ona niekompletnym typem. Na przykład:

struct S; // S jest nazwą jakiegoś typu
extern S a;
S f();
void g(S);
S*h(S*);

Wielu z takich deklaracji nie można użyć, dopóki typ S nie zostanie zdefiniowany:

void k(S*p)
{
    Sa; // błąd: nie zdefiniowano S; do alokacji potrzebny jest rozmiar
    f(); // błąd: nie zdefiniowano S; aby zwrócić wartość, potrzebny jest rozmiar
    g(a); // błąd: nie zdefiniowano S; aby przekazać argument, potrzebny jest rozmiar
    p−>m = 7; // błąd: nie zdefiniowano S; nieznana nazwa składowej
    S*q = h(p); // OK: wskaźniki można alokować i przekazywać
    q−>m = 7; // błąd: nie zdefiniowano S; nieznana nazwa składowej
}

Z powodów mających związek jeszcze z prehistorią języka C istnieje możliwość zadeklarowania struktury i innej konstrukcji o tej samej nazwie w tym samym zakresie. Na przykład:

struct stat { /*...*/ };
int stat(char*name, struct stat*buf);

W tym kodzie zwykła nazwa stat dotyczy konstrukcji nie będącej strukturą i dlatego do struktury trzeba się odnosić przy użyciu przedrostka struct. Także słów kluczowych class, union i enum można używać w celu wyeliminowania niejednoznaczności. Najlepiej jednak nadawać konstrukcjom takie nazwy, aby w ogóle nie było trzeba korzystać z tej techniki.

Struktury a klasy

Struktura to tak naprawdę po prostu klasa, której składowe są domyślnie publiczne. To oznacza, że struktury mogą też zawierać funkcje składowe. Szczególnie ważne jest to, że struktura może mieć konstruktory. Na przykład:

struct Points {
    vector<Point> elem; // musi zawierać przynajmniej jeden obiekt Point
    Points(Point p0) { elem.push_back(p0);}
    Points(Point p0, Point p1) { elem.push_back(p0); elem.push_back(p1); }
    //...
};
Points x0; // błąd: brak konstruktora domyślnego
Points x1{ {100,200} }; // jeden obiekt Point
Points x1{ {100,200}, {300,400} }; // dwa obiekty Point

Nie trzeba definiować konstruktora tylko po to, aby po kolei zainicjować składowe. Na przykład:

struct Point {
    int x, y;
};
Point p0; // niebezpieczeństwo: niezainicjowany, jeśli w zakresie lokalnym (6.3.5.1)
Point p1 {}; // domyślna konstrukcja: {{},{}}; tzn. {0.0}
Point p2 {1}; // druga składowa jest tworzona domyślnie: {1,{}}; tzn. {1,0}
Point p3 {1,2}; // {1,2}

Konstruktor jest potrzebny, gdy trzeba zmienić kolejność lub sprawdzić poprawność argumentów albo je zmodyfikować bądź ustanowić niezmienniki (2.4.3.2, 13.4) itd. Na przykład:

struct Address {
    string name; // "Jim Dandy"
    int number; // 61
    string street; // "South St"
    string town; // "New Providence"
    char state[2]; // 'N' 'J'
    char zip[5]; // 07974
    Address(const string n, int nu, const string& s, const string& t, const string& st, int z);
};

Dodałem konstruktor, aby mieć pewność, że każda składowa zostanie zainicjowana, oraz aby móc używać typów string i int dla kodu pocztowego, zamiast kombinować z pojedynczymi znakami. Na przykład:

Address jd = {
    "Jim Dandy",
    61, "South St",
    "New Providence",
    "NJ", 7974 // (07974 oznaczałoby wartość ósemkową)
};

Konstruktor Address może mieć następującą definicję:

Address::Address(const string& n, int nu, const string& s, const string& t, const
string& st, int z)
// weryfikacja kodu pocztowego
    :name{n},
    number{nu},
    street{s},
    town{t}
{
    if (st.size()!=2)
    error("Skrót nazwy stanu powinien być dwuliterowy")
    state = {st[0],st[1]}; // zapisanie kodu pocztowego jako znaków
    ostringstream ost; // strumień wyjściowy łańcuchów; zobacz 38.4.2
    ost << z; // wydobycie znaków z int
    string zi {ost.str()};
    switch (zi.size()) {
    case 5:
        zip = {zi[0], zi[1], zi[2], zi[3], zi[4]};
        break;
    case 4: // zaczyna się od '0'
        zip = {'0', zi[0], zi[1], zi[2], zi[3]};
        break;
    default:
        error("Niespodziewany format kodu pocztowego");
    }
    //... Sprawdzenie, czy kod jest sensowny...
}

 Język C++. Kompendium wiedzy. Wydanie IV, Autor: Bjarne Stroustrup, Wydawnictwo: Helion 

Podobne artykuły

Podziel się ze znajomymi tym artykułem - udostępnij na FB lub wyślij e-maila korzystając z poniższych opcji:

wszystkie oferty