通过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个操作。比如:某些类必须定义拷贝构造,拷贝赋值,析构才能正确工作时,这些类通常用于管理一个资源,拷贝一个资源会导致一些额外开销。在这种拷贝非必要的情况下,定义了移动构造函数和移动赋值运算符来避免这种开销。
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
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; //错误
Foo::Foo(Foo rhs); //错误,参数必须是引用类型,否则会造成调用死循环
Foo::Foo(const Foo& rhs); //正确
②是参数特殊的构造函数,因此可以使用初始值列表对函数进行初始化数据成员
Sales_data(const Sales_data& s): bookNo(s.bookNo) { }
③某些如IO类,unique_ptr的类当不允许进行拷贝时,可以通过关键字delete进行删除;
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;
}
}
//在没有移动赋值的情况下,若返回student将会再次调用copy assignment导致死循环发生
student& operator=(const student& s);
②赋值操作时,应当能够正确处理自赋值的情况,
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函数一般都会包括拷贝构造和析构两个流程,为了
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)则为未定义行为
⑤析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常时,析构函数
class people{
public:
~people() {cout<< "~people()" << endl;}
};
class student:public people {
public:
~student() {cout<< "~student()" << endl;};
};
//使用结果
people *p = new student();
delete p; //错误,仅调用people析构
//打印结果
~people()
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)); //拷贝构造函数,因为未定义移动构造函数
//插入函数一般都提供两个版本:一个右值引用,一个左值引用
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