1 變數的宣告和定義有什麼區別
變數的定義為變數分配地址和儲存空間, 變數的宣告不分配地址。一個變數可以在多個地方宣告, 但是隻在一個地方定義。 加入extern 修飾的是變數的宣告,說明此變數將在檔案以外或在檔案後面部分定義。
說明:很多時候一個變數,只是宣告不分配記憶體空間,直到具體使用時才初始化,分配記憶體空間, 如外部變數。
int main()
{
extern int A;
//這是個宣告而不是定義,宣告A是一個已經定義了的外部變數
//注意:宣告外部變數時可以把變數型別去掉如:extern A;
dosth(); //執行函式
}
int A; //是定義,定義了A為整型的外部變數
2 簡述#ifdef、#else、#endif和#ifndef的作用
利用#ifdef、#endif將某程式功能模組包括進去,以向特定使用者提供該功能。在不需要時使用者可輕易將其遮蔽。
#ifdef MATH
#include “math。c”
#endif
在子程式前加上標記,以便於追蹤和除錯。
#ifdef DEBUG
printf (“Indebugging……!”);
#endif
應對硬體的限制。由於一些具體應用環境的硬體不一樣,限於條件,本地缺乏這種裝置,只能繞過硬體,直接寫出預期結果。
注意:雖然不用條件編譯命令而直接用if語句也能達到要求,但那樣做目標程式長(因為所有語句都編譯),執行時間長(因為在程式執行時間對if語句進行測試)。而採用條件編譯,可以減少被編譯的語句,從而減少目標程式的長度,減少執行時間。
3 寫出int 、bool、 float 、指標變數與 “零值”比較的if 語句
//int與零值比較
if ( n == 0 )
if ( n != 0 )
//bool與零值比較
if (flag) // 表示flag為真
if (!flag) // 表示flag為假
//float與零值比較
const float EPSINON = 0。00001;
if ((x >= - EPSINON) && (x <= EPSINON) //其中EPSINON是允許的誤差(即精度)。
//指標變數與零值比較
if (p == NULL)
if (p != NULL)
4 結構體可以直接賦值嗎
宣告時可以直接初始化,同一結構體的不同物件之間也可以直接賦值,但是當結構體中含有指標“成員”時一定要小心。
注意:當有多個指標指向同一段記憶體時,某個指標釋放這段記憶體可能會導致其他指標的非法操作。因此在釋放前一定要確保其他指標不再使用這段記憶體空間。
5 sizeof 和strlen 的區別
sizeof是一個運算子,strlen是庫函式。
sizeof的引數可以是資料的型別,也可以是變數,而strlen只能以結尾為‘\0’的字串作引數。
編譯器在編譯時就計算出了sizeof的結果,而strlen函式必須在執行時才能計算出來。並且sizeof計算的是資料型別佔記憶體的大小,而strlen計算的是字串實際的長度。
陣列做sizeof的引數不退化,傳遞給strlen就退化為指標了
6 C 語言的關鍵字 static 和 C++ 的關鍵字 static 有什麼區別
在 C 中 static 用來修飾區域性靜態變數和外部靜態變數、函式。而 C++中除了上述功能外,還用來定義類的成員變數和函式。即靜態成員和靜態成員函式。
注意:程式設計時 static 的記憶性,和全域性性的特點可以讓在不同時期呼叫的函式進行通訊,傳遞資訊,而 C++的靜態成員則可以在多個物件例項間進行通訊,傳遞資訊。
7 C 語言的 malloc 和 C++ 中的 new 有什麼區別
new 、delete 是運算子,可以過載,只能在C++ 中使用。
malloc、free 是函式,可以覆蓋,C、C++ 中都可以使用。
new 可以呼叫物件的建構函式,對應的delete 呼叫相應的解構函式。
malloc 僅僅分配記憶體,free 僅僅回收記憶體,並不執行構造和解構函式
new 、delete 返回的是某種資料型別指標,malloc、free 返回的是void 指標。
注意:malloc 申請的記憶體空間要用free 釋放,而new 申請的記憶體空間要用delete 釋放,不要混用。
8 寫一個 “標準”宏MIN
#define min(a,b)((a)<=(b)?(a):(b))
9 ++i和i++的區別
++i先自增1,再返回,i++先返回i,再自增1
10 volatile有什麼作用
狀態暫存器一類的並行裝置硬體暫存器。
一箇中斷服務子程式會訪問到的非自動變數。
多執行緒間被幾個任務共享的變數。
注意:雖然volatile在嵌入式方面應用比較多,但是在PC軟體的多執行緒中,volatile修飾的臨界變數也是非常實用的。
11 一個引數可以既是const又是volatile嗎
可以,用const和volatile同時修飾變數,表示這個變數在程式內部是隻讀的,不能改變的,只在程式外部條件變化下改變,並且編譯器不會最佳化這個變數。每次使用這個變數時,都要小心地去記憶體讀取這個變數的值,而不是去暫存器讀取它的備份。
注意:在此一定要注意const的意思,const只是不允許程式中的程式碼改變某一變數,其在編譯期發揮作用,它並沒有實際地禁止某段記憶體的讀寫特性。
12 a 和&a 有什麼區別
&a:其含義就是“變數a的地址”。
*a:用在不同的地方,含義也不一樣。
在宣告語句中,*a只說明a是一個指標變數,如int *a;
在其他語句中,*a前面沒有運算元且a是一個指標時,*a代表指標a指向的地址記憶體放的資料,如b=*a;
*a前面有運算元且a是一個普通變數時,
a代表乘以a,如c=b
a。
13 用C 編寫一個死迴圈程式
while(1)
{ }
注意:很多種途徑都可實現同一種功能,但是不同的方法時間和空間佔用度不同,特別是對於嵌入 式軟體,處理器速度比較慢,儲存空間較小,所以時間和空間優勢是選擇各種方法的首要考慮條件。
14 結構體記憶體對齊問題
請寫出以下程式碼的輸出結果:
#include
struct S1
{
int i:8;
char j:4;
int a:4;
double b;
};
struct S2
{
int i:8;
char j:4;
double b;
int a:4;
};
struct S3
{
int i;
char j;
double b;
int a;
};
int main()
{
printf(“%d\n”,sizeof(S1)); // 輸出8
printf(“%d\n”,sizeof(S1); // 輸出12
printf(“%d\n”,sizeof(Test3)); // 輸出8
return 0;
}
sizeof(S1)=16
sizeof(S2)=24
sizeof(S3)=32
說明:結構體作為一種複合資料型別,其構成元素既可以是基本資料型別的變數,也可以是一些複合型型別資料。對此,編譯器會自動進行成員變數的對齊以提高運算效率。預設情況下,按自然對齊條件分配空間。各個成員按照它們被宣告的順序在記憶體中順序儲存,第一個成員的地址和整個結構的地址相同,向結構體成員中size最大的成員對齊。
許多實際的計算機系統對基本型別資料在記憶體中存放的位置有限制,它們會要求這些資料的首地址的值是某個數k(通常它為4或8)的倍數,而這個k則被稱為該資料型別的對齊模數。
15 全域性變數和區域性變數有什麼區別?實怎麼實現的?作業系統和編譯器是怎麼知道的?
全域性變數是整個程式都可訪問的變數,誰都可以訪問,生存期在整個程式從執行到結束(在程式結束時所佔記憶體釋放);
而區域性變數存在於模組(子程式,函式)中,只有所在模組可以訪問,其他模組不可直接訪問,模組結束(函式呼叫完畢),區域性變數消失,所佔據的記憶體釋放。
作業系統和編譯器,可能是透過記憶體分配的位置來知道的,全域性變數分配在全域性資料段並且在程式開始執行的時候被載入。區域性變數則分配在堆疊裡面。
16 簡述C、C++程式編譯的記憶體分配情況
從靜態儲存區域分配:
記憶體在程式編譯時就已經分配好,這塊記憶體在程式的整個執行期間都存在。速度快、不容易出錯, 因為有系統會善後。例如全域性變數,static 變數,常量字串等。
在棧上分配:
在執行函式時,函式內區域性變數的儲存單元都在棧上建立,函式執行結束時這些儲存單元自動被釋 放。棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限。大小為2M。
從堆上分配:
即動態記憶體分配。程式在執行的時候用 malloc 或new 申請任意大小的記憶體,程式設計師自己負責在何 時用free 或delete 釋放記憶體。動態記憶體的生存期由程式設計師決定,使用非常靈活。如果在堆上分配了空間,就有責任回收它,否則執行的程式會出現記憶體洩漏,另外頻繁地分配和釋放不同大小的堆空間將會產生 堆內碎塊。
一個C、C++程式編譯時記憶體分為5 大儲存區:堆區、棧區、全域性區、文字常量區、程式程式碼區。
17 簡述strcpy、sprintf 與memcpy 的區別
操作物件不同,strcpy 的兩個操作物件均為字串,sprintf 的操作源物件可以是多種資料型別, 目的操作物件是字串,memcpy 的兩個物件就是兩個任意可操作的記憶體地址,並不限於何種資料型別。
執行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。
實現功能不同,strcpy 主要實現字串變數間的複製,sprintf 主要實現其他資料型別格式到字 符串的轉化,memcpy 主要是記憶體塊間的複製。
注意:strcpy、sprintf 與memcpy 都可以實現複製的功能,但是針對的物件不同,根據實際需求,來 選擇合適的函式實現複製功能。
18 請解析(*(void (*)( ) )0)( )的含義
void (*0)( ) :
是一個返回值為void,引數為空的函式指標0。
(void (*)( ))0:
把0轉變成一個返回值為void,引數為空的函式指標。
*(void (*)( ))0:
在上句的基礎上加*表示整個是一個返回值為void,無引數,並且起始地址為0的函式的名字。
(*(void (*)( ))0)( ):
這就是上句的函式名所對應的函式的呼叫。
19 C語言的指標和引用和c++的有什麼區別?
指標有自己的一塊空間,而引用只是一個別名;
使用sizeof看一個指標的大小是4,而引用則是被引用物件的大小;
作為引數傳遞時,指標需要被解引用才可以對物件進行操作,而直接對引 用的修改都會改變引用所指向的物件;
可以有const指標,但是沒有const引用;
指標在使用中可以指向其它物件,但是引用只能是一個物件的引用,不能 被改變;
指標可以有多級指標(**p),而引用止於一級;
指標和引用使用++運算子的意義不一樣;
如果返回動態記憶體分配的物件或者記憶體,必須使用指標,引用可能引起記憶體洩露。
20 typedef 和define 有什麼區別
用法不同:typedef 用來定義一種資料型別的別名,增強程式的可讀性。define 主要用來定義 常量,以及書寫複雜使用頻繁的宏。
執行時間不同:typedef 是編譯過程的一部分,有型別檢查的功能。define 是宏定義,是預編譯的部分,其發生在編譯之前,只是簡單的進行字串的替換,不進行型別的檢查。
作用域不同:typedef 有作用域限定。define 不受作用域約束,只要是在define 聲明後的引用 都是正確的。
對指標的操作不同:typedef 和define 定義的指標時有很大的區別。
注意:typedef 定義是語句,因為句尾要加上分號。而define 不是語句,千萬不能在句尾加分號。
21 指標常量與常量指標區別
指標常量是指定義了一個指標,這個指標的值只能在定義時初始化,其他地方不能改變。常量指標 是指定義了一個指標,這個指標指向一個只讀的物件,不能透過常量指標來改變這個物件的值。 指標常量強調的是指標的不可改變性,而常量指標強調的是指標對其所指物件的不可改變性。
注意:無論是指標常量還是常量指標,其最大的用途就是作為函式的形式引數,保證實參在被呼叫 函式中的不可改變特性。
22 簡述佇列和棧的異同
佇列和棧都是線性儲存結構,但是兩者的插入和刪除資料的操作不同,佇列是“先進先出”,棧是 “後進先出”。
注意:區別棧區和堆區。堆區的存取是“順序隨意”,而棧區是“後進先出”。棧由編譯器自動分 配釋放 ,存放函式的引數值,區域性變數的值等。其操作方式類似於資料結構中的棧。堆一般由程式設計師 分配釋放, 若程式設計師不釋放,程式結束時可能由OS 回收。分配方式類似於連結串列。 它與本題中的堆和棧是兩回事。堆疊只是一種資料結構,而堆區和棧區是程式的不同記憶體儲存區域。
23 設定地址為0x67a9 的整型變數的值為0xaa66
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa66;
注意:這道題就是強制型別轉換的典型例子,無論在什麼平臺地址長度和整型資料的長度是一樣的, 即一個整型資料可以強制轉換成地址指標型別,只要有意義即可。
24 編碼實現字串轉化為數字
編碼實現函式atoi(),設計一個程式,把一個字串轉化為一個整型數值。例如數字:“5486321 ”, 轉化成字元:5486321。
int myAtoi(const char * str)
{
int num = 0; //儲存轉換後的數值
int isNegative = 0; //記錄字串中是否有負號
int n =0;
char *p = str;
if(p == NULL) //判斷指標的合法性
{
return -1;
}
while(*p++ != ‘\0’) //計算數字符串度
{
n++;
}
p = str;
if(p[0] == ‘-’) //判斷陣列是否有負號
{
isNegative = 1;
}
char temp = ‘0’;
for(int i = 0 ; i < n; i++)
{
char temp = *p++;
if(temp > ‘9’ ||temp < ‘0’) //濾除非數字字元
{
continue;
}
if(num !=0 || temp != ‘0’) //濾除字串開始的0 字元
{
temp -= 0x30; //將數字字元轉換為數值
num += temp *int( pow(10 , n - 1 -i) );
}
}
if(isNegative) //如果字串中有負號,將數值取反
{
return (0 - num);
}
else
{
return num; //返回轉換後的數值
}
}
25 C語言的結構體和C++的有什麼區別
C語言的結構體是不能有函式成員的,而C++的類可以有。
C語言的結構體中資料成員是沒有private、public和protected訪問限定的。而C++的類的成員有這些訪問限定。
C語言的結構體是沒有繼承關係的,而C++的類卻有豐富的繼承關係。
注意:雖然C的結構體和C++的類有很大的相似度,但是類是實現面向物件的基礎。而結構體只可以簡單地理解為類的前身。
26 簡述指標常量與常量指標的區別
指標常量是指定義了一個指標,這個指標的值只能在定義時初始化,其他地方不能改變。常量指標是指定義了一個指標,這個指標指向一個只讀的物件,不能透過常量指標來改變這個物件的值。
指標常量強調的是指標的不可改變性,而常量指標強調的是指標對其所指物件的不可改變性。
注意:無論是指標常量還是常量指標,其最大的用途就是作為函式的形式引數,保證實參在被呼叫函式中的不可改變特性。
27 如何避免“野指標”
指標變數宣告時沒有被初始化。解決辦法:指標宣告時初始化,可以是具體的地址值,也可讓它指向NULL。
指標p被free或者delete之後,沒有置為NULL。解決辦法:指標指向的記憶體空間被釋放後指標應該指向NULL。
指標操作超越了變數的作用範圍。解決辦法:在變數的作用域結束前釋放掉變數的地址空間並且讓指標指向NULL。
28 控制代碼和指標的區別和聯絡是什麼?
控制代碼和指標其實是兩個截然不同的概念。Windows系統用控制代碼標記系統資源,隱藏系統的資訊。你只要知道有這個東西,然後去呼叫就行了,它是個32it的uint。指標則標記某個物理記憶體地址,兩者是不同的概念。
29 new/delete與malloc/free的區別是什麼
new能自動計算需要分配的記憶體空間,而malloc需要手工計算位元組數。
int *p = new int[2];
int *q = (int *)malloc(2*sizeof(int));
new與delete直接帶具體型別的指標,malloc和free返回void型別的指標。
new型別是安全的,而malloc不是。例如int *p = new float[2];就會報錯;而int
p = malloc(2
sizeof(int))編譯時編譯器就無法指出錯誤來。
new一般分為兩步:new操作和構造。new操作對應與malloc,但new操作可以過載,可以自定義記憶體分配策略,不做記憶體分配,甚至分配到非記憶體裝置上,而malloc不行。
new呼叫建構函式,malloc不能;delete呼叫解構函式,而free不能。
malloc/free需要庫檔案stdlib。h的支援,new/delete則不需要!
注意:delete和free被呼叫後,記憶體不會立即回收,指標也不會指向空,delete或free僅僅是告訴作業系統,這一塊記憶體被釋放了,可以用作其他用途。但是由於沒有重新對這塊記憶體進行寫操作,所以記憶體中的變數數值並沒有發生變化,出現野指標的情況。因此,釋放完記憶體後,應該講該指標指向NULL。
30 說一說extern“C”
extern “C”的主要作用就是為了能夠正確實現C++程式碼呼叫其他C語言程式碼。加上extern “C”後,會指示編譯器這部分程式碼按C語言(而不是C++)的方式進行編譯。由於C++支援函式過載,因此編譯器編譯函式的過程中會將函式的引數型別也加到編譯後的程式碼中,而不僅僅是函式名;而C語言並不支援函式過載,因此編譯C語言程式碼的函式時不會帶上函式的引數型別,一般只包括函式名。
這個功能十分有用處,因為在C++出現以前,很多程式碼都是C語言寫的,而且很底層的庫也是C語言寫的,為了更好的支援原來的C程式碼和已經寫好的C語言庫,需要在C++中儘可能的支援C,而extern “C”就是其中的一個策略。
C++程式碼呼叫C語言程式碼
在C++的標頭檔案中使用
在多個人協同開發時,可能有的人比較擅長C語言,而有的人擅長C++,這樣的情況下也會有用到
31 請你來說一下C++中struct和class的區別
在C++中,class和struct做型別定義是隻有兩點區別:
預設繼承許可權不同,class繼承預設是private繼承,而struct預設是public繼承
class還可用於定義模板引數,像typename,但是關鍵字struct不能同於定義模板引數 C++保留struct關鍵字,原因
保證與C語言的向下相容性,C++必須提供一個struct
C++中的struct定義必須百分百地保證與C語言中的struct的向下相容性,把C++中的最基本的物件單元規定為class而不是struct,就是為了避免各種相容性要求的限制
對struct定義的擴充套件使C語言的程式碼能夠更容易的被移植到C++中
32 C++類內可以定義引用資料成員嗎?
可以,必須透過成員函式初始化列表初始化。
33 C++中類成員的訪問許可權
C++透過 public、protected、private 三個關鍵字來控制成員變數和成員函式的訪問許可權,它們分別表示公有的、受保護的、私有的,被稱為成員訪問限定符。在類的內部(定義類的程式碼內部),無論成員被宣告為 public、protected 還是 private,都是可以互相訪問的,沒有訪問許可權的限制。在類的外部(定義類的程式碼之外),只能透過物件訪問成員,並且透過物件只能訪問 public 屬性的成員,不能訪問 private、protected 屬性的成員
34 什麼是右值引用,跟左值又有什麼區別?
左值和右值的概念:
左值:能取地址,或者具名物件,表示式結束後依然存在的持久物件;
右值:不能取地址,匿名物件,表示式結束後就不再存在的臨時物件; 區別:
左值能定址,右值不能;
左值能賦值,右值不能;
左值可變,右值不能(僅對基礎型別適用,使用者自定義型別右值引用可以透過成員函式改變);
35 面向物件的三大特徵
封裝性:將客觀事物抽象成類,每個類對自身的資料和方法實行 protection (private , protected , public )。
繼承性:廣義的繼承有三種實現形式:實現繼承(使用基類的屬性和方法而無需額外編碼的能力)、可 視繼承(子窗體使用父窗體的外觀和實現程式碼)、介面繼承(僅使用屬性和方法,實現滯後到子類實現)。
多型性:是將父類物件設定成為和一個或更多它的子物件相等的技術。用子類物件給父類物件賦值 之後,父類物件就可以根據當前賦值給它的子物件的特性以不同的方式運作。
36 說一說c++中四種cast轉換
C++中四種類型轉換是:static_cast, dynamic_cast, const_cast, reinterpret_cast
1、const_cast
用於將const變數轉為非const
2、static_cast
用於各種隱式轉換,比如非const轉const,void*轉指標等, static_cast能用於多型向上轉化,如果向下轉能成功但是不安全,結果未知;
3、dynamic_cast
用於動態型別轉換。只能用於含有虛擬函式的類,用於類層次間的向上和向下轉化。只能轉指標或引用。向下轉化時,如果是非法的***對於指標返回NULL,對於引用拋異常***。要深入瞭解內部轉換的原理。
向上轉換:指的是子類向基類的轉換
向下轉換:指的是基類向子類的轉換
它透過判斷在執行到該語句的時候變數的執行時型別和要轉換的型別是否相同來判斷是否能夠進行向下轉換。
4、reinterpret_cast
幾乎什麼都可以轉,比如將int轉指標,可能會出問題,儘量少用;
5、為什麼不使用C的強制轉換?
C的強制轉換表面上看起來功能強大什麼都能轉,但是轉化不夠明確,不能進行錯誤檢查,容易出錯。
37 C++的空類有哪些成員函式
預設建構函式。
預設複製建構函式。
預設解構函式。
預設賦值運算子。
預設取址運算子。
預設取址運算子 const 。
注意:有些書上只是簡單的介紹了前四個函式。沒有提及後面這兩個函式。但後面這兩個函式也是 空類的預設函式。另外需要注意的是,只有當實際使用這些函式的時候,編譯器才會去定義它們。
38 對c++中的smart pointer四個智慧指標:shared_ptr,unique_ptr,weak_ptr,auto_ptr的理解
C++裡面的四個智慧指標: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中後三個是c++11支援,並且第一個已經被11棄用。
智慧指標的作用是管理一個指標,因為存在以下這種情況:申請的空間在函式結束時忘記釋放,造成記憶體洩漏。使用智慧指標可以很大程度上的避免這個問題,因為智慧指標就是一個類,當超出了類的作用域是,類會自動呼叫解構函式,解構函式會自動釋放資源。所以智慧指標的作用原理就是在函式結束時自動釋放記憶體空間,不需要手動釋放記憶體空間。
auto_ptr(c++98的方案,cpp11已經拋棄)
採用所有權模式。
auto_ptr< string> p1 (new string (“I reigned lonely as a cloud。”));
auto_ptr
p2 = p1; //auto_ptr不會報錯。
此時不會報錯,p2剝奪了p1的所有權,但是當程式執行時訪問p1將會報錯。所以auto_ptr的缺點是:存在潛在的記憶體崩潰問題!
unique_ptr(替換auto_ptr)
unique_ptr實現獨佔式擁有或嚴格擁有概念,保證同一時間內只有一個智慧指標可以指向該物件。它對於避免資源洩露(例如“以new建立物件後因為發生異常而忘記呼叫delete”)特別有用。
採用所有權模式。
unique_ptr
unique_ptr
p4 = p3;//此時會報錯!!
編譯器認為p4=p3非法,避免了p3不再指向有效資料的問題。因此,unique_ptr比auto_ptr更安全。
另外unique_ptr還有更聰明的地方:當程式試圖將一個 unique_ptr 賦值給另一個時,如果源 unique_ptr 是個臨時右值,編譯器允許這麼做;如果源 unique_ptr 將存在一段時間,編譯器將禁止這麼做,比如:
unique_ptr
unique_ptr
pu2 = pu1; // #1 not allowed
unique_ptr
pu3 = unique_ptr
其中#1留下懸掛的unique_ptr(pu1),這可能導致危害。而#2不會留下懸掛的unique_ptr,因為它呼叫 unique_ptr 的建構函式,該建構函式建立的臨時物件在其所有權讓給 pu3 後就會被銷燬。這種隨情況而已的行為表明,unique_ptr 優於允許兩種賦值的auto_ptr 。
注:如果確實想執行類似與#1的操作,要安全的重用這種指標,可給它賦新值。C++有一個標準庫函式std::move(),讓你能夠將一個unique_ptr賦給另一個。例如:
unique_ptr
ps1 = demo(”hello“);
ps2 = move(ps1);
ps1 = demo(”alexia“);
cout << *ps2 << *ps1 << endl;
shared_ptr
shared_ptr實現共享式擁有概念。多個智慧指標可以指向相同物件,該物件和其相關資源會在“最後一個引用被銷燬”時候釋放。從名字share就可以看出了資源可以被多個指標共享,它使用計數機制來表明資源被幾個指標共享。可以透過成員函式use_count()來檢視資源的所有者個數。除了可以透過new來構造,還可以透過傳入auto_ptr, unique_ptr,weak_ptr來構造。當我們呼叫release()時,當前指標會釋放資源所有權,計數減一。當計數等於0時,資源會被釋放。
shared_ptr 是為了解決 auto_ptr 在物件所有權上的侷限性(auto_ptr 是獨佔的), 在使用引用計數的機制上提供了可以共享所有權的智慧指標。
成員函式:
use_count 返回引用計數的個數
unique 返回是否是獨佔所有權( use_count 為 1)
swap 交換兩個 shared_ptr 物件(即交換所擁有的物件)
reset 放棄內部物件的所有權或擁有物件的變更, 會引起原有物件的引用計數的減少
get 返回內部物件(指標), 由於已經過載了()方法, 因此和直接使用物件是一樣的。如 shared_ptrsp(new int(1)); sp 與 sp。get()是等價的
weak_ptr
weak_ptr 是一種不控制物件生命週期的智慧指標, 它指向一個 shared_ptr 管理的物件。 進行該物件的記憶體管理的是那個強引用的 shared_ptr。 weak_ptr只是提供了對管理物件的一個訪問手段。weak_ptr 設計的目的是為配合 shared_ptr 而引入的一種智慧指標來協助 shared_ptr 工作, 它只可以從一個 shared_ptr 或另一個 weak_ptr 物件構造, 它的構造和析構不會引起引用記數的增加或減少。weak_ptr是用來解決shared_ptr相互引用時的死鎖問題,如果說兩個shared_ptr相互引用,那麼這兩個指標的引用計數永遠不可能下降為0,資源永遠不會釋放。它是對物件的一種弱引用,不會增加物件的引用計數,和shared_ptr之間可以相互轉化,shared_ptr可以直接賦值給它,它可以透過呼叫lock函式來獲得shared_ptr。
class B;
class A
{
public:
shared_ptr pb_;
~A()
{
cout<<”A delete
“;
}
};
class B
{
public:
shared_ptr pa_;
~B()
{
cout<<”B delete
“;
}
};
void fun()
{
shared_ptr pb(new B());
shared_ptr pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout< cout< } int main() { fun(); return 0; } 可以看到fun函式中pa ,pb之間互相引用,兩個資源的引用計數為2,當要跳出函式時,智慧指標pa,pb析構時兩個資源引用計數會減一,但是兩者引用計數還是為1,導致跳出函式時資源沒有被釋放(A B的解構函式沒有被呼叫),如果把其中一個改為weak_ptr就可以了,我們把類A裡面的shared_ptr pb_; 改為weak_ptr pb_; 執行結果如下,這樣的話,資源B的引用開始就只有1,當pb析構時,B的計數變為0,B得到釋放,B釋放的同時也會使A的計數減一,同時pa析構時使A的計數減一,那麼A的計數為0,A得到釋放。 注意:不能透過weak_ptr直接訪問物件的方法,比如B物件中有一個方法print(),我們不能這樣訪問,pa->pb_->print(); 英文pb_是一個weak_ptr,應該先把它轉化為shared_ptr,如:shared_ptr p = pa->pb_。lock(); p->print(); 39 說說強制型別轉換運算子 static_cast 用於非多型型別的轉換 不執行執行時型別檢查(轉換安全性不如 dynamic_cast) 通常用於轉換數值資料型別(如 float -> int) 可以在整個類層次結構中移動指標,子類轉化為父類安全(向上轉換),父類轉化為子類不安全(因為子類可能有不在父類的欄位或方法) dynamic_cast 用於多型型別的轉換 執行行執行時型別檢查 只適用於指標或引用 對不明確的指標的轉換將失敗(返回 nullptr),但不引發異常 可以在整個類層次結構中移動指標,包括向上轉換、向下轉換 const_cast 用於刪除 const、volatile 和 __unaligned 特性(如將 const int 型別轉換為 int 型別 ) reinterpret_cast 用於位的簡單重新解釋 濫用 reinterpret_cast 運算子可能很容易帶來風險。除非所需轉換本身是低級別的,否則應- 使用其他強制轉換運算子之一。 允許將任何指標轉換為任何其他指標型別(如 char* 到 int* 或 One_class* 到 Unrelated_class* 之類的轉換,但其本身並不安全) 也允許將任何整數型別轉換為任何指標型別以及反向轉換。 reinterpret_cast 運算子不能丟掉 const、volatile 或 __unaligned 特性。 reinterpret_cast 的一個實際用途是在雜湊函式中,即,透過讓兩個不同的值幾乎不以相同的索引結尾的方式將值對映到索引。 bad_cast 由於強制轉換為引用型別失敗,dynamic_cast 運算子引發 bad_cast 異常。 bad_cast 使用 try { Circle& ref_circle = dynamic_cast } catch (bad_cast b) { cout << ”Caught: “ << b。what(); } 40 談談你對複製建構函式和賦值運算子的認識 複製建構函式和賦值運算子過載有以下兩個不同之處: 複製建構函式生成新的類物件,而賦值運算子不能。 由於複製建構函式是直接構造一個新的類物件,所以在初始化這個物件之前不用檢驗源物件 是否和新建物件相同。而賦值運算子則需要這個操作,另外賦值運算中如果原來的物件中有記憶體分配要先把記憶體釋放掉。 注意:當有類中有指標型別的成員變數時,一定要重寫複製建構函式和賦值運算子,不要使用預設 的。 41 在C++中,使用malloc申請的記憶體能否透過delete釋放?使用new申請的記憶體能否用free? 不能,malloc /free主要為了相容C,new和delete 完全可以取代malloc /free的。malloc /free的操作物件都是必須明確大小的。而且不能用在動態類上。new 和delete會自動進行型別檢查和大小,malloc/free不能執行建構函式與解構函式,所以動態物件它是不行的。當然從理論上說使用malloc申請的記憶體是可以透過delete釋放的。不過一般不這樣寫的。而且也不能保證每個C++的執行時都能正常。 42 用C++設計一個不能被繼承的類 template { friend T; private: A() {} ~A() {} }; class B : virtual public A { public: B() {} ~B() {} }; class C : virtual public B { public: C() {} ~C() {} }; void main( void ) { B b; //C c; return; } 注意:建構函式是繼承實現的關鍵,每次子類物件構造時,首先呼叫的是父類的建構函式,然後才 是自己的。 43 C++自己實現一個String類 #include #include using namespace std; class String{ public: // 預設建構函式 String(const char *str = nullptr); // 複製建構函式 String(const String &str); // 解構函式 ~String(); // 字串賦值函式 String& operator=(const String &str); private: char *m_data; int m_size; }; // 建構函式 String::String(const char *str) { if(str == nullptr) // 加分點:對m_data加NULL 判斷 { m_data = new char[1]; // 得分點:對空字串自動申請存放結束標誌‘\0’的 m_data[0] = ‘\0’; m_size = 0; } else { m_size = strlen(str); m_data = new char[m_size + 1]; strcpy(m_data, str); } } // 複製建構函式 String::String(const String &str) // 得分點:輸入引數為const型 { m_size = str。m_size; m_data = new char[m_size + 1]; //加分點:對m_data加NULL 判斷 strcpy(m_data, str。m_data); } // 解構函式 String::~String() { delete[] m_data; } // 字串賦值函式 String& String::operator=(const String &str) // 得分點:輸入引數為const { if(this == &str) //得分點:檢查自賦值 return *this; delete[] m_data; //得分點:釋放原有的記憶體資源 m_size = strlen(str。m_data); m_data = new char[m_size + 1]; //加分點:對m_data加NULL 判斷 strcpy(m_data, str。m_data); return *this; //得分點:返回本物件的引用 } 44 訪問基類的私有虛擬函式 寫出以下程式的輸出結果: #include class A { virtual void g() { cout << ”A::g“ << endl; } private: virtual void f() { cout << ”A::f“ << endl; } }; class B : public A { void g() { cout << ”B::g“ << endl; } virtual void h() { cout << ”B::h“ << endl; } }; typedef void( *Fun )( void ); void main() { B b; Fun pFun; for(int i = 0 ; i < 3; i++) { pFun = ( Fun )*( ( int* ) * ( int* )( &b ) + i ); pFun(); } } 輸出結果: B::g A::f B::h 注意:考察了面試者對虛擬函式的理解程度。一個對虛擬函式不瞭解的人很難正確的做出本題。 在學習面向物件的多型性時一定要深刻理解虛擬函式表的工作原理。 45 對虛擬函式和多型的理解 多型的實現主要分為靜態多型和動態多型,靜態多型主要是過載,在編譯的時候就已經確定;動態多型是用虛擬函式機制實現的,在執行期間動態繫結。舉個例子:一個父類型別的指標指向一個子類物件時候,使用父類的指標去呼叫子類中重寫了的父類中的虛擬函式的時候,會呼叫子類重寫過後的函式,在父類中宣告為加了virtual關鍵字的函式,在子類中重寫時候不需要加virtual也是虛擬函式。 虛擬函式的實現:在有虛擬函式的類中,類的最開始部分是一個虛擬函式表的指標,這個指標指向一個虛擬函式表,表中放了虛擬函式的地址,實際的虛擬函式在程式碼段(。text)中。當子類繼承了父類的時候也會繼承其虛擬函式表,當子類重寫父類中虛擬函式時候,會將其繼承到的虛擬函式表中的地址替換為重新寫的函式地址。使用了虛擬函式,會增加訪問記憶體開銷,降低效率。 46 簡述類成員函式的重寫、過載和隱藏的區別 (1)重寫和過載主要有以下幾點不同。 範圍的區別:被重寫的和重寫的函式在兩個類中,而過載和被過載的函式在同一個類中。 引數的區別:被重寫函式和重寫函式的引數列表一定相同,而被過載函式和過載函式的引數列表一 定不同。 virtual 的區別:重寫的基類中被重寫的函式必須要有virtual 修飾,而過載函式和被過載函式可以被 virtual 修飾,也可以沒有。 (2)隱藏和重寫、過載有以下幾點不同。 與過載的範圍不同:和重寫一樣,隱藏函式和被隱藏函式不在同一個類中。 引數的區別:隱藏函式和被隱藏的函式的引數列表可以相同,也可不同,但是函式名肯定要相同。 當引數不相同時,無論基類中的引數是否被virtual 修飾,基類的函式都是被隱藏,而不是被重寫。 注意:雖然過載和覆蓋都是實現多型的基礎,但是兩者實現的技術完全不相同,達到的目的也是完 全不同的,覆蓋是動態態繫結的多型,而過載是靜態繫結的多型。 47 連結串列和陣列有什麼區別 儲存形式:陣列是一塊連續的空間,宣告時就要確定長度。連結串列是一塊可不連續的動態空間, 長度可變,每個結點要儲存相鄰結點指標。 資料查詢:陣列的線性查詢速度快,查詢操作直接使用偏移地址。連結串列需要按順序檢索結點, 效率低。 資料插入或刪除:連結串列可以快速插入和刪除結點,而陣列則可能需要大量資料移動。 越界問題:連結串列不存在越界問題,陣列有越界問題。 注意:在選擇陣列或連結串列資料結構時,一定要根據實際需要進行選擇。陣列便於查詢,連結串列便於插 入刪除。陣列節省空間但是長度固定,連結串列雖然變長但是佔了更多的儲存空間。 48 用兩個棧實現一個佇列的功能 typedef struct node { int data; node *next; }node,*LinkStack; //建立空棧: LinkStack CreateNULLStack( LinkStack &S) { S = (LinkStack)malloc( sizeof( node ) ); // 申請新結點 if( NULL == S) { printf(”Fail to malloc a new node。\n“); return NULL; } S->data = 0; //初始化新結點 S->next = NULL; return S; } //棧的插入函式: LinkStack Push( LinkStack &S, int data) { if( NULL == S) //檢驗棧 { printf(”There no node in stack!“); return NULL; } LinkStack p = NULL; p = (LinkStack)malloc( sizeof( node ) ); // 申請新結點 if( NULL == p) { printf(”Fail to malloc a new node。\n“); return S; } if( NULL == S->next) { p->next = NULL; } else { p->next = S->next; } p->data = data; //初始化新結點 S->next = p; //插入新結點 return S; } //出棧函式: node Pop( LinkStack &S) { node temp; temp。data = 0; temp。next = NULL; if( NULL == S) //檢驗棧 { printf(”There no node in stack!“); return temp; } temp = *S; if( S->next == NULL ) { printf(”The stack is NULL,can‘t pop!\n“); return temp; } LinkStack p = S ->next; //節點出棧 S->next = S->next->next; temp = *p; free( p ); p = NULL; return temp; } //雙棧實現佇列的入隊函式: LinkStack StackToQueuPush( LinkStack &S, int data) { node n; LinkStack S1 = NULL; CreateNULLStack( S1 ); //建立空棧 while( NULL != S->next ) //S 出棧入S1 { n = Pop( S ); Push( S1, n。data ); } Push( S1, data ); //新結點入棧 while( NULL != S1->next ) //S1 出棧入S { n = Pop( S1 ); Push( S, n。data ); } return S; } 注意:用兩個棧能夠實現一個佇列的功能,那用兩個佇列能否實現一個佇列的功能呢?結果是否定 的,因為棧是先進後出,將兩個棧連在一起,就是先進先出。而佇列是現先進先出,無論多少個連在一 起都是先進先出,而無法實現先進後出。 49 vector的底層原理 vector底層是一個動態陣列,包含三個迭代器,start和finish之間是已經被使用的空間範圍,end_of_storage是整塊連續空間包括備用空間的尾部。 當空間不夠裝下資料(vec。push_back(val))時,會自動申請另一片更大的空間(1。5倍或者2倍),然後把原來的資料複製到新的記憶體空間,接著釋放原來的那片空間[vector記憶體增長機制]。 當釋放或者刪除(vec。clear())裡面的資料時,其儲存空間不釋放,僅僅是清空了裡面的資料。因此,對vector的任何操作一旦引起了空間的重新配置,指向原vector的所有迭代器會都失效了。 50 vector中的reserve和resize的區別 reserve是直接擴充到已經確定的大小,可以減少多次開闢、釋放空間的問題(最佳化push_back),就可以提高效率,其次還可以減少多次要複製資料的問題。reserve只是保證vector中的空間大小(capacity)最少達到引數所指定的大小n。reserve()只有一個引數。 resize()可以改變有效空間的大小,也有改變預設值的功能。capacity的大小也會隨著改變。resize()可以有多個引數。 51 vector中的size和capacity的區別 size表示當前vector中有多少個元素(finish - start); capacity函式則表示它已經分配的記憶體中可以容納多少元素(end_of_storage - start); 52 vector中erase方法與algorithn中的remove方法區別 vector中erase方法真正刪除了元素,迭代器不能訪問了 remove只是簡單地將元素移到了容器的最後面,迭代器還是可以訪問到。因為algorithm透過迭代器進行操作,不知道容器的內部結構,所以無法進行真正的刪除。 53 vector迭代器失效的情況 當插入一個元素到vector中,由於引起了記憶體重新分配,所以指向原記憶體的迭代器全部失效。 當刪除容器中一個元素後,該迭代器所指向的元素已經被刪除,那麼也造成迭代器失效。erase方法會返回下一個有效的迭代器,所以當我們要刪除某個元素時,需要it=vec。erase(it);。 54 正確釋放vector的記憶體(clear(), swap(), shrink_to_fit()) vec。clear():清空內容,但是不釋放記憶體。 vector()。swap(vec):清空內容,且釋放記憶體,想得到一個全新的vector。 vec。shrink_to_fit():請求容器降低其capacity和size匹配。 vec。clear();vec。shrink_to_fit();:清空內容,且釋放記憶體。 55 list的底層原理 ist的底層是一個雙向連結串列,使用連結串列儲存資料,並不會將它們儲存到一整塊連續的記憶體空間中。恰恰相反,各元素佔用的儲存空間(又稱為節點)是獨立的、分散的,它們之間的線性關係透過指標來維持,每次插入或刪除一個元素,就配置或釋放一個元素空間。 list不支援隨機存取,如果需要大量的插入和刪除,而不關心隨即存取 56 什麼情況下用vector,什麼情況下用list,什麼情況下用deque vector可以隨機儲存元素(即可以透過公式直接計算出元素地址,而不需要挨個查詢),但在非尾部插入刪除資料時,效率很低,適合物件簡單,物件數量變化不大,隨機訪問頻繁。除非必要,我們儘可能選擇使用vector而非deque,因為deque的迭代器比vector迭代器複雜很多。 list不支援隨機儲存,適用於物件大,物件數量變化頻繁,插入和刪除頻繁,比如寫多讀少的場景。 需要從首尾兩端進行插入或刪除操作的時候需要選擇deque。 57 priority_queue的底層原理 priority_queue:優先佇列,其底層是用堆來實現的。在優先佇列中,隊首元素一定是當前佇列中優先順序最高的那一個。 58 map 、set、multiset、multimap的底層原理 map 、set、multiset、multimap的底層實現都是紅黑樹,epoll模型的底層資料結構也是紅黑樹,linux系統中CFS程序排程演算法,也用到紅黑樹。 紅黑樹的特性: 每個結點或是紅色或是黑色; 根結點是黑色; 每個葉結點是黑的; 如果一個結點是紅的,則它的兩個兒子均是黑色; 每個結點到其子孫結點的所有路徑上包含相同數目的黑色結點。 59 為何map和set的插入刪除效率比其他序列容器高 因為不需要記憶體複製和記憶體移動 60 為何map和set每次Insert之後,以前儲存的iterator不會失效? 因為插入操作只是結點指標換來換去,結點記憶體沒有改變。而iterator就像指向結點的指標,記憶體沒變,指向記憶體的指標也不會變。 61 當資料元素增多時(從10000到20000),map的set的查詢速度會怎樣變化? RB-TREE用二分查詢法,時間複雜度為logn,所以從10000增到20000時,查詢次數從log10000=14次到log20000=15次,多了1次而已。 62 map 、set、multiset、multimap的特點 set和multiset會根據特定的排序準則自動將元素排序,set中元素不允許重複,multiset可以重複。 map和multimap將key和value組成的pair作為元素,根據key的排序準則自動將元素排序(因為紅黑樹也是二叉搜尋樹,所以map預設是按key排序的),map中元素的key不允許重複,multimap可以重複。 map和set的增刪改查速度為都是logn,是比較高效的。 63 為何map和set的插入刪除效率比其他序列容器高,而且每次insert之後,以前儲存的iterator不會失效? 儲存的是結點,不需要記憶體複製和記憶體移動。 插入操作只是結點指標換來換去,結點記憶體沒有改變。而iterator就像指向結點的指標,記憶體沒變,指向記憶體的指標也不會變。 64 為何map和set不能像vector一樣有個reserve函式來預分配資料? 在map和set內部儲存的已經不是元素本身了,而是包含元素的結點。也就是說map內部使用的Alloc並不是map 65 set的底層實現實現為什麼不用雜湊表而使用紅黑樹? set中元素是經過排序的,紅黑樹也是有序的,雜湊是無序的 如果只是單純的查詢元素的話,那麼肯定要選雜湊表了,因為雜湊表在的最好查詢時間複雜度為O(1),並且如果用到set中那麼查詢時間複雜度的一直是O(1),因為set中是不允許有元素重複的。而紅黑樹的查詢時間複雜度為O(lgn) 66 hash_map與map的區別?什麼時候用hash_map,什麼時候用map? 建構函式:hash_map需要hash function和等於函式,而map需要比較函式(大於或小於)。 儲存結構:hash_map以hashtable為底層,而map以RB-TREE為底層。 總的說來,hash_map查詢速度比map快,而且查詢速度基本和資料量大小無關,屬於常數級別。而map的查詢速度是logn級別。但不一定常數就比log小,而且hash_map還有hash function耗時。 如果考慮效率,特別當元素達到一定數量級時,用hash_map。 考慮記憶體,或者元素數量較少時,用map。 67 迭代器失效的問題 插入操作: 對於vector和string,如果容器記憶體被重新分配,iterators,pointers,references失效;如果沒有重新分配,那麼插入點之前的iterator有效,插入點之後的iterator失效; 對於deque,如果插入點位於除front和back的其它位置,iterators,pointers,references失效;當我們插入元素到front和back時,deque的迭代器失效,但reference和pointers有效; 對於list和forward_list,所有的iterator,pointer和refercnce有效。 刪除操作: 對於vector和string,刪除點之前的iterators,pointers,references有效;off-the-end迭代器總是失效的; 對於deque,如果刪除點位於除front和back的其它位置,iterators,pointers,references失效;當我們插入元素到front和back時,off-the-end失效,其他的iterators,pointers,references有效; 對於list和forward_list,所有的iterator,pointer和refercnce有效。 對於關聯容器map來說,如果某一個元素已經被刪除,那麼其對應的迭代器就失效了,不應該再被使用,否則會導致程式無定義的行為。 68 STL執行緒不安全的情況 在對同一個容器進行多執行緒的讀寫、寫操作時; 在每次呼叫容器的成員函式期間都要鎖定該容器; 在每個容器返回的迭代器(例如透過呼叫begin或end)的生存期之內都要鎖定該容器; 在每個在容器上呼叫的演算法執行期間鎖定該容器。