Bootstrap

嵌入式笔试题+C/C++ 中 volatile static关键字详解,变量定义

变量命名

在c语言中,我们习惯的变量定义方式为

数据类型+变量名

实际上完整的变量定义方式为:

//存储类型+特征修饰+数据类型+变量名
static volatile int value;

存储类型:决定变量的存储位置
特征修饰:决定变量的特征属性
数据类型:决定变量的存储空间和数据范围
变量名字:决定变量的引用标识

存储类型
auto static extern register (存储类型)决定变量存在什么位置

存储类型存储位置修饰变量生命周期
auto只能修饰局部变量所在函数执行到结束的生命周期
static静态区局部/全局生命周期存储在程序结束
externDATA数据区全/局部
registercpu寄存器局部/全局
  • 在 C 语言中,“register” 关键字用于建议编译器将变量存储在寄存器中以提高访问速度,但编译器不一定会按照建议执行,并且在现代编译器中该关键字的实际作用有限。而 “extern” 关键字用于声明一个在其他文件中定义的全局变量或函数,其生命周期取决于所关联的全局变量或函数的存储类别(通常为静态存储期或动态存储期)。
  • extern告诉编译器不给变量分配空间,意在告诉编译器,其它文件已经定义了变量,只能修饰全局变量,找不到外部变量,直接报错
  • 栈是编译器申请,编译器释放
  • 堆里没有变量
  • register: 寄存器没地址,不能取地址

特征修饰

  • const 把一个值的属性改为只读 还是变量,不是常量
  • volatile的意思是“易变的”,表示该变量随时都可能改变,因此编译器在编译的时候对该变量的存储操作,不能进行优化,即告诉编译器,每次存取该变量都要从内存中去存取,而不是直接从寄存器之前的备份存取

变量的访问

1.读变量
内存->寄存器(cpu)
2.写变量
寄存器(cpu)->内存

示例

int a,b ; //为a,b申请内存
a=1;  //1->寄存器   寄存器->内存(&a)  
b=a;  //内存(&a) ->寄存器  寄存器->内存(&b)

在计算机工作时,内存的访问速度远不及cpu的处理速度,为了提升计算机的整体性能,在软硬件层面,都有相应的机制去优化内存的访问

  • 硬件层面:
    引入高速缓存(Cache)
  • 软件层面
    1)编码优化(程序员)
    2)编译优化(编译器)
volatile int a=1,b,c;  //为a,b,c申请内存空间,并初始化a
b=a;  //内存(&a) -> 寄存器  寄存器->内存(&b)
c=a;  //内存(&a) ->寄存器(没加volatile关键字,这一步有可能省略和不省略,加了则一定不能省略)   
      //寄存器->内存(&c)

省略了一步,就是进行了优化,减少了反复的内存读取操作,但是,优化的前提是a的值在赋给b和c中间不能改变,往往,可能产生这种改变的有以下三种方式(因此也是需要Volatile的应用):

  • 中断:中断服务程序若是修改其它程序中的变量
  • 多线程:多个线程都要访问的变量
  • 硬件寄存器:硬件寄存器的值往往会随着硬件工作状态的变化而改变

面试题

一、static 修饰全局变量和局部变量的不同

静态全局变量(static 修饰的全局变量):
作用域:只在定义它的源文件内可见,其他源文件无法访问。
生命周期:从程序开始执行到程序结束。
存储位置:静态存储区。
静态局部变量(static 修饰的局部变量):
作用域:仅在定义它的函数体内可见。
生命周期:从程序开始执行到程序结束,而不是像普通局部变量那样在函数调用结束后就被销毁。
存储位置:静态存储区。

二、static 函数(静态函数)和普通函数的不同

作用域不同:
static 函数:只能在定义它的源文件内被调用,其他源文件无法看到和调用该函数。
普通函数:在整个程序中只要声明后都可以被调用,其作用域通常是整个程序(多个源文件组成的项目中,在声明后可以跨源文件调用)。

三、static 全局变量与普通全局变量的区别

作用域不同:
static 全局变量:只在定义它的源文件内可见。
普通全局变量:在整个程序中可见,多个源文件组成的项目中,在声明后其他源文件也可以通过 extern 关键字访问。
链接属性不同:
static 全局变量具有内部链接属性。
普通全局变量具有外部链接属性。

四、面向过程和面向对象是两种不同的编程范式,区别如下:

设计思想

  • 面向过程:以过程(可以理解为步骤或函数)为中心,强调的是解决问题的步骤和流程。把一个大问题分解为多个小问题,通过一个个函数来实现这些小问题,然后按照特定的顺序调用这些函数来解决整个问题。
  • 面向对象:以对象为中心,将现实世界中的事物抽象成对象,每个对象具有自己的属性(数据)和行为(方法)。通过对象之间的交互来解决问题。

数据和行为的封装性

  • 面向过程:通常数据和操作数据的函数是分离的,缺乏良好的封装性。数据可以被程序的任何部分直接访问和修改,容易导致数据的不一致性和错误。
  • 面向对象:强调封装,将数据和操作数据的方法封装在对象中。外部只能通过对象提供的方法来访问和修改对象的内部数据,提高了数据的安全性和一致性。

