嵌入式面试八股文100题(一)

在B站看的【嵌入式自学-DeepMeet学长】的课程《嵌入式八股文面试题合集》,课程讲得一般,但是过一遍这一百道题还可以。用这个专栏记录一下学习笔记。


目录

1 函数指针和指针函数的区别

2 指针的大小

3 sizeof和strlen的区别

4 数组指针和指针数组的区别

5 c语言里内存分配方式

6 if的两种判断写法

7 左值和右值

7.1 左值(Lvalue)

7.2 右值(Rvalue)

7.3 右值引用(C++11引入)

7.4 对比总结

8 共用体的大小

9 struct结构体和union联合体的区别

10 野指针

11 数组和链表的区别

12 判断链表是否有环

13 宏函数:写一个宏返回输入参数中较小的一个

14 #include<>和#include""的区别

15 全局变量和局部变量的区别

16 define和typedef的区别

17 static的作用

18 内存泄漏

19 内存碎片

19.1 内存碎片的分类

19.1.1 外部碎片

19.1.2 内部碎片

19.2 内存碎片的影响

19.3 内存碎片的解决办法

20 内存对齐

21 取消内存对齐的方法

22 数组名和指针的区别

23 指针常量和常量指针 

24 堆和栈的区别

25 队列和栈的区别

26 malloc和new的区别

27 vector和list的适用场景

27.1 std::vector

27.2 std::list

28 在C++中struct和class的区别

29 C++中的类有几个访问权限

30 C语言位域

30.1 位域的定义方式

30.2 位域的存储

30.3 使用方式

30.4 位域的对齐

30.5 位域的匿名字段

30.6 位域的实际使用

30.7 注意事项

30.8 优点

31 extern C

31.1 作用

31.2 使用场景

32 内联函数

33 回调函数

33.1 特点

33.2 常见应用

33.3 回调函数的例子

34 memcpy和strcpy的区别

35 使用C语言实现strcpy函数

36 使用C语言实现strcmp函数

37 使用C语言实现strcat函数

38 使用C语言实现栈

39 select、poll、epoll

39.1 select API

39.2 poll API 

39.3 epoll API

39.3.1 epoll_create1/epoll_create

39.3.2 epoll_ctl

39.3.3 epoll_wait

39.4 select、poll、epoll区别

40 构造函数和析构函数

40.1 构造函数

40.2 析构函数

41 深拷贝和浅拷贝的区别

42 虚函数

43 函数重载

44 智能指针

45 大小端

46 线程池

46.1 线程池的工作流程

46.2 线程池的优势

46.3 线程池的应用场景

47 进程和线程的区别

48 多进程和多线程的适用场景区别

48.1 多线程

48.2 多进程

49 进程间通信方式

50 fork和vfork的区别

51 当for循环遇到fork函数

52 进程有几个状态

53 什么是僵尸进程、孤儿进程、守护进程

54 自旋锁和互斥锁

54.1 自旋锁(Spinlock)

54.2 互斥锁(Mutex)

54.3 区别总结

55 程序分为几个段

56 静态链接和动态链接的区别

57 一个.c文件怎么转换为可执行程序

58 交叉编译 


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语言里内存分配方式

  1. 静态存储器分配:全局变量、静态变量
  2. 栈上分配:函数中定义出的局部变量
  3. 堆上分配: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)。

右值引用的主要作用:

  1. 避免不必要的拷贝,提高性能(如std::move())
  2. 允许修改右值对象(如临时对象的优化)
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 数组和链表的区别

  1. 数组的地址空间是连续的;链表的地址空间是不连续的。
  2. 数组的访问速度比较快,直接通过下标访问;链表的访问需要遍历。
  3. 链表的增删改查速度比数组快。

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 作用

  1. 防止C++名字修饰(Name Mangling): 在C++中,编译器会对函数名进行“名字修饰”或“符号重整”,也就是将函数名根据其参数类型,返回值等信息转换为一个唯一的符号,这种机制支持函数重载,但会导致编译后的函数名和函数声明的名字不同。
  2. 跨语言链接: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 特点

  1. 作为参数传递:回调函数本质上是一个函数指针(在C/C++中),或函数引用(在Java/Python中),可以传递给其他函数。
  2. 延迟执行:回调函数通常不会立即执行,而是在某个事件、条件或者另一函数调用的特定时刻执行。
  3. 灵活性:通过回调函数,程序可以在不同的上下文中执行不同的操作,增加了代码的灵活性和复用性。

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 线程池的工作流程

  1. 初始化线程池:启动应用后,线程池会创建一定数量的线程并使它们处于等待状态。
  2. 提交任务:当有任务需要执行时,任务被提交到线程池的任务队列中。
  3. 分配任务:线程池从任务队列中取出任务,并将任务分配给空闲线程执行。
  4. 任务执行:线程从任务队列中取到任务后开始执行,执行完成后线程返回到线程池中继续等待下一个任务。
  5. 重复利用线程:线程池中的线程可以被多个任务重复使用,避免了频繁的创建和销毁操作,降低了系统资源消耗。

46.2 线程池的优势

  • 减少线程创建和销毁的开销:线程创建和销毁是比较耗时的操作,线程池通过重用线程减少了这种开销。
  • 控制并发数:线程池可以通过控制线程的最大数量,防止系统因线程过多而造成资源枯竭。
  • 提高响应速度:当有任务提交时,可以立即使用已有的线程处理任务,无需等待新线程的创建。

46.3 线程池的应用场景

  • 服务器并发处理:如Web服务器、数据库服务器等,需要同时处理大量请求的应用。
  • 并行计算任务:如图像处理、数据分析等需要高并发运算的场景。
  • 后台任务:如日志记录、异步消息处理等不需要立即返回结果的任务。

47 进程和线程的区别

  • 定义:
    • 进程是资源分配的基本单位,进程>线程
    • 线程是进程中的执行单元,是CPU调度和执行的基本单位
  • 资源占用:
    • 每一个进程都有自己独立的地址空间
    • 线程共享进程的地址空间
  • 容错性:
    • 当一个进程出错不会影响到其他进程执行
    • 当一个线程出错可能会导致整个程序崩溃,从而影响到其他线程
  • 调度和切换:
    • 进程是独立的单位,由调度器进行调度和切换,需要恢复的上下文的内容比较多,消耗资源比较多
    • 线程消耗的资源比较少

48 多进程和多线程的适用场景区别

48.1 多线程

  • 优点:
    • 轻量级:线程之间的切换开销小,创建和销毁的开销小
    • 共享内存:线程之间可以轻松交互数据和资源,通信方式比较简单
  • 缺点:
    • 线程安全:会有数据竞争和死锁问题
    • 调试问题:调试困难
  • 适用场景:
    • web服务器,网络服务,用户界面

48.2 多进程

  • 优点:
    • 每一个进程都有自己的内存空间,不同进程之间不会互相影响,安全性和稳定性比较高
    • 适合CPU密集型任务,多进程可以利用多核CPU的能力,提高程序的并行处理能力
  • 缺点:
    • 创建和管理进程的方式开销比较大
    • 通信方式比较复杂
    • 启动时间比较长
  • 适用场景:
    • CPU密集任务,以及需要高度隔离的任务

49 进程间通信方式

进程间通信有几种方式?

  1. 管道
  2. 命名管道
  3. 共享内存(memmap)
  4. 信号量
  5. 消息队列
  6. 套接字(socket)
  7. 信号

哪几种方式需要借助内核?

  • 管道
  • 共享内存(在内核创建的共享内存)
  • 信号量
  • 消息队列
  • 套接字

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 进程有几个状态

  1. 创建状态:当调用fork函数后进入创建状态
  2. 就绪状态:进程已经准备好,但是还没有运行
  3. 运行状态:进程已经获得系统的资源可以运行
  4. 阻塞状态:等待信号量或者互斥量等事件的时候会进入这个状态
  5. 终止状态:进程结束

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 程序分为几个段

  1. 代码段:用于存储程序的可执行指令。只读,防止被篡改。
  2. 数据段(data段):用于存储已经初始化的全局变量和静态变量
  3. BSS段:用于存储没有初始化的全局变量和静态变量
  4. 堆:malloc和free管理的
  5. 栈:存储局部变量,栈的申请和释放是由操作系统来决定的

56 静态链接和动态链接的区别

静态链接(.a)和动态链接(.so)是程序编译过程中两种不同的链接方式,它们主要体现在程序的构建、执行效率、内存使用等方面。

  • 链接时机
    • 静态链接:在编译期间,所有的库和依赖会在生成可执行文件时,一并打包进可执行文件中。链接的内容固定在编译阶段。
    • 动态链接:在程序运行时,库文件才会被加载。可执行文件本身只包含对动态库的引用,实际的代码在程序运行时由操作系统加载。
  • 可执行文件的大小
    • 静态链接:由于库文件已经嵌入可执行文件中,因此生成的可执行文件通常较大
    • 动态链接:可执行文件较小,因为库并未嵌入,而是运行时由操作系统加载
  • 运行时依赖
    • 静态链接:不需要额外的外部库,因为所有依赖都已经打包进了可执行文件,通常可以独立运行
    • 动态链接:需要在运行时找到并加载所依赖的动态库(如.dll、.so文件),如果缺少库,程序将无法运行
  • 更新维护
    • 静态链接:更新库文件需要重新编译整个应用程序。如果库有了新版本,程序不会自动更新,必须手动重新链接和发布。
    • 动态链接:库文件可以独立更新,程序无需重新编译和发布。只要库的接口没有改变,替换或更新库文件后,程序即可使用新版库。
  • 内存使用
    • 静态链接:每个程序都会有一份独立的库代码拷贝,因此多个程序使用相同库时,会重复加载,占用更多的内存。
    • 动态链接:多个程序可以共享同一份库文件,减少内存使用量,因此操作系统可以让多个程序使用同一个动态库。
  • 执行效率
    • 静态链接:启动时不需要加载外部库,通常启动速度稍快。
    • 动态链接:在程序启动时需要加载动态库,因此启动速度可能稍慢一些,但由于动态库通常优化良好,运行时性能差异较小。

57 一个.c文件怎么转换为可执行程序

  1. 预处理:将头文件和宏定义展开,生成没有注释的源代码->.i文件
  2. 编译:将预处理得到的源代码转换为汇编代码->.s文件
  3. 汇编:将汇编代码转换为机器码,生成对应的目标文件->.o文件
  4. 链接:将全部.o文件链接成一个可执行程序

58 交叉编译  

交叉编译指在一个平台上编译出另一个平台的可执行程序。

<think>好的,我现在需要处理用户关于嵌入式系统面试常见问及答案的查询。首先,用户提到了他们之前提供的引用内容,特别是引用[1]关于堆栈和引用[2]关于嵌入式面试八股文的信息。用户希望获取更多相关的常见问及答案,可能涉及八股文类型的目。 首先,我需要确认用户的需求。他们可能正在准备嵌入式系统的面试,需要了解常见的面试以及如何回答。根据引用[2],里面提到了保护现场、中断处理、串口通讯等内容,这些都是嵌入式系统面试中的典型问。因此,我需要围绕这些主展开,整理出常见的问和答案,并确保符合用户提到的八股文格式。 接下来,我需要考虑嵌入式系统面试通常涵盖哪些主。通常包括以下几个方面: 1. 基础概念:如堆栈、中断、DMA等。 2. 实时操作系统(RTOS):任务调度、同步机制等。 3. 硬件相关:GPIO、定时器、ADC/DAC等。 4. 通信协议:UART、SPI、I2C等。 5. 调试与优化:内存管理、代码优化等。 用户提供的引用中提到了保护现场、中断服务程序、串口通讯,这些都是基础概念和通信协议的部分。因此,我应该优先整理这些问,并补充其他常见的问。 然后,我需要确保回答符合用户的要求,即结构清晰、分步解答、真实可靠。每个问需要给出简明扼要的答案,必要时举例或引用技术细节。同时,需要按照用户提供的引用格式,在适当的位置添加引用标识,例如引用[^1]和[^2]。 例如,关于堆栈的问,引用[1]已经提到堆栈是LIFO结构,用于函数调用、递归等。因此,在回答堆栈相关问时,可以引用这点。而中断处理流程在引用[2]中提到保护现场、执行中断程序、恢复现场等步骤,这也需要在相关问的答案中引用。 此外,用户要求生成相关问,因此需要在回答末尾添加几个相关问,引导用户进步探索。这些问应围绕嵌入式面试的常见主,如RTOS、内存管理、看门狗定时器等。 最后,要检查回答是否符合LaTeX格式要求,所有数学表达式用$...$,独立公式用$$...$$,并且正确使用中文。确保没有使用任何Markdown格式,保持自然的中文口语化表达,同时避免使用专业格式如章节标。 总结步骤: 1. 整理嵌入式系统面试常见问,覆盖基础、RTOS、硬件、通信、调试等方面。 2. 根据用户提供的引用内容,补充相关问及答案,适当添加引用标识。 3. 确保回答结构清晰,分点回答,符合用户要求。 4. 生成相关问,促进用户进步探索。 5. 检查格式,确保LaTeX正确,无Markdown,语言自然。</think>### 嵌入式系统面试常见问与答案 #### 、基础概念与机制 1. **堆栈的作用及操作流程** 堆栈是后进先出(LIFO)的数据结构,用于保存函数调用时的返回地址、局部变量和寄存器状态[^1]。 **操作流程**: - 压栈(push):将数据放入栈顶; - 弹栈(pop):从栈顶取出数据。 2. **中断处理流程** 嵌入式系统中,中断处理需遵循以下步骤[^2]: - **保护现场**:保存当前寄存器状态到堆栈; - **执行中断服务程序(ISR)**:处理中断事件; - **恢复现场**:从堆栈恢复寄存器状态; - **返回主程序**:继续执行被中断的代码。 3. **DMA(直接存储器访问)的作用** DMA允许外设与内存直接交换数据,无需CPU干预,提升系统效率。例如,ADC采集数据时可通过DMA传输到内存,减少CPU占用率。 --- #### 二、实时操作系统(RTOS) 4. **任务调度策略** - **优先级抢占式调度**:高优先级任务可抢占低优先级任务的执行权; - **时间片轮转**:相同优先级任务轮流执行。 5. **死锁的避免方法** - 按固定顺序获取资源; - 设置超时机制; - 使用资源计数(如信号量)。 --- #### 三、硬件与外设 6. **GPIO的配置模式** - 输入模式:浮空、上拉、下拉; - 输出模式:推挽、开漏。 7. **看门狗定时器(WDT)的作用** 用于检测系统死锁或异常,若未定期“喂狗”,则触发复位。例如: ```c while(1) { task_run(); feed_watchdog(); // 喂狗操作 } ``` --- #### 四、通信协议 8. **UART如何保证数据完整传输?** - 添加校验位(奇偶校验、CRC); - 使用硬件流控(RTS/CTS)[^2]。 9. **SPI与I2C的区别** - **SPI**:全双工,4线制(SCK, MOSI, MISO, CS),速度更快; - **I2C**:半双工,2线制(SDA, SCL),支持多主从设备。 --- #### 五、调试与优化 10. **内存泄漏的检测方法** - 静态分析工具(如PC-Lint); - 动态跟踪分配/释放记录(如自定义内存管理模块)。 11. **代码优化技巧** - 使用寄存器变量(`register`关键字); - 减少函数调用层级; - 启用编译器优化选项(如`-O2`)。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

趣多多代言人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值