徹底搞定C語言指針

kavacan 13年前發布 | 3K 次閱讀 PMD Rocks
前言
    指針是C的靈魂,正是指針使得C存在了這么多年,而且將長期存在下去。事實上,我自己不用C語言寫程序已經有一年了,工作中接觸到的只有java,python和javascript.最近用C完成了一下類似于OO中的封裝(即"類")的概念,順便把指針復習了下,感覺有必要記一下。
    本文中的例子有這樣兩個概念:任務(Task),執行器(Executor)。任務有名稱(taskName),并且可以執行(execute)。 而執行器與具體任務所執行的內容無關,只是回調(callback)任務的執行方法,這樣我們的執行器就可以做的比較通用。而任務接口只需要實現一個execute方法即可,這樣我們的任務就可以是多種多樣的,可以通過統一的接口set給執行器執行。這是面向對象中基本的思想,也是比較常用的抽象方式。下面我們具體看下例子。
    可以想象,main函數大概是這個樣子:
 int main(int argc, char** argv) {
    Task *t1 = TaskConstruction("Task1", run);//此處的run是一個函數指針
    Executor *exe = ExecutorConstruction();
    exe->setTask(t1);
    exe->begin();
    exe->cancel();
    Task *t2 = TaskConstruction("Task2", run2);//此處的run2也是一個函數指針,用于構造一個Task.
    exe->setTask(t2);
    exe->begin();
    exe->cancel();
    
    return (EXIT_SUCCESS);
}
    運行結果為:
 task : [Task1] is ready to run
[a = 1.200000, b = 2.300000]
[(a + b) * (a - b) = -3.850000]
cancel is invoked here
task : [Task2] is ready to run
another type of execute,just print out some information
cancel is invoked here
 好了,下面詳細看看實現:
    定義接口
    首先,定義Task和Executor兩個實體的接口:
    Task接口,注意其中的_this字段,這個指針在后邊有很重要的作用,用于hold整個Task的實例。然后是一個taskName的字符串,和一個函數指針,這個指針在初始化(構造)Task時傳入。這個execute()函數比較有意思,它不在內部使用,而是讓執行器回調執行的。
 #ifndef _ITASK_H
#define    _ITASK_H
typedef struct Task{
    struct Task *_this;
    char *taskName;
    void (*execute)();
}Task;
void execute();
#endif    /* _ITASK_H */
    執行器接口比Task接口復雜一些,其中包含_this指針,包含一個對Task的引用,然后是對外的接口begin(), cancel()。對接口的使用者來說,他們只需要調用接口實例上的setTask(),將任務傳遞給執行器,然后在適當時期調用begin(),等待任務正常結束或者調用cancel()將其取消掉。
 #include "ITask.h"
#ifndef _IEXECUTOR_H
#define    _IEXECUTOR_H
typedef struct Executor{
    struct Executor *_this;
    Task *task;
    char *(*setTask)(Task* task);
    void (*begin)();
    void (*cancel)();
}Executor;
char *setTask(Task *task);
void begin();
void cancel();
#endif /* _IEXECUTOR_H */
    實現接口
 #include <stdlib.h>
#include "ITask.h"
Task *task = NULL;
void execute();
/*
 * The construction of Task object.
 * name : the name of the task
 * execute : execute method of the task
 * 
 */
Task *TaskConstruction(char *name, void (*execute)()){
    task = (Task*)malloc(sizeof(strlen(name))+sizeof(execute));
    task->taskName = name;
    task->execute = execute;
    task->_this = task;
    
    return (Task*)task;//返回一個自身的指針,通過內部的_this指針,兩者即可實現封裝
}
/*
 * Destruction of task, not used current time.
 *
 */
void TaskDestruction(){
    task->taskName = NULL;
    task->execute = NULL;
    task->_this = NULL;
    task = NULL;
}
/*
 * private method, should register to executor
 *
 */
void execute(){
    task->_this->execute();//調用_this上的execute()方法
}

 執行器的實現一樣,稍微復雜一點,構造的時候,將函數指針在內部設置好,當外部調用時動態的執行需要執行的函數,這句話可能有些繞口,這么看:在構造Executor的時候,executor->begin = begin; 這條語句是將下面void begin()的實現注冊到結構體中,但是要執行什么還是不確切的,當setTask以后,回調函數的地址已經明確:(executor->_this->task = task;),此時調用begin()即可正確的調用到注冊的Task上。
 #include <stdlib.h>
#include "IExecutor.h"
Executor *executor = NULL;
Executor *ExecutorConstruction(){
    executor = (Executor*)malloc(sizeof(Executor));
    executor->begin = begin;
    executor->cancel = cancel;
    executor->setTask = setTask;
    executor->_this = executor;
    return (Executor*)executor;
}
void ExecutorDestruction(){
    executor->begin = NULL;
    executor->cancel = NULL;
    executor->setTask = NULL;
    executor = NULL;
}
char *setTask(Task *task){
    executor->_this->task = task;
}
void begin(){
    printf("task : [%s] is ready to runn",executor->_this->task->taskName);
    executor->_this->task->execute();
}
void cancel(){//這個函數沒有實現,只是做了一個占位符,以后如果有多線程,可以用來停止主動線程。
    printf("cancel is invoked heren");
}
    其實,兩個實現的代碼都不算復雜,如果對C的指針理解的稍好,基本就沒什么問題了。
    在C中使用OO
    為了試驗,我們不妨設計兩個不同的Task,一個Task是計算兩個數的某四則混合運算,另一個僅僅是用來打印一點信息。然后我們可以看到,他們使用完全相同的接口來執行:
 #include <stdio.h>
void run(){//計算(a+b)*(a-b)
    float a, b, r;
    a = 1.2;
    b = 2.3;
    r = 0.0;
    printf("[a = %f, b = %f]n", a, b);
    printf("[(a + b) * (a - b) = %f]n",((a+b)*(a-b)));
}
void run2(){//打印一句話,事實上,這些函數可以做任何事,比如I/O,網絡,圖片處理,音樂播放等等。
    printf("another type of execute,");
    printf("just print out some informationn");
}
    然后,在Main中獎他們注冊給Task,代碼如下所示:
 #include <stdio.h>
#include <stdlib.h>
#include "ITask.h"
#include "IExecutor.h"
extern void run();
extern void run2();
int main(int argc, char** argv) {
//代碼的風格上,應該可以看出和OO的風格及其類似。
    Task *t1 = TaskConstruction("Task1", run);//new Task("Task 1", run);
    Executor *exe = ExecutorConstruction();// new Executor();
    exe->setTask(t1);
    exe->begin();
    exe->cancel();
 
    Task *t2 = TaskConstruction("Task2", run2);
    exe->setTask(t2);
    exe->begin();
    exe->cancel();
    
    return (EXIT_SUCCESS);
}
    程序的輸出結果上文中已經可以看到了,這里就不貼了。
    當然,本文的主要目的不是想說什么“C也可以實現面向對象”之類的幼稚觀點,只要誰沒有嚴重的自虐傾向,相信不會有誰真的會用C來做OO的開發。只是想表達一下,指針在C中的重要性和指針的一點高級用法。其實現在的OO語言,基本還是以面向過程的表達式來表達面向對象而已。并沒有什么神奇之處,OO主要是思想上的抽象,可以說是語言無關的(language independent)。


概述
    Joel Spolsky認為,對指針的理解是一種aptitude,不是通過訓練就可以達到的。雖然如此,我還是想談一談這個C/C++語言中最強勁也是最容易出錯的要素。
    鑒于指針和目前計算機內存結構的關聯,很多C語言比較本質的特點都孕育在其中,因此,本篇和第六、第七兩篇我都將以指針為主線,結合在實際編程中遇到的問題,來詳細談談關于指針的幾個重要方面。
    指針類型的本質分析
    1、指針的本質
    指針的本質:一種復合的數據類型。下面我將以下面幾個作為例子進行展開分析:
    a)、int *p;b)、int **p;c)、int (*parValue)[3];d)、int (*pFun)();
    分析:
    所謂的數據類型就是具有某種數據特征的東東,比如數據類型char,它的數據特征就是它所占據的內存為1個字節, 指針也很類似,指針所指向的值也占據著內存中的一塊地址,地址的長度與指針的類型有關,比如對于char型指針,這個指針占據的內存就是1個字節,因此指針也是一種數據類型,但我們知道指針本身也占據了一個內存空間地址,地址的長度和機器的字長有關,比如在32位機器中,這個長度就是4個字節,因此指針本身也同樣是一種數據類型,因此,我們說,指針其實是一種復合的數據類型,
    好了,現在我們可以分析上面的幾個例子了。
    假設有如下定義:
    int nValue;那么,nValue的類型就是int,也就是把nValue這個具體變量去掉后剩余的部分,因此,上面的4個聲明可以類比進行分析:
    a)、int *
    *代表變量(指針本身)的值是一個地址,int代表這個地址里面存放的是一個整數,這兩個結合起來,int *定義了一個指向整數的指針,類推如下:
    b)、int **
    指向一個指向整數的指針的指針。
    c)、int (*)[3]
    指向一個擁有三個整數的數組的指針。
    d)、int (*)()
    指向一個函數的指針,這個函數參數為空,返回值為整數。
    分析結束,從上面可以看出,指針包括兩個方面,一個是它本身的值,是一個內存中的地址;另一個是指針所指向的物,是這個地址中所存放著具有各種各樣意義的數據。
    2、對指針本身值的分析
    下面例子考察指針本身的值(環境為32位的計算機):
    void *p = malloc( 100 );請計算sizeof ( p ) = ?
    char str[] = “Hello” ;char *p = str ;
    請計算sizeof ( p ) = ?
    void Func ( char str[100])
    {
    請計算 sizeof( str ) = ? //注意,此時,str已經退化為一個指針,詳情見
    //下一篇指針與數組
    }
    分析:上面的例子,答案都是4,因為從上面的討論可以知道,指針本身的值對應著內存中的一個地址,它的size只與機器的字長有關(即它是由系統的內存模型決定的),在32位機器中,這個長度是4個字節。
    3、對指針所指向物的分析
    現在再對指針這個復合類型的第二部分,指針所指向物的意義進行分析。
    上面我們已經得到了指針本身的類型,那么將指針本身的類型去掉 “*”號就可得到指針所指向物的類型,分別如下:
    a)、int
    所指向物是一個整數。
    b)、int*
    所指向物是一個指向整數的指針。
    c)、int ()[3]
    ()為空,可以去掉,變為int [3],所指向物是一個擁有三個整數的數組。
    d)、int ()()
    第一個()為空,可以去掉,變為int (),所指向物是一個函數,這個函數的參數為空,返回值為整數。
    4、附加分析
    另外,關于指針本身大小的問題,在C++中與C有所不同,這里我也順帶談一下。
    在C++中,對于指向對象成員的指針,它的大小不一定是4個字節,這主要是因為在引入多重虛擬繼承以及虛擬函數的時候,有些附加的信息也需要通過這個指針進行傳遞,因此指向對象成員的指針會增大,不論是指向成員數據,還是成員函數都是如此,具體與編譯器的實現有關,你可以編寫個很小的C++程序去驗證一下。另外,對一個類的靜態成員(static member,可以是靜態成員變量或者靜態成員函數)來說,指向它的指針只是普通的函數指針,而不是一個指向類成員的指針,所以它的大小不會增加,仍舊是4個字節。

指針運算符&和*
    “&和*”,它們是一對相反的操作,‘&’取得一個物的地址(也就是指針本身),‘*’得到一個地址里放的物(指針所指向的物)。這個東西可以是值(對象)、函數、數組、類成員(class member)等等。
    參照上面的分析我們可以很好地理解&與*.
    使用指針的好處?
    關于指針的本質和基本的運算符我們討論過了,在這里,我想再籠總地談一談使用指針的必要性和好處,為我們今后的使用和對后面篇章的理解做好鋪墊。簡而言之,指針有以下好處:
    1)、方便使用動態分配的數組。
    這個解釋我放在本系列第六篇中進行講解。
    2)、對于相同類型(甚至是相似類型)的多個變量進行通用訪問。
    就是用一個指針變量不斷在多個變量之間指來指去,從而使得非常應用起來非常靈活,不過,這招也比較危險,需要小心使用:因為出現錯誤的指針是編程中非常忌諱的事情。
    3)、變相改變一個函數的值傳遞特性。
    說白了,就是指針的傳地址作用,將一個變量的地址作為參數傳給函數,這樣函數就可以修改那個變量了。
    4)、節省函數調用代價。
    我們可以將參數,尤其是大個的參數(例如結構,對象等),將他們地址作為參數傳給函數,這樣可以省去編譯器為它們制作副本所帶來的空間和時間上的開銷。
    5)、動態擴展數據結構。
    因為指針可以動態地使用malloc/new生成堆上的內存,所以在需要動態擴展數據結構的時候,非常有用;比如對于樹、鏈表、Hash表等,這幾乎是必不可少的特性。
    6)、與目前計算機的內存模型相對應,可按照內存地址進行直接存取,這使得C非常適合于一些較底層的應用。
    這也是C/C++指針一個強大的優點,我會在后面講述C語言的底層操作時,較詳細地介紹這個優點的應用。
    7)、遍歷數組。
    據個例子來說吧,當你需要對字符串數組進行操作時,想一想,你當然要用字符串指針在字符串上掃來掃去。
    …實在太多了,你可以慢慢來補充^_^.
    指針本身的相關問題
    1、問題:空指針的定義
    曾經看過有的。h文件將NULL定義為0L,為什么?
    答案與分析:
    這是一個關于空指針宏定義的問題。指針在C語言中是經常使用的,有時需要將一個指針置為空指針,例如在指針變量初始化的時候。
    C語言中的空指針和Pascal或者Lisp語言中的NIL具有相同的地位。那如何定義空指針呢?下面的語句是正確的:
    char *p1 = 0;int *p2;if (p != 0)
    {……
    } p2 = 0;也就是說,在指針變量的初始化、賦值、比較操作中,0會被編譯器理解為要將指針置為空指針。至于空指針的內部表示是否是0,則隨不同的機器類型而定,不過通常都是0.但是在另外一些場合下,例如函數的參數原型是指針類型,函數調用時如果將0作為參數傳入,編譯器則不能將其理解為空指針。此時需要明確的類型轉換,例如:
    void func (char *p);func ((char *)0);一般情況下,0是可以放在代碼中和指針關聯使用的,但是有些程序員(數量還不少呦!也許就包括你在內)不喜歡0的直白,認為其不能表示作為指針的特殊含義,于是要定義一個宏NULL,來明確表示空指針常量。這也是對的,人家C語言標準就明確說:“ NULL應該被定義為與實現相關的空指針常量”。但是將NULL定義成什么樣的值呢?我想你一定見過好幾種定義NULL的方法:


 #define NULL 0
    #define NULL (char *)0
    #define NULL (void *)0
    在我們使用的絕大多數計算系統上,例如PC,上述定義是能夠工作的。然而,世界上還有很多其它種類的計算機,其CPU也不是Intel的。在某些系統上,指針和整數的大小和內部表示并不一致,甚至不同類型的指針的大小都不一致。為了避免這種可移植性問題,0L是一種最為安全的、最妥帖的定義方式。0L的含義是: “值為0的整數常量表達式”。這與C語言給出的空指針定義完全一致。因此,建議采用0L作為空指針常量NULL的值。
    其實 NULL定義值,和操作系統的的平臺有關, 將一個指針定義為 NULL, 其用意是為了保護操作系統,因為通過指針可以訪問任何一塊地址, 但是,有些數據是不許一般用戶訪問的,比如操作系統的核心數據。 當我們通過一個空(NULL)的指針去方位數據時,系統會提示非法, 那么系統又是如何知道的呢??
    以windows2000系統為例, 該系統規定系統中每個進程的起始地址(0x00000000)開始的某個地址范圍內是存放系統數據的,用戶進程無法訪問, 所以當用戶用空指針(0)訪問時,其實訪問的就是0x00000000地址的系統數據,由于該地址數據是受系統保護的,所以系統會提示錯誤(指針訪問非法)。
    這也就是說NULL值不一定要定義成0,起始只要定義在系統的保護范圍的地址空間內,比如定義成(0x00000001, 0x00000002)都會起到相同的作用,但是為了考慮到移植性,普遍定義為0 .
    2、問題:與指針相關的編程規則&規則分析
    指針既然這么重要,而且容易出錯,那么有沒有方法可以很好地減少這些指針相關問題的出現呢?
    答案與分析:
    減少出錯的根本是徹底理解指針。
    在方法上,遵循一定的編碼規則可能是最立竿見影的方法了,下面我來闡述一下與指針相關的編程規則:
    1) 未使用的指針初始化為NULL .
    2) 在給指針分配空間前、分配后均應作判斷。
    3) 指針所指向的內容刪除后也要清除指針本身。
    要牢記指針是一個復合的數據結構這個本質,所以我們不論初始化和清除都要同時兼顧指針本身(上述規則1,3)和指針所指向的內容(上述規則2,3)這兩個方面。
    遵循這些規則可以有效地減少指針出錯,我們來看下面的例子:
 void Test(void)
{
 char *str = (char *) malloc(100);
 strcpy(str, “hello”);
 free(str);
 if(str != NULL)
 {
  strcpy(str, “world”);
  printf(str);
 }
}
    請問運行Test函數會有什么樣的結果?
    答:
    篡改動態內存區的內容,后果難以預料,非常危險。因為free(str);之后,str成為野指針,if(str != NULL)語句不起作用。
    如果我們牢記規則3,在free(str)后增加語句:
    str = NULL;
    那么,就可以防止這樣的錯誤發生。

int download_addr;
void abc(void)
{
download_addr = 0x0c400000;
void (*fun)(void);
fun = (void (*)(void))download_addr;
(*fun)();
}
    解釋:
    download_addr 是一個函數指針
    void (*fun)(void); 定義一個函數指針func 輸入參數為void返回類型為void
    fun = (void (*)(void))download_addr;
    這句話是將download_addr這個函數指針強制轉換為參數為void返回類型為void的函數指針,然后賦值給func
    最后一句就是要執行這個函數

我們可以定義指針變量指向任何類型的變量。在上述的處理過程中,指針變量指向的變量通過傳遞變量的地址來實現。指針變量的取值是內存的地址,這個地址應當是安全的,不可以是隨意的,否則,寫入內存單元的值將會使得已存放的數據或程序丟失。應使用編譯系統提供的標準函數來實現地址分配。
    ansi標準建議設置了兩個最常用的動態分配內存的函數malloc()和free(),并包含在stdlib.h中,但有些c編譯卻使用malloc.h包含。使用時請參照具體的c編譯版本。
    我們這里所指的動態內存分配其含義是指:當定義指針變量時,其變量的取值是隨機的,可能指向內存的任一單元。若指針的指向是不安全的內存地址,在該地址空間上的數據交換就會產生意料不到的效果。為此,在程序的執行過程中,要保證指針操作的安全性,就要為指針變量分配安全地址。在程序執行時為指針變量所做的地址分配就稱之為動態內存分配。
    當無需指針變量操作時,可以將其所分配的內存歸還系統,此過程我們稱之為內存單元的釋放。
    malloc()用以向編譯系統申請分配內存;free()用以在使用完畢釋放掉所占內存。
    [例6-21]兩個字符串的交換。
 #include<stdlib.h>
