嵌入式面试常见问题汇总
这是我面试过程中常被问到的问题,都是自己整理的希望对你们有帮助。
常见基本类型的字节大小
32位操作系统
char :1个字节(固定)
*(即指针变量): 4个字节(32位机的寻址空间是4个字节。同理64位编译器)(变化*)
short int : 2个字节(固定)
int: 4个字节(固定)
unsigned int : 4个字节(固定)
float: 4个字节(固定)
double: 8个字节(固定)
long: 4个字节
unsigned long: 4个字节(变化*,其实就是寻址控件的地址长度数值)
long long: 8个字节(固定)
1.指向字符串常量的指针,指向字符串的常量指针(const)
const char* p = "hello"; // 指向 "字符串常量"
p[0] = 'X'; // 错误! 想要修改字符串的第一个字符. 但是常量不允许修改
p = p2; // 正确! 让p指向另外一个指针.
char* const p = "hello"; // 指向字符串的" 常量的指针"
p[0] = 'X'; // 正确! 允许修改字符串, 因为该字符串不是常量
p = p2; // 错误! 指针是常量, 不许修改p的指向
char const * 和 const char* 是一样的. const 的位置在char左边还是右边都一样.
常量指针的const应当写在 *星号的右边.
指向常量字符串的常量指针的写法是 const char* const p = "xx"; 要2个const
2.typedef & #define的问题
有下面两种定义pStr数据类型的方法,两者有什么不同?哪一种更好一点?
typedef char* pStr;
#define pStr char*;
分析:通常讲,typedef要比#define要好,特别是在有指针的场合。请看例子:
typedef char* pStr1;
#define pStr2 char *
pStr1 s1, s2;
pStr2 s3, s4;
在上述的变量定义中,s1、s2、s3都被定义为char *,而s4则定义成了char,不是我们所预期的指针变量,根本原因就在于#define只是简单的字符串替换而typedef则是为一个类型起新名字。上例中define语句必须写成 pStr2 s3, *s4; 这这样才能正常执行。
3.const的问题
(1)可以定义const常量,具有不可变性。
例如:const int Max=100; int Array[Max];
(2)便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患。
例如: void f(const int i) { .........} 编译器就会知道i是一个常量,不允许修改;
(3)可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。
如(1)中,如果想修改Max的内容,只需要:const int Max=you want;即可!
(4)可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。 还是上面的例子,如果在函数体内修改了i,编译器就会报错;
例如: void f(const int i) { i=10;//error! }
(5)可以节省空间,避免不必要的内存分配。 例如:
#define PI 3.14159 //常量宏
const doublePi=3.14159; //此时并未将Pi放入RAM中 ......
doublei=Pi; //此时为Pi分配内存,以后不再分配!
double I=PI; //编译期间进行宏替换,分配内存
double j=Pi; //没有内存分配
double J=PI; //再进行宏替换,又一次分配内存!
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。
(6)提高了效率。
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
sizeof与strlen的区别:
char str[20]="0123456789";
int a=strlen(str); // a=10;strlen 计算字符串的长度,以\0'为字符串结束标记。
int b=sizeof(str); // b=20;sizeof 计算的则是分配的数组str[20] 所占的内存空间的大小,不受里面存储的内容影响.
上面是对静态数组处理的结果,如果是对指针,结果就不一样了
char* ss = "0123456789";
sizeof(ss)结果4===》ss是指向字符串常量的字符指针,sizeof 获得的是一个指针的之所占的空间,应该是长整型的,所以是4,sizeof(*ss) 结果1===》*ss是第一个字符 其实就是获得了字符串的第一位'0' 所占的内存空间,是char类型的,占了1位strlen(ss)= 10如果要获得这个字符串的长度,则一定要使用 strlen
Sizeof结构体为结构体中定义的数据类型的总的空间(注意字节对齐)。
Sizeof对union为union中定义的数据类型的最大数据类型的大小。
5 .auto, register, static分析
auto即C语言中局部变量的默认属性,编译器默认所有的局部变量都是auto的,定义的变量都是在栈中分配内存。
static关键字指明变量的“静态”属性,同时具有“作用域限定符”的意义,修饰的局部变量存储在程序静态区,static的另一个意义是文件作用域标示符。
static修饰的全局变量作用域只是声明的文件中,static修饰的函数作用域只是声明的文件中
register关键字指明将变量存储于寄存器中,register只是请求寄存器变量,但不一定请求成功。register变量的必须是CPU寄存器可以接受的值,不能用&运算符获取register变量的地址,这样使用的好处是处理快。
6. const, volatile同时修饰变量(1) “编译器一般不为const变量分配内存,而是将它保存在符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作。”
(2) volatile的作用是“告诉编译器,i是随时可能发生变化的,每次使用它的时候必须从内存中取出i的值”。
一,const, volatile含义
(1)const含义是“请做为常量使用”,而并非“放心吧,那肯定是个常量”。
(2)volatile的含义是“请不要做自以为是的优化,这个值可能变掉的”,而并非“你可以修改这个值”。
二,const, volatile的作用以及起作用的阶段
(1)const只在编译期有用,在运行期无用
const在编译期保证在C的“源代码”里面,没有对其修饰的变量进行修改的地方(如有则报错,编译不通过),而运行期该变量的值是否被改变则不受const的限制。
(2)volatile在编译期和运行期都有用
在编译期告诉编译器:请不要做自以为是的优化,这个变量的值可能会变掉;
在运行期:每次用到该变量的值,都从内存中取该变量的值。
补充:编译期 -- C编译器将源代码转化为汇编,再转化为机器码的过程;运行期 -- 机器码在CPU中执行的过程。
三,const, volatile同时修饰一个变量
(1)合法性
“volatile”的含义并非是“non-const”,volatile和cons不构成反义词,所以可以放一起修饰一个变量。
(2)同时修饰一个变量的含义
表示一个变量在程序编译期不能被修改且不能被优化;在程序运行期,变量值可修改,但每次用到该变量的值都要从内存中读取,以防止意外错误。
7 、栈、堆、静态存储区
栈:主要函数调用的使用
栈是从高地址向低地址方向使用,堆的方向相反。
在一次函数调用中,栈中将被依次压入:参数,返回地址,EBP。如果函数有局部变量,接下来,就在栈中开辟相应的空间以构造变量。
在C语言程序中,参数的压栈顺序是反向的。比如func(a,b,c)。在参数入栈的时候,是:先压c,再压b,最后a。在取参数的时候,由于栈的先入后出,先取栈顶的a,再取b,最后取c。
堆:主要内存动态分配
空闲链表法,位图法,对象池法等等 。
Int* p=(int*)malloc(sizeof(int));
静态存储区:保存全局变量和静态变量
程序静态存储区随着程序的运行而分配空间,直到程序运行结束,在程序的编译期静态存储区的大小就已经确定,程序的静态存储区主要用于保存程序中的全局变量和静态变量与栈和堆不同,静态存储区的信息最终会保存到可执行程序中 。
知识点:堆栈段在程序运行后才正式存在,是程序运行的基础
1.函数放在代码段:.Test section。 .text段存放的是程序中的可执行代码
2.带初始值的全局变量和静态变量在数据段:.data section。 .data段保存的是那些已经初始化了的全局变量和静态变量
3.不带初始值得全局变量和静态变量在.bss。 .bss段存放的是未初始化的全局变量和静态变量
.rodata(read only)段存放程序中的常量值,如字符串常量
同是全局变量和静态变量,为什么初始化和未初始化的变量保存在不同的段中?
答:为了启动代码的简单化,编译链接器会把已初始化的变量放在同一个段:.data,这个段的映像(包含了各个变量的初值)保存在“只读数据段”,这样启动代码就可以简单地复制这个映像到 .data 段,所有的已初始化变量就都初始化了。而未初始化变量也放在同一个段:.bss,启动代码简单地调用 memset 就可以把所有未初始化变量都清0。
void *memset(void *s, int ch, size_t n);
函数解释:将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。
memset:作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
#define Malloc(type,n) (type*)malloc(n*sizeof(type))
8 、野指针
产生原因:
1、局部指针变量没有初始化
2、使用已经释放的指针
3、指针所指向的变量在指针之前被销毁
A.用malloc申请了内存之后,应该立即检查指针值是否为NULL,防止使用为NULL的指针:
B.牢记数组的长度,防止数组越界操作,考虑使用柔性数组
C.动态申请操作必须和释放操作匹配,防止内存泄露和多次释放
D.free指针之后必须立即赋值为NULL
9 、void类型指针
指针有两个属性:指向变量/对象的地址和长度,但是指针只存储地址,长度则取决于指针的类型;编译器根据指针的类型从指针指向的地址向后寻址,指针类型不同则寻址范围也不同,比如:
int*从指定地址向后寻找4字节作为变量的存储单元
double*从指定地址向后寻找8字节作为变量的存储单元
void即“无类型”,void *则为“无类型指针”,可以指向任何数据类型。
void指针可以指向任意类型的数据,即可用任意数据类型的指针对void指针赋值。例如
int *pint;
void *pvoid; //它没有类型,或者说这个类型不能判断出指向对象的长度
pvoid = pint; //只获得变量/对象地址而不获得大小,但是不能 pint =pvoid;
9.2 如果要将pvoid赋给其他类型指针,则需要强制类型转换如:
pint = (int *)pvoid; //转换类型也就是获得指向变量/对象大小
9.3 void指针不能复引用(即取内容的意思)
*pvoid //错误
要想复引用一个指针,或者使用“->”运算符复引用一部分,都要有对于指针指向的内存的解释规则。
例如,int *p;
那么,当你后面复印用p的时候,编译器就会把从p指向的地址开始的四个字节看作一个整数的补码。
因为void指针只知道指向变量/对象的起始地址,而不知道指向变量/对象的大小(占几个字节)所以无法正确引用。
在实际的程序设计中,为迎合ANSI标准,并提高程序的可移植性,我们可以这样编写实现同样功能的代码:
void*pvoid;
(char*)pvoid++; //ANSI:正确;GNU:正确
(char*)pvoid+=1; //ANSI:错误;GNU:正确