1. 定义函数
C++对于返回值的类型有一定限制:不能是数组,但可以是其他任何类型——整数、浮点数、指针,甚至可以是结构和对象。(虽然C++函数不能直接返回数组,但可以将数组作为结构或对象组成部分来返回)。
函数是如何返回值的?通常,函数通过将返回值复制到指定的CPU寄存器或内存单元中来将其返回。随后,调用程序将查看该内存单元。返回函数和调用函数必须就该内存单元中存储的数据的类型达成一致。函数原型将返回值类型告知调用程序,而函数定义命令被调用函数应返回什么类型的数据。
调用函数什么时候结束?函数在执行返回语句后结束。如果函数包含多条返回语句(例如位于不同的if else选项中),则函数在执行遇到的第一条返回语句后结束。
函数原型:它经常隐藏在include文件中。为什么需要原型?原型描述了函数到编译器的接口,它将函数返回值的类型以及参数的类型和数量告诉编译器。C++允许将一个程序放在多个文件中,单独编译这些文件,然后再将它们组合起来。如果函数位于库中,情况也将如此。函数原型不要求提供变量名,有类型列表就足够了。通常,原型自动将被传递的参数强制转换为期望的类型(函数重载可能导致二义性,因此不允许某些自动强制类型转换)。
2. 引用变量
(1)临时变量、引用参数和const
如果实参和引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做。(为什么是const? 说白了就是只能用,不能改)
什么时候将创建临时变量? 1)实参的类型正确,但不是左值;2)实参类型不正确,但可以转换为正确的类型。
什么是左值?左值参数是可以被引用的数据对象,如变量、数组元素、结构成员、引用、解除引用的指针都是左值。非左值包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。在C语言中,左值最初指的是可出现在赋值语句左边的实体,但这是引入关键字const之前的情况。现在,常规变量和const变量都可以视为左值,因为可以通过地址访问它们。但常规变量属于可修改的左值,而const变量属于不可修改的左值。
应尽可能使用const。将引用参数声明为常量数据的引用的原因由三个:1)使用const可避免无意中修改数据的编程错误;2)使用const使函数能够处理const和非const实参,否则将只能接收非const实参;3)使用const引用使函数能够正确生成并使用临时变量。因此应尽可能将引用形参声明为const。
左值引用和右值引用。
(2)将引用用于结构——返回引用的例子
// 8.6
#include <iostream>
#include <string>
using namespace std;
struct free_throws {
string name;
int made;
int attempts;
float percent;
};
void display(const free_throws& ft);
void set_pc(free_throws& ft);
free_throws& accumulate(free_throws& target, const free_throws& source);
int main() {
free_throws one = { "Ifelsa Branch", 13,14 };
free_throws two = { "Andor knott", 10,16 };
free_throws three = { "Minnie Max", 7, 9 };
free_throws four = { "Whily Looper", 5, 9 };
free_throws five = { "Long Long", 6, 14 };
free_throws team = { "Throwgoods", 0, 0 };
free_throws dup;
set_pc(one);
display(one);
accumulate(team, one);
display(team);
display(accumulate(team, two));
accumulate(accumulate(team, three), four);
display(team);
dup = accumulate(team, five);
cout << "Displaying team:\n";
display(team);
cout << "Displaying dup after assignment:\n";
display(dup);
set_pc(four);
// 容易错误的语句,不应该使用
accumulate(dup, five) = four;
cout << "Displaying dup after ill advised assignment:\n";
display(dup);
return 0;
}
void display(const free_throws& ft) {
cout << "Name: " << ft.name << endl;
cout << " Made: " << ft.made << '\t';
cout << "Attemps: " << ft.attempts << '\t';
cout << "Percent: " << ft.percent << endl;
}
void set_pc(free_throws& ft) {
if (ft.attempts != 0)
ft.percent = 100.0f * float(ft.made) / float(ft.attempts);
else
ft.percent = 0;
}
free_throws& accumulate(free_throws& target, const free_throws& source) {
target.attempts += source.attempts;
target.made += source.made;
set_pc(target);
return target;
}
为何要返回引用?如果accumulate()返回一个结构,而不是指向结构的引用,将把整个结构复制到一个临时位置,再将这个拷贝复制给dup。但是返回值为引用时,将直接把team复制到dup,其效率更高。返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元引用。
将const用于引用返回类型。
(3)对象、继承和引用
继承的一个特征是,基类引用可以指向派生类对象,而无需进行强制类型转换。这种特征的一个实际结果是,可以定义一个接收基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。例如参数类型为ostream&的函数可以接受ostream对象(如cout)或您声明的ofstream对象作为参数。(ofstream类派生于ostream类)
程序8.8通过调用同一个函数(只有函数调用参数不同)将数据写入文件和显示到屏幕上来说明上述内容。该程序要求用户输入望远镜物镜和一些目镜的焦距,然后计算并显示每个目镜的放大倍数。放大倍数等于物镜的焦距除以目镜的焦距。该程序还使用了一些格式化方法,这些方法用于cout和ofstream对象(在这个例子中为fout)时作用相同。
// 8.8 filefunc.cpp -- function with ostream & parameter
#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;
void file_it(ostream& os, double fo, const double fe[], int n);
const int LIMIT = 5;
int main() {
ofstream fout;
const char* fn = "ep-data.txt";
fout.open(fn);
if (!fout.is_open()) {
cout << "Can't open " << fn << ". Bye.\n";
exit(EXIT_FAILURE);
}
double objective;
cout << "Enter the focal length of your telescope objective in mm: ";
cin >> objective;
double eps[LIMIT];
cout << "Enter the focal lengths, in mm, of " << LIMIT << " eyepieces: \n";
for (int i = 0; i < LIMIT; i++) {
cout << "Eyepieces #" << i + 1 << ": ";
cin >> eps[i];
}
file_it(fout, objective, eps, LIMIT);
file_it(cout, objective, eps, LIMIT);
cout << "Done\n";
return 0;
}
void file_it(ostream& os, double fo, const double fe[], int n) {
ios_base::fmtflags initial;
initial = os.setf(ios_base::fixed);
os.precision(0);
os << "Focal length of objective: " << fo << " mm\n";
os.setf(ios::showpoint);
os.precision(1);
os.width(12);
os << "f.1. eyepiece";
os.width(15);
os << "magnification" << endl;
for (int i = 0; i < n; i++) {
os.width(12);
os << fe[i];
os.width(15);
os << int(fo / fe[i] + 0.5) << endl; //四舍五入
}
os.setf(initial);
}
3. 默认参数、函数重载
默认参数让程序员能够使用不同数目的参数调用同一个函数,在函数原型声明中添加默认值,对于带参数列表的函数,必须从右向左添加默认值。函数重载(函数多态)让程序员能够使用多个同名的函数。C++使用上下文来确定要使用的重载函数版本。
关于函数重载的重要知识:
(1)函数重载的关键是函数的参数列表——函数特征标
函数特征标:参数数目、参数类型、参数的排列顺序(!不包括函数返回值、参数变量名)
使用被重载的函数时,需要在函数调用中使用正确的参数类型。
编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标。
将非const值赋给const变量合法,反之不合法。
(2)重载引用参数
类设计和STL经常使用引用参数。以下三个原型:
void sink(double& r1); // 左值引用参数r1与可修改的左值参数(如double变量)匹配;
void sink(const double& r2); // const左值引用参数r2与可修改的左值参数、const左值参数和右值参数(如两个double值的和)匹配;
void sink(double&& r3); // 右值引用参数r3与右值匹配;
如果重载使用这三种参数的函数,将调用最匹配的版本。这让程序员能够根据参数是左值、const还是右值来定制函数的行为。
(3)重载示例
// 8.10 leftover.cpp -- overloading the left() function
// 注意特殊情况
#include <iostream>
using namespace std;
unsigned long left(unsigned long num, unsigned ct);
char* left(const char* str, int n = 1);
int main() {
const char* trip = "Hawaii!! "; // test value
unsigned long n = 12345678; // test value
int i;
char* temp;
for (i = 1; i < 10; i++) {
cout << left(n, i) << endl;
temp = left(trip, i);
cout << temp << endl;
delete[] temp; // point to temporary storage
}
return 0;
}
// 本函数返回输入数字num的前ct个数字
unsigned long left(unsigned long num, unsigned ct) {
unsigned digits = 1;
unsigned long n = num;
if (ct == 0 || num == 0)
return 0;
while (n /= 10)
digits++; //看num一共多少位
if (digits > ct) {
ct = digits - ct;
while (ct--)
num /= 10;
return num;
}
else
return num; // 当返回位数大于数组实际位数时
}
// 本函数返回输入字符串的前n个字符组成的字符串的指针
char* left(const char* str, int n) {
if (n < 0)
n = 0;
int m = 0;
while (m < n && str[m])
m++;
char* ptr = new char[m + 1];
int i;
for (i = 0; i < m; i++)
ptr[i] = str[i];
while (i <= m)
ptr[i++] = '\0';
return ptr;
}
(4)何时使用函数重载
仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。
4. 函数模板
(1)示例:
// 8.11 funtemp.cpp -- using a function template
#include <iostream>
using namespace std;
// 模板函数声明
template<typename T>
void Swap(T& a, T& b);
int main() {
int i = 10;
int j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using compiler-generated int swapper:\n";
Swap(i, j);
cout << "Now i, j = " << i << ", " << j << ".\n";
double x = 24.5;
double y = 81.7;
cout << "x, y = " << x << ", " << y << ".\n";
Swap(x, y);
cout << "Using compiler-generated double swapper:\n";
cout << "Now x, y = " << x << ", " << y << ".\n";
return 0;
}
template<typename T>
void Swap(T& a, T& b) {
T temp;
temp = a;
a = b;
b = temp;
}
函数模板不能缩短可执行程序,最终的代码不包含任何模板,而只包含了为程序生成的实际函数。使用模板的好处是,它使生成多个函数定义更简单、更可靠。更常见的情形是,将模板放在头文件中,并在需要使用模板的文件中包含头文件。
重载的模板:
// 8.12 twotemps.cpp
#include <iostream>
using namespace std;
// 模板函数声明
template<typename T>
void Swap(T& a, T& b);
template<typename T>
void Swap(T* a, T* b, int n);
void Show(int a[]);
const int Lim = 8;
int main() {
int i = 10;
int j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using compiler-generated int swapper:\n";
Swap(i, j);
cout << "Now i, j = " << i << ", " << j << ".\n";
double x = 24.5;
double y = 81.7;
cout << "x, y = " << x << ", " << y << ".\n";
Swap(x, y);
cout << "Using compiler-generated double swapper:\n";
cout << "Now x, y = " << x << ", " << y << ".\n";
int d1[Lim] = { 0,7,0,4,1,7,7,6 };
int d2[Lim] = { 0,7,2,0,1,9,6,9 };
cout << "Original arrays:\n";
// 数组,只能传指针
Show(d1);
Show(d2);
Swap(d1, d2, Lim);
cout << "Swapped arrays:\n";
Show(d1);
Show(d2);
// cin.get();
return 0;
}
template<typename T>
void Swap(T& a, T& b) {
T temp;
temp = a;
a = b;
b = temp;
}
template<typename T>
void Swap(T a[], T b[], int n) {
T temp;
for (int i = 0; i < n; i++) {
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
void Show(int a[]) {
cout << a[0] << a[1] << "/";
cout << a[2] << a[3] << "/";
for (int i = 4; i < Lim; i++)
cout << a[i];
cout << endl;
}
(2)显示具体化
模板的局限性:编写的模板函数很可能无法处理某些类型,如结构数组等。一种解决方案是,重载运算符,另一种解决方案是,为特定类型提供具体化的模板定义。
显示具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
// 8.13 twoswap.cpp -- specialization overrides a template
#include <iostream>
using namespace std;
// 模板函数声明
template<typename T>
void Swap(T& a, T& b);
struct job {
char name[40];
double salary;
int floor;
};
template<> void Swap<job>(job& j1, job& j2);
void Show(job& j);
int main() {
int i = 10;
int j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using compiler-generated int swapper:\n";
Swap(i, j);
cout << "Now i, j = " << i << ", " << j << ".\n";
double x = 24.5;
double y = 81.7;
cout << "x, y = " << x << ", " << y << ".\n";
Swap(x, y);
cout << "Using compiler-generated double swapper:\n";
cout << "Now x, y = " << x << ", " << y << ".\n";
job sue = { "Susan Yaffee", 73000.60,7 };
job sidney = { "Sidney Taffee", 78060, 9 };
cout << "Before job swapping:\n";
Show(sue);
Show(sidney);
Swap(sue, sidney);
cout << "After job swapping:\n";
Show(sue);
Show(sidney);
cin.get();
return 0;
}
template<typename T>
void Swap(T& a, T& b) {
T temp;
temp = a;
a = b;
b = temp;
}
template<> void Swap<job>(job& j1, job& j2) {
double t1;
int t2;
t1 = j1.salary;
j1.salary = j2.salary;
j2.salary = t1;
t2 = j1.floor;
j1.floor = j2.floor;
j2.floor = t2;
}
void Show(job& j) {
cout << j.name << ": $" << j.salary << " on floor " << j.floor << endl;
}
(3)实例化和具体化
隐式实例化(implicit instantiation)
显示实例化(explicit instantiation):
template void Swap<int>(int, int);
// 其含义为:使用Swap()模板生成int类型的函数定义
显示具体化(explicit specialization)使用下面两个等价的声明之一:
template <> void Swap<int> (int& , int&);
template <> void Swap(int&, int&);
// 其含义为:不要使用Swap()模板来生成函数定义,而应使用专门为int类型显示地定义地函数定义。这些原型必须有自己的定义。
警告:试图在同一个文件(或转换单元)中使用同一种类型的显示实例和显示具体化将出错。
编译器选择使用哪个函数版本? 重载解析,部分排序规则(partial ordering rules)primer P309
(4)函数模板的发展
decltype关键字
后置返回类型