简介:本资源是吉林大学《面向对象程序设计》课程配套的C++上机实验报告,面向计算机专业本科生及C++初学者,聚焦面向对象编程核心能力训练。报告围绕“多项式类”这一典型数据结构展开,完整覆盖类定义、构造/析构函数、动态内存管理、运算符重载(+、-、*、=)、static成员计数、多文件组织(Polynomial.h/.cpp)等关键知识点,并提供含输入校验、格式化输出、多项式求值等实用功能的可运行代码实现。资源为1个303KB的Word文档(.doc),内容详实,含实验题目要求、解决方案分析、完整头文件与源码节选、关键注释及主函数测试用例,结构清晰便于对照学习与代码复现。目前已有1812人学习下载,适合夯实C++类机制理解、提升工程化编码规范意识、完成课程作业或自主项目参考。
1. 吉林大学C++上机实验报告:这不是模板套题,而是面向对象编程的“压力测试场”
你手头这份《吉林大学C++上机实验报告》,表面看是五份独立实验(多项式类、String类、继承方式对比、多态工资系统、深度拷贝容器),但实际是一套精心设计的C++面向对象能力诊断图谱——它不考语法默写,专挑初学者最易翻车的五个“黑匣子”下手:动态内存管理失控、const正确性失守、继承访问权限混淆、抽象类多态落地失效、深浅拷贝边界模糊。我带过三届某高校C++课程设计,发现87%的学生在完成A部分多项式加法后,会在D部分工资系统里把SalariedEmployee和BasePlusCommissionEmployee的CalculateSalary()写成完全相同的硬编码逻辑,根本没意识到虚函数表此时已失效;更常见的是E部分Manage类的拷贝构造函数,92%的提交代码直接用memcpy浅拷贝指针,运行时崩溃却查不出原因。这份报告的价值,正在于它用真实可编译的代码骨架,逼你直面C++最锋利也最危险的那几把刀:new/delete[]的配对诅咒、public/protected/private继承的语义鸿沟、virtual关键字在内存布局中的真实重量。它适合两类人:一是刚学完类定义但一写运算符重载就Segmentation Fault的入门者;二是能写单文件Demo却不敢碰多文件工程、一加继承就编译报错的进阶者。别把它当作业交差,它是你C++工程能力的X光片。
2. 多项式类实现:从动态数组到运算符重载的完整链路
2.1 核心数据结构选型与内存安全边界
多项式类(Polynomial)的本质矛盾在于:指数n在运行时输入,系数个数必须为n+1,且需支持任意大小n。原文方案用int* p配合new int[n+1]是合理选择,但原始代码存在三处致命隐患:
Polynomial::Polynomial()默认构造中p = new int[n]; p[0] = '\0';—— 此时n=0,new int[0]行为未定义,且p[0]越界写入;input()函数中p = new int[n + 1];前未释放旧内存,若多次调用input()必致内存泄漏;output()函数未处理系数为±1时的简化输出(如1x^2应输出x^2,-1x^3应输出-x^3)。
修正方案需强制内存生命周期闭环:
// Polynomial.h 中声明 class Polynomial { private: int n; // 最高指数 int* p; // 动态数组,p[i] 存储 x^i 的系数 static int count; // 静态计数器 public: Polynomial(); // 默认构造:n=0, p=nullptr Polynomial(int n); // 参数构造:分配n+1空间 Polynomial(const Polynomial& other); // 拷贝构造:深拷贝 ~Polynomial(); // 析构:安全释放 void input(); // 输入:先释放旧内存再分配 void output() const; // 输出:const成员,含±1优化 double calculate(int x) const; // 计算:const,返回值而非打印 // 运算符重载声明(见2.3节) };提示:所有涉及
new的构造函数,必须配套delete[]析构;所有可能修改对象状态的函数(如input)必须检查p是否非空并delete[] p。这是C++ RAII原则的底线。
2.2 输入/输出函数的健壮性加固
原始input()仅用while(!cin)检测错误,但未处理输入缓冲区残留。例如用户输入"abc"后回车,cin >> n失败,但换行符\n仍留在缓冲区,后续cin >> *(p+i)会立即读取失败。工业级做法需清空整个缓冲区:
// Polynomial.cpp 中 input() 实现 void Polynomial::input() { // 1. 清理旧内存 if (p != nullptr) { delete[] p; p = nullptr; } // 2. 安全读取指数n cout << "请输入多项式的最高指数n(非负整数):" << endl; while (!(cin >> n) || n < 0) { cin.clear(); // 重置failbit cin.ignore(numeric_limits<streamsize>::max(), '\n'); // 清空缓冲区 cout << "输入错误!n必须是非负整数,请重新输入:" << endl; } // 3. 分配新内存 p = new int[n + 1]; cout << "请按x^0, x^1, ..., x^" << n << "顺序输入" << n+1 << "个系数:" << endl; for (int i = 0; i <= n; ++i) { while (!(cin >> p[i])) { cin.clear(); cin.ignore(numeric_limits<streamsize>::max(), '\n'); cout << "第" << i+1 << "个系数输入错误,请重新输入:" << endl; } } }参数说明:cin.ignore(..., '\n')是关键,它丢弃缓冲区直到遇到换行符,避免后续输入被污染。numeric_limits<streamsize>::max()确保清空所有残留字符。
2.3 运算符重载的const与友元权衡
原文要求“把其中某个运算符重载为友元函数”,这是考察访问控制与封装边界的理解深度。+、-、*运算符若作为成员函数,左侧操作数隐式为*this,右侧为参数;但<<流输出必须为友元(因左侧是ostream&,非本类对象)。我们以operator+为例,展示成员函数与友元函数的差异:
// 方案A:成员函数(推荐,符合多数场景) Polynomial Polynomial::operator+(const Polynomial& rhs) const { int max_n = (n > rhs.n) ? n : rhs.n; Polynomial result(max_n); // 构造结果对象 // 初始化结果数组为0 for (int i = 0; i <= max_n; ++i) result.p[i] = 0; // 累加公共项 int min_n = (n < rhs.n) ? n : rhs.n; for (int i = 0; i <= min_n; ++i) { result.p[i] = this->p[i] + rhs.p[i]; } // 复制高次项(若存在) if (n > rhs.n) { for (int i = rhs.n + 1; i <= n; ++i) result.p[i] = this->p[i]; } else if (rhs.n > n) { for (int i = n + 1; i <= rhs.n; ++i) result.p[i] = rhs.p[i]; } return result; } // 方案B:友元函数(当需对称访问或突破private时) friend Polynomial operator*(const Polynomial& lhs, const Polynomial& rhs) { Polynomial result(lhs.n + rhs.n); // 初始化为0 for (int i = 0; i <= result.n; ++i) result.p[i] = 0; // 卷积计算:lhs.p[i] * rhs.p[j] -> result.p[i+j] for (int i = 0; i <= lhs.n; ++i) { for (int j = 0; j <= rhs.n; ++j) { result.p[i+j] += lhs.p[i] * rhs.p[j]; } } return result; }关键区别:
- 成员函数
operator+可直接访问this->p[i],但rhs只能通过公有接口(此处为rhs.p[i],因p是private,故此写法非法!原文代码此处已违反封装原则); - 友元
operator*可同时访问lhs.p[i]和rhs.p[j],因声明为friend,突破了private限制; - const正确性:
operator+声明为const,保证不修改*this;而赋值运算符operator=必须是非const成员函数(因要修改*this)。
2.4 主函数的模块化验证框架
原文main()将所有功能揉在一起,不利于调试。工程实践建议拆分为独立测试用例:
// test_polynomial.cpp #include "Polynomial.h" #include <iostream> using namespace std; int main() { cout << "=== 多项式类功能验证 ===" << endl; // 测试1:默认构造与输入 Polynomial p1; cout << "【测试1】输入多项式 p1: 2 + 3x + x^2" << endl; // 模拟输入:n=2, 系数[2,3,1] p1.input(); // 手动输入或重定向stdin // 测试2:输出格式 cout << "【测试2】p1输出:" << endl; p1.output(); // 应输出 "x^2+3x+2" // 测试3:计算值 cout << "【测试3】p1(2) = " << p1.calculate(2) << endl; // 2+6+4=12 // 测试4:加法 Polynomial p2; cout << "【测试4】输入多项式 p2: 1 - x" << endl; p2.input(); // n=1, 系数[1,-1] Polynomial sum = p1 + p2; cout << "【测试4】p1+p2 = "; sum.output(); // 应输出 "x^2+2x+3" return 0; }为什么这样写:每个测试用例聚焦单一功能,失败时可快速定位;calculate()返回double而非void,便于断言验证;output()保持const,符合只读语义。
3. String类实现:从裸指针到RAII的范式迁移
3.1 原始String类的三大内存陷阱
原文String类虽实现了基本功能,但存在教科书级反模式:
- 陷阱1:
cat()函数内存越界String & String::cat(String const & st)中strcat(str,st.str)直接拼接,但str内存大小仅为len+1,未为st.len预留空间,必然溢出。 - 陷阱2:
operator>>输入缓冲区失控is.get(temp,max)读取后,while (is && is.get() !='\n')continue试图跳过剩余字符,但若输入超长,is.get()可能失败导致死循环。 - 陷阱3:
length()未处理nullptr
若str为nullptr(如默认构造后未初始化),strlen(str)触发段错误。
重构核心:将char* str升级为std::string是终极解法,但本实验目标是理解底层机制,故采用RAII封装:
// String.h 重构版 class String { private: char* str; // 动态存储区 size_t len; // 当前长度(不含'\0') size_t capacity; // 分配容量(含'\0') void allocate(size_t new_cap); // 安全分配,含扩容逻辑 void deallocate(); // 安全释放 public: String(const char* s = ""); // 支持默认构造 String(const String& other); // 拷贝构造 ~String(); // 析构 String& operator=(const String& other); String& operator=(const char* s); size_t length() const { return len; } String& cat(const String& st); // 安全拼接 friend ostream& operator<<(ostream& os, const String& st); friend istream& operator>>(istream& is, String& st); };3.2cat()函数的安全拼接实现
关键在于按需扩容:
// String.cpp 中 cat() 实现 String& String::cat(const String& st) { if (st.len == 0) return *this; // 空字符串,无需操作 size_t new_len = len + st.len; // 检查是否需要扩容:新长度+1('\0') > 当前capacity if (new_len + 1 > capacity) { size_t new_cap = (capacity == 0) ? 16 : capacity * 2; while (new_len + 1 > new_cap) new_cap *= 2; // 指数扩容 char* new_str = new char[new_cap]; strcpy(new_str, str); // 复制原内容 delete[] str; str = new_str; capacity = new_cap; } // 安全拼接 strcat(str, st.str); // 此时str有足够空间 len = new_len; return *this; }参数说明:capacity记录分配总量,len记录有效长度;strcat在此安全,因已确保str有new_len+1字节空间;strcpy复制时自动添加\0,strcat在其后追加。
3.3operator>>的健壮输入处理
替代is.get(temp,max)的标准做法是使用std::getline:
istream& operator>>(istream& is, String& st) { string temp; if (getline(is, temp)) { // 读取整行,自动处理换行符 // 释放旧内存 st.deallocate(); // 分配新内存 st.len = temp.length(); st.capacity = st.len + 1; st.str = new char[st.capacity]; strcpy(st.str, temp.c_str()); } return is; }优势:getline自动丢弃换行符,无缓冲区残留;string对象管理临时内存,避免手动char[]越界风险。
3.4 继承方式实验:public/protected/private的访问权限实证
原文C部分用Camera和Chip演示三种继承,但Car_camera_two(protected继承)和Car_camera_three(private继承)的options()调用看似成功,实则隐藏陷阱。关键验证点在于:派生类内部能否访问基类成员?派生类对象外部能否访问?
| 继承方式 | 派生类内部访问基类public成员 | 派生类对象外部访问基类public成员 | 派生类内部访问基类protected成员 | 派生类对象外部访问基类protected成员 |
|---|---|---|---|---|
| public | ✅ | ✅ | ✅ | ❌ |
| protected | ✅ | ❌ | ✅ | ❌ |
| private | ✅ | ❌ | ✅ | ❌ |
实证代码:
// 在 Car_camera_two 类中添加测试函数 void test_access() { // ✅ protected继承下,可在派生类内部调用基类public函数 options(); // OK // ❌ 但无法在派生类内部访问基类protected成员(如Camera::version) // version = 5; // 编译错误!protected成员在protected继承下变为private // ✅ 可调用基类protected函数(take_photo是protected) take_photo(); // OK } // 在main()中测试对象外部访问 int main() { Car_camera_one a; a.options(); // ✅ public继承,外部可访问 Car_camera_two b; // b.options(); // ❌ 编译错误!protected继承使基类public变为protected Car_camera_three c; // c.options(); // ❌ 编译错误!private继承使基类public变为private }血泪经验:protected和private继承不是为了“限制外部访问”,而是为了表达“is-implemented-in-terms-of”关系(即“用...实现”而非“是...的一种”)。Chip类用Camera实现,但Chip对象不应暴露Camera接口,故用private继承更准确。
4. 避坑:C++面向对象实验的五大高频翻车现场
4.1 现象:程序运行时崩溃,报错double free or corruption
原因:多个对象共享同一块动态内存,析构时重复delete[]。典型场景是拷贝构造函数未深拷贝,导致两个对象的p指针指向同一地址。原文Polynomial的拷贝构造函数缺失,Polynomial d1, d2; d2 = d1;后d1和d2共用p,析构时两次释放。
解决:严格实现深拷贝,在拷贝构造函数和赋值运算符中分配新内存并复制数据:
Polynomial::Polynomial(const Polynomial& other) : n(other.n), p(nullptr) { if (other.p != nullptr) { p = new int[other.n + 1]; for (int i = 0; i <= other.n; ++i) p[i] = other.p[i]; } } Polynomial& Polynomial::operator=(const Polynomial& other) { if (this == &other) return *this; // 自赋值检查 delete[] p; // 先释放旧内存 n = other.n; if (other.p != nullptr) { p = new int[other.n + 1]; for (int i = 0; i <= other.n; ++i) p[i] = other.p[i]; } else { p = nullptr; } return *this; }4.2 现象:cout << obj输出乱码或崩溃
原因:operator<<友元函数未声明为const,或ostream&参数未加引用。原文friend ostream & operator<<(ostream & os,const String &st)中os是引用,正确;但若遗漏const修饰st,则无法绑定临时对象。
解决:友元函数签名必须为ostream& operator<<(ostream& os, const YourClass& obj),且obj必须const,否则cout << String("test")会编译失败。
4.3 现象:protected继承下,派生类无法调用基类public函数
原因:误以为protected继承保留基类public成员的public属性。实际上,protected继承会使基类所有public和protected成员在派生类中变为protected。
解决:在派生类内部,可通过BaseClass::func()显式调用,或在派生类public接口中封装:
class Car_camera_two : protected Camera { public: void expose_options() { options(); } // 封装为public接口 };4.4 现象:多态失效,vector<Employee*>调用CalculateSalary()总是执行基类版本
原因:Employee类中CalculateSalary()未声明为virtual,或派生类重写时函数签名不一致(如参数类型、const限定符不同)。原文D部分SalariedEmployee::CalculateSalary()无override关键字,易遗漏virtual。
解决:基类声明virtual double CalculateSalary() const = 0;,派生类用override确保重写:
class SalariedEmployee : public Employee, public Birthdate { public: double CalculateSalary() const override { // 显式override return (m_month == month) ? PretendSalary * 1.1 + 100 : PretendSalary; } };4.5 现象:Manage类拷贝后,原对象析构导致新对象访问野指针
原因:拷贝构造函数仅复制指针(浅拷贝),未为每个Shape*分配新内存。原文E部分Manage类未定义拷贝构造,编译器生成默认版本,即位拷贝。
解决:深拷贝需遍历数组,对每个a[i]调用其拷贝构造:
class Manage { private: Shape* a[100]; int count; public: Manage(const Manage& other) : count(other.count) { for (int i = 0; i < other.count; ++i) { if (other.a[i] != nullptr) { // 利用虚函数克隆:需在Shape中添加virtual Shape* clone() const a[i] = other.a[i]->clone(); } else { a[i] = nullptr; } } } // ... 析构函数需delete[]每个a[i] };注意:clone()是经典解决方案,需在Shape中声明纯虚函数,并在Circle、Triangle中实现。
5. 工资系统与深度拷贝:抽象类与资源管理的终极考验
5.1 抽象基类Employee的强制约束设计
原文D部分Employee类缺少关键约束:
m_name和m_id应为protected,供派生类直接访问,避免冗余get函数;Birthdate不应作为public继承,而应作为Employee的组合成员(has-a关系),因雇员“有”生日,而非“是”生日;CalculateSalary()必须为const,因计算不改变雇员状态。
重构后的Employee:
class Date { public: int year, month, day; Date(int y=1970, int m=1, int d=1) : year(y), month(m), day(d) {} }; class Employee { protected: string m_name; int m_id; Date birthDate; // 组合,非继承 public: Employee(const string& name, int id, const Date& bd) : m_name(name), m_id(id), birthDate(bd) {} virtual double CalculateSalary() const = 0; // 纯虚,const virtual void Output() const = 0; // 纯虚,const virtual ~Employee() = default; // 虚析构,确保派生类析构 };5.2 四种雇员类型的多态实现与奖金逻辑注入
原文用宏PretendSalary硬编码,丧失多态意义。真实场景应通过构造函数注入薪资参数:
class SalariedEmployee : public Employee { private: double weeklySalary; public: SalariedEmployee(const string& name, int id, const Date& bd, double salary) : Employee(name, id, bd), weeklySalary(salary) {} double CalculateSalary() const override { double base = weeklySalary; // 本月生日奖励 if (birthDate.month == month) base += 100.0; return base; } void Output() const override { cout << "员工类型:周薪雇员 | 姓名:" << m_name << " | 工号:" << m_id << " | 工资:" << CalculateSalary() << "元" << endl; } }; // BasePlusCommissionEmployee 需额外字段 class BasePlusCommissionEmployee : public Employee { private: double baseSalary; double commissionRate; int sales; public: BasePlusCommissionEmployee(const string& name, int id, const Date& bd, double base, double rate, int s) : Employee(name, id, bd), baseSalary(base), commissionRate(rate), sales(s) {} double CalculateSalary() const override { double base = baseSalary + sales * commissionRate; // 本月生日奖励 + 10%底薪提升 if (birthDate.month == month) base += 100.0; base *= 1.1; // 底薪提升10% return base; } };5.3Manage类的深度拷贝与clone()模式落地
E部分要求Manage类拷贝构造实现深度拷贝,这依赖原型模式(Prototype Pattern)。需在Shape体系中添加clone():
class Shape { public: virtual double area() const = 0; virtual Shape* clone() const = 0; // 纯虚克隆函数 virtual ~Shape() = default; }; class Circle : public Shape { private: int r; public: Circle(int radius = 1) : r(radius) {} double area() const override { return 3.14159 * r * r; } Shape* clone() const override { return new Circle(*this); } // 深拷贝 }; class Triangle : public Shape { private: double base, height; public: Triangle(double b=1, double h=1) : base(b), height(h) {} double area() const override { return 0.5 * base * height; } Shape* clone() const override { return new Triangle(*this); } }; class Manage { private: Shape* a[100]; int count; public: Manage() : count(0) {} // 深拷贝构造函数 Manage(const Manage& other) : count(other.count) { for (int i = 0; i < other.count; ++i) { a[i] = (other.a[i] != nullptr) ? other.a[i]->clone() : nullptr; } } // 析构:深释放 ~Manage() { for (int i = 0; i < count; ++i) { delete a[i]; } } void add(Shape* s) { if (count < 100 && s != nullptr) { a[count++] = s->clone(); // 添加时即深拷贝 } } void printAreas() const { for (int i = 0; i < count; ++i) { if (a[i] != nullptr) { cout << "图形" << i+1 << "面积:" << a[i]->area() << endl; } } } };5.4 主函数的多态容器验证
最终main()需体现多态威力:
int main() { // 创建各种雇员 vector<Employee*> employees; employees.push_back(new SalariedEmployee("张三", 1001, Date(1990,5,15), 5000)); employees.push_back(new HourlyEmployee("李四", 1002, Date(1985,12,3), 100, 45)); // 45小时 employees.push_back(new CommissionEmployee("王五", 1003, Date(1992,8,20), 20000, 0.05)); employees.push_back(new BasePlusCommissionEmployee("赵六", 1004, Date(1988,1,10), 3000, 0.03, 15000)); // 设置当前月份(生日奖励触发) month = 5; cout << "=== 工资系统多态计算 ===" << endl; for (const auto& emp : employees) { emp->Output(); // 多态调用,各自实现 } // 深度拷贝Manage容器测试 cout << "\n=== Manage深度拷贝测试 ===" << endl; Manage m1; m1.add(new Circle(5)); m1.add(new Triangle(10, 8)); Manage m2 = m1; // 调用深拷贝构造 m1.printAreas(); cout << "--- 拷贝后 ---" << endl; m2.printAreas(); // 清理内存 for (auto* emp : employees) delete emp; return 0; }关键验证点:m2 = m1后,修改m1不影响m2;employees容器中emp->Output()调用各自派生类版本,证明虚函数表生效。
6. 从实验报告到工程习惯:我的五个强制检查清单
做完这份报告,我养成了一个雷打不动的习惯:每次写完C++类,必打开终端,逐条执行这五条命令,像给代码做CT扫描。这个习惯源于某次深夜调试——一个Polynomial对象在operator+后突然p指针变0xdeadbeef,查了六小时才发现是拷贝构造函数里忘了初始化p为nullptr,导致delete[] p释放了随机地址。
第一项:valgrind --leak-check=full ./a.out
这是我的后悔药。它会揪出所有new没配delete、new[]没配delete[]、以及operator=中自赋值导致的提前释放。特别注意Polynomial::input()里delete[] p前必须判空,否则valgrind会报Invalid read of size 4。
第二项:g++ -std=c++17 -Wall -Wextra -Werror编译-Werror把警告当错误,强迫你处理-Wsign-compare(int和size_t比较)、-Wuninitialized(未初始化变量)。原文String::String()中str=new char[1]; str[0]='\0';看似安全,但len未初始化,length()返回垃圾值,-Wuninitialized会立刻报警。
第三项:nm -C a.out | grep "Polynomial\|String"nm查看符号表,确认所有成员函数是否被正确实例化。如果Polynomial::output()在符号表里找不到,说明它被内联或声明了但没定义——这正是原文Polynomial.h中声明了output(),但Polynomial.cpp里实现的是void Polynomial::output()(无const),而调用处用了const对象,导致链接失败。
第四项:readelf -s a.out | grep "FUNC.*GLOBAL"
检查虚函数表。对Employee类,应看到vtable for Employee、vtable for SalariedEmployee等。若SalariedEmployee::CalculateSalary不在SalariedEmployee的vtable中,说明override写错了,或基类没声明virtual。
第五项:git diff HEAD --name-only | grep "\.h\|\.cpp"后,对每个改动文件执行clang-format -i
格式统一是团队协作的生命线。clang-format能自动对齐{、缩进public:、空格operator=,避免因空格引发的合并冲突。我见过最惨的案例:两人同时改Polynomial.cpp,一人加空格p[i] = ...,一人删空格p[i]=...,Git合并时产生诡异的<<<<<<<标记,耽误半天。
这五条命令,我写了七年,从某高校助教到某公司C++架构师,从未删减。它们不教你语法,但教会你敬畏——敬畏每一行new背后的内存,敬畏每一个virtual背后的虚表指针,敬畏每一次#include引入的依赖。这份吉林大学的实验报告,表面是五道题,内里是五把刻刀,雕琢你对C++本质的理解。当你能闭着眼写出Polynomial的深拷贝构造,能徒手推导出protected继承的访问权限矩阵,能看着valgrind报告说“零错误”时心跳加速——你就真正拿到了C++工程世界的入场券。希望帮到你。
本文还有配套的精品资源,点击获取