#include<string.h>
#include<stdio.h>
main()
{
char *ptr1,*ptr2,*temp;
ptr1=malloc(30);/*動態為指針變量分配長度為30字節的存儲空間*/
ptr2=malloc(20);
temp=malloc(30);
printf(\"inputstr1:\");
gets(ptr1);/*輸入字符串*/
printf(\"inputstr2:\");
gets(ptr2);
printf(\"str1------------str2\\n\");
printf(\"%s.......%s\\n\",ptr1,ptr2);
strcpy(temp,ptr1);/*串復制*/
strcpy(ptr1,ptr2);
strcpy(ptr2,temp);
printf(\"str1------------str2\\n\");
printf(\"%s.......%s\\n\",ptr1,ptr2);
free(ptr1);
free(ptr2);
}
    為指針變量分配的存儲空間長度取決于存放字符的多少。在上述的程序中,兩個串的交換可以通過標準函數strcpy()來完成,也可以通過串指針交換指向完成,用temp=ptr1;ptr1=ptr2;ptr2=temp;三條賦值語句實現。但是,利用指針交換指向,其物理意義與串通過函數進行的復制完全不同。前者是存放串地址的指針變量數據交換,后者是串在內存物理空間的數據交換。指針變量用完后,將指針變量所占的存儲空間釋放。

平時我們使用電腦時,會遇到自己喜歡的壁紙被修改;IE選項被修改等種種情況,這些情況的改變為自己使用電腦帶來了諸多不便。如果想要把這些修改回來就需要動用其他軟件來完成,那有沒有什么方法能夠讓我們防患于未然,在被修改之前就將其禁止呢?利用Windows XP自帶的組策略就可以實現,只需要對相關的選項進行設置就可以輕松做到不被修改。
    如何啟動組策略
  啟動組策略時,單擊“開始”按鈕,選擇“運行”命令,在“運行”文本框中輸入“gpedit.msc”命令,即可啟動組策略。
    我的壁紙你別改
  我們經常把自己喜歡的圖片設置為桌面壁紙,不過有時候設置好的壁紙會被別人修改。想要禁止別人修改自己的壁紙需要什么方法呢?其實可以利用組策略來實現。組策略中有禁止修改壁紙的相關選項,只需要進行相應的設置就可以了。
  在組策略中,提供了一個“Active Desktop壁紙”策略,通過這個策略允許用戶設置桌面上的墻紙并防止用戶更改壁紙及其外觀。
  設置方法為:在組策略窗口左側的“本地計算機策略”中依次展開 “用戶配置→管理模版→桌面→Active Desktop”(圖1)。隨后在右窗口中雙擊“Active Desktop壁紙”策略項,彈出一個設置對話框,首先點選“已啟用”單選項,這時下面的文本框已被激活,其中在“壁紙名稱”項中鍵入墻紙圖像文件所在的文件夾和名稱(圖2)。并且在下面的“壁紙樣式”中指定墻紙是否居于中央、是否平鋪或拉伸等設置,在此我們可以根據需要選擇,然后單擊“確定”按鈕即可,以后其他人在再也不會更改你桌面上的壁紙了。
 
  

圖1
    
   

圖2
    
    在該策略中,系統提供了一個UNC 路徑,通過該功能我們可以設置網絡中的圖片或局域網其他電腦中的圖片做壁紙,如果設置其他電腦上的圖片我們可以輸入//Server/l.jpg格式。通過該策略設置后,用戶就不能通過“系統屬性”更改壁紙了。


隱藏你的“Internet 選項”
  如果你和別人共有一臺電腦,當你對IE進行了必要的設置,如安全級別、控件啟用設置等。這些設置后,你不想讓他人進行修改,這時我們可以通過組策略將Internet 選項中的各項屏蔽掉,以后使用時啟用即可。
  屏蔽IE的“Internet 選項”項目時,首先依次展開“用戶配置→管理模版→Windows組建→Internet Explorer→Internet 控制模版”,此時在右窗口中出現了禁用“Internet 選項”的所有標簽項策略,如果要屏蔽某一標簽項,如:常規標簽項。在此雙擊“禁用常規標簽項”策略,然后在彈出的對話框中點選“已啟用”選項,即可將其屏蔽,以后我們再使用各項功能時在此點選“未配置”項即可將其進行恢復(圖3)。
   

圖3

    
        讓“任務管理器”消失
  我們平時按Ctrl+Alt+Del組合鍵時可以顯示用戶選項,這里包括任務管理器、鎖定計算機、更改Windows密碼、注銷Windows、關機等選項。這些選項都是非常重要的,為了防止他人操作我們可以在組策略中屏蔽這些按鈕。
  首先依次展開“用戶設置→管理模版→系統→Ctrl+Alt+Del選項”分支,在該分支下我們可以共有“刪除任務管理器、刪除鎖定計算機、刪除更改密碼、刪除注銷、”四個策略(圖4),雙擊其中需要屏蔽的項目,彈出一個設置對話框,在該對話框中選擇啟用選項即可。
 
   

圖4
    
    控制非法下載文件
  往往我們駕著IE瀏覽網頁時,總會遇上一些好的電影、軟件,就想下載,這樣不僅占用硬盤空間,還給我們系統安全帶來很多問題。為了讓自己的系統更安全,我們需要安裝很多軟件來屏蔽下載功能,這樣不僅麻煩而且煩心。其實Windows XP早已經在組策略中考慮到了這些,也設置相應的選項。
  禁用IE下載功能時,在控制臺窗口中依次展開“計算機配置→管理模板→Windows Components→安全功能→限制文件下載”,在右側窗口中雙擊“Internet Explorer”,隨后彈出一個設置對話框,在此點選“啟用”,這樣即可啟用此策略設置,將阻止 Internet Explorer 進程的非用戶初始化的文件下載提示。“確定”以后就不能利用IE下載文件了。
文件和文件夾設置審核
  Windows XP 可以使用審核跟蹤用于訪問文件或其他對象的用戶賬戶、登錄嘗試、系統關閉或重新啟動以及類似的事件。如果利用組策略來進行文件、文件夾的審核,就可以保證它們的安全,其他人是無法輕易進行破壞的。
  在 “組策略”窗口中,逐級展開右側窗格中的“計算機配置→Windows設置→安全設置→本地策略”,然后在該分支下選擇“審核策略”選項。在右側窗格中用鼠標雙擊“審核對象訪問”選項,在彈出的“本地安全策略設置”窗口中,將“本地策略設置”框內的“成功”和“失敗”復選框都打上勾選標記(圖5)。
 
   

圖5
    
    通過上面的設置我們可以讓Windows XP系統更安全,其實在組策略中為用戶提供了更多安全的設置,在此我們主要選擇了幾個典型的設置,其他安全策略的設置方法,在此就不一一贅述了
為初學者服務。這是本文的宗旨。指針是c和c++中的難點和重點。有些程序員們精通dos下的basic.c語言的其它各種特性,在basic中都有類似的東西。只有指針,是baisc所不具備的。指針是c的靈魂。
 
  我不想重復大多數書上說得很清楚的東西,我只是把我看過的書中說得不清楚或沒有說,而我又覺得我理解得有點道理的東西寫出來。
 
  1、指針的概念
 
  指針是一個特殊的變量,它里面存儲的數值被解釋成為內存里的一個地址。要搞清一個指針需要搞清指針的四方面的內容:指針的類型,指針所指向的類型,指針的值或者叫指針所指向的內存區,還有指針本身所占據的內存區。讓我們分別說明。
 
  先聲明幾個指針放著做例子:
 
  例一:
 
 (1)int*ptr;
(2)char*ptr;
(3)int**ptr;
(4)int(*ptr)[3];
(5)int*(*ptr)[4];

  1、指針的類型
 
  從語法的角度看,你只要把指針聲明語句里的指針名字去掉,剩下的部分就是這個指針的類型。這是指針本身所具有的類型。讓我們看看例一中各個指針的類型:
 
 (1)int*ptr;
//指針的類型是int*
(2)char*ptr;
//指針的類型是char*
(3)int**ptr;
//指針的類型是int**
(4)int(*ptr)[3];
//指針的類型是int(*)[3]
(5)int*(*ptr)[4];
//指針的類型是int*(*)[4]

  怎么樣?找出指針的類型的方法是不是很簡單?
 
  2、指針所指向的類型
 
  當你通過指針來訪問指針所指向的內存區時,指針所指向的類型決定了編譯器將把那片內存區里的內容當做什么來看待。
 
  從語法上看,你只須把指針聲明語句中的指針名字和名字左邊的指針聲明符*去掉,剩下的就是指針所指向的類型。例如:

 (1)int*ptr;
//指針所指向的類型是int
(2)char*ptr;
//指針所指向的的類型是char
(3)int**ptr;
//指針所指向的的類型是int*
(4)int(*ptr)[3];
//指針所指向的的類型是int()[3]
(5)int*(*ptr)[4];
//指針所指向的的類型是int*()[4]

  在指針的算術運算中,指針所指向的類型有很大的作用。指針的類型(即指針本身的類型)和指針所指向的類型是兩個概念。當你對C越來越熟悉時,你會發現,把與指針攪和在一起的"類型"這個概念分成"指針的類型"和"指針所指向的類型"兩個概念,是精通指針的關鍵點之一。
 
  3、指針的值,或者叫指針所指向的內存區或地址
 
  指針的值是指針本身存儲的數值,這個值將被編譯器當作一個地址,而不是一個一般的數值。在32位程序里,所有類型的指針的值都是一個32位整數,因為32位程序里內存地址全都是32位長。
 
  指針所指向的內存區就是從指針的值所代表的那個內存地址開始,長度為sizeof(指針所指向的類型)的一片內存區。以后,我們說一個指針的值是XX,就相當于說該指針指向了以XX為首地址的一片內存區域;我們說一個指針指向了某塊內存區域,就相當于說該指針的值是這塊內存區域的首地址。
 
  指針所指向的內存區和指針所指向的類型是兩個完全不同的概念。在例一中,指針所指向的類型已經有了,但由于指針還未初始化,所以它所指向的內存區是不存在的,或者說是無意義的。
 
  以后,每遇到一個指針,都應該問問:這個指針的類型是什么?指針指向的類型是什么?該指針指向了哪里?
 
  4、指針本身所占據的內存區
 
  指針本身占了多大的內存?你只要用函數sizeof(指針的類型)測一下就知道了。在32位平臺里,指針本身占據了4個字節的長度。指針本身占據的內存這個概念在判斷一個指針表達式是否是左值時很有用。
 

第二章 指針的算術運算
 
  指針可以加上或減去一個整數。指針的這種運算的意義和通常的數值的加減運算的意義是不一樣的。例如:
 
  例二:
 1.chara[20];
2.int*ptr=a;
...
...
3.ptr++;

  在上例中,指針ptr的類型是int*,它指向的類型是int,它被初始化為指向整形變量a.接下來的第3句中,指針ptr被加了1,編譯器是這樣處理的:它把指針ptr的值加上了sizeof(int),在32位程序中,是被加上了4.
 
  由于地址是用字節做單位的,故ptr所指向的地址由原來的變量a的地址向高地址方向增加了4個字節。由于char類型的長度是一個字節,所以,原來ptr是指向數組a的第0號單元開始的四個字節,此時指向了數組a中從第4號單元開始的四個字節。我們可以用一個指針和一個循環來遍歷一個數組,看例子:
 
  例三:
 
 intarray[20];
int*ptr=array;
...
//此處略去為整型數組賦值的代碼。
...
for(i=0;i<20;i++)
{
(*ptr)++;
ptr++;
}

  這個例子將整型數組中各個單元的值加1.由于每次循環都將指針ptr加1,所以每次循環都能訪問數組的下一個單元。
 
  再看例子:
 
  例四:
 
 1.chara[20];
2.int*ptr=a;
...
...
3.ptr+=5;

  在這個例子中,ptr被加上了5,編譯器是這樣處理的:將指針ptr的值加上5乘sizeof(int),在32位程序中就是加上了5乘4=20.由于地址的單位是字節,故現在的ptr所指向的地址比起加5后的ptr所指向的地址來說,向高地址方向移動了20個字節。
 
  在這個例子中,沒加5前的ptr指向數組a的第0號單元開始的四個字節,加5后,ptr已經指向了數組a的合法范圍之外了。雖然這種情況在應用上會出問題,但在語法上卻是可以的。這也體現出了指針的靈活性。
 
 如果上例中,ptr是被減去5,那么處理過程大同小異,只不過ptr的值是被減去5乘sizeof(int),新的ptr指向的地址將比原來的ptr所指向的地址向低地址方向移動了20個字節。
 
  總結一下,一個指針ptrold加上一個整數n后,結果是一個新的指針ptrnew,ptrnew的類型和ptrold的類型相同,ptrnew所指向的類型和ptrold所指向的類型也相同。ptrnew的值將比ptrold的值增加了n乘sizeof(ptrold所指向的類型)個字節。就是說,ptrnew所指向的內存區將比ptrold所指向的內存區向高地址方向移動了n乘sizeof(ptrold所指向的類型)個字節。
 
  一個指針ptrold減去一個整數n后,結果是一個新的指針ptrnew,ptrnew的類型和ptrold的類型相同,ptrnew所指向的類型和ptrold所指向的類型也相同。ptrnew的值將比ptrold的值減少了n乘sizeof(ptrold所指向的類型)個字節,就是說,ptrnew所指向的內存區將比ptrold所指向的內存區向低地址方向移動了n乘sizeof(ptrold所指向的類型)個字節。
 
  第三章。運算符&和*
 
  這里&是取地址運算符,*是……書上叫做"間接運算符".&a的運算結果是一個指針,指針的類型是a的類型加個*,指針所指向的類型是a的類型,指針所指向的地址嘛,那就是a的地址。
 
  *p的運算結果就五花八門了。總之*p的結果是p所指向的東西,這個東西有這些特點:它的類型是p指向的類型,它所占用的地址是p所指向的地址。
 
  例五:
 
 inta=12;
intb;
int*p;
int**ptr;
p=&a;
//&a的結果是一個指針,
類型是int*,指向的類型是int,指向的地址是a的地址。
*p=24;
//*p的結果,在這里它的類型是int,
它所占用的地址是p所指向的地址,顯然,*p就是變量a。
ptr=&p;
//&p的結果是個指針,該指針的類型是p的類型加個*,
在這里是int**。該指針所指向的類型是p的類型,
這里是int*。該指針所指向的地址就是指針p自己的地址。
*ptr=&b;
//*ptr是個指針,&b的結果也是個指針,
且這兩個指針的類型和所指向的類型是一樣的,
所以用&b來給*ptr賦值就是毫無問題的了。
**ptr=34;
//*ptr的結果是ptr所指向的東西,在這里是一個指針,
對這個指針再做一次*運算,結果就是一個int類型的變量。

第四章 指針表達式
 
  一個表達式的最后結果如果是一個指針,那么這個表達式就叫指針表達式。下面是一些指針表達式的例子:
 
  例六:
 inta,b;
intarray[10];
int*pa;
pa=&a;
//&a是一個指針表達式。
int**ptr=&pa;
//&pa也是一個指針表達式。
*ptr=&b;
//*ptr和&b都是指針表達式。
pa=array;
pa++;
//這也是指針表達式。

  例七:
 
 char*arr[20];
char**parr=arr;
//如果把arr看作指針的話,arr也是指針表達式
char*str;
str=*parr;
//*parr是指針表達式
str=*(parr+1);
//*(parr+1)是指針表達式
str=*(parr+2);
//*(parr+2)是指針表達式

  由于指針表達式的結果是一個指針,所以指針表達式也具有指針所具有的四個要素:指針的類型,指針所指向的類型,指針指向的內存區,指針自身占據的內存。
 
  好了,當一個指針表達式的結果指針已經明確地具有了指針自身占據的內存的話,這個指針表達式就是一個左值,否則就不是一個左值。
 
  在例七中,&a不是一個左值,因為它還沒有占據明確的內存。*ptr是一個左值,因為*ptr這個指針已經占據了內存,其實*ptr就是指針pa,既然pa已經在內存中有了自己的位置,那么*ptr當然也有了自己的位置。
 
  第五章。數組和指針的關系
 
  數組的數組名其實可以看作一個指針。看下例:
 
  例八:
 
 intarray[10]={0,1,2,3,4,5,6,7,8,9},value;
...
...
value=array[0];
//也可寫成:value=*array;
value=array[3];
//也可寫成:value=*(array+3);
value=array[4];
//也可寫成:value=*(array+4);


上例中,一般而言數組名array代表數組本身,類型是int[10],但如果把array看做指針的話,它指向數組的第0個單元,類型是int*,所指向的類型是數組單元的類型即int.因此*array等于0就一點也不奇怪了。同理,array+3是一個指向數組第3個單元的指針,所以*(array+3)等于3.其它依此類推。
 
  例九:
 char*str[3]=
{
"Hello,thisisasample!",
"Hi,goodmorning.",
"Helloworld"
};
chars[80];
strcpy(s,str[0]);
//也可寫成strcpy(s,*str);
strcpy(s,str[1]);
//也可寫成strcpy(s,*(str+1));
strcpy(s,str[2]);
//也可寫成strcpy(s,*(str+2));

  上例中,str是一個三單元的數組,該數組的每個單元都是一個指針,這些指針各指向一個字符串。把指針數組名str當作一個指針的話,它指向數組的第0號單元,它的類型是char**,它指向的類型是char*.
 
  *str也是一個指針,它的類型是char*,它所指向的類型是char,它指向的地址是字符串"Hello,thisisasample!"的第一個字符的地址,即'H'的地址。str+1也是一個指針,它指向數組的第1號單元,它的類型是char**,它指向的類型是char*.
 
  *(str+1)也是一個指針,它的類型是char*,它所指向的類型是char,它指向"Hi,goodmorning."的第一個字符'H',等等。
 
  下面總結一下數組的數組名的問題。聲明了一個數組TYPEarray[n],則數組名稱array就有了兩重含義:第一,它代表整個數組,它的類型是TYPE[n];第二,它是一個指針,該指針的類型是TYPE*,該指針指向的類型是TYPE,也就是數組單元的類型,該指針指向的內存區就是數組第0號單元,該指針自己占有單獨的內存區,注意它和數組第0號單元占據的內存區是不同的。
 
  該指針的值是不能修改的,即類似array++的表達式是錯誤的。在不同的表達式中數組名array可以扮演不同的角色。
 
  在表達式sizeof(array)中,數組名array代表數組本身,故這時sizeof函數測出的是整個數組的大小。在表達式*array中,array扮演的是指針,因此這個表達式的結果就是數組第 0號單元的值。sizeof(*array)測出的是數組單元的大小。
 
  表達式array+n(其中n=0,1,2,……。)中,array扮演的是指針,故array+n的結果是一個指針,它的類型是TYPE*,它指向的類型是TYPE,它指向數組第n號單元。故sizeof(array+n)測出的是指針類型的大小。
 
  例十:
 
 intarray[10];
int(*ptr)[10];
ptr=&array;

  上例中ptr是一個指針,它的類型是int(*)[10],他指向的類型是int[10],我們用整個數組的首地址來初始化它。在語句ptr=&array中,array代表數組本身。
 
  本節中提到了函數sizeof(),那么我來問一問,sizeof(指針名稱)測出的究竟是指針自身類型的大小呢還是指針所指向的類型的大小?答案是前者。例如:
 
 int(*ptr)[10];
則在32位程序中,有:
sizeof(int(*)[10])==4
sizeof(int[10])==40
sizeof(ptr)==4

  實際上,sizeof(對象)測出的都是對象自身的類型的大小,而不是別的什么類型的大小。


 C語言指針的概念
    指針是一個特殊的變量,它里面存儲的數值被解釋成為內存里的一個地址。 要搞清一個指針需要搞清指針的四方面的內容:指針的類型,指針所指向的 類型,指針的值或者叫指針所指向的內存區,還有指針本身所占據的內存區。讓我們分別說明。
    先聲明幾個指針放著做例子:
    例一:
    (1)int*ptr;
    (2)char*ptr;
    (3)int**ptr;
    (4)int(*ptr)[3];
    (5)int*(*ptr)[4];
    如果看不懂后幾個例子的話,請參閱我前段時間貼出的文章<<如何理解c和c 的復雜類型聲明>>.
    指針的類型
    從語法的角度看,你只要把指針聲明語句里的指針名字去掉,剩下的部分就是這個指針的類型。這是指針本身所具有的類型。讓我們看看例一中各個指針的類型:
    (1)int*ptr;//指針的類型是int*
    (2)char*ptr;//指針的類型是char*
    (3)int**ptr;//指針的類型是int**
    (4)int(*ptr)[3];//指針的類型是int(*)[3]
    (5)int*(*ptr)[4];//指針的類型是int*(*)[4]
    怎么樣?找出指針的類型的方法是不是很簡單?
    C語言指針所指向的類型
    當你通過指針來訪問指針所指向的內存區時,指針所指向的類型決定了編譯器將把那片內存區里的內容當做什么來看待。
    從語法上看,你只須把指針聲明語句中的指針名字和名字左邊的指針聲明符*去掉,剩下的就是指針所指向的類型。例如:
    (1)int*ptr;//指針所指向的類型是int
    (2)char*ptr;//指針所指向的的類型是char
    (3)int**ptr;//指針所指向的的類型是int*
    (4)int(*ptr)[3];//指針所指向的的類型是int()[3]
    (5)int*(*ptr)[4];//指針所指向的的類型是int*()[4]
    在指針的算術運算中,指針所指向的類型有很大的作用。
    指針的類型(即指針本身的類型)和指針所指向的類型是兩個概念。當你對C越來越熟悉時,你會發現,把與指針攪和在一起的\"類型\"這個概念分成\"指針的類型\"和\"指針所指向的類型\"兩個概念,是精通指針的關鍵點之一。我看了不少書,發現有些寫得差的書中,就把指針的這兩個概念攪在一起了,所以看起書來前后矛盾,越看越糊涂。
    指針的值,或者叫指針所指向的內存區或地址
    指針的值是指針本身存儲的數值,這個值將被編譯器當作一個地址,而不是一個一般的數值。在32位程序里,所有類型的指針的值都是一個32位整數,因為32位程序里內存地址全都是32位長。 指針所指向的內存區就是從指針的值所代表的那個內存地址開始,長度為si zeof(指針所指向的類型)的一片內存區。以后,我們說一個指針的值是XX,就相當于說該指針指向了以XX為首地址的一片內存區域;我們說一個指針指向了某塊內存區域,就相當于說該指針的值是這塊內存區域的首地址。
    指針所指向的內存區和指針所指向的類型是兩個完全不同的概念。在例一中,指針所指向的類型已經有了,但由于指針還未初始化,所以它所指向的內存區是不存在的,或者說是無意義的。
    以后,每遇到一個指針,都應該問問:這個指針的類型是什么?指針指的類型是什么?該指針指向了哪里?
    C語言指針本身所占據的內存區
    指針本身占了多大的內存?你只要用函數sizeof(指針的類型)測一下就知道了。在32位平臺里,指針本身占據了4個字節的長度。
    指針本身占據的內存這個概念在判斷一個指針表達式是否是左值時很有用。



1.語言中變量的實質
    要理解C指針,我認為一定要理解C中“變量”的存儲實質,所以我就從“變量”這個東西開始講起吧!
    先來理解理解內存空間吧!請看下圖:
    內存地址→  6      7   8      9   10      11      12      13
    -----------------------------------------------------------------
    。。。 |   |   |   |   |  |   |   |.。
    -----------------------------------------------------------------
    如圖所示,內存只不過是一個存放數據的空間,就好像我的看電影時的電影院中的座位一樣。每個座位都要編號,我們的內存要存放各種各樣的數據,當然我們要知道我們的這些數據存放在什么位置吧!所以內存也要象座位一樣進行編號了,這就是我們所說的內存編址。座位可以是按一個座位一個號碼的從一號開始編號,內存則是按一個字節一個字節進行編址,如上圖所示。每個字節都有個編號,我們稱之為內存地址。好了,我說了這么多,現在你能理解內存空間這個概念嗎?
    我們繼續看看以下的C、C++語言變量申明:
    int I;
    char a;
    每次我們要使用某變量時都要事先這樣申明它,它其實是內存中申請了一個名為i的整型變量寬度的空間(DOS下的16位編程中其寬度為二個字節),和一個名為a的字符型變量寬度的空間(占一個字節)。
    我們又如何來理解變量是如何存在的呢。當我們如下申明變量時:
    int I;
    char a;
    內存中的映象可能如下圖:
    內存地址→   6      7   8      9      10      11    12      13
    ------------------------------------------------------------------
    。。。|   |   |   |   |   |   |   |.。
    ------------------------------------------------------------------
    變量名|→i    ←|→a  ←|
    圖中可看出,i在內存起始地址為6上申請了兩個字節的空間(我這里假設了int的寬度為16位,不同系統中int的寬度是可能不一樣的),并命名為i. a在內存地址為8上申請了一字節的空間,并命名為a.這樣我們就有兩個不同類型的變量了。
    2.賦值給變量
    再看下面賦值:
    i=30
    a=‘t’
    你當然知道個兩個語句是將30存入i變量的內存空間中,將‘t’字符存入a變量的內存空間中。我們可以這樣的形象理解啦:
    內存地址→   6      7   8      9      10      11    12      13
    -----------------------------------------------------------------------
    。。。 |   30      |  ‘t’  |   |   |   |   |.。
    -----------------------------------------------------------------------
    |→i    ←|→a  ←|
    3.變量在哪里?(即我想知道變量的地址)
    好了,接下來我們來看看&i是什么意思?
    是取i變量所在的地址編號嘛!我們可以這樣讀它:返回i變量的地址編號。你記住了嗎?
    我要在屏幕上顯示變量的地址值的話,可以寫如下代碼:
    printf(“%d”,&i);
    以上圖的內存映象所例,屏幕上顯示的不是i值30,而是顯示i的內存地址編號6了。當然實際你操作的時,i變量的地址值不會是這個數了。
    這就是我認為作為初學者們所應想象的變量存儲實質了。請這樣理解吧!
    最后總結代碼如下:
 int main()
{
   int i=39;
   printf(“%d\n”,i);   //①
   printf(“%d\n”, &i);  //②
     }
    現在你可知道①、②兩個printf分別在屏幕上輸出的是i的什么東西啊?
    好啦!下面我們就開始真正進入指針的學習了。Come on !(待續…)

二、指針是什么東西
    想說弄懂你不容易啊!我們許多初學指針的人都要這樣的感慨。我常常在思索它,為什么呢?其實生活中處處都有指針。我們也處處在使用它。有了它我們的生活才更加方便了。沒有指針,那生活才不方便。不信?你看下面的例子。
    這是一個生活中的例子:比如說你要我借給你一本書,我到了你宿舍,但是你人不在宿舍,于是我把書放在你的2層3號的書架上,并寫了一張紙條放在你的桌上。紙條上寫著:你要的書在第2層3號的書架上。當你回來時,看到這張紙條。你就知道了我借與你的書放在哪了。你想想看,這張紙條的作用,紙條本身不是書,它上面也沒有放著書。那么你又如何知道書的位置呢?因為紙條上寫著書的位置嘛!其實這張紙條就是一個指針了。它上面的內容不是書本身,而是書的地址,你通過紙條這個指針找到了我借給你的本書。
    那么我們C,C++中的指針又是什么呢?請繼續跟我來吧,看下面看一個申明一整型指針變量的語句如下:
    int * pi;
    pi是一個指針,當然我們知道啦,但是這樣說,你就以為pi一定是個多么特別的東西了。其實,它也只過是一個變量而已。與上一篇中說的變量并沒有實質的區別。不信你看下面圖。
    內存地址→6     7   8      9     10     11      12     13     14
    --------------------------------------------------------------
    ...|    30      |  ‘t’ |      |      |      |      |      |      |……
     --------------------------------------------------------------
    變量 |→i   ←|→a   ←|       |→ pi      ←|
    (說明:這里我假設了指針只占2個字節寬度,實際上在32位系統中,指針的寬度是4個字節寬的,即32位。)由圖示中可以看出,我們使用int *Pi申明指針變量; 其實是在內存的某處申明一個一定寬度的內存空間,并把它命名為Pi.你能在圖中看出pi與前面的i,a 變量有什么本質區別嗎,沒有,當然沒有!pi也只不過是一個變量而已嘛!那么它又為什么會被稱為指針?關鍵是我們要讓這個變量所存儲的內容是什么。現在我要讓pi成為真正有意義上的指針。請接著看下面語句:
    pi=&i;
    你應該知道 &i是什么意思吧!再次提醒你啦:這是返回i變量的地址編號。整句的意思就是把i地址的編號賦值給pi,也就是你在pi上寫上i的地址編號。結果如下圖所示:
    內存地址→6     7   8   9   10     11    12     13     14
    ------------------------------------------------------------------
    ...|     30      |  ‘t’  |      |      |     6      |      |      |……
    ------------------------------------------------------------------
    變量 |→i   ←|→a    ←|       |→ pi     ←|
    你看,執行完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 ) ,請結合上圖好好體會吧!各位還有什么疑問,可以發Email:yyf977@163.com.
    到此為止,你掌握了類似&i , *pi寫法的含義和相關操作嗎。總的一句話,我們的紙條就是我們的指針,同樣我們的pi也就是我們的紙條!剩下的就是我們如何應用這張紙條了。最后我給你一道題:
    程序如下
 char  a,*pa
a=10
pa=&a
*pa=20
printf( “%d”, a)
    你能直接看出輸出的結果是什么嗎?如果你能,我想本篇的目的就達到了。好了,就說到這了。Happy to Study!在下篇中我將談談“指針的指針”即對int * * ppa;中ppa 的理解。

 1.數組元素
    看下面代碼
 int i,a[]={3,4,5,6,7,3,7,4,4,6};
for (i=0;i<=9;i++)
{
    printf ( “%d”, a[i] );
}
    很顯然,它是顯示a 數組的各元素值。
    我們還可以這樣訪問元素,如下
 int i,a[]={3,4,5,6,7,3,7,4,4,6};
for (i=0;i<=9;i++)
{
    printf ( “%d”,  *(a+i) );
}
    它的結果和作用完全一樣
    2. 通過指針訪問數組元素
 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”, 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”, *(pa+i) );
}
    看pa=a即數組名賦值給指針,以及通過數組名、指針對元素的訪問形式看,它們并沒有什么區別,從這里可以看出數組名其實也就是指針。難道它們沒有任何區別?有,請繼續。
    3. 數組名與指針變量的區別
    請看下面的代碼:
 int i,*pa,a[]={3,4,5,6,7,3,7,4,4,6};
pa =a;
for (i=0;i<=9;i++)
{
    printf ( “%d”, *pa );
     pa++ ;  //注意這里,指針值被修改
}
    可以看出,這段代碼也是將數組各元素值輸出。不過,你把{}中的pa改成a試試。你會發現程序編譯出錯,不能成功。看來指針和數組名還是不同的。其實上面的指針是指針變量,而數組名只是一個指針常量。這個代碼與上面的代碼不同的是,指針pa在整個循環中,其值是不斷遞增的,即指針值被修改了。數組名是指針常量,其值是不能修改的,因此不能類似這樣操作:a++.前面4,5節中pa[i],*(pa+i)處,指針pa的值是使終沒有改變。所以變量指針pa與數組名a可以互換。
    4. 申明指針常量
    再請看下面的代碼:
 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”, *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上實驗。


 1 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與前兩個式子又有什么不同呢?我下面就來具體分析它們的格式與語義吧!
    2 const int * pi的語義
    我先來說說const int * pi是什么作用 (當然int const * pi也是一樣的,前面我們說過,它們實際是一樣的)。看下面的例子:
 //*************代碼開始***************
int i1=30;
int i2=40;
const int * pi=&i1;
pi=&i2;    //4.注意這里,pi可以在任意時候重新賦值一個新內存地址
i2=80;    //5.想想看:這里能用*pi=80;來代替嗎?當然不能
printf( “%d”, *pi ) ;  //6.輸出是80
//*************代碼結束***************
    語義分析:
    看出來了沒有啊,pi的值是可以被修改的。即它可以重新指向另一個地址的,但是,不能通過*pi來修改i2的值。這個規則符合我們前面所講的邏輯嗎?當然符合了!
    首先const  修飾的是整個*pi(注意,我寫的是*pi而不是pi)。所以*pi是常量,是不能被賦值的(雖然pi所指的i2是變量,不是常量)。
    其次,pi前并沒有用const 修飾,所以pi是指針變量,能被賦值重新指向另一內存地址的。你可能會疑問:那我又如何用const 來修飾pi呢?其實,你注意到int * const pi中const 的位置就大概可以明白了。請記住,通過格式看語義。哈哈,你可能已經看出了規律吧?那下面的一節也就沒必要看下去了。不過我還得繼續我的戰斗!
    3 再看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;    4.注意這里,pi不能再這樣重新賦值了,即不能再指向另一個新地址。
   //所以我已經注釋了它。
i1=80;    //5.想想看:這里能用*pi=80;來代替嗎?可以,這里可以通過*pi修改i1的值。
     //請自行與前面一個例子比較。
printf( “%d”, *pi ) ;  //6.輸出是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)!我一定會答復的。
    3)  補充三種情況。
    這里,我再補充以下三種情況。其實只要上面的語義搞清楚了,這三種情況也就已經被包含了。不過作為三種具體的形式,我還是簡單提一下吧!
    情況一: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****************
    下篇預告:函數參數的指針傳遞,值傳遞,引用傳遞 迷惑(以為a,b已經代替了x,y,對x,y的操作就是對a,b的操作了,這是一個錯誤的觀點啊!)。


 一、 三道考題 開講之前,我先請你做三道題目。(嘿嘿,得先把你的頭腦搞昏才行……唉呀,誰扔我雞蛋?)
    1. 考題一:程序代碼如下:
 void Exchg1(int x, int y)
{
   int tmp;
   tmp=x;
   x=y;
   y=tmp;
   printf(“x=%d,y=%d\n”,x,y)
}
void main()
{
   int a=4,b=6;
   Exchg1 (a,b) ;
   printf(“a=%d,b=%d\n”,a,b)
}
    輸出的結果:
    x=____, y=____
    a=____, b=____
    問下劃線的部分應是什么,請完成。
    2. 考題二:代碼如下。
 Exchg2(int *px, int *py)
{
   int tmp=*px;
   *px=*py;
    *py=tmp;
   print(“*px=%d,*py=%d\n”,*px,*py);
}
main()
{
   int a=4;
   int b=6;
   Exchg2( &a,&b);
   Print(“a=%d,b=%d\n”, a, b);
}
    輸出的結果為:
    *px=____, *py=____
    a=____, b=____
    問下劃線的部分應是什么,請完成。
    3. 考題三:
 Exchg2(int &x, int &y)
{
    int tmp=x;
    x=y;
    y=tmp;
   print(“x=%d,y=%d\n”,x,y);
}
main()
{
   int a=4;
   int b=6;
   Exchg2(a,b);
   Print(“a=%d,b=%d\n”, a, b);
}
    輸出的結果:
    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兩個變量值的對調,程序如下:
 void main()
{
   int a=4,b=6;
   Exchg1 (a,b)     //a,b變量為Exchg1函數的實際參數。
/  printf(“a=%d,b=%d\n”,a,b)
}
    我問: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”,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的操作了,這是一個錯誤的觀點啊!)。


指向另一指針的指針
    一、針概念:
    早在本系列第二篇中我就對指針的實質進行了闡述。今天我們又要學習一個叫做指向另一指針地址的指針。讓我們先回顧一下指針的概念吧!
    當我們程序如下申明變量:
    short int i;
    char a;
    short int * pi;
    程序會在內存某地址空間上為各變量開辟空間,如下圖所示。
    內存地址→6     7  8     9     10     11    12    13     14    15
    -------------------------------------------------------------------------------------
    …  |     |  |  |  |  |  |  |  |  |
    -------------------------------------------------------------------------------------
    |short int i |char a|  |short int * pi|
    圖中所示中可看出:
    i 變量在內存地址5的位置,占兩個字節。
    a變量在內存地址7的位置,占一個字節。
    pi變量在內存地址9的位置,占兩個字節。(注:pi 是指針,我這里指針的寬度只有兩個字節,32位系統是四個字節)
    接下來如下賦值:
    i=50;
    pi=&i;
    經過上在兩句的賦值,變量的內存映象如下:
    內存地址→6     7  8     9     10     11    12    13  14     15
    --------------------------------------------------------------------------------------
    …  |    50  |  |  |    6   |  |  |  |
    --------------------------------------------------------------------------------------
    |short int i |char a|  |short int * pi|
    看到沒有:短整型指針變量pi的值為6,它就是I變量的內存起始地址。所以,這時當我們對*pi進行讀寫操作時,其實就是對i變量的讀寫操作。如:
    *pi=5;   //就是等價于I=5;
    你可以回看本系列的第二篇,那里有更加詳細的解說。
    二、 指針的地址與指向另一指針地址的指針
    在上一節中,我們看到,指針變量本身與其它變量一樣也是在某個內存地址中的,如pi的內存起始地址是10.同樣的,我們也可能讓某個指針指向這個地址。
    看下面代碼:
    short int * * ppi;    //這是一個指向指針的指針,注意有兩個*號
    ppi=π
    第一句:short int * * ppi;——申明了一個指針變量ppi,這個ppi是用來存儲(或稱指向)一個short int * 類型指針變量的地址。
    第二句:&pi那就是取pi的地址,ppi=π就是把pi的地址賦給了ppi.即將地址值10賦值給ppi.如下圖:
    內存地址→6     7  8     9     10     11    12    13  14    15
    ------------------------------------------------------------------------------------
    …  |    50     |  |  |  6  |  10  |  |
    ------------------------------------------------------------------------------------
    |short int i|char a|  |short int * pi|short int ** ppi|
    從圖中看出,指針變量ppi的內容就是指針變量pi的起始地址。于是……
    ppi的值是多少呢?——10.
    *ppi的值是多少呢?——6,即pi的值。
    **ppi的值是多少呢?——50,即I的值,也是*pi的值。
    呵呵!不用我說太多了,我相信你應明白這種指針了吧!
    三、 一個應用實例
    1. 設計一個函數:void find1(char array[], char search, char * pi)
    要求:這個函數參數中的數組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;
   }
    }
}
    你覺得這個函數能實現所要求的功能嗎?
    調試:
    我下面調用這個函數試試。
 void main()
{
   char str[]={“afsdfsdfdf\0”};  //待查找的字符串
   char a=’d’;   //設置要查找的字符
   char * p=0;  //如果查找到后指針p將指向字符串中查找到的第一個字符的地址。
   find1(str,a,p);  //調用函數以實現所要操作。
   if (0==p )
   {
      printf (“沒找到!\n”);//1.如果沒找到則輸出此句
   }
   else
   {
      printf(“找到了,p=%d”,p);  //如果找到則輸出此句
   }
}
    分析:
    上面代碼,你認為會是輸出什么呢?
    運行試試。
    唉!怎么輸出的是:沒有找到!
    而不是:找到了,……。
    明明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;
    (*FunP)(20);
   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);
}
 
    輸出結果:略
    分析:(看我寫的注釋。你可按我注釋的①②③④⑤順序自行分析。)

 本文由用戶 kavacan 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!