程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 完整的學習C++的讀書路線圖(3)

完整的學習C++的讀書路線圖(3)

編輯:關於C++

指針

無疑,指針是C中最精髓的部分,因為指針可以在初始化後,可以同時擁有所指變量的兩樣東西——值和地址。這就給我們寫程序時很大的空間,可以直接與內存對話!這也同樣引出了千奇百怪的錯誤,不知道該怎麼表達,其實最根本的是要明白我們在使用指針的時候,知道我們使用的究竟是她的哪個性質!是值?還是地址?於此對應的,指針有兩種最基本的操作:一個是取地址&,主要用於初始化時的賦值操作。&必須是左值。

一個是取指向的值*,*可以取任意指向的值,返回其左值。

對指針的操作猶如打太極一般,有很多招式,但又歸於一招。最基礎的是分清指針賦值和值賦值:p1=p2;指針賦值,是p1和p2指向同一位置。

*p1=*p2;值賦值,把p2為地址的內存中的內容賦到p1為地址的內存的。

注意:指針也是有地址的,它本身也需要在內存中開辟一塊存儲,這塊存儲空間裡是他所指變量的地址,然後根據這個地址,可以找到所指變量的值!

指針可以被運算,但要注意的是指針所指向對象的類型,指針都是一樣的——4,而他的指向的解析方式是不同的,所以同樣的形式會有不同的運算方法,如:p++,對於int型和對於double型所跨越的實際地址是不同的!

指針和數組

我們使用指針的時候,其作用和其他變量相似,可以把他的行為與基本類型劃等號。但是如果是數組,就不同了,數組聲明後保有很多內存單元,每個元素都有一個內存單元,數組名不與某個單獨的內存單元相對應,而是和整個內存單元集合相對應,所以這一點和普通變量不同。

當變量做最普通的聲明時,會體現數組和指針最關鍵的區別:int array「5」;和int * p;內存的分配!這樣指針是不分配內存的 ,但是數組分配!

*/

/*一、文件包含

#include <頭文件名稱>

#include "頭文件名稱"

第一種形式 : 用來包含開發環境提供的庫頭文件,它指示編譯預處理器在開發環境設定的搜索路徑中查找所需的頭文件

第二種形式 : 用來包含自己編寫的頭文件,它指示編譯預處理器首先在當前工作目錄下搜索頭文件,如果找不到再到開發環境設定的路徑中查找。

內部包含衛哨和外部包含衛哨

在頭文件裡面使用內部包含衛哨,就是使用一種標志宏,可以放心的在同一個編譯單元及其包含的頭文件中多次包含同一個頭文件而不會造成重復包含。如:

#ifndef _STDDEF_H_INCLUDED_

#define _STDDEF_H_INCLUDED_

…… //頭文件的內容

#endif

當包含一個頭文件的時候,如果能夠始終如一地使用外部包含衛哨,可以顯著地提高編譯速度,因為當一個頭文件被一個源文件反復包含多次時,可以避免多次查找和打開頭文件地操作。如:

#if !defined(_INCLUDED_STDDEF_H_)

#include <stddef.h>

#define _INCLUDED_STDDEF_H_

#endif

建議外部包含衛哨和內部包含衛哨使用同一個標志宏,這樣可以少定義一個標志宏。如:

#if !defined_STDDEF_H_INCLUDED_

#include <stddef.h>

#endif

頭文件包含的合理順序

在頭文件中:

1、包含當前工程中所需的自定義頭文件

2、包含第三方程序庫的頭文件

3、包含標准頭文件

在源文件中:

1、包含該源文件對應的頭文件

2、包含當前工程中所需的自定義頭文件

3、包含第三方程序庫的頭文件

4、包含標准頭文件

避免重定義

如果把一個struct定義放在一個頭文件中,就有可能在一個編譯程序中多次包含這個頭文件。編譯器認為重定義是一個錯誤。如下面的例子:

// file : type.h
struct type01
{
int a,b,c;
};
// file : a.h
#include "type.h"
……
// file : b.h
#include "type.h"
……
// file main.cpp
#include "a.h"
#include "b.h"
int main(void)
{
……
}

編譯程序,編譯器給出以下的錯誤提示:

error C2011: “type01” : “struct”類型重定義

原因是頭文件type.h定義了一個struct類型type01,頭文件a.h和b.h都包含了頭文件type.h.而在main.cpp文件裡卻同時包含了頭文件a.h和b.h.因此出現了重定義的錯誤。

可以通過像以下那樣改寫type.h文件,從而避免重定義錯誤:

// file : type.h
#ifndef __TYPE_H__ // 如果標記沒被設置
#define __TYPE_H__ // 設置標記
struct type01
{
int a,b,c;
};
#endif // End of __TYPE_H__

通過這樣改寫type.h文件後,程序可以順利編譯過去了。

我們是通過測試預處理器的標記來檢查type.h頭文件是否已經包含過了。如果這個標記沒有設置,表示這個頭文件沒有被包含過,則應該設計標記。反之,如果這個標記已經設置,則表示這個頭文件已經被包含,所以應該忽略。

*/

/* C++為類中提供類成員的初始化列表

類對象的構造順序是這樣的:

1.分配內存,調用構造函數時,隱式/顯示的初始化各數據成員

2.進入構造函數後在構造函數中執行一般計算

使用初始化列表有兩個原因:

1.必須這樣做:

如果我們有一個類成員,它本身是一個類或者是一個結構,而且這個成員它只有一個帶參數的構造函數,而沒有默認構造函數,這時要對這個類成員進行初始化,就必須調用這個類成員的帶參數的構造函數,如果沒有初始化列表,那麼他將無法完成第一步,就會報錯。

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有了顯示的帶參數的構造函數,那麼他是無法依靠編譯器生成無參構造函數的,所以沒有三個int型數據,就無法創建ABC的對象。

ABC類對象是MyClass的成員,想要初始化這個對象abc,那就只能用成員初始化列表,沒有其他辦法將參數傳遞給ABC類構造函數。

另一種情況是這樣的:當類成員中含有一個const對象時,或者是一個引用時,他們也必須要通過成員初始化列表進行初始化,因為這兩種對象要在聲明後馬上初始化,而在構造函數中,做的是對他們的賦值,這樣是不被允許的。

2.效率要求這樣做:類對象的構造順序顯示,進入構造函數體後,進行的是計算,是對他們的賦值操作,顯然,賦值和初始化是不同的,這樣就體現出了效率差異,如果不用成員初始化類表,那麼類對自己的類成員分別進行的是一次隱式的默認構造函數的調用,和一次復制操作符的調用,如果是類對象,這樣做效率就得不到保障。

注意:構造函數需要初始化的數據成員,不論是否顯示的出現在構造函數的成員初始化列表中,都會在該處完成初始化,並且初始化的順序和其在聲明時的順序是一致的,與列表的先後順序無關,所以要特別注意,保證兩者順序一致才能真正保證其效率。

為了說明清楚,假設有這樣一個類:

class foo{
private :
int a, b;
};

1、foo(){}和foo(int i = 0){}都被認為是默認構造函數,因為後者是默認參數。兩者不能同時出現。

2、構造函數列表的初始化方式不是按照列表的的順序,而是按照變量聲明的順序。比如foo裡面,a在b之前,那麼會先構造a再構造b.所以無論 foo():a(b + 1), b(2){}還是foo():b(2),a(b+1){}都不會讓a得到期望的值。如果先聲明b再聲明a則會更好。

3、構造函數列表能夠對const成員初始化。比如foo裡面有一個int const c;則foo(int x) : c(x){}可以讓c值賦成x.不過需要注意的是,c必須在每個構造函數(如果有多個)都有值。

4、在繼承裡面,只有初始化列表可以構造父類的private成員。比如說

class child : public foo{

}

foo裡面的構造函數是這樣寫的:foo (int x) { a = x; }.

而在child裡面寫child(int x){ foo(x); }是通過不了編譯的。只有把父類初始化改為foo(int x) : a(x){}而子類構造寫作child (int x) : foo(x){}才可以。

另一篇關於初始化列表的文章:

C++初始化類的成員,不但可以用構造函數(constructor)完成,而且可以用初始化類成員列表來完成。MFC大量用到此方法。例如有些初學者可能不大理解如下代碼:

class A
{
public:
int member_var; //成員變量
A(); //構造函數
}
A::A():member_var(0)
{
}

他們覺得這個構造函數的定義應該只能這樣寫:

A::A()
{
member_var=1;
}

其實兩種方法都可。但是有些情況下,只能用第一種,而且通常情況下用第一種也會效率高些。

其實,第一種方法是真正的初始化(initialization),而在構造函數內實現的“=”操作其實是賦值(assign)。這兩種方法的一切區別從這兒開始。區別大概如下:

我們知道普通變量編譯器都會默認的替你初始化。他們既能初始化,也能被賦值的,而常量(const)按照其意思只能被初始化,不能賦值。否則與變量就無區別了。所以常量成員(const member)只能用成員初始化列表來完成他們的“初始化”,而不能在構造函數內為他們“賦值”。

我們知道類的對象的初始化其實就是調用他的構造函數完成,如果沒有寫構造函數,編譯器會為你默認生成一個。如果你自定義了帶參數的構造函數,那麼編譯器將不生成默認構造函數。這樣這個類的對象的初始化必須有參數。如果這樣的類的對象來做另外某個類的成員,那麼為了初始化這個成員,你必須為這個類的對象的構造函數傳遞一個參數。同樣,如果你在包含它的這個類的構造函數裡用“=”,其實是為這個對象“賦值”而非“初始化”它。所以一個類裡的所有構造函數都是有參數的,那麼這樣的類如果做為別的類的成員變量,你必須顯式的初始化它,你也是只能通過成員初始化列表來完成初始化。例如:

class B
{
......
}

class A
{
public:
B member_b;
A();
}
A::A():B(...) //你必須顯式初始化它,因為他的所有構造函數
//都是有參數的,之後才能被賦值。
{
B=...; //因為如上所寫,已經初始化了,才能被賦值,否則錯誤。
}



——————————————————————————————————————

初始化順序:

class test
{

const int a;

std:string str;

object o;

test():str(“df”),o(null),a(0)

{

}

};

黃色的既是初始化列表,他們會在構造函數正式調用前被調用,且他們的初始化順序並不是根據 初始化列表中出現的順序,而是他們聲明的順序來初始化。如上:

初始化順序是:a, str, o;

一般用於初始化 常量類型,靜態類型的數據,或者不能獨立存在的數據*/

/* C++的11個注意要點

下面的這些要點是對所有的C++程序員都適用的。我之所以說它們是最重要的,是因為這些要點中提到的是你通常在C++書中或網站上無法找到的。如:指向成員的指針,這是許多資料中都不願提到的地方,也是經常出錯的地方,甚至是對一些高級的C++程序員也是如此。

這裡的要點不僅僅是解釋怎樣寫出更好的代碼,更多的是展現出語言規則裡面的東西。很顯然,它們對C++程序員來說是永久的好資料。我相信這一篇文章會使你收獲不小。

首先,我把一些由不同層次的C++程序員經常問的問題歸到一起。我驚奇的發現有很多是有經驗的程序員都還沒意識到 .h 符號是否還應該出現在標准頭文件中。

要點1: <iostream.h> 還是 <iostream>?

很多C++程序員還在使用<iostream.h>而不是用更新的標准的<iostream>庫。這兩者都有什麼不同呢?首先,5年前我們就開始反對把。h符號繼續用在標准的頭文件中。繼續使用過時的規則可不是個好的方法。從功能性的角度來講,<iostream>包含了一系列模板化的I/O類,相反地<iostream.h>只僅僅是支持字符流。另外,輸入輸出流的C++標准規范接口在一些微妙的細節上都已改進,因此,<iostream>和<iostream.h>在接口和執行上都是不同的。最後,<iostream>的各組成都是以STL的形式聲明的,然而<iostream.h>的各組成都是聲明成全局型的。

因為這些實質上的不同,你不能在一個程序中混淆使用這兩個庫。做為一種習慣,在新的代碼中一般使用<iostream>,但如果你處理的是過去編寫的代碼,為了繼承可以用繼續用<iostream.h>舊保持代碼的一致性。

要點2:用引用傳遞參數時應注意的地方

在用引用傳遞參數時,最好把引用聲明為const類型。這樣做的好處是:告訴程序不能修改這個參數。在下面的這個例子中函數f()就是傳遞的引用:

void f(const int & i);int main()

{ f(2); /* OK */ }

這個程序傳遞一個參數2給f()。在運行時,C++創建一個值為2的int類型的臨時變量,並傳遞它的引用給f()。這個臨時變量和它的引用從f()被調用開始被創建並存在直到函數返回。返回時,就被馬上刪除。注意,如果我們不在引用前加上const限定詞,則函數f()可能會更改它參數的值,更可能會使程序產生意想不到的行為。所以,別忘了const.

這個要點也適用於用戶定義的對象。你可以給臨時對象也加上引用如果是const類型:

struct A{};void f(const A& a);int main()

{ f(A()); // OK,傳遞的是一個臨時A的const引用}

要點3:“逗號分離”表達形式

“逗號分離”表達形式是從C繼承來的,使用在for-和while-循環中。當然,這條語法規則被認為是不直觀的。首先,我們來看看什麼是“逗號分離”表達形式。

一個表達式由一個或多個其它表達式構成,由逗號分開,如:

if(++x, ——y, cin.good()) //三個表達式

這個if條件包含了三個由逗號分離的表達式。C++會計算每個表達式,但完整的“逗號分離”表達式的結果是最右邊表達式的值。因此,僅當cin.good()返回true時,if條件的值才是true.下面是另一個例子:

int j=10;
int i=0;
while( ++i, --j)
{
//直到j=0時,循環結束,在循環時,i不斷自加
}

要點4,使用全局對象的構造函數在程序啟動前調用函數

有一些應用程序需要在主程序啟動前調用其它函數。如:轉態過程函數、登記功能函數都是必須在實際程序運行前被調用的。最簡單的辦法是通過一個全局對象的構造函數來調用這些函數。因為全局對象都是在主程序開始前被構造,這些函數都將會在main()之前返回結果。如:

class Logger
{

public:
Logger()
{
activate_log();//譯者注:在構造函數中調用你需要先運行的函數
}
};
Logger log; //一個全局實例

int main()
{
record * prec=read_log();//譯者注:讀取log文件數據
//.. 程序代碼
}

全局對象log在main()運行之前被構造,log調用了函數activate_log()。從而,當main()開始執行時,它就可以從log文件中讀取數據。

毫無疑問地,在C++編程中內存管理是最復雜和最容易出現bug的地方。直接訪問原始內存、動態分配存儲和最大限度的發揮C++指令效率,都使你必須盡力避免

有關內存的bug.

要點5:避免使用復雜構造的指向函數的指針

指向函數的指針是C++中可讀性最差的語法之一。你能告訴我下面語句的意思嗎?

void (*p[10]) (void (*)());

P是一個“由10個指針構成的指向一個返回void類型且指向另一個無返回和無運算的函數的數組”。這個麻煩的語法真是讓人難以辨認,不是嗎?你其實可以簡單的通過typedef來聲明相當於上面語句的函數。首先,使用typedef聲明“指向一個無返回和無運算的函數的指針”:

typedef void (*pfv)();

接著,聲明“另一個指向無返回且使用pfv的函數指針”:

typedef void (*pf_taking_pfv) (pfv);

現在,聲明一個由10個上面這樣的指針構成的數組:

pf_taking_pfv p[10];

與void (*p[10]) (void (*)())達到同樣效果。但這樣是不是更具有可讀性了!

要點6:指向成員的指針

一個類有兩種基本的成員:函數成員和數據成員。同樣的,指向成員的指針也有兩種:指向函數成員的指針和指向數據成員的指針。後則其實並不常用,因為類一般是不含有公共數據成員的,僅當用在繼承用C寫的代碼時協調結構(struct)和類(class)時才會用到。

指向成員的指針是C++語法中最難以理解的構造之一,但是這也是一個C++最強大的特性。它可以讓你調用一個類的函數成員而不必知道這個函數的名字。這一個非常敏捷的調用工具。同樣的,你也可以通過使用指向數據成員的指針來檢查並改變這個數據而不必知道它的成員名字。

指向數據成員的指針

盡管剛開始時,指向成員的指針的語法會使你有一點點的迷惑,但你不久會發現它其實同普通的指針差不多,只不過是*號的前面多了::符號和類的名字,例:定義一個指向int型的指針:

int * pi;
定義一個指向為int型的類的數據成員:
int A::*pmi; //pmi是指向類A的一個int型的成員
你可以這樣初始化它:
class A
{
public:
int num;
int x;
};
int A::*pmi = & A::num;

上面的代碼是聲明一個指向類A的一個int型的num成員並將它初始化為這個num成員的地址。通過在pmi前面加上*你就可以使用和更改類A的num成員的值:

A a1, a2;
int n=a1.*pmi; //把a1.num賦值給n
a1.*pmi=5; // 把5賦值給a1.num
a2.*pmi=6; // 把6賦值給6a2.num

如果你定義了一個指向類A的指針,那麼上面的操作你必須用 ->*操作符代替:
A * pa=new A;
int n=pa->*pmi;
pa->*pmi=5;

指向函數成員的指針

它由函數成員所返回的數據類型構成,類名後跟上::符號、指針名和函數的參數列表。舉個例子:一個指向類A的函數成員(該函數返回int類型)的指針:

class A
{
public:
int func ();
};
int (A::*pmf) ();

上面的定義也就是說pmf是一個指向類A的函數成員func()的指針。實際上,這個指針和一個普通的指向函數的指針沒什麼不同,只是它包含了類的名字和::符號。你可以在在任何使用*pmf的地方調用這個函數

func():
pmf=&A::func;
A a;
(a.*pmf)(); //調用a.func()
如果你先定義了一個指向對象的指針,那麼上面的操作要用->*代替:
A *pa=&a;
(pa->*pmf)(); //調用pa->func()

指向函數成員的指針要考慮多態性。所以,當你通過指針調用一個虛函數成員時,這個調用將會被動態回收。另一個需要注意的地方,你不能取一個類的構造函數和析構函數的地址。

要點7、避免產生內存碎片

經常會有這樣的情況:你的應用程序每運行一次時就因為程序自身缺陷而產生內存漏洞而洩漏內存,而你又在周期性地重復著你的程序,結果可想而知,它也會使系統崩潰。但怎樣做才能預防呢?首先,盡量少使用動態內存。在大多數情況下,你可能使用靜態或自動存儲或者是STL容器。第二,盡量分配大塊的內存而不是一次只分配少量內存。舉個例子:一次分配一個數組實例所需的內存,而不是一次只分配一個數組元素的內存。

要點8、是delete還是delete[]

在程序員中有個荒誕的說法:使用delete來代替delete[]刪除數組類型時是可以的!

舉個例子吧:

int *p=new int[10];

delete p; //錯誤,應該是:delete[] p

上面的程序是完全錯誤的。事實上,在一個平台上使用delete代替delete[]的應用程序也許不會造成系統崩潰,但那純粹是運氣。你不能保證你的應用程序是不是會在另一個編譯器上編譯,在另一個平台上運行,所以還是請使用delete[].

要點9、優化成員的排列

一個類的大小可以被下面的方式改變:

struct A

{

bool a;

int b;

bool c;

}; //sizeof (A) == 12

在我的電腦上sizeof (A) 等於12.這個結果可能會讓你吃驚,因為A的成員總數是6個字節:1+4+1個字節。那另6字節是哪兒來的?編譯器在每個bool成員後面都插入了3個填充字節以保證每個成員都是按4字節排列,以便分界。你可以減少A的大小,通過以下方式:

struct B

{

bool a;

bool c;

int b;

}; // sizeof (B) == 8

這一次,編譯器只在成員c後插入了2個字節。因為b占了4個字節,所以就很自然地把它當作一個字的形式排列,而a和c的大小1+1=2,再加上2個字節就剛好按兩個字的形式排列B.

要點10、為什麼繼承一個沒有虛析構函數的類是危險的?

一個沒有虛析構函數的類意味著不能做為一個基類。如std::string,std::complex, 和 std::vector 都是這樣的。為什麼繼承一個沒有虛析構函數的類是危險的?當你公有繼承創建一個從基類繼承的相關類時,指向新類對象中的指針和引用實際上都指向了起源的對象。因為析構函數不是虛函數,所以當你delete一個這樣的類時,C++就不會調用析構函數鏈。舉個例子說明:

class A
{
public:
~A() // 不是虛函數
{
// ...
}
};
class B: public A //錯; A沒有虛析構函數
{
public:
~B()
{
// ...
}
};

int main()
{
A * p = new B; //看上去是對的
delete p; //錯,B的析構函沒有被調用
}

要點11、以友元類聲明嵌套的類

當你以友元類聲明一個嵌套的類時,把友元聲明放在嵌套類聲明的後面,而不前面。

class A
{
private:
int i;
public:
class B //嵌套類聲明在前
{
public:
B(A & a) { a.i=0;};
};
friend class B;//友元類聲明
};

如果你把友元類聲明放在聲明嵌套類的前面,編譯器將拋棄友元類後的其它聲明。

*/

/*

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved