
理清二维数组的指针各种表达方式的思维导图
目录
引入

内存是电脑上的存储设备,程序运行时会加载到内存。
任务管理器可以查看内存使用情况:

内存中的每一个字节都有自己的编号:

这个编号是用十六进制表示的(“0x”是用来表示这个数是十六进制数而不是其他进制,可以对一个整形变量赋值一个十六进制数,如 int a = 0x11223344)。这个编号也可以叫做地址、指针,它们都是一回事。我们平时口头说的指针是指针变量。

指针是内存中一个最小存储单元的编号,也就是地址。
& ---- 取地址运算符
假如我们定义了一个整形变量 a ,a 占四个字节:

如果此时我们 &a (对 a 取地址),在上图中,四个红色方块(也就是四个字节)的编号都是 a 的地址,那么对 a 取地址,取的是哪个地址呢?
答案是:取第一个字节的地址(最小的地址)
一个变量的地址是它占用的字节的地址中最小的地址
如果我们把这个地址赋值给一个变量,这个变量就叫指针变量。存放在指针变量中的值都被当做地址处理
指针变量的定义和大小
指针变量定义时只需要在类型名后加一个 * 就可以了,如:
int* p = &a ;
指针就是地址,我们平时口头说的指针是指针变量。
指针变量存放的地址用 %p 打印:

(编址)
对于32位的机器 , 假设有32根地址线 , 那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);
那么32根地址线产生的地址就有32个0或1,能够寻找2的32次方个地址(4GB),所以32位的机器的内存通常在1到4GB。
这里我们就明白:
● 在32位(x86)的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
● 那如果在64(x86_64)位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
总结:
● 指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
● 指针的大小在32位平台是4个字节,在64位平台是8个字节。
指针的类型
上面说了指针的大小在32位平台是4个字节,在64位平台是8个字节,既然指针的大小是一样的,那为啥我们在定义指针的时候要指定它的基类型呢?为啥不用一个统一的类型来定义指针变量呢,如 ptr i ?
分析以下代码:

将十六进制数赋值给整形变量 a 后,定义一个字符指针变量 p ,使它指向 a ,将 0 赋值给 *p(*p就是 a) 后,我们发现 a 的值改变为 0x11223300 ,为什么 a 的值不改变为 0x00000000 呢 ?
(上面的代码中,a 是整形变量,p 是字符指针变量,将 a 的地址赋值给 p 要将 a 的类型强制转换为 char* ,不然编译器会报出警告)
这是因为:
指针类型其实是有意义的
1. 指针类型决定了指针进行解引用操作的时候一次性访问几个字节
如果是 char* 的指针,解引用访问 1 个字节
如果是 int* 的指针,解引用访问 4 个字节
与指针指向的变量类型无关

2. 指针类型决定指针的步长 ( 对指针 +1 或 -1 跳过几个字节)
字符指针 +1 , 跳过 1 个字节
整形(int*)指针 +1 , 跳过 4 个字节
(指针的类型还有 int(*)[ ],这是指向一维数组的指针,具体见下文。)
指针的解引用
解引用操作符(*)用于访问指针所指向的内存位置中的数据,而不是地址本身。
例如,如果有一个指向整数的指针 p,那么 *p 的意思是通过 p 的值(一个地址)找到是这个地址的变量的值,再返回这个值,使之替代 *p 。同样,如果有一个指向字符的指针 q,那么 *q 将返回 q 所指向的字符。
解引用操作可以用于读取和修改指针所指向的值。
需要注意的是,解引用操作符只能用于有效的指针,即指向有效内存地址的指针。如果指针是空指针、未初始化的指针或者指向已经释放的内存块的指针(野指针),解引用它将导致未定义的行为,可能会引发程序崩溃或其他错误。因此,在使用指针时,应该确保指针是有效的,并且在解引用之前进行必要的检查。
危险的野指针
野指针的成因
1、指针未初始化
int* p;
*p = 20;
整形指针变量 p 未初始化,可能指向系统文件,随意篡改系统文件将导致严重的后果。
2、指针的越界访问
int a[10] = {0};
int*p = a;
for( int i = 0 , i <= 11 , i++ )
{
*p = i;
p++;
}
当指针的指向超出数组的范围时,该指针就是野指针。
3、指针指向的空间已被释放
int* test()
{
int a = 10;
return &a;|
}
int main()
{
int*p = test();
printf("%d\n",*p);
return 0;
}
退出 test 函数时,变量 a 已经被销毁,再用指针变量 p 去访问 a 的存储空间不合法。
但我们如果运行以上代码,会发现可以输出 10,这是因为在指针变量得到 a 的地址时立刻就打印 p 所指向的地址,test 函数的函数栈帧没有被覆盖,p 所指向的地址存储的还是 10。如果我们在printf(“%d\n”,*p) 前面加上printf(“hehe\n”),发现打印出来的就不是 10 了。
如何规避野指针
1. 指针初始化
在定义一个指针变量时,就指定它的指向。如 int*p = &a,
如果暂时不知道该指针该指向谁,
要使它成为一个空指针(NULL,NULL = (*void)0)
空指针有时也非常危险,如:int*p = NULL;*p = 10;(p 不指向任何一个变量,对 p “所指向的变量赋值 10 是非法的)
规避空指针可以用 if(p != NULL){...}
2. 小心指针越界
3. 指针指向空间释放,及时置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性(是不是空指针)
指针运算
1、指针加减一个整数的运算
指针加减一个整数的意义参考上文
2、指针减去指针(地址减去地址)
前提:两个指针指向同一个数组
意义:得到的结果的绝对值是两个指针之间的元素个数。
应用:模拟实现 string 函数

3、指针的关系运算
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
指向数组的指针
指针的类型(如 int*、char*)决定了指针的步长,有一种指针的步长是整个一维数组,即指向一维数组的指针。
如何定义指向数组的指针:
数组的类型(*指针名)[数组元素个数]
如:
int arr[5];
int(*p)[5] = &arr;
这样就定义了一个指向一维整形数组的指针,它的步长是 5 * 4 = 20,即对 p 加 1 ,p 跳过 20 个字节,正好是数组 arr 的所有字节。
注意:
不能写成:int* p[5] = &arr; ,这样 p 就先与 [5] 结合,p 就是一个指针数组了。
也不能写成:int(*p)[5] = arr;,因为 p 的类型是 int(*)[5],而 arr 的类型是 int*。
指向数组的指针一般用在二维数组。
int(*p)[5] = &arr;其中 p 还可以是数组,如 int(*p[5])[5];,具体见下文指针数组中“数组指针数组”
一维数组的指针
引用一维数组元素的方法:
1、下标法,如 a[1]、a[2]
a[i] 与 *(a + i)等价
[ ] 是变址运算符,编译时会把 A[B] 转化为 *(A + B)
2、指针法
一维数组的数组名是数组首元素的地址,这个地址是可以放在指针变量中的,可以通过该指针访问数组元素
int a[10];
int*p = a;//不能写成int*p = &a;
则
*(p + i)、 a[i] 、 p[i] 等价
(一维数组的数组名是指针常量,不能对它赋值)
综上所述,若 a 是一维数组名,p 是指针变量且 p = a,则:
a[i] 、 *(a + i)、 *(p + i)、 p[i] 等价

指针数组
指针数组是数组,是存储指针的数组。
整形指针数组:
int main()
{
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 2,3,4,5,6 };
int arr3[5] = { 3,4,5,6,7 };
int arr4[5] = { 0,0,0,0,0 };
int* arr[4] = {arr1, arr2, arr3, arr4};
return 0;
}
arr 就是一个整形指针数组。其中的每一个元素都指向它所指向的数组的首元素。
字符指针数组:
int main()
{
//存放字符指针的数组
char* arr[4] = { "abcdef", "qwer", "hello bit", "hehe" };
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%s\n", arr[i]);
}
return 0;
}
arr 就是一个字符指针数组,其中的每一个元素都指向它所指向的字符串的首字符的地址。
“数组指针数组”:
数组指针数组中,每一个元素都是一个指向数组的指针。
int main()
{
int a[5];
int b[5];
int c[5];
int(*p[3])[5] = { &a,&b,&c };
return 0;
}
p 就是一个数组指针数组,p先与 [3] 结合,说明 p 是数组,然后 p 的类型是 int(*)[5],即 p 是有 5 个元素的数组,每个元素的类型是 int(*)[5]。
二维数组的指针
若 a 是二维数组名,则:
上图是部分有关二维数组的指针的表达,其中类型可以理解为”步长“,即对该指针进行加减一个整数,指针跳过的字节数。
&:这个运算符可以理解为“升维”,假如 arr 是一维数组名,arr 是指向数组元素的指针,则 &arr 就是指向一维数组的指针。
*:这个运算符可以理解为“降维”,假如 arr 是一维数组名,arr 是指向数组元素的指针,则 &arr 就是指向一维数组的指针,*&arr就是又指向数组元素的指针了。
用一维指针数组模拟二维数组:

定义了三个一维数组:
int a[4]; int b[4]; int c[4]; 其中 a ,b ,c 是一维数组名,表示数组首元素的地址。
又定义了一个一维指针数组:
int* arr[3] = { a,b,c };则 arr[1]表示 a 数组首元素的地址。

上图中,根据 [ ] 变址运算符的运算,arr[i][j] 等价于 *(a[i] + j ),而 a[i] 等价于 *(a + i),所以 arr[i][j] 等价于
*(*(a + i) + j )
[ ] 是变址运算符,编译时会把 A[B] 转化为 *(A + B)
这样子应该可以很好理解二维数组的指针。
用 windows 自带的计算器计算指针步长来判断指针的类型:


用计算器计算 4C - 38 = 14(十六进制)= 20(十进制),说明对 a 加一,a 跳过了 20 个字节,正好是 5 个数组元素的长度,推测 a 的类型是 int(*)[5]。
二级指针
指针变量是变量,是变量就有地址,指针变量的地址可以存储到二级指针。

二级整形指针可以用 int** 定义,三级整形指针可以用 int*** 定义,以此类推。
如:int* *p;第二颗 * 表示 p 是指针变量,第一颗 * 表示 p 的类型是 int* 型。
对二级指针加 1 ,与一级指针一样。如对上述 p 加 1,p 跳过 int* 类型所占的字节数。
怎么通过二级指针访问它最终指向的变量呢?
如上图:*ppa ----> pa ----> *pa ----> a
也可以用 **ppa ----> a
二级指针的作用:
在自定义函数中,如果我们想改变主函数的实参的值,比如 int a = 10;在自定义函数中我们想将它的值改变为 20,那么自定义函数的参数就不能是一个 int 类型的变量,因为形参是实参的临时拷贝,形参的改变不影响实参。如果自定义函数的实参是 Int* 类型的,就可以解决问题。
同样的道理,如果我们想在自定义函数中改变一个 int* 类型变量的指向的内容,那么形参就不能是 int* 类型的,而是指向 int* 类型的指针,即 int**。
例:
void Func(int* ptr)
{
ptr = (int*)malloc(sizeof(int));
}
int main()
{
int* px = NULL;
Func(px);
return 0;
}
ptr 的改变不影响 px 的值。(内存泄漏)
void Func(int** ptr)
{
*ptr = (int*)malloc(sizeof(int));
}
int main()
{
int* px = NULL;
Func(&px);
free(px);
return 0;
}
这才是正确的做法。
字符指针
const char *p = "abc";
*p = "hello world !";//error
上面代码中 p 就是一个字符指针,用 const 修饰 *p 的原因是“abc”是字符串常量,存储在字符常量区(而 p 是指针变量,存储在栈区),不能被修改,如果试图修改,程序会报错说“表达式必须是可修改的左值”,如果不加 const ,程序不会报错,但是在运行时会崩溃,这时你不知道哪里出错了,所以定义字符串指针最好声明为 const char*。
一道面试题:
int main()
{
char str1[] = "hello world.";
char str2[] = "hello world.";
const char* str3 = "hello world.";
const char* str4 = "hello world.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("strl and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
应该注意字符数组与字符串指针的区别:
对于字符数组:
各自在栈上分配独立的内存空间,即使内容相同
对于字符串指针:
指向字符串常量(在字符常量区)
编译器会对相同的字符串常量进行优化(字符串池化),即:str3 和 str4 都指向“hello world”
注意字符数组初始化的不同:
char str1[] = "hello bit."; // 自带 '\0'
char str2[] = { 'h','e','l','l','o',' ','b','i','t','.' }; // 没有 '\0'
传参时的参数设计
一维数组传参
#include <stdio.h>
void test(int arr[])//正确
{}
void test(int arr[10])//正确
{}
void test(int *arr)//正确
{}
void test2(int *arr[20])//正确
{}
void test2(int ** arr)//正确
{}
int main()
{
int arr[10] = {0};
int *arr2[[0] = {0};
test(arr);
test2(arr2);
return 0;
}
一维数组传参时,实参是数组名,形参可以是:int arr[ ]、int arr[10]、int* arr。
实际上,不管形参是哪种形式,本质上传的都是都是指针,是实参数组首元素的地址。
当形参是 int arr[ ] 时,编译器认为它是指针,其实 int arr[10] 中不必指定数组的大小,编译器不在意形参数组的大小。
二维数组传参
void test(int arr [3] [5])//正确
{}
void test(int arr[][])//错误,第二个[]的数字不能省略
{}
void test(int arr[] [5])//正确
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//错误,arr是指向维数组的指针,类型不是 int*
{}
void test(int* arr[5])//错误
{}
void test(int (*arr) [5])//正确
{}
void test(int ** arr)//错误
{}
int main()
{
int arr[3][5] = {0};
test(arr);//arr是二维数组名,类型是:int(*)[5],即指向一维数组的指针
return 0;
}
二维数组传参时,实参是数组名,形参可以是:int arr[ ][5]、int arr[3][5]、int(*arr)[5]。
编译器在检查形参时,不检查一维的大小,所以 int arr[3][5] 中一维的大小是无需指定的。
一级指针传参
这个很简单,实参就是一级指针名,形参可以是 int* p,
思考 : 当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
二级指针传参
这个也很简单,实参可以是二级指针名或一级指针的地址,形参是二级指针:int** ptr
思考 : 当一个函数的参数部分为二级指针的时候,函数能接收什么参数?
例:指针数组的数组名,因为指针数组的数组名是二级指针(int* arr[5],arr 的类型是int**)
函数指针
int Add(int x, int y)
{
return x + y;
}
int main()
int (*pf)(int, int) = &Add;//正确
int (*pf)(int, int) = Add;//也正确
//&函数名和函数名都是函数的地址
//pf 是一个存放函数地址的指针变量 - 函数指针
return 0;
}
如何定义函数指针
函数返回值类型 (*函数指针名)(形参1,形参2...)
如以上代码的 pf 就是一个指向函数的指针,对它赋值时,既可以用 & + 函数名,也可以直接用函数名。
函数指针的类型:
函数返回值类型 (*)(形参1的类型,形参2的类型...)
如以上代码的 pf 的类型是 int(*)(int,int)
分析以下代码:
int main()
}
(*( void (*)() ) 0 )() ;
return 0;
}
解析:从 0 开始分析,0 的左边首先是一个括号,括号的内容是 void(*)(),这是函数指针的类型,即将 0 强制转化为 void(*)()类型,也就是将 0 当成一个函数的地址,然后对 0 解应用 *,再调用这个函数。
分析以下代码:
#include <stdio.h>
int main()
{
void (* signal( int, void(*)(int) ) )(int);
//该代码是一次函数的声明
//声明的函数名字叫signal
//signal函数的参数有2个,第一个是int类型,第二个是函数指针类型,该函数指针能够指向的那个函数的参数是int
//返回类型是void
//signal函数的返回类型是一个函数指针,该函数指针能够指向的那个函数的参数是int,返回类型是void
return 0;
}
以上代码可以用 typedef 简化:
typedef void (*p)(int);//使 void(*)(int)这种类型的名字叫作 p
则 void (* signal( int, void(*)(int) ) )(int);可以简化为:
p signal(int,p);
使用函数指针调用函数
int Add(int x, int y)
{
return x + y;
}
int main()
int (*pf)(int, int) = &Add;
int (*pf)(int, int) = Add;
int ret = (*P)(2,3);
printf("%d\n",ret);
return 0;
}
以上代码使用(*pf)(2,3)的形式来调用 Add 函数其实是不必要的,可以直接用函数指针名调用 Add 函数:pf(2,3)。
函数指针数组
函数指针数组是存储函数地址的数组。
如何定义函数指针数组
函数返回值类型 ( *数组名 [元素个数] )(形参1的类型,形参2的类型...)
如:
int ( *p [ 5 ] ) ( int int );
p 先与 [ 5 ] 结合,说明 p 是数组,然后 p 的类型是 int(*)(int int)型,这是指向函数的指针类型,说明 p 是函数指针数组。
p 中存放的是函数的地址(指向函数的指针,或函数名),如:
int ( *pf [5] )( int, int ) = { NULL, Add, Sub, Mul, Div };
pf 中存放的是 Add、Sub、Mul、Div(自定义函数的函数名)
函数指针数组的类型:
函数返回值类型 ( * [元素个数] )(形参1的类型,形参2的类型...)
应用:当程序中函数太多,并且这些函数的返回值、形参的数量和类型一致时,可以把它们放在一个函数指针数组中,避免使用 switch 语句,这样可以简化程序。
指向函数指针数组的指针
“ 指向函数指针数组的指针”是指针,该指针指向一个数组(存放一个数组的地址),这个数组存放指向函数的指针。
如何定义“指向函数指针数组的指针”
函数返回值类型 (*(*函数指针名)[元素个数])(形参1的类型,形参2的类型...)
如:
int(*(*p)[5])( int,int)
p 就是一个 “ 指向函数指针数组的指针”,p 先与 * 结合,说明 p 是指针,然后 p 与 [ 5 ] 结合,说明 p 指向的是数组,数组中每个元素的类型是 int(*)(int,int),这是函数指针的类型。
“指向函数指针数组的指针”的类型
函数返回值类型 (*(*)[元素个数])(形参1的类型,形参2的类型...)
回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针 ( 地址 ) 作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时 , 我们就说这是回调函数。回调函数不是由该函数的实现方直接调用(不直接调用该函数), 而是在特定的事件或条件发生时由另外的一方调用的 , 用于对该事件或条件进行响应。
例子:

以上代码是一个简易计算器的代码片段,我们发现有一些代码(蓝色框)总是相似并重复的出现,为简化代码,我们将蓝色框内的代码封装成一个函数 calc( ):

calc( )的返回值类型是 void,形参是 int(*pf)(int,int),这是函数指针,通过 calc 函数调用 Add 函数,即间接调用 Add 函数,此时 Add 就是一个回调函数。calc 就像一个“控制中心”
void* 类型的指针
void* p;
p 是一个 void* 类型的指针,它可以接收任何类型的数据的地址:
int a = 0;float b = 5.5f;double c = 9.9;
p = &a;p = &b;p = &c;//都是合法的
不能对 p 解引用:
printf(“%d”,*p)//error
也不能对 p 加减一个整数:
p++;//error,p 是 void 类型,p 的步长不确定。
可以对 p 强制类型转换:
(int*)p;//合法
qsort 函数
qsort 函数是一个库函数,使用它时要包含头文件 include <stdlib.h>
它利用快速排序对数组排序,函数原型如下:
void qsort (void* base, size_t num, size_t size,
int (*compar)(const void*,const void*));
下面介绍它的参数:
void* base:数组的起始地址,就是数组名
size_t num:数组元素的个数,size_t 就是 unsigned int 类型
size_t size:数组元素的类型占多少个字节
int ( *compar ) ( const void* , const void* ):
指向一个叫 compar 的函数,compar 函数有两个参数:const void* e1, const void* e2
e1、e2 就是待比较的数组元素的地址(因为 qsort 函数的作者不知道用户要排序的数组的元素类型,所以将 e1、e2 设计成 void* 类型,comper 函数由用户自己设计,用户设计 compar 函数时就可以强制类型转换 e1、e2)当 *e1> *e2 时,qsort 函数要求 compar 函数返回一个正数,*e1= *e2 时,返回 0,*e1 < *e2 时,返回负数。我们可以将 compar 函数设计成:
要排序的数组的元素类型 compar(const void* e1, const void* e2)
{
return *(要排序的数组的元素类型 *)e1 - *(要排序的数组的元素类型 *)e2;
}
模拟实现 qsort 函数
指针的练习题
1、
#include <stdio.h>
int main()
{
int a[4] = { 1,2,3,4 };
int* ptr = (int*)((int)a + 1);
printf("%x", *ptr);
return 0;
}
先将 a 的类型强制转换为 int 类型,再对它加 1,假设 a 的值是 0x0012ff40,加 1 后变成0x0012ff41,此时 a 指向 01 的下一个字节。再把((int)a+1)的值强制转换为 int* 赋给 ptr,对ptr 解引用一次性访问 4 个字节(紫色框),再以 %x (以十六进制形式打印)输出 *ptr 的值,由于是小端字节序存储,所以输出20000000。

2、
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf ( "%p %d\n" &p[4][2] - &a[4][2] &p[4][2] - &a[4][2] );
return 0;
}
a 的类型是 int(*)[5],而 p 的类型是 int(*)[4],将 a 的值赋给 p 时,最后 p 的类型还是 int(*)[4] 类型,

如图,&p[4][2] - &a[4][2] 的值是 -4,现在用 %p (用十六进制打印)打印这个数,-4 在内存中的存储形式是:
11111111 11111111 11111111 11111000 - 负 4 的补码
%p 认为 11111111 11111111 11111111 11111100 是一个十六进制数,转化为十六进制就是:
0x FF FF FF FC,所以最后打印的是 FFFFFFFC -4。
3、
#include <stdio.h>
int main()
{
char* a[] = { "work", "at", "A" };
char ** pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}

4、
#include <stdio.h>
int main()
{
char* c[] = { "ENTER" , "NEW" , "POINT" , "FIRST" } ;
char** cp[] = { c + 3, c + 2, c + 1, c } ;
char*** cpp = cp;
printf("%s\n", ** ++cpp) ;
printf("%s\n", *--* ++cpp + 3);
printf("%s\n", *cpp[-2] + 3);
printf("%s\n", cpp[-1][-1] +1);
return 0;
}
定义了一级字符指针数组 c,c 中每一个元素的值是个个字符串的首字符的地址。
定义了二级字符指针数组 cp,cp 中每一个元素的值是 c 的一个元素的地址。
定义了三级字符指针 cpp,cpp 的值是 cp 数组首元素的值。
**++cpp:对 cpp 自加一,cpp 原先指向 cp[0],现在指向 cp[1](cpp 的类型是 char**,对它加 1 跳过一个 char** 类型的字节数,即 cp 数组一个元素的字节数),对 cpp 解一次应用,得到 cp[1] 的值(即 c + 2),对 cp[1] 解一次应用(也就是对 cpp 解两次应用),得到 c[2] 的值,c[2] 的值就是常量字符串“POINT”首字符 ‘P' 的地址。用 %s 打印时,%s 的参数是一个地址。
*--*++cpp + 3:(++,-- 的优先级比 * 高)对 cpp 自加一,cpp 原先指向 cp[1],现在指向 cp[2], 解一次应用,得到 cp[2] 的值(即 c + 1),再对 cp[2] 的值(即 c + 1)减 1,cp[2] 的值变为 c ,再对 cp[2] 解一次应用,得到 c[0] 的值,c[0] 的值就是常量字符串“ENTER”首字符 ‘E' 的地址,再对 c[0] 加 3,c[0] 的类型是 char* ,加 1 跳过 1 个字节,加 3 就跳过 3 个字节,现在,*--*++cpp + 3 这个表达式的值就是常量字符串“ENTER”第 4 个字符 ‘E' 的地址。然后用 %s 打印,打印的结果是 ER。
*cpp[-2] + 3:cpp[-2] 与 *(cpp - 2)等价,因此 *cpp[-2] + 3 就是 **(cpp - 2),之后的分析同理。
2751

被折叠的 条评论
为什么被折叠?



