1.3.1.簡述一下什么是面向對象
回答:
1. 面向對象是一種編程思想,把一切東西看成是一個個對象,比如人、耳機、鼠標、水杯等,他們各 自都有屬性,比如:耳機是白色的,鼠標是黑色的,水杯是圓柱形的等等,把這些對象擁有的屬性 變量和操作這些屬性變量的函數打包成一個類來表示
2. 面向過程和面向對象的區別 面向過程:根據業務邏輯從上到下寫代碼 面向對象:將數據與函數綁定到一起,進行封裝,這樣能夠更快速的開發程序,減少了重復代碼的 重寫過程
1.3.2.簡述一下面向對象的三大特征
回答:面向對象的三大特征是封裝、繼承、多態。
1. 封裝:將數據和操作數據的方法進行有機結合,隱藏對象的屬性和實現細節,僅對外公開接口來和 對象進行 交互。封裝本質上是一種管理:我們如何管理兵馬俑呢?比如如果什么都不管,兵馬俑 就被隨意破壞了。那么我們首先建了一座房子把兵馬俑給封裝起來。但是我們目的全封裝起來,不 讓別人看。所以我們開放了售票通 道,可以買票突破封裝在合理的監管機制下進去參觀。類也是 一樣,不想給別人看到的,我們使用protected/private把成員封裝起來。開放一些共有的成員函 數對成員合理的訪問。所以封裝本質是一種管理。
2. 繼承:可以使用現有類的所有功能,并在無需重新編寫原來的類的情況下對這些功能進行擴展。 三種繼承方式
3. 多態:用父類型別的指針指向其子類的實例,然后通過父類的指針調用實際子類的成員函數。實現 多態,有二種方式,重寫,重載。
1.3.3.簡述一下 C++ 的重載和重寫,以及它們的區別
回答:
1. 重寫
是指派生類中存在重新定義的函數。其函數名,參數列表,返回值類型,所有都必須同基類中被重 寫的函數一致。只有函數體不同(花括號內),派生類對象調用時會調用派生類的重寫函數,不會 調用被重寫函數。重寫的基類中被重寫的函數必須有virtual修飾。
#include <bits/stdc++.h>using namespace std;class A
{
public:virtual void fun(){cout << "A";}
};class B : public A
{
public:virtual void fun(){cout << "B";}
};int main(void)
{A* a = new B();a->fun(); // 輸出 B,A 類中的 fun 在 B 類中重寫
}
2. 重載 我們在平時寫代碼中會用到幾個函數但是他們的實現功能相同,但是有些細節卻不同。例如:交換 兩個數的值其中包括(int, float,char,double)這些個類型。在C語言中我們是利用不同的函數名來 加以區分。這樣的代碼不美觀而且給程序猿也帶來了很多的不便。于是在C++中人們提出了用一個 函數名定義多個函數,也就是所謂的函數重載。函數重載是指同一可訪問區內被聲明的幾個具有不 同參數列(參數的類型,個數,順序不同)的同名函數,根據參數列表確定調用哪個函數,重載不 關心函數返回類型。
#include <bits/stdc++.h>using namespace std;class A
{
public:void fun() {}void fun(int i) {}void fun(int i, int j) {}void fun1(int i, int j) {}
};
1.3.4.說說 C++ 的重載和重寫是如何實現的
回答:
1. C++利用命名傾軋(name mangling)技術,來改名函數名,區分參數不同的同名函數。命名傾 軋是在編譯階段完成的。
C++定義同名重載函數:
#include <iostream>
using namespace std;int func(int a, double b)
{return (a + b);
}int func(double a, float b)
{return (a + b);
}int func(float a, int b)
{return (a + b);
}int main()
{return 0;
}
由上圖可得,d代表double,f代表float,i代表int,加上參數首字母以區分同名函數。
2. 在基類的函數前加上virtual關鍵字,在派生類中重寫該函數,運行時將會根據對象的實際類型來調 用相應的函數。如果對象類型是派生類,就調用派生類的函數;如果對象類型是基類,就調用基類 的函數。
1. 用virtual關鍵字申明的函數叫做虛函數,虛函數肯定是類的成員函數。
2. 存在虛函數的類都有一個一維的虛函數表叫做虛表,類的對象有一個指向虛表開始的虛指 針。虛表是和類對應的,虛表指針是和對象對應的。
3. 多態性是一個接口多種實現,是面向對象的核心,分為類的多態性和函數的多態性。
4. 重寫用虛函數來實現,結合動態綁定。
5. 純虛函數是虛函數再加上 = 0。
6. 抽象類是指包括至少一個純虛函數的類。
純虛函數:virtual void fun()=0。即抽象類必須在子類實現這個函數,即先有名稱,沒有內容,在 派生類實現內容。
1.3.5.說說 C 語言如何實現 C++ 語言中的重載
回答:
c語言中不允許有同名函數,因為編譯時函數命名是一樣的,不像c++會添加參數類型和返回類型作為函 數編譯后的名稱,進而實現重載。如果要用c語言顯現函數重載,可通過以下方式來實現:
1. 使用函數指針來實現,重載的函數不能使用同名稱,只是類似的實現了函數重載功能
2. 重載函數使用可變參數,方式如打開文件open函數
3. gcc有內置函數,程序使用編譯函數可以實現函數重載
#include <stdio.h>void func_int(void *a)
{printf("%d\n", *(int *)a); // 輸出 int 類型,注意 void * 轉化為 int *
}void func_double(void *b)
{printf("%.2f\n", *(double *)b); // 輸出 double 類型,注意 void * 轉化為 double *
}typedef void (*ptr)(void *); // typedef 定義一個函數指針類型void c_func(ptr p, void *param)
{p(param); // 調用對應的函數
}int main()
{int a = 23;double b = 23.23;c_func(func_int, &a); // 調用 func_int 處理 int 類型c_func(func_double, &b); // 調用 func_double 處理 double 類型return 0;
}
1.3.6.說說構造函數有幾種,分別什么作用
回答:C++中的構造函數可以分為4類:默認構造函數、初始化構造函數、拷貝構造函數、移動構造函數。
1. 默認構造函數和初始化構造函數。 在定義類的對象的時候,完成對象的初始化工作。
class Student
{
public:// 默認構造函數Student(){num = 1001;age = 18;}// 初始化構造函數Student(int n, int a) : num(n), age(a) {}private:int num;int age;
};int main()
{// 使用默認構造函數初始化對象 s1Student s1;// 使用初始化構造函數初始化對象 s2Student s2(1002, 18);return 0;
}
有了有參的構造了,編譯器就不提供默認的構造函數。
2. 拷貝構造函數
#include "stdafx.h"
#include "iostream.h"class Test
{int *p;public:Test(int ai, int value){i = ai;p = new int(value);}~Test(){delete p;}Test(const Test& t){this->i = t.i;this->p = new int(*t.p);}
};// 復制構造函數用于復制本類的對象
int main(int argc, char* argv[])
{Test t1(1, 2);Test t2(t1); // 將對象 t1 復制給 t2。注意復制和賦值的概念不同return 0;
}
賦值構造函數默認實現的是值拷貝(淺拷貝)。
3. 移動構造函數。用于將其他類型的變量,隱式轉換為本類對象。下面的轉換構造函數,將int類型 的r轉換為Student類型的對象,對象的age為r,num為1004.
Student(int r){int num=1004;int age= r;}
1.3.7.只定義析構函數,會自動生成哪些構造函數
回答:
只定義了析構函數,編譯器將自動為我們生成拷貝構造函數和默認構造函數。 默認構造函數和初始化構造函數。 在定義類的對象的時候,完成對象的初始化工作。
class Student
{
public:// 默認構造函數Student(){num = 1001;age = 18;}// 初始化構造函數Student(int n, int a) : num(n), age(a) {}private:int num;int age;
};int main()
{// 使用默認構造函數初始化對象 s1Student s1;// 使用初始化構造函數初始化對象 s2Student s2(1002, 18);return 0;
}
有了有參的構造了,編譯器就不提供默認的構造函數。
拷貝構造函數
#include "stdafx.h"
#include "iostream.h"class Test
{int i;int *p;public:Test(int ai, int value){i = ai;p = new int(value);}~Test(){delete p;}Test(const Test& t){this->i = t.i;this->p = new int(*t.p);}
};int main(int argc, char* argv[])
{Test t1(1, 2); // 使用帶參構造函數初始化對象 t1Test t2(t1); // 使用復制構造函數將 t1 復制給 t2return 0;
}
賦值構造函數默認實現的是值拷貝(淺拷貝)。
1.3.8.說說一個類,默認會生成哪些函數
定義一個空類
class Empty{};
默認會生成以下幾個函數
1. 無參的構造函數 在定義類的對象的時候,完成對象的初始化工作。
Empty(){}
2. 拷貝構造函數 拷貝構造函數用于復制本類的對象
Empty(const Empty& copy){}
3. 賦值運算符
Empty& operator = (const Empty& copy){}
4. 析構函數(非虛)
~Empty(){}
1.3.9.說說 C++ 類對象的初始化順序,有多重繼承情況下的順序
回答:
1. 創建派生類的對象,基類的構造函數優先被調用(也優先于派生類里的成員類);
2. 如果類里面有成員類,成員類的構造函數優先被調用;(也優先于該類本身的構造函數)
3. 基類構造函數如果有多個基類,則構造函數的調用順序是某類在類派生表中出現的順序而不是它們 在成員初始化表中的順序;
4. 成員類對象構造函數如果有多個成員類對象,則構造函數的調用順序是對象在類中被聲明的順序而 不是它們出現在成員初始化表中的順序;
5. 派生類構造函數,作為一般規則派生類構造函數應該不能直接向一個基類數據成員賦值而是把值傳 遞給適當的基類構造函數,否則兩個類的實現變成緊耦合的(tightly coupled)將更加難于正確地 修改或擴展基類的實現。(基類設計者的責任是提供一組適當的基類構造函數)
6. 綜上可以得出,初始化順序: 父類構造函數–>成員類對象構造函數–>自身構造函數 其中成員變量的初始化與聲明順序有關,構造函數的調用順序是類派生列表中的順序。 析構順序和構造順序相反。
1.3.10.簡述下向上轉型和向下轉型
1. 子類轉換為父類:向上轉型,使用dynamic_cast(expression),這種轉換相對來說比較 安全不會有數據的丟失;
2. 父類轉換為子類:向下轉型,可以使用強制轉換,這種轉換時不安全的,會導致數據的丟失,原因 是父類的指針或者引用的內存中可能不包含子類的成員的內存
1.3.11.簡述下深拷貝和淺拷貝,如何實現深拷貝
回答:
1. 淺拷貝:又稱值拷貝,將源對象的值拷貝到目標對象中去,本質上來說源對象和目標對象共用一份 實體,只是所引用的變量名不同,地址其實還是相同的。舉個簡單的例子,你的小名叫西西,大名 叫冬冬,當別人叫你西西或者冬冬的時候你都會答應,這兩個名字雖然不相同,但是都指的是你。
2.深拷貝,拷貝的時候先開辟出和源對象大小一樣的空間,然后將源對象里的內容拷貝到目標對象中 去,這樣兩個指針就指向了不同的內存位置。并且里面的內容是一樣的,這樣不但達到了我們想要 的目的,還不會出現問題,兩個指針先后去調用析構函數,分別釋放自己所指向的位置。即為每次 增加一個指針,便申請一塊新的內存,并讓這個指針指向新的內存,深拷貝情況下,不會出現重復 釋放同一塊內存的錯誤。
3. 深拷貝的實現:深拷貝的拷貝構造函數和賦值運算符的重載傳統實現:
STRING(const STRING& s)
{// _str = s._str; // 錯誤的實現,會導致淺拷貝_str = new char[strlen(s._str) + 1];strcpy_s(_str, strlen(s._str) + 1, s._str);
}STRING& operator=(const STRING& s)
{if (this != &s){delete[] _str; // 釋放原有內存_str = new char[strlen(s._str) + 1];strcpy_s(_str, strlen(s._str) + 1, s._str);}return *this;
}
這里的拷貝構造函數我們很容易理解,先開辟出和源對象一樣大的內存區域,然后將需要拷貝的數 據復制到目標拷貝對象 , 那么這里的賦值運算符的重載是怎么樣做的呢?
這種方法解決了我們的指針懸掛問題,通過不斷的開空間讓不同的指針指向不同的內存,以防止同 一塊內存被釋放兩次的問題。
1.3.12.簡述一下 C++ 中的多態
回答:
由于派生類重寫基類方法,然后用基類引用指向派生類對象,調用方法時候會進行動態綁定,這就是多 態。 多態分為靜態多態和動態多態:
1. 靜態多態:編譯器在編譯期間完成的,編譯器會根據實參類型來推斷該調用哪個函數,如果有對應 的函數,就調用,沒有則在編譯時報錯。
2. 動態多態:其實要實現動態多態,需要幾個條件——即動態綁定條件:
1. 虛函數。基類中必須有虛函數,在派生類中必須重寫虛函數。
2. 通過基類類型的指針或引用來調用虛函數。
說到這,得插播一條概念:重寫——也就是基類中有一個虛函數,而在派生類中也要重寫一個原型 (返回值、名字、參數)都相同的虛函數。不過協變例外。協變是重寫的特例,基類中返回值是基 類類型的引用或指針,在派生類中,返回值為派生類類型的引用或指針。
1.3.13.說說為什么要虛析構,為什么不能虛構造
1. 虛析構:將可能會被繼承的父類的析構函數設置為虛函數,可以保證當我們new一個子類,然后使 用基類指針指向該子類對象,釋放基類指針時可以釋放掉子類的空間,防止內存泄漏。如果基類的 析構函數不是虛函數,在特定情況下會導致派生來無法被析構。
1. 用派生類類型指針綁定派生類實例,析構的時候,不管基類析構函數是不是虛函數,都會正 常析構
2. 用基類類型指針綁定派生類實例,析構的時候,如果基類析構函數不是虛函數,則只會析構 基類,不會析構派生類對象,從而造成內存泄漏。為什么會出現這種現象呢,個人認為析構 的時候如果沒有虛函數的動態綁定功能,就只根據指針的類型來進行的,而不是根據指針綁 定的對象來進行,所以只是調用了基類的析構函數;如果基類的析構函數是虛函數,則析構 的時候就要根據指針綁定的對象來調用對應的析構函數了。
C++默認的析構函數不是虛函數是因為虛函數需要額外的虛函數表和虛表指針,占用額外的內存。 而對于不會被繼承的類來說,其析構函數如果是虛函數,就會浪費內存。因此C++默認的析構函數 不是虛函數,而是只有當需要當作父類時,設置為虛函數。
2. 不能虛構造:
1. 從存儲空間角度:虛函數對應一個vtale,這個表的地址是存儲在對象的內存空間的。如果將構 造函數設置為虛函數,就需要到vtable 中調用,可是對象還沒有實例化,沒有內存空間分 配,如何調用。(悖論)
2. 從使用角度:虛函數主要用于在信息不全的情況下,能使重載的函數得到對應的調用。構造 函數本身就是要初始化實例,那使用虛函數也沒有實際意義呀。所以構造函數沒有必要是虛 函數。虛函數的作用在于通過父類的指針或者引用來調用它的時候能夠變成調用子類的那個 成員函數。而構造函數是在創建對象時自動調用的,不可能通過父類的指針或者引用去調 用,因此也就規定構造函數不能是虛函數。
3. 從實現上看,vbtl 在構造函數調用后才建立,因而構造函數不可能成為虛函數。從實際含義 上看,在調用構造函數時還不能確定對象的真實類型(因為子類會調父類的構造函數);而 且構造函數的作用是提供初始化,在對象生命期只執行一次,不是對象的動態行為,也沒有 太大的必要成為虛函數。
1.3.14.說說模板類是在什么時候實現的
回答:
1. 模板實例化:模板的實例化分為顯示實例化和隱式實例化,前者是研發人員明確的告訴模板應該使 用什么樣的類型去生成具體的類或函數,后者是在編譯的過程中由編譯器來決定使用什么類型來實 例化一個模板不管是顯示實例化或隱式實例化,最終生成的類或函數完全是按照模板的定義來實現 的
2. 模板具體化(模板特化):當模板使用某種類型類型實例化后生成的類或函數不能滿足需要時,可以考慮對模板 進行具體化。具體化時可以修改原模板的定義,當使用該類型時,按照具體化后的定義實現,具體 化相當于對某種類型進行特殊處理。
1.3.15.說說類繼承時,派生類對不同關鍵字修飾的基類方法的訪問權限
回答:
類中的成員可以分為三種類型,分別為public成員、protected成員、private成員。類中可以直接訪問自 己類的public、protected、private成員,但類對象只能訪問自己類的public成員。
1. public繼承:派生類可以訪問基類的public、protected成員,不可以訪問基類的private成員; 派生類對象可以訪問基類的public成員,不可以訪問基類的protected、private成員。
2. protected繼承:派生類可以訪問基類的public、protected成員,不可以訪問基類的private成 員; 派生類對象不可以訪問基類的public、protected、private成員。
3. private繼承:派生類可以訪問基類的public、protected成員,不可以訪問基類的private成員; 派生類對象不可以訪問基類的public、protected、private成員。
1.3.16.簡述一下移動構造函數,什么庫用到了這個函數?
回答:
C++11中新增了移動構造函數。與拷貝類似,移動也使用一個對象的值設置另一個對象的值。但是,又 與拷貝不同的是,移動實現的是對象值真實的轉移(源對象到目的對象):源對象將丟失其內容,其內 容將被目的對象占有。移動操作的發生的時候,是當移動值的對象是未命名的對象的時候。這里未命名 的對象就是那些臨時變量,甚至都不會有名稱。典型的未命名對象就是函數的返回值或者類型轉換的對 象。使用臨時對象的值初始化另一個對象值,不會要求對對象的復制:因為臨時對象不會有其它使用, 因而,它的值可以被移動到目的對象。做到這些,就要使用移動構造函數和移動賦值:當使用一個臨時 變量對對象進行構造初始化的時候,調用移動構造函數。類似的,使用未命名的變量的值賦給一個對象 時,調用移動賦值操作。
移動操作的概念對對象管理它們使用的存儲空間很有用的,諸如對象使用new和delete分配內存的時 候。在這類對象中,拷貝和移動是不同的操作:從A拷貝到B意味著,B分配了新內存,A的整個內容被 拷貝到為B分配的新內存上。
而從A移動到B意味著分配給A的內存轉移給了B,沒有分配新的內存,它僅僅包含簡單地拷貝指針。 看下面的例子:
#include <iostream>
#include <string>
using namespace std;class Example6 {string* ptr;
public:// 構造函數Example6(const string& str) : ptr(new string(str)) {}// 析構函數~Example6() { delete ptr; }// 移動構造函數Example6(Example6&& x) : ptr(x.ptr) {x.ptr = nullptr;}// 移動賦值運算符Example6& operator=(Example6&& x) {delete ptr;ptr = x.ptr;x.ptr = nullptr;return *this;}// 訪問內容const string& content() const { return *ptr; }// 加法運算符重載Example6 operator+(const Example6& rhs) {return Example6(content() + rhs.content());}
};int main() {Example6 foo("Exam"); // 構造函數// 使用移動構造函數Example6 bar(move(foo)); // 調用 move 后,foo 變為一個右值引用變量// 移動賦值bar = bar + bar; // 產生一個臨時值,調用移動賦值運算符cout << "foo's content: " << foo.content() << '\n';return 0;
}
結果:foo's content: Example
1.3.17.請你回答一下 C++ 類內可以定義引用數據成員嗎?
回答:
c++類內可以定義引用成員變量,但要遵循以下三個規則:
1. 不能用默認構造函數初始化,必須提供構造函數來初始化引用成員變量。否則會造成引用未初始化 錯誤。
2. 構造函數的形參也必須是引用類型。
3. 不能在構造函數里初始化,必須在初始化列表中進行初始化。
1.3.17.構造函數為什么不能被聲明為虛函數?
1. 從存儲空間角度:虛函數對應一個vtale,這個表的地址是存儲在對象的內存空間的。如果將構造函 數設置為虛函數,就需要到vtable 中調用,可是對象還沒有實例化,沒有內存空間分配,如何調 用。(悖論)
2. 從使用角度:虛函數主要用于在信息不全的情況下,能使重載的函數得到對應的調用。構造函數本 身就是要初始化實例,那使用虛函數也沒有實際意義呀。所以構造函數沒有必要是虛函數。虛函數 的作用在于通過父類的指針或者引用來調用它的時候能夠變成調用子類的那個成員函數。而構造函 數是在創建對象時自動調用的,不可能通過父類的指針或者引用去調用,因此也就規定構造函數不 能是虛函數。
3. 從實現上看,vbtl 在構造函數調用后才建立,因而構造函數不可能成為虛函數。從實際含義上看, 在調用構造函數時還不能確定對象的真實類型(因為子類會調父類的構造函數);而且構造函數的 作用是提供初始化,在對象生命期只執行一次,不是對象的動態行為,也沒有太大的必要成為虛函數
1.3.19.簡述一下什么是常函數,有什么作用
回答:
類的成員函數后面加 const,表明這個函數不會對這個類對象的數據成員(準確地說是非靜態數據成 員)作任何改變。在設計類的時候,一個原則就是對于不改變數據成員的成員函數都要在后面加 const,而對于改變數據成員的成員函數不能加 const。所以 const 關鍵字對成員函數的行為作了更明 確的限定:有 const 修飾的成員函數(指 const 放在函數參數表的后面,而不是在函數前面或者參數表 內),只能讀取數據成員,不能改變數據成員;沒有 const 修飾的成員函數,對數據成員則是可讀可寫 的。除此之外,在類的成員函數后面加 const 還有什么好處呢?那就是常量(即 const)對象可以調用 const 成員函數,而不能調用非const修飾的函數。正如非const類型的數據可以給const類型的變量賦 值一樣,反之則不成立。
#include <iostream>
using namespace std;class CStu
{
public:int a;CStu(){a = 12;}void Show() const{// a = 13; // 帶const的函數不能修改數據成員cout << a << " I am show()" << endl;}
};int main()
{CStu st;st.Show();system("pause");return 0;
}
1.3.20.說說什么是虛繼承,解決什么問題,如何實現?
虛繼承是解決C++多重繼承問題的一種手段,從不同途徑繼承來的同一基類,會在子類中存在多份拷 貝。這將存在兩個問題:其一,浪費存儲空間;第二,存在二義性問題,通常可以將派生類對象的地址 賦值給基類對象,實現的具體方式是,將基類指針指向繼承類(繼承類有基類的拷貝)中的基類對象的 地址,但是多重繼承可能存在一個基類的多份拷貝,這就出現了二義性。虛繼承可以解決多種繼承前面 提到的兩個問題
#include <iostream>
using namespace std;class A {
public:int _a;
};class B : virtual public A {
public:int _b;
};class C : virtual public A {
public:int _c;
};class D : public B, public C {
public:int _d;
};// 菱形繼承和菱形虛繼承的對象模型
int main() {D d;d.B::_a = 1; // 訪問通過 B 繼承的 _ad.C::_a = 2; // 訪問通過 C 繼承的 _ad._b = 3;d._c = 4;d._d = 5;cout << sizeof(D) << endl;return 0;
}
分別從菱形繼承和虛繼承來分析:
菱形繼承中A在B,C,D,中各有一份,虛繼承中,A共享。
上面的虛繼承表實際上是一個指針數組。B、C實際上是虛基表指針,指向虛基表。
虛基表:存放相對偏移量,用來找虛基類
1.3.21.簡述一下虛函數和純虛函數,以及實現原理
1. C++中的虛函數的作用主要是實現了多態的機制。關于多態,簡而言之就是用父類型的指針指向其 子類的實例,然后通過父類的指針調用實際子類的成員函數。這種技術可以讓父類的指針有“多種 形態”,這是一種泛型技術。如果調用非虛函數,則無論實際對象是什么類型,都執行基類類型所 定義的函數。非虛函數總是在編譯時根據調用該函數的對象,引用或指針的類型而確定。如果調用 虛函數,則直到運行時才能確定調用哪個函數,運行的虛函數是引用所綁定或指針所指向的對象所 屬類型定義的版本。虛函數必須是基類的非靜態成員函數。虛函數的作用是實現動態聯編,也就是在程序的運行階段動態地選擇合適的成員函數,在定義了虛函數后,可以在基類的派生類中對虛函 數重新定義,在派生類中重新定義的函數應與虛函數具有相同的形參個數和形參類型。以實現統一 的接口,不同定義過程。如果在派生類中沒有對虛函數重新定義,則它繼承其基類的虛函數。
#include <iostream>
using namespace std;class Person {
public:// 虛函數virtual void GetName() {cout << "PersonName:xiaosi" << endl;}
};class Student : public Person {
public:void GetName() {cout << "StudentName:xiaosi" << endl;}
};int main() {// 指針Person *person = new Student();// 基類指針調用子類的函數person->GetName(); // 輸出: StudentName:xiaosireturn 0;
}
虛函數(Virtual Function)是通過一張虛函數表(Virtual Table)來實現的。簡稱為V-Table。在 這個表中,主是要一個類的虛函數的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應 實際的函數。這樣,在有虛函數的類的實例中這個表被分配在了這個實例的內存中,所以,當我們 用父類的指針來操作一個子類的時候,這張虛函數表就顯得由為重要了,它就像一個地圖一樣,指 明了實際所應該調用的函數。
2.純虛函數是在基類中聲明的虛函數,它在基類中沒有定義,但要求任何派生類都要定義自己的實現 方法。在基類中實現純虛函數的方法是在函數原型后加“=0” virtualvoid GetName() =0。在很多情 況下,基類本身生成對象是不合情理的。例如,動物作為一個基類可以派生出老虎、孔雀等子類, 但動物本身生成對象明顯不合常理。為了解決上述問題,將函數定義為純虛函數,則編譯器要求在 派生類中必須予以重寫以實現多態性。同時含有純虛擬函數的類稱為抽象類,它不能生成對象。這 樣就很好地解決了上述兩個問題。將函數定義為純虛函數能夠說明,該函數為后代類型提供了可以 覆蓋的接口,但是這個類中的函數絕不會調用。聲明了純虛函數的類是一個抽象類。所以,用戶不 能創建類的實例,只能創建它的派生類的實例。必須在繼承類中重新聲明函數(不要后面的=0) 否則該派生類也不能實例化,而且它們在抽象類中往往沒有定義。定義純虛函數的目的在于,使派 生類僅僅只是繼承函數的接口。純虛函數的意義,讓所有的類對象(主要是派生類對象)都可以執 行純虛函數的動作,但類無法為純虛函數提供一個合理的缺省實現。所以類純虛函數的聲明就是在 告訴子類的設計者,“你必須提供一個純虛函數的實現,但我不知道你會怎樣實現它”。
// 抽象類 Person
class Person {
public:// 純虛函數virtual void GetName() = 0;
};class Student : public Person {
public:Student() {// 構造函數}void GetName() {cout << "StudentName:xiaosi" << endl;}
};int main() {Student student;return 0;
}
1.3.22.說說純虛函數能實例化嗎,為什么?派生類要實現嗎,為什么?
1. 純虛函數不可以實例化,但是可以用其派生類實例化,示例如下:
#include <iostream>
using namespace std;class Base {
public:virtual void func() = 0; // 純虛函數
};class Derived : public Base {
public:void func() override { // 重寫純虛函數cout << "哈哈" << endl;}
};int main() {Base *b = new Derived(); // 基類指針指向派生類對象b->func(); // 調用派生類的實現return 0;
}
2. 虛函數的原理采用 vtable。類中含有純虛函數時,其vtable 不完全,有個空位。
即“純虛函數在類的vftable表中對應的表項被賦值為0。也就是指向一個不存在的函數。由于編譯 器絕對不允許有調用一個不存在的函數的可能,所以該類不能生成對象。在它的派生類中,除非重 寫此函數,否則也不能生成對象。” 所以純虛函數不能實例化。
3. 純虛函數是在基類中聲明的虛函數,它要求任何派生類都要定義自己的實現方法,以實現多態性。
4. 定義純虛函數是為了實現一個接口,用來規范派生類的行為,也即規范繼承這個類的程序員必須實 現這個函數。派生類僅僅只是繼承函數的接口。純虛函數的意義在于,讓所有的類對象(主要是派 生類對象)都可以執行純虛函數的動作,但基類無法為純虛函數提供一個合理的缺省實現。所以類 純虛函數的聲明就是在告訴子類的設計者,“你必須提供一個純虛函數的實現,但我不知道你會怎 樣實現它”。
1.3.23.說說C++中虛函數與純虛函數的區別
1. 虛函數和純虛函數可以定義在同一個類中,含有純虛函數的類被稱為抽象類,而只含有虛函數的類 不能被稱為抽象類。
2. 虛函數可以被直接使用,也可以被子類重載以后,以多態的形式調用,而純虛函數必須在子類中實 現該函數才可以使用,因為純虛函數在基類有聲明而沒有定義。
3. 虛函數和純虛函數都可以在子類中被重載,以多態的形式被調用。
4. 虛函數和純虛函數通常存在于抽象基類之中,被繼承的子類重載,目的是提供一個統一的接口。 5. 虛函數的定義形式: virtual{} ;純虛函數的定義形式: virtual { } = 0;在虛函數和純虛函 C 數的定義中不能有static標識符,原因很簡單,被static修飾的函數在編譯時要求前期綁定,然而虛 函數卻是動態綁定,而且被兩者修飾的函數生命周期也不一樣。
1.3.24.說說 C++ 中什么是菱形繼承問題,如何解決
1. 下面的圖表可以用來解釋菱形繼承問題。
假設我們有類B和類C,它們都繼承了相同的類A。另外我們還有類D,類D通過多重繼承機制繼承 了類B和類C。因為上述圖表的形狀類似于菱形,因此這個問題被形象地稱為菱形繼承問題。現 在,我們將上面的圖表翻譯成具體的代碼:
/*** Animal類對應于圖表的類A**/
class Animal {int weight;public:int getWeight() { return weight; }
};class Tiger : public Animal {/* ... */
};class Lion : public Animal {/* ... */
};class Liger : public Tiger, public Lion {/* ... */
};
在上面的代碼中,我們給出了一個具體的菱形繼承問題例子。Animal類對應于最頂層類(圖表中 的A),Tiger和Lion分別對應于圖表的B和C,Liger類(獅虎獸,即老虎和獅子的雜交種)對應于 D。
現在,問題是如果我們有這種繼承結構會出現什么樣的問題。 看看下面的代碼后再來回答問題吧。
int main( ){Liger lg;/*編譯錯誤,下面的代碼不會被任何C++編譯器通過 */int weight = lg.getWeight();
}
在我們的繼承結構中,我們可以看出Tiger和Lion類都繼承自Animal基類。所以問題是:因為Liger 多重繼承了Tiger和Lion類,因此Liger類會有兩份Animal類的成員(數據和方法),Liger對 象"lg"會包含Animal基類的兩個子對象。
所以,你會問Liger對象有兩個Animal基類的子對象會出現什么問題?再看看上面的代碼-調 用"lg.getWeight()"將會導致一個編譯錯誤。這是因為編譯器并不知道是調用Tiger類的getWeight() 還是調用Lion類的getWeight()。所以,調用getWeight方法是不明確的,因此不能通過編譯。
2. 我們給出了菱形繼承問題的解釋,但是現在我們要給出一個菱形繼承問題的解決方案。如果Lion類 和Tiger類在分別繼承Animal類時都用virtual來標注,對于每一個Liger對象,C++會保證只有一個 Animal類的子對象會被創建。看看下面的代碼:
class Tiger : virtual public Animal { /* ... */ };class Lion : virtual public Animal { /* ... */ };
你可以看出唯一的變化就是我們在類Tiger和類Lion的聲明中增加了"virtual"關鍵字。現在類Liger 對象將會只有一個Animal子對象,下面的代碼編譯正常:
1.3.25.請問構造函數中的能不能調用虛方法
1. 不要在構造函數中調用虛方法,從語法上講,調用完全沒有問題,但是從效果上看,往往不能達到 需要的目的。
派生類對象構造期間進入基類的構造函數時,對象類型變成了基類類型,而不是派生類類型。 同樣,進入基類析構函數時,對象也是基類類型。
所以,虛函數始終僅僅調用基類的虛函數(如果是基類調用虛函數),不能達到多態的效果,所以 放在構造函數中是沒有意義的,而且往往不能達到本來想要的效果。
1.3.26.請問拷貝構造函數的參數是什么傳遞方式,為什么
1. 拷貝構造函數的參數必須使用引用傳遞
2. 如果拷貝構造函數中的參數不是一個引用,即形如CClass(const CClass c_class),那么就相當于采 用了傳值的方式(pass-by-value),而傳值的方式會調用該類的拷貝構造函數,從而造成無窮遞歸地 調用拷貝構造函數。因此拷貝構造函數的參數必須是一個引用。
需要澄清的是,傳指針其實也是傳值,如果上面的拷貝構造函數寫成CClass(const CClass* c_class),也是不行的。事實上,只有傳引用不是傳值外,其他所有的傳遞方式都是傳值。
1.3.27.如何理解抽象類?
1. 抽象類的定義如下:
純虛函數是在基類中聲明的虛函數,它在基類中沒有定義,但要求任何派生類都要定義自己的實現 方法。在基類中實現純虛函數的方法是在函數原型后加“=0”,有虛函數的類就叫做抽象類。
2. 抽象類有如下幾個特點:
1)抽象類只能用作其他類的基類,不能建立抽象類對象。
2)抽象類不能用作參數類型、函數返回類型或顯式轉換的類型。
3)可以定義指向抽象類的指針和引用,此指針可以指向它的派生類,進而實現多態性。
1.3.28.什么是多態?除了虛函數,還有什么方式能實現多態?
1. 多態是面向對象的重要特性之一,它是一種行為的封裝,就是不同對象對同一行為會有不同的狀 態。(舉例 : 學生和成人都去買票時,學生會打折,成人不會)
2. 多態是以封裝和繼承為基礎的。在C++中多態分為靜態多態(早綁定)和動態多態(晚綁定)兩 種,其中動態多態是通過虛函數實現,靜態多態通過函數重載實現,代碼如下:
class A{public:
void do(int a);
void do(int a, int b);};
1.3.29.簡述一下拷貝賦值和移動賦值?
1. 拷貝賦值是通過拷貝構造函數來賦值,在創建對象時,使用同一類中之前創建的對象來初始化新創 建的對象。
2. 移動賦值是通過移動構造函數來賦值,二者的主要區別在于
1)拷貝構造函數的形參是一個左值引用,而移動構造函數的形參是一個右值引用;
2)拷貝構造函數完成的是整個對象或變量的拷貝,而移動構造函數是生成一個指針指向源對象或 變量的地址,接管源對象的內存,相對于大量數據的拷貝節省時間和內存空間。
1.3.30.仿函數了解嗎?有什么作用
1. 仿函數(functor)又稱為函數對象(function object)是一個能行使函數功能的類。仿函數的語 法幾乎和我們普通的函數調用一樣,不過作為仿函數的類,都必須重載operator()運算符,舉個例 子:
#include <iostream>
using namespace std;class Func {
public:void operator()(const string& str) const {cout << str << endl;}
};int main() {Func myFunc;myFunc("helloworld!");return 0;
}
2. 仿函數既能想普通函數一樣傳入給定數量的參數,還能存儲或者處理更多我們需要的有用信息。我 們可以舉個例子:
假設有一個 vector <string>,你的任務是統計長度小于5的string的個數,如果使用count_if函數的話,你的代碼可能長這樣
#include <iostream>
#include <vector>
#include <algorithm> // for count_ifbool LengthIsLessThanFive(const std::string& str) {return str.length() < 5;
}int main() {std::vector<std::string> vec = {"apple", "banana", "cat", "dog", "elephant"};int res = std::count_if(vec.begin(), vec.end(), LengthIsLessThanFive);std::cout << "Number of strings with length less than 5: " << res << std::endl;return 0;
}
其中c ount_if 函數的第三個參數是一個函數指針,返回一個bool類型的值。一般的,如果需要將 特定的閾值長度也傳入的話,我們可能將函數寫成這樣:
bool LenthIsLessThan(const string& str, int len) {return str.length()<len;}
這個函數看起來比前面一個版本更具有一般性,但是他不能滿足 count_if 函數的參數要求: count_if 要求的是unary function(僅帶有一個參數)作為它的最后一個參數。如果我們使用仿 函數,是不是就豁然開朗了呢:
class ShorterThan {
public:explicit ShorterThan(int maxLength) : length(maxLength) {}bool operator()(const string& str) const {return str.length() < length;}
private:const int length;
};