嵌入式面试总结

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申请大小问题

#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规定不可以申请一个负数

参考博客

12、变量全置0与全置1

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

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

13、实现strcpy

已知strcpy函数的原型是:

char * strcpy(char * strDest,const char * strSrc);

1.不调用库函数,实现strcpy函数。

2.解释为什么要返回char *。

13.1 代码实现

char * strcpy(char * strDest,const char * strSrc)
   
{
    if ((NULL==strDest) || (NULL==strSrc)) 
    //[1]
    throw "Invalid argument(s)"; 
    //[2]
    char * strDestCopy = strDest; 
    //[3]
    while ((*strDest++=*strSrc++)!='\\0'); 
    //[4]
    return strDestCopy;
}

错误的做法[1]:

(A) 不检查指针的有效性,说明答题者不注重代码的健壮性。

(B) 检查指针的有效性时使用((!strDest)||(! strSrc))或(!(strDest&&strSrc)),说明答题者对C语言中类型的隐式转换没有深刻认识。在本例中char *转换为bool即是类型隐式转换,这种功能虽然灵活,但更多的是导致出错概率增大和维护成本升高。所以C++专门增加了bool、true、false三个关键字以提供更安全的条件表达式

© 检查指针的有效性时使用((strDest0)||(strSrc0)),说明答题者不知道使用常量的好处。直接使用字面常量(如本例中的0)会减少程序的可维护性。0虽然简单,但程序中可能出现很多处对指针的检查,万一出现笔误,编译器不能发现,生成的程序内含逻辑错误,很难排除。而使用NULL代替0,如果出现拼写错误,编译器就会检查出来。

错误的做法[3]:

(A)忘记保存原始的strDest值,说明答题者逻辑思维不严密。

错误的做法[4]:

(A)循环写成while ( * strDestCopy++ = * strSrc++);,同[1](B)。

(B)循环写成while (*strSrc!=’\\0’) * strDest++ = * strSrc++;,说明答题者对边界条件的检查不力。循环体结束后,strDest字符串的末尾没有正确地加上’\\0’。

13.2 strcpy能把strSrc的内容复制到strDest,为什么还要char *类型的返回值?

返回strDest的原始值使函数能够支持链式表达式,增加了函数的“附加值”。同样功能的函数,如果能合理地提高的可用性,自然就更加理想。

​ 链式表达式的形式如:

​ int iLength=strlen(strcpy(strA,strB));

​ 又如:

​ char * strA=strcpy(new char[10],strB);

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

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

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

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

15.1 实现硬件寄存器

15.2 中断中用到的变量

15.3 线程之间共享变量

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*

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iBEf3cfN-1623580014556)(C:\\Users\\Jocelin\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607214257340.png)]

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 * 主要用于数据结构的封装

24、sizeof的返回类型

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

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FXNk6tXP-1623580014560)(H:\\software\\有道云本地文件\\weixinobU7VjhbDPCdu5duX0DmyyhWb5gE\\c901ab43046f498692cfc30ec3c6d5c2\\clipboard.png)]

结果为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类型:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Urc45oOh-1623580014564)(C:\\Users\\Jocelin\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210608200505511.png)]

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;
}

C++部分

1、C++异常

抛出异常throw 和

捕获异常try catch

try 块中的代码有可能抛出异常。它后面通常跟着一个或多个catch 块在想要处理问题的地方,通过异常处理程序捕获异常

foo()
{
	//[1]
	...
	...
	.A.
	.B.
	//[2]
	throw
	//[3]
	...
	...
	...
	//[4]
}

如果在throw处发生了异常,会干两件事:

1、程序首先会回到foo()函数的调用处,[3]底下的代码都不去运行了,

2、上面[1] - [2]之前的代码都会进行清理,一个回转的操作,如果调用了A或B,则回去调用A和B的析构函数,避免资源泄露 也可以理解成代码调转到[4]上结束运行这个函数,栈上的数据都销毁把A和B进行析构。

