这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » MCU » 如何写好 C\C++ 程序

共1条 1/1 1 跳转至

如何写好 C\C++ 程序

院士
2002-10-25 22:09:36     打赏
1. 基本认识 1.1. 计算机科学是"人为"的科学 计算机科学是一门新兴的科学,它不同于物理、化学、天文等自然科学,它是人为创造的,它的研究对象也是人为创造的,是一门人为科学。自然科学的研究对象是客观世界,是对未知的探索;发现自然规律,研究并利用自然规律是其根本目的。作为工程师,要在给定条件下"做得更好",就是说要根据实际情况利用现有条件充分发挥"人为"的优越性。 1.2. 软件经常是理论落后于实践 众所周知,任何一门科学,都有它的理论,都应做到理论与实践相结合,软件理所当然应该这样,然而,实际情况并非如此。软件是工程,软件理论落后于实践的情况比比皆是。比如说,几乎没有人将图灵机理论应用于实际软件开发,计算机更多的是工程,工程是靠实践的,工程师要始终坚持"身体力行",不要忽视写程序,要从实际应用中去领悟软件的真谛。 1.3. 编程语言不是目的 首先,语言是工具,编程语言同样是工具;其次,语言不是目的,学习语言的目的就是为了更好地表达人的意思,而不是为了学习而学习。因此,对于工程师来说,不应把学习几门编程语言作为最终目的,而是要多快好省的"使用工具"实现自己的目的,开发出优秀的软件。 1.4. 冯·诺伊曼模型至今颠扑不破 存储程序理论至今已有五十年历史,冯·诺伊曼模型今后仍是计算机的基本模型,非冯模型不是不可能,但却越来越遥远。因此,工程师要恪守"规矩成方圆"的准则,好的工程师要会用工具,要会用"尺"来衡量,这里的"尺"就是冯·诺伊曼机的内存,做工程时一定要用"尺"量,尽量减小误差,编写程序不能违背计算机的基本模型,掌握语言,要做到知其然更知其所以然,利用语言操作计算机硬件,使其有效工作,才是最终目的。 2. C/C++编程中应该特别注意的一些问题 下面通过变量、栈、堆在设计中如何使用,来讨论如何写好C/C++程序(所有例题答案附在全文后) 2.1. 全程、局部、动态变量 例1:请回答下面程序中注释行中提出的问题 int a; // 内存中占多少字节?分配在哪一区? int b = 1; // b 与 a 是否相连?什么是数据区? void main() { int *p; // 分配在哪一区? p = (int *)malloc(100); // 100 字节在哪? … } 2.2. 内存空间 不清楚内存就写不了好程序,因此对内存的理解,对工程师来说至关重要。(对于本公司的工程师,搞不清楚内存,程序不可以check in) 下图是内存空间的简单逻辑结构示意图,低(零)地址在下面,高地址在上面。 · 用户禁区:是从零地址开始的一段空间,严禁用户使用,保护系统空间; · 程序代码区:用来存储当前运行程序的源代码; · 全程数据区:存储全程变量,大小不受限制; · 堆:堆是内存中可以动态分配的一片空间,用于存放程序运行过程中动态产生的变量,它是从低地址向高地址生长的,可以是链式结构,原则上讲只要内存中有剩余空间,堆就可以动态增长下去; · 栈:是从高地址向低地址方向生长的存储区,它的实现有特殊性,是后进先出的,并且它的大小是受限的,使用中具有灵活,速度快,不需要人工释放,不存在竞争冒险问题等优点; · 动态连接库代码;动态连接库数据;每个动态连接库又有它的堆、栈。 例2:一个关于指针入栈的问题。 void foo(char * p) {p = "世界";} void main() { char *p = "你好"; foo(p); printf("%s\n", p); // 输出什么? } 此题要求察看反汇编代码,建议使用Microsoft的compiler。并且要求了解编译后产生的所有文件,至少知道它们都是做什么的,有什么用途。 例3:请回答下面程序中注释行中提出的问题。 int a[10000000]; // 是否出错? void main() { int a[10000000]; // 是否出错? int *a = (int *) malloc(10000000); // 是否出错? *a = 1; printf("%d\n", *a); } 例4:关于堆使用的问题。 void main() { char *p = (char *) malloc(10000000); char *q = new char[10000000]; …… free(p); // 释放多少内存? delete q; // 释放多少内存? } 2.3. C/C++ Calling Conventions(调用习惯) 调用习惯决定编译程序编译源文件时,如何处理函数调用时传递参数的压栈次序,由谁(调用者还是被调用者)负责弹栈等。(参见MSDN) · __cdecl Caller pops stack (C) · __stdcall Callee pops stack (C++) · __fastcall ECX passes this pointer (优化C) __cdecl 是C\C++函数调用的默认形式,由调用函数清除栈,生成的可执行代码包含清除栈的部分,因此,对于同样的函数,它比__stdcall形式调用的可执行代码长。 __stdcall通常用于Win32 API函数调用,由被调用函数清除栈,采用这种方式调用,要有函数原型。 __fastcall用于优化的C,只要可能,就使用寄存器,速度快。 3. 程序运行基本概念 3.1. 主程序/子程序(EXE) 二进制文件,可以在机器上直接运行。 3.2. 程序库/模版库(LIB) 对于一些常用的函数,如printf、strcpy等,把他们编成库函数,由使用者调用,减少重复劳动和出错的可能,但编译后代码长度并没有变小。 3.3. 动态链接库(DLL) 当多个进程都需要调用某个函数时,为了节省内存空间把这些函数编成动态连接库,由多个进程动态共享。 在选择使用LIB还是DLL时,要考虑应用中具体情况,比如说多少进程共享一个DLL合适,效率如何等等,更具实际做出权衡。另外,DLL也有其缺点,例如不同版本DLL的兼容性不可能做到完美。 4. 面向对象程序设计 在面向对象的程序设计中,我们常涉及到的是封装、继承、多态、类、映射等特性。 4.1. 向谁最重要 面向对象,面向谁最为重要。这个问题可以从三个方面来考虑。首先,也是最重要的,要面向使用者,因为软件的最终目的是满足实际应用;其次,要面向开发者,开发者可能会使用这个对象去构造更大的对象,实现更多的功能;最后,要面向机器,对象最终必须能在冯·诺伊曼机器上运行,设计中必须考虑运行效率、软件模型、实际误差等问题。 4.2. 封装 (Encapsulation) C++中有public、protected、private关键字,这样就可以叫封装了。C++中封装是对程序员的限制,没有实现真正意义上的封装。 下面的例子是两种类的声明。 class A { class B { public:   public: virtual foo();    foo();    virtual bar(); protected:       virtual foobar(); bar(); } private: foobar(); int a; } class B的虚表(vtable)的物理存储结构如下图: 如果将上例class A中protected改为public,比较前后两种情况下编译之后的结果,就会发现它们是一样的,也就是说在源文件中的不同之处编译后在内存中是没有反映的,既然没有反映,就谈不上真正的区别,所以,这里的所谓封装只是对程序员自身的一种约束,没有真正的意义。 4.3. 继承(Inheritance) 继承是指能够直接获得已有的性质和特征,而不必重复定义。是子类自动的共享基类中定义的数据和方法的机制。 C++ Base Class的程序复用(编译时的静态复用) _继承的实质就是少写代码,节省打字时间,减少出错可能,它只是程序复用,是一种静态机制。因为,代码是自己写的还是从别的类中继承的,反映在内存中是一样的,没有区别,所以,继承只是在工程上有意义,更多是面向开发者,工程师可以利用这个方法提高开发效率,对使用者而言,则毫无意义。 COM Aggregation(聚合) 组件复用(运行时的动态复用)、相对灵活构造。(参见COM编程技术) 4.4. 多态(Polymorphism) 多态一词来源于希腊语,简单说就是有许多形态,它增加了面向对象软件系统的灵活性,C++中使用虚函数实现,是C++中最重要的概念。例5中class B的函数前加上virtual关键字之后,在这个数据结构中前4个字节保留下来,形成运算表(虚函数表vtbl),运算表中存放函数的入口地址,通过指针访问,形成间址结构,如下图所示。只要通过指针访问这个表(通过这层间址),调用不同的函数,从前的很多程序问题都可迎刃而解。 UNIX 的驱动程序模型就是用的这种结构,取得了巨大的成功。如: _open(); close(); read(); write(); ioctl(); 4.5. 类(Class) 类厂(ClassFactory) 类是对具有相同属性和行为的一个或多个对象的描述,它是建立对象时使用的"样板"。C++中类的概念是假的,它的类在编译之后,到内存中没有实体与其对应,因此C++不存在真正的类的概念。而smalltalk中类是一种特殊的对象,对象是一种特殊的类,对象这种数据结构是占内存的,所以类也是占内存的,这样才是真正的类。 类的概念有多重要性是: _第一,对象要有生死的概念(lifetime),对象从哪里来,由类来控制,Microsoft则叫它类厂,强调产生类、产生对象这个概念。 第二,类引出映射的概念。例如整数集合上的加、减、乘、除这四个运算,它们是跟整数类相对应的,按照冯·诺伊曼的存储程序理论,程序就是数据,所以它们既是运算也是数据,作为数据应该放在整数类中,放在类中有什么用呢?这里引出映射的概念,来解答这个问题。 4.6. 映射(Reflection) 下面 通过对加法运算的两种不同情况阐述映射的概念。 · 普通的32位加法,如果溢出了,怎么办?可不可以做64位加呢?既然是数,就能改,可以在运行过程中(动态)把一个32位数用一个PUSH语句入栈,再把另一个也压入栈中,这样32位加法运算转变为64位加法运算,问题解决了。同样的加、减、乘、除这四个运算,可以映射出不同的运算实现来,32位运算,64位运算是两种不同的实现,这种不同的实现就是对同一个描述的不同映射。 · 网络上的运算如何实现?同样还是加、减、乘、除运算,在网上,计算可以在本地进行,也可以在远程进行。比如说会做加法的进程在远程,这时本地进程将运算数据打包发给远程进程,远程进程计算后将结果打包送回。本地和远程,这是两种不同的实现方式,对于设计者来说,本地计算方式是用一段代码进行计算,远程计算方式是用一段代码与远程进行通信,但对于用户来说都是加法,除了时间上可能有差别外,其它完全一样,这是完全透明的网络运算。这就是映射。 C++中不存在真正的类,更不存在映射的概念。然而,类和映射的概念至关重要,要提到一个新的高度来认识。C++源程序编译后的目标代码是 .obj,而JAVA源程序编译后目标代码是 .class,JAVA想说明运算可以在运行中改,只有有了这个前提才能真正做到完全透明的网络运算。认识到这一点,举一反三,JAVA还能把32位加法动态提升为64位加法等等。如果把这种思想应用于软件设计,那么是不是能做到他人无法做到的事情呢? 映射(Reflection)这个概念怎么强调也不过分,它是程序设计的一个里程碑,从结构化设计到面向对象,有了多态这个概念,程序设计灵活了,现在又有了映射,程序设计更加灵活,人们走入了一个新境界。 5. C++、COM、COM+程序设计的比较 5.1. C++ 程序模型 C++是面向对象编程的,它的模块是静态的,链接后不可分割,这是C++的最主要的缺点,它不能动态升级。 模块实现 C++将运算与数据结合起来,放在类中,通过继承实现程序重用,采用二进制标准,将不同模块联系起来实现不同功能。 C++最关键的技术要点就是它加了一个vtbl(如下图),它带来很多好处,这在前面已经介绍过了,与原来的程序设计语言相比已经是一个跨越式的发展。 这里区分两个概念,语言和思想。C++是一种语言,是一种非常好的语言。它表达设计者的思想最容易、最直观,但它或多或少的带有设计者的历史局限性,语言虽然还是好语言,但如今思想的局限性却越来越明显。 5.2. COM 1993年6月,Microsoft发布了COM标准,主要是认识到了C++的不足。另外,并不是只有Microsoft这样认为,包括IBM发明Smalltalk、Digital发明CORBA、SUN发明JAVA,也都是认识到了这一点。在美国,所有大软件公司的大的工程项目很少直接采用C++的编程思想,而是采用它们自己的编程思想,实现软件设计。 COM解决了C++做不到的不同来源的组件之间的互操作,使某个组件升级时不影响其他组件,并且独立于编程语言,实现了组件在进程内、跨进程甚至于跨网络运行时对用户的透明性。 程序模型 COM解决了C++做不到的不同来源的组件之间的互操作,使某个组件升级时不影响其他组件,并且独立于编程语言,实现了组件在进程内、跨进程甚至于跨网络运行时对用户的透明性。 模块实现 在C++模型基础上增加了Interface ptr,目的就是要实现动态升级。举例说明这个问题,例如下图中已经有了两个域,如果需要增加域怎么办?前面讲过,栈有很多好处,要充分利用它,但是,如果一个数据结构放在栈上,这块空间一旦分配,它是不可以更改的,这个模块也就不可升级了,所以任何语言(JAVA、COM、C#等),它的组件、构件不可以生成在栈上,这是一个基本原则;然而,"指针,放之四海而皆准",可以在栈上放一指针(Interface ptr)来解决这个问题,(这个指针在JAVA中不是一个物理指针,是一个虚的,叫handle,相当于一个标识符,能访问到就可以了),理论上讲,这是增加一层间址,解决了组件动态可替换问题。 注册数据库 C++语言中使用new在堆上动态分配内存。例如,用new操作动态生成一个class,new是知道它所要分配的内存的大小的,既然知道大小,就没有办法升级。这个信息new是不该知道,这也正是C++当初违背封装原则的一个体现;即使new不知道分配空间的大小,也要有一套协议,来查询谁来支持这个class,谁来完成new操作。要想使模块可以升级,就不能再用new。 在COM中,使用CoCreateInstance(),本公司的ezCOM使用NEW_COMPONENT,实际上是重载了new。为了在"new"的时候,知道class现在的大小、在什么位置、由谁支持,需要在运行过程中创建一个数据库,通过间址的方式查询这个数据库,获得关于class的信息,之后才能创建一个class,只有这样才能实现动态升级。创建class时要用一层间址,调用时同样用一层间址,这两方面构成了COM的基本内涵。 元数据库(TypeLib) COM,93年出现的时候,它已经提出了元数据的概念,但当时没有给予强调,随着Internet时代的到来,94、95年出现了browsor,SUN发明了JAVA,这两件事大大加速了人们对软件的理解。元数据已经应用在COM、JAVA和脚本语言中,但COM并没有将元数据提高到一个无所不能的高度来理解,JAVA的设计者则不同,他把元数据提高到一个高层次,通过 .class便可一目了然。如何写程序进入了崭新的阶段。 元数据(Metadata)是定义存储在数据库中数据的形式的数据,可认为是关于数据的数据。元数据是对运算的描述,比如对整数集上的加、减、乘、除这四个运算的描述就称为整数集合(CLASS)上的Class Information,既为Metadata。CDL文件对ezCOM来说就是元数据,TLB是CDL的二进制表现,二者表达的信息是一样的。 构件(Component) = 对象(Object) + 元数据(Metadata),构件是由两部分组成的,一部分是对象,一部分是元数据,两者打包在一起构成一个dll文件的构件。构件在物理上与对象是不同的,强调一点,如果在物理上不同(内存中反映不同),在现实中就会有不同的体现。正是因为这个原因,本公司的ezCOM与C++截然不同,如果认为ezCOM与C++完全一样,或者就是用C++来编写的,那就完全错了。 程序应该用零件来构造,强调元数据的重要性,这是新的程序设计理念。 5.3. 关于自动化/自行化的概述 脚本语言 脚本语言(script),英文愿意是手迹、手稿、副本的意思。脚本语言是解释执行的,就相当于舞台上话剧演员按着剧本的内容演出一样,要一句一句的来。 模型/显示/控制 (MVC)编程方式 本公司的图形系统就是通过自动化来实现的,Model View Controller这个概念是smalltalk提出的,Windows、X-Windows都没有按照面向对象、按照这套思路来做,JAVA、.NET都是按照这一套思路来做的,本公司也这样做。 代理组件自动生成 自动远程通讯主要通过自动化,通过元数据实现。(参见…) 自描述数据结构 虽然本公司的产品开发使用C++,但是在所有的接口函数上,参数都必须使用自描述数据结构,这也是对C++的限制,目的就是要动态生成中间件,通过映射动态生成不同的程序实现。能不能实现动态替换,很大程度上是通过元数据和自描述数据结构来完成的。 本公司的自描述数据结构如下(参见基础数据类型文档): INT,LONG,CHAR,etc. EzStr,EzByteBuf,etc. EzIntArray,EzStrArray,etc. EzVariant,EzDelegate,etc. 5.4. COM+ a. COM+ 程序模型 代理(虚拟)组件 COM+比COM更加重视元数据,有了元数据之后,达到的新境界,就是操作系统可以动态生成代理组件(由系统生成的组件就是中间件)。 在用户程序与组件模块之间插入代理组件带来很多好处。Windows2000的COM+就是在强调这一点。代理组件,"薄"的时候可以什么都没有,就是一层间址,速度不会受损。它的灵活性是指可以动态替换零件,完成不同的功能,比如可以把一个图形软件放入内核或放到其它机器上等等,对用户没有影响;再如使用ORACLE时,假如有100个用户,可是只买了10个版权,这时可以通过代理组件来给每个用户分组件,实现动态共享;还比如说,在用户通讯的过程中,可以通过代理组件中零件的替换实现呼叫转移,接入Internet时,可以通过不同的零件加密、监控等等,这些都是加上代理组件的优越性。 COM+ 模块实现 在COM的基础上,再增加一层间址,定义运行环境 (Context),变为多层间址。工程师只要理解内存,学会用"尺"后,就会发现这种多重间址的妙处,程序设计就会变得非常灵活。 b. 组件运行环境对用户透明 (详细请参见其他相关文档) 6. COM技术要点及编程(详细请参见COM编程相关书籍) 1、 面向接口,可改变程序实现 2、 二进制标准 (无虚拟机) 3、 计数器控制生命周期 4、 动态模块构造 5、 元数据支持解释程序 6、 自描述数据结构 7、 自动远程通讯 7. 例题解答 例1: int a; // 占4个字节,在全程数据区;在_BSS段,可参考cod文件 int b=1; // 占4个字节,在全程数据区;与 a 不相连,在_DATA段 void main() { int * p; // 分配在栈上 p = (int *) malloc (100); // 100 字节在堆上 …… } 例2:你好 例3: int a[10000000]; // 不出错 void main() { int a[10000000]; // 出错,栈的大小有限制,但可通过编译器开关修改栈的大小 int *a = (int *) malloc(10000000); // 在堆上只要硬件上有足够的内存,是不出错的,(堆可动态增长) *a = 1; printf("%d\n", *a); } 例4:(堆) void main() { char *p = (char *) malloc(10000000); char *q = new char[10000000]; …… free(p); // 释放10000000字节的内存,malloc在堆上分配10000000字节的内 // 存,不会自动释放,需要用free来释放 //delete q; // 释放10000000字节的内存,new 在堆上分配10000000字节的内存 }



关键词: 如何     写好     程序     对象     软件     语言     就是     使用         

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]