资讯详情

C++第11章练习精讲:运算符重载、友元与类型转换实战

📅 2026/10/14 17:57:27 | 华诺云谱 👁 阅读
C++第11章练习精讲:运算符重载、友元与类型转换实战
第11章的编程练习我前前后后写了三天。最开始的抵触不是因为题难而是这一章的练习几乎全部以“改写教材里现成的类”为核心Stonewt要反复改三遍Vector要改两遍最后还要从零写一个复数类。改类比写类更折磨人因为你必须在别人画好的边界里动刀动一个成员构造函数、运算符重载、友元、类型转换可能会连环报错。这篇文章把我整理清楚的答案和当时的思考过程一起放出来所有代码都用C11标准编译运行过类声明的风格尽量沿用教材示例适合已经看完第11章但卡在练习上的人做参考。题目顺序按中文版第6版来排练习1和2是Stonewt练习3和4是Vector练习5和6回到Stonewt练习7是自定义复数类。1. 第11章练习题到底在考什么1.1 一张表看清七道题的考点分布第11章的标题是“使用类”但真正串起整章练习的核心其实是三条线运算符重载、友元函数、类型转换。这三条线在七道题里反复组合难度层层递进。我先把整章练习的分布列出来方便你对号入座题号载体类核心考点最容易卡住的位置练习1Stonewt6个关系运算符重载、友元实现乘法友元函数与成员函数如何分工练习2Stonewt状态成员、重载输出状态切换时输出格式如何变化练习3Vector运算符重载复用、随机游走、文件输出角度与弧度的换算、每步位置记录练习4Vector多轮模拟、最大最小平均值统计每轮必须重置起点和步数练习5Stonewt加减乘除四则重载、混合操作数左操作数不是类对象时的友元方案练习6Stonewt转换函数、隐式转换的二义性同时定义int和double转换后的歧义练习7Complex完整重载体系、输入输出流重载 与 while 循环的配合这张表花不了多少时间但能帮你判断自己卡在哪一环。很多人一上来就写复数类结果被运算符重载的成员/非成员选择搞懵其实前面六道题已经把每个坑都铺了一遍。1.2 建议的做题顺序我的建议是先做练习1、2把关系运算符、状态成员这两个“类内重载”的基础打牢再做练习3、4用Vector验证运算符重载在真实场景里的组合能力然后回到Stonewt做练习5、6体验混合类型运算和转换函数最后再碰复数类这时候你已经形成了一套肌肉记忆知道哪个运算符该做成成员、哪个必须靠友元。如果你已经做完了其中几道也没关系下面每一节都能单独看。代码里我会标清楚新增了哪些内容、为什么这么写。2. 练习1和2Stonewt关系运算符、状态成员与输出2.1 练习1的关键代码六个关系运算符和一个友元乘法教材里的Stonewt类内部有三个成员int stone 表示石数double pds_left 表示剩余磅数double pounds 表示总磅数。练习1要求重载全部6个关系运算符再增加一个乘以double的操作。先看类声明中需要新增的部分// stonewt.h 中新增接口 class Stonewt { private: enum {Lbs_per_stn 14}; int stone; double pds_left; double pounds; public: Stonewt(double lbs); Stonewt(int stn, double lbs); Stonewt(); ~Stonewt(); void show_lbs() const; void show_stn() const; // 练习1新增 bool operator(const Stonewt s) const; bool operator(const Stonewt s) const; bool operator(const Stonewt s) const; bool operator!(const Stonewt s) const; bool operator(const Stonewt s) const; bool operator(const Stonewt s) const; Stonewt operator*(double n) const; friend Stonewt operator*(double n, const Stonewt s); };实现部分的核心思路是所有关系运算都统一用pounds比较不要再去换算 stone 和 pds_left。这样最不容易出错因为pounds本身在构造函数里已经算好了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 pounds s.pounds; } 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 pounds s.pounds; } Stonewt Stonewt::operator*(double n) const { return Stonewt(pounds * n); } Stonewt operator*(double n, const Stonewt s) { return s * n; }这里有个关键设计点Stonewt * double可以写成成员函数因为左操作数是Stonewt对象但double * Stonewt不能写成成员函数因为左操作数是double不是Stonewt。要让2.5 * sw这种写法成立必须把这个运算符重载成非成员函数而它又需要访问Stonewt的私有成员所以只能声明为友元。友元的实现我直接return s * n;它内部会调用成员版本operator*(double)一举两得。2.2 练习1的测试程序对象数组与动态初始化原题要求声明一个包含6个Stonewt对象的数组初始化前3个并用new初始化后3个。由于类里已经有接受double的构造函数后3个可以用用户输入初始化。这里要注意的是如果用指针数组后面比较时要解引用#include iostream #include stonewt.h int main() { using namespace std; Stonewt * arr[6]; arr[0] new Stonewt(11, 0.0); arr[1] new Stonewt(12, 3.5); arr[2] new Stonewt(10, 8.0); cout Enter pounds for 3 Stonewt objects:\n; for (int i 3; i 6; i) { double lbs; cin lbs; arr[i] new Stonewt(lbs); } Stonewt * maxp arr[0]; Stonewt * minp arr[0]; for (int i 1; i 6; i) { if (*arr[i] *maxp) maxp arr[i]; if (*arr[i] *minp) minp arr[i]; } cout Max: ; maxp-show_stn(); cout Min: ; minp-show_stn(); for (int i 0; i 6; i) delete arr[i]; return 0; }使用new Stonewt(lbs)时Stonewt类必须有合适的构造函数这没问题。真正容易忘的是程序结束前把6个指针全部delete掉虽然考试未必检查内存泄漏但养成习惯很重要。2.3 练习2状态成员控制两种输出格式练习2要求给Stonewt加一个状态成员让对象可以按“英石磅”或“纯磅”两种格式输出并且重载 运算符。先声明一个枚举表示当前模式// 私有成员中新增 enum Mode {STONE, POUNDS}; Mode mode;再增加两个设置状态的方法void set_stone() { mode STONE; } void set_pounds() { mode POUNDS; }注意构造函数里必须给 mode 一个初始值否则输出时mode是未初始化的垃圾值程序表现会非常奇怪Stonewt::Stonewt(double lbs) { stone int(lbs) / Lbs_per_stn; pds_left int(lbs) % Lbs_per_stn (lbs - int(lbs)); pounds lbs; mode STONE; }接下来是重载 。这里必须用友元原因很直接cout stonewt这条语句里左操作数是cout类型是std::ostream不是Stonewt。如果写成成员函数调用方式会变成stonewt cout语义完全反了。所以只能让运算符函数成为Stonewt的友元从而访问私有成员std::ostream operator(std::ostream os, const Stonewt s) { if (s.mode Stonewt::STONE) os s.stone stone, s.pds_left pounds; else os s.pounds pounds; return os; }返回值必须是std::ostream 这样cout a b才能连续拼接。判断s.mode时因为 operator 不是成员函数必须借助友元访问私有成员如果漏写friend编译器会直接报 mode 是私有成员的错。3. 练习3和4Vector类与随机游走的两层玩法3.1 Vector类自带运算符重载的回顾教材里的Vector类内部同时保存直角坐标(x, y)和极坐标(mag, ang)再用一个mode标记当前模式。练习3、4都建立在Vector已经能正确执行运算符重载的基础上所以先把它最重要的几个运算符列出来Vector Vector::operator(const Vector b) const { return Vector(x b.x, y b.y); } Vector Vector::operator-(const Vector b) const { return Vector(x - b.x, y - b.y); } Vector Vector::operator-() const { return Vector(-x, -y); } Vector Vector::operator*(double n) const { return Vector(x * n, y * n); } Vector operator*(double n, const Vector a) { return a * n; }operator和operator-的内部实现直接用直角坐标相加、相减然后让构造函数自动计算mag和ang。这样做的好处是运算逻辑简单缺点是每次运算后的对象都默认变成直角坐标模式。这一点在随机游走中要注意如果你之前把result切成了polar模式执行一次result result step;后result的模式会变回直角坐标。由于坐标值没变对最终结果没有影响但如果你想连续用极坐标查看位置就要在每次输出前重新调用polar_mode()。3.2 练习3随机游走并写入文件练习3的典型要求是修改随机游走程序把每一步的位置写入文件同时记录初始条件和结果摘要。先看主程序框架#include iostream #include fstream #include cstdlib #include ctime #include vector.h int main() { using namespace std; using VECTOR::Vector; srand(time(0)); double target; double dstep; double direction; Vector step; Vector result(0.0, 0.0); unsigned long steps 0; ofstream fout(walk.txt); if (!fout.is_open()) { cerr Cant open file.\n; return 1; } cout Enter target distance (q to quit): ; while (cin target) { cout Enter step length: ; if (!(cin dstep)) break; result.reset(0.0, 0.0); steps 0; fout Target distance: target , step length: dstep endl; fout 0: result endl; while (result.magval() target) { direction rand() % 360; step.reset(dstep, direction, Vector::POL); result result step; steps; fout steps : result endl; } cout After steps steps, subject has location:\n; cout result endl; result.polar_mode(); cout Location in polar: result endl; result.rect_mode(); fout After steps steps, subject has location:\n; fout result endl; fout -------------------------------------------\n; cout Next target distance (q to quit): ; } cout Bye!\n; return 0; }几个细节值得解释。第一rand() % 360产生0到359的整角度教材的Vector构造函数在极坐标模式下会把第二个参数当作角度值并自动转换成弧度再计算x和y所以这里不需要手动乘以π。第二文件输出每行都用fout steps : result endl;完全依赖Vector重载的 。第三result.polar_mode()只改变输出模式不会改变坐标所以拍照式输出完成后切换回rect模式也不影响后续加法。3.3 练习4多次试验统计最大最小平均步数练习4通常是在练习3的基础上加一层循环做多次随机游走统计步数的最大值、最小值和平均值。核心不是运算符重载而是“每轮状态重置”#include iostream #include fstream #include cstdlib #include ctime #include climits #include vector.h int main() { using namespace std; using VECTOR::Vector; srand(time(0)); double target, dstep, direction; Vector step; cout Enter target distance: ; cin target; cout Enter step length: ; cin dstep; const int TRIALS 10; unsigned long max_steps 0; unsigned long min_steps ULONG_MAX; unsigned long total 0; for (int i 0; i TRIALS; i) { Vector result(0.0, 0.0); unsigned long steps 0; while (result.magval() target) { direction rand() % 360; step.reset(dstep, direction, Vector::POL); result result step; steps; } cout Trial i 1 : steps steps endl; total steps; if (steps max_steps) max_steps steps; if (steps min_steps) min_steps steps; } cout Max steps: max_steps endl; cout Min steps: min_steps endl; cout Average steps: double(total) / TRIALS endl; return 0; }这里最容易犯的错就是把result声明在循环外然后只在每次循环开始时 reset。结果是前一轮到达的位置会成为下一轮的起点步数统计全部错乱。最稳妥的写法是每一轮都在循环内部重新构造Vector result(0.0, 0.0);天然隔离状态。另外min_steps初始值不要用0否则最小值永远算不出来用climits里的ULONG_MAX更安全。4. 练习5和6四则运算重载与转换函数的二义性实验4.1 练习5混合操作数下加减乘除怎么设计练习5要求给Stonewt重载加减乘除并且支持Stonewt double、double Stonewt、Stonewt * double、double * Stonewt等混合写法。设计接口时我建议把 double 版本的运算全部显式声明出来而不是依赖 Stonewt 构造函数的隐式转换。显式声明的好处是行为清晰不会让编译器在多个候选函数之间犹豫。接口如下Stonewt operator(double n) const; Stonewt operator-(double n) const; Stonewt operator*(double n) const; Stonewt operator/(double n) const; friend Stonewt operator(double n, const Stonewt s); friend Stonewt operator-(double n, const Stonewt s); friend Stonewt operator*(double n, const Stonewt s); friend Stonewt operator/(double n, const Stonewt s);实现时成员函数版本统一以pounds作为计算基准Stonewt Stonewt::operator(double n) const { return Stonewt(pounds n); } Stonewt Stonewt::operator-(double n) const { return Stonewt(pounds - n); } Stonewt Stonewt::operator*(double n) const { return Stonewt(pounds * n); } Stonewt Stonewt::operator/(double n) const { if (n 0.0) { std::cerr Error: division by zero.\n; return *this; } return Stonewt(pounds / n); }友元版本里最容易写错的不是乘法而是减法和除法。double - Stonewt的语义是用double减去Stonewt的磅数不能直接复用s - n因为方向反了Stonewt operator(double n, const Stonewt s) { return s n; } Stonewt operator-(double n, const Stonewt s) { return Stonewt(n - s.pounds); } Stonewt operator*(double n, const Stonewt s) { return s * n; } Stonewt operator/(double n, const Stonewt s) { if (s.pounds 0.0) { std::cerr Error: division by zero.\n; return Stonewt(n); } return Stonewt(n / s.pounds); }关于除零教材里通常不会纠结但实际工程里必须处理。我的做法是打印一条错误信息并返回原对象或Stonewt(0)至少让程序不至于直接崩溃。你完全可以根据场景换成抛出异常。4.2 练习6转换函数怎么写以及二义性怎么观察练习6给Stonewt添加两个转换函数一个转int一个转doubleStonewt::operator int() const { return int(pounds 0.5); } Stonewt::operator double() const { return pounds; }转换函数有三个非常重要的语法特征必须是非static成员函数、不能写返回类型、参数列表必须为空。所以下面这个写法是错的int operator int() const; // 错误转换函数不能指定返回类型我在做这套题时第一次就栽在了这里编译器提示也很抽象。转换函数的真正坑在于二义性。当Stonewt同时有int和double两个转换函数时某些表达式会让编译器不知道该选哪个。比如Stonewt sw(10.6); cout sw 1 endl;这里 sw 可以转成 int也可以转成 double表达式sw 1可能被解释成int int也可能被解释成double double编译器自己都拿不定主意甚至可能直接报二义性错误。所以练习6的测试代码里建议你显式写出类型转换Stonewt sw(9, 2.8); double p_wt double(sw); int w int(sw); std::cout double: p_wt std::endl; std::cout int: w std::endl;如果你希望禁止隐式转换C11已经把转换函数也纳入了explicit的管辖范围explicit operator int() const; explicit operator double() const;加了explicit之后double d sw;这种隐式赋值就不成立了必须写double d double(sw);或static_castdouble(sw)。教材第6章部分配套编译器在老标准下可能不认explicit转换函数我建议用支持C11的g编译加上-stdc11即可。5. 练习7复数类的完整重载与输入输出处理5.1 类声明与文件组织练习7是从零定义一个表示复数的类并重载加法、减法、乘法、输入和输出。这道题是把整章知识压缩成一次完整实践类声明如下// complex.h #ifndef COMPLEX_H_ #define COMPLEX_H_ #include iostream class Complex { private: double real; double imag; public: Complex(double r 0.0, double i 0.0) : real(r), imag(i) {} Complex operator(const Complex c) const; Complex operator-(const Complex c) const; Complex operator*(const Complex c) const; Complex operator*(double n) const; friend Complex operator*(double n, const Complex c); Complex operator-() const; friend std::ostream operator(std::ostream os, const Complex c); friend std::istream operator(std::istream is, Complex c); }; #endif构造函数用了默认参数这样Complex a;、Complex b(1.0);、Complex c(1.0, 2.0);都能通过灵活性刚好适合用户交互场景。5.2 复数运算的实现与数学原理复数加减法很简单对应分量直接相加减。复数乘法需要用到公式(a bi) * (c di) (ac - bd) (ad bc)i关键是不能偷懒写成real * c.real、imag * c.imag这样逐项相乘那得到的是错误的数值。正确实现如下Complex Complex::operator(const Complex c) const { return Complex(real c.real, imag c.imag); } Complex Complex::operator-(const Complex c) const { return Complex(real - c.real, imag - c.imag); } Complex Complex::operator-() const { return Complex(-real, -imag); } Complex Complex::operator*(const Complex c) const { return Complex(real * c.real - imag * c.imag, real * c.imag imag * c.real); } Complex Complex::operator*(double n) const { return Complex(real * n, imag * n); } Complex operator*(double n, const Complex c) { return c * n; }这里operator*一共出现了三个版本复数乘复数、复数乘double、double乘复数。第一和第二个版本都是成员函数第三个必须是友元。其中2 * c的调用会走友元版本内部的c * n再调用成员版本逻辑很干净。5.3 输入输出重载与 while 循环的配合复数类的 和 都是典型的流重载。 的重载要把复数格式化成(real, imagi)之类可读的形式std::ostream operator(std::ostream os, const Complex c) { os ( c.real , c.imag i); return os; }的重载要注意交互提示、读取失败判断和返回值。我的实现是在重载函数里直接打印提示这样主程序里只需要写cin c就能完成“提示读取”两个动作std::istream operator(std::istream is, Complex c) { std::cout real: ; if (!(is c.real)) return is; std::cout imaginary: ; is c.imag; return is; }如果用户输入了字母而不是数字is c.real会失败istream进入错误状态这个operator立即返回is主程序里的while (cin c)判断到失败状态就退出循环。这里特别要注意如果在operator里写入失败后不做返回继续尝试读取 imagistream会一直失败程序可能陷入死循环。主测试程序可以这样写#include iostream #include complex.h int main() { using namespace std; Complex a(3.0, 4.0); Complex c; cout Enter a complex number (q to quit):\n; while (cin c) { cout c is c endl; cout a is a endl; cout a c is a c endl; cout a - c is a - c endl; cout a * c is a * c endl; cout 2 * c is 2 * c endl; cout Enter another complex number (q to quit):\n; } cout Done.\n; return 0; }输入时第一次提示real第二次提示imaginary。如果想偷懒一次性输入3 4也是可以的因为流重载会依次读取两个double。需要测试复数乘法对不对多算几个手算结果对照即可比如(3 4i) * (2 1i) 2 11i。6. 做完这套练习我整理的重载自查清单6.1 编译期最常见的三个错误第一个错误是友元函数定义处误加类名限定符。友元函数不是成员函数定义时不能写成std::ostream Stonewt::operator(...)直接写std::ostream operator(...)就对了。第二个错误是转换函数写了返回类型。int operator int() const是错的编译器不会把它当作转换函数而是当作一个名为int的普通函数签名报错会很奇怪。第三个错误是给成员运算符函数加错了const。比较运算、算术运算这类不修改对象的运算符声明时末尾都要加const否则在const对象上调用会编译失败。6.2 运行期容易忽略的四件事除法运算符要考虑除零。随机游走程序里srand(time(0))只在main开头调用一次放进循环里会导致同一秒内多次运行得到完全相同的随机序列。Stonewt构造时如果不初始化mode输出会读到垃圾值。指针数组用new创建对象后删除时用delete arr[i]而不是delete[] arr[i]后者是为数组分配准备的用错属于未定义行为。6.3 验证答案是否正确的小技巧关系运算符重载完之后先写几个手算断言Stonewt(5.0) Stonewt(6.0)必然为trueStonewt(2.0) * 2 Stonewt(4.0)也应该成立。乘法验证时我会临时让operator参与判断一句代码就能确认。转换函数验证时把int(Stonewt(10.9))和double(Stonewt(10.9))打出来看看四舍五入是否符合预期。复数类则多准备几组手算样例尤其是乘法中实部实乘减虚部虚乘的符号最容易错。这套题的价值不在把代码跑通而在每一次编译报错都能加深你对运算符重载本质的理解。你如果能不看任何资料把七个运算符里每一个为什么选成员、为什么选友元、为什么返回引用都讲清楚这一章就可以踏踏实实翻篇了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