C语言:指针典型例题详解
基于对前面的深入理解指针1,2,3,4的学习,本篇文章带来指针的典型例题。俗话说:光说不练假把戏。在指针的学习过程中最重要的还是要学会动手实操。学习本节内容之前,先复习一下涉及的相关知识,以便更好的理解。
基于对前面的深入理解指针1,2,3,4的学习,本篇文章带来指针的典型例题。俗话说:光说不练假把戏。在指针的学习过程中最重要的还是要学会动手实操。
学习本节内容之前,先复习一下涉及的相关知识,以便更好的理解。
一,重点知识回顾
除了博主指针专题的博文---《深入理解指针1,2,3,4》之外,还需要复习sizeof(),strlen
1. sizeof和strlen的对比
sizeof关注的是内存中的内容
sizeof | strlen(关注内存中的内容) |
1. sizeof是操作符,而非函数 | strlen是库函数,使用时需要包含头文件<string.h> |
2. sizeof计算操作数所占内存的大小,单位是字节 | strlen求字符串长度,统计字符串种' \0'之前的长度, |
不关注内存中存放什么数据 | 关注内存种是否有'\0',如果没有,就会持续向后找,可能会越界 |
2. 数组名的理解
数组名是数组首元素的地址,但是有两个例外:
· &数组名---数组名代表数组首元素的地址,取出的是整个数组点的地址
· sizeof(数组名):数组名表示整个数组,计算的是整个数组的大小,单位是字节
除此之外的其他情况的数组名都是数组首元素的地址!
3. 指针变量类型的意义
A.指针类型决定了指针解引用后所访问的权限大小,比如:
char*类型 的指针解引用一次访问1个字节
int*类型 的指针解引用一次访问4个字节
……
B. 指针的类型决定了指针向前或者向后走一步(+1或者-1)有多大
char*类型指针+1向前访问1个字节
int *类型指针+1向前访问4个字节
……
C. void*类型指针又叫做无类型指针,这类指针可以用来接收任意类型地址,但是它不能进行解引用操作,不能直接进行指针的+-整数运算(可以通过强制类型转换间接+-整数操作)。
4. 字符指针变量
字符指针变量接收字符串的本质是将字符串的首字符地址存放到字符指针变量中,所以如果字符串出现在表达式中,他的值就是第一个字符的地址。
5. 二维数组传参本质
二维数组的首元素就是第一行,是个一维数组。
二维数组的数组名就是就是第一行(一维数组)的地址,二维数组传参本质是传递第一行这个一维数组的地址。
二维数组第一行的一维数组的数据类型是int[n],所以第一行的地址类型就是数组指针类型int(*)[n],所以我们可以将形参类型写成指针形式。
二,数组和指针例题详解
注意:
1. 编程环境是在x64环境下,指针(地址)的大小是8Byte
2. 为了方便代码和笔记同时观察,我选择了将笔记以注释的形式写在下面的代码筐中。
3. 给出的代码例题都是分析程序运行结果
1. 一维数组
#include <stdio.h>
int main()
{
int a[] = { 1,2,3,4 };
printf("%zd\n", sizeof(a));//16 sizeof(数组名)计算的是整个数组的大小,单位字节
printf("%zd\n", sizeof(a + 0));//8 a表示首元素的地址,int*类型,0是地址偏移量,a+0还是首元素地址,大小8Byte
//这里的a不是单独的a,表示首元素的地址
printf("%zd\n", sizeof(*a));// 4 a是首元素的地址,解引用得到首元素1,int类型大小4Byte
//*a==a[0]==*(a+0)
printf("%zd\n", sizeof(a + 1));// 8 a是首元素的地址,+1跳过1个整型(1代表地址偏移量),a+1是第二个元素的地址,大小8Byte
printf("%zd\n", sizeof(a[1]));//4 a[1]是数组第二个元素2,int类型,大小4Byte
printf("%zd\n", sizeof(&a));//8 &a得到整个数组的地址,地址的大小8Byte(x64环境下)
printf("%zd\n", sizeof(*&a));//16 这里容易理解错
//第1种理解方法:*和&互相抵消&----取出地址,*----通过地址找到所指向的对象,一来一去,相互抵消,
//sizeof(*&a)===sizeof(a),计算整个数组的大小,为16Byte
//第2种理解方法: &a得到整个数组的地址,类型:int(*)[4](数组指针),对数组指针解引用访问的是数组,计算数组的大小为16Byte
printf("%zd\n", sizeof(&a + 1));//8 &a:取出了整个数组的地址,&a+1跳过了整个数组之后的地址(元素4外旁边的地址),地址大小仍然是8Byte
printf("%zd\n", sizeof(&a[0]));//8 a[0]是首元素,&a[0]得到首元素地址,大小8Byte
printf("%zd\n", sizeof(&a[0] + 1));//8 &a[0]是首元素地址,+1得到第二个元素的地址,大小8Byte
return 0;
}
2. 字符数组
2.1 花括号形式字符数组
2.1.1 花括号形式字符数组之sizeof
#include <stdio.h>
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%zd\n", sizeof(arr));//6 数组名单独放在sizeof里面,计算的是数组的大小,6Byte
printf("%zd\n", sizeof(arr + 0));//8 arr是首元素地址,0是地址偏移量,arr+0仍是首元素地址,大小8Byte
printf("%zd\n", sizeof(*arr));//1 arr是首元素地址,解引用得到首元素,char类型,大小1Byte
//*arr==arr[0]==*(arr+0)
printf("%zd\n", sizeof(arr[1]));//1 arr[1],第二个元素,char类型,1Byte
printf("%zd\n", sizeof(&arr));//8 &arr得到整个数组的地址(char(*)[6]),大小8Byte
printf("%zd\n", sizeof(&arr + 1));//8 &arr是整个数组的地址,+1跳过整个数组,指向数组后面的空间,8Byte
printf("%zd\n", sizeof(&arr[0] + 1));//8 第二个元素的地址,8Byte
return 0;
}
2.1.2 花括号形式字符数组之strlen
//strlen的参数类型是地址
int main()
{
char arr[] = { 'a','b','c','d','e','f' };// 没有\0
printf("%d\n", strlen(arr));//随机值x arr是首元素地址,从首元素地址开始检索,但是没有\0,出现越界访问,所以得到随机值
printf("%d\n", strlen(arr + 0));//随机值x arr是首元素地址,0是地址偏移量,arr+0是首元素地址,从首元素地址开始检索,但是没有\0,出现越界访问,所以得到随机值
//printf("%d\n", strlen(*arr));// err arr是首元素地址,*arr得到首元素‘a’,对应ASCII码值为97(0x000000061),此地址是不允许访问的,所以报错
//相当于把97作为地址传递给strlen,strlen就得到了野指针(地址不合法)
//printf("%d\n", strlen(arr[1]));//err arr[1]是第二个元素‘b’,对应ASCII码值为98,但是strlen的参数类型是地址,所以报错
printf("%d\n", strlen(&arr));//随机值x &arr是数组地址,起始位置是数组第一个元素的位置,得到随机值
printf("%d\n", strlen(&arr + 1));//随机值x-6 &arr是数组首元素地址,+1还是地址,所以得到的是随机值
printf("%d\n", strlen(&arr[0] + 1));//随机值x-1
return 0;
}
2.2 双引号字符串
2.2.1 双引号字符串之sizeof
int main()
{
char arr[] = "abcdef";//a b c d e f \0
printf("%zd", sizeof(arr));//7 arr是数组名,单独放在sizeof内部,计算的是数组的总大小,7Byte
printf("%zd", sizeof(arr + 0));//1 arr是数组首元素地址,0是地址偏移量,arr+0仍然是数组首元素地址,1Byte
printf("%zd", sizeof(*arr));//1 arr表示数组首元素地址,*arr是首元素‘a’,char类型,大小1Byte
printf("%zd", sizeof(arr[1]));//1 arr[1]是‘b’,char类型,1Byte
printf("%zd", sizeof(&arr));//8 &arr得到的是整个数组的地址,数组的地址也是地址,8Byte
printf("%zd", sizeof(&arr + 1));//8 &arr得到的是整个数组的地址,+1跳过整个数组,还是地址,8Byte
printf("%zd", sizeof(&arr[0] + 1));//8 &arr[0]+1是第二个元素的地址,8Byte
return 0;
}
2.2.2 双引号字符串之strlen
int main()
{
char arr[] = "abcdef";// a b c d e f \0
printf("%d\n", strlen(arr));//6 arr是数组名,是首元素地址,而且strlen能够找到\0,因此6Byte
printf("%d\n", strlen(arr + 0));//6 arr是首元素地址,arr+0还是首元素地址,6Byte
printf("%d\n", strlen(*arr));//err arr是首元素地址,*arr得到首元素‘a’,ASCII码值97,但是strlen的参数类型是地址,所以报错
printf("%d\n", strlen(arr[1]));//err arr[1]是第二个元素地址,但是strlen的参数类型是地址,所以报错
printf("%d\n", strlen(&arr));//6 &arr得到整个数组的地址,志向的也是起始位置,从起始位置出发开始检索\0,大小6Byte
printf("%d\n", strlen(&arr + 1));//随机值 &arr+1是跳过整个数组的地址,但是不知道跳过数组后\0位置在哪里,所以随机值
printf("%d\n", strlen(&arr[0] + 1));//5 &arr[0]是数组首元素地址,+1得到第二个元素地址,往后检索,5Byte
return 0;
}
2.3 char*指针变量存储字符地址
2.3.1 sizeof形式
int main()
{
const char * p = "abcdef";//常量字符串在存储时为了避免被修改,加入const
printf("%zd\n", sizeof(p));//8 p是指针变量,存放的是首元素a的地址计算的是指针变量的大小,8Byte
printf("%zd\n", sizeof(p + 1));//8 p+1是b的地址,地址大小8Byte
printf("%zd\n", sizeof(*p));//1 p的类型是const char*,对p解引用得到a元素,char类型,1Byte
printf("%zd\n", sizeof(p[0]));//1 两种理解:1. p[0]==*(p+0)==*p,得到‘a’,char类型,1Byte
// 2. 把常量字符串想象成数组,p可以理解为数组名,p[0]就是首元素,char类型,大小1Byte
printf("%zd\n", sizeof(&p));//8 &p得到的是指针变量p自身的地址,地址大小bByte
//char* *q = &p; &p+1
printf("%zd\n", sizeof(&p + 1));//8 &p+1得到的是跳过p指针变量后的地址(跳过一个char*类型的元素),仍然是一个地址,大小8Byte
printf("%zd\n", sizeof(&p[0] + 1));//8 &p[0]得到字符串首元素‘a’的地址,+1,得到第二个字符‘b’的地址
return 0;
}
2.3.2 strlen形式
int main()
{
char* p = "abcdef";//
printf("%d\n", strlen(p));//6 从a在\0之前,6Byte
printf("%d\n", strlen(p + 1));//5 从b开始,5Byte
//printf("%d\n", strlen(*p));//err *p就是a--97--error
//printf("%d\n", strlen(p[0]));//err p[0]==*(p+0)==*p
printf("%d\n", strlen(&p));//随机值x &p得到的是p变量自己的地址(二级指针),而且不止到这个地址指向的对象的内容
//strlen从p指针变量气死位置开始向后检索,随机值
//char* *q = &p;
printf("%d\n", strlen(&p + 1));//随机值y 随机值x和y没有联系,他们之间相差了一个p的空间,p空间里可能会有\0
printf("%d\n", strlen(&p[0] + 1));//5 &p[0]得到字符串首元素‘a’的地址,+1,得到第二个字符‘b’的地址,从b开始检索\0,5Byte
return 0;
}
3. 二维数组
int main()
{
int a[3][4] = { 0 };
printf("%zd\n", sizeof(a));//48 a是数组名,单独放在sizeof内,计算整个数组大小,48Byte
printf("%zd\n", sizeof(a[0][0]));//4 a[0][0]==第一行第一列元素,int类型,大小4Byte
printf("%zd\n", sizeof(a[0]));//16 a[0]是第一行数组的数组名代表整个数组,sizeof计算第一行数组的大小,16Byte
printf("%zd\n", sizeof(a[0] + 1));//8 a[0]表示第一行数组名,放在sizeof里面,不是单独的,代表数组首元素a[0][0]地址,+1表示a[0][1]的地址,地址大小8Byte
printf("%zd\n", sizeof(*(a[0] + 1)));//4 这里的a[0]表示第一行数组名,不是单独的就代表是数组首元素a[0][0]地址,+1代表a[0][1]地址,解引用得到int类型元素,4Byte
printf("%zd\n", sizeof(a + 1));//8 a是二维数组的数组名,但是既没有&,也没有单独放在sizeof里面,
// 所以根据二维数组传参的本质,数组名代表数组第一行元素地址
//a + 1就是第二行地址(a+1是数组指针),地址大小8Byte
printf("%zd\n", sizeof(*(a + 1)));//16 两种理解:1. *(a + 1)== a[1],数组名单独放在sizeof里面,计算第二行的大小,16Byte
// 2. a + 1是第二行数组地址,解引用得到第二行元素,计算第二行元素大小,16Byte
printf("%zd\n", sizeof(&a[0] + 1));//8 a[0]是第一行的数组名,&a[0]取出的是第一行数组的地址,+1得到第二行地址,大小8Byte
printf("%zd\n", sizeof(*(&a[0]+1)));//16 *(&a[0] + 1)是对第二行地址解引用,访问的就是第二行,大小16Byte
printf("%zd\n", sizeof(*a));//16 a作为数组名并没有单独放在sizeof内部,a表示二维数组首元素(第一行)的地址,*a访问第一行,计算第一行大小16Byte
//*a==*(a+0)==a[0]
printf("%zd\n", sizeof(a[3]));//16 a[3]计算的是第四行的大小,类型是int,4*4=16,不需要关注内容,只需要知道类型以及元素个数
//a[3]无需真实存在,仅仅通过类型的推断就可以计算出长度
//sizeof(int)计算int类型的大小,并不需要知道内容
return 0;
}
4. 指针运算
4.1 整型指针和结构体指针
int main()
{
int a[5] = { 1,2,3,4,5 };//
int* ptr = (int*)(&a + 1);//&a得到整个数组的地址(int(*[5]),强制类型转换为int*类型,
printf("%d,%d", *(a + 1), *(ptr - 1));//
//*(a + 1)== a[1]== 2
//*(ptr - 1)== 5
return 0;
}
//指针+-整数
/*在x86环境下,假设结构体大小20Byte,程序输出结果是啥?*/
struct Test
{
int num;
char* pnName;
short sDate;
char cha[2];
short sBa[4];
} * p = (struct Test*)0x100000;//结构体指针+1跳过一个结构体
int main()
{
printf("%p\n", p + 0x1);//指针+10x100000+20===00100014
printf("%p\n", (unsigned long)p + 0x1);//p是struct Test*类型,强制类型转换成unsigned long类型,整型,不是指针类型,+1就是真的+1,===00100001
printf("%p\n", (unsigned int*)p + 0x1);//p是struct Test*类型,强制类型转换成unsigned int*类型,0x100000+4====00100004
return 0;
}
4.2 指针的各种运算
4.2.1 题目1
int main()
{
int a[3][2] = { (0,1),(2,3),(4,5) };//这里用的是小括号,三个逗号表达式,从左向右依次计算,结果是最后一个表达式的结果
//所以a[3][2]={1,3,5}
//1 3
//5 0
//0 0
int* p;
p = a[0];//a[0]表示数组第一行数组名,是数组首元素的地址
printf("%d", p[0]);//p[0]==*(p+0)==*p---数组首元素的地址解引用,得到1
return 0;
}//运行结果为1
4.2.2 题目2
运行环境为x86,程序输出结果是什么?
int main()
{
int a[5][5];
int(*p)[4];//p是一个数组指针,p指向的数组是4个整型元素
p = a;//a是数组名
//a---int(*)[5](数组名是数组首元素的地址,这里就表示地址)
//p---int(*)[4]类型不一样,会出现警告
//p[4][2]===*(*(p+4)+2) -4用%d形式打印
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
// -4(用%p打印得到的是地址,10000000000000000000000000000100,源码)
// 11111111111111111111111111111011,反码
// 1111 1111 1111 1111 1111 1111 1111 1100,补码
// F F F F F F F C
// 指针-指针,两个指针之间的元素个数
return 0;
}
4.2.3 题目3
int main()
{
int aa[2][5] = { 1,2,3,4,5,6,7,8,9,10 };
// 下标: (第一行) 0 1 2 3 4, 5 6 7 8 9
// 下标对应元素 (第二行) 1 2 3 4 5, 6 7 8 9 10
int* ptr1 = (int*)/*强制类型转换*/(&aa + 1);//aa是数组名,即数组首元素的地址,+1,跳过整个数组
//ptr1存了&aa + 1的int*类型的地址,也就是数组aa旁边位置的地址
int* ptr2 = (int*)(*(aa + 1));//*(aa + 1) == aa[1],数组名,是数组第二行首元素的地址,
//ptr2存了第二行元素中首元素的地址
printf("%d %d", *(ptr1 - 1), *(ptr2 - 1));
// ptr2 - 1表示数组第一行最后一个元素的地址,解引用得到第一行最后一个元素5
// ptr1 - 1表示数组最后一行最后一个元素的地址,解引用得到最后一行最后一个元素10
return 0;
}
4.2.4 题目4
int main()
{
char* a[] = { "work","at","alibaba" };//数组中存入的是字符串首元素的地址(a是一个数组用来存放指针)
char** pa = a;//a是数组名,代表数组首元素的地址,指向的是'w'的地址
//二级指针,存放的是一级指针
pa++;//pa现在指向的是'a'(第二个元素)的地址
printf("%d\n", *pa);//*pa是‘a’元素,以%s的形式打印,则遇到\0才停止
//*pa表示at
return 0;
}
4.2.5 题目5
int main()
{
char* c[] = { "ENTER","NEW","POINT","FIRST" };
char** cp[] = { c + 3,c + 2,c + 1,c };
char*** cpp = cp;
printf("%s\n", **++cpp);
//先++,得到c +2的地址。注意:++cpp是对cpp空间里面的值进行操作,所以此时cpp的值已经变了
//再解引用第一次,得到c + 2这个元素,对他解引用,得到p元素的地址,打印%s形式,则输出POINT
printf("%s\n", *-- * ++cpp + 3);//++cpp改变了cpp空间中的值
//++cpp得到的是c + 1这个元素的地址,
//*(++cpp):对c + 1元素解引用得到的是c + 1 元素
//--(*(++cpp))再进行自减操作,(c + 1) - 1得到c元素,注意:这里的自减操作是在cpp空间里面进行的操作,(也就是cp第三个元素的值由c + 1变成了c)
//对c元素解引用得到的是得到E的地址,%s形式打印,输出ER
printf("%s\n", *cpp[-2] + 3);//cpp[-1]操作不会改变cpp空间中的值
//cpp[-2]==*(cpp-2),cpp-2指向cp中的c+3位置,解引用得到c+3元素
//解引用得到c+3指向的F的地址
//+3,得到指S的地址,用%s的形式打印得到:ST
printf("%s\n", cpp[-1][-1] + 1);
//cpp经过一系列操作后指向cp的空间的C元素(第三个元素,有之前的c+1改成现在的C)
// cpp[-1][-1]===*(*(cpp-1)-1),先进行-1和解引用:cpp-1得到cp空间中的c+2地址,解引用得到c+2元素,再进行一次-1和解引用操作:得到
//N的地址,+1得到E的地址,%s形式输出:EW
//对于此题最好的研究方法就是画图,鉴于电脑这个画图贼不好用,因此就选择以文字形式呈现解析
return 0;
}
uu们都看到这儿了,点个赞支持一下呗!
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)