
簡介本資源是吉林大學《面向對象程序設計》課程配套的C上機實驗報告面向高校計算機類專業(yè)本科生及C初學者聚焦面向對象編程核心能力訓練。報告圍繞“多項式類”這一典型數據結構展開完整覆蓋類定義、構造/析構函數、動態(tài)內存管理、運算符重載、-、*、、static成員計數、多文件組織Polynomial.h/.cpp等關鍵知識點并提供含輸入校驗、格式化輸出、多項式求值等實用功能的可運行代碼實現。資源為1個303KB的Word文檔.doc內容包含實驗題目要求、詳細解決方案說明、完整頭文件與源碼節(jié)選、關鍵實現邏輯注釋及主函數測試用例結構清晰、注釋充分便于理解與復現。已有1810人學習下載適合用于課程作業(yè)參考、OOP概念鞏固及C工程化編碼規(guī)范入門實踐。1. 吉林大學C上機實驗報告這不是模板套題而是面向對象編程的「肌肉記憶訓練場」你手頭這份《吉林大學C上機實驗報告》表面看是五份獨立實驗多項式類、String類、繼承方式對比、雇員工資系統(tǒng)、圖形管理容器但實際是一條精心設計的「C能力進階鏈」——從內存裸操作new/delete起步到const正確性、運算符重載語義、繼承訪問控制邊界、抽象類多態(tài)落地、再到深度拷貝的資源所有權轉移。它不考語法糖專治“能看懂代碼但寫不出健壯類”的通病。我?guī)н^三屆某高校C課程設計學生最常翻車的不是邏輯錯誤而是delete[] p忘加方括號、operator沒處理自賦值、protected繼承后在派生類外誤調基類函數、virtual析構函數缺失導致子類資源泄漏……這些坑全被壓縮在這五組實驗里。它適合兩類人一是剛學完C基礎語法、正卡在“怎么把書上概念變成可運行代碼”階段的新手二是想用最小成本系統(tǒng)補全面向對象工程細節(jié)的老手——比如你發(fā)現自己的項目里總要反復加if (this rhs) return *this;那說明你缺的不是技巧是肌肉記憶。這份報告不是交差作業(yè)它是用5個真實場景逼你親手把C的“安全契約”刻進手指神經。2. 多項式類實現動態(tài)數組管理與運算符重載的完整閉環(huán)2.1 為什么必須用動態(tài)數組靜態(tài)數組在這里是死路多項式次數n在運行時由用戶輸入決定編譯期無法確定大小。若用int p[100]不僅浪費內存低次多項式占滿100項更致命的是當n100時直接越界崩潰。而動態(tài)數組int* p new int[n1]將內存分配權交給堆區(qū)n多大就申請多大。但代價是——你必須親手管理生命周期。看原始代碼中Polynomial::Polynomial(int n)構造函數Polynomial::Polynomial(int n){ p new int[n]; // 注意這里漏了 1應為 n1因為指數0到n共n1項 }這個bug會導致后續(xù)input()中p new int[n 1]覆蓋原指針造成內存泄漏。正確做法是統(tǒng)一用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]根本不存在直接訪問會崩潰。正確解法是創(chuàng)建結果多項式時按最大指數分配空間并對缺失項補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已預留空間但循環(huá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狀態(tài)機的三步清洗法原始代碼中input()函數對錯誤輸入的處理是半成品。cin.clear()只重置錯誤標志while(cin.get() ! \n)只清空緩沖區(qū)但沒處理p數組的內存泄漏。當用戶輸入字母而非數字時cin n失敗p仍為nullptr后續(xù)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)更魯棒它能處理緩沖區(qū)殘留的多個字符。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的四大雷區(qū)自賦值、異常安全、資源泄漏、淺拷貝原始String::operator實現雖有自賦值檢查但缺少異常安全防護。若new char[len1]拋出bad_allocdelete[] str已執(zhí)行str變野指針。工業(yè)級寫法采用復制-交換慣用法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); }此寫法天然規(guī)避自賦值問題交換自身無影響且other的構造若失敗*this完全不受影響。3.3length()為何必須是const成員函數const正確性的工程價值length()只讀取len成員不修改對象狀態(tài)。若不加const則無法在const String s引用上調用void printLength(const String s) { cout s.length(); // 編譯錯誤除非length()聲明為const }這不僅是語法要求更是接口契約const成員函數向調用者承諾“我絕不改變對象”讓編譯器能做更多優(yōu)化如內聯也讓其他開發(fā)者敢放心傳遞const引用。原始代碼中int length() const{ return len; }寫法完全正確這是C面向對象設計的基石習慣。4. 繼承方式實戰(zhàn)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()時會發(fā)現它不可見——因為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. 雇員工資系統(tǒng)抽象類多態(tài)與容器管理的工程落地5.1 抽象類設計的硬性約束純虛函數與虛析構函數原始Employee類定義為class Employee { virtual double CalculateSalary() 0; // 純虛函數 virtual void Output() 0; public: string m_name; int m_id; };但缺少虛析構函數若通過Employee* ptr new SalariedEmployee;創(chuàng)建對象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 容器管理的多態(tài)實踐用vector存儲基類指針原始Main.cpp未實現容器管理需補充#include vector #include memory int main() { vectorunique_ptrEmployee employees; employees.push_back(make_uniqueSalariedEmployee()); employees.push_back(make_uniqueHourlyEmpolyee()); // ... 添加其他類型 // 多態(tài)調用 for (const auto emp : employees) { emp-Output(); cout 工資: emp-CalculateSalary() endl; } }unique_ptr確保資源自動釋放vector提供動態(tài)擴容emp-Output()自動調用對應派生類版本——這才是面向對象容器管理的標準姿勢。6. 圖形管理容器深度拷貝的資源所有權移交與避坑指南6.1 深度拷貝的核心每個指針都必須new每個new都必須delete原始Manage類要求實現深度拷貝。若只復制指針淺拷貝兩個Manage對象將指向同一塊Shape*內存析構時delete同一地址兩次程序崩潰。深度拷貝必須為每個a[i]創(chuàng)建新對象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) { // 根據動態(tài)類型創(chuàng)建新對象需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**模式讓基類能根據實際類型創(chuàng)建副本。6.2 避坑常見問題與血淚排查記錄現象1程序運行時崩潰報錯double free or corruption原因Manage的拷貝構造函數未實現深度拷貝導致兩個對象的a[i]指向同一內存析構時delete兩次。解決嚴格遵循“每個new配對一個delete”拷貝時對每個非空指針調用clone()?,F象2Manage對象復制后Circle的area()計算結果錯誤原因Circle::clone()返回new Circle(*this)但Circle未定義拷貝構造函數r成員被正確復制但若Circle有動態(tài)成員則會淺拷貝。解決為所有派生類顯式定義拷貝構造函數或使用 default若無動態(tài)資源?,F象3Manage析構時部分Shape*未被delete原因count成員未在拷貝構造中正確初始化導致析構循環(huán)次數錯誤。解決在拷貝構造函數首行初始化count other.count并在所有構造函數中初始化count?,F象4clone()函數返回nullptr后續(xù)調用area()崩潰原因clone()未處理nullptr輸入或Shape基類未定義clone()純虛函數。解決在Shape中聲明virtual Shape* clone() const 0;所有派生類必須實現?,F象5編譯報錯clone is not a member of Shape原因clone()函數未在Shape類中聲明或聲明為private。解決在Shape公共接口中添加virtual Shape* clone() const 0;。7. 從調試日志到生產習慣我的C類開發(fā)checklist做完這五組實驗我養(yǎng)成了一個雷打不動的習慣每次寫完一個類立刻打開終端用以下四步驗證——不是為了交作業(yè)是為了讓代碼在真實項目里活下來。第一步內存泄漏掃描。用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%是你記混了繼承規(guī)則。第四步運算符重載語義審計。對照C標準檢查operator是否返回新對象而非修改左值operator是否處理自賦值operator是否滿足自反性aa為true、對稱性ab等價于ba。曾有個項目operator沒處理nullptr線上服務偶發(fā)coredump查了三天。這五組實驗的價值不在代碼本身而在于它強迫你直面C最硬核的命題誰擁有內存誰負責釋放誰能看到什么誰承諾不改變什么從那以后我每次寫類都會在頭文件頂部用注釋寫明// 資源所有權p由本類完全管理析構時delete[]// const接口input()非constoutput()和length()為const// 繼承契約public繼承基類public成員對外可見——不是為了炫技是給三個月后的自己留一份能救命的說明書。希望幫到你。本文還有配套的精品資源點擊獲取