C++面試常溫問題(一)
程式語言常用有關於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的儲存
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*
字元指標是一個稍微特殊一點點的指標,平常看起來會直接拿來當成字串用,但是本質上還是指標。
- 賦值:可以直接用字串的方式進行賦值,直接賦一個字元陣列;
char* str = "hello";
- 輸出:
- 從字元指標的本質看,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以及連續字元的儲存
- 但是為什麼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。
- 由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 指向陣列的指標
- 指向一維陣列:兩種方式
- 直接a 進行賦值,a本身就代表陣列a的首地址;
int a[10];
int* apoint;
apoint = a;
- 表達的更清楚一點,取首元素a[0]的地址賦值;
int a[10];
int* apoint;
apoint = &(a[0]);
- 指向二維陣列:*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問題:指標可以為空指標,但是引用不可以為空引用
- 從定義上講:引用相當於一個變數的別名,定義時一定要有初始化,並不能宣告一個不指向任何物件的引用。
- 指標可以為空,它是值指向某個變數地址的變數,空指標即不指向任何物件。
3.2 改變指向物件問題:指標可以改變指向的物件,引用不可以
- “至死不渝”的引用: 不可以改變指向的物件,不能從變數a的別名改成變數b的別名;
int i = 13;
int j = 1313;
int& iref = i;
iref = j;
cout << i << " " << iref << endl; //輸出結果為:1313 1313
以上程式碼編譯沒有問題,但是並沒有改變iref的指向,iref = j相當於直接使用引
用的儲存地址進行賦值,由於引用是共享地址,所以相當於直接對i進行了賦值。
- “花心大蘿蔔”的指標:可以改變指向的物件,只要改變記憶體下儲存的地址就指向新的物件,和之前指向的物件不再有關聯。
int i = 13;
int j = 1313;
int* ipoint = &i;
ipoint = &j;
cout << i << " " << *ipoint << endl; //輸出結果為:13 1313
以上程式碼ipoint本來指向物件變數i,後來改了指向物件變數b。
3.3 記憶體問題:指標變數有自己的空間,而引用和指向變數共享空間
- 引用只是別名,和指向變數本體共享空間;
- 指標有自己的空間,和指向的物件型別一致,但是本質上沒有直接的關聯;
3.4 使用時引用比指標更安全
- 指標可以隨意切換指向物件;
- 指標可以不被初始化;
- 指標使用時要檢驗是否為NULL;
- const指標雖然不能改變指向,但是仍然可能有NULL問題;
- 可能有野指標的問題:空指標是指指標目前為空閒,沒有指向任何物件;而野指標是指一個指標指向了一塊不可使用的記憶體空間,產生原因主要有三個:
- 指標沒有進行初始化:任何指標變數初始化時不會自動設定為NULL指標,它的設定是隨機的,可能指向一塊不可使用的記憶體空間,變成野指標;
- delete或者free的時候,只是釋放了指標指向的記憶體空間釋放掉,如果沒有將指標置為NULL,該指標變成野指標。另外如果有多個指標指向同一塊記憶體區域,當一個指標delete或者free,其他指標都將變成野指標;
- 當指標操作超出了指向記憶體空間的作用範圍,此時指標越界也會變成一個野指標;
4. 類的問題
4.1 public, protected, private
- 屬性: private: 只能由該類中的函式訪問、其友元函式訪問,不能被任何其他訪問,該類的物件也不能訪問; protected: 可以被該類中的函式、子類的函式、以及其友元函式訪問,但不能被該類的物件訪問; public: 可以被該類中的函式、子類的函式、其友元函式訪問,也可以由該類的物件訪問; 注:友元函式包括兩種:設為友元的全域性函式,設為友元類中的成員函式
- 繼承方式: public繼承:不改變基類成員的訪問許可權; private繼承:使得基類所有成員在子類中的訪問許可權變為private protected繼承:將基類中public成員變為子類的protected成員,其它成員的訪問 許可權不變。
4.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()計算陣列的長度,長度不包含結束符
- strlen()是用來計算陣列長度的函式,用’\0’ 作為陣列結束符作為判斷;
- 統計單位為個,即 統計陣列中有多少個元素;
- 最後統計的長度 不會包含’\0’結束符;
- sizeof()統計資料所用記憶體空間,包含結束符佔用空間
- sizeof()是用來統計資料所佔記憶體空間的大小;
- 用位元組數表示佔用記憶體空間;
- 最後統計的空間 會包含結束符’\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;