news 2026/10/13 21:37:34

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 练习1:Vector 类的重构完整实现

这道题在原书中的分量很重。它要求你维护一个二维向量类,既能用直角坐标(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 练习2:Time 类的加减乘与输出

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_cast<long>((hours * 60 + minutes) * n + 0.5); return Time(static_cast<int>(totalMinutes / 60), static_cast<int>(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 练习3:Stonewt 类的运算与比较重载

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_cast<int>(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_cast<int>(lbs)截断小数,pds_left再保留剩余部分。这样Stonewt(200.6)会输出14 stones, 4.6 pounds,因为 1 stone = 14 pounds,200.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,要么给转换函数加explicit(C++11 起支持),根据你希望哪个方向保持隐式来决定。

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_cast<int>(totalMeters); centimeters = static_cast<int>((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_cast<unsigned>(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隐式构造函数和转换函数形成双向通道给其中一个加explicit
cannot access private member外部函数访问了私有成员添加友元声明,或改用公有接口
returning reference to local variable返回了函数内创建的局部对象引用改成按值返回ClassType
no 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前面加一行注释,说明“为什么这里用成员函数而不是友元函数”“返回类型为什么是对象/引用/流引用”。这个整理习惯会在后面动态内存、继承和多态章节帮上大忙。运算符重载不是背语法,而是在理解“编译器看到表达式后到底会调用哪个函数”,想清楚这一点,后续写模板代码和自定义迭代器都会顺很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 21:36:59

车机CarPlay插件集成指南:四通道架构、调参与量产避坑

简介&#xff1a;面向iOS车载开发者的CarPlay通信插件源码包&#xff0c;内含Apple CarPlay Communications Plugin的轻微修改版&#xff0c;聚焦iPhone与车载多媒体系统之间的数据互通&#xff0c;覆盖音频流、导航指令、电话、短信等关键链路&#xff0c;既适合初学CarPlay架…

作者头像 李华
网站建设 2026/10/13 21:36:38

从CRUD到领域拆分:后端模块化架构演进之路

做后端开发这几年&#xff0c;我最大的感触是&#xff1a;大部分项目并不是被业务复杂本身压垮的&#xff0c;而是被没有跟着业务一起演进的结构拖垮的。CRUD 写起来很爽&#xff0c;一个控制器、一个服务、一个数据访问接口&#xff0c;数据库表一映射&#xff0c;接口一铺开&…

作者头像 李华
网站建设 2026/10/13 21:35:58

SSM+Java社团管理系统:2026毕设选题与全流程解析

每年到了三、四月份&#xff0c;我都会被一批又一批做毕业设计的同学问到同一个问题&#xff1a;毕设题目选什么&#xff1f;在我这几年看到的题目里&#xff0c;SSM Java 社团管理系统几乎是出现频率最高的一个。为什么大家都选它&#xff1f;因为这类系统技术上不冷门、业务…

作者头像 李华
网站建设 2026/10/13 21:33:26

自制桌面收纳系统aa---(10):需求、选材与实测全解析

这套作品被我在工作文件夹里记成aa---(10)&#xff0c;起因比较随意。当时连续做了九个版本的桌面整理系统&#xff0c;每个版本都只是在原结构上加加减减&#xff0c;命名一直用 AA 做主键再加版本号&#xff0c;做到第十版的时候&#xff0c;改动范围明显超过了小修的界限&am…

作者头像 李华
网站建设 2026/10/13 21:30:49

用官方能力把PS5打造成家庭媒体中心:AnyPS5完整折腾指南

“AnyPS5”这个项目名&#xff0c;说白了就是让手里那台PS5不再只当游戏机用。很多玩家买回来就只玩几个大作&#xff0c;时间一长积灰吃灰&#xff0c;完全没意识到这台机器本身就是一台性能不错的家庭娱乐主机。这个项目就是把它的闲置能力盘活&#xff1a;本地视频随便放、各…

作者头像 李华
网站建设 2026/10/13 21:30:17

操作系统进程线程模型:状态切换、同步互斥与死锁详解

2. 状态模型与控制方法&#xff1a;从“三态图”到上下文切换2.1 三态与五态&#xff1a;多出来的“创建态”和“终止态”用来干嘛先解决一个最基础的问题&#xff1a;进程到底有哪几种状态&#xff1f;很多教材开头就给出三态模型——运行态、就绪态、阻塞态——配合那张经典的…

作者头像 李华