【嵌入式八股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 标准中,允许在函数内部的栈空间中定义可变长度数组(Variable Length Array,简称 VLA)。可变长度数组的长度可以在运行时根据实际需求进行确定,而不是在编译时就固定下来。

示例如下:

#include <stdio.h>
void test_func(int len) {
    int arr[len];
    arr[0] = 1;  // 不可在定义时初始化
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    test_func(3);
    return 0;
}

需要注意的是,可变长度数组不能在定义时进行初始化,因为其长度是在运行时确定的。

11. 变长结构体

变长结构体是一种特殊的结构体,它允许在结构体中定义长度可变的缓冲区。通常的做法是在结构体中定义一个长度为 0 的数组,用于后续开辟变长的缓冲区。在释放这种结构体时,只需要释放结构体本身即可。

示例代码如下:

#include <iostream>
#include <cstring>
#include <cstdlib>
const int BUF_SIZE = 100;

// 普通结构体,使用指针指向不定长缓冲区
struct s_one {
    int s_one_cnt;
    char* s_one_buf;  // 用指针指向不定长 buf
};

// 变长结构体,使用长度为 0 的数组指向不定长缓冲区
struct s_two {
    int s_two_cnt;
    char s_two_buf[0];  // 用数组指向不定长 buf
};

int main() {
    // 赋值用
    const char* tmp_buf = "abcdefghijklmnopqrstuvwxyz";
    int ntmp_buf_size = strlen(tmp_buf);

    // 注意 s_one 与 s_two 的大小的不同
    std::cout << "sizeof(s_one) = " << sizeof(s_one) << std::endl; // 8
    std::cout << "sizeof(s_two) = " << sizeof(s_two) << std::endl; // 4
    std::cout << std::endl;

    // 为 buf 分配 100 个字节大小的空间
    int ntotal_stwo_len = sizeof(s_two) + (1 + ntmp_buf_size) * sizeof(char);

    // 给 s_one buf 赋值
    s_one* p_sone = (s_one*)malloc(sizeof(s_one));  // 开辟结构体
    memset(p_sone, 0, sizeof(s_one));
    p_sone->s_one_buf = (char*)malloc(1 + ntmp_buf_size);  // 开辟 buf
    memset(p_sone->s_one_buf, 0, 1 + ntmp_buf_size);
    memcpy(p_sone->s_one_buf, tmp_buf, ntmp_buf_size);

    // 给 s_two buf 赋值
    s_two* p_stwo = (s_two*)malloc(ntotal_stwo_len);  // 开辟结构体
    memset(p_stwo, 0, ntotal_stwo_len);
    memcpy((char*)(p_stwo->s_two_buf), tmp_buf, ntmp_buf_size);  // 不用加偏移量,直接拷贝!

    std::cout << "p_sone->s_one_buf = " << p_sone->s_one_buf << std::endl;
    std::cout << "p_stwo->s_two_buf = " << p_stwo->s_two_buf << std::endl;
    std::cout << std::endl;

    // 注意 s_one 与 s_two 释放的不同!
    if (NULL != p_sone->s_one_buf) {  // 用指针保存需要释放两次
        free(p_sone->s_one_buf);  // 释放指针
        p_sone->s_one_buf = NULL;

        if (NULL != p_sone) {
            free(p_sone);  // 释放结构体
            p_sone = NULL;
        }
        std::cout << "free(p_sone) successed!" << std::endl;
    }

    if (NULL != p_stwo) {  // 结构体保存释放一次
        free(p_stwo);  // 仅释放结构体
        p_stwo = NULL;
        std::cout << "free(p_stwo) successed!" << std::endl;
    }

    return 0;
}

12. CRC 校验

循环冗余校验(Cyclic Redundancy Check,简称 CRC)是一种常用的数据校验方法,广泛应用于数据通信和存储领域,用于检测数据在传输或存储过程中是否发生错误。

CRC 参数模型

一个完整的 CRC 参数模型应该包含以下信息:

  • NAME:参数模型名称,用于标识不同的 CRC 算法。
  • WIDTH:宽度,即生成的 CRC 数据位宽。例如,CRC - 8 表示生成的 CRC 为 8 位。
  • POLY:十六进制多项式,省略最高位 1。例如,对于多项式 ,其二进制为 1 0000 0111,省略最高位 1 后,转换为十六进制为 0x07
  • INIT:CRC 初始值,其位宽与 WIDTH 一致。通常如果只给出了一个多项式,而其他参数没有说明,则 INIT = 0x00
  • REFIN:布尔值,truefalse。表示在进行计算之前,原始数据是否需要进行翻转。例如,原始数据为 0x34(二进制 0011 0100),如果 REFINtrue,则翻转后为 0010 1100(十六进制 0x2c)。
  • REFOUT:布尔值,truefalse。表示运算完成之后,得到的 CRC 值是否需要进行翻转。例如,计算得到的 CRC 值为 0x97(二进制 1001 0111),如果 REFOUTtrue,则翻转后为 1110 1001(十六进制 0xE9)。
  • XOROUT:计算结果需要与此参数进行异或运算后得到最终的 CRC 值,其位宽与 WIDTH 一致。通常如果只给出了一个多项式,而其他参数没有说明,则 XOROUT = 0x00

