你或许还不那么懂指针
是 int *a 还是 int* a ?
对于这个问题,答案是二者均可
实际上,int *a 更偏向于一种语法规则,在我们定义多个变量时,就能够体现其好处,比如
int *a, b;这个语句的含义就是定义了一个名为 a 的整型指针变量和一个名为 b 的整型变量。即“*”仅修饰了 a
而 int* a 则更能表示这个语句的本质,即 a 是一个 int* 类型的变量
因此,下面这个语句表示的不是一个数组的指针,而是一个数组,其中有 10 个 int* 类型的变量
int *a[10];与之需要区分的是行指针(数组指针),即 a 是一个指针,指向一个大小为 10 个 int 的数组,详情可看本文的 数组退化
int (*a)[10];常量指针和指针常量
我们直到使用 const 关键字可以定义常量,比如const int a = 10;
那么,如何理解下面两个语句
const int *p1;
int *const p2;我们把 p1 称作常量指针,把p2称作指针常量
这两个名称记起来并不困难,const int *p 中,const 在前,所以先读常量,* 在后,后读指针,因此这是一个常量指针
另一个同理
至于二者的区别,可以理解为 const int *p 中,const 修饰了 int,即 p 指向的值的类型,那么这个值就是无法修改的,比如
int a = 10, b = 20;
const int *p1 = &a;
int *p2 = &b // p2 指向 b 的地址,可写
// *p1 = 20; // 错误:常量指针指向的值无法修改
p1 = p2 // p1 指向 b 的地址,且只读
// p2 = p1 // 错误:p2 是个可读可写的指针变量,p1 只读
*p2 = 20; // 即使 p1 只读,但是仍然可以通过 p2 修改 b 的值同样的,int *const p;中,const修饰了p这个指针变量,因此,指针指向的地址不能改变,比如
int a = 10, b = 20;
int *const p1 = &a;
int *p2 = &b;
*p1 = b; // p1 指向的地址的值可以修改,即 a = b
// p1 = p2 // p1 指向的地址不能修改此外,还有常量指针常量,即值和地址都不能修改
const int *const p;| 类型 | 声明方式 | 修饰对象 | 指针指向(地址) | 指针内容(值) |
|---|---|---|---|---|
| 常量指针 | const int *p; |
指针指向的数据 | 可变 | 不可变 |
| 指针常量 | int *const p; |
指针本身 | 不可变 | 可变 |
| 常量指针常量 | const int *const p; |
指针指向的数据和指针本身 | 不可变 | 不可变 |
函数指针
函数指针用于指向函数“代码”内存的地址,通过对指针解引用,就可以使用函数,如下
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int main()
{
// 定义函数指针,返回值为 int 且接收两个 int 类型的参数
int (*p)(int, int) = NULL;
// 为函数指针赋值
p = add;
int result1 = p(10, 20); // 像使用函数名一样使用指针
int result2 = (*p)(10, 20); // 显式解引用调用
printf("Result: %d\n", result1);
return 0;
}从上面的代码可以看出
-
函数名本身就是一个函数指针
-
函数指针可以直接使用,也可以先解引用再调用
我们还可以通过类型别名简化代码,以便将函数作为参数 [1]
#include <stdio.h>
int max(int a, int b)
{
return a > b ? a : b;
}
int min(int a, int b)
{
return a < b ? a : b;
}
// 函数指针类型别名
typedef int (*PFun)(int, int);
int func(PFun pfun, int a, int b)
{
return pfun(a, b);
}
int main()
{
PFun comp = max;
printf("%d", func(comp, 1, 2));
return 0;
}在上面的代码中,comp函数作为参数被func函数调用,comp被称为回调函数
三个可能导致错误的指针
空指针
空指针(Null Pointer)即明确指向“无”的指针
在 C 语言中定义为 NULL(本质是一个宏),而在 C++11 后使用 nullptr
空指针不能解引用,因此在对一个指针进行解引用时需要判断是否为空指针,但是判断空指针的前提是该指针不是野指针或悬挂指针
因此一般在一个指针(如函数指针)初始化时,我们用空指针进行占位,否则就会变成野指针
野指针
野指针(Wild Pointer)即未初始化的指针
指针变量在刚创建时(比如 int *p;),它里面是随机分配的垃圾值
此时如果对其进行解引用(如 *p = 10;),程序可能会报错
这常常发生在用结构体构建链表的过程中,比如
typedef struct Node
{
int data;
struct Node *next;
} Node;
int main()
{
// 下面的代码会报错
// 因为 node1 和 node2 是野指针
Node *node1, *node2;
node1->data = 10;
node2->data = 20;
node1->next = node2;
node2->next = NULL;
return 0;
}要解决这个问题,有两种方法:静态分配内存和动态分配内存
// 静态分配内存:在栈中申请内存
Node n1, n2;
Node *node1 = &n1, *node2 = &n2;
// 动态分配内存:在堆中申请内存
#include <stdlib.h> // 包含 malloc 和 free
Node *node1 = (*Node)malloc(sizeof(Node));
Node *node2 = (*Node)malloc(sizeof(Node));
...
free(node1);
free(node2);悬空指针
当一个指针变量 p 指向的内存中的值被释放(手动释放或自动释放)后,p 仍然指向该地址,而地址中的值都是些垃圾值,此时 p 就成为了悬空指针(Dangling Pointer)
在用 free(p) 释放指针后,让指针为空 p = NULL,避免指针悬挂
这里我们发现悬空指针和野指针差不多,但是前者是本来有值,但是后来被释放了;一个是本来就没有值
这中指针产生的问题可能会发生在一些初学者身上,比如
int *get_nums()
{
int nums[3] = {1, 2, 3};
return nums;
}
int main()
{
// 错误:nums 是悬空指针
int *nums = get_nums();
return 0;
}我们知道,函数在栈中运行,当函数结束时,函数内存(包括其中的变量)都会被自动释放,因此,get_nums 函数中的 nums 并不能被成功返回
解决这个问题,一般有下面几种解决方式
1.使用动态内存分配
int *get_nums()
{
int nums[3] = (int *)malloc(sizeof(int) * 3);
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
return nums;
}
int main()
{
int nums = get_nums();
...
free(nums)
return 0;
}在其他函数使用完后要注意手动释放内存
2.由调用者提供空间
void fill_array(int *arr)
{
arr[0] = 1; arr[1] = 2; arr[2] = 3;
}
int main() {
int my_arr[3];
fill_array(my_arr);
return 0;
}3.声明静态变量
int* get_nums()
{
static int arr[3] = {1, 2, 3};
return arr; // 安全
}万能指针 void*
void*类型的变量可以接收任意类型的指针的值,比如
int main()
{
int a;
char b;
void *p = &a;
p = &b;
return 0;
}上面代码中,指针 p 既可以赋值为一个 int* 类型的指针,也可以被赋值为一个 char* 类型的指针
问题在于,由于不知道 p 指向的空间有多大,因此不能进行 *p 的加减法移位操作
利用万能指针作为函数参数,可以避免不知道参数类型的情况,比如memcpy函数
// 标准库原型:它可以处理任何类型的数据块
void *memcpy(void *dest, const void *src, size_t n);因此,善用万能指针,能避免不必要的函数重载
数组退化和函数退化
数组退化
数组名的本质
首先我们需要明确一点:数组名不是指针变量
这里要对指针和指针变量进行区分
-
指针:即地址,是一个常量
-
指针变量:是一个变量,可以修改,一个指针变量的内容(值)是一个指针(地址)
在大多数情况,我们将指针变量称为指针
要验证这点,我们可以做下面的实验
#include <stdio.h>
int main()
{
int a;
int *p = &a;
printf("p = %#p\n", p);
printf("&p = %#p\n", &p);
int arr[4];
printf("arr = %#p\n", arr);
printf("&arr = %#p\n", &arr);
return 0;
}得到的输出如下
p = 0000005ED19FFACC
&p = 0000005ED19FFAC0
arr = 0000005ED19FFAB0
&arr = 0000005ED19FFAB0在这个实验中,我们定义了一个 int* 类型的指针变量 p,作为指针变量,我们能够打印出其指向变量的地址;而其本身作为变量,我们也可以通过 &p 打印其自身的地址,事实证明,这两个地址确实不一样
然而,arr作为数组名,我们发现 arr 和 &arr 打印出来的结果是一样的
对此的解释是,arr 不作为指针或指针变量,仅仅是一个数组的标识
且其自身是一个常量,因此无法进行“整体”的修改,如下
int a[] = {1, 2, 3};
a = {3, 4, 5}; // Error 数组是一个常量,无法修改其整体
a++; // Error实际上,{1, 2, 3} 也是一个表达式,并非指针,因此下面的写法也是错误的
int *p = {1, 2, 3};与之相对的,"123" 是一个指针,因此下面的写法是正确的
char *p = "123";导致数组退化的情况 [2]
虽谈数组名不是指针,但是在C/C++中,数组名在大部分情况下会退化成指针
通过这个指针,我们就能访问和修改数组中的元素,比如
-
算术运算导致数组退化
int arr[] = {1, 2, 3};
printf("%d", arr[0]); // 1
arr[0] = 10;
printf("%d", arr[0]); // 10需要注意 arr[i],和 *(arr + i) (算术运算)的等价性
-
赋值导致的数组退化
将数组赋值给相应类型的指针
int arr[] = {1, 2, 3};
int *p = arr;
p++; // 这个是合法的,但是 arr++ 是不被允许的-
传参导致数组退化
在数组作为函数参数时,我们往往要添加一个数组长度的参数
这是因为直接传入数组名后,参数(数组名)会退化成一个指针
void func(int arr[100], int size)
{
printf("%d\n" sizeof(arr));
printf("%d\n", size);
}在这里,arr是一个int*类型的指针变量,因此输出结果为 8(64 位操作系统下),而不是 100 * sizeof(int),这里就能体现出 size 这个参数的必要性
不会退化的情况 [2]
虽然大部分情况下,数组会退化,但是还是有不会退化的情况的
-
sizeof运算
通过 sizeof 函数/关键字,我们仍然可以得到数组的大小
int arr[100] = {0};
printf("%d", sizeof arr); // 400-
取地址
上面我们发现数组名可以取地址,实际上,通过 &arr,我们可以得到一个指向数组的指针
具体可以参考下面的对比
#include <stdio.h>
int main()
{
int a[4];
printf("a = %p\n", a);
printf("a + 1 = %p\n", a + 1);
printf("&a = %p\n", &a);
printf("&a + 1 = %p\n", &a + 1);
return 0;
}输出结果如下
a = 00000066ABBFFB20
a + 1 = 00000066ABBFFB24
&a = 00000066ABBFFB20
&a + 1 = 00000066ABBFFB30可以看到,a + 1 相当于在 a 的基础上加一个 int 的大小(退化),而 &a + 1 则是加了四个 int(即一个数组)的大小
多维数组的退化
经过前面的实验,我们知道了一个一维数组退化后会变成一个对应类型的指针(如int*)
多维数组类似,即“降维”,比如
int arr[2][2];
int (*p)[2] = a;可以看到,这实际上就是行指针,这里的定义 p 的过程如下
-
首先,
p是一个指针,(*p) -
这个指针指向一个行(一维数组),
(*p)[2] -
每一行中的元素类型是整型,
int (*p)[2]
同样的,三维数组如下
int arr[3][2][2];
int (*p)[2][2] = a;-
首先,
p是一个指针,(*p) -
这个指针指向一个矩阵(二维数组),
(*p)[2][2] -
每一矩阵中的元素类型是整型,
int (*p)[2][2]
函数的退化
在上文介绍的函数指针中,就涉及到了一点函数退化的机制,比如
void (*p1)() = myFunc; // 隐式退化:函数名直接赋值
void (*p2)() = &myFunc; // 显式取地址:手动获取函数指针基于此特性,就会有下面的奇怪的写法
#include <stdio.h>
void test() { printf("Hello!\n"); }
int main() {
printf("%p\n", test); // 函数名(退化为地址)
printf("%p\n", &test); // 取地址
printf("%p\n", *test); // 解引用
printf("%p\n", ***test); // 多重解引用
// 以上所有打印结果完全一样
return 0;
}因为 test 会退化成指针,而对指针解引用 *test 后,它又因为身处表达式中而再次退化成指针
因此可以对一个函数名无限的解引用而其结果不变