
目录
问题引入
我们知道,当我们创建了一个类,但是类中什么都没有,就称作空类。但是,事实上真的空类中就什么都没有吗?任何一个类在我们没有写东西的情况下都会生成6个默认的函数成员,包括:构造函数、析构函数、拷贝构造函数、赋值重载,以及两个取地址。
本篇中我们重点讨论,构造函数、析构函数和拷贝构造函数。
1. 6个默认成员函数
任何一个类在我们不写的情况下,都会自动生成下面6 个默认成员函数:
2. 构造函数
2.1 构造函数的概念
正常情况下,我们看这段代码的运行结果:
class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; } void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Init(2025, 07, 21); d1.Print(); return 0; }运行结果:
但是如果我们忘记了初始化,那么会不会打印出来的数值会是随机值,从而导致程序崩溃呢?
当然为了避免这种歌情况发生,C++的大佬们设计了构造函数。
构造函数不需要init函数,构造函数是一个特殊的成员函数,是在创建类对象时由编译器自动创建的,目的就是为了保证成员变量一定会有一个合适的初始值,当然构造函数在对象的生命周期只被调用一次。
2.2 构造函数的特性
但是值得注意的是,构造函数虽然名字上叫做“构造”,但是它的作用并不是开辟内存空,而仅仅是对成员变量进行初始化。
特征如下:
- 构造函数名与类名相同
- 没有返回值
- 在对象实例化时,编译器自动调用构造函数
- 构造函数可以重载
class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; } //构造函数,自动调用 Date() { _year = 1; _month = 1; _day = 1; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print(); return 0; }调试过程中就能够发现,自动调用了构造函数:
2.2.1 构造函数可以重载
class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; } //自动调用 // 1.无参构造函数 Date() { _year = 1; _month = 1; _day = 1; } // 2.带参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print(); Date d2(2025,7,25); d2.Print(); return 0; }我们来看一下运行结果:
注意:
如果是通过无参构造函数去创建对象的话,那么对象名后面不需要加括号,因为不然的话就变成了函数声明。
Date d3();这就是一个使用无参构造函数,但是后面加了括号,那么此时d3就成了一个返回类型为Date的函数声明。
这里其实可以对上面的代码进行优化,将上面那个无参构造函数和带参构造函数结合在一起,更急方便:
//全缺省参数 Date(int year = 1,int month = 1 ,int day = 1) { _year = year; _month = month; _day = day; }
这样的话就会很方便的进行自主传参:
这里要注意的是,如果没有显式的定义构造函数的时候,那么C++编译器就会自主创建一个隐式的构造函数,如果自己写的话,就不会生成了。
class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print(); return 0; }就像这里,我们并没有定义构造函数,但其实C++编译器会自动生成一个构造函数:
但是这里大家可能会有疑问,这个数据怎么这么想随机值呢,编译器真的自动创建了一个构造函数来进行初始化吗?
这里要进行知识的补充,C++将所有的变量分为两种:
- 内置类型/基本类型:int,char,double,指针......
- 自定义类型:class/struct去定义类型对象
C++默认生成构造函数对于内置型的变量不会进行处理,对于自定义类型就会进行处理。
这里的年月日是int类型的内置型,故而不进行处理,那么我们来看一看对于自定义类型会不会进行处理:
class A { public: A() { cout << "A()" << endl; _a = 0; } private: int _a; }; class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; A _aa; }; int main() { Date d1; d1.Print(); return 0; }
我们发现自定义的_aa被初始化成了0,也就证明了自定义类型是会被初始化的。
再次强调一遍,自动初始化的本质并不是编译器去给它一个合适的值,而是说编译器在对自定义类型变量初始化时,会自动(隐式调用)它的构造函数。
3. 析构函数
3.1 概念
前面我们构造函数了解了对象是怎么创建的,那么对象是怎么销毁的呢?
析构函数:与构造函数相反,销毁对象与析构函数相关。但是函数的销毁并不是直接通过析构函数,局部对象的销毁由编译器完成,而在对象销毁时会调用析构函数进行资源的清理。
3.2 特性
- 析构函数的函数名是在类名前加上字符 ~
- 没有参数、没有返回值
- 一个类只有一个析构函数。如果没有显式定义,系统会默认生成一个析构函数
- 当对象生命周期结束的时候,C++编译器自动调用析构函数
class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; } ~Date() { cout << "~Date" << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; return 0; }运行结果:
从上面的结果来看,很明显程序结束时调用了析构函数。
但是我们刚才提到了析构函数实际上是对资源的清理,但是我们观察到Date类中都是对象成员本身,这些东西是会随着函数栈帧的销毁而销毁的。那么什么东西需要资源的释放呢?一般是使用了new、malloc等关键字的对象:
class Stack { public: Stack(int capacity = 10) { _a = (int*)malloc(sizeof(int) * capacity); assert(_a); _top = 0; _capacity = capacity; } ~Stack() { free(_a); _a = nullptr; _top = _capacity = 0; } private: int* _a; int _top; int _capacity; }; int main() { Stack st; return 0; }看这段代码中的_a变量是通过malloc申请来的,所以需要析构函数来进行最后资源的释放:
~Stack() { free(_a); _a = nullptr; _top = _capacity = 0; }这里我们就可以理解为,构造函数相当于我们的初始化;析构函数就相当于Destory。
思考:
Stack st1; Stack st2;我们这里定义两个Stack类型变量,请问谁先构造,谁先析构?
通过添加监视,然后逐语句分析,我们发现编译器先初始化了st1。然后在两行语句都执行完了之后,程序要结束时,进入了析构函数,并且先析构了st2:
所以可以得到的结论:先构造的后析构。
4. 拷贝构造函数
4.1 概念
拷贝构造函数:只有一个形参,这个形参是对本类类型对象的引用(一般使用const修饰)。拷贝构造函数在使用已存在的类类型对象 创建信对象时编译器自动调用。
4.2 特性
拷贝构造函数同样也是特殊的成员函数:
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝构造函数的参数只有一个,而且必须使用引用传参,如果使用传值方式会导致无穷递归调用
这里使用这样一段代码作为说明:
class InfiniteRecursion { public: int value; // 错误的拷贝构造函数:参数是传值而非引用 InfiniteRecursion(InfiniteRecursion obj) { // 这里应该用 const InfiniteRecursion& value = obj.value; std::cout << "Copy Constructor Called! Value: " << value << std::endl; } // 普通构造函数 InfiniteRecursion(int v) : value(v) {} }; int main() { InfiniteRecursion obj1(42); InfiniteRecursion obj2 = obj1; // 这里会触发无限递归! return 0; }根据我们之前写的拷贝构造函数的概念,拷贝构造函数在使用已存在的类类型对象 创建信对象时编译器自动调用:
- 所以InfiniteRecursion obj2 = obj1; 这个时候会自动调用拷贝构造函数
- 但是这里的拷贝构造函数使用的传值方式,所以存在将obj1拷贝给形参obj这样一个步骤(此时又需要调用拷贝构造函数)
- 从此不断循环地调用下去,直至栈溢出
但是这段代码并不是运行过程中崩溃,编译器会自动识别,并直接报错:
而我们将拷贝构造函数改为传引用:
4.3 思考
那么栈的拷贝可不可以这样写:
class Stack { public: Stack(int capacity = 10) { _a = (int*)malloc(sizeof(int) * capacity); assert(_a); _top = 0; _capacity = capacity; } Stack(const Stack& st) { _a = st._a; _top = st._top; _capacity = st._capacity; } ~Stack() { free(_a); _a = nullptr; _top = _capacity = 0; } private: int* _a; int _top; int _capacity; }; int main() { Stack st1(1); Stack st2(st1); return 0; }答案:不能
因为这样使用拷贝构造函数,st1和st2指向的是同一块内存,所以在析构的时候就可能会出现同一块内存被多次释放的情况。
实际上这个问题就是典型的深拷贝和浅拷贝的问题,后面我们针对这个问题进行一个详细的讲解。
(本篇完)
















被折叠的 条评论
为什么被折叠?



