1. 程式人生 > >C++面試常溫問題(一)

C++面試常溫問題(一)

重點部落格記得看 常用的技術基礎主要包括:程式語言相關;資料結構,作業系統,計算機網路,計算機組成原理;資料庫相關;linux命令列相關;以及常用的工具等等。

程式語言常用有關於C++,python,golang,JAVA等,主要會根據幾種語言的特性來問。

1. C++和別的語言相比有什麼特點?

C++語言和C語言相比

2. 什麼是指標?

我自己對指標的理解,指標相當於一個索引,不是通過變數的地址直接定址,而是把變數地址存到指標ipoint中,通過ipoint進行索引在找到i的地址。 總結指標就是“變數記憶體地址下存放的是另一個變數的地址”的變數。 因此&i這種也是一種指標。

int i =
5; int* ipoint = &i; //指標ipoint為指向i的指標,即ipoint變數地址下儲存的是i的地址 cout << *ipoint << endl; //輸出i的值 cout << ipoint << endl; //輸出i的地址 cout << &ipoint << endl; //輸出ipoint的地址
其中指標ipoint為指向i的指標,即ipoint變數地址下儲存的是i的地址;
*ipoint 輸出的是i的值;
ipoint 輸出的是i的地址;
&ipoint 輸出的是ipoint的地址。

下圖1:整型變數i和指標變數ipoint的儲存 整型變數i和指標變數ipoint的儲存

2.1 符號 & 和 *

符號&就是取一個變數的地址,就是取一個記憶體地址下的變數內容。 但是的操作物件只能是指標(指標包括&i型別的指標)

int i = 5;
int* ipoint = *i;   //==語法錯誤==,雖然int*和int*型別匹配,但是*的操作物件只能是指標;
cout << *(&i) <<endl;   //輸出i的值,先取i的地址再根據i的地址取值。
int* ipoint = *i;   ===語法錯誤===,雖然int*和int*型別匹配,但是*的操作物件
只能是指標;
*(&i) 沒有語法錯誤,先取i的地址再根據i的地址取值,輸出的是i的值。

2.2 是複製還是指向問題

int* temp; 是宣告一個指標變數temp,相當於只是對指標變數temp分配了空間,但是temp是一個空指標,因為沒有對指向內容*temp分配空間。

  • 複製問題:要求temp和*temp都要有記憶體空間。 以下程式碼是複製問題,直接更改指標指向記憶體下的內容,。 但是因為沒有對 *temp分配空間,所以會編譯報錯。但是dev上面可以執行,因為dev會在複製時臨時給一個隨機地址存值,但是這種隨機的空間沒辦法回收,會造成記憶體洩漏
void swap2(int* a,int* b)
{
	int* temp;
	*temp = *a;
}
  • 指向問題:只要求temp本身有空間,對*temp沒有要求。 以下程式碼是指向問題,直接改的是指標指向的地址。
void swap3(int* a,int* b)
{
	int* temp;
	temp = a;
	a = b;
	b = temp;
}

2.3 字元指標char*

字元指標是一個稍微特殊一點點的指標,平常看起來會直接拿來當成字串用,但是本質上還是指標。

  1. 賦值:可以直接用字串的方式進行賦值,直接賦一個字元陣列;
char* str = "hello";
  1. 輸出:
  • 從字元指標的本質看,str地址下儲存的是字元陣列的首地址即‘h’的地址,但是直接cout輸出看,會直接打印出整個字元陣列char[] 的內容:
char* str = "hello";
cout << str << endl;   //輸出結果:hello
cout << str[0] << endl;   //輸出結果:h
cout << *str << endl;   //輸出結果:h
cout << &str << endl;   //輸出結果:0031F99C
str 輸出結果:hello;
str[0] 輸出結果:h;
*str 輸出結果:h;
&str 輸出結果:0031F99C。

下圖2:字元指標變數str和指向字元h以及連續字元的儲存 圖2:字元指標變數str和指向字元h以及連續字元的儲存

  • 但是為什麼cout << str 會直接輸出整個字元陣列char[]的內容,而不只是指向的陣列首地址? 原因: << 對字串的過載,對str對整個字元陣列char[]輸出。詳情見下:C++指標困惑,為什麼char *p cout 直接輸出了整個字元陣列而不是輸出首個地址
  • 現在已知cout << str輸出不是指向字元‘h’的地址,如果非要輸出指標指向字元變數的地址:使用強制型別轉換的方法把str轉換成一般指標,輸出時就不會再因為 << 對字元指標的過載輸出hello了。
char* str = "hello";
cout << &str << endl;    //輸出為str變數的地址,即圖2中的地址1
cout << (void*) str << endl;   //型別強制轉化,輸出為指標指向變數的地址,即地址2
&str 輸出為str變數的地址,即圖2中的地址1;
(void*) str 是型別強制轉化,輸出為指標指向變數的地址,即地址2。
  1. 由char* str = "hello"賦值導致的**程式崩潰**問題:
char* str = "hello";
strcpy(str, "olleh");
cout << str << endl;
  • 以上程式碼平平無奇,但是卻會導致程式崩潰。原因是因為由 = 對char* 變數賦值時,系統在常量區給“hello”開了空間,指標str指向字元‘hello’,即str指向的是常量。即當前指向地址下的內容不可以更改,但是可以直接更改指標指向的地址本身。
  • 修改指向地址下的內容:導致崩潰
char* str = "abcd";   // ==程式崩潰==
str[0] = 'p';
char* str = "abcd";  會導致 ===程式崩潰===
  • 修改指標指向的地址本身:指向一個新的物件變數
char* str = "abcd";
str = "dbca";   //相當於直接改了指標變數下儲存的變數地址,將“dbca”換成十六進位制
cout << str << endl;
char* str = "abcd" 先給str分配指向物件,
str = "dbca" 更改指標變數下儲存的變數地址,將“dbca”換成十六進位制相當於存的地址。
  • 需要修改指向物件的值:不用char*而用char[]
char str[6] = "hello";
strcpy(str, "olleh");
cout << str << endl;   //一段正常的程式,輸出為olleh

2.4 指向陣列的指標

  1. 指向一維陣列:兩種方式
  • 直接a 進行賦值,a本身就代表陣列a的首地址;
int a[10];
int* apoint;
apoint = a;
  • 表達的更清楚一點,取首元素a[0]的地址賦值;
int a[10];
int* apoint;
apoint = &(a[0]);
  1. 指向二維陣列:*apoint才表示指向也是指向二維陣列的指標,apoint表示指向(*apoint)的指標。
  • 定義方法1:不用先宣告二維陣列,直接宣告指標
int (*apoint)[3][6];
cout << sizeof(*apoint) << endl;
  • 定義方法2:先宣告一個二維陣列,然後宣告指向該陣列的指標
int a[3][6];
int** apoint = NULL;
*apoint = &(a[0][0]);   //*apoint已經表示指向二維陣列的指標,儲存的是二維陣列首元素的地址;
(*apoint)表示apoint是指向二維陣列int [3][6]陣列的指標,並不是取內容,大小為72位元組;
(**apoint)表示指向一維陣列int [6]的指標,大小為sizeof(int) * 6 = 32位元組;
(***apoint)表示指向int的指標,大小為sizeof(int) = 4位元組;
  • 指標初始化1:和一維陣列 apoint = a 初始化不一樣,取apoint = &a
int a[3][6];
int (*apoint)[3][6];
(apoint) = &a;
  • 指標初始化2:取*apoint = &(a[0][0])
int a[3][6];
int (*apoint)[3][6];
(*apoint) = &a[0][0];
  • 指標初始化3:取(*apoint) = a這樣會有編譯錯誤 在這裡插入圖片描述
  • 一個複雜的例子:
double* (*a)[3][6];
cout << sizeof(a) << endl;   //a只是指標,指向指標(double*) [3][6],即a記憶體地址下存的是地址,大小為4位元組;
cout << sizeof(*a) << endl;   //*a就是(double*)[3][6],大小為sizeof(double*) * 3 * 6 = 72位元組;
cout << sizeof(**a) << endl;   //**a就是(double*)[6],大小為sizeof(double*) * 6 = 24位元組;
cout << sizeof(***a) << endl;   //***a就是(double*),大小為sizeof(double*) = 4位元組;
cout << sizeof(****a) << endl;   //****a就是double,大小為sizeof(double) = 8位元組;
sizeof(a) 大小為4位元組,因為a是指標,記憶體下存的就是一個地址;
sizeof(*a) = sizeof(double*) * 3 * 6 = 72位元組,因為*a記憶體下就是double(*)[3][6];
sizeof(**a)  = sizeof(double*) * 6 = 24位元組,因為**a記憶體下就是(double*)[6];
sizeof(***a) = sizeof(double*) = 4位元組,因為***a記憶體下就是(double*);
sizeof(****a) = sizeof(double) = 8位元組,因為****a記憶體下就是一個double;

3. 指標*和引用&的區別?

3.1 NULL問題:指標可以為空指標,但是引用不可以為空引用

  1. 從定義上講:引用相當於一個變數的別名,定義時一定要有初始化,並不能宣告一個不指向任何物件的引用。
  2. 指標可以為空,它是值指向某個變數地址的變數,空指標即不指向任何物件。

3.2 改變指向物件問題:指標可以改變指向的物件,引用不可以

  1. “至死不渝”的引用: 不可以改變指向的物件,不能從變數a的別名改成變數b的別名;
int i = 13;
int j = 1313;
int& iref = i;
iref = j;
cout << i << " " << iref << endl;   //輸出結果為:1313 1313
以上程式碼編譯沒有問題,但是並沒有改變iref的指向,iref = j相當於直接使用引
用的儲存地址進行賦值,由於引用是共享地址,所以相當於直接對i進行了賦值。
  1. “花心大蘿蔔”的指標:可以改變指向的物件,只要改變記憶體下儲存的地址就指向新的物件,和之前指向的物件不再有關聯。
int i = 13;
int j = 1313;
int* ipoint = &i;
ipoint = &j;
cout << i << " " << *ipoint << endl;   //輸出結果為:13 1313
以上程式碼ipoint本來指向物件變數i,後來改了指向物件變數b。

3.3 記憶體問題:指標變數有自己的空間,而引用和指向變數共享空間

  1. 引用只是別名,和指向變數本體共享空間;
  2. 指標有自己的空間,和指向的物件型別一致,但是本質上沒有直接的關聯;

3.4 使用時引用比指標更安全

  1. 指標可以隨意切換指向物件;
  2. 指標可以不被初始化;
  3. 指標使用時要檢驗是否為NULL;
  4. const指標雖然不能改變指向,但是仍然可能有NULL問題;
  5. 可能有野指標的問題:空指標是指指標目前為空閒,沒有指向任何物件;而野指標是指一個指標指向了一塊不可使用的記憶體空間,產生原因主要有三個:
  • 指標沒有進行初始化:任何指標變數初始化時不會自動設定為NULL指標,它的設定是隨機的,可能指向一塊不可使用的記憶體空間,變成野指標;
  • delete或者free的時候,只是釋放了指標指向的記憶體空間釋放掉,如果沒有將指標置為NULL,該指標變成野指標。另外如果有多個指標指向同一塊記憶體區域,當一個指標delete或者free,其他指標都將變成野指標;
  • 當指標操作超出了指向記憶體空間的作用範圍,此時指標越界也會變成一個野指標;

4. 類的問題

4.1 public, protected, private

  1. 屬性: private: 只能由該類中的函式訪問、其友元函式訪問,不能被任何其他訪問,該類的物件也不能訪問; protected: 可以被該類中的函式、子類的函式、以及其友元函式訪問,但不能被該類的物件訪問; public: 可以被該類中的函式、子類的函式、其友元函式訪問,也可以由該類的物件訪問; 注:友元函式包括兩種:設為友元的全域性函式,設為友元類中的成員函式
  2. 繼承方式: public繼承:不改變基類成員的訪問許可權; private繼承:使得基類所有成員在子類中的訪問許可權變為private protected繼承:將基類中public成員變為子類的protected成員,其它成員的訪問 許可權不變。

4.2 淺拷貝和深拷貝

簡單說【淺拷貝】是增加了一個指標,指向原來已經存在的記憶體。而【深拷貝】新開闢了一塊空間。

  1. 淺拷貝: 只是增加了一個指標,指向原來存在物件的記憶體,共享記憶體。缺點為: 當一個物件值有改變,另一個物件的值隨之改變; 當其中一個物件釋放了記憶體,另一個物件指標將變成野指標; 當類中的兩個物件指向同一個記憶體空間,一個物件的解構函式釋放空間後,另一個物件再次執行解構函式會出現錯誤;
  2. 深拷貝: 開闢了一塊新的記憶體地址用於存放複製的物件,只拷貝內容。

4.3 類中淺拷貝和深拷貝的實現—拷貝建構函式

當沒有自定義拷貝建構函式時,編譯器會自動寫一個拷貝建構函式,實現方式為淺拷貝

class String
{
private:
    char* pstr;
public:
#if !is_deep_copy   
//淺拷貝方式實現的拷貝建構函式,不會為新物件中的屬性分配空間,
//只是把物件內的屬性指向同一塊記憶體。
    String(const String& s):pstr(s.pstr)
    {}
#endif
    
#if is_deep_copy
//深拷貝方式實現的拷貝建構函式,先重新分配空間,再複製內容。
    String(String& s):pstr(new char[strlen(s.pstr)+1])
    {
        strcpy(pstr,s.pstr);
    }
#endif
}

4.4 拷貝建構函式和=運算子過載問題

class String
{
private:
    char* pstr;
    
public:
    String(const char *pStr = " ")
    {
        if(pStr == NULL)
        {
            pstr = new char[1];
            *pstr = '\0';
        }
        else{
            pstr = new char[strlen(pStr) + 1];
            strcpy(pstr, pStr);
        }
    }
    
#if !is_deep_copy
    String(const String& s):pstr(s.pstr)
    {}    
    String& operator=(const String& s)
    {
        if(this != &s)
        {
            delete[] pstr;
            strcpy(pstr,s.pstr);
        }
        return *this;
    }
#endif
    
#if is_deep_copy
    String(String& s):pstr(new char[strlen(s.pstr)+1])
    {
        strcpy(pstr,s.pstr);
    }
    String& operator=(const String& s)
    {
        if(this != &s)
        {
            char* temp = new char[strlen(s.pstr) + 1];
            delete[] pstr;
            strcpy(temp,s.pstr);
            pstr = temp;
        }
        return *this;
    }
#endif   
    ~String()
    {
        if(pstr != NULL)
        {
            delete[] pstr;
            pstr = NULL;
        }
    }
};
int main()
{
    String s1("hello");   //呼叫建構函式
    String s2 = s1;        //呼叫拷貝建構函式
    String s3("nice to meet you");   //呼叫建構函式
    s3 = s2;     //呼叫=運算子過載函式
    return 0;
}
其中String s1("hello");   呼叫建構函式
String s2 = s1;           看起來是呼叫了運算子=,但其實是呼叫拷貝建構函式。
s3 = s2;                  呼叫=運算子過載函式

物件進行賦值時,呼叫=運算子過載函式或者是拷貝建構函式? 主要看 是否創造了新物件,產生新物件即為拷貝建構函式,未產生新物件即為=運算子過載函式, 對於s2是在用s1賦值時產生的新物件,因此呼叫的是拷貝建構函式; 對於s3是已經建立好的物件,再利用s2對s3進行賦值時並未產生新物件,因此此處呼叫的=運算子過載函式。

5. sizeof()的問題?

5.1 sizeof是關鍵字而不是函式

首先得知道sizeof是c語言中的一個關鍵字而不是函式,在編譯階段就已經確定。

int a = 1;
int b = sizeof(++a);
cout << a << endl;
由於sizeof不是一個函式,關鍵字在編譯階段就已經確定,所以括號中不會被執行,
因此輸出為結果:1。

5.2 一般變數的sizeof大小

在這裡插入圖片描述

變數型別 sizeof()
char 1位元組
short 2位元組
int 4位元組
unsigned int 4位元組
unsigned = unsigned int 4位元組
float 4位元組
double 8位元組
__int64 8位元組

5.3 class和struct的sizeof

- 空class和struct:編譯器仍留1位元組的空間

struct A{};
class B{};
int main()
{
	cout << sizeof(A) << endl;
	cout << sizeof(B) << endl;
}
空的結構體A和類B,sizeof(A)和sizeof(B)都為1位元組。

- 非空class和struct:編譯器不會多給1位元組

struct A{ int a; };
class B{};
int main()
{
	cout << sizeof(A) << endl;
	cout << sizeof(B) << endl;
}
對於空的結構體和類,仍然會分配一個1位元組的空間,但是對於非空的類和結構體,
不會單獨多加1位元組的空間,因此sizeof(A)和sizeof(B)分別為4位元組和1位元組;

- class和struct整體空間一定是佔用空間最大的元素的整數倍,否則要有資料對齊

對於class和struct,對於一般的變數元素,直接以元素的sizeof進行補齊; 但是對於陣列型別的元素,不是看成一個整體,而是以元素為單位進行補齊。

class s1
{
    char a[8];
};
struct s2
{
    double d;
};
class s3
{
    s1 s;
    char ch;
};
struct s4
{
    s2 s;
    char ch;
};

int main()
 {
    cout <<sizeof(s1) << endl;
    cout <<sizeof(s2) << endl;
    cout <<sizeof(s3) << endl;
    cout <<sizeof(s4) << endl;
}
sizeof(s1)輸出結果為8位元組,sizeof(s2)輸出結果為8位元組;
sizeof(s3)輸出結果為9位元組,sizeof(s4)輸出結果為16位元組;
對於s4來說,s2大小為8位元組,需要進行對齊,因此兩個元素對齊8位元組為16位元組;
但是對於s3來說,s1雖然為8位元組,但是對於陣列是每個元素單獨存放,以元素大小
為單位進行對齊,因此s3中相當於9個char元素,不需要特別補齊。

- class中static成員變數不算入物件記憶體空間,const算入

class B
{
	int a;
	const int b;
	static int c;
};
int main()
{
	cout << sizeof(B) << endl;
}
sizeof(B)大小為8位元組,其中類中的static成員變數是屬於類域而不是屬於物件的,
因此static的成員變數不算入class B的記憶體大小;但是const成員變數算入class B的
記憶體大小;

- class一般成員函式不算入物件記憶體空間,如果有virtual成員函式,物件中會包含一個指向虛擬函式表的指標

class B
{
	int fun(int b){ return b; };
	virtual void func(){ int x = 0; };
};
int main()
{
	cout << sizeof(B) << endl;
}
sizeof(B)大小為4位元組,其中一般的函式fun()沒有計入物件的空間大小中,但是對於
virtual函式func(),物件包含一個指標指向虛擬函式表,因此指標大小為4位元組。

5.4 與strlen()函式的比較

- strlen()計算陣列的長度,長度不包含結束符

  1. strlen()是用來計算陣列長度的函式,用’\0’ 作為陣列結束符作為判斷;
  2. 統計單位為個,即 統計陣列中有多少個元素;
  3. 最後統計的長度 不會包含’\0’結束符;

- sizeof()統計資料所用記憶體空間,包含結束符佔用空間

  1. sizeof()是用來統計資料所佔記憶體空間的大小;
  2. 用位元組數表示佔用記憶體空間;
  3. 最後統計的空間 會包含結束符’\0’在內;

5.5 指標和靜態陣列的sizeof()問題

- 指標記憶體大小是指向物件地址大小,32位系統就是4位元組,64位就是8位元組

對於指標來說,不管指向的物件是一般變數或者類或者結構體,不管指向物件的記憶體大小,都是指物件的地址。因此 32位系統的指標大小全部為4位元組,64系統的指標大小全部為8位元組。

- 靜態陣列作為形參使用時,陣列名稱當做指標使用

void f(int p[])
{
	cout << sizeof(p) << endl;
}
int main()
{
	int p[5];
	f(p);
}
當系統為32位時f(p)輸出的結果為4位元組,64系統時輸出的結果為8位元組,因為陣列p
作為函式形參使用時,被當做一個指標指向原陣列,因此輸出p的大小為指標大小。

- 對於字元陣列,sizeof()多計算末尾結束符’\0’的大小

char ch[] = "hello";
cout << sizeof(ch) << endl;
sizeof(ch)輸出結果為6位元組,因為hello5個字元加一個'\0'位元組,用6位元組。

- 指向二維陣列的指標的複雜問題

double* (*a)[3][6];
cout << sizeof(a) << endl;   //a只是指標,指向指標(double*) [3][6],即a記憶體地址下存的是地址,大小為4位元組;
cout << sizeof(*a) << endl;   //*a就是(double*)[3][6],大小為sizeof(double*) * 3 * 6 = 72位元組;
cout << sizeof(**a) << endl;   //**a就是(double*)[6],大小為sizeof(double*) * 6 = 24位元組;
cout << sizeof(***a) << endl;   //***a就是(double*),大小為sizeof(double*) = 4位元組;
cout << sizeof(****a) << endl;   //****a就是double,大小為sizeof(double) = 8位元組;
sizeof(a) 大小為4位元組,因為a是指標,記憶體下存的就是一個地址;
sizeof(*a) = sizeof(double*) * 3 * 6 = 72位元組,因為*a記憶體下就是double(*)[3][6];
sizeof(**a)  = sizeof(double*) * 6 = 24位元組,因為**a記憶體下就是(double*)[6];
sizeof(***a) = sizeof(double*) = 4位元組,因為***a記憶體下就是(double*);
sizeof(****a) = sizeof(double) = 8位元組,因為****a記憶體下就是一個double;