在前面的例子中,我们已经用过了最常见的函数:主函数,其实在C语言还可以灵活地根据需要定义自己的函数,而在C++中,函数类型会更加地丰富。在面向对象的语言中,经常会有方法这个概念,看着比较像,其实作用也是基本一样的,都是对一段代码的抽象。只不过函数是直接传值的,而方法是直接处理对象上的数据,其依赖类或者对象,不能独立存在。
按照惯例,先来看看本章的知识框架(思维导图)。
思维导图
7.1函数定义
在书中并没有对函数比较通俗的定义,其实要说通俗的定义应该就是对某一段代码的抽象,也就是说我们若是想要实现某个功能,就将这些代码写到一起,然后通过语法和其他符号,就构建了一个函数,然后以后若是想反复使用这段代码,直接调用即可。函数定义的语法如下。
类型
函数名(形式参数)
代码块
另外有一个概念需要清楚:如果函数无需向调用程序返回一个值,它就会被省略。这类函数在绝大多数其他语言中被称为过程。这个概念在我学习《操作系统》过程中有提及。
7.2函数声明
一般函数都会有函数声明和函数实现。函数原型和函数声明几乎没有什么区别。因为C语言一般都是从main函数开始执行的,而在调用某函数的时候,我们告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。否则程序就无法顺利运行,因而函数声明应运而生。比方说下面的程序。
#include <stdio.h> size_t strlength(char *string); int main() { int res = 0; char a[] = "qwer"; res = strlength(a); if (res) printf("The string length is %d\n",res); else printf("The string length is zero!\n"); system("pause"); return 0; } size_t strlength(char *string) { int length = 0; while (*string++ != '\0') length++; return length; }
可以看到在程序的一开始就对strlength
函数进行了声明。
7.3函数的参数
函数的参数按照通俗的分类,会分为形参和实参,顾名思义,形参就是“形式”上的参数,实参就是实际调用的时候传入的参数。在C语言中,一般传入的都是实际值的一份拷贝,注意是拷贝。
而参数若是数组,则一般会传入数组第一个元素的地址,这个行为被称为“传址调用” 。举个例子。
#include <stdio.h> void strexchange(char a[]); int main() { int res = 0; char a[] = "wwww"; strexchange(a); for(int i = 0; i < 4; i++) printf("The string elements is %c\n",a[i]); system("pause"); return 0; } void strexchange(char a[]) { a[0] = 'q'; }
打印输出
可以看到,在strexchange函数中,就可以完成对字符数组第一个元素的修改。所以肯定是“传址调用”。
7.4ADT和黑盒
书中有这样的描述:
C可以用于设计和实现抽象数据类型(ADT,abctract data type),因为它可以显示函数和数据定义的作用域,这个技巧也可以被称为黑盒(black box)设计。
其实也比较好理解,也就是说,有时候我们在开发中,并不需要知道函数的具体实现过程,只是单纯想调用它来实现相应的功能。
通过static
关键字可以限制在其他文件中直接对其进行访问。所以在其他文件中,只需要关注其功能即可,而不需要关注其体具体的实现过程。
书上有了一个例子,后经过了些许补充:
创建addrlist.h
文件,编写如下程序:
#pragma once #define NAME_LENGTH 20 //姓名最大长度 #define ADDR_LENGTH 100 //地址最大长度 #define PHONE_LENGTH 11 //电话号码最大长度 #define MAX_ADDRESSES 1000 //地址个数限制 void data_init(); char const *lookup_address(char const *name); char const *lookup_phone(char const *name);
创建addrlist.c
文件,编写如下程序:
#include "addrlist.h" #include <stdio.h> #include <string.h> static char name[MAX_ADDRESSES][NAME_LENGTH]; static char address[MAX_ADDRESSES][ADDR_LENGTH]; static char phone[MAX_ADDRESSES][PHONE_LENGTH]; static int find_entry(char const *name_to_find) { int entry; for (entry = 0; entry < MAX_ADDRESSES; entry++) if (strcmp(name_to_find, name[entry]) == 0) return entry; return -1; } //给定一个名字,找到对应的地址,如果找不到,则返回空指针 char const *lookup_address(char const *name) { int entry; entry = find_entry(name); if (entry == -1) return NULL; else return address[entry]; } char const *lookup_phone(char const *name) { int entry; entry = find_entry(name); if (entry == -1) return NULL; else return phone[entry]; } void data_init() { char name_1[NAME_LENGTH] = "zhangsan"; for (int i = 0; i < NAME_LENGTH; i++) { name[0][i] = name_1[i]; } char address_1[ADDR_LENGTH] = "shanghai/zhangjiang"; for (int i = 0; i < ADDR_LENGTH; i++) { address[0][i] = address_1[i]; } }
在main.c
中编写如下的代码:
#include "addrlist.h" #include <stdio.h> #include<stdlib.h> int main() { static char find_addr[MAX_ADDRESSES] = "zhangsan"; char const *addr_res = NULL; //数据初始化 data_init(); addr_res = lookup_address(find_addr); if (addr_res == NULL) printf("^-^"); else { for (int i = 0; i < ADDR_LENGTH; i++) { if (addr_res[i] != 0) printf("%c", addr_res[i]); else break; } } }
运行,打印输出:
可以看到,当我们输入张三的时候,直接查到了张三的住址:上海张江。而此时在main.c文件中,我们并不知道具体的查询过程。所以这样就起到了封装的效果。
7.5 递归
递归是一种非常重要的编程思想,直观地说,就是函数自己调用自己。
关于递归,书上有这样一段描述:
一旦你理解了递归,阅读递归函数最容易的方法不是纠缠它的执行过程,而是相信递归函数会顺利完成它的任务,如果你的步骤正确无误,你的限制条件设置正确,并且每次调用之后更接近限制条件,递归函数总能正确地完成任务。
最最经典的例子当属斐波那契数列了,程序如下:
int fibonacci(int const n) { int sum = 0; if (n == 0) return 0; if (n == 1 || n == 2) return 1; return fibonacci(n - 1) + fibonacci(n-2); }
当然,我们也可以写一个非递归版本的,只是稍微复杂一些:
int fibonacci(int const n) { int f1 = 1, f2 = 1, f3 = 0; if (n == 0) return 0; if (n == 1 || n == 2) return 1; for (int i = 3; i <= n; i++) { f3 = f1 + f2; f1 = f2; f2 = f3; } return f3; }
开始可能会觉得非递归版本好理解一些,但是习惯了之后,会发现递归版本的更加方便。而且在有的时候,使用递归要比循环容易得多,比方说下面这个例子:
- 各位相加
给定一个非负整数
num
,反复将各个位上的数字相加,直到结果为一位数。返回这个结果。
如果我们用非递归的方法,可能比较难解决,其中一种解决思路如下:
int addDigits(int num){ int add = 0; do { add = 0; while(num > 0) { add += num % 10; num /= 10; } num = add; }while(add >= 10); return add; }
也就是当求和的结果大于10
的时候继续执行相同的操作,直到小于10
,返回计算的结果。但如果我们采用递归,就会变得更加简单:
int addDigits(int num){ int add = 0; while(num > 0) { add += num % 10; num /= 10; } num = add; return add < 10 ? add : addDigits(add); }
所谓的重复的操作,我们就可以直接递归调用,然后就可以输出想要的结果。
注:要把握递归的深度,且确保递归是可终止的,否则可能会出现堆栈溢出的情况。
7.6 可变参数列表
在实际的项目开发中,我们经常会遇到传递的参数个数未知的情况,这个时候就需要用到可变参数列表。书中有这样一个例子:
//可变参数列表头文件 #include<stdarg.h> float average(int n_values, ...) { va_list var_arg; int count; float sum = 0; //准备访问可变参数 va_start(var_arg, n_values); //添加取自可变参数的值 for (count = 0; count < n_values; count++) { sum += va_arg(var_arg, int); } //完成处理可变参数 va_end(var_arg); return sum / n_values; }
也就是一个求平均值的函数,但我们事先并不知道 究竟有多少个数需要求平均值。调用的时候可以这样:
printf("%f\n", average(10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
就可以直接打印出算得的结果,打印输出:
注意:可变参数必须从头到尾按照顺序逐个访问。如果在访问了几个可变参数后想半途中止,这是可以的。
从这点上来看,可变参数列表和链表的访问很类似。
总结
C语言也可以用来设计和实现抽象数据类型。
递归在熟练以后用起来很方便,但并非在所有情况下都会那么高效,同时要注意由此可能引发的堆栈溢出问题。
---------------------------------------------------------------------------END---------------------------------------------------------------------------