前言
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_ptr
,unique_ptr
,weak_ptr
等,具体用法省略。
//禁用拷贝构造函数和赋值构造函数的类
class NoCopyable
{
protected:
NoCopyable() = default;
~NoCopyable() = default;
// 禁用复制构造(拷贝构造)
NoCopyable(const NoCopyable&) = delete;
// 禁用赋值构造
NoCopyable& operator = (const NoCopyable&) = delete;
};
(三),关于开辟内存空间位置的问题:
1,静态存储区: 存放全局变量,静态(static
)变量的区域,里面的数据在程序编译时就已经分配好,存在与整个程序的生命周期中。
2,堆区:提供给程序员动态申请的区域,空间比栈区大,但效率比栈区慢些。申请方式有new
,malloc
等,相对应的释放方式为delete
,free
等;对该区域的内存,程序员有权利申请,同样有义务释放;否则容易产生堆内碎块,野指针等;
3,栈区:存放局部变量的区域,它的内存申请和释放都由系统决定。生命周期会随着函数或类的结束而结束;