【嵌入式八股9】基础语法
1. register
关键字
在早期的 C++ 标准里,register
是一个特殊的关键字,其主要用途是向编译器提出建议,希望编译器将所声明的变量存储在寄存器中。寄存器是位于 CPU 内部的高速存储单元,相较于普通的内存,对寄存器中数据的访问速度要快得多,因为访问寄存器无需像访问内存那样经过一系列复杂的寻址操作,从而能够显著提高程序对变量的访问速度,进而提升程序的整体性能。
以下是一个简单示例:
#include <iostream>
int main() {
register int num = 10;
std::cout << num << std::endl;
return 0;
}
不过,需要明确的是,使用 register
关键字声明变量,并不意味着该变量一定会被存储在寄存器中。编译器会综合考虑多种因素,如寄存器的可用性、变量的作用域等,来决定是否将变量存储在寄存器中。若编译器无法满足将变量存储在寄存器的要求,那么该变量将按照常规方式存储在内存中。
自 C++11 标准开始,register
关键字已被弃用。这是因为现代的编译器已经具备了非常强大的智能化优化能力,它们能够基于自身先进的优化算法以及对代码的深入分析,自动且精准地决定何时将变量存储在寄存器中,而无需开发人员再使用 register
关键字进行手动提示。所以,即便在代码中使用了 register
关键字,编译器也会直接忽略它,而是依据自身的优化策略来选择最佳的存储方式。
2. sizeof()
运算符
sizeof
运算符是 C 和 C++ 中一个非常实用的操作符,它主要用于计算数据类型或表达式所占用的字节数。其工作机制是在编译阶段,编译器会查找符号表,判断数据的类型,然后根据基础类型来确定并返回对应的字节数。
示例如下:
#include <iostream>
int main() {
int num;
std::cout << "int 类型占用字节数: " << sizeof(num) << std::endl;
return 0;
}
然而,如果 sizeof
运算符的参数是一个不定长数组,情况就有所不同了。此时,编译器无法在编译时确定数组的长度,因此需要在运行时进行计算。
3. 字符设备与块设备
在嵌入式系统和操作系统中,字符设备和块设备是两种常见的设备类型,它们有着不同的功能和特点。
字符设备
字符设备主要用于操纵并读取硬件状态。它以字符为单位进行数据的传输和处理,数据的传输是连续的、无结构的。例如,键盘、鼠标、串口等设备都属于字符设备。字符设备通常用于实时性要求较高的场景,数据的读写操作是逐字符进行的。
块设备
块设备则主要具备存储功能,它以块(通常是扇区)为单位进行数据的读写操作。用户可以先将数据写入块设备,之后再进行读取。常见的块设备有硬盘、U盘等。块设备的数据传输效率较高,适合处理大量的数据存储和读取任务。
4. extern "C"
的作用
在 C++ 编程中,extern "C"
是一个非常重要的特性,其主要作用是实现 C++ 代码对 C 编写的模块的正确调用。
由于 C++ 支持函数重载和命名空间等特性,编译器在编译 C++ 代码时会对函数名进行修饰(Name Mangling),以保证函数名的唯一性。而 C 语言不支持这些特性,其函数名不会被修饰。因此,当 C++ 代码需要调用 C 编写的函数时,如果不使用 extern "C"
,就会出现链接错误。
示例如下:
// C 代码,test.c
#include <stdio.h>
void hello() {
printf("Hello from C!\n");
}
// C++ 代码,test.cpp
#include <iostream>
extern "C" {
void hello();
}
int main() {
hello();
return 0;
}
5. 32 位与 64 位系统的区别
32 位系统和 64 位系统在多个方面存在显著差异,主要体现在 CPU 通用寄存器的数据宽度和寻址能力上。
CPU 通用寄存器的数据宽度
CPU 通用寄存器的数据宽度决定了 CPU 一次能够并行处理的二进制位数。32 位系统的 CPU 通用寄存器宽度为 32 位,即一次能够处理 32 位(4 字节)的数据;而 64 位系统的 CPU 通用寄存器宽度为 64 位,一次能够处理 64 位(8 字节)的数据。这使得 64 位系统在处理大规模数据和复杂计算时具有更高的效率。
寻址能力
寻址能力是指系统能够访问的内存地址范围。32 位系统的寻址能力有限,仅支持最大 4GB 的内存寻址。这是因为 32 位系统使用 32 位二进制数来表示内存地址,其最大可表示的地址数为 ,即 4GB。而 64 位系统的寻址能力则要强大得多,理论上可以支持高达
字节的内存寻址,这在处理大规模数据和运行复杂应用程序时具有明显的优势。
以下是不同数据类型在 32 位机和 64 位机上所占字节数的对比:
char |
1 | 1 |
short |
2 | 2 |
int |
4 | 4 |
long |
4 | 8 |
float |
4 | 4 |
char * |
4 | 8 |
long long |
8 | 8 |
double |
8 | 8 |
long double |
10/12 | 10/16 |
6. 大小端模式
在计算机系统中,数据的存储方式存在大端模式和小端模式两种。
大小端模式的定义
- 大端模式:在大端模式下,数据的高位字节存储在低地址,低位字节存储在高地址。也就是说,数据的存储顺序与人们通常的书写顺序一致。
- 小端模式:小端模式则相反,数据的低位字节存储在低地址,高位字节存储在高地址。
例如,对于十六进制数 0x12345678
,在大端模式和小端模式下的内存排布如下:
存储方式 | 高位存在低地址 | 高位存在高地址 |
内存排布 0x12345678 |
低地址 - 高地址:12 34 56 78 |
低地址 - 高地址:78 56 34 12 |
举个例子,假设有一个 32 位的整数 0x12345678,存储在内存中的方式如下:
-
大端模式:
地址 数据 0x00 0x12 0x01 0x34 0x02 0x56 0x03 0x78
- **小端模式**:
```Objective-C++
地址 数据
0x00 0x78
0x01 0x56
0x02 0x34
0x03 0x12
常见芯片的大小端模式
STM32 芯片采用的是小端模式。以 0x12345678
为例,在 STM32 的内存中,从低地址到高地址依次存储的是 78 56 34 12
。
大小端模式的判断方法
#include <stdio.h>
// 方法一:使用共用体
union Un {
int a;
char b;
};
int is_little_endian1(void) {
union Un un;
un.a = 0x12345678;
if (un.b == 0x78) {
printf("小端\n");
} else {
printf("大端\n");
}
return 0;
}
// 方法二:使用指针
int is_little_endian2(void) {
int a = 0x12345678;
char b = *((char *)(&a)); // 指针方式其实就是共用体的本质
if (b == 0x78) {
printf("小端\n");
} else {
printf("大端\n");
}
return 0;
}
int main() {
is_little_endian1();
is_little_endian2();
return 0;
}
大小端模式的转换方法
// 变为 u8 类型数组后位移拼接
#include <stdint.h>
static inline uint32_t lfs_fromle32(uint32_t a) {
return (((uint8_t*)&a)[0] << 0) |
(((uint8_t*)&a)[1] << 8) |
(((uint8_t*)&a)[2] << 16) |
(((uint8_t*)&a)[3] << 24);
}
7. 段错误
在 Linux 下的 C/C++ 编程中,段错误(Segmentation Fault)是一种常见且较为严重的错误,它通常是由于访问内存管理单元(MMU)异常所导致的。当程序试图访问一个不属于当前进程地址空间中的地址时,操作系统会进行干涉,引发 SIGSEGV
信号,从而产生段错误。
常见的段错误原因
- 空指针:程序尝试操作地址为 0 的内存区域。例如,对一个未初始化的指针进行解引用操作,就会导致空指针异常。
#include <stdio.h>
int main() {
int *ptr = NULL;
*ptr = 10; // 会引发段错误
return 0;
}
- 野指针:野指针是指指向一个已被释放或未分配内存的指针。访问野指针所指向的内存是不合法的,可能会导致数据被破坏或程序崩溃。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int));
free(ptr);
*ptr = 10; // 会引发段错误
return 0;
}
- 堆栈越界:当程序访问数组或其他数据结构时,如果超出了其合法的边界范围,就会导致堆栈越界错误。这可能会破坏相邻的内存数据,从而引发段错误。
#include <stdio.h>
int main() {
int arr[5];
arr[10] = 10; // 会引发段错误
return 0;
}
- 修改只读数据:如果程序试图修改只读数据(如字符串常量),也会引发段错误。
#include <stdio.h>
int main() {
char *str = "hello";
str[0] = 'H'; // 会引发段错误
return 0;
}
8. 局部变量未定义时初始化结果不确定的原因
在 C 和 C++ 中,当定义局部变量时,实际上是在栈中通过移动栈指针,为程序提供一个内存空间,并将这个空间与局部变量名进行绑定。由于栈内存是被反复使用的,每次使用完后并不会进行清零操作,所以这块内存空间可能还保留着上次使用时的数据,也就是所谓的“脏数据”。
因此,如果在定义局部变量时不进行初始化,那么该变量所占用的内存空间中的值就是一个随机的垃圾值,每次运行程序时这个值可能都不一样,这就导致了局部变量未定义时初始化结果的不确定性。
示例如下:
#include <stdio.h>
int main() {
int num;
printf("未初始化的局部变量 num 的值: %d\n", num);
return 0;
}
9. printf
函数的返回值
printf
函数是 C 语言中常用的输出函数,它的返回值是实际输出的字符数量。这个返回值可以用来检查输出是否成功或者统计输出的字符数。
示例如下:
#include <stdio.h>
int main() {
int count = printf("Hello, World!\n");
printf("输出的字符数量: %d\n", count);
return 0;
}
10. 可变长度数组(VLA)
在 C99 标准中,允许在函数
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
一些八股模拟拷打Point,万一有点用呢