C++ : Classes et objets (ci-dessous) - ce pointeur, constructeur (de copie), destructeur, surcharge de l'opérateur de copie

Table des matières

Un, ce pointeur

1.1 Introduction

1.2 Questions

1.3 Caractéristiques

Deuxièmement, le constructeur

2.1 Conception

2.2 Caractéristiques

2.3 Grammaire

2.4 Remarques

3. Destructeur

3.1 Conception

3.2 Caractéristiques

3.3 Exemples

Quatrièmement, le constructeur de copie

4.1 Conception

4.2 Caractéristiques

4.3 Exemples

4.4 Copie profonde et superficielle

5. Surcharge des opérateurs d'affectation

5.1 Conception

5.2 Syntaxe

5.3 Exemples


Un, ce pointeur

1.1 Introduction

Regardez d'abord un morceau de code

#include <iostream>

class Person
{
public:
    void Init(const std::string& name, int age)
    {
        _name = name;
        _age = age;
    }

    void Print()
    {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

private:
    std::string _name;
    int _age;
};

int main()
{
    Person p1, p2;
    p1.Init("John", 30);
    p2.Init("Alice", 25);
    p1.Print(); // Output: Name: John, Age: 30
    p2.Print(); // Output: Name: Alice, Age: 25
    return 0;
}

1.2 Questions

Il y a deux fonctions membres Init et Print dans la classe Person , et il n'y a pas de distinction entre les différents objets dans le corps de la fonction, alors comment distinguer quel objet appelle ?

Les concepteurs C++ proposent d'utiliser le pointeur this pour résoudre ce problème. Lorsque nous appelons des fonctions membres, le compilateur C++ ajoute en interne un paramètre de pointeur masqué, c'est-à-dire un pointeur , à chaque fonction membre non statique . Ce pointeur est un pointeur constant vers l'adresse de l'objet courant , qui pointe vers l'objet qui a appelé la fonction membre. Dans le corps de la fonction, toutes les opérations sur les variables membres sont accessibles via des pointeurs. C'est juste que toutes les opérations sont transparentes pour l'utilisateur, c'est-à-dire que l'utilisateur n'a pas besoin de le passer et que le compilateur le complète automatiquement.thisthisthis

 Comme le montre la figure, lorsque nous définissons une fonction membre de classe, elle doit être définie dans le premier format ci-dessus. Le second est que le compilateur passe automatiquement le pointeur constant au membre appelant la fonction membre non statique dans la liste des paramètres. La figure ci-dessus est juste pour que les lecteurs puissent faire l'expérience du processus, les attributs de membre peuvent être utilisés directement dans la fonction. Lorsque le nom du paramètre de fonction est en conflit avec l'attribut de membre, la manière de this-> attribut de membre peut être utilisée pour faire la distinction !! !

Dans les fonctions membres, nous pouvons utiliser thisdes pointeurs pour accéder aux variables membres et aux fonctions membres de l'objet actuel. Par exemple, s'il y a une variable membre et une fonction appelée dans la classe value, nous pouvons l'utiliser this->valuepour indiquer clairement que la variable membre est accessible à la place de la fonction. De plus, en retournant dans une fonction membre *this, nous pouvons implémenter des appels chaînés, améliorant la lisibilité et la simplicité du code.

1.3 Caractéristiques

  • thisLe type du pointeur est , c'est-à-dire que le pointeur 类类型* constne peut pas recevoir de valeur dans la fonction membre , car il pointe vers l'adresse de l'objet actuel et qu'il n'est pas autorisé à pointer vers d'autres objets.this
  • thisLes pointeurs ne peuvent être utilisés qu'à l'intérieur des fonctions membres, pas dans les fonctions non membres d'une classe ou de fonctions globales.
  • thisUn pointeur est essentiellement un paramètre implicite d'une fonction membre. Lorsqu'un objet appelle une fonction membre, le compilateur transmet l'adresse de l'objet en tant que paramètre réel au thispointeur. Par conséquent, l'objet lui-même ne stocke pas thisde pointeurs.

Deuxièmement, le constructeur

Classe vide : il n'y a pas de propriétés membres et de fonctions membres dans la classe.

En C++, si vous ne le définissez pas dans la classe, la classe générera automatiquement des fonctions membres par défaut pour vous, si vous ne les définissez pas explicitement. Ces fonctions membres par défaut incluent :

