🏐tips: 本文部分内容改编自《c和指针》。

📰引入

🏉内存与地址

在了解指针前,我们需要先理解内存地址的含义。
在c和指针一书中,很形象的将计算机的内存比作是一排排的房屋,至于房屋有多大,取决于你所处的环境。计算机内存由以亿记的位(bit)组成,每一个bit都可以容纳下0或者1,但如果将一个bit位作为一个房屋,那容纳的量未免太小了。所以我们通常会将许多位共同组成一栋房子,这样的房子被我们称为字节(btye)。一个字节容纳八个位。字节也可以由多个组成更大的单位——,每个字都有2字节或者4字节组成。需要注意的是,虽然字节或者字会包含多个比特位,但通常我们只认为他只有一个地址

上图将三者关系做了一个解释。
当我们知道了一个地址,就可以通过调用这个地址来获得存储在这个地址上的值。但这样做是很笨拙的,因为我们在写程序时不会也不能记住每一个需要被调用的值的地址,所以我们常常使用一个名字来代替地址。
这里的1个单位代表1个字节。

正如上面所写的,他们只可以有一个地址。这个地址通常是左边第一个位。
在介绍这一块时,c和指针一书还额外提了一点——不可以简单的通过检查一个值来判断其类型,这点也是非常重要的,要判断其类型就需要去观察其使用方式。例如,一个32位的数字串可以被解释为多种不同的类型。

🏸什么是指针

指针,其实是指针变量,只是口头上我们将其口语化为指针,真实的指针其实指的是地址。指针变量,是用来存放地址的,而地址呢,则是起到可以唯一的标识一块空间的作用。

上图的c就可以被看做是一个指针,c中存储的值是a的地址。

1
int* c=&a;

&是一个取址符,代表取出a这块空间的地址,*则代表c是一个指针,int则是c指向的那块空间的类型。那么这个表达式的意思就是:定义了一个c,起作用是存储a的地址,同时a是一块整形变量,那么这个指针就是指向整形的指针。
一定要会区分c的值与c的地址。c的地址是108,但存储的地址是100。
但c并不可以自动的去取得a中的值,这点非常重要。c仅仅是存储a的地址,想要通过地址访问地址里存储的数据我们称为解引用指针或者间接访问。用于这个操作的操作符叫做*,*c作为左值时就可以得到a中所存储的数据,50。
这就是指针的基本使用。
指针的大小并不受其类型的控制,而是受到了平台的限制。在32位平台,指针的大小为4字节,在64位平台,指针的大小就是8字节。那他的类型有什么作用呢?答案是决定走的步长。在很多时候,我们都会利用指针这个特性,去取到一些我们需要的数值。例如在int型的空间里,我们可以使用char型的指针去取其中的某个字节。这一特性使得指针灵活性大大提高。这个特性会放在后面去讲。
指针,是c语言中堪称核心的一环,也是c语言的灵魂。但指针并非是什么困难高深的东西,指针只是一扇门,当你推开它时,你才能真正的进入c语言。

🏑 非法指针与空指针

1
int* p;

很明显这是一个错误的示范,只是定义了一个指针,却没有给出指向的地址。那么接下来程序的操作是不可预知的。如果程序为其分配一个非法的地址,那么程序报错终止,你很幸运知道了这里有问题。但要是分配的是一个合法的地址,你对指针进行操作就有可能修改这个你不知道的地址,这种错误是很难去捕捉的,所以请确保在定义一个指针时,确保它被初始化
在指针里有一个十分特殊的存在,NULL指针。它并不指向任何东西。同时,对NULL指针进行解引用是非法的。有些编译器会访问0这个地址,但大多数的编译器会直接报错,这样是更好的情况,有一句话是这么说的:宣布错误比隐藏错误要好得多,因为程序员会更容易的修正它。

📓各种指针

📔字符型指针

字符型指针的类型是char*。例如一个简单的例子:

1
2
char a='a';
char* p =&a;

*代表了p是一个指针,char则表示指针指向的类型。&则是取到了a的地址。
指针也可以直接指向一个常量字符串,例如

1
char* p="chengzi";

此时的p更具体的来说,是指向了这个字符串的第一个字符---c。同时需要注意的是,由于此时指向了常量字符串,所以p指向的内容是不应该被修改的,那么更规范的写法应该是给char前面加一个const,限制p,使其指向值不能通过p来解引用而引起改变。
下面这个例子也是同样的道理。

1
2
3
4
char arr1[]="chengzi";
char arr2[]="chengzi";
char* arr3="chengzi";
char* arr4="chengzi";

上面一共给出了4个项,那么请问arr1是否等于arr2呢?arr3又是否等于arr4呢?
答案是 arr1不等于arr2,arr3等于arr4 。
首先我们要明白,等于在这里是什么意思?在程序里我们通常这样解释:

1
if(arr1==arr2)

这里的等于并非数值上的相等,而是地址是否相同。那么arr1和arr2是不相同的。虽然两个字符串数组的初始化内容一样,但开辟的内存块是不同的,也就是说,本质上arr1和arr2只是形状一样的处于不同地点的两栋房子,其本质上是具有区别的。而指针就不同了,指针可以理解为门牌号,arr3和arr4都是一个门牌号,他们都指向chengzi这栋房子。从内存角度来讲,c/c++会将常量字符串存储至单独的一块内存区域,当有几个指针同时指向这个字符串时,都是指向同一块内存的。

📒数组指针

在讲数组指针之前,先谈谈指针数组
指针数组的本质是一个数组而非指针。只是这个数组中存放的是指针型而已。千万不要搞混淆了。

1
2
int* arr[10];
char* str[10];

这样类型的都是指针数组,存放着地址的数组
但数组指针就不一样了,数组指针是指针。那么类比整型指针,字符型指针,不难得出结论:数组指针,就是指向数组的指针。
其基本格式为:

1
int (*p)[10];

括号不能去掉,一旦去掉,就成为了指针数组。因为在一般情况下,[]的优先级会比*更高。
括起来之后,*就先与p结合,说明了此时的p是一个指针,再将p取出来,剩下的就是int(*)[10],说明其指向的是一个存放了10个数的数组。

arr与&arr的区别

在这里提一下arr与&arr的区别,arr是一个数组的名字,在编译器上对二者进行printf,会发现两个值通过%p打印出来都是相同的地址,这能否说明二者相同呢?
在编译器上运行这个代码:

1
2
3
int arr[10]={0};
printf("%p\n",arr);
printf("%p\n",&arr);

二者打印结果相同。再加上两行:

1
2
printf("%p\n",arr+1);
printf("%p\n",&arr+1);

运行得到结果,会发现第三个第四个的打印结果不一样。
这里参照我的运行实例。

00000080F2B8FB78
00000080F2B8FB78
00000080F2B8FB7C
00000080F2B8FBA0

第三个和第四个相减,转换为十进制,得到36。
所以,不难猜到,&arr表示的是数组的地址,而并非数组首元素的地址。
那么联系到上文提到的指针的步长,&arr其实就是指向的就是int(*)[]类型,它加1就使得整个指针跳过了数组。而arr单单只可以表示首元素的地址。那么,其+1就不过是将指针指向arr[1]而已。二者差距9个元素,一个元素4字节,也就是36个字节了。

📕使用示例

那到底数组指针有什么意义呢?它在写的代码里又能起到什么作用呢?
要记住的是:数组指针中存放着数组的地址。

1
2
3
4
5
int main()
{
int arr[3][3] = { 1,2,3,4,5,6,7,8,9 };
PrintArr(arr, 3, 3);
}

这里传递的arr实际上就是相当于第一行的那个一维数组的地址,这很重要。在一维数组中arr是第一个元素的地址,同时也是整个数组的地址,二维数组中,arr则是第一个元素的地址,也是第一个一维数组的地址。
实现一个函数,让PrintArr打印出这个二维数组。其中的一个参数就可以用数组指针来接收这个一维数组的地址。

1
2
3
4
5
6
7
8
9
void PrintArr(int(*arr)[3],int a,int b) {
for (int i = 0; i < a; i++)
{
for (int j = 0; j < b; j++)
{
printf("%d ", arr[i][j]);
}
}
}

类比一下一维数组传参,可以传入int arr[],也可以传入int* arr,此时的arr就是指针,arr+1就会跳过4字节,也就可以得到数组中第二个数,实际上,arr[1]就等同于arr+1
二维数组传参可以传入整个数组,也就是换成普通的int arr[3][3]。
也可以使用指针的写法。那么此时就需要传入一个指针,这个指针应该是指向一维数组的,例如arr[3][3],就代表有3个一维数组,每个一维数组都有3个整形数据,那么传入一维数组的指针即可。
int(*arr)[3] 不正是这个例子吗,传入的arr是第一个一维数组的地址,也正好作为指针使用,arr[1]就代表直接跳到了第二个一维数组,arr[2]就跳到了第三个一维数组,想要访问其中某一个一维数组,直接arr[i][j]即可。
一旦遇到上面这种情况,就一定要睁大眼睛咯,毕竟这种写法在平常较为少见,也不是很好理解。

📗函数指针

函数指针,也就是指向函数的指针。
先上一个示例:

1
2
printf("%p",hanshu1);
printf("%p",&hanshu1);

在编译器上运行,得到的结果也是相同的。二者所得到的都是函数的地址,并且在这里意义相同

函数指针就是用来保存函数地址的,其格式如下:

1
void (*p)();

和上文的数组指针一个道理,这个指针指向一个函数,并且本代码的指针指向函数的类型是void,并且无参。

来看一个很有意思的例子,来自于书《c陷阱与缺陷》。

1
(*(void(*)())0)();

第一眼看起来是个很复杂的式子,但只要拆开来也不是很复杂。首先把void(*)()单独提出来,就是刚才才提到的函数指针类型,它将0强制转换成了一个函数指针的类型,那么整体的来看其实就是将0当作一个函数地址,然后通过解引用的方法去调用这个函数,最后跟的()意思是因为0被调用后是无参类型的,所以()也就不用写参数。

既然指针数组都存在,那么函数指针数组也肯定存在了。放在这里简单的提一下。
其基本格式为

1
int (*p[10])();

根据优先级的规定,p会先与[]结合,说明其是一个数组,去掉这个数组,剩下了int(*)(),不正好就是函数指针类型吗?说明这是一个存放着函数指针的数组。
函数指针数组常用来精简代码量。让代码看起来更简洁且易读。

例如,如果实现一个简单的计算器,就需要做多个函数,例如就添加加减乘除四个功能,就需要单独的四个函数,add,sub,mul,div。然后通过switch结构调用不同的函数来实现。
但可以使用函数指针数组来实现代码的精简化。
例如:

1
int(*p[4])(int x ,int y)={add,sub,mul,div};

这样就将四个函数放进了一个函数指针的数组。(&函数名和函数名是同样作用)
然后通过控制p[i]中的i,就能调用不同的函数,就实现了对switch结构的精简化。

📗指向函数指针数组的指针

正在逐步的套娃化·····
简单的对定义做一个拆分,这个指针指向一个数组,其数组里装的元素都是函数指针类型。
该如何去书写呢?

1
2
3
4
5
void(*p)()=test; //函数指针p指向test函数
void(*parr[5])();
*parr[0]=test; //将test函数存入一个函数指针数组
void(*(*pparr)[5])()=&parr; //指向函数指针数组的指针

拆分一下,* 先与pparr结合,说明了pparr是一个指针。把 * pparr丢掉,得到void(*()[5])(),其是一个函数指针数组类型。
但是并不怎么用到,只是简单的介绍。
到这里,就将c中常见的指针介绍完毕了。

📘传参

接下来也是个重点内容—传参。在构建函数时,我们总是会遇到一个问题—参数到底怎么写?
在上文就已经简单的介绍过数组的传参了,想要看懂还是需要对一维和二维数组有一定的理解与掌握。
简单的就像普通的数据类型,int,char,稍复杂的会遇到二级指针之类的,在这里我们简单的盘点一下常见的函数传参怎么个写法。

📙数组传参

这块可以分为一维和二维来处理。
先说一维的。

1
2
int arr1[10]={0};
int* arr2[10]={0};

在这里定义了两个数组,arr1存放int型,arr2存放int*型,那么二者该如何放进函数里进行传参呢?
常见的有这五种。

1
2
3
4
5
6
7
//arr1
void test(int arr1[]);
void test(int arr1[10]);
void test(int* arr1);
//arr2
void test(int* arr2[10]);
void test(int**arr2);

简单的讲一下。
对于arr1而言,就是一个简单的int型数组,那么最普通的写法也就是照猫画虎直接传,[]里面的可写可不写。
也可以写成指针的形式,传入arr1这个数组的首地址。
arr2也是同样的道理,要不就照猫画虎,要不就采用指针的形式来传入。这里的int* *arr2,第一个 * 和int一起,代表指针指向类型为int *,第二个 *则代表这是一个指针。这也是一个二级指针,应用也是非常广泛,当需要对指针进行更改时,由于传参进入的指针仅仅是一份拷贝,那么就可以考虑传入指针的指针,也就是二级指针。

讲完一维再来说说二维。

1
int arr[2][2]={0};

下面哪几种是可行的呢?

1
2
3
4
5
6
7
void test(int arr[2][2]);
void test(int arr[][]);
void test(int arr[][2]);
void test(int *arr);
void test(int* arr[2]);
void test(int(*arr)[2]);
void test(int* *arr);

这里提供了七种写法,一种一种的来分析下。
第一种肯定是没问题的,原样传入。第二种就不行了,二维数组由于其特殊性,至少需要要求列的数值,如果没有列的数值,是无法计算的。而行可以省略。那么第三种也是可行的。
第四种是一个典型的一维数组传法,所以不正确。
第五种则是上文讲到的指针数组,其本质是数组,存放的是指针。不正确。
第六种是一个数组指针,在这里想一下二维数组的相关知识点。二维数组可以看成多个一维数组排列而成的。如果传入像第四种,那么传入的实际就是第一行那个一维数组的首元素地址,也就无法接收到整个二维数组了。
那么第六种显然是可行的,使用数组指针来接收数组。
第七种采用的是二级指针的写法,在这里书写显然是不合适的。int**arr本质上用于接收一级指针的地址

📚指针传参

接下来就讲讲一级和二级指针传参。

先说一级。

1
void test(int* p);

当你看到这样的例子,你能写出它能接收哪些形式的传参吗?
大概常见的有以下几种。

1
2
3
test(&p);
test(pp); //这里的pp是一个一级指针
test(arr); //arr是数组名

指针为二级的时候呢?

1
void test(int* *p);

答案如下:

1
2
3
4
5
6
int* *prr;
test(ppr); //接收为二级,传入当然也可以为二级
int *pp;
test(&pp); //取一级指针的地址
int* arr[3];
test(arr); //传入指针数组

稍微提一下指针数组。当传入arr时,p就可以作为指向int*型的指针。其本质与传入一维数组时,参数写成int *p是一样的道理。与p相结合的 * 说明p为指针。

🎿指针表达式

下面介绍一些常用的指针表达式。在运用指针时,指针的各种表达式被当做左值或者右值是常有的事,那么理解其意义并运用就是我们的基本功了。
接下来会用到左值与右值的概念。

左值:指表达式结束后依然存在的持久对象,可以取地址,具名变量或对象
右值:表达式结束后就不再存在的临时对象,不可以取地址,没有名字。
比如 int a = b + c;,a 就是一个左值,可以对a取地址,而b+c 就是一个右值,对表达式b+c 取地址
会报错。左值指既能够出现在等号左边,也能出现在等号右边的变量;右值则是只能出现在等号右边的变量。

1
2
char ch='a';
char* cp=&ch;

给出了一个字符型指针cp,指向ch,ch中存储的值是字符a。

1
ch

ch作为右值,其就是代表’a’,这个没什么好说的。
ch作为左值,就代表被命名为ch的这块空间。那么此时为ch赋值就可以更改其存储的数据了。

1
&ch

&ch作为左值,进行求值时,这是非法的,因为&ch确实存在,但你不知道他在哪里。而且左值本就不可以取地址。
&ch作为右值,就是取ch的地址

1
cp

cp作为左值,则代表着cp所处的内存空间。

1
cp=NULL;

我们把cp置为了NULL,这个表达式改变的就是cp这整块空间。

cp作为右值,代表cp的值。

1
int *  p=cp;

也就是将p这个指针也指向了cp指向的那个空间。那个空间的地址就是cp这块空间里所存储的值

1
&cp

&cp作为左值同样是非法的,其地址对程序员而言是透明的。
&cp作为右值,则是取得了cp的地址,并会将其放到某个变量中存储起来。

1
*cp

\cp是间接引用操作,\cp作为左值,代表对cp这块空间里的地址的值进行操作。
*\cp作为右值,则是则是取出了所指向空间的值,并赋值给某个变量。

1
*cp+1

*的优先级是高于+的。那么*先与cp结合,+1后作为左值是非法的。
作为右值时,*cp取出了’a’,再加1就得到了’b’。

1
*(cp+1)

括起来后,cp+1优先级更高,作为左值时,cp+1代表跳过了一个字节(char类型为一个字节),指向了原先cp后面的那个空间,然后对其进行解引用。
作为右值时,取得原cp后面那个空间的值。
还有许多类似与cp,cp等表达式,搞清楚前置与后置的区别即可。

📖一些例题

❤️题1

声明一个指向含有10个元素的数组的指针,其中每个元素是一个函数指针,该函数的返回值是int,参数是int*,正确的是( )
A.(int p[10])(int)
B.int [10]*p(int )
C.int (
(*p)[10])(int *)
D.int ((int *)[10])*p

题解:整体上去看是一个存放数组的指针。那么其指向类型可以写为 int(*)[],到这里基本就已经确定答案为c了。再去分析下c,将存放这个数组的指针剥离,留下了(int)( * )(int *),和题的后文相吻合。答案确定为c。

💛题2

1
2
3
4
5
6
7
8
int main()
{
int aa[2][5] = {10,9,8,7,6,5,4,3,2,1};
int *ptr1 = (int *)(&aa + 1);
int *ptr2 = (int *)(*(aa + 1));
printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}

求输出结果。

题解: 这里主要考察了对数组取地址的相关知识点。aa+1和&aa+1有着本质上的区别。在上文我们已经介绍过,&aa代表着取出了整个数组的地址,而aa仅代表着取出数组首元素的地址。那么&aa+1就跳过了整个数组,由于指针是int型,那么ptr-1只会减去4个字节,对于int型数组而言,也就是向前挪动一个元素。再来看ptr2,aa在这里是首元素地址,但这里和一维数组不同的是,此时的aa+1是相对于行而言的,他跳过的是第一行,而不是第一个元素,这是需要留意的点。*(aa+1)其实就等价于aa[1],此时它指向的就是第二行首元素,也就是5,减1减去的是4个字节,也就得到了6。那么答案就是输出1,6。

💚题3

1
2
3
4
5
6
7
int main()
{
int a[5] = {5, 4, 3, 2, 1};
int *ptr = (int *)(&a + 1);
printf( "%d,%d", *(a + 1), *(ptr - 1));
return 0;
}

题解:&a取到整个数组,ptr指向&a+1,也就指向了数组的末尾。减一就指向了元素1。a+1则是单纯从首元素+1,在这里就不过多解释了,得出答案,输出4,1。

💙题4

1
2
3
4
5
6
7
8
9
10
11
int a[]={1,2,3};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a+0));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(a[1]));
printf("%d\n",sizeof(&a));
printf("%d\n",sizeof(*&a));
printf("%d\n",sizeof(&a+1));
printf("%d\n",sizeof(&a[0]));
printf("%d\n",sizeof(&a[0]+1));

判断以上示例的输出。
题解:

1
2
3
4
5
6
7
8
9
10
11
int a[]={1,2,3};
printf("%d\n",sizeof(a)); //答案为12,因为sizeof(数组名)时,计算的是整个数组的大小。所以为3*4=12
printf("%d\n",sizeof(a+0)); //答案为4或者8(与编译平台有关),这里不是单纯数组名,而是一个表达式,所以a其仅代表首元素地址,地址+0仍然是地址,其大小为4/8.
printf("%d\n",sizeof(*a)); //答案为4,这里通过解引用操作取出了数组的首元素,其大小为4个字节。
printf("%d\n",sizeof(a+1)); //答案为4/8,a+1明显是一个地址,这里是偏移到了第二个元素的地址。
printf("%d\n",sizeof(a[1])); //答案为4。
printf("%d\n",sizeof(&a)); //答案为4/8,&数组名在一般情况下都是指取出整个数组的地址。
printf("%d\n",sizeof(*&a)); //答案为12,&a拿到整个数组的地址,再进行解引用,其本质还是相当于sizeof(a)
printf("%d\n",sizeof(&a+1)); //答案是4/8,&a+1跳过了整个数组,但指向后面时也仍然是一个地址。
printf("%d\n",sizeof(&a[0])); //答案是4/8,取址符&取到了a[0]的地址。
printf("%d\n",sizeof(&a[0]+1)); //答案是4/8,同上。

总结几点做这种题的关键,也是后面几道题需要掌握的核心知识。

sizeof(数组名)和&数组名,两种情况其中的数组名都是代表整个数组,所以sizeof会计算整个数组的大小,&会取出整个数组的地址。
其他情况遇到数组名都是代表数组的首元素。
遇到地址时,一律判断其大小为4/8,具体取决于你的编译器。如x86平台就是4字节。
&a+1会跳过整个数组,因为&a取出的是整个数组。

💜题5

就不再过多浪费篇幅,题目和答案一同给出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char a[]={'a','b','c','d'};
printf("%d\n",sizeof(arr)); //答案为4,这里是整个char型数组的大小。
printf("%d\n",sizeof(arr+0)); //答案为1,此时的arr仅代表首元素地址。
printf("%d\n",sizeof(*arr)); //答案为1,取出的是首元素。
printf("%d\n",sizeof(arr[1])); //答案为1,同上。
printf("%d\n",sizeof(&arr)); //答案为4/8,是整个数组的地址。
printf("%d\n",sizeof(&arr+1)); //答案为4/8
printf("%d\n",sizeof(&arr[0]+1)); // 答案为4/8

printf("%d\n",strlen(arr)); //答案为未知,因为strlen的计算规则是遇到'\0'才停止。
printf("%d\n",strlen(arr+0)); //答案为未知,同上。
printf("%d\n",strlen(*arr)); //答案为错误,*arr取出的是本质是个int型,而非地址。
printf("%d\n",strlen(arr[1])); //答案为错误,同上。
printf("%d\n",strlen(&arr)); //答案为未知,同样不清楚斜杠0到底位于哪里。
printf("%d\n",strlen(&arr+1)); //答案为未知,同上。
printf("%d\n",strlen(&arr[0]+1)); //答案为未知,同上。

🖤题6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char arr[]="abcdef";
printf("%d\n",sizeof(arr)); //答案为7,和上一题的数组不同,这里的是字符串,所以在末位包含了一个'\0',也要算进sizeof的计数规则来。
printf("%d\n",sizeof(arr+0)); //答案为4/8,arr此时代表地址。
printf("%d\n",sizeof(*arr)); //答案为1。不再赘述。
printf("%d\n",sizeof(arr[1])); //答案为1。
printf("%d\n",sizeof(&arr)); //答案为4/8。
printf("%d\n",sizeof(&arr+1)); //答案为4/8。
printf("%d\n",sizeof(&arr[0]+1)); //答案为4/8。

printf("%d\n",strlen(arr)); //答案为6,此时字符串尾部有斜杠0,所以值不再随机。
printf("%d\n",strlen(arr+0)); //答案为6,同上。
printf("%d\n",strlen(*arr)); //错误。
printf("%d\n",strlen(arr[1])); //错误。
printf("%d\n",strlen(&arr)); //答案为6,同第一种情况。
printf("%d\n",strlen(&arr+1)); //答案为未知,+1跳过了整个数组。
printf("%d\n",strlen(&arr[0]+1)); //答案为5,从arr[1]开始计数,直到斜杠0。

💗题7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char *p="abcdef";
printf("%d\n",sizeof(p)); //答案为4/8,指针的大小也是4/8,取决于平台是32位还是64位。
printf("%d\n",sizeof(p+1)); //答案为4/8,是一个地址。
printf("%d\n",sizeof(*p)); //答案为1,*p取出来的是a这个字符。
printf("%d\n",sizeof(p[0])); //答案为1,和*p同理。
printf("%d\n",sizeof(&p)); //答案为4/8,是一个地址。
printf("%d\n",sizeof(&p+1)); //答案为4/8。
printf("%d\n",sizeof(&p[0]+1)); //答案为4/8。

printf("%d\n",strlen(p)); //答案为6,p里存放的正是常量字符串的地址。
printf("%d\n",strlen(p+1)); //答案为5,p+1偏移到b这个字符上。
printf("%d\n",strlen(*p)); //答案为错误,只能放地址。
printf("%d\n",strlen(p[0])); //错误,同上。
printf("%d\n",strlen(&p)); //答案为未知,取的是p的地址,斜杠0不知道在哪里出现。
printf("%d\n",strlen(&p+1)); //答案为未知,同上。
printf("%d\n",strlen(&p[0]+1)); //答案为5,同第二种情况。

💖题8

1
2
3
4
5
6
7
8
9
10
11
12
int a[3][3]={0};
printf("%d\n",sizeof(a)); //答案为36,这里的a代表了整个二维数组。
printf("%d\n",sizeof(a[0][0])); //答案为4。
printf("%d\n",sizeof(a[0])); //答案为12,这里的a[0]就是二维数组的第一排,也就是一个一维数组。在这里也就是计算一个一维数组的大小。
printf("%d\n",sizeof(a[0]+1)); //答案为4/8,这里是一个地址,代表着二位数组的第二排。
printf("%d\n",sizeof(*(a[0]+1))); //答案为4,解引用出来的就是第二排首元素。
printf("%d\n",sizeof(a+1)); //答案为4/8,这里是第二行的一维数组的地址。
printf("%d\n",sizeof(*(a+1))); //答案为12,对第二行的数组进行解引用,取出了第二行整个数组。
printf("%d\n",sizeof(&a[0]+1)); //答案为4/8,&a[0]是第一行的地址,+1变成了第二行的地址。
printf("%d\n",sizeof(*(&a[0]+1))); //答案为12,同第七种。
printf("%d\n",sizeof(*a)); //答案为12,解引用出来是第一行的元素。
printf("%d\n",sizeof(a[3])); //答案为12。

请牢记,二维数组的名字,相当于是二维数组的第一行的地址,是一个一维数组的地址

💘题9

1
2
3
4
int a[5]={1,2,3,4,5};
int *ptr=(int*)(&a+1);
printf("%d,%d",*(a+1),*(ptr-1));
return 0;

题解:&a是取出了数组的整个地址,+1会跳过整个数组。而(a+1)仅代表其从首元素位置前进4个字节,解引用得到2,*(ptr-1)得到5。

💝题10

假设p为结构体变量指针,结构体大小为20个字节,p的值为0x100000

1
2
3
printf("%p\n",p+0x1);
printf("%p\n",(unsigned long)p+0x1);
printf("%p\n",(unsigned int*)p+0x1);

题解:p自己加1,意味着加一个结构体的大小,所以是0x100014,记得是十六进制哦。
p被强转为unsigned long型,是个整型,整型+1直接加1即可。得到0x100001。
p被强转为指针型,+1就加一个该指针类型的大小,所以+4,得到0x100004。

💕题11

1
2
3
4
5
int a[]={1,2,3,4};
int *ptr1=(int*)(&a+1);
int *ptr2=(int*)((int)a+1);
printf("%x.%x",ptr1[-1],*ptr2);
return 0;

题解: ptr1跳过数组,指向数组尾部。ptr1[-1]可以转化为*(ptr1-1),也就是指向了4,要注意题目是要求十六进制输出。第二个就有点意思了。a被强转成了(int),也就意味着此时+1也不过移动了一个字节而已。那么ptr2解引用取到的就是从首元素的第二个字节开始取四个字节,也就取出了00 00 00 02,
默认环境为小端,还原为02 00 00 00 ,转换为十六进制是多少就不多赘述。

💞题12

1
2
3
4
int a[3][2]={(0,1),(2,3),(4,5)};
int *p;
p=a[0];
printf("%d",p[0]);

题解: 本题存在一个易错点,就在二维数组那里。他并非是一个初始化了三行二列的数组,实际上他只初始化了一排,其中的(0,1)这些是逗号表达式,其值为1,后面的值为3,5,要想完全初始化是这样的:

1
int a[3][2]={{0,1},{2,3},{4,5}};

一定要看仔细了。那么p[0]得到的就是1了。

❣️题13

1
2
3
4
int a[5][5];
int(*p)[4];
p=a;
printf("%p,%d",&p[4][2]-&a[4][2],&p[4][2]-&a[4][2]);

题解:这道题稍难一些。a是一个二维数组,上文讲过,a是第一行的数组地址。那么a的类型就是int(*)[5],p的类型是int( * )[4],二者的类型是不匹配的,但是仍然可以实现强转。但是强转之后就要注意了,二者类型是不同的,那就意味着+1的步长是不同的。这点明白了就很好做题了,也就是,p+1一次跳过4个子节,而a+1一次会跳过五个字节,图就在下方,自行理解。

剩下的也就是%d和%p打印的区别了,%d打印出来就是-4,而当作地址打印时要转换为原码(-4被当作是一个地址),然后换为16进制才行。

<有好题再不定期更新到这来。>