指针
定义
指针是一个变量,存储另一个变量的内存地址,它允许直接访问和操作内存中的数据,使得程序能够以更灵活和高效的方式处理数据和内存。
获取变量地址:使用取地址符&
。
访问地址上的数据:使用解引用符*
。
例子1
指针是存储另一个变量地址的变量。通过使用取地址符&
和解引用符*
,我们可以灵活地访问和操作内存中的数据 。
#include <stdio.h>int main() { int var = 10; // 定义一个整数变量 int *p = &var; // 定义一个指向整数的指针,并将其初始化为变量 var 的地址 printf("Address of var: %p\n", &var); // 输出变量 var 的地址 printf("Address stored in pointer p: %p\n", p); // 输出指针 p 中存储的地址 printf("Value of var using pointer: %d\n", *p); // 通过指针 p 解引用获取 var 的值 // 修改 var 的值,通过指针 p *p = 20; printf("New value of var: %d\n", var); // 输出修改后的 var 的值 return 0;}
例子2
指针类型决定了它指向的变量类型,以及通过指针可以访问的数据大小。不同类型的指针在操作时会有不同的步长,比如:
int *p = (int *)a;
:将char
类型数组的首地址强制转换为int
指针类型。由于int
类型通常占用 4 个字节,因此通过 p
访问数据时,每次会读取 4 个字节的数据。
char *q = a;
:将char
类型数组的首地址赋值给char
指针类型。char
类型占用 1 个字节,因此通过 q
访问数据时,每次只会读取 1 个字节的数据。
#include <stdio.h>int main() { char a[12] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C}; int *p = (int *)a; // 将 char 数组的地址赋值给 int 指针 char *q = a; // 将 char 数组的地址赋值给 char 指针 // 使用 int 指针访问数据 printf("Using int pointer:\n"); for (int i = 0; i < 3; i++) { printf("p[%d]: 0x%08x\n", i, *(p + i)); } // 使用 char 指针访问数据 printf("Using char pointer:\n"); for (int i = 0; i < 12; i++) { printf("q[%d]: 0x%02x\n", i, *(q + i)); } return 0;}
一级指针和二级指针
一级地址(一级指针)
定义
一级地址指的是指向普通变量的指针,也就是直接存储变量的地址的指针。在C中,通常我们操作的是一级地址,例如指向整数、浮点数或其他基本数据类型的指针。
例子
int *ptr; // ptr 是一个指向整数的指针,是一级地址float *ptr_float; // ptr_float 是一个指向浮点数的指针,也是一级地址
二级地址(二级指针)
定义
二级地址是指向指针的指针,也就是存储另一个指针的地址的指针。在C中,可以通过二级指针来操作指向指针的指针,用来间接修改指针指向的值或者传递指针的引用。
例子
int x = 10;int *ptr1 = &x; // ptr1 是一个指向 x 的指针,是一级地址int **ptr2 = &ptr1; // ptr2 是一个指向 ptr1 的指针,是二级地址
例子
分析表达式*(*(&arr + 1) - 1)
的值:
int arr[5] = {1, 2, 3, 4, 5};
arr[5] = {1, 2, 3, 4, 5}
:这定义了一个包含 5 个整数的数组arr
,其元素分别是{1, 2, 3, 4, 5}
。&arr
:这是数组arr
的地址。需要注意的是,&arr
的类型是int (*)[5]
,即指向一个包含 5 个整数的数组的指针。&arr + 1
:这是&arr
指针加 1。在这个上下文中,&arr
被看作是一个指向整个数组的指针,因此&arr + 1
将指向紧随arr
之后的内存位置。也就是说,它指向arr
后面的内存地址,而不是数组中的下一个元素。&arr + 1
的类型仍然是int (*)[5]
。*(&arr + 1)
:这是对&arr + 1
解引用。&arr + 1
是一个指向数组的指针,对其解引用后,得到的仍然是一个指向数组末尾之后的指针。它的类型是int *
,即指向数组末尾之后的指针。*(&arr + 1) - 1
:这将刚才得到的指针减去 1。由于指针的减法是以元素为单位的,这个操作将指针向后移动一个int
的大小。因为*(&arr + 1)
指向的是数组arr
末尾之后的位置,减去 1 后,这个指针将指向数组arr
的最后一个元素。*(*(&arr + 1) - 1)
:最后,对指针*(&arr + 1) - 1
解引用,即获取这个指针所指向的值。这个指针现在指向arr
的最后一个元素,所以这个表达式的值就是arr
的最后一个元素的值。
因此,*(*(&arr + 1) - 1)
的值是5
,即数组arr
的最后一个元素。
指针的自增运算
*++p
这个表达式是先对指针p
进行自增操作,然后对新的指针进行解引用,得到新的指针所指向的值。
步骤:
++p
:指针p
先自增,指向下一个元素( 自增的是指针 )。*
:对自增后的指针解引用,得到新指针所指向的值。
++*p
这个表达式是对指针p
所指向的值进行解引用,然后对解引用得到的值进行自增操作。
步骤:
*p
:对指针p
进行解引用,得到指向的值。++
:对解引用得到的值进行自增操作( 自增的是指针指向的值 )。
函数地址与数组地址
函数地址
函数名就是函数的入口地址,因此可以直接把函数名赋给函数指针,也可以加上取地址符号&
。取地址符号&
是可选的,它只是显式地说明了编译器隐式执行的任务。获取函数的地址时,加不加取地址符号&
都可以。
#include <stdio.h>int test(int i) { return i;}int main(void) { int (*p1)(int) = test; int (*p2)(int) = &test; printf("%p, %p\n", (void *)p1, (void *)p2); return 0;}
数组地址
数组名本身是数组第一个元素的地址。例如,对于int
类型的数组来说,数组名arr
的类型是int*
,它是一个整型指针,不是数组指针。如果要取整个数组的地址,必须在数组名前面加上取地址符号&
,这样才能赋值给数组指针。获取数组的地址时,必须加上取地址符号&
。
在下面的例子中,p1
是一个整型指针,指向数组的第一个元素,而p2
是一个数组指针,指向整个数组。对p1
和p2
分别进行加 1 操作,p1
会加 4 个字节(假设 int
为 4 字节),而 p2
会加 20 个字节(5 个 int
)。
#include <stdio.h>int main(void) { int array[5] = {1, 2, 3, 4, 5}; int* p1 = array; // 指向数组第一个元素 int(*p2)[5] = &array; // 指向整个数组 printf("p1 %p\n", (void*)p1); printf("p2 %p\n", (void*)p2); printf("p1+1 %p\n", (void*)(p1+1)); // p1 加 1,增加 4 个字节(假设 int 为 4 字节) printf("p2+1 %p\n", (void*)(p2+1)); // p2 加 1,增加 20 个字节(5 个 int) return 0;}
指针和数组
不需要严格区分的情况
访问数组元素
对于数组和指针,都可以使用下标形式或指针形式来访问元素。
#include <stdio.h>int main(void) { char array[] ="hello world!"; char* p = array; array[1] = 'x'; // 使用下标访问数组 *(array + 1) = 'x'; // 使用指针形式访问数组 p[1] = 'y'; // 使用下标访问指针 *(p + 1) = 'y'; // 使用指针形式访问指针 printf("%s\n", array); // 输出: hyllo world! printf("%s\n", p); // 输出: hyllo world! return 0;}
作为函数参数传递
实参传递数组时,形参可以是数组或指针。实参传递指针时,形参也可以是数组或指针。编译器会将数组参数退化为指针。
#include <stdio.h>void printArray(int arr[], int size) { for (int i = 0; i < size; i++) { printf("%d", arr[i]); } printf("\n");}int main(void) { int array[] = {1, 2, 3, 4, 5}; printArray(array, 5); // 传递数组 return 0;}
需要严格区分的情况
使用sizeof
计算长度
sizeof(array)
返回数组的字节数。sizeof(pointer)
返回指针的大小(在64位系统中为8字节,在32位系统中为4字节)。
#include <stdio.h>int main(void) { char array[] ="hello world!"; char* p = array; printf("sizeof(array) = %lu\n", sizeof(array)); // 输出: 13 printf("sizeof(p) = %lu\n", sizeof(p)); // 输出: 8 (在64位系统中) return 0;}
计算数组长度
当数组作为参数传递时,在函数内部不能使用sizeof
计算数组长度,因为数组会退化为指针。
#include <stdio.h>void test(int arg[]) { printf("sizeof(arg) = %lu\n", sizeof(arg)); // 输出指针大小,例如在64位系统中为8}int main(void) { int array[10]; test(array); // 传递数组 return 0;}
声明外部变量
在一个文件中定义指针p
,在另一个文件中不能声明为数组。
//file1.c:int* p;// file2.cextern int* p; // 正确:声明为指针// extern int p[]; // 错误:不能声明为数组
指针常量和常量指针
指针常量
定义
不能改变指向的指针。也就是说,一旦初始化后,就不能再改变指向其它地址,但可以修改指针所指向地址上的内容。
例子
比如指针p
,因为它被const
修饰,所以p
不能被修改,它只能指向str
。如果强行对p
进行p++
操作,编译时就会报错。我们称指针p
为指针常量。尽管p
本身不能被修改,但可以通过p
来修改str
的内容,例如*p = 'H';
。
char str[] ="hello world!";char *const p = str;
常量指针
定义
不能改变所指向的值的指针。也就是说,通过这个指针不能修改它所指向的值,但可以修改指针的指向。
例子
p
是一个指向char
类型常量的指针。可以修改p
的指向,例如p = another_str;
,但不能通过p
修改其指向的内容,例如*p = 'H';
是不允许的。
const char *p ="hello world!";char const *p ="hello world!";
指针数组和数组指针
指针数组
定义
本质是一个数组,数组的每个元素都是指针。
例子1
int *p[4];
这一声明中,p
先与中括号结合,表示p
是一个有四个元素的数组,每个元素的类型都是int *
(指向整型的指针)。因此,我们称p
为指针数组。
int *p[4];
例子2
int a = 1, b = 2, c = 3, d = 4;int *p[4] = { &a, &b, &c, &d };for (int i = 0; i < 4; i++) { printf("%d", *p[i]);}// 输出:1 2 3 4
数组指针
定义
本质是一个指针,指向一个数组。
例子1
int (*p)[4];
这一声明中,p
先与*
结合,表示p
是一个指针,然后与中括号结合,表示p
指向一个有四个元素的数组,每个元素的类型都是int
。因此,我们称p
为数组指针。
int (*p)[4];
例子2
int arr[4] = {1, 2, 3, 4};int (*p)[4] = &arr;for (int i = 0; i < 4; i++) { printf("%d", (*p)[i]);}// 输出:1 2 3 4
函数指针和指针函数
函数指针
定义
本质是一个指针,指向一个函数。每个函数在内存中都有一个地址,函数调用就是跳转到这个地址开始执行,函数指针记录了这个地址的变量。
例子
p
是一个指针,指向一个函数,该函数有一个int
类型的参数,返回值是int
。可以通过函数名test(1)
来调用函数,也可以通过指针p
来调用p(1)
或者(*p)(1)
。
#include <stdio.h>int test(int i) { return i;}int main(void) { int res = 0; int (*p)(int) = test; res = test(1); printf("%d\n", res); res = p(1); printf("%d\n", res); res = (*p)(1); printf("%d\n", res); return 0;}
应用
往结构体中放入函数
C语言的结构体不支持成员函数,但可以通过函数指针实现类似的功能。结构体中可以定义一个函数指针,用来保存函数地址。函数名可以直接赋值给函数指针。C语言的结构体在使用时必须加上struct
关键字,而 C++ 可以省略。
#include <stdio.h>#include <stdlib.h>// 定义一个打印函数void print(int a, int b) { printf("%d %d\n", a, b);}// 定义一个结构体,其中包含一个函数指针struct Test { void (*p)(int, int);};int main(void) { // 动态分配结构体内存 struct Test* t = (struct Test *)malloc(sizeof(struct Test)); if (t == NULL) { fprintf(stderr,"Memory allocation failed\n"); return 1; } // 将函数指针指向打印函数 t->p = print; // 调用函数指针 t->p(3, 4); // 输出: 3 4 // 释放动态分配的内存 free(t); return 0;}
回调函数
是一种通过函数指针实现的技术,允许在一个函数中调用另一个函数。通常用于事件驱动编程或处理异步操作。
#include <stdio.h>// 定义一个回调函数类型typedef void (*Callback)(int);// 定义一个回调函数void myCallback(int value) { printf("Callback called with value: %d\n", value);}// 定义一个执行操作并调用回调函数的函数void performOperation(int x, Callback callback) { printf("Performing operation with value: %d\n", x); // 调用回调函数 callback(x);}int main(void) { // 使用回调函数 performOperation(5, myCallback); return 0;}
动态函数调用
在需要根据某些条件动态选择和调用不同函数时,函数指针非常有用。例如,在一个计算器程序中可以动态选择不同的操作函数。
#include <stdio.h>// 定义操作函数int add(int a, int b) { return a + b; }int subtract(int a, int b) { return a - b; }int multiply(int a, int b) { return a * b; }int divide(int a, int b) { return a / b; }int main(void) { // 定义一个函数指针数组 int (*operations[4])(int, int) = { add, subtract, multiply, divide }; int a = 10, b = 5; char op = '+'; // 根据操作符选择对应的函数 int (*operation)(int, int) = NULL; switch (op) { case '+': operation = operations[0]; break; case '-': operation = operations[1]; break; case '*': operation = operations[2]; break; case '/': operation = operations[3]; break; } if (operation != NULL) { printf("Result: %d\n", operation(a, b)); } else { printf("Invalid operation\n"); } return 0;}
实现多态行为
通过函数指针数组,可以在C语言中实现类似C++的多态行为。这种技术广泛应用于设计模式和框架中。
#include <stdio.h>// 定义一个基类结构体struct Shape { void (*draw)(struct Shape*);};// 定义一个派生类结构体struct Circle { struct Shape base; // 基类 int radius;};// 定义一个绘制函数void drawCircle(struct Shape* shape) { struct Circle* circle = (struct Circle*)shape; printf("Drawing a circle with radius: %d\n", circle->radius);}int main(void) { // 创建一个 Circle 对象 struct Circle c; c.base.draw = drawCircle; c.radius = 5; // 调用绘制函数 c.base.draw((struct Shape*)&c); return 0;}
实现状态机
是一种在不同状态之间转换的编程模式。可以通过函数指针实现状态之间的动态切换。
#include <stdio.h>// 定义状态函数类型typedef void (*StateFunction)();// 定义状态函数void stateA() { printf("State A\n");}void stateB() { printf("State B\n");}void stateC() { printf("State C\n");}int main(void) { // 定义一个状态函数指针数组 StateFunction states[3] = { stateA, stateB, stateC }; int currentState = 0; for (int i = 0; i < 5; i++) { // 调用当前状态函数 states[currentState](); // 切换到下一个状态 currentState = (currentState + 1) % 3; } return 0;}
代码跳转到指定位置执行
比如((void(*)())0)();
代表让程序跳转到地址为0的地方去运行。
解析步骤
(void (*)())
:这是一个类型转换,表示一个指向返回类型为void
、无参数的函数的指针。具体来说,void (*)()
是一种函数指针类型,它指向的函数没有参数并返回void
。0
:这是一个整数常量0
。在C中,可以将整数常量0
作为空指针常量。(void (*)())0
:这将整数0
转换为类型为void (*)()
的函数指针。换句话说,这是将0
解释为一个指向无参数、返回类型为void
的函数的指针。((void (*)())0)
:这只是对前面步骤的类型转换进行一次包裹,结果仍然是一个类型为void (*)()
的函数指针,指向0
地址。((void (*)())0)()
:这表示对0
地址的函数指针进行调用。具体来说,这是试图调用位于地址0
处的函数。
指针函数
定义
本质是一个函数,其返回值是一个指针。
例子
错误示例
下面这个例子是一个典型的错误,因为不能返回一个局部变量的地址。函数调用完毕后,局部变量的内存会被释放,即使返回了这个地址也不能使用。
int* test() { int array[5] = {0}; // 局部变量 return array; // 错误:返回局部变量的地址}int main(void) { int* p = test(); // 不安全 return 0;}
返回堆空间地址
#include <stdio.h>#include <stdlib.h>int* test() { int* array = (int*)malloc(sizeof(int) * 5); // 动态分配堆空间 if (array != NULL) { for (int i = 0; i < 5; ++i) { array[i] = i; // 初始化数组 } } return array; // 返回堆空间地址}int main(void) { int* p = test(); if (p != NULL) { for (int i = 0; i < 5; ++i) { printf("%d", p[i]); // 输出:0 1 2 3 4 } free(p); // 释放动态分配的内存 } return 0;}
返回全局变量地址
#include <stdio.h>int array[5]; // 全局变量int* test() { for (int i = 0; i < 5; ++i) { array[i] = i; // 初始化数组 } return array; // 返回全局变量地址}int main(void) { int* p = test(); for (int i = 0; i < 5; ++i) { printf("%d", p[i]); // 输出:0 1 2 3 4 } return 0;}
返回静态变量地址
#include <stdio.h>int* test() { static int array[5]; // 静态变量 for (int i = 0; i < 5; ++i) { array[i] = i; // 初始化数组 } return array; // 返回静态变量地址}int main(void) { int* p = test(); for (int i = 0; i < 5; ++i) { printf("%d", p[i]); // 输出:0 1 2 3 4 } return 0;}
指针函数指针
定义
是一个指向返回指针的函数的指针。它不仅是一个指向函数的指针,而且该函数返回的也是一个指针。
例子
#include <stdio.h>#include <stdlib.h>// 定义一个返回整数指针的函数int* allocateArray(int size) { int* array = (int*)malloc(size * sizeof(int)); // 动态分配内存 if (array != NULL) { for (int i = 0; i < size; ++i) { array[i] = i; // 初始化数组 } } return array; // 返回堆空间地址}// 定义一个指向返回整数指针的函数的指针类型typedef int* (*ArrayAllocator)(int);int main(void) { // 定义一个指向返回整数指针的函数的指针 ArrayAllocator allocator = allocateArray; int size = 5; // 使用指针函数指针调用函数 int* array = allocator(size); if (array != NULL) { for (int i = 0; i < size; ++i) { printf("%d", array[i]); // 输出:0 1 2 3 4 } printf("\n"); free(array); // 释放动态分配的内存 } return 0;}
悬挂指针和野指针
悬挂指针
定义
悬挂指针是指向已经释放(通过free
函数释放)或者已经超出作用域的内存的指针。当我们试图通过这样的指针访问或操作内存时,可能会导致未定义行为,因为那块内存可能已经被操作系统重新分配给其它程序使用了。
#include <stdio.h>#include <stdlib.h>void dangling_pointer_example() { int *p = (int *)malloc(sizeof(int)); *p = 42; printf("Value: %d\n", *p); // 输出42 free(p); // 释放内存 // p现在是悬挂指针,访问*p将导致未定义行为 // printf("Value: %d\n", *p); // 不安全,可能导致崩溃或未定义行为}int main() { dangling_pointer_example(); return 0;}
解决方法
立即将指针设为NULL:在释放内存后,将指针设置为NULL。
free(p);p = NULL;
避免返回局部变量的指针:不要返回局部变量的指针,因为它们在函数返回后将超出作用域。
int* incorrect_function() { int x = 42; return &x; // 返回局部变量的指针,错误}
野指针
定义
野指针是一个未初始化的指针,它的值是未知的,可能指向任意内存地址。当我们试图通过这样的指针访问或操作内存时,可能会导致未定义行为。
#include <stdio.h>void wild_pointer_example() { int *p; // 未初始化的指针 // *p = 42; // 不安全,可能导致崩溃或未定义行为}int main() { wild_pointer_example(); return 0;}
解决方法
在声明指针时初始化:声明指针时将其初始化为NULL或有效地址。
int *p = NULL;
确保在使用前分配内存:在使用指针之前,确保它已经被正确初始化和分配内存。
int *p = (int *)malloc(sizeof(int));if (p != NULL) { *p = 42;}