Chapter 7函数——C++的编程模块
本章需要重点掌握的内容:
- 设计函数
- 使用const指针参数
- 调用自身的函数
- 指向函数的指针
7.1 函数的基本知识
要使用C++函数,必须完成如下工作:
- 提供函数定义
- 提供函数原型
- 调用函数
7.1.1 定义函数
无返回值的函数通用格式如下:</font>
void functionName(parameterList)
{
statement(s);
}
有返回值的函数:
typeName functionName(parameterList):
{
statement(s);
return vlaue; // value is type cast to type typeName
}
7.1.2 函数原型和函数调用
函数原型通常隐藏在inclue文件中。
- 为什么需要原型
原型提供了函数的参数类型和数量以及返回值的类型(如果有的话)告诉编译器。 - 原型的语法
函数原型是一条语句,必须以分号结束。
函数原型不要求提供变量名,有函数列表就足够了。 - 原型的功能
原型确保以下几点:
- 编译器正确处理函数返回值
- 编译器检查使用参数数目是否正确
- 编译器检查使用参数类型是否正确如果不正确,则转换为正确的类型(如果可以的话)
7.2 函数参数和按值传递
参数(argument)表示实参,参量(parameter)表示形参,参数传递将参数赋给参量。
7.2.1 多个参数
函数的多个参数用逗号隔开。
7.3 函数和数组
7.3.1 函数如何使用指针来处理数组
在大多数情况下,cookies == &cookies[0]
,&cookies将返回整个数组的地址,通常是一个大的内存块。
在函数声明中,int * arr
与int arr[]
是等效的。
7.3.2 将数组作为参数意味着什么
数组名与指针对应:
- 将数组地址作为参数可以节省复制整个数组所需的时间和内存
- 使用原始数据增加了破坏数据的风险。C++使用const限定符解决该问题。
注:指针本身并没有指出数组的长度,将数组类型和元素数量告诉数组处理函数时,请通过两个不同的参数来传递他们;而不要试图用方括号表示法来传递数组长度。
void fillArray(int arr[size])
//NO -- bad prototype
7.3.3 更多数组函数示例
构思程序时将存储属性与操作结合起来,便是朝oop思想迈进了重要的一步。
- 填充数组
在数组填满之前停止读取数据,在函数中建立这种特性。
int fill_array(double arr[], 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')
coutinue;
cout << "Bad input; input process terminated.\n";
break;
}
else if(temp < 0)
break;
arr[i] = temp;
}
return i;
}
- 显示数组及用const保护数组
创建显示数组内容的函数很简单,需要确保显示函数不修改原始数组。
void show_array(const double ar[], int n)
{
using namespace std;
for(int i = 0; i < n; i++)
{
cout << "Property #" << i << " : $";
cout << ar[i] << endl;
}
}
- 修改数组
void revalue(double r, double ar[], int n)
{
for(int i = 0; i < n; i++)
ar[i] *= r;
}
- 数组处理函数常用的编写方式
修改数组:
void f_modify(double ar[], int n)
不修改数组:
void f_no_change(const double ar[], int n);
这两种编写方式不能用sizeof来获悉原始数组的长度。
7.3.4 使用数组区间的函数
另一种数组参数传递的方法,指定元素区间,通过两个指针来完成。
注:使用const时,在函数中定义指针也需要const.
7.3.5 指针和const
两种不同的方式将const关键字用于指针。
- 让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值
首先,声明一个指向常量的指针pt:
int age = 39;
const int * pt = &age;
该声明指出,pt指向一个const int(这里为39),因此不能使用pt来修改这个值。*pt的值为const,不能被修改:
*pt += 1;// INVALID beacuse pt point to a const int
cin >> pt;// INVALID for the same reason
- 将指针本身声明为常量(const),这样可以防止改变指针指向的位置
两种可能:将const变量的地址赋给指向const的指针,将const的地址赋给常规指针。
第一种操作可行,第二种不可行。
const float g_earth = 9.80;
const float * pe = &g_earth; // VALID
const float g_moon = 1.63;
float * pm = &g_moon; // INVALID
如果上面的操作可以,则可以通过pm修改g_moon的值,这使得const的状态很荒谬。因此禁止该情况。
尽可能使用const
将指针参数声明为指向常量数据的指针有两条理由:
- 这样可以避免由于无意修改数据而导致的编程错误;
- 使用const是的函数能够处理const和非const实参,否则只能接受非const数据。
如果条件允许,则应该将指针形参声明为指向const的指针。
7.4 函数和二维数组
二维数组作为参数的函数声明和调用:
- 使用指针
int sum_ar2((*ar2)[4], int size)
- 使用数组
int sum_ar2(int ar2[][4], int size)
要想得到二维数组的数据:
* ((* ar2 + r) + c)
得到r行c列的数据。
7.5 函数和C-风格字符串
将字符串作为参数时意味着传递的是地址,可以使用const来禁止对字符串参数进行修改。
7.5.1 将C-风格字符串作为参数的函数
处理字符串中字符的标准方式:
while(*str)
{
statements;
str++;
}
7.5.2 返回C-风格字符串的函数
返回的是一个地址,接受函数也应该是一个地址,使用动态存储记得使用delete释放空间。
char * buildstr(char ch, int n);
调用:
char * pt = bulid('+',20);
完成后记得 delete[]pt
7.6 函数和结构
结构名只是结构的名称,数组名是第一个数组元素的地址。结构必须使用&地址运算符。
7.6.1 传递和返回结构
当结构比较小时,按值传递结构最合理。
7.6.2 传递结构的地址
传递结构的地址可以节省时间和空间。
修改:
- 调用函数时,将结构的地址(&pplace)而不是结构本身传递;
- 形参声明为指向结构的指针,即polar * 的类型,在不修改结构时使用const修饰符
- 形参是指针,因此应接成员运算符
->
而不是.
;
7.7 函数和string对象
string对象与结构的用途更相似。
string数组,string对象读行getline(cin,str);
7.8 函数与array对象
类对象是基于结构的。在传递给函数时有两种方式:
- 按值传递给函数,这种情况下,函数处理的时原始对象的副本;
- 传递指向对象的指针,函数可以操作原始对象。
7.9 递归
函数自己调用自己,该功能成为递归。(C++不允许main()函数调用自己)。
7.9.1 包含一个递归调用的递归
void recurs(argumentlist)
{
statements1
if (test)
recurs(arguments)
statements2
}
该程序recurs进行5次递归调用,则第一个statements1顺序执行5次,第二个statements2将与函数相反的顺序执行5次。
7.9.2 包含多个递归调用的递归
样例程序:
// ruler.cpp -- using recursion to subdivide a ruler
#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 = 1; 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 = i; 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);
}
该程序使用变量level来控制递归层,函数调用自身时,将level-1,当level为0时,函数将不在调用自己。subdivide调用自己两次,一次针对左半部分,一次针对右半部分。注意,调用次数将呈几何级数增长.当递归层次较少时,该方法很适合。
7.10 函数指针
与数据项类似,函数也有地址。函数的地址是存储其机器语言代码的内存的开始地址。
7.10.1 函数指针的基础知识
将程序员要使用的算法函数的地址传给estimate(),为此,必须完成以下工作:
- 获取函数的地址;
使用函数名,后面不跟参数就是该函数的地址。
process(think); // passes address of think()
thought(think()); // passes the return value of think - 声明一个函数指针;
正确的声明如下:
double pam(int);
double (*pt) (int);// () is must
pt = pam;
double *pf (int)
意味着声明了一个函数,返回一个double类型的指针
- 使用函数指针来调用函数。
使用指针函数是,将他看成函数名即可。
7.10.2 深入讨论函数指针
使用auto来简化简化初始化,注:auto只能用于单值初始化
7.10.3 使用typedef进行简化
声明函数指针类型的别名
typedef const double *(*p_fun)(const double * ,int);
p_fun p1 = f1;
7.11 复习题
- 使用函数的3个步骤是什么?
- 函数声明(提供原型)
- 函数定义
- 函数调用
-
请创建与下面的描述匹配的函数原型。
a. igor没有参数,且没有返回值。
void igor();
b. tofu()接受一个int参数,并返回一个float。
float tofu(int);
c. mpg()接受两个double参数,并返回一个double参数。
double mpg(double miles, double gallons);
d. summation()将long数组名和数组长度作为参数,并返回一个long值。
long summation(long[], int);
e. doctor()接受一个字符串参数(不能修改该字符串),并返回一个double值。double doctor(const std::string s); //wrong
double doctor(const char * str);
f. ofcourse()将boss结构作为参数,不返回值。
void ofcourse(boss b);
g. plot将map结构的指针作为参数,,并返回一个字符串。
string plot(map *pt); //wrong, can't return string, can return point
char * plot(map *pmap);
-
编写一个接受三个参数的函数,int数组名、数组长度和一个int值,并将数组的所有元素都设置为该int值。
void setar(int ar[], int len, int n)
for(int i = 9; i < len; i++)
ar[i] = n;
- 编写一个接受三个参数的函数,指向数组区间第一个元素的指针、指向数组区间最后一个元素的指针和一个int值,并将数组的所有元素都设置为该int值。
void setar(int * begin, int * end, int n)
int * pt;
pt = begin;
while(pt != end)
{
*pt = n;
pt++;
}
- 编写将double数组名和数组长度作为参数,并返回该数组中最大值的函数。该函数不应该修改数组的内容。
double find_max(const double ar[], int len)
{
double max = ar[0];
for(int i = 0; i < len; i++)
if(max < ar[i])
max = ar[i];
return max; //
}
- 为什么不对类型为基本类型的函数参数使用const限定符?
函数传递基本类型的参数按值传递使用的是参数副本,不会修改原始数据,因此不需要const限定符,使用指针怕对原始数据产生影响,因此使用const限定符。 - C++程序可以使用哪3中C-风格字符串格式?
- 字符数组
- 字符串常量
string对象指向字符串第一个字符的指针
- 编写一个函数,其原型如下:
int replace(char *str, char c1, char c2);
该函数将字符串中所有的c1都替换为c2,并返回替换次数;
int replace(char *str, char c1, char c2)
{
int count = 0;
while(*str != '\0')
{
if(*str == c1)
{
*str = c2;
count++;
}
str++;
}
return count;
}
- 表达式
*"pizza"
的含义是什么?"taco"[2]
呢?
*"pizza"
代表常字符串"pizza"的地址字符串"pizza"的字符p的地址,"taco"[2]
存在一个字符串数组,有两个元素,每个元素都是常字符串"taco"指的是字符串数组的第三个字符,即c。 - C++允许按值传递结构,也允许传递该结构的地址。如果glitz是一个结构变量,如何按值传递它?如何传递它的地址?这两种方法有何利弊?
函数会创建glitz的一个副本,使用该函数的副本,不会对glitz的原始数据产生影响,但会浪费时间和存储空间;使用地址式函数直接使用指针指向该结构,利用指针直接访问该结构,该方法可以节约空间和时间,但可能会改变原始数据。 - 函数judge()的返回类型为int,它将这样一个函数的地址作为参数:将const char指针作为参数,并返回一个int值。请编写judge()函数的原型。
int judge((*int)(const char*));//wrong
int judge(int(*pt)(const char *));
- 假设有如下结构声明:
struct applicant{
char name[30];
int credit_ratings[3];
}
a. 编写一个函数,它将application结构作为参数,并显示该结构的内容。
b. 编写一个函数,它将application结的地址构作为参数,并显示该结构的内容。
a.
void display(const applicant application)
{
cout << "Name: " << application.name << endl;
for(int i = 0; i < 3; i++)
cout << "credit_rate #" << i+1 << ": " << application.credit_ratings << endl;
}
b.
void display(const applicant * application)
{
cout << "Name: " << application->name << endl;
for(int i = 0; i < 3; i++)
cout << "credit_rate #" << i+1 << ": " << application->credit_ratings << endl;
}
- 假设函数f1()和f2()的原型如下:
void f1(applicant * a);
const char * f2(const applicant * a1, const applicant * a2);
请将p1和p2分别声明为指向f1和f2的指针;将ap声明为一个数组,它包含5个类型与p1相同的指针;将pa声明为一个指针,它指向的数组包含10个类型与p2相同的指针。使用typedef来完成这项工作。
typedef void (*pf1)(applicant *a);//typedef void (*pf1)(applicant *)
pf1 p1;
p1 = f1; // pf1 p1 = f1;
typedef const char * (*pf2)(applicant *, applicant *);
pf2 p2;
p2 = f2;// pf2 p2 = f2;
pf1 ap[5];
pf2 ap2[10];
auto pa = &ap2
void *(*(*pf2))[10])(applicant *a1, applicant *a2) = &ap2;
//pf2 (*pa)[10]