指针是C语言的灵魂,理解指针首先需要理解计算机的内存模型。在C语言中,指针本质上是一个变量,它存储的是另一个变量的内存地址。每个变量在内存中都有一个唯一的地址,指针通过保存这个地址来间接访问数据。
内存可以看作是一个巨大的字节数组,每个字节都有一个唯一的编号(地址)。当声明一个变量时,系统会根据变量类型分配相应大小的内存空间。例如:
int a = 5; // 分配4字节内存,存储值5
int *p = &a; // p存储a的地址
指针变量本身也占用内存空间(32位系统4字节,64位系统8字节),存储的是它所指向变量的地址。
指针的声明格式为:数据类型 *指针变量名
。关键操作符包括:
&
:取地址运算符,获取变量的内存地址*
:解引用运算符,访问指针指向的内存内容->
:通过指针访问结构体成员(等价于(*ptr).member
)int var = 10;
int *ptr = &var; // ptr指向var
printf("%d", *ptr); // 输出10
指针的类型决定了:
int arr[5] = {1,2,3,4,5};
int *p1 = arr; // 指向数组首元素
int (*p2)[5] = &arr; // 指向整个数组
// p1+1移动4字节,p2+1移动20字节
结构体指针是指向结构体变量的指针,它允许我们通过指针间接访问结构体成员。
struct Student {
char name[50];
int age;
float score;
};
struct Student stu = {"Tom", 18, 90.5};
struct Student *pStu = &stu;
// 访问成员两种方式
printf("%s", (*pStu).name); // 方式1
printf("%s", pStu->name); // 方式2(更常用)
结构体指针在函数参数传递中特别有用,可以避免复制整个结构体的开销:
void printStudent(const struct Student *s) {
printf("Name: %s, Age: %d", s->name, s->age);
}
结构体指针常与动态内存分配配合使用:
struct Student *createStudent(const char *name, int age) {
struct Student *stu = malloc(sizeof(struct Student));
if(stu) {
strcpy(stu->name, name);
stu->age = age;
}
return stu;
}
// 使用后记得释放
free(createStudent("Alice", 20));
结构体可以包含函数指针成员,实现类似面向对象的行为:
typedef struct {
int x, y;
void (*print)(int, int); // 函数指针成员
} Point;
void printXY(int x, int y) {
printf("(%d,%d)", x, y);
}
int main() {
Point p = {10, 20, printXY};
p.print(p.x, p.y); // 调用结构体中的函数指针
}
函数指针是指向函数的变量,它存储的是函数的入口地址。
int add(int a, int b) { return a + b; }
int (*funcPtr)(int, int) = add; // 定义函数指针并初始化
printf("%d", funcPtr(3,5)); // 通过指针调用函数
函数指针的三种定义方式:
// 方式1
typedef int(FUNC_TYPE)(int,int);
FUNC_TYPE *f1 = add;
// 方式2
typedef int(*FUNC_PTR)(int,int);
FUNC_PTR f2 = add;
// 方式3
int (*f3)(int,int) = add;
函数指针常用于回调机制和策略模式:
// 回调函数示例
void processArray(int *arr, int size, int (*process)(int)) {
for(int i=0; i<size; i++)
arr[i] = process(arr[i]);
}
int square(int x) { return x*x; }
int main() {
int arr[5] = {1,2,3,4,5};
processArray(arr, 5, square); // 传递square函数作为回调
}
函数指针数组可以实现"跳转表"或"命令模式":
void start() { printf("Starting...\n"); }
void stop() { printf("Stopping...\n"); }
void quit() { exit(0); }
int main() {
void (*commands[])() = {start, stop, quit};
while(1) {
printf("1.Start 2.Stop 3.Quit: ");
int choice;
scanf("%d", &choice);
commands[choice-1](); // 通过数组索引调用不同函数
}
}
双重指针(int **pp
)是指向指针的指针,常用于动态二维数组和修改指针参数。
void allocate(int **ptr, int size) {
*ptr = malloc(size * sizeof(int));
}
int main() {
int *arr;
allocate(&arr, 10); // 通过双重指针修改外部指针
free(arr);
}
三重指针(int ***ppp
)使用较少,主要用于三维数据结构或更复杂的间接访问。
数组名在大多数情况下会退化为指向首元素的指针,但二者有重要区别:
特性 | 数组 | 指针 |
---|---|---|
sizeof | 返回整个数组大小 | 返回指针本身大小 |
&操作 | 返回数组指针 | 返回指针的地址 |
赋值 | 不能直接赋值 | 可以重新指向 |
存储位置 | 栈或静态区 | 可指向任意位置 |
int arr[5] = {1,2,3,4,5};
int *p = arr;
printf("%zu %zu", sizeof(arr), sizeof(p));
// 输出20 8(64位系统)
虽然C语言没有内置智能指针,但了解C++的智能指针有助于理解指针管理:
std::shared_ptr
:共享所有权的智能指针std::unique_ptr
:独占所有权的智能指针std::weak_ptr
:不增加引用计数的观察指针// C++示例
std::shared_ptr<Base> p = std::make_shared<Derived>();
// 自动调用Derived的析构函数
未初始化指针:使用前必须初始化
int *p; // 错误
*p = 10;
空指针解引用:使用前检查NULL
int *p = NULL;
if(p) *p = 10; // 检查后再使用
内存泄漏:分配后忘记释放
void func() {
int *p = malloc(100);
// 忘记free(p);
}
野指针:指针指向已释放内存
int *p = malloc(100);
free(p);
*p = 10; // 危险!
指针越界:访问超出分配范围
int arr[5];
int *p = arr;
p[5] = 10; // 越界访问
初始化原则:定义指针时立即初始化
int *p = NULL; // 好习惯
分配与释放配对:每个malloc对应一个free
int *p = malloc(100);
// ...使用...
free(p);
p = NULL; // 避免野指针
指针参数检查:函数内检查传入指针有效性
void safeCopy(char *dst, const char *src, size_t len) {
if(!dst || !src) return;
// ...复制操作...
}
const正确性:合理使用const修饰指针
const int *p; // 指向常量(值不可改)
int * const p; // 常量指针(指向不可改)
const int * const p; // 都不可改
使用静态分析工具:如Valgrind检测内存问题
虽然C语言没有引用,但与指针密切相关的C++引用值得了解:
特性 | 指针 | 引用 |
---|---|---|
本质 | 存储地址的变量 | 变量的别名 |
空值 | 可以NULL | 必须绑定对象 |
重绑定 | 可以改变指向 | 初始化后不能改变 |
多级 | 支持多级指针 | 只有一级 |
操作符 | *和-> | . |
内存占用 | 占用存储空间 | 通常不额外占用空间 |
// C++示例
int x = 10;
int *p = &x; // 指针
int &r = x; // 引用
r = 20; // 直接修改x
*p = 30; // 通过指针修改x
typedef struct Node {
int data;
struct Node *next;
} Node;
Node* createNode(int val) {
Node *n = malloc(sizeof(Node));
if(n) {
n->data = val;
n->next = NULL;
}
return n;
}
void insert(Node **head, int val) {
Node *newNode = createNode(val);
newNode->next = *head;
*head = newNode;
}
void printList(Node *head) {
while(head) {
printf("%d ", head->data);
head = head->next;
}
}
typedef int (*CompareFunc)(int, int);
int ascending(int a, int b) { return a - b; }
int descending(int a, int b) { return b - a; }
void bubbleSort(int *arr, int n, CompareFunc cmp) {
for(int i=0; i<n-1; i++)
for(int j=0; j<n-i-1; j++)
if(cmp(arr[j], arr[j+1]) > 0) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
// 使用:bubbleSort(arr, 5, ascending);
typedef struct Shape {
void (*draw)(void);
void (*area)(void);
} Shape;
void circleDraw() { printf("Drawing circle\n"); }
void circleArea() { printf("Area=πr²\n"); }
void squareDraw() { printf("Drawing square\n"); }
void squareArea() { printf("Area=side²\n"); }
int main() {
Shape shapes[2] = {
{circleDraw, circleArea},
{squareDraw, squareArea}
};
for(int i=0; i<2; i++) {
shapes[i].draw();
shapes[i].area();
}
}
指针是C语言最强大也最危险的特征。通过本文我们系统性地探讨了:
拓展思考方向:
指针的学习曲线陡峭,但掌握后能极大提升编程能力。建议从简单案例开始,逐步深入,同时养成良好的内存管理习惯,避免常见陷阱。