  • Constructeur par défaut
  • Destructeur par défaut
  • Constructeur de copie par défaut
  • Opérateur d'affectation de copie par défaut
  • Constructeur de déplacement par défaut
  • Opérateur d'affectation de déplacement par défaut

Dans cet article, nous introduisons principalement les quatre premières fonctions, puis introduisons d'autres fonctions.

2.1 Conception

Nous pouvons nous référer au code ci-dessus pour développer l'introduction.

class Person
{
public:
    void Init(const std::string& name, int age)
    {
        _name = name;
        _age = age;
    }

    void Print()
    {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

private:
    std::string _name;
    int _age;
};

Lorsque nous créons un objet, nous devons appeler la fonction Init pour attribuer des valeurs aux propriétés de l'objet à chaque fois.Cependant, si nous appelons cette méthode pour attribuer des valeurs à chaque fois que nous créons un objet, cela semble un peu lourd. Peut-on définir les informations lors de la création de l'objet ? Cela crée notre constructeur .

  • Fonction : Permet d'initialiser la valeur par défaut de la variable membre lors de la création de l'objet.
  • Utilisation : lorsque vous créez un objet de classe, si vous ne fournissez pas explicitement de constructeur, le compilateur générera automatiquement un constructeur par défaut pour vous. Le constructeur par défaut n'a pas de paramètres, et il initialise les variables membres à leurs valeurs par défaut de leurs types correspondants (par exemple, 0 pour les types numériques, nullptr pour les types pointeurs, et les membres des objets de classe appellent leurs propres constructeurs pour initialiser) . L'implémentation peut être différente selon les compilateurs. Certains compilateurs ne traitent pas les types intégrés, qui sont des valeurs aléatoires. Les membres des objets de classe appelleront leurs constructeurs.
  • Le constructeur est une fonction membre spéciale portant le même nom que le nom de la classe, qui est automatiquement appelée par le compilateur lors de la création d'un objet de type classe pour s'assurer que chaque membre de données a une valeur initiale appropriée, et n'est appelée qu'une seule fois dans toute la vie. cycle de l' objet.

2.2 Caractéristiques

Le constructeur est une fonction membre spéciale. Il convient de noter que bien que le nom du constructeur soit appelé construction, la tâche principale du constructeur n'est pas de créer un espace pour créer un objet, mais d'initialiser l'objet et d'initialiser les propriétés du membre dans l'objet.

fonctionnalité:

  • 1. Le nom de la fonction est le même que le nom de la classe.
  • 2. Aucune valeur de retour.
  • 3. Le compilateur appelle automatiquement le constructeur correspondant lorsque l'objet est instancié.
  • 4. Le constructeur peut être surchargé .

2.3 Grammaire

#include <iostream>
#include <string>

class Person
{
public:
    // 1.无参构造函数
    Person()
    {}

    // 2.带参构造函数
    Person(const std::string& name, int age)
    {
        _name = name;
        _age = age;
    }

    // 3.打印个人信息
    void PrintInfo()
    {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

private:
    std::string _name;
    int _age;
};

À partir du code ci-dessus, nous pouvons temporairement diviser les constructeurs en constructeurs sans paramètres et en constructeurs paramétrés. Voyons comment utiliser ces deux paramètres pour initialiser des objets.

int main()
{
    // 调用无参构造函数创建对象
    Person p1;
    p1.PrintInfo();

    // 调用带参构造函数创建对象
    Person p2("John", 30);
    p2.PrintInfo(); // Output: Name: John, Age: 30

    return 0;
}

Lorsque vous utilisez une construction sans argument pour initialiser un objet, vous pouvez utiliser directement le nom de la classe + le nom de l'objet . Lorsque vous utilisez la construction de paramètres, vous devez transmettre les paramètres correspondants pour initialiser les propriétés du membre.

rappeler

Person person();

Lors de l'instanciation d'un objet avec une construction sans argument, n'ajoutez pas () après l'objet. Cela empêchera le compilateur d'identifier s'il s'agit d'une instanciation d'un objet utilisant une construction sans argument ou d'une fonction qui déclare que le retour value est un type Person. Il est préférable de ne pas l'utiliser ! ! !

Si nous n'écrivons pas activement un constructeur lors de la définition d'une classe, le système générera automatiquement un constructeur par défaut, c'est-à-dire un constructeur sans argument. Une fois que l'utilisateur définit explicitement que le compilateur ne générera plus, c'est-à-dire que l'utilisateur implémente le paramètre ou aucun paramètre, le compilateur n'implémente plus le constructeur par défaut (pas de paramètre).

Si vous annotez le constructeur sans argument du code ci-dessus, en ne laissant que le constructeur argument-argument, vous ne pouvez instancier l'objet qu'en appelant le constructeur argument-argument et vous ne pouvez pas utiliser le constructeur sans argument.

2.4 Remarques

Le constructeur sans paramètre et le constructeur par défaut sont appelés constructeurs par défaut et il ne peut y avoir qu'un seul constructeur par défaut. Remarque : Les constructeurs sans argument, les constructeurs par défaut complets et les constructeurs que nous n'avons pas écrits pour être générés par le compilateur par défaut peuvent tous être considérés comme des constructeurs par défaut .

class Date {
public:
    Date() {
        _year = 1900;
        _month = 1;
        _day = 1;
    }

