嵌入式工程师是指具有 C/C++ 语言、汇编语言等基础,熟悉模拟电子技术等硬件知识,了解处理器体系结构,做嵌入式系统设计和开发,包括硬件系统的建立和相关软件开发、移植、调试等工作的人。

面试八股文

1、C/C++
1.1 关键字
(参考”嵌入式及Linux那些事“以及众多帖子汇总而成)
 
volatile
​ 当声明指向设备寄存器的指针时一定要用volatile,它会告诉编译器不要对存储在这个地
址的数据进行假设。
 
​ 中断服务程序中修改的供其他程序检测的变量。 中断中直接从变量地址中读取数据,而不是从寄存器中读取。
 
​ 多线程应用中被几个任务共享的变量。
 
static
​ 1、函数体内的变量,这个变量只被声明一次。
 
​ 2、在模块内的变量,表示只能被模块内函数使用,不能被模块外函数访问,表示本地全局变量
 
​ 3、模块内的函数,限制在模块内使用,同上。
 
extern
1、引用同一文件变量
 
使用在声明之前时,可以使用关键字extern,让声明在程序任意位置。
 
2、引用另一个文件中的变量
 
extern可以引用其他文件中的全局变量,而且extern只需要指明数据类型和
 
extern int num=4; 这样不行。
 
3、引用另一个文件中的函数
 
可以不用包含头文件引用函数。
 
new/delete malloc/free
​ 1、new/delete是操作符,malloc/free是库函数
 
​ 2、new/delete可以调用构造函数/析构函数,m/f 只是分配内存。
 
struct 和 union区别
​ 1、联合体公用一块地址空间,联合体变量长度等于最长的成员的长度
 
​ 2、对不同成员赋值,会将其他成员重写。
 
const
​ 1、定义变量为常量
 
​ 2、修饰参数为常量
 
​ 3、修饰返回值为常量
 
总结:只读
 
sizeof和strlen
​ 1、sizeof是运算符,strlen是函数
 
​ 2、sizeof可以用类型、函数作为参数,strlen只能计算char*,还必须以/0结尾
 
​ 3、sizeof编译的时候计算,strlen是运行期计算,表示字符串长度,不是内存大小。
 
typedef和 define
1、都是替对象去一个别名,增强程序的可读性
 
2、define为预处理指令,不做正确性检查,只有带入之后才能发现
 
3、typedef用来定义类型别名,不止包含内部类型还包含自定义类型(与机器无关),方便记忆
 
4、define不仅可以给类型取别名,还能定义常量、变量、编译开关。
 
5、define没有作用域限制,typedef有。
 
# define还是 const ,谁定义常量最好
1、define只是文本替换,声明周期止于编译期,不分配内存空间,存在于代码段。const常量存在于数据段,堆栈中分配了空间。
 
2、const有数据类型,编译器可以对const进行安全检查。
 
3、const有保护常量不被修改的作用,提高程序的健壮性。
 
总结:一般倾向于用const定义常量
 
1.2 内存
C语言内存分配方式
1、静态储存区分配
 
2、栈上分配
 
3、堆上分配
 
C++内存管理是怎样的
分为代码段、数据段、BSS段、堆区、栈区、文件映射区
 
代码段:分为只读区和文本区,只读取储存字符串常量,文本区储存机器代码。
 
数据段:储存以及初始化的全局变量和静态变量
 
BSS段:储存未初始化的全局变量和静态变量,以及初始化为0的全局和静态。
 
堆区:手动分配的内存
 
栈:局部变量参数返回值等
 
映射区:储存动态链接库,mmap函数的文件映射
 
堆和栈的区别
1、申请方式。 栈为操作系统自动分配/释放,堆为手动
 
2、申请大小,栈空间有限,向低地址拓展的连续区域,堆是向高地址拓展的不连续区域,链表储存的空闲地址。
 
3、申请效率,栈是系统自动分配,速度快,不可控。堆是由new分配,速度比较慢,容易产生内存碎片。
 
栈的作用
1、储存临时变量
 
2、多线程编程的基础。每个线程至少有一个栈用来存储临时变量和返回的值。
 
内存泄漏
申请了没有释放,由程序申请的一块内存,没有任何指针指向它,这个内存就泄露了。
 
避免内存泄漏方法
1、分配的内存以链表管理,使用完毕后从链表删除,程序结束的时候检查链表
 
2、良好的编程习惯,在设计内存的程序段,检验出内存泄漏,使用了内存分配的函数,使用完毕后将使用的相应函数释放掉
 
3、smart pointer
 
指针
数组指针和指针数组
int (*p)[20]; 数组指针,本质是一个指针,指向一个数组
 
int *p[20]; 指针数组,本质是一个数组,里面装的是指针。
 
函数指针和指针函数
1、函数指针 int(*p)(int,int);本身是一个指针,指向一个函数的地址
 
2、指针函数 int *p(int,int); 指针函数表示一个函数,返回数是指针。
 
数组名和指针区别
1、指针保存的是地址,数组保存的是数据,单数组名是第一个元素的地址
 
2、指针间接访问,数组直接下标或者偏移量
 
3、sizeof 有区别,指针为指针大小,数组为全体数据大小
 
指针常量,常量指针、指向常量的指针
1、int *const p 指向地址不变,地址值可变
 
2 int const *p 指向地址可变,地址值不能边
 
3、const int * const p 都不能变
 
指针与引用区别
1、都是地址,指针是地址,应用是别名
 
2、引用本质是指针常量,对象不变,对象的值可变
 
3、++不同,指针是地址自增,引用是对象自增
 
4、指针需要解引用
 
5、指针可为空,引用不行
 
6、sizeof不同 一个是指针大小一个是对象大小
 
野指针
1、指向不可用内存的指针,指针被创建时如果没有初始化就是野指针
 
2、指针被free、delete时没有指向NULL就是野指针
 
3、指针超出了变量的地址范围
 
智能指针
C++智能指针是指一个类,用来存储指针
 
1.3 预处理
预处理器标识#error的目的是什么?
1、遇到#error就会生成一个编译错误提示信息,并停止编译
 
define声明一年多少秒
#define SECOND_OF_PER_YEAR (3652460*60)UL
 
#include"" 和 include<>区别
<>号先搜索标准库搜索系统文件比较快,“”号先搜索工作路径搜索自定义文件比较快
 
头文件作用
1、通过文件调用库功能,源码保护
 
2、头文件加强类型安全检查,编译器报错
 
头文件定义静态变量
1、资源浪费,每个头文件都会单独存在一个静态变量
 
不使用流程控制语句,打印1~1000数字
#include<stdio.h>
#define A(x) x;x;x;x;x;x;x;x;x;x;
int main()
{
int n=1;
A(A(A(printf("%d",n++))));
return 0;
}

 

 
 
 
1.4 变量
全局变量和静态变量
1、全局变量作用域为程序块,局部变量为当前函数
 
2、全局变量储存在静态区,后者为栈
 
3、全局变量生命周期为主函数,局部变量生命周期在局部函数中,甚至循环体内
 
1.5 函数
写个函数在main函数执行前执行
1、attribute可以设置函数属性
 
#include <stdio.h>
void before() __attribute__((constructor));
void after() __attribute__((destructor));
void before() {
  printf("this is function %s\n",__func__);
  return;
}
void after(){
  printf("this is function %s\n",__func__);
  return;
}
int main(){
  printf("this is function %s\n",__func__);
  return 0;
}
// 输出结果
// this is function before
// this is function main
// this is function after

 

 
为什么析构函数必须是虚函数
1、基类指针指向子类时,释放基类指针也能释放掉子类的空间,防止内存泄漏。
 
2、最好是作为父类的类的析构函数作为虚函数
 
为什么C++默认的析构函数不是虚函数?
1、虚函数有额外的虚函数表和虚指针表,占用额外的内存,对于那些不会被继承的类当然也不需要虚函数作为析构函数。
 
静态函数和虚函数的区别?
1、静态函数编译时确定运行时机
 
2、虚函数运行时动态绑定,并且使用虚函数表,内存开销增加
 
重载与覆盖
1、覆盖是子类和父类的关系,垂直关系,重载是一个类之间的关系,水平关系
 
2、覆盖一对一,重载多个方法
 
3、覆盖由对象类型决定,重载根据调用的参数表决定
 
虚函数表实现多态方法
原理:
虚函数表示一个类的地址表,子类创建时,按照函数声明吮吸会将函数的地址存在虚函数表中。子类重写父类虚函数的时候,父类虚函数表中的位置会被子类虚函数地址覆盖。
 
C语言函数调用方法
1、使用栈来支持函数调用操作,栈被用来传递参数,返回值,局部变量等。
 
2、函数调用主要操作栈帧结构
 
select函数
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
1
fork wait exec函数
1、附近产生的子进程使用fork拷贝出一个父进程的副本
 
数组的下标可以为负数吗?
可以,数组下标指地址偏移量,根据偏移量能定位得到目标地址。
 
inline函数和宏定义的区别
1、内联函数在编译时展开,而宏在预编译时展开。
 
2、在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
 
3、内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能。
 
4、宏不是函数,而inline是函数。
 
5、宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性。而内联函数不会出现二义性。
 
6、inline可以不展开,宏一定要展开。因为inline指示对编译器来说,只是一个建议,编译器可以选择忽略该建议,不对该函数进行展开。
 
7、宏定义在形式上类似于一个函数,但在使用它时,仅仅只是做预处理器符号表中的简单替换,因此它不能进行参数有效性的检测,也就不能享受C++编译器严格类型检查的好处,另外它的返回值也不能被强制转换为可转换的合适的类型,这样,它的使用就存在着一系列的隐患和局限性。
 
宏和函数的优缺点
1、函数调用时,先求出实参表达式的值,然后带入形参。而使用带参数的函数只是进行简单的字符替换
 
2、函数调用实在程序运行时处理的,分配的临时的内存单元;而宏展开则是在编译时进行的,在展开时不分配i内存单元,不进行值的传递,也没有"返回值的概念"
 
3、函数实参形参都要定义类型,二者要求一致 ,宏不存在类型问题,宏没有类型,宏的参数只是一个符号代表,展开时代入指定的字符就行,宏定义时字串可以是任意内心的数据
 
4、函数只可以得到一个返回值,宏可以设法得到多个
 
5、使用宏次数多时,展开后源程序长,每次展开都使程序增长,而函数调用不使源程序变长。
 
6、宏的替换不占用时间,只占用编译时间,函数调用占用运行时间。
 
简单回答:宏由编译计算,增加编译时间,函数运行的时候计算,增加运行时间;函数的返回值入口参数有数据类型,宏只是简单的符号加减。
 
ASSERT()作用
ASSERT()是一个调试程序时经常使用的宏,在程序运行时它计算括号内的表达式,如果表达式为FALSE (0), 程序将报告错误,并终止执行。如果表达式不为0,则继续执行后面的语句。
 
strcpy()和memcpy()的区别
1、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
 
1.6 位操作
求解整型类二进制表示1的个数
int func(int x)
{
int countx = 0;
    while(x)
    {
        countx++;
        x = x&(x-1);
    }
return countx;
}

 

 
求解整型类二进制表示0的个数
int CountZeroBit(int num)
{
    int count = 0;
        while (num + 1)
        {
            count++;
            num |= (num + 1); //算法转换
        }
    return count;
}

 

给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在
以上两个操作中,要保持其它位不变。
 
void clearbit3(int a)
{
a&=~(1<<3);
}
void setbit3(int a)
{
a|=1<<3;
}

 

 
1.7 容器与算法
map与set区别和底层实现
1、底层实现都是红黑树
 
2、map是键值对,关键字起到索引作用,值表示与索引相关联的数据,set是关键字的集合并且每个元素只包含一个关键字。
 
3、set迭代器是const不能修改元素值,map允许修改value不能修改key
 
4、map支持下标操作,set不支持,map可以用key作为下标,set用find
 
STL的allocator有什么作用?
1、内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责;对象构造由::construct()负责,对象析构由::destroy()负责。
 
2、提升内存管理效率, STL采用了两级配置器,当分配的空间大小超过128B时,会使用第一级空间配置器;当分配的空间大小小于128B时,将使用第二级空间配置器。第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。
 
STL迭代器如何删除元素?
对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边
每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器;
对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,
删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即
可。
对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上
面两种正确的方法都可以使用
 
STL中map与unordered_map有什么区别?
1、map底层红黑树实现,unordered_map采用hash表实现’
 
2、map中序遍历有序,un——map无序
 
vector和list的区别是什么
1、vector为数组实现,list为双向链表
 
2、vector支持随机访问,list不行
 
3、vector顺序储存,list随机
 
4、vector一次性分配内存,不够才二倍扩容,list一个个分配
 
5、vector随机访问性能好,插入删除比较慢,list反之
 
迭代器与指针
1、迭代器又名游标模式,提供一种顺序访问一个聚合对象中各个元素,但又不暴露该对象的内部表示。
 
2、迭代器是类模板,表现得象指针,重载了指针一些操作,封装了指针,指针的++只是递增地址,但是不能对list生效,迭代器可以。
 
3、迭代器有着更良好的用法begin,end等不用担心越界
 
STL里resize和reserve的区别是什么?
1、resize改变当前容器内含有元素的数量,会新增元素0,reserve只是增加空间,不新增元素。
 
1.8 类和数据抽象
c++类成员访问权限
1、C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限
 
2、类内随便访问,类外通过对象访问,且只能访问public成员
 
引用与指针的区别
1、引用必须被初始化,指针不必。
 
2、引用初始化以后不能被改变,指针可以改变所指的对象。
 
3、不存在指向空值的引用,但是存在指向空值的指针。
 
struct与class区别
1、c++中两者都可以定义类,但是struct没有权限,默认public
 
面对对象和泛型编程
1、面对对象是一种程序设计思想,把对象作为程序的基本单元,一个对象包括了数据和操作数据的函数
 
2、泛型编程让类型参数化,使程序可以从逻辑功能上抽象。吧处理的对象当成参数来传递
 
右值引用,和左值的区别。
左值可以寻址,而右值不可以。
左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。
左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)。C++的类和C里面的struct有什么区别?
析构函数可以为 virtual 型,构造函数则不能,为什么?
1、虚函数主要是用作多态,如果构造函数也用了,那么派生类必须在初始化列表给基类参数初始化
 
2、构造函数运行的时候对象的动态类型还不完整,没法确定没所以不能动态绑定。
 
C++的类和C里面的struct有什么区别?
c++中的类具有成员保护功能,并且具有继承,多态这类特点,而c里的struct没有
c里面的struct没有成员函数,不能继承,派生等等.
 
C++中空类默认产生哪些类成员函数?
1、构造函数
 
2、拷贝构造
 
3、析构函数
 
4、赋值运算符重载函数
 
5、取值运算符重载函数
 
6、const取址运算符重载函数
 
静态成员函数与非静态成员函数的区别
前者没有 this 指针,后者有 this 指针。
静态成员函数只要用来访问静态数据成员,而不访问非静态成员
 
1.9 面对对象
面向对象和面向过程有什么区别?
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
 
1、面对对象以对象为中心,面向过程以过程为中心
 
2、面对对象把代码封装成一个整体,其他对象不能直接修改其数据。面向过程直接使用程序来处理数据,各模块存在控制与被控制的关系。
 
3、面对对象是将问题分为不同的对象,给予对象赋予属性和行为。面对过程则是将事件分为不同的步骤,按照步骤完成编程。
 
面对对象的基本特点
1、封装: 把过程和数据封装起来,只有定义的接口才能调用
 
2、继承:子类继承父类的功能
 
3、多态:不同的对象对从父类继承的同一动作做出不同的反应,
 
4、抽象:不打算了解问题全部,只关注当前目标。过程抽象和数据抽象。过程抽象是指任何操作都被当成实体看待,不在乎它是不是由其他子函数完成。
 
什么是深拷贝?什么是浅拷贝?
1、深拷贝复制一份
 
2、浅拷贝哟与可能共享成员变量
 
友元
1、友元函数:普通函数对一个访问某个类中的私有或者保护成员
 
2、友元类:类A中的成员函数访问类B中的私有或保护成员。
 
初始化列表和构造函数初始化的区别?
Example::Example() : ival(0), dval(0.0) {} //初始化列表的构造函数
Example::Example()   //构造函数
{   
ival = 0;   
dval = 0.0;
}

 

 
结果是一样的,使用初始化列表的构造函数表示 初始化类的成员,使用初始化列表的构造函数是对类成员的赋值,而不是初始化。所以一下情况需要对成员初始化所以必须用初始化列表的方法。
 
1、成员类型为没有默认构造函数的类
 
2、const成员或引用类型的成员
 
类的成员变量的初始化顺序是什么?
​ 1、成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。
 
​ 2、不使用初始化列表的话就与构造函数有关。
 
Public继承、protected继承、private继承的区别?
1、public继承就是公有继承完还是公有,保护还是保护,私有还是私有
 
2、protected继承就是公有变保护,保护还是保护,私有还是私有
 
3、private继承就是所有变成私有
 
1.10 虚函数
虚函数注意内容
1、只需要在声明的函数体中使用关键字virtual将函数声明为虚函数,定义中不需要
 
2、基类某一成员为虚函数之后,派生类中的同名函数自动成为虚函数
 
3、非类的成员函数不能定义为虚函数,全局函数以及类的成员函数和构造函数也不能定义为虚函数,可以将析构函数定义为虚函数
 
什么函数不能声明为虚函数
主要有:普通函数(非成员函数);静态成员函数;类联成员函数;构造函数:友元函数。
 
1.11数据结构
链表和数组的区别
数组在内存中栈上按顺序存储的,而链表是在堆上随机存储的。
 
要访问数组中的元素可以按下标索引来访问,速度比较快,如果对他进行插入操作的话,就得移动很多元素,所以对数组进行插入操作效率很低. 由于连表是随机存储的,链表在插入,删除操作上有很高的效率(相对数组)
 
如果要访问链表中的某个元素的话,那就得从链表的头逐个遍历,直到找到所需要的元素为止,所以链表的随机访问的效率就比数组要低 。
 
2、ARM体系与架构
2.1 硬件基础
NAND FLASH 和NOR FLASH异同?
类别 读 写 擦除 可靠性 容量 用途 价格
 
NOR 快 慢 非常慢 比较高 小 保存代码 高
 
NAND 快 快 快 低 大 保存数据 低
 
CPU,MPU,MCU,SOC,SOPC联系与差别?
1、CPU:是一台计算机的运算核心和控制核心
 
2、MPU: 微处理器稍强的CPU
 
3、MCU:将计算机的CPU、RAM、ROM、定时计数器和多种I/O接口集成在一片芯片上。
 
4、SOC: 系统级芯片不单单是放简单的代码,可以放系统级的代码,也就是说可以运行操作系统
 
CPU中cache的作用?cache的基本组织结构?
(1)高速缓冲存储器Cache是位于CPU与内存之间的临时存储器,它的容量比内存小但交换速度快。
 
在Cache中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可避开内存直接从Cache中调用,从而加快读取速度。由此可见,在CPU中加入Cache是一种高效的解决方案,这样整个内存储器(Cache+内存)就变成了既有Cache的高速度,又有内存的大容量的存储系统了。
(2)全相连映射,直接映射,组相连映射
 
交叉编译
​ 在一种计算机环境中运行的编译程序,能编译出在另外一种环境下运行的代码,我们就称这种编译器支持交叉编译。这个编译过程就叫交叉编译。
 
C/C++的编译包括几个部分
1、预编译:预处理器对c程序进行一些预处理工作,例如对宏定义的变量进行替换;
 
​ 1)将所有的#define删除,并展开所有的宏定义;
 
​ 2)处理所有的预编译指令,例如:#if,#elif,#else,#endif;
 
​ 3)处理#include预编译指令,将被包含的文件插入到预编译指令的位置;
 
​ 4)添加行号信息文件名信息,便于调试;
 
​ 5)删除所有的注释:// /**/;
 
​ 6)保留所有的#pragma编译指令,因为在编写程序的时候,我们经常要用到#pragma指令来设定编译器的状态或者是指示编译器完成一些特定的动作;
 
​ 最后生成.i文件;
 
​ 总的来说,包括(1)去注释 (2)宏替换 (3)头文件展开 (4)条件编译
 
2、编译:编译器将c语言程序翻译成汇编语言程序;
 
​ 1)扫描,语法分析,语义分析,源代码优化,目标代码生成,目标代码优化;
 
​ 2)生成汇编代码;
 
​ 3)汇总符号;
 
​ 4)生成.s文件;
 
3、汇编:汇编语言通过汇编器编译成可重定位目标程序.o,与之相反称为反汇编;
 
​ 1)根据汇编指令和特定平台,把汇编指令翻译成二进制形式;
 
​ 2)合并各个section,合并符号表;
 
​ 3)生成.o文件;
 
4、链接:将目标文件和所需的库函数用链接器进行链接,常见的链接器有Unix;
 
​ 1)合并各个.obj文件的section,合并符号表,进行符号解析;
 
​ 2)符号地址重定位;
 
​ 3)生成可执行文件;