C语言指针笔记
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;
}
以上都是,个人浅薄的见解,如有不当,欢迎各位大佬们指出