C++面向对象整理(2)之构造函数 与 析构函数
注:整理一些突然学到的C++知识,随时mark一下
例如:忘记的关键字用法,新关键字,新数据结构
C++中 构造函数、析构函数、this指针
提示:本文为 C++ 中常用的 构造函数 和 析构函数 的用法和举例
一、构造函数
1、构造函数的分类,为什么要有它
构造函数(constructor)是名字与类同名,没有返回值的特殊的成员函数,可以重载。在C++中,类对象都是由构造函数驱动的,请记住这句理解。假设有个类A
,在初始化A
的对象时,如A a;A a(1,2,3)或A a =A(1,2,3); A a2 = a;
这时会对应的自动调用类的构造函数实现初始化赋值,而这三种情况分别对应三种构造函数,即默认构造函数、有参构造函数、拷贝构造函数。注意构造函数的函数体{}中可以做任何函数实现,不仅是成员变量的赋值和初始化,也可以不做任何实现。
下面详细解释这三种初始化情况分别对应调用哪种构造函数:
【1】默认构造函数(也叫无参构造)
默认构造函数是()中没有任何参数的构造函数。
默认构造函数书写方式举例:没有提供任何参数的构造函数,用于创建无参对象。
class MyClass {
public:
MyClass() {
// 初始化代码,可以什么都不做,也可以给成员赋默认初始值
}
};
对于A a;
这种类声明并初始化一个无参对象a
时,如果类中没有显式定义默认构造函数,且类的所有成员变量都可以被默认初始化(例如都是基本类型或具有默认构造函数的类型),那么编译器会自动生成一个默认的构造函数。如果类中有不能默认初始化的成员变量(比如没有默认构造函数的自定义类型),编译器则不会生成默认构造函数,此时会导致编译错误。另外注意不能括号来无参构造,即A a();
这样是错的。
【2】有参构造函数
有参构造函数是接受一个或多个参数的构造函数,用于创建有传参的对象的初始化操作。书写代码示例如下
class MyClass {
public:
MyClass(double x, double y) {
// 使用x和y进行初始化,注意这里的x和y和成员变量名字可以相同
}
};
有参构造函数也常使用初始化列表(initializer list)的形式来初始化成员变量。为了让这个例子完整,假设MyClass有两个成员变量,名为m_x和m_y,它们都是double类型,下面是将构造函数写成列表初始化的形式:
class MyClass {
private:
double m_x;
double m_y;
public:
MyClass(double x, double y) : m_x(x), m_y(y) {
// 构造函数体是空的,因为所有成员变量都已经通过初始化列表进行了初始化
//注意初始化列表也可以在这里继续写任何你想的实现
}
};
当使用下面代码,带参数的声明来初始化对象a时,
A a(1,2,3);
A a=A(1,2,3);//也可以用匿名对象显示调用来初始化
编译器会尝试调用一个与提供的参数匹配的有参构造函数。在这个例子中,编译器会寻找一个接受三个参数提供的值匹配的类型的构造函数。如果找不到这样的构造函数,或者提供的参数与任何构造函数的参数列表都不匹配,那么会导致编译错误。注意找不到也不会自动生成该有参构造函数。
被初始化后如果成员变量不是private的话,就可以使用点号.或者箭头(aptr是a的指针)来提取对应的值,如a.y; aptr->y
。
【3】拷贝构造函数
拷贝构造函数(Copy Constructor):接受一个同类型常量的引用作为参数传递的构造函数。用于复制对象。
拷贝构造函数书写示例:
class MyClass {
public:
MyClass(const MyClass& other) { // 拷贝构造函数
// 使用other来初始化新对象
}
};
当这里使用了一个已经存在的对象a来初始化一个新的对象a2的这种情况下,分别给出隐式赋值和显示赋值的方法:
A a2 = a;//隐式类型转换
A a2 = A(a);//用匿名对象显示调用来初始化
A a2 (a);//直接括号初始化
//注意A a3 = A(A(1,2,3));拷贝构造匿名对象是错误的
当使用显示、括号方法时候,编译器会调用类的拷贝构造函数。拷贝构造函数接受一个对同类对象的常量引用作为参数,如果类中没有自己显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,用于逐个复制对象的成员变量。
此外,从C++11开始,还可以通过delete关键字来禁止拷贝构造函数来防止对象被复制:
class NonCopyable {
public:
NonCopyable(const NonCopyable&) = delete; // 删除拷贝构造函数
// 其他成员函数、变量...
};
这样定义的 NonCopyable 类将不能被复制
任何尝试复制该类的对象都会导致编译错误。
2、析构函数及其调用时机
析构函数(Destructor):用于释放对象所占内存资源的,且语法名为~ClassName()
的成员函数。该函数编译器自动提供,通常不用去写,它在对象生命周期的结束时自动调用。析构函数主要用于执行与对象销毁相关的清理操作。
析构函数的调用时机通常发生在以下几种情况:
局部对象离开作用域时:在函数或代码块中定义了一个局部对象,当该对象离开其作用域(例如函数返回或代码块结束)时,其析构函数将被自动调用。
void someFunction() {
MyClass obj; // obj是一个局部对象
// 当someFunction返回时,obj的析构函数将被调用
}
全局或静态对象在程序结束时:全局对象和静态对象的析构函数在程序正常结束时调用。如果定义了多个全局或静态对象,则它们的析构函数调用顺序与它们的构造顺序相反。
static MyClass staticObj; // 静态对象
int main() {
// ... 程序执行 ...
// 当main函数返回,程序结束时,staticObj的析构函数将被调用
}
动态分配的对象被显式释放时:当使用new操作符动态分配了一个对象时,需要使用delete操作符来显式释放该对象。在delete表达式执行时,对象的析构函数将被调用。
int main() {
MyClass* ptr = new MyClass(); // 动态分配对象
// ... 使用ptr指向的对象 ...
delete ptr; // 删除对象,此时MyClass的析构函数将被调用
}
临时对象在表达式结束时:当创建了一个临时对象(例如,通过返回值或函数内的中间计算),在包含该临时对象的表达式结束时,其析构函数会被调用。
MyClass functionReturningObject() {
MyClass tempObj; // 临时对象
return tempObj; // 返回时,tempObj的析构函数不会立即调用,但离开作用域时会调用
}
int main() {
MyClass obj = functionReturningObject(); // 当这个表达式结束时,返回的临时对象的析构函数将被调用
}
在异常处理中:如果在异常处理过程中创建了一个对象,那么当异常被抛出并离开当前作用域时,该对象的析构函数会被调用。
void mightThrow() {
throw std::runtime_error("Error!");
}
int main() {
try {
MyClass obj; // 局部对象
mightThrow(); // 抛出异常,此时obj的析构函数将被调用
} catch (const std::exception& e) {
// 处理异常
}
}
使用智能指针时当使用智能指针(如std::unique_ptr或std::shared_ptr)管理对象时,智能指针会在其生命周期结束时自动调用所管理对象的析构函数。(关于智能指针)
3、拷贝构造函数的调用时机
拷贝构造函数除了使用已经初始化的对象1来拷贝给对象2的时候会调用时,还有两种情况会调用它
[1] 值传递的方式给函数参数传值
在下面例子中,当 obj
作为参数传递给 function1
函数时,会创建一个 MyClass
的临时副本,这个副本是通过拷贝构造函数创建的。
void function1(MyClass param) {
param.printValue(); // 在函数内部使用拷贝的对象
}
int main() {
MyClass obj(10); // 调用构造函数
function1(obj); // 调用拷贝构造函数,obj作为参数传递时发生拷贝
return 0;
}
[2] 以值方式返回局部的类对象
MyClass createObject(int value) {
MyClass localObj(value); // 调用构造函数创建局部对象
return localObj; // 调用拷贝构造函数(取决于优化和编译器设置)
}
int main() {
MyClass obj = createObject(20); // 这里会发生拷贝调用
obj.printValue();
return 0;
}
在这个例子中,createObject 函数创建了一个局部对象 localObj,当 localObj 作为返回值时,通常情况下会调用拷贝构造函数来创建一个返回值的副本。但,现代编译器通常使用返回值优化(RVO)或命名返回值优化(NRVO)来避免不必要的拷贝,直接构造返回的对象。如果编译器不支持这些优化,或者禁用了这些优化,那么会调用拷贝构造函数。引入了移动构造函数后的编译器可能会选择使用移动构造函数来避免不必要的拷贝(如果编译器支持且没有禁用移动语义)。
4、默认情况编译器自己生成的构造函数
1、无参(默认)构造函数
2、拷贝构造函数
3、析构函数
4、赋值符=的重载,使A a =....
能成立,所以自动生成
5、取地址符&的重载,使& a
能成立才能用指针,所以自动生成
6、const修饰的&的重载,使函数中const &传参
成立,所以自动生成
关于运算符重载先不讲,重载等号的形式为ClassName& operator=(const ClassName& other)
。必须有重载函数在才能对自定义的类的对象进行该运算符的操作。
5、拓展:移动构造 和 委托构造
移动构造函数:用于将一个对象的资源(如动态分配的内存)的所有权转移给另一个对象,而不是进行复制。这涉及指针或资源的交换,涉及左右值,这里不深入。
class MyClass {
public:
MyClass(MyClass&& other) noexcept : data(other.data) { // &&就是右值
other.data = nullptr; // 防止悬挂指针
}
private:
int* data;
};
请注意,explicit关键字可以防止不期望的隐式类型转换。
委托构造函数:
一个构造函数调用同一个类的另一个构造函数来执行初始化。这从C++11开始支持。
总结
以下是一个Point类的定义,包括默认构造、有参构造、拷贝构造和析构函数:
class Point {
private:
double x;
double y;
double z;
public:
// 默认构造函数
Point() {
x = 0;
y = 0;
z = 0;
std::cout << "默认构造函数被调用" << std::endl;
}
// 有参构造函数
Point(double x_val, double y_val, double z_val) {
x = x_val;
y = y_val;
z = z_val;
std::cout << "有参构造函数被调用" << std::endl;
}
// 拷贝构造函数
Point(const Point& other) {
x = other.x;
y = other.y;
z = other.z;
std::cout << "拷贝构造函数被调用" << std::endl;
}
// 析构函数
~Point() {
std::cout << "析构函数被调用" << std::endl;
}
// 获取坐标
double getX() const { return x; }
double getY() const { return y; }
double getZ() const { return z; }
// 设置坐标
void setX(double x_val) { x = x_val; }
void setY(double y_val) { y = y_val; }
void setZ(double z_val) { z = z_val; }
// 输出坐标
void print() const {
std::cout << "(" << x << ", " << y << ", " << z << ")" << std::endl;
}
};
下面是初始化列表的形式:
class Point {
private:
double x, y, z;
public:
// 默认构造函数
Point() : x(0), y(0), z(0) {
std::cout << "默认构造函数被调用" << std::endl;
}
// 有参构造函数
Point(double x_val, double y_val, double z_val) : x(x_val), y(y_val), z(z_val) {
std::cout << "有参构造函数被调用" << std::endl;
}
// 拷贝构造函数
Point(const Point& other) : x(other.x), y(other.y), z(other.z) {
std::cout << "拷贝构造函数被调用" << std::endl;
}
// 析构函数
~Point() {
std::cout << "析构函数被调用" << std::endl;
}
// 获取坐标
double getX() const { return x; }
double getY() const { return y; }
double getZ() const { return z; }
// 设置坐标
void setX(double x_val) { x = x_val; }
void setY(double y_val) { y = y_val; }
void setZ(double z_val) { z = z_val; }
// 输出坐标
void print() const {
std::cout << "(" << x << ", " << y << ", " << z << ")" << std::endl;
}
};