基本数据类型
基本数据类型表格
基本数据类型语义
-
尺度语义:告知编译器该类型的内存大小(空间解释权)
-
char 的语义:告诉编译器,这个变量的内存是1字节。
-
int的语义:告诉编译器,这个变量的内存是4字节
-
-
解释语义:告知该段内存里的0和1是什么意思(数据解释权)
内存里存的都是二进制(比如 01000001),它本身没有意义。类型的语义决定了如何翻译这串二进制。
假设内存存了4个字节:01000000 00000000 00000000 00000000
-
如果是 int 语义:
int语义:编译器会把它翻译成整数:1073741824。 -
如果是
float语义:编译器会把它翻译成浮点数:0.5。 -
如果是
char*语义:编译器会把它翻译成字符 ‘@’(以及后面三个垃圾字符)。
-
行为语义:能对这段内存做什么操作(运算规则权)
-
int的语义:支持加减乘除,取模。 -
指针的语义:支持“解引用”(去地址找数据),“指针加减”(移动步长)。 -
结构体的语义:支持“成员访问”(用点号.找子数据)。
-
整数在内存中的存储
整数一律采用补码的形式来存储
整数的原码,反码,补码相同。
负数的原码,反码,补码各不相同
- 原码
将一个整数转换成二进制形式,就是其原码。
short a = 6:a的原码就是 0000 0000 0000 0110
short a = 18:a 的原码就是1000 0000 0001 0010
- 反码
对于正数,原码和反码相同。
对于负数,反码是将原码中除符号位以外的所有位(数值位)取反,也就是 0 变成 1,1 变成 0。
short a = 6:a的反码就是 0000 0000 0000 0110
short a = 18:a 的反码就是1111 1111 1110 1101
- 补码
对于正数,原码和补码相同。
对于负数,补码是其反码加 1。
short a = 6:a的补码就是 0000 0000 0000 0110
short a = 18:a 的反码就是1111 1111 1110 1110
固定宽度类型
-
固定宽度类型表格
-
固定宽度类型打印
-
包含头文件:
#include <inttypes.h> -
拼接格式符:使用
PRI开头的宏。
-
类型修饰
const
核心概念与本质
- const 关键字的作用是什么?它定义的变量是真正的常量么?
const 是类型限定符,用于修饰变量,指针或函数参数,表示该对象是只读的。
-
不是真正的常量:严格来说,const修饰的是“只读变量”。它在内存中依然 占据空间(有地址),只是在编译器禁止了通过变量名进行的直接修改。
-
与宏的区别:相比
#define,const有数据类型,编译会进行 类型安全检查,且调试时可见(#define只是文本替换,无类型检查)。
- const 变量的存储位置在哪里?
这取决于变量的作用域和编译器实现:
-
局部
const变量:通常存储在 栈区,和普通局部变量一样,只是编译器限制了写操作。 -
全局
const变量:在现代编译器中,通常存储在只读数据段(.rodata)。如果尝试通过指针暴力修改,程序可能会崩溃(Segmentation Fault)。
指针与 const 的纠缠
建议记住口诀:“左定值,右定向”(即 const 在 * 左边修饰内容,在右边修饰指针本身)。
- 请解释
const int *p,int *const p和cosnt int *const p的区别
函数中的应用
- 为什么函数参数要使用
const修饰(如void func(const char *str))?
-
保护数据:明确告诉调用者和编译器,函数内部不会修改传入的数据,防止以外篡改,增强代码健壮性。
-
效率优化:当传递大型结构体或类对象时,使用
const引用(C++中)或const指针可以 避免深拷贝 带来的性能开销,同时保证数据安全。
- 函数返回值加
const有什么意义?
通常用于返回指针或引用。加上 const 是为了防止调用者利用返回值修改函数内部的私有数据(例如静态变量或成员变量)。
例子:const char* get_error_msg() 返回一个字符串字面量,调用者不应也不能修改这个字符串。
进阶与陷阱
- 如何绕过
const的限制修改变量?这样做安全么?
可以通过强制类型转换去除 const 属性。
const int val = 10;
int *p = (int *)&val; *// 强制转换*
*p = 20; *// 试图修改*
const** 和volatile可以同时使用吗?是什么意思?**
可以。const volatile int a; 这种写法常见于嵌入式开发。
-
含义:
-
const:告诉程序代码,我不能修改这个变量(只读)。 -
volatile:告诉编译器,这个变量可能会被外部因素(如硬件寄存器、中断服务程序)修改,不要做优化,每次必须从内存地址重新读取。
-
-
场景: 比如一个只读的状态寄存器,软件只能读,但硬件会自动更新它的值。
volatile
基础原理与本质
- 请简述
volatile关键字的作用?
volatile 是一个类型限定符,用于告诉编译器:“这个变量是不稳定的,随时可能被程序之外的因素修改。”
因此,编译器禁止对该变量进行任何优化。每次使用该变量时,必须直接从内存地址中读取其最新值,而不是使用寄存器中的缓存值;每次写入也必须立即写回内存。
- 如果不加
volatile,编译器通常会做哪些优化导致程序出错?
编译器为了提高效率,通常会进行以下两种优化,这在处理特殊变量时会导致灾难性后果:
-
寄存器缓存:编译器将变量值读入 CPU 寄存器后,后续访问直接用寄存器的值,不再去读内存。如果外部硬件修改了内存中的值,程序却还在用旧的寄存器值。
-
死代码消除/冗余读取:如果编译器发现两次读取之间没有修饰变量,它可能会优化掉第二次读取。
举例:在等待一个状态标志位变化的 while(!flag); 循环中,如果没有 volatile ,编译器可能认为 flag 在循环内没变,直接将其优化为死循环。
三大经典应用场景
- 你在什么场景下会使用
volatile?请举例说明。
| 场景 |
描述 | 代码/逻辑示例 |
|---|---|---|
| 访问硬件寄存器 | 在嵌入式中,外设寄存器(如 GPIO、串口数据寄存器) 的值由硬件自动更新,而非软件赋值。 |
#define STATUS_REG (*(volatile uint32_t *)0x40021000) 必须每次从该地址读取真实硬件状态。 |
| 中断服务函数 (ISR) | 全局变量在主程序中被检测,但在 ISR 中被修改。 主程序必须感知到 ISR 的修改。 |
主循环检测 flag,ISR 中置位 flag=1。若无 volatile, 主循环可能永远看不到变化。 |
| 多任务/多线程共享 | 多任务/多线程共享 | 任务 A 修改 run_flag,任务 B 根据该标志位决定是否退出循环。 |
多线程与并发陷阱
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)
它不是一个独立的数据类型,而是结构体的一种特殊成员。它允许你精确指定一个成员所占用的比特位数。
-
特点:用于精细控制内存,可以节省空间,但可移植性较差。
-
用途:在嵌入式开发或网络协议中,需要按位操作硬件或解析特定格式的数据包。