(轉)C++|深入理解聲明、定義(實現)、初始化及頭、源文件的組織-pf文件

C++|深入理解聲明、定義(實現)、初始化及頭、源文件的組織-pf文件

原文鏈接:https://www.shangyouw.cn/wenjian/arc52510.html

程序是對聲明和定義的數據的處理。數據可以聲明爲變量、常量、數據結構或類的數據成員,處理可以聲明或實現爲函數,或類的成員方法。

我們現代使用的絕大多數計算機都是“存儲程序”概念的計算機。也就是說,程序處理的數據和處理數據的代碼都保存在可以隨機訪問的內存中。

數據存儲有一個很重要的概念:“要存得進去,取得出來”,且還需要考慮空間(存儲空間)和時間(程序運行時間)的效率。

數據存儲到內存中,需要考慮幾個問題(以下內容中,括號內的內容是程序語言相應的解決方案及使用的關鍵字):

 

1 數據需要不需要有保護機制?(常量和變量,常量是對數據的保護,如關鍵字const,但只有常量不夠,正像我們不能只計算3+4、6+7這樣的算術運算,我們要能實現有一定通用性的a+b這樣的變量運算,所以還需要變量)

 

2 需要多大空間?(不同的數據類型,如int、unsign int、char、double等,有不同的取值範圍,使用不同長度的內存空間)

3 在多大的範圍內可見(有效或可以被訪問到)?(作用域,如auto和文件域)

我們的程序語言都需要考慮大型程序的需要,例如幾萬行代碼,語言多個文件並組織成一個項目。並且在文件內部還需要通過函數或類的對象組織成模塊,既是爲了避免代碼重複,也是爲了提高代碼的健壯性(減少了耦合度)、可維護性、可讀性。文件除了資源文件以外,一般還區分.h頭文件和.cpp實現文件,用.h頭文件來實現數據、函數、類的聲明,在.cpp文件中來完成其實現或定義,且通過預處理指令#include來實現關連。因爲多個文件的存儲,所以上述的範圍有文件內的塊({}之間)範圍,有塊以外的全局的範圍,以及文件之間的範圍(在程序語言中稱之爲文件域)。

4 需要存儲多久?(存儲期,如static)

5 是編譯時分配分配內存空間還是程序運行時分配空間?(如malloc\free、new\delete)

內存對應的地址如果用一個變量存儲起來,就是一個指針,這種語法機制可以讓程序運行時動態地在內存的堆中申請內存空間來保存和處理數據。

6 怎樣命名這個空間(命名規則和命名空間)?

由基本數據類型複合而來的結構數據類型,以及函數都具有與上述相同的語法機制,都是通過標識符(變量名、常量名、數組名、結構體名、對象名、函數名)來尋址。

上述的語法機制的實現,由操作系統給運行的程序分配五塊空間來實現,分別是代碼區、堆區、棧區、全局和靜態變量區、常量區。

考慮了上述的問題之後,還有三個問題需要考慮:

1 合適的分配內存的時間;(聲明和定義)

2 合適的首次賦值的時間;(初始化)

3 如何合理地組織多文件?包括頭文件.h或.c、.cpp源文件。(聲明、定義、實現、初始化並如何合理佈局到各文件中)

數據類型是所有程序語言的基礎。C++程序的所有功能都是建立在內置於C++語言的基本數據類型基礎之上的。數據類型可以告訴數據代表的意義以及程序可以對數據執行的哪些操作,它確定了數據和數據操作在程序中的意義。

 

爲因應大程序的需要,除了基本類型以外,我們還需要結構數據類型,如數組、結構體,或類等自定義類型。爲實現模塊化的需要,還需要函數這種語法機制。

所以,一個聲明通常包含如下幾個部分(但是並非都必不可少):存儲類型、基本類型、類型限定詞和最終聲明符(也可能包含初始化列表)。每個聲明不僅聲明一個新的標識符,同時也表明標識符是數組、指針、函數還是他們的任意組合。

當在VC這樣的開發工具上編寫完代碼,點擊編譯按鈕準備生成exe文件時,VC其實做了兩步工作,第一步,將每個.cpp(.c)和相應.h文件編譯成obj文件;第二步,將工程中所有的obj文件進行LINK生成最終的.exe文件,那麼錯誤就有可能在兩個地方產生,一個是編譯時的錯誤,這個主要是語法錯誤,另一個是連接錯誤,主要是重複定義變量等。我們所說的編譯單元就是指在編譯階段生成的每個obj文件,一個obj文件就是一個編譯單元,也就是說一個cpp(.c)和它相應的.h文件共同組成了一個編譯單元,一個工程由很多個編譯單元組成,每個obj文件裏包含了變量存儲的相對地址等。

