1. 程式人生 > >[C/C++][面試]面試題目匯總

[C/C++][面試]面試題目匯總

類指針 ram 虛基類 顯式 拷貝構造 題目 IV eve 存儲

之前面試被問到C++裏static的作用是什麽,但我卻只知道static在java裏的作用是什麽,於是就google了一下c++相關面試題,發現這個同學總結的很棒。

就記錄一下。

原文地址:C/C++面試知識總結

侵刪

--------------------------正文分割線------------------------------------

const

//
class A
{
private:
    const int a;                // 常對象成員,只能在初始化列表賦值

public:
    // 構造函數
    A() { };
    A(int x) : a(x) { };        //
初始化列表 // const可用於對重載函數的區分 int getValue(); // 普通成員函數 int getValue() const; // 常成員函數,不得修改類中的任何數據成員的值 }; void function() { // 對象 A b; // 普通對象,可以調用全部成員函數 const A a; // 常對象,只能調用常成員函數、更新常成員變量 const A *p = &a; // 常指針 const
A &q = a; // 常引用 // 指針 char greeting[] = "Hello"; char* p1 = greeting; // 指針變量,指向字符數組變量 const char* p2 = greeting; // 指針變量,指向字符數組常量 char* const p3 = greeting; // 常指針,指向字符數組變量 const char* const p4 = greeting; // 常指針,指向字符數組常量 } // 函數 void
function1(const int Var); // 傳遞過來的參數在函數內不可變 void function2(const char* Var); // 參數指針所指內容為常量 void function3(char* const Var); // 參數指針為常指針 void function4(const int& Var); // 引用參數在函數內為常量 // 函數返回值 const int function5(); // 返回一個常數 const int* function6(); // 返回一個指向常量的指針變量,使用:const int *p = function6(); int* const function7(); // 返回一個指向變量的常指針,使用:int* const p = function7();

作用

  1. 修飾變量,說明該變量不可以被改變;
  2. 修飾指針,分為指向常量的指針和指針常量;
  3. 常量引用,經常用於形參類型,即避免了拷貝,又避免了函數對值的修改;
  4. 修飾成員函數,說明該成員函數內不能修改成員變量。

Volatile

volatile int i = 10; 
  • volatile 關鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器未知的因素(操作系統、硬件、其它線程等)更改。
  • volatile 關鍵字聲明的變量,每次訪問時都必須從內存中取出值(沒有被 volatile 修飾的變量,可能由於編譯器的優化,從 CPU 寄存器中取值)
  • const 可以是 volatile (如只讀的狀態寄存器)
  • 指針可以是 volatile

static

作用

  1. 修飾普通變量,修改變量的存儲區域和生命周期,使變量存儲在靜態區,在 main 函數運行前就分配了空間,如果有初始值就用初始值初始化它,如果沒有初始值系統用默認值初始化它。
  2. 修飾普通函數,表明函數的作用範圍,僅在定義該函數的文件內才能使用。在多人開發項目時,為了防止與他人命令函數重名,可以將函數定位為 static。
  3. 修飾成員變量,修飾成員變量使所有的對象只保存一個該變量,而且不需要生成對象就可以訪問該成員。
  4. 修飾成員函數,修飾成員函數使得不需要生成對象就可以訪問該函數,但是在 static 函數內不能訪問非靜態成員。

this 指針

  1. this 指針是一個隱含於每一個成員函數中的特殊指針。它指向正在被該成員函數操作的那個對象。
  2. 當對一個對象調用成員函數時,編譯程序先將對象的地址賦給 this 指針,然後調用成員函數,每次成員函數存取數據成員時,由隱含使用 this 指針。
  3. 當一個成員函數被調用時,自動向它傳遞一個隱含的參數,該參數是一個指向這個成員函數所在的對象的指針。
  4. this 指針被隱含地聲明為: ClassName *const this,這意味著不能給 this 指針賦值;在 ClassName 類的 const成員函數中,this 指針的類型為:const ClassName* const,這說明不能對 this 指針所指向的這種對象是不可修改的(即不能對這種對象的數據成員進行賦值操作);
  5. this 並不是一個常規變量,而是個右值,所以不能取得 this 的地址(不能 &this)。
  6. 在以下場景中,經常需要顯式引用 this 指針:
    1. 為實現對象的鏈式引用;
    2. 為避免對同一對象進行賦值操作;
    3. 在實現一些數據結構時,如 list

inline 內聯函數

特征

  • 相當於把內聯函數裏面的內容寫在調用內聯函數處;
  • 相當於不用執行進入函數的步驟,直接執行函數體;
  • 相當於宏,卻比宏多了類型檢查,真正具有函數特性;
  • 不能包含循環、遞歸、switch 等復雜操作;
  • 類中除了虛函數的其他函數都會自動隱式地當成內聯函數。

使用

// 聲明1(加 inline,建議使用)
inline int functionName(int first, int secend,...);

// 聲明2(不加 inline)
int functionName(int first, int secend,...);

// 定義
inline int functionName(int first, int secend,...) {/****/};

編譯器對inline函數的處理步驟

  1. 將 inline 函數體復制到 inline 函數調用點處;
  2. 為所用 inline 函數中的局部變量分配內存空間;
  3. 將 inline 函數的的輸入參數和返回值映射到調用方法的局部變量空間中;
  4. 如果 inline 函數有多個返回點,將其轉變為 inline 函數代碼塊末尾的分支(使用 GOTO)。

優缺點

優點

  1. 內聯函數同宏函數一樣將在被調用處進行代碼展開,省去了參數壓棧、棧幀開辟與回收,結果返回等,從而提高程序運行速度。
  2. 內聯函數相比宏函數來說,在代碼展開時,會做安全檢查或自動類型轉換(同普通函數),而宏定義則不會。
  3. 在類中聲明同時定義的成員函數,自動轉化為內聯函數,因此內聯函數可以訪問類的成員變量,宏定義則不能。
  4. 內聯函數在運行時可調試,而宏定義不可以。

缺點

  1. 代碼膨脹。內聯是以代碼膨脹(復制)為代價,消除函數調用帶來的開銷。如果執行函數體內代碼的時間,相比於函數調用的開銷較大,那麽效率的收獲會很少。另一方面,每一處內聯函數的調用都要復制代碼,將使程序的總代碼量增大,消耗更多的內存空間。
  2. inline 函數無法隨著函數庫升級而升級。inline函數的改變需要重新編譯,不像 non-inline 可以直接鏈接。
  3. 是否內聯,程序員不可控。內聯函數只是對編譯器的建議,是否對函數內聯,決定權在於編譯器。

虛函數(virtual)可以是內聯函數(inline)嗎?

Are "inline virtual" member functions ever actually "inlined"?

  • 虛函數可以是內聯函數,內聯是可以修飾虛函數的,但是當虛函數表現多態性的時候不能內聯。
  • 內聯是在編譯器建議編譯器內聯,而虛函數的多態性在運行期,編譯器無法知道運行期調用哪個代碼,因此虛函數表現為多態性時(運行期)不可以內聯。
  • inline virtual 唯一可以內聯的時候是:編譯器知道所調用的對象是哪個類(如 Base::who()),這只有在編譯器具有實際對象而不是對象的指針或引用時才會發生。
#include <iostream>  
using namespace std;
class Base
{
public:
    inline virtual void who()
    {
        cout << "I am Base\n";
    }
    virtual ~Base() {}
};
class Derived : public Base
{
public:
    inline void who()  // 不寫inline時隱式內聯
    {
        cout << "I am Derived\n";
    }
};

int main()
{
    // 此處的虛函數 who(),是通過類(Base)的具體對象(b)來調用的,編譯期間就能確定了,所以它可以是內聯的,但最終是否內聯取決於編譯器。 
    Base b;
    b.who();

    // 此處的虛函數是通過指針調用的,呈現多態性,需要在運行時期間才能確定,所以不能為內聯。  
    Base *ptr = new Derived();
    ptr->who();

    // 因為Base有虛析構函數(virtual ~Base() {}),所以 delete 時,會先調用派生類(Derived)析構函數,再調用基類(Base)析構函數,防止內存泄漏。
    delete ptr;
    ptr = nullptr;

    system("pause");
    return 0;
} 

assert()

斷言,是宏,而非函數。assert 宏的原型定義在<assert.h>(C)、<cassert>(C++)中,其作用是如果它的條件返回錯誤,則終止程序執行。

assert( p != NULL );

sizeof()

  • sizeof 對數組,得到整個數組所占空間大小。
  • sizeof 對指針,得到指針本身所占空間大小。

#pragma pack(n)

設定結構體、聯合以及類成員變量以 n 字節方式對齊

#pragma pack(push)  // 保存對齊狀態
#pragma pack(4)     // 設定為 4 字節對齊

struct test
{
    char m1;
    double m4;
    int m3;
};

#pragma pack(pop)   // 恢復對齊狀態

extern "C"

  • 被 extern 限定的函數或變量是 extern 類型的
  • extern "C" 修飾的變量和函數是按照 C 語言方式編譯和連接的

extern "C" 的作用是讓 C++ 編譯器將 extern "C" 聲明的代碼當作 C 語言代碼處理,可以避免 C++ 因符號修飾導致代碼不能和C語言庫中的符號進行鏈接的問題。

#ifdef __cplusplus
extern "C" {
#endif

void *memset(void *, int, size_t);

#ifdef __cplusplus
}
#endif

struct 和 typedef struct

C 中

// c
typedef struct Student {
    int age; 
} S;

等價於

// c
struct Student { 
    int age; 
};

typedef struct Student S;

此時 S 等價於 struct Student,但兩個標識符名稱空間不相同。

另外還可以定義與 struct Student 不沖突的 void Student() {}

C++ 中

由於編譯器定位符號的規則(搜索規則)改變,導致不同於C語言。

一、如果在類標識符空間定義了 struct Student {...};,使用 Student me; 時,編譯器將搜索全局標識符表,Student未找到,則在類標識符內搜索。

即表現為可以使用 Student 也可以使用 struct Student,如下:

// cpp
struct Student { 
    int age; 
};

void f( Student me );       // 正確,"struct" 關鍵字可省略

二、若定義了與 Student 同名函數之後,則 Student 只代表函數,不代表結構體,如下:

typedef struct Student { 
    int age; 
} S;

void Student() {}           // 正確,定義後 "Student" 只代表此函數

//void S() {}               // 錯誤,符號 "S" 已經被定義為一個 "struct Student" 的別名

int main() {
    Student(); 
    struct Student me;      // 或者 "S me";
    return 0;
}

C++ 中 struct 和 class

總的來說,struct 更適合看成是一個數據結構的實現體,class 更適合看成是一個對象的實現體。

區別

  • 最本質的一個區別就是默認的訪問控制
    1. 默認的繼承訪問權限。struct 是 public 的,class 是 private 的。
    2. struct 作為數據結構的實現體,它默認的數據訪問控制是 public 的,而 class 作為對象的實現體,它默認的成員變量訪問控制是 private 的。

C 實現 C++ 類

C 語言實現封裝、繼承和多態

explicit(顯式)構造函數

explicit 修飾的構造函數可用來防止隱式轉換

如下

class Test1
{
public:
    Test1(int n)            // 普通構造函數
    {
        num=n;
    }
private:
    int num;
};

class Test2
{
public:
    explicit Test2(int n)   // explicit(顯式)構造函數
    {
        num=n;
    }
private:
    int num;
};

int main()
{
    Test1 t1=12;            // 隱式調用其構造函數,成功
    Test2 t2=12;            // 編譯錯誤,不能隱式調用其構造函數
    Test2 t2(12);           // 顯式調用成功
    return 0;
}

friend 友元類和友元函數

  • 能訪問私有成員
  • 破壞封裝性
  • 友元關系不可傳遞
  • 友元關系的單向性
  • 友元聲明的形式及數量不受限制

using 引入命名空間成員

using namespace_name::name

盡量不要使用 using namespace std; 汙染命名空間

一般說來,使用 using 命令比使用 using 編譯命令更安全,這是由於它只導入了制定的名稱。如果該名稱與局部名稱發生沖突,編譯器將發出指示。using編譯命令導入所有的名稱,包括可能並不需要的名稱。如果與局部名稱發生沖突,則局部名稱將覆蓋名稱空間版本,而編譯器並不會發出警告。另外,名稱空間的開放性意味著名稱空間的名稱可能分散在多個地方,這使得難以準確知道添加了哪些名稱。

盡量不要使用

using namespace std;

應該使用

int x;
std::cin >> x ;
std::cout << x << std::endl;

或者

using std::cin;
using std::cout;
using std::endl;
int x;
cin >> x;
cout << x << endl;

:: 範圍解析運算符

:: 可以加在類型名稱(類、類成員、成員函數、變量等)前,表示作用域為全局命名空間

int count = 0;      // global count

int main() {
  int count = 0;    // local count
  ::count = 1;      // set global count to 1
  count = 2;        // set local count to 2
  return 0;
}

  • 宏定義可以實現類似於函數的功能,但是它終歸不是函數,而宏定義中括弧中的“參數”也不是真的參數,在宏展開的時候對 “參數” 進行的是一對一的替換。

初始化列表

好處

  • 更高效:少了一次調用默認構造函數的過程。
  • 有些場合必須要用初始化列表:
    1. 常量成員,因為常量只能初始化不能賦值,所以必須放在初始化列表裏面
    2. 引用類型,引用必須在定義的時候初始化,並且不能重新賦值,所以也要寫在初始化列表裏面
    3. 沒有默認構造函數的類類型,因為使用初始化列表可以不必調用默認構造函數來初始化,而是直接調用拷貝構造函數初始化。

面向對象

面向對象程序設計(Object-oriented programming,OOP)是種具有對象概念的程序編程典範,同時也是一種程序開發的抽象方針。

技術分享圖片

面向對象三大特征 —— 封裝、繼承、多態

封裝

  • 把客觀事物封裝成抽象的類,並且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。
  • 關鍵字:public, protected, friendly, private。不寫默認為 friendly。
關鍵字當前類包內子孫類包外
public
protected ×
friendly × ×
private × × ×

繼承

  • 基類(父類)——> 派生類(子類)

多態

  • 多態,即多種狀態,在面向對象語言中,接口的多種不同的實現方式即為多態。
  • C++ 多態有兩種:靜態多態(早綁定)、動態多態(晚綁定)。靜態多態是通過函數重載實現的;動態多態是通過虛函數實現的。
  • 多態是以封裝和繼承為基礎的。

靜態多態(早綁定)

函數重載

class A
{
public:
    void do(int a);
    void do(int a, int b);
};

動態多態(晚綁定)

  • 虛函數:用 virtual 修飾成員函數,使其成為虛函數

註意:

  • 普通函數(非類成員函數)不能是虛函數
  • 靜態函數(static)不能是虛函數
  • 構造函數不能是虛函數(因為在調用構造函數時,虛表指針並沒有在對象的內存空間中,必須要構造函數調用完成後才會形成虛表指針)
  • 內聯函數不能是表現多態性時的虛函數,解釋見:虛函數(virtual)可以是內聯函數(inline)嗎?
class Shape                     // 形狀類
{
public:
    virtual double calcArea()
    {
        ...
    }
    virtual ~Shape();
};
class Circle : public Shape     // 圓形類
{
public:
    virtual double calcArea();
    ...
};
class Rect : public Shape       // 矩形類
{
public:
    virtual double calcArea();
    ...
};
int main()
{
    Shape * shape1 = new Circle(4.0);
    Shape * shape2 = new Rect(5.0, 6.0);
    shape1->calcArea();         // 調用圓形類裏面的方法
    shape2->calcArea();         // 調用矩形類裏面的方法
    delete shape1;
    shape1 = nullptr;
    delete shape2;
    shape2 = nullptr;
    return 0;
}
  • 虛析構函數
class Shape
{
public:
    Shape();                    // 構造函數不能是虛函數
    virtual double calcArea();
    virtual ~Shape();           // 虛析構函數
};
class Circle : public Shape     // 圓形類
{
public:
    virtual double calcArea();
    ...
};
int main()
{
    Shape * shape1 = new Circle(4.0);
    shape1->calcArea();    
    delete shape1;  // 因為Shape有虛析構函數,所以delete釋放內存時,先調用子類析構函數,再調用基類析構函數,防止內存泄漏。
    shape1 = NULL;
    return 0;
}

抽象類、接口類、聚合類

  • 抽象類:含有純虛函數的類
  • 接口類:僅含有純虛函數的抽象類
  • 聚合類:用戶可以直接訪問其成員,並且具有特殊的初始化語法形式。滿足如下特點:
    • 所有成員都是 public
    • 沒有有定於任何構造函數
    • 沒有類內初始化
    • 沒有基類,也沒有 virtual 函數
    • 如:
// 定義
struct Date 
{
    int ival;
    string s;
}
// 初始化
Data vall = { 0, "Anna" };

虛函數、純虛函數

CSDN . C++ 中的虛函數、純虛函數區別和聯系

  • 類裏如果聲明了虛函數,這個函數是實現的,哪怕是空實現,它的作用就是為了能讓這個函數在它的子類裏面可以被覆蓋,這樣的話,這樣編譯器就可以使用後期綁定來達到多態了。純虛函數只是一個接口,是個函數的聲明而已,它要留到子類裏去實現。
  • 虛函數在子類裏面也可以不重載的;但純虛函數必須在子類去實現。
  • 虛函數的類用於 “實作繼承”,繼承接口的同時也繼承了父類的實現。當然大家也可以完成自己的實現。純虛函數關註的是接口的統一性,實現由子類完成。
  • 帶純虛函數的類叫虛基類,這種基類不能直接生成對象,而只有被繼承,並重寫其虛函數後,才能使用。這樣的類也叫抽象類。抽象類和大家口頭常說的虛基類還是有區別的,在 C# 中用 abstract 定義抽象類,而在 C++ 中有抽象類的概念,但是沒有這個關鍵字。抽象類被繼承後,子類可以繼續是抽象類,也可以是普通類,而虛基類,是含有純虛函數的類,它如果被繼承,那麽子類就必須實現虛基類裏面的所有純虛函數,其子類不能是抽象類。

虛函數指針、虛函數表

  • 虛函數指針:在含有虛函數類的對象中,指向虛函數表,在運行時確定。
  • 虛函數表:在程序只讀數據段(.rodata section,見:目標文件存儲結構),存放虛函數指針,如果派生類實現了基類的某個虛函數,則在虛表中覆蓋原本基類的那個虛函數指針,在編譯時根據類的聲明創建。

虛繼承、虛函數

虛繼承

虛繼承用於解決多繼承條件下的菱形繼承問題(浪費存儲空間、存在二義性)。

底層實現原理與編譯器相關,一般通過虛基類指針和虛基類表實現,每個虛繼承的子類都有一個虛基類指針(占用一個指針的存儲空間,4字節)和虛基類表(不占用類對象的存儲空間)(需要強調的是,虛基類依舊會在子類裏面存在拷貝,只是僅僅最多存在一份而已,並不是不在子類裏面了);當虛繼承的子類被當做父類繼承時,虛基類指針也會被繼承。

實際上,vbptr 指的是虛基類表指針(virtual base table pointer),該指針指向了一個虛基類表(virtual table),虛表中記錄了虛基類與本類的偏移地址;通過偏移地址,這樣就找到了虛基類成員,而虛繼承也不用像普通多繼承那樣維持著公共基類(虛基類)的兩份同樣的拷貝,節省了存儲空間。

虛繼承與虛函數

  • 相同之處:都利用了虛指針(均占用類的存儲空間)和虛表(均不占用類的存儲空間)
  • 不同之處:
    • 虛繼承
      • 虛基類依舊存在繼承類中,只占用存儲空間
      • 虛基類表存儲的是虛基類相對直接繼承類的偏移
    • 虛函數
      • 虛函數不占用存儲空間
      • 虛函數表存儲的是虛函數地址

內存分配和管理

malloc、calloc、realloc、alloca

  1. malloc:申請指定字節數的內存。申請到的內存中的初始值不確定。
  2. calloc:為指定長度的對象,分配能容納其指定個數的內存。申請到的內存的每一位(bit)都初始化為 0。
  3. realloc:更改以前分配的內存長度(增加或減少)。當增加長度時,可能需將以前分配區的內容移到另一個足夠大的區域,而新增區域內的初始值則不確定。
  4. alloca:在棧上申請內存。程序在出棧的時候,會自動釋放內存。但是需要註意的是,alloca 不具可移植性, 而且在沒有傳統堆棧的機器上很難實現。alloca 不宜使用在必須廣泛移植的程序中。C99 中支持變長數組 (VLA),可以用來替代 alloca。

malloc、free

申請內存,確認是否申請成功

char *str = (char*) malloc(100);
assert(str != nullptr);

釋放內存後指針置空

free(p); 
p = nullptr;

new、delete

  1. new/new[]:完成兩件事,先底層調用 malloc 分了配內存,然後調用構造函數(創建對象)。
  2. delete/delete[]:也完成兩件事,先調用析構函數(清理資源),然後底層調用 free 釋放空間。
  3. new 在申請內存時會自動計算所需字節數,而 malloc 則需我們自己輸入申請內存空間的字節數。
int main()
{
    T* t = new T();     // 先內存分配 ,再構造函數
    delete t;           // 先析構函數,再內存釋放
    return 0;
}

delete this 合法嗎?

Is it legal (and moral) for a member function to say delete this?

合法,但:

  1. 必須保證 this 對象是通過 new(不是 new[]、不是 placement new、不是棧上、不是全局、不是其他對象成員)分配的
  2. 必須保證調用 delete this 的成員函數是最後一個調用 this 的成員函數
  3. 必須保證成員函數的 delete this 後面沒有調用 this 了
  4. 必須保證 delete this 後沒有人使用了
--------------------------------------------------------------- 還沒有結束……這位同學真的很厲害。。。

[C/C++][面試]面試題目匯總