分手后不能做朋友
分手后不能做朋友。我选择做陌生人的理由是:既然分手了,就不要再有暧昧的关系,那样只会让自己伤心,难受,进也不是,退也不是,既然分手了,就彻底的退出,快乐的生活,开心是现在唯一的追求。我只能对她说句对不起,不管是什么原因,分手了就分手了,既然我们不能再相互爱下去,就让我们做陌生人吧
分手后不能做朋友.如果你曾经深深的爱过这个人,她曾经是你生命中不可分割的一部分,那么怎么去转换角色,才能若无其事的把她看成是一个朋友?这对大多数人来说,恐怕都是难以做到的。
分手后不能做朋友。如果说分手后,还会保持联系,这只能说明有一方还没真正放下对方,她还不愿从对方的生活中彻底消失,所以会找各种看似很冠冕堂皇的理由去接近对方。可是,扪心自问,如果你们是真正的爱过一场的话,如果他真的对不起你,真的能做成朋友?NO,因为现实是残酷的,并没有我们想象的那么多姿多彩。
分手后不能做朋友。假设双方分手之后都另结新欢,你却仍以旧爱做朋友,新情人心理怎么想。谁不认为你与旧爱是藕断丝连呢?新欢必然心中不满,更怕在拉拉扯扯中搞出一个对角关系来。虽然也有一些相安无事的例子,但这些故事总不免另人听出一丝尴尬来。当你看到旧情人,你又怎能忘记曾与他同甘共苦一起走过的日子?除非未曾刻骨铭心。
分手后不能做朋友。看着自己以前的恋人,与别人亲亲我我、欢天喜地,你心里真的就那么平衡吗?人都是有尊严的,何必自找苦吃呢?还是早点脱离苦海,把它封在心里,怀念,或者扔掉它,重新过自己的生活这才是明智之举!
分手后不能做朋友。分手了,不做朋友是为了不再给对方希望,不给任何机会,这个人没爱过你或者已经不再爱你,再抱着希望简直是一种酷刑,放手,反而是大家解脱的最好办法,而做为任何一方,既然没有了爱,也就没有必要苦恋不舍了。所以,既然没有缘分一路同行的走下去,那就彻底分开吧,默默的祝福你,但是我的生活中,已经没有你。
分手后不能做朋友。分手之后仍要做一对朋友,岂不是把过去的一切又带回自己的生活中?何苦呢?歌里就这样唱到:“日复一日,年复一年,淡忘了原初的伤疤,只是一回想起来,还是有一丝隐隐的酸楚在脑海里残留着,久久不能散去。曾经挚爱的人转眼间已成陌路,那份爱也已经随风而逝,说是可以忘记,可生活就象表演,不经意间的相遇,看着她远去的背影往昔一样在脑海间漂动,那种伤是难以语表的。慢慢发现,激情与浪漫已渐渐远去,难以再爱。 ”
是啊,有时候感情就像一场游戏,但也并不是谁都愿意这么做。人这一辈子记得最多的应该是最快乐的那段日子,即使有伤害和痛苦,也不会为此后悔过,人不能后悔,后悔就代表否定自己,否定自己那就不可能快乐。也许你的收获仅仅是一些支离破碎的记忆,并不如你想象的那么美好,有一个美满快乐的结局,但起码自己曾经那么的珍惜和努力过,足矣!
分手后不能做朋友。我选择做陌生人的理由是:既然分手了,就不要再有暧昧的关系,那样只会让自己伤心,难受,进也不是,退也不是,既然分手了,就彻底的退出,快乐的生活,开心是现在唯一的追求。我只能对她说句对不起,不管是什么原因,分手了就分手了,既然我们不能再相互爱下去,就让我们做陌生人吧
分手后不能做朋友.如果你曾经深深的爱过这个人,她曾经是你生命中不可分割的一部分,那么怎么去转换角色,才能若无其事的把她看成是一个朋友?这对大多数人来说,恐怕都是难以做到的。
分手后不能做朋友。如果说分手后,还会保持联系,这只能说明有一方还没真正放下对方,她还不愿从对方的生活中彻底消失,所以会找各种看似很冠冕堂皇的理由去接近对方。可是,扪心自问,如果你们是真正的爱过一场的话,如果他真的对不起你,真的能做成朋友?NO,因为现实是残酷的,并没有我们想象的那么多姿多彩。
分手后不能做朋友。假设双方分手之后都另结新欢,你却仍以旧爱做朋友,新情人心理怎么想。谁不认为你与旧爱是藕断丝连呢?新欢必然心中不满,更怕在拉拉扯扯中搞出一个对角关系来。虽然也有一些相安无事的例子,但这些故事总不免另人听出一丝尴尬来。当你看到旧情人,你又怎能忘记曾与他同甘共苦一起走过的日子?除非未曾刻骨铭心。
分手后不能做朋友。看着自己以前的恋人,与别人亲亲我我、欢天喜地,你心里真的就那么平衡吗?人都是有尊严的,何必自找苦吃呢?还是早点脱离苦海,把它封在心里,怀念,或者扔掉它,重新过自己的生活这才是明智之举!
分手后不能做朋友。分手了,不做朋友是为了不再给对方希望,不给任何机会,这个人没爱过你或者已经不再爱你,再抱着希望简直是一种酷刑,放手,反而是大家解脱的最好办法,而做为任何一方,既然没有了爱,也就没有必要苦恋不舍了。所以,既然没有缘分一路同行的走下去,那就彻底分开吧,默默的祝福你,但是我的生活中,已经没有你。
分手后不能做朋友。分手之后仍要做一对朋友,岂不是把过去的一切又带回自己的生活中?何苦呢?歌里就这样唱到:“日复一日,年复一年,淡忘了原初的伤疤,只是一回想起来,还是有一丝隐隐的酸楚在脑海里残留着,久久不能散去。曾经挚爱的人转眼间已成陌路,那份爱也已经随风而逝,说是可以忘记,可生活就象表演,不经意间的相遇,看着她远去的背影往昔一样在脑海间漂动,那种伤是难以语表的。慢慢发现,激情与浪漫已渐渐远去,难以再爱。 ”
是啊,有时候感情就像一场游戏,但也并不是谁都愿意这么做。人这一辈子记得最多的应该是最快乐的那段日子,即使有伤害和痛苦,也不会为此后悔过,人不能后悔,后悔就代表否定自己,否定自己那就不可能快乐。也许你的收获仅仅是一些支离破碎的记忆,并不如你想象的那么美好,有一个美满快乐的结局,但起码自己曾经那么的珍惜和努力过,足矣!
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
执行范例:
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]#
还有其它很多命令,我就不一一介绍拉。
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);
}
输出结果:略
分析:(看我写的注释。你可按我注释的①②③④⑤顺序自行分析。)
要理解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 的工具软件,可以帮助我们更容易的去开发程序,而且更容易维护源代码。
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 的工具软件,可以帮助我们更容易的去开发程序,而且更容易维护源代码。
1. 男人是社会的主体,不管你信或不信。所以男人应该有种责任感。
2. 25岁之前,请记得,爱情通常是假的,或者不是你所想象的那样纯洁和永远。如果你过了25岁,那么你也许会懂得这个道理。
3. 吃饭7成饱最舒服。对待女友最多也请你保持在7成。
4. 30岁之前请爱惜自己的身体,前30年你找病,后30年病找你。如果你过了30岁,你自然也会懂得这个道理。
5. 事业远比爱情重要。如果说事业都不能永恒,那么爱情只能算是昙花一现。
6. 不要轻易接受追求你的女孩。女追男隔层纱。如果你很容易就陷进去,你会发现你会错过很多东西,失去很多东西。
7. 请你相信,能用钱解决的问题,都不是问题。如果你认为钱索王道,有钱有女人,没钱没女人,那么。女人不是问题。
8 . 请永远积极向上。每个男人都有他可爱的地方,但是不可爱的地方只有不积极面对生活。
9. 不要连续2次让同一个女人伤害。好马不吃回头草,是有他道理的。如果认真考虑过该分手,那么请不要做任何舍不得的行动。
10. 如果你和你前女友能做朋友,那么你要问自己:为什么?如果分手后还是朋友,那么只有2个可能:。你们当初都只是玩玩而已,没付出彼此最真的感情。或者:必定有个人是在默默的付出无怨无悔!
11. 永远不要太相信女人在恋爱时的甜言蜜语。都说女人爱听甜言蜜语,其实,男人更喜欢。
12. 请不要为自己的相貌或者身高过分担心和自卑。人是动物,但是区别于动物。先天条件并不是阻挡你好好生活的借口。人的心灵远胜于相貌,请相信这点。如果有人以相貌取人,那么你也没必要太在意。因为他从某种意义来讲,只是只动物。你会跟动物怄气吗?
13. 失恋时,只有2种可能,要么你爱她她不爱你,或者相反。那么,当你爱的人不再爱你,或者从来没爱过你时。你没有遗憾,因为你失去的只是一个不爱你的人。
14. 请不要欺骗善良的女孩。这个世界上,善良的女孩太少。
15. 不能偏激的认为金钱万能,至少,金钱治不好艾滋病。
16. 请一定要有自信。你就是一道风景,没必要在别人风景里面仰视。
17. 受到再大的打击,只要生命还在,请相信每天的太阳都是新的。
18. 爱情永远不可能是天平。你想在爱情里幸福就要舍得伤心。
19. 如果你喜欢一个认为别人应该对她好的mm,请尽早放弃。直接血缘关系的亲人以外,没有一个人理所当然要对另一个人好。如果她不明白这个道理,也就是她根本不懂得珍惜。
20. 不要因为寂寞而找gf,寂寞男人请要学会品味寂寞。请记住:即使寂寞,远方黑暗的夜空下,一定有人和你一样,寂寞的人不同,仰望的星空却是唯一。
也许有些人的真心将长时间的寂寞,也许有些不寂寞的人最终要走向寂寞。不管你相不相信这句话,或愿不愿意成为有些人中的一员。
21. 任何事没有永远。也别问怎样才能永远。生活有很多无奈。请尽量充实自己,充实生活。请善待生活。
2. 25岁之前,请记得,爱情通常是假的,或者不是你所想象的那样纯洁和永远。如果你过了25岁,那么你也许会懂得这个道理。
3. 吃饭7成饱最舒服。对待女友最多也请你保持在7成。
4. 30岁之前请爱惜自己的身体,前30年你找病,后30年病找你。如果你过了30岁,你自然也会懂得这个道理。
5. 事业远比爱情重要。如果说事业都不能永恒,那么爱情只能算是昙花一现。
6. 不要轻易接受追求你的女孩。女追男隔层纱。如果你很容易就陷进去,你会发现你会错过很多东西,失去很多东西。
7. 请你相信,能用钱解决的问题,都不是问题。如果你认为钱索王道,有钱有女人,没钱没女人,那么。女人不是问题。
8 . 请永远积极向上。每个男人都有他可爱的地方,但是不可爱的地方只有不积极面对生活。
9. 不要连续2次让同一个女人伤害。好马不吃回头草,是有他道理的。如果认真考虑过该分手,那么请不要做任何舍不得的行动。
10. 如果你和你前女友能做朋友,那么你要问自己:为什么?如果分手后还是朋友,那么只有2个可能:。你们当初都只是玩玩而已,没付出彼此最真的感情。或者:必定有个人是在默默的付出无怨无悔!
11. 永远不要太相信女人在恋爱时的甜言蜜语。都说女人爱听甜言蜜语,其实,男人更喜欢。
12. 请不要为自己的相貌或者身高过分担心和自卑。人是动物,但是区别于动物。先天条件并不是阻挡你好好生活的借口。人的心灵远胜于相貌,请相信这点。如果有人以相貌取人,那么你也没必要太在意。因为他从某种意义来讲,只是只动物。你会跟动物怄气吗?
13. 失恋时,只有2种可能,要么你爱她她不爱你,或者相反。那么,当你爱的人不再爱你,或者从来没爱过你时。你没有遗憾,因为你失去的只是一个不爱你的人。
14. 请不要欺骗善良的女孩。这个世界上,善良的女孩太少。
15. 不能偏激的认为金钱万能,至少,金钱治不好艾滋病。
16. 请一定要有自信。你就是一道风景,没必要在别人风景里面仰视。
17. 受到再大的打击,只要生命还在,请相信每天的太阳都是新的。
18. 爱情永远不可能是天平。你想在爱情里幸福就要舍得伤心。
19. 如果你喜欢一个认为别人应该对她好的mm,请尽早放弃。直接血缘关系的亲人以外,没有一个人理所当然要对另一个人好。如果她不明白这个道理,也就是她根本不懂得珍惜。
20. 不要因为寂寞而找gf,寂寞男人请要学会品味寂寞。请记住:即使寂寞,远方黑暗的夜空下,一定有人和你一样,寂寞的人不同,仰望的星空却是唯一。
也许有些人的真心将长时间的寂寞,也许有些不寂寞的人最终要走向寂寞。不管你相不相信这句话,或愿不愿意成为有些人中的一员。
21. 任何事没有永远。也别问怎样才能永远。生活有很多无奈。请尽量充实自己,充实生活。请善待生活。
匹配正则: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
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
如果是降序,则反之
这个折半查找法的思想 和 微积分中间的中值定理的思维有点像
来个 原理:
以升序为例
1:第一各中间值是 全部元素的个数/2(或者(元素的序数+1)/2 )
2:判断你所要的值和这个中间值的大小
如果大,那么就是 (第一次中间值序数+1 + 末尾元素序数)/2
如果小,那么就是 (第一次中间值序数-1 + 首元素序数(通常是0))/2
这样逐步缩小范围
3:而后如果出现
比中间值小(这一轮的中间值),但是比上一步中间值大(上一轮的中间值)
那么, 新的中间值序数=((上轮中间值序数)+(这轮中间值序数))/2
如果是降序,则反之
这个折半查找法的思想 和 微积分中间的中值定理的思维有点像
指针是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不是指针)。
本书中规中矩地讲解了指针的概念、定义与初始化、操作等。正如上面提到的“饱汉不知饿汉饥”,我似乎很健忘,以至于不记得指针的难点在哪儿了。
指针的灵活性可以把大量的工作化繁为易,前提是必须首很把足够繁的指针弄懂。听起来有点像绕口令,事实就是这样,你现在把难懂的东西弄懂了,日后可以把难事化简,大事化小。
从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” 。。
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” 。。
[站长原创]百度笔试的一小题,SHELL求解
Php/Js/Shell/Go jackxiang 2007-11-8 15:49
以本人性格,就是喜欢抄别人的,这次来点自己的吧,其实也是大家的,一哥们去百度参加面试的题目。。。呵呵,见笑!
如百度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
[/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
如百度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
刚在在《经理世界网》看到篇文章,觉得很有启发性。作为一个毕业的大学生,觉得这篇文章的经验之谈,你们很有必要看一下,当然最重要的是采取行动。
我也是先实习后毕业,未毕业之前就觉得现在的就业竞争激烈,所以还是提前找公司实习,一方面补充自己的专业知识,一方面慢慢熟悉职场文化,对以后进入公司快速融入公司很有帮助,如果像文中所说在大一大二就开始实习,那么对你的学业和以后的工作的帮助将更大,虽然这会给你带来一些压力,但我个人觉得是自己的,如果你还在大学中,你不妨也看看这篇文章,希望对你们有所启发。
闲话少叙,请看正文。
快实习!
----石丹 转自 http://www.cio.com.cn/index.asp?node1=90&node2=82&node3=233
“大学生的毕业工资只有1500元!这样聪明的孩子,为什么会遇到这样的处境?我想,一定是因为他们没有找对门路。”群邑集团(GroupM)中国区人力资源总监李文海对我说。他被校园宣讲过程中看到的聪明、主见、积极的学生所打动。
这是我们采访了近50家企业人力资源相关负责人之后最震撼人心的发现。我们请这些每年都和大学生直接接触的人力资源负责人说出年轻学生的最大弊病,有80%的人力资源部门发言人却回答:“没有什么大弊病”、“聪明的学生”、“优秀的年轻人”、“我们的未来”,就算是提到一些类似眼高手低、缺乏责任心的问题,也说是因为没有实际经验所致,需要的是教育的改良。
如果说在人才的需求和供应量都很大的现代中国,却有着供需不能对应的困境的话,那么至少其中有一个原因在于教育的缺失,一个原因在于“没有找对门路”。
当越来越多的企业认识到这一点,自发地提供工作教育的机会——实习,以补充教育缺口的时候,我们那些充满梦想、激情与智慧的年轻学生,是不是可以完成另一部份的任务——找对门路?
“快实习”既是一句发起动员令,也是指“快速找对实习门路”,这是《职场》在2008年招聘大潮到来前,能够给予所有大学生的最好工具。你可以用阅读一期特别策划的时间,去了解自己的特点、公司的信息、如何完成对接。这期特别策划在角度和信息上,都力求智慧、客观、全面。
实习的误区
罗莉是北京外企人力资源服务有限公司人力资源部经理。HiAll是目前国内著名的从事与职业生涯、人生规划相关的教育咨询培训机构,CEO曾舒煜和罗莉一起分析了我们在实习过程中最容易进入的四种误区。
误区1 有实习就去
实习是一个锻炼,帮助自己从学生思维向社会人思维转变。一般说来,有实习经历总比没有实习经历强,但并不等于有实习机会就要去。如果实习职位不是自己的兴趣所在,那请再等等,与其在一个自己没有兴趣的岗位上混几个月,还不如休息或者看书。有一个办法可以帮助你衡量这个机会是不是自己需要的,那就是T型表。表的两边分别是“得到”和“失去”,尽可能地填上你能想到的理由。反正现在都是在线投简历,我们总是习惯把简历各处投遍,以为只要付出的筹码足够多,老虎机里的硬币总是可以倾洒出来。你错了,无目的的简历投得越多,你被选上的可能性就越低。
误区2 只去大公司
我们总是觉得今天的实习就是今后简历上的一行文字而已,所以碰到一家好公司镀金是主要的,究竟可以学什么是次要的。但实际上,在你实习的时候,你就开始选择了你今后工作的领域和职业,只去大公司,却选择了错误的、没有整体规划的岗位,在你今后的雇主眼里,这样的行为就像笑话。一般的企业不一定就没有实习的价值,特别是一些非大企业目标院校的学生,还是要从对自己能力提高和职业规划方面来考虑实习单位。曾舒煜认为,衡量一个实习机会是不是好,不应该仅仅看企业规模,也应考虑以下几方面:实习所在的公司是否注重实习生的培训,实习是否能够提供相对多的业务学习机会,学习到的东西是否与期望从事行业所需的素质相匹配。
误区3 快毕业的时候再实习
实习要趁早。现在,一些学生要么为考研或出国做准备,直到大四才开始琢磨实习,要么就是到了假期只是休闲。尽管人生应该过得相对从容,但是,时刻还是应该有实习这根弦。要想毕业获得好的工作,大二就应当开始实习。尤其是咨询或者投行领域的同学,都是大二暑假海外交流,大三暑假名企实习,有了这些才具备应聘的资格。
误区4 认为实习时所做的琐碎的工作,没有任何价值
通常企业让实习生做的很多工作都是基础工作。这时候,即便是自己兴趣所在,也难免感觉枯燥无味。但这并不代表不能学到东西。大多数工作都是由无数简单重复劳动组成的。比如,做软件工程师,至少有一半的时间都是在维护别人的代码,在调试和除错。很多年轻人觉得计算机很酷,但他们只想编程创新,不想
做维护、调试和除错等一些基础工作,这些想法都是不切实际的。你必须把这些枯燥的事情当做追寻兴趣必须付出的代价。另外,一些工作虽然自己做不了,但是可以看别人是如何做的,去学习别人的沟通方法和解决事情的方式。
“快实习!”的9个理由
前6条,是从自我的视角看为何应该“快实习”,后3条,是从公司的视角看为何应该“快实习”。
1 养成“三岁看老”职业观
实习的时候,你是一张彻底的白纸,工作是什么样子?应该以什么样的状态投入?什么才算职业理想?要有哪些职业态度以及工作习惯?从你在办公室里无所适从地坐下、玩命地观察时起,你所在的这家公司就开始在你未来的职业生涯中持续地发挥着影响。“快实习”不得不慎重。
2 熟悉行业和职位的概念
“我感觉,学生的理想和现实差距过大是一个比较突出的问题。实践经验缺乏,期望值又过高,有时候会造成眼高手低。这些问题不光在实习中会出现,也存在于学生的整个求职就业过程中。”LG电子(中国)人事管理部部长张晖说,他发现很多学生入学之后,对实习和就业没有概念,未必知道所学专业是做什么的,也不知道适合不适合自己,“而通过实习,他们可以修正对自己未来社会角色的理解偏差。”对行业和职位的理解将一直是帮助你成功的砝码,当你还是一个学生的时候,它帮助你独立思考,当你是一个高级经理人,它助你拓宽视野、战略性地制订计划。
3 借助大公司了解自己的潜力和定位
“实习是你们学习和事业中重要的里程碑。”IBM大中华区业务运营总监张台杰在“IBM2007年蓝色之路大学生夏季实习计划”闭幕仪式上说,他已在IBM服务30年,大儿子正在读大学三年级。他给他儿子的建议是:“实习最重要的作用就是让你认识自己,为下一步的决策做一个依据,一个personal的定位。”
4 经验上受限程度最低
很多走出大学的应届生都痛不欲生地发现,公司对求职者要求最多的就是经验:有没有在这个行业中工作的经验?有没有在这个岗位上工作的经验?只剩实习生是没有经验限定的特殊群体。很多公司规定对实习生的考察只关注个人潜力,所以任何其他条件都不会迷惑考察者的双眼,只要你是璞玉,就会被发现。
5 最积极的时光
如果这是你的正式工作,那么让你一天打50个电话,每天录入表格,整理档案文件,你肯定觉得自己无法再工作下去。但是实习生不会这么想。
实习虽然是报酬最少的时候,却也是你人生中难得开足马力证明自己能力的时期,每个人都铆足劲儿想看看往空桶里舀水到底能舀多少。
6 转换领域的机会
在采访中,很多学生告诉我,他不喜欢自己的大学专业,更不想今后从事和这个专业相关的工作,如果你也如此,你更应该去努力把握实习这个机会。因为当你一旦真正进入了这个行业,你面对的转换行业的风险将会越来越大,你会经受种种的考验,比如从头做起,过去的成绩一笔勾销。所以不如在只被关注潜力的时期多进行尝试,提前完成转型。
7 公司校园招聘制度的改革
GE今年不会进行大规模的校园招聘了,而会优先考虑从实习生中挑选应届生源。做出同样考虑的还有IBM和西门子等实习生项目运作得比较系统的公司。公司对校园招聘制度做出这样改革的同时,你还能不“快实习”吗?
8 破除盲目性
为什么越来越多的企业愿意开展实习?“因为实习这几个月,你可以把它看成一个互相去试探的过程,你是不是喜欢这家公司、它是不是合适你,一定要有这个过程。”群邑中国区人力资源总监李文海说,“我觉得直接把一个Offer给学生可能对双方都不好。因为他也要选择,如果他还不了解你,盲目加入,会为今后埋下人员流失的伏笔。”
9 当做未来
实习生计划有往另外一种计划上逐步转型的趋势,那就是管理培训生计划(MT),由于企业发现目前最稀缺的是具有领导力的管理人才,所以希望自主培养。很多项目虽然目前叫实习生计划,但都增加了轮岗机会以及面试中对潜在领导力的考察,很多企业人力资源负责人表示,他们实际上已经在把这些实习生当做公司的未来力量培养。
我也是先实习后毕业,未毕业之前就觉得现在的就业竞争激烈,所以还是提前找公司实习,一方面补充自己的专业知识,一方面慢慢熟悉职场文化,对以后进入公司快速融入公司很有帮助,如果像文中所说在大一大二就开始实习,那么对你的学业和以后的工作的帮助将更大,虽然这会给你带来一些压力,但我个人觉得是自己的,如果你还在大学中,你不妨也看看这篇文章,希望对你们有所启发。
闲话少叙,请看正文。
快实习!
----石丹 转自 http://www.cio.com.cn/index.asp?node1=90&node2=82&node3=233
“大学生的毕业工资只有1500元!这样聪明的孩子,为什么会遇到这样的处境?我想,一定是因为他们没有找对门路。”群邑集团(GroupM)中国区人力资源总监李文海对我说。他被校园宣讲过程中看到的聪明、主见、积极的学生所打动。
这是我们采访了近50家企业人力资源相关负责人之后最震撼人心的发现。我们请这些每年都和大学生直接接触的人力资源负责人说出年轻学生的最大弊病,有80%的人力资源部门发言人却回答:“没有什么大弊病”、“聪明的学生”、“优秀的年轻人”、“我们的未来”,就算是提到一些类似眼高手低、缺乏责任心的问题,也说是因为没有实际经验所致,需要的是教育的改良。
如果说在人才的需求和供应量都很大的现代中国,却有着供需不能对应的困境的话,那么至少其中有一个原因在于教育的缺失,一个原因在于“没有找对门路”。
当越来越多的企业认识到这一点,自发地提供工作教育的机会——实习,以补充教育缺口的时候,我们那些充满梦想、激情与智慧的年轻学生,是不是可以完成另一部份的任务——找对门路?
“快实习”既是一句发起动员令,也是指“快速找对实习门路”,这是《职场》在2008年招聘大潮到来前,能够给予所有大学生的最好工具。你可以用阅读一期特别策划的时间,去了解自己的特点、公司的信息、如何完成对接。这期特别策划在角度和信息上,都力求智慧、客观、全面。
实习的误区
罗莉是北京外企人力资源服务有限公司人力资源部经理。HiAll是目前国内著名的从事与职业生涯、人生规划相关的教育咨询培训机构,CEO曾舒煜和罗莉一起分析了我们在实习过程中最容易进入的四种误区。
误区1 有实习就去
实习是一个锻炼,帮助自己从学生思维向社会人思维转变。一般说来,有实习经历总比没有实习经历强,但并不等于有实习机会就要去。如果实习职位不是自己的兴趣所在,那请再等等,与其在一个自己没有兴趣的岗位上混几个月,还不如休息或者看书。有一个办法可以帮助你衡量这个机会是不是自己需要的,那就是T型表。表的两边分别是“得到”和“失去”,尽可能地填上你能想到的理由。反正现在都是在线投简历,我们总是习惯把简历各处投遍,以为只要付出的筹码足够多,老虎机里的硬币总是可以倾洒出来。你错了,无目的的简历投得越多,你被选上的可能性就越低。
误区2 只去大公司
我们总是觉得今天的实习就是今后简历上的一行文字而已,所以碰到一家好公司镀金是主要的,究竟可以学什么是次要的。但实际上,在你实习的时候,你就开始选择了你今后工作的领域和职业,只去大公司,却选择了错误的、没有整体规划的岗位,在你今后的雇主眼里,这样的行为就像笑话。一般的企业不一定就没有实习的价值,特别是一些非大企业目标院校的学生,还是要从对自己能力提高和职业规划方面来考虑实习单位。曾舒煜认为,衡量一个实习机会是不是好,不应该仅仅看企业规模,也应考虑以下几方面:实习所在的公司是否注重实习生的培训,实习是否能够提供相对多的业务学习机会,学习到的东西是否与期望从事行业所需的素质相匹配。
误区3 快毕业的时候再实习
实习要趁早。现在,一些学生要么为考研或出国做准备,直到大四才开始琢磨实习,要么就是到了假期只是休闲。尽管人生应该过得相对从容,但是,时刻还是应该有实习这根弦。要想毕业获得好的工作,大二就应当开始实习。尤其是咨询或者投行领域的同学,都是大二暑假海外交流,大三暑假名企实习,有了这些才具备应聘的资格。
误区4 认为实习时所做的琐碎的工作,没有任何价值
通常企业让实习生做的很多工作都是基础工作。这时候,即便是自己兴趣所在,也难免感觉枯燥无味。但这并不代表不能学到东西。大多数工作都是由无数简单重复劳动组成的。比如,做软件工程师,至少有一半的时间都是在维护别人的代码,在调试和除错。很多年轻人觉得计算机很酷,但他们只想编程创新,不想
做维护、调试和除错等一些基础工作,这些想法都是不切实际的。你必须把这些枯燥的事情当做追寻兴趣必须付出的代价。另外,一些工作虽然自己做不了,但是可以看别人是如何做的,去学习别人的沟通方法和解决事情的方式。
“快实习!”的9个理由
前6条,是从自我的视角看为何应该“快实习”,后3条,是从公司的视角看为何应该“快实习”。
1 养成“三岁看老”职业观
实习的时候,你是一张彻底的白纸,工作是什么样子?应该以什么样的状态投入?什么才算职业理想?要有哪些职业态度以及工作习惯?从你在办公室里无所适从地坐下、玩命地观察时起,你所在的这家公司就开始在你未来的职业生涯中持续地发挥着影响。“快实习”不得不慎重。
2 熟悉行业和职位的概念
“我感觉,学生的理想和现实差距过大是一个比较突出的问题。实践经验缺乏,期望值又过高,有时候会造成眼高手低。这些问题不光在实习中会出现,也存在于学生的整个求职就业过程中。”LG电子(中国)人事管理部部长张晖说,他发现很多学生入学之后,对实习和就业没有概念,未必知道所学专业是做什么的,也不知道适合不适合自己,“而通过实习,他们可以修正对自己未来社会角色的理解偏差。”对行业和职位的理解将一直是帮助你成功的砝码,当你还是一个学生的时候,它帮助你独立思考,当你是一个高级经理人,它助你拓宽视野、战略性地制订计划。
3 借助大公司了解自己的潜力和定位
“实习是你们学习和事业中重要的里程碑。”IBM大中华区业务运营总监张台杰在“IBM2007年蓝色之路大学生夏季实习计划”闭幕仪式上说,他已在IBM服务30年,大儿子正在读大学三年级。他给他儿子的建议是:“实习最重要的作用就是让你认识自己,为下一步的决策做一个依据,一个personal的定位。”
4 经验上受限程度最低
很多走出大学的应届生都痛不欲生地发现,公司对求职者要求最多的就是经验:有没有在这个行业中工作的经验?有没有在这个岗位上工作的经验?只剩实习生是没有经验限定的特殊群体。很多公司规定对实习生的考察只关注个人潜力,所以任何其他条件都不会迷惑考察者的双眼,只要你是璞玉,就会被发现。
5 最积极的时光
如果这是你的正式工作,那么让你一天打50个电话,每天录入表格,整理档案文件,你肯定觉得自己无法再工作下去。但是实习生不会这么想。
实习虽然是报酬最少的时候,却也是你人生中难得开足马力证明自己能力的时期,每个人都铆足劲儿想看看往空桶里舀水到底能舀多少。
6 转换领域的机会
在采访中,很多学生告诉我,他不喜欢自己的大学专业,更不想今后从事和这个专业相关的工作,如果你也如此,你更应该去努力把握实习这个机会。因为当你一旦真正进入了这个行业,你面对的转换行业的风险将会越来越大,你会经受种种的考验,比如从头做起,过去的成绩一笔勾销。所以不如在只被关注潜力的时期多进行尝试,提前完成转型。
7 公司校园招聘制度的改革
GE今年不会进行大规模的校园招聘了,而会优先考虑从实习生中挑选应届生源。做出同样考虑的还有IBM和西门子等实习生项目运作得比较系统的公司。公司对校园招聘制度做出这样改革的同时,你还能不“快实习”吗?
8 破除盲目性
为什么越来越多的企业愿意开展实习?“因为实习这几个月,你可以把它看成一个互相去试探的过程,你是不是喜欢这家公司、它是不是合适你,一定要有这个过程。”群邑中国区人力资源总监李文海说,“我觉得直接把一个Offer给学生可能对双方都不好。因为他也要选择,如果他还不了解你,盲目加入,会为今后埋下人员流失的伏笔。”
9 当做未来
实习生计划有往另外一种计划上逐步转型的趋势,那就是管理培训生计划(MT),由于企业发现目前最稀缺的是具有领导力的管理人才,所以希望自主培养。很多项目虽然目前叫实习生计划,但都增加了轮岗机会以及面试中对潜在领导力的考察,很多企业人力资源负责人表示,他们实际上已经在把这些实习生当做公司的未来力量培养。
电脑是我们工作不可或缺的助手,也是我们的健康克星。大家都知道面对电脑时间长了不好,那该怎么办? 其实每天四杯茶,还有注意食物营养的均衡摄取,不但可以对抗辐射的侵害,还可以保护眼睛,保证我们的身体健康。
1、上午一杯绿茶:绿茶中含强效的抗氧化剂以及维生素C,不但可以清除体内的自由基,还能分泌出对抗紧张压力的激素。绿茶中所含的少量咖啡因可以刺激中枢神经,振奋精神。不过最好在白天饮用,以免影响睡眠。
2、下午一杯菊花茶:菊花有明目清肝的作用,有些人就干脆用菊花加上枸杞一起泡来喝,或是在菊花茶中加入蜂蜜,都对解郁有帮助。
3、疲劳了一杯枸杞茶:枸杞子含有丰富的β胡萝卜素、维生素B1、维生素C、钙、铁,具有补肝、益肾、明目的作用。其本身具有甜味,可以泡茶也可以像葡萄干一样作零食,对解决电脑族眼睛涩、疲劳都有功效。
4、晚间一杯决明茶:决明子有清热、明目、补脑髓、镇肝气、益筋骨的作用
还有如果允许的话,建议大家多吃以下几种食物:
香蕉 :经常在电脑前工作的人常会觉得眼睛干涩疼痛,所以,在电脑桌上放几支香蕉很有必要,香蕉中的钾可帮助人体排出多余的盐分,让身体达到钾钠平衡,缓解眼睛的不适症状。此外,香蕉中含有大量的β胡萝卜素,当人体缺乏这种物质时,眼睛就会变得疼痛、干涩、眼珠无光、失水少神,多吃香蕉不仅可减轻这些症状,还可一定程度上缓解眼睛疲劳,避免眼睛过早衰老。
菠菜: 菠菜含大量的铁及维他命B,能有效防治血管方面疾病,并能预防盲眼症。一碗菠菜只有41个卡路里,热量极低,爱美的女士可以安心食用。
坚果 :坚果不仅可以降低胆固醇,还能降低血液中的甘油三酯,是预防心脏病的首选食品。要注意的是,食用时务必要适量,千万不要过度食用。
燕麦:每天食用燕麦可以降低胆固醇,燕麦也可以降低血压,它所含的丰富纤维素会使人很快就有饱腹的感觉,如此一来可以减少摄取其它油腻的食品,达到控制体重的目的。
草莓:在所有蔬果中,草莓拥有极高的抗氧化剂,除了可以预防心脏病和癌症,还能增进脑力。
对我们来说健康才是我们的命根子,特别是程序员,IT从业其实比较容易犯职业病.
就象革命时期说的话,身体是革命的本钱.
而现在,对于我们这些Iters来说,有健康才有将来。
所以大家动起来,养成良好的生活,工作习惯。对自己,对自己的未来才是最重要的保证。
愿大家都健康,未来我们共创!!!
1、上午一杯绿茶:绿茶中含强效的抗氧化剂以及维生素C,不但可以清除体内的自由基,还能分泌出对抗紧张压力的激素。绿茶中所含的少量咖啡因可以刺激中枢神经,振奋精神。不过最好在白天饮用,以免影响睡眠。
2、下午一杯菊花茶:菊花有明目清肝的作用,有些人就干脆用菊花加上枸杞一起泡来喝,或是在菊花茶中加入蜂蜜,都对解郁有帮助。
3、疲劳了一杯枸杞茶:枸杞子含有丰富的β胡萝卜素、维生素B1、维生素C、钙、铁,具有补肝、益肾、明目的作用。其本身具有甜味,可以泡茶也可以像葡萄干一样作零食,对解决电脑族眼睛涩、疲劳都有功效。
4、晚间一杯决明茶:决明子有清热、明目、补脑髓、镇肝气、益筋骨的作用
还有如果允许的话,建议大家多吃以下几种食物:
香蕉 :经常在电脑前工作的人常会觉得眼睛干涩疼痛,所以,在电脑桌上放几支香蕉很有必要,香蕉中的钾可帮助人体排出多余的盐分,让身体达到钾钠平衡,缓解眼睛的不适症状。此外,香蕉中含有大量的β胡萝卜素,当人体缺乏这种物质时,眼睛就会变得疼痛、干涩、眼珠无光、失水少神,多吃香蕉不仅可减轻这些症状,还可一定程度上缓解眼睛疲劳,避免眼睛过早衰老。
菠菜: 菠菜含大量的铁及维他命B,能有效防治血管方面疾病,并能预防盲眼症。一碗菠菜只有41个卡路里,热量极低,爱美的女士可以安心食用。
坚果 :坚果不仅可以降低胆固醇,还能降低血液中的甘油三酯,是预防心脏病的首选食品。要注意的是,食用时务必要适量,千万不要过度食用。
燕麦:每天食用燕麦可以降低胆固醇,燕麦也可以降低血压,它所含的丰富纤维素会使人很快就有饱腹的感觉,如此一来可以减少摄取其它油腻的食品,达到控制体重的目的。
草莓:在所有蔬果中,草莓拥有极高的抗氧化剂,除了可以预防心脏病和癌症,还能增进脑力。
对我们来说健康才是我们的命根子,特别是程序员,IT从业其实比较容易犯职业病.
就象革命时期说的话,身体是革命的本钱.
而现在,对于我们这些Iters来说,有健康才有将来。
所以大家动起来,养成良好的生活,工作习惯。对自己,对自己的未来才是最重要的保证。
愿大家都健康,未来我们共创!!!
我知道主页原创区不准转载其他文章,但是这篇文章真是太棒了,不仅仅是因为我敬佩乔布斯,所以冒着被骂的风险也要拿出来和大家分享,希望dudu原谅。,也希望这篇文章能带给你些收获和启发。
-文/王育琨 来自:《商界评论》杂志
作者简介:王育琨,首钢研究院企业所所长
2007年6月29日,一股强劲的旋风由美国的史蒂夫·乔布斯和iPhone发起,iPhone开始销售一周之内,已经启用了100万部iPhone手机。史蒂夫·乔布斯原来的目标是在2007年年底之前销售100万部iPhone,实际上他只用了6天时间就实现了这个目标。而当初iPod推出的第一年,只卖出了10万台。iPhone吸引了全球媒体和重要消费类电子制造商的眼球。美国谐星Stephen Colbert开玩笑说,iPhone发售是人类历史上仅次于耶稣诞生的第二个重大事件。诺基亚、摩托罗拉、三星、索爱、LG等等都在紧张地关注,iPhone是否会重塑手机和消费类电子行业的未来。有一点可以确信,iPhone已经重新塑造了苹果市值。到7月12日,苹果市值已经达到1140亿美元,超越了甲骨文和戴尔。分析师更做出了乐观预计,苹果市值将在18个月内超越IBM(市值1450亿美元)和英特尔(1610亿美元)。
而中国人则出奇淡定。遇到几个中国消费类电子产品制造商的高管,对此不屑一顾,他们有更重要的事情要关心。中国当仁不让现在是消费类电子产品的生产大国。可惜,中国还没有一家消费类电子的世界级品牌。电子消费品的灵魂是什么?中国消费类电子制造商,把iPhone上市看成了一个简单的营销事件,从而低估了iPhone带来的冲击波,并没有做好应对iPhone冲击的准备。iPhone是关乎消费类电子和手机产业未来的一个“小物件”。中国人需要跟着乔布斯和他的iPhone,来理解消费类电子的未来。
“小物件”,大世界
古代斯巴达的立法者利库尔戈斯,毕其一生让斯巴达人民保证不违背他制定的任何法律。古代爱琴海莱斯沃斯岛上有名的奇特拉琴家弗里尼斯,他给原为七根弦的奇特拉琴增加了两根弦。斯巴达法官根本不问这增加的两根弦是否让音乐更加悦耳动听,高举标准的旗帜,认为这是糟蹋音乐,便粗暴地将琴砍断了。只因为那两根弦破坏了旧的标准,就应该受到制裁。历史上这样的标准不计其数,遏制了多少创造的激情。精神自由的乔布斯,自然受不了这样的约束。因此他把他的坐标定在了人性,定在了性价比,定在了改善消费者的福利层次,定在了改善人们的生活品质。
最了解乔布斯的当数比尔·盖茨,这个持续了30年的强势竞争对手。盖茨新近这样评价乔布斯:“苹果公司一直致力于生产消费者想用的产品。乔布斯似乎总能够了解行业下一步会向什么方向发展。他拥有难以置信的品位和高雅。”乔布斯本人不是技术尖子,他擅长的是商业判断力和对商业模式的敏锐感触力。
乔布斯对生产商聚在一起建立什么标准和平台,具有一种天然的抵触情绪。他的生命就是围绕着品位和粗俗斗争,围绕个性和标准化斗争。他深知自己,也知世人。惊奇是人类的瑰宝。消费者不购买平台,不购买标准,也不购买战略,只购买自己所醉心的产品。当巨头们聚在一起创立标准的时候,乔布斯则躲在一隅醉心研究他的“小物件”。他的所有的奇思妙想和对新产品的大胆构思,都发源于对人性的至察。
1976年,乔布斯同史蒂夫·沃兹尼亚克在一间车库里创建了苹果,比IBM进入个人计算机市场早了5年。史蒂夫·沃兹尼亚克所写的BASIC堪称是世界第一,缺点只有一个:只支持整数计算,不支持浮点。乔布斯要求加入这一功能,但并没有得到满足。于是,乔布斯砸下3.1万美元的血本,向刚出道的比尔·盖茨寻求帮助。当时,微软拥有最受欢迎、最优秀的浮点BASIC。于是诞生了世界上第一台个人电脑。乔布斯也因此成为IT行业的奠基人之一。苹果公司成了规模上市公司后,乔布斯面对投资者和监管机构要求规范的压力,厌倦了。当时他对管理有一种莫名的神秘感。于是不惜自己让位请来高手CEO。可是高手带来了官僚体制,官僚体制最后引发了一场宫廷政变,乔布斯被废,被赶出了十年打拼的苹果。被驱赶出去的乔布斯,一气之下卖掉了苹果股份,只象征性地保留了一股。经历了短暂的痛苦折磨,爽朗而自由的天性解救了他。没有了重压和拘束,带着丰富的嗅觉和执着,他重新投入了“小物件”的创造事业中。
离开乔布斯的苹果随后却步入泥沼,不得不收购乔布斯新创的Next公司和皮克斯公司,以求新生。苹果抓住了稻草,乔布斯抓住了契机。1997年7月,在连续第5个季度亏损后,苹果公司董事会罢免了CEO,乔布斯临危受命。8月在波士顿举行的Mac World会议上,是乔布斯回归苹果后的首次公开亮相。会上,乔布斯宣布了一连串令整个业界震惊不已的消息:甲骨文总裁拉里·埃里森进入苹果董事会;微软注资1.5亿美元购买苹果股票并与苹果达成为期5年的专利权交叉许可协议等等。乔布斯对奄奄一息的苹果公司进行大刀阔斧的公司改组,砍掉没有起色的产品线以及新产品降价促销的措施,终于使苹果恢复了元气。
凭借判断力与高品位,乔布斯把那种产品创新基因也重新植入了苹果。苹果重新成为世界的宠儿。苹果开发出的“小物件”Mac、Next、Powerbook、Nano、iMac、iTune、iPod等等,改变了媒体业、音乐界以及电子消费品的格局。新近上市的“小物件”iPhone,很有可能颠覆手机产业和消费类电产业现有格局。IDC的研究报告指出,苹果的iPhone将对美国移动电话行业产生影响。iPhone的整个吸引力不是一种功能或者设计,而是整体的效果。
乔布斯的苹果所醉心的“小物件”,正是消费类电子产品的灵魂。苹果战略实际上很简单:只要聚焦于制造最好的产品,回报自然随之而来。苹果的每样产品都卓尔不群。在苹果公司的会议上乔布斯可以懒散地把鞋子脱掉把脚架在桌子上来回晃动,但是在对待产品品质的追求上,乔布斯强求的却是一种残忍的完美主义。
令人惊奇的“小物件”中寄托了乔布斯的理想。当年在游说百事可乐公司的约翰·斯库利加盟苹果时,他最具震撼力的语录无意中透露了他的一种使命意识:“你想卖一辈子糖水,还是想改变世界?”
iPhone冲击波
苹果耀眼的产品,常常把人们引入歧途。人们过多地聚焦在那一个个“小物件”本体,而忽视了“小物件”背后的本然。苹果推出的不是单一产品,而是一套以客户为始点和终点的敏捷供应链,是个囊括了众多参与者的虚拟大企业,是一个排他性的面向未来的强大联盟。
这个世界已经习惯了跟随苹果的冲击波前进。可是起初鲜有人能够理解苹果冲击波的威力。iPod惊艳上市时,那些竞争对手只是看到了一台精巧的音乐播放器。于是他们纷纷推出自己的MP3应对iPod冲击波。可是70多个月过去了,没有一家竞争对手能够撼动iPod市场统治地位。后来人们才醒过味来。iPod不仅仅是一部播放器,而是将播放器、版权保护技术和iTune音乐店联合在一起的商业模式。苹果公司把庞大的消费类电子厂商、芯片制造商、软件公司、音乐公司、电脑厂商和零售商的力量整合在一起,形成一个排他性的联盟。iPod的销量已经超过1亿台,促使苹果在线iTunes音乐商店跻身全球三大音乐产品零售商。前两位是沃尔玛和百思买。
同样的故事将在iPhone上演。iPhone不仅仅是一部手机,也不仅仅是全球首台真正意义上的功能强大的迷你笔记本电脑。iPhone是苹果试图建立人们用来看网页、听音乐、看电视电影、打电话等方面的全新体验,同时也是对手机制造商、网络运营商、制造商、电影和电视节目发行商以及计算机公司间实力的重新划分。AT&T、三星等一群新的合作者已经准备好参与这个轻薄电话、网络浏览器、音乐播放器兼数码相机的饕餮盛宴。
苹果跟所有行业的潜规陋习势不两立。到目前为止,移动运营商一直规定移动设备中采用的连接功能。摩托罗拉和诺基亚在移动运营商的压力下不得不提供双模式手机。苹果不理会这些潜规陋习。它自己定义了16种服务,并且在iPhone的手机主页上清楚地标示出来,用户可以通过iTune来签署服务合同,而不必经过AT&T的主页或者它的店面。iPhone将因此改变移动运营商和厂商之间的关系。许多手机厂商也开始直接向消费者销售手机,不理会运营商了。从运营商驱动的范式转向用户驱动的范式,势必将对诺基亚、索爱、摩托罗拉、三星这样的公司产生积极的影响,引起更酷的设计、高级的功能和缩短的更新换代周期。
当然,跟iPhone绑在一起的AT&T也不纯然是局外人。在这种量身定制的服务中,它既可以收取稳定的增值服务费,又可以从其他运营商那里偷取客户。从现在100万iPhone的客户群构成看,至少有40%是非AT&T的客户。“如果iPhone改变了整个手机市场的游戏规则,那我们不得不尽早去适应。”或许这种言论代表了移动巨头们时下的心理。
从独行侠到敏捷供应链盟主
苹果冲击波的发源地来自乔布斯打开了把辽阔隔起来的壁垒。1985年被赶出苹果,使得乔布斯看待世界的视角发生了根本性的改变。
乔布斯发现了自己先前的滑稽:在一个网络的世界里,苹果却硬要扮演独行侠来替天行道。他认识到,“苹果生活在一个生态系统中,它需要其他伙伴帮助,它也需要去帮助别人。”乔布斯从一个纯粹的理想主义者,变成了一个在战略上的现实主义者。1997年乔布斯夺回CEO位置后,在众多棘手问题还没有解决之前,他就邀请老对头盖茨通过视频会议加入他在舞台上的表演,宣布双方达成微软用1.5亿美元来交换苹果放弃专利起诉的协议,以及微软所做出的继续制造与Mac兼容的Office的承诺。从2006年起,他又让苹果电脑使用英特尔公司的芯片,并开发出在苹果电脑上应用Windows的软件。而1985年之前,乔布斯曾经认为微软和英特尔的产品是“邪恶的”。
那时,乔布斯是以一个独行侠的身份在世界上立足。他的一切行为和表现,都是由自我恐惧主导的。他的价值观和行为通常是为了安全、无风险、赢得竞争或尊重。结果生产出第一台电脑的苹果,最后却被挤出了计算机主流企业的行列。现在乔布斯已经对苹果的生态环境有了新的理解。他和他的苹果不再受恐惧和预言所控制,一种为人类服务的自由精神激励着他。创造力和创新,是他实现自身价值的最佳途径。
乔布斯把爱心都倾注到客户身上去了,合作伙伴在许多时候体味到的并不是温柔的爱。所有得到邀请加入乔布斯联盟的合作伙伴,都有点受宠若惊的感觉。可是,想成为乔布斯俱乐部的一员,你得放弃从设计到定价的每一件事情上的一些独立性。而且还要有足够强健的意志,乐于忍受苹果残酷的完美主义。一旦跟不上苹果“对可靠性和用户体验”拼命想达到的疯狂高标准,公司就会被从俱乐部剔除。被剔除的后果不堪想象。
芯片制造商PortalPlayer的前CEO伽利·约翰对此深有体会。这家公司因为给第一代iPod提供电子芯片而大获成功。后来苹果决定不采用一款该公司已经开发了一年多的芯片,而这款芯片被指望给公司带来一半的销售收入。2006年4月的一个早晨,当华尔街获知这个消息后,公司的股票暴跌50%。7个月后,公司被以3.57亿美元的低价买走,还不足这之前市值的一半。约翰的最大感受是,与苹果一起工作令人精疲力竭,而一旦脱离苹果又会被市场抛弃。
无论何时,如果一个项目失败了或是没有达到苹果的要求,它的工程师们都会要求在12小时内得到“根本原因分析和解释”。苹果不接受任何人性化的安抚,只坚持要结果。苹果几乎从来不给出说明技术要求的书面文件,总是倾向于口头沟通,以免泄密。没有哪个供应商完全知道苹果到底在做些什么,所有事情都只限于“必须知道”的范围。就好像你存在的全部理由就是为苹果服务。一旦哪家公司不再忍受苹果的残酷完美主义,那么紧跟着发生的事情就是被投资者和客户忘却。
苹果现在已经转变为科技世界中最有影响力的热门产品制造者。2006年,超过20万家公司签署了制造与苹果相容的产品的协议,比前一年增加了26%。这些公司包括了游戏制造电子艺界(Electronic Arts,EA)和虚拟机软件Vmware,它们都受到比整个PC市场增长速度快3倍的Mac销售的推动。随着iPhone的热卖,势必将有更多的合作伙伴成为乔布斯敏捷供应链上的一环。
谁为iPod和iPhone创造了价值?
iPod和iPhone,都是由遍布多国的数十家公司分工制造而成,每一生产阶段都为最后的价值做出大小不一的贡献。三位Irvine加州大学的研究员在斯隆基金会的资助下完成了一项针对美国市场上一款零售价为299美元30GB的iPod供应链的研究。
三位教授的研究发现,iPod真正的价值不在于内含的零件,更不在于把零件组装起来的整合机,而是集中在iPod的构思与设计。苹果聪明在想出如何把451种普通零件,组装成一台高价值的产品。苹果未必亲手制造iPod,但他们是其创造者。这才是关键。
据报告的粗略分析,苹果借品牌和设计当仁不让地拿到了大头80美元;75美元分给美国的渠道商与零售商;13美元由林林总总的美国国内零组件厂商瓜分;由东芝制造的最昂贵的硬盘,成本价约73美元;最末阶段的组装在中国大陆完成,每台约4美元。其余还有54美元的构成说不清楚,韩国的三星该有不少进账。
组装生产的特殊性值得一提。留给中国的虽然只有4美元收入(要覆盖组装生产、库存、厂房租金、运输、水电、工资等所有工厂开支),每一台iPod成品出口到美国,却要记入中国出口额150美元。这是目前美中贸易逆差巨大的重要原因之一。
iPhone同样是一个庞大的敏捷供应链生产。此前业内估计,iPhone带给苹果的利润率在20%至50%之间,而根据分析公司iSuppli进行的拆卸确定,8GB iPhone的硬件物料清单和制造成本共为265.83美元,按照每个8GB iPhone以599.00美元的零售价进行出售,那么它的毛利润率超过了55%。当然,该估算成本中未包括专利权费、物流、销售等费用。
iSuppli的拆卸分析认为,在各部件供货商中,三星公司仍为最大的获利者。在8GB版iPhone的成本里,三星公司提供的部件总计占了76.25美元,占BOM(物料清单)的30.5%,是最大的供货商;触摸屏模块的提供商为德国Balda参股的坐落在中国的TPK制造厂,该模块估算成本为27美元,占8GB版iPhone BOM的10.8%;显示屏则来自包括EPSON、SHARP、Toshiba Matsushita等多家厂商,预估成本为24.5美元,占8GB版iPhone BOM的9.8%;德国的英飞凌是苹果家族供应商的新成员,为iPhone提供了包括数字基带、无线发射和电源管理等核心通信部件,预估总成本为15.25美元,占8GB版iPhone BOM的6.1%;美国国家半导体公司提供的元件在BOM单中占的比例较小,仅为1.5美元,不到1%;iPhone由鸿海旗下的富士康负责组装。
无论是原有的还是新增加的供应商,都为赢得了iPhone的订单而欢呼雀跃。订单注定对提升其竞争地位更加有利,从而带来更多的市场收益。若干软件供应商都在期盼iPhone能够设立一个软件创意人员社区,从而得以分iPhone的红利。
乔布斯的供应链是否足够强大,可以排斥其他强势竞争对手?iPod创造了70个月没有媲美产品的纪录,iPhone也会吗?还要看乔布斯的供应链是否坚不可摧。因为作为消费者,会对创造者们弄出来的很酷产品欣喜若狂,但是却没有多少人愿意真正付费。这就如同一道强大的地球引力,最终会把任何与之相左的力量抹掉。乔布斯能够支撑多久?
乔布斯自己可能并不认为有这样一场战争。因为苹果是最敢于自我革命的公司,它从来没有停留于任何一款单一成功的产品。当iPhone被地球引力拉下神坛,乔布斯一定看好了更好的产品。有关地球引力的争论,可能会引伸出一个很有意思的话题:苹果如何保持创新优势?
世界上最优秀的创新公司苹果,研发投入并不高。据统计,2006年高科技企业的研发投入排行中,苹果仅以7.15亿美元列第15位,约为排名榜首的微软的1/9,甚至少于雅虎等公司。这是一个不肯以自己拥有的资源来规划创新战略的创新公司。乔布斯掌握着一个庞大的研发敏捷供应链,擅长借助外部智慧。比如,McIntosh率先使用的鼠标、iPhone所使用的Mutli-Touch技术都来自于其他公司,甚至iPod的最初开发工作也是外包完成的。
当然,苹果总是拥有或控制着他们所做的所有事的核心技术。至少在过去乔布斯重新执政的10年里,这家以生产精美硬件产品的公司,其核心技术集中于软件领域。1997年,乔布斯在裁减产品线同时,大肆招募软件领域的人才。他坚信软件将成为未来所有产品的共同“灵魂”。正是这十年的积累,让苹果有能力为iPod、Apple TV和iPhone赋予灵魂。
乔布斯不是技术人才,却是“一个技术标杆”。所有公司都能做出接近完美的模型,但很少有公司能做出品质优良的产品,因为在产品开发过程中,技术、设计等部门会以“做不来”为由,进行缩水处理。这时候就需要一个铁腕领导者将“no”变成“yes”。这是一种超越了技术、超越了理性、超越了现实的直觉判断力。无论是苹果的技术员还是合作伙伴,当场理解和认同他要求的并不多,但很多人承认,乔布斯的压力让他们做了一些超越自己能力的成果。即使那些他参与不多的产品,也会因为他的最终审核而提升水准。
伟大的企业家都是一个开放的系统,他们决不会把辽阔圈起来,拘押了自己。技术背景的薄弱,丝毫没有减低乔布斯的创新激情。那一个时刻灵动捕捉信息的大脑,一定有独特的构造。如果说那不是天赋,也一定有独特的安排。一次偶然答问,乔布斯透露了细节。许多颠覆性的想法,多在睡觉前产生。那是乔布斯可以游离公司业务,独立处理个人电子邮件的时候。乔布斯在六个不同的服务器注册了邮箱并公之于众。每天都要收到300多封有效邮件。一些全然陌生的网友,在邮件中大谈理想或者一些癫狂的设想,给乔布斯无尽的启迪。许多好的点子就是在那样的碰撞中产生。
转动世界的思维
一如索罗斯与拉里·埃里森,史蒂夫·乔布斯深爱哲学。他曾经表示:“我愿意用自己享受的一切高科技,换取与苏格拉底共度一下午的机会。”这不是说说而已。
乔布斯的生母是一名年轻的未婚妈妈。由于非婚生子女在当时为社会所不齿,于是她决定把乔布斯送人。一对工薪夫妇收养了乔布斯。被弃养的烙印,深深地触动了少年的乔布斯的灵魂深处。让他在很小的时候,就开始追问:我是谁?我有什么价值?我来这个世界干什么?人的最大无知,是对人生的无知,对生死的无知。归根到底,是对自心的无知。有些人一辈子过去了,也没想到要弄清楚这个问题。而小小的乔布斯,少年时就开始内在自觉形成,甚是少见。
17岁那年,记不得什么书上的一段话对他产生了强烈的震撼:“如果你把每一天都当作生命的最后一天过,总有一天你的假设会成为现实。”乔布斯记住了这句话。从那时起,他每天早晨都对着镜子扪心自问:假如今天是我生命中的最后一天,我还会去做今天要做的事吗?这件事真值得我去为它投放激情吗?在这样不懈的追问中,乔布斯很早就得以逃脱了“畏惧失去”这个人生的最大陷阱。
乔布斯看上去总是那么精力充沛,总是在他关注的领域探微知著提出很高而又能够实现的标杆,总是能够照亮他的团队和世界。原因就在于他发自少年的内心自觉:生命是短暂的,不久以后我们都将走到尽头,这就是现实。
对生死和人生的思考,最终使他皈依佛法,跟着大野考宾这个日本人修习禅宗,并成为素食主义者。佛教内在“责任自觉”和“空中妙有”的教义,以及那本体、本然与本真的思维层次,对乔布斯特质的淬炼发生了直接影响。
一如山姆·沃尔顿、比尔·盖茨、亨利·福特、稻盛和夫等伟大企业家,乔布斯有着双重性格。作为CEO的乔布斯,有着嫉恶如仇的火爆脾气,到处是一竿子到底的不近人情。以至于没有人敢跟他乘坐同一趟电梯,以防一言不合被炒了鱿鱼。可是作为佛教徒的乔布斯,心静、气静、神静,理亦静。那束创造惊喜“小物件”的通明心思,让他平添了几份“转物而不被物转,转事而不被事转,转人而不为人转”的定力。iPhone的上市,是我们透视乔布斯心底通明处的最新例证。
几年前,美国国家健康学会的技术负责人,曾希望说服乔布斯去开发一款类似Tablet PC的平板电脑。乔布斯坦率拒绝了对方的要求。乔布斯认为这不是一个大市场。相对于每年销量两亿台的个人电脑市场,平板电脑以万台为计量单位的年销量并不足取。健康学会的人又建议他去开发黑莓一类的产品。乔布斯亦不以为然:这是另一个细分市场。他明确看到了电脑和电话的融合将是一个潮流,但黑莓不是这个趋势的一部分。乔布斯非常认真的揭开了他对未来手机的预期:一款可以装在衣服口袋里,并比电脑、手机独立存在时都更好的产品。
谈话还有许多细节不为人知。但是从乔布斯不作准备的这种自然流露中,我们可以看出乔布斯的本体、本然、本真的思维特质。
本体视角:个人和企业的使命与责任自觉。“空中妙有”的信条,让乔布斯醉心于承载无数“空性”的“妙有”。那些承载的“空性”越多,越具备大众市场特质的“小物件”,就越能激发他的本能直觉和想象。正是由于这种直觉,他能够率先造出第一台个人电脑,能够制造出销量过亿台的iPod,能够创造出势将搅动世界秩序的iPhone。这是一种本体视角的思维,是商业活动的出发点。
本然视角:人人生活在一个系统中,需要伙伴帮助,也要帮助别人。乔布斯并不是着眼自己能否解决技术难题,而是放眼相关行业看是否有相关的技术突破。苹果所有的产品,都是与相关行业的顶尖公司进行合作,就是这个道理。一件产品受制于许多行业技术进步的现状。其他行业的一些具体的技术障碍,将影响本行业的产品的性能。医疗与绘图行业的本质规定性,现有的技术还很难突破。苹果电脑是三维制作独一无二的产品,还没有其他厂商能够撼动苹果的位置。乔布斯对医疗与制图行业的特殊规定性,了然于胸。
本真视角:抓住行业的本质。创造惊奇就是要超乎消费者的想象增加新价值在新产品中。如果不能增加新价值,或增加的新价值有限,就没有必要多耗精力。开拓新生活,改变世界,是乔布斯灵魂深处的冲动与追求。手机功能的迅速提升,给纯粹的音乐播放器带来巨大的威胁。身处排头兵,乔布斯最清楚危机来自哪里。于是,他要制作电脑和电话融合的新产品。这个新产品是比电脑电话单独存在时更好的产品,要创造新价值。
一个人旺盛的创造力往往会使自己产生一种幻象,似乎自己拥有着无边的伟力。一旦产生这份执著,这个人的创造力也就开始消减了。乔布斯看上去似乎暂时避免了这等的无知。当人们深信伟力的时候,往往把一件最重要的事情给忘了,就是“死亡”。一名佛教徒步入上等智慧的重要门槛就是对死亡的喜爱。十几年前那场虚张声势的癌症,让乔布斯对死亡有了更为通明的认识。起初诊断为恶性晚期,医生宣布他只能活3~5个月,切片后发现是良性。和死神离得最近的一次经历,让乔布斯能够以一种轻松和自由的方式来看待死亡,他甚至把死亡看成“生命最好的一项发明”,再没有比死亡更能推进生命的新陈代谢了。
在那次对斯坦福大学毕业生的著名演讲中,乔布斯深情地说,你们的时间有限,所以不要把时间浪费在重复其他人的生活上。不要让他人的观点所发出的噪音淹没自己内心的声音。最为重要的是,要有遵从自己内心和直觉的勇气,它们可能已经知道你其实想成为一个什么样的人。
最后,乔布斯把17岁以来警醒自己的座右铭送给了在场的大学生,同时也送给了世界上那些不甘平庸的灵魂:
“保持饥饿,保持愚蠢”。
-文/王育琨 来自:《商界评论》杂志
作者简介:王育琨,首钢研究院企业所所长
2007年6月29日,一股强劲的旋风由美国的史蒂夫·乔布斯和iPhone发起,iPhone开始销售一周之内,已经启用了100万部iPhone手机。史蒂夫·乔布斯原来的目标是在2007年年底之前销售100万部iPhone,实际上他只用了6天时间就实现了这个目标。而当初iPod推出的第一年,只卖出了10万台。iPhone吸引了全球媒体和重要消费类电子制造商的眼球。美国谐星Stephen Colbert开玩笑说,iPhone发售是人类历史上仅次于耶稣诞生的第二个重大事件。诺基亚、摩托罗拉、三星、索爱、LG等等都在紧张地关注,iPhone是否会重塑手机和消费类电子行业的未来。有一点可以确信,iPhone已经重新塑造了苹果市值。到7月12日,苹果市值已经达到1140亿美元,超越了甲骨文和戴尔。分析师更做出了乐观预计,苹果市值将在18个月内超越IBM(市值1450亿美元)和英特尔(1610亿美元)。
而中国人则出奇淡定。遇到几个中国消费类电子产品制造商的高管,对此不屑一顾,他们有更重要的事情要关心。中国当仁不让现在是消费类电子产品的生产大国。可惜,中国还没有一家消费类电子的世界级品牌。电子消费品的灵魂是什么?中国消费类电子制造商,把iPhone上市看成了一个简单的营销事件,从而低估了iPhone带来的冲击波,并没有做好应对iPhone冲击的准备。iPhone是关乎消费类电子和手机产业未来的一个“小物件”。中国人需要跟着乔布斯和他的iPhone,来理解消费类电子的未来。
“小物件”,大世界
古代斯巴达的立法者利库尔戈斯,毕其一生让斯巴达人民保证不违背他制定的任何法律。古代爱琴海莱斯沃斯岛上有名的奇特拉琴家弗里尼斯,他给原为七根弦的奇特拉琴增加了两根弦。斯巴达法官根本不问这增加的两根弦是否让音乐更加悦耳动听,高举标准的旗帜,认为这是糟蹋音乐,便粗暴地将琴砍断了。只因为那两根弦破坏了旧的标准,就应该受到制裁。历史上这样的标准不计其数,遏制了多少创造的激情。精神自由的乔布斯,自然受不了这样的约束。因此他把他的坐标定在了人性,定在了性价比,定在了改善消费者的福利层次,定在了改善人们的生活品质。
最了解乔布斯的当数比尔·盖茨,这个持续了30年的强势竞争对手。盖茨新近这样评价乔布斯:“苹果公司一直致力于生产消费者想用的产品。乔布斯似乎总能够了解行业下一步会向什么方向发展。他拥有难以置信的品位和高雅。”乔布斯本人不是技术尖子,他擅长的是商业判断力和对商业模式的敏锐感触力。
乔布斯对生产商聚在一起建立什么标准和平台,具有一种天然的抵触情绪。他的生命就是围绕着品位和粗俗斗争,围绕个性和标准化斗争。他深知自己,也知世人。惊奇是人类的瑰宝。消费者不购买平台,不购买标准,也不购买战略,只购买自己所醉心的产品。当巨头们聚在一起创立标准的时候,乔布斯则躲在一隅醉心研究他的“小物件”。他的所有的奇思妙想和对新产品的大胆构思,都发源于对人性的至察。
1976年,乔布斯同史蒂夫·沃兹尼亚克在一间车库里创建了苹果,比IBM进入个人计算机市场早了5年。史蒂夫·沃兹尼亚克所写的BASIC堪称是世界第一,缺点只有一个:只支持整数计算,不支持浮点。乔布斯要求加入这一功能,但并没有得到满足。于是,乔布斯砸下3.1万美元的血本,向刚出道的比尔·盖茨寻求帮助。当时,微软拥有最受欢迎、最优秀的浮点BASIC。于是诞生了世界上第一台个人电脑。乔布斯也因此成为IT行业的奠基人之一。苹果公司成了规模上市公司后,乔布斯面对投资者和监管机构要求规范的压力,厌倦了。当时他对管理有一种莫名的神秘感。于是不惜自己让位请来高手CEO。可是高手带来了官僚体制,官僚体制最后引发了一场宫廷政变,乔布斯被废,被赶出了十年打拼的苹果。被驱赶出去的乔布斯,一气之下卖掉了苹果股份,只象征性地保留了一股。经历了短暂的痛苦折磨,爽朗而自由的天性解救了他。没有了重压和拘束,带着丰富的嗅觉和执着,他重新投入了“小物件”的创造事业中。
离开乔布斯的苹果随后却步入泥沼,不得不收购乔布斯新创的Next公司和皮克斯公司,以求新生。苹果抓住了稻草,乔布斯抓住了契机。1997年7月,在连续第5个季度亏损后,苹果公司董事会罢免了CEO,乔布斯临危受命。8月在波士顿举行的Mac World会议上,是乔布斯回归苹果后的首次公开亮相。会上,乔布斯宣布了一连串令整个业界震惊不已的消息:甲骨文总裁拉里·埃里森进入苹果董事会;微软注资1.5亿美元购买苹果股票并与苹果达成为期5年的专利权交叉许可协议等等。乔布斯对奄奄一息的苹果公司进行大刀阔斧的公司改组,砍掉没有起色的产品线以及新产品降价促销的措施,终于使苹果恢复了元气。
凭借判断力与高品位,乔布斯把那种产品创新基因也重新植入了苹果。苹果重新成为世界的宠儿。苹果开发出的“小物件”Mac、Next、Powerbook、Nano、iMac、iTune、iPod等等,改变了媒体业、音乐界以及电子消费品的格局。新近上市的“小物件”iPhone,很有可能颠覆手机产业和消费类电产业现有格局。IDC的研究报告指出,苹果的iPhone将对美国移动电话行业产生影响。iPhone的整个吸引力不是一种功能或者设计,而是整体的效果。
乔布斯的苹果所醉心的“小物件”,正是消费类电子产品的灵魂。苹果战略实际上很简单:只要聚焦于制造最好的产品,回报自然随之而来。苹果的每样产品都卓尔不群。在苹果公司的会议上乔布斯可以懒散地把鞋子脱掉把脚架在桌子上来回晃动,但是在对待产品品质的追求上,乔布斯强求的却是一种残忍的完美主义。
令人惊奇的“小物件”中寄托了乔布斯的理想。当年在游说百事可乐公司的约翰·斯库利加盟苹果时,他最具震撼力的语录无意中透露了他的一种使命意识:“你想卖一辈子糖水,还是想改变世界?”
iPhone冲击波
苹果耀眼的产品,常常把人们引入歧途。人们过多地聚焦在那一个个“小物件”本体,而忽视了“小物件”背后的本然。苹果推出的不是单一产品,而是一套以客户为始点和终点的敏捷供应链,是个囊括了众多参与者的虚拟大企业,是一个排他性的面向未来的强大联盟。
这个世界已经习惯了跟随苹果的冲击波前进。可是起初鲜有人能够理解苹果冲击波的威力。iPod惊艳上市时,那些竞争对手只是看到了一台精巧的音乐播放器。于是他们纷纷推出自己的MP3应对iPod冲击波。可是70多个月过去了,没有一家竞争对手能够撼动iPod市场统治地位。后来人们才醒过味来。iPod不仅仅是一部播放器,而是将播放器、版权保护技术和iTune音乐店联合在一起的商业模式。苹果公司把庞大的消费类电子厂商、芯片制造商、软件公司、音乐公司、电脑厂商和零售商的力量整合在一起,形成一个排他性的联盟。iPod的销量已经超过1亿台,促使苹果在线iTunes音乐商店跻身全球三大音乐产品零售商。前两位是沃尔玛和百思买。
同样的故事将在iPhone上演。iPhone不仅仅是一部手机,也不仅仅是全球首台真正意义上的功能强大的迷你笔记本电脑。iPhone是苹果试图建立人们用来看网页、听音乐、看电视电影、打电话等方面的全新体验,同时也是对手机制造商、网络运营商、制造商、电影和电视节目发行商以及计算机公司间实力的重新划分。AT&T、三星等一群新的合作者已经准备好参与这个轻薄电话、网络浏览器、音乐播放器兼数码相机的饕餮盛宴。
苹果跟所有行业的潜规陋习势不两立。到目前为止,移动运营商一直规定移动设备中采用的连接功能。摩托罗拉和诺基亚在移动运营商的压力下不得不提供双模式手机。苹果不理会这些潜规陋习。它自己定义了16种服务,并且在iPhone的手机主页上清楚地标示出来,用户可以通过iTune来签署服务合同,而不必经过AT&T的主页或者它的店面。iPhone将因此改变移动运营商和厂商之间的关系。许多手机厂商也开始直接向消费者销售手机,不理会运营商了。从运营商驱动的范式转向用户驱动的范式,势必将对诺基亚、索爱、摩托罗拉、三星这样的公司产生积极的影响,引起更酷的设计、高级的功能和缩短的更新换代周期。
当然,跟iPhone绑在一起的AT&T也不纯然是局外人。在这种量身定制的服务中,它既可以收取稳定的增值服务费,又可以从其他运营商那里偷取客户。从现在100万iPhone的客户群构成看,至少有40%是非AT&T的客户。“如果iPhone改变了整个手机市场的游戏规则,那我们不得不尽早去适应。”或许这种言论代表了移动巨头们时下的心理。
从独行侠到敏捷供应链盟主
苹果冲击波的发源地来自乔布斯打开了把辽阔隔起来的壁垒。1985年被赶出苹果,使得乔布斯看待世界的视角发生了根本性的改变。
乔布斯发现了自己先前的滑稽:在一个网络的世界里,苹果却硬要扮演独行侠来替天行道。他认识到,“苹果生活在一个生态系统中,它需要其他伙伴帮助,它也需要去帮助别人。”乔布斯从一个纯粹的理想主义者,变成了一个在战略上的现实主义者。1997年乔布斯夺回CEO位置后,在众多棘手问题还没有解决之前,他就邀请老对头盖茨通过视频会议加入他在舞台上的表演,宣布双方达成微软用1.5亿美元来交换苹果放弃专利起诉的协议,以及微软所做出的继续制造与Mac兼容的Office的承诺。从2006年起,他又让苹果电脑使用英特尔公司的芯片,并开发出在苹果电脑上应用Windows的软件。而1985年之前,乔布斯曾经认为微软和英特尔的产品是“邪恶的”。
那时,乔布斯是以一个独行侠的身份在世界上立足。他的一切行为和表现,都是由自我恐惧主导的。他的价值观和行为通常是为了安全、无风险、赢得竞争或尊重。结果生产出第一台电脑的苹果,最后却被挤出了计算机主流企业的行列。现在乔布斯已经对苹果的生态环境有了新的理解。他和他的苹果不再受恐惧和预言所控制,一种为人类服务的自由精神激励着他。创造力和创新,是他实现自身价值的最佳途径。
乔布斯把爱心都倾注到客户身上去了,合作伙伴在许多时候体味到的并不是温柔的爱。所有得到邀请加入乔布斯联盟的合作伙伴,都有点受宠若惊的感觉。可是,想成为乔布斯俱乐部的一员,你得放弃从设计到定价的每一件事情上的一些独立性。而且还要有足够强健的意志,乐于忍受苹果残酷的完美主义。一旦跟不上苹果“对可靠性和用户体验”拼命想达到的疯狂高标准,公司就会被从俱乐部剔除。被剔除的后果不堪想象。
芯片制造商PortalPlayer的前CEO伽利·约翰对此深有体会。这家公司因为给第一代iPod提供电子芯片而大获成功。后来苹果决定不采用一款该公司已经开发了一年多的芯片,而这款芯片被指望给公司带来一半的销售收入。2006年4月的一个早晨,当华尔街获知这个消息后,公司的股票暴跌50%。7个月后,公司被以3.57亿美元的低价买走,还不足这之前市值的一半。约翰的最大感受是,与苹果一起工作令人精疲力竭,而一旦脱离苹果又会被市场抛弃。
无论何时,如果一个项目失败了或是没有达到苹果的要求,它的工程师们都会要求在12小时内得到“根本原因分析和解释”。苹果不接受任何人性化的安抚,只坚持要结果。苹果几乎从来不给出说明技术要求的书面文件,总是倾向于口头沟通,以免泄密。没有哪个供应商完全知道苹果到底在做些什么,所有事情都只限于“必须知道”的范围。就好像你存在的全部理由就是为苹果服务。一旦哪家公司不再忍受苹果的残酷完美主义,那么紧跟着发生的事情就是被投资者和客户忘却。
苹果现在已经转变为科技世界中最有影响力的热门产品制造者。2006年,超过20万家公司签署了制造与苹果相容的产品的协议,比前一年增加了26%。这些公司包括了游戏制造电子艺界(Electronic Arts,EA)和虚拟机软件Vmware,它们都受到比整个PC市场增长速度快3倍的Mac销售的推动。随着iPhone的热卖,势必将有更多的合作伙伴成为乔布斯敏捷供应链上的一环。
谁为iPod和iPhone创造了价值?
iPod和iPhone,都是由遍布多国的数十家公司分工制造而成,每一生产阶段都为最后的价值做出大小不一的贡献。三位Irvine加州大学的研究员在斯隆基金会的资助下完成了一项针对美国市场上一款零售价为299美元30GB的iPod供应链的研究。
三位教授的研究发现,iPod真正的价值不在于内含的零件,更不在于把零件组装起来的整合机,而是集中在iPod的构思与设计。苹果聪明在想出如何把451种普通零件,组装成一台高价值的产品。苹果未必亲手制造iPod,但他们是其创造者。这才是关键。
据报告的粗略分析,苹果借品牌和设计当仁不让地拿到了大头80美元;75美元分给美国的渠道商与零售商;13美元由林林总总的美国国内零组件厂商瓜分;由东芝制造的最昂贵的硬盘,成本价约73美元;最末阶段的组装在中国大陆完成,每台约4美元。其余还有54美元的构成说不清楚,韩国的三星该有不少进账。
组装生产的特殊性值得一提。留给中国的虽然只有4美元收入(要覆盖组装生产、库存、厂房租金、运输、水电、工资等所有工厂开支),每一台iPod成品出口到美国,却要记入中国出口额150美元。这是目前美中贸易逆差巨大的重要原因之一。
iPhone同样是一个庞大的敏捷供应链生产。此前业内估计,iPhone带给苹果的利润率在20%至50%之间,而根据分析公司iSuppli进行的拆卸确定,8GB iPhone的硬件物料清单和制造成本共为265.83美元,按照每个8GB iPhone以599.00美元的零售价进行出售,那么它的毛利润率超过了55%。当然,该估算成本中未包括专利权费、物流、销售等费用。
iSuppli的拆卸分析认为,在各部件供货商中,三星公司仍为最大的获利者。在8GB版iPhone的成本里,三星公司提供的部件总计占了76.25美元,占BOM(物料清单)的30.5%,是最大的供货商;触摸屏模块的提供商为德国Balda参股的坐落在中国的TPK制造厂,该模块估算成本为27美元,占8GB版iPhone BOM的10.8%;显示屏则来自包括EPSON、SHARP、Toshiba Matsushita等多家厂商,预估成本为24.5美元,占8GB版iPhone BOM的9.8%;德国的英飞凌是苹果家族供应商的新成员,为iPhone提供了包括数字基带、无线发射和电源管理等核心通信部件,预估总成本为15.25美元,占8GB版iPhone BOM的6.1%;美国国家半导体公司提供的元件在BOM单中占的比例较小,仅为1.5美元,不到1%;iPhone由鸿海旗下的富士康负责组装。
无论是原有的还是新增加的供应商,都为赢得了iPhone的订单而欢呼雀跃。订单注定对提升其竞争地位更加有利,从而带来更多的市场收益。若干软件供应商都在期盼iPhone能够设立一个软件创意人员社区,从而得以分iPhone的红利。
乔布斯的供应链是否足够强大,可以排斥其他强势竞争对手?iPod创造了70个月没有媲美产品的纪录,iPhone也会吗?还要看乔布斯的供应链是否坚不可摧。因为作为消费者,会对创造者们弄出来的很酷产品欣喜若狂,但是却没有多少人愿意真正付费。这就如同一道强大的地球引力,最终会把任何与之相左的力量抹掉。乔布斯能够支撑多久?
乔布斯自己可能并不认为有这样一场战争。因为苹果是最敢于自我革命的公司,它从来没有停留于任何一款单一成功的产品。当iPhone被地球引力拉下神坛,乔布斯一定看好了更好的产品。有关地球引力的争论,可能会引伸出一个很有意思的话题:苹果如何保持创新优势?
世界上最优秀的创新公司苹果,研发投入并不高。据统计,2006年高科技企业的研发投入排行中,苹果仅以7.15亿美元列第15位,约为排名榜首的微软的1/9,甚至少于雅虎等公司。这是一个不肯以自己拥有的资源来规划创新战略的创新公司。乔布斯掌握着一个庞大的研发敏捷供应链,擅长借助外部智慧。比如,McIntosh率先使用的鼠标、iPhone所使用的Mutli-Touch技术都来自于其他公司,甚至iPod的最初开发工作也是外包完成的。
当然,苹果总是拥有或控制着他们所做的所有事的核心技术。至少在过去乔布斯重新执政的10年里,这家以生产精美硬件产品的公司,其核心技术集中于软件领域。1997年,乔布斯在裁减产品线同时,大肆招募软件领域的人才。他坚信软件将成为未来所有产品的共同“灵魂”。正是这十年的积累,让苹果有能力为iPod、Apple TV和iPhone赋予灵魂。
乔布斯不是技术人才,却是“一个技术标杆”。所有公司都能做出接近完美的模型,但很少有公司能做出品质优良的产品,因为在产品开发过程中,技术、设计等部门会以“做不来”为由,进行缩水处理。这时候就需要一个铁腕领导者将“no”变成“yes”。这是一种超越了技术、超越了理性、超越了现实的直觉判断力。无论是苹果的技术员还是合作伙伴,当场理解和认同他要求的并不多,但很多人承认,乔布斯的压力让他们做了一些超越自己能力的成果。即使那些他参与不多的产品,也会因为他的最终审核而提升水准。
伟大的企业家都是一个开放的系统,他们决不会把辽阔圈起来,拘押了自己。技术背景的薄弱,丝毫没有减低乔布斯的创新激情。那一个时刻灵动捕捉信息的大脑,一定有独特的构造。如果说那不是天赋,也一定有独特的安排。一次偶然答问,乔布斯透露了细节。许多颠覆性的想法,多在睡觉前产生。那是乔布斯可以游离公司业务,独立处理个人电子邮件的时候。乔布斯在六个不同的服务器注册了邮箱并公之于众。每天都要收到300多封有效邮件。一些全然陌生的网友,在邮件中大谈理想或者一些癫狂的设想,给乔布斯无尽的启迪。许多好的点子就是在那样的碰撞中产生。
转动世界的思维
一如索罗斯与拉里·埃里森,史蒂夫·乔布斯深爱哲学。他曾经表示:“我愿意用自己享受的一切高科技,换取与苏格拉底共度一下午的机会。”这不是说说而已。
乔布斯的生母是一名年轻的未婚妈妈。由于非婚生子女在当时为社会所不齿,于是她决定把乔布斯送人。一对工薪夫妇收养了乔布斯。被弃养的烙印,深深地触动了少年的乔布斯的灵魂深处。让他在很小的时候,就开始追问:我是谁?我有什么价值?我来这个世界干什么?人的最大无知,是对人生的无知,对生死的无知。归根到底,是对自心的无知。有些人一辈子过去了,也没想到要弄清楚这个问题。而小小的乔布斯,少年时就开始内在自觉形成,甚是少见。
17岁那年,记不得什么书上的一段话对他产生了强烈的震撼:“如果你把每一天都当作生命的最后一天过,总有一天你的假设会成为现实。”乔布斯记住了这句话。从那时起,他每天早晨都对着镜子扪心自问:假如今天是我生命中的最后一天,我还会去做今天要做的事吗?这件事真值得我去为它投放激情吗?在这样不懈的追问中,乔布斯很早就得以逃脱了“畏惧失去”这个人生的最大陷阱。
乔布斯看上去总是那么精力充沛,总是在他关注的领域探微知著提出很高而又能够实现的标杆,总是能够照亮他的团队和世界。原因就在于他发自少年的内心自觉:生命是短暂的,不久以后我们都将走到尽头,这就是现实。
对生死和人生的思考,最终使他皈依佛法,跟着大野考宾这个日本人修习禅宗,并成为素食主义者。佛教内在“责任自觉”和“空中妙有”的教义,以及那本体、本然与本真的思维层次,对乔布斯特质的淬炼发生了直接影响。
一如山姆·沃尔顿、比尔·盖茨、亨利·福特、稻盛和夫等伟大企业家,乔布斯有着双重性格。作为CEO的乔布斯,有着嫉恶如仇的火爆脾气,到处是一竿子到底的不近人情。以至于没有人敢跟他乘坐同一趟电梯,以防一言不合被炒了鱿鱼。可是作为佛教徒的乔布斯,心静、气静、神静,理亦静。那束创造惊喜“小物件”的通明心思,让他平添了几份“转物而不被物转,转事而不被事转,转人而不为人转”的定力。iPhone的上市,是我们透视乔布斯心底通明处的最新例证。
几年前,美国国家健康学会的技术负责人,曾希望说服乔布斯去开发一款类似Tablet PC的平板电脑。乔布斯坦率拒绝了对方的要求。乔布斯认为这不是一个大市场。相对于每年销量两亿台的个人电脑市场,平板电脑以万台为计量单位的年销量并不足取。健康学会的人又建议他去开发黑莓一类的产品。乔布斯亦不以为然:这是另一个细分市场。他明确看到了电脑和电话的融合将是一个潮流,但黑莓不是这个趋势的一部分。乔布斯非常认真的揭开了他对未来手机的预期:一款可以装在衣服口袋里,并比电脑、手机独立存在时都更好的产品。
谈话还有许多细节不为人知。但是从乔布斯不作准备的这种自然流露中,我们可以看出乔布斯的本体、本然、本真的思维特质。
本体视角:个人和企业的使命与责任自觉。“空中妙有”的信条,让乔布斯醉心于承载无数“空性”的“妙有”。那些承载的“空性”越多,越具备大众市场特质的“小物件”,就越能激发他的本能直觉和想象。正是由于这种直觉,他能够率先造出第一台个人电脑,能够制造出销量过亿台的iPod,能够创造出势将搅动世界秩序的iPhone。这是一种本体视角的思维,是商业活动的出发点。
本然视角:人人生活在一个系统中,需要伙伴帮助,也要帮助别人。乔布斯并不是着眼自己能否解决技术难题,而是放眼相关行业看是否有相关的技术突破。苹果所有的产品,都是与相关行业的顶尖公司进行合作,就是这个道理。一件产品受制于许多行业技术进步的现状。其他行业的一些具体的技术障碍,将影响本行业的产品的性能。医疗与绘图行业的本质规定性,现有的技术还很难突破。苹果电脑是三维制作独一无二的产品,还没有其他厂商能够撼动苹果的位置。乔布斯对医疗与制图行业的特殊规定性,了然于胸。
本真视角:抓住行业的本质。创造惊奇就是要超乎消费者的想象增加新价值在新产品中。如果不能增加新价值,或增加的新价值有限,就没有必要多耗精力。开拓新生活,改变世界,是乔布斯灵魂深处的冲动与追求。手机功能的迅速提升,给纯粹的音乐播放器带来巨大的威胁。身处排头兵,乔布斯最清楚危机来自哪里。于是,他要制作电脑和电话融合的新产品。这个新产品是比电脑电话单独存在时更好的产品,要创造新价值。
一个人旺盛的创造力往往会使自己产生一种幻象,似乎自己拥有着无边的伟力。一旦产生这份执著,这个人的创造力也就开始消减了。乔布斯看上去似乎暂时避免了这等的无知。当人们深信伟力的时候,往往把一件最重要的事情给忘了,就是“死亡”。一名佛教徒步入上等智慧的重要门槛就是对死亡的喜爱。十几年前那场虚张声势的癌症,让乔布斯对死亡有了更为通明的认识。起初诊断为恶性晚期,医生宣布他只能活3~5个月,切片后发现是良性。和死神离得最近的一次经历,让乔布斯能够以一种轻松和自由的方式来看待死亡,他甚至把死亡看成“生命最好的一项发明”,再没有比死亡更能推进生命的新陈代谢了。
在那次对斯坦福大学毕业生的著名演讲中,乔布斯深情地说,你们的时间有限,所以不要把时间浪费在重复其他人的生活上。不要让他人的观点所发出的噪音淹没自己内心的声音。最为重要的是,要有遵从自己内心和直觉的勇气,它们可能已经知道你其实想成为一个什么样的人。
最后,乔布斯把17岁以来警醒自己的座右铭送给了在场的大学生,同时也送给了世界上那些不甘平庸的灵魂:
“保持饥饿,保持愚蠢”。
虽然自己一直都是做网站开发的,没做过什么软件开发,但使用过的软件也不算少。渐渐的发现有些软件真的功能强大,操作性太复杂,于是该软件真的变成了所谓的专业软件,只有经过专业培训的人才能使用。问题是如果不是有很特别的需要,又有多少人真的肯或觉得值得为使用该软件而去花时间。
在我的眼里,最完美的软件是用最傻瓜式的操作。
一直很喜欢西门子的一句广告词,精于心,简于形。设计的理念如此到底,让人不得不佩服。再看看它的产品,真的也不是盖的。
而软件的设计最高精境界就是提供最简单的操作,提供最优秀的功能。这也是为什么Web OS受到推崇的原因,如果Web OS 真的成熟了,那么电脑的开机的真的就是开机而已,打开显示屏,输入你的电脑帐号就可以使用了, 不用学装系统,也不用学备份系统,也不用装一堆的软件。你需要什么只要在网页的搜索栏里输入关键词, 下面就自动列出作优秀的3款软件供你点击使用,真的所见即所得,所得即可用。那时候还管它是WINDOWS 还是LIUNX 或MAC OS,消费者关注的是使用的便利性。
科技以人为本,人需要什么?
人需要懒惰!人需一学即会,一看即懂,一用便精。
你也许会鄙视的说那还用什么技术可言,对,就是要无技术可言。让你的设计的产品,在普通消费者使用起来觉得无须专业知识,无须学习那才是最好的。只是我也觉得这近乎完美,所以近乎不可能,但是努力减少用户使用你的产品的学习时间,把你的产品设计往这个方向走肯定没错。
根据80/20原则,80%的用户只用某款产品20%的功能,而这20%的功能是最有用的,剩下来80%的功能的可以说是花俏,可有可无;但是有20% 的用户却需要额外的80%的功能。于是很多公司都通过功能的限制来发布版本;什么入门级,进阶级,专业级以占有最大的市场。其实这是合法的商业手段,问题是很多公司的都过不了易操作性这一关,这也难怪有些公司对自己的产品信心满满,推出之后却市场反应平平,能占有的也只是所谓的专业人士市场。当然如果真的定位专业市场我无话可说,但即使是专业市场,易操作也不是缺点吧。
随着SaaS的推广,软件的操作性将越来越重要,没有哪一位用户会喜欢操作繁琐的软件,所以即使是到了SaaS时期,即使是在线软件,少了安装调试的步骤,但也别忘了用户的操作体验。
在我的眼里,最完美的软件是用最傻瓜式的操作。
一直很喜欢西门子的一句广告词,精于心,简于形。设计的理念如此到底,让人不得不佩服。再看看它的产品,真的也不是盖的。
而软件的设计最高精境界就是提供最简单的操作,提供最优秀的功能。这也是为什么Web OS受到推崇的原因,如果Web OS 真的成熟了,那么电脑的开机的真的就是开机而已,打开显示屏,输入你的电脑帐号就可以使用了, 不用学装系统,也不用学备份系统,也不用装一堆的软件。你需要什么只要在网页的搜索栏里输入关键词, 下面就自动列出作优秀的3款软件供你点击使用,真的所见即所得,所得即可用。那时候还管它是WINDOWS 还是LIUNX 或MAC OS,消费者关注的是使用的便利性。
科技以人为本,人需要什么?
人需要懒惰!人需一学即会,一看即懂,一用便精。
你也许会鄙视的说那还用什么技术可言,对,就是要无技术可言。让你的设计的产品,在普通消费者使用起来觉得无须专业知识,无须学习那才是最好的。只是我也觉得这近乎完美,所以近乎不可能,但是努力减少用户使用你的产品的学习时间,把你的产品设计往这个方向走肯定没错。
根据80/20原则,80%的用户只用某款产品20%的功能,而这20%的功能是最有用的,剩下来80%的功能的可以说是花俏,可有可无;但是有20% 的用户却需要额外的80%的功能。于是很多公司都通过功能的限制来发布版本;什么入门级,进阶级,专业级以占有最大的市场。其实这是合法的商业手段,问题是很多公司的都过不了易操作性这一关,这也难怪有些公司对自己的产品信心满满,推出之后却市场反应平平,能占有的也只是所谓的专业人士市场。当然如果真的定位专业市场我无话可说,但即使是专业市场,易操作也不是缺点吧。
随着SaaS的推广,软件的操作性将越来越重要,没有哪一位用户会喜欢操作繁琐的软件,所以即使是到了SaaS时期,即使是在线软件,少了安装调试的步骤,但也别忘了用户的操作体验。