1. 程式人生 > 其它 >C語言指標第一篇

C語言指標第一篇

指標

1、概述

指標是C語言靈魂所在。指標是靈活的,但是也是危險的。

指標:記憶體單元的編號,計算機中的記憶體單元不是按照位元來算的,而是按照位元組來計算的,一個位元組一個編號,這個編號就是地址;

指標就是地址,地址就是指標。

在最開始的入門程式中:

int i = 10;

這個i不是一個指標,但是有著指標的概念。i是變數,但是&i對應的值就是地址,也就是指標。

看看指標入門程式:

# include <stdio.h>
int main(void){
	// int * 表示的是p變數存放的int型別變數的地址。也就是說p變數存放的是int型別變數的地址,通過這個地址,可以找到變數所能夠儲存的資料。
	int * p;
    // 表示的q變數存放整型變數的值。而這個值是儲存在記憶體單元編號中的01程式碼
    int q;
	int i = 10;
	return 0;
}

從上面可以看出來,變數儲存的是記憶體單元中的01程式碼,而指標變數儲存的是記憶體單元的編號;二者的關係是記憶體地址編號中儲存著變數的資料,變數對應這些資料。

上面的關係對應的就是

# include <stdio.h>
int main(void){
	
	int * p;
	int i = 10;
	p = &i;
	return 0;
}

同種資料型別只能夠儲存同種型別的

錯誤寫法如下:

double * p;
int i = 10;
p = &i; // doule和int不同 
---------------
double j;    
p = j;  // 這裡是01變數,而不是地址  

那麼這裡的講解和第一章中的寫法就有問題了。因為在第一章中我寫的是int i ,這裡的i代表的就是變數中儲存的01程式碼,而不是地址,但是通過&i,又可以拿到對應的地址值。

2、深入理解指標

# include <stdio.h>
int main(void){
	
	int * p;
	int i = 10;
	p=&i;
	return 0;
}

p指標儲存了i變數的地址,但是修改了p的值不影響i的值,修改了i的值,不影響p的值。

指標變數p儲存了q變數的地址,所以有了指向,也就是p指標變數指向了q變數的地址;*p表示的以p的內容作為地址的變數

修改了p的值,只是說p指標指向了別的地方,不影響q的值;修改了q的值,只是說將01程式碼給修改了,但是並不影響p指標指向的地址值;習慣上也說是指標是存放了變數的地址。

*p就是將p變數的記憶體單元中的值給出去來了,也就是將1000H的這個值給取出來了;變數q的記憶體單元地址也是1000H,這個記憶體單元中的值是01010101

所以說給變數q賦值就是說,拿到了q的記憶體地址,那麼就可以向這個記憶體地址中放入對應的值了,也就是01程式碼;&i就是獲取得到i變數的地址1000H;

同理,和指標也是一樣的,&p也就是說拿到了指標的地址:123H;

操作這個變數q,就相當於是操作*p,所以這就提供了另外一種操作變數的方式。

這裡的p變數和q變數是不同的概念,一種是指標,另外一種是變數。但是*p和q是一個概念。

int *是資料型別,而p是變數名字;

int是q的資料型別,,q是變數名字;

# include <stdio.h>
int main(void){
	
	int * p;
	int i = 10;
	p=&i;
	
	int k;
	k = *p;
	printf("k對應的值是:%d\n",k);
	return 0;
}

輸出的值是:10

再次改進:

# include <stdio.h>
int main(void){
	
	int * p;
	int i = 10;
	p=&i;
	
	int k;
	k = *p;
	printf("k對應的值是:%d\n",k); // 10 
	// 修改i的值
	*p = 11;
	 printf("k對應的值是:%d\n",k); // 10
	 printf("i對應的值是:%d\n",i); // 11
	return 0;
}

很簡單的就可以來驗證了。

指標的優勢:

能夠表示複雜的資料結構;資料結構能夠理解。

快速的傳遞資料(函式引數)

使得方法返回一個以上的值;

能夠直接訪問硬體;獲取得到變數的地址

能夠方便的處理字串;

