【C语言】指针终结者-进阶

目录

前言

 一、字符指针

二、数组指针

1、数组指针的定义

2、&数组名与数组名

3、数组指针的使用

1)数组指针的打印

2)在二维数组中的使用

4、练习题

三、数组参数、指针参数

1、一维数组传参

2、二维数组传参

3、一级指针传参

4、二级指针传参

四、函数指针

1、&函数名与函数名

2、函数指针的书写

3、函数指针的使用

4、代码分析

五、函数指针数组

1、函数指针数组书写

2、函数指针的作用—转移表

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

总结


前言

在上一篇文章:【C语言】指针终结者-初阶中我们主要介绍了指针相关的基本知识:

  • 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
  • 指针的大小是固定的4/8个字节(32位平台/64位平台)。
  • 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。
  • 指针的运算。

在这篇文章中我们将要讲解指针更深层次的内容,建议有指针基础后在阅读本文。本文将介绍:字符指针、数组指针、参数、函数指针等内容。


 一、字符指针

在前面讨论指针的类型时我们提到过一种类型:字符指针类型-char*。接下来我们详细地讨论一下字符指针的使用。

  • 其基本使用如下:
int main()
{
    char ch = 'w';
    char *pc = &ch;
    *pc = 'a';
    return 0;
}
  • 其第二种用法如下:
int main()
{
    const char* pstr = "hello world";
    printf("%s\n", pstr);
    return 0;
}

这里有一个极其理解错误的地方:这里是把一个字符串放到pstr指针变量中吗?并不是,其本质是把一个字符串hello world首字符的地址放到了pstr中,上面代码的意思便是把常量字符串的首字符h的地址存放到指针变量pstr中。注意打印时不需要使用*符号,使用%s打印

【C语言】指针终结者-进阶_第1张图片

 由于这种写法赋值的是常量字符串,故不可修改其中的内容。

【C语言】指针终结者-进阶_第2张图片

这样写会使程序崩溃。 

  • 其第三种用法如下: 
int main()
{
    char arr[] = "abcdefg";
    char* pc = arr;
    printf("%s\n", arr);
    printf("%s\n", pc);
    return 0;
}

由于数组名是首元素的地址,那么可以直接将数组名赋值给字符指针,那么字符指针就指向整个数组。

接下来我们来看一道面试题:

#include
int main()
{
	char str1[] = "hello world";
	char str2[] = "hello world";
	const char* str3 = "hello world";
	const 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 and str2 are not same

str3 and str4 are same

我们一起来分析其结果:当使用相同的常量字符串去初始化不同的数组的时候会开辟出不同的内存块了,所以str1和str2不同;C/C++会把常量字符串存储到一个内存区域,其允许几个指针同时指向该空间,所以str3与str4指向的是同一块空间,所以相同。


二、数组指针

1、数组指针的定义

数组指针是一个指针还是一个数组呢?它是一个指针。我们在初阶的内容说到整型指针(int* pi;)是能够指向整型数据的指针,字符型指针(char* pc)是能够指向字符型数据的指针,依此类推,数组指针便是一个指向数组的指针。

我们来看看数组指针的代码书写,判断一下下面哪个是数组指针的定义形式:

int* p1[10];
int  (*p2)[10];

上面两个代码中是数组指针的是:int  (*p2)[10]; *与p2先结合表示p2是一个指针变量,然后指向的是一个大小为10个元素,每个元素为int类型的数组。

为什么不能像第一个代码那样写呢?这是由于[ ]的优先级要高于*号的,如果不加()提高其优先级,p1会与[ ]结合,这时p1就是一个数组了,所以必须加上()提升其优先级。

2、&数组名与数组名

这个知识点我们已经在【C语言】指针终结者-初阶文章中已经详细介绍过,这里只做简单的回顾。

我们都知道到数组名表示首元素的地址,而有两种情况数组名不是表示数组的首元素地址,&数组名就是其中一种情况。我们看下面的代码:

#include
int main()
{
	int arr[10] = { 0 };
	printf("arr = %p\n", arr);
	printf("arr+1 = %p\n", arr + 1);

	printf("----------\n");

	printf("&arr = %p\n", &arr);
	printf("&arr+1 = %p\n", &arr + 1);
	return 0;
}

数组名与&数组名虽然值一样,但&数组名取出的是整个数组。&arr 的类型是:int(*)[10] ,是一种数组指针类型,数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40。

3、数组指针的使用

既然数组指针是指向数组的,那么其存放的就是数组的地址。其基本使用如下:

#include 
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p

    return 0;
}

1)数组指针的打印

方式一:

	for (int i = 0; i < 10; i++){
		printf("%d ", (*p)[i]);
	}

方式二:

	for (int i = 0; i < 10; i++){
		printf("%d ", *(*p+i)); //*p == arr;
	}

方式三:

	int* p = arr;
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}

2)在二维数组中的使用

  • 形参是数组的形式
#include 
void print_arr1(int arr[3][5], int row, int col)
{
    int i = 0;
    for(i=0; i

数组名arr表示首元素的地址,但是二维数组的首元素是二维数组的第一行,所以这里传递的arr其实是相当于第一行的地址,是一维数组的地址,故可用数组指针来接受,如下:

  • 形参是数组指针的形式
void print_arr2(int (*arr)[5], int row, int col)
{
    int i = 0;
    for(i=0; i

其访问元素:*(*( p + i ) + j) 

4、练习题

写出下面各代码的意思:

int arr[5];
arr是一个数组,数组中有5个元素,每个元素类型为int

int *parr1[10];
parr1是一个数组,数组中有10个元素,每个元素的类型为int*

int (*parr2)[10];
parr2是一个指针,指向的是一个数组,这个数组有十个元素,每个元素的类型是int

【C语言】指针终结者-进阶_第3张图片

 int (*parr3[10])[5];

parr3是一个有10个元素的数组,它的每个元素是一个数组指针,整个数组指针能够指向一个五个元素,每个元素为int的数组,

【C语言】指针终结者-进阶_第4张图片


三、数组参数、指针参数

我们在今后编写代码的时候我们难免会遇到需要将数组或指针作为实参传给函数,那这时函数的参数该如何来设计呢?

1、一维数组传参

我们直接来看代码:

#include 
void test(int arr[]) //正确
{}
void test(int arr[10])//正确
{}
void test(int *arr) //正确
{}
void test2(int *arr[20])//正确
{}
void test2(int **arr)//正确
{}
int main()
{
    int arr[10] = {0};
    int *arr2[20] = {0};
    test(arr);
    test2(arr2);
}

由上述传参代码举例可知:

  • arr:当我们把一维数组进行传参时,参数部分可以写成数组,数组的大小可以省略也可以不省略;由前面学到过的知识知道,数组名是首元素的地址,故传数组名时我们也可以使用指针来接收。
  • arr2:实参arr2是一个有20个元素,每个元素为int*的数组,故使用一个有20个元素,每个元素为int*的数组来接受是肯定可以的,当然数组元素个数是可以省略的;而arr2是数组名,为首元素的地址,而首元素是一个指针,故数组名arr2是一级指针的地址,那么就必然需要二级指针来接收。

故以上传参方式均是可行的。

2、二维数组传参

见下面的代码:

void test1(int arr[3][5])//正确
{}
void test2(int arr[][])//错误
{}
void test3(int arr[][5])//正确
{}

void test4(int *arr)//错误
{}
void test5(int* arr[5])//错误
{}
void test6(int (*arr)[5])//正确
{}
void test7(int **arr)//错误
{}
int main()
{
    int arr[3][5] = {0};
    test(arr);
    return 0;
}

由上述传参代码举例可知:

  • 对于使用数组接收的情况:函数的形参部分只能省略第一个[ ]中的数子,因为对于一个二维数组来说可以不知道有多少行,但是必须知道一行的元素个数是但是,故第一与第三个函数形参是正确的,第二个函数形参是错误的。
  • 对于使用指针接收的情况:我们先来分析第四个函数的形参,arr是一个二维数组,故其传参时传过去的是一维数组的地址,不能是一个整型指针来接受,故第四个函数传参错误;第五个函数参数也是存在问题的,其参数部分表示的是arr是一个有五个int*数据类型的数组,明显与实参不符合;第七个函数参数也是存在问题的,函数参数部分是一个二级指针应该用来接收一级指针的地址,而test(arr)传过去的是一个一维数组的地址,并不是一级指针,故第七个函数传参也是错误的。                                                                                                                        使用指针接收正确的写法应该是test6的参数的写法:int (*arr)[5]; arr是一个指针指向的是有五个元素,每个元素为int的数组。

3、一级指针传参

一级指针传参需要一级指针来接收,见代码:

#include 
void print(int *p, int sz)  //使用一级指针来接收
{
    int i = 0;
    for(i=0; i

一级指针传参非常简单,我们来看一个思考:

当一个函数的参数部分(形参)为一级指针的时候,函数能接收什么参数?

即:

void test1(int *p)                                            void test2(char* p)
{}                                                                    {}  
//test1函数能接收什么参数?                         //test2函数能接收什么参数?

  • 我们先来看第一个:形参部分为一级指针,那么肯定可以将地址作为实参传过去让它接收,test1(&a);它也可以将一级指针(即存放整型变量的地址)作为实参。

【C语言】指针终结者-进阶_第5张图片

  • 我们再来看第二个:由上同理,其实参部分可以为字符变量的地址,以及存放字符变量地址的指针。

【C语言】指针终结者-进阶_第6张图片

4、二级指针传参

二级指针传参需要二级指针来接收,见代码,代码中的pp就是一个二级指针:

#include 
void test(int** ptr)
{
    printf("num = %d\n", **ptr);
}
int main()
{
    int n = 10;
    int*p = &n;
    int **pp = &p;
    test(pp);
    return 0;
}

与一级指针类似我们来看:

当函数的参数为二级指针的时候,可以接收什么参数?

当形参为二级指针时,其实参部分可以为一级指针地址,以及存放一级指针地址的二级指针。

还有一种情况也是可以作为实参的,见下面的代码段:

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

由于arr是一个指针数组,故其数组名是int*的地址,即一级指针的地址。

【C语言】指针终结者-进阶_第7张图片


四、函数指针

函数指针顾名思义就是指向函数的指针,存放函数的地址。

1、&函数名与函数名

我们使用代码来看看他们的关系:

#include 
void test()
{
    printf("hello world\n");
}
int main()
{
    printf("%p\n", test);
    printf("%p\n", &test);
    return 0;
}

代码运行结果如下:

【C语言】指针终结者-进阶_第8张图片

由此可见:&函数名和函数名都是首元素的地址。

2、函数指针的书写

判断下面两个代码哪个能够存放函数的地址:

void (*pfun1)();
void* pfun2();

首先函数指针是一个指针,能够存放地址,那么我们就直接可以判断出pfun1是一个函数指针,pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。而pfun2是一个函数,其返回值为void*。

例子:

  • int (*pa) (int,int) = Add; — pa是一个函数指针,指向的函数有两个int类型的参数,返回值为int
  • void (*pc) (const char*) = Print; — pc是一个函数指针,指向的函数的参数为const char*,返回值为void

3、函数指针的使用

我们直接来看代码:

#include 
int Add(int num1,int num2)
{
	return num1 + num2;
}
void Print(const char* ptr)
{
	printf("%s\n", ptr);
}
int main()
{
	int (*pa)(int, int) = Add;
    //多种打印方式
	printf("%d\n", (*pa)(4, 5));
    printf("%d\n", pa(4, 5));
	printf("%d\n", Add(4, 5));

	void (*pc)(const char*) = Print;
	(*pc)("Hello world!");
	return 0;
}

4、代码分析

代码一:

(* ( void (*)() )0) ();

解读:该代码意思为

  • 把0强制类型转换成:void(*) ()函数指针类型 — 0就是一个函数的地址,再调用0地址处的函数。

代码二:

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

解读:该代码的意思为

  • signal是一个函数声明
  • 其有两个函数参数:一个是int类型,一个是函数指针,该函数指针指向的函数参数为int,返回值为void
  • signal函数的返回类型也是一个函数指针:该函数指针指向的函数参数是int,返回值为void

对于代码二其很复杂,可以进行如下简化:

typedef void(*pfun_t) (int);
pfun_t signal(int,pfun_t);

五、函数指针数组

根据前面一些概念的学习我想大家可以非常容易地分析出函数指针数组。

函数指针数组是一个数组,用于存放函数指针(即函数的地址)的数组。

1、函数指针数组书写

同理,我们先一起来学习函数指针数组的定格式,分析下面的代码中哪个能够正确表示函数指针数组:

int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];

正确的是parr1,首先parr1先与[ ]结合,表示parr1是一个数组,有10个元素,每个元素的类型为 int (*) (),故其指向的函数参数为空,返回值为int。

若打印内容,代码如下:

int (*parr[4]) (int,int) = {add, sub};
for(int i=0; i<2; i++)
{
    printf("%d\n", parr[i](2,3));
}

2、函数指针的作用—转移表

使用switch语句实现简单的计算器(思路比较简单,这里就不做过多的讲解):代码如下:

#include 
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("*************************\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;
}

使用转移表实现计算器:函数部分都一致,这里只给出主函数部分:

int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
	while (input)
	{
		printf("*************************\n");
		printf(" 1:add 2:sub \n");
		printf(" 3:mul 4:div \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		if ((input <= 4 && input >= 1))
		{
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = (*p[input])(x, y);
		}
		else
			printf("输入有误\n");
		printf("ret = %d\n", ret);
	}
	return 0;
}

这里定义了一个p为函数指针数组,将add、sub、mul、div函数的地址(函数名即其地址)f放入该数组中,再在后面的代码中通过(*p[input])(x, y)进行间接地调用相对应的函数。这就是转移表。


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

我们刚刚说的函数指针数组是一个数组,那么这数组也会有一个地址,则指向函数指针数组的指针就是用来指向函数指针数组的,存放函数指针数组的地址的。

其定义方法如下:

int (*pfArr[4]) (int,int); //pfArr是一个数组 - 函数指针数组

int (* (*ppfArr) [4]) (int,int) = &pfArr

ppfArr是一个数组指针,指针指向的数组有4个元素,指向的数组的每个元素的类型是一个函数指针 int (*) (int,int)

void test(const char* str)
{
    printf("%s\n", str);
}
int main()
{
    //函数指针pfun
    void (*pfun)(const char*) = test;
    //函数指针数组pfunArr
    void (*pfunArr[5])(const char* str);
    pfunArr[0] = test;
    //指向函数指针数组pfunArr的指针ppfunArr
    void (*(*ppfunArr)[5])(const char*) = &pfunArr;
    return 0;
}

总结

在这篇文章中深入得探讨了指针的相关内容,我相信大家学完之后会发现指针的奇妙与强大,但是要分清楚这篇文章中一些概念还需要更多的练习来加深印象,以便我们熟练运用。

感谢大家的支持。

你可能感兴趣的:(C语言,c语言,开发语言)