C++ Primer Plus第11章编程练习答案:运算符重载、友元与类型转换全解

发布时间:2026/10/13 21:37:53
C++ Primer Plus第11章编程练习答案:运算符重载、友元与类型转换全解 如果你正在啃《C Primer Plus》第11章编程练习绝对是你遇到的第一道真正需要“憋一口气”才能写完的关卡。这一章的主线非常集中运算符重载、友元函数、类的自动类型转换。练习数量不算多但几乎每一道题都在逼你把“类”当成一种自带行为的类型来设计而不只是结构体加点成员函数。这篇稿子我把第11章编程练习的完整答案整理成了可以直接编译运行的代码针对每一个重载函数的选型、参数设计、返回类型都做了说明不管你是刚开始学运算符重载还是已经写完一遍想对照检查都可以直接按题号对号入座重点看注释和“踩坑提示”。市面上能搜到的答案很多但大多数要么只贴代码不讲思路要么把题目改得面目全非。这篇我尽量还原原书的练习目标代码以可读性和规范性优先不搞“炫技”写法。所有代码都是单文件即可编译的形态类定义、成员实现、测试主函数放在一起方便你复制到本地环境里逐个验证。1. 第11章的调试基石三个核心概念1.1 运算符重载让对象“长得像”内置类型第11章核心就一句话自定义类也可以使用 - * / 这些运算符。编译器本身不知道Vector Vector是什么意思但你可以通过operator函数告诉它怎么做。本质上运算符重载就是普通函数调用a b在编译器眼里等价于a.operator(b)或operator(a, b)具体是哪一个取决于我们把它声明成成员函数还是友元函数。这里有一个非常容易混淆的点成员函数版本会让“左侧操作数”成为调用对象所以参数列表里只需要写右操作数友元版本则两个操作数都写在参数列表里。写练习时首先要判断的就是“当前这个运算符到底该用哪种形式”。判断依据很简单如果左操作数确定是这个类的对象优先成员函数如果左操作数可能是其他类型比如double * Vector、cout Vector就必须用友元函数。1.2 友元函数给普通函数开一条“私有通道”友元函数本质上是普通函数不是成员函数但它被授权可以访问类的私有成员。没有友元时外部函数只能通过公有接口读取数据一旦运算符涉及ostream、istream、或者非本类对象在左侧就必须借助友元突破封装。我见过不少初学者死记“必须用友元”但你如果能理解背后的原因就不容易忘cout v实际上调用的是operator(cout, v)左操作数是ostream对象不是Vector对象。如果把它定义成成员函数就变成v cout语义完全反了编译器也会直接报错说“成员函数重载operator必须只有一个参数”。所以当你看到must take exactly one argument时第一反应就应该是是不是把写成了成员函数。友元还有一点要注意它声明在类内部但不是成员所以不受公有/私有访问权限影响也不能用const修饰因为根本没有this指针。在类里声明friend时不写static、不写inline也可以但在类外实现时不要加friend关键字也不要加类名限定符。1.3 自动转换与转换函数类型之间的双向通道第11章的第三个大知识点是类型转换。构造函数可以充当“从其他类型转换到本类类型”的通道比如Stonewt(155.0)把一个double变成Stonewt对象而转换函数负责反方向比如把Stonewt变成double。两个方向都能控制但很容易踩到二义性。C 的规则是单参数构造函数默认允许隐式转换Stonewt x 155.0;会触发隐式调用Stonewt(155.0)。如果不想让这种隐式转换发生就加explicit关键字。反过来转换函数写operator double() const返回值类型不用写在函数名前面只需要在函数体里return pounds;。这一块在练习里往往是最后一问或加分项目的是让你理解“什么时候自动转换是便利什么时候是麻烦”。2. 答案全解上Vector 与 Time 的运算符重载2.1 练习1Vector 类的重构完整实现这道题在原书中的分量很重。它要求你维护一个二维向量类既能用直角坐标x、y表示也能用极坐标长度、角度表示同时要把 - *和输出流全部重载。直接看代码。#include iostream #include cmath namespace VECTOR { class Vector { public: enum Mode { RECT, POL }; Vector(); Vector(double n1, double n2, Mode form RECT); void reset(double n1, double n2, Mode form RECT); ~Vector(); double x() const { return x_; } double y() const { return y_; } double magval() const { return mag_; } double ang() const { return ang_; } void polar_mode() { mode_ POL; } void rect_mode() { mode_ RECT; } Vector operator(const Vector b) const; Vector operator-(const Vector b) const; Vector operator-() const; Vector operator*(double n) const; friend Vector operator*(double n, const Vector a) { return a * n; } friend std::ostream operator(std::ostream os, const Vector v); private: double x_; double y_; double mag_; double ang_; Mode mode_; void set_mag(); void set_ang(); }; Vector::Vector() : x_(0), y_(0), mag_(0), ang_(0), mode_(RECT) {} Vector::Vector(double n1, double n2, Mode form) { reset(n1, n2, form); } void Vector::reset(double n1, double n2, Mode form) { mode_ form; if (form RECT) { x_ n1; y_ n2; set_mag(); set_ang(); } else { mag_ n1; ang_ n2; x_ mag_ * cos(ang_); y_ mag_ * sin(ang_); } } Vector::~Vector() {} void Vector::set_mag() { mag_ sqrt(x_ * x_ y_ * y_); } void Vector::set_ang() { ang_ atan2(y_, x_); } Vector Vector::operator(const Vector b) const { return Vector(x_ b.x_, y_ b.y_, RECT); } Vector Vector::operator-(const Vector b) const { return Vector(x_ - b.x_, y_ - b.y_, RECT); } Vector Vector::operator-() const { return Vector(-x_, -y_, RECT); } Vector Vector::operator*(double n) const { return Vector(x_ * n, y_ * n, RECT); } std::ostream operator(std::ostream os, const Vector v) { if (v.mode_ Vector::RECT) { os (x, y) ( v.x_ , v.y_ ); } else { os (m, a) ( v.mag_ , v.ang_ * 180.0 / acos(-1.0) degrees); } return os; } }这段代码里有几个关键决定值得解释一下。operator内部选择用直角坐标相加再构造 RECT 模式的临时对象返回。为什么不是极坐标相加因为极坐标相加不是“角度加角度、长度加长度”那么简单很容易产生数学错误。稳妥的做法是先转回直角坐标计算完成后再交回构造函数重新计算极坐标。这也是实际工程里的通用思路——内部运算永远使用最直观、最容易验证的形式展示层再用用户需要的模式。operator*返回Vector而不是double是因为我们重载的是“向量乘标量”不是“向量点乘”。如果你顺手把*写成内积语义就完全不同了。这道题练习的就是“你定义什么是什么”运算符的意义完全由你控制所以写的时候一定要在注释里说明。friend Vector operator*(double n, const Vector a)我们实现了类内定义形式这样2.0 * vec就能写而vec * 2.0走成员函数operator*(double)。两种形式都支持后vec * 2.0和2.0 * vec就都能编译。这里有个隐藏细节类内定义的友元函数虽然是内联但它仍然需要看到类已经完整定义所以放在类声明内部没问题。测试主函数可以写得很短int main() { using VECTOR::Vector; Vector a(3.0, 4.0, Vector::RECT); Vector b(1.0, 2.0, Vector::RECT); Vector c a b; Vector d 2.5 * a; std::cout a a std::endl; std::cout c c std::endl; std::cout d d std::endl; return 0; }跑出来a b是(4, 6)2.5 * a是(7.5, 10)。如果你把operator里的RECT参数漏掉编译器会因为调不到匹配构造函数而报错如果你把operator的返回值写成void那么cout a endl一整条链就会直接失败。返回ostream不是为了好看是为了让可以连续拼接。2.2 练习2Time 类的加减乘与输出Time 类练习的核心是“时分换算”和“运算符重载的返回值”。题目通常要求处理小时和分钟并支持加法、减法、乘以浮点数最后用输出。#include iostream class Time { private: int hours; int minutes; public: Time() : hours(0), minutes(0) {} Time(int h, int m 0) : hours(h), minutes(m) {} Time operator(const Time t) const; Time operator-(const Time t) const; Time operator*(double n) const; friend Time operator*(double m, const Time t) { return t * m; } friend std::ostream operator(std::ostream os, const Time t); }; Time Time::operator(const Time t) const { Time sum; sum.minutes minutes t.minutes; sum.hours hours t.hours sum.minutes / 60; sum.minutes % 60; return sum; } Time Time::operator-(const Time t) const { int total1 hours * 60 minutes; int total2 t.hours * 60 t.minutes; int diff total1 - total2; if (diff 0) { diff 0; } return Time(diff / 60, diff % 60); } Time Time::operator*(double n) const { long totalMinutes static_castlong((hours * 60 minutes) * n 0.5); return Time(static_castint(totalMinutes / 60), static_castint(totalMinutes % 60)); } std::ostream operator(std::ostream os, const Time t) { os t.hours hours, t.minutes minutes; return os; }operator的写法是模拟时钟进位先把分钟相加如果超过 60 就整除进位分钟取余数。这里有一个常见错误有人说小时部分直接% 60那是错的小时没有“满 24 进位”的要求。除非题目额外要求千万不要自己加上“按 24 小时循环”的逻辑。operator*里(hours * 60 minutes) * n是int * double结果类型是double直接赋给long会截断小数。加上0.5是为了四舍五入避免1.5 小时被算成1 小时 29 分钟。这类“转换精度”问题是编程练习中很容易忽略的隐藏分改卷时结果差一分钟也是错。operator-我选择把负数结果截成 0。原书在这个练习上一般不会要求处理“负时间”的语义你如果想让代码更严谨可以在 diff 小于 0 时抛异常或者输出提示但为了保持程序简单直接置零也是一种合理选择。写注释说明这个决定即可。3. 答案全解下Stonewt 与类型转换3.1 练习3Stonewt 类的运算与比较重载Stonewt 是重量单位类常用英制单位1 stone 14 pounds。类内部可以同时保存“石数 剩余磅数”和“总磅数”两种表示这种“双表示”的设计是第11章的重点因为重载比较和算术运算时统一到总磅数会让逻辑简单很多。#include iostream #include cmath class Stonewt { private: enum { LbsPerStn 14 }; int stone; double pds_left; double pounds; public: Stonewt(double lbs); Stonewt(int stn, double lbs); Stonewt(); ~Stonewt(); void set_pounds(double lbs); double get_pounds() const { return pounds; } Stonewt operator(const Stonewt s) const; Stonewt operator-(const Stonewt s) const; Stonewt operator*(double n) const; friend Stonewt operator*(double n, const Stonewt s) { return s * n; } bool operator(const Stonewt s) const; bool operator(const Stonewt s) const; bool operator(const Stonewt s) const; bool operator!(const Stonewt s) const; operator double() const { return pounds; } friend std::ostream operator(std::ostream os, const Stonewt s); }; Stonewt::Stonewt(double lbs) { set_pounds(lbs); } Stonewt::Stonewt(int stn, double lbs) { pounds stn * LbsPerStn lbs; set_pounds(pounds); } Stonewt::Stonewt() : stone(0), pds_left(0), pounds(0) {} Stonewt::~Stonewt() {} void Stonewt::set_pounds(double lbs) { pounds lbs; stone static_castint(lbs) / LbsPerStn; pds_left lbs - stone * LbsPerStn; } Stonewt Stonewt::operator(const Stonewt s) const { return Stonewt(pounds s.pounds); } Stonewt Stonewt::operator-(const Stonewt s) const { double result pounds - s.pounds; if (result 0) { result 0; } return Stonewt(result); } Stonewt Stonewt::operator*(double n) const { return Stonewt(pounds * n); } bool Stonewt::operator(const Stonewt s) const { return pounds s.pounds; } bool Stonewt::operator(const Stonewt s) const { return pounds s.pounds; } bool Stonewt::operator(const Stonewt s) const { return std::fabs(pounds - s.pounds) 1e-6; } bool Stonewt::operator!(const Stonewt s) const { return !(*this s); } std::ostream operator(std::ostream os, const Stonewt s) { os s.stone stones, s.pds_left pounds; return os; }set_pounds是这道题最值得关注的函数。它把总磅数拆成“石数 剩余磅数”两部分static_castint(lbs)截断小数pds_left再保留剩余部分。这样Stonewt(200.6)会输出14 stones, 4.6 pounds因为 1 stone 14 pounds200.6 / 14 14 余 4.6。注意set_pounds没有处理负数因为负数重量本身没有实际意义你可以加一句防御性判断也可以让它自然出错。比较运算符我提供了四种、、、!。浮点数比较不要直接因为 14 个 stone 加上 4.6 磅可能和 200.6 磅存在浮点误差。我用了1e-6的容差这个尺度在重量场景下足够小不会影响业务判断。类末还定义了operator double()转换函数。有了它Stonewt wt(155.5); double w wt;会直接把pounds成员取出来。如果你再定义一个Stonewt(double)构造函数就会出现“double和Stonewt之间双向转换”的局面编译器在某个运算符重载的候选匹配里很容易报 ambiguous。遇到这种情况要么给构造函数加explicit要么给转换函数加explicitC11 起支持根据你希望哪个方向保持隐式来决定。3.2 练习4类型转换的正确姿势这一节专门补一类题设计一个类同时具备“从内置类型构造”和“转换成内置类型”的能力。最常见的是距离、长度、重量、温度这些单位类。我拿 Distance 距离类演示。#include iostream class Distance { private: int meters; int centimeters; public: Distance(int m 0, int cm 0) : meters(m), centimeters(cm) {} explicit Distance(double totalMeters) { meters static_castint(totalMeters); centimeters static_castint((totalMeters - meters) * 100 0.5); } operator double() const { return meters centimeters / 100.0; } friend std::ostream operator(std::ostream os, const Distance d) { os d.meters m d.centimeters cm; return os; } };这个类里Distance(double)构造函数把1.85这种总米数拆成1 m 85 cm。如果它是隐式的那么Distance d 1.85;可以编译很多人觉得这很方便。但如果你同时还需要operator double()那么写double x d 1.85;时编译器就不知道该把1.85转成Distance再把d转成double还是直接把d转成double再相加两条路都合理于是报ambiguous conversion。加explicit后Distance d 1.85;会被禁止必须写Distance d(1.85);或Distance d Distance(1.85);。这就把隐式转换的歧义控制住了。我在实际写代码时会把所有单参数构造函数默认声明为explicit除非我明确知道某个类型需要经常隐式参与算术。这是从练习里总结出来最值得养成的好习惯。4. 综合应用随机游走与批量数据统计4.1 用 Vector 类做一次随机游走实验前面练习1里的Vector重构之后原书通常会给一个“随机游走”的应用题一个人从原点出发每一步走固定距离但方向随机问需要多少步能走出指定半径的圆多次实验统计最大、最小、平均步数。这道题不只是在练随机数而是在练“把自定义类当作普通类型使用”。#include iostream #include cmath #include ctime #include cstdlib #include vector.h int main() { using std::cin; using std::cout; using std::endl; double target, step; int trials; cout 请输入目标距离、单步步长、实验次数; cin target step trials; std::srand(static_castunsigned(std::time(nullptr))); double totalSteps 0; double maxSteps 0; double minSteps 1e9; for (int i 0; i trials; i) { VECTOR::Vector current(0.0, 0.0, VECTOR::Vector::RECT); int count 0; while (current.magval() target) { double angle rand() % 360 * M_PI / 180.0; VECTOR::Vector stepVec(step, angle, VECTOR::Vector::POL); current current stepVec; count; } totalSteps count; if (count maxSteps) maxSteps count; if (count minSteps) minSteps count; } cout 平均步数: totalSteps / trials endl; cout 最大步数: maxSteps endl; cout 最小步数: minSteps endl; return 0; }这里有几个容易翻车的细节。第一rand() % 360返回的是int角度转弧度需要乘以M_PI / 180.0但M_PI在某些编译器环境里并不是标准库自动定义的宏尤其是 Windows 上或某些严格标准的 Linux 环境。稳妥做法是自己定义const double PI acos(-1.0);免得因为一个宏折腾半天的编译错误。第二随机游走的循环条件判断current.magval()这个函数在Vector里返回的是极坐标下的模长。因为current current stepVec之后current的模式被operator强制设成了RECT但这不影响magval()它始终由私有成员mag_返回。只要set_mag()在直角坐标模式更新过结果就是对的。第三实验次数如果输入太大循环里连续调用构造函数会产生大量临时对象。这道题练习里不需要考虑性能优化但如果你想多跑几十万次实验建议把stepVec的构造提前这也是一个不错的优化思路。4.2 用 Stonewt 完成重量排序统计综合应用的另一类是“把比较运算符用到真实场景”。我拿 6 个重量对象组成数组写一段程序找出最重和最轻。这正是练习里常见的小场景也非常适合验证你重载的比较运算符是否稳定。#include stonewt.h #include iostream int main() { Stonewt weights[6] { Stonewt(190.5), Stonewt(120.0), Stonewt(13, 5), Stonewt(150.4), Stonewt(178.2), Stonewt(200.6) }; Stonewt maxWeight weights[0]; Stonewt minWeight weights[0]; for (int i 1; i 6; i) { if (maxWeight weights[i]) { maxWeight weights[i]; } if (weights[i] minWeight) { minWeight weights[i]; } } std::cout 最重: maxWeight std::endl; std::cout 最轻: minWeight std::endl; return 0; }这段代码能跑通依赖三件事第一Stonewt默认构造不存在也没关系因为数组初始化列表给每个元素都调用了带参构造。第二maxWeight weights[i]和weights[i] minWeight走的是不同的比较方向所以两个都要定义好。第三operator输出格式里用到了 stone 和 pds_left这要求set_pounds在构造时正确完成拆分否则输出会是0 stones, 0 pounds。如果你想把比较函数写得少一点可以实现operator再用友元形式实现operator为b a。但那样maxWeight weights[i]和weights[i] minWeight就不对称了容易让人看不明白。在这道题的场景里把四个比较运算符都写全代码冗余一点但逻辑是直白的。5. 高频编译错误与调试心得5.1 六个常见报错的速查表表格是我从自己跑这些练习时整理的报错记录每一个都对应一个真实的编码失误。报错信息原因解决办法must take exactly one argument把operator写成成员函数改成friend std::ostream operator(std::ostream, const 类名)binary operator not defined操作数类型没有匹配的重载函数检查是否漏写const参数类型是否用引用ambiguous conversion隐式构造函数和转换函数形成双向通道给其中一个加explicitcannot access private member外部函数访问了私有成员添加友元声明或改用公有接口returning reference to local variable返回了函数内创建的局部对象引用改成按值返回ClassTypeno matching function for call to构造函数参数表不匹配检查模式参数如RECT/POL和默认参数表格里的“加上const”很关键。如果你把operator声明为Vector operator(const Vector b)而漏掉末尾的const那么const Vector a, b; Vector c a b;就会报错因为非 const 成员函数不能给 const 对象调用。这是新手最容易忽略的一类错误。5.2 我踩过坑之后的调试流程我个人的调试习惯是写完一个类不马上做完整项目先写一个 10 行以内的最小测试用例。比如写Time类就先测试构造、加一次、减一次、输出一次。确认一个运算符没问题再做下一个。这么做的原因是运算符重载的报错经常是多处叠加的一旦主函数里同时用了加法、乘法、友元函数编译器会一下子抛出一长串错误很难看出根因是哪一个。遇到编译错误优先读第一条不是读最后一条。工具链的报错机制往往会因为第一个错误产生连锁反应后续错误很多都是子虚乌有。把第一个错误修掉重新编译往往能一次解决一大片。还有一个心得代码里不要急着写“炫技”的运算符链。把表达式拆开写几行逐步观察中间结果能极大减小调试成本。等你完整跑通所有运算符后再把表达式压缩成a b * c这种形式也不迟。这章练习全部跑通之后建议回头做一件事给每个operator前面加一行注释说明“为什么这里用成员函数而不是友元函数”“返回类型为什么是对象/引用/流引用”。这个整理习惯会在后面动态内存、继承和多态章节帮上大忙。运算符重载不是背语法而是在理解“编译器看到表达式后到底会调用哪个函数”想清楚这一点后续写模板代码和自定义迭代器都会顺很多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询