类-构造、析构、拷贝、移动使用总结

1.如何定义,类的拷贝控制成员有何关系?

  通过default关键字显式地要求编译器自动合成, 各个函数常规的声明方式如下所示:

class Foo {
public:
	//使用default显示地合成构造函数
	Foo() = default;					   //构造函数

	Foo(const Foo &) = default;		       //拷贝构造,参数为const引用
	Foo(Foo &&)      = default;			   //移动构造,参数为非常量的右值	

	Foo& operator=(const Foo &) = default;  //拷贝赋值,参数为const引用,返回引用类型  
	Foo& operator=(Foo &&) = default; 		//移动赋值,参数为非常量的右值,返回引用类型   

	~Foo()=default;
};

三/五法则:除构造函数外,其他5个拷贝控制成员应该看成一个整体:一般来说,如果一个类定义了任何一个操作,它就应该定义所有的5个操作。比如:某些类必须定义拷贝构造,拷贝赋值,析构才能正确工作时,这些类通常用于管理一个资源,拷贝一个资源会导致一些额外开销。在这种拷贝非必要的情况下,定义了移动构造函数和移动赋值运算符来避免这种开销。

2.构造函数使用

    ①当所有参数提供默认实参,则它实际上也定义了默认构造函数
Sales_data(const std::string & s = ""): bookNo(s) { }
    ②类成员有const,引用或未提供默认构造函数的类,必须通过初始值列表为成员提供初值。注意:初始值列表只能说明成员的初始值,不能限定初始化顺序,其顺序与类中定义一致
class ConRef{
	public:
		ConRef(int li);
	private:
		int i;
		const int ci;
		int& ri;
};

//错误:ci和ri必须被
ConRef::ConRef(int li)
{ //赋值
	i  =  li;	//正确
	ci = li;    //错误:不能给const赋值
	ri = i;     //错误:ri没有被初始化 
}

//正确:
ConRef::ConRef(int &li):i(li),ci(li),ri(li) {}
   ③C++11可以通过委托构造函数定义构造函数
Sales_data(const std::string &book, unsigned cnt, double price);
Sales_data():Sales_data("",0,0) {}; //委托 
④当定义抽象基类时,不想被用户实例化时,可以将构造与析构的作用域定义为protected
C++11中可以通过抽象基类的方法定义为 = 0也可;
class People
{
	void work() = 0;   //采用=0,也可以
protected:
	People();          //允许derived对象析构
	virtual ~People();
}

People people; //抽象类,不能被定义
⑤可以使用initializer_list对类内容器成员进行初始化
//#include 	头文件与类名一致
class strvec{
public:
	strvec(initializer_list li):vec(li){}	//copy(li.begin(),li.end(),back_inserter(vec));
private:
	vector vec;
};

//如下使用
strvec vec = {"hello", "goodbye"};  //初始化后,vec中含有两个string:hello goodbye
⑥初始化派生类时,其基类构造函数先调用,构造函数的非static成员按类中的定义顺序初始化
class student: public people; //people构造函数先执行
⑦不要在构造或析构过程中调用virtual函数,否则调用将当前构造基类的版本。简单说:在base class构造期间,virtual函数不是virtual函数
class Log{
public:
	Log() { open();}	//在构造中调用virtual open
	virtual bool open() const = 0;
};

class TxtLog:public Log
public:
	virtual bool open() const;
};

//原意是想打开txt文本,但由于构造函数先调用base class Log::Log()
//此时调用的版本将是base class open,引发错误 
TxtLog log; 
⑧可以通过allocator类将对象内存分配和初始化分开。一般情况下,我们通过new进行内存分配和对象构造会造成不必要的浪费,但通过allocator类则没有此种问题。
const *const p = new string[n]; 	//构造n个的string 

//采用allocator
allocator alloc;			//可以分配string的allocator对象
auto const p = alloc.allocate(n);	//分配n个未初始化的string
auto q = p; 						//q指向最后构造元素之后的位置
alloc.construct(q++);				//*q为空字符串
alloc.construct(q++, "hello");		//*q为hello
⑨可以通过explicit关键字避免隐式转化,此时我们只能通过直接初始化的方式使用
//未使用explicit,允许一个类型转换
struct Sales_data{
	Sales_data(const string& s): bookNo(s) { };
	Sales_data combine(const Sales_data&)
};

string book("string"); //
item.combine(book);	   //正确,构建了一个临时变量Sales_data
item.combine("string"); //错误,只允许一次隐式转化

//使用explicit,允许一个类型转换
struct Sales_data{
	//只能在类内声明explicit
	explicit Sales_data(const string& s): bookNo(s) { }; 
	Sales_data combine(const Sales_data&)
};
string book("string"); //
item.combine(book);	   //错误

//构造函数只能用于直接初始化
Sales_data item1(book);    //正确
Sales_data item2 = book;   //错误

3.拷贝构造函数

  ①定义:第一个参数是自身类型的引用(一般为常量引用),且其他所有参数(若有的话)都有默认值
Foo::Foo(Foo rhs);  		//错误,参数必须是引用类型,否则会造成调用死循环
Foo::Foo(const Foo& rhs); //正确
②是参数特殊的构造函数,因此可以使用初始值列表对函数进行初始化数据成员
Sales_data(const Sales_data& s): bookNo(s.bookNo) { }
  ③某些如IO类,unique_ptr的类当不允许进行拷贝时,可以通过关键字delete进行删除;
   注意:也可以将其声明为private,但不定义他们来阻止拷贝(防止友元和成员函数进行拷贝)
Sales_data(const Sales_data& s) = delete;
④复制对象时勿忘记其每一个成分,因此,如果非必要最好采用系统自动合成的copy构造函数
//情形1:在类中添加新的数据成员
class people{
public:
	people(const people& s):name(s.name) {}
private:
	string name; 
}; 

//如果类中增加age类,但忘记修改拷贝构造函数则容易出错
class people{
public:
	people(const people& s):
	name(s.name){} 	//忘了对age进行赋值 
private:
	string name;
	int    age; 
}; 

//情形2:派生类的拷贝构造中忘记对基类进行拷贝
//接上题,下题如果基类有默认构造函数,当进行拷贝时,s基类数据未赋值类中 
struct student:public people{
	student(const student & s):id(s.id){}
	student& operator=(const student& s) {id = s.id;} 
private:
	int	id;		//正确 
}; 

//修改:
struct student:public people{
	student(const student & rhs): 
		people(rhs), id(rhs.id){} //调用基类copy构造 
	student& operator=(const student& rhs) 
	{
		people::operator=(rhs);		//调用基类copy assignment 
		id = rhs.id;
		return *this;
	} 
}

4.拷贝赋值运算符

  处理copy构造的③④条注意事项外,还有以下注意事项
  ①赋值运算通常因返回一个指向其左侧运算对象的引用
//在没有移动赋值的情况下,若返回student将会再次调用copy assignment导致死循环发生
student& operator=(const student& s); 
②赋值操作时,应当能够正确处理自赋值的情况,
   注意:部分情况下,可以采用swap+copy构造的方式避免自赋值的情况,在后续移动构造中进行减少
class HasPtr{
public:
	HasPtr& operator=(const HasPtr &p);
private:
	string *ps;
};

inline HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
	auto newp = new string(*rhs.ps);  //先创建对象,放置rhs与this指向同一对象
	delete ps;       				  
	ps = newp;       					
	return *this;
}
③含内存资源管理的类,copy assignment函数一般都会包括拷贝构造和析构两个流程,为了
    避免重复代码,可将成员拷贝构造和析构分别写成两个私有函数供copy构造,copy赋值,析构使用
5.析构函数使用
  部分注意事项,请参见constructor构造函数
 ①在一个析构函数中,首先执行函数体,然后销毁成员,成员按初始化顺序的逆序销毁。若为drive class
先调用drive析构再调用bass class析构,注意析构函数并不直接销毁成员
class people{
public:
  ~people() {} //在(空/默认)析构函数体执行完毕后,成员会被自动销毁,顺序int -> string 
private:
   string name;
   int    i;
}
②调用析构函数的情况
//1)变量在离开其作用域时被销毁;
//2)当一个对象被销毁时,其成员被销毁
//3)容器或数组被销毁时,其元素被销毁
//4)对于动态分配的内存,调用delete运算符被销毁
//5)对于临时对象,当创建它的完整表达式结束时,被销毁

{//新的作用域
	string *p = new string();		 //p是一个内置指针
	auto p2 = make_shared(); //p2是一个shared_ptr(本质是一个类)
	string item(*p);				 //拷贝构造函数将*p拷贝到item中
	vector vec;				 //局部对象
	vec.push_back(*p2);		         //拷贝p2指向的对象
	delete p;						 //对p指向的对象执行析构函数
}
//退出局部作用域;对item, p2和vec调用析构函数
//销毁p2会递减其引用计数;如果引用计数变为0,对象被释放
//销毁vec会销毁它的元素
③当指向一个对象的引用或指针离开作用域时,析构函数不会执行
{//新的作用域
	string *p = new string();		 //p是一个内置指针
}
//若不显示的调用delete p,则会调用内存泄漏
④当类含有move构造和赋值运算符,应保证析构函数能正确执行
class HasPtr{
   HasPtr(string s = ""):ps(new string(s)){}
   HasPtr(HasPtr&& rhs):ps(rhs.ps) (rhs.ps = nullptr;)
  ~HasPtr() { if(ps) delete ps;}
  private:
	string *ps;
}

