譚浩強C程序設計講到,如果想交換兩個變量a,b的值,我們會這麼做:
func(int* a,int* b)
{
int temp;
temp = *a;
*a = *b;
*b = *a;
}
func(&a,&b);
因爲C語言單向“值傳遞”就這樣規定的,想改變外部實參a和b的值,你就要傳實參的地址纔行,沒有可理解的,你就是要按照規定來,就像沒一個漢字讀什麼,都是最初的規定者規定的。
指針就有這個特權,當函數參數是一個一重指針的時候,那麼傳入的就應該是變量的地址。
那麼在函數的內部就可以直接通過修改該地址中存放的值。這樣就能修改到外部變量的值了。
現在進一步思考,如果函數內部不是修改變量的值,而是指針的值及變量地址的值,又會怎樣呢?沒錯這回就得用到二重指針。
可能有人會問,修改指針的值是想幹嘛?修改指針的值的另一種說法就是:
1、將A指針指向另一個B指針變量
1.1 譬如int c=5;int* A=NULL;int *B=c;你現在想將A指向指針B,一般我們直接使用A=B;即可,如果你想封裝成函數呢?這時就要傳入指針A的地址,才能改變A的指向。
func(int *B,int** A)
{
(*A)=B;
}
void main()
{
int c=5;int* A=NULL;int *B=c;
func(B,&A);//這樣其實等同於A=B;你會問爲什麼一行代碼可以解決的還需要函數封裝?是爲了模塊化,是因爲這個例子比較簡單
}
1.2 如果你形參定義成int *A,傳入func(A,B)則無法改變指針A的指向。比如 int* p = a,如果需要將其封裝成函數,就會造成在函數中修改指針值的情況。假設一個功能函數需要爲指針分配一個新的空間:
func(int* a)
{
a = malloc(10);
}
void main()
{
int* p;
func(p);
}
func(p)執行完了之後,指針p仍然沒有分配到新的空間。變的只是函數內部臨時變量指針a的值,p並沒有改變。
要想改變P的值:有如下2種方式:
(1)那麼要傳入的參數就是指針的地址值:&p 。func(&p);這樣函數內部就能修改外部指針的值。
func(int** a)
{
(*a) = malloc(10);
}
void main()
{
int *p = NULL;
func(&p);
}
(2)使用函數返回值,因爲函數返回時,返回值還是有效的,所以我們在該函數外部定義一個指針p,去接收這個func函數返回的臨時指針,接受完之後,該func函數就結束了,裏面的臨時變量和返回值都內存釋放了,因爲他們是放在棧上,而malloc申請的內存不會釋放,因爲在堆上,但是指針變量a會釋放,這是就會造成內存泄漏,因爲現在指向malloc的指針a出函數體後,a會釋放,所以這時需要外部通過一個指針去接收,釋放是也用這個指針p。
char *func(void)
{
char *a=(char *)malloc(100);
return a;
}
void main()
{
char *p=NULL;
p = Getmemery();
strcpy(p,"hello world");
printf("%s",p);
free(p);
}
總結:
函數內部修改外部變量的值,需要一重指針;
函數內部修改外部指針變量的值,需要二重指針;
2 變量的引用過程
如下代碼中,定義了一個全局指針變量B和一個main函數內的局部變量B,首先要知道這是可以的,因爲變量作用域不同,main函數中的臨時變量B當main函數結束後就會釋放,而全局變量的作用域是整個文件中的所以函數,這時問題就來了
(1)**A的打印值是多少?
第13行指針B指向變量c
第16行將指針(*A)指向指針B //注意A是一個二重指針,所以將(*A)看成一個整體就是一重指針,也就是將二重指針進行 一次解引用就變成了一重指針
第15行打印(**A) //**A就是兩次解引用後,變成了變量的數組,就是一重指針(*A)的解引用
實際上**A的值爲8,變量的引用過程,類似於棧,就是說編譯器從變量引用處以最近邏輯距離開始搜索,找到後則開始使用,然後停止搜索;如果查找失敗則編譯出錯。舉個例子,就是優先引用函數內部的,其次引用同一個源文件中全局的,如果函數內部、同一個源文件中沒有,再搜索包含的頭文件(譬如另一個文件中external int *p聲明,然後其他文件包含了這個文件)
如下代碼:func功能函數形參沒有出現指針B,而func裏面卻使用了B,則這個B就會使用全局變量B,即第5行代碼的指向。雖然第14行執行時,第13行對B進行了操作,但沒有意義,因爲func形參沒有B,所以他會找全局變量B,你會問不是說就近原則嗎?13行與14行纔是最近的。要知道這裏的就近是在函數定義時,你想啊,爲什麼func函數裏面使用了指針變量B能通過?是因爲main函數裏面12行代碼定義的嗎,不是的,而是第5行代碼定義了,所以func纔不會報錯,所以就近原則是指第5/6行。而如果將func改成void func(int *B,int **A),就不一樣了,因爲形參B出現,這是第13行傳入的B就有意義了,func函數裏面出現的B就會聽從形參傳過來的變量,而形參中的變量是在13行定義的,就近原則是指第13/14行。
1 #include "stdio.h"
2 #include <string.h>
3 #include <unistd.h>
4 int C = 8;
5 int *B = &C;
6 void func(int *d,int **A);
7 void main()
{
8 int c = 6;
9 int *d[5] = {NULL};
10 int **A = NULL;
11 A = d;
12 int *B;
13 B = &c;
14 func(B,A);
15 printf("%d%d",**A,*A[1]);
}
void func(int *d,int **A)
{
16 A[0] = B;
17 A[1] = B;
}
3 爲什麼我會使用**A,與*A[1],因爲他們能替換,*A == A[0],A是二重指針,所以A[0]一重指針,對一重指針解引用不就是*A[0]嗎
如果代碼如下:首先func(int *B,int **A),這時全局B無效,第4行改成了一重指針A,所以我們要傳入&A,即指針取地址,就是二重指針
疑問1:爲什麼A[0] = B,B是指針,第4行定義的A是指針,A[0]是啥?
#include "stdio.h"
#include <string.h>
#include <unistd.h>
1int C = 8;
2int *B = &C;
void func(int *d,int **A);
void main()
{
3 int c = 6;
4 int *A = NULL;
5 int *B;
6 B = &c;
7 func(B,&A);
8 printf("%d%d\n",A[0],A[1]);
9 printf("%p\n",A);
printf("%p\n",&A[1]);
}
void func(int *B,int **A)
{
10 A[0] = B;
11 A[1] = B;
}
這時要注意,A[0]中的A是一個二重指針,他是形參,是外部的指針A取地址傳入的,不等價,所以A[0] = B後,外部的指針A被改變了,因爲我們傳了指針A的地址進去,儘管第4行指針A是一個一重指針,要改變他的指向就要就要傳入他的地址,記住這個就行了,而A[1] = B卻無效,A[1]只是函數內臨時分配的一個指針變量,因爲我們傳入了指針A的地址,可以說A[1]已經存在了,就在&A賦值時定義了,但是他在外部沒有分配空間,而第10行A[0]就是外部傳入的一重指針A,而第8行中的A[0]與第10行的A[0]不一樣,第8行就是代表第4行的指針A的解引用,也可以寫成*A,而第8行的A[1]的值是未知的,打印後,得到如下圖,指針是後續的,但是裏面的值是隨機的,func函數裏面的A[1]指向了指針B,但是A[1]本身是一個臨時變量,沒有再外部定義實體,沒有外部實體與之對應,而A[0]對應的就是指針A。所以打印A[1]的值時是隨機的,而指針的地址&A[1]與A是連貫的。
4 創建二叉樹時傳參問題
(1)如下代碼中,Createtree4(BinaryTree* p,char *str,int *index),我們傳入的是二重指針p,和一重指針index,爲什麼?因爲我們要改變一重指針(*p)的指向以及變量(*index)的值,所以傳入他們的地址進入才能改變,一重指針的地址就是二重指針。
typedef struct BtNode
{
struct BtNode *leftchild;
struct BtNode *rightchild;
char Ltag;char Rtag;
ElemType data;
}BtNode,*BinaryTree;
void Createtree4(BinaryTree* p,char *str,int *index)
{
if(str[*index] != '#')
{
i++;
(*p) = Buynode();
(*p)->data = str[*index];
*index += 1;
printf("T->data = %c\n",(*p)->data);
Createtree4(&(*p)->leftchild,str,index);
Createtree4(&(*p)->rightchild,str,index);
}
}
(2)如下代碼中序遍歷進行中序線索化,傳入的是一重指針p,爲什麼?因爲線索化二叉樹之前已經創建過二叉樹了,也就是下面的代碼是以上面的代碼爲基礎,所以線索化不需要再使用malloc申請內存了,不需要改變一重指針的指向,p一直指向的是之前創建二叉樹時指向的malloc申請的內存地址,線索化只是改變這個內存地址裏面元素的值,所以一重指針的傳入正是爲了改變值對吧,比如p->Ltag = 1;等操作
void InTree(BinaryTree p)
{
if(p)
{
InTree(p->leftchild);
if(!p->leftchild)
{
p->Ltag = 1;
}
InTree(p->rightchild);
}
}