C++后端知识点
面试 13

C++开发知识点

C++特性

面向对象特性

封装、继承、多态和抽象 封装就是将数据或函数等集合在一个个的单元中(我们称之为类) 从封装的角度看,public, private 和 protected 属性的特点如下。

  • 不管哪种属性,内类都是可以访问的
  • public 是一种暴露的手段,比如暴露接口,类的对象可以访问
  • private 是一种隐藏的手段,类的对象不能访问
  • protected 成员:
    • 和 public 一样可以被子类继承
    • 和 private 一样不能在类外被直接调用
    • 特例:在衍生类中可以通过衍生类对象访问

继承主要实现重用代码,节省开发时间。 子类可以继承父类的一些东西。

  • a.公有继承(public) 公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态(基类的私有成员仍然是私有的,不能被这个派生类的子类所访问)。
  • b.私有继承(private) 私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员(并且不能被这个派生类的子类所访问)。
  • c.保护继承(protected) 保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员(并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的)。 这里特别提一下虚继承。虚继承是解决C++多重继承问题(其一,浪费存储空间;第二,存在二义性问题)的一种手段。比如菱形继承,典型的应用就是 iostream, 其继承于 istream 和 ostream,而 istream 和 ostream 又继承于 ios。
  • 多态: 多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。与之相对应的编译时绑定函数称为静态绑定

多态

重载,覆盖,重写

  1. overload,将语义相近的几个函数用同一个名字表示,但是参数列表(参数的类型,个数,顺序不同)不同,这就是函数重载,返回值类型可以不同 特征:相同范围(同一个类中)、函数名字相同、参数不同、virtual关键字可有可无
  2. override,派生类覆盖基类的虚函数,实现接口的重用,返回值类型必须相同 特征:不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字(必须是虚函数)
  3. overwrite,派生类屏蔽了其同名的基类函数,返回值类型可以不同 特征:不同范围(基类和派生类)、函数名字相同、参数不同或者参数相同且无virtual关键字

虚函数表存在常量区,编译时确定,一旦确定就不会进行修改

编译时多态:函数重载、运算符重载、模板

运行时多态:虚函数virtual和重写override、通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为

虚函数作用

  • 用于声明一个函数可以在派生类中被重写(覆盖)。
  • 如果基类中的函数被声明为 virtual,则派生类可以重写该函数,并且通过基类指针或引用调用时,会执行派生类的版本(运行时多态)。
  • 如果基类中的函数没有 virtual,则派生类即使定义同名函数,也不会覆盖基类的函数(而是隐藏基类的函数),此时调用取决于指针/引用的类型(编译时绑定)。

override:显式标记子类中的函数是重写基类的虚函数,主要增强可读性,不重写会报错

构造函数一般不定义为虚函数,析构函数一般写成虚函数

  1. 构造函数不能声明为虚函数 1). 因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等 2). 虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了
  2. 析构函数最好声明为虚函数,为什么? 析构函数可以为虚函数,当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。 如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,就会出现调用基类的析构函数而不调用派生类析构函数,出现内存泄漏。析构函数调用的次序时先派生类后基类的。和构造函数的执行顺序相反。并且析构函数要是virtual的,否则如果用父类的指针指向子类对象的时候,析构函数静态绑定,不会调用子类的析构。

C++新特性

  • 智能指针,移动语义,final,override,nullptr,auto,decltype
  • bind,lambda
  • RAII,用于管理资源(如内存、文件句柄、锁等),确保资源在作用域结束时自动释放,避免资源泄漏

C++面试语法知识

如何实现多线程?

thread:线程管理

mutex lock_guard(互斥锁)

condition_variable 条件变量,用于线程同步

atomic:用于简单变量的线程安全访问

引用和指针的区别

  • 指针有自己的一块空间,而引用只是一个别名
  • 引用必须被初始化,但是指针可以为NULL
  • 引用的sizeof是对象的大小,指针大小和地址空间大小有关
  • 指针在使用中可以指向其他对象,但是引用只能是一个对象的引用,不能被改变

