指针进阶两万字总结(深入理解字符指针、指针数组、数组指针、数组及指针传参、函数指针、函数指针数组、回调函数等,指针笔试题总结)

一、字符指针

字符指针即指向字符类型的指针char*;
我们知道C语言中字符串有两种保存方式:数组保存指针保存,这两种方式从本质上来说有着很大的区别。
如图所示:数组保存时,在栈上开辟空间,字符串可以被访问修改;指针保存时,字符串保存在常量区,只可被访问,不能修改。
在这里插入图片描述
看下面这个程序:

#include <stdio.h>
int main()
{
    
    
    char str1[] = "hello world!";
    char str2[] = "hello world!";
    char *str3 =  "hello world!";
    char *str4 =  "hello world!";
    if(str1 ==str2)
 printf("str1 and str2 are same\n");
    else
 printf("str1 and str2 are not same\n");
       
    if(str3 ==str4)
 printf("str3 and str4 are same\n");
    else
 printf("str3 and str4 are not same\n");
       
    return 0;
}

运行结果:
在这里插入图片描述
str1和str2代表的是数组首元素的地址,而str1和str2是两个不同的数组,所以首元素地址不相同。str3和str4是两个指针,都指向保存在字符常量区的同一个字符串,所以地址值相同。

二、数组指针和指针数组

(一)区分

初学时容易混淆,所以先区分这两个概念。

1.从语言描述上

根据汉语定语修饰词在前,主语在后的习惯,“数组指针”中的“数组”是定语,“指针”是主语,所以数组指针是指针,而对于指针,我们第一反应就是这个指针指向什么,显然,数组指针是指向数组的。“指针数组”刚好相反,所以指针数组是数组,且存放的内容是指针。

2.从书写形式

看操作符的优先级
在这里插入图片描述
由于 [ ] 的优先级比*高,所以p1先和 [ ] 结合,必然是数组;p2有圆括号,所以先和*结合,构成指针。

(二)指针数组

1.定义

指针数组是一个存放指针的数组。如:

int *p[10];//一个数组,存放的内容是整型指针
int **p2[10];//存放二级整型指针的数组

2.指针数组的使用

整型指针数组

在使用整型数组指针时,要注意,不能直接将整型变量赋值给指针数组,因为指针数组的元素类型时int*,而赋予的变量类型是int型,类型不匹配。
如图:定义了一个整型指针数组并初始化,编译以后会发现程序有告警。
在这里插入图片描述

字符指针数组

字符指针数组可以直接赋值:

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
#include<windows.h>

int main()
{
    
    
	char* arr[] = {
    
     "I","am ","gonna"," make"," the",\
		             " world"," a", "better"," place" };
	printf("%s\n",arr[5]);//打印数组的第五个元素
	system("pause");
	return 0;
}

程序运行结果:
在这里插入图片描述

在这里,一个容易犯的错误就是打印输出时加上*解引用,导致程序崩溃:
在这里插入图片描述
arr[5]本质上是一个指向指向字符串"world"的首元素的指针,对其解引用得到的是字符’w’,而如果以s%的形式打印输入,便会引起程序崩溃。

补充:数组下标访问和指针访问

定义一个数组:

int num=0;
int arr[5][5]={
    
    };

下面这两种方式访问的是同一个元素:

num=arr[3][3];
num=*(*(arr+3)+3);

数组访问的方式很容易理解,下面对指针访问的方式进行分析:
在这里插入图片描述
arr是数组名,在这里代表首元素地址,即arr[0]的地址,arr[0]的地址+3表示数组第四个元素的地址,即a[3],而C语言中,a[3]表示一维数组的数组名,根据前面的结论,数组名即首元素地址,所以a[3]表示a[3][0] 的地址,&a[3][0]+3即为a[3][3]的地址,再解引用便是a[3][3]。

(三)数组指针

1.定义

int(*p)[10];//因为[]优先级比*高,所以要加括号

需要注意的是,数组的类型不仅指元素类型,还包括数组元素的个数,所以数组指针的类型也包括数组元素的个数,这里p指针指向的是一有10个整型元素的数组。

