一、共用体

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.注意事项

  1. 内存共享:所有成员的起始内存地址相同,修改一个成员会破坏其他成员的数据。
  2. 初始化限制只能对第一个成员进行初始化无法同时给多个成员赋值
  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. 关键注意事项

  1. 枚举成员是常量不能在程序中修改其值(如Mon = 10;是非法的)。
  2. 枚举变量的本质是整数,可赋值为任意整数(不仅限于枚举成员),但不推荐这样做,会失去枚举的可读性优势。
  3. 枚举类型的大小与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. 关键注意事项

  1. typedef的别名具有作用域,在函数内部定义的别名仅在该函数内有效,全局定义的别名在整个文件有效。
  2. #define的区别typedef是编译阶段处理,有类型检查#define是预处理阶段文本替换,无类型检查。
  3. 为指针类型起别名时,注意优先级问题,避免混淆(如typedef int* Ptr;Ptr a, b;ab都是 int 指针)。

四、 位运算

1. 核心定义

        位运算是直接对 二进制位(bit) 进行操作的运算方式,效率极高(直接操作硬件底层),常用于底层开发、数据加密、性能优化等场景。

  • 操作数:通常为整数类型(char、short、int、long)。
  • 核心运算符:&(与)、|(或)、^(异或)、~(取反)、<<(左移)、>>(右移)。

2. 核心知识点与实现

知识点 1:异或(^)实现两个值交换(无需临时变量)

异或的核心特性
  1. 任何数与自身异或,结果为0a ^ a = 0)。
  2. 任何数与0异或,结果为自身(a ^ 0 = a)。
  3. 异或满足交换律和结合律(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)。
右移(>>)
  • 分为逻辑右移算术右移
    1. 逻辑右移:左侧补0(适用于无符号整数)。
    2. 算术右移:左侧补符号位(适用于有符号整数,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位):
    1. 保存a的高位n位(通过a >> (32 - n)获取)。
    2. a普通左移n位。
    3. 将两步的结果进行按位或(|),得到循环左移结果。
实现代码
#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) 内存申请函数
  1. malloc申请指定大小的未初始化内存

    • 函数原型:void* malloc(size_t size);
    • 参数:size:要申请的内存大小(字节)。
    • 返回值:成功返回指向申请内存的指针,失败返回NULL
    • 注意:返回值是void*,需要强制类型转换为所需指针类型;申请的内存内容是随机的(未初始化)。
  2. calloc申请指定数量、指定大小的初始化内存(初始值为 0)

    • 函数原型:void* calloc(size_t num, size_t size);
    • 参数:num:元素个数;size:每个元素的大小(字节)。
    • 返回值:成功返回指向申请内存的指针,失败返回NULL
    • 优势:自动初始化内存为 0,无需手动清零。
  3. realloc调整已申请的堆内存大小

    • 函数原型:void* realloc(void* ptr, size_t size);
    • 参数:ptr:已通过malloc/calloc申请的内存指针;size:调整后的新大小(字节)。
    • 返回值:成功返回调整后内存的指针(可能是原地址,也可能是新地址),失败返回NULL
    • 注意:如果ptrNULLrealloc等价于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. 内存管理注意事项(核心避坑点)

  1. 内存泄漏:申请的堆内存未释放,程序运行期间内存持续占用,最终可能导致内存耗尽。避免方式:申请内存后,确保在不再使用时调用free释放。
  2. 野指针访问:释放内存后,指针未置为NULL,后续误操作该指针导致程序崩溃。避免方式:free后立即将指针赋值为NULL
  3. 重复释放:对同一块内存多次调用free,会导致程序崩溃。避免方式:释放后将指针置为NULLfree(NULL)是安全的,不会报错)。
  4. 内存越界访问:访问的内存超出了申请的大小,会破坏其他内存数据,导致程序异常。避免方式:严格控制访问的索引范围,不超出申请的内存边界。
  5. 忽略内存申请失败malloc/calloc/realloc可能申请失败(返回NULL),未判空直接操作会导致程序崩溃。避免方式:申请内存后,先判断指针是否为NULL

总结

  1. 共用体核心是内存共享,大小为最大成员大小,可高效判断系统大小端;枚举是有名字的整数常量集合,可读性优于零散宏定义。
  2. typedef不创建新类型,仅为现有类型起别名,核心价值是简化自定义类型(结构体 / 共用体)的书写,提高代码可移植性。
  3. 位运算直接操作二进制位,效率极高,异或可无临时变量交换整数,循环左移需保留溢出高位并补到低位。
  4. 内存管理核心是堆区的手动申请(malloc/calloc/realloc)与释放(free,关键要避坑内存泄漏、野指针、重复释放等问题。
  5. 今日学习的内容均是 C 语言底层开发的基础,尤其是共用体、位运算、内存管理,在驱动开发、嵌入式开发中应用广泛。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