类的5种特殊成员函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数
- 移动赋值运算符
- 析构函数
1. 拷贝、赋值和销毁
拷贝构造函数
Sales_data::Sales_data(const Sales_data &orig):
bookNo(orig.bookNo),
units_sold(orig.units_sold),
revenue(orig.revenue)
{}
- 定义:类构造函数的第一个参数是自身类类型的引用,且额外的参数都有默认值;
- 拷贝初始化调用了拷贝构造函数:
string dots(10,'.'); // 直接初始化
string s(dots); // 直接初始化
string s2 = dots; //拷贝初始化
string null_book = "99999999"; //拷贝初始化
string nines = string(100,'9'); //拷贝初始化
- 什么时候拷贝初始化发生?
- 使用
=
定义变量; - 将一个对象作为实参传给一个非引用类型的形参;
- 从一个返回类型为非引用类型的函数返回一个对象;
- 列表初始化
- 使用
- 为什么拷贝构造函数的第一个参数必须是引用类型?
- 因为如果参数不是引用类型,为了调用构造函数,必须拷贝实参,为了拷贝实参,又必须调用拷贝构造函数,因此调用永远都不会成功。
- 拷贝构造函数的第一个参数最好为const类型,因为拷贝构造函数并不会更改被拷贝对象。
拷贝赋值运算符
Sales_data& Sales_data::operator=(const Sales_data &rhs){
bookNo=rhs.bookNo; //调用string::operator=
units_sold=rhs.units_sold;//内置的int赋值
revenue=rhs.revenue; //内置的double赋值
return *this; //返回此对象的引用
}
- 重载运算符本质上是函数,函数名由关键字operator和运算符符号组成,赋值运算符就是一个名为**operator=**的函数;
- 为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向左侧运算对象的引用;
析构函数
- 析构函数释放对象使用的资源,销毁对象的非static数据成员;
- 析构函数不接受参数,也不能被重载,一个类有唯一的析构函数;
class Sales_data{
public:
~Sales_data();
......
};
- 什么时候调用析构函数?无论何时一个对象被销毁,就会调用其析构函数:
-
变量在离开其作用域时被销毁;
-
当一个对象被销毁,其成员也被销毁;
-
容器(或数组)被销毁,其元素也被销毁;
-
对于动态分配的对象,当delete p时被销毁;
-
对于临时对象,当创建它的完整表达式结束时被销毁;
//临时对象被销毁的一个例子: int *x=new int(1024); void process(shared_ptr<int> ptr){} process(shared_ptr<int>(x));//在这一步操作中,process函数接受了一个临时对象,该临时对象在 //表达式结束之后就销毁了,因此x对应的内存被智能指针释放,传入的是一个被释放的空指针
-
- 当指向一个对象的指针或引用离开作用域时,析构函数不会执行;
例题 1:(里面有很多需要注意的点)
- 编写String类的构造函数、拷贝构造函数、赋值函数和析构函数,已知String类的原型如下:
class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数
~ String(void); // 析构函数
String & operator =(const String &other); // 赋值函数
private:
char *m_data; // 用于保存字符串
};
- 解答:
- 注意:String中的数据成员是一个指针,未被初始化,不指向任何内存,因此所有构造函数都需要重新分配内存
//普通构造函数
String::String(const char *str){
//这里如果传入的str指针没有被正确的初始化,对其进行strlen操作也会产生不确定的值,但是我们在函数内部只能判断其
//是否为空,不能判断是否正确的初始化,这应该由函数的使用者来保证
if(str==NULL){ //这里先判断str是否为空,空指针不能对其进行strlen操作,会产生段错误
//错误示范:*m_data='\0';//不能对一个未初始化的指针进行解引用操作,必须先申请内存
m_data = new char[1];//申请大小为1的char数组来存放空字符
*m_data='\0';//现在指针指向了一块内存,因此可以进行解引用操作
}
else{
int len=strlen(str);
m_data = new char[len+1]; //申请空间的时候要注意申请空字符的存储空间
strcpy(m_data,str);
}
}
//拷贝构造函数
String::String(const String &other){
//先判断other的m_data是否为空
if(other.m_data==NULL){
m_data=new char[1];
*m_data = '\0';
}
else{
int len=strlen(other.m_data);//这里不要有疑问,被拷贝对象other可以访问其private成员,因为这是在类内定义的构造函数
m_data=new char[len+1];
strcpy(m_data, other.m_data);//strcpy会将结尾的空字符一并拷贝
}
}
//拷贝赋值运算符
String & String::operator=(const String &other){
//检查自赋值,如果自赋值,直接返回*this,不能进行后面的内存释放操作了,如果释放了内存,则参数中的对象也被释放了,就无从拷贝了
if(this==&other){//判断是否是自赋值,要用指针来比较,不能用对象来比较,对象就算相同,也不一定是自赋值
return *this;
}
//如果不是自赋值,就先释放内存
delete [] m_data;
int len=strlen(other.m_data);
m_data=new char[len+1];
strcpy(m_data, other.m_data);
return *this;
}
//析构函数
String::~ String(void){
delete [] m_data;
}
拷贝、赋值、销毁定义的基本原则:
- 基本原则一:需要析构函数的也需要拷贝构造和拷贝赋值运算符
- 基本原则二:需要拷贝操作的类也需要赋值操作,反之亦然
阻止拷贝
- 对于某些类,比如iostream,拷贝和赋值是没有意义的,因此必须阻止拷贝,方法是定义删除的函数:
struct NoCopy{
NoCopy()=default; //使用合成的默认构造函数
NoCopy(const NoCopy&)=delete;
NoCopy& operator=(NoCopy&)=delete;
~NoCopy()=default; //析构函数不能使删除的成员
};
2. 拷贝控制和资源管理
- 对于一个类对象,我们对其进行拷贝,有两种语义:第一种,这个类像一个值,原始对象和拷贝的副本拥有独立的内存,改变原对象不会对副本产生任何影响,反之亦然;第二种,这个类像一个指针,拷贝操作只是拷贝了这个指针,但原始对象和拷贝的副本使用的是相同的底层数据
行为像值的类:
- 例题1就是一个行为像值的类,这里再做一道例题,编写一个类值版本的HasPtr构造函数:
class HasPtr{
public:
HasPtr(const string &s=string()):ps(new string(s)),i(0){} //普通构造函数
HasPtr(const HasPtr&); //拷贝构造函数
HasPtr& operator=(const HasPtr&);//赋值运算符
~HasPtr();//析构函数
private:
string *ps;
int i;
}
- 拷贝构造函数:
HasPtr::HasPtr(const HasPtr ©){
i=copy.i;
ps=new string(*copy.ps);
}
也可以直接使用构造函数初始值列表:
HasPtr::HasPtr(const HasPtr ©):i(copy.i),ps(new string(*copy.ps));
- 赋值运算符:(不需要检测自赋值的写法)
HasPtr& HasPtr::operator=(const HasPtr &orig){
auto temp=new string(*orig.ps);//使用中间变量的好处是,不需要检测自赋值
delete ps;
ps=temp;
i=orig.i;
return *this;
}
- 析构函数:
HasPtr::~HasPtr(){delete ps;}
- 思考:这里拷贝构造函数为什么不需要判断ps为空?
因为,这里默认构造函数中已经为ps申请了一个空间,该空间至少存放一个空string,因此ps一定不是空指针,而且对空string解引用也是正确的。
而在例题一中,默认构造函数中接受的参数是一个内置指针,我们无法像string一样对他进行初始化。