简介:吉林大学C++上机实验报告围绕多项式类(Polynomial)的完整设计与实现,面向正在学习面向对象编程的C++初学者和课程设计学生。报告重点演示了类定义、构造与析构函数、拷贝构造、静态成员计数、运算符重载(+、-、*、=)以及多文件工程组织等核心知识点,并配有可运行的代码清单与解决方案描述,适合对照练习和课设参考。资源包内共1个doc文档,大小约303KB,内容涵盖实验题目、设计思路、程序实现与测试要点,便于一键查阅。已有1808人学习浏览,对需要完整C++实验报告范式和上机代码示例的读者有较强参考价值。文档对动态数组管理、内存释放、错误输入处理以及运算符重载语义等易错环节均有清晰说明,能帮助读者快速理解面向对象程序设计的落地写法。
1. C++ 实验报告:一份覆盖类设计、内存管理与继承体系的完整参考
如果你正在被C++课程设计折磨,看到「实验报告」四个字大概只想找个能直接复现的代码包。这份名为「面向对象程序课程设计」的上机实验报告值得你花二十分钟拆开看一遍:它覆盖了五个典型的面向对象实验——多项式类、自定义String类、继承访问权限、抽象类工资系统、容器深度拷贝,几乎把C++课里最容易扣分的几个考点一次性打穿。和网上那种只贴代码不解释的实验报告不同,这份报告保留了完整的思考过程,包括为什么用动态数组、哪些成员函数可以声明为 const、运算符重载时析构函数调用时机这些容易被忽略的细节。适合正在做C++课程设计的学生、准备期末上机复习的初级开发者,以及想找一个能覆盖多知识点的综合案例来做代码走读的人。下面按实验顺序拆解每部分的核心实现和真实存在的坑。
2. 多项式类:动态数组存储与四则运算重载的完整实现
2.1 为什么多项式必须用动态数组而不是固定长度数组
实验第一步就抛出一个问题:多项式可能任意大,用什么数据结构存储?报告中给出的答案是动态数组:类里用int n记录最高次数,用int* p动态分配 n+1 个 int 空间存系数,p[0] 对应常数项 a0,p[n] 对应最高次项的系数 anxn。为什么不用固定长度数组?因为多项式次数在运行时才能确定,固定数组要么开太大浪费内存,要么开太小直接越界。动态数组配合 new 分配,容量完全由输入的 n 决定,这是一开始就该想清楚的选型逻辑。
构造函数里有一个容易被忽略的细节:Polynomial::Polynomial()默认构造时p = new int[n]而 n=0,等于 new 了一个长度为 0 的数组。这在 C++ 里是合法但边界的行为——new int[0] 返回一个非空指针,但你不能对它做任何读写操作,代码里随后p[0] = '\0'实际上是越界写。这个问题在后面很多对象被默认构造时会暴露出来,比如运算符重载内部创建临时对象时用默认构造然后重新赋值 p 指针,这里先记下,后面避坑章会展开。
2.2 构造函数、static 成员计数与输入输出函数的写法
类定义中包含了三个构造函数:无参构造、带参构造Polynomial(int n)和拷贝构造。注意原代码里拷贝构造的声明写成了Polynomial(const int& Polynomial),参数名和类型都写错了,应该是Polynomial(const Polynomial& other),这种典型的「声明参数写错」在课程设计里非常常见,IDE 不报错但调用时会匹配到错误的构造函数,你需要自己修正。析构函数用delete[] p释放数组,配套关系必须是一一对应的 new[] 配 delete[],这是考核点之一。
static 成员变量在 B 部分的 String 类里体现得更明显,多项式部分只在报告中提示了「用 static 成员存储定义的多项式数量」,实际代码在 Operation.h 里定义static int count,并在构造函数里count++。这里有个语法要点:静态成员变量必须在类外初始化,所以 Operation.cpp 里有int polynomial::count = 0;,少了这一行直接链接报错。如果你在别的编译器上跑这段代码,第一件事就是检查这个类外定义有没有写。
输入函数 input() 里有一段错误处理值得学习:while (!cin)检测输入流状态,cin.clear()重置错误标志,再用while (cin.get() != '\n') continue;清空缓冲区里的残留字符,然后提示用户重新输入。这段逻辑对非数字输入(比如用户打了字母)有效,但有一个 bug:内层 for 循环读系数时遇到错误 break 了,之后没有重新触发出错系数位的输入,可能导致数组部分未初始化。正确做法是加一个「重新输入当前位」的循环,或者把整段输入改成循环重试直到填完所有系数。
2.3 加减乘与赋值运算符重载的算法细节
加法运算operator+的实现思路:先比较两个多项式的次数,Max_n = n > dxs.n ? n : dxs.n,然后申请 Max_n+1 长度的数组,把高次多项式的内容先拷进结果对象,再把低次的多项式逐位累加。这里有个隐含问题:结果对象 Sum 用默认构造函数创建时已经 new 了一次,但函数里直接Sum.p = new int[Sum.n + 1]覆盖了指针,原先分配的内存没有 delete,造成一次内存泄漏。这就是报告里提到的「要注意运算符重载过程中析构函数的调用」——临时对象在 return 时析构会 delete[] 结果数组,但中途被覆盖的旧指针没人管。
减法逻辑类似,但原代码里有一段赋值可疑:当dxs.n == Max_n时把dxs.p[i]拷给 dif.p,否则把p[i]拷给 dif.p,然后再统一做dif.p[i] = p[i] - dxs.p[i]。乍看没问题,但循环边界用的是Max_n + 1,当被减数比减数高阶时,超出减数范围的项直接用 p[i] 减去越界的 dxs.p[i],这是未定义行为。正确做法是按各自长度分区间处理:公共区间做减法,高次区间单独拷贝。
乘法运算符的算法是正确的:multiply.n = n + dxs.n,两层 for 循环,每一项交叉相乘,结果下标 i+j。但致命问题是multiply.p刚 new 出来时内容是未定义的,直接做multiply.p[i+j] = multiply.p[i+j] + temp[j]等于用垃圾值加完再赋值。必须先把结果数组清零——memset或for循环统一置 0,才能做累加。这个 bug 几乎会导致乘法结果完全错误,是这份代码里最大的雷。
2.4 拷贝构造缺失与 const 成员函数的取舍
报告的第 7 条要求考虑哪些成员函数可以声明为 const。分析一下:output()只读取成员不修改,可以声明 const;calculate(int x)内部只读 p 和 n,也应声明 const;input()要修改 n 和 p,不能加 const;运算符重载里operator+读两个对象但返回新对象,理论上是 const 成员函数,但 C++ 有语法限制——运算符重载若是非静态成员函数,右操作数可以是 const 引用,左操作数的 const 限定会影响调用。看看实际代码:Polynomial operator+(const Polynomial& dxs)没加 const 修饰,意思是左操作数的成员可以被修改,这放宽了语义约束,不算错但不够严谨。
深层的坑是这份报告没有真正实现拷贝构造函数。反过来看 B 部分的 String 类写了完整的拷贝构造,A 部分 Polynomial 的拷贝构造只是声明错了没实现。这意味着Polynomial d2 = d1;这种代码会走默认拷贝构造——浅拷贝,两个对象共享同一个 p 指针,析构时双重释放。如果你把这个实验当参考,补上拷贝构造函数是必须的。
3. 自定义 String 类:深拷贝、赋值重载与内存管理的教科书
3.1 字符串指针成员的内存模型
B 部分的实验要求模仿标准 string 类实现一个 String 类,核心是理解「字符串并不保存在对象中,字符串单独保存在堆内存中,对象仅保存了指向堆内存的指针」。类定义里有两个私有成员:char* str指向动态分配的字符数组,int len记录长度。构造函数的核心操作是:len = strlen(s); str = new char[len+1]; strcpy(str, s);,多出来的 +1 是为了存字符串末尾的 '\0',这个细节经常被新手漏掉,漏了就是缓冲区溢出。
默认构造函数里有个奇怪的写法:len = 4; str = new char[1]; str[0] = '\0';——len 设为 4 但只分配了 1 字节,len 和实际存储完全对不上。如果之后有人调用 length() 会返回 4,但字符串实际是空的。这个 bug 在单独跑 main 测试时不明显,因为没用到默认构造对象的 length(),但它是一个典型的「看起来能用、一扩功能就炸」的问题。修法是len = 0; str = new char[1]; str[0] = '\0';。
3.2 三个构造函数与拷贝构造的深拷贝实现
String 类写了三个构造函数:String(const char* s)用于String s = "字符串"的隐式转换;默认构造函数;String(const String& st)拷贝构造。拷贝构造是深度拷贝的范本:读取源对象的 len,分配同样大小的新数组,strcpy 复制内容,这样两个对象各自持有独立的堆内存,析构时不会互相踩踏。对比前面 Polynomial 类拷贝构造的缺失,这里才是正确的深拷贝范式。
运算符重载这一块有两个版本:operator=(const String&)和operator=(const char*),分别处理「String 对象赋值给 String 对象」和「C 风格字符串直接赋值给 String 对象」。第一个版本里有标准的自赋值检查if (this == &st) return *this;——如果没有这个检查,后续delete[] str后紧接着strcpy(str, st.str),当 this 和 st 是同一个对象时,str 已被释放,st.str 指向的内存也失效了,结果是未定义行为。报告总结里专门提到这一点:「函数应该避免将对象赋给自身;否则,给对象重新赋值之前,释放内存操作可能删除对象的内容」。这不是理论上吓唬人,是实际会翻车的场景。
3.3 输入输出流重载与 cat 连接函数的一个隐藏雷
友元函数operator<<和operator>>被声明为类的 friend,这样可以直接访问私有成员 str。operator<<的实现简单直接——os << st.str。operator>>里有一个边界处理:用char temp[max]先接收输入,max = 90意味着最多读 89 个字符,然后is.get(temp, max)会留下 '\n' 在缓冲区里,while (is && is.get() != '\n') continue;就是用来清空剩余缓冲的。这段逻辑和 Polynomial 输入函数里的清空手法类似,可以通用。
cat 函数的问题比较大。原代码:
String & String::cat(String const & st){ int len1,len2; len1=strlen(str); len2=strlen(st.str); char *strTmp = new char[len1+len2+1]; strcat(str,st.str); return *this; }这里strTmp分配了空间却没用,直接调strcat(str, st.str)把 st 的字符串拼接到 str 后面。如果 str 原来的容量不够存放拼接后的字符串,就是缓冲区溢出;如果 st 就是 this 自己(s.cat(s)),strcat的源和目标是同一块内存,属于重叠区域的未定义行为。正确的实现应该是把 len1 + len2 + 1 的内容全部拷进 strTmp,再 delete[] 旧的 str,把 str 指向 strTmp,更新 len。这份报告展示了「思路对但实现有缺陷」的典型状态,你复用时需要补全。
4. 继承三形态与抽象类工资系统:从访问权限到多态容器
4.1 三种继承方式对基类成员可见性的影响
C 部分的实验围绕一个场景:摄像机类 Camera 作为基类,行车记录仪 Car_camera 通过三种不同的继承方式(public、protected、private)继承,观察成员访问属性的差异。Camera 类里有三种访问级别的成员:private 的 version 和 code()、protected 的 take_photo() 和 image_set()、public 的构造函数。这个设计刻意让三种权限都有代表性成员,方便对比。
继承对可见性的影响可以用一张表概括:
| 继承方式 | 基类 public 成员在派生类中 | 基类 protected 成员在派生类中 | 基类 private 成员在派生类中 | 外部通过派生类对象访问基类 public 成员 |
|---|---|---|---|---|
| public | public | protected | 不可见 | 可以 |
| protected | protected | protected | 不可见 | 不可以 |
| private | private | private | 不可见 | 不可以 |
报告中的三个派生类 Car_camera_one、Car_camera_two、Car_camera_three 分别对应上述三种继承方式。main 函数里的测试很直观:第一种情况(public 继承)能直接a.take_photo(),因为 take_photo 在 Chip 里被重新声明为 protected,但 Car_camera_one 里又 public 化了;第二种情况只能b.options()和b.open(),因为 protected 继承把基类接口降级成 protected,外部对象调不到;第三种情况 private 继承同理。这三个类的对比跑一遍,比背书上的表格更管用。
4.2 虚函数、重定义与访问降级的组合设计
Chip 类里有一个值得注意的细节:virtual void take_photo()在 Camera 中是 protected virtual,Chip 里重定义并加了一行Camera::take_photo();调用基类版本,然后在autopicture()里调Chip::take_photo()。这里用限定符显式调用基类是被允许的,即使基类成员是 protected——派生类可以访问基类的 protected 成员,前提是通过派生类对象或 this 指针。Car_camera_one 里又把 take_photo 重定义为 public,实现了「行车记录仪用户可以操作摄像机的摄像功能」的需求。而 Car_camera_two 和 Car_camera_three 不暴露 take_photo,只暴露 options(),模拟「用户仅可以操作操作菜单」的限制。
报告中总结的三句话是关键考点:私有继承可以用类名和作用域解析运算符调用基类方法;保护继承与私有继承一样,基类接口在派生类内可用,但在继承体系之外不可用;私有/保护继承中调用基类函数要显式使用作用域限定符。跑完这段代码后自己改一改继承方式再编译,观察哪些调用报错、哪些不报错,比任何表格记忆都深。
4.3 抽象基类与四类雇员的工资计算
D 部分实验要求设计雇员工资发放系统:四类雇员(周薪、时薪、佣金、带底薪佣金),每类计算工资和输出信息,还要加上生日字段和生日当月奖励 100 元的逻辑。设计提示明确要求使用抽象类,所以 Employee 类里定义了两个纯虚函数:virtual double CalculateSalary() = 0和virtual void Output() = 0。
四种派生类各自实现这两个函数。注意代码里用#define PretendSalary 1000把四种雇员的收入基础都简化为 1000 了,这显然是应付作业的偷懒做法——真实的周薪和时薪计算逻辑完全不同,时薪雇员有 40 小时工作制的加班 150% 条款,佣金雇员收入取决于销售量。如果你要扩展成能用的系统,应该为每个派生类设计独立的成员变量(比如 hours、rate、sales、commission),并重写 CalculateSalary。
抽象类的关键知识点是:不能实例化抽象类对象,但可以定义抽象类的指针或引用,用多态调度。报告在 main 里直接创建了四个具体派生类对象分别调用,其实没有真正用到多态容器。题目明确要求「在主函数中创建一个容器用来管理公司各种雇员对象,多态地计算并输出每个雇员的工资」,所以标准做法是Employee* employees[4]或者 vector<Employee*>,分别 new 出四个类型,循环调用 CalculateSalary 和 Output。报告没写这一步,属于未完成实现。
4.4 多继承、生日奖金与静态绑定的隐患
SalariedEmployee 等四个类都继承了 Employee 和 Birthdate 两个基类,这是多重继承的典型用法。Birthdate 只有int m_month一个成员,全局变量int month存储当前月份,每个派生类的 CalculateSalary 里判断m_month == month就发 100 元生日奖金。这个设计能满足题面,但有一个多态陷阱:Employee 里没有把 CalculateSalary 和 Output 设为 public virtual,默认是 private 虚函数。如果通过基类指针Employee* e = new SalariedEmployee()调用 e->CalculateSalary() 会报错,因为派生类实现的 CalculateSalary 是 public,但基类里的声明是 private 且没有 public 访问点。实际代码里四个派生类的 m_id 和 m_name 都是直接 public 访问,这暴露了设计上没想清楚抽象类公开接口的问题。
另一个隐患在 Output 函数的实现方式:cout << "员工工资为:" << this->CalculateSalary() << endl;在 Output 内部调用虚函数 CalculateSalary,这是多态的正确用法(虚函数内部调虚函数仍然是动态绑定),但如果你在 main 里用具体类型对象调用,就走的是静态绑定,两个版本可能产生不同结果。报告里没有体现这个差异,需要自己补测试验证。
5. 避坑指南:五个内存管理与运算符重载的常见问题
5.1 现象:程序运行时报「double free or corruption」后崩溃
原因:多个对象共享同一个动态数组指针。典型场景是拷贝构造缺失时,Polynomial d2 = d1;让 d1 和 d2 的 p 指向同一块堆内存,两个对象依次析构时delete[] p执行了两次。报告 A 部分声明了拷贝构造函数但参数写错(const int&),实质上没有深拷贝实现,这是一个潜伏的地址越界和释放问题。
解决:补上合法的拷贝构造,分配新内存并逐位复制。标准写法为Polynomial(const Polynomial& other) : n(other.n), p(new int[other.n + 1]) { for (int i = 0; i <= n; ++i) p[i] = other.p[i]; }。
5.2 现象:多项式乘法结果出现天文数字或负数,明显不对
原因:operator*里multiply.p = new int[multiply.n + 1]分配后没有清零,紧接着multiply.p[i + j] = multiply.p[i + j] + temp[j]是在垃圾值上做累加。
解决:分配后立刻for (int i = 0; i <= multiply.n; ++i) multiply.p[i] = 0;或memset(multiply.p, 0, sizeof(int) * (multiply.n + 1));再进入双层循环累加。
5.3 现象:调用s.cat(a)后字符串内容错乱,甚至崩溃
原因:原 cat 函数中strcat(str, st.str)直接把 st 追加到 str 末尾,没有检查 str 的容量是否足够。最可怕的是s.cat(s)自身连接时源和目标是同一块内存,strcat 行为未定义,可能陷入死循环或破坏数据。
解决:先计算新长度 len1 + len2 + 1,new 一块新内存,依次 strcpy 和 strcat 到新内存,然后 delete[] 旧 str,更新 len,最后return *this。
5.4 现象:B 部分默认构造函数计算出的 length() 是 4,但字符串是空的
原因:String::String(){ len = 4; str = new char[1]; str[0] = '\0'; }里 len 被硬编码为 4,与实际分配的 1 字节数组和空字符串完全不匹配。任何基于 len 的后续操作都会出错。
解决:改为len = 0; str = new char[1]; str[0] = '\0';,保持 length() 与实际字符串长度一致。
5.5 现象:通过基类指针调用虚函数报错「’virtual double Employee::CalculateSalary()’ is private」
原因:抽象基类 Employee 里的virtual double CalculateSalary() = 0;和virtual void Output() = 0;声明在public:关键字之前,落入了类定义的默认 private 区域。派生类覆盖为 public 不会改变基类成员的访问权限——从外部通过基类指针访问时,看的是基类声明处的访问级别。
解决:在抽象基类里显式写public:,把这两个纯虚函数放在 public 访问区域,然后派生类覆盖时也保持 public。这是抽象类设计最容易犯的隐藏 bug,编译器不一定报错,但多态代码跑不起来。
6. 收尾一步:把整份实验报告改成能直接交的完整工程
拿到这份报告的光看代码是不够的,我建议你花一小时做一次重构:把 A 部分和 B 部分合并成多文件工程,补上所有缺失的拷贝构造函数、修正运算符重载中的清零和边界问题,然后用验证工具跑一遍。
第一步先修语法:把Polynomial(const int& Polynomial)改成合法的拷贝构造,String 类默认构造的 len 改成 0。第二步统一命名规范,A 部分用了 Polynomial 和 polynomial 两套类名,全改成 Polynomial。第三步按多文件标准组织目录:Polynomial.h放类声明,Polynomial.cpp放成员函数实现,main.cpp放测试代码;String 类同样拆成String.h、String.cpp、main.cpp。第四步补一个测试函数,覆盖Polynomial d2 = d1的拷贝、两个对象相加、相乘,以及 String 类自赋值s = s的场景。
第五步也是最容易被忽略的一步——用内存检测工具验证没有泄漏和非法访问。Linux 下直接g++ -g Polynomial.cpp main.cpp -o poly然后valgrind --leak-check=full ./poly,看到 "definitely lost: 0 bytes" 才算过关。Windows 下用 Visual Studio 的调试模式运行,检查输出窗口的 CRT 检测报告。实验报告里的代码几乎每次跑 valgrind 都会报一堆 "Invalid read/write" 和 "Mismatched free",你修复的过程中能真切理解深拷贝和析构函数的意义。
从那以后我每次写涉及动态内存的类,都强制走一遍「默认构造是否自洽、拷贝构造是否深拷贝、赋值是否自赋值安全、析构是否正常释放、运算符重载是否处理了中间对象」这五个检查点,再跑一次 valgrind 才算完。这份实验报告最大的价值不是代码本身能直接用,而是它提供了一个完整的案例——你顺着修一遍,C++ 面向对象的基本功就算打实了。希望帮到你。
本文还有配套的精品资源,点击获取