【嵌入式八股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 位机上所占字节数的对比:

数据类型 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,万一有点用呢

全部评论
接好运
点赞 回复 分享
发布于 03-06 20:00 山东
sizeof怎么用的
点赞 回复 分享
发布于 03-06 00:01 北京
register还有用吗
点赞 回复 分享
发布于 03-04 19:21 陕西
接好运
点赞 回复 分享
发布于 03-04 07:52 陕西

相关推荐

起一个响亮的名字吧xzx:学习 c++
点赞 评论 收藏
分享
评论
1
4
分享

创作者周榜

更多
牛客网
牛客企业服务