嵌入式面试高频!!!C语言(二)

一、源码,反码,补码

源码(原码)

定义
  • 源码是最直观的二进制表示方法,最高位为符号位(0 表示正数,1 表示负数),其余位表示数值的绝对值。
  • 以 8 位二进制数为例:
    • 正数 +5 的源码:0000 0101(符号位 0,数值位 5 的二进制)
    • 负数 -5 的源码:1000 0101(符号位 1,数值位 5 的二进制)
特点
  • 简单直观:直接对应数值的正负和大小,便于人类理解。
  • 零的表示不唯一
    • +0 的源码:0000 0000
    • -0 的源码:1000 0000
  • 运算局限性:正数与负数相加时,需单独处理符号位,无法直接用加法器运算(如 +5 + (-5) 需手动处理符号)。

反码

定义
  • 正数的反码:与源码相同。
  • 负数的反码:符号位保持为 1,其余位按位取反(0 变 1,1 变 0)。
  • 以 8 位二进制数为例:
    • 正数 +5 的反码:0000 0101(同源码)
    • 负数 -5 的源码:1000 0101 → 反码:1111 1010(数值位取反)
特点
  • 零的表示仍不唯一
    • +0 的反码:0000 0000
    • -0 的反码:1111 1111
  • 运算改进:负数反码可用于简化加减法运算,但仍存在零的二义性问题。
  • 示例运算
    +5(0000 0101) + (-5的反码1111 1010) = 1111 1111(即-0的反码),结果需进一步处理。

补码

定义
  • 正数的补码:与源码、反码相同。
  • 负数的补码:符号位保持为 1,其余位先取反,再在最低位加 1(即 “反码 + 1”)。
  • 以 8 位二进制数为例:
    • 正数 +5 的补码:0000 0101(同源码)
    • 负数 -5 的源码:1000 0101 → 反码:1111 1010 → 补码:1111 1011
特点
  • 零的表示唯一
    +0 和 -0 的补码均为 0000 0000(例如 -0 的源码 1000 0000 → 反码 1111 1111 → 补码 1111 1111 + 1 = 1 0000 0000,8 位下高位进位舍去,结果为 0000 0000)。
  • 高效运算:补码可将减法转换为加法,计算机只需实现加法器即可处理所有整数运算。
    原理:负数的补码本质是其绝对值对 2^n(n 为位数)的模,例如 8 位补码中,-5 的补码等于 256 - 5 = 251(即二进制 1111 1011)。
  • 运算示例
    +5(0000 0101) + (-5的补码1111 1011) = 1 0000 0000(8 位下进位舍去,结果为 0000 0000,即 0)。

三种编码的对比

