1. 面向对象 vs 面向过程
    面向对象三大特性:封装、继承、多态
    c++ 不是完全面向对象的语言
    封装:把数据和方法打包成一个类,隐藏实现细节,只提供接口
    继承:子类继承父类的属性和方法,子类可以扩展父类的功能,实现代码复用
    多态:在运行时,根据对象的实际类型,调用对应的函数。主要通过虚函数和继承实现

  2. C++ 编译过程
    预处理 -> 编译 -> 汇编 -> 链接
    预处理:预处理指令、宏定义、包含文件 (处理 #include, #define, 条件编译等)
    编译:把源代码编译成汇编代码 (将 .cpp 转为 .s)
    汇编:把汇编代码编译成机器码
    链接:把多个对象文件链接成可执行文件 (将多个目标文件和库合并成可执行文件)
    第三方库就是在第 4 步 —— 链接(Linking)阶段加入的
    在 链接阶段,通过 -lxxx(如 -lcurl)告诉链接器:“我要用这个库”,然后链接器去找到对应的 静态库(.a 或 .lib)或动态库(.so 或 .dll)
    静态库在在编译过程中(链接阶段)被载入可执行程序,生成的可执行文件体积较大
    动态库是在运行时才载入内存,但更新库后需要重新编译。动态库是运行时库

  3. 静态链接和动态链接
    静态链接:把所有用到的库文件都链接到可执行文件中,编译速度慢,但运行速度快
    动态链接:把所有用到的库文件都放到一个单独的文件中,运行时再加载,编译速度快,但运行速度慢

  4. 静态多态(编译时多态) 和 动态多态(运行时多态)

  • 动态多态(Dynamic Polymorphism)—— 运行时多态
    这是通过 继承 + 虚函数(virtual function) 实现的。动态多态是通过虚函数重写实现的,是在运行期间确定的多态,是一种晚绑定机制(或动态绑定),在运行期间才能确定调用哪一个函数

基类中的虚函数:使用 virtual 关键字声明。
派生类重写(override)虚函数。
通过基类指针或引用调用虚函数。
必须通过 基类指针或引用 调用虚函数,涉及虚函数表( vtable )和虚指针( vptr

虚函数如何实现晚绑定?
每个有虚函数的类有一个 虚函数表(vtable),存函数指针。(编译时候产生的,放在静态存储区中)
每个对象构造时会有一个 虚指针(vptr),指向自己的 vtable。
调用虚函数时:
程序运行时,通过对象的 vptr 找到 vtable
在 vtable 中查找对应函数地址
调用该函数
这个过程在运行时完成,所以是晚绑定。

  • 静态多态(Static Polymorphism)—— 编译时多态
    在编译期就确定了调用哪个函数,不涉及运行时开销,属于 “早绑定(Early Binding)”

函数重载(Function Overloading)
运算符重载(Operator Overloading)
函数模板(Function Templates)

例子
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
#include <iostream>
using namespace std;

// 1. 函数重载
void print(int x) {
cout << "Integer: " << x << endl;
}

void print(double x) {
cout << "Double: " << x << endl;
}

void print(const string& s) {
cout << "String: " << s << endl;
}
> 符号表如何支持函数重载?
在支持重载的语言(如 C++)中,符号表不会把同名函数合并成一个条目,而是为每个不同的函数签名单独存储一条记录,并将它们组织成一个“重载集合”,用的时候根据实参的类型和数量等样进行匹配,找到最匹配的函数调用。

// 2. 函数模板(泛型)
template<typename T>
void printGeneric(const T& value) {
cout << "Generic: " << value << endl;
}

// 3. 运算符重载
class Point {
public:
int x, y;
Point(int x, int y) : x(x), y(y) {}

Point operator+(const Point& other) const {
return Point(x + other.x, y + other.y);
}
};

int main() {
print(42); // 调用 print(int)
print(3.14); // 调用 print(double)
print("Hello"); // 调用 print(string)

printGeneric(100); // 编译时生成 printGeneric<int>
printGeneric(2.5); // 生成 printGeneric<double>

Point p1(1, 2), p2(3, 4);
Point p3 = p1 + p2; // 使用重载的 +
cout << "Point: (" << p3.x << ", " << p3.y << ")" << endl;

return 0;
}
  1. 参数推导;2. 实例化具体函数 (在符号表中对应不同的条目); 3. 编译链接
  1. 必须通过指针或引用调用 才能触发多态。如果直接用对象调用,会是静态绑定。
    想用多态,必须用基类指针或基类引用去操作派生类对象
例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;

class Animal {
public:
virtual void speak() {
cout << "Animal speaks" << endl;
}
};

class Dog : public Animal {
public:
void speak() override {
cout << "Dog barks: 汪汪!" << endl;
}
};
  • 通过指针调用 → 触发多态(动态绑定)
1
1
2
3
4
5
6
7
int main() {
Dog dog;
Animal* ptr = &dog; // 基类指针指向派生类对象

ptr->speak(); // 输出 Dog barks: 汪汪!
return 0;
}
  • 通过引用调用 → 也触发多态
2
1
2
3
4
5
6
7
8
9
void makeAnimalSpeak(Animal& animal) {
animal.speak(); // 动态绑定
}

int main() {
Dog dog;
makeAnimalSpeak(dog); // 输出:Dog barks: 汪汪!
return 0;
}
  • 直接对对象调用,静态绑定
3
1
2
3
4
5
6
7
int main() {
Dog dog;
Animal a = dog; // 把 Dog 对象赋值给 Animal 对象(会发生“对象切片”)

a.speak(); // 输出 Animal speaks
return 0;
}
  1. 指针 vs 引用
    引用是别名,指针是表示地址的变量
    引用的特性:

必须初始化,引用定义时必须绑定到一个变量
不能重新绑定,一旦绑定,就不能再指向其他变量
操作即原变量,对引用的所有操作都作用于原变量

引用
1
2
3
4
int& ref;     // 错误!未初始化
int a = 5, b = 10;
int& ref = a;
ref = b; // 注意:这是赋值!不是重新绑定!a 变成 10,ref 仍绑定 a,此时a 和 ref 都是 10

引用的常见用法:

函数参数传递:传递大对象(如 string、vector)
函数返回值(返回对象本身,支持链式调用)

引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyInt {
int value;
public:
MyInt(int v) : value(v) {}

// 返回引用,支持链式赋值
MyInt& operator=(const MyInt& other) {
if (this != &other) { // 自赋值检查
value = other.value;
}
return *this; // 返回当前对象的引用
}
};

MyInt a(1), b(2), c(3);
a = b = c; // 链式赋值:b=c 返回 b 的引用,再赋给 a

不能返回局部变量的引用

错误
1
2
3
4
int& getRef() {
int x = 10;
return x; // 错误!x 是局部变量,函数结束后销毁
}

遍历容器(避免拷贝元素)

遍历容器
1
2
3
4
5
6
7
8
9
10
11
vector<string> names = {"Alice", "Bob", "Charlie"};

// 使用引用遍历,避免拷贝字符串
for (const string& name : names) {
cout << name << endl;
}

// 或使用 auto&
for (auto& name : names) {
name += " (modified)"; // 如果想修改原元素
}

作为输出参数(替代指针)

  1. const 引用
    引用是别名,const 引用是常量别名,不能修改别名指向的变量
const引用
1
2
const int& cref = x;  // cref 是 x 的常引用
cref = 20; // 错误!不能通过 cref 修改 x

可以用来绑定临时对象(延长生命周期)

常引用
1
2
3
4
5
6
double getValue() {
return 3.14;
}

const double& r = getValue(); // 合法!临时对象生命周期延长到 r 结束
cout << r; // 输出 3.14

getValue () 的值在表达式结束就应该销毁了,但是当一个 const 引用绑定到临时对象时,这个临时对象的生命周期会被 “延长”,直到引用 r 结束为止。
用法:

避免不必要的拷贝

引用
1
const string& s = "hello";  // 不拷贝字符串

支持函数链式调用

引用
1
2
const string& result = getPrefix() + getName() + getSuffix();
// 临时 string 对象被 const 引用延长生命周期

STL 中使用

STL
1
for (const auto& item : vec) { ... }  // 避免拷贝,安全高效
  1. 左值引用 vs 右值引用
    左值:有名字、能取地址的变量,可以放在赋值号左边,生命周期较长
    右值:临时变量,不能取地址,生命周期较短,通常在表达式中 “用完就扔”
右值
1
2
3
10;                // 字面量,右值
x + 1; // 表达式结果,右值
string("hello"); // 临时 string 对象,右值

右值引用(T&&)就是一种可以绑定到右值(临时对象)的引用。

右值引用
1
2
3
4
//类型&& 右值引用名 = 右值;
int&& rref = 10; // 10 是右值,rref 是它的右值引用
int&& rref2 = 5 + 3; // 表达式结果是右值
string&& sref = string("临时字符串"); // 绑定临时 string 对象

用法:对临时变量移动构造,避免深拷贝

移动构造
1
2
3
4
5
// std:move(x) 将左值转换为右值引用,触发移动语义
MyString s1("hello");
MyString s2 = std::move(s1); // 告诉编译器:“s1 可以被移动了”

cout << s1.data; // 可能是 nullptr!s1 处于“有效但不可用”状态
  1. 继承
继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal {
protected:
string name;
public:
Animal(string n) : name(n) {}
void eat() { cout << name << " 在吃东西" << endl; }
};

// Dog 继承 Animal,继承方式为 public
class Dog : public Animal {
public:
Dog(string n) : Animal(n) {} // 调用基类构造函数
void bark() { cout << name << " 汪汪叫!" << endl; }
};

int main() {
Dog d("小黄");
d.eat(); // 继承自 Animal
d.bark(); // Dog 自己的方法
return 0;
}
继承方式 基类 public 成员 基类 protected 成员 基类 private 成员
public 在派生类中仍是 public 在派生类中是 protected 不可访问
protected 在派生类中是 protected 在派生类中是 protected 不可访问
private 在派生类中是 private 在派生类中是 private 不可访问

继承中的构造与析构顺序

构造顺序:
基类构造函数
成员对象构造函数(按声明顺序)
派生类构造函数
析构顺序:与构造相反
派生类析构函数
成员对象析构函数(按声明逆序)
基类析构函数

继承中的构造与析构顺序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base {
public:
Base() { cout << "Base 构造\n"; }
~Base() { cout << "Base 析构\n"; }
};

class Member {
public:
Member() { cout << "Member 构造\n"; }
~Member() { cout << "Member 析构\n"; }
};

class Derived : public Base {
Member m;
public:
Derived() { cout << "Derived 构造\n"; }
~Derived() { cout << "Derived 析构\n"; }
};

int main() {
Derived d;
return 0;
}

输出:
Base 构造
Member 构造
Derived 构造
Derived 析构
Member 析构
Base 析构

继承是实现 动态多态 的基础。
条件:

  • 基类有 virtual 函数
  • 派生类 override
  • 通过 基类指针或引用 调用

继承类不能访问基类的私有成员
构造函数不能继承,C++11 起支持 using Base::Base; 继承构造函数
析构函数应为虚函数:如果类可能被继承,析构函数必须是 virtual ,否则 delete基类指针 会只调用基类析构,导致派生类资源泄漏

多重继承

如果一个类可能被继承(作为基类),那么它的析构函数就应该声明为 virtual
否则,当用基类指针(或引用)删除派生类对象时,派生类的析构函数不会被调用,造成资源泄漏
原因:virtual 让析构函数变成动态绑定,delete 时,程序会通过虚函数表(vtable)找到实际对象的类型

  1. 为什么构造函数不能是虚函数?
    虚函数依赖对象的 vptr
    构造函数执行时,对象还没完全构造,vptr 还没初始化
    每个有虚函数的类,编译器会生成一个 虚函数表(vtable),里面存着虚函数的地址。
    每个对象内部有一个虚指针(vptr),指向自己的 vtable。
    调用虚函数时:
    对象 -> vptr -> vtable -> 找到函数地址 -> 调用
  • 构造函数执行时:
例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal {
public:
virtual void speak() { cout << "Animal sound\n"; }
Animal() { // 构造函数
// 在这里,vptr 被设置为指向 Animal 的 vtable
}
};

class Dog : public Animal {
public:
virtual void speak() override { cout << "汪汪!\n"; }
Dog() { // Dog 的构造函数
// 在这里,vptr 被更新为指向 Dog 的 vtable
}
};

调用 Dog dog;
先调用 Animal 的构造函数
此时,dog 对象的 vptr 被初始化为指向 Animal 的 vtable
再调用 Dog 的构造函数
vptr 被更新为指向 Dog 的 vtable

虚拟构造函数(后面在学)

  1. 重载(Overload)、重写(Override)、隐藏(Hiding)
    Overload: 同一个作用域(通常是同一个类),函数名相同,参数列表不同(类型、个数、顺序),返回类型可以不同(但不能仅靠返回类型区分)
    Override: 发生在继承关系中,基类函数必须是 virtual,函数名、参数列表、返回类型(或协变)完全相同,派生类中使用 override
    Hiding: 两种情况① 函数名相同,参数不同;② 函数名相同,但基类函数不是 virtual
隐藏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Animal {
public:
virtual void speak() {
cout << "Animal speak" << endl;
}

virtual void speak(string msg) {
cout << "Animal says: " << msg << endl;
}
};

class Dog : public Animal {
public:
void speak() override { // ✅ 重写 speak()
cout << "Dog barks" << endl;
}
// 注意:speak(string) 被隐藏了!
};

Dog d;
d.speak(); // 输出:Dog barks
// d.speak("hello"); // 编译错误!speak(string) 被隐藏

解除隐藏:添加 using Base::speak;

  1. 深拷贝浅拷贝
    深拷贝复制相同大小的内容到堆区,浅拷贝把源对象的指针赋给目标对象,多个对象的指针会指向同一块内存,容易导致双重释放

类里面有动态分配的资源比如 int* , char* 必须进行深拷贝:

  • 析构函数
  • 构造函数和拷贝构造需要手动实现深拷贝(比如 strcpy)
  • 重载赋值运算符
深拷贝
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
#include <iostream>
#include <cstring>
using namespace std;

class MyString {
private:
char* data; // 指向堆上的字符串

public:
// 1️⃣ 构造函数
MyString(const char* str = nullptr) {
if (str == nullptr || str[0] == '\0') {
data = new char[1];
data[0] = '\0';
} else {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
}

// 2️⃣ 析构函数:释放资源
~MyString() {
delete[] data; // 释放堆内存
data = nullptr; // 防止悬空(可选)
}

// 3️⃣ 拷贝构造函数:深拷贝
MyString(const MyString& other) {
if (other.data == nullptr) {
data = nullptr;
} else {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
}

// 4️⃣ 拷贝赋值运算符:深拷贝赋值
MyString& operator=(const MyString& other) {
// 自我赋值检查
if (this == &other) {
return *this;
}

// 先释放旧资源
delete[] data;

// 分配新内存,深拷贝
if (other.data == nullptr) {
data = nullptr;
} else {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}

return *this;
}

// 辅助函数:打印
void print() const {
cout << (data ? data : "null") << endl;
}
};
  1. 拷贝构造参数不是引用行吗?
    拷贝构造的参数不是引用会导致无限递归,加 const 是为了避免通过形参修改实参

  2. 堆和栈的区别?
    堆是由程序员手动申请和释放的,可以使用 new 和 malloc 申请,释放使用 delete 和 free。内存空间较大,从低地址到高地址,访问速度较慢,有碎片问题,适合用于大对象、动态数组等
    栈由编译器自动分配和释放,内存空间较小,从高地址到低地址,访问速度快,无碎片问题,适合用于局部变量、函数参数、返回地址

  • 为什么栈快,堆慢?
    栈:内存分配只是移动栈指针(esp),是 CPU 指令级操作,极快。
    堆:需要调用操作系统 API,查找合适的内存块,维护空闲链表,效率较低。
  1. 内联函数
    请求编译器将函数体直接插入到调用处的机制,避免函数调用的开销
    但是只是对编译器的建议,编译器不一定会实现
    一般来说递归函数不适合内联,因为递归调用会生成临时变量,这些变量在函数退出时会被销毁,内联函数的临时变量会一直存在,导致内存泄漏;虚函数也不适合内联,内联函数在编译的时候确定代码,但虚函数是运行时多态
  • 使用 inline 关键字
  • 类内定义函数自动内联
    内联函数比宏定义安全:
宏定义vs内联函数
1
2
3
4
5
#define MAX(a,b) ((a) > (b) ? (a) : (b))
int x = MAX(i++, j++); // i 和 j 可能被 ++ 两次!

inline int max(int a, int b) { return a > b ? a : b; }
int x = max(i++, j++); // 安全,参数只计算一次

定义写在类内不需要显式写 inline 关键字,定义在类外需要

  1. 友元
    允许某个函数或类 “突破封装”,访问另一个类的私有(private)和保护(protected)成员。
  • 将非成员函数声明为友元函数:非成员函数可以直接访问 privateprotected 成员
  • 将其他类的成员函数声明为友元函数:其他类成员函数可以直接访问 privateprotected 成员
友元
1
friend void Student::show(Address *addr);
  • 友元类:一个类声明为另一个类的友元类,则该类中的所有成员函数都可以访问该类的 privateprotected 成员
友元类
1
2
3
4
5
6
7
8
9
10
11
class Address{
public:
Address(char *province, char *city, char *district);
public:
//将Student类声明为Address类的友元类,Student类中的所有成员函数都可以访问Address类的private成员
friend class Student;
private:
char *m_province; //省份
char *m_city; //城市
char *m_district; //区(市区)
};
  1. 智能指针
    new 了没有 delete —— 内存泄漏
    delete 之后没有置为 nullptr / 指针指向的对象生命周期结束 —— 野指针
    多个指针指向同一块内存 —— 悬空指针
    智能指针本质是封装指针的类模板
    RAII(Resource Acquisition Is Initialization)原则:资源在构造时获取,在析构时释放。
  • unique_ptr :独占指针,只能有一个指针指向一块内存,不能拷贝,不能赋值,析构时会自动释放内存。赋值只能用 std::move() 转移所有权,此时 u1 的所有权被转移给 u3u1 变成空指针。
    unique_ptr 支持管理数组,它会使用 delete[] 来释放数组内存(区别于 auto_ptr ,它使用 delete 来释放内存)
unique_ptr
1
2
3
std::unique_ptr<int> u1(new int(10));
// std::unique_ptr<int> u2 = u1; // 编译错误,不允许拷贝
std::unique_ptr<int> u3 = std::move(u1);
  • shared_ptr :共享指针,多个指针可以指向一块内存,通过引用计数机制来管理对象的生命周期 。当一个 shared_ptr 指向对象时,引用计数加 1;当 shared_ptr 离开作用域或被重新赋值时,引用计数减 1,当引用计数为 0 时,对象会被自动释放
    shared_ptr 在多线程环境中使用时,需要注意线程安全问题,因为引用计数的修改不是原子操作,可能会导致数据竞争 。为了解决这个问题,可以使用 std::atomic 来实现原子操作,或者使用互斥锁来保护引用计数的修改
  • weak_ptr :不能访问指针指向的对象,不控制对象的生命周期,只能判断指针是否为空,不能赋值,析构时会自动释放内存。主要是用来解决两个 shared_ptr 对象之间形成循环引用的问题(循环引用计数永远不会归零,导致这两个对象永远无法被销毁),当一个 shared_ptr 对象被另一个 shared_ptr 对象所引用时,两个对象之间形成循环引用,导致内存泄漏。 weak_ptr 对象用来监视 shared_ptr 中管理的资源是否存在,可以判断指针是否为空,也可以获取指针指向的对象,但无法访问对象。 weak_ptr 对象可以通过 lock() 方法获取对应的 shared_ptr 对象,如果对象仍然存在,则返回对应的 shared_ptr 对象,否则返回一个空的 shared_ptr 对象。
    使用 weak_ptr 时,需要通过 lock 函数将其提升为 shared_ptr ,才能访问对象 。如果对象已经被释放, lock 函数会返回一个空的 shared_ptr
  1. 模板
    允许你写一份代码,编译器根据不同的类型自动生成对应的函数或类
    比如 swap
模板
1
2
3
4
5
6
template <typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}

基本语法:

函数模板
1
2
3
4
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) { // auto表示返回值在后面指定,编译器会根据a+b的结果自动推导返回类型
return a + b;
}
类模板
1
2
3
4
5
6
7
8
9
10
11
12
13
// template <typename T1, typename T2, ...>
// class ClassName {
// // 使用 T1, T2 等作为类型
// };
template <typename T, int N>
class Array {
T data[N];
public:
T& operator[](int index) {
return data[index];
}
int size() { return N; }
};

模板的实现(函数体)必须和声明放在同一个头文件中,STL 就是模板

  1. volatile
    原因:多处理器访问变量,缓存和主存可能存在不一致的问题
    volatile 让变量的读写都从主存中读写,而不是从缓存中读写
  • 易变性(可见性): … 可能不一致
  • 不可优化的:编译器不要做优化
volatile
1
2
3
volatile int a;
a = 1;
printf(%d, a);

不加 volatile 的话编译器可能会优化成 `printf (% d, 1);

  • 顺序执行的:编译器会对无关的语句进行优化,可能会把 printf 放到 a = 1 的前面执行
1
2
3
4
int i = 1;
int j = 1
i += 1;
j += 2;

实际上执行顺序可能不是这样的(原因详见编译原理),写了 volatile 之后,编译器就不会改变顺序

  • 使用场景:多线程共享字段(标志位)且经常被修改;中断服务程序和硬件设备访问相关的情况(指针可以是 volatile
  • 误区: volatile 修饰的变量不能保证原子性,原子性需要通过原子操作或者锁来实现
  1. static
    c 语言中:
  • 用于局部变量(函数内部)
    • 作用:使局部变量具有静态存储期(即程序运行期间一直存在),但作用域仍限于当前括号之内。
    • 初始化:只在第一次进入函数时初始化一次。
    • 默认值:未显式初始化则为 0。
  • 用于全局变量或函数
    • 作用:将变量或函数的链接属性(linkage)设为内部链接(internal linkage),即仅在当前源文件中可见,其他文件无法访问。
    • 目的:实现封装,避免命名冲突。
      c++ 中:
      C++ 继承了 Cstatic 的所有用法,还增加了面向对象场景下的新用途
  • 静态成员变量:
    • 属于类本身,而不是类的某个对象。
    • 所有对象共享同一个静态成员。
    • 必须在类外定义(C17 前),C17 起可用 inline static 在类内定义。
static
1
2
3
4
5
6
7
class MyClass {
public:
static int count; // 声明
};
int MyClass::count = 0; // 定义(C++17 前必需)
// 或 C++17 起:
// inline static int count = 0;
  • 静态成员函数:
    • 不属于任何对象,不能访问非静态成员(因为没有 this 指针)。
    • 可直接通过类名调用。
static
1
2
3
4
5
6
7
8
9
class MyClass {
public:
static void print() {
// 不能访问非静态成员
std::cout << "Static function\n";
}
};

MyClass::print(); // 无需对象

C++11 起,static 局部变量的初始化自动线程安全,c 语言没有这种保证

  1. c 语言保证线程安全
    C 语言标准库本身(C89/C99/C11 等)在早期版本中并不直接提供线程或锁操作,但 C11 标准(ISO/IEC 9899:2011)引入了多线程支持,包括锁(mutex)等同步原语。
    C11 引入了 <threads.h> 头文件,提供了类似 POSIX 的线程和锁接口。
mutex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <threads.h>

// 互斥锁类型
mtx_t mutex;

// 初始化锁
int mtx_init(mtx_t *mtx, int type);

// 加锁(阻塞)
int mtx_lock(mtx_t *mtx);

// 尝试加锁(非阻塞)
int mtx_trylock(mtx_t *mtx);

// 解锁
int mtx_unlock(mtx_t *mtx);

// 销毁锁
void mtx_destroy(mtx_t *mtx);

比如线程安全计数器

mutex
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
#include <stdio.h>
#include <threads.h>

int counter = 0;
mtx_t mutex;

int increment(void* arg) {
for (int i = 0; i < 100000; i++) {
mtx_lock(&mutex);
counter++;
mtx_unlock(&mutex);
}
return 0;
}

int main() {
mtx_init(&mutex, mtx_plain);

thrd_t t1, t2;
thrd_create(&t1, increment, NULL);
thrd_create(&t2, increment, NULL);

thrd_join(t1, NULL);
thrd_join(t2, NULL);

printf("Counter = %d\n", counter); // 应为 200000
mtx_destroy(&mutex);
return 0;
}

实际上用得多的可能是 pthreads

pthreads
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 或动态初始化
pthread_mutex_init(&mutex, NULL);

// 加锁
pthread_mutex_lock(&mutex);

// 尝试加锁
pthread_mutex_trylock(&mutex);

// 解锁
pthread_mutex_unlock(&mutex);

// 销毁
pthread_mutex_destroy(&mutex);

示例

pthreads
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
#include <stdio.h>
#include <pthread.h>

int counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* increment(void* arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&mutex);
counter++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}

int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);

pthread_join(t1, NULL);
pthread_join(t2, NULL);

printf("Counter = %d\n", counter);
pthread_mutex_destroy(&mutex);
return 0;
}
  1. typedef 和 define
    #define 是 “文本替换”,像 Word 的 “查找替换”,预处理的时候替换;
    typedef 是 “类型定义” 的关键字,是编译器理解的真正类型别名。typedef 可以定义新的类型,也可以定义新的类型别名,但不能定义常量 / 宏函数,在编译的时候处理

  2. 数组指针和指针数组
    数组指针:
    指向数组的指针

array pointer
1
2
3
int *p[4];
int b[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
p = b;

指针数组:
数组的元素是指针

pointer array
1
int* p[4];
  1. 函数指针
    函数指针:
    指向函数的指针
function pointer
1
2
3
4
5
int (*p)(int, int); // 括号不能省略
p = add;
int add(int a, int b) { return a + b; }
int result = p(1, 2);
printf("%d\n", result); // 3

函数指针没有自增和自减运算

区分 int *p 和 int (*p)
前者是一个指向 int 类型的指针,后者有可能是一个数组指针或是返回 int 类型的函数指针

  1. 如何保证线程安全?
  • 原子操作
atomic
1
2
3
4
5
6
7
8
9
#include <atomic>
std::atomic<int> counter;
counter.fetch_add(1); // fetch_sub(), fetch_and(), fetch_or()...
counter.load();
counter.store(0);
counter.exchange(0);
counter.compare_exchange_strong(old_value, new_value); // 如果old_value == counter,则将 counter 设为 new_value
counter.compare_exchange_weak(old_value, new_value); // 区别:weak 版本允许虚假失败,需要循环
counter.is_lock_free();
  • 线程同步原语
  1. const 用法
  • 常量变量
    必须在定义时初始化
  • 常量指针 vs 指针常量:见上
  • 引用与 const:见上 const 引用
  • 类中的 const:
    • const 成员函数:不能修改对象成员变量,不能调用非 const 成员函数
    • const 对象:const 对象只能调用 const 成员函数
  1. 多线程
    https://www.runoob.com/cplusplus/cpp-multithreading.html

  2. 协程

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

NoResponse WeChat Pay

WeChat Pay

NoResponse Alipay

Alipay

NoResponse PayPal

PayPal