C++入门:构造函数,析构函数,拷贝构造函数详解

C++入门:构造函数,析构函数,拷贝构造函数详解,第1张

目录

类的6个默认成员函数

一.构造函数

1.概念

2.特征如下:

1. 函数名与类名相同。

2. 无返回值。

3. 对象实例化时编译器自动调用对应的构造函数。

4. 构造函数可以重载。

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

 6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

7. 关于编译器生成的默认成员函数,很多同学会有疑惑:

 8. (了解)C++11打的补丁:

二.析构函数

1.概念

2.特征

三.拷贝构造

1.大体的概念:创建一个新对象时,把一个已有对象完全拷贝给这个新对象

2.特征


类的6个默认成员函数 如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6 个默认成员函数。

一.构造函数 1.概念 构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是 初始化对象 2.特征如下: 1. 函数名与类名相同。 2. 无返回值。 3. 对象实例化时编译器自动调用对应的构造函数。 4. 构造函数可以重载。
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(); // 无参不能这么写,编译器无法区分函数声明还是类的定义
	Date d1;	//无参写法
	d1.Print();

	Date d2(2022, 5, 15);	//含参写法,全缺省函数传3个参数可以
	d2.Print();

	Date d3(2022);    //全缺省函数传1个参数可以
	d3.Print();

	Date d4(2022, 10);    //全缺省函数传2个参数可以
	d4.Print();

	return 0;
}
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。      我们不写,编译器会生成一个默认无参构造函数
     内置类型/基本类型:int/char/double/指针...
     自定义类型:class/struct去定义类型对象
     默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理
class Date 不写构造函数时:d1中的内置类型 _year,int _month,int _day 都是随机值,不会初始化,但自定义类型 _aa 会去class A中调用类型A的构造函数, 如果A也不写构造函数 A() ,在类A中int _a 也是内置类型,那么_aa._a 也就是随机值了
class A
{
public:
	A()
	{
		cout << " A()" << endl;
		_a = 0;
	}
private:
	int _a;
};

class Date
{
public:
	// 我们不写,编译器会生成一个默认无参构造函数
	// 内置类型/基本类型:int/char/double/指针...
	// 自定义类型:class/struct去定义类型对象
	// 默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日

	A _aa;
};
int main()
{
	Date d1;
	d1.Print();

	return 0;
}

总结:如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数。如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数。90%都需要自己写构造函数

举例:10%不需要自己写构造函数的情况:class MyQueue 这种只要自定义类型的类就可以不用写构造函数了

class Stack
{
public:
	Stack()
	{
		_a = nullptr;
		_top = _capacity = 0;
	}

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

class MyQueue {
public:
	// 默认生成构造函数就可以用了
	void push(int x) {
	}

	int pop() {
	}

private:
	Stack _st1;
	Stack _st2;
};

int main()
{
	MyQueue q;
	Stack st;

	return 0;
}
 6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

注意:我们写的无参的构造函数  Stack()我们写的全缺省的构造函数  Stack(int capacity = 10)我们没写编译器默认生成的构造函数  (上面两个都不写,编译器就会自己生成一个),都可以认为是默认构造函数。——>不用传参就可以调用的 Stack(int capacity)这个需要传参,所以不是默认构造函数

class Stack
{
public:
	/*Stack()	//1.我们写的无参的构造函数
	{
		_a = nullptr;
		_top = _capacity = 0;
	}*/

	/*Stack(int capacity = 10)	//2.我们写的全缺省的构造函数
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}*/
	//3.或者上面两个都不写编译器默认生成的构造函数
	Stack(int capacity)	//但是这个需要传参数就,不是默认构造函数
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue {
public:
	// 默认生成构造函数就可以用了
	void push(int x) {
	}

	int pop() {
	}

private:
	Stack _st1;
	Stack _st2;
};

int main()
{
	MyQueue q;
	Stack st;

	return 0;
}

用上面注释的那三种默认构造函数都可以正常运行,但是如果像上面这样用的是Stack(int capacity),这个带参的就不是默认构造函数,因为class MyQueue中我们没写默认构造函数所以编译器应该自己生成默认构造函数,但是MyQueue的默认构造函数无法生成,因为生成的条件是要先调用 Stack 的默认构造函数来初始化 _st1 和 _st2 ,因为我们写的不是默认构造函数,而是一个带参的函数,所以无法调用Stack 的默认构造函数,则MyQueue的默认构造函数无法生成,进而导致报错。

7. 关于编译器生成的默认成员函数,很多同学会有疑惑: 在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用? 对象调用了编译器生成的默认构造函数,但是 对象 year/ month/_day ,依旧是随机值。也就说在这里 编译器生成的默认构造函数并没有什么卵 用?? 解答: C++ 把类型分成内置类型 ( 基本类型 ) 和自定义类型。内置类型就是语法已经定义好的类型:如int/char...,自定义类型就是我们使用 class/struct/union 自己定义的类型,编译器生成默认的构造函数会对自定类型成员 调用的它的默认成员函数  8. (了解)C++11打的补丁:

针对编译器自己生成默认成员函数不初始化的问题:当既有内置类型也有自定义类型时,我们不写默认构造函数而用编译器自己生成的默认构造函数时内置类型就会是随机值, 为了使内置类型也初始化,C++11使成员变量声明时可以被赋值缺省参数,默认构造函数调用时会使用这个缺省参数

class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);
	
		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
 
class MyQueue {
public:
	// 默认生成构造函数就可以用了

	void push(int x) {
	}
	
	int pop() {
	}
	
private:
	// C++11打的补丁,针对编译器自己生成默认成员函数不初始化的问题
	// 给的缺省值,编译器自己生成默认构造函数用
	int _size = 0;

	Stack _st1;
	Stack _st2;
};
	
int main()
{
	MyQueue q;
	
	return 0;
}
二.析构函数 1.概念 前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的? 析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而 对象在销毁时会自动调用析构函数,完成类的一些资源清理工作

2.特征

析构函数是特殊的成员函数。

特征: 1. 析构函数名是在类名前加上字符 ~ 2. 无参数无返回值。(所以析构函数不能构成函数重载) 3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
class Stack
{
public:
	// Init
	// Destroy

	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		cout << "~Stack():" << this << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue {
public:
	// 默认生成构造函数就可以用了
	// 默认生成析构函数就可以用了
	void push(int x) {
	}
	
	int pop() {
	}
	
private:
	Stack _st1;
	Stack _st2;
};

int main()
{
	/*Date d1;
	int i = 10;
	if (i > 0)
	{
		Stack st;
	}*/

	// 栈里面定义对象,析构顺序和构造顺序是反的
	Stack st1(1);
	Stack st2(2);

	MyQueue q;

	return 0;
}

 

 

4. 对象生命周期结束时(最近那层大括号里面就是生命周期), C++ 编译系统系统自动调用析构函数。 日期类没必要写析构函数,栈类这种malloc和new出新空间的才用写析构函数
class Stack
{
public:
	// Init
	// Destroy

	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		cout << "~Stack():" << this << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

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

	//~Date()
	//{
	//	cout << "~Date()" << endl;
	//}

private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

int main()
{
	Stack st1(1);
	Stack st2(2);
	return 0;
}

3.构造函数和析构函数自动调用的顺序

在下面这个栈里面定义对象,析构顺序和构造顺序是反的

class Stack
{
public:
	// Init
	// Destroy

	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		cout << "~Stack():" << this << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue {
public:
	// 默认生成构造函数就可以用了
	// 默认生成析构函数就可以用了
	void push(int x) {
	}
	
	int pop() {
	}
	
private:
	Stack _st1;
	Stack _st2;
};

int main()
{
	// 栈里面定义对象,析构顺序和构造顺序是反的
	Stack st1(1);
	Stack st2(2);

	MyQueue q;

	return 0;
}

构造函数调用:st1先,st2后

析构函数调用:st2先,st1后

三.拷贝构造 1.大体的概念:创建一个新对象时,把一个已有对象完全拷贝给这个新对象 构造函数 :只有单个形参,该形参是对本 类类型对象的引用 ( 一般常用 const 修饰 ) ,在用 已存在的类类型对象 创建新对象时由编译器自动调用 2.特征 拷贝构造函数也是特殊的成员函数,其 特征 如下: 1. 拷贝构造函数 是构造函数的一个重载形式 2. 拷贝构造函数的 参数只有一个 必须使用引用传参 ,使用 传值方式会引发无穷递归调用 Date d2(d1); 对象调用类中的拷贝构造函数 Date(Date d) 时,先传参,实参d1传参给形参Date d,因为是 自定义类型对象,拷贝初始化规定要调用拷贝构造完成 ,因为这里传参就是把实参拷贝给形参,所以传参时还要调用一次拷贝构造,这次拷贝构造相当于是 Data data(d1) 去调用的(这里的data是第一次拷贝构造的形参,d1是第一次拷贝构造的实参) Date(Date d),调用这次拷贝构造又要传参,传参又要调用拷贝构造,所以形成无穷递归。所以务必加上引用, Date(Date& d)传参时就不用调用拷贝构造了,因为形参就是实参的别名(当实参,形参是内置类型时就直接赋值就行,没有内置类型的拷贝构造函数) 3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。 4. 那么 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了 ,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试 浅拷贝:只适用于日期类这种,不适用栈类
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	// Date d2(d1);
	//Date(Date& d)
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;

		//d._year = _year;        //形参加上const如果写反了会报错,就可以轻松看出错误
		//d._month = _month;
		//d._day = _day;
	}

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

private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
//void Func(Date& d)
// 自定义类型对象,拷贝初始化规定要调用拷贝构造完成
void Func(Date d)
{}

int main()
{
	Date d1(2022, 5, 15);
	Func(d1);
	int x = 0;
	Func(x);

	Date d2(d1); // 拷贝构造
	d2.Print();
	d1.Print();

	return 0;
}

 错误例子:栈类用浅拷贝就会运行崩溃:

class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}

	// st2(st1)
	// 只能深拷贝实现
	/*Stack(const Stack& st)
	{
	_a = st._a;
	_top = st._top;
	_capacity = st._capacity;
	}*/

	~Stack()
	{
		cout << "~Stack():" << this << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st1(10);
	Stack st2(st1);
	return 0;
}

 把st1拷贝给st2,会导致st1和st2中的指针都指向同一个空间,就会有问题,应该用深拷贝处理,深拷贝后面再讲。

把栈类进行浅拷贝运行结果必然会崩溃:

 

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/920784.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存