C++知识整理笔记

第一章语言基础

1.1 简述C++语言的特点

  1. C++在C语言基础上引入了面对对象的机制,同时也兼容C语言。
  2. C++有三大特性:封装,继承,多态。封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态则是为了实现另一个目的——接口重用。
  3. C++语言编写出的程序结构清晰、易于扩充,程序可读性好。
  4. C++生成的代码质量高,运行效率高,仅比汇编语言慢10%~20%;
  5. C++更加安全,增加了const常量、引用、四类cast转换(static_cast、dynamic_cast、const_cast、reinterpret_cast)、智能指针、try—catch等等;
  6. C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL(Standard Template Library)。
  7. 同时,C++是不断在发展的语言。C++后续版本更是发展了不少新特性,如C++11中引入了nullptr、auto变量、Lambda匿名函数、右值引用、智能指针。

1.2 简述C语言的特点

  • C语言有出色的可移植性 ,能在多种不同体系结构的软/硬平台上运行。
  • 简洁紧凑,使用灵活的语法机制 ,并能直接访问硬件。
  • C语言具有很高的运行效率,直接操作底层寄存器。

1.3 为什么嵌入式用C语言比较多

能够直接访问硬件的语言有汇编和C语言,汇编属于低级语言,难以完成一些复杂的功能,但是汇编比C语言访问硬件的效率更高。所以,一般将硬件初始化的工作交给汇编,比较复杂的操作交给C语言。

1.4 面向对象有哪些特征和原则

包括四大基本特征和五大基本原则。

  • 特征:抽象、继承、多态、封装
  • 原则:单一职责原则、开放封闭原则、替换原则、依赖原则、接口分离原则

1.5 面向过程与面向对象的语言有哪些优缺点

面向对象的语言:

  • 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;
  • 缺点:没有面向对象易维护、易复用、易扩展

面向对象的语言:

  • 优点:易维护、易复用、易扩展,
  • 缺点:性能比面向过程低

第二章 指针和引用

2.1 智能指针

参考:

c++智能指针详解

2.1.1 什么是智能指针

智能指针本质是一个封装了一个原始C++指针的类模板,为了确保动态内存的安全性而产生的。实现原理是通过一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源。

从比较简单的层面来看,智能指针是RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制对普通指针进行的一层封装。这样使得智能指针的行为动作像一个指针,本质上却是一个对象,这样可以方便管理一个对象的生命周期。

在c++中,智能指针类通常定义在 头文件中,一共定义了4种:auto_ptr、unique_ptr、shared_ptr 和 weak_ptr。其中,auto_ptr 在C++11已被摒弃,在C++17中已经移除不可用。

2.1.2 原始指针的问题

  1. 忘记删除
  2. 删除后的情况没有考虑清楚,容易造成悬挂指针(dangling pointer)或者说野指针(wild pointer)。
  3. 程序有异常导致准备的删除操作无法执行。

2.1.3 unique_ptr

unique_ptr是独享被管理对象指针所有权(owership)的智能指针。unique_ptr对象封装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。

创建unique_ptr:

#include <iostream>
#include <string>
#include <memory>
using namespace std;

void f1() {
    unique_ptr<int> p(new int(5));
    cout<<*p<<endl;
    unique_ptr<int> p2(p);
    unique_ptr<int> p3 = p;
}

上面的代码就创建了一个unique_ptr。

需要注意的是,unique_ptr没有复制构造函数,不支持普通的拷贝和赋值操作。因为unique_ptr独享被管理对象指针所有权,当p2, p3失去p的所有权时会释放对应资源,此时会执行两次delete p的操作。对于p2,p3对应的行,IDE会提示报错:

无法引用 函数 "std::__1::unique_ptr<_Tp, _Dp>::unique_ptr(const std::__1::unique_ptr<int, std::__1::default_delete<int>> &) [其中 _Tp=int, _Dp=std::__1::default_delete<int>]" (已隐式声明) -- 它是已删除的函数

unique_ptr虽然不支持普通的拷贝和赋值操作,但却可以将所有权进行转移,使用std::move方法即可。

void f1() {
    unique_ptr<int> p(new int(5));
    unique_ptr<int> p2 = std::move(p);
    //error,此时p指针为空: cout<<*p<<endl; 
    cout<<*p2<<endl;
}

unique_ptr最常见的使用场景,就是替代原始指针,为动态申请的资源提供异常安全保证。

对于下面的写法:

objtype *p = new objtype();
p -> func();
delete p

引入unique_ptr改为:

unique_ptr<objtype> p(new objtype());
p -> func();
delete p

2.2 野指针,悬空指针,空指针

野指针是没有被初始化过的指针,指向的位置是不可知的(随机的、不正确的、没有明确限制的)。此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。

  • 成因:指针变量未初始化、指针释放后之后未置空、指针操作超越变量作用域
  • 解决:初始化时置 NULL、释放时置 NULL
  • 悬空指针:指针最初指向的内存已经被释放了的一种指针。(例子:返回局部变量的地址)
  • 空指针:指针的值为0,不指向任何有效数据
    产生原因及解决办法:
  • 野指针:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。
  • 悬空指针:指针free或delete之后没有及时置空 => 释放操作后立即置空。

2.3 野指针避免办法

使用时遵循以下步骤:

  1. 初始化置NULL
  2. 申请内存后判空
  3. 指针释放后置NULL
  4. 使用智能指针

2.4 指针数组和数组指针

指针数组:数组,元素是指针类型 int* arr[3]={&num1,&num2,&num3};

数组指针:指针,指向数组 int(* p)[3]=&arr;

不能把数组当参数直接传递,要用数组指针。

2.5 左值引用和指针的区别?

  1. 是否初始化:指针可以不用初始化,引用必须初始化
  2. 性质不同:指针是一个变量,引用是对被引用的对象取一个别名
  3. 占用内存单元不同:指针有自己的空间地址,引用和被引用对象占同一个空间。

2.6 右值引用是什么,为什么要引入右值引用?

