1、指针基础
1.1、指针的声明
指针变量用于存储内存地址,声明时需要指定指向的数据类型:
/* 指针的声明 */
char* c; /* 指向字符的指针 */
int* p; /* 指向整型的指针 */
float* f; /* 指向浮点数的指针 */
指针只能指向某种特定类型的对象,即每个指针都必须指向某种特定的数据类型。例如上面示例中,“int *p;”就是说明p是一个指向整型对象的指针。但是有一个例外,指向void类型的指针可以存放指向任何类型的指针,但是它不能间接引用其自身。
指向任何对象的指针都可以转换为void *类型,且不会丢失信息。如果将结果再转换为初始指针类型,则可以恢复初始指针。指针可以被赋值为void *类型的指针,也可以赋值给void *类型的指针,并可与void *类型的指针进行比较。
但是要注意一个问题,上面的三个指针仅是作为声明,一般情况下不要这样写,是比较危险的,因为三个指针变量指向哪里我们无法确定。如果覆盖到其他的内存区域,甚至是系统正在使用的关键区域,十分危险,但是这种情况系统一般会驳回程序的运行,此时程序会被中止并报错。万一覆盖到一个合法的地址,那么接下来的赋值就会导致一些有用的数据被莫名其妙地修改,此类bug是十分不好排查的,所以使用指针的时候一定要注意初始化。
int a = 10;
int *p = &a; /* 声明指针时立即初始化 */// 或者将指针指向NULL
int *pa = NULL; /* 暂时不知道该指向哪的时候先指向NULL */
在C语言中,NULL被称为空指针,该指针不指向任何数据。本身是一个宏定义:
#define NULL ((void *)0)
在大部分操作系统中,地址0通常都是一个不被使用的地址,如果一个指针指向NULL,意味着不指向任何东西。
1.2、取地址操作符(&)
通过 & 获取变量的内存地址:
/* 通过 & 获取变量的内存地址 */
int a = 10;
int* p = &a; /* p存储变量a的地址 */
printf("address = %p\n", p); /* address = 0000008655AFF774 */
printf("address = %p\n", &a); /* address = 0000008655AFF774 */
1.3、解引用操作符(*)
通过 * 访问指针指向的内存内容:
/* 通过 * 访问指针指向的内存内容 */
int a = 10;
int* p = &a;
printf("a = *p = %d\n", *p); /* 输出10(通过p访问a的值) */
直接通过变量名来访问变量的值称为直接访问,而通过指针这样的形式访问变量值称之为间接访问,因此解引用操作符也可称为间接运算符。
2、指针的常见操作
2.1、指针赋值
指针可以直接指向另一变量的地址:
int a = 10, b = 20;
int* p = &a;
printf("a = *p = %d\n", *p); /* 输出10(通过p访问a的值) */
p = &b;
printf("*p = %d\n", *p); /* 输出20(通过p访问b的值) */
2.2、指针算术运算
指针支持加减运算(按数据类型大小移动地址):
/* 指针算术运算 */
int arr[3] = { 10, 20, 30 };
int* p = arr; /* p指向数组首元素 */
p++; /* p指向数组第二个元素arr[1] */
printf("%d\n", *p); /* 输出20,对应arr[1]的值 */
上述示例中,p 是指向数组首元素的指针,那么 p++ 将对 p 进行自增运算并指向下一个元素arr[1];而 p += i 将对p进行加i的增量运算,使其指向指针 p 当前所指向的元素之后的第 i 个元素。
指针的减法运算的意义:如果 p1 和 p2 指向相同数组中的不同元素,且 p1 < p2,那么 p2-p1+1 指向的元素之间的元素数目。可以编写一个获取字符串的长度的自定义函数strlen()。
int strlen(char* s)
{char* p = s;while (*p != '\0')p++;return p - s;
}int main()
{char s[] = "Hello World!"; /* 定义一个数组 */char* p = "Hello World!"; /* 定义一个指针 */printf("strlen(s) = %d\n", strlen(s)); /* 12 */printf("strlen(p) = %d\n", strlen(p)); /* 12 */return 0;
}
上述示例中,需要特别注意 s 和 p 的区别,尽管它们的长度是一样的。但是 s 只是一个存放初始化字符串以及空字符 '\0' 的一维数组,只是 s 作为数组首元素的地址进行传递,数组中的单个字符可以进行修改。但是 p 是一个指针,始终指向同一个存储位置,其初值指向一个字符串常量,之后它可以被修改以指向其他的地址,但不允许修改字符串的内容。
2.3、指针比较
在某些情况下指针可以通过“==”、“=”、“>”、“<”来比较地址的大小:
/* 指针比较 */
int arr[5];
int* p1 = &arr[0];
int* p2 = &arr[4];
if (p1 < p2) {printf("p1的地址在p2之前!\n");
}
上述指针进行比较的前提是,指针 p1 和 p2 是指向同一数组中不同元素的指针。任何指针与 0 进行相等或不等的比较运算都有意义。但是指向不同数组元素的指针之间的算术或比较运算没有定义。有一个特例:指针的算术运算中可使用数组最后一个元素的下一个元素的地址。
3、常量指针和指针常量
常量指针和指针常量的记忆技巧:
- 常量指针:const在 * 左侧,如const int *p,表示“指向常量的指针”。记忆口诀:内容不可变,指针可变。
- 指针常量:const在 * 右侧,如int* const p,表示“指针本身是常量”。记忆口诀:指针不可变,内容可变。
3.1、常量指针(Pointer to Constant)
常量指针表示指针指向的内容是常量,不能通过该指针修改指向的值,但指针本身可以指向其他地址。其声明语法为:
const 数据类型 *指针名;
// 或
数据类型 const *指针名;
/* 常量指针 */
int a = 10;
const int* p = &a; /* p是常量指针,指向的int不可变 */// *p = 20; /* 错误:不能通过p修改a的值 */
a = 20; /* 正确:直接修改变量a的值是允许的 */
printf("*p = %d\n", *p); /* *p = 20 */int b = 30;
p = &b; /* 正确:指针可以指向其他变量 */
printf("*p = %d\n", *p); /* *p = 30 */
特点:
- 指向的值不可通过指针修改。
- 指针本身可以重新指向其他地址。
- 常用于函数参数,避免意外修改外部数据。
void printData(const int* arr, int size)
{for (int i = 0; i < size; i++) {printf("%d ", arr[i]); /* 确保不会修改数组内容 */}
}
3.2、指针常量(Constant Pointer)
指针常量表示指针本身是常量,即指针的指向不可变(必须初始化),但可以通过指针修改指向的值。其声明语法为:
数据类型 *const 指针名 = 初始地址;
/* 指针常量 */
int a = 10;
int* const p = &a; /* p是指针常量,指向不可变 */
printf("p = %p\n", (void*)p);*p = 20; /* 正确:可以通过p修改a的值 */
printf("a = %d\n", a); /* a = 20 */
printf("p = %p\n", (void*)p);int b = 30;
// p = &b; /* 错误:指针的指向不可变 */
特点:
- 指针的指向固定,不可修改。
- 可以通过指针修改指向的值。
- 常用于固定访问某个内存地址的场景(如硬件寄存器)。
int reg = 0x1000;
int* const REG_ADDR = (int*)reg; /* 指向固定地址 */*REG_ADDR = 1; /* 修改寄存器值 */
3.3、指向常量的指针常量(Constant Pointer to Constant)
指针和指向的内容都不可修改。其声明语法为:
const 数据类型 *const 指针名 = 初始地址;
/* 指向常量的指针常量 */
const int a = 10;
const int* const p = &a; /* 指针和指向的内容均不可变 */// *p = 20; /* 错误:不可修改值 */
int b = 30;
// p = &b; /* 错误:不可修改指向 */
特点:
- 指针的指向和指向的值均不可变。
- 适用于完全只读的场景(如全局配置数据)。
const float* const PI = &3.1415926;
4、指针与数组
4.1、数组名的指针特性
数组名就是数组首元素的地址:
/* 数组名的指针特性 */int arr[5] = { 10, 20, 30, 40, 50 };int* p = arr; /* p指向arr[0] */printf("*p = %d\n", *p); /* *p = 10 */printf("arr = %p\n", arr); /* arr = 00000016BAEFF7D8 */printf("p = %p\n", p); /* p = 00000016BAEFF7D8 */
也就是说,“int *p = arr;”和“int *p = &arr[0];”是等价的。
4.2、通过指针遍历数组
如果指针变量p指向数组中的某个特定元素,可根据指针运算的定义,p+1 将转向下一个元素,p+i 将指向数组元素之后的第 i 个元素,而 p-i 将指向所致数组元素之前的第 i 个元素。
/* 通过指针遍历数组 */
int arr[5] = { 10, 20, 30, 40, 50 };
int* p = arr;
for (int i = 0; i < 5; i++) {printf("%d ", *(p + i)); /* 10 20 30 40 50 */
}
上述示例中,指针p指向arr,也就是arr[0],所以解引用*(p+i)得到的是数组元素a[i]的内容。
还有另一等价的遍历数组元素写法:
int arr[5] = { 10, 20, 30, 40, 50 };
for (int* p = arr; p < arr + 5; p++) {printf("%d ", *p); /* 输出10 20 30 40 50 */
}
上述示例中使用了指向数组最后一个元素的下一个元素的地址 arr+5,这是被允许使用的。
5、指针与函数
5.1、指针作为函数参数(传址调用)
由于C语言中是以传值的方式将参数值传递给被调用函数。也就是说,被调用函数不能直接修改主调函数中变量的值。 但是可以通过指针修改函数外部的变量:
void swap(int* a, int* b)
{int temp = *a;*a = *b;*b = temp;
}int main()
{int x = 10, y = 20;swap(&x, &y); printf("x = %d, y = %d\n", x, y); /* x=20, y=10 */return 0;
}
上述示例是将swap()函数的所有参数声明为指针,并且通过指针来间接地访问它们所指向的操作数,指针参数使得被调用函数能够访问和修改主调函数中对象的值。
如果上述示例中的swap()函数的形式为swap(int a, int b),这样是无法达到目的的,此形式的swap()函数仅仅交换了a和b的副本的值。
void swap(int a, int b)
{int temp = a;a = b;b = temp;
}int main()
{int x = 10, y = 20;swap(x, y);printf("x = %d, y = %d\n", x, y); /* x = 10, y = 20 */return 0;
}
5.2、返回指针的函数(指针函数)
指针作为函数的返回值,是指函数的返回类型是一个指针类型,即函数返回某个数据的内存地址。这种函数通常用于动态分配内存或返回数据结构的地址。函数可以返回指针,但需要确保指向的内存有效:
int* findMin(int* arr, int size)
{int* min = arr;for (int i = 0; i < size; i++) {if (arr[i] < *min)min = &arr[i];}return min; /* 返回最小值的地址 */
}int main()
{int arr[5] = { 3, 8, 6, 2, 4 };int* p = findMin(arr, 5);printf("p = %p\n", p); /* p = 0000009D194FF724 */printf("&arr[3] = %p\n", &arr[3]); /* &arr[3] = 0000009D194FF724 */return 0;
}
注意事项:
- 内存管理:如果返回动态分配的指针,一定要注意,调用者需要负责释放内存(如free)。
- 局部变量:不要返回局部变量的地址,因为函数结束后内存失效。
- 野指针:一定要确保返回的指针是指向的有效内存。
6、动态内存管理
6.1、malloc和free
动态分配和释放内存:
/* 动态分配和释放内存 */
int* p = (int*)malloc(5 * sizeof(int)); /* 分配5个int的空间 */
if (p != NULL) {for (int i = 0; i < 5; i++) {p[i] = i + 1;printf("p[%d] = %d\n", i, p[i]); /* 1 2 3 4 5 */}free(p); /* 释放内存 */
}
malloc并不是从一个在编译是就确定的固定大小的而数组中分配存储空间,而是在需要时向操作系统申请空间。所以,malloc管理的空间不一定时连续的。这样,空闲存储空间以空闲块链表的方式组织,每个块包含一个长度、一个指向下一块的指针以及指向自身存储空间的指针。当有申请请求时,malloc将扫描空闲块链表,直到找到一个足够大的块为止。如果块太大,则将它分成两部分:大小合适的块返回给用户,剩下的部分留在空闲块链表中。如果找不到一个足够大的块,则向操作系统申请一个大块并加入到空闲块链表中。
释放过程也是首先搜索空闲块链表,以找到可以插入被释放块的合适位置。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合成一个更大的块,这样存储空间就不会有太多的碎片。
6.2、避免内存泄漏
确保每次malloc都有对应的free:
/* 确保每次malloc都有对应的free */int* p = malloc(sizeof(int));int a = 10;p = &a;free(p); /* 释放后,p变为野指针 */p = NULL; /* 推荐置空 */
7、高级指针操作
7.1、指针数组
由于指针也是变量,所以它们也可以像其他变量一样存储在一个数组中。下面示例中arr是一个存储指针的数组:
/* 指针数组 */
int a = 1, b = 2, c = 3;
int* arr[3] = { &a, &b, &c }; /* arr是一个指针数组,存储3个int指针 */
内存布局:指针数组的每个元素独立指向内存中的某个地址。
arr[0] --> &a
arr[1] --> &b
arr[2] --> &c
典型应用——存储多个字符串(字符串数组):
char* names[] = { "Alice", "Bob", "Klein" };
printf("names[0] = %s\n", names[0]); /* names[0]指向"Alice" */
printf("names[1] = %s\n", names[1]); /* names[1]指向"Bob" */
printf("names[2] = %s\n", names[2]); /* names[2]指向"Klein" */
7.2、数组指针
数组指针是一个指针 ,指向一个完整的数组。它保存的是整个数组的起始地址。下面示例中的 p 是一个数组指针。
int nums = { 10, 20, 30 };
int (*p)[3] = &nums; /* p是一个数组指针,指向包含3个int的数组 */
内存布局:数组指针指向整个数组的起始地址:
p --> nums[0](整个数组的首地址)
典型应用1、操作二维数组。
/* 数组指针的应用 -- 操作二维数组 */
int matrix[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
int (*p)[4] = matrix; /* p指向二维数组的第一行(即matrix[0]) */
printf("%d\n", (*p)[2]); /* 输出3(matrix[0][2]) */
printf("%d\n", (*(p+1))[3]); /* 输出8(matrix[1][3] */
printf("%d\n", (*p)[10]); /* 输出11(matrix[2][2],即二维数组第11个元素的值 */
典型应用2、传递二维数组到函数。
/* 数组指针的应用 -- 传递二维数组到函数 */
void printMatrix(int (*mat)[4], int rows)
{for (int i = 0; i < rows; i++) {for (int j = 0; j < 4; j++) {printf("%d ", mat[i][j]);}printf("\n");}
}int main()
{int matrix[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };printMatrix(matrix, 3);return 0;
}
指针数组和数组指针的关键区别:
特性 | 指针数组 | 数组指针 |
本质 | 数组,元素是指针 | 指针,指向一个数组 |
声明语法 | int *arr[5]; | int (*p)[5]; |
内存占用 | 多个指针的空间(如5个指针) | 单个指针的空间 |
用途 | 存储多个独立指针(如字符串数组) | 操作整个数组(如二维数组的行指针) |
访问方式 | arr[i]访问第 i 个指针 | (*p)[i]访问数组的第 i+1 个元素 |
运算符优先级:
- int *arr[5]:[ ]优先级高于*,因此是指针数组。
- int (*p)[5]:()强制*优先结合,因此是数组指针。
常见错误:
- 错误赋值:
int nums[3] = { 1, 2, 3 };
int (*p)[3] = nums; /* 错误,nums是首元素地址,类型为int* */
int (*p)[3] = &nums; /* 正确,&nums是整个数组的地址 */
- 越界访问:
int nums[3] = { 1, 2, 3 };
int (*p)[3] = &nums;
printf("%d\n", (*p)[3]); /* 越界访问(数组索引为0~2) */
7.3、多级指针(指针的指针)
在C语言中,指针的指针(即二级指针)是一种指向指针的指针变量,常用于处理多级间接访问和动态内存管理。
/* 多级指针(指针的指针) */
int a = 10;
int* p = &a;
int** pp = &p; /* pp指向指针p */
printf("p的地址:%p\n", (void*)pp); /* 输出p的地址 */
printf("a的地址:%p\n", (void*)*pp); /* 输出a的地址 */
printf("a = **pp = %d", **pp); /* 输出10 */
指针的指针存储的是另一个指针的地址,声明时使用两个星号(**)。示例中,pp 是 int 型指针的指针,指向 p。在解引用的时候,*pp 获取的是 p 的值(即a的地址),而 **pp 是获取 a 的值。
指针的指针的应用场景:
- 动态修改指针的值
若需要在函数中修改指针指向的地址,需要传递指针的指针:
void allocate(int** ptr)
{*ptr = malloc(sizeof(int)); /* 修改外部指针指向新内存 */**ptr = 100; /* 设置值 */
}int main()
{int* p = NULL;allocate(&p); /* 传递指针的地址 */printf("%d\n", *p); /* 输出100 */free(p);return 0;
}
- 字符串数组(命令行参数)
main函数的参数char **argv是典型的指针的指针用法:
int main(int argc, char **argv)
{for (int i = 0; i < argc; i++) {printf("参数 %d:%s\n", i, argv[i]); /* argv[i]是char*类型 */}return 0;
}
常见错误:
- 野指针:在动态申请后,释放内存后未置空指针的指针。
free(*pp);
*pp = NULL; /* 避免野指针 */
- 错误的解引用层级:
int **pp = NULL;
*pp = 10; /* 错误:*pp是int*类型,不能直接赋值int */
**pp = 10; /* 正确:需先确保*pp指向有效内存 */
- 类型不匹配:
int a = 10;
int *p = &a;
char **pp = &p; /* 错误:类型不兼容 */
7.4、函数指针
在C语言中,函数本身不是变量,但可以定义指向函数的指针。函数指针是指向函数的指针变量,存储函数的入口地址,允许通过指针调用函数。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值等等。如下是一个指向函数的指针的示例。
int add(int a, int b)
{return a + b;
}
int (*funcPtr)(int, int) = add; /* 声明函数指针 */int main()
{printf("%d\n", funcPtr(2, 3)); /* 输出5 */return 0;
}
注意:函数指针的返回类型和参数列表必须与目标函数一致,并且函数指针需要指向实际存在的函数。
实现一个回调函数:
int add(int a, int b)
{return a + b;
}void process(int a, int b, int (*callback)(int, int))
{printf("结果:%d\n", callback(a, b));
}int main()
{/* 回调函数 */process(5, 3, add); /* 输出:8 */return 0;
}