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. 赋值重载

未完待续~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值