指針深入探討

C語言變量的內存實質

一、先來理解C語言中變量的實質

要理解C指針,我認為一定要理解C中“變量”的存儲實質,所以我就從“變量”這個東西開始講起吧!先來理解理解內存空間吧!請看下圖:
如上圖所示,內存只不過是一個存放數據的空間,就好像我的看電影時的電影院中的座位一樣。電影院中的每個座位都要編號,而我們的內存要存放各種各樣的數據,當然我們要知道我們的這些數據存放在什么位置吧!所以內存也要象座位一樣進行編號了,這就是我們所說的內存編址。座位可以是遵循“一個座位對應一個號碼”的原則,從“第1號”開始編號。而內存則是按一個字節接著一個字節的次序進行編址,如上圖所示。每個字節都有個編號,我們稱之為內存地址。好了,我說了這么多,現在你能理解內存空間這個概念嗎?

我們繼續看看以下的C/C++語言變量聲明:
? ?int i;
? ?char a;
每次我們要使用某變量時都要事先這樣聲明它,它其實是內存中申請了一個名為i的整型變量寬度的空間(DOS下的16位編程中其寬度為2個字節),和一個名為a的字符型變量寬度的空間(占1個字節)。

我們又如何來理解變量是如何存在的呢。當我們如下聲明變量時:
? ?int i;
? ?char a;
內存中的映象可能如下圖:
圖中可看出,i在內存起始地址為6上申請了兩個字節的空間(我這里假設了int的寬度為16位,不同系統中int的寬度可能是不一樣的),并命名為i。a在內存地址為8上申請了一字節的空間,并命名為a。這樣我們就有兩個不同類型的變量了。

二、賦值給變量

再看下面賦值:
? ?i = 30;
? ?a = ’t’;
你當然知道個兩個語句是將30存入i變量的內存空間中,將“t”字符存入a變量的內存空間中。我們可以利用這樣的形象來理解啦:

三、變量在哪里?(即我想知道變量的地址)

好了,接下來我們來看看&i是什么意思?
是取i變量所在的地址編號嘛!我們可以這樣讀它:返回i變量的地址編號。你記住了嗎?

我要在屏幕上顯示變量的地址值的話,可以寫如下代碼:
? ?printf("%x", &i);
以上圖的內存映象為例,屏幕上顯示的不是i值30,而是顯示i的內存地址編號6了。當然,在你的實際操作中,i變量的地址值不會是這個數了。

這就是我所認為的作為初學者應該能夠想象到的變量存儲的實質了。請這樣理解吧!
最后總結代碼如下:
main()
{
? ?int i = 39;
? ?printf(“%d\n”, i); /*①*/
? ?printf(“%d\n”, &i); /*②*/
? ?return(0);
}
現在你可知道①、②兩個printf分別在屏幕上輸出的是i的什么東西啊?

好啦!下面我們就開始真正進入指針的學習了。


指針,想說弄懂你不容易啊!我們許多初學指針的人都要這樣感慨。我常常在思索它,為什么呢?其實生活中處處都有指針,我們也處處在使用它。有了它我們的生活才更加方便了。沒有指針,那生活才不方便。不信?你看下面的例子。

這是一個生活中的例子:比如說你要我借給你一本書,我到了你宿舍,但是你人不在宿舍,于是我把書放在你的2層3號的書架上,并寫了一張紙條放在你的桌上。紙條上寫著:你要的書在第2層3號的書架上。當你回來時,看到這張紙條,你就知道了我借與你的書放在哪了。你想想看,這張紙條的作用,紙條本身不是書,它上面也沒有放著書。那么你又如何知道書的位置呢?因為紙條上寫著書的位置嘛!其實這張紙條就是一個指針了。它上面的內容不是書本身,而是書的地址,你通過紙條這個指針找到了我借給你的這本書。

那么我們C/C++中的指針又是什么呢?請繼續跟我來吧,下面看一條聲明一個指向整型變量的指針的語句:
? ?int *pi;
pi是一個指針,當然我們知道啦,但是這樣說,你就以為pi一定是個多么特別的東西了。其實,它也只過是一個變量而已。與上一篇中說的變量并沒有實質的區別。不信你看下面圖:

說明:這里我假設了指針只占2個字節寬度,實際上在32位系統中,指針的寬度是4個字節寬的,即32位。
由圖示中可以看出,我們使用“int *pi”聲明指針變量—— 其實是在內存的某處聲明一個一定寬度的內存空間,并把它命名為pi。你能在圖中看出pi與前面的i、a 變量有什么本質區別嗎?沒有,當然沒有!pi也只不過是一個變量而已嘛!那么它又為什么會被稱為“指針”?關鍵是我們要讓這個變量所存儲的內容是什么。現在我要讓pi成為具有真正“指針”意義的變量。請接著看下面語句:
? ?pi = &i;
你應該知道&i是什么意思吧!再次提醒你啦:這是返回i變量的地址編號。整句的意思就是把i地址的編號賦值給pi,也就是你在pi里面寫上i的地址編號。結果如下圖所示:
你看,執行完pi=&i后,在圖示中的內存中,pi的值是6。這個6就是i變量的地址編號,這樣pi就指向了變量i了。你看,pi與那張紙條有什么區別?pi不就是那張紙條嘛!上面寫著i的地址,而i就是那個本書。你現在看懂了嗎?因此,我們就把pi稱為指針。所以你要記住,指針變量所存的內容就是內存的地址編號!好了,現在我們就可以通過這個指針pi來訪問到i這個變量了,不是嗎?看下面語句:
? ?printf("%d", *pi);
那么*pi什么意思呢?你只要這樣讀它:pi的內容所指的地址的內容(嘻嘻,看上去好像在繞口令了),就是pi這張“紙條”上所寫的位置上的那本 “書”—— i 。你看,Pi的內容是6,也就是說pi指向內存編號為6的地址。*pi嘛,就是它所指地址的內容,即地址編號6上的內容了,當然就是30這個“值”了。所以這條語句會在屏幕上顯示30。也就是說printf("%d", *pi)等價于printf("%d", i) ,請結合上圖好好體會吧!各位還有什么疑問?

到此為止,你掌握了類似&i、*pi寫法的含義和相關操作嗎?總的一句話,我們的紙條就是我們的指針,同樣我們的pi也就是我們的紙條!剩下的就是我們如何應用這張紙條了。最后我給你一道題:程序如下。
? ?char a,*pa;
? ?a = 10;
? ?pa = &a;
? ?*pa = 20;
? ?printf("%d", a);
你能直接看出輸出的結果是什么嗎?如果你能,我想本篇的目的就達到了。好了,就說到這了。Happy Study! 在下篇中我將談談“指針的指針”即對
? ?int **ppa;
中ppa的理解。

一、通過數組名訪問數組元素

看下面代碼:
int i, a[] = {3,4,5,6,7,3,7,4,4,6};
for (i = 0; i <= 9; i++)
{
? ?printf("%d\n", a[i]);
}
很顯然,它是顯示a 數組的各元素值。我們還可以這樣訪問元素,如下:
int i, a[] = {3,4,5,6,7,3,7,4,4,6};
for (i = 0; i <= 9; i++)
{
? ?printf("%d\n", *(a+i));
}
它的結果和作用完全一樣。

二、通過指針訪問數組元素

int i, *pa, a[] = {3,4,5,6,7,3,7,4,4,6};
pa = a; /*請注意數組名a直接賦值給指針pa*/
for (i = 0; i <= 9; i++)
{
? ?printf("%d\n", pa[i]);
}
很顯然,它也是顯示a 數組的各元素值。另外與數組名一樣也可如下:
int i, *pa, a[] = {3,4,5,6,7,3,7,4,4,6};
pa = a;
for (i = 0; i <= 9; i++)
{
? ?printf("%d\n", *(pa+i));
}
看pa = a,即數組名賦值給指針,以及通過數組名、指針對元素的訪問形式看,它們并沒有什么區別,從這里可以看出:數組名其實也就是指針。難道它們沒有任何區別?有,請繼續。

三、數組名與指針變量的區別

請看下面的代碼:
int i, *pa, a[] = {3,4,5,6,7,3,7,4,4,6};
pa = a;
for (i = 0; i <= 9; i++)
{
? ?printf("%d\n", *pa);
? ?pa++; /*注意這里,指針值被修改*/
}
可以看出,這段代碼也是將數組各元素值輸出。不過,你把循環體{}中的pa改成a試試。你會發現程序編譯出錯,不能成功。看來指針和數組名還是不同的。其實上面的指針是指針變量,而數組名只是一個指針常量。這個代碼與上面的代碼不同的是,指針pa在整個循環中,其值是不斷遞增的,即指針值被修改了。數組名是指針常量,其值是不能修改的,因此不能類似這樣操作:a++。

前面4、5節中pa[i],*(pa+i)處,指針pa的值是使終沒有改變。所以變量指針pa與數組名a可以互換。

四、聲明指針常量

再請看下面的代碼:
int i, a[] = {3,4,5,6,7,3,7,4,4,6};
int *const pa = a; /* 注意const的位置:不是const int *pa */
for (i = 0; i <= 9; i++)
{
? ?printf("%d\n", *pa);
? ?pa++ ; /*注意這里,指針值被修改*/
}
這時候的代碼能成功編譯嗎?不能。因為pa指針被定義為常量指針了。這時與數組名a已經沒有不同。這更說明了數組名就是常量指針。但是……
? ?int *const a = {3,4,5,6,7,3,7,4,4,6}; /*不行*/
? ?int a[]={3,4,5,6,7,3,7,4,4,6}; /*可以,所以初始化數組時必定要這樣。*/
以上都是在VC6.0上實驗。


一、從const int i 說起

你知道我們聲明一個變量時象這樣int i ;這個i是可能在它處重新變賦值的。如下:
? ?int i = 0;
? ?/* . . . */
? ?i = 20; /*這里重新賦值了*/
不過有一天我的程序可能需要這樣一個變量(暫且稱它變量),在聲明時就賦一個初始值。之后我的程序在其它任何處都不會再去重新對它賦值。那我又應該怎么辦呢?用const 。
? ?/* . . . */
? ?const int ic =20;
? ?/* . . . */
? ?ic = 40; /*這樣是不可以的,編譯時是無法通過,因為我們不能對const 修飾的ic重新賦值的。*/
? ?/*這樣我們的程序就會更早更容易發現問題了。*/
? ?/* . . . */
有了const修飾的ic 我們不稱它為變量,而稱符號常量,代表著20這個數。這就是const 的作用。ic是不能在它處重新賦新值了。

認識了const 作用之后,另外,我們還要知道格式的寫法。有兩種:
? ?const int ic = 20;

? ?int const ic = 20;
它們是完全相同的。這一點我們是要清楚。總之,你務必要記住const 與int哪個寫前都不影響語義。有了這個概念后,我們來看這兩個家伙:
? ?const int *pi

? ?int const *pi
按你的邏輯看,它們的語義有不同嗎?呵呵,你只要記住一點:int 與const 哪個放前哪個放后都是一樣的,就好比const int ic;與int const ic;一樣。也就是說,它們是相同的。

好了,我們現在已經搞定一個“雙包胎”的問題。那么
? ?int *const pi;
與前兩個語句又有什么不同呢?我下面就來具體分析它們的格式與語義吧!

二、const int *pi的語義

我先來說說const int *pi是什么作用 (當然int const *pi也是一樣的,前面我們說過,它們實際是一樣的)。看下面的例子:
? ?/* 代碼開始 */
? ?int i1 = 30;
? ?int i2 = 40;
? ?const int *pi = &i1;
? ?pi = &i2; /* 注意這里,pi可以在任意時候重新賦值一個新內存地址*/
? ?i2 = 80; /* 想想看:這里能用*pi = 80來代替嗎?當然不能!*/
? ?printf("%d\n", *pi); /* 輸出是80 */
? ?/* 代碼結束 */

語義分析:
看出來了沒有啊,pi的值是可以被修改的。即它可以重新指向另一個地址的,但是,不能通過*pi來修改i2的值。這個規則符合我們前面所講的邏輯嗎?當然符合了!

首先const 修飾的是整個*pi(注意,我寫的是*pi而不是pi)。所以*pi是常量,是不能被賦值的(雖然pi所指的i2是變量,不是常量)。
其次,pi前并沒有用const 修飾,所以pi是指針變量,能被賦值重新指向另一內存地址的。你可能會疑問:那我又如何用const 來修飾pi呢?其實,你注意到int *const pi中const 的位置就大概可以明白了。請記住,通過格式看語義。哈哈,你可能已經看出了規律吧?那下面的一節也就沒必要看下去了。不過我還得繼續我的戰斗!

三、再看int *const pi

確實,int *const pi與前面的int const *pi會很容易給混淆的。注意:前面一句的const 是寫在pi前和*號后的,而不是寫在*pi前的。很顯然,它是修飾限定pi的。我先讓你看例子:
? ?/* 代碼開始 */
? ?int i1 = 30;
? ?int i2 = 40;
? ?int *const pi = &i1;
? ?/* pi = &i2; 注意這里,pi不能再這樣重新賦值了,即不能再指向另一個新地址。(第4行的注釋)*/
? ?/* 所以我已經注釋了它。*/
? ?i1 = 80; /* 想想看:這里能用 *pi = 80; 來代替嗎?可以,這里可以通過*pi修改i1的值。(第5行的注釋)*/
? ?/* 請自行與前面一個例子比較。 */
? ?printf("%d", *pi); /* 輸出是80 */
? ?/* 代碼結束 */
語義分析:
看了這段代碼,你明白了什么?有沒有發現pi值是不能重新賦值修改了。它只能永遠指向初始化時的內存地址了。相反,這次你可以通過*pi來修改i1的值了。與前一個例子對照一下吧!看以下的兩點分析:
1)pi因為有了const 的修飾,所以只是一個指針常量:也就是說pi值是不可修改的(即pi不可以重新指向i2這個變量了)(請看第4行的注釋)。
2)整個*pi的前面沒有const 的修飾。也就是說,*pi是變量而不是常量,所以我們可以通過*pi來修改它所指內存i1的值(請看第5行的注釋)。

總之一句話,這次的pi是一個指向int變量類型數據的指針常量。

我最后總結兩句:
1) 如果const 修飾在*pi前,則不能改的是*pi(即不能類似這樣:*pi=50;賦值)而不是指pi。
2) 如果const 是直接寫在pi前,則pi不能改(即不能類似這樣:pi=&i;賦值)。

請你務必先記住這兩點,相信你一定不會再被它們給搞糊了。現在再看這兩個聲明語句int const *pi和int *const pi時,呵呵,你會頭昏腦脹還是很輕松愜意?它們各自聲明的pi分別能修改什么,不能修改什么?再問問自己,把你的理解告訴我吧,可以發帖也可以發到我的郵箱(我的郵箱yyf977@163.com)!我一定會答復的。

四、補充三種情況

這里,我再補充以下三種情況。其實只要上面的語義搞清楚了,這三種情況也就已經被包含了。不過作為三種具體的形式,我還是簡單提一下吧!
情況一:int *pi指針指向const int i常量的情況
/* begin */
const int i1 = 40;
int *pi;
pi = &i1; /* 這樣可以嗎?不行,VC下是編譯錯。*/
/* const int 類型的i1的地址是不能賦值給指向int 類型地址的指針pi的。否則pi豈不是能修改i1的值了嗎!*/
pi = (int *) &i1; /* 這樣可以嗎?強制類型轉換可是C所支持的。*/
/* VC下編譯通過,但是仍不能通過 *pi = 80來修改i1的值。去試試吧!看看具體的怎樣。*/
/* end */

情況二:const int *pi指針指向const int i1的情況
/* begin */
const int i1=40;
const int * pi;
pi=&i1;/* 兩個類型相同,可以這樣賦值。很顯然,i1的值無論是通過pi還是i1都不能修改的。 */
/* end */

情況三:用const int *const pi聲明的指針
/* begin */
int i;
const int * const pi=&i; /*你能想象pi能夠作什么操作嗎?pi值不能改,也不能通過pi修改i的值。因為不管是*pi還是pi都是const的。 */
/* end */之前,我先請你做三道題目。(嘿嘿,得先把你的頭腦搞昏才行……唉呀,誰扔我雞蛋?)

考題一,程序代碼如下:
void Exchg1(int x, int y)
{
? ?int tmp;
? ?tmp = x;
? ?x = y;
? ?y = tmp;
? ?printf("x = %d, y = %d\n", x, y);
}
main()
{
? ?int a = 4,b = 6;
? ?Exchg1(a, b);
? ?printf("a = %d, b = %d\n", a, b);
? ?return(0);
}
輸出的結果為:
x = ____, y=____.
a = ____, b=____.
問下劃線的部分應是什么,請完成。

考題二,程序代碼如下:
void Exchg2(int *px, int *py)
{
? ?int tmp = *px;
? ?*px = *py;
? ?*py = tmp;
? ?printf("*px = %d, *py = %d.\n", *px, *py);
}
main()
{
? ?int a = 4;
? ?int b = 6;
? ?Exchg2(&a, &b);
? ?printf("a = %d, b = %d.\n", a, b);
? ?return(0);
}
輸出的結果為為:
*px=____, *py=____.
a=____, b=____.
問下劃線的部分應是什么,請完成。

考題三,程序代碼如下:
void Exchg3(int &x, int &y)
{
? ?int tmp = x;
? ?x = y;
? ?y = tmp;
? ?printf("x = %d,y = %d\n", x, y);
}
main()
{
? ?int a = 4;
? ?int b = 6;
? ?Exchg3(a, b);
? ?printf("a = %d, b = %d\n", a, b);
? ?return(0);
}
輸出的結果為:
x=____, y=____.
a=____, b=____.
問下劃線的部分應是什么,請完成。你不在機子上試,能作出來嗎?你對你寫出的答案有多大的把握?正確的答案,想知道嗎?(呵呵,讓我慢慢地告訴你吧!)

好,廢話少說,繼續我們的探索之旅了。
我們都知道:C語言中函數參數的傳遞有:值傳遞、地址傳遞、引用傳遞這三種形式。題一為值傳遞,題二為地址傳遞,題三為引用傳遞。不過,正是這幾種參數傳遞的形式,曾把我給搞得暈頭轉向。我相信也有很多人與我有同感吧?

下面請讓我逐個地談談這三種傳遞形式。

二、函數參數傳遞方式之一:值傳遞

(1)值傳遞的一個錯誤認識
先看考題一中Exchg1函數的定義:
void Exchg1(int x, int y) /* 定義中的x,y變量被稱為Exchg1函數的形式參數 */
{
? ?int tmp;
? ?tmp = x;
? ?x = y;
? ?y = tmp;
? ?printf("x = %d, y = %d.\n", x, y);
}
問:你認為這個函數是在做什么呀?
答:好像是對參數x、y的值對調吧?
請往下看,我想利用這個函數來完成對a,b兩個變量值的對調,程序如下:
main()
{
? ?int a = 4,b = 6;
? ?Exchg1(a, b); /*a,b變量為Exchg1函數的實際參數。*/
? ?printf("a = %d, b = %d.\n”, a, b);
? ?return(0);
}
我問:Exchg1()里頭的printf("x = %d, y = %d.\n", x, y);語句會輸出什么啊?我再問:Exchg1()后的printf("a = %d, b = %d.\n”, a, b);語句輸出的是什么?
程序輸出的結果是:
x = 6, y = 4.
a = 4, b = 6.
為什么不是a = 6,b = 4呢?奇怪,明明我把a、b分別代入了x、y中,并在函數里完成了兩個變量值的交換,為什么a、b變量值還是沒有交換(仍然是a = 4、b = 6,而不是a = 6、b = 4)?如果你也會有這個疑問,那是因為你根本就不知實參a、b與形參x、y的關系了。

(2)一個預備的常識
為了說明這個問題,我先給出一個代碼:
? ?int a = 4;
? ?int x;
? ?x = a;
? ?x = x + 3;
看好了沒,現在我問你:最終a值是多少,x值是多少?
(怎么搞的,給我這個小兒科的問題。還不簡單,不就是a = 4、x = 7嘛!)
在這個代碼中,你要明白一個東西:雖然a值賦給了x,但是a變量并不是x變量哦。我們對x任何的修改,都不會改變a變量。呵呵!雖然簡單,并且一看就理所當然,不過可是一個很重要的認識喔。

(3)理解值傳遞的形式
看調用Exch1函數的代碼:
main()
{
? ?int a = 4,b = 6;
? ?Exchg1(a, b) /* 這里調用了Exchg1函數 */
? ?printf("a = %d, b = %d.\n", a, b);
}
Exchg1(a, b)時所完成的操作代碼如下所示。
int x = a; /* ← */
int y = b; /* ← 注意這里,頭兩行是調用函數時的隱含操作 */
int tmp;
tmp = x;
x = y;
y = tmp;
請注意在調用執行Exchg1函數的操作中我人為地加上了頭兩句:
? ?int x = a;
? ?int y = b;
這是調用函數時的兩個隱含動作。它確實存在,現在我只不過把它顯式地寫了出來而已。問題一下就清晰起來啦。(看到這里,現在你認為函數里面交換操作的是a、b變量或者只是x、y變量呢?)

原來,其實函數在調用時是隱含地把實參a、b 的值分別賦值給了x、y,之后在你寫的Exchg1函數體內再也沒有對a、b進行任何的操作了。交換的只是x、y變量。并不是a、b。當然a、b的值沒有改變啦!函數只是把a、b的值通過賦值傳遞給了x、y,函數里頭操作的只是x、y的值并不是a、b的值。這就是所謂的參數的值傳遞了。

哈哈,終于明白了,正是因為它隱含了那兩個的賦值操作,才讓我們產生了前述的迷惑(以為a、b已經代替了x、y,對x、y的操作就是對a、b的操作了,這是一個錯誤的觀點啊!)。

三、函數參數傳遞方式之二:地址傳遞

繼續!地址傳遞的問題!
看考題二的代碼:
void Exchg2(int *px, int *py)
{
? ?int tmp = *px;
? ?*px = *py;
? ?*py = tmp;
? ?printf("*px = %d, *py = %d.\n", *px, *py);
}
main()
{
? ?int a = 4;
? ?int b = 6;
? ?Exchg2(&a, &b);
? ?printf("a = %d, b = %d.\n”, a, b);
? ?return(0);
}
它的輸出結果是:
*px = 6, *py = 4.
a = 6, b = 4.
看函數的接口部分:Exchg2(int *px, int *py),請注意:參數px、py都是指針。再看調用處:Exchg2(&a, &b);
它將a的地址(&a)代入到px,b的地址(&b)代入到py。同上面的值傳遞一樣,函數調用時作了兩個隱含的操作:將&a,&b的值賦值給了px、py。
? ?px = &a;
? ?py = &b;
呵呵!我們發現,其實它與值傳遞并沒有什么不同,只不過這里是將a、b的地址值傳遞給了px、py,而不是傳遞的a、b的內容,而(請好好地在比較比較啦)整個Exchg2函數調用是如下執行的:
? ?px = &a; /* ← */
? ?py = &b; /* ← 請注意這兩行,它是調用Exchg2的隱含動作。*/
? ?int tmp = *px;
? ?*px = *py;
? ?*py = tmp;
? ?printf("*px =%d, *py = %d.\n", *px, *py);
這樣,有了頭兩行的隱含賦值操作。我們現在已經可以看出,指針px、py的值已經分別是a、b變量的地址值了。接下來,對*px、*py的操作當然也就是對a、b變量本身的操作了。所以函數里頭的交換就是對a、b值的交換了,這就是所謂的地址傳遞(傳遞a、b的地址給了px、py),你現在明白了嗎?

四、函數參數傳遞方式之三:引用傳遞

看題三的代碼:
void Exchg3(int &x, int &y) /* 注意定義處的形式參數的格式與值傳遞不同 */
{
? ?int tmp = x;x = y;
? ?y = tmp;
? ?printf("x = %d, y = %d.\n", x, y);
}
main()
{
? ?int a = 4;
? ?int b = 6;
? ?Exchg3(a, b); /*注意:這里調用方式與值傳遞一樣*/
? ?printf("a = %d, b = %d.\n”, a, b);
}
輸出結果:
x = 6, y = 4.
a = 6, b = 4. /*這個輸出結果與值傳遞不同。*/
看到沒有,與值傳遞相比,代碼格式上只有一處是不同的,即在定義處:
? ?Exchg3(int &x, int &y)
但是我們發現a與b的值發生了對調。這說明了Exchg3(a, b)里頭修改的是a、b變量,而不只是修改x、y了。

我們先看Exchg3函數的定義處Exchg3(int &x, int &y)。參數x、y是int的變量,調用時我們可以像值傳遞(如: Exchg1(a, b); )一樣調用函數(如: Exchg3(a, b);)。但是x、y前都有一個取地址符號“&”。有了這個,調用Exchg3時函數會將a、b 分別代替了x、y了,我們稱:x、y分別引用了a、b變量。這樣函數里頭操作的其實就是實參a、b本身了,也就是說函數里是可以直接修改到a、b的值了。

最后對值傳遞與引用傳遞作一個比較:
1)在函數定義格式上有不同:
值傳遞在定義處是:Exchg1(int x, int y);
引用傳遞在這義處是:Exchg3(int &x, int &y);

2)調用時有相同的格式:
值傳遞:Exchg1(a, b);
引用傳遞:Exchg3(a, b);

3)功能上是不同的:
值傳遞的函數里操作的不是a、b變量本身,只是將a、b值賦給了x、y。函數里操作的只是x、y變量而不是a、b,顯示a、b的值不會被Exchg1函數所修改。
引用傳遞Exchg3(a, b)函數里是用a、b分別代替了x、y。函數里操作的就是a、b變量的本身,因此a、b的值可在函數里被修改的。


一、回顧指針概念

早在本書第貳篇中我就對指針的實質進行了闡述。今天我們又要學習一個叫做“指向另一指針地址”的指針。讓我們先回顧一下指針的概念吧!
當我們程序如下聲明變量:
? ?short int i;
? ?char a;
? ?short int * pi;
程序會在內存某地址空間上為各變量開辟空間,如下圖所示:
圖中所示中可看出:
? ?i 變量在內存地址5的位置,占2個字節。
? ?a變量在內存地址7的位置,占1個字節。
? ?pi變量在內存地址9的位置,占2個字節。(注:pi 是指針,我這里指針的寬度只有2個字節,32位系統是4個字節)
接下來如下賦值:
? ?i = 50;
? ?pi = &i;
經過上在兩句的賦值,變量的內存映象如下:
看到沒有:短整型指針變量pi的值為5,它就是i變量的內存起始地址。所以,這時當我們對*pi進行讀寫操作時,其實就是對i變量的讀寫操作。如:
? ?*pi=5; /* 就是等價于i = 5; */
你可以回看本書的第貳篇,那里有更加詳細的解說。

二、指針的地址與指向另一指針地址的指針

在上一節中,我們看到,指針變量本身與其它變量一樣也是在某個內存地址中的,如pi的內存起始地址是9。同樣的,我們也可能讓某個指針指向這個地址。看下面代碼:
? ?short int **ppi; /* 這是一個指向指針的指針,注意有兩個“*”號 */
? ?*ppi = &pi;
第一句:short int **ppi; —— 聲明了一個指針變量ppi,這個ppi是用來存儲(或稱指向)一個short int * 類型指針變量的地址。
第二句:&pi那就是取pi的地址,**ppi = &pi就是把pi的地址賦給了ppi。即將地址值9賦值給ppi。如下圖:
從圖中看出,指針變量ppi的內容就是指針變量pi的起始地址。于是……
? ?ppi的值是多少呢?—— 9。
? ?*ppi的值是多少呢?—— 5,即pi的值。
? ?**ppi的值是多少呢?——50,即i的值,也是*pi的值。
呵呵!不用我說太多了,我相信你應明白這種指針了吧!

三、一個應用實例

(1)設計一個函數:void find1(char array[], char search, char *pa)
要求:這個函數參數中的數組array是以0值為結束的字符串,要求在字符串array中查找字符是參數search里的字符。如果找到,函數通過第三個參數(pa)返回值為array字符串中第一個找到的字符的地址。如果沒找到,則為pa為0。

設計:依題意,實現代碼如下。
void find1(char array[], char search, char *pa)
{
? ?int i;
? ?for (i = 0; *(array + i) != 0; i++)
? ?{
? ? ? if ( *(array+i) == search)
? ? ? {
? ? ? ? ?pa = array + i;
? ? ? ? ?break;
? ? ? }
? ? ? else if (*(array+i) == 0)
? ? ? {
? ? ? ? ?pa = 0;
? ? ? ? ?break;
? ? ? }
? ?}
}
你覺得這個函數能實現所要求的功能嗎?

調試:我下面調用這個函數試試。
main()
{
? ?char str[] = {"afsdfsdfdf\0"}; /* 待查找的字符串 */
? ?char a = ’d’; /* 設置要查找的字符 */
? ?char *p = 0; /* 如果查找到后指針p將指向字符串中查找到的第1個字符的地址。 */
? ?find1(str, a, p); /* 調用函數以實現所要操作。 */
? ?if (0 == p)
? ?{
? ? ? printf("沒找到!\n"); /* 如果沒找到則輸出此句 */
? ?}
? ?else
? ?{
? ? ? printf("找到了,p = %d", p); /* 如果找到則輸出此句 */
? ?}
? ?return(0);
}
分析:上面代碼,你認為會是輸出什么呢?運行試試。
唉!怎么輸出的是:沒有找到!而不是“找到了,……”。

明明a值為’d’,而str字符串的第四個字符是’d’,應該找得到呀!
再看函數定義處:void find1(char array[], char search, char *pa)
看調用處:find1(str, a, p);

依我在第伍篇的分析方法,函數調用時會對每一個參數進行一個隱含的賦值操作。整個調用如下:
array = str;
search = a;
pa = p; /* 請注意:以上三句是調用時隱含的動作。*/
int i;
for(i =0; *(array+i) != 0; i++)
{
? ?if (*(array+i) == search)
? ?{
? ? ? pa = array + i;
? ? ? break;
? ?}
? ?else if (*(array+i)==0)
? ?{
? ? ? pa=0;
? ? ? break;
? ?}
}
哦!參數pa與參數search的傳遞并沒有什么不同,都是值傳遞嘛(小語:地址傳遞其實就是地址值傳遞嘛)!所以對形參變量pa值(當然值是一個地址值)的修改并不會改變實參變量p值,因此p的值并沒有改變(即p的指向并沒有被改變)。(如果還有疑問,再看一看《第五篇:函數參數的傳遞》了。)

修正:
void find2(char array[], char search, char **ppa)
{
? ?int i;
? ?for (i=0; *(array + i) != 0; i++)
? ?{
? ? ? if(*(array + i) == search)
? ? ? {
? ? ? ? ?*ppa = array + i;
? ? ? ? ?break;
? ? ? }
? ? ? else if(*(array + i) == 0)
? ? ? {
? ? ? ? ?*ppa = 0;
? ? ? ? ?break;
? ? ? }
? ?}
}
主函數的調用處改如下:
? ?find2(str, a, &p); /*調用函數以實現所要操作。*/
再分析:這樣調用函數時的整個操作變成如下:
array = str;
search = a;
ppa = &p; /* 請注意:以上三句是調用時隱含的動作。 */
int i;
for (i = 0; *(array + i) != 0; i++)
{
? ?if (*(array + i) == search)
? ?{
? ? ? *ppa = array + i
? ? ? break;
? ?}
? ?else if (*(array+i)==0)
? ?{
? ? ? *ppa=0;
? ? ? break;
? ?}
}
看明白了嗎?ppa指向指針p的地址。對*ppa的修改就是對p值的修改。你自行去調試。

經過修改后的程序就可以完成所要的功能了。看懂了這個例子,也就達到了本篇所要求的目的。

二、通常的函數調用

一個通常的函數調用的例子:
/* 自行包含頭文件 */
void MyFun(int x); /* 此處的聲明也可寫成:void MyFun(int) */
int main(int argc, char* argv[])
{
? ?MyFun(10); /* 這里是調用MyFun(10) 函數 */
? ?return(0);
}
void MyFun(int x) /* 這里定義一個MyFun函數 */
{
? ?printf("%d\n",x);
}
這個MyFun函數是一個無返回值的函數,它并不“完成”什么事情。這種調用函數的格式你應該是很熟悉的吧!看主函數中調用MyFun函數的書寫格式:
? ?MyFun(10);
我們一開始只是從功能上或者說從數學意義上理解MyFun這個函數,知道MyFun函數名代表的是一個功能(或是說一段代碼)。直到——學習到函數指針概念時。我才不得不在思考:函數名到底又是什么東西呢?

(不要以為這是沒有什么意義的事噢!呵呵,繼續往下看你就知道了。)

二、函數指針變量的聲明

就象某一數據變量的內存地址可以存儲在相應的指針變量中一樣,函數的首地址也以存儲在某個函數指針變量里的。這樣,我就可以通過這個函數指針變量來調用所指向的函數了。
在C系列語言中,任何一個變量,總是要先聲明,之后才能使用的。那么,函數指針變量也應該要先聲明吧?那又是如何來聲明呢?以上面的例子為例,我來聲明一個可以指向MyFun函數的函數指針變量FunP。下面就是聲明FunP變量的方法:
? ?void (*FunP)(int) ; /* 也可寫成void (*FunP)(int x)*/
你看,整個函數指針變量的聲明格式如同函數MyFun的聲明處一樣,只不過——我們把MyFun改成“(*FunP)”而已,這樣就有了一個能指向MyFun函數的指針FunP了。(當然,這個FunP指針變量也可以指向所有其它具有相同參數及返回值的函數了。)

三、通過函數指針變量調用函數

有了FunP指針變量后,我們就可以對它賦值指向MyFun,然后通過FunP來調用MyFun函數了。看我如何通過FunP指針變量來調用MyFun函數的:
/* 自行包含頭文件 */
void MyFun(int x); /* 這個聲明也可寫成:void MyFun( int )*/
void (*FunP)(int ); /*也可聲明成void(*FunP)(int x),但習慣上一般不這樣。 */
int main(int argc, char* argv[])
{
? ?MyFun(10); /* 這是直接調用MyFun函數 */
? ?FunP = &MyFun; /* 將MyFun函數的地址賦給FunP變量 */
? ?(*FunP)(20); /* (★)這是通過函數指針變量FunP來調用MyFun函數的。 */
}
void MyFun(int x) /* 這里定義一個MyFun函數 */
{
? ?printf("%d\n",x);
}
請看(★)行的代碼及注釋。運行看看。嗯,不錯,程序運行得很好。哦,我的感覺是:MyFun與FunP的類型關系類似于int 與int * 的關系。函數MyFun好像是一個如int的變量(或常量),而FunP則像一個如int * 一樣的指針變量。
? ?int i,*pi;
? ?pi = &i; /* 與FunP = &MyFun比較。*/
(你的感覺呢?)呵呵,其實不然……

四、調用函數的其它書寫格式

函數指針也可如下使用,來完成同樣的事情:
/* 自行包含頭文件 */
void MyFun(int x);
void (*FunP)(int );/* 聲明一個用以指向同樣參數,返回值函數的指針變量。 */
int main(int argc, char* argv[])
{
? ?MyFun(10); /* 這里是調用MyFun(10)函數 */
? ?FunP = MyFun; /* 將MyFun函數的地址賦給FunP變量 */
? ?FunP(20); /* (★)這是通過函數指針變量來調用MyFun函數的。*/
? ?return 0;
}
void MyFun(int x) //這里定義一個MyFun函數
{
? ?printf("%d\n",x);
}
我改了(★)行(請自行與之前的代碼比較一下)。運行試試,啊!一樣地成功。咦?
? ?FunP = MyFun;
可以這樣將MyFun值同賦值給FunP,難道MyFun與FunP是同一數據類型(即如同的int 與int的關系),而不是如同int 與int*的關系了?(有沒有一點點的糊涂了?)看來與之前的代碼有點矛盾了,是吧!所以我說嘛!

請容許我暫不給你解釋,繼續看以下幾種情況(這些可都是可以正確運行的代碼喲!):
代碼之三:
int main(int argc, char* argv[])
{
? ?MyFun(10); /* 這里是調用MyFun(10)函數 */
? ?FunP = &MyFun; /* 將MyFun函數的地址賦給FunP變量 */
? ?FunP(20); /* 這是通過函數指針變量來調用MyFun函數的。 */
? ?return 0;
}

代碼之四:
int main(int argc, char* argv[])
{
? ?MyFun(10); /* 這里是調用MyFun(10)函數 */
? ?FunP = MyFun; /* 將MyFun函數的地址賦給FunP變量 */
? ?(*FunP)(20); /*這是通過函數指針變量來調用MyFun函數的。*/
? ?return 0;
}
真的是可以這樣的噢!(哇!真是要暈倒了!)還有吶!看——
int main(int argc, char* argv[])
{
? ?(*MyFun)(10); /*看,函數名MyFun也可以有這樣的調用格式*/
? ?return 0;
}
你也許第一次見到吧:函數名調用也可以是這樣寫的啊!(只不過我們平常沒有這樣書寫罷了。)那么,這些又說明了什么呢?

呵呵!依據以往的知識和經驗來推理本篇的“新發現”,我想就連“福爾摩斯”也必定會由此分析并推斷出以下的結論:
1)其實,MyFun的函數名與FunP函數指針都是一樣的,即都是函數指針。MyFun函數名是一個函數指針常量,而FunP是一個函數數指針變量,這是它們的關系。
2)但函數名調用如果都得如(*MyFun)(10)這樣,那書寫與讀起來都是不方便和不習慣的。所以C語言的設計者們才會設計成又可允許MyFun(10)這種形式地調用(這樣方便多了并與數學中的函數形式一樣,不是嗎?)。
3)為統一起見,FunP函數指針變量也可以FunP(10)的形式來調用。
4)賦值時,即可FunP = &MyFun形式,也可FunP = MyFun。


上述代碼的寫法,隨便你愛怎么著!請這樣理解吧!這可是有助于你對函數指針的應用嘍!最后 ——

補充說明一點,在函數的聲明處:
? ?void MyFun(int); /*不能寫成void (*MyFun)(int)。*/
? ?void (*FunP)(int); /*不能寫成void FunP(int)。*/
(請看注釋)這一點是要注意的。

五、定義某一函數的指針類型

就像自定義數據類型一樣,我們也可以先定義一個函數指針類型,然后再用這個類型來聲明函數指針變量。
我先給你一個自定義數據類型的例子。
typedef int* PINT; /* 為int* 類型定義了一個PINT的別名*/
int main()
{
? ?int x;
? ?PINT px = &x; /* 與“int *px=&x;”是等價的。PINT類型其實就是int * 類型 */
? ?*px = 10; /* px就是int*類型的變量 */
? ?return 0;
}
根據注釋,應該不難看懂吧!(雖然你可能很少這樣定義使用,但以后學習Win32編程時會經常見到的。)下面我們來看一下函數指針類型的定義及使用:(請與上對照!)
/* 自行包含頭文件 */
void MyFun(int x); /*此處的聲明也可寫成:void MyFun( int )*/
typedef void (*FunType)(int); /*(★)這樣只是定義一個函數指針類型*/
FunType FunP; /*然后用FunType類型來聲明全局FunP變量*/
int main(int argc, char* argv[])
{
? ?FunType FunP; /*函數指針變量當然也是可以是局部的 ,那就請在這里聲明了。 */
? ?MyFun(10);
? ?FunP = &MyFun;
? ?return 0;
}
void MyFun(int x)
{
? ?printf("%d\n",x);
}
看(★)行:
首先,在void (*FunType)(int)前加了一個typedef 。這樣只是定義一個名為FunType函數指針類型,而不是一個FunType變量。
然后,“FunType FunP;”這句就如“PINT px;”一樣地聲明一個FunP變量。


