update 2025.03.01,对实际情况中遇到的内容使用📌标记。

一.关键字/基础知识

CONST

1.作用

  • 定义常量,防止修改,起保护作用。
  • 类型检查:#define不会进行类型检查,const则是具有类型的。
  • 节省空间:常量只有一份数据,可以避免多份拷贝。

2.默认为文件局部变量

const 对象默认为文件局部变量,如果需要在其他文件中访问,则需要extern定义:

1
2
3
4
5
6
7
//file1.cpp
#include<iostream>
extern const int ext = 2; //无const修饰的变量不需要extern指定
int main(){...}
//file2.cpp
extern const int ext;
int main(){...}

3.常量在定义时必须被初始化

4.常指针和指向常量的指针

  • const int 为指向常量的指针, int const为常指针。
  • 指向常量的指针可以指向非常量,但不能修改。
  • 常指针必须初始化且不能修改,常指针不能指向常量,如果需要则必须为const int * const形式。

5.函数中使用const

  • const指向参数或常指针参数无意义,因为本来就是临时变量拷贝的。
  • 可以为指向常量的指针。
  • 可以为引用。

6.类中使用const

  • const成员函数不能修改成员或调用非const成员函数。
  • const对象只能访问const成员函数。
  • const成员变量必须初始化列表初始化,C++11支持定义处初始化,或者可以结合static进行定义处初始化。
1
2
3
4
5
6
7
8
9
class Apple{
private:
int people[100];
public:
Apple(int i);
const int apple_number;
};

Apple::Apple(int i):apple_number(i){}

STATIC

1.C语言中的STATIC

  • STATIC修饰的全局变量/函数只能在本文件使用,其他文件无法直接调用。即无外部链接性。
  • STATIC修饰的局部变量必须初始化且仅初始化一次,在程序编译时确定初始值,程序运行结束后才销毁。

2.C++中的STATIC

类中的静态变量由对象共享。不能使用构造函数初始化,也不能在类中初始化,📌只能在外部进行初始化。

类中的静态函数只能访问静态变量。

📌工厂单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace base{
class CPUDeviceAllocatorFactory{
public:
static std::shared_ptr<CPUDeviceAllocator> get_instance(){
if(instance == nullptr){
instance = std::make_shared<CPUDeviceAllocator>();
}
return instance;
}
private:
static std::shared_ptr<CPUDeviceAllocator> instance;
};
}
//alloc_cpu.cpp
namespace base{
std::shared_ptr<CPUDeviceAllocator> CPUDeviceAllocatorFactory::instance = nullptr;
}

THIS

this指针指向类对象本身,通常用于返回对象本身,或在参数和成员变量名相同时使用。

INLINE

inline可以避免函数调用的开销,但会导致代码膨胀,因此只在函数非常短时使用。

虚函数可以是内联函数,但只有在已知对象是哪个类时可以内联。使用指针访问时表现多态性,不能调用内联的虚函数。

SIZEOF

  • 空类的大小为1字节。
  • 类中的函数和静态数据成员不占用对象存储空间。
  • 继承时,派生类继承基类成员,按照字节对齐计算大小。
  • 继承多个有虚函数的基类时,继承多个虚指针。

纯虚函数和抽象类

  • 📌纯虚函数通过赋值0来声明。包含纯虚函数的类为抽象类。
  • 所有纯虚函数被覆盖,类才不会变为抽象类。
  • 纯虚函数没有实现,虚函数必须实现。
1
2
3
4
class Shape {
public:
virtual void draw() = 0; // 纯虚函数
};

虚函数

  • 友元不是成员函数,不能是虚函数,但是可以让友元函数调用虚函数,实现友元的虚拟。
  • 虚函数是通过vptr实现多态的。每个使用虚函数的类都有一个vptr,指向虚函数表,表中为函数指针。当使用基类指针调用虚函数时,通过vptr找到子类的虚函数表调用子类的函数实现。子类的虚函数表中没有重写的虚函数都仍然为基类指针,重写的为指向重写函数的指针。
  • 📌构造函数不能是虚函数,析构函数可以是,并且析构函数应该是虚函数,调用相应类型的对象的析构函数,然后调用基类的虚构函数。如果不是虚函数,使用基类指针进行delete时,就会导致派生类析构未调用,释放不正确。例如以下代码,基类析构就必须是virtual的。
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
#include <iostream>

class Base {
public:
virtual ~Base() { // 将析构函数声明为 virtual
std::cout << "Base destructor" << std::endl;
}
};

class Derived : public Base {
private:
int* data;

public:
Derived() {
data = new int[100]; // 动态分配内存
std::cout << "Derived constructor" << std::endl;
}

~Derived() {
delete[] data; // 释放内存
std::cout << "Derived destructor" << std::endl;
}
};

int main() {
Base* ptr = new Derived(); // 基类指针指向派生类对象
delete ptr; // 通过基类指针删除对象
return 0;
}
  • 函数的默认参数是静态绑定的,因此默认参数是根据指针或引用使用的,而不是对象类型。例如base指针调用函数,使用的是基类的默认参数。调用的是指向对象的虚函数实现,取决于实际的指向对象。
  • 静态函数不能是虚函数,因为他不属于任何类对象。
  • 虚函数私有:要把基类声明为public,继承类为private,就可以用指针正常访问。

VOLATILE

  • volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
  • volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)。
  • const 可以是 volatile (如只读的状态寄存器),指针可以是 volatile。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
volatile  bool bStop=false;  //bStop 为共享全局变量 
//如果bstop不用volatile声明,这个值可能会读取到寄存器当中,从而导致thread中的死循环。
//第一个线程
void threadFunc1()
{
...
while(!bStop){...}
}
//第二个线程终止上面的线程循环
void threadFunc2()
{
...
bStop = true;
}

ASSERT

断言主要用于检查逻辑上不可能的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h> 
#include <assert.h>

int main() {
int x = 7;
/* Some big code in between and let's say x
is accidentally changed to 9 */
x = 9
// Programmer assumes x to be 7 in rest of the code
assert(x==7);
/* Rest of the code */
return 0;
}

在代码开头加上#define NDEBUG可以忽略断言。

位域

位域必须是整型或枚举,通常使用结构体声明,为每个成员设置名称,并决定其宽度:

1
2
3
4
5
6
struct _PRCODE
{
unsigned int code1: 2;
unsigned int cdde2: 2;
unsigned int code3: 8;
};

C语言用 unsigned int 作为位域的基本单位,即使一个结构的唯一成员为 1 Bit 的位域,该结构大小也和一个 unsigned int 大小相同。 有些系统中,unsigned int 为 16 Bits,在 x86 系统中为 32 Bits。

一个位域成员不允许跨越两个 unsigned int 的边界,如果成员声明的总位数超过了一个 unsigned int 的大小, 那么编辑器会自动移位位域成员,使其按照 unsigned int 的边界对齐。

extern “C”

C++和C中对函数编译生成的符号不同,C编译的函数名类似_add,没有参数,因为C语言中没有重载。所以如果链接C和CPP文件,则需要在引用C的头文件时,加extern “C”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//add.h
#ifndef ADD_H
#define ADD_H
int add(int x,int y);
#endif
//add.c
#include "add.h"
int add(int x,int y) {
return x+y;
}

//add.cpp
#include <iostream>
using namespace std;
extern "C" {
#include "add.h"
}
int main() {
add(2,3);
return 0;
}

要将.c文件编译为.o文件(gcc -c)后进行链接。

STRUCT

struct名字和函数名可以相同,但是不能是typedef定义别名,而且必须在声明前加struct:

1
2
3
4
5
6
7
8
9
//不定义别名
struct Student {};
Student(){}
Struct Student s; //ok
Student s; //error

//定义别名
typedef struct Base1 {}B;
//void B() {} //error! 符号 "B" 已经被定义为一个 "struct Base1" 的别名

和类的区别为struct成员默认为public,更适合看成数据结构实现体。

UNION

联合可以有多个数据成员,任意时刻只有一个数据成员有值。默认为public,可以有构造和析构函数,不能含有引用类型成员,不能继承或作基类或含虚函数。

EXPLICIT

📌explicit修饰构造函数,防止隐式转换和拷贝初始化;修饰转换函数时可以防止隐式转换,按语境转换除外。实际上,大部分拷贝初始化都会被自动优化为直接初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct B{
int num;
explicit B(int b): num(b){}
};

void func(B b){}

int main(){
B b1(1); //OK
func(1); //error
B b2 = 1; //error
B b3 = (B)1;//OK static_cast
}

FRIEND

友元共有两种形式,友元函数和友元类。友元没有继承和传递。

友元函数声明在类中,定义在外部:

1
2
3
4
5
6
7
8
9
10
class A{
public:
A(int _a):a(_a){};
friend int geta(A &ca); ///< 友元函数
private:
int a;
};
int geta(A &ca) {
return ca.a;
}

友元类声明在类声明中,实现在类外:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A{
public:
A(int _a):a(_a){};
friend class B;
private:
int a;
};

class B{
public:
int getb(A ca) {
return ca.a;
};
};

📌对于流重载这样的操作,不一定要使用友元,类应该有对外接口访问其成员。

1
2
3
4
std::ostream& operator<<(std::ostream& os, const Status& x) {
os << x.get_err_msg();
return os;
}

USING

作用域:

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
#include <iostream>
#define isNs1 1
//#define isGlobal 2
using namespace std;
void func() {
cout<<"::func"<<endl;
}

namespace ns1 {
void func(){
cout<<"ns1::func"<<endl;
}
}

namespace ns2 {
#ifdef isNs1
using ns1::func; /// ns1中的函数
#elif isGlobal
using ::func; /// 全局中的函数
#else
void func() {
cout<<"other::func"<<endl;
}
#endif
}

int main() {
/**
* 这就是为什么在c++中使用了cmath而不是math.h头文件
*/
ns2::func(); // 会根据当前环境定义宏的不同来调用不同命名空间下的func()函数
return 0;
}

改变访问性:

1
2
3
4
5
6
7
8
9
10
11
12
class Base{
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
};
class Derived : private Base {
public:
using Base::size; //改变私有继承的size()为public
protected:
using Base::n;
};

重载:在继承过程中,派生类可以覆盖重载函数的0个或多个实例,一旦定义了一个重载版本,那么其他的重载版本都会变为不可见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

class Base{
public:
void f(){ cout<<"f()"<<endl;}
void f(int n){cout<<"Base::f(int)"<<endl;}
};

class Derived : private Base {
public:
using Base::f;
void f(int n){cout<<"Derived::f(int)"<<endl;}
};

int main(){
Base b;
Derived d;
d.f();
d.f(1);
return 0;
}

取代typedef:

1
2
typedef vector<int> V1; 
using V2 = vector<int>;

ENUM

枚举有一个问题是作用域不受限,会引起命名冲突:

1
2
enum Color {RED,BLUE};
enum Feeling {EXCITED,BLUE};

经典做法是加上前缀:COLOR_BLUE,但是这样会很累赘,有个代替的方法是使用结构体来限定作用域:

1
2
3
4
5
6
7
struct Color1{
enum Type{
RED=102,
YELLOW,
BLUE
};
};

另外两个问题是enum会转换为int,以及表示枚举变量的实际类型不能明确指定,C++11中引入了枚举类来解决:

1
2
3
4
5
6
7
enum class Color2{
RED=2,
YELLOW,
BLUE
};
Color2 c2 = Color2::RED;
cout << static_cast<int>(c2) << endl; //必须转!

枚举类可以用特定类型来存储enum:

1
2
3
4
5
6
7
8
enum class Color3:char;  // 前向声明

// 定义
enum class Color3:char {
RED='r',
BLUE
};
char c3 = static_cast<char>(Color3::RED);

类中可以使用枚举来表示常量,枚举常量不会占用对象的存储空间,它们在编译时被全部求值。

1
2
3
4
5
6
7
8
class Person{
public:
typedef enum {
BOY = 0,
GIRL
}SexType;
};
//访问的时候通过,Person::BOY或者Person::GIRL来进行访问。

decltype

decltype(expression)用于查询表达式的类型(不会对表达式求值)。常见用法:

  • 推导表达式类型:decltype(i) a;
  • 定义类型
  • 重用匿名类型
1
2
3
4
5
struct {
int d ;
doubel b;
}anon_s;
decltype(anon_s) as ;//定义了一个上面匿名的结构体
  • 泛型编程中结合auto,追踪函数的返回类型(最大用途):
1
2
3
4
template <typename T>
auto multiply(T x, T y)->decltype(x*y){
return x*y;
}

引用和指针

引用与指针

  • 引用必须初始化,指针不必要。

  • 引用不能为空,指针可以,所以指针需要检查是否为空。

  • 指针可以随时改变指向,但是引用无法改变,只能指向初始化时指向的对象。

引用

左值是存储在内存中、有明确存储地址(可寻址)的数据,右值是可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。

  • 左值引用是常规引用,一般表示对象的身份。左值引用要求右边的值必须能够取地址,如果不能取址,可以使用指向常量的引用,不过这样数据就无法修改了。
1
const int &var = 10;	//常量有地址,可以取址
  • 右值引用是必须绑定到一个临时对象、将要销毁的对象的引用,一般表示对象的值。右值引用主要目的是消除不必要的对象拷贝,更简洁明确地定义泛型函数。
1
2
3
4
5
int num = 10;
//int && a = num; //右值引用不能初始化为左值
int && a = 10; //右值引用可以修改
a = 100;
cout << a << endl;

引用型返回值

使用引用型返回值常常是在运算符重载时,例如重载[]:

1
2
vector<int> v(10);
v[5] = 10; //[]操作符返回引用,然后vector对应元素才能被修改

常量左值引用

常量左值引用既可以绑定左值又可以绑定右值,常见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//不考虑编译器优化
#include <iostream>
using namespace std;

class Copyable {
public:
Copyable(){}
Copyable(const Copyable &o) {
cout << "Copied" << endl;
}
};
Copyable ReturnRvalue() {
return Copyable(); //返回一个临时对象
}
void AcceptVal(Copyable a) {}
void AcceptRef(const Copyable& a) {}

int main() {
cout << "pass by value: " << endl;
AcceptVal(ReturnRvalue()); // 应该调用两次拷贝构造函数
cout << "pass by reference: " << endl;
AcceptRef(ReturnRvalue()); //应该只调用一次拷贝构造函数
}

移动语义

移动语义是为了解决不必要的构造次数过多的问题产生的方法,例如以下的一段MyString类实现:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class MyString
{
public:
static size_t CCtor; //统计调用拷贝构造函数的次数
// static size_t CCtor; //统计调用拷贝构造函数的次数
public:
// 构造函数
MyString(const char* cstr=0){
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = '\0';
}
}

// 拷贝构造函数
MyString(const MyString& str) {
CCtor ++;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
// 拷贝赋值函数 =号重载
MyString& operator=(const MyString& str){
if (this == &str) // 避免自我赋值!!
return *this;

delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}

~MyString() {
delete[] m_data;
}

char* get_c_str() const { return m_data; }
private:
char* m_data;
};
size_t MyString::CCtor = 0;

int main()
{
vector<MyString> vecStr;
vecStr.reserve(1000); //先分配好1000个空间,不这么做,调用的次数可能远大于1000
for(int i=0;i<1000;i++){
vecStr.push_back(MyString("hello"));
}
cout << MyString::CCtor << endl;
}

在push_back的时候进行了1000次拷贝构造,构造出来的临时字符串都没有什么用,导致了没用的资源申请和释放。如果使用移动语义,就可以直接进行移动:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class MyString{
public:
static size_t CCtor; //统计调用拷贝构造函数的次数
static size_t MCtor; //统计调用移动构造函数的次数
static size_t CAsgn; //统计调用拷贝赋值函数的次数
static size_t MAsgn; //统计调用移动赋值函数的次数

public:
// 构造函数
MyString(const char* cstr=0){
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = '\0';
}
}

// 拷贝构造函数
MyString(const MyString& str) {
CCtor ++;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
// 移动构造函数
MyString(MyString&& str) noexcept
:m_data(str.m_data) {
MCtor ++;
str.m_data = nullptr; //不再指向之前的资源了
}

// 拷贝赋值函数 =号重载
MyString& operator=(const MyString& str){
CAsgn ++;
if (this == &str) // 避免自我赋值!!
return *this;

delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}

// 移动赋值函数 =号重载
MyString& operator=(MyString&& str) noexcept{
MAsgn ++;
if (this == &str) // 避免自我赋值!!
return *this;

delete[] m_data;
m_data = str.m_data;
str.m_data = nullptr; //不再指向之前的资源了
return *this;
}

~MyString() {
delete[] m_data;
}

char* get_c_str() const { return m_data; }
private:
char* m_data;
};
size_t MyString::CCtor = 0;
size_t MyString::MCtor = 0;
size_t MyString::CAsgn = 0;
size_t MyString::MAsgn = 0;
int main()
{
vector<MyString> vecStr;
vecStr.reserve(1000); //先分配好1000个空间
for(int i=0;i<1000;i++){
vecStr.push_back(MyString("hello"));
}
cout << "CCtor = " << MyString::CCtor << endl;
cout << "MCtor = " << MyString::MCtor << endl;
cout << "CAsgn = " << MyString::CAsgn << endl;
cout << "MAsgn = " << MyString::MAsgn << endl;
}

以上代码直接把右值引用的指针修改了,相当于直接让自己的指针指向右值引向的临时变量的值,这样就避免了一次没有意义的拷贝,而临时变量也马上会被销毁,因此将其指针改为nullpter就可以了。(必须要改,不然数据被释放了)

delete空指针是合法的。并且为了安全,delete非空指针后最好要把指针置空,因为delete只是释放指向的空间,指针还会指向那部分空间。

右值引用可以移动,如果左值引用的局部变量生命周期很短,也是可以移动的,C++11提供了std::move()来将左值转换为右值,告诉编译器使用移动构造函数。

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
int main()
{
vector<MyString> vecStr;
vecStr.reserve(1000); //先分配好1000个空间
for(int i=0;i<1000;i++){
MyString tmp("hello");
vecStr.push_back(tmp); //调用的是拷贝构造函数
}
cout << "CCtor = " << MyString::CCtor << endl;
cout << "MCtor = " << MyString::MCtor << endl;
cout << "CAsgn = " << MyString::CAsgn << endl;
cout << "MAsgn = " << MyString::MAsgn << endl;

cout << endl;
MyString::CCtor = 0;
MyString::MCtor = 0;
MyString::CAsgn = 0;
MyString::MAsgn = 0;
vector<MyString> vecStr2;
vecStr2.reserve(1000); //先分配好1000个空间
for(int i=0;i<1000;i++){
MyString tmp("hello");
vecStr2.push_back(std::move(tmp)); //调用的是移动构造函数
}
cout << "CCtor = " << MyString::CCtor << endl;
cout << "MCtor = " << MyString::MCtor << endl;
cout << "CAsgn = " << MyString::CAsgn << endl;
cout << "MAsgn = " << MyString::MAsgn << endl;
}

但是需要注意变量作用域,tmp在移动构造以后失去了内容,但是还没有被销毁,这之后使用是可能产生错误的。

通用引用

当遇到模版中的自动类型推导或auto时,&&会变成通用引用,到底是左值还是右值,取决于他的初始化:

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
template<typename T>
void f( T&& param){}
f(10); //10是右值
int x = 10;
f(x); //x是左值

//----------------------------------------------------------

template<typename T>
void f( T&& param); //这里T的类型需要推导,所以&&是一个 universal references

template<typename T>
class Test {
Test(Test&& rhs); //Test是一个特定的类型,不需要类型推导,所以&&表示右值引用
};

void f(Test&& param); //右值引用

//复杂一点
template<typename T>
void f(std::vector<T>&& param); //在调用这个函数之前,这个vector<T>中的推断类型
//已经确定了,所以调用f函数的时候没有类型推断了,所以是 右值引用

template<typename T>
void f(const T&& param); //右值引用
// universal references仅仅发生在 T&& 下面,任何一点附加条件都会使之失效


完美转发

完美转发是指函数将参数进行转交时,不会改变参数的左/右值特征,就是完美的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void process(int& i){
cout << "process(int&):" << i << endl;
}
void process(int&& i){
cout << "process(int&&):" << i << endl;
}

void myforward(int&& i){
cout << "myforward(int&&):" << i << endl;
process(i);
}

int main(){
int a = 0;
process(a); //a被视为左值 process(int&):0
process(1); //1被视为右值 process(int&&):1
process(move(a)); //强制将a由左值改为右值 process(int&&):0
myforward(2); //右值经过forward函数转交给process函数,却称为了一个左值,
//原因是该右值有了名字 所以是 process(int&):2
myforward(move(a)); // 同上,在转发的时候右值变成了左值 process(int&):0
// forward(a) // 错误用法,右值引用不接受左值
}

上面的例子是不完美转发,C++11提供了std::forward()函数解决这个问题:

1
2
3
4
5
6
void myforward(int&& i){
cout << "myforward(int&&):" << i << endl;
process(std::forward<int>(i));
}

myforward(2); // process(int&&):2

上面修改后,右值可以完美转发了,但是左值还是不行,这时就需要借助上述提到的通用引用来解决:

1
2
3
4
template<typename T>
void perfectForward(T && t) {
RunCode(forward<T> (t));
}

这样无论是左值还是右值,都可以完美转发了。

二.常见方法

1.RAII

RAII的意思是资源获取即初始化,是一种编程技术。RAII保证资源的声明周期与一个对象的生存期相绑定。保证资源能够用于任何访问该对象的函数,避免内存泄露。

RAII可以总结如下:

  • 每个资源封装在一个类中,构造函数请求资源,在无法完成时抛出异常,析构函数释放资源且决不会抛出异常。
  • 在使用资源时始终通过RAII类的具有自动存储器或临时生存期的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::mutex m;

void bad()
{
m.lock(); // 请求互斥体
f(); // 如果 f() 抛出异常,那么互斥体永远不会被释放
if(!everything_ok()) return; // 提早返回,互斥体永远不会被释放
m.unlock(); // 只有 bad() 抵达此语句,互斥体才会被释放
}

void good()
{
std::lock_guard<std::mutex> lk(m); // RAII类:互斥体的请求即是初始化
f(); // 如果 f() 抛出异常,那么就会释放互斥体
if(!everything_ok()) return; // 提早返回也会释放互斥体
}

2.智能指针

参考:

1.https://light-city.github.io

2.https://www.jianshu.com/p/d19fc8447eaa

3.https://zh.cppreference.com/w/cpp/language/raii