编码类型 正数表示 负数表示 零的表示 运算特性
源码 符号位 0 + 数值位 符号位 1 + 数值位 +0-0不同 无法直接用于加减运算
反码 同源码 符号位 1 + 数值位取反 +0-0不同 可简化运算,但仍有缺陷
补码 同源码 反码 + 1 唯一(0000...0 支持直接加减,计算机首选

补码的本质与意义

  • 模运算思想:补码利用 “模” 的概念将负数转换为正数,例如 8 位二进制的模为 2^8 = 256,则 -5 ≡ 251 (mod 256),251 即 -5 的补码。
  • 计算机硬件友好:补码使加减法统一为加法运算,减少硬件设计复杂度,是现代计算机存储有符号整数的标准方式。
  • 范围扩展:n 位补码的表示范围为 [-2^(n-1), 2^(n-1)-1],例如 8 位补码可表示 -128 到 +127(其中 -128 的补码为 1000 0000,无对应的源码和反码)。

        

二、计算机编程中的continue(核心场景)

1. 定义与作用
  • 关键词功能:在循环结构(如forwhile)中,用于跳过当前循环的剩余代码,直接进入下一次循环迭代。
  • 核心逻辑:中断 “本次循环” 的执行,但不终止整个循环流程(区别于break)。
# 输出1-10中除5以外的数字
for i in range(1, 11):
    if i == 5:
        continue  # 跳过i=5的情况
    print(i)  # 输出:1,2,3,4,6,7,8,9,10

2. 应用场景
  • 数据过滤:跳过不符合条件的元素(如上述示例)。
  • 错误处理:忽略暂时无法处理的异常情况,继续后续流程。
  • 性能优化:提前跳过无效计算,减少循环开销

三、计算机编程中的break(核心场景)

1. 定义与作用
  • 关键词功能:在循环(for/while)或分支结构(如switch)中,强制终止当前结构的执行,跳出至外层代码。
  • 核心逻辑:立即中断 “整个循环 / 结构”,不再执行后续迭代或分支(区别于continue仅跳过当前迭代)。

2. 代码示例(Python) 

# 找到第一个能被3整除的数并终止循环
numbers = [1, 2, 4, 5, 7, 9]
for num in numbers:
    if num % 3 == 0:
        print(f"找到第一个能被3整除的数:{num}")
        break  # 终止循环
# 输出:找到第一个能被3整除的数:9
3. 应用场景
  • 条件满足时提前退出:如上述示例的 “快速查找” 场景。
  • 异常终止:当检测到错误条件时,立即停止当前操作(如文件读取失败时中断流程)。
  • 嵌套循环控制:在多层循环中,break仅跳出当前层(如需跳出多层,需配合标志位或函数封装)。
continue的对比(编程场景)
关键词 作用 对循环的影响
break 终止整个循环 / 结构 跳出至外层代码,不再执行
continue 跳过当前迭代,继续下一次 循环未终止,继续后续迭代

 四、计算机编程中的return(核心场景)

        结束当前循环,退出函数,用在函数体中,返回特定值。 

五、计算机编程中的Volatile(核心场景) 

1. 定义与底层机制
  • 关键词功能:用于修饰变量,告知编译器 “该变量的值可能在程序之外被改变”,禁止对其进行优化(如缓存、重排序)。
  • 核心逻辑:确保每次访问变量时,直接从内存读取最新值(而非寄存器或缓存),写入时立即同步到内存。
2. 为什么需要volatile
  • 编译器优化的 “副作用”
    编译器为提升性能,可能将频繁访问的变量缓存至寄存器(如int a = 0; while(a==0) { ... }中,若a未被标记volatile,编译器可能假设a值不变,直接优化为死循环)。
  • 多线程 / 硬件交互场景
    变量可能被外部设备(如 IO 端口)或其他线程修改,需强制每次访问时获取最新值。

volatile int flag = 0;  // 标记为易变变量

// 线程1: 等待flag被修改
while (flag == 0) {
    // 若flag未被标记volatile,编译器可能优化为死循环
}
printf("Flag changed!");

// 线程2: 其他线程修改flag
flag = 1;

二、硬件与系统编程中的volatile

1. 嵌入式系统与寄存器操作
  • 场景:操作硬件寄存器时(如 GPIO、定时器),必须使用volatile
    例:volatile unsigned int *GPIO_PIN = (unsigned int*)0x12345678;
    (若不标记volatile,编译器可能缓存寄存器值,导致硬件操作失效)。
2. 多线程共享变量的可见性
  • volatile相关的并发问题
    • 缓存不一致:不同线程可能读取到变量的旧值(如 CPU 缓存未同步)。
    • 指令重排序:编译器 / 处理器可能调整指令顺序,导致逻辑错误。
  • volatile的局限性
    仅保证 “可见性”,不保证原子性(如i++仍需加锁)。
关键字 解决的问题 性能开销
volatile 可见性、有序性
synchronized(Java) 原子性、可见性、有序性
lock(C#) 原子性、资源互斥

 六、struct 结构体 C和C++的区别 求结构体大小,使用的注意事项

一、C 与 C++ 中结构体的核心区别

1. 默认访问权限
  • C:所有成员默认public(仅支持公开访问)。
  • C++:可指定public/private/protected(默认public,但支持封装)。
2. 成员函数与继承
  • C
    • 仅包含变量,不支持函数、构造函数、析构函数。
    • 不支持继承。
  • C++
    • 可包含成员函数、静态成员、常量。
    • 支持继承其他类 / 结构体,实现多态。
3. 类型声明
  • C:使用时需显式加struct关键字(除非用typedef)。
struct Point { int x, y; };
struct Point p;  // 必须带struct

 C++:可直接使用类型名。

struct Point { int x, y; }; Point p; // 无需struct 

4. 初始化方式
  • C
    • 只能用聚合初始化(按顺序初始化所有成员)

struct Point p = {1, 2};  // 正确
struct Point p = {.y=2, .x=1};  // C99指定初始化器

 C++

  • 支持构造函数、默认值、列表初始化。
struct Point { int x=0, y=0; };
Point p{1, 2};  // 列表初始化
Point p = {.y=2};  // C++20指定初始化器

二、结构体大小计算规则

1. 内存对齐(Alignment)
  • 原则
    • 成员变量按自身大小或指定对齐值(#pragma pack)的较小值对齐。
    • 结构体总大小必须是最大对齐值的整数倍。

2. 计算示例

// 示例1
struct S1 {
    char a;   // 1字节 → 偏移0
    int b;    // 4字节 → 对齐到4的倍数,偏移4(填充3字节)
    short c;  // 2字节 → 偏移8
};  // 总大小:10 → 对齐到4的倍数,最终12字节

// 示例2(调整顺序)
struct S2 {
    int b;    // 4字节 → 偏移0
    short c;  // 2字节 → 偏移4
    char a;   // 1字节 → 偏移6
};  // 总大小:7 → 对齐到4的倍数,最终8字节

3. 指定对齐方式
  • #pragma pack(C/C++)
#pragma pack(1)  // 按1字节对齐(取消填充)
struct S3 {
  char a;  // 1字节 → 偏移0
  int b;   // 4字节 → 偏移1
  short c; // 2字节 → 偏移5
};  // 总大小:7字节
#pragma pack()

三、使用结构体的注意事项

1. 内存对齐优化
  • 成员排序:按大小降序排列(如int → short → char),减少填充。
  • 慎用#pragma pack:虽减少空间,但可能降低内存访问效率(非对齐访问更耗时)。
2. C++ 特有的陷阱
  • 空结构体大小:C++ 中空结构体大小至少为 1 字节(保证不同对象地址唯一)。
struct Empty {};  // sizeof(Empty) = 1

 虚函数与虚表:含虚函数的结构体有虚表指针(通常 4/8 字节)。

struct Base { virtual void f() {} };  // sizeof(Base) = 8(64位系统)

总结对比表

特性 C 语言结构体 C++ 结构体
默认访问权限 全部public 可指定public/private
成员函数 不支持 支持(包括构造 / 析构函数)
继承与多态 不支持 支持
类型声明 需显式写struct 可直接使用类型名
初始化方式 聚合初始化 支持构造函数、列表初始化
空结构体大小 未定义(通常 0) 至少 1 字节
内存对齐原则 依赖编译器 依赖编译器(更复杂)

  七、class和Sture的区别

C++ 中classstruct的核心区别

在 C++ 中,classstruct都用于定义自定义数据类型,但它们在默认访问控制继承方式设计意图上存在关键差异:

一、默认访问控制

特性 struct class
默认成员权限 public(所有成员公开) private(所有成员私有)
默认继承权限 public(基类成员保持原权限) private(基类成员变为私有)
struct S {
    int a;  // 默认public
private:
    int b;  // 显式设为private
};

class C {
    int a;  // 默认private
public:
    int b;  // 显式设为public
};

二、继承与多态

  • 继承方式

struct Base { int x; };
struct Derived_S : Base {};  // 默认public继承
class Derived_C : Base {};   // 默认private继承
  • Derived_S可访问Basepublic成员(因public继承)。
  • Derived_C无法访问Basepublic成员(因private继承)。

多态性
两者均可通过虚函数实现多态,但需注意: 

struct Base { virtual void f() {} };
class Derived : public Base { void f() override {} };

Base* ptr = new Derived();  // 需显式public继承才能正确转型

三、设计意图与最佳实践

场景 推荐使用struct 推荐使用class
数据聚合 仅包含数据,无复杂行为 -
封装与抽象 - 需隐藏实现细节,提供接口
面向对象设计 - 需继承、多态、复杂方法
与 C 兼容 纯数据结构(无 C++ 特性) -

典型案例

  • struct常用于表示简单数据(如坐标点、配置参数):

struct Point { double x, y; };  // 纯数据,无方法

class常用于实现抽象数据类型(如容器、算法):

class Vector {
private:
    int* data;
    size_t size;
public:
    Vector(size_t n);  // 构造函数
    ~Vector();         // 析构函数
    void push_back(int value);
};

四、总结:何时用哪个?

  • struct:当类型主要是数据集合,且成员需默认公开时。
  • class:当类型需封装(隐藏实现)、继承或复杂行为时。

本质上,C++ 的struct是 C 结构体的扩展,而class是 C++ 面向对象特性的核心。选择两者的关键在于访问控制需求设计意图,而非功能限制(技术上struct也能实现class的全部功能)。

你可能感兴趣的:(嵌入式八股文,c语言,开发语言,面试,嵌入式硬件,物联网,单片机,mcu)