函数
1. 函数的定义及调用
函数是C程序的构建块; 每个函数本质上都是一个自带声明和语句的小程序; 通过函数可以将程序划分功能, 便于理解修改和复用
1.1 定义函数
-
函数是由 返回值类型, 数名, 形式参数或void, 花括号{}, 以及函数体 构成的
-
函数不能返回数组, 对于别的返回值类型没有其他限制
-
指定位void的类型即函数没有返回值
-
如果省略了返回值类型, 那么C89会假定返回值类型位int型, C99中是不合法的
1.2 调用函数
-
函数调用就是通过函数名和其后面紧跟的小括号以及小括号里面的参数(无参函数括号里面为空)构成
-
可以通过函数什么的返回值类型的一个变量来接受非void函数的返回值
2. 函数声明
- 函数声明解决的是定义的函数出现在调用之后, 编译器就会为该函数创建一个隐式声明(implicit declaration), 且编译器并不知道调用的函数的实参个数和参数类型, 只能进行默认的实际参数提升; 当编译器遇到定义的函数时, 如果函数的返回类型和默认的不一致, 就会出现一条错误信息
- 解决的办法就是调用前先声明函数, 即定义好函数的返回类型, 函数名, 以及形参的类型, 不需要定义形参的名以及函数体, 只需给出返回类型, 参数类型以及函数名; 如: double average(double, double);
3. 实际参数
参数传递: C语言中实际参数是通过值传递的: 调用函数时, 计算出每个实际参数的值并且把它赋值给相应的形式参数; 在函数执行过程中, 对形式参数的改变不会影响实际参数的值, 这是因为形式参数中包含的是实际参数值的副本
3.1 实际参数的转换
C语言允许在实际参数的类型与形式参数的类型不匹配的情况下进行函数调用
- 编译器在调用前遇到原型
- 就像赋值一样, 每个实际参数的值的被隐式地转换成相应形式参数的类型
- 如: 期望得到double类型, 但是实际传递的是int类型, 那么实际参数会被自动转换成double类型
- 编译器在调用前没有遇到原型
- 编译器执行**
默认的实际参数提升
** - 把float类型实际参数转换成double类型
- 执行整值提升(C89), 把char类型和short类型的实际参数转换成int类型
- 所以最好的就是在调用函数之前先声明函数
- 编译器执行**
3.2 数组型实际参数
- 当一维数组作为形式参数时, 可以不说明数组的长度; 而实际参数可以是元素类型正确的任何一维数组
- 一种书上的用法是: 当传递第一个形参是数组的时候, 可以在第二个参数传递数组的长度; 如: function1(int arr[], int len);
- 函数无法检测传入的数组长度的正确性; 所以可以传入比声明的数组的长度小的值
- 函数可以改变数组型形式参数的元素, 且改变会在相应的实际参数中体现出来
- 如果形式参数是多维数组, 声明参数的时候只能省略第一维数组的长度
3.3 变长数组形式参数
- 变长数组就是在指定数组的长度时, 不像之前一样指定一个常量, 而是可以使用非常量(变长数组的长度可以是任何表达式)来作为数组的长度, 具体的长度则由编译器运行时自动计算; 变长数组也可以作为形式参数
- 如**声明一个函数的形参为变长数组: int suma(int n, int arr[n]);**这里数组的长度n, 要出现在变长数组的前面, 否则程序会报错
- 一维变长数组形式参数的几种声明形式
- int suma(int n, int arr[n]); 跟函数一样定义
- int suma(int n, int arr[*]); 使用*来取代数组的长度
- int suma(int , int [*]); 省略掉变量名以及数组名
- 二维变长数组形式参数的几种形式
- int sumb(int n, int m, int arrb[n][m])
- int sumb(int n, int m, int arrb[*][*])
- int sumb(int n, int m, int arrb[][m])
- int sumb(int n, int m, int arrb[][*])
3.4 在数组参数声明中使用static
- C99 允许在数组中使用关键字 static; 将static关键字放在数组长度前面, 可以保证该数组的长度至少可以保证是static修饰的长度; 如: int func(int arr[static 2], int n)
- static 只是起到一个提示编译器的作用, C编译器可以据此生成更快的指令来访问数组; (如果编译器知道数组总是具有某个最小值, 那么它可以在函数调用时预先从内存中去除这些元素值, 而不是遇到函数内部实际需要用到这些元素的语句时才取出相应的值)
- 如果数组参数是多维的, 那么static关键字只能修饰第一维; 如: int sumc(int arrc[static 3][5]);
3.5 复合字面量
- 复合字面量是通过指定其包含的元素而创建的没有名字的数组; 如调用形参为一维数组的函数: func((int []){3, 1, 4, 0, 5}, 5); (我理解: 就行Java需要一个Person类型实例, 你可以定义一个Person类型的变量, 初始化, 然后将变量传给需要的, 其实也可以直接在需要的地方 new Person(); 这样来解决)
- 复合字面量的格式: 现在一对圆括号内给定类型名, 随后在一堆花括号内设定所包括的元素
- 复合字面量和初始化式遵循同样的规则, 符合字面量可以包含初始化式, 就像指定初始化式一样; 并且符合字面量可以包含任意的表达式, 如: func((int []){2*i, i+j, j*k}, 3)
- 复合字面量为左值
(左值就想变量(其值可变), 右值就想常量(其值不可变))
, 所以其元素的值可以改变, 可以通过 const 关键字来将其值为 "只读"; 如: (const int []){5, 4}
4. return 语句
-
return 用于非void的函数, 且返回的只能是常量或变量
-
如果return语句中表达式的类型和函数的返回类型不一致的时候, 系统会把表达式的类型隐式的转换成返回的类型
-
void 函数也可以出现 "return; "
5. 程序终止
main 函数返回的值是状态码, 在某些操作系统中程序终止时可以检测到状态码; main函数返回0表示程序正常终止, 而返回非0值则表示程序异常终止
5.1 exit 函数
-
exit(); 属于 <stdlib.h>头, 也是表示程序终止的一种方法; exit 和 main函数的返回值都用于说明程序终止时的状态
-
由于0有点模糊, 所以 <stdlib.h> 头中宏定义了 EXIT_SUCCESS 和 EXIT_FAILURE; 通常分别是0和1
-
return 和 exit 的异同:
- 无论哪个函数调用exit函数都会导致程序终止; 而return语句只有当main函数调用的时候才会导致程序终止
- exit 函数可以一招全部退出, 以便更容易定位程序中的全部退出点
6. 递归
递归: 反正就记得是调用自己; 而且必须有可以让递归停止的条件
这里是大佬的博客, 看完可以透彻理解递归
分治法: 分治顾名思义“分而治之”,英文的意思翻译为“分割并征服”; 分治思想就是将原问题分解成与“原问题相同但是规模更小”的子问题,并可以反复执行这个过程,使得问题规模减小到可以求解为止
递归和分治法一毛钱关系都没有, 递归就好像是一个循环, 而分治法是一种思想; 递归经常作为分治法(divide-and-conquer)的结果出现
6.1 快速排序算法
分治法的经典示例就是流行的排序算法--快速排序(quicksort); 快速排序通过找分割元素(即找到该元素的正确位置)不断的分割数组, 分割的数组也再找分割元素不断的递归调用, 是的数组的每个元素找到它的正确位置
快速排序思想
- 先从数组中取出一个元素作为基准元素("分割元素"); 然后重新排列数组是的分割元素左边的元素都小于或等于它, 分割元素右边的元素都大于或等于它
- 递归的采用快速排序, 对分割元素左边和右边的数组进行排序; (导致的结果是到最后分割到数组只包含一个元素时, 即每个元素都找到了自己的位置, 则数组也排序了)
示例
# include <stdio.h>
// 宏定义数组的长度为10
# define N 10
// 函数声明
void quicksort(int a[], int low, int high);
int split(int a[], int low, int high);
int main(void) {
int a[N], i;
printf("Enter %d numbers to be sorted: ", N);
// for 循环将scanf每次扫描的一个int类型的值作为a数组的元素值
for (i = 0; i < N; i++)
scanf("%d", &a[i]);
// 调用快速排序的函数
quicksort(a, 0, N - 1);
// 打印输出排序后的数组
printf("In sorted order: ");
for(i = 0; i < N; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}
/*
* 快速排序函数
* 接受一个数组, 以及两个int类型的值, 无返回值
*/
void quicksort(int a[], int low, int high) {
int middle;
if (low >= high) return;
// 获取分割元素
middle = split(a, low, high);
// 递归调用快速排序函数对分割元素左边的数组排序
quicksort(a, low, middle-1);
// 递归调用快速排序函数对分割元素右边的数组排序
quicksort(a, middle+1, high);
}
/*
* 获取分割元素的函数
* 接受一个数组, 以及两个int类型的值, 返回的是分割元素
*/
int split(int a[], int low, int high) {
// 首先将a[low]的值赋给一个临时变量
int part_element = a[low];
for(;;) {
// 判断 low < high 并且 part_element 的值小于a[high]
while (low < high && part_element <= a[high])
// 满足上面条件的话, 将high的位置左移一个位置
high--;
// 如果 low >= high 则跳出循环
if (low >= high) break;
// 否则, a[low++]位置的值就是 a[high], 刚好比a[low] 大1
a[low++] = a[high];
// 判断 low < high 并且 part_element 的值大于等于 a[low]
while(low < high && a[low] <= part_element)
// 满足上面条件的话, 将low的位置右移一个位置
low++;
// 如果 low >= high 则跳出循环
if (low >= high) break;
// 否则, a[high--]位置的值就是 a[low], 刚好比a[high] 小1
a[high--] = a[low];
}
// 最后, high 位置的元素的值就是分割元素
a[high] = part_element;
// 最后返回high的值
return high;
}