C++六大默认成员函数

1. 六大默认成员函数

在这里插入图片描述

默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数。
我们需要从从两方面入手:
第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
第⼆:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?

2. 构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使⽤的局部对象在栈帧创建时,空间就开好了),⽽是对象实例化时初始化对象。构造函数的本质是要替代我们以前StackDate类中写的Init函数的功能,构造函数⾃动调⽤的特点就完美的替代的了Init
构造函数有如下特点:
1. 函数名与类型同名
2. 可以重载
3. 没有返回值(不用写void
4. 如果用户没有显式写构造函数,编译器会生成一个默认的无参构造函数,⼀旦⽤⼾显
式定义编译器将不再⽣成。

// 构造函数
// 1. 函数名和类名同名 2. 可以重载 3. 没有返回值 4. 用户不写编译器会默认生成无参的构造函数
class Date
{
public:
	// 无参构造
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

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

	//// 全缺省构造
	//Date(size_t year = 1, size_t month = 1, size_t day = 1)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}
	//
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	size_t _year;
	size_t _month;
	size_t _day;
};

int main()
{
	// 调用带参数的构造
	Date d1(2025,7,5);
	d1.Print();

	//// 无参构造和全缺省构造会产生调用歧义
	//Date d2;
	//d2.Print();

	// 无参的不能这么写 会和函数声明搞混 eg: void func
	// 这是函数声明还是函数定义呢?
	/*Date d2();
	d2.Print();*/

	//// 如果注释掉无参的构造和全缺省构造,会报错
	//// C2512 没有合适的默认构造函数可用
	//Date d2;
	//d2.Print();

	// 调用无参的构造函数
	Date d3;
	d3.Print();

	return 0;

}

默认构造函数分为三类:

  • 全缺省构造函数
  • 无参构造函数
  • 编译器默认生成的构造函数
    总结一下:不传参的构造函数就是默认构造函数,这三个函数不能同时存在
    而全缺省构造函数和无参构造函数虽然构成函数重载,但是调用时会产生调用歧义
    我们不显式写构造函数,编译器默认生成的构造函数会如何处理数据?
using namespace std;
class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}

private:
	size_t _hour;
	size_t _minute;
	size_t _second;
};

class Date
{
public:
	// 不写构造函数 编译器会自动生成默认构造函数
	// 对于内置类型 编译器是否处理没有明确要求
	// 对于自定义类型 调用该类型的默认构造函数

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	size_t _year;	
	size_t _month;
	size_t _day;
	Time _t;
};

int main()
{
	Date d1;
	d1.Print();
	
	return 0;
}

在这里插入图片描述

观察调试结果,我们可以得到如下结论:
对于编译器默认生成的构造函数,处理不同类型数据有不同行为:

  • 对于内置类型,编译器没有特别要求,对于VS环境,给出随机值
  • 对于自定义类型,该类型会调用它默认的构造函数
    如果把Time类的无参构造函数注释掉,会有如下现象:
    在这里插入图片描述

Time类调用它的默认构造函数,而Time类的默认构造函数是编译器生成的,又是处理内置类型,所以VS不做处理,给出随机值
针对这个问题C++11打了个补丁:内置类型成员变量在声明时给缺省值,用缺省值初始化

using namespace std;
class Time
{
public:
	/*Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}*/

private:
	// C++11 在声明时给缺省值
	size_t _hour = 1;
	size_t _minute = 1;
	size_t _second = 1;
};

class Date
{
public:
	// 不写构造函数 编译器会自动生成默认构造函数
	// 对于内置类型 编译器是否处理没有明确要求
	// 对于自定义类型 调用该类型的默认构造函数

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	// C++11 在声明时给缺省值
	size_t _year = 1;	
	size_t _month = 1;
	size_t _day = 1;
	Time _t;
};

int main()
{
	// 此时 Time类和Date类只有编译器默认生成的构造函数
	Date d1;
	d1.Print();
	
	return 0;
}

![[Pasted image 20250707095835.png]]

总结:什么时候要显式定义构造函数?

  • 一般情况构造函数都要显式实现
  • 只有成员全为自定义类型的类不用显式实现

3. 析构函数

析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放工作。析构函数的功能类⽐我们之前Stack实现的Destroy功能,⽽像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的
析构函数有如下特点:
1. 函数名和类名相同,在函数名前加~
2. 没有返回值
3. 不能重载,意味着一个类只有一个析构函数
4. 如果用户没有显式写,编译器会默认生成析构函数
5. 对象的生命周期结束,编译器自动调用析构函数

class Stack
{
public:
	Stack(size_t n = 4)
	{
		cout << "Stack(size_t n = 4) 析构" << endl;
		_arr = (int*)malloc(sizeof(int) * n);
		if (_arr == nullptr)
		{
			perror("malloc err!");
			return;
		}

		_capacity = n;
		_top = 0;
	}

	~Stack()
	{
		cout << "~Stack() 析构" << endl;
		assert(_arr);
		free(_arr);
		_arr = nullptr;
		_capacity = _top = 0;
	}

private:
	int* _arr;
	int _capacity;
	int _top;
};

int main()
{
	Stack st1;

	return 0;
}

和构造函数一样,如果我们不显式实现析构函数,编译器生成的析构函数对于内置类型不做处理,对于定义类型会调用它的析构函数,值得一提的是,是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数

class tmp
{
public:
	~tmp()
	{
		cout << "~tmp() 析构" << endl;
	}
private:
	int _num;
};
class Stack
{
public:
	Stack(size_t n = 4)
	{
		cout << "Stack(size_t n = 4) 构造" << endl;
		_arr = (int*)malloc(sizeof(int) * n);
		if (_arr == nullptr)
		{
			perror("malloc err!");
			return;
		}

		_capacity = n;
		_top = 0;
	}

	/*~Stack()
	{
		cout << "~Stack() 析构" << endl;
		assert(_arr);
		free(_arr);
		_arr = nullptr;
		_capacity = _top = 0;
	}*/

private:
	int* _arr;
	int _capacity;
	int _top;
	tmp _t;

};

int main()
{
	Stack st1;

	return 0;
}

我们可以通过调试观察:
在这里插入图片描述

总结:什么时候需要显式实现析构函数?

  • 有资源需要清理,就必须写析构函数,例如:Stack List
  • 无资源要清理,可以不写
  • 内置类型成员没有资源要清理,剩下全是自定义类型,可以不写
    还有一个重要的点:一个局部域的多个对象,后定义的先析构
设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )
C c;
int main()
{
	A a;
	B b;
	static D d;
	return 0;
}
  1. 类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象
  2. 全局对象先于局部对象进行构造
  3. 部对象按照出现的顺序进行构造,无论是否为static
  4. 所以构造的顺序为 c a b d
  5. 析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部 对象之后进行析构
  6. 因此析构顺序为B A D C

4. 拷贝构造函数

拷贝构造函数的第一个参数是自身类型的引用,且任何额外的参数都有缺省值,这样的函数叫做拷贝构造函数,用于同类对象的拷贝初始化,是构造函数的重载。
本文以最常规情况的拷贝构造函数展开,即有且仅有一个参数:类类型对象的引用
拷贝构造函数有如下特点:

  • 拷贝构造函数是构造函数的一个重载
  • 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器会报错(会引发无穷递归调用),拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值
// 拷贝构造函数
// 构造函数的重载,第一个参数必须是类类型对象的引用
// 用于同类对象的拷贝初始化

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

	Date(Date& d)
	{
		cout << "call Date(Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

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

private:
	size_t _year;
	size_t _month;
	size_t _day;
};

int main()
{
	Date d1;
	// 两种写法都可以
	Date d2 = d1;
	// d是d1的别名,d3是this指针
	Date d3(d1);

	d1.Print();
	d2.Print();
	d3.Print();

	return 0;
}

再来看一段代码:

Date(Date& d)
{
	cout << "call Date(Date& d)" << endl;
	// 如果不小心写反了会发生什么?
	d._year = _year;
	d._month = _month;
	d._day = _day;
}

其余部分不变
在这里插入图片描述

初始的d1也被修改成随机值了,我们进行拷贝构造,提供拷贝值的对象是不能被修改的,所以为了防止这样的情况发生,我们做如下处理:Date(const Date& d)保证d的只读性

// 拷贝构造函数
// 构造函数的重载,第一个参数必须是类类型对象的引用
// 用于同类对象的拷贝初始化

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

	Date(const Date& d)
	{
		cout << "call Date(Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

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

private:
	size_t _year;
	size_t _month;
	size_t _day;
};

int main()
{
	Date d1;
	// 两种写法都可以
	Date d2 = d1;
	// d是d1的别名,d3是this指针
	Date d3(d1);

	d1.Print();
	d2.Print();
	d3.Print();

	return 0;
}
  • 自定义类型对象进行拷贝行为必须调用拷贝构造,所以自定义类型传值传参和传值返回都会调用拷贝构造
// 自定义类型对象进行拷贝行为必须调用拷贝构造
// 自定定义类型传值传参和传值返回都会调用拷贝构造完成
class Date
{
public:
	Date(size_t year, size_t month, size_t day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		cout << "调用 Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	size_t _year;
	size_t _month;
	size_t _day;
};
void func(Date d)
{
	d.Print();
}

int main()
{
	Date d1(2025, 7, 9);
	func(d1);
	return 0;
}

在这里插入图片描述

调试看一下函数行为:

而传指针和传引用可避免拷贝构造:
在这里插入图片描述
在这里插入图片描述

C++推荐使用传引用的方式,因为引用语义更清晰、不能为 null、更安全也更简洁,适合绝大多数函数参数传递场景,除非参数可以为空或需要修改指针本身,否则优先使用引用传参
为什么传值会引发无穷递归?
在这里插入图片描述

  • 如果不显式写拷贝构造,编译器会默认生成拷贝构造,⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤它的拷⻉构造
// 如果不显式写拷贝构造,编译器会默认生成拷贝构造,
// 自动⽣成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),
// 对自定义类型成员变量会调用它的拷贝构造
class Stack
{
public:
	Stack(size_t n = 4)
	{
		// cout << "Stack(size_t n = 4) 构造" << endl;
		_arr = (int*)malloc(sizeof(int) * n);
		if (_arr == nullptr)
		{
			perror("malloc err!");
			return;
		}

		_capacity = n;
		_top = 0;
	}

	void Push(int data)
	{
		_arr[_top++] = data;
	}

	~Stack()
	{
		// cout << "~Stack() 析构" << endl;
		assert(_arr);
		free(_arr);
		_arr = nullptr;
		_capacity = _top = 0;
	}

private:
	int* _arr;
	int _capacity;
	int _top;
};

int main()
{
	Stack st1;
	st1.Push(1);
	st1.Push(1);
	st1.Push(1);

	Stack st2 = st1;

	return 0;
}

在这里插入图片描述

崩溃了
在这里插入图片描述

完成了拷贝,程序却崩溃了,为什么?我们没有显式写拷贝构造,默认生成的拷贝构造调用栈对象的拷贝构造,进行了浅拷贝。
![[Pasted image 20250709174150.png]]

而深拷贝,会复制一个相同的空间,这样就不会冲突
![[Pasted image 20250709175310.png]]

class Stack
{
public:
	Stack(size_t n = 4)
	{
		// cout << "Stack(size_t n = 4) 构造" << endl;
		_arr = (int*)malloc(sizeof(int) * n);
		if (_arr == nullptr)
		{
			perror("malloc err!");
			return;
		}

		_capacity = n;
		_top = 0;
	}

	// 深拷贝
	// Stack st2 = st1;
	Stack(const Stack& st)
	{
		_arr = (int*)malloc(sizeof(int) * st._capacity);
		if (_arr == nullptr)
		{
			perror("malloc err!");
			return;
		}

		memcpy(_arr, st._arr, sizeof(int)* st._capacity);

		_capacity = st._capacity;
		_top = st._top;
	}

	void Push(int data)
	{
		_arr[_top++] = data;
	}

	~Stack()
	{
		// cout << "~Stack() 析构" << endl;
		assert(_arr);
		free(_arr);
		_arr = nullptr;
		_capacity = _top = 0;
	}

private:
	int* _arr;
	int _capacity;
	int _top;
};

int main()
{
	Stack st1;
	st1.Push(1);
	st1.Push(1);
	st1.Push(1);

	Stack st2 = st1;

	return 0;
}

![[Pasted image 20250709175225.png]]

完成深拷贝,二者有自己的独立空间

总结:什么时候需要显式写拷贝构造?

  1. 若无资源管理,不用显式写拷贝构造,eg:Date
  2. 若类的成员变量都是自定义类型,并且内置类型的的成员没有指向的资源,不用显示写拷贝构造
    tips: 不显式写析构,就不用写拷贝
  3. 若类内部有指针或者一些值指向资源,要写析构释放,就需要显式写深拷贝,eg:Stack

5. 赋值重载

5.1. 运算符重载

int main()
{
	int num1 = 10;
	int num2 = 30;
	int mul = num1 * num2;
	int plus = num1 + num2;

	return 0;
}

对于内置类型,使用操作符会调用具体的指令
在这里插入图片描述

对于自定义类型,我们需要使用函数自己控制运算符的作用,这里就引入了运算符重载,格式如下:
返回类型 operator操作符(参数列表),这就是函数,只不过函数名是operator操作符
运算符重载有以下注意事项:

  1. 不能创造新的操作符,例如C++不支持@这个符号
  2. 重载操作符必须有一个类类型的参数 void operator-(int a, int b)是不行的
// 参数类型必须要有一个是类类型
// “operator -”必须至少有一个类类型的形参
void operator-(int a, int b){}

![[Pasted image 20250720145107.png]]

  1. 有五个操作符不能重载:.* . sizeof ?: ::
    .*这个运算符的场景很少见:
// .*不能重载
class A
{
public:
	void func()
	{
		cout << "A::func()" << endl;
	}
};
typedef void(A::* PF)(); //成员函数指针类型

int main()
{
	// C++规定成员函数要加&才能取到函数指针
	PF pf = &A::func;

	A obj;//定义ob类对象temp

	// 对象调⽤成员函数指针时,使⽤.*运算符
	(obj.*pf)();

	return 0;
}

我们看个案例:

// 自定义类型呢?

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	~Date()
	{
		_year = -1;
		_month = -1;
		_day = -1;
	}
// private:
	int _year;
	int _month;
	int _day;
};
bool operator>(const Date& d1, const Date& d2)
{
	if (d1._year > d2._year)
	{
		if (d1._month > d2._month)
		{
			if (d1._day > d2._day)
			{
				return true;
			}
		}
	}
	else
	{
		return false;
	}
}

int main()
{
	Date d1(2025, 7, 17);
	Date d2(2025, 7, 20);
	// 转换成调用 operator>(d1, d2)
	cout << (d1 > d2) << endl;
	// 可以显式调用
	cout << operator>(d1, d2) << endl;

	return 0;

}

不推荐显式调用,因为这样就突出不了运算符重载的优势了
![[Pasted image 20250720145708.png]]

注意看:private被我注释掉了,此时数据是公开访问的
![[Pasted image 20250720145837.png]]

而如果数据被private保护,我们如何重载运算符呢?这里提供三种方法

  1. 实现私有成员的Get Set函数,Java使用这个方法偏多
//eg:
int GetYear()
{
	return _year;
}
  1. 使用友元函数(这个会在后文中详细讲)
  2. 重载成类的成员函数,这里要注意,重载成成员函数,会默认多一个this,要调整参数个数
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	~Date()
	{
		_year = -1;
		_month = -1;
		_day = -1;
	}
	bool operator>(const Date& d2)
	{
		if (this->_year > d2._year)
		{
			if (this->_month > d2._month)
			{
				if (this->_day > d2._day)
				{
					return true;
				}
			}
		}
		else
		{
			return false;
		}
	}

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

int main()
{
	Date d1(2025, 7, 17);
	Date d2(2025, 7, 20);
	// 转换成调用 operator>(d1, d2)
	cout << (d1 > d2) << endl;
	// 可以显式调用
	cout << d1.operator>(d2) << endl;

	return 0;
}

5.2. 赋值运算符重载

赋值运算符重载用于一个已经存在的对象拷贝复制给另一个已经存在的对象,这里要和拷贝构造函数区分开。以下是赋值运算符重载的特点:

  1. 赋值运算符重载是一个运算符重载,规定必须重载为成员函数,参数建议写成const 当前类类型引用,否则会传值传参产生拷贝
  2. 有返回值,建议写成当前类类型引用,引用返回可以减少拷贝,提高效率,有返回值的目的是为了支持连续赋值的场景
    对于内置类型:
    在这里插入图片描述

而对于自定义类型:

class Date
{
public:
	// 构造
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 拷贝构造
	Date(const Date& d)
	{
		cout << "Date(const Date & d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	// 赋值运算符重载
	Date& operator=(const Date& d)
	{
		// 防止自己给自己赋值
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		// 返回值是Date类
		return *this;
	}
	// 析构 
	~Date()
	{
		_year = _month = _day = -1;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2025, 7, 20);
	Date d2(2024, 7, 28);
	// 拷贝构造
	Date d3 = d1;
	// 赋值运算符重载
	d1 = d2 = d3;

	d1.Print();
	d2.Print();
	d3.Print();
}

![[Pasted image 20250720152705.png]]

如果返回值为空,就会报错:

