C高级:结构体、公共体、枚举、存储类型、函数指针、指针函数、函数指针数组

一、结构体(struct)—— 自定义复合数据类型

1. 核心本质

结构体是将不同类型的数据(如int、char、数组、指针)打包成一个整体的自定义类型,解决了“单一基本类型无法描述复杂对象”的问题(比如描述一个学生:学号、姓名、成绩)。

2. 完整语法(定义+变量+初始化+访问)

#include <stdio.h>
#include <string.h>

// 1. 定义结构体类型(描述"学生"这个复杂对象)
struct Student {
    // 成员(字段):不同类型的数据
    int id;         // 学号
    char name[20];  // 姓名
    float score;    // 成绩
};

int main() {
    // 2. 定义结构体变量的3种方式
    // 方式1:先定义类型,再定义变量
    struct Student s1;
    // 方式2:定义类型时直接定义变量
    struct Teacher {
        char name[20];
        int age;
    } t1;
    // 方式3:匿名结构体(只能定义一次变量,无法复用类型)
    struct {
        int x;
        int y;
    } point = {10, 20};

    // 3. 结构体初始化
    // 方式1:按成员顺序初始化
    struct Student s2 = {101, "张三", 95.5};
    // 方式2:指定成员初始化(C99标准,推荐,顺序无关)
    struct Student s3 = {
        .id = 102,
        .score = 88.0,
        .name = "李四"  // 注意:字符数组直接赋值仅初始化时有效
    };

    // 4. 结构体成员访问
    // 普通变量:用"."访问
    s1.id = 100;
    strcpy(s1.name, "王五");  // 字符数组赋值需用strcpy,不能直接s1.name = "王五"
    s1.score = 90.0;
    printf("学号:%d,姓名:%s,成绩:%.1f\n", s1.id, s1.name, s1.score);

    // 指针变量:用"->"访问(等价于(*p).成员)
    struct Student *p = &s2;
    printf("指针访问:%d %s %.1f\n", p->id, p->name, p->score);

    return 0;
}

3. 核心特性

  • 内存布局:结构体大小 = 各成员大小之和 + 内存对齐(编译器优化,避免跨字节访问);
    示例:struct { char c; int i; } 大小不是5,而是8(char占1字节,对齐到4字节,补3字节,int占4字节);
  • 成员作用域:仅属于结构体,不同结构体的同名成员互不影响;
  • 结构体赋值:可直接用=赋值(浅拷贝),比如s1 = s2;(注意:若含指针成员,浅拷贝会导致野指针)。

4. 典型用途

  • 描述复杂对象(学生、商品、坐标、网络报文);
  • 模块化封装数据(比如把函数参数打包成结构体,减少参数个数);
  • 链表、队列等数据结构的基础(结构体+指针实现)。

5. 避坑指南

  • 字符数组成员不能直接用=赋值(初始化除外),需用strcpy
  • 结构体指针访问成员用->,普通变量用.
  • 避免结构体嵌套过深(影响内存效率);
  • 含指针成员的结构体赋值时,需手动实现深拷贝(否则多个指针指向同一块内存,释放时重复free)。

二、共用体(union,也称公共体)—— 内存共享的“多面手”

1. 核心本质

共用体是自定义类型,但所有成员共享同一块内存空间,同一时间只有一个成员生效;内存大小等于最大成员的大小(满足对齐)。核心价值是“内存复用”。

2. 完整语法

#include <stdio.h>

// 1. 定义共用体类型
union Data {
    int i;      // 4字节
    float f;    // 4字节
    char c;     // 1字节
    char str[8];// 8字节(最大成员)
};

