面试题1---赋值运算符函数详解

1.题目

如下类型CMyString的声明,请为该类型添加赋值运算符函数。

class CMyString
{
  public:
  CMyString(char* pData=nullptr);
  CMyString(const CMyString& str);
  ~CMyString(void);
  private:
  char* m_pData;
}

2.疑问

  • 1.赋值运算符函数是什么?
  • 2.nullptr是什么?是null吗?
  • 3.我写出来的会是什么东西?

3.涉及知识点

(一)类的构造函数

1.构造函数与类名相同,是特殊的公有成员函数
2.构造函数无函数返回类型说明,实际上构造函数是有返回值的,其返回值类型即为构造函数所构建到的对象。
3.当新对象被建立时,构造函数便被自动调用,实例化的每个对象仅调用一次构造函数。
4.构造函数可以被重载(即允许有多个构造函数),重载由不同参数进行区分,构造时系统按照函数重载规则选择一个进行执行。
5.如果类中没有构造函数,则系统自动会生成缺省的构造函数。
6.只要我们定义了构造函数,则系统便不会生成缺省的构造函数。
7.构造函数也可在类外进行定义。
8.若构造函数是无参的或者各个参数均有缺省值,C++编译器均认为是缺省的构造函数。但是注意,缺省的构造函数只允许有一个。

(二)类的析构函数

1.析构函数无返回值无参数,其名字与类名相同,只在类名前加上~ 即:~类名(){…}
2.析构函数有且只有一个
3.对象注销时自动调用析构函数,先构造的对象后析构。

#include<iostream>
using namespace std;
class Text
{
  private:
     long b;
     char a;
     double c;
  public:
     Text();
     //Text(char a=0);无参数的和各个参数均有缺省值的构造函数均被认为是缺省构造函数
     Text(char a);
     Text(long b,double c);//参数列表不同的构造函数的重载
     ~Text()//析构函数有且只能有一个
     { 
      cout<<"The Text was free."<<this<<endl;
     }
     void print();
};
Text::Text()
{
  cout<<"The Text was built."<<this<<endl;
  this->a=0;
  this->b=0;
  this->C=0;
}
Text::Text(char a)
{
  cout<<"The Text was built."<<this<<endl;
  this->a=a;
}
Text::Text(long b,double c)
{
  cout<<"The Text was built."<<this<<endl;
  this->a='0';
  this->b=b;
  this->c=c;
}
void Text:print()
{
  cout<<"a= "<<this->a<<" b= "<<" c="<<this->c<<endl; 
}

(三)引用

1.引用简介
引用就是某个变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。
引用的声明方法:类型标识符 &引用名=目标变量名。
【例1】:int a;int &ra=a;//定义引用ra,它是变量a的引用,即别名。
说明:
(1)&在此不是求地址运算,而是起标识作用。
(2)类型标识符是指目标变量的类型。
(3)声明引用时,必须同时对其进行初始化。
(4)引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名。
ra=1;等价于a=1;
(5)声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址。&ra与&a相等。
(6)不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。
2.引用应用
(1)引用作为参数:引用的一个重要作用就是作为函数的参数。以前的C语言中函数参数传递是值传递,如果有大块数据作为参数传递的时候,采用的方案往往是指针,因为这样可以避免将整块数据全部压栈,可以提高程序的效率。但是现在(C++中)又增加了一种同样有效率的选择(在某些特殊情况下又是必须的选择),就是引用。
【例2】:void swap(int &p1,int &p2);//此处函数的形参p1,p2都是引用
{int p;p=p1;p1=p2;p2=p;}
为在程序中调用该函数,则相应的主调函数的调用点处,直接以变量作为实参进行调用即可,而不需要实参变量有任何的特殊要求。如:对应上面定义的swap函数,相应的主调函数可写为:

int main()
{
 int a,b;
 cin>>a>>b;//输入a,b两变量的值
 swap(a,b);//直接以变量a和b作为实参调用swap函数
 cout<<a<<' '<<b;//输出结果
}

