1.對於變量、指針和指針變量的關系,大家可以看看下面的圖:
指針的概念:
指針與指針變量:
大家要注意:
a.普通變量存儲數值,指針變量存儲地址,即指針
b.指針變量的數據類型決定了其尋址范圍的大小
c.通過變量來訪問變量的存儲空間叫做直接訪問;先獲取其地址,再根據地址找到存儲單元叫做間接訪問。
d.只要是指針變量,無論是什麼類型,在32位的CPU下,都占4個字節。
大家看看下面這個例子:
很簡單,對於指針變量,剛剛已經說得很明確,都是4個字節32位CPU下);而對於其他的普通變量,該是什麼類型,就是什麼類型,所以,打印:
大家要知道,int *p中的*僅僅是告訴編譯器p是一個指針變量用來存儲地址的,如:int a = 10;那麼有a = *( &a );指針變量的數據類型本質上就是其指向對象的類型。
我們操作指針的時候要注意:
定義什麼樣的指針變量(保存什麼樣的變量地址)由一次操作的幾個內存單元決定
要怎樣獲取合法的地址
怎樣去讀、寫
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