int main() {
    union Data d;
    // 2. 共用体大小:等于最大成员大小(8字节)
    printf("共用体大小:%zu 字节\n", sizeof(d));

    // 3. 成员赋值:覆盖式存储
    d.i = 0x12345678;  // 给int成员赋值
    printf("d.i = 0x%x\n", d.i);       // 输出0x12345678
    printf("d.c = 0x%x\n", d.c);       // 输出0x78(小端存储:低字节存低地址)
    printf("所有成员地址相同:%p == %p == %p\n", &d.i, &d.f, &d.str);

    d.f = 3.14f;       // 给float成员赋值,覆盖int的内存
    printf("d.f = %.2f\n", d.f);       // 输出3.14
    printf("d.i = %d\n", d.i);         // 输出随机值(float二进制转int)

    return 0;
}

3. 核心特性

  • 内存共享:所有成员起始地址相同,修改一个成员会覆盖其他成员;
  • 大小计算:仅由最大成员决定(比如union { char c; double d; } 大小为8);
  • 初始化:只能初始化第一个成员(比如union Data d = {10}; 等价于d.i = 10)。

4. 典型用途

  • 内存复用:嵌入式开发中节省内存(比如同一内存区,有时存整数,有时存浮点数);
  • 大小端判断(经典面试题):
// 判断CPU是小端(低字节存低地址)还是大端(高字节存低地址)
int isLittleEndian() {
    union {
        int i;
        char c;
    } u = {.i = 1}; // 0x00000001
    return u.c == 1; // 小端返回1,大端返回0
}
  • 二进制数据解析:比如把4字节int拆成4个char字节,用于网络传输/文件存储。

5. 避坑指南

  • 不要同时访问多个成员(除了用于类型转换),结果无意义;
  • 共用体不能存储需要同时生效的数据;
  • 含指针成员的共用体,需注意指针指向的内存生命周期。

三、枚举(enum)—— 语义化的常量集合

1. 核心本质

枚举是将一组有名字的整数常量组合成一个类型,本质是int(可指定底层类型),目的是“用语义化名字代替魔法数字”,让代码更易读、易维护。

2. 完整语法

#include <stdio.h>

// 1. 定义枚举类型(状态码:代替0/1/2等魔法数字)
enum Status {
    SUCCESS = 0,       // 手动指定值(默认从0开始,依次+1)
    ERROR,             // 自动为1
    FILE_NOT_FOUND = 2,// 手动指定,后续+1
    NETWORK_ERROR      // 自动为3
};

// 2. 定义枚举类型(星期:指定起始值)
enum Week {
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
};

int main() {
    // 3. 枚举变量定义与使用
    enum Status ret = FILE_NOT_FOUND;
    if (ret == FILE_NOT_FOUND) {
        printf("错误码:%d(文件不存在)\n", ret); // 输出2
    }

    enum Week today = WED;
    printf("今天是星期%d\n", today); // 输出3

    // 4. 枚举常量是只读的(不能修改)
    // SUCCESS = 10; // 编译报错:枚举常量是左值

    // 5. 枚举变量可赋值为整数(不推荐,破坏语义)
    today = 9; // 合法,但无意义
    printf("非法赋值:%d\n", today); // 输出9

    return 0;
}

3. 核心特性

  • 枚举常量:是编译期只读常量,值默认从0开始,可手动指定;
  • 底层类型:默认int,C11可指定(比如enum Color : char {RED, GREEN};);
  • 类型检查:比#define常量更安全(枚举有类型,#define只是文本替换)。

4. 典型用途

  • 定义状态码、错误码(如网络请求状态、函数返回值);
  • 定义有限可选值(颜色、方向、设备状态);
  • 替代#define(避免宏定义的命名污染)。

5. 避坑指南

  • 不要给枚举常量赋重复值(无意义,易混淆);
  • 避免枚举变量直接赋值整数(破坏语义,失去枚举的意义);
  • 枚举常量不能作为左值(不能修改)。

四、存储类型 —— 变量的“生命周期+作用域+存储位置”

存储类型决定了变量的作用域(可见范围)、生命周期(存在时间)、存储位置(栈/堆/全局区),C语言核心有4种存储类型:

存储类型

关键字

作用域

生命周期

存储位置

默认值

核心用途

自动存储

auto

局部(函数/代码块内)

函数/块执行时创建,结束销毁

随机值

普通局部变量(默认auto)

静态存储

static

局部/全局

程序启动到结束

全局区

0

局部持久化、

全局作用域限制

寄存器

存储

register

局部

(函数/代码块内)

函数/块执行时创建,结束销毁

寄存器/栈

随机值

高频访问变量提速

外部存储

extern

全局(跨文件)

程序启动到结束

全局区

0

跨文件访问全局变量/函数

1. auto(自动变量)

  • 关键字可省略(所有未指定存储类型的局部变量默认是auto);
  • 示例:
void test() {
    auto int a = 10; // 等价于int a = 10;
    int b = 20;      // 默认auto
} // a、b销毁,栈内存释放

2. static(静态变量)—— 重点!

(1)局部静态变量(函数/块内)
  • 作用域:仅在定义的函数/块内可见;
  • 生命周期:程序全程(只初始化一次,后续保留值);
  • 示例(统计函数调用次数):
#include <stdio.h>

void count() {
    static int num = 0; // 只初始化一次,函数结束后值保留
    num++;
    printf("调用次数:%d\n", num);
}

int main() {
    count(); // 1
    count(); // 2
    count(); // 3
    return 0;
}
(2)全局静态变量(函数外)
  • 作用域:仅在当前文件可见(限制作用域,避免跨文件命名冲突);
  • 示例:
// file1.c
static int g_val = 100; // 仅file1.c可见
void test() { g_val++; }

// file2.c
extern int g_val; // 编译报错:无法访问file1.c的static变量

3. register(寄存器变量)

  • 建议编译器将变量存入CPU寄存器(而非栈),提升访问速度;
  • 限制:不能取地址(&),编译器可忽略该关键字(比如寄存器满时);
  • 示例:
void calc() {
    register int i; // 高频循环变量,建议存寄存器
    for (i = 0; i < 1000000; i++) {
        // 减少内存访问,提速
    }
}

4. extern(外部变量/函数)

  • 作用:声明“变量/函数定义在其他文件”,用于跨文件访问;
  • 示例(跨文件访问):
// file1.c(定义)
int g_num = 200; // 全局变量
void func() { printf("file1: %d\n", g_num); }

// file2.c(声明+使用)
#include <stdio.h>
extern int g_num; // 声明:g_num定义在其他文件
extern void func(); // 声明函数

int main() {
    g_num = 300;
    func(); // 输出file1: 300
    return 0;
}

5. 避坑指南

  • static局部变量初始化语句仅执行一次(即使函数被多次调用);
  • extern是“声明”不是“定义”(不分配内存),定义变量时不能加extern(除非初始化);
  • register变量不能取地址(&register_var 编译报错);
  • 全局变量默认是extern(可跨文件访问),如需限制作用域,加static。

五、函数指针、指针函数、函数指针数组(指针进阶核心)

这三个概念是C高级的难点,核心是优先级规则:后缀(()、[])优先级高于前缀(*),记住:

  • int (*fp)():函数指针(fp是指针,指向返回int的函数);
  • int *func():指针函数(func是函数,返回int*指针);
  • int (*fp_arr[5])():函数指针数组(fp_arr是数组,元素是函数指针)。

1. 函数指针 —— 指向函数的指针

(1)核心本质

函数在内存中有入口地址,函数指针就是存储这个地址的变量,可通过指针调用函数(解耦函数调用,实现回调)。

(2)完整语法
#include <stdio.h>

// 1. 定义普通函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

// 2. 定义函数指针类型(typedef简化)
typedef int (*CalcFunc)(int, int); // CalcFunc是函数指针别名

int main() {
    // 3. 函数指针赋值(函数名就是函数地址)
    CalcFunc fp = add; // 等价于fp = &add;

    // 4. 函数指针调用(两种方式,推荐第一种)
    int res1 = fp(10, 5);    // 15
    int res2 = (*fp)(10, 5); // 15(等价,可读性差)
    printf("add: %d\n", res1);

    // 5. 切换函数(解耦核心)
    fp = sub;
    printf("sub: %d\n", fp(10, 5)); // 5

    return 0;
}
(3)典型用途:回调函数

