C 指针
本章导览 指针是 C 语言最强大也最容易出错的特性。指针 = 地址,理解了这一点,其余都是推论。 本章涵盖:指针基本概念 → 指针变量 → 指针运算 → 数组指针 → 字符串指针 → 函数指针 → 指针数组与多级指针 → 动态内存分配 → 综合小结
Abstract: 本章导览 指针是 C 语言最强大也最容易出错的特性。指针 = 地址,理解了这一点,其余都是推论。
本章涵盖:指针基本概念 → 指针变量 → 指针运算 → 数组指针 → 字符串指针 → 函数指针 → 指针数组与多级指针 → 动态内存分配 → 综合小结
1 指针是什么
指针就是内存地址。
数据存储在内存中,每一块内存都有一个编号(地址)。如果一个变量存储了另一份数据的地址,我们就称它为指针变量。
指针变量的值就是某份数据的地址,这份数据可以是普通变量、数组、字符串、函数,甚至是另一个指针变量。
Example: 直观理解 假设有一个
char类型的变量c,它存储了字符'K'(ASCII 码 75),占用地址0x11A的内存。另有一个指针变量p,它的值为0x11A,正好等于变量c的地址——这种情况我们就称 p 指向了 c,或者说 p 是指向变量 c 的指针。
内存地址: ... | 0x11A | 0x11B | ...
内存内容: ... | 'K' | ... | ...
↑
p ────┘ (p 的值 = 0x11A)
2 指针变量
定义指针变量
在变量名前加星号 *:
datatype *name;
// 或者定义时初始化
datatype *name = value;
*表示这是一个指针变量datatype表示该指针所指向的数据的类型
int a = 100;
int *p_a = &a; // p_a 指向 a,p_a 的类型是 int*
Attention: 取地址符
&指针需要的是地址,所以变量前必须加&取地址符。
// 定义普通变量
float a = 99.5, b = 10.6;
char c = '@', d = '#';
// 定义指针变量
float *p1 = &a;
char *p2 = &c;
// 修改指针变量的值(注意:赋值时不带 *)
p1 = &b;
p2 = &d;
Caution: 关键规则
- 定义指针变量时必须带
*- 给指针变量赋值时不能带
*p1的类型是float*,p2的类型是char*- 连续定义多个指针变量时,每个变量前面都需要带
*:int *p1, *p2, *p3;
通过指针变量取得数据
通过指针获取其指向地址上的数据,使用指针运算符 *:
*pointer // 取得 pointer 所指向的数据
#include <stdio.h>
int main(){
int a = 15;
int *p = &a;
printf("%d, %d\n", a, *p); // 两种方式都可以输出 a 的值
return 0;
}
运行结果:15, 15
- 使用变量名是直接获取数据
- 使用指针是间接获取数据(代价更高,但灵活性更强)
Note:
*在不同场景的含义
场景 含义 示例 定义指针变量 表明这是指针变量 int *p;使用指针变量 获取指针指向的数据 printf("%d", *p);赋值指针变量 不能加 *p = &a;✅*p = &a;❌
通过指针交换两个变量的值
这是理解指针作为函数参数的经典例子。
❌ 不能实现交换的写法
写法一:未初始化的指针 temp
void swap(int *p1, int *p2) {
int *temp;
*temp = *p1; // temp 没有赋值,*temp 无法代表某个变量
*p1 = *p2;
*p2 = *temp;
}
temp是野指针,没有指向合法内存,行为未定义。
写法二:值传递,无法影响外部变量
void swap(int x, int y) {
int temp;
temp = x; x = y; y = temp;
}
// 调用:swap(a, b);
从
a、b到x、y是单向值传递,交换x和y不能交换a和b。
写法三:交换了指针本身,而非指向的数据
void swap(int *p1, int *p2) {
int *p;
p = p1;
p1 = p2;
p2 = p;
}
交换的是函数内部形参
p1和p2的值,不影响外部的实参。
✅ 正确的交换写法
void swap(int *p1, int *p2) {
int temp;
temp = *p1; // 取出 p1 指向的值
*p1 = *p2; // 将 p2 指向的值赋给 p1 指向的地址
*p2 = temp; // 将原来的值赋给 p2 指向的地址
}
// 调用
int a = 10, b = 20;
swap(&a, &b); // 传递地址
// 此时 a=20, b=10
Tip: 核心原则 要在函数内部修改外部变量,必须传递变量的地址(指针),然后通过
*操作指针所指向的内容。
3 指针变量的运算
指针保存的是地址,地址本质上是整数,因此指针可以进行加法、减法和比较运算。
int a = 10, *pa = &a, *paa = &a;
double b = 99.9, *pb = &b;
char c = '@', *pc = &c;
// 最初的值
printf("&a=%#X, &b=%#X, &c=%#X\n", &a, &b, &c);
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
// 加法运算
pa++; pb++; pc++;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
// 减法运算
pa -= 2; pb -= 2; pc -= 2;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
// 比较运算
if(pa <mark> paa){
printf("%d\n", *paa);
} else {
printf("%d\n", *pa);
}
运行结果:
&a=0X28FF44, &b=0X28FF30, &c=0X28FF2B
pa=0X28FF44, pb=0X28FF30, pc=0X28FF2B
pa=0X28FF48, pb=0X28FF38, pc=0X28FF2C
pa=0X28FF40, pb=0X28FF28, pc=0X28FF2A
2686784
Important: 指针加减的本质 指针每次 +1,地址增加的是所指向类型的大小(而不是 1 个字节):
类型 大小 p++地址变化p-=2地址变化int4 字节 +4 -8 double8 字节 +8 -16 char1 字节 +1 -2 这是为了保证指针总是指向完整的数据单元的起始位置。
两个指针相减:结果是两个指针之间相隔的元素个数(不是字节数)。仅当两个指针指向同一个数组时,相减才有意义。
4 数组与指针
数组在内存中是连续排列的。数组名可以认为是一个指针,它指向数组的第 0 个元素。第 0 个元素的地址称为数组的首地址。
int arr[] = { 99, 15, 100, 888, 252 };
内存分布:
| arr[0] | arr[1] | arr[2] | arr[3] | arr[4] |
| 99 | 15 | 100 | 888 | 252 |
↑
arr(首地址)
4.1 引用数组元素
引用一个数组元素,可以用下标法和指针法两种方式:
| 下标法 | 指针法 |
|---|---|
a[i] |
*(a+i) |
p[i] |
*(p+i) |
其中 a 是数组名(int a[10]),p 是指针变量(int *p = a)。
类似地,元素的地址也有两种表示:
| 下标法 | 指针法 |
|---|---|
&a[i] |
a+i |
&p[i] |
p+i |
因此,输入数组元素时:
for(i=0; i<N; i++) scanf("%d", &a[i]);
// 等价于
for(i=0; i<N; i++) scanf("%d", a+i);
4.2 三种遍历数组的方式
(1)下标法 — 最直观
#include <stdio.h>
int main() {
int a[10];
int i;
printf("enter 10 integer numbers:\n");
for(i=0; i<10; i++) scanf("%d", &a[i]);
for(i=0; i<10; i++) printf("%d ", a[i]);
printf("\n");
return 0;
}
(2)通过数组名计算元素地址
#include <stdio.h>
int main() {
int a[10];
int i;
printf("enter 10 integer numbers:\n");
for(i=0; i<10; i++) scanf("%d", a+i);
for(i=0; i<10; i++) printf("%d ", *(a+i));
printf("\n");
return 0;
}
(3)用指针变量遍历 — 执行速度最快
#include <stdio.h>
int main() {
int a[10];
int *p;
printf("enter 10 integer numbers:\n");
for(p=a; p<(a+10); p++) scanf("%d", p);
for(p=a; p<(a+10); p++) printf("%d ", *p);
printf("\n");
return 0;
}
Warning: 指针遍历的常见错误
int *p = a; for(i=0; i<10; i++) scanf("%d", p++); // 此时 p 已经指向数组末尾之后! for(i=0; i<10; i++, p++) printf("%d ", *p); // 错误!第一个循环结束后,
p已经移动到了数组之外。必须在第二个循环前重置p = a。
4.3 *p++、*++p、(*p)++ 的区别
假设 p 指向数组第 n 个元素:
| 表达式 | 等价于 | 含义 | 口诀 |
|---|---|---|---|
*p++ |
*(p++) |
先取第 n 个元素的值,再让 p 指向下一个元素 | 先用后加 |
*++p |
*(++p) |
先让 p 指向下一个元素,再取该元素的值(即第 n+1 个) | 先加后用 |
(*p)++ |
— | 先取第 n 个元素的值,再对该元素的值 +1 | 值自增 |
4.4 数组作为函数参数
在函数一章中已知数组可以作为函数参数。C 语言规定形参中的数组实质上是指针变量。
实参和形参可以有 4 种对应组合:
| 实参 | 形参 |
|---|---|
| 数组名 | 数组名 |
| 数组名 | 指针变量 |
| 指针变量 | 数组名 |
| 指针变量 | 指针变量 |
Example: 例:将数组逆序排列
// 实参:数组名,形参:指针变量 #include <stdio.h> void inv(int *x, int n) { int temp, *y = x + n - 1; for ( ; x < y; x++, y--) { temp = *x; *x = *y; *y = temp; } } int main() { int i, a[10]; for(i=0; i<10; i++) scanf("%d", a+i); inv(a, 10); for(i=0; i<10; i++) printf("%d ", a[i]); printf("\n"); return 0; }
Example: 例:选择排序(指针实现)
void sort(int *x, int n) { int i, j, k, t; for(i=0; i<n-1; i++) { k = i; for(j=i+1; j<n; j++) if(*(x+j) > *(x+k)) k = j; if(k != i) { t = *(x+i); *(x+i) = *(x+k); *(x+k) = t; } } }
5 二维数组与指针
5.1 二维数组元素的地址
int a[3][4] = {1,3,5,7, 9,11,13,15, 17,19,21,23};
可以把 a 看成由一维数组构成的一维数组:a 有 3 个元素 a[0]、a[1]、a[2],每个元素又是一个含有 4 个 int 的一维数组。
“行的地址”与“元素的地址”:
a+i是行的地址(第 i 行一维数组的地址),也可表示为&a[i]a[i]+j是元素的地址(第 i 行第 j 列),也可表示为*(a+i)+j或&a[i][j]
二维数组元素的 4 种表示方式:
| 表示方式 | 说明 |
|---|---|
a[i][j] |
下标法 |
*(*(a+i)+j) |
纯指针法 |
*(a[i]+j) |
混合法 |
(*(a+i))[j] |
混合法 |
5.2 指向二维数组元素的指针变量
方式一:指向单个元素的指针
将二维数组看作一维连续内存,用 int *p 遍历:
#include <stdio.h>
int main() {
int a[3][4] = {1,3,5,7, 9,11,13,15, 17,19,21,23};
int *p;
for(p = a[0]; p < a[0]+12; p++) {
if((p - a[0]) % 4 </mark> 0) printf("\n");
printf("%4d", *p);
}
printf("\n");
return 0;
}
对于 int a[M][N],用 int *p = &a[0][0],则 p+k 指向 a[i][j],其中:
k = i × N + j, i = k / N, j = k % N
方式二:指向一维数组(行)的指针(行指针)
int (*p)[4]; // p 是指向含 4 个 int 的一维数组的指针
p = a;
此时 *(*(p+i)+j) 就是 a[i][j]。
#include <stdio.h>
int main() {
int a[3][4] = {1,3,5,7, 9,11,13,15, 17,19,21,23};
int (*p)[4], i, j;
p = a;
printf("enter row and column: ");
scanf("%d,%d", &i, &j);
printf("a[%d][%d] = %d\n", i, j, *(*(p+i)+j));
return 0;
}
Caution:
int *p[4]vsint (*p)[4]
int *p[4]— 指针数组:p 是包含 4 个int*元素的数组int (*p)[4]— 行指针:p 是指向含 4 个int的一维数组的指针括号至关重要!
5.3 用指针作形参处理二维数组
// 形参为指向元素的指针——将二维数组"拉平"
void average(float *p, int n) {
float *p_end = p + n - 1;
float sum = 0;
for( ; p <= p_end; p++)
sum += (*p);
printf("average = %5.2f\n", sum / n);
}
// 形参为行指针——保留二维结构
void search(float (*p)[4], int n) {
int i;
printf("The score of No.%d are:\n", n);
for(i=0; i<4; i++)
printf("%5.2f ", *(*(p+n)+i));
printf("\n");
}
// 调用
float score[3][4] = {{65,67,70,60},{80,87,90,81},{90,99,100,98}};
average(*score, 12); // *score 即 score[0],类型为 float*
search(score, 2); // score 的类型为 float(*)[4]
6 字符串与指针
C 语言没有专门的字符串类型,字符串存放在字符数组中。数组章节中讲到的指针与数组的规则同样适用于字符数组。
6.1 字符串的两种引用方式
(1)用字符数组存放字符串
char string[] = "I love China!";
printf("%s\n", string);
printf("%c\n", string[7]); // 输出 'C'
(2)用字符指针指向字符串
char *string = "I love China!";
printf("%s\n", string);
字符指针可以随时指向不同的字符串:
char *string;
string = "I love China!";
printf("%s\n", string);
string = "I am a student.";
printf("%s\n", string);
6.2 用指针复制字符串
#include <stdio.h>
int main() {
char a[] = "I am a boy.", b[20], *p1, *p2;
p1 = a; p2 = b;
for( ; *p1 != '\0'; p1++, p2++)
*p2 = *p1;
*p2 = '\0';
printf("string a is: %s\n", a);
printf("string b is: %s\n", b);
return 0;
}
6.3 字符指针作函数参数
用函数实现字符串的复制,形参和实参可以分别是字符数组名或字符指针变量。
// 写法一:数组形参
void copy_string(char from[], char to[]) {
int i = 0;
while(from[i] != '\0') {
to[i] = from[i];
i++;
}
to[i] = '\0';
}
// 写法二:指针形参
void copy_string(char *from, char *to) {
for( ; *from != '\0'; from++, to++)
*to = *from;
*to = '\0';
}
// 写法三:极简版(经典 C 风格)
void copy_string(char *from, char *to) {
while(*to++ = *from++);
}
写法三利用了赋值表达式的值:当
*from为'\0'(即 0)时,赋值表达式的值为 0,循环终止,同时'\0'已经被复制到了to中。
6.4 字符数组与字符指针的重要区别
| 特性 | 字符数组 char a[] |
字符指针 char *p |
|---|---|---|
| 存储内容 | 各个字符 | 字符串的首地址 |
| 赋值方式 | 只能在定义时整体赋值,之后逐字符赋值 | 可以随时赋值指向新字符串 |
| 数组名/指针值 | 是常量,不能修改 a = a+7 ❌ |
是变量,可以修改 p = p+7 ✅ |
| 修改字符 | a[2] = 'r' ✅ |
p[2] = 'r' ❌(字符串常量不可修改) |
| 存储区域 | 栈(可读写) | 字符串常量区(只读) |
char a[] = "House"; // a 在栈上,内容可修改
char *b = "House"; // b 指向常量区,内容不可修改
a[2] = 'r'; // ✅ 合法,a 变为 "Horse"
b[2] = 'r'; // ❌ 未定义行为(段错误)
Tip: 指针指向格式字符串
char *format = "a=%d, b=%f\n"; printf(format, a, b); // 等价于 printf("a=%d, b=%f\n", a, b);
7 函数指针
7.1 什么是函数指针
函数在编译时会被分配一段存储空间,这段空间的起始地址就是函数的指针。可以定义一个指针变量指向函数:
int (*p)(int, int);
p是一个指向函数的指针变量- 该函数的参数为两个
int,返回值为int p的类型用int (*)(int, int)表示
Caution: 区分函数指针和返回指针的函数
int (*p)(int, int);— 函数指针:p 指向一个函数int *p(int, int);— 返回指针的函数:p 是一个函数,返回int*
7.2 用函数指针调用函数
#include <stdio.h>
int max(int x, int y) {
return (x > y) ? x : y;
}
int main() {
int (*p)(int, int); // 定义函数指针
int a, b, c;
p = max; // 将函数 max 的地址赋给 p
printf("please enter a and b: ");
scanf("%d,%d", &a, &b);
c = (*p)(a, b); // 通过函数指针调用
// 也可以写成 c = p(a, b);
printf("%d, %d, max=%d\n", a, b, c);
return 0;
}
Attention: 函数指针使用注意
p = max;✅ — 将函数地址赋给指针p = max(a,b);❌ — 这是函数调用,不是赋地址p + n、p++、p--等运算无意义,不允许
7.3 函数指针的实际应用
根据用户选择调用不同函数:
#include <stdio.h>
int max(int x, int y) { return (x>y) ? x : y; }
int min(int x, int y) { return (x<y) ? x : y; }
int add(int x, int y) { return x + y; }
int main() {
int (*p)(int, int);
int a, b, n;
scanf("%d,%d", &a, &b);
scanf("%d", &n);
if(n <mark> 1) p = max;
else if(n </mark> 2) p = min;
else p = add;
printf("result = %d\n", (*p)(a, b));
return 0;
}
将函数指针作为函数参数(回调函数):
void fun(int x, int y, int (*p)(int, int)) {
int result = (*p)(x, y);
printf("%d\n", result);
}
// 调用
fun(34, -21, max); // 求最大值
fun(34, -21, min); // 求最小值
fun(34, -21, add); // 求和
Tip: 回调函数 将函数指针作为参数传递给另一个函数,使得被调用的函数可以“回调”传入的函数。这是 C 语言实现多态和策略模式的基本手段。
8 返回指针值的函数
一个函数可以返回指针类型的数据(即地址)。定义格式:
类型名 *函数名(参数列表);
float *search(float (*pointer)[4], int n) {
float *pt;
pt = *(pointer + n); // 获取第 n 行的首地址
return pt;
}
// 调用
float score[3][4] = {{60,70,80,90},{56,89,67,88},{34,78,90,66}};
float *p;
int k;
scanf("%d", &k);
p = search(score, k);
for(int i=0; i<4; i++)
printf("%5.2f\t", *(p+i));
Example: 查找不及格学生
float *search(float (*pointer)[4]) { int i; float *pt = NULL; for(i=0; i<4; i++) if(*(*pointer + i) < 60) pt = *pointer; return pt; // 返回 NULL 表示该生全部及格 } // 遍历所有学生 for(i=0; i<3; i++) { p = search(score + i); if(p <mark> *(score + i)) { printf("No.%d fails: ", i+1); for(j=0; j<4; j++) printf("%5.1f ", *(p+j)); printf("\n"); } }
9 指针数组与多级指针
9.1 指针数组
一个数组,其所有元素都是指针类型,称为指针数组。
类型名 *数组名[数组长度];
int *p[4]; // p 是包含 4 个 int* 元素的数组
指针数组适合管理多个字符串(各字符串长度不等时,比二维数组更省内存):
#include <stdio.h>
#include <string.h>
void sort(char *name[], int n) {
char *temp;
int i, j, k;
for(i=0; i<n-1; i++) {
k = i;
for(j=i+1; j<n; j++)
if(strcmp(name[k], name[j]) > 0) k = j;
if(k != i) {
temp = name[i];
name[i] = name[k];
name[k] = temp;
}
}
}
int main() {
char *name[] = {"Follow", "Great", "FORTRAN", "Computer"};
int n = 4;
sort(name, n);
for(int i=0; i<n; i++)
printf("%s\n", name[i]);
return 0;
}
注意:排序时交换的是指针(地址),而不是字符串内容本身,效率很高。
9.2 指向指针数据的指针(二级指针)
可以定义一个指针变量指向指针数组:
char *name[4] = {"Follow", "Great", "FORTRAN", "Computer"};
char **p = name; // p 是二级指针
for(int i=0; i<4; i++) {
p = name + i;
printf("%s\n", *p);
}
p的类型是char**,它指向一个char*类型的数据*p得到的是char*(某个字符串的首地址)**p得到的是char(某个字符)
9.3 带参数的 main 函数
main 函数可以接收命令行参数:
int main(int argc, char *argv[])
argc:参数的个数(包含程序名本身)argv:指针数组,每个元素指向一个命令行参数字符串
例如运行 file1 C program 时:
argc = 3argv[0] = "file1"、argv[1] = "C"、argv[2] = "program"
#include <stdio.h>
int main(int argc, char *argv[]) {
while(argc > 1) {
++argv;
printf("%s\n", *argv);
--argc;
}
return 0;
}
10 动态内存分配
10.1 什么是内存的动态分配
- 栈(Stack):局部变量自动分配,函数结束自动释放
- 堆(Heap):程序员手动申请和释放,大小可在运行时决定
10.2 四个动态内存函数
需要包含头文件 #include <stdlib.h>(或 #include <malloc.h>)
malloc — 分配内存
void *malloc(unsigned int size);
- 在堆中分配
size个字节的连续空间 - 返回该空间首字节的地址(
void*类型) - 分配失败返回
NULL
int *p = (int *)malloc(10 * sizeof(int)); // 分配 10 个 int 的空间
calloc — 分配并初始化
void *calloc(unsigned n, unsigned size);
- 分配
n个大小为size的连续空间,并全部初始化为 0 - 相当于为一维数组动态分配空间
int *p = (int *)calloc(50, sizeof(int)); // 分配 50 个 int,全部初始化为 0
free — 释放内存
void free(void *p);
- 释放
p指向的动态空间(p必须是malloc/calloc的返回值) - 释放后该空间可被重新使用
- 无返回值
free(p); // 释放 p 指向的空间
p = NULL; // 好习惯:置空防止野指针
realloc — 重新分配
void *realloc(void *p, unsigned int size);
- 将
p指向的已分配空间大小改为size字节 - 返回新空间的首地址(可能与原地址不同)
- 失败返回
NULL
p = (int *)realloc(p, 100 * sizeof(int)); // 扩大到 100 个 int
Example: 动态数组排序
#include <stdio.h> #include <stdlib.h> void sort(int a[], int n) { int i, j, t; for(j=0; j<n-1; j++) for(i=0; i<n-1-j; i++) if(a[i] > a[i+1]) { t = a[i]; a[i] = a[i+1]; a[i+1] = t; } } int main() { int *p, i, n; printf("请输入整数的个数:\n"); scanf("%d", &n); p = (int *)calloc(n, sizeof(int)); // 动态分配 printf("输入 %d 个整数:\n", n); for(i=0; i<n; i++) scanf("%d", &p[i]); sort(p, n); printf("排序结果:\n"); for(i=0; i<n; i++) printf(" %d", p[i]); printf("\n"); free(p); // 释放内存 return 0; }
11 综合小结
核心概念回顾
- 指针就是地址。“指向”意味着通过地址可以找到目标数据。
- 只有基类型相同的数据的地址才能存入对应的指针变量。
void*是通用指针,可指向任何类型。 - 要通过指针修改函数外部的变量,必须传递变量的地址。
有关指针类型的完整对比
| 定义 | 含义 |
|---|---|
int i; |
普通整型变量 |
int *p; |
指向 int 数据的指针变量 |
int a[N]; |
整型数组,含 N 个元素 |
int *p[N]; |
指针数组,含 N 个 int* 元素 |
int (*p)[N]; |
行指针,指向含 N 个 int 的一维数组 |
int f(); |
返回 int 的函数 |
int *f(); |
返回指针的函数,返回 int* |
int (*p)(); |
函数指针,指向返回 int 的函数 |
int **p; |
二级指针,指向一个 int* 指针变量 |
指针运算规则
| 运算 | 说明 |
|---|---|
p + n / p - n |
前移/后移 n 个元素(不是 n 个字节) |
p++ / p-- |
移动到下一个/上一个元素 |
p1 - p2 |
两个指针之间的元素个数(须指向同一数组) |
p1 </mark> p2 / p1 < p2 |
比较两个指针的位置 |
p = NULL |
空指针,不指向任何有效数据 |
使用指针的黄金法则
Danger: 必须牢记
- 永远不要使用未初始化的指针(野指针)
malloc/calloc之后必须检查是否为NULLfree之后立即将指针置为NULL- 指针的基类型必须与指向的数据类型匹配
- 数组名是常量,不能对其赋值(
a = a+1❌)
Quote: 相关笔记
- c-arrays — 数组基础、字符数组与字符串
- c-functions — 函数定义、形参与实参、值传递
- c-linked-list — 指针在动态数据结构中的典型应用
- 历史版本保留在本地归档中,不随网站发布。