2.数组名与&数组名的区别

定义一个指针数组arr,分给打印arr和&arr:

int*arr[10]={
    
    0};
printf("%p\n ",arr);
printf("%p\n ",&arr);

在这里插入图片描述

打印结果相同,但是他们真的完全按一样吗?
通过下面这程序可以深刻理解数字名与&数组名的区别:

#include <stdio.h>
#include<Windows.h>
int main()
{
    
    
    int arr[10] = {
    
     0 };
    printf("arr = %p\n", arr);
    printf("&arr= %p\n", &arr);
    printf("arr+1 = %p\n", arr + 1);
    printf("&arr+1= %p\n", &arr + 1);
    system("pause");
    return 0;
}

运行结果:
在这里插入图片描述
可见,arr+1是首元素地址加1,加上其所指向内容的大小,即4个字节;而&arr+1加上的是40个字节,是整个数组的大小。
对程序稍作更改,数组第一个和第二个元素分别赋值为1和2,查看内存,得到下面的结果:
在这里插入图片描述
很明显,对arr解引用得到的是数组第一个元素的值,所以arr代表的是数组首元素的地址;而对&arr解引用得到的是一个数组,所以从&arr是整个数组的地址,从本质上来说,&arr是数组指针类型。
结论:数组名在&后面和单组在sizeof中出现时表示整个数组,其余所有情况都表示数组首元素的地址。

3.数组指针的使用

数组指针主要在函数传参时使用,用于接收二维数组。

三、数组和指针的传参问题

一维数组传参

一维数组作函数实参时,形参可以是一维数组或者指针。
定义一个一维数组,让其作实参:

int arr[10];
Sort(arr);//调用sort函数

参数arr的类型是一个指向整型的指针,形参可以是下面任意一个:

void Sort(int *arr);//整型指针
void Sort(int arr[]);//整型数组

二维和多维数组

多维数组名作为函数参数传递的方式本质和一维数组相同——传递一个指向数组第一个元素的指针。但是,区别就是,多维数组的每个元素本身是另外一个数组,编译器需要知道它的维数,以便于为函数形参的下标表达式求值。
二维数组做实参,形参可以是二维数组或一个数组指针,且这个数组指针必须是指向二维数组的元素类型的指针。如:

int arr[3][10];//定义一个二维数组
Sort(arr);//调用Sort函数,arr作形参

形参可以是以下类型:

void Sort(int (*arr)[10]);//定义一个指向10个整型变量的数组指针
void Sort(int arr[][10]);//定义一个数组(二维),其元素类型是有10个整型变量的一维数组

这里的关键是编译器必须知道第2个及以后各维的长度才能对各下标进行求值,因此形参必须声明这些维度的长度。
比如,下面的这一组不能作为形参接收二维数组:

void Sort(int **arr);//定义了一个二级指针,即指向整型指针的指针
void Sort(int *arr[10])//指针数组

这两个都和实参arr的类型不匹配,所以不能作形参。
多维数组传参和二维数组原理相同,只有第一个维度可以省略,二维及以上都不能省,用数组指针时,类型也必须是指向数组元素类型的指针。

一级指针传参

一级指针作实参,形参可以是一维数组或者一级指针。

二级指针传参

二级指针作传参,形参可以是二级指针、指针数组。

传参规则总结

  • 当二级指针作为函数形参时,能作为函数实参的是二级指针,指针数组,一级指针的地址
  • 当数组指针作为函数形参时,能作为函数实参的是二维数组,数组指针
  • 当二维数组作为函数形参时,能作为函数实参的是二维数组,数组指针
  • 当指针数组作为函数形参时,能作为函数实参的是指针数组,二级指针,一级指针的地址

对于二维数组和数组指针,有一个共同点,在定义时二维数组必须定义第二维的大小,而数组指针也必须指定其所指向的数组的大小,所以他们可以互相作为参数。
而对于指针数组,其数组名本质上是一个指向数组第一个元素的指针,即二级指针,类型匹配,所以可以作为形参接收二级指针,并被二级指针接收。

四、函数指针及其应用

(一)函数指针

定义

指向函数地址的指针

int fun();//函数声明
int (*pf)()=&fun//定义一个函数指针,指向fun函数

上面程序中,便是函数指针。定义函数指针时需注意优先级,变量名后面的()优先级比*高,所以要加()。
另外,函数名在使用时编译器会自动将其转换为函数指针,所以单目操作符&可以省略,即&fun和fun等价。函数名实际存放的是函数入口的地址。

使用

对函数指针定义并初始化以后,便可以使用,看下面三种方式:

int num=0;
num=fun(10);//1
num=(*pf)(10);//2
num=pf(10);//3

第一种:如上所述,函数名存放的是函数入口的地址,所以使用函数名调用函数的具体执行过程是,函数名fun首先被转换成一个函数指针,这个指针指向函数在内存中的存放位置,然后执行指针所指向位置的代码。
第二种:对pf进行解引用操作,指向函数名,在执行与第一种相同的步骤。
第三种:与第二种相同,操作符*在实际使用中可以被省略。

代码解析

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

结论:这是一个从0地址处开始的函数调用
解析:

在这里插入图片描述
这里需要注意的是,0是一个常数,可以被当作地址来看待,也可以被强制转换为其他类型的指针,比如函数指针,然后只需要从里向外层层分解就可以理解这个语句的含义。

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

结论:这是一个返回类型是函数指针的函数声明,该函数的参数是一个int类型和一个函数指针
解析:
在这里插入图片描述
这里要理解的关键是signal函数的返回值类型是一个函数指针,可以把绿色方框内整体看作一个函数如:signal(),忽略其形参。而该函数的返回值是一个函数指针:void(*)int,便可以得到:void (*signal() ) (int),最后将函数的形参填入即可。

(二)函数指针数组

定义

把函数的地址存放到数组中,即为函数指针数组。如:

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

arr是一个函数指针数组,数组内存放的是指向返回值为int,形参也为int的函数指针。

应用:转移表

使用函数指针数组设计一个计算器,程序如下:

#include<stdio.h>
#include<windows.h>
#include<stdlib.h>//exit函数头文件

void Menu()//菜单
{
    
    
	printf("#####################################\n");
	printf("######    1.Add        2.Sub   ######\n");
	printf("######    3.Mul        4.Div   ######\n");
	printf("######           0.Exit        ######\n");
	printf("#####################################\n");
}
int MyExit(int x,int y)//退出函数
{
    
    
	exit(0);//exit函数需要有参数,可以设为0
}

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)
{
    
    
	//除法要先判断被除数不能为0
	if (y == 0) {
    
    
		printf("Warning:Div zero!\n");
		return -1;
	}
	return x / y;
}

int main()
{
    
    
	int x = 0;
	int y = 0;
	int select = 0;//接收选项
	int ret = 0;//运算结果
	int (*p[])(int, int) = {
    
     MyExit,Add,Sub,Mul,Div };//转移表
	char* operators = "+-*/";//将符号保存在字符串中
	while (1)
	{
    
    
		Menu();
		printf("Please select:");
		scanf_s("%d", &select);
		if (select >= 1 && select <= 4) {
    
    
			printf("Please enter the number:");
			scanf_s("%d %d", &x, &y);
			ret = p[select](x, y);//通过函数指针数组调用函数
			printf("%d %c %d=%d \n", x, operators[select - 1], y, ret);//下标方式访问operators
		}
		else if (select == 0) {
    
    
			printf("Bye-bye!\n");
				p[0](0, 0);//0为占位符,没有实际意义
		}
		else
			printf("Enter error!\n");
	}
	
	system("pause");
	return 0;
}

这里之所以能使用函数指针数组,是因为四则运算的每个函数类型相似,都有两个类型相同的参数,返回值类型也相同。这样用函数指针数组调用的方式,减少了很多重复代码。

(三)指向函数指针数组的指针

指向函数指针数组的指针是一个指针,它指向的是一个数组,这个数组保存的是函数指针。

int fun(int x)
{
    
    
return x;
}
int (*pfun)(int)=&test;//函数指针,&可以不写
int (*pfun_arr[])(int);//函数指针数组
int (*(*p_pfun_arr)[])(int)=&(*pfun[])(int);//指向函数指针数组的指针

▲(四)回调函数

1. 概念

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

2.应用——qsort函数

qsort是C语言的库函数,包含在<stdlib.h>头文件下,可以对任意数据类型进行排序
在这里插入图片描述
在这里插入图片描述

这个函数共有4个参数:

  • base:void*类型,指向要排序的数据的起始地址处。
  • num:unsigned int类型,要进行比较的数据个数。
  • size:数据的大小。
  • compar:函数指针类型,它指向一个用于比较指定类型的两个数据大小的函数,这个函数由使用者自己写出,它传入的参数是进行比较的两个数据的地址,返回值为正数、负数、或者0,返回值决定排序按升序还是降序进行。

int型数据qsort排序

#include<stdio.h>
#include<Windows.h>
#include<assert.h>
int int_compare(const void* x, const void* y)
{
    
    
	assert(x);//判断指针合法性
	assert(y);
	const int* x_ = (const int*)x;//将函数参数强转成int*类型并赋值给x_
	const int* y_ = (const int*)y;
	if (*x_ > *y_) {
    
    
		return 1;
	}
	else if (*x_ == *y_) {
    
    
		return 0;
	}
	else {
    
    
		return -1;
	}
}

int main()
{
    
    
	int arr[] = {
    
     10,24,98,19,2,8 };
	int num = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, num, sizeof(int), int_compare);

	for (int i = 0; i < num; i++) {
    
    
		printf("%d ", arr[i]);
	}
	system("pause");
	return 0;
}

double型qsort排序

double类型的数据比较大小,原理与int相同,区别就是compare函数的写法,有一点需要注意,double型数据不能用‘==’比较。

#include<stdio.h>
#include<Windows.h>
#include<assert.h>
int double_compare(const void* x, const void* y)
{
    
    
	assert(x);//判断指针合法性
	assert(y);
	const double* x_ = (const double*)x;//将函数参数强转成double*类型并赋值给x_
	const double* y_ = (const double*)y;
	if (*x_ > *y_) {
    
    
		return 1;
	}
	else if (*x_< *y_) {
    
    
		return -1;
	}
	else {
    
    
		return 0;
	}
}

void Print(double arr[],int num)
{
    
    
	for (int i = 0; i < num; i++) {
    
    
		printf("%.3f ", arr[i]);
	}
	printf("\n");
}
int main()
{
    
    
	double arr[] = {
    
     9.84,6.200,6.52,4.21,48.25,58.666,98593.55 };
	int num = sizeof(arr) / sizeof(arr[0]);
	Print(arr, num);
	qsort(arr, num, sizeof(double), double_compare);
	Print(arr, num);
	system("pause");
	return 0;
}

字符串使用qsort排序

字符串比较大小的规则是:从起始位置开始进行字符的ASCII值比较,遇到第一个不相同的字符,ASCII值大的字符串大。

#include<stdio.h>
#include<Windows.h>
#include<assert.h>
#include<string.h>
int String_compare(const void* x, const void* y)
{
    
    
	assert(x);//判断指针合法性
	assert(y);

	//因为函数的参数是进行比较的变量的地址而此处要比较的\
	  两个变量是char*类型,所以这里要用二级指针char**
	const char ** x_ = (const char**)x;
	const char ** y_ = (const char**)y;

	//strcmp的参数是字符串起始地址,所以此处要对*x_和*y_进行解引用
	return strcmp(*x_, *y_);
}

int main()
{
    
    
	//定义一个指针数组,数组元素指向要进行比较的字符串
	char *arr[] = {
    
    
	"abcd1234",
	"alkjgllkgj",
	"sdagfsagf",
	"agsafidfk",
	"sagjsdakjd"
	};
	int num = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, num, sizeof(char*), String_compare);

	for (int i = 0; i < num; i++) {
    
    
		printf("%s\n", arr[i]);
	}
	system("pause");
	return 0;
}

3.qsort函数模拟

用冒泡排序法模拟实现qsort函数

#include<stdio.h>
#include<Windows.h>
#include<assert.h>
#include<string.h>
int String_compare(const void* x, const void* y)
{
    
    
	assert(x);//判断指针合法性
	assert(y);

	//因为函数的参数是进行比较的变量的地址而此处要比较的\
	  两个变量是char*类型,所以这里要用二级指针char**
	const char** x_ = (const char**)x;
	const char** y_ = (const char**)y;

	//strcmp的参数是字符串起始地址,所以此处要对*x_和*y_进行解引用
	return strcmp(*x_, *y_);
}

void Swap(void* x, void* y,size_t size)
{
    
    
	char* x_ = (char*)x;
	char* y_ = (char*)y;

	//逐比特位交换两个变量,这种方式可以交换任意类型变量的值
	for (size_t j=0; j < size; j++) {
    
    
		*x_ ^= *y_;
		*y_ ^= *x_;
		*x_ ^= *y_;
		x_++, y_++;
	}
}

void MyQsort(void* base, size_t num, size_t sz, int(*compare)(const void* x, const void* y))
{
    
    
	assert(base);
	assert(compare);
	for (size_t i = 0; i < num-1; i++) {
    
    
		int flag = 1;
		for (size_t j = 0; j < num - i - 1; j++) {
    
    

			//因为要排序的数据类型是不确定的,所以用char*来指向
			//通过起始地址+元素大小*所经历元素个数的方式,可以确定数据的地址\
			将数据地址传递给compare指向的函数,确定类型后可以比较两个数据的大小
			if (compare((char*)base + j * sz, (char*)base + (j + 1) * sz) > 0) {
    
    
				Swap((char*)base + j * sz, (char*)base + (j + 1) * sz, sz);
				flag = 0;
			}
		}
		if (flag)
		{
    
    
			break;
		}
	}
}

int main()
{
    
    
	//定义一个指针数组,数组元素指向要进行比较的字符串
	char* arr[] = {
    
    
	"nbcd1234",
	"jlkjgllkgj",
	"xdagfsagf",
	"ygsafidfk",
	"xagjsdakjd"
	};
	int num = sizeof(arr) / sizeof(arr[0]);
	MyQsort(arr, num, sizeof(char*), String_compare);//自定义函数MyQsort

	for (int i = 0; i < num; i++) {
    
    
		printf("%s\n", arr[i]);
	}
	system("pause");
	return 0;
}

程序的内核是一个冒泡排序算法,而由于数据类型不能确定,与普通冒泡排序略有区别区别,首先,无法直接比较两个数据的大小,所以函数的形参中有个函数指针,在该函数中调用另一个函数,即回调函数,用于比较某一确定的数据类型的值的大小;其次,确定数据在内存中的地址方式也不一样,将起始地址强转成char*类型,加上类型大小乘以跨度确定地址;最后,交换两个变量的值时,逐比特位交换内容。

在这里插入图片描述

五、试题解析

(一)指针和数组试题

第一组 整型数组

ps:注释中“下一个数组的地址”这样的描述不准确,只是为了便于理解,其实际指的是数组最后一个元素下一个位置的地址,是一个指针数组,其指向与已经定义的数组类型相同。

	//一维数组
	int a[] = {
    
     1,2,3,4 };
	printf("%d\n", sizeof(a));//整个数组大小,16
	printf("%d\n", sizeof(a + 0));//数组名不是单独出现,首元素地址,4
	printf("%d\n", sizeof(*a));//首元素 4
	printf("%d\n", sizeof(a + 1));//第二个元素地址,4
	printf("%d\n", sizeof(a[1]));//第二个元素,4
	printf("%d\n", sizeof(&a));//整个数组的地址,4
	printf("%d\n", sizeof(*&a));//整个数组,16
	printf("%d\n", sizeof(&a + 1));//下一个数组的地址,4
	printf("%d\n", sizeof(&a[0]));//第一个元素的地址,4
	printf("%d\n", sizeof(&a[0] + 1));//第二个元素的地址,4

第二组 数组保存的单个字符

先了解strlen函数
在这里插入图片描述
C 字符串的长度等于字符串开头和终止空字符之间的字符数(不包括终止空字符本身)。

	//字符数组
	char arr[] = {
    
     'a', 'b', 'c', 'd', 'e', 'f' };
	printf("%d\n", sizeof(arr));//整个数组,6
	printf("%d\n", sizeof(arr + 0));//第一个元素的地址,4
	printf("%d\n", sizeof(*arr));//首元素,1
	printf("%d\n", sizeof(arr[1]));//第二个元素,1
	printf("%d\n", sizeof(&arr));//整个数组的地址,4
	printf("%d\n", sizeof(&arr + 1));//下一个数组的地址,4
	printf("%d\n", sizeof(&arr[0] + 1));//第二个元素的地址,4

	printf("%d\n", strlen(arr));//首元素地址,没有'\0',所以结果为随机值
	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));//第二个元素的地址,随机值

第三组 数组保存的字符串

	char arr[] = "abcdef";
	printf("%d\n", sizeof(arr));//整个数组,7
	printf("%d\n", sizeof(arr + 0));//字符指针,4
	printf("%d\n", sizeof(*arr));//字符型,1
	printf("%d\n", sizeof(arr[1]));//第二个元素,字符型,1
	printf("%d\n", sizeof(&arr));//数组指针,4
	printf("%d\n", sizeof(&arr + 1));//数组指针,4
	printf("%d\n", sizeof(&arr[0] + 1));//字符指针,第二个元素的地址,4

	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

第四组 char*指向的字符串

	char *p = "abcdef";
	printf("%d\n", sizeof(p));//指针,4
	printf("%d\n", sizeof(p + 1));//指针,4
	printf("%d\n", sizeof(*p));//字符a,1
	printf("%d\n", sizeof(p[0]));//下标引用,字符a,1
	printf("%d\n", sizeof(&p));//二级指针,4
	printf("%d\n", sizeof(&p + 1));//4
	printf("%d\n", sizeof(&p[0] + 1));//字符b的地址,4

	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

▲第五组 二维数组

	//二维数组
	int a[3][4] = {
    
     0 };
	printf("%d\n", sizeof(a));//整个数组,48
	printf("%d\n", sizeof(a[0][0]));//第一个元素,4
	printf("%d\n", sizeof(a[0]));//第一个数组的数组名,单组在sizeof中,表示整个数组,16
	printf("%d\n", sizeof(a[0] + 1));//没有单独放在sizeof内,第一个一维数组的第二的元素的地址,4
	printf("%d\n", sizeof(*(a[0] + 1)));//第一个一维数组的第二个元素,4
	printf("%d\n", sizeof(a + 1));//a是首元素地址,即第一个一位数组的地址,加一表示第二个一维数组的地址,4
	printf("%d\n", sizeof(*(a + 1)));//第二个一维数组的地址解引用,即第二个一维数组,16 
	printf("%d\n", sizeof(&a[0] + 1));//第二个一维数组的地址,4
	printf("%d\n", sizeof(*(&a[0] + 1)));//对第二个一维数组的地址解引用,表示第二个一维数组,16
	printf("%d\n", sizeof(*a));//a是第一行地址,第一个一维数组的地址解引用,表示第一个一维数组,16
	printf("%d\n", sizeof(a[3]));//sizeof不会访问目标,只根据类型来计算大小,故无报错,a[3]是一个包含四个整形的一维数组,16

二维数组的题要抓住几点:

  • 数组名大部分情况都表示首元素地址,而对于二维数组来说,首元素就是一个一维数组。
  • 数组名只有单独出现在sizeof内部和&符号后表示整个数组。
  • 二维数组的每一个一维数组名的表示方法

(二)笔试题

第一题

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,5
分析:&a表示整个数组的地址,加一指向数组后下一个位置,由于ptr类型是int*,减一实际减去所指向类型的大小,指向5的前面;a表示首元素地址,加一为第二个元素的地址,解引用后表示第二个元素。
如图:
在这里插入图片描述

第二题

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

运行结果: 0x100014,0x100001,0x100004
分析: 结构体的大小为20个字节,所以指针加一,加上的是20,转换成16进制即为14;将p强制转换成无符号长整型后,对其加一,实际值也加一;将p强转成unsigned int *类型,加一,实际值加四,因为指针的大小为四字节。

第三题

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

运行结果: 4,2000000
分析: ‘%p’格式与’%x’格式都是按十六进制输出,区别是 ‘%p’格式会输出八位,不够八位的补零,’%x’格式不会补0。
这里指针ptr指向的是数组最后一个元素的下一个位置,因其被强转成int*类型,ptr[-4]指向的是原来位置往前四个字节的位置,即指向4的最前面;
a强制转换为int型后值加1,再强为int *类型,指向的位置如下图所示,数据在内存中的存储遵从大小端原则,小端存储转低权值位在低地址处,读取时同样遵从小端的规律,高地址处的数据放在高权值位,所以读取顺序为02 00 00 00,最终结果为2000000。
在这里插入图片描述

第四题

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

运行结果: 1
分析: 数组在内存中的存储如图所示,圆括号中是逗号表达式,最终数组元素的值是逗号右边的值;a[0]是第一个一维数组的数组名,表示首元素地址,即a[0][0]的地址,赋值给p后,p指向首元素的位置,p[0]数组下标引用,表示偏移量为0,仍然指向原来1的位置。
在这里插入图片描述

第五题

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

运行结果: FFFFFFFC,-4
分析: 内存布局如下图所示,指针p跨度为4,最终与 &p[4][2] 与 &a[4][2]之间差4个字节,结果为-4,以%p打印输出结果则为FFFFFFFC。
在这里插入图片描述

第六题

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

运行结果:10,5
分析:&aa+1指向二维数组最后一个元素后面的位置;aa+1是指向第二个一维数组的数组指针,解引用后表示整个数组,等价于aa[1],即第个二维数组的数组名,表示第二个二维数组首元素的地址,即a[1][0]的地址,强转后减一,指向a[0][4]。在这里插入图片描述

第七题

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

运行结果: at
分析: 字符指针数组的写法并不是把字符串放保存在数组里,字符串是保存在内存中的静态字符常量区,这里只是用指针指向每个字符串第一个字符的地址,将这样的指针保存在数组中,与定义指针指向字符串原理相同,如:

char *pc="hello world";

这里的pc指向字符串的第一个字符’h’,并不是把字符串保存在pc中。
本题的数组a,是一个指针数组,保存了3个指向字符串的指针,如图:
在这里插入图片描述
pa是一个二级指针,指向a[0]这个一级指针,对pa自增后,其指向下一个一级指针a[1],pa解引用得到一个字符指针,%s打印输出,结果则为’at’。

第八题

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

“ENTER” “NEW” “POINT” “FIRST”
运行结果:
POINT
ER
ST
EW
分析: 内存分布图如下:
在这里插入图片描述
1.printf("%s\n", **++cpp);
++cpp对cpp进行自增,需要注意,cpp的指向会发生改变,且对后面都有影响。
在这里插入图片描述
所以结果为POINT。
2.printf("%s\n", *--*++cpp+3);
需要注意此时的cpp要用第一次自增后的指向;此时的cpp指向cp[1],自增后则指向产品[2],对cp[2]自减,其指向由cp[1]变为c[0],解引用则得到c[0]原来指向字符第一个字符’E’,c[0]+3指向第二个字符’E’,但是从[0]的指向只是临时改变,并没有赋值保存。如图:
在这里插入图片描述
3.printf("%s\n", *cpp[-2]+3);
cpp[-2]为数组下标引用,表示cp[0],其指向c[3],所以*cpp[-2]表示c[3],c[3]指向字符’F’,所以c[3]+3指向字符’S’,打印结果为’ST’。注意cpp[-2]与cpp-2的区别,cpp[-2]等价于*(cpp-2)。
在这里插入图片描述
4.printf("%s\n", cpp[-1][-1]+1);

等价于*(*(cpp-1)-1),cpp[-1][-1]表示c[2],c[2]是一个字符指针,指向字符’N’,加1后指向字符’E’。如图所示:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_44631587/article/details/120802211
今日推荐