[學習筆記] C++ 歷年題程式題解析
發現程式題也挺有價值的。
順便記錄下來幾道。
1.題目
#include <iostream> #include <cstring> using namespace ① std ; void Swap(char * const str1, char * const str2) { // 形參為鎖定目標的指標(即指標常量,亦即指標的指向不能改變,內容可更改) int n = strlen(② str1 ) + 1; char *temp = new char[③ n ]; ④ strcpy(temp, str1) ; ⑤ strcpy(str1, str2) ; ⑥ strcpy(str2, temp) ;delete [] temp; } void Exchange(const char *&str1, const char *&str2) { // 形參不使用二級指標,而使用指標的常引用(常量指標的別名) ⑦ const char *temp; ⑧ temp = str1 ; ⑨ str1 = str2 ; ⑩ str2 = temp ; } int main() { char str1[80] = "Tom", str2[80] = "Jerry"; const char *p1 = "Tom", *p2 = "Jerry"; cout << str1 << '\t' << str2 << endl; cout << p1 << '\t' << p2 << endl; Swap(str1, str2); Exchange(p1, p2); cout << str1 << '\t' << str2 << endl; cout<< p1 << '\t' << p2 << endl; return 0; }
解析:
這題應該是要搞清楚const char *p; 和 char * const p;
const char *p可以這麼理解,const後面是char*,所以char*對應的內容就不能改變;而char* const p的const後面是p,所以p的值不能改變,p的值是一個指標地址。綜上,緊接著const後面的內容就是不可改變的內容。這下應該可以做題了。
swap函式的const後跟的是指標,所以指標地址不能改變,而其指向地址對應的值是可以改變的,所以,我們只能想辦法改變str1和str2指向的值,由於可以呼叫strcpy進行賦值,中間變數自然就是對應的char * const temp,這裡圖中沒用const也可以,但我感覺要對應,還是加上const 最好。當然,我已經改過之後跑過了。我的理解是對的。
exchange函式的const後跟的是char*,這就意味著字串的值是不能改變的,那麼我們就只能交換兩個別名對應的地址了,注意不要搞混,別名的繫結是不能改變的,我們只能改變別名對應的地址。臨時變數也應該對應 const char* temp;這樣就對了。
2.題目
#include <iostream> using namespace std; class ctest { public: ctest(int x=0) : a(x) { cout << "構造物件(" << a << ")\n"; } ~ctest(){ cout << "析構物件(" << a << ")\n";} ctest & Add() // 本成員函式為引用返回 { ++a; return *this; } ctest add() // 本成員函式為值返回 { ctest temp(*this); ++a; return temp; } friend ostream & operator<<(ostream &out, const ctest &c) { out << c.a; return out; } private: int a; }; int main() { ctest a(100), b(200); cout << a << ", " << b << endl; // 第3行輸出 a.Add().Add(); b.add().add(); // 拷貝構造臨時無名物件時無輸出 cout << a << ", " << b << endl; // 第6行輸出 a.~ctest(); // 主動呼叫解構函式,並不銷燬物件 b.~ctest(); // 主動呼叫解構函式,並不銷燬物件 cout << a << ", " << b << endl; // 物件a,b仍可被訪問。第9行輸出 cout << "返回作業系統。" << endl; // 第10行輸出 return 0; } /* 構造物件(100) 構造物件(200) 100, 200 析構物件(200) 析構物件(201) 102, 201 析構物件(102) 析構物件(201) 102, 201 返回作業系統。 析構物件(201) 析構物件(102) */
解析:
這個題考輸出,要注意的是無名物件的析構問題,也就是第四和第五行的輸出,我們來逐行分析程式碼,首先建立a和b物件,分別初始化為100和200,這時候會呼叫兩者的建構函式,因此輸出兩行建構函式該輸出的東西。然後輸出a和b的值,“100,200”,再往下執行,呼叫a.Add()函式,是引用返回,但是返回是其本身,期間沒有建立中間變數,也就是說a.Add執行完了之後返回的結果還是a,然後後面就再執行一個a.Add,這時候返回的還是a,但是a物件的成員值自增了兩次,已經變成102了,期間沒有物件銷燬,因此沒有析構,沒有輸出。再往下執行,呼叫b.add函式,根據函式定義我們知道,函式執行過程中先拷貝構造一個temp物件,但是沒有寫拷貝建構函式,因此不輸出任何內容,且預設是淺拷貝,然後b的成員值自增變為201,但這時候temp的成員值還是200,因為拷貝過去之後會重新分配空間,所以temp的成員值和b的成員值不是一個地址,然後返回temp,且是值返回,值返回之後會建立一個無名變數,然後這個無名變數再呼叫add函式,導致的是temp物件的成員值+1=201,而這時候就已經和物件b無關了。但是要注意的是,由於b呼叫的add是值返回,b.add().add()結束之後呢,兩個無名變數就沒用了,就會被銷燬(之前講過,無名變數在語句結束之後被銷燬),所以析構兩個無名物件,無名物件的析構順序又和正常物件的析構順序剛好相反(這個我專門查了資料),所以是依此析構,因此因此輸出的是200和201。再往下走,輸出a和b的成員值,a是102,b是201,然後a主動析構,輸出“析構物件102”,然後b主動析構,輸出“析構物件201”,注意,這裡是主動析構,解構函式裡面除了輸出什麼也沒有,只是相當於呼叫了一個輸出函式。下一步輸出a、b的成員值,還是102和201,然後輸出返回作業系統。下一步結束main函式,這時候才是a、b真正的析構時間,b先析構,然後是a。就很簡單了。
3.題目
#include <iostream> using namespace std; class Base // 基類 { public: Base(int x=0) : a(x) { cout << "構造基類物件(" << a << ")\n"; } Base(const Base &b) : a(b.a) { cout << "拷貝構造基類的物件(" << a << ")\n"; } virtual ~Base() { cout << "析構基類物件(" << a << ")\n"; } protected: int a; static int num; // 靜態資料成員 }; int Base::num = 0; // 靜態資料成員定義及初始化 class Derived : public Base // 派生類 { public: Derived(int x=0, int y=0) : Base(x), b(y) { // 建構函式中輸出了物件個數[num],在園括號之前 cout << "構造派生類的物件[" << ++num << "](" << a << ", " << b << ")\n"; } Derived(const Derived &d) : Base(d), b(d.b) { // 拷貝建構函式中輸出了物件個數[num],在園括號之前 cout << "拷貝構造派生類的物件[" << ++num << "](" << a << ", " << b << ")\n"; } ~Derived() // 解構函式中輸出了物件個數[num],在園括號之後 { cout << "析構派生類的物件(" << a << ", " << b << ")[" << --num << "]\n"; } void Set(int x, int y) { a = x; b = y; } friend ostream &operator<<(ostream &out, const Derived &d) { out << '(' << d.a << ", " << d.b << ')'; return out; } private: int b; }; void f(Derived &r, const Derived *p, Derived x) { // 形參分別為引用傳遞、指標傳遞、值傳遞 cout << "in f function..." << endl; cout << r << endl; r.Set(50, 80); // 引用傳遞的物件,值被重置 cout << *p << endl; // 雖然從指標p看其目標為常物件,但是…… cout << x << endl; x.Set(0, 0); cout << x << endl; } int main() { Derived d(100, 200); cout << "Calling f function..." << endl; f(d, &d, d); // 請注意:用同一個物件或物件的地址值做實參 cout << "return to Operating System." << endl; return 0; } /* 構造基類物件(100) 構造派生類的物件[1](100, 200) Calling f function... 拷貝構造基類的物件(100) 拷貝構造派生類的物件[2](100, 200) in f function... (100, 200) (50, 80) (100, 200) (0, 0) 析構派生類的物件(0, 0)[1] 析構基類物件(0) return to Operating System. 析構派生類的物件(50, 80)[0] 析構基類物件(50) */
解析:
還是至上而下分析,先初始化一個派生類物件d,賦值100,200,這時候先執行基類的建構函式,輸出“構造基類物件100”,然後執行派生類建構函式,輸出“構造派生類物件[1](100,200)”,然後進入函式,輸出第三行,進入函式的過程中,第一個引數是引用傳遞,不構建物件,第二個引數是值傳遞的地址,也沒有構造物件,因此這兩者都不會呼叫建構函式,且兩者指向的是同一物件;而第三個引數是值傳遞,傳遞的是物件,因此會產生實參是臨時物件,會為實參構建物件,實參呼叫建構函式,分別輸出第五行和第六行。接下來輸出第七行,然後輸出r的值,(100,200),然後r呼叫set函式重新賦值,輸出p成員的值,剛才說過,r和p指向同一物件,雖然通過p不能改變其成員的值,但是通過r可以改,因此輸出set之後的值(50,80)。接下來輸出x,x是個臨時物件,其值是構造時候的(100,200),輸出。然後x呼叫其set函式,將臨時物件的成員值都設為 了0。輸出x,當然是兩個0了。出函式,臨時變數該析構了。按照析構的順序先析構派生類,再析構基類。輸出兩行析構。接著往下走,cout一行英文,出main函式,該析構物件d了,輸出兩行析構,結束。
三套試卷我全看完了,只有去年的題出的用心了,出題老師顯然很用心。之前的都是送分的,比較簡單,就不解析了。。