走进类和对象(中):代码世界的关键环节解读

类的默认成员函数

在C++中,当用户没有显式定义某些成员函数时,编译器会自动提供默认的实现。任何一个类,即使我们什么都不写,类中也会自动生成6个默认成员函数(你写了就不生成):

class Date {}; //空类
  1. 默认构造函数(Default Constructor): 如果类中没有定义任何构造函数,编译器将提供一个默认构造函数,它不接受任何参数,并且不会初始化类中的成员变量。如果类中有任何非静态数据成员或基类,而这些成员或基类没有默认构造函数,编译器将无法生成默认构造函数。
  2. 拷贝构造函数(Copy Constructor): 编译器将自动生成一个拷贝构造函数,用于通过已存在的对象来初始化新对象。默认的拷贝构造函数执行浅拷贝,即简单的成员复制
  3. 拷贝赋值运算符(Copy Assignment Operator): 默认的拷贝赋值运算符同样执行浅拷贝,用于将一个对象的状态复制给另一个对象。如果类包含指针或资源,可能需要自定义该运算符以实现深拷贝
  4. 析构函数(Destructor): 编译器提供的默认析构函数是无参的,它不执行任何操作。如果类包含动态分配的内存或其他需要释放的资源,应该自定义析构函数。
  5. 取地址运算符(Address-of Operator): & 运算符重载,用于获取对象的地址。通常情况下,这是编译器自动生成的,除非你有特殊需求。
  6. 取常量地址运算符(Address-of Const Operator): & 运算符用于常量对象,类似于上述取地址运算符。

C++11 引入了另外两个默认成员函数:

  1. 移动构造函数(Move Constructor): 用于从右值引用初始化对象,可以更有效地处理临时对象或不需要深拷贝的情况。
  2. 移动赋值运算符(Move Assignment Operator): 用于将一个临时对象或右值引用的状态移动到现有对象,通常用于资源转移

默认成员函数很重要,也⽐较复杂,我们要从两个⽅⾯去学习:

  • 第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
  • 第⼆:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?

构造函数

构造函数的概念

构造函数是特殊的成员函数,其主要任务并非创建对象的空间(如局部对象在栈帧创建时空间已开好),而是在对象实例化时对对象进行初始化,替代了之前在如 Stack 和 Date 类中自行编写的 Init 函数的功能,凭借自动调用的特点实现了更便捷的对象初始化操作

构造函数的特点

一、 函数名与类名相同。

class MyClass {
public:
    // 构造函数,函数名与类名MyClass相同
    MyClass() {
        // 可在此进行对象的初始化操作,比如初始化成员变量等
    }
};

二、⽆返回值。(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)

class AnotherClass {
public:
    // 正确的构造函数定义,无返回值
    AnotherClass() {
        // 执行对象的初始化操作,例如初始化成员变量等
    }
};

 

三、对象实例化时系统会⾃动调⽤对应的构造函数。  

四、构造函数可以重载。

构造函数重载举例

class Date
{
public:
	//无参
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

	//带参数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//全缺省
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	//创建对象
	// 
	// 不能这么写
	//Date d1();
	//在 C++ 中,“Data f1(); ” 这种写法会被编译器当作函数声明,即声明一个无参且返回类型为 Data 的函数。
	//但若本意是创建 Data 类对象,正确写法应是 “Data f1; ” 等形式,所以该写法有歧义,不能这么写。
	
	//无参
	Date d1;

	//带参数
	Date d2(2024, 8, 5);

	//全缺省函数
	Date d3(2024);

	return 0;
}

以前我们调用的时候都是函数名+参数这里是对象+参数因为函数在对象实例化的时候自动调用创建对象的时候边创建边调用而且没有参数的时候后面还不能加括号,加括号是存在歧义的

注意:在定义类中的函数时,需避免将无参数的函数和全缺省的函数放在一起,因为这样的组合极易产生歧义,影响程序的正确理解与运行。

这里有三种写法,你会发现全缺省是最优解写法,既可以无参,也可以带参数,前两个方法构成重载,但是最后一个方法集合了前两者的优点

以下为全缺省初始化的例子

#include<iostream>

using namespace std;

typedef int STDataType;
class stack
{
public:
	//利用构造函数初始化
	stack(int n = 4) {
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (_a == nullptr)
		{
			perror("malloc失败\n");
		}

		_capacity = n;
		_top = 0;
	}

private:
	//成员变量
	STDataType* _a;
	int _top;
	int _capacity;
};


int main() {
	stack s1;//不传参数,就开辟4个空间
	stack s2(10);//传参数,就开辟你传的参数个空间
	return 0;
}

 

五、无参的构造函数全缺省的构造函数以及我们不写编译器自动生成的构造函数都称为默认构造函数,并且默认构造函数只能有一个

初学 C++ 时,别以为只有编译器自动生成的构造函数才叫默认构造函数哦。其实以下 3 种都是:

  1. 我们不写,编译器自动生成的构造函数。
  2. 我们自己写的无参构造函数。
  3. 我们自己写的全缺省构造函数。

总之,无需传参就能调用的构造函数就是默认构造函数(无实参和函数)

