su    :切换并取代该用户的身份
   执行范例:
   1>切换到root
   [lammy198@localhost lammy198]$ su -    //可以写成su - root
   Password:                    //输入密码
   [root@localhost root]#
   2>切换到lammy198
   由root切换,则不需要输入密码
   [root@localhost root]# su - lammy198
   [lammy198@localhost lammy198]$
   不是由root切换,则需要输入密码
   [lammy@localhost lammy]$ su - lammy198    //不可以写成su -
   Password:                    //输入密码
   [lammy198@localhost lammy198]$
adduser    :新建系统上的用户
   [root@localhost root]# adduser -D        //显示建立帐号时的默认值
   GROUP=100                //所属主组的ID
   HOME=/home                //用户的根目录
   INACTIVE=-1
   EXPIRE=
   SHELL=/bin/bash                //所使用的SHELL
   SKEL=/etc/skel                //所应用的设置文件
   创建lammyt的帐号
   [root@localhost root]# adduser -m lammyt
   [root@localhost root]# id lammyt        //为什么groups=502而不是100?
   uid=502(lammyt) gid=502(lammyt) groups=502(lammyt)
userdel    :删除帐号    
   [root@localhost root]# userdel lammyt
   [root@localhost root]# id lammyt
   id: lammyt: No such user
useradd    :新建帐号
   [root@localhost root]# useradd lammyt
   [root@localhost root]# id lammyt
   uid=504(lammyt) gid=504(lammyt) groups=504(lammyt)
who    :显示登陆当前登陆用户的信息
   [root@localhost root]# who
   root     :0           Sep 16 18:05
   root     pts/2        Sep 21 12:16 (:0.0)
id    :显示拥护组的ID
   [root@localhost root]# id -a            //显示用户的帐户信息
   uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel)
ps    :显示当前系统中由该用户运行的进程列表
   [root@localhost root]# ps -ef
   UID        PID PPID C STIME TTY          TIME CMD
   root         1     0 0 14:38 ?        00:00:06 init
   root         2     1 0 14:38 ?        00:00:00 [keventd]
   root         3     1 0 14:38 ?        00:00:00 [kapmd]
   root         4     1 0 14:38 ?        00:00:00 [ksoftirqd_CPU0]
   root         9     1 0 14:38 ?        00:00:00 [bdflush]
   root         5     1 0 14:38 ?        00:00:00 [kswapd]
   root         6     1 0 14:38 ?        00:00:00 [kscand/DMA]
free    :查看当前系统内存的使用情况
   [root@localhost root]# free
                 total       used         free       shared       buffers         cached
   Mem:        412856     402316      10540          0      35908     218360
   -/+ buffers/cache:     148048     264808
   Swap:       522104       1612     520492
df    :查看文件系统的磁盘空间占用情况
   [root@localhost root]# df
   文件系统               1K-块        已用     可用 已用% 挂载点
   /dev/sda3              9653708   2690872   6472452 30% /    
   /dev/sda1               147766      9376    130761   7% /boot
   none                    206428         0    206428   0% /dev/shm
du    :统计目录或文件所占磁盘空间的大小
fdisk    :查看磁盘分区情况及对硬盘进行分区管理
   [root@localhost root]# fdisk -l

   Disk /dev/sda: 10.7 GB, 10737418240 bytes
   255 heads, 63 sectors/track, 1305 cylinders
   Units = cylinders of 16065 * 512 = 8225280 bytes

       Device Boot    Start       End    Blocks   Id System
   /dev/sda1   *         1        19    152586   83 Linux
   /dev/sda2            20        84    522112+ 82 Linux swap
   /dev/sda3            85      1305   9807682+ 83 Linux
   使用fdisk必须拥有root的权限
   [lammy@localhost lammy]$ fdisk -l
   -bash: fdisk: command not found
mount    :磁盘挂载
   [root@localhost root]# mount -l        //列出以挂载的设备,文件系统名称和挂载点
   /dev/sda3 on / type ext3 (rw) [/]
   none on /proc type proc (rw)
   usbdevfs on /proc/bus/usb type usbdevfs (rw)
   /dev/sda1 on /boot type ext3 (rw) [/boot]
   none on /dev/pts type devpts (rw,gid=5,mode=620)
   none on /dev/shm type tmpfs (rw)
   none on /proc/fs/vmblock/mountPoint type vmblock (rw)
cd    :更改工作路径
   cd -可以回到前次工作目录
   ./代表当前目录, ../代表上级目录
ls    :列出目录内容
   [root@localhost /]# ls                //列出目录
   bin   dev home    lib         misc opt   root soft    tmp var
   boot etc initrd lost+found mnt   proc sbin themes usr    
   [root@localhost /]# ls -l                //列出目录的详细内容
   总用量 201
   drwxr-xr-x    2 root     root         4096 9月 5 23:19 bin
   drwxr-xr-x    4 root     root         1024 9月 5 23:07 boot
   drwxr-xr-x   20 root     root       118784 9月 16 18:05 dev
   drwxr-xr-x   56 root     root         4096 9月 26 21:41 etc
   drwxr-xr-x    8 root     root         4096 9月 26 21:40 home
   drwxr-xr-x    2 root     root         4096 2003-01-25 initrd 后面的没有贴出来
mkdir    :创建目录
   [root@localhost root]# mkdir -p ./test/test        //-p设置路径
   [root@localhost root]# ls
   gcc_programe minicom.log other test vmware-tools-distrib
   [root@localhost root]# cd test
   [root@localhost test]# ls
   test
cat    :连接并显示指定的一个和多个文件的有关信息
   -n 由第一行开始对所有输出的行数编号
   -b 和-n相似,只不过对于空白行不编号
cp    :将给出的文件或目录复制到另一个文件或目录中
   [root@localhost root]# mkdir -p ./test/lammy
   [root@localhost root]# cp -a ./test/lammy ./
   [root@localhost root]# ls
   gcc_programe lammy minicom.log other test vmware-tools-distrib
   [root@localhost root]# ls ./test
   lammy test
mv    :为文件或目录改名或将文件由一个目录移入到另一个目录中
   [root@localhost root]# mkdir -p ./test/lammyt
   [root@localhost root]# ls ./test
   lammy lammyt test
   [root@localhost root]# mv -i ./test/lammyt ./
   [root@localhost root]# ls
   gcc_programe lammy lammyt minicom.log other test vmware-tools-distrib
   [root@localhost root]# ls ./test
   lammy test
   该实例把./test下的lammyt移到./下
   [root@localhost root]# mkdir ./lammyt/lammyt
   [root@localhost root]# ls ./lammyt
   lammyt
   [root@localhost root]# mv ./lammyt/lammyt ./lammyt/lammy198
   [root@localhost root]# ls ./lammyt
   lammy198
   重命名实验
rm    :删除一个目录中的一个或多个文件
   [root@localhost root]# rm -i lammy        
   rm:是否删除目录‘lammy’? y
   rm: 无法删除目录‘lammy’: 是一个目录
   [root@localhost root]# rm -r lammy            //删除单个目录
   rm:是否删除目录‘lammy’? y
   [root@localhost root]# ls
   gcc_programe lammyt minicom.log other test vmware-tools-distrib
   该实例说明如果煤油使用-r,则rm不会删除目录;
   [root@localhost root]# rm -r test            //删除多个目录
   rm:是否进入目录‘test’? y
   rm:是否删除目录‘test/test’? y
   rm:是否删除目录‘test/lammy’? y
   rm:是否删除目录‘test’? y
简单示例:

gdb server
b server.c 10 //在server.c的第10行断点
n
p
bt //堆栈查看
p cfg->type  //查看结构体某个字段的值




得到的函数列表:
info functions
启动 GDB 调试工具
$ gdb --quiet
info 命令列出程序信息
(gdb) info proc
list 命令
(gdb) list main
disassemble 命令
(gdb) disass main
========================================
    刚刚学了下gdb调试器的简单使用,感觉还不错,趁热打铁,把过程讲述下,自己也增强下,呵呵,废话少说,Begin!!!
[root@localhost hello]# vim test.c      //新建一个test.c的源文件
在test.c中键入如下代码,很简单的程序:
/*test.c*/
#include
int sum(int m);
                                                                                
int main(void)
{
        int i,m =0;
        sum(50);
        for(i=50; i!=0; i--)m+=i;
        printf("The sum of 1~50 is %d \n",m);
}
                                                                                
int sum(int m)
{
        int i,n =0;
//      sum(50);
        for(i=m; i!=0; i--)n+=i;
        printf("The sum of 1~m is %d \n",n);
}
完了后保存它,返回终端
[root@localhost hello]# gcc -g test.c -o test         //记得一定要加 -g,这样编译出的可执行代码才包含调试的信息,否则gdb是无法载入的
[root@localhost hello]# gdb test                 //test为带有调试信息的目标文件  
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Copyright 2003 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...
(gdb)            //以上的信息就是gdb的启动画面,指定了gdb的版本号等信息
//然后键入 l 查看加载的文件,l表示list,b表示breakpoint,p表示print
(gdb) l         //一次加载10行总[个人总结],
1       /*test.c*/
2       #include
3       int sum(int m);
4
5       int main(void)
6       {
7               int i,m =0;
8               sum(50);
9               for(i=50; i!=0; i--)m+=i;
10              printf("The sum of 1~50 is %d \n",m);
(gdb)
(gdb) l         //继续通过参数 l 加载,加载接下来的10行
11      }
12
13      int sum(int m)
14      {
15              int i,n =0;
16      //      sum(50);
17              for(i=m; i!=0; i--)n+=i;
18              printf("The sum of 1~m is %d \n",n);
19      }
(gdb) l         //加载完毕,再加载显然是不会在加了哈
Line number 20 out of range; test.c has 19 lines.
//首先来看怎么设断点
(gdb) b 8      //b表示设断点,后面表示行号,就是加载时显示的行号
Breakpoint 1 at 0x804833f: file test.c, line 8.
(gdb) b 9
Breakpoint 2 at 0x804834c: file test.c, line 9.
(gdb) info b   //我设了两个断点,通过info 来查看断点信息
Num Type           Disp Enb Address    What
1   breakpoint     keep y   0x0804833f in main at test.c:8
2   breakpoint     keep y   0x0804834c in main at test.c:9
(gdb)
(gdb) r      //r表示run,运行至断点1,如果没设断点,则运行至结尾
Starting program: /root/hello/test

