七、构造函数与析构函数
- 构造函数
- 析构函数
- 调用机制
- 构造函数的调用机制
- 析构函数的调用机制
- 总结
- 构造函数分类
- 深拷贝与浅拷贝
- 浅拷贝(Shallow Copy)
- 深拷贝(Deep Copy)
- 示例
- 初始化参数列表
- 委托构造
- `default`、`delete`和 `explicit`
- `default`
- `delete`
- `explicit`
构造函数
在C++中,构造函数是一种特殊的成员函数,它用于初始化类的对象。当创建类的对象时,构造函数会被自动调用。构造函数的名字与类的名字相同,并且没有返回类型(即使是void
也没有)。
下面是一个简单的例子来说明构造函数的使用:
class MyClass {
private:int value;public:// 构造函数MyClass(int v) : value(v) { // 使用初始化列表来初始化成员变量// 构造函数的主体(在这里是空的)}// 另一个构造函数(无参构造函数)MyClass() : value(0) {// 初始化为0}// 成员函数来访问valueint getValue() const {return value;}
};int main() {// 使用带有一个参数的构造函数创建对象MyClass obj1(10);std::cout << "obj1的值: " << obj1.getValue() << std::endl; // 输出: obj1的值: 10// 使用无参构造函数创建对象MyClass obj2;std::cout << "obj2的值: " << obj2.getValue() << std::endl; // 输出: obj2的值: 0return 0;
}
在上面的例子中,MyClass
类有两个构造函数:一个接受一个整数参数,另一个不接受任何参数(称为默认构造函数)。构造函数使用初始化列表(: value(v)
)来初始化成员变量value
。
注意:
- 如果类中没有定义任何构造函数,编译器会提供一个默认的构造函数,它什么也不做(不会初始化成员变量)。但是,如果类中定义了其他构造函数,编译器就不会再提供默认构造函数了。
- 构造函数可以被重载,这意味着可以有多个构造函数,它们接受不同类型的参数或不同数量的参数。
- 构造函数可以抛出异常,但通常建议避免在构造函数中抛出异常,因为这可能导致资源泄漏或其他问题。
- 构造函数可以是虚函数(在基类中),但通常不建议这样做,因为虚函数主要用于在派生类中重写基类中的函数。构造函数在创建对象时被调用,而不是在通过指针或引用调用对象时被调用。
析构函数
在C++中,析构函数是另一个特殊的成员函数,它在对象的生命周期结束时被自动调用。析构函数的名字是在类的名字前面加上波浪符(~
)。析构函数不接受任何参数(也不能有返回类型,即使是void
),也没有参数列表。
析构函数主要用于释放对象可能占用的资源,如动态分配的内存、文件句柄、数据库连接等。
下面是一个简单的例子来说明析构函数的使用:
#include <iostream>class MyClass {
private:int* ptr;public:// 构造函数MyClass(int v) {ptr = new int(v); // 动态分配内存}// 析构函数~MyClass() {delete ptr; // 释放动态分配的内存std::cout << "MyClass对象被销毁" << std::endl;}// 成员函数来访问值int getValue() const {return *ptr;}
};int main() {MyClass obj(10);std::cout << "obj的值: " << obj.getValue() << std::endl; // 输出: obj的值: 10// 当obj离开作用域时,析构函数会被自动调用// 输出: MyClass对象被销毁return 0;
}
在上面的例子中,MyClass
类有一个指向整数的指针ptr
。在构造函数中,我们使用new
运算符动态地分配了一个整数,并将其地址赋给ptr
。在析构函数中,我们使用delete
运算符来释放这块动态分配的内存。
当obj
离开其作用域(在main
函数的末尾)时,它的析构函数会被自动调用,输出"MyClass对象被销毁",并释放了动态分配的内存。
注意:
- 析构函数不能被显式调用(即不能直接调用
obj.~MyClass()
),它们是由编译器在对象生命周期结束时自动调用的。 - 如果类中有动态分配的资源,那么应该在析构函数中释放这些资源,以避免内存泄漏。
- 析构函数可以是虚函数,这在处理基类指针指向派生类对象(多态)时非常重要。通过将基类的析构函数声明为虚函数,可以确保在删除基类指针时调用正确的析构函数(即派生类的析构函数)。
- 析构函数不应该抛出异常(除非有特殊的异常处理策略),因为如果在析构函数中抛出异常且没有被捕获,程序会被终止。
调用机制
构造与析构函数的调用机制在C++中遵循一定的规则,这些规则确保了对象在创建和销毁时的正确初始化与清理。
构造函数的调用机制
-
自动调用:
- 当在函数体、全局或命名空间作用域中定义类的对象时,构造函数会被自动调用。
- 如果类中没有定义任何构造函数,编译器会提供一个默认的无参构造函数(但如果有其他构造函数被定义,编译器则不会提供默认无参构造函数)。
-
重载:
- 构造函数可以重载,即可以有多个构造函数,它们接受不同类型的参数或不同数量的参数。
- 重载的构造函数允许以不同的方式初始化对象。
-
初始化列表:
- 构造函数可以使用初始化列表来初始化成员变量,这是一种更高效的初始化方式。
- 初始化列表在构造函数的函数体之前执行。
-
拷贝构造:
- 当使用另一个同类型的对象来初始化一个新对象时,拷贝构造函数会被调用。
- 拷贝构造函数有两种主要形式:浅拷贝和深拷贝。浅拷贝只是复制对象的指针,而深拷贝会复制指针指向的实际数据。
-
调用顺序:
- 在创建派生类对象时,首先调用基类的构造函数,然后调用派生类的构造函数。
- 如果在类定义中显式地指定了初始化列表中的基类或成员变量初始化顺序,则按照指定的顺序进行初始化。
析构函数的调用机制
-
自动调用:
- 析构函数在对象的生命周期结束时被自动调用。
- 当局部对象离开其作用域时,析构函数会被调用。
- 全局或静态对象的析构函数在
main
函数结束后调用。 - 如果使用
new
运算符在堆上动态分配的对象,则当delete
运算符被用于该对象时,析构函数会被调用。
-
调用顺序:
- 在销毁派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数。
- 析构函数的调用顺序与构造函数的调用顺序相反。
-
资源释放:
- 析构函数通常用于释放对象在生命周期中分配的资源,如动态内存、文件句柄等。
- 如果析构函数抛出异常且没有被捕获,程序会被终止。因此,析构函数中应尽量避免抛出异常。
总结
- 构造函数和析构函数是C++中用于管理对象生命周期的特殊成员函数。
- 构造函数在对象创建时自动调用,用于初始化对象;析构函数在对象销毁时自动调用,用于清理对象并释放资源。
- 构造函数可以重载,以支持不同的初始化方式;析构函数不能重载。
- 构造函数的初始化列表提供了一种高效的初始化方式;析构函数则用于释放资源并确保对象的正确销毁。
构造函数分类
构造函数在C++中扮演着初始化对象的重要角色。它们与类名相同,没有返回值,并在对象实例化时由编译器自动调用。构造函数的分类主要包括以下几种:
-
无参数构造函数(默认构造函数)
- 定义:最简单的构造函数,函数没有参数。
- 特点:
- 如果在类中未显式定义任何构造函数,编译器会自动生成一个无参的默认构造函数。
- 一旦用户显式定义了构造函数(无论是否有参数),编译器将不再自动生成默认构造函数。
- 用途:
- 为对象的成员变量提供默认值。
- 如果类中有自定义类型的成员变量,编译器生成的默认构造函数会调用这些成员的默认构造函数进行初始化。
-
有参数构造函数(重载构造函数)
- 定义:带参数的构造函数,可以根据需要为成员变量提供初始值。
- 特点:
- 可以有多个有参构造函数,只要它们的参数列表(个数、类型或顺序)不同,以实现函数重载。
- 用途:
- 在创建对象时,通过构造函数参数为成员变量设置具体的初始值。
-
拷贝构造函数(复制构造函数)
- 定义:拷贝现有对象,并以此拷贝的副本为数据创建一个新的对象。
- 形式:
ClassName(const ClassName& other);
- 特点:
- 拷贝构造函数的参数是同类型对象的常量引用。
- 如果没有显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数。
- 用途:
- 当一个对象需要以另一个对象作为初值进行初始化时,拷贝构造函数会被调用。
- 在对象赋值、函数参数传递、函数返回值等情况下,如果涉及同类型对象的复制,可能会隐式调用拷贝构造函数。
-
移动构造函数(C++11及以后版本)
- 定义:用于将一个临时对象(如右值)的资源“移动”到另一个对象,以实现资源的高效利用。
- 形式:
ClassName(ClassName&& other);
- 特点:
- 移动构造函数的参数是同类型对象的右值引用。
- 通过移动构造函数,可以避免不必要的资源复制,提高程序性能。
- 用途:
- 在处理临时对象或即将被销毁的对象时,使用移动构造函数可以避免资源的浪费。
总结:构造函数的分类主要基于其参数和用途。无参数构造函数和有参数构造函数用于对象的初始化,而拷贝构造函数和移动构造函数则用于对象的复制和移动。在编写类时,应根据实际需要选择和设计合适的构造函数。
深拷贝与浅拷贝
在C++中,深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是两种对象复制的方式,它们之间的主要区别在于如何处理对象的成员变量,特别是当成员变量是指针或引用类型时。
浅拷贝(Shallow Copy)
浅拷贝只是简单地将对象的成员变量值复制到另一个对象中。如果对象的成员变量是指针或引用,那么浅拷贝只是复制指针或引用的值,而不是指向的实际数据。因此,两个对象将指向同一块内存区域。
浅拷贝的问题在于,如果两个对象试图修改它们指向的相同内存区域的数据,可能会导致数据不一致或其他不可预测的行为。此外,如果其中一个对象删除了它指向的数据,那么另一个对象将成为一个悬挂指针(dangling pointer),指向不再有效的内存区域。
在C++中,编译器提供的默认拷贝构造函数和拷贝赋值运算符通常执行浅拷贝。
深拷贝(Deep Copy)
深拷贝会创建一个新的内存区域来存储对象的成员变量值,特别是当成员变量是指针或引用类型时。深拷贝会递归地复制对象的所有成员变量,包括指针或引用指向的实际数据。因此,两个对象将拥有各自独立的内存区域和数据副本。
深拷贝可以确保对象之间的独立性,每个对象都可以安全地修改自己的数据而不会影响其他对象。但是,深拷贝也可能导致更多的内存使用和更长的复制时间,因为需要创建新的内存区域并复制数据。
在C++中,如果需要执行深拷贝,通常需要显式地定义拷贝构造函数和拷贝赋值运算符。例如,如果类包含一个动态分配的数组作为成员变量,那么拷贝构造函数和拷贝赋值运算符应该使用new
运算符来分配新的内存区域,并逐个复制数组元素。
示例
下面是一个简单的示例,展示了浅拷贝和深拷贝的区别:
#include <iostream>
#include <cstring>class String {
private:char* data;size_t len;public:// 构造函数String(const char* str) {len = strlen(str);data = new char[len + 1];strcpy(data, str);}// 浅拷贝构造函数(不安全的)String(const String& other) {len = other.len;data = other.data; // 浅拷贝,只复制指针值}// 深拷贝构造函数(安全的)// String(const String& other) {// len = other.len;// data = new char[len + 1]; // 分配新内存// strcpy(data, other.data); // 复制数据// }// 析构函数~String() {delete[] data;}// ... 其他成员函数 ...
};int main() {String str1("Hello");String str2(str1); // 使用浅拷贝构造函数// 如果使用浅拷贝,这里将出现悬挂指针问题,因为str1在销毁时会删除其数据// 如果使用深拷贝,则每个对象都有自己的数据副本,可以安全地销毁return 0;
}
在上面的示例中,如果使用了浅拷贝构造函数,那么在str1
被销毁时,其指向的数据也会被删除。但是,由于str2
的data
成员变量指向了相同的内存区域,因此它现在成为了一个悬挂指针。为了避免这种情况,应该使用深拷贝构造函数来确保每个对象都有自己的数据副本。
初始化参数列表
初始化参数列表是在构造函数定义的开始部分使用冒号:
后跟初始化列表的形式。这种方式可以直接初始化成员变量,甚至对于const成员变量和引用成员变量,这是唯一的初始化方式。
class MyClass {
public:int x;double y;MyClass(int a, double b) : x(a), y(b) {} // 使用初始化参数列表
};
使用初始化参数列表的好处包括:
- 更高的效率:对于某些类型(如
const
成员、引用成员、类类型的成员),只能使用初始化参数列表进行初始化。 - 可以避免一些不必要的赋值操作,从而减少代码量,提高效率。
委托构造
委托构造是C++11引入的新特性,允许一个构造函数调用另一个同类的构造函数,以避免代码重复。
class MyClass {
public:int x;double y;MyClass() : MyClass(0, 0.0) {} // 委托给另一个构造函数MyClass(int a, double b) : x(a), y(b) {}
};
在这个例子中,无参数的构造函数通过委托构造调用了带有两个参数的构造函数,从而实现了成员变量的初始化。
委托构造的使用场景包括:
- 当类有多个构造函数,并且它们之间有共同的初始化逻辑时,可以使用委托构造来避免代码重复。
- 当你想要在一个构造函数中扩展另一个构造函数的行为时。
总结,初始化参数列表和委托构造都是C++中用于初始化类成员变量的有用特性,它们各有适用场景,可以帮助你编写更高效、更易于维护的代码。
default
、delete
和 explicit
default
default
关键字用于显式地要求编译器生成默认的特殊成员函数,比如默认构造函数、默认析构函数、默认拷贝构造函数、默认拷贝赋值运算符等。这对于想要编译器生成默认行为,同时又因为某些原因(比如定义了其他构造函数)导致编译器不会自动生成默认行为的情况非常有用。
例如:
class MyClass {
public:MyClass() = default; // 显式要求编译器生成默认构造函数MyClass(const MyClass&) = default; // 显式要求编译器生成默认拷贝构造函数// ...
};
delete
delete
关键字用于删除某些特殊的成员函数或者重载的函数,这意味着这些函数不能被调用,无论是显式调用还是隐式调用。这对于禁止某些操作非常有用,比如禁止拷贝。
例如:
public:MyClass(const MyClass&) = delete; // 禁止拷贝构造函数MyClass& operator=(const MyClass&) = delete; // 禁止拷贝赋值运算符// ...
};
explicit
explicit
关键字用于修饰类的一个参数的构造函数,表示该构造函数是显式的,这意味着它不能用于隐式类型转换。这主要用于防止构造函数在某些情况下被意外地用作类型转换函数。
例如:
class MyClass {
public:explicit MyClass(int x) { /* ... */ } // 显式构造函数// ...
};void func() {MyClass obj = 10; // 错误:构造函数是显式的,不能用于隐式类型转换MyClass obj2(10); // 正确:显式调用构造函数
}
综上所述,default
、delete
和explicit
是C++中用于控制类的特殊成员函数行为的三个关键字,它们分别用于显式要求编译器生成默认行为、删除某些函数以及防止隐式类型转换。