struct和union的区别

结构体中的每个成员都有自己独立的地址,它们是同时存在的;共同体中的所有成员占用同一段内存,它们不能同时存在;

sizeof(struct)是内存对齐后所有成员长度的总和,sizeof(union)是内存对齐后最长数据成员的长度,union中的所有成员起始地址都是一样的

struct和class的区别

内部成员变量及成员函数的默认访问属性

define和const的区别

  1. #define定义的常量没有类型,所给出的是一个立即数;const定义的常量有类型名字,存放在静态区域
  2. #define定义的宏变量在预编译替换;const所定义的变量有类型,放在静态内存区域
  3. const定义的常量可以用指针去指向该常量的地址
  4. #define可以定义简单的函数,const不可以定义函数

new / delete 如何实现

简单类型直接调用 operator new 分配内存;复杂数据类型new 的时候先调用operator new,然后在分配的内存上调用构造函数

简单数据类型delete默认只是调用free函数;复杂数据类型先调用析构函数再调用operator delete

delete[] 和delete

delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数 用new分配的内存用delete释放,用new[]分配的内存用delete[]释放

new / delete 与 malloc / free有什么异同

  1. new / delete 是c++关键字,需要编译器支持。 malloc/free是库函数,需要c的头文件支持
  2. new操作符申请内存分配时无须制定内存块的大小,编译器会根据类型信息自行计算。而mallco则需要显式地指出所需内存的尺寸
  3. new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,故new是符合类型安全性的操作符;而malloc内存成功分配返回的是void *,需要通过类型转换将其转换为我们需要的类型
  4. new / delete 可以做自定义类型对象构造和析构(并且允许重载),而malloc和free之只能动态申请和释放内存,不会构造和析构
  5. new是自由存储区(可以不在堆),malloc只能在堆中申请

const 关键字

const修饰类的成员变量,表示常量不可能被修改 const修饰类的成员函数,表示该函数不会修改类中的数据成员,不会调用其他非const的成员函数 const函数只能调用const函数,非const函数可以调用const函数

static 关键字

函数体内: static 修饰的局部变量作用范围为该函数体,不同于auto变量,其内存只被分配一次,因此其值在下次调用的时候维持了上次的值

模块内:static修饰全局变量或全局函数,可以被模块内的所有函数访问,但是不能被模块外的其他函数访问,使用范围限制在声明它的模块内

类中:修饰成员变量,表示该变量属于整个类所有,对类的所有对象只有一份拷贝

类中:修饰成员函数,表示该函数属于整个类所有,不接受this指针,只能访问类中的static成员变量 注意和const的区别!!!const强调值不能被修改,而static强调唯一的拷贝,对所有类的对象

  • 总结:const和static关键字都可以修饰变量和函数,const关注不可修改,static关注生命周期和作用域

volatile

用于告诉编译器:该变量的值可能会在程序的控制之外被改变,因此编译器 不应该对该变量进行优化(如缓存到寄存器、指令重排等)

atomic

std::atomic 提供 原子操作内存顺序控制,适用于多线程同步。

定义和声明的区别

为变量分配地址和存储空间的称为定义,不分配地址的称为声明。一个变量可以在多个地方声明,但是只在一个地方定义。 加入 extern 修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。

左值和右值

  1. 左值:具有地址,存储在内存中,可以出现在=的左侧,可以取地址&,变量、对象、数组元素都是左值
  2. 右值:通常没有地址,字面量/表达式都是右值,只能在=的右侧
  3. 左值引用,引用是变量的别名,引用是通过指针实现的
  4. 右值引用,const int &&a = 20 因为20没有地址,先将20放在栈里面,再用指针指向这个临时地址(纯右值、将亡值)

右值引用可以实现移动语义、完美转发,避免内存泄漏问题

std::move(a),将a改变性质为右值

std::move,避免深拷贝带来的资源浪费

移动语义和完美转发

