指针进阶篇

指针的基本概念:

  1. 指针是一个变量,对应内存中唯一的一个地址
  2. 指针在32位平台下的大小是4字节,在64位平台下是8字节
  3. 指针是有类型的,指针类型决定该指针的步长,即走一步是多长
  4. 指针运算:指针-指针表示的是两个指针之间的元素个数

回顾完指针的初阶用法,今天我们要来讲讲指针更高阶的用法

一、字符指针

字符指针就是一个指针,指向一个字符

int main()
{
	char ch = 'a';
	char* pc = &ch;
	printf("%c\n", *pc);//打印a

	return 0;
}

还有另一种用法:

int main()
{
	char* p = "abcdefg";
	printf("%c\n", *p);//打印a

	return 0;
}

该怎么理解呢?其实右边的"abcdefg"是一个字符常量,p中存放的是该字符常量首元素的地址。也就是'a'的地址。

来看看一组练习:

int main()
{
	char arr1[] = "abcdefg";
	char arr2[] = "abcdefg";

	char* arr3 = "abcdefg";
	char* arr4 = "abcdefg";

	if (arr1 == arr2)
		printf("arr1和arr2相等\n");
	else
		printf("arr1和arr2不相等\n");

	if(arr3 == arr4)
		printf("arr3和arr4相等\n");
	else
		printf("arr1和arr2不相等\n");

	return 0;
}

解答:

指针进阶篇_第1张图片

二、指针数组

首先,我们得明白指针数组是一个数组,数组中的每个元素是指针类型

int main()
{
	int a = 1;
	int b = 2;
	int c = 3;
	int d = 4;
	int* p1[] = { &a,&b,&c,&d };//数组中的每个元素是整型指针

	char ch1 = 'a';
	char ch2 = 'b';
	char ch3 = 'c';
	char ch4 = 'd';
	char* p2[] = { &ch1,&ch2,&ch3,&ch4 };//数组中的每个元素是字符指针

	return 0;
}

指针数组可以用来模拟实现一个二维数组,代码如下:

int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };
	int* arr[3] = { arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}

	return 0;
}

打印时,arr[i]相当于每行首元素的地址,通过下标j就可以依次访问每个整型数组中的元素了。 

在内存中对应的关系如下:

指针进阶篇_第2张图片

指针数组还有另一种用法:

int main()
{
	char* arr[] = { "baiyahua","dashidai","tassel" };
	for (int i = 0; i < 3; i++)
	{
		printf("%s ", arr[i]);
	}

	return 0;
}

我们得知道,arr中存的是字符'b','d','t'的地址,因此分别打印三个字符长常量。

三、数组指针

3.1数组指针的定义

要同前面的指针数组区别开,数组指针本质上是一个指针,该指针指向一个数组。

int arr[] = { 1,2,3,4,5 };

拿这个代码举例,我们想要将该数组的地址存一个指针当中,得到的那个指针就是一个数组指针。要完成这个操作,需要解决两个问题。

1.怎么取出数组的地址?

2.数组指针的类型改怎么写?

对于第一个问题,我们先来梳理一下数组名的概念:

数组名表示首元素的地址,两个情况除外:

  • sizeof(数组名):表示整个数组的大小,单位是字节
  • &数组名:表示取出整个数组的地址

到这里,第一个问题就解决了,要得到一个数组的地址,只需要&数组名就行了。

对于第二个问题,C语言规定的语法是这样写的:

int(*p)[5] = &arr;

int *p[5] = &arr;//错误的写法

注意:不能写成下面的形式,因为p首先会跟[]结合,表明这是一个数组,数组中的每个元素是int*类型,p就变成了一个指针数组了。所以一定要将*和p括号起来

对于上面的形式,我们可以这样理解,p首先跟*结合,表明这是一个指针,在跟[]结合,表明指针指向一个数组,数组中的每个元素是int类型,就是一个数组指针了。

3.2&数组名和数组名

