結構體類型的變量名並不能直接當作地址使用,這一點和基本數據類型相同。(修改版)

有了前面兩篇的基礎,下面正式開扒變量名和內存的關係,先看一段大家很熟悉的代碼:

  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;不合法的原因。因此,不管指針變量是全局的還是局部的、靜態的還是非靜態的,都應該在聲明它的同時進行初始化,要麼賦予一個有效的地址,要麼賦予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;。

  而如果是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*作爲中轉,詳見下面的函數實現:


//這裏差不 多是前後數反轉放, 4個字節4次交換
//這裏 形參=實參  void* =float*   ,相反則不行.
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); 
	cout<<fa<<" "<<fb<<endl;
	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指向進行截圖:

 

  最後需要注意的是,由於函數在內存中的分佈方式並不是齊整的,所以函數指針並沒有++自增運算和—自減運算。


 [s1]

  通過括號強行將pMax首先與“*”結合,也就意味着,pMax是一個指針;接着與後面的“()”結合,說明該指針指向的是一個函數,然後再與前面的int結合,也就是說,該函數的返回值是int。由此可見,pfun是一個指向返回值爲int的函數的指針。

發佈了11 篇原創文章 · 獲贊 0 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章