【嵌入式软件实习、秋招八股文之第一期:C、C++八股文汇总】

第一期 C/C++相关 (嵌入式软件八股文)



前言

C/C++嵌入式相关知识点归纳

  1. 无论是Windows32还是64位下,各数据类型占用字节大小都是一样的(char–1字节,short–2字节,int–4字节,long–4字节,float–4字节,long long–8字节,double–8字节),但指针则分别占4字节和8字节。

一、关键字

在了解关键字之前,先要明确定义与声明的区别:
定义: 编译器创建一个对象,为这个对象分配一块内存并给取一个名字,这个名字就是变量名或对象名。
声明: 告诉编译器,这个名字已经匹配到一块内存上了,其不能再用这个名字进行定义。
主要区别: 定义创建了对象并为这个对象分配了内存,而声明没有分配内存。

由ANSI标准定义的C语言关键字共32个:
auto double int struct break else long switch
case enum register typedef char extern return union
const float short unsigned continue for signed void
default goto sizeof volatile do if while static

根据关键字的作用,可以将关键字分为数据类型关键字流程控制关键字两大类。

  • 1.1 C语言宏定义中“#”和“##”的用法和区别

宏定义的一般用法稍后补充。

我们来看进阶用法

(#)当作字符串转化操作符,其作用是将宏定义中传入的参数,转换成后面用一对双括号括起来的参数名字符串。

来看一个例子:

#define example( instr ) printf( "the input string is:\t%s\n", #instr )
#define example1( instr ) #instr

这两个宏展开后:

example( abc ); // 在编译时将会展开成:printf("the input stringis:\t%s\n","abc")
string str = example1( abc ); // 将会展成:string str="abc"

(##)符号连接操作符,作用是将宏定义的多个形参连接成一个实际的参数名。

例如:

#define exampleNum( n ) num##n
//当用##连接形参时,##前后的空格可有可无,上下两个宏定义是一样的
#define exampleNum1( n ) num ## n

使用该宏时:

int num8 = 8;
int num = exampleNum( 8 ); // 将会扩展成 int num = num8

这里我们要注意的是,连接后的实际参数名,必须为实际存在的参数名或是编译器已知的宏定义。比如连接后的num8,在前面已经被定义为int类型

此外,如果##后的参数本身也是一个宏的话,##会阻止这个宏的展开。

来看一个综合例题

#include <stdio.h>
#include <string.h>

#define STRCPY(a, b) strcpy(a ## _p, #b)
int main()
{
char var1_p[20];
char var2_p[30];
strcpy(var1_p, "aaaa");
strcpy(var2_p, "bbbb");
STRCPY(var1, var2);
STRCPY(var2, var1);
printf("var1 = %s\n", var1_p);
printf("var2 = %s\n", var2_p);

//STRCPY(STRCPY(var1,var2),var2);
//这里是否会展开为: strcpy(strcpy(var1_p,"var2")_p,"var2“)?答案是否定的:
//展开结果将是: strcpy(STRCPY(var1,var2)_p,"var2")
//## 阻止了参数的宏展开!如果宏定义里没有用到 # 和 ##, 宏将会完全展开
// 把注释打开的话,会报错:implicit declaration of function 'STRCPY'
return 0;
}

最后输出为:

var1 = var2
var2 = var1
  • 1.2 描述一下你对关键字volatile的理解?举几个实际利用场景。

volatile是一个类型修饰符(type specifier)。它是被设计用来修饰被不同线程访问和修改的变量。如果没有volatile,基本上会导致这样的结果:要么无法编写多线程程序,要么编译器失去大量优化的机会

Volatile意思是“易变的”,“易变”是因为外在因素引起的,像多线程,中断等。

volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。

如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

举一个例子来帮助理解一下上面的内容:

volatile int i = 0;
int a = i;
....
//其他代码,并未明确告诉编译器,对i进行过操作
int b = i;

可以看到变量 i 用了volatile进行了修饰,每次使用它的时候必须从 i 的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。

不使用volatile时,编译器会对代码进行优化处理,其发现两次从 i 读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i 是一个寄存器变量或者表示一个端口数据就容易出错。

  • volatile的具体应用场景:

(1)中断服务程序中修改的供其它程序检测的变量,需要加volatile;
(2)多任务环境下各任务间共享的标志,应该加volatile;
(3)并行设备的硬件寄存器(如:状态寄存器)。

  • 1.3 关键字static有什么用。

(1)在函数体内定义的变量,在函数调用过程中只会被初始化一次,即一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
(2)在模块内(但在函数体外) 定义的静态变量,只能被模块内的函数所访问,是一个本地的全局变量
(3)在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用(只能被当前文件使用)。

  • 在C语言中,static变量为什么只初始化一次呢?
    答:对于所有的对象,初始化都只有一次,但是静态变量初始化以后一直被保存在静态数据区,所以其不会被再次初始化,它的生命周期和所在的程序周期一样。但是对于auto变量,其存放在栈区,一旦函数调用结束,就会被立刻销毁

  • 1.4 关键字extern和extern “C”的作用是什么?

(1)extern关键字

extern关键字的用法很简单,比如 extern int i ,这里声明了一个变量 i ,这个 i 在别处已经被定义,内存也已经分配完毕。

注意声明定义的区别,定义一个变量,会进行内存空间的分配,而声明并不会。

extern一般用于声明,且一般写在.h文件中,声明变量或函数。其他文件访问该变量或函数时,包含头文件就好。

(2)extern “C”

在C++中,extern “C”修饰的语句是按照C语言的方式进行编译。
通常在C++ 中,假如需要使用C语言中的库文件的话,可以使用extern "C"去包含;

多的不说了

  • 1.5 关键字const的作用?使用场景?

与预编译指令 #define 相比,const修饰符有以下的优点:

1、预编译指令只是对值进行简单的替换,不能进行类型检查。而const有对应的数据类型,是要进行判断的,可以避免一些低级的错误。

2、可以保护被修饰的东西,防止意外修改,增强程序的健壮性

3、就存储方式而言:#define只是进行展开,有多少地方使用,就替换多少次,它定义的宏常量在内存中有若干个备份;const定义的只读变量在程序运行过程中只有一份备份。

const用法如下:

(1) 定义变量(局部或全局变量)为常量,例如:

const int Num = 10; //此处定义一个常量Num
Num = 9;
const int n; //报错,常量在定义时必须初始化

const char* str = "abcdefg" //修饰常量静态字符串,这样的话如果后续尝试对str进行修改,则在程序编译阶段就会报错。

(2)常量指针和指针常量

  • 首先看常量指针,这个指针指向的内容是常量,定义方法如下:
const int* n;
int const* n;

不可以通过常量指针来修改变量的值,但是可以通过别的引用来改变,比如:

int a = 5;
const int* n = &a;
a = 6;

常量指针指向的值不能改变,但是这并不是意味着指针本身不能改变,常量指针可以指向其他的地址。

int a = 5;
int b = 6;
const int* n = &a;
n = &b; //可以改变常量指针,使其指向其他的地址。
  • 指针常量是指,指针本身是个常量(包含的地址);不能再指向其他的地址了,定义如下:
int* const n;

需要注意的是,指针常量指向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向该地址的指针来修改。

int a = 5;
int *b = &a;
int* const n = &a;
*b = 8;    //可以通过其他指针来修改指针常量指向的地址的值。
  • 指向常量的指针常量: 指针指向的位置不能改变并且也不能通过这个指针改变变量的值,但是依然可以通过其他的普通指针改变变量的值。
const int* const p;

(3) 修饰函数的参数

根据前面所说的常量指针和指针常量,const修饰的函数参数分为以下三种情况:

(3.1)防止修改指针指向的内容
static ssize_t iic_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)

可以看到,给 buf 加上const修饰以后,如果函数体内的语句试图改动buf的内容,编译器则会报错。因此 buf 内部是只读内容。

(3.2)防止修改指针指向的地址
void swap(int* const p1, int* const p2)

常用的变量交换函数,这里指针p1和指针p2指向的地址都不能修改。

(4) 修饰函数的返回值

如果给“以指针传递”方式的函数返回值添加 const 修饰,那么函数返回值的内容不可被修改,参考常量指针;且该返回值只能被赋给添加 const 修饰的同类型指针。例如:

const char*  a(void); 

char *str = a();         //错误
const char *str = a();   //正确

1.6 sizeof 和 strlen有什么区别?

区别1:sizeof() 是一个运算符,而 strlen() 是一个函数。

区别2:sizeof() 计算的是变量或类型所占用的内存字节数,而strlen() 计算的是字符串中字符的个数

区别3:sizeof() 可以用于任何类型的数据,而 strlen() 只能用于以空字符’\0’ 结尾的字符串

区别4:sizeof() 计算字符串的长度,包含末尾的 ‘\0’,strlen()计算字符串的长度,不包含字符串末尾的 ‘\0’。

sizeof用法如下:

sizeof(type);       //type是数据类型
sizeof(variable);   //variable是变量

char s[] = "Hello,world!";
sizeof(s)      //输出 14,即字符串 s 中有14个字符(包含结尾的'/0');

strlen用法如下:

char s[] = "Hello,world!";
strlen(s)      //输出 13(不包含结尾的'/0');

除此之外,我们还要注意的是,sizeof运算符的结果类型是size_t,它在头文件中typedef为unsigned int 类型,保证能容纳所建立的最大对象的字节大小。

sizeof是在编译的时候计算的,所以可以通过 sizeof(x)来定义数组维数。而 strlen则是在运行期计算的

下面看一个例子,“不使用sizeof,求int占用的字节数”

#include <stdio.h>
#define Sizeof(value) (char *)(&value+1) - (char *)&value
int main()
{
	int i ;
	double f;
	double *q;
	printf("%d\r\n",MySizeof(i));
	printf("%d\r\n",MySizeof(f));
	printf("%d\r\n",MySizeof(a));
	printf("%d\r\n",MySizeof(q));
	return 0;
}

输出为:

4 8 32 4

为什么呢?(char *)&value 返回的是 value 的地址的第一个字节,(char *)(&value+1) 返回的是 value 地址的下一个地址的首字节,所以它们之间的差值就是 value 所占的字节数。

再看一个例子:

strlen("\0") = 0;
siozeof("\0") = 2;

这下明白两者的区别了吗?小朋友。

  • 1.7 C语言中 struct 和 union的区别是什么?写代码时什么情况下会用到?

struct(结构体)与 union(联合体)是C语言中两种不同的数据结构,两者都是常见的复合结构,其区别主要表现在以下两个方面。

(1):结构体与联合体虽然都是由多个不同的数据类型成员组成的,但不同之处在于联合体中所有成员共用一块地址空间,即联合体只存放了一个被选中的成员,而结构体中所有成员占用空间是累加的,其所有成员都存在,不同成员会存放在不同的地址。在计算一个结构型变量的总长度时,其内存空间大小等于所有成员长度之和(需要考虑字节对齐),而在联合体中,所有成员不能同时占用内存空间,它们不能同时存在,所以一个联合型变量的长度等于其最长的成员的长度。

(2):对于联合体的不同成员赋值,将会对它的其他成员重写,原来成员的值就不存在了,而对结构体的不同成员赋值是互不影响的。

下面举个例子:

typedef union {double i; int k[5]; char c;}DATE;
typedef struct data( int cat; DATE cow;double dog;)too;
DATE max;
printf ("%d", sizeof(too)+sizeof(max));

输出为64
1、对于联合体DATE,其最大变量类型是 int[5],占用了20个字节,但是double变量占了8个字节,union需要进行8字节对齐,所以联合体DATE大小为24个字节(3*8 = 24)
2、data结构体变量大小为其各成员变量大小叠加,即 4 + 24 + 8 = 36,按照8字节对齐,即为40.
3、所以结果为,24 + 40 = 64.

  • 1.9 ++a 和 a++有什么区别?具体是怎么实现的?

a++的具体运算过程:

int temp = a;
a = a + 1;
return temp;

++a 的具体运算过程:

a = a + 1;
return a;

后置自增运算符需要把原来变量的值复制到一个临时的存储空间,等运算结束后才会返回这个临时变量的值。所以前置自增运算符效率比后置自增要高

二、内存相关

  • 2.1 new/delete 与 malloc/free的区别是什么?

区别1: new/delete是C++中的操作符,而malloc和free是标准库函数。

区别2:new返回的是指定类型的指针,并且可以自动计算所申请内存的大小。而 malloc需要我们计算申请内存的大小,并且在返回时强行转换为实际类型的指针。

  • 2.2 C语言中变量的内存分配方式有几种?

静态存储区分配: 内存分配在程序编译之前完成,且在程序的整个运行期间都存在,例如全局变量、静态变量等。

栈上分配: 在函数执行时,函数内的局部变量的存储单元在栈上创建,函数执行结束时这些存储单元自动释放。

堆上分配

  • 2.3 说一下栈和堆的区别?

(1)申请方式存在区别: 栈的空间由操作系统自动分配/释放;而堆上申请的内存需要手动分配/释放,即malloc/free。
(2)申请大小存在区别:

  • 在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。即栈顶的地址和栈的最大容量是系统预先规定好的,一般是2M,在编译时就确定的一个常数。如果申请的空间超过栈的剩余空间,则会提示overflow错误。因此栈的空间较小。
  • 堆是很大的自由存储区。堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大

(3)申请效率存在区别:

  1. 栈由系统自动分配,速度较快。
  2. 堆由程序员自主分配,速度较慢,而且容易产生内存碎片。
  • 2.4 栈在C语言中有什么作用?

(1)C语言中栈用来存储临时变量,临时变量包括函数参数函数内部定义的临时变量函数返回地址寄存器等均保存在栈中,函数调动返回后,利用栈恢复寄存器和临时变量等函数调用前运行场景。
(2)栈也是多线程编程的基石,每一个线程都最少有一个自己专属的栈,用来存储本线程运行时各个函数的临时变量和维系函数调用和函数返回时的函数调用关系和函数运行场景。多线程切换的过程就是不同线程栈切换的过程。

  • 2.5 C语言函数参数压栈顺序是什么样的?描述一下main函数调用一个子函数时,栈的变化情况?

首先做一些基础知识补充:ss、sp、bp、esp、ebp分别是什么?有什么联系?

首先,上面这些全部都是寄存器,但是存在于不同位数的CPU。

在16位CPU中,如X86的实模式下,栈寄存器SS存放的是栈的基地址,表明栈所在的逻辑段;栈指针寄存器SP存放的是栈顶的地址,即始终指向最后推入堆栈的数据所作的单元。存储单元的地址由(SS)×16+(SP)形成。 基数指针寄存器BP(base pointer)是一个寄存器,它的用途有点特殊,是和堆栈指针SP联合使用的,作为SP校准使用的,只有在寻找堆栈里的数据和使用个别的寻址方式时候才能用到。
在这里插入图片描述

在32位CPU下,BP再扩展16位就变成了EBP,SP再扩展16位就变成了ESP,作用没有发生变化。

每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址)寄存器esp指向当前的栈帧的顶部(低地址)。(这里主要讲的是函数调用时栈的变化过程,所以没有涉及到ess寄存器,我们主要用到esp、ebp寄存器)

听起来是不是有点扑朔迷离?

我们来看一个实例,加深你对C语言函数调用时,栈中成员的构成。

在这里插入图片描述

现在是不是就可以回答这个问题了?C语言函数参数压栈顺序是从右往左,因为自左向右的入栈方式,最前面的参数被压在栈
底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。因此,C语言函数参数采用自右向左的入栈顺序,主要原因是为了支持可变长参数形式

至于第二个问题(描述一下main函数调用一个子函数时,栈的变化情况),好好理解上面的图文吧!!

  • 2.6 C++的内存管理是怎么样的?内存分区?

在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。

低地址到高地址六个段分布依次为:

  • 代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
  • 数据段:存储程序中已初始化的全局变量静态变量
  • BSS段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态
    变量。可执行文件并没有为 bss 段变量分配存储空间,在可执行文件中只需记录 bss 段的位置及其所需大小,直到程序运行时,由加载器来分配这一段内存空间。
  • 堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
  • 映射区:存储动态链接库以及调用mmap函数进行的文件映射。
  • 栈区:用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}” 中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外, 在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数传递的实参和返回值也会被存放回栈中。由于栈的先进先出特点,所以 栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

在这里插入图片描述


总结

提示:这里对文章进行总结:

例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

  • 51
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值