7 函数--C++的编程模块
7.1 复习函数的基本知识
要使用函数,必须:
* 提供函数定义
* 提供函数原型
* 调用函数
7.1.1 定义函数
函数包括有返回值和无返回值,无返回值的函数称为void函数。
void函数可选的返回语句标记了函数的结尾,否则,函数将在右花括号处结束。
有返回值的函数,返回的值的类型比如是typeName或者可以转换为typeName(例如,声明的返回类型是double,但实际却返回了int,则int被强制转换成double)。
C++对返回值的类型有一定的限制,不能是数组,可以是任何类型--整数、浮点数、甚至结构和对象。(虽然不能返回数组,但是,可以将数组作为结构或者对象组成部分返回)
通常,函数通过将返回值复制到指定的CPU寄存器和内存单元中来将其返回。随后,调用程序将查看该内存单元。返回函数和调用函数必须就该内存单元中存储的数据的类型达成一致。
函数原型将返回值类型告知调用程序,而函数定义命令被调用函数应返回什么类型的数据。
函数在执行返回语句后结束。如果函数包含多条返回语句,则函数在执行遇到的第一条返回语句后结束。
7.1.2 函数原型和函数调用
函数原型经常隐藏在include文件中。
* C++要求提供原型的原因
* 原型正确的语法
1、为什么需要原型
原型描述了函数到编译器的接口,它将函数返回值的类型以及参数的类型和数量告诉编译器。
* 若程序没有提供相应的参数,原型将让编译器能够捕获这种错误。
* 函数完成计算后,将把返回值放置在指定的位置--可能是CPU寄存器或者是内存中,然后调用函数将从这个位置中取得返回值。由于原型指出了返回值的类型,因此编译器知道应该检索多少个字节以及如何解释它们。
* 避免使用原型的唯一办法:在首次使用函数之前定义它,但这并不可行。另外,C++的编程风格是将main()放在最前面,因为它通常提供了程序的整体结构。
2、原型的语法
3、原型的功能
void cheers(int);double cube(double);int main() { using namespace std; cheers(5); double f = cube(5); cout << "f: " << f << endl; return 0;}void cheers(int n) { using namespace std; for (int i = 0; i < n; i++) cout << "Cheers!" << endl; return;}double cube(double n) { double m = n * n; return m;}
原型可以帮助编译器完成很多工作,对程序员有什么帮助?极大降低程序出错的几率
* 编译器正确处理函数返回值;
* 编译器检查使用的参数数目是否正确;
* 编译器检查使用的参数类型是否正确。如果不正确,则转换为正确的类型(如果可能的话);
(1)假设进行了如下调用:
double z= cube();
如果没有函数原型,编译器将允许它通过。当函数被调用时,它将找到cube()调用存放值得位置,并使用这里的值。这正是C从C++借鉴原型之前,C语言的工作方式。
由于对C来说,原型是可选的,因此有些C语言程序正是这样工作的。
但在C++中,原型不是可选的,因此可以确保不会发生这类错误。
(2)假设提供了一个参数,但其类型不正确。
在C语言中,将造成奇怪的错误。
例如,如果函数需要一个int值(假设占16位),而程序员传递了一个double值(假设占64位),而函数将只检查64位中的前6位,并试图将它们解释为一个int。
但C++自动将传递的值转换为原型中指定的类型,条件时两者都是算术类型。
例如
cheer(cube(2))
将int为2传递给cube,而cube希望是double类型。编译器注意到原型的定义,因此将int类型转double。
解析来,cube(2)返回一个double值,用作cheer的参数。编译器在进行一次原型检查,发现cheer要求一个int类型,因此它将返回值转换为整数。
通常,原型自动将被传递的参数强制转换为期望的类型(但第8张介绍的函数重载可能导致二义性,因此不允许某些自动强制类型转换)
自动类型转换并不能避免所有的错误,例如当较大的类型被自动转换为较小的类型是,有些编译器将发出警告,提出这可能丢失数据。
仅当有意义时,原型化才会导致类型转换。例如,原型不会将整数转换为结构或者指针。
在编译阶段进行的原型化被称为静态类型检查。可以看出,静态类型检查可以捕获许多在运行阶段非常难以捕获的错误。
7.2 函数参数和按值传递
7.2.1 多个参数
7.2.2 另外一个接受两个参数的函数
7.3 函数和数组
const int ArSize = 5;int sumArr(int arr[], int n);int main() { using namespace std; int Cookies[ArSize] = {1,2,3,4,5}; int total = sumArr(Cookies, ArSize); cout << "total: " << total << endl; return 0;}int sumArr(int arr[], int n) { int total = 0; for(int i = 0; i < n; i++) { total += arr[i]; } return total;}
sumArr(int arr[], int n)函数sumArr指出arr是一个数组,而方括号为空则表明,可以将任何长度的数组传递给该函数。但实际情况并非如此:arr实际上并不是数组,而是一个指针!好消息是,在编写函数的其余部分时,可以将arr看作是数组。
7.3.1 函数如何使用指针来处理数组
第4章介绍过,C++将数组名解释为其第一个元素的地址;
该规则有一些例外:
* 数组声明使用数组名来标记存储位置;
* 对数组名使用sizeof将得到整个数组的长度(以字节为单位);
* 将地址运算符&用于数组名时,将返回整个数组的地址,例如&cookies将返回一个20字节内存块的地址(如果int长4个字节)
sumArr(Cookies, ArSize);
其中,Cookies是数组名,而Cookies是其第一个元素的地址,因此函数传递的是地址。
由于数组元素类型是int,因此 Cookies类型必须是int指针,即int*这表明,正确的函数头应该是:
int sumArr(int *arr, int n)
用int *arr替换int arr[]。表明这两个函数头都是正确的,因为在C++中,当(且仅当)用于函数头或函数原型中,int *arr和int arr[]的含义才是相同的。它们都意味着arr是一个int指针。
然而,数组表示法(int arr[])提醒用户,arr不仅指向int,还指向int数组的第一个int,当指针指向数组的第一个元素时,本书使用数组表示法;而当指针指向一个独立的值时,使用指针表示法。
而在其他的上下文中,int* arr和int arr[]的含义并不相同。例如,不能再函数体重使用int tip[]来声明指针。
请读者记住下面两个恒等式:
arr[i] == *(arr+i)&arr[i] = arr+i
记住,将指针(包括数组名)加1,实际上加上了一个与指针指向的类型的长度相等的值。使用指针加法和数组下标是等效的。
7.3.2 将数组作为参数意味着什么
上面的程序表明,并没有将数组内容传递给函数,而是将数组的地址、包含的类型以及元素数目提交给函数。有了这些信息,函数便可以使用原来的数组。
传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。
数组名誉指针对应是好事吗?是的,将数组地址作为参数可以节省复制整个数组所需的时间和空间。
但另一方面,使用原始数据增加了破坏数据的风险。
#include#include #include #include #include const int ArSize = 5;int sumArr(int arr[], int n);int main() { using namespace std; int Cookies[ArSize] = {1,2,3,4,5}; cout << "address: " << Cookies << endl; cout << "size of : " << sizeof Cookies << endl; int total = sumArr(Cookies, ArSize); cout << "sum: " << total << endl; total = sumArr(Cookies, 3); cout << "first three: " << total << endl; total = sumArr(Cookies+2, 3); cout << "last three: " << total << endl; return 0;}int sumArr(int arr[], int n) { using namespace std; int total = 0; cout << "sumArr address: " << arr << endl; cout << "sumArr size of : " << sizeof arr << endl; for(int i = 0; i < n; i++) { total += arr[i]; } return total;}
地址值和数组的长度随系统而异;
有些C++实现以十进制而不是十六进制格式显示地址;
程序说明:
cookies和arr指向同一个地址。但sizeof cookies的值为20,而sizeof arr为8;这是由于sizeof cookies是整个数组的长度,而sizeof arr只是指针变量的长度。
这也是必须显式传递数组的长度,而不能在sumArr中使用sizeof arr的原因,指针本身并没有指出数组的长度。
7.3.3 更多数组函数示例
要对一个房地产数组进行操作,分别是,将值输入到数组中和显示数组内容。以及重新评估每种房地产的值。
1、填充数组
由于接受数组名参数的函数访问的是原始数组,而不是其副本,因此可以通过该函数将值赋给数组元素。第二个参数是数组长度,函数返回实际输入的元素数目。
int fill_array(double arr[], int limit );
使用循环连续将值输入到数组中,如何提早结束循环呢?
* 使用一个特殊值来指出结束输入
2、显示数组及用const保护数组
如何确保显示函数不修改原始数组 。除非函数目的就是修改数组,否则应该避免这种情况。
使用普通参数,保护自动实现,因为C++按值传递,使用的是数据的副本。
而接受数组名的函数将使用原始数据。为防止修改数组内容,可以在声明形参时指定const
void show_array(const double arr[],int n)
这意味着,arr指向的是常量数据。意味着不能使用arr修改数据,但可以使用。
注意,这并不意味着原始数组必须是常量,而只是意味着不能再show_array中使用arr来修改这些数据。
3、修改数组
由于需要修改数组的值,因此,声明arr不能使用const
4、将上述代码结合起来
main编程非常简单,只是调用前面开发的函数。
const int MAX = 5;int fill_array(double arr[], int limit);void show_array(const double arr[], int n);int main() { using namespace std; double properties[MAX]; int size = fill_array(properties, MAX); show_array(properties, size); return 0;}int fill_array(double arr[], int limit){ using namespace std; int num; int i; for (i = 0; i < limit; i++) { cin>>num; while (!cin) { cin.clear(); while(cin.get() != '\n') { continue; } cin >> num; } arr[i] = num; } return i;}void show_array(const double arr[], int n){ using namespace std; for(int i = 0; i < n; i++) { cout << arr[i] << endl; }}
5、程序说明
6、数组处理函数的常用编写方式
7.3.4 使用数组区间的函数
对于处理数组的C++函数,比如将数组的数据类型、起始位置和数组元素个数提交给它;
还有另一种方法,即指定元素区间,传递两个指针来完成;一个指针标识数组的开头,另一个指针标识数组的尾部。
对数组而言,标识数组结尾的参数是指向最后一个元素后面的指针。
const int MAX = 5;// double sum_array(double begin[], double end[]);double sum_array(const double * begin, const double * end);int main() { using namespace std; double properties[MAX] = {1,2,3,4,5}; double sum = sum_array(properties, properties+MAX); cout << "sum: " << sum << endl; sum = sum_array(properties, properties+3); cout << "sum: " << sum << endl; return 0;}double sum_array(const double * begin, const double * end){ const double *pt = begin; double total = 0; for(;pt!=end;pt++) { total += *pt; } return total;}
7.3.5 指针和const
* 让指针指向一个常量对象--防止使用指针来修改所指向的值;
* 将指针本身声明为常量--防止改变指针指向的位置;
int age = 39;const int* pt = &ages;//pt指向一个const int,因此不能通过pt来修改这个值,即*pt是const的
但,pt的声明并不意味着它指向的值是一个常量,而只是意味着对pt而言,这个值是常量。
* 以前,将常规变量的地址赋给常规指针
* 现在,将常规变量的地址赋给指向const的 指针
* 还有,将const 变量的地址赋给指向const的指针;
* 将const变量的地址赋给常规指针(这个不可以,若非这么做,可以使用强制类型转换来突破这种限制,15章对运算符const_cast的讨论)
假如有一个const数据组成的数组;则禁止将常量数组的地址赋给非常量指针将意味着不能将数组名作为参数传递给使用非常量形参的函数。
尽可能使用const理由:
* 可以避免无意间修改而导致的编程错误;
* 使用const使得函数能够处理const和非const实参,否则只能接受非const数据;
int age = 39;const int * pt=&age;//这里,只能防止修改pt指向的值,而不能修改pt的值。int sloth = 3;const int * ps = &sloth;//不允许使用ps来修改sloth的值,但允许将ps指向另一个位置。int * const finger = &sloth;//这种声明finger只能指向slot,但允许使用finger来修改slot的值。
总而言之,*ps和finger是const,ps和*finger不是const。
还可以声明指向const对象的const指针。
但,void show_array(const double arr[], int n);
声明const意味着不能修改传递给它的数组中的值。只要只有一层间接关系就可以使用这种技术。
例如,这里数组元素是基本类型,但如果它们是指针或者指向指针的指针,则不能使用const;
7.4 函数和二维数组
。。。。。。。。
7.5函数和C-风格字符串
7.5.1 将C-风格字符串作为参数的函数
要将字符串作为参数传递给函数:
* char数组;
* 用引号括起的字符串常量(也称字符串字面值);
* 被设置为字符串的地址的char指针;
可以将字符串作为参数来传递,但实际传递的是字符串第一个字符的地址。这意味着字符串函数原型应将其标识字符串的形参声明为char*类型。
C-风格字符串与常规char数组之间的重要区别,字符串有内置的结束字符(包含字符,但不以空值字符结尾的char数组只是数组,不是字符串)。
这意味着不必将字符串长度作为参数传递给函数,而函数可以使用循环依次检查字符串的每个字符,直到遇到结尾的空值字符为止。
int c_in_str(const char * ch_arr, char ch);int main() { using namespace std; char ch_arr[20] = "minumum"; int count = c_in_str(ch_arr, 'm'); cout << "cout: " << count << endl; return 0;}int c_in_str(const char * ch_arr, char ch){ int i = 0; int count = 0; while(ch_arr[i]){ if (ch_arr[i] == ch) count++; i++; } return count;}
7.5.2 返回C-风格字符串的函数
假设要编写一个返回字符串的函数。函数无法返回一个字符串,但可以返回字符串地址。
要创建包含n个字符的字符串,需要能存储n+1个字符的空间,以便存储空值字符。
char * build_str(char ch, int size);int main() { using namespace std; char * ptr = build_str('m', 10); cout << "ptr: " << ptr << endl; delete [] ptr; return 0;}char * build_str(char ch, int size){ char * ptr = new char[size+1]; ptr[size]='\0'; while(size--){ ptr[size] = ch; } return ptr;}
注意,变量ptr的作用域为build_str函数内,因此该函数结束时,ptr(而不是字符串)使用的内存将被释放。但由于返回了ptr的值,因此程序可通过main函数的指针ptr来访问新建的字符串。
当不需要该字符串时,需要释放该字符串占用的内存。
这种设计(让函数返回一个指针,该指针指向new分配的内存)的缺点是,程序员必须记住使用delete。
12章,将知道如何使用构造函数和析构函数来处理这些细节。
7.6 函数和结构
与数组不同,结构将其数据组合成单个实体或数据对象,该实体被视为一个整体。
前面讲过,可以讲一个结构赋值给另一个结构。
同样可以按值传递结构,就像普通变量那样。这样,函数使用原始结构的副本。
函数也可以返回结构。结构名只是结构名,要获得结构的地址,必须使用地址运算符&。另外,C++还使用该运算符来表示引用变量,这第8章介绍。
* 使用结构编程时,最直接的方式是像处理基本类型那样来处理结构;也就是说,将结构作为参数传递,并在需要时将结构用作返回值使用。
* 然后,按值传递有一个缺点,若结构非常大,即复制结构将增加内存要求,降低系统运行的速度。出于这些原因,程序员更倾向于传递结构的地址,然后使用指针来访问结构的内容。
* C++也提供了第三种选择--按引用传递(第8章介绍)
先介绍前两种。
7.6.1 传递和返回结构
struct travel_time { int hour; int minute;};travel_time sum(travel_time b, travel_time e);void show_travle(travel_time t);int main() { using namespace std; travel_time day1 = {5, 45}; travel_time day2 = {4, 55}; travel_time sum_day = sum(day1, day2); show_travle(sum_day); return 0;}travel_time sum(travel_time b, travel_time e){ travel_time sum_day = { b.hour+e.hour, b.minute+e.minute }; return sum_day;}void show_travle(travel_time t){ using namespace std; cout << "hour: " << t.hour << endl; cout << "minutes: " << t.minute << endl; cout << "total minutes: " << t.hour * 60 + t.minute << endl;}
7.6.2 另一个处理结构的函数示例
struct rect { double x; double y;};struct polar { double angle; double distant;};polar rect_to_ploar(rect rplace);void show_polar(polar p1);int main() { using namespace std; rect rplace; polar pplace; while(cin>>rplace.x>>rplace.y) { pplace = rect_to_ploar(rplace); show_polar(pplace); cout << "go on, Enter two numbers,(q to quit)" << endl; } return 0;}polar rect_to_ploar(rect rplace) { polar pplace; pplace.distant = sqrt(rplace.x * rplace.x + rplace.y * rplace.y); pplace.angle = atan2(rplace.x, rplace.y); return pplace;}void show_polar(polar p1){ using namespace std; double Rad_to_deg = 57; cout << "distan: " << p1.distant << endl; cout << "angle: " << p1.angle * Rad_to_deg <
来复习一下该程序如何使用cin来控制while循环。
cin是istream类的一个对象。>>被设计成cin>>rplace.x也是一个istream对象。
使用cin>>rplace.x时,程序调用一个函数,返回一个istream值。
因此整个while循环的测试表达式的最终结果为cin,而cin被用于测试表达式中时,将根据输入是否成功,被转换为bool值。
例如,cin期望用户输入两个数字,若输入了q,cin>>就知道q不是数字,从而将q留在输入队列中,并返回一个被转换为false的值,导致循环结束。
7.6.3 传递结构的地址
假设要传递结构的地址而不是整个结构以节省时间和空间,则需要:使用指向结构的指针:
* 调用函数时,将结构的地址(&pplace)而不是结构本身传递给它;
* 将形参声明为执行poloar的指针,即polar*类型。由于不应该修改结构,使用const;
* 由于形参是指针而不是结构,因此使用间接成员运算符->,而不是成员运算符(.);
#include#include #include #include #include #include struct polar{ double distant; double angle;};struct rect{ double x; double y;};void rect_to_polar(const rect * rplace, polar * pplace);void show_polar(const polar * pplace);int main() { using namespace std; polar pplace; rect rplace; while(cin>>rplace.x>>rplace.y) { rect_to_polar(&rplace, &pplace); show_polar(&pplace); cout << "next two numbers(q to quit):" << endl; } return 0;}void rect_to_polar(const rect * rplace, polar * pplace){ // notice : 使用->而不是. pplace->distant = sqrt(rplace->x * rplace->x + rplace->y * rplace->y); pplace->angle = atan2(rplace->y, rplace->x);}void show_polar(const polar * pplace) { using namespace std; cout << pplace->distant << endl; cout << pplace->angle << endl;}
7.7 函数与string对象
尽管C-风格字符串和String对象类似,但是与数组相比,string和结构更相似。例如,可以将一个结构赋给另一个结构,也可以将一个对象赋给另一个对象。
可以将结构作为完整的实体传递给函数,也可将String对象作为完整的实体传递给函数。
如果需要多个字符串,可以创建一个string对象数组,而不是二维char数组。
#include#include #include #include #include const int SIZE = 5;void display(const string sa[], int size);int main() { using namespace std; string sa[SIZE]; for(int i = 0; i < SIZE; i++) { getline(cin, sa[i]); } display(sa, SIZE); return 0;}void display(const string sa[], int size) { using namespace std; for (int i = 0; i < size; i++) { cout << sa[i] << endl; }}编译不过
7.8 函数与array对象
C++中,类是基于结构的,因此结构编程方面的有些考虑因素也适用于类。
* 直接将对象传递给函数。这样,处理的是原始对象的副本;
* 传递指向对象的指针,这样,函数操作原始对象;
#include#include #include #include #include #include const int Seasons = 4;using namespace std;const array Snames = { "Spring", "Summer", "Fall", "Winter"};void Show(std::array expenses);void Fill(std::array * expenses);int main() { array expenses; Fill(&expenses); Show(expenses); return 0;}void Fill(std::array * expenses) { for (int i = 0; i < Seasons; i++) { cout << "Enter " << Snames[i] << endl; cin.get(&expenses[i]); }}void Show(std::array expenses) { for (int i = 0; i < Seasons; i++) { cout << Snames[i] << ": " << expenses[i] << endl; }}
编译不了,貌似必须得C++11
7.9 递归
C++函数--可以调用自己,称为递归。
7.9.1 包含一个递归调用的递归
如果递归函数调用自己,则被调用的函数也将调用自己,这将无限循环下去,除非代码中包含终止调用链的内容。通常的方法将递归调用放在if语句中。
void recurs(argumentlist){ statements1 if (test) recurs(arguments) statements2}
test最终将为false,调用链将断开。
递归调用将导致一系列有趣的事件。只要if语句为true,每个recurs调用都将执行statement1,然后在调用recurs,而不会执行statement2,当if语句为false时,当前调用将执行statement2,。当前调用结束后,程序控制权将返回给调用它的recurs,而该recurs将执行器statement2部分,然后结束,并将控制权返回给前一个调用,以此类推。因此,如果recurs进行了5次递归调用,则第一个statement1部分将按函数调用的顺序执行5次,然后statement2部分将以与函数调用相反的顺序执行5次。
进入5层递归后,程序将沿进入的路径返回。
#include#include void countdown(int n);int main() { countdown(4); return 0;}void countdown(int n) { using namespace std; cout << "countdown: " << n << endl; if (n > 0) countdown(n-1); cout << n << "Kalm Down" << endl;}
每个递归调用都将创建自己的一套变量,因此当程序到达5次调用时,将有5个独立的n变量;5个n的地址不同;
另外,在countdown阶段和kalmdown阶段的相同层级,n的地址相同;
7.9.2 包含多个递归调用的递归
在需要将一项工作不断分为两项较小的、类似的工作时,递归非常有用。
#include#include const int Len = 66;const int Divs = 6;void subdiv(char ruler[], int min, int max, int level);int main() { using namespace std; char ruler[Len]; for (int i = 1; i < Len-2; i++) ruler[i] = ' '; ruler[Len-1] = '\0'; int min = 0; int max = Len-2; ruler[min] = ruler[max] = '|'; for (int i = 0; i < Divs; i++) { subdiv(ruler, min, max, i); cout << ruler << endl; for (int i = 1; i < Len-2; i++) { ruler[i] = ' '; } } return 0;}void subdiv(char ruler[], int min, int max, int level) { if (level == 0) return; int mid = (max + min) / 2; ruler[mid] = '|'; subdiv(ruler, min, mid, level-1); subdiv(ruler, mid, max, level-1);}
该程序,6层调用能够填充64个元素。
如果要求的递归层次很多,这种递归方式将是一种糟糕的选择;然而,如果递归层次较少,这将是一种精致而简单的选择。
7.10 函数指针
大致介绍,完整的介绍留给更高级的图书。
与数据项相似,函数也有地址。函数的地址是存储器其机器语言代码的内存的开始地址。
通常,这些地址对用户并没有什么用处,但对程序而言,却很有用。
例如,可以编写将另一个函数的地址作为参数的函数。这样第一个函数能够找到第二个函数,并运行它。
与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的函数。
7.10.1 函数指针的基础知识
设计一个estimate函数,估算编写指定行数的代码所需的时间,并希望不同程序员都将使用该函数。
该函数允许每个程序员提供自己的算法来估算时间。
为实现,采用的机制是:将程序员要使用的算法函数的地址传递给estimate:
* 获取函数地址;
* 声明一个函数指针;
* 使用函数指针来调用函数;
1、获取函数的地址:
* 使用函数名即可(后面不跟参数)
2、声明函数指针
声明指向某种数据类型的指针时,必须指定指针指向的类型。
同样,声明指向函数的指针时,必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征表(参数列表)。
函数原型:
double pam(int);
double (*pf)(int);
这与声明pam()类似,就是将pam换成了*pf。由于pam是函数,*pf也是函数。
而如果*pf是函数,则pf就是函数指针。
提示:通常,声明指向特定类型的函数的指针,可以首先编写这种函数的原型,然后用*pf替换函数名,这样pf就是这类函数的指针。
为提供正确的运算符优先级,必须在声明中使用括号将*pf括起。
括号优先级比*高,因此*pf(int)意味着pf是一个返回指针的函数,而(*pf)(init)意味着pf是一个指向函数的指针;
正确声明pf后,就可以将相应函数的地址赋给它。
3、运用指针来调用函数
使用指针来调用被指向的函数。线索来自指针声明。
前面讲过,(*pf)扮演的角色与函数名相同,因此使用(*pf)时,只需将它看作函数名即可:
7.10.2 函数指针示例
7.10.3 深入探讨函数指针
。。。。
7.10.4 使用typedef进行简化
。。。。