<C++> Classes et objets (moyen) - fonctions membres par défaut des classes

1. La fonction membre par défaut de la classe

Fonction membre par défaut : la fonction membre générée par le compilateur sans implémentation explicite par l'utilisateur est appelée fonction membre par défaut.

S’il n’y a aucun membre dans une classe, on parle simplement de classe vide.

N'y a-t-il vraiment rien dans la classe vide ? Non, lorsqu'une classe n'écrit rien, le compilateur générera automatiquement les fonctions membres par défaut suivantes.

  1. Constructeur par défaut : Si aucun constructeur n'est explicitement défini pour une classe, le compilateur génère un constructeur par défaut. Ce constructeur n'a aucun paramètre et permet d'effectuer les opérations d'initialisation nécessaires lors de la création d'un objet.
  2. Constructeur de copie : Si aucun constructeur de copie n'est défini pour la classe, le compilateur générera un constructeur de copie par défaut. Ce constructeur est utilisé pour copier les valeurs d'un objet existant dans le nouvel objet lorsque l'objet est initialisé .
  3. Opérateur d'affectation surchargé : Si aucun opérateur d'affectation surchargé n'est défini pour une classe, le compilateur génère un opérateur d'affectation de copie par défaut. Cet opérateur permet d' attribuer la valeur d'un objet existant à un autre objet existant.
  4. Destructeur : Si aucun destructeur n'est défini pour une classe, le compilateur génère un destructeur par défaut. Le destructeur est appelé lorsque l'objet est détruit pour libérer les ressources occupées par l'objet.

2. Constructeur

2.1 La notion de constructeur

Pour la classe Date suivante :

#include <iostream>
using namespace std;
class Date {
    
    
public:
    void Init(int year, int month, int day) {
    
    
        _year = year;
        _month = month;
        _day = day;
    }

    void Print() {
    
    
        cout << _year << "-" << _month << "-" << _day << endl;
    }

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

int main() {
    
    
    Date d1;
    d1.Init(2022, 7, 5);
    d1.Print();
    Date d2;
    d2.Init(2022, 7, 6);
    d2.Print();
    return 0;
}

Pour Dateles classes, vous pouvez définir la date de l'objet via la fonction publique Init, mais si vous appelez cette méthode pour définir les informations à chaque fois que l'objet est créé, c'est un peu gênant. Pouvez-vous définir les informations lorsque l'objet est créé ?

La réponse est d'utiliser des 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 garantir que chaque membre de données a une valeur initiale appropriée et n'est appelé qu'une seule fois dans toute la vie. cycle de l'objet .

2.2 Caractéristiques du constructeur

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 d'ouvrir l'espace pour créer des objets, mais d'initialiser les objets .

Ses caractéristiques sont les suivantes :

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é . C'est-à-dire que lorsque l'objet est créé, les variables membres sont initialisées.

4. Le constructeur peut être surchargé. (Une classe peut avoir plusieurs constructeurs, c'est-à-dire plusieurs méthodes d'initialisation)

Exemple:

#include <iostream>
using namespace std;
class Date {
    
    
public:
    Date() {
    
    
        _year = 1;
        _month = 1;
        _day = 1;
    }

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

    
    void Print() {
    
    
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }

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

int main() {
    
    
    Date d1;
    Date d2(2023, 2, 3); 
	
    d1.Print();
    d2.Print();   
	
    Date d3();
    //注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明

    return 0;
}

5. Le constructeur sans paramètre et le constructeur par défaut complet 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.

Pourquoi ne peut-il y avoir qu’un seul constructeur par défaut ? Parce qu'une ambiguïté se produit lors de l'appel

Le constructeur peut être appelé sans passer de paramètres. Il est généralement recommandé que chaque classe fournisse un constructeur par défaut

class Date {
    
    
public:
    Date() {
    
    
        _year = 1;
        _month = 1;
        _day = 1;
    }
	
    //全缺省构造函数
    Date(int year = 1, int month = 1, int day = 1) {
    
    
        _year = year;
        _month = month;
        _day = day;
    }

    
    void Print() {
    
    
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }

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

Un constructeur par défaut complet ne peut pas coexister avec un constructeur sans argument - une ambiguïté surgit, le compilateur ne sait pas lequel appeler

insérer la description de l'image ici

6. S'il n'y a pas de constructeur explicitement défini dans la classe, le compilateur C++ générera automatiquement un constructeur par défaut sans paramètres. Une fois que l'utilisateur aura défini explicitement le compilateur, il ne le générera plus.

#include <iostream>
using namespace std;
class Date {
    
    
public:
    // 如果用户显式定义了构造函数,编译器将不再生成
    // Date(int year, int month, int day) {
    
    
    //     _year = year;
    //     _month = month;
    //     _day = day;
    // }