移动语义:可以理解为转移所有权,拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用,通过C++11新增的移动语义可以省去很多拷贝负担,如何利用移动语义,主要通过移动构造函数

完美转发:指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参。转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数也是右值

C++内存管理

堆和栈的区别(内存上)

  • 堆栈空间分配区别: 栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈; 堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
  • 堆栈的缓存方式区别 栈:是内存中存储值类型的,大小为2M(window,linux下默认为8M,可以更改),超出则会报错,内存溢出; 堆:内存中,存储的是引用数据类型,引用数据类型无法确定大小,堆实际上是一个在内存中使用到内存中零散空间的链表结构的存储空间,堆的大小由引用类型的大小直接决定,引用类型的大小的变化直接影响到堆的变化。

内存分区

  1. 栈区:由编译器自动分配释放 ,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构中的栈
  2. 堆区:由程序员分配释放(new运算符), 若程序员不释放,程序结束时由OS回收 。
  3. 全局区(静态区):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放。
  4. 常量区:常量字符串就是放在这里的。 程序结束后由系统释放
  5. 代码区:存放函数体的二进制代码
高地址 ----------------------
        栈区(Stack)
        ---------------------
        堆区(Heap)
        ---------------------
        常量存储区(Constant Storage)
        ---------------------
        常量字符串(常量区)
        ---------------------
        代码区
低地址 ----------------------

OS有一张空闲链表,来管理堆区的内存分配和释放的。

申请的时候,遍历空闲链表,找到大于等于申请空间的内存块。然后开始分配内存,多余的分割出来,形成新的内存块。在分配内存块的首地址处记录分配的大小,用于后续的内存释放操作。返回一个用户可用的内存区域的指针。

释放的时候,根据用户提供的指针,向前偏移一字节,找到元数据的大小,标记内存块为空闲,如果相邻的内存块也是空闲的,就会进行合并空闲块。

  • 动态内存分配通过空闲链表管理堆区的内存。
  • 分配时,系统会查找合适的空闲块,并可能分割多余部分。
  • 释放时,系统会标记内存块为空闲,并尝试合并相邻的空闲块。

堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。

全局/静态存储区:存放全局变量和静态变量 常量区:存放常量,不允许被修改

全局变量和局部变量

  • 生命周期不同: 全局变量随主程序创建和创建,随主程序销毁而销毁 局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在; 内存中分配在全局数据区
  • 使用方式不同:通过声明后全局变量程序的各个部分都可以用到;局部变量只能在局部使用,分配在栈区

C++文件编译与执行的四个阶段

  1. 预处理:根据文件中的预处理指令来修改源文件的内容
  2. 编译:编译成汇编代码
  3. 汇编:把汇编代码翻译成目标机器指令
  4. 链接:链接目标代码生成可执行程序

静态链接和动态链接,静态链接在编译时,静态链接的库代码被直接嵌入到可执行文件中;动态链接在运行时加载,可能会增加启动时间

深拷贝和浅拷贝的区别

深拷贝和浅拷贝可以简单的理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,如果资源重新分配了就是深拷贝;反之没有重新分配资源,就是浅拷贝

构造函数和析构函数的执行顺序

👀️全局对象的构造函数会在main函数之前执行。

  • 构造函数
  1. 首先调用父类的构造函数
  2. 调用成员变量的构造函数
  3. 调用类自身的构造函数
  • 析构函数,和构造函数的调用顺序相反

关于sizeof