代码示例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aAzNIL3I-1623580014566)(C:\\Users\\Jocelin\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210608170158193.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-At558k52-1623580014569)(C:\\Users\\Jocelin\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210608170218948.png)]

通过抛出异常返回一个 字符串"my exception" 在catch中捕捉到了这个字符串打印输出。

不可以滥用异常,销毁数据会造成性能消耗!

只有你实在没有办法解决的时候,比如一个除法,分母为0的时候,我不知道该怎么,我不能返回0和-1,因为结果并不是0和-1,我就可以抛出一个异常 const *str的字符串,并且你可以catch这个const *str

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ImABj2QQ-1623580014571)(C:\\Users\\Jocelin\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210608165759377.png)]

2、引用

2.1 什么是引用?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qcTPzlju-1623580014573)(C:\\Users\\Jocelin\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210608201006077.png)]

别名:同样的内存地址

2.2 引用 VS 指针

引用的内存意义:同样的内存地址!!!

而指针是int i = 5;

int * p = & i;

明显需要开辟一个新的地址存放 i 的地址,指向 i

主要有三个不同:

1、不存在空引用!引用必须连接到一块合法的地址

2、一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。

3、引用必须在创建时被初始化。指针可以在任何时间被初始化。

2.3 函数传参用到引用

foo( int a,int *b ,int& c)
{
	cout << a << b << c << endl;
}

main()
{
	//[1]
	int a = 5;
	int *b = & a;
	int &c = a;
	//[2]
	foo();
	return 0;  
}

执行到[1] - [2]之间的时候,堆栈上为 a ,b分配空间,c指向a ,运行到foo()后,堆栈会继续分配一个a的空间,以及一个b的空间,而c并不占空堆栈空间,直接指向了第一次堆栈上为a分配空间的地址2000上

​ 堆栈:

2000a
1996b
1992a
1988b

2.4 引用的优点和需要遵守的规则

将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?
好处:在内存中不产生被返回值的副本
注意:
1.不能返回局部变量的引用,主要是局部变量会在函数返回后被销毁,因此被返回的引用就成了无所指的引用,程序会进入未知状态
2.不能返回函数内部new分配的内存的引用(容易造成内存泄漏)

string& foo()
{
        string* str = new string("abc");
        return *str; 
}
void main()
{
	//[1] 不会内存泄漏
    string& str1=foo();
    delete &str1;
    //[2] 内存泄漏
    string str = foo();
    
}

​ str1是局部变量、出了作用域就没了。你申请的内存还在、但是“载体”没了。引用是“载体”的引用、 它本身不是“载体”

3.可以返回类成员的引用,但最好是const

3、 C++函数中的默认值

foo( int a,int *b ,int& c,int d = 200)
{
	cout << a << b << c << endl;
}
main()
{
	int a = 5;
	int *b = & a;
	int &c = a;
	foo(a,b,c); //正确
	foo(100,b, c,200); //错误,默认值只能放在最后面,几个都可以,但是不能放在第一个
}

4、register存储类

register定义存储在寄存器中而不是内存中的局部变量(只是希望而已)

然而,完全取决于编译器!

Best Practice:主要为了优化性能。注意,不要过早优化不要因为C++可以做什么就做什么!

优化什么呢:我想频繁的使用这个变量,从寄存器里面读肯定会快!

但是也有问题:

register int b;
int * a = & b;

这个时候b肯定不能放在寄存器里面,因为你想取地址b,寄存器哪有地址,编译器就不会这么做。

5、内联函数inline

如何一个函数比较小巧,经常调用,可以减少函数调用用到堆栈的开销

优点:

  • 没有了调用的开销,效率也很高。
  • 编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确
  • 然后进行一系列的相关检查,就像对待任何一个真正的函数一样
  • 这样就消除了它的隐患和局限性。(宏替换不会检查参数类型,安全隐患较大:对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患。)
  • 可以作为一个类的成员函数,与类的普通成员函数作用相同

缺点:

  • 内联函数的函数体一般来说不能太大,如果内联函数的函数体过大,一般的编译器会放弃内联方式
  • 如果函数体变大的话,会造成很多的cache miss 发生具体原理可以去看一下。

6、const和指针

C++之父推荐的读法是:从右往左读

6.1 char * const cp;

//example 1:
char * const cp;
// cp is cosnt pointer to char 说明他是一个cosnt指针,指向的内容为char
// 因此它的指针指向是不可以更改的,如果 cp = &x;
//试图将 cp的指针指向x地址这是不可以的,因为它不可以修改。

6.2 const char * p;

//example 2:
const char * p
// cp is pointer to cosnt char 说明他是一个指针,指向的内容为 const char
// 因此它的指针指向的内容是不可以更改的,如果 *p = x;
//试图将 p的指针指向的内容进行修改这是不可以的,因为它不可以修改。

6.3 char const * p;

char const跟 const char是一样的

//example 3:
const char * p
// cp is a pointer to char that is cosnt  说明他是一个指针,指向的内容为 char const
// 因此它的指针指向的内容是不可以更改的,如果 *p = x;
//试图将 p的指针指向的内容进行修改这是不可以的,因为它不可以修改。

6.4 char const * const p;

//example 3:
const char * p
// cp is a const pointer to cosnt char  
//说明他是一个cosnt指针,指向的内容为 const char

7、构造函数

1、一个构造函数包含构造函数和析构函数这是最基本的

2、构造函数没有返回值,构造函数初始化成员在后面用:给成员初始化(使用初始化列表的构造函数是显式的初始化类的成员),而不是{ sz = size};在构造函数里面,因为如果这个数据类型比较大的话,开销就大了。

class Vector{

private:
	size_t sz; // size_t = unsigned int
	double *elem;
	Vector(size_t size) 
		: sz(size). 
		  elem(new double[sz])
	{
	
	}
public:
	Vector(initializer_list<double> lst);	//类内声明
	double& operator[](int i){
		return elem[i];	
	}
	
	size_t size(){
		return sz;
	}
	~Vector(){};
}

Vector::Vector(initializer_list<double> lst) //可以类内声明,类外定义函数
	:elem(new double[lst.size()])
{
	std::copy(lst.begin(),lst.end(),elem);
}

3、构造函数可以有很多个,但是会有一个默认的构造函数

Vector(){}

explicit构造函数

解决隐式类型转换问题

Vector v1 = 7; // OK ,V1 有7个元素

如果写成explicit Vector(int s); // no implicit  不可以隐式的
Vector v1(7); // OK ,V1 有7个元素
Vector v1 = 7; // error,不可以隐式转换

initializer_list的用法

Vector(initializer_list<double> lst)
	:elem(new double[lst.size()])
{
	std::copy(lst.begin(),lst.end(),elem);
}

Vector v2 {1,2,3,4,5}; //写一个这样的构造函数就可以实现使用列表初始化

8、拷贝构造函数

接着7中的例子:

8.1 浅拷贝

void bad_copy*(Vector v1)
{
	Vector v2 = v1;
	v1[0] = 2;
	v1[1] = 3;
	v2[0] = 10;
	v2[1] = 20; //此时你以为v1 v2是两个不同的东西,实际上是两个相同的东西
}

此时v2 = v1;

在栈中v2和v1分别在两个地方保存, v1中st和v2中的st存在于不同的地址空间,但是指针他们指向的确实同一块空间。

8.2 实现拷贝构造函数

classname (const classname &obj){...}
//在...中实现自己的拷贝构造函数
  • 通过使用另一个同类型的对象来初始化新创建的对象
  • 复制对象把它作为参数传递给函数
  • 复制对象,并从函数返回这个对象
  • 如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数⭐⭐⭐⭐⭐

如何去做:

​ 首先建立对象,并调用其构造函数,然后成员被拷贝。

​ 用A初始化B的完成方式是内存拷贝,复制所有成员的值

指针虽然复制了,但所指向的空间并没有复制,而是由两个对象共用了

Vector::Vector(const Vector& other)
	:sz(other.size()),
	:elem(new double[other.size()])
{
	for(int i = 0; i!=sz; i++)
		elem[i] = other.elem[i];
}

8.3 拷贝构造函数被调用的情况

  • 对象初始化拷贝赋值 Mytype B = A

  • 定义新对象,并用已有对象初始化新对象时,即执行语句“MyType B=A;”时(定义对象时使用赋值初始化)

  • 当对象直接作为参数传给函数时,函数将建立对象的临时拷贝,这个拷贝过程也将调同拷贝构造函数

  • 函数返回时,函数栈区的对象会复制一份到函数的返回去

    Mytype foo()
    {
    	return A; //堆栈上开辟临时变量,复制给B
    }
    Mytype B = foo();
    

8.4 深拷贝与浅拷贝⭐⭐⭐⭐⭐

浅拷贝是增加了一个指针,指向原来已经存在的内存。而深拷贝是增加了一个指针,并新开辟了一块空间让指针指向这块新开辟的空间。浅拷贝在多个对象指向一块空间的时候,释放一个空间会导致其他对象所使用的空间也被释放了,再次释放便会出现错误。

8.5 不会进入到拷贝构造函数,而是进入赋值构造函数的情况⭐⭐⭐⭐

Mytype B;
B = A;

此时就不会进入拷贝构造函数!!!

9、赋值构造函数

如何进入赋值构造函数,见8.4

Vector& Vector::operator=(const Vector& a)
{
	if( &a == this )
		return *this;
    double *p = new double[a.size()];
    for(int i = 0; i!=sz;++i)
        p[i] = a.elem[i];       //记住C++中分配空间和初始化写在一起。
    delete[] elem;
    elem = p;
    sz = a.sz;
    return *this;
}

10、左值、右值引用是什么?

左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式)

int a; 
int b; 
 
a = 3; 
b = 4; 
a = b; 
b = a; 
 
// 以下写法不合法。 
3 = a; 
a+b = 4;

右值指的则是只能出现在等号右边的变量(或表达式)。如运算操作(加减乘除,函数调用返回值等)所产生的中间结果。

10.1 左值引用

MyType &引用名 = 左值表达式;
int main(){ 
    int a = 0; 
    int &b = a; 
    b = 11; 
    return 0; 
}

10.2 右值引用

右值的引用,就是给右值取别名

Type &&引用名 = 右值表达式;

10.3 为什么要有右值引用呢?

左值引用我们知道可以用于函数传参,减少不必要的对象拷贝,提升效率;或者用于替代指针的使用

C++11引入右值引用主要是为了实现移动语义完美转发

10.3.4 移动语义

首先实现string类⭐⭐⭐⭐⭐

#include <iostream> 
#include <cstring> 
#include <vector> 
using namespace std; 
 
class MyString{ 
public: 
    static size_t Ctor; //统计调用构造函数的次数 
    static size_t CCtor; //统计调用拷贝构造函数的次数 
public: 
    //构造函数 
    MyString(const char* str = nullptr) { 
        ++Ctor; 
        if (str != nullptr) { 
            m_data = new char[strlen(str) + 1]; 
            strcpy(m_data, str); 
        } 
        else { 
            m_data = new char[1]; 
            *m_data = '\\0'; 
        } 
    } 
 
    // 拷贝构造函数 
    MyString(const MyString& other) { 
        ++CCtor; 
        m_data = new char[strlen(other.m_data) + 1]; 
        strcpy(m_data, other.m_data); 
    } 
 
    // 拷贝赋值函数 =号重载 
    MyString& operator=(const MyString& other) { 
        if (this == &other) // 避免自我赋值!! 
           return *this; 
 
        delete[] m_data;  // 先释放原来的空间
        m_data = new char[strlen(other.m_data) + 1]; 
        strcpy(m_data, other.m_data); 
        return *this; 
    } 
 
    ~MyString() { 
        delete[] m_data; 
        m_data = NULL; 
    } 
private: 
    char* m_data; 
};

测试一下代码:

size_t MyString::Ctor = 0; 
size_t MyString::CCtor = 0; 
 
int main(){ 
    vector<MyString> vecStr; 
    vecStr.reserve(1000); //先分配好1000个空间 
    for (int i = 0; i < 1000; i++) { 
        vecStr.push_back(MyString("hello")); 
    } 
    cout << "构造次数:" << MyString::Ctor << endl; 
    cout << "拷贝构造次数:" << MyString::CCtor << endl; 
}

结果:

构造次数:1000
拷贝构造次数:1000

我们每次拷贝一个临时变量都需要深拷贝即进入拷贝构造函数中,这样会降低效率,因此拷贝构造函数每次都是重新分配一块新的空间,同时将要拷贝的对象复制过来。

要是我们能够减少这个拷贝的次数,效率就提升了,所以这个时候右值引用就派上用场了

右值引用可以引用并修改右值,但是通常情况下,修改一个临时值是没有意义的。然而在对临时值进行拷贝时,我们可以通过右值引用来将临时值内部的资源移为己用,从而避免了资源的拷贝

增加移动构造函数:

size_t MyString::MCtor = 0;  //统计调用移动构造函数的次数  
// 移动构造函数  
MyString(MyString&& str)  
    :m_data(str.m_data) {  
    ++MCtor;  
    str.m_data = nullptr; //不再指向之前的资源了  
}  

测试结果:

构造次数:1000
拷贝构造次数:0
移动构造次数:1000

成功减少了临时对象拷贝的次数。

11、移动构造函数

移动构造函数拷贝构造函数的区别是,拷贝构造的参数是const Mytype& str,是常量左值引用,而移动构造的参数是Mytype&& str,是右值引用

移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间同时将要拷贝的对象复制过来,而是"拿"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,那么就没有“拿”过来。

拷贝构造函数中,对于指针,我们一定要采用深层复制

而移动构造函数中,对于指针,我们采用浅层复制

注意:指针的浅层复制危害性极大!

之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了,所以我们要重设指针为nullptr(因为a我们不再使用了,如果不设为nullptr那么指针还指向新创建的空间,一旦出现内存被释放了以后,指针却还指向该内存,就会出现“野指针”的问题,容易出现内存非法访问错误。)

用a初始化b后,a我们就不需要了,最好是初始化完成后就将a析构(不能用了)所以移动构造函数,专门处理这种,用a初始化b后,就将a析构的情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JrjC2o6x-1623580014577)(C:\\Users\\Jocelin\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210610212324324.png)]

移动构造函数 在初始化的时候就是作了一个浅拷贝

然后把指针指向了空。

关于move的使用调用移动构造函数⭐⭐⭐⭐⭐

move方法来将左值转换为右值,从而调用移动构造函数而不是拷贝构造函数。

使用移动构造函数会提升函数的效率,见下面代码中return z;

Vector foo()
{
	Vector x(2000);
	Vector y(2000);
	Vector z(2000);
	z = x;  // 执行赋值构造函数
	y = std::move(x); //执行移动构造函数
	return z; // 我们返回z,再想一个问题之前提到8.3的第四条中,在栈中返回,它是一个将亡之人,如果我们调用拷贝构造函数的话会效率很低,因为这个z我后面并不需要它了,因此我们用浅拷贝(移动构造函数)就可以了。
}

12、拷贝构造函数的参数为什么必须是引用?

拷贝构造函数的参数为什么必须是引用?_nwd0729的专栏-CSDN博客

13、 静态成员与静态成员函数

  • 静态成员函数可以把函数与类的任何特定对象独立开来

  • 静态成员函数即使在类对象不存在的情况下也能被调用,只要使用类名加范围解析运算符::就可以访问

class A{
	int sz;
	static void foo()
	{
	
	}
};
A a;
A::foo();

创建了一个对象a,但是foo并不属于a

  • 静态成员函数只能访问静态成员数据、其他静态成员函数和类外部函数静态成员函数不能访问类的 this 指针,都需要对象所以就没有this指针

    static int foo()
    {
    	return 2 * sz;
    }
    

    这就是错误的了,不能访问某个对象的成员。

操作系统部分

0、什么是内核?

0.1 Linux内核系统体系结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fu6GIWBJ-1623580014579)(D:\\desktop\\33题.png)]

5大模块:进程调度模块、内存管理模块、文件系统模块、进程间通信模块和网络接口模块。

**进程调度模块:**控制进程被CPU资源的使用,采取的策略是不同进程能够公平合理的访问CPU,同时保证内存能够及时的执行硬件操作

**内存管理模块:**保证所有的进程能够安全的来共享机器上的内存区,同时这个内存管理的模块还支持虚拟内存的管理方式,能够使得支持进程使用比实际使用内存的空间更大,并且可以利用文件系统把这些暂时不用的内存数据块交换到外部的存储设备上。

**文件系统模块:**支持对外部设备的驱动和一些存储,虚拟文件系统VFS(这里面的回答很重要⭐⭐⭐⭐⭐)这个模块通过所有的外部设备提供一个通用的文件接口,他隐藏了各种各样硬件设备以及实现细节

0.1.1虚拟文件系统:

img

​ 虚拟文件系统是一套代码框架(framework),它处于文件系统的使用者与具体的文件系统之间,将两者隔离开来。这种引入 一个抽象层次的设计思想,即“上层不依赖于具体实现,而依赖于接口;下层不依赖于具体实现,而依赖于接口”,就是著名的“依赖反 转”,它在 Linux内核中随处可见。
​ VFS框架的设计,需要满足如下需求:

​ 1、 为上层的用户提供统一的文件和目录的操作接口,如 open, read, write

​ 2、 为下层的具体的文件系统,定义一系列统一的操作“接口”, 如 file_operations, inode_operations, dentry_operation,而 具体的 文件系统必须实现这些接口,才能融入VFS框架中。

**进程间通信模块:**用于多种进程间通信

**网络接口模块:**支持各种网络通信的标准

0.2 Linux内核结构体

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OHxdS25r-1623580014585)(C:\\Users\\Jocelin\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210612190659502.png)]

0.3

内核的内存管理的基本单位是页(page),源码下面 /include/linux/mm_types
在这里插入图片描述

1、cache

1.1 cache是什么

Cache存储器:电脑中为高速缓冲存储器,是位于CPU和主存储器DRAM之间,规模较小,但速度很高的存储器,通常由SRAM静态存储器组成。

高速缓冲存储器最重要的技术指标是它的命中率CPU要访问的数据在Cache中有缓存,称为“命中” (Hit),反之则称为“缺失” (Miss)。

现在 CPU 的 Cache 又被细分了几层,常见的有 L1 Cache, L2 Cache, L3 Cache,其读写延迟依次增加,实现的成本依次降低。

现代系统采用从 Register ―> L1 Cache ―> L2 Cache ―> L3 Cache ―> Memory ―> Mass storage的层次结构,是为解决性能与价格矛盾所采用的折中设计。

下图描述的就是CPU、Cache、内存、以及DMA之间的关系。程序的指令部分和数据部分一般分别存放在两片不同的cache中,对应指令缓存(I-Cache)和数据缓存(D-Cache)。

img

1.2 为什么需要cache

CPU缓存(Cache Memory)位于CPU与内存之间的临时存储器,它的容量比内存小但交换速度快。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可避开内存直接从缓存中调用,从而加快读取速度。