补充:但实际上,如果我们在代码中写了一个既不是全缺省函数也不是无参函数的函数(它有明确的三个参数 yearmonth 和 day,并且这三个参数没有默认值设定

Data(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
     }


那么编译器就不会再生成额外的构造函数了,此时这个类中就不会有构造函数了。

六、如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显式定义编译器将不再⽣成。 

说到这,你可能会想:既然不写时编译器会自动生成构造函数,那是不是就不用自己写了?

看看以下代码:

#include<iostream>

using namespace std;
class Data
{
public:

    // 定义一个打印函数,用于输出日期信息,格式为年/月/日
    void Print()
    {
        cout << _year << "/" << _month << "/" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    // 创建一个Data类的对象d1
    // 这里虽然没有显式地编写构造函数的代码,但C++编译器会自动生成一个默认构造函数
    // 由于没有对成员变量进行初始化,所以通过d1.Print()打印出来的将是三个随机值
    Data d1;
    d1.Print();

    return 0;
}

  

最终 d1 里的年月日都是随机值

这时你可能会问:d1 对象调用编译器自动生成的构造函数后,其_year、_month、_day 还是随机值,那这构造函数有啥意义?

编译器自动生成构造函数机制如下:

  1. 对内置类型不处理。
  2. 对自定义类型,会调用其自身的默认构造函数(可以看看下面MyQueue的生成)。

总结一下:虽然在我们不写的情况下,编译器会自动生成构造函数,但是编译器自动生成的构造函数可能达不到我们想要的效果,所以大多数情况下都需要我们自己写构造函数。 

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

注意:C++把类型分成内置类型(基本类型)和⾃定义类型。内置类型就是语⾔提供的原⽣数据类型, 如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型。

编译器自动生成构造函数的例子

#include<iostream>

using namespace std;

// 定义一个Stack类,用于模拟栈的数据结构
// 这里使用typedef为int类型定义了一个别名STDataType,方便后续代码使用
typedef int STDataType;
class Stack
{
public:
    // 定义Stack类的构造函数,它接受一个可选参数n,默认值为4
    // 在构造函数内部,通过malloc函数为栈申请内存空间,用于存储数据
    // 如果申请空间失败,会输出错误信息并返回
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc申请空间失败");
            return;
        }
        _capacity = n;
        _top = 0;
    }

private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

// 然后定义一个MyQueue类,用于通过两个栈来实现一个队列的数据结构
class MyQueue
{
public:


private:
    Stack _pushst; // 用于入栈操作的栈对象
    Stack _popst;  // 用于出栈操作的栈对象
};

// 在主函数中进行相关对象的创建和操作
int main()
{

    // 创建一个MyQueue类的对象mq,这里同样没有显式编写构造函数,
    // 但编译器会根据需要自动处理相关初始化操作(如果可行的话)
    MyQueue mq;

    return 0;
}

编译器默认生成MyQueue的构造函数调用了我们之前写的Stack的构造,完成了两个成员的初始化 

析构函数

析构函数的概念

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

析构函数的特点

一、 析构函数名是在类名前加上字符~。

class Date
{
public:
	Date()// 构造函数
	{}
	~Date()// 析构函数
	{}
private:
	int _year;
	int _month;
	int _day;
};

二、⽆参数⽆返回值。(这⾥跟构造类似,也不需要加void)

class AnotherClass {
public:
    // 正确的析构函数定义,无参数无返回值
    ~AnotherClass() {
        // 执行资源清理操作,比如释放动态分配的内存等
    }
};

三、 ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。

编译器自动生成的析构函数机制:
 1、编译器自动生成的析构函数对内置类型不做处理。
 2、对于自定义类型,编译器会再去调用它们自己的默认析构函数。 

四、对象⽣命周期结束时,系统会⾃动调⽤析构函数。

五、 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。

  • Date类,其成员变量_year_month_day是内置类型,编译器自动生成的析构函数对它们不处理内置类型内存管理简单,对象结束时自动回收内存
  • 对于Myqueue类中的Stack自定义类型)成员变量,若编译器自动生成析构函数,会调用它们的析构函数来清理资源,如释放Stack类中申请的内存。

六、还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。

Myqueue类为例,虽在其析构函数~Myqueue()里没显式调用_pushst_popstStack类型)的析构函数,但对象销毁时,系统会自动调用它们的析构函数清理资源。同理,其他类如有自定义类型成员变量,对象销毁时也会自动调用其析构函数。总之,无论哪种情况,自定义类型成员变量都会自动调用析构函数来清理资源。

#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

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;
};

class Myqueue
{
public:
	~Myqueue()
	{
		cout << "~Myqueue()" << endl;
		//free(_ptr);

		// 不需要
		//_pushst.~Stack();
	}

private:
	Stack _pushst;
	Stack _popst;
	//int* _ptr;
};

int main()
{
	Date d;
	Stack st;

	Myqueue mq;

	return 0;
}

 

通过在析构函数中打印文字,观察它的调用情况 

七、 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数

  1. Date类:由于Date类可能不包含需要动态分配的资源,因此可能不需要显式定义析构函数。
  2. MyQueue类:即使MyQueue内部使用了标准库容器或其他已妥善管理资源的数据结构,通常情况下也不需要显式定义析构函数,因为这些数据结构的析构函数会正确释放资源。
  3. Stack类:如果Stack类使用了动态内存分配来存储元素,那么它需要一个自定义的析构函数来释放这些内存,防止内存泄漏

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

C和C++的比较——有效的括号算法题

拷⻉构造函数

拷贝构造函数的概念 

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

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0, 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;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2024, 11, 22);
	Date d2(d1); // 用已存在的对象d1创建对象d2

	return 0;
}

拷⻉构造的特点

一、函数重载关系

拷贝构造函数是构造函数的一个重载形式(因为拷贝构造函数的函数名也与类名相同),用于实现对象拷贝的特定功能。

