文章目录
-
- 1. 资源管理
-
- 1.1 什么是资源
- 1.2 RAII管理资源的由来
- 1.3 管理资源的智能指针
- 2. 函数参数与返回值
-
- 3. 类
-
- 3.1 成员变量
- 3.2 编码习惯
- 3.3 类设计的理解
- 3. 标准库使用
-
- 4. 递归
- 5. 指针的使用
1. 资源管理
1.1 什么是资源
资源包括内存,文件句柄,socket句柄,db连接等
资源的特点是使用指针/句柄(handle)来获取一堆数据.
根据内存模型,内存可分为栈内存和堆内存,堆内存的生命周期由用户控制,包括资源申请和
资源释放,句柄也包含类似的申请和释放过程.
1.2 RAII管理资源的由来
编程时自己管理资源的弊端:
1)容易遗漏资源的释放(如delete指针)
2)一个函数中涉及较多资源的申请释放时代码比较丑陋
3)若在释放资源前发生异常,则会引起资源泄露,程序崩溃
为简化编程,另外引入对象来管理资源,对象(局部变量)定义在栈上,离开作用域时自动释放资源.
资源的表示:类对象的数据成员,资源的释放通过数据成员的销毁;
资源的获取在构造函数中完成(给数据成员赋值),资源的释放通过析构函数对数据成员的自动销毁完成;
在使用时资源管理类对象的初始化和资源的获取同时完成,消除中间环节,避免:
Handle handle = GetHandle()
process(handle)
HandleManager handle_manager(handle)
的形式,因为process(handle)可能会出现异常,导致资源无法释放.
该方式称为RAII(Resource Acquisition Is Initialization)资源获取即初始化.
1.3 管理资源的智能指针
以堆内存为例,对每种可能以new方式创建的类对象都要定义对应的资源管理类,很麻烦,可使用模板机制.
智能指针即是语言层面提供的资源管理类模板,包括std::unique_ptr, std::shared_ptr, std::weak_ptr,
boost::scoped_ptr.
scoped_ptr是对指针的简单包装,资源管理权唯一且不可复制不可转移(禁用了类的拷贝和移动语义)
unique_ptr是对指针的简单包装,资源管理权唯一,可转移
shared_ptr使用引用计数管理资源,资源管理权不唯一,可复制
weak_ptr依赖于shared_ptr而存在,为了解决shared_ptr循环引用导致资源无法释放的问题.
资源管理权不唯一,可复制,但是不增加引用计数
循环引用示例:
class A{ std::shared_ptr pb_; };
class B{ std::shared_ptr pa_; };
main() {
std::shared_ptr pa(new A());
std::shared_ptr pb = std::make_shared();
pa->pb_ = pb;
pb->pa_ = pa;
}
改进方法: 将pa_改为std::weak_ptr, pb_类似处理
scoped_ptr用于类: 作为数据成员,成员函数只修改对应资源类,并不返回类实例
shared_ptr用于类: 作为数据成员,成员函数需要返回类实例或作为参数传入
!!! 避免shared_ptr的滥用:
- 除非需要在函数的参数,返回值,标准容器中用到资源,需要对拷贝语义的支持才使用shared_ptr
- 如果使用资源且不需要拷贝使用unique_ptr
- 对定义在栈上的对象只能使用普通指针,不能使用智能指针,否则会多次释放(对象销毁时释放一次,
智能指针销毁时释放一次)
- 智能指针不能用于静态生命周期的变量,在变量析构(程序结束时)会引起内存泄露(2次free)
func() {
static X x; // free 2nd, memory leak
shared_ptr px(&x); // free 1st
}
其本质是智能指针的初始化方式不能是1) 实例化对象 2)用对象的地址初始化智能指针
而必须是 1)定义指向对象的指针,new 2)用指针初始化智能指针
因为第一种方式在析构时会析构2次,导致memory leak
2. 函数参数与返回值
2.1 函数参数
- 变量: 使用该变量,内置类型,智能指针也可,只使用,不改变
- 常量引用: 非内置类型,因为类类型的拷贝构造比较低效,没必要.只使用,不改变
- 指针: 调用前构造对象,通过函数改变该对象
2.2 函数返回值
- 变量: 内置类型,常为调用状态的指示(int), 调用是否成功(bool), std::shared_ptr
不返回局部对象的引用和指针的原因: 离开函数作用域后被销毁
创建类类型的对象只能在堆上,为了避免直接使用指针和支持拷贝语义,使用std::shared_ptr
3. 类
3.1 成员变量
- 如果不需要类创建对象,而是从外部传参,使用裸指针;
- 如果需要类创建对象,使用智能指针,在构造函数体内使用unique_ptr.reset(pointer);
- 对定义在栈上的对象使用普通指针,对定义在堆上的对象使用智能指针
- 对于指针类型,聚合类型(vector, set)的数据成员,不对其指向的元素或其内部的元素默认初始化
普通指针并不管理资源,析构指针对象仅仅是解除了该指针与原对象的关联同时清理该指针;
智能指针是将资源通过构造函数传入,赋值给智能指针类内的普通指针,释放时delete该普通指针
来完成资源的获取和自动释放,实现对资源的管理;
vector等聚合类型在析构时会释放其内部存储的对象
一般不建议在vector中存储指针,因为这样存储的指针是未定义的
3.2 编码习惯
- 如非明显需要,禁止拷贝,除了接口类,必须显式定义构造函数(也便于代码理解),
否则编译器会将拷贝构造函数误认为构造函数,创建对象出错
- 尽可能使用const, const参数用于引用传对象参数, const函数用于非void返回非指针的情形
注意声明为const的函数其内部显式使用的this指针变为指向常量的指针
- 单参数构造函数使用explicit关键字
3.3 类设计的理解
-
引入类机制的目的是方便对属性和方法的封装,面对使用者,起关键作用的是类方法
-
类的继承有两点目的:
a) 方法的复用,子类直接使用父类中的方法,同时增加自己的特定方法
b) 数据成员的复用,子类对象包含两部分:父类部分和子类特有部分,因此构造函数中必须使用
父类的构造函数初始化父类的数据成员
-
继承: 继承是子类对父类接口的丰富,途径是添加方法,应当避免在子类中使用和父类同名的方法,
父类和子类都会被使用
-
多态: 多态是子类对父类接口的覆盖,通过virtual 方法和指针/引用的配合完成运行时动态绑定
一般使用多态时,我们希望调用者用到子类而不再用到父类,即不能实例化父类
即使只有virtual析构函数而没有virtual成员函数,也能构成多态
抽象类的设计:
a) 成员函数:
- 父类自己实现,子类直接继承,不能在子类中修改,负责对象之间的共性部分
- 父类声明为纯虚函数,子类必须自己实现,负责对象之间的特性部分
- virtual 只和纯虚函数配合使用,不在父类中实现virtual函数
- 面向抽象类编程,具体子类中的所有接口(声明为public的成员函数)均在父类中
声明.且为public,因为面向抽象类编程的目标即为提供扩展性,保持接口不变,
不需要修改代码,只需要扩充实现的具体类即可增加新功能,同时用户的调用方式
不会受影响
- 如果发现父类中的接口并非在每一个子类中都用到,应当拆分为多个接口,每个接口
实现单一的功能,子类通过多继承接口来实现
- 父类中需要供子类访问但不需要外部访问的函数声明为protected
- 类内调用的函数一律声明为private
b) 数据成员:
- 纯接口没有数据成员,不纯的接口有数据成员,但二者均包含纯虚函数,不能被
实例化
- 引入数据成员的目的是具体对象的共性操作依赖于一个数据成员,该数据成员
可能表示共性状态,也可能表示共性外部依赖
- 数据成员如果是共性状态,则声明为private,因为抽象类负责操作,子类不需要
操作
- 数据成员如果是共性外部依赖,且子类中需要访问该外部依赖,使用protected,
2种方法取其一:
- 抽象类提供protected函数用于访问共性外部依赖数据成员,此时返回的应当是
具体子类都需要看到的公共信息.因为通过函数提供的信息灵活性很差,限制
了外部依赖能返回的信息类型,确实有必要让所有子类看到这一个信息才如此
- 抽象类用protected关键字修饰共性外部依赖数据成员,此时返回的信息可以由
子类自己决定,因为可以调用共性外部依赖的不同方法,有了灵活性
c) 构造函数和析构函数
- 抽象类若有数据成员,则定义为protected,否则不定义,因为子类可能使用抽象
父类的无参构造函数,如果有数据成员,默认的构造函数可能无能为力
具体类如果只能通过传参初始化,则不显示定义无参构造函数,否则就显示定义
- 只要显示写出构造函数,就要定义,不能只声明,否则会报错"未定义的引用"
- 始终初始化非static内置类型数据成员,如int, char, double, 指针类型,因为这些
类型不会默认初始化,而类对象会调用默认构造函数初始化(很少直接使用类对象,
常使用的有std::string)
- vector内最好不要使用指针,但对于需要保存抽象类元素的vector,只能存储
指针,因为抽象类不能被实例化
- 子类的构造函数会自行调用父类的无参构造函数,但不会自行调用父类的有参
构造函数,因此只需要Derived(param or not),不需要Derived(param or not)
: Base(), 除非需要 Derived(param) : Base(param)
- 始终显式定义virtual析构函数,因为子类对象析构时指针是父类,若父类析构函
数不是virtual,则不会调用子类的析构函数,对象无法被清理;
- 不同于构造函数需要子类显式调用,子类析构时其实编译器自动调用了父类的
析构函数,子类的析构函数负责清理子类部分,父类的析构函数负责清理父类
部分;
- 虚析构函数不是纯虚函数,因此必须定义
d) 禁止拷贝构造和赋值
不对抽象类禁止拷贝构造和赋值,因为没必要,抽象类使用纯虚函数,已经不能实例化
对象,在子类构造函数中调用并不算实例化.
3. 标准库使用
3.1 vector的删除
注意迭代器失效:
iterator erase(iterator it);
iterator erase(iterator start, iterator end);
- 删除元素后指向下一迭代器,当删除最后一个元素后要避免越界访问(++it).
- 该删除操作改变vector的size()
- 不能使用解引用判断是否到达尾后迭代器,以int元素为例,未使用erase,尾后迭代器解引用为0,
使用一次后,尾后迭代器的解引用与最后一个元素的值相等
- 删除一个元素可使用erase(),find后立即返回
- 删除多个元素不可for循环遍历迭代器来删除,要使用std::remove
iterator remove(iterator start, iterator end, T target)
并不实际删除元素,而是通过元素的前移覆盖被删除的元素,返回最后一个移动元素的下一位置
如 2 1 5 3 5 8 9 10删除元素5后变为 2 1 3 8 9 10 9 10,返回的迭代器指向倒数第一个9,因此
不会导致迭代器失效,执行删除时可调用erase(remove(begin, end, target), end)
4. 递归
- 递归是函数的一种编写方式,"调用自身"指的是在一个函数内调用该函数(普通函数or成员函数)本身,
经过编译器的实现达到的效果类似于父节点调用其子节点,其中父子节点有相同的函数接口
- 对象的递归要求抽象出父类对象和子类对象的共同接口,针对次共同接口编程
- 递归的要点就2个: 递推关系式 and 终止条件
- 递归并未减小实际运行的时间开销,其实还增加了栈的空间开销.编译器内部对递归的展开减轻了编程难度,
但是要留意递归深度,过深会导致内存爆掉
5. 指针的使用
只在三种情况下使用裸指针:
a) 不需要返回对象指针
b) 确定只在main函数中创建该对象,然后给使用者,使用栈对象
c) 创建型模式,专门的类来创建该对象,使用new,创建好的对象不暴露给用户,只用于
自己实现的接口内部来使用,即new创建对象只是中间的一步,并不作为接口的一部分
如果可能在函数或类中创建并返回类对象的指针,不能使用new,必须使用shared_ptr或
unique_ptr,因为无法保证使用者不直接使用裸指针