【C/C++】一、C++ 默认构造/拷贝构造/赋值构造/带参构造 函数详解

前言

c++不像JAVA那样有资源回收机制,造成c++开发的猿类经常在内存的创建与回收上踩坑;内存溢出是常态,内存泄漏惹人烦啊;所以我特意抽了点时间看了看一点c++的构造函数相关内容,特来分享给各位。下面由我一一为大家讲解。

一,默认构造函数

定义:

ClassName();

特点:

1,属于类中一个特殊的成员函数
2,函数名与类名相同
3,函数无返回值
4,函数无参数

作用:

创建类对象时,系统调用默认构造函数并为其分配内存空间。

实例:

class ClassName
{
public:
	//重写默认构造函数
	ClassName(){
		printf(">>>默认构造函数被调用\n");
	}
	
	//重写默认析构函数
	~ClassName(){
		printf("默认析构函数被调用\n");
	}
};

void main()
{
	ClassName cnObj;//创建类对象,系统自动调用默认构造函数;
}                   //对象cnObj的生命周期结束,自动调用析构函数
输出:
>>>默认构造函数被调用
默认析构函数被调用

二,拷贝构造函数

定义:

ClassName(const ClassName& cnObj);

特点:

1,属于类中一个特殊的成员函数
2,函数名与类名相同
3,函数无返回值
4,函数带一个参数,且参数类型为当前类的引用;

作用:

创建类对象时,如果调用拷贝构造函数,则在为其分配内存空间的同时,利用参数(对象的引用初始化成员变量
(注意:拷贝构造函数初始化含指针的成员变量时,默认使用的是浅拷贝

实例:

class ClassName
{
public:
	//重写默认构造函数
	ClassName(){
		printf("默认构造函数被调用\n");
		m_pcArray = new char[30];//在堆区开辟30个字节的内存空间
		char* pChar = "测试字符串内容";
		memcpy(cbcn.m_pcArray, pChar, strlen(pChar)); //使用memcpy对堆区中的内存赋值;
	}
	
	//重写默认析构函数
	~ClassName(){
		printf("默认析构函数被调用\n");
		if(NULL != m_pcArray){
			delete m_pcArray;//释放堆区内存
			m_pcArray = NULL;//指针置空
		}
	}

	//重写拷贝构造函数
	ClassName(const ClassName& cbcnObj){
		printf(">>>拷贝构造函数被调用\n");	
		/*类成员含指针变量是,建议重写拷贝构造函数,对指针变量赋值*/
		this->m_pcArray = new char[30];      //深拷贝,开辟新的内存空间
		memcpy(this->m_pcArray, cbcnObj.m_pcArray, strlen(cbcnObj.m_pcArray));
		
		/*
		*默认赋值方式
		*this->m_pcArray = cbcnObj->m_pcArray;//浅拷贝,共用已存在的内存空间
		*/
	}
	
public:
	char* m_pcArray;		//类成员变量
};

void main()
{
	ClassName cnObj;				//调用默认构造函数创建对象,但不初始化成员变量
	ClassName copyWays1(cnObj);	    //使用拷贝构造函数创建对象,同时初始化类成员变量
	ClassName copyWays2 = cnObj;	//使用拷贝构造函数创建对象,同时初始化类成员变量
}//对象cnObj生命周期结束,第一次自动调用析构函数	
//对象copyWays1 生命周期结束,第二次自动调用析构函数				
//对象copyWays2 生命周期结束,第三次自动调用析构函数				
输出:
默认构造函数被调用
>>>拷贝构造函数被调用
>>>拷贝构造函数被调用
默认析构函数被调用
默认析构函数被调用
默认析构函数被调用

注意:
如上代码所示,析构函数调用两次,释放了两次堆内存空间,成为了隐患点;
如果没有重写拷贝构造函数,容易常见的内存“双杀”现象。
在默认拷贝函数是进行浅拷贝的前提下,两个对象的成员变量m_pcArray共用一块堆内存空间,在第一次调用cnObj的析构函数后,m_pcArray的内存已经被释放。
再一次调用copyWays1析构函数时,虽然m_pcArray值不为空,仍会执行delete操作。
再次释放会造成内存溢出的错误,最终造成程序崩溃。

三,赋值构造函数

定义:

ClassName& operation= (const ClassName& cnObj);

特点:

1,重写赋值操作符"="的函数
2,函数返回值与类名相同
3,函数带一个参数,且参数类型为当前类的引用;

作用:

给已存在对象赋值时,系统调用赋值构造函数并利用参数(对象的引用)初始化成员变量
(注意:赋值构造函数初始化含指针的成员变量时,默认使用的是浅拷贝

实例:

class ClassName
{
public:
	//重写默认构造函数
	ClassName(){
		printf("默认构造函数被调用\n");
		m_pcArray = new char[30];//在堆区开辟30个字节的内存空间
		char* pChar = "测试字符串内容";
		memcpy(cbcn.m_pcArray, pChar, strlen(pChar)); //使用memcpy对堆区中的内存赋值;
	}
	
	//重写默认析构函数
	~ClassName(){
		printf("默认析构函数被调用\n");
		if(NULL != m_pcArray){
			delete m_pcArray;//释放堆区内存
			m_pcArray = NULL;//指针置空
		}
	}

	//重写拷贝构造函数
	ClassName(const ClassName& cbcnObj){
		printf("拷贝构造函数被调用\n");	
		/*类成员含指针变量是,建议重写拷贝构造函数,对指针变量赋值*/
		this->m_pcArray = new char[30];      //深拷贝,开辟新的内存空间
		memcpy(this->m_pcArray, cbcnObj.m_pcArray, strlen(cbcnObj.m_pcArray));
		
		/*
		*默认赋值方式
		*this->m_pcArray = cbcnObj->m_pcArray;//浅拷贝,共用已存在的内存空间
		*/
	}
	
	//重写赋值构造函数
	CBaseClassName& operator = (const CBaseClassName& dbObj)
	{
		printf(">>>赋值构造函数被调用\n");
		if (this == &cbObj){
			return *this;
		}
		
		//重写赋值语句
		memcpy(this->m_pcArray, cbObj.m_pcArray, strlen(cbObj.m_pcArray));
	
		//默认赋值语句
		//this->m_pcArray = cbObj.m_pcArray;
	}
	
public:
	char* m_pcArray;		//类成员变量
};

void main()
{
	ClassName cnObj;				//调用默认构造函数
	ClassName copyCnObj;			
	copyCnObj = cnObj;				//调用赋值构造函数
}//对象cnObj生命周期结束,第一次自动调用析构函数	
//对象copyCnObj生命周期结束,第二次自动调用析构函数						
输出:
默认构造函数被调用
默认构造函数被调用
>>>赋值构造函数被调用
默认析构函数被调用
默认析构函数被调用

注意:
赋值构造默认对指针函数的赋值还是浅拷贝,所以拷贝构造隐藏的内存“双杀”问题仍然存在。

四,一般带参构造函数

定义:

ClassName(char* pChar);

特点:

1,属于类中一个特殊的成员函数
2,函数名与类名相同
3,函数无返回值
4,函数带参数且数量任意。

作用:

创建类对象时,系统调用默认构造函数并为其分配内存空间。同时利用参数对成员变量进行赋值。

实例:

class ClassName
{
public:
	//重写默认构造函数
	ClassName(){
		printf(">>>默认构造函数被调用\n");
	}
		
	//重写默认析构函数
	~ClassName(){
		printf("默认析构函数被调用\n");
	}
	
	//一般构造函数
	ClassName(char* pChar){
		printf(">>>一般带参构造函数被调用\n");
		memcpy(this->m_pcArray, pChar, strlen(pChar));
		//this->m_pcArray = pChar;  //指针指向了栈区,无法delete
	}
public:
	char* m_pcArray = new char[30];		//类成员变量
};

void main()
{
	ClassName cnObj("test context");//创建类对象,系统自动调用带参构造函数;
}                   //对象cnObj的生命周期结束,自动调用析构函数
输出:

>>>一般带参构造函数被调用
默认析构函数被调用

注意:
带参构造中操作this->m_pcArray = pChar;额外解释一下, 这个操作将指针m_pcArray指向了pChar的地址;但pChar是字符指针变量,在栈区开辟的内存,会自动回收;所以如果在析构函数中使用delete将其释放,则编译器会报错误;

五,总结

(一),构造函数分为如下四类:

1,默认构造函数:创建对象,不初始化成员变量;ClassName()
2,拷贝构造函数:创建对象,并利用同类型参数引用初始化成员变量;ClassName(const ClassName& cbcnObj)
3,赋值构造函数:不创建对象,利用重写操作符函数初始化成员变量;CBaseClassName& operator = (const CBaseClassName& dbObj)
4,一般构造函数:创建对象,并利用参数给成员变量赋值;ClassName(char* pChar)

(二),构造函数问题及解决方法:

1,在拷贝构造和赋值构造函数的使用中遇到的堆内存双杀的问题,造成内存泄漏。

根源
函数中默认初始化成员变量是通过浅拷贝的方式进行赋值,造成指针变量共享同一片内存空间;
解决方法
方法1:直接重写拷贝构造函数和赋值构造函数,如上所示,在赋值时,重新new一遍,让每次调用函数时,都创建一片堆内存空间,就不会出现同一片堆空间释放两次的情况了;
方法2:直接禁用拷贝构造函数和赋值构造函数;简单的方法就是让自己的类继承一个禁用当前操作的类就可以;例如下面的NoCopyable类;
方法3:使用智能指针,例如:shared_ptrunique_ptrweak_ptr等,具体用法省略。

//禁用拷贝构造函数和赋值构造函数的类
class NoCopyable
{
protected:
    NoCopyable() = default;
    ~NoCopyable() = default;
    // 禁用复制构造(拷贝构造)
    NoCopyable(const NoCopyable&) = delete; 
    // 禁用赋值构造
    NoCopyable& operator = (const NoCopyable&) = delete;
};

(三),关于开辟内存空间位置的问题:

1,静态存储区: 存放全局变量,静态(static)变量的区域,里面的数据在程序编译时就已经分配好,存在与整个程序的生命周期中。
2,堆区:提供给程序员动态申请的区域,空间比栈区大,但效率比栈区慢些。申请方式有newmalloc等,相对应的释放方式为delete,free等;对该区域的内存,程序员有权利申请,同样有义务释放;否则容易产生堆内碎块,野指针等;
3,栈区:存放局部变量的区域,它的内存申请和释放都由系统决定。生命周期会随着函数或类的结束而结束;

  • 6
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

修道-0323

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值