class Point {
public:
    Point(int x = 0, int y = 0) : _x(x), _y(y) {} // 普通构造函数

    // 拷贝构造函数,是构造函数的重载形式
    Point(const Point& other) : _x(other._x), _y(other._y) {}

private:
    int _x;
    int _y;
};

int main() {
    Point p1(1, 2);
    // 使用普通构造函数创建对象p1

    Point p2(p1); 
    // 使用拷贝构造函数创建对象p2,这里体现了拷贝构造函数作为构造函数的重载,根据传入参数的不同(这里是一个已存在的同类对象)来调用不同的构造函数

    return 0;
}

 二、参数要求

  • 第一个参数必须是类类型对象的引用,若使用传值方式,编译器会直接报错,因为这在语法逻辑上会引发无穷递归调用。
  • 拷贝构造函数可以有多个参数,但第一个参数必须满足是类类型对象的引用这一条件,且后面的参数必须有缺省值。

const 权限缩小,阻止改变形参

拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。

要调用拷贝构造函数就需要先传参,若传参使用传值传参,那么在传参过程中又需要进行对象的拷贝构造,如此循环往复,最终引发无穷递归调用。

提醒:自定义类型的对象进行函数传参时,一般推荐使用引用传参。使用传值传参也可以,但每次传参时都会调用拷贝构造函数。 

三、 自定义类型对象的拷贝行为

C++ 规定自定义类型对象进行拷贝行为时必须调用拷贝构造函数,所以在自定义类型进行传值传参和传值返回操作时,都会调用拷贝构造函数来完成相应的拷贝操作。

 四、编译器自动生成情况

  • 未显式定义拷贝构造函数,编译器会自动生成一个拷贝构造函数。
  • 对于自动生成的拷贝构造函数:
    • 内置类型成员变量会完成值拷贝(即浅拷贝按字节逐个拷贝)。
    • 自定义类型成员变量会调用其自身的拷贝构造函数来完成拷贝。
#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;
	//	}
private:
		int _year;
		int _month;
		int _day;
	};

int main()
{
	Date d1(2024,8,24);
	Date d2(d1);

	return 0;
}

拷贝函数注释与否,调试结果相同,编译器自动生成的拷贝构造函数最终还是完成了对象的拷贝构造。

 五、根据类成员情况决定是否需显式实现

  • 像 Date 类这种成员变量全是内置类型且没有指向什么资源的情况,编译器自动生成的拷贝构造函数就可满足拷贝需求,无需显式实现拷贝构造函数。
  • 对于像 Stack 类,虽成员变量也都是内置类型,但存在如_a 这样指向资源的情况,编译器自动生成的拷贝构造函数完成的浅拷贝不符合需求,就需要自己实现深拷贝(对指向的资源也进行拷贝)。
  • 像 MyQueue 类这种内部主要是自定义类型 Stack 成员的情况,编译器自动生成的拷贝构造函数会调用 Stack 的拷贝构造函数,同样不需要显式实现 MyQueue 的拷贝构造函数。

但某些场景下浅拷贝并不能达到我们想要的效果。例如,栈(Stack)这样的类,编译器自动生成的拷贝构造函数就不能满足我们的需求了:

#include<iostream>
using namespace std;
class Stack
{
public:
	Stack(int capacity = 4)
	{
		_ps = (int*)malloc(sizeof(int) * capacity);
		_size = 0;
		_capacity = capacity;
	}
	void Print()
	{
		cout << _ps << endl;// 打印栈空间地址
	}
private:
	int* _ps;
	int _size;
	int _capacity;
};

int main()
{
	Stack s1;
	s1.Print();// 打印s1栈空间的地址
	Stack s2(s1);// 用已存在的对象s1创建对象s2
	s2.Print();// 打印s2栈空间的地址
	return 0;
}

 

浅拷贝的问题

当类的成员变量为指向经动态分配内存所获取的指针时,往往便会出现浅拷贝相关的问题。在此种情形之下,浅拷贝操作将会致使两个对象均指向同一内存地址,进而引发数据共享的状况。如此一来,一旦其中某一个对象对该内存执行删除操作,那么另一个对象后续再尝试访问此内存时,便会因内存已被释放而导致访问无效内存的情况发生,最终产生未定义行为。

而且这种情况下,还会出现对同一块空间释放多次的问题。若我们自己定义的析构函数是正确的情况下,当程序运行结束,s2栈将被析构,此时那块栈空间被释放,然后s1栈也要被析构,再次对那一块空间进行释放。

以下为深拷贝代码:

#include <iostream>
using namespace std;

class Stack
{
public:
    // 构造函数,初始化栈的容量、大小以及分配内存空间
    Stack(int capacity = 4)
    {
        _ps = (int*)malloc(sizeof(int) * capacity);
        _size = 0;
        _capacity = capacity;
    }

    // 拷贝构造函数,实现深拷贝
    Stack(const Stack& other)
    {
        // 分配与源对象相同大小的内存空间
        _ps = (int*)malloc(sizeof(int) * other._capacity);
        _size = other._size;
        _capacity = other._capacity;

        // 逐个复制源对象栈中的数据到新分配的内存空间
        for (int i = 0; i < _size; ++i)
        {
            _ps[i] = other._ps[i];
        }
    }

    // 析构函数,释放动态分配的内存空间
    ~Stack()
    {
        free(_ps);
    }

    void Print()
    {
        cout << _ps << endl; // 打印栈空间地址
    }

private:
    int* _ps;
    int _size;
    int _capacity;
};