	// 二元“=”: 没有找到接受“void”类型的右操作数的运算符(或没有可接受的转换)`
	d1 = d2 = d3;

![[Pasted image 20250720152925.png]]

我们现在来着重讨论传值返回和传引用返回的区别:

// 传值返回(非优化版本,强制拷贝构造)
Date func()
{
	Date d1;
	Date d2;

	if (time(0) % 2)
	{
		return d1;  // 编译器无法预测 取消RVO优化
	}
	else
	{
		return d2;
	}
}

int main()
{
	const Date& ref = func();
	return 0;
}

![[Pasted image 20250720160957.png]]

看一下具体行为(非优化版本):

传值返回

画一个函数栈帧理解一下(优化版本):

┌──────────────────────────────┐
│         main()               │
│ ┌──────────────────────────┐ │
│ │  ref = func()            │ │
│ │                          │ │
│ │  ┌─────────────────────┐ │ │
│ │  │     func()          │ │ │
│ │  │ ┌───────────────┐   │ │ │
│ │  │ │  Date d;      │   │ │ │
│ │  │ │ (构造#1)      │   │ │ │
│ │  │ └───────────────┘   │ │ │
│ │  │                     │ │ │
│ │  │ ┌───────────────┐   │ │ │
│ │  │ │ return d;     │   │ │ │
│ │  │ │ => 拷贝构造#2  │   │ │ │
│ │  │ └───────────────┘   │ │ │
│ │  │                     │ │ │
│ │  │ d 析构 (析构#1)      │ │ │
│ │  └─────────────────────┘ │ │
│ │                          │ │
│ │ ┌──────────────────────┐ │ │
│ │ │  const Date& ref     │ │ │
│ │ │  绑定返回值的临时      │ │  │
│ │ │  (有时会额外复制)    │ │  │
│ │ │  => 析构#2, 析构#3    │ │ │
│ │ └──────────────────────┘ │ │
│ └──────────────────────────┘ │
└──────────────────────────────┘

总结:

[func 调用栈]
┌───────────────┐
│ Date d;       │  ← 局部变量 d
│               │
│ return d;     │  ← 拷贝构造!临时#1
└───────────────┘
           │
           ▼
[main 调用栈]
┌───────────────┐
│ const Date& ref; │  ← 绑定返回值临时
└───────────────┘

析构:
1️⃣ 局部 d 析构(离开 func)
2️⃣ 返回值临时析构(ref 生命周期结束)
可能还有:
3️⃣ 有的编译器生成额外延续临时,也会析构

C++ 值返回需要拷贝,RVO 省掉拷贝;const& 会延续临时,多次析构是自然现象。d 析构 1 次(func 结束),return 时临时值析构 1 次,mainref 绑定的延续临时析构 1 次


而使用引用返回会生成d1 d2的别名,减少了拷贝
![[Pasted image 20250720162717.png]]

reftmp,是d1 d2的别名
看一下具体行为:

传引用返回

分析:

+----------------------------+
|         main()             |
|                            |
| const Date& ref = func();  |
|   (悬空引用)                |
+----------------------------+
             |
             ▼
+----------------------------+
|         func()             |
|                            |
| +----------------------+   |
| | Date d1;             |   |
| |  (栈上局部变量)        |   |
| +----------------------+   |
|                            |
| +----------------------+   |
| | Date d2;             |   |
| |  (栈上局部变量)        |   |
| +----------------------+   |
|                            |
| if (...) return d1;        |
| else return d2;            |
|                            |
| 【func() 结束】             |
| ├─ d1 析构                  |
| ├─ d2 析构                  |
+----------------------------+

为什么没有拷贝?
func 的返回值是 Date&(引用),不是按值返回。
所以 return d1 / return d2 返回的就是 局部变量本身的别名,不需要拷贝,也就不会走拷贝构造。
为什么会析构两次?
1️⃣ d1d2 都是 func 的局部变量,存在 func 的栈帧里。
2️⃣ if 分支随机选择:
return d1; 时,main 拿到 d1 的引用
return d2; 时,main 拿到 d2 的引用
无论你拿到谁的引用,func 一旦返回,d1d2 都会自动析构。
所以 func 结束时: d1 会析构 d2 会析构


再加一段代码:

// 传引用返回
Date& func()
{
	Date d1;
	Date d2;

	if (time(0) % 2)
	{
		return d1;  // 编译器无法预测 取消优化
	}
	else
	{
		return d2;
	}
}

int func1()
{
	int a = 1;
	int b = 2;
	int c = 3;
	return a + b + c;
}

int main()
{
	Date& ref = func();
	ref.Print();
	return 0;
}

此时输出结果为:
在这里插入图片描述

为什么?我们看下函数栈帧

Step 1:
[func 栈帧]
 ┌───────────────┐
 │ Date d1;      │
 │ Date d2;      │
 └───────────────┘

Step 2:
return d1/d2 的地址 ----> 被 ref 接住

[main 栈帧]
 ┌───────────────┐
 │ Date& ref ----┼────────┐
 └───────────────┘        │
                          │
                          ▼
                   指向 func 的局部变量

Step 3:
func 返回后
 --> func 栈帧销毁
 --> d1/d2 内存仍然是旧的,但悬空

Step 4:
调用 func1()

[func1 栈帧]
 ┌───────────────┐
 │ int a;        │
 │ int b;        │
 │ int c;        │
 └───────────────┘

⬆️⬆️⬆️⬆️⬆️⬆️⬆️⬆️⬆️
这里可能会复用func 原来的栈空间

Step 5:
ref.Print() 访问到的其实是
a,b,c 占用的区域里的数据

===> 输出被覆盖的脏数据

C++ 函数如果返回局部变量引用,栈帧销毁后,这个引用就悬空。
如果后面又有新的函数调用分配局部变量,就可能覆盖原来的内存区域。
这会导致引用指向的内容被篡改,输出是垃圾值,属于未定义行为。

总结:
在 C++ 里,传值返回和传引用返回的选择,核心看返回值要不要和原来的对象共享
如果是局部变量或者新建对象,比如 operator+,就必须传值返回,这样才能把局部结果安全拷贝或者移动出来
如果是内部状态或者链式调用,比如 operator=vector::operator[],就传引用返回,这样可以直接在原对象上操作,省掉拷贝
唯一需要注意的是局部变量绝不能传引用返回,不然栈帧一结束,引用就悬空了,行为是未定义的
另外现代编译器对值返回会做 RVO 优化,很多时候根本不会产生拷贝开销。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值