回调函数是“通过函数指针传递给另一个函数的函数”,实现通用逻辑(比如qsort排序、事件处理):

#include <stdio.h>

// 通用计算器函数(接收函数指针,不关心具体计算逻辑)
int calc(int a, int b, CalcFunc fp) {
    return fp(a, b);
}

int main() {
    int a = 20, b = 8;
    // 加法回调
    printf("a+b = %d\n", calc(a, b, add));
    // 减法回调
    printf("a-b = %d\n", calc(a, b, sub));
    return 0;
}

2. 指针函数 —— 返回指针的函数

(1)核心本质

指针函数是普通函数,只是返回值类型是指针(int*、char*、结构体指针等),重点是“函数”,不是“指针”。

(2)完整语法
#include <stdio.h>
#include <string.h>

// 1. 返回字符串指针的函数(根据状态返回描述)
char* getStatusDesc(enum Status s) {
    switch (s) {
        case SUCCESS: return "成功";
        case ERROR: return "通用错误";
        case FILE_NOT_FOUND: return "文件不存在";
        default: return "未知错误";
    }
}

// 2. 返回数组指针的函数(注意:不能返回局部数组指针)
int* getArray(int size) {
    static int arr[100]; // 静态数组,生命周期全程
    for (int i = 0; i < size; i++) {
        arr[i] = i * 3;
    }
    return arr; // 返回数组首地址
}

int main() {
    enum Status ret = ERROR;
    printf("状态描述:%s\n", getStatusDesc(ret)); // 通用错误

    int* arr = getArray(5);
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]); // 0 3 6 9 12
    }

    return 0;
}
(3)避坑指南
  • 禁止返回局部变量的指针:局部变量存在栈,函数结束后内存释放,指针变为野指针;
  • 可返回:静态变量、全局变量、堆内存(malloc分配)的指针;
  • 返回的指针需确保指向的内存未被释放(比如堆内存需手动free)。

3. 函数指针数组 —— 存储函数指针的数组

(1)核心本质

函数指针数组是数组,每个元素都是同类型的函数指针,用于批量管理函数(比如菜单驱动、命令解析)。

(2)完整语法(菜单驱动示例)
#include <stdio.h>

// 1. 定义功能函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return b != 0 ? a / b : 0; }

// 2. 定义函数指针数组(存储所有功能函数)
CalcFunc func_arr[] = {add, sub, mul, div};
// 数组大小:sizeof(数组)/sizeof(元素)
int func_count = sizeof(func_arr) / sizeof(CalcFunc);

// 3. 菜单显示
void showMenu() {
    printf("===== 计算器 =====\n");
    printf("1. 加法\n2. 减法\n3. 乘法\n4. 除法\n0. 退出\n");
    printf("==================\n");
}

int main() {
    int choice, a, b;
    while (1) {
        showMenu();
        printf("请选择:");
        scanf("%d", &choice);
        if (choice == 0) break;
        if (choice < 1 || choice > func_count) {
            printf("无效选择!\n");
            continue;
        }

        printf("输入两个数:");
        scanf("%d %d", &a, &b);
        // 4. 通过数组调用对应函数(choice-1是数组下标)
        int res = func_arr[choice-1](a, b);
        printf("结果:%d\n", res);
    }
    printf("退出程序!\n");
    return 0;
}
(3)典型用途
  • 菜单驱动程序(如计算器、终端命令);
  • 回调函数列表(如中断处理函数、事件响应函数);
  • 动态函数调度(根据配置选择不同函数执行)。

4. 避坑指南

  • 函数指针数组的元素必须是“同参数、同返回值”的函数;
  • 数组下标从0开始,需注意菜单选项与下标的对应(如choice=1对应下标0);
  • typedef简化函数指针语法后,代码可读性大幅提升,推荐优先使用。
#include <stdio.h>
#define DEF
 
int main(int argc, const char *argv[])
{
#ifdef DEF          //如果DEF定义则编译下面代码,否则编译#else下面代码
    printf("hello\n");
#else
    printf("world\n");
#endif
 
    return 0;
}

六、条件编译

按照条件是否满足决定代码是否被编译,是预处理指令。

简单来说就是后面的条件语句(condition)如果执行结果不为0,则该#if语句块内的代码会被编译,否则就不会被编译。

1. 根据宏是否定义

#define 宏名

#ifdef 宏名

代码块1

#else

代码2

#endif

执行逻辑:宏名如果定义则编译代码块1,否则编译代码块。

#include <stdio.h>
#define DEF
 
int main(int argc, const char *argv[])
{
#ifdef DEF          //如果DEF定义则编译下面代码,否则编译#else下面代码
    printf("hello\n");
#else
    printf("world\n");
#endif
 
    return 0;
}

2. 根据宏值

#define 宏名 宏值

#if 宏名

代码块1

#else

代码块2

#endif

执行逻辑:宏值为非0则编译代码块1,否则为0的话则编译代码块2。

#include <stdio.h>
#define DEF 1
 
int main(int argc, const char *argv[])
{
#if DEF          
    printf("hello\n");
#else
    printf("world\n");
#endif
 
    return 0;
}

3. 防止头文件重复包含

放在头文件中:

#ifndef 宏名

#define 宏名

头文件中的代码

#endif

七、make工具

1. 定义

make: 工程管理器,顾名思义,是指管理较多的文件。

make: 工程管理器也就是个"自动编译管理器",这里的"自动"是指它能够根据文件时间戳自动发现更新过的文件而减少编译的工作量,同时,它通过读入"Makefile"文件的内容来执行大量的编译工作。

Makefile或makefile是make读取的唯一配置文件。

2. Makefile 格式

目标:依赖

TAB命令

注意: 命令前面要用TAB键

写一个test.c文件,再写一个makefile文件管理它

test:test.o
	gcc test.o -o test
test.o:test.c
	gcc -c test.c -o test.o

目标加: 是伪目标

伪目标:它的目的并不是创建目标文件(所以称作"伪"),而是想区执行这个目标下面的命令。

执行:make clean

规则中 rm 命令不是为了创建 clean 这个文件,而是执行删除某些文件的任务。当工作目录中不存在以 clean 命令的文件时,输入 make clean 命令,命令 rm -rf test*.o 总会被执行, 这也是我们期望的结果。

如果避免同名文件可以加:.PHONY:clean

3. 用make管理多个文件

先建立多个文件:

写一个makefile管理:

执行:make

清除中间文件:make clean

.PHONY:clean 为了避免路径中出现同名文件

4. makefile变量

4.1. 自定义变量

自己定义的变量:一般用大小表示变量名, 取变量的值用$(变量名)

给变量赋值:

= 递归方式展开

:= 直接赋值(当前的值是什么就立即赋值)

+= 追加新的值

?= 判断之前是否定义,如果定义不重新赋值,否则赋值

4.2. 预定义变量

系统预先定义好的一些变量,可能有默认的值也可能没有默认值需要自己定义

RM 文件删除程序的名称,默认值伪rm -f

CC C编译器的名称,默认值为cc

CPP C预编译器的名称,默认值为$(CC) -E

CFLAGS 编译器的选项,无默认值

OBJS 生成的二进制文件或目标文件,自己定义

4.3. 自动变量

$< 第一个依赖文件的名称

$^ 所有不重复的依赖文件,以空格隔开

$@ 目标文件的完整名称

目标:依赖

可以用%.c和%.o代替每一个.c和.o文件:相当于让每个.c文件生成各自的.o

补充: make指令

make -s :隐藏执行的指令

make -C 路径:进入指定路径指向make指令

Logo

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

更多推荐