目录
接上一节数组和指针笔试题解析
字符串数组
二维数组
指针运算笔试题解析
题⽬1
题⽬2
题⽬3
顺便复习一下逗号表达式:
题⽬4
题⽬5
题⽬6
题⽬7
总结
再再再三强调一下:
数组名是首元素的地址,但是有两个例外:
1.sizeof(数组名)计算的是整个数组的大小
注意:只有当sizeof()括号内放的是单独一个数组名才是表示整个数组,如果不是单独的数组名则表示首元素的地址,
比如sizeof(arr + 0)),那这里的arr表示的是首元素地址
2.&(数组名)取出来的是整个数组的大小
int main()
{
//字符串数组---用字符串初始化
char arr[] = "abcdef";//这个数组是7个元素,原型是"abcdef\0",第七个是\0
printf("%zd\n", sizeof(arr));//7
printf("%zd\n", sizeof(arr + 0));//4或者8,arr+0是首元素的地址,地址的大小是4或者8
printf("%zd\n", sizeof(*arr));//1,计算的是数组首元素的大小
printf("%zd\n", sizeof(arr[1]));//1,计算的是第二个元素的大小
printf("%zd\n", sizeof(&arr));//4或者8,取出来的是数组的地址,是地址,大小就是4或者8
printf("%zd\n", sizeof(&arr + 1));// 4或者8,跳过整个数组,指向了数组的后边的某个空间的地址
printf("%zd\n", sizeof(&arr[0] + 1));//4或者8,计算的是第二元素的地址,是地址,大小就是4或者8
return 0;
}
换成strlen计算又会如何呢?
#include//strlen()的头文件
//strlen()括号内要求被传的是个地址!!!!!否则程序崩溃
int main()
{
char arr[] = "abcdef";//
printf("%zd\n", strlen(arr));//6
//arr在这里表示的是首元素的地址,那就从首元素开始计算,直到遇到\0终止,\0不计入内
printf("%zd\n", strlen(arr + 0));//6,arr首元素的地址,+0等于没加,还是首元素的地址
//printf("%zd\n", strlen(*arr));//?err
//arr表示首元素的地址,解引用后找到首元素'a','a'的ASCII码值是97,streln将97当成地址去寻找对应的元素,程序崩溃
//printf("%zd\n", strlen(arr[1]));//?err,传的是第二个元素,而不是地址,程序崩溃
printf("%zd\n", strlen(&arr));//6
//注:&(数组名)取出来的是整个数组的大小,&arr虽然是数组的地址,但是也是指向数组的起始位置,所以也从首元素开始计算
//上一节我们讲过strlen的原型是size_t strlen(const char* str),
//参数类型是一个char*,即一个存放了地址的指针,而&arr直接是个数组的地址,
//所以传给strlen时和strlen要求的参数类型有所差异,所以编辑器会报个小小的警告
//正确做法是将&arr取到的地址存放到一个指针里面再传过去,比如: char (*parr)[7]=&arr;而parr的类型是 char(*)[7]
printf("%zd\n", strlen(&arr + 1));//随机值
//数组的地址+1就跳过整个数组"abcdef\0",从\0后面开始计算,但是不知道后面是什么,得到的是一个随机值
printf("%zd\n", strlen(&arr[0] + 1));//5,第二个元素的地址,从第二个元素开始数,结果是5
return 0;
}
再看看这串代码
int main()
{
char* p = "abcdef";
//指针p指向字符串"abcdef\0",p存放了字符串的起始位置即首字符'a'的地址,p的类型是char*
printf("%zd\n", sizeof(p));//4或者8,,计算的是p这个指针变量的大小,而指针的大小总是4或者8
printf("%zd\n", sizeof(p + 1));//4或者8,p + 1是第二个字符的地址
printf("%zd\n", sizeof(*p));//1,计算的是'a'的大小
printf("%zd\n", sizeof(p[0]));//1,p[0]==*(p+0)计算的是'a'的大小
printf("%zd\n", sizeof(&p));//4或者8,&p取出指针变量p的地址,是地址,大小就是4或者8
printf("%zd\n", sizeof(&p + 1));//4或者8
//此处需要复习一下二级指针:
//先看一级指针
//int a = 10;
//int* p = &a;
//p + 1;//p跳过一个int的大小
//再看二级指针
//char* p;
//char** q = &p;
//靠近q的*告诉我们q是个指针(即q里面存放了地址),而char*告诉我们q指向的对象是个char*型的指针
//而q+1跳过一个char*的大小,即跳过一个char*型指针变量的大小,指向p指针后面的空间的某个地址,大小是4或者8
//如果还是看不懂请看下图1
printf("%zd\n", sizeof(&p[0] + 1));//4或者8,是字符'b'的地址
return 0;
}
【图1】
一个小小的感悟:指针变量名和数组名一般存放的都是首元素的地址
//指针变量名[ ]==数组名[ ]
int main()
{
char* p = "abcdef";//原型"abcdef\0"
printf("%d\n", strlen(p));//6,p存放的是字符串的首字符的地址,从首字符开始向后数
printf("%d\n", strlen(p + 1));//5,从'b'开始数
printf("%d\n", strlen(*p));//传的不是地址,程序崩溃
printf("%d\n", strlen(p[0])); //传的不是地址,程序崩溃
printf("%d\n", strlen(&p));//随机值
//取出来的是指针p的地址,从p的地址开始数,后面不知道是什么,
//不知道什么时候遇到\0,以结果是随机值,可以看下图2想象一下,
//注意:p的地址和字符串的地址是两个不同的空间。
printf("%d\n", strlen(&p + 1));//随机值
//跳过p的地址向后数,后面是什么就更不知道了,可以看下图3想象一下,
printf("%d\n", strlen(&p[0] + 1));//5,从'b'开始向后数
return 0;
}
【图2】
【图3】
模仿strlen()函数计算字符数组里面的字符个数
size_t my_strlen(const char*str)
{
int count = 0;
while (*str)//只要不是\0就继续循环,遇到\0就跳出循环
//'\0'是空字符,对应的ASCII码值是0,while(0),0表示假,因此跳出循环
{
count++;
str++;
}
return count;
}
int main()
{
char str[7] = "abcdef";
size_t count=my_strlen(str);
printf("%zd", count);
return 0;
}
int main()
{
//注意:二维数组也是数组,之前对数组名理解也是适合的
int a[3][4] = { 0 };
printf("%zd\n", sizeof(a));//12*=48,a是二维数组的数组名,计算的是整个数组
printf("%zd\n", sizeof(a[0][0]));//4,第一行第一列的元素
printf("%zd\n", sizeof(a[0]));//16,计算的是整个第一行的大小
//a[0]是第一行一维数组的数组名,数组名单独放在sizeof的括号内,表示的是计算整个第一行的一维数组的大小
printf("%zd\n", sizeof(a[0] + 1));//4或者8
//注意:这个a[0]不是单独放在sizeof的括号内,因此它在这里不表示整个第一行的一维数组
//在这里a[0]表示的是第一行的一维数组首元素的地址,+1表示第一行的第二个元素,即a[0][1]的地址
//是地址,大小就是4或者8
printf("%zd\n", sizeof(*(a[0] + 1)));//4,a[0][1]
printf("%zd\n", sizeof(a + 1));//4或者8
//在这里a不单独放在括号内,表示的是首元素的地址
//注意:对于二维数组来说,首元素就是第一行,即第一行一维数组的地址,+1即a[1]第二行一维数组的地址
printf("%zd\n", sizeof(*(a + 1)));//16
//*(a + 1))==a[0],第二行的数组名单独放在sizeof内部,计算的是第二行的大小
printf("%zd\n", sizeof(&a[0] + 1));//4或者8,&a[0]取的是整个第一行的地址,+1就取到了第二行的地址
printf("%zd\n", sizeof(*(&a[0] + 1)));//16,第二行的一维数组的大小
//int(*p)[4]==&a[0]+1;
//对这样一个数组指针解引用拿到的是这个数组,数组里面有4个int元素
printf("%zd\n", sizeof(*a));//16,这里a是第一行的地址,*a第一行的数组
//*a==*(a+0)==a[0]
printf("%zd\n", sizeof(a[3]));//16
//没有第四行,但是这个不存在越界,因为sizeof内部的表达式不会真实计算的
//sizeof只要知道类型就能计算大小
//如果这个数组有第四行的话,那a[3]其实跟a[0],a[1],a[2]一个道理,也就是说a[3]存在的话,类型也是int
//计算的是第四行的大小,但是它不会真的去访问第四行,只是根据类型来确定
return 0;
}
strlen只计算字符,不计算整型!
以上最重要的是理解数组名表示的意义
数组名的意义:
1. sizeof(数组名),这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩。
2. & 数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址。
3. 除此之外所有的数组名都表⽰⾸元素的地址。
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*)(&a + 1);//&a取得是整个数组的地址
//(int*)强制转换成类型指针
printf("%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
程序的结果是什么?
打印结果为2,5(图解见图4)
【图4】
在X86环境下
假设结构体的⼤⼩是20个字节
程序输出的结果是啥?
struct Test//结构体标签名
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p = (struct Test*)0x100000;//p是结构体变量名,后面是初始化内容
如果没有 (struct Test*)时,0x100000是一个整型,强制类型转换成(struct Test*)才能赋值给一个指针变量
因为p的类型就是一个struct Test*
补充一点:以上结构体的类型是以下这一整坨,包括最后那个*号
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*
int main()
{
printf("%p\n", p + 0x1);//0x1 十六进制的1就是1,
//p是结构体指针,结构体指针+1就跳过一个结构体大小
//p+0x1则表示0x100000+20
//16进制逢16进1,20进1还剩下4,则0x100000+20==0x100014
printf("%p\n", (unsigned long)p + 0x1);
//将p强制类型转换成整数(注意:这里转换成的不是指针!),整数+1就是加1,
//0x100000+1=0x100001
printf("%p\n", (unsigned int*)p + 0x1);
//将p强制类型转换成整型指针,整型指针+1就跳过1个int类型大小
//0x100000+4=0x100004
//注意:因为上面是用%p打印的,地址打印时,在X86环境下是打印8位的
printf("%x\n", p + 0x1);//如果改成用%x打印的话,打印的结果将省略前面的2个零
printf("%#x\n", p + 0x1);//如果想要打印0x的话,可以写成%#x
return 0;
}
补充:每个十六进制数为一个半字节,两个十六进制数合在一起为一个字节。
1个字节是8位,二进制8位:xxxxxxxx 范围从00000000-11111111,表示0到255。
一位16进制数(用二进制表示是xxxx)最多只表示到15(即对应16进制的F),
要表示到255, 就还需要第二位。
所以1个字节=2个16进制字符,一个16进制位=0.5个字节。
int main()
{
//注意逗号表达式
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
//原本如果想要数组的第一行是0,1的话,应该是用大括号的:
//int a[3][2] = { {0, 1}, {2, 3}, {4, 5} };
//则这个数组将会是这样的:
//第一行:0 1
//第二行:2 3
//第三行:4 5
//但是这道题用的是小括号,里面则是一个逗号表达式
//所以这个数组只有三个真正初始化的元素,其他的默认为0,应该是这样的:
//第一行:1 3
//第二行:5 0
//第三行:0 0
//不知道为什么的请往下翻看一下逗号表达式的复习!
int* p;
p = a[0];//a[0]表示第一行的一维数组的数组名,放的是这个一维数组的首元素的地址
printf("%d", p[0]);//结果为1
return 0;
}
逗号表达式的要领:
(1) 逗号表达式的运算过程为:从左往右逐个计算表达式。
(2) 逗号表达式作为一个整体,它的值为最后一个表达式(也即表达式n)的值。
(3) 逗号运算符的优先级别在所有运算符中最低。
举个例子:
int main()
{
int a1, a2, b = 2, c = 7, d = 5; // 第1行
a1 = (++b, c--, d + 3); // 第2行,用()括起来表示先运行()里面的表达式,a1=2+1=3=b,a1=7 c=6, a1=5+3=8
a2 = ++b, c--, d + 3; //第3行,没有括号,则赋值运算符优先于逗号运算符,a2=b=3+1=4,c=5,5+3=8
//对于给a1赋值的代码,有三个表达式,用逗号分开,所以最终的值应该是最后一个表达式的值,也就是(d + 3)的值,为8,所以a1的值为8。
//对于给a2赋值的代码,也是有三个表达式,这时的三个表达式为a2 = ++b,c--,d + 3;
//(这是因为赋值运算符比逗号运算符优先级高)虽然最终表达式的值虽然也为8,
//但b = 4(第2行代码运算完时,b = 3,即第3行代码运行时,b的值为4),所以a2 = 4。
printf("%d %d", a1, a2);
return 0;
}
假设环境是x86环境,程序输出的结果是啥?
int main()
{
int a[5][5];
int(*p)[4];
//p是个数组指针,指向的数组有4个int类型的元素
p = a;//int(*ptr)[5]=a
//这里将a赋给p时肯定有类型的差异,但是没关系
//将数组a第一行的地址赋给p,则p指向了数组a第一行的首元素的地址
//而p指向的数组是只有4个int类型的元素
//p[4][2]就是p+1跳过4个整型(可跨行)
//这里不明白的话请结合以下图5理解一下
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
//结果为fffffffc -4
//地址-地址=地址与地址之间的元素个数
//或者说指针-指针的绝对值得到的是指针和指针之间的元素个数
//又因为低地址-高地址,得到的是负数(随着数组下标的增长,地址是由低到高变化的)
//因此&p[4][2] - &a[4][2]结果等于-4
//%d打印出来的是原码的值:-4
//原码:10000000000000000000000000000100
//反码:11111111111111111111111111111011
//补码:11111111111111111111111111111100 ==反码+1
//用%p打印时把结果-4当成了地址,因为%p打印地址看的是内存中的值,内存里面的是什么就打印什么
//在内存中存的是补码,进去内存中看的时候是显示成16进制给我们看的
//补码:1111 1111 1111 1111 1111 1111 1111 1100
//将以上补码翻译成16进制:fffffffc
//因此用%p打印时,显示出来的是内存中的补码的16进制形式fffffffc
return 0;
}
【图5】
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* ptr1 = (int*)(&aa + 1);
//&aa就是第二行的地址,+1跳过第二行的地址指向外面某一块内存的地址
int* ptr2 = (int*)(*(aa + 1));
//aa是二维数组首元素的地址(即第一行的地址)
//*(aa + 1)==aa[1]第二行的数组名,相当于第二行的一维数组的首元素的地址,即6的地址
//(int*)其实可以删掉,加上是为了迷惑做题者
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));//10 5
//*(ptr1 - 1)指向10的地址
//*(ptr2 - 1)指向5的地址
return 0;
}
int main()
{
char* a[] = { "work","at","alibaba" };
//a是指针数组,数组里面的元素是指针(存放了地址的元素),每个元素是char*类型的
//将每一个字符串的起始地址都存放在数组a的每一个元素当中,而每一个元素都是指针
//第一个指针指向字符串"work\0"
//第二个指针指向字符串"at\0"
//第三个指针指向字符串"alibaba\0"
char** pa = a;
//将数组a中首元素的地址交给了pa
//二级指针pa指向a数组中的第一个指针的地址
pa++;
//pa+1跳过一个char*的大小指向数组a中的第二个指针的地址
printf("%s\n", *pa);
//pa解引用后找到第二个指针,而第二个指针里存放的是字符串"at\0"的地址
//再通过字符串"at"的地址找到字符串"at\0"
//不明白的话结合下图6理解
return 0;
}
【图6】
int main()
{
char* c[] = { "ENTER","NEW","POINT","FIRST" };
//指针数组,c放的是每一个字符串首字符的地址
char** cp[] = { c + 3,c + 2,c + 1,c };
//c数组名表示首元素的地址,即c数组中存放了"ENTER"首字符地址的指针的地址
//c+3 表示跳过3个char*的大小,指向a数组中的第4个指针
//c + 2指向a数组中的第3个指针
//c + 1指向a数组中的第2个指针
//c指向a数组中的第1个指针
//cp每一个元素是char**
char*** cpp = cp;
//cpp存放了cp首元素的地址,类型是char***
//以上不明白的,请结合以下图7理解
printf("%s\n", **++cpp);//"POINT"
//结合图8理解
printf("%s\n", *-- * ++cpp + 3);//ER
//在这里+3的优先级是最低的
//结合图9理解
printf("%s\n", *cpp[-2] + 3);//ST
//cpp[-2]==*(cpp-2)即cpp的地址后退2个char***
//结合图10理解
printf("%s\n", cpp[-1][-1] + 1);//EW
//cpp[-1][-1]==*(*(cpp-1)-1)
//cpp[-1]==*(cpp-1)即cpp的地址后退1个char***
//*(cpp - 1)==c+2
//*(cpp-1)[-1]==*(c+2-1)==*(c+1)
//结合图11理解
return 0;
}
【图7】
【图8】
【图9】
【图10】
【图11】
自我感悟:
一级指针和二级指针只需解引用*一次就找到最终值
三级指针需解引用**两次才找到最终值
需要强调一下:
我们打印字符串数组的时候传给printf()的其实是首字符的地址
int main()
{
char(*arr)[] = "abcdef";//数组指针,指向了数组的指针,数组的每一个元素是char型
printf("%s\n", arr);//传a的地址过去,然后从a开始往后数全部打印出整个字符串
return 0;
}
关于指针的学习一定要学会画图!,画一个正确的图能够事半功倍!
至此,指针部分的重要内容已经全部学习完了,这一篇博客是指针的完结篇,希望对于指针问题有些困惑的伙伴们有所帮助。
由于临近期末,博客需要断更两三个星期,寒假继续努力学习,共勉!