C++类构造函数&析构函数

类的构造函数

类的 构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。

  • 构造函数名与类名相同。

  • 不能有任何返回值类型,包括 void

  • 构造函数可以有多个,支持重载。

  • 可以包含初始化列表,格式为 : 成员1(参数1), 成员2(参数2),初始化列表只能出现在构造函数中。

类名::类名(形参总表) : 类名(形参总表) : 成员1(参数1), 成员2(参数2) {
// 构造函数的函数体
}

创建对象方式:

类名 对象名;          // 默认构造函数
类名 对象名(参数); // 带参数的构造函数
// 无名对象(堆分配)
类名* 对象指针 = new 类名; // 默认构造函数
类名* 对象指针 = new 类名(参数); // 带参数的构造函数

构造函数特殊性:

  • 可以使用默认参数: 构造函数可以定义带有默认参数的形式。

    类名(int 参数1 = 默认值);
  • 构造函数可以相互调用: 通过初始化列表中的委托构造调用其他构造函数。

    类名() : 类名(参数) {}
#include <iostream>

using namespace std;

class Line
{
public:
void setLength( double len );
double getLength( void );
Line(double len); // 这是构造函数

private:
double length;
};

// 构造函数
Line::Line( double len)
{
cout << "Object is being created, length = " << len << endl;
length = len;
}

void Line::setLength( double len )
{
length = len;
}

double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line(10.0);

// 获取默认设置的长度
cout << "Length of line : " << line.getLength() <<endl;
// 再次设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl;

return 0;
}
/* Output
Object is being created, length = 10
Length of line : 10
Length of line : 6
*/

使用初始化列表来初始化字段

使用初始化列表来初始化字段:

Line::Line( double len): length(len)
{
cout << "Object is being created, length = " << len << endl;
}

上面的语法等同于如下语法:

Line::Line( double len)
{
length = len;
cout << "Object is being created, length = " << len << endl;
}

初始化列表与类中赋值方式的不同参考:C++初始化列表与赋值

假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化,同理地,您可以使用上面的语法,只需要在不同的字段使用逗号进行分隔,如下所示:

C::C( double a, double b, double c): X(a), Y(b), Z(c)
{
....
}

类的析构函数

类的 析构函数是类的一种特殊的成员函数,它是在对象的生存期即将结束的时刻被自动调用的。

类名:: ~类名{
// 析构函数的函数体
}

析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数通常用于释放在构造函数或在对象生命期内获取的资源。

#include <iostream>

using namespace std;

class Line
{
public:
void setLength( double len );
double getLength( void );
Line(); // 这是构造函数声明
~Line(); // 这是析构函数声明

private:
double length;
};

// 成员函数定义,包括构造函数
Line::Line(void)
{
cout << "Object is being created" << endl;
}
Line::~Line(void)
{
cout << "Object is being deleted" << endl;
}

void Line::setLength( double len )
{
length = len;
}

double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line;

// 设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl;

return 0;
}
/* Output
Object is being created
Length of line : 6
Object is being deleted
*/

调用顺序

1、构造函数的调用顺序

基类构造函数、对象成员构造函数、派生类本身的构造函数

2、析构函数的调用顺序

派生类本身的析构函数、对象成员析构函数、基类析构函数(与构造顺序正好 相反

#include <iostream>
using namespace std;
class A
{
public:
A(){cout<<"A::constructor"<<endl;};
~A(){cout<<"A::deconstructor"<<endl;};
};
class B
{
public:
B(){cout<<"B::constructor"<<endl;};
~B(){cout<<"B::deconstructor"<<endl;};
};
class C : public A
{
public:
C(){cout<<"C::constructor"<<endl;};
~C(){cout<<"C::deconstructor"<<endl;};
private:
// static B b;
B b;
};
class D : public C
{
public:
D(){cout<<"D::constructor"<<endl;};
~D(){cout<<"D::deconstructor"<<endl;};
};

int main(void)
{
C* pd = new D();
delete pd;
return 0;
}
/* Output
----->B b
A::constructor
B::constructor
C::constructor
D::constructor
C::deconstructor
B::deconstructor
A::deconstructor

----->static B b
A::constructor
C::constructor
D::constructor
C::deconstructor
A::deconstructor
*/

static B bB类的构造函数没有被调用的原因是因为 B 类对象 b 被声明为静态成员变量。静态成员变量在类的定义中只是声明,需要在类外部进行定义和初始化。

拷贝构造函数&拷贝赋值运算符

拷贝构造函数和拷贝赋值运算符用于在对象之间进行数据的拷贝操作。它们的定义和功能有所不同。

拷贝构造函数用于创建一个新对象,该对象与另一个已存在的同类对象具有相同的值和属性。它的定义形式为: 类名(const 类名& 对象名),const不是必须的,形参为本类的对象引用。

拷贝构造函数通常用于以下情况:

  • 通过赋值或参数传递方式将一个对象的值复制给另一个对象。

  • 当函数返回一个对象时,会调用拷贝构造函数来生成一个临时副本。

  • 当对象作为参数传递给函数时,会调用拷贝构造函数来生成参数对象的副本。

class MyClass {
public:
// 拷贝构造函数
MyClass(const MyClass& obj) {
// 拷贝对象的数据
this->data = obj.data;
}

private:
int data;
};

int main() {
MyClass obj1;
// 使用拷贝构造函数创建一个新对象obj2,它与obj1具有相同的值
MyClass obj2(obj1);

return 0;
}

拷贝赋值运算符用于将一个已存在的对象的值赋给另一个已存在的对象。它的定义形式为:类名& operator=(const 类名& 对象名)。拷贝赋值运算符通常用于以下情况:

  • 在一个对象已经存在的情况下,将其赋值给另一个对象。

  • 对象在创建后需要动态地改变其值。

  • 拷贝赋值运算符实际上是对对象的状态进行了复制,它们仍然是两个独立的对象,在内存中具有不同的地址。

class MyClass {
public:
// 赋值构造函数
MyClass& operator=(const MyClass& obj) {
// 将对象的数据赋值给当前对象
this->data = obj.data;
return *this;
}

private:
int data;
};

int main() {
MyClass obj1;
MyClass obj2;
// 将obj1的值赋给obj2,调用赋值构造函数
obj2 = obj1;

return 0;
}

总结:

  • 拷贝构造函数用于创建一个新对象,并将另一个对象的值复制给该新对象。

  • 拷贝赋值运算符用于将一个已存在的对象的值赋给另一个已存在的对象。

  • 拷贝构造函数在对象创建、返回和参数传递时调用,拷贝赋值运算符在对象赋值时调用。

移动构造函数&移动赋值运算符

在C++中,移动构造函数和移动赋值运算符的引入解决了对象复制时性能低下的问题,特别是当对象包含动态分配的资源时。移动操作允许资源的所有权从一个对象转移到另一个对象,而不必进行资源的深拷贝,这些资源通常是临时资源。

  • 移动构造函数:负责将资源从一个对象“移动”到另一个对象,通常通过“偷走”资源指针而不是复制资源。

  • 移动赋值运算符:与移动构造函数类似,但用于已存在的对象之间的资源转移。

当满足以下条件时,C++会使用移动操作:

  1. 目标对象是右值(例如临时对象或返回的函数值)。

  2. 类提供了移动构造函数或移动赋值运算符。

#include <iostream>
#include <utility> // 用于 std::move

class MyClass {
private:
int* data; // 动态分配的资源
public:
// 构造函数
MyClass(int value) : data(new int(value)) {
std::cout << "构造函数被调用。值: " << *data << std::endl;
}

// 析构函数
~MyClass() {
delete data; // 释放资源
std::cout << "析构函数被调用。" << std::endl;
}

// 拷贝构造函数
MyClass(const MyClass& other) : data(new int(*other.data)) {
std::cout << "拷贝构造函数被调用。" << std::endl;
}

// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr; // 将 other 的指针设为 nullptr,转移资源所有权
std::cout << "移动构造函数被调用。" << std::endl;
}

// 拷贝赋值运算符
MyClass& operator=(const MyClass& other) {
if (this == &other) return *this; // 自赋值检查
delete data; // 释放当前对象的资源
data = new int(*other.data); // 深拷贝
std::cout << "拷贝赋值运算符被调用。" << std::endl;
return *this;
}

// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if (this == &other) return *this; // 自赋值检查
delete data; // 释放当前对象的资源
data = other.data; // 转移资源所有权
other.data = nullptr; // 将 other 的指针设为 nullptr
std::cout << "移动赋值运算符被调用。" << std::endl;
return *this;
}

// 显示当前对象的值
void show() const {
if (data)
std::cout << "值: " << *data << std::endl;
else
std::cout << "值: nullptr" << std::endl;
}
};

int main() {
MyClass obj1(10); // 构造函数被调用
MyClass obj2 = std::move(obj1); // 移动构造函数被调用

obj2.show(); // obj2 应该有值
obj1.show(); // obj1 应该是 nullptr

MyClass obj3(20); // 构造函数被调用
obj3 = std::move(obj2); // 移动赋值运算符被调用

obj3.show(); // obj3 应该有值
obj2.show(); // obj2 应该是 nullptr

return 0;
}


  • 当使用std::moveobj1的所有权转移给obj2时,obj2获得了资源,obj1被设置为nullptr

  • 同样,obj2的资源转移给obj3后,obj2被设置为nullptr

代码解析:

移动构造函数 (MyClass(MyClass&& other)): 将资源指针从other对象转移到当前对象,并将other的指针设为nullptr

移动赋值运算符 (MyClass& operator=(MyClass&& other)): 在进行自赋值检查后,释放当前对象的资源,然后将other的资源转移到当前对象,最后将other的指针设为nullptr

explicit显示声明

explicit 关键字用于修饰构造函数,防止隐式类型转换。默认情况下,构造函数是隐式的,允许通过类型转换自动生成对象。但这种隐式转换可能导致意外行为。

使用 explicit 后,构造函数必须显式调用,避免不必要的隐式转换。这通常适用于只有一个参数的构造函数。如果构造函数的其他参数都有默认值,即使有多个参数,explicit 仍然有效。

#include <iostream>

class MyClass {
public:
explicit MyClass(int x, int y = 0) : value(x + y) {}

void display() {
std::cout << "Value: " << value << std::endl;
}

private:
int value;
};

int main() {
// MyClass obj = 42; // 错误: 不能隐式转换 int -> MyClass
MyClass obj(42); // 正确: 显式调用构造函数,默认 y = 0
obj.display(); // 输出: Value: 42
return 0;
}

浅拷贝与深拷贝

浅拷贝指的是对象的复制过程仅仅拷贝对象的字段值,而不重新分配动态内存。对于成员变量是指针的类,浅拷贝会导致多个对象指向同一块内存区域。

在浅拷贝情况下,如果一个对象释放了内存,另一个对象的指针将成为野指针,导致程序崩溃或出现不可预测的行为。典型的问题是析构函数释放同一块内存多次。

深拷贝通过复制过程重新分配动态内存,使得每个对象拥有自己的内存副本,从而避免多个对象共享同一块内存。深拷贝通常通过自定义拷贝构造函数或赋值运算符来实现。

#include <iostream>
#include <cstring>

class MyClass {
public:
// 构造函数
MyClass(const char* str) {
data = new char[strlen(str) + 1]; // 动态分配内存
strcpy(data, str);
}

// 拷贝构造函数 (实现深拷贝)
MyClass(const MyClass& other) {
data = new char[strlen(other.data) + 1]; // 重新分配内存
strcpy(data, other.data);
}

// 析构函数
~MyClass() {
delete[] data; // 释放内存
}

void display() {
std::cout << "Data: " << data << std::endl;
}

private:
char* data; // 动态内存指针
};

int main() {
MyClass obj1("Hello");
MyClass obj2 = obj1; // 深拷贝,独立内存

obj1.display(); // 输出: Hello
obj2.display(); // 输出: Hello

return 0;
}

Reference

[1] C++ 类构造函数 & 析构函数: https://www.runoob.com/cplusplus/cpp-constructor-destructor.html

------------------------------- 本文结束啦❤感谢您阅读-------------------------------
赞赏一杯咖啡