C++上機(jī)實(shí)驗(yàn):面向?qū)ο蠊こ虒?shí)踐與內(nèi)存安全訓(xùn)練)
簡介本資源是吉林大學(xué)《面向?qū)ο蟪绦蛟O(shè)計(jì)》課程配套的C上機(jī)實(shí)驗(yàn)報(bào)告面向高校計(jì)算機(jī)類專業(yè)本科生及C初學(xué)者聚焦面向?qū)ο缶幊毯诵哪芰τ?xùn)練。報(bào)告圍繞“多項(xiàng)式類”這一典型數(shù)據(jù)結(jié)構(gòu)展開完整覆蓋類定義、構(gòu)造/析構(gòu)函數(shù)、動(dòng)態(tài)內(nèi)存管理、運(yùn)算符重載、-、*、、static成員計(jì)數(shù)、多文件組織Polynomial.h/.cpp等關(guān)鍵知識點(diǎn)并提供含輸入校驗(yàn)、格式化輸出、多項(xiàng)式求值等實(shí)用功能的可運(yùn)行代碼實(shí)現(xiàn)。資源為1個(gè)303KB的Word文檔.doc內(nèi)容包含實(shí)驗(yàn)題目要求、詳細(xì)解決方案說明、完整頭文件與源碼節(jié)選、關(guān)鍵實(shí)現(xiàn)邏輯注釋及主函數(shù)測試用例結(jié)構(gòu)清晰、注釋充分便于理解與復(fù)現(xiàn)。已有1810人學(xué)習(xí)下載適合用于課程作業(yè)參考、OOP概念鞏固及C工程化編碼規(guī)范入門實(shí)踐。1. 吉林大學(xué)C上機(jī)實(shí)驗(yàn)報(bào)告這不是模板套題而是面向?qū)ο缶幊痰摹讣∪庥洃浻?xùn)練場」你手頭這份《吉林大學(xué)C上機(jī)實(shí)驗(yàn)報(bào)告》表面看是五份獨(dú)立實(shí)驗(yàn)多項(xiàng)式類、String類、繼承方式對比、雇員工資系統(tǒng)、圖形管理容器但實(shí)際是一條精心設(shè)計(jì)的「C能力進(jìn)階鏈」——從內(nèi)存裸操作new/delete起步到const正確性、運(yùn)算符重載語義、繼承訪問控制邊界、抽象類多態(tài)落地、再到深度拷貝的資源所有權(quán)轉(zhuǎn)移。它不考語法糖專治“能看懂代碼但寫不出健壯類”的通病。我?guī)н^三屆某高校C課程設(shè)計(jì)學(xué)生最常翻車的不是邏輯錯(cuò)誤而是delete[] p忘加方括號、operator沒處理自賦值、protected繼承后在派生類外誤調(diào)基類函數(shù)、virtual析構(gòu)函數(shù)缺失導(dǎo)致子類資源泄漏……這些坑全被壓縮在這五組實(shí)驗(yàn)里。它適合兩類人一是剛學(xué)完C基礎(chǔ)語法、正卡在“怎么把書上概念變成可運(yùn)行代碼”階段的新手二是想用最小成本系統(tǒng)補(bǔ)全面向?qū)ο蠊こ碳?xì)節(jié)的老手——比如你發(fā)現(xiàn)自己的項(xiàng)目里總要反復(fù)加if (this rhs) return *this;那說明你缺的不是技巧是肌肉記憶。這份報(bào)告不是交差作業(yè)它是用5個(gè)真實(shí)場景逼你親手把C的“安全契約”刻進(jìn)手指神經(jīng)。2. 多項(xiàng)式類實(shí)現(xiàn)動(dòng)態(tài)數(shù)組管理與運(yùn)算符重載的完整閉環(huán)2.1 為什么必須用動(dòng)態(tài)數(shù)組靜態(tài)數(shù)組在這里是死路多項(xiàng)式次數(shù)n在運(yùn)行時(shí)由用戶輸入決定編譯期無法確定大小。若用int p[100]不僅浪費(fèi)內(nèi)存低次多項(xiàng)式占滿100項(xiàng)更致命的是當(dāng)n100時(shí)直接越界崩潰。而動(dòng)態(tài)數(shù)組int* p new int[n1]將內(nèi)存分配權(quán)交給堆區(qū)n多大就申請多大。但代價(jià)是——你必須親手管理生命周期??丛即a中Polynomial::Polynomial(int n)構(gòu)造函數(shù)Polynomial::Polynomial(int n){ p new int[n]; // 注意這里漏了 1應(yīng)為 n1因?yàn)橹笖?shù)0到n共n1項(xiàng) }這個(gè)bug會導(dǎo)致后續(xù)input()中p new int[n 1]覆蓋原指針造成內(nèi)存泄漏。正確做法是統(tǒng)一用n1因?yàn)槎囗?xiàng)式a0 a1x ... an*x^n共有n1個(gè)系數(shù)。我一般會強(qiáng)制在構(gòu)造函數(shù)參數(shù)名上體現(xiàn)語義Polynomial::Polynomial(int degree) : n(degree) { p new int[n 1](); // ()初始化為0避免野值 }提示new int[n 1]()中的()很重要——它將數(shù)組所有元素初始化為0否則p[i]可能是隨機(jī)垃圾值導(dǎo)致output()輸出亂碼。2.2 運(yùn)算符重載的語義陷阱加減乘的邊界條件必須顯式處理原始代碼中operator和operator-的實(shí)現(xiàn)存在嚴(yán)重邏輯漏洞。它假設(shè)兩個(gè)多項(xiàng)式指數(shù)范圍完全對齊但實(shí)際d1可能是3x^22x1n2d2是5x^44x^3n4。此時(shí)d1.p[3]和d1.p[4]根本不存在直接訪問會崩潰。正確解法是創(chuàng)建結(jié)果多項(xiàng)式時(shí)按最大指數(shù)分配空間并對缺失項(xiàng)補(bǔ)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 // 將當(dāng)前多項(xiàng)式系數(shù)復(fù)制到sum for (int i 0; i n; i) { sum.p[i] p[i]; } // 將dxs多項(xiàng)式系數(shù)加到sum自動(dòng)跳過超出dxs.n的索引 for (int i 0; i dxs.n; i) { sum.p[i] dxs.p[i]; } return sum; }同理operator*的實(shí)現(xiàn)中multiply.p[i j] multiply.p[i j] temp[j]有雙重風(fēng)險(xiǎn)一是ij可能越界multiply.n n dxs.n已預(yù)留空間但循環(huán)未校驗(yàn)二是temp數(shù)組未初始化。修正版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中間數(shù)組 } } return multiply; }2.3 輸入容錯(cuò)機(jī)制cin狀態(tài)機(jī)的三步清洗法原始代碼中input()函數(shù)對錯(cuò)誤輸入的處理是半成品。cin.clear()只重置錯(cuò)誤標(biāo)志while(cin.get() ! \n)只清空緩沖區(qū)但沒處理p數(shù)組的內(nèi)存泄漏。當(dāng)用戶輸入字母而非數(shù)字時(shí)cin n失敗p仍為nullptr后續(xù)p new int[n 1]會因n為隨機(jī)值而崩潰。完整清洗流程void polynomial::input() { cout 請輸入多項(xiàng)式的最高指數(shù) endl; while (!(cin n)) { // 檢測輸入失敗 cin.clear(); // 清除failbit cin.ignore(numeric_limitsstreamsize::max(), \n); // 清空整行 cout 輸入錯(cuò)誤請重新輸入一個(gè)整數(shù) endl; } // 安全分配內(nèi)存 p new int[n 1](); cout 請從低到高輸入系數(shù)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 個(gè)系數(shù)輸入錯(cuò)誤請重輸 endl; } } }注意cin.ignore(...)比原始代碼的while(cin.get() ! \n)更魯棒它能處理緩沖區(qū)殘留的多個(gè)字符。3. String類模擬深拷貝、賦值安全與資源所有權(quán)轉(zhuǎn)移3.1 為什么cat()函數(shù)是典型反模式字符串拼接的內(nèi)存陷阱原始String::cat()實(shí)現(xiàn)存在兩處致命錯(cuò)誤strcat(str,st.str)直接修改原字符串str但str空間僅夠存原字符串拼接后必然越界char *strTmp new char[len1len21]分配了新空間卻未使用return *this返回的是被破壞的原對象。正確做法是先分配足夠空間再復(fù)制原字符串最后追加新字符串。但更關(guān)鍵的是——cat()不該修改自身而應(yīng)返回新對象類似標(biāo)準(zhǔn)庫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); // 調(diào)用String(const char*)構(gòu)造函數(shù) delete[] new_str; // 釋放臨時(shí)空間 return result; }3.2operator的四大雷區(qū)自賦值、異常安全、資源泄漏、淺拷貝原始String::operator實(shí)現(xiàn)雖有自賦值檢查但缺少異常安全防護(hù)。若new char[len1]拋出bad_allocdelete[] str已執(zhí)行str變野指針。工業(yè)級寫法采用復(fù)制-交換慣用法copy-and-swapString String::operator(String other) { // 注意參數(shù)傳值自動(dòng)調(diào)用拷貝構(gòu)造 swap(*this, other); // 交換this和other的str指針 return *this; // other析構(gòu)時(shí)自動(dòng)釋放舊內(nèi)存 } void swap(String first, String second) noexcept { using std::swap; swap(first.str, second.str); swap(first.len, second.len); }此寫法天然規(guī)避自賦值問題交換自身無影響且other的構(gòu)造若失敗*this完全不受影響。3.3length()為何必須是const成員函數(shù)const正確性的工程價(jià)值length()只讀取len成員不修改對象狀態(tài)。若不加const則無法在const String s引用上調(diào)用void printLength(const String s) { cout s.length(); // 編譯錯(cuò)誤除非length()聲明為const }這不僅是語法要求更是接口契約const成員函數(shù)向調(diào)用者承諾“我絕不改變對象”讓編譯器能做更多優(yōu)化如內(nèi)聯(lián)也讓其他開發(fā)者敢放心傳遞const引用。原始代碼中int length() const{ return len; }寫法完全正確這是C面向?qū)ο笤O(shè)計(jì)的基石習(xí)慣。4. 繼承方式實(shí)戰(zhàn)public/protected/private的訪問控制邊界驗(yàn)證4.1 三種繼承方式的本質(zhì)差異不是語法糖是接口暴露策略原始Camera體系中Chip類繼承Camera再由Car_camera_one/two/three分別以public/protected/private繼承Chip。關(guān)鍵點(diǎn)在于繼承方式?jīng)Q定基類成員在派生類內(nèi)部的可訪問性以及在派生類外部的可見性。繼承方式基類public成員在派生類內(nèi)基類protected成員在派生類內(nèi)基類public成員在派生類對象外publicpublicprotected可見如a.take_photo()protectedprotectedprotected不可見b.take_photo()編譯錯(cuò)誤privateprivateprivate不可見c.take_photo()編譯錯(cuò)誤原始代碼中Car_camera_two和Car_camera_three的options()函數(shù)能調(diào)用Chip::options()是因?yàn)閛ptions()在Chip中是public而protected/private繼承只是限制了外部調(diào)用不阻止派生類內(nèi)部調(diào)用基類public成員。4.2protected繼承的典型誤用試圖在外部調(diào)用基類函數(shù)常見錯(cuò)誤寫法Car_camera_two b; b.take_photo(); // 編譯錯(cuò)誤take_photo()在Car_camera_two中是protected但原始main()中并未出現(xiàn)此調(diào)用說明作者理解正確。真正易錯(cuò)的是當(dāng)Car_camera_two的子類想調(diào)用take_photo()時(shí)會發(fā)現(xiàn)它不可見——因?yàn)閜rotected繼承使基類public成員在派生類中降級為protected而protected成員不能被派生類的子類訪問除非該子類也protected繼承。4.3private繼承的隱藏價(jià)值實(shí)現(xiàn)復(fù)用而非接口繼承private繼承常被誤解為“錯(cuò)誤用法”實(shí)則是C中組合composition的替代方案。當(dāng)Car_camera_three需要Chip的功能但不想暴露其接口時(shí)private繼承比has-a組合更輕量無需額外成員變量。原始代碼中c.options()能調(diào)用是因?yàn)閛ptions()在Chip中是publicprivate繼承后它在Car_camera_three內(nèi)部仍是可訪問的只是對外不可見。這完美匹配需求“其他公司購買行車記錄儀不得泄露其全部內(nèi)容”。5. 雇員工資系統(tǒng)抽象類多態(tài)與容器管理的工程落地5.1 抽象類設(shè)計(jì)的硬性約束純虛函數(shù)與虛析構(gòu)函數(shù)原始Employee類定義為class Employee { virtual double CalculateSalary() 0; // 純虛函數(shù) virtual void Output() 0; public: string m_name; int m_id; };但缺少虛析構(gòu)函數(shù)若通過Employee* ptr new SalariedEmployee;創(chuàng)建對象delete ptr時(shí)只會調(diào)用Employee析構(gòu)函數(shù)SalariedEmployee的資源如有不會釋放。必須添加virtual ~Employee() default; // C11起推薦default或{}實(shí)現(xiàn)5.2 多重繼承的隱患Birthdate的菱形繼承風(fēng)險(xiǎn)原始代碼中SalariedEmployee : public Employee, public Birthdate采用多重繼承。若未來CommissionEmployee也繼承Birthdate而BasePlusCommissionEmployee同時(shí)繼承二者則Birthdate會被重復(fù)繼承菱形繼承。解決方案是讓Birthdate成為virtual基類class Birthdate { /* ... */ }; class SalariedEmployee : public Employee, virtual public Birthdate { /* ... */ };這樣所有派生類共享同一份Birthdate子對象。5.3 容器管理的多態(tài)實(shí)踐用vector存儲基類指針原始Main.cpp未實(shí)現(xiàn)容器管理需補(bǔ)充#include vector #include memory int main() { vectorunique_ptrEmployee employees; employees.push_back(make_uniqueSalariedEmployee()); employees.push_back(make_uniqueHourlyEmpolyee()); // ... 添加其他類型 // 多態(tài)調(diào)用 for (const auto emp : employees) { emp-Output(); cout 工資: emp-CalculateSalary() endl; } }unique_ptr確保資源自動(dòng)釋放vector提供動(dòng)態(tài)擴(kuò)容emp-Output()自動(dòng)調(diào)用對應(yīng)派生類版本——這才是面向?qū)ο笕萜鞴芾淼臉?biāo)準(zhǔn)姿勢。6. 圖形管理容器深度拷貝的資源所有權(quán)移交與避坑指南6.1 深度拷貝的核心每個(gè)指針都必須new每個(gè)new都必須delete原始Manage類要求實(shí)現(xiàn)深度拷貝。若只復(fù)制指針淺拷貝兩個(gè)Manage對象將指向同一塊Shape*內(nèi)存析構(gòu)時(shí)delete同一地址兩次程序崩潰。深度拷貝必須為每個(gè)a[i]創(chuàng)建新對象class Manage { Shape* a[100]; int count; // 當(dāng)前存儲的圖形數(shù)量 public: Manage() : count(0) {} // 深度拷貝構(gòu)造函數(shù) Manage(const Manage other) : count(other.count) { for (int i 0; i other.count; i) { if (other.a[i] ! nullptr) { // 根據(jù)動(dòng)態(tài)類型創(chuàng)建新對象需Shape有虛clone()函數(shù) a[i] other.a[i]-clone(); // 假設(shè)Shape定義了virtual Shape* clone() 0; } else { a[i] nullptr; } } } // 深度拷貝賦值 Manage operator(const Manage other) { if (this other) return *this; // 先清理當(dāng)前資源 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]; } } };關(guān)鍵點(diǎn)clone()是典型的**虛構(gòu)造函數(shù)virtual constructor**模式讓基類能根據(jù)實(shí)際類型創(chuàng)建副本。6.2 避坑常見問題與血淚排查記錄現(xiàn)象1程序運(yùn)行時(shí)崩潰報(bào)錯(cuò)double free or corruption原因Manage的拷貝構(gòu)造函數(shù)未實(shí)現(xiàn)深度拷貝導(dǎo)致兩個(gè)對象的a[i]指向同一內(nèi)存析構(gòu)時(shí)delete兩次。解決嚴(yán)格遵循“每個(gè)new配對一個(gè)delete”拷貝時(shí)對每個(gè)非空指針調(diào)用clone()?,F(xiàn)象2Manage對象復(fù)制后Circle的area()計(jì)算結(jié)果錯(cuò)誤原因Circle::clone()返回new Circle(*this)但Circle未定義拷貝構(gòu)造函數(shù)r成員被正確復(fù)制但若Circle有動(dòng)態(tài)成員則會淺拷貝。解決為所有派生類顯式定義拷貝構(gòu)造函數(shù)或使用 default若無動(dòng)態(tài)資源?,F(xiàn)象3Manage析構(gòu)時(shí)部分Shape*未被delete原因count成員未在拷貝構(gòu)造中正確初始化導(dǎo)致析構(gòu)循環(huán)次數(shù)錯(cuò)誤。解決在拷貝構(gòu)造函數(shù)首行初始化count other.count并在所有構(gòu)造函數(shù)中初始化count?,F(xiàn)象4clone()函數(shù)返回nullptr后續(xù)調(diào)用area()崩潰原因clone()未處理nullptr輸入或Shape基類未定義clone()純虛函數(shù)。解決在Shape中聲明virtual Shape* clone() const 0;所有派生類必須實(shí)現(xiàn)?,F(xiàn)象5編譯報(bào)錯(cuò)clone is not a member of Shape原因clone()函數(shù)未在Shape類中聲明或聲明為private。解決在Shape公共接口中添加virtual Shape* clone() const 0;。7. 從調(diào)試日志到生產(chǎn)習(xí)慣我的C類開發(fā)checklist做完這五組實(shí)驗(yàn)我養(yǎng)成了一個(gè)雷打不動(dòng)的習(xí)慣每次寫完一個(gè)類立刻打開終端用以下四步驗(yàn)證——不是為了交作業(yè)是為了讓代碼在真實(shí)項(xiàng)目里活下來。第一步內(nèi)存泄漏掃描。用valgrind --leak-checkfull ./a.out跑一遍所有測試用例。重點(diǎn)看definitely lost和still reachable。曾有個(gè)學(xué)生Polynomial析構(gòu)函數(shù)漏了delete[] pvalgrind直接標(biāo)紅8 bytes in 1 blocks are definitely lost他當(dāng)場改掉。第二步const正確性壓測。把所有const參數(shù)、const成員函數(shù)、const引用全加上然后故意寫const String stest; s.cat(abc);——編譯器報(bào)錯(cuò)就是最好的老師。我至今記得第一次看到error: passing const String as this argument discards qualifiers時(shí)的頓悟const不是修飾符是契約。第三步繼承訪問邊界驗(yàn)證。寫個(gè)測試文件用g -c編譯專門嘗試在protected繼承的派生類外部調(diào)用基類public函數(shù)。編譯失敗說明你理解對了。成功趕緊查文檔——99%是你記混了繼承規(guī)則。第四步運(yùn)算符重載語義審計(jì)。對照C標(biāo)準(zhǔn)檢查operator是否返回新對象而非修改左值operator是否處理自賦值operator是否滿足自反性aa為true、對稱性ab等價(jià)于ba。曾有個(gè)項(xiàng)目operator沒處理nullptr線上服務(wù)偶發(fā)coredump查了三天。這五組實(shí)驗(yàn)的價(jià)值不在代碼本身而在于它強(qiáng)迫你直面C最硬核的命題誰擁有內(nèi)存誰負(fù)責(zé)釋放誰能看到什么誰承諾不改變什么從那以后我每次寫類都會在頭文件頂部用注釋寫明// 資源所有權(quán)p由本類完全管理析構(gòu)時(shí)delete[]// const接口input()非constoutput()和length()為const// 繼承契約public繼承基類public成員對外可見——不是為了炫技是給三個(gè)月后的自己留一份能救命的說明書。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取