C++ 类成员函数全家桶

RAII

Resource Acquisition Is Initialization,资源获取即初始化

这是一种解决资源管理问题的方法,将资源的有效期与持有资源的对象的生命期严格绑定,由对象的构造函数完成资源的分配,由析构函数完成资源的释放
C++借助构造函数和析构函数,解决了传统的 malloc & freenew & delete 管理资源方法无法有效应对复杂资源管理场景的问题

对于资源管理需求,无GC机制的C++语言提供了基于析构函数的RAII方法和智能指针,有GC机制的Java语言提供了垃圾回收机制(Garbage Collection,GC).需要注意的是,虽然它们都能避免发生内存泄漏问题,但实现原理并不相同.

首先,从语言的设计角度来说,C++的设计应用场景要求其对代码的执行过程做到可控,RAII方法仍然需要手动释放资源,而GC机制并不需要开发者手动释放资源(这也是RAII方法和GC机制的一个显著区别),
即RAII并没有剥夺开发者手动管理资源的权限,它只是通过构造和析构函数提供了一种安全管理资源的方法,开发者对于执行过程是具有控制权的.而GC机制把资源的回收过程交由JVM负责,开发者无权控制.

关于可控性,还体现在析构函数和垃圾回收发生的时机上,在C++中通过{}来界定作用范围,当超出作用范围后即会调用对象的析构函数,即析构函数的调用时间是可预知的(适用于对时序有严格要求的场景),而垃圾回收机制的发生时间取决于JVM的资源管理策略,是开发者无权控制的.

前言

资源管理和类的控制实际是两个分离的过程,RAII方法则是利用类的成员函数解决资源管理的方法之一

普通构造函数

无参

  1. 自定义
    1.1 普通形式
    1.2 使用初始化表达式

  2. 默认
    POD陷阱(哪些类型不会自动初始化为0,大括号指定初始化值(0初始化方式))或等号指定初始化值(无法采用小括号指定初始化值)
    支持单纯的大括号初始化,等号和大括号同时使用的初始化方式

当自定义构造函数后,默认无参构造函数就不存在了,可通过default生成默认的无参构造函数

单参数

此时单参数的构造函数,实现了从一种类型隐式地转换为类类型,因此此时的构造函数也称为转换构造函数,需要注意的是C++ 类型转换中提到的编译器只支持一步隐式类型转换规则.

如果想禁止这种隐式类型转换,可以使用explicit关键字修饰单参数的构造函数,此时该构造函数只能通过直接初始化使用,编译器也不会在隐式类型转换过程中调用该构造函数

额外需要注意的是explicit关键字只能出现在类内部构造函数声明处
explicit的中文释义是明确的,也就是说其修饰的函数应当显式调用,如果不加此修饰符,隐式类型转换过程会在背后执行这一函数,不能一眼看出

多参数

  1. 普通形式
  2. 使用初始化表达式
class ConstRef {
public:
	ConstRef(int ii);
private:
	int i;
	const int ci;
	int &ri;
};

ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) {}

What circumstances must use the initialization expression ?

  1. const
  2. quote
  3. class type which doesn't provide default constructor function

explicit禁止隐式转换(我自己说的,具体作用要查一下)

拷贝构造函数(copy constructor)

  • 定义了用同类型的另一个对象初始化本对象时的操作

首先涉及到的就是拷贝的概念。拷贝分为深拷贝和浅拷贝。
浅拷贝是指把一个对象所在内存中的数据以二进制形式复制到另一个对象中,对于包含指针类型成员变量的类而言,浅拷贝意味着源对象和目标对象的指针指向相同的内存空间,即改变其中一者的值,另一者的值也会发生改变。在此情况下,深拷贝在复制对象本身同时,会把对象拥有的数据一同复制,源对象和目标对象是两个独立的对象。

拷贝构造函数特征

  • 第一个参数是自身类型引用
  • 额外参数均存在默认值
  • 存在几种情况需要被隐式使用,因此通常不设置为explicit
  • 可通过delete禁用拷贝构造函数
拷贝构造函数示例代码
class Foo {
public:
  Foo();
  Foo(const Foo&);
};

问: 为什么拷贝构造函数的参数需要是引用类型
答: 拷贝构造函数用于初始化非引用类类型参数,为了调用拷贝构造函数,需要拷贝它的实参,如果参数不是引用类型,则为了拷贝实参,仍然需要调用拷贝构造函数,进入无限循环[1].(目前并没有完全理解)

拷贝赋值运算符(copy-assignment operator)

  • 定义了将一个对象赋值同类型的另一个对象时的操作

拷贝赋值运算符并不是一个新的内容,其遵循运算符重载的规则

可以通过delete禁止对象赋值

struct NoCopy {
  NoCopy& operator=(const NoCopy&) = delete; // 禁止赋值
};

为何需要自定义拷贝赋值运算符?
一个类类型中包含多个成员属性,如果在进行对象赋值时,仅需要修改部分属性值,则需要自定义实现拷贝赋值运算符

需要了解拷贝构造函数和拷贝赋值运算符作用上的区别,拷贝构造是在对象还没有初始化时,拷贝另一个对象用于初始化当前对象,拷贝赋值是当前对象已经初始化,用另一个对象修改当前对象.

移动构造函数(move constructor)

  • 定义了用同类型的另一个对象初始化本对象时的操作
  • C++11 引入
Node(Node &&) = delete;

问: 为何引入移动语义?
答: 1. 多数情况下对象拷贝操作后就被销毁了,此时移动要比拷贝+删除更能提高性能
2. 拷贝意味着存在多份,由于某些资源不允许共享,因此不能拷贝但是可以移动

即使有以上两点引入移动语义的原因,但有时仍会产生一些疑问,例如:想不到移动操作的应用场景有哪些,移动操作的效果是把一个对象的数据移动到另一个对象中,并且让源对象数据失效,数据在源对象中正常存在为什么一定要移动到一个新的对象当中呢,并且还让源对象失效,既然这样为什么不直接使用源对象。
解释:关于这一点疑惑,unique_ptr就是一个用于解释很好的例子,参数传递是一个会发生对象转移的场景,而unique_ptr恰好不支持出现多份数据,只能保留一份,在没有移动语义之前,这种场景极容易发生错误,而移动语义则在保证始终只保留一份数据的情况下,做到数据的转移。

在理解移动语义之前,需要理解C++ 左值、右值、右值引用的概念,主要是std::move函数的概念

在能够确定移动构造函数不会触发异常时,要添加noexcept标识符
原因:p500 避免标准库容器在不清楚情况的状态下做出一些损耗性能的额外工作
不过这种规则似乎已经涉及到优化的问题了,而我们目前还没有达到这一步

移动赋值运算符(move-assignment operator)

  • 定义了将一个对象赋值同类型的另一个对象时的操作
  • C++11 引入
Node& operator=(Node &&) = delete;

解构(析构)函数(destructor)

定义了对象销毁时的操作

解构(析构)函数特征

  • 无参数,故不能被重载
  • 一个类只存在一个析构函数

合成操作

所谓"合成"是指在开发者没有明确给出实现的情况下,由编译器负责给出默认的实现

  1. 合成默认构造函数

  2. 合成拷贝操作(拷贝构造函数和拷贝赋值运算符)的条件
    未声明自定义拷贝构造和拷贝赋值时,编译器会合成这些操作

  3. 合成移动操作(移动构造函数和移动赋值运算符)的条件

  • 类没有自定义的拷贝控制函数和拷贝赋值运算符
  • 类的每个非static数据成员均可移动

三/五法则

  • 三法则是指负责拷贝控制操作的拷贝构造函数拷贝赋值运算符析构函数三者的使用规则
  • 五法则是指在三法则基础上,添加了C++11中新增的移动语义所带来的移动构造函数移动赋值运算符的使用规则
  1. 如果一个类需要自定义析构函数,那么它也需要自定义拷贝构造函数和拷贝赋值运算符
    解释: 需要自定义析构函数,说明资源管理是需要自定义的,那么资源分配过程显然也需要自定义,自然就需要自定义拷贝构造和赋值;

  2. 需要拷贝操作的类也需要赋值操作,需要赋值操作的类也需要拷贝操作
    拷贝构造和拷贝赋值是近似的操作,二者几乎是同时存在的,但是需要构造和赋值并不一定需要析构


  1. C++ Primer 中文版(第 5 版)-P442-参数和返回值 ↩︎

posted @ 2023-06-18 18:48  0x7F  阅读(31)  评论(0编辑  收藏  举报