C语言指针 ------ 详细知识点总结
一、指针的相关概念
1.1 什么是地址
在计算机内存中,每一个字节(Byte)都有一个唯一的编号,这个编号就叫做内存地址。操作系统通过地址来管理和访问内存中的数据。
当我们在C语言中定义一个变量时,系统会自动在内存中为该变量分配一定大小的存储空间,该空间的起始地址就是这个变量的地址。
1.2 什么是指针
指针就是内存地址。通过指针,我们可以直接访问和操作内存中某个地址所存储的数据。
指针的核心思想:存储的不是数据本身,而是数据的地址。
1.3 什么是指针变量
指针变量 是一种特殊的变量,它用来存储另一个变量的内存地址。换句话说,指针变量的值就是另一个变量的地址。
1.4 指针的作用
- 使函数能够修改调用者中的变量(通过传地址)
- 高效地操作数组和字符串
- 实现动态内存分配
- 构建复杂的数据结构(链表、树、图等)
- 函数返回多个值
二、地址与指针
2.1 取地址运算符 &
& 运算符用于获取变量的内存地址。它的操作数必须是一个变量,不能是常量或表达式。
语法:
c
&变量名
示例:
c
int a = 10;
printf("%p", &a); // 输出变量a的内存地址
2.2 地址的大小
在不同的系统中,地址的大小不同:
- 32位系统:地址占4个字节(32位),指针变量大小为4字节
- 64位系统:地址占8个字节(64位),指针变量大小为8字节
可以使用 sizeof 运算符来查看指针变量的大小。
2.3 入门案例:查看变量的地址
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
int a = 100; // 定义整型变量a,并赋值为100
float b = 3.14f; // 定义单精度浮点型变量b,并赋值为3.14
char c = 'A'; // 定义字符型变量c,并赋值为字符'A'
// 使用%p格式符输出各变量的内存地址
// &a、&b、&c 分别表示变量a、b、c的地址
printf("变量a的地址: %p\n", &a);
printf("变量b的地址: %p\n", &b);
printf("变量c的地址: %p\n", &c);
// 使用sizeof查看指针的大小
printf("int*类型指针的大小: %lu 字节\n", sizeof(int*));
printf("float*类型指针的大小: %lu 字节\n", sizeof(float*));
printf("char*类型指针的大小: %lu 字节\n", sizeof(char*));
return 0; // 程序正常结束
}
运行结果示例(64位系统):
变量a的地址: 0x7ffd5e8a3b4c
变量b的地址: 0x7ffd5e8a3b48
变量c的地址: 0x7ffd5e8a3b47
int*类型指针的大小: 8 字节
float*类型指针的大小: 8 字节
char*类型指针的大小: 8 字节
说明:
- 每次运行程序,变量的地址可能不同(由操作系统分配)
- 不管指向什么类型,指针变量本身的大小在同一个系统中是固定的
三、变量与指针
3.1 变量的直接访问与间接访问
| 访问方式 | 说明 |
|---|---|
| 直接访问 | 通过变量名直接访问变量的值,例如 a = 20; |
| 间接访问 | 通过指针变量找到变量的地址,再通过地址访问变量的值 |
打个比方:
- 直接访问 = 你直接去找某个人(知道他在哪个房间)
- 间接访问 = 你先找到一张纸条(指针),纸条上写着那个人的房间号(地址),你再去找
3.2 变量的两个重要属性
每个变量都有两个属性:
- 地址(左值):变量在内存中的位置
- 值(右值):变量中存储的数据
c
int num = 42;
// num 的地址(左值):系统分配的内存位置,例如 0x7ffd...
// num 的值(右值):42
四、指针变量
4.1 指针变量的定义
语法:
c
基类型 *指针变量名;
说明:
*表示这是一个指针变量- 基类型决定了指针变量可以指向的变量类型,也决定了指针移动时的步长
*是指针声明符,可以靠近类型名,也可以靠近变量名
示例:
c
int *p; // p是指向int类型的指针变量
float *fp; // fp是指向float类型的指针变量
char *cp; // cp是指向char类型的指针变量
double *dp; // dp是指向double类型的指针变量
注意:
c
int *p, *q; // 定义了两个指针变量p和q(都指向int)
int *p, q; // p是指针变量,q是普通int变量(容易混淆,不推荐)
4.2 指针变量的初始化与赋值
方式一:定义时初始化
c
int a = 10;
int *p = &a; // 定义指针p,初始化为变量a的地址
方式二:先定义后赋值
c
int a = 10;
int *p; // 先定义指针变量
p = &a; // 再将a的地址赋值给p
方式三:赋值为NULL(空指针)
c
int *p = NULL; // 定义一个空指针,表示当前不指向任何有效地址
重要规则:
- 指针变量只能存储地址,不能直接存储整数等数据
- 指针变量的类型必须与所指向变量的类型匹配
- 未初始化的指针(野指针)非常危险,一定要先赋值再使用
4.3 取值运算符 *(解引用运算符)
* 运算符用于通过指针访问(读取或修改)其所指向的变量的值,也叫做解引用。
语法:
c
*指针变量名 // 获取指针所指向的变量的值
4.4 入门案例:指针的基本使用
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
int a = 10; // 定义整型变量a,初始值为10
int *p = &a; // 定义整型指针p,指向变量a(存储a的地址)
// 直接访问变量a的值
printf("a的值(直接访问): %d\n", a);
// 通过指针p间接访问变量a的值
// *p 表示"取出p所指向地址中存储的值"
printf("a的值(间接访问): %d\n", *p);
// 输出变量a的地址和指针p中存储的地址(两者相同)
printf("a的地址: %p\n", &a);
printf("p存储的地址: %p\n", p);
// 通过指针修改变量a的值
*p = 100; // 将p所指向的地址中的值修改为100
printf("修改后a的值: %d\n", a); // a的值变成了100
// 指针p本身的地址(p自己也是一个变量,也有地址)
printf("p自身的地址: %p\n", &p);
return 0; // 程序正常结束
}
运行结果示例:
a的值(直接访问): 10
a的值(间接访问): 10
a的地址: 0x7ffd12345678
p存储的地址: 0x7ffd12345678
修改后a的值: 100
p自身的地址: 0x7ffd12345680
4.5 入门案例:两个指针指向同一个变量
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
int a = 50; // 定义整型变量a,初始值为50
int *p1 = &a; // 定义指针p1,指向变量a
int *p2 = &a; // 定义指针p2,也指向变量a
// 两个指针指向同一个变量,通过任一指针都可以访问a
printf("a = %d\n", a); // 直接访问: 50
printf("*p1 = %d\n", *p1); // 通过p1访问: 50
printf("*p2 = %d\n", *p2); // 通过p2访问: 50
// 通过p1修改a的值
*p1 = 200;
printf("\n通过p1修改后:\n");
printf("a = %d\n", a); // 200
printf("*p1 = %d\n", *p1); // 200
printf("*p2 = %d\n", *p2); // 200(p2看到的值也变了)
return 0; // 程序正常结束
}
五、指针的自增、自减运算
5.1 基本概念
指针变量可以进行自增(++)和自减(--)运算,其含义是使指针向前或向后移动一个"单元"。
关键点: 指针移动的字节数取决于指针的基类型。
| 基类型 | 大小(字节) | p++ 后移动的字节数 |
|---|---|---|
char |
1 | 1 字节 |
int |
4 | 4 字节 |
float |
4 | 4 字节 |
double |
8 | 8 字节 |
5.2 指针自增/自减的形式
假设 p 是一个指针变量:
| 表达式 | 含义 | 说明 |
|---|---|---|
p++ 或 ++p |
p 指向下一个元素 | 先用原值(或先加),然后地址 +sizeof(基类型) |
p-- 或 --p |
p 指向上一个元素 | 先用原值(或先减),然后地址 -sizeof(基类型) |
*p++ |
先取 *p 的值,然后 p 指向下一个元素 |
等价于 *(p++),后缀++优先级高于* |
*++p |
先让 p 指向下一个元素,再取 *p 的值 |
等价于 *(++p) |
(*p)++ |
先取 *p 的值,然后将该值加1 |
先解引用,再对值做自增 |
++(*p) |
先将 *p 的值加1,再取值 |
先对值做自增,再使用 |
5.3 入门案例:指针自增运算
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
int arr[] = {10, 20, 30, 40, 50}; // 定义一个整型数组,包含5个元素
int *p = arr; // 定义指针p,指向数组的第一个元素(arr等价于&arr[0])
// 输出初始状态
printf("初始状态:\n");
printf("p指向的值: %d\n", *p); // 输出10(arr[0]的值)
printf("p存储的地址: %p\n\n", p); // 输出arr[0]的地址
// p++:先使用p的当前值,然后p向后移动一个int的位置(4字节)
printf("执行*p++后:\n");
printf("*p++的返回值: %d\n", *p++); // 先取*p=10,然后p后移
printf("现在p指向的值: %d\n", *p); // 输出20(arr[1]的值)
printf("p存储的地址: %p\n\n", p); // 地址增加了4
// ++p:先让p向后移动一个int的位置,再取值
printf("执行*++p后:\n");
printf("*++p的返回值: %d\n", *++p); // p先后移到arr[2],再取*p=30
printf("现在p指向的值: %d\n\n", *p); // 输出30
// (*p)++:先取*p的值,然后该值加1(但不改变p的指向)
printf("执行(*p)++后:\n");
printf("(*p)++的返回值: %d\n", (*p)++); // 先取*p=30,然后* p变为31
printf("现在*p的值: %d\n\n", *p); // 输出31
// 指针自减
printf("执行*p--后:\n");
printf("*p--的返回值: %d\n", *p--); // 先取*p=31,然后p前移
printf("现在p指向的值: %d\n", *p); // 输出20(arr[1])
return 0; // 程序正常结束
}
5.4 入门案例:用指针遍历数组
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 定义并初始化数组
int n = 10; // 数组元素个数
int *p = arr; // 指针p指向数组首元素
printf("数组元素: ");
// 使用指针的自增运算来遍历数组
for (int i = 0; i < n; i++)
{
printf("%d ", *p); // 输出当前p指向的元素值
p++; // p向后移动一个int的位置,指向下一个元素
}
printf("\n");
return 0; // 程序正常结束
}
运行结果:
数组元素: 1 2 3 4 5 6 7 8 9 10
5.5 入门案例:指针运算的地址变化
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
int arr[] = {100, 200, 300}; // 定义整型数组
int *p = arr; // p指向arr[0]
// 观察指针自增过程中地址的变化
printf("元素值\t地址\n");
printf("%d\t%p\n", *p, p); // arr[0]: 100
p++; // p指向arr[1]
printf("%d\t%p\n", *p, p); // arr[1]: 200
p++; // p指向arr[2]
printf("%d\t%p\n", *p, p); // arr[2]: 300
// 注意:相邻int元素的地址差为4(32位int占4字节)
// 即每次p++地址值增加了4
return 0; // 程序正常结束
}
六、数组与指针
6.1 一维数组与指针
6.1.1 数组名与指针的关系
在C语言中,数组名在大多数情况下会自动转换为指向数组第一个元素的指针(即数组首元素的地址)。
c
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // arr 等价于 &arr[0]
重要等价关系:
| 表达式 | 等价形式 | 含义 |
|---|---|---|
arr |
&arr[0] |
数组首元素的地址 |
arr + i |
&arr[i] |
第i个元素的地址 |
*(arr + i) |
arr[i] |
第i个元素的值 |
p + i |
&p[i] |
指针p后移i个位置的地址 |
*(p + i) |
p[i] |
p后移i个位置后取值 |
6.1.2 用指针访问一维数组元素的三种方式
c
// 方式1:下标法
arr[i]
// 方式2:指针偏移法(不改变指针)
*(arr + i)
// 方式3:指针变量自增法(改变指针)
*p++
6.1.3 入门案例:一维数组与指针
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
int arr[] = {10, 20, 30, 40, 50}; // 定义并初始化一维数组
int *p = arr; // 定义指针p,指向数组首元素
int n = 5; // 数组元素个数
// 方法1:下标法访问数组元素
printf("方法1(下标法):\n");
for (int i = 0; i < n; i++)
{
printf("arr[%d] = %d\n", i, arr[i]); // 用下标直接访问
}
// 方法2:数组名+偏移量法
printf("\n方法2(数组名偏移法):\n");
for (int i = 0; i < n; i++)
{
// *(arr + i) 等价于 arr[i]
printf("*(arr + %d) = %d\n", i, *(arr + i));
}
// 方法3:指针变量法
printf("\n方法3(指针变量法):\n");
for (int i = 0; i < n; i++)
{
// *(p + i) 等价于 p[i]
printf("*(p + %d) = %d\n", i, *(p + i));
}
return 0; // 程序正常结束
}
6.1.4 入门案例:指针与数组名的区别
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
int arr[] = {1, 2, 3, 4, 5}; // 定义数组
int *p = arr; // 指针p指向arr[0]
// 数组名是"常量指针",不能被赋值(不能改变指向)
// arr++; // 错误!数组名不能自增
// 指针变量可以自增,改变指向
p++; // 合法,p现在指向arr[1]
printf("*p = %d\n", *p); // 输出2
// sizeof的区别
printf("sizeof(arr) = %lu\n", sizeof(arr)); // 整个数组的大小: 5*4=20字节
printf("sizeof(p) = %lu\n", sizeof(p)); // 指针变量的大小: 8字节(64位)
printf("数组元素个数 = %lu\n", sizeof(arr) / sizeof(arr[0])); // 计算元素个数:5
return 0; // 程序正常结束
}
6.2 二维数组与指针
6.2.1 二维数组的地址结构
二维数组可以看作"数组的数组"。以 int a[3][4] 为例:
a[0] → a[0][0] a[0][1] a[0][2] a[0][3] // 第0行(一个含4个int的数组)
a[1] → a[1][0] a[1][1] a[1][2] a[1][3] // 第1行
a[2] → a[2][0] a[2][1] a[2][2] a[2][3] // 第2行
6.2.2 二维数组与指针的等价关系
设 int a[3][4];,则:
| 表达式 | 含义 | 类型 |
|---|---|---|
a |
第0行的地址(行指针) | int (*)[4] |
a + i |
第i行的地址 | int (*)[4] |
*(a + i) |
第i行第0列的地址(等价于 a[i]) |
int * |
*(a + i) + j |
第i行第j列的地址(等价于 &a[i][j]) |
int * |
*(*(a + i) + j) |
第i行第j列的值(等价于 a[i][j]) |
int |
a[i] |
第i行第0列的地址 | int * |
a[i] + j |
第i行第j列的地址 | int * |
a[i][j] |
第i行第j列的值 | int |
6.2.3 行指针(数组指针)
指向一维数组的指针叫做数组指针(也叫行指针):
c
int a[3][4];
int (*p)[4] = a; // p是指向含4个int元素的数组的指针
p + 1跳过一整行(4个int,16字节)*(p + i) + j等价于&a[i][j]*(*(p + i) + j)等价于a[i][j]
6.2.4 入门案例:二维数组与指针
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
int a[3][4] = { // 定义3行4列的二维数组
{1, 2, 3, 4}, // 第0行
{5, 6, 7, 8}, // 第1行
{9, 10, 11, 12} // 第2行
};
int i, j; // 循环变量
// 方法1:下标法
printf("方法1(下标法):\n");
for (i = 0; i < 3; i++) // 遍历每一行
{
for (j = 0; j < 4; j++) // 遍历每一列
{
printf("%4d", a[i][j]); // 用下标法访问元素
}
printf("\n"); // 每行输出后换行
}
// 方法2:指针法(使用*(a+i)+j形式)
printf("\n方法2(指针法):\n");
for (i = 0; i < 3; i++)
{
for (j = 0; j < 4; j++)
{
// *(*(a + i) + j) 等价于 a[i][j]
printf("%4d", *(*(a + i) + j));
}
printf("\n");
}
// 方法3:使用行指针(数组指针)
printf("\n方法3(行指针法):\n");
int (*p)[4] = a; // p是指向含4个int的数组的指针(行指针)
for (i = 0; i < 3; i++)
{
for (j = 0; j < 4; j++)
{
// *(*(p + i) + j) 等价于 a[i][j]
printf("%4d", *(*(p + i) + j));
}
printf("\n");
}
return 0; // 程序正常结束
}
运行结果:
方法1(下标法):
1 2 3 4
5 6 7 8
9 10 11 12
方法2(指针法):
1 2 3 4
5 6 7 8
9 10 11 12
方法3(行指针法):
1 2 3 4
5 6 7 8
9 10 11 12
七、字符串与指针
7.1 用字符指针指向字符串
在C语言中,字符串可以用字符数组存储,也可以用字符指针指向一个字符串常量。
c
// 方式1:字符数组
char str[] = "Hello";
// 方式2:字符指针
char *p = "Hello"; // p指向字符串常量"Hello"的首字符
两种方式的区别:
| 特性 | char str[] = "Hello"; |
char *p = "Hello"; |
|---|---|---|
| 存储位置 | 数组在栈区,内容可以修改 | 指针指向常量区,内容不可修改 |
| 能否修改 | str[0] = 'h'; 合法 |
p[0] = 'h'; 未定义行为 |
| sizeof | 6(包含'\0') | 指针的大小(4或8) |
| str/p本身 | 是常量,不能被赋值 | 是变量,可以改变指向 |
7.2 字符指针的常见操作
通过字符指针可以实现:
- 遍历字符串
- 字符串比较
- 字符串复制
- 字符串连接
7.3 入门案例:字符指针的基本使用
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
char *p = "Hello, World!"; // 定义字符指针,指向字符串常量
// 逐个字符输出
printf("逐个字符输出: ");
while (*p != '\0') // 当p未指向字符串结束符时,继续循环
{
printf("%c", *p); // 输出当前字符
p++; // 指针后移,指向下一个字符
}
printf("\n");
// 注意:此时p已经指向了'\0'之后,不再指向字符串开头
// 如果需要再次使用,需要重新赋值
p = "Hello, World!"; // 重新让p指向字符串开头
// 用下标法访问
printf("用下标法输出: ");
for (int i = 0; p[i] != '\0'; i++) // 用p[i]遍历直到遇到结束符
{
printf("%c", p[i]); // 输出p[i]对应的字符
}
printf("\n");
return 0; // 程序正常结束
}
7.4 入门案例:字符指针与字符数组的区别
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
// 字符数组方式:可以修改内容
char str[] = "Hello"; // str是字符数组,存储在栈区,内容可修改
str[0] = 'h'; // 合法:将第一个字符改为小写'h'
printf("字符数组str: %s\n", str); // 输出: hello
// 字符指针方式:指向字符串常量,不可修改内容
char *p = "Hello"; // p指向常量区的字符串
// p[0] = 'h'; // 错误!运行时会报段错误(segmentation fault)
p = "World"; // 合法:p可以重新指向另一个字符串
printf("字符指针p: %s\n", p); // 输出: World
// sizeof的区别
printf("sizeof(str) = %lu\n", sizeof(str)); // 6(H,e,l,l,o,\0共6字节)
printf("sizeof(p) = %lu\n", sizeof(p)); // 8(64位系统指针大小)
return 0; // 程序正常结束
}
7.5 入门案例:用字符指针实现字符串复制
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
char source[] = "I love C programming!"; // 源字符串(字符数组)
char dest[50]; // 目标字符数组,足够大
char *ps = source; // ps指向源字符串
char *pd = dest; // pd指向目标字符串
// 逐字符复制,直到遇到源字符串的结束符'\0'
while (*ps != '\0') // 源字符串未结束
{
*pd = *ps; // 将源字符复制到目标位置
ps++; // 源指针后移
pd++; // 目标指针后移
}
*pd = '\0'; // 在目标字符串末尾添加结束符
printf("源字符串: %s\n", source); // 输出源字符串
printf("目标字符串: %s\n", dest); // 输出复制后的字符串
return 0; // 程序正常结束
}
运行结果:
源字符串: I love C programming!
目标字符串: I love C programming!
八、指针数组
8.1 指针数组的定义
指针数组:一个数组,其每个元素都是一个指针。
语法:
c
基类型 *数组名[数组大小];
说明:
[]的优先级高于*,所以*数组名[大小]先表示数组,再表示每个元素是指针- 每个数组元素都是一个指向"基类型"的指针
8.2 指针数组的典型用途
| 用途 | 说明 |
|---|---|
| 管理多个字符串 | 每个元素指向一个字符串的首地址 |
| 模拟二维数组 | 元素可以指向不同长度的数组 |
| main函数参数 | char *argv[] 就是一个指针数组 |
8.3 入门案例:指针数组管理多个字符串
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
// 定义一个指针数组,每个元素指向一个字符串常量
char *fruits[] = {
"Apple", // fruits[0] 指向 "Apple"
"Banana", // fruits[1] 指向 "Banana"
"Cherry", // fruits[2] 指向 "Cherry"
"Durian", // fruits[3] 指向 "Durian"
"Elderberry" // fruits[4] 指向 "Elderberry"
};
// 计算数组元素个数
int n = sizeof(fruits) / sizeof(fruits[0]); // 5个指针元素
// 遍历指针数组,输出每个字符串
printf("水果列表:\n");
for (int i = 0; i < n; i++)
{
// fruits[i] 是一个char*类型的指针,可以直接用%s输出字符串
printf(" fruits[%d] = %s\n", i, fruits[i]);
}
return 0; // 程序正常结束
}
运行结果:
水果列表:
fruits[0] = Apple
fruits[1] = Banana
fruits[2] = Cherry
fruits[3] = Durian
fruits[4] = Elderberry
8.4 入门案例:指针数组与二维字符数组的区别
c
#include <stdio.h> // 包含标准输入输出头文件
#include <string.h> // 包含字符串处理函数头文件
int main()
{
// 方式1:二维字符数组(每个字符串长度固定,浪费空间)
char names1[3][20] = {
"Zhang San", // 占用20字节,即使只有9个字符+1个\0
"Li Si", // 占用20字节
"Wang Wu" // 占用20字节
};
// 方式2:指针数组(每个字符串长度灵活,节省空间)
char *names2[] = {
"Zhang San", // 只占用实际字符+1的存储空间
"Li Si",
"Wang Wu"
};
// 两种方式都可以用下标法访问
printf("二维字符数组:\n");
for (int i = 0; i < 3; i++)
{
printf(" %s\n", names1[i]); // names1[i] 是第i行的首地址
}
printf("\n指针数组:\n");
for (int i = 0; i < 3; i++)
{
printf(" %s\n", names2[i]); // names2[i] 是第i个指针
}
// 二维数组可以修改单个字符
names1[0][0] = 'z'; // 合法
printf("\n修改后names1[0]: %s\n", names1[0]); // zhang San
// 指针数组不能修改字符串内容,但可以修改指针指向
// names2[0][0] = 'z'; // 错误!字符串常量不可修改
names2[0] = "New Name"; // 合法:改变指针指向
printf("修改后names2[0]: %s\n", names2[0]); // New Name
return 0; // 程序正常结束
}
九、指向指针的指针(二级指针)
9.1 基本概念
二级指针 (指向指针的指针):一个指针变量,它存储的是另一个指针变量的地址。
变量a → 值: 100 地址: 0x1000
指针p → 值: 0x1000 地址: 0x2000 (存储a的地址)
二级指针pp → 值: 0x2000 地址: 0x3000 (存储p的地址)
9.2 二级指针的定义与使用
语法:
c
基类型 **指针变量名;
说明:
**pp表示pp是一个指向指针的指针*pp得到的是pp所指向的那个指针的值(即一级指针的值,也就是普通变量的地址)**pp得到的是最终的变量值
9.3 入门案例:二级指针的基本使用
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
int a = 100; // 定义普通整型变量a,值为100
int *p = &a; // 定义一级指针p,指向变量a
int **pp = &p; // 定义二级指针pp,指向指针p
// 通过不同方式访问a的值
printf("a的值(直接访问): %d\n", a); // 直接访问: 100
printf("a的值(一级指针): %d\n", *p); // 通过一级指针: 100
printf("a的值(二级指针): %d\n", **pp); // 通过二级指针: 100
// 通过不同方式访问a的地址
printf("\na的地址: %p\n", &a); // a的地址
printf("p的值(a的地址): %p\n", p); // p中存储的值就是a的地址
printf("*pp(p的值): %p\n", *pp); // *pp就是p的值,即a的地址
// 通过二级指针修改a的值
**pp = 999; // 等价于 *p = 999; 等价于 a = 999;
printf("\n通过二级指针修改后:\n");
printf("a = %d\n", a); // 999
printf("*p = %d\n", *p); // 999
printf("**pp = %d\n", **pp); // 999
// 各变量自身的地址
printf("\na的地址: %p\n", &a);
printf("p的地址: %p\n", &p); // p自身的地址
printf("pp的地址: %p\n", &pp); // pp自身的地址
return 0; // 程序正常结束
}
9.4 入门案例:二级指针与指针数组
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
// 定义指针数组,每个元素指向一个字符串
char *names[] = {"Alice", "Bob", "Charlie"};
// 定义二级指针pp,指向指针数组names的首元素
// names 是数组首元素的地址,即 &names[0],类型为 char**
char **pp = names;
// 通过二级指针遍历指针数组中的每个字符串
printf("使用二级指针遍历:\n");
for (int i = 0; i < 3; i++)
{
// *(pp + i) 等价于 names[i],得到第i个字符串的首地址
// 用%s输出该地址处的字符串
printf(" names[%d] = %s\n", i, *(pp + i));
}
// 也可以逐级解引用
printf("\n逐字符访问:\n");
printf("names[0]的第1个字符: %c\n", **pp); // 'A'(names[0]的首字符)
printf("names[0]的第2个字符: %c\n", *(*pp + 1)); // 'l'(names[0]的第2个字符)
return 0; // 程序正常结束
}
运行结果:
使用二级指针遍历:
names[0] = Alice
names[1] = Bob
names[2] = Charlie
逐字符访问:
names[0]的第1个字符: A
names[0]的第2个字符: l
十、指针变量作函数参数
10.1 为什么需要指针作函数参数
在C语言中,函数参数传递默认是值传递 (传值的副本),函数内部对形参的修改不会影响实参。
如果需要在函数内部修改调用者中的变量 ,就需要传递变量的地址 (指针),这就是地址传递(传址调用)。
10.2 值传递与地址传递的对比
| 特性 | 值传递 | 地址传递 |
|---|---|---|
| 传递内容 | 变量的值(副本) | 变量的地址 |
| 函数内能否修改实参 | 不能 | 能 |
| 语法 | void func(int a) |
void func(int *a) |
| 调用方式 | func(x) |
func(&x) |
10.3 入门案例:值传递(不能修改实参)
c
#include <stdio.h> // 包含标准输入输出头文件
// 值传递:形参a是实参x的副本
void change(int a) // 接收int类型的值
{
a = 999; // 只修改了副本a,不影响实参x
printf("函数内a = %d\n", a); // 999
}
int main()
{
int x = 100; // 定义变量x
printf("调用前x = %d\n", x); // 100
change(x); // 值传递:将x的值复制给形参a
printf("调用后x = %d\n", x); // 100(x没有被修改!)
return 0; // 程序正常结束
}
运行结果:
调用前x = 100
函数内a = 999
调用后x = 100
10.4 入门案例:地址传递(可以修改实参)
c
#include <stdio.h> // 包含标准输入输出头文件
// 地址传递:形参p是指针,接收变量的地址
void change(int *p) // p接收的是变量x的地址
{
*p = 999; // 通过指针修改p所指向地址的值(即修改了x)
printf("函数内*p = %d\n", *p); // 999
}
int main()
{
int x = 100; // 定义变量x
printf("调用前x = %d\n", x); // 100
change(&x); // 地址传递:将x的地址传给函数
printf("调用后x = %d\n", x); // 999(x被成功修改!)
return 0; // 程序正常结束
}
运行结果:
调用前x = 100
函数内*p = 999
调用后x = 999
10.5 入门案例:用指针参数实现两个变量的交换
c
#include <stdio.h> // 包含标准输入输出头文件
// 交换函数:接收两个指针参数
void swap(int *pa, int *pb) // pa和pb分别指向要交换的两个变量
{
int temp; // 定义临时变量
temp = *pa; // 将pa所指向的值存入temp
*pa = *pb; // 将pb所指向的值赋给pa所指向的位置
*pb = temp; // 将temp的值赋给pb所指向的位置
}
int main()
{
int a = 10; // 定义变量a
int b = 20; // 定义变量b
printf("交换前: a = %d, b = %d\n", a, b); // a=10, b=20
swap(&a, &b); // 传递a和b的地址
printf("交换后: a = %d, b = %d\n", a, b); // a=20, b=10
return 0; // 程序正常结束
}
运行结果:
交换前: a = 10, b = 20
交换后: a = 20, b = 10
10.6 入门案例:数组名作函数参数(本质是指针)
c
#include <stdio.h> // 包含标准输入输出头文件
// 计算数组元素之和
// 参数arr实际上是一个指针,不是整个数组的副本
int arraySum(int arr[], int n) // arr[]等价于 *arr
{
int sum = 0; // 初始化累加和为0
for (int i = 0; i < n; i++)
{
sum += arr[i]; // 累加每个元素的值
}
return sum; // 返回总和
}
// 用指针方式实现同样的功能
int arraySum2(int *arr, int n) // 明确用指针接收
{
int sum = 0; // 初始化累加和为0
for (int i = 0; i < n; i++)
{
sum += *(arr + i); // 用指针偏移方式访问元素
}
return sum; // 返回总和
}
int main()
{
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 定义数组
int n = sizeof(arr) / sizeof(arr[0]); // 计算元素个数
printf("数组元素之和: %d\n", arraySum(arr, n)); // 55
printf("数组元素之和: %d\n", arraySum2(arr, n)); // 55
return 0; // 程序正常结束
}
十一、返回指针值的函数
11.1 基本概念
函数可以返回一个指针(地址),这样的函数叫做返回指针值的函数。
语法:
c
基类型 *函数名(参数列表)
{
// 函数体
return &某变量; // 或 return 某指针;
}
注意: * 紧跟在函数名前面,表示返回值是一个指针。
11.2 重要注意事项
绝对不能返回局部变量的地址! 局部变量在函数结束后被销毁,其地址不再有效。
c
// 错误示例------返回局部变量的地址(悬垂指针)
int *func()
{
int a = 10; // a是局部变量,函数结束后会被销毁
return &a; // 危险!返回了一个已销毁变量的地址
}
安全的做法:
- 返回全局变量或静态局部变量的地址
- 返回通过
malloc动态分配的内存地址 - 返回传入的指针参数
11.3 入门案例:返回静态局部变量的地址
c
#include <stdio.h> // 包含标准输入输出头文件
// 返回两个数中较大数的地址
int *maxPtr(int *pa, int *pb) // 接收两个指针,返回较大者的指针
{
// 比较两个指针所指向的值
if (*pa >= *pb) // 如果a >= b
{
return pa; // 返回a的地址
}
else // 否则
{
return pb; // 返回b的地址
}
}
int main()
{
int a = 30; // 定义变量a
int b = 50; // 定义变量b
// 调用函数,获取较大值的地址
int *pMax = maxPtr(&a, &b);
printf("a = %d, b = %d\n", a, b);
printf("较大值 = %d\n", *pMax); // 50
return 0; // 程序正常结束
}
11.4 入门案例:在数组中查找元素并返回指针
c
#include <stdio.h> // 包含标准输入输出头文件
#include <stddef.h> // 包含NULL的定义
// 在数组arr中查找值为target的元素,找到则返回其地址,否则返回NULL
int *findElement(int arr[], int n, int target)
{
for (int i = 0; i < n; i++) // 遍历数组
{
if (arr[i] == target) // 如果找到目标值
{
return &arr[i]; // 返回该元素的地址
}
}
return NULL; // 未找到,返回NULL
}
int main()
{
int arr[] = {10, 25, 33, 47, 58, 62, 79}; // 定义数组
int n = sizeof(arr) / sizeof(arr[0]); // 计算元素个数
int target = 47; // 要查找的目标值
// 调用查找函数
int *result = findElement(arr, n, target);
if (result != NULL) // 如果找到了
{
printf("找到了!值为%d,位于地址%p\n", *result, result);
printf("它是数组的第%ld个元素\n", result - arr + 1);
// 指针相减得到两个元素之间的距离(元素个数)
}
else // 没找到
{
printf("未找到值为%d的元素\n", target);
}
return 0; // 程序正常结束
}
运行结果:
找到了!值为47,位于地址0x...
它是数组的第4个元素
十二、指针数组作main函数的参数
12.1 带参数的main函数
标准的 main 函数可以接收命令行参数:
c
int main(int argc, char *argv[])
{
// 函数体
return 0;
}
参数说明:
| 参数 | 含义 |
|---|---|
argc |
argument count,命令行参数的个数(包含程序名本身) |
argv |
argument vector,是一个指针数组,每个元素指向一个字符串(参数内容) |
注意:
argv[0]始终是程序的名称(含路径)argv[1]到argv[argc-1]是用户输入的命令行参数argv[argc]的值是NULL
12.2 命令行参数的执行方式
假设编译后生成可执行文件 program:
./program hello world 123
则:
argc = 4argv[0]="./program"argv[1]="hello"argv[2]="world"argv[3]="123"argv[4]=NULL
12.3 入门案例:输出所有命令行参数
c
#include <stdio.h> // 包含标准输入输出头文件
// 带命令行参数的main函数
int main(int argc, char *argv[])
{
// argc: 命令行参数的总个数
// argv: 指针数组,每个元素指向一个参数字符串
printf("参数个数: %d\n\n", argc); // 输出参数个数
// 遍历所有参数
for (int i = 0; i < argc; i++)
{
// argv[i] 是第i个参数的字符串地址
printf("argv[%d] = \"%s\"\n", i, argv[i]);
}
// 输出程序名称
if (argc > 0)
{
printf("\n程序名称: %s\n", argv[0]);
}
// 输出用户参数(跳过程序名)
if (argc > 1)
{
printf("\n用户参数:\n");
for (int i = 1; i < argc; i++) // 从argv[1]开始
{
printf(" 第%d个参数: %s\n", i, argv[i]);
}
}
else
{
printf("\n未提供额外参数\n");
}
return 0; // 程序正常结束
}
编译和运行:
bash
gcc program.c -o program
./program hello world 2024
运行结果:
参数个数: 4
argv[0] = "./program"
argv[1] = "hello"
argv[2] = "world"
argv[3] = "2024"
程序名称: ./program
用户参数:
第1个参数: hello
第2个参数: world
第3个参数: 2024
12.4 入门案例:简易命令行计算器
c
#include <stdio.h> // 包含标准输入输出头文件
#include <stdlib.h> // 包含atoi函数(字符串转整数)
// 通过命令行参数实现简单的两个数的运算
// 使用方式: ./calc 10 + 20
int main(int argc, char *argv[])
{
// 检查参数个数是否正确(程序名 + 数字1 + 运算符 + 数字2 = 4个参数)
if (argc != 4)
{
printf("用法: %s 数字1 运算符 数字2\n", argv[0]);
printf("示例: %s 10 + 20\n", argv[0]);
return 1; // 参数错误,返回1
}
// 从命令行参数中读取两个操作数
// atoi()函数将字符串转换为整数
int num1 = atoi(argv[1]); // 第一个数字
char op = argv[2][0]; // 运算符(取字符串的第一个字符)
int num2 = atoi(argv[3]); // 第二个数字
int result; // 存储运算结果
// 根据运算符执行相应的运算
switch (op)
{
case '+': // 加法
result = num1 + num2;
printf("%d + %d = %d\n", num1, num2, result);
break; // 跳出switch
case '-': // 减法
result = num1 - num2;
printf("%d - %d = %d\n", num1, num2, result);
break;
case '*': // 乘法
result = num1 * num2;
printf("%d * %d = %d\n", num1, num2, result);
break;
case '/': // 除法
if (num2 != 0) // 检查除数是否为0
{
result = num1 / num2;
printf("%d / %d = %d\n", num1, num2, result);
}
else
{
printf("错误:除数不能为0!\n");
}
break;
default: // 不支持的运算符
printf("不支持的运算符: %c\n", op);
break;
}
return 0; // 程序正常结束
}
十三、指针相关的重要补充知识
13.1 NULL指针
c
int *p = NULL; // 定义一个空指针,不指向任何有效地址
NULL在大多数系统中定义为((void*)0)或0- 使用指针前应检查是否为
NULL - 对
NULL指针进行解引用会导致程序崩溃(段错误)
c
if (p != NULL) // 使用前先检查
{
printf("%d\n", *p); // 安全地使用
}
13.2 指针的常见错误
| 错误类型 | 示例 | 后果 |
|---|---|---|
| 野指针(未初始化) | int *p; *p = 10; |
未定义行为,可能崩溃 |
| 空指针解引用 | int *p = NULL; *p = 10; |
段错误 |
| 类型不匹配 | float f; int *p = &f; |
数据解释错误 |
| 返回局部变量地址 | int *f() { int a; return &a; } |
悬垂指针 |
13.3 void 指针(通用指针)
c
void *p; // 可以指向任何类型的数据
void *不能直接解引用,需要先强制类型转换void *可以与任何类型的指针互相赋值(无需强制转换)- 常见于
malloc等内存分配函数的返回值
13.4 指针的大小
c
#include <stdio.h>
int main()
{
printf("char*的大小: %lu\n", sizeof(char*)); // 8(64位系统)
printf("int*的大小: %lu\n", sizeof(int*)); // 8
printf("float*的大小: %lu\n", sizeof(float*)); // 8
printf("double*的大小: %lu\n", sizeof(double*)); // 8
// 无论指向什么类型,指针本身的大小在同一个系统中是固定的
// 32位系统: 4字节
// 64位系统: 8字节
return 0;
}
13.5 指针与数组常用运算总结
假设 int arr[5] = {10, 20, 30, 40, 50}; 且 int *p = arr;
| 表达式 | 值 | 说明 |
|---|---|---|
*p |
10 | p当前指向的元素值 |
*(p + 2) |
30 | p偏移2个位置后的元素值 |
*p + 2 |
12 | p指向的值加2(注意与上面的区别) |
p[3] |
40 | 等价于 *(p + 3) |
p++ |
--- | p指向下一个元素 |
p - arr |
0 | 两个指针相减,得到元素个数之差 |
总结图
┌─────────────────────────────────────────────────┐
│ 指针知识体系 │
├─────────────────────────────────────────────────┤
│ │
│ 基础概念 │
│ ├── 地址(变量在内存中的位置编号) │
│ ├── 指针(就是地址) │
│ ├── 指针变量(存储地址的变量) │
│ ├── &取地址运算符 │
│ └── *解引用运算符(通过地址访问值) │
│ │
│ 指针运算 │
│ ├── 赋值:p = &a │
│ ├── 解引用:*p │
│ ├── 自增自减:p++, p-- │
│ ├── 指针+整数:p + i │
│ ├── 指针-整数:p - i │
│ ├── 指针-指针:p1 - p2(得到元素间距) │
│ └── 指针比较:p1 == p2, p1 < p2 │
│ │
│ 指针与数组 │
│ ├── 一维数组:arr 等价于 &arr[0] │
│ ├── *(arr + i) 等价于 arr[i] │
│ ├── 二维数组:*(*(a + i) + j) 等价于 a[i][j] │
│ └── 行指针:int (*p)[列数] │
│ │
│ 指针与字符串 │
│ ├── char *p = "Hello"; │
│ └── 字符数组 vs 字符指针的区别 │
│ │
│ 指针数组 │
│ ├── int *arr[n]; (n个指针的数组) │
│ └── 常用于管理多个字符串 │
│ │
│ 二级指针 │
│ ├── int **pp = &p; │
│ └── 通过**pp逐级解引用得到最终值 │
│ │
│ 函数与指针 │
│ ├── 指针作函数参数(实现地址传递) │
│ ├── 返回指针的函数 │
│ └── main(int argc, char *argv[]) │
│ │
└─────────────────────────────────────────────────┘
学习建议: 指针是C语言中最核心也是最难掌握的概念。建议先理解内存地址的概念,再从最基本的"取地址+解引用"开始练习,逐步过渡到数组指针、函数指针等高级用法。多画内存图,多动手调试,是学好指针的关键。