C++支持3種編程方法即“面向過程編程”、“泛型編程”和“面向對象編程”。掌握這3中編程方法,就能夠學好C++。
這篇日志談談如何用C++進行面向過程編程。
C語言的編程方式就是面向過程編程,將C++與C對比起來學習是掌握C++面向過程編程的一種捷徑。
對比1:基本語法
在基本語法上C++和C語言是相同的,C語言有的語法C++都有,而C++有的語法C語言基本全有。如果不考慮泛型編程和面向對象編程,C++在基本語法上與C語言無區別。因此學習過C語言的程序員,可以直接跳過C++基本語法的學習。
對比2:基本數據類型
所謂基本數據類型是指int、float、double等這些類型。相對于C語言,C++增加了bool型。bool型只有兩個值true和false,分別表示真和假。
其通常使用的方式如下:
bool flag=(x>10);
if(flag)
printf("x>10");
else
printf("x<=10");
當然這是一個示例,實際的情況可能要復雜得多。bool型可以賦值為整型。
例如:
bool flag=10;
bool flag=-10;
bool flag=0;
只要賦值的整型不是0,那么bool型的值就是true,反之是false。
對比3:static變量
C++允許將局部變量定義為static。例如:
int fun()
{
static int x=0;
x++;
return x;
}
如果按照下列方式調用該函數:
fun();
fun();
int x=fun();
問x的值是多少?
答案是3。
其原因在于,一旦一個變量被聲明為static,則該變量一直存在于函數的局部,而不會在函數退出時銷毀。故此按照上述調用實際上執行了三次x++。
static變量的引入是為了解決全局變量的過度使用。如果C語言沒有static變量,那么程序只能這么寫。
int x;
int fun()
{
x++;
return x;
}
此時x是一個全局變量。
對比4:變量初始化
C++允許一種新的變量初始化方法,如下:
int a(10);
此語句與C語言中的:
int a=10;
等價。
引入這種新語法的原因是為了與對象的初始化統一。例如:
ifstream f("c:\\test.txt");
該語句定義了對象f,f是一個輸入文件流對象(關于對象的問題后面再談)。由于對象的初始化只能采用上面的方式,因此索性引入了
int a(10);
這樣的語法。否則傻瓜編譯器可能就要出現二義性錯誤了。
換言之,這種新語法完全可以不用,用也沒有壞處,反正編譯器認識。
對比5:默認函數參數
int fun(int x=0)
{
x++;
return x;
}
上述函數使用了默認參數,即給參數x一個默認值0。調用函數時
int y=fun();
int y=fun(0);
兩者的結果完全等價,y的值都是1。而
int y=fun(10);
調用的結果就是11。
對比6:引用
下列代碼中,b稱為a的引用。
int a=10;
int &b=a;
此時若執行如下代碼:
b=b+10;
請問a的值是多少?答案是20。
為什么修改b,同時也會修改a呢?因為b是a的引用。也就是b和a是同一個東西。
上述代碼可以換成指針形式,即:
int a=10;
int *b=&a;
*b=*b+10;
其結果仍然是把a修改為了20。
那么為什么要引入引用呢?因為引用比指針更加安全。如果我們使用指針,那么就可能出現下面的情況:
int a=10;
int *b=&a;
b=b+10;
這個程序員把代碼寫錯了,但是編譯器并不會發出任何警告。因為在編譯器眼中,這個代碼是完全正確的。
b=b+10;
意味著b指向的地址后移10,此時b已經不再指向a,而指向了一個我們不知道的位置。如果把這個代碼改成下面的形式,結果更是災難性的。
int a=10;
int *b=&a;
b=b+10; //本意是寫*b=*b+10,因為疏忽忘記了*
*b=100;
按照程序員的本意,此時a和b都應該變成100。但很遺憾上述代碼不僅a仍然是10,并且還修改了一個不確定位置的值為100。若修改的位置正好是你的銀行存款,原本的數字是10000000,而現在修改為了100,你甚至都不知道是怎么修改的。你說這不是災難性的嗎?
如果將指針改為引用,情況就好得多了。
換言之指針類型可以直接操作地址值,而引用是不行的。
引用的另外一個特點是不允許空引用。例如
int&b;
這樣的聲明就是錯誤的,而指針是可以為空的。
int *b;
是合法的。
扯一句題外話,java的引用和C++的引用也是不同的。java的引用是可以為空的。因此java的引用類型等價于C++不允許修改地址的指針。
引用的一個重大用途是用于參數傳遞。先看下列代碼:
int fun(vector<int> v)
{
v[0]++;
return v[0];
}
int main()
{
vector<int> v(3);
v[0]=1;
v[1]=2;
v[2]=3;
int y=fun(v);
return 0;
}
其中vector是C++標準庫中的新類型。大家可以先把它認為是數組。
上述代碼,將v傳遞到fun中,并在其中將0號元素++后返回。程序看似沒有問題,但實際上隱藏了巨大的隱患。
vector<int> v(300000000);
v[0]=1;
v[1]=2;
v[2]=3;
.....
v[300000000]=....;
int y=fun(v);
把程序修改成上面就可以看出問題了。問題在于v的大小為300000000,這個長度簡直是太大了。但調用fun的時候,編譯器會將v拷貝一份,然后將這個拷貝傳遞到函數fun。可以想象一下這么大的一個數據,拷貝是不是要花時間?拷貝是不是要消耗內存?怎么避免這個問題呢?
事實上C語言可以用指針解決這個問題,代碼修改后如下
int fun(vector<int> *v)
{
(*v)[0]++;
return (*v)[0];
}
int main()
{
vector<int> v(3);
v[0]=1;
v[1]=2;
v[2]=3;
int y=fun(&v);
return 0;
}
此時函數調用時傳遞的就是指針,不再是整個值,也就不存在拷貝的問題。用引用同樣可以解決這個問題。
int fun(vector<int> &v)
{
v[0]++;
return v[0];
}
int main()
{
vector<int> v(3);
v[0]=1;
v[1]=2;
v[2]=3;
int y=fun(v);
return 0;
}
從代碼上看是不是引用更友好一些呢?
但是我們也發現引用和指針有一個副作用,那就是v[0]的值會在fun中被修改。因此代碼應該修改一下:
int fun(vector<int> &v)
{
return v[0]+1;
}
但我們知道程序員是會犯錯誤的,盡管我們知道應該如此寫,但也許一不小心就犯錯了。有解決的辦法嗎?那就是給引用加上const。
對比7:const變量
C++引入了const關鍵字,用于定義常量。例如:
const int a=10;
此時a稱為一個整型常量,即a的值不能修改,若修改則編譯器會報錯。
在參數傳遞時可以為參數加上const,以避免參數被修改。如下:
int fun(const vector<int> &v)
{
v[0]++; //此處將報錯
return v[0];
}
const的另一個有趣問題在于const放的位置。
const int a=10;
int const a=10;
都是合法的。那么在使用上有區別嗎?也沒有區別,都是int常量。可是如果將int換成引用或者指針情況就大不一樣了。
cont int *a; //常量指針,a指向的類型為const int, a的類型為*
int const *a; //常量指針,a指向的類型為int const , a的類型為*
int* const a; //指針常量,a指向的類型為int, a的類型為*const
const int* const a; //指向常量的指針常量,a指向的類型為const int, a的類型為*const
有了*號之后組合數量猛增,那么引用情況也是類似的
const int &a; //常量引用,a引用的類型為const int, a的類型為&
int const &a; //常量引用,a引用的類型為int const, a的類型為&
int& const a; //引用常量,a引用的類型為int, a的類型為& const
const int& const a; //指向常量的引用常量,a引用的類型為const int, a的類型為& const
對初學者而言這實在是很復雜,不過了解一下編譯器的原理,就很容易搞清楚。
說白了上面的代碼都是在定義變量a,a是引用或者指針,引用就是&,指針就是*。對于引用或者指針,那么都有引用或者指向的變量類型。
以“&”和“*”為分隔符,“&”和“*”前面的就是引用或者指向的變量類型。
而在“&”和“*”之后的符號則說明,變量a本身是不是一個const。
搞明白了這樣的原則,那么下面的類型就不是很變態了。
const int** &const a;
通常如此變態的用法只有在考試時出現,如果是在實際項目中誰敢這么用,那么就有開除的風險了。
對比8:new和delete
new和delete是非常重要的操作符,用于動態內存管理。C語言里面有malloc和free兩個函數與之對應。
但很多C語言課程講到malloc和free的時候都直接跳過不講,很多學習C語言的學生也通常沒有學習過內存管理。
因此new和delete盡管很重要但很多學生在大學四年內都無法掌握,盡管這是實際應用中非常重要的。
這也難怪,如果我們總是做一些計算水仙花數的題目,是不需要用到內存管理的。
假設現在有一個需求:將硬盤上的50MB的數據讀取到程序中,并保存在變量a里面。
我們不管怎么讀取硬盤數據,僅考慮變量a應該怎么定義。
unsigned char a[1024*1024*50]; //事實上這么寫可能會出錯,因為可能不允許定義這么大的數組哦
應該是這樣吧,一個unsigned char是8位,也就是1個byte,1024*1024*50個byte剛好50MB。
現在考慮一個復雜的情況,如果文件的大小事不確定的,又應該如何定義變量呢?
unsigned char a[SIZE];
其中SIZE表示文件的大小(字節數),但問題在于我們根本不知道SIZE有多大。如果SIZE是一個變量,上述定義是無法編譯的。
因此就必須有一種動態分配內存的機制:
unsigned char *a=new unsigned char[SIZE];
此處的SIZE可以是一個變量。
我們看看下面代碼的執行效果:
unsigned char *a=new unsigned char[1024*1024*50];
while(true);
動態分配50MB的內存,同時循環不退出,這是為了便于觀察系統內存的變化。看下圖

注意看Demos.exe*32這個進程,它占用了51,748K內存,也就是大約50MB內存,換言之分配50MB是成功的。
程序修改為如下形式:
unsigned char *a=new unsigned char[1024*1024*100];
while(true);

沒錯分配了100MB內存。下面還有一個代碼:
while(true)
{
unsigned char *a=new unsigned char[1024*1024*100];
}
感興趣的可以自己試試,此代碼可以測試你的系統在幾秒內藍屏。我是不會測試這個代碼的。
這個代碼會在循環中不斷的分配100MB的內存,很快內存就會分配完,然后機器掛掉。
有人也許會有疑問:a不是一個局部變量嗎?再次循環的時候前一個a變量不是銷毀了嗎?
的確a會不斷的產生和銷毀,但那是變量a銷毀。a是一個指針,a所指的對象并沒有銷毀。
若要銷毀分配的內存可以如下做:
while(true)
{
unsigned char *a=new unsigned char[1024*1024*100];
//do something
delete a[];
}
這個程序的執行結果如下圖:
時刻A

時刻B

時刻A和B的內存是不同的,這說明程序在動態分配內存,同時內存不會一直增加,由于有delete釋放內存,因此程序的內存在100MB以內(為什么是變化的?自己考慮吧)
new和delete比較容易混淆的地方是它們的另外一種用法:
int *a=new int(10);
delete a;
=============
int *a=new int[10];
delete []a;
上述代碼有一個非常細微的差別
new int(10);和new int[10];
delete a;和delete []a;
解釋一下
int *a=new int(10);
表示動態分配了1個int型的內存,并將內存的值初始化為10,然后將地址賦值給指針變量a。
delete a;
用于刪除指針變量a。
===========================
int *a=new int[10];
表示動態分配了10個int型的內存,并將內存的首地址賦值給指針變量a。
delete []a;
用于刪除指針變量a。
============================
換言之
new 類型(參數)用于動態生成單個變量,并賦初值
delete 變量 用于刪除
new 類型[數量] 用于分配多個內存空間
delete []變量 用于刪除
關于new和delete另一個需要記住的地方是new之后必須delete,否則就會內存泄漏。
如果你的程序開始需要new,那么很快你也會掌握delete。
但如果你的程序從來都不需要new,那么你也不會明白delete。
如果你的C++程序從來沒有new和delete,那么沒有哪個公司敢雇用你,除了讓你當清潔工。
對比9:名字空間
在C++中如果想使用標準庫中的類和函數就會用到名字空間。例如:
#include<iostream>
using namespace std; //這就是名字空間
int main()
{
cout<<"hello world"<<endl;
return 0;
}
其中std是名字空間,using namespace表示使用std這個名字空間。
關于名字空間,一般正常的C++書籍都會介紹,這里就不說了。如果你的C++書籍沒有解釋名字空間,那么建議換一本書(這是有可能的,特別是譚浩強之類的書)。
比較奇怪的一點是,盡管名字空間這個技術還算有用,并且標準庫中也用了,但在實際編程中,大家還很少用

。也許C++程序員現在多數都是些LIB和DLL,名字沖突的概率比直接寫源碼低的原因吧。
對比10:inline
一個函數用inline修飾就成為內聯函數,如下:
inline void fun()
{
}
inline是給編譯器用的。如果一個函數聲明為inline,那么編譯器會在調用該函數的地方直接展開函數(而不是函數調用)。這樣會增加函數調用的效率。內聯函數可以部分替代C語言的宏定義。
以上只是官方的一種說法。官方其實還有另外一個說法,就是inline不是強制性的。換句話說,編譯器可以忽略掉inline。
個人看法,inline還是不inline真的無法提高什么性能,尤其是如果你想用C++寫面向對象程序。
對比11:頭文件的使用
C++標準庫的頭文件都是不帶.h結尾的。例如
#include<iostream>
#include<string>
#include<vector>
事實上頭文件以.h結尾只是一個習慣,對于編譯器而言,就算你用.exe結尾,它也照樣不管。
對比12:編寫自定義頭文件
如果你沒有編寫過自定義頭文件,那么說明你的程序還不夠大。不夠大的含義是:恐怕不到100行。。。。。
但凡大點的程序都是要寫自定義頭文件的。這有幾個原因:1.程序分模塊;2.程序分工;3.編譯效率;4.編譯器限制太大的文件。
不詳細解釋了。自定義頭文件也是項目大到一定程度,自然會寫的。
關鍵是怎么寫才正確,這里有幾個原則。
原則1:只有函數聲明,不能有函數實現
以下代碼稱為函數聲明
void fun();
以下代碼稱為函數實現
void fun()
{
}
那么函數實現寫在什么地方呢?寫在一個cpp文件里面。
至于為什么要這么做,就要扯到編譯器的實現問題了。總之不這么做,遲早有一天會出錯。而且錯得讓人找不到北。
這里有一個例外情況inline函數的實現是可以放在頭文件里面的。但這只是例外。
原則2:變量聲明加extern
頭文件中聲明的變量一般都是全局變量。如果此變量是常量,那么可以不加extern,其他變量是加上的好。
原因也是編譯器的實現機制。
全局變量若定義在頭文件里面應該加上extern,如下:
//a.h文件
extern int x;
全局變量的初始化則放到一個cpp文件中,例如:
//a.cpp
int x=10;
然后在b.cpp文件中就可以使用x了,如下:
#include"a.h"
int main()
{
x=20;
}
上述代碼如果去掉extern會出錯。
但如果去掉extern,并且把初始化放到頭文件中,則有可能正確。例如
//a.h文件
int x=10;
==========================
#include"a.h"
int main()
{
x=20;
}
這完全可能編譯正確,但也僅僅是由于運氣比較好。
如果a.h在多個cpp文件中被引用就會出錯的。
原則3:用宏定義避免同一個頭文件被多次#include
//a.h文件
int x=10;
==============================
#include"a.h"
#include"a.h"
int main()
{
x=20;
}
注意a.h被#include了兩次,x就被定義了兩次,不出錯才怪呢。
用宏定義就可以避免這種錯誤。
//a.h文件
#ifndef A_H
#define A_H
int x=10;
#endif
這樣無論#include幾次,x都只定義1次。
特別注意:如果不采用原則2的寫法,即使加上了宏定義,下列代碼仍然是錯誤的。
//b.h
#ifndef B_H
#define B_H
void fun();
#endif
==================
//b.cpp
#include"a.h"
void fun()
{
x++;
}
===================
#include "a.h"
#include "b.h"
void main()
{
x++;
}
C++的面向過程編程基本上是對C語言的增強,面向對象才是C++的核心內容。如果你用面向對象編程,那么本文的很多內容其實可以忽略。
寫完了,大家看看有沒有什么遺漏。