C语言修炼——还不会指针?一次讲明白!最终弹!!

一、回调函数是什么?

回调函数本质就是被函数指针调用的函数。
回调函数有什么用呢?我们先来看一段代码:

#include <stdio.h>
int add(int a, int b)
{
	return a + b;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}
int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	do
	{
		printf("*************************\n");
		printf("***  1:add    2:sub   ***\n");
		printf("***  3:mul    4:div   ***\n");
		printf("***  0:exit           ***\n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
			case 1:
				printf("输入操作数:");
				scanf("%d %d", &x, &y);
				ret = add(x, y);
				printf("ret = %d\n", ret);
				break;
			case 2:
				printf("输入操作数:");
				scanf("%d %d", &x, &y);
				ret = sub(x, y);
				printf("ret = %d\n", ret);
				break;
			case 3:
				printf("输入操作数:");
				scanf("%d %d", &x, &y);
				ret = mul(x, y);
				printf("ret = %d\n", ret);
				break;
			case 4:
				printf("输入操作数:");
				scanf("%d %d", &x, &y);
				ret = div(x, y);
				printf("ret = %d\n", ret);
				break;
			case 0:
				printf("退出程序\n");
				break;
			default:
				printf("选择错误\n");
				break;
		}
	} while (input);
	return 0;
}

可以看出上述代码显得比较冗余:

case i:
		printf("输入操作数:");
		scanf("%d %d", &x, &y);
		ret = xxx(x, y);
		printf("ret = %d\n", ret);
		break;

那么有什么方法能改进呢?这里就用到了回调函数。我们使用回调函数就可以用最简洁高效的代码根据情况灵活的调用特定函数解决特定问题。
代码改进后:

#include <stdio.h>
int add(int a, int b)
{
	return a + b;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}
void cacl(int (*pf)(int, int))
{
	int ret = 0;
	int x, y;
	printf("输入操作数:");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);
	printf("ret = %d\n", ret);
}
int main()
{
	int input = 1;
	do
	{
		printf("*************************\n");
		printf("***  1:add    2:sub   ***\n");
		printf("***  3:mul    4:div   ***\n");
		printf("***  0:exit           ***\n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
			case 1:
				cacl(add);
				break;
			case 2:
				cacl(sub);
				break;
			case 3:
				cacl(mul);
				break;
			case 4:
				cacl(div);
				break;
			case 0:
				printf("退出程序\n");
				break;
			default:
				printf("选择错误\n");
				break;
		}
	} while (input);
	return 0;
}

二、qsort使用举例

在排序算法中,我们常见的有冒泡排序,选择排序,插入排序,快速排序,希尔排序等,而qsort基于快速排序的可以直接用来排序数据的库函数。

2.1 使用qsort排序整型数据

在这里插入图片描述
上图是qsort的一些介绍
主要有这么几点我们需要特别关注一下:

qsort有四个参数void* basesize_t numsize_t sizeint (*compar)(const void*, const void*)。这四个的含义分别是排序的数据所占内存空间的地址数据元素的个数数据元素的大小(所占字节数)比较函数的地址

#include<stdio.h>
int cmp_int(const void* p1, const void* p2)//整型比较函数
{
	return *(int*)p1 - *(int*)p2;
}
void print_arr(int* p_arr, int s)//整型数组打印
{
	for (int i = 0; i < s; i++)
	{
		printf("%d ", p_arr[i]);
	}
}
int main()
{
	int arr[10] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(int), cmp_int);
	print_arr(arr, sz);
	return 0;
}

比方说这里有一个整型数组,我们需要按升序排列它,那么我们需要怎么用qsort函数来排序呢?
我们需要传递数组的首地址,数组的元素个数,数组元素的大小以及该数组元素类型的比较函数。我们需要知道,当我们使用qsort时,我们既可以排序整型数据,浮点型数据,也可以排序字符型数据,甚至是结构体类型数据。但当qsort被实现时,设计者并不知道我们传递的是什么数据,也就无从指定比较方式,所以我们需要自己来给出比较函数。

//当我们需要比较整型数组,我们需要设计整型比较函数
//仿照qsort的第四个参数int (*compar)(const void*, const void*)我们设计整型比较函数
//例如:
int cmp_int(const void* p1, const void* p2)//整型比较函数
{
	//两个数据元素*p1与*p2比较
	//当*p1大于*p2,返回值大于0;当*p1等于*p2,返回值等于0;当*p1小于*p2,返回值小于0
	return *(int*)p1 - *(int*)p2;
}
//当我们需要比较字符型数组或者字符串,我们需要设计字符比较函数
//例如:
int cmp_int(const void* p1, const void* p2)//字符比较函数
{
	//字符串不能直接比较,需要用字符比较函数strcmp
	//strcmp的返回值为>0,=0或<0,分别对应p1>p2,p1=p2,p1<p2
	//字符串的比较方式是根据ASCII码来比较的,两个字符串从前往后,依次相比较字符
	//例如"abcd"和"abd"
	//a=a,b=b,相等就继续向后比较,而d>c,所以后者大于前者
	return strcmp((char*)p1, (char*)p2);
}

2.2 使用qsort排序结构体数据

当我们学会了如何设计比较函数时,我们也就差不多学会了qsort的使用了。前面提到,我们甚至可以用qsort来排序结构体变量,如何实现呢?
如下:

#include<stdio.h>
struct Stu //学⽣
{
	char name[20];//名字
	int age;//年龄
};
//假设按照年龄来⽐较
int cmp_stu_by_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int cmp_stu_by_name(const void* e1, const void* e2)
{
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
//按照年龄来排序
void test2()
{
	struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}
//按照名字来排序
void test3()
{
	struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main()
{
	test2();
	test3();
	return 0;
}

三、qsort函数的模拟实现

使⽤回调函数,模拟实现qsort(采⽤冒泡排序的⽅式)。
这里拓展一下空类型void的概念:void的字面意思是"无类型",有什么作用呢?我们一般有两个用途:

1. 对函数返回的限定; 2. 对函数参数的限定。

当我们使用void来限制函数返回值时,函数的返回值为空,即无需返回值;当我们使用void来限制函数的参数时,通常是以void*的形式(用void来创建变量没有实际意义),结合之前篇章中C语言修炼——还不会指针?一次讲明白!第一弹!!的第三部分《指针变量类型的意义》的知识,我们可以知道不同类型的指针变量,可以访问的字节数是不一样的,而使用void*来创建的指针变量,是一种泛型指针,即传参时可以接受任意类型的指针变量,但相应的由于这种包容性,泛型指针是没有访问的字节数的限制的,所以我们不能直接使用泛型指针,往往需要通过强制转换类型来使用它。

3.1 完整代码展示

#include <stdio.h>
int int_cmp(const void* p1, const void* p2)//整型比较函数
{
	return (*(int*)p1 - *(int*)p2);
}
void _swap(void* p1, void* p2, int size)//交换函数
{
	int i = 0;
	for (i = 0; i < size; i++)
	{
		//按照字节依次交换
		char tmp = *((char*)p1 + i);
		*((char*)p1 + i) = *((char*)p2 + i);
		*((char*)p2 + i) = tmp;
	}
}
void my_bubble_qsort(void* base, int num, int size, int(*compar)(const void*, const void*))
{
	for (int i = 0; i < num - 1; i++)
	{
		for (int 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);
			}
		}
	}
}
int main()
{
	int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
	bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

3.2 详解

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

这是我们一般实现冒泡排序的方法与思路。
对于一个整数数列9 8 7 6 5 4 3 2 1 0,就像十个萝卜十个坑,这里每个数字都有自己的位置,我们需要做的是依次比较相邻位置的数字,先是1位置与2位置,再是2位置与3位置,以此类推。例如数字9,这样排序下去先是9,8互换,再是9,7互换,直至9位于末尾,这就完成了第一趟排序,而第一趟排序可以确保最后一个位置的数字为最大值,这里有10个数字,所以我们需要排列9趟,每趟比较9次才能完成排序,这就是冒泡排序。
那么我们如何用冒泡排序的方法与思路来实现qsort函数呢?

void my_bubble_qsort(void* base, size_t num, size_t size, int (*compar)(const void*, const void*))
{
	for (int i = 0; i < num - 1; i++)
	{
		for (int j = 0; j < num - 1 - i; j++)
		{
			//比较判断...	
		}
	}
}

容易知道,对于两个for循环体,我们不需要做太大的变动,我们真正需要关注的是比较交换的部分。
但我们并不知道我们的my_bubble_qsort函数接收并比较的是什么类型的数据,所以我们可以利用回调函数的知识来灵活操控不同的比较函数来处理特定的比较问题。我们可以将之前的if语句改写:

if ( compar( (char*)base + sz * j, (char*)base + sz * (j + 1) ) )
{
	//交换...
}

当我们写出比较函数并传参给my_bubble_qsort函数后,compar这个函数指针会接收比较函数的地址,所以我们可以在if语句中使用函数指针直接调用我们传过去的比较函数。
这里的两个参数分别代表什么意思呢?
C语言修炼——还不会指针?一次讲明白!第一弹!!第一部分《内存和地址》中我们提到,地址的编排是以字节为单位的,所以不管我们传参传的是整型指针也好,浮点型指针也好,本质上形参接受的都是某个字节的地址。所以这里的两个参数(char*)base + sz * j(char*)base + sz * (j + 1)本质上就是相邻位置的两个元素各自占用的全部字节中的其中一个字节,当我们的比较函数compar的两个形参void* p1, void* p2分别接受了对应字节的地址后,我们只需要通过对p1p2强制类型转换就可以访问全部有效字节。

例如:
这里有两个相邻的整型元素,它们的大小size4,两个整型元素有8个字节,靠前元素的4个字节中的第一个字节的地址为base,容易知道base+size是靠后元素的4个字节中的第一个字节的地址,用这种方法我们可以不断向后找到所有元素的地址。
而强制类型转换就可以充当限定访问字节数的角色,因为一个整型元素有4个有效字节数,而我们访问一个整型元素也只需要访问4个字节就够了,所以我们仅仅拥有一个元素若干个字节中的第一个字节的地址是不够的,我们可以用元素对应的指针类型来强制转换void*类型指针,从而正确访问所有有效字节。

知道了上述知识,交换函数也就很容易知道如何书写了。由于数据类型的不确定,我们不能直接指定中间变量tmp的类型,但我们可以以字节为单位来依次交换,比如一个整型元素只需要交换4次就可以完成整个元素的交换。所以我们可以这样写:

void _swap(void* p1, void* p2, int size)//交换函数
{
	for (int i = 0; i < size; i++)
	{
		//按照字节依次交换
		char tmp = *((char*)p1 + i);
		*((char*)p1 + i) = *((char*)p2 + i);
		*((char*)p2 + i) = tmp;
	}
}

四、sizeof和strlen的对比

4.1 sizeof

sizeof本质上是C语言中的一个操作符,sizeof计算的是变量所占内存空间的⼤⼩,单位是字节,如果操作数是类型的话,计算的是使⽤类型创建的变量所占内存空间的⼤⼩。sizeof只关注占⽤内存空间的⼤⼩,不在乎内存中存放什么数据。

//比如:
#include<stdio.h>
int main()
{
	int a = 0;
	printf("%d\n", sizeof(a));  //结果:4
	printf("%d\n", sizeof a);   //结果:4
	printf("%d\n", sizeof(int));//结果:4
	return 0;
}

4.2 strlen

strlen是C语言库函数,功能是求字符串长度,函数原型如下:
在这里插入图片描述
统计的是从strlen函数的参数str这个地址开始向后,\0之前字符串中字符的个数。strlen函数会⼀直向后找\0字符,直到找到为⽌,所以可能存在越界查找。

#include <stdio.h>
int main()
{
	char arr1[3] = {'a', 'b', 'c'};
 	char arr2[] = "abc";
 	printf("%d\n", strlen(arr1));//结果未知
 	printf("%d\n", strlen(arr2));//结果:3
	printf("%d\n", sizeof(arr1));//结果:3
	printf("%d\n", sizeof(arr2));//结果:4
 	return 0;
}

4.3 sizeof与strlen的对比

在这里插入图片描述

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

在这一部分,我们通过一些题目来巩固我们对于地址,指针,数组名等的了解。

5.1 一维数组

int a[] = { 1, 2, 3, 4 };
printf("%d\n",sizeof(a));//此处a代表整个数组,求整个数组的大小:4*4
printf("%d\n",sizeof(a+0));//此处a代表首元素地址,加0,结果为地址:4/8
printf("%d\n",sizeof(*a));//此处a代表首元素地址,解引用,结果为首元素:4
printf("%d\n",sizeof(a+1));//此处a代表首元素地址,加1,结果为a[1]的地址:4/8
printf("%d\n",sizeof(a[1]));//a[1]相当于首元素地址加1并解引用,即*(a+1),结果:4
printf("%d\n",sizeof(&a));//此处a代表整个数组,取出整个数组的地址,地址的大小为:4/8
printf("%d\n",sizeof(*&a));//此处a代表整个数组,取出整个数组的地址解引用,结果为数组的大小:4*4
printf("%d\n",sizeof(&a+1));//此处a代表整个数组的地址,加1,结果为数组后一个字节的地址,仍为地址:4/8
printf("%d\n",sizeof(&a[0]));//取出a[0]的地址,仍为地址:4/8
printf("%d\n",sizeof(&a[0]+1));//取出a[0]的地址,加1,a[1]的地址:4/8

5.2 字符数组

代码1:

char arr[] = {'a','b','c','d','e','f'};//无'\0'
printf("%d\n", sizeof(arr));//此处arr代表整个数组,计算整个数组所占内存空间的大小:6*1
printf("%d\n", sizeof(arr+0));//此处arr代表数组首元素的地址,加0,仍为地址:4/8
printf("%d\n", sizeof(*arr));//此处arr代表数组首元素的地址,解引用,结果为首元素的大小:1
printf("%d\n", sizeof(arr[1]));//arr[1]相当于数组首元素地址加1并解引用,结果为:1
printf("%d\n", sizeof(&arr));//此处arr代表整个数组,取出整个数组的地址,地址的大小为:4/8
printf("%d\n", sizeof(&arr+1));//此处arr代表整个数组,取出整个数组的地址加1,结果为数组后一个字节的地址:4/8
printf("%d\n", sizeof(&arr[0]+1));//取出arr[0]的地址加1,结果为arr[1]的地址:4/8

代码2:

char arr[] = {'a','b','c','d','e','f'};//无'\0'
printf("%d\n", strlen(arr));//从arr数组首元素的地址往后找,无'\0',结果为随机值
printf("%d\n", strlen(arr+0));//从首元素地址往后查找,无'\0',结果为随机值
printf("%d\n", strlen(*arr));//对arr数组首元素的地址解引用,得到字符'a',字符'a'的本质是a的ASCII码值97,strlen接受的是一个地址,97被默认当成地址,无法访问
printf("%d\n", strlen(arr[1]));//arr[1]为字符'b',字符'b'的本质是b的ASCII码值98,98被默认当成地址,无法访问
printf("%d\n", strlen(&arr));//取出整个数组的地址,在数值上等于数组首元素的地址,所以仍是从首元素向后查找,无'\0',结果为随机值
printf("%d\n", strlen(&arr+1));//取出整个数组的地址,加1,得到整个数组后一个字节的地址,向后查找,仍无'\0',结果为随机值
printf("%d\n", strlen(&arr[0]+1));//取出arr[0]的地址加1,得到arr[1]的地址,无'\0',结果为随机值

代码3:

char arr[] = "abcdef";//加上'\0'实际为7个字符
printf("%d\n", sizeof(arr));//此处arr代表整个数组,大小为:7*1
printf("%d\n", sizeof(arr + 0));//此处arr代表数组首元素的地址,加0,仍为地址:4/8
printf("%d\n", sizeof(*arr));//此处arr代表数组首元素的地址,解引用得到数组首元素:1
printf("%d\n", sizeof(arr[1]));//数组中下标为1的元素的大小,结果为:1
printf("%d\n", sizeof(&arr));//此处arr代表整个数组,取出整个数组的地址,仍为地址:4/8
printf("%d\n", sizeof(&arr + 1));//此处arr代表整个数组的地址,加1,仍为地址:4/8
printf("%d\n", sizeof(&arr[0] + 1));//取出arr[0]的地址,加1,相当于arr[1]的地址:4/8

代码4:

char arr[] = "abcdef";//加上'\0'实际为7个字符
printf("%d\n", strlen(arr));//此处arr代表数组首元素的地址,向后查找,遇到'\0'停止,结果为:6
printf("%d\n", strlen(arr + 0));//此处arr代表数组首元素的地址,加0,结果仍为首元素的地址,向后查找:6
printf("%d\n", strlen(*arr));//对数组首元素的地址解引用,得到字符'a',本质为字符'a'的ASCII值97,97被当成地址,无法访问
printf("%d\n", strlen(arr[1]));//数组中下标为1的元素,即字符'b',本质为98,98被当成地址,无法访问
printf("%d\n", strlen(&arr));//此处arr代表整个数组,取出整个数组的地址,数值上仍为首元素的地址,结果:6
printf("%d\n", strlen(&arr + 1));//此处arr代表整个数组,取出整个数组的地址加1,结果为整个数组后一个字节的地址,其后有无'\0'无法确定,结果为随机值
printf("%d\n", strlen(&arr[0] + 1));//取出arr[0]的地址加1,结果为arr[1]的地址,结果:5

代码5:

char* p = "abcdef";//将字符串"abcdef"的首元素的地址赋给字符型指针变量p,整个字符串算上'\0'有7个字符
printf("%d\n", sizeof(p));//此处p为指针变量,指针=地址,大小为:4/8
printf("%d\n", sizeof(p + 1));//此处p为指针变量,加1,相当于字符串元素'b'的地址,指针=地址,大小为:4/8
printf("%d\n", sizeof(*p));//p存储的是字符串首元素的地址,对p解引用,得到的是字符串"abcdef",计算结果为字符'a'的大小:1
printf("%d\n", sizeof(p[0]));//p[0]相当于*(p+0),得到的是字符'a',计算的是一个字符的大小:1
printf("%d\n", sizeof(&p));//取出指针变量p的地址,结果为二级指针,仍为地址:4/8
printf("%d\n", sizeof(&p + 1));//取出指针变量p的地址,加1,相当于二级指针加1,结果为该指针变量所占内存空间后一个字节的地址:4/8(需注意分辨存储指针变量的地址和指针变量存储的地址)
printf("%d\n", sizeof(&p[0] + 1));//取出p[0],即字符'a'的地址并加1,得到字符'b'的地址,结果仍为地址:4/8

代码6:

char* p = "abcdef";
printf("%d\n", strlen(p));//p存储的是字符串首元素的地址,向后查找结果为:6
printf("%d\n", strlen(p + 1));//p加1,相当于从第二个字符开始查找,结果为:5
printf("%d\n", strlen(*p));//*p得到字符串的首元素,即字符'a',本质为97,被当成地址传参,无法访问
printf("%d\n", strlen(p[0]));//p[0]相当于*(p+0),得到字符'a',无法访问
printf("%d\n", strlen(&p));//取出指针变量p的地址,无法确定'\0'的存在,结果为随机值
printf("%d\n", strlen(&p + 1));//指针变量p的地址加1,无法确定'\0'的存在,结果为随机值
printf("%d\n", strlen(&p[0] + 1));//取出p[0]的地址加1,即p[1]的地址,从第二个字符开始查找,结果为:5

5.3 二维数组

//二维数组本质上是以一维数组作元素,所以二维数组首元素的地址实际上是整个一维数组的地址,注意是整个
int a[3][4] = { 0 };//一个3行4列的二维数组,本质上是有3个一维数组a[4]作元素的数组
printf("%d\n", sizeof(a));//此处a代表整个二维数组,计算的是该二维数组在内存空间中所占大小:3*4*4
printf("%d\n", sizeof(a[0][0]));//相当于*(*(a+0)+0),得到二维数组首元素,这个一维数组的首元素的大小,结果为一个整型:4
printf("%d\n", sizeof(a[0]));//相当于*(a+0),得到二维数组的首元素,结果为整个一维数组a[4]的大小:4*4
printf("%d\n", sizeof(a[0] + 1));//相当于*(a+0)+1,得到二维数组下标为1的元素的地址,地址的大小:4/8
printf("%d\n", sizeof(*(a[0] + 1)));//相当于a[0][1],计算的是一个整型元素的大小:4
printf("%d\n", sizeof(a + 1));//此处a相当于二维数组首元素的地址,加1,相当于二维数组下标为1的元素的地址,地址的大小为:4/8
printf("%d\n", sizeof(*(a + 1)));//相当于a[1],得到一个一维数组,计算结果为:4*4
printf("%d\n", sizeof(&a[0] + 1));//相当于a[0]的地址加1,得到二维数组第二个元素的地址,地址的大小:4/8
printf("%d\n", sizeof(*(&a[0] + 1)));//相当于*(a+1),得到二维数组第二个一维数组元素的地址并解引用,结果为一个一维数组的大小:4*4
printf("%d\n", sizeof(*a));//相当于*(a+0),结果为一个一维数组的大小:4*4
printf("%d\n", sizeof(a[3]));//相当于*(a+3),结果为一个一维数组的大小:4*4(无关是否存在,只关注该类型可能需要占多大内存空间)

数组名的意义:

  1. sizeof(数组名),这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩
  2. &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址
  3. 除此之外所有的数组名都表⽰⾸元素的地址

六、指针运算笔试题

6.1 题目1

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

对于

int *ptr = (int *)(&a + 1);
//取出整个a数组的地址加1
//结果为存储a数组的内存空间后一字节的地址
//强制类型转换为int*类型,再赋值给int*类型的指针变量

对于

printf( "%d,%d", *(a + 1), *(ptr - 1));
//此处a代表数组首元素的地址,加1,得到第二个元素的地址
//解引用得到第二个元素2
//此处ptr-1,减去的是一个int*类型的字节空间
//所以ptr应该指向5所在的空间,解引用得到元素5

6.2 题目2

//在X86环境下
//假设结构体的⼤⼩是20个字节
//程序输出的结果是啥?
#include <stdio.h>
struct Test
{
	int Num;
	char* pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p = (struct Test*)0x100000;
//0x100000是十六进制整数,需要强制类型转换为指针类型赋值给指针变量p
int main()
{
	printf("%p\n", p + 0x1);
	//十六进制数0x1即十进制数1,该结构体的大小为20个字节,则该结构体类型的指针加1等于加20个字节,0x100000加20变成0x100014
	printf("%p\n", (unsigned long)p + 0x1);
	//p被强制类型转换为整型,整数直接相加,0x100000+0x1=0x100001
	printf("%p\n", (unsigned int*)p + 0x1);
	//p被强制类型转换为整型指针变量,加1相当于加4个字节,0x100000加4个字节为0x100004
	return 0;
}

6.3 题目3

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

这一道题非常简单,但涉及到了两个关键知识点:逗号表达式二维数组
我们来分析一下int a[3][2] = { (0, 1), (2, 3), (4, 5) };
这3个括号分别代表着什么?
这条语句创建的二维数组又有什么不同?
一般情况下我们是这样子创建二维数组的:int a[3][2] = { {0, 1}, {2, 3}, {4, 5} };。我们得到的3行2列的二维数组是这样子:
在这里插入图片描述
这里我们用的是{}来表示二维数组的一个元素,而不是(),这是因为二维数组的元素是一维数组,和我们创建一维数组时一样,我们都应该使用{}。所以这里的三个括号我们可以知道肯定不是代表3个一维数组。事实上,这3个括号分别是二维数组首元素一维数组a[0]的两个元素,以及二维数组第二个元素一维数组a[1]的首元素。
在这里插入图片描述
那么这三个括号究竟是什么呢?这里就涉及逗号表达式了:逗号表达式是C语言提供一种特殊的运算符——逗号运算符。它的优先级别最低,它将两个及其以上的式子联接起来,从左往右逐个计算表达式,整个表达式的值为最后一个表达式的值。所以我们就明白了,括号里面的逗号表达式的值原来是最右边的表达式的值,也就是说a[0][0],a[0][1],a[1][0]的值应该分别是1,3,5
所以结果应该是:

int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int* p;
p = a[0];//将二维数组首元素的地址赋值给p
printf("%d", p[0]);//p[0]相当于a[0][0],所以最终结果为1

6.4 题目4

//假设环境是x86环境,程序输出的结果是啥?
#include <stdio.h>
int main()
{
	int a[5][5];//创建一个五行五列的二维数组
	int(*p)[4];//创建一个数组指针,且该指针指向的数组有4个整型元素
	p = a;//a为二维数组首元素的地址,即&a[0],类型为int (*)[5]
	//a赋值给int (*)[4]类型的指针变量p,会报错但不影响程序运行
	printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);//本质上是指针-指针
	//&p[4][2]相当于&*(*(p+4)+2),&a[4][2]相当于&*(*(a+4)+2)
	return 0;
}

创建二维数组a[5][5],其在内存中的情况如下:
在这里插入图片描述
我们知道&p[4][2]相当于&*(*(p+4)+2),&a[4][2]相当于&*(*(a+4)+2),所以最终这分别是两个地址,但它们分别指向哪里呢?
在这里插入图片描述
我们知道指针变量的类型决定了我们使用该指针变量可以访问的空间大小,也决定了我们进行指针运算时加1可以跳过的字节数,这里a的类型是int [5][5]p的类型是int (*)[4],所以*(a+4)跳过4个大小为int [5]的一维数组,*(*(a+4)+2)跳过2个int的大小的字节数;*(p+4)跳过4个大小为int [4]的一维数组,*(*(p+4)+2)跳过2个int的大小。
前面讲解指针的文章说过,指针-指针计算结果的绝对值(地址有低地址与高地址)为两地址间的元素个数,所以&p[4][2] - &a[4][2]的结果为-4,数组的存储是从低地址到高地址,所以前者为低地址,后者为高地址,结果为负数。
虽然我们渡过千难万险求出了&p[4][2] - &a[4][2]的值,但需要注意不同格式符%p%d最终输出的结果也应该不同。

  1. %d以十进制输出有符号的整型变量
  2. %p以十六进制完整打印地址

-4在x86环境下的二进制形式的原码为1000 0000 0000 0000 0000 0000 0000 0100,补码为1111 1111 1111 1111 1111 1111 1111 1100%d输出仍为-4,但用%p补码被当作地址,由于地址没有正负,1不被认作符号位,最终补码直接转换为十六进制数输出,结果为FFFFFFFC
在这里插入图片描述

6.5 题目5

#include <stdio.h>
int main()
{
	int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int* ptr1 = (int*)(&aa + 1);//取出整个二维数组的地址,加1,跳过一个二维数组的大小,强制类型转换为int*赋值给ptr1
	int* ptr2 = (int*)(*(aa + 1));//aa为二维数组首元素的地址。即第一行的地址,加1,跳过一个一维数组的大小,指向第二行一维数组,解引用得到aa[1]
	printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));//ptr1-1,跳过一个int的大小,指向元素10;ptr2-1,跳过一个int的大小,指向5
	//最终结果为10,5
	return 0;
}

6.6 题目6

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

这题小有难度,我们逐句画图分析来看一看:
在这里插入图片描述
之前的文章我们已经知道了指针数组这个概念,这里char* a[] = { "work","at","alibaba" };很容易知道其实是一个字符指针型数组,元素为3个字符型指针变量,分别存储着"work""at""alibaba"三个字符串的首元素的地址。
char** pa是一个字符型二级指针,赋值a指向的是该字符指针数组第一个元素——指针变量a[0]的地址,很清楚,用二级指针存储一级指针的地址没毛病。
pa++,本来指向apa,加1跳过一个一级字符指针变量,指向a+1,解引用得到a+1指向的空间的地址,即"at"的首元素的地址。
%s从字符串的首地址开始向后打印字符,直到遇见'\0',所以最终结果为at

6.7 题目7

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

当我们创建好所有变量,内存中的存储情况如下:
在这里插入图片描述

首先来看第一句printf("%s\n", **++cpp)++cppcpp加1指向cp+1
在这里插入图片描述
第一次*++cpp解引用,相当于cp[1],结果得到c+2;第二次*c+2解引用,相当于c[2],结果得到"POINT",故第一个printf打印POINT

再来看第二句printf("%s\n", * -- * ++cpp + 3)++cppcpp继续加1向后跳并指向cp+2
在这里插入图片描述
+优先级最低,先看*。对此时的cpp解引用,相当于cp[2],得到指针变量c+1--(c+1)得到c,解引用相当于c[0],得到字符串"ENTER"的首元素的地址,此时计算+3,即向后跳3个字符,指向E所在地址。所以第二个printf最终的打印结果为ER

继续看第三句printf("%s\n", *cpp[-2] + 3)cpp[-2],由于之前的++运算,所以此处的cpp指向的是cp+2cpp[-2]*(cpp-2),相当于cp[0],结果得到指针变量c+3。再对cpp[-2]解引用,相当于c[3],得到字符串"FIRST"的首元素的地址,再+3则跳过3个字符,指向S所在的地址。所以第三个printf的打印结果为ST

最后来看第四句printf("%s\n", cpp[-1][-1] + 1)**:**由于之前的cpp[-2]并不改变cpp的指向,所以cpp仍然指向cp+2cpp[-1][-1]相当于*(*(cpp-1)-1)*(cpp-1),相当于cp[1],结果得到c+2*( (c+2)-1 ),相当于*(c+1)c[1],结果得到字符串"NEW"的首元素的地址,此时+1,跳过一个字符,最终指向的是E所在地址。所以第四个printf的打印结果为EW

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

David猪大卫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值