【C++學習筆記】C++經典十二道筆試題!你能做出幾道?


1. 運行下面的C++代碼,得到的結果是什么?

#include "stdafx.h"

#include<iostream>

using namespace std;

class A 

{ 

private: 

        int m_value; 

 

public: 

        A(int value) 

        { 

                m_value = value; 

        } 

        void Print1() 

        { 

                printf("hello world"); 

        } 

        void Print2() 

        { 

                printf("%d", m_value); 

        } 

    virtual void Print3() 

        { 

                printf("hello world"); 

        } 

};

int _tmain(int argc, _TCHAR* argv[])

{

A* pA;

pA->Print1();

pA->Print2();

pA->Print3();

return 0;

}

 

答案是:Print1調用正常,打印出hello world,但運行至Print2時,程序崩潰。

       調用Print1時,並不需要pA的地址,因為Print1的函數地址是固定的。編譯器會給Print1傳入一個this指針,該指針為NULL,但在Print1中該this指針並沒有用到。

       只要程序運行時沒有訪問不該訪問的內存就不會出錯,因此運行正常。

       在運行print2時,需要this指針才能得到m_value的值。由於此時this指針為NULL,因此程序崩潰了。

       對於Print3這一虛函數,C++在調用虛函數的時候,要根據實例(即this指針指向的實例)中虛函數表指針得到虛函數表,再從虛函數表中找到函數的地址。由於這一步需要訪問實例的地址(即this指針),而此時this指針為空指針,因此導致內存訪問出錯。

 

2. 運行下面的C++代碼,得到的結果是什么?

#include<iostream>

using namespace std;

class A

{

public: 

        A() 

        { 

                Print(); 

        } 

        virtual void Print() 

        { 

                cout<<"A is constructed."<<endl;

        } 

};

class B: public A 

{ 

public: 

        B() 

        { 

                Print(); 

        } 

        virtual void Print() 

        { 

              cout<<"B is constructed."<<endl;

        } 

}; 

int main() 

{ 

        A* pA = new B(); 

        delete pA; 

        return 0; 

}

 

       先后打印出兩行:A is constructed. B is constructed. 調用B的構造函數時,先會調用B的基類A的構造函數。

       然后在A的構造函數里調用Print。由於此時實例的類型B的部分還沒有構造好,本質上它只是A的一個實例,他的虛函數表指針指向的是類型A的虛函數表。

       因此此時調用的Print是A::Print。接着調用類型B的構造函數,並調用Print。此時已經開始構造B,並且虛函數表的指針已指向類B的虛函數表地址,因此此時調用的Print是B::Print。

 

3. 運行下面的C++代碼,輸出是什么?

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; 

        a.Print(); 

        return 0; 

}

 

       輸出n1是一個隨機的數字,n2為0。

       在C++中,成員變量的初始化順序與變量在類型中的申明順序相同,而與它們在構造函數的初始化列表中的順序無關。 

       因此首先初始化n1,而初始n1的參數n2還沒有初始化,是一個隨機值。初始化n2時,根據參數0對其初始化,故n2=0。

 

4. 編譯運行下面的C++代碼,結果是什么?

(A)編譯錯誤;(B)編譯成功,運行時程序崩潰;(C)編譯運行正常,輸出10。請選擇正確答案並分析原因。

class A{

private: 

        int value; 

public: 

        A(int n) 

        { 

                value = n; 

        } 

        A(A other) 

        { 

                value = other.value; 

        } 

        void Print() 

        { 

                std::cout << value << std::endl; 

        } 

}; 

int _tmain(int argc, _TCHAR* argv[]) 

{ 

        A a = 10; 

        A b = a; 

        b.Print(); 

        return 0; 

}

 

       編譯錯誤。在復制構造函數中傳入的參數是A的一個實例。

       由於是傳值,把形參拷貝到實參會調用復制構造函數。因此如果允許復制構造函數傳值,那么會形成永無休止的遞歸並造成棧溢出。因此C++的標准不允許復制構造函數傳值參數,而必須是傳引用或者常量引用。

       復制構造函數的參數需要改為:const A& other。

 

5. 運行如下的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; 

      a.Fun(); 

return 0;

    } 

 

       輸出 B::Fun with number 10。

       由於a是一個指向B實例的引用,因此在運行的時候會調用B::Fun。但缺省參數是在編譯期決定的。在編譯的時候,編譯器只知道a是一個類型a的引用,具體指向什么類型在編譯期是不能確定的,因此會按照A::Fun的聲明把缺省參數number設為10。

       這一題的關鍵在於理解確定缺省參數的值是在編譯的時候,但確定引用、指針的虛函數調用哪個類型的函數是在運行的時候。

 

6. 運行如下的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. \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而被釋放掉。

 

7. 運行下圖中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.\n"); 

      else 

          printf("str1 and str2 are not same.\n"); 

      if(str3 == str4) 

          printf("str3 and str4 are same.\n"); 

      else 

          printf("str3 and str4 are not same.\n"); 

      return 0; 

    } 

 

       這個題目與上一題目類似。str1和str2是兩個字符串數組。我們會為它們分配兩個長度為12個字節的空間,並把"hello world"的內容分別拷貝到數組中去。

       這是兩個初始地址不同的數組,因此比較str1和str2的值,會不相同。str3和str4是兩個指針,我們無需為它們分配內存以存儲字符串的內容,而只需要把它們指向"hello world“在內存中的地址就可以了。

       由於"hello world”是常量字符串,它在內存中只有一個拷貝,因此str3和str4指向的是同一個地址。因此比較str3和str4的值,會是相同的。

 

8. 運行Test,輸出結果是什么?

void Test()

{ 

    class B 

    { 

    public: 

        B(void) 

        { 

            cout<<"B\t"; 

        } 

        ~B(void) 

        { 

            cout<<"~B\t"; 

        } 

    }; 

    struct C 

    { 

        C(void) 

        { 

            cout<<"C\t"; 

        } 

        ~C(void) 

        { 

            cout<<"~C\t"; 

        } 

    }; 

    struct D : B 

    { 

        D() 

        { 

            cout<<"D\t"; 

        } 

        ~D() 

        { 

            cout<<"~D\t"; 

        } 

    private: 

        C c; 

    }; 

    D d; 

} 

 

       運行結果:B C D ~D ~ C ~B。

       當實例化D對象時,由於繼承自B,因而首先調用B的構造函數,之后初始化私有成員C,完成父類的構造與私有成員的初始化后再進入D的構造函數體內;之后,按照相反順序完成對象的析構操作。

       初始化與賦值是不同的,一般初始化是在初始化列表完成的,構造函數體中進行的是賦值操作。

 

9. 下列程序輸出結果是什么?

class A

{ 

public: 

    int a;//4字節 

    char b;//1字節 

    double c;//8字節,以此為基本單位進行字節對齊,上面的兩個變量對齊后共為8字節,加上當前字節數,共為8+8=16字節。 

    virtual void print()//虛函數,構建虛函數表,虛函數表指針需要4字節,字節對其,擴充為8字節 

    { 

        cout<<"this is father's function!"<<endl; 

    } 

    virtual void print1()//地址存於虛函數表 

    { 

        cout<<"this is father's function1!"<<endl; 

    } 

        virtual void print2()//無需分配內存 

        { 

            cout<<"this is father's function2!"<<endl; 

        } 

private: 

    float d;//4字節,字節對其,擴充為8字節 

}; 

 

class B : A//首先承載A的大小:32字節 

{ 

public: 

    virtual void print()//修改虛函數表地址 

    { 

        cout<<"this is children's function!"<<endl; 

    } 

    void print1()//僅存有函數入口地址,無需分配內存 

    { 

        cout<<"this is children's function1!"<<endl; 

    } 

private: 

    char e;//1字節,字節對齊,擴充為8字節(可以發現,繼承后,字節對齊單位也放生變化) 

}; 

int main(void) 

{ 

    cout<<sizeof(A)<<"  "<<sizeof(B)<<endl; 

    system("pause"); 

    return 0; 

} 

 

       運行結果:32,40.

       這個題目解決的關鍵在於掌握字節對齊的相關知識點。具體見上面注釋。

 

10. 以下程序,在編譯與運行時或發生什么?

class A

{ 

public: 

    virtual void foo()  {  } 

}; 

class B 

{ 

public: 

    virtual void foo()  {  } 

}; 

class C : public A , public B 

{ 

public: 

    virtual void foo()  {  } 

}; 

void bar1(A *pa) 

{ 

    B *pc = dynamic_cast<B*>(pa);//運行期遍歷繼承樹 

} 

void bar2(A *pa) 

{ 

    B *pc = static_cast<B*>(pa);//兩個類無關,編譯出錯 

} 

void bar3() 

{ 

    C c; 

    A *pa = &c; 

    B *pb = static_cast<B*>(static_cast<C*>(pa));//存在繼承關系,編譯正確 

}

 

       對於bar1,dynamic_cast是在運行時遍歷繼承樹,所以,在編譯時不會報錯。

       但是因為A和B無繼承關系,所以運行時報錯。static_cast:編譯器隱式執行的任何類型轉換都可由它顯示完成。

       其中對於:

(1)基本類型。如可以將int轉換為double(編譯器會執行隱式轉換),但是不能將int用它轉換到double(沒有此隱式轉換)。

(2)對於用戶自定義類型,如果兩個類無關,則會出錯,如果存在繼承關系,則可以在基類和派生類之間進行任何轉型,在編譯期間不會出錯。所以bar3可以通過編譯。

 

11. 執行下列程序,會發生什么?

class A

    { 

    public: 

        string a; 

        void f1() 

        { 

            printf("Hello World"); 

        } 

        void f2() 

        { 

            a = "Hello World"; 

            printf("%s",a.c_str()); 

        } 

        virtual void f3() 

        { 

            a = "Hello World"; 

            printf("%s",a.c_str()); 

        } 

        static void f4() 

        { 

            printf("Hello World"); 

        } 

    }; 

    int main(void) 

    { 

        A *aptr = NULL;  //創建一個A對象,對象指針為空,意味着對象僅有空殼,無法借助指針訪問成員變量 

        aptr->f1();      //運行成功,調用f1函數僅需函數入口地址,無需訪問對象中的成員變量 

            aptr->f2();      //運行失敗,調用f2需訪問成員變量 

            aptr->f3();      //運行失敗,同上 

            aptr->f4();      //靜態成員不屬於任何對象,運行成功 

        return 0; 

    } 

此題解答如程序注釋所示。

 

12. 下列函數運行情況如何?

int func()

{ 

    char b[2]={0}; 

    strcpy(b,"aaa"); 

}

 

       Debug版崩潰,Release版正常。因為在Debug中有ASSERT斷言保護,所以要崩潰,而在Release中就會刪掉ASSERT,所以正常運行。但是不推薦這么做,因為這樣會覆蓋不屬於自己的內存。


看到這里是不是又學到了很多新知識呢~

如果你很想學編程,小編推薦我的C語言/C++編程學習基地【點擊進入】!

都是學編程小伙伴們,帶你入個門還是簡簡單單啦,一起學習,一起加油~

還有許多學習資料和視頻,相信你會喜歡的!

涉及:游戲開發、常用軟件開發、編程基礎知識、課程設計、黑客等等......


 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM