C++编程思想02.pdf
![资源得分’ title=](/images/score_1.gif)
![资源得分’ title=](/images/score_1.gif)
![资源得分’ title=](/images/score_1.gif)
![资源得分’ title=](/images/score_1.gif)
![资源得分’ title=](/images/score_05.gif)
《C++编程思想02.pdf》由会员分享,可在线阅读,更多相关《C++编程思想02.pdf(21页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、下载下载第2章数 据 抽 象C+是一个能提高效率的工具。为什么我们还要努力(这是努力,不管我们试图做的转变多么容易)使我们从已经熟悉且效率高的语言(在这里是 C语言)转到另一种新的语言上?而且使用这种新语言,我们会在确实掌握它之前的一段时间内降低效率。这归因于我们确信通过使用新工具将会得到更大的好处。用程序设计术语,多产意味着用较少的人在较少的时间内完成更复杂和更重要的程序。然而,选择语言时确实还有其他问题,例如运行效率(该语言的性质引起代码臃肿吗?)、安全性(该语言能有助于我们的程序做我们计划的事情并具有很强的纠错能力吗?)、可维护性(该语言能帮助我们创建易理解、易修改和易扩展的代码吗?)。
2、这些都是本书要考察的重要因素。简单地讲,提高生产效率,意味着本应花费三个人一星期的程序,现在只需要花费一个人一两天的时间。这会涉及到经济学的多层次问题。生产效率提高了,我们很高兴,因为我们正在建造的东西功能将会更强;我们的客户(或老板)很高兴,因为产品生产又快,用人又少;我们的顾客很高兴,因为他们得到的产品更便宜。而极大提高效率的唯一办法是使用其他人的代码,即使用库。库,简单地说就是一些人已经写的代码,按某种方式包装在一起。通常,最小的包是带有扩展名如L I B的文件和向编译器声明库中有什么的一个或多个头文件。连接器知道如何在 L I B文件中搜索和提取相应的已编译的代码。但是,这只是提供库的
3、一种方法。在跨越多种体系结构的平台上,例如U N I X,通常,提供库的最明智的方法是用源代码,这样在新的目标机上它能被重新编译。而在微软Wi n d o w s上,动态连接库是最明智的方法,这使得我们能够利用新发布的D D L经常修改我们的程序,我们的库函数销售商可能已经将新 D D L发送给我们了。所以,库大概是改进效率的最重要的方法。C+的主要设计目标之一是使库容易使用。这意味着,在C中使用库有困难。懂得这一点就对 C+设计有了初步的了解,从而对如何使用它有了更深入的认识。2.1 声明与定义首先,必须知道“声明”和“定义”之间的区别,因为这两个术语在全书中会被确切地使用。“声明”向计算机
4、介绍名字,它说,“这个名字是什么意思”。而“定义”为这个名字分配存储空间。无论涉及到变量时还是函数时含义都一样。无论在哪种情况下,编译器都在“定义”处分配存储空间。对于变量,编译器确定这个变量占多少存储单元,并在内存中产生存放它们的空间。对于函数,编译器产生代码,并为之分配存储空间。函数的存储空间中有一个由使用不带参数表或带地址操作符的函数名产生的指针。定义也可以是声明。如果该编译器还没有看到过名字 A,程序员定义int A,则编译器马上为这个名字分配存储地址。声明常常使用于e x t e r n关键字。如果我们只是声明变量而不是定义它,则要求使用 e x t e r n。对于函数声明,e x
5、 t e r n是可选的,不带函数体的函数名连同参数表或返回值,自动地作为一个声明。函数原型包括关于参数类型和返回值的全部信息。int f(float,char);是一个函数原型,因为它不仅介绍f这个函数的名字,而且告诉编译器这个函数有什么样的参数和返回值,使得编译器能对参数和返回值做适当的处理。C+要求必须写出函数原型,因为它增加了一个重要的安全层。下面是一些声明的例子。在函数声明时,参数名可给出也可不给出。而在定义时,它们是必需的。这在 C语言中确实如此,但在C+中并不一定。全书中,我们会注意到,每个文件的第一行是一个注释,它以注释符开始,后面跟冒号。这是我用的技术,可以利用诸如“g r
6、e p”和“a w k”这样的文本处理工具从代码文件中提取信息。在第一行中还包含有文件名,因此能在文本和其他文件中查阅这个文件,本书的代码磁盘中也很容易定义这个文件。2.2 一个袖珍C库一个小型库通常以一组函数开始,但是,已经用过别的 C 库的程序员知道,这里通常有更多的东西,有比行为、动作和函数更多的东西。还有一些特性(颜色、重量、纹理、亮度),它们都由数据表示。在C语言中,当我们处理一组特性时,可以方便地把它们放在一起,形成一个s t r u c t。特别是,如果我们想表示我们的问题空间中的多个类似的事情,则可以对每件事情创建这个s t r u c t的一个变量。这样,在大多数C库中都有一
7、组s t r u c t和一组活动在这些s t r u c t上的函数。现在看一个这样的例子。假设有一个程序设计工具,当创建时它的表现像一个数组,但它的长度能在运行时建立。我称它为s t a s h。第2章 数 据 抽 象23下载在结构内部需要引用这个结构时可以使用这个 s t r u c t的别名,例如,创建一个链表,需要指向下一个s t r u c t的指针。在C库中,几乎可以在整个库的每个结构上看到如上所示的 t y p e d e f。这样做使得我们能把s t r u c t作为一个新类型处理,并且可以定义这个s t r u c t的变量,例如:stash A,B,C;注意,这些函数声
8、明用标准 C 风格的函数原型,标准 C 风格比“老”C 风格更安全和更清楚。我们不仅介绍了函数名,而且还告诉编译器参数表和返回值的形式。s t o r a g e指针是一个unsigned char*。这是 C 编译器支持的最小的存储片,尽管在某些机器上它可能与最大的一般大,这依赖于具体实现。人们可能认为,因为 s t a s h被设计用于存放任何类型的变量,所以v o i d*在这里应当更合适。然而,我们的目的并不是把它当作某个未知类型的块处理,而是作为连续的字节块。这个执行文件的源代码(如果我们买了一个商品化的库,我们可能得到的只是编译好的O B J或L I B或D D L等)如下:24C
9、+编程思想下载注意本地的#include 风格,尽管这个头文件在本地目录下,但仍然以相对于本书的根目录给出。这样做,可以创建不同于这本书根目录的另外的目录,很容易拷贝文件到这个新目录下去实验,而不必担心改变#include 中的路径。第2章 数 据 抽 象25下载initialize()完成对 struct stash 的必要的设置,即设置内部变量为适当的值。最初,设置s t o r a g e指针为零,设置size 指示器也为零,表示初始存储未被分配。add()函数在s t a s h的下一个可用位子上插入一个元素。首先,它检查是否有可用空间,如果没有,它就用后面介绍的 inflate()函
10、数扩展存储空间。因为编译器并不知道被存放的特定变量的类型(函数返回的都是 v o i d*),所以我们不能只做赋值,虽然这的确是很方便的事情。代之,我们必须用标准 C 库函数memcpy()一个字节一个字节地拷贝这个变量,第一个参数是 memcpy()开始拷贝字节的目的地址,由下面表达式产生:&(S-storageS-next*S-size)它指示从存储块开始的第n e x t个可用单元结束。这个数实际上就是已经用过的单元号加一的计数,它必须乘上每个单元拥有的字节数,产生按字节计算的偏移量。这不产生地址,而是产生处于这个地址的字节,为了产生地址,必须使用地址操作符&。memcpy()的第二和第
11、三个参数分别是被拷贝变量的开始地址和要拷贝的字节数。n e x t计数器加一,并返回被存值的索引。这样,程序员可以在后面调用 fetch()时用它来取得这个元素。fetch()首先看索引是否越界,如果没有越界,返回所希望的变量地址,地址的计算采用与add()中相同的方法。对于有经验的C程序员count()乍看上去可能有点奇怪,它好像是自找麻烦,做手工很容易做的事情。例如,如果我们有一个 struct stash,例假设称为i n t S t a s h,那么通过用i n t S t a s h.n e x t找出它已经有多少个元素的方法似乎更直接,而不是去做 c o u n t(&i n t
12、S t a s h)函数调用(它有更多的花费)。但是,如果我们想改变s t a s h的内部表示和计数计算方法,那么这个函数调用接口就允许必要的灵活性。并且,很多程序员不会为找出库的“更好”的设计而操心。如果他们能着眼于s t r u c t和直接取n e x t的值,那么可能不经允许就改变n e x t。是不是能有一些方法使得库设计者能更好地控制像这样的问题呢?(是的,这是可预见的)。动态存储分配我们不可能预先知道一个s t a s h需要的最大存储量是多少,所以由s t o r a g e指向的内存从堆中分配。堆是很大的内存块,用以在运行时分一些小单元。在我们写程序时,如果我们还不知道所需
13、内存的大小,就可以使用堆。这样,我们可以直到运行时才知道需要存放 2 0 0个a i r p l a n e变量,而不仅是 2 0个。动态内存分配函数是标准C 库的一部分,包括 malloc()、calloc()、realloc()和free()。inflate()函数使用realloc()为s t a s h得到更大的空间块。realloc()把已经分配而又希望重分配的存储单元首地址作为它的第一个参数(如果这个参数为零,例如 initialize()刚刚被调用时,realloc()分配一个新块)。第二个参数是这个块新的长度,如果这个长度比原来的小,这个块将不需要作拷贝,简单地告诉堆管理器剩下
14、的空间是空闲的。如果这个长度比原来的大,在堆中没有足够的相临空间,所以要分配新块,并且要拷贝内存。assert()检查以确信这个操作成功。(如果这个堆用光了,malloc()、calloc()和realloc()都返回零。)注意,C 堆管理器相当重要,它给出内存块,对它们使用 free()时就回收它们。没有对堆进行合并的工具,如果能合并就可以提供更大的空闲块。如果程序多次分配和释放堆存储,最终会导致这个堆有大量的空闲块,但没有足够大且连续的空间能满足我们对内存分配的需要。但是,如果用堆合并器移动内存块,又会使得指针保存的不是相应的值。一些操作环境,例如Microsoft Wi n d o w
15、s有内置的合并,但它们要求我们使用专门的内存句柄(它们能临时地翻转为26C+编程思想下载指针,锁住内存后,堆压紧器不能移动它),而不是使用指针。assert()是在A S S E RT.H中的预处理宏。assert()取单个参数,它可以是能求得真或假值的任何表达式。这个宏表示:“我断言这是真的,如果不是,这个程序将打印出错信息,然后退出。”不再调试时,我们可以用一个标志使得这个断言被忽略。在调试期间,这是非常清楚和简便的测试错误的方法。不过,在出错处理时,它有点生硬:“对不起,请进行控制。我们的 C 程序对一个断言失败,并且跳出去。”在第1 7章中,我们将会看到,C+是如何用出错处理来处理重要
16、错误的。编译时,如果在栈上创建一个变量,那么这个变量的存储单元由编译器自动开辟和释放。编译器准确地知道需要多少存储容量,根据这个变量的活动范围知道这个变量的生命期。而对动态内存分配,编译器不知道需要多少存储单元,不知道它们的生命期,不能自动清除。因此,程序员应负责用 free()释放这块存储,free()告诉堆管理器,这个存储可以被下一次调用的malloc()、calloc()或realloc()重用。合理的方法是使用库中cleanup()函数,因为在这里,该函数做所有类似的事情。为了测试这个库,让我们创建两个s t a s h。第一个存放i n t,第二个存放8 0个字符的数组(我们可以把它
17、看作新数据类型)。第2章 数 据 抽 象27下载在main()的开头定义了一些变量,其中包括两个s t a s h结构变量,当然。稍后我们必须在这个程序块的对它们初始化。库的问题之一是我们必须向用户认真地说明初始化和清除函数的重要性,如果这些函数未被调用,就会出现许多问题。遗憾的是,用户不总是记得初始化和清除是必须的。他们只知道他们想完成什么,并不关心我们反复说的:“喂,等一等,您必须首先做这件事。”一些用户甚至认为初始化这些元素是自动完成的。的确没有机制能防止这种情况的发生(只有多预示)。i n t S t a s h适合于整型,s t r i n g S t a s h适合于字符串。这些字
18、符串是通过打开源代码文件L I B T E S T.C 和把这些行读到 stringStash 而产生的。注意一些有趣的地方:标准 C 库函数打开和读文件所使用的技术与在s t a s h中使用的技术类似。fopen()返回一个指向 FILE struct的指针,这个 FILE struct是在堆上创建的,并且能将这个指针传给涉及到这个文件的任何函数。(在这里是fgets())。fclose()所做的事情之一是向堆释放这个FILE struct。一旦我们开始注意到这种模式的,包含着s t r u c t和有关函数的 C 库后,我们就能到处看到它。装载了这两个s t a s h之后,可以打印出它
19、们。i n t S t a c h的打印用一个f o r循环,用count()确定它的限度。s t r i n g S t a s h的打印用一个w h i l e语句,如果fetch()返回零则表示打印越界,这时跳出循环。在我们考虑有关 C 库创建的问题之前,应当了解另外一些事情(我们可能已经知道这些,因为我们是 C 程序员)。第一,虽然这里用头文件,而且实际上用得很好,但它们不是必须的。在C中可能会调用还未声明的函数。好的编译器会告诫我们应当首先声明函数,但不强迫这样做。这是很危险的,因为编译器能假设以 i n t参数调用的函数有包含 i n t的参数表,并据此处理它,这是很难发现的错误。
20、注意,头文件 LIB.H 必须包含在涉及 stash 的所有文件中,因为编译器不可能猜出这个结构是什么样子的。它能猜出函数,即便它可能不应当这样,但这是 C 的一部分。每个独立的C文件就是一个处理单元。就是说,编译器在每个处理单元上单独运行,而编译器在运行时只知道这个单元。这样,用包含头文件提供信息是相当重要的,因为它为编译器提供了对程序其他部分的理解。在头文件中的声明特别重要,因为无论是在哪里包含这个头文件,编译器都会知道要做什么。例如,若在一个头文件中声明void foo(float),编译器就会知道,如果我们用整型参数调用它,它会自动把i n t转变为f l o a t。如果没有声明,这
21、个编译器就会简单地猜测,有一个函数存在,而不会做这个转变。对于每个处理单元,编译器创建一个目标文件,带有扩展名.o 或.obj 或类似的名字。必须再用连接器将这些目标文件连同必要的启动代码连接成可执行程序。在连接期间,所有的外部引用都必须确定。例如在 L I B T E S T.C 中,声明并使用函数 initialize()和fetch(),(也就是,编译器被告知它们像什么,)但未定义。它们是在 LIB.C 中定义的,这样,在 L I B T E S T.C 中的这些调用都是外部引用。当连接器将目标文件连接在一起时,它找出未确定的引用并寻找这些引28C+编程思想下载用对应的实际地址,用这些地
22、址替换这些外部引用。重要的是认识到,在 C 中,引用就是函数名,通常在它们前面加上下划线。所以,连接器所要做的就是让被调用的函数名与在目标文件中的函数体匹配起来。如果我们偶然做了一个调用,编译器解释为 f o o(i n t),而在其他目标文件中有 f o o(f l o a t)的函数体,连接器将认为一个_ f o o在一处而另一个_ f o o在另一处,它会认为这都是对的。在调用 foo()处将一个i n t放进栈中,而foo()函数体期望在这个栈中的是一个 f l o a t。如果这个函数只读这个值而不对它写,尚不会破坏这个栈。但从这个栈中读出的f l o a t值可能会有另外的某种理解
23、。这是最坏的情况,因为很难发现这个错误。2.3 放在一起:项目创建工具分别编译时(把代码分成多个处理单元),我们需要一些方法去编译所有的代码,并告诉连接器把它们与相应的库和启动代码放在一起,形成一个可执行文件。大部分编译器允许用一条命令行语句。例如编译器命名为c p p,可写:cpp libtest.c lib.c这个方法带来的问题是,编译器必须首先编译每个处理单元,而不管这个单元是否需要重建。虽然我们只改变了一个文件,但却需要耗费时间来对项目中的每一个文件进行重新编译。对这个问题的第一种解决办法,已由 U N I X(的诞生地)提出,是一个被称为 make 的程序。m a k e比较源代码文
24、件的日期和目标文件的日期,如果目标文件的日期比源代码文件的早,make 就调用这个编译器对这个单元进行处理。我们可以从编译器文档1 中学到更多的关于 make 的知识。make 是有用的,但学习和配置 makefile 有点乏味。makefile 是描述项目中所有文件之间关系的文本文件。因此,编译器销售商发行它们自己的项目创建工具。这些工具向我们询问项目中有哪些处理单元,并确定它们的关系。这些关系有些类似于 m a k e f i l e文件,通常称为项目文件。程序设计环境维护这个文件,所以我们不必为它担心。项目文件的配置和使用随系统而异,假设我们正在使用我们选择的项目创建工具来创建程序,我们
25、会发现如何使用它们的相应文档(虽然由编译器销售商提供的项目文件工具通常是非常简单的,可以不费劲地学会它们)。文件名应当注意的另一个问题是文件命名。在 C中,惯例是以扩展名.h命名头文件(包含声明),以.c 命名实现文件(它引起内存分配和代码生成)。C+继续演化。它首先是在 Unix 上开发的,这个操作系统能识别文件名的大小写。原来的文件名简单地变为大写,形成.H 和.C 版本。这样对于不区分大小写的操作系统,例如 M S-D O S,就行不通了。DOS C+厂商对于头文件和实现文件分别使用扩展名.hxx 和.c x x。后来,有人分析出,需要不同扩展名的唯一原因是使得编译器能确定编译C还是C+
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- C+ 编程 思想 02
![提示](https://www.taowenge.com/images/bang_tan.gif)
限制150内