C语言篇+ 指针进阶(上)

c语言 专栏收录该内容
15 篇文章 3 订阅

在这里插入图片描述

一、前言

在上一篇博客中作者已将指针的基础知识归纳在这篇博客中,《 指针和结构体(初级)》,而今天我们要进入指针的下一个环节,指针进阶!!为了怕大家忘记之前讲过的知识,本人再次梳理出一些知识,请各位耐心观看,以下又是老调重弹

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

1.1进入主题

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

int main() 
{
	char *p = NULL;
	p = "hello word";
	return 0;
}

在这里插入图片描述

进入正题,我们都知道指针的大小(所占用的字节),是跟操作系统有关的,指针的大小是固定的4/8个字节(32位平台/64位平台),而字符在内存中是占用一个字节的,那么上面的代码是将整个的 hello word 存放到字符指针中去,还是只存放字符串的第一个字符呢?
在这里插入图片描述
在内存窗口中我们可以看到指针变量p存放的恰好就是字符串的首字符的地址,直到后面的 00 00 ,前面的都是有效字符,试想字符在内存中是占一个字节的,如果要把整个字符串存放到一个指针变量中去,那不就得溢出了吗,所以存放的是首字符的地址,字符串在内存中是挨着存的,有了首字符的地址不难找到其他字符串内容,另外值得一提的一点就是字符串是存放在字符串常量区的,既然是一个常量,那么就不可以通过别的方式对其修改,否则就会引发程序出错,如果不小心修改了怎么办呢,建议在前面加const,这样即使你想改,编译器也会编译不过去
在这里插入图片描述

1.2牛刀小试

你见过这样的一道题吗?

#include <stdio.h>
int main()
{
    char str1[] = "hello word.";
    char str2[] = "hello word.";
    const char *str3 = "hello word.";
    const char *str4 = "hello word.";
    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; 
}

请把你心目中的答案,在心里默默的讲一句
没错我想你应该知道了,但是这里还是得再说一遍

第一个if语句中比较的是两个地址,因为数组名是字符首地址,那么既然是不同的数组名,想必在内存中的存放位置也是不一样的,即使在内存中存放的值都是 hello word ,但是在比较的时候还是比较的地址,试想不是在同一块内存开辟的同一块空间,他们的地址会一样吗?显然不是,而在 str3 和 str4比较的时候比较的也是地址,并且这两个指针变量指向的都是一块空间,既然是指向同一块空间,那么指向的就是字符首地址,因为在内存中压根就不会存放两份一样的数据,我们都知道字符串是常量不能被修改,既然是不可变的那么被两个指针去指向这一份不会被修改的内容又有什么关系呢而且还不会浪费内存,所以在这个地方比较的是同一个地址,当我们的程序执行起来,可以看到和我们的预期结果是一样的
在这里插入图片描述
结论:
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始
化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同

二、指针数组

指针数组的主体是一个数组,数组中存放的每一个元素是一个指针

int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组

这么理解指针数组的语法:

就拿arr1来说,[ ]的优先级是最高的,所以[ ]会先和arr1结合,那么arr1表示的是一个数组,而数组的元素类型是什么,int * 表示的就是数组的元素类型,所以arr1是一个每个元素都是int *的指针数组

1.1指针数组的初始化

int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int d = 40;
	
	int *arr[4] = {&a,&b,&c,&d};
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%d ", *(arr[i]));
	}
	return 0;
}

数组是连续存储的,而指针数组的每一个元素都是一个指针变量,并且这个指针变量指向的元素类型是int类型,所以在初始化的时候是将变量的地址存入到数组中,以下就是指针数组在内存中的布局,而打印的过程也很简单,通过数组下标索引数组中的每一个元素,元素对应变量的地址,对地址解引用就能找到此地址对应的变量值
在这里插入图片描述

1.2指针数组存放一维数组的地址

int main() 
{
	int arr1[5] = { 1,2,3,4,5 };
	int arr2[5] = { 2,3,4,5,6 };
	int arr3[5] = { 3,4,5,6,7 };

	int *parr[3] = { arr1,arr2,arr3 };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++) 
		{
			printf("%d ",parr[i][j]);
			//另外一种写法  printf("%d ",*(*(parr + i) + j));
		}
		printf("\n");
	}
	
	return 0;
}

parr是一个指针数组,这个指针数组存放的元素是一维数组的地址,每一个元素类型是int 因为数组名是数组首元素的地址(数组首地址),而数组首地址的类型是int ,所以指针数组的每一个元素都是int ,而指针数组的类型是int [3]
printf("%d ",
(
(parr + i) + j)); 如何理解这句代码,
(parr + i)是找到下标为i的那个元素,而这个元素是一个数组首地址啊
(parr + i) + j,找到了一维数组的首地址再偏移j个长度,不是就找到了一维数组中的第j个元素的地址了吗,((parr + i) + j),再通过()解引用就能找到地址上对应的数值,其实(*(parr + i) + j) 和 parr[i][j]这两种访问数组元素的原理是差不多的

在这里插入图片描述

int main() 
{
	char *arr[] = { "春眠不觉晓","处处闻啼鸟","夜来风雨声","花落知多少" };
	
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%s\n",arr[i]);
	}
	return 0;
}

字符指针数组的内存布局
在这里插入图片描述

指针数组中存放的每一个元素都是一个指针变量,这个指针变量的类型char*,每一个元素都指向着字符串起始地址,在以%s打印的时候会通过指针指向的起始地址找到‘\0’出现的前面字符,依次打印

三、数组指针

数组指针的主体是指针,该指针指向一个数组首地址(数组名)

int (*parr)[10]

这么理解数组指针的语法,()的优先级是最高的,所以* 会先和
parr结合,这么一看是一个指针,往后一看[10],表示的是指针指向的数组的元素个数是10,前面的int 表示数组指针指向的数组的每一个元素都是int ,而数组指针的类型是 int (*)[10]

1.1&数组名VS数组名

前提概念:1、数组名是数组首地址,数组名表示数组首元素的地址
2、&arr取出的是整个数组的地址
3、&arr的类型是数组指针 int (*)[10]

int mian()
{
	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);
	return 0;
}

在这里插入图片描述
从打印结果可以看出数组首元素的地址 + 1会跳过一个整形,而&数组名会跳过40个整形,原因是因为&arr的类型是 int(*)[10],对它 + 1会跳过40个字节,这里的地址不是数组首元素的地址,而是数组的地址,在这里要有一个概念性的了解

1.2数组指针的使用

1.2.1数组指针的错误示范

void func(int (*arr)[10],int sz) 
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ",arr[i]);//err
		printf("%d ", *(arr + i));//err
	}
}

int main() 
{
	int arr[10] = {1,2,3,4,5,67,8,9,10};
	int len = sizeof(arr) / sizeof(arr[0]);
	func(&arr,len);
	return 0;
}

在这里插入图片描述

根据前面的逻辑我们知道&arr取出的是数组的地址,int (*arr)[10] 是个数组指针,本质上还是一个指针,访问的是整个数组的地址,而数组的地址 + 1会偏移40个字节,随后访问的都是随机值。

1.2.2数组指针的正确示范示范

void func(int (*arr)[10],int sz) 
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
	//以下几种方式均可以
		printf("%d ", arr[0][i]);
		printf("%d ",(*arr)[i]);
		printf("%d ",*(*arr + i));
	}
}

int main() 
{
	int arr[10] = {1,2,3,4,5,67,8,9,10};
	int len = sizeof(arr) / sizeof(arr[0]);
	func(&arr,len);
	return 0;
}

解读:

arr [0][i] 有了第一个元素的下标向后访问i下标的元素,因为数组是连续存储的,有了第一个元素的下标,自然也能找到其他下标位置处的元素

(*arr)[i],数组指针是指向数组的地址,对指针解引用就能找到指针指向的内容,*arr表示的是数组的首元素地址,有了首元素的地址就可以向后偏移i位,其实等价于arr[i]

*(*arr + i),数组指针是指向数组的地址,对指针解引用就能找到指针指向的内容,*arr表示的是数组的首元素地址,对首元素的地址(数组首地址),偏移i个长度,会跳过4 * i个字节,再将偏移后的指针解引用找到指针指向的内容

希望以上的解释对大家的理解有所帮助,这也是博主对这些知识在一定程度上的理解,对这些语法的解释

接下来再看看另外一种用法


/* 常规写法 */
void func(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("%d ",arr[i][j]);
		}
		printf("\n");
	}
}

/* 使用数组指针 */
void func(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("%d ",arr[i][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};
	int row = sizeof(arr) / sizeof(arr[0]);
	int col = sizeof(arr[0]) / sizeof(arr[0][0]);
	func(&arr,row,col);
	return 0;
}

主要还是说一下数组指针的使用方式

首先我们来看这是35的二维数组
在这里插入图片描述
当使用数组指针的时候获取的是它的下标为【0】的一维数组的地址,数组指针里面存放的就是它的地址
在这里插入图片描述
假设现在要找处arr[1][4]的元素,下面我们直接看图
在这里插入图片描述
在这里是通过让数组指针指向下标【1】的位置处,有了数组的起始地址向后偏移,方便找到其他的元素,不过这种int(
)[5]类型的数组指针只是针对一维数组的,存放的是一维数组的地址,这一点请大家务必注意

有了上面知识的理解,再来看一组,你现在知道它们代表着什么吗?
在这里插入图片描述

1、int arr[5]; --》 表示的是一个数组,数组的每一个元素都是一个int
2、int *parr1[10]; --》表示的是指针数组,数组中的每个元素都是int *类型的指针变量
3、 int (*parr2)[10]; --》数组指针,本质上是一个指针,指向的是一个数组,数组的元素个数是10,数组的元素类型是int
4、int (*parr3[10])[5]; 重点来了,先看操作符的优先级,()的优先级是最高的,所以先看()里的内容,其次是[ ]优先级是第二,arr3先和[ ] 结合就是一个数组,数组里面存放着10个元素,每一个元素的类型是 int( * )[5],所以每一个元素都是一个数组指针,该数组指针指向的数组是【5】个元素,每一个元素是int类型,在这里插入图片描述

四、数组参数、指针参数

1.1一维数组传参

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

1.2二维数组传参

void test(int arr[3][5])//ok
{}
void test(int arr[][])//err
{}
void test(int arr[][5])//ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//err
{}
void test(int* arr[5])//err
{}
void test(int (*arr)[5])//ok
{}
void test(int **arr)//err
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);
}

1.3一级指针传参

#include <stdio.h>
void print(int *p, int sz) {
 int i = 0;
 for(i=0; i<sz; i++)
 {
 printf("%d\n", *(p+i));
 }
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9};
 int *p = arr;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //一级指针p,传给函数
 print(p, sz);
 return 0; }

1.4二级指针传参

#include <stdio.h>
void test(int** ptr) {
 printf("num = %d\n", **ptr); 
}
int main()
{
 int n = 10;
 int*p = &n;
 int **pp = &p;
 int *arr[10];//指针数组
 test(pp);//ok,二级指针传参给,二级指针接收
 test(&p);//ok,二级指针存放一级指针的地址
 test(arr);//ok,指针数组传参,传递的是首元素的地址,元素类型是int,元素地址是int *,二级指针能存放一级指针的地址
 return 0; }

五、函数指针

我们都知道函数指针变量是用来存放函数的地址的,那么函数的地址长什么样呢

在这里插入图片描述

1.1函数的地址和数组的地址有哪些地方不太一样

&数组名 - 数组的地址
数组名 - 数组首元素的地址

函数名 - 函数的地址
&函数名 - 函数的地址

1.2函数的地址又要存放到哪里去

void test()
{
 printf("hello word\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();

pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void,函数指针的类型是int ( *)()。

1.3函数指针的使用

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

int main()
{
	int ret = Add(10,20);
	printf("%d\n", ret);

	int (*pf)(int x, int y) = NULL;//定义函数指针变量
	pf = Add;//函数指针变量指向函数地址
	//pf = &Add; 可以写成这种方式
	ret = pf(20,30);//有了函数的地址,就可以调用该函数
	//ret = (*pf)(20,30);  可以写成这种形式,便于初学者理解
	//ret = (******pf)(20,30);  多加几颗*也没有影响
	printf("%d\n",ret);
}

1.4阅读两段有趣的代码:

代码来自《C陷阱和缺陷》
在这里插入图片描述
代码一

先看代码一,0是整形,类型是int,(void () ())外面的一层小括号表示的是强制类型转换,而void()()表示的是一个类型,函数指针类型(void(*) ()) 0拼接起来的意思不就是将int型的0强制类型转换为
void( * ) ()类型的函数指针吗,有了了函数地址,再对指针解引用就能调用该函数,被调用的函数是一个无参,返回值类型为void的函数
以上代码确实符合语法,因为0地址处是被操作系统调用的,而程序员无法调用
总结:这是一次函数调用

代码二

这是一次函数声明,声明的函数名是signal,signal函数有两个参数,第一个是int类型,第二个是void( * )(int)的函数指针类型,signal函数的返回类型是void( * )(int)的函数指针类型

1.4.1代码二的简化写法

typedef void(*pfunc_t)(int);//给函数指针类型取别名为pfunc_t
pfunc_t signal(int,pfunc_t);

这样子写起来是不是很简明,方便理解,熟练的使用typedef也可以解决这一类的问题

六、函数指针数组

函数指针数组的主体是一个数组,该数组存放的每一个元素是一个函数指针类型

1.1函数指针数组的初始化

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

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

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

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

int main()
{
	int(*pfArr[4])(int x, int y) = { Add ,Sub ,Mul ,Div};
	return 0;
}

怎么理解函数指针数组的语法呢?
int(*pfArr[4])(int x, int y)
首先【】的优先级是最高的,所以会先和pfArr结合,这一看就是一个数组嘛,数组的元素个数是4,数组的每一个元素类型就是int ( * )(int,int)类型的,那么我们可以知道数组的每一个元素都是一个函数指针,

这种初始化方式结合之前所学的函数指针和指针数组的知识相信屏幕前的你可以看懂,没错就是一个数组里面存放着的每一个元素都是一个函数指针,数组的元素个数是4,数组的每一个元素类型是int(*)(int,int)

1.2运用之前的知识

假设现在要实现一个简单的计算器程序
先按常规的写法实现一个计算器

void Menu() 
{
	printf("****************************\n");
	printf("*******1、Add   2、Sub *****\n");
	printf("*******3、Mul   4、Div *****\n");
	printf("*******     0、exit    *****\n");
	printf("****************************\n");
}
int main()
{
	int input = 0;
	do
	{
		int x = 0;
		int y = 0;
		int ret = 0;
		Menu();
		printf("请输入你的选择\n");
		scanf("%d",&input);
		switch (input)
		{
		case 0:
			printf("退出计算器\n");
			break;
		case 1:
			printf("请输入两个操作数\n");
			scanf("%d %d", &x, &y);
			ret = Add(x,y);
			printf("ret = %d\n",ret);
			break;
		case 2:
			printf("请输入两个操作数\n");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("ret = %d\n",ret);
			break;
		case 3:
			printf("请输入两个操作数\n");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("ret = %d\n",ret);
			break;
		case 4:
			printf("请输入两个操作数\n");
			scanf("%d %d", &x, &y);
			ret = Div(x, y);
			printf("ret = %d\n",ret);
			break;

		

default:
			printf("输入错误请重新输入\n");
			break;
		}
		

	} while (input);

  
	return 0;
}

简化版本

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

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

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

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

void Menu() 
{
	printf("****************************\n");
	printf("*******1、Add   2、Sub *****\n");
	printf("*******3、Mul   4、Div *****\n");
	printf("*******     0、exit    *****\n");
	printf("****************************\n");
}
int main()
{
	int input = 0;
	do
	{
		int x = 0;
		int y = 0;
		int ret = 0;
		Menu();
		printf("请输入你的选择\n");
		scanf("%d",&input);
		
		int(*pfArr[5])(int x, int y) = { 0,Add ,Sub ,Mul ,Div };
		
		if (!input)
		{
			printf("退出计算器\n");
			break;
		}
		else if (input >= 1 && input < 5)
		{
			printf("请输入两个操作数\n");
			scanf("%d %d",&x,&y);
			ret = pfArr[input](x, y);
			printf("ret = %d\n",ret);
		}
		else
		{
			printf("输入错误\n");
		}

	} while (input);


	return 0;
}

在这里插入图片描述
相比于之前的常规写法,这样的组织代码的方式,只让相同功能的代码只保存一份

可以看到函数指针数组的使用极大的降低了代码的冗余,而且可扩展性也能有所提升,将来要实现其他的功能,比如 >> 、<<、^、&=、这些计算都是可以的,只需要在函数指针数组初始化的时候再添加一个函数进去,再把else if分走语句的条件判断范围再给扩大那就ok,这种函数指针数组的使用方法在专业术语上叫做转移表

1.3通过回调函数的方式实现另外一种方案

1.4何为回调函数呢?

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

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

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

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

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

void Menu() 
{
	printf("****************************\n");
	printf("*******1、Add   2、Sub *****\n");
	printf("*******3、Mul   4、Div *****\n");
	printf("*******     0、exit    *****\n");
	printf("****************************\n");
}

void Calc(int(*pf)(int x,int y)) 
{
	int x = 0;
	int y = 0;
	int ret = 0;
	printf("请输入两个操作数\n");
	scanf("%d %d", &x, &y);
	ret = (*pf)(x, y);
	printf("ret = %d\n", ret);
}
int main()
{
	int input = 0;
	do
	{
		int x = 0;
		int y = 0;
		int ret = 0;
		Menu();
		printf("请输入你的选择\n");
		scanf("%d",&input);
		switch (input)
		{
		case 0:
			printf("退出计算器\n");
			break;
		case 1:
			Calc(Add);
			break;
		case 2:
			Calc(Sub);
			break;
		case 3:
			Calc(Mul);
			break;
		case 4:
			Calc(Div);
			break;

		default:
			printf("输入错误请重新输入\n");
			break;
		}


	} while (input);


	return 0;
}

这样的代码组织方式也让相同功能的代码只保存一份

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

指向函数指针数组的指针是一个 指针, 指针指向一个 数组 ,数组的元素都是 函数指针 ;

怎么定义呢?

	int(*arr)[5] = {0};//数组指针
	int(*pf)(int ,int) = NULL;//函数指针
	int(*pfArr[5])(int, int) = {0};//函数指针数组

	int(*(*ppfArr)[5])(int, int) = &pfArr;//指向函数指针数组的指针

怎么理解指向函数指针数组的指针的语法呢?
int(* (* ppfArr ) [5] ) (int, int)
首先ppfArr 是先和 * 结合的,那么他就是一个指针变量,指针变量指向一个什么呢,向后一看【5】,原来指针变量指向的是一个数组,数组的元素类型是什么呢,int ( * )(int,int),原来是函数指针

总结

总结:指向函数指针数组的指针 是一个指针,该指针变量指向的数组中存放的元素个数是5,数组的每一个元素类型是 int ( * )(int,int)

指针进阶还有部分内容将在下一章讲解,如果想进一步了解指针变量请关注博主,如果觉得本章内容复杂建议收藏反复观看,不要停止学习的步伐,毕竟欲戴王冠必承其重,祝你拿到好offer,感谢各位看官老爷
在这里插入图片描述

©️2021 CSDN 皮肤主题: 黑客帝国 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值