☰
C++ Primer Plus第6版第11章编程练习全解:运算符重载、友元函数与类设计
2026/10/13 12:12:31 网站建设 项目流程

《C++ Primer Plus 第6版》第11章是我见过劝退率最高的一章。前面你还在写简单类,到这里突然要求你用类去重载运算符、写友元函数、做类型转换,课后编程练习的难度直接从“填空”跳到“综合”。我当初学这章时,花了整整一个周末才把 Stonewt、Vector、Complex 这几个练习题全部跑通。这篇就把我整理的 C++ Primer Plus 第6版第11章编程练习答案(全)分享出来,不过与其说给你抄,不如说是把每道题背后的设计点讲透,你拿到任何一版题目都能套进去。适合正在做课后题、或者做完想对答案的朋友。

1. 第11章到底在练什么:先搞懂运算符重载的底层逻辑

1.1 这一章的练习到底在考什么

第11章的核心不是让你背语法,而是逼你完成一轮“类设计思维升级”。前面的章节里,类里放的都是一般成员函数,调用方式是obj.method(),这没什么好纠结的。但到了第11章,练习里大量出现v1 + v2、cout << obj、3.0 * v这种平时看惯了的表达式。你作为一个类的设计者,得决定这些表达式到底走哪个函数、中间发生了几次拷贝、谁可以访问私有成员。

我手边这版教材的课后练习,主要题型集中在三个类上:Stonewt 的两次改造,Vector 的完整重载,再加上一个 Complex 或 Time 类的综合题。Stonewt 考的是关系运算符和流运算符,Vector 考的是二元运算符与静态成员,Complex 或 Time 则把加减乘除、前置后置、输入输出全部串起来。你把这三个吃透,第11章练习基本没有死角。

1.2 一个运算符该写成成员还是友元?我的判断标准

这是整章练习最容易卡住的第一关。很多人写operator+时习惯性写成成员函数,发现a + b能用,但3.0 * v编译不过,然后就开始怀疑编译器是不是坏了。

其实判断标准很朴素:如果二元运算符的左操作数是类对象,而且这个类需要访问私有成员,那么写成成员函数是自然的,因为左操作数就是this。但如果左操作数可能是内置类型,或者你希望它参与隐式类型转换,就必须写友元函数。典型例子是cout << obj,左操作数是ostream,你不可能去修改标准库类,所以必须用友元函数来重载operator<<。另一个例子是3.0 * v,3.0 不是 Vector 对象,编译器没办法把它当作成员函数的调用者,只能靠普通函数或友元函数接住。

还有一个容易忽略的点:成员运算符函数如果不写const,那么常量对象调用它时会报“discards qualifiers”之类的错误。我建议所有不修改对象内容的运算符重载都写成const,返回值尽量按值返回新对象。这样既避免意外修改,也能兼容常量对象。

1.3 返回值类型选择:按值返回新对象,还是返回引用?

运算符重载的返回值类型经常被写成“凭感觉”。我的经验是:operator+、operator-、operator*这类原地生成新结果的运算符,一律按值返回新对象。理由很简单,a + b的结果是一个全新的值,它不应该和a或b有任何别名关系。如果返回引用,其实是引用了某个临时对象或原对象,调用链稍长就会出现悬空引用。

但是operator+=、operator*=这类复合赋值运算符,它们修改的是左操作数本身,所以应该返回*this的引用。operator++前置版本返回引用,后置版本返回旧值的副本。这个规则在后面的 Complex 和 Time 类练习里会反复出现。简单记法就是:表达式的值语义用返回值,表达式的赋值语义用引用。

2. Stonewt 两道题:关系运算符、状态显示与 << 重载

2.1 第一题:六个关系运算符不一定要写六份独立逻辑

Stonewt 这个类在书里出现过很多次,成员有stone、pds_left、pounds,其中pounds是以磅为单位的完整重量。第一题通常会要求重载全部六个关系运算符:<、>、<=、>=、==、!=。

最直接的写法是六个函数都去比较pounds成员。但我不想把同样的逻辑复制六遍,所以我用两个基础运算符去推导其它四个。下面是我常用的实现思路:

bool Stonewt::operator<(const Stonewt & st) const { return pounds < st.pounds; } bool Stonewt::operator==(const Stonewt & st) const { return fabs(pounds - st.pounds) < 1e-8; } bool Stonewt::operator>(const Stonewt & st) const { return st < *this; } bool Stonewt::operator<=(const Stonewt & st) const { return !(st < *this); } bool Stonewt::operator>=(const Stonewt & st) const { return !(*this < st); } bool Stonewt::operator!=(const Stonewt & st) const { return !(*this == st); }

这里有两个值得说的地方。第一,operator==在处理浮点数时不要用裸的pounds == st.pounds,因为浮点精度问题会让两个理论相等的重量被判为不相等。第二,>直接复用st < *this,<=复用! (st < *this),逻辑上干净很多。初学阶段完全可以逐个写,但能推导就不要复制,后面维护起来省心。

另外这题一般还要加一个成员函数,以整数形式返回以英石为单位的重量。比如:

int Stonewt::stone_val() const { return stone; }

注意这个函数不需要重写什么重量计算,直接返回已有的stone成员即可。如果题目强调“以整数形式返回”,那就确保返回值类型是int,不要手滑写成double。

2.2 第二题:重载 << 必须用友元,状态成员别滥用

第二题会给 Stonewt 增加一个“显示状态”成员,让对象能以三种方式输出:纯英石、纯磅、英石加剩余磅。这题有两个考点,一个是状态枚举成员,一个是operator<<的友元重载。

状态成员本身不复杂,通常就是一个枚举:

enum Mode { STONE, POUNDS, STONE_POUNDS }; Mode mode;

构造函数里给它设默认值,再提供几个成员函数切换状态,比如set_stone_mode()、set_pounds_mode()、set_stone_pounds_mode()。真正容易错的是operator<<。它必须是友元,因为左操作数是ostream对象,不是 Stonewt 对象。

ostream & operator<<(ostream & os, const Stonewt & st) { if (st.mode == Stonewt::STONE) os << st.stone << " stone"; else if (st.mode == Stonewt::POUNDS) os << st.pounds << " pounds"; else os << st.stone << " stone, " << st.pds_left << " pounds"; return os; }

这个函数我踩过一次坑:忘了在函数开头加friend声明,然后编译报错说private成员不可访问。还有一种错误是返回类型写成了ostream而不是ostream &,这样会导致连续输出失效。return os;是必须的,因为<<是左结合的,cout << a << b会先得到cout,再继续往左操作数上执行下一次输出。

3. Vector 类练习:从二维向量到极坐标转换

3.1 Vector 类的基本形态:直角坐标与极坐标必须同步更新

Vector 类的练习通常比 Stonewt 更综合,因为它的内部同时维护两组表示:直角坐标x、y,极坐标mag、ang。书里这个类用mode来标识当前构造时传入的是哪种坐标形式,内部有一套私有的set_x()、set_y()、set_mag()、set_ang()函数,保证修改任何一个坐标后,另外一组数据也能同步更新。

写这个类时最忌讳的是“两个坐标系统各改各的”。比如你直接修改x却忘了更新mag,那对象内部的极坐标数据就是脏的,后面的operator<<会根据 mode 输出极坐标时给出一个错误的模长。

我一般建议把更新逻辑全部收敛到私有函数里。构造函数传入x、y或mag、ang后,只调用对应的set函数,不要在外面单独操作成员。这样后续重载运算符时,永远基于直角坐标去计算,然后让构造函数负责同步极坐标。

3.2 加减法:返回新对象,别污染原对象

Vector 的加法在数学上就是分量相加。重载时需要注意,operator+是一个二元运算符,左操作数是当前对象,右操作数通过参数传入。同时这个方法必须是const,因为加法不应该改变v1,只能生成一个新向量。

Vector Vector::operator+(const Vector & v) const { return Vector(x + v.x, y + v.y); } Vector Vector::operator-(const Vector & v) const { return Vector(x - v.x, y - v.y); }

这里有一个很容易被忽略的问题:如果当前对象的mode是极坐标,而参数对象是直角坐标,那么x + v.x和y + v.y依然是直角坐标运算,所以返回的新 Vector 最好用直角坐标构造。书里的构造函数默认form == 'r',所以直接写Vector(x + v.x, y + v.y)就没问题。如果你写的是自定义构造函数,千万记住别把极坐标的成员mag拿来直接相加,那是错的。

3.3 乘法:一个成员函数加一个友元函数

Vector 练习里一般会出现v * 2.5和2.5 * v两种写法。第一种可以写成成员函数,因为左操作数是 Vector 对象。第二种就必须用友元函数,因为左操作数是一个double。

Vector Vector::operator*(double mult) const { return Vector(x * mult, y * mult); } Vector operator*(double mult, const Vector & v) { return v * mult; }

注意友元函数里直接调用v * mult是在调用上面那个成员函数,相当于做了一个简单的转发。这样写的好处是,实际乘法逻辑只有一份,不需要在两个函数里各写一遍。如果你还在练习中看到一元负号的需求,比如-v,一般是这样:

Vector Vector::operator-() const { return Vector(-x, -y); }

一元负号是单目运算符,没有参数,返回的是按值构造的新向量,同样要加const。

3.4 对象计数:静态成员是个高频坑

Vector 练习里经常有一个“跟踪当前存在多少个 Vector 对象”的扩展任务。这个考点看起来简单,但每次都能坑掉不少人,主要坑在静态成员的定义位置。

类内声明只能声明静态成员,不能初始化静态成员。你必须把这个定义放在全局作用域,通常是放在类定义后面的源文件里:

int Vector::count = 0;

构造函数里写++count;,析构函数里写--count;。这样统计的是当前存活对象数量。如果题目问的是累计创建过多少个对象,那析构函数里就不要递减。

还要注意,静态成员函数只能访问静态成员,它没有this指针。如果你写了一个static int how_many() { return count; },那么调用时用Vector::how_many(),而不是某个对象去调用。频繁复制、赋值时,拷贝构造函数也要记得处理计数,否则对象的数量统计就是错的。

4. Complex 和 Time:把综合题一次写对

4.1 Complex 类的重载清单

很多版本的课后练习会把复数类 Complex 放在最后作为综合题。复数运算本质上就是二维平面上的向量运算,但多了乘法的特殊规则,所以比 Vector 多一层逻辑。一个常规 Complex 类至少要支持以下操作:

  • c1 + c2、c1 - c2
  • c1 * c2
  • c1 * 3.0和3.0 * c1
  • cin >> c、cout << c

加法和减法没什么好说的,直接对实部和虚部分别运算即可。乘法的公式是:

Complex Complex::operator*(const Complex & c) const { return Complex(real * c.real - imag * c.imag, real * c.imag + imag * c.real); }

复数乘法不是对应分量相乘,我见过有人写成real * c.real和imag * c.imag,那结果完全不对。这里还是建议先回忆一下复数公式:实部等于ac - bd,虚部等于ad + bc。

复数乘实数也常常考。成员函数版本接收一个double,友元版本接收一个double作为左操作数,然后转发回去:

Complex Complex::operator*(double n) const { return Complex(real * n, imag * n); } Complex operator*(double n, const Complex & c) { return c * n; }

输入输出重载是最后的重点。很多人会把提示语直接写在operator>>里,比如:

istream & operator>>(istream & is, Complex & c) { cout << "real: "; is >> c.real; cout << "imaginary: "; is >> c.imag; return is; }

这种写法在交互式练习里能跑通,但有一个坏处:如果输入源是文件流而不是终端,提示语也会被写进文件,这就污染了数据。更规范的做法是operator>>只负责读取两个数字,提示语放在main函数里输出。练习阶段两种都可以,但你要知道自己写的是交互友好型还是数据流友好型。

4.2 Time 类扩展:前置后置递增的写法

如果你的作业或练习把最后的综合题改成了 Time 类,那恭喜你,考点会多一个递增运算符。Time 类一般维护小时和分钟两个成员。++的前置版本先增加再返回自身,后置版本必须保存旧值然后返回旧值。

Time & Time::operator++() { minutes++; if (minutes >= 60) { hours++; minutes -= 60; } return *this; } Time Time::operator++(int) { Time temp = *this; ++*this; return temp; }

这里最诡异的地方是 C++ 语法:后置operator++的括号里要写一个int参数,但这个参数在函数体里通常不用,只是为了和前置版本做区分。返回类型也不一样,前置返回Time &,后置返回Time。如果你把后置版本也写成返回引用,那它返回的就是悬空引用,指向一个已经销毁的临时变量。这个问题在面试和考试里都很经典,需要格外注意。

5. 编译调试与常见错误排查

5.1 这些错误我几乎每道题都会遇到

我在完成这章练习时,编译器的报错信息远比运行结果多。下面这张表是我个人的高频踩坑记录,你可以先存着,遇到类似报错直接对照。

编译错误原因处理方式
no match for 'operator<<'没有为自定义类重载<<,或重载函数不是友元添加友元声明,返回ostream &
undefined reference to Vector::count静态成员只声明,没有在类外定义在源文件里写int Vector::count = 0;
passing 'const Stonewt' as 'this' argument discards qualifiers成员函数没有加const,但对象是常量对象把不修改成员的操作符函数加上const
candidate expects 1 argument, 2 provided成员函数重载二元运算符时和友元函数重复声明,参数数量冲突检查是否同时把同一个运算符既写成成员又写成非友元函数
程序输出0 stone, 0 pounds但构造时有值构造函数初始化顺序错误,或operator<<访问了错误的成员检查成员初始化顺序,确认pounds是否被正确赋值

第三类错误尤其常见。比如你在main里定义了一个const Stonewt a(10);,然后调用a < b,如果operator<没有const,编译器会报错。因为常量对象只能调用常量成员函数。

5.2 我的验证顺序

写完代码别急着交,我一般按三步验证。第一步,先写一个最简单的main,只构造几个对象,分别用成员函数和友元函数输出,确认基本功能正常。第二步,测试边界情况,比如 Vector 的零向量加法、Stonewt 的重量等于 0、Complex 的复数 0、负数乘 Vector。第三步,故意写出“错误”的调用方式,比如2.5 * v,确认编译器能透明地调用友元版本。这个方法看起来啰嗦,但能把隐患提前暴露。

6. 易错点速查与自查清单

6.1 一张表记住最关键的 5 个规范

把第11章练习里最常见的规范总结成一张表,写代码前扫一眼,能省很多编译时间。

规范正确写法常见错误
operator<<返回值ostream &,最后return os;返回void或ostream,导致无法连续输出
二元算术运算符成员函数加const,按值返回新对象忘记const,或返回引用导致悬空
左操作数为内置类型必须用友元函数只写成员函数,导致2.5 * v编译失败
静态成员定义在类外初始化,如int Vector::count = 0;只在类内声明,链接时报 undefined reference
转换函数不写返回类型,但函数名是operator type()写了void或返回类型,编译器直接不认

6.2 自查清单

最后,我在做每一道练习题时都会问自己五个问题,你也可以照着自查:

  • 这个运算符的左操作数是不是我的类对象?如果是内置类型,我写友元了吗?
  • 所有不改变对象内容的运算符函数都加了const吗?
  • 静态变量在类外定义了吗?如果构造函数和析构函数都动了计数器,拷贝构造有没有同步?
  • operator<<返回的是ostream &吗?最后有没有return os?
  • 转换函数有没有写返回类型?写了就删掉,转换函数本身不能声明返回类型。

我自己在实际操作中最大的体会是:第11章练习并不是在考你记住多少语法,而是在考你能不能像一个库设计者那样思考。每写一个运算符重载,都要问一遍调用者是谁、返回值是什么、会不会有歧义、会不会改变原对象。把这些想清楚,Stonewt、Vector、Complex 哪个先写都一样。如果卡住了,就先把这章开头那十几个类声明的例子重新敲一遍,再回来写练习,思路会顺非常多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询