使用Cache改善系统性能的依据是程序的局部性原理,包括时间局部性和空间局部性。即最近被CPU访问的数据,短期内CPU 还要访问(时间);被 CPU 访问的数据附近的数据,CPU 短期内还要访问(空间)。因此如果将刚刚访问过的数据缓存在Cache中,那下次访问时,可以直接从Cache中取,其速度可以得到数量级的提高。

1.3 cpu与cache 内存交互的过程

CPU接收到指令后,它会最先向CPU中的一级缓存(L1 Cache)去寻找相关的数据,然一级缓存是与CPU同频运行的,但是由于容量较小,所以不可能每次都命中。这时CPU会继续向下一级的二级缓存(L2 Cache)寻找,同样的道理,当所需要的数据在二级缓存中也没有的话,会继续转向L3 Cache、内存(主存)和硬盘.

1.4 cache写机制

Cache写机制分为write through和write back两种。

Write-through(直写模式)在数据更新时,同时写入缓存Cache和后端存储。此模式的优点是操作简单;缺点是因为数据修改需要同时写入存储,数据写入速度较慢。

Write-back(回写模式)在数据更新时只写入缓存Cache。只在数据被替换出缓存时,被修改的缓存数据才会被写到后端存储。此模式的优点是数据写入速度快,因为不需要写存储;缺点是一旦更新后的数据未被写入存储时出现系统掉电的情况,数据将无法找回。

读机制

贯穿读出式(Look Through)

该方式将Cache隔在CPU与主存之间,CPU对主存的所有数据请求都首先送到Cache,由Cache自行在自身查找。如果命中。 则切断CPU对主存的请求,并将数据送出;不命中。则将数据请求传给主存。

该方法的优点是降低了CPU对主存的请求次数,缺点是延迟了CPU对主存的访问时间。

旁路读出式(Look Aside)

在这种方式中,CPU发出数据请求时,并不是单通道地穿过Cache。而是向Cache和主存同时发出请求。由于Cache速度更快,如果命中,则Cache在将数据回送给CPU的同时,还来得及中断CPU对主存的请求;不命中。则Cache不做任何动作。由CPU直接访问主存。它的优点是没有时间延迟,缺点是每次CPU对主存的访问都存在,这样。就占用了一部分总线时间。

1.5 cache 一致性⭐⭐⭐⭐⭐

DMA和cache一致性问题

2、Norflash与Nandflash的区别

(1)、NAND闪存的容量比较大
(2)、由于NandFlash没有挂接在地址总线上,所以如果想用NandFlash作为系统的启动盘,就需要CPU具备特殊的功能,
如s3c2410在被选择为NandFlash启动方式时会在上电时自动读取NandFlash的4k数据到地址0的SRAM中。
(3)、NAND Flash一般地址线和数据线共用,对读写速度有一定影响。NOR Flash闪存数据线和地址线分开,
所以相对而言读写速度快一些。

3、反码、补码

反码:对原码除符号位外的其余各位逐位取反就是反码

补码:负数的补码就是对反码加1

正数的原码、反码、补码都一样

4、内存管理MMU的作用

  • 内存分配和回收
  • 内存保护
  • 内存扩充
  • 地址映射

5、SRAM、DRAM、SDRAM

SRAM:CPU的缓存就是SRAM,静态的随机存取存储器,加电情况下,不需要刷新,数据不会丢失
DRAM,动态随机存取存储器最为常见的系统内存,需要不断刷新,才能保存数据
SDRAM:同步动态随机存储器,即数据的读取需要时钟来同步。

6、主宰操作系统的经典算法

以上是关于嵌入式面试总结的主要内容,如果未能解决你的问题,请参考以下文章

嵌入式面试总结

高级嵌入式软件面试题专栏目录

第一次嵌入式软件实习生面试过程以及复盘总结

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

嵌入式软件开发就业面试题。2022最新,最全总结。

面试常用的代码片段