六. 函数——C++的编程模块
6.1 复习函数的基本知识
要使用C++函数,要完成如下工作:
· 提供函数定义;
· 提供函数原型;
· 调用函数。
calling.cpp
#include <iostream>
using namespace std;
//函数原型
void simple();
int main() {
cout << "main() will call the simple() function:\n";
//函数调用
simple();
cout << "main() is finished with the simple() function.\n";
return 0;
}
//函数定义
void simple()
{
cout << "I'm but a simple function.\n";
}
6.1.1 定义函数、
可以将函数分为两类:没有返回值的函数和有返回值的函数。没有返回值的函数被称为void函数,其通用格式如下:
void functionName(parameterList)
{
statement(s);
return;
}
其中,parameterList指定了传递给函数的参数类型和数量,可选的返回语句标记了函数的结尾,否则函数将在花括号处结束。
有返回值的函数将生成一个值,并将它返回给调用函数,其通用格式如下:
typeName functionName(parameterList)
{
statements;
return value;
}
对于有返回值的函数,必须使用返回语句,以便将值返回给调用函数。值本身可以是常量、变量,也可以是表达式,只是其结果的类型必须为typeName类型或可以被转换为ypeName(例如如果声明的返可类型为double,而函数返回一个int表达式则该int值将被强制转换为double类型)。然后,函数将最终的值返回给调用函数。C++对于返回值的类型有一定的限制,不能是数组,但可以是其他任何类型——整数、浮点数、指针,甚至可以是结构和对象。虽然:C++函数不能直接返回数组,但可以将数组作为结构或对象组成部分来返回。
6.1.2 函数原型和函数调用
1. 为什么需要原型
原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型(如果有)以及参数的类型和数量告诉编译器。
2. 原型的语法
函数原型是一条语句,因此必须以分号结束。函数原型不要求提供变量名,有类型列表就够了。通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。
3. 原型的功能
· 确保编译器正确处理函数返回值;
· 编译器检查使用的参数数目是否正确;
· 编译器检查使用的参数类型是否正确。
通常,原型自动将被传递的参数强制转换为期望的类型。仅当有意义时,原型化才会导致类型转换,例如原型不会将整数转换为结构或指针。
在编译阶段进行的原型化被称为静态类型检查(static type checking),它可以捕获许多在运行阶段难以捕获的错误。
6.2 函数参数和按值传递
用于接收传递值的变量被称为形参,传递给函数的值被称为实参,C++标准使用参量(argument)来表示实参,使用参数(parameter)来表示形参,因此参数传递将参量赋给参数。
在函数中声明的变量(包括参数)是该函数私有的,在函数被调用时,计算机将为这些变量分配内存;在函数结束时,计算机将释放这些变量使用的内存,这样的变量被称为局部变量。
6.2.1 多个参数
函数可以有多个参数,在调用函数时,只需用逗号将这些参数分开即可。
twoarg.cpp
#include <iostream>
using namespace std;
void n_chars(char, int);
int main() {
int times;
char ch;
cout << "Enter a character: ";
cin >> ch;
while (ch != 'q')
{
cout << "Enter an integer: ";
cin >> times;
n_chars(ch, times);
cout << "\nEnter another character or press the"
"q-key to quit: ";
cin >> ch;
}
cout << "The value of times is " << times << ".\n";
cout << "Bye\n";
return 0;
}
void n_chars(char c, int n)
{
while (n-- > 0)
cout << c;
}
6.3 函数和数组
arrfun1.cpp
#include <iostream>
const int Arsize = 8;
int sum_arr(int arr[], int n);
int main() {
using namespace std;
int cookies[Arsize] = { 1,2,4,8,16,32,64,128 };
int sum = sum_arr(cookies, Arsize);
cout << "Total cookies eaten: " << sum << "\n";
return 0;
}
int sum_arr(int arr[], int n) {
int total = 0;
for (int i = 0; i < n; i++)
total = total + arr[i];
return total;
}
6.3.1 函数如何使用指针来处理数组
在大多数情况下,C++和C语言一样,也将数组名视为指针,C++将数组名解释为其第一个元素的地址。下面两个式子有助于理解:
arr[i] == *(arr + i);
&arr[i] == arr + i;
再arrfun1.cpp中,实际上没有将数组内容传递给函数,而是将数组地址、包含的元素种类以及元素数目提交给函数,传递常规变量时,函数将使用该变量的拷贝,但传递数组时,函数将使用原来的数组。
arrfun2.cpp
#include <iostream>
const int Arsize = 8;
int sum_arr(int arr[], int n);
int main() {
int cookies[Arsize] = { 1,2,4,8,16,32,64,128 };
std::cout << cookies << " = array address, ";
std::cout << sizeof cookies << " = sizeof cookies\n";
int sum = sum_arr(cookies, Arsize);
std::cout << "Total cookies eaten: " << sum << std::endl;
sum = sum_arr(cookies, 3);
std::cout << "First three eaters ate " << sum << " cookies.\n";
sum = sum_arr(cookies + 4, 4);
std::cout << "Last four eaters ate " << sum << " cookies.\n";
return 0;
}
int sum_arr(int arr[], int n) {
int total = 0;
std::cout << arr << " = arr, ";
std::cout << sizeof arr << " = sizeof arr\n";
for (int i = 0; i < n; i++)
total = total + arr[i];
return total;
}
为将数组类型和元素数量告诉数组处理函数,请通过两个不同的参数来传递它们:
void fillArray(int arr[], int size);
而不要试图通过方括号表示法来传递数组长度:
void fillArray(int arrsize[size]);
6.3.2 一个简单房地产案例
arrfun3.cpp
#include <iostream>
const int Max = 5;
int fill_array(double ar[], int n);
void show_array(const double ar[], int n);
void revalue(double r, double ar[], int n);
int main() {
using namespace std;
double properties[Max];
int size = fill_array(properties, Max);
show_array(properties, size);
if (size > 0)
{
cout << "Enter revalution factor: ";
double factor;
while (!(cin >> factor))
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; Please enter a number: ";
}
revalue(factor, properties, size);
show_array(properties, size);
}
cout << "Done.\n";
cin.get();
cin.get();
return 0;
}
int fill_array(double ar[], int limit) {
using namespace std;
double temp;
int i;
for (i = 0; i < limit; i++) {
cout << "Enter value#" << (i + 1) << ": ";
cin >> temp;
if (!cin) {
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; intput process terminated.\n";
break;
}
else if (temp < 0)
break;
ar[i] = temp;
}
return i;
}
void show_array(const double ar[], int n) {
using namespace std;
for (int i = 0; i < n; i++) {
cout << "Property #" << (i + 1) << ": $";
cout << ar[i] << endl;
}
}
void revalue(double r, double ar[], int n) {
for (int i = 0; i < n; i++)
ar[i] *= r;
}
int fill_array(double ar[], int limit);
该函数接受两个参数,一个是数组名,另一个指定了要读取的最大元素数;该函数返回实际读取的元素数。
void show_array(const double ar[], int n);
为防止函数无意中修改数组的内容,可在声明形参时使用关键字const,C++将声明const double ar[]解释为const double* ar,这意味着ar指向的是常量数据,不能在show_array()函数中使用ar来修改这些数据,即show_array()将数组视为只读数据。
6.3.3 指针和const
可以用两种不同的方式将const关键字用于指针。第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止改变指针所指向的位置。
指向常量的指针:
int age = 39;
const int* pt = &age;
声明指针为常量:
int sloth = 3;
const int * ps = &sloth;
int* const finger = &sloth;
这种声明格式使得finger只能指向sloth,但允许finger来修改sloth的值。
通常,将指针作为函数参数来传递时,可以使用指向const的指针来保护数据。
6.4 函数和二维数组
int data[3][4] = {{1, 2, 3,4}, {9, 8, 7, 6}, {2, 4, 6, 8}};
int total = sum(data,3);
Data是一个数组名,该数组有3个元素。第一个元素本身是一个数组,有4个int值组成,因此data的类型是指向由四个int组成的数组的指针,原型如下:
int sum(int (*ar2)[4], int size);
或者:
int sum(int ar2[][4], int size);
6.5 函数和C风格字符串
假设要将字符串作为参数传递给函数,则表示字符串的方式有三种:
· char数组;
· 用引号括起的字符串常量(也称字符串字面值);
· 被设置为字符串的地址的char指针。
可以说是将字符串作为参数来传递,但实际传递的是字符串第一个字符的地址。这意味着字符串函数原型应将其表示字符串的形参声明为char*类型。
C-风格字符串与常规 char 数组之间的一个重要区别是,字符串有内置的结束字符。这意味着不必将字符串长度作为参数传递给函数,而函数可以使用循环依次检查字符串中的每个字符,直到遇到结尾的空值字符为止。
strgfun.cpp
#include <iostream>
unsigned int c_in_str(const char* str, char ch);
int main() {
using namespace std;
char mmm[15] = "minimum";
const char* wail = "ululate";
unsigned int ms = c_in_str(mmm, 'm');
unsigned int us = c_in_str(wail, 'u');
cout << ms << " m characters in " << mmm << endl;
cout << us << " u characters in " << wail << endl;
return 0;
}
unsigned int c_in_str(const char* str, char ch) {
unsigned int count = 0;
while (*str)
{
if (*str == ch)
count++;
str++;
}
return count;
}
返回C风格字符串的函数:
strback.cpp
#include <iostream>
char* buildstr(char c, int n);
int main() {
using namespace std;
int times;
char ch;
cout << "Enter a character: ";
cin >> ch;
cout << "Enter a integer: ";
cin >> times;
char* ps = buildstr(ch, times);
cout << ps << endl; //使用cout << 来输出一个指针时,它将打印指针所指向的 C 风格字符串
delete[] ps;
ps = buildstr('+', 20);
cout << ps << "-DONE-" << ps << endl;
delete[] ps;
return 0;
}
char* buildstr(char c, int n) {
char* pstr = new char[n + 1];
pstr[n] = '\0';
while (n-- > 0)
pstr[n] = c;
return pstr;
}
6.6 函数和结构
6.6.1 传递和返回结构
travel.cpp
#include <iostream>
struct travel_time {
int hours;
int mins;
};
const int Mins_per_hr = 60;
travel_time sum(travel_time t1, travel_time t2);
void show_time(travel_time t);
int main() {
using namespace std;
travel_time day1 = { 5,45 };
travel_time day2 = { 4,55 };
travel_time trip = sum(day1, day2);
cout << "Two day total: ";
show_time(trip);
travel_time day3 = { 4,32 };
cout << "Three day total: ";
show_time(sum(trip, day3));
return 0;
}
travel_time sum(travel_time t1, travel_time t2) {
travel_time total;
total.mins = (t1.mins + t2.mins) % Mins_per_hr;
total.hours = t1.hours + t2.hours+(t1.mins + t2.mins) / Mins_per_hr;
return total;
}
void show_time(travel_time t) {
using namespace std;
cout << t.hours << " hours, "
<< t.mins << " minutes\n";
}
atrctfun.cpp
#include <iostream>
#include <cmath>
struct polar {
double distance;
double angle;
};
struct rect {
double x;
double y;
};
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);
int main() {
using namespace std;
rect rplace;
polar pplace;
cout << "Enter the x and y values: ";
while (cin >> rplace.x >> rplace.y) {
pplace = rect_to_polar(rplace);
show_polar(pplace);
cout << "Next two numbers (q to quit): ";
}
cout << "Done.\n";
return 0;
}
polar rect_to_polar(rect xypos) {
using namespace std;
polar answer;
answer.distance =
sqrt(xypos.x * xypos.x + xypos.y * xypos.y);
answer.angle = atan2(xypos.y, xypos.x);
return answer;
}
void show_polar(polar dapos) {
using namespace std;
const double Rad_to_deg = 57.29577951;
cout << "distance = " << dapos.distance;
cout << ", angle = " << dapos.angle* Rad_to_deg;
cout << " degrees\n";
}
cin 是 istream类的一个对象。抽取运算符(>>)被设计成使得 cin>>rplacc.x 也是一个istream
对象。类运算符是使用函数实现的。使用 cin>>rplace.x 时,程序将调用一个函数,该函数返回一个istream值将抽取运算符用于 cin>>rplace.x对象(就像cin>>rplacex>>rplacey这样), 也将获得一个istream对象。因此,整个 while 循环的测试表达式的最终结果为 cin,而cin 被用于测试表达式
中时,将根据输入是否成功,被转换为 bool值 true 或false, 例如,在上述程序中的循环中,cin 期望用户输入两个数字,如果用户输入了 q(前面的输出示例就是这样做的),cin>>将知道 不是数字,从而将q留在输入队列中,并返回一个将被转换为 fasle 的值,导致循环结束。
6.6.2 传递结构的地址
重新编写show_polar()函数,需要修改三个地方:
· 调用函数时,将结构的地址(&pplace)而不是结构本身传递给它;
· 将形参声明为指向polar的指针,即polar*类型,由于函数不应该修改结构,因此使用了从const修饰符;
· 由于形参时指针而不是结构,因此应使用间接成员运算符(->),而不是成员运算符(句点)。
strctptr.cpp
#include <iostream>
#include <cmath>
struct polar {
double distance;
double angle;
};
struct rect {
double x;
double y;
};
void rect_to_polar(const rect* pxy, polar* pda);
void show_polar(const polar* pda);
int main() {
using namespace std;
rect rplace;
polar pplace;
cout << "Enter the x and y values: ";
while (cin >> rplace.x >> rplace.y) {
rect_to_polar(&rplace,&pplace);
show_polar(&pplace);
cout << "Next two numbers (q to quit): ";
}
cout << "Done.\n";
return 0;
}
void rect_to_polar(const rect* pxy, polar* pda) {
using namespace std;
pda->distance =
sqrt(pxy->x * pxy->x + pxy->y * pxy->y);
pda->angle = atan2(pxy->y, pxy->x);
}
void show_polar(const polar* pda) {
using namespace std;
const double Rad_to_deg = 57.29577951;
cout << "distance = " << pda->distance;
cout << ", angle = " << pda->angle* Rad_to_deg;
cout << " degrees\n";
}
6.7 函数和string对象
topfive.cpp
#include <iostream>
#include <string>
using namespace std;
const int SIZE = 5;
void display(const string sa[], int n);
int main() {
string list[SIZE];
cout << "Enter your " << SIZE << " favorite astronomical sights:\n";
for (int i = 0; i < SIZE; i++) {
cout << i + 1 << ": ";
//getline(cin, list[i]);
cin >> list[i];
}
cout << "Your list:\n";
display(list, SIZE);
return 0;
}
void display(const string sa[], int n) {
for (int i = 0; i < n; i++)
cout << i + 1 << ": " << sa[i] << endl;
}
6.8 函数与array对象
在C++中,类对象是基于结构的,因此结构编程方面的有些考虑因素也适用于类。例如,可按值将对传递给函数,在这种情况下,函数处理的是原始对象的副本。另外,也可传递指向对象的指针,这让函能够操作原始对象。下面来看一个使用C++模板类array的例子。
arrobj.cpp
#include <iostream>
#include <string>
#include <array>
const int SEASONS = 4;
const std::array<std::string, SEASONS> Snames = {
"Spring", "Summer", "Fall", "Winter"
};
void fill(std::array<double, SEASONS>* pa);
void show(std::array<double, SEASONS> da);
int main() {
std::array<double, SEASONS> expenses;
fill(&expenses);
show(expenses);
return 0;
}
void fill(std::array<double, SEASONS>* pa) {
using namespace std;
for (int i = 0; i < SEASONS; i++) {
cout << "Enter " << Snames[i] << " expenses: ";
cin >> (*pa)[i];
}
}
void show(std::array<double, SEASONS> da) {
using namespace std;
double total = 0.0;
cout << "\nEXPENSES\n";
for (int i = 0; i < SEASONS; i++) {
cout << Snames[i] << ": $" << da[i] << endl;
total += da[i];
}
cout << "Total Expenses: $" << total << endl;
}
6.9 递归
6.9.1 包含一个递归调用的递归
recur.cpp
#include <iostream>
void countdown(int n);
int main() {
countdown(4);
return 0;
}
void countdown(int n) {
using namespace std;
cout << "Counting down ... " << n << endl;
if (n > 0)
countdown(n - 1);
cout << n << ": Kaboom!\n";
}
6.9.2 包含多个递归调用的递归
ruler.cpp
#include <iostream>
const int Len = 66;
const int Divs = 6;
void subdivide(char ar[], int low, int high, int level);
int main() {
char ruler[Len];
int i;
for (i = 0; i < Len - 2; i++)
ruler[i] = ' ';
ruler[Len - 1] = '\0';
int max = Len - 2;
int min = 0;
ruler[min] = ruler[max] = '|';
std::cout << ruler << std::endl;
for (i = 1; i <= Divs; i++) {
subdivide(ruler, min, max, i);
std::cout << ruler << std::endl;
for (int j = 1; j < Len - 2; j++)
ruler[j] = ' ';
}
return 0;
}
void subdivide(char ar[], int low, int high, int level) {
if (level == 0)
return;
int mid = (high + low) / 2;
ar[mid] = '|';
subdivide(ar, low, mid, level - 1);
subdivide(ar, mid, high, level - 1);
}
6.10 函数指针
1. 获取函数的地址
获取函数的地址很简单:只要使用函数名(后面不跟参数)即可。也就是说,如果 think()是一个函数,则 think 就是该函数的地址。要将函数作为参数进行传递,必须传递函数名。--定要区分传递的是函数的地址还是函数的返回值。
2. 声明函数指针
声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的参数列表。也就是说声明应像函数原型那样指出有关函数的信息。
通常,要声明指向特定类型的函数的指针,可以首先编写这种函数的原型,然后用(*pf)替换函数名。这样pf就是这类函数的指针。
double (*pf) (int); //pf是一个指向函数的指针
double* pf(int); //pf()是一个返回指针的函数
3. 使用指针来调用函数
将(*pf)看作函数名即可。
fun_ptr.cpp
#include <iostream>
double betsy(int);
double pam(int);
void estimate(int lines, double (*pf)(int));
int main() {
using namespace std;
int code;
cout << "How many lines of code do you need?";
cin >> code;
cout << "Here's Betsy's estimate:\n";
estimate(code, betsy);
cout << "Here's Pam's estimate:\n";
estimate(code, pam);
return 0;
}
double betsy(int lns) {
return 0.05 * lns;
}
double pam(int lns) {
return 0.03 * lns + 0.0004 * lns * lns;
}
void estimate(int lines, double (*pf)(int)) {
using namespace std;
cout << lines << " lines will take ";
cout << (*pf)(lines) << " hour(s)\n";
}
6.11 总结
函数是C++的编程模块。要使用函数,必须提供定义和原型,并调用该函数。函数定义是实现函数功能的代码,函数原型描述了函数的接口,传递给函数的值的数目和种类以及函数的返回类型。函数调用使得程序将参数传递给函数,并执行函数的代码。
在默认情况下,C++函数按值传递参数。这意味着函数定义中的形参是新的变量,它们被初始化为函数调用所提供的值。因此,C++函数通过使用拷贝,保护了原始数据的完整性。
C++将数组名参数视为数组第一个参数的地址。从技术上讲,这仍然是按值传递的,因为指针是原始地址的拷贝,但函数将使用指针来访问原始数据的内容。当且仅当声明函数的形参时,下面两个声明才是等价的:
typeName arr[];
typeName* arr;
这两个声明都表明,arr是指向typeName的指针,但在编写函数代码时,可以像使用数组名那样使用arr来访问元素:arr[i]。即使在传递指针时,也可以将形参声明为const指针,来保护原始数据的完整性。由于传递函数地址时,并不会传输有关数组的长度信息,因此通常将数组长度作为独立参数来传递。另外,也可以传递两个指针(其中一个指向数组开头,另一个指向数组末尾的下一个元素),以指定一个范围,就像STL使用的算法一样。
C++提供了3中表示C-风格字符串的方法:字符数组、字符串常量和字符串指针。它们的类型都是char*(char指针),因此被作为char*类型参数传递给函数。C++使用空值字符(\0)来结束字符串,因此字符串函数检测空值字符来确定字符串的结尾。
C++还提供了string类,用于表示字符串。函数可以接受string对象作为参数以及将string对象作为返回值。string类的方法size()可用于判断其存储的字符串的长度。
C++处理结构的方式与基本类型完全相同,这意味着可以按值传递结构,并将其用作函数返回类型。然而,如果结构非常大,则传递结构指针的效率将更高,同时及函数能够使用原始数据。这些考虑因素也适用于类对象。
C++函数是可以递归的,也就是说,函数代码中可以包括对函数本身的调用。
C++函数名与函数地址的作用相同。通过将函数指针作为参数,可以传递要调用的函数的名称。