題目(一):我們可以用static修飾一個類的成員函式,也可以用const修飾類的成員函式(寫在函式的最後表示不能修改成員變數,不是指寫在前面表示返回值為常量)。請問:能不能同時用static和const修飾類的成員函式?
分析:答案是不可以。C++編譯器在實現const的成員函式的時候為了確保該函式不能修改類的例項的狀態,會在函式中新增一個隱式的引數const this*。但當一個成員為static的時候,該函式是沒有this指標的。也就是說此時static的用法和static是衝突的。
我們也可以這樣理解:兩者的語意是矛盾的。static的作用是表示該函式只作用在型別的靜態變數上,與類的例項沒有關係;而const的作用是確保函式不能修改類的例項的狀態,與型別的靜態變數沒有關係。因此不能同時用它們。
題目(二):執行下面的程式碼,輸出是什麼?
class A
{
};
class B
{
public:
B() {}
~B() {}
};
class C
{
public:
C() {}
virtual ~C() {}
};
int _tmain(int argc, _TCHAR* argv[])
{
printf("%d, %d, %d", 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。
題目(三):執行下面中的程式碼,得到的結果是什麼?
class A
{
private:
int m_value;
public:
A(int value)
{
m_value = value;
}
void Print1()
{
printf("hello world");
}
void Print2()
{
printf("%d", m_value);
}
};
int _tmain(int argc, _TCHAR* argv[])
{
A* pA = NULL;
pA->Print1();
pA->Print2();
return 0;
}
分析:答案是Print1呼叫正常,打印出hello world,但執行至Print2時,程式崩潰。呼叫Print1時,並不需要pA的地址,因為Print1的函式地址是固定的。編譯器會給Print1傳入一個this指標,該指標為NULL,但在Print1中該this指標並沒有用到。只要程式執行時沒有訪問不該訪問的記憶體就不會出錯,因此執行正常。在執行print2時,需要this指標才能得到m_value的值。由於此時this指標為NULL,因此程式崩潰了。
題目(四):執行下面中的程式碼,得到的結果是什麼?
class A
{
private:
int m_value;
public:
A(int value)
{
m_value = value;
}
void Print1()
{
printf("hello world");
}
virtual void Print2()
{
printf("hello world");
}
};
int _tmain(int argc, _TCHAR* argv[])
{
A* pA = NULL;
pA->Print1();
pA->Print2();
return 0;
}
分析:答案是Print1呼叫正常,打印出hello world,但執行至Print2時,程式崩潰。Print1的呼叫情況和上面的題目一樣,不在贅述。由於Print2是虛擬函式。C++呼叫虛擬函式的時候,要根據例項(即this指標指向的例項)中虛擬函式表指標得到虛擬函式表,再從虛擬函式表中找到函式的地址。由於這一步需要訪問例項的地址(即this指標),而此時this指標為空指標,因此導致記憶體訪問出錯。
題目(五):靜態成員函式能不能同時也是虛擬函式?
分析:答案是不能。呼叫靜態成員函式不要例項。但呼叫虛擬函式需要從一個例項中指向虛擬函式表的指標以得到函式的地址,因此呼叫虛擬函式需要一個例項。兩者相互矛盾。
題目(六):執行下列C++程式碼,輸出什麼?
struct Point3D
{
int x;
int y;
int z;
};
int _tmain(int argc, _TCHAR* argv[])
{
Point3D* pPoint = NULL;
int offset = (int)(&(pPoint)->z);
printf("%d", offset);
return 0;
}
答案:輸出8。由於在pPoint->z的前面加上了取地址符號,執行到此時的時候,會在pPoint的指標地址上加z在型別Point3D中的偏移量8。由於pPoint的地址是0,因此最終offset的值是8。
&(pPoint->z)的語意是求pPoint中變數z的地址(pPoint的地址0加z的偏移量8),並不需要訪問pPoint指向的記憶體。只要不訪問非法的記憶體,程式就不會出錯。
題目(七):執行下列C++程式碼,輸出什麼?
class A
{
public:
A()
{
Print();
}
virtual void Print()
{
printf("A is constructed.");
}
};
class B: public A
{
public:
B()
{
Print();
}
virtual void Print()
{
printf("B is constructed.");
}
};
int _tmain(int argc, _TCHAR* argv[])
{
A* pA = new B();
pA;
return 0;
}
答案:先後打印出兩行: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。因此虛擬函式在建構函式中,已經失去了虛擬函式的動態繫結特性。
題目(八):執行下列C#程式碼,輸出是什麼?
namespace ChangesOnString
{
class Program
{
static void Main(string[] args)
{
String str = "hello";
per();
rt(0, " WORLD");
eLine(str);
}
}
}
答案:輸出是hello。由於在中,String有一個非常特殊的性質:String的例項的狀態不能被改變。如果String的成員函式會修改例項的狀態,將會返回一個新的String例項。改動只會出現在返回值中,而不會修改原來的例項。所以本題中輸出仍然是原來的字串值hello。
如果試圖改變String的內容,改變之後的值可以通過返回值拿到。用StringBuilder是更好的選擇,特別是要連續多次修改的時候。如果用String連續多次修改,每一次修改都會產生一個臨時物件,開銷太大。
題目(九):在C++和C#中,struct和class有什麼不同?
答案:在C++中,如果沒有標明函式或者變數是的訪問許可權級別,在struct中,是public的;而在class中,是private的。
在C#中,如果沒有標明函式或者變數的訪問許可權級別,struct和class中都是private的。struct和class的區別是:struct定義值型別,其例項在棧上分配記憶體;class定義引用型別,其例項在堆上分配記憶體。
題目(十):執行下圖中的C#程式碼,輸出是什麼?
namespace StaticConstructor
{
class A
{
public A(string text)
{
eLine(text);
}
}
class B
{
static A a1 = new A("a1");
A a2 = new A("a2");
static B()
{
a1 = new A("a3");
}
public B()
{
a2 = new A("a4");
}
}
class Program
{
static void Main(string[] args)
{
B b = new B();
}
}
}
答案:打印出四行,分別是a1、a3、a2、a4。
在呼叫型別B的程式碼之前先執行B的'靜態建構函式。靜態函式先初始化型別的靜態變數,再執行靜態函式內的語句。因此先列印a1再列印a3。接下來執行B b = new B(),即呼叫B的普通建構函式。建構函式先初始化成員變數,在執行函式體內的語句,因此先後打印出a2、a4。
題目(11):執行下圖中的C#程式碼,輸出是什麼?
namespace StringValueOrReference
{
class Program
{
internal static void ValueOrReference(Type type)
{
String result = "The type " + ;
if (lueType)
eLine(result + " is a value type.");
else
eLine(result + " is a reference type.");
}
internal static void ModifyString(String text)
{
text = "world";
}
static void Main(string[] args)
{
String text = "hello";
ValueOrReference(ype());
ModifyString(text);
eLine(text);
}
}
}
答案:輸出兩行。第一行是The type String is reference type. 第二行是hello。型別String的定義是public sealed class String {...},既然是class,那麼String就是引用型別。
在方法ModifyString裡,對text賦值一個新的字串,此時改變的不是原來text的內容,而是把text指向一個新的字串"world"。由於引數text沒有加ref或者out,出了方法之後,text還是指向原來的字串,因此輸出仍然是"hello".
題目(12):執行下圖中的C++程式碼,輸出是什麼?
#include
class A
{
private:
int n1;
int n2;
public:
A(): n2(0), n1(n2 + 2)
{
}
void Print()
{
std::cout << "n1: " << n1 << ", n2: " << n2 << std::endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
A a;
t();
return 0;
}
答案:輸出n1是一個隨機的數字,n2為0。在C++中,成員變數的初始化順序與變數在型別中的申明順序相同,而與它們在建構函式的初始化列表中的順序無關。因此在這道題中,會首先初始化n1,而初始n1的引數n2還沒有初始化,是一個隨機值,因此n1就是一個隨機值。初始化n2時,根據引數0對其初始化,故n2=0。
題目(13):編譯執行下圖中的C++程式碼,結果是什麼?(A)編譯錯誤;(B)編譯成功,執行時程式崩潰;(C)編譯執行正常,輸出10。請選擇正確答案並分析原因。
#include
class A
{
private:
int value;
public:
A(int n)
{
value = n;
}
A(A other)
{
value = e;
}
void Print()
{
std::cout << value << std::endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
A a = 10;
A b = a;
t();
return 0;
}
答案:編譯錯誤。在複製建構函式中傳入的引數是A的一個例項。由於是傳值,把形參拷貝到實參會呼叫複製建構函式。因此如果允許複製建構函式傳值,那麼會形成永無休止的遞歸併造成棧溢位。因此C++的標準不允許複製建構函式傳值引數,而必須是傳引用或者常量引用。在Visual Studio和GCC中,都將編譯出錯。
題目(14):執行下圖中的C++程式碼,輸出是什麼?
int SizeOf(char pString[])
{
return sizeof(pString);
}
int _tmain(int argc, _TCHAR* argv[])
{
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);
return 0;
}
答案:4, 1, 100, 4。pString1是一個指標。在32位機器上,任意指標都佔4個位元組的空間。*pString1是字串pString1的第一個字元。一個字元佔一個位元組。pString2是一個數組,sizeof(pString2)是求陣列的大小。這個陣列包含100個字元,因此大小是100個位元組。而在函式SizeOf中,雖然傳入的引數是一個字元陣列,當陣列作為函式的引數進行傳遞時,陣列就自動退化為同類型的指標。因此size4也是一個指標的大小,為4.
題目(15):執行下圖中程式碼,輸出的結果是什麼?這段程式碼有什麼問題?
#include
class A
{
public:
A()
{
std::cout << "A is created." << std::endl;
}
~A()
{
std::cout << "A is d." << std::endl;
}
};
class B : public A
{
public:
B()
{
std::cout << "B is created." << std::endl;
}
~B()
{
std::cout << "B is d." << std::endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
A* pA = new B();
pA;
return 0;
}
答案:輸出三行,分別是:A is created. B is created. A is d。用new建立B時,回撥用B的建構函式。在呼叫B的建構函式的時候,會先呼叫A的建構函式。因此先輸出A is created. B is created.
接下來執行語句時,會呼叫解構函式。由於pA被宣告成型別A的指標,同時基類A的解構函式沒有標上virtual,因此只有A的解構函式被呼叫到,而不會呼叫B的解構函式。
由於pA實際上是指向一個B的例項的指標,但在析構的時候只調用了基類A的解構函式,卻沒有呼叫B的解構函式。這就是一個問題。如果在型別B中建立了一些資源,比如檔案控制代碼、記憶體等,在這種情況下都得不到釋放,從而導致資源洩漏。
問題(16):執行如下的C++程式碼,輸出是什麼?
class A
{
public:
virtual void Fun(int number = 10)
{
std::cout << "A::Fun with number " << number;
}
};
class B: public A
{
public:
virtual void Fun(int number = 20)
{
std::cout << "B::Fun with number " << number;
}
};
int main()
{
B b;
A &a = b;
();
}
答案:輸出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";
return p;
}
int _tmain(int argc, _TCHAR* argv[])
{
printf("GetString1 returns: %s.", GetString1());
printf("GetString2 returns: %s.", 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"。
問題(18):執行下圖中C#程式碼,輸出的結果是什麼?
namespace StaticVariableInAppDomain
{
[Serializable]
internal class A : MarshalByRefObject
{
public static int Number;
public void SetNumber(int value)
{
Number = value;
}
}
[Serializable]
internal class B
{
public static int Number;
public void SetNumber(int value)
{
Number = value;
}
}
class Program
{
static void Main(string[] args)
{
String assamblyName = ntryAssembly()Name;
AppDomain domain = teDomain("NewDomain");
er = 10;
String nameOfA = typeof(A)Name;
A a = teInstanceAndUnwrap(assamblyName, nameOfA) as A;
umber(20);
eLine("Number in class A is {0}", er);
er = 10;
String nameOfB = typeof(B)Name;
B b = teInstanceAndUnwrap(assamblyName, nameOfB) as B;
umber(20);
eLine("Number in class B is {0}", er);
}
}
}
答案:輸出兩行,第一行是Number in class A is 10,而第二行是Number in class B is 20。上述C#程式碼先建立一個命名為NewDomain的應用程式域,並在該域中利用反射機制建立型別A的一個例項和型別B的一個例項。我們注意到型別A是繼承自MarshalByRefObject,而B不是。雖然這兩個型別的結構一樣,但由於基類不同而導致在跨越應用程式域的邊界時表現出的行為將大不相同。
由於A繼承MarshalByRefObject,那麼a實際上只是在預設的域中的一個代理,它指向位於NewDomain域中的A的一個例項。當umber時,是在NewDomain域中呼叫該方法,它將修改NewDomain域中靜態變數er的值並設為20。由於靜態變數在每個應用程式域中都有一份獨立的拷貝,修改NewDomain域中的靜態變數er對預設域中的靜態變數omain沒有任何影響。由於eLine是在預設的應用程式域中輸出er,因此輸出仍然是10。
B只從Object繼承而來的型別,它的例項穿越應用程式域的邊界時,將會完整地拷貝例項。在上述程式碼中,我們儘管試圖在NewDomani域中生成B的例項,但會把例項b拷貝到預設的域。此時,呼叫umber也是在預設的域上進行,它將修改預設的域上的er並設為20。因此這一次輸出的是20。
問題(19):執行下圖中C程式碼,輸出的結果是什麼?
int _tmain(int argc, _TCHAR* argv[])
{
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.");
else
printf("str1 and str2 are not same.");
if(str3 == str4)
printf("str3 and str4 are same.");
else
printf("str3 and str4 are not same.");
return 0;
}
答案:輸出兩行。第一行是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的值,會是相同的。
問題(20):執行下圖中C#程式碼,輸出的結果是什麼?並請比較這兩個型別各有什麼特點,有哪些區別。
namespace Singleton
{
public sealed class Singleton1
{
private Singleton1()
{
eLine("Singleton1 constructed");
}
public static void Print()
{
eLine("Singleton1 Print");
}
private static Singleton1 instance = new Singleton1();
public static Singleton1 Instance
{
get
{
return instance;
}
}
}
public sealed class Singleton2
{
Singleton2()
{
eLine("Singleton2 constructed");
}
public static void Print()
{
eLine("Singleton2 Print");
}
public static Singleton2 Instance
{
get
{
return ance;
}
}
class Nested
{
static Nested() { }
internal static readonly Singleton2 instance = new Singleton2();
}
}
class Program
{
static void Main(string[] args)
{
t();
t();
}
}
}
答案: 輸出三行:第一行“Singleton1 constructed”,第二行“Singleton1 Print”,第三行“Singleton2 Print”。
當我們呼叫t時,執行時會自動呼叫Singleton1的靜態建構函式,並初始化它的靜態變數。此時會建立一個Singleton1的例項,因此會呼叫它的建構函式。Singleton2的例項是在Nested的靜態建構函式裡初始化的。只有當型別Nested被使用時,才回觸發執行時呼叫它的靜態建構函式。我們注意到我們只在ance裡面用到了Nested。而在我們的程式碼中,只調用了t。因此不會建立Singleton2的例項,也不會呼叫它的建構函式。
這兩個型別其實都是單例模式(Singleton)的實現。第二個實現Singleton2只在真的需要時,才會建立例項,而第一個實現Singleton1則不然。第二個實現在空間效率上更好。
問題(21):C#是一門託管語言,那麼是不是說明只要用C#,就能保證不會出現記憶體洩露和其他資源洩漏?如果不是,在哪些情況下可能會出現洩漏?
答案:C#不能保證沒有資源洩漏。比如如下幾種情況可能會造成資源洩漏:(1) 呼叫Native code,比如用P/Invoke或者呼叫COM;(2) 讀寫檔案時的,沒有及時close stream, 或者連資料庫時,沒有及時關閉連線,也算資源洩漏?(3)註冊事件後沒有remove,導致publisher和subscriber的強依 賴,垃圾回收可能會被推遲;(4)還定義了一些方法直接申請非託管記憶體,比如cHGlobal和cCoTaskMem。通過這種方式得到的記憶體,如果沒有及時釋放,也會造成記憶體洩露。
問題(22):下面的兩段C#有哪些不同?
static void CatchException1()
{
try
{
Function();
}
catch
{
throw;
}
}
static void CatchException2()
{
try
{
Function();
}
catch (Exception e)
{
throw e;
}
}
答案:兩個函式的catch都是重新丟擲截獲的exception,但丟擲的exception的call stack是不一樣的。對於第一種方法,exception的call stack是從最開始的丟擲地點開始的。對於第二種方法,exception的call stack是從CatchException2開始的,最初丟擲的地方相關的資訊被隱藏了。
問題(23):執行下圖中的C++程式碼,打印出的結果是什麼?
bool Fun1(char* str)
{
printf("%s", str);
return false;
}
bool Fun2(char* str)
{
printf("%s", str);
return true;
}
int _tmain(int argc, _TCHAR* argv[])
{
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”)都將被忽略。
問題(24):執行下面的C#程式碼,打印出來的結果是什麼?
struct Person
{
public string Name;
public override string ToString()
{
return Name;
}
}
class Program
{
static void Main(string[] args)
{
ArrayList array = new ArrayList();
Person jim = new Person() {Name = "Jim"};
(jim);
Person first = (Person)array[0];
= "Peter";
eLine(array[0]ring());
}
}
答案:Person的定義是一個struct,因此是一個值型別。在執行到語句Person first = (Person)array[0]的時候,first是array[0]的一個拷貝,first和array[0]不是一個例項。因此修改first對array[0]沒有影響。
問題(25):執行下面的C++程式碼,列印的結果是什麼?
class Base
{
public:
void print() { doPrint();}
private:
virtual void doPrint() {cout << "Base::doPrint" << endl;}
};
class Derived : public Base
{
private:
virtual void doPrint() {cout << "Derived::doPrint" << endl;}
};
int _tmain(int argc, _TCHAR* argv[])
{
Base b;
t();
Derived d;
t();
return 0;
}
答案:輸出兩行,分別是Base::doPrint和Derived::doPrint。在print中呼叫doPrint時,doPrint()的寫法和this->doPrint()是等價的,因此將根據實際的型別呼叫對應的doPrint。所以結果是分別呼叫的是Base::doPrint和Derived::doPrint2。如果感興趣,可以檢視一下彙編程式碼,就能看出來呼叫doPrint是從虛擬函式表中得到函式地址的。