一次吃透C指针

本文详细探讨了C语言中指针与数组的相关知识点,包括sizeof运算符、数组地址、指针运算和字符串处理。通过多个实例分析了各种情况下的输出结果,如指针加减、解引用、数组越界等复杂场景,帮助读者深入理解C语言指针操作的本质。
该文章已生成可运行项目,

前言:

              本章以c指针中经典的笔试题作为主要内容,由于作者的水平有限,本文中难免会有出错不准确的地方,我也很想知道这些出错的地址,恳望读者批评指正。大家一起努力,拿下这块内容 冲!

目录

实例1:

实例2:

 实例3:

实例4:

 实例5:

实例6:

实例7:

实例8:

实例9:

实例10:


实例1:

思考如下代码输出的结果

int main()
{
	int a[3][4] = { 0 };

	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof(a[0][0]));
	printf("%d\n", sizeof(a[0]));
	printf("%d\n", sizeof(a[0] + 1));
	printf("%d\n", sizeof(*(a[0] + 1)));
	printf("%d\n", sizeof(a + 1));
	printf("%d\n", sizeof(*(a + 1)));
	printf("%d\n", sizeof(&a[0] + 1));
	printf("%d\n", sizeof(*a));
	printf("%d\n", sizeof(a[3]));


	return 0;
}

接下来我们来一一解释这段代码:在解释之前我们要首先了解这样一个概念

sizeof(数组名):这里的数组名表示的是整个数组,计算的是整个数组的大小。

&数组名:这里的数组名表示的是整个数组,取出的是整个数组的地址。

除此之外所有的数组名都代表首元素的地址

1.sizeof(a) : a单独存放在sizeof内部,计算的是整个数组的大小,所以这就应该是3*4*4=48。

2.sizeof(a[0][0]):a[0][0]是数组第一行第一列的元素,当然为4。

3.sizeof(a[0]):a[0]我们可以理解为二维数组第一行的数组名,他单独放在了sizeof内部,计算的就是整个数组的大小,第一行4个元素,大小就是4*4=16.

 4.sizeof(a[0] + 1):a[0]不是单独放在sizeof内部,所以a[0]表示的就是第一行第一个元素的地址,+1跳过一个整形,指向的就是第一行第二个元素的地址,计算的是个地址,地址的大小自然是4\8

5. sizeof(*(a[0] + 1)):上文已经解释a[0]+1指向的就是数组第一行第二列的元素,对他进行解引用,计算的便是第一行第二列元素的大小   答案为 4。

6.sizeof(a + 1):a是二维数组的数组名,并没有单存放在sizeof内部,所以a表示的是数组首元素的地址,二维数组首元素的地址其实就是第一行的地址,a+1就直接跳过一行,指向第二行的地址,既然是地址,那肯定就是4\8个字节。

7.sizeof(*(a + 1)):a+1指向第二行这个一维数组的地址,对他进行解引用,计算的就是整个第二行的大小,第二行4个元素 那答案就是4*4=16。

8.sizeof(&a[0] + 1):a[0]代表的是第一行的地址,&a[0]取出的就是整个数组的地址,+1跳过一行指向第二行,他也是个地址,是地址那自然就是4\8字节。

9.sizeof(*a):a没有单独放在sizeof内部,也没有&,所以这个时候的a是首元素的地址,也即是第一行的地址,对他解引用,计算的就是第一行的大小,四个元素答案就是16。

10.sizeof(a[3]):最后一个比较特殊,在这里我们看到a[3]首先第一反应是数组越界,此时应该报错才对,其实不然,比如表达式1+2,他其实有两个属性:

因为是三行四列的二维数组,每一行都有四个元素, 那我们此时的a[3]==int[4],sizeof根据类型就可以直接算出他的大小为16,并不会真正访问他。

我们再来看以下这段代码:

 此时1 处的结果为几呢?答案为2,因为sizeof内部的表达式是不会进行计算的,不管a+6这两个整形最终算出的结果有多大,放到short类型的s内部,都要发生截断,sizeof计算的大小当然为2。

2处的结尾为5,因为sizeof内部的表达式压根没计算,自然也不会改变s的值。

实例2:

按照上一题的思路,下面这道题就要简单的多

int main()
{
	int a[] = { 1,2,3,4 };
	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));

	return 0;
}

这道题相对简单,我们用注释的方式来进行解释 :

 

 实例3:

int main()
{
	char arr[] = { 'a','b','c','d','e','f' };

	printf("%d\n", sizeof(arr));
	printf("%d\n", sizeof(arr + 0));
	printf("%d\n", sizeof(*arr));
	printf("%d\n", sizeof(arr[1]));
	printf("%d\n", sizeof(&arr));
	printf("%d\n", sizeof(&arr + 1));
	printf("%d\n", sizeof(&arr[0] + 1));

	printf("%d\n", strlen(arr));
	printf("%d\n", strlen(arr+0));
	printf("%d\n", strlen(*arr));
	printf("%d\n", strlen(arr[1]));
	printf("%d\n", strlen(&arr));
	printf("%d\n", strlen(&arr + 1));
	printf("%d\n", strlen(&arr[0] + 1));
	
	return 0;
}

 sizeof这块比较简单,我们重点说明strlen,我们知道strlen是计算字符串长度,遇到 '\0' 才会停止。做题时可以参照下图:

 1.strlen(arr):随机值,这儿是从a的位置往后找 '\0' 我们不知道在内存中哪里会出现 \0,所以为随机值。

2.strlen(arr+0):随机值,arr+0找到的还是首元素a的地址,往后走再去寻找 \0 。

3.strlen(*arr):报错,这里比较特殊,strlen在接受时应当接收一个char*的地址才对,在这里*arr相当于把字符a的ascii值97传给了strlen,把97当成一个地址让strlen去找谁知道找哪去了,这里自然就报错了。

4.strlen(arr[1]):报错,与*arr同理,这儿是把字符b的ascii值传给strlen。

5.strlen(&arr):随机值,&arr是整个数组的地址,类型应该为char(*)[6],strlen只会接受char*的地址,所以这里其实发生了强制类型转换,&arr转换为数组首元素的地址。

6.strlen(&arr + 1):随机值,&arr+1跳过整个数组,从数组结束位置1开始找,我们仍然不知道他在哪里会遇到 \0 。

7.strlen(&arr[0] + 1):随机值,与1 2一样,&arr[0]+1从字符b的位置往后找。

实例4:

int main()
{
	char arr[] = "abcdef";
	printf("%d\n", sizeof(arr));
	printf("%d\n", sizeof(arr + 0));
	printf("%d\n", sizeof(*arr));
	printf("%d\n", sizeof(arr[1]));
	printf("%d\n", sizeof(&arr));
	printf("%d\n", sizeof(&arr + 1));
	printf("%d\n", sizeof(&arr[0] + 1));

	printf("%d\n", strlen(arr));
	printf("%d\n", strlen(arr+0));
	printf("%d\n", strlen(*arr));
	printf("%d\n", strlen(arr[1]));
	printf("%d\n", strlen(&arr));
	printf("%d\n", strlen(&arr + 1));
	printf("%d\n", strlen(&arr[0] + 1));

	return 0;
}

如图所示: 

 结合以上图像,我们还是以注释的方式进行解释:

 

 实例5:

int main()
{
	char* p = "abcdef";

	printf("%d\n", sizeof(p));
	printf("%d\n", sizeof(p+1));
	printf("%d\n", sizeof(*p));
	printf("%d\n", sizeof(p[0]));
	printf("%d\n", sizeof(&p));
	printf("%d\n", sizeof(&p + 1));
	printf("%d\n", sizeof(p[0]) + 1);

	printf("%d\n", strlen(p));
	printf("%d\n", strlen(p+1));
	printf("%d\n", strlen(*p));
	printf("%d\n", strlen(p[0]));
	printf("%d\n", strlen(&p));
	printf("%d\n", strlen(&p + 1));
	printf("%d\n", strlen(&p[0] + 1));

	return 0;
}

注意:char*p="abcdef",这种方式带来的内存布局一定是这个样子

 1.sizeof(p):4\8 , 计算p这个地址的大小。 

2.sizeof(p+1):4\8 ,计算p+1处地址的大小。

3.sizeof(*p):1 , 对p地址进行解引用,计算第一个元素的大小。

4.sizeof(p[0]):1,也是计算第一个元素的大小。

5,sizeof(&p):4\8,计算p这个地址处的地址,sizeof(&p+1)与sizeof(p[0]+1)同理。

6.strlen(p):6, p内放的a的地址,strlen进行计算就是从a处往后找 \0。

7.strlen(p+1):5,p+1存放b的地址,strlen从b的位置往后找 \0。

8.strlen(*p):报错,*p相当与把a的ascii值传给strlen,p[0]与此同理。

9.strlen(&p):随机值,取出p处的地址,在p内部我们根本不知道存放了什么。

10.strlen(&p + 1):随机值,同理。

11.strlen(&p[0] + 1):5,&p[0]+1找到b的地址,从b的位置往后找\0。

           好,这就是我们的前5道例题,只是小试牛刀,并不算特别复杂,接下来的这些题目才是本章真正的重头戏,废话不多说,我们一道一道的刚!!

实例6:

分析这段代码输出的结果:

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

我们还是用画图的方式整理一下思路

 首先我们创建了一个5个元素的数组,&a取到了整个数组的地址,&a+1指向数组的结尾位置,此时他的类型应该是int(*)[4],把他强制类型转换为(int*)存放在ptr里,此时ptr应当也指向数组的结尾位置*(ptr-1)找到了5这个元素,而*(a+1)表示首元素的地址也就1这个元素的地址+1。所以答案就是2,5。

这里可能会有人疑问,为什么要进行强制类型转换?首先&a+1是一个数组指针类型也就是int(*)[4],

强制类型转换为(int*)才能很好的赋给ptr。

再次注意:*(a+1)并没有出现&a和sizeof,他表示的就是首元素的地址,千万别搞混。

实例7:

这道题告知结构体大小为20字节,假设p的值为0x100000。

struct Test
{
	int num;
	char* pcname;
	short sDate;
	char cha[20];
	short sBa[4];
}*p;
int main()
{
	printf("%p\n", p + 0x1);
	printf("%p\n",(unsigned long) p + 0x1);
	printf("%p\n", (unsigned int*)p + 0x1);
	return 0;
}

这道题考察的是指针类型决定指针了的运算

p+0x1:p是结构体的地址+1跳过整个结构20个字节,答案为0x100014,14就是16进制中的20;

(unsigned long) p + 0x1:把p强制类型转换为无符号的整形,此时的p他就是一个整形,整形+1其实就是加了个1嘛,答案:0x100001;

(unsigned int*)p + 0x1:强制类型转化为int*的指针,ing*的指针+1其实就是跳过一个int,指针+1取决于他的类型,char类型就跳过1,结构体类型就跳过一个结构体 。答案0x100004;

实例8:

这道题我们假设他是以小端的方式进行存储

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

 

 

1.首先我们创建了一个四个元素的一维数组,&a+1指向的就是数组结尾处的地址,把一个int(*)[4]类型的地址强制类型转换为(int*)存放在pyr1中,此时的ptr1指向的也是数组结尾处,ptr[-1],其实就是ptr+(-1),指向4这个元素的地址,以%x的打印,只打印有效位的话那就只打印一个4。

2.a是数组首元素的地址,强制类型转换为(int),整形+1就是只加了一个1,跳过一个字节,在这里他不会跳过4个字节!指向了绿色箭头的位置,再把他强制类型转换为(int*),他就是以绿色箭头的位置向后找4个字节,存放在ptr2中,*ptr2一%x的形式打印,答案为2 00 00 00。

实例9:

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

 我在第一次做这道题是也掉入了这个坑里,首先他是个逗号表达式!!结果自然就是1,3,5,存放在二维数组中的形式如图所示,a[0]取出的是第一行的地址,由于没有“&数组名”也没有放在sizeof内部,所以此时的a[0]就是首元素的地址放到了p这个指针变量中,答案为   1。

实例10:

下面这道题就有难度了

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[4][2]好理解,这其中最重要的便是p中存放的地址,a是数组名,取出的是a[0]这个以为数组的地址放入p中,但是p的类似是int (*p)[4],是指向四个元素的数组指针,当把a存放到p中时只能存放4个元素,如图绿色箭头所示,p[4]其实就是p的地址+4,p[4][2]就是图中的绿色区域。

&p[4][2] - &a[4][2]:指针减指针得到的是两指针之间的元素个数, 由于是低地址减去高地址,值应该为-4;

%p的方式打印,以地址的方式打印没有正负之分,所以他内存中的补码会直接解析成源码打印,以16进制的方式打印出来的结果为:FFFFFFFC

-4的原码:10000000000000000000000000000100

       反码:111111111111111111111111111111111011

       补码:111111111111111111111111111111111100

实例11:

这道题和上面的一些题类似

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

 &aa取出整个数组的地址,&aa+1指向的是数组末尾处的地址,强制类型转换为(int*)赋给ptr1,

*(ptr1-1)找到的就是元素9;aa没有取地址,也没有单独存放在sizeof内部,此时的aa就是数组首元素的地址,也就二维数组第一行这个一维数组的地址,aa+1跳过整个一维数组指向6这个的地址,

*(ptr2-1)答案就是 5。

实例12:

int main()
{
	char* a[] = { "work","at","alibaba" };
	char** pa = a;
	pa++;
	printf("%s ", *pa);
	return 0;
}

 a数组三个元素每个元素都是char*类型,a数组名相当于首元素的地址,首元素是一个char*类型,他的地址理所应当用一个二级指针char**来接受,所以pa存放的就是首元素的地址,pa++指向的就是第二个元素的地址,对他进行解引用找到的就是a这个元素,由于是%s进行打印,他就从a这往后找到\0停止,打印的结果就是:at

实例13(难度就有点大了):

这道题就比较难了,但我们也别害怕,一步一步分析

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数组中存放了四个chan*类型的字符串

CP数组中依次存放了这4个字符串的地址,char*类型的地址理当拿char**的指针变量接受

CPP中存放了cp首元素的地址,首元素为char**类型,所以要用char***的三级指针来接受

1. **++cpp:cpp 指向c+3,++cpp先 ++后使用,指向c+2这个地址,*(c+2)就找到了c+2这个元素

*(*(c+2))再对他解引用就找到了c数组中char*蓝色框框的内容,以%s的形式打印,答案为:POINT

2.*-- * ++cpp + 3:cpp现在指向c+2,++cpp就指向了c+1,*++cpp就找到了cp数组中c+1这个元素,c+1指向c数组中紫色框框的元素   *--就指向了第一个元素,+3就指向了E的位置,以%s的形式打印,答案:ER

3.*cpp[-2] + 3:cpp[-2]可以理解为**(cpp-2)+3,cpp指向c+1的位置,cpp-2指向c+3的位置,c+3指向c数组中最后一个元素,进行解引用+3就找到了s的位置,打印的结果为:ST

4.cpp[-1][-1] + 1:cpp[-1][-1]可以理解为  *(*(cpp-1)-1)+1,cpp-1指向c+2的位置,*(cpp)-1指向的接受c数组中的第二个元素,再解引用+1就找到了E的位置,打印的结果:EW

指针这块的练习题到这就结束了,感谢您的阅读,我们一起努力,一定会拿下c,坚持住兄弟们

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浪花猪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值