sizeof 计算的是在栈中分配的内存大小

  1. sizeof不计算static变量占用的内存
  2. 32位系统的指针的大小是4个字节,64位系统的指针是8字节,而不用管指针类型;
  3. char型占1个字节,int占4个字节,short int占2个字节 long int占4个字节,float占4字节,double占8字节,string占4字节 一个空类占1个字节,单一继承的空类占1个字节,虚继承涉及到虚指针所以占4个字节
  4. 数组的长度: 若指定了数组长度,则不看元素个数,总字节数=数组长度*sizeof(元素类型) 若没有指定长度,则按实际元素个数类确定 若是字符数组,则应考虑末尾的空字符
  5. 结构体对象的长度 在默认情况下,为方便对结构体内元素的访问和管理,当结构体内元素长度小于处理器位数的时候,便以结构体内最长的数据元素的长度为对齐单位,即为其整数倍。若结构体内元素长度大于处理器位数则以处理器位数为单位对齐。
  6. unsigned影响的只是最高位的意义,数据长度不会改变,所以sizeof(unsigned int)=4
  7. 自定义类型的sizeof取值等于它的类型原型取sizeof
  8. 对函数使用sizeof,在编译阶段会被函数的返回值的类型代替
  9. sizeof后如果是类型名则必须加括号,如果是变量名可以不加括号,这是因为sizeof是运算符
  10. 当使用结构类型或者变量时,sizeof返回实际的大小。当使用静态数组时返回数组的全部大小,sizeof不能返回动态数组或者外部数组的尺寸

STL

STL六大组件

容器、迭代器、算法、函数对象、适配器、分配器

STL的底层实现

map 、set 、multiset 、multimap 的底层实现都是红黑树。 红黑树的特性 :

  1. 每个结点或是红色或是黑色;
  2. 根结点是黑色;
  3. 每个叶结点是黑的;
  4. 如果一个结点是红的,则它的两个儿子均是黑色;
  5. 每个结点到其子孙结点的所有路径上包含相同数目的黑色结点

红黑树比AVL的优势,为何用红黑树

红黑树的查询性能略微逊色于AVL树,因为他比avl树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的AVL树最多多一次比较,但是,红黑树在插入和删除上完爆avl树,AVL树每次插入删除会进行大量的平衡度计算,而红黑树为了维持红黑性质所做的红黑变换和旋转的开销,相较于AVL树为了维持平衡的开销要小得多

map容器,在for循环中erase(it)会怎么样,怎么修改

除当前迭代器 it 指向的元素,会导致 迭代器失效

std::map::erase 返回 下一个有效迭代器,但更安全的方式是 先递增迭代器,再删除

vector使用注意

  • 在push_back()、resize()、insert()后有可能引起重新分配内存,此时原始的迭代器失效,需要重新生成一次迭代器
  • 为了防止多次扩容造成性能低,可以先使用reserve(),预留足够的空间,最后在使用 shrink_to_fit()收缩空间
  • vector不能用来存储bool类型的元素,存储机理不同,bool类型的元素存储到vector中,会转化为1个bit,不是1个byte,可以用deque或者是bitset存储bool类型的
  • vector的扩容:VS是1.5倍、GCC是2倍

list和vector的底层实现区别

std::vector 动态数组 连续内存存储,支持随机访问
std::list 双向链表 非连续内存存储,每个元素存储指向前驱和后继的指针
  • std::vector
    • 类似于动态数组,内存连续分配。
    • 支持高效的随机访问(O(1) 时间复杂度)。
    • 插入/删除操作(非尾部)可能需要移动元素(O(n))。
  • std::list
    • 双向链表结构,每个节点存储数据和指向前后节点的指针。
    • 不支持随机访问(必须遍历链表,O(n) 时间复杂度)。
    • 插入/删除操作(已知位置)是 O(1),因为只需调整指针。

内联函数

函数调用是有时间和空间开销的。程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码;函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码。如果函数体代码比较多,需要较长的执行时间,那么函数调用机制占用的时间可以忽略;如果函数只有一两条语句,那么大部分的时间都会花费在函数调用机制上,这种时间开销就就不容忽视。 为了消除函数调用的时空开销,C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数。

默认生成的6个特殊成员函数

  • 默认构造函数
  • 析构函数
  • 赋值构造函数(和拷贝构造函数的区别是是否有新对象产生)
  • 拷贝构造函数
  • 移动构造函数
  • 移动赋值函数

拷贝构造函数

A::A(const A &a){}

为什么必须是当前类的引用呢? 循环调用。如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。

只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。

为什么是 const 引用呢? 拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了

另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能直接转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了

如果拷贝构造函数传指针而不是引用:无法通过编译

如果不是引用而是值,无限递归,编译错误

调用拷贝构造函数的三种情况

系统自动生成的构造函数:普通构造函数和拷贝构造函数 生成一个实例化的对象会调用一次普通构造函数,而用一个对象去实例化一个新的对象所调用的就是拷贝构造函数 调用拷贝构造函数的情形:

  1. 用类的一个对象去初始化另一个对象的时候
  2. 当函数的参数是类的对象时,就是值传递的时候,如果是引用传递则不会调用
  3. 当函数的返回值是类的对象或者引用的时候

智能指针

C++11 引入了三种主要的智能指针类型,用于自动管理动态分配的内存,避免内存泄漏:

  1. std::shared_ptr:共享所有权指针,多个 shared_ptr 可以指向同一个对象,通过引用计数管理对象的生命周期
  2. std::unique_ptr:独占所有权指针,同一时间只能有一个 unique_ptr 指向某个对象
  3. std::weak_ptr:弱引用指针,不增加引用计数,用于解决 shared_ptr 循环引用问题
  • unique_ptr
    • 独占所有权,适用于明确资源归属的场景(如工厂模式返回对象)。
    • 高性能,无引用计数开销。
  • shared_ptr
    • 共享所有权,适用于多个对象需要共享同一资源的场景(如缓存、观察者模式)。
    • 需注意循环引用问题(需配合 weak_ptr解决)

shared_ptr 的引用计数是如何实现的

std::shared_ptr 使用引用计数来管理对象的生命周期。其核心实现原理如下:

shared_ptr 的引用计数是存放在堆上的,多个 shared_ptr 的对象的引用计数都指向同一个堆地址。

实现方法:每个由 shared_ptr 管理的对象都有一个关联的控制块

  • 引用计数
  • 指向动态分配对象的指针
  • 自定义删除器,分配器

当创建一个新的 shared_ptr 时,引用计数初始化为1, 当复制 shared_ptr(拷贝构造或赋值)时,引用计数加1,当销毁 shared_ptr(析构或重置)时,引用计数减1,当引用计数减到0时,删除所管理的对象,当弱引用计数减到0时,删除控制块本身

weak_ptr需要访问对象的声明周期,但是不能影响对方,不然会出现循环引用

unique_ptr 的unique 是如何实现的

std::unique_ptr 的"唯一所有权"特性通过以下方式实现:

  1. 禁止拷贝构造和拷贝赋值
    • unique_ptr 的拷贝构造函数和拷贝赋值运算符被声明为 delete
    • 这样编译器会阻止任何复制 unique_ptr 的尝试
  2. 允许移动语义
    • 提供了移动构造函数和移动赋值运算符
    • 允许通过 std::move 转移所有权

make_shared 和 make_unique 的作用

优先使用 make_shared 和 make_unique 的原因是为了避免直接 new 可能导致的异常安全问题

智能指针自动内存管理方面的优缺点

用了智能指针还会出现内存泄漏吗?怎么解决?

循环引用(shared_ptr 特有)

  • 如果两个 shared_ptr 互相引用,引用计数无法归零,导致内存泄漏

自定义删除器未正确释放资源

  • 如果 shared_ptrunique_ptr 使用了自定义删除器,但删除器未正确释放资源,可能导致内存泄漏

智能指针使用注意事项

  • 不使用相同的内置指针值初始化,或reset多个智能指针
  • 不delete get()返回的指针
  • 不使用get()初始化或reset另一个智能指针
  • get()返回的智能指针可能变成dangling pointer
  • 如果智能指针管理的内存不是new出来的,需要提供删除器

设计模式

线程安全的单例模式怎么实现?有哪几种方法

  1. 在程序启动的时候创建单例对象,因此是线程安全的,只在主线程中完成
  2. 在第一次调用的时候创建,C++11保证局部变量的初始化时线程安全的
class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() {}
};

