彻底搞定C语言指针

13年前
前言
    指针是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);
}
 
    输出结果:略
    分析:(看我写的注释。你可按我注释的①②③④⑤顺序自行分析。)