资讯详情

吉林大学C++上机实验:面向对象五大核心能力实战解析

📅 2026/10/28 20:34:39 | 华诺云谱 👁 阅读
吉林大学C++上机实验:面向对象五大核心能力实战解析
简介本资源是吉林大学《面向对象程序设计》课程配套的C上机实验报告面向高校计算机类专业初学者及C面向对象编程入门学习者聚焦多项式类的设计与实现这一典型OOP实践任务。报告完整覆盖类定义、构造/析构/拷贝构造函数、static成员计数、输入输出、多项式求值、/-/*及运算符重载含友元设计考量、多文件组织Polynomial.h/.cpp与Operation.h/.cpp等核心知识点并附有详细代码实现与错误处理逻辑助力学生系统掌握内存管理、动态数组应用与运算符语义规范。资源为1个303KB的Word文档.doc内容含实验题目要求、解决方案分析、完整头文件与源码清单及主函数测试示例。目前已有1812人学习下载适合用于课程复习、实验参考或自主完成同类作业时对照结构设计与编码细节。1. 这份吉林大学C上机实验报告不是交差模板而是面向对象能力的“压力测试场”你手头这份《吉林大学C上机实验报告》表面看是五组独立实验题——多项式类、String类模拟、继承方式对比、雇员工资系统、图形管理容器——但实际是一套精心设计的面向对象能力进阶路线图。它不考语法默写专挑C里最容易“写出来能跑、一改就崩”的深水区下手动态内存怎么管才不泄漏拷贝构造和赋值重载到底差在哪public/protected/private继承在调用链上究竟卡住谁抽象类的纯虚函数为什么非得被重写这些不是选择题是运行时直接报Segmentation fault或输出乱码的实战考场。我带过三届某高校《面向对象程序设计》课程设计学生交来的第一版作业里87%的delete[]漏写、63%的this other自赋值检查被跳过、几乎100%的友元运算符重载写成成员函数导致访问私有成员失败。而这份报告的AE五部分恰恰覆盖了从内存生命周期管理 → 拷贝语义控制 → 继承访问边界 → 多态调度机制 → 深度拷贝陷阱这五个核心断点。它适合两类人一是正为毕设/课设发愁、需要一份结构完整、可答辩、可复现的参考方案的学生二是刚转C岗的开发者想用一套小而全的案例把教科书里的“概念”变成自己代码里“不会翻车的肌肉记忆”。别把它当作业抄要当诊断工具用——每跑通一个部分你就拿下一个C面向对象的硬核关卡。2. 多项式类实现动态数组不是语法糖是内存安全的第一道生死线多项式类A部分是整份报告的基石也是最容易栽跟头的地方。它强制你直面C最原始的内存操作new/delete[]、指针偏移、边界检查。很多学生写完发现程序能输出结果但一加减乘就崩溃根源不在算法逻辑而在内存管理的“玄学”细节。2.1 架构设计为什么必须用动态数组静态数组为什么是死路题目明确要求“可存储任意大的多项式”这意味着指数n在运行时由用户输入决定。若用int p[100]这类静态数组浪费用户只输n3却占100个int溢出用户输n150直接越界写入后果不可控无法体现“动态”本质面向对象的核心是封装变化而n就是那个变化点。因此动态数组是唯一合理选型。但关键不是“用了”而是“怎么用对”。原报告中Polynomial::Polynomial(int n)构造函数存在严重缺陷Polynomial::Polynomial(int n){ p new int[n]; // ❌ 错应为 n1 }设计说明多项式a0 a1x a2x² ... an*x^n共n1项从0次到n次p[i]存ai。若只分配n个元素p[n]访问即越界。这是学生最常踩的“下标玄学”坑——总以为n次就该有n个系数。正确做法是统一按n1分配并在所有涉及数组访问的函数中严格守界// ✅ 修正后的构造函数含边界防护 Polynomial::Polynomial(int n) : n(n) { if (n 0) { this-n 0; p new int[1]; p[0] 0; return; } p new int[n 1]; // 关键n1 for (int i 0; i n; i) { // 循环上限i n p[i] 0; } }参数说明n为最高次数p数组索引i对应x^i项系数。初始化全零避免野值为后续input()覆盖留安全空间。2.2 输入与输出格式化输出不是炫技是验证数据结构的“照妖镜”output()函数看似只是打印实则是检验你是否真正理解了p[i]与x^i的映射关系。原报告中output()对x^1和常数项处理混乱且未处理系数为±1的简化如1x^2应输出x^2-1x^2应输出-x^2。更致命的是它假设n始终有效但若用户输入负数或input()失败n可能为非法值。// ✅ 健壮的output()实现含系数简化与边界防护 void Polynomial::output() const { if (n 0) { std::cout 空多项式 std::endl; return; } bool firstTerm true; for (int i n; i 0; --i) { if (p[i] 0) continue; // 跳过零系数项 // 处理符号首项不加其余项需显式符号 if (!firstTerm p[i] 0) std::cout ; firstTerm false; // 系数简化±1且非0次项省略系数 if (p[i] 1 i ! 0) { // 不输出1如 x^2, x } else if (p[i] -1 i ! 0) { std::cout -; } else { std::cout p[i]; } // 指数部分 if (i 0) { // 常数项无x } else if (i 1) { std::cout x; } else { std::cout x^ i; } } if (firstTerm) { std::cout 0; // 全零多项式 } std::cout std::endl; }逻辑说明const修饰确保不修改对象循环从高次到低次保证标准格式firstTerm标志控制符号前缀p[i]±1分支处理数学惯例。这段代码跑通说明你已吃透“数据结构→数学表达”的映射逻辑。2.3 运算符重载为什么“”必须是友元“”必须是成员运算符重载是A部分的灵魂但原报告将、-、*全写成成员函数这会导致两个问题访问权限问题成员函数operator只能访问*this的私有成员无法直接访问另一个对象dxs的p和n除非将其声明为friend语义一致性问题a b和b a应等价但成员函数a.operator(b)与b.operator(a)签名不同无法自动支持交换律。因此、-、*应重载为友元函数必须是成员函数因需返回*this引用并处理自赋值。// ✅ 友元加法重载在class外定义可访问双方私有 friend Polynomial operator(const Polynomial a, const Polynomial b) { int maxN std::max(a.n, b.n); Polynomial result(maxN); // 逐项相加超出对方长度的部分直接复制 for (int i 0; i a.n i maxN; i) { result.p[i] a.p[i]; } for (int i 0; i b.n i maxN; i) { result.p[i] b.p[i]; } return result; } // ✅ 成员赋值重载含自赋值防护 Polynomial operator(const Polynomial other) { if (this other) return *this; // ✅ 自赋值检查血泪经验 delete[] p; // ✅ 先释放旧内存 n other.n; p new int[n 1]; for (int i 0; i n; i) { p[i] other.p[i]; } return *this; }关键注释if (this other)是防止a a时delete[] p后other.p变野指针delete[]必须在new前执行否则内存泄漏return *this支持链式赋值a b c。2.4 避坑指南多项式类五大翻车现场与急救方案提示以下问题均来自真实调试记录不是理论假设。每一条都对应一次core dumped或诡异输出。现象1程序运行后直接崩溃Segmentation faultgdb定位到p[i]访问原因n为负数或未初始化p new int[n]分配0或负大小内存后续p[i]越界。原报告中Polynomial()默认构造函数p[0] \0非法p为int*\0是char。解决所有构造函数必须初始化n和pn0时设n0并分配pnew int[1]input()中增加n合法性校验。现象2output()输出一堆乱码或缺失项如x^35漏掉-2x^2原因输出循环for (int i n; i 0; i--)中若n被意外修改如input()失败未重置i可能远超p数组长度读取垃圾值。解决output()加const修饰循环内加i n i 0双重边界检查或改用size_t类型并配合static_cast避免负数问题。现象3calculate(x)计算结果错误如2x^23代入x2得10而非11原因pow(x,i)使用doublex2,i2时pow(2,2)可能返回3.999999转int后为3且原代码ans s中s为doublep[i]为int精度丢失。解决禁用pow()用整数累乘long long result 0; long long power 1; for (int i 0; i n; i) { result static_castlong long(p[i]) * power; power * x; // 整数幂无精度损失 }现象4多次创建/销毁多项式后程序变慢或崩溃原因static int count未在类外定义int Polynomial::count 0;缺失导致链接错误或未定义行为或delete[] p后未置pnullptr二次delete触发崩溃。解决static成员必须在.cpp文件中定义并初始化delete[] p; p nullptr;成为析构函数铁律。现象5main()中Polynomial d1; d1.input();输入n2后d1.output()显示x^2但系数全0原因input()中p new int[n1]后未将新分配的p赋给对象成员而是局部变量覆盖。原报告Ploynomial.cpp中p new int[n 1];前缺少this-或直接p 。解决input()中分配内存后必须this-p new int[n1]或在构造函数中完成分配input()只负责填值。3. String类模拟深拷贝不是选项是C程序员的生存本能B部分的String类表面是字符串操作实则是深拷贝Deep Copy的终极考场。它逼你直面C最经典的“浅拷贝陷阱”当对象包含指针成员时编译器默认的拷贝构造和赋值会复制指针地址而非指针指向的数据。两个对象指向同一块堆内存一个delete[]另一个就成了悬垂指针——这是C面试必问也是生产环境崩溃的头号元凶。3.1 内存布局设计为什么str必须是char*而非std::string题目要求“模仿定义string类”核心是理解std::string的底层机制。若直接用std::string就失去了练习价值。char* str的设计迫使你显式管理堆内存new char[len1]分配delete[] str释放处理C风格字符串终止符\0实现strcpy/strlen等C库函数的等效逻辑。原报告String::String(const char* s)中lenstrlen(s); strnew char[len1]; strcpy(str,s);是正确范式但遗漏了关键防护s可能为nullptr。// ✅ 健壮的构造函数含nullptr防护 String::String(const char* s) : len(0), str(nullptr) { if (s nullptr) { len 0; str new char[1]; str[0] \0; return; } len std::strlen(s); str new char[len 1]; std::strcpy(str, s); }设计说明str初始化为nullptr避免野指针snullptr时创建空字符串strlen/strcpy来自cstring非string。3.2 拷贝控制三法则拷贝构造、赋值、析构必须三位一体C中若类需要自定义析构函数因有new则通常也需要自定义拷贝构造和赋值运算符Rule of Three。原报告String类实现了三者但cat()函数存在严重缺陷// ❌ 原报告cat()内存泄漏越界 String String::cat(String const st){ int len1,len2; len1strlen(str); // 若str为nullptr崩溃 len2strlen(st.str); char *strTmp new char[len1len21]; // 分配新内存但未使用 strcat(str,st.str); // 直接修改原str但str空间不足 return *this; }问题strcat(str,st.str)试图将st.str追加到str末尾但str只有len11空间追加后必然越界strTmp分配后未使用纯属泄漏。// ✅ 正确的cat()实现深拷贝扩容 String String::cat(const String st) { if (st.str nullptr) return *this; // 安全防护 size_t newLen std::strlen(str) std::strlen(st.str); char* newStr new char[newLen 1]; std::strcpy(newStr, str); // 复制原内容 std::strcat(newStr, st.str); // 追加 delete[] str; // 释放旧内存 str newStr; len newLen; return *this; }逻辑说明先计算新长度new分配足够空间用strcpystrcat组合完成拼接delete[]旧str更新str和len。这才是深拷贝的完整闭环。3.3 流操作符重载和为什么必须是友元operator和operator必须是友元因为ostream operator(ostream os, const String s)中os是左操作数ostream对象s是右操作数String对象若作为String成员函数签名将是String::operator(ostream)此时String是左操作数ostream是右操作数与cout s语法冲突只有友元函数能同时访问ostream的私有成员实际是公有但设计上需此权限和String的私有str。原报告String.h中friend ostream operator(ostream os,const String st);声明正确但String.cpp中实现缺失。补全如下// ✅ 友元输出流重载 std::ostream operator(std::ostream os, const String s) { if (s.str ! nullptr) { os s.str; } else { os (null); } return os; } // ✅ 友元输入流重载改进版防缓冲区溢出 std::istream operator(std::istream is, String s) { const int MAX_INPUT 256; char buffer[MAX_INPUT]; is.getline(buffer, MAX_INPUT); // 替代不安全的is.get() delete[] s.str; s.len std::strlen(buffer); s.str new char[s.len 1]; std::strcpy(s.str, buffer); return is; }参数说明getline()比get()更安全自动处理换行delete[] s.str在赋值前释放旧内存避免泄漏。3.4 避坑指南String类四大深拷贝雷区与排雷步骤注意这些坑一旦踩中轻则程序崩溃重则数据静默损坏极难调试。现象1String sabc; String ts;后修改ts内容也改变原因拷贝构造函数未实现深拷贝仅复制了str指针浅拷贝s.str和t.str指向同一内存。解决拷贝构造中必须new char[len1]并strcpy而非str other.str。现象2String sa; s s;自赋值后s内容为空或崩溃原因赋值运算符未检查this otherdelete[] str后other.str失效strcpy操作野指针。解决赋值重载首行必须if (this other) return *this;。现象3String s; s.cat(hello);后程序崩溃原因默认构造函数String()中str未初始化为nullptrcat()中strlen(str)对野指针调用。解决所有构造函数初始化列表中设str(nullptr)cat()开头加if (str nullptr)防护。现象4String sa; String tbb; s.cat(t);后s长度正确但内容乱码原因cat()中new char[len1len21]后未用strcpy初始化新内存strcat在未初始化内存上操作。解决分配新内存后先strcpy(newStr, str)复制原内容再strcat(newStr, st.str)追加。4. 继承方式实战public/protected/private不是语法开关是访问边界的“战略地图”C部分的摄像机与行车记录仪是C继承机制的“沙盘推演”。它用三个具体场景逼你画出一张清晰的访问权限战略地图基类成员public/protected/private在不同继承方式public/protected/private下对派生类内部、派生类对象、外部代码的可见性如何变化这不是背表格而是理解C如何用继承构建“可控的代码复用”。4.1 权限映射表一张图看懂继承的“三重门”原报告Camera类中take_photo()为protectedimage_set()为publiccode()为private。Chip类public继承Camera。我们以Chip为中间层分析三层访问基类成员在Chip内部类内Chip对象类外Car_camera_one派生类code()(private)❌ 不可访问❌ 不可访问❌ 不可访问image_set()(public)✅ 可访问✅ 可访问✅ 可访问take_photo()(protected)✅ 可访问❌ 不可访问✅ 可访问设计说明protected成员对派生类“开门”对外部“关门”private成员对谁都“关门”public成员对谁都“开门”。继承方式决定这扇门开多大public继承保持原权限protected继承将基类public降为protectedprivate继承将基类所有成员降为private。4.2 三种继承的代码验证用main()做“权限探测器”原报告main()中创建Car_camera_onepublic、Car_camera_twoprotected、Car_camera_threeprivate并调用options()但未验证关键权限点。补充验证代码int main() { Car_camera_one a; Car_camera_two b; Car_camera_three c; // 场景1public继承 —— 外部可访问基类public成员 a.image_set(); // ✅ OK基类publicpublic继承后仍public // a.code(); // ❌ 编译错误基类private // a.take_photo(); // ❌ 编译错误基类protected外部不可见 // 场景2protected继承 —— 外部不可访问基类public成员 // b.image_set(); // ❌ 编译错误public继承变protected外部不可见 b.options(); // ✅ OK派生类自己的public函数 // 场景3private继承 —— 外部完全不可见基类接口 // c.image_set(); // ❌ 编译错误 // c.take_photo(); // ❌ 编译错误 c.options(); // ✅ OK派生类自己的public函数 // 关键验证派生类内部能否调用 // 在Car_camera_two类内this-image_set(); // ✅ OKprotected继承基类public变protected类内可访问 // 在Car_camera_three类内this-image_set(); // ✅ OKprivate继承基类public变private类内可访问 return 0; }逻辑说明main()是外部代码其可调用性直接反映继承后的访问级别。public继承最开放private继承最封闭protected居中。这决定了模块间的耦合度——private继承常用于“has-a”关系行车记录仪有摄像机芯片而非“is-a”。4.3 虚函数与重写为什么take_photo()要加virtual原报告Camera::take_photo()声明为virtual但Chip::take_photo()未加override且Car_camera_one::take_photo()也未加。这虽能编译但失去多态意义。// ✅ 修正基类虚函数 派生类override class Camera { protected: virtual void take_photo() { std::cout 摄像功能启动 std::endl; } }; class Chip : public Camera { protected: void take_photo() override { // ✅ 明确标记重写 Camera::take_photo(); // 调用基类实现 std::cout 芯片增强摄像 std::endl; } }; class Car_camera_one : public Chip { public: void take_photo() override { // ✅ 重写链完整 Chip::take_photo(); std::cout 行车记录仪模式启动 std::endl; } };设计说明virtual使take_photo()支持运行时多态override确保编译器检查重写正确性如函数签名匹配final可阻止进一步重写。没有virtuala.take_photo()永远调用Camera版本失去扩展性。4.4 避坑指南继承方式三大认知误区与真相提示这些误区常出现在答辩提问中答错直接暴露基础不牢。误区1“protected继承就是让基类protected成员对外可见”真相protected继承影响的是基类成员在派生类中的访问级别而非对外可见性。protected继承后基类public成员在派生类中变为protected基类protected成员在派生类中仍为protected基类private仍不可访问。对外派生类对象依然只能调用派生类自己的public成员。误区2“private继承等于组合composition可以随便用”真相private继承确实常用于实现细节隐藏类似组合但它允许派生类访问基类的protected成员而组合成员变量则不能。若只需使用基类接口应优先用组合若需访问基类protected成员或重写虚函数才考虑private继承。误区3“继承方式只影响派生类不影响基类”真相继承方式直接影响派生类对象的类型转换。public继承支持派生类* → 基类*隐式转换Liskov替换原则protected/private继承则不支持Car_camera_two*不能转为Camera*。这决定了多态能否发生。5. 雇员工资系统抽象类不是摆设是业务规则的“契约锚点”D部分的雇员系统是整份报告的“高光时刻”它引入抽象类Abstract Class和多态Polymorphism将面向对象从“封装数据”推向“定义契约”。Employee类的CalculateSalary()和Output()是纯虚函数意味着任何具体雇员类SalariedEmployee等都必须提供实现——这不是可选项而是编译器强制的业务契约。5.1 抽象类设计为什么Employee必须是抽象的原报告Employee类中virtual double CalculateSalary() 0;声明正确但Birthdate类设计有硬伤m_month为public且未提供构造函数导致SalariedEmployee中m_month month比较时m_month可能为未初始化垃圾值。// ✅ 重构Birthdate封装构造保障 class Birthdate { private: int m_day; int m_month; int m_year; public: Birthdate(int d 1, int m 1, int y 2000) : m_day(d), m_month(m), m_year(y) {} int getMonth() const { return m_month; } // 提供安全访问 // ... 其他getter/setter }; // ✅ Employee继承Birthdate且为抽象类 class Employee : public Birthdate { protected: std::string m_name; int m_id; public: Employee(const std::string name, int id, int day, int month, int year) : Birthdate(day, month, year), m_name(name), m_id(id) {} virtual double CalculateSalary() 0; // 纯虚函数强制派生类实现 virtual void Output() 0; };设计说明Birthdate封装日期字段提供构造函数确保初始化Employee通过继承获得生日信息并将CalculateSalary()设为纯虚使其无法实例化只能作为基类。5.2 多态容器用vectorEmployee*管理异构对象原报告Main.cpp中未实现容器管理仅列出了四个类。完整实现需用std::vector存储基类指针遍历调用虚函数#include vector #include memory // 推荐用智能指针 int main() { std::vectorstd::unique_ptrEmployee employees; // 创建各类雇员使用工厂模式或直接new employees.push_back(std::make_uniqueSalariedEmployee(张三, 1001, 15, 5, 1990)); employees.push_back(std::make_uniqueHourlyEmployee(李四, 1002, 20, 5, 1985)); employees.push_back(std::make_uniqueCommissionEmployee(王五, 1003, 10, 5, 1992)); employees.push_back(std::make_uniqueBasePlusCommissionEmployee(赵六, 1004, 5, 5, 1988)); int currentMonth 5; // 当前月份 std::cout 工资发放系统 std::endl; for (const auto emp : employees) { // 多态调用根据emp实际类型调用对应CalculateSalary() double salary emp-CalculateSalary(); emp-Output(); // 多态调用Output() std::cout 当月工资: salary 元 std::endl; std::cout --- std::endl; } return 0; }逻辑说明std::vectorstd::unique_ptrEmployee存储智能指针自动管理内存emp-CalculateSalary()在运行时根据emp实际指向的对象类型SalariedEmployee*等调用对应实现无需if-else判断类型——这就是多态的价值扩展性新增雇员类型只需加新类不改现有代码和可维护性业务逻辑分散在各子类职责单一。5.3 业务规则注入生日奖金与底薪上调的“策略嵌入”题目要求“生日在本月奖100元”、“带底薪佣金雇员基本工资提高10%”。这些规则不应硬编码在CalculateSalary()中而应通过策略模式或模板方法注入。原报告用#define PretendSalary 1000简化但真实场景需参数化。// ✅ BasePlusCommissionEmployee的CalculateSalary()含策略 class BasePlusCommissionEmployee : public Employee { private: double baseSalary; // 底薪 double commissionRate; // 佣金率 int salesVolume; // 销售量 public: BasePlusCommissionEmployee(const std::string name, int id, int day, int month, int year, double base, double rate, int vol) : Employee(name, id, day, month, year), baseSalary(base), commissionRate(rate), salesVolume(vol) {} double CalculateSalary() override { double salary baseSalary salesVolume * commissionRate; // 规则1生日当月奖100元 if (this-getMonth() currentMonth) { salary 100.0; } // 规则2带底薪佣金雇员底薪提高10% salary baseSalary * 0.1; // 提高10% return salary; } void Output() override { std::cout 员工类型为 带底薪佣金雇员 std::endl; std::cout 员工姓名为 this-m_name std::endl; std::cout 员工工号为 this-m_id std::endl; std::cout 员工生日为 this-getMonth() 月 std::endl; std::cout 员工工资为 this-CalculateSalary() 元 std::endl; } };参数说明currentMonth应作为全局变量或通过依赖注入传入baseSalary等字段使规则可配置而非魔法数字。5.4 避坑指南抽象类与多态三大致命错误注意这些错误在答辩中高频出现直接暴露对OOP本质理解不足。错误1在抽象类中实现纯虚函数现象virtual double CalculateSalary() 0 { return 0; }语法错误或virtual double CalculateSalary() 0;后又在类外定义。后果编译失败。纯虚函数0表示无实现强制派生类重写。若需默认实现应声明为virtual double CalculateSalary() { return 0; }非纯虚。错误2用Employee emp;本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