嵌入式面试总结(持续更新)

Posted Jocelin47

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了嵌入式面试总结(持续更新)相关的知识,希望对你有一定的参考价值。

文章目录

1、 字符串数组打印(指针的步长)

1.1 指针变量+1

char *p = NULL;
printf("%d\\n",p); // 0
printf("%d\\n",p+1); // 1

int *p2 = NULL;
printf("%d\\n",p2);  // 0 
printf("%d\\n",p2+1);  // 4

1.2 字符串数组的步长

main() 
  
 char *str[]="ab","cd","ef","gh","ij","kl"; 
 char *t; 
 t=(str+4)[-1]; 
 printf("%s",t); 
 

则显示"gh"

为什么呢:

首先要知道存放的是char* 类型的数组,所以str + 4 也就是数组的第5个元素:“ij”,最后得到的是第5个元素的首地址,我们在去数组的[-1]索引也就是“gh”的首地址,最终打印的就是gh

1.3 跨行加⭐⭐⭐⭐⭐

main()

	//例子[1]
    int a[5]=1,2,3,4,5;
    int *ptr=(int *)(&a+1);//&a相当于变成了行指针,加1则变成了下一行首地址
    printf("%d,%d,%d",*(a+1),*(ptr-1));
    //例子[2]
    int * ptr1 = (int *)( (int)a + 1);
    int * ptr2 = (int *)( (int)a + 4);
    printf("%d,%d\\n", ptr[-1],*ptr2);

例子[1]

1. *(a+1)就是a[1],执行结果是2
	因为a是int*类型,a+1步长为4
2.*(ptr-1)就是a[4],结果为5
    首先我们得到的是&a的地址,而&a是一个含有5个int类型的数组,所以&a+1的步长就是a数组整个的大小,加到a数组的末尾后面,
    (int *)(&a+1)这一句话,把它转换成int *类型的指针,步长又为4了,后面给它-1,即*(ptr-1) 相当于减了一个int* 的步长,结果为5

例子[2]

首先看里面的语句  (int)a + 1 、(int)a + 4
这个意思是我们把a的地址得到,然后把a的地址+1和+4,
那么ptr1肯定是一个乱的值,因为取的是不对的地址
而ptr2得到的是数组第二个元素的地址,再转换成(int *)类型
这样我们又可以进行后续的+-操作进行指针的引用了。

2、大端小端

小端:低位字节数据存储在低地址
大端:高位字节数据存储在低地址
例如:int a=0x12345678;(a首地址为0x2000)
0x2000 0x2001 0x2002 0x2003
0x12 0x34 0x56 0x78 大端格式

3、异步IO和同步IO区别

如果是同步IO,当一个IO操作执行时,应用程序必须等待,直到此IO执行完,相反,异步IO操作在后台运行,
IO操作和应用程序可以同时运行,提高系统性能,提高IO流量; 在同步文件IO中,线程启动一个IO操作然后就立即进入等待状态,直到IO操作完成后才醒来继续执行,而异步文件IO中,
线程发送一个IO请求到内核,然后继续处理其他事情,内核完成IO请求后,将会通知线程IO操作完成了。

4、变量a的不同定义

一个整型数 int a;
一个指向整型数的指针 int *a;
一个指向指针的指针,它指向的指针式指向一个整型数 int **a;
一个有10个整型数的数组 int a[10];
一个有10指针的数组,该指针是指向一个整型数 int *a[10];
一个指向有10个整型数数组的指针 int (*a)[10];
一个指向函数的指针,该函数有一个整型数参数并返回一个整型数 int ( *a)(int);

一个有10个指针的数组,该指针指向一个函数,该函数有一个整型数参数并返回一个整型 int (*a[10])(int);

5、关于char越界的数值

int foo(void)

 int i;
 char c=0x80;
 i=c;
 if(i>0)
 	return 1;
 return 2;

返回值为2;因为i=c=-128;如果c=0x7f,则i=c=127

6、利用移位、与实现模

a=b*2;a=b/4;a=b%8;a=b/8*8+b%4;a=b*15;实现效率最高的算法

     a=b*2 -> a=b<<1;
     a=b/4 -> a=b>>2;
     a=b%8 -> a=b&7;
     a=b/8*8+b%4 -> a=((b>>3)<<3)+(b&3)
     a=b*15 -> a=(b<<4)-b

7、无符号与有符号相加结果为无符号类型

int main(void)
 
  unsigned int a = 6;
  int b = -20;
  char c;
  (a+b>6)?(c=1):(c=0);
 

c=1,但a+b=-14;如果a为int类型则c=0。
原来有符号数和无符号数进行比较运算时(==,<,>,<=,>=),有符号数隐式转换成了无符号数(即底层的补码不变,但是此数从有符号数变成了无符号数),
比如上面 (a+b)>6这个比较运算,a+b=-14,-14的补码为1111111111110010。此数进行比较运算时, 被当成了无符号数,它远远大于6,所以得到上述结果。

如果a = 1,b = -2 结果为2^32 -1

8、实现某一位置0或置1操作,保持其它位不变

 #define BIT3 (0x1<<3)
 static int a;
 void set_bit3(void)
 
	 a |= BIT3;
 
 void clear_bit3(void)
 
	 a &= ~BIT3;
 
 实现多位置1与置0
 
 a &= ~( 1 << 3 | 1 << 4); //置0
 a |= (1<< 3 | 1 << 4); //置1

9、设置一绝对地址为0x67a9的整型变量的值为0xaa66

int *ptr;
  ptr = (int *)0x67a9;
  *ptr = 0xaa66;(建议用这种)

一个较晦涩的方法是:
*(int * const)(0x67a9) = 0xaa66;

10、中断函数中的注意问题

__interrupt void compute_area (void) 
  
   double area = PI * radius * radius; 
   printf(" Area = %f", area); 
   return area; 
  

1、 ISR不可能有参数和返回值的!
2、 ISR尽量不要使用浮点数处理程序,浮点数的处理程序一般来说是不可重入的,而且是消耗大量CPU时间的!!

10.1 什么是不可重入函数

  • 函数体内使用了静态(static)的数据结构;

  • 函数体内调用了 malloc() 或者 free() 函数;

  • 函数体内调用了标准 I/O 函数;

    printf函数一般也是不可重入的,UART属于低速设备,printf函数同样面临大量消耗CPU时间的问题!

不可重入函数在实现时候通常使用了全局的资源,在多线程的环境下,如果没有很好的处理数据保护和互斥访问,就会发生错误,常见的不可重入函数有:

  • printf --------引用全局变量stdout

  • malloc --------全局内存分配表

  • free --------全局内存分配表

  • 满足下列条件的函数多数是不可重入的:

    (1)函数体内使用了静态的数据结构;

    (2)函数体内调用了malloc()或者free()函数;

    (3)函数体内调用了标准I/O函数。

10.2 如何写出可重入的函数?⭐⭐⭐⭐⭐

  • 在函数体内不访问那些全局变量;
  • 如果必须访问全局变量,记住利用互斥信号量来保护全局变量。或者调用该函数前关中断,调用后再开中断;
  • 不使用静态局部变量;
  • 坚持只使用缺省态(auto)局部变量;
  • 在和硬件发生交互的时候,切记关闭硬件中断。完成交互记得打开中断,在有些系列上,这叫做“进入/退出核心”或者用 OS_ENTER_KERNAL/OS_EXIT_KERNAL 来描述;
  • 不能调用任何不可重入的函数;
  • 谨慎使用堆栈。最好先在使用前先 OS_ENTER_KERNAL;

11、malloc内存分配

11.1malloc申请大小问题

#include <stdio.h>
#include <malloc.h>
int main()

        char *ptr;
        if((ptr = (char *)malloc(0)) == NULL)
                puts("got a null pointer\\n");
        else
                puts("got a valid pointer\\n");

        int a =  malloc_usable_size(ptr);
        printf("size = %d\\n", a);
        return 0;

malloc申请一段长度为0的空间,malloc依然会返回一段地址,还有一段地址空间,所以ptr不等于NULL。
malloc这个函数,会有一个阈值,申请小于这个阈值的空间,那么会返回这个阈值大小的空间。
如阈值为24,那么申请小于24的值就会返回24

结果如下图所示

这个阈值会随着编译器的不同而不同

如果申请一个负数,那么返回的是0,如下图

这是因为malloc规定不可以申请一个负数

参考博客

11.2 malloc底层实现原理

1)当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)

2)当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。

具体分析

从操作系统角度看,进程分配内存有两种方式,分别由两个系统调用完成:brk 和 mmap (不考虑共享内存)
这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

1)brk 是将数据段(.data)的最高地址指针 _edata 往高地址推

将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图:

1,进程启动的时候,其(虚拟)内存空间的初始布局如图1所示

2,进程调用A=malloc(30K)以后,内存空间如图2:

malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配

你可能会问:难道这样就完成内存分配了?

事实是:_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的

3,进程调用B=malloc(40K)以后,内存空间如图3

2)mmap 是在进程的虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空闲的虚拟内存。


4,进程调用C=malloc(200K)以后,内存空间如图4

默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存

这样子做主要是因为:

brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,因为只有一个_edata 指针,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放。

当然,还有其它的好处,也有坏处,再具体下去,有兴趣的同学可以去看glibc里面malloc的代码了。

5,进程调用D=malloc(100K)以后,内存空间如图5

6,进程调用free( C )以后,C对应的虚拟内存和物理内存一起释放

7,进程调用free(B)以后,如图7所示

B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢?当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了
  这里是因为
:malloc是从堆里面申请内存,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

8,进程调用free(D)以后,如图8所示

B和D连接起来,变成一块140K的空闲内存

9,默认情况下:

当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图9所示

参考博客:
malloc底层实现及原理
linux malloc内存申请相关参数设置

12、变量全置0与全置1

unsigned int zero = 0;
unsigned int compzero = 0xFFFF;

对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:
unsigned int compzero = ~0; //这样才是全是置位1

13、你真的了解数组吗?

数组作为形参声明

int add(int a[])
int add[int a[][]] //错误的
int add(int a[][4])

声明一个指向N维数组的指针时,只能省略最左边方括号中的值。
因为第一对方括号只用于表明这是一个指针,而其他方括号则用于描述所指向数据对象的类型。

//等价的写法
int a[][3][4][5];
int (*a)[3][4][5];

变长数组

int sum(int ar[][col]int  row)


可以输入不同的row行的值,表明输入的行有多少,不用一开始就确定了大小

字符串与字符数组

char str[5] = 'H' , 'E' ,'L' , 'L', 'O' ;//字符数组
char str[5] = 'H' , 'E' ,'L' , 'L', '\\0' ;//字符串
char str[] = "hello";
sizeof(str) = 6
strlen(str) = 5
char src[5] = "hello"; //编译器会报错,还需要一个空间存放\\0",需要改成6

数组名不可以++,指针可以

char head[] = “I love”;
const char *phead = “I love”;

*(phead++)可以,但是 * (head++)错误的,可以 * ( head + 1)。因为head是常量,而phead是变量

字符串的地址

const char* a = "abc";
const char* b = "abc";
printf("%s  %p  %p\\n",a,&a,a);
printf("%s  %p  %p\\n",b,&b,b);

结果

abc  0x7fffc8fd1920  0x4006e4
abc  0x7fffc8fd1928  0x4006e4

表明a和b的地址是不同的,但是他们里面指向的内容是一样的。

14、写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个

#define Min(a,b) (a) <= (b) ? (a) : (b)

15、说明关键字volatile有什么含意,并给出例子

(深入理解计算机操作系统P536页)

volatile表示被修饰的符号是易变的。告诉编译器不要随便优化我的代码!!

简要的说法:volatile关键字定义一个 变量的时候,是需要告诉编译器不要缓存这个变量到寄存器,每次从内存中读取该变量的值。可以应用在外围设备的特殊功能寄存器或者需要一个进程需要利用该变量判断条件,另一个进程改变该变量的状态。

15.1 外围设备的特殊功能寄存器

在嵌入式偏硬件方面的程序,我们经常要控制一些外围硬件设备,就拿I/O端口来说,我们会去操作映射到对应IO端口的寄存器。假设某一个寄存器的地址为0x1234,在C语言中,我们可以定义一个指针pRegister指向这个地址:

unsigned int *pRegister = (unsigned int *)0x1234;

实际应用中:我们经常会去判断一个寄存器中的值(或者寄存器中某一位)为‘0’还是‘1’。例如下面程序:

unsigned int *pRegister = (unsigned int *)0x1234;  

//wait  
while(*pRegister == 0)
    //不改变*pRegister的值
  

//Code...  

我们的代码目的是不断的判断 *pRegister的值是否为‘0’。如果 *pRegister的值(值由硬件改变)在中途变为‘1’,则跳出死循环。
因为上面的循环中,*pRegister的值并没有发生改变,因为我们的编译器会对上述代码进行优化,如下:

unsigned int *pRegister = (unsigned int *)0x1234;  

//wait  
if (*pRegister == 0)
    while(1)
        //不改变*pRegister的值
      


//Code... 

经过优化后,在上面的循环中,*pRegister的值不会发生改变,所以循环中就不再判断 *pRegister的值了,运行效率提升。但是pRegister指向的特殊功能寄存器,其值是由硬件改变的,而软件却不再判断 *pRegister的值了,那么就进入死循环了,即使 *pRegister的值发生了改变,软件也察觉不到了。

15.2 在中断服务函数中修改全局变量

static int flag = 1;  

void main(void)  

   while (flag == 1)  
       //code ...  
     
   //code ...  
  

void do_interrupt(void)  //中断服务程序
   //code...  
   flag = 0;  
  

中断程序中修改了全局变量,上面的代码简单,只要flag的值为‘1’,就会一直运行循环里面的程序。刚才我们已经讲了,因为flag值在循环里没有改变,编译器就将对其优化。如下:

static int flag = 1;  

void main(void)  

   if (flag == 1)  
       while (1)  
           //code ...   
         
     
   //code ...   
  

void do_interrupt(void)  
   //code...  
   flag = 0;  
  

15.3 线程之间共享变量(在多线程中修改全局变量)

int  cnt;  

void task1(void)  
    cnt = 0;  
    while (cnt == 0)   
        sleep(1);  
      
  

void task2(void)  
    cnt++;  
    sleep(10);  
  

同理对while进行了优化

15.4 volatile的顺序性

对于两个volatile的变量在一起的时候编译器不会优化他们的顺序
而一个volatile和非volatile在一起的时候可能会改变他们的执行顺序。

16、位反转

实现一个8位数据反转

位 8  7  6  5  4  3  2  1
数 v8 v7 v6 v5 v4 v3 v2  v1

转换后:
 位 8  7  6  5  4  3  2  1
数 v1 v2 v3 v4 v5  v6 v7 v8
unsigned char bit_reverse(unsigned char c)

	unsigned char buf;
	int bit = 8;
	while(bit)
	
		bit--;//最后需要移动0位,所以要先bit--,最后才能到0,如果在后面最后0的时候就会退出了
		buf |= ( (c&1) << bit);
		c >>= 1;
	
	return buf;

17、字符串翻转

18、引用和指针的区别

(1). 指针是一个实体,而引用仅是个别名;
(2). 引用使用时无需解引用(*),指针需要解引用;
(3). 引用只能在定义时被初始化一次,之后不可变;指针可变;
(4). 引用没有 const,指针有 const,const 的指针不可变;
(5). 引用不能为空,指针可以为空;
(6). “sizeof 引用”得到的是所指向的变量(对象)的大小,
而“sizeof指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
(7). 指针和引用的自增(++)运算意义不一样;

19、-1,2,7,28,126请问28和126中间那个数是什么?为什么?

答案应该是4^3-1=63 规律是n3-1(当n为偶数0,2,4)n3+1(当n为奇数1,3,5)

20、一级指针无效传参

先看这个例子:

进入test02后栈上给s分配了一个空间,然后进入getstring,首先常量区给helloworld开辟了空间,然后str这个局部变量,在栈上也存放了helloworld内容,最后返回str指针的地址0x002,但是栈区的空间在执行完getstring后已经被回收了,所以打印的是乱码的东西。

答:结果可能是乱码。因为getstring返回的是指向“栈内存”的指针,该指针的地址不是 NULL,
但其原现的内容已经被清除,新内容不可知。

#include<stdio.h>
#include<memory.h>

void allo(char *p)

    p = malloc(100);
    memset(p,0,100);
    strcpy(p,"hello word");


int main()

    char *p = NULL;
    allo(p);
    printf("%d\\n",p);
	system("pause");
    return 0;

结果为0;

同理这里进入allo函数后函数为char *p形参在栈上分配空间,然后p指向堆中一块内存,再把helloworld内容拷到p指向内存的地址,但是这个函数执行完后,p内容处的内容就释放掉了,并不会打印。

解决方法:改用二级指针或者返回char*

21、写出float x 与“零值”比较的if语句

if(x>0.000001&&x<-0.000001)

22、修改野指针

void Test(void)
 
     char *str = (char *) malloc(100);
     strcpy(str, “hello”);
     free(str);     
     if(str != NULL)
     
         strcpy(str, “world”); 
         printf(str);
     
 

篡改动态内存区的内容,后果难以预料,非常危险。因为free(str);之后,str成为野指针, if(str != NULL)语句不起作用。

野指针不是NULL指针,是指向被释放的或者访问受限内存指针。

造成原因:指针变量没有被初始化,任何刚创建的指针不会自动成为NULL;
指针被free或delete之后,没有置NULL;
指针操作超越了变量的作用范围,比如要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

23、void* 类型

任何数据都是有类型的,告诉编译器分配多少内存

void val; //错误的

但是void* 可以 ,四个字节

void* 是所有类型指针的祖宗

int * pInt = NULL;
char *PChar = pInt;

从int* 到char* 系统会警告,可以使用类型转换:

int * pInt = NULL;
char *PChar = (char *)pInt;

或者用void * 接收

void * pVoid = pInt;

任何类型的指针,都可以不经过强制转换。转换成void* 类型,所以可以理解为void* 是所有类型指针的祖宗。

  • void * 主要用于数据结构的封装

  • 在取 指针的内容的时候,比如int *p,或者char *cp 可以取地址的内容 * cp ,但是如果是一个void *类型是不可以取内容的,因为未知目标类型。

  • void * 可以作为函数的返回值,函数中接收void * 返回值类型的函数返回的值时,需要对数据进行强制类型转换

24、sizeof的返回类型

(0)sizeof是操作符,sizeof测量的实体大小在编译阶段就已经确定

(1)sizeof返回的是unsigned int 所以

sizeof(int) - 5 其实结果是 > 0的 

(2)当数组作为参数传入的时候,退化为首元素的指针

结果为28,4

25、typedef的作用

26、浮点数

首先我们如何表示一个浮点数,例子:

1011.01

8 4 2 1 . 0.5 0.25 按照这个规则,得到上面二进制的结果为11.25

单精度一共32位,我们也可以按照这个规律去定义

其中最高位为1为S符号位,中间8位为指数(E),表示小数点在第几位上,后面23位小数(M)表示二进制

S|->8位<-||->23位<-|

结果为:

浮点数:0X41360000

0100 0001 0011 0110 000 0 0000 0000 0000

M = 011 0110 0000 0000 0000 0000 ,E = 100 0001 0

  1. M * 2 ^( E-127) = 1.0110110 * 2^3 相当于小数点往后移动3位 —> 1011.0110 = 11.375

double类型:

27、i++操作

27.1 i++的操作顺序

int a = 5, b = 7, c;
  c = a+++b;

c = 12;

++优先级比+高,所以先++后+

27.2 可以 &(i++) 吗?

为什么见:34

28、什么是左值与右值

左值就是出现在表达式左边的值(等号左边),可以被改变,他是存储数据值的那块内存的地址,也称为变量的地址;

右值是指存储在某内存地址中的数据,也称为变量的数据。

左值可以作为右值,但右值不可以是左值。

因此也只有左值才能被取地址。

一句话概括就是:左值就是可以被寻址的值

28.1 举例:

int i = 0;

(i++)+=i; //错误

(++i)+=i; //正确 

int *ip = &(i++); //错误

int *ip = &(++i); //正确

28.2 i++为什么不能作为左值

我们来看i++和i++的实现

// 前缀形式:
int& int::operator++() //这里返回的是一个引用形式,就是说函数返回值也可以作为一个左值使用
//函数本身无参,意味着是在自身空间内增加1的
  *this += 1;  // 增加
  return *this;  // 取回值

//后缀形式:
const int int::operator++(int) //函数返回值是一个非左值型的,与前缀形式的差别所在。
//函数带参,说明有另外的空间开辟
  int oldValue = *this;  // 取回值
  ++(*this);  // 增加
  return oldValue;  // 返回被取回的值

简单得到理解,就是i++返回的是一个临时变量,函数返回后不能被寻址得到,它只是一个数据值,而非地址,因此不能作为左值。

更简单的代码解释

// i++:

int tmp;
tmp=i;
i=i+1;
return tmp;

 
// ++i:

i=i+1;
return i;

29、union、struct结构体对齐总结(超全)

29.1 union对齐

对齐规则:
1、占用的内存空间大小需要是结构体中占用最大内存空间的类型的整数倍
2、共用内存大小

例子1:

union example   
    int a[5];  
    char b;  
    double c;  
;  
int result = sizeof(exampleAndroid面试题最全总结系列 (持续更新中...)

Android面试题最全总结系列 (持续更新中...)

Java面试复习体系总结(2021版,持续更新)

SpringCloud面试题及答案 300道,springcloud面试题总结 (持续更新)

史上最全Hashmap面试总结,51道附带答案,持续更新中...

史上最全Hashmap面试总结,51道附带答案,持续更新中...