# C 语言指针笔记
# 前言
指针 可以说是 C 语言的灵魂,最巧妙的地方。不明白,不理解指针 那就是等于没学 C 语言.
指针这玩意说难也不难,主要是细节问题。比如最常见的,指针数组和数组指针、指针常数和常数指针、指针函数和函数指针。刚学完指针还好,时间一久,听到之这些东西很难短时间内反应过来
指针这块 用的多的 就是字符串了,其他的用的都比较少,所以久而久之总是忘,所以干脆写篇博客加深印象,也便于日后回顾我才不承认是我自己,搞混了概念,重新学了一遍,想和人分享没人听才写的博客,绝对不是!
# 基础部分
🔼
写在前面: 指针是一种变量,地址是一种数据.
大概的介绍一下 什么是指针,指针的作用,以及指针的基本操作
PS: 以下假设环境均为 64 位 1904 Win10 vscode gcc8.1.0
# 指针的作用
我先说明作用后面慢慢分析.
作用:
- 数据共享更加便捷,打破共享壁垒
- 以更精简的方式引用大的数据结构
- 利用指针可以直接操纵内存地址 (在 MCU, 嵌入式开发上体会会很深刻)
- 利用指针,能在程序执行过程中预留新的内存空间 (当然在没有 MMU 的单片机中,无法实现,能写内存管理的大佬除外)
先抛开指针,让我们来想想,变量的作用是什么?使内存空间易于管理.
好,为什么内存空间难管理,因为内存标号 (内存地址) 往往都是一串 16 进制数,难于记忆,所以对内存地址取个名字,便于记忆
局部变量都有一定的局限性,所谓的作用域,全局变量虽然没有局限性,但大量的全局变量会浪费许多内存.
然而很多时候我们需要在作用域外来修改内存上的值 (例如,两数交换), 数据唯一不变的只有地址,所以想要实现数据共享,只能通过 传递地址 来实现了,指针 —— 一种特殊的变量,就应运而生了.(学过汇编后,会对指针的数据共享理解更加深刻)
第二个作用,很大程度上体现在函数传参和字符串上,思考一个问题,如果我有 一个结构体,如下:
struct RxBufferTypeDef{ | |
int size; | |
int read; | |
int write; | |
int *buffer; | |
}; |
里面有 3 个整型变量,一个指针变量,一个 int 变量 4 字节,一个指针变量 8 字节,一个结构体 20 个字节大小,如果 一个函数调用时传递的是整个结构体,那么就相当于在内存中又开辟了 20 字节大小空间,用于实现对传入结构体的复制,而且不能对结构体的成员进行修改,如果传递结构体指针,大小恒为 8 字节,而且可以对结构体进行修改,整个程序中只占用 20 字节,在内存和运行速度上,比之直接传结构体更为方便.
大多数情况下,可以看到程序使用的内存是通过显式声明分配给变量的内存 (也就是静态内存分配). 这一点对于节省计算机内存是有帮助的,因为计算机可以提前为需要的变量分配内存。但是在很多应用场合中,可能程序运行时不清楚到底需要多少内存,这时候可以使用指针,让程序在运行时获得新的内存空间 (实际上应该就是动态内存分配 malloc
, calloc
), 并让指针指向这一内存更为方便.
# 指针的声明
语法: 数据类型 * 指针名称 = 初始地址;
示例:
int main(void) | |
{ | |
int i_variable = 10; | |
double d_variable = 1.1; | |
// 声明一个 指向整型的指针,指向 i_variable | |
int *ptr_a = &i_variable; | |
// 声明一个 指向整型的指针,指向 d_variable | |
int *ptr_b = &d_variable; | |
return 0; | |
} |
# 指针的使用
两个和指针息息相关的运算符:
&
是 C 语言中的取地址符号,用于获取地址*
是 C 语言中的解引用的符号,用于获取地址上的值
操作:
- 修改指向的地址;
语法:ptr_b = &a;
- 修改指向地址上的值;
语法:*ptr_b = 100;
- 所有指针都之和 处理位数以及 编译器相关,一般来说 是 8 字节或者 4 字节,比较特殊的 51 单片机是 12 位
示例 1:
int main(void) | |
{ | |
int i_variable1 = 10; | |
int i_variable2 = 20; | |
double d_variable = 1.1; | |
// 声明一个 指向整型的指针,指向 i_variable1 | |
int *ptr_a = &i_variable1; | |
// 声明一个 指向整型的指针,指向 d_variable | |
int *ptr_b = &d_variable; | |
// 打印指针大小 | |
printf("sizeof(ptr_a) = %d\n",sizeof(ptr_a)); | |
printf("sizeof(ptr_b) = %d\n",sizeof(ptr_b)); | |
// 打印 i_variable 指向的地址上的值 | |
printf("ptr_a = %d\n", *ptr_a); | |
ptr_a = &i_variable2; // 改变 ptr_a 的指向 | |
printf("ptr_a = %p\n", *ptr_a); | |
*ptr_a = 100; | |
printf("i_variable1 = %d\n", i_variable1); // | |
printf("i_variable2 = %d\n", i_variable2); | |
printf("ptr_a = %d\n", *ptr_a); | |
return 0; | |
} |
# 空指针
- 定义:指针变量指向内存中编号为 0 的空间
- 用途:初始化指针变量,不知道指哪里,就先指向这里
- 注意:空指针指向的内存是不可以访问的
- 示例:
int main(void) | |
{ | |
// 指针变量 p 指向内存地址编号为 0 的空间 | |
int * p = NULL; | |
// 访问空指针报错 | |
// 内存编号 0 ~255 为系统占用内存,不允许用户访问 | |
printf("0x%x", p); | |
printf("%d", *p); | |
return 0; | |
} |
# 野指针
定义:指针变量指向非法的内存空间
非法空间是指,指针指向系统和程序协商后可访问空间之外的地址示例:
int main(void) | |
{ | |
// 指针变量 p 指向内存地址编号为 0x1100 的空间 | |
int * p = (int *)0x1100; | |
// 访问野指针报错 | |
printf("%d", *p); | |
return 0; | |
} |
# 一级指针
简单来说就是只有 一个 *
运算符的指针变量,大多数的一维指针会用于指向结构体、数组、字符串...
# 指向变量的指针
指向变量的语法,如上面指针的基本内容所述,只要存在数据类型,都可以定义指针
这说一个比较特殊的 指针 —— void
指针,众所周知, void
是一种空类型,那么,正常思路下指向 void
的指针,就是指向空的指针,简称除了 NULL
啥都不能指.
其实不然,正所谓 万物皆虚,万事皆允 (we are assassins!), void 指针啥都能指,最简单的例子就是 内置 qsort
的回调函数 cmp(const void *a, const void *b)
, 咱们总不可能开两个空数组吧 (滑稽)
其实无论什么数据,在存储器上都是高低电平,只是读取的方式不同,才有了不同的数据类型, void
指针正是依靠了这个特性,通过强制类型转换来实现,任意传参.
其实无论什么指针,都可以强制转化,用于传递地址,只是因为指针只存储地址,且数据的意义只与读取方式相关所以才可以进行相互转换
PS: 个人建议,如果要传入不知道什么类型的数据时可以考虑以 void * 作为参数,不到万不得已,不要使用其他指针来实现类型转换
示例
typedef struct _Nodes | |
{ | |
int id; | |
char *str; | |
} MyStruct; | |
int main(void) | |
{ | |
int a = 10; | |
double d = 1.1; | |
MyStruct node1 = {1, "This is a test about void pointer"}; | |
void *v_ptr = &a; | |
// 转化位 int | |
printf("====================int=======================\n"); | |
printf("a = %d\n", a); | |
printf("&a = 0x%x\n", &a); | |
printf("*v_ptr = %d\n", *(int *)v_ptr); | |
printf("v_ptr = 0x%x\n", v_ptr); | |
// 转化位 double | |
v_ptr = &d; | |
printf("====================double=======================\n"); | |
printf("d = %.2lf\n", d); | |
printf("&d = 0x%x\n", &d); | |
printf("*v_ptr = %.2lf\n", *(double *)v_ptr); | |
printf("v_ptr = 0x%x\n", v_ptr); | |
// 转化为 结构体 | |
v_ptr = &node1; | |
printf("====================structer=======================\n"); | |
printf("node.id = %d\n", node1.id); | |
printf("node.str = %s\n", node1.str); | |
printf("&d = 0x%x\n", &node1); | |
printf("(*v_ptr).id = %d\n", (*(MyStruct *)v_ptr).id); | |
printf("(*v_ptr).str = %s\n", (*(MyStruct *)v_ptr).str); | |
printf("v_ptr = 0x%x\n", v_ptr); | |
return 0; | |
} |
示例 2:
这一部分是标准库调用示例:
int cmp(const void *a, const void *b) | |
{ | |
return (*(int *)a) > (*(int *)b) ? 1 : -1; | |
} | |
void travelArray(int *arr, int n); | |
int main(void) | |
{ | |
int n; | |
// 获取数组长度 | |
scanf("%d", &n); | |
// 动态开辟数组 | |
int *arr = (int *)malloc(sizeof(int) * n); | |
// 读入数据 | |
for (int i = 0; i < n; i++) | |
{ | |
scanf("%d", arr + i); | |
} | |
travelArray(arr, n); | |
qsort(arr, n, sizeof(arr[0]), cmp); | |
travelArray(arr, n); | |
return 0; | |
} | |
/** | |
* @brief 遍历数组 | |
* | |
* @param arr 数组首地址 | |
* @param n 数组大小 | |
*/ | |
void travelArray(int *arr, int n) | |
{ | |
for (int i = 0; i < n; i++) | |
{ | |
printf("%d ", arr[i]); | |
} | |
putchar('\n'); | |
} |
# const 指针
这就是一个经典的问题了,** **, const 修饰不同 的地方,指针的效果就不一样,const 一共有三种修饰方式:
- 第一种:
const int* ptr = &a;
常量指针,可以改变指向方向 - 第二种:
int* const ptr = &a;
指针常量,可以改变地址上的值 - 第三种:
const int* const ptr = &a;
上面两种的结合体,可以称为指针常数
顾名思义, 常量指针,指向常量的指针,指向的是常量,指针不是常量,可以改变指向的地址,但是不能改变指向的值
指针常量,指针自己是一个常量,指向的不一定是常量,所以可以改变所指向地址上的值,不能改变指向的地址
指针常数,这个就不多说了,啥都改不了,指向的是常数
示例:
int main(void) | |
{ | |
int a = 10, b = 20; | |
int *pa = &a; | |
// 常量指针 | |
const int *cpa = pa; | |
// 指针常量 | |
int *const pca = pa; | |
// 指针常数 | |
const int *const cpca = pa; | |
// cout << "a = " << a << endl; | |
printf("pa = 0x%x\n", pa); | |
printf("*cpa = %d\n", *cpa); | |
printf("*pca = %d\n", *pca); | |
printf("*cpca = %d\n", *cpca); | |
puts("================修改cpa指向==================\n"); | |
cpa = &b; // 正确,修改常量指针指向的地址 | |
//pca = &b; // 错误,修改指针指常量向的地址 | |
printf("&b = 0x%x\n", &b); | |
printf("cpa = 0x%x\n", cpa); | |
printf("pca = 0x%x\n", pca); | |
cpa = &a; | |
puts("=================修改pca指向的值===============\n"); | |
// *cpa = 90; // 错误,修改常量指针的指向的变量的值 | |
*pca = 100; // 正确,修改指针常量指向的值 | |
printf("*pa = %d\n", *pa); | |
printf("*cpa = %d\n", *cpa); | |
printf("*pca = %d\n", *pca); | |
//cpca = &b; // 错误,双 const 啥都不能改 | |
// *cpca = 90; // 错误,双 const 啥都不能改 | |
return 0; | |
} |
# 指向函数的指针
话外:为什么不能用二级指针直接指向二维数组
- 什么是函数指针:
和变量类似,如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址,在 debug 时,我们会在主栈中看到,被压入的函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针. - 函数指针的声明:
函数指针与其他指针声明方式不同,正如前面所说,指针只能指向一种数据类型,所以 函数指针的声明有些复杂,大致格式如下:返回数据类型 (* 指针名称)(参数列表);
例如:int (*funPtr)(int *, int *);
这个语句就定义了一个指向函数的指针变量funPtr
. 首先它是一个指针变量,所以要有一个*
, 即 (*p); 其次前面的int
表示这个指针变量可以指向返回值类型为 int 型的函数;后面括号中的两个int
表示这个指针变量可以指向有两个参数且都是int
型的函数。所以合起来这个语句的意思就是:定义了一个指针变量funPtr
, 该指针变量可以指向返回值类型为int
型,且有两个整型参数的函数.funPtr
的类型为int(*)(int, int)
- 如何用函数指针调用函数PS: 函数指针 没有
int Func(int x); /* 声明一个函数 */
int (*pFunc) (int x); /* 定义一个函数指针 */
pFunc = Func; /* 将 Func 函数的首地址赋给指针变量 p*/
++
和--
的操作
- 示例
#include <stdio.h>
int Max(int x, int y); // 函数声明
int main(void)
{
int a = 0, b = 0, c = 0;
int (*p)(int, int); // 定义一个函数指针
// 把函数 Max 赋给指针变量 p, 使 p 指向 Max 函数
p = Max;
printf("please enter a and b:");
scanf("%d%d", &a, &b);
// 通过函数指针调用 Max 函数
c = (*p)(a, b);
printf("a = %d\nb = %d\nmax = %d\n", a, b, c);
return 0;
}
/**
* @brief 比较连个数大小
*
* @param x 比较数 x
* @param y 比较数 y
* @retval int 最大值
*/
int Max(int x, int y) // 定义 Max 函数
{
return x > y ? x : y;
}
# 二级指针
🔼
与 一级指针类似,需要两次 *
操作才能得到最顶层值 的 指针变量,最常见的就是 字符串数组。一级指针往往比较简单,维度一升高 后就开始变得复杂起来
三者之间的关系如图,手残,将就一下
在内存中的图示
# 指向指针的指针
指针可以指向一份普通类型的数据,例如 int、double、char 等,也可以指向一份指针类型的数据,例如 int *
、 double *
、 char *
等,所以就有了指向指针的指针
上面图片的关系用 C 语言来描述就是
int a = 10; | |
int *ptr_a = &a; | |
int **pptr_a = &ptr_a; |
指针变量也是一种变量,也会占用存储空间,也可以使用 &
获取它的地址.C 语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号 *
. p1
是一级指针,指向普通类型的数据,定义时有一个 *
; p2
是二级指针,指向一级指针 p1
, 定义时有两个 *
.
那么,为什么要有二级指针呢?
先来看一下这段代码:
有两个变量 a
, b
, 指针 q
, q
指向 a
, 我们想让 q
指向 b
, 在函数里面实现.
这里贴一下用于测试的主函数
#include <stdio.h> | |
int a = 10; | |
int b = 100; | |
int *q; | |
void func(int *p); | |
int main(void) | |
{ | |
printf("&a=0x%x, &b=0x%x, &q=0x%x\n", &a, &b, &q); // 1 | |
q = &a; | |
printf("*q=%d, q=0x%x, &q=0x%x\n", *q, q, &q); // 2 | |
func(q); | |
printf("*q=%d, q=0x%x, &q=0x%x\n", *q, q, &q); // 5 | |
return 0; | |
} |
- 用 一级指针 实现:
void func(int *p)
{
printf("func: &p=0x%x, p=%d\n", &p, p); // 3
p = &b; // 让指针 p 指向 b;
printf("func: &p=0x%x, p=%d\n", &p, p); // 4
}
看起来 在逻辑上代码没有什么问题,但是众所周知,程序执行后 *q
不等于 100, 为什么呢?
来简单看一下,测试输出的结果
&a=0x403010, &b=0x403014, &q=0x407970
*q=10, q=0x403010, &q=0x407970
func: &p=0x61fe00, p=4206608
func: &p=0x61fe00, p=4206612
*q=10, q=0x403010, &q=0x407970
来分析一下输出:
- 注释 1:
a, b, q
都有一个地址. - 注释 2:
q
指向a
,q
的值发生了变化,地址是固定的 - 注释 3: 进入函数后的参数
p
的地址跟q
不一样了。这是因为在函数调用时,为了保障原数据不变对其进行了拷贝,也就是说p
和q
不是同一个指针,但是 他们指向的地址是相同的,都指向 &a (0x403010) - 注释 4:
p
指向b
, 这时候p
的值发生了变化 - 注释 5: 回到主函数后,函数栈释放,
p
也就丢失了,q
也不会有任何变化.
结论:
编译器会对函数的每个参数制作临时副本,指针参数 p
的副本是 q
, 编译器使 p = q
(但是 &p != &q
, 也就是他们并不在同一块内存地址,只是他们的内容一样,都是 a 的地址). 如果函数体内的程序修改了 p 的内容 (比如在这里它指向 b). 在本例中,p 申请了新的内存,只是把 p 所指的内存地址改变了 (变成了 b 的地址,但是 q 指向的内存地址没有影响), 所以在这里并不影响函数外的指针 q.
其实,这就是 所谓的 传值调用 和 传地址调用,这两个概念就是一个抽象概念,value 和 address 是相对的,对于指针变量来说,传值 也是地址,传地址也是地址,只不过前者是传递指向的地址,后者是传递本身的地址.
例如 swap 函数,如果参数为 (int a, int b)
, 那就是传值调用,因为我们想要交换 a
和 b
的值,如果仅仅传入值,那么调用函数产生的副本,也仅仅是 数值与 a b
相同的两个全新变量而已。我们想要交换两个变量,就必须要传入地址,在地址上直接对值进行操作.
上例中, p
对应的是 a b
变量,我们只传进想要改变的值,而非传入值所在的地址,所以 q
并没产生变化。这时候我们就需要传入,指针 *q
的地址了,对应的函数参数类型,就变量了指向指针的指针,也就是二级指针.
二级指针操作
void func(int **p)
{
printf("func: &p=0x%x, p=%d\n", &p, p);
*p = &b;
printf("func: &p=0x%x, p=%d\n", &p, p);
}
改动的地方很少.
因为传了指针
q
的地址 (二级指针**p
) 到函数,所以二级指针拷贝 (拷贝的是p
, 一级指针中拷贝的是q
, 就是指向的地址),(拷贝了指针但是指针内容也就是指针所指向的地址是不变的) 所以它还是指向一级指针q
(*p = q
). 在这里无论拷贝多少次,它依然指向q
, 那么*p = &b;
自然的就是q = &b;
了.PS:
到这里其实我,想说的是其实 一级指针也可以实现二级指针的效果,但是并不推荐,咱们永远不知道这种方法的通用性,
我们可以 一级指针的调用的时候传入q
的地址,然后把赋值语句改为*p = &b;
也可以得到相应的效果,因为指针都是 8 字节,里面进行了一次 (隐式) 强制类型转换。由于指针指向的类型比较简单,没有导致数据异常,所以 GUN 仅仅是抛出了 warming.
# 指针与数组
🔼
这里 大概会涉及到几个内容:
- 指针 与 数组首地址
- 指针数组 和 指向数组的指针
先声明一下:
- 指针:是用于存储 地址的变量
- 数组:是一串相同类型变量的
- 地址:是一种数据,与指针不同的地方在于,没有数据类型 (某种意义上像是指针常量)
先来说一下第一个:指针 与 数组首地址
在我初学指针的时候一直有一个疑惑就是
int arr[4];
和int *ptr;
的区别,因为老师经常说,数组的首地址等价于指针,而且 访问数组的时候使用arr[1]
和*(arr+1)
的效果是一样的,我一度就把 一级指针 等价为 一维数组.
直到有一次,有个小姐姐问我,为什么sizeof(ptr)
算不出数组大小 而sizeof(arr)
可以。我当时的回答是,因为arr
是 一个数组ptr
是一个指针变量。说完我就察觉到不对了,如果arr
完全等价于ptr
那么为什么sizeof(ptr)
不等于sizeof(arr)
,arr = ptr
会报错。果然啊,教学相长 (有点丢脸,错失良机啊)
其实 数组的首地址、变量的取地址 这些得到的都是指针常量 (上面说过, 传送门), 只能充当右值,不能作为左值.ptr
仅仅是一个指针变量,用于存储地址的变量。对于二维数组,也是如此,int arr[2][3] = {0};
,arr
整个二维数组的首地址,arr[i]
为 每一行一维数组的首地址.
示例:
#include <stdio.h> | |
int main(void) | |
{ | |
int arr[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; | |
int *ptr1 = arr; | |
for (int i = 0; i < 9; i++) | |
{ | |
printf("ptr1_val = %d \n", ptr1[i]); | |
} | |
return 0; | |
} |
接着咱们 来讨论一下:指针数组和指向数组的指针
这个问题 经常出现在,声明指向二维数组指针上,经常会有 问题是
int *arr[5]
和int(*arr)[5]
哪个是指向数组的指针之流.
在说这个问题之前,先来说明一下两个概念:
- 行指针:指的是一整行,不指向具体元素
声明格式:数据类型 (*指针名)[长度];
- 列指针:指的是一行中某个具体元素 (一维指针)
声明格式:数据类型 *指针名称
;
示例
#include <stdio.h> | |
int main(void) | |
{ | |
int arr[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; | |
int *ptr1 = arr; // 声明列指针 | |
int(*ptr2)[9] = &arr; // 声明行指针 | |
for (int i = 0; i < 9; i++) | |
{ | |
printf("ptr1_val = %d, ptr2_val = %d\n", ptr1[i], (*ptr2)[i]); | |
} | |
return 0; | |
} |
PS: 可以将列指针理解为行指针的具体元素,行指针理解为列指针的地址
言归正传,假设 我们需要声明一个指向 int arr[5][9];
和 一个大小为 4 指向整型指针的数组.
先来解决第一个:
假设 指针名为 ptr
ptr
先是一个指针,所以第一步是*ptr
,*ptr
指向的是数组,由于[]
运算优先级比*
高,所以 需要加上()
, 即(*arr)[9]
(*arr)[9]
指向的类型为int
, 得出最终结果int (*arr)[9] = &arr;
然后是指针数组
设 ptrs 为数组名:
ptrs
先是一个数组,得到ptrs[4]
ptrs[4]
的成员是指针,得到*ptrs[4]
*ptrs[4]
成员指向的数据类型为int
, 得到int ptrs[4];
小结:指向数组的指针格式一般以 (*指针名)
打头,而指针数组一般以 *数组名
打头
正如上面所说,一般情况下指向数组的指针格式以 (*指针名)
打头,咱们这行只要是一般,就必然有例外.
由于 数组是在一块连续的内存上定义的 所以 只要找到 第一个元素所在的地址,即数组起始地址,就等级于找到整个数组.
所以就有了 p = &arr[0]
、 p = &arr[0][0]
、 *p = &arr[0][0][0];
的奇观.
这也就是上面所说的 列指针。突然就感觉 上面的行指针不香了 (滑稽)
示例:
#include <stdio.h> | |
int main(void) | |
{ | |
int arr[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; | |
int *ptr1 = &arr[0][0]; | |
int(*ptr2)[3] = arr; | |
for (int i = 0; i < 9; i++) | |
{ | |
printf("ptr1_val = %d, ptr2_val = %d\n", ptr1[i], (*ptr2)[i]); | |
} | |
printf("======================================================\n"); | |
for (int i = 0; i < 3; i++) | |
{ | |
for (int j = 0; j < 3; j++) | |
{ | |
printf("ptr1_val = %d, ptr2_val = %d\n", ptr1[i * 3 + j], ptr2[i][j]); | |
} | |
} | |
return 0; | |
} |
# 话外:为什么不能用二级指针直接指向二维数组
举个例子:
int arr[2][3] = {1, 2, 3, 4, 5, 6}; | |
int **pptr = arr; // 编译出错,不能用二级指针直接指向二维数组 | |
int(*ptrRow)[3] = arr; // 对,ptrRow 是指向一维数组的指针,可以指向二维数组 | |
int *ptrCol = arr[0]; // 可以,ptrCol 也是一维指针,可以指向二维数组 |
理论上一维数组对应一维指针,例如 int arr[3]; int *ptr = arr
;
那么二维数组应该也对应于二级指针才对啊.
对于这个问题,咱们先来看一下二级指针的定义:
二级指针指向一级指针,一级指针是取一次地址,二级指针是取两次地址.
就此可以推及到 更高的维度: n 级指针是指向 n-1 级指针的指针,n 级指针是取 n 次地址
现在我们来分析一下int **pptr = arr;
为什么会出错
首先 **pptr = arr
是等价于 **pptr = &arr[0][0];
那么 *ptr
得到的结果为 1
, 如果 再对其进行 *
操作,就会访问到 内存地址 1 上的值,显然这是不允许的
PS: 二级指针是指向一级指针的,那么二级指针 pptr
每次移动的大小就是 sizeof(int *)
也就是 8 个字节,所以 pptr+1
不是像二维数组 arr+1
那样移动到下一行首地址,而是移动 8 个字节.
int **pp=a;
不行。那 int **pp=&a;
呢?
很遗憾也不行,原因也是数据类型不一致,导致地址偏移非法.
凡事总有那么个例外:
例如:
char *str[2] = {"hello","world"}; | |
char **strptrs = str; |
本质原因是因为这是一个指针数组,而非 真正的二维数组,每一行的首地址 本身即为指针,符合了两次取地址的要求。而且指针偏移量也为 8 字节, strptrs++
偏移正常,所以 strptrs
才能指向 str
其实这个问题的核心在于 数据类型的不匹配 导致的地址自加异常.
示例
#include <stdio.h> | |
int main(void) | |
{ | |
int arr[2][4] = {1, 2, 3, 4, 5, 6, 7, 8}; | |
int **pptr = arr; | |
char *str[4] = {"hello", "world", "C pointer", "G++"}; | |
char **strptrs = str; | |
printf("%c\n", strptrs[0][0]); | |
//printf ("% d\n", pptr [0][1]);// 异常,不会打印数据 | |
return 0; | |
} |
# 话外:如何定义一个返回函数指针且参数为函数指针的函数
2021.10.21 更新
首先来说明一下,函数指针的类型为 指向没有返回值且参数为 int 的函数
咱一步步来,首先函数的定义如下:
int func(void); // 返回值为整型 参数为空
这一个基本的带返回值的函数声明,现在咱来回顾一下,函数指针是什么样的
int *func(void);
现在我再把参数放入
int *func(void(*func_p)(int));
最后再将 int
也替换为 void(*func_p)(int)
那么得出的结果如下:
(void(*ptr)(int id)) *func(void(*func_p)(int));
然而这就是想当然了。放弃刚刚的想法,咱们来分析一下这个声明
前半部分声明了一个指针变量,后半部分声明了一个函数,再 *func
前面加个分号刚刚好拆成两个声明.
如果不能这么定义那么函数指针又该如何定义呢?
很显然 对于 C 语言编译器来说,它先扫描到了 ptr
这个关键字,进行类型匹配后判定为指针变量,然后再扫描到 func
进行识别后判定为 函数.
一个变量当然不能作为函数的返回值来使用,所以返回 函数指针的函数 该如何定义 返回值的函数指针呢?
首先, func(void(*func_p)(int))
这一部分是没有什么问题的,来回以一下,函数指针的通式是什么样的返回值类型 (*指针名称)(参数);
, 既然从左到右最左边的非关键字的字符串视为函数名,那么就只能以 func
为中心进行处理.void (* )(int id);
前缀的缩写因该是这样的,再将 func(void(*func_p)(int))
视为整体当作变量名称放入括号中.
得到 void (*func(void(*func_p)(int)))(int id)
, 这个定义刚好就是所需要的定义.
抛开上面的所有来分析一下,首先从 func
开始,
func
先和()
结合,所以func
是个函数- 再读取括号中的信息,
func
是一个函数,参数为指针,这个指针指向 返回值位空参数为int
的函数 - 再和
*
结合,func
是一个返回指针的函数. (*func( ))
再和void (int id)
, 表示这是指向函数指针,指向 返回值位空,参数为 整型的函数
结合起来就是 func 是一个返回值为函数指针的函数,参数为指针,这个指针指向返回值为空参数为整型的函数,返回值的指针指向返回值为空参数为整型的函数.
# 但是
正儿八经的声明永远不长这样,这么复杂的声明咱还是别写的好. C 语言为了化简数据类型的定义专门出了一个关键字,叫 typedef
.
咱们使用 typedef
来实现一下函数声明typedef
的语法
typedef 类型声明 重命名
先举个简单的例子, typedef struct node* ListNode_PTR_T
这的意思就是将 结构体 struct node
的指针重命名为 ListNode_PTR_T
从此之二者等效使用。很类似 #define
但是 #define
仅仅是字符串的等效替代, debug
时无法进入,而且会有点 bug, 更详细的讨论会专门写一篇博客
言归正传其实在上面那个函数的声明,返回值和参数时一致的 可以使用 typedef
来简化.
如下:
typedef void (*RVOID_PINT_FUNCPTR_T)(int); |
那么 上面的声明就可以简写为
RVOID_PINT_FUNCPTR_T func(RVOID_PINT_FUNCPTR_T); |
这里是验证测试的代码
// 原始定义 | |
void (*func(void (*func_p)(int)))(int id); | |
// 重命名一个 指向 返回值为空参数为整型的函数 的指针类型 为 RVOID_PINT_FUNCPTR_T | |
typedef void (*RVOID_PINT_FUNCPTR_T)(int); | |
RVOID_PINT_FUNCPTR_T funcx(RVOID_PINT_FUNCPTR_T func_ptr); | |
// 重命名 指向上面这个函数的指针 的类型名称 | |
typedef RVOID_PINT_FUNCPTR_T (*TEST_TYPE)(RVOID_PINT_FUNCPTR_T func_ptr); | |
// 赋值成功表示类型向等 | |
TEST_TYPE CC = func; | |
TEST_TYPE C = funcx; |
这样子的代码配上注释可读性就大大提升,毕竟源码是给人看,没必要写的云里雾里.
但是毕竟早些年的人会不乐意使用 typedef, 所以阅读能力还是需要有的
例如,ANSI C 的信号的的声明,啧啧啧
不过现在 Window 下的声明就不一样了
以上都是,个人浅薄的见解,如有不当,欢迎各位大佬们指出