是理解面嚮物件語言的基礎;

C語言的靈魂!

3、指標分類

基本型別指標、指標和陣列、指標和函式、指標和結構體、多級指標

地址:記憶體單元的編號;從0開始的非負整數,範圍劃分:

CPU和記憶體中是如何進行互動的,首先通過控制線傳遞CPU的指令,只讀、只寫、可讀、可寫?

然後通過地址線找到對應的資料,然後資料線來將資料讀入到CPU的暫存器;或者是將CPU暫存器中的資訊寫到記憶體中去;

那麼重點就是這個地址線。

地址線有多少根?

一根地址線就代表了當前的CPU能夠訪問到的狀態是多少,一根只有兩種狀態,那麼就是2,那麼對於32位或者是64位的作業系統來說,互動太過於頻繁,而且效率低下,所以增加地址線,增加到什麼程度呢?取決於自己當前的CPU。

一般來說,32根的地址線,可以訪問得到的狀態是:2^32=4G,那麼CPU來說,如果記憶體是4G的,只需要一次就可以定位到資料在記憶體條中的位置,速度極快;

64位的作業系統既然支援的話,那麼地址線就應該是264,這個數字機器龐大,可以在一次互動的過程中,讀取得到264位的資料到CPU中去,這個是非常恐怖的。

將讀取後的位通過資料匯流排來進行傳輸,這個資料匯流排取決於我們能夠讀取到資料到CPU中去,一般來說CPU都具有快取的概念。所以這裡是否真的是按照對應的讀入,也不一定,因為也有區域性性原理和時間原理,不一定說按照CPU的快取來讀的。反正記住是很快速的來讀就行了。

那麼通過資料型別也能夠確定讀取多少資料到CPU中去,因為CPU很快,能夠很快的定位到對應的資料存放的位置,然後將資料讀入到CPU中去執行。

我當前的記憶體大小是8G,所以記憶體地址編號就是0~8G-1,這個範圍也是極大的。

所以這裡又對記憶體單元編號有了一個新的理解:

32根地址線,每根線是 0 1 狀態,通過地址線來找記憶體單元地址,一共可以找到2的32次方個記憶體單元地址,並不是一個位元組算一個地址的編號,是cpu的地址線的定址能力決定記憶體單元的地址個數,不是反過來。
一個記憶體地址單元是儲存一個位元組的資料。
在32位cpu下,一共有2的32次方個記憶體單元地址,那麼指標變數就必須要大於等於記憶體單元地址的總數,所以一個指標變數的長度就是32位。而1個記憶體單元地址裡面儲存一個位元組的資料,所以記憶體的最大值就是2的32次方Bytes,也就是4gb。8位 16位 64位同理

那麼也就是char型別的指標一次性可以定位到每一個記憶體單元地址;而int型別的指標可以定位到四個連續記憶體單元地址

所以說在不同的作業系統中,指標所佔用的位元組數也是不一樣的。

那麼這個記憶體記憶體單元也是通過電訊號來進行連線的,相當於一個二維陣列,找到對應的一行或者是一列來進行儲存資料;按照行列式的方式來進行排列。

因為計算機能夠操作的最小單元是8個位元,也就是一個位元組;所以為了命名,我暫時將這個當做記憶體地址編號,每八個一位;

按照資料型別來劃分多個位元組佔據一個,那麼就可以直接取到多個位元組的型別的資料;

我之前一直以為指標是佔用一個位元組的,原來不止!而是地址匯流排來的,地址匯流排決定了一個指標能夠佔用多少位。

那麼64位的作業系統就決定了一個指標能夠佔用8個位元組了。這個之後回來證明。

寫了一個程式:

# include <stdio.h>
int main(void){
	
	int * p;
	int i = 10;
	*p = i;
	printf("*p對應的值是:%d\n",*p); 
	return 0;
}

檢視輸出控制檯,發現什麼都沒有。

仔細分析一波,為什麼?首先p指向的記憶體單元肯定是有值的,但是這個值指向的地址OS是否允許訪問,這就是一個首要的問題了。