其它相同。整個程序完成了相同的事。這樣做法的好處是:
有了FunType類型后,我們就可以同樣地、很方便地用FunType類型來聲明多個同類型的函數指針變量了。如下:
? ?FunType FunP2;
? ?FunType FunP3;
? ?/* . . . */

六、函數指針作為某個函數的參數

既然函數指針變量是一個變量,當然也可以作為某個函數的參數來使用的。所以,你還應知道函數指針是如何作為某個函數的參數來傳遞使用的。

給你一個實例:
要求:我要設計一個CallMyFun函數,這個函數可以通過參數中的函數指針值不同來分別調用MyFun1、MyFun2、MyFun3這三個函數(注:這三個函數的定義格式應相同)。
實現:代碼如下:
/* 自行包含頭文件 */
void MyFun1(int x);
void MyFun2(int x);
void MyFun3(int x);
typedef void (*FunType)(int ); /* ②. 定義一個函數指針類型FunType,與①函數類型一致 */
void CallMyFun(FunType fp,int x);
int main(int argc, char* argv[])
{
? ?CallMyFun(MyFun1,10); /* ⑤. 通過CallMyFun函數分別調用三個不同的函數 */
? ?CallMyFun(MyFun2,20);
? ?CallMyFun(MyFun3,30);
}
void CallMyFun(FunType fp,int x) /* ③. 參數fp的類型是FunType。*/
{
? ?fp(x);/* ④. 通過fp的指針執行傳遞進來的函數,注意fp所指的函數是有一個參數的。 */
}
void MyFun1(int x) /* ①. 這是個有一個參數的函數,以下兩個函數也相同。 */
{
? ?printf("函數MyFun1中輸出:%d\n",x);
}
void MyFun2(int x)
{
? ?printf("函數MyFun2中輸出:%d\n",x);
}
void MyFun3(int x)
{
? ?printf("函數MyFun3中輸出:%d\n",x);
}
輸出結果:略分析:看我寫的注釋。你可按我注釋的①②③④⑤順序自行分析。指針為C語言編程提供了強大的支持——如果你能正確而靈活地利用指針,你就可以直接切入問題的核心,或者將程序分割成一個個片斷。一個很好地利用了指針的程序會非常高效、簡潔和精致。

利用指針你可以將數據寫入內存中的任意位置,但是,一旦你的程序中有一個野指針("wild”pointer),即指向一個錯誤位置的指針,你的數據就危險了——存放在堆中的數據可能會被破壞,用來管理堆的數據結構也可能會被破壞,甚至操作系統的數據也可能會被修改,有時,上述三種破壞情況會同時發生。此后可能發生的事情取決于這樣兩點:
第一,內存中的數據被破壞的程度有多大;
第二,內存中的被破壞的部分還要被使用多少次。

在有些情況下,一些函數(可能是內存分配函數、自定義函數或標準庫函數)將立即(也可能稍晚一點)無法正常工作。在另外一些情況下,程序可能會終止運行并報告一條出錯消息;或者程序可能會掛起;或者程序可能會陷入死循環;或者程序可能會產生錯誤的結果;或者程序看上去仍在正常運行,因為程序沒有遭到本質的破壞。

值得注意的是,即使程序中已經發生了根本性的錯誤,程序有可能還會運行很長一段時間,然后才有明顯的失常表現;或者,在調試時,程序的運行完全正常,只有在用戶使用時,它才會失常。???

在C語言程序中,任何野指針或越界的數組下標(out-of-bounds array subscript)都可能使系統崩潰。兩次釋放內存的操作也會導致這種結果。你可能見過一些C程序員編寫的程序中有嚴重的錯誤,現在你能知道其中的部分原因了。

有些內存分配工具能幫助你發現內存分配中存在的問題,例如漏洞(leak,見7.21),兩次釋放一個指針,野指針,越界下標,等等。但這些工具都是不通用的,它們只能在特定的操作系統中使用,甚至只能在特定版本的編譯程序中使用。如果你找到了這樣一種工具,最好試試看能不能用,因為它能為你節省許多時間,并能提高你的軟件的質量。

指針的算術運算是C語言(以及它的衍生體,例如C++)獨有的功能。匯編語言允許你對地址進行運算,但這種運算不涉及數據類型。大多數高級語言根本就不允許你對指針進行任何操作,你只能看一看指針指向哪里。

C指針的算術運算類似于街道地址的運算。假設你生活在一個城市中,那里的每一個街區的所有街道都有地址。街道的一側用連續的偶數作為地址,另一側用連續的奇數作為地址。如果你想知道River Rd.街道158號北邊第5家的地址,你不會把158和5相加,去找163號;你會先將5(你要往前數5家)乘以2(每家之間的地址間距),再和158相加,去找River Rd.街道的168號。同樣,如果一個指針指向地址158(十進制數)中的一個兩字節短整型值,將該指針加3=5,結

果將是一個指向地址168(十進制數)中的短整型值的指針(見7.7和7.8中對指針加減運算的詳細描述)。???

街道地址的運算只能在一個特定的街區中進行,同樣,指針的算術運算也只能在一個特定的數組中進行。實際上,這并不是一種限制,因為指針的算術運算只有在一個特定的數組中進行才有意義。對指針的算術運算來說,一個數組并不必須是一個數組變量,例如函數malloc()或calloc()的返回值是一個指針,它指向一個在堆中申請到的數組。

指針的說明看起來有些使人感到費解,請看下例:
??? ?char *p;???
上例中的說明表示,p是一個字符。符號“*”是指針運算符,也稱間接引用運算符。當程序間接引用一個指針時,實際上是引用指針所指向的數據。???

在大多數計算機中,指針只有一種,但在有些計算機中,指向數據和指向函數的指針可以是不同的,或者指向字節(如char。指針和void *指針)和指向字的指針可以是不同的。這一點對sizeof運算符沒有什么影響。但是,有些C程序或程序員認為任何指針都會被存為一個int型的值,或者至少會被存為一個long型的值,這就無法保證了,尤其是在IBM PC兼容機上。

注意:以下討論與Macintosh或UNIX程序員無關。

最初的IBM PC兼容機使用的處理器無法有效地處理超過16位的指針(人們對這種結論仍有爭議。16位指針是偏移量,見9.3中對基地址和偏移量的討論)。盡管最初的IBM PC機最終也能使用20位指針,但頗費周折。因此,從一開始,基于IBM兼容機的各種各樣的軟件就試圖沖破這種限制。

為了使20位指針能指向數據,你需要指示編譯程序使用正確的存儲模式,例如緊縮存儲模式。在中存儲模式下,你可以用20位指針指向函數。在大和巨存儲模式下,用20位指針既可以指向數據,也可以指向函數。在任何一種存儲模式下,你都可能需要用到far指針(見7.18和7.19)。

基于286的系統可以沖破20位指針的限制,但實現起來有些困難。從386開始,IBM兼容機就可以使用真正的32位地址了,例如象MS-Windows和OS/2這樣一些操作系統就實現了這一點,但MS—DOS仍未實現。

如果你的MS—DOS程序用完了基本內存,你可能需要從擴充內存或擴展內存中分配更多的內存。許多版本的編譯程序和函數庫都提供了這種技術,但彼此之間有所差別。這些技術基本上是不通用的,有些能在絕大多數MS-DOS和MS-WindowsC編譯程序中使用,有些只能在少數特定的編譯程序中使用,還有一些只能在特定的附加函數庫的支持下使用。如果你手頭有能提供這種技術的軟件,你最好看一下它的文檔,以了解更詳細的信息。



對已說明的變量來說,變量名就是對變量值的直接引用。對指向變量或內存中的任何對象的指針來說,指針就是對對象值的間接引用。如果p是一個指針,p的值就是其對象的地址;*p表示“使間接引用運算符作用于p”,*p的值就是p所指向的對象的值。
??? *p是一個左值,和變量一樣,只要在*p的右邊加上賦值運算符,就可改變*p的值。如果p是一個指向常量的指針,*p就是一個不能修改的左值,即它不能被放到賦值運算符的左邊,請看下例:

例 7.1 一個間接引用的例子
#include <stdio.h>
int
main()
{
??? int i;
??? int? * p ;
??? i = 5;
??? p = & i;???????? / *? now? * p = = i? * /
??? / *?? %Pis described in FAQ VII. 28 * /
??? printf("i=%d, p=%P,?? * p= %d\n" , i, P,? *p);?
??? * p = 6;??????? / *? same as i = 6? * /
??? printf("i=%d, p=%P,?? * p= %d\n" , i, P,? *P);
??? return 0;?????? / *? see FAQ XVI. 4? * / }
}
上例說明,如果p是一個指向變量i的指針,那么在i能出現的任何一個地方,你都可以用*p代替i。在上例中,使p指向i(p=&i)后,打印i或*p的結果是相同的;你甚至可以給*p賦值,其結果就象你給i賦值一樣。


對這個問題的回答與“指針的層數”所指的意思有關。如果你是指“在說明一個指針時最多可以包含幾層間接引用”,答案是“至少可以有12層”。請看下例:
int??????? i = 0;
int??????? * ip0l = &d;
int??????? ** ip02 = &ip01;
int??????? ***ip03 = &ip02;
int??????? **** ip04 = &dp03;
int??????? ***** ip05 = &ip04;
int??????? ****** ip06 = &ip05;
int??????? ******* ip07 = &ip06;
int??????? ******** ip08 = &ip07;
int??????? ********* ip09 = &ip08;
int??????? **********ip10 = &ip09;
int??????? ***********ipll = &ip10;
int??????? ************ ip12 = &ipll;
************ ip12 = 1;???????? / *? i = 1? * /