    Date(int year = 1900, int month = 1, int day = 1) {
        _year = year;
        _month = month;
        _day = day;
    }

private:
    int _year;
    int _month;
    int _day;
};

// 以下测试函数能通过编译吗?
void Test() {
    Date d1;
}

La réponse est non.

Parce qu'à ce stade, le constructeur sans argument et le constructeur par défaut complet peuvent instancier des objets pour cette ligne de code, provoquant une ambiguïté et un échec de compilation ! ! !

3. Destructeur

3.1 Conception

Le destructeur est une fonction membre spéciale en C++, qui est utilisée pour nettoyer et libérer des ressources lorsque l'objet est détruit. Son nom est précédé d'un tilde (~) avant le nom de la classe, par exemple, si le nom de la classe est ClassName, alors le nom du destructeur est ~ClassName.

Le rôle du destructeur est de gérer les conséquences de l'objet. Lorsque le cycle de vie de l'objet se termine (par exemple, l'objet sort de la portée, est explicitement supprimé ou le programme se termine), le destructeur sera appelé automatiquement .

3.2 Caractéristiques

Les destructeurs ont les caractéristiques suivantes :

  1. Les destructeurs n'ont aucune valeur de retour, y compris void, et aucun paramètre.
  2. Une classe peut avoir un et un seul destructeur, et elle ne peut pas être surchargée.
  3. Si vous ne définissez pas explicitement un destructeur, le compilateur générera automatiquement un destructeur par défaut pour vous.
  4. S'il existe des ressources allouées dynamiquement dans la classe (telles que la mémoire sur le tas, les descripteurs de fichiers, etc.), ces ressources doivent être libérées dans le destructeur pour éviter les fuites de mémoire et les fuites de ressources.

3.3 Exemples

#include <iostream>

class MyClass {
public:
    // 构造函数
    MyClass() {
        std::cout << "Constructor called." << std::endl;
    }

    // 析构函数
    ~MyClass() {
        std::cout << "Destructor called." << std::endl;
    }
};

int main() {
    std::cout << "Creating object..." << std::endl;
    MyClass obj; // 创建对象,调用构造函数

    std::cout << "Object will be destroyed..." << std::endl;
    // 在这里,obj超出了作用域,对象的生命周期结束,析构函数被自动调用

    return 0;
}
Creating object...
Constructor called.
Object will be destroyed...
Destructor called.

Cela prouve que le constructeur et le destructeur de l'objet sont appelés respectivement lorsque l'objet est créé et détruit. L'appel du destructeur peut garantir que l'objet termine le travail de nettoyage nécessaire lorsqu'il est détruit, libère des ressources et évite les fuites de ressources.

Rappel : Si vous n'écrivez pas manuellement un destructeur, le système générera automatiquement un destructeur, mais le propre destructeur du système est implémenté dans le temps et l'espace, et ne fait rien. Bien sûr, s'il n'y a pas d'espace créé par la zone de tas à l'intérieur de l'objet de classe, utilisez simplement celui généré par le système. Mais s'il y a de l'espace ouvert dans la zone du tas, il doit être libéré manuellement à l'intérieur du destructeur, sinon cela provoquera facilement des fuites de mémoire ! ! ! Deuxièmement, s'il y a d'autres attributs de membre de classe dans l'objet de classe, le destructeur de l'attribut de membre de classe sera appelé automatiquement lorsque l'objet sera détruit, et il n'est pas nécessaire de le gérer dans le destructeur de l'objet de classe ! ! !

Quatrièmement, le constructeur de copie

4.1 Conception

Le constructeur de copie est un constructeur spécial en C++, qui est utilisé pour créer un nouvel objet lorsque l'objet est copié et copier la valeur de l'objet d'origine dans le nouvel objet. Sa fonction est de générer un nouvel objet, qui a le même contenu que l'objet d'origine, mais ils sont indépendants, et la modification du contenu d'un objet n'affectera pas l'autre objet.

Si vous ne définissez pas explicitement un constructeur de copie, le compilateur générera un constructeur de copie par défaut pour vous. Le constructeur de copie par défaut copie les valeurs des variables membres une par une et fait une copie superficielle des membres du pointeur dans la classe (c'est-à-dire qu'il copie la valeur du pointeur au lieu de copier l'objet pointé par le pointeur). S'il y a des ressources dans la classe qui ont besoin d'une copie profonde (telles que la mémoire allouée dynamiquement), vous devez définir un constructeur de copie pour terminer la copie profonde, sinon un morceau d'espace sera libéré à plusieurs reprises dans le destructeur et provoquera des erreurs.

4.2 Caractéristiques

  • Le constructeur de copie est une forme surchargée du constructeur.
  • Le paramètre du constructeur de copie est un seul et doit être une référence à un objet de type classe, et le compilateur signalera directement une erreur si la méthode de passage de valeur est utilisée, car cela provoquera des appels récursifs infinis.
ClassName(const ClassName& other);

4.3 Exemples

Supposons que nous ayons une classe MyClasset que nous essayions de définir un constructeur de mauvaise copie qui prend les paramètres par valeur :

Dans l'exemple ci-dessus, nous avons défini une MyClassclasse appelée et essayé de définir un constructeur de copie en utilisant le passage par valeur. Lorsque nous essayons de créer obj2un objet à l'aide du constructeur de copie, cela se traduit par un appel récursif infini, entraînant un débordement de pile.

Cela se produit parce que la méthode pass-by-value appelle le constructeur de copie lui-même pour créer une copie du paramètre passé, puis crée à nouveau une copie du paramètre lors de l'appel au constructeur de copie, ce qui entraîne une récursivité infinie.

Afin d'éviter des appels récursifs infinis, les paramètres du constructeur de copie doivent être reçus par référence, de sorte que seule la référence de l'objet sera passée lors de l'appel du constructeur de copie, et aucune nouvelle copie ne sera créée.

Voici l'exemple de code corrigé, utilisant la méthode de référence pour définir le constructeur de copie correct :

#include <iostream>

class MyClass {
public:
    // 正确的拷贝构造函数
    MyClass(const MyClass& obj) {
        std::cout << "Copy constructor called." << std::endl;
    }
};

int main() {
    MyClass obj1;
    MyClass obj2 = obj1; // 正确,使用引用方式传递参数
    MyClass obj3(obj2);    //此种方式也可以

    return 0;
}

4.4 Copie profonde et superficielle

Copie superficielle (Shallow Copy) : Copie superficielle signifie que lors de la copie d'un objet, seule la valeur de la variable membre dans l'objet est copiée, y compris la valeur de la variable membre du pointeur. Cela signifie que le nouvel objet et l'objet d'origine partageront les mêmes ressources, plutôt que de créer des copies de ressources distinctes pour le nouvel objet. Si l'objet d'origine contient des membres pointeurs pointant vers la mémoire de tas, les membres pointeurs du nouvel objet et l'objet d'origine pointent vers la même mémoire de tas après une copie superficielle, ce qui fait que deux objets gèrent la même ressource, ce qui peut entraîner des problèmes de libération de ressources et erreurs potentielles.

Copie profonde (Deep Copy) : copie profonde signifie que lorsqu'un objet est copié, une copie de ressource indépendante sera créée pour le nouvel objet au lieu des ressources partagées. Si l'objet d'origine a un membre pointeur pointant vers la mémoire de tas, la copie approfondie allouera de la mémoire pour le membre pointeur du nouvel objet séparément et copiera le contenu pointé par le pointeur d'objet d'origine vers la nouvelle mémoire. De cette façon, les deux objets ont leurs propres ressources indépendantes, et la modification des ressources d'un objet n'affectera pas l'autre objet.

#include <iostream>
#include <cstring>
#include <cstdlib>

class Person {
public:
    // 构造函数
    Person(const char* name, int age) {
        _name = (char*)malloc(strlen(name) + 1);
        strcpy(_name, name);
        _age = age;
    }

    // 拷贝构造函数(浅拷贝)
    Person(const Person& other) {
        _name = other._name; // 浅拷贝,共享资源
        _age = other._age;
    }

    // 深拷贝构造函数(深拷贝)
    Person(const Person& other) {
        _name = (char*)malloc(strlen(other._name) + 1); // 深拷贝,为新对象分配独立资源
        strcpy(_name, other._name);
        _age = other._age;
    }

    // 析构函数
    ~Person() {
        free(_name);
    }

    // 打印个人信息
    void PrintInfo() {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

private:
    char* _name;
    int _age;
};

int main() {
    // 创建一个Person对象
    Person p1("John", 30);

    // 浅拷贝
    Person p2(p1);
    p1.PrintInfo(); // Output: Name: John, Age: 30
    p2.PrintInfo(); // Output: Name: John, Age: 30

    // 修改p1的值
    p1 = Person("Alice", 25);
    p1.PrintInfo(); // Output: Name: Alice, Age: 25
    p2.PrintInfo(); // Output: Name: Alice, Age: 30(由于浅拷贝,p2共享p1的资源,也被修改为Alice)

    // 深拷贝
    Person p3(p1);
    p1.PrintInfo(); // Output: Name: Alice, Age: 25
    p3.PrintInfo(); // Output: Name: Alice, Age: 25(由于深拷贝,p3拥有独立的资源,不受p1的修改影响)

    return 0;
}

5. Surcharge des opérateurs d'affectation

5.1 Conception

La surcharge d'opérateur d'affectation est une fonction spéciale qui permet des opérations d'affectation entre les membres de classes personnalisées en C++. En surchargeant l'opérateur d'affectation, nous pouvons implémenter un comportement d'affectation personnalisé entre les objets de classe pour assurer une copie correcte des objets et la gestion des ressources.

5.2 Syntaxe

返回类型 operator=(const 类名& 另一个对象) {
    // 赋值操作的实现
    // 返回对象本身的引用
}

Parmi eux, le type de retour est généralement un type de référence, qui peut prendre en charge les opérations d'affectation continue. L'argument est une constréférence représentant l'objet à droite de l'opérateur d'affectation transmis.

5.3 Exemples

#include <iostream>
#include <cstring>

class Person {
public:
    Person(const char* name, int age) {
        _name = new char[strlen(name) + 1];
        strcpy(_name, name);
        _age = age;
    }

    // 拷贝构造函数
    Person(const Person& other) {
        _name = new char[strlen(other._name) + 1];
        strcpy(_name, other._name);
        _age = other._age;
    }

    // 赋值运算符重载
    Person& operator=(const Person& other) {
        if (this == &other) { // 自我赋值检测
            return *this;
        }
        delete[] _name; // 释放旧资源

        _name = new char[strlen(other._name) + 1];
        strcpy(_name, other._name);
        _age = other._age;

        return *this; // 返回对象本身的引用
    }

    ~Person() {
        delete[] _name;
    }

    void PrintInfo() {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

private:
    char* _name;
    int _age;
};

int main() {
    Person p1("John", 30);
    Person p2("Alice", 25);

    p1.PrintInfo(); // Output: Name: John, Age: 30
    p2.PrintInfo(); // Output: Name: Alice, Age: 25

    p2 = p1; // 赋值操作

    p1.PrintInfo(); // Output: Name: John, Age: 30
    p2.PrintInfo(); // Output: Name: John, Age: 30(p2被赋值为p1的内容)

    return 0;
}

Dans cet exemple, nous Personavons surchargé l'opérateur d'affectation dans la classe. Dans la fonction surchargée, nous vérifions d'abord si une auto-assignation s'est produite (l'objet lui-même est assigné à lui-même), et si c'est le cas, nous renvoyons directement la référence à l'objet. Libérez ensuite les anciennes ressources (supprimez l'ancienne _namemémoire), puis réallouez la mémoire et copiez le nouveau contenu.

Je suppose que tu aimes

Origine blog.csdn.net/weixin_57082854/article/details/132123515
conseillé
Classement