|
为16、32、64位架构编写可移植代码
与16位相比,32位意味着程序更快、可直接寻址访问更多的内存和更好的处理器架构。鉴于此,越来越多的程序员已经开始考虑利用64位处理器所带来的巨大优势了。 克雷研究(Cray Research 应为品牌名)计算机已经开始使用64位字,并可访问更大的内存地址。然而,作为正在向开发标准软件和与其他操作系统相互努力的一部分,我们已经停止了移植那些原本基于32位处理器的代码。事实上,我们不断遇到我们称之为“32位主义”的代码,这些代码都是在假定机器字长为32位的情况下编写的,因而很难移植这种代码,所以必须确立一些简单的指导方针,以便助于编写跨16、32、64位处理器平台的代码。 由于有一些遗留问题, C语言在数据类型和数据构造方面,显得有点过剩了。可以使用的不仅仅是char、short、int和long类型,还有相应的unsigned(无符号)类型,当然你可在结构(structure)和联合(union)中混合使用,可以在联合中包含结构,再在结构中包含联合,如果还嫌数据类型不够复杂,还可以转换成比特位,当然也可以把一种数据 类型转换成另一种你想要的数据类型。正是因为这些工具太强大,如果不安全地使用它们,就有可能会伤到自己了。 高级代码的高级结构 在Kernighan和Plauger经典的《The Elements of Programming Style》一书中,他们的建议是“选择使程序看上去简单的数据表示法”。对个人而言,这意味着为高级编程使用高级 数据结构,而低级编程使用低级数据结构。 在我们移植的程序中,有一个常见的32位主义bug,不如拿它来做个例子,科内尔大学编写的用于网间互访的路由协议引擎,在伯克利网络环境下(指TCP/IP),自然是使用inet_addr( )来把表示Internet地址的 字符串转换成二进制形式。Internet地址碰巧也是32位的,就如运行伯克利网络系统的其他计算机一样,都是有着同样的字宽。 但也存在着有关Internet地址的高级定义:in_ addr结构。这个结构定义包括了子域s_ addr,它是一个包含了Internet地址的unsigned long标量。inet_addr()接受一个指向字符的指针,并且返回一个unsigned long,在转换地址字符串过程中如果发生错误,inet_addr将返回-1。 程序Gated读取以文本格式存放Internet地址的配置文件,并把它们放入sockaddr_in(这是一个包含了结构in_addr的高级结构)。例1(a)中的代码可以在32位电脑上正常运行,但移植到克雷研究计算机上,却无法运行,为什么呢? 例1:高级代码的高级结构 (a) struct sockaddr_in saddrin char *str;
if ((saddrin.sin_addr.s_addr = inet_addr(str)) == (unsigned long)-1) { do_some_error_handling; } |
(b) struct sockaddr_in saddrin char *str;
if (inet_aton(str, &saddrin.sin_addr) ! = OK) { do_some_error_handling; } |
因为只要inet_addr能够正确地解析字符串,那么一切OK。当inet_addr在64位计算机上返回一个错误时,这段代码却未能捕捉到。你必须要考虑比较语句中的数据位宽,来确定到底是哪出了错。 首先,inet_addr返回错误值——unsigned long -1,在64位中表示为比特位全为1,这个值被存储在结构in_addr下的子域s_addr中,而in_addr必须是32位来匹配Internet地址,所以它是一个32比特位的unsigned int(在我们的编译器上,int是64位)。现在我们存储进32个1,存储进的值将与unsigned long -1比较。当我们存储32个1于unsigned int时,编译器自动把32位提升为64位;这样,数值0x00000000 ffffffff与0xffffffff ffffffff的比较当然就失败了。这是一个很难发现的bug,特别是在这种因为32位到64位的隐式提升上。 那我们对这个bug怎么办呢?一个解决方法是在语句中比较0xffffffff,而不是-1,但这又会使代码更加依赖于特定大小的对象。另一个方法是,使用一个中间的unsigned long变量,从而在把结果存入sockaddr_in前,执行比较,但这会让程序代码更复杂。 真正的问题所在是,我们期望一个unsigned long值与一个32位量(如Internet地址)相等。Internet地址必须以32位形式进行存储,但有些时候用一个标量,来访问这个地址的一部分,是非常方便的。在32位字长的电脑中,用一个long数值(常被当作32位)来访问这个地址,看上去没什么问题。让我们暂时不想一个低级的数据项(32位Internet地址)是否与一个机器字相等,那么高级数据类型结构in_addr就应该被一直使用。因为in_addr中没有无效值,那么应有一个单独的状态用作返回值。 解决方案是定义一个新的函数,就像inet_addr那样,但返回一个状态值,而且接受一个结构in_addr作为参数,参见例1(b)。因为高级的数据元素可以一直使用,而返回的值是没有溢出的,所以这个代码是可以跨架构移植的,而不管字长是多少。虽然伯克利发布了NET2,其中的确定义了一个新的函数inet_aton(),但如果试着改变inet_addr()中的代码,将会损坏许多程序。 低级代码的低级结构 低级编程意味着直接操纵物理设备或者特定协议的通讯格式,例如,设备驱动程序经常使用非常精确的位模式来操纵控制寄存器。此外, 网络协议通过特定的比特位模式传输数据项时,也必须适当地转译。 为了操纵物理数据项,此处的数据结构必须准确地反映被操纵的内容。比特位是一个不错的选择,因为它们正好指定了比特的位数及排列。事实上,正是这种精确度,使比特位相对于short、int、long,更好地映像了物理结构(short、int、long会因为电脑的不同而改变,而比特位不会)。 当映像一个物理结构时,是通过定义格式来使比特位达到这种精度的,这就使你必须一直使用一种编码风格来访问结构。此时的每个位域都是命名的,你写出的代码可直接访问这些位域。当访问物理结构时,有件事可能你并不想去做,那就是使用标量数组(short、int、or long),访问这些数组的代码都假定存在一个特定的比特位宽,当移植这些代码到一台使用不同字宽的电脑上时,就可能不正确了。 在我们移植PEX图像库时遇到的一个问题,就涉及到其映像的协议消息结构。在某台电脑上,如果int整型的长度与消息中的元素一样,那例2(a)中的代码就会工作得很正常。32位的数据元素在32字长的电脑上没有问题,拿到64位的克雷计算机上,它就出错了。对例2(b)而言,不单要改变结构定义,还要改变所有涉及到coord数组的代码。这样,摆在我们面前就有两个选择,要么重写涉及此消息的所有代码,要么定义一个低级的结构和一个高级的结构,然后用一段特殊的代码把数据从一个拷贝到另一个当中,不过我也不期望可以找出每个对zcoord = draw_ msg.coord[2]的引用,而且,当现在需要移植到一个新架构上时,把所有的代码都改写成如例2(c)所示无疑是一项艰苦的工作。这个特殊的问题是由于忽视字长的不同而带来的,所以不能假设在可移植的代码中机器字长、short、int、long都是具有同样的大小。 例2:低级代码的低级结构 (a) struct draw_msg { int objectid; int coord[3]; } |
(b) struct draw_msg { int objectid:32; int coord1:32; int coord2:32; int coord3:32; } |
(c) int *iptr, *optr, *limit; int xyz[3];
iptr = draw_msg.coord; limit = draw_msg.coord + sizeof(draw_msg.coord);
optr = xyz; while (iptr < limit) *optr++ = *iptr++; | 结构打包和字对齐
正是因为编译器会对结构进行打包,所以不同计算机上字长的变化,还导致了另一个问题。C编译器在字(word)的边界上对齐字长,当具有一个字长的数据后面紧接着一个较小的数据时,这种方法会产生内存空缺(不过也有例外,比如说当有足够多的小数据刚好可以填充一个字时)。 一些聪明的程序员在声明联合时,往往在其中会带有两个或更多的结构,其中一个结构刚好填充联合,另一个则可以用来从不同的角度来看待这个联合,参见例3(a)。假设这段代码是为16位字长的计算机所写,int为16位,long为32位,那么存取这个结构的代码将会得到正常的映射关系(如图1),而例3(b)中的代码也会按预期的那样工作。可是,如果这段代码一旦移植到另一台具有32位字长的计算机上时,映射关系就改变了。如果新计算机上的编译器允许你使用16位的int,那么字的对齐就会像图2所示了,或者如果编译器遵循K&R约定,那么int将会和一个字(32比特)一样长,对齐就如图3所示,在任一情况下,这都将导致问题。 例3:结构打包和字对齐 (a) union parse_hdr { struct hdr { char data1; char data2; int data3; int data4; } hdr; struct tkn { int class; long tag; } tkn; } parse_item; |
(b) char *ptr = msgbuf;
parse_item.hdr.data1 = *ptr++; parse_item.hdr.data2 = *ptr++; parse_item.hdr.data3 = (*ptr++ << 8 | *ptr++); parse_item.hdr.data4 = (*ptr++ << 8 | *ptr++);
if (parse.tkn.class >= MIN_TOKEN_CLASS && parse.tkn.class <= MAX_TOKEN_CLASS) { interpret_tag(parse.tkn.tag); } |
在第一个情况中(图2),tag域不是像期望的那样线性拉长,而被填充了一些垃圾。而在第二个情况中(图3),无论是class还是tag域,都不再有意义,两个char值因为被打包成一个int,所以也都不再正确。再次强调,首先不要假设标准数据类型大小一样,其次还要了解它们是怎样被映射成其他数据类型的,这才是书写可移植代码的最好方法。 机器寻址特性 几乎所有的处理器都在字边界上以字为单位进行寻址,而且通常都为此作了一些优化。另有一些的处理器允许其他类型的寻址,如以字节为单位寻址、或在半个字边界上以半字为单位寻址,甚至还有一些处理器有辅助硬件允许在奇数边界上同时以字和半字进行寻址。 寻址机制在不同计算机上会有所变化,最快的寻址模式是在字边界上以字为单位进行寻址。其他方式的寻址需要辅助硬件,通常都会对内存访问增加了一些时钟周期。而这些过多的模式和特殊硬件的支持,是与RISC处理器的设计初衷背道而驰的,就拿克雷计算机来说,就只支持在字边界上以字为单位进行寻址。 在那些不提供多种数据类型寻址方式的计算机上,编译器可以提供一些模拟。例如:编译器可以生成一些指令,当读取一个字时,通过移位和屏蔽,来找到所想要的位置,以此来模拟在字中的半字寻址,但这会需要额外的时钟周期,并且代码体积会更大。 从这点上来说,位域的效率是非常低的,在以位域来取出一个字时,它们产生的代码体积最大。当你存取同一个字中的其他位域时,又需要对包含这个位域字的内存,重新进行一遍寻址,这就是典型的以空间换时间。 当在设计数据结构时,我们总是想用可以保存数据的最小数据类型,来达到节省空间的目的。我们有时小气得经常使用char和short,来存取位特域,这就像是为了节省一角钱,而花了一元钱。储存空间上的高效,会付出在程序速度和体积上隐藏的代价。 试想你只为一个紧凑结构分配了一小点的空间,但却产生了大量的代码来存取结构中的域,而且这段代码还是经常执行的,那么,会因为非字寻址,而导致代码运行缓慢,而且充斥了大量用于提取域的代码,程序体积也因此增大。这些额外代码所占的空间,会让你前面为节省空间所做的努力付之东流。 在高级数据结构中,特定的比特定位已不是必须的了,应在所有的域中都使用字(word),而不要操心它们所占用的空间。特别是在程序某些依赖于机器的部分,应该为字准备一个typedef,如下: /*在这台计算机上,int是一个字长*/ typedef word int; |
在高级结构中,对所有域都使用字有如下好处: a.. 对其他计算机架构的可移植性 b.. 编译器可能生成最快的代码 c.. 处理器可能最快地访问到所需内存 d.. 绝对没有结构对齐的意外发生 必须也承认,在某些时候,是不能做到全部使用字的。例如,有一个很大的结构,但不会被经常存取,如果使用了数千个字的话,体积将会增大25%,但使用字通常会节省空间、提高执行速度,而且更具移植性。 以下是我们的结论: 书写跨平台移植的代码,其实是件简单的事情。最基本的规则是,尽可能地隐藏机器字长的细节,用非常精确的数据元素位大小来映射物理数据结构。或者像前面所建议的,为高级编程使用高级数据结构,而低级编程使用低级数据结构,当阐明高级数据结构时,对标准C的标量类型,不要作任何假设
使用宏定义
在C语言中,宏是产生内嵌代码的唯一方法。对于嵌入式系统而言,为了能达到性能要求,宏是一种很好的代替函数的方法。 写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个: 错误做法: #define MIN(A,B) ( A <= B ? A : B ) |
正确做法: #define MIN(A,B) ((A)<= (B) ? (A) : (B) ) |
对于宏,我们需要知道三点: (1)宏定义"像"函数; (2)宏定义不是函数,因而需要括上所有"参数"; (3)宏定义可能产生副作用。 下面的代码: 将被替换为: ( (*p++) <= (b) ?(*p++):(b) ) |
发生的事情无法预料。 因而不要给宏定义传入有副作用的"参数"。 使用寄存器变量 当对一个变量频繁被读写时,需要反复访问内存,从而花费大量的存取时间。为此,C语言提供了一种变量,即寄存器变量。这种变量存放在CPU的寄存器中,使用时,不需要访问内存,而直接从寄存器中读写,从而提高效率。寄存器变量的说明符是register。对于循环次数较多的循环控制变量及循环体内反复使用的变量均可定义为寄存器变量,而循环计数是应用寄存器变量的最好候选者。 (1) 只有局部自动变量和形参才可以定义为寄存器变量。因为寄存器变量属于动态存储方式,凡需要采用静态存储方式的量都不能定义为寄存器变量,包括:模块间全局变量、模块内全局变量、局部static变量; (2) register是一个"建议"型关键字,意指程序建议该变量放在寄存器中,但最终该变量可能因为条件不满足并未成为寄存器变量,而是被放在了存储器中,但编译器中并不报错(在C++语言中有另一个"建议"型关键字:inline)。 下面是一个采用寄存器变量的例子: /* 求1+2+3+….+n的值 */ WORD Addition(BYTE n) { register i,s=0; for(i=1;i<=n;i++) { s=s+i; } return s; } |
本程序循环n次,i和s都被频繁使用,因此可定义为寄存器变量。 内嵌汇编 程序中对时间要求苛刻的部分可以用内嵌汇编来重写,以带来速度上的显著提高。但是,开发和测试汇编代码是一件辛苦的工作,它将花费更长的时间,因而要慎重选择要用汇编的部分。 在程序中,存在一个80-20原则,即20%的程序消耗了80%的运行时间,因而我们要改进效率,最主要是考虑改进那20%的代码。 嵌入式C程序中主要使用在线汇编,即在C程序中直接插入_asm{ }内嵌汇编语句: /* 把两个输入参数的值相加,结果存放到另外一个全局变量中 */ int result; void Add(long a, long *b) { _asm { MOV AX, a MOV BX, b ADD AX, [BX] MOV result, AX } } | 利用硬件特性 首先要明白CPU对各种存储器的访问速度,基本上是: CPU内部RAM > 外部同步RAM > 外部异步RAM > FLASH/ROM 对于程序代码,已经被烧录在FLASH或ROM中,我们可以让CPU直接从其中读取代码执行,但通常这不是一个好办法,我们最好在系统启动后将FLASH或ROM中的目标代码拷贝入RAM中后再执行以提高取指令速度; 对于UART等设备,其内部有一定容量的接收BUFFER,我们应尽量在BUFFER被占满后再向CPU提出中断。例如计算机终端在向目标机通过RS-232传递数据时,不宜设置UART只接收到一个BYTE就向CPU提中断,从而无谓浪费中断处理时间; 如果对某设备能采取DMA方式读取,就采用DMA读取,DMA读取方式在读取目标中包含的存储信息较大时效率较高,其数据传输的基本单位是块,而所传输的数据是从设备直接送入内存的(或者相反)。DMA方式较之中断驱动方式,减少了CPU 对外设的干预,进一步提高了CPU与外设的并行操作程度。 活用位操作 使用C语言的位操作可以减少除法和取模的运算。在计算机程序中数据的位是可以操作的最小数据单位,理论上可以用"位运算"来完成所有的运算和操作,因而,灵活的位操作可以有效地提高程序运行的效率。举例如下: /* 方法1 */ int i,j; i = 879 / 16; j = 562 % 32; /* 方法2 */ int i,j; i = 879 >> 4; j = 562 - (562 >> 5 << 5); |
对于以2的指数次方为"*"、"/"或"%"因子的数学运算,转化为移位运算"<< >>"通常可以提高算法效率。因为乘除运算指令周期通常比移位运算大。 C语言位运算除了可以提高运算效率外,在嵌入式系统的编程中,它的另一个最典型的应用,而且十分广泛地正在被使用着的是位间的与(&)、或(|)、非(~)操作,这跟嵌入式系统的编程特点有很大关系。我们通常要对硬件寄存器进行位设置,譬如,我们通过将AM186ER型80186处理器的中断屏蔽控制寄存器的第低6位设置为0(开中断2),最通用的做法是: #define INT_I2_MASK 0x0040 wTemp = inword(INT_MASK); outword(INT_MASK, wTemp &~INT_I2_MASK); |
而将该位设置为1的做法是: #define INT_I2_MASK 0x0040 wTemp = inword(INT_MASK); outword(INT_MASK, wTemp | INT_I2_MASK); |
判断该位是否为1的做法是: #define INT_I2_MASK 0x0040 wTemp = inword(INT_MASK); if(wTemp & INT_I2_MASK) { … /* 该位为1 */ } |
上述方法在嵌入式系统的编程中是非常常见的,我们需要牢固掌握。 总结 在性能优化方面永远注意80-20准备,不要优化程序中开销不大的那80%,这是劳而无功的。 宏定义是C语言中实现类似函数功能而又不具函数调用和返回开销的较好方法,但宏在本质上不是函数,因而要防止宏展开后出现不可预料的结果,对宏的定义和使用要慎而处之。很遗憾,标准C至今没有包括C++中inline函数的功能,inline函数兼具无调用开销和安全的优点。 使用寄存器变量、内嵌汇编和活用位操作也是提高程序效率的有效方法。 除了编程上的技巧外,为提高系统的运行效率,我们通常也需要最大可能地利用各种硬件设备自身的特点来减小其运转开销,例如减小中断次数、利用DMA传输方式等。
Posted on 2006-06-24 16:55 Gin 阅读(125) 评论(0) 编辑 收藏 摘自网上的,呵,这几个是自己比较少用到的
F2 当你选中一个文件的话,这意味着“重命名” ALT+ ENTER或 ALT+双击 查看项目的属性 F10或ALT 激活当前程序的菜单栏 CTRL+ ESC 显示“开始”菜单 ALT+空格键 显示当前窗口的系统菜单 CTRL+F5 强行刷新 ALT+RIGHT ARROW 显示前一页(前进键) ALT+LEFT ARROW 显示后一页(后退键) ALT+ESC 切换当前程序 CTRL+N 新建一个新的文件 CTRL+O 打开“打开文件”对话框 CTRL+P 打开“打印”对话框
SHIFT+F10 显示某个链接的快捷菜单 CTRL+W 关闭当前窗口 ALT+D 选择地址栏中的文字 CTRL+ENTER 在地址栏中将"www."添加到键入的文本的前面,将".com"添加到文本的后面 CTRL+D 将当前Web 页添加到收藏夹中 CTRL+B 打开"整理收藏夹"对话框 CTRL+I 在浏览栏中打开收藏夹 ALT+ENTER 将 Windows 下运行的命令行窗口在窗口和全屏幕状态间切换;查看选定的文件的属性;选定任务栏时打开"任务栏和开始菜单"属性 Alt+空格→X 最大化当前窗口 Alt+空格→N 最小化当前窗口
在用C++写要导出类的库时,我们经常只想暴露接口,而隐藏类的实现细节。也就是说我们提供的头文件里只提供要暴露的公共成员函数的声明,类的其他所有信息都不会在这个头文件里面显示出来。这个时候就要用到接口与实现分离的技术。 下面用一个最简单的例子来说明。 类ClxExp是我们要导出的类,其中有一个私有成员变量是ClxTest类的对象,各个文件内容如下: lxTest.h文件内容: class ClxTest { public: ClxTest(); virtual ~ClxTest(); void DoSomething(); }; |
lxTest.cpp文件内容: #include "lxTest.h"
#include <iostream> using namespace std;
ClxTest::ClxTest() {}
ClxTest::~ClxTest() {}
void ClxTest::DoSomething() { cout << "Do something in class ClxTest!" << endl; }
//////////////////////////////////////////////////////////////////////////// |
lxExp.h文件内容: #include "lxTest.h"
class ClxExp { public: ClxExp(); virtual ~ClxExp(); void DoSomething(); private: ClxTest m_lxTest; void lxTest(); }; |
lxExp.cpp文件内容: #include "lxExp.h"
ClxExp::ClxExp() {}
ClxExp::~ClxExp() {}
// 其实该方法在这里并没有必要,我这样只是为了说明调用关系 void ClxExp::lxTest() { m_lxTest.DoSomething(); }
void ClxExp::DoSomething() { lxTest(); } |
为了让用户能使用我们的类ClxExp,我们必须提供lxExp.h文件,这样类ClxExp的私有成员也暴露给用户了。而且,仅仅提供lxExp.h文件是不够的,因为lxExp.h文件include了lxTest.h文件,在这种情况下,我们还要提供lxTest.h文件。那样ClxExp类的实现细节就全暴露给用户了。另外,当我们对类ClxTest做了修改(如添加或删除一些成员变量或方法)时,我们还要给用户更新lxTest.h文件,而这个文件是跟接口无关的。如果类ClxExp里面有很多像m_lxTest那样的对象的话,我们就要给用户提供N个像lxTest.h那样的头文件,而且其中任何一个类有改动,我们都要给用户更新头文件。还有一点就是用户在这种情况下必须进行重新编译! 上面是非常小的一个例子,重新编译的时间可以忽略不计。但是,如果类ClxExp被用户大量使用的话,那么在一个大项目中,重新编译的时候我们就有时间可以去喝杯咖啡什么的了。当然上面的种种情况不是我们想看到的!你也可以想像一下用户在自己程序不用改动的情况下要不停的更新头文件和编译时,他们心里会骂些什么。其实对用户来说,他们只关心类ClxExp的接口DoSomething()方法。那我们怎么才能只暴露类ClxExp的DoSomething()方法而不又产生上面所说的那些问题呢?答案就是--接口与实现的分离。我可以让类ClxExp定义接口,而把实现放在另外一个类里面。下面是具体的方法: 首先,添加一个实现类ClxImplement来实现ClxExp的所有功能。注意:类ClxImplement有着跟类ClxExp一样的公有成员函数,因为他们的接口要完全一致。 lxImplement.h文件内容: #include "lxTest.h"
class ClxImplement { public: ClxImplement(); virtual ~ClxImplement();
void DoSomething(); private: ClxTest m_lxTest; void lxTest(); }; |
lxImplement.cpp文件内容: #include "lxImplement.h"
ClxImplement::ClxImplement() {}
ClxImplement::~ClxImplement() {}
void ClxImplement::lxTest() { m_lxTest.DoSomething(); }
void ClxImplement::DoSomething() { lxTest(); } |
然后,修改类ClxExp。 修改后的lxExp.h文件内容: // 前置声明 class ClxImplement;
class ClxExp { public: ClxExp(); virtual ~ClxExp(); void DoSomething(); private: // 声明一个类ClxImplement的指针,不需要知道类ClxImplement的定义 ClxImplement *m_pImpl; }; |
修改后的lxExp.cpp文件内容: // 在这里包含类ClxImplement的定义头文件 #include "lxImplement.h"
ClxExp::ClxExp() { m_pImpl = new ClxImplement; }
ClxExp::~ClxExp() { delete m_pImpl; }
void ClxExp::DoSomething() { m_pImpl->DoSomething(); } |
通过上面的方法就实现了类ClxExp的接口与实现的分离。请注意两个文件中的注释。类ClxExp里面声明的只是接口而已,而真正的实现细节被隐藏到了类ClxImplement里面。为了能在类ClxExp中使用类ClxImplement而不include头文件lxImplement.h,就必须有前置声明class ClxImplement,而且只能使用指向类ClxImplement对象的指针,否则就不能通过编译。 在发布库文件的时候,我们只需给用户提供一个头文件lxExp.h就行了,不会暴露类ClxExp的任何实现细节。而且我们对类ClxTest的任何改动,都不需要再给用户更新头文件(当然,库文件是要更新的,但是这种情况下用户也不用重新编译!)。这样做还有一个好处就是,可以在分析阶段由系统分析员或者高级程序员来先把类的接口定义好,甚至可以把接口代码写好(例如上面修改后的lxExp.h文件和lxExp.cpp文件),而把类的具体实现交给其他程序员开发。
前些日子开始看《C++ Primer》,顺便做一些笔记,既有书上的,也有自己理解的。 因为刚学C++不久,笔下难免有谬误之处,行文更是凌乱; 所幸不是用来显配的东西,发在linuxsir只是为了方便自己阅读记忆,以防只顾上网忘了正事。 书看了不到一半,所以大约才写了一半,慢慢补充。 ========================================= ========================================== 转载务必注明原作者 neplusultra 2005.2.3 ========================================== const要注意的问题 1、下面是一个几乎所有人刚开始都会搞错的问题: 已知:typedef char *cstring; 在以下声明中,cstr的类型是什么? extern const cstring cstr; 错误答案:const char *cstr; 正确答案:char *const cstr; 错误在于将typedef当作宏扩展。const 修饰cstr的类型。cstr是一个指针,因此,这个定义声明了cstr是一个指向字符的const指针。 2、指针是const还是data为const? 辨别方法很简单,如下: 代码: char *p="hello"; //non-const pointer, non-const data;
const char *p="hello"; // non-const pointer, const data;
char * const p="hello"; // const pointer , non-const data;
const char * const p="hello"; // const pointer, const data;
要注意的是,"hello"的类型是const char * ,按C++standard规则,char *p="hello" 是非法的(右式的const char* 不能转换为左式的char *),违反了常量性。但是这种行为在C中实在太频繁,因此C++standard对于这种初始化动作给予豁免。尽管如此,还是尽量避免这种用法。 3、const初始化的一些问题 const 对象必须被初始化: 代码: const int *pi=new int; // 错误,没有初始化
const int *pi=new int(100); //正确
const int *pci=new const int[100]; //编译错误,无法初始化用new表达式创建的内置类型数组元素。
什么时候需要copy constructor,copy assignment operator,destructor 注意,若class需要三者之一,那么它往往需要三者。 当class的copy constructor内分配有一块指向hcap的内存,需要由destructor释放,那么它也往往需要三者。 为什么需要protected 访问级别 有人认为,protected访问级别允许派生类直接访问基类成员,这破坏了封装的概念,因此所有基类的实现细节都应该是private的;另外一些人认为,如果派生类不能直接访问基类的成员,那么派生类的实现将无法有足够的效率供用户使用,如果没有protected,类的设计者将被迫把基类成员设置为public。 事实上,protected正是在高纯度的封装与效率之间做出的一个良好折衷方案。 为什么需要virtual member function又不能滥用virtual 若基类设计者把本应设计成virtual的成员函数设计成非virtual,则继承类将无法实现改写(overridden),给继承类的实现带来不便; 另一方面,一旦成员函数被设计成virtual,则该类的对象将额外增加虚拟指针(vptr)和虚拟表格(vtbl),所以倘若出于方便继承类overridden的目的而使所有成员函数都为virtual,可能会影响效率,因为每个virtual成员函数都需付出动态分派的成本。而且virtual成员函数不能内联(inline),我们知道,内联发生在编译时刻,而虚拟函数在运行时刻才处理。对于那些小巧而被频繁调用、与类型无关的函数,显然不应该被设置成virtual。 关于引用的一些注意点 1、把函数参数声明为数组的引用:当函数参数是一个数组类型的引用时,数组长度成为参数和实参类型的一部分,编译器检查数组实参的长度和与在函数参数类型中指定的长度是否匹配。 代码: //参数为10个int数组
void showarr(int (&arr)[10]);
void func()
{
int i,j[2],k[10];
showarr(i); //错误!实参必须是10个int的数组
showarr(j); //错误!实参必须是10个int的数组
showarr(k); //正确!
}
//更灵活的实现,借助函数模板。下面是一个显示数组内容的函数。
template <typename Type , int size>
void printarr(const Type (& r_array)[size])
{
for(int i=0;i<size;i++) std::cout<< r_array[i] <<' ';
std::cout << std::endl;
}
void caller()
{
int ar[5]={1,2,5,3,4}; //数组可以任意大小。
printarr(ar); //正确!自动正确调用printarr()
}
2、 3、 goto语句的一些要注意的地方 1、label语句只能用作goto的目标,且label语句只能用冒号结束,且label语句后面不能紧接右花括号'}',如 办法是在冒号后面加一个空语句(一个';'即可),如 2、goto语句不能向前跳过如下声明语句: 代码: goto label6;
int x=1; //错误,不能跳过该声明!
cout<<x<<endl; //使用x
label6:
//其他语句
但是,把int x=1; 改为int x; 则正确了。另外一种方法是: 代码: goto label6;
{
int x=1; //正确,使用了语句快
cout<<x<<endl;
}
label6:
//其他语句
3、goto语句可以向后(向程序开头的方向)跳过声明定义语句。 代码: begin:
int i=22;
cout<< i <<endl;
goto begin; //非常蹩脚,但它是正确的
变量作用域 1、花括号可以用来指明局部作用域。 2、在for、if、switch、while语句的条件/循环条件中可以声明变量,该变量仅在相应语句块内有效。 3、extern为声明但不定义一个对象提供了一种方法;它类似于函数声明,指明该对象会在其他地方被定义:或者在此文本的其他地方,或者在程序的其他文本文件中。例如extern int i; 表示在其他地方存在声明 int i; extern 声明不会引起内存分配,他可以在同一个文件或同一个程序中出现多次。因此在全局作用域中,以下语句是正确的: 代码: extern int c;
int c=1; //没错
extern int c; //没错
但是,extern声明若指定了一个显式初始值的全局对象,将被视为对该对象的定义,编译器将为其分配存储区;对该对象的后续定义将出错。如下: 代码: extern int i=1;
int i=2; //出错!重复定义
auto_ptr若干注意点 1、auto_ptr的主要目的是支持普通指针类型相同的语法,并为auto_ptr所指对象的释放提供自动管理,而且auto_ptr的安全性几乎不会带来额外的代价(因为其操作支持都是内联的)。定义形式有三种: 代码: auto_ptr<type_pointed_to>identifier(ptr_allocated_by_new);
auto_ptr<type_pointed_to>identifier(auto_ptr_of_same_type);
auto_ptr<type_pointed_to>identifier;
2、所有权概念。auto_ptr_p1=auto_ptr_p2的后果是,auto_ptr_p2丧失了其原指向对象的所有权,并且auto_ptr_p2.get()==0。不要让两个auto_ptr对象拥有空闲存储区内同一对象的所有权。注意以下两种种初始化方式的区别: 代码: auto_ptr<string>auto_ptr_str1(auto_ptr_str2.get()); //注意!用str2指针初始化str1, 两者同时拥有所有权,后果未定义。
auto_ptr<string>auto_ptr_str1(auto_ptr_str2.release());//OK!str2释放了所有权。
3、不能用一个指向“内存不是通过应用new表达式分配的”指针来初始化或者赋值auto_ptr。如果这样做了,delete表达式会被应用在不是动态分配的指针上,这将导致未定义的程序行为。 C风格字符串结尾空字符问题代码: char *str="hello world!"; //str末尾自动加上一个结尾空字符,但strlen不计该空字符。
char *str2=new char[strlen(str)+1] // +1用来存放结尾空字符。
定位new表达式 头文件:<new> 形式:new (place_address) type-specifier 该语句可以允许程序员将对象创建在已经分配好的内存中,允许程序员预分配大量的内存供以后通过这种形式的new表达式创建对象。其中place_address必须是一个指针。例如: 代码: char *buf=new char[sizeof(myclass-type)*16];
myclass-type *pb=new (buf) myclass-type; //使用预分配空间来创建对象
// ...
delete [] buf; // 无须 delete pb。
名字空间namespace
1、namespace的定义可以是不连续的(即namespace的定义是可以积累的),即,同一个namespace可以在不同的文件中定义,分散在不同文件中的同一个namespace中的内容彼此可见。这对生成一个库很有帮助,可以使我们更容易将库的源代码组织成接口和实现部分。如:在头文件(.h文件)的名字空间部分定义库接口;在实现文件(如.c或.cpp文件)的名字空间部分定义库实现。名字空间定义可积累的特性是“向用户隐藏实现细节”必需的,它允许把不同的实现文件(如.c或.cpp文件)编译链接到一个程序中,而不会有编译错误和链接错误。 2、全局名字空间成员,可以用“::member_name”的方式引用。当全局名字空间的成员被嵌套的局部域中声明的名字隐藏时,就可以采用这种方法引用全局名字空间成员。 3、名字空间成员可以被定义在名字空间之外。但是,只有包围该成员声明的名字空间(也就是该成员声明所在的名字空间及其 外围名字空间)才可以包含它的定义。 尤其要注意的是#include语句的次序。假定名字空间成员mynamespace::member_i的声明在文件dec.h中,且#include "dec.h"语句置于 全局名字空间,那么在include语句之后定义的其他名字空间内,mynamespace::member_i的声明均可见。即,mynamespace::member_i可以在#include "dec.h"之后的任何地方任何名字空间内定义。 4、未命名的名字空间。我们可以用未命名的名字空间声明一个局部于某一文件的实体。未命名的名字空间可以namespace开头,其后不需名字,而用一对花括号包含名字空间声明块。如: 代码: // 其他代码略
namespace
{
void mesg()
{
cout<<"**********\n";
}
}
int main()
{
mesg(); //正确
//...
return 0;
}
由于未命名名字空间的成员是程序实体,所以mesg()可以在程序整个执行期间被调用。但是,未命名名字空间成员只在特定的文件中可见,在构成程序的其他文件中是不可以见的。未命名名字空间的成员与被声明为static的全局实体具有类似的特性。在C中,被声明为static的全局实体在声明它的文件之外是不可见的。 using关键字 1、using声明与using指示符:前者是声明某名字空间内的一个成员,后者是使用整个名字空间。例如: 代码: using cpp_primer::matrix; // ok,using声明
using namespace cpp_primer; //ok,using指示符
2、 该using指示符语句可以加在程序文件的几乎任何地方,包括文件开头(#include语句之前)、函数内部。不过用using指定的名字空间作用域(生命周期)受using语句所在位置的生命周期约束。如,函数内部使用“using namespace myspacename;”则 myspacename仅在该函数内部可见。 3、可以用using语句指定多个名字空间,使得多个名字空间同时可见。但这增加了名字污染的可能性,而且只有在 使用各名字空间相同成员时由多个using指示符引起的二义性错误才能被检测到,这将给程序的检测、扩展、移植带来很大的隐患。因此,因该尽量使用using声明而不是滥用using指示符。 重载函数 1、如果两个函数的参数表中参数的个数或者类型不同,则认为这两个函数是重载的。 如果两个函数的返回类型和参数表精确匹配,则第二个声明被视为第一个的重复声明,与参数名无关。如 void print(string& str)与void print(string&)是一样的。 如果两个函数的参数表相同,但是返回类型不同,则第二个声明被视为第一个的错误重复声明,会标记为编译错误。 如果在两个函数的参数表中,只有缺省实参不同,则第二个声明被视为第一个的重复声明。如int max(int *ia,int sz)与int max(int *, int=10)。 参数名类型如果是由typedef提供的,并不算作新类型,而应该当作typedef的原类型。 当参数类型是const或者volatile时,分两种情况:对于实参按值传递时,const、volatile修饰符可以忽略;对于把const、volatile应用在指针或者引用参数指向的类型时,const、volatile修饰符对于重载函数的声明是有作用的。例如: 代码: //OK,以下两个声明其实一样
void func(int i);
void func(const int i);
//Error,无法通过编译,因为func函数被定义了两次。
void func(int i){}
void func(const int i){}
//OK,声明了不同的函数
void func2(int *);
void func2(const int *);
//OK,声明了不同的函数
void func3(int&);
void func3(const int&);
2、链接指示符extern "C"只能指定重载函数集中的一个函数。原因与内部名编码有关,在大多数编译器内部,每个函数明及其相关参数表都被作为一个惟一的内部名编码,一般的做法是把参数的个数和类型都进行编码,然后将其附在函数名后面。但是这种编码不使用于用链接指示符extern "C"声明的函数,这就是为什么在重载函数集合中只有一个函数可以被声明为extern "C"的原因,具有不同的参数表的两个extern "C"的函数会被链接编辑器视为同一函数。例如,包含以下两个声明的程序是非法的。 代码: //error:一个重载函数集中有两个extern "C"函数
extern "C" void print(const char*);
extern "C" void print(int);
函数模板 1、定义函数模板: 代码: template <typename/class identifier, ...>
[inline/extern]
ReturnType FunctionName(FuncParameters...)
{
//definition of a funciton template...
}
条款28: 划分全局名字空间全局空间最大的问题在于它本身仅有一个。在大的软件项目中,经常会有不少人把他们定义的名字都放在这个单一的空间中,从而不可避免地导致名字冲突。例如,假设library1.h定义了一些常量,其中包括: const double lib_version = 1.204; 类似的,library2.h也定义了: const int lib_version = 3; 很显然,如果某个程序想同时包含library1.h和library2.h就会有问题。对于这类问题,你除了嘴里骂几句,或给作者发报复性邮件,或自己编辑头文件来消除名字冲突外,也没其它什么办法。 但是,作为程序员,你可以尽力使自己写的程序库不给别人带来这些问题。例如,可以预先想一些不大可能造成冲突的某种前缀,加在每个全局符号前。当然得承认,这样组合起来的标识符看起来不是那么令人舒服。 另一个比较好的方法是使用c++ namespace。namespace本质上和使用前缀的方法一样,只不过避免了别人总是看到前缀而已。所以,不要这么做: const double sdmbook_version = 2.0; // 在这个程序库中, // 每个符号以"sdm"开头 class sdmhandle { ... }; sdmhandle& sdmgethandle(); // 为什么函数要这样声明? // 参见条款47 而要这么做: namespace sdm { const double book_version = 2.0; class handle { ... }; handle& gethandle(); } 用户于是可以通过三种方法来访问这一名字空间里的符号:将名字空间中的所有符号全部引入到某一用户空间;将部分符号引入到某一用户空间;或通过修饰符显式地一次性使用某个符号: void f1() { using namespace sdm; // 使得sdm中的所有符号不用加 // 修饰符就可以使用 cout << book_version; // 解释为sdm::book_version ... handle h = gethandle(); // handle解释为sdm::handle, // gethandle解释为sdm::gethandle ... } void f2() { using sdm::book_version; // 使得仅book_version不用加 // 修饰符就可以使用 cout << book_version; // 解释为 // sdm::book_version ... handle h = gethandle(); // 错误! handle和gethandle // 都没有引入到本空间 ... } void f3() { cout << sdm::book_version; // 使得book_version // 在本语句有效 ... double d = book_version; // 错误! book_version // 不在本空间 handle h = gethandle(); // 错误! handle和gethandle // 都没有引入到本空间 ... } (有些名字空间没有名字。这种没命名的名字空间一般用于限制名字空间内部元素的可见性。详见条款m31。) 名字空间带来的最大的好处之一在于:潜在的二义不会造成错误(参见条款26)。所以,从多个不同的名字空间引入同一个符号名不会造成冲突(假如确实真的从不使用这个符号的话)。例如,除了名字空间sdm外,假如还要用到下面这个名字空间: namespace acmewindowsystem { ... typedef int handle; ... } 只要不引用符号handle,使用sdm和acmewindowsystem时就不会有冲突。假如真的要引用,可以明确地指明是哪个名字空间的handle: void f() { using namespace sdm; // 引入sdm里的所有符号 using namespace acmewindowsystem; // 引入acme里的所有符号 ... // 自由地引用sdm // 和acme里除handle之外 // 的其它符号 handle h; // 错误! 哪个handle? sdm::handle h1; // 正确, 没有二义 acmewindowsystem::handle h2; // 也没有二义 ... } 假如用常规的基于头文件的方法来做,只是简单地包含sdm.h和acme.h,这样的话,由于handle有多个定义,编译将不能通过。 名字空间的概念加入到c++标准的时间相对较晚,所以有些人会认为它不太重要,可有可无。但这种想法是错误的,因为c++标准库(参见条款49)里几乎所有的东西都存在于名字空间std之中。这可能令你不以为然,但它却以一种直接的方式影响到你:这就是为什么c++提供了那些看起来很有趣的、没有扩展名的头文件,如<iostream>, <string>等。详细介绍参见条款49。 由于名字空间的概念引入的时间相对较晚,有些编译器可能不支持。就算是这样,那也没理由污染全局名字空间,因为可以用struct来近似实现namespace。可以这样做:先创建一个结构用以保存全局符号名,然后将这些全局符号名作为静态成员放入结构中: // 用于模拟名字空间的一个结构的定义 struct sdm { static const double book_version; class handle { ... }; static handle& gethandle(); }; const double sdm::book_version = 2.0; // 静态成员的定义 现在,如果有人想访问这些全局符号名,只用简单地在它们前面加上结构名作为前缀: void f() { cout << sdm::book_version; ... sdm::handle h = sdm::gethandle(); ... } 但是,如果全局范围内实际上没有名字冲突,用户就会觉得加修饰符麻烦而多余。幸运的是,还是有办法来让用户选择使用它们或忽略它们。 对于类型名,可以用类型定义(typedef)来显式地去掉空间引用。例如,假设结构s(模拟的名字空间)内有个类型名t,可以这样用typedef来使得t成为s::t的同义词: typedef sdm::handle handle; 对于结构中的每个(静态)对象x,可以提供一个(全局)引用x,并初始化为s::x: const double& book_version = sdm::book_version; 老实说,如果读了条款47,你就会不喜欢定义一个象book_version这样的非局部静态对象。(你就会用条款47中所介绍的函数来取代这样的对象) 处理函数的方法和处理对象一样,但要注意,即使定义函数的引用是合法的,但代码的维护者会更喜欢你使用函数指针: sdm::handle& (* const gethandle)() = // gethandle是指向sdm::gethandle sdm::gethandle; // 的const 指针 (见条款21) 注意gethandle是一个常指针。因为你当然不想让你的用户将它指向别的什么东西,而不是sdm::gethandle,对不对? (如果真想知道怎么定义一个函数的引用,看看下面: sdm::handle& (&gethandle)() = // gethandle是指向 sdm::gethandle; // sdm::gethandle的引用 我个人认为这样的做法也很好,但你可能以前从没见到过。除了初始化的方式外,函数的引用和函数的常指针在行为上完全相同,只是函数指针更易于理解。) 有了上面的类型定义和引用,那些不会遭遇全局名字冲突的用户就会使用没有修饰符的类型和对象名;相反,那些有全局名字冲突的用户就会忽略类型和引用的定义,代之以带修饰符的符号名。还要注意的是,不是所有用户都想使用这种简写名,所以要把类型定义和引用放在一个单独的头文件中,不要把它和(模拟namespace的)结构的定义混在一起。 struct是namespace的很好的近似,但实际上还是相差很远。它在很多方面很欠缺,其中很明显的一点是对运算符的处理。如果运算符被定义为结构的静态成员,它就只能通过函数调用来使用,而不能象常规的运算符所设计的那样,可以通过自然的中缀语法来使用: // 定义一个模拟名字空间的结构,结构内部包含widgets的类型 // 和函数。widgets对象支持operator+进行加法运算 struct widgets { class widget { ... }; // 参见条款21:为什么返回const static const widget operator+(const widget& lhs, const widget& rhs);
... }; // 为上面所述的widge和operator+ // 建立全局(无修饰符的)名称 typedef widgets::widget widget; const widget (* const operator+)(const widget&, // 错误! const widget&); // operator+不能是指针名
widget w1, w2, sum; sum = w1 + w2; // 错误! 本空间没有声明 // 参数为widgets 的operator+ sum = widgets::operator+(w1, w2); // 合法, 但不是 // "自然"的语法 正因为这些限制,所以一旦编译器支持,就要尽早使用真正的名字空间。
编写高效简洁的C语言代码,是许多软件工程师追求的目标。本文就工作中的一些体会和经验做相关的阐述,不对的地方请各位指教。
第1招:以空间换时间
计算机程序中最大的矛盾是空间和时间的矛盾,那么,从这个角度出发逆向思维来考虑程序的效率问题,我们就有了解决问题的第1招——以空间换时间。 例如:字符串的赋值。 方法A,通常的办法: #define LEN 32 char string1 [LEN]; memset (string1,0,LEN); strcpy (string1,“This is a example!!”); 方法B: const char string2[LEN] =“This is a example!”; char * cp; cp = string2 ; (使用的时候可以直接用指针来操作。)
从上面的例子可以看出,A和B的效率是不能比的。在同样的存储空间下,B直接使用指针就可以操作了,而A需要调用两个字符函数才能完成。B的缺点在于灵活性没有A好。在需要频繁更改一个字符串内容的时候,A具有更好的灵活性;如果采用方法B,则需要预存许多字符串,虽然占用了大量的内存,但是获得了程序执行的高效率。
如果系统的实时性要求很高,内存还有一些,那我推荐你使用该招数。
该招数的变招——使用宏函数而不是函数。举例如下: 方法C: #define bwMCDR2_ADDRESS 4 #define bsMCDR2_ADDRESS 17 int BIT_MASK(int __bf) { return ((1U << (bw ## __bf)) - 1) << (bs ## __bf); } void SET_BITS(int __dst, int __bf, int __val) { __dst = ((__dst) & ~(BIT_MASK(__bf))) | \ (((__val) << (bs ## __bf)) & (BIT_MASK(__bf)))) }
SET_BITS(MCDR2, MCDR2_ADDRESS, RegisterNumber); 方法D: #define bwMCDR2_ADDRESS 4 #define bsMCDR2_ADDRESS 17 #define bmMCDR2_ADDRESS BIT_MASK(MCDR2_ADDRESS) #define BIT_MASK(__bf) (((1U << (bw ## __bf)) - 1) << (bs ## __bf)) #define SET_BITS(__dst, __bf, __val) \ ((__dst) = ((__dst) & ~(BIT_MASK(__bf))) | \ (((__val) << (bs ## __bf)) & (BIT_MASK(__bf))))
SET_BITS(MCDR2, MCDR2_ADDRESS, RegisterNumber);
函数和宏函数的区别就在于,宏函数占用了大量的空间,而函数占用了时间。大家要知道的是,函数调用是要使用系统的栈来保存数据的,如果编译器里有栈检查选项,一般在函数的头会嵌入一些汇编语句对当前栈进行检查;同时,CPU也要在函数调用时保存和恢复当前的现场,进行压栈和弹栈操作,所以,函数调用需要一些CPU时间。而宏函数不存在这个问题。宏函数仅仅作为预先写好的代码嵌入到当前程序,不会产生函数调用,所以仅仅是占用了空间,在频繁调用同一个宏函数的时候,该现象尤其突出。
D方法是我看到的最好的置位操作函数,是ARM公司源码的一部分,在短短的三行内实现了很多功能,几乎涵盖了所有的位操作功能。C方法是其变体,其中滋味还需大家仔细体会。
第2招:数学方法解决问题
现在我们演绎高效C语言编写的第二招——采用数学方法来解决问题。
数学是计算机之母,没有数学的依据和基础,就没有计算机的发展,所以在编写程序的时候,采用一些数学方法会对程序的执行效率有数量级的提高。 举例如下,求 1~100的和。 方法E int I , j; for (I = 1 ;I<=100; I ++){ j += I; } 方法F int I; I = (100 * (1+100)) / 2
这个例子是我印象最深的一个数学用例,是我的计算机启蒙老师考我的。当时我只有小学三年级,可惜我当时不知道用公式 N×(N+1)/ 2 来解决这个问题。方法E循环了100次才解决问题,也就是说最少用了100个赋值,100个判断,200个加法(I和j);而方法F仅仅用了1个加法,1次乘法,1次除法。效果自然不言而喻。所以,现在我在编程序的时候,更多的是动脑筋找规律,最大限度地发挥数学的威力来提高程序运行的效率。
第3招:使用位操作
实现高效的C语言编写的第三招——使用位操作,减少除法和取模的运算。
在计算机程序中,数据的位是可以操作的最小数据单位,理论上可以用“位运算”来完成所有的运算和操作。一般的位操作是用来控制硬件的,或者做数据变换使用,但是,灵活的位操作可以有效地提高程序运行的效率。举例如下: 方法G int I,J; I = 257 /8; J = 456 % 32; 方法H int I,J; I = 257 >>3; J = 456 - (456 >> 4 << 4);
在字面上好像H比G麻烦了好多,但是,仔细查看产生的汇编代码就会明白,方法G调用了基本的取模函数和除法函数,既有函数调用,还有很多汇编代码和寄存器参与运算;而方法H则仅仅是几句相关的汇编,代码更简洁,效率更高。当然,由于编译器的不同,可能效率的差距不大,但是,以我目前遇到的MS C ,ARM C 来看,效率的差距还是不小。相关汇编代码就不在这里列举了。 运用这招需要注意的是,因为CPU的不同而产生的问题。比如说,在PC上用这招编写的程序,并在PC上调试通过,在移植到一个16位机平台上的时候,可能会产生代码隐患。所以只有在一定技术进阶的基础下才可以使用这招。
第4招:汇编嵌入
高效C语言编程的必杀技,第四招——嵌入汇编。
“在熟悉汇编语言的人眼里,C语言编写的程序都是垃圾”。这种说法虽然偏激了一些,但是却有它的道理。汇编语言是效率最高的计算机语言,但是,不可能靠着它来写一个操作系统吧?所以,为了获得程序的高效率,我们只好采用变通的方法 ——嵌入汇编,混合编程。
举例如下,将数组一赋值给数组二,要求每一字节都相符。 char string1[1024],string2[1024]; 方法I int I; for (I =0 ;I<1024;I++) *(string2 + I) = *(string1 + I) 方法J #ifdef _PC_ int I; for (I =0 ;I<1024;I++) *(string2 + I) = *(string1 + I); #else #ifdef _ARM_ __asm { MOV R0,string1 MOV R1,string2 MOV R2,#0 loop: LDMIA R0!, [R3-R11] STMIA R1!, [R3-R11] ADD R2,R2,#8 CMP R2, #400 BNE loop } #endif
方法I是最常见的方法,使用了1024次循环;方法J则根据平台不同做了区分,在ARM平台下,用嵌入汇编仅用128次循环就完成了同样的操作。这里有朋友会说,为什么不用标准的内存拷贝函数呢?这是因为在源数据里可能含有数据为0的字节,这样的话,标准库函数会提前结束而不会完成我们要求的操作。这个例程典型应用于LCD数据的拷贝过程。根据不同的CPU,熟练使用相应的嵌入汇编,可以大大提高程序执行的效率。
虽然是必杀技,但是如果轻易使用会付出惨重的代价。这是因为,使用了嵌入汇编,便限制了程序的可移植性,使程序在不同平台移植的过程中,卧虎藏龙,险象环生!同时该招数也与现代软件工程的思想相违背,只有在迫不得已的情况下才可以采用。切记,切记。
使用C语言进行高效率编程,我的体会仅此而已。在此以本文抛砖引玉,还请各位高手共同切磋。希望各位能给出更好的方法,大家一起提高我们的编程技巧。
一.系统环境 2 二.gSOAP的简要使用例子 2 三.图示说明 6 四.要注意的问题 6 五.参考文档 7 六.备注 7
一.系统环境 linux操作系统kernel2.4.2,安装gsoap2.6到目录/usr/local/gsoap 二.gSOAP的简要使用例子 下面是一个简单的例子,实现一个加法运算的WebService,具体功能是cli端输入num1和num2,server端返回一个num1和num2相加的结果sum。
1. 首先,我们需要做的是写一个函数声明文件,来定义接口函数ns__add,文件名字为add.h,内容如下:
//gsoap ns service name: add //gsoap ns service namespace: http://mail.263.net/add.wsdl //gsoap ns service location: http://mail.263.net //gsoap ns service executable: add.cgi //gsoap ns service encoding: encoded //gsoap ns schema namespace: urn:add
int ns__add( int num1, int num2, int* sum );
2. 然后我们需要创建文件Makefile,从而利用gsoapcpp2工具由add.h生成一些.xml文件、.c文件和.h文件,这些文件均为自动生成,Makefile的内容如下:
GSOAP_ROOT=/usr/local/gsoap WSNAME=add CC=g++ -g -DWITH_NONAMESPACES INCLUDE=-I $(GSOAP_ROOT) SERVER_OBJS=$(WSNAME)C.o $(WSNAME)Server.o stdsoap2.o CLIENT_OBJS=$(GSOAP_ROOT)/env/envC.o $(WSNAME)ClientLib.o stdsoap2.o ALL_OBJS=${WSNAME}server.o $(WSNAME)C.o $(WSNAME)Server.o ${WSNAME}test.o ${WSNAME}client.o $(WSNAME)ClientLib.o
#总的目标 all:server
${WSNAME}.wsdl:${WSNAME}.h $(GSOAP_ROOT)/soapcpp2 -p$(WSNAME) -i -n -c ${WSNAME}.h
stdsoap2.o:$(GSOAP_ROOT)/stdsoap2.c $(CC) -c $?
#编译一样生成规则的.o文件 $(ALL_OBJS):%.o:%.c $(CC) -c $? $(INCLUDE)
#编译服务器端 server:Makefile ${WSNAME}.wsdl ${WSNAME}server.o $(SERVER_OBJS) $(CC) ${WSNAME}server.o $(SERVER_OBJS) -o ${WSNAME}server
#编译客户端 client:Makefile ${WSNAME}.wsdl ${WSNAME}client.c ${WSNAME}test.c $(ALL_OBJS) stdsoap2.o $(CC) ${WSNAME}test.o ${WSNAME}client.o $(CLIENT_OBJS) -o ${WSNAME}test
cl: rm -f *.o *.xml *.a *.wsdl *.nsmap $(WSNAME)H.h $(WSNAME)C.c $(WSNAME)Server.c $(WSNAME)Client.c $(WSNAME)Stub.* $(WSNAME)$(WSNAME)Proxy.* $(WSNAME)$(WSNAME)Object.* $(WSNAME)ServerLib.c $(WSNAME)ClientLib.c $(WSNAME)server ns.xsd $(WSNAME)test
3.我们先来做一个server端,创建文件addserver.c文件,内容如下:
#include "addH.h" #include "add.nsmap"
int main(int argc, char **argv) { int m, s; /* master and slave sockets */ struct soap add_soap; soap_init(&add_soap); soap_set_namespaces(&add_soap, add_namespaces); if (argc < 2) { printf("usage: %s <server_port> \n", argv[0]); exit(1); } else { m = soap_bind(&add_soap, NULL, atoi(argv[1]), 100); if (m < 0) { soap_print_fault(&add_soap, stderr); exit(-1); } fprintf(stderr, "Socket connection successful: master socket = %d\n", m); for ( ; ; ) { s = soap_accept(&add_soap); if (s < 0) { soap_print_fault(&add_soap, stderr); exit(-1); } fprintf(stderr, "Socket connection successful: slave socket = %d\n", s); add_serve(&add_soap);//该句说明该server的服务 soap_end(&add_soap); } } return 0; } //server端的实现函数与add.h中声明的函数相同,但是多了一个当前的soap连接的参数 int ns__add(struct soap *add_soap, int num1, int num2, int *sum) { *sum = num1 + num2; return 0; }
4.让我们的server跑起来吧: shell>make shell>./addserver 8888 如果终端打印出“Socket connection successful: master socket = 3”,那么你的server已经在前台run起来了,应该是值得高兴的。 打开IE,键入http://本机IP:8888,显示XML,服务已经启动,终端打印出“Socket connection successful: slave socket = 4”,表示服务接收到了一次soap的连接。
5.让我们再来写个客户端(这个只是将soap的客户端函数封装一下,具体的调用参见下面的addtest.c),创建文件addclient.c,内容如下:
#include "addStub.h" #include "add.nsmap" /** * 传入参数:server:server的地址 * num1,num2:需要相加的数 * 传出参数:sum:num1和num2相加的结果 * 返回值:0为成功,其他为失败 */ int add( const char* server, int num1, int num2, int *sum ) { struct soap add_soap; int result = 0; soap_init(&add_soap); soap_set_namespaces(&add_soap, add_namespaces);
//该函数是客户端调用的主要函数,后面几个参数和add.h中声明的一样,前面多了3个参数,函数名是接口函数名ns__add前面加上soap_call_ soap_call_ns__add( &add_soap, server, "", num1, num2, sum ); if(add_soap.error) { printf("soap error:%d,%s,%s\n", add_soap.error, *soap_faultcode(&add_soap), *soap_faultstring(&add_soap) ); result = add_soap.error; } soap_end(&add_soap); soap_done(&add_soap); return result; }
6.我们最终写一个可以运行的客户端调用程序,创建文件addtest.c,内容如下:
#include <stdio.h> #include <stdlib.h>
int add(const char* server, int num1, int num2, int *sum);
int main(int argc, char **argv) { int result = -1; char* server="http://localhost:8888"; int num1 = 0; int num2 = 0; int sum = 0; if( argc < 3 ) { printf("usage: %s num1 num2 \n", argv[0]); exit(0); }
num1 = atoi(argv[1]); num2 = atoi(argv[2]);
result = add(server, num1, num2, &sum); if (result != 0) { printf("soap err,errcode = %d\n", result); } else { printf("%d+%d=%d\n", num1, num2, sum ); } return 0; }
7.让我们的client端和server端通讯 shell>make client shell>./addtest 7 8 当然,你的server应该还在run,这样得到输出结果7+8=15,好了,你成功完成了你的第一个C写的WebService,恭喜。 三.图示说明
四.要注意的问题 1. add.h文件前面的几句注释不能删除,为soapcpp2需要识别的标志 2. 接口函数的返回值只能是int,是soap调用的结果,一般通过soap.error来判断soap的连接情况,这个返回值没有用到。 3. 接口函数的最后一个参数为传出参数,如果需要传出多个参数,需要自己定义一个结构将返回项封装。 4. 在.h文件中不能include别的.h文件,可能不能生效,需要用到某些结构的时候需要在该文件中直接声明。 5. 如果客户端的调用不需要返回值,那么最后一个参数 五.参考文档 1.gsoap主页 http://gsoap2.sourceforge.net
2.跟我一起写Makefile http://dev.csdn.net/develop/article/20/20025.shtm
3.Web Services: A Technical Introduction(机械工业出版社) 六.备注 192.168.18.233和192.168.18.234的/usr/local/gsoap目录下的3个需要的文件及一个env目录,不是编译安装的,是在别的地方编译好了直接copy过来的(实际编译结果中还有wsdl2h工具及其他一些文件,但是我们的实际开发中只是用到了这3个文件及env目录)。因为时间仓促,本人还没有时间研究编译的问题,相关细节可以查看参考文档1。 在192.168.18.233的/home/weiqiong/soap/sample目录下及192.168.18.234的/tmp/soap/sample目录下有本文讲到的加法运算的例子。
|
全文结束 |
#ifndef MSG_H #define MSG_H //msgid #define LISTEN_THREAD 7 #define CENTER_THREAD 0 #define SEND_THREAD 2 #define REV_THREAD 3 #define TIME_THREAD 4 //lp #define EXIT 0 #define SEND_SGIP_SUBMIT 1 #define SEND_SGIP_BIND #define SEND_SGIP_R #define SEND_SGIP_UNBIND #define SEND_SGIP_UNBIND_R #define REV_SGIP_SOCKET //wp #define SEND_SUCCESS #define PACK_FAIL #define SEND_FAIL enum mgnt_cmd_type { event_login = 0, event_logout, event_sip_init_para, event_log_init_para, event_sip_clean, event_set_dtmf_mode, event_set_dhcp, event_set_pppoe, event_pstn_call_out, event_sip_call_out, event_answer_sipcall, event_release_sipcall, event_loadBMP_init, event_pstn_call_in=20, event_sip_call_in, event_remote_release_call, event_remote_establish_call, event_remote_cancelcall, event_login_return, event_remote_ignore,
event_set_pstn_ring, event_set_sip_ring, event_set_alarm_ring, event_set_ring_volume };
typedef struct msgbuf { long msgtype; unsigned long msgid; unsigned long lp; unsigned long wp; }MSGBuf, *pMSGBuf;
int vvMSGSend(long thread_id, unsigned long msgid, unsigned long lp, unsigned long wp); int vvMSGRecv(long thread_id, struct msgbuf *msg, int is_wait);
#ifndef _WINDOWS
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <unistd.h>
#define MSG_FILE_NAME "/rw/" //"/mnt/" #define MSG_FLAG (IPC_CREAT | 00666) //| IPC_EXCL
typedef struct sendMsg { int sd; void *content; }SendMsg, *pSendMsg;
#endif
#endif //MSG_H
#include "vvmsg.h" #include <ps_log.h>
#ifndef _WINDOWS #include <phone_Interface.h> #include <pthread.h> #include <basegdi.h> #include <keyboard.h> //#include "hash.h" extern pthread_t g_incomingthread; //extern hash_table table; #endif
int vvMSGSend(long thread_id, unsigned long msgid, unsigned long lp, unsigned long wp) { struct msgbuf bmsg; #ifndef _WINDOWS key_t key;
int msg_id; bmsg.msgtype = thread_id; bmsg.msgid = msgid; bmsg.lp = lp; bmsg.wp = wp;
if((key = ftok(MSG_FILE_NAME,'a')) == -1) { return -1; }
if((msg_id = msgget(key,MSG_FLAG)) == -1) { return -1; }
if (msgsnd(msg_id, &bmsg, sizeof(struct msgbuf), IPC_NOWAIT) == -1) { return -1; } #endif return 1;
}
int vvMSGRecv(long thread_id, struct msgbuf *msg, int is_wait) { #ifndef _WINDOWS key_t key; int msg_id; if((key = ftok(MSG_FILE_NAME,'a')) == -1) { printf("Recv msg error 1!\n"); return -1; } if((msg_id = msgget(key,MSG_FLAG)) == -1) { printf("Recv msg error 2!\n"); return -1; } if (is_wait != 1) { if (msgrcv(msg_id, msg, sizeof(struct msgbuf), thread_id, IPC_NOWAIT) == -1) { printf("Recv msg error 3!\n"); return -1; } } else { if (msgrcv(msg_id, msg, sizeof(struct msgbuf), thread_id, 0) == -1) { //printf("Recv msg error 4!\n"); return -1; } } #endif return 1;
}
void *skype_thread_start(void *arg) { #ifndef _WINDOWS MSGBuf msg; pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,0);//设置线程属性 pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,0); for (;;) { pthread_testcancel();//设置取消点 if (vvMSGRecv((long)g_incomingthread, &msg, 1) == -1) continue; // analysis the message switch (msg.msgid) { ps_show_str(log_DEBUG, "vvmsg event!!!!!!!!!!!!!!!%d\r\n", msg.msgid); case event_login: { userLogin(); } break; case event_logout: { userLogout(); } case event_sip_clean: { SipClean(); } break; case event_sip_init_para: { ps_show_str(log_DEBUG, "event before################UpdateSipInitPara\r\n"); UpdateSipInitPara(); ps_show_str(log_DEBUG, "event after##################UpdateSipInitPara\r\n"); } break; case event_log_init_para: { UpdateLogInitPara(); } break; case event_set_dtmf_mode: { int i = (int)msg.lp; ps_show_str(log_DEBUG, "event_set_dtmf_mode########################%d\r\n", i); SetDTMFMode(i); } break; case event_set_dhcp: { SetDHCP(); } break; case event_set_pppoe: { SetPPPOE(); } break;
case event_pstn_call_out: { pstncall((char*)msg.lp); } break; case event_sip_call_out: { sipcall((char*)msg.lp); } break;
case event_answer_sipcall: { callmgr_answercall((LINE_ID_T *)msg.lp); } break; case event_release_sipcall: { callmgr_releasecall((LINE_ID_T *)msg.lp); } break; case event_loadBMP_init: { CreateSysBmp(); } break;
case event_pstn_call_in: { LINE_ID_T *line = (LINE_ID_T *)msg.wp; sipcome_create(line); } break; case event_sip_call_in: { LINE_ID_T *line = (LINE_ID_T *)msg.wp; sipcome_create(line); } break; case event_remote_establish_call: { LINE_ID_T *line = (LINE_ID_T *)msg.wp; pstnchat_create(line); if(g_Hwnd[HWND_CALLOUT]!=0) calling_destroy(g_Hwnd[HWND_CALLOUT]); } break; case event_remote_cancelcall: { if(g_Hwnd[HWND_CALLIN]!=0) SendMessage(g_Hwnd[HWND_CALLIN],MSG_KEYDOWN,KEY_SW_RSK,0); } break; case event_remote_release_call: { if(g_Hwnd[HWND_CHAT]!=0) SendMessage(g_Hwnd[HWND_CHAT],MSG_KEYDOWN,KEY_SW_RSK,0); } break; case event_login_return: { printf("sfds0000000000000000000000000000000dssssssss^^^^^^^^^^^^^^^^^\r\n"); if(g_Hwnd[HWND_MAINSCREEN]!=0) { UpdateWindow(g_Hwnd[HWND_MAINSCREEN],1); // SetFocusChild(g_Hwnd[HWND_MAINSCREEN]); // ShowWindow(g_Hwnd[HWND_MAINSCREEN], SW_SHOW); } } break; case event_remote_ignore: { if(g_Hwnd[HWND_CALLOUT]!=0) SendMessage(g_Hwnd[HWND_CALLOUT],MSG_KEYDOWN,KEY_SW_RSK,0); } break; case event_set_pstn_ring: { SetPstnRing((int)msg.lp); } break; case event_set_sip_ring: { SetSipRing((int)msg.lp); } break; case event_set_ring_volume: { SetRingVolume((int)msg.lp); } break; }
} #endif }
附(创建线程):if (pthread_create(&g_incomingthread, NULL, skype_thread_start, NULL)) return -1;
一个简化的问题示例 链表的难点在于必须复制链表处理函数来处理不同的对象,即便逻辑是完全相同的。例如两个结构类似的链表: struct Struct_Object_A
{
int a;
int b;
Struct_Object_A *next;
} OBJECT_A;
typedef struct Struct_Object_B
{
int a;
int b;
int c;
Struct_Object_B *next;
} OBJECT_B; |
上面定义的两个结构只有很小的一点差别。OBJECT_B 和 OBJECT_A 之间只差一个整型变量。但是,在编译器看来,它们仍然是非常不同的。必须为存储在链表中的每个对象复制用来添加、删除和搜索链表的函数。为了解决这个问题,可以使用具有全部三个变量的一个联合或结构,其中整数 c 并不是在所有的情况下都要使用。这可能变得非常复杂,并会形成不良的编程风格。
C 代码解决方案:虚拟链表
此问题更好的解决方案之一是虚拟链表。虚拟链表是只包含链表指针的链表。对象存储在链表结构背后。这一点是这样实现的,首先为链表节点分配内存,接着为对象分配内存,然后将这块内存分配给链表节点指针,如下所示:
虚拟链表结构的一种实现
typedef struct liststruct
{
liststruct *next;
} LIST, *pLIST;
pLIST Head = NULL;
pLIST AddToList( pLIST Head,
void * data, size_t datasize )
{
pLIST newlist=NULL;
void *p;
// 分配节点内存和数据内存
newlist = (pLIST) malloc
( datasize + sizeof( LIST ) );
// 为这块数据缓冲区指定一个指针
p = (void *)( newlist + 1 );
// 复制数据
memcpy( p, data, datasize );
// 将这个节点指定给链表的表头
if( Head )
{
newlist->next = Head;
}
else
newlist->next = NULL;
Head = newlist;
return Head;
} |
链表节点现在建立在数据值副本的基本之上。这个版本能很好地处理标量值,但不能处理带有用 malloc 或 new 分配的元素的对象。要处理这些对象,LIST 结构需要包含一个一般的解除函数指针,这个指针可用来在将节点从链表中删除并解除它之前释放内存(或者关闭文件,或者调用关闭方法)。
一个带有解除函数的链表
typedef void (*ListNodeDestructor)( void * );
typedef struct liststruct
{
ListNodeDestructor DestructFunc;
liststruct *next;
} LIST, *pLIST;
pLIST AddToList( pLIST Head, void * data,
size_t datasize,
ListNodeDestructor Destructor )
{
pLIST newlist=NULL;
void *p;
// 分配节点内存和数据内存
newlist = (pLIST) malloc
( datasize + sizeof( LIST ) );
// 为这块数据缓冲区指定一个指针
p = (void *)( newlist + 1 );
// 复制数据
memcpy( p, data, datasize );
newlist->DestructFunc = Destructor;
// 将这个节点指定给链表的表头
if( Head )
{
newlist->next = Head;
}
else
newlist->next = NULL;
Head = newlist;
return Head;
}
void DeleteList( pLIST Head )
{
pLIST Next;
while( Head )
{
Next = Head->next;
Head->DestructFunc(
(void *) Head );
free( Head );
Head = Next;
}
}
typedef struct ListDataStruct
{
LPSTR p;
} LIST_DATA, *pLIST_DATA;
void ListDataDestructor( void *p )
{
// 对节点指针进行类型转换
pLIST pl = (pLIST)p;
// 对数据指针进行类型转换
pLIST_DATA pLD = (pLIST_DATA)
( pl + 1 );
delete pLD->p;
}
pLIST Head = NULL;
void TestList()
{
pLIST_DATA d = new LIST_DATA;
d->p = new char[24];
strcpy( d->p, "Hello" );
Head = AddToList( Head, (void *) d,
sizeof( pLIST_DATA ),
ListDataDestructor );
// 该对象已被复制,现在删除原来的对象
delete d;
d = new LIST_DATA;
d->p = new char[24];
strcpy( d->p, "World" );
Head = AddToList( Head, (void *) d,
sizeof( pLIST_DATA ),
ListDataDestructor );
delete d;
// 释放链表
DeleteList( Head );
} |
在每个链表节点中包含同一个解除函数的同一个指针似乎是浪费内存空间。确实如此,但只有链表始终包含相同的对象才属于这种情况。按这种方式编写链表允许您将任何对象放在链表中的任何位置。大多数链表函数要求对象总是相同的类型或类。
虚拟链表则无此要求。它所需要的只是将对象彼此区分开的一种方法。要实现这一点,您既可以检测解除函数指针的值,也可以在链表中所用的全部结构前添加一个类型值并对它进行检测。
当然,如果要将链表编写为一个 C++ 类,则对指向解除函数的指针的设置和存储只能进行一次。
C++ 解决方案:类链表
本解决方案将 CList 类定义为从 LIST 结构导出的一个类,它通过存储解除函数的单个值来处理单个存储类型。请注意添加的 GetCurrentData() 函数,该函数完成从链表节点指针到数据偏移指针的数学转换。一个虚拟链表对象
// 定义解除函数指针
typedef void (*ListNodeDestructor)
( void * );
// 未添加解除函数指针的链表
typedef struct ndliststruct
{
ndliststruct *next;
} ND_LIST, *pND_LIST;
// 定义处理一种数据类型的链表类
class CList : public ND_LIST
{
public:
CList(ListNodeDestructor);
~CList();
pND_LIST AddToList
( void * data, size_t datasize );
void *GetCurrentData();
void DeleteList( pND_LIST Head );
private:
pND_LIST m_HeadOfList;
pND_LIST m_CurrentNode;
ListNodeDestructor
m_DestructFunc;
};
// 用正确的起始值构造这个链表对象
CList::CList(ListNodeDestructor Destructor)
: m_HeadOfList(NULL),
m_CurrentNode(NULL)
{
m_DestructFunc = Destructor;
}
// 在解除对象以后删除链表
CList::~CList()
{
DeleteList(m_HeadOfList);
}
// 向链表中添加一个新节点
pND_LIST CList::AddToList
( void * data, size_t datasize )
{
pND_LIST newlist=NULL;
void *p;
// 分配节点内存和数据内存
newlist = (pND_LIST) malloc
( datasize + sizeof( ND_LIST ) );
// 为这块数据缓冲区指定一个指针
p = (void *)( newlist + 1 );
// 复制数据
memcpy( p, data, datasize );
// 将这个节点指定给链表的表头
if( m_HeadOfList )
{
newlist->next = m_HeadOfList;
}
else
newlist->next = NULL;
m_HeadOfList = newlist;
return m_HeadOfList;
}
// 将当前的节点数据作为 void 类型返回,
以便调用函数能够将它转换为任何类型
void * CList::GetCurrentData()
{
return (void *)(m_CurrentNode+1);
}
// 删除已分配的链表
void CList::DeleteList( pND_LIST Head )
{
pND_LIST Next;
while( Head )
{
Next = Head->next;
m_DestructFunc( (void *) Head );
free( Head );
Head = Next;
}
}
// 创建一个要在链表中创建和存储的结构
typedef struct ListDataStruct
{
LPSTR p;
} LIST_DATA, *pND_LIST_DATA;
// 定义标准解除函数
void ClassListDataDestructor( void *p )
{
// 对节点指针进行类型转换
pND_LIST pl = (pND_LIST)p;
// 对数据指针进行类型转换
pND_LIST_DATA pLD = (pND_LIST_DATA)
( pl + 1 );
delete pLD->p;
}
// 测试上面的代码
void MyCListClassTest()
{
// 创建链表类
CList* pA_List_of_Data =
new CList(ClassListDataDestructor);
// 创建数据对象
pND_LIST_DATA d = new LIST_DATA;
d->p = new char[24];
strcpy( d->p, "Hello" );
// 创建指向链表顶部的局部指针
pND_LIST Head = NULL;
//向链表中添加一些数据
Head = pA_List_of_Data->AddToList
( (void *) d,
sizeof( pND_LIST_DATA ) );
// 该对象已被复制,现在删除原来的对象
delete d;
// 确认它已被存储
char * p = ((pND_LIST_DATA) pA_List_of_Data->GetCurrentData())->p;
d = new LIST_DATA;
d->p = new char[24];
strcpy( d->p, "World" );
Head = pA_List_of_Data->AddToList
( (void *) d, sizeof( pND_LIST_DATA ) );
// 该对象已被复制,现在删除原来的对象
delete d;
// 确认它已被存储
p = ((pND_LIST_DATA)
pA_List_of_Data->GetCurrentData())->p;
// 删除链表类,析构函数将删除链表
delete pA_List_of_Data;
} |
小结
从前面的讨论来看,似乎仅编写一个简单的链表就要做大量的工作,但这只须进行一次。很容易将这段代码扩充为一个处理排序、搜索以及各种其他任务的 C++ 类,并且这个类可以处理任何数据对象或类(在一个项目中,它处理大约二十个不同的对象)。您永远不必重新编写这段代码。
|