C/C++【知識點筆記】

塵虛緣_KY發表於2016-08-31
一、程式效率的提升
(1)對於過載自定義的++i和i++,++i比i++的效率高;
    原因: ++i在運算過程中不產生臨時物件,返回的就是i,是個左值,類似++i=1這樣的表示式是合法的,而i++在運算的過程中會產生臨時物件,返回的是零時物件的值,是個右值,像i++=1這樣的表示式是非法的;
    對於內建型別,單獨的i++和++i語句,現在的編譯器基本上都會優化成++i,所以就沒什麼區別了。
    例如:下面的連結串列內部迭代器
List::Iterator& List::Iterator::operator++()//前加
{
    pNote = pNote->pNext;
    return *this;
}
List::Iterator List::Iterator::operator++(int)//後加
{
    Iterator tmp(*this);
    pNote = pNote->pNext;
    return tmp;
}
 從後加的方式可以知道,物件利用自己建立一個臨時物件,然後改變自己的狀態,並返回這個臨時物件,而前加的方式直接改變自己的狀態,並返回自己的引用!由於迭代器一般都是遍歷容器的,大數應用於迴圈,所以如果該連結串列有100個元素,如下程式碼所示:
for(_SingleList::Iterator it = list.begin(); it != list.end(); ++it)
{
    //do something

for(_SingleList::Iterator it = list.begin(); it != list.end(); it++)
{
    //do something

那麼第二個程式碼就要呼叫200多個函式這樣程式的效率就不可忽視了!

(2)函式的引數傳引用比傳值的效率更高。尤其在引數為類的物件的時候;
bool Compare(string s1)   
bool Compare(string *s1)  //傳指標;
bool Compare(string &s1)  //傳引用;
bool Compare(const string &s1) //常量引用,否則不能引用常量;
第一個函式(值傳遞),則在引數傳遞和函式返回時,需要呼叫string的建構函式和解構函式兩次,而其他的三個函式---指標傳遞和引用傳遞--則不需要呼叫這兩個個函式。因為指標和引用都不會建立新的物件。如果一個構造一個物件和析構一個物件的開銷是龐大的,這就是會效率造成一定的影響。引用與使用普通值傳遞一樣方便直觀,同時具有指標傳遞的高效和能力。因為引用是一個變數的別名,對其操作等同於對實際物件操作,所以當你確定在你的函式是不會或不需要變數引數的值時,就大膽地在宣告的前面加上一個const吧,就如最後的一個函式宣告一樣。同時加上一個const還有一個好處,就是可以對常量進行引用,若不加上const修飾符,引用是不能引用常量的。
(3)如果選擇為if---if---if--,則應選擇if--else if--else或者是switch-case;後兩者比較的次數少,效率高;
例如:
if(1==a)  {dosomething;}
if(2==a)  {dosomething;}
if(3==a)  {dosomething;}
以上三個if都得執行,但是下面的:
switch(a)
{
case 1:do something; break;
case 2:do something; break;
case 3:do something; break;
}
只會執行一個case語句,提高了效率。
(4)對於類的物件有賦值和初始化時優先選擇初始化,因為這樣可以避免生成臨時物件,賦值會產生臨時物件;
father f1;
father f2(f1); //直接初始化;
father f3=f1; //賦值初始化;
  當用於類型別物件時,初始化的複製形式和直接形式有所不同:直接初始化直接呼叫與實參匹配的建構函式,複製初始化總是呼叫複製建構函式。複製初始化首先使用指定建構函式建立一個臨時物件,然後用複製建構函式將那個臨時物件複製到正在建立的物件。建立臨時物件就會呼叫建構函式和解構函式,多了兩次函式呼叫,增減了開銷。
(5)如果程式中cout使用次數很少或只用一次,則可以使用std::cout來節省空間。
   
因為這樣比匯入整個名稱空間更經濟;
(6)對於類的物件返回引用比返回物件的效率要高。
  
因為不會呼叫拷貝建構函式,生成臨時物件;但是特別注意臨時物件和區域性變數不能返回引用;
(7)六、避免使用多重繼承
   在C++中,支援多繼承,即一個子類可以有多個父類。書上都會跟我們說,多重繼承的複雜性和使用的困難,並告誡我們不要輕易使用多重繼承。其實多重繼承並不僅僅使程式和程式碼變得更加複雜,還會影響程式的執行效率。這是因為在C++中每個物件都有一個this指標指向物件本身,而C++中類對成員變數的使用是通過this的地址加偏移量來計算的,而在多重繼承的情況下,這個計算會變數更加複雜,從而降低程式的執行效率。而為了解決二義性,而使用虛基類的多重繼承對效率的影響更為嚴重,因為其繼承關係更加複雜和成員變數所屬的父類關係更加複雜。
(8)減少除法的使用。
無論是整數還是浮點數運算,除法都是一件運算速度很慢的指令,在計算機中實現除法是比較複雜的。所以要減少除法運算的次數。我們可以通過移位,或者轉化為乘法來做除法運算。
例如:4/2  ---- 4>>1;
            if(a>b/c) ----if(a*c>b)
(9)將小粒度函式宣告為行內函數(inline)
   由於呼叫函式是需要保護現場,為區域性變數分配記憶體,函式結束後還要恢復現場等開銷,而行內函數則是把它的程式碼直接寫到呼叫函式處,所以不需要這些開銷,但會使程式的原始碼長度變大。所以若是小粒度的函式,如下面的Max函式,由於不需要呼叫普通函式的開銷,所以可以提高程式的效率。
int Max(int a, int b)
{
    return a>b?a:b;
}

二、20個重點筆記。
1、C++的const比C語言#define更好的原因?

首先,它能夠明確指定型別,有型別檢查功能
其次,可以使用C++的作用域規則將定義限制在特定的函式[常函式]或檔案中
第三,可以將const用於更復雜的型別,比如陣列和結構
C語言中也有const,在C語言中分配記憶體,其與C++中const的區別是:一是作用域規則不同;另一個是,在C++中可以用const值來宣告陣列長度。在C++中const在沒有取地址和加extern時,是不分配記憶體空間的,和#define有相同的效果常量摺疊。
注意:外部要能訪問test.cpp中的const形式的n, 必須在test.cpp中定義的時候用extern限定。

test.cpp
extern const int n = 100;  //必須加extern

#include <iostream>  
using namespace std;  
extern const int n;  
int main()  
{  
    cout << n << endl;  
    return 0;  
}  
2、不能簡單地將整數賦給指標,如下所示:
int *ptr;
ptr = 0x12342342;  // type mismatch
在這裡,左邊是指向int的指標,但右邊是一個整數。在C99標準釋出之前,C語言允許這樣賦值。但C++在型別一致方面的要求更嚴格,編譯器將顯示一條錯誤訊息,通告型別不匹配。要將數字值作為地址來使用,應通過強制型別轉換將數字轉換為適當的地址型別:
int *ptr;
ptr = (int *) 0x12342342;  // type now match
這樣,賦值語句的兩邊都是整數的地址,因此這樣賦值有效。但是這樣做很危險。隨意的數值可能是指向系統的重要檔案,這樣就會是系統崩潰。
注意,pt是int值的地址並不意味著pt本身的型別是int。例如,在有些平臺中,int型別是個2位元組值,而地址是個4位元組值。
3、逗號運算子
逗號運算子的特性有下面幾個:
(1)它確保先計算第一個表示式,然後計算第二個表示式;
      i = 20, j = 2 * i; // i set to 20, then j set to 40
(2)逗號表示式的值是第二部分的值。例如,上面表示式的值為40。在所有運算子中,逗號運算子的優先順序是最低的。例如:
     cats = 17, 240; //被解釋為:(cats = 17), 240;
也就是說,將cats設定為17,後面的240不起作用。如果是:
   cats = (17, 240); //cats=240。
4、下面的程式輸出結果是多少?
#include<stdio.h>
int main()
{
int x=2,y,z;
x *=(y = z = 5); printf(“%d\n”,x); //10
z = 3;
x ==(y = z); printf(“%d\n”,x);  //10
x =(y == z); printf(“%d\n”,x);  //1 
x = (y&z); printf(“%d\n”,x);     // 與運算 3
x = (y&&z); printf(“%d\n”,x);  //1 
y = 4;
x = (y | z); printf(“%d\n”,x);//或運算 7 
x = (y || z); printf(“%d\n”,x);//1
return 0;
}
5.下面的程式輸出結果是多少?
#include<stdio.h>
main()
{
int b = 3;
int arr[] = {6,7,8,9,10};
int *ptr = arr;
*(ptr++)+=123;//先算等號右邊再算左邊的;
//*ptr=*ptr+_123;  即arr[0]=129;
//千萬不要認為是:*(ptr++)=*(ptr++)+123;
//還有就是要認清++和--的作用;

printf(“%d,%d\n”,*ptr,*(++ptr));  //8  8
//printf函式的運算設計到進棧和出棧,運算順序是從右向左;
//先算*(++ptr),後算*ptr;
printf("%d\n",arr[0]);   //129
}
6、C++儲存方案:C++三種,C++11四種
這些方案的區別就在於資料保留在記憶體中的時間。
自動儲存持續性:在函式定義中宣告的變數(包括函式引數)的儲存持續性為自動的。它們在程式開始執行其所屬的函式或程式碼塊時被建立,也即用到時分配記憶體,在執行完函式或程式碼塊時,它們使用的記憶體被釋放回收,且能夠重複使用。C++有兩種儲存持續性為自動的變數。
靜態儲存持續性:在函式定義外定義的變數和使用關鍵字static定義的變數的儲存持續性都為靜態。它們在程式整個執行過程中都存在,且靜態變數一開始就在記憶體中。C++有3種儲存持續性為靜態的變數。
動態儲存持續性:用new運算子分配的記憶體將一直存在,直到使用delete運算子將其釋放或程式結束為止。這種記憶體的儲存持續性為動態,有時被稱為自由儲存(free store)或堆(heap)。
執行緒儲存持續性(C++11):當前,多核處理器很常見,這些CPU可同時處理多個執行任務。這讓程式能夠將計算放在可並行處理的不同執行緒中。如果變數是使用關鍵字thread_local宣告的,則其生命週期與所屬的執行緒一樣長。本書不探討並行程式設計。
7、過載類的 = 號操作符時需要注意事項:
CString & CString::operator=(const CString &st)
{
 if(this == & st)  
    return * this;
 delete [] str;
 len = st.len;
 str = new char [len + 1];
 strcpy(str,st.str);
 return *this;
}
主要注意一下四點:
(1)、是否把返回值型別宣告為該型別的引用?並在結束前是否返回該例項自身的引用(保證連續賦值);
(2)、是否把引數設定為const &?(否則會呼叫拷貝建構函式,其次防止意外改變引數)
(3)、是否記得釋放自身的記憶體?(否則會記憶體洩露,指標指向了其他的地方)
(4)、是否進行了例項物件的等值判斷,是否為同一個例項?(否則釋放例項的時候,將引數的記憶體頁釋放,找不到賦值物件引起錯誤)
8、C++的多型性分為靜態多型和動態多型。 
靜態多型性:編譯期間確定具體執行哪一項操作,主要是通過函式過載和運算子過載來實現的; 
動態多型性:執行時確定具體執行哪一項操作,主要是通過虛擬函式來實現的。
9、虛擬函式原理考點,例如下面程式的輸出是什麼?
class A {
public:
    virtual void funa();
    virtual void funb();
    void func();
    static void fund();
    static int si;
private:
    int i;
    char c;
};
問:sizeof(A) = ?
解答: 
關於類佔用的記憶體空間,有以下幾點需要注意: 
(1)如果類中含有虛擬函式,則編譯器需要為類構建虛擬函式表,類中需要儲存一個指標指向這個虛擬函式表的首地址,注意不管有幾個虛擬函式,都只建立一張表,所有的虛擬函式地址都存在這張表裡,類中只需要一個指標指向虛擬函式表首地址即可。 
(2)類中的靜態成員是被類所有例項所共享的,它不計入sizeof計算的空間 
(3)類中的普通函式或靜態普通函式都儲存在棧中,不計入sizeof計算的空間 
(4)類成員採用位元組對齊的方式分配空間
答案:12(32位系統)或16(64位系統)
10、C++中可以過載的運算子:new/delete、new[]/delete[]、++等。 
        不可以過載的運算子:、.、::、?:、sizeof、typeid、.、**、不能改變運算子的優先順序。
11、過載++和–時是怎麼區分字首++和字尾++的? 
   當編譯器看到++a(先自增)時,它就呼叫operator++(a)/operator++(); 
  但當編譯器看到a++時,它就呼叫operator++(a, int) /operator++(int)
  [有型別無變數是啞元]。即編譯器通過呼叫不同的函式區別這兩種形式。
12、在C++中,如果確定了某一個建構函式的建立過程,在該建構函式中如果呼叫了其它過載的建構函式,它將不會執行其它建構函式的初始化列表部分程式碼,而是執行函式體程式碼,此時已經退化成普通函式了。例子說明如下:
class CBook {
public:
    double m_price;
    CBook() {
        CBook(8.8);
    }
    CBook(double price) : m_price(price) { }
};
int main() {
    CBook c;
    cout << c.m_price << endl;  // 此時並不會輸出理想中的8.8
}
12、C的結構體和C++結構體的區別
(1)C的結構體內不允許有函式存在,C++允許有內部成員函式,且允許該函式是虛擬函式。所以C的結構體是沒有建構函式、解構函式、和this指標的。 
(2)C的結構體對內部成員變數的訪問許可權只能是public,而C++允許public,protected,private三種。 
(3)C語言的結構體是不可以繼承的,C++的結構體是可以從其他的結構體或者類繼承過來的。
以上都是表面的區別,實際區別就是程式導向和麵向物件程式設計思路的區別:
C的結構體只是把資料變數給包裹起來了,並不涉及演算法。 
而C++是把資料變數及對這些資料變數的相關演算法給封裝起來,並且給對這些資料和類不同的訪問許可權。 
C語言中是沒有類的概念的,但是C語言可以通過結構體內建立函式指標實現物件導向思想。
13、可作為函式過載判斷依據的有:引數個數、引數型別、const修飾符; 
不可以作為過載判斷依據的有:返回型別。
14、如何將一個小數分解成整數部分和小數部分? 
要記得利用標頭檔案中的庫函式modf,下面是函式原型(記住一些實用的庫函式,避免自己重寫):
fractpart= double modf(double num, double *Intpart); // 將num分解為整數部分intpart和小數部分fractpart(返回值決定)
15、易誤解:
如果int a[5], 那麼a與&a是等價的,因為兩者地址相同。 
解答:一定要注意a與&a是不一樣的,雖然兩者地址相同,但意義型別不一樣,
&a是整個陣列物件的首地址,int (*p)[5];因此&a+1相當於a的地址值加上sizeof(int) * 5,也就是a[5],下一個物件的地址,已經越界了;
而a是陣列首地址,也就是a[0]的地址,&a[0];a[0]的型別是int,而a+1相當於a的地址加上sizeof(int),即a[1]的地址。
16、注意sizeof不是函式而是運算子,所以在計算變數所佔用空間大小時,括號是可以省略的,但在計算型別大小時括號則不能省略:
比如int i = 0; 則
sizeof int; //ERROR
sizeof i=4;  //OK
17、資料庫中nchar()和char()、varchar()和nvarchar()的區分?
char[10]

是定長的,也就是當你輸入的字元小於你指定的數目8時,它會再後面補空值-'\0'。當你輸入的字元大於指定的數時,它會擷取超出的字元。
varchar[10]  
    長度為 n 個位元組的可變長度且非 Unicode 的字元資料。n 必須是一個介於 1 和 8,000 之間的數值。儲存大小為輸入資料的位元組的實際長度,而不是 n 個位元組。所輸入的資料字元長度可以為零。
    例如輸入“hello”,那麼氣長度不是10,而是5+1=6,
    為什麼“+1”呢?這一個位元組用於儲存實際使用了多大的長度。  
nvarchar[10]
    包含 n 個字元的可變長度 Unicode 字元資料。n 的值必須介於 1 與 4,000 之間。位元組的儲存大小是所輸入字元個數的兩倍。所輸入的資料字元長度可以為零。Unicode字符集就是為了解決字符集這種不相容的問題而產生的,它所有的字元都用兩個位元組表示,即英文字元也是用兩個位元組表示。
例如兩欄位分別有欄位值:我和coffee
那麼varchar欄位佔2×2+6=10個位元組的儲存空間,而nvarchar欄位佔8×2=16個位元組的儲存空間。
從空間上考慮,用varchar合適;從效率上考慮,用char合適,關鍵是根據實際情況找到權衡點。 
   這三種從名字上看比前面三種多了個“N”。它表示儲存的是Unicode資料型別的字元。我們知道字元中,英文字元只需要一個位元組儲存就足夠了,但漢字眾多,需要兩個位元組儲存,英文與漢字同時存在時容易造成混亂,Unicode字符集就是為了解決字符集這種不相容的問題而產生的,它所有的字元都用兩個位元組表示,即英文字元也是用兩個位元組表示。nchar、nvarchar的長度是在1到4000之間。和char、varchar比較起來,nchar、nvarchar則最多儲存4000個字元,不論是英文還是漢字;而char、varchar最多能儲存8000個英文,4000個漢字。可以看出使用nchar、nvarchar資料型別時不用擔心輸入的字元是英文還是漢字,較為方便,但在儲存英文時數量上有些損失。  
所以一般來說,選擇標準:如欄位值只是英文可選擇varchar,而欄位值存在較多的雙位元組(中文、韓文等)字元時用nvarchar
優缺點:
char:可以看出,是利用空間換時間,處理比較方便,英文一個位元組儲存。
【儲存很短的資訊;固定長度的,比如人名,這些步長而且長度大體相當;十分頻繁改變的column。】
varchar:空間效率高,節約記憶體;但是時間效率有所下降,容易出現記憶體碎片,尤其當更新時的資料比後設資料量大時,比較繁瑣。另外,varchar申請的時候不能過於康概,按需索取對於相同的字串,其儲存空間雖然相同,但是對於記憶體的消耗是不同的。
18、如何解決在建構函式裡統計物件計數不準確的問題?
原因:可能因為拷貝建構函式而產生的物件,這樣就不會呼叫建構函式。解決辦法,提供一個顯示的賦值建構函式處理計數問題。
student::student(const student &st)
{
num++;
}
19、快排中中值的選取:
(1)固定元素法:取第一個或者最後一個作為partition的pivot。
(2)隨機元素法:隨機生成作為partition的pivot。
(3)三數取中或者無數取中法;
20、快速排序為什麼比堆排序效率高?
(1)比較的次數,堆排序比較的次數比快排多;有很多都是無謂的比較,快排每調整一次每個元素都是朝正確的位置進一步,但是堆排序不是,最後一個元素調整到第一個,下次可能又跳回到最後一個;隨機性太大;
(2)cache區域性性原理。堆排序都是不相鄰的,資料極不友好,快排都是相鄰的,移動的快,充分利用率cache。
   缺點:快排是遞迴呼叫,消耗的空間較大。

 21、由一個日期,怎樣知道是星期幾?
int dayofweek(int y, int m, int d)  /* 0 = Sunday */  
{  
    static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};  
    y -= m < 3;  
    return (y + y/4 - y/100 + y/400 + t[m - 1] + d) % 7;  
}  
22、memcpy()和memmove()有什麼區別?
如果源和目的引數有重疊,memmove()提供有保證的行為。
而memcpy()則不能提供這樣的保證,因此可以實現的更加有效率。
如果有疑問,最好使用memmvoe()。
23、extern可以置於變數或者函式前,以標示變數或者函式的定義在別的檔案中,提示編譯器遇到此變數和函式時在其他模組中尋找其定義。此外extern也可用來進行連結指定。也就是說extern有兩個作用:
第 一,當它與"C"一起連用時,如: extern "C" void fun(int a, int b);則告訴編譯器在編譯fun這個函式名時按著C的規則去翻譯相應的函式名而不是C++的,C++的規則在翻譯這個函式名時會把fun這個名字變得面目 全非,可能是fun@aBc_int_int#%$也可能是別的,這要看編譯器的"脾氣"了(不同的編譯器採用的方法不一樣),為什麼這麼做呢,因為 C++支援函式的過載;
第二,當extern不與"C"在一起修飾變數 或函式時,
如在標頭檔案中: extern int g_Int; 它的作用就是宣告函式或全域性變數的作用範圍的關鍵字,其宣告的函式和變數可以在本模組活其他模組中使用,記住它是一個宣告不是定義!也就是 說B模組(編譯單元)要是引用模組(編譯單元)A中定義的全域性變數或函式時,它只要包含A模組的標頭檔案即可,在編譯階段,模組B雖然找不到該函式或變數, 但它不會報錯,它會在連線時從模組A生成的目的碼中找到此函式。
24、向一個接受指標的函式傳遞二維指標時要注意的問題?
void int functon(int [][col]){}
或者:
void int function(int (*p)[col]){}

相關文章