小米嵌入式研发工程师校招面试总结

21-01-22更新:经过几轮面试,终于收到了小米offer,以下为博主总结的相关资料,希望能帮到求职的你。

刚参加完小米面试,博主一共经历了三面,面试相对简单,面试官也都挺好:

  • 一面:牛客视频面试,主要就是询问项目经验,主要和项目硬件相关,如NB-IoT、IIC、RS485和RS232异同、FreeRTOS相关知识等,最后手撕代码(题目见7、手撕代码),时间30多分钟;

  • 二面:牛客视频面试,将近一个星期以后二面,也基本都是项目相关,询问了一些C语言基础(问了变量内存分配,如static、const变量存放位置等,这块博主只回答了一个大概),基本都是下述C语言基础总结内容,最后手撕代码(题目见7、手撕代码),时间40多分钟;

  • 三面:电话面试,距离上次两天后,这个电话挺突然的,周四晚上7点打过来的,说有没有时间,想和我约一个时间电话面试,我就说随时可以,然后面试就开始了,主要问的也是项目经验,问题有:

    • 介绍一下你花费时间最长、难度最大的一个项目
    • 压力大的时候如何放松自己、
    • 讲解一下MQTT协议
    • 多个项目一起进行如何分配时间(博主校内大创和科研项目、校外外包项目比较多,所以问了)、
    • 有没有参加其它面试和收到其它offer等;此时博主已经和海康签约三方了,然后如是回答了最后一个问题;接着又问了一些违约相关的;

    最后,面试官说和其它几位面试官沟通后给结果,等待后续通知就行,希望能收到小米的offer。

