c++基本编程实践手记

文章目录

    • 1. 资源管理
      • 1.1 什么是资源
      • 1.2 RAII管理资源的由来
      • 1.3 管理资源的智能指针
    • 2. 函数参数与返回值
      • 2.1 函数参数
      • 2.2 函数返回值
    • 3. 类
      • 3.1 成员变量
      • 3.2 编码习惯
      • 3.3 类设计的理解
    • 3. 标准库使用
      • 3.1 vector的删除
    • 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,因为无法保证使用者不直接使用裸指针

你可能感兴趣的:(c++,raii)