深入了解C语言指针(3)

深入了解指针(3)

1.字符指针变量

我们先来看一种之前我们用的方式去使用字符指针变量

char str= 'a';
char* p = &str;
*p = 's';

现在我们再来学习一个新的使用方法

int main()
{
	const char* p = "hello world"; // 我们把这个字符串理解成一个字符数组 也就是把首元素地址传给了p指针	
	// 这个字符串叫做常量字符串   是不能被修改的   即使不加这个const 也是不能修改的
	//*p = 'a'; // error
	// 我们无法通过解引用操作去修改常量字符串
	// 我们来验证一下
	printf("%c\n", *p);
	// 输出  三种方法的结果是一样的
	printf("%s\n", p); // 打印的时候只需要一个起始地址 后面的内容只要是连续的地址 就可以一次性打印出来
	printf("%s\n", "hello world");
	int len = strlen(p);
	for (int i = 0; i < len; i++)
	{
		printf("%c", *(p + i));
	}
	return 0;
}

再来看一道题

// 我们再来看一种情况
#include <stdio.h>
int main9()
{
	char strl[] = "hello bit.";
	char str2[] = "hello bit.";
	const char* str3 = "hello bit.";
	const char* str4 = "hello bit.";
	if (strl == str2)
		printf("strl and str2 are same \n"); // 1
	else
		printf("strl and str2 are not same \n"); // 2
	if (str3 == str4)
		printf("str3 and str4 are same \n"); // 3
	else
		printf("str3 and str4 are not same\n");// 4
	// 最终结果是2和3  
	// 因为str1 和 str2 是两个字符数组  在内存中保存了两份  他们两个根本就没有关联
	// 但是 str3 和 str4 指向的都是一个常量字符串 常量字符串 在内存中不会创建两个内存空间去储存
	// 因此实际上str3 和 str4 这两个指针指向的都是都一个地址

	return 0;
}

2.数组指针变量

2.1数组指针变量是什么

数组指针式一种指针变量,是存放数组地址的指针变量

存放的是这种地址 &arr 代表整个数组的地址

我们来分析一下:

int arr[10] = {0};
int (*p)[10] = &arr;  // 我们需要一个*号去和p结合让他变成指针变量
// 这个的意思就是有个指针变量是p指向了一个10个元素的数组的首元素地址 这个数组每个元素都是int形
// 这个p现在就是数组指针
// 如果没有这个括号 int* p[10] 这个时候p先和[]结合 就是指针数组  放着指针的数组

再来一个例子:

char* arr[5];
char* (*p)[5] = &arr;
总结 (重要!!!!!!!)
  1. 数组指针变量的类型 是根据指向的数组的类型相关的
  2. 数组指针类型是【 类型 ( * )[数组元素数量] = 整个数组地址】(比如 (char* ( * )[5])就是一个数组指针类型)
  3. 需要一个括号来使*和p结合 这样才是指针变量

这里我们可以联系一下之前的 &arr + 1 为什么是跳过一个数组的地址大小呢 也就是+40

因为数组指针类型是 是int ( * )[10] 也就是10个int类型元素的数组 所以+ 1就是跳过一个数组 也就是40个字节

2.2数组指针变量怎么初始化

数组指针变量是用来存放数组地址的 那么怎么初始化呢

就是将数组的地址传给数组指针变量

我们之前学过& 通过这个就可以获得数组的地址

int (*p)[10]  = &arr; // 这个就是初始化

image-20240322161954310

3.二维数组传参的本质

前面我们学习了数组指针变量

我们先来看一下下面这个例子:

// 数组指针变量的运用
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int(*p)[10] = &arr;
	int sz = sizeof(arr) / sizeof(&arr[0]);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", (*p)[i]);// 这个写法并不好  
		// 这里这个(*p)[i]  可以这样理解(*p)== (*&arr)
		// (*&arr) == arr  所以这里其实就是arr[i]
		// 因为p里面放的是整个数组的地址 
	}
	return 0;
}

我们发现上面是在一维数组上运用的数组指针变量 非常的别扭 ,并不是一种好的写法

实际上,数组指针变量 会用在二维数组

在看实例之前我们先要知道一个知识 那就是二维数组传参的实质是什么

首先 我们知道数组名 是数组的首元素地址

那么二维数组的数字名字自然也不例外 也是二维数组的首元素地址

那么二维数组的首元素是什么呢

实际上二维数组的首元素 是一维数组 以一维数组为元素的数组是二维数组

以二维数组为元素的是三维数组 以此类推

因此 二维数组的首元素 实际上就是 第一个一维数组的地址 是一个数组指针

我们来看一个实例

// 数组指针变量, 会在二维数组使用
// 我们知道二维数组传过来的是一个一维数组的数组地址
// 我们就要用 数组指针变量来存放这个一维数组的地址
void test(int (*p)[5], int x, int y)
{
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%d ",(*(p + i))[j]);// *(*(p + i) + j)  +j 表示跳过j个整形元素
			// *(p + i) 代表解引用出一维数组的首元素地址  +i 可以跳过i个数组
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
	test(arr, 3, 5);
	return 0;
}

4.函数指针变量

其实和数组指针变量类似

数组指针 指向数组–存放数组的地址

函数指针-指向函数-存放函数的地址

所以函数指针变量 就是存放函数地址的变量

来看一个实例

// 函数指针变量
// 
// 函数指针变量的写法和数组指针变量的写法很类似
int Add(int x,int y)
{
	return x + y;
}

int main2()
{
	int arr[8] = { 0 };
	int(*pa)[8] = &arr; // 这个是数组指针变量
	int (*pf)(int, int) = &Add;  //这个是函数指针变量
    //  (*pf)代表这是个指针变量  (int, int)代表指向的函数接受两个int 型的参数  int代表指向的函数返回int类型的数据
	int a = 10;
	int b = 20;
	int z = Add(a, b);
	printf("%p\n", &Add);
	printf("%p\n", Add);
	// 函数名和&函数名都代表的函数的地址
}

函数指针变量 提供了一个可行的道路去操作 调用函数

int main()
{
	int (*pa)(int, int) = Add;
	int (*pb)(int, int) = &Add;
	int r1 = (*pa)(3, 7);
	int r2 = pb(3, 7); // 这个*号是可以不写的 因为里面存档的是一个函数地址,不管解引用还是不解引用都是函数地址
	int r3 =  Add(3, 7);
	printf("%d\n", r1);
	printf("%d\n", r2);
	printf("%d\n", r3);

	return 0;
}

*函数指针变量的格式: 函数返回的类型 (指针变量名字)(函数形参) = 函数名/&函数名

我们来看两段有趣的代码

int main()
{
	(* (void (*)() ) 0 )(); // 这其实是一次函数调用
	// 首先void (*) () 是一个函数指针类型  一个不返回任何类型 也不接受参数的函数
	// 用括号括起来(void (*) ()) 表示强制转换  (void (*) ())0 的意思就是将0这个int类型 强制转换成 void (*) ()类型
	// 而0就是这个函数指针变量的名字 相当于int (*pb)(int, int)中的pb
	//(*(void (*) ())0)) 就相当于解引用 相当于这个(*pb)   实际上这个(*)是可以省略的
	// 因此(* (void (*)() ) 0 )();其实就是一次函数调用
	return 0;
}
// 函数声明
// 声明的函数叫 signal
// signal有两个参数 第一个参数类型是int  
// 第二个参数类型是 void(*)(int)  这是一个函数指针类型 该指针可以指向一个函数,这个函数的参数是int 返回类型是void
//  signal函数的返回值是void(*)(int)  的函数指针  
// 也就是void (*signal(int, void(*)(int)))(int) 去掉signal(int, void(*)(int))  剩下的就是它的返回类型
// 按照我们之前的习惯我们可能会这样写
// void(*)(int) signal(int,void(*)(int));  // 实际上这个代码的意思就是函数声明 但是编译器不支持
// 实际上这个会报错  编译器不支持这个写法
void (*signal(int, void(*)(int)))(int);

int main()
{

	return 0;
}

我们发现这种void (*signal(int, void( * )(int)))(int); 函数声明的方式实在是有点难理解了

那有没有更好理解的想法呢 那我们就要学习typedef关键字了

4.1typedef关键字

typedef是用来 类型重命名的 可以将复杂的类型,简单化

比如 有些时候我们觉得unsigned int 太麻烦了 我们就可以用typedef重命名类型名字

typedef unsigned int unit;

int main()
{
	unsigned int um1;
	unit um2;
	return 0;
}

那指针是否可以进行同样操作呢,是可以的

typedef int* pint;
int main()
{
	int* p1 = NULL;
	pint p2 = NULL;

	return 0;
}

我们来看数组指针类型

//typedef int(*)[5] parr_t; // 这种写法是错误的
typedef int(*parr_t)[5];
// 这个时候parr_t  等价于 int(*)[5]  是一个数组指针类型
int main()
{
	int arr[5] = { 0 };
	int (*p1)[5] = &arr;
	parr_t  p2 = &arr;
	return 0; 
}

再来看函数指针类型

// 函数指针重命名
typedef void(*pf_t)(char*); //同样的 这个pf_t 等价于 void(*)(char*)
void test(char* s)
{

}
int main()
{
	void(*p1)(char*) = test;
	pf_t p2 = test;
	return 0;
}

有了上面知识的丰富

我们再回过头来看之前那个代码

typedef void(*pf)(int);
pf signal(int, pf); // 这句代码和下面那个代码的功能是一样的
void (*signal(int, void(*)(int)))(int);

注意

typedef int* pint;
#define PINT int*
int main()
{
	pint p1; // 这里是pint是一个完整的类型就是int*
    PINT p2; // 这里是PINT 替换成int*
    // 上面两个代码的效果是一样的
    pint p1,p2;// 这里的两个变量都是int*类型
    PINT p3,p4;// 这里p3 是指针 p4是指针
    // 这两句话的效果就不一样了
    return 0;
}

5.函数指针数组

我们知道 指针数组是用来专门存放指针的数组

  1. 字符指针数组: char* arr[5]; 存放字符指针
  2. 整形指针数组:int* arr[11]; 存放整形指针

因此 函数指针数组 就是存放函数的地址的数组 (注意是要相同的函数类型 才能放到一个数组里面)

格式 : 函数指针类型 数组名 [元素个数];

我们来看一个实例

int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mid(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}
int main()
{	
	int (*p1)(int, int) = Add;
	int (*p2)(int, int) = Sub;
	int (*p3)(int, int) = Mid;
	int (*p4)(int, int) = Div;
	// 我们发现这四个函数指针变量都是同一个函数指针类型
	// 我们就考虑将其放入函数指针数组里面
	// 但是函数指针数组的 书写方式  和之前的函数声明一样都会不一样
	//int (*)(int, int) arr[4] = { Add,Sub,Mid,Div }; // 按照以前的思路我们会这样写
	// 实际上编译器并不支持 这样子  
	int (*arr[4])(int, int) = { Add,Sub,Mid,Div }; // 这个就是正确的写法  arr就是数组名  [4]就是元素个数
	// 对这个函数指针数组进行使用
	for (int i = 0; i < 4; i++)
	{
		int ret = arr[i](8, 4);
		printf("%d\n", ret);
	}
	return 0;
}

那这个函数指针数组在什么时候会用到呢

我们来看一个实例 (做一个简易的计算器)

// 想写一个计算机器
// 完成两个整数的运算
// 1.加法  2.减法 3.乘法 4.除法 
void menu()
{
	printf("**************************************\n");
	printf("**************简单计算器***************\n");
	printf("******1.Add  2.Sub  3.Mid  4.Div******\n");
	printf("****************0.exit****************\n");
}
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)
{
	return x / y;
}
// 按照我们以前的思路去编写就是像下面这样
int main()
{
	int input = 0;
	int x, y;
	int ret = 0;
	do 
	{
		menu();
		scanf("%d", &input);
		// 但是我们可以发现  我们写的代码有点冗余了
		// 并且后期我们想添加功能,代码也会大量增加
		switch (input)
		{
		case 1:
			printf("请输入两个操作数\n");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("%d\n", ret);
			break;
		case 2:
			printf("请输入两个操作数\n");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("%d\n", ret);
			break;
		case 3:
			printf("请输入两个操作数\n");
			scanf("%d %d", &x, &y);
		    ret = Mul (x, y);
			printf("%d\n", ret);
			break;
		case 4:
			printf("请输入两个操作数\n");
			scanf("%d %d", &x, &y);
			ret = Div(x, y);
			printf("%d\n", ret);
			break;
		case 0:
			printf("一退出计算器\n");
			break;
		default:
			printf("选择错误,请重新选择\n");
			break;
		}

	} while (input);
	return 0;
}

我们可以发现 上面所有函数的类型都是一样的 返回的类型也是一样的 接受的参数也是一样的

并且我们以后拓展功能的时候 还有很多函数类型都是这个类型 int 函数名[int,int]

既然如此我们就可以用函数指针数组来优化这个代码

// 想写一个计算机器
// 完成两个整数的运算
// 1.加法  2.减法 3.乘法 4.除法 
void menu()
{
	printf("**************************************\n");
	printf("**************简单计算器***************\n");
	printf("******1.Add  2.Sub  3.Mid  4.Div******\n");
	printf("****************0.exit****************\n");
}
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)
{
	return x / y;
}
int main()
{
	int input = 0;
	int x, y;
	int ret = 0;
	int (*arr[5])(int, int) = { NULL,Add,Sub,Mul,Div }; // NULL 的原因是 让数组的下标和用户的选择对的上
	menu();
	scanf("%d", &input);
	printf("请输入两个操作数\n");
	scanf("%d %d", &x, &y);
	ret = arr[input](x, y);
	do
	{
		menu();
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数\n");
			scanf("%d %d", &x, &y);
			ret = arr[input](x, y);
			printf("%d\n", ret);
			// 使用函数指针数组优化后  以后即便进行功能优化  代码也不会冗余 只需要在函数指针数组内多放几个函数地址就行了
		}
		else if (input == 0)
			printf("已推出计算器\n");
		else
			printf("输入不合法,请重新输入\n");

	} while (input);
	return 0;
}

其实上述代码的函数指针数组的运用被称作转移表

我们来重温一下之前学的很多有关指针的概念

image-20240323131927609

image-20240323131913369

拓展:(指向函数指针数组的指针)

我们都知道指针本身是有内存空间的 也就是指针本身也是有地址的 那就可以有指针去指向指针

例如

int a = 10;
int* p1 = &a;
int* *p2 = &p1;

这个指针叫做二级指针 我们也学习过

那如果我想把函数指针数组的地址取出来放到一个指针变量中 我们要怎么做呢

char* test(int n, char* s)  //一个函数
{
	
}
// 该函数的函数指针
char* (*pf)(int,char* s) = test; // pf是指针变量名字
// 该函数的函数指针数组
char* (*pfarr[])(int, char* s);
// 那么我们如果想要取出这个函数指针数组的地址 放到一个指针变量中 要怎么书写呢
char* (*(*pfarr)[])(int, char* s)
// (*pfarr) 这个指针指向了一个数组[]  这个数组里面放的元素类型都是char* (*)(int, char* s)

拓展:

我们上面使用了函数指针数组来解决了计算器代码 冗余的问题

那我们其实也可以用函数指针来解决

void menu()
{
	printf("**************************************\n");
	printf("**************简单计算器***************\n");
	printf("******1.Add  2.Sub  3.Mid  4.Div******\n");
	printf("****************0.exit****************\n");
}
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)
{
	return x / y;
}
// 其实我们还有一个思路
// 我们说在switch语句中 的冗余代码 我们可以考虑用函数去代替这个冗余代码
// 在把冗余代码编写成函数的时候我们说 按照以前的思路是不行的
// 如果我们按照以前的思路编写出下面这个函数
//void Calc()
//{
//	int x, y;
//	int ret = 0;
//	printf("请输入两个操作数\n");
//	scanf("%d %d", &x, &y);
//	ret = Add(x, y);
//	printf("%d\n", ret);
//}
// 我们发现 实际上  这个只能解决一个计算 比如加法 或者减法  
// 但是在学习了函数指针数组之后  我们就可以解决这个问题
// 我们将函数指针作为参数传进去  就可以在函数中调用不同的函数了
void Calc(int (*p)(int, int))
{
	int x, y;
	int ret = 0;
	printf("请输入两个操作数\n");
	scanf("%d %d", &x, &y);
	ret = p(x, y); // p代表传进来的函数的地址
	printf("%d\n", ret);
}
int main()
{
	int input = 0;
	int x, y;
	int ret = 0;
	do
	{
		menu();
		scanf("%d", &input);
		//  
		switch (input)
		{
		case 1:
			Calc(Add);
			break;
		case 2:
			Calc(Sub);
			break;
		case 3:
			Calc(Mul);
			break;
		case 4:
			Calc(Div);
			break;
		case 0:
			printf("一退出计算器\n");
			break;
		default:
			printf("选择错误,请重新选择\n");
			break;
		}

	} while (input);
	return 0;
}

ret);
//}
// 我们发现 实际上 这个只能解决一个计算 比如加法 或者减法
// 但是在学习了函数指针数组之后 我们就可以解决这个问题
// 我们将函数指针作为参数传进去 就可以在函数中调用不同的函数了
void Calc(int (*p)(int, int))
{
int x, y;
int ret = 0;
printf(“请输入两个操作数\n”);
scanf(“%d %d”, &x, &y);
ret = p(x, y); // p代表传进来的函数的地址
printf(“%d\n”, ret);
}
int main()
{
int input = 0;
int x, y;
int ret = 0;
do
{
menu();
scanf(“%d”, &input);
//
switch (input)
{
case 1:
Calc(Add);
break;
case 2:
Calc(Sub);
break;
case 3:
Calc(Mul);
break;
case 4:
Calc(Div);
break;
case 0:
printf(“一退出计算器\n”);
break;
default:
printf(“选择错误,请重新选择\n”);
break;
}

} while (input);
return 0;

}


  • 17
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: C 语言中的指针可以说是一个比较难理解的概念,因为它涉及到了计算机内存的操作。如果你不熟悉内存的工作原理,指针的概念可能会比较抽象和难以理解。 此外,指针的概念需要对数据类型、地址、内存分配等方面有一定的了解,而这些知识对于初学者来说可能是比较困难的。 同时,指针的错误使用也很容易导致程序崩溃,因此需要十分小心。 总的来说,C 语言中的指针是一个需要深入理解和练习的概念,但是它是 C 语言编程中很重要的一部分,并且对于提高编程能力有很大的帮助。 ### 回答2: C语言指针之所以被认为难学,是因为它相对于其他编程语言来说更加底层且概念较为抽象。以下是几个可能的原因: 1. 指针与内存管理:C语言中,指针与内存密切相关。指针可以直接访问内存地址,但也需要手动进行内存的分配和释放,否则可能会导致内存泄漏或者悬空指针等问题。 2. 指针算术:C语言允许对指针进行算术操作,如指针加减、指针递增、递减等。这要求学习者掌握指针和内存地址之间的关系,并且需谨慎处理指针算术,以确保不发生内存越界等错误。 3. 多级指针C语言支持多级指针,允许指针指向指针。这增加了学习的难度,尤其是对于初学者来说,理解多级指针的概念和应用可能会比较困难。 4. 字符串处理:在C语言中,字符串是以字符数组的形式存储和处理的。使用指针可以更有效地操作字符串,然而对于初学者来说,指针与字符串的关系可能需要花费更多的时间去理解和掌握。 虽然C语言指针难度较高,但掌握好指针的概念和使用方法对于理解底层编程、内存管理以及某些高级语言的底层原理都具有重要意义。通过坚持学习、阅读指针相关的资料和教程,并进行实践,可以逐渐掌握好C语言指针。 ### 回答3: C语言中的指针是许多初学者感到困惑的一个概念,因为它与其他高级语言的概念不同且较为抽象。以下是指针难以学习的几个原因: 第一,指针是一种直接访问内存地址的方式。这意味着我们需要理解计算机内存的工作原理,并学会如何在程序中正确地使用这些内存地址。这对于没有计算机底层知识的人来说可能会很复杂。 第二,指针有许多复杂的语法规则。例如,我们需要使用星号(*)来声明指针变量,并使用地址运算符(&)来获取变量的地址。此外,我们还需要理解指针的类型匹配规则以及指针算术运算的规则。这些规则对于初学者来说可能会很令人困惑。 第三,指针容易出现错误。由于指针直接访问内存地址,因此在使用指针时出现错误可能导致程序崩溃或产生未定义的行为。例如,当指针没有正确初始化或者指针指向的内存已被释放时,我们可能会遇到段错误或野指针等问题。 第四,指针的概念相对抽象。指针是一种间接引用的方式,通过指针我们可以访问其他变量的值或者修改其他变量的值。这种间接性可能会增加学习的难度,尤其是对于没有编程经验的人来说。 总之,C语言指针之所以难以学习,是因为它需要理解底层的内存访问机制、掌握复杂的语法规则、注意错误的出现以及理解抽象的概念。然而,一旦掌握了指针的概念和用法,它可以成为编写高效、灵活的程序的重要工具。因此,努力学习和理解指针是编程学习的关键。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值