有了前面兩篇的基礎,下面正式開扒變量名和內存的關系,先看一段大家很熟悉的代碼:
int i; scanf_s("%d", &i);
int i;,在這一句就為i分配了內存(但尚未對這塊內存進行初始化),所以可以通過&i直接使用這塊內存。賦值就更不用說啦,i = 3;。
變量名i,是為方便編程人員使用,是這塊內存的別名,指代到塊內存,對編程人員i代表這塊內存中存儲的值(實際上是i指到這個內存,然后取值)。通常我們都是通過變量名來使用已知的內存的。
i代表取(這塊內存中存儲的)值,而&i代表取(這塊內存的)址。程序本身是不為i這個變量名分配空間的。在最終的機器代碼中,是不會出現變量名的,這一點在分析反匯編語言時可以看出(命令:dumpbin /disasm xx.obj >xx_disasm.asm可以查看反匯編語言)。那么編譯器是如何處理變量名的呢,變量名會存儲在符號表中,並將符號表中的索引對應到實際物理空間(地址)上去,當調用這個變量時,查找符號表就可以找到對應的地址並取值了。
上面分析的是基本數據類型(如int、char等)的變量名。C中除了變量名之外,還有函數名、常量名、指針名、數組名、結構名等。和變量名不同,這些標識符都是直接對應着地址的。基本數據類型的變量,和地址的對應關系需要取址符&才能得到地址,而其余的這些,名字本身就對應着地址。
例如char *pc = “se”;,就是將字符串常量”se”的首地址(位於常量存儲區)賦值給了字符指針pc。這也就解釋了為什么不需要為pc分配地址就可以為其賦值,而不會遇到類似下面代碼所帶來的野指針問題:
int *pi; *pi = 1;
int *pi句,是為pi分配空間,而不是開辟pi所指向的空間。
下面分別來看不同類型變量的變量名和內存見的關系:
先看C中的常量:
C對常量是怎么處理的呢?比如上面的i = 3;中的常量3,存儲常量3的地址並不是隨機分配的,是在程序中的數據段中(.data?這個我也還不是很確定,希望知道的前輩們給個指導),也就是程序本身並不為3分配內存,而是直接寫入指令。3是數字常量,對於字符常量和字符串常量,又分別是怎么處理的呢?
字符常量和數字常量是一樣的處理方式,都是類似匯編中的立即數,直接寫入指令;
而字符串常量,則是存儲在常量存儲區,可以使用&(“string”)取得具體地址。也就是字符串常量名字本身指代着地址,只是不能直接操作(和int i中的i相同)。
再看各種類型的變量名,c中的數據類型除常量之外大致有5種:
基本數據類型:int、float、double、char等:
對各基本數據類型的變量名及其聲明時的處理方式都是一樣的,聲明時即分配內存,並使用變量名直接操作這段內存;使用取地址符號&取得地址的數字表示,至於聲明時要不要做初始化,要看是不是全局變量或者 static變量了。
這類變量名指向一個地址空間,但不能直接當做地址使用,而是通過取址符&操作其地址。
構造數據類型:數組、結構、聯合:
1) 數組
數組在聲明時,即分配了空間:
int a[5];
一旦聲明a[5],相當於有a、a[0]、a[1]、a[2]、a[3]、a[4]這6個變量名。a[i]的指代方式和普通的變量名int i相同,指到一個對應的內存空間;關鍵是變量名a,本身就可以做地址用。我們知道a是數組名,但a並不代表整個數組的地址,而是指向數組首元素的地址(雖然在數值上是相同的,下面會有詳細解釋),所以可以有 int *p = a;。那么&a又怎么解釋呢?對於int i而言,i代表一個空間,&i表示i所代表的空間地址;那么&a應該也是表示a所代表的地址了,也就是整個數組的地址。
a、&a和&a[0]同代表地址,且由於數組是順序存儲,所以a、&a和&a[0]所表示的地址在數據上是相同的,但是實際的指代意義卻是不同的:
- a是個int*類型的數據,相當於&(*a),是指向數組首元素的地址;
- &a指代整個數組,是個int(*)[]類型的數據,是指針數組的地址;
- &a[0]則是僅指代一個存儲int的空間,是int*類型的數據。
也就是數組名,本身可以作為地址使用,指代該結構的首元素的地址。
2) 結構
結構在聲明的時候,就分配了空間。結構體和數組不同,結構體類型的變量名並不能直接當作地址使用,這一點和基本數據類型相同。需要對結構體名使用取址符&才能進行地址操作,並且取址所得到地址代表的是指向結構體的指針,只是在數據上和結構體中的首元素地址相同。
對於結構體中的各個元素,其名稱的指代關系和其數據類型相同,並不因為是結構體的元素而受到影響。具體見下面代碼:
struct stu{ int age; char sex; char* name; int score[5]; }; int main() { int i; struct stu st1; //st1是 結構體stu類型 printf("%d\n", &st1); //&st1是 stu*類型 printf("%d\n", &st1.age); //&st1.age是 int*類型,st1.age就是個int型,名字指向地址,但不能直接作地址
printf("%d\n", &st1.sex); //&st1.sex是 char*類型,名字解析同上 printf("%d\n", &st1.name); //&st1.name是 char**類型,st1.name是char*類型 printf("%d\n", st1.score); // st1.score是個數組類型,名字代表數組中首元素的地址 return 0; }
3) 聯合:聯合是特殊的結構體,為節省空間,在其各元素依次存儲,各元素的首地址均相對變量的基地址偏移為0,具體各變量名的分析和結構體同。
指針類型
聲明一個指針類型 int *p;,則是為存儲指針p分配空間,而並不會為p所指向的內存做任何動作,這就是野指針的原因。如下代碼,p就是一個未指向任何已知內存的指針,為*p賦值,自然會出現錯誤:
int *p; *p = 1;
指針中,char *是個很特殊的指針。一般的指針,僅指向一個所定義類型的內存,而char *則可以指向一個字符串,之所以可以實現這個功能是字符串結尾符’\0’的存在標識了字符串的結束。如下的代碼,就是將pc指向了“string”所指代的常量存儲區地址。
char *pc = “string”;
這也是char *pc = “string”合法,而int *p =1不合法的原因:"string"本身即代表了它的存儲地址,而整型常量1僅僅是個操作數,並不是地址,如果希望使用數據為指針(指向的地址)賦值,可以使用一個強制轉換 int*p = (int*)1,只是這樣如果不加以檢查的話,寫出來的代碼會存在安全隱患。因此,不管指針變量是全局的還是局部的、靜態的還是非靜態的,都應該在聲明它的同時進行初始化,要么賦予一個有效的地址,要么賦予NULL。
另外,聲明一個指針,只是在棧區為指針本身的存儲分配了地址,而不限制指針所指向的內存到底是在棧區、還是在堆區、還是在常量存儲區。這也就造成了 函數調用返回值 會因實現不同而有不同意義,是函數調用結束后返回值有效性不同的原因。詳見《從字符串截取說指針和地址》
空類型
C中有void關鍵字,但其實C中是並沒有空類型的。比如我們不能做如下定義:
void a;
因為C、C++是靜態類型的語言,定義變量就會分配內存。然而,不同類型的變量所占內存不同,如果定義一個任意類型的變量,就無法為其分配內存。所以,C、C++中沒有任意類型的變量。
但是定義void *p;是合法的,void *所定義的p表示以指針,所指向的類型未定。因為void *p;聲明是為指針p分配了空間,無論指針指向什么類型,存儲指針所需的空間的固定的,所以不存在因為所需空間大小而無法為p分配空間的問題。
但void *p的使用也是很受限制的,由於不知道其指向的數據類型,所以是不能對p進行自增操作的;void的主要作用有兩點,一個是限制函數的返回值,一個是限制函數的參數類型;void *則常用於指針的類型轉換。如下代碼:
int *pi; float *pf;
如果想將pi指向pf所指向的內存空間,就必須進行類型轉換:pi = (int *)pf;。
而如果是將pi換成void *p,就不需要轉換,可以直接為指針賦值。這樣的直接賦值,只能是將一個已知類型的指針賦值給void *p,而不能是將void *p未加強制轉換地賦值給一個已知類型的指針,如:
void *p; int *pi; float *pf; p = pf; // pf = p;就是非法的,不能將 "void *" 類型的值分配到 "float *" 類型的實體 p = pi;
但需要注意的是,即使進行了轉換,p仍然是個void*類型的指針,不能對其進行sizeof(p)等涉及所指類型的操作,同樣地p也不能直接用於具體數據類型的操作。如下面的代碼中*p = 1.73; 和printf("%f", *p)都是非法的:
void *p; float *pf; p = pf;
*p = 1.73; //*pf = 1.73;合法 printf("%f", *p); //printf("%f", *pf); 合法
這樣說來,void *的意義何在呢?可以使用強制類型轉換使用void *p作為中介,見下面的代碼:
float *pf; void *p; float f=1.6; p = (void*)&f; pf = (float*)p;
這樣,float *pf就指向了float f所在的地址,但注意p依然不能直接使用。這個例子,只是為我們展示了void *有這樣的功能,但平常代碼中很少這樣無意義地轉換,更多地是將void *作為函數參數,這樣就可以接受任意類型的指針了,典型的如內存操作函數memcpy和memset的函數,其原型分別為:
void * memcpy(void *dest, const void *src, size_t len); void * memset ( void * buffer, int c, size_t num );
也可以編寫自己的將void *作為函數參數的函數,由於char是C中最小長度的變量,其它任何變量的長度都是它的整數倍。可以使用char*作為中轉,詳見下面的函數實現:
void swap(void *pvData1, void *pvData2, int iDataSize) { unsigned char *pcData1 = NULL; unsigned char *pcData2 = NULL; unsigned char ucTmp1; pcData1 = (unsigned char *)pvData1; pcData2 = (unsigned char *)pvData2; do{ ucTmp1 = *pcData1; *pcData1 = *pcData2; *pcData2 = ucTmp1; pcData1++; pcData2++; } while (--iDataSize > 0); } int main() { float fa = 1.23, fb = 2.32; float *f1=&fa, *f2=&fb; int iDataSize = sizeof(float)/sizeof(char);
swap(f1, f2, iDataSize); return 0; }
NULL
C中對NULL的預定義有兩個:
#define NULL 0 #define NULL ((void *)0)
並且標准C規定,在初始化、賦值或比較時,如果一邊是變量或指針類型的表達式,則編譯器可以確定另一邊的常數0為空指針,並生成正確的空指針值。即在指針上下文中“值為0的整型常量表達式”在編譯時轉換為空指針。那么也就是上面的兩個的定義在指針上下文中是一致的了。
我們經常在聲明一個指針時,為避免野指針的情況常用的int *pi = NULL;中的NULL,是會被自動轉換為(void *)0的。所以下面的代碼也是合法的:
int *pi = 0; if(pi == 0){ …… }
函數類型 和 函數指針
盡管函數並不是變量,但它在內存中仍有其物理地址。每個函數都有一個入口地址,由函數名指向這個入口地址,函數名相當於一個指向其函數入口的指針常量。
可以將函數名賦值給一個指針,使該指針指向這個函數的入口,即是函數指針。
這里注意和指針函數區分開來:
指針函數是一個返回指針的函數,指針函數具體定義方式:
char *Convert(char *pName , int length);
函數指針的定義要和具體所指向的函數的形式一致,如對函數int Max(int a, int b)定義一個函數指針:
int (*pMax)(int a, int b); pMax = Max;
int (*pMax)(int a, int b)句中,函數指針pMax外的括號一定要帶上[s1] ,因為“()”的優先級高於“*”,如果無括號,就變成了int *pMax(int a, int b)的形式,變成了一個函數(指針函數)的聲明了。pMax=Max句將代表函數int Max(int a, int b)入口地址的其函數名Max,賦值給了指向同類型函數的指針pMax。這樣pMax就和Max有相同的指代作用,並且pMax還可以指向與int Max(int a, int b)同參同返回值的函數。
int Max(int a, int b); int Min(int a, int b);
int (*p)(int a, int b); int max, min; p = Max; max = (*p)(3, 5); //進行調用時,也要記得帶括號(*p) p = Min; min = (*p)(3, 5)
執行中對指針p指向進行截圖:
最后需要注意的是,由於函數在內存中的分布方式並不是齊整的,所以函數指針並沒有++自增運算和—自減運算。
通過括號強行將pMax首先與“*”結合,也就意味着,pMax是一個指針;接着與后面的“()”結合,說明該指針指向的是一個函數,然后再與前面的int結合,也就是說,該函數的返回值是int。由此可見,pfun是一個指向返回值為int的函數的指針。