C语言修炼——还不会指针?一次讲明白!第二弹!!

一、数组名的理解

不知道童鞋们有没有看过、写过这样的代码:

int arr[10] = { 0 };
int* p = &arr[0];

这里我们通过&arr[0]拿到了arr数组的第一个元素的地址,但其实通过arr我们也能直接拿到arr数组的第一个元素的地址,因为arr,即数组名就是地址,且一般情况下代表的就是数组首元素的地址
我们来做个测试:

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
 	printf("&arr[0] = %p\n", &arr[0]);//%p以十六进制打印地址
 	printf("arr     = %p\n", arr);
 	return 0;
}

输出结果(x86即32位系统环境下输出结果):
(地址不唯一,向内存申请空间后临时分配)
在这里插入图片描述
这时候有同学会有疑问?数组名如果是数组⾸元素的地址,那下⾯的代码怎么理解呢?

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("%d\n", sizeof(arr));
 	return 0;
}

输出的结果是:40
如果arr是数组⾸元素的地址,那输出结果应该是4或者8才对。(地址在32位环境下有32/8个字节,在64位环境下有64/8个字节)
其实数组名就是数组⾸元素(第⼀个元素)的地址是对的,但是有两个例外:

  • sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩,单位是字节
  • &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)

除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地址。
这个知识点知道以后,可能有好奇的童鞋想问:整个数组的地址和数组首元素的地址的区别在哪里呢?童鞋们可以试试下面的代码:

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
 	printf("&arr[0] = %p\n", &arr[0]);
 	printf("arr     = %p\n", arr);
 	printf("&arr    = %p\n", &arr);
 	return 0;
}

尝试过后,童鞋们可能会发现这三个打印结果都相同,那么到底arr&arr的区别在哪里呢?

#include <stdio.h>
int main()
{
 	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("&arr[0]   = %p\n", &arr[0]);//打印arr[0]的地址
	printf("&arr[0]+1 = %p\n", &arr[0] + 1);//打印arr[0]的地址加1
	printf("arr       = %p\n", arr);//打印arr
	printf("arr+1     = %p\n", arr + 1);//打印arr加1
	printf("&arr      = %p\n", &arr);//打印&arr
	printf("&arr+1    = %p\n", &arr + 1);//打印&arr加1
 	return 0;
}

输出结果:
在这里插入图片描述
对于结果1、2和结果3、4,看过上一篇《C语言修炼——还不会指针?一次讲明白!第一弹!!》中指针运算一节的童鞋应该都知道,&arr[0]&arr[0]+1相差4个字节,arrarr+1相差4个字节,是因为&arr[0]arr都是⾸元素的地址,+1就是跳过⼀个元素。
但是&arr&arr+1相差40个字节,这就是因为&arr是数组的地址,+1操作是跳过整个数组的。到这⾥⼤家应该搞清楚数组名的意义了吧。
数组名是数组⾸元素的地址,但是有2个例外。

二、使用指针访问数组

有了前⾯知识的⽀持,再结合数组的特点,我们就可以很⽅便的使⽤指针访问数组了。

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
 	int sz = sizeof(arr) / sizeof(arr[0]);//数组大小为40,首元素大小为4,得到数组长度
 	int* p = arr;//指针p指向数组首元素的地址
 	//输入
 	for (int i = 0; i < sz; i++)
 	{
 		scanf("%d", p+i);//指针访问数组的方法
 	}
 	//输出
 	for (int i=0; i < sz; i++)
 	{
 		printf("%d ", *(p+i));//指针解引用访问数组元素
 	}
 	return 0;
}

这个代码搞明⽩后,我们再试⼀下,如果我们再分析⼀下,数组名arr是数组⾸元素的地址,可以赋值给p,其实数组名arrp在这⾥是等价的。那我们可以使⽤arr[i]可以访问数组的元素,那p[i]是否也可以访问数组呢?

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
 	int sz = sizeof(arr) / sizeof(arr[0]);
 	int* p = arr;
 	//输入
 	for(int i = 0; i < sz; i++)
 	{
 		scanf("%d", p+i);
 		//scanf("%d", arr+i);//也可以这样写
 	}
 	//输出
 	for(i=0; i<sz; i++)
 	{
 		printf("%d ", p[i]);
 	}
 	return 0;
}

运行后,我们发现,这段代码也可以正常打印,说明指针p和数组名arr本质上是一致的,所以对于数组arr[i]是与arr + i等价的,对于指针p + i是与p[i]等价的。
数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的。所以我们以后在使用数组或者指针的时候,ptr + iptr[i]都是可以的。

三、一维数组传参的本质

数组我们学过了,之前也讲了(C语言修炼——数组与函数全析!!!),数组是可以传递给函数的,这个⼩节我们讨论⼀下数组传参的本质。
⾸先从⼀个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给⼀个函数后,函数内部求数组的元素个数吗?

#include<stdio.h>
void test(int arr[])
{
	int sz2 = sizeof(arr) / sizeof(arr[0]);
	printf("%d\n", sz2);
}
int main()
{
	int arr[10] = { 0 };
	int sz1 = sizeof(arr) / sizeof(arr[0]);
	printf("%d\n", sz1);
	test(arr);
	return 0;
}

代码结果:
在这里插入图片描述
显然,在test函数内部是没有办法求出数组长度的。为什么呢?
在前面,我们已经说过,除了两种特殊情形外,数组名在一般情况下代表的是数组首元素的地址。
在此处,test(arr)中传递的参数arr就是arr数组首元素的地址。所以我们在test函数内部得到的形参arr实际上代表的是arr数组首元素的地址,所以我们在计算的时,实际上是拿地址的大小除以int类型的大小(地址的大小在x86(32位)环境下是4个字节,在x64(64位)环境下是8个字节),此处是x86环境,所以第二行的结果是1,如果在x64环境下则结果为2。
所以我们可以得到结论:

void test(int arr[])

等价于

void test(int* arr)

即参数写出数组形式,但其本质上还是指针。
总结:
一维数组传参,形参可以写成数组形式,也可以写成指针形式。

四、冒泡排序

下面我们借用指针的知识来写个冒泡排序熟悉一下所讲的知识。

#include<stdio.h>
void bubble_sort(int* arr, int sz)
{
	for (int i = 0; i < sz - 1; i++)
	{
		int flag = 1;//假定数组有序,flag为1
		for (int j = 0; j < sz - 1 - i; j++)
		{	
			//当if语句一次也不执行,数组已经有序
			if (arr[j] > arr[j + 1])
			{
				flag = 0;
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
		if (flag == 1)//数组有序,提前跳出循环
			break;
	}
}
int main()
{
	int arr[] = { 0, 8, 9, 6, 5, 2, 1, 4, 3, 11 };
	int sz = sizeof(arr) / sizeof(arr[0]);//计算数组长度
	printf("排序前:\n");
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);//也可以使用*(arr+i)的形式
	}
	bubble_sort(arr, sz);//传递arr数组首元素的地址和数组长度
	printf("\n排序后:\n");
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);//也可以使用*(arr+i)的形式
	}
	return 0;
}

在这里插入图片描述
这里的冒泡排序函数bubble_sort,我在这里做了一个优化,我们容易知道,对于数列1 0 2 3 4 5 6 7 8 9和数列9 8 7 6 5 4 3 2 1 0两者的“无序程度”是不同的(这里单就从小到大排序而言),所以对于前者,无序程度小的数列,我们需要判断数列是否有序,以避免无意义的循环判断并提前跳出排序,在这里flag就起到了记录数列是否有序的作用,我们假定数列有序,如果执行了一次if语句就代表数列无序,故令flag = 0;如果一次if语句都没有执行,就容易知道,数列已经有序,我们可以判断后面的循环与判断都无意义,flag仍为1,进入if (flag == 0)语句内,执行breka跳出循环,提前结束排序。
总结:
我们需要准确理解指针、地址、数组名之间的关系,三者在一定意义下是等价关系的。

五、二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪⾥?
这就得讲到二级指针了。
在这里插入图片描述

在这里插入图片描述
假设存储变量a、一级指针变量pa、二级指针变量paa的内存空间地址分别为0x00efca0c0x00efca000x00efcc00(无实际意义),那么很容易知道,a的内存空间中存储的为10pa的内存空间中存储的为a的地址,paa的内存空间中存储的为pa的地址。
对于二级指针的运算有:

  1. *ppa通过对ppa解引用得到ppa内存空间中存储的pa的地址,找到pa , 即*ppa 得到指针变量pa
  2. **ppa先通过*ppa得到指针变量pa,然后对pa进⾏解引⽤操作:*pa ,得到变量a

六、指针数组

我们知道,整型有:short,int,long,long long;字符型有:char;浮点型有:float,double;布尔型有:bool。整型、字符型的各个类型还有有无符号之分,即signedunsigned,同时这些类型都可以作为数组元素的类型创建数组。
我们需要知道指针变量也是变量,只是因为这个变量的类型是指针类型,所以我们特别称呼它为指针变量而已。上一篇《C语言修炼——还不会指针?一次讲明白!第一弹!!》2.2.2节中我们已经讲过了如何拆解指针类型,而指针类型有哪些也都是建立在前面我们已知的类型之上。
这一part的重点就在于如何理解指针数组,数组指针到底是指针还是数组?
其实我们类比一下就知道了,整型数组,是存放整型变量的数组;字符数组,是存放字符变量的数组。
那指针数组呢?存放指针变量的数组。

int arr[5] = { int, int, int, int, int };//整型数组
char arr[5] = { char, char, char, char, char };//字符数组
int* arr[5] = { int*, int*, int*, int*, int* };//整型指针数组

容易知道,指针数组的每个元素都是用来存放地址(指针)的。指针数组的每个元素都是地址,同时又指向一块内存空间。

七、指针数组模拟二维数组

我们在之前的篇章中(C语言修炼——数组与函数全析!!)已经知道,数组在内存中都是连续存放的,二维数组当然也是。
在这里插入图片描述

int arr[3][3] = { };

当我们直接创建一个二维数组时,该数组在内存中的存储如上图所示,连续存储且arr[0],arr[1],arr[2]相当于二维数组中存储的对应的一维数组数组名。

之前说过,如果我们把⼀维数组做为数组的元素,这时候就是⼆维数组,这就与二级指针联系起来了。显然二维数组中存储的是一维数组,那每个元素,即每个一维数组应该也有自己的地址。由此,我们可以借用指针数组,存储每个一维数组的首元素的地址,通过指针数组结合指针运算与解引用操作,我们既可以找到需要的一维数组,也可以找到对应一维数组中的所有元素。

//arr1,arr2,arr3为对应一维数组首元素地址,类型为int*
int* parr[3] = { arr1, arr2, arr3 };

代码演示:

#include <stdio.h>
int main()
{
   int arr1[] = { 1, 2, 3, 4, 5 };
   int arr2[] = { 2, 3, 4, 5, 6 };
   int arr3[] = { 3, 4, 5, 6, 7 };
   //数组名是数组⾸元素的地址,故可将数组名理解为指针变量名
   //且该指针指向的是该数组所在空间,类型是int*
   //int*指针类型拆解:
   //	int* arr,'*'结合'arr'代表arr变量是指针变量
   //	int代表该指针变量指向一个整型变量
   int* parr[3] = { arr1, arr2, arr3 };
   for (int i = 0; i < 3; i++)
   {
   	for (int j = 0; j < 5; j++)
   	{
   		printf("%d ", parr[i][j]);
   	}
   	printf("\n");
   }
   return 0;
}

需要注意,指针数组模拟出来的二维数组本质上并不是真正的二维数组,因为任何数组在内存中存储都是连续的,而指针数组模拟的二维数组中的每个元素—一维数组的存储并不连续。

八、字符指针变量

在指针类型中我们知道有一种指针叫作字符指针char*
一般情况下使用:

int main()
{
	char ch = 'w';
 	char* pc = &ch;
 	*pc = 's';
 	return 0;
}

还有一种使用方法如下:

int main()
{
	//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?
 	const char* pstr = "hello world.";
 	printf("%s\n", pstr);
 	return 0;
}

其实这里非常容易让童鞋们误会。const char* pstr = "hello world."本质上其实是把字符串"hello world."首字符的地址放到了pstr中。

《剑指offer》中收录了⼀道和字符串相关的笔试题,我们⼀起来学习⼀下:

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

在这里插入图片描述
看出名堂的童鞋不要急,没看出来的童鞋也不要慌。
我们来分析一下:
首先,char str1[]char str2[]很明显是两个字符数组,我们不要被两个数组存储的相同的字符所迷惑,因为本质上我们是向内存申请分配了两块空间,创建了两个数组,但刚好两块空间存储的字符串一致而已。
但是,对于const char*类型的两个字符指针变量str3str4而言,

const char* str3 = "hello world.";
const char* str4 = "hello world.";

这两段语句所做的都是将字符串hello world.首字符的地址存储到str3str4中,所以两者最终指向的都是同一片空间。
因此,很明显,我们可以知道,str1str2!=的,而str3str4==的。

九、数组指针变量

9.1 数组指针变量是什么?

前面我们学习了指针数组,指针数组是⼀种数组,数组中存放的是地址(指针)。
那么数组指针变量是指针变量?还是数组?
答案是:指针变量。
我们已经熟悉:

  • 整形指针变量:int* p, 存放的是整型变量的地址,能够指向整型数据的指针。
  • 浮点型指针变量:float* p,存放浮点型变量的地址,能够指向浮点型数据的指针。

那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
下⾯代码哪个是数组指针变量?

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

思考⼀下:p1, p2分别是什么?
数组指针变量:

int (*p)[10];

解释:p先和*结合,说明p是⼀个指针变量变量,然后指着指向的是⼀个⼤⼩为10个整型的数组。所以p是⼀个指针,指向⼀个数组,叫数组指针。
这⾥要注意:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。
所以我们可以知道上面的p1,p2分别是指针数组、数组指针。

9.2 数组指针变量的初始化

数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址呢?就是我们学习过的&数组名 。

int arr[10] = { 0 };
int (*p)[10] = &arr;//得到数组的地址

&arrp的类型是完全一致的,都为int[10] *

数组指针类型解析:

int (*p) [10] = &arr;
 |    |    |
 |    |    |
 |    |    p指向数组的元素个数
 |    p是数组指针变量名
 p指向的数组的元素类型

十、二维数组传参的本质

有了数组指针的理解,我们就能够讲⼀下⼆维数组传参的本质了。
过去我们有⼀个⼆维数组的需要传参给⼀个函数的时候,我们是这样写的:

#include <stdio.h>
void test(int a[3][5], int r, int c)
{
	for (int i = 0; i < r; i++)
	{
		for (int j = 0; j < c; j++)
		{
			printf("%d ", a[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} };
	test(arr, 3, 5);
	return 0;
}

这⾥实参是⼆维数组,形参也写成⼆维数组的形式,那还有什么其他的写法吗?
⾸先我们再次理解⼀下⼆维数组,⼆维数组其实可以看做是每个元素是⼀维数组的数组,也就是⼆维数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。
所以,根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是int [5],所以第⼀⾏的地址的类型就是数组指针类型int(*)[5]。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀⾏这个⼀维数组的地址,那么形参也是可以写成指针形式的。
如下:

#include <stdio.h>
void test(int (*p)[5], int r, int c)
{
	for (int i = 0; i < r; i++)
	{
		for (int j = 0; j < c; j++)
		{
			printf("%d ", *(*(p + 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} };
	test(arr, 3, 5);
	return 0;
}

总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。

十一、函数指针变量

11.1 函数指针变量的创建

通过前面学过的整型指针,字符指针,数组指针,我们很容易知道,所谓的函数指针变量不过是存储函数的地址的指针罢了。
有的童鞋可能有疑惑了,怎么函数也有地址呢?

我们可以测试一下这段代码:

#include <stdio.h>
void test()
{
	printf("hehe\n");
}
int main()
{
 	printf("test: %p\n", test);
 	printf("&test: %p\n", &test);
 	return 0;
}

结果:
在这里插入图片描述
可以看到,确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过&函数名的⽅式获得函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量,函数指针变量的写法其实和数组指针⾮常类似。
如下:

void test()
{
	printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;//加不加&皆可
int Add(int x, int y)
{
 	return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//形参的变量名x、y写上或者省略都可以

函数指针类型解析:

int (*pf3) (int x, int y)
 |     |    ------------
 |     |          |
 |     |     pf3指向的函数 的参数类型和个数的交代
 |   函数指针变量名
 pf3指向的函数 的返回类型
int (*) (int x, int y) //pf3函数指针变量的类型

11.2 函数指针变量的使用

通过函数指针调⽤指针指向的函数:

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*pf3)(int, int) = Add;
	printf("%d\n", (*pf3)(2, 3));
	printf("%d\n", pf3(3, 5));
	return 0;
}

结果:
在这里插入图片描述

十二、两段有趣的代码

代码1:

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

我们一层一层的剥开来看:

*(void (*)())0

一个数字0的前面有一个括号和一个*,0代表什么呢?
我们知道十六进制的地址一般是这种形式0x00ef00ca,那么0可不可以是地址呢?
如果这样一想,这里是不是很像强制类型转换呢?没错,0的类型为整型,这里实际上是将0当作地址强制类型转换成函数指针类型void (*)(),然后通过*进行解引用访问,而徘徊在左边括号外,靠近分号的这个括号是不是也很容易就可以知道它是我们平时用来传参的括号呢。
所以这一条语句其实是一条函数调用语句,意思是在0这个地址上存储着一个返回值类型为void,参数为空的函数(当然0这个地址是不能访问的,所以这是一条错误语句)。

代码2:

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

这个该如何理解呢?我们可以知道signal肯定是个符号,是个名称,再结合signal后面的括号,我们可以判断signal一定是个函数,而( int, void(*)(int) )中的intvoid(*)(int)都是形参。
不知道大家还记不记得函数指针变量的创建呢?
是不是void (*p)()int (*p)(int, int)呢?再一结合signal函数的返回值类型,我们是不是恍然大悟呢?
原来这是一条函数声明语句,不规范的表示一下

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

这样是不是就很容易理解了呢?所以这条语句本质上是创建了一个signal函数,函数的形参为整型int与函数指针类型void(*)(int),返回值类型为函数指针类型void(*)(int)
两段代码均出⾃:《C陷阱和缺陷》这本书。有兴趣的同学建议可以搜来看看。

12.1 typedef关键字

typedef是⽤来类型重命名的,可以将复杂的类型,简单化。
我们通常是不会直接用前面那么复杂的类型的,毕竟这非常不方便且可读性比较差。这时我们就用到了typedef了。

typedef void(*pfun_t)(int);//重定义类型名称
pfun_t signal(int, pfun_t);//直接用重定义的名称创建该类型变量或函数

十三、函数指针数组

数组是⼀个存放相同类型数据的存储空间,我们已经学习了指针数组,
比如:

int *arr[10];
//数组的每个元素是int*

那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[3])();//是它吗?
int *parr2[3]();//还是它?
int (*)() parr3[3];//或者是它?

答案是:parr1
parr1先和[]结合,说明parr1是数组,数组的内容是什么呢?是int (*)()类型的函数指针。

十四、转移表

那么函数指针数组有什么用途呢?
函数指针数组的⽤途:转移表。
举例:计算器的⼀般实现

#include <stdio.h>
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("***  0:exit           ***\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;
}

这是比较繁杂的写法,但我们现在学了函数指针数组,我们可以发现case1,case2,case3,case4其实是非常相似的,

printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;

其内在逻辑都是输入操作数——调用函数得到返回值——打印并break。我们是否可以用函数指针数组简化一下这段代码呢?

#include <stdio.h>
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;
 	int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
	do
	{
		 printf("*************************\n");
		 printf(" 1:add 2:sub \n");
		 printf(" 3:mul 4:div \n");
		 printf(" 0:exit \n");
		 printf("*************************\n");
		 printf( "请选择:" );
		 scanf("%d", &input);
		 if ((input <= 4 && input >= 1))
		 {
		 printf( "输⼊操作数:" );
		 scanf( "%d %d", &x, &y);
		 ret = (*p[input])(x, y);
		 printf( "ret = %d\n", ret);
		 }
		 else if(input == 0)
		 {
		 printf("退出计算器\n");
		 }
		 else
		 {
		 printf( "输⼊有误\n" );
	 	 }
	 }while (input);
 	 return 0;
}

  • 33
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: 好的,下面是对c语言实验——一元二次方程Ⅰ的回复: 这个实验主要是让学生通过c语言来实现一元二次方程的求解。一元二次方程的一般形式为ax²+bx+c=,其中a、b、c为已知数,x为未知数。求解一元二次方程的公式为x=(-b±√(b²-4ac))/(2a)。 在c语言中,我们可以通过输入a、b、c的值,然后利用公式计算出x的值。具体实现可以使用scanf函数来输入a、b、c的值,然后使用sqrt函数来计算平方根,最后使用printf函数输出x的值即可。 需要注意的是,当b²-4ac小于时,方程无实数解,需要进行特殊处理。此外,当a为时,方程不是一元二次方程,也需要进行特殊处理。 希望以上回复能够对您有所帮助。 ### 回答2: 题目要求我们使用C语言编写求解一元二次方程的程序,计算出方程的解并输出。一元二次方程的标准形式为:ax²+bx+c=0,其中a,b,c均为已知系数,我们需要求解x的值。 解一元二次方程的一般公式一般为x = (-b ± √b²-4ac) / 2a。在实现这个公式的时候需要注意以下几点: 1. 公式中存在一些需要计算的中间数值,比如b²-4ac和2a,需要提前计算好并存储。 2. 当方程无解或有且仅有一个实根时需要特判处理,否则程序会出现错误结果。 3. 输入系数值时需要判断是否输入的为数字,避免程序因为输入错误而崩溃。 以下是一个实现求解一元二次方程的C语言程序示例: ```c #include <stdio.h> #include <math.h> int main() { double a, b, c, delta, x1, x2; printf("请分别输入一元二次方程的三个系数:"); if(scanf("%lf%lf%lf", &a, &b, &c) != 3) { printf("输入错误,请重新输入!"); return -1; } delta = b * b - 4 * a * c; if(delta < 0) printf("此方程无实数解"); else if(delta == 0) printf("此方程有唯一实根:%lf", - b / (2 * a)); else { x1 = (-b + sqrt(delta)) / (2 * a); x2 = (-b - sqrt(delta)) / (2 * a); printf("此方程有两个实根,分别为:%lf和%lf", x1, x2); } return 0; } ``` 以上程序通过输入三个系数求解出方程的解,并根据不同的情况输出结果,实现了求解一元二次方程的功能。 ### 回答3: 这个实验要求我们用C语言编写一个解一元二次方程的程序。在正式开始编程前,我们需要了解一元二次方程的基本形式以及解法。 一元二次方程的一般形式为:ax² + bx + c = 0 其中,a,b,c为常数,x为未知数。解一元二次方程的方法有多种,常见的有配方法、公式法和图像法等。在本实验中,我们采用公式法。 公式法的原理是:当ax² + bx + c = 0(a ≠ 0)时,方程的解为x = (-b ± √(b² - 4ac)) / 2a。 在编写程序时,我们需要考虑到以下几点: 1. 用户输入的系数a,b,c可能为浮点型,因此需要用float或double类型来存储。 2. 在计算中,涉及到开方和除法运算,需要用到math.h头文件中的函数。 3. 当判别式(b² - 4ac)小于0时,方程无实数解,需要进行特殊处理并给出提示。当判别式等于0时,方程有两个相等的实数根。当判别式大于0时,方程有两个不等的实数根。 4. 在输出时,需要注意格式化输出,尽可能准确地显示根的值。 根据上述要点,我们可以开始编写程序,具体实现方法可以参考以下代码: #include <stdio.h> #include <math.h> int main() { float a, b, c, delta, x1, x2; // 提示用户输入系数 printf("请分别输入一元二次方程的系数a、b、c:\n"); scanf("%f%f%f", &a, &b, &c); // 计算判别式 delta = b * b - 4 * a * c; if (delta < 0) { printf("方程无实数解!\n"); } else if (delta == 0) { x1 = x2 = -b / (2 * a); printf("方程有一个实数根:x = %.2f\n", x1); } else { x1 = (-b + sqrt(delta)) / (2 * a); x2 = (-b - sqrt(delta)) / (2 * a); printf("方程有两个实数根:x1 = %.2f,x2 = %.2f\n", x1, x2); } return 0; } 通过运行以上程序,我们可以得到一个解一元二次方程的可执行文件,并得到方程根的输出结果。这个实验不仅让我们掌握了解一元二次方程的方法,更让我们深入理解了C语言的基本语法和编程思想。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

David猪大卫

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

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

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

打赏作者

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

抵扣说明:

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

余额充值