宏
宏(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;
}
指针的类型
指针是有类型的,类型决定了:
- 指针指向的数据是什么类型。
- 解引用时读多少字节。
- 指针算术运算的步长。
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