基本数据类型

基本数据类型表格

基本数据类型语义

  1. 尺度语义:告知编译器该类型的内存大小(空间解释权)

    1. char 的语义:告诉编译器,这个变量的内存是1字节。

    2. int的语义:告诉编译器,这个变量的内存是4字节

  2. 解释语义:告知该段内存里的0和1是什么意思(数据解释权)

内存里存的都是二进制(比如 01000001),它本身没有意义。类型的语义决定了如何翻译这串二进制。

假设内存存了4个字节:01000000 00000000 00000000 00000000

  • 如果是 int 语义:int 语义:编译器会把它翻译成整数:1073741824。

  • 如果是 float 语义:编译器会把它翻译成浮点数:0.5。

  • 如果是 char* 语义:编译器会把它翻译成字符 ‘@’(以及后面三个垃圾字符)。

  1. 行为语义:能对这段内存做什么操作(运算规则权)

    1. int 的语义:支持加减乘除,取模。

    2. 指针 的语义:支持“解引用”(去地址找数据),“指针加减”(移动步长)。

    3. 结构体 的语义:支持“成员访问”(用点号 . 找子数据)。

整数在内存中的存储

整数一律采用补码的形式来存储

整数的原码,反码,补码相同。

负数的原码,反码,补码各不相同

  1. 原码

将一个整数转换成二进制形式,就是其原码。

short a = 6:a的原码就是 0000 0000 0000 0110

short a = 18:a 的原码就是1000 0000 0001 0010

  1. 反码

对于正数,原码和反码相同。

对于负数,反码是将原码中除符号位以外的所有位(数值位)取反,也就是 0 变成 1,1 变成 0。

short a = 6:a的反码就是 0000 0000 0000 0110

short a = 18:a 的反码就是1111 1111 1110 1101

  1. 补码

对于正数,原码和补码相同。

对于负数,补码是其反码加 1。

short a = 6:a的补码就是 0000 0000 0000 0110

short a = 18:a 的反码就是1111 1111 1110 1110

固定宽度类型

  1. 固定宽度类型表格

  2. 固定宽度类型打印

    1. 包含头文件:#include <inttypes.h>

    2. 拼接格式符:使用 PRI 开头的宏。

类型修饰

const

核心概念与本质

  1. const 关键字的作用是什么?它定义的变量是真正的常量么?

const 是类型限定符,用于修饰变量,指针或函数参数,表示该对象是只读的。

  • 不是真正的常量:严格来说,const修饰的是“只读变量”。它在内存中依然 占据空间(有地址),只是在编译器禁止了通过变量名进行的直接修改。

  • 与宏的区别:相比 #define ,const 有数据类型,编译会进行 类型安全检查,且调试时可见(#define 只是文本替换,无类型检查)。

  1. const 变量的存储位置在哪里?

这取决于变量的作用域和编译器实现:

  • 局部 const 变量:通常存储在 栈区,和普通局部变量一样,只是编译器限制了写操作。

  • 全局 const 变量:在现代编译器中,通常存储在只读数据段(.rodata)。如果尝试通过指针暴力修改,程序可能会崩溃(Segmentation Fault)。

指针与 const 的纠缠

建议记住口诀:“左定值,右定向”(即 const 在 * 左边修饰内容,在右边修饰指针本身)。

  1. 请解释 const int *p,int *const p和cosnt int *const p 的区别

函数中的应用

  1. 为什么函数参数要使用 const 修饰(如 void func(const char *str))?
  • 保护数据:明确告诉调用者和编译器,函数内部不会修改传入的数据,防止以外篡改,增强代码健壮性。

  • 效率优化:当传递大型结构体或类对象时,使用 const 引用(C++中)或 const 指针可以 避免深拷贝 带来的性能开销,同时保证数据安全。

  1. 函数返回值加 const 有什么意义?

通常用于返回指针或引用。加上 const 是为了防止调用者利用返回值修改函数内部的私有数据(例如静态变量或成员变量)。

例子:const char* get_error_msg() 返回一个字符串字面量,调用者不应也不能修改这个字符串。

进阶与陷阱

  1. 如何绕过 const 的限制修改变量?这样做安全么?

可以通过强制类型转换去除 const 属性。

const int val = 10;
int *p = (int *)&val; *// 强制转换*
*p = 20;              *// 试图修改*
  1. const** 和 volatile 可以同时使用吗?是什么意思?**

可以。const volatile int a; 这种写法常见于嵌入式开发。

  • 含义:

    • const:告诉程序代码,我不能修改这个变量(只读)。

    • volatile:告诉编译器,这个变量可能会被外部因素(如硬件寄存器、中断服务程序)修改,不要做优化,每次必须从内存地址重新读取。

  • 场景: 比如一个只读的状态寄存器,软件只能读,但硬件会自动更新它的值。

volatile

基础原理与本质

  1. 请简述 volatile 关键字的作用?

volatile 是一个类型限定符,用于告诉编译器:“这个变量是不稳定的,随时可能被程序之外的因素修改。”

因此,编译器禁止对该变量进行任何优化。每次使用该变量时,必须直接从内存地址中读取其最新值,而不是使用寄存器中的缓存值;每次写入也必须立即写回内存。

  1. 如果不加 volatile,编译器通常会做哪些优化导致程序出错?

编译器为了提高效率,通常会进行以下两种优化,这在处理特殊变量时会导致灾难性后果:

  • 寄存器缓存:编译器将变量值读入 CPU 寄存器后,后续访问直接用寄存器的值,不再去读内存。如果外部硬件修改了内存中的值,程序却还在用旧的寄存器值。

  • 死代码消除/冗余读取:如果编译器发现两次读取之间没有修饰变量,它可能会优化掉第二次读取。

举例:在等待一个状态标志位变化的 while(!flag); 循环中,如果没有 volatile ,编译器可能认为 flag 在循环内没变,直接将其优化为死循环。

三大经典应用场景

  1. 你在什么场景下会使用 volatile?请举例说明。
场景
描述 代码/逻辑示例
访问硬件寄存器 在嵌入式中,外设寄存器(如 GPIO、串口数据寄存器)
的值由硬件自动更新,而非软件赋值。
#define STATUS_REG (*(volatile uint32_t *)0x40021000)
必须每次从该地址读取真实硬件状态。
中断服务函数 (ISR) 全局变量在主程序中被检测,但在 ISR 中被修改。
主程序必须感知到 ISR 的修改。
主循环检测 flag,ISR 中置位 flag=1。若无 volatile,
主循环可能永远看不到变化。
多任务/多线程共享 多任务/多线程共享 任务 A 修改 run_flag,任务 B 根据该标志位决定是否退出循环。

多线程与并发陷阱

  1. volatile** 能保证线程安全吗?为什么?**
  • 标准回答:

不能! 这是一个非常经典的误区。

  • volatile 只能保证可见性(Visibility),即保证每次读取的是内存中的最新值。

  • 它不保证原子性(Atomicity)。

  • 举例说明:

对于 counter++ 操作,它包含“读取 -> 加 1 -> 写回”三个步骤。即使 counter 是 volatile 的,两个线程同时执行时,仍可能发生竞争条件(Race Condition),导致结果错误。

解决方案: 真正的线程安全需要使用互斥锁(Mutex)、信号量或 C11 标准中的原子操作(_Atomic)。

typedef

typedef 的作用是什么?

主要用途体现在以下三个方面:

  • 简化复杂类型声明:为冗长的指针、数组、结构体或函数指针类型起一个简洁的名字,让代码更清爽。

  • 增强代码可读性:用语义化的名字替代原始类型,让代码意图更清晰。例如,用 BYTE 代替 unsigned char,用 Status 代替 int。

  • 提升代码可移植性:当需要适配不同平台时,只需修改 typedef 的定义即可。例如,通过条件编译为 int 定义一个别名 INT32,在不同平台上调整其实际类型。

typedef 和 #define 有什么区别?

构造类型

数组

核心定义与基础语法

数组(Array)允许你使用一个变量名来存储多个同类型的数据,避免了定义成百上千个独立变量的繁琐。

  • 定义格式:
dataTyoe arrayName[length];
// dataType:数据类型(如 int,char)
// arrayName:数组名
// length:数组长度(元素个数)
  • 下标规则:数组下标从 0 开始,而非1。例如 nums[0]代表第一个元素,nums[3]代表第四个元素。

数组的初始化与内存特性

理解数组如何在内存中存储以及如何赋值,是避免Bug的关键。

  • 初始化方式:

    • 部分赋值:若 {} 中的值少于数组长度,剩余元素会自动初始化为 0(整型)、'\0'(字符型)或 00.0(浮点型)。

    • 整体赋值限制:只能逐个元素赋值,不能直接用 int a[10] = 1; 这种形式给整个数组赋值。

    • 自动推断长度:如果给所有元素赋值,可以省略长度,由编译器自动推断(例如 int a[] = {1,2,3};)。

  • 内存布局:数组在内存中是连续存储的,元素之间紧密排列,没有缝隙。

二维数组与字符数组(字符串)

数组的概念可以扩展到多维,而字符数组是处理字符串的核心。

  • 二维数组:

    • 格式:dataType arrayName[length1][length2];(例如 int a[3][4] 表示3行4列)。

    • 存储方式:按行排列,先存满第一行,再存第二行。

    • 初始化:支持按行分段赋值({{1,2}, {3,4}})或按行连续赋值({1,2,3,4})。

  • 字符数组与字符串:

    • 结束标志:C语言中字符串以 '\0' 作为结束标志。由双引号 "" 包围的字符串会自动在末尾添加 '\0',但手动逐个字符赋值时不会自动添加,必须手动补上,否则可能导致乱码或越界。

    • 常用函数:

      • strcat():字符串连接(需确保目标数组足够大)。

      • strcpy():字符串复制。

      • strcmp():字符串比较(基于ASCII码值)。

静态特性与变长数组

这是C语言数组的一个重要特性,也是初学者容易混淆的点。

  • 静态性:在标准C语言中,数组一旦定义,其长度是固定的。

    • 限制:不能直接插入或删除元素。

    • 后果:如果需要改变大小,必须重新定义一个新数组并复制数据。

  • 变长数组(C99标准):

    • C89:数组长度必须是常量表达式(不能包含变量)。

    • C99:允许使用变量来指明数组长度(例如 int n; scanf("%d", &n); int a[n];)。注:虽然GCC等编译器支持,但在部分旧版编译器(如VC/VS)中可能报错。

结构体 (struct)

用于将多个不同类型但逻辑上相关的数据组合成一个整体。结构体的每个成员都有自己独立的内存空间。

  • 特点:成员独立存储,总大小是各成员大小之和(需考虑内存对齐)。

  • 用途:描述一个实体,如学生(包含姓名、年龄、学号等)。

枚举 (enum)

用于定义一组命名的整数常量,使代码更具可读性。

  • 特点:本质上是一系列整型常量的集合,用于替代“魔术数字”。

  • 用途:表示有限的状态或选项,如星期(Mon, Tue, Wed...)、颜色(RED, GREEN, BLUE)。

  • 注意点:

    • 枚举列表中的 Mon、Tues、Wed 这些标识符的作用范围是全局的(严格来说是 main() 函数内部),不能再定义与它们名字相同的变量。

    • Mon、Tues、Wed 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量。枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。

联合体 (union)

允许在同一段内存空间中存储不同类型的数据。在任何给定时刻,只有一个成员是有效的。

  • 特点:所有成员共享内存,总大小等于最大成员的大小。

  • 用途:节省内存,或在不同时间以不同方式解释同一块数据,如硬件寄存器映射、协议解析。

  • 大端模式(Big-endian)和小端模式(Little-endian)

位域 (Bit-field)

它不是一个独立的数据类型,而是结构体的一种特殊成员。它允许你精确指定一个成员所占用的比特位数。

  • 特点:用于精细控制内存,可以节省空间,但可移植性较差。

  • 用途:在嵌入式开发或网络协议中,需要按位操作硬件或解析特定格式的数据包。