加载中...


宏

宏(Macro)是C语言中的一种预处理指令,简单说就是代码的自动替换工具。用最直白的话讲:

宏就是给一段代码起个名字,编译前把名字替换成对应的代码。

宏的本质:编译前的”查找替换”

宏是在编译之前(预处理阶段)由预处理器处理的。它不参与编译,也不占用运行时资源。
基本语法:

#define 宏名 替换内容

最简单的例子:

#define PI 3.14159

float area = PI * 5 * 5;

预处理后,代码变成:

float area = 3.14159 * 5 * 5;

PI被原封不动替换成了3.14159。

宏的两种主要类型

对象宏(Object-like Macro)—— 用于常量

#define MAX_SIZE 100
#define FILE_PATH "/usr/local/data"
#define DEBUG 1

用途:给常量起个有意义的名字,方便统一修改。
好处:如果MAX_SIZE要从100改成200,只需要改宏定义处,所有用到的地方自动更新。

函数宏(Function-like Macro)—— 用于简单计算

#define SQUARE(x) ((x) * (x))
#define MAX(a,b) ((a) > (b) ? (a) : (b))

int y = SQUARE(5);      // 变成 int y = ((5) * (5));
int z = MAX(3, 8);      // 变成 int z = ((3) > (8) ? (3) : (8));

看起来很像个函数,但本质还是替换

宏vs函数:关键区别

对比项 宏 函数
处理时机 编译前(预处理阶段) 运行时(调用时)
性能 无函数调用开销 有压栈、跳转开销
类型检查 无(纯文本替换) 有严格类型检查
代码体积 每用一次就复制一份,代码膨胀 只有一份代码
调试 难以调试(替换后代码与源码不同) 可正常调试
作用域 全局(直到#undef) 局部(函数内)

新手最容易踩的坑

坑1:宏参数不加括号

#define SQUARE(x) x * x

int y = SQUARE(3 + 2);  // 期望 25
// 实际替换:3 + 2 * 3 + 2 = 3 + 6 + 2 = 11

正确写法:

#define SQUARE(x) ((x) * (x))

坑2:宏展开后的副作用

#define MAX(a,b) ((a) > (b) ? (a) : (b))

int x = 5;
int y = MAX(x++, 10);  // 期望x变成6
// 实际替换:((x++) > (10) ? (x++) : (10))
// x被自增了两次!最后x = 7

解决方案:这种场景用函数,别用宏。

坑3:多语句宏不加do-while

#define SWAP(a,b) { int temp = a; a = b; b = temp; }

if (flag)
    SWAP(x, y);  // 这里没问题
else
    // ...

但如果这样写:

if (flag)
    SWAP(x, y);  // 展开成{ int temp = x; x = y; y = temp; };
    // 多了一个分号,后面的else匹配出错!

正确写法:

#define SWAP(a,b) do { int temp = a; a = b; b = temp; } while(0)

宏的高级用法

条件编译(最实用)

#define DEBUG 1

#ifdef DEBUG
    printf("变量值:%d\n", x);  // 只在调试时输出
#endif
#ifndef HEADER_H
#define HEADER_H
// 头文件内容(防止重复包含)
#endif

字符串化(#)

#define STR(x) #x
printf(STR(hello));  // 输出"hello"

连接(##)

#define CONCAT(a,b) a##b
int xy = 10;
printf("%d\n", CONCAT(x,y));  // 输出10(变成变量名xy)

预定义宏

printf("文件名:%s\n", __FILE__);   // 当前文件名
printf("行号:%d\n", __LINE__);     // 当前行号
printf("编译日期:%s\n", __DATE__); // 编译日期
printf("编译时间:%s\n", __TIME__); // 编译时间

什么时候用宏?什么时候不用?

适合用宏:

  • 简单常量(#define PI 3.14159)
  • 简单计算(#define ABS(x) ((x) < 0 ? -(x) : (x)))
  • 条件编译(调试开关、跨平台适配)
  • 日志打印的包装

不适合用宏(用函数替代):

  • 复杂逻辑(多行代码)
  • 需要类型安全检查的场景
  • 需要调试的场景
  • 参数有副作用(如x++)

现代C语言的趋势

  • 常量: 用const替代#define(有类型检查)
  • 函数: 用inline函数替代函数宏(有类型检查,无调用开销)
  • 枚举: 用enum替代一组相关的宏

但条件编译和头文件防护仍然是宏的不可替代领域。

C语言的宏和Java的全局变量什么区别

C语言的宏和Java的全局变量,虽然在某些场景下都能起到“在多个地方共享某个值或逻辑”的作用,但它们的本质、工作机制和使用方式差别非常大。下面从几个维度来对比。

本质是什么

C宏:是预处理阶段的文本替换工具。它由预处理器(preprocessor)在编译之前处理,本质上是“把一段文本原样替换到使用的位置”。宏不是变量,也不是函数,它没有类型、没有作用域、不占运行时的内存。

#define MAX 100
#define SQUARE(x) ((x)*(x))

int a = MAX;        // 预处理后变成 int a = 100;
int b = SQUARE(3);  // 预处理后变成 int b = ((3)*(3));

Java全局变量:通常指类中的static成员变量(静态变量)。它是真正的变量,有类型、有作用域、在运行时存在于内存中,属于类而不是某个对象。

public class Config {
    public static int MAX = 100;
}
int a = Config.MAX;  // 运行时读取静态变量的值

处理时机不同

C宏 Java静态变量
处理阶段 编译前(预处理) 编译期确定符号,运行期分配/访问
是否有类型 无 有明确类型
是否占内存 不占(只是文本) 占(存在方法区/元空间)
是否可调试 很难(已被替换掉) 可以正常调试

作用域与可见性

  • C宏:没有真正的作用域概念。一旦#define,从定义点往后整个翻译单元都有效,直到#undef。它不受花括号、函数、文件边界(除非用#undef或头文件管理)限制,容易造成命名污染。
  • Java静态变量:有严格的访问控制(public/private/protected/包访问)和类作用域。通过类名访问,命名空间清晰,不会随意污染。

类型安全与副作用

C宏最大的坑是没有类型检查,且参数可能被多次求值:

#define SQUARE(x) ((x)*(x))
int i = 3;
int r = SQUARE(i++);  // 展开为((i++)*(i++)),i被自增两次,结果未定义

Java静态变量有完整类型检查,作为变量只求值一次,不存在这种文本替换带来的副作用。

能否被修改

  • C宏:定义后是固定文本,不能像变量那样在运行时改值。想改只能重新#define(通常不这么做)。
  • Java静态变量:如果不是final,可以在运行时被修改;如果是static final,则是常量,但仍是一个有类型的变量,且编译器可能做常量折叠。

常见用途对应关系

用途 C宏 Java对应做法
定义常量 #define MAX 100 static final int MAX = 100;
定义“函数” 函数式宏 #define SQUARE(x) ... 普通方法 / static 方法
条件编译 #ifdef DEBUG 没有直接对应,通常用配置/日志级别
代码片段复用 宏展开 方法、泛型、继承等

总结

  • C宏是编译前的文本替换,无类型、无作用域、不占内存,灵活但危险。
  • Java全局变量(静态变量)是运行时的真实变量,有类型、有访问控制、占内存,安全但受语言规则约束。

所以它们只是在“给某个东西起个名字,多处使用”这一点上看起来相似,底层机制和工程影响完全不同。Java中更接近C宏“常量”角色的,其实是static final常量;而C宏的“函数式宏”在Java里根本没有等价物,只能用方法来替代。

指针

什么是指针

指针就是一个变量,它的值是另一个变量的内存地址。

普通变量存的是“数据本身”,指针变量存的是“数据在哪里”。

int a = 10;      // a存的是10
int *p = &a;     // p存的是a的地址

内存示意:

变量名   地址        值
a        0x1000     10
p        0x2000     0x1000   ← p的值是a的地址
  • &a:取地址运算符,得到a的地址。
  • *p:解引用运算符,得到p指向的那个变量的值。
  • int *p:声明p是一个“指向int的指针”。

两个核心运算符

运算符 名称 作用
& 取地址 得到变量的内存地址
* 解引用 访问指针指向的内存
int a = 10;
int *p = &a;

printf("%p\n", (void*)p);  // 输出a的地址,如0x7ffd...
printf("%d\n", *p);        // 输出10

*p = 20;                   // 通过指针修改a
printf("%d\n", a);         // 输出20

注意:*在声明和表达式里含义不同:

  • int *p;里的*表示“p是指针”。
  • *p = 20;里的*表示“解引用”。

掌握指针的关键是多画内存图,清楚每个指针指向哪里、生命周期多长、谁负责释放。

核心概念:变量就是盒子

int a = 10;

在内存里:

地址:0x1000
+------+
|  10  |  ←这个盒子叫a,里面装着10
+------+

&(取地址符)

问”盒子在哪?”

int a = 10;
int *p = &a;   // p存储了a的地址(0x1000)
地址:0x1000         地址:0x2000
+------+            +----------+
|  10  |  ← a       | 0x1000   |  ← p(指着a)
+------+            +----------+

*(解引用符)

去”盒子”里取/放东西

printf("%d\n", *p);   // 去p指向的盒子取东西→输出10
*p = 20;              // 去p指向的盒子放东西→a变成20
printf("%d\n", a);    // 输出20

声明时的*vs使用时的*

这是初学者最混淆的地方:

int *p;      // 声明:*表示"p是指针"
*p = 10;     // 使用:*表示"去p指向的位置"

记忆口诀:

  • & = 的地址(&a读作”a的地址”)
  • * = 指向的内容(*p读作”p指向的内容”)

实战示例

#include <stdio.h>

int main() {
    int a = 10;
    int *p = &a;        // p指向a
    
    printf("a 的值:%d\n", a);        // 10
    printf("a 的地址:%p\n", &a);     // 0x7ffc...
    printf("p 的值(地址):%p\n", p); // 0x7ffc...(同&a)
    printf("p 指向的值:%d\n", *p);   // 10
    
    *p = 20;            // 通过指针修改a
    printf("修改后 a:%d\n", a);      // 20
    
    return 0;
}

指针的类型

指针是有类型的,类型决定了:

  1. 指针指向的数据是什么类型。
  2. 解引用时读多少字节。
  3. 指针算术运算的步长。
int    *p1;   // 指向int
char   *p2;   // 指向char
double *p3;   // 指向double
int   **p4;   // 指向“指向int的指针”,即二级指针

在64位系统上,所有指针本身通常都是8字节,但指向的类型不同,行为不同。

int a = 0x12345678;
int *p = &a;
char *c = (char*)&a;

printf("%d\n", *p);   // 0x12345678
printf("%x\n", *c);   // 取决于字节序,小端下是0x78

指针的大小

指针的大小与平台和架构有关,与指向类型无关:

平台 指针大小
32 位 4字节
64 位 8字节
printf("%zu\n", sizeof(int*));    // 64位下通常是8
printf("%zu\n", sizeof(char*));   // 也是8
printf("%zu\n", sizeof(double*)); // 也是8

指针的初始化与野指针

野指针:指向未知或非法内存的指针,使用它会导致未定义行为。

int *p;        // 未初始化,野指针
*p = 10;       // 危险!可能崩溃

正确做法:

int *p = NULL; // 初始化为空指针
if (p != NULL) {
    *p = 10;
}

常见错误来源:

  • 未初始化的指针。
  • 指向已释放内存的指针(悬空指针)。
  • 指向局部变量但变量已销毁。
int *bad() {
    int x = 10;
    return &x;   // 错误!x是局部变量,函数返回后失效
}

指针与数组

数组名在大多数表达式中会退化为指向首元素的指针。

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;      // 等价于&arr[0]

printf("%d\n", *p);       // 1
printf("%d\n", *(p+1));   // 2
printf("%d\n", p[2]);     // 3

arr[i]本质上等价于*(arr + i),p[i]等价于*(p + i)。

区别:

  • sizeof(arr)得到整个数组大小(如20字节)。
  • sizeof(p)得到指针大小(如8字节)。
  • 数组名不是可修改的左值,arr++非法;指针可以p++。

关键结论:数组名就是指向第一个元素的指针(常量指针)。

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;    // arr等价于&arr[0]

内存布局:

arr[0]  arr[1]  arr[2]  arr[3]  arr[4]
+-------+-------+-------+-------+-------+
|  10   |  20   |  30   |  40   |  50   |
+-------+-------+-------+-------+-------+
↑
arr(指向第一个元素)

数组名就是指针(但有区别)

int arr[5] = {1,2,3,4,5};
int *p = arr;

// 完全等价的操作
arr[0] == p[0] == *arr == *p    // 都是1
arr[1] == p[1] == *(arr+1) == *(p+1)  // 都是2

数组名vs指针的区别

特性 数组名arr 指针p
本质 地址常量 变量
能否赋值 不能arr = NULL 可以p = NULL
大小 sizeof(arr) = 整个数组大小 sizeof(p) = 指针大小(8字节)
指向 固定指向数组首元素 可以指向任何地方
int arr[5] = {1,2,3,4,5};
int *p = arr;

printf("%zu\n", sizeof(arr));  // 20(5 * 4字节)
printf("%zu\n", sizeof(p));    // 8(64位系统指针大小)

// arr = NULL;   // ❌ 编译错误
p = NULL;          // ✅ 可以

指针算术:p+1跳多远?

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;

printf("%p\n", p);     // 0x1000
printf("%p\n", p+1);   // 0x1004(跳过了4字节,因为int占4字节)

公式:p + n = 地址 + n × sizeof(类型)

二维数组与指针

int matrix[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

内存中连续存放:

1  2  3  4  5  6

访问方式:

matrix[1][2] == *(*(matrix+1) + 2)  // 都是6

指针算术

指针加减的步长是“指向类型的大小”。

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;

p++;          // 地址增加sizeof(int),通常4字节
printf("%d\n", *p);  // 20

p += 2;       // 再前进2个int
printf("%d\n", *p);  // 40

int *q = arr + 4;
printf("%ld\n", q - p);  // 指针相减,得到元素个数

合法运算:

  • 指针 ± 整数
  • 指针 - 指针(同一数组内)
  • 指针比较(同一数组内)

非法或未定义:

  • 两个无关指针相减。
  • 指针相加。
  • 越界访问。

指针与字符串

C 中没有独立的字符串类型,字符串就是以'\0'结尾的字符数组。

char s1[] = "hello";      // 可修改的字符数组
char *s2 = "hello";       // 指向字符串字面量,通常不可修改
const char *s3 = "hello"; // 推荐写法
s1[0] = 'H';   // 合法
// s2[0] = 'H'; // 未定义行为,字符串字面量通常在只读区

字符串遍历:

char *p = s1;
while (*p != '\0') {
    putchar(*p);
    p++;
}

指针与函数

指针作为参数

C是值传递,想在被调函数中修改调用者的变量,必须传指针。

void swap(int *a, int *b) {
    int t = *a;
    *a = *b;
    *b = t;
}

int x = 1, y = 2;
swap(&x, &y);   // x=2, y=1

指针作为返回值

int* find_max(int *arr, int n) {
    int *max = arr;
    for (int i = 1; i < n; i++) {
        if (arr[i] > *max) max = &arr[i];
    }
    return max;
}

注意不要返回局部变量的地址。

函数指针

函数也有地址,可以赋给函数指针。

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int (*op)(int, int) = add;
printf("%d\n", op(3, 4));   // 7

op = sub;
printf("%d\n", op(3, 4));   // -1

函数指针常用于回调、策略表:

int (*ops[])(int, int) = {add, sub};
printf("%d\n", ops[0](5, 2));  // 7

把函数当作变量来用,可以赋值、传参、存储。

基本语法

// 声明一个函数指针:指向返回int,参数为(int,int)的函数
int (*func_ptr)(int, int);

// 赋值
int add(int a, int b) { return a + b; }
func_ptr = add;  // 或&add

// 调用
int result = func_ptr(3, 5);  // 等价于add(3,5)

作为回调函数(最常用)

#include <stdio.h>

// 操作函数
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }

// 计算器:接受函数指针作为参数
int calculate(int a, int b, int (*operation)(int, int)) {
    return operation(a, b);
}

int main() {
    printf("%d\n", calculate(10, 5, add));        // 15
    printf("%d\n", calculate(10, 5, subtract));   // 5
    printf("%d\n", calculate(10, 5, multiply));   // 50
    return 0;
}

函数指针数组(实现分支逻辑)

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) { return b ? a / b : 0; }

int main() {
    // 函数指针数组
    int (*ops[4])(int, int) = {add, subtract, multiply, divide};
    
    int a = 10, b = 5;
    for (int i = 0; i < 4; i++) {
        printf("%d\n", ops[i](a, b));
    }
    // 输出:15,5,50,2
    
    return 0;
}

作为结构体成员(模拟面向对象)

typedef struct {
    int x;
    int y;
    int (*add)(struct Point*);  // 成员函数指针
} Point;

int add_point(Point *p) {
    return p->x + p->y;
}

int main() {
    Point p = {10, 20, add_point};
    printf("%d\n", p.add(&p));  // 30
    return 0;
}

typedef简化函数指针

typedef int (*Operation)(int, int);  // 定义类型别名

Operation op = add;  // 简洁多了

二级指针

指向指针的指针。

int a = 10;
int *p = &a;
int **pp = &p;

printf("%d\n", **pp);   // 10
**pp = 20;
printf("%d\n", a);      // 20

常见用途:

  • 在函数中修改指针本身。
void alloc(int **p) {
    *p = (int*)malloc(sizeof(int));
    **p = 100;
}

int *q = NULL;
alloc(&q);
printf("%d\n", *q);  // 100
free(q);
  • 动态二维数组。

指针与动态内存

int *p = (int*)malloc(sizeof(int) * 10);  // 分配10个int
if (p == NULL) {
    // 处理分配失败
}

for (int i = 0; i < 10; i++) {
    p[i] = i;
}

free(p);      // 释放
p = NULL;     // 避免悬空指针

相关函数:

函数 作用
malloc 分配未初始化内存
calloc 分配并清零
realloc 调整已分配内存大小
free 释放内存

常见错误:

  • 忘记free → 内存泄漏。
  • free后继续使用 → 悬空指针。
  • 重复free→ 未定义行为。
  • free非malloc返回的指针 → 未定义行为。

const与指针

const 和指针结合有几种情况,容易混淆:

const int *p1;        // 指向常量的指针:不能通过p1改值,p1本身可改
int const *p2;        // 同上
int * const p3 = &a;  // 常量指针:p3本身不能改,但可通过p3改值
const int * const p4 = &a; // 两者都不能改

记忆方法:看const在*的左边还是右边。

  • const在*左边:指向的内容不可改。
  • const在*右边:指针本身不可改。

void指针

void* 是通用指针,可以指向任意类型,但不能直接解引用。

int a = 10;
void *p = &a;

// printf("%d\n", *p);  // 错误
printf("%d\n", *(int*)p);  // 需要先转换类型

常见用途:

  • malloc返回void*。
  • 通用函数参数,如qsort、memcpy。
void* memcpy(void *dest, const void *src, size_t n);

指针数组vs数组指针

这两个容易混淆:

int *a[10];    // 指针数组:a是数组,含10个int*
int (*b)[10];  // 数组指针:b是指针,指向含10个int的数组

记忆:看括号和优先级。[]优先级高于*。

  • int *a[10]:先a[10],说明a是数组,元素是int*。
  • int (*b)[10]:先*b,说明b是指针,指向int[10]。

数组指针常用于二维数组:

int arr[3][4];
int (*p)[4] = arr;   // p指向一行,每行4个int
printf("%d\n", p[1][2]);  // 访问arr[1][2]

常见陷阱

指针未初始化

int *p;
*p = 10;  // 未定义行为

数组越界

int arr[5];
int *p = arr;
p[10] = 1;  // 越界,可能破坏其他内存

返回局部变量地址

int* f() {
    int x = 10;
    return &x;  // 错误
}

忘记free

while (1) {
    int *p = malloc(1000);
    // 没有free,内存持续泄漏
}

指针类型不匹配

int a = 10;
char *p = (char*)&a;  // 可以,但解释方式不同

混淆*p++、(*p)++、*++p

int arr[] = {1, 2, 3};
int *p = arr;

*p++;    // 取*p,然后p自增
(*p)++;  // p指向的值自增
*++p;    // p先自增,再取值

指针的用途总结

用途 说明
动态内存 malloc/free 操作堆内存
函数修改外部变量 传指针实现“引用传递”
数组和字符串操作 高效遍历、传递
函数指针 回调、策略、表驱动
数据结构 链表、树、图等
系统编程 直接访问硬件、内存映射
通用接口 void*、qsort、memcpy

一句话理解

指针就是地址,解引用就是访问那个地址上的数据。

它的强大在于:

  • 能直接操作内存,效率高。
  • 能实现灵活的数据结构和函数回调。
  • 是C与底层硬件之间的桥梁。

它的危险也在于:

  • 没有边界检查,没有自动回收。
  • 一个错误就可能崩溃或产生安全漏洞。

所以用指针的核心原则是:始终清楚它指向哪里、是否有效、谁负责释放。

一个综合例子:链表

#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int data;
    struct Node *next;
} Node;

Node* create(int data) {
    Node *n = (Node*)malloc(sizeof(Node));
    if (!n) return NULL;
    n->data = data;
    n->next = NULL;
    return n;
}

void push_front(Node **head, int data) {
    Node *n = create(data);
    n->next = *head;
    *head = n;
}

void print(Node *head) {
    while (head) {
        printf("%d -> ", head->data);
        head = head->next;
    }
    printf("NULL\n");
}

void free_list(Node *head) {
    while (head) {
        Node *tmp = head;
        head = head->next;
        free(tmp);
    }
}

int main() {
    Node *head = NULL;
    push_front(&head, 3);
    push_front(&head, 2);
    push_front(&head, 1);
    print(head);      // 1 -> 2 -> 3 -> NULL
    free_list(head);
    return 0;
}

这个例子用到了:结构体指针、二级指针、动态内存、指针遍历、内存释放。

结构体(struct)

把不同类型的数据打包成一个整体。

定义和使用

// 定义结构体类型
struct Student {
    char name[50];
    int age;
    float score;
};

int main() {
    // 声明变量
    struct Student s1;
    
    // 赋值(用.操作符)
    strcpy(s1.name, "Alice");
    s1.age = 20;
    s1.score = 95.5;
    
    // 访问
    printf("%s, %d, %.1f\n", s1.name, s1.age, s1.score);
    
    // 初始化
    struct Student s2 = {"Bob", 22, 88.0};
    
    // 结构体赋值(拷贝)
    struct Student s3 = s2;  // 复制了一份
    
    return 0;
}

结构体指针(->操作符)

struct Student s1 = {"Alice", 20, 95.5};
struct Student *p = &s1;

// 两种方式等价
printf("%s\n", (*p).name);   // 麻烦
printf("%s\n", p->name);     // 推荐(箭头操作符)

p->age = 21;  // 通过指针修改

typedef:起别名

typedef struct Student {
    char name[50];
    int age;
    float score;
} Student;  // 现在Student就是类型名了

// 不用再写struct前缀
Student s1 = {"Alice", 20, 95.5};

结构体嵌套

struct Date {
    int year;
    int month;
    int day;
};

struct Student {
    char name[50];
    struct Date birthday;  // 嵌套
    float score;
};

int main() {
    struct Student s = {"Alice", {2000, 1, 15}, 95.5};
    printf("%d年%d月%d日\n", s.birthday.year, s.birthday.month, s.birthday.day);
    return 0;
}

动态内存分配(malloc/free)

程序运行时从堆(heap)申请内存。

malloc:申请内存

#include <stdlib.h>

// 申请5个int的空间
int *arr = (int*)malloc(5 * sizeof(int));

if (arr == NULL) {
    printf("内存分配失败!\n");
    return 1;
}

// 使用
arr[0] = 10;
arr[1] = 20;
// ...

// 释放
free(arr);

calloc:申请并初始化为0

int *arr = (int*)calloc(5, sizeof(int));  // 5个int,全初始化为0
// arr[0] ~ arr[4]都是0

realloc:调整内存大小

int *arr = (int*)malloc(5 * sizeof(int));
// 需要扩大到10个
int *new_arr = (int*)realloc(arr, 10 * sizeof(int));
if (new_arr == NULL) {
    // 扩展失败,arr仍然有效
    free(arr);
    return 1;
}
arr = new_arr;  // 使用新内存

动态字符串

char *str = (char*)malloc(100 * sizeof(char));
if (str == NULL) {
    return 1;
}
strcpy(str, "Hello Dynamic Memory!");
printf("%s\n", str);
free(str);

动态结构体

typedef struct {
    char name[50];
    int age;
} Student;

Student *p = (Student*)malloc(sizeof(Student));
if (p == NULL) return 1;

strcpy(p->name, "Alice");
p->age = 20;

printf("%s, %d\n", p->name, p->age);
free(p);

野指针和内存泄漏

// 野指针(未初始化的指针)
int *p;
*p = 10;  // 崩溃!p不知道指向哪

// 内存泄漏(忘记释放)
int *p = (int*)malloc(10 * sizeof(int));
// 使用...
// 忘记free(p);→程序结束前内存一直占用

// 好习惯:释放后置NULL
free(p);
p = NULL;  // 防止野指针

引用

你写了一个.c文件想给别人用,核心问题是:怎么把代码打包成交付物,让别人方便地调用?

方式一:直接给源代码

把.c和.h直接发给对方,让对方自己编译。

你的交付物:

my_math_lib/
├── my_math.h      (头文件,声明函数)
├── my_math.c      (源文件,实现函数)
└── README.txt     (使用说明)

my_math.h(接口声明):

#ifndef MY_MATH_H
#define MY_MATH_H

// 函数声明
int add(int a, int b);
int subtract(int a, int b);
float divide(int a, int b);

#endif

my_math.c(实现):

#include "my_math.h"

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

float divide(int a, int b) {
    if (b == 0) return 0;
    return (float)a / b;
}

对方怎么用(main.c):

#include <stdio.h>
#include "my_math.h"   // 包含你的头文件

int main() {
    printf("%d\n", add(3, 5));        // 8
    printf("%d\n", subtract(10, 4));  // 6
    printf("%.2f\n", divide(10, 3));  // 3.33
    return 0;
}

对方怎么编译:

# 必须把你的.c一起编译
gcc main.c my_math.c -o program

优缺点:

  • 优点:对方能看到源码,可以自己改
  • 缺点:暴露了所有实现细节,编译时要把所有.c都带上

方式二:打包成静态库(.a / .lib)

把源代码编译成.a(Linux/Mac)或.lib(Windows)文件,隐藏了源码,只提供头文件和库文件。

步骤1:你编译成静态库

Linux/Mac:

# 编译成目标文件(.o)
gcc -c my_math.c -o my_math.o
# 打包成静态库(.a)
ar rcs libmy_math.a my_math.o
# 也可以一步到位
gcc -c my_math.c && ar rcs libmy_math.a my_math.o

Windows(用Visual Studio或MinGW):

# MinGW方式
gcc -c my_math.c -o my_math.o
ar rcs libmy_math.a my_math.o   # 或者
ar rcs my_math.lib my_math.o    # Windows常用.lib后缀

步骤2:你的交付物

my_math_lib/
├── include/
│   └── my_math.h        # 头文件(接口说明)
├── lib/
│   └── libmy_math.a     # 静态库(实现,已编译)
└── README.txt           # 使用说明

步骤3:对方怎么用

对方写main.c,但不需要你的.c文件:

#include <stdio.h>
#include "my_math.h"   // 只包含头文件

int main() {
    printf("%d\n", add(3, 5));
    return 0;
}

对方编译(必须指定库路径和库名):

# -I:头文件路径 -L:库文件路径 -l:库名(去掉lib前缀和.a后缀)
gcc main.c -I./include -L./lib -lmy_math -o program

优缺点:

  • 优点:隐藏源码,编译快了(不需要重新编译你的代码)
  • 缺点:库文件会和编译器/平台绑定(Linux的.a不能在Windows直接用)

方式三:打包成动态库(.so / .dll)

动态库在运行时才加载,可以多个程序共享一份库文件。

步骤1:你编译成动态库

Linux/Mac:

# 编译成位置无关代码(-fPIC)并生成动态库(-shared)
gcc -fPIC -shared -o libmy_math.so my_math.c

Windows(MinGW):

# 生成.dll
gcc -shared -o my_math.dll my_math.c
# 同时生成.lib(导入库,供链接用)
gcc -shared -o my_math.dll my_math.c -Wl,--out-implib,libmy_math.lib

步骤2:你的交付物

my_math_lib/
├── include/
│   └── my_math.h
├── lib/
│   ├── libmy_math.so   (Linux动态库)
│   └── libmy_math.a    (导入库,帮助链接,可选)
└── README.txt

步骤3:对方怎么用

对方写main.c不变。

编译时(链接阶段):

gcc main.c -I./include -L./lib -lmy_math -o program

运行时(执行阶段):

# Linux: 需要告诉系统在哪找.so
export LD_LIBRARY_PATH=./lib:$LD_LIBRARY_PATH
./program

# 或者把.so放到系统目录(/usr/lib/)
sudo cp lib/libmy_math.so /usr/lib/
./program

优缺点:

  • 优点:体积小,可多个程序共享,支持热更新(替换.so无需重新编译)
  • 缺点:部署时要确保动态库在正确路径(LD_LIBRARY_PATH问题)

三种方式对比

对比项 直接给源码 静态库(.a/.lib) 动态库(.so/.dll)
交付物 .c + .h .h + .a .h + .so
隐藏源码 完全暴露 隐藏 隐藏
编译速度 每次重新编译你的代码 只编译对方的代码 只编译对方的代码
可移植性 跨平台(重新编译即可) 绑定平台/编译器 绑定平台/编译器
部署难度 最简单 简单 稍复杂(路径问题)
适用场景 开源项目、教学 内部使用、嵌入式 大型项目、插件系统

工程最佳实践:带pkg-config支持(专业级)

如果你的库给很多人用,可以生成pkg-config文件,让别人用一行命令搞定编译。

1. 创建.pc文件(libmy_math.pc):

prefix=/usr/local
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include

Name: my_math
Description: A simple math library
Version: 1.0.0
Libs: -L${libdir} -lmy_math
Cflags: -I${includedir}

2. 安装到系统:

sudo cp libmy_math.pc /usr/lib/pkgconfig/

3. 对方使用(自动获取编译参数):

gcc main.c $(pkg-config --cflags --libs libmy_math) -o program

文章作者: xmxe
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 xmxe !
赏
 本篇
C语言 C语言
宏宏(Macro)是C语言中的一种预处理指令,简单说就是代码的自动替换工具。用最直白的话讲: 宏就是给一段代码起个名字,编译前把名字替换成对应的代码。 宏的本质:编译前的”查找替换”宏是在编译之前(预处理阶段)由预处理器处理的。它不参与
下一篇 
学习智能体 学习智能体
Claude Code命令基本用法 在会话里直接敲/弹出所有可用命令,支持字母过滤。 命令只有放在消息开头才会被识别,后续文字作为参数(如/plan 修复登录的bug)。 参数约定:<arg>表示必填,[arg]表示可选。
  目录