近期,正逢毕业求职季,根据我的面试经历,给大家分享一些面试嵌入式软件工程师这一岗位经常会遇到的面试题,希望这些内容对大家面试有所帮助。
1、结构体(struct)和共用体(union)
结构体(struct)的各个成员会占用不同的内存,互相之间没有影响;而共用体(union)的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
结构体占用的内存大于等于所有成员占用的内存的总和(内存对齐),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
在C语言中,结构体(struct)和共用体(union)的内存大小计算有一些不同。
结构体(struct):结构体的总大小是其所有成员大小的加和,但具体的布局和字节对齐规则可能会影响实际的大小。例如,如果一个结构体中有多个成员,那么编译器可能会在它们之间插入一些填充字节来确保每个成员都按其合适的对齐方式开始。这种行为被称为"结构体打包"或"结构体对齐"。
为了获取一个结构体的确切大小,你可以使用C语言中的sizeof运算符。例如:
struct example {
int a; //4
double b; //8
char c; //1
};
printf("Size of struct: %zu bytes\n", sizeof(struct example)); //2*8>13 16
共用体(union):共用体的总大小是其最大成员的大小,因为所有成员共享同一块内存空间。即使其他成员没有在某个时刻被使用,它们所占的空间也不能被重用。
同样,你可以使用sizeof运算符来获取一个共用体的确切大小。例如:
union example {
int a; //4
double b; //8
char c; //1
};
printf("Size of union: %zu bytes\n", sizeof(union example)); //8
请注意,%zu是用于打印size_t类型的格式说明符,它表示sizeof运算符返回的结果类型。
结构体和共用体结合
面试题:
#include <stdio.h>
typedef union {
long a; //8 Linux64位
int b[5]; //20
char c; //1
}Myunion;
typedef struct {
int a; //4
Myunion d1; //20
char b; //1
} Mystruct;
int main() {
printf("%zu", sizeof(Mystruct)); //20*2>25 40
return 0;
}
这些规则对于大多数的现代编译器都是适用的,但具体的实现可能会因编译器和平台的不同而有所差异。
2、#define
#define是C语言中的一个预处理指令,用于定义一个标识符作为特定代码块的替代。在编译程序之前,预处理器会将代码中的所有标识符替换为定义的文本。
以下是#define的一些用法:
简单的宏定义:
#define <宏名> <字符串>
例如:
#define PI 3.1415926
在程序中使用PI时,预处理器将其替换为3.1415926。
带参数的宏定义:
#define <宏名>(<参数表>) <宏体>
例如:
#define SQUARE(x) x * x
在程序中使用SQUARE(5)时,预处理器将其替换为5 * 5。
宏的单行定义和多行定义:
宏定义中允许包含两行以上的命令,此时必须在最右边加上\,并且该行后面不能再有任何字符,包括注释。例如:
#define max(a, b) \
((a) > (b) ? (a) : (b))
条件编译:
在大规模的开发过程中,特别是跨平台和系统的软件里,#define最重要的功能是条件编译。例如:
#define DEBUG 1
#define TRACE 0
#define PRINT_DEBUG_INFO if(DEBUG && TRACE) printf(...)
在这个例子中,PRINT_DEBUG_INFO在程序中被调用时,预处理器将其替换为if(DEBUG && TRACE) printf(…),只有当DEBUG和TRACE都为真时,才会执行打印操作。
宏定义中的do{}while(0)
使用do{}while(0)将宏定义括起的原因:
(1)空的宏定义避免waring
(2)存在一个独立的block,可以用来进行变量定义,进行比较复杂的实现
(3)如果出现在判断语句过后的宏,可以保证作为一个整体来实现
#include <stdio.h>
#define FUNC(x) \
do{ \
if(NULL == x) \
return 0; \
}while(0) \
int main(){
char* x = NULL;
FUNC(x);
}
面试题:
- 宏定义求数组元素个数(数组长度)
#define SIZE(array) (sizeof(array) / sizeof(array[0])) //括号括起来
宏定义来实现对一个数据的位进行置位和清零操作
#include <stdio.h>
#define SET_BIT(x, n) (x | (1 << (n-1))) //宏定义置位操作是将一个数的某一位(bit)置位,即将该位上的二进制数设为1
#define CLEAR_BIT(x, n) (x & ~(1 << (n-1))) //宏定义清零操作是将一个数的某一位(bit)清零,即将该位上的二进制数设为0
int main() {
int x=0; //00000000
x=SET_BIT(x, 3);
printf("%d",x); //4
return 0;
}
宏定义就是直接替换
#include<stdio.h>
#define double(b) b + b
#define DOUBLE(x) (x+x)
int main()
{
int num = double(5) * 5; // num=5+5*5
int num1 = DOUBLE(5) * 5; // num=5+5*5
printf("%d\n", num); // 30
printf("%d", num); // 30
return 0;
}
3、指针
int p; 变量名叫p,类型为int ,可存放一个int数据的地址 。
(1)intptr;
(2)charptr;
(3)intptr;
(4)int(ptr)[3];
(5)int(ptr)[4];
1.指针的类型
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:
(1)intptr;//指针的类型是int*
(2)charptr;//指针的类型是char
(3)intptr;//指针的类型是int**
(4)int(ptr)[3];//指针的类型是int()[3]
(5)int*(ptr)[4];//指针的类型是int()[4]
怎么样?找出指针的类型的方法是不是很简单?
2.指针所指向的类型
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符去掉,剩下的就是指针所指向的类型。例如:
(1)intptr; //指针所指向的类型是int
(2)charptr; //指针所指向的的类型是char
(3)int**ptr; //指针所指向的的类型是int*
(4)int(ptr)[3]; //指针所指向的的类型是int()[3]
(5)int(ptr)[4]; //指针所指向的的类型是int()[4]
在指针的算术运算中,指针所指向的类型有很大的作用。
指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C 越来越熟悉时,你会发现,把与指针搅和在一起的"类型"这个概念分成"指针的类型"和"指针所指向的类型"两个概念,是精通指针的关键点之一。我看了不少书,发现有些写得差的书中,就把指针的这两个概念搅在一起了,所以看起书来前后矛盾,越看越糊涂。
3.指针的值----或者叫指针所指向的内存区或地址
指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32 位程序里,所有类型的指针的值都是一个32 位整数,因为32 位程序里内存地址全都是32 位长。指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX 为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。
以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?(重点注意)
4 指针本身所占据的内存区
指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。在32 位平台里,指针本身占据了4 个字节的长度。指针本身占据的内存这个概念在判断一个指针表达式(后面会解释)是否是左值时很有用。
4、数组名可以作数组的首地址,而&数组名是整个数组的指针
#include <stdio.h>
int main() {
int a[5] = {1, 2, 3, 4, 5};
int* p = (int*)(&a + 1);
int* p1 = (int*)(a + 1);
printf("%d,%d,%d", *(a + 1), *(p - 1), *(p1 - 1)); //2,5,1
return 0;
}
5、const关键字修饰指针
在C中,const关键字可以用来修饰指针。const关键字修饰指针有三种情况:
const指针
const指针是指指针本身是一个常量,即指针指向的地址不能改变,但是指针指向的内容可以改变。例如:
const int *p; // p是指向const int类型的指针
int a = 10;
const int *p = &a; // p指向a,a的值不能通过p来改变
*p = 20; // 编译错误,不能通过p来改变a的值
指向const的指针
指向const的指针是指针本身的值不能改变,但是指针指向的内容是一个const常量,即不能通过指针来改变const常量的值。例如:
int a = 10;
int *const p = &a; // p是一个常量指针,指向a,p的值不能改变,a是一个普通变量
*p = 20; // 正确,可以通过p来改变a的值
const修饰指针和指针指向const的组合
const修饰指针和指针指向const的组合是指指针本身是一个const常量,并且指针指向的内容也是一个const常量。例如:
const int a = 10;
const int *const p = &a; // p是一个常量指针,指向const int类型的a,p的值不能改变,a是一个const常量,不能通过任何方式来改变其值
*p = 20; // 编译错误,不能通过p来改变a的值
6、volatile 关键字
volatile关键字的作用是告诉编译器不要优化涉及该关键字的代码,以保持变量的可见性和确保原子性。具体来说,volatile关键字可以确保以下几点:
变量在多个线程间可见性:使用volatile关键字可以强制从公共内存中读取变量,而不是从线程的私有数据栈中取得变量的值,这可以确保多个线程间变量的可见性。
防止编译器优化:volatile关键字可以防止编译器对涉及该关键字的代码进行优化,从而确保变量的读取和写入操作是原子的。
需要注意的是,volatile关键字并不能保证原子性,因此对于复杂的并发操作,还需要使用其他同步机制,例如synchronized关键字。
7、函数不能返回局部变量的地址
函数不能返回局部变量的地址,因为局部变量是在函数内部定义的,其生命周期仅限于函数执行期间。当函数执行完毕后,局部变量会被销毁,因此其地址可能被回收并分配给其他变量使用,这就可能导致未定义的行为。
如果需要在函数之间共享数据,可以考虑使用全局变量、静态局部变量、堆分配等其他方式。
面试题:
int* fun1()
{
static int a[2] = { 2,4 }; //a为局部变量,函数执行完毕就销毁,其地址被回收,所以要加static让它变成静态局部变量
int* p = a, ret = 0;
printf("%d ", *p *(*p + 1));
*p++;
return p;
}
int main()
{
int* ret = 0;
ret = fun1();
printf("%d\n",*ret);
return 0;
}