C++ Primer学习二 拷贝控制
rule of three:定义了其中一个,剩下的几个都要定义
copy constructor
copy assign operator
destructor
rule of five:定义了其中一个,剩下的几个都要定义
copy constructor
copy assign operator
destructor
move constructor
move assign operator
深拷贝与浅拷贝
a. 浅拷贝:简单的赋值拷贝操作。浅拷贝带来的问题就是堆区的内存重复释放。要利用深拷贝解决。 b. 深拷贝:在堆区重新申请空间,进行拷贝操作。编译器会默认生成拷贝构造函数,但是如果类里面有动态申请的内存空间,那么一定要自定义拷贝构造函数,用深拷贝去解决浅拷贝带来的问题,并且拷贝构造函数/类中重载赋值运算符一定要使用索引,因为不用索引的话,去调用对象的时候会首先调用一次拷贝构造,造成死循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 类似实现一种资源管理的方式,应该从哪里考虑: 构造函数初始化,拷贝构造++,析构函数--,如果减到0 ,直接delete 掉。class Foo {public : int * resource_; size_t * count_; Foo (int resource):resource_ (new int (resource)){ cout<<"in constructor" <<endl; this ->count_ = new size_t (1 ); } Foo (const Foo& foo){ this ->resource_ = foo.resource_; this ->count_ = foo.count_; *(this ->count_)+=1 ; cout<<"update this->count " <<*(this ->count_)<<endl; } ~Foo (){ *(this ->count_) -=1 ; if (*(this ->count_) ==0 ){ cout<<"do destructor" <<endl; delete this ->count_; delete this ->resource_; } } };int main () { Foo foo1 (1 ) ; Foo foo2 (foo1) ; Foo foo3 (foo2) ; Foo foo4 (foo3) ; cout<<"how many count " <<*(foo2.count_)<<endl; return 0 ; } 输出结果 in constructor update this ->count 2 update this ->count 3 update this ->count 4 how many count 4 do destructor
移动构造
右值:右值可以偷过来 a. 容易消失,没有名字,不可修改。 b. 没有其他的人在使用。
如果有移动构造,移动来源的资源一定要释放掉。
运算符重载 a. + - / * 运算符重载:定义新的运算规则 b. 左移运算符重载:定义输出方式,一定要定义成友元函数,没法用隐式转换 c. 递增运算符重载:实现自己的数据类型。 d. 赋值运算符重载:进行赋值,注意深浅拷贝。重载赋值符号,也是放置浅拷贝的重要一项。 e. 关系运算符重载:让两个自定义对象进行对比操作。 f. 函数调用重载:仿函数,定义类似函数的行为,比较自由。
拷贝构造函数和拷贝赋值运算符的调用时机:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> namespace _nmp2_2{ class A { public : A ():m_caa (0 ),m_cab (0 ){}; A (const A& tmp){ this ->m_caa = tmp.m_caa; this ->m_cab = tmp.m_cab; } A& operator = (const A& tmp){ m_caa = tmp.m_caa; m_cab = tmp.m_cab; return *this ; } public : int m_caa; int m_cab; }; };int main () { _nmp2_2::A oba; oba.m_caa = 10 ; oba.m_cab = 20 ; _nmp2_2::A obb = oba; obb = oba; return 0 ; }
类和类之间的关系一般就是继承关系,或者是组合关系。
委托关系:一个类中包含指向另一个类的指针。
面向对象
运行时多态:虚函数+动态绑定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Book {public : string ISBN; virtual void func () { cout<<"Book" <<endl; } };class ComicBook : public Book{public : void func () { cout<<"Comic Book" <<endl; } };class ActionBook : public Book{public : void func () { cout<<"Action Book" <<endl; } };int main () { ComicBook cb; ActionBook ac; Book* b1 = (Book*) &cb; Book& b2 = ac; b1->func (); b2.func (); return 0 ; }
派生类构造的时候,要先构造基类的东西。
派生类析构的时候,先析构自己的东西,再析构基类的东西。
vtable:找到子类函数的关键。
静态成员:静态变量和静态成员函数,加static关键字: a. 静态成员变量: i. 所有对象共享同一份数据 ii. 在编译阶段分配内存,不和类对象在同一个存储空间上,只有非静态成员变量才属于类的对象上 iii. 类内声明,类外初始化。 b. 静态函数: i. 所有对象共享同一个函数。 ii. 静态成员函数只能访问静态成员变量。
纯虚函数不能实例化对象。
如何让抽象类不能生成对象?
构造函数和拷贝构造函数都用protected修饰。
protect和private的区别 :子类中不可访问父类中的private的属性和函数,但可以访问protect的。
C++中struct和类的唯一区别就是权限不同,struct默认public,public继承,class 默认private,private继承。
基类的析构函数,一般加上virtual,让派生类释放自己的,基类释放自己的,每个类做好自己的事情,派生类不要管基类的释放。
容器中存放类对象:如果vector< Base > vc 中push_back一个派生类,那么派生类相对于基类多出来的部分,会被砍掉。但是如果说把类型换换,vector< Base* > vc2, vc2中插入一个派生类指针,根据多态性,可以访问到多出来的那部分。
如果类中包含静态成员变量,无论这个静态成员变量是否使用,都会给这个静态成员变量分配内存。
全局对象的初始化顺序是不固定的
做父类应该有个虚析构函数。
对于不允许进行拷贝构造或者拷贝赋值运算符的函数:用=delete或者定义为private函数。
模板与泛型编程
C++除了面向对象编程思想之外,还有泛型编程思想,主要利用的技术就是模板。
面向对象编程和泛型编程都能处理在编写程序时不知道类型的情况,不同之处在于,面向对象编程能够处理在程序运行之前都都未知的情况,而泛型编程,在编译时就能获知类型 。
模板的声明和定义要都放在.h中 :编译器看到模板不会生成代码,一定要实例化一个模板的一个特定版本的时候,编译器才会生成代码,为了生成一个实例化版本,模板的头文件中要包含模板的定义和声明。
函数模板:建立一个通用函数,其函数返回值类型和形参类型可以不具体指定,用一个虚拟的类型来代表。
类模板:建立一个通用类,类中的成员数据类型可以不具体制定,用一个虚拟的类型来代表。类模板在初始化的时候一定要显式的表明是什么类型的。 在类内,声明T之后,就不用再用T再次声明,但是在类外,但凡要用到A类,就要表明T的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 template <typename T>class A { T a; A& func () { cout<<"hello world" <<endl; } A& func2 () ; };template <typename T> A<T>& A<T>::func2 (){ cout<<"hello world 2" <<endl; return *this ; }
对于类模板中的静态数据,每个类型共享一个,而不是所有的共享一个。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 template <typename T>class A {public : static int count; };template <typename T>int A<T>::count = 0 ;int main () { A<int > a1; a1.count+=1 ; A<long > a2; a2.count+=10 ; cout<<A<int >::count<<endl; cout<<A<long >::count<<endl; return 0 ; }
普通类中包含模板函数 …
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class DebugDelete {public : template <typename T> void operator () (T*p) { cout<<"delete it" <<endl; delete p; } };int main () { double * p = new double (10 ); DebugDelete d; d (p); return 0 ; }
一个类型推断的小例子
1 2 3 4 template <typename It>auto func (It begin, It end) -> decltype (*begin) { return *begin; }
虽然不能直接将一个右值引用绑定到到一个左值上,但可以用move获得一个绑定到左值上的右值引用 。std::move不会创建新的对象,仅仅是类型转换,但是std::move会影响到编译器函数重载的匹配。 std::move(a) == static_cast< A&&>(a);
左值常引用相当于万能型:可以用左值或者右值进行初始化。
改成const &,可以省去参数的拷贝,一定要用const &,多用,好用。
完美转发 = 引用折叠 + 万能引用 + std::forward。
想保留左值右值属性的时候 ,用std::forward。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 template <typename FUNC, typename T1, typename T2>void flip (FUNC func, T1&& t1, T2&& t2) { func (t2,t1); func (std::forward<T2>(t2),std::forward<T1>(t1)); }void f (int v1,int & v2) { cout<<v1 <<" " <<++v2<<endl; }int main (int argc, char * argv[]) { int i = 10 ; flip (f,i,42 ); cout<<i<<endl; }
模板重载的时候,会按照匹配度去调用。 非模板匹配的好的,就会调用非函数模板。
可变参数模板:当参数个数未知,类型未知,一定要用模板,有点类似递归,要有一个最终的出口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 template <typename T>ostream& print (ostream& os, const T& t) { os<<t<<endl; return os; }template <typename T, typename ... Args>ostream& print (ostream& os, const T& t, Args... args) { os<<t<<" " ; print (os,args...); return os; }int main (int argc, char * argv[]) { print (cout,1 ,"string" , 0.0 , 3L ); }
命名空间:减少命名冲突。
头文件当中禁止使用using,cpp文件中放到匿名命名空间中。