C 语言底层语法(共用体 - 枚举 - typedef - 位运算 - 内存管理)笔记
一、共用体
1.核心定义
共用体(也叫联合)是一种数据类型,它允许在同一块内存空间中存储不同类型的数据(但同一时间只能存储其中一种,不能同时存储多种)。
- 共用体的大小 = 其内部最大成员的大小(为了容纳最大的成员数据)。
- 核心特点:所有成员共享同一块内存,修改一个成员会覆盖其他成员的值。
2. 基本语法(定义、初始化、赋值)
#include <stdio.h>
// 1. 定义共用体
union Data {
char c; // 1字节
short s; // 2字节
int i; // 4字节(共用体大小最终为4字节)
};
int main() {
// 2. 初始化:只能初始化第一个成员,不能同时初始化多个
union Data d1 = {'a'}; // 合法,初始化第一个成员char c
union Data d2; // 定义未初始化的共用体变量
// 3. 赋值:同一时间只能有效存储一个成员的值
d2.i = 100;
printf("d2.i = %d\n", d2.i); // 输出:100
d2.c = 'b';
printf("d2.c = %c\n", d2.c); // 输出:b
printf("d2.i = %d\n", d2.i); // 输出:被覆盖,不再是100(具体值取决于字符'b'的ASCII码和内存布局)
return 0;
}
3.注意事项
- 内存共享:所有成员的起始内存地址相同,修改一个成员会破坏其他成员的数据。
- 初始化限制:只能对第一个成员进行初始化,无法同时给多个成员赋值。
- 不适合同时存储多类数据,仅适用于数据互斥使用的场景。
4. 核心应用:判断系统大小端
大小端概念
- 大端(Big Endian):高字节数据存储在低内存地址,低字节数据存储在高内存地址(符合人类阅读习惯)。
- 小端(Little Endian):低字节数据存储在低内存地址,高字节数据存储在高内存地址(大部分计算机系统采用)。
实现代码(利用共同体内存共享特性)
#include <stdio.h>
// 利用共用体判断大小端
union EndianCheck {
int num; // 4字节整数
char byte; // 1字节字符(共享num的起始内存地址)
};
int main() {
union EndianCheck ec;
ec.num = 1; // 将整数1存入共用体(二进制:0x00000001)
// 判断起始内存地址的字节值
if (ec.byte == 1) {
printf("小端模式\n");
} else {
printf("大端模式\n");
}
return 0;
}
原理说明
整数1的十六进制表示为0x00000001,如果是小端模式,低字节0x01会存在内存低地址(即ec.byte对应的地址),因此ec.byte == 1;大端模式下,高字节0x00存在低地址,因此ec.byte == 0。
二、枚举类型
1.核心定义
枚举类型是一种用户自定义的整数类型,用于将一组具有离散含义的常量(如状态、选项)命名,提高代码的可读性和可维护性。
- 枚举中的每个常量(枚举成员)都有一个默认的整数值,从
0开始依次递增。 - 枚举成员的值可以手动指定,未手动指定的成员会继承前一个成员的值并递增(每次增加1)。
2. 基本语法(定义、变量定义、使用)
#include <stdio.h>
// 1. 定义枚举类型
enum Weekday {
Mon, // 默认值 0
Tue, // 默认值 1
Wed, // 默认值 2
Thu = 5,// 手动指定 5
Fri, // 继承前一个,值 6
Sat, // 7
Sun // 8
};
int main() {
// 2. 定义枚举变量(两种方式)
enum Weekday today = Wed; // 方式1:直接使用枚举类型定义
enum Weekday tomorrow; // 方式2:先定义变量,后赋值
tomorrow = Thu;
// 3. 使用枚举变量(本质是整数,可以参与整数运算)
printf("今天是周%d\n", today); // 输出:今天是周2
printf("明天是周%d\n", tomorrow); // 输出:明天是周5
// 手动修改枚举变量的值(不推荐,违背枚举的设计初衷)
today = 10;
printf("修改后:今天是周%d\n", today); // 输出:修改后:今天是周10
return 0;
}
3. 关键注意事项
- 枚举成员是常量,不能在程序中修改其值(如
Mon = 10;是非法的)。 - 枚举变量的本质是整数,可赋值为任意整数(不仅限于枚举成员),但不推荐这样做,会失去枚举的可读性优势。
- 枚举类型的大小与
int类型一致(通常 4 字节)。
4. 与宏定义(#define)的核心区别
三、 typedef
1. 核心定义
typedef是 C 语言中的一个关键字,用于为已有的数据类型(内置类型、自定义类型)起一个新的别名,简化代码书写,提高代码的可移植性。
- 注意:
typedef不创建新的数据类型,只是给现有类型起了一个 “外号”。
2. 常见用法
用法 1:为内置数据类型起别名
#include <stdio.h>
// 为int起别名Int,为char起别名Char
typedef int Int;
typedef char Char;
int main() {
Int a = 100; // 等价于 int a = 100;
Char c = 'x'; // 等价于 char c = 'x';
printf("a = %d, c = %c\n", a, c); // 输出:a = 100, c = x
return 0;
}
用法 2:为自定义类型(结构体、共用体、枚举)起别名(核心场景)
#include <stdio.h>
// 1. 为结构体起别名
typedef struct Student {
char name[20];
int age;
} Stu; // Stu 是 struct Student 的别名
// 2. 为共用体起别名
typedef union Data {
int i;
char c;
} Dt; // Dt 是 union Data 的别名
// 3. 为枚举起别名
typedef enum Weekday {
Mon,
Tue,
Wed
} Wd; // Wd 是 enum Weekday 的别名
int main() {
// 直接使用别名定义变量,无需写 struct/union/enum
Stu s = {"张三", 18};
Dt d = {10};
Wd w = Tue;
printf("姓名:%s,年龄:%d\n", s.name, s.age); // 输出:姓名:张三,年龄:18
printf("d.i = %d\n", d.i); // 输出:d.i = 10
printf("w = %d\n", w); // 输出:w = 1
return 0;
}
用法 3:为指针类型起别名
#include <stdio.h>
#include <string.h>
// 为 char* 起别名 String
typedef char* String;
int main() {
String str = (char*)malloc(20 * sizeof(char));
strcpy(str, "Hello typedef");
printf("str = %s\n", str); // 输出:str = Hello typedef
free(str);
return 0;
}
3. 关键注意事项
typedef的别名具有作用域,在函数内部定义的别名仅在该函数内有效,全局定义的别名在整个文件有效。- 与
#define的区别:typedef是编译阶段处理,有类型检查;#define是预处理阶段文本替换,无类型检查。 - 为指针类型起别名时,注意优先级问题,避免混淆(如
typedef int* Ptr;,Ptr a, b;中a和b都是 int 指针)。
四、 位运算
1. 核心定义
位运算是直接对 二进制位(bit) 进行操作的运算方式,效率极高(直接操作硬件底层),常用于底层开发、数据加密、性能优化等场景。
- 操作数:通常为整数类型(char、short、int、long)。
- 核心运算符:
&(与)、|(或)、^(异或)、~(取反)、<<(左移)、>>(右移)。
2. 核心知识点与实现
知识点 1:异或(^)实现两个值交换(无需临时变量)
异或的核心特性
- 任何数与自身异或,结果为
0(a ^ a = 0)。 - 任何数与
0异或,结果为自身(a ^ 0 = a)。 - 异或满足交换律和结合律(
a ^ b ^ c = a ^ (b ^ c))。
实现代码
#include <stdio.h>
int main() {
int a = 10, b = 20;
printf("交换前:a = %d, b = %d\n", a, b); // 输出:交换前:a = 10, b = 20
// 异或交换(无临时变量)
a = a ^ b;
b = a ^ b; // 等价于 (a^b) ^ b = a ^ (b^b) = a ^ 0 = a
a = a ^ b; // 等价于 (a^b) ^ a = b ^ (a^a) = b ^ 0 = b
printf("交换后:a = %d, b = %d\n", a, b); // 输出:交换后:a = 20, b = 10
return 0;
}
注意事项
- 该方法仅适用于整数类型。
- 不能用于交换同一个变量(如
a ^ a会导致值变为0)。
知识点 2:左移(<<)与右移(>>)
左移(<<)
- 语法:
a << n(将a的二进制位向左移动n位,右侧补0)。 - 数学意义:无溢出情况下,
a << n等价于a * 2^n(效率远高于乘法运算)。 - 示例:
3 << 2(3 的二进制:00000011,左移 2 位后:00001100,对应十进制 12)。
右移(>>)
- 分为逻辑右移和算术右移:
- 逻辑右移:左侧补
0(适用于无符号整数)。 - 算术右移:左侧补符号位(适用于有符号整数,C 语言中默认采用算术右移)。
- 逻辑右移:左侧补
- 语法:
a >> n(将a的二进制位向右移动n位,左侧补对应值,右侧舍弃)。 - 数学意义:无溢出情况下,
a >> n等价于a / 2^n(效率远高于除法运算)。 - 示例:
12 >> 2(12 的二进制:00001100,右移 2 位后:00000011,对应十进制 3)。
代码示例
#include <stdio.h>
int main() {
int a = 3, b = 12;
printf("3 << 2 = %d\n", a << 2); // 输出:12
printf("12 >> 2 = %d\n", b >> 2); // 输出:3
// 有符号数右移(算术右移,补符号位)
int c = -4; // 二进制(简化为8位):11111100
printf("-4 >> 1 = %d\n", c >> 1); // 输出:-2(右移1位后:11111110)
return 0;
}
知识点 3:循环左移
- 核心定义:将左移时溢出的高位二进制位,补到右侧的低位(区别于普通左移的补 0)。
- 实现思路(以
int类型(32 位)为例,循环左移n位):- 保存
a的高位n位(通过a >> (32 - n)获取)。 - 将
a普通左移n位。 - 将两步的结果进行按位或(
|),得到循环左移结果。
- 保存
实现代码
#include <stdio.h>
// 循环左移函数:a为要移位的数,n为移位位数(32位int)
unsigned int cycle_left_shift(unsigned int a, int n) {
// 限制n在32以内,避免无效移位
n = n % 32;
return (a << n) | (a >> (32 - n));
}
int main() {
// 以8位二进制演示(实际是32位):00001111(15)循环左移2位 → 00111100(60)
unsigned int num = 15;
unsigned int result = cycle_left_shift(num, 2);
printf("15 循环左移 2 位结果:%d\n", result); // 输出:60
return 0;
}
五、 内存管理
1. 核心背景
C 语言中,内存分为栈区、堆区、全局 / 静态区、常量区、代码区,其中堆区的内存需要程序员手动申请和释放,这也是内存管理的核心内容。
- 栈区:自动分配释放(如局部变量、函数参数),生命周期随函数调用结束。
- 堆区:手动申请(
malloc/calloc/realloc)、手动释放(free),生命周期由程序员控制。 - 全局 / 静态区:存放全局变量、静态变量(
static),程序结束时由系统释放。 - 常量区:存放字符串常量、const 常量,程序结束时由系统释放,只读不可修改。
- 代码区:存放程序的二进制执行代码,只读。
2. 核心函数(堆区内存操作)
(1) 内存申请函数
-
malloc:申请指定大小的未初始化内存- 函数原型:
void* malloc(size_t size); - 参数:
size:要申请的内存大小(字节)。 - 返回值:成功返回指向申请内存的指针,失败返回
NULL。 - 注意:返回值是
void*,需要强制类型转换为所需指针类型;申请的内存内容是随机的(未初始化)。
- 函数原型:
-
calloc:申请指定数量、指定大小的初始化内存(初始值为 0)- 函数原型:
void* calloc(size_t num, size_t size); - 参数:
num:元素个数;size:每个元素的大小(字节)。 - 返回值:成功返回指向申请内存的指针,失败返回
NULL。 - 优势:自动初始化内存为 0,无需手动清零。
- 函数原型:
-
realloc:调整已申请的堆内存大小- 函数原型:
void* realloc(void* ptr, size_t size); - 参数:
ptr:已通过malloc/calloc申请的内存指针;size:调整后的新大小(字节)。 - 返回值:成功返回调整后内存的指针(可能是原地址,也可能是新地址),失败返回
NULL。 - 注意:如果
ptr为NULL,realloc等价于malloc;如果size为 0,realloc等价于free。
- 函数原型:
(2) 内存释放函数
free:释放已申请的堆内存- 函数原型:
void free(void* ptr); - 参数:
ptr:已通过malloc/calloc/realloc申请的内存指针。 - 注意:释放后,
ptr会变成 “野指针”,建议手动将其赋值为NULL,避免悬空访问。
- 函数原型:
#include <stdio.h>
#include <stdlib.h>
int main() {
// 1. malloc 申请内存(未初始化)
int* p1 = (int*)malloc(5 * sizeof(int));
if (p1 == NULL) { // 判空:避免内存申请失败导致后续操作异常
printf("malloc 内存申请失败\n");
return -1;
}
// 手动初始化
for (int i = 0; i < 5; i++) {
p1[i] = i + 1;
}
printf("malloc 申请的内存内容:");
for (int i = 0; i < 5; i++) {
printf("%d ", p1[i]); // 输出:1 2 3 4 5
}
printf("\n");
// 2. calloc 申请内存(自动初始化为0)
int* p2 = (int*)calloc(5, sizeof(int));
if (p2 == NULL) {
printf("calloc 内存申请失败\n");
return -1;
}
printf("calloc 申请的内存内容:");
for (int i = 0; i < 5; i++) {
printf("%d ", p2[i]); // 输出:0 0 0 0 0
}
printf("\n");
// 3. realloc 调整内存大小(将p1的内存从5个int调整为10个int)
int* p3 = (int*)realloc(p1, 10 * sizeof(int));
if (p3 == NULL) {
printf("realloc 内存调整失败\n");
free(p1); // 调整失败,释放原内存
p1 = NULL;
return -1;
}
p1 = p3; // 调整成功,更新指针
// 初始化新增的5个int
for (int i = 5; i < 10; i++) {
p1[i] = i + 1;
}
printf("realloc 调整后的内存内容:");
for (int i = 0; i < 10; i++) {
printf("%d ", p1[i]); // 输出:1 2 3 4 5 6 7 8 9 10
}
printf("\n");
// 4. free 释放内存
free(p1);
p1 = NULL; // 避免野指针
free(p2);
p2 = NULL;
return 0;
}
4. 内存管理注意事项(核心避坑点)
- 内存泄漏:申请的堆内存未释放,程序运行期间内存持续占用,最终可能导致内存耗尽。避免方式:申请内存后,确保在不再使用时调用
free释放。 - 野指针访问:释放内存后,指针未置为
NULL,后续误操作该指针导致程序崩溃。避免方式:free后立即将指针赋值为NULL。 - 重复释放:对同一块内存多次调用
free,会导致程序崩溃。避免方式:释放后将指针置为NULL(free(NULL)是安全的,不会报错)。 - 内存越界访问:访问的内存超出了申请的大小,会破坏其他内存数据,导致程序异常。避免方式:严格控制访问的索引范围,不超出申请的内存边界。
- 忽略内存申请失败:
malloc/calloc/realloc可能申请失败(返回NULL),未判空直接操作会导致程序崩溃。避免方式:申请内存后,先判断指针是否为NULL。
总结
- 共用体核心是内存共享,大小为最大成员大小,可高效判断系统大小端;枚举是有名字的整数常量集合,可读性优于零散宏定义。
typedef不创建新类型,仅为现有类型起别名,核心价值是简化自定义类型(结构体 / 共用体)的书写,提高代码可移植性。- 位运算直接操作二进制位,效率极高,异或可无临时变量交换整数,循环左移需保留溢出高位并补到低位。
- 内存管理核心是堆区的手动申请(
malloc/calloc/realloc)与释放(free),关键要避坑内存泄漏、野指针、重复释放等问题。 - 今日学习的内容均是 C 语言底层开发的基础,尤其是共用体、位运算、内存管理,在驱动开发、嵌入式开发中应用广泛。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)