int *a还是int* a

对于这个问题,答案是二者均可

实际上,int *a更偏向于一种语法规则,在我们定义多个变量时,就能够体现其好处,比如

1
int *a, b;

这个语句的含义就是定义了一个名为a的整型指针变量和一个名为b的整型变量。即“*”仅修饰了a

int* a则更能表示这个语句的本质,即a是一个int*类型的变量

因此,下面这个语句表示的不是一个数组的指针,而是一个数组,其中有 10 个int*类型的变量

1
int *a[10];

与之需要区分的是行指针

1
int (*a)[10];

常量指针和指针常量

我们直到使用const关键字可以定义常量,比如const int a = 10;

那么,如何理解下面两个语句

1
2
const int *p1;
int *const p2;

我们把p1称作常量指针,把p2称作指针常量

这两个名称记起来并不困难,const int *p中,const在前,所以先读常量,*在后,后读指针,因此这时一个常量指针

另一个同理

至于二者的区别,可以理解为const int *p中,const修饰了int,即p指向的值的类型,那么这个值就是无法修改的,比如

1
2
3
4
5
6
7
8
int a = 10, b = 20;
const int *p1 = &a;
int *p2 = &b // p2 指向 b 的地址,可写

// *p = 20; // 错误:常量指针指向的值无法修改
p1 = p2 // p1 指向 b 的地址,且只读
// p2 = p1 // 错误:p2 是个可读可写的指针变量,p1 只读
*p2 = 20; // 即使 p1 只读,但是仍然可以通过 p2 修改 b 的值

同样的,int *const p;中,const修饰了p这个指针变量,因此,指针指向的地址不能改变,比如

1
2
3
4
5
6
int a = 10, b = 20;
int *const p1 = &a;
int *p2 = &b;

*p1 = b; // p1 指向的地址的值可以修改,即 a = b
// p1 = p2 // p1 指向的地址不能修改

此外,还有常量指针常量,即值和地址都不能修改

1
const int *const p;
类型 声明方式 修饰对象 指针指向(地址) 指针内容(值)
常量指针 const int *p; 指针指向的数据 可变 不可变
指针常量 int *const p; 指针本身 不可变 可变
常量指针常量 const int *const p; 指针指向的数据和指针本身 不可变 不可变

函数指针

函数指针用于指向函数“代码”内存的地址,通过对指针解引用,就可以使用函数,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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. 函数名本身就是一个函数指针

  2. 函数指针可以直接使用,也可以先解引用再调用

我们还可以通过类型别名简化代码,以便将函数作为参数 [1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#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;),程序可能会报错

这常常发生在用结构体构架链表的过程中,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

要解决这个问题,有两种方法:静态分配内存动态分配内存

1
2
3
4
5
6
7
8
9
10
11
// 静态分配内存:在栈中申请内存
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)

这中指针产生的问题可能会发生在一些初学者身上,比如

1
2
3
4
5
6
7
8
9
10
11
12
int *get_nums()
{
int nums[3] = {1, 2, 3};
return nums;
}

int main()
{
// 错误:nums 是悬空指针
int *nums = get_nums();
return 0;
}

我们知道,函数在栈中运行,当函数结束时,函数内存(包括其中的变量)都会被自动释放,因此,get_nums函数中的nums并不能被成功返回

解决这个问题,一般有下面几种解决方式

1.使用动态内存分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.由调用者提供空间

1
2
3
4
5
6
7
8
9
10
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.声明静态变量

1
2
3
4
5
int* get_nums()
{
static int arr[3] = {1, 2, 3};
return arr; // ✅ 安全
}

万能指针void*

void*类型的变量可以接收任意类型的指针的值,比如

1
2
3
4
5
6
7
8
int main()
{
int a;
char b;
void *p = &a;
p = &b;
return 0;
}

上面代码中,指针p既可以赋值为一个int*类型的指针,也可以被赋值为一个char*类型的指针

问题在于,由于不知道p指向的空间有多大,因此不能进行*p和加减法移位操作

利用万能指针,作为函数参数,可以避免不知道参数类型的情况,比如memcpy函数

1
2
// 标准库原型:它可以处理任何类型的数据块
void *memcpy(void *dest, const void *src, size_t n);

因此,善用万能指针,能避免不必要的函数重载

数组退化和函数退化

数组退化

数组名的本质

首先我们需要明确一点:数组名不是指针变量

要验证这点,我们可以做下面的实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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;
}

得到的输出如下

