2010-10-18 wcdj
本文接:Dissection C Chapter 1_2
http://blog.csdn.net/delphiwcdj/archive/2010/10/06/5924226.aspx
[1] C語言有多少個關鍵字?sizeof怎麼用?它是函數嗎?
[2] 什麼是定義?什麼是聲明?它們有何區別?
[3] 關鍵字角色的逐個分析
(1) auto
(2) register
(3) static
(4) 基礎資料型別 (Elementary Data Type)
(5) sizeof
(6) signed, unsigned
(7) if, else
(8) switch, case
(9) do, while, for
(10) goto
(11) void
(12) return
(13) const
(14) volatile
(15) extern
(16) struct
(17) union
(18) enum
(19) typedef
(13) const
const是constant的縮寫,是恒定不變的意思,也翻譯為常量、常數等。
【注意】
【1】 很多人都認為被const修飾的值是常量。這是不精確的,精確地說應該是:唯讀變數,其值在編譯時間不能被使用,因為編譯器在編譯時間不知道其儲存的內容。
#include <cstdio><br />int main()<br />{<br />const int MAX=100;<br />int a[MAX]={0};<br />return 0;<br />}<br />
想:這樣用正確嗎?MAX是唯讀變數,其值在編譯時間不能被使用。(C中不正確,C++中可以)
【2】 const推出的目的,正是為了取代先行編譯指令,消除它的缺點,同時繼承它的優點。注意,define不是關鍵字。
PS: 可以參考《Essential C++》其中一條款的詳細介紹。
【3】 const修飾的唯讀變數必須在定義的同時初始化。
【4】 case語句後面可以使用const修飾的唯讀變數嗎?(當然可以)
#include <cstdio><br />int main()<br />{<br />const int I_AM_CONST=100;<br />switch (I_AM_CONST)<br />{<br />case 1:<br />break;<br />case 2:<br />break;<br />case 100:<br />printf("ok/n");<br />break;<br />default:<br />NULL;<br />}<br />return 0;<br />}
【5】節省空間的,避免不必要的記憶體配置,同時提高效率。
編譯器通常不為普通(全域的?)const唯讀變數分配儲存空間,而是將它們儲存在符號表中,這使得它成為一個編譯期間的值,沒有了儲存與讀記憶體的操作,使得它的效率也很高。
const定義的唯讀變數從彙編的角度來看,只是給出了對應的記憶體位址,而不是像#define一樣給出的是立即數,所以,const定義的唯讀變數在程式運行過程中只有一份拷貝(因為它是全域的唯讀變數,存放在靜態區),而#define定義的宏常量在記憶體中有若干個拷貝。#define宏是在先行編譯階段進行替換,而const修飾的唯讀變數是在編譯的時候確定其值。#define宏沒有類型,而const修飾的唯讀變數具有特定的類型。
為了驗證上述的論述,我做了一下的測試:
#include <cstdio><br />#define M 3// 宏常量<br />const int N=100;// 此時並未將N放入記憶體中<br />int g_val=1;<br />int main()<br />{<br />0042D600 push ebp<br />0042D601 mov ebp,esp<br />0042D603 sub esp,0F0h<br />0042D609 push ebx<br />0042D60A push esi<br />0042D60B push edi<br />0042D60C lea edi,[ebp-0F0h]<br />0042D612 mov ecx,3Ch<br />0042D617 mov eax,0CCCCCCCCh<br />0042D61C rep stos dword ptr es:[edi]<br />//const int N=100;// 此時並未將N放入記憶體中<br />int a=N;// 此時為N分配記憶體,以後不再分配<br />0042D61E mov dword ptr [a],64h<br />int b=M;// 先行編譯期間進行宏替換,分配記憶體<br />0042D625 mov dword ptr [b],3<br />int c=N;// 沒有記憶體配置<br />0042D62C mov dword ptr [c],64h<br />int d=M;// 再進行宏替換,又一次分配記憶體<br />0042D633 mov dword ptr [d],3<br />return 0;<br />0042D63A xor eax,eax<br />}<br />0042D63C pop edi<br />0042D63D pop esi<br />0042D63E pop ebx<br />0042D63F mov esp,ebp<br />0042D641 pop ebp<br />0042D642 ret
我的疑問是
:(將const變數作為全域和局部)
當const變數是全域的時候,VS編譯器沒有為其分配儲存空間,即把當做立即數進行替換。
但是,當聲明 const變數為局部變數的時候,VS編譯器是為其分配了空間的。這樣的話,所提的const的這個“優點”是不是特指的全域變數的時候呢。
為討論這個問題發布的一個文章:
http://topic.csdn.net/u/20101013/16/bca742a9-b0c4-4d6b-b301-7d273db44c59.html
最後的結論是
:Debug 下,編譯器會儲存大量運行時不需要的資料,用於調試資訊。所以和真正的release版的彙編不同。關於這個問題還沒有完全解決,對於如何測試和驗證這個問題,現在還沒有找到一個好的方法。(如果您知道請您告訴我,Thx)
【6】修飾一般變數。
const int i=2;<br />// the above same as<br />int const i=2;
即,修飾符const可以用在類型說明符前,也可以用在類型說明符後。(習慣多用在類型說明符前,但建議用在類型說明符後,見4.2.5 section in C++ Primer)
【7】修飾數組。
const int a[]={1,2,3,4,5};<br />// same as<br />int const a[]={1,2,3,4,5};<br />
【8】修飾指標。
const int *p1;// p1可變,p1指向的對象不可變<br />int const *p2;// 同上<br />int ival=1;<br />int* const p3=&ival;// p3不可變且必須初始化,p3指向的對象可變<br />const int* const p4=&ival;// 指標p4和p4指向的對象都不可變,且p4必須初始化
記憶方法1
:const修飾符可以放在所修飾類型的前面或後面,建議放在後面。見4.2.5 section in C++ Primer
記憶方法2
:先忽略類型名(編譯器解析的時候也是忽略類型名),我們看const離哪個近,就是修飾它後面的那個類型。
const int
*p1;// const修飾*p1
int
const *p2;// 同上
int ival=1;
int
* const p3=&ival;// const修飾p3
const int
* const p4=&ival;// 第一個const修飾*p4,第二個const修飾p4
【9】修飾函數的參數。
const修飾符也可以修飾函數的參數,當不希望這個參數值被函數體內意外改變時使用。
例如:void fun(const int i);
告訴編譯器i在函數體中不能改變,從而防止了使用者的一些無意的或錯誤的修改。
【10】修飾函數的傳回值。
const修飾符還可以修飾函數的傳回值,傳回值不可被改變。
例如:const int fun();
【11】C++中對const進行了擴充,其特性可查相關資料。
(14) volatile
volatile是易變的、不穩定的意思。很多人根本沒有見過這個關鍵字,不知道它的存在,也有很多人知道它的存在,但從來沒有用過它。
volatile關鍵字和const一樣是一種類型修飾符,用它修飾的變數表示可以被某些編譯器未知的因素更改,比如作業系統、硬體或者其它線程等。遇到這個關鍵字聲明的變數,編譯器對訪問該變數的代碼不再進行最佳化,從而可以提供對特殊地址的穩定訪問。
例子:
int i=10;
int j=i;// (1)
int k=i;// (2)
這時候編譯器對代碼進行最佳化,因為在(1)、(2)兩條語句中,i沒有被用作左值。這個時候編譯器認為i的值沒有發生改變,所以在(1)語句時從記憶體中取出i的值賦給j之後,這個值並沒有被丟掉,而是在(2)語句時繼續用這個值給k賦值。編譯器不會重新從記憶體裡取i的值,這樣提高了效率。但要注意:(1)、(2)語句之間i沒有被用作左值才行。
另一個例子:
volatile int i=10;
int j=i;// (3)
int k=i;// (4)
volatile關鍵字告訴編譯器i是隨時可能發生變化的,每次使用它的時候必須從記憶體中取出i的值,因而編譯器產生的彙編代碼會重新從i的地址處讀取資料放在k中。這樣看來,如果i是一個寄存器變數或者表示一個連接埠資料或者是多個線程的共用資料,就容易出錯,所以說volatile可以保證對特殊地址的穩定訪問。
【注意】
在VC6中,一般debug模式沒有進行代碼最佳化,所以這個關鍵字的作用有可能看不出來。我們可以同時產生debug版和release版的程式做個測試。
【思考】請問下面代碼是否有問題,如果沒有,那i到底是什麼屬性?(i是const屬性)
const volatile int i=10;
i=20;
(15) extern
extern是外面、外來的意思。extern可以置於變數或者函數前,以標識變數或者函數的定義在別的檔案中,下面的代碼用到的這些變數或函數是外來的,不是本檔案定義的,提示編譯器遇到此變數和函數時在其他模組中尋找其定義。
【問題】extern和包含標頭檔有什麼關係呢?
(16) struct
struct是個神奇的關鍵字,它將一些相關聯的資料打包成一個整體,方便使用。
在網路通訊協定、通訊控制、嵌入式系統、驅動開發等地方,我們經常要傳送的不是簡單的位元組流(char型數組),而是多種資料群組合起來的一個整體,其表現形式是一個結構體。經驗不足的開發人員往往將所有需要傳送的內容順序儲存在char型數組中,通過指標位移的方法傳送網路報文等資訊(這種方法自己做過,現在看來確實很笨),這樣做編程複雜且易出錯。
這個時候,只需要一個結構體就能搞定。平時我們要求函數的參數盡量不多於4個,如果函數的參數多於4個使用起來非常容易出錯(包括每個參數的意義和順序),效率也會降低(與具體CPU有關,ARM晶片對於超過4個參數的處理就有講究,具體請參考相關資料)。這個時候,可以用結構體壓縮參數個數。(在MFC下超過4個參數的函數很多)
【問題】空結構體多大?
結構體所佔的記憶體大小是——其成員所佔記憶體之和(注意:關於結構體的記憶體對齊)。
#include <cstdio><br />struct student<br />{<br />};<br />int main()<br />{<br />student stu;<br />printf("%d/n",sizeof(stu));// 1<br />return 0;<br />}
在VS2008下,測試輸出為1。
【注意】struct與class的區別
在C++裡struct關鍵字與class關鍵字一般可以通用,只有一個很小的區別。(當然,在C++中習慣用class)
struct的成員預設情況下屬性是public的,而class成員卻是private的。
【測試】通過下面的代碼,說明:(1) struct的成員預設為public。(2) struct內可以定義函數。
#include <cstdio><br />typedef struct st_type<br />{<br />int i;<br />st_type()<br />{<br />j=1;<br />printf("this is constructor./n");<br />}<br />~st_type()<br />{<br />printf("this is destructor./n");<br />}<br />void fun_in()<br />{<br />printf("fun_in is in the struct./n");<br />}<br />void fun_out();<br />private:<br />int j;<br />}type_a;<br />void type_a::fun_out()<br />{<br />printf("fun_out is out the struct./n");<br />}<br />int main()<br />{<br />type_a st;<br />st.fun_in();<br />st.fun_out();<br />printf("%d/n",sizeof(type_a));<br />//printf("%d/n",st.j);// error, can not access the private member j<br />return 0;<br />}<br />/*<br />output:<br />this is constructor.<br />fun_in is in the struct.<br />fun_out is out the struct.<br />8<br />this is destructor.<br />*/
(17) union
union維護足夠的空間來放置多個資料成員中的“一種”,而不是為每一個資料成員配置空間,在union中所有的資料成員共用一個空間,同一時間只能儲存其中一個資料成員,所有的資料成員具有相同的起始地址。
【注意】
【1】一個union只配置一個足夠大的空間來容納最大長度的資料成員,以下面這個例子而言,最大長度是double類型,所以StateMachine的空間大小就是double資料類型的大小。(區別:結構體和類的大小和位元組對齊有關,見#pragma pack(n)的用法,VS下預設是按4B對齊)
例如:
#include <cstdio><br />union StateMachine<br />{<br />char character;<br />int number;<br />char *str;<br />double exp;// 8<br />};<br />int main()<br />{<br />printf("%d/n",sizeof(StateMachine));// 8<br />return 0;<br />}
【2】在C++裡,union的成員預設屬性頁為public。
【3】union主要用來壓縮空間,如果一些資料不可能同一時間同時被用到,則可以使用union。
【4】大小端模式對union類型資料的影響。
#include <cstdio><br />union<br />{<br />int i;<br />char a[2];<br />}*p, u;<br />int main()<br />{<br />p=&u;<br />p->a[0]=0x39;<br />p->a[1]=0x38;<br />printf("%x/n",p->i);// 3839 (hex.)<br />printf("%d/n",p->i);// 111000 00111001=14393 (decimal)<br />return 0;<br />}
分析如所示
:
高地址 低地址
—— —— —— —— int
0 | 0 | 56 | 57
—— —— —— ——
—— —— char
56 | 57
—— ——
這裡需要考慮儲存模式:大端模式和小端模式。
大端模式(Big-endian)
:資料的低位元組存放在高地址中。
小端模式(Little-endian)
:資料的低位元組
存放在低地址
中。
union型資料所佔的空間等於其最大的成員所佔的空間,對union型成員的存取都是相對於該聯合體基地址的位移量為0處開始,即,聯合體的訪問不論對哪個變數的存取都是從union的首地址位置開始。因此,上面程式輸出的結果就顯而易見了。
【5】如何用程式確認當前系統的儲存模式(大端還是小端)?
【問題】寫一個C函數,若處理器是Big-endian的,則返回0;若是Little-endian的,則返回1。
分析:根據大小端模式的定義,假設int類型變數i被初始化為1。
以大端模式儲存,其記憶體布局如:
高地址 低地址
—— —— —— —— int i=1;
0x1 | 0x0 | 0x0 | 0x0
—— —— —— ——
以小端模式儲存,其記憶體布局如:
高地址 低地址
—— —— —— —— int i=1;
0x0 | 0x0 | 0x0 | 0x1
—— —— —— ——
變數i佔4個位元組,但只有一個位元組的值為1,另外三個位元組的值都為0。如果取出低地址上的值為0,則是大端模式;反之為小端模式。
因此,可以利用union類型資料的特點:所有成員的起始地址一致。
方法1:利用union類型
#include <cstdio><br />int checkSystem()<br />{<br />union check<br />{<br />int i;<br />char ch;<br />}c;<br />c.i=1;<br />return (c.ch==1);<br />}<br />int main()<br />{<br />checkSystem()==1 ? printf("Little-endian/n") : printf("Big-endian/n");<br />return 0;<br />}
方法2:利用數群組類型
#include <cstdio><br />int checkSystem()<br />{<br />char s[]="1000";<br />return (s[0]=='1');<br />}<br />int main()<br />{<br />checkSystem()==1 ? printf("Little-endian/n") : printf("Big-endian/n");<br />return 0;<br />}
【思考】
在x86系統下(即32位系統下),輸出的值為多少?
#include <cstdio><br />int main()<br />{<br />int a[5]={1,2,3,4,5};<br />int *ptr1=(int*)(&a+1);// 0x0012ff68<br />int *ptr2=(int*)((int)a+1);// 0x0012ff55<br />int *ptr3=(int*)(a+1);// 0x0012ff58<br />printf("0x%x,0x%x,0x%x",ptr1[-1],*ptr2,*ptr3);// 0x5,0x2000000,0x2<br />return 0;<br />}<br />
分析如:
高地址 低地址
—— | —— —— —— —— | …… —— —— —— —— | —— —— —— ——
? | 0x0 | 0x0 | 0x0 | 0x05 | …… 0x0 | 0x0 | 0x0 | 0x02 | 0x0 | 0x0 | 0x0 | 0x01
—— | —— —— —— —— | …… —— —— —— —— | —— —— —— ——
^(ptr1) ^(ptr2)
因此,
ptr1[-1]=5(dec)=0x5(hex)
*ptr2=0x02 0x0 0x0 0x0=0x2 00 00 00(hex)=33554432(dec)
此題主要考查的是,指標不同位移量的計算。
int a[5]={1,2,3,4,5};// a==&a==0x0012ff54<br />int *ptr1=(int*)(&a+1);// [1] 0x0012ff68<br />int *ptr2=(int*)((int)a+1);// [2] 0x0012ff55<br />int *ptr3=(int*)(a+1);// [3] 0x0012ff58
注意區別
:
a==&a==&a[0]==0x0012ff54
但是,
a+1==&a[0]+1==0x0012ff58 此時的位移量為:一個int型的大小
&a+1==0x0012ff68 此時的位移量為:整個數組的大小
[1] (&a+1)等於數組的起始地址+數組的整個大小,即,此時為數組最後一個元素的後一個元素的地址。(int*)(&a+1)強制類型轉換後,表示指標的位移量為一個int型的大小。
[2] ((int)a+1)等於數組的起始地址+一個位元組的大小。
[3] (a+1)等於數組的起始地址+一個數組元素的大小。
(18) enum
很多初學者對枚舉類系感到迷惑,或者認為沒有用,其實enum是個很有用的資料類型。
【一般的使用方法】
enum enum_type_name<br />{<br />ENUM_CONST_1,<br />ENUM_CONST_2,<br />// ...<br />ENUM_CONST_n<br />}enum_variable_name;
enum_variable_name是enum_type_name類型的一個枚舉變數,它只能取花括弧內的(枚舉常量,一般用大寫)任何一個值。enum變數還可以給其中的常量符號賦值,如果不賦值則會從被賦值的那個常量開始依次加1,如果都不賦值,它們的值從0開始依次遞增1。
例如:
#include <cstdio><br />enum Color<br />{<br />GREEN=1,<br />RED,<br />BLUE,<br />GREEN_RED=10,<br />GREEN_BLUE<br />}ColorVal;<br />int main()<br />{<br />ColorVal=GREEN;<br />if (ColorVal==1)<br />printf("GREEN==1/n");<br />ColorVal=RED;<br />if (ColorVal==2)<br />printf("RED==2/n");<br />ColorVal=BLUE;<br />if (ColorVal==3)<br />printf("BLUE==3/n");<br />ColorVal=GREEN_RED;<br />if (ColorVal==10)<br />printf("GREEN_RED==10/n");<br />ColorVal=GREEN_BLUE;<br />if (ColorVal==11)<br />printf("GREEN_BLUE==11/n");<br />printf("%d/n",sizeof(ColorVal));// 4<br />return 0;<br />}
【enum與#define宏的區別】
[1] #define宏常量是在先行編譯階段進行簡單替換;枚舉常量則是在編譯的時候確定其值。
[2] 一般在編譯器裡,可以調試枚舉常量,但是不能調試宏常量。
[3] 枚舉可以一次定義大量相關的常量,而#define宏一次只能定義一個。
【問題】sizeof(ColorVal)的值為多少?為什嗎?(為4)
(19) typedef
用typedef關鍵字來建立各種——馬甲
typedef是用來給一個已經存在的資料類型取一個別名,而非定義一個新的資料類型。這樣做的目的是:用新的名字可以更好地表達其意思。
在實際項目中,為了方便,可能很多資料類型(尤其是結構體之類的自訂資料類型)需要我們重新取一個適用實際情況的,這時候typedef可以幫我們的忙。
typedef struct student<br />{<br />// code<br />}stu_st, *stu_pst;<br />struct student stu1;// same as below<br />stu_st stu1;<br />struct student *stu2;// same as below<br />stu_pst *stu2;
【注意一】把typedef與const放在一起的情況
const stu_pst stu3;// [1]<br />stu_pst const stu4;// [2]
方法是,我們看const修飾誰的時候,完全可以將資料類型名視而不見,當它不存在。因此,修飾的是指標,而不是stu3、stu4指向的對象。
【注意二】#define是在先行編譯的時候進行宏替換,因此是正確的;而typedef取的別名不支援類型擴充。
#define INT32 int<br />unsigned INT32 i=10;// ok<br />typedef int int32;<br />unsigned int32 j=10;// error<br />typedef unsigned int int32;// ok<br />typedef static int int32;// error<br />#define PCHAR char*<br />PCHAR p3, p4;// error, p3 is a pointer, however, p4 is not<br />typedef char* pchar;<br />pchar p1, p2;// ok, p1 and p2 are pointer
第一章關鍵字 (已總結完)