本章主要讲解13道关于指针的经典练习。
数组名的意义
首先回顾一下关于数组名的相关知识:
1.sizeof(数组名):这里数组名表示整个数组;
2.&数组名:这里数组名表示整个数组;
3.其他情况下,数组名都表示数组首元素地址
strlen库函数:求字符串长度
sizeof操作符:求变量或者类型创建的变量所占空间的大小(单位是字节)
练习
题1
//代码1
int main()
{
//一维数组
int a[] = { 1,2,3,4 };
//sizeof(a) - 表示整个数组
printf("%d\n", sizeof(a));//16
//a+0表示首元素的地址
printf("%d\n", sizeof(a + 0));// 4/8
//*a表示第一个元素
printf("%d\n", sizeof(*a));//4
//a+1表示第二个元素的地址
printf("%d\n", sizeof(a + 1));// 4/8
//a[1]是第二个元素
printf("%d\n", sizeof(a[1]));//4
//&a表示整个数组的地址
printf("%d\n", sizeof(&a));// 4/8
//&a表示数组的地址,对数组地址解引用得到数组
printf("%d\n", sizeof(*&a));//16
//&a+1表示跨过整个数组后的地址
printf("%d\n", sizeof(&a + 1));// 4/8
//&a[0]表示首元素的地址
printf("%d\n", sizeof(&a[0]));// 4/8
//&a[0]+1表示第二个元素的地址
printf("%d\n", sizeof(&a[0] + 1));// 4/8
return 0;
}
解析:
(1) sizeof(a):数组名放在sizeof中,表示整个数组,计算整个数组的大小,16个字节;
(2) sizeof(a + 0):这里数组名不是直接放在sizeof中,所以这里数组名表示数组首元素的地址,a+0仍然表示首元素的地址,所以sizeof(a+0)是计算地址的大小,在32位平台是4字节,在64位平台是8字节;
(3) sizeof(*a):这里a表示数组首元素地址,*a对首元素地址解引用得到首元素1,int类型,所以是4个字节;
(4) sizeof(a + 1):这里a表示首元素地址,a+1表示第二个元素的地址,指针是4个字节或者8个字节;
(5) sizeof(a[1]):a[1]表示第二个元素,也就是2,int类型,4个字节
(6) sizeof(&a):&a表示数组的地址,指针的大小4个字节或者8个字节;
(7) sizeof(*&a):&a得到数组的地址,对数组地址解引用得到数组,也就是数组名,*&a 等价于a,所以sizeof(数组名)是计算整个数组的大小,16个字节;
(8) sizeof(&a + 1):&a得到数组地址,&a+1跨过一个数组后的地址,地址大小4个字节或者8个字节;
(9) sizeof(&a[0]):a[0]表示首元素,&a[0]表示首元素的地址,地址大小是4个字节或者8个字节;
(10) sizeof(&a[0] + 1):&a[0]+1表示第二个元素的地址,地址大小是4个字节或者8个字节;
题2
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));//6
printf("%d\n", sizeof(arr + 0));//4/8
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));//越界访问,只是访问没有改值,但是也存在一定的问题,随机值
printf("%d\n", strlen(arr + 0));//随机值
//strlen就以为传进来的'a'的ascii码值97是地址
printf("%d\n", strlen(*arr));//报错 'a'-97,把97作为地址开始数字符串长度,野指针,非法访问内存
printf("%d\n", strlen(arr[1]));//'b'-98 报错
//strlen的参数是const char*
printf("%d\n", strlen(&arr));//&arr是数组的地址,类型不匹配,但是不影响
printf("%d\n", strlen(&arr + 1));//随机值
printf("%d\n", strlen(&arr[0] + 1));//随机值
return 0;
}
解析:
(1) sizeof(arr):数组名放在sizeof中表示整个数组,这里是求数组的大小,6个字节;
(2) sizeof(arr + 0):arr不是直接放在sizeof中,表示数组首元素的地址,a+0仍然表示首元素的地址,地址大小是4个字节或者8个字节;
(3) sizeof(*arr):*arr表示对数组首元素地址解引用,得到首元素,也就是字符’a’,1个字节;
(4) sizeof(arr[1]):arr[1]表示第二个元素,字符’b’,1个字节;
(5) sizeof(&arr):&arr表示数组的地址,地址是4个字节或者8个字节;
(6) sizeof(&arr + 1):&arr表示数组地址,&arr+1表示跨过整个数组的地址,4个字节或者8个字节;
(7) sizeof(&arr[0] + 1):&arr[0]表示首元素的地址,&arr[0]+1表示第二个元素的地址,4个字节或者8个字节;
(8) strlen(arr):数组名表示首元素的地址,这里strlen函数是求字符串长度,遇到’\0’停止,’\0’不算作字符串长度,字符数组arr中只存储了6个字符,并没有存储’\0’,所以什么时候会遇到’\0’并不清楚,会越界访问,只是访问,没有改变值,但是也存在一定的问题,结果是随机值;
(9) strlen(arr + 0):同(8),结果是随机值;
(10) strlen(*arr):strlen函数的形参是const char*类型的指针,此处,*arr得到的是首元素,字符’a’,对应的ASCII码是97,所以这里strlen函数会把97看做地址,然后访问该地址空间,由于指针指向不明,野指针,造成非法访问内存,会报错;
(11) strlen(arr[1]):解释同(10),将字符’b’的ASCII码值98看做地址,非法访问内存;
(12) strlen(&arr):&arr是数组的地址,类型为char (*)[6],strlen函数的形参是const char*类型,类型不匹配,会报警告,但是不影响,结果是随机值,但是结果同(8)、(9);
(13) strlen(&arr + 1):&arr表示数组地址,&arr+1跨过数组之后的地址,越界访问,但是没有改变值,结果是随机值,但是结果要比(12)小6;
(14) strlen(&arr[0] + 1):&arr[0] + 1表示第二个元素的地址,结果比(8)小1;
题3
int main()
{
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//7
printf("%d\n", sizeof(arr + 0));//4/8
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
printf("%d\n", strlen(arr + 0));//6
printf("%d\n", strlen(*arr));//'a'-97作为地址 野指针,非法访问内存
printf("%d\n", strlen(arr[1]));//'b'-98作为地址 野指针,非法访问内存
printf("%d\n", strlen(&arr));//6
printf("%d\n", strlen(&arr + 1));//随机值
printf("%d\n", strlen(&arr[0] + 1));//5
return 0;
}
解析:
(1)sizeof(arr):数组名直接放在sizeof中,表示整个数组,所以结果为7
(2)sizeof(arr + 0):这里数组名表示首元素地址,arr+0仍然表示首元素地址,地址是个指针,所以结果为4/8
(3)sizeof(*arr):这数组名表示首元素地址,对arr解引用,得到首元素,所以结果为1
(4)sizeof(arr[1]):arr[1] = *(arr+1),得到第二个元素,结果为1
(5)sizeof(&arr):这里arr表示整个元素,&arr表示数组的地址,是个指针,所以结果为4/8
(6)sizeof(&arr + 1):&arr+1跨过这个数组,所以结果为4/8
(7)sizeof(&arr[0] + 1):&arr[0]得到首元素地址,&arr[0]+1表示第二个元素的地址,结果为4/8
(8)strlen(arr):srr表示首元素地址,这里计算字符串的长度,从数组首元素地址开始算起,直到遇到第一个’\0’结束,’\0’不算作字符串长度,结果为6
(9)strlen(arr + 0):结果同上
(10)strlen(*arr):*arr得到数组首元素,这里把字符’a’对应的ASCII码值97作为首地址,向后查找第一个’\0’,因为地址为97的空间,指向不明,造成野指针,非法访问内存,程序崩溃
(11)strlen(arr[1]):arr[1]表示数组第二个元素,这里把第二个元素’b’对应的ASCII码值98作为地址,计算字符串长度,因为地址为98的那块空间指向不明,造成野指针,非法访问内存,程序崩溃
(12)strlen(&arr):&arr是数组的地址,计算字符串长度结果为6
(13)strlen(&arr + 1):&arr+1是个指针,跨过整个数组,结果为随机数,因为’\0’的位置不明确
(14)strlen(&arr[0] + 1):&arr[0]+1,结果为数组第二个元素的地址,计算字符串的长度,结果为5
题4
int main()
{
char* p = "abcdef";
printf("%d\n", sizeof(p));//4/8
printf("%d\n", sizeof(p + 1));//4/8
printf("%d\n", sizeof(*p));//1
printf("%d\n", sizeof(p[0]));//1
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
printf("%d\n", strlen(p + 1));//5
printf("%d\n", strlen(*p));//'a'-97作为地址,非法访问内存
printf("%d\n", strlen(p[0]));//'a'-97作为地址,非法访问内存
printf("%d\n", strlen(&p));//随机值
printf("%d\n", strlen(&p + 1));//随机值
printf("%d\n", strlen(&p[0] + 1));//5
return 0;
}
解析:
(1)sizeof§:p是指针,结果为4/8
(2)sizeof(p + 1):p+1仍为指针,结果为4/8
(3)sizeof(*p):*p得到字符’a’,结果为1
(4)sizeof(p[0]):p[0] = *(p+0),为字符’a’,结果为1
(5)sizeof(&p):&p得到指针p的地址,也就是二级指针,结果为4/8
(6)sizeof(&p + 1):&p得到指针p的地址,二级指针,类型为char**,结果为4/8,指针+1跨过的字节数等于指针指向的类型的大小
(7)sizeof(&p[0] + 1):&p[0] + 1 = &*(p+0)+1 = p+0+1,仍为一个指针,结果是4/8
(8)strlen§:计算字符串长度,结果为6
(9)strlen(p + 1):p+1指向字符’b’,结果为5
(10)strlen(*p):*p得到字符’a’,以字符’a’对应的ASCII码值97作为地址,计算字符串的长度,该指针指向不明,是野指针,造成非法访问内存,程序崩溃
(11)strlen(p[0]):结果同上
(12)strlen(&p):&p得到指针的指针,也就是二级指针,如下图所示
strlen函数的形参是const char*类型
以&p作为首地址,计算字符串长度,&p类型是char**,放入strlen函数中,会报警告,如下
但是仍会把它看做char*类型指针来计算,这里结果是随机数,因为p的内容不确定,并不知道第一个’\0’在哪里
(13)strlen(&p + 1)):&p是二级指针,如下图所示,结果为随机值,因为\0的位置不明确
(14)strlen(&p[0] + 1):&*(p+0)+1 = p+0+1,结果为5
题5
int main()
{
//二维数组
int a[3][4] = { 0 };
printf("%d\n", sizeof(a));//48
printf("%d\n", sizeof(a[0][0]));//4
//a[0] = *(a+0),a是首元素地址,a+0也是首元素地址,*(a+0)是首元素,也就是第一个一维数组,
//sizeof(数组名):整个数组的大小
printf("%d\n", sizeof(a[0]));//16
//a[0]是第一行数组的数组名 - 数组首元素的地址,
//a[0]+1是第一行第二个元素的地址
printf("%d\n", sizeof(a[0] + 1));//4/8
//第一行第二个元素
printf("%d\n", sizeof(*(a[0] + 1)));//4
//二维数组的数组名,表示首元素的地址,第一行的地址
//第二个一维数组的地址
//数组名a并没有单独放在sizeof内部,也没有取地址,所以a表示数组首元素的地址
printf("%d\n", sizeof(a + 1));//4/8
printf("%d\n", sizeof(*(a + 1)));//16
//a[0]是第一行的数组名
//&a[0]是是第一行的地址
//&a[0]+1得到第二行的地址
printf("%d\n", sizeof(&a[0] + 1));//4/8
//*(&a[0] + 1))得到第二行的数组名
printf("%d\n", sizeof(*(&a[0] + 1)));//16
//*a得到第一行的数组名
printf("%d\n", sizeof(*a));//16
//a[3]相当于第四行的数组名,但是sizeof内部不做运算,并不会真的越界访问
printf("%d\n", sizeof(a[3]));//16
//*&arr:得到arr,也就是数组名,至于arr到底是整个数组还是数组首元素地址,要看放在哪里
//对一维数组的地址解引用得到一维数组的数组名
return 0;
}
(1)sizeof(a):数组名单独放在sizeof内部,表示整个数组
(2)sizeof(a[0][0]):这里数组名代表首元素地址,a[0][0] = * (*(a+0)+0),a[0][0]=0,结果为4
(3)sizeof(a[0]):a[0]表示首元素,这里表示第一个一维数组的数组名,数组名单独放在sizeof内部,表示整个数组
(4)sizeof(a[0] + 1):a[0]表示首元素,这里表示第一个一维数组数组名,这里数组名表示首元素地址,a[0]+1表示第一个一维数组中,第二个元素的地址,结果为4/8
(5)sizeof(*(a[0] + 1)):a[0]表示一个一维数组的数组名,a[0]+1表示第一个一维数组的第二个元素的地址,*(a[0]+1)表示第一行第二个元素,结果为4
(6)sizeof(a + 1):a+1表示数组第二个元素的地址,结果为4/8
(7)sizeof(*(a + 1)):*(a+1)表示第二个一维数组,结果为16
(8)sizeof(&a[0] + 1):&a[0]表示首元素的地址,&a[0]+1表示第二个元素的地址,结果为4/8
(9)sizeof(*(&a[0] + 1)):&a[0]+1表示第二个元素的地址,*(&a[0]+1)表示第二个元素,这里表示第二个一维数组,结果为16
(10)sizeof(*a):*a表示第一个元素,也就是第一个一维数组的数组名,结果为16
(11)sizeof(a[3]):a[3]表示第4个元素,结果为16,虽然越界访问,但是没有改变内存中的数据,所以没有关系。
题6
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*)(&a + 1);
printf("%d,%d", *(a + 1), *(ptr - 1));//2,5
return 0;
}
题7
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
int main()
{
p = (struct Test*)0x100000;
//p是结构体类型指针,p+0x1属于指针+1 ,越过一个指针指向元素的类型,这里越过20个字节
printf("%p\n", p + 0x1);//0x100014
//p被强制类型转换为unsigned long类型,整型+1
printf("%p\n", (unsigned long)p + 0x1);//0x100001
//p被强制类型转换为unsigned int*类型指针,指针+1
printf("%p\n", (unsigned int*)p + 0x1);//0x100004
return 0;
}
将地址0x100000强制类型转换为struct Test*类型,那么指针p+1跨过的字节数为20,16进制表示是14,所以p+0x1 = 0x100014
(unsigned long)p + 0x1将p强制类型转换成unsigned long类型,所以p是一个无符号整数,整数+1就是单纯加1,结果为0x100001
(unsigned int*)p + 0x1把p强制类型转换为无符号int型,p+1跨过4个字节,所以结果为0x100004
注意:%p是打印地址,地址是无符号数。
题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.大小端问题,对于一个数据(大于一个字节),存储方式有两种,大端存储、小端存储;
2.对于数组,随着数组元素下标的递增,地址是变大的。
计算机一般是小端存储,所以如图所示,数组每个元素按小端字节序存储,(int)a中a是数组首元素的地址,将a强制类型转换为int类型,整数+1,然后再强制类型转换为int*指针,所以ptr2的指向如图所示。从内存中取数据时,也按照小端存储方式取数据,将低地址的数据放在低字节,将高地址的数据放在高字节。
题9
#include <stdio.h>
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p;
p = a[0];
printf( "%d", p[0]);
return 0;
}
a[0] = *(a+0):a是数组首元素地址,a的每个元素都是一个一维数组,所以a也就是第一个一维数组的地址,*(a+0)得到第一个一维数组,也就是第一个一维数组的数组名(除了两种情况外,数组名都表示数组首元素的地址),所以指针p表示第一个一维数组的首元素的地址,那么p[0]就得到第一个一维数组的首元素,结果是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是首元素的地址,也就是第一个一维数组的地址,类型为int(*)[5],将该地址赋值给p,但是p的类型为int(*)[4](会报警告);&p[4][2] = &*( *(p+4)+ 2) = (p+4)+2
因为p是int(*)[4]类型指针,p+1跨过一个有4个int元素的数组,所以(p+4)得到如下图中蓝色框中的数组(数组名),数组名是首元素的地址,所以*(p+4)+2得到的是该数组第3个元素的地址,同理&a[4][2]位置如下图所示,(指针-指针)的绝对值是指针之间元素的个数,有4个元素。
&p[4][2] < &a[4][2],&p[4][2] - &a[4][2] = -4
原码:
//-4
//原码
10000000 00000000 00000000 00000100
//反码
11111111 11111111 11111111 11111011
//补码
11111111 11111111 11111111 11111100
%p:打印地址,地址是无符号数,不可能小于0,所以这里把-4在内存中的补码看成无符号数字取出来打印,也就是FF FF FF FC
%d:以有符号形式、10进制形式打印数字,也就是-4
题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跨过整个数组,
aa是首元素地址,aa+1是第二个一维数组的地址,*(aa+1)得到第二个一维数组,也就是数组名–首元素地址,将该地址强制类型转换为int*类型,如下图所示
*(ptr1 - 1) = 10
*(ptr2 - 1) = 5
题12
#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
数组名a是首元素的地址,所以a是char**类型的指针,pa的类型也是char**,指向的数据类型为char*,将a的值赋值给pa,pa++(指针+1,跨过的字节数为它所指向元素的类型大小的字节),跨过4个字节(因为char*占4个字节),所以此时pa指向数组a的第二个元素,*pa 得到数组a的第二个元素,是一个指针,指向常量字符串"at"的首字符’a’,所以%s形式打印字符串,结果为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;
}
我们来逐一分析:
(1) **++cpp:
指针cpp+1,跨过的字节数为cpp指向的元素的大小,也就是4个字节(指针+1跨过的字节数由指针类型决定),此时cpp的指向如下图所示:
**cpp = *(c+2) 是字符’P’的地址,所以以%s形式打印得到的是"POINT"
(2) –++cpp+3:
前置++的优先级大于+
++cpp后cpp的指向如下图所示
*的优先级大于+,++cpp得到c+1,此时表达式为–(c+1)+3,前置–的优先级大于+,所以表达式变为*c+3,得到字符’E’的地址,以%s的形式打印结果为“ER”
(3) *cpp[-2]+3
此时cpp的指向仍为上图所示,*cpp[-2]+3 = *( *(cpp - 2)) + 3
cpp - 2 的指向如下图所示,*的优先级比+高,* ( *(cpp-2))得到的是*(c+3),也就是字符’S’的地址,结果为“ST”
(4)cpp[-1][-1]+1
此时cpp的指向仍为上图所示,cpp[-1][-1] +1 = *(*(cpp -1) - 1) + 1 = *((c + 2) - 1)+1 =\ *(c+1)+1,是字符’E’的地址,所以结果为“EW”