函数——C++的编程模块
1.C++对于函数返回值的类型有一定的限制:不能是数组,但可以是其他任何类型——整数、浮点数、指针,甚至可以是结构和对象!(但可以将数组作为结构或对象组成部分来返回。)
2.通常,函数通过将返回值复制到指定的CPU寄存器或内存单元中来将其返回。随后,调用程序将查看该内存单元。返回函数和调用函数必须就该内存单元中存储的数据的类型达成一致。函数原型将返回值类型告知调用程序,而函数定义命令被调用函数应返回什么类型的数据。
3.函数原型
1)为什么需要原型
原型描述了函数到编译器的接口,即将函数返回值的类型以及参数的类型和数量告诉了编译器。
C++允许将一个程序放在多个文件中,单独编译这些文件,然后再将它们组合起来。
避免使用函数原型的唯一方法是,在首次使用函数之前定义它,但这并不总是可行的。C++的编程风格是将main()放在最前面,因为它通常提供了程序的整体结构。
2)原型语法
函数原型是一条语句,因此必须以分号结束。通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。
4.如果函数的两个参数的类型相同,则必须分别指定每个参数的类型,而不能像声明常规变量那样,将声明组合在一起:
void fifi(float a,float b) //daclare each variable separately
void fufu(float a, b) //NOT acceptable
5.一个接受两个参数的函数
例:
//lotto.cpp --probability of winning
#include<iostream>
//Note:some implementations require double instead of long double
long double probability(unsigned numbers, unsigned picks);
int main()
{
using namespace std;
double total, choices;
cout << "Enter the total number of choices on the game card and\n"
"the number of picks allowed:\n";
while(cin >> total >> choices && choices <= total)
{
cout << "You have one chance in ";
cout << probability(total, choices); //compute the odds
cout << " of winning.\n";
cout << "Next two numbers (q to quit): ";
}
cout << "Bye\n";
cin.get();
cin.get();
return 0;
}
long double probability(unsigned numbers, unsigned picks)
{
long double result = 1.0;
long double n;
unsigned p;
for(n = numbers, p = picks; p >0; n--,p-- )
result = result * n / p;
return result;
}
6. 函数和数组
1)函数如何使用指针来处理数组
在大多数情况下,C++和C语言一样,也将数组名视为指针。C++将数组名解释为其第一个元素的地址:
cookies == &cookies[0] //array name is address of first element
该规则有一些例外。首先,数组声明使用数组名来标记存储位置;其次,对数组名使用sizeof将得到整个数组的长度(以字节为单位);第三,将地址运算符&用于数组名时,将返回整个数组的地址。
在C++中,当(且仅当)用于函数头或函数原型中,int *arr和int arr[]的含义才是相同的。它们意味着arr是一个指针。然而,数组表示法(int arr[])提醒用户,arr不仅指向int,还指向int数组的第一个int。(数组表示法和指针表示法)
两个恒等式:
arr[i] == *(arr+i) //values in two notations
&arr[i] == arr+i //addresses in two notations
将指针(包括数组)加1,实际上是加上了一个与指针指向的类型的长度(以字节为单位)相等的值。对于遍历数组而言,使用指针假发和数组下标时是等效的。
注意!!!cookies和arr指向同一个地址,sizeof cookies是整个数组的长度,sizeof arr只是指针变量的长度。
2)数组函数示例
//array function and const
#include<iostream>
const int Max = 5;
//function prototypes
int fill_array(double ar[], int limit);
void show_array(const double ar[], int n); //don't change data
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 revaluation factor: ";
double factor;
while(!(cin >> factor)) //bad input
{
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) //bad input
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << " Bad input; input process terminated.\n";
break;
}
else if (temp < 0)
break;
ar[i] = temp;
}
return i;
}
// the following function can use, but not alter
// the array whose address is ar
void show_array(const double ar[], int n) //显示数组及使用const保护数组
{
using namespace std;
for(int i = 0; i <n; i++)
{
cout << "Property #" << (i+1) << ":$";
cout << ar[i] <<endl;
}
}
//multiplies each element of ar[] by r
void revalue(double r, double ar[], int n) //修改数组
{
for(int i = 0; i < n; i++)
ar[i] *= r;
}
程序说明:
1)填充数组
函数原型:
int fill_array(double ar[], int limit);
该函数接受两个参数,一个是数组名,另一个指定了要读取的最大元素数;该函数返回实际读取的元素数。
2)显示数组及用const保护数组
为防止函数无意中修改数组的内容,可在声明形参时使用关键字const:
void show_array(const double ar[], int n);
该声明表明,指针ar指向的是常量数据。不能使用ar修改数据,即可以使用像ar[0]这样的值,但不能修改。注意,这并不意味着原始数组必须是常量,而只是意味着不能在show_array()函数中使用ar来修改这些数据。因此,show_array()将数组视为只读数据。
7.使用数组区间的函数
对于处理数组的C++函数,必须将数组中的数据类型、数组的起始位置和数组中元素数量提交给它;传统的C/C++方法是,将指向数组起始处的指针作为一个参数,将数组长度作为第二个参数(指针指出数组的位置和数据类型),这样便给函数提供了找到所有数据所需的信息。
指定元素区间(range),通过传递两个指针来完成:一个指针标识数组的开头,另一个指针标识数组的尾部。C++标准模板库(STL)使用“超尾”概念来指定区间。即对于数组而言,标识数组结尾的参数是指向最后一个元素后面的指针。
例:
//function with an array range
#include<iostream>
const int ArSize = 8;
int sum_arr(const int *begin, const int *end);
int main()
{
using namespace std;
int cookies[ArSize] = {1,2,4,8,16,32,64,128};
int sum = sum_arr(cookies, cookies + ArSize);
cout << "Total cookies eaten: " << sum << endl;
sum = sum_arr(cookies, cookies + 3); //first 3 elements
cout << "First three eaten ate " << sum <<" cookies.\n";
sum = sum_arr(cookies + 4, cookies + 8); //last 4 elements
cout << "Last four eaten ate " << sum <<" cookies.\n";
cin.get();
cin.get();
return 0;
}
//return the sum of an integer array
int sum_arr(const int *begin, const int *end)
{
const int *pt;
int total = 0;
for(pt = begin; pt != end; pt++)
total += *pt;
return total;
}
注意,在sum_arr()中,表达式end-begin是一个整数值,等于数组的元素数目。
8.指针和const
可以用2种不同的方式将const关键字用于指针:
1)让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值;
2)将指针本身声明为常量,这样可以防止改变指针指向的位置。
a.将常规变量的地址赋给常规指针
b.将常规变量的地址赋给指向const的指针
例,声明一个指向常量的指针pt:
int age = 39;
const *pt = &age;
该声明指出,pt指向一个const int,因此不能使用pt来修改这个值。即,*pt的值为const,不能被修改:
*pt = 1; //INVALID because pt points to a const int
cin >> *pt; //INVALID for the same reason
pt的声明并不意味着它指向的值实际上就是一个常量,而只是意味着对pt而言,这个值是常量。
例如,pt指向age,而age不是const。可以直接通过age变量来修改gae的值,但不能使用pt指针来修改它:
*pt = 20; //INVALID because pt points to a const int
age =20; //VALID because age is not declared to be const
c.将const变量的地址赋给指向const的指针(可行)
d.将const变量的地址赋给常规指针(不可行)
const float g_earth = 9.80;
const float *pe = &g_earth; //VALID
const float g_moon = 1.63;
float *pm = &g_moon; //INVALID
C++禁止将const的地址赋给非const指针。(如果非要这样做,可以使用强制类型转换来突破这种限制。)
仅当只有一层间接关系时,才可以将非const地址或指针赋给const指针。
注意:如果数据类型本身并不是指针,则可以将const数据或非const数据的地址赋给指向const的指针,但只能讲非const数据的地址赋给非const指针。
int sloth = 3;
const int *ps = &sloth; //a pointer to const int
int *const finger = &sloth; //a const pointer to int
在中间的声明不允许使用ps来修改sloth的值(可防止修改pt指向的值),但允许将ps指向另一个位置(不能防止修改pt的值,即可以将一个新地址赋给它)。最后一个声明中,使得finger只能指向sloth(即无法修改指针的值),但允许使用finger来修改sloth的值。简而言之,finger和*ps都是const,而*finger和ps不是。
通常,将指针作为函数参数来传递时,可以使用指向const的指针来保护数据。如show_array()的原型:
void show_array(const double arr[], int n);
在该声明中使用const意味着show_array()不能修改传递给它的数组中的值。只要只有一层间接关系,就可以使用这种技术。例如,这里的数组元素是基本类型,但如果它们是指针或指向指针的指针,则不能使用const。
9.函数和二维数组
为编写将二维数组作为参数的函数,数组名被视为其地址。因此,相应的形参时一个指针。
正确的原型:
int sum(int (*ar2)[4], int size);
其中的括号是必不可少的,因为下面的声明将声明一个由4个指向int的指针组成的数组,而不是由一个指向由4个int组成的数组的指针;另外,函数参数不能是数组:
int *ar2[4]
函数原型另一种格式:
int sum(int ar2[][4], int size);
上述两个原型都指出,ar2是指针而不是数组。指针类型指出,它指向由4个int组成的数组。因此,指针类型指定了列数。
ar2[r][c] = *(*(ar2+r)+c) //same thing
10.函数和C-风格字符串
C-风格字符串由一系列字符组成,以空值字符结尾。例如,字符串作为参数时意味着传递的是地址,但可以使用const来禁止对字符串参数进行修改。
1)将C-风格字符串作为参数的函数
假设要将字符串作为参数传递给函数,则表示字符串的方式有三种:
- char数组;
- 用引号括起的字符串常量(也称字符串字面值)
- 被设置为字符串的地址的char指针
但上述3种选择的类型都是char指针( 准确地说是char*),因此可以将其作为字符串处理函数的参数:
char ghost[15] = "galloping";
char *str = "galumphing";
int n1 = strlen(ghost); //ghost is &ghost[0]
int n2 = strlen(sting); //pointer to char
int n3 = strlen("gamboliong"); //address of string
可以说是将字符串作为参数来传递,但实际传递的是字符串第一个字符的地址。意味着字符串函数原型应将其表示字符串的形参声明为char*类型。
C-风格字符串与常规char数组之间的一个重要区别:
字符串有内置的结束字符(包含字符,但不以空值字符结尾的char数组只是数组,而不是字符串)。这意味着不必将字符串长度作为参数传递给函数,而函数可以使用循环依次检查字符串中的每个字符,直到遇到结尾的空值字符为止。
例:
//function with a string argument
#include<iostream>
unsigned int c_in_str(const char *str,char ch);
int main()
{
using namespace std;
char mmm[15] = "minimum"; // string in an array
char *wail = "ululate"; // wail points to string
unsigned int ms = c_in_str(mmm, 'm');
unsigned int ns = c_in_str(wail, 'u');
cout << ms << " m characters in " << mmm << endl;
cout << ns << " u characters in " << wail << endl;
cin.get();
cin.get();
return 0;
}
unsigned int c_in_str(const char *str, char ch)
//使用一个函数来计算特定的字符在字符串中出现的次数
{
unsigned int count = 0;
while(*str) //quit when *str is '\0'
{
if(*str == ch)
count++;
str++; //move pointer to next char
}
return count;
}
c_int_str()函数不应该修改原始字符串,因此声明形参str时使用了限定符const。也可以在函数头中使用数组表示法,而不声明str:
unsigned int c_in_str(const char str[], char ch); //also okay
使用指针表示法提醒,参数不一定必须是数组名,也可以是其他形式的指针。
2)返回C-风格字符串的函数
//编写一个返回字符串的函数。函数无法返回字符串,但可以返回字符串的地址。
//a function that returns apointer to char
#include<iostream>
char *buildstr(char c,int n); //prototype
int main()
{
using namespace std;
int times;
char ch;
cout << "Enter a character: ";
cin >> ch;
cout << "Enter an integer: ";
cin >> times;
char *ps = buildstr(ch, times);
cout << ps << endl;
delete[] ps; //free memory
ps = buildstr('+', 20); //reuse pointer
cout << ps << "-DONE-" << ps << endl;
delete[] ps; //free memory
cin.get();
cin.get();
return 0;
}
//buildes string made of n c characters
char *buildstr(char c,int n)
{
char *pstr = new char[n+1];
pstr[n] = '\0'; //terminate string
while(n-- > 0)
pstr = c;
return pstr;
}
程序说明:
要创建包含n个字符的字符串,需要能够存储n+1个字符的空间,以便能够存储空值字符。将最后一个字节设置为空值字符,然后从后向前对数组进行填充。从前向后填充:
int i = 0;
while(i < n)
pstr[i++] = c;
注意,变量pstr的作用域为buildstr函数,因此该函数结束时,pstr(而不是字符串)使用的内存将被释放。但由于函数返回了pstr的值,因此程序将可以通过main()中的指针ps来访问新建的字符串。
当该字符串不在需要时,程序使用delete释放该字符串占用的内存。
11.函数和结构
与数组不同,结构将其数据组合成单个实体或数据对象,该实体被视为一个整体。
可以将一个结构赋给另一个结构。同样,也可以按值传递结构,就像普通变量那样。在这种情况下,函数将使用原始结构的副本。另外,函数也可以返回结构。与数组名就是数组第一个元素的地址不同的是,结构名只是结构的名称,要获得结构的地址,必须使用地址运算符&。
在C语言和C++中,都使用符号&来表示地址运算符;另外,C++还使用该运算符来表示引用变量。
1)传递和返回结构
按值传递结构缺点:如果结构非常大,则复制结构将增加内存要求,降低系统运行的速度。
例:
在默认情况下,C++函数按值传递参数,因此函数调用show_time(sum(trip, day3))将执行函数调用sum(trip, day3),以获得其返回值。然后,show_time()调用sum()的返回值(而不是函数本身)传递给show_time()。
例:
程序说明:
如何使用cin来控制while循环:
while(cin >> rplace.x >> rplace.y)
cin是istream类的对象。抽取运算符(>>)被设计成使得cin>>rplace.x也是一个istream对象。类运算符是使用函数实现的。使用cin>>rplace.x时,程序将调用一个函数,该函数返回一个istream值。将抽取运算符用于cin>>rplace.x对象(就像cin>>rplace.x>>rplace.y这样),也将获得一个istream对象。因此,整个while循环的测试表达式的最终结果为cin,而cin被用于测试表达式中时,将根据输入是否成功,被转换为bool值true或false。
在需要使用循环来输入数字时,别忘了使用这种方式。非数字输入将设置一个错误条件,禁止进一步读取输入。如果程序在输入循环后还需要进行输入,则必须使用cin.get()重置输入,然后还可能需要通过读取不合法的输入来丢弃它们。
2)传递结构的地址
假设要传递结构的地址而不是整个结构以节省时间和空间,则需要重新编写前面的函数,使用指向结构的指针。
- 调用函数时,将结构的地址(&pplace)而不是结构本身(pplace)传递给它;
- 将形参声明为指向polar的指针,即polar*类型。由于函数不应该修改结构,因此使用了const修饰符;
- 由于形参是指针而不是结构,因此应简洁成员运算符(->),而不是成员运算符(句号)。