CRC 校验的使用示例

#include <stdio.h>
#include <stdint.h>

// crc8 generator polynomial: G(x) = x^8 + x^5 + x^4 + 1
const unsigned char CRC8_INIT = 0xff;
const unsigned char CRC8_TAB[256] = {
    0x00, 0x5e, 0xbc, 0xe2, 0x61, 0x3f, 0xdd, 0x83, 0xc2, 0x9c, 0x7e, 0x20, 0xa3, 0xfd, 0x1f, 0x41,
    0x9d, 0xc3, 0x21, 0x7f, 0xfc, 0xa2, 0x40, 0x1e, 0x5f, 0x01, 0xe3, 0xbd, 0x3e, 0x60, 0x82, 0xdc,
    0x23, 0x7d, 0x9f, 0xc1, 0x42, 0x1c, 0xfe, 0xa0, 0xe1, 0xbf, 0x5d, 0x03, 0x80, 0xde, 0x3c, 0x62,
    0xbe, 0xe0, 0x02, 0x5c, 0xdf, 0x81, 0x63, 0x3d, 0x7c, 0x22, 0xc0, 0x9e, 0x1d, 0x43, 0xa1, 0xff,
    0x46, 0x18, 0xfa, 0xa4, 0x27, 0x79, 0x9b, 0xc5, 0x84, 0xda, 0x38, 0x66, 0xe5, 0xbb, 0x59, 0x07,
    0xdb, 0x85, 0x67, 0x39, 0xba, 0xe4, 0x06, 0x58, 0x19, 0x47, 0xa5, 0xfb, 0x78, 0x26, 0xc4, 0x9a,
    0x65, 0x3b, 0xd9, 0x87, 0x04, 0x5a, 0xb8, 0xe6, 0xa7, 0xf9, 0x1b, 0x45, 0xc6, 0x98, 0x7a, 0x24,
    0xf8, 0xa6, 0x44, 0x1a, 0x99, 0xc7, 0x25, 0x7b, 0x3a, 0x64, 0x86, 0xd8, 0x5b, 0x05, 0xe7, 0xb9,
    0x8c, 0xd2, 0x30, 0x6e, 0xed, 0xb3, 0x51, 0x0f, 0x4e, 0x10, 0xf2, 0xac, 0x2f, 0x71, 0x93, 0xcd,
    0x11, 0x4f, 0xad, 0xf3, 0x70, 0x2e, 0xcc, 0x92, 0xd3, 0x8d, 0x6f, 0x31, 0xb2, 0xec, 0x0e, 0x50,
    0xaf, 0xf1, 0x13, 0x4d, 0xce, 0x90, 0x72, 0x2c, 0x6d, 0x33, 0xd1, 0x8f, 0x0c, 0x52, 0xb0, 0xee,
    0x32, 0x6c, 0x8e, 0xd0, 0x53, 0x0d, 0xef, 0xb1, 0xf0, 0xae, 0x4c, 0x12, 0x91, 0xcf, 0x2d, 0x73,
    0xca, 0x94, 0x76, 0x28, 0xab, 0xf5, 0x17, 0x49, 0x08, 0x56, 0xb4, 0xea, 0x69, 0x37, 0xd5, 0x8b,
    0x57, 0x09, 0xeb, 0xb5, 0x36, 0x68, 0x8a, 0xd4, 0x95, 0xcb, 0x29, 0x77, 0xf4, 0xaa, 0x48, 0x16,
    0xe9, 0xb7, 0x55, 0x0b, 0x88, 0xd6, 0x34, 0x6a, 0x2b, 0x75, 0x97, 0xc9, 0x4a, 0x14, 0xf6, 0xa8,
    0x74, 0x2a, 0xc8, 0x96, 0x15, 0x4b, 0xa9, 0xf7, 0xb6, 0xe8, 0x0a, 0x54, 0xd7, 0x89, 0x6b, 0x35
};

// 计算 CRC 值
unsigned char Get_CRC8_Check_Sum(unsigned char *pchMessage, unsigned int dwLength, unsigned char ucCRC8) {
    unsigned char ucIndex;
    while (dwLength--) {
        ucIndex = ucCRC8 ^ (*pchMessage++);
        ucCRC8 = CRC8_TAB[ucIndex];
    }
    return ucCRC8;
}

