赋值运算符函数
对于定义一个赋值运算符函数时,需要注意一下几点:
(1)函数的返回类型必须是一个引用,因为只有返回引用,才可以连续赋值
(2)传入的参数声明为常量引用,可以提高代码效率,同时赋值运算函数内不会改变传入的实例状态
(3)一定要记得释放实例自身已有的内存,否则程序容易出现内存泄露
(4)注意传入的参数和当前的实例是不是同一个实例,如果是同一个,则不用进行赋值操作,直接返回即可。
复制构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是复制构造函数。
什么时候调用复制构造函数?
(1)当用类的一个对象初始化该类的另一个对象时;
(2)将一个对象作为实参传递给一个非引用类型的形参时;
(3)从一个返回类型为非引用类型的函数返回一个对象时;
深拷贝和浅拷贝的区别:
1. 默认拷贝构造函数
很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它一般具有以下形式:
Rect::Rect(const Rect& r)
{
width = r.width;
height = r.height;
}
2. 浅拷贝
所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了。
当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。
3. 深拷贝
在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:
深拷贝主要解决的问题是指针成员变量浅拷贝的问题。
1. 防止默认拷贝(也能够禁止复制)
有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。如下程序:
#include <iostream>
using namespace std; class CExample
{
private:
int value; public:
//构造函数
CExample(int val)
{
value = val;
cout << "creat: " << value << endl;
} private:
//拷贝构造,只是声明
CExample(const CExample& C); public:
~CExample()
{
cout << "delete: " << value << endl;
} void Show()
{
cout << value << endl;
}
}; //全局函数
void g_Fun(CExample C)
{
cout << "test" << endl;
} int main()
{
CExample test();
// g_Fun(test); // 按值传递将出错 return ;
}
而根据《C++ Primer》第四版13.1.3节,要禁止类的复制, 类必须显示声明其复制构造函数为private。
小问题:一个类中可以有多个拷贝构造函数吗?
解答:类中可以存在超过一个拷贝构造函数。
1 class X {
2 public:
3 X(const X&); // const 的拷贝构造
4 X(X&); // 非const的拷贝构造
5 };
关于拷贝构造函数与拷贝赋值操作符的区别:
两都都是用已存在的对象A来初始化另一个对象B。不同之处在于:
复制构造函数是针对一个未存在的对象进行初始化;赋值是针对已存在的对象进行初始化。
#include<iostream>
#include<cstring>
using namespace std;
class CMyString
{
private:
//int value;
char *m_pdata;
public:
CMyString(char *pdata=NULL);
CMyString(const CMyString &str);//复制构造函数
CMyString & operator = (const CMyString &str);//赋值运算符函数
~CMyString(void);
void print();
};
CMyString::CMyString(char *pdata)
{
if(pdata==NULL)
{
m_pdata=new char[];
m_pdata[]='\0';
}
else
{
int len=strlen(pdata);
m_pdata=new char[len+];
strcpy(m_pdata,pdata);
}
}
CMyString::CMyString(const CMyString &str)//深拷贝 默认的是浅拷贝
{
// cout<<"hello xiaoming"<<endl;
int len=strlen(str.m_pdata);
m_pdata=new char[len+];
strcpy(m_pdata,str.m_pdata);
}
CMyString::~CMyString()
{
delete []m_pdata;
}
CMyString& CMyString::operator =(const CMyString&str)
{
if(this==&str)
return *this;
delete []m_pdata;
m_pdata=NULL; m_pdata=new char[strlen(str.m_pdata)+];
strcpy(m_pdata,str.m_pdata); return *this;
}
void CMyString::print()
{
cout<<m_pdata<<endl;
} void test1()
{
cout<<"test() begin"<<endl;
char text[]="hello world";
CMyString str1(text);
CMyString str2,str3;
str3=str2=str1;//调用赋值运算操作符
cout<<"The expected result is: "<<text<<endl;
cout<<"the str2 actual result is: "<<endl;
str2.print();
cout<<endl;
cout<<"The expected result is: "<<text<<endl;
cout<<"the str3 actual result is: "<<endl;
str3.print();
cout<<endl;
}
void test2()
{
cout<<"test4() begin"<<endl;
char text[]="hello world";
CMyString str1(text); //初始化操作 调用构造函数
CMyString str2=str1; //标准写法CMyString str2(str1); 调用复制构造函数而不是赋值
str2.print();
cout<<endl;
}
int main()
{
test1();
test2();
return ;
}
代码
class CMyString
{
public:
CMyString(char *ptr = nullptr);
CMyString(const CMyString &str);
~CMyString();
CMyString& operator=(const CMyString& str); private:
char *pData;
}; CMyString& CMyString::operator=(const CMyString& str)
{
pData = str.pData;
return *this;
}
存在问题:
这个赋值运算符重载函数存在的问题如下:
1)浅拷贝;
2)没有(检查)释放实例自身已有的内存。如果我们忘记在分配新内存之前释放自身已有的空间,程序将出现内存泄漏;
3)没有判断传入的参数和当前的实例(*this)是不是同一个实例。如果是同一个,则不进行复制操作,直接返回。如果事先不判断就进行赋值,那么在释放实例自身的内存的时候就会导致严重问题:当*this和传入的参数是同一个实例时,那么一旦释放了自身的内存,传入的参数的内存也同时被释放了,因此再也找不到需要赋值的内容了。
修改之后的赋值运算符重载函数如下:
CMyString& CMyString::operator=(const CMyString& str)
{
if (this == &str)
return *this; delete []pData;
pData = nullptr; pData = new char[strlen(str.pData) + ];
strcpy(pData, str.pData); return *this;
}
上述代码现在的问题在于4)异常安全性,即new可能会抛出异常,而我们却没有处理!所以我们可以将程序继续修改:
CMyString& CMyString::operator=(const CMyString& str)
{
if (this == &str)
return *this; char *tmp = new(nothrow) char[strlen(str.pData) + ];
if (tmp == nullptr)
return *this; strcpy(tmp, str.pData); delete []pData;
pData = tmp;
tmp = nullptr; return *this;
}
除了前边提到的4个点,赋值运算符重载还有两点需要注意:
5)是否把返回值的类型声明为该类型的引用,并在函数结束前返回实例自身的引用(即*this)。只有返回一个引用,才可以允许连续赋值。否则如果函数的返回值是void,应用该赋值将不能做连续赋值。假设有3个CMyString对象:str1、str2和str3,在程序中语句str1=str2=str3将不能通过编译。
6)是否把传入的参数的类型声明为常量引用。如果传入的参数不是引用而是实例,那么从形参到实参会调用一次拷贝构造函数。把参数声明为引用可以避免这样的无谓消耗,从而提高代码效率。同时,我们在赋值运算符函数内不会修改传入的实例的状态,因此应该为传入的引用参数加上const关键字。