    void Print() {
    
    
        cout << _year << "-" << _month << "-" << _day << endl;
    }

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

int main() {
    
    
    // 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
    // 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
    // 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
    Date d1;
    d1.Print();
    return 0;
}

Mais nous avons constaté que la sortie est une valeur aléatoire ?

insérer la description de l'image ici

Beaucoup de gens ont des doutes sur les fonctions membres par défaut générées par le compilateur : si le constructeur n'est pas implémenté, le compilateur générera un constructeur par défaut. Mais il semble que le constructeur par défaut soit inutile ? L'objet d appelle le constructeur par défaut généré par le compilateur, mais la valeur de l'objet d est toujours une valeur aléatoire. En d'autres termes , le constructeur par défaut généré par le compilateur est inutile ici ?

Réponse : C++ divise les types en types intégrés (types de base) et types personnalisés. Le type intégré est le type de données fourni par le langage, tel que : int/char..., le type personnalisé est le type que nous définissons nous-mêmes en utilisant class/struct/union, etc. Si vous regardez le programme suivant, vous constaterez que le constructeur par défaut généré par le compilateur sera sa fonction membre par défaut appelée sur le membre de type personnalisé _t.

C++ spécifie le constructeur généré par défaut :

1. Les membres du type intégré ne sont pas traités.
2. Les membres du type personnalisé appelleront le constructeur de la classe du type personnalisé.
Remarque : En C++11, un correctif a été appliqué pour le défaut selon lequel les membres de type intégrés ne sont pas initialisés, c'est-à-dire que les variables membres de type intégrées peuvent recevoir une valeur par défaut lorsqu'elles sont déclarées dans la classe.

Scénarios pour le constructeur par défaut :

#include <iostream>
using namespace std;

class Time {
    
    
public:
    Time() {
    
    
        cout << "Time()" << endl;
        _hour = 0;
        _minute = 0;
        _second = 0;
    }

private:
    int _hour;
    int _minute;
    int _second;
};

class Date {
    
    
private:
    // 基本类型(内置类型)
    int _year;
    int _month;
    int _day;
    // 自定义类型
    Time _t;
};

int main() {
    
    
    Date d;
    return 0;
}

insérer la description de l'image ici

On constate que le constructeur par défaut généré par l'objet d appelle le constructeur du type personnalisé _t

Avis:

En C++11, un correctif a été appliqué pour le défaut selon lequel les membres de type intégrés ne sont pas initialisés, c'est-à-dire que les variables membres de type intégrées peuvent recevoir des valeurs par défaut lorsqu'elles sont déclarées dans la classe .

Exemple:

class Date {
    
    
private:
    // 基本类型(内置类型)
    int _year = 2023;
    int _month = 8;
    int _day = 4;
    // 自定义类型
    Time _t;
};

2.3 Liste d'initialisation

Lors de la création d'un objet, le compilateur appelle le constructeur pour donner à chaque variable membre de l'objet une valeur initiale appropriée.

Mais cela ne peut pas être appelé l'initialisation des variables membres dans l'objet, et l'instruction dans le corps du constructeur ne peut être appelée que l'attribution de valeur initiale , pas l'initialisation. Parce que l'initialisation ne peut être initialisée qu'une seule fois et que le corps du constructeur peut être attribué plusieurs fois .

En termes d'efficacité, les listes d'initialisation sont généralement plus efficaces que l'initialisation des variables membres dans le corps du constructeur, car elles initialisent les variables membres en même temps que la création de l'objet, au lieu de créer d'abord des objets puis d'attribuer des variables membres.

Liste d'initialisation : commence par deux points , suivis d'une liste de données membres séparées par des virgules , chaque " variable membre " suivie d'une valeur initiale ou d'une expression entre parenthèses .

class Date {
    
    
public:
    Date(int year, int month, int day)
        //初始化列表
        : _year(year), _month(month), _day(day) {
    
    }

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

Avis:

1. Chaque variable membre ne peut apparaître qu'une seule fois dans la liste d'initialisation (l'initialisation ne peut être initialisée qu'une seule fois)

2. La classe contient les membres suivants, qui doivent être placés dans la liste d'initialisation pour l'initialisation :

  • variable de membre de référence
  • variable membre const
  • Un membre de type personnalisé (et la classe n'a pas de constructeur par défaut)
class A {
    
    
public:
    A(int a)
        : _a(a) {
    
    }

private:
    int _a;
};

class B {
    
    
public:
    B(int a, int ref)
        : _aobj(a), _ref(ref), _n(10) {
    
    }

private:
    A _aobj;     // 没有默认构造函数
    int &_ref;   // 引用
    const int _n;// const
};

3. Essayez d'utiliser la liste d'initialisation pour initialiser, car que vous utilisiez ou non la liste d'initialisation, pour les variables membres de type personnalisé, vous devez d'abord utiliser la liste d'initialisation pour initialiser.

class Time {
    
    
public:
    Time(int hour = 0)
        : _hour(hour) {
    
    
        cout << "Time()" << endl;
    }

private:
    int _hour;
};

class Date {
    
    
public:
    Date(int day) {
    
    }

private:
    int _day;
    Time _t;   //调用Time()的构造函数
};

int main() {
    
    
    Date d(1);
}

4. L'ordre d'initialisation dans la liste d'initialisation est cohérent avec l'ordre de déclaration des variables membres de la classe et n'a rien à voir avec l'ordre de la liste d'initialisation

#include <iostream>
using namespace std;

class A {
    
    
public:
    A(int a)
        : _a1(a), _a2(_a1) {
    
    
    }

    void Print() {
    
    
        cout << _a1 << " " << _a2 << endl;
    }

private:
    int _a2;   //随机值
    int _a1;   //随机值
};

int main() {
    
    
    A aa(1);
    aa.Print();
}

a2 est initialisé en premier, a1 appelle la liste d'initialisation au moment de l'initialisation, et a1 est une valeur aléatoire à ce moment, donc a2 est une valeur aléatoire après a2(a1)

2.4 explicite

explicitEst un mot-clé, généralement utilisé pour modifier le constructeur à argument unique , son but est d'empêcher la conversion de type implicite . Cela affecte si le compilateur effectue des conversions de type implicites lors de l'appel des constructeurs.

Lorsque le constructeur d'une classe n'a qu'un seul paramètre et n'est pas explicitmodifié avec le mot-clé, le compilateur exécutera automatiquement le constructeur si nécessaire, convertira le type de paramètre en type de classe et créera un objet temporaire.

Décorer les constructeurs avec explicitdes mots-clés peut empêcher cette conversion de type implicite, évitant ainsi certains comportements inattendus et erreurs potentielles. Ceci est utile pour éviter les conversions de type automatiques inutiles par le compilateur, qui peuvent parfois conduire à un comportement de code ambigu.

Exemple 1:

class Date {
    
    
public:
    //1.单参构造函数,没有使用explicit修饰,具有类型转换作用
    /*Date(int year) : _year(year) {}*/

    // 2,虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用 - int转换为Date类
    Date(int year, int month = 1, int day = 1)
        : _year(year), _month(month), _day(day) {
    
    }

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

int main() {
    
    
    Date d1(2022);

    d1 = 2023;  //用一个整型变量给日期类型对象赋值
    //实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
    return 0;
}

Exemple 2 :

Utilisation de explicitconversions de types interdites

class Date {
    
    
public:
    // explicit修饰构造函数,禁止类型转换
    explicit Date(int year, int month = 1, int day = 1)
        : _year(year), _month(month), _day(day) {
    
    }

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

int main() {
    
    
    Date d1(2022);

    d1 = 2023;   //err 编译器报错,没有与这些操作数匹配的 "=" 运算符,操作数类型为:  Date = int
    return 0;
}

Exemple 3 :

class MyClass {
    
    
public:
    explicit MyClass(int value) : val(value) {
    
    }

private:
    int val;
};

void func(MyClass obj) {
    
    
    // ...
}

int main() {
    
    
    MyClass obj1 = 42;  // 编译错误,因为构造函数是 explicit 的,禁止隐式类型转换
    MyClass obj2(42);   // 正确,显式调用构造函数
    func(42);           // 编译错误,因为构造函数是 explicit 的,禁止隐式类型转换
    func(obj2);         // 正确,调用 func 时显式地传递 MyClass 对象
    return 0;
}

Résumé : L'utilisation explicitdu mot-clé peut aider à éviter les conversions de type implicites, améliorant ainsi la clarté et la fiabilité du code.

3. Destructeur

Un destructeur est une fonction membre spéciale qui effectue les opérations de nettoyage et de libération de ressources nécessaires lorsqu'un objet est détruit. C'est l'opposé d'un constructeur, qui est appelé lors de la création d'un objet, alors qu'un destructeur est appelé automatiquement lorsqu'un objet est détruit . Le destructeur porte le même nom que le nom de la classe, précédé d'un tilde ~.

L'objectif principal du destructeur est d'effectuer un nettoyage des ressources à la fin de la durée de vie de l'objet, comme libérer de la mémoire allouée dynamiquement, fermer des fichiers, libérer d'autres ressources, etc. En C++, les destructeurs assurent la gestion des ressources lors de la destruction des objets pour éviter les fuites de mémoire et les fuites de ressources.

Les caractéristiques du destructeur :

  1. Le nom du destructeur est préfixé par le caractère ~ avant le nom de la classe.
  2. Aucun paramètre et aucun type de retour.
  3. Une classe ne peut avoir qu'un seul destructeur. S'il n'est pas explicitement défini, le système générera automatiquement un destructeur par défaut. Remarque : les destructeurs ne peuvent pas être surchargés
  4. Lorsque le cycle de vie de l'objet se termine, le système de compilation C++ appelle automatiquement le destructeur.
  5. Ceux construits en premier sont détruits, et ceux construits plus tard sont détruits en premier , car l'objet est défini dans la fonction, et l'appel de fonction créera un cadre de pile, et la construction et la destruction de l'objet dans le cadre de pile doivent également être conformes aux principe du premier entré, dernier sorti.

Est-ce que quelque chose est fait concernant les destructeurs générés automatiquement par le compilateur ? Dans le programme suivant, nous verrons que le destructeur par défaut généré par le compilateur appelle son destructeur pour le membre de type personnalisé.

#include <iostream>
using namespace std;
class Time {
    
    
public:
    ~Time() {
    
    
        cout << "~Time()" << endl;
    }

private:
    int _hour;
    int _minute;
    int _second;
};

class Date {
    
    
private:
    // 基本类型(内置类型)
    int _year = 1970;
    int _month = 1;
    int _day = 1;
    // 自定义类型
    Time _t;   //释放Date的时候调用_t的析构函数
};

int main() {
    
    
    Date d;
    return 0;
}

Résultat de sortie :

~Time()

S'il n'y a pas d'application de ressources dans la classe, le destructeur ne peut pas être écrit et le destructeur par défaut généré par le compilateur est utilisé directement, comme la classe Date ; lorsqu'il y a une application de ressources, elle doit être écrite, sinon elle le sera provoquer une fuite de ressources, comme la classe Stack.

4. Copier le constructeur

Un constructeur de copie est un constructeur spécial utilisé pour créer un objet qui est une copie d'un objet existant. Il est généralement utilisé pour créer de nouveaux objets ayant les mêmes valeurs que les objets existants lors de l'initialisation de l'objet et dans des situations telles que le passage de paramètres de fonction. Le constructeur de copie prend un paramètre, une référence à l'objet à copier.

Les caractéristiques de la fonction copie sont les suivantes :

1. Le constructeur de copie est une forme surchargée du constructeur.

2. Le paramètre du constructeur de copie n'en est qu'un 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 par valeur est utilisée, car elle provoquera des appels récursifs infinis.

3. S'il n'est pas explicitement défini, le compilateur générera un constructeur de copie par défaut. L'objet constructeur de copie par défaut est copié dans l'ordre des octets en fonction du stockage en mémoire. Ce type de copie est appelé copie superficielle , ou copie de valeur

Pourquoi cela provoque-t-il une récursion infinie ?

En supposant que le paramètre du constructeur de copie est un type non référence :

class MyClass {
    
    
public:
    MyClass(MyClass other) {
    
      // 这里不是引用类型
        // ...
    }
};

Lorsque le constructeur de copie est appelé, il prend un objet comme paramètre. Si le paramètre est un type non référence, alors l'objet paramètre transmis au constructeur de copie créera une copie temporaire de l'objet via le constructeur de copie. Mais cela provoquera une boucle infinie, car lors du processus de création d'un objet temporaire, le constructeur de copie sera appelé pour créer un autre objet temporaire, et ainsi de suite, ce qui entraînera une récursion infinie.

L’utilisation de paramètres de type référence évite ce problème. Lorsque l'argument est un type référence, ce qui est transmis au constructeur de copie est une référence à l'objet d'origine lui-même, et non à l'objet temporaire créé. Cela évite une situation de boucle infinie.

Exemple correct :

#include <iostream>
using namespace std;
//正确的写法
class Date {
    
    
public:
    Date(int year = 2023, int month = 1, int day = 1) {
    
    
        _year = year;
        _month = month;
        _day = day;
    }

    Date(const Date &d) {
    
    
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    void Print() {
    
    
        cout << _year << "/" << _month << "/" << _day << endl;
    }

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

int main() {
    
    
    Date d1(2021, 2, 3);
    Date d2(d1);
    d2.Print();
    return 0;
}

Résultat de sortie :

2021/2/3

4.1 Copie superficielle et copie profonde

La copie superficielle fait référence à la copie uniquement de la valeur des variables membres de l'objet , y compris les variables de pointeur. Dans une copie superficielle, l'objet copié partage les mêmes ressources que l'objet d'origine , ce qui peut entraîner des effets secondaires involontaires. Si la ressource est libérée ou modifiée, les deux objets sont affectés.

Le constructeur de copie par défaut généré par le compilateur peut déjà copier des valeurs ordonnées en octets. Dois-je l'implémenter explicitement moi-même ? Bien sûr, les classes comme la classe Date ne sont pas nécessaires. Qu’en est-il des cours suivants ? Essayez de le vérifier ?

#include <iostream>
using namespace std;

//自动生成构造拷贝函数对自定义类型进行拷贝
typedef int DataType;
class Stack {
    
    
public:
    Stack(size_t capacity = 10) {
    
    
        cout << "Stack(size_t capacity = 10)" << endl;

        _array = (DataType*)malloc(capacity * sizeof(DataType));
        if (nullptr == _array) {
    
    
            perror("malloc申请空间失败");
            exit(-1);
        }

        _size = 0;
        _capacity = capacity;
    }

    void Push(const DataType& data) {
    
    
        // CheckCapacity();
        _array[_size] = data;
        _size++;
    }

    ~Stack() {
    
    
        cout << "~Stack()" << endl;

        if (_array) {
    
    
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }

private:
    DataType* _array; 
    size_t _size; 
    size_t _capacity; 
};

int main() {
    
    
    Stack st1;
    st1.Push(1);
    st1.Push(2);
    st1.Push(3);

    Stack st2(st1);
    return 0;
}

insérer la description de l'image ici

On peut constater que le programme plante directement, quelle en est la raison ?
insérer la description de l'image ici

Avis:

Si l'application de ressource n'est pas impliquée dans la classe, le constructeur de copie peut être écrit ou non ; une fois l'application de ressource impliquée, le constructeur de copie doit être écrit, et ce doit être une copie complète !

La copie profonde signifie que lorsqu'un objet est copié, non seulement la valeur de la variable membre de l'objet est copiée, mais également la ressource elle-même pointée par le pointeur. De cette façon, le nouvel objet est complètement indépendant de l’objet d’origine et les modifications apportées à un objet n’affecteront pas l’autre.

Exemple:

#include <iostream>
using namespace std;

typedef int DataType;
class Stack {
    
    
public:
    Stack(size_t capacity = 10) {
    
    
        cout << "Stack(size_t capacity = 10)" << endl;

        _array = (DataType*)malloc(capacity * sizeof(DataType));
        if (nullptr == _array) {
    
    
            perror("malloc申请空间失败");
            exit(-1);
        }

        _size = 0;
        _capacity = capacity;
    }

    void Push(const DataType& data) {
    
    
        // CheckCapacity();
        _array[_size] = data;
        _size++;
    }

    //stack类的拷贝构造深拷贝
    Stack(const Stack& st) {
    
    
        cout << "Stack(const Stack& st)" << endl;
        //深拷贝开额外空间,为了避免指向同一空间
        _array = (DataType*)malloc(sizeof(DataType) * st._capacity);
        if (nullptr == _array) {
    
    
            perror("malloc申请空间失败");
            exit(-1);
        }
        //进行字节拷贝
        memcpy(_array, st._array, sizeof(DataType) * st._size);
        _size = st._size;
        _capacity = st._capacity;
    }

    ~Stack() {
    
    
        cout << "~Stack()" << endl;

        if (_array) {
    
    
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }

private:
    DataType* _array;
    size_t _size;
    size_t _capacity;
};

class MyQueue {
    
    
public:
    //MyQueue什么都不写,会调用默认的构造函数,也就是Stack类的构造函数
    // 默认生成构造
    // 默认生成析构
    // 默认生成拷贝构造

private:
    //默认构造函数初始化 - 默认析构函数
    Stack _pushST;
    //默认构造函数初始化 - 默认析构函数
    Stack _popST;
    int _size = 0;
};

int main() {
    
    
    Stack st1;
    st1.Push(1);
    st1.Push(2);
    st1.Push(4);

    Stack st2(st1);
    cout << "=============================" << endl;

    MyQueue q1;
    //q1拷贝q2  q1中有两个Stack类和一个size,size直接拷贝,stack类是调用stack拷贝构造进行拷贝
    MyQueue q2(q1);

    return 0;
}

4.2 Scénarios d'appel typiques du constructeur de copie :

a. Créer un nouvel objet en utilisant un objet existant

b. Le type de paramètre de fonction est un objet de type classe

c. Le type de valeur de retour de la fonction est un objet de type classe

Exemple:

#include <iostream>
using namespace std;
class Date {
    
    
public:
    Date(int year, int minute, int day) {
    
    
        cout << "Date(int,int,int):" << this << endl;
    }

    Date(const Date &d) {
    
    
        cout << "Date(const Date& d):" << this << endl;
    }
    
    ~Date() {
    
    
        cout << "~Date():" << this << endl;
    }

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

Date Test(Date d) {
    
    
    Date temp(d);
    return temp;
}

int main() {
    
    
    Date d1(2022, 1, 13);
    Test(d1);
    return 0;
}

5. Surcharge de tâches

Par défaut, C++ génère des opérateurs d'affectation par défaut pour les classes, mais si la classe contient des pointeurs ou des ressources, des opérateurs d'affectation personnalisés peuvent être nécessaires pour éviter des problèmes de copie superficielle.

operator=Pour surcharger l'opérateur d'affectation, vous devez définir une fonction membre spéciale appelée dans la classe .

Format:

Type de paramètre :const T& , la transmission de références peut améliorer l'efficacité de la transmission des paramètres

Type de valeur de retour : T&, la référence de retour peut améliorer l'efficacité du retour et le but de la valeur de retour est de prendre en charge l'affectation continue

Vérifiez si vous vous attribuez une valeur

Retourner *this : pour composer le sens de l'affectation continue

Exemple :

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

    Date(const Date &d) {
    
    
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    Date &operator=(const Date &d) {
    
    
        if (this != &d) {
    
    
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }

        return *this;
    }

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

fonctionnalité:

1. L'opérateur d'affectation ne peut être surchargé qu'en tant que fonction membre d'une classe et ne peut pas être surchargé en tant que fonction globale

class Date {
    
    
public:
    Date(int year = 1900, int month = 1, int day = 1) {
    
    
        _year = year;
        _month = month;
        _day = day;
    }
    int _year;
    int _month;
    int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date &operator=(Date &left, const Date &right) {
    
    
    if (&left != &right) {
    
    
        left._year = right._year;
        left._month = right._month;
        left._day = right._day;
    }
    return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员

raison:

Si l'opérateur d'affectation n'est pas explicitement implémenté, le compilateur en générera un par défaut. À l'heure actuelle, si l'utilisateur implémente une surcharge d'opérateur d'affectation globale en dehors de la classe, elle entrera en conflit avec la surcharge d'opérateur d'affectation par défaut générée par le compilateur dans la classe, de sorte que la surcharge d'opérateur d'affectation ne peut être qu'une fonction membre de la classe.

3. Lorsque l'utilisateur n'implémente pas explicitement, le compilateur génère une surcharge d'opérateur d'affectation par défaut, qui est copiée octet par octet sous forme de valeur . Remarque : Les variables membres de type intégrées sont directement affectées, tandis que les variables membres de type personnalisées doivent appeler la surcharge d'opérateur d'affectation de la classe correspondante pour terminer l'affectation.

Maintenant que la fonction de surcharge d'opérateur d'affectation par défaut générée par le compilateur peut déjà effectuer une copie de valeur ordonnée en octets, dois-je encore l'implémenter moi-même ?

Ce problème est le même que celui du constructeur de copie, qui implique une copie approfondie.

Si la gestion des ressources n'est pas impliquée dans la classe, peu importe que l'opérateur d'affectation soit implémenté ; une fois que la gestion des ressources est impliquée, elle doit être implémentée.

Je suppose que tu aimes

Origine blog.csdn.net/ikun66666/article/details/132199905
conseillé
Classement