C 语言(六)|指针基础:地址、解引用与指针运算
详细介绍C语言指针的核心概念,从内存地址到数组指针的底层逻辑,包含assert断言、排序算法指针实现、二级指针和指针数组等内容。
版权声明:本文为作者原创,版权归作者所有。如需转载或引用,请联系作者获得授权,并在使用时注明出处。
前言
指针是C语言的灵魂,也是其魅力所在。掌握指针意味着你真正理解了计算机内存管理机制。指针概念抽象、内容繁多、绕人烧脑是初学者的共同感受——概念抽象是因为理解指针需要"脑子转得过弯",内容繁多是因为涉及内存、地址、变量类型、数组等多知识点联动,绕人烧脑则是因为一不小心就写出bug。
指针从入门到精通需要持续学习、反复实践、不断深化。初学时的"懂了但没完全懂"是正常现象,关键是在后续学习中持续回顾、以理论指导实践、用实践验证理论,最终达到融会贯通。
一、内存与地址
1.1 理解内存
内存是计算机中用于存储数据的硬件设备,程序运行时所有数据都存储在内存中。以酒店为例理解内存:整栋大楼是内存,每个楼层是内存的一个单元,每个楼层有楼层号(类似内存地址),每个房间住着数据。
内存的基本单位是字节(byte)。每个字节都有唯一的地址,就像每个房间都有门牌号一样。
1.2 地址的概念
地址是内存中每个字节的编号,从0开始递增。例如:
- 地址0x0010:第一个字节
- 地址0x0011:第二个字节
- 地址0x0012:第三个字节
重要结论:地址是内存的"门牌号",通过地址可以找到对应的内存空间。
1.3 &操作符:获取地址
&(取地址操作符)用于获取变量的内存地址。
#include <stdio.h>
int main() {
int a = 10;
printf("a的地址:%p\n", &a);
printf("a的值:%d\n", a);
return 0;
}
运行结果(地址值因计算机而异):
a的地址:0x0061FE44
a的值:10
二、指针变量的定义与使用
2.1 什么是指针?
指针是一个变量,其值为另一个变量的地址。换句话说,指针"指向"另一个变量。
通俗理解:指针是地址的"收藏者",通过指针可以找到变量的位置(地址),进而访问变量。
2.2 指针变量的定义
语法格式:type *pointer_name;
type:指针指向的数据类型*:表明这是一个指针变量pointer_name:指针变量的名字
示例:
int *p; // 指向int类型变量的指针
char *pc; // 指向char类型变量的指针
float *pf; // 指向float类型变量的指针
double *pd; // 指向double类型变量的指针
注意:指针变量的类型决定了它能指向什么类型的变量,也决定了解引用时读取几个字节。
2.3 指针的初始化
指针必须在使用前初始化,否则可能指向未知地址,导致程序崩溃。
#include <stdio.h>
#include <assert.h>
int main() {
int a = 10;
int *p = &a; // 初始化指针p,指向变量a的地址
// 断言:确保指针非空
assert(p != NULL);
printf("a的地址:%p\n", &a);
printf("p的值:%p\n", p);
printf("p指向的值:%d\n", *p);
return 0;
}
2.4 解引用操作符(*)
*(解引用操作符)用于访问指针指向的变量。
#include <stdio.h>
#include <assert.h>
int main() {
int a = 10;
int *p = &a;
printf("a的值:%d\n", a); // 直接访问a
printf("*p的值:%d\n", *p); // 通过指针访问a
*p = 20; // 通过指针修改a的值
printf("修改后a的值:%d\n", a);
return 0;
}
运行结果:
a的值:10
*p的值:10
修改后a的值:20
2.5 指针变量的大小
指针变量的大小由系统地址位数决定,与指向的数据类型无关。
#include <stdio.h>
int main() {
int *p1;
char *p2;
float *p3;
double *p4;
printf("int* 的尺寸:%zu\n", sizeof(p1));
printf("char* 的尺寸:%zu\n", sizeof(p2));
printf("float* 的尺寸:%zu\n", sizeof(p3));
printf("double* 的尺寸:%zu\n", sizeof(p4));
printf("所有指针尺寸相同:%zu\n", sizeof(p1) == sizeof(p2));
return 0;
}
结论:32位系统指针占4字节,64位系统指针占8字节。
三、指针类型的意义
指针类型决定了指针的两个核心行为:
- 解引用权限:解引用时能访问的内存字节数
- 指针运算步长:指针±整数时地址偏移的字节数
#include <stdio.h>
int main() {
int a = 0x12345678; // 4字节整数
int *pa = &a;
char *pc = (char *)&a; // 强制转换为char*
printf("*pa = 0x%x\n", *pa); // 读取4字节:0x12345678
printf("*pc = 0x%x\n", *pc); // 读取1字节:0x78(取决于小端/大端)
printf("pa + 1 = %p\n", pa + 1); // 移动4字节
printf("pc + 1 = %p\n", pc + 1); // 移动1字节
return 0;
}
核心结论:指针的类型决定了指针"怎么走"(运算步长)和"看多少"(解引用权限)。
void*指针
指针类型中有一个特殊类型void *,可理解为无具体类型的指针(泛型指针):
- 优点:可以接收任意类型的地址
- 局限性:不能直接进行指针±整数和解引用运算
四、指针运算
4.1 指针与整数的运算
指针与整数相加减时,指针会移动"n个元素大小"的距离,而非简单的内存字节数。
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // 数组名就是数组首元素的地址
printf("p = %p, *p = %d\n", p, *p);
printf("p+1 = %p, *(p+1) = %d\n", p+1, *(p+1));
printf("p+2 = %p, *(p+2) = %d\n", p+2, *(p+2));
return 0;
}
重要结论:
p + n:指针向后移动n个"元素"的位置p - n:指针向前移动n个"元素"的位置
4.2 指针之间的减法
两个指针相减,结果是它们之间"元素的个数",而非地址的数值差。
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *p1 = &arr[0];
int *p2 = &arr[3];
printf("p2 - p1 = %td\n", p2 - p1); // 输出3
printf("p1 - p2 = %td\n", p1 - p2); // 输出-3
return 0;
}
4.3 指针之间的关系比较
指针可以进行比较运算,比较的是地址的先后顺序。
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;
int *end = &arr[5];
while (p < end) {
printf("%d ", *p);
p++;
}
printf("\n");
return 0;
}
运行结果:10 20 30 40 50
五、数组与指针
5.1 数组名的理解
关键结论:数组名是数组首元素的地址(有两个例外)。
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
printf("arr = %p\n", arr);
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr = %p\n", &arr);
printf("arr+1 = %p\n", arr + 1);
printf("&arr[0]+1 = %p\n", &arr[0] + 1);
printf("&arr+1 = %p\n", &arr + 1); // 跳过整个数组
return 0;
}
重要区分:
arr + 1:跳过一个整型元素(4字节)&arr + 1:跳过整个数组(5个整型 = 20字节)
两个例外:
sizeof(数组名):计算整个数组的内存大小&数组名:取整个数组的地址
5.2 数组的访问方式
数组元素可以通过下标和指针两种方式访问。
#include <stdio.h>
#include <assert.h>
void Print(int *ptr, int n) {
// 断言:确保指针非空 + 数组长度合法
assert(ptr != NULL);
assert(n > 0);
int* q = ptr;
while (ptr < q + n) {
printf("%d ", *ptr);
ptr++;
}
printf("\n");
}
int main() {
int arr[] = {10, 20, 30, 40, 50};
int len = sizeof(arr) / sizeof(arr[0]);
printf("下标访问:arr[2] = %d\n", arr[2]);
printf("指针访问:*(arr+2) = %d\n", *(arr + 2));
Print(arr, len); // 使用指针遍历打印
return 0;
}
结论:arr[i]、(arr+i)、(p+i)、p[i]四种方式完全等价。
5.3 模拟strlen(指针实现)
#include <stdio.h>
#include <assert.h>
size_t my_strlen(char *p) {
// 断言:确保传入的字符串指针非空
assert(p != NULL);
char* q = p;
while (*p) {
p++;
}
return (p - q); // 指针相减,得到元素个数
}
int main() {
char arr[] = "abcdef";
size_t len = my_strlen(arr);
printf("%zu\n", len); // 输出6
return 0;
}
六、const修饰指针
6.1 const修饰指针的三种情况
#include <stdio.h>
int main() {
int a = 10, b = 20;
// 情况1:const修饰指针指向的内容
const int *p1 = &a;
// *p1 = 30; // 错误:不能通过p1修改a的值
p1 = &b; // 正确:p1可以指向其他地址
// 情况2:const修饰指针本身
int *const p2 = &a;
*p2 = 30; // 正确:可以通过p2修改a的值
// p2 = &b; // 错误:p2不能指向其他地址
// 情况3:const同时修饰指针和指针指向的内容
const int *const p3 = &a;
// *p3 = 30; // 错误
// p3 = &b; // 错误
return 0;
}
6.2 记忆技巧
- **const在
*左边**:修饰指向的内容,内容不能改 - **const在
*右边**:修饰指针本身,指针不能改
七、野指针与assert断言
7.1 野指针的产生原因
- 指针变量未初始化:
int p; p = 10;(p为随机地址) - 指针越界访问:数组遍历超出范围
- 指针指向的内存被释放:内存释放后未将指针置空
7.2 野指针的规避方法
- 指针定义时立即初始化(赋值合法地址或NULL)
- 指针越界检查:遍历内存时严格限制范围
- 内存释放后立即置空:
p = NULL; - 解引用前判空:
if(p != NULL) { *p = 20; }
7.3 assert断言
assert是调试阶段的断言检查,用于快速定位指针空值、条件非法等问题,是C语言调试的"利器"。
核心特性:
- 自动标识文件名和出错行号,快速定位问题
- 无需修改代码,通过宏定义即可开启/关闭所有assert()语句
- 仅在调试模式生效,不影响发布版本的性能
基础使用:
#include <stdio.h>
#include <assert.h>
void Swap(int *x, int *y) {
// 断言:确保传入的指针非空
assert(x != NULL);
assert(y != NULL);
int temp = *x;
*x = *y;
*y = temp;
}
int main() {
int a = 10, b = 20;
int *p = NULL;
Swap(p, &b); // 触发assert断言
printf("a=%d, b=%d", a, b);
return 0;
}
运行结果(触发断言):
Assertion failed: (x != NULL), function Swap, file test.c, line 6.
开启/关闭assert:
- 启用:直接引入
<assert.h>(默认启用) - 禁用:在
<assert.h>前定义宏NDEBUG
禁用示例:
#include <stdio.h>
#define NDEBUG // 定义NDEBUG,禁用所有assert
#include <assert.h>
void Swap(int *x, int *y) {
// 断言被禁用,不会执行检查
assert(x != NULL);
assert(y != NULL);
int temp = *x;
*x = *y;
*y = temp;
}
最佳实践:
- 解引用指针前,必加
assert(p != NULL) - 数组传参时,断言数组长度
assert(n > 0) - 仅用于"调试阶段的非法条件检查",不替代正常的业务逻辑判断(如if)
八、指针作为函数参数
8.1 传值调用 vs 传址调用
#include <stdio.h>
#include <assert.h>
// 传值调用:交换失败
void swap_fail(int x, int y) {
int temp = x;
x = y;
y = temp;
}
// 传址调用:交换成功
void swap_success(int *px, int *py) {
// 断言:确保指针非空
assert(px != NULL);
assert(py != NULL);
int temp = *px;
*px = *py;
*py = temp;
}
int main() {
int a = 10, b = 20;
printf("交换前:a=%d, b=%d\n", a, b);
swap_fail(a, b);
printf("传值交换后:a=%d, b=%d\n", a, b); // a和b不变
swap_success(&a, &b);
printf("传址交换后:a=%d, b=%d\n", a, b); // a和b交换成功
return 0;
}
8.2 指针参数的使用场景
- 修改外部变量:函数需要修改外部变量的值
- 减少复制开销:大型结构体直接传地址而非传值复制
- 返回多个值:函数需要返回多个结果
九、排序算法的指针实现
9.1 冒泡排序
#include <stdio.h>
#include <assert.h>
void BubbleSort(int *arr, int n) {
assert(arr != NULL);
assert(n > 0);
for(int i=0; i<n-1; i++) {
int flag = 1; // 优化:标记是否已有序
for(int j=0; j<n-1-i; j++) {
if(*(arr+j) > *(arr+j+1)) {
int temp = *(arr+j);
*(arr+j) = *(arr+j+1);
*(arr+j+1) = temp;
flag = 0;
}
}
if(flag == 1) break;
}
}
int main() {
int arr[6] = {5,3,4,1,2,6};
int n = sizeof(arr)/sizeof(arr[0]);
BubbleSort(arr, n);
for(int i=0; i<n; i++) {
printf("%d ", arr[i]); // 输出1 2 3 4 5 6
}
return 0;
}
9.2 选择排序
#include <stdio.h>
#include <assert.h>
void SelectSort(int *arr, int n) {
assert(arr != NULL);
assert(n > 0);
for (int i = 0; i < n - 1; i++) {
int *min_ptr = arr + i;
for (int j = i + 1; j < n; j++) {
if (*(arr + j) < *min_ptr) {
min_ptr = arr + j;
}
}
if (min_ptr != arr + i) {
int temp = *(arr + i);
*(arr + i) = *min_ptr;
*min_ptr = temp;
}
}
}
int main() {
int arr[6] = {5, 3, 4, 1, 2, 6};
int n = sizeof(arr) / sizeof(arr[0]);
SelectSort(arr, n);
for(int i=0; i<n; i++) {
printf("%d ", arr[i]); // 输出1 2 3 4 5 6
}
return 0;
}
9.3 插入排序
#include <stdio.h>
#include <assert.h>
void InsertSort(int *arr, int n) {
assert(arr != NULL);
assert(n > 0);
for (int i = 1; i < n; i++) {
int insert_val = *(arr + i);
int *pre_ptr = arr + i - 1;
while (pre_ptr >= arr && *pre_ptr > insert_val) {
*(pre_ptr + 1) = *pre_ptr;
pre_ptr--;
}
*(pre_ptr + 1) = insert_val;
}
}
int main() {
int arr[6] = {5, 3, 4, 1, 2, 6};
int n = sizeof(arr) / sizeof(arr[0]);
InsertSort(arr, n);
for(int i=0; i<n; i++) {
printf("%d ", arr[i]); // 输出1 2 3 4 5 6
}
return 0;
}
十、二级指针
10.1 二级指针的定义
二级指针是指向指针变量的指针。
#include <stdio.h>
#include <assert.h>
int main() {
int a = 10, b = 20;
int *p = &a;
int **pp = &p; // 二级指针
// 断言:确保指针非空
assert(p != NULL);
assert(pp != NULL);
*pp = &b; // 等价于p = &b,修改一级指针指向
**pp = 30; // 等价于*p = 30,修改b的值
printf("%d", b); // 输出30
return 0;
}
核心符号使用:
*pp:解引用二级指针,得到一级指针p的值**pp:二次解引用,得到a的值
10.2 应用场景
- 修改一级指针的指向
- 遍历指针数组
十一、指针数组
11.1 指针数组的定义
指针数组是存放指针的数组(本质是数组,元素为指针)。
#include <stdio.h>
#include <assert.h>
int main() {
int a = 1, b = 2, c = 3;
int *pArr[3] = {&a, &b, &c}; // 指针数组
for(int i=0; i<3; i++) {
assert(pArr[i] != NULL);
printf("%d ", *(pArr[i])); // 输出1 2 3
}
return 0;
}
11.2 常用场景
存放多个字符串的首地址(比二维字符数组节省内存):
#include <stdio.h>
int main() {
char *strArr[3] = {"abc", "def", "ghi"};
for(int i=0; i<3; i++) {
printf("%s\n", strArr[i]);
}
return 0;
}
11.3 指针数组模拟二维数组
#include <stdio.h>
#include <assert.h>
int main() {
int row1[3] = {1,2,3};
int row2[3] = {4,5,6};
int row3[3] = {7,8,9};
int *pArr[3] = {row1, row2, row3}; // 指针数组
for(int i=0; i<3; i++) {
assert(pArr[i] != NULL);
for(int j=0; j<3; j++) {
printf("%d ", pArr[i][j]);
}
printf("\n");
}
return 0;
}
核心区别:
- 真正二维数组:内存连续,所有行紧密排列
- 指针数组模拟:内存不连续,每行是独立一维数组
十二、总结
本篇我们学习了C语言指针的核心知识:
- 内存与地址:内存是存储数据的空间,地址是内存的"门牌号"
- 指针变量:存储地址的变量,通过解引用访问指向的数据
- 指针类型:决定解引用读取的字节数和指针运算的单位
- 指针运算:与整数加减、指针相减、指针比较
- 数组与指针:数组名是首元素地址,指针可以遍历数组
- const修饰指针:限制指针或指向内容的可修改性
- assert断言:调试阶段的非法条件检查利器
- 指针作为函数参数:实现传址调用,修改外部变量
- 排序算法的指针实现:冒泡、选择、插入排序
- 二级指针与指针数组:指针的进阶应用
指针是C语言的精髓,掌握指针意味着你真正理解了计算机的内存管理机制。继续学习指针的高级用法,如函数指针、动态内存管理等,你将开启C语言编程的新境界!