Breakpoint 1, main () at test.c:8
8               sum(50);
(gdb) c      //c表示continue,继续运行
Continuing.
The sum of 1~m is 1275

Breakpoint 2, main () at test.c:9
9               for(i=50; i!=0; i--)m+=i;
(gdb)      //呵呵,上面几个操作看明白了吧
//怎么来看变量值呢
(gdb) l      //先用l来看看源码
4
5       int main(void)
6       {
7               int i,m =0;
8               sum(50);
9               for(i=50; i!=0; i--)m+=i;
10              printf("The sum of 1~50 is %d \n",m);
11      }
12
13      int sum(int m)
(gdb) p m   //p表示print,打印m的值
$1 = 0
(gdb) p i   //打印i的值,i为什么这么大,应该不用我介绍了吧
$2 = 1073828704
//变量知道怎么看了,那么单步运行呢
(gdb) n      //n表示单步运行,这表示一次运行一行,所以它不会进入调用的函数
10              printf("The sum of 1~50 is %d \n",m);
(gdb) n      //n的运行机制通过这俩个n应该要看出个门道来喽,我用颜色强调了下,明白了没
The sum of 1~50 is 1275
11      }
//那么如果碰到调用函数怎么进入呢,不急,有办法
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/hello/test

Breakpoint 1, main () at test.c:8
8               sum(50);   //前面设的断点,表示下一步将运行这断点所在的行
(gdb) s      //用s来进入函数,也就说s也是单步运行的意思,但是它碰到函数时会进入函数运行; 而n不会,直接跳过
sum (m=50) at test.c:15   //仔细看看,是不是进入了sum函数哈
15              int i,n =0;
(gdb) s
17              for(i=m; i!=0; i--)n+=i;
(gdb) s
18              printf("The sum of 1~m is %d \n",n);
(gdb)
//这样在windows下的编译器的调试方法在gdb中都有相应的命令对应,并且更加灵活,哦忘了说怎么取消断点拉,呵呵简单
(gdb) info b      //首先查看断点信息
Num Type           Disp Enb Address    What
1   breakpoint     keep y   0x0804833f in main at test.c:8
        breakpoint already hit 1 time
2   breakpoint     keep y   0x0804834c in main at test.c:9
(gdb) delet 1      //用delet取消断点,后面的数字应该明白吧
//还有其它命令可以通过输入help来查看,或者help command[需要查看的命令]来查看
//退出gdb,返回终端是通过q来实现的
(gdb) q
The program is running.  Exit anyway? (y or n) y
[root@localhost hello]#
还有其它很多命令,我就不一一介绍拉。  
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 的理解。

《彻底搞定C指针》第3篇--指针与数组名
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:76
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上实验。

《彻底搞定C指针》第4篇const int * pi/int * const pi的区别
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:69
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的操作了,这是一个错误的观点啊!)。


彻底搞定C指针——第5篇:函数参数的传递
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:99
一. 三道考题 开讲之前,我先请你做三道题目。(嘿嘿,得先把你的头脑搞昏才行……唉呀,谁扔我鸡蛋?)

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的操作了,这是一个错误的观点啊!)。


第6篇 指向另一指针的指针
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:119
一.针概念:
早在本系列第二篇中我就对指针的实质进行了阐述。今天我们又要学习一个叫做指向另一指针地址的指针。让我们先回顾一下指针的概念吧!
当我们程序如下申明变量:
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值的修改。
你自行去调试。
经过修改后的程序就可以完成所要的功能了。
看懂了这个例子,也就达到了本篇所要求的目的。

第7篇 函数名与函数指针
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:94
一 数调用
    一个通常的函数调用的例子:
//自行包含头文件
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);
}
输出结果:略
分析:(看我写的注释。你可按我注释的①②③④⑤顺序自行分析。)
在 Unix 上写过程序的人一般都遇到过 Makefile,尤其是用 C 来开发程序的人。用 make 来开发和编译程序的确很方便,可是要写出一个MakeFile就不那么简单了。偏偏介绍 Makefile 的文件不多,GNU Make 那份印出来要几百页的文件,光看完 Overview 自己就快要先Over了,难怪许多人闻 Unix色变。本文将介绍如何利用 GNU Autoconf 及 Automake 这两套软件来帮助『自动』产生 Makefile 文件,并且让开发出来的的软件可以象 Apache, MySQL 和常见的 GNU 软件一样,只要会 ``./configure'', ``make'', ``make install'' 就可以把程序安装到系统中。如果您有心开发 Open Source 的软件,或只是想在 Unix 系统下写写程序。希望这份介绍文件能帮助您轻松的进入 Unix Programming 的殿堂。  
1. 简介  
Makefile 基本上就是『目标』(target), 『关联』(dependencies) 和『动作』三者所组成的一系列规则。而 make 就会根据 Makefile 的规则来决定如何编译 (compile) 和连接 (link) 程式。实际上,make 可做的不只是编译和连接程序,例如 FreeBSD 的 port collection 中,Makefile还可以做到自动下载远程程序,解压缩 (extract) , 打补丁 (patch),设定,然后编译,安装到系统中。  
Makefile 基本结构虽然很简单,但是妥善运用这些规则就可以变换出许多不同的花样。却也因为这样,许多刚刚开始学习写Makefile 时会觉得没有规范可以遵循,每个人写出来的Makefile都不大一样,不知道从哪里下手,而且常常会受到自己的开发环境的限制,只要环境参数不同或者路径更改,可能 Makefile 就得跟着修改修改。虽然有 GNU Makefile Conventions (GNU Makefile惯例例)订出一些使用 GNU 程式设计时撰写 Makefile 的一些标准和规范,但是内容很长而且很复杂,并且经常作一些调整,为了减轻程序开发人员维护Makefile 的负担,因此出现了Automake。  
程序设计者只需要写一些预先定义好的宏 (macro),提交给Automake处理后会产生一个可以供 Autoconf 使用的 Makefile.in文件。再配合利用 Autoconf产生的自动培植设置文件 configure 即可产生一份符合符合 GNU Makefile 惯例的 Makeifle 了。  
2. 上路之前  
在开始使用 Automake 之前,首先确认你的系统安装有如下软件:
   
1.  
GNU Automake  
2.  
GNU Autoconf  
3.  
GNU m4  
4.  
perl  
5.  
GNU Libtool (如果你需要产生 shared library)  
建议最好也使用 GNU C/C++ 编译器 、GNU Make 以及其它 GNU 的工具程序来作为开发的环境,这些工具都是属于 Open Source Software 不但免费而且功能强大。如果你是使用 Red Hat Linux 可以找到所有上述软件的 rpm 文件,FreeBSD 也有现成的 package 可以直接安装,或也可以自行下载这些软件的源代码回来安装。下面的示例是在Red Hat Linux 5.2 + CLE2 的环境下所完成的。  
3. 一个简单的例子  
Automake 所产生的 Makefile 除了可以做到程式的编译和连接,也已经把如何产生程序文件 (如 manual page, info 文件及 dvi 文件) 的动作,还有把源码文件包装起来以供发布都考虑进去了,所以程序源代码所存放的目录结构最好符合GNU 的标准惯例,接下来就用一个hello.c 来做为例子。  
在工作目录下建立一个新的子目录"devel"',再在 devel 下建立一个"hello"' 的子目录,这个目录将作为存放 hello这个程序及其相关文件的地方:  
% mkdir devel
% cd devel
% mkdir hello  
% cd hello  
用编辑器写一个hello.c文件,  
#include  
int main(int argc, char** argv) {  
printf(``Hello, GNU!\n'');  
return 0;  
}  
接下来就要用 Autoconf 及 Automake 来产生 Makefile 文件了,  
1.  
用 autoscan 产生一个 configure.in 的原型,执行autoscan 后会产生一个configure.scan 的文件,可以用它作为 configure.in文件的蓝本。  
   
% autoscan  
% ls  
configure.scan hello.c  
2.  
编辑 configure.scan文件,如下所示,并且改名为configure.in  
dnl Process this file with autoconf to produce a configure script. AC_INIT(hello.c) AM_INIT_AUTOMAKE(hello, 1.0)  
dnl Checks for programs.  
AC_PROG_CC  
dnl Checks for libraries.  
dnl Checks for header files.  
dnl Checks for typedefs, structures, and compiler characteristics.  
dnl Checks for library functions.  
AC_OUTPUT(Makefile)  
3.  
执行 aclocal 和 autoconf ,分别会产生 aclocal.m4 及 configure 两个文件  
% aclocal  
% autoconf  
% ls  
aclocal.m4 configure configure.in hello.c  
4.  
编辑 Makefile.am 文件,内容如下  
AUTOMAKE_OPTIONS= foreign  
bin_PROGRAMS= hello  
hello_SOURCES= hello.c  
5.  
执行 automake --add-missing ,Automake 会根据Makefile.am 文件产生一些文件,包含最重要的 Makefile.in  
% automake --add-missing automake: configure.in: installing `./install-sh' automake: configure.in: installing `./mkinstalldirs' automake: configure.in: installing `./missing'  
6.  
最后执行 ./configure ,  
% ./configure creating cache ./config.cache checking for a BSD compatible install... /usr/bin/install -c checking whether build environment is sane... yes checking whether make sets $... yes checking for working aclocal... found checking for working autoconf... found checking for working automake... found checking for working autoheader... found checking for working makeinfo... found checking for gcc... gcc checking whether the C compiler (gcc ) works... yes checking whether the C compiler (gcc ) is a cross-compiler... no checking whether we are using GNU C... yes checking whether gcc accepts -g... yes updating cache ./config.cache creating ./config.status creating Makefile  
现在你的目录下已经产生了一个 Makefile 档,下个 ``make'' 指令就可以开始编译 hello.c 成执行档,执行 ./hello 和 GNU 打声招呼吧!  
% make gcc -DPACKAGE=\"hello\" -DVERSION=\"1.0\" -I. -I. -g -O2 -c hello.c gcc -g -O2 -o hello hello.o % ./hello Hello! GNU!  
你还可以试试 ``make clean'',''make install'',''make dist'' 看看会有什麼结果。你也可以把产生出来的 Makefile 秀给你的老板,让他从此对你刮目相看 :-)  
4. 追根问底  
上述产生Makefile 的过程和以往自行编写的方式非常不一样,舍弃传统自定义make 的规则,使用 Automake 只需用到一些已经定义好的宏就可以了。我们把宏及目标 (target)写在Makefile.am 文件内,Automake 读入 Makefile.am 文件后会把这一串已经定义好的宏展开并产生相对应的 Makefile.in 文件,然后再由 configure这个 shell script 根据 Makefile.in 产生合适的Makefile。  


