八股C++
RVO:避免函数返回对象时产生临时拷贝,直接调用栈上构造返回值。C++11:结合move构造,当RVO不适用时,可将返回值资源迁移,减少深拷贝。浅拷贝和深拷贝:浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象共享同一块内存。深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不影响旧对象。而浅拷贝会。Websocket是一种协议,用于Web应用程序和服务器之间建立实时
有试过编译程序吗,主要用哪些工具。
编译程序,将程序员所写的代码转换成计算机能够识别的可执行程序,程序运行:
- 预编译:将头文件编译,进行宏替换,输出.i文件
- 编译:将其转化为汇编语言文件,主要做词法分析,语义分析以及检查错误,检查无误后将代码翻译成编译语言,生成.s文件
- 汇编:汇编器将汇编语言文件翻译成机器语言,生成.o文件(二进制文件)
- 链接:将目标文件和库链接到一起,生成可执行文件
编译工具gcc与g++
GCC(GNU Compiler Collection)是一个强大的编译器套件,支持多种编程语言。
GCC包含多个编译器前端和一个通用的后端。这个通用后端负责将中间表示(由前端生成的,通常是汇编代码)转换成目标机器代码。
gcc(GNU C Compiler)是GCC的一个组成部分,它是GCC编译器集合中用于编译C(以及其他语言)的命令行工具和驱动程序。gcc和g++都属于GCC(也可以叫GNU组织),gdb是GNU的调试器。
实际使用中,我们更习惯用gcc指令编译C语言程序,使用g++指令编译C++代码。但是gcc 指令也可以用来编译 C++ 程序,同样 g++ 指令也可以用于编译 C 语言程序。
(1)后缀为.c的, gcc 把它当作是 c程序,而g++当作是 c++程序;
(2)后缀为.cpp 的,两者都会认为是C++程序,c++的语法规则更加严谨一些,编译阶段对于c++代码, gcc会调用g++,因为gcc命令不能自动和c++程序使用的库联接,通常用g++来完成链接, 为了统一起见,干脆编译/链接统统用g++ 了,这就给人―种错觉,好像cpp程序只能用g++似的;
(3)编译可以用gcc/g++,而链接可以用g++或者gcc -lstdc++;gcc命令不能自动和C++程序使用的库联接.所以通常使用g++来完成联接。但在编译阶段, gcc会自动调用g++,二者等价。
make和makefile
当我们的程序只有一个源文件时,直接就可以用gcc命令编译它。但是,如果我们的程序包含很多个源文件时,就需要用make工具来进行编译。
make工具可以看成一个智能的批处理工具,它本身没有编译和链接的功能,而是类似于批处理的方式-通过调用makefile文件中用户指定的命令来进行编译和链接。
make工具就根据makefile中的命令进行编译和链接,makefile中就包含了调用gcc(也可以是别的编译器)去编译某个源文件的命令。
cmake和cmakelist
cmake可以更加简单生成makefile文件给上面的make用,而且cmake还是一个很好用的跨平台编译工具,就是可以跨平台生成对应平台能用的makefile。
cmake需要根据CMakeLists.txt文件生成makefile文件。
nmake是Microsoft Visual Studio中的附带命令,需要安装VS,实际上可以说相当于linux的make。
VScode中CPP编译调试环境配置
VScode通过项目文件夹来管理项目,项目的配置文件保存在.vscode文件夹中,其中
- tasks.json (compiler build settings) ,负责编译
- launch.json (debugger settings),负责调试
- c_cpp_properties.json (compiler path and IntelliSense settings),负责更改路径等设置
- setting.json,负责vsode编辑器和插件的相关配置工作
- extensions.json,负责推荐当前项目中使用的插件
详细看:
关于gcc和g++版本,通常在/usr/bin/gcc。。
usr是指Unix System Resource,而不是User。
/usr/bin下面都是系统预装的可执行程序,系统升级有可能会被覆盖。
/usr/local/bin目录是给用户放置自己的可执行程序,不会被系统升级而覆盖同名文件
/bin、/sbin、/usr/bin、/usr/sbin、/usr/local/bin、/user/local/sbin都是存放可执行文件的目录,但各有特定用途。
Linux下开发的调试命令有哪些,常用的命令和一些管理内存、进程的命令
GDB(GNU symbolic debugger),看名称可以知道如上gcc、g++都诞生至于GNU计划,是Unix及Unix-like下常用的程序调试工具。
- 程序启动时可以按照自定义的要求运行程序,例如设置参数和环境变量
- 使程序在指定条件下停止,并查看此时程序的运行状态(查看变量或表达式的值)
- 程序执行过程中,可以修改程序中内容
基于Linux系统是免费、开源,Linux也衍生出了不同的发行版本。LInux发行版可以分为两个系列,分为RedHat系列和Dibian
系列,不同的Linux发行版,系统管理包的工具也不同,所有安装的指令也不同。
RedHat系列代表Linux发行版有RedHat、CentOS、Fedora等,使用yum作为包管理器。RedHat系列的Linux发行版通过在命令行 窗口中执行sudo yum -y install gdb 指令,即可完成GDB调试器的安装。
Debian系列代表Linux发行版有Debian、Ubuntu等,使用apt作为包管理器。这系列的Linux发行版通过执行sudo apt -y install gdb 指令,既可完成GDB调试器的安装。
查看GDB调试器是否安装成功,在命令行窗口中执行gdb -v,执行结果如下图所示则表示安装GDB调试器成功。
GDB调试需要程序与GDB调试器一起运行在,则必须指示编译器将调试信息放到程序的代码对象中。
这些调试信息也称为是调试符号或符号信息,它们包含函数和变量的名称以及CPU指令、源文件和行号之间的关系。
对于GNU编译器gcc和大部分编译器来说,进行调试的编译器标志是-g,所以在编译程序的指令后面加上-g才能生成满足调试的可执行文件。
默认优化模式下的编译程序指令:gcc hello.c -o hell,生成可执行文件hell,大小为8464
程序调试模式下的编译程序指令:gcc hello.c -o hello -g,生成可调试的可执行文件hello,大小为9760
常用调试命令,run启动目标程序(r),start,break location(b),break...if cond条件断点,watch cond观察断点,catch event捕捉断点,单步调试next(n)count,step(s)count,until(u),查看输出print(p)num,display,删除clear,delete,禁用disable。。。。详细看上面链接深入学习。。还没遇到考这个。。。
Linux常用命令
关于Shell,Shell 是操作系统与用户之间的桥梁,它接收用户输入的命令,并调用系统内核去执行相应的任务。在 Linux 系统中,最常用的 Shell 是 Bash(Bourne Again Shell)。我们平时在终端中输入的命令,实际上都是通过 Shell 来解析和执行的。
Shell命令:Shell 不仅是一个交互工具,更是一种具备编程能力的“命令解释器”。它允许我们将多个命令写入一个脚本文件中,批量执行,从而实现自动化操作。
Shell 命令是 Linux 系统操作的核心工具,比如我们常用的 ls、cd、cp、echo 等命令,都是在 Shell 中运行的指令。
Shell 脚本:是一系列 Shell 命令的集合,保存在一个文本文件中,按照顺序依次执行。 通俗地讲:你在终端一个个敲的命令,可以提前写进一个 .sh 文件中,将其变成一个“可执行的任务集合”,以后只需要运行这个文件,就能一次性执行所有操作,无需手动重复输入。
- Shell 是工具,负责与系统交互
- Shell 命令是操作单位,如
ls,rm,echo(常用的liunx命令很多都是shell命令) - Shell 脚本是程序,可以批量执行命令,实现自动化处理
搜索命令
find / -name 'b' 查询根目录下(包括子目录),名以b的目录和文件;
find / -name 'b*' 查询根目录下(包括子目录),名以b开头的目录和文件;
find . -name 'b' 查询当前目录下(包括子目录),名以b的目录和文件;
重命名
mv 原先目录 文件的名称 mv tomcat001 tomcat
剪切命令(有目录剪切到制定目录下,没有的话剪切为指定目录)
mv /aaa /bbb 将根目录下的aaa目录,移动到bbb目录下(假如没有bbb目录,则重命名为bbb);
mv bbbb usr/bbb 将当前目录下的bbbb目录,移动到usr目录下,并且修改名称为bbb;
mv bbb usr/aaa 将当前目录下的bbbb目录,移动到usr目录下,并且修改名称为aaa;
内存管理命令
- free:显示系统中物理内存(RAM)和交换空间(swap)使用情况的工具。查看当前系统未使用和已使用的内存,以及被内核使用的内存缓冲区。默认以kb为单位。
free -m free -h free free -m -s 5
- df:用于显示文件系统的磁盘空间使用情况.
df -hT df df -mT df -h
-
du:用于显示文件和目录的磁盘使用情况。它可以递归地显示目录下每个文件和子目录的大小,也可以显示单个文件的大小。
du ./* -hsc du ./* -hs du -sh * ### 建议用这个 - mount:用于将文件系统(如硬盘分区、USB驱动器、网络文件系统等)挂载到指定的挂载点,使其可以被访问,一般需要root权限
- dumpsys meminfo:查看进程或包名的内存使用情况
- cat /proc/meminfo:记录系统内存的使用情况
- MemTotal:可用的总内存大小
- MemFree:当前还没有被用到的内存
- MemAvailable:应用程序可用内存大小,系统中有些内存虽然被使用但是可以回收,MemFree不能代表所有可用内存,要加上可回收的部分
。。。还有一些其他命令。。
进程相关命令
- ps:显示当前进程的状态(Process Status)
// 显示所有进程的详细信息 ps -aux // 列出所有的进程,相比 ps -aux 信息要少一些 ps -ef 使用 ps -ef | grep "可执行文件名" 查找进程 -
top:实时显示进程的信息
top [-] [i] [n数字] i:不显示任何闲置 (idle) 或无用 (zombie) 的进程 n:更新的次数,完成后将会退出top 常用说法 // 不显示任何闲置 (idle) 或无用 (zombie) 的进程 top -i // 更数指定次数后,退出top命令 top -n数字 -
pstree:将所有的进程以树型结构的方式进行展示
-
kill:用于结束进程的命令或者用于显示相关信号
网络相关命令
- ifconfig:显示或设置网络设备
- ip:与ifconfig类似
- ping:检测主机
- route:显示和操作ip路由表
- lsof:查看进程打开的文件的工具
- netstat:查看当前操作系统的网络连接状态
再补充把。。。
define和模板的区别
define是在编译程序的预处理阶段纯文本替换,无类型检查,宏出现几次就会被执行替换几次;而template类型参数化是在编译阶段阶段带类型检查,的泛型编程,根据传入的模板实参,实例化出对应类型的具体函数,代码。参数仅在调用时求值一次。
为什么基类有子类基础的时候基类的析构要写成虚函数
主要是为了确保在删除基类指针指向的派生类对象时,派生类的析构函数能够正确调用,避免内存泄漏或未释放的资源。
如果基类的析构函数不是虚函数,当基类指针指向派生类对象,并通过基类指针删除对象时,只会调用基类的析构函数,不会调用派生类的析构函数,导致派生类的资源没有释放,造成内存泄漏。
这不得不谈一下构造析构的调用顺序。
全局变量、静态变量和局部变量
- 全局变量在程序开始时调用构造函数、在程序结束时调用析构函数。
- 静态变量在所在函数第一次被调用时调用构造函数、在程序结束时调用析构函数,只调用一次。
- 局部变量在所在的代码段被执行时调用构造函数,在离开其所在作用域(大括号括起来的区域)时调用析构函数。可以调用任意多次。
每次调用析构函数时总是析构最近被构造的、且没有被析构的对象。也就是:先被构造的对象后被析构,析构顺序恰与构造顺序相反。
- 先调用基类的构造函数
- 再调用子对象类(成员变量)的构造函数
- 最后调用派生类的构造函数
- 调用顺序与派生类构造函数冒号后面给出的初始化列表(Derived(): m1(), m2(), Base1(), Base2())没有任何关系,按照继承的顺序和变量再类里面定义的顺序进行初始化。 先继承Base2,就先构造Base2。先定义m2,就先构造m2。
- 析构函数调用顺序仍然与构造函数构造顺序相反。
这里相反就说明,先从派生类开始析构,再到基类。。所以先从基类指针指向的派生类析构函数调用,如果不是虚函数就无法完全清理掉派生类的资源。!!!
使用父类指针指向子类时,当父类析构函数为非虚函数时,只会调用父类的析构函数,子类的析构函数不会被调用,从而造成内存泄漏。而把父类的析构函数定为虚函数之后,再使用父类执行指向子类并且删除该对象时,会先调用派生类的析构函数,再调用基类的析构函数
析构函数的作用
释放对象占用的资源,如动态分配的内存、打开的文件、数据库连接等。
析构函数和delete的区别
在所有函数之外创建的对象为全局对象,与全局变量类似,位于内存分区的全局数据区,程序在结束时,会调用这些对象的析构函数。
在函数内部创建的对象是局域对象,它与局部变量类似,位于栈区,函数结束时会调用它们的析构函数;
而new创建的对象位于堆区,在通过delete删除时才会调用析构函数;如果没有delete,则析构函数就不会被调用。!!
栈:由编译器管理分配和回收,存放局部变量和函数参数。
堆:由程序员管理,需要手动 new malloc delete free 进行分配和回收,空间较大,但可能会出现内存泄漏和空闲碎片的情况。
全局/静态存储区:分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量和静态变量。
常量存储区:存储常量,一般不允许修改。
代码区:存放程序的二进制代码。
指针和引用的区别,什么时候分别用什么
引用和指针都是C++中用来间接访问内存中对象的机制,
- 引用,是某个变量的别名,在声明时必须被初始化,并且初始化后不能改变引用的目标
- 指针,是一个变量,它存储另一个变量的内存地址,指针可以在运行时被重新赋值,并且可以为空nullptr
| 指针* | 引用& | |
| 声明时不用初始化 | 声明时必须进行初始化 | |
| 需要使用解引用运算符*访问指针所指向的对象 | 引用直接使用对象的名字来访问所引用的对象,无需解引用 | |
| 指针有自己的内存地址 | 引用本身没有自己的内存地址 | |
| 指针可以是nullptr | 引用不能指向nullptr或者无效的对象 | |
| 指针通过解引用修改所指对象的值 | 引用可以修改所引用对象的值,引用本质上就是对该对象的直接访问 |
1.指针是一个实体,而引用是一个别名;在汇编上,引用的底层是以指针的方式实现的,定义一个引用变量,相当于就是定义了一个指针,然后把引用内存的地址写到这个指针里面,当通过引用变量修改它所引用的内存时,它先访问了指针里面的地址,然后在这个地址的内存里面对值进行修改
2.指针可以不初始化,通过赋值可以指向任意同类型的内存;但是引用必须初始化,而且引用一经引用一块内存,再也不能引用其它内存了,即引用不能被改变
3.在进行 sizeof 操作时, sizeof 指针在 32 位系统下永远是 4 个字节,而 sizeof 引用计算的 是它所引用内存的大小
4.引用是内存单元的别名,不是数值的别名。如int &a=10;//error,底层是指向10的地址,引用不能引用不能取地址的数据
5.引用只能使用引用变量所引用的数据,例如b是a的别名,b只能使用a的数据
很多说法。。。。大概意思
来一个手撕。
双向链表
与普通的单向链表不同之处在于,每个节点除了存储数据元素外,还存储着指向前一个节点和后一个节点的指针
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义双向链表中节点的结构
typedef int LDateType;
typedef struct ListNode {
LDateType date;
struct ListNode* prev;
struct ListNode* next;
}LNode;
//注意,双向链表带有哨兵位,插入数据之前必须要初始化哨兵位
/*void LInit(LNode** plist);*///初始化哨兵位
LNode* LInit();//无需传参初始化哨兵位(函数里面创建哨兵位)
//申请节点
LNode* LBuyNode(LDateType x);
//不需要改变哨兵位,则不需要传二级指针
void LPushBack(LNode* pphead, LDateType x);//尾插
void LPushFront(LNode* phead, LDateType x);//头插(在第一个有效节点之前插入)
//打印双向链表
void LPrint(LNode* phead);
//头删
void LPopFront(LNode* phead);
//尾删
void LPopBack(LNode* phead);
//指定位置之后插入数据
void LInsert(LNode* pos, LDateType x);
//删除pos位置的数据
void LErase(LNode* pos);
//查找
LNode* LFind(LNode* phead, LDateType x);
//链表销毁
void LDestroy(LNode** pphead);//这里是用二级指针销毁
//推荐一级 ——>(保持接口一致性)
#include "List.h"
//void LInit(LNode** pphead)
//{
// *pphead = (LNode*)malloc(sizeof(LNode));
// if (*pphead == NULL)
// {
// perror("malloc fail!");
// exit(1);
// }
// (*pphead)->date = -1;//该数据无作用
// (*pphead)->next = (*pphead)->prev = *pphead;//或者NULL
//}
LNode* LInit()
{
//创建一个哨兵位
LNode* phead = LBuyNode(-1);
return phead;
}
//申请节点
LNode* LBuyNode(LDateType x) {
LNode* newnode = (LNode*)malloc(sizeof(LNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(1);
}
newnode->date = x;
newnode->next = newnode->prev = newnode;
return newnode;
}
//链表的打印
void LPrint(LNode* phead) {
assert(phead);
LNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->date);
pcur = pcur->next;
}
printf("\n");
}
//尾插和头插
void LPushBack(LNode* phead, LDateType x)
{
assert(phead);//双向链表第一个节点不可能为空,无需assert*phead
LNode* newnode = LBuyNode(x);
//phead phead->prev(ptail) newnode 修改指针连接
newnode->next = phead;
newnode->prev = phead->prev;
(phead->prev)->next = newnode;
phead->prev = newnode;
}
void LPushFront(LNode* phead, LDateType x)
{
assert(phead);
LNode* newnode = LBuyNode(x);
// phead newnode phead->next 修改这三个节点的连接
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
void LPopBack(LNode* phead) {
assert(phead);
//若哨兵位节点的next指针或prev指针指向的是自己,则链表为空
assert(phead->next != phead);
LNode* del = phead->prev;
LNode* prev = del->prev;
//实现结果一致,无需分情况讨论,
prev->next = phead;
phead->prev = prev;
free(del);
del = NULL;
}
void LPopFront(LNode* phead) {
assert(phead);
assert(phead->next != phead);
LNode* del = phead->next;
LNode* next = del->next;
//改变指针指向,删除指定节点即可
next->prev = phead;
phead->next = next;
free(del);
del = NULL;
}
LNode* LFind(LNode* phead, LDateType x) {
assert(phead);
LNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->date == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
void LInsert(LNode* pos, LDateType x) {
assert(pos);
LNode* newnode = LBuyNode(x);
//pos newnode pos->next
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
void LErase(LNode* pos) {
assert(pos);
//pos->prev pos pos->next
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
void LDestroy(LNode** pphead) {
assert(pphead);
//哨兵位不能为空
assert(*pphead);
LNode* pcur = (*pphead)->next;
while (pcur != *pphead)
{
LNode* next = pcur->next;
free(pcur);
pcur = next;
}
//最后链表只剩哨兵位
//销毁哨兵位
free(*pphead);
*pphead = NULL;
}
测试
#include "List.h"
//void ListTest01() {
// LNode* plist = NULL;
// LInit(&plist);
//}
void ListTest02() {
LNode* plist = LInit();//初始化
//尾插
LPushBack(plist, 1);
LPushBack(plist, 3);
LPushBack(plist, 5);
LPrint(plist);
}
void ListTest03() {
LNode* plist = LInit();//初始化
//尾插
LPushFront(plist, 1);
LPushFront(plist, 3);
LPushFront(plist, 5);
LPrint(plist);
LPopBack(plist);
LPrint(plist);
LPopBack(plist);
LPrint(plist);
LPopBack(plist);
LPrint(plist);
}
void ListTest04()
{
LNode* plist = LInit();//初始化
//尾插
LPushFront(plist, 1);
LPushFront(plist, 3);
LPushFront(plist, 5);
LPrint(plist);
LPopFront(plist);
LPrint(plist);
LPopFront(plist);
LPrint(plist);
LPopFront(plist);
LPrint(plist);
}
void ListTest05()
{
LNode* plist = LInit();//初始化
//尾插
LPushFront(plist, 1);
LPushFront(plist, 3);
LPushFront(plist, 5);
LPrint(plist);
LNode* findRet1 = LFind(plist, 2);
if (findRet1 == NULL) printf("未找到!\n");
else printf("找到了!\n");
LNode* findRet2 = LFind(plist, 3);
if (findRet2 == NULL) printf("未找到!\n");
else printf("找到了!\n");
}
void ListTest06()
{
LNode* plist = LInit();//初始化
//尾插
LPushFront(plist, 1);
LPushFront(plist, 3);
LPushFront(plist, 5);
LPrint(plist);
LNode* findRet1 = LFind(plist, 3);
LInsert(findRet1, 666);
LPrint(plist);
LNode* findRet2 = LFind(plist, 666);
LErase(findRet2);
LPrint(plist);
LDestroy(&plist);
}
int main()
{
//ListTest01();//由传址的哨兵位初始化
//ListTest02();//初始化+尾插
//ListTest03();//头插+尾删
//ListTest04();//头删
//ListTest05();//查找
ListTest06();//在指定位置之后插入数据,删除数据,销毁链表
return 0;
}
后序遍历
递归
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector res;
void traversal(TreeNode* root) {
if (root == nullptr) return ;
traversal(root->left);
traversal(root->right);
res.push_back(root->val);
}
vector postorderTraversal(TreeNode* root) {
traversal(root);
return res;
}
};
迭代
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector postorderTraversal(TreeNode* root) {
vector res;
stack st;
if (root != nullptr) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
res.push_back(node->val);
if (node->left != nullptr) st.push(node->left);
if (node->right != nullptr) st.push(node->right);
//栈先进后出
}
reverse(res.begin(), res.end());
return res;
}
};
层序遍历
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector> levelOrder(TreeNode* root) {
vector> res;
queue que;
if (root != nullptr) que.push(root);
while (!que.empty()) {
vector vec;
int size = que.size();
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
if (node->left != nullptr) que.push(node->left);
if (node->right != nullptr) que.push(node->right);
}
res.push_back(vec);
}
return res;
}
};
C++11中,swap、move和拷贝构造的区别和使用场景
- swap:交换两对象的资源或内容,适用于快速交换大对象或容器
- move:转移对象资源到新对象,原对象置空,适用于返回值、临时对象优化
- 拷贝构造:创建新对象并拷贝原对象内容,适用于需要保留原对象时
swap是互换,move是转移,拷贝构造是复制。选择策略可显著影响性能。
什么是RVO(返回值优化)?结合C++11的move构造说明高效返回对象的方法。
RVO:避免函数返回对象时产生临时拷贝,直接调用栈上构造返回值。
C++11:结合move构造,当RVO不适用时,可将返回值资源迁移,减少深拷贝。
浅拷贝和深拷贝:
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象共享同一块内存。
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不影响旧对象。而浅拷贝会。
static和全局变量的区别
静态全局变量static和全局变量,都是静态存储
全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,全局变量在各个源文件中都有效。在一个文件内定义全局变量,另一个文件中,通过extern全局变量名的声明,就可以使用全局变量。
static修饰的静态全局变量则限制了其作用域,只在定义该变量的源文件内有效,在同一源程序的其他源文件不能使用它
这里还有一个静态局部变量,局部变量只在函数体内有效,静态局部变量在一个文件内有效
.C文件到可执行文件的过程
.C文件即C源程序->编译预处理->编译->汇编程序->链接程序->可执行程序
第一个问题也有编译程序的过程
- 预编译:将头文件编译,进行宏替换,输出.i文件
- 编译:将其转化为汇编语言文件,主要做词法分析,语义分析以及检查错误,检查无误后将代码翻译成编译语言,生成.s文件
- 汇编:汇编器将汇编语言文件翻译成机器语言,生成.o文件(二进制文件)
- 链接:将目标文件和库链接到一起,生成可执行文件
匿名函数如何获取成员对象
匿名函数绝大多数情况下,就是lambda。lambda就是匿名函数一种,lambda是专门快速写匿名函数的语法糖。
lambda:
[捕获列表](参数列表)mutable->返回值类型{
函数体
}
默认情况下,Lambda函数总是一个const函数,mutable可以取消其常量性。可变规范允许lambda表达式的主体修改由值捕获的变量。在使用该关键字时,参数列表不可省略(即使参数为空)。
捕捉列表能够访问或获取上下文中的变量供lambda函数使用。
- 通过引用访问:具有&前缀的变量[&b]
- 通过值访问:无&前缀的变量[a]
空捕获 [ ]:
不从周围捕获变量
在 lambda 表达式中捕获外界变量的默认捕获模式:
- 可以为全部变量默认指定捕获方式,然后为特定变量显式指定模式
- [&]:引用捕获外部所有变量
[=]:捕获外部所有变量
静态成员不需要捕获
捕获当前对象指针[this]
//lambda主体通过引用访问外部变量total,通过值访问外部变量factor
//顺序不同 等效
[&total, factor]
[factor, &total]
//默认引用捕获 显式指定factor为值捕获
[&, factor]
//默认值捕获 显式指定total为引用捕获
[=, &total]
引用捕获和值捕获模式的区别:
- 引用捕获可修改外部变量原始数据,值捕获只能修改副本,不能修改原始数据。
- 引用捕获反映外部变量的更新,值捕获不反映。
- 引用捕获引入了生存期依赖关系,值捕获没有生存期依赖关系。如果在异步lambda中通过引用捕获本地,在lambda运行时,本地数据消失会导致运行时访问冲突。
这里还是有些不清不楚。。。需要回顾更新。
链表是否有环,环的入口
做一个高性能模块,需要用链表,会优先考虑std的list吗,还是有其他考量
做高性能模块,绝大多数情景下,不优先用std::list,甚至尽量别用。std::list性能很差,基本是C++标准库里“性能垫底”的容器之一。
不适合高性能
- 每个节点独立堆分配。每插入一个节点就一次new,缓存不友好,大量小对象极慢
- 内存不连续。CPU缓存命中率低,遍历比vector慢几倍到几十倍
- 双向链表overhead大。每个节点存两个指针prev/next,内存占用高
- 无法随机访问[i]。找第几个元素必须从头遍历
只有一种情况适合
频繁在中间插入、删除,且迭代器不能失效的情况下,但性能也远不及优化过的vector。
所以高性能首选std::vector,当作连续内存链表用
- 内存连续,缓存友好,性能碾压std的list
- 插入删除如果不是巨大量,速度反而更快
- 支持随机访问,能和算法、SIMD配合
如何避免内存泄漏
作为C/C++开发人员,内存泄漏是最容易遇到的问题之一。这是由于C/C++语言的特性,C/C++需要开发者去申请释放内存,要开发者去管理内存,内存使用不当,容易造成段错误segment fault或者内存泄漏memory leak。
性能下降、程序终止、系统崩溃、无法提供服务。
内存泄漏,指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致一系列后果。。很严重。。
我们在程序中对原始指针(raw pointer)使用new操作符或者free函数的时候,实际上是在堆上为其分配内存,这个内存指的是RAM,而不是硬盘等永久存储。持续申请而不释放(或者少量释放)内存的应用程序,最终因内存耗尽导致OOM(out of memory)。
当程序申请了内存,而不进行归还,久而久之,可用内存越来越少,OS就会进行自我保护,杀掉该进程,这就是我们常说的OOM(out of memory)。
内存泄漏分为以下两类:
- 堆内存泄漏:我们经常说的内存泄漏就是堆内存泄漏,在堆上申请了资源,在结束使用的时候,没有释放归还给OS,从而导致该块内存永远不会被再次使用
- 资源泄漏:通常指的是系统资源,比如socket,文件描述符等,因为这些在系统中都是有限制的,如果创建了而不归还,久而久之,就会耗尽资源,导致其他程序不可用

本文主要分析堆内存泄漏,所以后面的内存泄漏均指的是堆内存泄漏。
- 内核空间:供内核使用,存放的是内核代码和数据
- stack:这就是我们经常所说的栈,用来存储自动变量(automatic variable)
- mmap:也成为内存映射,用来在进程虚拟内存地址空间中分配地址空间,创建和物理内存的映射关系
- heap:就是我们常说的堆,动态内存的分配都是在堆上
- bss:包含所有未初始化的全局和静态变量,此段中的所有变量都由0或者空指针初始化,程序加载器在加载程序时为BSS段分配内存
- ds:初始化的数据块
包含显式初始化的全局变量和静态变量
此段的大小由程序源代码中值的大小决定,在运行时不会更改
它具有读写权限,因此可以在运行时更改此段的变量值
该段可进一步分为初始化只读区和初始化读写区- text:也称为文本段
该段包含已编译程序的二进制文件。
该段是一个只读段,用于防止程序被意外修改
该段是可共享的,因此对于文本编辑器等频繁执行的程序,内存中只需要一个副本

避免内存泄漏:
- 避免在堆上分配。在栈上。
众所周知,大部分的内存泄漏都是因为在堆上分配引起的,如果我们不在堆上进行分配,就不会存在内存泄漏了(这不废话嘛),我们可以根据具体的使用场景,如果对象可以在栈上进行分配,就在栈上进行分配,一方面栈的效率远高于堆,另一方面,还能避免内存泄漏,我们何乐而不为呢。 - 手动释放
对于malloc函数分配的内存,在结束使用的时候,使用free函数进行释放
对于new操作符创建的对象,切记使用delete来进行释放
对于new []创建的对象,使用delete[]来进行释放(使用free或者delete均会造成内存泄漏) - 避免使用裸指针
int fun(int *ptr) {//fun是一个接口或lib函数 //函数体 return 0; } int main() {} int a = 1000; int *ptr = &a; // ... fun(ptr); return 0; }使用STL中或者自己实现对象。
在C++中,提供了相对完善且可靠的STL供我们使用,所以能用STL的尽可能的避免使用C中的编程方式,比如:
。使用std::string 替代char *, string类自己会进行内存管理,而且优化的相当不错
。使用std::vector或者std::array来替代传统的数组
。其它适合使用场景的对象
-
智能指针
自C++11开始,STL中引入了智能指针(smart pointer)来动态管理资源。
unique_ptr是限制最严格的一种智能指针,用来替代之前的auto_ptr,独享被管理对象指针所有权。
unique_ptr是独占管理权,而shared_ptr则是共享管理权
weak_ptr的出现,主要是为了解决shared_ptr的循环引用 -
RAll
RAII是Resource Acquisition is Initialization(资源获取即初始化)的缩写,是C++语言的一种管理资源,避免泄漏的用法。
利用的就是C++构造的对象最终会被销毁的原则。利用C++对象生命周期的概念来控制程序的资源,比如内存,文件句柄,网络连接等。
RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命周期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。
简单地说,就是把资源的使用限制在对象的生命周期之中,自动释放。
举个简单的例子,通常在多线程编程的时候,都会用到std::mutex,
std::mutex mutex_;
void fun() {
mutex_.lock();
if (...) {
mutex_.unlock();
return;
}
mutex_.unlock()
}
如果if分支多的话,每个if分支里面都要释放锁,如果一不小心忘记释放,那么就会造成故障,为了解决这个问题,我们使用RAII技术
std::mutex mutex_;
void fun() {
std::lock_guard<std::mutex> guard(mutex_);
if (...) {
return;
}
}
在guard出了fun作用域的时候,会自动调用mutex_.lock()进行释放,避免了很多不必要的问题
这一篇也不错。。!
RAll思想
如上。
RAII是Resource Acquisition is Initialization(资源获取即初始化)。是一种C++中常见的编程范式,主要用于管理资源(如动态内存、文件句柄、网络链接等)。核心思想是将资源的生命周期绑定到对象的生命周期,通过对象的构造函数来获取资源,通过对象的析构函数来释放资源。这种方式避免了显式的资源管理,减少了资源泄漏的可能性。
- 资源获取即初始化:资源的分配与对象的初始化绑定在一起,资源的释放与对象的销毁绑定在一起。
- 自动释放资源:当对象超出作用域scope时,系统自动调用其析构函数,从而释放所占用的资源。这可以避免忘记释放资源或者手动释放资源时出现错误。
RAll典型应用。
- 内存管理:通过智能指针(如 std::unique_ptr 和 std::shared_ptr)自动管理堆内存。
- 文件管理:通过 std::fstream 自动管理文件的打开和关闭。
- 互斥锁管理:通过 std::lock_guard 或 std::unique_lock 自动管理锁的获取和释放。
所以前面说过,智能指针就是一个类,借助的就是RAll思想,资源管理
RAll管理文件
#include <iostream>
#include <fstream>
void readFile(const std::string& fileName) {
// 文件在构造时打开,在析构时关闭
std::ifstream file(fileName);
if (!file.is_open()) {
std::cerr << "Failed to open the file." << std::endl;
return;
}
std::string line;
while (std::getline(file, line)) {
std::cout << line << std::endl;
} // 文件在作用域结束时自动关闭
}
int main() {
readFile("example.txt");
return 0;
}
RAII 管理互斥锁
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void printThreadID(int id) {
// 使用 std::lock_guard 自动管理互斥锁
std::lock_guard<std::mutex> lock(mtx);
std::cout << "Thread " << id << " is running." << std::endl;
}
int main() {
std::thread t1(printThreadID, 1);
std::thread t2(printThreadID, 2);
t1.join();
t2.join();
return 0;
}
new和delete的规范使用
C语言中动态开辟内存的方法是malloc/calloc/realloc;释放动态开辟内存是free
C++中是new和delete
- malloc和free是函数,new和delete是操作符;
-
malloc申请的空间不会初始化,new可以初始化;
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可;
- 申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型;
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。
- 。。。。。区别还有一些
new底层其实就是malloc实现开辟空间,new先调用operator new函数底层malloc,再调用自定义类型的构造函数
- new不用担心动态开辟空间失败,因为失败时会抛出异常;malloc开辟空间失败后返回空指针
delete先调用自定义析构函数,再调用operator delete函数,operator delete底层是free
这里需要再细化。。。。
手搓智能指针(文件管理),(内存释放)
野指针。
指针变量没有被初始化,或者指向的内存已被释放。
内存泄漏。
分配的内存没有被释放,且失去了对这块内存的引用。
悬空指针。
类似于野指针,特指指向已经被释放的内存的指针。
C++的RAII(资源获取即初始化)思想就是答案。其核心是:利用对象的生命周期来管理资源。构造函数中获取资源,析构函数中释放资源。
#include <iostream>
template <typename T>
class SmartPtr {
private:
T* m_ptr; // 底层原始指针
public:
// 构造函数,获取资源
explicit SmartPtr(T* ptr = nullptr) : m_ptr(ptr) {}
// 析构函数,释放资源
~SmartPtr() {
delete m_ptr; // 安全地对nullptr执行delete
std::cout << "Resource freed by SmartPtr." << std::endl;
}
// 禁止拷贝构造和赋值,防止多个SmartPtr管理同一份资源
SmartPtr(const SmartPtr&) = delete;
SmartPtr& operator=(const SmartPtr&) = delete;
// 允许移动构造(进阶功能,体现现代C++思想)
SmartPtr(SmartPtr&& other) noexcept : m_ptr(other.m_ptr) {
other.m_ptr = nullptr;
}
// 允许移动赋值(进阶功能)
SmartPtr& operator=(SmartPtr&& other) noexcept {
if (this != &other) {
delete m_ptr;
m_ptr = other.m_ptr;
other.m_ptr = nullptr;
}
return *this;
}
// 重载操作符,让它用起来像指针
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
explicit operator bool() const { return m_ptr != nullptr; }
};
// 使用示例
int main() {
{
// 在作用域内创建我们的“智能指针”
SmartPtr<int> sp(new int(42));
// 像普通指针一样使用
std::cout << "Value: " << *sp << std::endl;
*sp = 100;
std::cout << "New Value: " << *sp << std::endl;
// 离开这个作用域时,sp的析构函数会自动调用,delete内部指针
// 你不需要手动调用free/delete!
}
std::cout << "Now sp is out of scope." << std::endl;
return 0;
}
是封装一个指针,初始化,单例模式?
看这个把
template<typename T>
class SharedPtr{
public:
SharedPtr(){
count = new int(0);
ptr = new T(0);
}
SharedPtr(T* p){
count = new int(1);
ptr = p;
}
Sharedptr(T& p) {
ptr = new string(p);
count = new int(1);
}
SharedPtr(SharedPtr<T>& other){
count = other.count;
add_count();
ptr = other.ptr;
}
T* operator->(){
return ptr;
}
T& operator*(){
return *ptr;
}
void add_count(){
(*count)++;//测试下区别,去掉括号
}
void sub_count(){
(*count)--;
}
SharedPtr<T>& operator = (SharedPtr<T>& other){
if(this == &other){
return *this;
}
other.add_count();
sub_count();
if(ptr && *count == 0){
delete count;
delete ptr;
cout<<"delete ptr = "<<endl;
}
ptr = other.ptr;
count = other.count;
return *this;
}
~SharedPtr(){
sub_count();
if(ptr && *count == 0){
delete count;
delete ptr;
cout<<"delete ptr ~ "<<endl;
}
}
int getRef(){
return *count;
}
private:
int* count;
T* ptr;
};
还是这样。。。没细看
namespace bit
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否为⾃⼰给⾃⼰赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针⼀样使⽤
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
template<class T>
class unique_ptr
{
public:
explicit unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针⼀样使⽤
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>&sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;
unique_ptr(unique_ptr<T> && sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
unique_ptr<T>& operator=(unique_ptr<T> && sp)
{
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
}
private:
T* _ptr;
};
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1))
{}
template<class D>
shared_ptr(T * ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _del(del)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _del(sp._del)
{
++(*_pcount);
}
void release()
{
if (--(*_pcount) == 0)
{
// 最后⼀个管理的对象,释放资源
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
_del = sp._del;
}
return *this;
}
~shared_ptr()
{
release();
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
//atomic<int>* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
// 需要注意的是我们这⾥实现的shared_ptr和weak_ptr都是以最简洁的⽅式实现的,
// 只能满⾜基本的功能,这⾥的weak_ptr lock等功能是⽆法实现的,想要实现就要
// 把shared_ptr和weak_ptr⼀起改了,把引⽤计数拿出来放到⼀个单独类型,shared_ptr
// 和weak_ptr都要存储指向这个类的对象才能实现,有兴趣可以去翻翻源代码
template<class T>
class weak_ptr
{
public:
weak_ptr()
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
private:
T* _ptr = nullptr;
};
}
int main()
{
bit::auto_ptr<Date> ap1(new Date);
// 拷⻉时,管理权限转移,被拷⻉对象ap1悬空
bit::auto_ptr<Date> ap2(ap1);
// 空指针访问,ap1对象已经悬空
//ap1->_year++;
bit::unique_ptr<Date> up1(new Date);
// 不⽀持拷⻉
//unique_ptr<Date> up2(up1);
// ⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎
bit::unique_ptr<Date> up3(move(up1));
bit::shared_ptr<Date> sp1(new Date);
// ⽀持拷⻉
bit::shared_ptr<Date> sp2(sp1);
bit::shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
return 0;
}
推荐这个。。后面细看,还不会手搓。。。。。
虚函数是什么,如何实现虚函数?
虚函数(Virtual Function)是在面向对象编程中用于实现动态多态性的一种机制。通过将基类中的成员函数声明为虚函数,可以在派生类中重写(Override)这些函数,从而根据对象的实际类型确定调用的函数版本。通过以下的代码块,参考。。
具体实现:
- 在基类中使用virtual关键字声明虚函数:
class Base { public: virtual void func() { ... } };派生类重写基类的虚函数
class Derived extends Base { public: virtual void func() { ... } };- 通过基类的指针或引用调用虚函数,会执行实际所指派生类的实现
Base* b = new Derived(); b->func(); // Calls Derived::func()重写基类的虚函数时,可以使用override关键字检查是否正确重写,如果Derived::func()不是正确重写Base::func(),编译器会报错。
class Derived extends Base { public: virtual void func() override { ... } };派生类可以选择不重写基类的虚函数,这时调用基类
class Derived extends Base { }; Base* b = new Derived(); b->func(); // Still calls Base::func()基类可以声明纯虚函数,这要求所有派生类都重写该虚函数, 如果派生类为重写,则也变成抽象类。
class Base { public: virtual void func() = 0; // Pure virtual function };extends 是 Java / C# 的继承关键字,C++ 根本不认识 extends。。。。大概是怎么个意思
下面才对。。。
- 首先定义基类(父类):在基类中生命虚函数,并将其标记为虚函数,使用关键字virtual。
class Base{ public: virtual void func(); }; -
接下来是定义派生类(子类):在派生类中重写基类的虚函数,并保持函数签名(返回类型、参数列表)一致。
class Derived : public Base{ public: void func() override; }; -
虚函数表:编译器会将每个包含虚函数的类生成一个虚函数表,其中存储了指向各个虚函数的函数指针。每个对象都有一个隐藏的指针指向该虚函数表。
-
调用虚函数:当通过基类指针或引用调用虚函数时,会根据对象的实际类型选择正确的虚函数进行调用。通过虚函数表中的函数指针,确定要调用的函数,并在运行时动态绑定到正确的函数。
#include <iostream> class Base { public: virtual void func() { std::cout << "Base::func()" << std::endl; } }; class Derived : public Base { public: void func() override { std::cout << "Derived::func()" << std::endl; } }; int main(){ Base* tr = new Derived(); ptr->fuinc(); delete ptr; return 0; }
如何用栈实现一个队列
用两个栈来实现队列,一个用于输入,一个用于输出
因为栈遵循LIFO原则(先进后出),输入的数据会被反转顺序。所以用两个栈,就可以将数据的顺序反转过来,从而实现队列的FIFO原则(先进先出)

队列,队头Front,队尾Rear
定义栈结构,包含数据的数组,栈顶指针以及栈的容量
typedef struct Stack {
int* data; // 存储数据的数组
int top; // 栈顶指针
int capacity; // 栈的容量
}Stack;
定义队列结构,包含输入栈和输出栈
typedef struct {
Stack* in_stack;
Stack* out_stack;
}MyQueue;
创建栈并初始化
Stack* createStack(int capacity) {
struct Stack* stack = (Stack*)malloc(sizeof(Stack));
if (stack == NULL) {
return NULL;
}
stack->data = (int*)malloc(sizeof(int) * capacity);
if (stack->data == NULL) {
free(stack);
return NULL;
}
stack->top = -1;
stack->capacity = capacity;
return stack;
}
检查栈是否为空。指向栈的指针为空或者栈的栈顶指针等于-1(初始为-1)时,栈为空
bool isStackEmpty(Stack* stack) {
return (stack == NULL || stack->top == -1);
}
检查栈是否已满。当栈的栈顶指针指向栈的数据数组的最后一位,说明栈已满
bool isFullStack(Stack* stack) {
return stack->top == stack->capacity - 1;
}
入栈操作。先判断栈是否已满,如果栈已满,就无法再入栈
bool pushStack(Stack* stack, int val) {
if (isStackFull(stack)) {
return false;
}
stack->data[++stack->top] = val;
return true;
}
元素出栈。先判断栈是否为空,为空则无法出栈,反之将元素加入到栈的数据数组中,栈顶指针向前移动一位
bool popStack(Stack* stack, int* value) {
if (isStackEmpty(stack)) {
return false;
}
*value = stack->data[stack->top];
stack->top--;
return true;
}
查看栈顶元素
// 查看栈顶元素
bool peekstack(Stack* stack, int* value) {
if (isStackEmpty(stack)) {
return false;
}
*value = stack->data[stack->top];
return true;
}
释放栈内存。先释放栈中数组的内存,再释放栈结构的内存
void clearStack(Stack* stack) {
if (stack == NULL) {
return;
}
free(stack->data);
free(stack);
}
创建队列并初始化。包含两个栈,一个用于输入,一个用于输出
// 创建队列并初始化
MyQueue* CreatemyQueue(int capacity) {
// 分配队列结构内存
MyQueue* queue = (MyQueue*)malloc(sizeof(MyQueue));
if (queue == NULL) {
return NULL;
}
// 创建输入栈(用于入队操作)
queue->in_stack = createStack(capacity);
// 创建输出栈(用于出队操作)
queue->out_stack = createStack(capacity);
return queue;
}
判断队列是否为空。判断输出栈为空且输入栈也为空. 如果输出栈为空但输入栈不为空时,可以将输入栈中的元素压入输出栈中
// 检查队列是否为空
// 输出栈和输入栈都为空
bool isMyQueueEmpty(MyQueue* queue) {
return isStackEmpty(queue->in_stack) && isStackEmpty(queue->out_stack);
}
判断队列是否已满。这个要判断的标准是输入栈是已满且输出栈不为空 ,因为如果输入栈已满但输出栈不为空时是不能将输入栈中的元素压入输出栈中的,会打乱数据的顺序,不符合LIFO原则
// 检查队列是否已满
// 输入栈已满并且输出站不为空 (输入站已满即使输出站又空位也不能插入元素,会打乱元素顺序)
bool isMyQueueFull(MyQueue* queue) {
return isStackFull(queue->in_stack) && !(isStackEmpty(queue->out_stack));
}
入队操作
bool PushmyQueue(MyQueue* queue, int val) {
// 如果输入栈满了,检查是否可以转移元素
if (isStackFull(queue->in_stack)) {
// 只有输出栈为空时才能转移所有元素
// 如果输出栈不为空但是又空位,是不能将输入栈的元素转移到输出栈中的,会带乱数据的顺序,队列遵循FIFO,栈遵循LIFO
if (isStackEmpty(queue->out_stack)) {
// 转移所有输入栈元素到输出栈
while (!isStackEmpty(queue->in_stack)) {
int temp;
if (popStack(queue->in_stack, &temp)) {
pushStack(queue->out_stack, temp);
}
}
// 现在输入栈空了,可以入队
return pushStack(queue->in_stack, val);
} else {
// 输出栈不为空,无法转移,队列满
return false;
}
}
// 输入栈未满,直接入队
return pushStack(queue->in_stack, val);
}
出队操作
bool PopmyQueue(MyQueue* queue) {
// 检查输出栈是否为空
int value;
if (isStackEmpty(queue->out_stack)) {
// 如果输入栈也为空,就直接返回
if (isStackEmpty(queue->in_stack)) {
return false;
} else {
// 输出栈为空,检查输入栈,如果输入栈不为空,将输入栈中的元素全部转移到输出栈
while (!isStackEmpty(queue->in_stack)) {
if (popStack(queue->in_stack, &value)) {
pushStack(queue->out_stack, value);
}
}
}
}
// 输出栈不为空,直接出栈
popStack(queue->out_stack, &value);
return true;
}
查看队首元素。先判断队列是否为空 队列不为空,就在判断输出栈是否为空,不为空就直接将输出栈的栈顶元素传给要接受队首元素的指针 如果输出栈为空,就将输入站中的元素全部压入输出栈中,(注意是全部,否则会打乱数据的顺序)
bool PeekMyQueue(MyQueue* queue, int* value) {
if (isMyQueueEmpty(queue)){
return false;
}
if (isStackEmpty(queue->out_stack)) {
while (!isStackEmpty(queue->in_stack)) {
popStack(queue->in_stack, value);
pushStack(queue->out_stack, *value);
}
}
*value = queue->out_stack->data[queue->out_stack->top];
printf("队首元素是 %d ", * value);
return true;
}
最后释放队列内存 先释放队列中两个栈的内存,再释放队列结构的内存
void FreeMyQueue(MyQueue* queue) {
freeStack(queue->in_stack);
freeStack(queue->out_stack);
free(queue);
}
TCP的流量控制,拥塞控制,区别
拥塞控制和流量控制虽然都是TCP原来调节数据传输速率的机制,但他们解决的问题和关注的焦点是不同的。
目的不同。
流量控制主要是为了解决通信双方处理能力不匹配的问题,防止发送方发送数据太快导致接收方缓冲区溢出;而拥塞控制则为了解决网络拥塞问题,避免过多数据包涌入网络造成网络崩溃。
控制对象不同。
流量控制是端到端的,只关注发送方和接收方两个端点;而拥塞控制关注整个网络的状态,包括中间的路由器等设备。
实现机制不同。
流量控制主要通过滑动窗口协议实现,接收方在ACK中告诉对方自己还能接收多少数据;拥塞控制主要通过拥塞窗口、慢启动、拥塞避免、快重传和快恢复等算法实现,主要是发送方根据网络状况自行调整。
触发条件不同。
流量控制由接收方的处理能力决定,当接收方缓冲区快满时就会减小窗口;而拥塞控制主要由网络状况决定,当检测到丢包或超时等现象时,判断为网络拥塞,就会相应的调整发送速率。
TCP实际应用使用这两种机制,取两者较小值作为实际发送窗口。如即使接收方有足够缓冲区,但如果网络拥塞,发送方也会降低发送速率;反之网络良好,但接收方处理不来,发送方也会限制发送速度。
多路复用(epoll/IO复用),Reactor/Proactor模型,多线程服务器架构
IO,即操作系统的网络IO操作,一次完整的网络IO分为两个不可拆分的核心阶段:
- 数据准备阶段:内核等待网卡接收网络数据,将数据写入内核缓冲区
- 数据拷贝阶段:内核将缓冲区的数据,拷贝到用户进程的内存空间
所有IO模型的差异,本质就是这两个阶段的阻塞/非阻塞、调度方式不同
IO多路复用,是一种经典的同步IO模型,也是高并发网络编程的核心技术。是一种工作机制,
核心用单个线程/进程同时监听成百上千个文件描述符fd、socket、文件句柄。。他让程序监视多个文件描述符(通常是套接字),等待其中一个或多个文件描述符变为就绪状态。一旦某个fd就绪可读可写异常,就立即通知用户程序执行对应的IO操作。
解决传统阻塞IO模型,一个线程只能处理一个fd,高并发场景下会创建海量线程,导致CPU上下文切换开销爆炸、系统性能急剧下降。
Linux平台下,IO多路复用有三种经典实现:(也是多路转接技术,放快递站?
- select
- poll
- epoll,性能最强、应用最广
select是最古老的IO多路复用API,跨平台支持,监视一组文件描述符。几乎支持所有类型的Unix系统(包括Linux、Mac等)。基本工作原理是用户态进程将一组文件描述符传递给内核,由内核来检查这些文件描述符的状态变化;当调用返回时,会告诉用户哪些文件描述符已经准备好了读写操作。性能随文件描述符增多下降。
poll比select更现代,支持更多文件描述符,效率高于select,跨平台
epoll不跨平台,Linux特有。Linux下最高效的多路复用机制,事件驱动,仅返回就绪描述符
kqueue,类似于epoll,适用于BSD和macOS,高效的事件通知机制
阻塞IO
需要结合多进程/多线程,每个进程/线程处理一路IO

客户端越多,需要创建的进程/线程越多,相对占用内存 资源较多
非阻塞IO
单进程可以处理,但是需要不断检测客户端是否发出IO请求,需要不断占用cpu,消耗 cpu 资源

多路复用IO
本质上就是通过复用一个进程来处理多个IO请求
由内核来监控多个文件描述符是否可以进行I/O操作,如果有就绪的文件描述符,将结果告知给用户进程,则用户进程在进行相应的I/O操作
阻塞IO等数据死等
非阻塞IO不等数据,没数据就走,要反复问
多路复用内核帮着盯着一堆IO,哪个好了通知你,不用你挨个问
reactor和proactor模型是高性能网络编程中两大核心事件驱动架构设计模式,目标解决高并发场景下IO处理效率问题。
reactor基于同步IO模型长搭配IO多路复用epoll/select/poll实现;proactor基于操作系统原生异步IO(AIO)模型实现。
同步和异步
同步:调用方发起请求,必须主动等待、主动轮询、主动完成核心操作,最终自己获取结果,整个流程在调用方的控制流里顺序执行
异步:调用方发起请求后,立即返回,无需等待;核心操作完全交给被调用方/系统内核执行,执行完成后,被调用方会通过回调、事件通知等方式,主动把结果推送给调用方
详细的还需补充。。先空着,回来补充。。
线程之间同步和异步
同步和异步是描述两个或多个事件、操作或进程之间的关系。
同步意味着事件、操作或进程是有序的,一个操作必须在另一个操作完成之后开始执行。
异步则意味着事件、操作或进程是独立的,可以在不等待其他操作完成的情况下开始执行。
介绍一下Websocket
Websocket是一种协议,用于Web应用程序和服务器之间建立实时、双向的通信连接。它通过一个单一的TCP连接提供了持久化连接,这使得Web应用程序可以更加实时的传递数据。
WebSocket 是基于 TCP 的一种新的应用层网络协议。它提供了一个全双工的通道,允许服务器和客户端之间实时双向通信。因此,在 WebSocket 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,客户端和服务器之间的数据交换变得更加简单。
为什么需要 WebSocket?
我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处? 因为 HTTP 协议有一个缺陷:通信只能由客户端发起,不具备服务器推送能力。 举例来说,我们想了解查询今天的实时数据,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。 这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用’轮询’:每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。 在 WebSocket 协议出现以前,创建一个和服务端进双通道通信的 web 应用,需要依赖HTTP协议,进行不停的轮询,这会导致一些问题: 服务端被迫维持来自每个客户端的大量不同的连接 大量的轮询请求会造成高开销,比如会带上多余的header,造成了无用的数据传输。 http协议本身是没有持久通信能力的,但是我们在实际的应用中,是很需要这种能力的,所以,为了解决这些问题,WebSocket协议由此而生,
主要是通过与HTTP对比
连接方式
Http协议是基于请求-响应模式的,每次通信都需要客户端发起一个请求,服务器返回一个响应后断开连接。
而Websocket协议是一种持久连接的协议,客户端和服务器之间建立一次连接后可以持续通信,双方都可以随时发送和接收数据。
数据格式
Http协议传输的数据一般采用明文的ASCII文本格式,通常是HTML、JSON、XML等格式的数据。
而Websocket协议可以传任意格式的数据,包括二进制数据,可以灵活处理各种类型的数据。
通信效率
Http每次通信都需要经过完整的请求-响应过程,包括建立连接、发送请求、服务器处理请求、发送响应等步骤,因此在频繁通信的场景下,Http的开销较大。
而Websocket协议在建立连接后只需要发送少量的头部信息,然后就可以直接进行数据传输,通信效率高。
服务器推送
Http是一种客户端主动发起请求的协议,服务器只能在收到请求后才能返回响应。
而Websocket协议支持服务器主动推送数据给客户端,服务器可以随时向客户端发送数据,实现实时通信。
缺点
- 不支持无连接: WebSocket 是一种持久化的协议,这意味着连接不会在一次请求之后立即断开。这是有利的,因为它消除了建立连接的开销,但是也可能导致一些资源泄漏的问题。
- 不支持广泛: WebSocket 是 HTML5 中的一种标准协议,虽然现代浏览器都支持,但是一些旧的浏览器可能不支持 WebSocket。
- 需要特殊的服务器支持: WebSocket 需要服务端支持,只有特定的服务器才能够实现 WebSocket 协议。这可能会增加系统的复杂性和部署的难度。
- 数据流不兼容: WebSocket 的数据流格式与 HTTP 不同,这意味着在不同的网络环境下,WebSocket 的表现可能会有所不同。
tcpudp编程,多线程编程。。如何写出。。(补充
以下是精简版,需要补充,STL高频考点
vector的底层实现、扩容机制,为什么扩容是 2 倍(或 1.5 倍)?
vector是动态数组,底层是一段连续的内存空间,包含三个指针:_M_start(指向数组起始位置)、_M_finish(指向已使用元素的末尾)、_M_end_of_storage(指向整个数组容量的末尾)。
当_M_finish == _M_end_of_storage时,会触发扩容。步骤:
- 申请一块更大的连续内存(通常是原容量的 2 倍或 1.5 倍)
- 将原数组中的元素拷贝到新内存中
- 释放原内存
- 更新三个指针指向新内存
- 2 倍扩容:GCC 标准库采用,保证每次扩容后可以容纳之前所有释放的内存块,减少内存碎片
- 1.5 倍扩容:VS 标准库采用,避免 2 倍扩容导致的内存浪费,同时保证扩容次数不会过多
- (地图引擎应用:地图数据通常是大量连续的点、线、面数据,
vector是存储这些数据的首选容器,了解其扩容机制可以避免频繁扩容导致的性能问题)
emplace_back()和push_back()的本质区别,什么时候必须用emplace_back()?
本质区别:
push_back():接收一个已构造好的对象,然后将其拷贝(或移动)到容器末尾emplace_back():直接在容器末尾的内存位置原地构造对象,避免了拷贝或移动操作
必须用
emplace_back()的场景:
- 对象不可拷贝且不可移动(如
std::mutex)- 构造函数参数较多,使用
emplace_back()可以直接传递构造参数,代码更简洁- 追求极致性能,避免不必要的拷贝开销
vector、list、deque的底层结构、优缺点及适用场景对比
| 容器 | 底层结构 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
vector |
连续数组 | 随机访问 O (1),尾插尾删 O (1),内存连续缓存友好 | 中间插入删除 O (n),扩容有开销 | 频繁随机访问,尾插尾删多 |
list |
双向链表 | 任意位置插入删除 O (1),不会扩容 | 随机访问 O (n),内存不连续缓存差,额外指针开销 | 频繁中间插入删除,不需要随机访问 |
deque |
分段连续数组 | 头尾插删 O (1),随机访问 O (1)(比 vector 慢) | 中间插入删除 O (n),内存管理复杂 | 头尾操作多,需要随机访问 |
map和unordered_map的底层实现、时间复杂度、优缺点及适用场景
| 容器 | 底层实现 | 时间复杂度 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
map |
红黑树 | 插入 / 删除 / 查找 O (logn) | 有序,稳定性高 | 查找速度比 unordered_map 慢 | 需要有序遍历,对稳定性要求高 |
unordered_map |
哈希表 | 平均 O (1),最坏 O (n) | 查找速度极快 | 无序,哈希冲突有开销 | 频繁查找,不需要有序 |
set和unordered_set的区别,如何实现自定义类型的排序 / 哈希
- 区别:与
map和unordered_map的区别类似,set是有序集合,unordered_set是无序集合 - 自定义类型排序(
set):- 重载
<运算符 - 提供自定义比较函数
- 重载
- 自定义类型哈希(
unordered_set):- 提供
std::hash的特化版本 - 提供自定义哈希函数
- 提供
STL 迭代器失效的所有情况及解决方法(重点vector和map)
vector迭代器失效:- 扩容操作:所有迭代器失效
- 中间插入 / 删除:插入 / 删除位置之后的所有迭代器失效
- 解决方法:使用
insert()/erase()的返回值更新迭代器
map迭代器失效:- 只有被删除元素的迭代器失效,其他迭代器仍然有效
- 解决方法:删除前保存下一个迭代器
- 通用解决方法:尽量使用范围 for 循环,避免手动操作迭代器
sort算法的底层实现
STL 的sort是混合排序算法:
- 数据量较大时:使用快速排序
- 数据量较小时(通常小于 16):使用插入排序
- 递归深度过深时:使用堆排序
queue和stack的底层默认容器为什么是deque而不是vector?
queue和stack只需要头尾操作,deque的头尾插删都是 O (1)vector的头插是 O (n),不适合作为queue的底层容器deque不需要像vector那样进行整体扩容,性能更稳定
priority_queue的底层实现,如何实现大顶堆和小顶堆
- 底层实现:默认是
vector作为底层容器,使用堆算法实现 - 大顶堆:默认实现,使用
less<T>比较函数 - 小顶堆:使用
greater<T>比较函数 - 示例:
priority_queue<int, vector<int>, greater<int>> pq;
Boost 库是一套免费、开源、跨平台的 C++ 扩展库集合,被称为 “准标准库”,用来补充和扩展 C++ 标准库(STL),很多组件后来直接进入了 C++11/14/17/20 标准。
- 跨平台:Windows/Linux/macOS 全支持,兼容 GCC/Clang/MSVC。
- 高质量:工业级稳定,经严格评审与测试,大厂(Google/Adobe)在用。
- 头文件为主:多数库只需
#include即可用,少数(如asio、regex)需链接。 - 填补 STL 空白:提供 STL 没有的高频能力(智能指针、线程、网络、几何计算)。
地图引擎开发最常用的 Boost 组件(必背)
1. 智能指针(内存管理)
boost::shared_ptr:引用计数智能指针,解决内存泄漏,进化为std::shared_ptr。
boost::weak_ptr:弱引用,解决shared_ptr循环引用(如道路↔交叉口互指)。作用:地图要素(点 / 线 / 面)复杂关系的安全内存管理。
2.
boost::geometry(地图核心,必考)
功能:几何计算专用库,处理点 / 线 / 面 / 多边形,支持空间运算。
常用能力:
几何表示:
point2d、linestring、polygon空间计算:距离、面积、长度、相交、包含、缓冲区
坐标转换:WGS84/UTM 等坐标系互转
作用:高精地图核心算法依赖(空间查询、拓扑校验、坐标转换)。
3.
boost::asio(网络通信)
功能:跨平台异步 IO 库,支持 TCP/UDP/ 串口,事件驱动模型。
作用:地图数据网络下载 / 实时更新、引擎与后端通信。
4.
boost::thread(多线程)
功能:C++11 前标准线程库,支持线程、互斥锁、条件变量。
作用:地图数据并行加载 / 解析、后台渲染、异步任务处理。
5.
boost::serialization(数据序列化)
功能:对象序列化 / 反序列化,支持二进制 / 文本格式。
作用:地图数据持久化存储、进程间 / 网络数据传输。
6.
boost::program_options(命令行解析)
功能:简洁解析命令行参数(如输入文件、输出路径、日志级别)。
作用:地图工具(格式转换 / 校验)命令行接口开发。
Boost 与 C++ 标准库的关系(高频对比)
前身与试验场:Boost 先出,成熟后进入标准(
shared_ptr→C++11,filesystem→C++17)。互补不替代:标准库够用时用标准,不够时用 Boost(如几何、网络、多线程高级特性)。
兼容共存:可同时用,接口风格一致,迁移成本低。
boost::shared_ptr和std::shared_ptr的区别,weak_ptr的作用及解决的问题
- 区别:
boost::shared_ptr是 Boost 库实现的,在 C++11 之前就存在std::shared_ptr是 C++11 标准引入的,基于 Boost 的实现- 两者功能基本一致,
std::shared_ptr增加了一些新特性(如make_shared)
weak_ptr的作用:- 解决
shared_ptr的循环引用问题 - 可以观察
shared_ptr的引用计数,但不会增加引用计数 - 使用
lock()方法可以获取一个有效的shared_ptr
- 解决
boost::bind和boost::function的使用,与 C++11 std::bind/std::function的对比
boost::function:是一个函数包装器,可以存储任意可调用对象(函数、函数指针、成员函数、lambda 等)boost::bind:是一个函数绑定器,可以将函数的部分参数绑定,生成一个新的可调用对象- 与 C++11 标准的对比:
- C++11 的
std::function和std::bind是基于 Boost 的实现,功能基本一致 - C++11 的 lambda 表达式在很多场景下可以替代
bind,代码更简洁
- C++11 的
boost::thread库的使用,与 C++11 线程库的区别
boost::thread:Boost 库实现的线程库,在 C++11 之前就存在- 与 C++11 线程库的区别:
- C++11 的
std::thread是基于 Boost 的实现,功能基本一致 - Boost 线程库提供了更多的功能(如线程组、中断等)
- C++11 的
boost::geometry库(地图核心):如何表示点、线、面等几何要素,常用的空间计算
- 几何要素表示:
- 点:
boost::geometry::model::point<double, 2, boost::geometry::cs::cartesian> - 线:
boost::geometry::model::linestring<Point> - 面:
boost::geometry::model::polygon<Point>
- 点:
- 常用空间计算:
- 距离:
boost::geometry::distance(p1, p2) - 面积:
boost::geometry::area(polygon) - 相交:
boost::geometry::intersects(l1, l2) - 包含:
boost::geometry::within(p, polygon) - 缓冲区:
boost::geometry::buffer(geom, buffer, distance)
- 距离:
boost::asio库:异步 IO 模型,如何实现地图数据的网络传输
- 异步 IO 模型:基于事件驱动,当 IO 操作完成时会调用回调函数
- 实现地图数据网络传输:
- 创建
io_context对象 - 创建
socket对象 - 调用
async_read_some或async_write_some进行异步读写 - 调用
io_context.run()运行事件循环
- 创建
以上是boost库,C++扩展库。以下是C++ 面向对象与现代特性
多态的实现原理(虚函数表、虚指针),纯虚函数的作用
- 实现原理:
- 当一个类包含虚函数时,编译器会为该类生成一个虚函数表(vtable),存储虚函数的地址
- 每个对象会包含一个虚指针(vptr),指向该类的虚函数表
- 调用虚函数时,通过对象的虚指针找到虚函数表,再找到对应的函数地址进行调用
- 纯虚函数的作用:
- 声明一个接口,让派生类实现
- 使类成为抽象类,不能实例化
构造函数和析构函数的调用顺序,为什么析构函数要声明为虚函数
- 调用顺序:
- 构造函数:先调用基类构造函数,再调用派生类构造函数
- 析构函数:先调用派生类析构函数,再调用基类析构函数
- 为什么析构函数要声明为虚函数:
- 当用基类指针指向派生类对象时,如果析构函数不是虚函数,删除基类指针只会调用基类的析构函数,不会调用派生类的析构函数,导致内存泄漏
拷贝构造函数和赋值运算符重载的区别,深拷贝与浅拷贝
- 区别:
- 拷贝构造函数:创建一个新对象,用已有对象初始化新对象
- 赋值运算符重载:将已有对象的值赋给另一个已有对象
- 浅拷贝:只拷贝对象的成员变量的值,对于指针成员,只拷贝指针地址,不拷贝指针指向的内容
- 深拷贝:不仅拷贝对象的成员变量的值,还拷贝指针指向的内容
C++11/14/17/20 的核心新特性
- C++11:
auto、range-based for、lambda、智能指针、移动语义、右值引用、constexpr、nullptr、线程库 - C++14:泛型 lambda、
std::make_unique、变量模板 - C++17:结构化绑定、
if constexpr、std::optional、std::variant、文件系统库 - C++20:概念(concepts)、协程、范围(ranges)、
std::format
移动语义的原理,std::move和std::forward的作用
- 移动语义:将一个对象的资源(如动态分配的内存)转移给另一个对象,避免不必要的拷贝
std::move:将一个左值转换为右值引用,触发移动构造函数或移动赋值运算符std::forward:完美转发,保持参数的左值 / 右值属性不变
const关键字的所有用法
- 修饰变量:表示变量不可修改
- 修饰指针:
const int* p:指向 const 的指针,指针指向的内容不可修改int* const p:const 指针,指针本身不可修改const int* const p:指向 const 的 const 指针
- 修饰函数参数:表示参数在函数内部不可修改
- 修饰成员函数:表示该成员函数不会修改对象的任何成员变量
- 修饰函数返回值:表示返回值不可修改
static关键字的所有用法
- 全局静态变量:作用域限制在当前文件,生命周期为整个程序运行期间
- 局部静态变量:作用域限制在函数内部,生命周期为整个程序运行期间
- 类静态成员变量:所有类对象共享一份,在类外初始化
- 类静态成员函数:只能访问类静态成员变量,不需要对象即可调用
异常处理机制,try-catch-throw的执行流程,RAII 思想
- 执行流程:
- 在
try块中执行代码 - 如果发生异常,抛出异常对象
- 匹配
catch块,执行对应的异常处理代码 - 如果没有匹配的
catch块,程序终止
- 在
- RAII 思想:资源获取即初始化,利用对象的生命周期来管理资源(如内存、文件句柄、锁等)
内存管理:堆和栈的区别,new/delete和malloc/free的区别
| 特性 | 栈 | 堆 |
|---|---|---|
| 分配方式 | 自动分配 | 手动分配 |
| 分配速度 | 快 | 慢 |
| 空间大小 | 有限 | 较大 |
| 内存碎片 | 无 | 有 |
| 生长方向 | 向下 | 向上 |
new/delete和malloc/free的区别:
new会调用构造函数,delete会调用析构函数new是运算符,malloc是函数new返回类型安全的指针,malloc返回void*new失败会抛出异常,malloc失败会返回NULL
静态库和动态库的区别,如何制作和链接静态库 / 动态库
| 特性 | 静态库 | 动态库 |
|---|---|---|
| 链接时机 | 编译时链接 | 运行时链接 |
| 可执行文件大小 | 大 | 小 |
| 更新难度 | 难(需要重新编译) | 易(只需要替换库文件) |
| 内存占用 | 每个程序一份 | 多个程序共享一份 |
-
- 制作静态库:
g++ -c lib.cpp -o lib.o ar rcs libmylib.a lib.o - 链接静态库:
g++ main.cpp -L. -lmylib -o main - 制作动态库:
g++ -fPIC -shared lib.cpp -o libmylib.so - 链接动态库:
g++ main.cpp -L. -lmylib -o main
- 制作静态库:
ELF 文件格式的组成,各段的作用
- ELF 头:包含文件的基本信息(如类型、目标架构、入口地址等)
- 程序头表:描述可执行文件的段信息,用于加载程序到内存
- 节头表:描述文件的节信息,用于链接和调试
- 常用节:
.text:代码段,存储可执行指令.data:数据段,存储已初始化的全局变量和静态变量.bss:未初始化数据段,存储未初始化的全局变量和静态变量.rodata:只读数据段,存储常量.symtab:符号表,存储函数和变量的符号信息.strtab:字符串表,存储符号名称
GDB 常用调试命令
- 断点:
b 文件名:行号、b 函数名、info b、d 断点号 - 运行:
r、c、n(单步跳过)、s(单步进入)、finish(跳出函数) - 查看变量:
p 变量名、x 内存地址、display 变量名 - 栈回溯:
bt、f 帧号 - core 文件调试:
gdb 可执行文件 core文件 - 多线程调试:
info threads、thread 线程号
Makefile 的基本语法,如何编写一个通用的 Makefile
- 基本语法:
makefile
目标: 依赖 命令 - 通用 Makefile 示例:
makefile
CC=g++ CFLAGS=-Wall -g SRCS=$(wildcard *.cpp) OBJS=$(SRCS:.cpp=.o) TARGET=main all: $(TARGET) $(TARGET): $(OBJS) $(CC) $(CFLAGS) $^ -o $@ %.o: %.cpp $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f $(OBJS) $(TARGET)
CMake 的基本语法,如何编写 CMakeLists.txt
- 基本语法:
cmake
cmake_minimum_required(VERSION 3.10) project(MyProject) set(CMAKE_CXX_STANDARD 11) add_executable(main main.cpp lib.cpp) - 常用命令:
add_executable:添加可执行文件add_library:添加库文件target_link_libraries:链接库文件include_directories:添加头文件目录link_directories:添加库文件目录
Linux 常用命令
- 文件操作:
ls、cd、pwd、mkdir、rm、cp、mv、cat、more、less - 进程管理:
ps、top、kill、pkill - 网络操作:
ifconfig、netstat、ping、curl、wget - 文本处理:
grep、sed、awk、sort、uniq
多线程编程:C++11 std::thread的使用,同步机制
std::thread的使用:#include <thread> void func(int a, int b) { // 线程函数 } int main() { std::thread t(func, 1, 2); t.join(); // 等待线程结束 return 0; }- 同步机制:
- 互斥锁:
std::mutex、std::lock_guard、std::unique_lock - 条件变量:
std::condition_variable - 信号量:C++20 引入
std::semaphore - 读写锁:
std::shared_mutex
- 互斥锁:
进程间通信的方式及各自的优缺点
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 管道 | 简单易用 | 半双工,只能在有亲缘关系的进程间使用 | 父子进程间通信 |
| 消息队列 | 全双工,可在无亲缘关系的进程间使用 | 有大小限制 | 少量数据传输 |
| 共享内存 | 速度最快 | 需要同步机制 | 大量数据传输 |
| 信号 | 简单 | 只能传递信号值 | 事件通知 |
| 套接字 | 可跨主机通信 | 速度较慢 | 网络通信 |
网络编程:Socket 编程的基本流程,TCP 和 UDP 的区别
- TCP Socket 编程流程:
- 服务器:
socket()->bind()->listen()->accept()->recv()/send()->close() - 客户端:
socket()->connect()->send()/recv()->close()
- 服务器:
- TCP 和 UDP 的区别:
特性 TCP UDP 连接方式 面向连接 无连接 可靠性 可靠 不可靠 传输速度 慢 快 拥塞控制 有 无 适用场景 对可靠性要求高的场景 对实时性要求高的场景
插入一些python内容
NumPy 数组的基本操作,广播机制的原理
- 基本操作:
- 创建数组:
np.array()、np.zeros()、np.ones()、np.arange()- 数组属性:
shape、dtype、size- 数组索引和切片:
arr[0]、arr[1:3]、arr[:, 0]- 数组运算:
+、-、*、/、np.dot()
Pandas 的核心数据结构,数据清洗的常用方法
- 核心数据结构:
Series:一维带标签的数组DataFrame:二维带标签的表格- 数据清洗的常用方法:
- 缺失值处理:
isnull()、dropna()、fillna()- 重复值处理:
duplicated()、drop_duplicates()- 异常值处理:使用统计方法(如 3σ 原则)或箱线图检测异常值,然后删除或替换
Pandas 的数据筛选、分组聚合、合并拼接
- 数据筛选:
df[df['age'] > 18]、df.loc[df['age'] > 18, ['name', 'age']]- 分组聚合:
df.groupby('category')['value'].sum()、df.groupby('category').agg({'value': ['sum', 'mean']})- 合并拼接:
pd.merge():按列合并pd.concat():按行或列拼接df.join():按索引合并
Matplotlib/Seaborn 的基本使用,如何绘制常用图表
- Matplotlib 基本使用:
import matplotlib.pyplot as plt plt.plot(x, y) plt.xlabel('X') plt.ylabel('Y') plt.title('Title') plt.show()- 常用图表:
- 折线图:
plt.plot()- 柱状图:
plt.bar()- 散点图:
plt.scatter()- 热力图:
sns.heatmap()
Python 中的多线程和多进程,GIL 锁的影响
- GIL 锁:全局解释器锁,同一时刻只有一个线程可以执行 Python 字节码
- 影响:Python 的多线程在 CPU 密集型任务中无法利用多核 CPU,在 IO 密集型任务中可以提高性能
- 多进程:可以利用多核 CPU,适合 CPU 密集型任务
正则表达式的基本语法,如何用正则处理文本格式的地图数据
- 基本语法:
.:匹配任意字符*:匹配前面的字符 0 次或多次+:匹配前面的字符 1 次或多次?:匹配前面的字符 0 次或 1 次[]:匹配括号中的任意一个字符():分组
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)