- 类中的三五原则
- 拷贝控制操作:拷贝构造、拷贝赋值,移动构造、移动赋值,析构,一般编译器会自行生成,但对于类中含有指针类成员时,编译器仅是作了相应的值传递,往往达不到用户要求。
- 拷贝构造,第一参数是自身类类型,且任何额外的参数都有默认值。其可以发生在以下四种情况
- 用‘=’定义变量时
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中地元素或聚合类。
- 需要定义其析构函数的类同时也需要重新定义拷贝和赋值
- 需要拷贝定义的类同时往往也需要定义赋值的操作
- =default由编译器生成合成版本的构造函数。=delete表明阻止拷贝,=delete必须出现在函数第一次出现的时候,且可以出现在任何函数上。
- 另析构函数不能是=delete,由于一个删除的析构函数的类无法销毁,且不能创建该类型的变量或临时变量。且若一个类的某个成员具有删除析构,则这个类自身依旧不能创建变量或临时变量。但可以创建一个指针指向该类型,但依旧不能使用delete 销毁该指针。
#include <iostream> using namespace std; const int d = 33; struct t { ~t() = delete; }; int main() { // t b;不能直接创建变量,由于析构函数被删,域范围结束时,析构确无以着落。 t* a = new t(); //delete a;若为指针,但由于析构删除被删,delete 无法调用,故此不通。 return 0; }
- 所以当由编译器合成一些拷贝控制成员时,若本类中有数据成员不能以默认的构造、拷贝、赋值或销毁,相应的其方法也同样会被编译器定为=delete。
- 若一个有const 成员,若引用成员,编译器不会为其合成默认构造函数。
#include <iostream> using namespace std; const int d = 33; struct s{ int k; s(int i=1):k(i){} }; struct t { const s kk; int i; t(s a):kk(a){ } }; int main() { s p(2); t a(p); s q(3); t b(q); //b = a;此时的赋值操作为delete cout<<a.kk.k; cout << b.kk.k; return 0; }
- 原通过private来阻止拷贝,但这防止不了友元的问题,所以=delete可能还是好一些。
- 从本质上说,写一个类无非两种情况,一是类像值一般传播,另一个是像一个指针一般。
- 类值的类,从本质上说,便是类中的资源(指针)在拷贝、赋值时另开辟一块,不要和原指针共月那一块地址
- 类指针的类,从本质上说,类中的资源被类中的多个对象所共享,即在赋值、拷贝时可以采用默认的方法,但要注意在析构时,要防止因对象析构时,造成指针多次删除的问题,所以,这种方法需要在类中建一个引用计数器,若引用计数器减为0,则析构,释放空间,要不,则拒绝。同时需要注意这个引用计数器也为所有类所共享,所以其也是一个指针。当然,其在拷贝、赋值时是一个值的传递,不存在空间的开辟。当然,其间也伴随的引用计数的增加。
8.交换操作,在自定义类中swap,请先声明 using std::swap;再针对所有成员数据依次分别调用。
9.noexcept ,在c++11中有两种情况,一是无表达式的noexcept,一种是含表达式noexcept(express),其中常量表达式的结果会转换成一个bool类型的值。该值为true时,表达式函数不会抛出异常,反之可能抛出异常。若标记为noexcept时修饰的函数抛出的异常,将会直接调用 std::terminate();另一个析构函数会被默认声明为noexcept(true),但当我们重新定义后,但当用户为类指寂了noexcept或者类的基类或成员有noecept(false)的析构函数,析构函数就不会再保持默认值。
10.关于移动构造中和移动赋值函数
- 移动操作窃取资源,一般不分配资,且移动操作不抛出任何异常。所在需要显示声明noexcept
- 与拷贝操作不同,移动操作永远不会隐式定义为删除的函数。但是,如果我们显示地要求编译器生成=default的移动操作时,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的。
- 与拷贝构造函数不同,移动构造函数被定义为删除的条件是:
- 有类成员定义了自己的拷贝构造函数、但未定义自己的移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。
- 如果有类成员的移动构造函数或移动赋值函数是删除的。则类的移动构造函数或移动构造函数是删除的。
- 类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数为删除的
- 类似拷贝构造函数,如果类成员是const或是引用,则类的移动赋值运算符定为删除的。
- 与拷贝操作不同,编译器根本不会为一个类定义了自己的拷贝构造、拷贝赋值或析构函数的类生成移动构造函数。
11.如果类中有拷贝构造,但无移动构造函数时,若传入一个右值,仍会以拷贝构造函数代替移动构造
12.若一个类既有拷贝赋值,又有移动赋值,此二者可合为一。可以通过单一的赋值运算符,结合交换swap来实现。这一个启示:关于赋值操作,还是用swap来实现吧,简单,快捷方便,无死角。
13.一个移后源对象具有不确定的状态,对其调用 std::move是危险的。当我们调用move时,必须绝对确认移后源对象没有其他用户。#include <iostream> using namespace std; struct t; void swap (t& a, t& b); struct t { int* pr; int i; t(int j ,int*ps){ i=j; pr = new int(*ps); } t& operator = (t a){ swap(*this,a); return *this; } }; void swap(t& a, t& b){ using std::swap; swap(a.i,b.i); swap(a.pr,b.pr); } int main() { int k = 3; int p =10; int q =33; t a(1,&k); t b(2,&p); t c(3,&q); a=b; cout<<a.i<<" "<<*a.pr<<endl; cout << b.i << " " << *b.pr <<endl; a = std::move(c); cout<<a.i<<" "<<*a.pr<<endl; cout << c.i << " " << *c.pr <<endl; return 0; }
14.若一个类定义了一个拷贝操作,一般情况其就应该定义五个操作,某些类必须定义拷贝构造、拷贝赋值、移动构造、移动赋值、析构才能正确工作。这些类通常可能需要拷贝一些资源,一般来说拷贝一些资源需要额外开销,这种开销可以通过移动构造和移动赋值可以避免。
15.参数为右值的函数。若参数为右值,则其可接受左值和右值,当接受左值,则是一个左值函数,若接受一个右值 ,则其是一个右值函数。当然若只有一个左值,若传入右值,也仅会按照左值来处理的。当然精确匹配最好,可以处理二义性。
16.类中还存在左值和右值成员函数引用限定符。引用限定符可以是&或&&(其必须同时出现在函数的声明和定义中,与noexcept同样),其与类中有名的const限定符一样,都是限定成员函数中this的属性的。&指this只能做为左值 ,&&指明this只能做为右值。这也就是说若我们一个函数返回一个右值,其也可以链式调用限定为右值的方法了。
17.限定符之间的关系。引用限定符一般在const限定符之后,且引用限定符不作为函数区分的一个主要内容。即我们若定义两个具有相同名字、参数的函数,若一个有引用限定符,则另一个必须要有。如
#include <iostream> using namespace std; struct t; struct s { void display()&&; void display() const &&; int i; }; void s::display() && { cout << i <<endl; } void s::display() const && { cout << i <<endl; } s func(int k) { s a; a.i =k; return a; } const s func1(int m) { return func(m); } int main() { func(4).display();//调用非const版本,输出4 func1(5).display();//调用const版本,输出5 return 0; }