C 语言中的指针
指针是一种变量,它存储的是另一个变量的内存地址。与直接存储值不同,指针存储的是 值 在内存(运行内存)中的位置(地址)。它是 C 语言中进行底层内存操作的核心工具。
指针的声明方式是指定其数据类型,并在变量名前加上星号(*)。
ini
指针声明语法格式为: 数据类型 *指针名;
数据类型表示该指针可以指向的变量类型。例如,int *ptr; 声明了一个指向整型变量的指针。
直接访问指针变量只会得到它所存储的地址。如果想获取该地址中存储的值,我们需要使用 * 运算符,这称为"解引用"操作符。
需要注意的是,* 在指针中有两种不同的用途:
- 用于声明指针变量;
- 作为运算符,用于获取指针所指向地址中的值。
c
#include <stdio.h>
int main()
{
// 普通变量
int var = 10;
// 指针变量 ptr,用于存储变量 var 的地址
int *ptr = &var;
// 直接访问 ptr 会输出一个地址
printf("%d", ptr);
return 0;
}
输出: 1751215308 这个十六进制整数(以 0x 开头)是内存地址。
让我们来了解一下上述程序的不同步骤。
1. 初始化指针
指针的初始化是通过使用取地址运算符(&)将其指向一个变量的地址来完成的。
语法:pointer_name = &variable;
初始化指针可以确保在使用前它指向一个有效的内存位置。
如果指针暂时不指向任何变量,也可以将其初始化为 NULL:int *ptr = NULL;
2. 解引用指针
我们必须先对指针进行解引用,才能访问该内存地址中存储的值。这一步通过解引用运算符(*)完成(该运算符与声明指针时使用的符号相同)。
c
#include <stdio.h>
int main() {
int var = 10;
// 指针变量的初始化, 存储变量 var 的地址
int* ptr = &var;
printf("%d", *ptr); // 解引用 ptr 以访问其中的值
printf("%p", ptr); // 打印指针
return 0;
}
输出
10
0x7ffc83d4f7cc
注意:之前我们曾用 %d 打印指针,但 C 专门提供了格式说明符 %p 用于打印指针。
3. 指针的大小
C 语言中指针的大小取决于机器操作系统的架构(位数),而不是它所指向的数据类型。
- 在 32 位系统上,所有指针通常占用 4 字节。
- 在 64 位系统上,所有指针通常占用 8 字节。
无论数据类型是什么(int*、char*、float* 等),指针的大小都是固定的。我们可以用 sizeof 运算符来验证这一点。
c
#include <stdio.h>
int main() {
int *ptr1;
char *ptr2;
// 使用 sizeof() 获取大小
printf("%zu\n", sizeof(ptr1));
printf("%zu", sizeof(ptr2));
return 0;
}
输出
8
8
所有指针大小相同,是因为它们存储的是内存地址,而不是数据本身。既然不同内存地址的存储需求相同,那么各类指针所需的内存空间也就一致。
注意:指针的实际大小可能因编译器和系统架构而异,但在同一系统上,所有数据类型的指针大小始终是统一的。
特殊类型的指针
有 4 种在不同场景下使用或提到的特殊指针类型:
第一种类型:NULL 指针
NULL 指针是指那些不指向任何内存地址的指针。
可以通过将指针赋值为 NULL 来创建。任何类型的指针都可以被赋值为 NULL。
这样我们就能通过判断指针是否等于 NULL,来检查它是否指向一个有效的内存位置。
c
#include <stdio.h>
int main() {
// 空指针
int *ptr = NULL;
return 0;
}
第二种类型:void 指针(空类型指针)
C 语言中的 void 指针是指类型为 void 的指针。 这意味着它们没有关联的具体数据类型。
因此也被称为"通用指针",可以指向任意类型的数据,并且可以通过类型转换变成任意其他类型的指针。
c
#include <stdio.h>
int main() {
// void 指针
void *ptr;
return 0;
}
第三种类型:野指针
野指针是指那些尚未被初始化的指针。
这类指针在程序中会引发各种问题,甚至可能导致程序崩溃。 如果通过野指针去修改值,可能会造成数据异常或数据损坏。
c
#include <stdio.h>
int main() {
// 野指针
int *ptr;
return 0;
}
第四种类型:悬空指针
当指针指向的内存已被释放(delete 或 free)后,该指针就称为悬空指针。
这种情况会导致程序出现意想不到的行为,也是 C 程序中常见的 bug 来源之一。
c
#include <stdio.h>
#include <stdlib.h>
int main() {
int* ptr = (int*)malloc(sizeof(int));
// 执行 free 后,ptr 变成悬空指针
free(ptr);
printf("Memory freed\n");
// 解除悬空状态
ptr = NULL;
return 0;
}
输出
Memory freed
第五种类型:常量指针(Constant Pointers)
在常量指针中,指针内部存储的内存地址是固定的,一旦定义后便不能再被修改。也就是说,它将始终指向同一个内存地址。
c
#include <stdio.h>
int main() {
int a = 90;
int b = 50;
// 创建一个常量指针
int* const ptr = &a;
// 尝试将其重新指向 b
ptr = &b; // 错误:不能给只读变量 'ptr' 赋值
return 0;
}
输出错误信息:
ini
solution.c: In function 'main':
solution.c:11:9: error: assignment of read-only variable 'ptr'
11 | ptr = &b;
| ^
第六种类型:函数指针
函数指针是一种存储函数地址的指针,它允许将函数作为参数传递,并可以动态调用函数。这在回调函数、事件驱动程序等技术中非常有用。
c
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main() {
// 声明一个与 add() 函数签名匹配的函数指针
int (*fptr)(int, int);
// 将 add() 的地址赋给指针
fptr = &add;
// 通过指针调用函数
printf("%d", fptr(10, 5));
return 0;
}
输出
15
第七种类型:多级指针
在 C 语言中,我们可以创建任意层级的多级指针,例如:***ptr3、****ptr4、******ptr5 等等。
其中最常用的是二级指针(指针的指针)。它存储的是另一个指针的内存地址。也就是说,它指向的不是数据本身,而是另一个指针。
c
#include <stdio.h>
int main() {
int var = 10;
// 指向 int 的指针
int *ptr1 = &var;
// 指向指针的指针(二级指针)
int **ptr2 = &ptr1;
// 使用这三种方式访问值
printf("var: %d\n", var);
printf("*ptr1: %d\n", *ptr1);
printf("**ptr2: %d", **ptr2);
return 0;
}
输出
makefile
var: 10
*ptr1: 10
**ptr2: 10
指针的优势
以下是 C 语言中指针的主要优势:
- 用于动态内存的分配与释放
- 借助指针可高效访问数组或结构体
- 便于直接访问内存地址
- 可构建链表、图、树等复杂数据结构
- 缩短程序代码并减少运行时间
指针带来的问题
指针容易产生错误,主要缺点如下:
- 若赋予指针错误的值,可能导致内存损坏
- 概念相对复杂,不易掌握
- 是 C 语言中内存泄漏的主要原因
- 通过指针访问的速度比直接访问变量慢
- 未初始化的指针可能引发段错误