嵌入式面试题

发布时间 2023-10-08 21:54:12作者: 啊唯0o

@

前言

记录常见的嵌入式面试题

C语言

关键字

volatile

volatile是关键字(易变的),是一种类型修饰符,用它修饰的变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其他线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码不再进行优化,从而可以提供对特殊地址的稳定访问。

volatile关键字告诉编译器该变量是随时可能发生变化的,每次使用它的时候必须从内存中取出它的值,因而编译器生成的汇编代码会从原内存地址中读取数据使用。
一般说来,volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
如果一个寄存器或者变量表示一个端口或者多个线程的共享数据,就容易出错,所以volatile可以保证对特殊地址的稳定访问。

static

程序的局部变量存在于(堆栈)中,全局变量存在于(静态区)中,动态申请数据存在于(堆)中。

作用于变量

声明局部变量

局部变量指在代码块{}内部定义的变量,只在代码块内部有效(作用域),其缺省的存储方式是自动变量或说是动态存储的,即指令执行到变量定义处时才给变量分配存储单元,跳出代码块时释放内存单元(生命期)。用static声明局部变量时,则改变变量的存储方式(生命期),使变量成为静态的局部变量,即编译时就为变量分配内存,直到程序退出才释放存储单元。使得该局部变量有记忆功能,可以记忆上次的数据,不过由于仍是局部变量,因而只能在代码块内部使用(作用域不变)。

声明外部变量

外部变量指在所有代码块{}之外定义的变量,它缺省为静态变量,编译时分配内存,程序结束时释放内存单元。同时其作用域很广,整个文件都有效甚至别的文件也能引用它。为了限制某些外部变量的作用域,使其只在本文件中有效,而不能被其他文件引用,可以用static关键字对其作出声明。

作用于函数

使用static用于函数定义时,对函数的连接方式产生影响,使得函数只在本文件内部有效,对其他文件是不可见的。这样的函数又叫作静态函数。
使用静态函数的好处是,不用担心与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。如果想要其他文件可以引用本地函数,则要在函数定义时使用关键字extern,表示该函数是外部函数,可供其他文件调用。另外在要引用别的文件中定义的外部函数的文件中,使用extern声明要用的外部函数即可。

const

用于变量

修饰变量,以下两种定义形式在本质上是一样的。
它的含义是:const修饰的类型为TYPE的变量value是不可变的, readonly。

TYPE const ValueName = value;  
const TYPE ValueName = value;

用于指针

(1)指针本身是常量不可变
    char* const pContent;  
(2)指针所指向的内容是常量不可变
    const char* pContent;
(3)两者都不可变
    const char* const pContent;
(4)还有其中区别方法,沿着*号划一条线:
如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;
如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。

用于函数

a. 传递过来的参数在函数内不可以改变(无意义,因为Var本身就是形参)

void function(const int Var);  无意义

b. 参数指针所指内容为常量不可变

void function(const char* Var); 

c. 参数指针本身为常量不可变(也无意义,因为char* Var也是形参)

void function(char* const Var); 无意义

sizeof

sizeof是在编译阶段处理,且不能被编译为机器码。sizeof的结果等于对象或类型所占的内存字节数,sizeof的返回类型为size_t。

位域

#include<stdio.h>
#include<string.h>  

struct str1 {
    unsigned  char a:7;/*字段a占用了1个字节的7个bit*/
    unsigned  char b:2;/*字段b占用了2个bit*/
    unsigned  char c:7;/*字段c占用了7个bit*/
} s1;

struct str2 {
    unsigned int a:31;
    unsigned int b:2;/*前一个整型变量只剩下1个bit,容不下2个bit,所以只能存放在下一个整型变量*/
    unsigned int c:31;
} s2;

int main(void) {
    printf("%d,%d", sizeof(s1), sizeof(s2));
    return 0;   
}  
//结果等于3,12

位域的好处:

  1. 有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态,用一位二进位即可。这样节省存储空间,而且处理简便。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
  2. 可以很方便的利用位域把一个变量给按位分解。比如只需要4个大小在0到3的随即数,就可以只rand()一次,然后每个位域取2个二进制位即可,省时省空间。

位域的缺点:

  1. 不同系统对位域的处理可能有不同的结果,如位段成员在内存中是从左向右分配的还是从右向左分配的,所以位域的使用不利于程序的可移植性。

字节对齐(#pragma pack)

用来改变编译器的字节对齐方式。常规用法为:

#pragma pack(n)        //编译器的字节对齐方式设为n,n的取值一般为1、2、4、8、16,默认为8
#pragma pack(show)     //以警告信息的方式将当前的字节对齐方式输出
#pragma pack(push)     //将当前的字节对齐方式放入到内部编译器栈中
#pragma pack(push,4)   //将字节对齐方式4放入到内部编译器栈中,并将当前的内存对齐方式设置为4
#pragma pack(pop)      //将内部编译器栈顶的记录弹出,并将其作为当前的内存对齐方式
#pragma pack(pop,4)    //将内部编译器栈顶的记录弹出,并将4作为当前的内存对齐方式

举例如下:结构体里面最大的字节数来安排字节

#include<stdio.h>
#include<string.h>  

#pragma pack(4)  //作用:C编译器将按照n个字节对齐
typedef struct {
    char a;
    int b;
    short c;
} str;
#pragma pack()//作用:取消自定义字节对齐方式。

int main(void) {
    printf("%d", sizeof(str));
    return 0;   
} 
//结果等于12

结构体成员数组大小为0

#include <stdio.h>
#include <string.h>

typedef struct
{
    int a;
    int b;
    char d[1];
    char c[0];//当数组为0,不占用空间
} st1;

int main(void)
{
    printf("%d\n", sizeof(st1));
    return 0;
}
//结果等于12

函数

memset

memset是计算机中C/C++语言函数。将s所指向的某一块内存中的前n个字节的内容全部设置为ch指定的ASCII值,第一个值为指定的内存地址,块的大小由第三个参数指定,这个函数通常为新申请的内存做初始化工作,其返回值为指向s的指针。该函数对数组操作时只能用于数组的置0或-1,其他值无效。

//s所指向的内存 ASCII值块的大小
void *memset(void *s, int ch, size_t n);
//函数解释:将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。
//memset:作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法[1]。

memcpy

memcpy() 用来复制内存,其原型为:

void * memcpy ( void * dest, const void * src, size_t num );

memcpy() 会复制 src 所指的内存内容的前 num 个字节到 dest 所指的内存地址上。
memcpy() 并不关心被复制的数据类型,只是逐字节地进行复制,这给函数的使用带来了很大的灵活性,可以面向任何数据类型进行复制。
需要注意的是:
dest 指针要分配足够的空间,也即大于等于 num 字节的空间。如果没有分配空间,会出现段错误。
dest 和 src 所指的内存空间不能重叠(如果发生了重叠,使用 memmove() 会更加安全)。
与 strcpy() 不同的是,memcpy() 会完整的复制 num 个字节,不会因为遇到“\0”而结束。
返回值: 返回指向 dest 的指针。注意返回的指针类型是 void,使用时一般要进行强制类型转换。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N     (10)
int main() {
    char* p1 = "abcde";//4乘以10
    int a =sizeof (int)*N;//1 乘以10
    char* p2 = (char*)malloc(sizeof(char) * N);
    char* p3 = (char*)memcpy(p2, p1, N);
    printf("p2 = %s\np3 = %s\n", p2, p3);
    printf("a = %d\n", a);
    free(p2);
    p2 = NULL;
    p3 = NULL;
    system("pause");
    return 0;
}
//运行结果:
//p2 = abcde
//p3 = abcde
//a  = 40
/*
代码说明:
1) 代码首先定义p1,p2,p3三个指针,但略有不同,p1指向一个字符串字面值,给p2分配了10个字节的内存空间。
2) 指针p3通过函数memcpy直接指向了指针p2所指向的内存,也就是说指针p2、p3指向了同一块内存。然后打印p2,p3指向的内存值,结果是相同的。
3) 最后按照好的习惯释放p2,并把p3也置为NULL是为了防止再次访问p3指向的内存,导致野指针的发生。
*/

变量

变量的本质:内存中一段空闲的存储空间。
变量是一段实际连续存储空间的别名,程序中通过变量来申请并命名存储空间,通过变量的名字可以使用存储空间。

变量定义写法

  1. 一个整型数:int a;
  2. 一个指向整型数的指针:int *a;
  3. 一个指向指针的指针,它指向的指针是指向一个整型数:int **a;
  4. 一个有10个整型数的数组: int a[10];
  5. 一个有10个指针的数组,该指针是指向一个整型数 int *a[10];
  6. 一个指向函数的指针,该函数有一个整型参数并返回一个整型数:int (*a)(int);
  7. 一个有10个函数指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数。int (*a[10]) (int)

字节长度(32位系统)

  • char     出来的内存大小是1 个字节。
  • short    出来的内存大小是2 个字节;
  • int      出来的内存大小是4 个字节;
  • long     出来的内存大小是4 个字节;
  • float    出来的内存大小是4 个字节;
  • double   出来的内存大小是8 个字节;
    一个字节=8个二进制位=2个十六进制位

编译

预编译

预编译又称预处理,是整个编译过程最先做的工作,即程序执行前的一些预处理工作,主要处理#开头的指令。如拷贝#include包含的文件代码,替换#denfine定义的宏,条件编译#if等。
何时需要预编译:

  1. 总是使用不经常改动的大型代码体。
  2. 程序由多个模块组成,所有模块都是用一组标准的包含文件和相同的编译选项。在这种情况下,可以将所有包含文件预编译为一个预编译头。

如何避免头文件被重复包含

例如:为避免头文件my_head.h被重复包含,可在其中使用条件编译:

#ifndef _MY_HEAD_H
#define _MY_HEAD_H    /*空宏*/
/*其他语句*/
#endif

#与##的作用

‘#’符号会将宏的参数进行字符串字面量化,并且加""号,
’##’是把两个宏参数连接的运算符。

#define STR(y)     (#y)                 //则宏STR(1)展开时为”1”
#define NUM(y)     (2##y)               //则宏NAME(1)展开为21

内存

程序的内存分配

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

堆栈溢出一般是由什么原因导致的

1. 递归调用: 如果一个函数无限递归调用自身或递归层次太深,堆栈会不断增长,最终导致堆栈溢出。
2. 大数据结构的局部变量: 如果在函数内部定义了大型的局部变量数组,这些变量会存储在堆栈中,如果太大,可能会导致堆栈不够用而溢出。
3. 动态申请空间使用后没有释放: 如果在申请空间使用后没有释放,会导致内存占用越来越多,最终导致堆栈溢出。

指针

指针有什么好处

  • 动态分配的数组
  • 对多个相似变量的一般访问
  • 各种动态分配的数据结构,尤其是树和链表
  • 遍历数组(例如,解析字符串)
  • 高效地,按引用“复制”数组和结构,特别是作为函数参数的时候。
  • 还有其他等等。

其他

assert

断言assert是一个宏,其原型定义在<assert.h>中。当使用assert时候,给他个参数,即一个判读为真的表达式。其作用是如果它的条件返回错误,则终止程序执行,原型定义:

#include <assert.h>
void assert( int expression );

我们一般可以用在判断某件操作是否成功上。
程序一般分为Debug版本和Release版本,Debug版本用于内部调试,Release版本发行给用户使用。
断言assert是仅在Debug版本起作用的宏,它用于检查“不应该”发生的情况。
以下是一个内存复制程序,在运行过程中,如果assert的参数为假,那么程序就会中止(一般地还会出现提示对话,说明在什么地方引发了assert)。

void memcpy(void *pvTo, void *pvFrom, size_t size)
{
    void *pbTo = (byte*) pvTo;
    void *pbFrom = (byte*) pvFrom;
    assert(pvTo != NULL && pvFrom != NULL);
    while(size-- > 0) {
        *pbTo++ = *pbFrom++;
    }
    return (pvTo);
}

assert不是一个仓促拼凑起来的宏,为了不在程序的Debug版本和Release版本引起差别,assert不应该产生任何副作用。所以assert不是函数,而是宏。
程序员可以把assert看成一个在任何系统状态下都可以安全使用的无害测试手段。
以下是使用断言的几个原则:

  1. 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
  2. 使用断言对函数的参数进行确认。
  3. 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。
  4. 一般教科书都鼓励程序员们进行防错性的程序设计,但要记住这种编程风格会隐瞒错误。当进行防错性编程时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。