简介本资源是吉林大学《面向对象程序设计》课程配套的C上机实验报告面向高校计算机类专业本科生及C初学者聚焦面向对象编程核心能力训练。报告围绕“多项式类”这一典型数据结构展开完整覆盖类定义、构造/析构函数、动态内存管理、运算符重载、-、*、、static成员计数、多文件组织Polynomial.h/.cpp等关键知识点并提供含输入校验、格式化输出、多项式求值等实用功能的可运行代码实现。资源为1个303KB的Word文档.doc内容包含实验题目要求、详细解决方案说明、完整头文件与源码节选、关键实现逻辑注释及主函数测试用例结构清晰、注释充分便于理解与复现。已有1810人学习下载适合用于课程作业参考、OOP概念巩固及C工程化编码规范入门实践。1. 吉林大学C上机实验报告这不是模板套题而是面向对象编程的「肌肉记忆训练场」你手头这份《吉林大学C上机实验报告》表面看是五份独立实验多项式类、String类、继承方式对比、雇员工资系统、图形管理容器但实际是一条精心设计的「C能力进阶链」——从内存裸操作new/delete起步到const正确性、运算符重载语义、继承访问控制边界、抽象类多态落地、再到深度拷贝的资源所有权转移。它不考语法糖专治“能看懂代码但写不出健壮类”的通病。我带过三届某高校C课程设计学生最常翻车的不是逻辑错误而是delete[] p忘加方括号、operator没处理自赋值、protected继承后在派生类外误调基类函数、virtual析构函数缺失导致子类资源泄漏……这些坑全被压缩在这五组实验里。它适合两类人一是刚学完C基础语法、正卡在“怎么把书上概念变成可运行代码”阶段的新手二是想用最小成本系统补全面向对象工程细节的老手——比如你发现自己的项目里总要反复加if (this rhs) return *this;那说明你缺的不是技巧是肌肉记忆。这份报告不是交差作业它是用5个真实场景逼你亲手把C的“安全契约”刻进手指神经。2. 多项式类实现动态数组管理与运算符重载的完整闭环2.1 为什么必须用动态数组静态数组在这里是死路多项式次数n在运行时由用户输入决定编译期无法确定大小。若用int p[100]不仅浪费内存低次多项式占满100项更致命的是当n100时直接越界崩溃。而动态数组int* p new int[n1]将内存分配权交给堆区n多大就申请多大。但代价是——你必须亲手管理生命周期。看原始代码中Polynomial::Polynomial(int n)构造函数Polynomial::Polynomial(int n){ p new int[n]; // 注意这里漏了 1应为 n1因为指数0到n共n1项 }这个bug会导致后续input()中p new int[n 1]覆盖原指针造成内存泄漏。正确做法是统一用n1因为多项式a0 a1x ... an*x^n共有n1个系数。我一般会强制在构造函数参数名上体现语义Polynomial::Polynomial(int degree) : n(degree) { p new int[n 1](); // ()初始化为0避免野值 }提示new int[n 1]()中的()很重要——它将数组所有元素初始化为0否则p[i]可能是随机垃圾值导致output()输出乱码。2.2 运算符重载的语义陷阱加减乘的边界条件必须显式处理原始代码中operator和operator-的实现存在严重逻辑漏洞。它假设两个多项式指数范围完全对齐但实际d1可能是3x^22x1n2d2是5x^44x^3n4。此时d1.p[3]和d1.p[4]根本不存在直接访问会崩溃。正确解法是创建结果多项式时按最大指数分配空间并对缺失项补0polynomial polynomial::operator(const polynomial dxs) { int max_n (n dxs.n) ? n : dxs.n; polynomial sum; sum.n max_n; sum.p new int[max_n 1](); // 全部初始化为0 // 将当前多项式系数复制到sum for (int i 0; i n; i) { sum.p[i] p[i]; } // 将dxs多项式系数加到sum自动跳过超出dxs.n的索引 for (int i 0; i dxs.n; i) { sum.p[i] dxs.p[i]; } return sum; }同理operator*的实现中multiply.p[i j] multiply.p[i j] temp[j]有双重风险一是ij可能越界multiply.n n dxs.n已预留空间但循环未校验二是temp数组未初始化。修正版polynomial polynomial::operator*(const polynomial dxs) { polynomial multiply; multiply.n n dxs.n; multiply.p new int[multiply.n 1](); // 初始化为0 for (int i 0; i n; i) { for (int j 0; j dxs.n; j) { multiply.p[i j] p[i] * dxs.p[j]; // 直接累加无需temp中间数组 } } return multiply; }2.3 输入容错机制cin状态机的三步清洗法原始代码中input()函数对错误输入的处理是半成品。cin.clear()只重置错误标志while(cin.get() ! \n)只清空缓冲区但没处理p数组的内存泄漏。当用户输入字母而非数字时cin n失败p仍为nullptr后续p new int[n 1]会因n为随机值而崩溃。完整清洗流程void polynomial::input() { cout 请输入多项式的最高指数 endl; while (!(cin n)) { // 检测输入失败 cin.clear(); // 清除failbit cin.ignore(numeric_limitsstreamsize::max(), \n); // 清空整行 cout 输入错误请重新输入一个整数 endl; } // 安全分配内存 p new int[n 1](); cout 请从低到高输入系数a0, a1, ..., an endl; for (int i 0; i n; i) { while (!(cin p[i])) { cin.clear(); cin.ignore(numeric_limitsstreamsize::max(), \n); cout 第 i 个系数输入错误请重输 endl; } } }注意cin.ignore(...)比原始代码的while(cin.get() ! \n)更鲁棒它能处理缓冲区残留的多个字符。3. String类模拟深拷贝、赋值安全与资源所有权转移3.1 为什么cat()函数是典型反模式字符串拼接的内存陷阱原始String::cat()实现存在两处致命错误strcat(str,st.str)直接修改原字符串str但str空间仅够存原字符串拼接后必然越界char *strTmp new char[len1len21]分配了新空间却未使用return *this返回的是被破坏的原对象。正确做法是先分配足够空间再复制原字符串最后追加新字符串。但更关键的是——cat()不该修改自身而应返回新对象类似标准库operator。修正版String String::cat(const String st) const { int len1 strlen(str); int len2 strlen(st.str); char* new_str new char[len1 len2 1]; strcpy(new_str, str); strcat(new_str, st.str); String result(new_str); // 调用String(const char*)构造函数 delete[] new_str; // 释放临时空间 return result; }3.2operator的四大雷区自赋值、异常安全、资源泄漏、浅拷贝原始String::operator实现虽有自赋值检查但缺少异常安全防护。若new char[len1]抛出bad_allocdelete[] str已执行str变野指针。工业级写法采用复制-交换惯用法copy-and-swapString String::operator(String other) { // 注意参数传值自动调用拷贝构造 swap(*this, other); // 交换this和other的str指针 return *this; // other析构时自动释放旧内存 } void swap(String first, String second) noexcept { using std::swap; swap(first.str, second.str); swap(first.len, second.len); }此写法天然规避自赋值问题交换自身无影响且other的构造若失败*this完全不受影响。3.3length()为何必须是const成员函数const正确性的工程价值length()只读取len成员不修改对象状态。若不加const则无法在const String s引用上调用void printLength(const String s) { cout s.length(); // 编译错误除非length()声明为const }这不仅是语法要求更是接口契约const成员函数向调用者承诺“我绝不改变对象”让编译器能做更多优化如内联也让其他开发者敢放心传递const引用。原始代码中int length() const{ return len; }写法完全正确这是C面向对象设计的基石习惯。4. 继承方式实战public/protected/private的访问控制边界验证4.1 三种继承方式的本质差异不是语法糖是接口暴露策略原始Camera体系中Chip类继承Camera再由Car_camera_one/two/three分别以public/protected/private继承Chip。关键点在于继承方式决定基类成员在派生类内部的可访问性以及在派生类外部的可见性。继承方式基类public成员在派生类内基类protected成员在派生类内基类public成员在派生类对象外publicpublicprotected可见如a.take_photo()protectedprotectedprotected不可见b.take_photo()编译错误privateprivateprivate不可见c.take_photo()编译错误原始代码中Car_camera_two和Car_camera_three的options()函数能调用Chip::options()是因为options()在Chip中是public而protected/private继承只是限制了外部调用不阻止派生类内部调用基类public成员。4.2protected继承的典型误用试图在外部调用基类函数常见错误写法Car_camera_two b; b.take_photo(); // 编译错误take_photo()在Car_camera_two中是protected但原始main()中并未出现此调用说明作者理解正确。真正易错的是当Car_camera_two的子类想调用take_photo()时会发现它不可见——因为protected继承使基类public成员在派生类中降级为protected而protected成员不能被派生类的子类访问除非该子类也protected继承。4.3private继承的隐藏价值实现复用而非接口继承private继承常被误解为“错误用法”实则是C中组合composition的替代方案。当Car_camera_three需要Chip的功能但不想暴露其接口时private继承比has-a组合更轻量无需额外成员变量。原始代码中c.options()能调用是因为options()在Chip中是publicprivate继承后它在Car_camera_three内部仍是可访问的只是对外不可见。这完美匹配需求“其他公司购买行车记录仪不得泄露其全部内容”。5. 雇员工资系统抽象类多态与容器管理的工程落地5.1 抽象类设计的硬性约束纯虚函数与虚析构函数原始Employee类定义为class Employee { virtual double CalculateSalary() 0; // 纯虚函数 virtual void Output() 0; public: string m_name; int m_id; };但缺少虚析构函数若通过Employee* ptr new SalariedEmployee;创建对象delete ptr时只会调用Employee析构函数SalariedEmployee的资源如有不会释放。必须添加virtual ~Employee() default; // C11起推荐default或{}实现5.2 多重继承的隐患Birthdate的菱形继承风险原始代码中SalariedEmployee : public Employee, public Birthdate采用多重继承。若未来CommissionEmployee也继承Birthdate而BasePlusCommissionEmployee同时继承二者则Birthdate会被重复继承菱形继承。解决方案是让Birthdate成为virtual基类class Birthdate { /* ... */ }; class SalariedEmployee : public Employee, virtual public Birthdate { /* ... */ };这样所有派生类共享同一份Birthdate子对象。5.3 容器管理的多态实践用vector存储基类指针原始Main.cpp未实现容器管理需补充#include vector #include memory int main() { vectorunique_ptrEmployee employees; employees.push_back(make_uniqueSalariedEmployee()); employees.push_back(make_uniqueHourlyEmpolyee()); // ... 添加其他类型 // 多态调用 for (const auto emp : employees) { emp-Output(); cout 工资: emp-CalculateSalary() endl; } }unique_ptr确保资源自动释放vector提供动态扩容emp-Output()自动调用对应派生类版本——这才是面向对象容器管理的标准姿势。6. 图形管理容器深度拷贝的资源所有权移交与避坑指南6.1 深度拷贝的核心每个指针都必须new每个new都必须delete原始Manage类要求实现深度拷贝。若只复制指针浅拷贝两个Manage对象将指向同一块Shape*内存析构时delete同一地址两次程序崩溃。深度拷贝必须为每个a[i]创建新对象class Manage { Shape* a[100]; int count; // 当前存储的图形数量 public: Manage() : count(0) {} // 深度拷贝构造函数 Manage(const Manage other) : count(other.count) { for (int i 0; i other.count; i) { if (other.a[i] ! nullptr) { // 根据动态类型创建新对象需Shape有虚clone()函数 a[i] other.a[i]-clone(); // 假设Shape定义了virtual Shape* clone() 0; } else { a[i] nullptr; } } } // 深度拷贝赋值 Manage operator(const Manage other) { if (this other) return *this; // 先清理当前资源 for (int i 0; i count; i) { delete a[i]; } // 再深拷贝 count other.count; for (int i 0; i other.count; i) { a[i] other.a[i] ? other.a[i]-clone() : nullptr; } return *this; } ~Manage() { for (int i 0; i count; i) { delete a[i]; } } };关键点clone()是典型的**虚构造函数virtual constructor**模式让基类能根据实际类型创建副本。6.2 避坑常见问题与血泪排查记录现象1程序运行时崩溃报错double free or corruption原因Manage的拷贝构造函数未实现深度拷贝导致两个对象的a[i]指向同一内存析构时delete两次。解决严格遵循“每个new配对一个delete”拷贝时对每个非空指针调用clone()。现象2Manage对象复制后Circle的area()计算结果错误原因Circle::clone()返回new Circle(*this)但Circle未定义拷贝构造函数r成员被正确复制但若Circle有动态成员则会浅拷贝。解决为所有派生类显式定义拷贝构造函数或使用 default若无动态资源。现象3Manage析构时部分Shape*未被delete原因count成员未在拷贝构造中正确初始化导致析构循环次数错误。解决在拷贝构造函数首行初始化count other.count并在所有构造函数中初始化count。现象4clone()函数返回nullptr后续调用area()崩溃原因clone()未处理nullptr输入或Shape基类未定义clone()纯虚函数。解决在Shape中声明virtual Shape* clone() const 0;所有派生类必须实现。现象5编译报错clone is not a member of Shape原因clone()函数未在Shape类中声明或声明为private。解决在Shape公共接口中添加virtual Shape* clone() const 0;。7. 从调试日志到生产习惯我的C类开发checklist做完这五组实验我养成了一个雷打不动的习惯每次写完一个类立刻打开终端用以下四步验证——不是为了交作业是为了让代码在真实项目里活下来。第一步内存泄漏扫描。用valgrind --leak-checkfull ./a.out跑一遍所有测试用例。重点看definitely lost和still reachable。曾有个学生Polynomial析构函数漏了delete[] pvalgrind直接标红8 bytes in 1 blocks are definitely lost他当场改掉。第二步const正确性压测。把所有const参数、const成员函数、const引用全加上然后故意写const String stest; s.cat(abc);——编译器报错就是最好的老师。我至今记得第一次看到error: passing const String as this argument discards qualifiers时的顿悟const不是修饰符是契约。第三步继承访问边界验证。写个测试文件用g -c编译专门尝试在protected继承的派生类外部调用基类public函数。编译失败说明你理解对了。成功赶紧查文档——99%是你记混了继承规则。第四步运算符重载语义审计。对照C标准检查operator是否返回新对象而非修改左值operator是否处理自赋值operator是否满足自反性aa为true、对称性ab等价于ba。曾有个项目operator没处理nullptr线上服务偶发coredump查了三天。这五组实验的价值不在代码本身而在于它强迫你直面C最硬核的命题谁拥有内存谁负责释放谁能看到什么谁承诺不改变什么从那以后我每次写类都会在头文件顶部用注释写明// 资源所有权p由本类完全管理析构时delete[]// const接口input()非constoutput()和length()为const// 继承契约public继承基类public成员对外可见——不是为了炫技是给三个月后的自己留一份能救命的说明书。希望帮到你。本文还有配套的精品资源点击获取