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.什么時候用到拷貝函數?
b.一個對象以值傳遞的方式從函數返回;
如果在類中沒有顯式地聲明一個拷貝構造函數,那么,編譯器將會自動生成一個默認的拷貝構造函數,該構造函數完成對象之間的位拷貝。位拷貝又稱淺拷貝;
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 連續的空間。
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.詳解拷貝構造函數相關知識
非常好的一篇博客:鏈接