博主是通过boss直聘投的简历,然后收到的面试邀请,之前在小米校招官网投的状态一直是简历筛选(博主12月才开始找工作,可能秋招已经结束了,建议大家找工作要趁早啊!!!

以下为博主根据各大面试平台回答,总结的面试题,仅供参考,如有侵权,请联系博主删除。

1、C语言基础

1、const、static、volitale关键字

const:

说明:

只读关键词,要求其所修饰的对象为常量,不可对其修改和二次赋值操作(不能作为左值出现)。

使用场景:

  1. 使一个变量只读,不可改变,需要先初始化;
  2. 对于一个指针而言,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
  3. 在一个函数声明中,const可以修饰形参表明为一个输入参数,在函数内部不可以改变其值;
  4. 对于类的成员函数,有时必须指定为const,表明其为一个常函数,不能修改类的成员变量;
  5. 对于类的成员函数,有时必须指定其返回值为cons,以使其返回值不为"左值"。

static:

说明:

  1. static局部变量定义,生存周期为整个源程序,但其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出函数后,该变量还继续存在,但不能使用它,再次调用该函数可以再次使用;
  2. static修饰全局变量时,这个全局变量只能在本文件访问,不能在其它文件中访问,即便时extern外部声明也不可以;
  3. static修饰一个函数,则这个函数只能在本文件中调用,不能被其它文件调用;
  4. static修饰的局部变量存放在全局数据区的静态变量去,初始化的时候自动初始化为0。

使用场景:

  1. 不想被释放,如修饰函数存放在栈空间的数组
  2. 如果不想该数组在函数调用结束释放
  3. 考虑数据安全性(当程序想要使用全局变量的时候应该优先使用static)

volatile:

说明:

作用是告知编译器,它修饰的变量随时都可能被改变,因此,编译后的程序每次在使用该变量的值时,都会从变量的地址中读取数据,而不是从寄存器中获取。

使用场景:

  1. 并行设备的硬件寄存器(如状态寄存器)
  2. 一个中断服务子程序回访问到的非自动变量(Non-automatic variables)
  3. 多线程应用中被几个任务共享的变量

参考链接

c++ extern,static,const,volatile关键字

2、c++中的extern “c”

作用一:告知编译器编译函数时按照C的规则去翻译函数名,而C++翻译由于有函数重载,翻译规则和C不一样,这样避免在库中找不到符号。C语言不支持extern "C"语法。如:

extern "C" {
#endif}
#endif

例如函数void fun(int, int),编译后的可能是*fun_int_int(不同编译器可能不同,但都采用了类似的机制,用函数名和参数类型来命名编译后的函数名);而C语言没有类似的重载机制,一般是利用函数名来指明编译后的函数名的,对应上面的函数可能会是*fun这样的名字。

作用二:申明函数或全局变量的作用范围的关键字,其申明的函数和变量可以在本模块和其它模块中使用,默认函数时extern的,所以可以不写,全局变量需要写。如:

#include<stdio.h>
namespace myname {int var = 42;
}
extern "C" int _ZN6myname3varE;
int ()
{printf("%dn", _ZN6myname3varE);myname::var ++;printf("%dn", _ZN6myname3varE);printf("%pn",&_ZN6myname3varE);printf("%pn", &myname::var);return 0;
}

参考链接

c++ extern,static,const,volatile关键字

3、数组与指针区别

C、C++程序中,指针和数组可以相互替换使用,两者有如下区别:

  • 数组要么在静态存储区被创建(如全局数组),在么在栈上被创建,数组对应着一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
  • 指针可以随时指向任意类型的内存块,它的特性是“可变”,所以我们常用指针来操作动态内存
  • 指针比数组灵活,但更危险。

参考链接

数组指针和指针数组的区别; 二维数组和指针的关系

4、数组指针与指针数组区别

数组指针(也称行指针)

定义int (*p)[n]()优先级高,首先说明p使一个指针,指向一个整型的一维数组,这个一维数组的长度为n,也可以说是p的步长。执行p+1时,p要跨过n个整型数据的长度。

如果将二维数组赋给一个数组指针,指向含4个元素的一维数组:

int a[3][4];
int (*p)[4];// 定义一个数组指针,指向含有四个元素的一维数组
p=a;// 将二维数组a的首地址赋值给p,也就是a[0]或&a[0][0]
p++;// 执行过后,也就是p=p+1,p跨过行a[0]指向行a[1],也就是跨过4个步长
// 所以数组指针也称为一维数组的指针,也叫行指针。

指针数组

定义int *p[n][]优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时错误的,p=a赋值也是错误的,因为p是不可知的表示,只存在p[0]、p[1]、p[2]...p[n-1],而且它们分别是指针变量,用来存放变量地址。*p=a赋值可以,*p表示指针数组的第一个元素的值,a的首地址的值。如果将二维数组赋给指针数组:

int *p[3];
int a[3][4];
for(i=0;i<3;i++)p[i] = a[i];

上述int *p[3]表示一个一维数组存放着三个指针变量,分别是p[0]、p[1]、p[2],所以需要分别赋值。

总结

数组指针只是一个指针变量,似乎是C语言专门用来指向二维数组的,它占有内存中一个指针的存储空间。指针数组是多个指针变量,以数组的形式存在内存中,占用多个指针的存储空间。

需要注意的是,同时用来指向二维数组时,其引用和用数组名引用都是一样的。比如要表示数组中i行j列的一个元素:

*[p[i]+j]、*(*(p+i)+j)、(*(p+i))[j]、p[i][j]

参考链接

数组指针和指针数组的区别; 二维数组和指针的关系

5、C语言#和##的用法

宏定义可以包含两个专用的运算符:#和##。编译器不会识别这两个运算符,他们会预处理时被执行。

#运算符用法

#运算符用在预编译时期,用于将宏参数转换为字符串,即是加上双引号。测试如下:

#include <stdio.h>#define PRINT_MACRO_HELPER(x) #x
#define PRINT_MACRO(x) PRINT_MACRO_HELPER(x)
#define PRINT_ANOTHER_MACRO(x) #x"="PRINT_MACRO_HELPER(x)int main()
{   int i=982366632;char *str=PRINT_MACRO_HELPER(1235982536);char *str2=PRINT_MACRO(i);char *str3=PRINT_ANOTHER_MACRO(i);printf("the string of str is %s,the string of str2 is %s ,the string of str3 is %s\n",str,str2,str3);printf("%s\n","i love android!");return 0;
}

输出:

the string of str is 1235982536,the string of str2 is i ,the string of str3 is i=i
i love android!

##运算符用法

##用在预编译期,用来粘连两个符号,增大了宏的使用灵活性。测试如下:

#include <stdio.h>#define NAME(n) int_name##nint main()
{int NAME(a);int NAME(b);NAME(a) = 520;NAME(b) = 111;printf("%d\n", NAME(a));printf("%d\n", NAME(b));return 0;
}

输出:

520
111

应用特例

需要注意的是凡宏定义里有用’#‘或’##'的地方宏参数是不会再展开。

解决这个问题的方法很简单,加多一层中间转换宏,加这层宏的用意是把所有宏的参数在这层里全部展开,那么在转换宏里的那一个宏就能得到正确的宏参数。

  • 合并匿名变量名
#define  ___ANONYMOUS1(type, var, line)  type  var##line
#define  __ANONYMOUS0(type, line)  ___ANONYMOUS1(type, _anonymous, line)
#define  ANONYMOUS(type)  __ANONYMOUS0(type, __LINE__)
// 例:ANONYMOUS(static int);  即: static int _anonymous70;  70表示该行行号;
// 第一层:ANONYMOUS(static int);  -->  __ANONYMOUS0(static int, __LINE__);
// 第二层:                        -->  ___ANONYMOUS1(static int, _anonymous, 70);
// 第三层:                        -->  static int  _anonymous70;
// 即每次只能解开当前层的宏,所以__LINE__在第二层才能被解开;
  • 填充结构
#define  FILL(a)   {a, #a}enum IDD{OPEN, CLOSE};
typedef struct MSG{IDD id;const char * msg;
}MSG;MSG _msg[] = {FILL(OPEN), FILL(CLOSE)};
// 相当于:
MSG _msg[] = {{OPEN, "OPEN"},{CLOSE, "CLOSE"}};
  • 记录文件名
#define  _GET_FILE_NAME(f)   #f
#define  GET_FILE_NAME(f)    _GET_FILE_NAME(f)
static char  FILE_NAME[] = GET_FILE_NAME(__FILE__);
  • 得到一个数值类型所对应的字符串缓冲大小
#define  _TYPE_BUF_SIZE(type)  sizeof #type
#define  TYPE_BUF_SIZE(type)   _TYPE_BUF_SIZE(type)
char  buf[TYPE_BUF_SIZE(INT_MAX)];
//      -->  char  buf[_TYPE_BUF_SIZE(0x7fffffff)];
//      -->  char  buf[sizeof "0x7fffffff"];
// 这里相当于:
char  buf[11];

参考链接

C语言宏定义中#与##的用法

C语言宏定义中#符号和##的妙用

6、宏定义函数和普通函数区别

宏是根据一系列预定义的规则替换一定的文本模式:

#define BUFFER_SIZE 1024
// 预处理阶段,fun=(char)malloc(BUFFER_SIZE);会被替换成fun=(char)malloc(1024);
#define NUMBERS 1,2,3
// 预处理阶段,int x[]={NUMBERS};会被扩展为int x[]={1,2,3};

宏函数

宏名之后带括号的宏被认为是宏函数

#define add(a,b) (a+b)
# y=add(1,2);会被扩展为y=1+2;

宏函数和普通函数异同

  • 相同点:若宏是一种替换工具,那么相同点就只有计算结果。

  • 不同点:宏函数在预处理阶段进行文本搜索和替换。而普通函数是在编译过程会分配空间,创建栈帧,传参,传返回值等。

宏函数与普通函数的优缺点

宏函数

  • 优点:没有普通函数保存寄存器和参数传递,返回值传递的开销,展开后的代码效率高,速度快。

  • 缺点:展开后的代码体积大,宏函数替换是纯文本替换,C预处理器不对宏函数做任何语法检查,就像缺个括号等预处理器是不会管的。

#define add(a,b) a+b
int main()
{int t=add(1,2)*2;printf("%d",t);return 0;
}
// 原本是想计算(1+2)* 2的值,但是实际上计算的是1+2 *2的值,所以就出现了计算错误。

普通函数

  • 优点:代码体积小,降低程序的复杂性,使程序更容易维护。
  • 缺点:程序调用普通函数会开辟一片空间,进行寄存器保存,传值的操作,降低代码效率。

总结

虽然代码执行速度上宏函数更快,但是在《google编程规范》和《Effective C++》是不推荐使用宏的,因为宏的使用在某种程度上会降低程序的可维护性,还可能出现一些莫名其妙的错误,而找不到源头,如果对宏的使用不熟练,还是避免使用宏定义。

参考链接

宏函数与普通函数的区别

堆栈

内存中的堆栈

堆是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程也可以向系统要额外的堆,但是使用完需要释放,否则内存泄漏;

栈是线程独有的,保存其运行状态和局部自动变量。栈在线程开始的时候初始化,每个线程的栈相互独立。每个函数都有自己的栈,栈被用来在函数之间传递参数。操作系统在切换线程的时候会自动的切换栈,就是切换SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。

两者不同点如下:

  1. 管理方式不同:栈自动分配释放;堆开发人员;
  2. 空间大小:栈大小远小于堆大小,堆约等于虚拟内存大小;
  3. 生长方向:栈向下生长,内存地址由高至低;堆向上,内存地址由低至高;
  4. 分配方式:堆都是动态分配的,无静态;栈静态分配,由操作系统完成,动态分配由alloc函数实现,栈不同于堆,释放由操作系统完成;
  5. 分配效率:栈远高于堆,因为会在硬件层对栈提供支撑,分配专门的寄存器存放栈的地址,压栈和出栈都有专门的指令执行。堆则由C/C++提供的库函数来完成申请和管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片;
  6. 存放内容:栈存放的内容为函数返回地址、相关参数、局部变量和寄存器内容等;一般情况堆顶使用一个字节空间存放堆的大小,而堆中具体存放内容由程序员决定。

注意:无论是堆还是栈,都需要防止非法越界问题。

参考链接

堆与栈的区别

7、C语言中malloc和free

malloc

函数原型: void * malloc(size_t size);

用途:用来动态的分配内存空间,传入参数为分配的内存大小,以字节计;如果分配成功,则会返回分配内存的首地址,如果分配失败,就会返回空指针。

注意:内存使用完以后,需要释放,虽然程序在结束以后可能会释放内存。

该函数在使用时需要的一些注意事项:

  • 申请内存以后,需要判断内存是否分配成;
  • 当不需要使用申请的内存时,记得释放;内存释放以后应该把指向这块内存的指针指向NULL,防止后续程序不小心使用;
  • malloc()free()时配套使用的,申请以后不释放会造成内存泄漏,如果无故释放那就是什么也没有做。只能释放一次,释放两次及以上会出错(释放空指针除外,释放多少次空指针都没问题);
  • malloc()函数的返回类型是(void*),任何类型的指针都可以转换成(void*),但是最好在前面添加强制类型转换,这样可以躲过一些编译器检查。

malloc内存空间如何获取

malloc分配的空间是从堆里面获取的空间,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表,当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

参考链接

C语言中 malloc函数用法

C语言指针之二malloc的用法及详解

C语言malloc()函数:动态分配内存空间

8、C语言的左值和右值

  • 左值就是一个可被存储的单元,右值就是一个可被读取的数据。
  • 左值必须是一个被明确了的内存存储单元,可以用来被赋值;右值必须是一个能被读出来的确确实实的值,这个值可以是数据,可以是指针,可以是结构,反正只要能被读出来的,都可以定义为右值。

参考链接

C语言的左值(lvalue)和右值(rvalue)的含义是什么?

9、纯虚函数是什么

没详细学过C++,如需了解网上查资料。

参考链接

C++ 虚函数、纯虚函数

10、inline的作用是什么

inline含义

inline修饰符用来表示该函数为内联函数。在C语言中,如果一些函数被频繁调用,不断地有函数入栈,即函数栈,会造成栈空间或者栈内存的大量消耗。为了解决该问题,引入inline修饰符。

栈空间指放置程序的局部数据,也就是函数内数据的内存空间,在系统下,栈空间是有限的,加入频繁大量的使用就会造成因栈空间不足所造成的程序出错问题,函数的死循环递归调用的最终结果就是导致栈空间内存枯竭。

以下为内联函数函数例子:

#include <stdio.h>  //函数定义为inline即:内联函数
inline char* dbtest(int a)
{  return (i % 2 > 0) ? "奇" : "偶";
}   int main()
{  int i = 0;  for (i=1; i < 100; i++) {  printf("i:%d    奇偶性:%s /n", i, dbtest(i));      }
}

在上述for循环中调用的dbtest(i)的地方都被换成了(i % 2 > 0) ? "奇" : "偶",这样就避免了频繁调用函数对栈内存重复开辟所带来的消耗。

inline注意事项

  • 关键词inline必须与函数定义体放在一起才能使函数成为内联,仅将inline放在函数声明前是不起任何作用。如:
// 不能成为内联函数
inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y)
{}// 为内联函数
void Foo(int x, int y);
inline void Foo(int x, int y) // inline 与函数定义体放在一起
{}

所以说,inline是一种用于实现的关键字,而不是一种用于声明的关键字。关于inline关键词是否应该出现在函数申明存在争议:大多数教科书内联函数的声明和定义体钱都添加了inline关键字,但有些人认为在函数声明前没必要添加,因为体现了高质量C/C++程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没必要、也不应该知道函数是否需要内联。

  • inline只适合函数体内部代码简单的函数使用,不能包含复杂的结构控制语句,如while、switch等,并且内联函数本身不能是直接递归函数。
  • 谨慎使用内联函数,内联虽然可以提高函数的执行效率,但是以代码膨胀(复制)为代价,仅省去了函数调用的开销,内联函数的调用都要复制代码,将使函数的总代码量增大,消耗更多的内存空间。一下情况不宜使用内联:
    • 如果函数体内代码较长,使用内联将导致内存消耗代价较高;
    • 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

总结

将内联函数放置在头文件实现最合适,减少为每个文件实现一次的麻烦。

参考链接

内联函数 —— C 中关键字 inline 用法解析

11、数组访问方式

网上找到关于数组的10种访问方式:

/*
数组的访问方式
*/#include<stdio.h>
int main()
{int a[10]={0,1,2,3,4,5,6,7,8,9};int *p=a;printf("%d %d %d %d %d %d %d %d %d %d ",0[a],*(p+1),*(a+2),a[3],p[4],5[p],(&a[5])[1],1[(&a[6])],(&a[9])[-1],9[&a[0]]);return 0;
}

参考链接:

C语言专题—10种数组的访问方式

12、strcpy和memcpy区别以及代码实现

使用strcpy和memcpy,需要引入#include<string.h>头文件,两者函数申明如下:

char *strcpy(char* dest,const char* src);
void *memcpy(void*dest, const void *src, size_t n);

两者区别如下:

  • strcpy只能拷贝字符串,strcpy遇到’\0’结束拷贝(当dest的内存长度大于src的长度,拷贝时将’\0’拷贝过去,其后的内容不拷贝);如果当dest的内存长度小于src的长度,那么会造成内存溢出等问题,所以才有了strncpy函数,就是增加了长度控制。
  • memcpy从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中,可以拷贝任意数据。除了拷贝字符串,还能拷贝其它类型的数据,比如结构体、整型数据、类等。mencpy拷贝时需要带有长度参数。

源代码

// strcpy
// 一个字节一个字节复制
char * __cdecl strcpy(char * dst, const char * src)
{char * cp = dst;while( *cp++ = *src++ ); /* Copy src over dst */return( dst );
}
// memcpy
#include <string.h>
#include <memcopy.h>
#include <pagecopy.h>#undef memcpyvoid *
memcpy (dstpp, srcpp, len)void *dstpp;const void *srcpp;size_t len;
{unsigned long int dstp = (long int) dstpp;unsigned long int srcp = (long int) srcpp;/* Copy from the beginning to the end.  *//* If there not too few bytes to copy, use word copy.  */if (len >= OP_T_THRES){/* Copy just a few bytes to make DSTP aligned.  */len -= (-dstp) % OPSIZ;BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);/* Copy whole pages from SRCP to DSTP by virtual address manipulation,as much as possible.  */PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);/* Copy from SRCP to DSTP taking advantage of the known alignment ofDSTP.  Number of bytes remaining is put in the third argument,i.e. in LEN.  This number may vary from machine to machine.  */WORD_COPY_FWD (dstp, srcp, len, len);/* Fall out and copy the tail.  */}/* There are just a few bytes to copy.  Use byte memory operations.  */BYTE_COPY_FWD (dstp, srcp, len);return dstpp;
}
libc_hidden_builtin_def (memcpy)

关于源码详细介绍,可以查看以下链接:

  • glibc–memcpy源码分析

Linux man手册关于两者描述

/*The strcpy() function copies the string pointed to by src, including the terminating null byte ('\0'), to the buffer pointed to by dest. The strings may not overlap, and the destination string dest must be large enough to receive the copy. Beware of buffer overruns!  (See BUGS.)The strncpy() function is similar, except that at most n bytes of src are copied. Warning: If there is no null byte among the first n bytes of src, the string placed in dest will not be null-terminated.If the length of src is less than n, strncpy() writes additional null bytes to dest to ensure that a total of n bytes are written.中文翻译:
strcpy()函数将src指向的字符串,包括终止null字节('\0')复制到dest指向的缓冲区,字符串不能重叠,目标字符串dest必须足够大才能接收到拷贝。小心缓冲区溢出!(见缺陷)
strncpy()函数与此类似,只是最多复制了n个字节的src。警告:如果src的前n个字节中没有null字节,放置在dest中的字符串将不会以null结尾。如果src的长度小于n, strncpy()将额外的空字节写入dest,以确保总共写入了n个字节。The  memcpy() function copies n bytes from memory area src to memory area dest.  The memory areas must not overlap.  Use memmove(3) if the memory areas do overlap.       中文翻译:
memcpy()函数从内存区域src复制n个字节到内存区域dest。内存区域不能重叠。如果内存区域确实重叠,则使用memmove(3)。
*/

总结

两者都能拷贝字符串,但是memcpy可以拷贝任意类型,strcpy只能拷贝字符串,在拷贝时需要注意内存溢出,建议使用strncpy。

效率:一般情况下,memcpy效率高于strcpy;

安全:memcpy有长度控制,相对来说更安全,但有内存重叠风险。

参考链接:

glibc–memcpy源码分析

BYTE,WORD,DWORD的大小及一些特殊的"高低位宏"

strcpy和memcpy的区别以及实现

memcpy和strcpy区别,以及源代码学习

13、C语言内存管理

内存分配的类型

在C/C++中内存分为5个区,分别为栈区、堆区、全局/静态存储区、常量存储区、代码区。

静态内存分配:编译时分配,包括:全局、静态全局、静态局部三种变量;

动态内存分配:运行时分配,包括:栈(stack):局部变量;堆(heap):C语言中用到的变量被动态的分配在内存中(malloc或calloc、realloc、free函数)。

变量的内存分配

  • 栈区(stack):指那些由编译器在需要的时候分配,不需要时自动清除的变量所在的存储区,如函数执行时,函数的形参以及函数内的局部变量分配在栈区,函数运行结束后,形参和局部变量去栈(自动释放)。栈内存分配运算内置于处理器的指令集中,效率高但是分配的内存空间有限。
  • 堆区(heap):指那些由程序员手动分配释放的存储区,如果程序员不释放这块内存、内存将一直被占用,知道程序运行结束由系统自动回收,C语言中使用malloc和free申请和释放空间。
  • 静态存储区(static):全局变量和静态变量的存储是放在一块的,其中初始化的全局变量和静态变量在一个区域,这块空间当程序运行结束后由系统释放。
  • 常量存储区(const):常量字符串存储区域,如“ABC”字符串等,存储在常量区的只读不可写。const修饰的全局变量存储在常量区,const修饰的局部变量依然存储在栈上。
  • 程序代码区:存放程序的二进制代码。

在C语言中提供了多个内存分配函数,在头文件<stdlib.h>中:

void *calloc(int num, int size);
// 在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。void *malloc(int num);
// 在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。void *realloc(void *address, int newsize);
// 该函数重新分配内存,把内存扩展到 newsize。void free(void *address);
// 该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。

注意:void*类型表示未确定类型的指针。C、C++规定void*类型可以通过强制类型转换为任何其它类型的指针。

参考链接

C语言:内存分配

C 内存管理

14、C语言二维数组作为函数参数

C语言中,二维数组是按行存放的,其实际上就是特殊的一维数组,它的每个元素也是一个一维数组。

二维数组作为函数参数有如下规定:如果将二维数组作为参数传递给函数,那么函数的参数声明中必须指明数组的列数,数组的行数没有太大关系,可以指定也可以不指定。因为函数调用的时候是一个指针,它指向由行向量组成的一维数组,二维数组作为函数参数正确写法如下:

void func(int array[3][10]);
void func(int array[][10]);
// 因为行数无关紧要,还可以写成如下形式:
void func(int (*array)[10]);
// 上述声明参数是一个指针,它指向具有10个元素的一维数组。因为[]的优先级比*优先级高,故*array必须用括号括起来,否则变成:
void func(int *array[10]);
// 上述参数相当于是声明了一个数组,该数组有10个元素
// 在定义时,不能省略二维及更高维度大小,如下述定义不合理:
void func(int array[][]);
void func(int array[3][]);

参考链接

C语言二维数组作为函数参数传递

15、C语言-结构体所占字节数(内存对齐)

理论上讲,对于任何变量的访问都可以从任何地址开始访问,但事实上不是如此,实际上访问特定的变量只能在特定的地址访问,这就需要各个变量在空间上按一定的规则排列,而不是简单的顺序排列,这就是内存对齐。

内存对齐原因:

  • 一些平台只能在特定的地址处访问特定类型的数据
  • 提高存取数据的速度。比如有的平台每次都是从偶地址处读取数据,对于一个int类型的变量,若从偶地址单元存放,则只需要一个读取周期即可,但是从奇地址单元存放,则需要2个读取周期读取该变量。

在C99标准中,对内存对齐的细节没有过多描述,具体的实现交由编译器处理,所以不同编译环境下,内存对齐可能略有不同,但基本原则一直,对于结构体对齐主要有以下两点:

  • 结构体每个成员相对结构体首地址的偏移量(offset)是对齐参数的整数倍。如果需要会在成员之间填充字节,编译器在为结构体成员开辟空间时,首先检查预开辟空间的地址相对于结构体首地址的偏移量是否为对齐参数的整数倍,若是,则存放该成员,若不是,则填充若干字节,以达到整数倍的要求。
  • 结构体变量所占空间的大小是对齐参数的整数倍。如果需要会在最后一个成员末尾填充若干字节使得所占空间大小是对齐参数大小的整数倍。

注意:对于每个变量,它自身有对齐参数,这个自身对齐参数在不同编译环境下不同,下面列举的是两种最常见的编译环境下各种类型变量的自身对齐参数:

char short int float double 指针
Windows(32位) 长度 1 2 4 4 8 4
Visual C++ 6.0 自身对齐参数 1 2 4 4 8 4
Linux(32位) 长度 1 2 4 4 8 4
GCC 自身对齐参数 1 2 4 4 4 4

由上表可知,在Windows(32)VC6.0下各种类型的变量的自身对齐参数就是该类型变量所占字节数的大小,而在Linux(32)GCC下double类型变量自身对齐参数是4,是因为Linux(32)GCC下如果该类型变量的长度没有超过CPU的字长,则该类型变量的长度作为自身对齐参数,如果该类型变量的长度超过CPU字长,则对齐参数为CPU字长,而32位系统的CPU字长为4,所以Linux(32)GCC下double类型的变量自身对齐参数为4,如果在Linux(64)下,则double类型的自身对齐参数为8.

除了变量自身对齐参数,还有编译器默认的对齐参数:#pragma pack(n),这值可以通过代码去设定,如果没有设定,则取系统默认,在Windows(32)VC6.0下,n的取值可以为1、2、4、8,默认情况下为8,在Linux(32)GCC下,n的取值只能为1、2、4,默认情况下为4。注意DEV-CPP、MinGW等在Windows下n的取值和VC的相同。

参考链接

C语言 - 结构体所占字节数

16、已知结构体中的一员地址,如何访问其它成员

C语言的结构体可以将不同类型的对象聚合到一个对象中,在内存中,编译器按照成员列表顺序分别为每个结构体变量成员分配内存,但由于C的内存对齐机制以及不同机器间的差异,各个成员之间可能会有间隙,所以不能简单的通过成员类型所占的字长来推断其它成员或结构体对象的地址。

如果要计算结构体中某成员相对于该结构体首地址的偏移量,一般就是采用该成员地址减去结构体对象首地址。

首先来看Linux是如何计算偏移量的:

/* * 选自 linux-2.6.7 内核源码 * filename: linux-2.6.7/include/linux/stddef.h */
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

Linux中定义了一个宏offsetof,接收两个参数,TYPE代表结构体类型,MEMBER代表结构体中的成员。
((TYPE*)0):先将0强制转换为TYPE*型的指针类型,这样就把以0地址(0x00000000)为首的一片连续内存当成一个结构体对象操作。
((TYPE*)0)->MEMBER:表示获取TYPEMEMBER成员,再结合&就可以获得MEMBER成员的地址,由于该结构体的首地址为0x00000000,所以MEMBER成员的地址就为偏移量,在使用size_t强制转换就可以获取相对于结构体首地址的偏移量。

然后再看如何获取结构体首地址:

/* * 选自 linux-2.6.7 内核源码 * filename: linux-2.6.7/include/linux/stddef.h */
#define container_of(ptr, type, member) ({          \  const typeof( ((type *)0)->member ) *__mptr = (ptr); \  (type *)( (char *)__mptr - offsetof(type,member) );})

Linux中定义的宏为container_of,接收三个参数,ptr表示已知成员地址,type表示结构体的类型,member表示已知的成员。

typeof( ((type *)0)->member ):获取member成员的类型(使用typeof),然后使用该类型定义一个只读的指针变量__mptr,并将其赋值为ptr;接着将该指针减去偏移量,就获得了该结构体的首地址,最后使用type*强制转换为对应的指针类型,这样就得到了结构体对象的地址。

以下为测试工程文件:

#include "stdio.h"// 获取偏移量
#define offsetof(type,member) ((size_t)&((type*)0)->member)
// 获取结构体地址
#define container_of(ptr, type, member) ({          \const typeof( ((type *)0)->member ) *__mptr = (ptr); \(type *)( (char *)__mptr - offsetof(type,member) );})typedef struct list_node {int ivar;char cvar;double dvar;struct list_node *next;
} list_node;struct list_node nodes={100,127,300,NULL
};int main(void){printf("adress node: 0x%016x\n",&(nodes));printf("adress ivar: 0x%016x\n",&(nodes.ivar));printf("adress cvar: 0x%016x\n",&(nodes.cvar));printf("adress dvar: 0x%016x\n",(&(nodes.dvar)));printf("adress next: 0x%016x\n",&(nodes.next));printf("offsetof: %d\n",offsetof(list_node,dvar));printf("container_of: %016x\n",container_of(&nodes.dvar,list_node,dvar));return 0;
}

参考链接

通过结构体成员的地址获取结构体变量的地址

17、C语言函数重载

C语言中不允许有同名函数,所以就没有函数重载,但是可以通过下述方法实现类似的功能:

  • 使用函数指针来实现,重载的函数不能使用同名称,只是类似的实现了函数重载功能;
  • 重载函数使用可变参数,方式如打开文件open函数;
  • gcc有内置函数,程序使用编译函数可以实现函数重载。

示例如下:

#include<stdio.h>void func_int(void * a)
{printf("%d\n",*(int*)a);  //输出int类型,注意 void * 转化为int
}void func_double(void * b)
{printf("%.2f\n",*(double*)b);
}typedef void (*ptr)(void *);  //typedef申明一个函数指针void c_func(ptr p,void *param)
{p(param);                //调用对应函数
}int main()
{int a = 23;double b = 23.23;c_func(func_int,&a);c_func(func_double,&b);return 0;
}

参考链接

C语言实现函数重载

18、C++深拷贝浅拷贝

参考文链接

c++深拷贝和浅拷贝

19、c++多态是什么、它的实现原理

参考链接

C++多态的实现原理 - CSDN博客

C++ 多态的实现原理分析_阿飞的博客-CSDN博客

C++多态的实现原理- 知乎

2、数据结构基础

1、队列和栈的区别 平衡二叉树

2、hashmap的put

3、单片机基础

1、IIC通信过程以及相关知识点

简介

IIC是一种简单、双向二线制同步串行总线,是多主设备的总线,无物理的芯片选择信号线,没有仲裁逻辑电路,只有两条信号线:Serial Data(SDA)和Serial Clock(SCL);IIC的传输速率有标准模式(100Kbps)、快速模式(400Kbps)高速模式(3.4Mbps),另外一些变种的协议实现了低速模式(10Kbps)和快速+模式(1Mbps)。

硬件结构

每个IIC总线器件内部的SDA、SCL引脚电路结构是一样的,引脚的输出驱动与输入缓冲连载一起,其中输出为漏极开路的场效应管、输入缓冲为一只高输入阻抗的同相器,此种电路具有两个特点:

  • 由于SDA、SCL为漏极开路结构,借助于外部的上拉电路实现信号的“线与”逻辑;
  • 引脚在输出信号的同时还能对引脚上的电平进行检测,检测是否与刚才输出一致,为“时钟同步”和“总线仲裁”提供硬件基础。

10位设备地址

任何IIC设备都有一个7位地址,理论上只能有127中不同的IIC设备,于是有了10位设备地址(扩展),10位设备地址有如下影响:

  • 地址帧为两个字节长,原来的为一个字节;
  • 第一个字节前五位最高有效位用作10位地址标识,约定为:11110;
  • 除10位地址标识,还预留了一些地址码用作其它用途。

数据类型

IIC协议规定:

  • 每一支IIC设备都有一个唯一的七位设备地址;
  • 数据帧大小为8位的字节;
  • 数据帧中的某些数据位用于控制通信的开始、停止、方向(读写)和应答机制。

物理实现上,IIC总线由两根信号线和一根地线组成,规定发起通信的为主设备。

总线空闲状态

IIC总线的SDA和SCL同时为高电平,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。

起始信号

SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。

结束信号

SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。

重新开始信号

在IIC总线上,由主机发送一个开始信号启动一次通信后,在首次发送停止信号之前,主机通过发送重新开始信号,可以转换与当前从机的通信模式,或是切换到与另一个从机通信。当SCL为高电平时,SDA由高电平向低电平跳变,产生重新开始信号,它的本质是一个开始信号。

应答信号

IIC总线上的所有数据都是以8位字节传送,发送器每发送一个字节,就在第9个始终脉冲期间释放数据线,由接收器反馈一个应答信号。应答信号位低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号位高电平时,规定位非应答位(NACK),一般表示接收器接收该字节失败。

因此接收一个完整的字节数据传输需要9个始终脉冲。如果从机作为接收方向主机发送非应答信号,主机方就认为此次数据传输失败;如果时主机作为接收方,在从机发送器发送完一个字节数据后,向从机发送了非应答信号,从机就认为数据传输结束,并释放SDA线,不论哪种情况都会终止数据传输,这时主机或是产生停止信号释放总线或是产生重新开始信号,开始一次新的通信。

数据传送位

在IIC总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。进行数据传送时,在SCL呈现高电平期间,SDA上的电平必须保持稳定。只有在SCL位低电平期间,才允许SDA上的电平改变状态。

插入等待时间

如果被控器需要延迟下一个数据字节开始传送的时间,则可以通过把时钟线SCL电平拉低并且保持,使主控器进入等待状态。一旦被控器释放时钟线,数据传输就可以继续下去,这样可以使被控器得到足够时间转移或者处理已经接收到的数据字节,或者准备好即将发送的数据字节。

总线冲突和总线仲裁

当总线系统中存在两个主设备,它们都有控制总线的能力,这就有可能出现总线冲突,现在总线上有两个主设备,分别位主设备1和主设备2,其输出数据分别为DATA1和DATA2:

假设在某一瞬间两者相继向总线发出启动信号,鉴于IIC总线的”线与“特性,使得SDA上得到的信号波形是两者相与的结果,所以总线启动,总线控制权得不到裁定结果;

接着主设备1企图发送数据”101…“,主设备2企图发送”100…“,两个主设备在每次发出一个数据位的同时都对自己输出端的信号电平进行检测,只要抽检的结果与其预期电平相符,就会继续占用总线。主设备1第三位为”1“,也就是输出高电平,主设备2第三位为”0“,也就是输出低电平,由于”线与“特性,总线上此时为低电平。主设备1检测到不相匹配的电平”0“,所以只能放弃总线控制权,主设备2成为总线占有者,总线控制权得出最终裁定结果,从而实现了总线仲裁功能。

从以上总线仲裁的过程可以得出,仲裁过程各个设备不会丢失数据;系统实际上遵循的是”低电平优先“的仲裁原则,将总线判给在数据线上先发送低电平的主设备,而其他发送主设备则失去总线控制权。

时钟同步

还是上述两个主设备,其时钟输出端分别为CLK1和CLK2,它们多有控制总线的能力,假设两者相继发出不同的时钟脉冲CLK1和CLK2,在总线控制权还没有裁定之前是可能出现的。同样的,鉴于IIC总线”线与“特性,使得得到的信号波形为两者进行逻辑与的结果。这两者合成波形作为共同的同步时钟信号,一旦总线控制权裁定给某个主设备,则总线时钟将只由该主设备产生。

总线封锁状态

在特殊情况下,需要禁止所有发生在IIC总线上的通信活动,这时可以封锁或者关闭总线,只需要挂接于总线上的任意一个设备将时钟线SCL锁定在低电平上即可。

高速模式

原理上,使用上拉电阻来设置逻辑1会限制总线的最大传输速率。而速率是限制总线应用的因素之一。这也说明为什么要引入高速模式(3.4Mbps)。在发起一次高速模式传输前,主设备必须先在低速的模式下(例如快速模式)发出特定的“High Speed Master”信号。为缩短信号的周期和提高总线速度,高速模式必须使用额外的I/O缓冲区。另外,总线仲裁在高速模式下可屏蔽掉。

时钟拉伸

在IIC通信中,主设备决定了时钟速度,因为时钟脉冲信号是由主设备显式发出的。但是当从设备没法跟上主设备的速度时,从设备需要一种机制来请求主设备慢一点。这种机制称为时钟拉伸,基于IIC特殊性,这种机制得以实现。当从设备需要降低传输的速度时,它可以拉下时钟线,逼迫主设备进入等待状态,知道从设备释放时钟线,通信才继续。

IIC数据传输

主设备往从设备写数据(->):

S 从设备地址 W(0) ACK 数据 ACK 数据 ACK P
开始 7位 写(1位) 应答 一字节 应答 一字节 应答 停止

主设备往从设备读数据(<-):

S 从设备地址 R(1) ACK 数据 ACK 数据 ACK P
开始 7位 读(1位) 应答 一字节 应答 一字节 应答 停止

主设备往从设备写数据+主设备往从设备读数据或主设备往从设备读数据+主设备往从设备写数据:可重启开始条件:

S 从设备地址 R/W ACK 数据 ACK RS 从设备地址 R/W 数据 ACK P
开始 读/写 开始

参考链接

I2C总线 百度百科

IIC总线解析

2、SPI总线及其知识点

SPI是串行外设接口(Serial Peripheral Interface),是Motorola公司推出的一种同步串行接口技术,是一种高速的、全双工、同步的通信总线,被广泛应用于ADC、LCD等设备与MCU之间。

SPI优点:支持全双工通信,通信简单,数据传输速率快;

缺点:没有指定的流控制,没有应答机制确认是否接收到数据,所以跟IIC总线协议比较在数据可靠性上有一定的缺陷。

特点:高速、同步、全双工、非差分、总线式;主从通信模式。

协议通信时序

1)SPI的通信原理很简单,它以主从方式工作,这种模式通常有一个主设备和一个或多个从设备,需要至少4根线,事实上3根也可以(单向传输时)。也是所有基于SPI的设备共有的,它们是SDI(数据输入)、SDO(数据输出)、SCLK(时钟)、CS(片选)。

  • SDO/MOSI:主设备数据输出,从设备数据输入;
  • SDI/MISO:主设备数据输入,从设备数据输出;
  • SCLK:时钟信号,由主设备产生;
  • CS/SS:从设备使能信号,由主设备控制。当有多个从设备的时候,因为每个从设备上都有一个片选引脚接入到主设备机中,当我们的主设备和某个从设备通信时将需要将从设备对应的片选引脚拉低或是拉高。

2)SPI有四种不同的模式,不同从设备可能在出厂时就是配置为某种模式,无法改变;但是为了能够通信,需要设置为同一工作模式下,所以可以对主设备的SPI模式进行配置,通过CPOL(时钟极性)和CPHA(时钟相位)为控制我们主设备的通信模式,具体如下:

  • Model0:CPOL=0,CPHA=0;
  • Model1:CPOL=0,CPHA=1;
  • Model2:CPOL=1,CPHA=0;
  • Model3:CPOL=1,CPHA=1。

时钟极性CPOL用于配置SCLK的电平处于哪种状态时是空闲态或者有效态,时钟相位CPHA是用于配置数据采样是在第几个边沿:

  • CPOL=0,表示当SCLK=0时处于空闲态,所以有效状态就是SCLK处于高电平时;
  • CPOL=1,表示当SCLK=1时处于空闲态,所以有效状态就是SCLK处于低电平时;
  • CPHA=0,表示数据采样是在第1个边沿,数据发送在第2个边沿;
  • CPHA=1,表示数据采样是在第2个边沿,数据发送在第1个边沿;

例如:

  • CPOL=0,CPHA=0:此时空闲态时,SCLK处于低电平,数据采样是在第1个边沿,也就是 SCLK由低电平到高电平的跳变,所以数据采样是在上升沿,数据发送是在下降沿。
  • CPOL=0,CPHA=1:此时空闲态时,SCLK处于低电平,数据发送是在第1个边沿,也就是 SCLK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。
  • CPOL=1,CPHA=0:此时空闲态时,SCLK处于高电平,数据采集是在第1个边沿,也就是 SCLK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。
  • CPOL=1,CPHA=1:此时空闲态时,SCLK处于高电平,数据发送是在第1个边沿,也就是 SCLK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。

注意:在SPI中,当没有数据交流时,时钟线要么保持高电平,要么保持低电平。

内部工作机制

SSPSR是SPI设备内部的移位寄存器(Shift Register),它的主要作用是根据SPI时钟信号状态,往SSPBUF里移入或者移出数据,每次移动的数据大小由Bus-Width以及Channel-Width所决定。

参考链接

SPI通信协议(SPI总线)学习

3、ARM各个系列有什么区别,m低功耗、r实时、a高性能如何实现??

ARM公司自2004年推出ARMv7内核架构时,摒弃了以往"ARM+数字"这种处理器命名方法(ARM11之前的处理器统称经典处理器系列),重新启用Cortex来命名,并将Cortex系列细分为三大类:

  • Cortex-A系列:A是Application的缩写,面向性能密集型系统的应用处理器内核
  • Cortex-R系列:R是RealTime的缩写,面向实时应用的高性能内核
  • Cortex-M系列:M是Micro的缩写,面向各类嵌入式应用的微控制器内核,Cortex-M系列主要是用来取代经典处理器ARM7系列(比如基于ARMv4架构的ARM7TDMI),Cortex-M比ARM7的架构高了3代,性能也有较大提升,所以新的设计推荐使用Cortex-M。

A8/A9/A12/A15和STM32其实都是基于ARMv7架构的,只不过A8/A9/A12/A15是基于基于ARMv7-A 架构的;STM32是基于使用ARMv7-m架构的,用的cm3。

ARM7:ARMv4架构,ARM9:ARMv5架构,ARM11:ARMv6架构,ARM-Cortex 系列:ARMv7架构、 ARM7没有MMU(内存管理单元),只能叫做MCU(微控制器),不能运行诸如Linux、WinCE等这些现代的多用户多进程操作系统, 因为运行这些系统需要MMU,才能给每个用户进程分配进程自己独立的地址空间 。

ARM系列按照时间顺序

ARM7(ARMv4/ARMv5内核,冯诺依曼结构)–>ARM9(ARMv5内核,哈佛结构)–>ARM11(ARMv6内核,哈佛结构)->Cortex-A5/A7/A8/A9/A12/A15(32位的ARMv7-A内核,哈佛结构)–>Cortex-A57/A53(64位的ARMv8架构,哈佛结构)

ARMv7 和ARM7:

  • ARMv7是内核,现在STM32和A8/A9/A12/A15之类依然在用ARMv7的内核。现在市面大多数常用的ARM的片子都是基于ARMv7内核的。直到ARM推出了Cortex-A53和Cortex-A57这两款属于Cortex-A50系列的CPU,首次采用了64位的ARMv8架构。

  • ARM7发布很早,最早推出是在1994年的时候,包括ARM7TDMI-S(基于ARMv4T架构)和ARM7EJ-S(基于ARMv5TEJ架构等等)。现在连嵌入式领域都已经不用了。

ARM9 和ARM A9

  • ARM A9通常指的是ARM Cortex-A系列的A9芯片。它的内核是ARMv7系列的,用的ARMv7-A架构。
  • ARM9 是基于ARMv5架构的,ARM11 是基于ARMv6架构的 ARM9和ARM11已经是很老的片子了,用得很少。现在学ARM基本都是从Cortex-A8/A9 上手。

ARM7,ARM9的区别在于是否有MMU(存储器管理单元)或MPU(存储器保护单元)架构上v5E相比v4T则是在于v5E新加入的增强型DSP(数字信号处理)指令,v4T则是Thumb指令集的加入,v6架构则是开始支持SIMD以及Thumb2的问世。

参考链接

ARM各系列CPU与STM32之间的关系

ARM7、ARM9、ARM11、ARM-Cortex系列的关系

4、冯·诺依曼和哈佛结构

冯·诺依曼

冯·诺依曼结构又称作普林斯顿体系结构(Princeton Architecture)。冯·诺依曼结构的处理器使用同一个存储器,经由同一个总线传输,通过分时复用的方式进行,缺点时在高速运行时,不能达到同时取指令和取操作数,从而形成了传输过程的瓶颈。由于程序指令存储地址和数据地址指向同一个存储器的不同物理位置,因此程序指令和数据的宽度相同,冯·诺依曼结构处理器具有以下特点:

  • 必须有一个存储器;
  • 必须有一个控制器;
  • 必须有一个运算器,用于完成算术运算和逻辑运算;
  • 必须有输入和输出设备,用于进行人机通信。

哈佛结构

哈佛结构是一种将程序指令存储和数据存储分开的存储器结构。中央处理器首先到程序指令存储器中读取程序指令内容,解码后得到数据地址,再到相应的数据存储器中读取数据,并进行下一步操作。程序指令存储和数据存储分开,可以使指令和数据有不同的数据宽度。

  • 哈佛结构的微处理器通常具有较高的执行效率。其程序指令和数据指令分开组织和存储的,执行时可以预先读取下一条指令;
  • 哈佛结构是指程序和数据空间独立的体系结构,目的是为了减轻程序运行时的访存瓶颈;
  • 哈佛结构能基本上解决取指和取数的冲突问题。

改进型哈佛结构

改进型哈佛结构虽然也使用两个不同的存储器:程序存储器和数据存储器,但它把两个存储器的地址总线合并了,数据总线也进行了合并,即原来的哈佛结构需要4条不同的总线,改进后需要两条总线。

改进型哈佛结构特点为:

  • 使用两个独立的存储器模块,分别存储指令和数据,每个存储模块都不允许指令和数据并存,以便实现并行处理;
  • 具有一条独立的地址总线和一条独立的数据总线,利用公用地址总线访问两个存储模块(程序存储模块和数据存储模块),公用数据总线则被用来完成程序存储模块或数据存储模块与CPU之间的数据传输;
  • 两条总线由程序存储器和数据存储器分时共用。

总结

  • 哈佛结构的高性能体现在单片机、DSP芯片平台上运行的程序种类和花样较少,因为各个电子娱乐产品中的软件升级较少,应用程序可以用汇编作为内核,最高效率的利用流水线技术,获得最高的效率;
  • 冯·诺依曼主要是基于电脑购买者对电脑的使用途径不同,如各种娱乐用户、专业用户等,且安装的软件种类多、升级频繁、多种软件同时运行时处理的优先级比较模糊;
  • 冯氏结构简单、易实现、成本低,但效率偏低;哈佛结构效率高但复杂,对外围的连接与处理要求高,十分不适合外围存储器的扩展。现在的处理器依托CACHE,已经很好的将二者统一起来,虽然外部总线上看是冯诺依曼结构,但是由于内部CACHE的存在,因此实际上内部来看已经类似改进型哈佛结构了。

参考链接

  • 什么是冯诺依曼结构、哈佛结构、改进型哈佛结构?
  • 哈佛结构和冯诺依曼结构

4、嵌入式操作系统

1、操作系统任务调度和优先级(相同优先级如何处理)

5、计算机网络基础

1、IP协议的头有多长

头部长度:通常20字节,有选项时更长,总共不超过60字节。
IP数据报长度:65535字节。

  • 版本(Version)字段:占4比特。用来表明IP协议实现的版本号,当前一般为IPv4,即0100。
  • 报头长度(Internet Header Length,IHL)字段:占4比特。是头部占32比特的数字,包括可选项。普通IP数据报(没有任何选项),该字段的值是5,即160比特=20字节。此字段最大值为60字节。
  • 服务类型(Type of Service ,TOS)字段:占8比特。其中前3比特为优先权子字段(Precedence,现已被忽略)。第8比特保留未用。第4至第7比特分别代表延迟、吞吐量、可靠性和花费。当它们取值为1时分别代表要求最小时延、最大吞吐量、最高可靠性和最小费用。这4比特的服务类型中只能置其中1比特为1。可以全为0,若全为0则表示一般服务。服务类型字段声明了数据报被网络系统传输时可以被怎样处理。例如:TELNET协议可能要求有最小的延迟,FTP协议(数据)可能要求有最大吞吐量,SNMP协议可能要求有最高可靠性,NNTP(Network News Transfer Protocol,网络新闻传输协议)可能要求最小费用,而ICMP协议可能无特殊要求(4比特全为0)。实际上,大部分主机会忽略这个字段,但一些动态路由协议如OSPF(Open Shortest Path First Protocol)、IS-IS(Intermediate System to Intermediate System Protocol)可以根据这些字段的值进行路由决策。
  • 总长度字段:占16比特。指明整个数据报的长度(以字节为单位)。最大长度为65535字节。
  • 标志字段:占16比特。用来唯一地标识主机发送的每一份数据报。通常每发一份报文,它的值会加1。
  • 标志位字段:占3比特。标志一份数据报是否要求分段。
  • 段偏移字段:占13比特。如果一份数据报要求分段的话,此字段指明该段偏移距原始数据报开始的位置。
  • 生存期(TTL:Time to Live)字段:占8比特。用来设置数据报最多可以经过的路由器数。由发送数据的源主机设置,通常为32、64、128等。每经过一个路由器,其值减1,直到0时该数据报被丢弃。
  • 协议字段:占8比特。指明IP层所封装的上层协议类型,如ICMP(1)、IGMP(2) 、TCP(6)、UDP(17)等。
  • 头部校验和字段:占16比特。内容是根据IP头部计算得到的校验和码。计算方法是:对头部中每个16比特进行二进制反码求和。(和ICMP、IGMP、TCP、UDP不同,IP不对头部后的数据进行校验)。
  • 源IP地址、目标IP地址字段:各占32比特。用来标明发送IP数据报文的源主机地址和接收IP报文的目标主机地址。
  • 可选项字段:占32比特。用来定义一些任选项:如记录路径、时间戳等。这些选项很少被使用,同时并不是所有主机和路由器都支持这些选项。可选项字段的长度必须是32比特的整数倍,如果不足,必须填充0以达到此长度要求。

参考链接

ip头、tcp头、udp头详解及定义,结合Wireshark抓包看实际情况

TCP/IP报文头部结构整理

网络协议相关,主要是TCP,IP协议栈相关

2、TCP头有多长

头部长度:一般为20字节,选项最多40字节,限制60字节。

  • 源、目标端口号字段:占16比特。TCP协议通过使用"端口"来标识源端和目标端的应用进程。端口号可以使用0到65535之间的任何数字。在收到服务请求时,操作系统动态地为客户端的应用程序分配端口号。在服务器端,每种服务在"众所周知的端口"(Well-Know Port)为用户提供服务。
  • 顺序号字段:占32比特。用来标识从TCP源端向TCP目标端发送的数据字节流,它表示在这个报文段中的第一个数据字节。
  • 确认号字段:占32比特。只有ACK标志为1时,确认号字段才有效。它包含目标端所期望收到源端的下一个数据字节。
  • 头部长度字段:占4比特。给出头部占32比特的数目。没有任何选项字段的TCP头部长度为20字节;最多可以有60字节的TCP头部。
  • 标志位字段(U、A、P、R、S、F):占6比特。各比特的含义如下:
    • URG:紧急指针(urgent pointer)有效。
    • ACK:确认序号有效。
    • PSH:接收方应该尽快将这个报文段交给应用层。
    • RST:重建连接。
    • SYN:发起一个连接。
    • FIN:释放一个连接。
  • 窗口大小字段:占16比特。此字段用来进行流量控制。单位为字节数,这个值是本机期望一次接收的字节数。
  • TCP校验和字段:占16比特。对整个TCP报文段,即TCP头部和TCP数据进行校验和计算,并由目标端进行验证。
  • 紧急指针字段:占16比特。它是一个偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。
  • 选项字段:占32比特。可能包括"窗口扩大因子"、"时间戳"等选项。

参考链接

ip头、tcp头、udp头详解及定义,结合Wireshark抓包看实际情况

TCP/IP报文头部结构整理

3、TCP选项(Option)有什么内容

TCP头部一般20字节,最大60字节,所以TCP Option最大长度为40字节。TCP Option一般数据结构如下:

Kind(1字节)+Length(1字节)+Info(n字节)

目前,TCP常用的Option如下:

Kind(Type) Length Name Reference 描述 & 用途
0 1 EOL RFC 793 选项列表结束
1 1 NOP RFC 793 无操作(用于补位填充)
2 4 MSS RFC 793 最大Segment长度
3 3 WSOPT RFC 1323 窗口扩大系数(Window Scaling Factor)
4 2 SACK-Premitted RFC 2018 表明支持SACK
5 可变 SACK RFC 2018 SACK Block(收到乱序数据)
8 10 TSPOT RFC 1323 Timestamps
19 18 TCP-MD5 RFC 2385 MD5认证
28 4 UTO RFC 5482 User Timeout(超过一定闲置时间后拆除连接)
29 可变 TCP-AO RFC 5925 认证(可选用各种算法)
253/254 可变 Experimental RFC 4727 保留,用于科研实验
  • EOL和NOP Option(Kind 0、Kind 1)只占1 Byte,没有Length和Value字段;
  • NOP用于 将TCP Header的长度补齐至32bit的倍数(由于Header Length字段以32bit为单位,因此TCP Header的长度一定是32bit的倍数);
  • SACK-Premitted Option占2 Byte,没有Value字段;
  • 其余Option都以1 Byte的“Kind”开头,指明Option的类型;Length指明Option的总长度(包括Kind和Length)
  • 对于收到“不能理解”的Option,TCP会无视掉,并不影响该TCP Segment的其它内容;

参考链接

Tcp报文简介以及头部选项字段(Tcp Options字段)

常用的TCP Option

4、socket编程需要用到的函数和步骤

TCP编程的服务器端一般步骤是

1、 创建一个socket,用函数socket();

2、 设置socket属性,用函数setsockopt(); * 可选

3、 绑定IP地址、端口等信息到socket上,用函数bind();

4、 开启监听,用函数listen();

5、 接收客户端上来的连接,用函数accept();

6、 收发数据,用函数send()和recv(),者read()和write();

7、 关闭网络连接;

8、 关闭监听;

TCP编程的客户端一般步骤是:

1、 创建一个socket,用函数socket();

2、 设置socket属性,用函数setsockopt();* 可选

3、 设置要连接的对方的IP地址和端口等属性;

4、 连接服务器,用函数connect();

5、 收发数据,用函数send()和recv(),或者read()和write();

6、 关闭网络连接;

UDP编程的服务器端一般步骤是:

1、 创建一个socket,用函数socket();

2、 设置socket属性,用函数setsockopt();* 可选

3、 绑定IP地址、端口等信息到socket上,用函数bind()

4、 循环接收数据,用函数recvfrom();

5、 关闭网络连接;

UDP编程的客户端一般步骤是:

1、 创建一个socket,用函数socket();

2、 设置socket属性,用函数setsockopt();* 可选

3、 设置对方的IP地址和端口等属性;

4、 发送数据,用函数sendto();

5、 关闭网络连接;

参考链接

socket编程函数和步骤

5、MSS是什么,MTU是什么

**MSS(最大报文段大小):**指TCP层所能够接收的最大段大小,该值只包括TCP段的数据部分,不包括选项部分。)

**MTU最大传输单元:**MTU为1500个字节。一个IP数据报在以太网中传输,如果它的长度大于该MTU值,就要进行分片传输,使得每片数据报的长度小于MTU。分片传输的IP数据报不一定按序到达,但IP首部中的信息能让这些数据报片按序组装

6、TCP四次分手说一下,为什么要四次分手

参考链接:

(重要!!!!)面试官,不要再问我三次握手和四次挥手

7、2MSL有什么用

概念:MSL是报文在网络中最长生存时间,这是一个工程值(经验值),不同的系统中可能不同。

场景:

  1. A发出ACK后,等待一段时间T,确保如果B重传FIN自己一定能收到

分析:

  1. ACK从A到B最多经过1MSL,超过这个时间B会重发FIN
  2. B重发的FIN最多经过1MSL到达A

结论:如果B重发了FIN,且网络没有故障(重发的FIN被丢弃或错误转发),那么A一定能在2MSL之内收到该FIN,因此A只需要等待2MSL。

参考链接

为什么TCP4次挥手时等待为2MSL?

8、DNS协议说说

参考链接

DNS协议分析

6、Linux操作系统基础

1、linux怎么查找当前目录

PWD

2、Linux怎么查看内存:

free -m

3、linux怎么查看进程

ps a 显示现行终端机下的所有程序,包括其他用户的程序。

ps -A 显示所有程序。

ps e 列出程序时,显示每个程序所使用的环境变量。

ps f 用ASCII字符显示树状结构,表达程序间的相互关系。

ps(ps -ef|grep XXX):按照关键词(XXX)搜索进程。

杀死进程:

使用kill命令结束进程:kill xxx

常用:kill -9 324

Linux下还提供了一个killall命令,可以直接使用进程的名字而不是进程标识号,例如:# killall -9 NAME

参考链接

Linux如何查看进程、杀死进程、启动进程等常用命令

4、linux怎么查看进程对应的端口号

linux 下查看进程占用端口:

  • 查看程序对应的进程号: ps -ef | grep 进程名字
  • 查看进程号所占用的端口号: netstat -nltp | grep 进程号
  • ubuntu :查看进程占用端口号:netstat -anp | grep pid

注意:遇到无法使用netstat指令,可能是系统没有安装网络组件工具,使用sudo apt install net-tools安装即可。

参考链接

linux 下查看进程占用端口和端口号占用进程命令

在centos7系统中无法使用命令netstat

5、用户态怎么和内核态通讯

参考链接

Linux用户态与内核态通信的几种方式

Linux用户态与内核态通信的几种方式

6、Linux进程通信

参考链接

进程间通信IPC (InterProcess Communication)

Linux进程间通信的几种方式总结–linux内核剖析(七)

7、进程与线程的区别,应用?

两者区别

  • 进程是分配资源的基本单位;线程是系统调度和分派的基本单位。
  • 属于同一进程的线程,堆是共享的,栈是私有的。
  • 属于同一进程的所有线程都具有相同的地址空间。

参考链接

进程和线程的区别,以及应用场景

进程与线程区别以及应用场景

8、mutex和spinlock的区别

spin_lock和mutex两个都是互斥锁,不同的地方是spinlock是忙等待,不支持睡眠
mutex是可以睡眠,把当前等待mutex的task置于睡眠等待队列中,等mutex被释放之后再调度。

mutex:互斥锁

  1. mutex获取一旦失败,进程会进入sleep
  2. 防止多处理器中并发访问临界区,防止内核抢占造成的竞争

spin_lock:自旋锁

  1. 忙等待,等待该锁的cpu会耗费大量资源;无调度开销,忙等待的task不能被其他task打断
  2. 进程的抢占被禁止
  3. 锁定期间不能睡眠。
  4. 防止多处理器并发访问临界资源
  5. 可以被中断打断,进而去抢占

spin_lock_irqsave:禁止内核抢占,关闭中断,保存中断状态寄存器的标志位spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq = spin_unlock() + local_irq_enable()
spin_lock_irqsave = spin_lock() + local_irq_save()
spin_lock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_nlock_bh() = spin_unlock() + local_bh_enable()

参考链接

spinlock和mutex的区别

mutex与spinlock的区别和应用场景

9、Linux抢占怎么实现

参考链接

Linux用户抢占和内核抢占详解(概念, 实现和触发时机)–Linux进程的管理与调度(二十)

10、IRQ和FIQ的区别,为什么要有FIQ?

FIQ和IRQ是两种不同类型的中断,ARM为了支持这两种不同的中断,提供了对应的叫做FIQ和IRQ处理器模式(ARM有7种处理模式)。

一般的中断控制器里我们可以配置与控制器相连的某个中断输入是FIQ还是IRQ,所以一个中断是可以指定为FIQ或者IRQ的,为了合理,要求系统更快响应,自身处理所耗时间也很短的中断设置为FIQ,否则就设置了IRQ。

FIQ比IRQ有更高优先级,FIQ优先级为3,IRQ优先级为4。如果FIQ和IRQ同时产生,那么FIQ先处理;

参考链接

FIQ和IRQ区别

11、copy_to_user和memcopy的区别,如何用memcopy实现copy_to_user的功能?

参考链接

copy_{to,from}_user Vs memcpy

12、用过GDB吗,常用的GDB调试方法?

参考链接

GDB常用的调试命令及方法总结

13、poll、select的区别,它们是同步的还是异步的

参考链接

select、poll、epoll、同步、异步之间的区别总结[整理]

select、poll、epoll之间的区别

14、什么是同步

计算机领域中的同步和异步的概念和我们平时生活中的同步和异步是不一样的,这就让很多人难以理解。

同步就是整个处理过程顺序执行,当各个过程都执行完毕,并返回结果。是一种线性执行的方式,执行的流程不能跨越。一般用于流程性比较强的程序,比如用户登录,需要对用户验证完成后才能登录系统。

异步则是只是发送了调用的指令,调用者无需等待被调用的方法完全执行完毕;而是继续执行下面的流程。是一种并行处理的方式,不必等待一个程序执行完,可以执行其它的任务,比如页面数据加载过程,不需要等所有数据获取后再显示页面。

他们的区别就在于一个需要等待,一个不需要等待,在部分情况下,我们的项目开发中都会优先选择不需要等待的异步交互方式,比如日志记录就可以使用异步方式进行保存。

参考链接

计算机领域中的同步(Synchronous)和异步(Asynchronous)

15、系统调用是什么

16、进程的虚拟内存了解吗,说说是怎么组成的

17、64位系统的虚拟内存

18、预编译…编译…汇编…链接过程中每一个阶段的源文件里面什么

19、系统API特性

20、系统实现原理

21、内存管理原理

22、操作系统实现原理

23、uboot启动流程

24、camera参数

25、Linux内存空间分配,分用户空间和内存空间

26、CPU调度的最小单位

27、Linux内核中进程调度的方式

28、OS造成缓存一致性的原因

29、Linux同步和异步的概念

30、Linux内核中实现同步的机制

31、Linux驱动模型platform的介绍

32、Linux驱动模块的编译和加载方式、设备号分配

33、Linux总线

7、手撕代码

博主两次面试的题目比较简单:

  • 一面:输入一个int64类型,输出对应的字符串,不能使用内置函数,如输入1234567,输出“1234567”

  • 二面:输入一串字符串,将字符串中的“*”字符提前,其它往后移,如输入“a**b*cd*ef”,输出“****abcdef”

以下为博主根据各大平台面经总结的代码题目:

1、反转部分链表*2

题目

反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。

说明:
1 ≤ m ≤ n ≤ 链表长度。

示例:

输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL

解题思路

如果 m 等于 n 不用反转直接返回
沿用反转整个链表的三指针方式,三个指针分别指向前一个节点、当前节点和后一个节点。
设指针 left 为 m 位置结点的前一个节点,right 为 n 位置的节点。
反转后,left->next 应为 n 位置的节点,right->next 应为 n->next 位置的节点。
以 1 2 3 4 5,m = 2, n = 4 为例:

如果 m = 1 也就是从头节点开始反转,那么新的头节点应该为 n 位置的节点,
如果 m > 1 那么头节点没有变,还应该是 head

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/struct ListNode* reverseBetween(struct ListNode* head, int m, int n)
{if (m == n)return head;struct ListNode *prev = NULL, *next = NULL, *p = head;// left is the m point left point, right is the m point// left->next should be the n point, right->next should be the n->next pointstruct ListNode *left = NULL, *right = NULL;int count = 1;while (p != NULL && count <= n){next = p->next;if (count == m){left = prev;right = p;}if (count >= m)p->next = prev;if (count == n){if (left != NULL)left->next = p;right->next = next;// if reverse from the original head, the new head should locate on the n point// otherwise the head should be the original headif (m == 1)head = p;}prev = p;p = next;count++;}return head;
}

参考链接

力扣(LeetCode)

2、判断链表是否有环

题目

给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。 否则,返回 false 。

解题思路

最容易想到的方法是遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。

具体地,我们可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。

struct hashTable {struct ListNode* key;UT_hash_handle hh;
};struct hashTable* hashtable;struct hashTable* find(struct ListNode* ikey) {struct hashTable* tmp;HASH_FIND_PTR(hashtable, &ikey, tmp);return tmp;
}void insert(struct ListNode* ikey) {struct hashTable* tmp = malloc(sizeof(struct hashTable));tmp->key = ikey;HASH_ADD_PTR(hashtable, key, tmp);
}bool hasCycle(struct ListNode* head) {hashtable = NULL;while (head != NULL) {if (find(head) != NULL) {return true;}insert(head);head = head->next;}return false;
}

复杂度分析

  • 时间复杂度:O(N)O(N),其中 NN 是链表中的节点数。最坏情况下我们需要遍历每个节点一次。
  • 空间复杂度:O(N)O(N),其中 NN 是链表中的节点数。主要为哈希表的开销,最坏情况下我们需要将每个节点插入到哈希表中一次。

参考链接

力扣(LeetCode)

3、删除链表的倒数第n个节点

题目

给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

示例:

给定一个链表: 1->2->3->4->5, 和 n = 2.

当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:

给定的 n 保证是有效的。

进阶:

你能尝试使用一趟扫描实现吗?

思路与算法(一次遍历)

我们也可以在不预处理出链表的长度,以及使用常数空间的前提下解决本题。

由于我们需要找到倒数第 n 个节点,因此我们可以使用两个指针first 和 second 同时对链表进行遍历,并且first 比second 超前 n 个节点。当 first 遍历到链表的末尾时,second 就恰好处于倒数第 n 个节点。

具体地,初始时 first 和 second 均指向头节点。我们首先使用 first 对链表进行遍历,遍历的次数为 n。此时,first 和second 之间间隔了 n−1 个节点,即 first 比 second 超前了 n 个节点。

在这之后,我们同时使用 first 和 second 对链表进行遍历。当 first 遍历到链表的末尾(即 first 为空指针)时,second 恰好指向倒数第 n 个节点。

根据方法一和方法二,如果我们能够得到的是倒数第 n 个节点的前驱节点而不是倒数第 n 个节点的话,删除操作会更加方便。因此我们可以考虑在初始时将 second 指向哑节点,其余的操作步骤不变。这样一来,当first 遍历到链表的末尾时,second 的下一个节点就是我们需要删除的节点。

struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {struct ListNode* dummy = malloc(sizeof(struct ListNode));dummy->val = 0, dummy->next = head;struct ListNode* first = head;struct ListNode* second = dummy;for (int i = 0; i < n; ++i) {first = first->next;}while (first) {first = first->next;second = second->next;}second->next = second->next->next;struct ListNode* ans = dummy->next;free(dummy);return ans;
}

复杂度分析

  • 时间复杂度:O(L)O(L),其中 LL 是链表的长度。
  • 空间复杂度:O(1)O(1)。

参考链接

19. 删除链表的倒数第N个节点

4、二叉树的最大深度

题目

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:
给定二叉树 [3,9,20,null,null,15,7]

 3
/ \
9  20
/  \
15   7

返回它的最大深度 3 。

思路与算法(递归)

如果我们知道了左子树和右子树的最大深度 ll 和 rr,那么该二叉树的最大深度即为

max(l,r)+1

而左子树和右子树的最大深度又可以以同样的方式进行计算。因此我们在计算当前二叉树的最大深度时,可以先递归计算出其左子树和右子树的最大深度,然后在 O(1) 时间内计算出当前二叉树的最大深度。递归在访问到空节点时退出。

int maxDepth(struct TreeNode *root) {if (root == NULL) return 0;return fmax(maxDepth(root->left), maxDepth(root->right)) + 1;
}

复杂度分析

时间复杂度:O(n),其中 n 为二叉树节点的个数。每个节点在递归中只被遍历一次。
空间复杂度:O(height),其中 height 表示二叉树的高度。递归函数需要栈空间,而栈空间取决于递归的深度,因此空间复杂度等价于二叉树的高度。

参考链接

104.二叉树的最大深度

5、反转字符串

思路与算法

对于长度为N 的待被反转的字符数组,我们可以观察反转前后下标的变化,假设反转前字符数组为s[0] s[1] s[2] ... s[N - 1],那么反转后字符数组为 s[N - 1] s[N - 2] ... s[0]。比较反转前后下标变化很容易得出 s[i]的字符与s[N - 1 - i] 的字符发生了交换的规律,因此我们可以得出如下双指针的解法:

left指向字符数组首元素,right指向字符数组尾元素。
left < right
交换s[left]s[right]
left指针右移一位,即 left = left + 1
right 指针左移一位,即right = right - 1
left >= right,反转结束,返回字符数组即可。

void swap(char *a, char *b) {char t = *a;*a = *b, *b = t;
}void reverseString(char *s, int sSize) {for (int left = 0, right = sSize - 1; left < right; ++left, --right) {swap(s + left, s + right);}
}

复杂度分析

  • 时间复杂度:O(N),其中 NN 为字符数组的长度。一共执行了 N/2N/2 次的交换。
  • 空间复杂度:O(1)。只使用了常数空间来存放若干变量。

6、统计字符串

题目

统计字符串中的单词个数,这里的单词指的是连续的不是空格的字符。

请注意,你可以假定字符串里不包括任何不可打印的字符。

示例:

输入: “Hello, my name is John”
输出: 5
解释: 这里的单词是指连续的不是空格的字符,所以 “Hello,” 算作 1 个单词。

解题思路

判定一个单词结束:当前字符非空格 && 下一字符为空格或结束符

int countSegments(char * s){int cnt = 0;while (*s) {if (*s != ' ' && (*(s + 1) == ' ' || *(s + 1) == '\0'))cnt++;s++;}return cnt;
}

力扣(LeetCode)

7、数组实现一个堆

堆满足两个要求:

  1. 完全二叉树
  2. 父节点的元素大于(或小于)子节点的元素

堆的实现

为了实现一个堆,我们需要创造一个堆的数据结构,以及实现堆的插入和删除等操作函数。

堆的存储

由于堆是完全二叉树,因此可以用数组存放堆。第i个节点就放在数组的第i个位置上。它的左子节点是 2i, 它的右子节点是2i+1, 它的父节点是i/2.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>/* 堆(heap)* 满足两个要求:*   1. 完全二叉树*   2. 父节点的元素大于(或小于)子节点的元素*/typedef struct _heap {int *heap; //堆int n; //堆的大小int count; //堆目前的元素
} heap;// 交换数组的元素arr[idx1]和arr[idx2]
void swap(int *arr, int idx1, int idx2)
{int tmp = arr[idx1];arr[idx1] = arr[idx2];arr[idx2] = tmp;
}// 打印堆中的元素
void dump(heap *h){int n = h->n;int count = h->count;int *heap = h->heap;int i;for ( i = 1; i <= count; i++){printf("%08d\n", heap[i]);}}//构建一个大小为n的堆
heap* createHeap(int n){heap *h;h = (heap*)malloc( sizeof(heap) );h->heap = (int*)malloc( sizeof(int) * (n+1));h->n = n;h->count = 0;return h;
}//插入元素
void insertElement(heap *h, int e){int count = h->count;int n = h->n;if ( count >= n) return;// 在数组最后加入新的元素int *heap = h->heap;heap[++count] = e;h->count = count;int i = count;// 数组索引//堆化,while( (i/2) > 0 && heap[i] > heap[i/2] ){swap(heap, i, i/2); //交换父子节点i = i / 2;}
}//删除顶部元素
void removeTop(heap *h){if (h->count < 1) return ;int count = h->count;int *heap = h->heap;//用最后一个元素替代第一个元素heap[1] = heap[count];//删除最后一个元素h->count = --count;// 自上而下堆化int i = 1;while ( true ){int max_pos = i;if ( i*2 <=count && heap[i] < heap[i*2]) max_pos = i*2; //和左子节点比较if ( i*2+1<=count && heap[i] < heap[i*2+1]) max_pos = i*2+1; //和右子节点比较if (max_pos == i) break; // 不再发生交换, 当前位置就是最大位置swap(heap, i, max_pos); //将当前节点和子节点进行交换i = max_pos ; //将i设置为子节点的索引}}int main(int argc, char *argv[])
{heap *h;h = createHeap(100);int arr[] = {2,1,10,21,19,6,5,12,9};int i;for ( i = 0; i < sizeof(arr)/sizeof(int); i++){insertElement(h, arr[i]);}dump(h);printf("--------\n");removeTop(h);dump(h);
}

参考链接

learn-algo/heap.c

使用C语言实现堆(heap)

堆排序(大顶堆、小顶堆)----C语言

8、数组实现队列功能

队列结构体定义

#define QueueSize 40 // 队伍容量typedef struct{DataType queue[QueueSize];// 用于保存类型为DataType队列元素的数组int front,rear;// 用于保存队头和队尾下标信息
}SeqQueue;

实现算法

// 初始化队列
void InitQueue(SeqQueue *SQ){SQ->front = SQ->rear = 0;
}// 判断队列是否为空
int QueueEmpty(SeqQueue SQ){// 队头坐标与队尾坐标相等时,即为空队列if(SQ.front == SQ.rear){return 1;}else{return 0;      }
}// 入队操作
int EnterQueue(SeqQueue *SQ,DataType e){// 边界判断,假如队列满了不能入队if(SQ->rear == QueueSize){printf("队列已满,不能入队.\n");return 0;}// 新元素入队,需要将队尾指针往后移动SQ->queue[SQ->rear++] = e;return 1;
}// 出队操作
int DeleteQueue(SeqQueue *SQ,DataType *e){// 边界判断,假如队列空了不能出队if(SQ->front == SQ->rear){printf("队列已空,不能出队.\n");return 0;}// 元素出队,需要将队头指针往后移动*e = SQ->queue[SQ->front++];return 1;
}

参考链接

使用数组实现队列(C语言)

9、矩阵的旋转

问题描述

对于任意一个N*N的矩阵请依次按顺时针,逆时针,顺或逆180输出。

解题思路

对于矩阵的旋转只要熟练运用二维数组,其实它就是非常水的一道题。

以aij为例,i,j均从1开始计数

90度旋转:

  • 列号变为行号
  • (n - 行号 + 1)变成列号
  • 规律: a[i][j] = b[j][n - i + 1]

180度旋转:

  • (n - 行号 + 1)变为行号
  • (n - 列号 + 1)变为列号
  • 规律:a[i][j] = b[n - i + 1][n - j + 1]

270度旋转(相当于逆时针旋转90度):

  • 行号变为列号
  • (n - 列号 + 1)变为行号
  • 规律:a[i][j] = b[n - j + 1][i]
#include<stdio.h>
int main()
{int a[40][40];int i,j,n;while(scanf("%d",&n)!=EOF){for(i=0;i<n;i++)for(j=0;j<n;j++)scanf("%d",&a[i][j]);//顺时针旋转90for(i=0;i<4;i++){for(j=3;j>=0;j--)printf("%d ",a[j][i]);printf("\n");}printf("\n");//逆时针旋转90for(i=3;i>=0;i--){for(j=0;j<4;j++)printf("%d ",a[j][i]);printf("\n");}printf("\n");//顺时针转180或逆时针转180for(i=3;i>=0;i--){for(j=3;j>=0;j--)printf("%d ",a[i][j]);printf("\n");} }return 0;
}

题目描述

任意输入两个9阶以下矩阵,要求判断第二个是否是第一个的旋转矩阵,如果是,输出旋转角度(0、90、180、270),如果不是,输出-1。
要求先输入矩阵阶数,然后输入两个矩阵,每行两个数之间可以用任意个空格分隔。行之间用回车分隔,两个矩阵间用任意的回车分隔。

输入:
输入有多组数据。
每组数据第一行输入n(1<=n<=9),从第二行开始输入两个n阶矩阵。

输出:
判断第二个是否是第一个的旋转矩阵,如果是,输出旋转角度(0、90、180、270),如果不是,输出-1。

如果旋转角度的结果有多个,则输出最小的那个。

样例输入:
3
1 2 3
4 5 6
7 8 9
7 4 1
8 5 2
9 6 3
样例输出:
90

#include <stdio.h>
#include <stdlib.h>
#define len 10
int switchMatrix(int (*a)[len], int (*b)[len], int n);
int main(){int i, j, n, angle;int a[len][len], b[len][len];while(scanf("%d", &n) != EOF){//接收第一个矩阵for(i = 0; i < n; i ++){for(j = 0; j < n; j ++){scanf("%d", *(a + i) + j);}}//接收第二个数组for(i = 0; i < n; i ++){for(j = 0; j < n; j ++){scanf("%d", *(b + i) + j);}}//矩阵比较angle = switchMatrix(a, b, n);printf("%d\n", angle);}return 0;
}int switchMatrix(int (*a)[len], int (*b)[len], int n){int angle, i, j;for(angle = 0, i = 0; i < n; i ++){for(j = 0; j < n; j ++){if(angle == 0){if(a[i][j] == b[i][j]){continue;}else{angle = 90;}}if(angle == 90){if(a[i][j] == b[j][n - i - 1]){continue;}else{angle = 180;}}if(angle == 180){if(a[i][j] == b[n - i - 1][n - j -1]){continue;}else{angle = 270;}}if(angle == 270){if(a[i][j] == b[n - j - 1][i]){continue;}else{angle = -1;}}if(angle == -1){break;}}if(angle == -1){break;}}return angle;
}

参考链接

矩阵的旋转C语言

矩阵旋转——(c语言)

10、最大公共子序列

题目

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

示例 1:

输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。

示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。

示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。

提示:

1 <= text1.length <= 1000
1 <= text2.length <= 1000
输入的字符串只含有小写英文字符。

解题思路

  • 定义int dp[1001][1001]数组用于存放最长公共子序列的长度的所有值,并元素都初始化为0;
  • 遍历dp数组从1开始,检查判断text1[i-1]text2[j-1]的值,判断当前字符的最长公共子序列;
  • 遍历完成dp[strlen(text1)][strlen(text2)]的值即是最长公共子序列长度。
#define max(A,B) ((A) > (B) ? (A) : (B))
int longestCommonSubsequence(char * text1, char * text2){if(text1 == NULL || strlen(text1) == 0) return 0;if(text2 == NULL || strlen(text2) == 0) return 0;int i,j,lcs;int dp[1001][1001] = {0};for(i = 1; i <= strlen(text1); i++) {for(j = 1; j <= strlen(text2); j++) {if(text1[i - 1] == text2[j - 1]) {dp[i][j] = dp[i - 1][j - 1] + 1;} else {dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);}}}lcs = dp[strlen(text1)][strlen(text2)];return lcs;
}

参考链接

力扣(LeetCode)

11、输入uint64数值,格式化输出对应字符串

这是博主参加一面的面试题,面试官说勉强通过。

我的思路就是循环使用%取余,然后加上’0’,从而得到单个字符,不过这个时候是反序输出的,下一步是反转字符串就行。

博主比较笨,方法不是特别好,仅供参考。

8、其它问题

  1. 反问(新人入职的培训)
  2. 拿到了别的offer没有
  3. 对小米公司的看法。

小米嵌入式研发工程师校招面试总结相关推荐

  1. 求职招聘之Java开发工程师校招面试都有哪些步骤和注意事项?

    文章目录 Java开发工程师校招面试解析 网易案例 一般的校园招聘面试流程 业务逻辑面试(讲项目) 基础知识面试 Java开发工程师校招面试解析 网易案例 下面是一个网易Java开发工程师的基本要求: ...

  2. 谈谈前华为荣耀软件测试工程师校招面试(已拿到offer)

    截止到现在,一共参加了2次笔试,2次面试,具体时间参照截图 机试:一共三道编程题,共500分好像,全对了一道,就是提交后通过测试,另外两道写完了,但是提交测试没通过 性格测试: 就正常选了,保证前后一 ...

  3. 中兴通讯 嵌入式开发工程师 校招一面面经

    分享数字IC相关面经,内容来源于 SSP面试笔记 网站: 通过校园招聘投递简历,刚刚结束了一面技术面,问题简单,来分享下我的面试问题. 1.自我介绍 2.在校课程,成绩 3.项目介绍 4.用应为对简历 ...

  4. 大疆 嵌入式开发工程师 校招面经(已offer)

    分享数字IC相关面经,内容来源于 SSP面试笔记 网站: 分享一个完整的嵌入式开发面试经验,一共两轮,目前已经收到了offer,希望对大家有帮助. 一面 1.自我介绍 2.聊项目,问的很细 3.sta ...

  5. 中航锂电大数据开发工程师校招面试问题

    中航锂电是一家大型央企,但实际上在大数据方面并不成熟,面试我的是地区IT总监,但其实他也是数据库出身的,所以问的问题基本都是数据库相关,问题质量也挺水的,很容易就通过了. 问题一:简单介绍一下你常用的 ...

  6. 新华三 嵌入式开发工程师 校招 一面面经

    分享数字IC相关面经,内容来源于 SSP面试笔记 网站: 总体的表现都不是特别的好,分享自己的经验给大家,希望能给到你们帮助. 1.自我介绍 2.在校学了哪些课程,课题研究方向 3.参加过的项目,成果 ...

  7. 北京近期校园招聘java_JAVA研发工程师-校招,北京

    一.工作内容: 1.理解业务,沟通需求及功能实现: 2.按代码和流程规范,完成相关产品模块的设计.开发.测试及相关文档编写: 3.负责上线产品的维护和迭代,及时修复反馈的问题,保证上线产品的稳定运行: ...

  8. C 工程师校招面试考点基础篇汇总含答案解析

    9.请你说一下你理解的c++中的smart pointer四个智能指针: 10.请回答一下数组和指针的区别 11.请你回答一下野指针是什么? 12.请你介绍一下C++中的智能指针 13.请你回答一下* ...

  9. 软件工程师校招面试救急包

    LeetCode牛人总结(手撕代码前看看,抱佛脚) https://github.com/labuladong/fucking-algorithm/blob/master/README.md 剑指of ...

最新文章

  1. 历史命令history
  2. 【30集iCore3_ADP出厂源代码(ARM部分)讲解视频】30-10底层驱动之I2C
  3. 我读研时通过实习和比赛收入五十万
  4. CF-1207 G.Indie Album(Trie上跑AC自动机)
  5. chromium浏览器_微软将全面向Windows 10用户推送Chromium版Edge浏览器
  6. 整个csdn网站处于不死不活的状态
  7. java运算符重载_为什么Java不支持运算符重载?
  8. WCF与AJAX编程开发实践(2):支持ASP.NET AJAX的Web Service
  9. easypoi 表头数据导入_使用easypoi根据表头信息动态导出excel
  10. 58同城赶集网简历怎么下载?【58同城赶集网简历采集,真实手机号联系方式获取】
  11. 二阶低通有源滤波器设计与仿真测试
  12. 公司电脑加域之后用不了USB但是可以用鼠标键盘得解决方法
  13. 输入三个整数a,b,c。并进行两两相加,最后比较相加和的最大值。
  14. 软件测试的度量方法包括,软件测试过程的度量
  15. Linux中nvme驱动详解
  16. 世界上十个著名悖论详解
  17. thinkpad X1catbon2019款装系统时无法U盘启动解决办法
  18. 从FutureTask内部类WaitNode深入浅出分析FutureTask实现原理
  19. 什么是高内聚与低耦合?
  20. 资本退潮后,CEEC国际经贸链带你穿越币圈熊市!

热门文章

  1. android studio迁移,AndroidStudio 一键迁移至 AndroidX
  2. aws高可用mysql实现_Amazon RDS 的高可用性(多可用区) - Amazon Relational Database Service...
  3. 计算机毕业设计ssm基于微信的的高校起床协会管理61rmm系统+程序+源码+lw+远程部署
  4. 高中数学数列技巧解题秒杀视频:数列小题秒杀技巧
  5. indesign中怎么在冒号后面ctrl_InDesign不完全使用指南
  6. Java系统线上生产问题排查一把梭
  7. java lpad oracle_oracle中lpad函数是干嘛用的?
  8. 0基础编程资源大全(先收藏~慢慢看~)
  9. PostgreSQL测试套-pg_regress使用
  10. 图形学---中点画线法---opengl中实现