拷贝控制
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. 深拷贝:在堆区重新申请空间,进行拷贝操作。
编译器会默认生成拷贝构造函数,但是如果类里面有动态申请的内存空间,那么一定要自定义拷贝构造函数,用深拷贝去解决浅拷贝带来的问题,并且拷贝构造函数/类中重载赋值运算符一定要使用索引,因为不用索引的话,去调用对象的时候会首先调用一次拷贝构造,造成死循环
类似实现一种资源管理的方式,应该从哪里考虑:
构造函数初始化,拷贝构造++,析构函数--,如果减到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. 函数调用重载:仿函数,定义类似函数的行为,比较自由。
拷贝构造函数和拷贝赋值运算符的调用时机:
#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;
}
- 类和类之间的关系一般就是继承关系,或者是组合关系。
- 委托关系:一个类中包含指向另一个类的指针。
面向对象
- 运行时多态:虚函数+动态绑定:
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(); // cout Comic Book
b2.func(); // cout Action Book
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的类型。
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;
}
对于类模板中的静态数据,每个类型共享一个,而不是所有的共享一个。
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; // 1
cout<<A<long>::count<<endl; // 10
return 0;
}
普通类中包含模板函数 …
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;
}
- 一个类型推断的小例子
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。
template <typename FUNC, typename T1, typename T2>
void flip(FUNC func, T1&& t1, T2&& t2){
func(t2,t1); // 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;
// FUNC->f
// t1->int
// t2->int
// 左值传到右值里面相当于一个引用
flip(f,i,42);
cout<<i<<endl; //11
}
- 模板重载的时候,会按照匹配度去调用。 非模板匹配的好的,就会调用非函数模板。
- 可变参数模板:当参数个数未知,类型未知,一定要用模板,有点类似递归,要有一个最终的出口。
//出口
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文件中放到匿名命名空间中。