{//新的作用域
HasPtr ptr1("hello));
HasPtr ptr2(std::move(ptr1));  
}
//ptr1在退出时,自动调用析构,如不判断if(ps)则为未定义行为
⑤析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常时,析构函数
   应该捕捉异常,然后吐下它们或结束程序。详参见effective c++ 条款08
⑥为多态基类声明virtual析构函数
class people{
  public:
  ~people() {cout<< "~people()" << endl;}
};

class student:public people {
  public:
  ~student() {cout<< "~student()" << endl;};
};
  
//使用结果
people *p = new student();
delete p;  //错误,仅调用people析构 

//打印结果
~people() 

6.移动构造函数

本质是一种特殊的构造函数,相关注意事项参考构造和拷贝构造函数
  ①移动是从给定对象”窃取“资源而不是拷贝资源,因此,移动操作一般都不会发生异常,在
   定义时可添加noexcept关键字。
class StrVec{
public:
	StrVec(const StrVec&);
	StrVec(StrVec&&) noexcept;
	StrVec& operator=(StrVec&&) noexcept;
	
	~StrVec() noexcept;
};
②可以销毁一个移动后的对象,也可以赋予新值,但不能使用它;
 string str1("world");
   string str2(std::move(str1));
   cout<< str1[0] << endl;			//错误,str1不能被使用,行为未定义
   str1 = string("hello");  		//正确,可以被赋予新值
③进行移动操作时,被移动的函数应能保证被正确析构
class HasPtr{
public:						//移动后的函数,应对rhs进行处理
   HasPtr(HasPtr&& rhs):ps(rhs.ps) {rhs.ps = nullptr;} 
  ~HasPtr() { if(ps) delete ps;}
private:
	string *ps;
}
④如果一个类有可用的拷贝构造函数而没有移动构造函数,则其对象时通过拷贝
   构造函数来“移动”,拷贝赋值与移动赋值也相同。
class Foo {
public:
	Foo(const Foo &) = default;		       //拷贝构造,参数为const引用
	Foo& operator=(const Foo &) = default;  //拷贝赋值,参数为const引用,返回引用类型  

	~Foo()=default;
};

Foo x;
Foo y(x);				//拷贝构造函数;x是一个左值
Foo z(std::move(x));	//拷贝构造函数,因为未定义移动构造函数

7.移动赋值运算符

除了移动构造函数需要注意的事项外,移动赋值需要注意如下情况
   ①函数可以提供右值引用参数,以提高函数效率
//插入函数一般都提供两个版本:一个右值引用,一个左值引用
void push_back(const X&);	//拷贝:绑定到任意类型的X
void push_back(X&&);		//移动:只能绑定到类型的可修改右值

vector vec;
string s = "void push_back(const X&);";
vec.push_back(s); 			//拷贝
vec.push_back("done");		//移动:从done创建一个临时的对象
②进行移动赋值运算时,应当要考虑自赋值的情况。
class HasPtr{
	friend void swap(HasPtr& lhs, HasPtr& rhs); 
public: 
	HasPtr(const string &s = string(), int i = 0);
	//HasPtr& operator=(HasPtr p);  //
	HasPtr& operator=(const HasPtr &p);
	HasPtr& operator=(HasPtr &&p);
	
private:
	string *ps;
	int		ival; 
};

inline
void swap(HasPtr& lhs, HasPtr& rhs){
	cout<< "void swap(HasPtr& lhs, HasPtr& rhs)"<
③注意临时对象赋值和变量赋值与移动的区别:
//如上图
HasPtr ptr1, ptr2, ptr3;
ptr1 = ptr2;   		   //仅调用了copy assignment
ptr1 = string("hello"); //调用了三个函数constructor(临时变量)->move assignment->destructor(临时变量)
ptr1 = std::move(ptr3); //仅调用了
④可以采用拷贝并交换代替赋值运算符和移动操作,此种操作关键是创建了一个临时变量
   与赋值对象交互,避免自赋值的情况,但也因需要创建临时变量造成不必要的开销
HasPtr& operator=(HasPtr p);  //赋值运算符代替赋值运算符和移动操作
//HasPtr& operator=(const HasPtr &p);
//HasPtr& operator=(HasPtr &&p);

HasPtr ptr1, ptr2, ptr3;
ptr1 = ptr2;   		   //左值,调用了两个函数copy constructor -> assignment
ptr1 = string("hello"); //调用了三个函数constructor(临时变量)->assignment->destructor(临时变量)
ptr1 = std::move(ptr3); //右值,调用了两个函数move constructor -> assignment































你可能感兴趣的:(C/C++)