刚刚我们提到,平常的数组名表示的是数组首元素的地址,而&数组名表示的是整个数组的地址,在内存中怎么体现出来呢?

还记得我们最开始讲的指针类型决定该指针的步长,如果是数组指针,那么+1会跳过一个数组。

int main()
{
	int arr[5] = { 1,2,3,4,5 };

	printf("%p\n", arr);
	printf("%p\n", arr + 1);
	printf("\n");

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

	printf("%p\n", &arr);
	printf("%p\n", &arr + 1);
	printf("\n");

	return 0;
}

指针进阶篇_第3张图片

 可以看到,虽然&arr和arr,&arr[0]的值相等,当时它们+1的步长不同,这是因为&arr取出数组的地址,+1跳过了整个数组。

3.3数组指针的使用

理解完数组指针,就该谈谈数组指针的使用了,实际写代码中,数组指针一般用在二维数组。

一般的二维数组打印:

void Print(int arr[3][5], int row, int col)
{
	int i = row;
	for (i = 0; i < row; i++)
	{
		int j = col;
		for (j = 0; j < col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
	Print(arr,3,5);

	return 0;
}

这里的数组传参,形参我们用了数组的形式,实参部分,我们传了二维数组的数组名,数组名是首元素的地址,只不过对于二维数组,数组的首元素是第一行的地址,也就是说,实参传了一个数组的地址,那形参是不是就可以用我们刚刚学的数组指针来接受了。于是就有了第二种写法:

void Print(int(*p)[5] , int row, int col)
{
	int i = row;
	for (i = 0; i < row; i++)
	{
		int j = col;
		for (j = 0; j < col; j++)
		{
			printf("%d ", p[i][j]);
		}
		printf("\n");
	}
}

这里来解释一下p[i][j]

p[i]相当于*(p+i),p+i是每行的地址

*(p+i)就是每行的数组名,而数组名又是数组首元素的地址

因此p[i]就相当于每行首元素的地址,通过j就可以依次访问每行的元素。

四、数组传参和指针传参

写代码时,我们难免要将一个数组或指针传参,那么形参部分该如果设计?

判断下面的传参是否正确:

4.1一维数组传参

void test(int arr[])//ok?
{}

void test(int arr[10])//ok?
{}

void test(int* arr)//ok?
{}

void test2(int* arr[20])//ok?
{}

void test2(int** arr)//ok?
{}

int main()
{
	int arr[10] = { 0 }; 
	int* arr2[20] = { 0 };
	test(arr);
	test2(arr2);
}

指针进阶篇_第4张图片

 4.2二维数组传参

void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
void test(int* arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int(*arr)[5])//ok?
{}
void test(int** arr)//ok?
{}
int main()
{
	int arr[3][5] = { 0 };
	test(arr);
}

指针进阶篇_第5张图片

 4.3一级指针传参

void Print(int* p, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
}

int main()
{
	int arr[] = { 1,2,3,4,5,6 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	Print(p, sz);

	return 0;
}

思考:当函数的形参是一级指针的时候,可以接受什么参数?

void test(int* p)
{}

int main()
{
	int a = 1;
	int* p = &a;
	int arr[] = { 1,2,3 };

	test(p);//传整型指针
	test(&a);//传整型变量的地址
	test(arr);//传整型一维数组的数组名

	return 0;
}

4.4二级指针传参

void test(int** pp)
{
	printf("%d\n", **pp);
}

int main()
{
	int a = 1;
	int* p = &a;
	int** pp = &p;
	test(pp);
	test(&p);

	return 0;
}

思考:当函数的参数是二级指针的时候,可以传什么参数?

void test(int** pp)
{}

int main()
{
	int a = 1;
	int b = 2;
	int c = 3;
	int* p = &a;
	int** pp = &p;
	int* arr[] = { &a,&b,&c };

	test(pp);//传二级指针变量
	test(&p);//传一级指针变量的地址
	test(arr);//传整型指针数组的数组名

	return 0;
}

五、函数指针

5.1函数指针的定义

函数指针就是存放一个函数地址的指针,同理解数组指针一样,我们得弄明白

1.怎么得到函数的地址?

2.函数指针变量怎么写?

这里就直接给出结论了,&函数名==函数名,都是取出函数的地址。

函数指针变量的写法:

指针进阶篇_第6张图片

int test(int x ,int y)
{}

int main()
{
	int(*p)(int, int) = test;
	int(*p)(int, int) = &test;
	//两种写法都可以

	return 0;
}

5.2函数指针的使用

int Add(int x ,int y)
{
	return x + y;
}

int main()
{
	int a = 1;
	int b = 2;
	int c = Add(1, 2);
	int d = (*Add)(1, 2);

	printf("c = %d d = %d", c, d);

	return 0;
}

对于函数指针,*我们可以加也可以不加,两者是同等的,*并没有实际的作用,但是如果要加*,必须跟p括号起来。

来看看两个有趣的代码:

//代码1
(*(void (*)())0)();

//代码2
void (*signal(int , void(*)(int)))(int);

代码1解读:

该代码我们从0开始,一个整数前面加上括号,第一想到的应该是强制转换。

再看括号里面的部分,void (*)(),是一个函数指针类型,该函数没有参数,返回值是void类型。

加上*,就是引用该函数。

于是,代码的总体意思就是:引用0地址处的函数,该函数没有参数,返回值为void类型

代码2解读:

首先来看signal,后面加上(),容易想到是函数的定义或声明。

再看参数只有类型,没有参数,能确定是函数的声明。

将signal(int,void(*)(int))去掉,余下的void (*)(int)就是函数的返回值类型了。

该代码的意思是:声明一个函数名为signal的函数,该函数的参数类型分别是int和void(*)(int),返回值类型是void(*)(int)

我们还可以对该代码进行一个简化

typedef void (*ptr)(int);

将void(*)(int)类型重命名为ptr,以后我们想用该类型创建变量时,可以直接用ptr。

ptr signal(int, void(*)(int));

六、函数指针数组

函数指针数组,本质上是数组,数组中每个元素都是函数指针。

6.1函数指针数组的定义

int (*arr[])(int, int);

arr首先与[]结合,表明arr是个数组,数组中每个元素的类型是int (*)(int, int)。

6.2函数指针数组的使用

首先我们来模拟一个简单的计算器,可以实现加减乘除法

//计算器version1
void menu()
{
	printf("*************************\n");
	printf("***  1.Add    2.Sub  ****\n");
	printf("***  3.Mul    4.Div  ****\n");
	printf("***      0.Exit     ****\n");
	printf("*************************\n");
}

int Add(int x, int y)
{
	return x + y;
}

int Sub(int x, int y)
{
	return x - y;
}

int Mul(int x, int y)
{
	return x * y;
}

int Div(int x, int y)
{
	return x / y;
}

int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;

	do
	{
		menu();
		printf("请输入操作:>");
		scanf("%d", &input);
		
		switch (input)
		{
		case 1:
			printf("请输入两个操作数:>");
			scanf("%d%d", &x, &y);
			ret = Add(x, y);
			printf("%d\n", ret);
			break;
		case 2:
			printf("请输入两个操作数:>");
			scanf("%d%d", &x, &y);
			ret = Sub(x, y);
			printf("%d\n", ret);
			break;
		case 3:
			printf("请输入两个操作数:>");
			scanf("%d%d", &x, &y);
			ret = Mul(x, y);
			printf("%d\n", ret);
			break;
		case 4:
			printf("请输入两个操作数:>");
			scanf("%d%d", &x, &y);
			ret = Div(x, y);
			printf("%d\n", ret);
			break;
		case 0:printf("退出程序\n");
			break;
		default:printf("输入错误,请重新输入\n");
			break;
		}

	}while (input);

	return 0;
}

该代码虽然能完成我们的要求,但试想,如果我们要更多的运算,一方面我们得增加运算的定义,另一方面我们还要增加case语句的长度,代码就会变得非常长,这时就可以使用函数指针数组来优化我们的代码。

优化后的代码如下:

//计算器version2
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;

	int(*pf[5])(int, int) = { NULL, Add, Sub, Mul, Div };
	//将运算函数的地址存到一个函数指针数组中
	//输入数字,通过数组下标执行相应的函数
	//为了保证输入的数字和我们想要执行的函数对应
	//加上NULL
	do
	{
		menu();
		printf("请输入操作:>");
		scanf("%d", &input);

		if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:>");
			scanf("%d%d", &x, &y);
			ret = pf[input](x, y);
			printf("%d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出程序\n");
		}
		else
		{
			printf("输入错误,请重新输入\n");
		}

	} while (input);

	return 0;
}

这时,我们想要增加运算时,只要添加运算的定义,在函数指针数组中增加新增函数的地址,修改一下if条件,而我们的代码长度不变,大大增加了代码的可读性和维护性。

需要注意的是,由于数组是存储一系列相同类型的元素,因此所有函数的参数类型和返回值必须相同才能使用函数指针数组。

七、指向函数指针数组的指针

首先,指向函数指针数组的指针是一个指针,指向一个数组,数组中每个元素的类型都是函数指针类型。

定义:

int (*pf[5])(int, int) = { NULL,Add,Sub,Mul,Div };

int (*(*p)[5])(int, int) = &pf;
//*先跟p结合,表明这是个指针
//往后看到[],表明指针指向一个数组,数组中有5元素
//剩下的int (* )(int, int)就是元素的类型

当然,我们日常写代码时很少用到指向函数指针数组的指针,大家可以做个了解。

八、回调函数

8.1回调函数的定义和使用

回调函数的定义:回调函数不是由函数的实现方直接调用,而是处于某种特定的情境下,由另一个函数通过函数指针的方式调用的。

这里用上面计算器 version1举例,我们发现代码有很多冗余的部分。

指针进阶篇_第7张图片

这些部分能不能只写成一份?于是就有了我们的计算器version3。

//计算器version3
void Calc(int (*p)(int, int))
{
	int x = 0;
	int y = 0;
	int ret = 0;
	
	printf("请输入两个操作数:>");
	scanf("%d%d", &x, &y);
	ret = p(x, y);
	printf("%d\n", ret);
}

int main()
{
	int input = 0;
	
	do
	{
		menu();
		printf("请输入操作:>");
		scanf("%d", &input);

		switch (input)
		{
		case 1:
			Calc(Add);
			break;
		case 2:
			Calc(Sub);
			break;
		case 3:
			Calc(Mul);
			break;
		case 4:
			Calc(Div);
			break;
		case 0:printf("退出程序\n");
			break;
		default:printf("输入错误,请重新输入\n");
			break;
		}

	} while (input);

	return 0;
}

 计算器version3中的Add,Sub,Mul,Div就是回调函数,这些函数不是本身直接调用,而是作为一个参数,传到Calc函数中,由Calc函数调用的。

这里再用库中的qsort函数来帮助大家更好的理解回调函数

8.2qsort的使用

在讲qsort函数之前,简单回顾一下冒泡排序,由于该排序比较简单,就直接给出代码了。

void Bubble_Sort(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

void Print(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

int main()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	Print(arr, sz);
	Bubble_Sort(arr, sz);
	Print(arr,sz);

	return 0;
}

该代码的缺陷是只能排序整型数据,当我们想要排序结构体类型,字符类型等的数据,它就不能完成要求,但qsort函数是可以做到的。

qsort函数是C语言的库函数,大家可以自行查找具体用法,这里就给出它大致的用法。

void qsort(void* base, 
	       size_t num, 
	       size_t size,
	       int (*compar)(const void* p1, const void* p2));
//base指向待排序的数组
//num是待排序数组元素的个数
//size是待排序数组一个元素的大小
//compar是一个函数指针,p1和p2是两个待比较的元素

void*类型可以接受任意类型的数据,但不能进行解引用和+-1操作,它没有具体的类型

为什么base,p1和p2要写成void*类型呢?

因为qsort要实现的是对任意类型的数据进行排序,如果是int*,char*等具体类型的指针,就只能接收对应类型的数据,想要排序其他类型的数据,就得改变base,p1和p2的类型,而我们的库函数肯定是不能随便乱动的。

对于qsort,我们只需要实现数据的比较方法函数,再将该函数作为函数指针参数,传到qsort函数中,qsort就会自动为我们排序。

而对于compar函数,我们要完成的功能是:

当p1所指对象

当p1所指对象>p2所指对象,返回正数

当p1所指对象==p2所指对象,返回0

下面使用qsort来排序一个结构体

struct Stu
{
	char name[20];
	int age;
};

void PirntStruct(const void* arr, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%s %d\n", (((struct Stu*)arr)+i)->name, ((struct Stu*)arr+i)->age);
	}
	printf("\n");
}

//以年龄排序
int compar_by_age(const void* p1, const void* p2)
{
	return (((struct Stu*)p1)->age - ((struct Stu*)p2)->age);
}

//以名字排序
int compar_by_name(const void* p1, const void* p2)
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
    //字符串的比较用strcmp函数,strcmp函数的返回值正好与我们要求的返回值一致
}

int main()
{
	struct Stu stu[] = { {"baiyahua",20},{"zhangsan",18},{"lisi",13} };
	int sz = sizeof(stu) / sizeof(stu[0]);
	printf("排序前:\n");
	PirntStruct(stu, sz);

	printf("以名字排序后:\n");
	qsort(&stu, sz, sizeof(stu[0]), compar_by_name);
	PirntStruct(stu, sz);

	printf("以年龄排序后:\n");
	qsort(&stu, sz, sizeof(stu[0]), compar_by_age);
	PirntStruct(stu, sz);

	return 0;
}

8.3qsort的模拟实现

知道了qsort的使用方法,我们能不能自己实现一个qsort函数,这里底层用冒泡排序来模拟一个qsort函数。

需要解决的问题:

函数的参数是怎么设计的?这里可以参考库中的qsort函数

怎么比较两个数据?由于需要排序不同类型的数据,就需要不同类型数据的比较方法,我们将方法独立写成一个函数,将该函数的指针传为我们的qsort函数,再我们的qsort函数中调用

怎么交换两个数据?我们将两个数据的内存全部交换相当于交换的两个数据

int compar(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}

void Swap(char* e1, char* e2, int size)
{
	int i = 0;
	for (i = 0; i < size; i++)
	{
		char tmp = e1[i];
		e1[i] = e2[i];
		e2[i] = tmp;
	}
}

void my_qsort(void* base, size_t num, size_t size, int compar(const void*,const void*))
{
	int i = 0;
	for (i = 0; i < num - 1; i++)
	{
		int j = 0;
		for (j = 0; j < num - 1 - i; j++)
		{
			if (compar((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
			{
				Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}
}

这里解释一下两个地方

指针进阶篇_第8张图片

 想要排序结构体数据时只要给出结构体数据的比较方法即可

九、指针和数组笔试题解析

int main()
{
	//一维数组
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a));// 16,sizeof(数组名),计算整个数组的大小
	printf("%d\n", sizeof(a + 0));// 4/8,32位机器下4字节,64位机器下8字节
	printf("%d\n", sizeof(*a));// 4,sizeof(1),整形数据的大小
	printf("%d\n", sizeof(a + 1));// 4/8,地址的大小
	printf("%d\n", sizeof(a[1]));// 4,sizeof(2)
	printf("%d\n", sizeof(&a));// 4/8,地址的大小
	printf("%d\n", sizeof(*&a));// 16,*和&相抵消,相当于sizeof(a)
	printf("%d\n", sizeof(&a + 1));// 4/8,地址的大小
	printf("%d\n", sizeof(&a[0]));// 4/8,地址的大小
	printf("%d\n", sizeof(&a[0] + 1));// 4/8,地址的大小

	return 0;
}
//字符数组
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,字符'a'的大小
	printf("%d\n", sizeof(arr[1]));//1,字符'b'的大小
	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));//随机值
	printf("%d\n", strlen(*arr));
	//程序报错,'a'的ASCII码值是97,strlen认为97是个地址,而97对于的内存地址不属于我们
	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;
}
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));//程序报错
	printf("%d\n", strlen(arr[1]));//程序报错
	printf("%d\n", strlen(&arr));//6
	printf("%d\n", strlen(&arr + 1));//随机值
	printf("%d\n", strlen(&arr[0] + 1));//5

	return 0;
}
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));//程序报错
	//printf("%d\n", strlen(p[0]));//程序报错
	printf("%d\n", strlen(&p));//随机值
	printf("%d\n", strlen(&p + 1));//随机值
	printf("%d\n", strlen(&p[0] + 1));//5

	return 0;
}

10.指针笔试题

笔试题1:

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

代码解析:

  • a是数组名,既没有sizeof(数组名),也没有&数组名,所以就表示数组首元素地址;
    *(a+1)就表示数组第二个元素,也就是2
  • &a取出整个数组的地址,(&a+1)跳过一个数组,再强转成一个int*类型;因此ptr-1就指向数组倒数第一个元素,*(ptr-1)就是5

笔试题2: 

//由于还没学习结构体,这里告知结构体的大小是20个字节
struct Test
{
	int Num;
	char* pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
	p = 0x100000;
	printf("%p\n", p + 0x1);
	printf("%p\n", (unsigned long)p + 0x1);
	printf("%p\n", (unsigned int*)p + 0x1);
	return 0;
}

题目解析:

  • p是一个结构体指针,+1跳过一个结构体的大小,因此(p+1)跳过20个字节,转换成16进制就是0x00000014,(p+0x1)就是00100014
  • p被强转成无符号长整型,+1就是整型+1,因此((unsigned long)p+0x1)就是00100001
  • p被强转成无符号整型指针,+1跳过一个整型指针类型,也就是4字节,((unsigned int*)p+0x1)就是00100004

笔试题3:

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

题目解析:

  • (&a+1)跳过数组,再强转成整型;ptr[-1]相当于*(ptr-1),答案是4
  • a被强转成整型,+1就是数值+1,再被强转整型指针,这里我们需要画出a数组的详细内存图指针进阶篇_第9张图片
    *ptr2就是图中的红色部分,但内存中是以小端字节序存储的,拿出来要还原,因此答案就是02000000

笔试题4:

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

题目解析:

  • 需要注意,数组的初始化中有(),不要误以为这是在初始化数组,()中是一个表达式,其结果是最后一个表达式的结果,因此(0,1)是1,(2,3)是3,(4,5)是5,实际的数组中的数据

    指针进阶篇_第10张图片

    a[0]是数组首行的地址,也是第一行的数组名,数组名表示首元素的地址,a[0]就是第一行第一个元素的地址,p[0]相当于*(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;
}

题目解析:

  • 这题需要我们画出数组内存图指针进阶篇_第11张图片
    (&p[4][2]-&a[4][2])是一个高地址-低地址,得出的结果是-4;
    -4的补码是:11111111 11111111 11111111 11111100,表示成16进制就是FFFFFFFC
    以%d打印就是-4

笔试题6: 

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

代码解析:

  • (ptr1-1)是数组倒数第一个元素的地址,*(ptr1-1)就是10
  • *(aa+1)可以看成a[1],是第二行的数组名,也就是第二行首元素的地址,被强转成了int*;因此(ptr2-1)是第一行倒数第一个元素的地址,答案是5指针进阶篇_第12张图片

笔试题7:

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

题目解析:

  • 内存分布图指针进阶篇_第13张图片
    答案就是at

面试题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;
}

题目解析:

  • 内存分布图指针进阶篇_第14张图片
    需要注意:++会改变cpp的值
    因此答案就是POINT,ER,ST,EW

关于指针的内容就讲到这!

你可能感兴趣的:(c语言)