【C语言】指针进阶的详细讲解--(数组指针、指针数组、函数指针、函数指针数组、指向函数指针数组)

在这里插入图片描述

​📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:C语言学习
🎯长路漫漫浩浩,万事皆有期待

1.字符指针:

1.1字符指针的使用:

形式一:

int main()
{
	char ch = 'w';
	char* pc = &ch;
	*pc = 'w';
	printf("%c\n", *pc);
}

形式二:

int main()
{
	const char* pstr = "hello bit.";
	printf("%s\n", *pstr);
	return 0;
}

第二种用法的本质是把常量字符串 ’ hello bit. ’ 的首字符 ’ h ’ 的地址放到了pstr中,不是把一个字符串放到pstr指针变量里。

1.2.常量字符串:

注意,常量字符串作为常量,它们不可被修改

int main()
{
    char* p = "abcdef";
    //使指针指向常量区的常量字符串abcdef
    *p = 'A';
    //按照我们的理解,*p中存放的是首元素a的地址,我们尝试改变它
    //我们运行起来发现无法完成编译运行,程序会直接卡住
    //于是我们可以得出,当使用常量字符串时,常量字符串abcdef不可修改
    return 0;
}

这些常量字符串中的常量字符,是原本就储存在常量区(只读数据区)的一些常量,不同于需要我们输入的字符变量。所以当使用字符指针直接指向常量字符串时,可以直接进行调用但无法对其进行修改。

当出现这种错误时,程序往往是可以正常编译运行但会卡住而得不出结果,会给我们的代码书写造成很大的困扰。为了尽可能的避免这种情况的发生,我们通常在指向常量字符串时,使用 const 对指针变量进行修饰,进行这样的书写操作之后,当我们试图对常量字符串进行改动时,将会直接报错而无法进行编译运行, 便于我们找到问题的所在

1.3.相关面试题:

int main()
{
    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    //创建两个数组、两个字符型指针变量
    const char* str3 = "hello bit.";
    const char* str4 = "hello bit.";
//判断数组内容是否相同:
    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;
}

这道面试题中定义了两个字符型数组和两个字符型指针变量,将两个字符型数组进行对比,又将两个字符型指针变量尽行了对比,目的是为了验证它们在存储时内存的开辟。

这里 str3 和 str4 指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域(在我们的内存中存在着常量区用于存放一些常量),当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同

2.指针数组:

指针数组的本质是数组,是用于存放指针变量的数组:

int main()
{
    //创建变量:
    int a = 1;
    int b = 2;
    int c = 3;
 //创建指针数组,用于存放指针(即变量的地址):
    int* arr[3] = { &a,&b,&c }; 
//打印验证内容:
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("%p\n", arr[i]);
    }
//也可以通过指针数组(存放的是各个变量的地址)来访问变量内容:
    for (i = 0; i < 3; i++)
    {
        printf("%d\n", *(arr[i]));
    }
    return 0;
}

更常用的却是另外一种情况:

int main()
{
 //创建数组:
    int arr1[3] = { 1,2,3 };
    int arr2[3] = { 4,5,6 };
    int arr3[3] = { 7,8,9 };
	int* parr[3] = { arr1,arr2,arr3 };
    //我们都知道,数组名即为数组内首元素地址
    //即此处的数组名为地址,故可以存放在指针变量内
 
//验证指针数组内存放数据:
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("整型数组arr%d[3]首元素地址为:%p\n", i + 1, parr[i]);
    }
    printf("\n");
 
 //通过指针数组访问整型数组内的数据:
    for (i = 0; i < 3; i++)
    {
        printf("arr%d[3]中存储的数据为:", i + 1);
        int j = 0;
        for (j = 0; j < 3; j++)
        {
            printf("%d ", *(parr[i] + j));
        }
        printf("\n");
    }
    return 0;
}

3.数组指针:

3.1 数组指针的定义:

数组指针的本质是指针。不同于其他指针,数组指针指向的不是地址而是数组

int main()
{
    int* p1[10];
    //p1是数组,指针数组
 	int(*p2)[10];
    //p2是指针。数组指针
    return 0;
}

上面的示例中,p2先和*进行结合,说明p2是一个指针变量,再指向大小为10的整型数组。即p2是一个指向数组的指针,叫做数组指针。简单来说,数组指针就是专门用来存放一个数组的地址的指针。

3.2.&数组名VS数组名:

我们都知道,除一些特殊情况外,大多数的数组名都表示数组首元素的地址,那么既然数组名已经是地址了,&+数组名的组合又代表着什么呢?

int main()
{
    int arr[5] = { 0 };
    printf("arr :%p\n", arr);
    printf("&arr:%p\n", &arr);
    return 0;
}

我们如果通过打印,会发现 arr 的打印结果与 &arr 相同:
那它们真的是一回事吗?
我们将上面这段代码进行改写:

int main()
{
    int arr[5] = { 0 };
	printf("arr :%p\n", arr);
    printf("&arr:%p\n", &arr);
	printf("\n");
	printf("arr+1 = %p\n", arr + 1);
    printf("&arr+1= %p\n", &arr + 1);
    return 0;
}

我们使 arr 与 &arr 同时向后走一步,这时我们看到了不一样的结果:
实际上,&arr 表示的是整个数组的地址,而 arr 仅表示其中首元素的地址,这就导致了我们在使它们在向后走的时候,它们向后走的步幅不同,&arr 跨过了整个数组的长度而 arr 仅仅只跳过了一个数据元素。

3.3.数组指针的使用:

在上面的介绍中,我们看到了数组指针的最基础用法:

int arr[10] = { 0 };
int (*p2)[10] = &arr;

但我们却很少会这样去使用它。更多的是在我们进行函数调用,进行数组传参时,可以通过数组指针来进行接收:

void print_arr1(int arr[3][5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        int j = 0;
        for (j = 0; j < col; j++)
        {
            printf("%02d ", arr[i][j]);
        }
        printf("\n");
    }
}
//使用数组指针来进行接收:
void print_arr2(int(*arr)[5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        int j = 0;
        for (j = 0; j < col; j++)
        {
            printf("%02d ", arr[i][j]);
        }
        printf("\n");
    }
}
int main()
{
    int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
    print_arr1(arr, 3, 5);
    //数组名arr,表示首元素的地址
    //但是二维数组的首元素是二维数组的第一行,所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
    printf("\n");
   //可以使用数组指针来进行接收:
    print_arr2(arr, 3, 5);
    return 0;
}

4.数组参数:

4.1一维数组传参:

我们都知道,在对数组进行传参时并不会真实的在内存中创建临时数组,数组名的意义是作为数组内首元素的地址,因此当函数参数为数组名时,实际上传递的是数组中首元素的地址,于是我们可以发现下面三种传参方式都是可行的

//方式1:标准传参方式
//完整的传递数组内容
void test1(int arr[3])
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
//方式2:省略数组大小
//形参部分的数组大小可以省略
void test2(int arr[])
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
//方式3:扩大形参数组大小
//实际仍为传递首元素地址,故可行,但不建议
void test3(int arr[100])
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
	int arr[3] = { 1,2,3 };
 //调用函数:
	test1(arr);
	test2(arr);
	test3(arr);
	return 0;
}

这三种将形参写作数组形式的方式都是可行的,但是为了避免出现错误,推荐大家尽可能的使用第一种方式,其次是第二种方式。第三种方式虽然也可以运行,但有可能会出现难以预料的错误,不建议使用。
同时,以上三种将形式参数写成数组形式的写法,也可以改写为使用指针做形式参数的形式

//使用指针作为形式参数:
//一级指针:
void test1(int* p)
{
	int i;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", *(p + i));
	}
	printf("\n");
}
//二级指针_形式1:
void test2(int** p)
{
	int i;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", *(p + i));
	}
	printf("\n");
}
//二级指针_形式2:
void test3(int** p)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", **(p + i));
	}
}
int main()
{
	int arr[3] = { 1,2,3 };
    int* parr = &arr;
   int arrA[3] = { 1,2,3 };
	int arrB[3] = { 4,5,6 };
	int arrC[3] = { 7,8,9 };
	int* arrD[3] = { &arrA,&arrA,&arrC };
  //调用函数:
	test1(arr);
    test2(parr);
	test3(arrD);
 	return 0;
}

并且同理,使用指针作为形式参数的另外两种形式也是可以(但最后一种方式同样不建议)的:

void test(int* p[3])
{
    ...
}
 void test(int* p[100])
//可以但不建议使用
{
    ...
}

4.2 二维数组传参:

我们在前面初阶的部分学习二维数组传参时就知道了,二维数组在进行传参时可以不知道有多好行,但必须知道有多少列,这样计算机才知道应该在何时进行换行。如此只要知道了什么时候进行换行,对于行数就不需要再进行强制要求了,所以在数组进行传参时,允许写成以下三种形式:

//二维数组传参:
//方式1:标准传参
void test1(int arr[3][3])
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 3; j++)
		{
			printf("arr[%d][%d] = %d ", i, j, arr[i][j]);
		}
		printf("\n");
	}
}
//方式2:行数可以省略
void test2(int arr[][3])
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 3; j++)
		{
			printf("arr[%d][%d] = %d ", i, j, arr[i][j]);
		}
		printf("\n");
	}
}
//方式3:行数可以超出原数组上限
void test3(int arr[100][3])
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 3; j++)
		{
			printf("arr[%d][%d] = %d ", i, j, arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
 //函数调用:
	test1(arr);
	test2(arr);
	test3(arr);
	return 0;
}

同样的,二维数组除了可以使用数组作为函数的参数以外,也可以使用指针作为函数的参数进行传参,区别于一维数组,二维数组在传参时传递的不是首元素的地址而是首行元素的地址

void test(int(*p)[3])
//此处的数组大小3为列数
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 3; j++)
		{
			printf("%d ", (*p + i * 3)[j]);
            //这里注意p+(i*3)是因为i为行号,跳过行时需要跳过i*3个数据元素
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
 test(arr);
return 0;
}

但是要格外注意,二维数组与一维数组不同,不可以使用二级指针进行传参。其原理是,二级指针的作用是用于存储一级指针的的地址,而传递过来的参数是二维数组第一行(这里可以简单理解为一个一维数组,但本质上不是)的地址,无法使用二级指针进行存储。

5.指针参数:

5.1一级指针传参:

当我们在函数调用,并使用一级指针作为参数时,很容易理解:一级指针 p 中存放的是数组 arr 中首元素的地址,即传址做参,于是我们就可以在函数参数设计时,使用一级指针进行接收,就可以达到我们的目的。

void test(int* p)
//传递的是一级指针,存储的是arr首元素的地址,使用一级指针进行接收
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", *p + i);
	}
}
int main()
{
	int arr[5] = { 1,2,3,4,5 };
	int* p = arr;
	//数组名为首元素地址
	test (p)//等价于:test(arr);
	return 0;
}

5.2 二级指针传参:

首先最基础的用法是传递二级指针,就使用二级指针进行接收。我们直接上示例即可:

void test(int** p)
//传递二级指针,使用二级指针进行接收
{
	printf("%c", **p);
}
int main()
{
	char a = 'w';
	char* pa = &a;
	char** ppa = &pa;
 test(ppa);
	return 0;
}

当函数参数为二级指针时,都可以接收什么参数?
我们都知道,二级指针是用来存储一级指针的地址的,那么除了定义二级指针、存储一级指针地址,我们在前面还学过一个知识点也是用来存储地址的——指针数组。那么指针数组可以做为函数参数进行传递吗?答案是:可以

//指针数组做函数参数:
void test(int** p)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%c ", **p + i);
	}
}
 
int main()
{
	char a = 'a';
	char b = 'b';
	char c = 'c';
	char* arr[3] = { &a,&b,&c };
 test(arr);
return 0;
}

6.函数指针:

6.1函数指针:

在我们的程序中,各种值和组成成分都有自己的一片空间,我们的自定函数也不例外:

void test()
{
	printf("hehe\n");
}
int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);
	return 0;
}

自定义函数也有着自己的储存空间,那么当我们想要将函数的地址储存起来时,又该如何进行处理呢?函数指针给出了答案。其定义格式为:

函数返回类型( * + 函数指针名 )(函数参数类型)= 函数名;

int Add(int x, int y)
{
	int z = x + y;
	return z;
}
 
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int ret = Add(a, b);
	printf("a + b = %d\n", ret);
	printf("\n");
 int(*p)(int, int) = Add;
    //Add函数的返回类型为int类型,函数指针名为p,两个参数类型分别为int类型、int类型
	printf("%p\n", p);
 return 0;
}

我们很清楚的看到,通过使用函数指针就可以将函数的地址存储起来,并且我们可以通过使用函数指针优化我们的代码,提升我们代码的可读性:
比如:

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

很明显这段代码的可读性非常差,理解起来非常麻烦,于是我们可以通过使用函数指针来提升我们代码的可读性:

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

6.2 函数指针数组:

对于函数指针,同样可以使用函数指针数组存储多个函数指针:

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 main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int ret1 = Add(a, b);
	int ret2 = Sub(a, b);
	int ret3 = Mul(a, b);
	printf("ADD = %d SUB = %d Mui = %d\n", ret1, ret2, ret3);
  //使用函数指针从存放函数地址:
	int(*padd)(int, int) = Add;
	int(*psub)(int, int) = Sub;
	int(*pmul)(int, int) = Mul;
//函数指针数组:
	int(*p[3])(int, int) = { padd,psub,pmul };
//通过函数指针数组打印函数地址:
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("指针p[%d]中存放的地址为:%p\n", i, p[i]);
	}
 //通过函数指针数组调用函数:
	for (i = 0; i < 3; i++)
	{
		int RET = p[i](a, b);
		printf("%d ", RET);
	}
	printf("\n");
  return 0;
}

7.指向函数指针数组的指针:

7.1 书写格式:

函数返回类型( * ( * 指针名 ))( 函数参数类型) = &函数名;

7.2 示例:

使用中我们会遇到的指向数组的指针的实际使用基础案例:

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 main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	//使用函数指针从存放函数地址:
	int(*padd)(int, int) = Add(a, b);
	int(*psub)(int, int) = Sub(a, b);
	int(*pmul)(int, int) = Mul(a, b);
	//函数指针数组:
	int(*p[3])(int, int) = { padd,psub,pmul };
 
//使用指针指向函数指针数组:
	//即pp为指向函数指针数组的指针
	int* pp = &p;
	
//上面这种形式是为了便于我们理解
	//而更多的会写成下面这种形式:
	int(*(*p_p))(int, int) = &p;
 
//验证指向函数指针数组的指针指向是否正确:
	printf("%p\n", &p);
	printf("%p\n", pp);
	printf("%p\n", p_p);
 return 0;
}

8.回调函数:

8. 1 定义:

我们前面都知道了,函数的调用除了我们最基础的调用方式之外,通过函数指针我们也可以实现对函数的调用。而这种通过函数指针调用函数的方式,就称为回调函数
在实际的使用中,如果你把函数的地址(即使用指针)传递给另一个函数 ,且当这个指针调用了它指向的函数时我们就把这样的函数称作回调函数。

8.2 使用实例:

我们一起来看一个回调函数的基础使用实例:

//函数1:
void test()
{
	printf("test\n");
}
//函数2:
//使用函数指针接收,并对函数test进行调用:
void TEST(void(*p)())
{
	//使用函数指针调用test函数:
	p();
	printf("TEST\n");
}
int main()
{
	//将函数地址传递给另一个函数:
	TEST(test);
	return 0;
}

在这个过程中,函数 test 就被作为函数参数传递了出去,并且在函数 TEST 中被函数指针接收,且该指针调用了它指向的 test 函数,于是我们就可以说 test 函数是回调函数
结合我们之前写的三个算数函数我们再来看一看回调函数在实际使用中的真实使用方式:

//计算函数功能实现:
void Add(int x, int y)
{
	int z = x + y;
	printf("%d + %d = %d\n", x, y, z);
}
void Sub(int x, int y)
{
	int z = x - y;
	printf("%d - %d = %d\n", x, y, z);
}
void Mul(int x, int y)
{
	int z = x * y;
	printf("%d * %d = %d\n", x, y, z);
}
 
//使用函数指针调用函数:
void calc(void(*p)(int, int))
{
	int x = 0;
	int y = 0;
	printf("请输入两个操作数:>");
	scanf("%d %d", &x, &y);
	p(x, y);
}
void menu()
{
	printf("********************\n");
	printf("*****   1.ADD  *****\n");
	printf("*****   2.SUB  *****\n");
	printf("*****   3.MUL  *****\n");
	printf("*****  0.EXIT  *****\n");
	printf("********************\n");
	printf("请输入:");
}
 
int main()
{
	int input;
	do
	{
		menu();
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			calc(Add);
			break;
		case 2:
			calc(Sub);
			break;
		case 3:
			calc(Mul);
			break;
		default:
			break;
		}
	} while (input);
 return 0;
}

8.3 使用回调函数模拟库函数 qsort 的实现:

库函数 qsort 基于快速排序法实现的一个排序函数。而通过上面的学习,我们可以通过使用回调函数来模拟出库函数 qsort 的功能实现:(注:这里我们的模拟实现方式采用冒泡方式

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 bubble(void* base, int count, int size, int(*cmp)(void*, void*))
{
    int i = 0;
    int j = 0;
    for (i = 0; i < count - 1; i++)
    {
        for (j = 0; j < count - i - 1; j++)
        {
            if (cmp((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 };
    //char *arr[] = {"aaaa","dddd","cccc","bbbb"};
    int i = 0;
    bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
    for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
    return 0;
}

9.总结:

今天我们对指针的知识又有了新的了解,学习了字符数组、数组指针、指针数组、数组参数、指针参数、函数指针、函数指针数组、指向函数指针数组的指针以及回调函数的相关知识,算是给我们的指针进阶内容画上了一个完美的句号,希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

在这里插入图片描述

  • 9
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Sherry的成长之路

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

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

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

打赏作者

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

抵扣说明:

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

余额充值