C++面試題目彙總

C++ 面試題目彙總

因爲最近開始面試了,雖然C++平時用的還算多,但是很多理論部分的東西總會忘,但那些又是面試的重點,所以在這裏自己總結一下,順便放上自己擼的代碼。

C++基礎

這裏只放上和C++本身有關的試題


#include <> 和 #include ""的區別

#include用來指明引用的頭文件,但是一般只有相對路徑,不會引用完整路徑。

#include <> 表明從預定的缺省路徑下找頭文件,而#include ""表明先從本文件的當前目錄下查找,如果沒有,再按照#include <>預定的缺省路徑下查找。


#ifdef x #define x … #endif 作用

防止頭文件在被多次引用的時候重複定義,這是C和C++的所有編譯器通用的。

但是微軟的編譯器還提供了另外一種方式#pragma once

因爲#ifdef ...這種方法是利用宏定義特性來保證不會被重複引用的,但是#ifdef 這個宏定義可以在文件的任何地方使用,所以編譯器必須將文件讀完才能完成工作。但是#pragma once是單獨的一個宏定義,編譯器只要可以立即標記,所以第二種更快,但無法跨平臺。


extern “C”

extern "C"由兩部分組成。

extern 表示可以被外部調用,"C"告訴編譯器作用域中的代碼要以C語言的方式編譯。

首先明確一個概念,C語言和C++是兩個獨立的語言,只不過C++看起來像C語言而已,他們的標準不一樣,也就導致編譯器編譯的時候是不一樣的。就好比同樣是“元宵”,南方人和北方人所說的不是一個東西,反過來說,可能因爲地域或其他原因,雙方想要表達同一樣東西的時候可能說的東西也不一樣。

同樣的,以void fun();這個函數聲明爲例,同樣的一個函數聲明,用gcc編譯器編譯的時候可能是用_fun來標記,而g++編譯器編譯的時候可能又是用_fun_來標記。導致的問題就是,在同一個工程下編譯時,同一個函數在不同源文件編譯的時候標記不同,到鏈接的時候自然在C++的源文件中無法找到那個聲明瞭但沒有被實現的函數。

下面用兩個例子來說明

先在test.c文件中定義print()函數,然後在main.cpp中聲明這個函數爲外部引用,而已要以C語言的方式編譯!
// test.c
#include <stdio.h>

void print(){
    printf("ok\n");
}

// main.cpp
#include <iostream>
using namespace std;

extern "C" void print();

int main()
{
    print();
    return 0;
}
但通常情況下是直接調用api,所以相當於C語言源文件和C++源文件會共享這個頭文件,我們只需要讓這個頭文件在兩個源文件調用的時候分別以各自的規則編譯就好了。
//test.h
#ifndef TEST_H
#define TEST_H

#ifdef __cplusplus
extern "C" {
#endif // __cplusplus

void print();

#ifdef __cplusplus
}
#endif // __cplusplus


#endif // TEST_H
頭文件裏利用宏定義__cplusplus(注意是兩個下劃線)來區別編譯,這個宏定義是編譯器內置的,還有更多可以自行查詢
//test.c
#include <stdio.h>
#include "test.h"

void print(){
    printf("ok\n");
}
//main.cpp
#include <iostream>
#include "test.h"

using namespace std;

int main()
{
    print();
    return 0;
}

C/C++的各自優缺點

C語言
  • 面向過程,因爲語言自身的原因不會也無法輕鬆的開發過於龐大的工程,所以更注重通過過程利用數據結構等更快的得到結果。
  • 語法相對簡單一點,可以進行底層操作是他的優勢。
C++
  • 面向對象,但同時也可以進行底層操作,但是modern C++建議儘量用對象而不是指針等。因爲面對對象,所以解決問題更貼近生活實際。
  • 模板編程更靈活

語言本身沒有好壞,只是適用場景不同罷了。


const關鍵字

const作用和#define很像,但區別在於const會真實的聲明一個變量,只不過無法直接修改值,但可以通過const_cast去除變量指針的const屬性,然後通過更改指針對應的值來間接更改變量的值。

同時const還可以用在函數聲明中,可以給需要保護的參數加上const修飾符,防止調用時被修改。

雖然const放在只讀數據段,但因爲const是變量,編譯器可以對其進行類型安全檢查,有些IDE還可以對他進行調試。

而宏定義是直接替換,無法進行檢查,很容易出錯,而且不容易定位錯的位置。


static,const,局部,全局變量存放位置

static變量全局變量存放在全局/靜態區域編譯期分配內存,程序結束時釋放。

const變量儲存在只讀數據段,編譯時存在符號表中,第一次使用時分配空間,程序結束時釋放。

局部變量儲存在棧內,作用域結束時被釋放。


數組和指針

編譯期中數組可以利用sizeof等運算符參與計算,有數組的性質。也就是說,數組這個概念是面對編譯器的,不是面對應用程序的。

運行期中數組就是指針的另外一種表現形式而已,只不過靜態數組定長,動態數組不定長等等。


sizeof

sizeof不是一個函數,是一個運算符

sizeof只能用於計算佔用棧中內存的大小。

數組在編譯器過後就只是指針的另外一個形式而已,所以如果是計算外部數組的大小,sizeof無法完成。

sizeof只能計算靜態數組的大小,而且是全部大小,不是非空大小。

sizeof如果是類型名需要加括號,變量名不需要,因爲他是運算符。


空指針,懸垂指針,野指針,智能指針

空指針是指指向地址爲NULL(0)的指針變量

懸垂指針是指指向一個已經不存在的對象的地址的指針

野指針是指因爲沒有初始化等原因指向一處隨機或者無效的地址的指針

智能指針首先是在boost庫中實現的,後來被C++標準引用


malloc/free,new/delete,new[]/delete[]三者區別

malloc/free是c語言的標準庫函數,用戶可以用它進行內存的申請和釋放

new/delete是c++的關鍵字,對於內置數據類型,他們功能是一樣的;但是對於對象,malloc/free無法滿足需求,因爲對象需要自動創建構造函數,銷燬時需要自動調用析構函數,這些只能編譯器來操作。

new/delete和new[]/delete[]一定要配套使用,因爲二者的實現機制不同。


OO的基本概念,三個基本特徵?

基本概念:類,對象,繼承
三個基本特徵:封裝,繼承,多態


C++空類默認成員函數

構造函數,析構函數,複製構造函數,賦值函數


靜態變量可以在不同實例中共享

#include <iostream>

using namespace std;

class A{
public:
    A(){}
    virtual ~A(){}
    // 這裏只是聲明,無法直接定義
    static int i;
};
// 靜態變量需要在外部定義
int A::i = 0;

int main()
{
    A a,b;
    a.i = 0;
    cout << b.i <<endl;
    return 0;
}

類和實例的關係

類和實例的關係相當於課程和物理課的關係,相當於學生和我的關係,相當於父親和我的爸爸的關係,等等等…

這個問題很重要,因爲很多時候我們會忘記。


類內靜態變量只可聲明,不能定義?

上個問題說清楚了類和實例的關係,那麼類中定義的變量都會在創建實例的時候全部都聲明並定義一遍,靜態變量也是,如果允許類內定義靜態變量也就是如果允許在類內存在static int a = 0;這樣的語句,由因爲前面說了,類內靜態變量是可以在不同的實例之間共用的,那麼就會讓這個公共變量每次在創建新的實例的時候賦值爲0。


虛函數

在類的繼承中,是允許子類中存在和父類相同的方法的,這種情況下,如果子類的實例調用這個方法,只會運行子類的方法,而不是父類的;如果子類沒有同名方法的定義,調用的時候會自動調用父類的,有的時候類之間的關係很複雜,需要找半天才能找到對應的方法所在的類,所以虛函數可以解決這個問題。

但有例外,構造函數和析構函數,這兩個方法是創建實例時就會調用的

#include <iostream>

using namespace std;

class A{
public:
    A(){cout << "a1\n";}
    virtual ~A(){cout << "a2\n";}
    static int i;
    int d = 0;
    void fun(){
        cout << "ok" << endl;
    }
};

class B : public A{
public:
    B(){cout << "b1\n";}
    virtual ~B(){
        cout << "b2\n";
    }
};

int A::i = 0;

int main()
{
    B b;
    return 0;
}

父子類調用順序
很清楚的看到,調用順序是,父類構造函數->子類構造函數->子類析構函數->父類析構函數,那麼有一個問題,如果我們不想調用父類的析構函數呢?這時就是虛函數的用處了。

但這裏要說一點,這二者中只有析構函數可以被聲明爲虛函數,原因自不用說,爲什麼構造函數不可以呢?因爲虛函數本身是一種欠缺信息的處理方式,對象被創建時相對較爲複雜,必須要明確的知道他的類型等信息,所以構造函數不可以。

總而言之,虛函數就是把修飾的對象限定在本類及子類中(不向上追溯),可以顯式表明這個函數/變量沒有繼承父類,也可以防止意外調用父類的函數/使用父類的成員變量。


純虛函數

純虛函數只有接口,沒有定義,可以基類聲明接口,子類完善定義。


爲什麼不能濫用虛函數

首先虛函數的功能不是每個函數都適用或者必須的,其次,虛函數是有代價的,每個虛函數都要維護一個虛函數表,因爲使用虛函數的時候會帶來一定的系統開銷。


構造函數可以是內聯函數


什麼是多態?多態有什麼用?

多態是將父類的指針/引用指向子類的對象。
多態是由虛函數機制實現的,多態的作用是接口重用。


重載和覆蓋

子類重新定義基類的虛函數叫做覆蓋。
重載對於編譯器而言,只是取了一個另外的名字而已,所以編譯期就可以確定,但是虛函數是運行期才能確定。


public,protected,private區別

public的變量/函數可以被對象任意訪問和修改
private只能被類內的成員訪問/修改
在沒有繼承的情況下,protected和private一樣
繼承的情況下,派生類對象無法訪問基類中的protected變量/函數,只有派生類可以訪問。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章