【C++】从理论到实践:类和对象完全指南(中)

一、类的默认成员函数

默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数

一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数:

  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • 赋值运算符重载
  • 取地址运算符重载(普通对象和 const 对象取地址)

|----------|----------|---------------|----------------|
| 函数类型 | 调用时机 | 主要作用 | 编译器默认生成条件 |
| 构造函数 | 对象创建时 | 初始化对象成员变量 | 用户未定义任何构造函数时生成 |
| 析构函数 | 对象销毁时 | 清理资源,释放内存 | 总是自动生成 |
| 拷贝构造函数 | 对象拷贝初始化时 | 用已有对象创建新对象 | 用户未定义时生成浅拷贝版本 |
| 赋值运算符重载 | 对象赋值时 | 将一个对象值赋给另一个对象 | 用户未定义时生成浅拷贝版本 |
| 取地址运算符重载 | 取对象地址时 | 返回对象地址 | 总是自动生成 |

需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解一下即可。

其次就是C++11以后还会增加两个默认成员函数, 移动构造和移动赋值,这个我们后面再讲解。

默认成员函数很重要,也比较复杂,我们要从两个方面去学习:

  • 第一:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
  • 第二:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?

二、构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象 (我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象

构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init。


2.1 特点

  • 函数名与类名完全相同

  • 没有返回类型(连 void 都没有)

  • 可以重载(多个构造函数)

  • 对象实例化时系统自动调用,不能手动调用


2.2 用法细节

  • 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器将不再生成。

  • 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数 ,都叫做默认构造函数 。但是这三个函数有且只有⼀个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。

  • 要注意很多人会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结一下就是不传实参就可以调用的构造就叫默认构造

  • 我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求 ,也就是说是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化 。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表,我们下个章节再细细讲解。

    cpp 复制代码
    class Date
    {
    private:
    	int _year;
    	int _month;
    	int _day;
    public:
    	//1. 无参构造函数
    	Date()
    	{
    		_year = 1;
    		_month = 1;
    		_day = 1;
    	}
    	//2. 全缺省构造函数
    	Date(int year = 1, int month = 1, int day = 1)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	//3. 带参构造函数
    	Date(int year, int month, int day)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	void Print()
    	{
    		cout << _year << "/" << _month << "/" << _day << endl;
    	}
    };
    
    int main()
    {
    	// 如果留下三个构造中的第⼆个带参构造,第⼀个和第三个注释掉
    	// 编译报错:error C2512 : "Date":没有合适的默认构造函数可⽤
    
    	Date d1;           // 调⽤默认构造函数
    	Date d2(2025,1,1); // 调⽤带参的构造函数
    	// 注意:如果通过⽆参构造函数创建对象时,对象后⾯不⽤跟括号,否则编译器⽆法区分这⾥是函数声明还是实例化对象
    	// // warning C4930: "Date d3(void)": 未调⽤原型函数(是否是有意⽤变量定义的? )
    	Date d3()
    	
    	d1.Print();
    	d2.Print();
    	d3.Print();
    
    	return 0;
    }

由上图可知,虽然我们显式定义了构造函数,但它不是默认构造函数,所以我们不传参数的话就用不了它。


2.3 实践分析

在C语言中,我们实现栈的初始化时,我们要主动调用 Init 函数,那么我们现在使用C++中的构造函数来自动初始化,就会事半功倍了。

从上图来看,我们并没有写 Init 函数,栈也进行了初始化,就是因为系统自动调用了栈的构造函数。

那么接下来我们来看看一个使用编译器默认生成的构造函数来进行初始化的例子:

这里我们并没有写 MyQueue 的构造函数,但是 Stack 里的变量还是被初始化了,是因为编译器默认生成 MyQueue 的构造函数调用了 Stack 的构造,完成了两个成员的初始化。

总结:大部分情况下,构造函数都需要我们自己去实现,少部分像 MyQueue 且 Stack 有默认构造时, MyQueue 自动生成的构造函数就可以用。

三、析构函数

析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,它就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能,而像Date没有 Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。

3.1 特点

  • 函数名:~类名

  • 没有参数和返回值

  • 不能重载(每个类只有一个析构函数)

  • 对象生命周期结束时,系统会自动调用,不能手动调用

3.2 用法细节

  • 跟构造函数类似,我们不写,编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用它的析构函数

  • 还需要注意的是我们显式地写析构函数,对于自定义类型成员也会调用它的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数

  • 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构就可以用,也就不需要显式写析构,如MyQueue;但是有资源申请时,一定要自己写析构,否则会造成资源泄漏,如Stack。

  • ⼀个局部域的多个对象,C++规定后定义的先析构。

    cpp 复制代码
    #include<iostream>
    using namespace std;
    
    typedef int STDataType;
    class Stack
    {
    public:
    	Stack(int n = 4)
    	{
    		_a = (STDataType*)malloc(sizeof(STDataType) * n);
    		if (nullptr == _a)
    		{
    			perror("malloc申请空间失败");
    			return;
    		}
    		_capacity = n;
    		_top = 0;
    	}
    	// 析构函数
        ~Stack()
    	{
    		free(_a);
    		_a = nullptr;
    		_capacity = _top = 0;
    	}
    private:
    	STDataType* _a;
    	size_t _capacity;
    	size_t _top;
    };
    
    class MyQueue
    {
    private:
    	Stack pushst;
    	Stack popst;
    };
    int main()
    {
    	MyQueue mq;
    
    	return 0;
    }

3.3 实践分析

下面图片中的代码与上面的一致。

从上图中我们可以看出,程序执行到结尾时,会自动调用析构函数,结果如下图:

Stack 中的数据都被清空了,虽然我们没有显式的写 MyQueue 的析构函数,但是它自动调用了 Stack 的析构函数。

下面我们来对比一下用C++和C语言实现栈的区别吧。


四、拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。

4.1 特点

  • 函数名:与类同名
  • 拷贝构造函数是构造函数的一个重载。
  • 返回值:构造函数没有返回类型

4.2 用法细节

  • C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
  • 第一个参数必须是对本类类型对象的引用 ,并且几乎总是 const 的(因为我们不希望修改源对象)后面的参数必须有缺省值。如果不使用引用,而是直接传值,那么在传参过程中又会调用拷贝构造函数,从而形成无限递归,导致栈溢出。

接下来我们讲一讲为什么要用引用,以及更深一层的逻辑。

4.2.1 引用

重新理解"值传递"的含义:

首先,我们要明白值传递到底是什么意思:

cpp 复制代码
void func(int x) 
{  // x 是值传递
    x = 100;  // 修改的是副本,不影响外面的变量
}

int main() 
{
    int a = 5;
    func(a);  // 这里会创建一个临时变量来存储 a 的值,再将 a 的值传给函数形参 x
    // a 还是 5,没有被修改
}

关键 :值传递会创建参数的完整副本

现在来看我们的问题场景:

假设我们错误地这样写拷贝构造函数:

cpp 复制代码
class Date 
{
public:
    // 错误的拷贝构造函数:按值传递
    Date(const Date d) // 注意:这里没有 &
    {   
        // 拷贝内容...
    }
};
一步一步分析会发生什么

步骤1:创建对象

cpp 复制代码
Date d1;  // 使用普通构造函数
Date d2(d1);  // 调用拷贝构造函数

步骤2:进入拷贝构造函数

当执行 Date d2(d1) 时:

步骤3:无限递归开始

  1. 调用 Date(const Date d)

  2. 参数 d1 是按值传递的

  3. 为了创建 d2 ,需要调用拷贝构造函数

文本讲解:

  1. 创建 d2 时,需要调用 Date(const Date d)

  2. 参数 d 是按值传递的,这意味着需要创建 d 的副本

  3. 要创建 d 的副本,又需要调用拷贝构造函数 Date(const Date d)

  4. 这个调用又需要创建参数的副本,再次调用拷贝构造函数...

  5. 无限递归开始!

流程图表示:

Date d2(d1);

调用 Date(const Date d) // 第1次调用

需要创建参数 d 的副本

调用 Date(const Date d) // 第2次调用(递归!)

需要创建参数 d 的副本

调用 Date(const Date d) // 第3次调用(再次递归!)

需要创建参数 d 的副本

调用 Date(const Date d) // 第4次调用...

...... 无限循环 ......

正确的解决方案:使用引用
cpp 复制代码
class Date 
{
public:
    // 正确的拷贝构造函数:传引用传递
    Date(const Date& d) // 注意这里加了 & 
    {   
        // 拷贝内容...
    }
};
为什么引用能解决问题?
  • 引用是别名:引用只是已存在对象的另一个名字,不创建新对象

  • 没有拷贝操作:传递引用时,不需要创建参数的副本

  • 避免递归:因为不需要创建副本,所以不会触发拷贝构造函数的调用

执行流程:

创建 b2

调用 Date (const Date& b) // b 是 b1 的引用

直接使用 b 初始化 b2 // 没有递归!

完成!

更深层次的理解

效率考虑:

即使没有无限递归的问题,使用引用也是更高效的:

  • 值传递:需要创建整个对象的副本,对于大对象开销很大

  • 引用传递:只传递一个地址(通常4或8字节),效率极高

const 的重要性
  • 保证不会意外修改源对象

  • 可以接受临时对象或常量对象作为参数

总结

拷贝构造函数必须使用引用参数的根本原因是为了防止无限递归。

  1. 值传递需要创建副本 → 调用拷贝构造函数 → 需要创建副本 → 无限递归

  2. 引用传递不需要创建副本 → 直接使用原对象 → 没有递归

  3. 额外好处:引用传递更高效,特别是对于大对象


  • 若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。

接下来我们详细讲一讲什么是内置类型、自定义类型、浅拷贝

4.2.2 未显式定义拷贝构造

第一部分:对内置类型成员变量完成值拷贝/浅拷贝

什么是内置类型?

内置类型就是C++语言基本的数据类型:

  • int ,float, double, char, bool等

  • 指针类型

示例一:只有内置类型的类

cpp 复制代码
#include <iostream>

class SimpleClass {
public:
    int number;
    double value;
    char character;
    
    // 我们没有定义任何构造函数
    // 编译器会自动生成:默认构造、拷贝构造、析构等
};

int main() {
    SimpleClass obj1;
    obj1.number = 42;
    obj1.value = 3.14;
    obj1.character = 'A';
    
    // 使用编译器自动生成的拷贝构造函数
    SimpleClass obj2 = obj1;  // 浅拷贝/值拷贝
    
    std::cout << "obj1: " << obj1.number << ", " << obj1.value << ", " << obj1.character << std::endl; // 输出:obj1: 42, 3.14, A
    std::cout << "obj2: " << obj2.number << ", " << obj2.value << ", " << obj2.character << std::endl; // 输出:obj2: 42, 3.14, A
    
    // 修改obj2,不会影响obj1(因为是值拷贝)
    obj2.number = 100;
    std::cout << "修改后 - obj1.number: " << obj1.number << std::endl;  // 还是42
    std::cout << "修改后 - obj2.number: " << obj2.number << std::endl;  // 变成100
    
    return 0;
}

关键理解:对于内置类型,自动生成的拷贝构造函数就是简单地把每个成员的值从一个对象复制到另一个对象。

第二部分:指针成员的浅拷贝问题
cpp 复制代码
typedef int STDataType;
class Stack
{
public:
    // 构造函数
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc申请空间失败");
            return;
        }
        _capacity = n;
        _top = 0;
    }
    // 析构函数
    ~Stack()
    {
        cout << "~Stack()" << endl;
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }
    // 注意:我们没有定义拷贝构造函数!
    // 编译器会自动生成一个浅拷贝的拷贝构造函数
