14.3 — Funkcje składowe

W lekcji 13.7 — Wprowadzenie do struktur, elementów i wyboru elementów wprowadziliśmy struct typ zdefiniowany w programie, który może zawierać zmienne składowe. Oto przykład struktury używanej do przechowywania daty:

struct Date
{
    int year {};
    int month {};
    int day {};
};

Teraz, jeśli chcemy wydrukować datę na ekranie (coś, co prawdopodobnie często chcemy robić), warto napisać funkcję, która to zrobi. Oto pełny program:

#include <iostream>

struct Date
{
    // here are our member variables
    int year {};
    int month {};
    int day {};
};

void print(const Date& date)
{
    // member variables accessed using member selection operator (.)
    std::cout << date.year << '/' << date.month << '/' << date.day;
}

int main()
{
    Date today { 2020, 10, 14 }; // aggregate initialize our struct

    today.day = 16; // member variables accessed using member selection operator (.)
    print(today);   // non-member function accessed using normal calling convention

    return 0;
}

Ten program wypisuje:

2020/10/16

Rozdzielenie właściwości i działań

Rozejrzyj się wokół siebie — wszędzie, gdzie spojrzysz, są przedmioty: książki, budynki, jedzenie, a nawet ty. Rzeczywiste obiekty mają dwa główne elementy: 1) pewną liczbę obserwowalnych właściwości (np. wagę, kolor, rozmiar, solidność, kształt itp.) oraz 2) pewną liczbę działań, które mogą wykonać lub które na nich wykonały (np. otwarcie, uszkodzenie czegoś innego itp.) w oparciu o te właściwości. Te właściwości i akcje są nierozłączne.

W programowaniu reprezentujemy właściwości za pomocą zmiennych, a akcje za pomocą funkcji.

W Date w przykładzie powyżej zwróć uwagę, że zdefiniowaliśmy nasze właściwości (zmienne składowe Date) i akcje, które wykonujemy przy użyciu tych właściwości (funkcja print())) oddzielnie. Pozostaje nam wywnioskować połączenie pomiędzy Date i print() opierając się wyłącznie na const Date& parametrze print().

Chociaż moglibyśmy umieścić oba Date i print() w przestrzeni nazw (aby było jaśniejsze, że te dwa elementy mają być spakowane razem), to dodaje to jeszcze więcej nazw do naszego programu i więcej przedrostków przestrzeni nazw, zaśmiecając nasz kod.

Z pewnością tak byłoby fajnie, gdyby istniał sposób na zdefiniowanie naszych właściwości i akcji razem, jako pojedynczy pakiet.

Funkcje członkowskie

Oprócz zmiennych składowych, typy klas (w tym struktury, klasy i związki) mogą mieć także własne funkcje! Funkcje należące do typu klasy nazywane są funkcjami składowymi.

Na marginesie…

W innych językach obiektowych (takich jak Java i C#) nazywane są metodami. Chociaż termin „metoda” nie jest używany w C++, programiści, którzy najpierw nauczyli się jednego z tych języków, mogą nadal używać tego terminu.

Funkcje, które nie są funkcjami składowymi, nazywane są funkcjami nieskładkowymi (lub czasami funkcjami wolnymi), aby odróżnić je od funkcji składowych. Powyższa print() funkcja nie jest funkcją składową.

Nota autora

W tej lekcji użyjemy struktur, aby pokazać przykłady funkcji składowych — ale wszystko, co tu pokażemy, odnosi się równie dobrze do klas. Z powodów, które staną się oczywiste, gdy już tam dotrzemy, w nadchodzącej lekcji pokażemy przykłady klas z funkcjami składowymi (14.5 -- Członkowie publiczni i prywatni oraz specyfikatory dostępu).

Funkcje składowe muszą być zadeklarowane w definicji typu klasy i można je definiować wewnątrz lub na zewnątrz definicji typu klasy. Dla przypomnienia definicja jest również deklaracją, więc jeśli zdefiniujemy funkcję składową wewnątrz klasy, będzie się to liczyło jako deklaracja.

Aby wszystko było proste, zdefiniujemy nasze funkcje składowe w definicji typu klasy dla teraz.

Powiązana treść

Pokazujemy, jak zdefiniować funkcje składowe poza definicją typu klasy w lekcji 15.2 — Klasy i pliki nagłówkowe.

Przykład funkcji składowej

Przepiszmy Date przykład z góry lekcji, przekształcając print() z funkcji niebędącej składową na funkcję składową:

// Member function version
#include <iostream>

struct Date
{
    int year {};
    int month {};
    int day {};

    void print() // defines a member function named print
    {
        std::cout << year << '/' << month << '/' << day;
    }
};

int main()
{
    Date today { 2020, 10, 14 }; // aggregate initialize our struct

    today.day = 16; // member variables accessed using member selection operator (.)
    today.print();  // member functions also accessed using member selection operator (.)

    return 0;
}

Ten program kompiluje i daje taki sam wynik jak powyżej:

2020/10/16

Istnieją trzy kluczowe różnice między przykładami niebędącymi członkami i członkami:

  1. Gdzie deklarujemy (i definiujemy) print() funkcja
  2. Jak wywołujemy print() funkcja
  3. Jak uzyskujemy dostęp do członków wewnątrz print() funkcja

Przyjrzyjmy się każdemu z nich po kolei.

Funkcje składowe są deklarowane wewnątrz typu klasy definicja

W przykładzie nieczłonkowym, print() funkcja nieczłonkowa jest zdefiniowana poza Date struct, w globalnej przestrzeni nazw. Domyślnie ma zewnętrzne powiązanie, więc można ją wywołać z innych plików źródłowych (z odpowiednią deklaracją forward).

W przykładzie elementu print() funkcja członkowska jest zadeklarowana (i w tym przypadku zdefiniowana) wewnątrz definicji struktury Date . Ponieważ print() jest zadeklarowany jako część Date, mówi to kompilatorowi, że print() jest funkcją składową.

Funkcje składowe zdefiniowane w definicji typu klasy są domyślnie wbudowane, więc nie spowodują naruszenia reguły jednej definicji, jeśli definicja typu klasy jest zawarta w wielu plikach kodu.

Powiązana treść

Funkcje składowe można również (do przodu) zadeklarować w definicji klasy, i zdefiniowane po definicji klasy. Omówimy to w lekcji 15.2 — Klasy i pliki nagłówkowe.

Wywoływanie funkcji składowych (i ukrytego obiektu)

W przykładzie niebędącym elementem składowym wywołanie print(today), gdzie today jest (jawnie) przekazywane jako argument.

W przykładzie składowym wywołujemy today.print(). Ta składnia, która wykorzystuje operator wyboru elementu członkowskiego (.) do wyboru funkcji składowej do wywołania, jest spójna ze sposobem, w jaki uzyskujemy dostęp do zmiennych składowych (np. today.day = 16;).

Wszystkie (niestatyczne) funkcje składowe muszą być wywoływane przy użyciu obiektu tego typu klasy. W tym przypadku today jest obiektem, który print() jest wywoływany.

Zauważ, że w przypadku funkcji składowej nie potrzebujemy przekazać today jako argument. Obiekt, dla którego wywoływana jest funkcja składowa, jest niejawnie przekazywany do funkcji członkowskiej. Z tego powodu obiekt, dla którego wywoływana jest funkcja członkowska, jest często nazywany obiektem ukrytym.

Innymi słowy, gdy wywołujemy today.print(), today jest obiektem ukrytym i tak jest pośrednio przekazywany do print() funkcja składowa.

Powiązana treść

Mechanikę tego, w jaki sposób skojarzony obiekt jest faktycznie przekazywany do funkcji składowej, omawiamy w lekcji 15.1 — Ukryty „ten” wskaźnik i łączenie funkcji składowych.

Dostęp do elementów wewnątrz funkcji składowej wykorzystuje obiekt ukryty

Oto wersja niebędąca składową print() ponownie:

// non-member version of print
void print(const Date& date)
{
    // member variables accessed using member selection operator (.)
    std::cout << date.year << '/' << date.month << '/' << date.day;
}

Ta wersja print() ma odniesienie parametr const Date& date. W ramach funkcji uzyskujemy dostęp do elementów poprzez ten parametr referencyjny, ponieważ date.year, date.month, I date.day. Podczas oceny print(today) , wykonywana jest instrukcja date parametr referencyjny jest powiązany z argumentem today, I date.year, date.month, I date.day oceną today.year, today.month, I today.day .

. Teraz spójrzmy ponownie na definicję print() funkcji członkowskiej:

    void print() // defines a member function named print()
    {
        std::cout << year << '/' << month << '/' << day;
    }

W przykładzie elementu uzyskujemy dostęp do elementów jako year, month, i day.

Wewnątrz funkcją składową, z obiektem ukrytym powiązany jest dowolny identyfikator elementu członkowskiego, który nie jest poprzedzony operatorem wyboru elementu członkowskiego (.).

Innymi słowy, gdy today.print() today jest naszym ukrytym obiektem i year, month, I day (które nie są poprzedzone) ocenia do wartości today.year, today.month, I today.day .

Kluczowa informacja

W przypadku funkcji niebędących składowymi musimy jawnie przekazać obiekt do funkcji, z którą ma pracować, a elementy członkowskie są jawnie do którego można uzyskać dostęp za pośrednictwem tego obiektu.

W przypadku funkcji składowych niejawnie przekazujemy obiekt do funkcji, z którą należy pracować, a dostęp do elementów uzyskuje się poprzez ten obiekt.

Inny przykład funkcji składowej

Oto przykład z nieco bardziej złożoną funkcją składową:

#include <iostream>
#include <string>

struct Person
{
    std::string name{};
    int age{};

    void kisses(const Person& person)
    {
        std::cout << name << " kisses " << person.name << '\n';
    }
};

int main()
{
    Person joe{ "Joe", 29 };
    Person kate{ "Kate", 27 };

    joe.kisses(kate);

    return 0;
}

To daje wynik:

Joe kisses Kate

Przyjrzyjmy się, jak to działa.Najpierw definiujemy dwie Person struktury, joe i kate. Następnie wywołujemy joe.kisses(kate). joe jest tutaj ukryty obiekt i kate jest przekazywany jako jawny argument.

Gdy kisses() funkcja składowa jest wykonywana, identyfikator name nie używa operatora wyboru składowej (.), więc odwołuje się do ukrytego obiektu, który is joe. Zatem sprowadza się to do joe.name. person.name użycia operatora wyboru elementu członkowskiego, więc nie odnosi się do obiektu ukrytego. Ponieważ person jest referencją dla kate, oznacza to kate.name.

Kluczowa informacja

Bez funkcji składowej napisalibyśmy kisses(joe, kate) z funkcją składową, zapisalibyśmy joe.kisses(kate). Zwróć uwagę, o ile lepiej czyta to drugie i jak dokładnie wyjaśnia, który obiekt inicjuje akcję, a który wspiera.

Zmienne składowe i funkcje można definiować w dowolnej kolejności

Kompilator C++ zwykle kompiluje kod od góry do dołu. Dla każdej napotkanej nazwy kompilator określa, czy widział już deklarację tej nazwy, aby mógł poprawnie sprawdzić typ.

Elementy niebędące członkami muszą zostać zadeklarowane, zanim będzie można ich użyć, w przeciwnym razie kompilator będzie narzekał:

int x()
{
    return y(); // error: y not declared yet, so compiler doesn't know what it is
}
 
int y()
{
    return 5;
}

Aby rozwiązać ten problem, zazwyczaj albo definiujemy nasze elementy niebędące członkami w przybliżonej kolejności użycia (co wymaga pracy za każdym razem, gdy musimy zmienić kolejność) lub używamy deklaracji forward (co wymaga pracy add).

Jednakże w definicji klasy to ograniczenie nie ma zastosowania: możesz uzyskać dostęp do zmiennych składowych i funkcji składowych przed ich zadeklarowaniem. Oznacza to, że możesz definiować zmienne składowe i funkcje składowe w dowolnej kolejności!

Na przykład:

struct Foo
{
    int z() { return m_data; } // We can access data members before they are defined
    int x() { return y(); }    // We can access member functions before they are defined

    int m_data { y() };        // This even works in default member initializers (see warning below)
    int y() { return 5; }
};

Zalecaną kolejność definicji prętów omówimy w nadchodzącej lekcji 14.8 — Korzyści z ukrywania danych (hermetyzowania).

Ostrzeżenie

Elementy danych są inicjowane w kolejności deklaracji. Jeśli inicjalizacja elementu danych uzyskuje dostęp do innego elementu danych, który został zadeklarowany dopiero później (a zatem nie został jeszcze zainicjowany), inicjalizacja spowoduje niezdefiniowane zachowanie.

struct Bad
{
    int m_bad1 { m_data }; // undefined behavior: m_bad1 initialized before m_data
    int m_bad2 { fcn() };  // undefined behavior: m_bad2 initialized before m_data (accessed through fcn())

    int m_data { 5 };
    int fcn() { return m_data; }
};

Z tego powodu ogólnie dobrym pomysłem jest unikanie używania innych elementów wewnątrz domyślnych inicjatorów elementów.

Dla zaawansowanych czytelników

Aby umożliwić definiowanie elementów danych i funkcji składowych w dowolnej kolejności, kompilatory stosują sprytną sztuczkę. Kiedy kompilator napotka funkcję członkowską zdefiniowaną w definicji klasy:

  • Funkcja członkowska jest domyślnie zadeklarowana w przód.
  • Definicja funkcji składowej jest przenoszona natychmiast po zakończeniu definicji klasy.

W ten sposób, zanim kompilator faktycznie skompiluje definicje funkcji składowych, widział już pełną definicję klasy (zawierającą deklaracje dla wszystkich składowych!)

Na przykład, gdy kompilator napotyka następujący problem:

struct Foo
{
    int z() { return m_data; } // m_data not declared yet
    int x() { return y(); }    // y not declared yet
    int y() { return 5; }

    int m_data{};
};

Skompiluje odpowiednik tego:

struct Foo
{
    int z(); // forward declaration of Foo::z()
    int x(); // forward declaration of Foo::x()
    int y(); // forward declaration of Foo::y()

    int m_data{};
};

int Foo::z() { return m_data; } // m_data already declared above
int Foo::x() { return y(); }    // y already declared above
int Foo::y() { return 5; }

Funkcje składowe mogą zostać przeciążone

Podobnie jak funkcje nieelementowe, funkcje składowe mogą zostać przeciążone, o ile każda funkcja składowa może być zróżnicowana.

Powiązana treść

Zróżnicowanie przeciążenia funkcji omówimy na lekcji 11.2 -- Różnicowanie przeciążenia funkcji.

Oto przykład a Date struktura z przeciążonymi print() funkcjami składowymi:

#include <iostream>
#include <string_view>

struct Date
{
    int year {};
    int month {};
    int day {};

    void print()
    {
        std::cout << year << '/' << month << '/' << day;
    }

    void print(std::string_view prefix)
    {
        std::cout << prefix << year << '/' << month << '/' << day;
    }
};

int main()
{
    Date today { 2020, 10, 14 };

    today.print(); // calls Date::print()
    std::cout << '\n';

    today.print("The date is: "); // calls Date::print(std::string_view)
    std::cout << '\n';

    return 0;
}

Wypisuje:

2020/10/14
The date is: 2020/10/14

Struktury i funkcje składowe

W C struktury mają tylko elementy danych, a nie funkcje składowe.

W C++ podczas projektowania klas Bjarne Stroustrup spędził trochę czasu rozważając, czy strukturom (odziedziczonym z C) należy przyznać możliwość mają funkcje członkowskie. Po namyśle stwierdził, że powinni.

Na marginesie…

Ta decyzja wywołała kaskadę innych pytań o to, do czego inne nowe struktury C++ powinny mieć dostęp. Bjarne obawiał się, że zapewnienie strukturom dostępu do ograniczonego podzbioru możliwości doprowadzi do zwiększenia złożoności i skrajnych przypadków do języka. Dla uproszczenia ostatecznie zdecydował, że struktury i klasy będą miały ujednolicony zestaw reguł (co oznacza, że ​​struktury mogą robić wszystko, co klasy i odwrotnie), a konwencja może dyktować, w jaki sposób struktury będą faktycznie używane.

We współczesnym C++ dobrze jest, jeśli struktury mają funkcje składowe. Nie obejmuje to konstruktorów, które są specjalnym typem funkcji składowych, o którym mowa w nadchodzącej lekcji 14.9 -- Wprowadzenie do konstruktorów. Typ klasy z konstruktorem nie jest już agregatem i chcemy, aby nasze struktury pozostały agregatami.

Najlepsza praktyka

Funkcji składowych można używać zarówno ze strukturami, jak i klasami.

Jednak w strukturach należy unikać definiowania funkcji składowych konstruktora, ponieważ czyni to je nieagregacją.

Typy klas bez elementów danych

Możliwe jest tworzenie typów klas bez danych członkowie (np. typy klas, które mają tylko funkcje członkowskie). Możliwe jest także tworzenie instancji obiektów typu:

#include <iostream>

struct Foo
{
    void printHi() { std::cout << "Hi!\n"; }
};

int main()
{
    Foo f{};
    f.printHi(); // requires object to call

    return 0;
}

Jeśli jednak typ klasy nie ma żadnych elementów danych, wówczas użycie typu klasy jest prawdopodobnie przesadą. W takich przypadkach rozważ zamiast tego użycie przestrzeni nazw (zawierającej funkcje niebędące składowymi). Dzięki temu czytelnik staje się jaśniejszy, że nie są zarządzane żadne dane (i nie wymaga tworzenia instancji obiektu w celu wywołania funkcji).

#include <iostream>

namespace Foo
{
    void printHi() { std::cout << "Hi!\n"; }
};

int main()
{
    Foo::printHi(); // no object needed

    return 0;
}

Najlepsza praktyka

Jeśli typ klasy nie ma elementów danych, preferuj użycie przestrzeni nazw.

Czas quizu

Pytanie nr 1

Utwórz strukturę o nazwie IntPair , która przechowuje dwie liczby całkowite. Dodaj funkcję składową o nazwie print , która wypisuje wartość dwóch liczb całkowitych.

Następująca funkcja programu powinna się skompilować:

#include <iostream>

// Provide the definition for IntPair and the print() member function here

int main()
{
	IntPair p1 {1, 2};
	IntPair p2 {3, 4};

	std::cout << "p1: ";
	p1.print();

	std::cout << "p2: ";
	p2.print();

	return 0;
}

i wygenerować wynik:

p1: Pair(1, 2)
p2: Pair(3, 4)

Pokaż rozwiązanie

Pytanie nr 2

Dodaj nową funkcję składową do IntPair nazwany isEqual , która zwraca wartość logiczną wskazującą, czy jedna IntPair jest równa drugiej.

Następująca funkcja programu powinna się skompilować:

#include <iostream>

// Provide the definition for IntPair and the member functions here

int main()
{
	IntPair p1 {1, 2};
	IntPair p2 {3, 4};

	std::cout << "p1: ";
	p1.print();

	std::cout << "p2: ";
	p2.print();

	std::cout << "p1 and p1 " << (p1.isEqual(p1) ? "are equal\n" : "are not equal\n");
	std::cout << "p1 and p2 " << (p1.isEqual(p2) ? "are equal\n" : "are not equal\n");

	return 0;
}

i wygenerować wynik:

p1: Pair(1, 2)
p2: Pair(3, 4)
p1 and p1 are equal
p1 and p2 are not equal 

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
477 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze