这一章在干嘛?
第 7 章学的是”C 就有的函数”,这一章全是 C++ 新增的函数特性:让函数”原地展开”的内联、给变量起别名的引用、能省略实参的默认参数、同名多形态的重载,以及”一次编写、处处适用”的函数模板。它们是 C++(plus 的部分)真正区别于 C 的地方,也是后续类设计的地基。
8.1 内联函数
普通函数调用是有代价的:程序要记住返回地址、把参数复制到栈、跳到函数地址执行、再跳回来。内联函数(inline function)让编译器直接把函数代码”复制粘贴”到调用处——不跳转,跑得更快,代价是代码变多(10 处调用就有 10 份副本)。
用法:在声明或定义前加 inline。惯用做法是省略原型,把整个定义放在原型该在的位置。编译器不保证照办——函数太大、有递归,都可能拒绝内联。

▲ 图 8.1 内联函数与常规函数的对比
// inline.cpp -- 使用内联函数
#include <iostream>
// 内联函数定义
inline double square(double x) { return x * x; }
int main()
{
using namespace std;
double a, b;
double c = 13.0;
a = square(5.0);
b = square(4.5 + 7.5); // 可以传表达式
cout << "a = " << a << ", b = " << b << "\n";
cout << "c = " << c;
cout << ", c squared = " << square(c++) << "\n";
cout << "Now c = " << c << "\n";
return 0;
}a = 25, b = 144
c = 13, c squared = 169
Now c = 14注意两点:① 内联函数同样按值传递——square(4.5 + 7.5) 先算出 12 再传入,square(c++) 先用 13 平方、c 再加一次 1,这远胜 C 的宏;② 定义放在首次使用之前,本身就充当了原型,所以传 int/long 也能自动转 double。
常见坑:把 C 的
#define宏当内联用
#define SQUARE(X) X*X是文本替换:SQUARE(4.5+7.5)展开成4.5+7.5*4.5+7.5(结果错),SQUARE(c++)让 c 自增两次。就算加满括号((X)*(X)),c++ 仍会加两次。函数式服务请一律改用 C++ 内联函数。
判断标准很简单:函数体一两行能放下就适合内联,写不下就不适合。
8.2 引用变量
引用(reference)是已定义变量的别名(alias)。声明里的 & 不是取地址符,而是类型名的一部分——int & 意为”指向 int 的引用”:
// firstref.cpp -- 定义和使用引用
#include <iostream>
int main()
{
using namespace std;
int rats = 101;
int & rodents = rats; // rodents 是引用
cout << "rats = " << rats;
cout << ", rodents = " << rodents << endl;
rodents++;
cout << "rats = " << rats;
cout << ", rodents = " << rodents << endl;
// 某些实现需要把地址转换为 unsigned 再输出
cout << "rats address = " << &rats;
cout << ", rodents address = " << &rodents << endl;
return 0;
}rats = 101, rodents = 101
rats = 102, rodents = 102
rats address = 0x0065fd48, rodents address = 0x0065fd48值相同、地址相同——rodents++ 加的是同一个变量的两个名字之一。
引用像”伪装的 const 指针”,但有三个关键差异:
- 声明时必须初始化,之后不能改投他主;
int & rodents = rats;本质上等价于int * const pr = &rats;,用 rodents 就像用*pr;- 给引用赋值 = 给原变量赋值,不是换绑定。
// secref.cpp 的核心实验
int rats = 101;
int & rodents = rats; // rodents 绑定 rats
int bunnies = 50;
rodents = bunnies; // 不是改绑!等价于 rats = bunnies;
// 输出:bunnies = 50, rats = 50, rodents = 50
// rats 和 rodents 地址仍相同,与 bunnies 的地址不同常见坑:以为
rodents = bunnies;是让引用改指 bunnies引用一旦绑定终身不换。这条语句只是把 bunnies 的值赋给了 rats(rodents 只是别名)。想换绑定对象,引用做不到——那是指针的活。
8.3 按引用传递
引用的主战场是函数参数:形参成为调用程序中变量的别名,函数直接操作原件——这就是按引用传递(passing by reference),打破了 C 只有按值传递的传统。

▲ 图 8.2 按值传递与按引用传递
用经典的”交换两个数”对比三种方案:
// swaps.cpp -- 用引用和用指针交换
#include <iostream>
void swapr(int & a, int & b); // a、b 是 int 的别名
void swapp(int * p, int * q); // p、q 是 int 的地址
void swapv(int a, int b); // a、b 是新变量
int main()
{
using namespace std;
int wallet1 = 300;
int wallet2 = 350;
swapr(wallet1, wallet2); // 传变量
swapp(&wallet1, &wallet2); // 传地址
swapv(wallet1, wallet2); // 传值(交换失败)
return 0;
}
void swapr(int & a, int & b) // 用引用
{
int temp;
temp = a;
a = b;
b = temp;
}
void swapp(int * p, int * q) // 用指针
{
int temp;
temp = *p;
*p = *q;
*q = temp;
}
void swapv(int a, int b) // 用值(没用)
{
int temp;
temp = a;
a = b;
b = temp;
}结果:引用和指针版本都交换成功,按值版本失败(交换的是复印件)。有意思的是 swapr(wallet1, wallet2) 和 swapv(wallet1, wallet2) 调用写法一模一样,只有看原型才知道谁按引用——而指针版本必须写 &wallet1,一眼可辨。
再看重量的另一面——引用会误伤原件:
// cubes.cpp -- 常规参数与引用参数
#include <iostream>
double cube(double a);
double refcube(double &ra);
int main ()
{
using namespace std;
double x = 3.0;
cout << cube(x);
cout << " = cube of " << x << endl;
cout << refcube(x);
cout << " = cube of " << x << endl;
return 0;
}
double cube(double a)
{
a *= a * a;
return a;
}
double refcube(double &ra)
{
ra *= ra * ra;
return ra;
}27 = cube of 3
27 = cube of 27cube() 按值,x 不变;refcube() 按引用,x 被改成 27。只想”用”不想”改”就加 const:double refcube(const double &ra);。
临时变量与 const 引用
实参类型不匹配或不是左值(lvalue,能被地址引用的数据对象:变量、数组元素、结构成员、解引用的指针等)时,若形参是 const 引用,编译器会生成一个临时匿名变量让引用指向它。两种情形:实参类型正确但非左值(如 x+3.0、字面量 7.0),或类型不对但可转换(如把 long edge 传给 const double &)。
为什么只对 const 开绿灯?想象 swapr(a, b) 收到两个 long——若允许生成临时 int 变量,交换发生在临时变量上,a、b 原封不动,函数”假装成功”。所以:想修改实参的函数禁止临时变量;const 引用只是读数据,临时变量无害且让函数更通用(顺便模拟了按值传递的宽容)。
通关标准:
能说出”能用 const 就用 const”的三个理由:防手滑改数据;让函数同时接受 const 和非 const 实参;必要时可用临时变量处理非左值/类型不符的实参。
C++11 还新增了右值引用 double && rref = std::sqrt(36.0);,可绑定字面量、表达式这类右值,主要服务于移动语义(第 18 章)。原来的 & 引用现在叫左值引用。
8.4 引用与结构体和类对象
引用正是为结构体、类这类大块头而生。用法和基本类型一样:void set_pc(free_throws & ft); 修改原结构,void display(const free_throws & ft); 只读。看完整例子(strc_ref.cpp,记录投篮统计):
//strc_ref.cpp -- 使用结构体引用
#include <iostream>
#include <string>
struct free_throws
{
std::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()
{
// 部分初始化——剩余成员置 0
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);
dup = accumulate(team, five); // 返回值用于赋值
set_pc(four);
accumulate(dup, five) = four; // 不推荐的用法
return 0;
}
void display(const free_throws & ft)
{
using std::cout;
cout << "Name: " << ft.name << '\n';
cout << " Made: " << ft.made << '\t';
cout << "Attempts: " << ft.attempts << '\t';
cout << "Percent: " << ft.percent << '\n';
}
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;
}程序把 team 的投篮数据逐轮累加(13/14 → 23/30 → 35/48 → 41/62),体现了三个要点:
- 返回引用:返回类型
free_throws &时,return target;返回的是 team 原件本身(不是副本),省去整结构复制;display(accumulate(team, two))等价于先 accumulate 再 display(team)。 - 别返回函数内的局部变量:局部变量随函数结束消亡,返回它的引用就是悬空引用——这是本节最大的雷。
- 返回引用让
accumulate(dup, five) = four;能编译(返回值是可修改的左值),但它先加后覆盖,纯属自找麻烦。把返回类型改成const free_throws &即可封死这种写法。
引用与类对象:ostream 的魔法
类的常用传参方式就是 const 引用。更妙的是继承带来的性质:基类引用可以指向派生类对象,无需类型转换。ostream & 形参既能收 cout(ostream 对象),也能收 fout(ofstream 文件流对象)——一个函数同吃屏幕和文件:
//filefunc.cpp -- 使用 ostream & 参数的函数
#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);
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 << "Eyepiece #" << 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.l. 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); // 恢复初始格式状态
}输入物镜焦距 1800mm 和目镜焦距 30、19、14、8.8、7.5,得到放大倍数 60、95、129、205、240,同时写入文件和屏幕。附加福利:setf() 返回调用前的格式设置(存进 ios_base::fmtflags),函数结束时还原——每个对象存各的格式,互不干扰。
8.5 何时使用引用
两条理由:修改调用程序中的数据;传大对象时省去复制。决策口诀:
- 函数不改数据:小对象(内置类型/小结构)按值传;数组用指针(唯一选择,加 const);较大的结构用 const 指针或 const 引用;类对象用 const 引用(C++ 的标准姿势)。
- 函数要改数据:内置类型用指针(
fixit(&x)一眼看出要改 x);数组用指针;结构用引用或指针;类对象用引用。
当然有例外:cin >> n 对基本类型就用引用,否则你得起 cin >> &n 了。
8.6 默认参数
默认参数:调用时省略的实参自动用预设值。必须在原型里从右往左设置(右边没默认的参数不能有默认):
int harpo(int n, int m = 4, int j = 5); // 合法
int chico(int n, int m = 6, int j); // 非法!j 在 m 右边却没默认值实参从左往右依次分配,不能跳着给:harpo(3, ,8) 非法。harpo(2) 即 harpo(2,4,5),harpo(1,8) 即 harpo(1,8,5)。
// left.cpp -- 带默认参数的字符串函数
#include <iostream>
const int ArSize = 80;
char * left(const char * str, int n = 1);
int main()
{
using namespace std;
char sample[ArSize];
cout << "Enter a string:\n";
cin.get(sample,ArSize);
char *ps = left(sample, 4);
cout << ps << endl;
delete [] ps; // 释放旧字符串
ps = left(sample); // n 用默认值 1
cout << ps << endl;
delete [] ps; // 释放新字符串
return 0;
}
// 返回指向新字符串的指针,内容为 str 的前 n 个字符
char *left(const char *str, int n)
{
if(n < 0)
n = 0;
char *p = new char[n+1];
int i;
for (i = 0; i < n && str[i]; i++)
p[i] = str[i]; // 复制字符
while (i <= n)
p[i++] = '\0'; // 其余补 '\0'
return p;
}输入 forthcoming,输出 fort 和 f。注意默认值只写在原型里,函数定义与平常无异。循环条件 i < n && str[i] 一石二鸟:既限定 n 个字符,又在遇到 \0(字符串太短)时提前收手。
常见坑:在定义里重复写默认值
默认值只能出现在原型中。若定义里再写一次
int n = 1,多数编译器会报错。也别忘了原型里的默认值只此一份,改默认值只需改原型。
8.7 函数重载
函数重载(function overloading,也叫函数多态):多个函数共用一个名字,靠函数签名(argument list,参数的个数、类型和顺序,与变量名和返回类型无关)区分。就像动词 “root” 多义——为球队喝彩、在土里拱松露——语境(实参类型)自动决定用哪个:
void print(const char * str, int width); // #1
void print(double d, int width); // #2
void print(long l, int width); // #3
void print(int i, int width); // #4
void print(const char *str); // #5
print("Pancakes", 15); // 用 #1
print(1999, 12); // 用 #4
print(1999L, 15); // 用 #3三个雷区:
- 类型转换会造成二义:
unsigned year; print(year, 6);不匹配任何原型,编译器试图标准转换,但 2/3/4 三个都候选——拒绝调用。只留一个原型时转换才能进行。 - 类型和它的引用是同一签名:
double cube(double x)与double cube(double & x)不能共存,cube(x)匹配不了到底哪个。 - 返回类型不算签名:
long gronk(int, float)和double gronk(int, float)不能重载;签名不同才可以。
const 与非 const 倒是能区分:dribble(char *) 和 dribble(const char *) 是合法重载,编译器按实参是否 const 选择。
C++ 编译器用名称修饰(name decoration/mangling)实现重载——它根据参数类型把名字加密成 ?MyFunctionFoo@@YAXH 这类内部符号,签名不同、符号不同。
重载引用参数(C++11 三件套)
void stove(double & r1); // 匹配可修改左值
void stove(const double & r2); // 匹配 const 左值
void stove(double && r3); // 匹配右值stove(x) 用第一版、stove(y)(y 为 const)用第二版、stove(x+y) 用第三版——精确匹配者优先。
原书还给出一个完整例子(Listing 8.10,leftover.cpp):字符串版 left(const char *, int) 与整数版 left(unsigned long, unsigned) 共存于同一程序,分别取字符串前 n 个字符和整数前 ct 位数字(用反复除以 10 的技巧数位、砍位)。这正是重载的典型场景:同一任务、不同数据形式。
常见坑:滥用重载
只为”基本相同的任务”重载。若只是参数个数不同,用默认参数更省事(一个函数、一份内存、一处修改);参数类型不同时才轮到重载出场。
8.8 函数模板
函数模板(function template)是通用函数描述:用泛型代替具体类型,编译器按实参类型自动生成对应版本——这叫泛型编程,类型成了参数(parameterized types)。手写多个版本费时易错:全局把 int 替换成 double,连 short interval 都会惨变成 short doubleerval。
// funtemp.cpp -- 使用函数模板
#include <iostream>
// 函数模板原型
template <typename T> // 或 class T
void Swap(T &a, T &b);
int main()
{
using namespace std;
int i = 10;
int j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using compiler-generated int swapper:\n";
Swap(i,j); // 生成 void Swap(int &, int &)
cout << "Now i, j = " << i << ", " << j << ".\n";
double x = 24.5;
double y = 81.7;
cout << "x, y = " << x << ", " << y << ".\n";
cout << "Using compiler-generated double swapper:\n";
Swap(x,y); // 生成 void Swap(double &, double &)
cout << "Now x, y = " << x << ", " << y << ".\n";
return 0;
}
// 函数模板定义
template <typename T> // 或 class T
void Swap(T &a, T &b)
{
T temp; // temp 是 T 类型的变量
temp = a;
a = b;
b = temp;
}i, j = 10, 20.
Using compiler-generated int swapper:
Now i, j = 20, 10.
x, y = 24.5, 81.7.
Using compiler-generated double swapper:
Now x, y = 81.7, 24.5.要点:typename 和 class 在此语境完全等价(typename 更直白);模板本身不是函数,只是生成函数的”图纸”,编译器把 T 替换成 int、double 各造一份——可执行程序不会因此变短,省的是你的编写时间和出错率。模板通常放头文件里(第 9 章讲头文件)。
重载模板
算法对不同类型不适配时,可以像函数一样重载模板(签名须不同)。Listing 8.12(twotemps.cpp)给 Swap 加了一个交换两个数组的版本:
// twotemps.cpp -- 重载模板函数
template <typename T> // 原模板
void Swap(T &a, T &b);
template <typename T> // 新模板
void Swap(T *a, T *b, int n);
...
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;
}
}Swap(i,j) 匹配原模板,Swap(d1,d2,Lim)(两个 int 数组加个数)匹配新模板。
显式具体化
模板有假设(a=b 要求类型支持赋值,a>b 要求支持 >,结构体、数组名都会翻车)。解法之一是显式具体化(explicit specialization):对特定类型给出专门定义。C++98 规矩:
- 同名函数可同时有:非模板函数、模板函数、显式具体化模板函数;
- 具体化的原型和定义前加
template <>并指名类型; - 优先级:非模板函数 > 显式具体化 > 模板生成的版本。
// twoswap.cpp -- 具体化覆盖模板
struct job { char name[40]; double salary; int floor; };
template <typename T>
void Swap(T &a, T &b); // 模板
template <> void Swap<job>(job &j1, job &j2); // 针对 job 的显式具体化
// <job> 可省:template <> void Swap(job &, job &);
// 只交换 job 的 salary 和 floor 字段
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;
}Swap(i,j) 走模板生成 int 版;Swap(sue, sidney)(两个 job)走具体化版——名字不动,工资和楼层对调。
实例化与具体化辨析
- 隐式实例化:编译器看到
Swap(i,j)自动生成 int 版; - 显式实例化:直接下命令
template void Swap<int>(int, int);(用模板造 int 版); - 显式具体化:
template <> void Swap<job>(job &, job &);(不要用模板,用我专门写的定义)。
区分口诀:显式实例化 template 后没有 <>;显式具体化 template 后有 <>。同一文件里对同一类型既显式实例化又显式具体化是错误。Add<double>(x, m) 这种写法还能强制实例化并顺手类型转换——但 Swap<double>(m, x) 不行,因为 double & 引用不了 int 变量。
8.9 模板进阶与 C++11 新工具
重载解析:编译器怎么挑函数
面对重载 + 模板的大杂烩,编译器三阶段决策:①收集候选(同名函数与模板);②筛出可行(参数个数对、存在隐式转换序列);③选出最佳,选不出就报错。转换优劣排序:
- 完全匹配(普通函数优先于模板)
- 提升转换(char/short→int,float→double)
- 标准转换(int→char,long→double)
- 用户自定义转换
完全匹配允许表 8.1 的无关紧要转换(trivial conversions):Type→Type &、Type &→Type、Type []→Type *、Type→const Type、Type *→const Type * 等。所以 recycle(ink) 对四个原型 recycle(blot)、recycle(const blot)、recycle(blot &)、recycle(const blot &) 都是精确匹配——直接二义报错。两个例外能救场:非 const 数据优先匹配非 const 指针/引用形参;非模板优先于模板。两个模板打平时,更具体(partially ordered,推导时转换更少)者胜——比如 recycle(Type *) 比 recycle(Type) 更具体,recycle(&ink) 会选前者。
Listing 8.14(tempover.cpp)实战了这条规则:ShowArray(things, 6)(int 数组)用模板 A;ShowArray(pd, 3)(double 指针数组)同时匹配 A(T=double *,显示地址)和 B(T=double,显示 *arr[i] 值),B 更具体而胜出,输出 2400 1300 1800 而非三个地址。Listing 8.15(choices.cpp)则展示了人为干预:lesser<>(m, n) 用尖括号强令编译器选模板,lesser<int>(x, y) 强制 int 实例化(double 被截断成 15)。
decltype:这个类型是什么?
模板里的经典难题:?type? xpy = x + y;——T1、T2 相加结果的类型,写代码时无法预知。C++11 的 decltype 按四步清单推断类型:
- 未加括号的标识符 → 与该标识符同类型(含 const 等限定符);
- 函数调用 → 返回类型(不真正调用,查原型即可);
- 左值 → 该类型的引用(
decltype((xx))因为多了一层括号,得到double &); - 以上皆非 → 与表达式同类型(
decltype(k+n)虽然是两个引用相加,结果是 int)。
于是:decltype(x + y) xpy = x + y; 一行搞定。
后置返回类型
返回类型也依赖参数时(?type? gt(T1 x, T2 y) { return x + y; }),decltype 帮不上——函数名处 x、y 还没声明。C++11 的后置返回类型(trailing return type)把返回类型挪到参数表后面:
double h(int x, float y);
// 等价于
auto h(int x, float y) -> double;
// 模板问题的最终解法
template<class T1, class T2>
auto gt(T1 x, T2 y) -> decltype(x + y)
{
...
return x + y;
}auto 在这里是占位符,真正的类型由 -> decltype(...) 提供——此时 x、y 已在作用域内。
自测:
int & rodents = rats; rodents = bunnies;之后 rats 是多少?是 bunnies 的值(50)。引用不能改绑,
rodents = bunnies等价于rats = bunnies,rats 与 rodents 的地址依旧相同。
自测:为什么
refcube(x + 3.0)不合法,而refcube(const double &)版本就可以?
x+3.0不是左值,非 const 引用不能绑定它;const 引用允许编译器生成临时变量存放表达式值,引用指向临时变量,函数结束时丢弃。
自测:
swapr(int & a, int & b)收到两个 long 实参会发生什么?类型不匹配。若允许生成临时 int 变量,交换将发生在临时变量上,原数据纹丝不动——函数白干。所以 C++ 禁止非 const 引用生成临时变量。
自测:为什么返回局部变量的引用是致命错误?
局部变量在函数返回时消亡,返回的引用指向一块已失效的内存,程序一旦使用就崩溃(strquote.cpp 的 version3 演示了这种崩溃)。正确做法:返回传入的引用参数,或用 new 在堆上创建的对象。
自测:
double cube(double x)和double cube(double & x)能共存吗?不能。编译器把”类型”和”该类型的引用”视为同一签名,
cube(x)无法决定调用哪个。反之cube(double)与cube(double &&)、const 与非 const 指针/引用参数则可以构成合法重载。
自测:显式实例化和显式具体化长得像,怎么区分?
看关键字后有没有
<>:template void Swap<int>(int, int);(无尖括号)是显式实例化——“用模板给我造一个 int 版”;template <> void Swap<job>(job &, job &);(有尖括号)是显式具体化——“别用模板,用我专门为 job 写的定义”。优先级:非模板 > 具体化 > 模板实例。
通关标准:
能独立完成:① 用 const 引用改写第 7 章的结构体函数并解释效率收益;② 写出带默认参数的原型并说明”从右往左”限制;③ 举出两个合法重载与两个非法重载的例子;④ 手写一个 Swap 模板并为自定义结构写出显式具体化,说清编译器选择函数的优先顺序。