private:
    // 指针成员
    STDataType* _a;
    
    size_t _capacity;
    size_t _top;
};

int main()
{
    Stack st1;
    
    // 使用自动生成的浅拷贝构造函数
    // 两个对象的 _a 指针指向同一块内存
    Stack st2(st1); // 危险!浅拷贝!
    
    // 程序结束后,由于两个对象的 _a 指针指向同一块内存,导致析构两次
    // 程序崩溃,双重释放!!!
    return 0;
}

调试我们可以看到 st1 和 st2 的_a 的地址相同,导致析构两次程序崩溃。

关键理解:对于指针成员,浅拷贝只复制指针值(地址),不复制指针指向的内容,导致多个对象共享同一块内存。

第三部分:对自定义类型成员调用其拷贝构造

那么既然使用浅拷贝不行,我们就要自己实现一个拷贝构造函数来进行深拷贝。

cpp 复制代码
Stack(const Stack& st)
{
    // 需要对 _a指向资源创建同样⼤的资源再拷⻉值
    _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
    if (nullptr == _a)
    {
        perror("malloc申请空间失败!!!");
        return;
    }
    memcpy(_a, st._a, sizeof(STDataType) * st._top);
    _top = st._top;
    _capacity = st._capacity;
}

我们在类中添加这个我们自己写的拷贝构造函数就可以进行深拷贝了。


  • 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显式实现拷贝构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型 Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显式实现 MyQueue的拷贝构造。这里还有一个小技巧,如果一个类显式实现了析构并释放资源,那么他就需要显式写拷贝构造,否则就不需要
  • 传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。

4.3 C++为什么传值传参要调用拷贝构造

学到这里有人也许会有这样一个疑问,为什么C++传值传参要调用拷贝构造呢?

假设我们用C语言实现了一个栈的结构体:

cpp 复制代码
void func(Stack st)
{
	// ...
}
int main()
{
	Stack st1;
	func(st1);
	return 0;
}

从代码中可知,将 st1 拷贝给 st ,两者的 _a 就指向了同一块资源,func 函数内部改变就会改变 st1 的值,最后 func 函数调用完毕,栈帧销毁,也会使函数外面的 st1 也销毁掉。


五、赋值运算符重载

5.1 运算符重载

  • 当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
  • 运算符重载是具有特殊名字的函数,它的名字是由 operator 和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。

5.1.1 熟悉一下运算符重载

我们通过一段代码来大概对运算符重载有一点概念。

特殊名字构成
cpp 复制代码
// 特殊名字的构成:
operator关键字 + 要重载的运算符

// 例子:
operator+      // 重载 + 运算符
operator==     // 重载 == 运算符  
operator<<     // 重载 << 运算符
operator[]     // 重载 [] 运算符
简单应用
cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
private:
	int _year;
	int _month;
	int _day;
};

bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

int main()
{
	Date d1(2024, 7, 5);
	Date d2(2024, 7, 6);
	// 运算符重载函数可以显⽰调⽤

	operator==(d1, d2);
	// 编译器会转换成
	operator==(d1, d2);
	d1 == d2;
	return 0;
}
  • 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
  • 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
	bool operator==(/*此处隐藏了this指针*/const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
};

int main()
{
	Date d1;
	Date d2(2025, 4, 3);
	d1.Print();
	d1 == d2;
	return 0;
}
  • 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
  • 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
  • .* :: sizeof ?: . 注意以上5个运算符不能重载。
  • 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。 C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。
cpp 复制代码
// 在实现 ++ 之前,我们要先实现一下 +=
// 前置++
Date& operator++()
{
	*this += 1;
	return *this;
}
// 后置++
Date operator++(int)
{
	Date tmp = *this;
	*this += 1;
	return tmp;
}
  • 重载 << 和 >> 时,,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了 "对象 << cout",不符合使用习惯和可读性。 重载为全局函数把ostream/istream放到第一个形参位置就可以了,第二个形参位置当类类型对象。
如果我们重载为成员函数时:
那这是为什么呢?

还记得上面的点提到过**"二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数",**当它是成员函数时,第一个参数是默认给的 this 指针,而我们在调用它时第一个传的参数是 cout ,第二个才要传给 this ,如图:

而我们想要正确调用的话,我们就要这要写:d1 << cout ,这样不符合我们的使用习惯。那么我们就可以重载为全局函数

如果我们重载为全局函数时:
cpp 复制代码
void operator<<(ostream& out, const Date& d)
{
	out << d._year << " " << d._month << " " << d._day << endl;
}

这样写就可以了,但是这样写我们需要将 private 给注释掉,要不然我们无法访问到私有的 _year, _month, _day 。

所以重载为全局函数有点太麻烦,我们就可以使用友元函数。

使用友元函数:

友元函数就像类的朋友一样,我们可以在类的外面访问类的私有成员。

我们只需要在类里面 写上重载函数并且在 void 前面加上 friend 就可以使用了。

cpp 复制代码
friend void operator<<(ostream& out, const Date& d)
{
	out << d._year << " " << d._month << " " << d._day << endl;
}
连续使用 cout

但是单一的打印并不能满足我们的需求,当我们使用 cout << d1 << d2 << endl; 时,就会报错,因为我们没有实现连续使用。

那么怎么实现呢?

<< 和 >> 是从左往右进行结合的,我们就可以设置返回值返回 out ,然后 out 就是 cout 作为左操作数进行再一次调用。

cpp 复制代码
friend ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << " " << d._month << " " << d._day << endl;
    return out;
}

5.2 赋值运算符重载

赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。

  • 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝。(此处传值传参不会出现无限递归)。
cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 拷贝构造
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	// 赋值重载拷贝
	void operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;

};


int main()
{
	Date d1;
	Date d2(2025, 4, 3);

	// 拷贝构造
	Date d3 = d2;
	d3.Print(); // 输出 2025 4 3

	// 赋值重载拷贝
	d1.Print(); // 输出 1 1 1
	d1 = d2;
	d1.Print(); // 输出 2025 4 3
	return 0;
}
  • 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
cpp 复制代码
// 修改后的赋值重载
Date& operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
	return *this;
}
int main()
{
	Date d1(1, 1, 1);
	Date d2(2, 2, 2);
	Date d3(2025, 4, 3);
	// 赋值重载拷贝
	d1.Print(); // 输出 1 1 1
	d2.Print(); // 输出 2 2 2
	d3.Print(); // 输出 2025 4 3
	d1 = d2 = d3;
	d1.Print(); // 输出 2025 4 3
	d2.Print(); // 输出 2025 4 3
	d3.Print(); // 输出 2025 4 3
	return 0;
}
  • 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用它的赋值重载函数。
  • 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显式实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我 们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。
  • 这里还有一个小技巧,如果一个类显式实现了析构并释放资源,那么他就需要显式写赋值运算符重载,否则就不需要。

六、检验练习题

6.1 题目一

在函数F中,本地变量a和b的构造函数(constructor)和析构函数(destructor)的调用顺序是: ( )

Class A;

Class B;

void F()

{

A a;

B b;

}

A.b构造 a构造 a析构 b析构

B.a构造 a析构 b构造 b析构

C.b构造 a构造 b析构 a析构

D.a构造 b构造 b析构 a析构

A.构造顺序是按照语句的顺序进行构造,析构是按照构造的相反顺序进行析构,因此先构造b错误

B.a析构的时机不对,对象析构要在生存作用域结束的时候才进行析构,因此先析构a错误

C.b的构造时机错误,先构造a

D.正确,构造顺序是按照语句的顺序进行构造,析构是按照构造的相反顺序进行析构

6.2 题目二

设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )

C c;

int main()

{

A a;

B b;

static D d;

return 0;

}

A.D B A C

B.B A D C

C.C D B A

D.A B D C

1、类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象

2、全局对象先于局部对象进行构造

3、局部对象按照出现的顺序进行构造,无论是否为static

4、所以构造的顺序为 c a b d

5、析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部 对象之后进行析构

6、因此析构顺序为B A D C

6.3 题目三

拷贝构造函数的特点是( )

A.该函数名同类名,也是一种构造函数,该函数返回自身引用

B.该函数只有一个参数,是对某个对象的引用

C.每个类都必须有一个拷贝初始化构造函数,如果类中没有说明拷贝构造函数,则编译器系统会自动生成一个缺省拷贝构造函数,作为该类的保护成员

D.拷贝初始化构造函数的作用是将一个已知对象的数据成员值拷贝给正在创建的另一个同类的对象

A.拷贝构造函数也是一构造函数,因此不能有返回值

B.该函数参数是自身类型的对象的引用

C.自动生成的缺省拷贝构造函数,作为该类的公有成员,否则无法进行默认的拷贝构造

D.用对象初始化对象这是拷贝构造函数的使命,故正确

相关推荐
千疑千寻~1 小时前
【C++】std::move与std::forward函数的区别
开发语言·c++
hansang_IR1 小时前
【记录】四道双指针
c++·算法·贪心·双指针
_OP_CHEN1 小时前
算法基础篇:(十二)基础算法之倍增思想:从快速幂到大数据运算优化
大数据·c++·算法·acm·算法竞赛·倍增思想
Murphy_lx1 小时前
C++ 条件变量
linux·开发语言·c++
xie0510_1 小时前
C++入门
c++
AA陈超1 小时前
ASC学习笔记0027:直接设置属性的基础值,而不会影响当前正在生效的任何修饰符(Modifiers)
c++·笔记·学习·ue5·虚幻引擎
羚羊角uou1 小时前
【C++】智能指针
开发语言·c++
杜子不疼.1 小时前
【C++】哈希表基础:开放定址法 & 什么是哈希冲突?
c++·哈希算法·散列表
代码不停1 小时前
网络原理——初识
开发语言·网络·php