1回顶部 注:这一部分涉及《箴言》第四章。 需要指出,同一个方向的goto会有很大的益处。 return success; abortPoint: 大家看看,这种用goto的代码是不是反而清晰的很呢?如果要用那些if/switch之类的,反而不美了,呵呵。 2回顶部 到了Pascal和C语言中,数据和代码之间的关系就模糊了。 ========================================= 呵呵,Pascal是我的第一门语言,C也用了这么多年了,怎么就没觉得它们数据和代码之间关系模糊啊?不理解。 有了重入功能,操作系统就很好设计了,就能实现多任务。 ========================================= 这种说法真的还是第一次听说。只要慎用全局变量,那么一个函数的可重入性还是比较容易做到的啊。操作系统的某些部分确实需要重入功能,但是单独拔高重入的重要性以前确实是没见过:-)另外,多任务实现的基础恐怕不是什么重入功能吧? C++是这样一种工作方式,即把数据和对其操作的代码进行捆绑。这样就可以和结构一样,进行内存的分配和使用。在C++中,函数内部用动态的可重入的变量,而C++又是工作在自己独立的数据区和代码区。 ========================================= C++的数据和相应的操作(方法)其实不在一个地方,至少VC生成的代码是这样,在下面我写了几个小程序,大家可以跑跑,自然就会得出自己的结论了。 另外,C++函数中内部用动态的可重入变量很正常啊,C也是这样啊。至于说C++又是工作在自己独立的数据区和代码区就让人困惑了,搞不清楚作者想要告诉我们些什么。 其实,一个C++ class如果只有public成员变量而没有方法的话,那么它和一个struct其实没什么不同,即使有了普通的方法,C++ class的内存表示还是没有变化,只不过编译器会另找一个地方存放方法代码本身。下面两个class: class A class B 3回顶部 如果你用sizeof()作用在这令各class上,就会发现,它们大小是一样的,B不会因为多了一个方法而导致其size变大。(当然,你如果将sayHello用virtual申明为虚方法,那么编译器会在你的类前面加一个所谓的VTable,其实就是一个32位指针,会导致你的class变大的,如果你用virtual修饰sayHello,那么class B就会比class A大4个bytes)另外,对于类里面的方法,只要在程序里定义了,那么在运行期不管你有没有生成这个类的实例,其实这些方法代码已经存在了,甚至我们可以用一些“脏”代码来调用: #include <iostream> class A int main() pa = NULL; //注意这里,并不会导致下面的语句出错,在vc和gcc里都pass return 0; 4回顶部 BTW,大家注意一下下面的这段代码: struct A { int a; int b; int c; }; &(((struct A *)NULL)->b)); //这里是取b在结构里的偏移,一些OS kernel代码会实现一些通用数据结构,比如单向链表,就会利用这种技术。具体的可以看DDK,或者在linux kernel代码里用list搜索一下,这些实现蛮巧妙的。 #include <stdio.h> class A typedef void (A::*PFN)(); //不清楚的同志去看《Thinking in C++》 int main() pFn = &A::sayHello; return 0; 5回顶部 OK,编译运行之,至少在VC环境里会出现“A::sayHello()”。 之所以要用sprintf和atoi是因为我不知道怎么把PFN转化程P_SAYHELLO,只好用这种“不干净”的代码了,呵呵:-) 因为定义C++的类时,代码和数据都浮动。然而代码浮动是没有意义的。 ========================================= 不懂是什么意思。至于浮动代码没意义就不对了,编译出来的程序不会知道自己会被加载到虚拟空间的什么地方,于是程序里都是一些相对于程序起始地址的偏移量,一些loaders甚至在加载的时候动态修改这些代码,完成最后的定位工作。大家去看看John R. Levine的《Linkers & Loaders》就什么都明白了。 对于同一个进程,代码当然只能有一份,因为代码不会自己改变自己的。所以,在C++中加了不能取类函数地址的限制。 ========================================= 上面给出的代码说明了可以取函数地址。至于上面这句话其中的因果关系,我还是没有能导出来。代码不会自己改变自己吗?决大多数情况下是这样,然而,一些特殊的程序,比如病毒,就完全有可能自己修改自己。其实从CPU的角度看,只要你在ring0把代码的属性改为可写,那么你想怎么改都可以:-) C++的设计理念是数据和代码都是浮动的,这样可以把整个对象传给一个进程活是当成一个数据类型,把这个对象从这个进程传入到另一个进程中去用。在这基础上,发展出分布对象。 把对象放入另一个计算机中去进行运算,运算完成后,只需要返回运算的结果。这种理论的模型到现在还不可能都做到,就是操作系统也做不到这一点。 6回顶部 当时在设计C++的时候就考虑到了这一点,但事实上这种模型太理想化了,在实践中不可能完全实现的。 ========================================= 老实说从我学C++的第一天一直到现在,我实在一点都看不出标准C++怎么来支持对象传递和分布式对象。我觉得对象传递和分布式对象怎么看都象是一个软件平台应该提供的功能,硬要让C++来承担不怕C++负担太重啊,呵呵:-) 后来在Pascal的基础上产生了C语言。 ========================================= C语言实在Pascal的基础上产生的吗?虽说Pascal比C早两年,然而直接导致C诞生的B语言好像还比Pascal还早吧?C应该脱胎于贝尔实验室的B语言,而这些语言都和Algol 60关系十分密切。 正是现在所有的编程语言都以人为中心,以人的思维为出发点,所以编写程序就会跟机器的实现相脱离。这就导致很多人看上去都会编程序,但是编写出的程序出现这样、那样的问题,或者效率极低。而这样的程序员又不能从根本上解决这样的问题。因为他对机器和编程语言的关系和原理不了解。 程序的入口和出口 7回顶部 汇编语言中一部分是代码,另一部分是数据,在汇编内是很清楚的。 ========================================= 我觉得恰恰是在汇编代码里数据和代码是分不清楚的,因为这完全取决于你让CPU怎么解释你生成的这些0、1串。其实大家都明白,在汇编代码里用db伪操作来将一些数字定义为代码的例子实在是太多了。 《箴言》一书中在4.1.1中最后的部分讨论了一点点程序链接的信息。 编程时一定要有一个好习惯,即数据和代码放到不同的地方,而不要在数据中插入一些变量的申明。只有这样,才能让编译器非常容易的处理数据......这样因为编译的原因产生的错误就少了。 其实在Windows内,TextOut有两个函数相对应。 【实例】:自定义程序的入口点 8回顶部 如果将C与C++相比较,C++就会为了某个问题会绕一大圈,所以代码会比较大,并且里面有一些没用的代码。 ========================================= 可惜《箴言》没有给个例子说明C++是怎么绕一个大圈子了。C++的对象机制应该是对问题的一种更高级别的抽象。作为一个一般的规律,计算机里面的每一次向上的抽象都会使的对人的界面友好一点,而这是要付出空间和时间的代价的。C++的对象显然从语义上要比C的结构丰富的多,因此C的结构可以不需要那些诸如构造函数,析构函数之类的东西,而C++在很多时候就需要它们了。如果以没用到就说它们没用,我只能表示遗憾了。这只能说明你还是用C的眼光看C++,而不是用C++的眼光看C++。 比如用结构的指针的处理写出来的C代码就会很复杂,因为里面有很多结构的指针,指来指去。 C++主要解决的是一个重入的问题,重入也是对象化的问题... ...早期的操作系统就是用结构来做的,否则就没有办法解决文件的问题。 然后在代码里真正链接的时候,只包含DATA和TEXT区,而BSS区域是程序装进来的时候给它分配空间的。 9回顶部 程序二: #include <stdio.h> long array[10 *1024 * 1024] = { 0xcccccccc }; int main() { printf("Hello World!\n"); } 所以,从这里看出为什么要了解平台。任何的程序编译出来都和平台有关。如果脱离平台,任何语言都没有什么意义了。 局部变量完全在堆栈里面去实现。 10回顶部 自动的意思就是自动的分配和清除,并且初始的值也是随机的。 ========================================= 在VC Debug版本里,栈中分配的值都会先用0xCCCCCCCC来处理一下,所以大家在Debug模式下调试程序发现在引用0xCCCCCCCC这样的值,就说明在试图使用一个没有初始化的值。这就是在Debug模式下调试的好处之一,如果在Release模式下,系统就不会用0xCCCCCCCC来处理一下了。至于为什么选择0xCCCCCCCC大概是因为 端点中断int 3 对应的机器码就是0xCC吧,我也不是很有把握。 用固定的地址是可以访问指针所指向的数据的。但是在一般情况下,Windows可能会报非法操作。 但是在函数的调用过程中,引用和指针又不一样,引用往往会在编译器的代码里面,加上一个自动搬移的过程,也就是把那个值搬过来。 #include <iostream> struct A void handle_1(struct A *a) 11回顶部 void handle_2(struct A a) { a.a = 0x1234; a.b = 0x4321; } void handle_3(struct A& a) int main(int argc, char* argv[]) return 0; 12回顶部 编译中,事实上引入了很多不可预测的因素。编译器是帮你把你的想法变成机器可以运行的代码,即把你的思想变成在内存中的相应映射。如果你真正了解了这些,编译器也就不是很重要了... ...很多做程序的人并不知道平台的作用,其实平台才是最重要的。有些人认为自己懂了VC,就懂了计算机了,实际上离计算机还有十万八千里。 ========================================= 呵呵,只要你对语言了解,那么从原理上看你就应该知道你的代码的具体含义,除非编译器有bug。老实说,在windows上的这些开发语言中,VC的门槛还是相对较高的,我想只要是真正懂VC的人,恐怕都不会的认为自己就懂计算机了吧。别说计算机这么个大范畴了,你就算精通VC了又怎么样,windows源码没有给你,你还是有无数个问号!懂计算机?谈何容易啊。 如果把类剖析出来会发现有一堆Vtable的指针,这个指针再jmp到一个函数的地址。 如果你用到类里面的函数时,并且不是静态分配,而都是动态分配,当程序做了很多内存的操作后,如果某个地方出了问题,就会出现call地址错误。 13回顶部 老一点的编译器在写struct时,可以把成员函数和变量放在一起使用,结构和类是一样的。但是,这个结构的属性相当于全部是public。 ========================================= MSDN里类似这种初始化COM的代码 struct XXX_OLE_INIT 总是可以在VC的各个版本里Pass的,此时的struct XXX_OLE_INIT就相当于一个全部成员都public的class。 当编译器调用时,就会产生一个相应的CALL ?add@@YAHHH@Z或CALL ?add@@YAMMMM@Z。 class class_abc class_abc _fuc((void *)&abc, a, b, c); 14回顶部 COM的继承都是分级指针的,因为COM没有给你提供源代码。 ========================================= 个人认为COM里的继承应该和C++的多重继承有可比性。不知道所谓的分级指针是什么意思。再说COM只是个标准,而不是实现,有点象Java里面的什么J2EE什么的。规范是不含实现的,当然参考实现出外。网上就有第三方的COM实现,在UNIX平台上。对于用户而言,一个COM服务给你个接口就行了,你用它就可以直接调用服务了,不需要知道具体的某个COM服务是如何实现的。(当然,知道了更好:-) 一个全局变量在A文件中,在B文件中用extern把这个变量引入,如果全局变量是由两个人写的,且名字都相同,两个程序都要LINK成一个EXE,这样就会出现问题。 #endif 以前有些书为了避免这些问题的发生,就说不要用全局变量,这些会导致程序互相冲突。 在程序中,用链一般会带来很多不稳定的因素。...... 程序的可读性会变差。 15回顶部 解释的运行方式中最常见的式Basic... ...其实,解释程序就是一个字符串的解释器。 ========================================= 很多脚本语言,比如大名鼎鼎的Perl,Python等等都是解释执行的。这些脚本语言的语义十分丰富,像Perl,Python都支持面向对象的编程,因此,这些脚本的解释程序本身十分复杂,编译原理的各个方面几乎都会涉及到,远远不是什么字符串的解释器可以比拟的。 许多书中没有说为什么参数从右向左传递... ...从而便于汇编和调试。 下面摘自VC提供的代码,va_xxx是可变参数编程的几个重要的宏。 #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) 我们可以看到,所谓可变,其实就是利用va_arg每次从栈中取出适当的字节数拼成所需要的参数。如果参数从右往左入栈,那么利用ebp可以顺利的依序取出第一个,第二个...第n个参数,否则,就没法用直接的简单的方法取到第一个参数了,原因是利用ebp只能直接定位到最后一个入栈的参数,而函数参数的个数未知,因此第一个参数在栈中的位置就不太容易定位了。当然,这不是绝对的,用些辅助的方法也可以做到,但是就不如让参数从右向左进栈实现起来轻松简单了。 |
正在阅读:《编程高手箴言》读后(4)《编程高手箴言》读后(4)
2004-04-08 14:39
出处:CSDN
责任编辑:sdq