一、多態性有哪些?
(靜態和動態,然後分別敘述一下虛函數和函數重載)
多態:指相同的對象收到不同的消息或者不同的對象收到相同的消息時產生的不同的實現動作。
C++支持兩種多態:編譯時多態(靜態)、運行時多態(動態)
編譯時多態:
就是在編譯期確定的一種多態。在C++中主要體現在函數模板,這裏需要注意,函數重載和多態無關,很多地方把函數重載誤認爲是編譯多態,這是錯誤的。
1. #include <iostream>
2. using namespace std;
3.
4. template <typename T>
5. T add(T a, T b)
6. {
7. t c = a+b;
8. return c;
9. }
10.
11. int main()
12. {
13. int a1 = 1;
14. int b1 = 2;
15. int c1 = add(a1,b1);
16. cout<<"c1:"<<c1<<endl;
17.
18. double a2 = 2.0;
19. double b2 = 4.0;
20. double c2 = add(a2,b2);
21. cout<<"c2:"<<c2<<endl;
22. }
上例中,我們定義了一個函數模板,用來計算兩個數的和。這兩個數的數據類型在使用時才知道,main函數中調用同一個函數分別計算了兩個int值和兩個double值的和,這就體現了多態,在編譯期,編譯器根據一定的最佳匹配算法來確定函數模板的參數類型到底是什麼,這就體現了編譯期的多態性。
運行時多態性
C++運行時多態性主要是通過虛函數來實現的。體現在具有繼承關係的父類和子類之間,子類重新定義父類的成員函數稱爲覆蓋或者重寫,而虛函數允許子類重新定義父類的成員函數,即重寫父類的成員函數。
下面舉例說明一下:
1. #include <iostream>
2. using namespace std;
3.
4. class A{
5. public:
6. void f1()
7. {
8. cout<<"A::f1()"<<endl;
9. }
10. virtual void f2()
11. {
12. cout<<"A::f2()"<<endl;
13. }
14. };
15.
16. class B:public A
17. {
18. public:
19. //覆蓋
20. void f1()
21. {
22. cout<<"B::f1()"<<endl;
23. }
24. //重寫
25. virtual void f2()
26. {
27. cout<<"B::f2()"<<endl;
28. }
29. };
30.
31. int main()
32. {
33. A* p = new B();
34. B* q = new B();
35. p->f1(); //調用A::f1()
36. p->f2(); //調用B::f2(),體現多態性
37. q->f1(); //調用B::f1()
38. q->f2(); //調用B::f2()
39. return 0;
40. }
說說例2中關於體現多態性的問題,我們在父類即A類中定義了一個虛函數f2()——由關鍵字virtual修飾。既然是虛函數,允許子類重寫這個函數,於是我們在子類即B類中重寫了函數f2()。之後我們在main函數中定義了一個A類指針p,請注意,雖然定義的是一個父類指針,但是它指向的卻是一個子類的對象(new B()),然後我們用這個父類的指針調用f2(),從結果來看,實際上調用的是子類的f2(),並不是父類的f2(),這就體現了多態性。雖然p是父類指針,但是它指向的是子類對象,而且調用的又是虛函數,那麼在運行期,就會找到動態綁定到父類指針上的子類對象,然後查找子類的虛函數表,找到函數f2()的入口地址,從而調用子類的f2()函數,這就是運行期多態。
接下來我們再來看看p->f1();這句話,從運行結果來看,調用的是父類的f1()函數,這裏是爲什麼沒有體現多態呢?原因很簡單,因爲f1()不是虛函數,所以根本就沒有多態性,雖然子類和父類都有f1()函數,但是子類僅僅是覆蓋或者說是隱藏了父類的f1()函數,注意這裏不是重寫,是覆蓋。而p是父類指針,所以只能調用父類的f1()函數。而指針q的類型爲子類指針,所以q->f1();調用子類的函數f1(),q->f2();調用子類的函數f2();
C++純虛函數
定義:純虛函數是在基類中聲明的虛函數,它在基類中沒有定義,但要求任何派生類都要定義自己的實現方法,基類中實現純虛函數的方法是在函數原型後面加“=0”。例如:
1. virtual void f() = 0;
爲什麼要引入純虛函數:
1、爲了使用多態特性,我們必須在基類中定義虛擬函數。
2、在很多情況下,基類本身生成對象是不合情理的。例如,動物作爲一個基類可以派生出老虎、孔雀等子類,但動物本身生成對象明顯不合常理。 爲了解決上述問題,引入了純虛函數的概念,將函數定義爲純虛函數(方法:virtualReturnType Function()= 0;),則編譯器要求在派生類中必須予以重寫以實現多態性。同時含有純虛擬函數的類稱爲抽象類,它不能生成對象。純虛函數永遠不會被調用,它們主要用來統一管理子類對象。
二、動態綁定怎麼實現?
(問一下基類與派生類指針和引用的轉換問題)
1.爲每一個包含虛函數的類設置一個虛表(VTABLE)每當創建一個包含有虛函數的類或從包含虛函數的類派生一個類時,編譯器就會爲這個類創建一個VTABLE。在VTABLE中,編譯器放置了這個類中,或者它的基類中所有已經聲明爲 virtual的函數的地址。如果在這個派生類中沒有對基類中聲明爲 virtual 的函數進行重新定義,編譯器就使用基類的這個虛函數的地址。而且所有VTABLE中虛函數地址的順序是完全相同的。
2.初始化虛指針(VPTR)然後編譯器在這個類的各個對象中放置VPTR。VPTR在對象的相同的位置(通常都在對象的開頭)。VPTR必須被初始化爲指向相應的VTABLE。
3.爲虛函數調用插入代碼 當通過基類的指針調用派生類的虛函數時,編譯器將在調用處插入相應的代碼,以實現通過VPTR找到VTABLE,並根據VTABLE中存儲的正確的虛函數地址,訪問到正確的函數。
爲了支持c++的多態性,才用了動態綁定和靜態綁定。理解他們的區別有助於更好的理解多態性,以及在編程的過程中避免犯錯誤。
需要理解四個名詞:
1、對象的靜態類型:對象在聲明時採用的類型。是在編譯期確定的。
2、對象的動態類型:目前所指對象的類型。是在運行期決定的。對象的動態類型可以更改,但是靜態類型無法更改。
關於對象的靜態類型和動態類型,看一個示例:
1. class B
2. {
3. }
4. class C : public B
5. {
6. }
7. class D : public B
8. {
9. }
10. D* pD = new D();//pD的靜態類型是它聲明的類型D*,動態類型也是D*
11. B* pB = pD;//pB的靜態類型是它聲明的類型B*,動態類型是pB所指向的對象pD的類型D*
12. C* pC = new C();
13. pB = pC;//pB的動態類型是可以更改的,現在它的動態類型是C*
3、靜態綁定:綁定的是對象的靜態類型,某特性(比如函數)依賴於對象的靜態類型,發生在編譯期。
4、動態綁定:綁定的是對象的動態類型,某特性(比如函數)依賴於對象的動態類型,發生在運行期。
1. class B
2. {
3. void DoSomething();
4. virtual void vfun();
5. }
6. class C : public B
7. {
8. void DoSomething();//首先說明一下,這個子類重新定義了父類的no-virtual函數,這是一個不好的設計,會導致名稱遮掩;這裏只是爲了說明動態綁定和靜態綁定才這樣使用。
9. virtual void vfun();
10. }
11. class D : public B
12. {
13. void DoSomething();
14. virtual void vfun();
15. }
16. D* pD = new D();
17. B* pB = pD;
讓我們看一下,pD->DoSomething()和pB->DoSomething()調用的是同一個函數嗎?
不是的,雖然pD和pB都指向同一個對象。因爲函數DoSomething是一個no-virtual函數,它是靜態綁定的,也就是編譯器會在編譯期根據對象的靜態類型來選擇函數。pD的靜態類型是D*,那麼編譯器在處理pD->DoSomething()的時候會將它指向D::DoSomething()。同理,pB的靜態類型是B*,那pB->DoSomething()調用的就是B::DoSomething()。
讓我們再來看一下,pD->vfun()和pB->vfun()調用的是同一個函數嗎?
是的。因爲vfun是一個虛函數,它動態綁定的,也就是說它綁定的是對象的動態類型,pB和pD雖然靜態類型不同,但是他們同時指向一個對象,他們的動態類型是相同的,都是D*,所以,他們的調用的是同一個函數:D::vfun()。
上面都是針對對象指針的情況,對於引用(reference)的情況同樣適用。
指針和引用的動態類型和靜態類型可能會不一致,但是對象的動態類型和靜態類型是一致的。
D D;
D.DoSomething()和D.vfun()永遠調用的都是D::DoSomething()和D::vfun()。
至於那些是動態綁定,那些是靜態綁定,有篇文章總結的非常好:
我總結了一句話:只有虛函數才使用的是動態綁定,其他的全部是靜態綁定。目前我還沒有發現不適用這句話的,如果有錯誤,希望你可以指出來。
特別需要注意的地方
當缺省參數和虛函數一起出現的時候情況有點複雜,極易出錯。我們知道,虛函數是動態綁定的,但是爲了執行效率,缺省參數是靜態綁定的。
1. class B
2. {
3. virtual void vfun(int i = 10);
4. }
5. class D : public B
6. {
7. virtual void vfun(int i = 20);
8. }
9. D* pD = new D();
10. B* pB = pD;
11. pD->vfun();
12. pB->vfun();
有上面的分析可知pD->vfun()和pB->vfun()調用都是函數D::vfun(),但是他們的缺省參數是多少?
分析一下,缺省參數是靜態綁定的,pD->vfun()時,pD的靜態類型是D*,所以它的缺省參數應該是20;同理,pB->vfun()的缺省參數應該是10。編寫代碼驗證了一下,正確。
對於這個特性,估計沒有人會喜歡。所以,永遠記住:
“絕不重新定義繼承而來的缺省參數(Never redefine function’s inheriteddefault parameters value.)”
關於c++語言
目前我基本上都是在c++的子集“面向對象編程”下工作,對於更復雜的知識瞭解的還不是很多。即便如此,到目前爲止編程時需要注意的東西已經很多,而且後面可能還會繼續增多,這也許是很多人反對c++的原因。
c++是Google的四大官方語言之一。但是Google近幾年確推出了go語言,而且定位是和c/c++相似。考慮這種情況,我認爲可能是Google的程序員們深感c++的複雜,所以想開發一種c++的替代語言。有時間要了解一下go語言,看它在類似c++的問題上時如何取捨的。
三、類型轉換有哪些?
(四種類型轉換,分別舉例說明)
1、 static_cast:
功能:完成編譯器認可的隱式類型轉換。
格式type1 a;
type2 b = staic_cast<type1>(a);將type1的類型轉化爲type2的類型;
使用範圍:
(1)基本數據類型之間的轉換,如int->double;
int a = 6;
double b = static_cast<int>(a);
(2)派生體系中向上轉型:將派生類指針或引用轉化爲基類指針或引用(向上轉型);
class base{ …. }
class derived : public base{ …. }
base *b;
derived *d = new derived();
b = static_cast<base *>(d);
2、 dynamic_cast
功能:執行派生類指針或引用與基類指針或引用之間的轉換。
格式:
(1) 其他三種都是編譯時完成的,dynamic_cast是運行時處理的,運行時要進行運行時類型檢查;
(2) 基類中要有虛函數,因爲運行時類型檢查的類型信息在虛函數表中,有虛函數纔會有虛函數表;
(3) 可以實現向上轉型和向下轉型,前提是必須使用public或protected繼承;
例子:
向上轉型:
class base{ … };
class derived : public base{ … };
int main()
{
base *pb;
derived *pd = new derived();
pb = dynamic_cast<base *>(pd);
return 0;
}
向下轉型:
class base{ virtualvoid func(){} };
class derived : public base{ void func(){} };
int main()
{
base *pb = new base();
derived *pd = dynamic_cast<derived *>(pb);//向下轉型
return 0;
}
3、const_cast:
只能對指針或者引用去除或者添加const屬性,對於變量直接類型不能使用const_cast;不能用於不同類型之間的轉換,只能改變同種類型的const屬性。
如:
const int a= 0;
int b = const_cast<int>(a);//不對的
const int *pi = &a;
int * pii = const_cast<int *>pi;//去除指針中的常量性,也可以添加指針的常量性;
const_cast的用法:
(1)常用於函數的形參是一個非const的引用,我想要穿進去一個const的引用,可以使用const_cast<Type&>para;去除實參的常量性,以便函數能夠接受這個參數。
(2)一個const對象,我們想要調用該對象中的非const函數,可以使用const_cast去除對象的常量性;
4、reinterpret_cast:
從字面意思理解是一個“重新解釋的類型轉換”。也就是說對任意兩個類型之間的變量我們都可以個使用reinterpret_cast在他們之間相互轉換,無視類型信息。
不常使用。
四、操作符重載,具體如何去定義?
(讓把操作符重載函數原型說一遍)
operator是C++的關鍵字,它和運算符一起使用,表示一個運算符函數,理解時應將operator=整體上視爲一個函數名。
這是C++擴展運算符功能的方法,雖然樣子古怪,但也可以理解:一方面要使運算符的使用方法與其原來一致,另一方面擴展其功能只能通過函數的方式(c++中,“功能”都是由函數實現的)。
1、爲什麼使用操作符重載?
對於系統的所有操作符,一般情況下,只支持基本數據類型和標準庫中提供的class,對於用戶自己定義的class,如果想支持基本操作,比如比較大小,判斷是否相等,等等,則需要用戶自己來定義關於這個操作符的具體實現。比如,判斷兩個人是否一樣大,我們默認的規則是按照其年齡來比較,所以,在設計person 這個class的時候,我們需要考慮操作符==,而且,根據剛纔的分析,比較的依據應該是age。那麼爲什麼叫重載呢?這是因爲,在編譯器實現的時候,已經爲我們提供了這個操作符的基本數據類型實現版本,但是現在他的操作數變成了用戶定義的數據類型class,所以,需要用戶自己來提供該參數版本的實現。2、如何聲明一個重載的操作符?
A: 操作符重載實現爲類成員函數重載的操作符在類體中被聲明,聲明方式如同普通成員函數一樣,只不過他的名字包含關鍵字operator,以及緊跟其後的一個c++預定義的操作符。
可以用如下的方式來聲明一個預定義的==操作符:
class person{
private:
int age;
public:
person(int a){
this->age=a;
}
inline bool operator == (const person &ps) const;
};
實現方式如下:
inline bool person::operator==(const person &ps) const
{
if (this->age==ps.age)
return true;
return false;
}
調用方式如下:
#include
using namespace std;
int main()
{
person p1(10);
person p2(20);
if(p1==p2) c
cout<<”the age is equal!”< return 0;
}
這裏,因爲operator ==是class person的一個成員函數,所以對象p1,p2都可以調用該函數,上面的if語句中,相當於p1調用函數==,把p2作爲該函數的一個參數傳遞給該函數,從而實現了兩個對象的比較。
B:操作符重載實現爲非類成員函數(全局函數)對於全局重載操作符,代表左操作數的參數必須被顯式指定。例如:
#include
#include
using namespace std;
class person
{
public:
int age;
public:
};
bool operator==(person const &p1 ,person const & p2)
//滿足要求,做操作數的類型被顯示指定
{
if(p1.age==p2.age)
return true;
return false;
}
int main()
{
person rose;
person jack;
rose.age=18;
jack.age=23;
if(rose==jack)
cout<<"ok"< return 0;
}
C:如何決定把一個操作符重載爲類成員函數還是全局名字空間的成員呢?
①如果一個重載操作符是類成員,那麼只有當與他一起使用的左操作數是該類的對象時,該操作符纔會被調用。如果該操作符的左操作數必須是其他的類型,則操作符必須被重載爲全局名字空間的成員。
②C++要求賦值=,下標[],調用(), 和成員指向-> 操作符必須被定義爲類成員操作符。任何把這些操作符定義爲名字空間成員的定義都會被標記爲編譯時刻錯誤。
③如果有一個操作數是類類型如string類的情形那麼對於對稱操作符比如等於操作符最好定義爲全局名字空間成員。
D:重載操作符具有以下限制:
(1) 只有C++預定義的操作符集中的操作符纔可以被重載;
C++允許重載的運算符
C++中絕大部分運算符都是可以被重載的。
不能重載的運算符只有5個:
. (成員訪問運算符)
.* (成員指針訪問運算符)
:: (域運算符)
sizeof (長度運算符)
?: (條件運算符)
前兩個運算符不能重載是爲了保證訪問成員的功能不能被改變,域運算符合sizeof運算符的運算對象是類型而不是變量或一般表達式,不具備重載的特徵。
(2)對於內置類型的操作符,它的預定義不能被改變,應不能爲內置類型重載操作符,如,不能改變int型的操作符+的含義;
(3) 也不能爲內置的數據類型定義其它的操作符;
(4) 只能重載類類型或枚舉類型的操作符;
(5) 重載操作符不能改變它們的操作符優先級;
(6) 重載操作符不能改變操作數的個數;
(7) 除了對( )操作符外,對其他重載操作符提供缺省實參都是非法的;
實例1 重載operator():
1. struct join_if_joinable
2. {
3. void operator()(thread& t)
4. {
5. if (t.joinable())
6. {
7. t.join();
8. }
9. }
10. };
11. //use
12. join_if_joinable(thread1);
五、內存對齊原則?
(原則敘述了一下並舉例說明)
1、內存對齊的原因
1>、平臺移植原因:不是所有的硬件平臺都能任意訪問任意地址上的數據,有些硬件平臺只能在某些特定地址處讀取特定的數據,否則會拋出硬件異常;2>、性能原因:數據結構(尤其是棧)應儘可能的在自然邊界對齊。原因在於,訪問未對齊的內存,處理器需要進行兩次訪問,而訪問對齊的內存,處理器只需要進行一次訪問。
2、內存對齊的規則
在具體講內存對齊的規則之前引入一個名詞:對齊係數,也叫對齊模數,每個編譯器都有自己默認的對齊係數,VC6.0默認爲8。程序員可以根據需要進行修改,可通過預編譯指令#pragma pack(n),n就是對齊係數,可以取1、2、4、8、16,具體對齊規則有三條,如下:(1).數據成員的對齊規則:結構體(struct)(或者聯合體(union))的數據成員,第一個數據成員放在偏移量爲0的地方,以後每個數據成員按照#pragma pack(n)和數據成員中比較小的那個數對齊,也就是說,起始地址需要時這個數的倍數,具體下面會舉例說明;
(2).結構體(struct)(或者聯合體(union))整體對齊規則:整體的大小應該按照#pragma pack(n)和結構中最長的數據結構中,最大的那個進行,也就是,需要是這個數的倍數;
(3).如果#pragmapack(n)比結構中任何一個數據成員類型都大,則對齊係數不起任何作用。下面舉例說明,環境:VS2013,32位操作系統
1. #include <iostream>
2. using namespace std;
3.
4. struct S
5. {
6. char a;
7. int b;
8. short c;
9. };
10.
11. struct T
12. {
13. short c;
14. char a;
15. int b;
16. };
17.
18. int main()
19. {
20. cout << "sizeof(S) is " << sizeof(S) << endl;
21. cout << "sizeof(T) is " << sizeof(T) << endl;
22. system("PAUSE");
23. return 0;
24. }
代碼輸出結果爲:
sizeof(S) is 12
sizeof(T) is 8
編譯器默認對齊係數爲8:
對於結構體S,成員a是char型數據,佔1字節大小,b是int型數據,佔用4個字節,因爲規則1,所以需要從內存偏移量爲4的倍數的地方開始,因此需要a需要補上3字節無用內存,此時a佔用4字節,下面是c,c是short型數據,佔用2個字節,此時偏移量4+4=8,是2的倍數,符合規則1,結構體此時總大小:4+4+2=10;又根據規則2,結構體成員變量類型,最長的爲4,因此結構體整體大小應該爲4的倍數,因此需要c多佔用2個字節,此時結構體大小爲:4+4+4=12。
對於結構體T,c是short型數據,佔用2個字節大小,a是char型數據,佔用1個字節大小,起始偏移量3,是1的倍數,滿足,此時大小爲:2+1=3,b爲int型數據,佔用4字節,起始偏移量爲3,顯然不滿足規則1,需要b補充1個字節,此時起始偏移量爲2+2=4,滿足規則1,此時結構體T的大小爲:2+2+4=8;又根據規則2,結構體成員變量類型中最長的爲4,結構體內存大小滿足,因此,最終大小爲8。
六、模板怎麼實現?
模板(Templates)是ANSI-C++標準中新引入的概念。如果你使用的 C++ 編譯器不符合這個標準,則你很可能不能使用模板。
函數模板( Function templates)
模板(Templates)使得我們可以生成通用的函數,這些函數能夠接受任意數據類型的參數,可返回任意類型的值,而不需要對所有可能的數據類型進行函數重載。這在一定程度上實現了宏(macro)的作用。它們的原型定義可以是下面兩種中的任何一個:
template <class identifier> function_declaration;
template <typename identifier> function_declaration;
上面兩種原型定義的不同之處在關鍵字class 或 typename的使用。它們實際是完全等價的,因爲兩種表達的意思和執行都一模一樣。
例如,要生成一個模板,返回兩個對象中較大的一個,我們可以這樣寫:
template <class GenericType>
GenericType GetMax (GenericType a, GenericType b) { return (a>b?a:b); }
在第一行聲明中,我們已經生成了一個通用數據類型的模板,叫做GenericType。因此在其後面的函數中,GenericType 成爲一個有效的數據類型,它被用來定義了兩個參數a和 b ,並被用作了函數GetMax的返回值類型。
GenericType 仍沒有代表任何具體的數據類型;當函數 GetMax 被調用的時候,我們可以使用任何有效的數據類型來調用它。這個數據類型將被作爲pattern來代替函數中GenericType 出現的地方。用一個類型pattern來調用一個模板的方法如下:
function <type> (parameters);
例如,要調用GetMax 來比較兩個int類型的整數可以這樣寫:
int x,y;
GetMax <int> (x,y);
因此,GetMax 的調用就好像所有的GenericType 出現的地方都用int 來代替一樣。
這裏是一個例子:
// function template
#include <iostream.h>
template <class T> T GetMax (T a, T b) {
T result;
result = (a>b)? a : b;
return (result);
}
int main () {
int i=5, j=6, k;
long l=10, m=5, n;
k=GetMax(i,j);
n=GetMax(l,m);
cout << k << endl;
cout << n << endl;
return 0;
}
運行結果:
6
10
(在這個例子中,我們將通用數據類型命名爲T 而不是 GenericType ,因爲T短一些,並且它是模板更爲通用的標示之一,雖然使用任何有效的標示符都是可以的。)
在上面的例子中,我們對同樣的函數GetMax()使用了兩種參數類型:int 和 long,而只寫了一種函數的實現,也就是說我們寫了一個函數的模板,用了兩種不同的pattern來調用它。
如你所見,在我們的模板函數 GetMax() 裏,類型 T 可以被用來聲明新的對象
T result;
result 是一個T類型的對象, 就像a 和 b一樣,也就是說,它們都是同一類型的,這種類型就是當我們調用模板函數時寫在尖括號<> 中的類型。
在這個具體的例子中,通用類型 T 被用作函數GetMax 的參數,不需要說明<int>或 <long>,編譯器也可以自動檢測到傳入的數據類型,因此,我們也可以這樣寫這個例子:
int i,j;
GetMax (i,j);
因爲i 和j 都是int 類型,編譯器會自動假設我們想要函數按照int進行調用。這種暗示的方法更爲有用,併產生同樣的結果:
// function template II
#include <iostream.h>
template <class T> T GetMax (T a, T b) {
return (a>b?a:b);
}
int main () {
int i=5, j=6, k;
long l=10, m=5, n;
k=GetMax(i,j);
n=GetMax(l,m);
cout << k << endl;
cout << n << endl;
return 0;
}
運行結果:
6
10
注意在這個例子的main() 中我們如何調用模板函數GetMax() 而沒有在括號<>中指明具體數據類型的。編譯器自動決定每一個調用需要什麼數據類型。
因爲我們的模板函數只包括一種數據類型 (class T),而且它的兩個參數都是同一種類型,我們不能夠用兩個不同類型的參數來調用它:
int i;
long l;
k = GetMax (i,l);
上面的調用就是不對的,因爲我們的函數等待的是兩個同種類型的參數。
我們也可以使得模板函數接受兩種或兩種以上類型的數據,例如:
template <class T>
T GetMin (T a, U b) { return (a<b?a:b); }
在這個例子中,我們的模板函數 GetMin() 接受兩個不同類型的參數,並返回一個與第一個參數同類型的對象。在這種定義下,我們可以這樣調用該函數:
int i,j;
long l;
i = GetMin <int, long> (j,l);
或者,簡單的用
i = GetMin (j,l);
雖然 j 和l 是不同的類型。
類模板(Class templates)
我們也可以定義類模板(class templates),使得一個類可以有基於通用類型的成員,而不需要在類生成的時候定義具體的數據類型,例如:
template <class T>
class pair {
T values [2];
public:
pair (T first, T second) {
values[0]=first;
values[1]=second;
}
};
上面我們定義的類可以用來存儲兩個任意類型的元素。例如,如果我們想要定義該類的一個對象,用來存儲兩個整型數據115 和 36 ,我們可以這樣寫:
pair<int> myobject (115, 36);
我們同時可以用這個類來生成另一個對象用來存儲任何其他類型數據,例如:
pair<float> myfloats (3.0, 2.18);
在上面的例子中,類的唯一一個成員函數已經被inline 定義。如果我們要在類之外定義它的一個成員函數,我們必須在每一函數前面加template <... >。
// class templates
#include <iostream.h>
template <class T> class pair {
T value1, value2;
public:
pair (T first, T second) {
value1=first;
value2=second;
}
T getmax ();
};
template <class T>
T pair::getmax (){
T retval;
retval = value1>value2? value1 : value2;
return retval;
}
int main () {
pair myobject (100, 75);
cout << myobject.getmax();
return 0;
}
運行結果:
100
注意成員函數getmax 是怎樣開始定義的:
template <class T>
T pair::getmax ()
所有寫 T 的地方都是必需的,每次你定義模板類的成員函數的時候都需要遵循類似的格式(這裏第二個T表示函數返回值的類型,這個根據需要可能會有變化)。
模板特殊化(Template specialization)
模板的特殊化是當模板中的pattern有確定的類型時,模板有一個具體的實現。例如假設我們的類模板pair 包含一個取模計算(module operation)的函數,而我們希望這個函數只有當對象中存儲的數據爲整型(int)的時候才能工作,其他時候,我們需要這個函數總是返回0。這可以通過下面的代碼來實現:
// Template specialization
#include <iostream.h>
template <class T> class pair {
T value1, value2;
public:
pair (T first, T second){
value1=first;
value2=second;
}
T module () {return 0;}
};
template <>
class pair <int> {
int value1, value2;
public:
pair (int first, int second){
value1=first;
value2=second;
}
int module ();
};
template <>
int pair<int>::module() {
return value1%value2;
}
int main () {
pair <int> myints (100,75);
pair <float> myfloats (100.0,75.0);
cout << myints.module() << '\n';
cout << myfloats.module() << '\n';
return 0;
}
運行結果:
25
0
由上面的代碼可以看到,模板特殊化由以下格式定義:
template <> class class_name <type>
這個特殊化本身也是模板定義的一部分,因此,我們必須在該定義開頭寫template <>。而且因爲它確實爲一個具體類型的特殊定義,通用數據類型在這裏不能夠使用,所以第一對尖括號<> 內必須爲空。在類名稱後面,我們必須將這個特殊化中使用的具體數據類型寫在尖括號<>中。
當我們特殊化模板的一個數據類型的時候,同時還必須重新定義類的所有成員的特殊化實現(如果你仔細看上面的例子,會發現我們不得不在特殊化的定義中包含它自己的構造函數 constructor,雖然它與通用模板中的構造函數是一樣的)。這樣做的原因就是特殊化不會繼承通用模板的任何一個成員。
模板的參數值(Parameter values for templates)
除了模板參數前面跟關鍵字class 或 typename 表示一個通用類型外,函數模板和類模板還可以包含其它不是代表一個類型的參數,例如代表一個常數,這些通常是基本數據類型的。例如,下面的例子定義了一個用來存儲數組的類模板:
// array template
#include <iostream.h>
template <class T, int N>
class array {
T memblock [N];
public:
void setmember (int x, T value);
T getmember (int x);
};
template <class T, int N>
void array<T,N>::setmember (int x, T value) {
memblock[x]=value;
}
template <class T, int N>
T array<T,N>::getmember (int x) {
return memblock[x];
}
int main () {
array <int,5> myints;
array <float,5> myfloats;
myints.setmember (0,100);
myfloats.setmember (3,3.1416);
cout << myints.getmember(0) << '\n';
cout << myfloats.getmember(3) << '\n';
return 0;
}
運行結果:
100
3.1416
我們也可以爲模板參數設置默認值,就像爲函數參數設置默認值一樣。
下面是一些模板定義的例子:
template <class T> // 最常用的:一個class 參數。
template <class T, class U> // 兩個class 參數。
template <class T, int N> // 一個class 和一個整數。
template <class T = char> // 有一個默認值。
template <int Tfunc (int)> // 參數爲一個函數。
模板與多文件工程 (Templates and multiple-file projects)
從編譯器的角度來看,模板不同於一般的函數或類。它們在需要時才被編譯(compiled on demand),也就是說一個模板的代碼直到需要生成一個對象的時候(instantiation)才被編譯。當需要instantiation的時候,編譯器根據模板爲特定的調用數據類型生成一個特殊的函數。
當工程變得越來越大的時候,程序代碼通常會被分割爲多個源程序文件。在這種情況下,通常接口(interface)和實現(implementation)是分開的。用一個函數庫做例子,接口通常包括所有能被調用的函數的原型定義。它們通常被定義在以.h 爲擴展名的頭文件 (header file) 中;而實現 (函數的定義) 則在獨立的C++代碼文件中。
模板這種類似宏(macro-like) 的功能,對多文件工程有一定的限制:函數或類模板的實現 (定義) 必須與原型聲明在同一個文件中。也就是說我們不能再 將接口(interface)存儲在單獨的頭文件中,而必須將接口和實現放在使用模板的同一個文件中。
回到函數庫的例子,如果我們想要建立一個函數模板的庫,我們不能再使用頭文件(.h) ,取而代之,我們應該生成一個模板文件(template file),將函數模板的接口和實現都放在這個文件中 (這種文件沒有慣用擴展名,除了不要使用.h擴展名或不要不加任何擴展名)。在一個工程中多次包含同時具有聲明和實現的模板文件並不會產生鏈接錯誤 (linkage errors),因爲它們只有在需要時才被編譯,而兼容模板的編譯器應該已經考慮到這種情況,不會生成重複的代碼。
七、指針和const的用法?
(就是四種情況說了一下)
1、C++函數聲明時在後面加const的作用:
非靜態成員函數後面加const(加到非成員函數或靜態成員後面會產生編譯錯誤),表示成員函數隱含傳入的this指針爲 const指針,決定了在該成員函數中,任意修改它所在的的成員的操作都是不允許的(因爲隱含了對this指針的const引用修飾);唯一的例外是對於 mutable修飾的成員。加了const的成員函數可以被非const對象和const對象調用,但不加const的成員函數只能被非const對象調用。
2、Cons修飾普通變量
一般有兩種寫法:
const int value;//即Value的值不能被改變
int const value;//即value的值不能被改變
上述的兩種寫法效果都是一樣的。
3、Const修飾指針類型變量
A.const char* pContent;//也可寫成const (char)* pContent;
B.char* const pContent;//也可寫成(char*)const pContent;
C.char const* pContent;//也可寫成(char)Const *pContent;
D.const char* const pContent;
對上述的總結可以將A與C分爲一類描述的結果都是const修飾*pContent做指向的內容不能被改變,對於B來說也可以寫成const (Char*) pContent;其含義是指const所修飾的是pConten是一個指針變量就是一個常量,本身不容被改變。D表示指針變量和指針變量所指向的內容都不能被改變。
4、const修飾函數參數
例如:voidfunction(const int Var);//表示const修飾的Var的值不能被改變
常常const修飾參數也用引用來提升執行效率如下所示:
例如:voidFuncation(const int& Var);
5、const修飾函數的返回值
例如:const intfuncation();
其含義是const所修飾的返回值必須是常量含義基本上與const修飾普通變量或者指針基本相同。
6.const修飾類對象、對象指針、對象引用
const修飾類對象表示該對象爲常量對象,其中的任何成員都不能被修改。對於對象指針和對象引用也是一樣。
const修飾的對象,該對象的任何非const成員函數都不能被調用,因爲任何非const成員函數會有修改成員變量的企圖。
7、const與define的區別
1、編譯器處理方式不同
A.define宏是在預處理階段展開。
B.const常量是編譯運行階段使用。
(2) 類型和安全檢查不同
A.define宏沒有類型,不做任何類型檢查,僅僅是展開。
B.const常量有具體的類型,在編譯階段會執行類型檢查。
(3) 存儲方式不同
A.define宏僅僅是展開,有多少地方使用,就展開多少次,不會分配內存。
B.const常量會在內存中分配(可以是堆中也可以是棧中)。
八、虛函數、純虛函數、虛函數與析構函數?
(純虛函數何如定義,爲什麼虛構函數要定義成虛函數)
1、虛函數
只有用virtual聲明類的成員函數,使之成爲虛函數,不能將類外的普通函數聲明爲虛函數。因爲虛函數的作用是允許在派生類中對基類的虛函數重新定義。所以虛函數只能用於類的繼承層次結構中。
一個成員函數被聲明爲虛函數後,在同一類族中的類就不能再定義一個非virtual的但與該虛函數具有相同的參數(包括個數和類型)和函數返回值類型的同名函數。
根據什麼考慮是否把一個成員函數聲明爲虛函數?
① 看成員函數所在的類是否會作爲基類
② 看成員函數在類的繼承後有無可能被更改功能,如果希望更改其功能的,一般應該將它聲明爲虛函數。
如果成員函數在類被繼承後功能不需修改,或派生類用不到該函數,則不要把它聲明爲虛函數。不要僅僅考慮到作爲基類而把類中的所有成員函數都聲明爲虛函數。
應考慮對成員函數的調用是通過對象名還是通過基類指針或引用去訪問,如果是通過基類指針或引用去訪問的,則應當聲明爲虛函數。有時在定義虛函數時,並不定義其函數體,即純虛函數。它的作用只是定義了一個虛函數名,具體功能留給派生類去添加。
說明:使用虛函數,系統要有一定的空間開銷。當一個類帶有虛函數時,編譯系統會爲該類構造一個虛函數表(vtbl),它是一個指針數組,存放每個虛函數的入口地址。系統在進行動態關聯的時間開銷很少,提高了多態性的效率。
2、純虛函數
有時候,基類中的虛函數是爲了派生類中的使用而聲明定義的,其在基類中沒有任何意義。此類函數我們叫做純虛函數,不需要寫成空函數的形式,只需要聲明成:
virtual 函數類型 函數名(形參表列)=0;
注意:純虛函數沒有函數體;
最後面的“=0“並不代表函數返回值爲0,只是形式上的作用,告訴編譯系統”這是純虛函數”;
這是一個聲明語句,最後應有分號。
純虛函數只有函數的名字但不具備函數的功能,不能被調用。在派生類中對此函數提供定義後,才能具備函數的功能,可以被調用。
3、虛析構函數
析構函數的作用是在對象撤銷之前把類的對象從內存中撤銷。通常系統只會執行基類的析構函數,不執行派生類的析構函數。
只需要把基類的析構函數聲明爲虛函數,即虛析構函數,這樣當撤銷基類對象的同時也撤銷派生類的對象,這個過程是動態關聯完成的。
如果將基類的析構函數聲明爲虛函數時,由該基類所派生的所有派生類的析構函數都自動成爲虛函數,即使派生類的析構函數與基類的析構函數名字不相同。
最好把基類的析構函數聲明爲虛函數,這將使所有派生類的析構函數自動成爲虛函數,如果程序中顯式delete運算符刪除一個對象,而操作對象用了指向派生類對象的基類指針,系統會調用相應類的析構函數。
構造函數不能聲明爲虛函數。
例如:
#include <iostream>
using namespace std;
class Animal
{
public:
Animal()
{
cout << "Animal::Animal() is called" << endl;
};
virtual ~Animal()
{
cout << "Animal::~Animal() is called" << endl;
}
virtual void eat()
{
cout << "Animal::eat() is called" << endl;
}
virtual void walk()
{
cout << "Animal::walk() is called" << endl;
}
/* data */
};
class Dog : public Animal
{
public:
Dog(int w,int h)
{
cout << "Dog::Dog() is called" << endl;
this->weight=w;
this->height=h;
}
virtual ~Dog()
{
cout << "Dog::~Dog() is called" << endl;
}
int weight;
int height;
void eat()
{
cout<<"i eat meat"<<endl;
}
void walk()
{
cout<<"run"<<endl;
}
/* data */
};
int main(int argc, char const *argv[])
{
/* code */
Animal *ani= new Dog(12,23);
Dog *dog=new Dog(23,34);
ani->eat();
ani->walk();
dog->eat();
dog->walk();
delete ani;
//delete dog;
return 0;
}
(C++ 析構函數一般定義爲虛函數)如果基類中析構函數沒有定義爲虛函數,則delete ani的時候,僅僅調用了父類的析構函數,子類的沒有調用,如果在父類和子類的構造函數中都有動態內存分配,那麼就會存在內存泄漏的問題。一般析構函數最好都寫成虛函數,尤其是父類。
九、內聯函數
(講了一下內聯函數的優點以及和宏定義的區別)
1. 內聯函數
在C++中我們通常定義以下函數來求兩個整數的最大值:int max(int a, int b)
{
return a > b ? a : b;
}
爲這麼一個小的操作定義一個函數的好處有:
① 閱讀和理解函數 max 的調用,要比讀一條等價的條件表達式並解釋它的含義要容易得多
② 如果需要做任何修改,修改函數要比找出並修改每一處等價表達式容易得多
③ 使用函數可以確保統一的行爲,每個測試都保證以相同的方式實現
④ 函數可以重用,不必爲其他應用程序重寫代碼
雖然有這麼多好處,但是寫成函數有一個潛在的缺點:調用函數比求解等價表達式要慢得多。在大多數的機器上,調用函數都要做很多工作:調用前要先保存寄存器,並在返回時恢復,複製實參,程序還必須轉向一個新位置執行
C++中支持內聯函數,其目的是爲了提高函數的執行效率,用關鍵字 inline 放在函數定義(注意是定義而非聲明,下文繼續講到)的前面即可將函數指定爲內聯函數,內聯函數通常就是將它在程序中的每個調用點上“內聯地”展開,假設我們將 max 定義爲內聯函數:
代碼如下:
inline int max(int a, int b)
{
return a > b ? a : b;
}
則調用: cout<<max(a,b)<<endl;
在編譯時展開爲: cout<<(a > b ? a : b)<<endl;
從而消除了把 max寫成函數的額外執行開銷
2. 內聯函數和宏
無論是《Effective C++》中的 “Prefer consts,enums,andinlines to #defines” 條款,還是《高質量程序設計指南——C++/C語言》中的“用函數內聯取代宏”,宏在C++中基本是被廢了,在書《高質量程序設計指南——C++/C語言》中這樣解釋到:
3. 將內聯函數放入頭文件
關鍵字 inline 必須與函數定義體放在一起才能使函數成爲內聯,僅將 inline 放在函數聲明前面不起任何作用。
如下風格的函數 Foo 不能成爲內聯函數:
inline void Foo(int x, int y); // inline 僅與函數聲明放在一起
void Foo(int x, int y)
{
...
}
而如下風格的函數 Foo 則成爲內聯函數:
void Foo(int x, int y);
inline void Foo(int x, int y) // inline 與函數定義體放在一起
{
...
}
所以說,C++ inline函數是一種“用於實現的關鍵字”,而不是一種“用於聲明的關鍵字”。一般地,用戶可以閱讀函數的聲明,但是看不到函數的定義。儘管在大多數教科書中內聯函數的聲明、定義體前面都加了 inline 關鍵字,但我認爲 inline 不應該出現在函數的聲明中。這個細節雖然不會影響函數的功能,但是體現了高質量C++/C 程序設計風格的一個基本原則:聲明與定義不可混爲一談,用戶沒有必要、也不應該知道函數是否需要內聯。
定義在類聲明之中的成員函數將自動地成爲內聯函數,例如:
class A
{
public:
void Foo(int x, int y) { ... } // 自動地成爲內聯函數
}
但是編譯器是否將它真正內聯則要看 Foo函數如何定義
內聯函數應該在頭文件中定義,這一點不同於其他函數。編譯器在調用點內聯展開函數的代碼時,必須能夠找到 inline 函數的定義才能將調用函數替換爲函數代碼,而對於在頭文件中僅有函數聲明是不夠的。
當然內聯函數定義也可以放在源文件中,但此時只有定義的那個源文件可以用它,而且必須爲每個源文件拷貝一份定義(即每個源文件裏的定義必須是完全相同的),當然即使是放在頭文件中,也是對每個定義做一份拷貝,只不過是編譯器替你完成這種拷貝罷了。但相比於放在源文件中,放在頭文件中既能夠確保調用函數是定義是相同的,又能夠保證在調用點能夠找到函數定義從而完成內聯(替換)。
但是你會很奇怪,重複定義那麼多次,不會產生鏈接錯誤?
我們來看一個例子:
A.h :
class A
{
public:
A(int a, int b) : a(a),b(b){}
int max();
private:
int a;
int b;
};
A.cpp :
#include "A.h"
inline int A::max()
{
return a > b ? a : b;
}
Main.cpp :
#include <iostream>
#include "A.h"
using namespace std;
inline int A::max()
{
return a > b ? a : b;
}
int main()
{
A a(3, 5);
cout<<a.max()<<endl;
return 0;
}
一切正常編譯,輸出結果:5
倘若你在Main.cpp中沒有定義max內聯函數,那麼會出現鏈接錯誤:
error LNK2001: unresolvedexternal symbol "public: int __thiscall A::max(void)"(?max@A@@QAEHXZ)main.obj
找不到函數的定義,所以內聯函數可以在程序中定義不止一次,只要 inline 函數的定義在某個源文件中只出現一次,而且在所有源文件中,其定義必須是完全相同的就可以。
在頭文件中加入或修改 inline 函數時,使用了該頭文件的所有源文件都必須重新編譯。
4. 慎用內聯
內聯雖有它的好處,但是也要慎用,以下摘自《高質量程序設計指南——C++/C語言》:
而在Google C++編碼規範中則規定得更加明確和詳細:
內聯函數:
Tip: 只有當函數只有 10 行甚至更少時纔將其定義爲內聯函數.
定義: 當函數被聲明爲內聯函數之後, 編譯器會將其內聯展開, 而不是按通常的函數調用機制進行調用.
優點: 當函數體比較小的時候, 內聯該函數可以令目標代碼更加高效. 對於存取函數以及其它函數體比較短, 性能關鍵的函數, 鼓勵使用內聯.
缺點: 濫用內聯將導致程序變慢. 內聯可能使目標代碼量或增或減, 這取決於內聯函數的大小. 內聯非常短小的存取函數通常會減少代碼大小, 但內聯一個相當大的函數將戲劇性的增加代碼大小. 現代處理器由於更好的利用了指令緩存, 小巧的代碼往往執行更快。
結論:一個較爲合理的經驗準則是, 不要內聯超過 10 行的函數. 謹慎對待析構函數, 析構函數往往比其表面看起來要更長, 因爲有隱含的成員和基類析構函數被調用!
另一個實用的經驗準則: 內聯那些包含循環或switch 語句的函數常常是得不償失 (除非在大多數情況下,這些循環或 switch 語句從不被執行).
有些函數即使聲明爲內聯的也不一定會被編譯器內聯, 這點很重要;比如虛函數和遞歸函數就不會被正常內聯. 通常, 遞歸函數不應該聲明成內聯函數.(遞歸調用堆棧的展開並不像循環那麼簡單, 比如遞歸層數在編譯時可能是未知的, 大多數編譯器都不支持內聯遞歸函數). 虛函數內聯的主要原因則是想把它的函數體放在類定義內, 爲了圖個方便, 抑或是當作文檔描述其行爲, 比如精短的存取函數.
-inl.h文件:
Tip: 複雜的內聯函數的定義, 應放在後綴名爲 -inl.h 的頭文件中.
內聯函數的定義必須放在頭文件中, 編譯器才能在調用點內聯展開定義. 然而, 實現代碼理論上應該放在 .cc文件中, 我們不希望 .h 文件中有太多實現代碼, 除非在可讀性和性能上有明顯優勢.
如果內聯函數的定義比較短小, 邏輯比較簡單, 實現代碼放在 .h 文件裏沒有任何問題. 比如, 存取函數的實現理所當然都應該放在類定義內. 出於編寫者和調用者的方便, 較複雜的內聯函數也可以放到 .h 文件中, 如果你覺得這樣會使頭文件顯得笨重, 也可以把它萃取到單獨的 -inl.h 中. 這樣把實現和類定義分離開來, 當需要時包含對應的 -inl.h 即可。
十、const、#define和typedef
(主要將const的用處,有哪些優點)
#DEFINE、CONST、TYPEDEF的差別
#define 並不是定義變量啊
#define只是用來做文本替換的
例如:
#define Pi 3.1415926
float angel;
angel=30*Pi/180;
那麼,當程序進行編譯的時候,編譯器會首先將 “#define Pi 3.1415926”以後的,所有代碼中的“Pi”全部換成 “3.1415926”
然後再進行編譯。
我查到一個講const與#define的差別的帖子,裏面談到const與#define最大的差別在於:前者在堆棧分配了空間,而後者只是把具體數值直接傳遞到目標變量罷了。或者說,const的常量是一個Run-Time的概念,他在程序中確確實實的存在並可以被調用、傳遞。而#define常量則是一個Compile-Time概念,它的生命週期止於編譯期:在實際程序中他只是一個常數、一個命令中的參數,沒有實際的存在。
const常量存在於程序的數據段,#define常量存在於程序的代碼段。
至於兩者的優缺點,要看具體的情況了。一般的常數應用,筆者個人認爲#define是一個更好的選擇:
i.從run-time的角度來看,他在空間上和時間上都有很好優勢。
ii.從compile-time的角度來看,類似m=t*10的代碼不會被編譯器優化,t*10的操作需要在run-time執行。而#define的常量會被合併(在上例中T*10將被0x82取代)。
但是:如果你需要粗魯的修改常數的值,那就得使用const了,因爲後者在程序中沒有實際的存在。(其實應該說修改數據段比代碼段要簡單^_^)。
有關#define的用法
1.簡單的define定義
#define MAXTIME 1000程序中遇到MAXTIME就會當作1000來處理.
一個簡單的MAXTIME就定義好了,它代表1000,如果在程序裏面寫
if(i<MAXTIME){.........}
編譯器在處理這個代碼之前會對MAXTIME進行處理替換爲1000。
這樣的定義看起來類似於普通的常量定義CONST,但也有着不同,因爲define的定義更像是簡單的文本替換,而不是作爲一個量來使用,這個問題在下面反映的尤爲突出。
2.define的“函數定義”
define可以像函數那樣接受一些參數,如下#define max(x,y) (x)>(y)?(x):(y);
這個定義就將返回兩個數中較大的那個,看到了嗎?因爲這個“函數”沒有類型檢查,就好像一個函數模板似的,當然,它絕對沒有模板那麼安全就是了。可以作爲一個簡單的模板來使用而已。
但是這樣做的話存在隱患,例子如下:
#define Add(a,b) a+b;
在一般使用的時候是沒有問題的,但是如果遇到如:c * Add(a,b) * d的時候就會出現問題,代數式的本意是a+b然後去和c,d相乘,但是因爲使用了define(它只是一個簡單的替換),所以式子實際上變成了
c*a + b*d
另外舉一個例子:
#define pin (int*);
pin a,b;
本意是a和b都是int型指針,但是實際上變成int* a,b;
a是int型指針,而b是int型變量。
這時應該使用typedef來代替define,這樣a和b就都是int型指針了。
所以我們在定義的時候,養成一個良好的習慣,建議所有的層次都要加括號。
3.宏的單行定義
#define A(x) T_##x#define B(x) #@x
#define C(x) #x
我們假設:x=1,則有:
A(1)------)T_1
B(1)------)'1'
C(1)------)"1"
4.define的多行定義
define可以替代多行的代碼,例如MFC中的宏定義(非常的經典,雖然讓人看了噁心)#define MACRO(arg1, arg2) do { \
/* declarations */ \
stmt1; \
stmt2; \
/* ... */ \
} while(0) /* (no trailing ; ) */
關鍵是要在每一個換行的時候加上一個"\" 摘抄自http://www.blog.edu.cn/user1/16293/archives/2005/115370.shtml 修補了幾個bug
5.在大規模的開發過程中,特別是跨平臺和系統的軟件裏,define最重要的功能是條件編譯
就是:#ifdef WINDOWS
......
......
#endif
#ifdef LINUX
......
......
#endif
可以在編譯的時候通過#define設置編譯環境
6.如何定義宏、取消宏
//定義宏#define [MacroName] [MacroValue]
//取消宏
#undef [MacroName]
普通宏
#define PI (3.1415926)
帶參數的宏
#define max(a,b) ((a)>(b)? (a),(b))
關鍵是十分容易產生錯誤,包括機器和人理解上的差異等等。
7.條件編譯
#ifdef XXX…(#else) …#endif例如
#ifdef DV22_AUX_INPUT
#define AUX_MODE 3
#else
#define AUY_MODE 3
#endif
#ifndef XXX … (#else) … #endif
8.頭文件(.h)可以被頭文件或C文件包含
重複包含(重複定義)由於頭文件包含可以嵌套,那麼C文件就有可能包含多次同一個頭文件,就可能出現重複定義的問題的。
通過條件編譯開關來避免重複包含(重複定義)
例如
#ifndef __headerfileXXX__
#define __headerfileXXX__
…
文件內容
…
#endif
typedef和#define的用法與區別
1、typedef的用法
在C/C++語言中,typedef常用來定義一個標識符及關鍵字的別名,它是語言編譯過程的一部分,但它並不實際分配內存空間,實例像:
typedef int INT;
typedef int ARRAY[10];
typedef (int*) pINT;
typedef可以增強程序的可讀性,以及標識符的靈活性,但它也有“非直觀性”等缺點。
2、#define的用法
#define爲一宏定義語句,通常用它來定義常量(包括無參量與帶參量),以及用來實現那些“表面似和善、背後一長串”的宏,它本身並不在編譯過程中進行,而是在這之前(預處理過程)就已經完成了,但也因此難以發現潛在的錯誤及其它代碼維護問題,它的實例像:
#define INT int
#define TRUE 1
#define Add(a,b) ((a)+(b));
#define Loop_10 for (int i=0; i<10; i++)
在Scott Meyer的EffectiveC++一書的條款1中有關於#define語句弊端的分析,以及好的替代方法,大家可參看。
typedef與#define的區別
從以上的概念便也能基本清楚,typedef只是爲了增加可讀性而爲標識符另起的新名稱(僅僅只是個別名),而#define原本在C中是爲了定義常量,到了C++,const、enum、inline的出現使它也漸漸成爲了起別名的工具。有時很容易搞不清楚與typedef兩者到底該用哪個好,如#define INT int這樣的語句,用typedef一樣可以完成,用哪個好呢?我主張用typedef,因爲在早期的許多C編譯器中這條語句是非法的,只是現今的編譯器又做了擴充。爲了儘可能地兼容,一般都遵循#define定義“可讀”的常量以及一些宏語句的任務,而typedef則常用來定義關鍵字、冗長的類型的別名。
宏定義只是簡單的字符串代換(原地擴展),而typedef則不是原地擴展,它的新名字具有一定的封裝性,以致於新命名的標識符具有更易定義變量的功能。請看上面第一大點代碼的第三行:
typedef (int*) pINT;
以及下面這行:
#define pINT2 int*
效果相同?實則不同!實踐中見差別:pINT a,b;的效果同int *a; int *b;表示定義了兩個整型指針變量。而pINT2 a,b;的效果同int *a, b;
表示定義了一個整型指針變量a和整型變量b。
注意:兩者還有一個行尾;號的區別哦!
const用法主要是防止定義的對象再次被修改,定義對象變量時要初始化變量
const常見的用法
1.用於定義常量變量,這樣這個變量在後面就不可以再被修改
const int Val = 10;
//Val = 20; //錯誤,不可被修改
2. 保護傳參時參數不被修改,如果使用引用傳遞參數或按地址傳遞參數給一個函數,在這個函數裏這個參數的值若被修改,則函數外部傳進來的變量的值也發生改變,若想保護傳進來的變量不被修改,可以使用const保護
void fun1(const int &val)
{
//val = 10; //出錯
}
void fun2(int &val)
{
val = 10; //沒有出錯
}
void main()
{
int a = 2;
int b = 2;
fun1(a); //因爲出錯,這個函數結束時a的值還是2
fun2(b);//因爲沒有出錯,函數結束時b的值爲10
}
如果只想把值傳給函數,而且這個不能被修改,則可以使用const保護變量,有人會問爲什麼不按值傳遞,按值傳遞還需要把這個值複製一遍,而引用不需要,使用引用是爲了提高效率//如果按值傳遞的話,沒必要加const,那樣根本沒意義
3. 節約內存空間,
#define PI 3.14 //使用#define宏
const double Pi = 3.14 //使用const,這時候Pi並沒有放入內存中
double a = Pi; //這時候才爲Pi分配內存,不過後面再有這樣的定義也不會再分配內存
double b = PI; //編譯時分配內存
double c = Pi; //不會再分配內存,
double d = PI; //編譯時再分配內存
const定義的變量,系統只爲它分配一次內存,而使用#define定義的常量宏,能分配好多次,這樣const就很節約空間
4.類中使用const修飾函數防止修改非static類成員變量
class
{
public:
void fun() const //加const修飾
{
a = 10; //出錯,不可修改非static變量
b = 10; //對,可以修改
}
private:
int a ;
static int b;
}
5.修飾指針const int *A; 或 int const *A; //const修飾指向的對象,A可變,A指向的對象不可變
int *const A; //const修飾指針A, A不可變,A指向的對象可變
const int *const A; //指針A和A指向的對象都不可變
6.修飾函數返回值,防止返回值被改變
const int fun();
接收返回值的變量也必須加const
7.修飾類的成員變量
使用const修飾的變量必須初始化,在類中又不能在定義時初始化,
如;
class
{
private:
int a = 10;
const int b = 10;
static const int c = 10;
//這樣初始化都是錯的,
}
初始化constint類型(沒有static),在類的構造函數上初始化
Class Test
{
Public:
Test():b(23) //構造函數上初始化b的值爲23
{
}
private:
const int b ;
}
初始化staticconstint這個類型的(帶有static的),在類的外面初始化
class Test
{
private:
static const int c;
}
const int Test::c=10; //類的外部初始化c爲10
8.const定義的對象變量只能作用於這個程序該C/C++文件,不能被該程序的其他C/C++文件調用,
如file1.cpp中 const int val;
在file2.cpp中, extern intval; //錯誤,無法調用,
要想const定義的對象變量能被其他文件調用,定義時必須使用extern修飾爲
externconst int val;
非const變量默認爲extern,要是const能被其他文件訪問必須顯示指定爲extern
十一、排序算法有哪些?快速排序怎麼實現?最好時間複雜度,平均時間複雜度
我們通常所說的排序算法往往指的是內部排序算法,即數據記錄在內存中進行排序。
排序算法大體可分爲兩種:
一種是比較排序,時間複雜度O(nlogn) ~ O(n^2),主要有:冒泡排序,選擇排序,插入排序,歸併排序,堆排序,快速排序等。
另一種是非比較排序,時間複雜度可以達到O(n),主要有:計數排序,基數排序,桶排序等。
這裏我們來探討一下常用的比較排序算法,非比較排序算法將在下一篇文章中介紹。下表給出了
常見比較排序算法的性能
有一點我們很容易忽略的是排序算法的穩定性(騰訊校招2016筆試題曾考過)。
排序算法穩定性的簡單形式化定義爲:如果Ai = Aj,排序前Ai在Aj之前,排序後Ai還在Aj之前,則稱這種排序算法是穩定的。通俗地講就是保證排序前後兩個相等的數的相對順序不變。
對於不穩定的排序算法,只要舉出一個實例,即可說明它的不穩定性;而對於穩定的排序算法,必須對算法進行分析從而得到穩定的特性。需要注意的是,排序算法是否爲穩定的是由具體算法決定的,不穩定的算法在某種條件下可以變爲穩定的算法,而穩定的算法在某種條件下也可以變爲不穩定的算法。
例如,對於冒泡排序,原本是穩定的排序算法,如果將記錄交換的條件改成A[i]>= A[i + 1],則兩個相等的記錄就會交換位置,從而變成不穩定的排序算法。
其次,說一下排序算法穩定性的好處。排序算法如果是穩定的,那麼從一個鍵上排序,然後再從另一個鍵上排序,前一個鍵排序的結果可以爲後一個鍵排序所用。基數排序就是這樣,先按低位排序,逐次按高位排序,低位排序後元素的順序在高位也相同時是不會改變的。
冒泡排序(Bubble Sort)
冒泡排序是一種極其簡單的排序算法,也是我所學的第一個排序算法。它重複地走訪過要排序的元素,依次比較相鄰兩個元素,如果他們的順序錯誤就把他們調換過來,直到沒有元素再需要交換,排序完成。這個算法的名字由來是因爲越小(或越大)的元素會經由交換慢慢“浮”到數列的頂端。
冒泡排序算法的運作如下:
比較相鄰的元素,如果前一個比後一個大,就把它們兩個調換位置。
對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
針對所有的元素重複以上的步驟,除了最後一個。
持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
由於它的簡潔,冒泡排序通常被用來對於程序設計入門的學生介紹算法的概念。冒泡排序的代碼如下:#include <stdio.h>
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(n^2)
// 最優時間複雜度 ---- 如果能在內部循環第一次運行時,使用一個旗標來表示有無需要交換的可能,可以把最優時間複雜度降低到O(n)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定
void Swap(int A[], int i, int j)
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
void BubbleSort(int A[], int n)
{
for (int j = 0; j < n - 1; j++) // 每次最大元素就像氣泡一樣"浮"到數組的最後
{
for (int i = 0; i < n - 1 - j; i++) // 依次比較相鄰的兩個元素,使較大的那個向後移
{
if (A[i] > A[i + 1]) // 如果條件改成A[i] >= A[i + 1],則變爲不穩定的排序算法
{
Swap(A, i, i + 1);
}
}
}
}
int main()
{
int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 }; // 從小到大冒泡排序
int n = sizeof(A) / sizeof(int);
BubbleSort(A, n);
printf("冒泡排序結果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
儘管冒泡排序是最容易瞭解和實現的排序算法之一,但它對於少數元素之外的數列排序是很沒有效率的。
冒泡排序的改進:雞尾酒排序
雞尾酒排序,也叫定向冒泡排序,是冒泡排序的一種改進。此算法與冒泡排序的不同處在於從低到高然後從高到低,而冒泡排序則僅從低到高去比較序列裏的每個元素。他可以得到比冒泡排序稍微好一點的效能。
雞尾酒排序的代碼如下:
#include <stdio.h>
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(n^2)
// 最優時間複雜度 ---- 如果序列在一開始已經大部分排序過的話,會接近O(n)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定
void Swap(int A[], int i, int j)
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
void CocktailSort(int A[], int n)
{
int left = 0; // 初始化邊界
int right = n - 1;
while (left < right)
{
for (int i = left; i < right; i++) // 前半輪,將最大元素放到後面
{
if (A[i] > A[i + 1])
{
Swap(A, i, i + 1);
}
}
right--;
for (int i = right; i > left; i--) // 後半輪,將最小元素放到前面
{
if (A[i - 1] > A[i])
{
Swap(A, i - 1, i);
}
}
left++;
}
}
int main()
{
int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 }; // 從小到大定向冒泡排序
int n = sizeof(A) / sizeof(int);
CocktailSort(A, n);
printf("雞尾酒排序結果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
以序列(2,3,4,5,1)爲例,雞尾酒排序只需要訪問一次序列就可以完成排序,但如果使用冒泡排序則需要四次。但是在亂數序列的狀態下,雞尾酒排序與冒泡排序的效率都很差勁。
選擇排序(Selection Sort)
選擇排序也是一種簡單直觀的排序算法。它的工作原理很容易理解:初始時在序列中找到最小(大)元素,放到序列的起始位置作爲已排序序列;然後,再從剩餘未排序元素中繼續尋找最小(大)元素,放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
注意選擇排序與冒泡排序的區別:冒泡排序通過依次交換相鄰兩個順序不合法的元素位置,從而將當前最小(大)元素放到合適的位置;而選擇排序每遍歷一次都記住了當前最小(大)元素的位置,最後僅需一次交換操作即可將其放到合適的位置。
選擇排序的代碼如下:#include <stdio.h>
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(n^2)
// 最優時間複雜度 ---- O(n^2)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 不穩定
void Swap(int A[], int i, int j)
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
void SelectionSort(int A[], int n)
{
for (int i = 0; i < n - 1; i++) // i爲已排序序列的末尾
{
int min = i;
for (int j = i + 1; j < n; j++) // 未排序序列
{
if (A[j] < A[min]) // 找出未排序序列中的最小值
{
min = j;
}
}
if (min != i)
{
Swap(A, min, i); // 放到已排序序列的末尾,該操作很有可能把穩定性打亂,所以選擇排序是不穩定的排序算法
}
}
}
int main()
{
int A[] = { 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }; // 從小到大選擇排序
int n = sizeof(A) / sizeof(int);
SelectionSort(A, n);
printf("選擇排序結果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
上述代碼對序列{ 8, 5, 2, 6, 9, 3, 1,4, 0, 7 }進行選擇排序的實現
選擇排序是不穩定的排序算法,不穩定發生在最小元素與A[i]交換的時刻。
比如序列:{ 5, 8, 5, 2, 9 },一次選擇的最小元素是2,然後把2和第一個5進行交換,從而改變了兩個元素5的相對次序。插入排序(Insertion Sort)
插入排序是一種簡單直觀的排序算法。它的工作原理非常類似於我們抓撲克牌
對於未排序數據(右手抓到的牌),在已排序序列(左手已經排好序的手牌)中從後向前掃描,找到相應位置並插入。
插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。
具體算法描述如下:
- 從第一個元素開始,該元素可以認爲已經被排序
- 取出下一個元素,在已經排序的元素序列中從後向前掃描
- 如果該元素(已排序)大於新元素,將該元素移到下一位置
- 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
- 將新元素插入到該位置後
- 重複步驟2~5
#include <stdio.h>
// 分類 ------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- 最壞情況爲輸入序列是降序排列的,此時時間複雜度O(n^2)
// 最優時間複雜度 ---- 最好情況爲輸入序列是升序排列的,此時時間複雜度O(n)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定
void InsertionSort(int A[], int n)
{
for (int i = 1; i < n; i++) // 類似抓撲克牌排序
{
int get = A[i]; // 右手抓到一張撲克牌
int j = i - 1; // 拿在左手上的牌總是排序好的
while (j >= 0 && A[j] > get) // 將抓到的牌與手牌從右向左進行比較
{
A[j + 1] = A[j]; // 如果該手牌比抓到的牌大,就將其右移
j--;
}
A[j + 1] = get; // 直到該手牌比抓到的牌小(或二者相等),將抓到的牌插入到該手牌右邊(相等元素的相對次序未變,所以插入排序是穩定的)
}
}
int main()
{
int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };// 從小到大插入排序
int n = sizeof(A) / sizeof(int);
InsertionSort(A, n);
printf("插入排序結果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
上述代碼對序列{ 6, 5, 3, 1, 8, 7, 2,4 }進行插入排序的實現過程
插入排序不適合對於數據量比較大的排序應用。但是,如果需要排序的數據量很小,比如量級小於千,那麼插入排序還是一個不錯的選擇。 插入排序在工業級庫中也有着廣泛的應用,在STL的sort算法和stdlib的qsort算法中,都將插入排序作爲快速排序的補充,用於少量元素的排序(通常爲8個或以下)。
插入排序的改進:二分插入排序
對於插入排序,如果比較操作的代價比交換操作大的話,可以採用二分查找法來減少比較操作的次數,我們稱爲二分插入排序,代碼如下:
#include <stdio.h>
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(n^2)
// 最優時間複雜度 ---- O(nlogn)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定
void InsertionSortDichotomy(int A[], int n)
{
for (int i = 1; i < n; i++)
{
int get = A[i]; // 右手抓到一張撲克牌
int left = 0; // 拿在左手上的牌總是排序好的,所以可以用二分法
int right = i - 1; // 手牌左右邊界進行初始化
while (left <= right) // 採用二分法定位新牌的位置
{
int mid = (left + right) / 2;
if (A[mid] > get)
right = mid - 1;
else
left = mid + 1;
}
for (int j = i - 1; j >= left; j--) // 將欲插入新牌位置右邊的牌整體向右移動一個單位
{
A[j + 1] = A[j];
}
A[left] = get; // 將抓到的牌插入手牌
}
}
int main()
{
int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 從小到大二分插入排序
int n = sizeof(A) / sizeof(int);
InsertionSortDichotomy(A, n);
printf("二分插入排序結果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
當n較大時,二分插入排序的比較次數比直接插入排序的最差情況好得多,但比直接插入排序的最好情況要差,所當以元素初始序列已經接近升序時,直接插入排序比二分插入排序比較次數少。二分插入排序元素移動次數與直接插入排序相同,依賴於元素初始序列。
插入排序的更高效改進:希爾排序(Shell Sort)
希爾排序,也叫遞減增量排序,是插入排序的一種更高效的改進版本。希爾排序是不穩定的排序算法。
希爾排序是基於插入排序的以下兩點性質而提出改進方法的:
插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率
但插入排序一般來說是低效的,因爲插入排序每次只能將數據移動一位
希爾排序通過將比較的全部元素分爲幾個區域來提升插入排序的性能。這樣可以讓一個元素可以一次性地朝最終位置前進一大步。然後算法再取越來越小的步長進行排序,算法的最後一步就是普通的插入排序,但是到了這步,需排序的數據幾乎是已排好的了(此時插入排序較快)。假設有一個很小的數據在一個已按升序排好序的數組的末端。如果用複雜度爲O(n^2)的排序(冒泡排序或直接插入排序),可能會進行n次的比較和交換才能將該數據移至正確位置。而希爾排序會用較大的步長移動數據,所以小數據只需進行少數比較和交換即可到正確位置。
#include <stdio.h>
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- 根據步長序列的不同而不同。已知最好的爲O(n(logn)^2)
// 最優時間複雜度 ---- O(n)
// 平均時間複雜度 ---- 根據步長序列的不同而不同。
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 不穩定
void ShellSort(int A[], int n)
{
int h = 0;
while (h <= n) // 生成初始增量
{
h = 3 * h + 1;
}
while (h >= 1)
{
for (int i = h; i < n; i++)
{
int j = i - h;
int get = A[i];
while (j >= 0 && A[j] > get)
{
A[j + h] = A[j];
j = j - h;
}
A[j + h] = get;
}
h = (h - 1) / 3; // 遞減增量
}
}
int main()
{
int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 從小到大希爾排序
int n = sizeof(A) / sizeof(int);
ShellSort(A, n);
printf("希爾排序結果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
希爾排序是不穩定的排序算法,雖然一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂。
比如序列:{ 3, 5, 10, 8, 7, 2, 8, 1, 20, 6 },h=2時分成兩個子序列 { 3, 10, 7, 8, 20} 和 { 5, 8, 2, 1, 6 } ,未排序之前第二個子序列中的8在前面,現在對兩個子序列進行插入排序,得到 { 3, 7, 8, 10,20 } 和 { 1, 2, 5, 6, 8 } ,即 { 3, 1, 7, 2, 8, 5, 10, 6,20, 8 } ,兩個8的相對次序發生了改變。
歸併排序(Merge Sort)
歸併排序是創建在歸併操作上的一種有效的排序算法,效率爲O(nlogn),1945年由馮·諾伊曼首次提出。
歸併排序的實現分爲遞歸實現與非遞歸(迭代)實現。遞歸實現的歸併排序是算法設計中分治策略的典型應用,我們將一個大問題分割成小問題分別解決,然後用所有小問題的答案來解決整個大問題。非遞歸(迭代)實現的歸併排序首先進行是兩兩歸併,然後四四歸併,然後是八八歸併,一直下去直到歸併了整個數組。
歸併排序算法主要依賴歸併(Merge)操作。歸併操作指的是將兩個已經排序的序列合併成一個序列的操作,歸併操作步驟如下:
申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列
設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置
比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置
重複步驟3直到某一指針到達序列尾
將另一序列剩下的所有元素直接複製到合併序列尾
歸併排序的代碼如下:
#include <stdio.h>
#include <limits.h>
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(nlogn)
// 最優時間複雜度 ---- O(nlogn)
// 平均時間複雜度 ---- O(nlogn)
// 所需輔助空間 ------ O(n)
// 穩定性 ------------ 穩定
void Merge(int A[], int left, int mid, int right)// 合併兩個已排好序的數組A[left...mid]和A[mid+1...right]
{
int len = right - left + 1;
int *temp = new int[len]; // 輔助空間O(n)
int index = 0;
int i = left; // 前一數組的起始元素
int j = mid + 1; // 後一數組的起始元素
while (i <= mid && j <= right)
{
temp[index++] = A[i] <= A[j] ? A[i++] : A[j++]; // 帶等號保證歸併排序的穩定性
}
while (i <= mid)
{
temp[index++] = A[i++];
}
while (j <= right)
{
temp[index++] = A[j++];
}
for (int k = 0; k < len; k++)
{
A[left++] = temp[k];
}
}
void MergeSortRecursion(int A[], int left, int right) // 遞歸實現的歸併排序(自頂向下)
{
if (left == right) // 當待排序的序列長度爲1時,遞歸開始回溯,進行merge操作
return;
int mid = (left + right) / 2;
MergeSortRecursion(A, left, mid);
MergeSortRecursion(A, mid + 1, right);
Merge(A, left, mid, right);
}
void MergeSortIteration(int A[], int len) // 非遞歸(迭代)實現的歸併排序(自底向上)
{
int left, mid, right;// 子數組索引,前一個爲A[left...mid],後一個子數組爲A[mid+1...right]
for (int i = 1; i < len; i *= 2) // 子數組的大小i初始爲1,每輪翻倍
{
left = 0;
while (left + i < len) // 後一個子數組存在(需要歸併)
{
mid = left + i - 1;
right = mid + i < len ? mid + i : len - 1;// 後一個子數組大小可能不夠
Merge(A, left, mid, right);
left = right + 1; // 前一個子數組索引向後移動
}
}
}
int main()
{
int A1[] = { 6, 5, 3, 1, 8, 7, 2, 4 }; // 從小到大歸併排序
int A2[] = { 6, 5, 3, 1, 8, 7, 2, 4 };
int n1 = sizeof(A1) / sizeof(int);
int n2 = sizeof(A2) / sizeof(int);
MergeSortRecursion(A1, 0, n1 - 1); // 遞歸實現
MergeSortIteration(A2, n2); // 非遞歸實現
printf("遞歸實現的歸併排序結果:");
for (int i = 0; i < n1; i++)
{
printf("%d ", A1[i]);
}
printf("\n");
printf("非遞歸實現的歸併排序結果:");
for (int i = 0; i < n2; i++)
{
printf("%d ", A2[i]);
}
printf("\n");
return 0;
}
上述代碼對序列{ 6, 5, 3, 1, 8, 7, 2,4 }進行歸併排序的實例
歸併排序除了可以對數組進行排序,還可以高效的求出數組小和(即單調和)以及數組中的逆序對。
堆排序(Heap Sort)
堆排序是指利用堆這種數據結構所設計的一種選擇排序算法。堆是一種近似完全二叉樹的結構(通常堆是通過一維數組來實現的),並滿足性質:以最大堆(也叫大根堆、大頂堆)爲例,其中父結點的值總是大於它的孩子節點。
我們可以很容易的定義堆排序的過程:
由輸入的無序數組構造一個最大堆,作爲初始的無序區
把堆頂元素(最大值)和堆尾元素互換
把堆(無序區)的尺寸縮小1,並調用heapify(A, 0)從新的堆頂元素開始進行堆調整
重複步驟2,直到堆的尺寸爲1
堆排序的代碼如下:#include <stdio.h>
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(nlogn)
// 最優時間複雜度 ---- O(nlogn)
// 平均時間複雜度 ---- O(nlogn)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 不穩定
void Swap(int A[], int i, int j)
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
void Heapify(int A[], int i, int size) // 從A[i]向下進行堆調整
{
int left_child = 2 * i + 1; // 左孩子索引
int right_child = 2 * i + 2; // 右孩子索引
int max = i; // 選出當前結點與其左右孩子三者之中的最大值
if (left_child < size && A[left_child] > A[max])
max = left_child;
if (right_child < size && A[right_child] > A[max])
max = right_child;
if (max != i)
{
Swap(A, i, max); // 把當前結點和它的最大(直接)子節點進行交換
Heapify(A, max, size); // 遞歸調用,繼續從當前結點向下進行堆調整
}
}
int BuildHeap(int A[], int n) // 建堆,時間複雜度O(n)
{
int heap_size = n;
for (int i = heap_size / 2 - 1; i >= 0; i--) // 從每一個非葉結點開始向下進行堆調整
Heapify(A, i, heap_size);
return heap_size;
}
void HeapSort(int A[], int n)
{
int heap_size = BuildHeap(A, n); // 建立一個最大堆
while (heap_size > 1) // 堆(無序區)元素個數大於1,未完成排序
{
// 將堆頂元素與堆的最後一個元素互換,並從堆中去掉最後一個元素
// 此處交換操作很有可能把後面元素的穩定性打亂,所以堆排序是不穩定的排序算法
Swap(A, 0, --heap_size);
Heapify(A, 0, heap_size); // 從新的堆頂元素開始向下進行堆調整,時間複雜度O(logn)
}
}
int main()
{
int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 從小到大堆排序
int n = sizeof(A) / sizeof(int);
HeapSort(A, n);
printf("堆排序結果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
堆排序是不穩定的排序算法,不穩定發生在堆頂元素與A[i]交換的時刻。
比如序列:{ 9, 5, 7, 5 },堆頂元素是9,堆排序下一步將9和第二個5進行交換,得到序列 { 5, 5,7, 9 },再進行堆調整得到{ 7, 5, 5, 9 },重複之前的操作最後得到{ 5, 5, 7, 9 }從而改變了兩個5的相對次序。
堆排序圖解詳見:https://www.cnblogs.com/MOBIN/p/5374217.html快速排序(Quick Sort)
快速排序是由東尼·霍爾所發展的一種排序算法。在平均狀況下,排序n個元素要O(nlogn)次比較。在最壞狀況下則需要O(n^2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他O(nlogn)算法更快,因爲它的內部循環可以在大部分的架構上很有效率地被實現出來。
快速排序使用分治策略(Divide and Conquer)來把一個序列分爲兩個子序列。步驟爲:
- 從序列中挑出一個元素,作爲"基準"(pivot).
- 把所有比基準值小的元素放在基準前面,所有比基準值大的元素放在基準的後面(相同的數可以到任一邊),這個稱爲分區(partition)操作。
- 對每個分區遞歸地進行步驟1~2,遞歸的結束條件是序列的大小是0或1,這時整體已經被排好序了。
#include <stdio.h>
// 分類 ------------ 內部比較排序
// 數據結構 --------- 數組
// 最差時間複雜度 ---- 每次選取的基準都是最大(或最小)的元素,導致每次只劃分出了一個分區,需要進行n-1次劃分才能結束遞歸,時間複雜度爲O(n^2)
// 最優時間複雜度 ---- 每次選取的基準都是中位數,這樣每次都均勻的劃分出兩個分區,只需要logn次劃分就能結束遞歸,時間複雜度爲O(nlogn)
// 平均時間複雜度 ---- O(nlogn)
// 所需輔助空間 ------ 主要是遞歸造成的棧空間的使用(用來保存left和right等局部變量),取決於遞歸樹的深度,一般爲O(logn),最差爲O(n)
// 穩定性 ---------- 不穩定
void Swap(int A[], int i, int j)
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
int Partition(int A[], int left, int right) // 劃分函數
{
int pivot = A[right]; // 這裏每次都選擇最後一個元素作爲基準
int tail = left - 1; // tail爲小於基準的子數組最後一個元素的索引
for (int i = left; i < right; i++) // 遍歷基準以外的其他元素
{
if (A[i] <= pivot) // 把小於等於基準的元素放到前一個子數組末尾
{
Swap(A, ++tail, i);
}
}
Swap(A, tail + 1, right); // 最後把基準放到前一個子數組的後邊,剩下的子數組既是大於基準的子數組
// 該操作很有可能把後面元素的穩定性打亂,所以快速排序是不穩定的排序算法
return tail + 1; // 返回基準的索引
}
void QuickSort(int A[], int left, int right)
{
if (left >= right)
return;
int pivot_index = Partition(A, left, right); // 基準的索引
QuickSort(A, left, pivot_index - 1);
QuickSort(A, pivot_index + 1, right);
}
int main()
{
int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 }; // 從小到大快速排序
int n = sizeof(A) / sizeof(int);
QuickSort(A, 0, n - 1);
printf("快速排序結果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
快速排序是不穩定的排序算法,不穩定發生在基準元素與A[tail+1]交換的時刻。
比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基準元素是5,一次劃分操作後5要和第一個8進行交換,從而改變了兩個元素8的相對次序。
Java系統提供的Arrays.sort函數。對於基礎類型,底層使用快速排序。對於非基礎類型,底層使用歸併排序。請問是爲什麼?
答:這是考慮到排序算法的穩定性。對於基礎類型,相同值是無差別的,排序前後相同值的相對位置並不重要,所以選擇更爲高效的快速排序,儘管它是不穩定的排序算法;而對於非基礎類型,排序前後相等實例的相對位置不宜改變,所以選擇穩定的歸併排序。
上面講到常用的比較排序算法,主要有冒泡排序,選擇排序,插入排序,歸併排序,堆排序,快速排序等。
下面我們來探討一下常用的非比較排序算法:計數排序,基數排序,桶排序。在一定條件下,它們的時間複雜度可以達到O(n)。
這裏我們用到的唯一數據結構就是數組,當然也可以利用鏈表來實現下述算法。
計數排序(Counting Sort)
計數排序用到一個額外的計數數組C,根據數組C來將原數組A中的元素排到正確的位置。
通俗地理解,例如有10個年齡不同的人,假如統計出有8個人的年齡不比小明大(即小於等於小明的年齡,這裏也包括了小明),那麼小明的年齡就排在第8位,通過這種思想可以確定每個人的位置,也就排好了序。當然,年齡一樣時需要特殊處理(保證穩定性):通過反向填充目標數組,填充完畢後將對應的數字統計遞減,可以確保計數排序的穩定性。
計數排序的步驟如下:
- 統計數組A中每個值A[i]出現的次數,存入C[A[i]]
- 從前向後,使數組C中的每個值等於其與前一項相加,這樣數組C[A[i]]就變成了代表數組A中小於等於A[i]的元素個數
- 反向填充目標數組B:將數組元素A[i]放在數組B的第C[A[i]]個位置(下標爲C[A[i]] - 1),每放一個元素就將C[A[i]]遞減
計數排序的實現代碼如下:
#include<iostream>
using namespace std;
// 分類 ------------ 內部非比較排序
// 數據結構 --------- 數組
// 最差時間複雜度 ---- O(n + k)
// 最優時間複雜度 ---- O(n + k)
// 平均時間複雜度 ---- O(n + k)
// 所需輔助空間 ------ O(n + k)
// 穩定性 ----------- 穩定
const int k = 100; // 基數爲100,排序[0,99]內的整數
int C[k]; // 計數數組
void CountingSort(int A[], int n)
{
for (int i = 0; i < k; i++) // 初始化,將數組C中的元素置0(此步驟可省略,整型數組元素默認值爲0)
{
C[i] = 0;
}
for (int i = 0; i < n; i++) // 使C[i]保存着等於i的元素個數
{
C[A[i]]++;
}
for (int i = 1; i < k; i++) // 使C[i]保存着小於等於i的元素個數,排序後元素i就放在第C[i]個輸出位置上
{
C[i] = C[i] + C[i - 1];
}
int *B = (int *)malloc((n) * sizeof(int));// 分配臨時空間,長度爲n,用來暫存中間數據
for (int i = n - 1; i >= 0; i--) // 從後向前掃描保證計數排序的穩定性(重複元素相對次序不變)
{
B[--C[A[i]]] = A[i]; // 把每個元素A[i]放到它在輸出數組B中的正確位置上
// 當再遇到重複元素時會被放在當前元素的前一個位置上保證計數排序的穩定性
}
for (int i = 0; i < n; i++) // 把臨時空間B中的數據拷貝回A
{
A[i] = B[i];
}
free(B); // 釋放臨時空間
}
int main()
{
int A[] = { 15, 22, 19, 46, 27, 73, 1, 19, 8 }; // 針對計數排序設計的輸入,每一個元素都在[0,100]上且有重複元素
int n = sizeof(A) / sizeof(int);
CountingSort(A, n);
printf("計數排序結果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
計數排序的時間複雜度和空間複雜度與數組A的數據範圍(A中元素的最大值與最小值的差加上1)有關,因此對於數據範圍很大的數組,計數排序需要大量時間和內存。
例如:對0到99之間的數字進行排序,計數排序是最好的算法,然而計數排序並不適合按字母順序排序人名,將計數排序用在基數排序算法中,能夠更有效的排序數據範圍很大的數組。
基數排序(Radix Sort)
基數排序的發明可以追溯到1887年赫爾曼·何樂禮在打孔卡片製表機上的貢獻。它是這樣實現的:將所有待比較正整數統一爲同樣的數位長度,數位較短的數前面補零。然後,從最低位開始進行基數爲10的計數排序,一直到最高位計數排序完後,數列就變成一個有序序列(利用了計數排序的穩定性)。
基數排序的實現代碼如下:#include<iostream>
using namespace std;
// 分類 ------------- 內部非比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(n * dn)
// 最優時間複雜度 ---- O(n * dn)
// 平均時間複雜度 ---- O(n * dn)
// 所需輔助空間 ------ O(n * dn)
// 穩定性 ----------- 穩定
const int dn = 3; // 待排序的元素爲三位數及以下
const int k = 10; // 基數爲10,每一位的數字都是[0,9]內的整數
int C[k];
int GetDigit(int x, int d) // 獲得元素x的第d位數字
{
int radix[] = { 1, 1, 10, 100 };// 最大爲三位數,所以這裏只要到百位就滿足了
return (x / radix[d]) % 10;
}
void CountingSort(int A[], int n, int d)// 依據元素的第d位數字,對A數組進行計數排序
{
for (int i = 0; i < k; i++)
{
C[i] = 0;
}
for (int i = 0; i < n; i++) // n個數各自的第d位數字+1 爲下標的C[ ]數值+1
{
C[GetDigit(A[i], d)]++;
}
for (int i = 1; i < k; i++)
{
C[i] = C[i] + C[i - 1]; //出現次數轉換爲排序位置
}
int *B = (int*)malloc(n * sizeof(int));
for (int i = n - 1; i >= 0; i--)
{
int dight = GetDigit(A[i], d); // 元素A[i]當前位數字爲dight
B[--C[dight]] = A[i]; // 根據當前位數字,把每個元素A[i]放到它在輸出數組B中的正確位置上
// 當再遇到當前位數字同爲dight的元素時,會將其放在當前元素的前一個位置上保證計數排序的穩定性
}
for (int i = 0; i < n; i++)
{
A[i] = B[i];
}
free(B);
}
void LsdRadixSort(int A[], int n) // 最低位優先基數排序
{
for (int d = 1; d <= dn; d++) // 從低位到高位
CountingSort(A, n, d); // 依據第d位數字對A進行計數排序
}
int main()
{
int A[] = { 20, 90, 64, 289, 998, 365, 852, 123, 789, 456 };// 針對基數排序設計的輸入
int n = sizeof(A) / sizeof(int);
LsdRadixSort(A, n);
printf("基數排序結果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
下圖給出了對{ 329, 457, 657, 839,436, 720, 355 }進行基數排序的簡單演示過程
基數排序的時間複雜度是O(n * dn),其中n是待排序元素個數,dn是數字位數。這個時間複雜度不一定優於O(n log n),dn的大小取決於數字位的選擇(比如比特位數),和待排序數據所屬數據類型的全集的大小;dn決定了進行多少輪處理,而n是每輪處理的操作數目。
如果考慮和比較排序進行對照,基數排序的形式複雜度雖然不一定更小,但由於不進行比較,因此其基本操作的代價較小,而且如果適當的選擇基數,dn一般不大於log n,所以基數排序一般要快過基於比較的排序,比如快速排序。由於整數也可以表達字符串(比如名字或日期)和特定格式的浮點數,所以基數排序並不是只能用於整數排序。桶排序(Bucket Sort)
桶排序也叫箱排序。工作的原理是將數組元素映射到有限數量個桶裏,利用計數排序可以定位桶的邊界,每個桶再各自進行桶內排序(使用其它排序算法或以遞歸方式繼續使用桶排序)。
桶排序的實現代碼如下:
#include<iostream>
using namespace std;
// 分類 ------------- 內部非比較排序
// 數據結構 --------- 數組
// 最差時間複雜度 ---- O(nlogn)或O(n^2),只有一個桶,取決於桶內排序方式
// 最優時間複雜度 ---- O(n),每個元素佔一個桶
// 平均時間複雜度 ---- O(n),保證各個桶內元素個數均勻即可
// 所需輔助空間 ------ O(n + bn)
// 穩定性 ----------- 穩定
/* 本程序用數組模擬桶 */
const int bn = 5; // 這裏排序[0,49]的元素,使用5個桶就夠了,也可以根據輸入動態確定桶的數量
int C[bn]; // 計數數組,存放桶的邊界信息
void InsertionSort(int A[], int left, int right)
{
for (int i = left + 1; i <= right; i++) // 從第二張牌開始抓,直到最後一張牌
{
int get = A[i];
int j = i - 1;
while (j >= left && A[j] > get)
{
A[j + 1] = A[j];
j--;
}
A[j + 1] = get;
}
}
int MapToBucket(int x)
{
return x / 10; // 映射函數f(x),作用相當於快排中的Partition,把大量數據分割成基本有序的數據塊
}
void CountingSort(int A[], int n)
{
for (int i = 0; i < bn; i++)
{
C[i] = 0;
}
for (int i = 0; i < n; i++) // 使C[i]保存着i號桶中元素的個數
{
C[MapToBucket(A[i])]++;
}
for (int i = 1; i < bn; i++) // 定位桶邊界:初始時,C[i]-1爲i號桶最後一個元素的位置
{
C[i] = C[i] + C[i - 1];
}
int *B = (int *)malloc((n) * sizeof(int));
for (int i = n - 1; i >= 0; i--)// 從後向前掃描保證計數排序的穩定性(重複元素相對次序不變)
{
int b = MapToBucket(A[i]); // 元素A[i]位於b號桶
B[--C[b]] = A[i]; // 把每個元素A[i]放到它在輸出數組B中的正確位置上
// 桶的邊界被更新:C[b]爲b號桶第一個元素的位置
}
for (int i = 0; i < n; i++)
{
A[i] = B[i];
}
free(B);
}
void BucketSort(int A[], int n)
{
CountingSort(A, n); // 利用計數排序確定各個桶的邊界(分桶)
for (int i = 0; i < bn; i++) // 對每一個桶中的元素應用插入排序
{
int left = C[i]; // C[i]爲i號桶第一個元素的位置
int right = (i == bn - 1 ? n - 1 : C[i + 1] - 1);// C[i+1]-1爲i號桶最後一個元素的位置
if (left < right) // 對元素個數大於1的桶進行桶內插入排序
InsertionSort(A, left, right);
}
}
int main()
{
int A[] = { 29, 25, 3, 49, 9, 37, 21, 43 };// 針對桶排序設計的輸入
int n = sizeof(A) / sizeof(int);
BucketSort(A, n);
printf("桶排序結果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
下圖給出了對{ 29, 25, 3, 49, 9, 37,21, 43 }進行桶排序的簡單演示過程
桶排序不是比較排序,不受到O(nlogn)下限的影響,它是鴿巢排序的一種歸納結果,當所要排序的數組值分散均勻的時候,桶排序擁有線性的時間複雜度。
十二、鏈接指示:extern “C”(作用)
C++程序有時需要調用其他語言編寫的函數,最常見的是調用C語言編寫的函數。像所有其他名字一樣,其他語言中的函數名字也必須在C++中進行聲明,並且該聲明必須指定返回類型和形參列表。對於其他語言編寫的函數來說,編譯器檢查其調用方式與處理普通C++函數的方式相同,但生成的代碼有所區別。C++使用鏈接指示(linkage directive)指出任意非C++函數所用的語言。
聲明一個非C++函數:
鏈接指示可以有兩種形式:單個或複合。鏈接指示不能出現在類定義或函數定義的內部。
例如:
單語句:
extern "C" size_t strlen(const char *);
複合語句:extern "C" {
int strcmp(const char*, const char*);
char *strcat(char*, const char*);
}
鏈接指示與頭文件:複合語句:
extern "C" {
#include <string.h>
}
指向extern "C"函數的指針:
編寫函數所用的語言是函數類型的一部分。(指向其他語言編寫的函數的指針必須與函數本身使用相同的鏈接指示)
extern "C" void(*pf)(int);
當我們使用pf調用函數時,編譯器認定當前調用的是一個C函數。
指向C函數的指針與指向C++函數的指針是不一樣的類型。
鏈接指示對整個聲明都有效:
當我們使用鏈接指示時,他不僅對函數有效,而且對作爲返回類型或形參類型的函數指針也有效。
//f1是一個C函數,它的形參是一個指向C函數的指針
extern "C" void f1( void(*)(int) );
因爲鏈接指示同時作用於聲明語句中的所有函數,所以如果我們希望給C++函數傳入一個指向C函數的指針,則必須使用類型別名。
//FC是一個指向C函數的指針
extern "C" typedef void FC( int );
//f2是一個C++函數,該函數的形參是指向C函數的指針
void f2(FC *);
導出C++函數到其他語言:
通過使用鏈接指示對函數進行定義,我們可以令一個C++函數在其他語言編寫的程序中可用。
//calc函數可以被C程序調用
extern "C" double calc( double dparm ) {/*......*/}
編譯器將爲該函數生成適合指定語言的代碼
對鏈接到C的預處理器的支持
有時需要在C和C++中編譯同一個源文件,爲了實現這一目的,在編譯C++版本的程序時預處理器定義 __cplusplus(兩個下劃線)。利用這個變量,我們可以在編譯C++程序的時候有條件地包含進來一些代碼:
#ifndef __cplusplus
//正確:我們在編譯C++程序
extern "C"
#endif
int strcmp( const char*, const char* );
重載函數與鏈接指示:
C語言不支持函數重載,因此也不難理解爲什麼一個C鏈接指示只能用於說明一組重載函數中的某一行了:
//錯誤:兩個extern "C"函數的名字相同
extern "C" void print ( const char* );
extern "C" void print ( int );
如果在一組重載函數中有一個C函數,則其餘函數必定都是C++函數。
十三、C語言和C++有什麼區別?
(大體講一下,繼承、多態、封裝、異常處理等)
差不多是win98跟winXP的關係。C++是在C的基礎上增加了新的理論,玩出了新的花樣。所以叫C加加。
C是一個結構化語言,它的重點在於算法和數據結構。C程序的設計首要考慮的是如何通過一個過程,對輸入(或環境條件)進行運算處理得到輸出(或實現過程(事務)控制)。
C++,首要考慮的是如何構造一個對象模型,讓這個模型能夠契合與之對應的問題域,這樣就可以通過獲取對象的狀態信息得到輸出或實現過程(事務)控制。所以C與C++的最大區別在於它們的用於解決問題的思想方法不一樣。之所以說C++比C更先進,是因爲“ 設計這個概念已經被融入到C++之中 ”。
這個問題問的就沒太大意義。因爲 C++ 把 C 作爲它的子集。那麼作爲一個問題,還是要大概回答一下的。換句話說,C++ 在 C 的基礎上引入了那些新的東西?
簡而言之,就是 C++ 比 C 多了 class,所以 C++ 裏引入了面向對象思想,產生了面向接口編程,比如說微軟的 COM 技術。C++ 引入模板,將對象類型抽象化和獨立,產生了通用編程思想。這是兩種語言的區別所在。
由於有了面向對象思想,所以在語言層面,C 中的數據,方法都是平面的,開放的,零散的,缺乏組織層次關係的,但也是非常明確的,靜態的。而 C++ 可以做到數據和方法的封裝,使得 C++ 在語言層面可以有組織,有層次,關係也是隱晦的。同時,C++ 引入對象後也引入了更多複雜性,編譯器需要做的事情更多,更復雜。
C++有以下幾個範型:
1,過程形式 2,面向對象形式 3,函數形式 4,泛型形式 5,元編程形式。這就超過C的範疇了。
可以認爲C++有四個次語言:
1,C. 2,面向對象C++ 3,模板C++ 4, STL
以下回答印象分情況:
c面向過程,c++面向對象(-⭐)
c適合開發比c++還底層的東西,如操作系統,協議棧。(⭐⭐)
精通c++的,跟c完全是2種語言,除了基本類型,條件循環語句,等差不多外,在編程思維上完全2種東西。c++思考的是對象,c思考的是寄存器,堆棧。沒有很好掌握的,c++就是比c還容易出錯的c,簡稱c++(⭐⭐⭐)
c能做的,c++也能做,在一定程度上,c++包含c(-⭐)
C語言與C++的區別有很多,下面是簡要概述:
1、全新的程序程序思維,C語言是面向過程的,而C++是面向對象的。
2、C語言有標準的函數庫,它們鬆散的,只是把功能相同的函數放在一個頭文件中;而C++對於大多數的函數都是有集成的很緊密,特別是C語言中沒有的C++中的API是對Window系統的大多數API有機的組合,是一個集體。但你也可能單獨調用API。
3、特別是C++中的圖形處理,它和C語言的圖形有很大的區別。C語言中的圖形處理函數基本上是不能用在C++中的。C語言標準中不包括圖形處理。
4、C和C++中都有結構的概念,但是在C語言中結構只有成員變量,而沒成員方法,而在C++中結構中,它可以有自己的成員變量和成員函數。但是在C語言中結構的成員是公共的,什麼想訪問它的都可以訪問;而在VC++中它沒有加限定符的爲私有的。
5、C語言可以寫很多方面的程序,但是C++可以寫得更多更好,C++可以寫基於DOS的程序,寫DLL,寫控件,寫系統。
6、C語言對程序的文件的組織是鬆散的,幾乎是全要程序處理;而c++對文件的組織是以工程,各文件分類明確。
7、C++中的IDE很智能,和VB一樣,有的功能可能比VB還強。
8、C++對可以自動生成你想要的程序結構使你可以省很多時間。有很多可用工具如加入MFC中的類的時候,加入變量的時候等等。
9、C++中的附加工具也有很多,可以進行系統的分析,可以查看API;可以查看控件。
10、調試功能強大,並且方法多樣。