C++ primer 第十三章复习
13.1 拷贝,赋值与销毁
类 有五种特殊成员函数控制对象拷贝,移动,赋值和销毁
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数
- 移动赋值运算符
- 析构函数
上述操作称为拷贝控制操作,若一个类没有定义这些函数,编译器会自动生成缺失的函数
拷贝构造函数
拷贝构造函数通常不应该是 explicit ( explicit 不可以隐式初始化对象)
class Foo{
public:
Foo(){} //构造函数
Foo(const Foo&){} //拷贝构造函数
};
在C++中若构造函数只有一个参数, 那么在编译时会有一个缺省的转换:将该构造函数对应数据类型数据转换为该类对象,例如
CxString string2 = 10
//转换为
CxString string2(10);
或
CxString temp(10);
CxString string2 = temp;
#include <iostream>
using namespace std;
class Test{
public:
Test(){}
//拷贝构造函数
Test(const Test &t){
cout << "in copy" << endl;
data = t.data;
}
Test& Test::operator=(const Test &t){
std::cout << "in =" << std::endl;
return *this;
}
private:
int data;
};
int main(){
Test t1;
Test t2(t1);//由于是显式调用拷贝构造函数,所以编译过
Test t3;
t3 = t2;
Test t4 = t2;//由于是隐式调用拷贝构造函数,所以编译不过
return 0;
}
//没有初始化对象无法调用 = 操作符函数(),所以如果要走 = 操作符函数,那么需要 Test t4; t4 = t2; 两步.故 Test t4 = t2 会被编译器优化为 Test t4(t2)
合成拷贝构造函数
涉及到其它类的拷贝,拷贝构造函数内部会调用其它类的拷贝构造
class Sales_data{
public:
Sales_data(const Sales_data&);
private:
std::string bookNo;
int units_sold = 0;
};
Sales_data::Sales_data(const Sales_data& orig):
bookNo(orig.bookNo), //调用string的拷贝构造函数
units_sold(orig.units_sold){}
拷贝初始化
拷贝构造函数用来初始化非引用类型参数,所以自己的参数必须是引用(如果不是引用,又会调用参数的拷贝构造)
std::string dots(10, '.'); //构造函数初始化
std::string str1(dots); //显式拷贝构造函数初始化
std::string str2 = dots; //隐式拷贝构造函数初始化
std::string str3 = "9-99-999"; //构造函数初始化
std::string str4 = std::string(100, '9');//显式构造函数 + 隐式拷贝构造函数初始化
拷贝初始化的限制
explicit 单参构造函数,无法使用 = 成员值进行拷贝构造,作为函数参数同理
explicit vector(size_type n); //vec的构造函数
vector (const vector& x); //vec的拷贝构造函数
std::vector<int> v1(10); //正确,构造函数初始化
std::vector<int> v2 = 10; //错误,构造函数是 explicit 的
void f(std::vector<int>);
f(10); //错误,构造函数是 explicit 的
f(std::vector<int>(10)); //正确, std::vector<int> tmp = std::vector<int>(10);
编译绕过拷贝构造函数
std::string str3 = "9-99-999";
//改写为
std::string str3("9-99-999");
//直接走构造函数初始化,而不会走隐式构造函数 + 隐式拷贝构造函数初始化
拷贝赋值运算符
拷贝赋值运算符接受一个调用类相同类型的参数
class Foo{
public:
Foo(){}
Foo(const Foo&){}
//拷贝赋值运算符接受一个调用类相同类型的参数
Foo& operator==(const Foo&){}
};
int main(){
Foo f2;
Foo f1;
f1 = f2;
}
合成拷贝赋值运算符
涉及到其它类的拷贝运算符,拷贝运算符会调用其它类的拷贝运算符
class Sales_data{
public:
Sales_data(const Sales_data&);
Sales_data& operator=(const Sales_data&);
private:
std::string bookNo;
int units_sold = 0;
};
Sales_data& Sales_data::operator=(const Sales_data&){
bookNo = orig.bookNo;
units_sold = orig.units_sold; //调用 string 的拷贝运算符
return *this;
}
析构函数
不接受参数,不允许重载
构造函数初始化对象的非 static 数据成员 (先初始化成员再构造函数 父 -> 子)
析构函数释放对象成员,并销毁非 static 数据成员(先调用析构函数,再释放成员 子 -> 父)
先调用 A 析构,再销毁 A 数据成员 ,最后销毁父类 B,和创建刚好相反
class Foo{
public:
~Foo(){}
};
1、成员初始化顺序:只与类成员的声明顺序有关
2、初始化列表与构造函数体内初始化的区别:(内置数据类型除外)
在成员初始化列表中初始化,和在构造函数体内赋值,内置数据类型,复合类型(指针,引用)性能和结果完全一样,但用户定义类型(类类型)结果相同,但性能上存在差别。因类类型的数据成员对象在进入函数体前已构造完成,在成员初始化列表处进行构造对象工作,若在构造函数体内赋值相当于先进行构造函数再调用拷贝运算符,而成员初始化列表只调用拷贝构造函数即可
合成析构函数
在类的析构函数结束后,会自动调用成员的析构函数
class Foo{
public:
//成员会自动销毁,不需要额外动作
~Foo(){}
private:
std::string data;
};
需要析构函数的类也需要拷贝和赋值操作
析构释放指针的类,需要拷贝函数和赋值操作;需要拷贝操作的类,也需要赋值操作,反之亦然
对一个类的每个对象分配一个唯一的 ID :需要自定义拷贝构造函数和赋值运算符,但不需要自定义析构函数
class HashPtr{
public:
//构造函数
HashPtr(const std::string& s = std::string()) :
data(new std::string(s)), num(0){}
//编译器生成的拷贝函数
HashPtr(const HashPtr& hashPtr){
(*this).data = hashPtr.data;
this->num = hashPtr.num;
}
//编译器生成的拷贝操作符
const HashPtr& operator=(const HashPtr& hashPtr){
(*this).data = hashPtr.data;
this->num = hashPtr.num;
return *this;
}
//析构函数
~HashPtr(){ delete data; }
private:
std::string* data;
int num;
};
HashPtr test(HashPtr hashPtr){ //调用编译器生成的拷贝构造 HashPtr hashPtr(p1)
HashPtr ret = hashPtr; //调用编译器生成的拷贝操作符
return ret; // hashPtr 对象和 ret 将被销毁
}
int main(){
HashPtr p1("some values");
test(p1); //调用结束后, p1.data 指向的内存被释放了
HashPtr p2(p1); //现在 p1,p2 的 data 变成了野指针
system("pause");
return 0;
}
显式缺省函数
(=default) , default只能用于6个特殊成员函数,但delete可用于任何成员函数
=default 可以显式的要求编译器生成合成的版本
class Sales_data{
public:
Sales_data() = default;
Sales_data(const Sales_data&) = default;
Sales_data& operator=(const Sales_data&);
~Sales_data() = default;
private:
std::string m_data;
};
// = default 生成的赋值运算符函数返回的对象类型是 Sales_data& 非 const
Sales_data& Sales_data::operator=(const Sales_data&) = default;
阻止拷贝
例,IOStream 类阻止拷贝,以免对个对象写入或读取相同的 IO 缓存
struct NoCopy
{
NoCopy() = default;
//通知编译器,不希望定义这些成员函数
NoCopy(const NoCopy&) = delete; //阻止拷贝
NoCopy& operator=(const NoCopy&) = delete;
~NoCopy() = default;
};
struct NoDtor
{
NoDtor() = default;//默认构造
~NoDtor() = delete;//阻止析构
};
对于析构已删除的类,不能定义该类的对象或释放该类指针
struct NoDtor
{
NoDtor() = default;//默认构造
~NoDtor() = delete;//阻止析构
};
int main(){
//NoDtor nd; //报错,编译检测,因为对象不能正常销毁
NoDtor* p = new NoDtor();
delete p; //报错
system("pause");
return 0;
}
本质上,当不能 拷贝,赋值,删除 类成员时,类的合成拷贝函数已被定义为删除的
C++11 1前,阻止拷贝是通过 private 实现的,但是友元和成员函数仍可以拷贝
struct PrivateCopy
{
private:
PrivateCopy(const PrivateCopy&);
PrivateCopy& operator=(const PrivateCopy&);
public:
//默认的合成构造函数,可使用类对象无法拷贝
PrivateCopy() = default;
~PrivateCopy() = delete;
};
单例模式
实现 = delete 实现单例
template<typename T>
class Singleton
{
public:
static T& GetInstance()
{
static T instance;
return instance;
}
Singleton(T&&) = delete;
Singleton(const T&) = delete;
void operator= (const T&) = delete;
protected:
Singleton() = default;
virtual ~Singleton() = default;
};