C++类与对象二:构造函数、析构函数、拷贝构造函数、赋值运算符重载函数、深拷贝与浅拷贝问题

一、构造与析构函数

1.1、为什么需要构造与析构函数

为什么需要构造与析构函数?
我们先来用OPP思想实现一个顺序栈:

#include <iostream>
using namespace std;

class SeqStack//顺序栈
{
public:
	void init(int size = 10)
	{
		_pstack = new int[size];
		_top = -1;
		_size = size;
	}

	void release()
	{
		delete []_pstack;
		_pstack = nullptr;
	}

	void push(int val)
	{
		if (full())
		{
			resize();
		}
		_pstack[++_top] = val;
	}

	void pop()
	{
		if (empty()) return;
		--_top;
	}

	int top()//获取栈顶元素
	{
		return _pstack[_top];
	}

	bool empty()
	{
		return _top == -1;
	}

	bool full()
	{
		return _top == _size-1;
	}
private:
	int *_pstack;//动态开辟数组,存储顺序栈的元素
	int _top;//指向栈顶位置
	int _size;//数组扩容的总大小

	void resize()
	{
		int *ptmp = new int[_size*2];

		for (int i=0; i<_size; i++)
		{
			ptmp[i] = _pstack[i];
		}
		delete []_pstack;
		_pstack = ptmp;
		_size *= 2;
	}
};

int main()
{
	SeqStack s;
	s.init(5);

	for (int i=0; i<15; ++i)
	{
		s.push(rand()%100);
	}

	while (!s.empty())
	{
		cout << s.top() << " ";
		s.pop();
	}
	return 0;
}

测试一下:测试成功。
在这里插入图片描述
等等,我们好像忘了什么。没错,聪明的你一定发现了,我们堆上开辟的资源忘记释放了。当我们代码过多的时候,初始化或者释放的时候必须手动调用,手动释放,稍不注意就忘记释放了,程序容易崩溃。
有没有什么好的方法能够让我们自动初始化,自动释放资源呢?
此时我们C++中的构造函数与析构函数就该登场了!

1.2、构造函数与析构函数

构造函数:主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。 特别的一个类可以有多个构造函数 ,可根据其参数个数的不同或参数类型的不同来区分它们,即构造函数的重载。
析构函数:析构函数(destructor) 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。析构函数不能重载。 析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)。

注意:
1.构造函数或析构函数与函数的名字和类名一样
2.构造函数或析构函数没有返回值

3.先构造的后析构,后构造的先析构。
4.析构函数不带参数,因此每一个类只能有一个析构函数;构造函数可以带参数,因此可以提供多个构造函数,即构造函数的重载。
5.构造函数无法自己调用,但是析构可以自己调用,析构函数调用后,对象不存在了,不能再调用对象的方法了,但是它的内存还在,等函数消亡后内存才不存在了,强行调用会造成内存的非法访问。即如下图:
在这里插入图片描述

我们定义一个对象,它会有如下操作:
1.开辟内存
2.调用构造函数
3.调用析构函数
4.释放内存

对象的产生与消亡:
构造函数调用完,对象就产生了。即先开辟内存,成员变量就有了,但是它们的值还不合法,合法的初始化后,对象才真正产生了。当调用析构后,对象就消失了。

不同对象的生命周期:
1.栈上局部对象:定义的时候调用构造,出函数作用域调用析构。
2.栈上全局对象(.data段):定义时候调用构造,程序结束时候才析构。
3.堆上对象:定义时:①.先malloc开辟内存②.调用构造;释放时:①.调用析构 ②.再释放内存。
例如:ps->~SeqStack() + free(ps);

我们举一个析构与构造例子:

SeqStack(int size = 10)//构造函数
{
	_pstack = new int[size];
	_top = -1;
	_size = size;
}

~SeqStack()//析构函数
{
	delete []_pstack;
	_pstack = nullptr;
}

我们将刚才的代码修改一下:用构造与析构调用并打印它们。

#include <iostream>
using namespace std;

class SeqStack//顺序栈
{
public:
	SeqStack(int size = 10)//构造函数
	{
		cout << this << " SeqStack() " <<endl;
		_pstack = new int[size];
		_top = -1;
		_size = size;
	}

	~SeqStack()//析构函数
	{
		cout << this << " ~SeqStack() " <<endl;
		delete []_pstack;
		_pstack = nullptr;
	}

	void push(int val)
	{
		if (full())
		{
			resize();
		}
		_pstack[++_top] = val;
	}

	void pop()
	{
		if (empty()) return;
		--_top;
	}

	int top()//获取栈顶元素
	{
		return _pstack[_top];
	}

	bool empty()
	{
		return _top == -1;
	}

	bool full()
	{
		return _top == _size-1;
	}
private:
	int *_pstack;//动态开辟数组,存储顺序栈的元素
	int _top;//指向栈顶位置
	int _size;//数组扩容的总大小

	void resize()
	{
		int *ptmp = new int[_size*2];

		for (int i=0; i<_size; i++)
		{
			ptmp[i] = _pstack[i];
		}
		delete []_pstack;
		_pstack = ptmp;
		_size *= 2;
	}
};

int main()
{
	SeqStack s;

	for (int i=0; i<15; ++i)
	{
		s.push(rand()%100);
	}

	while (!s.empty())
	{
		cout << s.top() << " ";
		s.pop();
	}
	cout << endl;
	return 0;
}

来瞧一瞧:构造函数与析构函数代替了对象成员变量的初始化与资源释放操作。这就是使用构造与析构的好处。
在这里插入图片描述

二、深拷贝与浅拷贝分析

2.1、浅拷贝

拷贝构造函数:我们没有提供拷贝构造时系统会自动提供,用同类型的已经存在的对象来产生一个同类型的新对象,做的是内存的数据拷贝,即浅拷贝。拷贝构造函数,又称复制构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其形参必须是引用,但并不限制为const,一般普遍的会加上const限制。 此函数经常用在函数调用时用户定义类型的值传递及返回。拷贝构造函数要调用基类的拷贝构造函数和成员函数。如果可以的话,它将用常量方式调用,另外,也可以用非常量方式调用。

浅拷贝:对象之间直接进行内存拷贝。
我么来看看几种构造情况:
情况1. 当没有提供构造函数的时候,编译器会为你生成默认构造和默认析构,是空函数,什么也不做。

SeqStack s;//没有提供构造函数的时候,编译器会为你生成默认构造和默认析构,是空函数,什么也不做。

情况2.调用带整型参数的构造函数

SeqStack s1(10);//s1的生成调用了带整型参数的构造函数

定义了一个s1对象,开辟内存在栈上12个字节,调用构造函数时在堆上new int[10],并且把堆内存的地址赋给栈。
在这里插入图片描述
情况3.拷贝构造:我们没有提供拷贝构造系统给自动提供,用同类型的已经存在的对象来产生一个同类型的新对象,做的是内存的拷贝。

//s2由s1初始化,有一个已经存在的栈对象来构造一个新的栈对象
//对象的生成一定会调用构造,拷贝构造函数
SeqStack s2 = s1;//等价于SeqStack s2(s1);

系统默认的拷贝构造:

//默认的拷贝构造
SeqStack(const SeqStack &src)
{
	_pstack = src._pstack;
	_top = src._top;
	_size = src._size;
}

但此时程序崩溃
在这里插入图片描述
我们来研究一下,为什么会崩溃,如图所示:
       同样,我们定义了一个s1对象,开辟内存在栈上12个字节,调用构造函数时在堆上new int[10],并且把堆内存的地址赋给栈。定义了一个s2,用s1进行了拷贝构造,做的是内存的拷贝,将s1内存中成员变量的值直接赋给s2,造成s1中指针的值与s2指针的值一样,s2中指针也指向了那块堆内存。s1先生成,s2后生成,s2先析构,将堆内存释放并将自己的指针指向NULL,造成s1中的指针为野指针,s1析构时变为释放野指针操作,因此程序崩溃。
在这里插入图片描述

浅拷贝出错问题: 对象使用浅拷贝不一定有错,但是对象有成员变量中有指针指向了对象内存之外的外部资源,那么当发生浅拷贝时,两个对象不同的指针指向同一个资源。第一次析构将堆内存释放,第二次析构时候就会出错。

情况4.都是已经存在的对象互相赋值。

s2 = s1;//赋值操作
s2.=(s1)//s1赋给s2是s1调用了自己的等号函数把s1当作实参传入。

此时程序崩溃
在这里插入图片描述
s1,s2都是已经存在的对象,我们此时没有给类提供赋值操作,那么他会用系统默认的赋值函数,做直接的内存拷贝。将s1赋值给s2,直接做的是内存的赋值,把s1的指针赋值给s2,s1与s2的指针指向同一块内存,会发生浅拷贝。而且还把s1指向的资源丢失了,s1指向的内存丢失了,释放的机会都没有。

赋值运算符的重载函数: 我们不提供,系统自动使用默认的内存拷贝,与默认拷贝构造一样。我们需要将其重载:将s2指向的原来的内存先释放掉,根据s1大小重新开辟一块大小相同的内存指向,自己的指针指向自己的内存,析构的时候各自释放各自的。s1赋给s2是s2调用了自己的等号函数把s1当作实参传入。因此我们需要重载等号。
赋值运算符符的重载函数三步:
①防止自赋值
②释放当前对象占用的外部资源
③拷贝构造一样的操作

//赋值运算符重载函数
void operator= (const SeqStack &src)
{
	cout << "operator=" << endl;
	
	if (this == &src)//防止自赋值
	{
		return;
	}
	
	//需要先释放当前对象占用的外部资源
	delete[]_pstack;

	_pstack = new int[src._size];//根据原始大小给当前开辟空间,与拷贝构造一样
	for (int i=0; i<=src._top; ++i)
	{
		_pstack[i] = src._pstack[i];
	}
	_top = src._top;
	_size = src._size;
}

此时,程序执行成功。
在这里插入图片描述

2.2、深拷贝

深拷贝: 对象的成员变量有指针,构造的时候指针指向对象外部的堆内存,这个对象发生默认的拷贝浅拷贝一定会发生问题,应该用深拷贝。 构造对象的成员变量不仅仅要把值拷贝,如果对象的指针指向了外部资源,应该给该对象再单独开辟一块外部资源,让新对象的指针去指向。此时,每个对象的指针就指向了自己独有的外部资源,析构的时候各自析构自己的内存。

因此,我们需要自定义拷贝构造函数。

//自定义拷贝构造函数
SeqStack(const SeqStack &src)
{
	cout << "SeqStack(const SeqStack &src" << endl;
	_pstack = new int[src._size];//根据原始大小给当前开辟空间
	for (int i=0; i<=src._top; ++i)
	{
		_pstack[i] = src._pstack[i];
	}
	_top = src._top;
	_size = src._size;
}

此时就是深拷贝了,为其单独开辟了一块同样大小的内存,再释放时没有问题。
在这里插入图片描述
程序执行成功:
在这里插入图片描述
这里还产生了一个问题:为什么拷贝时候要使用for循环,使用memcpy函数补上更简单吗?
       因为在进行数据拷贝时候,如果是整型,不会占用整型之外的资源,进行memcpy拷贝没有问题。但如果拷贝时候拷贝的对象,每一个对象中都含有指针,指针指向外部资源,会发生浅拷贝,析构指向时,释放同一块内存,程序崩溃。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值