c++面試
1 多型的實現
存在虛擬函式的類至少有一個(多繼承會有多個)一維的虛擬函式表叫做虛表(virtual table),屬於類成員,虛表的元素值是虛擬函式的入口地址,在編譯時就已經為其在資料端分配了空間。編譯器另外還為每個類的物件提供一個虛表指標(vptr),指向虛表入口地址,屬於物件成員。在例項化派生類物件時,先例項化基類,將基類的虛表入口地址賦值給基類的虛表指標,當基類建構函式執行完時,再將派生類的虛表入口地址賦值給基類的虛表指標(派生類和基類此時共享一個虛表指標,並沒有各自都生成一個),在執行父類的建構函式。
以上是C++多型的實現過程,可以得出結論:
- 1 有虛擬函式的類必存在一個虛表。
- 2 虛表的構建:基類的虛表構建,先填上虛解構函式的入口地址,之後所有虛擬函式的入口地址按在類中宣告順序填入虛表;派生類的虛表構建,先將基類的虛表內容複製到派生類虛表中,如果派生類覆蓋了基類的虛擬函式,則虛表中對應的虛擬函式入口地址也會被覆蓋,為了後面定址的一致性。
連結:https://www.jianshu.com/p/189956c94cef
struct和class的區別
1、struct的預設成員許可權是public
2、class的預設成員許可權是private
C++中可以使用struct、class來定義一個類
C++程式設計規範
1、全域性變數:g_
2、成員變數:m_3、靜態變數:s_
4、常量:c_
extern “C”,C++在呼叫C語言API時,需要使用extern "C"修飾C語言的函式宣告
背景: C++編譯器預設會對符號名(變數名、函式名等)進行改編、修飾, 過載時會生成多個不同的函式名,不同編譯器(MSVC、g++)有不同的生成規則
連結:https://www.jianshu.com/p/746e291f5190
1.什麼是虛擬函式?什麼是純虛擬函式?
虛擬函式是允許被其子類重新定義的成員函式。
虛擬函式的宣告:virtual returntype func(parameter);引入虛擬函式的目的是為了動態繫結;
純虛擬函式宣告:virtual returntype func(parameter)=0;引入純虛擬函式是為了派生介面。(使派生類僅僅只是繼承函式的介面)
2.基類為什麼需要虛解構函式?
防止記憶體洩漏。想去借助父類指標去銷燬子類物件的時候,不能去銷燬子類物件。假如沒有虛解構函式,釋放一個由基類指標指向的派生類物件時,不會觸發動態繫結,則只會呼叫基類的解構函式,不會呼叫派生類的。派生類中申請的空間則得不到釋放導致記憶體洩漏。
3.當i是一個整數的時候i++和++i那個更快?它們的區別是什麼?
幾乎一樣。i++返回的是i的值,++i返回的是i+1的值,即++i是一個確定的值,是一個可以修改的左值。
4.vector的reserve和capacity的區別?
reserve()用於讓容器預留空間,避免再次分配記憶體;capacity()返回在重新進行分配以前所能容納的元素數量。
5.如何初始化const和static資料成員?
通常在類外申明static成員,但是static const的整型(bool,char,int,long)可以在類中宣告且初始化,static const的其他型別必須在類外初始化(包括整型陣列)。
6.static 和const分別怎麼用,類裡面static和const可以同時修飾成員函式嗎?
static的作用:
對變數:
1.區域性變數:
在區域性變數之前加上關鍵字static,區域性變數就被定義成為一個區域性靜態變數。
1)記憶體中的位置:靜態儲存區
2)初始化:未經初始化的全域性靜態變數會被程式自動初始化為0(自動物件的值是任意的,除非他被顯示初始化)
3)作用域:作用域仍為區域性作用域,當定義它的函式或者語句塊結束的時候,作用域隨之結束。
注:當static用來修飾區域性變數的時候,它就改變了區域性變數的儲存位置(從原來的棧中存放改為靜態儲存區)及其生命週期(區域性靜態變數在離開作用域之後,並沒有被銷燬,而是仍然駐留在記憶體當中,直到程式結束,只不過我們不能再對他進行訪問),但未改變其作用域。
2.全域性變數
在全域性變數之前加上關鍵字static,全域性變數就被定義成為一個全域性靜態變數。
1)記憶體中的位置:靜態儲存區(靜態儲存區在整個程式執行期間都存在)
2)初始化:未經初始化的全域性靜態變數會被程式自動初始化為0(自動物件的值是任意的,除非他被顯示初始化)
3)作用域:全域性靜態變數在宣告他的檔案之外是不可見的。準確地講從定義之處開始到檔案結尾。
注:static修飾全域性變數,並未改變其儲存位置及生命週期,而是改變了其作用域,使當前檔案外的原始檔無法訪問該變數,好處如下:(1)不會被其他檔案所訪問,修改(2)其他檔案中可以使用相同名字的變數,不會發生衝突。對全域性函式也是有隱藏作用。而普通全域性變數只要定義了,任何地方都能使用,使用前需要宣告所有的.c檔案,只能定義一次普通全域性變數,但是可以宣告多次(外部連結)。注意:全域性變數的作用域是全域性範圍,但是在某個檔案中使用時,必須先宣告。
對類中的:
1.成員變數
用static修飾類的資料成員實際使其成為類的全域性變數,會被類的所有物件共享,包括派生類的物件。因此,static成員必須在類外進行初始化(初始化格式: int base::var=10;),而不能在建構函式內進行初始化,不過也可以用const修飾static資料成員在類內初始化 。因為靜態成員屬於整個類,而不屬於某個物件,如果在類內初始化,會導致每個物件都包含該靜態成員,這是矛盾的。
特點:
1.不要試圖在標頭檔案中定義(初始化)靜態資料成員。在大多數的情況下,這樣做會引起重複定義這樣的錯誤。即使加上#ifndef #define #endif或者#pragma once也不行。
2.靜態資料成員可以成為成員函式的可選引數,而普通資料成員則不可以。
3.靜態資料成員的型別可以是所屬類的型別,而普通資料成員則不可以。普通資料成員的只能宣告為 所屬類型別的指標或引用。
2.成員函式
- 用static修飾成員函式,使這個類只存在這一份函式,所有物件共享該函式,不含this指標。
- 靜態成員是可以獨立訪問的,也就是說,無須建立任何物件例項就可以訪問。base::func(5,3);當static成員函式在類外定義時不需要加static修飾符。
- 在靜態成員函式的實現中不能直接引用類中說明的非靜態成員,可以引用類中說明的靜態成員。因為靜態成員函式不含this指標。
不可以同時用const和static修飾成員函式。
C++編譯器在實現const的成員函式的時候為了確保該函式不能修改類的例項的狀態,會在函式中新增一個隱式的引數const this*。但當一個成員為static的時候,該函式是沒有this指標的。也就是說此時const的用法和static是衝突的。
我們也可以這樣理解:兩者的語意是矛盾的。static的作用是表示該函式只作用在型別的靜態變數上,與類的例項沒有關係;而const的作用是確保函式不能修改類的例項的狀態,與型別的靜態變數沒有關係。因此不能同時用它們。
const的作用:
1.限定變數為不可修改。
2.限定成員函式不可以修改任何資料成員。
3.const與指標:
const char *p 表示 指向的內容不能改變。
char * const p,就是將P宣告為常指標,它的地址不能改變,是固定的,但是它的內容可以改變。
7.指標和引用的區別
本質上的區別是,指標是一個新的變數,只是這個變數儲存的是另一個變數的地址,我們通過訪問這個地址來修改變數。
而引用只是一個別名,還是變數本身。對引用進行的任何操作就是對變數本身進行操作,因此以達到修改變數的目的。
注:
(1)指標:指標是一個變數,只不過這個變數儲存的是一個地址,指向記憶體的一個儲存單元;而引用跟原來的變數實質上是同一個東西,只不過是原變數的一個別名而已。如:
int a=1;int *p=&a;
int a=1;int &b=a;
上面定義了一個整形變數和一個指標變數p,該指標變數指向a的儲存單元,即p的值是a儲存單元的地址。
而下面2句定義了一個整形變數a和這個整形a的引用b,事實上a和b是同一個東西,在記憶體佔有同一個儲存單元。
(2)可以有const指標,但是沒有const引用(const引用可讀不可改,與繫結物件是否為const無關)
注:引用可以指向常量,也可以指向變數。例如int &a=b,使引用a指向變數b。而為了讓引用指向常量,必須使用常量引用,如const int &a=1; 它代表的是引用a指向一個const int型,這個int型的值不能被改變,而不是引用a的指向不能被改變,因為引用的指向本來就是不可變的,無需加const宣告。即指標存在常量指標int const *p和指標常量int *const p,而引用只存在常量引用int const &a,不存在引用常量int& const a。
(3)指標可以有多級,但是引用只能是一級(int **p;合法 而 int &&a是不合法的)
(4)指標的值可以為空,但是引用的值不能為NULL,並且引用在定義的時候必須初始化;
(5)指標的值在初始化後可以改變,即指向其它的儲存單元,而引用在進行初始化後就不會再改變了。
(6)"sizeof引用"得到的是所指向的變數(物件)的大小,而"sizeof指標"得到的是指標本身的大小;
(7)指標和引用的自增(++)運算意義不一樣;
(8)指標使用時需要解引用(*),引用則不需要;
8.什麼是多型?多型有什麼用途?
C++ 多型有兩種:靜態多型(早繫結)、動態多型(晚繫結)。靜態多型是通過函式過載實現的;動態多型是通過虛擬函式實現的。
1.定義:“一個介面,多種方法”,程式在執行時才決定要呼叫的函式。
2.實現:C++多型性主要是通過虛擬函式實現的,虛擬函式允許子類重寫override(注意和overload的區別,overload是過載,是允許同名函式的表現,這些函式引數列表/型別不同)。
注:多型與非多型的實質區別就是函式地址是靜態繫結還是動態繫結。如果函式的呼叫在編譯器編譯期間就可以確定函式的呼叫地址,併產生程式碼,說明地址是靜態繫結的;如果函式呼叫的地址是需要在執行期間才確定,屬於動態繫結。
3.目的:介面重用。封裝可以使得程式碼模組化,繼承可以擴充套件已存在的程式碼,他們的目的都是為了程式碼重用。而多型的目的則是為了介面重用。
4.用法:宣告基類的指標,利用該指標指向任意一個子類物件,呼叫相應的虛擬函式,可以根據指向的子類的不同而實現不同的方法。
用一句話概括:在基類的函式前加上virtual關鍵字,在派生類中重寫該函式,執行時將會根據物件的實際型別來呼叫相應的函式。如果物件型別是派生類,就呼叫派生類的函式;如果物件型別是基類,就呼叫基類的函式。
關於過載、重寫、隱藏的區別
Overload(過載):在C++程式中,可以將語義、功能相似的幾個函式用同一個名字表示,但引數或返回值不同(包括型別、順序不同),即函式過載。
(1)相同的範圍(在同一個類中);
(2)函式名字相同;
(3)引數不同;
(4)virtual 關鍵字可有可無。
Override(覆蓋或重寫):是指派生類函式覆蓋基類函式,特徵是:
(1)不同的範圍(分別位於派生類與基類);
(2)函式名字相同;
(3)引數相同;
(4)基類函式必須有virtual 關鍵字。
注:重寫基類虛擬函式的時候,會自動轉換這個函式為virtual函式,不管有沒有加virtual,因此重寫的時候不加virtual也是可以的,不過為了易讀性,還是加上比較好。
Overwrite(重寫):隱藏,是指派生類的函式遮蔽了與其同名的基類函式,規則如下:
(1)如果派生類的函式與基類的函式同名,但是引數不同。此時,不論有無virtual關鍵字,基類的函式將被隱藏(注意別與過載混淆)。
(2)如果派生類的函式與基類的函式同名,並且引數也相同,但是基類函式沒有virtual關鍵字。此時,基類的函式被隱藏(注意別與覆蓋混淆)。
虛擬函式表:
詳細解釋可以參考部落格:https://www.cnblogs.com/jin521/p/5602190.html
多型是由虛擬函式實現的,而虛擬函式主要是通過虛擬函式表(V-Table)來實現的。
如果一個類中包含虛擬函式(virtual修飾的函式),那麼這個類就會包含一張虛擬函式表,虛擬函式表儲存的每一項是一個虛擬函式的地址。如下圖:
這個類的每一個物件都會包含一個虛指標(虛指標存在於物件例項地址的最前面,保證虛擬函式表有最高的效能),這個虛指標指向虛擬函式表。
注:物件不包含虛擬函式表,只有虛指標,類才包含虛擬函式表,派生類會生成一個相容基類的虛擬函式表。
- 原始基類的虛擬函式表
下圖是原始基類的物件,可以看到虛指標在地址的最前面,指向基類的虛擬函式表(假設基類定義了3個虛擬函式)
- 單繼承時的虛擬函式(無重寫基類虛擬函式)
假設現在派生類繼承基類,並且重新定義了3個虛擬函式,派生類會自己產生一個相容基類虛擬函式表的屬於自己的虛擬函式表。
Derive Class繼承了Base Class中的3個虛擬函式,準確說是該函式的實體地址被拷貝到Derive Class的虛擬函式列表中,派生新增的虛擬函式置於虛擬函式列表後面,並按宣告順序擺放。
- 單繼承時的虛擬函式(重寫基類虛擬函式)
現在派生類重寫基類的x函式,可以看到這個派生類構建自己的虛擬函式表的時候,修改了base::x()這一項,指向了自己的虛擬函式。
- 多重繼承時的虛擬函式(class Derived :public Base1,public Base2)
這個派生類多重繼承了兩個基類base1,base2,因此它有兩個虛擬函式表。
它的物件會有多個虛指標(據說和編譯器相關),指向不同的虛擬函式表。
注:有關以上虛擬函式表等詳見c++物件模型。連結地址:https://www.cnblogs.com/inception6-lxc/p/9273918.html
純虛擬函式:
定義: 在很多情況下,基類本身生成物件是不合情理的。為了解決這個問題,方便使用類的多型性,引入了純虛擬函式的概念,將函式定義為純虛擬函式(方法:virtual ReturnType Function()=0;)純虛擬函式不能再在基類中實現,編譯器要求在派生類中必須予以重寫以實現多型性。同時含有純虛擬函式的類稱為抽象類,它不能生成物件。稱帶有純虛擬函式的類為抽象類。
特點:
1,當想在基類中抽象出一個方法,且該基類只做能被繼承,而不能被例項化;(避免類被例項化且在編譯時候被發現,可以採用此方法)
2,這個方法必須在派生類(derived class)中被實現;
目的:使派生類僅僅只是繼承函式的介面。
9.vector中size()和capacity()的區別。
size()指容器當前擁有的元素個數(對應的resize(size_type)會在容器尾新增或刪除一些元素,來調整容器中實際的內容,使容器達到指定的大小。);capacity()指容器在必須分配儲存空間之前可以儲存的元素總數。
size表示的這個vector裡容納了多少個元素,capacity表示vector能夠容納多少元素,它們的不同是在於vector的size是2倍增長的。如果vector的大小不夠了,比如現在的capacity是4,插入到第五個元素的時候,發現不夠了,此時會給他重新分配8個空間,把原來的資料及新的資料複製到這個新分配的空間裡。(會有迭代器失效的問題)
10.new和malloc的區別。
詳細參考:連結
- new是運算子,malloc()是一個庫函式;
- new會呼叫建構函式,malloc不會;
- new返回指定型別指標,malloc返回void*指標,需要強制型別轉換;
- new會自動計算需分配的空間,malloc不行;
- new可以被過載,malloc不能。
11.C++的記憶體分割槽
- 棧區(stack):主要存放函式引數以及區域性變數,由系統自動分配釋放。
- 堆區(heap):由使用者通過 malloc/new 手動申請,手動釋放。注意它與資料結構中的堆是兩回事,分配方式倒是類似於連結串列。
- 全域性/靜態區:存放全域性變數、靜態變數;程式結束後由系統釋放。
- 字串常量區:字串常量就放在這裡,程式結束後由系統釋放。
- 程式碼區:存放程式的二進位制程式碼。
12.vector、map、multimap、unordered_map、unordered_multimap的底層資料結構,以及幾種map容器如何選擇?
底層資料結構:
- vector基於陣列,map、multimap基於紅黑樹,unordered_map、unordered_multimap基於雜湊表。
根據應用場景進行選擇:
- map/unordered_map不允許重複元素
- multimap/unordered_multimap允許重複元素
- map/multimap底層基於紅黑樹,元素自動有序,且插入、刪除效率高
- unordered_map/unordered_multimap底層基於雜湊表,故元素無序,查詢效率高。
13.記憶體洩漏怎麼產生的?如何避免?
- 記憶體洩漏一般是指堆記憶體的洩漏,也就是程式在執行過程中動態申請的記憶體空間不再使用後沒有及時釋放,導致那塊記憶體不能被再次使用。
- 更廣義的記憶體洩漏還包括未對系統資源的及時釋放,比如控制代碼、socket等沒有使用相應的函式釋放掉,導致系統資源的浪費。
VS下檢測記憶體洩漏方法:
#define CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
//在入口函式中包含 _CrtDumpMemoryLeaks();
//即可檢測到記憶體洩露
//以如下測試函式為例:
int main()
{
char* pChars = new char[10];
//delete[]pChars;
_CrtDumpMemoryLeaks();
system("pause");
return 0;
}
解決方法:
- 養成良好的編碼習慣和規範,記得及時釋放掉記憶體或系統資源。
- 過載new和delete,以連結串列的形式自動管理分配的記憶體。
- 使用智慧指標,share_ptr、auto_ptr、weak_ptr。
14.說幾個C++11的新特性
- auto型別推導
- 範圍for迴圈
- lambda函式
- override 和 final 關鍵字
-
/*如果不使用override,當你手一抖,將foo()寫成了f00()會怎麼樣呢?結果是編譯器並不會報錯,因為它並不知道你的目的是重寫虛擬函式,而是把它當成了新的函式。如果這個虛擬函式很重要的話,那就會對整個程式不利。 所以,override的作用就出來了,它指定了子類的這個虛擬函式是重寫的父類的,如果你名字不小心打錯了的話,編譯器是不會編譯通過的:*/ class A { virtual void foo(); } class B :public A { void foo(); //OK virtual foo(); // OK void foo() override; //OK } class A { virtual void foo(); }; class B :A { virtual void f00(); //OK virtual void f0o()override; //Error };
/*當不希望某個類被繼承,或不希望某個虛擬函式被重寫,可以在類名和虛擬函式後新增final關鍵字,新增final關鍵字後被繼承或重寫,編譯器會報錯。例子如下:*/ class Base { virtual void foo(); }; class A : Base { void foo() final; // foo 被override並且是最後一個override,在其子類中不可以重寫 void bar() final; // Error: 父類中沒有 bar虛擬函式可以被重寫或final }; class B final : A // 指明B是不可以被繼承的 { void foo() override; // Error: 在A中已經被final了 }; class C : B // Error: B is final { };
- 空指標常量nullptr
- 執行緒支援、智慧指標等
15.C和C++區別?
C++在C的基礎上增添類,C是一個結構化語言,它的重點在於演算法和資料結構。C程式的設計首要考慮的是如何通過一個過程,對輸入(或環境條件)進行運算處理得到輸出(或實現過程(事務)控制),而對於C++,首要考慮的是如何構造一個物件模型,讓這個模型能夠契合與之對應的問題域,這樣就可以通過獲取物件的狀態資訊得到輸出或實現過程(事務)控制。
16.const與#define的區別
1.編譯器處理方式
define – 在預處理階段進行替換
const – 在編譯時確定其值
2.型別檢查
define – 無型別,不進行型別安全檢查,可能會產生意想不到的錯誤
const – 有資料型別,編譯時會進行型別檢查
3.記憶體空間
define – 不分配記憶體,給出的是立即數,有多少次使用就進行多少次替換,在記憶體中會有多個拷貝,消耗記憶體大
const – 在靜態儲存區中分配空間,在程式執行過程中記憶體中只有一個拷貝
4.其他
在編譯時, 編譯器通常不為const常量分配儲存空間,而是將它們儲存在符號表中,這使得它成為一個編譯期間的常量,沒有了儲存與讀記憶體的操作,使得它的效率也很高。
巨集替換隻作替換,不做計算,不做表示式求解。
17.懸空指標與野指標區別
- 懸空指標:當所指向的物件被釋放或者收回,但是沒有讓指標指向NULL;
{
char *dp = NULL;
{
char c;
dp = &c;
}
//變數c釋放,dp變成空懸指標
}
void func()
{
char *dp = (char *)malloc(A_CONST);
free(dp); //dp變成一個空懸指標
dp = NULL; //dp不再是空懸指標
/* ... */
}
- 野指標:那些未初始化的指標;
int func()
{
char *dp;//野指標,沒有初始化
static char *sdp;//非野指標,因為靜態變數會預設初始化為0
}
18.struct與class的區別?
本質區別是訪問的預設控制:預設的繼承訪問許可權,class是private,struct是public;
19.sizeof和strlen的區別?
功能不同:
sizeof是操作符,引數為任意型別,主要計算型別佔用記憶體大小。
strlen()是函式,其函式原型為:extern unsigned int strlen(char *s);其引數為char*,strlen只能計算以"\0"結尾字串的長度,計算結果不包括"\0"。
char* ss="0123456789";
//s1=4,ss為字元指標在記憶體中佔用4個位元組
int s1=sizeof(ss);
//s2=10,計算字串ss的長度
int s2=strlen(ss);
引數不同:
當將字元陣列作為sizeof()的引數時,計算字元陣列佔用記憶體大小;當將字元陣列作為strlen()函式,字元陣列轉化為char*。因為sizeof的引數為任意型別,而strlen()函式引數只能為char*,當引數不是char*必須轉換為char*。
char str[]="abced";
//a為6(1*6),字元陣列str包含6個元素(a,b,c,d,e,\0),每個元素佔用1個位元組
int a= sizeof(str);
//len為5,不包含"\0",
int len=strlen(str);
//str[0]是字元元素a,所以b=1
int b= sizeof(str[0]);
20.32位,64位系統中,各種常用內建資料型別佔用的位元組數?
char :1個位元組(固定)
*(即指標變數): 4個位元組(32位機的定址空間是4個位元組。同理64位編譯器)(變化*)
short int : 2個位元組(固定)
int: 4個位元組(固定)
unsigned int : 4個位元組(固定)
float: 4個位元組(固定)
double: 8個位元組(固定)
long: 4個位元組
unsigned long: 4個位元組(變化*,其實就是定址控制元件的地址長度數值)
long long: 8個位元組(固定)
64位作業系統
char :1個位元組(固定)
*(即指標變數): 8個位元組
short int : 2個位元組(固定)
int: 4個位元組(固定)
unsigned int : 4個位元組(固定)
float: 4個位元組(固定)
double: 8個位元組(固定)
long: 8個位元組
unsigned long: 8個位元組(變化*其實就是定址控制元件的地址長度數值)
long long: 8個位元組(固定)
除*與long 不同其餘均相同。
21.virtual, inline, decltype,volatile,static, const關鍵字的作用?使用場景?
inline:在c/c++中,為了解決一些頻繁呼叫的小函式大量消耗棧空間(棧記憶體)的問題,特別的引入了inline修飾符,表示為行內函數。
#include <stdio.h>
//函式定義為inline即:行內函數
inline char* dbtest(int a) {
return (i % 2 > 0) ? "奇" : "偶";
}
int main()
{
int i = 0;
for (i=1; i < 100; i++) {
printf("i:%d 奇偶性:%s /n", i, dbtest(i));
}
}//在for迴圈的每個dbtest(i)的地方替換成了 (i % 2 > 0) ? "奇" : "偶",避免了頻繁呼叫函式,對棧記憶體的消耗
decltype:從表示式中推斷出要定義變數的型別,但卻不想用表示式的值去初始化變數。還有可能是函式的返回型別為某表示式的的值型別。
volatile:volatile 關鍵字是一種型別修飾符,用它宣告的型別變量表示可以被某些編譯器未知的因素更改,比如:作業系統、硬體或者其它執行緒等。遇到這個關鍵字宣告的變數,編譯器對訪問該變數的程式碼就不再進行優化,從而可以提供對特殊地址的穩定訪問。
static:
- 隱藏
在變數和函式名前面如果未加static,則它們是全域性可見的。加了static,就會對其它原始檔隱藏,利用這一特性可以在不同的檔案中定義同名函式和同名變數,而不必擔心命名衝 突。static可以用作函式和變數的字首,對於函式來講,static的作用僅限於隱藏。
2.static變數中的記憶功能和全域性生存期
儲存在靜態資料區的變數會在程式剛開始執行時就完成初始化,也是唯一的一次初始化。共有兩種變數儲存在靜態儲存區:全域性變數和static變數,只不過和全域性變數比起來,static可以控制變數的可見範圍,說到底static還是用來隱藏的。PS:如果作為static區域性變數在函式內定義,它的生存期為整個源程式,但是其作用域仍與自動變數相同,只能在定義該變數的函式內使用該變數。退出該函式後, 儘管該變數還繼續存在,但不能使用它。
#include <stdio.h>
int fun(){
static int count = 10; //在第一次進入這個函式的時候,變數a被初始化為10!並接著自減1,以後每次進入該函式,a
return count--; //就不會被再次初始化了,僅進行自減1的操作;在static發明前,要達到同樣的功能,則只能使用全域性變數:
}
int count = 1;
int main(void)
{
printf("global\t\tlocal static\n");
for(; count <= 10; ++count)
printf("%d\t\t%d\n", count, fun());
return 0;
}
---基於以上兩點可以得出一個結論:把區域性變數改變為靜態變數後是改變了它的儲存方式即改變了它的生存期。把全域性變數改變為靜態變數後是改變了它的作用域,限制了它的使用範圍。因此static 這個說明符在不同的地方所起的作用是不同的。
3.static的第三個作用是預設初始化為0(static變數)
最後對static的三條作用做一句話總結。首先static的最主要功能是隱藏,其次因為static變數存放在靜態儲存區,所以它具備永續性和預設值0。
4.static的第四個作用:C++中的類成員宣告static(有些地方與以上作用重疊)
在類中宣告static變數或者函式時,初始化時使用作用域運算子來標明它所屬類,因此,靜態資料成員是類的成員,而不是物件的成員,這樣就出現以下作用:
(1)類的靜態成員函式是屬於整個類而非類的物件,所以它沒有this指標,這就導致 了它僅能訪問類的靜態資料和靜態成員函式。
(2)不能將靜態成員函式定義為虛擬函式。
(3)由於靜態成員聲明於類中,操作於其外,所以對其取地址操作,就多少有些特殊 ,變數地址是指向其資料型別的指標 ,函式地址型別是一個“nonmember函式指標”。
(4)由於靜態成員函式沒有this指標,所以就差不多等同於nonmember函式,結果就 產生了一個意想不到的好處:成為一個callback函式,使得我們得以將C++和C-based X W indow系統結合,同時也成功的應用於執行緒函式身上。(這條沒遇見過)
(5)static並沒有增加程式的時空開銷,相反她還縮短了子類對父類靜態成員的訪問 時間,節省了子類的記憶體空間。
(6)靜態資料成員在<定義或說明>時前面加關鍵字static。
(7)靜態資料成員是靜態儲存的,所以必須對它進行初始化。(程式設計師手動初始化,否則編譯時一般不會報錯,但是在Link時會報錯誤)
(8)靜態成員初始化與一般資料成員初始化不同:
初始化在類體外進行,而前面不加static,以免與一般靜態變數或物件相混淆;
初始化時不加該成員的訪問許可權控制符private,public等;
初始化時使用作用域運算子來標明它所屬類;
所以我們得出靜態資料成員初始化的格式:
<資料型別><類名>::<靜態資料成員名>=<值>
(9)為了防止父類的影響,可以在子類定義一個與父類相同的靜態變數,以遮蔽父類的影響。這裡有一點需要注意:我們說靜態成員為父類和子類共享,但我們有重複定義了靜態成員,這會不會引起錯誤呢?不會,我們的編譯器採用了一種絕妙的手法:name-mangling 用以生成唯一的標誌。
22.深拷貝與淺拷貝的區別?
1.什麼時候用到拷貝函式?
a.一個物件以值傳遞的方式傳入函式體;b.一個物件以值傳遞的方式從函式返回; c.一個物件需要通過另外一個物件進行初始化。
如果在類中沒有顯式地宣告一個拷貝建構函式,那麼,編譯器將會自動生成一個預設的拷貝建構函式,該建構函式完成物件之間的位拷貝。位拷貝又稱淺拷貝;
2.是否應該自定義拷貝函式?
自定義拷貝建構函式是一種良好的程式設計風格,它可以阻止編譯器形成預設的拷貝建構函式,提高原始碼效率。
3.什麼叫深拷貝?什麼是淺拷貝?兩者異同?
如果一個類擁有資源,當這個類的物件發生複製過程的時候,資源重新分配,這個過程就是深拷貝,反之,沒有重新分配資源,就是淺拷貝。4.深拷貝好還是淺拷貝好?
如果實行位拷貝,也就是把物件裡的值完全複製給另一個物件,如A=B。這時,如果B中有一個成員變數指標已經申請了記憶體,那A中的那個成員變數也指向同一塊記憶體。這就出現了問題:當B把記憶體釋放了(如:析構),這時A內的指標就是野指標了,出現執行錯誤。
參考部落格:https://blog.csdn.net/caoshangpa/article/details/79226270
http://www.cnblogs.com/BlueTzar/articles/1223313.html
23.派生類中建構函式,解構函式呼叫順序?
建構函式:“先基後派”;解構函式:“先派後基”。
24.C++類中資料成員初始化順序?
1.成員變數在使用初始化列表初始化時,與建構函式中初始化成員列表的順序無關,只與定義成員變數的順序有關。
2.如果不使用初始化列表初始化,在建構函式內初始化時,此時與成員變數在建構函式中的位置有關。
3.類中const成員常量必須在建構函式初始化列表中初始化。
4.類中static成員變數,只能在類內外初始化(同一類的所有例項共享靜態成員變數)。
初始化順序:
- 1) 基類的靜態變數或全域性變數
- 2) 派生類的靜態變數或全域性變數
- 3) 基類的成員變數
- 4) 派生類的成員變數
25.結構體記憶體對齊問題?結構體/類大小的計算?
注:記憶體對齊是看型別,而不是看總的位元組數。比如:
#include<iostream>
using namespace std;
struct AlignData1
{
int a;
char b[7];//a後面並不會補上3個位元組,而是由於char的型別所以不用補。
short c;
char d;
}Node;
struct AlignData2
{
bool a;
int b[2];//a後面並不會補上7個位元組,而是根據int的型別補3個位元組。
int c;
int d;
}Node2;
int main(){
cout << sizeof(Node) << endl;//16
cout << sizeof(Node2) << endl;//20
system("pause");
return 0;
}
補充:
- 每個成員相對於這個結構體變數地址的偏移量正好是該成員型別所佔位元組的整數倍。為了對齊資料,可能必須在上一個資料結束和下一個資料開始的地方插入一些沒有用處位元組。
- 最終佔用位元組數為成員型別中最大佔用位元組數的整數倍。
- 一般的結構體成員按照預設對齊位元組數遞增或是遞減的順序排放,會使總的填充位元組數最少。
struct AlignData1
{
char c;
short b;
int i;
char d;
}Node;
這個結構體在編譯以後,為了位元組對齊,會被整理成這個樣子:
struct AlignData1
{
char c;
char padding[1];
short b;
int i;
char d;
char padding[3];
}Node;
含有虛擬函式的類的大小:連結
補充:聯合體的大小計算:
聯合體所佔的空間不僅取決於最寬成員,還跟所有成員有關係,即其大小必須滿足兩個條件:1)大小足夠容納最寬的成員;2)大小能被其包含的所有基本資料型別的大小所整除。
union U1
{
int n;
char s[11];
double d;
}; //16,char s[11]按照char=1可以整除
union U2
{
int n;
char s[5];
double d;
}; //8
26.static_cast, dynamic_cast, const_cast, reinpreter_cast的區別?
補充:static_cast與dynamic_cast
- cast發生的時間不同,一個是static編譯時,一個是runtime執行時;
- static_cast是相當於C的強制型別轉換,用起來可能有一點危險,不提供執行時的檢查來確保轉換的安全性。
- dynamic_cast用於轉換指標和和引用,不能用來轉換物件——主要用於類層次間的上行轉換和下行轉換,還可以用於類之間的交叉轉換。在類層次間進行上行轉換時,dynamic_cast和static_cast的效果是一樣的;在進行下行轉換時,dynamic_cast具有型別檢查的功能,比static_cast更安全。在多型型別之間的轉換主要使用dynamic_cast,因為型別提供了執行時資訊。
#include <iostream>
using namespace std;
class CBasic
{
public:
virtual int test(){return 0;}
};
class CDerived : public CBasic
{
public:
virtual int test(){ return 1;}
};
int main()
{
CBasic cBasic;
CDerived cDerived;
CBasic * pB1 = new CBasic;
CBasic * pB2 = new CDerived;
CBasic * pB3 = new CBasic;
CBasic * pB4 = new CDerived;
//dynamic cast failed, so pD1 is null.
CDerived * pD1 = dynamic_cast<CDerived * > (pB1);
//dynamic cast succeeded, so pD2 points to CDerived object
CDerived * pD2 = dynamic_cast<CDerived * > (pB2);
//pD3將是一個指向該CBasic型別物件的指標,對它進行CDerive型別的操作將是不安全的
CDerived * pD3 = static_cast<CDerived * > (pB3);
//static_cast成功
CDerived * pD4 = static_cast<CDerived * > (pB4);
//dynamci cast failed, so throw an exception.
// CDerived & rD1 = dynamic_cast<CDerived &> (*pB1);
//dynamic cast succeeded, so rD2 references to CDerived object.
CDerived & rD2 = dynamic_cast<CDerived &> (*pB2);
return 0;
}
注:CBasic要有虛擬函式,否則會編譯出錯;static_cast則沒有這個限制。
27.智慧指標
- 智慧指標是在<memory>標頭檔案中的std名稱空間中定義的,該指標用於確保程式不存在記憶體和資源洩漏且是異常安全的。它們對RAII“獲取資源即初始化”程式設計至關重要,RAII的主要原則是為將任何堆分配資源(如動態分配記憶體或系統物件控制代碼)的所有權提供給其解構函式包含用於刪除或釋放資源的程式碼以及任何相關清理程式碼的堆疊分配物件。大多數情況下,當初始化原始指標或資源控制代碼以指向實際資源時,會立即將指標傳遞給智慧指標。
- 智慧指標的設計思想:將基本型別指標封裝為類物件指標(這個類肯定是個模板,以適應不同基本型別的需求),並在解構函式裡編寫delete語句刪除指標指向的記憶體空間。
- unique_ptr只允許基礎指標的一個所有者。unique_ptr小巧高效;大小等同於一個指標且支援右值引用,從而可實現快速插入和對STL集合的檢索。
- shared_ptr採用引用計數的智慧指標,主要用於要將一個原始指標分配給多個所有者(例如,從容器返回了指標副本又想保留原始指標時)的情況。當所有的shared_ptr所有者超出了範圍或放棄所有權,才會刪除原始指標。大小為兩個指標;一個用於物件,另一個用於包含引用計數的共享控制塊。最安全的分配和使用動態記憶體的方法是呼叫make_shared標準庫函式,此函式在動態分配記憶體中分配一個物件並初始化它,返回物件的shared_ptr。
28.計算類大小例子
class A {};: sizeof(A) = 1;
class A { virtual Fun(){} };: sizeof(A) = 4(32位機器)/8(64位機器);
class A { static int a; };: sizeof(A) = 1;
class A { int a; };: sizeof(A) = 4;
class A { static int a; int b; };: sizeof(A) = 4;
類中用static宣告的成員變數不計算入類的大小中,因為static data不是例項的一部分。static的屬於全域性的,他不會佔用類的儲存,他有專門的地方儲存(全域性變數區)
29.大端與小端的概念?各自的優勢是什麼?
- 大端與小端是用來描述多位元組資料在記憶體中的存放順序,即位元組序。大端(Big Endian)指低地址端存放高位位元組,小端(Little Endian)是指低地址端存放低位位元組。
- 需要記住計算機是以位元組為儲存單位。
- 為了方便記憶可把大端和小端稱作高尾端和低尾端,eg:如果是高尾端模式一個字串“11223344”把尾部“44”放在地址的高位,如果是地尾端模式,把“44”放在地址的低位。
各自優勢:
- Big Endian:符號位的判定固定為第一個位元組,容易判斷正負。
- Little Endian:長度為1,2,4位元組的數,排列方式都是一樣的,資料型別轉換非常方便。
舉一個例子,比如數字0x12 34 56 78在記憶體中的表示形式為:
- 1)大端模式:
低地址 -----------------> 高地址
0x12 | 0x34 | 0x56 | 0x78
- 2)小端模式:
低地址 ------------------> 高地址
0x78 | 0x56 | 0x34 | 0x12
30.C++中*和&同時使用是什麼意思?
template <class T>
void InsertFront(Node<T>* & head, T item)
上面一個函式的宣告,其中第一個引數*和&分別是什麼意思?
head是個指標,前面為什麼加個&
本來“* head”代表的是傳指標的,但是隻能改變head指向的內容,而“* &head”意思是說head是傳進來的指標的同名指標,就能既改變*head指向的內容,又能改變head這個指標。比如:main()有個Node<int>* p,int t;當呼叫insertFront(p,t)是,如果template <class T>void InsertFront(Node<T>* & head, T item)中有對head進行賦值改變時,main()中的p也會跟著改變,如果沒有&這個別名標識時,p則不會隨著head的改變而改變。
31.C++vector與list區別
https://www.cnblogs.com/shijingjing07/p/5587719.html
32.C語言中static關鍵字作用
在C語言中static的作用如下
第一、在修飾變數的時候,static修飾的靜態區域性變數只執行一次,而且延長了區域性變數的生命週期,直到程式執行結束以後才釋放。
第二、static修飾全域性變數的時候,這個全域性變數只能在本檔案中訪問,不能在其它檔案中訪問,即便是extern外部宣告也不可以。
第三、static修飾一個函式,則這個函式的只能在本檔案中呼叫,不能被其他檔案呼叫。Static修飾的區域性變數存放在全域性資料區的靜態變數區。初始化的時候自動初始化為0;
(1)不想被釋放的時候,可以使用static修飾。比如修飾函式中存放在棧空間的陣列。如果不想讓這個陣列在函式呼叫結束釋放可以使用static修飾
(2)考慮到資料安全性(當程想要使用全域性變數的時候應該先考慮使用static)
在C++中static關鍵字除了具有C中的作用還有在類中的使用
在類中,static可以用來修飾靜態資料成員和靜態成員方法
靜態資料成員
(1)靜態資料成員可以實現多個物件之間的資料共享,它是類的所有物件的共享成員,它在記憶體中只佔一份空間,如果改變它的值,則各物件中這個資料成員的值都被改變。
(2)靜態資料成員是在程式開始執行時被分配空間,到程式結束之後才釋放,只要類中指定了靜態資料成員,即使不定義物件,也會為靜態資料成員分配空間。
(3)靜態資料成員可以被初始化,但是隻能在類體外進行初始化,若為對靜態資料成員賦初值,則編譯器會自動為其初始化為0
(4)靜態資料成員既可以通過物件名引用,也可以通過類名引用。
靜態成員函式
(1)靜態成員函式和靜態資料成員一樣,他們都屬於類的靜態成員,而不是物件成員。
(2)非靜態成員函式有this指標,而靜態成員函式沒有this指標。
(3)靜態成員函式主要用來訪問靜態資料成員而不能訪問非靜態成員。
33.C/C++中堆和棧的區別
講解全面的一篇部落格:https://blog.csdn.net/Fiorna0314/article/details/49757195
34.定義一個空類編譯器做了哪些操作?
如果你只是宣告一個空類,不做任何事情的話,編譯器會自動為你生成一個預設建構函式、一個拷貝預設建構函式、一個預設拷貝賦值操作符和一個預設解構函式。這些函式只有在第一次被呼叫時,才會被編譯器建立。所有這些函式都是inline和public的。
定義一個空類例如:
class Empty
{
}
一個空的class在C++編譯器處理過後就不再為空,編譯器會自動地為我們宣告一些member function,一般編譯過就相當於:
class Empty
{
public:
Empty(); // 預設建構函式//
Empty( const Empty& ); // 拷貝建構函式//
~Empty(); // 解構函式//
Empty& operator=( const Empty& ); // 賦值運算子//
};
需要注意的是,只有當你需要用到這些函式的時候,編譯器才會去定義它們。
35.友元函式和友元類
36.什麼情況下,類的解構函式應該宣告為虛擬函式?為什麼?
基類指標可以指向派生類的物件(多型性),如果刪除該指標delete []p;就會呼叫該指標指向的派生類解構函式,而派生類的解構函式又自動呼叫基類的解構函式,這樣整個派生類的物件完全被釋放。
如果解構函式不被宣告成虛擬函式,則編譯器實施靜態繫結,在刪除基類指標時,只會呼叫基類的解構函式而不呼叫派生類解構函式,這樣就會造成派生類物件析構不完全。
37.哪些函式不能成為虛擬函式?
不能被繼承的函式和不能被重寫的函式。
1)普通函式
普通函式不屬於成員函式,是不能被繼承的。普通函式只能被過載,不能被重寫,因此宣告為虛擬函式沒有意義。因為編譯器會在編譯時繫結函式。
而多型體現在執行時繫結。通常通過基類指標指向子類物件實現多型。
2)友元函式
友元函式不屬於類的成員函式,不能被繼承。對於沒有繼承特性的函式沒有虛擬函式的說法。
3)建構函式
首先說下什麼是建構函式,建構函式是用來初始化物件的。假如子類可以繼承基類建構函式,那麼子類物件的構造將使用基類的建構函式,而基類建構函式並不知道子類的有什麼成員,顯然是不符合語義的。從另外一個角度來講,多型是通過基類指標指向子類物件來實現多型的,在物件構造之前並沒有物件產生,因此無法使用多型特性,這是矛盾的。因此建構函式不允許繼承。
4)內聯成員函式
我們需要知道行內函數就是為了在程式碼中直接展開,減少函式呼叫花費的代價。也就是說行內函數是在編譯時展開的。而虛擬函式是為了實現多型,是在執行時繫結的。因此顯然行內函數和多型的特性相違背。
5)靜態成員函式
首先靜態成員函式理論是可繼承的。但是靜態成員函式是編譯時確定的,無法動態繫結,不支援多型,因此不能被重寫,也就不能被宣告為虛擬函式。、
38.編寫一個有建構函式,解構函式,賦值函式,和拷貝建構函式的String類
//.h
class String{
public:
String(const char* str);
String(const String &other);
~String();
String & operate=(const String &other);
private:
char* m_data;
};
//.cpp
String::String(const char*str){
if(str==NULL){
m_data=new char[1];
*m_data='\0';
}
else{
int length=strlen(str);
m_data=new char[length+1];
strcpy(m_data,str);
}
}
String::String(const String &other){
int length=strlen(other.m_data);
m_data=new char[length+1];
strcpy(m_data,other.m_data);
}
String::~String(){
delete [] m_data;
}
String::String& operate=(const String & other){
if(&other==*this)return *this;//檢查自賦值
delete[]m_data;//釋放原有的記憶體資源
int length=strlen(other.m_data);
m_data=new char[length+1];
strcpy(m_data,other.m_data);
return *this;//返回本物件的引用
}
注:一個單鏈表的簡單實現:連結
39.this指標的理解
40.程式載入時的記憶體分佈
- 在多工作業系統中,每個程序都執行在一個屬於自己的虛擬記憶體中,而虛擬記憶體被分為許多頁,並對映到實體記憶體中,被載入到實體記憶體中的檔案才能夠被執行。這裡我們主要關注程式被裝載後的記憶體佈局,其可執行檔案包含了程式碼段,資料段,BSS段,堆,棧等部分,其分佈如下圖所示。
- 程式碼段(.text):用來存放可執行檔案的機器指令。存放在只讀區域,以防止被修改。
- 只讀資料段(.rodata):用來存放常量存放在只讀區域,如字串常量、全域性const變數等。
- 可讀寫資料段(.data):用來存放可執行檔案中已初始化全域性變數,即靜態分配的變數和全域性變數。
- BSS段(.bss):未初始化的全域性變數和區域性靜態變數以及初始化為0的全域性變數一般放在.bss的段裡,以節省記憶體空間。eg:static int a=0;(初始化為0的全域性變數(靜態變數)放在.bss)。
- 堆:用來容納應用程式動態分配的記憶體區域。當程式使用malloc或new分配記憶體時,得到的記憶體來自堆。堆通常位於棧的下方。
- 棧:用於維護函式呼叫的上下文。棧通常分配在使用者空間的最高地址處分配。
- 動態連結庫對映區:如果程式呼叫了動態連結庫,則會有這一部分。該區域是用於對映裝載的動態連結庫。
- 保留區:記憶體中受到保護而禁止訪問的記憶體區域。
41.智慧指標
- 智慧指標是在<memory>標頭檔案中的std名稱空間中定義的,該指標用於確保程式不存在記憶體和資源洩漏且是異常安全的。它們對RAII“獲取資源即初始化”程式設計至關重要,RAII的主要原則是為將任何堆分配資源(如動態分配記憶體或系統物件控制代碼)的所有權提供給其解構函式包含用於刪除或釋放資源的程式碼以及任何相關清理程式碼的堆疊分配物件。大多數情況下,當初始化原始指標或資源控制代碼以指向實際資源時,會立即將指標傳遞給智慧指標。
- 智慧指標的設計思想:將基本型別指標封裝為類物件指標(這個類肯定是個模板,以適應不同基本型別的需求),並在解構函式裡編寫delete語句刪除指標指向的記憶體空間。
- unique_ptr只允許基礎指標的一個所有者。unique_ptr小巧高效;大小等同於一個指標且支援右值引用,從而可實現快速插入和對STL集合的檢索。
- shared_ptr採用引用計數的智慧指標,主要用於要將一個原始指標分配給多個所有者(例如,從容器返回了指標副本又想保留原始指標時)的情況。當所有的shared_ptr所有者超出了範圍或放棄所有權,才會刪除原始指標。大小為兩個指標;一個用於物件,另一個用於包含引用計數的共享控制塊。最安全的分配和使用動態記憶體的方法是呼叫make_shared標準庫函式,此函式在動態分配記憶體中分配一個物件並初始化它,返回物件的shared_ptr。
參考:https://www.cnblogs.com/wxquare/p/4759020.html
注:1.引用計數問題
- 每個shared_ptr所指向的物件都有一個引用計數,它記錄了有多少個shared_ptr指向自己
- shared_ptr的解構函式:遞減它所指向的物件的引用計數,如果引用計數變為0,就會銷燬物件並釋放相應的記憶體
- 引用計數的變化:決定權在shared_ptr,而與物件本身無關
參考:https://www.cnblogs.com/xzxl/p/7852597.html
2.智慧指標支援的操作
- 使用過載的->和*運算子訪問物件。
- 使用get成員函式獲取原始指標,提供對原始指標的直接訪問。你可以使用智慧指標管理你自己的程式碼中的記憶體,還能將原始指標傳遞給不支援智慧指標的程式碼。
- 使用刪除器定義自己的釋放操作。
- 使用release成員函式的作用是放棄智慧指標對指標的控制權,將智慧指標置空,並返回原始指標。(只支援unique_ptr)
- 使用reset釋放智慧指標對物件的所有權。
#include <iostream>
#include <string>
#include <memory>
using namespace std;
class base
{
public:
base(int _a): a(_a) {cout<<"建構函式"<<endl;}
~base() {cout<<"解構函式"<<endl;}
int a;
};
int main()
{
unique_ptr<base> up1(new base(2));
// unique_ptr<base> up2 = up1; //編譯器提示未定義
unique_ptr<base> up2 = move(up1); //轉移物件的所有權
// cout<<up1->a<<endl; //執行時錯誤
cout<<up2->a<<endl; //通過解引用運算子獲取封裝的原始指標
up2.reset(); // 顯式釋放記憶體
shared_ptr<base> sp1(new base(3));
shared_ptr<base> sp2 = sp1; //增加引用計數
cout<<"共享智慧指標的數量:"<<sp2.use_count()<<endl; //2
sp1.reset(); //
cout<<"共享智慧指標的數量:"<<sp2.use_count()<<endl; //1
cout<<sp2->a<<endl;
auto sp3 = make_shared<base>(4);//利用make_shared函式動態分配記憶體
}
3.智慧指標的陷阱(迴圈引用等問題)
class B;
class A
{
public:
shared_ptr<B> m_b;
};
class B
{
public:
shared_ptr<A> m_a;
};
int main()
{
{
shared_ptr<A> a(new A); //new出來的A的引用計數此時為1
shared_ptr<B> b(new B); //new出來的B的引用計數此時為1
a->m_b = b; //B的引用計數增加為2
b->m_a = a; //A的引用計數增加為2
}
//b先出作用域,B的引用計數減少為1,不為0;
//所以堆上的B空間沒有被釋放,且B持有的A也沒有機會被析構,A的引用計數也完全沒減少
//a後出作用域,同理A的引用計數減少為1,不為0,所以堆上A的空間也沒有被釋放
}
迴圈引用”簡單來說就是:兩個物件互相使用一個shared_ptr成員變數指向對方會造成迴圈引用。
即A內部有指向B,B內部有指向A,這樣對於A,B必定是在A析構後B才析構,對於B,A必定是在B析構後才析構A,這就是迴圈引用問題,違反常規,導致記憶體洩露。
解決迴圈引用方法:
1. 當只剩下最後一個引用的時候需要手動打破迴圈引用釋放物件。
2. 當A的生存期超過B的生存期的時候,B改為使用一個普通指標指向A。
3. 使用weak_ptr打破這種迴圈引用,因為weak_ptr不會修改計數器的大小,所以就不會產生兩個物件互相使用一個shared_ptr成員變數指向對方的問題,從而不會引起引用迴圈。
42.vector擴容原理說明
- 新增元素:Vector通過一個連續的陣列存放元素,如果集合已滿,在新增資料的時候,就要分配一塊更大的記憶體,將原來的資料複製過來,釋放之前的記憶體,在插入新增的元素;
- 對vector的任何操作,一旦引起空間重新配置,指向原vector的所有迭代器就都失效了 ;
- 初始時刻vector的capacity為0,塞入第一個元素後capacity增加為1;
- 不同的編譯器實現的擴容方式不一樣,VS2015中以1.5倍擴容,GCC以2倍擴容。
- vector在push_back以成倍增長可以在均攤後達到O(1)的事件複雜度,相對於增長指定大小的O(n)時間複雜度更好。
- 為了防止申請記憶體的浪費,現在使用較多的有2倍與1.5倍的增長方式,而1.5倍的增長方式可以更好的實現對記憶體的重複利用,因為更好。
參考連結:https://blog.csdn.net/yangshiziping/article/details/52550291
43.行內函數和巨集定義的區別
1.巨集定義不是函式,但是使用起來像函式。前處理器用複製巨集程式碼的方式代替函式的呼叫,省去了函式壓棧退棧過程,提高了效率。
行內函數本質上是一個函式,行內函數一般用於函式體的程式碼比較簡單的函式,不能包含複雜的控制語句,while、switch,並且行內函數本身不能直接呼叫自身。如果行內函數的函式體過大,編譯器會自動 的把這個行內函數變成普通函式。
2. 巨集定義是在預處理的時候把所有的巨集名用巨集體來替換,簡單的說就是字串替換
行內函數則是在編譯的時候進行程式碼插入,編譯器會在每處呼叫行內函數的地方直接把行內函數的內容展開,這樣可以省去函式的呼叫的開銷,提高效率
3. 巨集定義是沒有型別檢查的,無論對還是錯都是直接替換
行內函數在編譯的時候會進行型別的檢查,行內函數滿足函式的性質,比如有返回值、引數列表等
4. 巨集定義和行內函數使用的時候都是進行程式碼展開。不同的是巨集定義是在預編譯的時候把所有的巨集名替換,行內函數則是在編譯階段把所有呼叫行內函數的地方把行內函數插入。這樣可以省去函式壓棧退棧,提高了效率
44.行內函數與普通函式的區別
1. 行內函數和普通函式的引數傳遞機制相同,但是編譯器會在每處呼叫行內函數的地方將行內函數內容展開,這樣既避免了函式呼叫的開銷又沒有巨集機制的缺陷。
2. 普通函式在被呼叫的時候,系統首先要到函式的入口地址去執行函式體,執行完成之後再回到函式呼叫的地方繼續執行,函式始終只有一個複製。
行內函數不需要定址,當執行到行內函數的時候,將此函式展開,如果程式中有N次呼叫了行內函數則會有N次展開函式程式碼。
3. 行內函數有一定的限制,行內函數體要求程式碼簡單,不能包含複雜的結構控制語句。如果行內函數函式體過於複雜,編譯器將自動把行內函數當成普通函式來執行。
45.C++中成員函式能夠同時用static和const進行修飾?
不能。
C++編譯器在實現const的成員函式(const加在函式右邊)的時候為了確保該函式不能修改類的中引數的值,會在函式中新增一個隱式的引數const this*。但當一個成員為static的時候,該函式是沒有this指標的。也就是說此時const的用法和static是衝突的。
即:static修飾的函式表示該函式是屬於類的,而不是屬於某一個物件的,沒有this指標。const修飾的函式表示該函式不能改變this中的內容,會有一個隱含的const this指標。兩者是矛盾的。
46.溢位,越界,洩漏
1.溢位
要求分配的記憶體超出了系統能給你的,系統不能滿足需求,於是產生溢位。
1)棧溢位
a.棧溢位是指函式中的區域性變數造成的溢位(注:函式中形參和函式中的區域性變數存放在棧上)
棧的大小通常是1M-2M,所以棧溢位包含兩種情況,一是分配的的大小超過棧的最大值,二是分配的大小沒有超過最大值,但是接收的buff比新buff小(buff:緩衝區, 它本質上就是一段儲存資料的記憶體)
例子1:(分配的的大小超過棧的最大值)
void
{
char a[99999999999999999];
}
例子2:(接收的buff比新buff小)
void
{
char a[10] = {0};
strcpy(a, "abjjijjlljiojohihiihiiiiiiiiiiiiiiiiiiiiiiiiii");
}
注意:除錯時棧溢位的異常要在函式呼叫結束後才會檢測到,因為棧是在函式結束時才會開始進行出棧操作
如:
int main(int argc, char* argv[])
{
char a[10] = {0};
strcpy(a, "abjjijjlljiojohihiihiiiiiiiiiiiiiiiiiiiiiiiiii");
exit(0);
return 0;
}
上面情況是檢測不到棧溢位的,因為函式還沒執行完就退出了
void fun()
{
char a[10] = {0};
strcpy(a, "abjjijjlljiojohihiihiiiiiiiiiiiiiiiiiiiiiiiiii");
}
int main(int argc, char* argv[])
{
fun();
exit(0);
return 0;
}
這種情況呼叫完fun函式就會檢測到異常了
b.棧溢位的解決辦法
如果是超過棧的大小時,那就直接換成用堆;如果是不超過棧大小但是分配值小的,就增大分配的大小
2)記憶體溢位
使用malloc和new分配的記憶體,在拷貝時接收buff小於新buff時造成的現象
解決:增加分配的大小
2.越界
越界通常指的是陣列越界,如
char a[9]={0};
cout << a[9] << endl;
3.洩漏
這裡洩漏通常是指堆記憶體洩漏,是指使用malloc和new分配的記憶體沒有釋放造成的
47.C/C++中分配記憶體的方法
1) malloc 函式: void *malloc(unsigned int size)
在記憶體的動態分配區域中分配一個長度為size的連續空間,如果分配成功,則返回所分配記憶體空間的首地址,否則返回NULL,申請的記憶體不會進行初始化。
2)calloc 函式: void *calloc(unsigned int num, unsigned int size)
按照所給的資料個數和資料型別所佔位元組數,分配一個 num * size 連續的空間。
calloc申請記憶體空間後,會自動初始化記憶體空間為 0,但是malloc不會進行初始化,其記憶體空間儲存的是一些隨機資料。3)realloc 函式: void *realloc(void *ptr, unsigned int size)
動態分配一個長度為size的記憶體空間,並把記憶體空間的首地址賦值給ptr,把ptr記憶體空間調整為size。
申請的記憶體空間不會進行初始化。4)new是動態分配記憶體的運算子,自動計算需要分配的空間,在分配類型別的記憶體空間時,同時呼叫類的建構函式,對記憶體空間進行初始化,即完成類的初始化工作。動態分配內建型別是否自動初始化取決於變數定義的位置,在函式體外定義的變數都初始化為0,在函式體內定義的內建型別變數都不進行初始化。
48.建構函式初始化列表
建構函式初始化列表以一個冒號開始,接著是以逗號分隔的資料成員列表,每個資料成員後面跟一個放在括號中的初始化式。例如:
class CExample {
public:
int a;
float b;
//建構函式初始化列表
CExample(): a(0),b(8.8)
{}
//建構函式內部賦值
CExample()
{
a=0;
b=8.8;
}
};
上面的例子中兩個建構函式的結果是一樣的。上面的建構函式(使用初始化列表的建構函式)顯式的初始化類的成員;而沒使用初始化列表的建構函式是對類的成員賦值,並沒有進行顯式的初始化。
初始化和賦值對內建型別的成員沒有什麼大的區別,像上面的任一個建構函式都可以。對非內建型別成員變數,為了避免兩次構造,推薦使用類建構函式初始化列表。但有的時候必須用帶有初始化列表的建構函式:
1.成員型別是沒有預設建構函式的類。若沒有提供顯示初始化式,則編譯器隱式使用成員型別的預設建構函式,若類沒有預設建構函式,則編譯器嘗試使用預設建構函式將會失敗。
2.const成員或引用型別的成員。因為const物件或引用型別只能初始化,不能對他們賦值。
初始化資料成員與對資料成員賦值的含義是什麼?有什麼區別?
首先把資料成員按型別分類並分情況說明:
1.內建資料型別,複合型別(指標,引用)
在成員初始化列表和建構函式體內進行,在效能和結果上都是一樣的
2.使用者定義型別(類型別)
結果上相同,但是效能上存在很大的差別。因為類型別的資料成員物件在進入函式體前已經構造完成(先進行了一次隱式的預設建構函式呼叫),也就是說在成員初始化列表處進行構造物件的工作,呼叫建構函式,在進入函式體之後,進行的是對已經構造好的類物件的賦值,又呼叫了拷貝賦值操作符才能完成(如果並未提供,則使用編譯器提供的預設按成員賦值行為)。
49.vector中v[i]與v.at(i)的區別
void f(vector<int> &v)
{
v[5]; // A
v.at[5]; // B
}
如果v非空,A行和B行沒有任何區別。如果v為空,B行會丟擲std::out_of_range異常,A行的行為未定義。
c++標準不要求vector<T>::operator[]進行下標越界檢查,原因是為了效率,總是強制下標越界檢查會增加程式的效能開銷。設計vector是用來代替內建陣列的,所以效率問題也應該考慮。不過使用operator[]就要自己承擔越界風險了。
如果需要下標越界檢查,請使用at。但是請注意,這時候的效能也是響應的會受影響,因為越界檢查增加了效能的開銷。
50.指向函式的指標--函式指標
51.C++中呼叫C的函式
extern "C"
52.指標常量與常量指標
常量指標(被指向的物件是常量)
定義:又叫常指標,可以理解為常量的指標,指向的是個常量
關鍵點:
- 常量指標指向的物件不能通過這個指標來修改,可是仍然可以通過原來的宣告修改;
- 常量指標可以被賦值為變數的地址,之所以叫常量指標,是限制了通過這個指標修改變數的值;
- 指標還可以指向別處,因為指標本身只是個變數,可以指向任意地址;
const int *p或int const *p
(記憶技巧:const讀作常量,*讀作指標)
#include <stdio.h>
// 常量指標(被指向的物件是常量)
int main() {
int i = 10;
int i2 = 11;
const int *p = &i;
printf("%d\n", *p);//10
i = 9; //OK,仍然可以通過原來的宣告修改值,
//Error,*p是const int的,不可修改,即常量指標不可修改其指向地址
//*p = 11; //error: assignment of read-only location ‘*p’
p = &i2;//OK,指標還可以指向別處,因為指標只是個變數,可以隨意指向;
printf("%d\n", *p);//11
return 0;
}
指標常量(指標本身是常量)
定義:
本質是一個常量,而用指標修飾它。指標常量的值是指標,這個值因為是常量,所以不能被賦值。
關鍵點:
- 它是個常量!
- 指標所儲存的地址可以改變,然而指標所指向的值卻不可以改變;
- 指標本身是常量,指向的地址不可以變化,但是指向的地址所對應的內容可以變化;
int* const p;
//指標常量(指標本身是常量)
#include <stdio.h>
int main() {
int i = 10;
int *const p = &i;
printf("%d\n", *p);//10
//Error,因為p是const 指標,因此不能改變p指向的內容
//p++;//error: increment of read-only variable ‘p’
(*p)++; //OK,指標是常量,指向的地址不可以變化,但是指向的地址所對應的內容可以變化
printf("%d\n", *p);//11
i = 9;//OK,仍然可以通過原來的宣告修改值,
return 0;
}
53.防止標頭檔案被重複包含
54.詳解拷貝建構函式相關知識
非常好的一篇部落格:連結