右值引用是为一个临时变量取别名,它只能绑定到一个临时变量或表达式(将亡值)上。实际开发中我们可能需要对右值进行修改(实现移动语义时就需要)而右值引用可以对右值进行修改。

引入目的:

  1. 为了支持移动语义,右值引用可以绑定到临时对象、表达式等右值上,这些右值在生命周期结束后就会被销毁,因此可以在右值引用中窃取其资源,从而避免昂贵的复制操作,实现高效的移动语义。
  2. 完美转发:右值引用可以绑定到任何类型的右值上,可以将其作为参数传递给函数,并在函数内部将其“转发”到其他函数中,从而实现完美转发。
  3. 拓展可变参数模板,实现更加灵活的模板编程。

2.7 weak_ptr真的不计数?是否有计数方式,在哪分配的空间。

计数,控制块中有强弱引用计数,如果是使用make_shared初始化的函数则它所在的控制块空间是在所引用的shared_ptr中同一块的空间,若是new则控制器所分配的内存与shared_ptr本身所在的空间不在同一块内存。

第三章 字符串

3.1 Sprintf strcpy memcpy

  • 操作对象不同,strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型,目的操作对象是字符串,memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。

  • 执行效率不同,memcpy最高,strcpy次之,sprintf的效率最低。

  • 实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字 符串的转化,memcpy 主要是内存块间的拷贝。

    注意:strcpy、sprintf 与memcpy 都可以实现拷贝的功能,但是针对的对象不根据实际需求,来 选择合适的函数实现拷贝功能。

函数原型:

char* strcpy(char* destination, const char* source);

destination 是目标字符串的指针,表示要将源字符串复制到的位置。

source 是源字符串的指针,表示要被复制的字符串。

strcpy 函数的作用是将源字符串复制到目标字符串中,直到遇到源字符串的空字符(‘\0’)为止。需要注意的是,目标字符串必须具有足够的空间来容纳源字符串,以避免缓冲区溢出。

void* memcpy(void* destination, const void* source, size_t num);

destination 是指向目标内存区域的指针,表示要拷贝数据的目标位置。

source 是指向源内存区域的指针,表示要被复制的数据的起始位置。

num 表示要被复制的字节数。

memcpy 函数的作用是将源内存区域中的数据复制到目标内存区域。需要注意的是,memcpy 不会检查目标内存区域的大小,因此在使用时需要确保目标内存区域足够大以容纳源数据。

int sprintf(char* str, const char* format, ...);

str 是一个指向字符数组的指针,表示要写入数据的目标字符串。

format 是一个格式化字符串,包含了要写入的数据的格式信息。

… 表示可变数量的参数,根据 format 字符串中的格式说明符,将数据写入到 str 中。

sprintf 函数的作用是根据指定的格式将数据写入到字符串中,类似于 printf 函数将输出打印到控制台,而 sprintf 则将输出写入到字符串中。

格式化字符串是一种特殊的字符串,其中包含格式说明符(format specifiers),用于指定在输出时如何格式化数据。在 C++ 中,格式化字符串通常用于将数据按照指定的格式输出到控制台、文件或其他目标。格式说明符以百分号(%)开头,后面紧跟着一个字符,用于指定要输出的数据类型或格式。一些常用的格式说明符包括:

%d:用于输出整数。
%f:用于输出浮点数。
%s:用于输出字符串。
%c:用于输出字符。
%x:用于输出十六进制整数。
%p:用于输出指针地址。
除了格式说明符外,格式化字符串中可能包含其他文本内容,用于在输出中显示固定文本或分隔符。

3.2.字符串指针变量/字符数组/字符指针数组

3.2.1 字符串指针变量/字符数组

示例代码:

int main(){
    // 字符指针数组
    char arr[15]="helloworld";
    // 字符串指针
    const char* str="helloworld";
    int n1= strlen(arr);
    int n2= strlen(str);
    // 字符串常量
    int n3= strlen("helloworld");
    cout << n1 << endl;
    cout << n2 << endl;
    cout << n3<< endl;
    cout << sizeof(arr) << endl;
    cout << sizeof(str) << endl;
    sizeof("helloworld")<< endl;
}

输出结果:

10 
10
10
15
4
11

strlen 函数通过遍历给定的字符串,从字符串的开始一直检查到遇到字符串末尾的空字符 ‘\0’ 为止。

sizeof用于获取数据类型或变量在内存中所占的字节数。

对于字符数组,返回数组的大小。

对于字符串指针,返回指针变量的大小,在32位机器中即4。

对于字符串常量,返回整个常量的大小,包含结尾空字符,因为这也是常量占据空间的一部分。

3.2.2 字符串指针数组

#include <iostream>
int main() {
    const char* strArray[] = {"Apple", "Banana", "Orange"}; // 字符指针数组
    cout<<sizeof(strArray);
    cout<<sizeof(strArray[0]);
    return 0;
}
12
4

字符指针数组是一个数组,每个元素都是指向字符的指针,可以用来表示多个字符串。

每个指针指向一个以 null 结尾的字符数组,类似于字符串数组。

对字符串指针数组使用 sizeof 运算符会返回整个数组所占内存空间的大小,而不是数组中每个指针所指向的字符串的长度或大小。

第四章 数组

4.1 区间迭代有哪些

4.1.1 范围基础 for 循环(Range-based for loop):

C++11 引入的范围基础 for 循环是一种方便的区间迭代方式,用于遍历容器、数组或其他支持迭代器的数据结构中的元素。示例代码如下:

std::vector<int> numbers = {1, 2, 3, 4, 5};
for (int num : numbers) {
    std::cout << num << " ";
}

4.1.2 STL 算法:

C++ 标准模板库(STL)提供了许多算法,如 std::for_each、std::accumulate、std::transform 等,可以对容器进行区间迭代和操作。这些算法可以用于对容器中的元素进行处理,而无需显式的迭代。示例代码如下:

std::vector<int> numbers = {1, 2, 3, 4, 5};
std::for_each(
    numbers.begin(), numbers.end(), [](int num) {
    	std::cout << num << " ";
	}
);

4.1.3 迭代器循环:

使用迭代器(iterator)进行循环遍历容器中的元素。可以使用 begin() 和 end() 方法获取容器的起始和结束迭代器,然后进行循环遍历。示例代码如下:

std::vector<int> numbers = {1, 2, 3, 4, 5};
for (auto it = numbers.begin(); it != numbers.end(); ++it) {
    std::cout << *it << " ";
}

4.1.4 范围迭代器(Range-based iterators):

一些库和工具提供了范围迭代器,可以简化区间迭代的操作,使代码更加简洁和易读。示例代码如下:

std::vector<int> numbers = {1, 2, 3, 4, 5};
for (const auto& num : boost::irange(1, 6)) {
    std::cout << num << " ";
}

4.2 迭代器与数组有哪些异同

4.2.1 相同点:

  1. 元素访问: 迭代器和数组都可以用于访问数据结构中的元素。
  2. 循环遍历: 迭代器和数组都可以用于循环遍历数据结构中的元素。

4.2.2 不同点:

4.2.2.1 内存分配:

数组: 数组在编译时就需要确定其大小,因此在创建数组时需要分配一定大小的连续内存空间。
迭代器: 迭代器是用于遍历容器或数据结构的抽象概念,不需要提前分配内存,可以动态地指向容器中的元素。

4.2.2.2 动态大小:

数组: 数组的大小在创建时确定,通常是固定的,不能动态改变大小。
迭代器: 迭代器可以在遍历容器时根据需要自由移动,可以灵活地处理不同大小的容器。

4.2.2.3 容器适用性:

数组: 适用于需要固定大小且元素类型相同的数据集合。
迭代器: 适用于需要动态大小或元素类型不同的数据结构,如容器(vector、list、map 等)。

4.2.2.4 指针语义:

数组: 数组名本质上是指向数组首元素的指针,因此数组的名称可以被解释为指针。
迭代器: 迭代器是一个对象,它具有指向容器中元素的功能,但不一定是指针。

4.2.2.5 操作灵活性:

数组: 数组的操作相对简单,但不能直接与标准库算法等配合使用。
迭代器: 迭代器提供了更多的操作和灵活性,可以与算法、STL 容器等高效地结合使用。

4.3 比较stack、queue、优先队列

4.3.1 栈(Stack)

遵循后进先出(Last In, First Out,LIFO)的原则。这意味着最后被放入栈的元素将会最先被取出。栈通常支持两种基本操作:压入(Push)和弹出(Pop)。

4.3.1.1 栈的基本特点:
  1. 后进先出(LIFO):最后压入栈的元素将会最先被弹出。

  2. 只能在栈顶进行操作:栈只允许在栈顶进行元素的插入和删除操作,不允许在中间或底部进行操作,这保证了操作的高效性。

4.3.1.2 栈的基本操作:
  1. Push(压入):将元素压入栈顶。

  2. Pop(弹出):从栈顶移除元素,并返回该元素的值。

  3. Top(查看栈顶元素):查看栈顶元素的值,但不对栈做任何修改。

4.3.1.3 栈的应用场景:
  1. 函数调用:函数调用时,局部变量和返回地址被压入栈,函数返回时再从栈中弹出这些信息。

  2. 表达式求值:中缀表达式转换为后缀表达式后,通过栈来实现后缀表达式的求值。

  3. 内存管理:栈内存用于存储函数调用时的局部变量和返回地址等信息。

4.3.1.4 栈的实现方式:
  1. 数组实现:使用数组来实现栈,通过指针来标记栈顶位置。

  2. 链表实现:使用链表来实现栈,每个节点存储栈中的一个元素。

4.3.1.5 C++ 中的栈

在 C++ 中,标准模板库(STL)提供了 std::stack 类模板,用于实现栈数据结构,同时也可以使用 std::vectorstd::list 来实现栈的功能。

下面是一个简单的示例代码,演示如何使用 std::stack 实现栈的基本操作:

#include <iostream>
#include <stack>
int main() {
    std::stack<int> myStack;

    myStack.push(1); // Push 1 into the stack
    myStack.push(2); // Push 2 into the stack

    std::cout << "Top element: " << myStack.top() << std::endl;

    myStack.pop(); // Pop the top element

    std::cout << "Top element after pop: " << myStack.top() << std::endl;

    return 0;
}

4.3.2 队列(Queue)

是一种常见的数据结构,它遵循先进先出(First In, First Out,FIFO)的原则。这意味着最先被放入队列的元素将会最先被取出。队列通常支持两种基本操作:入队(Enqueue)和出队(Dequeue)。

4.3.2.1 队列的基本特点:
  1. 先进先出(FIFO): 最先进入队列的元素将会最先被弹出。

  2. 只能在队列尾和头部进行操作: 队列只允许在队尾进行元素的插入(入队)操作,在队头进行元素的删除(出队)操作。

4.3.2.2 队列的基本操作:
  1. Enqueue(入队): 将元素插入队列的尾部。

  2. Dequeue(出队): 从队列的头部移除元素,并返回该元素的值。

  3. Front(查看队头元素): 查看队列头部元素的值,但不对队列做任何修改。

4.3.2​​​​​​​.3 队列的应用场景:
  1. 任务调度: 队列常用于实现任务调度,先进入队列的任务会先被执行。

  2. 广度优先搜索(BFS): 在图的广度优先搜索算法中,通常会使用队列来存储待访问节点。

  3. 缓冲区管理: 队列常用于实现缓冲区,确保数据按照先后顺序处理。

4.3.2​​​​​​​.4 队列的实现方式:
  1. 数组实现: 使用数组来实现队列,通过两个指针来标记队头和队尾。

  2. 链表实现: 使用链表来实现队列,每个节点存储队列中的一个元素,并维护指向下一个节点的指针。

4.3.2​​​​​​​.5 C++ 中的队列

在 C++ 中,标准模板库(STL)提供了 std::queue 类模板,用于实现队列数据结构,同时也可以使用 std::dequestd::list 来实现队列的功能。

下面是一个简单的示例代码,演示如何使用 std::queue 实现队列的基本操作:

#include <iostream>
#include <queue>

int main() {
    std::queue<int> myQueue;

    myQueue.push(1); // Enqueue 1 into the queue
    myQueue.push(2); // Enqueue 2 into the queue

    std::cout << "Front element: " << myQueue.front() << std::endl;

    myQueue.pop(); // Dequeue the front element

    std::cout << "Front element after dequeue: " << myQueue.front() << std::endl;

    return 0;
}

4.3.3 优先队列(Priority Queue)

4.3.3.1 优先队列的特点:
  1. 按优先级排序: 优先队列中的元素按照优先级顺序排列,而非按照插入顺序或其他顺序。

  2. 最高优先级元素先出队: 在优先队列中,具有最高优先级的元素将会最先被弹出(Dequeue)。

4.3.3.2 优先队列的基本操作:
  1. Insert(插入): 将元素插入到优先队列中,根据元素的优先级进行排序。

  2. Delete-Max/Delete-Min(删除最大/最小元素): 从优先队列中删除并返回具有最高/最低优先级的元素。

4.3.3.3 优先队列的实现方式:
  1. 基于堆(Heap-based): 最常见的实现方式是使用堆(Heap)数据结构,通常是最大堆或最小堆来实现优先队列。

  2. 基于有序数组或有序链表: 也可以使用有序数组或有序链表来实现优先队列,但插入和删除操作的时间复杂度可能会受影响。

4.3.3.4 优先队列的应用场景:
  1. 任务调度: 用于按照优先级顺序执行任务,如操作系统中的进程调度。

  2. 最短路径算法: 在 Dijkstra 算法等最短路径算法中,优先队列用于选择下一个要访问的节点。

4.3.3.5 C++ 中的优先队列:

在 C++ 中,标准模板库(STL)提供了 std::priority_queue 类模板,用于实现优先队列数据结构。默认情况下,std::priority_queue 是一个最大堆,但可以通过自定义比较函数来实现最小堆。

下面是一个简单的示例代码,演示如何使用 std::priority_queue 实现优先队列的基本操作:

#include <iostream>
#include <queue>

int main() {
    std::priority_queue<int> myPriorityQueue;

    myPriorityQueue.push(3); // Insert element 3
    myPriorityQueue.push(1); // Insert element 1
    myPriorityQueue.push(4); // Insert element 4

    std::cout << "Top element: " << myPriorityQueue.top() << std::endl;

    myPriorityQueue.pop(); // Remove the top element

    std::cout << "Top element after pop: " << myPriorityQueue.top() << std::endl;

    return 0;
}

4.4 数组名和指向数组首元素的指针的区别

  • 二者均可通过增减偏移量来访问数组中的元素。
  • 数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。
  • 当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。

第五章 树

5.1 介绍二叉搜索树

5.1.1 二叉搜索树的性质

  • 若它的左子树不空,则左子树上所有结点的值均小于它根结点的值。
  • 若它的右子树不空,则右子树上所有结点的值均大于它根结点的值。
  • 它的左、右树又分为二叉排序树。

5.1.2 二叉搜索树的构建(插入)

  • 只要左子树为空,就把小于父节点的数插入作为左子树
  • 只要右子树为空,就把大于父节点的数插入作为右子树
  • 如果不为空,就一直往下去搜索,直到找到合适的插入位置
void insert(int key)
{
	//定义一个临时指针 用于移动
	Node* temp = root;//方便移动 以及 跳出循环
	Node* prev = NULL;//定位到待插入位置的前一个结点
	while (temp != NULL)
	{
		prev = temp;
		if (key < temp->data)
		{
			temp = temp->left;
		}
		else if(key > temp->data)
		{
			temp = temp->right;
		}
		else
		{
			return;
		}
	}
 
	if (key < prev->data)
	{
		prev->left = (Node*)malloc(sizeof(Node));
		prev->left->data = key;
		prev->left->left = NULL;
		prev->left->right = NULL;
	}
	else
	{
		prev->right = (Node*)malloc(sizeof(Node));
		prev->right->data = key;
		prev->right->left = NULL;
		prev->right->right = NULL;
	}
}

5.1.3 二叉排序树的查找

/*查找元素key*/
bool search(Node* root, int key)
{
	while (root != NULL)
	{
		if (key == root->data)
			return true;
		else if (key < root->data)
			root = root->left;
		else
			root = root->right;
	}
	return false;
}

5.1.4 二叉排序树的删除

  1. 被删除结点为叶子结点

直接从二叉排序中删除即可,不会影响到其他结点。

  1. 被删除结点D仅有一个孩子
  • 如果只有左孩子,没有右孩子,那么只需要把要删除结点的左孩子连接到要删除结点的父亲结点,然后删除D结点;
  • 如果只有右孩子,没有左孩子,那么只要将要删除结点D的右孩子连接到要删除结点D的父亲结点,然后删除D结点。

以下图为例,D=14没有右孩子,只有左孩子。(先把10指向14的右指针移动,去指向13,然后再删除14)。
在这里插入图片描述
再以D=10为例,它没有左孩子,只有右孩子。(先把8指向10的右指针移动,去指向14,然后再删除10)
在这里插入图片描述

  1. 被删除结点左右孩子都在

找到当前节点的左子树的“最右边”节点,或右子树的“最左边”。转化为删除新的元素,直到删除的节点是叶子节点。

int delete_node(Node* node, int key)
{
	if (node == NULL)
	{
		return -1;
	}
	else
	{
		if (node->data == key)
		{
			//当我执行删除操作 需要先定位到删除结点的前一个结点(父节点)
			Node* tempNode = prev_node(root, node, key);
			Node* temp = NULL;
			
			//如果右子树为空,只需要重新连接结点(包含叶子结点),直接删除
			if (node->right == NULL)
			{
				temp = node;
				node = node->left;
				/*判断待删除结点是前一个结点的左边还是右边*/
				if (tempNode->left->data == temp->data)
				{
					Node* free_node = temp;
					tempNode->left = node;
					free(free_node);
					free_node = NULL;
				}
				else
				{
					Node* free_node = temp;
					tempNode->right = node;
					free(free_node);
					free_node = NULL;
				}
			}
			else if (node->left == NULL)
			{
				temp = node;
				node = node->right;
				if (tempNode->left->data == temp->data)
				{
					Node* free_node = temp;
					tempNode->left = node;
					free(free_node);
					free_node = NULL;
				}
				else
				{
					Node* free_node = temp;/
					tempNode->right = node;
					free(free_node);
					free_node = NULL;
				}
			}
			else//左右子树都不为空
			{
				temp = node;
				/*往左子树 找最大值*/
				Node* left_max = node;//找最大值的临时指针
				left_max = left_max->left;//先到左孩子结点
				while (left_max->right != NULL) 
				{
					temp = left_max;
					left_max = left_max->right;
				}
				node->data = left_max->data;
				if (temp != node)
				{
					temp->right = left_max->left;
					free(left_max);
					left_max = NULL;
				}
				else
				{
					temp->left = left_max->left;
					free(left_max);
					left_max = NULL;
				}
			}
			
		}
		else if(key < node->data)
		{
			delete_node(node->left, key);
		}
		else if (key > node->data)
		{
			delete_node(node->right, key);
		}
	}
}

5.1.5 完整代码

#include<stdio.h>
#include<stdlib.h>
typedef struct SortTree {
	int data;//存放数据的数据域
	struct SortTree* left;//指针域 左指针
	struct SortTree* right;//指针域 右指针
}Node;
/*全局变量*/
Node* root;//根节点
 
void Init(int);//初始化操作
void insert(int);//插入操作
void show(Node*);
int delete_node(Node*, int);
Node* prev_node(Node*, Node*, int);
bool search(Node* root, int key);
int main()
{
	Init(8);
	insert(4);
	insert(2);
	insert(5);
	insert(10);
	insert(9);
	insert(13);
	show(root);
	delete_node(root, 8);
	delete_node(root, 13);
	printf("\n");
	show(root);
}
 
/*初始化根节点
int key : 根节点的值
*/
void Init(int key)
{
	root = (Node*)malloc(sizeof(Node));
	root->data = key;
	root->left = NULL;
	root->right = NULL;
}
 
void insert(int key)
{
	//定义一个临时指针 用于移动
	Node* temp = root;//方便移动 以及 跳出循环
	Node* prev = NULL;//定位到待插入位置的前一个结点
	while (temp != NULL)
	{
		prev = temp;
		if (key < temp->data)
		{
			temp = temp->left;
		}
		else if(key > temp->data)
		{
			temp = temp->right;
		}
		else
		{
			return;
		}
	}
 
	if (key < prev->data)
	{
		prev->left = (Node*)malloc(sizeof(Node));
		prev->left->data = key;
		prev->left->left = NULL;
		prev->left->right = NULL;
	}
	else
	{
		prev->right = (Node*)malloc(sizeof(Node));
		prev->right->data = key;
		prev->right->left = NULL;
		prev->right->right = NULL;
	}
}
 
void show(Node* root)
{
	if (root == NULL)
	{
		return;
	}
	show(root->left);
	printf("%d ", root->data);
	show(root->right);
}
/*查找元素key*/
bool search(Node* root, int key)
{
	while (root != NULL)
	{
		if (key == root->data)
			return true;
		else if (key < root->data)
			root = root->left;
		else
			root = root->right;
	}
	return false;
}
int delete_node(Node* node, int key)
{
	if (node == NULL)
	{
		return -1;
	}
	else
	{
		if (node->data == key)
		{
			//当我执行删除操作 需要先定位到前一个结点
			Node* tempNode = prev_node(root, node, key);
			Node* temp = NULL;
			/*
			如果右子树为空 只需要重新连接结点
			叶子的情况也包含进去了 直接删除
			*/
			if (node->right == NULL)
			{
				temp = node;
				node = node->left;
				/*为了判断 待删除结点是前一个结点的左边还是右边*/
				if (tempNode->left->data == temp->data)
				{
					Node* free_node = temp;//释放用的指针
					tempNode->left = node;
					free(free_node);
					free_node = NULL;
				}
				else
				{
					Node* free_node = temp;//释放用的指针
					tempNode->right = node;
					free(free_node);
					free_node = NULL;
				}
			}
			else if (node->left == NULL)
			{
				temp = node;
				node = node->right;
				if (tempNode->left->data == temp->data)
				{
					Node* free_node = temp;//释放用的指针
					tempNode->left = node;
					free(free_node);
					free_node = NULL;
				}
				else
				{
					Node* free_node = temp;//释放用的指针
					tempNode->right = node;
					free(free_node);
					free_node = NULL;
				}
			}
			else//左右子树都不为空
			{
				temp = node;
				/*往左子树 找最大值*/
				Node* left_max = node;//找最大值的临时指针
				left_max = left_max->left;//先到左孩子结点
				while (left_max->right != NULL) 
				{
					temp = left_max;
					left_max = left_max->right;
				}
				node->data = left_max->data;
				if (temp != node)
				{
					temp->right = left_max->left;
					free(left_max);
					left_max = NULL;
				}
				else
				{
					temp->left = left_max->left;
					free(left_max);
					left_max = NULL;
				}
			}
			
		}
		else if(key < node->data)
		{
			delete_node(node->left, key);
		}
		else if (key > node->data)
		{
			delete_node(node->right, key);
		}
	}
}
/*定位到待删除节点的前一个结点
Node* root 从根节点开始
Node* node 待删除的结点
int key 待删除数据
*/
Node* prev_node(Node* root, Node* node, int key)
{
	if (root == NULL || node == root)
	{
		return node;
	}
	else
	{
		if (root->left != NULL && root->left->data == key)
		{
			return root;
		}
		else if(root->right != NULL && root->right->data == key)
		{
			return root;
		}
		else if (key < root->data)
		{
			return prev_node(root->left, node, key);
		}
		else
		{
			return prev_node(root->right, node, key);
		}
	}
}

5.2 介绍平衡二叉树AVL

参考:

平衡搜索二叉树之AVL树解析

5.2.1 AVL树的性质

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树

  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

5.2.2 AVL树节点的定义

template<class T>
struct AVLTreeNode
{
    AVLTreeNode(const T& data)
    : _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr)
    , _data(data), _bf(0)
    {}
    AVLTreeNode<T>* _pLeft; // 该节点的左孩子
    AVLTreeNode<T>* _pRight; // 该节点的右孩子
    AVLTreeNode<T>* _pParent; // 该节点的双亲
    T _data;
    int _bf; // 该节点的平衡因子
};

5.2.3 AVL树的插入

AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点

  2. 调整节点的平衡因子

 
bool Insert(const T& data)
{
// 1. 先按照二叉搜索树的规则将节点插入到AVL树中
// 
// 2. 新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否
破坏了AVL树
// 的平衡性
/*
pCur插入后,pParent的平衡因子一定需要调整,在插入之前,pParent
的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:
1. 如果pCur插入到pParent的左侧,只需给pParent的平衡因子-1即可
2. 如果pCur插入到pParent的右侧,只需给pParent的平衡因子+1即可
此时:pParent的平衡因子可能有三种情况:0,正负1, 正负2
1. 如果pParent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,插入后被调整
成0,此时满足
AVL树的性质,插入成功
2. 如果pParent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更
新成正负1,此
时以pParent为根的树的高度增加,需要继续向上更新
3. 如果pParent的平衡因子为正负2,则pParent的平衡因子违反平衡树的性质,需要对其进
行旋转处理
*/
    while (pParent)
    {
    // 更新双亲的平衡因子
        if (pCur == pParent->_pLeft)
        pParent->_bf--;
        else
        pParent->_bf++;
    // 更新后检测双亲的平衡因子
        if (0 == pParent->_bf)
        {
            break;
        }
        else if (1 == pParent->_bf || -1 == pParent->_bf)
        {
    // 插入前双亲的平衡因子是0,插入后双亲的平衡因为为1 或者 -1 ,说明以双亲
    为根的二叉树
    // 的高度增加了一层,因此需要继续向上调整
            pCur = pParent;
            pParent = pCur->_pParent;
        }
        else
        {
    // 双亲的平衡因子为正负2,违反了AVL树的平衡性,需要对以pParent
    // 为根的树进行旋转处理
        if(2 == pParent->_bf)
        {
            // ...
        }
        else
        {
            // ...
        }
      }
    }
    return true;
}

5.2.4 AVL树的旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:

5.2.4.1 新节点插入较高左子树的左侧—左左:右单旋

在这里插入图片描述

上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左子树增加了一层,导致以60为根的二叉树不平衡,要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,更新节点的平衡因子即可。在旋转过程中,有以下几种情况需要考虑:

  1. 30节点的右孩子可能存在,也可能不存在
  2. 60可能是根节点,也可能是子树

如果是根节点,旋转完成后,要更新根节点

如果是子树,可能是某个节点的左子树,也可能是右子树

void _RotateR(PNode pParent)
{
    // pSubL: pParent的左孩子
    // pSubLR: pParent左孩子的右孩子,注意:该
    PNode pSubL = pParent->_pLeft;
    PNode pSubLR = pSubL->_pRight;
    // 旋转完成之后,30的右孩子作为双亲的左孩子
    pParent->_pLeft = pSubLR;
    // 如果30的左孩子的右孩子存在,更新亲双亲
    if(pSubLR)
        pSubLR->_pParent = pParent;
    // 60 作为 30的右孩子
    pSubL->_pRight = pParent;
    // 因为60可能是棵子树,因此在更新其双亲前必须先保存60的双亲
    PNode pPParent = pParent->_pParent;
    // 更新60的双亲
    pParent->_pParent = pSubL;
    // 更新30的双亲
    pSubL->_pParent = pPParent;
    // 如果60是根节点,根新指向根节点的指针
    if(NULL == pPParent)
    {
        _pRoot = pSubL;
        pSubL->_pParent = NULL;
        }
    else
    {
    // 如果60是子树,可能是其双亲的左子树,也可能是右子树
        if(pPParent->_pLeft == pParent)
        pPParent->_pLeft = pSubL;
        else
        pPParent->_pRight = pSubL;
    }
    // 根据调整后的结构更新部分节点的平衡因子
    pParent->_bf = pSubL->_bf = 0;
}
5.2.4.2 新节点插入较高右子树的右侧—右右:左单旋

在这里插入图片描述
情况与右单旋相似

5.2.4.3 新节点插入较高左子树的右侧—左右:先左单旋再右单旋

在这里插入图片描述
将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新

// 旋转之前,60的平衡因子可能是-1/0/1,旋转完成之后,根据情况对其他节点的平衡因子进行调整
void _RotateLR(PNode pParent)
{
    PNode pSubL = pParent->_pLeft;
    PNode pSubLR = pSubL->_pRight;
    // 旋转之前,保存pSubLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节
    点的平衡因子
    int bf = pSubLR->_bf;
    // 先对30进行左单旋
    _RotateL(pParent->_pLeft);
    // 再对90进行右单旋
    _RotateR(pParent);
    if(1 == bf)
        pSubL->_bf = -1;
    else if(-1 == bf)
        pParent->_bf = 1;
}
5.2.4.4 新节点插入较高右子树的左侧—右左:先右单旋再左单旋

在这里插入图片描述
参考右左双旋。

5.2.4.5 旋转总结

假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑

  1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
    当pSubR的平衡因子为1时,执行左单旋
    当pSubR的平衡因子为-1时,执行右左双旋

  2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL

当pSubL的平衡因子为-1是,执行右单旋

当pSubL的平衡因子为1时,执行左右双旋

旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新

5.3 介绍红黑树

参考:

【数据结构】史上最好理解的红黑树讲解,让你彻底搞懂红黑树

5.3.1 红黑树的几个概念:

  • parent:父节点
  • sibling:兄弟节点
  • uncle:叔父节点( parent 的兄弟节点)
  • grand:祖父节点( parent 的父节点)

5.3.2 红黑树特性

首先,红黑树是一个二叉搜索树,它在每个节点增加了一个存储位记录节点的颜色,可以是RED,也可以是BLACK;通过任意一条从根到叶子简单路径上颜色的约束,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡(最短路径就是全黑节点,最长路径就是一个红节点一个黑节点,当从根节点到叶子节点的路径上黑色节点相同时,最长路径刚好是最短路径的两倍)。

它同时满足以下特性:

  1. 节点是红色或黑色
  2. 根是黑色
  3. 叶子节点(外部节点,空节点)都是黑色,这里的叶子节点指的是最底层的空节点(外部节点),下图中的那些null节点才是叶子节点,null节点的父节点在红黑树里不将其看作叶子节点
  4. 从根节点到叶子节点的所有路径上不能有 2 个连续的红色节点。即红色节点的子节点都是黑色,红色节点的父节点都是黑色。
  5. 从任一节点到叶子节点的所有路径都包含相同数目的黑色节点

5.3.2 红黑树的效率

红黑树的查找,插入和删除操作,时间复杂度都是O(logN)。

查找操作时,它和普通的相对平衡的二叉搜索树的效率相同,都是通过相同的方式来查找的,没有用到红黑树特有的特性。

但如果插入的时候是有序数据,那么红黑树的查询效率就比二叉搜索树要高了,因为此时二叉搜索树不是平衡树,它的时间复杂度O(N)。

插入和删除操作时,由于红黑树的每次操作平均要旋转一次和变换颜色,所以它比普通的二叉搜索树效率要低一点,不过时间复杂度仍然是O(logN)。总之,红黑树的优点就是对有序数据的查询操作不会慢到O(logN)的时间复杂度。

5.3.3 红黑树和AVL树的比较

AVL树的时间复杂度虽然优于红黑树,但是对于现在的计算机,cpu太快,可以忽略性能差异
红黑树的插入删除比AVL树更便于控制操作
红黑树整体性能略优于AVL树(红黑树旋转情况少于AVL树)

5.3.4 红黑树的等价变换

​​在这里插入图片描述
上面这颗红黑树,我们来将所有的红色节点上移到和他们的父节点同一高度上,就会形成如下结构
在这里插入图片描述
这个结构很明显,就是一棵四阶B树(一个节点最多放三个数据),如果画成如下的样子大家应该就能看的更清晰了。
在这里插入图片描述
由上面的等价变换我们就可以得到如下结论:

红黑树 和 4阶B树(2-3-4树)具有等价性
黑色节点与它的红色子节点融合在一起,形成1个B树节点
红黑树的黑色节点个数 与 4阶B树的节点总个数相等
在所有的B树节点中,永远是黑色节点是父节点,红色节点是子节点。黑色节点在中间,红色节点在两边。

5.3.5 红黑树的操作

参考:

平衡搜索二叉树之红黑树(拒绝死记硬背,拥抱理解记忆)

更详细版本(未参考)

红黑树详解

5.3.5.1 旋转操作

旋转操作分为左旋和右旋,左旋是将某个节点旋转为其右孩子的左孩子,而右旋是节点旋转为其左孩子的右孩子。如图:
在这里插入图片描述
以右旋为例进行说明,右旋节点 M 的步骤如下:

将节点 M 的左孩子引用指向节点 E 的右孩子
将节点 E 的右孩子引用指向节点 M,完成旋转
在这里插入图片描述

5.3.5.1 插入操作

红黑树的插入可分为两步:

  1. 按照二叉搜索的树规则插入新节点

  2. 检测新节点插入后,红黑树的性质是否造到破坏(重点)

新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整。

但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,所以我们需要介入调整的情况双亲节点(父节点)和插入的节点都为红。

此时需要对红黑树分情况来讨论,假设cur为当前节点,p为父节点(parent),g为祖父节点(granparent),u为叔叔节点(uncle):

情况一:cur为红,p为红,g为黑,u存在且为红

情况一

解析:在保证局部的每条路径的黑色节点数相同的前提,直接变色,由于修改了g节点颜色,有可能g节点的父节点为红,所以将g当作插入的红节点,向上循环调整直到g的父节点为黑色

解决方法:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。(变色)

情况二:cur为红,p为红,g为黑,u不存在/u存在且为黑

在这里插入图片描述

解析:从局部图发现插入前,p节点所在路径和u节点所在路径的黑色节点数不同,所以此时想直接通过变色就不行了,所以这里需要旋转 + 变色实现。

解决方法(单旋 + 变色):p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,p为g的右孩子,cur为p的右孩子,则进行左单旋转p、g变色–p变黑,g变红。

情况三: cur为红,p为红,g为黑,u不存在/u存在且为黑

在这里插入图片描述

解析:这种情况其实就是情况二的变异版,我们先通过旋转p,cur节点(看清楚了不是旋转g节点)变回情况二就可以借助情况二的方法解决了(啥?反骨又来了,觉得变情况二麻烦,想直接旋转 + 变色解决,你试试如果直接旋转的话,能不能把g、p、cur这三个节点的树形状捋直)

解决方法(双旋 + 变色):

p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;相反,

p为g的右孩子,cur为p的左孩子,则针对p做右单旋转

则转换成了情况2
————————————————

                        版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/qq_68140277/article/details/129686468

插入部分的代码:
 