因為指標隨便指向,我們對指標裡面的內容進行操作,可能會導致其他的應用程式有著更大的安全隱患。

可能這塊資料是我們不能夠訪問的或者說是沒有對應的許可權操作這塊記憶體空間。也可以理解成是許可權問題。

3.1、記憶體洩漏

首先講一下記憶體洩漏的概念。記憶體洩漏也就是說使用完了記憶體之後,沒有進行釋放,這塊記憶體對於作業系統來說,這塊記憶體是在使用的。而正在使用的程序覺得這塊我放在這裡是沒有問題的。那麼利用極限思想,如果這種記憶體很多,最終導致了整個記憶體中都是這種,那麼對於記憶體來說,相當於是沒有使用記憶體,那麼將會導致速率極慢,最終虛擬記憶體也堆積滿了,系統就宕機了。所以對於作業系統來說,這是很危險的操作。

所以要求程式設計師在使用完成之後,就要及時對其進行釋放記憶體。但是這裡也要注意一個細節:

int * p,q,a,b,c;
p=a=b=c=q;
free(p);

這裡的這個free代表的就是將p指向的記憶體給釋放掉了,交換給作業系統了。但是如果再次操作free(q)的話,如果被編譯器和作業系統檢測出來還好,但是如果沒有檢測出來,這個時候將會導致其他程序使用的這個記憶體空間給釋放掉了,可能會導致其他程式崩潰。

所以釋放一次就可以達到一種釋放的效果。

所以指標是非常靈活的,得合理的進行運用。釋放少一個不行,多釋放一個也不行;

3.2、測試

# include <stdio.h>

void swap(int x,int y){
	int tem;
	tem = x;
	x = y;
	y = tem;
}

int main(void){
	
	int a = 3;
	int b = 4;
	printf("a對應的值是:%d,b對應的值是:%d\n",a,b);
	swap(a,b);
	printf("a對應的值是:%d,b對應的值是:%d\n",a,b);
	return 0;
}

控制檯輸出:

a對應的值是:3,b對應的值是:4
a對應的值是:3,b對應的值是:4

--------------------------------
Process exited after 0.2064 seconds with return value 0
請按任意鍵繼續. . .

可以發現,儘管在函式中已經對變數進行了修改,但是主函式中依然沒有進行修改。為什麼?

再看下面的程式碼發現問題:

# include <stdio.h>

void swap(int * x,int * y){ 

	// 現用一個臨時指標來儲存
	int * tem;
	// tem中儲存的就應該是x指標中的值。操作一個變數,就相當於是操作這個指標中的01程式碼 
	tem = x;
	// 現在需要的是將x指標中儲存的地址修改成y的地址 
	x = y;
	// y指向了tem的地址 
	y = tem;
	
}

int main(void){
	
	int a = 3;
	int b = 4;
	printf("a對應的值是:%d,b對應的值是:%d\n",a,b);
	swap(&a,&b);
	printf("a對應的值是:%d,b對應的值是:%d\n",a,b);
	return 0;
}

控制檯輸出,得到結論:

a對應的值是:3,b對應的值是:4
a對應的值是:3,b對應的值是:4

--------------------------------
Process exited after 0.2064 seconds with return value 0
請按任意鍵繼續. . .

結果發現,沒有任何改變。但是從上面的結果中得出結論,操作一個變數,無論是普通變數還是指標變數,都相當於是操作變數裡面儲存的值

普通變數直接操作的就是01程式碼資料,而指標變數直接操作的就是地址值

在上面的操作中,新開闢了一個棧空間,只是交換了p和q變數中儲存的值,這裡交換的值是a和b的地址值,但是並沒有對a、b地址值中儲存的值進行修改。

這就是沒有修改的原因的地方。

畫個圖來進行演示:

再實驗:

# include <stdio.h>

void swap(int * x,int * y){ 

	// 剛剛那種方式修改了p和q中的值,那麼現在需要做的是修改各自指向的地址的值 
	int tem;
	// tem = 3
	tem = *x;
	// x指向的地址中的01程式碼成了5的01程式碼 
	*x = *y;
	// 3放入到y指向的地中的01程式碼 
	*y = tem;
	
}

int main(void){
	
	int a = 3;
	int b = 4;
	printf("a對應的值是:%d,b對應的值是:%d\n",a,b);
	swap(&a,&b);
	printf("a對應的值是:%d,b對應的值是:%d\n",a,b);
	return 0;
}

在上面的操作中可以看到,操作指標x,將指標x指向的內容,也就是3給取出來,然後交給tem儲存,然後將y中儲存的值(地址值),在經過*後操作之後,就取出來了這個y指標對應的地址空間空儲存的01程式碼,然後賦值給指標x所指向空間的01程式碼,然後在進行交換。

對應的圖如上所示:

所以在交換值的時候,是對立面的記憶體地址進行交換的。

對於方法來說,在C語言中可以先進行宣告,其中,在宣告中,不需要指定具體的方法名,只需要宣告引數型別即可。

如下所示:

# include <stdio.h>
// 對函式進行宣告
void swap(int *,int *);
// 對函式進行實現
void swap(int * x,int * y){ 
	// 先儲存下二者的值
	int a ,b;
	a = *x;
	b  = *y; 

	// 現用一個臨時指標來儲存
	int * tem;
	// tem中儲存的就應該是x指標中的值。操作一個變數,就相當於是操作這個指標中的01程式碼 
	tem = x;
	// 現在需要的是將x指標中儲存的地址修改成y的地址 
	x = y;
	// y指向了tem的地址 
	y = tem;	
}

int main(void){
	
	int a = 3;
	int b = 4;
	printf("a對應的值是:%d,b對應的值是:%d\n",a,b);
	swap(&a,&b);
	printf("a對應的值是:%d,b對應的值是:%d\n",a,b);
	return 0;
}

4、方法引數是指標和非指標

這個是一個很重要的點。說實話,無論是在學java還是C語言的時候,都一直搞不懂這一塊的東西,知道今天,才終於想起來總結一波這裡的內容。

拿上面的案例來舉列子:

# include <stdio.h>

void swap(int x,int y){
	int tem;
	tem = x;
	x = y;
	y = tem;
}

int main(void){
	
	int a = 3;
	int b = 4;
	printf("a對應的值是:%d,b對應的值是:%d\n",a,b);
	swap(a,b);
	printf("a對應的值是:%d,b對應的值是:%d\n",a,b);
	return 0;
}

在main函式中呼叫了swap函式,將main函式中的資料進行了傳遞。

首先入手的應該分析一波記憶體分配情況,這個是非常重要有助於理解的。

在記憶體中,因為有了int a,b的定義,所以向作業系統申請分配記憶體,在呼叫了方法swap方法的時候,因為x,y也屬於變數,那麼作業系統就重新分配了記憶體給x,y,所以方法呼叫和方法呼叫之間沒有任何的關係。但是因為方法在進行呼叫的時候,將值傳遞了過去;如果不是地址值,那麼將毫無關係;但是如果是地址值,那麼將會導致,兩個函式之間產生關係。

如果是地址值,那麼修改了之後,因為指標變數有指向,但是把指向的地址空間的內容給改了,再次從指標變數中進行獲取的時候,發現裡面的值是已經被修改過的值。

5、總結

1、指標是以內容為地址的變數;

2、修改了指標的值與指向的變數的值沒有關係;修改了變數的值與指標的值也沒有關係;

3、操作一個變數,就是操作這個變數的值。其實可以把每個變數都看成是一個容器,操作容器名字,就相當於是操作容器內的值;

4、指標所佔位元組是以當前的作業系統的地址匯流排決定的;

5、操作指標要注意先進行指向,而不應該直接進行賦值。因為直接賦值可能會造成許可權不夠或者是許可權問題。

6、方法呼叫方法,其實兩個方法中的變數是毫無關係的;取決於傳遞是普通變數還是地址值,如果是地址值,兩個函式中都有指向,一個修改了,其他的地方也跟著修改了;

千里之行,始於足下。不積跬步,無以至千里