利用 autoconf 及 automake产生Makefile 的流程

上图表示在上一范例中要使用的文件档案及产生出来的文件,有星号 (*) 者代表可执行文件。在此示例中可由 Autoconf 及 Automake 工具所产生的额外文件有 configure.scan、aclocal.m4、configure、Makefile.in,需要自行加入设置的有configure.in 及 Makefile.am。  
4.1 编辑 configure.in 文件  
Autoconf 是用来产生 'configure'文件的工具。'configure' 是一个 shell script,它可以自动设定原始程序以符合各种不同平台上Unix 系统的特性,并且根据系统参数及环境产生合适的Makefile文件或C 的头文件(header file),让原始程式可以很方便地在不同的平台上进行编译。Autoconf会读取 configure.in 文件然后产生'configure' 这个 shell script。  
configure.in 文件内容是一系列GNU m4 的宏,这些宏经autoconf处理后会变成检查系统特性的shell scripts。 configure.in 内宏的顺序并没有特别的规定,但是每一个configure.in 文件必须在所有宏前加入 AC_INIT 宏,然后在所有宏的最后加上 AC_OUTPUT宏。可先用 autoscan 扫描原始文件以产生一个 configure.scan 文件,再对 configure.scan 做些修改成 configure.in 文件。在范例中所用到的宏如下:  
dnl  
这个宏后面的字不会被处理,可以视为注释  
AC_INIT(FILE)  
该宏用来检查源代码所在路径,autoscan 会自动产生,一般无须修改它。  
AM_INIT_AUTOMAKE(PACKAGE,VERSION)  
这个是使用 Automake 所必备的宏,PACKAGE 是所要产生软件套件的名称,VERSION 是版本编号。  
AC_PROG_CC  
检查系统可用的C编译器,若源代码是用C写的就需要这个宏。  
AC_OUTPUT(FILE)  
设置 configure 所要产生的文件,若是Makefile ,configure 便会把它检查出来的结果带入 Makefile.in 文件后产生合适的 Makefile。  
实际上,这里使用 Automake 时,还需要一些其他的宏,这些额外的宏我们用 aclocal来帮助产生。执行 aclocal会产生aclocal.m4 文件,如果无特别的用途,可以不需要修改它,用 aclocal 所产生的宏会告诉 Automake如何动作。  
有了 configure.in 及 aclocal.m4两个文件以后,便可以执行 autoconf来产生 configure 文件了。  
4.2 编辑Makefile.am 文件  
接下来要编辑Makefile.am 文件,Automake 会根据 configure.in 中的宏把Makefile.am 转成 Makefile.in 文件。 Makefile.am 文件定义所要产生的目标:  
AUTOMAKE_OPTIONS  
设置 automake 的选项。Automake 主要是帮助开发 GNU 软件的人员来维护软件,所以在执行 automake 时,会检查目录下是否存在标准 GNU 软件中应具备的文件,例如 'NEWS'、'AUTHOR'、'ChangeLog' 等文件。设置 foreign 时,automake 会改用一般软件的标准来检查。  
bin_PROGRAMS  
定义要产生的执行文件名。如果要产生多个执行文件,每个文件名用空白符隔开。  
hello_SOURCES  
定义 'hello' 这个执行程序所需要的原始文件。如果 'hello'这个程序是由多个原始文件所产生,必须把它所用到的所有原始文件都列出来,以空白符隔开。假设 'hello' 还需要 'hello.c'、'main.c'、'hello.h' 三个文件的话,则定义  
hello_SOURCES= hello.c main.c hello.h  
如果定义多个执行文件,则对每个执行程序都要定义相对的filename_SOURCES。  
编辑好 Makefile.am 文件,就可以用 automake --add-missing来产生 Makefile.in。加上 --add-missing 选项来告诉 automake顺便假如包装一个软件所必须的文件。Automake产生生出来的 Makefile.in 文件是完全符合 GNU Makefile 的惯例,只要执行 configure这个shell script 便可以产生合适的 Makefile 文件了。  
4.3 使用 Makefile  
利用 configure 所产生的 Makefile文件有几个预先设定的目标可供使用,这里只用几个简述如下:  
make all  
产生设定的目标,既次范例中的执行文件。只敲入make 也可以,此时会开始编译源代码,然后连接并产生执行文件。  
make clean  
清除之前所编译的执行文件及目标文件(object file, *.o)。  
make distclean  
除了清除执行文件和目的文件以外,也把 configure 所产生的 Makefile 清除掉。  
make install  
将程序安装到系统中,若源码编译成功,且执行结果正确,便可以把程序安装到系统预先设定的执行文件存放路径中,若用 bin_PROGRAMS 宏的话,程序会被安装到 /usr/local/bin下。  
make dist  
将程序和相关的文档包装为一个压缩文档以供发布 (distribution) 。执行完在目录下会产生一个以PACKAGE-VERSION.tar.gz 为名称的文件。PACKAGE 和 VERSION 这两个参数是根据 configure.in 文件中 AM_INIT_AUTOMAKE(PACKAGE, VERSION) 的定义。在此范例中会产生 'hello-1.0.tar.gz' 的文件。  
make distcheck  
和 make dist 类似,但是加入检查包装以后的压缩文件是否正常,这个目标除了把程序和相关文档包装成 tar.gz 文件外,还会自动把这个压缩文件解开,执行 configure,并执行 make all ,确认编译无错误以后,户显示这个 tar.gz 文件已经准备好可以发布了。这个检查非常有用,检查过关的套件,基本上可以给任何具备 GNU 开发环境的人去重新编译成功。就 hello-1.tar.gz 这个范例而言,除了在Red Hat Linux 上,在 FreeBSD 2.2.x 也可以正确编译。  
要注意的是,利用 Autoconf 及 Automake 所产生出来的软件套件是可以在没有安装 Autoconf 及 Automake 的环境使用的,因为 configure 是一个 shell script,它己被设计为可以在一般 Unix 的 sh 这个 shell 下执行。但是如果要修改 configure.in 及 Makefile.am 文件再产生新的 configure 及 Makefile.in 文件时就一定要有 Autoconf 及 Automake 了。  
5. 相关资料  
Autoconf 和 Automake 功能十分强大,可以从它们附带的 info 稳当4中找到详细的使用方法说明。你也可以从许多现有的GNU 软件或 Open Source 软件中找到相关的 configure.in 或 Makefile.am 文件,他们是学习 Autoconf 及 Automake 更多技巧的最佳范例。
这个简介只用到了 Autoconf 及 Automake 的皮毛罢了,如果你有心加入 Open Source 软件开发的行列,希望这篇文章可以帮助你对产生 Makefile 有个简单的了解。其它有关开发 GNU 程式或 C 程序设计及 Makefile 的详细运用及技巧,建议从 GNU Coding Standards (GNU 编码规定) 读起,里面包含了 GNU Makefile 惯例,及开发 GNU 软件的标准程序和惯例。这些 GNU 软件的在线说明文件可以在 http://www.gnu.org/ 上找到。  
6. 结束语  
利用 Autoconf 及 Automake,产生一个 Makefile 似乎不再象以前那么困难了,而使用 Autoconf 也使得我们在不同平台上或各家 Unix 之间发布及便宜程序变的简单,这对于在Unix 系统上程序开发员来说减轻了许多负担。妥善运用这些 GNU 的工具软件,可以帮助我们更容易的去开发程序,而且更容易维护源代码。
匹配正则:test是文件名
awk '/^(no|yes)/' test  


几个实例
$ awk '/^(no|so)/' test-----打印所有以模式no或so开头的行。

$ awk '/^[ns]/{print $1}' test-----如果记录以n或s开头,就打印这个记录。

$ awk '$1 ~/[0-9][0-9]$/(print $1}' test-----如果第一个域以两个数字结束就打印这个记录。

$ awk '$1 == 100 || $2 < 50' test-----如果第一个或等于100或者第二个域小于50,则打印该行。

$ awk '$1 != 10' test-----如果第一个域不等于10就打印该行。

$ awk '/test/{print $1 + 10}' test-----如果记录包含正则表达式test,则第一个域加10并打印出来。

$ awk '{print ($1 > 5 ? "ok "$1: "error"$1)}' test-----如果第一个域大于5则打印问号后面的表达式值,否则打印冒号后面的表达式值。

$ awk '/^root/,/^mysql/' test----打印以正则表达式root开头的记录到以正则表达式mysql开头的记录范围内的所有记录。如果找到一个新的正则表达式root开头的记录,则继续打印直到下一个以正则表达式mysql开头的记录为止,或到文件末尾。



awk 用法:awk ‘ pattern {action} ‘

变量名 含义
ARGC 命令行变元个数
ARGV 命令行变元数组
FILENAME 当前输入文件名
FNR 当前文件中的记录号
FS 输入域分隔符,默认为一个空格
RS 输入记录分隔符
NF 当前记录里域个数
NR 到目前为止记录数
OFS 输出域分隔符
ORS 输出记录分隔符

1、awk ‘/101/’ file 显示文件file中包含101的匹配行。
awk ‘/101/,/105/’ file
awk ‘$1 == 5′ file
awk ‘$1 == “CT”‘ file 注意必须带双引号
awk ‘$1 * $2 >100 ‘ file
awk ‘$2 >5 && $21000000 ‘ 通过管道符获得输入,如:显示第4个域满足条件的行。
4、awk -F “|” ‘{print $1}’ file 按照新的分隔符“|”进行操作。
awk ‘BEGIN { FS=”[: \t|]” }
{print $1,$2,$3}’ file 通过设置输入分隔符(FS=”[: \t|]”)修改输入分隔符。