注意:ANSIC標準要求所有的編譯程序都必須能處理至少12層間接引用,而你所使用的編譯程序可能支持更多的層數。

如果你是指“最多可以使用多少層指針而不會使程序變得難讀”,答案是這與你的習慣有關,但顯然層數不會太多。一個包含兩層間接引用的指針(即指向指針的指針)是很常見的,但超過兩層后程序讀起來就不那么容易了,因此,除非需要,不要使用兩層以上的指針。

如果你是指“程序運行時最多可以有幾層指針”,答案是無限層。這一點對循環鏈表來說是非常重要的,因為循環鏈表的每一個結點都指向下一個結點,而程序能一直跟住這些指針。請看下例:

例7.2一個有無限層間接引用的循環鏈表
/ *? Would run forever if you didn't limit it to MAX? * /
# include <stdio. h>
struct circ_list
{
??? char??????? value[ 3 ];???????? /*? e.g.,"st" (incl '\0')? */
??? struct circ_list??? *? next;
};
struct circ_list?? suffixes[ ] =? {
??? "th" ,?&.suffixes[ 1 ], / *? Oth? * /
??? "st" , &.suffixes[ 2 ], / *? 1st? * /
??? "nd" , & suffixes[ 3 ], / *? 2nd? * /
??? "rd" , & suffixes[ 4 ], / *? 3rd? * /
??? "th",? &.suffixes[ 5 ], / *? 4th? * /
??? "th" , &.suffixes[ 6 ], / *? 5th? * /
??? "th" , & suffixes[ 7 ], / *? 6th? * /
??? "th" , & suffixes[ 8 ], / *? 7th? * /
??? "th",? & suffixes[ 9 ], / *? 8th? * /
??? "th" , & suffixes[ 0 ], / *? 9th? * /
};
# define? MAX? 20

main()
{
??? int i = 0;
??? struct circ_list???????? *p = suffixes;
??? while (i <=MAX) {
??????? printf("%ds%\n", i, p->value);
??????? + +i;
??????? p = p->next;?
??? }
}

在上例中,結構體數組suffixes的每一個元素都包含一個表示詞尾的字符串(兩個字符加上末尾的NULL字符)和一個指向下一個元素的指針,因此它有點象一個循環鏈表;next是一個指針,它指向另一個circ_list結構體,而這個結構體中的next成員又指向另一個circ_list結構體,如此可以一直進行下去。

上例實際上相當呆板,因為結構體數組suffixes中的元素個數是固定的,你完全可以用類似的數組去代替它,并在while循環語句中指定打印數組中的第(i%10)個元素。循環鏈表中的元素一般是可以隨意增減的,在這一點上,它比上例中的結構體數組suffixes要有趣一些。有時,在程序中需要使用這樣一種指針,它并不指向任何對象,這種指針被稱為空指針。空指針的值是NULL,NULL是在<stddef.h>中定義的一個宏,它的值和任何有效指針的值都不同。NULL是一個純粹的零,它可能會被強制轉換成void*或char*類型。即NULL可能是0,0L或(void*)0等。有些程序員,尤其是C++程序員,更喜歡用0來代替NULL。

指針的值不能是整型值,但空指針是個例外,即空指針的值可以是一個純粹的零(空指針的值并不必須是一個純粹的零,但這個值是唯一有用的值。在編譯時產生的任意一個表達式,只要它是零,就可以作為空指針的值。在程序運行時,最好不要出現一個為零的整型變量)。

注意:空指針并不一定會被存為零。絕對不能間接引用一個空指針,否則,你的程序可能會得到毫無意義的結果,或者得到一個全部是零的值,或者會突然停止運行。


空指針有以下三種用法:

(1)用空指針終止對遞歸數據結構的間接引用。
遞歸是指一個事物由這個事物本身來定義。請看下例:
??? /*Dumb implementation;should use a loop */
??? unsigned factorial(unsinged i)
??? {
????? ? if(i=0 || i==1)
????? ? {
?????????? return 1;
????? ? }
????? ? else
????? ? {
????????? ? return i * factorial(i-1);
????? ? }
??? }

在上例中,階乘函數factoriai()調用了它本身,因此,它是遞歸的。

一個遞歸數據結構同樣由它本身來定義。最簡單和最常見的遞歸數據結構是(單向)鏈表,鏈表中的每一個元素都包含一個值和一個指向鏈表中下一個元素的指針。請看下例:
??? struct string_list???
??? {???
??????? char? *str;? /* string(inthiscase)*/
??????? struct string_list??? *next;???
??? };
此外還有雙向鏈表(每個元素還包含一個指向鏈表中前一個元素的指針)、鍵樹和哈希表等許多整潔的數據結構,一本較好的介紹數據結構的書中都會介紹這些內容。

你可以通過指向鏈表中第一個元素的指針開始引用一個鏈表,并通過每一個元素中指向下一個元素的指針不斷地引用下一個元素;在鏈表的最后一個元素中,指向下一個元素的指針被賦值為NULL,當你遇到該空指針時,就可以終止對鏈表的引用了。請看下例:
??? while(p!=NULL)???
??? {
?????? /*dO something with p->str*/
?????? p=p->next;
??? }
請注意,即使p一開始就是一個空指針,上例仍然能正常工作。

(2)用空指針作函數調用失敗時的返回值。
許多C庫函數的返回值是一個指針,在函數調用成功時,函數返回一個指向某一對象的指針;反之,則返回一個空指針。請看下例:
??? if(setlocale(cat,loc_p)==NULL)
??? {
??????? /* setlocale()failed;do something*/
??????? /*? ...*/
??? }
返回值為一指針的函數在調用成功時幾乎總是返回一個有效指針(其值不等于零),在調用失敗時則總是返回一個空指針(其值等于零);而返回值為一整型值的函數在調用成功時幾乎總是返回一個零值,在調用失敗時則總是返回一個非零值。請看下例:
??? if(raise(sig)!=0){
??????? /* raise()failed;do something*/
??????? /*? ...? */
??? }
對上述兩類函數來說,調用成功或失敗時的返回值含義都是不同的。另外一些函數在調用成功時可能會返回一個正值,在調用失敗時可能會返回一個零值或負值。因此,當你使用一個函數之前,應該先看一下它的返回值是哪種類型,這樣你才能判斷函數返回值的含義。

(3)用空指針作警戒值???
警戒值是標志事物結尾的一個特定值。例如,main()函數的預定義參數argv是一個指針數組,它的最后一個元素(argv[argc])永遠是一個空指針,因此,你可以用下述方法快速地引用argv中的每一個元素:

/*
??? A simple program that prints all its arguments.
???? It doesn't use argc ("argument count"); instread.
???? it takes advantage of the fact that the last?
??? value in argv ("argument vector") is a null pointer.
*/
# include <stdio. h>
# include <assert. h>
int
main ( int argc, char? * * argv)
{
??? int i;
??? printf ("program name = \"%s\"\n", argv[0]);
??? for (i=l; argv[i] !=NULL; ++i)
??????? printf ("argv[%d] = \"%s\"\n", i, argv[f]);
??? assert (i = = argc) ;???????? / *? see FAQ XI. 5? * /
??? return 0;???????????????????? / * see FAQ XVI. 4? * /
}

void指針是什么??

void指針一般被稱為通用指針或泛指針,它是C關于“純粹地址(raw address)”的一種約定。void指針指向某個對象,但該對象不屬于任何類型。請看下例:
??? int??? *ip;
??? void??? *p;
??? 在上例中,ip指向一個整型值,而p指向的對象不屬于任何類型。
??? 在C中,任何時候你都可以用其它類型的指針來代替void指針(在C++中同樣可以),或者用void指針來代替其它類型的指針(在C++中需要進行強制轉換),并且不需要進行強制轉換。例如,你可以把char *類型的指針傳遞給需要void指針的函數。

什么時候使用void指針?

當進行純粹的內存操作時,或者傳遞一個指向未定類型的指針時,可以使用void指針。void指針也常常用作函數指針。

有些C代碼只進行純粹的內存操作。在較早版本的C中,這一點是通過字符指針(char *)實現的,但是這容易產生混淆,因為人們不容易判斷一個字符指針究竟是指向一個字符串,還是指向一個字符數組,或者僅僅是指向內存中的某個地址。

例如,strcpy()函數將一個字符串拷貝到另一個字符串中,strncpy()函數將一個字符串中的部分內容拷貝到另一個字符串中:
??? char? *strepy(char'strl,const char *str2);
??? char? *strncpy(char *strl,const char *str2,size_t n);???
??? memcpy()函數將內存中的數據從一個位置拷貝到另一個位置:
??? void? *memcpy(void *addrl,void *addr2,size_t n);
???
??? memcpy()函數使用了void指針,以說明該函數只進行純粹的內存拷貝,包括NULL字符(零字節)在內的任何內容都將被拷貝。請看下例:
??? #include "thingie.h"??? /* defines struct thingie */
??? struct thingie *p_src,*p_dest;
??? /*? ...? */
??? memcpy(p_dest,p_src,sizeof(struct thingie) * numThingies);

在上例中,memcpy()函數要拷貝的是存放在structthingie結構體中的某種對象op_dest和p_src都是指向structthingie結構體的指針,memcpy()函數將把從p_src指向的位置開始的sizeof(stuctthingie) *numThingies個字節的內容拷貝到從p_dest指向的位置開始的一塊內存區域中。對memcpy()函數來說,p_dest和p_src都僅僅是指向內存中的某個地址的指針。如果兩個指針向同一個數組,它們就可以相減,其為結果為兩個指針之間的元素數目。仍以本章開頭介紹的街道地址的比喻為例,假設我住在第五大街118號,我的鄰居住在第五大街124號,每家之間的地址間距是2(在我這一側用連續的偶數作為街道地址),那么我的鄰居家就是我家往前第(124-118)/2(或3)家(我和我的鄰居家之間相隔兩家,即120號和122號)。指針之間的減法運算和上述方法是相同的。

在折半查找的過程中,同樣會用到上述減法運算。假設p和q指向的元素分別位于你要找的元素的前面和后面,那么(q-p)/2+p指向一個位于p和q之間的元素。如果(q-p)/2+p位于你要找的元素之前,下一步你就可以在(q-p)/2+p和q之間查找要找的元素;反之,你可以停止查找了。

如果兩個指針不是指向一個數組,它們相減就沒有意義。假設有人住在梅恩大街110號,我就不能將第五大街118號減去梅恩大街110號(并除以2),并以為這個人住在我家往回第4家中。

如果每個街區的街道地址都從一個100的倍數開始計算,并且同一條街的不同街區的地址起址各不相同,那么,你甚至不能將第五大街204號和第五大街120號相減,因為它們盡管位于同一條街,但所在的街區不同(對指針來說,就是所指向的數組不同)。

C本身無法防止非法的指針減法運算,即使其結果可能會給你的程序帶來麻煩,C也不會給出任何提示或警告。

指針相減的結果是某種整類型的值,為此,ANSIC標準<stddef.h>頭文件中預定義了一個整類型ptrdiff_t。盡管在不同的編譯程序中ptrdiff_t的類型可能各不相同(int或long或其它),但它們都適當地定義了ptrdiff_t類型。

例7.7演示了指針的減法運算。該例中有一個結構體數組,每個結構體的長度都是16字節。

如果是對指向結構體數組的指針進行減法運算,則a[0]和a[8]之間的距離為8;如果將指向結構體數組的指針強制轉換成指向純粹的內存地址的指針后再相減,則a[0]和aL8]之間的距離為128(即十六進制數0x80)。如果將指向a[8]的指針減去8,該指針所指向的位置并不是往前移了8個字節,而是往前移了8個數組元素。
???
注意:把指針強制轉換成指向純粹的內存地址的指針,通常就是轉換成void *類型,但是,本例將指針強制轉換成char *類型,因為void。類型的指針之間不能進行減法運算。

例 7.7 指針的算術運算

# include <stdio. h>
# include <stddef.h>