上述程序运行,如果输入数据10 20并回车后,则输出结果为20 10。
由【例2】可看出:
(1)传递引用给函数与传递指针的效果是一样的。这时,被调用函数的形参就称为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用是,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
(3)使用指针作为函数的参数随二胺也能达到与使用引用的效果,但是,在被掉函数中同样要给形参分配存储单元,且需要重复使用“*指针变量名”的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。
3.常引用
常引用声明方式:const类型标识符 &引用名=目标变量名;
用这种方式声明的引用,不能通过引用对目标变量的值进行修改,从而使引用的目标成为const,达到了引用的安全性。
【例3】:

int a;
const int &ra=a;
ra=1;//错误
a=1;//正确

这不光是让代码更加健壮,也有些其它方面的需要。
【例4】:假设有如下函数声明:

string foo();
void bar(string & s);

那么下面的表达式将是非法的:

bar(foo());
bar("hello world");

原因在于foo()和"hello world"串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。引用型参数应该在能被定义为const的情况下,尽量定义为const。
4.引用作为返回值
要以引用返回函数值,则函数定义时要按以下格式:
类型标识符 &函数名(形参列表及类型说明){函数体}
说明:(1)以引用返回函数值,定义函数时需要在函数名前加&
(2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。
【例5】以下程序中定义了一个普通的函数fn1(它用返回值的方法返回函数值),另外一个函数fn2,它以引用的方法返回函数值。

#include<iostream.h>
float temp;//定义全局变量temp
float fn1(float r);//声明函数fn1
float &f2(float r);//声明函数fn2
float fn1(float r)//定义函数fn1,它以返回值的方法返回函数值
{
  temp=(float)(r*r*3.14);
  return temp;
}
float &fn2(float r)//定义函数fn2,它以引用方式返回函数值
{
   temp=(float)(r*r*3.14);
   return temp;
}
void main()//主函数
{
    float a=fn1(10.0);//第1种情况,系统生成要返回值的副本(即临时变量)
    float &b=fn1(10.0);//第2种情况,可能会出错(不同 C++系统有不同规定)
    //不能从被调函数中返回一个临时变量或局部变量的引用
    float c=fn2(10.0);//第3种情况,系统不生成返回值的副本
    //可以从背调函数中返回一个全局变量的引用
    float &d=fn2(10.0);//第4种情况,系统不生成返回值的副本
    //可以从被调函数中返回一个全局变量的引用
    cout<<a<<c<<d;
}

引用作为返回值,必须遵守以下规则:
(1)不能返回局部变量的引用。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了“无所指”的引用,程序会进入未知状态。
(2)不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间就无法释放,造成memory leak。
(3)可以返回类成员,但最好是const。主要原因是当对象的属性与某种业务规则相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。

(四)this指针

(1)我们知道类的成员函数可以访问类的数据(限定符只是限定于类外的一些操作,类内的一切对于成员函数来说都是透明的),那么成员函数如何知道哪个对象的数据成员要被操作呢,原因在于每个对象都拥有一个指针:this指针,通过this指针来访问自己的住址。注:this指针并不是对象的一部分,this指针所占的内存大小是不会反应在sizeof操作符上的。this指针的类型取决于使用this指针的成员函数类型以及对象类型,(1)假如this指针所在类的类型是Stu_Info_Manage类型,(下面的测试用例中的类的类型)并且如果成员函数是非常量的,则this的类型是:Stu_Info_Manager const类型,(2)即一个指向非const Stu_Info_Manager对象的常量(const)指针。
(2)this指针常用概念
this只能在成员函数中使用。全局函数,静态函数都不能使用this。实际上,成员函数默认第一个参数为T* const register this。
为什么this指针不能在静态函数中使用?
大家可以这样理解,静态函数如同静态变量一样,它不属于具体的哪一个对象,静态函数表示了整个范围意义上的信息,而this指针却实实在在的对应一个对象,所以this指针当然不能被静态函数使用了,同理,全局函数也一样。
this指针是什么时候创建的?
this在成员函数的开始执行前构造的,在成员的执行结束后清除。
this指针只有在成员函数中才有定义。因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们也无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以&this获得),也可以直接使用的。
(3)this指针的使用:
一种情况就是,在类的非静态成员函数中返回类对象本身的时候,我们可以使用源点运算符(this).,箭头运算符this->,另外,我们也可以返回关于this的引用。

(五)深拷贝与浅拷贝(位拷贝)

前提:在对象拷贝过程中,如果没有自定义拷贝构造函数,系统会提供一个缺省的拷贝构造函数,缺省的拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型成员变量,调用其相应类型的拷贝构造函数。
阅读《C++ primer》有一段这样的话:
由于并非所有的对象都会使用拷贝构造函数和复制函数,程序员可能对这两个函数有些轻视。请先记住以下的警告,在阅读正文时就会多心:
本章开头讲过,如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。以类String的两个对象a,b为例,假设a.m_data的内容为“hello”,b.m_data的内容为“world”。
下年a赋值给b,缺省赋值函数的“位拷贝”意味着执行b.m_data=a.m_data。这将造成三个错误:一是b.m_data原有的内存没被释放,造成内存泄漏;二是b.m_data和a.m_data指向同一块内存,a或b任何变动都会影响另一方;三是在对象被析构是,m_data被释放了两次。
拷贝构造函数和赋值函数非常容易混淆,常导致错写误用。拷贝构造函数是在堆象被创建时调用的,而赋值函数只能被已经存在了对象调用。以下程序中,第三个语句和第四个语句很相似,你分得清楚哪个调用了拷贝构造函数,哪个调用了赋值函数吗?
String a(“hello”);
String b(“world”);
String c=a;//调用了拷贝构造函数,最好写成c(a);
c=b;//调用了赋值函数
本例中第三个语句的风格较差,宜改写成String c(a)以区别第四个语句。
在这里插入图片描述 位拷贝(浅拷贝)举例,a指向b,b的改变其实会影响a的改变,同时a原本指向的空间发生泄漏。
然后这种情况下有了深拷贝。
何时调用 :以下情况都会调用拷贝构造函数:

  • 一个对象以值传递的方式传入函数体
  • 一个对象以值传递的方式从函数返回
  • 一个对象需要通过另外一个对象进行初始化。
    浅拷贝:位拷贝,拷贝构造函数,赋值重载,多个对象共用同一块资源,同一块资源释放多次,崩溃或者内存泄漏。
    深拷贝:每个对象共同拥有自己的资源,必须显示提供拷贝构造函数和赋值运算符。
    缺省拷贝构造函数在拷贝过程中是按字节复制的,对于指针型成员变量只复制指针本身,而不复制指针所指向的目标–浅拷贝。
    我们用自己编写的string举例。
class String
{
  private:
     char * _str;
  public:
    const char* c_str()
    {
      return _str;
    }
    String(const char* str=""):_str(new char[strlen(str)+1])
    {
      strcpy(_str,str);
    }
    String(const String &s):_str(NULL)
    {
       String tmp(s._str);
       swap(_str,tmp._str);
    }
    ~String()
    {
      if(_str)
      {
         delete[] _str;
      }
    }
}

通过开辟空间的方式,进行深拷贝,这种方式采取的拷贝构造,注意这个

String(const String &s):_str(NULL)
{
  String tmp(s.str);
  swap(_str,tmp._str);
}

代码解析:其中this指向拷贝的对象,s指向试图拷贝的原对象。其中利用构造函数开辟空间,建立临时的tmp,然后进行交换完成拷贝。当然,我们也可以使用赋值操作符重载完成这一功能

String& operator =(const String& s)
{
  if(this!=&s)
  {
    String tmp(s._str);
    swap(tmp._str,_str);
    return *this;
  }
}//调用构造析构
//本代码是tmp调用的构造函数
String (const char* str=""):_str(new char[strlen(str)+1])
{
 strcpy(_str,str);
}
//调用这个构造函数,开辟空间,建立一个和s1一样大小的空间,并拷贝值

s1(this),s2(s)
建立tmp,tmp和s2一样大的空间,一样的数值(调用构造函数),然后交换使s1(this)指向2号空间,获得拷贝,tmp指向3号空间,tmp声明周期结束调用析构函数释放,功能完成。
在这里插入图片描述

(六)const关键字

1.定义const对象:const修饰符可以把对象转变为常数对象,意思就是说利用const进行修饰的变量的值在程序的任意位置将不能再被修改,就如同常数一样使用~任何修改该变量的尝试都会导致编译错误,因为常量在定有就不能被修改,所以定义时必须初始化。对于类中的const成员变量必须通过初始化列表进行初始化,如下所示:

class A
{
public:
    A(int i);
    void print();
    const int &r;
 private:
    const int a;
    static const int b;
};
const int A::b=10;
A::A(int i):a(i),r(a)
{
}

2.const对象默认为文件的局部变量:在全局作用域里定义非const变量时,它在整个程序中可以访问,我们可以把一个非const变量定义在一个文件中,假设已经做了合适的声明,就可以在另外的文件中使用这个变量。在全局作用域声明的const变量是定义该对象的文件的局部变量。此变量只存在于那个文件中,不能被其他文件访问。通过指定const变量为extern,就可以在整个程序中访问const对象。

extern const int bufSize;

非const变量默认为extern。要使const变量能够在其他文件中访问,必须在文件中显示地指定它为extern。
3.const对象的动态数组:如果我们在自由存储区中创建的数组存储了内置类型的const对象,则必须为这个数组提供初始化:因为数组元素都是const对象,无法赋值。实现这个要求的唯一方法是对数组做值初始化。

//Error
const int *pci_bad=new const int[100];
//ok
const int *pci_ok=new const int[100]();

C++允许定义类类型的const数组,但该类类型必须提供默认构造函数

const string *pcs=new string[100];//这里便会调用string类的默认构造函数初始化数组元素。

4.指针和const限定符的关系
const限定符和指针结合起来常见的情况有以下几种。

  • (1)指向常量的指针(指向const对象的指针):C++为了保证不允许使用指针改变所指的const值这个特性,强制要求这个指针也必须具备const特性。
const double *cptr;

这里cptr是一个指向double类型const对象的指针,const确定了cptr指向的对象的类型,而而并非cptr本身,所以cptr本身并不是const。所以定义的时候并不需要对它进行初始,如果需要的话,允许给cptr重新赋值,让其指向另一个const对象。但不能通过cptr修改其所指对象的值。

*cptr=42;//error
  • (2)常指针(const指针):C++还提供了const指针—本身的值不能修改
int errNumb=0;
int *const curErr=&errNumb;//curErr是一个const指针

我们可以从右往左把上述定义语句读作“指向int型对象的const指针”。与其他const量一样,const指针的值不能被修改,这意味着不能使curErr指向其他对象。const指针也必须在定义的时候初始化。

curErr=curErr;//错误!即使赋给其相同的值

5.函数和const限定符的关系

  • (1)类中的const成员函数(常量成员函数):在一个类中,任何不会修改数据成员的函数都应该声明为const类型。使用const关键字进行说明的成员函数,称为常成员函数。只有常成员函数才有资格操作常量或常对象,没有使用const关键字说明的成员函数不能用来操作常对象。const是加载函数说明后面的类型修饰符,它是函数类型的一个组成部分,因此,在函数实现部分也要带const关键字。下面举例子说明常成员函数的特征。
class Stack
{
  private:
     int m_num;
     int m_data[100];
  public:
     void Push(int elem);
     int Pop(void);
  int GetCount(void)const;//定义为const成员函数
};
int Stack::GetCount(void)const
{
  ++m_num;//编译错误,企图修改数据成员,m_num
  Pop();//编译错误,企图非const成员函数
  return m_num;
}

既然const是定义为const函数的组成部分,那么就可以通过添加const实现函数重载。

class R
{
  public:
    R(int r1,int r2)
    {
      R1=r1;
      R2=r2;
    }
    void  print();
    void  print() const;
 private:
    int R1,R2;
};
void R::print()
{
  cout<<R1;
}
void R::print()const
{
  cout<<R2;
}
void main()
{
  R a(5,4);
  a.print();
  const R b(20,52);
  b.print();
}

6.const的难点

int b=100;
const int *a=&b;//[1]
int const *a=&b;//[2]
int* const a=&b;//[3]
const int* const a=&b;//[4]

如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向的对象为常量;如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。
因此,[1]和[2]的情况相同,都是指针所指向的内容为常量(const 放在变量声明符的位置无关),这种情况下不允许对内容进行更改操作,如*a=3;[3]为指针本身是常量,而指针所指向的内容不是常量,这种情况下不能对指针本身进行更改操作,如a++是错误的;[4]为指针本身和指向的内容均为常量。

(七)赋值运算符

1.概述:首先介绍为什么要对赋值运算符“=”进行重载。某些情况下,当我们编写一个类的时候,并不需要为该类重载“=”运算符,因为编译系统为每个类提供了默认的赋值运算符“=”,使用这个默认的赋值运算符操作类对象时,该运算符会把这个类的所有数据成员都进行一次赋值操作。例如有如下类:

class A
{
  public:
     int a;
     int b;
     int c;
};

那么对这个类的对象进行赋值时,使用默认的赋值运算符是没有问题的。但是,在下面的示例中,使用编译系统默认提供的赋值运算符,就会出现问题了。示例代码如下:

#include<iostream>
#include<string.h>
using namespace std;
class ClassA
{
 public:
   ClassA
   {
   }
   ClassA(const char* pszInputStr)//深拷贝
   {
     pszTestStr=new char[strlen(pszInputStr)+1];
     strncpy(pszTestStr,pszInputStr,strlen(PszInputStr)+1);
   }
   virtual ~ClassA()
   { 
     delete pszTestStr;
   }
   public:
      char* pszTestStr;
};

我们修改一下前面出错的代码示例,现增加赋值运算符重载函数的类,代码如下:

ClassA& operator=(const ClassA& cls)
{
   //避免自赋值
   if(this!=&cls)
   {
     //避免内存泄漏
     if(pszTestStr!=NULL)
     {
       delete pszTestStr;
       pszTestStr=NULL;
     }
     pszTestStr=new char[strlen(cls.pszTestStr)+1];
     strncpy(pszTestStr,cls.pszTestStr,strlen(cls.pszTestStr)+1);
   }
   return *this;
}

2.总结:综合上述示例内容,我们可以知道针对一下情况,需要显示地提供赋值运算符重载函数(即自定义赋值运算符重载函数):

  • 用非类A类型的值为类A的对象赋值时(当然,这种情况下我们可以不提供相应的赋值运算符重载函数,而值提供相应的构造函数)。
  • 当用类A类型的值为类A的对象赋值,且类A的数据成员中含有指针的情况下,必须显示提供赋值运算符重载函数。

(八)nullptr关键字

为了避免“野指针”(即指针在首次使用之前没有进行初始化)的出现,我们声明一个指针后最好马上对其进行初始化操作。如果暂时不明确该指针指向哪个变量,则需要赋予NULL值。除了NULL指针之外,C++11新标准中又引入了nullptr来声明一个“空指针”,这样,我们就有下面三种方法来获取一个“空指针”:
如下:

int *p1=NULL;//需要引入cstdlib头文件
int *p2=0;
int *p3=nullptr;

新标准中建议使用nullptr代替NULL来声明空指针。到这里,可能有疑问为什么要引入nullptr?这里有几个原因。

  • NULL在C++中的定义,NULL在C++中被明确定义为整数0;
  • nullptr关键字用于标识空指针,它可以转换成任何指针类型和bool布尔类型(主要是为了兼容普通指针可以作为条件判断语句的写法),但是不能转换为整数。
char *p1=nullptr;//正确
int *p2=nullptr;//正确
bool b=nullptr;//正确 if(b)判断为false
int a=nullptr;//error

4.就题论题

当面试官要求应聘者定义一个赋值运算符函数时,他会在检查应聘者写出的代码时关注如下几点:

  • 是否把返回值的类型声明为该类型的引用,并在函数结束前返回实例自身的引用(*this)。只有返回一个引用,才可以允许连续赋值。否则,如果函数的返回值是void,则应用该赋值运算符将不能进行连续赋值。假设有3个CMyString的对象:str、str2和str3,在程序中语句str1=str2=str3将不能通过编译。
  • 是否把传入的参数的类型声明为常量引用。如果传入的参数不是引用而是实例,那么从形参到实参会调用一次复制构造函数。把参数声明为引用可以避免这样的无谓消耗,能提高代码的效率。同时,我们在赋值运算函数内不会改变传入的实例的状态,因此应该为传入的引用参数加上const关键字。
  • 是否释放实例自身已有的内存。如果我们忘记在分配新内存之前释放自己已有的空间,则程序将出现内存侧漏。
  • 判断传入的参数和当前的实例(this)是不是同一个实例。如果是同一个,则不进行赋值操作,直接返回。如果事先不判断就进行赋值,那么释放实例自身内存的时候就会导致严重的问题:当this和传入的参数是同一个实例时,一旦释放了自身的内存,传入的参数的内存也同时释放了,因此再也找不到需要赋值的内容了。
    经典解法,适用于初级程序员
CMyString& CMyString::operator =(const CMyString &str)
{
  if(this==&str)
  return *this;
  delete []m_pData;
  m_pData=nullptr;
  m_pData=new char[strlen(str.m_pData)+1];
  strcpy(m_pData,str.m_pData);
  return *this;
}

考虑异常安全性的解法,高级程序员必备
在前面的函数中,我们在分配内存之前先用delete释放了实例m_pData的内存。如果此时内存不足导致new char抛出异常,则m_pData将是一个空指针,这样非常容易导致程序崩溃。也就是说,一旦在赋值运算符函数内部抛出一个异常,CMyString的实例不再保持有效状态,这就违背了异常安全性原则。
要想在赋值运算符函数中实现异常安全性,我们有两种方法。一种简单的办法是我们先用new分配新内容,在用delete释放已有的内容。这样只在分配内容成功之后在释放原来的内容,也就是当分配内存失败时我们能确保CMyString的实例不会被修改。我们还有一种更好的办法,即先创建一个临时实例,在交换临时实例和原来的实例。下面是这种思路的参考代码:

CMyString& CMyString::operator =(const CMyString &str)
{ 
   if(this!=&str)
   {
      CMyString strTemp(str);
      char* pTemp=strTemp.m_pData;
      strTemp.m_pData=m_pData;
      m_pData=pTemp;
   }
   return *this;
} 

在这个函数中,我们先创建一个临时实例strTemp,接着把strTemp.m_pData和实例自身的m_pData进行交换。由于strTemp是一个局部变量,但程序运行到if的外面时也就出了该变量的作用域,就会自动调用strTemp的析构函数,把strTemp.m_pData所指向的内存释放掉。由于strTemp.m_pData指向的内存就是实例之前m_pData的内存,这六相当于自动调用析构函数释放实例的内存。
在新的代码中,我们在CMyString的构造函数里用new分配内存。如果由于内存不足抛出注入bad_alloc异常,但我们还没有修改原来实例的状态,因此实例的状态还是有效的,这也就确保了异常安全性。
如果应聘者在面试的时候能够考虑到这个层面,面试官就会觉得他对代码的异常安全性有很深的理解,那么他自然也就能通过这轮面试了。

猜你喜欢

转载自blog.csdn.net/Achenming1314/article/details/105315561