精华内容
下载资源
问答
  • 1. The function primarily used for this is pthread_cond_wait ().... the first is a pointer to a condition variable, and the second is a locked mutex.函数pthreadcondwait()主要就是用于阻塞线程的...

    1. The function primarily used for this is pthread_cond_wait (). It takes two arguments; the first is a pointer to a condition variable, and the second is a locked mutex.

    函数pthreadcondwait()主要就是用于阻塞线程的,它有两个参数;第一个是一个指向条件变量的指针,第二个是一个锁定了的互斥量。

    2. However, pthread_cond_signal unblocks at least one of the threads waiting on the condition variable, not necessarily the reader thread with the longest waiting time.

    但是,pthreadcondsignal会释放至少一个等待条件变量的线程,这个线程不一定是等待时间最长的读线程。

    3. If another thread calls pthread_cond_signal () on a condition, then a thread that was waiting on that condition variable is woken up.

    如果另一个线程对一个条件变量调用pthreadcondsignal(),那么那个等待这个条件而被阻塞的线程就会被唤醒。

    4. When you're done with a condition variable, you can release any resources allocated during initialization with a call to pthread_cond_destroy ().

    当不再使用条件变量时,应该调用pthreadconddestroy()释放它在初始化时分配的资源。

    5. \u0064\u0061\u006E\u0063\u0069\u002E\u0039\u0031\u0031\u0063\u0068\u0061\u002E\u0063\u006F\u006D

    5. The recommended method is to use condition variables & that is, variables of type pthread_cond_t.

    推荐的方法是使用条件变量&即pthreadcondt类型的变量。

    6. Condition variables, like mutexes, must be initialized using an API call, in this case, pthread_cond_init ().

    条件变量同互斥量一样必须使用API调用对其初始化,这个API调用就是pthreadcondinit()。

    展开全文
  • 请教各位大神有木有遇到过这种情况,该怎么解决 import json import pymysql import random import time import urllib from urllib import request import doubans.config.linkconfig conn = ...
  • 2、它该告诉你,它为什么会存在、做什么事、怎么用。 3、如果名称需要注释来补充,那就不算是名副其实。 4、应该指明计量对象和计量单位的名称。 5、降低代码的模糊度:上下文在代码中未被明确体现的程度。 6、...

    说明:
    1、包括以下几个部分:命名、函数、注释、格式、对象与数据结构、错误处理、边界
    2、该笔记会保持更新

    一、命名

    1、一旦发现有更好的名称,就换掉旧的。
    2、它该告诉你,它为什么会存在、做什么事、怎么用。
    3、如果名称需要注释来补充,那就不算是名副其实。
    4、应该指明计量对象和计量单位的名称。
    5、降低代码的模糊度:上下文在代码中未被明确体现的程度。
    6、避免留下掩藏代码本意的错误线索。
    7、废话都是冗余。Variable不应该出现在变量名中,Table不应该出现在表名中
    8、使用可搜索的名称:单字母名称和数字常量很难在一大篇文字中找出来,如果被修改了,更难找到。
    9、名称长短应与其作用域大小相对应
    10、成员前缀:不必用m_前缀来表明成员变量应该把类和函数做得足够小,消除对成员前缀的需要。
    11、接口和实现:前导字母I被滥用说好听点是干扰,不好听就是废话。如果接口和实现必须选一个来编码的话,可以使用xxImp,Cxx
    12、类名和对象名应该是名词或名词短语。
    13、方法名应该是动词或动词短语。属性访问器、修改器和断言应该根据其值命令,应依Javabean标准加上get、set、is前缀。重载构造器时,使用描述了参数的静态工程方法名
    14、每个概念对应一个词:Eclipse和IntelliJ之类现代编程环境提供了与环境相关的线索,比如某个对象能调用的方法列表。不过要注意,列表中通常不会给出你为函数名和参数列表编写的注释。如果参数名称来自函数声明,你就太幸运了。函数名称应当独一无二,而且要保持一致,这样你才能不借助多余的浏览就找到正确的方法。
    15、避免将同一单词用于不同的目的。同一术语用于不同的概念,基本上就是双关语。
    16、如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。至少,负责维护代码的程序员就能去请教领域专家了。
    17、添加有意义的语境:可以给名称添加前缀,以便说明该名称所属一个大的结构中。更好的方案是创建名为Address的类。这样,即便是编译器也会知道这些变量隶属某个更大的概念了。
    18、为什么要搞得IDE没法帮助你?

    二、函数

    1、函数的第一规则是要短小。第二条规则是还要更短小。函数不该有100行那么长,20行封顶最佳。
    2、每个函数都依序把你带到下一个函数。
    3、编写函数毕竟是为了把大一些的概念(换言之,函数的名称)拆分为另一抽象层上的一系列步骤。
    4、要判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出一个函数。
    5、让代码读起来像是一系列自顶向下的TO起头段落是保持抽象层级协调一致的有效技巧。
    6、怎样简化switch:将switch语句埋到抽象工厂底下,不让任何人看到。该工厂使用switch语句为Employee的派生物创建适当的实体,而不同的函数,如calculatePay、isPayday和deliverPay等,则藉由Employee接口多态地接受派遣。
    7、函数越短小、功能越集中,就越便于取个好名字。
    8、命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。
    9、函数参数:最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)——所以无论如何也不要这么做。
    从测试的角度看,参数甚至更叫人为难。想想看,要编写能确保参数的各种组合运行正常的测试用例,是多么困难的事。如果没有参数,就是小菜一碟。输出参数比输入参数还要难以理解。读函数时,我们惯于认为信息通过参数输入函数,通过返回值从函数中输出。我们不太期望信息通过参数输出。所以,输出参数往往让人苦思之后才恍然大悟。
    10、极有用的单参数函数形式,那就是事件(event)。使用该参数修改系统状态。如果函数要对输入参数进行转换操作,转换结果就该体现为返回值。
    11、避免使用布尔值作为函数传入,如果这样做,无非想说明函数不止做一件事。推荐分解该函数。
    12、二元函数:即便是如assertEquals(expected,actual)这样的二元函数也有其问题。你有多少次会搞错actual和expected的位置呢?这两个参数没有自然的顺序。expected在前,actual在后,只是一种需要学习的约定罢了。
    13、参数对象:如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。
    14、输出函数:应避免使用输出参数。如果函数必须要修改某种状态,就修改所属对象的状态吧。
    15、分隔指令与询问:比如if的表达式中包含了set和is
    16、使用异常替代返回错误码:如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来。
    17、抽离Try/Catch代码块,它们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把try和catch代码块的主体部分抽离出来,另外形成函数。
    18、错误处理:函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。这意味着(如上例所示)如果关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。
    19、异常错误码类:返回错误码通常暗示某处有个类或是枚举,定义了所有错误码。其他许多类都得导入和使用它。当Error枚举修改时,所有这些其他的类都需要重新编译和部署。使用异常替代错误码,新异常就可以从异常类派生出来,无需重新编译或重新部署。
    20、重复的危害:问题,因为代码因此而臃肿,且当算法改变时需要修改4处地方。而且也会增加4次放过错误的可能性。比如数据库范式都是为消灭数据重复而服务。面向对象编程将代码集中到基类,从而避免了冗余。
    21、结构化编程:Dijkstra认为,每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或continue语句,而且永永远远不能有任何goto语句。
    只要函数保持短小,偶尔出现的return、break或continue语句没有坏处,甚至还比单入单出原则更具有表达力。

    三、注释

    1、如果你发现自己需要写注释,再想想看是否有办法翻盘,用代码来表达。
    2、程序员不能坚持维护注释,因此注释会撒谎。代码在变动,在演化,注释常常会与其所描述的代码分离开来
    3、程序员应当负责将注释保持在可维护、有关联、精确的高度。我同意这种说法。但我更主张把力气用在写清楚代码上,直接保证无须编写注释。
    4、唯一真正好的注释是你想办法不去写的注释。
    5、有时,用于警告其他程序员会出现某种后果的注释也是有用的。
    比如的@Ignore属性来关闭测试用例。
    6、有时,有理由用//TODO形式在源代码中放置要做的工作列表。
    无论TODO的目的如何,它都不是在系统中留下糟糕的代码的借口。
    你不会愿意代码因为TODO的存在而变成一堆垃圾,所以要定期查看,删除不再需要的。
    7、日志式注释:在没有源代码控制系统的前提下,该类注释有价值,这种冗长的记录只会让模块变得凌乱不堪,应当全部删除。
    8、位置标记:程序员喜欢在源代码中使用多个斜杠标记某个特别位置。如果标记栏不多,就会显而易见。所以,尽量少用标记栏,只在特别有价值的时候用。如果滥用标记栏,就会沉没在背景噪音中,被忽略掉。
    9、括号后面的注释:在括号后面放置特殊的注释。比如在while语句结尾使用//endwhile。尽管这对于含有深度嵌套结构的长函数可能有意义,但只会给我们更愿意编写的短小、封装的函数带来混乱。如果你发现自己想标记右括号,其实应该做的是缩短函数。
    10、归属与署名:有时会在代码中添加/Add by wxmgcs/,认为,这种注释大概有助于他人了解应该和谁讨论这段代码,时间一长,越来越不正确。源代码控制系统是这类信息最好的归属地。
    11、删除注释掉的代码:优良的源代码控制系统可以为我们记住不要的代码。我们无需再用注释来标记,删掉即可。
    12、假如你一定要写注释,请确保它描述了离它最近的代码。别在注释中添加有趣的历史性话题或者无关的细节描述。

    四、格式

    1、在封包声明、导入声明和每个函数之间,都有空白行隔开。这条极其简单的规则极大地影响到代码的视觉外观。每个空白行都是一条线索,标识出新的独立概念。
    2、关系密切的概念应该互相靠近。显然,这条规则并不适用于分布在不同文件中的概念。除非有很好的理由,否则就不要把关系密切的概念放到不同的文件中。实际上,这也是避免使用protected变量的理由之一。可以避免迫使读者在源文件和类中跳来跳去。
    3、变量声明应尽可能靠近其使用位置。因为函数很短,本地变量应该在函数的顶部出现。
    4、关于实体变量应该放在哪里,争论不断。在C++中,通常会采用所谓“剪刀原则”(scissorsrule),所有实体变量都放在底部。而在Java中,惯例是放在类的顶部。重点是在谁都知道的地方声明实体变量。大家都应该知道在哪儿能看到这些声明。
    5、相关函数:若某个函数调用了另外一个,就应该把它们放到一起,而且调用者应该尽可能放在被调用者上面。若坚定地遵循这条约定,读者将能够确信函数声明总会在其调用后很快出现。
    6、一行代码建议45个左右的字符。
    7、空格字符加强了分隔效果。比如赋值语句的左边和右边
    8、缩进规则
    9、确保空范围体的缩进,用括号包围起来。
    10、制订了一套编码风格:商定在什么地方放置括号,缩进几个字符,如何命名类、变量和方法,如此等等,然后,把这些规则编写进IDE的代码格式功能,接着就一直沿用。这些规则并非全是每位成员喜爱的;但它们是团队决定了的规则。

    五、对象与数据结构

    1、对象与数据结构之间的二分原理:过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。
    过程式代码难以添加新数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类。
    2、得墨忒耳律(TheLawofDemeter)认为,模块不应了解它所操作对象的内部情形。对象隐藏数据,曝露操作。这意味着对象不应通过存取器曝露其内部结构,因为这样更像是曝露而非隐藏其内部结构。更准确地说,得墨忒耳律认为,类C的方法f只应该调用以下对象的方法:由f创建的对象;作为参数传递给f的对象;由C的实体变量持有的对象。方法不应调用由任何函数返回的对象的方法。
    3、混杂:混淆有时会不幸导致混合结构,一半是对象,一半是数据结构。这种结构拥有执行操作的函数,也有公共变量或公共访问器及改值器。
    4、数据传送对象:最为精练的数据结构,是一个只有公共变量、没有函数的类。这种数据结构有时被称为数据传送对象,或DTO(DataTransferObjects)。DTO是非常有用的结构,尤其是在与数据库通信、或解析套接字传递的消息之类场景中。在应用程序代码里一系列将原始数据转换为数据库的翻译过程中。
    5、对象曝露行为,隐藏数据。便于添加新对象类型而无需修改既有行为,同时也难以在既有对象中添加新行为。数据结构曝露数据,没有明显的行为。便于向既有数据结构添加新行为,同时也难以向既有函数添加新数据结构。

    六、错误处理

    1、错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法。
    2、使用异常而非返回码:遇到错误时,最好抛出一个异常。
    3、在某种意义上,try代码块就像是事务。catch代码块将程序维持在一种持续状态,无论try代码块中发生了什么均如此。所以,在编写可能抛出异常的代码时,最好先写出try-catch-finally语句。这能帮你定义代码的用户应该应该期待什么,无论try代码块中执行的代码出什么错都一样。
    4、使用不可控异常:可控异常的代价就是违反开放/闭合原则。比如大型系统的调用层级。顶端函数调用它们之下的函数,逐级向下。假设某个位于最底层级的函数被修改为抛出一个异常。如果该异常是可控的,则函数签名就要添加throw子句。这意味着每个调用该函数的函数都要修改,捕获新异常,或在其签名中添加合适的throw子句。以此类推。最终得到的就是一个从软件最底端贯穿到最高端的修改链!封装被打破了,因为在抛出路径中的每个函数都要去了解下一层级的异常细节。既然异常旨在让你能在较远处处理错误,可控异常以这种方式破坏封装简直就是一种耻辱。
    5、给出异常发生的环境说明:你抛出的每个异常,都应当提供足够的环境说明,以便判断错误的来源和处所。比如在Java中,你可以从任何异常里得到堆栈踪迹(stacktrace);然而,堆栈踪迹却无法告诉你该失败操作的初衷。应创建信息充分的错误消息,并和异常一起传递出去。在消息中,包括失败的操作和失败类型。如果你的应用程序有日志系统,传递足够的信息给catch块,并记录下来。
    6、依调用者需要定义异常类:对错误分类有很多方式。可以依其来源分类:是来自组件还是其他地方?或依其类型分类:是设备错误、网络错误还是编程错误?不过,当我们在应用程序中定义异常类时,最重要的考虑应该是它们如何被捕获。
    7、别返回null值:如果你打算在方法中返回null值,不如抛出异常,或是返回特例对象。如果你在调用某个第三方API中可能返回null值的方法,可以考虑用新方法打包这个方法,在新方法中抛出异常或返回特例对象。
    比如使用空列表代替null。
    8、别传递null值
    9、整洁代码是可读的,但也要强固。可读与强固并不冲突。如果将错误处理隔离看待,独立于主要逻辑之外,就能写出强固而整洁的代码。做到这一步,我们就能单独处理它,也极大地提升了代码的可维护性。

    七、边界

    1、边界上的接口(Map)是隐藏的。它能随来自应用程序其他部分的极小的影响而变动。
    建议不要将Map(或在边界上的其他接口)在系统中传递。如果你使用类似Map这样的边界接口,就把它保留在类或近亲类中。避免从公共API中返回边界接口,或将边界接口作为参数传递给公共API。
    2、不要在生产代码中试验新东西,而是编写测试来遍览和理解第三方代码。JimNewkirk把这叫做学习性测试(learningtests)。在学习性测试中,我们如在应用中那样调用第三方代码。我们基本上是在通过核对试验来检测自己对那个API的理解程度。测试聚焦于我们想从API得到的东西。
    3、无论如何我们都得学习要使用的API,而编写测试则是获得这些知识的容易而不会影响其他工作的途径。学习性测试是一种精确试验,帮助我们增进对API的理解。
    4、当第三方程序包发布了新版本,我们可以运行学习性测试,看看程序包的行为有没有改变。边界。不使用这些边界测试来减轻迁移的劳力,我们可能会超出应有时限,长久地绑在旧版本上面。
    5、使用尚不存在的代码:将已知和未知分隔开的边界。在代码中总有许多地方是我们的知识未及之处。有时,边界那边就是未知的(至少目前未知)。有时,我们并不往边界那边看过去。
    编写我们想得到的接口,好处之一是它在我们控制之下。这有助于保持客户代码更可读,且集中于它该完成的工作。
    比如ADAPTER封装了与API的互动,也提供了一个当API发生变动时唯一需要改动的地方。
    6、边界上的代码需要清晰的分割和定义了期望的测试。应该避免我们的代码过多地了解第三方代码中的特定信息。

    展开全文
  • 第一章:基本概念 Java的特点:友好的语法,面向对象,...对象本身已知的事物成为实例变量(instance variable)。它们代表对象的状态(数据),且改类型的每一个对象都会独立的拥有一份该类型的值。所以也可以把...

    第一章:基本概念

    • Java的特点:友好的语法,面向对象,内存管理和跨平台可移植性

    • Java的工作方式:
      在这里插入图片图片描述
      第二章:类与对象

    • 类和对象的关系:类是对象的蓝图。当你设计类时,要记得对象是靠类的模具塑造出来的。

    • 怎么理解对象?
      t
      对象本身已知的事物成为实例变量(instance variable)。它们代表对象的状态(数据),且改类型的每一个对象都会独立的拥有一份该类型的值。所以也可以把对象当作为实例

    • 本章要点:
      在这里插入图片描述
      第三章:primitive主数据类型和引用

    • 变量有两种:primitive主数据类型和引用。
      在这里插入图片描述

    • Java的命名规则:在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    • 理解引用变量
      在这里插入图片描述在这里插入图片描述

    • 数组在这里插入图片描述

    • Java注重类型,一旦数组被声明出来,你就只能装入所声明类型的元素。(举个例子:你不能再Dog数组里放Cat进去)

    • 在这里插入图片描述
      注意:以上内容来源自head first,侵删,

    展开全文
  • 第2章 数据类型与表达式 1. C++中的数据类型如下: 2. C++中常量 变量的值应该是可以变化的,怎么值是固定的量也称变量呢?其实,从计算机实现的角度看,变量...因此常变量又称为只读变量(read-only-variable)...

    第2章 数据类型与表达式

    1. C++中的数据类型如下:

    2. C++中常量

    变量的值应该是可以变化的,怎么值是固定的量也称变量呢?其实,从计算机实现的角度看,变量的特征是存在一个以变量名命名的存储单元,在一般情况下,存储单元中的内容是可以变化的。对常变量来说,无非在此变量的基础上加上一个限定: 存储单元中的值不允许变化。因此常变量又称为只读变量(read-only-variable)。

    请区别用#define命令定义的符号常量和用const定义的常变量。符号常量只是用一个符号代替一个字符串,在预编译时把所有符号常量替换为所指定的字符串,它没有类型,在内存中并不存在以符号常量命名的存储单元。而常变量具有变量的特征,它具有类型,在内存中存在着以它命名的存储单元,可以用sizeof运算符测出其长度。与一般变量惟一的不同是指定变量的值不能改变。用#define命令定义符号常量是C语言所采用的方法,C++把它保留下来是为了和C兼容。C++的程序员一般喜欢用const定义常变量。虽然二者实现的方法不同,但从使用的角度看,都可以认为用了一个标识符代表了一个常量。有些书上把用const定义的常变量也称为定义常量,但读者应该了解它和符号常量的区别。

    第3章 程序设计初步

    1. 在输入流与输出流中使用控制符

    需要注意的是: 如果使用了控制符,在程序单位的开头除了要加iostream头文件外,还要加iomanip头文件。

    举例: 输出双精度数。

    double a=123.456789012345;对a赋初值

    (1) cout<<a;输出: 123.456

    (2) cout<<setprecision(9)<<a;输出: 123.456789

    (3) cout<<setprecision(6);恢复默认格式(精度为6)

    (4) cout<< setiosflags(iosfixed);输出: 123.456789

    (5) cout<<setiosflags(iosfixed)<<setprecision(8)<<a;输出: 123.45678901

    (6) cout<<setiosflags(iosscientific)<<a;输出: 1.234568e+02

    (7) cout<<setiosflags(iosscientific)<<setprecision(4)<<a; 输出: 1.2346e02

    下面是整数输出的例子:

    int b=123456;对b赋初值

    (1) cout<<b;输出: 123456

    (2) cout<<hex<<b; 输出: 1e240

    (3) cout<<setiosflags(iosuppercase)<<b;输出: 1E240

    (4) cout<<setw(10)<<b<<′,′<<b; 输出: 123456,123456

    (5) cout<<setfill(′*′)<<setw(10)<<b;输出: **** 123456

    (6) cout<<setiosflags(iosshowpos)<<b;输出: +123456

    如果在多个cout语句中使用相同的setw(n),并使用setiosflags(iosright),可以实现各行数据右对齐,如果指定相同的精度,可以实现上下小数点对齐。

    例3.1 各行小数点对齐。

    #include <iostream>

    #include <iomanip>

    using namespace std;

    int main( )

    {

    double a=123.456,b=3.14159,c=-3214.67;

    cout<<setiosflags(iosfixed)<<setiosflags(iosright)<<setprecision(2);

    cout<<setw(10)<<a<<endl;

    cout<<setw(10)<<b<<endl;

    cout<<setw(10)<<c<<endl;

    return 0;

    }

    第4章 函数与预处理

    1.变量属性小结

    一个变量除了数据类型以外,还有3种属性:

    (1) 存储类别 C++允许使用auto,static,register和extern 4种存储类别。

    (2) 作用域 指程序中可以引用该变量的区域。

    (3) 存储期 指变量在内存的存储期限。

    以上3种属性是有联系的,程序设计者只能声明变量的存储类别,通过存储类别可以确定变量的作用域和存储期。

    要注意存储类别的用法。auto, static和register 3种存储类别只能用于变量的定义语句中,如

    auto char c; //字符型自动变量,在函数内定义

    static int a; //静态局部整型变量或静态外部整型变量

    register int d; //整型寄存器变量,在函数内定义

    extern int b; //声明一个已定义的外部整型变量

    说明: extern只能用来声明已定义的外部变量,而不能用于变量的定义。只要看到extern,就可以判定这是变量声明,而不是定义变量的语句。

    下面从不同角度分析它们之间的联系。

    (1) 从作用域角度分,有局部变量和全局变量。它们采用的存储类别如下:

    ● 局部变量

    自动变量,即动态局部变量(离开函数,值就消失)

    静态局部变量(离开函数,值仍保留)

    寄存器变量(离开函数,值就消失)

    形式参数(可以定义为自动变量或寄存器变量)

    ● 全局变量

    静态外部变量(只限本文件引用)

    外部变量(即非静态的外部变量,允许其他文件引用)

    (2) 从变量存储期(存在的时间)来区分,有动态存储和静态存储两种类型。静态存储是程序整个运行时间都存在,而动态存储则是在调用函数时临时分配单元。

    ●动态存储

    自动变量(本函数内有效)

    寄存器变量(本函数内有效)

    形式参数

    ● 静态存储

    静态局部变量(函数内有效)

    静态外部变量(本文件内有效)

    外部变量(其他文件可引用)

    (3) 从变量值存放的位置来区分,可分为

    ● 内存中静态存储区

    静态局部变量

    静态外部变量(函数外部静态变量)

    外部变量(可为其他文件引用)

    ● 内存中动态存储区: 自动变量和形式参数

    ● CPU 中的寄存器: 寄存器变量

    (4) 关于作用域和存储期的概念。从前面叙述可以知道,对一个变量的性质可以从两个方面分析,一是从变量的作用域,一是从变量值存在时间的长短,即存储期。前者是从空间的角度,后者是从时间的角度。二者有联系但不是同一回事。图4.16是作用域的示意图,图4.17是存储期的示意图。

    第7章 结构体与共用体

    1. 用typedef声明类型

    除了用以上方法声明结构体、共用体、枚举等类型外,还可以用typedef声明一个新的类型名来代替已有的类型名。如

    typedef int INTEGER; //指定用标识符INTEGER代表int类型

    typedef float REAL; //指定用REAL代表float类型

    这样,以下两行等价:

    ① int i,j; float a,b;

    ② INTEGER i,j; REAL a,b;

    这样可以使熟悉FORTRAN的人能用INTEGER和REAL定义变量,以适应他们的习惯。

    如果在一个程序中,整型变量是专门用来计数的,可以用COUNT来作为整型类型名:

    typedef int COUNT; //指定用COUNT代表int型

    COUNT i,j; //将变量i,j定义为COUNT类型,即int类型

    在程序中将变量i,j定义为COUNT类型,可以使人更一目了然地知道它们是用于计数的。

    也可以声明结构体类型:

    typedef struct //注意在struct之前用了关键字typedef,表示是声明新名

    { int month;

    int day;

    int year;

    }DATE; //注意DATE是新类型名,而不是结构体变量名

    所声明的新类型名DATE代表上面指定的一个结构体类型。这样就可以用DATE定义变量:

    DATE birthday;

    DATE *p; //p为指向此结构体类型数据的指针

    还可以进一步:

    ① typedef int NUM[100]; //声明NUM为整型数组类型,包含100个元素

    NUM n; //定义n为包含100个整型元素的数组

    ② typedef char *STRING; //声明STRING为字符指针类型

    STRING p,s[10]; //p为字符指针变量,s为指针数组(有10个元素)

    ③ typedef int (*POINTER)( ) //声明POINTER为指向函数的指针类型,函数返回整型值

    POINTER p1,p2; // p1,p2为POINTER类型的指针变量

    归纳起来,声明一个新的类型名的方法是:

    ① 先按定义变量的方法写出定义语句(如int i;)。

    ② 将变量名换成新类型名(如将i换成COUNT)。

    ③ 在最前面加typedef(如typedef int COUNT)。

    ④ 然后可以用新类型名去定义变量。

    再以声明上述的数组类型为例来说明:

    ① 先按定义数组形式书写: int n[100];

    ② 将变量名n换成自己指定的类型名: int NUM[100];

    ③ 在前面加上typedef,得到typedef int NUM[100];

    ④ 用来定义变量: NUM n;(n是包含100个整型元素的数组)。

    习惯上常把用typedef声明的类型名用大写字母表示,以便与系统提供的标准类型标识符相区别。

    说明:

    (1) typedef可以声明各种类型名,但不能用来定义变量。用typedef可以声明数组类型、字符串类型,使用比较方便。

    (2) 用typedef只是对已经存在的类型增加一个类型名,而没有创造新的类型。

    (3) 当在不同源文件中用到同一类型数据(尤其是像数组、指针、结构体、共用体等类型数据)时,常用typedef声明一些数据类型,把它们单独放在一个头文件中,然后在需要用到它们的文件中用#include命令把它们包含进来,以提高编程效率。

    (4) 使用typedef有利于程序的通用与移植。有时程序会依赖于硬件特性,用typedef便于移植。

    第8章 类与对象

    1. 面向对象的软件开发

    随着软件规模的迅速增大,软件人员面临的问题十分复杂。需要规范整个软件开发过程,明确软件开发过程中每个阶段的任务,在保证前一个阶段工作的正确性的情况下,再进行下一阶段的工作。这就是软件工程学需要研究和解决的问题。

    面向对象的软件工程包括以下几个部分:

    (1). 面向对象分析(object oriented analysis,OOA)

    软件工程中的系统分析阶段,系统分析员要和用户结合在一起,对用户的需求作出精确的分析和明确的描述,从宏观的角度概括出系统应该做什么(而不是怎么做)。面向对象的分析,要按照面向对象的概念和方法,在对任务的分析中,从客观存在的事物和事物之间的关系,归纳出有关的对象(包括对象的属性和行为)以及对象之间的联系,并将具有相同属性和行为的对象用一个类(class)来表示。建立一个能反映真实工作情况的需求模型。

    (2). 面向对象设计(object oriented design,OOD)

    根据面向对象分析阶段形成的需求模型,对每一部分分别进行具体的设计,首先是进行类的设计,类的设计可能包含多个层次(利用继承与派生)。然后以这些类为基础提出程序设计的思路和方法,包括对算法的设计。在设计阶段,并不牵涉某一种具体的计算机语言,而是用一种更通用的描述工具(如伪代码或流程图)来描述。

    (3). 面向对象编程(object oriented programming,OOP)

    根据面向对象设计的结果,用一种计算机语言把它写成程序,显然应当选用面向对象的计算机语言(例如C++),否则无法实现面向对象设计的要求。

    (4). 面向对象测试(object oriented test,OOT)

    在写好程序后交给用户使用前,必须对程序进行严格的测试。测试的目的是发现程序中的错误并改正它。面向对象测试是用面向对象的方法进行测试,以类作为测试的基本单元。

    (5). 面向对象维护(object oriented soft maintenance,OOSM)

    因为对象的封装性,修改一个对象对其他对象影响很小。利用面向对象的方法维护程序,大大提高了软件维护的效率。

    2. 成员函数的存储方式

    用类去定义对象时,系统会为每一个对象分配存储空间。如果一个类包括了数据和函数,要分别为数据和函数的代码分配存储空间。按理说,如果用同一个类定义了10个对象,那么就需要分别为10个对象的数据和函数代码分配存储单元,如图8.4所示

    能否只用一段空间来存放这个共同的函数代码段,在调用各对象的函数时,都去调用这个公用的函数代码。如图8.5所示

    显然,这样做会大大节约存储空间。C++编译系统正是这样做的,因此每个对象所占用的存储空间只是该对象的数据部分所占用的存储空间,而不包括函数代码所占用的存储空间。如果声明了一个类:

    class Time

    {public:

    int hour;

    int minute;

    int sec;

    void set( )

    {cin>>a>>b>>c;}

    };

    可以用下面的语句来输出该类对象所占用的字节数:

    cout<<sizeof(Time)<<endl;

    输出的值是12。这就证明了一个对象所占的空间大小只取决于该对象中数据成员所占的空间,而与成员函数无关。函数代码是存储在对象空间之外的。如果对同一个类定义了10个对象,这些对象的成员函数对应的是同一个函数代码段,而不是10个不同的函数代码段。

    需要注意的是: 虽然调用不同对象的成员函数时都是执行同一段函数代码,但是执行结果一般是不相同的。不同的对象使用的是同一个函数代码段,它怎么能够分别对不同对象中的数据进行操作呢?原来C++为此专门设立了一个名为this的指针,用来指向不同的对象。

    需要说明:

    (1) 不论成员函数在类内定义还是在类外定义,成员函数的代码段都用同一种方式存储。

    (2) 不要将成员函数的这种存储方式和inline(内置)函数的概念混淆。

    (3) 应当说明: 常说的"某某对象的成员函数",是从逻辑的角度而言的,而成员函数的存储方式,是从物理的角度而言的,二者是不矛盾的。

    3. 构造函数的使用

    有关构造函数的使用,有以下说明:

    (1) 在类对象进入其作用域时调用构造函数。

    (2) 构造函数没有返回值,因此也不需要在定义构造函数时声明类型,这是它和一般函数的一个重要的不同之点。

    (3) 构造函数不需用户调用,也不能被用户调用。

    (4) 在构造函数的函数体中不仅可以对数据成员赋初值,而且可以包含其他语句。但是一般不提倡在构造函数中加入与初始化无关的内容,以保持程序的清晰。

    (5) 如果用户自己没有定义构造函数,则C++系统会自动生成一个构造函数,只是这个构造函数的函数体是空的,也没有参数,不执行初始化操作。

    4. 析构函数

    析构函数(destructor)也是一个特殊的成员函数,它的作用与构造函数相反,它的名字是类名的前面加一个"~"符号。在C++中"~"是位取反运算符,从这点也可以想到: 析构函数是与构造函数作用相反的函数。

    当对象的生命期结束时,会自动执行析构函数。具体地说如果出现以下几种情况,程序就会执行析构函数: ①如果在一个函数中定义了一个对象(它是自动局部对象),当这个函数被调用结束时,对象应该释放,在对象释放前自动执行析构函数。static局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数。如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束或调用exit函数) 时,调用该全局对象的析构函数。如果用new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数。

    析构函数的作用并不是删除对象,而是在撤销对象占用的内存之前完成一些清理工作,使这部分内存可以被程序分配给新对象使用。程序设计者事先设计好析构函数,以完成所需的功能,只要对象的生命期结束,程序就自动执行析构函数来完成这些工作。

    析构函数不返回任何值,没有函数类型,也没有函数参数。因此它不能被重载。一个类可以有多个构造函数,但只能有一个析构函数。

    实际上,析构函数的作用并不仅限于释放资源方面,它还可以被用来执行"用户希望在最后一次使用对象之后所执行的任何操作",例如输出有关的信息。这里说的用户是指类的设计者,因为,析构函数是在声明类的时候定义的。也就是说,析构函数可以完成类的设计者所指定的任何操作。

    一般情况下,类的设计者应当在声明类的同时定义析构函数,以指定如何完成"清理"的工作。如果用户没有定义析构函数,C++编译系统会自动生成一个析构函数,但它只是徒有析构函数的名称和形式,实际上什么操作都不进行。想让析构函数完成任何工作,都必须在定义的析构函数中指定。

    5. 调用构造函数和析构函数的顺序

    在使用构造函数和析构函数时,需要特别注意对它们的调用时间和调用顺序。

    在一般情况下,调用析构函数的次序正好与调用构造函数的次序相反: 最先被调用的构造函数,其对应的(同一对象中的)析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。如图9.1示意。

    但是,并不是在任何情况下都是按这一原则处理的。在第4章第4.11和4.12节中曾介绍过作用域和存储类别的概念,这些概念对于对象也是适用的。对象可以在不同的作用域中定义,可以有不同的存储类别。这些会影响调用构造函数和析构函数的时机。

    下面归纳一下什么时候调用构造函数和析构函数:

    (1) 在全局范围中定义的对象(即在所有函数之外定义的对象),它的构造函数在文件中的所有函数(包括main函数)执行之前调用。但如果一个程序中有多个文件,而不同的文件中都定义了全局对象,则这些对象的构造函数的执行顺序是不确定的。当main函数执行完毕或调用exit函数时(此时程序终止),调用析构函数。

    (2) 如果定义的是局部自动对象(例如在函数中定义对象),则在建立对象时调用其构造函数。如果函数被多次调用,则在每次建立对象时都要调用构造函数。在函数调用结束、对象释放时先调用析构函数。

    (3) 如果在函数中定义静态(static)局部对象,则只在程序第一次调用此函数建立对象时调用构造函数一次,在调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用析构函数。

    第9章

    1. 共用数据的保护

    C++虽然采取了不少有效的措施(如设private保护)以增加数据的安全性,但是有些数据却往往是共享的,人们可以在不同的场合通过不同的途径访问同一个数据对象。有时在无意之中的误操作会改变有关数据的状况,而这是人们所不希望出现的。

    既要使数据能在一定范围内共享,又要保证它不被任意修改,这时可以使用const,即把有关的数据定义为常量。

    (1). const数据成员

    其作用和用法与一般常变量相似,用关键字const来声明常数据成员。常数据成员的值是不能改变的。有一点要注意: 只能通过构造函数的参数初始化表对常数据成员进行初始化。如在类体中定义了常数据成员hour:

    const int hour; //声明hour为常数据成员

    不能采用在构造函数中对常数据成员赋初值的方法。

    在类外定义构造函数,应写成以下形式:

    Time∷Time(int h):hour(h){} //通过参数初始化表对常数据成员hour初始化

    常对象的数据成员都是常数据成员,因此常对象的构造函数只能用参数初始化表对常数据成员进行初始化。

    (2). 常成员函数

    前面已提到: 一般的成员函数可以引用本类中的非const数据成员,也可以修改它们。如果将成员函数声明为常成员函数,则只能引用本类中的数据成员,而不能修改它们,例如只用于输出数据等。如

    void get_time( ) const; //注意const的位置在函数名和括号之后

    const是函数类型的一部分,在声明函数和定义函数时都要有const关键字,在调用时不必加const。常成员函数可以引用const数据成员,也可以引用非const的数据成员。const数据成员可以被const成员函数引用,也可以被非const的成员函数引用。具体情况可以用书中表9.1表示。

    怎样利用常成员函数呢?

    (1) 如果在一个类中,有些数据成员的值允许改变,另一些数据成员的值不允许改变,则可以将一部分数据成员声明为const,以保证其值不被改变,可以用非const的成员函数引用这些数据成员的值,并修改非const数据成员的值。

    (2) 如果要求所有的数据成员的值都不允许改变,则可以将所有的数据成员声明为const,或将对象声明为const(常对象),然后用const成员函数引用数据成员,这样起到"双保险"的作用,切实保证了数据成员不被修改。

    (3) 如果已定义了一个常对象,只能调用其中的const成员函数,而不能调用非const成员函数(不论这些函数是否会修改对象中的数据)。这是为了保证数据的安全。如果需要访问对象中的数据成员,可将常对象中所有成员函数都声明为const成员函数,但应确保在函数中不修改对象中的数据成员。不要误认为常对象中的成员函数都是常成员函数。常对象只保证其数据成员是常数据成员,其值不被修改。如果在常对象中的成员函数未加const声明,编译系统把它作为非const成员函数处理。

    还有一点要指出: 常成员函数不能调用另一个非const成员函数。

    2. 对象的复制

    有时需要用到多个完全相同的对象。此外,有时需要将对象在某一瞬时的状态保留下来。这就是对象的复制机制。用一个已有的对象快速地复制出多个完全相同的对象。如

    Box box2(box1);

    其作用是用已有的对象box1去克隆出一个新对象box2。

    其一般形式为

    类名 对象2(对象1);

    用对象1复制出对象2。

    可以看到: 它与前面介绍过的定义对象方式类似,但是括号中给出的参数不是一般的变量,而是对象。在建立对象时调用一个特殊的构造函数——复制构造函数(copy constructor)。这个函数的形式是这样的:

    //The copy constructor definition.

    BoxBox(const Box& b)

    {height=b.height;

    width=b.width;

    length=b.length;}

    复制构造函数也是构造函数,但它只有一个参数,这个参数是本类的对象(不能是其他类的对象),而且采用对象的引用的形式(一般约定加const声明,使参数值不能改变,以免在调用此函数时因不慎而使对象值被修改)。

    此复制构造函数的作用就是将实参对象的各成员值一一赋给新的对象中对应的成员。

    回顾复制对象的语句

    Box box2(box1);

    这实际上也是建立对象的语句,建立一个新对象box2。由于在括号内给定的实参是对象,因此编译系统就调用复制构造函数(它的形参也是对象),而不会去调用其他构造函数。实参box1的地址传递给形参b(b是box1的引用),因此执行复制构造函数的函数体时,将box1对象中各数据成员的值赋给box2中各数据成员。

    如果用户自己未定义复制构造函数,则编译系统会自动提供一个默认的复制构造函数,其作用只是简单地复制类中每个数据成员。

    C++还提供另一种方便用户的复制形式,用赋值号代替括号,如

    Box box2=box1; //用box1初始化box2

    其一般形式为

    类名 对象名1 = 对象名2;

    可以在一个语句中进行多个对象的复制。如

    Box box2=box1,box3=box2;

    按box1来复制box2和box3。可以看出: 这种形式与变量初始化语句类似,请与下面定义变量的语句作比较:

    int a=4,b=a;

    这种形式看起来很直观,用起来很方便。但是其作用都是调用复制构造函数。

    3. 复制构造函数

    请注意对象的复制和9.8.1节介绍的对象的赋值在概念上和语法上的不同。对象的赋值是对一个已经存在的对象赋值,因此必须先定义被赋值的对象,才能进行赋值。而对象的复制则是从无到有地建立一个新对象,并使它与一个已有的对象完全相同(包括对象的结构和成员的值)。

    可以对例9.7程序中的主函数作一些修改:

    int main( )

    {Box box1(15,30,25); //定义box1

    cout<<″The volume of box1 is ″<<box1.volume( )<<endl;

    Box box2=box1,box3=box2; //按box1来复制box2,box3

    cout<<″The volume of box2 is ″<<box2.volume( )<<endl;

    cout<<″The volume of box3 is ″<<box3.volume( )<<endl;

    }

    执行完第3行后,3个对象的状态完全相同。

    请注意普通构造函数和复制构造函数的区别。

    (1) 在形式上

    类名(形参表列); //普通构造函数的声明,如Box(int h,int w,int len);

    类名(类名& 对象名); //复制构造函数的声明,如Box(Box &b);

    (2) 在建立对象时,实参类型不同。系统会根据实参的类型决定调用普通构造函数或复制构造函数。如

    Box box1(12,15,16); //实参为整数,调用普通构造函数

    Box box2(box1); //实参是对象名,调用复制构造函数

    (3) 在什么情况下被调用

    普通构造函数在程序中建立对象时被调用。

    复制构造函数在用已有对象复制一个新对象时被调用,在以下3种情况下需要克隆对象:

    ① 程序中需要新建立一个对象,并用另一个同类的对象对它初始化,如前面介绍的那样。

    当函数的参数为类的对象时。在调用函数时需要将实参对象完整地传递给形参,也就是需要建立一个实参的拷贝,这就是按实参复制一个形参,系统是通过调用复制构造函数来实现的,这样能保证形参具有和实参完全相同的值。

    void fun(Box b) //形参是类的对象

    { }

    int main( )

    {Box box1(12,15,18);

    fun(box1); //实参是类的对象,调用函数时将复制一个新对象b

    return 0;

    }

    ③ 函数的返回值是类的对象。在函数调用完毕将返回值带回函数调用处时。此时需要将函数中的对象复制一个临时对象并传给该函数的调用处。如

    Box f( ) //函数f的类型为Box类类型

    {Box box1(12,15,18);

    return box1; //返回值是Box类的对象

    }

    int main( )

    {Box box2; //定义Box类的对象box2

    box2=f( ); //调用f函数,返回Box类的临时对象,并将它赋值给box2

    }

    以上几种调用复制构造函数都是由编译系统自动实现的,不必由用户自己去调用,读者只要知道在这些情况下需要调用复制构造函数就可以了。

    第10章 运算符重载

    1.成员函数或友元函数

    C++规定,有的运算符(如赋值运算符、下标运算符、函数调用运算符)必须定义为类的成员函数,有的运算符则不能定义为类的成员函数(如流插入"<<"和流提取运算符">>"、类型转换运算符)。

    2. 重载流插入运算符和流提取运算符

    C++的流插入运算符"<<"和流提取运算符">>"是C++在类库中提供的,所有C++编译系统都在类库中提供输入流类istream和输出流类ostream。cin和cout分别是istream类和ostream类的对象。在类库提供的头文件中已经对"<<"和">>"进行了重载,使之作为流插入运算符和流提取运算符,能用来输出和输入C++标准类型的数据。因此,在本书前面几章中,凡是用"cout<<"和"cin>>"对标准类型数据进行输入输出的,都要用#include <iostream>把头文件包含到本程序文件中。

    用户自己定义的类型的数据,是不能直接用"<<"和">>"来输出和输入的。如果想用它们输出和输入自己声明的类型的数据,必须对它们重载。

    对"<<"和">>"重载的函数形式如下:

    istream & operator >> (istream &,自定义类 &);

    ostream & operator << (ostream &,自定义类 &);

    即重载运算符">>"的函数的第一个参数和函数的类型都必须是istream&类型,第二个参数是要进行输入操作的类。重载"<<"的函数的第一个参数和函数的类型都必须是ostream&类型,第二个参数是要进行输出操作的类。因此,只能将重载">>"和"<<"的函数作为友元函数或普通的函数,而不能将它们定义为成员函数。

    3. 转换构造函数

    转换构造函数(conversion constructor function) 的作用是将一个其他类型的数据转换成一个类的对象。

    先回顾一下以前学习过的几种构造函数:

    • 默认构造函数。以Complex类为例,函数原型的形式为

    Complex( ); //没有参数

    • 用于初始化的构造函数。函数原型的形式为

    Complex(double r,double i); //形参表列中一般有两个以上参数

    • 用于复制对象的复制构造函数。函数原型的形式为

    Complex (Complex &c); //形参是本类对象的引用

    • 现在又要介绍一种新的构造函数——转换构造函数。

    转换构造函数只有一个形参,如

    Complex(double r) {real=r;imag=0;}

    其作用是将double型的参数r转换成Complex类的对象,将r作为复数的实部,虚部为0。用户可以根据需要定义转换构造函数,在函数体中告诉编译系统怎样去进行转换。

    在类体中,可以有转换构造函数,也可以没有转换构造函数,视需要而定。以上几种构造函数可以同时出现在同一个类中,它们是构造函数的重载。编译系统会根据建立对象时给出的实参的个数与类型选择形参与之匹配的构造函数。

    4. 类型转换函数

    用转换构造函数可以将一个指定类型的数据转换为类的对象。但是不能反过来将一个类的对象转换为一个其他类型的数据(例如将一个Complex类对象转换成double类型数据)。

    C++提供类型转换函数(type conversion function)来解决这个问题。类型转换函数的作用是将一个类的对象转换成另一类型的数据。如果已声明了一个Complex类,可以在Complex类中这样定义类型转换函数:

    operator double( )

    {return real;}

    类型转换函数的一般形式为

    operator 类型名( )

    {实现转换的语句}

    在函数名前面不能指定函数类型,函数没有参数。其返回值的类型是由函数名中指定的类型名来确定的。类型转换函数只能作为成员函数,因为转换的主体是本类的对象。不能作为友元函数或普通函数。

    从函数形式可以看到,它与运算符重载函数相似,都是用关键字operator开头,只是被重载的是类型名。double类型经过重载后,除了原有的含义外,还获得新的含义(将一个Complex类对象转换为double类型数据,并指定了转换方法)。这样,编译系统不仅能识别原有的double型数据,而且还会把Complex类对象作为double型数据处理。

    例10.9 使用类型转换函数的简单例子。

    #include <iostream>

    using namespace std;

    class Complex

    {public:

    Complex( ){real=0;imag=0;}

    Complex(double r,double i){real=r;imag=i;}

    operator double( ) {return real;} //类型转换函数

    private:

    double real;

    double imag;

    };

    int main( )

    {Complex c1(3,4),c2(5,-10),c3;

    double d;

    d=2.5+c1; //要求将一个double数据与Complex类数据相加

    cout<<d<<endl;

    return 0;

    }

    第11章 继承与派生

    1.菱形继承

    在一个类中保留间接共同基类的多份同名成员,这种现象是人们不希望出现的。

    C++提供虚基类(virtual base class)的方法,使得在继承间接共同基类时只保留一份成员。

    现在,将类A声明为虚基类,方法如下:

    class A//声明基类A

    {…};

    class B :virtual public A //声明类B是类A的公用派生类,A是B的虚基类

    {…};

    class C :virtual public A //声明类C是类A的公用派生类,A是C的虚基类

    {…};

    注意: 虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。声明虚基类的一般形式为

    class 派生类名: virtual 继承方式 基类名

    经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次。

    在派生类B和C中作了上面的虚基类声明后,派生类D中的成员如图11.23所示。

    需要注意: 为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承。如果像图11.24所示的那样,在派生类B和C中将类A声明为虚基类,而在派生类D中没有将类A声明为虚基类,则在派生类E中,虽然从类B和C路径派生的部分只保留一份基类成员,但从类D路径派生的部分还保留一份基类成员。

    2. 虚基类的初始化

    如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生或间接派生的派生类)中,通过构造函数的初始化表对虚基类进行初始化。例如

    class A//定义基类A

    {A(int i){ } //基类构造函数,有一个参数

    …};

    class B :virtual public A //A作为B的虚基类

    {B(int n):A(n){ } //B类构造函数,在初始化表中对虚基类初始化

    …};

    class C :virtual public A //A作为C的虚基类

    {C(int n):A(n){ } //C类构造函数,在初始化表中对虚基类初始化

    …};

    class D :public B,public C //类D的构造函数,在初始化表中对所有基类初始化

    {D(int n):A(n),B(n),C(n){ }

    …};

    注意: 在定义类D的构造函数时,与以往使用的方法有所不同。规定: 在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。

    C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C) 对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。

    3. 基类与派生类的转换

    只有公用派生类才是基类真正的子类型,它完整地继承了基类的功能。

    基类与派生类对象之间有赋值兼容关系,由于派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其子类对象代替。具体表现在以下几个方面:

    (1) 派生类对象可以向基类对象赋值。

    可以用子类(即公用派生类)对象对其基类对象赋值。

    A a1; //定义基类A对象a1

    B b1; //定义类A的公用派生类B的对象b1

    a1=b1; //用派生类B对象b1对基类对象a1赋值

    在赋值时舍弃派生类自己的成员。如图11.26示意。实际上,所谓赋值只是对数据成员赋值,对成员函数不存在赋值问题。

    请注意: 赋值后不能企图通过对象a1去访问派生类对象b1的成员,因为b1的成员与a1的成员是不同的。假设age是派生类B中增加的公用数据成员,分析下面的用法:

    (2) 派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化。

    如已定义了基类A对象a1,可以定义a1的引用变量:

    A a1; //定义基类A对象a1

    B b1; //定义公用派生类B对象b1

    A& r=a1; //定义基类A对象的引用变量r,并用a1对其初始化

    这时,引用变量r是a1的别名,r和a1共享同一段存储单元。也可以用子类对象初始化引用变量r,将上面最后一行改为

    A& r=b1;//定义基类A对象的引用变量r,并用派生类B对象b1对其初始化

    或者保留上面第3行"A& r=a1;",而对r重新赋值:

    r=b1;//用派生类B对象b1对a1的引用变量r赋值

    注意: 此时r并不是b1的别名,也不与b1共享同一段存储单元。它只是b1中基类部分的别名,r与b1中基类部分共享同一段存储单元,r与b1具有相同的起始地址。见图11.27。

    (3) 如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象。

    如有一函数fun:

    void fun(A& r)//形参是类A的对象的引用变量

    {cout<<r.num<<endl;} //输出该引用变量的数据成员num

    函数的形参是类A的对象的引用变量,本来实参应该为A类的对象。由于子类对象与派生类对象赋值兼容,派生类对象能自动转换类型,在调用fun函数时可以用派生类B的对象b1作实参:

    fun(b1);

    输出类B的对象b1的基类数据成员num的值。

    与前相同,在fun函数中只能输出派生类中基类成员的值。

    (4) 派生类对象的地址可以赋给指向基类对象的指针变量,也就是说,指向基类对象的指针变量也可以指向派生类对象。

    第12章 多态与虚函数

    1.在什么情况下应当声明虚函数

    使用虚函数时,有两点要注意:

    (1)只能用virtual声明类的成员函数,使它成为虚函数,而不能将类外的普通函数声明为虚函数。因为虚函数的作用是允许在派生类中对基类的虚函数重新定义。显然,它只能用于类的继承层次结构中

    (2) 一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数。

    2.根据什么考虑是否把一个成员函数声明为虚函数呢?主要考虑以下几点:

    (1) 首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。

    (2) 如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。不要仅仅考虑到要作为基类而把类中的所有成员函数都声明为虚函数。

    (3) 应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。

    (4) 有时,在定义虚函数时,并不定义其函数体,即函数体是空的。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。在12.4节中将详细讨论此问题。

    需要说明的是: 使用虚函数,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function table,简称vtable),它是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联时的时间开销是很少的,因此,多态性是高效的

    3.虚析构函数

    如果将基类的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。

    最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数。这样,如果程序中显式地用了delete运算符准备删除一个对象,而delete运算符的操作对象用了指向派生类对象的基类指针,则系统会调用相应类的析构函数。

    虚析构函数的概念和用法很简单,但它在面向对象程序设计中却是很重要的技巧。专业人员一般都习惯声明虚析构函数,即使基类并不需要析构函数,也显式地定义一个函数体为空的虚析构函数,以保证在撤销动态分配空间时能得到正确的处理。

    构造函数不能声明为虚函数。这是因为在执行构造函数时类对象还未完成建立过程,当然谈不上函数与类对象的绑定。

     

    第13章 输入输出流

    第14章 C++工具

     

    转载于:https://www.cnblogs.com/xkfz007/archive/2012/07/27/2611676.html

    展开全文
  • // Purpose: extract variable outcome for subcatchment // { int errSubID; int cntSub = 0; int sumSubPrd = Nsubcatch * extNperiod; // total for subcatchment outcome float subValue = 0F; /...
  • 采用的是串口中断接收数据,是将接收到的数据放到接受缓存器U2RBR里面的,读GPS的时候就是读的U2RBR的内容,GPS的协议是标准的NMEA0813协议,我用串口发一帧数据如$BDRMC,133413.00,A,4717.13981,N,00833.86256,E,2....
  • mysql 查看索引使用情况这是以为主的线上库root@read 02:28:07>show status like ‘Handler_read%’;+———————–+——-+| Variable_name | Value |+———————–+——-+| Handler_read_first | 0 || ...
  • ``` public class Read3DNetCDF { public static void main(String[] args) { ...原理我理解的是,origin是我读取二维数组的起点,size是我要几个数据,这里面我想从0开始,各3个数据,但是不知道为什么会报错.
  • 译者注:到一篇James Bach最新的文章,说的是如果正确理解和对待测试者的"善变"的.大多数管理者都期望测试工程师能像其他工程师一样,呈现一个相对稳定工作过程和结果,但 由于测试工作的特殊性,这种期望并不十分...
  • n_episodes 的设置问题

    2020-12-08 20:30:25
    性好多了。 在这个版本的代码提交中,所有算法的n_episodes 都被强制为1了。先前版本这个值有5,8,然后所有的被拼接起来。所以现在应该是没有拼接这个步骤了,虽然代码还在(...
  • 在ECONOMETRICS上到一篇新的方法,想去实现这个代码 现在跑一个TENET的程序(具体程序代码见网页https://github.com/QuantLet/TENET/tree/master/TENET_SIM) 目前遇到问题见图片,报错说missing value ...
  • 今天,无论是重构本身,业界对重构的理解,还是开发工具对重构的支持力度,都与本书最初出版时不可同日而语,但书中所蕴涵的意味和精华,依然值得反复咀嚼,而且往往能够常常新。 第1章 重构,第一个案例 1.1 ...
  • 笨办法学Python(四)

    2019-10-06 05:16:40
    习题 4: 变量(variable)和命名 你已经学会了 print 和算术运算。下一步你要学的是“变量”。在编程中,变量只不过是用来指代某个东西的名字。程序员通过使用变量名可以让他们的程序起来更像英语。而且因为程序员...
  • 如果你正在这篇文章时候,很有可能你是在谷歌怎么解决这个问题:你在函数体里面传递一个指针给一个slice或者map,当你通过*variable[0]获取其中item值时候,就会报这个错:“does not support indexing” error ...
  • 今天,无论是重构本身,业界对重构的理解,还是开发工具对重构的支持力度,都与本书最初出版时不可同日而语,但书中所蕴涵的意味和精华,依然值得反复咀嚼,而且往往能够常常新。 第1章 重构,第一个案例 1.1 ...
  • 今天,无论是重构本身,业界对重构的理解,还是开发工具对重构的支持力度,都与本书最初出版时不可同日而语,但书中所蕴涵的意味和精华,依然值得反复咀嚼,而且往往能够常常新。 目录: 第1章 重构,第一个案例...
  • 我想到了两个办法,第一个是通过python,根据不同的参数,生成文件,然后jmeter参数化地文件执行。第二种方法是,通过python,根据不同地参数,更新jmeter脚本相应地参数, 直接执行。参数化有四种:1.用户自定义...
  • 1、变量、函数或类的名称要体现出:它为什么会存在、做什么事、应该怎么用。 2、避免使用与本意相悖的词 3、做有意义的区分:比如ProductInfo或ProductData类,虽然名称不同但是意思无区别。废话都是冗余。...
  • 读书笔记 http://www.runoob.com/python/python-variable-types.html 1.多个变量赋值 a, b, c = 1, 2, “john” ...其实不怎么习惯这种一行多个赋值,感觉java的逻辑上看维护行低。 2.标准数据类型 后来发现
  • 比较易,而且避免与不分大小写的组合键混淆。 以上是MS的官方帮助,下面我们举几个例子来具体说明一下For命令在入侵中的用途。 sample2: 利用For命令来实现对一台目标Win2k主机的暴力密码破解。 我们用...
  • 深入浅出MySQL数据库开发、优化与管理维护

    千次下载 热门讨论 2014-01-21 15:48:01
     20.3.7 InnoDB在不同隔离级别下的一致性及锁的差异   20.3.8 什么时候使用表锁   20.3.9 关于死锁   20.4 小结   第21章 优化MySQLServer   21.1 查看MySQLServer参数   21.2 影响MySQL性能...
  • # 上面从文件中到的目标句子是'X Y Z <eos>'的形式,我们需要从中生成'<sos> X Y Z'形式并加入到Dataset # 编码器只有输入,没有输出,而解码器有输入也有输出,输入为<sos>+(除去最后一位eos的label列表) # ...
  • java 面试题 总结

    2009-09-16 08:45:34
    例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。 当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望...
  • 算法导论(part2)

    2010-09-09 22:54:12
    书中的算法以英语加伪代码的形式给出,只要有一点程序设计经验的人都能懂,并可以用任何计算机语言(如C/C++和Java等)方便地实现。在书中,作者将算法的讨论集中在一些比较现代的例子上,它们来自分子生物学(如...
  • 算法导论(part1)

    2010-09-09 22:51:05
    书中的算法以英语加伪代码的形式给出,只要有一点程序设计经验的人都能懂,并可以用任何计算机语言(如C/C++和Java等)方便地实现。在书中,作者将算法的讨论集中在一些比较现代的例子上,它们来自分子生物学(如...
  • 1.2.1 How can I get/set an environment variable from a program? 我怎样在程序中获得/设置环境变量? 1.2.2 How can I read the whole environment? 我怎样读取整个环境变量表? 1.3 How can I sleep for less ...

空空如也

空空如也

1 2
收藏数 29
精华内容 11
关键字:

variable怎么读的