Sep=”|”
awk -F $Sep ‘{print $1}’ file 按照环境变量Sep的值做为分隔符。
awk -F ‘[ :\t|]’ ‘{print $1}’ file 按照正则表达式的值做为分隔符,这里代表空格、:、TAB、|同时做为分隔符。
awk -F ‘[][]’ ‘{print $1}’ file 按照正则表达式的值做为分隔符,这里代表[、]
5、awk -f awkfile file 通过文件awkfile的内容依次进行控制。
cat awkfile
/101/{print “47 Hello! 47″} –遇到匹配行以后打印 ‘ Hello! ‘.47代表单引号。
{print $1,$2} –因为没有模式控制,打印每一行的前两个域。
6、awk ‘$1 ~ /101/ {print $1}’ file 显示文件中第一个域匹配101的行(记录)。
7、awk ‘BEGIN { OFS=”%”}
{print $1,$2}’ file 通过设置输出分隔符(OFS=”%”)修改输出格式。
8、awk ‘BEGIN { max=100 ;print “max=” max} BEGIN 表示在处理任意行之前进行的操作。
{max=($1 >max ?$1:max); print $1,”Now max is “max}’ file 取得文件第一个域的最大值。
(表达式1?表达式2:表达式3 相当于:
if (表达式1)
表达式2
else
表达式3
awk ‘{print ($1>4 ? “high “$1: “low “$1)}’ file
9、awk ‘$1 * $2 >100 {print $1}’ file 显示文件中第一个域匹配101的行(记录)。
10、awk ‘{$1 == ‘Chi’ {$3 = ‘China’; print}’ file 找到匹配行后先将第3个域替换后再显示该行(记录)。
awk ‘{$7 %= 3; print $7}’ file 将第7域被3除,并将余数赋给第7域再打印。
11、awk ‘/tom/ {wage=$2+$3; printf wage}’ file 找到匹配行后为变量wage赋值并打印该变量。
12、awk ‘/tom/ {count++;}
END {print “tom was found “count” times”}’ file END表示在所有输入行处理完后进行处理。
13、awk ‘gsub(/\$/,”");gsub(/,/,”"); cost+=$4;
END {print “The total is $” cost>”filename”}’ file gsub函数用空串替换$和,再将结果输出到filename中。
1 2 3 $1,200.00
1 2 3 $2,300.00
1 2 3 $4,000.00

awk ‘{gsub(/\$/,”");gsub(/,/,”");
if ($4>1000&&$42000&&$43000&&$43000&&$43000) next;
else c4+=$4; }
END {printf “c4=[%d]\n”,c4}”‘ file
通过next在某条件时跳过该行,对下一行执行操作。

14、awk ‘{ print FILENAME,$0 }’ file1 file2 file3>fileall 把file1、file2、file3的文件内容全部写到fileall中,格式为
打印文件并前置文件名。
15、awk ‘ $1!=previous { close(previous); previous=$1 }
{print substr($0,index($0,” “) +1)>$1}’ fileall 把合并后的文件重新分拆为3个文件。并与原文件一致。
16、awk ‘BEGIN {”date”|getline d; print d}’ 通过管道把date的执行结果送给getline,并赋给变量d,然后打印。
17、awk ‘BEGIN {system(”echo \”Input your name:\\c\”"); getline d;print “\nYour name is”,d,”\b!\n”}’
通过getline命令交互输入name,并显示出来。
awk ‘BEGIN {FS=”:”; while(getline0) { if($1~”050[0-9]_”) print $1}}’
打印/etc/passwd文件中用户名包含050x_的用户名。

18、awk ‘{ i=1;while(i28) flag=1;
if ((j==4||j==6||j==9||j==11)&&i>30) flag=1;
if (flag==0) {printf “%02d%02d “,j,i}
}
}
}’
19、在awk中调用系统变量必须用单引号,如果是双引号,则表示字符串
Flag=abcd
awk ‘{print ‘$Flag’}’ 结果为abcd
awk ‘{print “$Flag”}’ 结果为$Flag

早上在签到的时候,看到玩得好的正看一道人家答的面试题,其中一道是C语言的折半查找,呵呵。。。
来个 原理:

以升序为例

1:第一各中间值是 全部元素的个数/2(或者(元素的序数+1)/2 )

2:判断你所要的值和这个中间值的大小

如果大,那么就是 (第一次中间值序数+1 + 末尾元素序数)/2

如果小,那么就是 (第一次中间值序数-1 + 首元素序数(通常是0))/2

这样逐步缩小范围

3:而后如果出现

比中间值小(这一轮的中间值),但是比上一步中间值大(上一轮的中间值)

那么, 新的中间值序数=((上轮中间值序数)+(这轮中间值序数))/2


如果是降序,则反之

这个折半查找法的思想 和 微积分中间的中值定理的思维有点像

指针

WEB2.0 jackxiang 2007-11-8 18:35
指针是C/C++的精华,也是最难的部分。——所有学习C/C++的人都明白这点,当年我初学的时候也是这样。但是,现在再回想指针,我却很难回忆它究竟难在哪儿。应该说这就叫“难者不会,会者不难”吧。“饱汉不知饿汉饥”是有一定的道理的,即使饱汉曾经饿过。
  本书中规中矩地讲解了指针的概念、定义与初始化、操作等。正如上面提到的“饱汉不知饿汉饥”,我似乎很健忘,以至于不记得指针的难点在哪儿了。
  指针的灵活性可以把大量的工作化繁为易,前提是必须首很把足够繁的指针弄懂。听起来有点像绕口令,事实就是这样,你现在把难懂的东西弄懂了,日后可以把难事化简,大事化小。
  从VB过来的人一定会熟悉“值传递”和“地址传递”这两个概念,实际上,“地址传递”这种说法正是为了弥补VB没有指针却有类似的需要才发明的。我认为C/C++程序员要想深入理解指针,首先要抛弃这个概念。在C/C++程序中,即使在函数调用中传递指针,也不能说“地址传递”,还应该说是值传递,只不过这次传递的值有点特殊,特殊在于借用这个值,可以找到其它值。就好像我给你一把钥匙一样,你通过钥匙可以间接获得更多,但是我给你的只不过是钥匙。
  我前阵子曾写过一篇关于指针的文章,之所以写那篇文章,是因为看到一大堆初学者在论坛上提问。通过对他们提的问题的分析,我总结了几点。下面,首先就先引用我自己写的《关于指针》中的片段吧(完整的文章请到我的个人主页查找):
  一、指针就是变量:
  虽然申明指针的时候也提类型,如:
  char *p1;
  int *p2;
  float *p3;
  double *p4;
  .....
  但是,这只表示该指针指向某类型的数据,而不表示该指针的类型。说白了,指针都是一个类型:四字节无符号整数(将来的64位系统中可能有变化)。
  二、指针的加减运算很特殊:
  p++、p--之类的运算并不是让p这个“四字节无符号整数”加一或减一,而是让它指向下一个或上一个存储单元,它实际加减的值就是它所指类型的值的size。
  比如:
  char *型指针,每次加减的改变量都是1;
  float *型的指针,每次加减的改变量都是4;
  void *型指针无法加减。
  还要注意的是:指针不能相加,指针相减的差为int型。
  正是因为指针有着不同于其它变量的运算方式,所以,在任何时候用到指针都必须明确“指针的类型”(即指针所指的变量的类型)。这就不难理解为什么函数声明时必须用“int abc(char *p)”而调用的时候却成了“a = abc(p);”这样的形式了。
  三、用指针做参数传递的是指针值,不是指针本身:
  要理解参数传递,首先必须把“形参”与“实参”弄明白。
  函数A在调用函数B时,如果要传递一个参数C,实际是在函数B中重新建立一个变量C,并将函数A中的C值传入其中,于是函数B就可以使用这个值了,在函数B中,无论有没有修改这个C值,对于函数A中的C都没有影响。函数B结束时,会将所有内存收回,局部变量C被销毁,函数B对变量C所做的一切修改都将被抛弃。
  以上示例中,函数A中的变量C称为“实参”,函数B中的变量C被称为“形参”,调用函数时,会在B函数体内建立一个形参,该形参的值与实参的值是相同的,但是形参的改变不影响实参,函数结束时,形参被销毁,实参依然没有发生变化。
  指针也是一个变量,所以它也符合以上的规定,但是,指针存放的不仅仅是一个值,而是一个内存地址。B函数对这个地址进行了改动,改动的并不是形参,而是形参所指的内存。由于形参的值与实参的值完全相同,所以,实参所指的内存也被修改。函数结束时,虽然这个形参会被销毁,指针的变化无法影响实参,但此前对它所指的内存的修改会持续有效。所以,把指针作为参数可以在被调函数(B)中改变主调函数(A)中的变量,好像形参影响了实参一样。
  注意:是“好像”。在这过程中,函数B影响的不是参数,而是内存。
  下面再来看刚才的例子:“int abc(char *p)”和“a = abc(p);”。为什么申请中要用*号,因为函数必须知道这是指针;为什么调用时不加*号,因为传递的是“指针值”,而不是“指针所指内存的值”。
  四、指向指针的指针:
  正因为指针也是一个变量,它一样要尊守形参与实参的规定。所以,虽然指针做参数可以将函数内对变量的修改带到函数外,但是,函数体内对指针本身作任何修都将被丢弃。如果要让指针本身被修改而且要影响函数外,那么,被调函数就应该知道“该指针所在的内存地址”。这时,指针不再是指针,而是“普通变量”。作为参数传递的不是这个“普通变量”,而是指向这个“普通变量”的指针。即“指向指针的指针”。
  如果p是一个指向指针的指针,那么*p就是一个指针,我们不妨就把它看成q。要访问q指针所指的内存,只要*q就是了。用初中数学的“等量代换”一换就知道,*q就是**p。
  五、指针数组。
  之所以要把“指针数组”单独提出来,是因为数组本身就与指针有着千丝万缕的关系。即使你不想用指针,只要你使用了数组,实际就在与指针打交道了。
  只要理解了指针本身就是变量,就不难理解“指针数组”,我们可以暂且把它当成普通数组来处理,a[0]、a[1]、a[2]……就是数组的元素,只是,a[0]是一个指针,a[1]、a[2]也是一个指针。那a呢?当然也是指针,但这是两码事。你可以完全无视a的存在,只去管a[0]等元素。*a[0]与*p没有什么本质的区别。  
  还有一个东西不得不提一下,它比较重要:
  指针的定义有两个可取的方式,它们各有优缺点:“int *p;”和“int* p;”是完全等价的,后者的好处是让人体会到p是一个“指向int的”指针,前者会让人误解为*p是一个int型变量(这里没有定义int型变量);但是前者的好处是不会产生混淆,如“int *p, *q;”让人一眼就看出定义了两个指针,而“int* p,q;”会让人误解成定义了两个指针(实际上q不是指针)。
Here are two sample files that we will use in numerous examples to
illustrate the output of `diff' and how various options can change it.
This is the file `lao':
The Way that can be told of is not the eternal Way;
The name that can be named is not the eternal name.
The Nameless is the origin of Heaven and Earth;
The Named is the mother of all things.
Therefore let there always be non-being,
so we may see their subtlety,
And let there always be being,
so we may see their outcome.
The two are the same,
But after they are produced,
they have different names.
This is the file `tzu':
The Nameless is the origin of Heaven and Earth;
The named is the mother of all things.
Therefore let there always be non-being,
so we may see their subtlety,
And let there always be being,
so we may see their outcome.
The two are the same,
But after they are produced,
they have different names.
They both may be called deep and profound.
Deeper and more profound,
The door of all subtleties!


看来国外程序员对中国的禅和道有兴趣的不少啊, python的一个East Egg(import this)题目就叫The Zen of Python, 除了The Zen of Programming 和 The Tao of Prgramming之外,那本大名鼎鼎的“The Art of Unix Programming”原版封面就是一个老和尚和一个小和尚, 最近一本畅销书也叫“The Zen of CSS” 。。
            以本人性格,就是喜欢抄别人的,这次来点自己的吧,其实也是大家的,一哥们去百度参加面试的题目。。。呵呵,见笑!
如百度4
baidu4:
11 11 11 22 33 44 ...
baidu4中有一行以空格隔开的十进制数,用shell编程求出它们的和并打印。
他shell不行,他用php写了一个。
求正解:
我写了一个如下:
FILE="baidu4"
read line < $FILE
r=0
for num in $line;do
r=$(expr $num + $r)
done
echo $r
在猜朋友写的php,我也写一个PHP的:
<?php
$lines = file('baidu4');
$result2 = 0;
foreach ($lines as $line_num => $line) {
  $result = explode(" ",$line);
  for($i=0;$i<count($result);$i++)
    {
    $result2 += $result[$i];
    }
  echo $result2;        
}
?>

再来个awk的:
#!/usr/bin/awk -f
BEGIN{}
        {
                sum = 0;
                for (i=1; i<=NF; i++)
                {
                        sum += $i;
                }
                printf("count == [%d]\n",  sum);
        }
END{}



我在用c语言写一个吧:
太难用指针,让玩得好的指导下写了一个感谢罗玉峰,可以求多行的结果呢:
#include <string.h>
#include <stdio.h>
char *pp,*p;
char linebuf[4096];
char tmp[1024];
int i,tmpl;
int  main(void)
{
FILE *fp;
if ( ( fp = fopen ("baidu4", "r") ) == NULL )
{
       printf("cant't open the baidu4 file ");
       exit(0);
}
while(fgets(linebuf,4096,fp))
{    
linebuf[strlen(linebuf)-1] = 0;
pp=linebuf;
tmpl=0;
p=strchr(pp,' ');
while(p != NULL)
{
  strncpy(tmp, pp, p-pp);
  printf("tmp=[%s]\n", tmp);
  tmpl+=atoi(tmp);
//  memset(tmpl, 0x00, sizeof(tmpl));
  memset(tmp, 0x00, sizeof(tmp));
  pp=p+1;
  p=strchr(pp,' ');
}
if (*pp)
{
  strcpy(tmp, pp);
  tmpl+=atoi(tmp);
  printf("tmp=[%s]\n", tmp);
}
printf("total=%d",tmpl);
memset(linebuf, 0, sizeof(linebuf));
//memset(tmpl, 0, sizeof(tmpl));
printf("----------------\n");
}
return 0;
}
那位哥们能用java写个就完美了,:-)
这位留言的哥哥真高,用sed替换和管道导入计算器bc来计算,确实很高:

Exaple: baidu4
11 11 11

jackxiang@jackxiang-laptop:~$ sed 's/ /+/g' baidu4
11+11+11
jackxiang@jackxiang-laptop:~$ sed 's/ /+/g' baidu4| bc
33
jackxiang@jackxiang-laptop:~$

感谢那个留言的哥们,bc
echo " 930307 -   921336"|bc

[/home/jackxiang/bc]# echo " 930307 -   921336"|bc
8971
   将10进制数转换成16进制数

  比如转换 65535 为 16进制

  echo 'obase=16; 65535' | bc

  得到 FFFF



echo 'obase=16; ibase=8; 177777' | bc

  可以直接将八进制的数177777变成十六进制,也是FFFF
复习一下文件的操作,很多时候都用的上。

fread函数和fwrite函数

1.函数功能

 用来读写一个数据块。

2.一般调用形式

 fread(buffer,size,count,fp);

 fwrite(buffer,size,count,fp);

3.说明

 (1)buffer:是一个指针,对fread来说,它是读入数据的存放地址。对fwrite来说,是要输出数据的地址。

 (2)size:要读写的字节数;

 (3)count:要进行读写多少个size字节的数据项;

 (4)fp:文件型指针。

注意:1 完成次写操(fwrite())作后必须关闭流(fclose());

          2 完成一次读操作(fread())后,如果没有关闭流(fclose()),则指针(FILE * fp)自动向后移动前一次读写的长度,不关闭流继续下一次读操作则接着上次的输出继续输出;

          3 fprintf() : 按格式输入到流,其原型是int fprintf(FILE *stream, const char *format[, argument, ...]);其用法和printf()相同,不过不是写到控制台,而是写到流罢了。注意的是返回值为此次操作写入到文件的字节数。如int c = fprintf(fp, "%s %s %d %f", str1,str2, a, b) ;str1:10字节;str2: 10字节;a:2字节;b:8字节,c为33,因为写入时不同的数据间自动加入一个空格。

文件使用之后一定要关闭,否则将不能正确显示内容.fwrite:读入两个学生信息然后用fwrite存入文件

fread:用fread从文件中读出学生信息。



fwrite.c

#include <stdio.h>
#define SIZE 2
struct student_type
{
char name[10];
int num;
int age;
char addr[10];
}stud[SIZE];
void save()
{
FILE *fp;
int i;
if((fp=fopen("stu_list","wb"))==NULL)
{
 printf("cant open the file");
 exit(0);
}
for(i=0;i<SIZE;i++)
{
  if(fwrite(&stud[i],sizeof(struct student_type),1,fp)!=1)
   printf("file write error\n");
}
fclose(fp);
}
main()
{
int i;
for(i=0;i<SIZE;i++)
{
  scanf("%s%d%d%s",&stud[i].name,&stud[i].num,&stud[i].age,&stud[i].addr);
  save();
}
for(i=0;i<SIZE;i++)
{
  printf("%s,%d,%d",stud[i].name,stud[i].num,stud[i].age,stud[i].addr);
}
}






fread.c

#include <stdio.h>
#define SIZE 2
struct student_type
{
char name[10];
int num;
int age;
char addr[10];
}stud[SIZE];
void read()
{
FILE *fp;
int i;
if((fp=fopen("stu_list","rb"))==NULL)
{
 printf("cant open the file");
 exit(0);
}
for(i=0;i<SIZE;i++)
{
  if(fread(&stud[i],sizeof(struct student_type),1,fp)!=1)
   printf("file write error\n");
}
fclose(fp);
}
main()
{

int i;
read();
for(i=0;i<SIZE;i++)
{
  printf("%s,%d,%d,%s",stud[i].name,stud[i].num,stud[i].age,stud[i].addr);
  printf("\n");
}
}

2007年4月16日更新

增加了一个令牌的判断,可以抓新版本的QQ和老版本的QQ包了.

2007年4月16日更新

因为朋友们说这个程序已经无法捕捉到最新的QQ登陆包了,所以修改了一下.因为时间关系,我没有修改本文的其他部分,仅仅修改了代码.

作者:梅劲松
本文档和程序为MIT授权

说到嗅探必须要讲到所支持的环境,并非只能对本机或者HUB环境才能使用。对于交换机,你可以指定一个口为嗅探口,从这个口能拿到所有端口的数据。如果这个交换是核心交换,那么你所能取到数据将更多。(三层交换一般都可以指定嗅探口)
1、如果你已经是个python爱好者你机器一定有了python的运行环境,如果你没有可以到www.python.org去下载一个。我使用的还是python 2.3。
2、这段程序需要pcap模块支持,你可以到http://monkey.org/~dugsong/pypcap/去下载一个,它有unix和win两个版本,请注意,win下他需要winpcap支持,如果你没有这个,请再下载winpcap。同样,如果你是在unix下使用,请下载libpcap。
3、安装pcap没有太多说的,win下是个exe,直接运行。unix下直接make就可以了。
4、打开你的记事本,将以下代码保存在sniffer-QQ.py这个文件中。


# -*- coding: cp936 -*-
import pcap ,struct

pack=pcap.pcap()
pack.setfilter('udp')
key=''
for recv_time,recv_data in pack:
  recv_len=len(recv_data)
  if recv_len == 102 and recv_data[42]== chr(02) and recv_data[101] == chr(03):
     print struct.unpack('>I',recv_data[49:53])[0]
     print '登陆了'
  elif recv_len == 55:
     print struct.unpack('>I',recv_data[49:53])[0]
     print '登陆了'


如果你在*nix下运行,请将# -*- coding: cp936 -*-更改为# -*- coding: utf-8 -*-
好了,你可以运行你的python程序了,试着登陆你的QQ。看你的QQ号码是否被抓下来了。
这里付上我的抓屏结果
D:\socket-qq>;sniffer-QQ.py
278333853
12345
1234567890
1234567890
1234567890
278333853
1234567890
1234567890
278333853
278333853


利用的什么原理呢。
QQ使用udp协议来和服务器进行通讯,当数据包在传输的时候。udp报文被抓了下来。而登陆包是以0x02开头0x03为结尾的,我们先判断是否为正确的登陆包,当然,登陆包的长度都为102个字节。我们取出结构中特定的位置,就是你的QQ号码了。
如果还有什么疑问,请大家跟贴。

netstat -aon|findstr "端口号"查看端口是否被占用
C:\Users\Administrator>netstat -ano|findstr 80
  TCP    0.0.0.0:7680           0.0.0.0:0              LISTENING       3668
  TCP    0.0.0.0:8680           0.0.0.0:0              LISTENING       8552
  TCP    10.10.0.94:61717       47.102.197.26:80       ESTABLISHED     13932
  TCP    10.10.0.94:61720       116.128.171.192:8080   ESTABLISHED     8552
  TCP    10.10.0.94:61838       47.102.253.179:80      ESTABLISHED     13932
  TCP    10.10.0.94:62936       47.102.253.179:80      ESTABLISHED     13932
  TCP    10.10.0.94:62974       223.166.152.100:80     TIME_WAIT       0
  TCP    127.0.0.1:9080         0.0.0.0:0              LISTENING       13932
  TCP    [::]:7680              [::]:0                 LISTENING       3668
  UDP    [fe80::a564:aba:9613:90ce%14]:2177  *:*                                    9484
  UDP    [fe80::a564:aba:9613:90ce%14]:5353  *:*                                    3400
  UDP    [fe80::a8aa:4679:3642:9444%18]:2177  *:*                                    9484


今天在安装Apache的时候,最后显示80端口被占用,安装无法完成,我机器上没有装IIS,仔细想想也没有什么其他占用80端口的软件,最后google了一下,用下面的方法居然查出是被迅雷占用,退出迅雷,成功安装Apache,只是不明白迅雷为什么要用80端口。
netstat --help
  -a            显示所有连接和侦听端口。
  -n            以数字形式显示地址和端口号。
  -o          显示拥有的与每个连接关联的进程 ID。
————————————————————————————————————————————————
用这个命令,在DOS窗口执行:netstat -ano
看看占用0.0.0:80端口的PID是多少
然后在“任务管理器”中查到与该PID对应的程序。
如果任务管理器的进程页中看不到PID栏,则在任务管理器的菜单 查看--选择列中选择一下PID(进程标识符)就可以了。

点击在新窗口中浏览此图片
点击在新窗口中浏览此图片
点击在新窗口中浏览此图片




————————————————————————————————————————————————
在windows命令行窗口下执行:
C:/>netstat -aon&#124;findstr "4444"
TCP    127.0.0.1:4444         0.0.0.0:0              LISTENING       2434
由上面得知,端口被进程号为2434的进程占用,继续执行下面命令:
C:/>tasklist&#124;findstr "2434"
javaw.exe                     2434 Console                 0     16,064 K
这样就可以很轻松的kill掉javaw.exe 来腾出4444端口

WINXP下杀死进程的一个DOS命令 ntsd
杀进程,关键是找到这个进程的启动方式,不然下次重启它又出来了。其实用Windows自带的工具就能杀大部分进程:
c:/>ntsd -c q -p PID
只有System、SMSS.EXE和CSRSS.EXE不能杀。前两个是纯内核态的,最后那个是Win32子系统,ntsd本身需要它。ntsd 从2000开始就是系统自带的用户态调试工具。被调试器附着(attach)的进程会随调试器一起退出,所以可以用来在命令行下终止进程。使用ntsd自动就获得了debug权限,从而能杀掉大部分的进程。ntsd会新开一个调试窗口,本来在纯命令行下无法控制,但如果只是简单的命令,比如退出(q),用 -c参数从命令行传递就行了。
开个cmd.exe窗口,输入:
ntsd -c q -p PID
把最后那个PID,改成你要终止的进程的ID。如果你不知道进程的ID,任务管理器->进程选项卡->查看->选择列->勾上"PID(进程标识符)",然后就能看见了。

来自:http://blog.csdn.net/kbeanwu/article/details/5779365
PHP4.4.4, MySQL4.0.27 Apache/2.0.59

struct.c

WEB2.0 jackxiang 2007-11-6 17:31
* Demonstrates structures that contain other structures. */
/* Receives input for corner coordinates of a rectangle and
  calculates the area. Assumes that the y coordinate of the
  lower-right corner is greater than the y coordinate of the
  upper-left corner, that the x coordinate of the lower-
  right corner is greater than the x coordinate of the upper-
  left corner, and that all coordinates are positive. */
#include <stdio.h>
int length, width;
long area;
struct coord{
   int x;
   int y;
};
struct rectangle{
   struct coord topleft;
   struct coord bottomrt;
} mybox;
int main( void )
{
   /* Input the coordinates */
   printf("\nEnter the top left x coordinate: ");
   scanf("%d", &mybox.topleft.x);
   printf("\nEnter the top left y coordinate: ");
   scanf("%d", &mybox.topleft.y);
   printf("\nEnter the bottom right x coordinate: ");
   scanf("%d", &mybox.bottomrt.x);
   printf("\nEnter the bottom right y coordinate: ");
   scanf("%d", &mybox.bottomrt.y);
   /* Calculate the length and width */
   width = mybox.bottomrt.x - mybox.topleft.x;
   length = mybox.bottomrt.y - mybox.topleft.y;
   /* Calculate and display the area */
   area = width * length;
   printf("\nThe area is %ld units.\n", area);
   return 0;
}
FreeBSD套接字模型
BSD套接字构建在基本的UNIX®模型上: 一切都是文件。那么,在我们的例子中,套接字将使我接收一个HTTP文件,就这么说。然后我们要负责将 PNG文件从中提取出来。

  由于联网的复杂性,我们不能只使用 open系统调用,或open() C 函数。而是我们需要分几步 “打开”一个套接字。

  一旦我们做了这些,我们就能以处理任何文件描述符 的方式处理套接字。我们从它读取 (read),向它写入(write),建立管道(pipe), 必定还要关闭(close)它。
   重要的套接字函数
  FreeBSD提供了与套接字相关的不同函数, “打开”一个套接字我们只需要四个函数。 有时我们只需要两个。
   1 客户端-服务器差异
  典型情况中,以套接字为基础的数据通信一端是一个 服务器,另一端是一个客户端。

   1.1 通用元素
   1.1.1 socket

  这一个函数在客户端和服务器都要使用:socket(2)。它是这样被声明的:

    int socket(int domain, int type, int protocol);

  返回值的类型与open的相同,一个整数。 FreeBSD从和文件句柄相同的池中分配它的值。这就是允许套接字被以对文件相同的方式处理的原因。

  参数domain告诉系统你需要使用什么协议族。有许多种协议族存在,有些是某些厂商专有的,其它的都非常通用。协议族的声明在 sys/socket.h中

  使用PF_INET是对于 UDP, TCP 和其它网间协议(IPv4)的情况。

  对于参数type有五个定义好的值,也在 sys/socket.h中。这些值都以 “SOCK_”开头。 其中最通用的是SOCK_STREAM, 它告诉系统你正需要一个可靠的流传送服务 (和PF_INET一起使用时是指 TCP)。

  如果指定SOCK_DGRAM, 你是在请求无连接报文传送服务 (在我们的情形中是UDP)。

  如何你需要处理基层协议 (例如IP),或者甚至是网络接口 (例如,以太网),你就需要指定 SOCK_RAW。

  最后,参数protocol取决于前两个参数,并非总是有意义。在以上情形中,使用取值0。

  未连接的套接字: 对于函数socket 我们还没有指定我们要连往什么其它(主机)系统。 我们新建的套接字还是未连接的。这是有意的:拿电话类比,我们刚把调制解调器接在电话线上。我们既没有告诉调制解调器发起一个呼叫,也不会应答电话振铃。

   1.1.2 sockaddr
  各种各样的套接字函数需要指定地址,那是一小块内存空间 (用C语言术语是指向一小块内存空间的指针)。在 sys/socket.h中有各种各样如 struct sockaddr的声明。 这个结构是这样被声明的:

/*
  * 内核用来存储大多数种类地址的结构
 */
struct sockaddr {
     unsigned char   sa_len;     /* 总长度 */
     sa_family_t sa_family;  /* 地址族 */
    char        sa_data[14];    /* 地址值,实际可能更长 */
 };
#define SOCK_MAXADDRLEN 255     /* 可能的最长的地址长度 */
  注意对于sa_data域的定义具有不确定性。 那只是被定义为14字节的数组, 注释暗示内容可能超过14字节

  这种不确定性是经过深思熟虑的。套接字是个非常强大的接口。多数人可能认为比Internet接口强不到哪里 ──大多数应用现在很可能都用它 ──套接字可被用于几乎任何种类的进程间通信, Internet(更精确的说是IP)只是其中的一种。

  sys/socket.h提到的各种类型的协议 将被按照地址族对待,并把它们就列在 sockaddr定义的前面:

/*
* 地址族
*/
#define AF_UNSPEC       0               /* 未指定 */
#define AF_LOCAL        1               /* 本机 (管道,portal) */
#define AF_UNIX         AF_LOCAL        /* 为了向前兼容 */
#define AF_INET         2               /* 网间协议: UDP, TCP, 等等 */
#define AF_IMPLINK      3               /* arpanet imp 地址 */
#define AF_PUP          4               /* pup 协议: 例如BSP */
#define AF_CHAOS        5               /* MIT CHAOS 协议 */
#define AF_NS           6               /* 施乐(XEROX) NS 协议 */
#define AF_ISO          7               /* ISO 协议 */
#define AF_OSI          AF_ISO
#define AF_ECMA         8               /* 欧洲计算机制造商协会 */
#define AF_DATAKIT      9               /* datakit 协议 */
#define AF_CCITT        10              /* CCITT 协议, X.25 等 */
#define AF_SNA          11              /* IBM SNA */
#define AF_DECnet       12              /* DECnet */
#define AF_DLI          13              /* DEC 直接数据链路接口 */
#define AF_LAT          14              /* LAT */
#define AF_HYLINK       15              /* NSC Hyperchannel */
#define AF_APPLETALK    16              /* Apple Talk */
#define AF_ROUTE        17              /* 内部路由协议 */
#define AF_LINK         18              /* 协路层接口 */
#define pseudo_AF_XTP   19              /* eXpress Transfer Protocol (no AF) */
#define AF_COIP         20              /* 面向连接的IP, 又名 ST II */
#define AF_CNT          21              /* Computer Network Technology */
#define pseudo_AF_RTIP  22              /* 用于识别RTIP包 */
#define AF_IPX          23              /* Novell 网间协议 */
#define AF_SIP          24              /* Simple 网间协议 */
#define pseudo_AF_PIP   25              /* 用于识别PIP包 */
#define AF_ISDN         26              /* 综合业务数字网(Integrated Services Digital Network) */
#define AF_E164         AF_ISDN         /* CCITT E.164 推荐 */
#define pseudo_AF_KEY   27              /* 内部密钥管理功能 */
#define AF_INET6        28              /* IPv6 */
#define AF_NATM         29              /* 本征ATM访问 */
#define AF_ATM          30              /* ATM */
#define pseudo_AF_HDRCMPLT 31           /* 由BPF使用,就不必在接口输出例程
                                        * 中重写头文件了
                                        */
#define AF_NETGRAPH     32              /* Netgraph 套接字 */
#define AF_SLOW         33              /* 802.3ad 慢速协议 */
#define AF_SCLUSTER     34              /* Sitara 集群协议 */
#define AF_ARP          35
#define AF_BLUETOOTH    36              /* 蓝牙套接字 */
#define AF_MAX          37
  用于指定IP的是 AF_INET。这个符号对应着常量 2。

  在sockaddr中的域 sa_family指定地址族, 从而决定预先只确定下大致字节数的 sa_data的实际大小。

  特别是当地址族 是AF_INET时,我们可以使用 struct sockaddr_in,这可在 netinet/in.h中找到,任何需要 sockaddr的地方都以此作为实际替代。

/*
* 套接字地址,Internet风格
*/
struct sockaddr_in {
   uint8_t     sin_len;
   sa_family_t sin_family;
   in_port_t   sin_port;
   struct  in_addr sin_addr;
   char    sin_zero[8];
};
  我们可这样描绘它的结构:


三个重要的域是: sin_family,结构体的字节1; sin_port,16位值,在字节2和3; sin_addr,一个32位整数,表示 IP地址,存储在字节4-7。

  现在,让我们尝试填满它。让我们假设我们正在写一个 daytime协议的客户端,这个协议只是简单的规定服务器写出一个代表当前日期和时间文本字符串到端口13。 我们需要使用 TCP/IP,所以我们需要指定在地址族域指定 AF_INET。 AF_INET被定义为 2。让我们使用 IP地址192.43.244.18,这指向 美国联邦政府(time.nist.gov)的服务器。


顺便说一下,域sin_addr被声明为类型 struct in_addr,这个类型定义在 netinet/in.h之中:

/*
* Internet 地址 (由历史原因而形成的结构)
*/
struct in_addr {
   in_addr_t s_addr;
};
  而in_addr_t是一个32位整数。

  192.43.244.18 只是为了表示32位整数的方便写法,按每个八位字节列出, 以最高位的字节开始。

  到目前为止,我已经看见了sockaddr。我们的计算机并不将短整数存储为一个16位实体,而是一个2字节序列。同样的,计算机将32位整数存储为4字节序列。

  想象我们这样写程序:

  sa.sin_family      = AF_INET;
   sa.sin_port        = 13;
   sa.sin_addr.s_addr = (((((192 << 8) | 43) << 8) | 244) << 8) | 18;
  结果会是什么样的呢?

  好,那当然是要依赖于其它因素的。在Pentium®或其它x86 为基础的计算机上,它会像这样:



  在另一个不同的系统上,它可能会是:



在一台PDP计算机上,它可能又是另一个样子。 不过上面两种情况是今天最常用的了。

译者注: PDP的字节顺序在英语中称为middle-endian或mixed-endian。例如,原数0x44332211会被PDP存储为0x33441122。 VAX也采用这种字节顺序。



  通常,要书写可移植的代码,程序员假设不存在那些差异。他们回避这种差异(除了他们使用汇编语言写代码的时候)。唉,可你不能在为套接字写代码时那样轻易的回避这种差异。

  为什么?

  因为当与另一台计算机通信时, 你通常不知道对方存储数据时是先存放最高位字节 (MSB)还是最低位字节 (LSB)。

  你可能会有问题,“那么,套接字可以为我把握这种差异吗?”

  它不能。

  这个回答可能先是让你感到惊讶, 请记住通用的套接字接口只明白结构体sockaddr 中的域sa_len和sa_family。 你不必担心那里的字节顺序(当然, 在FreeBSD上sa_family只有一个字节, 但是许多其它的 UNIX® 系统没有 sa_len 并使用2字节给 sa_family,而且数据使用何种顺序都取决于计算机(译者注:此处英文原文的用词为“is native to”))。

  其余的数据,也就只剩下sa_data[14]。 依照地址族,套接字只是将那些数据转发到目的地。

  事实上,我们输入一个端口号, 是为了让其它计算机知道我们需要什么服务。并且,当我们提供服务时, 只有读取了端口号我们才知道其它计算机期望从我们这里获得什么服务。另一方面,套接字只将端口号作为数据转发,完全不去理会(译者注:此处英文原文用词为“interpret”)其中的内容。

  同样的,我们输入IP地址,告诉途经的每台计算机要将我们的数据发送到哪里。 套接字依然只将其按数据转发。

  那就是为什么我们(指程序员,而不是套接字)不得不把使用在我们的计算机上的字节顺序和发送给其它计算机时使用的传统字节顺序区分开来。

  我们将把我们的计算机上使用的字节顺序称为 主机字节顺序, 或者就是主机顺序.

  有一个在IP发送多字节数据的传统: 最高位字节(MSB)优先。 这,我们将用网络字节顺序提及, 或者简单的称为网络顺序。

  现在,如果我们在Intel计算机上编译上面的代码, 我们的主机字节顺序将产生:



  但是网络字节顺序 要求我们先存储数据的最高位字节(MSB):



不幸的是,我们的主机顺序 恰恰与网络顺序相反。

  我们有几种方法解决这个问题。一种是在我们的代码中 倒置数值:

  sa.sin_family      = AF_INET;
   sa.sin_port        = 13 << 8;
   sa.sin_addr.s_addr = (((((18 << 8) | 244) << 8) | 43) << 8) | 192;
  这将欺骗我们的编译器把数据按网络字节顺序存储。在一些情形中,这的确是个有效的办法 (例如,用汇编语言编程)。然而,在多数情形中,这会导致一个问题。

  想象一下,你用C语言写了一个套接字程序。 你知道它将运行在一台Pentium计算机上, 于是你倒着输入你的所有常量,并且把它们强置为 网络字节顺序。 它工作正常。

  然而,有一台,你所信任的旧 Pentium 变成一台生了锈的旧 Pentium。你把它更换为一个 主机顺序与 网络顺序相同的系统。 你需要重新编译你的所有软件。你的所有软件中除了你写的那个程序,都继续工作正常。

  你早已经忘记你将全部常量强置为与 主机顺序相反。你花费宝贵时间拽头发,呼唤你曾经听到过的(有些是你编造的)所有上帝的名字, 用击球棍敲打你的显示器,还上演所有其它的传统仪式 试图找到一个原本好端端的程序突然完成不能工作的原因。

  最终,你找到了原因,发了一通誓言, 开始重写你的代码。

  幸运的是,你不是第一个面对这个问题的人。 其它人已经创建 htons(3) 和 htonl(3) C 语言函数分别将 short and long 从主机字节顺序转换为 网络字节顺序, 并且还有 ntohs(3) 和 ntohl(3) C 语言函数进行着另外的转换。

  在最高位字节(MSB)-最前 的系统上,这些函数什么都不做。在 最低位字节(LSB)-最前的系统上它们将值转换为正确的顺序。

  这样一来,无论你的软件在什么系统上编译, 如果你使用这些函数,你的数据最终都将是正确的顺序。
1.2 客户端函数
  典型情况中,客户端初始化到服务器的连接。 客户端知道要呼叫哪台服务器:它知道服务器的IP地址,并且知道服务器驻守的 端口。这就好比你拿起电话拨号码 (地址),然后,有人应答, 呼叫负责狂欢的人 (端口)。

 1.2.1 connect
  一旦一个客户端已经建立了一个套接字,就需要把它连接到一个远方系统的一个端口上。这使用 connect(2):

int connect(int s, const struct sockaddr *name, socklen_t namelen);
  参数 s 是套接字, 那是由函数socket返回的值。 name 是一个指向 sockaddr的指针,这个结构体我们已经展开讨论过了。 最后,namelen通知系统 在我们的sockaddr结构体中有多少字节。

  如果 connect 成功, 返回 0。否则返回 -1 并将错误码存放于 errno之中。

  有许多种connect可能失败的原因。例如,试图发起一个Internet连接时, IP 地址可能不存在,或可能停机, 或者就是太忙,或者可能没有在指定端口上有服务器监听。或者直接拒绝任何特定代码的请求。

1.2.2 我们的第一个客户端
  现在我们知道足够多去写一个非常简单的客户端, 一个从192.43.244.18获取当前时间并打印到 stdout的程序。

/*
* daytime.c
*
* G. Adam Stanislav 编程
*/
#include
#include
#include
#include

int main() {
 register int s;
 register int bytes;
 struct sockaddr_in sa;
 char buffer[BUFSIZ+1];

 if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
   perror("socket");
   return 1;
 }

 bzero(&sa, sizeof sa);

 sa.sin_family = AF_INET;
 sa.sin_port = htons(13);
 sa.sin_addr.s_addr = htonl((((((192 << 8) | 43) << 8) | 244) << 8) | 18);
 if (connect(s, (struct sockaddr *)&sa, sizeof sa) < 0) {
   perror("connect");
   close(s);
   return 2;
 }

 while ((bytes = read(s, buffer, BUFSIZ)) > 0)
   write(1, buffer, bytes);

 close(s);
 return 0;
}
  继续,把它输入到你的编辑器中,保存为 daytime.c,然后编译并运行:

% cc -O3 -o daytime daytime.c
% ./daytime

52079 01-06-19 02:29:25 50 0 1 543.9 UTC(NIST) *
%
  在这一情形中,日期是2001年6月19日,时间是 02:29:25 UTC。你的结果会很自然的变化。

1.3 服务器函数
  典型的服务器不初始化连接。 相反,服务器等待客户端呼叫并请求服务。服务器不知道客户端什么时候会呼叫, 也不知道有多少客户端会呼叫。服务器就是这样静坐在那儿,耐心等待,一会儿,又一会儿, 它突然发觉自身被从许多客户端来的请求围困,所有的呼叫都同时来到。

  套接字接口提供三个基本的函数处理这种情况。

1.3.1 bind
  端口像是电话线分机:在你拨一个号码后, 你拨分机到一个特定的人或部门。

  有65535个 IP 端口,但是一台服务器通常只处理从其中一个端口进入的请求。这就像告诉电话室操作员我们处于工作状态并在一个特定分机应答电话。 我们使用 bind(2) 告诉套接字我们要服务的端口。

int bind(int s, const struct sockaddr *addr, socklen_t addrlen);
  除了在 addr 中指定端口, 服务器还可以包含其自身的 IP 地址。不过,也可以就使用符号常量 INADDR_ANY,指示服务于无论哪个 IP上的指定端口上的请求。 这个符号和几个相同的常量,声明在 netinet/in.h之中。

#define    INADDR_ANY      (u_int32_t)0x00000000
  想象我们正在为 daytime协议在 TCP/IP的基础上写一个服务器。 回想起使用端口13。我们的sockaddr_in 结构应当像这样:



1.3.2 listen
  继续我们的办公室电话类比, 在你告诉电话中心操作员你会在哪个分机后,现在你走进你的办公室,确认你自己的电话已插上并且振铃已被打开。还有,你确认呼叫等待功能开启,这样即使你正在与其它人通话, 也可听见电话振铃。

  服务器执守所有经过函数 listen(2) 操作的套接字。

int listen(int s, int backlog);
  在这里,变量backlog 告诉套接字在忙于处理上一个请求时还可以接受多少个进入的请求。换句话说,这决定了挂起连接的队列的最大大小。

7.5.1.3.3 accept
  在你听见电话铃响后,你应答呼叫接起电话。 现在你已经建立起一个与你的客户的连接。这个连接保持到你或你的客户挂线。

  服务器通过使用函数 accept(2) 接受连接。

int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
  注意,这次 addrlen 是一个指针。这是必要的,因为在此情形中套接字要 填上 addr,这是一个 sockaddr_in 结构体。

  返回值是一个整数。其实, accept 返回一个 新套接字。你将使用这个新套接字与客户通信。

  老套接字会发生什么呢?它继续监听更多的请求 (想起我们传给listen的变量 backlog了吗?),直到我们 close(关闭) 它。

  现在,新套接字仅对通信有意义,是完全接通的。 我们不能再把它传给 listen接受更多的连接。

1.3.4 我们的第一个服务器
  我们的第一个服务器会比我们的第一个客户端复杂一些:我们不仅用到了更多的套接字函数, 还需要把程序写成一个守护程序。

  这最好写成:在绑定端口后建立一个子进程。 主进程随后退出,将控制权交回给 shell (或者任何调用主进程的程序)。

  子进程调用 listen,然后启动一个无休止循环。这个循环接受连接,提供服务, 最后关闭连接的套接字。

/*
* daytimed - 端口 13 的服务器
*
* G. Adam Stanislav 编程
* 2001年6月19日
*/
#include
#include
#include
#include
#include
#include

#define BACKLOG 4

int main() {
   register int s, c;
   int b;
   struct sockaddr_in sa;
   time_t t;
   struct tm *tm;
   FILE *client;

   if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
       perror("socket");
       return 1;
   }

   bzero(&sa, sizeof sa);

   sa.sin_family = AF_INET;
   sa.sin_port   = htons(13);

   if (INADDR_ANY)
       sa.sin_addr.s_addr = htonl(INADDR_ANY);

   if (bind(s, (struct sockaddr *)&sa, sizeof sa) < 0) {
       perror("bind");
       return 2;
   }

   switch (fork()) {
       case -1:
           perror("fork");
           return 3;
           break;
       default:
           close(s);
           return 0;
           break;
       case 0:
           break;
   }

   listen(s, BACKLOG);

   for (;;) {
       b = sizeof sa;

       if ((c = accept(s, (struct sockaddr *)&sa, &b)) < 0) {
           perror("daytimed accept");
           return 4;
       }

       if ((client = fdopen(c, "w")) == NULL) {
           perror("daytimed fdopen");
           return 5;
       }

       if ((t = time(NULL)) < 0) {
           perror("daytimed time");

           return 6;
       }

       tm = gmtime(&t);
       fprintf(client, "%.4i-%.2i-%.2iT%.2i:%.2i:%.2iZ\n",
           tm->tm_year + 1900,
           tm->tm_mon + 1,
           tm->tm_mday,
           tm->tm_hour,
           tm->tm_min,
           tm->tm_sec);

       fclose(client);
   }
}
  我们开始于建立一个套接字。然后我们填好 sockaddr_in 类型的结构体 sa。注意, INADDR_ANY的特定使用方法:

   if (INADDR_ANY)
       sa.sin_addr.s_addr = htonl(INADDR_ANY);
  这个常量的值是0。由于我们已经使用 bzero于整个结构体, 再把成员设为0将是冗余。 但是如果我们把代码移植到其它一些 INADDR_ANY可能不是0的系统上, 我们就需要把实际值指定给 sa.sin_addr.s_addr。多数现在C语言 编译器已足够智能,会注意到 INADDR_ANY是一个常量。由于它是0,他们将会优化那段代码外的整个条件语句。

  在我们成功调用bind后, 我们已经准备好成为一个 守护进程:我们使用 fork建立一个子进程。 同在父进程和子进程里,变量s都是套接字。 父进程不再需要它,于是调用了close, 然后返回0通知父进程的父进程成功终止。

  此时,子进程继续在后台工作。 它调用listen并设置 backlog 为 4。这里并不需要设置一个很大的值, 因为 daytime 不是个总有许多客户请求的协议,并且总可以立即处理每个请求。

  最后,守护进程开始无休止循环,按照如下步骤:

调用accept。 在这里等待直到一个客户端与之联系。在这里,接收一个新套接字,c, 用来与其特定的客户通信。

使用 C 语言函数 fdopen 把套接字从一个 低级 文件描述符 转变成一个 C语言风格的 FILE 指针。 这使得后面可以使用 fprintf。

检查时间,按 ISO 8601格式打印到 “文件” client。 然后使用 fclose 关闭文件。这会把套接字一同自动关闭。

  我们可把这些步骤 概括 起来,作为模型用于许多其它服务器:



  这个流程图很好的描述了顺序服务器, 那是在某一时刻只能服务一个客户的服务器,就像我们的daytime服务器能做的那样。这只能存在于客户端与服务器没有真正的“对话”的时候:服务器一检测到一个与客户的连接,就送出一些数据并关闭连接。整个操作只花费若干纳秒就完成了。

  这张流程图的好处是,除了在父进程 fork之后和父进程退出前的短暂时间内, 一直只有一个进程活跃:我们的服务器不占用许多内存和其它系统资源。

  注意我们已经将初始化守护进程 加入到我们的流程图中。我们不需要初始化我们自己的守护进程 (译者注:这里仅指上面的示例程序。一般写程序时都是需要的。), 但这是在程序流程中设置signal 处理程序、 打开我们可能需要的文件等操作的好地方。

  几乎流程图中的所有部分都可以用于描述许多不同的服务器。 条目 serve 是个例外,我们考虑为一个 “黑盒子”,那是你要为你自己的服务器专门设计的东西, 并且 “接到其余部分上”。

  并非所有协议都那么简单。许多协议收到一个来自客户的请求,回复请求,然后接收下一个来自同一客户的请求。 因此,那些协议不知道将要服务客户多长时间。这些服务器通常为每个客户启动一个新进程 当新进程服务它的客户时,守护进程可以继续监听更多的连接。

  现在,继续,保存上面的源代码为 daytimed.c (用字母d 结束守护程序名是个风俗)。在你编译好后,尝试运行:

% ./daytimed
bind: Permission denied
%
  这里发生了什么?正如你将回想起的, daytime协议使用端口13。 但是所有1024以下的端口保留给超级用户 (否则,任何人都可以启动一个守护进程伪装一个常用端口的服务, 这就导致了一个安全漏洞)。

  再试一次,这次以超级用户的身份:

# ./daytimed
#
  怎么……什么都没有?让我们再试一次:

# ./daytimed

bind: Address already in use
#
  在一个时刻,每个端口只能被一个程序绑定。我们的第一个尝试真的成功了:启动了守护子进程并安静的返回。守护子进程仍然在运行,并且继续运行到你关闭它,或是它使用的系统调用失败,或是你重启计算机时。

  好,我们知道它正在后台运行着。 但是它正在正常工作吗?我们如何知道它是个正常的 daytime 服务器?只需简单的:

% telnet localhost 13

Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
2001-06-19T21:04:42Z
Connection closed by foreign host.
%
  telnet 尝试新协议 IPv6,失败了。又重新尝试 IPv4,而后成功了。守护进程工作正常。

  如果你可以通过telnet 访问另一个 UNIX 系统,你可以用测试远程访问服务器。 我们计算机没有静态 IP 地址, 所以我这样做:

% who

whizkid          ttyp0   Jun 19 16:59   (216.127.220.143)
xxx              ttyp1   Jun 19 16:06   (xx.xx.xx.xx)
% telnet 216.127.220.143 13

Trying 216.127.220.143...
Connected to r47.bfm.org.
Escape character is '^]'.
2001-06-19T21:31:11Z
Connection closed by foreign host.
%
  又工作正常了。使用域名还会工作正常吗?

% telnet r47.bfm.org 13

Trying 216.127.220.143...
Connected to r47.bfm.org.
Escape character is '^]'.
2001-06-19T21:31:40Z
Connection closed by foreign host.
%
  顺序说一句,telnet 在我们的守护进程关闭套接字之后打印消息 Connection closed by foreign host (连接被外部主机关闭)。这告诉我们,实际上,在我们的代码中使用 fclose(client); 的工作情况就像前面说的一样。


分页: 255/272 第一页 上页 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 下页 最后页 [ 显示模式: 摘要 | 列表 ]