一、 爲什麼要區分聲明和定義(實現)?

在程序語言中,一般來說,有時並不需要在聲明時就馬上分配內存空間,而在真正需要的時候才分配內存空間,例如函數的形參、類的聲明。另外,函數的聲明還可以方便編譯器檢查函數的一致性。所以,在程序語言中,一般來說:

聲明是其類型的聲明,用於聲明其作用域、文件域、存續期、類型、標識符名稱,特別地,與類型在一起的語句就是聲明;聲明並不直接給成員變量分配內存,這只是告訴編譯器,這個聲明的類是什麼樣的?包含哪些數據以及能做什麼?編譯器根據類聲明的成員變量,就知道這個類的對象所需要的內存空間。

 

而對於自定義類型,例如結構體和類來說,在類型的聲明和定義(實現)時都不會分配內存空間,只有在確定好類型之後,用這個類型去定義變量和對象時,纔會分配內存空間。

對於函數來說,函數的聲明、定義(實現)還只是數據處理的一種方案,真正的實施是在函數調用時,也就是說,只在函數調用時才考慮是否爲其形參賦值或爲其局部變量分配空間。

聲明用於向程序表明變量的類型和名稱。所以類型和函數的定義也是一種聲明。

對於全局變量,使用extern關鍵字聲明變量名但是不定義它,如:

extern int a; // 聲明但是未定義a,在此文件的此處之前,未有int a的定義,定義在其後或其它文件中int a; // 聲明和定義同時進行

extern聲明不是定義,也不會分配存儲空間。它只是說明變量定義在程序的其他地方(聲明處的後面或其它文件中)。含有初始化的extern聲明被當做是定義,程序中變量可以用extern聲明多次,但只能定義一次。(對於靜態局部變量而言,其空間限制在語句塊中。對於非靜態局部變量而言,一是其空間和時間都限制在語句塊中,都無需多次聲明使用)

在一個程序中,變量有且只有一個定義(是說只分配空間一次,使用時作爲右值是取值操作,作爲左值是值更新操作)。

對於函數,其作用域是全局的,提供聲明的extern可以省略:

extern int f(int a, int b);

而函數的定義只是提供函數體的聲明。

把全局聲明放在頭文件中絕對是個好主意,當需要在多個源文件中共享變量或函數時,需要確保定義和聲明的一致性。最好的安排就是在某個相關的.c文件中定義,然後在頭.h文件中進行外部聲明,以便編譯器檢查定義和聲明的一致性。

C++程序通常由許多文件組成。爲了讓多個文件訪問相同的變量,C++區分了聲明和定義。定義和聲明有相同的時候,但聲明的主要目的是表明變量的類型和名稱,而定義的主要目的是爲變量分配存儲空間。

默認的聲明:塊中變量默認爲auto(auto自動變量是較新的C++版本新增的數據類型,聲明變量時,用auto代替變量類型,由編譯器根據初始字面值來判斷變量類型。),函數默認爲extern的文件域。

對於類來說,成員聲明還包括聲明其訪問的權限,如private、public、protected(在繼承關係中),friend(對訪問權限的突破)等。

二、用頭文件和源文件實現聲明和定義的分工

前面是考慮類型聲明的問題,這裏是考慮在頭文件在進行聲明,在源文件中實現聲明的分工問題。

理解多處聲明,一處定義(內存空間分配一次,作爲右值的值引用或值更新可以多次,函數的實現只能有一次,原型聲明可以是多次):

局部變量:聲明和定義在調用時(main函數調用,或main函數調用其他函數時)同時進行(同時分配內存);

全局變量:一處定義,多次(多處)extern聲明(聲明並不分配內存,定義時分配內存);

函數(涉及到形參、實參)的聲明、定義、調用(調用時分配內存);

結構體(涉及到成員屬性)的聲明、實例化(實例化時分配內存);

(涉及到成員變量和成員函數)的聲明、成員函數的定義、實例化(實例化時分配內存);

頭文件.h提供聲明信息,包括函數、結構體、類等的聲明以及宏定義和全局變量的extern聲明。源文件.c或.cpp提供函數和類方法實現信息以及全局變量的定義(聲明類型和初始化值),這樣的程序的邏輯結構更清晰,源文件包含頭文件的引用以及相關函數或類方法的實現。

extern int i; //聲明外部整形變量,一般建議放到頭文件中(.h文件)

char * f(); //聲明函數,一般建議放到頭文件中(.h文件)

變通但不推薦的做法是:如果想使用另一個文件中的全局變量定義int i,你可以不包含頭文件,而直接寫extern int i;。推薦的做法是在頭文件中寫extern int i;,而在需要的源文件中去包含這個頭文件。

調用庫時也時要用頭文件去包含其聲明,由其聲明鏈接其具體實現。

下面的語句屬於條件編譯語句,意思是如果沒有 define FUN_H 就 define FUN_H ,如果之前 define 過,#ifndef 到 #endif 的代碼段就不參與編譯了,這樣可以避免 #ifndef 到 #endif 的代碼段被重複包含。FUN_H 當然也可以取其他名字,只需要確保唯一性就可以了。

#ifndef _FUN_H_
#define _FUN_H_
//...
#endif

爲什麼不直接包含 .c 文件呢? 在 main.c 文件裏直接 #include“fun.c”不更方便嗎?當然,這樣編譯也能通過,可是以後要是又有一個模塊需要用到 fun.c 中定義的函數呢?再包含一次 fun.c ?這樣不就相當於一個函數有多處定義了嗎?這樣在程序鏈接階段就會有麻煩,或者根本無法生成可執行程序。如果包含的是頭文件,那無論包含多少次(聲明瞭多次),函數也只有一處定義,鏈接是不會有問題的了。(用頭文件多次聲明(include包含),一次定義(在源文件中))

作爲一般規則,儘量不要把實際的代碼(如函數體)或全局變量定義(即定義和初始化實例)放入頭文件中,而應該把下面所列的內容放入頭文件.h中:

1 宏定義(預處理#define);

2 結構、聯合、枚舉、類的聲明;

3 typedef聲明;

4 外部函數聲明(隱式地有包含關鍵字extern);

5 全局變量聲明(顯式地有包含關鍵字extern);

(只在頭文件中聲明外部鏈接的元素)

需要全局或自定義類型聲明的源文件用預處理命令#include包含該頭文件,相當於把其內容複製到了文件的頭部,形成全局聲明或自定義類型的聲明,這樣的頭文件能夠被多個源文件包含,但函數、類方法的具體實現卻在源文件中,這樣形成的格局就是“多次聲明,一次定義(實現)”。

爲什麼要分爲很多頭文件?如果所有的庫寫到一個庫文件中,你寫的程序會很大,所以頭文件只是把一些相似功能的函數寫到一個頭文件中,這樣引用時則數據比較少。

三、 初始化就是給給變量或常量首次賦值

通常僅聲明的變量是沒有初始值的(有一個歷史值或垃圾值的隨機值,當然也有可能是0值或NULL值),給變量填入數據最直接的辦法就是使用賦值運算符將值賦予變量。賦值運算符將值與變量綁定起來,也就是說,值寫入變量所引用的內存單元。在給變量、常量、指針、數組賦真正需要的實際值之前,爲了安全的考慮,需要先給其賦一個0值或NULL值,或一個確定的值。

變量的聲明當然也可包含對變量的初始化,但是不賦顯式的初始值的時候,某種特定的缺省初始化也會進行。

除了直接賦值初始化以外,也可以使用初始化函數將某一塊內存中的內容全部設置爲指定的值(menset()函數通常爲新申請的內存做初始化工作):

void *memset(void *s, int ch, size_t n);
//結構體和數組可以使用={0}來初始化零值

編譯器會自動給全局變量和靜態變量初始化一個0值或NULL。

C\C++的編譯對於局部變量並未自動初始化零值,可以是出於效率的考慮,因爲賦值也是需要運行時間的,特別是大批量賦值時,因爲早期的計算機的速度相對來說是較慢的。

編譯器強制要求常量在的聲明、定義、初始化同時進行。(因爲常量只有一次賦值或更新值的機會。)

對於函數中包含的數據來說,函數調用做了兩件事情:用對應的實參初始化函數的形參(創建變量並賦值),並將控制權轉移給被調用函數。主調函數的執行被掛起,被調函數開始執行。函數的運行以形參的(隱式)定義和初始化開始。

函數被調用時,系統爲每個形參分配內存單元,也就是相當於一個局部變量的聲明後的初始化。

另外,函數調用只能出現在自動變量(即局部非靜態變量)的初始式中。

函數指針的初始化:

extern int func();
int (*fp)() = func; // 當一個函數名出現在這樣的表達式中,它就會“退化”爲一個指針
// 即隱式地取出了地址值,有點類似數組名的行爲

C++的引用必須指向一個對象,C++要求引用必須初始化,並且沒有NULL引用這種概念。

可以像其他數組那樣聲明並初始化字符串:

char carr[] = {'w','w','u','h','n','\0'};

也可以用字面量初始化字符串的簡捷方式:

char carr[] = "wwuhn";

當然也可以使用指針的方式:

char *cp = "wwuhn";

結構體也支持聲明時定義並同時初始化的集合賦值操作,聲明以後不再支持集合賦值的操作。

定義二維數組時,若按一維格式初始化,則第一維的長度可以省略,此時,系統可根據初始化列表中值的個數及第二維的長度計算出省略的第一維長度,但無論如何,第二維的長度不能省略。沒有初始化時,第一維和第二維的長度都不能省略。

類的靜態成員必須在類中聲明,在類外初始化(或定義)。

對於類來說,一般用構造函數去初始化對象。

在類中,純虛函數使用純指示符(=0)聲明,代碼如下:

virtual CalArea() = 0;

類的構造函數的形參指定了創建類類型對象時使用的初始化式。通常,這些初始化式會用於初始化新創建對象的數據成員。構造函數通常應確保其每個數據成員都完成了初始化。

四、作用域、存儲期、文件域

複合語句,通常被稱爲塊,是用一對花括號括起來的語句序列(也可能是空的)。塊標識了一個作用域,在塊中引入的名字只能在該塊內部或嵌套在塊中的子塊裏訪問。通常,一個名字只從其定義處到該塊的結尾這段範圍內可見。

對於在控制語句中定義的變量,限制其作用域的一個好處是,這些變量名可以重複使用而不必擔心它們的當前值在每一次使用時是否正確。對於作用域外的變量,是不可能用到其在作用域內的殘留值的。

而全局作用域可以用來共享數據,但往往也就有了安全的隱患。

作用域也可以理解爲一種上下文。

在C++中,每個變量名都與唯一的實體(例如變量,函數和類型等)相關聯。儘管有這樣的要求,還是可以在程序中多次使用同一個變量名,只要它用在不同的區域中,且通過這些區域可以區分該變量名的不同意義。用來區分變量名的不同意義的區域稱爲作用域。大多數作用域是用花括號來劃定界限的。

存儲類型是從變量的存在時間(即生存期)來劃分變量。變量的存儲類型可分爲靜態存儲方式和動態存儲方式。對於動態存儲變量,當程序運行到該變量處時才爲其分配存儲空間,當程序運行到該變量所在作用域的結束處時自動收回爲其分配的存儲空間,因此它的生存期爲所在作用域。在程序開始執行時就爲其分配存儲空間,直到程序結束時,才收回變量的存儲空間,這種變量稱爲靜態存儲空間,其生命週期爲整個程序執行的過程。

在C++中,變量的存儲類型有自動類型、寄存器類型、靜態類型、外部類型等4種。

1 自動類型變量(auto)

自動類型只能是局部類型的變量,屬於動態存儲類型。

2 靜態類型變量(static)

static,即在程序運行的過程中靜態變量始終是佔用一個存儲空間。靜態變量只能在他的作用範圍內使用,使用局部靜態變量是爲了在下次調用該函數時,能使用上次調用後得到的該變量的值。

3 寄存器類型變量(register)

屬於動態存儲類型,編譯器不爲寄存器類型的變量分配內存空間,而是直接使用CPU的寄存器。以便提高對這類變量的存取速度。主要用於控制循環次數等不需要長期保存值得變量。

4 外部類型變量(extern)

外部類型變量必須是全局變量,在C++中,有兩種情況需要使用外部類型變量。一種是在同一源程序文件中,當在全局的定義之前使用該變量時,在使用前要對該變量進行外部類型變量聲明。另一種是當程序有多個文件組成時,若在一個源文件中要引用在另一個源文件中定義的全局變量,則在引用前必須對所引用的變量進行外部聲明。

如果在某文件中定義的全局變量不想被其他文件所調用,則必須用關鍵字static將該變量聲明爲靜態全局變量,也就是說,靜態全局變量只能供所在的文件使用。此時關鍵字static不再是存儲類型,而是文件域的定義了,編譯器會在上下文中去理解。

五、數據類型

數據類型是程序語言的基本要素。爲什麼需要數據類型?分類是人類認識外界事物的很重要的手段,如植物學、動物學的門綱目科屬種的分類就是這兩個學科很重要的概念。不同類別的事物具有不同的特徵,具有不同的屬性和行爲方式。

在C/C++中,數據類型分爲兩種,簡單類型和結構類型。簡單類型包括有整數類型、字符類型、浮點類型、指針類型、枚舉類型和void類型等。結構類型包括有數組、字符串、記錄和文件等。C/C++的基本數據類型屬於簡單類型。用戶可以創建的所有數據類型都是根據基本類型定義的。

根據基本數據類型聲明的變量可以告訴編譯器以下信息:

1 需要的內存空間;

2 取值的範圍;

3 可以執行的操作(如可以使用什麼運算符,使用運算符的規則及達到的效果);

找到正確的數據表示不僅僅是選擇一種數據類型,還要考慮必須進行哪些操作。也就是說,必須確定如何儲存數據,並且爲數據類型定義有效的操作。例如,C實現通常把int類型和指針類型都儲存爲整數,但是這兩種類型的有效操作不相同。例如,兩個整數可以相乘,但是兩個指針不相同;可以用*運算符解引用指針,但是對整數這樣做豪無意義。C語言爲它的基本類型都定義了有效的操作。但是,當你要設計數據表示的方案時,你可能需要自己定義有效操作。在C語言中,可以把所需的操作設計成C函數來表示。簡而言之,設計一種數據類型包括設計如何儲存該數據類型和設計一系管理該數據的函數。還要注意的是,C並未很好地實現整數。例如,整數是無窮大的數,但是2字節的int類型只能表示65536個整數。因此,不要混淆抽象概念和具體的實現。

爲了因應複雜問題和大程序的需要,只有基本類型是不夠的,需要通過複合去組合基本類型去形成複合(結構)數據的自定義類型。例如一年的12個月的天數,一輛車的品牌、重量、速度,同時,一輛車不只是有屬性,還有它的行爲方式,如開動、行駛、停車、倒車等。程序語言就是對世界的模擬與表示,這些概念形成程序語言的語法就是數組、結構體、類、及其相互複合。

複合(結構)數據不像單個的基本數據類型(原子元素),因爲其是多個元素組成的,用一個統一的標識符來表示這個集合體,自然就需要考慮其元素的相互關係,例如你求出10個城市之間的最短路徑,自然就要考慮這10個城市的交通網絡。

 

一個數據集合中元素的關係稱爲邏輯關係,邏輯關係有一對一的線性關係、二對多的樹型關係,多對多的圖形關係,還有隻屬於於同一個集合,除此以外無其他關係的集合關係。

除了邏輯關係以外,還要考慮如何存儲?我們知道,內存是可以隨機訪問的,是因爲其內存單元都有一個地址對應,內存單元之間是線性的邏輯關係。如果按一個整體分配到一個連續的內存區域中,稱爲順序存儲,這種順序存儲可以映射元素的線性邏輯關係。只要知道了第一個元素的位置,就可以順藤摸瓜,找到其它元素。但順序存儲有其優勢,也有其弊端,大塊的數據往往有可能存儲失敗。同時元素的增、刪會影響到其它元素的位置,對於需要大量這種操作的結構類型不利。解決的方法就是另外一個儲存方式,對每一個元素除了其元素本身的數據以外,再增加一個數據域,用來表示其相鄰元素的地址,這樣,與順序存儲相同,只要知道了第一個元素的位置,也可以按其存儲的地址域,可以依次訪問到各自的相鄰元素,這就是鏈式存儲。這時元素稱爲一個節點,可以用一個結構體來表示,這個節點體中有兩個數據域,一個數據域代表元素本身,一個就是地址域。

 

對於樹型關係和圖形關係,與鏈式存儲的思路一樣,邏輯關係也可通過增加數據域去表示,如用一個鄰接表去表示一種圖形關係(樹型是圖形的特例)。當然,簡單直接的方法就是用一個二維表去表示。

如同基本數據類型要考慮其值域及可以使用的操作(操作符)一樣,複合(結構)數據類型也要考慮其數據結構,和可以定義的操作(如元素的增、查、刪、改、遍歷、排序、查找等),這就是數據結構的概念。排序、查找這些操作可以考慮不同的方法,如分治法、窮舉法等,在程序中就是算法的概念。特定的操作可以形成函數,可以固定下來,形成函數庫或類庫。

複合(結構)數據不僅是表示複雜問題的需要,同時也提高了編程的顆粒度,就如同生產一個產品,不能都是從零開始生產每一個部件,而是可能組裝。

數據類型是一組性質相同的值集合以及定義在這個值集合上的一組操作的總稱。數據類型定義了兩個集合,即該類型的取值範圍以及該類型中可允許使用的一組運算。例如,高級語言中的數據類型就是已經實現的數據結構的實例。從這個意義上講,數據類型是高級語言中允許的變量種類,是程序設計語言中已經實現的數據結構(即程序中允許出現的數據形式)。

ADT(Abstract Data Type)定義了一個數據對象、數據對象中各元素間的結構關係以及一組處理數據的操作。ADT通常是指由用戶定義且用以表示應用問題的數據模型,由基本的數據類型組成,幷包括一組相關服務操作。

ADT{
 數據對象:<數據對象的定義>
 結構關係:<結構關係的定義>
 基本操作:<基本操作的定義>
} ADT

如圖這種數據結構,就包括其元素、元素關係及關於圖的操作。

#define宏定義只是一個簡單的替換,編譯器並不會做類型檢查。

靜態語言:與動態語言相比較,在寫程序時,所有變量必須聲明其數據類型。例如Java就是靜態語言,其聲明變量時,必須給定數據類型,並初始化。強類型定義語言:強制數據類型定義的語言。也就是說,一旦一個變量被指定了某個數據類型,如果不經過強制轉換,那麼它就永遠是這個數據類型了。強類型定義語言帶來的嚴謹性能夠有效的避免許多錯誤弱類型定義語言:數據類型可以被忽略的語言。它與強類型定義語言相反, 一個變量可以賦不同數據類型的值。

六、 變量和常量

常量是不可以改變值的量,變量是可以改變值的量,常量在定義時必須初始化,變量可以在定義時不初始化。常量不可以尋址,它的地址不允許賦給非常量指針,變量可以尋址。常量有相對較高的編譯執行效率。

變量,顧名思義就是在程序運行中,可以被改變的量。變量在定義後爲程序提供了一個有名字的內存區域,編程者可以通過程序對它進行讀寫和處理。變量值的改變是通過賦值操作進行的。變量的基本使用示例代碼如下:

double pi = 3.14; //定義double型變量pi
pi = 3.1415;
pi = 3.1415926; //可以通過賦值不斷改變pi的值 scanf( "%lf", &pi );

常量是對數據的一種保護方式,也是一種統一字面量值的一種方式。

常量類型:

const聲明的常量;

constxpr聲明的常量表達式;

enum聲明的枚舉常量;

#define定義的常量(已摒棄,不推薦);

字面常量;

對於下面的賦值操作,雖然可以正常編譯,但是賦值語句卻並不起作用,因爲“test”是常量,是不能再被賦值的,編譯器會自動把它定義爲常量指針。

char *p = “test”;
*p = ‘p’;

七、 複合聲明

基本數據類型、結構數據類型、指針、函數、數組可以相互組合,所以,程序中的聲明、定義、初始化除了基本數據類型的聲明、定義、初始化以外,還有上述概念的相互組合的複合聲明、定義和初始化。

下面到底哪個是數組指針,哪個是指針數組呢:

int *p1[10];
int (*p2)[10];

“[]”的優先級比“*”要高。p1 先與“[]”結合,構成一個數組的定義,數組名爲p1,int *修飾的是數組的內容,即數組的每個元素。那現在我們清楚,這是一個數組,其包含10 個指向int 類型數據的指針,即指針數組。至於p2 就更好理解了,在這裏“()”的優先級比“[]”高,“*”號和p2 構成一個指針的定義,指針變量名爲p2,int 修飾的是數組的內容,即數組的每個元素。數組在這裏並沒有名字,是個匿名數組。那現在我們清楚p2 是一個指針,它指向一個包含10 個int 類型數據的數組,即數組指針。

在複合聲明中,相對於數據運算符和函數運算符,指針聲明運算符*的優先級是最低的。

同時,()既是函數運算符(通常位於複合聲明的最後),()也是聲明最高優先級的運算符。

如果沒有用()來改變優先級,你就可以按從左至右的順序去理解,核心(最終類型)落在最後(前面的指針符號相當於是修飾):

int *ap[] 指針數組,元素是指針的數組;

int *fp() 指針函數,返回指針的函數;

如果有用()來改變優先級,,核心(最終類型)落在最後的聲明最高優先級()內的內容(其它符號相當於是修飾):

int (*ap)[] 數組指針,指向一個數組的指針;

int (*fp)() 函數指針,指向一個函數的指針;

例如,下面聲明可以按上面的規則去理解:

char * (*pfpc)();

把前面括號的內容放到最後去理解,剩下的從左至右:

指針函數指針,是一個指針,一個指向指針函數的指針,指針指向的函數返回一個指針。

注意:運算符*在C語句中是有重載的,在不同的上下文中,有不同的功能。當左右是數字時,是乘法運算符,當與int、float、char等數據類型在一起聲明覆合數據類型時,它是用於指針聲明的,當在上述兩種情況以外時,則是做爲指針解引用而存在的。同樣的,()也有不同的上下文。

typedef爲C語言的關鍵字,typedef主要是爲複雜的聲明定義簡單的別名。。這裏的數據類型包括內部數據類型(int,char等)和自定義的數據類型(struct等)。

八、static和extern

static在C++中有4個作用,

1 聲明局部變量爲靜態的存續期

函數體內static變量的作用範圍爲該函數體,不同於auto變量,該變量的內存只被分配一次,因此其值在下次調用時仍維持上次的值。

2 聲明全局變量爲靜態的文件域

聲明瞭static的局部變量的值可以持續到程序結束,是時間的概念;

聲明瞭static的全部變量可以訪問的空間限制在文件以內,是空間的概念。當然不影響其時間的存續期。與其相對的是關鍵字extern,表示在要使用此位置前未有定義,要使用別外定義的全局變量,所以,這裏的關鍵字static可以理解爲internal。

在模塊內的static全局變量可以被模塊內所用函數訪問,但不能被模塊外其他函數訪問。

3 聲明函數爲靜態的文件域

在模塊內的static函數只可以被這一模塊內的其他函數調用。這個函數的使用範圍被限制在聲明它的模塊內。

4 在類中聲明靜態成員

4.1 靜態成員變量

1)在編譯階段就分配空間,對象還沒有創建。

2)必須在類中聲明,在類外初始化(或定義)。

3)歸同一個類的所有對象所有,共享同一個靜態變量。在爲對象分配的空間中不包含靜態成員所佔空間

4)有權限限制:private靜態變量和 public靜態變量。

在類中的static成員變量屬於整個類所擁有,對類的所有對象只有一份拷貝。

4.2 靜態成員函數

在類中的static成員函數屬於整個類所擁有,這個函數不接收this指針,因而只能訪問類的static成員變量。

一般可以考慮將需要有文件作用域的變量集中在一個頭文件中用extern去聲明,集中在一個cpp文件中去初始化定義,在需要這些文件域的變量的cpp文件中去引用這個頭文件。而static聲明和定義的全局變量,都把它放在原文件中而不是頭文件(因爲你此時聲明static的目的就是爲了不跨文本使用)。

 

extern當它與"C"一起連用時,如: extern "C" void fun(int a, int b); 則告訴編譯器在編譯fun這個函數名時按着C的規則去翻譯相應的函數名而不是C++的, C++的規則在翻譯這個函數名時會把fun這個名字變得面目全非,可能是fun@aBc_int_int#%$也可能是別的,這要看編譯器的"脾氣"了(不同的編譯器採用的方法不一樣),爲什麼這麼做呢,因爲C++支持函數的重載啊。

九、const

1 欲阻止一個變量被改變,可以使用const關鍵字。在定義該const變量時,通常需要對它進行初始化,因爲以後就沒有機會再去改變它了。

2 對指針來說,可以指定指針本身爲const;也可以指定指針所指的數據爲const(不能用指針去修改其指向的內存單元的值),或二者同時指定爲const。

const char *p; //指針指向一個不能用指針修改的值,可用其它方式去修改
char const *p; //指針本身是一個常量,不能修改爲指向其它內存單元
char *const p; //同上

3 在一個函數聲明中,const可以修飾形參,表明它是一個輸入參數,在函數內部不能改變其值。

4 對於類的成員函數,若指定其爲const類型,則表明其是一個常函數,不能修改類的成員變量。

5 對於類的成員函數,有時候必須指定其返回值爲const類型,以使得其返回值不爲“左值”。

當const單獨使用時它就與static相同,而當與extern一起合作的時候,它的特性就跟extern的一樣了!

十、程序員通常需要處理下述5個內存區域:

在C++中,內存分成5個區,他們分別是堆、棧、自由存儲區、全局/靜態存儲區和常量存儲區。下面分別來介紹:

棧區(stack), 由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。

 

堆區(heap),一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表。C語言一般使用malloc()函數和realloc()函數申請,而C++一般使用new運算符申請堆空間;

全局區(靜態區)(static),全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。程序結束後由系統釋放。

文字常量區,常量字符串就是放在這裏的,程序結束後由系統釋放 。

程序代碼區,存放函數體的二進制代碼。

除了內存空間以外,也可以將數據申請存儲到寄存器,以獲得更快的處理速度。

局部變量的侷限性是不會持久化,函數返回時,局部變量將丟棄。全局變量解決了這個問題,但代價是整個程序中都能訪問它,這導致代碼容易出現bug,難以理解和維護。將數據放在堆中可解決這兩個問題。可將堆視爲大塊內存,其中有數以千計的文件架等待您存儲數據,但您不能像棧那樣標識這些文件架。您必須詢問預留的文件架的地址,將其存儲到指針,然後丟掉。每當函數返回時,都會清理棧。此時,所有局部變量都不在作用域內,從而從棧中刪除。只有到程序結束後纔會清理堆,因此使用完預留的內存後,您需要負責將其釋放。讓不再需要的信息留在堆中稱爲內存泄露。堆的優點在於,在顯式釋放前,您預留的內存始終可用。如果在函數中預留堆聽內存,在函數返回後,該內存仍然可用。以指針訪問堆中內存(而不是全局變量)的優點是,只有有權訪問指針的函數才能訪問它指向的數據。這提供了控制嚴密的數據接口,消除了函數意外修改數據的問題。如果不能從堆中分配內存(因爲內存資源有限),將引發異常。異常是處理錯誤的對象。

十一、綜合與補充

C語言有4種作用域(標識符聲明的有效區域):函數、文件、塊、原型(函數原型聲明的形參)。

C語言有4種命名空間:

1 行標label,是goto的目的地;

2 標籤tag,結構、聯合和枚舉的名稱;

3 結構或聯合的成員;

4 普通標識符,函數、變量、類型定義名稱和枚舉常量;(C++有統一的命名空間std,當用#include命令包含庫時,需要聲明其命令空間,不然其引用的對象前面都要加上std::來宣稱其命名空間)

C語言的3種“連接類型”:

1 外部連接:全局、非靜態變量和函數,在所有的源文件中有效;

2 內部連接:文件作用域內的靜態函數和靜態變量;

3 無連接:局部變量及類型定義(typedef)名稱和枚舉常量;

所有變量的作用域都開始於變量的聲明處,換句話說,變量必須先聲明再使用。

聲明指針爲什麼需要聲明類型,原因之一就是當指針運算產生偏轉或讀取數據時,它知道按不同的類型需要讀取多少內存或偏轉多少位置?

函數包括函數聲明、函數定義、函數定義的話,如果需要修改函數時,最好不需要三個方面都要修改,只需要修改一個地方而保持另外兩部分的穩定是最好的。如果接口設計良好的話,則只需要更改函數定義即可。

函數的原型或聲明,是爲了在函數調用時方便編譯器的檢查。

函數模板是對函數中參數和返回值的一種泛化。

聲明一個函數模板的格式是:

template <<模板形參表聲明>><函數聲明>

類模板就是一系列相關類的模板或樣板,這些類的成員組成相同,成員函數的源代碼形式相同,所不同的只是針對的類型(數據成員的類型以及成員函數的參數和返回值的類型)。對應類模板,數據類型本身成了參數,因而是一種參數化類型的類,是類的生成器。類模板中聲明的類稱爲模板類。

聲明一個模板類的格式是:

template <<模板形參表聲明>><類聲明>

聲明變量並不是多此一舉,因爲當你在某處使用這個變量的某一個字母寫錯時,會編譯報錯?如果沒有變量聲明機制,這樣的錯誤發現不了,可能會出現意料之外的錯誤。

malloc 只管分配內存,並不能對所得的內存進行初始化,所以得到的一片新內存中,其值將是隨機的。使用malloc函數需要指定內存分配的字節數並且不能初始化對象,New會自動調用對象的構造函數。delete會調用對象的destructor,而free不會調用對象的destructor。

C語言標準庫中有一個語言初始化函數memset()。作用是將某一塊內存中的內容全部設置爲指定的值, 這個函數通常爲新申請的內存做初始化工作。

for循環特別適用於需要初始化和更新的循環。使用逗號運算符可以在for循環中初始化和更新多個變量。

數組可以初始化,但不能整體賦值,如

char a[] = "hello";

但不能:

char a[11];
a = "hello";

可以這樣操作:

strcpy(a,"hello");

函數可以看作是由程序員來定義的操作,是劃分程序的各個程序塊,與內置操作符相同的是,每個函數都會實現一系列的計算,然後(大多數時候)生成一個計算結果。但與操作符不同的是,函數有自己的函數名,而且操作數沒有數量限制。與操作符一樣,函數可以重載,這意味着同樣的函數名可以對應多個不同的函數。

在類的定義外面定義成員函數必須指明它們是類的成員:

double Sales_item::avg_price() const

計算機科學領域已開發了一種定義新類型的好方法,用4個步驟完成從抽象到具體的過程。

1 建立抽象:提供類型屬性和相關操作的抽象描述。

2 建立接口:開發一個實現ADT的編程接口。也就是說,指明如何儲存數據和執行所需操作的函數。例如在C中,可以提供結構定義和操控該結構的函數原型。這些作用於用戶定義類型的函數相當於作用於C基本類型的內置運算符。需要使用該新類型的程序員可以使用這個接口進行編程。

3 實現接口:編寫代碼實現接口。這一步至關重要,但是使用該新類型的程序員無需瞭解具體的實現細節。

4 使用接口。

上面的抽象可以是函數、結構體或類的抽象,上面的接口可以是函數原型,也可以是類的方法聲明

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章