深度剖析构造函数与析构函数,你真的了解它吗

你真的了解构造函数吗

      • 构造函数
          • 1.构造函数初始化对象的方法
          • 2.缺省的构造函数和无参的构造函数
          • 3.关于编译器自动生成的默认构造函数
          • 4.关于同时存在内置类型数据和自定类型数据如何满足需求的调用默认构造函数
          • 5.三种默认构造函数
          • 6.拷贝构造函数
          • 7.默认生成的拷贝构造函数
            • 内置类型
            • 1.浅拷贝构造函数
            • 2.深拷贝构造函数
            • 自定义类型
      • 析构函数

构造函数

背景:由于c语言我们经常忘记Init,就有构造函数。

含义:构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任

并不是开空间创建对象,而是初始化对象

特征:

  1. 函数名与类名相同。

  2. 无返回值。

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

  4. 构造函数可以重载。

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

    用户显式定义编译器将不再生成。

    深度剖析构造函数与析构函数,你真的了解它吗_第1张图片

​ 日期类构造函数的的例子,如上,空间并不是构造函数开的,而是在main函数里的栈帧就开好了,创建对象就自动调用构造函数。与普通函数不一样的是,构造函数传参是在对象名之后,而不是函数名之后。

1.构造函数初始化对象的方法

1.有明确初始化的值进行传参,例如上图。

Date d1(2022,9,21)

2.可以提供多个构造函数,进行多种初始化方式,构造函数重载

深度剖析构造函数与析构函数,你真的了解它吗_第2张图片

    Date d1(2022, 9, 21);
	Date d2(2022, 9, 21);
	Date d3;//要注意的是不能写成Date d3()这样子,这样写会被误认为是函数声明,无参的不要加括号。

像这样构成构造函数重载,也能初始化对象。


2.缺省的构造函数和无参的构造函数

深度剖析构造函数与析构函数,你真的了解它吗_第3张图片

这是全缺省的构造函数,和无参的构造函数不能同时存在,语法上能存在,但是调用的时候会存在二义性。

深度剖析构造函数与析构函数,你真的了解它吗_第4张图片


3.关于编译器自动生成的默认构造函数

​ 关于编译器生成的默认成员函数,很多人会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象的_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??

​ 解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员Time_t调用的它的默认构造函数

​ 对于内置类型,有系统自动生成的构造函数,但是系统不会处理,不满足我们的需求,会是随机值,但对于内置类型有相应的自动生成构造函数。

class Time
{
public:
 Time()
 {
 cout << "Time()" << endl;
 _hour = 0;
 _minute = 0;
 _second = 0;
 }
private:
 int _hour;
 int _minute;
 int _second;
};
class Date
{
private:
 // 基本类型(内置类型)
 int _year;
 int _month;
 int _day;
 // 自定义类型
 Time _t;
};
int main()
{
 Date d;
 return 0;
}

​ 对于上述日期类,没有显示定义的构造函数,所以会调用系统默认生成的构造函数,默认生成的构造函数对于内置类型数据不做处理,而对自定类型数据会调用默认生成的构造函数,日期类有一个Time _t,Time类定义的对象t,所以会调用Time类的构造函数来给 _t这个类对象来复制,这也就是系统给Date类自动生成的默认构造函数,所以输出结果会显示cout << “Time()” << endl,来说明调用了默认生成的构造函数。

class Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack(int capacity = 4)" << endl;

		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}

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

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

	void Push(int x)
	{
		// ....
		// 扩容
		_a[_top++] = x;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue {
public:
	void push(int x)
	{
		_pushST.Push(x);
	}

	Stack _pushST;
	Stack _popST;

};
int main()
{
	MyQueue mq;
	mq.push(1);
	mq.push(2);
	return 0;
}

​ 定义一个简单的栈,且用两个栈实现一个队列,来看这个例子,在队列这个类里面,包含了两个栈(自定义类型),且类队列里没有相应的构造函数,所以就会调用自动生成的构造函数,就会调用stack类的构造函数,来初始化MyQueue类里的_pushST和 _popST,编译器默认生成的就够用,这里就不用写构造函数,且有构造函数可以用。这个例子中,析构函数对内置类型也不做处理,析构函数也具有这个特性,在类中若没有自己的析构函数,编译器也会自动生成析构函数,会调用stack类里的析构函数,来清理MyQueue类里定义的两个栈。看实验结果

深度剖析构造函数与析构函数,你真的了解它吗_第5张图片

​ Date ,Stack的构造函数需要自己写,MyQueue就不需要自己写,默认生成的就可以用,可以满足我们的需求,Stack的析构函数需要自己写,有资源要释放,但是有资源要释放需要自己写吗?也不一定MyQueue也有资源要释放,但是不需要自己写,默认生成的就可以用。Date类也不需要自己写析构函数,没有自定义类型的数据,会随着生命周期的结束而清理。

小结论

面向需求:编译默认生成的就可以满足,就不需要写,不满足就需要自己写。//面向需求的写


4.关于同时存在内置类型数据和自定类型数据如何满足需求的调用默认构造函数
class Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack(int capacity = 4)" << endl;

		_a = (int*)malloc(sizeof(int)*capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}

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

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}


private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue 
{
	
	void push(int x)
	{
		//_pushST.Push(x);
	}

private:
	Stack _pushST;
	Stack _popST;
	size_t _size;
};

​ C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在

类中声明时可以给默认值

private:
	Stack _pushST;
	Stack _popST;
	size_t _size = 0;//这是缺省值,不是初始化

只需要把上述代码块改成这样就能满足我们的需求,既调用了默认构造函数满足自定义类型的数据,又用缺省值满足内置类型数据,满足了我们所有的需求。


5.三种默认构造函数

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

注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为

​ 是默认构造函数。

三类默认构造函数罗列:

  • 无参的构造函数
  • 全缺省的构造函数
  • 我们不写,系统默认生成的构造函数

一个解释:为什么无参的和全缺省的默认构造函数语法上能同时存在,构成构造函数重载,但是使用会发生错误,因为你调用的时候会有二义性,同样是都不传参的,系统不知道调用哪一个构造函数,所以不能同时存在。

总结:不传参数就可以调用的构造函数,就叫默认构造函数

一个例子更好理解默认构造函数

深度剖析构造函数与析构函数,你真的了解它吗_第6张图片

​ 因为截图截不下,仍然还是日期类,上面有完整的代码块。这个代码截图能很好的说明上面所说的三种默认构造函数,这里我写了一个构造函数,但不是属于默认构造函数,它是半缺省的构造函数,但是我定义了一个日期类d1,也没有传参过去,而且我这里写了显式的构造函数,不会生成系统自动生成的构造函数,所以编译器会保错,没有合适的构造函数可用。

进一步理解默认构造函数

class Stack
{
public:
	Stack(int capacity)
	{
		cout << "Stack(int capacity = 4)" << endl;

		_a = (int*)malloc(sizeof(int)*capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}

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

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

	/*int* _a = (int*)malloc(sizeof(int)*4);
	int _top = 0;
	int _capacity = 4;*/
};

class MyQueue {
public:
	
	void push(int x)
	{
		//_pushST.Push(x);
	}

private:
	Stack _pushST;
	Stack _popST;
	size_t _size = 0; // 这里不是初始化,给的缺省值
};

int main()
{
	MyQueue q;

	return 0;
}

image-20221010005930772

这是代码的运行结果,同样是MyQueue没有默认的构造函数,因为这里创建对象q会调用系统生成的构造函数去初始化自定义类型的数据_ pushST,_popST,所以会去stack类调用默认构造函数去初始化MyQueue里的栈,但是,stack类没有可以调用的默认构造函数,是因为首先这里有显示的构造函数,所以stack类里没有系统生成的构造函数,第二是因为,stack类里的构造函数是需要传参的,不是全缺省的构造函数,所以也没有第二种默认构造函数,第三,这里很显然没有无参的构造函数,所以三种默认构造函数是都没有的,所以会报错。

三种方法解决:

  • 初始化列表解决(待整理)
  • 自己在MyQueue类里写一个构造函数
  • 用默认构造函数缺省值来初始化内置类型

深度剖析构造函数与析构函数,你真的了解它吗_第7张图片

​ 删去stack类的构造函数为了来调用系统自动生成的默认构造,然后给内置类型附上缺省值,这样调用的时候就能达到我们的目的。//把_a置成空,这里初始化不会malloc空间,可以去扩容的时候realloc。

​ 同样下面那段代码也能实现,区别是_a不为空。在附上缺省值的时候malloc空间,也可以但不是很完美,因为这样的话,我们就不能进行检查malloc是否成功。实验结果如下。

深度剖析构造函数与析构函数,你真的了解它吗_第8张图片


6.拷贝构造函数

定义:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存

​ 在的类类型对象创建新对象时由编译器自动调用。

特点:1. 拷贝构造函数是构造函数的一个重载形式

​ 2.拷贝构造函数的参数只有一个必须是类类型对象的引用,使用传值方式编译器直接报错

​ 因为会引发无穷递归调用。

只有当一个类对象去初始化另一个同类对象,才会发生拷贝构造。

但是用值传递,会引发无穷递归调用,这是因为什么呢?而要使用类对象的引用作为参数。

Date( Date d)
	{
		cout << "Date 拷贝构造" << endl;

		_year = d._year;
		_month = d._month;
		_day = d._day;

	}
这是一个错误的拷贝构造,用下面的main函数来调用这个错误的拷贝构造函数,会发生错误。至于为什么我们下面来验证。
int main()
{
Date d1(2022,9,22);
Date d2(d1);
}

深度剖析构造函数与析构函数,你真的了解它吗_第9张图片

​ 深入了解拷贝构造!!!

​ 为了验证类对象引用作参数,和类对象做参数的区别,首先我们写了一个正常的拷贝构造函数(类对象引用作参数的拷贝构造函数),且我们在拷贝构造函数里写了一句cout<<“Date 拷贝构造”<所以这里要用类对象的引用作形式参数,这样形式参数就是实参的别名,d就可以代表d1,用d就能访问到d1的成员,然后成员函数里有默认的this指针,再加上成员访问符,会拷贝一份d1出来给要拷贝的对象。

深度剖析构造函数与析构函数,你真的了解它吗_第10张图片

实验结果:

深度剖析构造函数与析构函数,你真的了解它吗_第11张图片

一个小注意:

const的添加,我们要在引用前加const,使权限缩小。

深度剖析构造函数与析构函数,你真的了解它吗_第12张图片

假如我们在拷贝构造函数中又没加const,又把拷贝行为写反了,就会出问题,我们原本的意图是拷贝一份d1给d2,现在搞成了d1的d_day元素反而被d2的随机值给改了,弄巧成拙了。所以加上const更好。


7.默认生成的拷贝构造函数

定义:若未显式定义,编译器会生成默认的拷贝构造函数。

特点:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而不是所有的内置类型都能被浅拷贝所满足,有的也需要深拷贝才能满足需求,是调用其拷贝构造函数完成拷贝的。

内置类型
1.浅拷贝构造函数

定义:对于内置类型,默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。相当于memcpy。

还是日期类的例子:

深度剖析构造函数与析构函数,你真的了解它吗_第13张图片

这是一个浅拷贝构造函数,很明显日期类里没有写拷贝构造函数,但是结果是d2成功拷贝了d1,对于内置类型系统自动生成的隐式的浅拷贝构造函数,按照字节序,直接进行拷贝的所以拷贝成功了。

但是浅拷贝不能完全满足我们的需求

例如:

class Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack(int capacity = 4)" << endl;

		_a = (int*)malloc(sizeof(int)*capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}
	
	// st2(st1)
	/*Stack(const Stack& st)
	{
		cout << "Stack(const Stack& st)" << endl;

		_a = (int*)malloc(sizeof(int)*st._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_a, st._a, sizeof(int)*st._top);
		_top = st._top;
		_capacity = st._capacity;
	}*/

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

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

