題目(一):我們可以用static修飾
個類的成員函數,也可以用const修飾類的成員函數(寫在函數的最後表示不能修改成員變量,不是指寫在前面表示傳回值為常量)。請問:能不能同時用static和const修飾類的成員函數?
分析:答案是不可以。C++編譯器在實作const的成員函數的時候為了確定該函數不能修改類的執行個體的狀态,會在函數中添加一個隐式的參數onst this*。但當一個成員為static的時候,該函數是沒有this指針的。也就是說此時static的用法和static是沖突的。
我們也可以這樣了解:兩者的語意是沖突的。static的作用是表示該函數隻作用在類型的靜态變量上,與類的執行個體沒有關系;而const的作用是確定函數不能修改類的執行個體的狀态,與類型的靜态變量沒有關系。是以不能同時用它們。
題目(二):運作下面的代碼,輸出是什麼?
class A
{
};
class B
public:
B() {}
~B() {}
class C
C() {}
virtual ~C() {}
int _tmain(int argc, _TCHAR* argv[])
printf("%d, %d, %d\n", sizeof(A), sizeof(B), sizeof(C));
return 0;
}
分析:答案是1, 1, 4。class A是一個空類型,它的執行個體不包含任何資訊,本來求sizeof應該是0。但當我們聲明該類型的執行個體的時候,它必須在記憶體中占有一定的空間,否則無法使用這些執行個體。至于占用多少記憶體,由編譯器決定。Visual Studio 2008中每個空類型的執行個體占用一個byte的空間。
class B在class A的基礎上添加了構造函數和析構函數。由于構造函數和析構函數的調用與類型的執行個體無關(調用它們隻需要知道函數位址即可),在它的執行個體中不需要增加任何資訊。是以sizeof(B)和sizeof(A)一樣,在Visual Studio 2008中都是1。
class C在class B的基礎上把析構函數标注為虛拟函數。C++的編譯器一旦發現一個類型中有虛拟函數,就會為該類型生成虛函數表,并在該類型的每一個執行個體中添加一個指向虛函數表的指針。在32位的機器上,一個指針占4個位元組的空間,是以sizeof(C)是4。
題目(三):運作下面中的代碼,得到的結果是什麼?
private:
int m_value;
A(int value)
{
m_value = value;
}
void Print1()
printf("hello world");
void Print2()
printf("%d", m_value);
A* pA = NULL;
pA->Print1();
pA->Print2();
分析:答案是Print1調用正常,列印出hello world,但運作至Print2時,程式崩潰。調用Print1時,并不需要pA的位址,因為Print1的函數位址是固定的。編譯器會給Print1傳入一個this指針,該指針為NULL,但在Print1中該this指針并沒有用到。隻要程式運作時沒有通路不該通路的記憶體就不會出錯,是以運作正常。在運作print2時,需要this指針才能得到m_value的值。由于此時this指針為NULL,是以程式崩潰了。
題目(四):運作下面中的代碼,得到的結果是什麼?
virtual void Print2()
分析:答案是Print1調用正常,列印出hello world,但運作至Print2時,程式崩潰。Print1的調用情況和上面的題目一樣,不在贅述。由于Print2是虛函數。C++調用虛函數的時候,要根據執行個體(即this指針指向的執行個體)中虛函數表指針得到虛函數表,再從虛函數表中找到函數的位址。由于這一步需要通路執行個體的位址(即this指針),而此時this指針為空指針,是以導緻記憶體通路出錯。
題目(五):靜态成員函數能不能同時也是虛函數?
分析:答案是不能。調用靜态成員函數不要執行個體。但調用虛函數需要從一個執行個體中指向虛函數表的指針以得到函數的位址,是以調用虛函數需要一個執行個體。兩者互相沖突。
題目(六):運作下列C++代碼,輸出什麼?
struct Point3D
int x;
int y;
int z;
Point3D* pPoint = NULL;
int offset = (int)(&(pPoint)->z);
printf("%d", offset);
答案:輸出8。由于在pPoint->z的前面加上了取位址符号,運作到此時的時候,會在pPoint的指針位址上加z在類型Point3D中的偏移量8。由于pPoint的位址是0,是以最終offset的值是8。
&(pPoint->z)的語意是求pPoint中變量z的位址(pPoint的位址0加z的偏移量8),并不需要通路pPoint指向的記憶體。隻要不通路非法的記憶體,程式就不會出錯。
題目(七):運作下列C++代碼,輸出什麼?
A()
Print();
virtual void Print()
printf("A is constructed.\n");
class B: public A
B()
printf("B is constructed.\n");
A* pA = new B();
delete pA;
答案:先後列印出兩行:A is constructed. B is constructed. 調用B的構造函數時,先會調用B的基類及A的構造函數。然後在A的構造函數裡調用Print。由于此時執行個體的類型B的部分還沒有構造好,本質上它隻是A的一個執行個體,他的虛函數表指針指向的是類型A的虛函數表。是以此時調用的Print是A::Print,而不是B::Print。接着調用類型B的構造函數,并調用Print。此時已經開始構造B,是以此時調用的Print是B::Print。
同樣是調用虛拟函數Print,我們發現在類型A的構造函數中,調用的是A::Print,在B的構造函數中,調用的是B::Print。是以虛函數在構造函數中,已經失去了虛函數的動态綁定特性。
題目 10
int SizeOf(char pString[])
return sizeof(pString);
char* pString1 = "google";
int size1 = sizeof(pString1);
int size2 = sizeof(*pString1);
char pString2[100] = "google";
int size3 = sizeof(pString2);
int size4 = SizeOf(pString2);
printf("%d, %d, %d, %d", size1, size2, size3, size4);
答案:4, 1, 100, 4。pString1是一個指針。在32位機器上,任意指針都占4個位元組的空間。*pString1是字元串pString1的第一個字元。一個字元占一個位元組。pString2是一個數組,sizeof(pString2)是求數組的大小。這個數組包含100個字元,是以大小是100個位元組。而在函數SizeOf中,雖然傳入的參數是一個字元數組,當數組作為函數的參數進行傳遞時,數組就自動退化為同類型的指針。是以size4也是一個指針的大小,為4.
題目(九):在C++和C#中,struct和class有什麼不同?
答案:在C++中,如果沒有标明函數或者變量是的通路權限級别,在struct中,是public的;而在class中,是private的。
題目(12):運作下圖中的C++代碼,輸出是什麼?
#include <iostream>
int n1;
int n2;
A(): n2(0), n1(n2 + 2)
void Print()
std::cout << "n1: " << n1 << ", n2: " << n2 << std::endl;
A a;
a.Print();
答案:輸出n1是一個随機的數字,n2為0。在C++中,成員變量的初始化順序與變量在類型中的申明順序相同,而與它們在構造函數的初始化清單中的順序無關。是以在這道題中,會首先初始化n1,而初始n1的參數n2還沒有初始化,是一個随機值,是以n1就是一個随機值。初始化n2時,根據參數0對其初始化,故n2=0。
題目(13):編譯運作下圖中的C++代碼,結果是什麼?(A)編譯錯誤;(B)編譯成功,運作時程式崩潰;(C)編譯運作正常,輸出10。請選擇正确答案并分析原因。
int value;
A(int n)
value = n;
A(A other)
value = other.value;
std::cout << value << std::endl;
A a = 10;
A b = a;
b.Print();
答案:編譯錯誤。在複制構造函數中傳入的參數是A的一個執行個體。由于是傳值,把形參拷貝到實參會調用複制構造函數。是以如果允許複制構造函數傳值,那麼會形成永無休止的遞歸并造成棧溢出。是以C++的标準不允許複制構造函數傳值參數,而必須是傳引用或者常量引用。在Visual Studio和GCC中,都将編譯出錯。
題目(15):運作下圖中代碼,輸出的結果是什麼?這段代碼有什麼問題?
std::cout << "A is created." << std::endl;
~A()
std::cout << "A is deleted." << std::endl;
class B : public A
std::cout << "B is created." << std::endl;
~B()
std::cout << "B is deleted." << std::endl;
答案:輸出三行,分别是:A is created. B is created. A is deleted。用new建立B時,回調用B的構造函數。在調用B的構造函數的時候,會先調用A的構造函數。是以先輸出A is created. B is created.
接下來運作delete語句時,會調用析構函數。由于pA被聲明成類型A的指針,同時基類A的析構函數沒有标上virtual,是以隻有A的析構函數被調用到,而不會調用B的析構函數。
由于pA實際上是指向一個B的執行個體的指針,但在析構的時候隻調用了基類A的析構函數,卻沒有調用B的析構函數。這就是一個問題。如果在類型B中建立了一些資源,比如檔案句柄、記憶體等,在這種情況下都得不到釋放,進而導緻資源洩漏。
問題(16):運作如下的C++代碼,輸出是什麼?
virtual void Fun(int number = 10)
{
std::cout << "A::Fun with number " << number;
}
virtual void Fun(int number = 20)
std::cout << "B::Fun with number " << number;
int main()
B b;
A &a = b;
a.Fun();
答案:輸出B::Fun with number 10。由于a是一個指向B執行個體的引用,是以在運作的時候會調用B::Fun。但預設參數是在編譯期決定的。在編譯的時候,編譯器隻知道a是一個類型a的引用,具體指向什麼類型在編譯期是不能确定的,是以會按照A::Fun的聲明把預設參數number設為10。
這一題的關鍵在于了解确定預設參數的值是在編譯的時候,但确定引用、指針的虛函數調用哪個類型的函數是在運作的時候。
問題(17):運作如下的C代碼,輸出是什麼?
char* GetString1()
char p[] = "Hello World";
return p;
char* GetString2()
char *p = "Hello World";
printf("GetString1 returns: %s. \n", GetString1());
printf("GetString2 returns: %s. \n", GetString2());
return 0;
答案:輸出兩行,第一行GetString1 returns: 後面跟的是一串随機的内容,而第二行GetString2 returns: Hello World. 兩個函數的差別在于GetString1中是一個數組,而GetString2中是一個指針。
當運作到GetString1時,p是一個數組,會開辟一塊記憶體,并拷貝"Hello World"初始化該數組。接着傳回數組的首位址并退出該函數。由于p是GetString1内的一個局部變量,當運作到這個函數外面的時候,這個數組的記憶體會被釋放掉。是以在_tmain函數裡再去通路這個數組的内容時,結果是随機的。
當運作到GetString2時,p是一個指針,它指向的是字元串常量區的一個常量字元串。該常量字元串是一個全局的,并不會因為退出函數GetString2而被釋放掉。是以在_tmain中仍然根據GetString2傳回的位址得到字元串"Hello World"。
問題(19):運作下圖中C代碼,輸出的結果是什麼?
char str1[] = "hello world";
char str2[] = "hello world";
char* str3 = "hello world";
char* str4 = "hello world";
if(str1 == str2)
printf("str1 and str2 are same.\n");
else
printf("str1 and str2 are not same.\n");
if(str3 == str4)
printf("str3 and str4 are same.\n");
printf("str3 and str4 are not same.\n");
答案:輸出兩行。第一行是str1 and str2 are not same,第二行是str3 and str4 are same。
str1和str2是兩個字元串數組。我們會為它們配置設定兩個長度為12個位元組的空間,并把"hello world"的内容分别拷貝到數組中去。這是兩個初始位址不同的數組,是以比較str1和str2的值,會不相同。str3和str4是兩個指針,我們無需為它們配置設定記憶體以存儲字元串的内容,而隻需要把它們指向"hello world“在記憶體中的位址就可以了。由于"hello world”是常量字元串,它在記憶體中隻有一個拷貝,是以str3和str4指向的是同一個位址。是以比較str3和str4的值,會是相同的。
問題(23):運作下圖中的C++代碼,列印出的結果是什麼?
bool Fun1(char* str)
printf("%s\n", str);
return false;
bool Fun2(char* str)
return true;
bool res1, res2;
res1 = (Fun1("a") && Fun2("b")) || (Fun1("c") || Fun2("d"));
res2 = (Fun1("a") && Fun2("b")) && (Fun1("c") || Fun2("d"));
return res1 || res2;
答案:列印出4行,分别是a、c、d、a。
在C/C++中,與、或運算是從左到右的順序執行的。在計算rest1時,先計算Fun1(“a”) && Func2(“b”)。首先Func1(“a”)列印出内容為a的一行。由于Fun1(“a”)傳回的是false, 無論Func2(“b”)的傳回值是true還是false,Fun1(“a”) && Func2(“b”)的結果都是false。由于Func2(“b”)的結果無關重要,是以Func2(“b”)會略去而不做計算。接下來計算Fun1(“c”) || Func2(“d”),分别列印出内容c和d的兩行。
在計算rest2時,首先Func1(“a”)列印出内容為a的一行。由于Func1(“a”)傳回false,和前面一樣的道理,Func2(“b”)會略去不做計算。由于Fun1(“a”) && Func2(“b”)的結果是false,不管Fun1(“c”) && Func2(“d”)的結果是什麼,整個表達式得到的結果都是false,是以Fun1(“c”) && Func2(“d”)都将被忽略。
問題(25):運作下面的C++代碼,列印的結果是什麼?
class Base
void print() { doPrint();}
virtual void doPrint() {cout << "Base::doPrint" << endl;}
class Derived : public Base
virtual void doPrint() {cout << "Derived::doPrint" << endl;}
Base b;
b.print();
Derived d;
d.print();
return 0;