14.14 — Wprowadzenie do kopiowania konstruktor

Rozważ następujący program:

#include <iostream>
 
class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };
 
public:
    // Default constructor
    Fraction(int numerator=0, int denominator=1)
        : m_numerator{numerator}, m_denominator{denominator}
    {
    }

    void print() const
    {
        std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
    }
};

int main()
{
    Fraction f { 5, 3 };  // Calls Fraction(int, int) constructor
    Fraction fCopy { f }; // What constructor is used here?

    f.print();
    fCopy.print();

    return 0;
}

Możesz być zaskoczony, gdy zauważysz, że ten program kompiluje się dobrze i daje wynik:

Fraction(5, 3)
Fraction(5, 3)

Przyjrzyjmy się bliżej, jak działa ten program.

Inicjalizacja zmienna f jest po prostu standardową inicjalizacją nawiasów klamrowych, która wywołuje Fraction(int, int) konstruktor.

Ale co z następną linią? Inicjalizacja zmiennej fCopy jest również w oczywisty sposób inicjalizacją, a wiadomo, że do inicjowania klas służą funkcje konstruktora. Jaki więc konstruktor wywołuje ta linia?

Odpowiedź brzmi: konstruktor kopiujący.

Konstruktor kopiujący

A konstruktor kopiujący jest konstruktorem używanym do inicjowania obiektu istniejącym obiektem tego samego typu. Po wykonaniu konstruktora kopiującego nowo utworzony obiekt powinien być kopią obiektu przekazanego jako inicjator.

Niejawny konstruktor kopiujący

Jeśli nie podasz konstruktora kopiującego dla swoich klas, C++ utworzy dla ciebie publiczny niejawny konstruktor kopiujący . W powyższym przykładzie instrukcja Fraction fCopy { f }; wywołuje niejawny konstruktor kopiujący w celu zainicjowania fCopy z f.

Domyślnie niejawny konstruktor kopiujący wykona inicjalizację członkowską. Oznacza to, że każdy członek zostanie zainicjowany przy użyciu odpowiedniego członka klasy przekazanego jako inicjator. W powyższym przykładzie fCopy.m_numerator jest inicjowany za pomocą f.m_numerator (która ma wartość 5) i fCopy.m_denominator jest inicjowany za pomocą f.m_denominator (która ma wartość 3).

Po wykonaniu konstruktora kopiującego elementy f i fCopy mają te same wartości, więc fCopy jest kopią f. Zatem wywołanie print() obu z nich daje ten sam rezultat.

Definiowanie własnego konstruktora kopiującego

Możemy również jawnie zdefiniować nasz własny konstruktor kopiujący. W tej lekcji sprawimy, że nasz konstruktor kopiujący wydrukuje komunikat, abyśmy mogli pokazać, że rzeczywiście jest wykonywany podczas tworzenia kopii.

Konstruktor kopiujący wygląda tak, jak można się spodziewać:

#include <iostream>
 
class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };
 
public:
    // Default constructor
    Fraction(int numerator=0, int denominator=1)
        : m_numerator{numerator}, m_denominator{denominator}
    {
    }

    // Copy constructor
    Fraction(const Fraction& fraction)
        // Initialize our members using the corresponding member of the parameter
        : m_numerator{ fraction.m_numerator }
        , m_denominator{ fraction.m_denominator }
    {
        std::cout << "Copy constructor called\n"; // just to prove it works
    }

    void print() const
    {
        std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
    }
};

int main()
{
    Fraction f { 5, 3 };  // Calls Fraction(int, int) constructor
    Fraction fCopy { f }; // Calls Fraction(const Fraction&) copy constructor

    f.print();
    fCopy.print();

    return 0;
}

Po uruchomieniu tego programu otrzymasz:

Copy constructor called
Fraction(5, 3)
Fraction(5, 3)

Konstruktor kopiujący, który zdefiniowaliśmy powyżej, jest funkcjonalnie równoważny temu, który otrzymamy domyślnie, z tą różnicą, że dodaliśmy instrukcję wyjściową w celu potwierdzenia kopii konstruktor jest faktycznie wywoływany. Ten konstruktor kopiujący jest wywoływany podczas fCopy inicjowania za pomocą f.

Przypomnienie

Kontrola dostępu działa w oparciu o klasę (a nie obiekt). Oznacza to, że funkcje składowe klasy mogą uzyskać dostęp do prywatnych elementów dowolnego obiektu klasy tego samego typu (nie tylko do obiektu ukrytego).

Wykorzystujemy to na naszą korzyść w Fraction konstruktorze kopiującym powyżej, aby uzyskać bezpośredni dostęp do prywatnych elementów parametru fraction . W przeciwnym razie nie mielibyśmy możliwości bezpośredniego dostępu do tych elementów (bez dodawania funkcji dostępu, czego moglibyśmy nie chcieć).

Konstruktor kopiujący nie powinien robić nic innego poza kopiowaniem obiektu. Dzieje się tak, ponieważ w niektórych przypadkach kompilator może zoptymalizować konstruktor kopiujący. Jeśli polegasz na konstruktorze kopiującym w przypadku zachowania innego niż kopiowanie, zachowanie to może wystąpić lub nie. Omówimy to w dalszej części lekcji 14.15 — Inicjalizacja klasy i elizja kopiowania.

Najlepsza praktyka

Konstruktory kopiujące nie powinny powodować żadnych skutków ubocznych poza kopiowaniem.

Preferuj niejawny konstruktor kopiujący

W przeciwieństwie do niejawnego konstruktora domyślnego, który nic nie robi (a zatem rzadko jest tym, czego chcemy), inicjalizacja członowa wykonywana przez niejawny konstruktor kopiujący jest zwykle dokładnie tym, czego chcemy. Dlatego w większości przypadków użycie niejawnego konstruktora kopiującego jest w zupełności wystarczające.

Najlepsza praktyka

Preferuj niejawny konstruktor kopiujący, chyba że masz konkretny powód, aby utworzyć własny.

Przypadki, w których konstruktor kopiujący będzie musiał zostać nadpisany, kiedy będziemy omawiać dynamiczną alokację pamięci (21.13 -- Kopiowanie płytkie a głębokie ).

Parametr konstruktora kopiującego musi mieć wartość odniesienie

Wymagane jest, aby parametr konstruktora kopiującego był odwołaniem do lwartości lub odwołaniem do stałej wartości. Ponieważ konstruktor kopiujący nie powinien modyfikować parametru, preferowane jest użycie odniesienia do stałej lwartości.

Najlepsza praktyka

Jeśli napiszesz własny konstruktor kopiujący, parametr powinien być odwołaniem do stałej wartości.

Przekaż wartość i konstruktor kopiujący

Gdy obiekt jest przekazywany przez wartość, argument jest kopiowany do parametru. Gdy argument i parametr są tego samego typu klasy, kopia jest wykonywana poprzez niejawne wywołanie konstruktora kopiującego.

Ilustruje to następujący przykład:

#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // Default constructor
    Fraction(int numerator = 0, int denominator = 1)
        : m_numerator{ numerator }, m_denominator{ denominator }
    {
    }

    // Copy constructor
    Fraction(const Fraction& fraction)
        : m_numerator{ fraction.m_numerator }
        , m_denominator{ fraction.m_denominator }
    {
        std::cout << "Copy constructor called\n";
    }

    void print() const
    {
        std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
    }
};

void printFraction(Fraction f) // f is pass by value
{
    f.print();
}

int main()
{
    Fraction f{ 5, 3 };

    printFraction(f); // f is copied into the function parameter using copy constructor

    return 0;
}

Na komputerze autora ten przykład wypisuje:

Copy constructor called
Fraction(5, 3)

W powyższym przykładzie wywołanie printFraction(f) przekazuje f przez wartość. Konstruktor kopiujący jest wywoływany w celu copy f z main do f parametru funkcji printFraction().

Return by value, a konstruktor kopiujący

W lekcji >2.5 — Wprowadzenie do zakresu lokalnego. Zauważyliśmy, że return by value tworzy obiekt tymczasowy (przetrzymujący kopię wartości zwracanej), który jest przekazywany z powrotem do obiektu wywołującego. Gdy typ zwracany i wartość zwracana są tego samego typu klasy, obiekt tymczasowy jest inicjowany przez niejawne wywołanie konstruktora kopiującego.

Na przykład:

#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // Default constructor
    Fraction(int numerator = 0, int denominator = 1)
        : m_numerator{ numerator }, m_denominator{ denominator }
    {
    }

    // Copy constructor
    Fraction(const Fraction& fraction)
        : m_numerator{ fraction.m_numerator }
        , m_denominator{ fraction.m_denominator }
    {
        std::cout << "Copy constructor called\n";
    }

    void print() const
    {
        std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
    }
};

void printFraction(Fraction f) // f is pass by value
{
    f.print();
}

Fraction generateFraction(int n, int d)
{
    Fraction f{ n, d };
    return f;
}

int main()
{
    Fraction f2 { generateFraction(1, 2) }; // Fraction is returned using copy constructor

    printFraction(f2); // f2 is copied into the function parameter using copy constructor

    return 0;
}

Po wywołaniu generateFraction zwraca a Fraction wróć do main, tworzony jest obiekt tymczasowy Fraction i inicjowany przy użyciu konstruktora kopiującego.

Ponieważ ten tymczasowy jest używany do inicjalizacji Fraction f2, powoduje to ponowne wywołanie konstruktora kopiującego w celu skopiowania tymczasowe do f2.

A kiedy f2 jest przekazywane do printFraction(), konstruktor kopiujący wywoływany jest po raz trzeci.

Tak więc na maszynie autora ten przykład wypisuje:

Copy constructor called
Copy constructor called
Copy constructor called
Fraction(1, 2)

Jeśli skompilujesz i wykonasz powyższy przykład, może się okazać, że wystąpią tylko dwa wywołania konstruktora kopiującego. Jest to optymalizacja kompilatora znana jako elision kopiowania. Elizję kopii omawiamy w dalszej części lekcji 14.15 — Inicjalizacja klasy i elizja kopiowania.

Użycie = default aby wygenerować domyślny konstruktor kopiujący

Jeśli klasa nie ma konstruktora kopiującego, kompilator domyślnie go dla nas wygeneruje. Jeśli wolimy, możemy jawnie poprosić kompilator o utworzenie dla nas domyślnego konstruktora kopiującego, używając = default składnia:

#include <iostream>
 
class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };
 
public:
    // Default constructor
    Fraction(int numerator=0, int denominator=1)
        : m_numerator{numerator}, m_denominator{denominator}
    {
    }

    // Explicitly request default copy constructor
    Fraction(const Fraction& fraction) = default;

    void print() const
    {
        std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
    }
};

int main()
{
    Fraction f { 5, 3 };
    Fraction fCopy { f };

    f.print();
    fCopy.print();

    return 0;
}

Użycie = delete aby zapobiec kopiowaniu

Czasami napotykamy przypadki, w których nie chcemy, aby obiekty określonej klasy można było kopiować. Możemy temu zapobiec, oznaczając funkcję konstruktora kopiującego jako usuniętą, używając = delete składnia:

#include <iostream>
 
class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };
 
public:
    // Default constructor
    Fraction(int numerator=0, int denominator=1)
        : m_numerator{numerator}, m_denominator{denominator}
    {
    }

    // Delete the copy constructor so no copies can be made
    Fraction(const Fraction& fraction) = delete;

    void print() const
    {
        std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
    }
};

int main()
{
    Fraction f { 5, 3 };
    Fraction fCopy { f }; // compile error: copy constructor has been deleted

    return 0;
}

W przykładzie, gdy kompilator będzie szukał konstruktora do inicjalizacji fCopy z f, zobaczy, że konstruktor kopiujący został usunięty. Spowoduje to wyemitowanie błędu kompilacji.

Na marginesie…

Możesz także uniemożliwić publiczne tworzenie kopii obiektu klasy, ustawiając konstruktor kopiujący jako prywatny (ponieważ funkcje prywatne nie mogą być używane przez publiczność). Jednak prywatny konstruktor kopiujący możesz w dalszym ciągu jest używany przez innych członków klasy, więc to rozwiązanie nie jest zalecane, chyba że jest to pożądane.

Dla zaawansowanych czytelników

Klasa Reguła trzech jest dobrze znaną zasadą C++, która stwierdza, że ​​jeśli klasa wymaga zdefiniowanego przez użytkownika konstruktora kopiującego, destruktora lub operatora przypisania kopii, to prawdopodobnie wymaga wszystkich trzech. W C++ 11 zostało to rozszerzone do reguły pięciu, która dodaje do listy konstruktor przenoszenia i operator przypisania przenoszenia.

Nieprzestrzeganie reguły trzech/reguły pięciu prawdopodobnie doprowadzi do nieprawidłowego działania kodu. Do zasad trzech i pięciu wrócimy, gdy będziemy omawiać dynamiczną alokację pamięci.

Destruktory omawiamy na lekcji 15.4 -- Wprowadzenie do destruktorów i 19.3 -- Destructors, a przypisywanie kopii na lekcji 21.12 — Przeciążanie operatora przypisania.

Czas quizu

Pytanie nr 1

W powyższej lekcji zauważyliśmy, że parametr konstruktora kopiującego musi być (stałym) odniesieniem. Dlaczego nie wolno nam używać przekazywania przez wartość?

Pokaż wskazówkę

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:  
239 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze