共1条
1/1 1 跳转至页
如何写好 C\C++ 程序
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 跳转至页
回复
有奖活动 | |
---|---|
【有奖活动】分享技术经验,兑换京东卡 | |
话不多说,快进群! | |
请大声喊出:我要开发板! | |
【有奖活动】EEPW网站征稿正在进行时,欢迎踊跃投稿啦 | |
奖!发布技术笔记,技术评测贴换取您心仪的礼品 | |
打赏了!打赏了!打赏了! |
打赏帖 | |
---|---|
【笔记】生成报错synthdesignERROR被打赏50分 | |
【STM32H7S78-DK评测】LTDC+DMA2D驱动RGBLCD屏幕被打赏50分 | |
【STM32H7S78-DK评测】Coremark基准测试被打赏50分 | |
【STM32H7S78-DK评测】浮点数计算性能测试被打赏50分 | |
【STM32H7S78-DK评测】Execute in place(XIP)模式学习笔记被打赏50分 | |
每周了解几个硬件知识+buckboost电路(五)被打赏10分 | |
【换取逻辑分析仪】RA8 PMU 模块功能寄存器功能说明被打赏20分 | |
野火启明6M5适配SPI被打赏20分 | |
NUCLEO-U083RC学习历程2-串口输出测试被打赏20分 | |
【笔记】STM32CUBEIDE的Noruletomaketarget编译问题被打赏50分 |