在前面的例子中,我们已经用过了最常见的函数:主函数,其实在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---------------------------------------------------------------------------