在B站看的【嵌入式自学-DeepMeet学长】的课程《嵌入式八股文面试题合集》,课程讲得一般,但是过一遍这一百道题还可以。用这个专栏记录一下学习笔记。
目录
39.3.1 epoll_create1/epoll_create
1 函数指针和指针函数的区别
- 函数指针:指向一个函数的指针
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int main()
{
int (*ptr)(int, int) = &add;
return 0;
}
- 指针函数:函数返回值是指针的函数
int *test(void)
{
}
2 指针的大小
指针的大小是固定的,只和编译器的位数有关,和指针的类型没有关系。
- 在32位的系统下指针大小是4个字节
- 在64位的系统下指针是8个字节
3 sizeof和strlen的区别
- sizeof是一个运算符,strlen是一个函数(使用时需要包含string.h头文件)
- sizeof计算的是所占内存的大小,strlen计算的是字符串的长度
- sizeof可以计算int float等类型的大小,strlen一般用于计算字符串的长度
#include <stdio.h>
#include <string.h>
int main()
{
printf("%d %d\n", sizeof("\0"), strlen("\0")); //输出:2 0
//因为字符串以"\0"结尾,"\0"是不需要计算长度的
//但是sizeof计算的是大小,"\0"字符串后面其实还跟着一个"\0",因此长度为2
return 0;
}
4 数组指针和指针数组的区别
- 数组指针指的是指向数组的指针,本质是一个数组
- 指针数组本质是一个数组,其每一个元素都是一个指针
int main()
{
int (*p)[10]; //数组指针
int *a, *b, *c;
int* p[10] = {a, b, c}; //指针数组
return 0;
}
5 c语言里内存分配方式
- 静态存储器分配:全局变量、静态变量
- 栈上分配:函数中定义出的局部变量
- 堆上分配:malloc / new
6 if的两种判断写法
关于if (x == 0)和if (0 == x),在工程中使用if (0 == x)可以防止少写一个=。
7 左值和右值
是两种基本的值的分类,主要用于区分表达式的存储特性和生命周期。
7.1 左值(Lvalue)
Locator Value,表示可以被寻址(即,存储在内存中)并可持续存在的对象
特点:
- 有持久的内存地址,可以被再次引用
- 可以出现在赋值号(=)左侧
- 通常是变量、数组元素、解引用指针、返回左值引用的函数等。
int a = 10; //变量a是左值
a = 20; //左值a可以出现在赋值号左侧
int &ref = a; //左值引用,ref绑定到a
这里a是左值,因为它可以被赋值,并且有一个内存地址可以被访问。
7.2 右值(Rvalue)
Read-only Value,表示不占据持久存储、短暂存在的值,通常是表达式的结果或者是字面值常量。
特点:
- 没有持久地址,通常是临时对象
- 不能出现在赋值号(=)的左侧,除非是右值引用
- 通常是字面值(如10)、表达式结果、返回非引用的函数值等。
int x = 10; //10是右值
int y = x + 5; //(x + 5)计算结果是右值
//错误示例:
//(x + 5) = 10; //错误,右值不能出现在赋值号(=)左侧
这里10和x+5都是右值,因为它们是计算结果,不可修改或持续存储,
7.3 右值引用(C++11引入)
C++引入了右值引用(Rvalue Reference),使用&&语法,可以捕获右值,用于移动语义(move semantics)和完美转发(perfect forwarding)。
右值引用的主要作用:
- 避免不必要的拷贝,提高性能(如std::move())
- 允许修改右值对象(如临时对象的优化)
std::string a = "hello";
std::string b = std::move(a); //a资源移动到b,避免拷贝
7.4 对比总结
类别 | 是否有持久存储 | 是否可作为赋值左侧 | 示例 |
左值 | 是 | 是 | int x;、x = 10;、int &ref = x; |
右值 | 否 | 否(但可用右值引用&&) | 10、x + 5、std::move(x) |
8 共用体的大小
共用体(联合体,union)的大小是由它内部最大成员的大小来决定的。
9 struct结构体和union联合体的区别
- union联合体:
- 所有成员共享一块内存空间(修改一个变量会影响所有变量的值)
- 联合体大小为最大成员的内存大小
- struct结构体
- 不同的成员放在不同的内存空间中
- 结构体大小为所有成员内存大小之和(字节对齐
typedef union
{
int i;
int j[5]; //最大成员
char k;
} TEST1;
typedef struct
{
int a; //4字节对齐
TEST1 b;
short c;
} TEST2;
int main()
{
printf("%d\n", sizeof(TEST1)); //20
printf("%d\n", sizeof(TEST2)); //28
printf("%d\n", sizeof(TEST1) + sizeof(TEST2)); //48
return 0;
}
10 野指针
野指针:指向不可用内存的指针
- 当指针被创建时,没有赋值,这个时候指针就成为了野指针。
- 当指针被free / delete后,没有把指针赋值为NULL,这个时候指针为野指针。
- 当指针越界时,也是野指针。
11 数组和链表的区别
- 数组的地址空间是连续的;链表的地址空间是不连续的。
- 数组的访问速度比较快,直接通过下标访问;链表的访问需要遍历。
- 链表的增删改查速度比数组快。
12 判断链表是否有环
定义:链表中的某个节点的next指针指向了链表中的前面的某个节点
判断:快慢指针:定义两个指针,一个指针走的比较快,一个指针走的比较慢,当两个指针再次指向同一个位置,就说明链表中有环。
struct ListNode
{
int val;
struct ListNode *next;
};
int IsCycle(struct ListNode *head)
{
if (head == NULL || head->next == NULL)
{
return 0; //无环
}
struct ListNode *slow = head;
struct ListNode *fast = head->next;
while (slow != fast)
{
if (fast == NULL || fast->next == NULL)
{
return 0; //无环
}
slow = slow->next; //慢指针走一步
fast = fast->next->next; //块指针走两步
}
return 1; //有环
}
13 宏函数:写一个宏返回输入参数中较小的一个
#define MIN(a, b) ((a) <= (b)? (a) : (b))
14 #include<>和#include""的区别
- 使用#include<>,编译器会从标准库的路径中去搜索,对于搜索标准库的文件速度比较快
- 使用#include"",编译器会从用户工作路径里面去搜索,对于用户自定义的头文件比较快
15 全局变量和局部变量的区别
- 作用域:全局变量作用域为程序块,局部变量为当前函数内部
- 生命周期:全局变量的生命周期为整个程序,局部变量为当前函数
- 存储方式:全局变量存储在全局数据区中,局部变量存储在栈里
- 使用方式:全局变量在程序的各个部分都可以使用,局部变量在函数内部使用
16 define和typedef的区别
- define是预处理指令;typedef是关键字
- define不会做正确性检查,直接进行替换;typedef会做正确性检查
- define没有作用域的限制;typedef有作用域的限制
- 对指针的操作不同:一般使用typedef
17 static的作用
定义一个静态变量或静态函数
- 在函数体中使用static去定义变量,那么该变量只会被初始化一次
- 定义的静态函数或静态变量只能在当前文件中使用(作用域的限制)
- 在函数内部定义的静态变量无法被其他函数使用
18 内存泄漏
内存泄漏指的是在程序运行时,动态分配的空间没有被回收或者是正常释放,导致了内存空间仍占据系统资源。
19 内存碎片
内存碎片(Memory Fragmentation)是指在内存分配和释放过程中,导致内存被分割成多个不连续的小块,无法为大块内存请求提供足够的连续空间的现象。这种情况会降低内存的利用率,导致系统性能下降,甚至可能引发程序崩溃或运行缓慢。
19.1 内存碎片的分类
19.1.1 外部碎片
- 当内存中有足够的空间,但这些空间并不是连续的,无法满足大块内存分配需求时,就会产生外部碎片。
- 外部碎片通常发生在动态内存分配中,如在程序运行过程中不断地分配和释放内存(如malloc和free的使用)。
例:
假设内存有1000字节空闲空间,但它分散在不同位置(如200字节、300字节、500字节),此时如果有一个600字节的请求,尽管总空闲内存量充足,但由于空间不连续,这个请求仍然无法满足。
19.1.2 内部碎片
- 当分配的内存块比实际需要的内存大,导致内存块内部的未使用空间浪费时,称为内部碎片。
- 内部碎片常发生在固定大小内存块的分配机制中,例如分配64字节的内存块,但程序只使用了50字节,剩下的14字节就是内部碎片。
例:
如果某个内存分配固定器固定分配64字节的块,而程序只需要50字节,那么剩余的14字节尽管已经分配,但不会被使用,从而造成内部碎片。
19.2 内存碎片的影响
- 降低内存利用率:由于碎片化,大量不连续的内存块可能无法满足大块内存分配的需求,导致空间内存无法被有效利用。
- 性能下降:程序频繁进行内存分配和释放,系统坑需要更多时间来查找合适的内存块,导致性能下降。
- 增加内存分配失败的风险:尽管系统内存未满,但由于碎片化,可能会无法为程序分配足够大的连续内存,最终导致分配失败。
19.3 内存碎片的解决办法
- 内存压缩(Compaction):通过移动已分配的内存块,将分散的内存块合并为连续的空闲空间。这种方法通常会消耗大量的CPU资源。适合需要优化碎片的系统,如操作系统。
- 内存池(Memory Pooling):使用预分配的固定大小内存块,避免碎片化,适合内存需求相对固定的场景。
- 垃圾回收机制(Garbage Collection):某些编程语言如Java、Python等使用垃圾回收机制,可以定期回收未使用的内存,减少碎片化。
- 最佳适配算法:内存分配算法如最佳适配(Best Fit),首次适配(First Fit)等,能够更有效的减少碎片化。
20 内存对齐
内存对齐指在存储数据是,将数据按照一定的规则放置在内存中的过程。
原则:以最大的变量所占据的内存来进行分配内存空间
typedef struct
{
char a; //1字节
int b; //4字节
short c; //2字节
} STU;
int main()
{
//都与最大成员的大小对齐,3 * 4字节
printf("%d\n", sizeof(struct STU)); //12字节
return 0;
}
21 取消内存对齐的方法
- #pragma pack()
- gcc编译器中,__attribute__((packed))关键字
//取消内存对齐
#pragma pack(1)
typedef struct
{
char a; //1字节
int b; //4字节
} STU;
//恢复默认内存对齐方式
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct STU)); //5字节
return 0;
}
22 数组名和指针的区别
- 数组名就是数组首元素地址,也可以看做一个常量指针(不能修改指向的值,内存访问4字节)
- 使用指针访问数组的时候需要使用解引用*,是间接访问;使用数组名访问数组是直接访问
- 使用sizeof对指针和数组名进行计算的时候是不同的:sizeof(指针)和编译器的位数有关,sizeof(数组名)是整个数组的大小
23 指针常量和常量指针
- 常量指针:指向一个常量的指针,无法修改所指向的值,但是可以修改指向
- 指针常量:指针是一个常量,即指针指向的地址是固定的,可以修改指向的值
24 堆和栈的区别
- 创建方式不同
- 栈是系统自动创建(用于保存局部变量)。当函数执行完成,栈被销毁
- 堆是程序员手动进行创建和销毁,malloc / new进行创建,free / delete进行销毁
- 空间大小不同
- 栈的空间比较小
- 堆的空间比较大
- 访问速度不同
- 栈的访问速度比较快
- 堆的访问速度比较慢
- 生命周期
- 当使用完成后,栈自动就会被销毁
- 堆要靠程序员手动销毁
25 队列和栈的区别
- 访问方式不同
- 栈:先进后出
- 队列:先进先出
- 操作方式不同
- 栈:只能在栈顶进行操作
- 队列:在队尾进行插入,在队首进行删除
- 应用场景
- 栈:主要用于函数调用、表达式求值
- 队列:用于任务调度、广度优先搜索
26 malloc和new的区别
- malloc是C语言中的标准库函数;new是C++中的操作符
- malloc分配内存后反悔的事void*类型的指针;new分配内存后返回的是对应对象类型的指针
- 使用malloc分配内存是需要指定分配内存的大小;使用new进行内存分配时不需要指定
- 使用malloc分配内存时不会调用到构造函数;使用new分配内存时会调用到构造函数
27 vector和list的适用场景
std::vector和std::list是C++中常用的容器,他们在使用场景上有不同的优势和适用性。选择使用vector或list主要取决于你的需求和操作模式。以下是它们的特点及使用场景:
27.1 std::vector
特点:
- 连续内存分配:vector使用连续的内存块存储数据,类似于动态数组。
- 快速随机访问:支持常数时间的随机访问(即可以使用索引[]或at()直接访问元素)。
- 尾部插入/删除效率高:在vector的末尾插入或删除元素时间复杂度为o(1)。
- 动态调整大小:当容量不够时,vector回自动调整大小,但会导致重新分配内存,影响效率。
std::vector<int> v = {1, 2, 3};
int x = v[2]; //快速访问第三个元素
适用场景:
- 频繁的随机访问:如果你需要快速访问容器中的任意元素,vector是首选。例如:
- 顺序插入/删除操作:如果你只在尾部插入或删除元素,vector的性能很好。它的复杂度为o(1)。
- 少量中间插入/删除:如果你的操作只偶尔在中间插入/删除,并且对性能要求不高,vector仍然可以使用,但注意性能损耗。
- 内存紧凑性:由于vector使用连续内存,因此在某些内存敏感的场景下,这种紧凑的存储方式非常有效。
不适合的场景:
- 频繁的中间插入/删除:在vector中间插入或删除元素时,所有后续元素都需要移动,导致时间复杂度为O(n),效率低下。
27.2 std::list
特点:
- 链式存储:list使用双向链表来存储元素,元素在内存中不是连续的。
- 快速的中间插入/删除:插入或删除任意位置的元素都非常高校,时间复杂度为O(1)(只需调整指针)。
- 无随机访问:无法通过索引直接访问元素,必须通过迭代器顺序遍历,时间复杂度为O(n)。
std::list<int> lst = {1, 2, 3};
auto it = std::next(lst.begin(), 1);
lst.insert(it, 4); //在第二个位置插入4
适用场景:
- 频繁的中间插入/删除:若操作主要是插入或删除容器中的中间元素,list是一个更好的选择,因为它不需要移动其他元素。
- 大量的非随机访问操作:当你只需要顺序遍历元素,而不需要随机访问时,list是合适的。
- 数据结构需要稳定的迭代器:对于list,插入和删除操作不会使已有的迭代器失效,而vector在内存中重新分配时可能使迭代器失效。
不适合的场景:
- 频繁的随机访问:由于list不能通过索引直接访问元素,如果你的程序需要频繁地随机访问某个位置的元素,list的性能很差,因为每次访问都需要从头遍历。
- 较高的内存开销:由于list需要存储额外的指针(每个节点有两个指针:前驱和后继),因此它的内存开销比vector大。
28 在C++中struct和class的区别
- struct成员默认是公有的,class默认的成员是私有的
- 继承方面:struct默认是公有继承,class默认是私有继承
- 使用场景:struct一般用于做简单的数据结构,class一般用于封装和继承
29 C++中的类有几个访问权限
- public:当成员声明为public时,就可以在类的外部进行访问
- protected:当成员声明为protected时,只能在类内或子类中进行访问
- private:当成员声明为private时,只能在类内进行访问
30 C语言位域
位域(Bit Fields)是一种结构体的特殊用法,用于在结构体以位(bit)为单位分配内存空间,而不是默认的字节(byte)。它主要用于节省存储空间,特别适用于寄存器操作、协议解析、嵌入式系统等场景。
30.1 位域的定义方式
在字段后面加上了:n,表示该字段占用n位。例如:
struct BitField
{
unsigned int a : 3; //占3位
unsigned int b : 5; //占5位
unsigned int c : 2; //占2位
};
int main()
{
struct BitField bf;
bf.a = 5; //5的二进制为101
bf.b = 15; //15的二进制为01111
bf.c = 2; //2的二进制为10
printf("a = %d, b = %d, c = %d\n", bf.a, bf.b, bf.c);
return 0;
}
30.2 位域的存储
- 位域是按字节存储的,不会跨字节,通常会放在同一个int或char变量的二进制位中。
- 位域的大小受限于基础数据类型,如int或char,不能使用float、double等类型。
- 位于的存储方式(高位在前还是低位在前)依赖于编译器。不同CPU可能有不同大小端规则。
30.3 使用方式
(1)限制变量的值范围
由于a只占3位,它的取值范围是0-7(2^3 - 1)。
bf.a = 8; //8超出范围,可能会被截断
(2)调整字段顺序以优化存储
struct BitField
{
unsigned char a : 3; //占3位
unsigned char b : 5; //占5位
}; //总共只占1个字节
30.4 位域的对齐
位域成员的排列可能受编译器的字节对齐规则影响。例如:
struct Test
{
unsigned char a : 3; //占3位
unsigned char b : 5; //占5位
unsigned int c : 6;
};
//a和b共用1个字节
//c由于是int,可能会对齐到4字节边界,导致存储空间浪费。
30.5 位域的匿名字段
struct Test
{
unsigned int a : 3;
unsigned int : 5; //5位填充,不使用,不会影响c的取值
unsigned int c : 6;
};
30.6 位域的实际使用
(1)用于寄存器映射
struct Register
{
unsigned int enable : 1; //使能位
unsigned int mode : 2; //工作模式
unsigned int status : 3; //状态位
unsigned int : 2; //匿名位
unsigned int error : 1; //错误标志
};
这样可以直接访问寄存器的特定位,而不用位运算,如:
struct Register reg;
reg.enable = 1;
reg.mode = 3;
(2)用于网络协议解析,如IP头解析
struct IPReader
{
unsigned int version : 4; //版本号
unsigned int ihl : 4; //头部长度
unsigned int tos : 8; //服务类型
unsigned int length : 16; //总长度
};
这样定义后,可以直接访问协议字段,而不需要位运算。
30.7 注意事项
(1)位域不能取地址
由于位域可能不对齐,不能对其使用&取地址。
struct Test t;
//printf("%p", &t.a); //错误
(2)跨平台问题
- 位域的存储顺序(高位在前或低位在前)依赖于CPU大小端,不同平台可能不同。
- 不同编译器可能会对齐不同,因此位域结构在不同平台上可能表现不一致。
(3)位域不能用于float、double
struct Test
{
//float f : 3; //错误
};
(4)位域通常基于int
- char、short可能可以使用,但依赖于编译器
- int可能占16位或32位,位域最大只能使用int的所有位
30.8 优点
特性 | 说明 |
节省存储空间 | 使用bit级存储,适合嵌入式系统 |
寄存器映射 | 可用于处理寄存器映射,简化硬件控制 |
协议解析 | 可用于解析网络协议,数据格式等 |
跨平台注意 | 可能受CPU大小端影响,结构体对齐方式可能不同 |
不能取地址 | 不能对位域字段取地址 |
31 extern C
extern C是用于在C++代码中告诉编译器以C语言的链接方式来处理代码的一个特性。它通常用于在C++程序中调用C语言编写的函数或是将C++代码暴露给C代码使用。
31.1 作用
- 防止C++名字修饰(Name Mangling): 在C++中,编译器会对函数名进行“名字修饰”或“符号重整”,也就是将函数名根据其参数类型,返回值等信息转换为一个唯一的符号,这种机制支持函数重载,但会导致编译后的函数名和函数声明的名字不同。
- 跨语言链接:extern C通常用于将C语言编写的库域C++代码结合使用。通过extern C声明,C++编译器会生成与C语言兼容的符号表,使得可以与C语言程序进行链接。
31.2 使用场景
(1)C++调用C函数
当要在C++中调用C语言编写的函数时,可以使用extern C,防止函数名被编译器重命名。
//C语言头文件
extern "C" {
#include "some_c_library.h"
}
(2)C调用C++函数
当要在C语言中调用C++中的函数时,可以使用extern C来告诉编译器使用C语言的链接方式。
extern "C" void myFunction(int x) {
//这是一个C++函数,但是可以被C调用
}
(3)结合多个函数
如果有多个函数需要使用extern C,可以将它们放入一个extern C块中:
extern "C" {
void function1(int x);
void function2(double y);
}
32 内联函数
是一种特殊的函数声明方式,通过在函数前面加上inline关键字,来指示编译器在调用这个函数时将他展开,而不是直接调用。
inline int add(int a, int b)
{
return a + b;
}
int main()
{
add(1, 2);
return 0;
}
优点:
- 可以减小函数调用的开销
- 提高函数执行效率
- 允许编译器进行优化,进一步提高性能
33 回调函数
回调函数(Callback Function)是在编程中一种常用的概念,指的是将一个函数作为参数传递给另一个函数,并在适当的时机由该函数调用它。简单来说,回调函数就是在某个事件或条件触发时由别的函数调用的函数。
33.1 特点
- 作为参数传递:回调函数本质上是一个函数指针(在C/C++中),或函数引用(在Java/Python中),可以传递给其他函数。
- 延迟执行:回调函数通常不会立即执行,而是在某个事件、条件或者另一函数调用的特定时刻执行。
- 灵活性:通过回调函数,程序可以在不同的上下文中执行不同的操作,增加了代码的灵活性和复用性。
33.2 常见应用
- 异步编程:在处理异步操作(如网络请求、文件读写、定时器等)时,回调函数常用于在操作完成后通知应用执行特定操作。
- 事件驱动编程:如GUI程序中的按钮点击、鼠标移动等事件,都可以通过回调函数来处理。
- 库函数或系统调用:一些库函数允许用户自定义回调函数,以便在特定时间或状态下执行特定逻辑。
33.3 回调函数的例子
示例:C语言中的回调函数
在C语言中,回调函数通常通过函数指针实现。如,排序函数qsort可以通过回调函数指定比较规则。
#include <stdio.h>
#include <stdlib.h>
//比较函数,用于qsort
int compara(const void *a, const void *b) {
return (*(int *)a - *(int *)b);
}
int main()
{
int numbers[] = {5, 2, 9, 1, 5, 6};
int size = sizeof(numbers) / sizeof(numbers[0]);
//调用qsort,传递compare函数作为回调函数传递,用于指定排序规则
qsort(numbers, size, sizeof(int), compare);
//输出排序后的数组
for (int i = 0; i < size; i++)
{
printf("%d", numbers[i]);
}
return 0;
}
34 memcpy和strcpy的区别
- memcpy用于复制任意类型的内存数据,如结构体,字符串、数组等,是按照字节数来进行复制的。使用时需要手动指定需要复制的字节数。
- strcpy专门用于复制以'\0'结尾的C语言字符串,遇到'\0'时结束复制。
35 使用C语言实现strcpy函数
字符串拷贝
char *my_strcpy(char *dest, char *src)
{
char *temp = dest;
while ((*dest++ = *src++));
return temp;
}
36 使用C语言实现strcmp函数
比较字符串的每一个字符是否都相同
int my_strcmp(const char *str1, const char *str2)
{
while (*str1 && str2)
{
if (*str1 != *str2)
{
return (str1 - *str2);
}
//遍历下一个字符是否相同
str1++;
str2++;
}
return (*str1 - *str2); //等于0
}
37 使用C语言实现strcat函数
字符串连接函数
char *my_strcat(char *dest, const char *src)
{
char *temp = dest;
//找到末尾位置
while (*temp != '\0')
{
temp++;
}
//追加
while(*src != '\0')
{
*temp = *src;
temp++;
src++;
}
*temp = '\0';
return temp;
}
38 使用C语言实现栈
栈:存储数据的空间、栈指针、出栈和入栈的操作
typedef struct
{
int data[100];
int top;
} Stack;
void initStack(Stack *s)
{
s->top = -1;
}
//val参数表示要入栈的值
int push(Stack *s, int val)
{
//栈满无法入栈
if (s->top == 99)
{
return -1;
}
s->data[++(s->top)] = val;
return 0;
}
//val指针用来接收要弹出的值
int pop(Stack *s, int *val)
{
//栈空无法出栈
if (s->top == -1)
{
return -1;
}
*val = s->data[(s->top)--];
return 0;
}
39 select、poll、epoll
这三个函数在网络编程里用的比较多,主要用于高并发服务器。
39.1 select API
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 参数说明:
- nfds:需要监听的文件描述符数量,即文件描述符集合中最大值加1
- readfds:监听可读事件的文件描述符集合
- writefds:监听可写事件的文件描述符集合
- exceptfds:监听异常事件的文件描述符集合
- timeout:设置超时时间,NULL表示永不超时
- 返回值:
- 成功时返回就绪的文件描述符数量,超时返回0,失败返回-1并设置errno。
39.2 poll API
#include <pollt.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 参数说明:
- fds:指向pollfd结构体数组的指针,定义了需要监听的文件描述符及其事件
- nfds:需要监听的文件描述符数量
- timeout:等待超时时间,以毫秒为单位,-1表示无限等待
- pollfd结构体:
struct pollfd
{
int fd; //文件描述符
short events; //要监听的事件
short revents; //实际发生的事件
};
- 返回值:
- 成功时返回就绪的文件描述符数量,超时返回0,失败返回-1并设置errno。
39.3 epoll API
39.3.1 epoll_create1/epoll_create
#include <sys/epoll.h>
int epoll_create1(int flags);
int epoll_create(int size); //旧版API,不推荐使用
- 参数说明
- flags:可设置为EPOLL_CLOEXEC以设置文件描述符的执行时关闭标志。
- size:无实际作用,历史遗留参数
- 返回值
- 成功时返回epoll实例的文件描述符,失败返回-1。
39.3.2 epoll_ctl
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 参数说明:
- epfd:epoll实例的文件描述符(由epoll_create1返回)。
- op:操作类型,可以是EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(删除)。
- fd:需要监听的文件描述符
- event:只想epoll_event结构体的指针,指定需要监听的事件
- epoll_event结构体:
struct epoll_event
{
uint32_t events;
epoll_data_t data;
};
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
- 返回值:
- 成功时返回0,失败返回-1。
39.3.3 epoll_wait
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 参数说明:
- epfd:epoll实例的文件描述符
- events:指向epoll_event结构体数组的指针,用于存储发生事件的文件描述符
- maxevents:每次可以返回的最大时间数
- timeout:等待超时时间,以毫秒为单位,-1表示无限等待
- 返回值:
- 成功时返回就绪事件的数量,超时返回0,失败返回-1并设置errno。
39.4 select、poll、epoll区别
1. API结构
- select
- 使用固定大小的fd_set,文件描述符数量有上限(典型为1024).
- 每次调用都需要重新初始化文件描述符集。
- poll
- 使用一个pollfd数组,没有文件描述符上限(仅受系统资源限制)。
- 同样需要每次调用重新遍历文件描述符数组。
- epoll
- 通过内核维护(红黑树)的文件描述符列表,不需要每次调用都传递整个文件描述符集。
- 使用epoll_ctl添加/删除感兴趣的文件描述符。
2. 性能
- select和poll:
- 每次调用都会遍历整个文件描述符集成数组,监视的文件描述符越多,开销越大,效率越低。
- epoll:
- 使用时间驱动模型,只在有事件时返回,性能与文件描述符的数量无关,适合大量并发连接的场景。
3. 文件描述符数量限制
- select
- 受文件描述符集的大小限制(通常为1024)。
- poll和epoll
- 没有硬性限制,最大数量只受系统资源限制。
4. 内存效率
- select
- 每次调用会拷贝fd_set,浪费内存和CPU资源。
- poll
- 使用pollfd数组,每次需要重新构建。
- epoll
- 内核维护文件描述符,用户态和内核态之间的数据交换更少,内存效率更高。
5. 边缘触发 vs 水平触发
- select和poll
- 只支持水平触发(Level Triggered),事件不会消失,必须处理完成。
- epoll
- 支持边缘触发(Edge Triggered)和水平触发,边缘触发更高效但使用复杂。
6. 适用场景
- select:是和少量文件描述符和较老的代码,简单但性能较差。
- poll:适合稍大规模的问文件描述符,但在大量连接时性能不佳。
- epoll:适合大量并发连接,如高性能服务器或网络程序。
40 构造函数和析构函数
40.1 构造函数
作用:
- 初始化对象, 设定初始值
特点:
- 名称和类名相同
- 没有返回值
- 可以有多个重载版本,支持不同初始化的方式
40.2 析构函数
作用:
- 释放资源,清空操作(释放new申请的空间以及释放文件句柄等)
特点:
- 没有参数也没有返回值
- 名称只能以~开头
- 不能重载
41 深拷贝和浅拷贝的区别
- 浅拷贝:
- 只复制指针的值,多个对象共享同一块动态分配的内存
- 适用于简单类型或者是无需独立管理资源的对象,可能导致内存泄漏或多次释放的问题
- 深拷贝:
- 复制指针指向的内存内容,申请一块独立的内存空间,多个对象各自拥有独立的内存
- 每个对象拥有独立的内存副本,不会互相影响
- 适用于包含动态分配内存的对象
class MyClass
{
private:
char *data;
public:
//构造函数
MyClass(const char *input) {
data = new char[strlen(input) + 1]; //分配内存
strcpy(data, input); //复制数据
}
//浅拷贝(默认的拷贝构造函数)
MyClass(const MyClass &other) {
data = other.data; //仅复制指针,指向同一块内存
std::cout << "浅拷贝构造函数被调用!" << std::endl;
}
//深拷贝(自定义拷贝构造函数)
MyClass &deepCopy(const MyClass &other) {
if (this != &other) { //避免自我赋值
delete[] data; //释放已有内存
data = new char[strlen(other.data) + 1]; //分配新的内存
strcpy(data, other.data); //复制数据
}
std::cout << "深拷贝构造函数被调用!" << std::endl;
return *this;
}
};
42 虚函数
虚函数是C++中的成员函数,可以通过父类指针或引用调用的时候,可以动态执行子类中的重写函数,而不是父类的函数。主要用于实现C++的多态特性。
在继承中,父类的函数可以被子类重写。使用虚函数能保证通过父类的指针可以调用到子类的重写版本函数。
语法:函数前加上virtual关键字
class Base
{
public:
//在父类中声明虚函数
virtual void show()
{
std::cout << "Base class show function";
}
//普通的非虚函数
void display()
{
std::cout << "Base class display function\n";
}
//虚析构函数,确保动态内存正确释放
virtual ~Base()
{
}
};
class Derived: public Base
{
public:
//重写虚函数
void show() override
{
std::cout << "Derived class show function\n";
}
//重写非虚函数(但这是普通函数重载)
void display()
{
std::cout << "Derived class display function\n";
}
}
int main()
{
//父类指针指向子类对象
Base *basePtr = new Derived();
//调用的是Derived类别的show(),因为它是虚函数
basePtr->show();
//调用的是Base类的display(),因为它不是虚函数
basePtr->display();
delete basePtr;
return 0;
}
43 函数重载
函数重载是C++中的一个特性,允许在同一个作用域中定义多个同名的函数,但是函数的参数列表、参数类型、参数个数不同。
本质:
在编译时,编译器根据实参的个数和类型来选择匹配对应的函数,并将函数名进行修饰,让这些重载的函数在底层拥有不同的符号。
规则:
- 函数名要求相同
- 函数的参数列表(参数类型、数量或者顺序需要有差异)必须不同。
- 返回值可以相同也可以不相同,不会通过简单的返回值来判断是否是函数重载
class Calculator
{
public:
//函数重载:用于加法的函数
int add(int a, int b)
{
return a + b;
}
//函数重载:参数类型不同
double add(double a, double b)
{
return a + b;
}
//函数重载:参数个数不同
int add(int a, int b, int c)
{
return a + b + c;
}
};
44 智能指针
智能指针是C++中的一种对象,用来自动管理动态分配的内存,智能指针里面封装了普通的指针,并且通过自动化管理内存来避免手动去释放内存。
class MyClass {
public:
MyClass()
{
std::cout << "MyClass Constructctor\n";
}
~MyClass()
{
std::cout << "MyClass Destructctor\n";
}
void show()
{
std::cout << "MyClass show method\n";
}
};
int main()
{
//创建一个unique_ptr,管理MyClass对象
std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
//使用unique_ptr访问对象
ptr->show();
//离开作用域时,unique_ptr自动释放内存
return 0;
}
45 大小端
概念:
多字节的数据在内存中的存储顺序方式,在不同的计算机中会使用不同的字节序来表示数据。
- 大端存储:高字节在低地址,低字节在高地址
- 小端存储:高字节在高地址,低字节在低地址
判断:
- 0x12345678,假设第一个字节是0x78那么这个系统为小端存储;假设第一个字节是0x12那么这个系统为大端存储。
int main()
{
char a = 0x12345678;
printf("%x\n", a);
return 0;
}
46 线程池
线程池是一种多线程管理模式,主要目的是提高系统的并发性能和资源利用效率。在线程池,预先创建了一定数量的线程,线程被重复利用来执行任务,从而避免了频繁创建和销毁线程的开销。线程池的核心组件通常包括线程队列、任务队列和线程管理机制。
46.1 线程池的工作流程
- 初始化线程池:启动应用后,线程池会创建一定数量的线程并使它们处于等待状态。
- 提交任务:当有任务需要执行时,任务被提交到线程池的任务队列中。
- 分配任务:线程池从任务队列中取出任务,并将任务分配给空闲线程执行。
- 任务执行:线程从任务队列中取到任务后开始执行,执行完成后线程返回到线程池中继续等待下一个任务。
- 重复利用线程:线程池中的线程可以被多个任务重复使用,避免了频繁的创建和销毁操作,降低了系统资源消耗。
46.2 线程池的优势
- 减少线程创建和销毁的开销:线程创建和销毁是比较耗时的操作,线程池通过重用线程减少了这种开销。
- 控制并发数:线程池可以通过控制线程的最大数量,防止系统因线程过多而造成资源枯竭。
- 提高响应速度:当有任务提交时,可以立即使用已有的线程处理任务,无需等待新线程的创建。
46.3 线程池的应用场景
- 服务器并发处理:如Web服务器、数据库服务器等,需要同时处理大量请求的应用。
- 并行计算任务:如图像处理、数据分析等需要高并发运算的场景。
- 后台任务:如日志记录、异步消息处理等不需要立即返回结果的任务。
47 进程和线程的区别
- 定义:
- 进程是资源分配的基本单位,进程>线程
- 线程是进程中的执行单元,是CPU调度和执行的基本单位
- 资源占用:
- 每一个进程都有自己独立的地址空间
- 线程共享进程的地址空间
- 容错性:
- 当一个进程出错不会影响到其他进程执行
- 当一个线程出错可能会导致整个程序崩溃,从而影响到其他线程
- 调度和切换:
- 进程是独立的单位,由调度器进行调度和切换,需要恢复的上下文的内容比较多,消耗资源比较多
- 线程消耗的资源比较少
48 多进程和多线程的适用场景区别
48.1 多线程
- 优点:
- 轻量级:线程之间的切换开销小,创建和销毁的开销小
- 共享内存:线程之间可以轻松交互数据和资源,通信方式比较简单
- 缺点:
- 线程安全:会有数据竞争和死锁问题
- 调试问题:调试困难
- 适用场景:
- web服务器,网络服务,用户界面
48.2 多进程
- 优点:
- 每一个进程都有自己的内存空间,不同进程之间不会互相影响,安全性和稳定性比较高
- 适合CPU密集型任务,多进程可以利用多核CPU的能力,提高程序的并行处理能力
- 缺点:
- 创建和管理进程的方式开销比较大
- 通信方式比较复杂
- 启动时间比较长
- 适用场景:
- CPU密集任务,以及需要高度隔离的任务
49 进程间通信方式
进程间通信有几种方式?
- 管道
- 命名管道
- 共享内存(memmap)
- 信号量
- 消息队列
- 套接字(socket)
- 信号
哪几种方式需要借助内核?
- 管道
- 共享内存(在内核创建的共享内存)
- 信号量
- 消息队列
- 套接字
50 fork和vfork的区别
- 拷贝时机
- fork的子进程会拷贝父进程的代码段和数据段(当需要改变共享数据段的变量时)
- vfork的子进程和父进程共享数据段。
- 执行顺序
- fork创建的子进程和父进程执行顺序不确定
- vfork保证子进程先运行,只有当子进程调用了exec或exit之后父进程才有可能被调度执行,如果在调用这两个函数之前,子进程依赖父进程的操作,那么会发生死锁。
51 当for循环遇到fork函数
for (int i = 0; i < 3; i++)
{
fork();
}
Iteration 0(i = 0);
Parent Process (P1)
|
+--- Fork ---> Child Process (P2)
Iteration 1(i = 1);
Parent Process (P1) Child Process (P2)
| |
+--- Fork ---> P3 +--- Fork ---> P4
Iteration 2(i = 2);
P1 P2 P3 P4
| | | |
+--- Fork ---> P5 +-- Fork ---> P6 +-- Fork ---> P7 +-- Fork ---> P8
初始进程P1:在第一次fork()调用时创建了进程P2
第二次循环:P1和P2分别创建了P3和P4
第三次循环:P1、P2、P3、P4各自再创建一个进程,最终有8个进程:p1到p8。
52 进程有几个状态
- 创建状态:当调用fork函数后进入创建状态
- 就绪状态:进程已经准备好,但是还没有运行
- 运行状态:进程已经获得系统的资源可以运行
- 阻塞状态:等待信号量或者互斥量等事件的时候会进入这个状态
- 终止状态:进程结束
53 什么是僵尸进程、孤儿进程、守护进程
- 僵尸进程:使用fork创建子进程后,如果子进程退出,父进程并没有调用wait或waitpid函数获取子进程的退出状态,那么该子进程的信息还保存在系统中,这个时候子进程就叫做僵尸进程。
- 孤儿进程:父进程异常结束,子进程就会变成孤儿进程,会被init1号进程收养。
- 守护进程:在父进程创建出子进程后,故意把父进程结束,那么该子进程就叫做守护进程。
54 自旋锁和互斥锁
自旋锁和互斥锁都是用于多线程编程中的同步机制,防止多个线程同时访问共享资源引发数据竞争或不一致。他们主要区别在于线程获取锁时的行为:
54.1 自旋锁(Spinlock)
自旋锁的工作原理是:当一个线程尝试获取锁但锁已被其他线程持有时,它就不会被阻塞或挂起,而是不停的轮询检查锁是否可用,即“自旋”等待,直到锁释放。这种方式适合用于等待时间非常短的情况,因为线程不会陷入休眠,从而避免了上下文切换的开销。
特点:
- 适用于锁的持有时间较短的情况。
- 不会引起线程的睡眠,如果锁很快释放,自旋锁的性能较好。
- 如果等待时间较长,自旋会消耗CPU资源,导致性能下降。
应用场景:
- 短时间的临界区操作,比如访问某个非常简单的变量。
- 多核处理器环境,避免线程频繁上下文切换。
54.2 互斥锁(Mutex)
互斥锁的工作原理是:当一个线程获取锁时,如果锁已被其他线程持有,当前线程会被挂起,等待锁被释放。这意味着被阻塞的线程不会占用CPU资源,系统会将它置于睡眠状态,直到可以获取锁时再唤醒它,
特点:
- 适用于锁的持有时间可能较长的场景。
- 线程在等待时会进入休眠状态,不会浪费CPU资源。
- 存在上下文切换的开销,当锁持有时间较短时,上下文切换的成本可能会大于自旋锁的轮询。
应用场景:
- 临界区操作时间较长,比如涉及I/O操作或复杂的计算。
- 单核或多核系统中都适用,特别是在长时间等待锁的情况下。
54.3 区别总结
1.等待方式
- 自旋锁:线程在等待时持续占用CPU,自旋轮询锁状态。
- 互斥锁:线程在等待时被挂起,不占用CPU资源。
2.适用场景
- 自旋锁:适合锁持有时间极短的情况。
- 互斥锁:适合锁持有时间较长的情况,尤其是涉及I/O等慢操作。
3.开销
- 自旋锁:由于自旋需要反复检查状态,长时间等待会消耗大量CPU时间。
- 互斥锁:线程被挂起时不消耗CPU,但涉及上下文切换的开销。
55 程序分为几个段
- 代码段:用于存储程序的可执行指令。只读,防止被篡改。
- 数据段(data段):用于存储已经初始化的全局变量和静态变量
- BSS段:用于存储没有初始化的全局变量和静态变量
- 堆:malloc和free管理的
- 栈:存储局部变量,栈的申请和释放是由操作系统来决定的
56 静态链接和动态链接的区别
静态链接(.a)和动态链接(.so)是程序编译过程中两种不同的链接方式,它们主要体现在程序的构建、执行效率、内存使用等方面。
- 链接时机
- 静态链接:在编译期间,所有的库和依赖会在生成可执行文件时,一并打包进可执行文件中。链接的内容固定在编译阶段。
- 动态链接:在程序运行时,库文件才会被加载。可执行文件本身只包含对动态库的引用,实际的代码在程序运行时由操作系统加载。
- 可执行文件的大小
- 静态链接:由于库文件已经嵌入可执行文件中,因此生成的可执行文件通常较大
- 动态链接:可执行文件较小,因为库并未嵌入,而是运行时由操作系统加载
- 运行时依赖
- 静态链接:不需要额外的外部库,因为所有依赖都已经打包进了可执行文件,通常可以独立运行
- 动态链接:需要在运行时找到并加载所依赖的动态库(如.dll、.so文件),如果缺少库,程序将无法运行
- 更新维护
- 静态链接:更新库文件需要重新编译整个应用程序。如果库有了新版本,程序不会自动更新,必须手动重新链接和发布。
- 动态链接:库文件可以独立更新,程序无需重新编译和发布。只要库的接口没有改变,替换或更新库文件后,程序即可使用新版库。
- 内存使用
- 静态链接:每个程序都会有一份独立的库代码拷贝,因此多个程序使用相同库时,会重复加载,占用更多的内存。
- 动态链接:多个程序可以共享同一份库文件,减少内存使用量,因此操作系统可以让多个程序使用同一个动态库。
- 执行效率
- 静态链接:启动时不需要加载外部库,通常启动速度稍快。
- 动态链接:在程序启动时需要加载动态库,因此启动速度可能稍慢一些,但由于动态库通常优化良好,运行时性能差异较小。
57 一个.c文件怎么转换为可执行程序
- 预处理:将头文件和宏定义展开,生成没有注释的源代码->.i文件
- 编译:将预处理得到的源代码转换为汇编代码->.s文件
- 汇编:将汇编代码转换为机器码,生成对应的目标文件->.o文件
- 链接:将全部.o文件链接成一个可执行程序
58 交叉编译
交叉编译指在一个平台上编译出另一个平台的可执行程序。