程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> 第六天

第六天

編輯:關於C語言

1.對於變量、指針和指針變量的關系,大家可以看看下面的圖:

指針的概念:

指針與指針變量:

大家要注意:

a.普通變量存儲數值,指針變量存儲地址,即指針

b.指針變量的數據類型決定了其尋址范圍的大小

c.通過變量來訪問變量的存儲空間叫做直接訪問;先獲取其地址,再根據地址找到存儲單元叫做間接訪問。

d.只要是指針變量,無論是什麼類型,在32位的CPU下,都占4個字節。

大家看看下面這個例子:

很簡單,對於指針變量,剛剛已經說得很明確,都是4個字節32位CPU下);而對於其他的普通變量,該是什麼類型,就是什麼類型,所以,打印:

大家要知道,int *p中的*僅僅是告訴編譯器p是一個指針變量用來存儲地址的,如:int a = 10;那麼有a = *( &a );指針變量的數據類型本質上就是其指向對象的類型。

我們操作指針的時候要注意:

  1. 定義什麼樣的指針變量(保存什麼樣的變量地址)由一次操作的幾個內存單元決定

  2. 要怎樣獲取合法的地址

  3. 怎樣去讀、寫





2.在嵌入式開發中,對於ARM裸板操作,經常用到強制類型轉換,例如:

# define  GPFCON  *( (volatile unsigned *)0x56000050 )

這個例子是將一個地址與GPFCON綁定,實現操作GPFCON就是操作0x56000050這個地址,我們一起來看看,這是怎樣實現的!

首先,0x56000050是一個具體的十六進制數值,(volatile unsigned *)0x56000050,這條語句是將這個具體的數值轉化成了一個無符號的地址,不過這個地址不能被編譯器優化,最後,在前面還有一個星號*,表示解指針,說明取的是內容,那麼這句話就可以這樣理解了:在內存中有一塊內存,地址邏輯地址)是0x56000050,裡面的內容是空的,然後,我們用GPFCON來給它定義一下,這樣,操作GPFCON就是操作邏輯地址0x56000050了。

還是上面那個例子,我們來稍稍改動一下:

我們一起來分析一下這個打印結果:

大家先要了解CPU的存儲模式,即字節序,也就是大家常說的大端小端模式,記住八個字:

小端模式:高高低低;大端模式:高低低高

小端模式下是高位存儲在高地址,低位存儲在低地址;大段模式下高位存儲在低地址,低位存儲在高地址。
而我們一般的英特爾CPU則是小端模式,至於為什麼,以後會有解釋。就是符合上面的高高低低原則。

所以對於0x1234,34是低位,12是高位,所以存儲的順序是34 12,這有兩個字節,而char則是一個字節的,所以截取了前面的高字節34,所以打印0x34。而剛剛說了的指針變量在32位的cpu下,都是4個字節,所以,都可以打印出來,對於上面的加一操作,只要記住指針變量的加一是加的該指針變量內存單元的長度即可。



3.在用指針操作內存單元的時候,我們會經常發現一個很糾結的錯誤,那就是段錯誤:segment fault,是由於虛擬內存管理單元的異常所致,而該異常則通常是由於解引用一個未初始化或非法值的指針引起的。在gcc下,我們可以用GDB進行調試,調試的步驟如下:

執行命令:

$ ulimit –c             //查看有沒有限制,如果結果為0,則有限制

$ ulimit –c unlimited     //去掉限制

$ ulimit –c             //再次查看,結果應該為unlimited

$ gcc –g xxx.c          // xxx.c表示c源文件,聲稱調試文件

$ ls                   // ls查看有沒有生成調試用的core文件

$ ./a.out               //執行可執行文件,會報段錯誤

$ gdb a.out core        //用GDB調試,在調試命令中輸入r,然後回車兩次即可看到在哪兒發生了段錯誤。

在我們寫代碼的時候,經常會有人將“NULL”和“NUL”混淆,一個“L”的NULL用於結束一個ASCII字符碼,兩個“L”的NULL用於表示什麼也指向,即空指針。



4.大家要區分好*p, *p++,(*p)++, *++p, ++*p這幾個的不同:

*p++先取指針p指向的值,再將指針p自增1;

(*p)++先去指針p指向的值,再將該值自增1;

*++p先將指針p自增1此時指向數組第二個元素),*操作再取出該值

++*p先取指針p指向的值,再將該值自增1

大家在用程序進行測試的時候,記得使用不同的指針變量,還是上次說的,指針進行自增或者自減運算,其地址都會改變。很簡單,這裡就不舉例子了!



5.總結一下,我們在操作指針的時候要注意的問題:

a.指針變量存的是地址

b.指針變量的數據類型決定其尋址范圍

c.指針變量的算術運算,加減常數指的是元素個數

d.注意指針的指向,一般初始化為NULL

e.定義什麼樣的指針變量由一次操作幾個內存單元決定

f.指針變量相互賦值要類型匹配

g.注意指針的當前值。即++或者- -之後,地址會發生相應的改變

h.連續的地址相減的值為元素的個數



6.大家看看這個程序:

沒有什麼實際作用,僅僅是為了測試所用。打印:

由結果可以得出下面的結論:

a + i  <=>  &a[i];

a[i]  <=>  *(a + i)  <=> *(p + i)  <=>  p[ i];

注意,這只是針對於一維數組。至於二維數組,待會兒會專門講解。



7.大家對於二維數組,是怎麼理解的?看看下面的圖:

二維數組的理解:

二維數組元素的引用:

對於二維數組a[3][4],其內部的存儲結構圖有:

我們要記住二維數組和指針的關系:

*行指針=列指針;&列指針=行指針

例如,對於上面的a[3][4]數組,如果我們想要訪問a[2][2],則我們可以訪問以下的地址,它們都是等效的:

&a[2][2],a[2]+2,a[0]+2*4+2,(int *)(a+2)+2,*(&a[0]+2)+2

我們也可以直接使用下面的值,也是等效的:

a[2][2],*(a+2)+2,*a+2*4+2

在二維數組a[3][4]中,若有int *p1 = a[0], *p2 = a[1], *p3 = a[2];我們可以用一個數組表示:int *p[3] = {a[0], a[1], a[2]};這種結構就是傳說中的指針數組,大家這樣理解:指針數組,重點是數組,只是裡面的元素是指針罷了,可以這樣看:int*  p[3] = {a[0, a[1], a[2]]};每個元素都是指針變量,而對應的p則是一個二級指針常量。

當然,與之對應的還有一個數組指針:int (*p)[3];數組指針,實質上還是一個指針,只不過指向了一個數組,p是一個數組指針,指向整個數組。

其實類似的還有很多,什麼數組指針數組等等。它們的重點都是在最後,其實都不是很難,只是數組或者指針的一種組合而已。



8.下面的一個代碼中有很多陷阱,等著我們跳,你會掉坑裡嗎?

# include <stdio.h>

# include <stdlib.h>

# include <string.h>

/*

程序首先申請一個char類型的指針str,並把str指向NULL(即str裡存的是NULL的地址,*str為NULL中的值為0),調用函數的過程中做了如下動作:

   1、申請一個char類型的指針p,

   2、把str的內容copy(傳)到了p裡(這是參數傳遞過程中系統所做的),

   3、為p指針申請了100個空間,

   4、返回Test函數.最後程序把字符串hello world拷貝到str指向的內存空間裡.到這裡錯誤出現了!str 的空間始終為 NULL 而並沒有實際 的空間.深刻理解函數調用的第 2 步,將不難發現問題所在!(注意:傳遞 的參數和消除的參數)

*/

/*

void GetMem(char *p)

{

   printf("p = str = %p\n",p);

   p = (char *)malloc(100);

   strcpy(p,"hello world\n");

   printf(p);

   printf("p = %p\n",p);

}

void Test(void)

{

   char *str;

   GetMem(str);

//  str = (char *)malloc(100);

   strcpy(str,"hello!\n");

   printf("str = %p\n",str);

}

*/

/*

char *GetMem(void)

{

   char p[] = "hello world\n";

//  printf(p);

//  printf("%p",p);

   return p;

//  free(p);           //數組裡面的內容已經被釋放,

                          //只是返回一個地址,並沒有內容

}

void Test()

{

   char *str = NULL;

   str = GetMem();

   printf(str);

//  printf("str = %p\n",str);

}

*/

/*

void GetMem(char **p, int n)

{

   *p = (char *)malloc(n);

}

void Test(void)     //內存洩漏,需要用free來釋放內存空間

{

   char *str = NULL;

   GetMem(&str,100);

   strcpy(str,"hello\n");

   printf(str);

//  free(str);

}

*/

void Test(void)

{

   char *str = (char *)malloc(7);

   strcpy(str,"hello\n");

   free(str);

   printf(str);

   if(str != NULL)

   {

       strcpy(str,"world\n");

       printf(str);

   }

}

int main()

{

   Test();

   return 0;

}

/*

1、C中內存分為四個區

棧:用來存放函數的形參和函數內的局部變量。由編譯器分配空間,在函數執行完後由編譯器自動釋放。

堆:用來存放由動態分配函數如malloc)分配的空間。是由程序員自己手動分配的,並且必須由程序員使用free釋放。如果忘記用free釋放,會導致所分配的空間一直占著不放,導致內存洩露。堆,順序隨意。棧,後進先出(Last-In/First-Out)。

全局區靜態區):用來存放全局變量和靜態變量。存在於程序的整個運行期間,是由編譯器分配和釋放的。

文字常量區:例如char *c =“123456”;則”123456”為文字常量,存放於文字常量區。也由編譯器控制分配和釋放。

程序代碼區:用來存放程序的二進制代碼。

2、內存分配方式

內存分配方式有三種:

1)從靜態存儲區域分配。內存在程序編譯的時候就已經分配好,這塊內存在程序的整

個運行期間都存在。例如全局變量,static變量。

2)在棧上創建。在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數

執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。

3)從堆上分配,亦稱動態內存分配。程序在運行的時候用malloc或new申請任意多少的內存,程序員自己負責在何時用free或delete釋放內存。動態內存的生存期由我們決定,使用非常靈活,但問題也最多。

3、常見的內存錯誤及其對策

發生內存錯誤是件非常麻煩的事情。編譯器不能自動發現這些錯誤,通常是在程序運行時才能捕捉到。而這些錯誤大多沒有明顯的症狀,時隱時現,增加了改錯的難度。有時用戶怒氣沖沖地把你找來,程序卻沒有發生任何問題,你一走,錯誤又發作了。常見的內存錯誤及其對策如下:

1)內存分配未成功,卻使用了它。

編程新手常犯這種錯誤,因為他們沒有意識到內存分配會不成功。常用解決辦法是,在使用內存之前檢查指針是否為NULL。如果指針p是函數的參數,那麼在函數的入口處用assert(p!=NULL)進行檢查。如果是用malloc或new來申請內存,應該用if(p==NULL)或if(p!=NULL)進行防錯處理。

2)內存分配雖然成功,但是尚未初始化就引用它。

犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以為內存的缺省初值全為零,導致引用初值錯誤例如數組)。內存的缺省初值究竟是什麼並沒有統一的標准,盡管有些時候為零值,我們寧可信其無不可信其有。所以無論用何種方式創建數組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。

3)內存分配成功並且已經初始化,但操作越過了內存的邊界。

例如在使用數組時經常發生下標“多1”或者“少1”的操作。特別是在for循環語句中,循環次數很容易搞錯,導致數組操作越界。

4)忘記了釋放內存,造成內存洩露。

含有這種錯誤的函數每被調用一次就丟失一塊內存。剛開始時系統的內存充足,你看不到錯誤。終有一次程序突然死掉,系統出現提示:內存耗盡。動態內存的申請與釋放必須配對,程序中malloc與free的使用次數一定要相同,否則肯定有錯誤new/delete同理)。

規則1】用malloc或new申請內存之後,應該立即檢查指針值是否為NULL。防止使用指針值為NULL的內存。

規則2】不要忘記為數組和動態內存賦初值。防止將未被初始化的內存作為右值使用。

規則3】注意不要返回指向“棧內存”的“指針”或者“引用”,因為該內存在函數體結束時被自動銷毀。

規則4】避免數組或指針的下標越界,特別要當心發生“多1”或者“少1”操作。

規則5】動態內存的申請與釋放必須配對,防止內存洩漏。

規則6】用free或delete釋放了內存之後,立即將指針設置為NULL,防止產生“野指針”。

4、動態分配釋放內存舉例

用malloc動態分配內存後一定要判斷一下分配是否成功,判斷指針的值是否為NULL。

內存分配成功後要對內存單元進行初始化。

內存分配成功且初始化後使用時別越界了。

內存使用完後要用freep)釋放,注意,釋放後,p的值是不會變的,仍然是一個地址值,仍然指向那塊內存區,只是這塊內存區的值變成垃圾了。為了防止後面繼續使用這塊內存,應在free(p)後,立即p=NULL,這樣後面如果要使用,判斷p是否為NULL時就會判斷出來。

*/


本文出自 “嵌入式學習之路” 博客,請務必保留此出處http://fancong.blog.51cto.com/3191877/1296147

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved