C++编程思想第二篇.docx

上传人:b****3 文档编号:11684134 上传时间:2023-06-02 格式:DOCX 页数:47 大小:347.81KB
下载 相关 举报
C++编程思想第二篇.docx_第1页
第1页 / 共47页
C++编程思想第二篇.docx_第2页
第2页 / 共47页
C++编程思想第二篇.docx_第3页
第3页 / 共47页
C++编程思想第二篇.docx_第4页
第4页 / 共47页
C++编程思想第二篇.docx_第5页
第5页 / 共47页
C++编程思想第二篇.docx_第6页
第6页 / 共47页
C++编程思想第二篇.docx_第7页
第7页 / 共47页
C++编程思想第二篇.docx_第8页
第8页 / 共47页
C++编程思想第二篇.docx_第9页
第9页 / 共47页
C++编程思想第二篇.docx_第10页
第10页 / 共47页
C++编程思想第二篇.docx_第11页
第11页 / 共47页
C++编程思想第二篇.docx_第12页
第12页 / 共47页
C++编程思想第二篇.docx_第13页
第13页 / 共47页
C++编程思想第二篇.docx_第14页
第14页 / 共47页
C++编程思想第二篇.docx_第15页
第15页 / 共47页
C++编程思想第二篇.docx_第16页
第16页 / 共47页
C++编程思想第二篇.docx_第17页
第17页 / 共47页
C++编程思想第二篇.docx_第18页
第18页 / 共47页
C++编程思想第二篇.docx_第19页
第19页 / 共47页
C++编程思想第二篇.docx_第20页
第20页 / 共47页
亲,该文档总共47页,到这儿已超出免费预览范围,如果喜欢就下载吧!
下载资源
资源描述

C++编程思想第二篇.docx

《C++编程思想第二篇.docx》由会员分享,可在线阅读,更多相关《C++编程思想第二篇.docx(47页珍藏版)》请在冰点文库上搜索。

C++编程思想第二篇.docx

C++编程思想第二篇

下载

 

第2章数据抽象

下载

 

C++是一个能提高效率的工具。

为什么我们还要努力(这是努力,不管我们试图做的转变

多么容易)使我们从已经熟悉且效率高的语言(在这里是C语言)转到另一种新的语言上?

且使用这种新语言,我们会在确实掌握它之前的一段时间内降低效率。

这归因于我们确信通过

使用新工具将会得到更大的好处。

用程序设计术语,多产意味着用较少的人在较少的时间内完成更复杂和更重要的程序。

而,选择语言时确实还有其他问题,例如运行效率(该语言的性质引起代码臃肿吗?

)、安全性

(该语言能有助于我们的程序做我们计划的事情并具有很强的纠错能力吗?

)、可维护性(该语

言能帮助我们创建易理解、易修改和易扩展的代码吗?

)。

这些都是本书要考察的重要因素。

简单地讲,提高生产效率,意味着本应花费三个人一星期的程序,现在只需要花费一个人

一两天的时间。

这会涉及到经济学的多层次问题。

生产效率提高了,我们很高兴,因为我们正

在建造的东西功能将会更强;我们的客户(或老板)很高兴,因为产品生产又快,用人又少;

我们的顾客很高兴,因为他们得到的产品更便宜。

而极大提高效率的唯一办法是使用其他人的

代码,即使用库。

库,简单地说就是一些人已经写的代码,按某种方式包装在一起。

通常,最小的包是带有

扩展名如LIB的文件和向编译器声明库中有什么的一个或多个头文件。

连接器知道如何在LIB

文件中搜索和提取相应的已编译的代码。

但是,这只是提供库的一种方法。

在跨越多种体系结

构的平台上,例如UNIX,通常,提供库的最明智的方法是用源代码,这样在新的目标机上它

能被重新编译。

而在微软Windows上,动态连接库是最明智的方法,这使得我们能够利用新发

布的DDL经常修改我们的程序,我们的库函数销售商可能已经将新DDL发送给我们了。

所以,库大概是改进效率的最重要的方法。

C++的主要设计目标之一是使库容易使用。

意味着,在C中使用库有困难。

懂得这一点就对C++设计有了初步的了解,从而对如何使用它

有了更深入的认识。

2.1声明与定义

首先,必须知道“声明”和“定义”之间的区别,因为这两个术语在全书中会被确切地使

用。

“声明”向计算机介绍名字,它说,“这个名字是什么意思”。

而“定义”为这个名字分配

存储空间。

无论涉及到变量时还是函数时含义都一样。

无论在哪种情况下,编译器都在“定义”

处分配存储空间。

对于变量,编译器确定这个变量占多少存储单元,并在内存中产生存放它们

的空间。

对于函数,编译器产生代码,并为之分配存储空间。

函数的存储空间中有一个由使用

不带参数表或带地址操作符的函数名产生的指针。

定义也可以是声明。

如果该编译器还没有看到过名字A,程序员定义intA,则编译器马上

为这个名字分配存储地址。

声明常常使用于extern关键字。

如果我们只是声明变量而不是定义它,则要求使用extern。

对于函数声明,extern是可选的,不带函数体的函数名连同参数表或返回值,自动地作为一个

声明。

 

下载

第2章数据抽象23

 

函数原型包括关于参数类型和返回值的全部信息。

intf(float,char);是一个函数原型,因为

它不仅介绍f这个函数的名字,而且告诉编译器这个函数有什么样的参数和返回值,使得编译

器能对参数和返回值做适当的处理。

C++要求必须写出函数原型,因为它增加了一个重要的安

全层。

下面是一些声明的例子。

 

在函数声明时,参数名可给出也可不给出。

而在定义时,它们是必需的。

这在C语言中确

实如此,但在C++中并不一定。

全书中,我们会注意到,每个文件的第一行是一个注释,它以注释符开始,后面跟冒号。

这是我用的技术,可以利用诸如“grep”和“awk”这样的文本处理工具从代码文件中提取信

息。

在第一行中还包含有文件名,因此能在文本和其他文件中查阅这个文件,本书的代码磁盘

中也很容易定义这个文件。

2.2一个袖珍C库

一个小型库通常以一组函数开始,但是,已经用过别的C库的程序员知道,这里通常有更

多的东西,有比行为、动作和函数更多的东西。

还有一些特性(颜色、重量、纹理、亮度),

它们都由数据表示。

在C语言中,当我们处理一组特性时,可以方便地把它们放在一起,形成

一个struct。

特别是,如果我们想表示我们的问题空间中的多个类似的事情,则可以对每件事

情创建这个struct的一个变量。

这样,在大多数C库中都有一组struct和一组活动在这些struct上的函数。

现在看一个这样

的例子。

假设有一个程序设计工具,当创建时它的表现像一个数组,但它的长度能在运行时建

立。

我称它为stash。

 

24C++编程思想

下载

 

在结构内部需要引用这个结构时可以使用这个struct的别名,例如,创建一个链表,需要

指向下一个struct的指针。

在C库中,几乎可以在整个库的每个结构上看到如上所示的typedef。

这样做使得我们能把struct作为一个新类型处理,并且可以定义这个struct的变量,例如:

stashA,B,C;

注意,这些函数声明用标准C风格的函数原型,标准C风格比“老”C风格更安全和更

清楚。

我们不仅介绍了函数名,而且还告诉编译器参数表和返回值的形式。

storage指针是一个unsignedchar*。

这是C编译器支持的最小的存储片,尽管在某些机器

上它可能与最大的一般大,这依赖于具体实现。

人们可能认为,因为stash被设计用于存放任何

类型的变量,所以void*在这里应当更合适。

然而,我们的目的并不是把它当作某个未知类型

的块处理,而是作为连续的字节块。

这个执行文件的源代码(如果我们买了一个商品化的库,我们可能得到的只是编译好的

OBJ或LIB或DDL等)如下:

 

下载

第2章数据抽象25

 

注意本地的#include风格,尽管这个头文件在本地目录下,但仍然以相对于本书的根目录

给出。

这样做,可以创建不同于这本书根目录的另外的目录,很容易拷贝文件到这个新目录下

去实验,而不必担心改变#include中的路径。

 

26C++编程思想

下载

 

initialize()完成对structstash的必要的设置,即设置内部变量为适当的值。

最初,设置

storage指针为零,设置size指示器也为零,表示初始存储未被分配。

add()函数在stash的下一个可用位子上插入一个元素。

首先,它检查是否有可用空间,如

果没有,它就用后面介绍的inflate()函数扩展存储空间。

因为编译器并不知道被存放的特定变量的类型(函数返回的都是void*),所以我们不能只

做赋值,虽然这的确是很方便的事情。

代之,我们必须用标准C库函数memcpy()一个字节一

个字节地拷贝这个变量,第一个参数是memcpy()开始拷贝字节的目的地址,由下面表达式产

生:

&(S->storage[S->next*S->size])

它指示从存储块开始的第next个可用单元结束。

这个数实际上就是已经用过的单元号加一

的计数,它必须乘上每个单元拥有的字节数,产生按字节计算的偏移量。

这不产生地址,而是

产生处于这个地址的字节,为了产生地址,必须使用地址操作符&。

memcpy()的第二和第三个参数分别是被拷贝变量的开始地址和要拷贝的字节数。

next计数

器加一,并返回被存值的索引。

这样,程序员可以在后面调用fetch()时用它来取得这个元素。

fetch()首先看索引是否越界,如果没有越界,返回所希望的变量地址,地址的计算采用与

add()中相同的方法。

对于有经验的C程序员count()乍看上去可能有点奇怪,它好像是自找麻烦,做手工很容易

做的事情。

例如,如果我们有一个structstash,例假设称为intStash,那么通过用intStash.next

找出它已经有多少个元素的方法似乎更直接,而不是去做count(&intStash)函数调用(它有更多

的花费)。

但是,如果我们想改变stash的内部表示和计数计算方法,那么这个函数调用接口就

允许必要的灵活性。

并且,很多程序员不会为找出库的“更好”的设计而操心。

如果他们能着

眼于struct和直接取next的值,那么可能不经允许就改变next。

是不是能有一些方法使得库设计

者能更好地控制像这样的问题呢?

(是的,这是可预见的)。

动态存储分配

我们不可能预先知道一个stash需要的最大存储量是多少,所以由storage指向的内存从堆中

分配。

堆是很大的内存块,用以在运行时分一些小单元。

在我们写程序时,如果我们还不知道

所需内存的大小,就可以使用堆。

这样,我们可以直到运行时才知道需要存放200个airplane变

量,而不仅是20个。

动态内存分配函数是标准C库的一部分,包括malloc()、calloc()、

realloc()和free()。

inflate()函数使用realloc()为stash得到更大的空间块。

realloc()把已经分配而又希望重分配

的存储单元首地址作为它的第一个参数(如果这个参数为零,例如initialize()刚刚被调用时,

realloc()分配一个新块)。

第二个参数是这个块新的长度,如果这个长度比原来的小,这个块

将不需要作拷贝,简单地告诉堆管理器剩下的空间是空闲的。

如果这个长度比原来的大,在堆

中没有足够的相临空间,所以要分配新块,并且要拷贝内存。

assert()检查以确信这个操作成

功。

(如果这个堆用光了,malloc()、calloc()和realloc()都返回零。

注意,C堆管理器相当重要,它给出内存块,对它们使用free()时就回收它们。

没有对堆

进行合并的工具,如果能合并就可以提供更大的空闲块。

如果程序多次分配和释放堆存储,最

终会导致这个堆有大量的空闲块,但没有足够大且连续的空间能满足我们对内存分配的需要。

但是,如果用堆合并器移动内存块,又会使得指针保存的不是相应的值。

一些操作环境,例如

MicrosoftWindows有内置的合并,但它们要求我们使用专门的内存句柄(它们能临时地翻转为

 

下载

指针,锁住内存后,堆压紧器不能移动它),而不是使用指针。

第2章数据抽象27

assert()是在ASSERT.H中的预处理宏。

assert()取单个参数,它可以是能求得真或假值的任

何表达式。

这个宏表示:

“我断言这是真的,如果不是,这个程序将打印出错信息,然后退出。

不再调试时,我们可以用一个标志使得这个断言被忽略。

在调试期间,这是非常清楚和简便的

测试错误的方法。

不过,在出错处理时,它有点生硬:

“对不起,请进行控制。

我们的C程序

对一个断言失败,并且跳出去。

”在第17章中,我们将会看到,C++是如何用出错处理来处理

重要错误的。

编译时,如果在栈上创建一个变量,那么这个变量的存储单元由编译器自动开辟和释放。

编译器准确地知道需要多少存储容量,根据这个变量的活动范围知道这个变量的生命期。

而对

动态内存分配,编译器不知道需要多少存储单元,不知道它们的生命期,不能自动清除。

因此,

程序员应负责用free()释放这块存储,free()告诉堆管理器,这个存储可以被下一次调用的

malloc()、calloc()或realloc()重用。

合理的方法是使用库中cleanup()函数,因为在这里,该函

数做所有类似的事情。

为了测试这个库,让我们创建两个stash。

第一个存放int,第二个存放80个字符的数组(我

们可以把它看作新数据类型)。

 

28C++编程思想

下载

 

在main()的开头定义了一些变量,其中包括两个stash结构变量,当然。

稍后我们必须在这

个程序块的对它们初始化。

库的问题之一是我们必须向用户认真地说明初始化和清除函数的重

要性,如果这些函数未被调用,就会出现许多问题。

遗憾的是,用户不总是记得初始化和清除

是必须的。

他们只知道他们想完成什么,并不关心我们反复说的:

“喂,等一等,您必须首先

做这件事。

”一些用户甚至认为初始化这些元素是自动完成的。

的确没有机制能防止这种情况

的发生(只有多预示)。

intStash适合于整型,stringStash适合于字符串。

这些字符串是通过打开源代码文件

LIBTEST.C和把这些行读到stringStash而产生的。

注意一些有趣的地方:

标准C库函数打开

和读文件所使用的技术与在stash中使用的技术类似。

fopen()返回一个指向FILEstruct的指针,

这个FILEstruct是在堆上创建的,并且能将这个指针传给涉及到这个文件的任何函数。

(在这

里是fgets())。

fclose()所做的事情之一是向堆释放这个FILEstruct。

一旦我们开始注意到这种

模式的,包含着struct和有关函数的C库后,我们就能到处看到它。

装载了这两个stash之后,可以打印出它们。

intStach的打印用一个for循环,用count()确定

它的限度。

stringStash的打印用一个while语句,如果fetch()返回零则表示打印越界,这时跳出循

环。

在我们考虑有关C库创建的问题之前,应当了解另外一些事情(我们可能已经知道这些,

因为我们是C程序员)。

第一,虽然这里用头文件,而且实际上用得很好,但它们不是必须的。

在C中可能会调用还未声明的函数。

好的编译器会告诫我们应当首先声明函数,但不强迫这样

做。

这是很危险的,因为编译器能假设以int参数调用的函数有包含int的参数表,并据此处理

它,这是很难发现的错误。

注意,头文件LIB.H必须包含在涉及stash的所有文件中,因为编译器不可能猜出这个结

构是什么样子的。

它能猜出函数,即便它可能不应当这样,但这是C的一部分。

每个独立的C文件就是一个处理单元。

就是说,编译器在每个处理单元上单独运行,而编

译器在运行时只知道这个单元。

这样,用包含头文件提供信息是相当重要的,因为它为编译器

提供了对程序其他部分的理解。

在头文件中的声明特别重要,因为无论是在哪里包含这个头文

件,编译器都会知道要做什么。

例如,若在一个头文件中声明voidfoo(float),编译器就会知道,

如果我们用整型参数调用它,它会自动把int转变为float。

如果没有声明,这个编译器就会简单

地猜测,有一个函数存在,而不会做这个转变。

对于每个处理单元,编译器创建一个目标文件,带有扩展名.o或.obj或类似的名字。

须再用连接器将这些目标文件连同必要的启动代码连接成可执行程序。

在连接期间,所有的外

部引用都必须确定。

例如在LIBTEST.C中,声明并使用函数initialize()和fetch(),(也就是,

编译器被告知它们像什么,)但未定义。

它们是在LIB.C中定义的,这样,在LIBTEST.C中的这

些调用都是外部引用。

当连接器将目标文件连接在一起时,它找出未确定的引用并寻找这些引

 

下载

用对应的实际地址,用这些地址替换这些外部引用。

第2章数据抽象29

重要的是认识到,在C中,引用就是函数名,通常在它们前面加上下划线。

所以,连接器

所要做的就是让被调用的函数名与在目标文件中的函数体匹配起来。

如果我们偶然做了一个调

用,编译器解释为foo(int),而在其他目标文件中有foo(float)的函数体,连接器将认为一个

_foo在一处而另一个_foo在另一处,它会认为这都是对的。

在调用foo()处将一个int放进栈中,

而foo()函数体期望在这个栈中的是一个float。

如果这个函数只读这个值而不对它写,尚不会

破坏这个栈。

但从这个栈中读出的float值可能会有另外的某种理解。

这是最坏的情况,因为很

难发现这个错误。

2.3放在一起:

项目创建工具

分别编译时(把代码分成多个处理单元),我们需要一些方法去编译所有的代码,并告诉

连接器把它们与相应的库和启动代码放在一起,形成一个可执行文件。

大部分编译器允许用一

条命令行语句。

例如编译器命名为cpp,可写:

cpplibtest.clib.c

这个方法带来的问题是,编译器必须首先编译每个处理单元,而不管这个单元是否需要重

建。

虽然我们只改变了一个文件,但却需要耗费时间来对项目中的每一个文件进行重新编译。

对这个问题的第一种解决办法,已由UNIX(C的诞生地)提出,是一个被称为make的

程序。

make比较源代码文件的日期和目标文件的日期,如果目标文件的日期比源代码文件的

早,make就调用这个编译器对这个单元进行处理。

我们可以从编译器文档[1]中学到更多的关

于make的知识。

make是有用的,但学习和配置makefile有点乏味。

makefile是描述项目中所有文件之间

关系的文本文件。

因此,编译器销售商发行它们自己的项目创建工具。

这些工具向我们询问项

目中有哪些处理单元,并确定它们的关系。

这些关系有些类似于makefile文件,通常称为项目

文件。

程序设计环境维护这个文件,所以我们不必为它担心。

项目文件的配置和使用随系统而

异,假设我们正在使用我们选择的项目创建工具来创建程序,我们会发现如何使用它们的相应

文档(虽然由编译器销售商提供的项目文件工具通常是非常简单的,可以不费劲地学会它们)。

文件名

应当注意的另一个问题是文件命名。

在C中,惯例是以扩展名.h命名头文件(包含声明),

以.c命名实现文件(它引起内存分配和代码生成)。

C++继续演化。

它首先是在Unix上开发的,

这个操作系统能识别文件名的大小写。

原来的文件名简单地变为大写,形成.H和.C版本。

样对于不区分大小写的操作系统,例如MS-DOS,就行不通了。

DOSC++厂商对于头文件和实

现文件分别使用扩展名.hxx和.cxx。

后来,有人分析出,需要不同扩展名的唯一原因是使得编

译器能确定编译C还是C++文件。

因为编译器不直接编译头文件,所以只有实现文件的扩展名需

要改变。

现在人们已经习惯于在各种系统上对于实现文件都使用.cpp而对于头文件使用.h。

2.4什么是非正常

我们通常有特别的适应能力,即使是对本不应该适应的事情。

stash库的风格对于C程序

员已经是常用的了,但是如果观察它一会儿,就会发现它是相当笨拙的。

因为在使用它时,必

[1]参看由作者编写的C++Inside&Out,(Osborne/McGraw-Hill,1993)。

 

30C++编程思想

下载

 

须向这个库中的每一个函数传递这个结构的地址。

而当读这些代码时,这种库机制会和函数调

用的含义相混淆,试图理解这些代码时也会引起混乱。

然而在C中,使用库的最大的障碍是名字冲突问题。

C对于函数使用单个名字空间,所以

当连接器找一个函数名时,它在一个单独的主表中查找,而当编译器在单个处理单元上工作时,

它只能对带有某些特定名字的函数进行处理工作。

现在假设要支持从不同的厂商购买的两个库,并且每个库都有一个必须被初始化和清除的

结构。

两个厂商都认为initialize()和cleanup()是好名字。

如果在某个处理单元中同时包含了这

两个库文件,C编译器怎么办呢?

幸好,标准C给出一个出错,告诉在声明函数的两个不同的

参数表中类型不匹配。

即便不把它们包含在同一个处理单元中,连接器也会有问题。

好的连接

器会发现这里有名字冲突,但有些编译器仅仅通过查找目标文件表,按照在连接表中给出的次

序,取第一个找到的函数名(实际上,这可以看作是一种功能,因为可以用自己的版本替换一

个库函数)。

无论哪种情况,都不允许使用包含具有同名函数的两个库。

为了解决这个问题,C库厂商

常常会在它们的所有函数名前加上一个独特字符串。

所以,initialize()和cleanup()可能变为

stash_initialize()和stash_cleanup()。

这是合乎逻辑的,因为它“分解了”这个struct的名字,

而该函数以这样的函数名对这个struct操作。

现在,迈向C++第一步的时候到了。

我们知道,struct内部的变量名不会与全局变量名冲

突。

而当一些函数在特定struct上运算时,为什么不把这一优点扩展到这些函数名上呢?

也就

是,为什么不让函数是struct的成员呢?

2.5基本对象

C++的第一步正是这样。

函数可以放在结构内部,作为“成员函数”。

在这里stash是:

 

首先注意到的可能是新的注释文法//。

这是对C风格注释的补充,C原来的注释文法仍然

能用。

C++注释直到该行的结尾,它有时非常方便。

另外,我们会在这本书中,在文件的第一

行的//之后加一个冒号,后面跟的是这个文件名和简要描述。

这就可以了解代码所在的文件。

并且还可以很容易地用本书列出的名字从电子源代码中识别出这个文件。

 

下载

第2章数据抽象31

 

其次,注意到这里没有typedef。

在C++中,编译器不要求我们创建typedef,而是直接把结

构名转变为这个程序的新类型名(就像int、char、float、double一样)。

stash的用法仍然相同。

所有的数据成员与以前完全相同,但现在这些函数在struct的内部了。

另外,注意到,对

应于这个库的C版本中第一个参数已经去掉了。

在C++中,不是硬性传递这个结构的地址作为

在这个结构上运算的所有函数的一个参数,而是编译器背地里做了这件事。

现在,这些函数仅

有的参数与这些函数所做的事情有关,而不与这些函数运算的机制有关。

认识到这些函数代码与在C库中的那些同样有效,是很重要的。

参数的个数是相同的(即

便我们还没有看到这个结构地址被传进来,实际上它在这里),并且每个函数只有一个函数体。

正因为如此,写:

stashA,B,C;

并不意味着每个变量得到不同的add()函数。

被产生的代码几乎和我们已经为C库写的一样。

有趣的是,这同时就包括了为过程

stash_initialize()、stash_cleanup()等所做的“名字分解”。

当函数在struct内时,编译器有效地

做了相同的事情。

因此,在stash内部的initialize()将不会与任何其他结构中的initialize()相

抵触。

大部分时间都不必为函数名字分解而担心—即使使用未分解的函数名。

但有时还必须

能够指出这个initialize()属于这个structstash而不属于任何其他的struct。

特别是,定义这个函

数时,需要完全指定它是哪一个。

为了完成这个指定任务,C++有一个新的运算符:

:

,即范围

分解运算符(这样命名是因为名字现在能在不同的范围:

在全局范围或在这个struct的范围)。

例如,如果希望指定initialize()属于stash,就写stash:

:

initialize(intSize,intQuantity)。

对于

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 总结汇报 > 实习总结

copyright@ 2008-2023 冰点文库 网站版权所有

经营许可证编号:鄂ICP备19020893号-2