(C语言进阶)指针进阶(下)

  • 指针和数组面试题的解析
  • 指针笔试题

———————————————————————————————————————————

首先,我们重新回顾一下:

数组是能够存放一组相同类型的元素,数组的大小取决于数组的元素个数和元素类型。而指针就是地址;指针变量来存放指针的地址,大小是4或8个字节。

指针是指针,数组是数组,两者不等价。

数组名是首元素的地址,这个地址可以存放在指针变量中。我们可以通过指针来遍历数组

数组名在大部分情况下数组名是数组的首元素地址,但是也有两个例外:

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

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


1. 指针和数组笔试题解析

1.一维数组

样例题目 :

//一维数组
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));

解析为:

int main()
{
	//一维数组
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a));//16
	//sizeof(a)就是数组名单独放在sizeof内部,计算的数组总大小,单位是字节
	printf("%d\n", sizeof(a + 0));//4或8字节
	//sizeof(a+0)这里没有把a单独放在sizeof内部,所以数组名是首元素地址,所以+0还是首元素地址因此为4/8个字节
	//a+0是首元素地址,同时注意+0不可以被忽略
	printf("%d\n", sizeof(*a));//4
	//a是数组首元素地址,相当于 &a[0]
	//*a等价于*&a[0],因此等于a[0]
	printf("%d\n", sizeof(a + 1));//4或8
	//a 是数组首元素的地址,类型是 int*
	//a+1 就是跳过一个 int* ,是第二个元素的地址
	printf("%d\n", sizeof(a[1]));//4
	printf("%d\n", sizeof(&a));//4或者8
	//&a 取出的是数组的地址,数组的地址也是地址,大小是4/8个字节
	//int (*pa)[4] = &a
	printf("%d\n", sizeof(*&a));//16
	//*&a == a,sizeof(a)计算的是数组总大小
	//或者理解为&a是数组的地址,得到的指针类型是: int(*)[4];(指向数组的指针)
	//当对指针进行解引用的时候,访问几个字节取决于指针的类型
	//这里*解引用访问数组的指针,解引用访问一个数组的大小,这个数组4个元素每个元素是int类型,所以4*4=16
	printf("%d\n", sizeof(&a + 1));//4/8
	//《特别注意》  &a 取出的是整个数组的地址,类型是int (*)[4] = &a;
	//&a+1 就跳过整个数组,指向的是紧跟数组后的地址
	printf("%d\n", sizeof(&a[0]));//4/8
	//&a[0] 取出数组首元素的地址
	printf("%d\n", sizeof(&a[0] + 1));//4/8
	//&a[0]+1 取出数组第二个元素的地址
	return 0;
}

注:对于&a+1这一行来说,&a+1跳跃的是整个数组(sizeof计算大小是4/8字节)。

而a+1是跳过一个元素,和&a[0]与&a[0]+1是一回事。

 2.字符数组

在此之前需要注意:

1. sizeof 计算的是占用内存空间的大小,单位是字节,不关注内存中到底存放的是什么

2. sizeof 不是函数,是操作符

3. strlen 是函数

4. strlen 是针对字符串,求的是字符串的长度,本质上统计的是 \0 之前出现的字符个数

(strlen才关注\0)

5.strlen的参数为const char* str,如果为数字,则会把数字当成地址产生非法访问的结果。

1. char arr[] = { 'a','b','c','d','e','f' };

样例题目 :

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;
}

解析:

前7个sizeof的计算

 

int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", sizeof(arr));//6
	printf("%d\n", sizeof(arr + 0));//4/8
	//arr+0 是数组首元素的地址
	printf("%d\n", sizeof(*arr));//1
	//arr 数组首元素的地址,*arr 表示数组首元素
	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));
	//随机值
	//arr 数组首元素地址
	printf("%d\n", strlen(arr + 0));
	//随机值
	//arr+0 数组首元素地址
	printf("%d\n", strlen(*arr));//非法访问—strlen访问97的地址的值
	//arr是首元素地址,*arr进行解引用操作 得到a
	//而strlen(const char* str) 接受的是字符指针 因此传入a的ASCII值 为97,而后会吧97的地址的值进行访问,但是地址是随意访问的
	printf("%d\n", strlen(arr[1]));
	//arr[1] 代表数组第二个元素,‘b’- 98 当成地址,形参非法访问
	printf("%d\n", strlen(&arr));//随机值 和strlen(arr)是一样的都是随机值
	//&arr 数组的地址,传给strlen后从起始位置开始计算
	printf("%d\n", strlen(&arr + 1));//随机值-6
	//&arr+1 整个数组后的地址————和上面sizeof跨越长度是一样的跨越一个数组,因此从f的下一个元素开始数(之间差了6个)
	printf("%d\n", strlen(&arr[0] + 1));//随机值-1
	//&arr[0]+1 数组第二个元素的地址 从第二个元素开始数
	return 0;
}

图解: 

2. char arr[] = "abcdef";

 样例题目 :

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));
}

 解析:

int main()
{
	char arr[] = "abcdef";
	printf("%d\n", sizeof(arr));//7  [a b c d e f \0]
	//sizeof(arr) 计算的是整个数组的大小,而且sizeof只关注占用内存空间的大小
	printf("%d\n", sizeof(arr + 0));//4/8
	//arr 数组首元素的地址,*arr 表示数组首元素 
	printf("%d\n", sizeof(*arr));//1
	printf("%d\n", sizeof(arr[1]));//1
	printf("%d\n", sizeof(&arr));//4/8
	//&arr 取出的是数组的地址
	printf("%d\n", sizeof(&arr + 1));//4/8
	//&arr+1 跳过一个数组后的地址
	printf("%d\n", sizeof(&arr[0] + 1));//4/8
	//&arr[0] 数组首元素的地址,+1 数组第二个元素的地址


	//strlen 针对字符串,求的是字符串的长度,本质上统计的是 \0 之前出现的字符个数
	printf("%d\n", strlen(arr));//6
	//arr 首元素的地址
	printf("%d\n", strlen(arr + 0));//6	
	//arr+0 首元素的地址
	printf("%d\n", strlen(*arr));//非法访问
	//*arr 代表首元素,但是 strlen 函数传入的时候是const char*str,因此将字符的ASCLL码的值作为地址给strlen作为参数
	printf("%d\n", strlen(arr[1]));//非法访问
	//arr[1] 代表数组第二个元素,但是 strlen 函数传入的时候是const char*str,因此将字符的ASCLL码的值作为地址给strlen作为参数
	printf("%d\n", strlen(&arr));//6
	//&arr -指针类型为 char(*)[7],之后传给strlen会发生强转,强转为const char *
	printf("%d\n", strlen(&arr + 1));//随机值
	//&arr+1 数组后的地址,因为不确定所以为随机值
	printf("%d\n", strlen(&arr[0] + 1));//5
	//&arr[0]+1 数组第二个元素的地址,即b到\0;
}

 图解: 

3.char *p = "abcdef";

sizeof计算大小 

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));
}

解析:

int main()
{
	char* p = "abcdef";
	printf("%d\n", sizeof(p));//4/8
	//把常量字符串的首元素地址放进*p里去了,也就是a的地址
	printf("%d\n", sizeof(p + 1));//4/8
	//p 的类型是char*,+1就跳过一个char*,也就是说跳过一个字符的地址,相当于b的地址
	printf("%d\n", sizeof(*p));//1
	//*p 指针解引用,因此*p表示首字符a
	printf("%d\n", sizeof(p[0]));//1
	//p[0]相当于*(p+0),表示字符串首元素a
	printf("%d\n", sizeof(&p));//4/8
	//&p 取出的是指针变量p在内存中的地址 类型是char** //char* *pp=&p
	printf("%d\n", sizeof(&p + 1));//4/8
	//&p+1 跳过p的地址,但还是一个地址
	printf("%d\n", sizeof(&p[0] + 1));//4/8
	//&p[0]得到a的地址,&p[0]+1,往后移动一位得到的就是b的地址
}

strlen计算大小

int main()
{
char *p = "abcdef";
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;
}

解析:

int main()
{
	char* p = "abcdef";
	printf("%d\n", strlen(p));//6
	printf("%d\n", strlen(p + 1));//5
	printf("%d\n", strlen(*p));//非法访问
	printf("%d\n", strlen(p[0]));//非法访问
	//p[0]相当于*(p+0),表示字符串首元素a
	printf("%d\n", strlen(&p));//随机值
	//拿到的是p的地址
	printf("%d\n", strlen(&p + 1));//随机值,且与&p无关系
	//拿到的是p后面的地址
	printf("%d\n", strlen(&p[0] + 1));//5
	//&p[0]得到a的地址,&p[0]+1,往后移动一位得到的就是b的地址
	return 0;
}

3.二维数组

//二维数组
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[0] + 1)));
	printf("%d\n", sizeof(*a));
	printf("%d\n", sizeof(a[3]));
}

解析: 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<string.h>
int main()
{
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));//48
	//二维数组的数组名a单独放在sizeof内部,计算的是整个数组的大小
	printf("%d\n", sizeof(a[0][0]));//4
	//a[0][0] 第1行第1个元素
	printf("%d\n", sizeof(a[0]));//16
	//a[0] 第1行的数组名,数组名单独放在sizeof内部
    //计算的是第1行数组的大小,单位是字节,16
	printf("%d\n", sizeof(a[0] + 1));//4/8
    //a[0]不是单独放在sizeof内部
    //arr[0]表示的是首元素地址,即第1行第1个元素的地址 - &a[0][0]
	//a[0]+1 是第1行第2个元素的地址  &a[0][1]
	printf("%d\n", sizeof(*(a[0] + 1)));//4
	//*(a[0]+1) 意思为访问第1行第2个元素a[0][1]
	printf("%d\n", sizeof(a + 1));//4/8
	//a作为二维数组的数组名并非单独放在sizeof内部,所以表示首元素的地址
    //二维数组的首元素是第一行,这里的a就是第一行的地址---int(*)[4]
    //a+1是跳过第一行,指向第二行
    //注意:这里是第二行的地址而不是第二行第一个首元素的地址
	printf("%d\n", sizeof(*(a + 1)));//16
	//*(a+1)--->a[1]
	printf("%d\n", sizeof(&a[0] + 1));//4/8
	//&a[0]是第一行地址,&a[0]+1是第二行的地址
	printf("%d\n", sizeof(*(&a[0] + 1)));//16 a[1]
	//*(&a[0]+1) 第二行的元素
	printf("%d\n", sizeof(*a));//16 *a 就是第一行
	//*a --*(a+0) --a[0]
	printf("%d\n", sizeof(a[3]));//16
	return 0;
} 

考考: 

    int arr[3][4] = { 0 };
	printf("%d\n", sizeof(*arr+1));//?

解析: 

首先arr不是单独放在sizeof内部,所以arr取的是首元素地址,而arr为二维数组所以首元素为arr[0],之后arr[0]+1,则取的是arr[0][0]+1的地址也就是&arr[0][1],答案则为4/8

其实,二维数组如果没有单独放在sizeof内部就会发生降级, 例如sizeof(a)sizeof(a+1)sizeof(a[0]+1)这些事例能充分反应出降级,且上面考考例子就是如此!

4.sizeof的表达式计算

例子:

int main()
{
	int a = 5;
	short s = 11;
	printf("%d\n", sizeof(s = a + 2));
	printf("%d\n", s);
	return 0;
}

输出结果: 

 为什么是这样的呢,而不是输出2和7呢?原因是因为sizeof不会计算括号内表达式的值(并且括号内的表达式不会执行,即被忽略),只会关注sizeof括号内部的类型属性的值

对于这个表达式来说int a=10;a+3;值属性是13,而类型属性是int,大小为4;再来分析上面的例子,代码"sizeof(s=a+2);"s的值属性为7,而s的类型属性为short,大小为2。sizeof只根据类型属性判断大小,不关注sizeof内部的表达式,即不会对s=a+2进行计算。因此下面的printf打印s的值还是11.因此也不会产生越界访问!

总结:

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

2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
3.除此之外所有的数组名都表示首元素的地址。

2.指针笔试题

1.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<string.h>
int main()
{
	int a[5] = { 1, 2, 3, 4, 5 };
	int* ptr = (int*)(&a + 1);
	printf("%d,%d", *(a + 1), *(ptr - 1));
	return 0;
} 

 结果:

2.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<string.h>
struct Test
{
	int Num;
	char* pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
    p = (struct Test*)0x100000;
	printf("%p\n", p + 0x1);
	printf("%p\n", (unsigned long)p + 0x1);
	printf("%p\n", (unsigned int*)p + 0x1);
	return 0;
}

结果: 

 整形指针加1,跳过1个整形,跳过4个字节;字符指针加1,跳过1个字符,跳过1个字节。而这里是自己定义的结构体,结构体大小为20,那么这里+1则跳过20个字节。那么p的值为0x100000时

1.p+0x1,对于结构体类型的p进行加1,需要跳过20个字节,对于16进制则为14,因此结果为0x100014

2. (unsigned long)p + 0x1 将结构体指针类型强制类型转换为unsigned long类型,16进制加减正常+1,因此结果为00100001。

3.(unsigned int*)p+0x1:将结构体指针类型强制类型转换为unsigned int*类型,是一个指针,需要增加一个指针的大小,即+4,结果为00100004。

3.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
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;
}

结果: 

分析:

(int*)(&a+1):首先&a取出整个数组的地址进行加+1,也就是跨越了一个数组,将其转化为int*类型在赋给指针变量ptr。

(int)a+1:a此时为首元素地址(假如a的其实地址为0x0012ff40),a强制类型转换为int类型,+1则正常数字运算,对地址的值进行加1,跳过1个字节,运算后(0x0012ff41)。如果是int*类型的话,+1跳过4个字节,(假如a的其实地址为0x0012ff40,则+1后变成0x0012ff44)

4.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
int main()
{
	int a[3][2] = { (0, 1), (2, 3), (4, 5) };
	//                 1       3       5
	//逗号表达式的结果是最后一个表达式的结果
	int* p;
	p = a[0];
	printf("%d", p[0]);
	return 0;
}

 这道题考察了逗号表达式(0,1),(2,3),(4,5)为逗号表达式,取逗号最右边的结果,即1,3,5,此时二维数组变成了a[3][2]={1,3,5};也就是第一行为a[0]={1,3},a[1]={5,0},a[2]={0,0};

a[0]是二维数组第一行的数组名,对a[0]这个数组名没有&,没有单独sizeof,所以a[0]这个数组名表示数组首元素的地址,即a[0][0]的地址,a[0]---->&a[0][0]

因此p=a[0][0],p指向为首元素地址,p[0]=1

5.

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;
}

 结果:

 &p[4][2] - &a[4][2]中间相差4个元素,而&p[4][2]为低地址处所以为-4。-4的补码在内存中存储,且被看作为地址:

10000000000000000000000000000100   //原码

111111111111111111111111111111111011   //反码

111111111111111111111111111111111100   //补码

%p转换成16进制输出(每4位二进制位为1位十六进制位),则转换为0xFF FF FF FC

6.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
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+1:&aa取出的是整个数组的地址,加1跳过整个数组的大小,指向整个数组的末尾。

*(aa+1):aa是数组首元素的地址,加1后aa+1为数组第2行元素的地址,即aa[1],

然后都-1因为是int*所以指针都往前4个字节,得到结果

7.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
int main()
{
	char* a[] = { "work","at","alibaba" };
	char** pa = a;
	pa++;
	printf("%s\n", *pa);
	return 0;
}

 char*a[]数组存放三个元素,分别是字符串a[0],a[1],a[2];a为首元素地址存放在char* *p指针中,pa=a[0],然后pa++,则pa此时指向的是a[1],有首元素地址打印在\0处结束。

8.

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;
}

**++cpp

操作顺序: 先++,后解引用

指针cpp存储了cp的地址,因此初始先指向cp[0],之后指针cpp到cp[1](也就是红色箭头指向),cp[1]存储的是c+2的地址,因此有了c[2]内容的首元素地址,打印c[2]至'\0'

*--*++cpp+3

操作顺序:  ++,解引用,再--,解引用,最后解引用位置+3

首先++--的优先级最高根据结合远近,先对cpp进行++,但是注意由于上一题++*cpp,是对cp自加并且将自加的值赋给cpp,因此cpp的位置从上一题的位置开始。++cpp后解引用为*cp[2],cp[2]的内容为c+1的指针(也就是c+1的地址),而c+1的地址-1再解引用此时为c[0],而之后+3则指针指向第三个元素的后面,最后从该位置打印至结束。

*cpp[-2]+3

操作顺序:cpp[2]-2到cpp[0],对c[0]指向c[3]对c[3]解引用,再向后移动3个单位也就是第三关字母的后面开始进行打印

 从上个题目的位置,再-2到cpp[-2]==*(cpp-2)==cp[0],cp[0]存储的是c+3的地址,之后对c+3的地址解引用,先指向首元素地址,再+3,在第三个元素的后面,最后从该位置打印至结束

cpp[-1][-1]+1

cpp[-1][-1]==*(cpp-1)[-1]==cp[1][-1]==*((c+2)-1)==*(c+1)==c[1],对cpp-1地址解引用得到c+3,后解引用在-1,解引用得到NEW,指针指向首元素地址,+1改变指针指向,最后打印得到结果

(此坑已完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值