1.类的默认成员函数
在 C++ 中,如果一个类没有显式定义某些成员函数,编译器会自动为该类生成默认的成员函数。以下是编译器可能会生成的默认成员函数:
默认构造函数 (`Default Constructor`)
- 如果没有为类定义任何构造函数,编译器会提供一个不带参数的默认构造函数。默认构造函数通常初始化类的成员变量为默认值(例如,数值类型为 0,指针为 `nullptr`)。
析构函数 (`Destructor`)
- 如果没有为类定义析构函数,编译器会提供一个默认的析构函数。默认析构函数负责释放类成员分配的资源,例如动态分配的内存。
拷贝构造函数 (`Copy Constructor`)
- 如果没有为类定义拷贝构造函数,编译器会提供一个默认的拷贝构造函数。默认拷贝构造函数执行成员的逐位复制(浅拷贝)。
拷贝赋值运算符 (`Copy Assignment Operator`)
- 如果没有为类定义拷贝赋值运算符,编译器会提供一个默认的拷贝赋值运算符。默认的拷贝赋值运算符执行成员的逐位赋值。
移动构造函数 (`Move Constructor`)(C++11及以后)
- 如果没有为类定义移动构造函数,且类中至少有一个非平凡的拷贝构造函数、非平凡的拷贝赋值运算符、非平凡的析构函数,或者类继承自具有非平凡移动构造函数的基类,则编译器会提供一个默认的移动构造函数。
移动赋值运算符 (`Move Assignment Operator`)(C++11及以后)
- 如果没有为类定义移动赋值运算符,且类中至少有一个非平凡的拷贝构造函数、非平凡的拷贝赋值运算符、非平凡的析构函数,或者类继承自具有非平凡移动赋值运算符的基类,则编译器会提供一个默认的移动赋值运算符。
2.构造函数
2.1 构造函数的定义
在 C++ 中,构造函数是一种特殊的成员函数,用于初始化对象的数据成员。构造函数的名称必须与类名相同,并且没有返回类型,即使是
void
也不行。当创建类的实例时,构造函数会被自动调用。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的 特点就完美的替代的了Init。
2.2 构造函数的特点
1. 函数名与类名相同。
2. 无返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
3. 对象实例化时系统会自动调用对应的构造函数。
4. 构造函数可以重载。
#include <iostream> using namespace std; class Date { public: /* // 1.⽆参构造函数 Date() { _year = 3; _month = 3; _day = 3; } */ /* //2.带参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } */ // 3.全缺省构造函数 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; }; int main() { // 如果留下三个构造中的第三个带参构造,第⼀,二注释掉,存在调用不明确 Date d1; // 调⽤默认构造函数 Date d2(2024, 7, 21); // 调⽤带参的构造函数 // 注意:如果通过⽆参构造函数创建对象时,对象后⾯不⽤跟括号,否则编译器⽆法区分这⾥是函数声明还是实例化对象 // warning C4930: “Date d3(void)”: 未调⽤原型函数(是否是有意⽤变量定义的?) Date d3(2024); d1.print(); d2.print(); d3.print(); return 0; }
其实第三个可以理解是前面两个的结合。
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
6. 无参构造函数、全缺省构造函数、编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多人会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结一下就是不传实参就可以调用的构造就叫默认构造。
7.编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决。
typedef int datatype; class Stack { public: Stack(int n = 4) { _a = (datatype*)malloc(sizeof(datatype) * n); if (_a ==nullptr) { perror("malloc fail"); return; } _top = 0; _capacity = n; } private: datatype* _a; int _capacity; int _top; }; class Myqueue { public: private: Stack push; Stack pop; }; int main() { Stack s1; Myqueue q1; return 0; }
运行后:
编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化
说明:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型, 如:int/char/double/指针等,自定义类型就是我们使用class/struct等关键字自己定义的类型
3.析构函数
3.1析构函数定义
在面向对象的编程语言中,析构函数(Destructor)是一个特殊的成员函数,它在对象生命周期结束时被自动调用,用于执行对象销毁前的清理工作。析构函数的主要作用是释放对象在生命周期内分配的资源,如动态内存、文件句柄、网络连接等。
析构函数的功能类比我们之前Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
3.2析构函数特点
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值。 (这里跟构造类似,也不需要加void)
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,系统会自动调用析构函数。
5. 跟构造函数类似,我们不写,编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。
6. 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
7. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue;但是有资源申请时,一定要自己写析构,否则会造成资源泄漏,如Stack。
8. 一个局部域的多个对象,C++规定后定义的先析构。
typedef int datatype; class Stack { public: Stack(int n = 4) { _a = (datatype*)malloc(sizeof(datatype) * n); if (_a ==nullptr) { perror("malloc fail"); return; } _top = 0; _capacity = n; } ~Stack() { free(_a); _a = nullptr; _top=_capacity = 0; } private: datatype* _a; int _capacity; int _top; }; class Myqueue { public: // 构造函数,如果没有显式定义,编译器会生成一个默认的构造函数 Myqueue() { // 构造函数可以初始化成员对象 } // 编译器默认生成的Myqueue的析构函数调用了Stack的析构,释放了Stack内部的资源 // 显式写析构,也会自动调用Stack的析构 ~Myqueue() { // 函数体为空,但是编译器会在这里自动调用成员对象push和pop的析构函数 // 不需要显式调用push.~Stack()或pop.~Stack(),编译器会自动处理 } private: Stack push; // Myqueue类包含两个Stack类的成员对象 Stack pop; // 当Myqueue对象被销毁时,这两个成员对象也会被销毁 }; int main() { Stack s1; Myqueue q1; return 0; }
调用析构后: