C++面試常見問題一
轉自:https://oldpan.me/archives/c-interview-answer-1
原作者:[oldpan][https://oldpan.me/]
前言
這里收集市面上所有的關于算法和開發崗最容易遇到的關于C++方面的問題,問題信息來自互聯網以及牛客網的C++面試題目匯總。答題總結的順序大體是按照問題出現的頻率進行排序的,也有自己被面試問到的問題,越在前面的問題再面試中越容易被問到,作為筆記。當然,這些C++概念適合所有人,并非是準備面試或者正在面試的童鞋,如果想對C++多了解一些或者想避免一些C++常見錯誤的,可以建議看一看本系列文章的內容。
不論是算法崗還是開發崗,對于C++的要求還是比較高的,我們需要對C++的使用有著比較深入的理解。當然有一些知識不論是C++亦或是其他語言我們都需要明白一些編程語言的基本原理(例如封裝、多態等)。還有很多計算機系統或者操作系統等其他我們需要知道的知識。
1 static關鍵字的作用
static這個關鍵字的用法其實有很多,不論是普通的變量,還是成員變量、成員函數等,在不同的場景下用法也不同。一般有三個完全不同的含義:用在全局變量,表明這個變量在每個編譯單元有獨自的實例:
// foo.h
static int a = 123;
// foo.cpp
#include "foo.h"
int foo_func() { return a++; }
// bar.cpp
#include "foo.h"
int bar_func() { return a++; }
如果你分別編譯foo.cpp和bar.cpp,再把它們鏈接在一起,全局變量a會有兩份,那兩個函數會操縱不一樣的a實例。
用在函數里的局部變量,表明它的生存周期其實是全局變量,但僅在函數內可見:int get_global_id()
{static int seed = 0;return seed++;
}
每次訪問這個函數的時候,會獲得不同的int值。那個=0的操作僅在第一次訪問時執行,其實是初始化而不是賦值。
用在類成員,表明成員或者方法是類的,而不是對象實例的。
struct Foo
{int a = 0;static int aaa = 0;static int bbb() { return 123456; }
};
每個Foo實例會只含有一個int a。bbb方法通過Foo::bbb()調用。
上述的描述來源于:https://www.zhihu.com/question/29307292/answer/68695290
2 C++中四種cast轉換(或者說是顯式轉換)
類型轉換這個行為一旦被忽略就是發生不可描述的錯誤,小則一個小Bug,重則一個大型的車禍現象,我們平時一定要對轉型的行為進行重點關注,每次轉型的時候必須要明白這個轉型是否正確,當然平時我們如果使用比較好的IDE(例如CLion)進行編寫代碼的時候,其靜態代碼分析工具會幫你找到轉型可能會發生的問題,并提示你該行為可能造成的后果。
下面要介紹的四種cast轉換類型都是顯式轉化類型,這時的類型轉化是顯式的,是我們提前知道的。
static_cast
static_cast
這個指針比較常用,用于在編譯器階段直接告訴編譯器,你的類型轉化是在已知的情況下進行的,不需要編譯器為了這個向你提出警告了。
/*static_cast 告訴編譯器 這樣的類型轉換是已知的,不需要警告*/
union U { int a; double b; } u;
void* x = &u; // x 指向u指針
double* y = static_cast<double*>(x); // 此時y的值是u.b
char* z = static_cast<char*>(x); // z的值是u
const_cast
const_cast用于移除類型的const、volatile和__unaligned屬性。常量指針被轉換成非常量指針,并且仍然指向原來的對象;常量引用被轉換成非常量引用,并且仍然引用原來的對象。
const char *pc;
char *p = const_cast<char*>(pc);
dynamic_cast
相比static_cast,dynamic_cast會在運行時檢查類型轉換是否合法,具有一定的安全性。由于運行時的檢查,所以會額外消耗一些性能。dynamic_cast使用場景與static相似,在類層次結構中使用時,上行轉換和static_cast沒有區別,都是安全的;下行轉換時,dynamic_cast會檢查轉換的類型,相比static_cast更安全。
reinterpret_cast
非常激進的指針類型轉換,在編譯期完成,可以轉換任何類型的指針,所以極不安全。非極端情況不要使用。
int *ip;
char *pc = reinterpret_cast<char*>(ip);
四種cast在Pytorch-1.0源碼中的出現頻率為 static_cast > reinterpret_cast > const_cast > dynamic_cast。
最終還是建議大家看《C++ Primer》,在這本書的145頁較為詳細地介紹了顯式類型轉換的知識,但是這里也提到,強烈建議程序員避免使用強制類型轉換,對于這個我們還是能謹慎就謹慎些。
3 C++ 中指針和引用的區別
這個在知乎上有比較好的回答:
C++primer中對對象的定義:對象是指一塊能存儲數據并具有某種類型的內存空間一個對象a,它有值和地址&a,運行程序時,計算機會為該對象分配存儲空間,來存儲該對象的值,我們通過該對象的地址,來訪問存儲空間中的值。
指針p也是對象,它同樣有地址&p和存儲的值p,只不過,p存儲的數據類型是數據的地址。如果我們要以p中存儲的數據為地址,來訪問對象的值,則要在p前加解引用操作符”“,即p。
對象有常量(const)和變量之分,既然指針本身是對象,那么指針所存儲的地址也有常量和變量之分,常量指針是指,指針這個對象所存儲的地址是不可以改變的,而指向常量的指針的意思是,不能通過該指針來改變這個指針所指向的對象。
我們可以把引用理解成變量的別名。定義一個引用的時候,程序把該引用和它的初始值綁定在一起,而不是拷貝它。計算機必須在聲明r的同時就要對它初始化,并且,r一經聲明,就不可以再和其它對象綁定在一起了。
實際上,你也可以把引用看做是通過一個常量指針來實現的,它只能綁定到初始化它的對象上。
關于指針和引用的對比,可以參看<<more effective C++>>中的第一條條款,引用的一個優點是它一定不為空,因此相對于指針,它不用檢查它所指對象是否為空,這增加了效率比如下面的代碼
int a,b,*p,&r=a; //正確
r=3; //正確:等價于a=3
int &rr; //出錯:引用必須初始化
p=&a; //正確:p中存儲a的地址,即p指向a
*p=4; //正確:p中存的是a的地址,對a所對應的存儲空間存入值4
p=&b //正確:p可以多次賦值,p存儲b的地址
相關的鏈接:https://www.zhihu.com/question/37608201/answer/72766337
4 堆和棧的區別
這個問題很經典,網上也有一篇被轉過無數次的文章,這里搬運一下:
一個由C/C++編譯的程序占用的內存分為以下幾個部分:
- 棧區(stack):由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其 操作方式類似于數據結構中的棧。
- 堆區(heap):一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回 收 。注意它與數據結構中的堆是兩回事,分配方式倒是類似于鏈表,呵呵。
- 全局區(靜態區)(static):全局變量和靜態變量的存儲是放在一塊的,初始化的 全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另 一塊區域。 – 程序結束后由系統釋放。
- 文字常量區:常量字符串就是放在這里的。 程序結束后由系統釋放
- 程序代碼區:存放函數體的二進制代碼。
#include "stdio.h"int a = 0;//全局初始化區 char *p1;// 全局未初始化區
void main(void)
{ int b;// 棧 char s[] = "abc";// 棧 char *p2;// 棧 char *p3 = "123456";// 123456/0在常量區,p3在棧上。 static int c =0;// 全局(靜態)初始化區 p1 = (char *)malloc(10); p2 = (char *)malloc(20); //分配得來得10和20字節的區域就在堆區。 strcpy(p1, "123456");// 123456/0放在常量區,編譯器可能會將它與p3所指向的"123456" 優化成一個地方。
}
堆和棧的理論知識
申請方式
- stack: 由系統自動分配。例如,聲明在函數中一個局部變量
int b;
系統自動在棧中為b開辟空間 - heap: 需要程序員自己申請,并指明大小,在c中malloc函數,如
p1=(char *)malloc(10);
在C++中用new運算符,如p2 = new char[10];
但是注意p1、p2本身是在棧中的。
申請后系統的響應
- 棧:只要棧的剩余空間大于所申請空間,系統將為程序提供內存,否則將報異常提示棧溢 出。
- 堆:首先應該知道操作系統有一個記錄空閑內存地址的鏈表,當系統收到程序的申請時, 會遍歷該鏈表,尋找第一個空間大于所申請空間的堆結點,然后將該結點從空閑結點鏈表 中刪除,并將該結點的空間分配給程序,另外,對于大多數系統,會在這塊內存空間中的 首地址處記錄本次分配的大小,這樣,代碼中的delete語句才能正確的釋放本內存空間。 另外,由于找到的堆結點的大小不一定正好等于申請的大小,系統會自動的將多余的那部 分重新放入空閑鏈表中。
申請大小的限制
- 棧:在Windows下,棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意 思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是2M(也有 的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將 提示overflow。因此,能從棧獲得的空間較小。
- 堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由于系統是用鏈表來存儲 的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小 受限于計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。
申請效率的比較
- 棧由系統自動分配,速度較快。但程序員是無法控制的。
- 堆是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便. 另外,在WINDOWS下,最好的方式是用VirtualAlloc分配內存,他不是在堆,也不是在棧是 直接在進程的地址空間中保留一塊內存,雖然用起來最不方便。但是速度快,也最靈活。
堆和棧中的存儲內容
- 棧:在函數調用時,第一個進棧的是主函數中后的下一條指令(函數調用語句的下一條可 執行語句)的地址,然后是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧 的,然后是函數中的局部變量。注意靜態變量是不入棧的。 當本次函數調用結束后,局部變量先出棧,然后是參數,最后棧頂指針指向最開始存的地 址,也就是主函數中的下一條指令,程序由該點繼續運行。
- 堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容由程序員安排。
存取效率的比較
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在運行時刻賦值的;
而bbbbbbbbbbb是在編譯時就確定的;
但是,在以后的存取中,在棧上的數組比指針所指向的字符串(例如堆)快。
舉個例子:
#include "stdio.h" void main(void)
{ char a = 1; char c[] = "1234567890"; char *p ="1234567890"; a = c[1]; a = p[1]; return;
} // 對應的匯編代碼
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一種在讀取時直接就把字符串中的元素讀到寄存器cl中,而第二種則要先把指針值讀到edx中,再根據edx讀取字符,顯然慢了。
小結:堆和棧的區別可以用如下的比喻來看出: 使用棧就象我們去飯館里吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自 由度小。 使用堆就象是自己動手做喜歡吃的菜肴,比較麻煩,但是比較符合自己的口味,而且自由 度大。
來源:https://blog.csdn.net/hairetz/article/details/4141043
5 什么是虛函數,作用是什么
虛函數的作用核心是在運行期實現多態。
首先強調一個概念:定義一個函數為虛函數,不代表函數為不被實現的函數。
定義他為虛函數是為了允許用基類的指針來調用子類的這個函數。定義一個函數為純虛函數,才代表函數沒有被實現。定義純虛函數是為了實現一個接口,起到一個規范的作用,規范繼承這個類的程序員必須實現這個函數。
1、簡介假設我們有下面的類層次:
class A
{
public: virtual void foo() { cout<<"A::foo() is called"<<endl; }
};
class B:public A
{
public: void foo() { cout<<"B::foo() is called"<<endl; }
};
int main(void)
{ A *a = new B(); a->foo(); // 在這里,a雖然是指向A的指針,但是被調用的函數(foo)卻是B的! return 0;
}
這個例子是虛函數的一個典型應用,通過這個例子,也許你就對虛函數有了一些概念。它虛就虛在所謂“推遲聯編”或者“動態聯編”上,一個類函數的調用并不是在編譯時刻被確定的,而是在運行時刻被確定的。由于編寫代碼的時候并不能確定被調用的是基類的函數還是哪個派生類的函數,所以被成為“虛”函數。
虛函數只能借助于指針或者引用來達到多態的效果。
C++純虛函數
定義
純虛函數是在基類中聲明的虛函數,它在基類中沒有定義,但要求任何派生類都要定義自己的實現方法。在基類中實現純虛函數的方法是在函數原型后加“=0” virtual void funtion1()=0
引入原因
- 1、為了方便使用多態特性,我們常常需要在基類中定義虛擬函數。
- 2、在很多情況下,基類本身生成對象是不合情理的。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身生成對象明顯不合常理。
為了解決上述問題,引入了純虛函數的概念,將函數定義為純虛函數(方法:virtual ReturnType Function()= 0;),則編譯器要求在派生類中必須予以重寫以實現多態性。同時含有純虛擬函數的類稱為抽象類,它不能生成對象。這樣就很好地解決了上述兩個問題。聲明了純虛函數的類是一個抽象類。所以,用戶不能創建類的實例,只能創建它的派生類的實例。純虛函數最顯著的特征是:它們必須在繼承類中重新聲明函數(不要后面的=0,否則該派生類也不能實例化),而且它們在抽象類中往往沒有定義。定義純虛函數的目的在于,使派生類僅僅只是繼承函數的接口。純虛函數的意義,讓所有的類對象(主要是派生類對象)都可以執行純虛函數的動作,但類無法為純虛函數提供一個合理的缺省實現。所以類純虛函數的聲明就是在告訴子類的設計者,“你必須提供一個純虛函數的實現,但我不知道你會怎樣實現它”。
抽象類的介紹抽象類是一種特殊的類,它是為了抽象和設計的目的為建立的,它處于繼承層次結構的較上層
- (1)抽象類的定義: 稱帶有純虛函數的類為抽象類。
- (2)抽象類的作用:抽象類的主要作用是將有關的操作作為結果接口組織在一個繼承層次結構中,由它來為派生類提供一個公共的根,派生類將具體實現在其基類中作為接口的操作。所以派生類實際上刻畫了一組子類的操作接口的通用語義,這些語義也傳給子類,子類可以具體實現這些語義,也可以再將這些語義傳給自己的子類。
- (3)使用抽象類時注意:
- 抽象類只能作為基類來使用,其純虛函數的實現由派生類給出。如果派生類中沒有重新定義純虛函數,而只是繼承基類的純虛函數,則這個派生類仍然還是一個抽象類。如果派生類中給出了基類純虛函數的實現,則該派生類就不再是抽象類了,它是一個可以建立對象的具體的類。
- 抽象類是不能定義對象的。
總結:
- 1、純虛函數聲明如下: virtual void funtion1()=0; 純虛函數一定沒有定義,純虛函數用來規范派生類的行為,即接口。包含純虛函數的類是抽象類,抽象類不能定義實例,但可以聲明指向實現該抽象類的具體類的指針或引用。
- 2、虛函數聲明如下:virtual ReturnType FunctionName(Parameter);虛函數必須實現,如果不實現,編譯器將報錯,錯誤提示為:error LNK****: unresolved external symbol “public: virtual void __thiscall ClassName::virtualFunctionName(void)”
- 3、對于虛函數來說,父類和子類都有各自的版本。由多態方式調用的時候動態綁定。
- 4、實現了純虛函數的子類,該純虛函數在子類中就編程了虛函數,子類的子類即孫子類可以覆蓋該虛函數,由多態方式調用的時候動態綁定。
- 5、虛函數是C++中用于實現多態(polymorphism)的機制。核心理念就是通過基類訪問派生類定義的函數。
- 6、在有動態分配堆上內存的時候,析構函數必須是虛函數,但沒有必要是純虛的。
- 7、友元不是成員函數,只有成員函數才可以是虛擬的,因此友元不能是虛擬函數。但可以通過讓友元函數調用虛擬成員函數來解決友元的虛擬問題。
- 8、析構函數應當是虛函數,將調用相應對象類型的析構函數,因此,如果指針指向的是子類對象,將調用子類的析構函數,然后自動調用基類的析構函數。有純虛函數的類是抽象類,不能生成對象,只能派生。他派生的類的純虛函數沒有被改寫,那么,它的派生類還是個抽象類。定義純虛函數就是為了讓基類不可實例化化因為實例化這樣的抽象數據結構本身并沒有意義。或者給出實現也沒有意義。
實際上我個人認為純虛函數的引入,是出于兩個目的
- 1、為了安全,因為避免任何需要明確但是因為不小心而導致的未知的結果,提醒子類去做應做的實現。
- 2、為了效率,不是程序執行的效率,而是為了編碼的效率。
上述關于虛函數的描述來源于 https://www.zhihu.com/question/23971699/answer/69592611
6 內存泄漏和內存溢出的區別
內存泄漏-memory Leak是指程序中己動態分配的堆內存由于某種原因程序未釋放或無法釋放,造成系統內存
的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重后果。
很典型的,內存泄漏會導致可用的內存越來越少,最終我們發現沒有內存可以使用了。在C++中可以理解為我們使用new分配的內存用完必須釋放,在可分配內存遠大于泄漏內存時影響不是很大,但是如果反過來的話影響就大大的了。
而內存溢出就是內存越界,可以理解為我們使用了我們本不應該使用的內存,或者我們使用的內存的量遠遠大于我們本可以使用的內存量。有一種很常見的情況是我們調用的數組下標越界了,這個數組使用了本不應該使用的內存地址。另外還有調用棧溢出(stackoverflow),這種情況可以看成是棧內存不足的一種體現,當然內存溢出同時也包含緩沖區溢出(緩沖區也可以理解為我們開辟的數組,但是這個數組使用了它本不應該使用的區域,而這個區域我們可能放著其他一些重要的信息)。
7 c++中的smart pointer-智能指針
為什么要提到這些智能指針,其實說白了這些指針的出現都是因為內存管理一直比較難處理,我們稍微一不小會就會導致內存泄漏、懸掛指針或者訪問直接越界等等。畢竟C++是個可以折騰的語言,沒有C#后者Java的垃圾回收機制那么好用的功能。
一般來說,為了管理內存等資源,我們一般會采取RAII機制(Resource Acquisition Is Initialization),即資源獲取即初始化機制,也就是在類的構造函數里申請資源,然后使用,最終在析構函數中釋放資源。
但是如果我們使用new和delete去分配和回收空間的時候,難免會忘記在new之后delete掉之前已經分配的內存,這樣就會造成內存泄漏(上一點中的介紹)。為了緩解這個問題,就出現了智能指針,這些智能指針可以方便的管理一個對象的生命期。
智能指針有很多,其中最開始使用的是auto_ptr指針,但是這個指針因為有缺陷已經被廢棄。所以在新的C++標準中我們建議使用unique_ptr
、shared_ptr
、weak_ptr
以及intrusive_ptr
,這幾個指針都是比較常用的,都是輕量級對象,速度與原始的指針相差無幾,都是異常安全的,而且對于所指向的類型T只有一個要求,類型T的析構函數不能拋出異常(但是在實際工程的時候,在嵌入式等cpu比較弱的平臺使用這些智能指針需要好好考慮一下,另外如果你不懂得如何使用這些智能指針,那就別使用它們)。
scoped_ptr
其實還有一個叫做scoped_ptr
的智能指針,只不過在實際項目中見的比較少。這里簡單說說,它是比較簡單的一種智能指針,正如其名字所述,scoped_ptr所指向的對象在作用域之外會自動得到析構,scoped_ptr是non-copyable的,也就是說你不能去嘗試復制一個scoped_ptr的內容到另外一個scoped_ptr中,這也是為了防止錯誤的多次析構同一個指針所指向的對象,也就是說scoped_ptr
的所有權很嚴格,不能轉讓,一旦scoped_ptr
獲取了對象的管理權,我們就無法再從它那里取回來。
unique_ptr
unique_ptr
指針一般用于取代曾經的auto_ptr
,其基本功能與scoped_ptr
相當,同樣可以在作用域內管理指針,也不允許拷貝構造和賦值。但是unique_ptr
要比scoped_ptr
有更多的功能:可以像原始指針一樣進行比較,也可以像shared_ptr
定制刪除器,也可以安全地放入標準容器。我們完全可以用unique_ptr
去代替scoped_ptr
。
shared_ptr
這個指針是很常見的智能指針,有時候我們提到智能指針其實就是說這個指針。基本上很多大型項目都會用到這個指針(例如Pytorch、TVM)。同時這個指針也在C++11標準里頭,其實現的是引用計數型的智能指針,可以被自由地拷貝和賦值,隨便共享,當沒有代碼使用它(引用計數為0)的時候才會刪除被包裝的動態分配的對象。看到這里可以發現,這個思想已經被很多對象使用過了,很多大型項目中的類會自動添加引用機制,或者python語言中某個對象在沒有計數為0的時候也會進行垃圾回收。
weak_ptr
weak_ptr
通常不會單獨使用,是為了配合shared_ptr
而引入的一種智能指針,它不具有普通指針的行為,沒有重載operator*和->。它的大作用在于協助shared_ptr
工作,像旁觀者那樣觀測資源的使用情況。也就是說這個指針通常用于從shared_ptr
或者另一個weak_ptr
指針對象中構造,獲得資源的觀測權。但是其沒有共享指針,它的構造函數不會引起指針引用計數的增加。同樣,weak_ptr
析構時也不會導致引用計數減少,它只是一個觀察者。
intrusive_ptr
其名字和自身實現的功能其實并沒有強關系,我們不要因為名字而以為它一定要修改代理對象的內部數據。這個指針也是一種引用計數型的智能指針,但是與shared_ptr
不同,它比較適合現存代碼已經有了引用計數機制管理的對象,也就是它可以包裝已有對象從而得到與shared_ptr
類似的智能指針。
在Pytorch的源碼中,上述指針使用的頻率為shared_ptr > intrusive_ptr > unique_ptr >> weak_ptr
這里只對這些指針進行了基本的功能描述,如果想要好好學會怎么使用這些指針可能需要大量的篇幅才能說清楚,這里就不進行描述了,大家可以自行查閱相關的書籍,這里推薦一本《Boost程序庫完全開發指南》,當做文檔看就可以。
8 數組和指針的區別
同樣也是很經典的問題,在我們平常的認知中,數組頭相當于一個不可變動的指針,但具體是怎么樣的,又需要我們去好好琢磨一下:
數組:連續存儲的N個相同類型的變量,用變量類型和數組長度來區分數組類型;
指針:某個變量的內存地址,用變量類型來區分指針類型。
舉個例子,對數組取地址時,數組名不會被解釋為其地址。等等,數組名難道不被解釋為數組的地址嗎?不完全如此:數組名被解釋為其第一個元素的地址,而對數組名應用地址運算符(即&)時,得到的是整個數組的地址:
int test[5] = {1,2,3,4,5}; // 聲明一個長度為20字節的數組(int型變量大小為4字節)
cout << test << endl; // 顯示&test[0]
cout << &test << endl; // 顯示整個數組的地址
可以看到打印結果兩個地址是相同的,但是實際上這兩個地址代表的信息還是不一樣的。
0x7ffd0538aa70
0x7ffd0538aa70
None
從數字上說,這兩個地址相同;但從概念上說,&test[0](即test)是一個4字節內存塊的地址,而&test是一個20字節內存塊的地址。因此,表達式test+1將地址值加4,而表達式&test+1將地址值加20。換句話說,test是一個int指針(int*),而&test是一個指向包含5個元素的test數組的指針(int(*) [5])。
那我們可能會問,前面有關&test的類型描述是如何來的呢?我們可以這樣聲明和初始化這種指針:
int (*pas) [5] = &test; //pas指向一個有5個int元素的數組
如果省略括號,優先級規則將使pas先與[5]結合,導致pas是一個包含5個int型指針的數組,因此括號是必不可少的。其次,如果要描述變量的類型,可將聲明中的變量名刪除。因此,pas的類型為int(*) [5]
。另外,由于pas被設置為test,因此*pas
與test
等價,所以(*pas) [0]
為test數組的第一個元素。
那么此時int型變量的大小為4字節時,test是一個4字節內存塊的地址,而&test是一個20字節內存塊的地址。雖然這兩個內存塊的起始位置相同,但是大小不同。
我們打印一下地址變動來觀察一下:
cout << test + 1 << endl;
cout << &test + 1 << endl;
可以發現打印的結果為:
0x7ffd0538aa74 // 與之前0x7ffd0538aa70多了4個字節
0x7ffd0538aa84 // 與之前0x7ffd0538aa70多了20個字節(16進制表示法)
None
但是我們要注意一點,數組名并不是常量指針,首先我們要明白:
- 數組的類型是type[size],和常量指針類型type* const不同
- sizeof(數組名)等于數組所有元素的大小,而不是sizeof(指針);對數組取地址,得到的指針進行加減,增減字節數是sizeof(數組);你可以用字符串字面量初始化一個字符數組,但是不能用常量指針來初始化一個字符數組。
我們sizeof一下之前的test數組:
cout << sizeof(test) << endl;
可以發現這個輸出的長度為20,但是如果我們在平常的時候使用這個test的時候,這個test會被隱式轉化為首元素指針右值,上述那個test + 1
的操作,這個時候這個數組名稱已經轉化為了首元素的指針右值,然后加上一,自然就是這個元素(4個單位長度)再加上一個單位長度的地址。
而我們在這個數組名稱右面使用[]進行取址操作的時候,那么數組會被隱式轉換為首元素指針右值,然后對這個值進行解引用。也就是下面這個操作:
cout << test[1] << endl;
cout << *(test + 1) << endl; // 作為對比
可以發現上面這兩個的輸出是一致的,地址的偏移單位都是4個字節。
最后放一段代碼:
typedef char(*AP)[5];
AP foo(char* p) {for (int i = 0; i < 3; i++) {p[strlen(p)] = 'A';}return (AP)p+1;
}
int main() {char s[] = "FROG\0SEAL\0LION\0LAMB";puts(foo(s)[1] + 2);
}
大家覺得這段代碼應該輸出什么呢(ONALAMB)?
這里推薦一本講解指針比較好的書籍《C和指針》,更多關于指針的詳細操作和解釋可以查看這本書的相關部分。
9 關于賦值操作與自我賦值
賦值操作相關的問題在《劍指offer》和《efficient C++》中都出現過,具體在《劍指offer的》面試題1和《efficient C++》中的條款10和11中。我們如果要寫賦值代碼需要注意幾點:
- 賦值操作的返回值應該是一個引用,也就是
operator=
返回一個reference to *this
,這樣可以保證連續賦值。 - 傳入賦值函數的參數應該聲明為常量引用,并且加上const關鍵字。
- 賦值操作中自身內存空間的問題,在賦值新的內存空間之后之前的空間是否已經釋放,否則會造成空間泄漏。
- 要留意自我賦值的操作,如果發生了自我賦值,直接返回原本的指針即可。
《劍指offer》中的正確的賦值運算符的代碼:
CMyString& CMyString::operator=(const CMyString& str)
{if(this != &str) // 如果不等于自己{CMyString strTemp(str);char* pTemp = strTemp.m_pdata;strTemp.m_pdata = m_pdata;m_pdata = pTemp;}return *this;
}
10 代碼的質量與完整性
這個總結不涉及到任何需要動腦子的東西,我們只需要記住即可,但是最主要的還是在實際編寫代碼的過程中積累經驗,久而久之養成正確的習慣為好。我們在平時的過程中要注意一下幾個地方:
- 編程的命名,這個老生常談,駝峰型還是其他什么型,切記不要單個字母(
int a
)或者中文來命名(int ceshi
) - 代碼縮進,規范,看的清楚,注釋該有的地方有,不該有的地方別寫
- 代碼考慮的地方是否全面,比如你的函數的基本功嫩、輸入邊界值還有非法輸入是否能夠正確處理
- 處理錯誤的方法,采用什么樣的形式,返回值還是異常還是log之類的,總之方便調試即可
以上這些希望我們都可以在平時的時候注意,多學習別人的代碼,慢慢積累一些經驗。
關于更多代碼的規范,可以參考谷歌的C++編程規范,這也是世界上最好的C++編程規范之一。
未完待續
因為每個問題的內容都比較多,所以這里不會一下列舉完,每個問題都值得我們去思考和品位,這些總結會一直更新。