可维护性和扩展性

  • 面向过程:如果程序规模较大,函数之间的调用关系复杂,那么维护和扩展会变得困难。因为一个函数的修改可能会影响到多个其他函数,需要仔细分析整个程序的流程才能确保修改的正确性。
  • 面向对象:具有较高的可维护性和扩展性。当需要修改一个对象的行为时,只需要修改该对象对应的类即可,不会影响到其他不相关的对象。同时,可以通过继承和多态等机制方便地扩展现有代码。

代码复用性

  • 面向过程:主要通过函数的复用实现代码复用。但函数的复用往往局限于特定的问题领域,复用程度相对较低。
  • 面向对象:可以通过类的继承、组合等方式实现更高程度的代码复用。子类可以继承父类的属性和方法,并且可以根据需要进行扩展和修改。同时,可以将多个对象组合成更复杂的对象,提高代码的复用性。
五、C 语言中堆(heap)和栈(stack)的区别

管理方式
栈:由编译器自动管理。当函数被调用时,为函数内的局部变量在栈上分配空间,函数执行结束后,这些空间自动被释放。
堆:由程序员手动管理,使用诸如 malloc、calloc、realloc 等函数进行分配,使用 free 函数进行释放。如果不进行释放,可能会导致内存泄漏。
空间大小
栈:空间通常较小,一般是几兆字节。
堆:空间相对较大,具体大小取决于系统的内存和虚拟内存设置。
增长方向
栈:向低地址方向增长。
堆:向高地址方向增长。
存储内容
栈:主要存储局部变量、函数参数、返回地址等。
堆:用于存储动态分配的数据结构,如动态数组、链表节点等。

六、进程和线程的区别

定义

  • 进程:是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。
  • 线程:是进程中的一个执行单元,是操作系统能够进行运算调度的最小单位。

资源占用

  • 进程:拥有独立的地址空间、内存、文件描述符等系统资源。
  • 线程:共享所属进程的资源,如地址空间、文件描述符等,但每个线程有自己的栈、寄存器等少量私有数据。

切换开销

  • 进程切换开销较大,因为需要切换地址空间、刷新内存缓存等操作。
  • 线程切换开销较小,因为多个线程共享同一地址空间,不需要切换地址空间等复杂操作。

通信方式

  • 进程间通信相对复杂,可以通过管道、消息队列、共享内存、信号量等方式进行通信。
  • 线程间通信相对简单,可以直接通过共享内存进行通信,也可以使用互斥锁、条件变量等同步机制进行协调。
七、对 bootloader 的认识及为何需要它才能启动 Linux

认识
Bootloader 是在操作系统内核运行之前运行的一段小程序。它的主要作用是初始化硬件设备、建立内存空间的映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核做好准备。
常见的 bootloader 有 U-Boot、GRUB 等。
为何需要 bootloader 才能启动 Linux

  • 硬件初始化:计算机硬件在启动时处于一个未知状态,需要进行初始化才能正常工作。Bootloader 负责初始化 CPU、内存、外设等硬件设备,为操作系统的运行创造条件。
  • 加载内核:操作系统内核通常存储在磁盘等存储设备上,需要被加载到内存中才能运行。Bootloader 负责从存储设备中读取内核映像,并将其加载到内存中的特定位置。
  • 传递参数:Bootloader 可以将一些启动参数传递给操作系统内核,如内核命令行参数、内存布局等信息,以便内核根据这些参数进行正确的初始化。
  • 启动选择:在一些系统中,可能存在多个操作系统或内核版本可供选择。Bootloader 可以提供一个启动菜单,让用户选择要启动的操作系统或内核版本。
八 指针的大小
#include <stdio.h>
int main()
{
	char str1[100];
	printf("sizeof(str1) is %d\n",sizeof(str1));
	void *p=malloc(100);
	printf("sizeof(p) is %d\n",sizeof(p));
 } 

为什么sizeof( p )是8?
在很多现代的系统中,指针的大小通常是固定的,并且取决于系统的架构。
如果是 64 位系统,指针通常占用 8 个字节;如果是 32 位系统,指针通常占用 4 个字节。
在你的例子中,p是一个指向 void类型的指针,在该系统中指针大小为 8 字节,所以 sizeof§ 的结果是 8。这反映了系统在内存寻址时使用的地址空间大小。在 64 位系统中,需要足够的位数来表示更大范围的内存地址,因此指针占用 8 字节。

void Func(char str2[100])
{
    sizeof(str2)=?
 }

在函数Func中,sizeof(str2)的结果取决于编译器和系统架构,但通常情况下,它会是一个指针的大小,而不是数组的实际大小。
这是因为在 C 语言中,当数组作为函数参数传递时,实际上传递的是数组首元素的地址,相当于一个指针。
如果是 64 位系统,指针通常为 8 个字节;如果是 32 位系统,指针通常为 4 个字节。
所以在不知道具体系统架构的情况下,不能确定sizeof(str2)的确切值,但通常不是 100。

9.跑步

某人准备跑20圈来锻炼自己的身体,他准备分多次(>1)跑完,每次都跑正整数圈,然后休息下再继续跑。 为了有效地提高自己的体能,他决定每次跑的圈数都必须比上次跑的多 设第一次圈数不能小于0,那么请问他可以有多少种跑完这 20 圈的方案? 输出方案总数,以及每种方案的排序。(比如1,19/ 1,2,17 都是有效方案)

;