C++ 拷贝控制 【学习笔记】

类的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'); //拷贝初始化
  • 什么时候拷贝初始化发生?
    1. 使用=定义变量;
    2. 将一个对象作为实参传给一个非引用类型的形参;
    3. 从一个返回类型为非引用类型的函数返回一个对象;
    4. 列表初始化
  • 为什么拷贝构造函数的第一个参数必须是引用类型?
    • 因为如果参数不是引用类型,为了调用构造函数,必须拷贝实参,为了拷贝实参,又必须调用拷贝构造函数,因此调用永远都不会成功。
  • 拷贝构造函数的第一个参数最好为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();
	......
};
  • 什么时候调用析构函数?无论何时一个对象被销毁,就会调用其析构函数:
    1. 变量在离开其作用域时被销毁;

    2. 当一个对象被销毁,其成员也被销毁;

    3. 容器(或数组)被销毁,其元素也被销毁;

    4. 对于动态分配的对象,当delete p时被销毁;

    5. 对于临时对象,当创建它的完整表达式结束时被销毁;

      //临时对象被销毁的一个例子:
      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 &copy){
	i=copy.i;
	ps=new string(*copy.ps);
}	

也可以直接使用构造函数初始值列表:

HasPtr::HasPtr(const HasPtr &copy):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一样对他进行初始化。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章