1
2
3
4
p = 0000005ED19FFACC
&p = 0000005ED19FFAC0
arr = 0000005ED19FFAB0
&arr = 0000005ED19FFAB0

在这个实验中,我们定义了一个int*类型的指针变量p,作为指针变量,我们能够打印出其指向变量的地址;而其本身作为变量,我们也可以通过&p打印其自身的地址

事实证明,这两个地址确实不一样

这里要对指针指针变量进行区分

  • 指针:即地址,是一个常量

  • 指针变量:是一个变量,可以修改,一个指针变量的内容是一个指针(地址)

在大多数情况,我们将指针变量称为指针

然而,arr作为数组名,我们发现arr&arr打印出来的结果是一样的

对此的解释是,arr不作为指针或指针变量,仅仅是一个数组的标识

其本身是一个常量,因此无法进行“整体”的修改,如下

1
2
3
int a[] = {1, 2, 3};
a = {3, 4, 5}; // Error 数组是一个常量,无法修改其整体
a++; // Error

实际上,{1, 2, 3}也是一个表达式,并非指针,因此下面的写法也是错误的

1
int *p = {1, 2, 3};

与之相对的,"123"是一个指针,因此下面的写法是正确

1
char *p = "123";

导致数组退化的情况 [2]

在C/C++中,数组名在大部分情况下会退化成指针

通过这个指针,我们就能访问和修改数组中的元素,比如

  • 算术运算导致数组退化

1
2
3
4
int arr[] = {1, 2, 3};
printf("%d", arr[0]); // 1
arr[0] = 10;
printf("%d", arr[0]); // 10

需要注意arr[i],和*(arr+i)(算术运算)的等价性

  • 赋值导致的数组退化

将数组赋值给相应类型的指针

1
2
3
int arr[] = {1, 2, 3};
int *p = arr;
p++; // valid 但是 arr++ 是不允许的
  • 传参导致数组退化

在数组作为函数参数时,我们往往要添加一个数组长度的参数

这是因为直接传入数组名无法传递数组的全部信息(数组退化,传递一个int*类型的指针)

1
2
3
4
void func(int arr[100])
{
printf("%d" sizeof(arr));
}

在这里,arr是一个int*类型的指针变量,因此输出结果为 8(64 位操作系统下),而不是100 * sizeof(int)

不会退化的情况 [2:1]

虽然大部分情况下,数组会退化,但是还是有不会退化的情况的

  • sizeof 运算

通过sizeof函数/关键字,我们仍然可以得到数组的大小

1
2
int arr[100] = {0};
printf("%d", sizeof arr); // 400
  • 取地址

上面我们发现数组名可以取地址,实际上,通过&arr,我们可以得到一个指向数组的指针

具体可以参考下面的对比

1
2
3
4
5
6
7
8
9
10
11
12
#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;
}

输出结果如下

1
2
3
4
a = 00000066ABBFFB20
a + 1 = 00000066ABBFFB24
&a = 00000066ABBFFB20
&a + 1 = 00000066ABBFFB30

可以看到,a + 1相当于在a的基础上加一个一个int的大小(退化),而&a + 1则是加了四个int(即一个数组)的大小

多维数组的退化

经过前面的实验,我们知道了一个一维数组退化后会变成一个对应类型的指针(如int*

多维数组类似,即“降维”,比如

1
2
int arr[2][2];
int (*p)[2] = a;

可以看到,这实际上就是行指针,这里的定义p的过程如下

  1. 首先,p是一个指针,(*p)

  2. 这个指针指向一个行(一维数组),(*p)[2]

  3. 每一行中的元素类型是整型,int (*p)[2]

同样的,三维数组如下

1
2
int arr[3][2][2];
int (*p)[2][2] = a;
  1. 首先,p是一个指针,(*p)

  2. 这个指针指向一个矩阵(二维数组),(*p)[2][2]

  3. 每一矩阵中的元素类型是整型,int (*p)[2][2]

函数的退化

在上文介绍的函数指针中,就涉及到了一点函数退化的机制,比如

1
2
void (*p1)() = myFunc;  // 隐式退化:函数名直接赋值
void (*p2)() = &myFunc; // 显式取地址:手动获取函数指针

基于此特性,就会有下面的奇怪的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
#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 后,它又因为身处表达式中而再次退化成指针

因此可以对一个函数名无限的解引用而其结果不变


  1. C 语言中文网 - typedef的用法,C语言typedef详解 ↩︎

  2. 博客园 - C与C++中数组退化为指针的情况 ↩︎ ↩︎