哈嘍,大家好。最近幾天,我把去年秋招總結的筆試面試的一些內容,又進行了重新規划分類。詳細分成了簡歷書寫,面試技巧,面經總結,筆試面試八股文總結等四個部分。
其中,八股文又分成了C/C++,數據結構與算法分析,Arm體系與架構,Linux驅動開發,操作系統,網絡編程,名企筆試真題等七個部分。本次八股文更新,對於部分不合適的內容進行了刪減,新增了C++相關內容。
以上七個部分的內容,會同步更新在github,鏈接https://github.com/ZhongYi-LinuxDriverDev/EmbeddedSoftwareEngineerInterview 。希望大家能給個star支持下,讓我有繼續更新下去的動力。所有內容更新完成后,會將這些內容整合成PDF。話不多說,看下目錄。
預警:本文內容很長,很長,很長!沒耐心看完的建議直接跳轉github,獲取PDF下載方式。
C/C++
關鍵字
C語言宏中"#“和”##"的用法
- (#)字符串化操作符
作用:將宏定義中的傳入參數名轉換成用一對雙引號括起來參數名字符串。其只能用於有傳入參數的宏定義中,且必須置於宏定義體中的參數名前。
如:
#define example( instr ) printf( "the input string is:\t%s\n", #instr )
#define example1( instr ) #instr當使用該宏定義時:
example( abc ); // 在編譯時將會展開成:printf("the input string is:\t%s\n","abc")
string str = example1( abc ); // 將會展成:string str="abc"
- (##)符號連接操作符
作用:將宏定義的多個形參轉換成一個實際參數名。
如:
#define exampleNum( n ) num##n
使用:
int num9 = 9;
int num = exampleNum( 9 ); // 將會擴展成 int num = num9
注意:
a. 當用##連接形參時,##前后的空格可有可無。
如:
#define exampleNum( n ) num ## n
// 相當於 #define exampleNum( n ) num##n
b. 連接后的實際參數名,必須為實際存在的參數名或是編譯器已知的宏定義。
c. 如果##后的參數本身也是一個宏的話,##會阻止這個宏的展開。
#include <stdio.h>
#include <string.h>
#define STRCPY(a, b) strcpy(a ## _p, #b)
int main()
{
char var1_p[20];
char var2_p[30];
strcpy(var1_p, "aaaa");
strcpy(var2_p, "bbbb");
STRCPY(var1, var2);
STRCPY(var2, var1);
printf("var1 = %s\n", var1_p);
printf("var2 = %s\n", var2_p);
//STRCPY(STRCPY(var1,var2),var2);
//這里是否會展開為: strcpy(strcpy(var1_p,"var2")_p,"var2“)?答案是否定的:
//展開結果將是: strcpy(STRCPY(var1,var2)_p,"var2")
//## 阻止了參數的宏展開!如果宏定義里沒有用到 # 和 ##, 宏將會完全展開
// 把注釋打開的話,會報錯:implicit declaration of function 'STRCPY'
return 0;
}
結果:
var1 = var2
var2 = var1
關鍵字volatile有什么含意?並舉出三個不同的例子?
-
並行設備的硬件寄存器。存儲器映射的硬件寄存器通常加volatile,因為寄存器隨時可以被外設硬件修改。當聲明指向設備寄存器的指針時一定要用volatile,它會告訴編譯器不要對存儲在這個地址的數據進行假設。
-
一個中斷服務程序中修改的供其他程序檢測的變量。volatile提醒編譯器,它后面所定義的變量隨時都有可能改變。因此編譯后的程序每次需要存儲或讀取這個變量的時候,都會直接從變量地址中讀取數據。如果沒有volatile關鍵字,則編譯器可能優化讀取和存儲,可能暫時使用寄存器中的值,如果這個變量由別的程序更新了的話,將出現不一致的現象。
-
多線程應用中被幾個任務共享的變量。單地說就是防止編譯器對代碼進行優化.比如如下程序:
XBYTE[2]=0x55;
XBYTE[2]=0x56;
XBYTE[2]=0x57;
XBYTE[2]=0x58;
對外部硬件而言,上述四條語句分別表示不同的操作,會產生四種不同的動作,但是編譯器卻會對上述四條語句進行優化,認為只有XBYTE[2]=0x58(即忽略前三條語句,只產生一條機器代碼)。如果鍵入volatile,編譯器會逐一的進行編譯並產生相應的機器代碼(產生四條代碼)。
關鍵字static的作用是什么?
-
在函數體,只會被初始化一次,一個被聲明為靜態的變量在這一函數被調用過程中維持其值不變。
-
在模塊內(但在函數體外),一個被聲明為靜態的變量可以被模塊內所用函數訪問,但不能被模塊外其它函數訪問。它是一個本地的全局變量(只能被當前文件使用)。
-
在模塊內,一個被聲明為靜態的函數只可被這一模塊內的其它函數調用。那就是,這個函數被限制在聲明它的模塊的本地范圍內使用(只能被當前文件使用)。
在C語言中,為什么 static變量只初始化一次?
對於所有的對象(不僅僅是靜態對象),初始化都只有一次,而由於靜態變量具有“記憶”功能,初始化后,一直都沒有被銷毀,都會保存在內存區域中,所以不會再次初始化。存放在靜態區的變量的生命周期一般比較長,它與整個程序“同生死、共存亡”,所以它只需初始化一次。而auto變量,即自動變量,由於它存放在棧區,一旦函數調用結束,就會立刻被銷毀。
extern”C” 的作用是什么?
extern "C"的主要作用就是為了能夠正確實現C++代碼調用其他C語言代碼。加上extern "C"后,會指示編譯器這部分代碼按C語言的進行編譯,而不是C++的。
const有什么作用?
- 定義變量(局部變量或全局變量)為常量,例如:
const int N=100;//定義一個常量N
N=50; //錯誤,常量的值不能被修改
const int n; //錯誤,常量在定義的時候必須初始化
-
修飾函數的參數,表示在函數體內不能修改這個參數的值。
-
修飾函數的返回值。
a.如果給用 const修飾返回值的類型為指針,那么函數返回值(即指針)的內容是不能被修改的,而且這個返回值只能賦給被 const修飾的指針。例如:
const char GetString() //定義一個函數 char *str= GetString() //錯誤,因為str沒有被 const修飾 const char *str=GetString() //正確
b.如果用 const修飾普通的返回值,如返回int變量,由於這個返回值是一個臨時變量,在函數調用結束后這個臨時變量的生命周期也就結束了,因此把這些返回值修飾為 const是沒有意義的。
-
節省空間,避免不必要的內存分配。例如:
#define PI 3.14159//該宏用來定義常量 const doulbe Pi=3.14159//此時並未將P放入只讀存儲器中 double i=Pi//此時為Pi分配內存,以后不再分配 double I=PI//編譯期間進行宏替換,分配內存 double j=Pi//沒有內存分配再次進行宏替換,又一次分配內存
什么情況下使用const關鍵字?
- 修飾一般常量。一般常量是指簡單類型的常量。這種常量在定義時,修飾符const可以用在類型說明符前,也可以用在類型說明符后。例如:
int const x=2;const int x=2
- 修飾常數組。定義或說明一個常數組可以采用如下格式:
int const a[8]={1,2,3,4,5,6,7,8}
const int a[8]={1,2,3,4,5,6,7,8}
- 修飾常對象。常對象是指對象常量,定義格式如下:
class A:
const A a:
A const a:
定義常對象時,同樣要進行初始化,並且該對象不能再被更新。修飾符 const可以放在類名后面,也可以放在類名前面。
- 修飾常指針
const int*p; //常量指針,指向常量的指針。即p指向的內存可以變,p指向的數值內容不可變
int const*p; //同上
int*const p;//指針常量,本質是一個常量,而用指針修飾它。 即p指向的內存不可以變,但是p內存位置的數值可以變
const int* const p;//指向常量的常量指針。即p指向的內存和數值都不可變
-
修飾常引用。被 const修飾的引用變量為常引用,一旦被初始化,就不能再指向其他對象了。
-
修飾函數的常參數。 const修飾符也可以修飾函數的傳遞參數,格式如下:
void Fun(const int Var)
告訴編譯器Var在函數體中不能被改變,從而防止了使用者一些無意的或錯誤的修改。
- 修飾函數的返回值。 const修飾符也可以修飾函數的返回值,表明該返回值不可被改變,格式如下:
const int FunI();
const MyClass Fun2();
- 在另一連接文件中引用 const常量。使用方式有
extern const int 1:
extern const int j=10;
new/delete與malloc/free的區別是什么?
-
new、delete是C++中的操作符,而malloc和free是標准庫函數。
-
對於非內部數據對象來說,只使用malloc是無法完成動態對象要求的,一般在創建對象時需要調用構造函數,對象消亡時,自動的調用析構函數。而malloc free是庫函數而不是運算符,不在編譯器控制范圍之內,不能夠自動調用構造函數和析構函數。而NEW在為對象申請分配內存空間時,可以自動調用構造函數,同時也可以完成對對象的初始化。同理,delete也可以自動調用析構函數。而mallloc只是做一件事,只是為變量分配了內存,同理,free也只是釋放變量的內存。
-
new返回的是指定類型的指針,並且可以自動計算所申請內存的大小。而 malloc需要我們計算申請內存的大小,並且在返回時強行轉換為實際類型的指針。
strlen("\0") =? sizeof("\0")=?
strlen("\0") =0,sizeof("\0")=2。
strlen用來計算字符串的長度(在C/C++中,字符串是以"\0"作為結束符的),它從內存的某個位置(可以是字符串開頭,中間某個位置,甚至是某個不確定的內存區域)開始掃描直到碰到第一個字符串結束符\0為止,然后返回計數器值sizeof是C語言的關鍵字,它以字節的形式給出了其操作數的存儲大小,操作數可以是一個表達式或括在括號內的類型名,操作數的存儲大小由操作數的類型決定。
sizeof和strlen有什么區別?
strlen與 sizeof的差別表現在以下5個方面。
-
sizeof是運算符(是不是被弄糊塗了?事實上, sizeof既是關鍵字,也是運算符,但不是函數),而strlen是函數。 sizeof后如果是類型,則必須加括弧,如果是變量名,則可以不加括弧。
-
sizeof運算符的結果類型是 size_t,它在頭文件中 typedef為 unsigned int類型。該類型保證能夠容納實現所建立的最大對象的字節大小
-
sizeof可以用類型作為參數, strlen只能用char*作參數,而且必須是以“0結尾的。 sizeof還可以以函數作為參數,如int g(),則 sizeof(g())的值等於 sizeof( int的值,在32位計算機下,該值為4。
-
大部分編譯程序的 sizeof都是在編譯的時候計算的,所以可以通過 sizeof(x)來定義數組維數。而 strlen則是在運行期計算的,用來計算字符串的實際長度,不是類型占內存的大小。例如, char str[20] = "0123456789”,字符數組str是編譯期大小已經固定的數組,在32位機器下,為 sizeof(char)*20=20,而其 strlen大小則是在運行期確定的,所以其值為字符串的實際長度10。
-
當數組作為參數傳給函數時,傳遞的是指針,而不是數組,即傳遞的是數組的首地址。
不使用 sizeof,如何求int占用的字節數?
#include <stdio.h>
#define MySizeof(Value) (char *)(&value+1)-(char*)&value
int main()
{
int i ;
double f;
double *q;
printf("%d\r\n",MySizeof(i));
printf("%d\r\n",MySizeof(f));
printf("%d\r\n",MySizeof(a));
printf("%d\r\n",MySizeof(q));
return 0;
}
輸出為:
4 8 32 4
上例中,(char*)& Value
返回 Value的地址的第一個字節,(char*)(& Value+1)
返回value的地址的下一個地址的第一個字節,所以它們之差為它所占的字節數。
C語言中 struct與 union的區別是什么?
struct(結構體)與 union(聯合體)是C語言中兩種不同的數據結構,兩者都是常見的復合結構,其區別主要表現在以下兩個方面。
-
結構體與聯合體雖然都是由多個不同的數據類型成員組成的,但不同之處在於聯合體中所有成員共用一塊地址空間,即聯合體只存放了一個被選中的成員,而結構體中所有成員占用空間是累加的,其所有成員都存在,不同成員會存放在不同的地址。在計算一個結構型變量的總長度時,其內存空間大小等於所有成員長度之和(需要考慮字節對齊),而在聯合體中,所有成員不能同時占用內存空間,它們不能同時存在,所以一個聯合型變量的長度等於其最長的成員的長度。
-
對於聯合體的不同成員賦值,將會對它的其他成員重寫,原來成員的值就不存在了,而對結構體的不同成員賦值是互不影響的。
舉個例子。下列代碼執行結果是多少?
typedef union {double i; int k[5]; char c;}DATE;
typedef struct data( int cat; DATE cow;double dog;)too;
DATE max;
printf ("%d", sizeof(too)+sizeof(max));
假設為32位機器,int型占4個字節, double型占8個字節,char型占1個字節,而DATE是一個聯合型變量,聯合型變量共用空間,uion里面最大的變量類型是int[5],所以占用20個字節,它的大小是20,而由於 union中 double占了8個字節,因此 union是要8個字節對齊,所占內存空間為8的倍數。為了實現8個字節對齊,所占空間為24.而data是一個結構體變量,每個變量分開占用空間,依次為 sizeof(int)+ sizeof(DATE)+ sizeof( double)=4+24+8=36按照8字節對齊,占用空間為40,所以結果為40+24=64。
左值和右值是什么?
左值是指可以出現在等號左邊的變量或表達式,它最重要的特點就是可寫(可尋址)。也就是說,它的值可以被修改,如果一個變量或表達式的值不能被修改,那么它就不能作為左值。
右值是指只可以出現在等號右邊的變量或表達式。它最重要的特點是可讀。一般的使用場景都是把一個右值賦值給一個左值。
通常,左值可以作為右值,但是右值不一定是左值。
什么是短路求值?
#include <stdio.h>
int main()
{
int i = 6;
int j = 1;
if(i>0||(j++)>0);
printf("%D\r\n",j);
return 0;
}
輸出結果為1。
輸出為什么不是2,而是1呢?其實,這里就涉及一個短路計算的問題。由於i語句是個條件判斷語句,里面是有兩個簡單語句進行或運算組合的復合語句,因為或運算中,只要參與或運算的兩個表達式的值都為真,則整個運算結果為真,而由於變量i的值為6,已經大於0了,而該語句已經為true,則不需要執行后續的j+操作來判斷真假,所以后續的j++操作不需要執行,j的值仍然為1。
因為短路計算的問題,對於&&操作,由於在兩個表達式的返回值中,如果有一個為假則整個表達式的值都為假,如果前一個語句的返回值為 false,則無論后一個語句的返回值是真是假,整個條件判斷都為假,不用執行后一個語句,而a>b的返回值為 false,程序不執行表達式n=c>d,所以,n的值保持為初值2。
++a和a++有什么區別?兩者是如何實現的?
a++的具體運算過程為
int temp = a;
a=a+1;
return temp;
++a的具體運算過程為
a=a+1;
return a;
后置自增運算符需要把原來變量的值復制到一個臨時的存儲空間,等運算結束后才會返回這個臨時變量的值。所以前置自增運算符效率比后置自增要高
內存
C語言中內存分配的方式有幾種?
- 靜態存儲區分配
內存分配在程序編譯之前完成,且在程序的整個運行期間都存在,例如全局變量、靜態變量等。
- 棧上分配
在函數執行時,函數內的局部變量的存儲單元在棧上創建,函數執行結束時這些存儲單元自動釋放。
- 堆上分配
堆與棧有什么區別?
-
申請方式
棧的空間由操作系統自動分配/釋放,堆上的空間手動分配/釋放。
-
申請大小的限制
棧空間有限。在Windows下,棧是向低地址擴展的數據結 構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是2M(也有的說是1M,總之是 一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能從棧獲得的空間較小堆是很大的自由存儲區。堆是向高地址擴展的數據結構,是不連續的內存區域。這是由於系統是用鏈表來存儲的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。
-
申請效率
棧由系統自動分配,速度較快。但程序員是無法控制的。堆是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便.
棧在C語言中有什么作用?
-
C語言中棧用來存儲臨時變量,臨時變量包括函數參數和函數內部定義的臨時變量。函數調用中和函數調用相關的函數返回地址,函數中的臨時變量,寄存器等均保存在棧中,函數調動返回后從棧中恢復寄存器和臨時變量等函數運行場景。
-
多線程編程的基礎是棧,棧是多線程編程的基石,每一個線程都最少有一個自己專屬的棧,用來存儲本線程運行時各個函數的臨時變量和維系函數調用和函數返回時的函數調用關系和函數運行場景。 操作系統最基本的功能是支持多線程編程,支持中斷和異常處理,每個線程都有專屬的棧,中斷和異常處理也具有專屬的棧,棧是操作系統多線程管理的基石。
C語言函數參數壓棧順序是怎樣的?
從右至左。
C語言參數入棧順序的好處就是可以動態變化參數個數。自左向右的入棧方式,最前面的參數被壓在棧底。除非知道參數個數,否則是無法通過棧指針的相對位移求得最左邊的參數。這樣就變成了左邊參數的個數不確定,正好和動態參數個數的方向相反。因此,C語言函數參數采用自右向左的入棧順序,主要原因是為了支持可變長參數形式。
C++如何處理返回值?
C++函數返回可以按值返回和按常量引用返回,偶爾也可以按引址返回。多數情況下不要使用引址返回。
C++中拷貝賦值函數的形參能否進行值傳遞?
不能。如果是這種情況下,調用拷貝構造函數的時候,首先要將實參傳遞給形參,這個傳遞的時候又要調用拷貝構造函數(aa = ex.aa; //此處調用拷貝構造函數)。如此循環,無法完成拷貝,棧也會滿。
class Example
{
public:
Example(int a):aa(a) {} //構造函數
Example(Example &ex) //拷貝構造函數(引用傳遞參數)
{
aa = ex.aa; //此處調用拷貝構造函數
}
private:
int aa;
};
int main()
{
Example e1(10);
Example e2 = e1;
return 0;
}
C++的內存管理是怎樣的?
在C++中,虛擬內存分為代碼段、數據段、BSS段、堆區、文件映射區以及棧區六部分。
代碼段:包括只讀存儲區和文本區,其中只讀存儲區存儲字符串常量,文本區存儲程序的機器代碼。
數據段:存儲程序中已初始化的全局變量和靜態變量
BSS 段:存儲未初始化的全局變量和靜態變量(局部+全局),以及所有被初始化為0的全局變量和靜態變量。
堆區:調用new/malloc函數時在堆區動態分配內存,同時需要調用delete/free來手動釋放申請的內存。
映射區:存儲動態鏈接庫以及調用mmap函數進行的文件映射
棧:使用棧空間存儲函數的返回地址、參數、局部變量、返回值
什么是內存泄漏?
簡單地說就是申請了一塊內存空間,使用完畢后沒有釋放掉。
它的一般表現方式是程序運行時間越長,占用內存越多,最終用盡全部內存,整個系統崩潰。由程序申請的一塊內存,且沒有任何一個指針指向它,那么這塊內存就泄露了。
如何判斷內存泄漏?
-
良好的編碼習慣,盡量在涉及內存的程序段,檢測出內存泄露。當程式穩定之后,在來檢測內存泄露時,無疑增加了排除的困難和復雜度。使用了內存分配的函數,一旦使用完畢,要記得要使用其相應的函數釋放掉。
-
將分配的內存的指針以鏈表的形式自行管理,使用完畢之后從鏈表中刪除,程序結束時可檢查改鏈表。
-
Boost 中的smart pointer。
-
一些常見的工具插件,如ccmalloc、Dmalloc、Leaky等等。
指針
數組指針和指針數組有什么區別?
數組指針就是指向數組的指針,它表示的是一個指針,這個指針指向的是一個數組,它的重點是指針。例如,int(*pa)[8]
聲明了一個指針,該指針指向了一個有8個int型元素的數組。下面給出一個數組指針的示例。
#include <stdio. h>
#include <stdlib. h>
void main()
{
int b[12]={1,2,3,4,5,6,7,8,9,10,11,12};
int (*p)[4];
p = b;
printf("%d\n", **(++p);
}
程序的輸出結果為 5。
上例中,p是一個數組指針,它指向一個包含有4個int類型數組的指針,剛開始p被初始化為指向數組b的首地址,++p相當於把p所指向的地址向后移動4個int所占用的空間,此時p指向數組{5,6,7,8},語句*(++p);
表示的是這個數組中第一個元素的地址(可以理解p為指向二維數組的指針,{1,2,3,4},{5,6,7,8},{9,10,11,12}。p指向的就是{1,2,3,4}的地址,*p
就是指向元素,{1,2,3,4},**p
指向的就是1),語句**(++p)會輸出這個數組的第一個元素5。
指針數組表示的是一個數組,而數組中的元素是指針。下面給出另外一個指針數組的示例
#include <stdio.h>
int main()
{
int i;
int *p[4];
int a[4]={1,2,3,4};
p[0] = &a[0];
p[1] = &a[1];
p[2] = &a[2];
p[3] = &a[3];
for(i=0;i<4;i++)
printf("%d",*p[i]);
printf("\n");
return 0;
}
程序的輸出結果為1234。
函數指針和指針函數有什么區別?
- 函數指針
如果在程序中定義了一個函數,那么在編譯時系統就會為這個函數代碼分配一段存儲空間,這段存儲空間的首地址稱為這個函數的地址。而且函數名表示的就是這個地址。既然是地址我們就可以定義一個指針變量來存放,這個指針變量就叫作函數指針變量,簡稱函數指針。
int(*p)(int, int);
這個語句就定義了一個指向函數的指針變量 p。首先它是一個指針變量,所以要有一個“*”,即(*p)
;其次前面的 int 表示這個指針變量可以指向返回值類型為 int 型的函數;后面括號中的兩個 int 表示這個指針變量可以指向有兩個參數且都是 int 型的函數。所以合起來這個語句的意思就是:定義了一個指針變量 p,該指針變量可以指向返回值類型為 int 型,且有兩個整型參數的函數。p 的類型為 int(*)(int,int)
。
我們看到,函數指針的定義就是將“函數聲明”中的“函數名”改成“(指針變量名)”。但是這里需要注意的是:“(指針變量名)”兩端的括號不能省略,括號改變了運算符的優先級。如果省略了括號,就不是定義函數指針而是一個函數聲明了,即聲明了一個返回值類型為指針型的函數。
最后需要注意的是,指向函數的指針變量沒有 ++ 和 -- 運算。
# include <stdio.h>
int Max(int, int); //函數聲明
int main(void)
{
int(*p)(int, int); //定義一個函數指針
int a, b, c;
p = Max; //把函數Max賦給指針變量p, 使p指向Max函數
printf("please enter a and b:");
scanf("%d%d", &a, &b);
c = (*p)(a, b); //通過函數指針調用Max函數
printf("a = %d\nb = %d\nmax = %d\n", a, b, c);
return 0;
}
int Max(int x, int y) //定義Max函數
{
int z;
if (x > y)
{
z = x;
}
else
{
z = y;
}
return z;
}
- 指針函數
首先它是一個函數,只不過這個函數的返回值是一個地址值。函數返回值必須用同類型的指針變量來接受,也就是說,指針函數一定有“函數返回值”,而且,在主調函數中,函數返回值必須賦給同類型的指針變量。
類型名 *函數名(函數參數列表);
其中,后綴運算符括號“()”表示這是一個函數,其前綴運算符星號“*”表示此函數為指針型函數,其函數值為指針,即它帶回來的值的類型為指針,當調用這個函數后,將得到一個“指向返回值為…的指針(地址),“類型名”表示函數返回的指針指向的類型”。
“(函數參數列表)”中的括號為函數調用運算符,在調用語句中,即使函數不帶參數,其參數表的一對括號也不能省略。其示例如下:
int *pfun(int, int);
由於“*”的優先級低於“()”的優先級,因而pfun首先和后面的“()”結合,也就意味着,pfun是一個函數。即:
int *(pfun(int, int));
接着再和前面的“*”結合,說明這個函數的返回值是一個指針。由於前面還有一個int,也就是說,pfun是一個返回值為整型指針的函數。
#include <stdio.h>
float *find(float(*pionter)[4],int n);//函數聲明
int main(void)
{
static float score[][4]={{60,70,80,90},{56,89,34,45},{34,23,56,45}};
float *p;
int i,m;
printf("Enter the number to be found:");
scanf("%d",&m);
printf("the score of NO.%d are:\n",m);
p=find(score,m-1);
for(i=0;i<4;i++)
printf("%5.2f\t",*(p+i));
return 0;
}
float *find(float(*pionter)[4],int n)/*定義指針函數*/
{
float *pt;
pt=*(pionter+n);
return(pt);
}
共有三個學生的成績,函數find()被定義為指針函數,其形參pointer是指針指向包含4個元素的一維數組的指針變量。pointer+n指向score的第n+1行。*(pointer+1)指向第一行的第0個元素。pt是一個指針變量,它指向浮點型變量。main()函數中調用find()函數,將score數組的首地址傳給pointer。
數組名和指針的區別與聯系是什么?
- 數據保存方面
指針保存的是地址(保存目標數據地址,自身地址由編譯器分配),內存訪問偏移量為4個字節,無論其中保存的是何種數據均已地址類型進行解析。
數組保存的數據。數組名表示的是第一個元素的地址,內存偏移量是保存數據類型的內存偏移量;只有對數組名取地址(&數組名)時數組名才表示整個數組,內存偏移量是整個數組的大小(sizeof(數組名))。
- 數據訪問方面
指針對數據的訪問方式是間接訪問,需要用到解引用符號(*數組名)。
數組對數據的訪問則是直接訪問,可通過下標訪問或數組名+元素偏移量的方式
- 使用環境
指針多用於動態數據結構(如鏈表,等等)和動態內存開辟。
數組多用於存儲固定個數且類型統一的數據結構(如線性表等等)和隱式分配。
指針進行強制類型轉換后與地址進行加法運算,結果是什么?
假設在32位機器上,在對齊為4的情況下,sizeof(long)的結果為4字節,sizeof(char*)的結果為4字節,sizeof(short int)的結果與 sizeof(short)的結果都為2字節, sizeof(char)的結果為1字節, sizeof(int)的結果為4字節,由於32位機器上是4字節對齊,以如下結構體為例:
struct BBB
{
long num;
char *name;
short int data;
char ha;
short ba[5];
}*p;
當p=0x100000;
則p+0×200=? (ulong)p+0x200=? (char*)p+0x200=?
其實,在32位機器下,sizeof(struct BBB)=sizeof(*p)=4+4+2+2+1+3/*補齊*/+2*5+2/*補齊*/=24字節
,而p=0x100000
,那么p+0x200=0x1000000+0x200*24
指針加法,加出來的是指針所指類型的字節長度的整倍數,就是p偏移sizeof(p)*0x200。
(ulong)p+0x200=0x10000010+0x200
經過ulong后,已經不再是指針加法,而變成一個數值加法了。
(char*)p+0x200=0x1000000+0×200*sizeof(char)
結果類型是char*。
常量指針,指向常量的指針,指向常量的常量指針有什么區別?
- 常量指針
int * const p
先看const再看 * ,是p是一個常量類型的指針,不能修改這個指針的指向,但是這個指針所指向的地址上存儲的值可以修改。
- 指向常量的指針
const int *p
先看*再看const,定義一個指針指向一個常量,不能通過指針來修改這個指針指向的值
- 指向常量的常量指針
const int *const p
對於“指向常量的常量指針”,就必須同時滿足上述1和2中的內容,既不可以修改指針的值,也不可以修改指針指向的值。
指針和引用的異同是什么?如何相互轉換?
相同
-
都是地址的概念,指針指向某一內存、它的內容是所指內存的地址;引用則是某塊內存的別名。
-
從內存分配上看:兩者都占內存,程序為指針會分配內存,一般是4個字節;而引用的本質是指針常量,指向對象不能變,但指向對象的值可以變。兩者都是地址概念,所以本身都會占用內存。
區別
-
指針是實體,而引用是別名。
-
指針和引用的自增(++)運算符意義不同,指針是對內存地址自增,而引用是對值的自增。
-
引用使用時無需解引用(*),指針需要解引用;(關於解引用大家可以看看這篇博客,傳送門)。
-
引用只能在定義時被初始化一次,之后不可變;指針可變。
-
引用不能為空,指針可以為空。
-
“sizeof 引用”得到的是所指向的變量(對象)的大小,而“sizeof 指針”得到的是指針本身的大小,在32位系統指針變量一般占用4字節內存。
#include "stdio.h"
int main(){
int x = 5;
int *p = &x;
int &q = x;
printf("%d %d\n",*p,sizeof(p));
printf("%d %d\n",q,sizeof(q));
}
//結果
5 8
5 4
由結果可知,引用使用時無需解引用(*),指針需要解引用;我用的是64位操作系統,“sizeof 指針”得到的是指針本身的大小,及8個字節。而“sizeof 引用”得到的是的對象本身的大小及int的大小,4個字節。
轉換
-
指針轉引用:把指針用*就可以轉換成對象,可以用在引用參數當中。
-
引用轉指針:把引用類型的對象用&取地址就獲得指針了。
int a = 5;
int *p = &a;
void fun(int &x){}//此時調用fun可使用 : fun(*p);
//p是指針,加個*號后可以轉換成該指針指向的對象,此時fun的形參是一個引用值,
//p指針指向的對象會轉換成引用X。
野指針是什么?
-
野指針是指向不可用內存的指針,當指針被創建時,指針不可能自動指向NULL,這時,默認值是隨機的,此時的指針成為野指針。
-
當指針被free或delete釋放掉時,如果沒有把指針設置為NULL,則會產生野指針,因為釋放掉的僅僅是指針指向的內存,並沒有把指針本身釋放掉。
-
第三個造成野指針的原因是指針操作超越了變量的作用范圍。
如何避免野指針?
- 對指針進行初始化。
//將指針初始化為NULL。
char * p = NULL;
//用malloc分配內存
char * p = (char * )malloc(sizeof(char));
//用已有合法的可訪問的內存地址對指針初始化
char num[ 30] = {0};
char *p = num;
- 指針用完后釋放內存,將指針賦NULL。
delete(p);
p = NULL;
注:malloc函數分配完內存后需注意:
a. 檢查是否分配成功(若分配成功,返回內存的首地址;分配不成功,返回NULL。可以通過if語句來判斷)
b. 清空內存中的數據(malloc分配的空間里可能存在垃圾值,用memset或bzero 函數清空內存)
//s是 需要置零的空間的起始地址; n是 要置零的數據字節個數。
void bzero(void *s, int n);
// 如果要清空空間的首地址為p,value為值,size為字節數。
void memset(void *start, int value, int size);
C++中的智能指針是什么?
智能指針是一個類,用來存儲指針(指向動態分配對象的指針)。
C++程序設計中使用堆內存是非常頻繁的操作,堆內存的申請和釋放都由程序員自己管理。程序員自己管理堆內存可以提高了程序的效率,但是整體來說堆內存的管理是麻煩的,C++11中引入了智能指針的概念,方便管理堆內存。使用普通指針,容易造成堆內存泄露(忘記釋放),二次釋放,程序發生異常時內存泄露等問題等,使用智能指針能更好的管理堆內存。
智能指針的內存泄漏如何解決?
為了解決循環引用導致的內存泄漏,引入了弱指針weak_ptr
,weak_ptr
的構造函數不會修改引用計數的值,從而不會對對象的內存進行管理,其類似一個普通指針,但是不會指向引用計數的共享內存,但是可以檢測到所管理的對象是否已經被釋放,從而避免非法訪問。
預處理
預處理器標識#error的目的是什么?
#error預處理指令的作用是,編譯程序時,只要遇到#error就會生成一個編譯錯誤提示消息,並停止編譯。其語法格式為:#error error-message。
下面舉個例子:
程序中往往有很多的預處理指令
#ifdef XXX
...
#else
#endif
當程序比較大時,往往有些宏定義是在外部指定的(如makefile),或是在系統頭文件中指定的,當你不太確定當前是否定義了 XXX 時,就可以改成如下這樣進行編譯:
#ifdef XXX
...
#error "XXX has been defined"
#else
#endif
這樣,如果編譯時出現錯誤,輸出了XXX has been defined,表明宏XXX已經被定義了。
定義常量誰更好?# define還是 const?
尺有所短,寸有所長, define與 const都能定義常量,效果雖然一樣,但是各有側重。
define既可以替代常數值,又可以替代表達式,甚至是代碼段,但是容易出錯,而 const的引入可以增強程序的可讀性,它使程序的維護與調試變得更加方便。具體而言,它們的差異主要表現在以下3個方面。
-
define只是用來進行單純的文本替換, define常量的生命周期止於編譯期,不分配內存空間,它存在於程序的代碼段,在實際程序中,它只是一個常數;而 const常量存在於程序的數據段,並在堆棧中分配了空間, const常量在程序中確確實實存在,並且可以被調用、傳遞
-
const常量有數據類型,而 define常量沒有數據類型。編譯器可以對 const常量進行類型安全檢査,如類型、語句結構等,而 define不行。
-
很多IDE支持調試 const定義的常量,而不支持 define定義的常量由於 const修飾的變量可以排除程序之間的不安全性因素,保護程序中的常量不被修改,而且對數據類型也會進行相應的檢查,極大地提高了程序的健壯性,所以一般更加傾向於用const來定義常量類型。
typedef和 define有什么區別?
typedef與 define都是替一個對象取一個別名,以此來增強程序的可讀性,但是它們在使用和作用上也存在着以下4個方面的不同。
- 原理不同
define是C語言中定義的語法,它是預處理指令,在預處理時進行簡單而機械的字符串替換,不做正確性檢査,不管含義是否正確照樣代入,只有在編譯已被展開的源程序時,才會發現可能的錯誤並報錯。
例如,# define Pl3.1415926
,當程序執行area=Pr*r
語句時,PI會被替換為3.1415926。於是該語句被替換為area=3.1415926*r*r
。如果把# define語句中的數字9寫成了g,預處理也照樣代入,而不去檢查其是否合理、合法。
typedef是關鍵字,它在編譯時處理,所以 typedef具有類型檢查的功能。它在自己的作用域內給一個已經存在的類型一個別名,但是不能在一個函數定義里面使用標識符 typedef。例如, typedef int INTEGER
,這以后就可用 INTEGER來代替int作整型變量的類型說明了,例如:INTEGER a,b;
用 typedef定義數組、指針、結構等類型將帶來很大的方便,不僅使程序書寫簡單而且使意義更為明確,因而增強了可讀性。例如:typedef int a[10];
表示a是整型數組類型,數組長度為10。然后就可用a說明變量,例如:語句a s1,s2;完全等效於語句 int s1[10],s2[10].同理, typedef void(*p)(void)表示p是一種指向void型的指針類型。
- 功能不同
typedef用來定義類型的別名,這些類型不僅包含內部類型(int、char等),還包括自定義類型(如 struct),可以起到使類型易於記憶的功能。
例如:typedef int (*PF)(const char *, const char*)
定義一個指向函數的指針的數據類型PF,其中函數返回值為int,參數為 const char*。typedef還有另外一個重要的用途,那就是定義機器無關的類型。例如,可以定義一個叫REAL的浮點類型,在目標機器上它可以獲得最高的精度:typedef long double REAL
,在不支持 long double的機器上,該 typedef看起來會是下面這樣:typedef double real
,在 double都不支持的機器上,該 typedef看起來會是這樣:typedef float REAL
。
define不只是可以為類型取別名,還可以定義常量、變量、編譯開關等。
- 作用域不同
define沒有作用域的限制,只要是之前預定義過的宏,在以后的程序中都可以使用,而 typedef有自己的作用域。
程序示例如下:
void fun()
{
#define A int
}
void gun()
{
//這里也可以使用A,因為宏替換沒有作用域,但如果上面用的是 typedef,那這里就不能用
//A,不過,一般不在函數內使用 typedef
}
-
對指針的操作不同
兩者修飾指針類型時,作用不同。
#define INTPTR1 int*
typedef int* INTPTR2;
INTPTR1 pl, p2;
INTPTR2 p3, p4;
INTPTR1 pl, p2和INTPTR2 p3, p4的效果截然不同。 INTPTR1 pl, p2進行字符串替換后變成int*p1,p2
,要表達的意義是聲明一個指針變量p1和一個整型變量p2.而 INTPTR2 p3, p4,由於 INTPTR2是具有含義的,告訴我們是一個指向整型數據的指針,那么p3和p4都為指針變量,這句相當於int*pl,*p2
.從這里可以看出,進行宏替換是不含任何意義的替換,僅僅為字符串替換;而用 typedef為一種數據類型起的別名是帶有一定含義的。
程序示例如下
#define INTPTR1 int*
typedef int* INTPTR2
int a=1;
int b=2;
int c=3;
const INTPTR1 p1=&a;
const INTPTR2 p2=&b;
INTPTR2 const p3=&c;
上述代碼中, const INTPTR1 p1表示p1是一個常量指針,即不可以通過p1去修改p1指向的內容,但是p1可以指向其他內容。而對於 const INTPTR2 p2,由於 INTPTR2表示的是個指針類型,因此用 const去限定,表示封鎖了這個指針類型,因此p2是一個指針常量,不可使p2再指向其他內容,但可以通過p2修改其當前指向的內容。 INTPTR2 const p3同樣聲明的是一個指針常量。
如何使用 define聲明個常數,用以表明1年中有多少秒(忽略閏年問題)
#define SECOND_PER_YEAR (60*60*24*365)UL
# include< filename. h>和# nclude" filename. h"有什么區別?
對於 include< filename. h>,編譯器先從標准庫路徑開始搜索 filename.h,使得系統文件調用較快。而對於# include“ filename.h"”,編譯器先從用戶的工作路徑開始搜索 filename.h,然后去尋找系統路徑,使得自定義文件較快。
頭文件的作用有哪些?
頭文件的作用主要表現為以下兩個方面:
-
通過頭文件來調用庫功能。出於對源代碼保密的考慮,源代碼不便(或不准)向用戶公布,只要向用戶提供頭文件和二進制的庫即可。用戶只需要按照頭文件中的接口聲明來調用庫功能,而不必關心接口是怎么實現的。編譯器會從庫中提取相應的代碼。
-
頭文件能加強類型安全檢查。當某個接口被實現或被使用時,其方式與頭文件中的聲明不一致,編譯器就會指出錯誤,大大減輕程序員調試、改錯的負擔。
在頭文件中定義靜態變量是否可行,為什么?
不可行,如果在頭文件中定義靜態變量,會造成資源浪費的問題,同時也可能引起程序錯誤。因為如果在使用了該頭文件的每個C語言文件中定義靜態變量,按照編譯的步驟,在每個頭文件中都會單獨存在一個靜態變量,從而會引起空間浪費或者程序錯誤所以,不推薦在頭文件中定義任何變量,當然也包括靜態變量。
寫一個"標准"宏MIN ,這個宏輸入兩個參數並返回較小的一個?
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
不使用流程控制語句,如何打印出1~1000的整數?
宏定義多層嵌套(10 * 10 * 10),printf多次輸出。
#include <stdio. h>
#define B P,P,P,P,P,P,P,P,P,P
#define P L,L,L,L,L,L,L,L,L,L
#define L I,I,I,I,I,I,I,I,I,I,N
#define I printf("%3d",i++)
#define N printf("n")
int main()
{
int i = 1;
B;
return 0;
}
簡便寫法,同樣使用多層嵌套
#include<stdio. h>
#define A(x)x;x;x;x;x;x;x;x;x;
int main ()
{
int n=1;
A(A(A(printf("%d", n++);
return 0;
}
變量
全局變量和靜態變量的區別是什么?
-
全局變量的作用域為程序塊,而局部變量的作用域為當前函數。
-
內存存儲方式不同,全局變量(靜態全局變量,靜態局部變量)分配在全局數據區(靜態存儲空間),后者分配在棧區。
-
生命周期不同。全局變量隨主程序創建而創建,隨主程序銷毀而銷毀,局部變量在局部函數內部,甚至局部循環體等內部存在,退出就不存在了。
-
使用方式不同。通過聲明為全局變量,程序的各個部分都可以用到,而局部變量只能在局部使用。
全局變量可不可以定義在可被多個.C文件包含的頭文件中?為什么?
可以,在不同的C文件中以static形式來聲明同名全局變量。
可以在不同的C文件中聲明同名的全局變量,前提是其中只能有一個C文件中對此變量賦初值,此時連接不會出錯。
局部變量能否和全局變量重名?
能,局部會屏蔽全局。
局部變量可以與全局變量同名,在函數內引用這個變量時,會用到同名的局部變量,而不會用到全局變量。
對於有些編譯器而言,在同一個函數內可以定義多個同名的局部變量,比如在兩個循環體內都定義一個同名的局部變量,而那個局部變量的作用域就在那個循環體內。
函數
請寫個函數在main函數執行前先運行
__attribute__可以設置函數屬性(Function Attribute)、變量屬性(Variable Attribute)和類型屬性(Type Attribute)。
gnu對於函數屬性主要設置的關鍵字如下:
alias: 設置函數別名。
aligned: 設置函數對齊方式。
always_inline/gnu_inline:
函數是否是內聯函數。
constructor/destructor:
主函數執行之前、之后執行的函數。
format:
指定變參函數的格式輸入字符串所在函數位置以及對應格式輸出的位置。
noreturn:
指定這個函數沒有返回值。
請注意,這里的沒有返回值,並不是返回值是void。而是像_exit/exit/abord那樣
執行完函數之后進程就結束的函數。
weak:指定函數屬性為弱屬性,而不是全局屬性,一旦全局函數名稱和指定的函數名稱
命名有沖突,使用全局函數名稱。
完整示例代碼如下:
#include <stdio.h>
void before() __attribute__((constructor));
void after() __attribute__((destructor));
void before() {
printf("this is function %s\n",__func__);
return;
}
void after(){
printf("this is function %s\n",__func__);
return;
}
int main(){
printf("this is function %s\n",__func__);
return 0;
}
// 輸出結果
// this is function before
// this is function main
// this is function after
為什么析構函數必須是虛函數?
將可能會被繼承的父類的析構函數設置為虛函數,可以保證當我們new一個子類,然后使用基類指針指向該子類對象,釋放基類指針時可以釋放掉子類的空間,防止內存泄漏。
為什么C++默認的析構函數不是虛函數?
C++默認的析構函數不是虛函數是因為虛函數需要額外的虛函數表和虛表指針,占用額外的內存。而對於不會被繼承的類來說,其析構函數如果是虛函數,就會浪費內存。因此C++默認的析構函數不是虛函數,而是只有當需要當作父類時,設置為虛函數。
C++中析構函數的作用?
如果構造函數打開了一個文件,最后不需要使用時文件就要被關閉。析構函數允許類自動完成類似清理工作,不必調用其他成員函數。
析構函數也是特殊的類成員函數。簡單來說,析構函數與構造函數的作用正好相反,它用來完成對象被刪除前的一些清理工作,也就是專門的掃尾工作。
靜態函數和虛函數的區別?
靜態函數在編譯的時候就已經確定運行時機,虛函數在運行的時候動態綁定。虛函數因為用了虛函數表機制,調用的時候會增加一次內存開銷。
重載和覆蓋有什么區別?
- 覆蓋是子類和父類之間的關系,垂直關系;重載同一個類之間方法之間的關系,是水平關系。
- 覆蓋只能由一個方法或者只能由一對方法產生關系;重載是多個方法之間的關系。
- 覆蓋是根據對象類型(對象對應存儲空間類型)來決定的;而重載關系是根據調用的實參表和形參表來選擇方法體的。
虛函數表具體是怎樣實現運行時多態的?
原理:
虛函數表是一個類的虛函數的地址表,每個對象在創建時,都會有一個指針指向該類虛函數表,每一個類的虛函數表,按照函數聲明的順序,會將函數地址存在虛函數表中,當子類對象重寫父類的虛函數的時候,父類的虛函數表中對應的位置會被子類的虛函數地址覆蓋。
作用:
在用父類的指針調用子類對象成員函數時,虛函數表會指明要調用的具體函數是哪個。
C語言是怎么進行函數調用的?
大多數CPU上的程序實現使用棧來支持函數調用操作,棧被用來傳遞函數參數、存儲返回信息、臨時保存寄存器原有的值以備恢復以及用來存儲局部變量。
函數調用操作所使用的棧部分叫做棧幀結構,每個函數調用都有屬於自己的棧幀結構,棧幀結構由兩個指針指定,幀指針(指向起始),棧指針(指向棧頂),函數對大多數數據的訪問都是基於幀指針。下面是結構圖:
棧指針和幀指針一般都有專門的寄存器,通常使用ebp寄存器作為幀指針,使用esp寄存器做棧指針。
幀指針指向棧幀結構的頭,存放着上一個棧幀的頭部地址,棧指針指向棧頂。
請你說一說select
- select函數原型
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
- 文件描述符的數量
單個進程能夠監視的文件描述符的數量存在最大限制,通常是1024,當然可以更改數量;(在linux內核頭文件中定義:#define __FD_SETSIZE 1024)
- 就緒fd采用輪詢的方式掃描
select返回的是int,可以理解為返回的是ready(准備好的)一個或者多個文件描述符,應用程序需要遍歷整個文件描述符數組才能發現哪些fd句柄發生了事件,由於select采用輪詢的方式掃描文件描述符(不知道那個文件描述符讀寫數據,所以需要把所有的fd都遍歷),文件描述符數量越多,性能越差
- 內核 /用戶空間內存拷貝
select每次都會改變內核中的句柄數據結構集(fd集合),因而每次調用select都需要從用戶空間向內核空間復制所有的句柄數據結構(fd集合),產生巨大的開銷
- select的觸發方式
select的觸發方式是水平觸發,應用程序如果沒有完成對一個已經就緒的文件描述符進行IO操作,那么之后每次調用select還是會將這些文件描述符通知進程。
- 優點
a. select的可移植性較好,可以跨平台;
b. select可設置的監聽時間timeout精度更好,可精確到微秒,而poll為毫秒。
- 缺點:
a. select支持的文件描述符數量上限為1024,不能根據用戶需求進行更改;
b. select每次調用時都要將文件描述符集合從用戶態拷貝到內核態,開銷較大;
c. select返回的就緒文件描述符集合,需要用戶循環遍歷所監聽的所有文件描述符是否在該集合中,當監聽描述符數量很大時效率較低。
請你說說fork,wait,exec函數
父進程產生子進程使用fork拷貝出來一個父進程的副本,此時只拷貝了父進程的頁表,兩個進程都讀同一塊內存,當有進程寫的時候使用寫實拷貝機制分配內存,exec函數可以加載一個elf文件去替換父進程,從此父進程和子進程就可以運行不同的程序了。fork從父進程返回子進程的pid,從子進程返回0.調用了wait的父進程將會發生阻塞,直到有子進程狀態改變,執行成功返回0,錯誤返回-1。exec執行成功則子進程從新的程序開始運行,無返回值,執行失敗返回-1。
數組
以下代碼表示什么意思?
*(a[1]+1)、*(&a[1][1])、(*(a+1))[1]
第一個: 因為a[1]是第2行的地址,a[1]+1偏移一個單位(得到第2行第2列的地址),然后解引用取值,得到a[1][1]
;
第二個:[]優先級高,a[1][1]取地址再取值。
第三個:a+1相當於&a[1],所以* (a+1)=a[1],因此*(a+1)[1]=a[1][1]
數組下標可以為負數嗎?
可以,因為下標只是給出了一個與當前地址的偏移量而已,只要根據這個偏移量能定位得到目標地址即可。下面給出一個下標為負數的示例:
數組下標取負值的情況:
#include <stdio.h>
int main()
{
int i:
int a[5]={0,1,2,3,4};
int *p=&a[4]
for(i=-4;i<=0;i++)
printf("%d %x\n", p[i], &p[i]);
return O.
}
//輸出結果為
//0 b3ecf480
//1 b3ecf484
//2 b3ecf488
//3 b3ecf48c
//4 b3ecf490
從上例可以發現,在C語言中,數組的下標並非不可以為負數,當數組下標為負數時,編譯可以通過,而且也可以得到正確的結果,只是它表示的意思卻是從當前地址向前尋址.
位操作
如何求解整型數的二進制表示中1的個數?
程序代碼如下:
#include <stdio.h>
int func(int x)
{
int countx = 0;
while(x)
{
countx++;
x = x&(x-1);
}
return countx;
}
int main()
{
printf("%d\n",func(9999));
return 0;
}
程序輸出的結果為8。
在上例中,函數func()的功能是將x轉化為二進制數,然后計算該二進制數中含有的1的個數。首先以9為例來分析,9的二進制表示為1001,8的二進制表示為1000,兩者執行&操作之后結果為1000,此時1000再與0111(7的二進制位)執行&操作之后結果為0。
為了理解這個算法的核心,需要理解以下兩個操作:
1)當一個數被減1時,它最右邊的那個值為1的bit將變為0,同時其右邊的所有的bit都會變成1。
2)每次執行x&(x-1)的作用是把ⅹ對應的二進制數中的最后一位1去掉。因此,循環執行這個操作直到ⅹ等於0的時候,循環的次數就是x對應的二進制數中1的個數。
如何求解二進制中0的個數
int CountZeroBit(int num)
{
int count = 0;
while (num + 1)
{
count++;
num |= (num + 1); //算法轉換
}
return count;
}
int main()
{
int value = 25;
int ret = CountZeroBit(value);
printf("%d的二進制位中0的個數為%d\n",value, ret);
system("pause");
return 0;
}
交換兩個變量的值,不使用第三個變量。即a=3,b=5,交換之后a=5,b=3;
有兩種解法, 一種用算術算法, 一種用^(異或)。
a = a + b;
b = a - b;
a = a - b;
a = a^b;// 只能對int,char..
b = a^b;
a = a^b;
or
a ^= b ^= a;
給定一個整型變量a,寫兩段代碼,第一個設置a的bit 3,第二個清除a 的bit 3。在以上兩個操作中,要保持其它位不變。
#define BIT3 (0x1<<3)
static int a;
void set_bit3(void)
{
a |= BIT3;
}
void clear_bit3(void)
{
a &= ~BIT3;
}
容器和算法
map和set有什么區別?分別又是怎么實現的?
map和set都是C++的關聯容器,其底層實現都是紅黑樹(RB-Tree)。
由於 map 和set所開放的各種操作接口,RB-tree 也都提供了,所以幾乎所有的 map 和set的操作行為,都只是轉調 RB-tree 的操作行為。
map和set的區別在於:
map中的元素是key-value(鍵值對)對:關鍵字起到索引的作用,值則表示與索引相關聯的數據;Set與之相對就是關鍵字的簡單集合,set中每個元素只包含一個關鍵字。
set的迭代器是const的,不允許修改元素的值;map允許修改value,但不允許修改key。
其原因是因為map和set是根據關鍵字排序來保證其有序性的,如果允許修改key的話,那么首先需要刪除該鍵,然后調節平衡,再插入修改后的鍵值,調節平衡,如此一來,嚴重破壞了map和set的結構,導致iterator失效,不知道應該指向改變前的位置,還是指向改變后的位置。所以STL中將set的迭代器設置成const,不允許修改迭代器的值;而map的迭代器則不允許修改key值,允許修改value值。
map支持下標操作,set不支持下標操作。
map可以用key做下標,map的下標運算符[ ]將關鍵碼作為下標去執行查找,如果關鍵碼不存在,則插入一個具有該關鍵碼和mapped_type類型默認值的元素至map中,因此下標運算符[ ]在map應用中需要慎用,const_map不能用,只希望確定某一個關鍵值是否存在而不希望插入元素時也不應該使用,mapped_type類型沒有默認值也不應該使用。如果find能解決需要,盡可能用find。
STL的allocator有什么作用?
STL的分配器用於封裝STL容器在內存管理上的底層細節。在C++中,其內存配置和釋放如下:
new運算分兩個階段:(1)調用::operator new配置內存;(2)調用對象構造函數構造對象內容
delete運算分兩個階段:(1)調用對象希構函數;(2)掉員工::operator delete釋放內存
為了精密分工,STL allocator將兩個階段操作區分開來:內存配置有alloc::allocate()負責,內存釋放由alloc::deallocate()負責;對象構造由::construct()負責,對象析構由::destroy()負責。
同時為了提升內存管理的效率,減少申請小內存造成的內存碎片問題,SGI STL采用了兩級配置器,當分配的空間大小超過128B時,會使用第一級空間配置器;當分配的空間大小小於128B時,將使用第二級空間配置器。第一級空間配置器直接使用malloc()、realloc()、free()函數進行內存空間的分配和釋放,而第二級空間配置器采用了內存池技術,通過空閑鏈表來管理內存。
STL迭代器如何刪除元素?
對於序列容器vector,deque來說,使用erase(itertor)后,后邊的每個元素的迭代器都會失效,但是后邊每個元素都會往前移動一個位置,但是erase會返回下一個有效的迭代器;
對於關聯容器map set來說,使用了erase(iterator)后,當前元素的迭代器失效,但是其結構是紅黑樹,刪除當前元素的,不會影響到下一個元素的迭代器,所以在調用erase之前,記錄下一個元素的迭代器即可。
對於list來說,它使用了不連續分配的內存,並且它的erase方法也會返回下一個有效的iterator,因此上面兩種正確的方法都可以使用。
STL中MAP數據如何存放的?
紅黑樹。unordered map底層結構是哈希表
STL中map與unordered_map有什么區別?
map在底層使用了紅黑樹來實現,unordered_map是C++11標准中新加入的容器,它的底層是使用hash表的形式來完成映射的功能,map是按照operator<比較判斷元素是否相同,以及比較元素的大小,然后選擇合適的位置插入到樹中。所以,如果對map進行遍歷(中序遍歷)的話,輸出的結果是有序的。順序就是按照operator< 定義的大小排序。
而unordered_map是計算元素的Hash值,根據Hash值判斷元素是否相同。所以,對unordered_map進行遍歷,結果是無序的。
使用map時,需要為key定義operator< 。 而unordered_map的使用需要定義hash_value函數並且重載operator==。對於內置類型,如string,這些都不用操心,可以使用默認的。對於自定義的類型做key,就需要自己重載operator< 或者hash_value()了。
所以說,當不需要結果排好序時,最好用unordered_map,插入刪除和查詢的效率要高於map。
vector和list的區別是什么?
-
vector底層實現是數組;list是雙向 鏈表。
-
vector支持隨機訪問,list不支持。
-
vector是順序內存,list不是。
-
vector在中間節點進行插入刪除會導致內存拷貝,list不會。
-
vector一次性分配好內存,不夠時才進行2倍擴容;list每次插入新節點都會進行內存申請。
-
vector隨機訪問性能好,插入刪除性能差;list隨機訪問性能差,插入刪除性能好。
STL中迭代器有什么作用?有指針為何還要迭代器?
1、迭代器
Iterator(迭代器)模式又稱Cursor(游標)模式,用於提供一種方法順序訪問一個聚合對象中各個元素, 而又不需暴露該對象的內部表示。或者這樣說可能更容易理解:Iterator模式是運用於聚合對象的一種模式,通過運用該模式,使得我們可以在不知道對象內部表示的情況下,按照一定順序(由iterator提供的方法)訪問聚合對象中的各個元素。
由於Iterator模式的以上特性:與聚合對象耦合,在一定程度上限制了它的廣泛運用,一般僅用於底層聚合支持類,如STL的list、vector、stack等容器類及ostream_iterator等擴展iterator。
2、迭代器和指針的區別
迭代器不是指針,是類模板,表現的像指針。他只是模擬了指針的一些功能,通過重載了指針的一些操作符,->、*、++、--等。迭代器封裝了指針,是一個“可遍歷STL( Standard Template Library)容器內全部或部分元素”的對象, 本質是封裝了原生指針,是指針概念的一種提升(lift),提供了比指針更高級的行為,相當於一種智能指針,他可以根據不同類型的數據結構來實現不同的++,--等操作。
迭代器返回的是對象引用而不是對象的值,所以cout只能輸出迭代器使用*取值后的值而不能直接輸出其自身。
3、迭代器產生原因
Iterator類的訪問方式就是把不同集合類的訪問邏輯抽象出來,使得不用暴露集合內部的結構而達到循環遍歷集合的效果。
epoll的原理是什么?
調用順序:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
首先創建一個epoll對象,然后使用epoll_ctl對這個對象進行操作,把需要監控的描述添加進去,這些描述如將會以epoll_event結構體的形式組成一顆紅黑樹,接着阻塞在epoll_wait,進入大循環,當某個fd上有事件發生時,內核將會把其對應的結構體放入到一個鏈表中,返回有事件發生的鏈表。
STL里resize和reserve的區別是什么?
改變當前容器內含有元素的數量(size()),eg: vectorv; v.resize(len);v的 size 變為 len,如果原來 v 的 size 小於 len,那么容器新增(len-size)個元素,元素的值為
默認為 0.當 v.push_back(3);之后,則是 3 是放在了 v 的末尾,即下標為 len,此時容器是 size為 len+1;
改變當前容器的最大容量(capacity),它不會生成元素,只是確定這個容器允許放入多少對象,如果 reserve(len)的值大於當前的 capacity(),那么會重新分配一塊能存 len 個對象的空間,然后把之前 v.size()個對象通過 copy construtor 復制過來,銷毀之前的內存;
類和數據抽象
C++中類成員的訪問權限?
C++通過 public、protected、private 三個關鍵字來控制成員變量和成員函數的訪問權限,它們分別表示公有的、受保護的、私有的,被稱為成員訪問限定符。在類的內部(定義類的代碼內部),無論成員被聲明為 public、protected 還是 private,都是可以互相訪問的,沒有訪問權限的限制。在類的外部(定義類的代碼之外),只能通過對象訪問成員,並且通過對象只能訪問 public 屬性的成員,不能訪問 private、protected 屬性的成員
C++中struct和class的區別是什么?
在C++中,可以用struct和class定義類,都可以繼承。區別在於:structural的默認繼承權限和默認訪問權限是public,而class的默認繼承權限和默認訪問權限是private。另外,class還可以定義模板類形參,比如template。
C++類內可以定義引用數據成員嗎?
可以,必須通過成員函數初始化列表初始化。
面向對象與泛型編程是什么?
- 面向對象編程簡稱OOP,是一種程序設計思想。OOP把對象作為程序的基本單元,一個對象包含了數據和操作數據的函數。
- 面向過程的程序設計把計算機程序視為一系列的命令集合,即一組函數的順序執行。為了簡化程序設計,面向過程把函數繼續切分為子函數,即把大塊函數通過切割成小塊函數來降低系統的復雜度。
- 泛型編程: 讓類型參數化,方便程序員編碼。
類型參數化: 使的程序(算法)可以從邏輯功能上抽象,把被處理對象(數據)的類型作為參數傳遞。
什么是右值引用,跟左值又有什么區別?
左值和右值的概念:
左值:能對表達式取地址、或具名對象/變量。一般指表達式結束后依然存在的持久對象。
右值:不能對表達式取地址,或匿名對象。一般指表達式結束就不再存在的臨時對象。
右值引用和左值引用的區別:
- 左值可以尋址,而右值不可以。
- 左值可以被賦值,右值不可以被賦值,可以用來給左值賦值。
- 左值可變,右值不可變(僅對基礎類型適用,用戶自定義類型右值引用可以通過成員函數改變)。
析構函數可以為 virtual 型,構造函數則不能,為什么?
構造函數不能聲明為虛函數,析構函數可以聲明為虛函數,而且有時是必須聲明為虛函數。不建議在構造函數和析構函數里面調用虛函數。
構造函數不能聲明為虛函數的原因是:
虛函數的主要意義在於被派生類繼承從而產生多態。派生類的構造函數中,編譯器會加入構造基類的代碼,如果基類的構造函數用到參數,則派生類在其構造函
數的初始化列表中必須為基類給出參數,就是這個原因。虛函數的意思就是開啟動態綁定,程序會根據對象的動態類型來選擇要調用的方法。然而在構造函數運行的時候,這個對象的動態類型還不完整,沒有辦法確定它到底是什么類型,故構造函數不能動態綁定。(動態綁定是根據對象的動態類型而不是函數名,在調用構造函數之前,這個對象根本就不存在,它怎么動態綁定?)
C++中空類默認產生哪些類成員函數?
C++中空類默認會產生以下6個函數:默認構造函數、復制構造函數、析構函數、賦值運算符重載函數、取址運算法重載函數、const取址運算符重載函數等。
class Empty
{
public:
Empty(); // 缺省構造函數
Empty( const Empty& ); // 拷貝構造函數
~Empty(); // 析構函數
Empty& operator=( const Empty& ); // 賦值運算符
Empty* operator&(); // 取址運算符
const Empty* operator&() const; // 取址運算符 const
};
面向對象
面向對象和面向過程有什么區別?
面向對象與面向過程有以下四個方面的不同:
- 出發點不同
面向對象使用符合常規思維的方式來處理客觀世界的問題,強調把解決問題領域的“動作”直接映射到對象之間的接口上。而面向過程則強調的是過程的抽象化與模塊化,是以過程為中心構造或處理客觀世界問題。
- 層次邏輯關系不同
面向對象使用計算機邏輯來模擬客觀世界中的物理存在,以對象的集合類作為處理問題的單位,盡可能地使計算機世界向客觀世界靠攏,以使處理問題的方式更清晰直接,面向對象使用類的層次結構來體現類之間的繼承與發展。面向過程處理問題的基本單位是能清晰准確地表達過程的模塊,用模塊的層次結構概括模塊或模塊間的關系與功能,把客觀世界的問題抽象成計算機可以處理的過程。
- 數據處理方式與控制程序方式不同
面向對象將數據與對應的代碼封裝成一個整體,原則上其他對象不能直接修改其數據,即對象的修改只能由自身的成員函數完成,控制程序方式上是通過“事件驅動”來激活和運行程序的。而面向過程是直接通過程序來處理數據,處理完畢后即可顯示處理的結果,在控制方式上是按照設計調用或返回程序,不能自由導航,各模塊之間存在着控制與被控制,調動與被調用的關系。
- 分析設計與編碼轉換方式不同
面向對象貫穿於軟件生命周期的分析、設計及編碼中,是一種平滑的過程,從分析到設計再到編碼是采用一致性的模型表示,實現的是一種無縫連接。而面向過程強調分析、設計及編碼之間按規則進行轉換貫穿於軟件生命周期的分析、設計及編碼中,實現的是一種有縫的連接。
面向對象的基本特征有哪些?
面向對象的編程方法有四個基本特性:
- 抽象:就是忽略一個主題中與當前目標無關的方面,以便更充分地注意與當前目標有關的方面。抽象並不打算了解全部問題,而只是選擇其中的一部分,暫時不用部分細節。抽象包括兩個方面,一是過程抽象,二是數據抽象。
過程抽象是指任何一個明確定義功能的操作都可被使用者看作單個的實體看待,盡管這個操作實際上可能由一系列更低級的操作來完成。數據抽象定義了數據類型和施加於該類型對象上的操作,並限定了對象的值,只能通過使用這些操作修改和觀察。
- 繼承:這是一種聯結類的層次模型,並且允許和鼓勵類的重用,它提供了一種明確表述共性的方法。對象的一個新類可以從現有的類中派生,這個過程稱為類繼承。新類繼承了原始類的特性,新類稱為原始類的派生類(子類),而原始類稱為新類的基類(父類)。
派生類可以從它的基類那里繼承方法和實例變量,並且類可以修改或增加新的方法使之更適合特殊的需要。這也體現了大自然中一般與特殊的關系。繼承性很好地解決了軟件的可重用性問題。
- 封裝:就是把過程和數據包圍起來,對數據的訪問只能通過已定義的接口。面向對象的計算始於這個基本概念,即現實世界可以被描繪成一系列完全自治、封裝的對象,這些對象通過一個受保護的接口訪問其他對象。一旦定義了一個對象的特性,則有必要決定這些特性的可見性,即哪些特性對外部世界是可見的,哪些特性用於表示內部狀態。
在這個階段定義對象的接口。通常,應禁止直接訪問一個對象的實際表示,而應通過操作接口訪問對象,這稱為信息隱藏。封裝保證了模塊具有較好的獨立性,使得程序維護修改較為容易。對應用程序的修改僅限於類的內部,因而可以將應用程序修改帶來的影響減少到最低限度。
- 多態:是指允許不同類的對象對同一消息做出響應。比如同樣的復制-粘貼操作,在字處理程序和繪圖程序中有不同的效果。多態性包括參數化多態性和包含多態性。多態性語言具有靈活、抽象、行為共享、代碼共享的優勢,很好地解決了應用程序函數同名問題。
什么是深拷貝?什么是淺拷貝?
深拷貝是徹底的拷貝,兩對象中所有的成員都是獨立的一份,而且,成員對象中的成員對象也是獨立一份。
淺拷貝中的某些成員變量可能是共享的,深拷貝如果不夠徹底,就是淺拷貝。
什么是友元?
有成員只能在類的成員函數內部訪問,如果想在別處訪問對象的私有成員,只能通過類提供的接口(成員函數)間接地進行。這固然能夠帶來數據隱藏的好處,利於將來程序的擴充,但也會增加程序書寫的麻煩。
C++是從結構化的C語言發展而來的,需要照顧結構化設計程序員的習慣,所以在對私有成員可訪問范圍的問題上不可限制太死。
C++ 設計者認為, 如果有的程序員真的非常怕麻煩,就是想在類的成員函數外部直接訪問對象的私有成員,那還是做一點妥協以滿足他們的願望為好,這也算是眼前利益和長遠利益的折中。因此,C++ 就有了友元(friend)的概念。打個比方,這相當於是說:朋友是值得信任的,所以可以對他們公開一些自己的隱私。
友元提供了一種 普通函數或者類成員函數 訪問另一個類中的私有或保護成員 的機制。也就是說有兩種形式的友元:
(1)友元函數:普通函數對一個訪問某個類中的私有或保護成員。
(2)友元類:類A中的成員函數訪問類B中的私有或保護成員。
基類的構造函數/析構函數是否能被派生類繼承?
基類的構造函數析構函數不能被派生類繼承。
基類的構造函數不能被派生類繼承,派生類中需要聲明自己的構造函數。設計派生類的構造函數時,不僅要考慮派生類所增加的數據成員初始化,也要考慮基類的數據成員的初始化。聲明構造函數時,只需要對本類中新增成員進行初始化,對繼承來的基類成員的初始化,需要調用基類構造函數完成。
基類的析構函數也不能被派生類繼承,派生類需要自行聲明析構函數。聲明方法與一般(無繼承關系時)類的析構函數相同,不需要顯式地調用基類的析構函數,系統會自動隱式調用。需要注意的是,析構函數的調用次序與構造函數相反。
初始化列表和構造函數初始化的區別?
構造函數初始化列表以一個冒號開始,接着是以逗號分隔的數據成員列表,每個數據成員后面跟一個放在括號中的初始化式。例如:
Example::Example() : ival(0), dval(0.0) {} //ival 和dval是類的兩個數據成員
上面的例子和下面不用初始化列表的構造函數看似沒什么區別:
Example::Example()
{
ival = 0;
dval = 0.0;
}
的確,這兩個構造函數的結果是一樣的。但區別在於:上面的構造函數(使用初始化列表的構造函數)顯示的初始化類的成員;而沒使用初始化列表的構造函數是對類的成員賦值,並沒有進行顯示的初始化。
初始化和賦值對內置類型的成員沒有什么大的區別,像上面的任一個構造函數都可以。但有的時候必須用帶有初始化列表的構造函數:
-
成員類型是沒有默認構造函數的類。若沒有提供顯示初始化式,則編譯器隱式使用成員類型的默認構造函數,若類沒有默認構造函數,則編譯器嘗試使用默認構造函數將會失敗。
-
const成員或引用類型的員。因為const對象或引用類型只能初始化,不能對他們賦值。
C++中有那些情況只能用初始化列表,而不能用賦值?
構造函數初始化列表以一個冒號開始,接着是以逗號分隔的數據成員列表,每個數據成員后面都跟一個放在括號中的初始化式。例如, Example:Example ival(o,dva(0.0){},其中ival與dva是類的兩個數據成員。
在C++語言中,賦值與初始化列表的原理不一樣,賦值是刪除原值,賦予新值,初始化列表開辟空間和初始化是同時完成的,直接給予一個值
所以,在C++中,賦值與初始化列表的使用情況也不一樣,只能用初始化列表,而不能用賦值的情況一般有以下3種:
- 當類中含有 const(常量)、 reference(引用)成員變量時,只能初始化,不能對它們進行賦值。常量不能被賦值,只能被初始化,所以必須在初始化列表中完成,C++的引用也一定要初始化,所以必須在初始化列表中完成。
- 派生類在構造函數中要對自身成員初始化,也要對繼承過來的基類成員進行初始化當基類沒有默認構造函數的時候,通過在派生類的構造函數初始化列表中調用基類的構造函數實現。
- 如果成員類型是沒有默認構造函數的類,也只能使用初始化列表。若沒有提供顯式初始化時,則編譯器隱式使用成員類型的默認構造函數,此時編譯器嘗試使用默認構造函數將會失敗
類的成員變量的初始化順序是什么?
-
成員變量在使用初始化列表初始化時,與構造函數中初始化成員列表的順序無關,只與定義成員變量的順序有關。因為成員變量的初始化次序是根據變量在內存中次序有關,而內存中的排列順序早在編譯期就根據變量的定義次序決定了。這點在EffectiveC++中有詳細介紹。
-
如果不使用初始化列表初始化,在構造函數內初始化時,此時與成員變量在構造函數中的位置有關。
-
注意:類成員在定義時,是不能初始化的
-
注意:類中const成員常量必須在構造函數初始化列表中初始化。
-
注意:類中static成員變量,必須在類外初始化。
-
靜態變量進行初始化順序是基類的靜態變量先初始化,然后是它的派生類。直到所有的靜態變量都被初始化。這里需要注意全局變量和靜態變量的初始化是不分次序的。這也不難理解,其實靜態變量和全局變量都被放在公共內存區。可以把靜態變量理解為帶有“作用域”的全局變量。在一切初始化工作結束后,main函數會被調用,如果某個類的構造函數被執行,那么首先基類的成員變量會被初始化。
當一個類為另一個類的成員變量時,如何對其進行初始化?
示例程序如下:
class ABC
{
public:
ABC(int x, int y, int z);
private :
int a;
int b;
int c;
};
class MyClass
{
public:
MyClass():abc(1,2,3)
{
}
private:
ABC abc;
};
上例中,因為ABC有了顯式的帶參數的構造函數,那么它是無法依靠編譯器生成無參構造函數的,所以必須使用初始化列表:abc(1,2,3),才能構造ABC的對象。
C++能設計實現一個不能被繼承的類嗎?
在Java 中定義了關鍵字final ,被final 修飾的類不能被繼承。但在C++ 中沒有final 這個關鍵字,要實現這個要求還是需要花費一些精力。
首先想到的是在C++ 中,子類的構造函數會自動調用父類的構造函數。同樣,子類的析構函數也會自動調用父類的析構函數。要想一個類不能被繼承,我們只要把它的構造函數和析構函 數都定義為私有函數。那么當一個類試圖從它那繼承的時候,必然會由於試圖調用構造函數、析構函數而導致編譯錯誤。
可是這個類的構造函數和析構函數都是私有函數了,我們怎樣才能得到該類的實例呢?這難不倒我們,我們可以通過定義靜態來創建和釋放類的實例。
基於這個思路,我們可以寫出如下的代碼:
/// // Define a class which can't be derived from /// class FinalClass1
{
public :
static FinalClass1* GetInstance()
{
return new FinalClass1;
}
static void DeleteInstance( FinalClass1* pInstance)
{
delete pInstance;
pInstance = 0;
}
private :
FinalClass1() {}
~FinalClass1() {}
};
這個類是不能被繼承,但在總覺得它和一般的類有些不一樣,使用起來也有點不方便。比如,我們只能得到位於堆上的實例,而得不到位於棧上實例。能不能實現一個和一般類除了不能被繼承之外其他用法都一樣的類呢?辦法總是有的,不過需要一些技巧。請看如下代碼:
/// // Define a class which can't be derived from /// template <typename T> class MakeFinal
{
friend T;
private :
MakeFinal() {}
~MakeFinal() {}
};
class FinalClass2 :
virtual public MakeFinal<FinalClass2>
{
public :
FinalClass2() {}
~FinalClass2() {}
};
這個類使用起來和一般的類沒有區別,可以在棧上、也可以在堆上創建實例。盡管類 MakeFinal <FinalClass2>
的構造函數和析構函數都是私有的,但由於類 FinalClass2 是它的友元函數,因此在 FinalClass2 中調用 MakeFinal <FinalClass2>
的構造函數和析構函數都不會造成編譯錯誤。但當我們試圖從 FinalClass2 繼承一個類並創建它的實例時,卻不同通過編譯。
class Try : public FinalClass2
{
public :
Try() {}
~Try() {}
}; Try temp;
由於類 FinalClass2 是從類 MakeFinal <FinalClass2>
虛繼承過來的,在調用 Try 的構造函數的時候,會直接跳過 FinalClass2 而直接調用 MakeFinal <FinalClass2>
的構造函數。非常遺憾的是Try 不是 MakeFinal <FinalClass2>
的友元,因此不能調用其私有的構造函數。
基於上面的分析,試圖從 FinalClass2 繼承的類,一旦實例化,都會導致編譯錯誤,因此是 FinalClass2 不能被繼承。這就滿足了我們設計要求。
構造函數沒有返回值,那么如何得知對象是否構造成功?
這里的“構造”不單指分配對象本身的內存,而是指在建立對象時做的初始化操作(如打開文件、連接數據庫等)。
因為構造函數沒有返回值,所以通知對象的構造失敗的唯一方法就是在構造函數中拋出異常。構造函數中拋出異常將導致對象的析構函數不被執行,當對象發生部分構造時,已經構造完畢的子對象將會逆序地被析構。
Public繼承、protected繼承、private繼承的區別?
public(公有)繼承、 protected(保護)繼承和 private(私有)繼承是常見的3種繼承方式。
- 公有繼承
對於子類的對象而言,采用公有繼承時,基類成員對子類對象的可見性與一般類成員對對象的可見性相同,公有成員可見,其他成員不可見。
對於子類而言,基類的公有成員和保護成員可見;基類的公有成員和保護成員作為派生類的成員時,它們都維持原有的可見性(基類 public成員在子類中還是public,基類 protected成員在子類中還是 protected);基類的私有成員不可見,基類的私有成員依然是私有的,子類不可訪問。
- 保護繼承
保護繼承的特點是:基類的所有公有成員和保護成員都成為派生類的保護成員,並且只能被它的派生類成員函數或友元訪問。基類的私有成員仍然是私有的。由此可以看出,基類的所有成員對子類的對象都是不可見的。
- 私有繼承
私有繼承的特點是,基類的公有成員和保護成員都作為派生類的私有成員,並且不能被這個派生類的子類所訪問。
C++提供默認參數的函數嗎?
C++可以給函數定義默認參數值。在函數調用時沒有指定與形參相對應的實參時,就自動使用默認參數。
默認參數的語法與使用:
(1) 在函數聲明或定義時,直接對參數賦值,這就是默認參數。
(2) 在函數調用時,省略部分或全部參數。這時可以用默認參數來代替。
通常調用函數時,要為函數的每個參數給定對應的實參。例如:
void delay(int loops=1000);//函數聲明
void delay(int loops) //函數定義
{
if(loops==0)
{
return;
}
for(int i=0;i<loops;i++)
;
}
在上例中,如果將delay()函數中的loops定義成默認值1000,這樣,以后無論何時調用delay()函數,都不用給loops賦值,程序都會自動將它當做值 1000進行處理。例如,當執行delay(2500)調用時,loops的參數值為顯性化的,被設置為 2500;當執行delay()時,loops將采用默認值1000。
默認參數在函數聲明中提供,當有聲明又有定義時,定義中不允許默認參數。如果函數只有定義,則默認參數才可出現在函數定義中。例如:
oid point(int=3,int=4);//聲明中給出默認值
void point(int x,int y) //定義中不允許再給出默認值
{
cout<<x<<endl;
cout<<y<<endl;
}
如果一組重載函數(可能帶有默認參數)都允許相同實參個數的調用,將會引起調用的二義性。例如:
void func(int);//重載函數之一
void func(int,int=4);//重載函數之二,帶有默認參數
void func(int=3,int=4);//重載函數三,帶有默認參數
func(7);//錯誤:到底調用3個重載函數中的哪個?
func(20,30);//錯誤:到底調用后面兩個重載函數的哪個?
虛函數
什么是虛函數?
指向基類的指針在操作它的多態類對象時,可以根據指向的不同類對象調用其相應的函數,這個函數就是虛函數。
虛函數的作用:在基類定義了虛函數后,可以在派生類中對虛函數進行重新定義,並且可以通過基類指針或引用,在程序的運行階段動態地選擇調用基類和不同派生類中的同名函數。(如果在派生類中沒有對虛函數重新定義,則它繼承其基類的虛函數。)
下面是一個虛函數的實例程序:
#include "stdafx.h"
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Print()//父類虛函數
{
printf("This is Class Base!\n");
}
};
class Derived1 :public Base
{
public:
void Print()//子類1虛函數
{
printf("This is Class Derived1!\n");
}
};
class Derived2 :public Base
{
public:
void Print()//子類2虛函數
{
printf("This is Class Derived2!\n");
}
};
int main()
{
Base Cbase;
Derived1 Cderived1;
Derived2 Cderived2;
Cbase.Print();
Cderived1.Print();
Cderived2.Print();
cout << "---------------" << endl;
Base *p1 = &Cbase;
Base *p2 = &Cderived1;
Base *p3 = &Cderived2;
p1->Print();
p2->Print();
p3->Print();
}
/*
輸出結果:
This is Class Base!
This is Class Derived1!
This is Class Derived2!
---------------
This is Class Base!
This is Class Derived1!
This is Class Derived2!
*/
需要注意的是,虛函數雖然非常好用,但是在使用虛函數時,並非所有的函數都需要定義成虛函數,因為實現虛函數是有代價的。在使用虛函數時,需要注意以下幾個方面的內容:
(1) 只需要在聲明函數的類體中使用關鍵字virtual將函數聲明為虛函數,而定義函數時不需要使用關鍵字virtual。
(2) 當將基類中的某一成員函數聲明為虛函數后,派生類中的同名函數自動成為虛函數。
(3) 非類的成員函數不能定義為虛函數,全局函數以及類的成員函數中靜態成員函數和構造函數也不能定義為虛函數,但可以將析構函數定義為虛函數。
(4) 基類的析構函數應該定義為虛函數,否則會造成內存泄漏。基類析構函數未聲明virtual,基類指針指向派生類時,delete指針不調用派生類析構函數。有 virtual,則先調用派生類析構再調用基類析構。
C++如何實現多態?
C++中通過虛函數實現多態。虛函數的本質就是通過基類指針訪問派生類定義的函數。每個含有虛函數的類,其實例對象內部都有一個虛函數表指針。該虛函數表指針被初始化為本類的虛函數表的內存地址。所以,在程序中,不管對象類型如何轉換,該對象內部的虛函數表指針都是固定的,這樣才能實現動態地對對象函數進行調用,這就是C++多態性的原理。
純虛函數指的是什么?
純虛函數是一種特殊的虛函數,格式一般如下
class <類名>
{
virtual()函數返回值類型 虛函數名(形參表)=0;
...
};
class <類名>
由於在很多情況下,基類中不能對虛函數給出有意義的實現,只能把函數的實現留給派生類。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但是動物本身生成對象不合情理,此時就可以將動物類中的函數定義為純虛函數,如果基類中有純虛函數,那么在子類中必須實現這個純虛函數,否則子類將無法被實例化,也無法實現多態。
含有純虛函數的類稱為抽象類,抽象類不能生成對象。純虛函數永遠不會被調用,它們主要用來統一管理子類對象。
什么函數不能聲明為虛函數?
常見的不能聲明為虛函數的有:普通函數(非成員函數);靜態成員函數;內聯成員函數;構造函數;友元函數。
1.為什么C++不支持普通函數為虛函數?
普通函數(非成員函數)只能被overload,不能被override,聲明為虛函數也沒有什么意思,因此編譯器會在編譯時邦定函數。
2.為什么C++不支持構造函數為虛函數?
這個原因很簡單,主要是從語義上考慮,所以不支持。因為構造函數本來就是為了明確初始化對象成員才產生的,然而virtual function主要是為了再不完全了解細節的情況下也能正確處理對象。另外,virtual函數是在不同類型的對象產生不同的動作,現在對象還沒有產生,如何使用virtual函數來完成你想完成的動作。(這不就是典型的悖論)
3.為什么C++不支持內聯成員函數為虛函數?
其實很簡單,那內聯函數就是為了在代碼中直接展開,減少函數調用花費的代價,虛函數是為了在繼承后對象能夠准確的執行自己的動作,這是不可能統一的。(再說了,inline函數在編譯時被展開,虛函數在運行時才能動態的邦定函數)
4.為什么C++不支持靜態成員函數為虛函數?
這也很簡單,靜態成員函數對於每個類來說只有一份代碼,所有的對象都共享這一份代碼,他也沒有要動態邦定的必要性。
5.為什么C++不支持友元函數為虛函數?
因為C++不支持友元函數的繼承,對於沒有繼承特性的函數沒有虛函數的說法。
C++中如何阻止一個類被實例化?
C++中可以通過使用抽象類,或者將構造函數聲明為private阻止一個類被實例化。抽象類之所以不能被實例化,是因為抽象類不能代表一類具體的事物,它是對多種具有相似性的具體事物的共同特征的一種抽象。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但是動物本身生成對象不合情理。
結語
如果大家在網上看到了不錯的資料,或者在筆試面試中遇到了資料中沒有的知識點,大家可以聯系我,我替大家整理。資料如有錯誤或者不合適的地方,請及時聯系作者。
這些內容都是我熬夜整理的,最近還在修改大論文,事情也挺多的。創作不易,大家不要忘了點擊「贊」支持下,也算沒有白白熬夜,對得起我掉的一根根頭發。
最后,再放下github鏈接(https://github.com/ZhongYi-LinuxDriverDev/EmbeddedSoftwareEngineerInterview),不要忘了點個star!