多线程&规约

面试C++后端,碰到一个多线程的题目,记录下,还是比较基础的,但是之前没怎么写过多线程的代码,比较吃力,说明没复习到位

题目: (多线程)

有一个很大的(100-200万个元素)字符串数组, 数组中元素存在重复的可能,统计每个字符串的出现次数, 并按字符串出现次数排序。

知识点

  1. vector<future<unordered_map<string, int>>> futures
    通过一个vector存储每个线程任务的返回值,future对象,并且每个异步对象返回类型为unordered_map
  2. async(launch::async, function_name, cerf(chrunks[i]))
    1. launch::async 表示强制在新线程中运行任务,而非延迟调用get()才执行
    2. cerf表示以常量引用的方式传给function,避免拷贝较大对象,并且是只读的
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <thread>
#include <mutex>
#include <algorithm>
#include <future>

using namespace std;

unordered_map<string, int> merge_maps(const unordered_map<string, int>& a, 
                                     const unordered_map<string, int>& b) {
    unordered_map<string, int> result = a;
    for (const auto& pair : b) {
        result[pair.first] += pair.second;
    }
    return result;
}

unordered_map<string, int> count_in_chunk(const vector<string>& chunk) {
    unordered_map<string, int> local_counter;
    for (const auto& str : chunk) {
        local_counter[str]++;
    }
    return local_counter;
}

unordered_map<string, int> count_frequencies(const vector<string>& strings, int num_threads = 4) {
    // 1. 数据分片
    vector<vector<string>> chunks(num_threads);
    int chunk_size = (strings.size() + num_threads - 1) / num_threads;
  
    for (size_t i = 0; i < strings.size(); ++i) {
        chunks[i % num_threads].push_back(strings[i]);
    }
  
    // 2. 创建线程池并分配任务
    vector<future<unordered_map<string, int>>> futures;
    for (int i = 0; i < num_threads; ++i) {
        futures.push_back(async(launch::async, count_in_chunk, cref(chunks[i])));
    }
  
    // 3. 合并结果
    unordered_map<string, int> final_counts;
    for (auto& future : futures) {
        final_counts = merge_maps(final_counts, future.get());
    }
  
    return final_counts;
}

void count_frequencies_sorted(const vector<string>& strings, int num_threads = 4) {
    // 1. 数据分片
    vector<vector<string>> chunks(num_threads);
    int chunk_size = (strings.size() + num_threads - 1) / num_threads;
  
    for (size_t i = 0; i < strings.size(); ++i) {
        chunks[i % num_threads].push_back(strings[i]);
    }
  
    // 2. 创建线程池并分配任务
    vector<future<unordered_map<string, int>>> futures;
    for (int i = 0; i < num_threads; ++i) {
        futures.push_back(async(launch::async, count_in_chunk, cref(chunks[i])));
    }
  
    // 3. 合并结果
    unordered_map<string, int> final_counts;
    for (auto& future : futures) {
        final_counts = merge_maps(final_counts, future.get());
    }
  
    // 4. 排序结果
    vector<pair<string, int>> sorted_counts(final_counts.begin(), final_counts.end());
    sort(sorted_counts.begin(), sorted_counts.end(), [](const auto& a, const auto& b) {
        if (a.second != b.second) {
            return a.second > b.second; // 按频率降序
        }
        return a.first < b.first; // 频率相同按字典序升序
    });
}

内存安全

内存泄露、野指针、数组越界

  • 动态分配内存所开辟的空间,在使用完毕后未手动释放,导致一直占据该内存,即为内存泄漏,注意malloc/free要配套,对指针赋值的时候应该注意被赋值的指针是否需要释放;使用的时候记得指针的长度,防止越界
  • 野指针,释放内存后没有将指针置为NULL
  • 数组越界,访问下标超出合理范围

解决方法?

C++后端知识点
https://www.lihuigu.cn//archives/c-hou-duan-zhi-shi-dian
作者
lihuigu
发布于
更新于
许可协议