news 2026/10/24 22:02:50

吉林大学C++上机实验:面向对象工程实践与内存安全训练

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
吉林大学C++上机实验:面向对象工程实践与内存安全训练

简介:本资源是吉林大学《面向对象程序设计》课程配套的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项),更致命的是:当n>100时直接越界崩溃。而动态数组int* p = new int[n+1]将内存分配权交给堆区,n多大就申请多大。但代价是——你必须亲手管理生命周期。看原始代码中Polynomial::Polynomial(int n)构造函数:

Polynomial::Polynomial(int n){ p = new int[n]; // 注意:这里漏了 +1!应为 n+1,因为指数0到n共n+1项 }

这个bug会导致后续input()中p = new int[n + 1]覆盖原指针,造成内存泄漏。正确做法是统一用n+1,因为多项式a0 + a1x + ... + an*x^n共有n+1个系数。我一般会强制在构造函数参数名上体现语义:

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^2+2x+1(n=2),d2是5x^4+4x^3(n=4)。此时d1.p[3]和d1.p[4]根本不存在,直接访问会崩溃。正确解法是创建结果多项式时,按最大指数分配空间,并对缺失项补0:

polynomial 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]有双重风险:一是i+j可能越界(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_limits<streamsize>::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_limits<streamsize>::max(), '\n'); cout << "第" << i << "个系数输入错误,请重输:" << endl; } } }

注意:cin.ignore(...)比原始代码的while(cin.get() != '\n')更鲁棒,它能处理缓冲区残留的多个字符。

3. String类模拟:深拷贝、赋值安全与资源所有权转移

3.1 为什么cat()函数是典型反模式?字符串拼接的内存陷阱

原始String::cat()实现存在两处致命错误:

  1. strcat(str,st.str)直接修改原字符串str,但str空间仅够存原字符串,拼接后必然越界;
  2. char *strTmp = new char[len1+len2+1]分配了新空间却未使用,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[len+1]抛出bad_alloc,delete[] str已执行,str变野指针。工业级写法采用复制-交换惯用法(copy-and-swap):

String& 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中是public,private继承后它在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; // C++11起推荐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() { vector<unique_ptr<Employee>> employees; employees.push_back(make_unique<SalariedEmployee>()); employees.push_back(make_unique<HourlyEmpolyee>()); // ... 添加其他类型 // 多态调用 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()。

现象2:Manage对象复制后,Circle的area()计算结果错误

原因:Circle::clone()返回new Circle(*this),但Circle未定义拷贝构造函数,r成员被正确复制,但若Circle有动态成员则会浅拷贝。解决:为所有派生类显式定义拷贝构造函数,或使用= default(若无动态资源)。

现象3:Manage析构时部分Shape*未被delete

原因:count成员未在拷贝构造中正确初始化,导致析构循环次数错误。解决:在拷贝构造函数首行初始化count = other.count,并在所有构造函数中初始化count。

现象4:clone()函数返回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-check=full ./a.out跑一遍所有测试用例。重点看definitely lost和still reachable。曾有个学生Polynomial析构函数漏了delete[] p,valgrind直接标红8 bytes in 1 blocks are definitely lost,他当场改掉。

第二步:const正确性压测。把所有const参数、const成员函数、const引用全加上,然后故意写const String s="test"; s.cat("abc");——编译器报错就是最好的老师。我至今记得第一次看到error: passing 'const String' as 'this' argument discards qualifiers时的顿悟:const不是修饰符,是契约。

第三步:继承访问边界验证。写个测试文件,用g++ -c编译,专门尝试在protected继承的派生类外部调用基类public函数。编译失败?说明你理解对了。成功?赶紧查文档——99%是你记混了继承规则。

第四步:运算符重载语义审计。对照C++标准,检查operator+是否返回新对象(而非修改左值),operator=是否处理自赋值,operator==是否满足自反性(a==a为true)、对称性(a==b等价于b==a)。曾有个项目operator==没处理nullptr,线上服务偶发coredump,查了三天。

这五组实验的价值,不在代码本身,而在于它强迫你直面C++最硬核的命题:谁拥有内存?谁负责释放?谁能看到什么?谁承诺不改变什么?从那以后我每次写类,都会在头文件顶部用注释写明:
// 资源所有权:p由本类完全管理,析构时delete[]
// const接口:input()非const,output()和length()为const
// 继承契约:public继承,基类public成员对外可见
——不是为了炫技,是给三个月后的自己,留一份能救命的说明书。希望帮到你。

本文还有配套的精品资源,点击获取

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

图论入门核心概念与实战解析:从欧拉图到邻接表

简介&#xff1a;本资源是哈尔滨工业大学《集合论与图论&#xff08;下&#xff09;》慕课MOOC课程的配套习题答案整理&#xff0c;面向计算机、数学及相关专业本科生及自学者&#xff0c;助力系统掌握图论核心理论与解题方法。内容覆盖图的基本类型&#xff08;简单图、有向/无…

作者头像 李华
网站建设 2026/10/24 21:46:03

PDAL点云流式读写机制与PointView内存模型解析

简介&#xff1a;本资源为PDAL点云数据抽象库官方完整文档&#xff08;Release 2.2.0&#xff09;&#xff0c;面向三维地理信息、遥感测绘、自动驾驶点云处理等领域的开发者与科研人员&#xff0c;解决点云多格式读写、管道化处理、坐标转换与特征分析等核心工程问题。文档以P…

作者头像 李华
网站建设 2026/10/24 21:45:50

农村集体三资系统部署:IE8、IIS 7.5与MySQL 5.5配置

简介&#xff1a;《农村集体三资信息管理系统操作手册》面向农村集体单位的三资管理人员、财务经办人员及基层监管人员&#xff0c;围绕资金、资产、资源信息在系统中的登记、变动、清查与监督流程展开&#xff0c;可用于岗前培训、日常操作查阅及管理制度落地参考。包内含1个d…

作者头像 李华
网站建设 2026/10/24 21:45:29

操作系统期末复习指南:进程线程、调度算法与虚拟内存核心知识点梳理

简介&#xff1a;这份山东大学操作系统期末复习笔记面向准备操作系统课程期末考核的高校学生&#xff0c;尤其适合需要系统梳理考点、快速回顾核心概念的备考人群。内容覆盖操作系统概念与功能、系统结构、进程管理、进程状态转换、进程调度与进程间通信等模块&#xff0c;并对…

作者头像 李华
网站建设 2026/10/24 21:20:48

相平面法在车辆动力学控制中的实践与应用

1. 相平面法在车辆动力学控制中的应用概述相平面法作为一种经典的动态系统分析方法&#xff0c;在车辆动力学控制领域展现出独特的价值。特别是在处理"质心侧偏角-质心侧偏角速度"这对关键状态变量时&#xff0c;该方法能够直观地反映车辆横向运动的稳定性特征。我曾…

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

Delphi直连MySQL:libmysql.dll原生调用实战指南

简介&#xff1a;本资源是面向Delphi中高级开发者的技术实践包&#xff0c;聚焦于绕过OLE DB、直接调用MySQL原生客户端库&#xff08;mysql.dll&#xff09;实现数据库连接与操作的核心方案。适用于需提升数据库访问性能、规避Windows平台中间层依赖、或定制轻量级数据访问层的…

作者头像 李华