黑客c语言.docx
- 文档编号:8025194
- 上传时间:2023-01-28
- 格式:DOCX
- 页数:8
- 大小:24.22KB
黑客c语言.docx
《黑客c语言.docx》由会员分享,可在线阅读,更多相关《黑客c语言.docx(8页珍藏版)》请在冰豆网上搜索。
黑客c语言
从接触,开始学习计算机也好几年了,一路走来,有很多的收获,也有不少的遗憾,现在正好有一段闲暇,就想对走过的路留下一些足迹,回忆。
每个人都有自己不同的人生,说到这里,就是程序人生了,歌德在《浮士德》中说过:
“如果不曾在悲哀中咀嚼过面包,不曾在哭泣中等待过明天,这样的人就不知道你——天的力量。
”所以我想记下一些带给我悲哀,带给我哭泣的程序人生。
其实学习计算机的基础课程是非常重要的,离散数学,编译原理,操作系统,形式语言……,如果你认真走过了这些路,在以后的日子你会发现你的路会越走越宽,以前的努力和汗水会不断的给你灵感,给你支持,给你前进的武器和勇气。
你会发现以后取得的很多成就,不过是朝花夕拾而已!
对于程序语言我喜欢的是C++,它能带给你别的语言无法给予你的无上的智力快感,当然也会给你一门语言所能给你的魔鬼般的折磨。
其实Java,C#,Python语言也非常的不错,我也极为喜欢。
它们都是非常成功的语言,我从来就不愿意做某一种语言的盲目信仰者,每种语言都有它成功的地方,失败的地方,都有它适合的地方,不如意的地方。
所以每一次看到评价语言的文章,我看看,但从来不会发言。
C++的前世是C,而且C所留下的神秘以及精简在C++中是青出于蓝而胜于蓝!
C所带给人的困惑以及灵活太多,即使一个有几年经验的高段C程序员仍然有可能在C语言的小水沟里翻船。
不过其实C语言真的不难,下面我想指出C语言中最神秘而又诡谲多变的四个地方,它们也继续在C++语言中变幻莫测。
指针,数组,类型的识别,参数可变的函数。
一.指针。
它的本质是地址的类型。
在许多语言中根本就没有这个概念。
但是它却正是C灵活,高效,在面向过程的时代所向披靡的原因所在。
因为C的内存模型基本上对应了现在vonNeumann(冯·诺伊曼)计算机的机器模型,很好的达到了对机器的映射。
不过有些人似乎永远也不能理解指针【注1】。
注1:
JoelSpolsky就是这样认为的,他认为对指针的理解是一种aptitude,不是通过训练就可以达到的
.../fog0000000073.html
指针可以指向值、数组、函数,当然它也可以作为值使用。
看下面的几个例子:
int*p;//p是一个指针,指向一个整数
int**p;//p是一个指针,它指向第二个指针,然后指向一个整数
int(*pa)[3];//pa是一个指针,指向一个拥有3个整数的数组
int(*pf)();//pf是一个指向函数的指针,这个函数返回一个整数
后面第四节我会详细讲解标识符(identifier)类型的识别。
1.指针本身的类型是什么?
先看下面的例子:
inta;//a的类型是什么?
对,把a去掉就可以了。
因此上面的4个声明语句中的指针本身的类型为:
int*
int**
int(*)[3]
int(*)()
它们都是复合类型,也就是类型与类型结合而成的类型。
意义分别如下:
pointtoint(指向一个整数的指针)
pointertopointertoint(指向一个指向整数的指针的指针)
pointertoarrayof3ints(指向一个拥有三个整数的数组的指针)
pointertofunctionofparameterisvoidandreturnvalueisint(指向一个函数的指针,这个函数参数为空,返回值为整数)
2.指针所指物的类型是什么?
很简单,指针本身的类型去掉“*”号就可以了,分别如下:
int
int*
int()[3]
int()()
3和4有点怪,不是吗?
请擦亮你的眼睛,在那个用来把“*”号包住的“()”是多余的,所以:
int()[3]就是int[3](一个拥有三个整数的数组)
int()()就是int()(一个函数,参数为空,返回值为整数)【注2】
注2:
一个小小的提醒,第二个“()”是一个运算符,名字叫函数调用运算符(functioncalloperator)。
3.指针的算术运算。
请再次记住:
指针不是一个简单的类型,它是一个和指针所指物的类型复合的类型。
因此,它的算术运算与之(指针所指物的类型)密切相关。
inta[8];
int*p=a;
int*q=p+3;
p++;
指针的加减并不是指针本身的二进制表示加减,要记住,指针是一个元素的地址,它每加一次,就指向下一个元素。
所以:
int*q=p+3;//q指向从p开始的第三个整数。
p++;//p指向下一个整数。
double*pd;
……//某些计算之后
double*pother=pd–2;//pother指向从pd倒数第二个double数。
4.指针本身的大小。
在一个现代典型的32位机器上【注3】,机器的内存模型大概是这样的,想象一下,内存空间就像一个连续的房间群。
每一个房间的大小是一个字节(一般是二进制8位)。
有些东西大小是一个字节(比如char),一个房间就把它给安置了;但有些东西大小是几个字节(比如double就是8个字节,int就是4个字节,我说的是典型的32位),所以它就需要几个房间才能安置。
注3:
什么叫32位?
就是机器CPU一次处理的数据宽度是32位,机器的寄存器容量是32位,机器的数据,内存地址总线是32位。
当然还有一些细节,但大致就是这样。
16位,64位,128位可以以此类推。
这些房间都应该有编号(也就是地址),32位的机器内存地址空间当然也是32位,所以房间的每一个编号都用32位的二进制数来编码【注4】。
请记住指针也可以作为值使用,作为值的时候,它也必须被安置在房间中(存储在内存中),那么指向一个值的指针需要一个地址大小来存储,即32位,4个字节,4个房间来存储。
注4:
在我们平常用到的32位机器上,绝少有将32位真实内存地址空间全用完的(232=4G),即使是服务器也不例外。
现代的操作系统一般会实现32位的虚拟地址空间,这样可以方便运用程序的编制。
关于虚拟地址(线性地址)和真实地址的区别以及实现,可以参考《Linux源代码情景分析》的第二章存储管理,在互联网上关于这个主题的文章汗牛充栋,你也可以google一下。
但请注意,在C++中指向对象成员的指针(pointertomemberdataormemberfunction)的大小不一定是4个字节。
为此我专门编制了一些程序,发现在我的两个编译器(VC7.1.3088和Dev-C++4.9.7.0)上,指向对象成员的指针的大小没有定值,但都是4的倍数。
不同的编译器还有不同的值。
对于一般的普通类(class),指向对象成员的指针大小一般为4,但在引入多重虚拟继承以及虚拟函数的时候,指向对象成员的指针会增大,不论是指向成员数据,还是成员函数。
【注5】。
注5:
在AndreiAlexandrescu的《ModernC++Design》的5.13节Page124中提到,成员函数指针实际上是带标记的(tagged)unions,它们可以对付多重虚拟继承以及虚拟函数,书上说成员函数指针大小是16,但我的实践告诉我这个结果不对,而且具体编译器实现也不同。
一直很想看看GCC的源代码,但由于旁骛太多,而且心不静,本身难度也比较高(这个倒是不害怕^_^),只有留待以后了。
还有一点,对一个类的staticmember来说,指向它的指针只是普通的函数指针,不是pointertoclassmember,所以它的大小是4。
5.指针运算符&和*
它们是一对相反的操作,&取得一个东西的地址(也就是指针),*得到一个地址里放的东西。
这个东西可以是值(对象)、函数、数组、类成员(classmember)。
其实很简单,房间里面居住着一个人,&操作只能针对人,取得房间号码;
*操作只能针对房间,取得房间里的人。
参照指针本身的类型以及指针所指物的类型很好理解。
小结:
其实你只要真正理解了1,2,就相当于掌握了指针的牛鼻子。
后面的就不难了,指针的各种变化和C语言中其它普通类型的变化都差不多(比如各种转型)。
二.数组。
在C语言中,对于数组你只需要理解三件事。
1.C语言中有且只有一维数组。
所谓的n维数组只是一个称呼,一种方便的记法,都是使用一维数组来仿真的。
C语言中数组的元素可以是任何类型的东西,特别的是数组作为元素也可以。
所以inta[3][4][5]就应该这样理解:
a是一个拥有3个元素的数组,其中每个元素是一个拥有4个元素的数组,进一步其中每个元素是拥有5个整数元素的数组。
是不是很简单!
数组a的内存模型你应该很容易就想出来了,不是吗?
:
)
2.数组的元素个数,必须作为整数常量在编译阶段就求出来。
inti;
inta;//不合法,编译不会通过。
也许有人会奇怪charstr[]=“test”;没有指定元素个数为什么也能通过,因为编译器可以根据后面的初始化字符串在编译阶段求出来,
不信你试试这个:
inta[];
编译器无法推断,所以会判错说“arraysizemissingina”之类的信息。
不过在最新的C99标准中实现了变长数组【注6】
注6:
如果你是一个好奇心很强烈的人,就像我一样,那么可以查看C99标准6.7.5.2。
3.对于数组,可以获得数组第一个(即下标为0)元素的地址(也就是指针),从数组名获得。
比如inta[5];int*p=a;这里p就得到了数组元素a[0]的地址。
其余对于数组的各种操作,其实都是对于指针的相应操作。
比如a[3]其实就是*(a+3)的简单写法,由于*(a+3)==*(3+a),所以在某些程序的代码中你会看到类似3[a]的这种奇怪表达式,现在你知道了,它就是a[3]的别名。
还有一种奇怪的表达式类似a[-1],现在你也明白了,它就是*(a-1)【注7】。
注7:
你肯定是一个很负责任的人,而且也知道自己到底在干什么。
你难道不是吗?
:
)所以你一定也知道,做一件事是要付出成本的,当然也应该获得多于成本的回报。
我很喜欢经济学,经济学的一个基础就是做什么事情都是要花成本的,即使你什么事情也不做。
时间成本,金钱成本,机会成本,健康成本……可以这样说,经济学的根本目的就是用最小的成本获得最大的回报。
所以我们在自己的程序中最好避免这种邪恶的写法,不要让自己一时的智力过剩带来以后自己和他人长时间的痛苦。
用韦小宝的一句话来说:
“赔本的生意老子是不干的!
”
但是对邪恶的了解是非常必要的,这样当我们真正遇到邪恶的时候,可以免受它对心灵的困扰!
对于指向同一个数组不同元素的指针,它们可以做减法,比如int*p=q+i;p-q的结果就是这两个指针之间的元素个数。
i可以是负数。
但是请记住:
对指向不同的数组元素的指针,这样的做法是无用而且邪恶的!
对于所谓的n维数组,比如inta[2][3];你可以得到数组第一个元素的地址a和它的大小。
*(a+0)(也即a[0]或者*a)就是第一个元素,它又是一个数组int[3],继续取得它的第一个元素,*(*(a+0)+0)(也即a[0][0]或者*(*a)),也即第一个整数(第一行第一列的第一个整数)。
如果采用这种表达式,就非常的笨拙,所以a[0][0]记法上的简便就非常的有用了!
简单明了!
对于数组,你只能取用在数组有效范围内的元素和元素地址,不过最后一个元素的下一个元素的地址是个例外。
它可以被用来方便数组的各种计算,特别是比较运算。
但显然,它所指向的内容是不能拿来使用和改变的!
关于数组本身大概就这么多,下面简要说一下数组和指针的关系。
它们的关系非常暧昧,有时候可以交替使用。
比如intmain(intargs,char*argv[])中,其实参数列表中的char*argv[]就是char**argv的另一种写法。
因为在C语言中,一个数组是不能作为函数引数(argument)【注8】直接传递的。
因为那样非常的损失效率,而这点违背了C语言设计时的基本理念——作为一门高效的系统设计语言。
注8:
这里我没有使用函数实参这个大陆术语,而是运用了台湾术语,它们都是argument这个英文术语的翻译,但在很多地方中文的实参用的并不恰当,非常的勉强,而引数表示被引用的数,很形象,也很好理解。
很快你就可以像我一样适应引数而不是实参。
dereferance,也就是*运算符操作。
我也用的是提领,而不是解引用。
我认为你一定智勇双全:
既有宽容的智慧,也有面对新事物的勇气!
你不愿意承认吗?
:
)
所以在函数参数列表(parameterlist)中的数组形式的参数声明,只是为了方便程序员的阅读!
比如上面的char*argv[]就可以很容易的想到是对一个char*字符串数组进行操作,其实质是传递的char*字符串数组的首元素的地址(指针)。
其它的元素当然可以由这个指针的加法间接提领(dereferance)【参考注8】得到!
从而也就间接得到了整个数组。
但是数组和指针还是有区别的,比如在一个文件中有下面的定义:
charmyname[]=“wuaihua”;
而在另一个文件中有下列声明:
externchar*myname;
它们互相是并不认识的,尽管你的本义是这样希望的。
它们对内存空间的使用方式不同【注9】。
对于charmyname[]=“wuaihua”如下
myname
w
u
a
i
h
u
a
\0
对于char*myname;如下表
myname
\|/
w
u
a
i
h
u
a
\0
注9:
可以参考AndrewKonig的《C陷阱与缺陷》4.5节。
改变的方法就是使它们一致就可以了。
charmyname[]=“wuaihua”;
externcharmyname[];
或者
char*myname=“wuaihua”;//C++中最好换成constchar*myname=“wuaihua”。
externchar*myname;
C之诡谲(下)
三.类型的识别。
基本类型的识别非常简单:
inta;//a的类型是a
char*p;//p的类型是char*
……
那么请你看看下面几个:
int*(*a[5])(int,char*);//#1
void(*b[10])(void(*)());//#2
doube(*)()(*pa)[9];//#3
如果你是第一次看到这种类型声明的时候,我想肯定跟我的感觉一样,就如晴天霹雳,五雷轰顶,头昏目眩,一头张牙舞爪的狰狞怪兽扑面而来。
不要紧(Takeiteasy)!
我们慢慢来收拾这几个面目可憎的纸老虎!
1.C语言中函数声明和数组声明。
函数声明一般是这样intfun(int,double);对应函数指针(pointertofunction)的声明是这样:
int(*pf)(int,double),你必须习惯。
可以这样使用:
pf=&fun;//赋值(assignment)操作
(*pf)(5,8.9);//函数调用操作
也请注意,C语言本身提供了一种简写方式如下:
pf=fun;//赋值(assignment)操作
pf(5,8.9);//函数调用操作
不过我本人不是很喜欢这种简写,它对初学者带来了比较多的迷惑。
数组声明一般是这样inta[5];对于数组指针(pointertoarray)的声明是这样:
int(*pa)[5];你也必须习惯。
可以这样使用:
pa=&a;//赋值(assignment)操作
inti=(*pa)[2]//将a[2]赋值给i;
2.有了上面的基础,我们就可以对付开头的三只纸老虎了!
:
)
这个时候你需要复习一下各种运算符的优先顺序和结合顺序了,顺便找本书看看就够了。
#1:
int*(*a[5])(int,char*);
首先看到标识符名a,“[]”优先级大于“*”,a与“[5]”先结合。
所以a是一个数组,这个数组有5个元素,每一个元素都是一个指针,指针指向“(int,char*)”,对,指向一个函数,函数参数是“int,char*”,返回值是“int*”。
完毕,我们干掉了第一个纸老虎。
:
)
#2:
void(*b[10])(void(*)());
b是一个数组,这个数组有10个元素,每一个元素都是一个指针,指针指向一个函数,函数参数是“void(*)()”【注10】,返回值是“void”。
完毕!
注10:
这个参数又是一个指针,指向一个函数,函数参数为空,返回值是“void”。
#3.doube(*)()(*pa)[9];
pa是一个指针,指针指向一个数组,这个数组有9个元素,每一个元素都是“doube(*)()”【也即一个指针,指向一个函数,函数参数为空,返回值是“double”】。
现在是不是觉得要认识它们是易如反掌,工欲善其事,必先利其器!
我们对这种表达方式熟悉之后,就可以用“typedef”来简化这种类型声明。
#1:
int*(*a[5])(int,char*);
typedefint*(*PF)(int,char*);//PF是一个类型别名【注11】。
PFa[5];//跟int*(*a[5])(int,char*);的效果一样!
注11:
很多初学者只知道typedefchar*pchar;但是对于typedef的其它用法不太了解。
StephenBlaha对typedef用法做过一个总结:
“建立一个类型别名的方法很简单,在传统的变量声明表达式里用类型名替代变量名,然后把关键字typedef加在该语句的开头”。
可以参看《程序员》杂志2001.3期《C++高手技巧20招》。
#2:
void(*b[10])(void(*)());
typedefvoid(*pfv)();
typedefvoid(*pf_taking_pfv)(pfv);
pf_taking_pfvb[10];//跟void(*b[10])(void(*)());的效果一样!
#3.doube(*)()(*pa)[9];
typedefdouble(*PF)();
typedefPF(*PA)[9];
PApa;//跟doube(*)()(*pa)[9];的效果一样!
3.const和volatile在类型声明中的位置
在这里我只说const,volatile是一样的【注12】!
注12:
顾名思义,volatile修饰的量就是很容易变化,不稳定的量,它可能被其它线程,操作系统,硬件等等在未知的时间改变,所以它被存储在内存中,每次取用它的时候都只能在内存中去读取,它不能被编译器优化放在内部寄存器中。
类型声明中const用来修饰一个常量,我们一般这样使用:
const在前面
constint;//int是const
constchar*;//char是const
char*const;//*(指针)是const
constchar*const;//char和*都是const
对初学者,constchar*;和char*const;是容易混淆的。
这需要时间的历练让你习惯它。
上面的声明有一个对等的写法:
const在后面
intconst;//int是const
charconst*;//char是const
char*const;//*(指针)是const
charconst*const;//char和*都是const
第一次你可能不会习惯,但新事物如果是好的,我们为什么要拒绝它呢?
:
)const在后面有两个好处:
A.const所修饰的类型是正好在它前面的那一个。
如果这个好处还不能让你动心的话,那请看下一个!
B.我们很多时候会用到typedef的类型别名定义。
比如typedefchar*pchar,如果用const来修饰的话,当const在前面的时候,就是constpchar,你会以为它就是constchar*,但是你错了,它的真实含义是char*const。
是不是让你大吃一惊!
但如果你采用const在后面的写法,意义就怎么也不会变,不信你试试!
不过,在真实项目中的命名一致性更重要。
你应该在两种情况下都能适应,并能自如的转换,公司习惯,商业利润不论在什么时候都应该优先考虑!
不过在开始一个新项目的时候,你可以考虑优先使用const在后面的习惯用法。
四.参数可变的函数
C语言中有一种很奇怪的参数“…”,它主要用在引数(argument)个数不定的函数中,最常见的就是printf函数。
printf(“Enjoyyourselfeveryday!
\n”);
printf(“Thevalueis%d!
\n”,value);
……
你想过它是怎么实现的吗?
1.printf为什么叫printf?
不管是看什么,我总是一个喜欢刨根问底的人,对事物的源有一种特殊的癖好,一段典故,一个成语,一句行话,我最喜欢的就是找到它的来历,和当时的意境,一个外文翻译过来的术语,最低要求我会尽力去找到它原本的外文术语。
特别是一个字的命名来历,我一向是非常在意的,中国有句古话:
“名不正,则言不顺。
”printf中的f就是format的意思,即按格式打印【注13】。
注13:
其实还有很多函数,很多变量,很多命名在各种语言中都是非常讲究的,你如果细心观察追溯,一定有很多乐趣和满足,比如哈希表为什么叫hashtable而不叫hashlist?
在C++的SGISTL实现中有一个专门用于递增的函数iota(不是itoa),为什么叫这个奇怪的名字,你想过吗?
看文章我不喜欢意犹未尽,己所不欲,勿施于人,所以我把这两个答案告诉你:
(1)table与list做为表讲的区别:
table:
-------|--------------------|-------
item1|kadkglasgaldfgl|jkdsfh
-------|--------------------|-------
item2|kjdszhahlka|xcvz
-------|--------------------|-------
list:
****
***
*******
*****
That'sthedifference!
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 黑客 c语言 语言