struct stuff {
??? char???name[l6];
??? / *? other stuff could go here, too? * /
};
struct stuff array [] =? {
??? { "The"? },
??? { "quick"? },
??? { "brown"? >,
??? { "fox" },
??? { "jumped"? },
??? { "over"? },
??? { "the"? },
??? { "lazy"? },
??? { "dog. "? },
??? /*
??? an empty string signifies the end;
??? not used in this program,
??? but without it, there'd be no way
??? to find the end (see FAQ IX. 4)
??? */
??? { " " }
};
main ( )
{
??? struct stuff???????? * p0 = &.array[0];
??? struct stuff???????? * p8 = &-array[8];
??? ptrdiff_t?????????? diff = p8-p0;
??? ptrdiff_t????????? addr.diff = (char * ) p8 - (char * ) p0;
??? /*
??? cast the struct stuff pointers to void *
??? (which we know printf() can handles see FAQ VII. 28)
??? */
??? printf ("&array[0] = p0 = %P\n" ,? (void* ) p0);
??? printf ("&. array[8] = p8 = %P\n" , (void* ) p8) ;?
??? */
??? cast the ptrdiff_t's to long's
??? (which we know printf () can handle)?
??? */
??? printf ("The difference of pointers is %ld\n" , (long) diff) ;
??? printf ("The difference of addresses is %ld\n" , (long) addr_diff);
??? printf ("p8-8 = %P\n" , (void*) (p8-8));
??? / *? example for FAQ VII. 8? * /
??? printf ("p0 + 8 =? %P (same as p8)\n",? (void* ) (p0 + 8));
??? return 0;??? / *? see FAQ XVI. 4? * /
}



當把一個整型值加到一個指針上后,該指針指向的位置就向前移動了一段距離。就純粹的內存地址而言,這段距離對應的字節數等于該值和該指針所指向的對象的大小的乘積;但是,就C指針真正的工作機理而言,這段距離對應的元素數等于該整型值。

在例7.7末尾,當程序將8和&array[o]相加后,所得的指針并不是指向&array[0]后的第8個字節,而是第8個元素。

仍以本章開頭介紹的街道地址的比喻為例,假設你住在沃克大街744號,在你這一側用連續的偶數作為街道地址,每家之間的地址間距是2。如果有人想知道你家往前第3家的地址,他就會先將2和3相乘,然后將6和你家的地址相加,得到他想要的地址750號。同理,你家往回第1家的地址是774+(-1)*2,即742號。???

街道地址的算術運算只有在一個特定的街區中進行才有意義,同樣,指針的算術運算也只有在一個特定的數組中進行才有意義。仍以上一段所介紹的背景為例,如果你想知道你家往回第400家的地址,你將得到沃克大街-56號,但這是一個毫無意義的地址。如果你的程序中使用了一個毫無意義的地址,你的程序很可能會被徹底破壞。


NULL總是被定義為0嗎?

NULL不是被定義為o,就是被定義為(void *)0,這兩種值幾乎是相同的。當程序中需要一個指針時(盡管編譯程序并不是總能指示什么時候需要一個指針),一個純粹的零或者一個void指針都能自動被轉換成所需的任何類型的指針。

NULL總是等于0嗎?

對這個問題的回答與“等于”所指的意思有關。如果你是指“與。比較的結果為相等”,例如:
??? if(/*? ...? */)
??? {???
??????? p=NULL;
??? }???
??? else???
??? {
??????? p=/* something else */;
??? }
??? /*? ...? */
??? if(p==0)
那么NULL確實總是等于0,這也就是空指針定義的本質所在。

如果你是指“其存儲方式和整型值。相同”,那么答案是“不”。NULL并不必須被存為一個整型值0,盡管這是NULL最常見的存儲方式。在有些計算機中,NULL會被存成另外一些形式。

如果你想知道NULL是否被存為一個整型值0,你可以(并且只能)通過調試程序來查看空指針的值,或者通過程序直接將空指針的值打印出來(如果你將一個空指針強制轉換成整類型,那么你所看到的很可能就是一個非零值)。

當把一個指針作為條件表達式時,所要判斷的條件實際上就是“該指針是否為一空指針”。在if,while,for或do/while等語句中,或者在條件表達式中,都可以使用指針。請看下例:
??? if(p)
??? {???????
??????? /*dO something*/
??? }???
??? else
??? {
??????? /* dOsomethingelse */
??? }

當條件表達式的值不等于零時,if語句就執行“then”子句(即第一個子句),即“if(/*something*/)”和“if(/*something*/!=0)”是完全相同的。因此,上例和下例也完全相同:
??? if(p !=0)
??? {
??????? /* dO something(not anull pointer)*/
??? }
??? else
??? {
??????? /* dOsomethingelse(a null pointer)*/
??? }
以上兩例中的代碼不易讀,但經常出現在許多C程序中,你不必編寫這樣的代碼,但要理解這些代碼的作用。兩個指針是不能相加的。仍以街道地址的比喻為例,假設你住在湖濱大道1332號,你的鄰居住在湖濱大道1364號,那么1332+1364指的是什么呢?其結果是一個毫無意義的數字。如果你的C程序試圖將兩個指針相加,編譯程序就會發出警告。

當你試圖將一個指針和另外兩個指針的差值相加的時候,你很可能會誤將其中的兩個指針相加,例如,你很可能會使用下述語句:
??? p=p+p2-p1;

上述語句是不正確的,因為它和下述語句完全相同:
??? p=(p+p2)-p1;
正確的語句應該是:
??? p=p+(p2-p1);
對此例來說,使用下述語句更好:
??? p+=p2-p1;


在使用指向函數的指針時,最難的一部分工作是說明該指針。例如,strcmp()函數的說明如下所示:
??? int strcmp(const char*,const char*);???

如果你想使指針pf指向strcmp()函數,那么你就要象說明strcmp()函數那樣來說明pf,但此時要用*pf代替strcmp:
??? int (*pr)(const char*,const char*);

請注意,*pf必須用括號括起來,因為
??? int *p{? (constchar? *? ,constchar? *? );??? /*? wrong? */
等價于
??? (int? *)pr(const char? *,const char? *? );??? /*? wrong? */
它們都只是說明了一個返回int *類型的函數。

在說明了pf后,你還要將<string.h>包含進來,并且要把strcmp()函數的地址賦給pf,即:
??? pf=strcmp;

??? pf=Slstrcmp;? /* redundant& */
此后,你就可以通過間接引用pf來調用strcmp()函數:
??? if(pr(strl,str2)>0)? /*...*/



函數的指針可以作為一個參數傳遞給另外一個函數,這一點非常有意思。一個函數用函數指針作參數,意味著這個函數的一部分工作需要通過函數指針調用另外的函數來完成,這被稱為“回調(callback)”。處理圖形用戶接口的許多C庫函數都用函數指針作參數,因為創建顯示風格的工作可以由這些函數本身完成,但確定顯示內容的工作需要由應用程序完成。

舉一個簡單的例子,假設有一個由字符指針組成的數組,你想按這些指針指向的字符串的值對這些指針進行排序,你可以使用qsort()函數,而qsort()函數需要借助函數指針來完成這項任務(關于排序的詳細介紹請參見第3章“排序和查找”。qsort()函數有4個參數:????
??? (1) 指向數組開頭的指針;
??? (2) 數組中的元素數目;
??? (3) 數組中每個元素的大小;
??? (4) 指向一個比較函數的指針。???
qsort()函數返回一個整型值。

比較函數有兩個參數,分別為指向要比較的兩個元素的指針。當要比較的第一個元素大于、等于或小于第二個元素時,比較函數分別返回一個大于o,等于。或小于。的值。一個比較兩個整型值的函數可能如下所示:
??? int icmp(const int? *p1,const int? *p2)???
??? {???
??????? return *p1-*p2;
??? }
排序算法和交換算法都是qsort()函數的部分內容。qsort()函數的交換算法代碼只負責拷貝指定數目的字節(可能調用memcpy()或memmove()函數),因此qsort()函數不知道要對什么樣的數據進行排序,也就不知道如何比較這些數據。比較數據的工作將由函數指針所指向的比較函數來完成。

對本例來說,不能直接用strcmp()函數作比較函數,其原因有兩點:第一,strcmp()函數的類型與本例不符(見下文中的介紹);第二,srtcmp()函數不能直接對本例起作用。strcmp()函數的兩個參數都是字符指針,它們都被strcmp()函數看作是字符串中的第一個字符;本例要處理的是字符指針(char *s),因此比較函數的兩個參數必須都是指向字符指針的指針。本例最好使用下面這樣的比較函數;
??? int strpcmp(const void *p1,const void *p2)
??? {
??????? char? * const? *sp1? =? (char? *? const? *)p1;
??????? char'const *sp2=(char *const *)p2;
??????? return strcmp(*sp1,*sp2);
??? }

本例對qsort()函數的調用可以如下所示:
??? qsort(array,numElements,sizeof(char *),pf2);
這樣,每當qsort()函數需要比較兩個字符指針時,它就可以調用strpcmp()函數了。

為什么不能直接將strcmp()函數傳遞給qsort()函數呢?為什么strpcmp()函數中的參數是如此一種形式呢?因為函數指針的類型是由它所指向的函數的返回值類型及其參數的數目和類型共同決定的,而qsort()函數要求比較函數含兩個const void *類型的參數:
?? void qsort(void *base,
??? size_t numElernents,
??? size_t sizeOfElement,
??? int(*compFunct)(const void *,const void *));???

qsort()函數不知道要對什么樣的數據進行排序,因此,base參數和比較函數中的兩個參數都是void指針。這一點很容易理解,因為任何指針都能被轉換成void指針,并且不需要強制轉換。但是,qsort()函數對函數指針參數的類型要求就苛刻一些了。本例要排序的是一個字符指針數組,盡管strcmp()函數的比較算法與此相符,但其參數的類型與此不符,所以在本例中strcmp()函數不能直接被傳給qsort()函數。在這種情況下,最簡單和最安全的方法是將一個參數類型符合qsort()函數的要求的比較函數傳給qsort()函數,而將比較函數的參數強制轉換成strcmp()函數所要求的類型后再傳給strcmp()函數;strpcmp()函數的作用正是如此。

不論C程序在什么樣的環境中運行,char *類型和void。類型之間都能進行等價的轉換,因此,你可以通過強制轉換函數指針類型使qsort()函數中的函數指針參數指向strcmp()函數,而不必另外定義一個strpcmp()這樣的函數,例如:
??? char??? table[NUM_ELEMENTS][LEMENT_SIZE);
??? /*? ...? */
??? /*? passing strcmp() to qsort for array Of array Of char? */
??? qsort(table,NUM_ELEMENTS,ELEMENT_SIZE,
??? (int(*)(const void *,const void *))strcmp);
不管是強制轉換strpcmp()函數的參數的類型,還是強制轉換指向strcmp()函數的指針的類型,你都必須小心進行,因為稍有疏忽,就會使程序出錯。在實際編程中,轉換函數指針的類型更容易使程序出錯。


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/450501.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/450501.shtml
英文地址,請注明出處:http://en.pswp.cn/news/450501.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

springboot_yml配置

server: port: 8097 session-timeout: 30 tomcat.max-threads: 0 tomcat.uri-encoding: UTF-8 spring: application: name: feedback # 數據庫配置 datasource: name: test #url: jdbc:mysql://192.168.0.180:3306/dsdb #url: jdbc:m…

Python 字符串查找子串的方法之 index() 和 find()

find(substr, beg0, endlen(string)): 在[beg, end]范圍內查找substring&#xff0c;找到返回substr的起始下標&#xff0c;否則返回 -1。 1 string Hello Python 2 print(string.find(h, 0, len(string))) # 輸出 9 3 print(string.find(thon) # 輸出 84 print(strin.find(…

sqlite 設置向下遞增

因為 sqlite是沒有floor函數的 所以用 cast(x as int) - (x < cast(x as int)) 原理 cast(1.1 as int) 1 cast(-1.1 as int) -1&#xff08;我們想要的結果為2&#xff09; 如果x為非負 cast(x as int)是可以的 但是有負的話就加上后面 (x < cast(x as int))…

Docker 是什么,組成

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 一、Docker 是一個開源的應用容器引擎&#xff0c;讓開發者可以打包他們的應用以及依賴包到一個可移植的容器中&#xff0c;然后發布到任…

VC6.0詳細教程

1 編制并運行一個簡單程序 1.1 編制并運行程序的“四步曲”1.2 工程(PROJECT)以及工程工作區(PROJECT WORKSPACE)1.3 啟動并進入VC6的集成開發環境1.4 創建工程并輸入源程序代碼(1)新建一Win32 Console Application工程(2)在工作區窗口中查看工程的邏輯架構(3)在工程中新建C源…

vim搜索替換工具

1、ag.vim(查找工具)安裝 在vimrc中添加 Plug rking/ag.vim 使用 :Ag 你要查找的內容 e open file and close the quickfix window. o open file (same as enter). go preview file (open but mainta…

JAVA好學嗎?工資待遇怎么樣?

時代的進步也離不開互聯網的推動&#xff0c;互聯網的發展已經形成了一種趨勢&#xff0c;而在這種大環境下我們不應該抗拒&#xff0c;如果自身條件允許&#xff0c;加入進來未嘗不是一個好的選擇。而在計算機行業內&#xff0c;JAVA的火爆毋庸自疑&#xff0c;很多人都意識到…

【project】十次方-01

前言 項目介紹 系統分為3大部分&#xff1a;微服務、網站前臺、網站管理后臺&#xff1b;功能模塊分為&#xff1a;問答、招聘、交友中心等 該項目融合了Docker容器化部署、第三方登陸、SpringBoot、SpringCloud、SpringData、RabbitMQ等&#xff0c;該項目采用完全的前后端分離…

Docker 容器 和 虛擬機 的異同

見&#xff1a;https://www.docker.com/what-container 將軟件打包成標準化的單元進行開發&#xff0c;發貨和部署 容器映像是一個軟件的輕量級獨立可執行軟件包&#xff0c;包含運行所需的所有內容&#xff1a;代碼&#xff0c;運行時&#xff0c;系統工具&#xff0c;系統庫&…

iTerm2 快捷鍵大全

轉載地址https://cnbin.github.io/blog/2015/06/20/iterm2-kuai-jie-jian-da-quan/ 標簽 新建標簽&#xff1a;command t關閉標簽&#xff1a;command w切換標簽&#xff1a;command 數字 command 左右方向鍵切換全屏&#xff1a;command enter查找&#xff1a;command …

大型軟件編程規范

“安全第一”的C語言編程規范 編者按&#xff1a;C語言是開發嵌入式應用的主要工具&#xff0c;然而C語言并非是專門為嵌入式系統設計&#xff0c;相當多的嵌入式系統較一般計算機系統對軟件安全性有更苛刻的要求。1998年&#xff0c;MISRA指出&#xff0c;一些在C看來可以接受…

設計行業的新寵——云渲染

無論是對任職設計崗位的人員還是專業的設計公司來說&#xff0c;3D渲染&#xff0c;都是工作中極其重要的一步。在做過渲染后&#xff0c;設計的作品才能展現出它最接近真實世界的狀態。 但是由于渲染實質上是對大量數據的處理&#xff0c;所以渲染這項工作對電腦硬件的要求非常…

[Xcode 實際操作]七、文件與數據-(17)解析JSON文檔

目錄&#xff1a;[Swift]Xcode實際操作 本文將演示如何解析JSON文檔。 項目中已添加一份JSON文檔&#xff1a;menu.json 1 {2 "menu":3 {4 "id": "file",5 "value": "File",6 "menuit…

Docker,容器,虛擬機和紅燒肉

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 Docker是什么&#xff1f;容器又是什么&#xff1f; Docker 直譯&#xff1a;碼頭工人。是在碼頭上搬運貨物的工人容器 英文&#xff1…

mac終端操作文件或文件夾(持續更新)

1、復制文件夾&#xff08;有文件&#xff09; cp -R 要復制的文件 要復制到哪個路徑 2、復制文件 cp 要復制的文件 要復制到哪個路徑 3、移動文件夾 mvdir 你要移動的文件夾 要移動到哪里

前端進階系列(六):盒模型

盒模型是界面布局需要掌握的基本功。盒模型基本概念 盒模型四要素&#xff1a;margin、border、padding、content。 盒模型分為&#xff1a;標準盒模型&#xff08;W3C盒模型&#xff09; 、 怪異盒模型&#xff08;IE盒模型&#xff09; 盒模型區別 怪異盒模型總寬度 content…

holer實現外網訪問內網數據庫

外網訪問本地數據庫 本地安裝了數據庫&#xff0c;只能在局域網內訪問&#xff0c;怎樣從公網也能訪問內網數據庫&#xff1f; 本文將介紹使用holer實現的具體步驟。 1. 準備工作 1.1 安裝并啟動數據庫 默認安裝的數據庫端口是3306。 2. 實現步驟 2.1 下載并解壓holer軟件包 Ho…

Docker 概念解析

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 Docker的功能定位Docker為基于Linux容器的開源項目&#xff0c;其利用Linux內核中的各項功能——例如命名空間與控制組——以在操作系統之…

C語言 常用API

MySQL的C語言API接口 1、首先當然是連接數據庫&#xff0c;函數原型如下&#xff1a; MYSQL * STDCALL mysql_real_connect(MYSQL *mysql, const char *host,const char *user,const char *passwd,const char *db,unsigned int port,const char *unix_socket,unsigned long …