int main()
{
    Stack s1;
    s1.Print(); // 打印s1栈空间的地址

    Stack s2(s1); // 用已存在的对象s1创建对象s2
    s2.Print(); // 打印s2栈空间的地址

    return 0;
}

⼩技巧:如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写拷⻉构造,否则就不需要。

五、返回值方式与拷贝关系

  • 传值返回会产生一个临时对象并调用拷贝构造函数来完成拷贝操作。
  • 传值引用返回时,返回的是返回对象的别名(引用),不会产生拷贝。但如果返回对象是当前函数局部域的局部对象,函数结束时该对象就会销毁,此时使用引用返回就会出现问题,其引用相当于野引用(类似野指针),所以传引用返回虽可减少拷贝,但必须确保返回对象在当前函数结束后依然存在,才能使用引用返回方式

如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回

 

以下st存在静态区,出了函数作用域不会销毁,可以引用返回 (注意返回对象的生命周期)

Stack& Func()
{
 	static Stack st;
	st.Push(1);
	st.Push(2);
	st.Push(3);
	//...

	return st;
}

int main()
{
	Stack ret = Func();
	cout << ret.Top() << endl;

	return 0;
}

赋值运算符重载

运算符重载

一、运算符重载的基本概念

  • 允许指定新含义:当运算符用于类类型的对象时,C++ 语言允许通过运算符重载的形式赋予其新的含义。
  • 必须对应重载否则报错:类类型对象使用运算符时,需转换成调用对应运算符重载,若无对应重载则会编译报错。  
#include<iostream>
using namespace std;

class Date
{
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==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

private:
		int _year;
		int _month;
		int _day;
	};

int main()
{

	Date d1(2024,11,23);
	Date d2(2024, 11, 24);
	// d1 == d2 -> d1.operator=(d2) //提高了可读性
	d1 == d2;
	return 0;
}

 如果没有对应运算符重载

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
		int _year;
		int _month;
		int _day;
	};

int main()
{

	Date d1(2024,11,23);
	Date d2(2024, 11, 24);
	d1 == d2;
	return 0;
}

二、运算符重载函数的构成

  • 特殊名字形式:运算符重载是具有特殊名字的函数,其名字由 “operator” 和后面要定义的运算符共同构成,与其他函数一样,具备返回类型、参数列表和函数体。
// 重载 + 运算符
bool operator==(const Date& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

三、参数与运算对象的关系

  • 参数个数对应:重载运算符函数的参数个数和该运算符作用的运算对象数量相同。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
  • 成员函数情况:若重载运算符函数是成员函数,其第一个运算对象默认传给隐式的 this 指针,所以作为成员函数时,参数比运算对象少一个。
#include<iostream>
using namespace std;

class Date
{
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==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

	//作为例子有缺陷
		Date operator+=(int day)
	{
			_day += day;
			return *this;
	}

	// 重载 - 运算符(一元运算符),用于计算++d1;
	// d1.operator++()
	Date& operator++()
	{
		*this += 1;
		return *this;
	}

	// 重载 ++ 运算符(二元运算符),用于计算d1++
	// d1.operator++(1)
	Date operator++(int i)
	{
		Date tmp(*this);
		*this += 1;
		return tmp;
	}


private:
		int _year;
		int _month;
		int _day;
	};

int main()
{

	Date d1(2024,11,23);
	//Date d2(2024, 11, 24);
	//// d1 == d2 -> d1.operator=(d2) //提高了可读性
	//d1 == d2;

	// 调用后置 ++ 运算符重载(成员函数),
	// 这里隐式传递了 this 指针指向的对象作为第一个运算对象
	d1++;
	d1.Print();

	// 调用前置 ++ 运算符重载(成员函数),
	// 这里隐式传递了 this 指针指向的对象作为第一个运算对
	++d1;
	d1.Print();

	return 0;
}

四、优先级与结合性及限制

  • 保持一致:运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
  • 不能创建新操作符:不能通过连接语法中没有的符号来创建新的操作符,如 “operator@”。
  • 不可重载的运算符:“.*”、“::”、“sizeof”、“?:”、“.” 这 5 个运算符不能重载,在选择题中常考需牢记。
int main() {
    MyNumber a(2);
    MyNumber b(3);
    MyNumber c(4);

    // 先计算乘法,再计算加法,与内置类型的运算符优先级和结合性一致
    MyNumber result = a + b * c; 

    return 0;
}

五、类类型参数及意义考量

  • 至少一个类类型参数:重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义。
  • 根据意义选择重载:一个类需要重载哪些运算符,要看哪些运算符重载后有意义,例如 Date 类重载 “operator-” 有意义,重载 “operator+” 可能没意义。
// 错误示例,试图改变内置类型对象的含义,但不符合重载规则
class WrongOverload {
public:
    // 以下函数定义会编译报错,因为没有类类型参数且试图改变内置类型对象的含义
    int operator+(int x, int y) {
        return x - y;
    }
};

比如 “2024 年 11 月 23 日” 加上 “2023 年 5 月 10 日”,很难想象这样相加后得到的结果应该代表什么实际的时间状态。 

六、特殊运算符重载情况

  • ++ 运算符重载:重载 “++” 运算符时,有前置 “++” 和后置 “++”,函数名都是 “operator++”,为区分后置 “++”,C++ 规定后置 “++” 重载时增加一个 int 形参,与前置 “++” 构成函数重载。
  • <<和>> 运算符重载:重载 “<<” 和 “>>” 时,需重载为全局函数,因重载为成员函数时,this 指针默认抢占第一个形参位置,不符合使用习惯和可读性,而重载为全局函数把 “ostream/istream” 放到第一个形参位置,第二个形参位置放类类型对象。

如果重载在类中如下:

重载为全局函数(存在如何访问类中private的问题):

以下用友元的方式解决访问问题: 

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

istream& operator>>(istream& in, Date& d)
{
	while (1)
	{
		cout << "请依次输入年月日:>";
		in >> d._year >> d._month >> d._day;

		if (d.CheckDate())
		{
			break;
		}
		else
		{
			cout << "日期非法,请重新输入" << endl;
		}
	}

	return in;
}

赋值运算符重载

基本概念

赋值运算符重载是一个默认成员函数,用于实现两个已存在对象间的拷贝赋值操作,要与拷贝构造函数区分开,拷贝构造是用于将一个对象拷贝初始化给另一个待创建的对象。

 特点

一、函数形式要求

  1. 它属于运算符重载,必须重载为成员函数
  2. 参数建议写成 const 当前类类型引用,若传值传参则会产生拷贝开销。

二、 返回值特点

  1. 有返回值,建议写成当前类类型引用。
  2. 引用返回可提高效率,且能支持连续赋值场景
  3. 引用返回的是*this赋值操作进行完毕时,我们应该返回赋值运算符的左操作数,而在函数体内我们只能通过this指针访问到左操作数,所以要返回左操作数就只能返回*this。

三、 编译器默认生成情况

  1. 若未显式实现赋值运算符重载,编译器会自动生成默认版本。
  2. 其行为与默认拷贝构造函数类似:对内置类型成员变量进行值拷贝 / 浅拷贝;例如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去,类似于memcpy。
  3. 自定义类型成员变量会调用其自身的赋值重载函数

四、是否需显式实现判断

  1. 对于成员变量全是内置类型且不指向资源的类(如 Date 类),编译器自动生成的赋值运算符重载可满足需求,无需显式实现。
  2. 对于成员虽为内置类型但指向资源的类(如 Stack 类),编译器自动生成的浅拷贝不符合需求,需自行实现深拷贝。
  3. 对于内部主要是自定义类型成员的类(如 MyQueue 类),编译器自动生成的赋值运算符重载会调用内部自定义类型的赋值运算符重载,通常无需显式实现;另外,若一个类显式实现了析构并释放资源,一般就需要显式编写赋值运算符重载,否则通常不需要。

以重载 = 运算符作为例子: 

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)// 构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date& operator=(const Date& d)// 赋值运算符重载函数
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

	}
private:
	int _year;
	int _month;
	int _day;
};

请注意对以下代码中所调用函数的区别哦:

Date d1(2024, 11, 24);
Date d2(d1);
Date d3 = d1;

这里有三行代码,我们现在都清楚第二行代码调用的是拷贝构造函数。那么,第三行代码调用的是哪个函数呢?它调用的也是拷贝构造函数哦,可不是赋值运算符重载函数呢。

这里要特别留意拷贝构造函数和赋值运算符重载函数各自的使用场景呀:

拷贝构造函数的作用是,使用一个已经存在的对象来对另一个即将被创建的对象进行构造并初始化。

赋值运算符重载函数则是在两个对象都已经存在的情况下,把其中一个对象的值赋给另一个对象。

 取地址运算符重载

const成员函数

const 成员函数的定义

  • 语法形式:将 const 修饰的成员函数称为 const 成员函数,其修饰方式是放在成员函数参数列表的后面。
	void Print()const// cosnt修饰的打印函数
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

const 修饰的本质及作用

  • 对 this 指针的修饰:const 实际修饰该成员函数隐含的 this 指针
  • 限制修改成员变量:表明在该成员函数中不能对类的任何成员进行修改。例如,当 const 修饰 Date 类的 Print 成员函数时,Print 隐含的 this 指针会变为 Date* const this 的形式,以此来约束函数内部不能对 Date 类的成员进行更改操作。

 const其实是修饰的this指针,至于为什么要把它写在后面,别忘了this指针不在参数中显示,否则报错

经典面试题:

问题一:const 对象能够调用非 const 成员函数吗?

问题二:非 const 对象可以调用 const 成员函数吗?

问题三:在 const 成员函数内部,能够调用其他的非 const 成员函数吗?

问题四:在非 const 成员函数内部,是否可以调用其他的 const 成员函数呢?

答案依次是:不可以、可以、不可以、可以。

具体解释如下:

对于问题一,非 const 成员函数意味着其 this 指针未被 const 修饰。当我们传入一个被 const 修饰的对象,却要用没有被 const 修饰的 this 指针去接收它时,这相当于进行了权限的放大操作,所以这种情况下函数调用会失败。

针对问题二,const 成员函数的 this 指针是被 const 修饰的。当传入一个未被 const 修饰的对象,并用被 const 修饰的 this 指针接收时,这属于权限的缩小操作,因此函数调用是能够成功的。

再看问题三,在一个被 const 修饰的成员函数当中去调用其他未被 const 修饰的成员函数,这实际上是把被 const 修饰的 this 指针的值赋给一个未被 const 修饰的 this 指针,此操作属于权限的放大,所以函数调用会失败。

最后看问题四,在一个未被 const 修饰的成员函数里面调用其他被 const 修饰的成员函数,也就是将未被 const 修饰的 this 指针的值赋给被 const 修饰的 this 指针,这属于权限的缩小操作,故而函数调用能够成功。

总结:一个成员函数,不修改成员变量的建议都都加上。

取地址运算符重载

取地址操作符重载和const取地址操作符重载,这两个默认成员函数一般不用自己重新定义,使用编译器自动生成的就行了:

class Date
{
public:
	Date* operator&()// 取地址操作符重载
	{
		return this;
	}
	const Date* operator&()const// const取地址操作符重载
	{
		return this;
	}
private:
	int _year;
	int _month;
	int _day;
};

练习——⽇期类实现

Date.h —— 函数申明

包含 日期类中所包含的成员函数和成员变量两个全局函数 的申明

#include <iostream>
#include <assert.h>

using namespace std;

class Date {
public:
    // 构造函数,可设置默认值
    Date(int year = 1900, int month = 1, int day = 1);

    // 打印函数
    void Print();

    // 检查日期是否合法的函数
    bool CheckDate();

    // 获取每个月的天数,设置为内联函数以便高频使用时提高效率
    int GetMonthDay(int year, int month);

    // 比较大小的运算符重载
    bool operator<(const Date& d);
    bool operator>(const Date& d);
    bool operator<=(const Date& d);
    bool operator>=(const Date& d);
    bool operator==(const Date& d);
    bool operator!=(const Date& d);

    // 实现 += 运算符重载,将加的数值赋值到对象自身,对象会因此改变
    Date& operator+=(int day);

    // 实现 + 运算符重载
    Date operator+(int day);

    // 实现 -= 运算符重载
    Date& operator-=(int day);

    // 实现 - 运算符重载
    Date operator-(int day);

    // 前置++ 运算符重载,返回++后的值
    Date& operator++();

    // 后置++ 运算符重载
    Date operator++(int);

    // 前置-- 运算符重载,返回--后的值
    Date& operator--();

    // 后置-- 运算符重载
    Date operator--(int);

    // 日期 - 日期,返回两个日期中间相差的天数
    int operator-(const Date& d);

    // 声明流提取运算符>>的友元函数
    friend istream& operator>>(istream& in, Date& d);

    // 声明流插入运算符<<的友元函数
    friend ostream& operator<<(ostream& out, const Date& d);

private:
    int _year;
    int _month;
    int _day;
};

// 流插入运算符<<的全局函数实现
ostream& operator<<(ostream& out, const Date& d);

// 流提取运算符>>的全局函数实现
istream& operator>>(istream& in, Date& d);

Date.cpp——构造函数

一、判断日期是否合法

首先需要检查日期的合法性,只有当日期合法时,才能进行后续的构造操作。 

// 获取某年某月的天数
inline int Date::GetMonthDay(int year, int month)
{
    // 数组存储平年每个月的天数
    static int dayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    int day = dayArray[month];
    if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
    {
        //闰年2月的天数
        day = 29;
    }
    return day;
}

 //判断日期是否非法
 bool Date::CheckDate() {
     if (
         _month < 1 || _month > 12 ||
         _day < 1 || _day > Date::GetMonthDay(_year, _month)
         )
     {
         return false;

     }
     else
     {
         return true;
     }

 }


// Date类的构造函数,在类外定义,需指定类域
Date::Date(int year, int month, int day)
{
    _year = year;
    _month = month;
    _day = day;

    // 检查日期是否合法,如果不合法则输出提示信息并打印当前对象
    if (!CheckDate())
    {
        cout << "日期非法,请重新输入->";
        cout << *this;
    }
}

GetMonthDay函数中的三个细节:
 1.该函数可能被多次调用,所以我们最好将其设置为内联函数
 2.函数中存储每月天数的数组最好是用static修饰,存储在静态区,避免每次调用该函数都需要重新开辟数组。
 3.逻辑与应该先判断month == 2是否为真,因为当不是2月的时候我们不必判断是不是闰年,加快程序运行速度。

注意:当函数声明和定义分开时,在声明时注明缺省参数,定义时不标出缺省参数。

二、打印函数

// Date类的打印函数,用于输出日期格式
void Date::Print()
{
    cout << _year << "-" << _month << "-" << _day << endl;
}

三、日期类比较大小

1. < 运算符重载

< 运算符的重载很简单,先判断年是否小于,再判断月是否小于,最后判断日是否小于,这其中有一者为真则函数返回true,否则返回false。

// 小于运算符重载,用于比较两个Date对象的大小
bool Date::operator<(const Date& d)
{
    if (_year < d._year) // 先比较年份,年小则整体小
    {
        return true;
    }
    else if (_year == d._year && _month < d._month) // 年份相同,比较月份
    {
        return true;
    }
    else if (_year == d._year && _month == d._month && _day < d._day) // 年份、月份相同,比较日
    {
        return true;
    }

    // 不满足上述小于条件,则为大于等于
    return false;
}
2. ==运算符的重载

==运算符的重载也是很简单,年月日均相等,则为真。

// 等于运算符重载,比较两个Date对象的年、月、日是否完全相同
bool Date::operator==(const Date& d)
{
    return _year == d._year && _month == d._month && _day == d._day;
}
3. <=运算符的重载 

<=, < 或者 == 是 <= 

// 小于等于运算符重载,基于小于和等于运算符重载的结果
bool Date::operator<=(const Date& d)
{
    return *this < d || *this == d;
}
4.> 运算符重载

>,小于等于的反面即是大于。

// 大于运算符重载,通过取反小于等于的结果得到大于的判断
bool Date::operator>(const Date& d)
{
    return!(*this <= d);
}
5.>=运算符的重载

>=,< 的反面即是 >=。

// 大于等于运算符重载,通过取反小于的结果得到大于等于的判断
bool Date::operator>=(const Date& d)
{
    return!(*this < d);
}
6.!=运算符的重载

!=,== 的反面即是 != 。

// 不等于运算符重载,通过取反等于的结果得到不等于的判断
bool Date::operator!=(const Date& d)
{
    return!(*this == d);
}

四、日期与天数的计算

1.日期 += 天数

在处理 += 运算符时,首先会把要增加的天数累加到日期的 “日” 部分。接着,需要检查此时的日期是否合法。要是日期不合法,就会按照如下调整思路来使其合法:

  1. 当 “日” 的数值达到或超过当前月份应有的天数时,就将 “日” 减去当前月份的天数,同时把 “月” 的值增加 1。
  2. 而一旦 “月” 的数值达到 13(意味着已满一年),那么就把 “年” 的值增加 1,并将 “月” 重新设置为 1。

会持续重复上述两个调整步骤,直至日期变得合法为止。 

// +=运算符重载,实现日期加上指定天数的功能,并更新当前对象
Date& Date::operator+=(int day)
{
    if (day < 0) // 如果要加的天数为负数,转化为 -= 操作
    {
        return *this -= -day;
    }

    _day += day;

    // 处理天数超过当月天数的情况,进行月份和年份的调整
    while (_day > GetMonthDay(_year, _month))
    {
        _day -= GetMonthDay(_year, _month);
        ++_month;

        if (_month == 13)
        {
            _year++;
            _month = 1;
        }
    }

    return *this;
}

注:当需要加的天数为负数时,转而调用-=运算符重载函数。

2.日期 + 天数

在进行 + 运算符的重载时,我们能够复用之前已经实现好的 += 运算符重载函数。

不过需要留意的是,尽管此函数返回的是经过加法运算之后的值,但实际上调用该函数的对象本身的值并不会发生改变。这就好比表达式 a = b + 1 ,其中 b + 1 这个运算会返回一个结果值(也就是 b 加上 1 的值),然而 b 自身的值在这个过程中依旧保持原样,没有任何变动。

// +运算符重载,基于 += 运算符重载实现,返回一个新的Date对象,原对象不变
Date Date::operator+(int day)
{
    Date tmp = *this; // 通过拷贝构造创建一个临时对象

    tmp += day; // 对临时对象进行 += 操作

    return tmp;
}

基于此特性,我们还可以使用 const 关键字来修饰这个 + 运算符重载函数,这样做的目的在于防止在函数内部不小心改变了由 this 指针所指向的对象的值,从而确保对象的原始状态不受影响

// 日期+天数
Date Date::operator+(int day) const
{
	Date tmp(*this);// 拷贝构造tmp,用于返回
	// 复用operator+=
	tmp += day;

	return tmp;
}

注意:在重载 += 运算符的函数中,采用的是引用返回的方式。这是因为当函数执行完毕,也就是出了函数作用域之后,this 指针所指向的那个对象依然是存在的,并没有被销毁,所以能够通过引用返回该对象,以便后续继续对其进行操作。

然而,对于 + 运算符的重载函数而言,情况就有所不同了。它的返回值只能采用传值返回的方式。原因在于,在 + 运算符重载函数内部,通常会创建一个临时对象(比如这里提到的对象 tmp),当函数执行结束,也就是出了函数作用域之后,这个临时对象 tmp 就会被销毁掉。既然它已经不存在了,自然就不能再通过引用返回的方式来返回该对象了,只能使用传值返回,将对象的值传递出去。

3.日期 -= 天数

在处理 -= 运算符时,我们首先要做的是将日期中的 “日” 减去需要减掉的天数。完成这一步之后,接着需要对得到的新日期进行合法性判断。要是发现这个日期并不合法,那就得按照特定的思路来对日期进行不断调整,直至其变得合法为止。

具体的调整思路如下:

  1. 第一步,如果经过减法运算后,“日” 的值变成了负数,那么就将 “月” 的值减 1。
  2. 第二步,要是 “月” 的值在减 1 之后变成了 0,这就意味着需要对 “年” 的值也进行调整,此时要将 “年” 的值减 1,并且把 “月” 重新设置为 12。
  3. 第三步,在完成前面两步操作之后,需要把 “日” 的值再加上当前月份所对应的天数。

就这样,反复按照上述这三个步骤来调整日期,一直到最终得到的日期是合法的为止。

// -=运算符重载,实现日期减去指定天数的功能,并更新当前对象
Date& Date::operator-=(int day)
{
    if (day < 0) // 如果要减的天数为负数,转化为 += 操作
    {
        return *this += -day;
    }

    _day -= day;

    // 处理天数小于等于0的情况,进行月份和年份的调整
    while (_day <= 0)
    {
        --_month;

        if (_month == 0)
        {
            _month = 12;
            --_year;
        }

        _day += GetMonthDay(_year, _month);
    }

    return *this;
}

注:当需要减的天数为负数时,转而调用+=运算符重载函数。

4.日期 - 天数

与 + 运算符重载的情况相类似,在对 - 运算符进行重载时,我们能够复用之前已经成功实现的 -= 运算符的重载函数。这样做可以充分利用已有的代码逻辑,提高代码的复用性和开发效率。

// -运算符重载,基于 -= 运算符重载实现,返回一个新的Date对象,原对象不变
Date Date::operator-(int day)
{
    Date tmp(*this); // 通过拷贝构造创建一个临时对象

    tmp -= day; // 对临时对象进行 -= 操作

    return tmp;
}

并且,在此过程中,也可以使用 const 关键字来对这个重载函数进行修饰,理由如上。

注意: -= 运算符重载函数用引用返回,因函数执行完出作用域时 this 指针指向对象还在。但 - 运算符重载函数得用传值返回,因为其内部 tmp 对象出函数作用域就销毁了,不能用引用返回。

5.前置 ++

前置++,我们可以复用+=运算符的重载函数。

// 前置++运算符重载,先对当前对象进行 += 1操作,然后返回更新后的对象本身
Date& Date::operator++()
{
    *this += 1;
    return *this;
}
6.后置 ++

前置 ++ 和后置 ++ 这两种运算符在形式上均为 “++”,这就导致在进行运算符重载时容易混淆。为了能够清晰地区分它们各自的运算符重载,我们采取了这样的办法:给后置 ++ 的运算符重载函数添加一个 int 型参数。

不过,在实际使用后置 ++ 运算符的时候,并不需要为这个 int 参数传入实际的参数值哦。这是因为在这里,这个 int 参数存在的主要作用就是为了和前置 ++ 的运算符重载形成区分,从而实现二者在重载时的有效辨别。

// 后置++运算符重载,先拷贝构造一个临时对象,对当前对象进行 += 1操作,然后返回临时对象(++前的值)
Date Date::operator++(int)
{
    Date tmp(*this);
    *this += 1;
    return tmp;
}

注意:后置++也是需要返回加了之前的值,只能先用对象tmp保存之前的值,然后再然对象加1,最后返回tmp对象。由于tmp对象出了该函数作用域就被销毁了,所以后置++只能使用传值返回,而前置++可以使用引用返回。

7.前置 –

前置–,我们也是可以复用前面的-=运算符的重载函数。

// 前置--运算符重载,先对当前对象进行 -= 1操作,然后返回更新后的对象本身
Date& Date::operator--()
{
    *this -= 1;
    return *this;
}
8.后置–

后置–需要注意的事项和后置++是一样的,我这里就不过多阐述了。

// 后置--运算符重载,先拷贝构造一个临时对象,对当前对象进行 -= 1操作,然后返回临时对象(--前的值)
Date Date::operator--(int)
{
    Date tmp(*this);
    *this -= 1;
    return tmp;
}

五、日期 - 日期

当进行 “日期 - 日期” 的计算时,我们要做的是求出传入的两个日期相差的天数。具体的计算思路是,不断地给较小的日期的天数加一,直至其与较大的日期相等。在这个过程中,较小日期所累加的总天数就是这两个日期差值的绝对值

// 两个日期相减运算符重载,计算两个Date对象之间相差的天数
int Date::operator-(const Date& d)
{
    Date max = *this;
    Date min = d;
    int flag = 1;

    // 通过小于运算符重载判断两个日期的大小,调整max和min
    if (*this < d)
    {
        max = d;
        min = *this;
        flag = -1;
    }

    int count = 0;
    // 通过不断递增较小的日期,直到与较大日期相等,统计递增次数得到相差天数
    while (min != max)
    {
        ++min;
        ++count;
    }

    return count * flag;
}

关于返回值的正负确定,若第一个日期大于第二个日期,就返回差值的正值;若第一个日期小于第二个日期,就返回差值的负值。在代码里,通过设定一个 flag 变量来标记返回值的正负情况,flag 为 1 时代表返回正值,flag 为 - 1 时代表返回负值。最后,只需返回总天数与 flag 相乘之后的结果就可以了。

六、流插入运算符<<的全局函数实现

// 流插入运算符<<的全局函数实现,用于输出Date对象的日期格式
ostream& operator<<(ostream& out, const Date& d)
{
    out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
    return out;
}

七、流提取运算符>>的全局函数实现

通过循环不断提示用户输入年月日,将输入值赋给d的成员变量后,用d.CheckDate()检查日期合法性,合法则跳出循环,不合法则提示重新输入。最后返回输入流引用,以支持链式输入操作。

// 流提取运算符>>的全局函数实现,用于从输入流中读取日期并赋值给Date对象,注意不能加const
istream& operator>>(istream& in, Date& d)
{
    while (1)
    {
        cout << "请一次输入年月日:";
        in >> d._year >> d._month >> d._day;

        if (d.CheckDate())
        {
            break;
        }
        else
        {
            cout << "日期非法,请重新输入" << endl;
        }
    }

    return in;
}

讨论:应该先实现 += 还是 +

一、+ 存在时实现 += 重载

1. + 操作符重载(Date Date::operator+(int day)

  • 不改变原对象,用拷贝构造创建临时对象tmp并对其天数操作,处理超出月份天数情况后传值返回,出作用域tmp会销毁。

2. += 操作符重载(Date& Date::operator+=(int day)

  • 基于已实现的 +,通过*this = *this + day实现 += 功能,返回*this。因先 + 再 += 拷贝次数多,选先 += 再 +。

二、+= 存在时实现 + 重载

1. += 操作符重载(Date& Date::operator+=(int day)

  • 按传入天数正负处理,正数直接加,负数转换为减法。处理天数超月份情况后引用返回更新后的对象。

2. + 操作符重载(Date Date::operator+(int day)

  • 用拷贝构造创建tmp,利用已实现的 += 对tmp赋值,传值返回tmp

三、总结

因拷贝次数问题选先 += 再 +

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值