// 验证 CRC 值
unsigned int Verify_CRC8_Check_Sum(unsigned char *pchMessage, unsigned int dwLength) {
    unsigned char ucExpected = 0;
    if ((pchMessage == 0) || (dwLength <= 2)) return 0;
    ucExpected = Get_CRC8_Check_Sum(pchMessage, dwLength - 1, CRC8_INIT);
    return (ucExpected == pchMessage[dwLength - 1]);
}

// 追加 CRC 值到数据末尾
void Append_CRC8_Check_Sum(unsigned char *pchMessage, unsigned int dwLength) {
    unsigned char ucCRC = 0;
    if ((pchMessage == 0) || (dwLength <= 2)) return;
    ucCRC = Get_CRC8_Check_Sum((unsigned char *)pchMessage, dwLength - 1, CRC8_INIT);
    pchMessage[dwLength - 1] = ucCRC;
}

int main() {
    uint8_t LENGTH = 10;
    uint8_t data[LENGTH];
    uint8_t crc;

    for (int i = 0; i < LENGTH; i++) {
        data[i] = i * 5;
        printf("%02x ", data[i]);
    }
    printf("\n");

    crc = Get_CRC8_Check_Sum(data, LENGTH, CRC8_INIT);
    printf("CRC - 8: %02x\n", crc);

    return 0;
}

13. 奇偶校验

奇偶校验是一种简单的数据校验方法,用于检测数据在传输或存储过程中是否发生了单个比特的错误。奇偶校验分为奇校验和偶校验两种。

奇校验

在奇校验中,如果数据中 1 的个数为奇数,则校验位为 0;如果数据中 1 的个数为偶数,则校验位为 1。这样,加上校验位后,整个数据(包括校验位)中 1 的个数为奇数。

偶校验

偶校验则相反,如果数据中 1 的个数为偶数,则校验位为 0;如果数据中 1 的个数为奇数,则校验位为 1。加上校验位后,整个数据(包括校验位)中 1 的个数为偶数。

例如,对于二进制数 1101,其中 1 的个数为 3(奇数),在奇校验中,校验码为 0;在偶校验中,校验码为 1。

14. 静态链接与动态链接

在程序的编译和运行过程中,链接是一个重要的环节,它将程序中引用的外部函数和库代码与目标代码进行关联。静态链接和动态链接是两种常见的链接方式,它们各有优缺点。

静态链接(Static Linking)

  • 链接过程:在编译时,静态链接会将所有的函数和库代码合并成一个可执行文件。链接器会从链接库和目标代码中提取所需的函数和库代码,将它们合并到最终的执行文件中。链接结果是一个独立的、完整的可执行文件,包含了所有依赖的函数和库代码。
  • 优点
    • 执行速度快,因为所有代码已经被编译和链接在一起,无需在运行时动态加载额外的库文件。
    • 可执行文件独立,可以在没有安装相应库文件的系统上运行。
  • 缺点
    • 可执行文件较大,因为所有依赖的函数和库代码都被静态链接到可执行文件中,会造成空间的浪费。
    • 更新和替换依赖的函数和库代码需要重新编译和链接整个程序,不够灵活。

动态链接(Dynamic Linking)

  • 链接过程:动态链接是在运行时通过动态链接库在内存中加载所需的函数和库代码。链接器在运行程序时动态加载所需的函数和库代码,而不是在编译时就将所有代码合并到可执行文件中。链接结果是一个可执行文件和一个或多个动态链接库,可执行文件只包含必要的启动代码和符号引用。
  • 优点
    • 可执行文件较小,因为只包含必要的启动代码和符号引用,节省了磁盘空间。
    • 动态链接库可以在多个可执行文件之间共享,节省了内存空间。
    • 更新和替换依赖的函数和库代码只需要替换对应的动态链接库,无需重新编译整个程序,更加灵活。
  • 缺点
    • 相对于静态链接,运行时需要额外的时间来加载和解析动态链接库,会影响程序的启动速度。
    • 系统中必须存在相应的动态链接库文件,否则程序无法运行,增加了程序的依赖度。

综上所述,静态链接将所有的函数和库代码合并到一个可执行文件中,执行速度快,但可执行文件较大;而动态链接在运行时加载所需的函数和库代码,可执行文件较小,但可能需要额外的加载时间,并且依赖系统存在相应的动态链接库文件。在实际开发中,需要根据项目的需求和考虑的因素来选择合适的链接方式。

#牛客激励计划#
嵌入式八股/模拟面试拷打 文章被收录于专栏

一些八股模拟拷打Point,万一有点用呢

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

相关推荐

04-03 22:41
兰州大学 C++
老六f:有时候是HR发错了,我之前投的百度的后端开发,他给我发的算法工程师,但是确实面的就是百度开发
点赞 评论 收藏
分享
评论
1
5
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务