bool Insert(const T& kv)
    {
        //根
        if (_data == nullptr)
        {
            _data = new node(kv);
            _data->_colour = BLACK;
            return true;
        }
        //寻找合适的插入位置
        node* parent = nullptr;
        node* cur = _data;
        while (cur)
        {
            if (cur->_data < kv)
            {
                parent = cur;
                cur = cur->_right;
            }
            else if (cur->_kv > kv)
            {
                parent = cur;
                cur = cur->_left;
            }
            else//相等
                return false;
        }
        //插入
        cur = new node(kv);
        cur->_parent = parent;
        if (parent->_kv.frist < kv)
            parent->_right = cur;
        else
            parent->_left = cur;
 
        //如果插入的cur的父节点为黑则不用调整
        //调整(父节点为红 -> 祖父结点为“黑”)
        //-> 新插入节点(cur)、父节点(parent)、祖父节点(grandfather)都确定且都为红
        //只有2个条件不确定:
        //1:cur插入的是父节点的左右子树
        //2:uncle(叔叔)节点的颜色/是否存在
        while (parent && parent->_colour == RED)
        {
            node* grandfather = parent->_parent;
            if (grandfather->_left == parent)
            {
                node* uncle = grandfather->_right;
                //情况1:uncle为红色 -> 此时只有祖父节点为红色 + cur在parent2边插入都可以
                if (uncle && uncle->_colour == RED)
                {                
                    //直接变色
                    grandfather->_colour = RED;
                    parent->_colour = BLACK;
                    uncle->_colour = BLACK;
                }
                //情况2、3:uncle为黑色/或不存在 + cur在parent左/右边插入 
                else
                {
                    //情况2:左边插入 -> 右旋 + 变色
                    if (cur == parent->_left)
                    {
                        RoateR(grandfather);
                        parent->_colour = BLACK;
                        grandfather->_colour = RED;
                    }
                //情况3:右边插入 -> 左旋 变为情况2 -> 右旋(双旋)+ 变色
                else
                {
                    RoateL(parent);
                    RoateR(grandfather);
                    cur->_colour = BLACK;
                    grandfather->_colour = RED;
                }
                    break;
                }
            }
            else//右
            {
                node* uncle = grandfather->_left;
                if (uncle && uncle->_colour == RED)
                {
                    //直接变色
                    grandfather->_colour = RED;
                    parent->_colour = BLACK;
                    uncle->_colour = BLACK;
                }
                else
                {
                    if (cur == parent->_left)
                    {
                        RoateL(grandfather);
                        parent->_colour = BLACK;
                        grandfather->_colour = RED;
                    }
                    //情况3:右边插入 -> 左旋 变为情况2 -> 右旋(双旋)+ 变色
                    else
                    {
                        RoateR(parent);
                        RoateL(grandfather);
                        cur->_colour = BLACK;
                        grandfather->_colour = RED;
                    }
                    break;
 
                }
            }
        }
        _data->_colour = BLACK;
        return true;
    }

第六章 面向对象编程

6.1 final标识符的作用是什么?

​放在类的后面表示该类无法被继承,也就是阻止了从类的继承,放在虚函数后面该虚函数无法被重写,表示阻止虚函数的重载。

6.2 虚函数是怎么实现的?它存放在哪里在内存的哪个区?什么时候生成的

在C++中,虚函数的实现原理基于两个关键概念:虚函数表和虚函数指针

虚函数表:每个包含虚函数的类都会生成一个虚函数表,其中存储着该类中所有虚函数的地址。虚函数表是一个由指针构成的数组,每个指针指向一个虚函数的实现代码。

虚函数指针:在对象的内存布局中,编译器会添加一个额外的指针,称为虚函数指针或虚表指针。这个指针指向该对象对应的虚函数表,从而让程序能够动态的调用虚函数。

当一个基类指针或引用调用虚函数时,编译器会使用虚表指针来查找该对象对应的虚函数表,并根据函数在虚函数表中的位置来调用正确的虚函数。

在编译阶段生成,虚函数和普通函数一样存放在代码段,只是它的指针又存放在了虚表之中。

第七章 RAII

7.1 简介

RAII(Resource Acquisition Is Initialization)是一种 C++ 编程中的重要设计理念,它指的是资源获取即初始化。RAII 的核心思想是通过对象的生命周期来管理资源的获取和释放,确保资源在对象创建时被分配,而在对象销毁时被释放,从而避免资源泄漏和提高代码的可靠性和安全性。

RAII 技术的基本原则是:将资源的获取和释放的代码封装在一个类的构造函数和析构函数中。在对象构造时,资源被获取并初始化;在对象析构时,资源被释放。这样,无论是正常退出还是异常退出,都可以确保资源被正确地释放,避免了资源泄漏和错误。

常见的应用 RAII 的场景包括:

  1. 智能指针:如 std::unique_ptr、std::shared_ptr 等,在对象销毁时自动释放所管理的动态内存资源。

  2. 文件操作:通过 RAII 可以在对象构造时打开文件,在对象析构时关闭文件,确保文件资源被正确释放。

  3. 互斥量:如 std::lock_guard,在对象的生命周期内管理互斥量的加锁和解锁,确保线程安全。

RAII 是 C++ 中一种重要的资源管理方式,它使得代码更加健壮、可维护,并且有助于避免资源泄漏和提高程序的安全性。

7.1 智能指针

见本文2.1节。
2.1 智能指针

7.1.1 简介

在 C++11 中,为了方便实现自动加锁和解锁的操作,提供了 lock_guard 类模板。它是一个轻量级的 RAII(资源获取即初始化)类,用于在作用域结束时自动释放互斥锁。

std::lock_guard 用于管理互斥锁的加锁和解锁操作。它的主要作用是在构造函数中获取一个互斥锁,然后在析构函数中自动释放该锁,以确保在锁保护区域的结束时正确解锁。

std::lock_guard 的作用是获取互斥量的锁,并在作用域结束时自动释放锁。这样可以避免手动管理锁的复杂性和风险,同时也可以确保在使用共享资源时不会被其他线程打断。

简单来说就是使用 std::lock_guard 让开发者使用时不用关心 std::mutex 锁的释放。

7.1.2 使用例子

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx; // the mutex to protect the shared resource

void thread_function()
{
    //访问临界区域资源
    // 主动设置局部作用域
	{	
		// 在构造函数中调用 lock()
	    std::lock_guard<std::mutex> lock(mtx); 
	    
	    std::cout << "Thread " << std::this_thread::get_id() << " is accessing the shared resource." << std::endl;
	    // access the shared resource here...
	}
	//离开作用域,lock_guard会调用其析构函数,会自动调用 unlock()

	//other code
}

int main()
{
    std::thread t1(thread_function);
    std::thread t2(thread_function);
    t1.join();
    t2.join();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值