	void Push(int x)
	{
		// ....
		// 扩容
		_a[_top++] = x;
	}

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

深度剖析构造函数与析构函数,你真的了解它吗_第14张图片

​ 结合上述代码块一起看没有拷贝构造函数(在我们上述代码块已经写出来了,但是被屏蔽了,默认为没有),所以系统会自动调用系统默认生成的浅拷贝构造函数,其实是调用成功了的,但是不满足我们的需求,可以看见其实st2中的_top和 _capacity都是拷贝成功了的,但是问题在于所有的内置类型元素全部被拷贝成和st一样的,包括st1和st2中的两个指针都是一样的指向同一块空间,伴随着问题来了,再进行析构的时候,会出现问题(先定义的后析构,后定义的先析构(类比栈)),st2先析构,st2. _a这个指针指向的空间,已经被free掉了,但是st1会继续调用它的析构函数,st1. _a这个指针也会执行free的操作,但是由于指针指向的同一块空间,在st1进行析构时那快空间已经被释放掉了,所以继续析构会出现野指针的问题,所以浅拷贝构造函数在有的情况是不能满足我们的需求的。

深度剖析构造函数与析构函数,你真的了解它吗_第15张图片

2.深拷贝构造函数

深度剖析构造函数与析构函数,你真的了解它吗_第16张图片

​ 这是解决上述浅拷贝构造函数不能满足需求问题的一个解决方法,这里就是自己写的一个深拷贝构造函数,我们知道,我们要让st2 _a和st1 _a指向不同的空间,但是其他内置类型数据要一样,浅拷贝完成不了我们的需求,我们传用引用做深拷贝构造函数的参数,这里的st就是st1的别名,所以这里可以直接用st. 访问st1里的数据,所以这里调用st. _capacity来保证开的空间大小一样,用memcpy来复制已经入栈的元素然后拷贝到st2中,把 _top和 _capacity复制成st1里一样的数据,看右边的监视结果,明显发现两个指针指向的不是同一块空间,且前两个数据是一样的(通过memcpy复制而来),并且拷贝成功之后对两个栈数据的插入是独立的,完美的解决了上述问题。

深度剖析构造函数与析构函数,你真的了解它吗_第17张图片

便于理解。

一个总结:需要写析构函数的都需要深拷贝的拷贝构造,不需要写析构函数的类,默认生成的浅拷贝构造函数就能用。

自定义类型

例如Myqueue这个类

代码块如下:

class Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack(int capacity = 4)" << endl;

		_a = (int*)malloc(sizeof(int)*capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}
	
	 //st2(st1)
	Stack(const Stack& st)
	{
		cout << "Stack(const Stack& st)" << endl;

		_a = (int*)malloc(sizeof(int)*st._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_a, st._a, sizeof(int)*st._top);
		_top = st._top;
		_capacity = st._capacity;
	}

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

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

	void Push(int x)
	{
		// ....
		// 扩容
		_a[_top++] = x;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};
// 
 需要写析构函数的类,都需要写深拷贝的拷贝构造              Stack
 不需要写析构函数的类,默认生成的浅拷贝的拷贝构造就可以用   Date/MyQueue
//
class MyQueue {
public:
	void push(int x)
	{
		_pushST.Push(x);
	}
private:
	Stack _pushST;
	Stack _popST;
	size_t _size = 0;
};

int main()
{
	Date d1(2022, 9, 22); // 构造 - 初始化

	// 拷贝一份d1
	Date d2(d1); // 拷贝构造 -- 拷贝初始化

	Stack st1;
	st1.Push(1);
	st1.Push(2);
	// 1 2

	Stack st2(st1); 
	st2.Push(10);

	st1.Push(3);
	// 1 2 10


	MyQueue q1;
	MyQueue q2(q1);


	return 0;
}

​ 这个类虽然没写MyQueue的拷贝构造函数,但是系统默认生成的就够用,对于内置类型数据,默认生成的浅拷贝构造函数就能满足需求,对于自定义类型数据,用stack类定义了俩类对象 _pushST, _popST,系统会调用默认生成的拷贝构造,就会调用stack类里的深拷贝构造函数来拷贝q1的数据到q2.又因为一个Myqueue的对象里有两个栈,所以会调用两次深拷贝构造函数来拷贝 _pushST和 _popST,调用四次析构函数来析构q1和q2。

深度剖析构造函数与析构函数,你真的了解它吗_第18张图片


析构函数

概念:

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由

编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

与destory的功能相似。

特点:

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~。//代表与构造函数功能相反

  2. 无参数无返回值类型。//不支持重载

  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构

函数不能重载

  1. 对象生命周期结束时,C++编译系统系统自动调用析构函数//局部对象,全局域,malloc的空间会影响生命周期

例子:(一个栈的析构函数)

和上述代码块是一起的,因为 MyQueue这个类里面没有它本身的析构函数,所以系统自动生成的析构函数(其实就是stack类的析构函数),对自定义类型的数据自动生成的析构函数有时候能满足你的需求,就不用再写了。对内置类型也会生成对应的析构函数,但是不处理,其实对于内置类型,不处理也没有关系,随着生命周期的结束,自定义类型的数据会自动销毁。

深度剖析构造函数与析构函数,你真的了解它吗_第19张图片


你可能感兴趣的:(c++,算法,开发语言)