《C++ Primer Plus(第6版)中文版》
[美] Stephen Prata 2140个笔记
第1章 预备知识
◆ 它不是将重点放在任务上,而是放在表示概念上。
◆ 而泛型编程提供了执行常见任务(如对数据排序或合并链表)的工具
◆ C++融合了OOP、泛型编程和传统的过程性方法,这表明C++强调的是实用价值,而不是意识形态方法,这也是该语言获得成功的原因之一。
◆ 于C++03没有改变语言特性,因此我们使用C++98表示C++98/C++2003。
◆ 实际上,ANSI C加入了C++首次引入的一些特性,如函数原型和类型限定符const。
第2章 开始学习C++
◆ 然而,通常,main( )被启动代码调用,而启动代码是由编译器添加到程序中的,是程序和操作系统(UNIX、Windows 7或其他操作系统)之间的桥梁。事实上,该函数头描述的是main( )和操作系统之间的接口。
◆ 如果编译器到达main( )函数末尾时没有遇到返回语句,则认为main( )函数以如下语句结尾:
◆ 本书中显示用引号括起的字符串时,通常使用换行符\n,在其他情况下则使用控制符endl。一个差别是,endl确保程序继续运行前刷新输出(将其立即显示在屏幕上);而使用“\n”不能提供这样的保证,这意味着在有些系统中,有时可能在您输入信息后才会出现提示。
◆ 每条语句占一行。
◆ 它是一个ostream类对象。ostream类定义(iostream文件的另一个成员)描述了ostream对象表示的数据以及可以对它执行的操作,如将数字或字符串插入到输出流中。
◆ C++提供了两种发送消息的方式:一种方式是使用类方法(本质上就是稍后将介绍的函数调用);另一种方式是重新定义运算符,cin和cout采用的就是这种方式。
◆ C++程序应当为程序中使用的每个函数提供原型
◆ 像变量声明
◆ 第二种方法更好,因为头文件更有可能使原型正确。
◆ 库文件中包含了函数的编译代码,而头文件中则包含了原型。
◆ 这个过程叫做初始化(initialization)
◆ 但C++与C一样,这两种变体都被称为函数
◆ 在使用之前必须提供其原型,通常把原型放到main( )定义之前
◆ 因此,main( )的返回值并不是返回给程序的其他部分,而是返回给操作系统。
◆ 退出值为0则意味着程序运行成功,为非零则意味着存在问题
◆ 另外,main不是关键字
◆ 参数列表指出了何种信息将被传递给函数,函数类型指出了返回值的类型。
◆ 可以采用另一种方法让两个函数都能够访问名称空间std,即将编译指令放在函数的外面,且位于两个函数的前面
◆ C++提供了两个用于处理输入和输出的预定义对象(cin和cout),它们是istream和ostream类的实例,这两个类是在iostream文件中定义的
◆ cin和cout都是智能对象,能够根据程序上下文自动将信息从一种形式转换为另一种形式。
第3章 处理数据
◆ 设计并扩展自己的数据类型
◆ 基本类型和复合类型
◆ 可以使用&运算符来检索braincount的内存地址
◆ 其中每种类型都有符号版本和无符号版本
◆ unsigned long存储不了地球上当前的人数和银河系的星星数,而long long能够
◆ “字节”的含义依赖于实现,因此在一个系统中,两字节的int可能是16位,而在另一个系统中可能是32位
◆ 有些头文件,尤其是那些被设计成可用于C和C++中的头文件,必须使用#define
◆ 在声明变量时对它进行初始化,可避免以后忘记给它赋值的情况发生
◆ 而这正是当前实现中最为常见的行为
◆ int被设置为对目标计算机而言最为“自然”的长度
◆ 如果没有非常有说服力的理由来选择其他类型,则应使用int
◆ 程序将把1492存储为int、long还是其他整型呢?答案是,除非有理由存储为其他类型(如使用了特殊的后缀来表示特定的类型,或者值太大,不能存储为int),否则C++将整型常量存储为int类型
◆ 可以采用任何一种顺序,大写小写均可
◆ cin和cout的行为都是由变量类型引导的
◆ 类ostream有一个put( )成员函数,用来输出字符
◆ 句点被称为成员运算符
◆ 在Release 2.0之后,C++将字符常量存储为char类型,而不是int类型。这意味着cout现在可以正确处理字符常量了
◆ 可替代end
◆ \u后面是8个十六进制位,\U后面则是16个十六进制位
◆ char在默认情况下既不是没有符号,也不是有符号
◆ 显式地将类型设置为signed char 或unsigned char
◆ 如果使用char变量来存储标准ASCII字符,则char有没有符号都没关系,在这种情况下,可以使用char
◆ 这种类型与另一种整型(底层(underlying)类型)的长度和符号属性相同
◆ cin和cout将输入和输出看作是char流
◆ C++将非零值解释为true,将零解释为false
◆ 字面值true和false都可以通过提升转换为int类型,true被转换为1,而false被转换为0
◆ 符号常量—预处理器方法
◆ 如果在声明常量时没有提供值,则该常量的值将是不确定的,且无法修改。
◆ C++内部表示浮点数的方法与此相同,只不过它基于的是二进制数,因此缩放因子是2的幂,不是10的幂
◆ 因此7.2 E6是非法的
◆ float至少32位,double至少48位,且不少于float,long double至少和double一样多
◆ 像8.24和2.4E8这样的浮点常量都属于double类型。如果希望常量为float类型,请使用f或F后缀
◆ 则C++将执行整数除法。这意味着结果的小数部分将被丢弃,使得最后的结果是一个整数
◆ 如果其中有一个(或两个)操作数是浮点值,则小数部分将保留,结果为浮点数
◆ 记住,浮点常量在默认情况下为double类型
◆ 使用相同的符号进行多种操作叫做运算符重载(operator overloading)
◆ C++还允许扩展运算符重载,以便能够用于用户定义的类,因此在这里看到的是一个重要的OOP属性
◆ 将两个short值相加涉及到的硬件编译指令可能会与将两个long值相加不同
◆ 扩展后将得到一个新值,这个值被存储在so_long中,而thirty的内容不变
◆ 因此,有些转换是安全的,有些则会带来麻烦。表3.3列出了一些可能出现的转换问题
◆ 将浮点值赋给整型将导致两个问题。首先,将浮点值转换为整型会将数字截短(除掉小数部分)。其次,float值对于int变量来说可能太大了。在这种情况下,C++并没有定义结果应该是什么;这意味着不同的实现的反应可能不同。
◆ 这导致C++没有对结果进行定义的情况发
◆ C++程序取得chickens和ducks的值,并将它们转换为int。
◆ wchar_t被提升成为下列类型中第一个宽度足够存储wchar_t取值范围的类型
◆ 说明操作数都是整型,因此执行整型提升
◆ 得到的结果取决于何时进行转换。计算auks时,首先将浮点值相加,然后在赋值时,将总数转换为int。但计算bats和coots时,首先通过强制类型转换将浮点值转换为int,然后计算总和
◆ 为此,它重新定义了auto的含义
第4章 复合类型
◆ 事实上,可以将数组中的每个元素看作是一个简单变量
◆ 即其中所有的值在编译时都是已知的
◆ 方法是使用下标或索引来对元素进行编号
◆ 如果将一个值赋给不存在的元素months[101],编译器并不会指出错误
◆ 如果没有初始化函数中定义的数组,则其元素值将是不确定的,这意味着元素的值为以前驻留在该内存单元中的值
◆ 如果将sizeof运算符用于数组名,得到的将是整个数组中的字节数
◆ 则编译器将把其他元素设置为0
◆ 如果初始化数组时方括号内([ ])为空,C++编译器将计算元素个数
◆ 列表初始化禁止缩窄转换
◆ 这两个数组都是char数组,但只有第二个数组是字符串
◆ 但尽管如此,还是不应将不是字符串的字符数组当作字符串来处理
◆ 有一种更好的、将字符数组初始化为字符串的方法—只需使用一个用引号括起的字符串即可,这种字符串被称为字符串常量(string constant)或字符串字面值(string literal)
◆ 用引号括起的字符串隐式地包括结尾的空字符,因此不用显式地包括它(参见图4.2)
◆ 各种C++输入工具通过键盘输入,将字符串读入到char数组中时,将自动加上结尾的空字符
◆ 这是因为处理字符串的函数根据空字符的位置,而不是数组长度来进行处理。C++对字符串长度没有限制
◆ 在确定存储字符串所需的最短数组时,别忘了将结尾的空字符计算在内
◆ 更糟糕的是,"S"实际上表示的是字符串所在的内存地址。
◆ 由于地址在C++中是一种独立的类型,因此C++编译器不允许这种不合理的做法(本章后面讨论指针后,将回过头来讨论这个问题)
◆ sizeof运算符指出整个数组的长度
◆ 但strlen( )函数返回的是存储在数组中的字符串的长度,而不是数组本身的长度
◆ trlen( )只计算可见的字符,而不把空字符计算在内
◆ cin使用空白(空格、制表符和换行符)来确定字符串的结束位置
◆ 这把Dreeb留在输入队列中。当cin在输入队列中搜索用户喜欢的甜点时,它发现了Dreeb,因此cin读取Dreeb,并将它放到dessert数组中
◆ 我们必须使用cin的较高级特性
◆ istream中的类(如cin)提供了一些面向行的类成员函数:getline( )和get( )。这两个函数都读取一行输入,直到到达换行符
◆ 该函数有两个参数。第一个参数是用来存储输入行的数组的名称,第二个参数是要读取的字符数。如果这个参数为20,则函数最多读取19个字符,余下的空间用于存储自动在结尾处添加的空字符。getline( )成员函数在读取指定数目的字符或遇到换行符时停止读取
◆ 但get并不再读取并丢弃换行符,而是将其留在输入队列中。假设我们连续两次调用get( )
◆ 果不借助于帮助,get( )将不能跨过该换行符
◆ 之所以可以这样做,是由于cin.get(name,ArSize)返回一个cin对象,该对象随后将被用来调用get( )函数。同样,下面的语句将把输入中连续的两行分别读入到数组name1和name2 中,其效果与两次调用cin.getline( )相同
◆ getline( )使用起来简单一些,但get( )使得检查错误更简单些
◆ 另一个潜在的问题是,输入字符串可能比分配的空间长。如果输入行包含的字符数比指定的多,则getline( )和get( )将把余下的字符留在输入队列中,而getline( )还会设置失效位,并关闭后面的输入。
◆ 当cin读取年份,将回车键生成的换行符留在了输入队列中
◆ 也可以利用表达式cin>>year返回cin对象,将调用拼接起来
◆ string类位于名称空间std中
◆ 可以使用数组表示法来访问存储在string对象中的字符
◆ 可以将string对象声明为简单变量,而不是数组
◆ str1的声明创建一个长度为0的string对象,但程序将输入读取到str1中时,将自动调整str1的长度
◆ 而string类变量是一个表示字符串的实体
◆ string类简化了字符串合并操作。可以使用运算符+将两个string对象合并起来,还可以使用运算符+=将字符串附加到string对象的末尾
◆ 使用字符数组时,总是存在目标数组过小,无法存储指定信息的危险
◆ 在这里,str1是一个string对象,而size( )是string类的一个方法
◆ 首先,为初始化的数组的内容是未定义的
◆ 函数strlen( )从数组的第一个元素开始计算字节数,直到遇到空字符
◆ str中的字符串长度为0。这是因为未被初始化的string对象的长度被自动设置为0
◆ getline( )使用它来避免超越数组的边界。
◆ 这表明这个getline( )不是类方法
◆ 那么,为何一个getline( )是istream的类方法,而另一个不是呢?
◆ 但前面处理string对象的代码使用string类的一个友元函数。有关友元函数及这种技术为何可行,将在第11章介绍。另外,您可以将cin和cout用于string对象,而不用考虑其内部工作原理
◆ 望有一种数据格式可以将所有这些信息存储在一个单元
◆ 结构是一种比数组更灵活的数据格式,因为同一个结构可以存储多种类型的数据,这使得能够将有关篮球运动员的信息放在一个结构中,从而将数据的表示合并到一起
◆ 首先,定义结构描述—它描述并标记了能够存储在结构中的各种数据类型。然后按描述创建结构变量(结构数据对象)。
◆ 则可能已经注意到了,C++允许在声明结构变量时省略关键字struct
◆ 这种变化强调的是,结构声明定义了一种新类型
◆ 顺便说一句,访问类成员函数(如cin.getline( ))的方式是从访问结构成员变量(如vincent.price)的方式衍生而来的。
◆ 另一种选择是将声明放到main( )的前面,这里采用的便是这种方式,位于函数外面的声明被称为外部声明
◆ 外部变量由所有的函数共
◆ C++不提倡使用外部变量,但提倡使用外部结构声明。另外,在外部声明符号常量通常更合理
◆ 使用由逗号分隔值列表,并将这些值用花括号括起
◆ 一定要让结构定义能够访问名称空间std。为此,可以将编译指令using移到结构定义之前;也可以像前面那样,将name的类型声明为std::string。
◆ 还可以使用赋值运算符(=)将结构赋给另一个同类型的结构,这样结构中每个成员都将被设置为另一个结构中相应成员的值,即使成员是数组
◆ 从中可以看出,成员赋值是有效的,因为choice结构的成员值与bouquet结构中存储的值相同
◆ 还可以声明没有名称的结构类型,方法是省略名称,同时定义一种结构类型和一个这种类型的变量
◆ 但这种类型没有名称,因此以后无法创建这种类型的变量
◆ gifts本身是一个数组,而不是结构,因此像gifts.price这样的表述是无效的
◆ 由于guests是一个inflatable数组,因此guests[0]的类型为inflatable,可以使用它和句点运算符来访问相应inflatable结构的成员
◆ 可以使用没有名称的字段来提供间距。每个成员都被称为位字段(bit field)
◆ 因此,pail有时可以是int变量,而有时又可以是double变量。成员名称标识了变量的容量。由于共用体每次只能存储一个值,因此它必须有足够的空间来存储最大的成员,所以,共用体的长度为其最大成员的长度
◆ 匿名共用体(anonymous union)没有名称,其成员将成为位于相同地址处的变量
◆ 由于共用体是匿名的,因此id_num和id_char被视为prize的两个成员,它们的地址相同,所以不需要中间标识符id_val
◆ C++的enum工具提供了另一种创建符号常量的方式
◆ 但将3赋给band将导致类型错误
◆ 非法的原因有些复杂。确实没有为枚举定义运算符+,但用于算术表达式中时,枚举将被转换为整数,因此表达式orange + red将被转换为1 + 0。这是一个合法的表达式,但其类型为int,不能将其赋给类型为spectrum的变量band。
◆ 则可以通过强制类型转换
◆ rst在默认情况下为0。后面没有被初始化的枚举量的值将比其前面的枚举量大1。因此,third的值为101
◆ 其中6不是枚举值,但它位于枚举定义的取值范围内
◆ 另外,在有些系统中,可能不会将这两个变量存储在相邻的内存单元中。
◆ 使用常规变量时,值是指定的量,而地址为派生量
◆ OOP强调的是在运行阶段(而不是编译阶段)进行决策。
◆ 使用OOP时,您可能在运行阶段确定数组的长度
◆ 使用关键字new请求正确数量的内存以及使用指针来跟踪新分配的内存的位置
◆ 处理存储数据的新策略刚好相反,将地址视为指定的量,而将值视为派生量。一种特殊类型的变量—指针用于存储值的地址。因此,指针名表示的是地址。*运算符被称为间接值(indirect velue)或解除引用(dereferencing)运算符,将其应用于指针,可以得到该地址处存储的值(这和乘法使用的符号相同
◆ 我们说p_updates指向int类型,我们还说p_updates的类型是指向int的指针,或int*
◆ 但要知道的是,下面的声明创建一个指针(p1)和一个int变量(p2)
◆ 对每个指针变量名,都需要使用一个*
◆ 在C++中,int *是一种复合类型,是指向int的指针。
◆ 指针变量不仅仅是指针,而且是指向特定类型的指针
◆ 一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的、适当的地址。这是关于使用指针的金科玉律
◆ 将数字值作为地址来使用,应通过强制类型转换将数字转换为适当的地址类型
◆ 指针真正的用武之地在于,在运行阶段分配未命名的内存以存储值
◆ 并返回该内存块的地址
◆ 它指的是为数据项分配的内存块
◆ 用来指定需要什么样的内存和用来声明合适的指针
◆ 该程序还指出了必须声明指针所指向的类型的原因之一
◆ 它们都是地址,但由于use_new.cpp声明了指针的类型,因此程序知道*pd是8个字节的double值,pt是4个字节的int值。use_new.cpp打印pd的值时,cout知道要读取多少字节以及如何解释它们。
◆ 而new从被称为堆(heap)或自由存储区(free store)的内存区域分配内存
◆ 在C++中,值为0的指针被称为空指针(null pointer)。C++确保空指针不会指向有效的数据,因此它常被用来表示运算符或函数失败(如果成功,它们将返回一个有用的指针
◆ 使用delete时,后面要加上指向内存块的指针(这些内存块最初是用new分配的)
◆ 一定要配对地使用new和delete
◆ 如果内存泄漏严重,则程序将由于不断寻找更多内存而终止
◆ 只能用delete来释放使用new分配的内存。然而,对空指针使用delete是安全的。
◆ 一般来说,不要创建两个指向同一个内存块的指针,因为这将增加错误地删除同一个内存块两次的可能性。但稍后您会看到,对于返回指针的函数,使用另一个指针确实有道理。
◆ 通常,对于大型数据(如数组、字符串和结构),应使用new,这正是new的用武之地
◆ 如果通过声明来创建数组,则在程序被编译时将为它分配内存空间。不管程序最终是否使用数组,数组都在那里,它占用了内存
◆ 静态联编(static binding
◆ 被称为动态联编(dynamic binding)
◆ new运算符返回第一个元素的地址。在这个例子中,该地址被赋给指针psome。
◆ 如果使用new [ ]为数组分配内存,则应使用delete [ ]来释放。如果使用new [ ]为一个实体分配内存,则应使用delete(没有方括号)来释放
◆ 可以将它看作是一根指向该元素的手指
◆ C和C++内部都使用指针来处理数组。数组和指针基本等价是C和C++的优点之一(这在有时候也是个问题,但这是另一码事)
◆ 不能修改数组名的值。但指针是变量,因此可以修改它的值。请注意将p3加1的效果。表达式p3[0]现在指的是数组的第2个值。因此,将p3加1导致它指向第2个元素而不是第1个。将它减1后,指针将指向原来的值,这样程序便可以给delete[ ]提供正确的地址。
◆ 但将指针变量加1后,增加的量等于它指向的类型的字节数。将指向double的指针加1后,如果系统对double使用8个字节存储,则数值将增加8;将指向short的指针加1后,如果系统对short使用2个字节存储,则指针值将增加2
◆ C++将数组名解释为数组第1个元素的地址
◆ 现在来看一看数组表达式stacks[1]。C++编译器将该表达式看作是*(stacks + 1),这意味着先计算数组第2个元素的地址,然后找到存储在那里的值。
◆ 区别之一是,可以修改指针的值,而数组名是常量
◆ 对数组应用sizeof运算符得到的是数组的长度,而对指针应用sizeof得到的是指针的长度,即使指针指向的是一个数组
◆ 对数组取地址时,数组名也不会被解释为其地址。等等,数组名难道不被解释为数组的地址吗?不完全如此:数组名被解释为其第一个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址:
◆ &tell[0](即tell)是一个2字节内存块的地址,而&tell是一个20字节内存块的地址
◆ 如果省略括号,优先级规则将使得pas先与[20]结合,导致pas是一个short指针数组,它包含20个元素,因此括号是必不可少的
◆ 一种例外情况是,将sizeof运算符用于数组名用时,此时将返回整个数组的长度(单位为字节)。
◆ 使用数组声明来创建数组时,将采用静态联编
◆ 将采用动态联编(动态数组)
◆ 这里的关键不在于flower是数组名,而在于flower是一个char的地址
◆ 逐个传递字符串中的所有字符相比,这样做的工作量确实要少
◆ 在cout和多数C++表达式中,char数组名、char指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址
◆ 编译器在内存留出一些空间,以存储程序源代码中所有用引号括起的字符串,并将每个被存储的字符串与其地址关联起来。)这意味着可以像使用字符串“wren”那样使用指针bird,如下面的示例所示:
◆ 字符串字面值是常量,这就是为什么代码在声明中使用关键字const的原因。以这种方式使用const意味着可以用bird来访问字符串,但不能修改它
◆ 创建未初始化的指针有点像签发空头支票:无法控制它将被如何使用
◆ 有些编译器将字符串字面值视为只读常量,如果试图修改它们,将导致运行阶段错误
◆ 则必须将这种指针强制转换为另一种指针类型
◆ 这两个指针将指向相同的内存单元和字符串。
◆ 将animal赋给ps是不可行的,因为这样只能修改存储在ps中的地址,从而失去程序访问新分配内存的唯一途径。
◆ 要避免这种问题,请使用strncpy( )
◆ 这样最多将19个字符复制到数组中,然后将最后一个元素设置成空字符。如果该字符串少于19个字符,则strncpy( )将在复制完该字符串之后加上空字符,以标记该字符串的结尾
◆ 应使用strcpy( )或strncpy( ),而不是赋值运算符来将字符串赋给数组。
◆ 在运行时创建数组优于在编译时创建数组,对于结构也是如此。需要在程序运行时为结构分配所需的空间,这也可以使用new运算符来完成
◆ 这将把足以存储inflatable结构的一块可用内存的地址赋给ps。这种句法和C++的内置类型完全相同
◆ 有时,C++新手在指定结构成员时,搞不清楚何时应使用句点运算符,何时应使用箭头运算符。规则非常简单。如果结构标识符是结构名,则使用句点运算符;如果标识符是指向结构的指针,则使用箭头运算符
◆ 因此(*ps).price是该结构的price成员
◆ (实际编写程序时,使用string类将更容易,因为这样可以使用内置的new和delete)
◆ 获得空间后,getname( )使用标准库函数strcpy( )将temp中的字符串复制到新的内存块中。
◆ 在函数内部定义的常规变量使用自动存储空间,被称为自动变量(automatic variable)
◆ 自动变量是一个局部变量,其作用域为包含它的代码块
◆ 自动变量通常存储在栈中。这意味着执行代码块时,其中的变量将依次加入到栈中,而在离开代码块时,将按相反的顺序释放这些变量,这被称为后进先出(LIFO)
◆ 静态存储是整个程序执行期间都存在的存储方式。使变量成为静态的方式有两种:一种是在函数外面定义它;另一种是在声明变量时使用关键字static
◆ 自动存储和静态存储的关键在于:这些方法严格地限制了变量的寿命。
◆ new和delete运算符提供了一种比自动变量和静态变量更灵活的方法。它们管理了一个内存池,这在C++中被称为自由存储空间(free store)或堆(heap)
◆ 使用new和delete让程序员对程序如何使用内存有更大的控制权
◆ 但new和delete的相互影响可能导致占用的自由存储区不连续,这使得跟踪新分配内存的位置更困难
◆ 即使包含指针的内存由于作用域规则和对象生命周期的原因而被释放,在自由存储空间上动态分配的变量或结构也将继续存在
◆ C++智能指针有助于自动完成这种任务
◆ 模板类vector类似于string类,也是一种动态数组
◆ 基本上,它是使用new创建动态数组的替代品。实际上,vector类确实使用new和delete来管理内存,但这种工作是自动完成的
◆ 其中参数n_elem可以是整型常量,也可以是整型变量。
◆ vector类的功能比数组强大,但付出的代价是效率稍低
◆ 首先,注意到无论是数组、vector对象还是array对象,都可使用标准数组表示法来访问各个元素
◆ array对象和数组存储在相同的内存区域(即栈)
◆ 注意到可以将一个array对象赋给另一个array对象
◆ 而对于数组,必须逐元素复制数据。
◆ 与C语言一样,C++也不检查这种超界错误。
◆ 中括号表示法和成员函数at()的差别在于,使用at()时,将在运行期间捕获非法索引,而程序默认将中断。这
◆ 如果数据对象是数组,则可以像使用数组名那样使用指针来访问元素;如果数据对象是结构,则可以用指针解除引用运算符(->)来访问其成员。
第5章 循环和关系表达式
◆ 另外,常见的做法是缩进for语句体,使它看上去比较显著
◆ 在C++中,每个表达式都有值
◆ C++将赋值表达式的值定义为左侧成员的值,因此这个表达式的值为20
◆ 赋值运算符是从右向左结合的
◆ C++必须将100赋给x。当判定表达式的值这种操作改变了内存中数据的值时,我们说表达式有副作用(side effect)
◆ 因此,判定赋值表达式会带来这样的副作用,即修改被赋值者的值。有可能把赋值看作预期的效果,但从C++的构造方式这个角度来看,判定表达式才是主要作用。
◆ 从表达式到语句的转变很容易,只要加分号即可
◆ 智能编译器甚至可能跳过这条语句
◆ 但int toad并不是表达式,因为它没有值。
◆ 微妙
◆ 这很方便,但并不适用于原来的句法,因为声明不是表达式。
◆ 声明语句表达式
◆ 对于for-init-statement来说,它既可以是表达式语句,也可以是声明。这种句法规则用语句替换了后面跟分号的表达式,语句本身有自己的分
◆ 初始化部分中声明和初始化变量
◆ 通过使用符号常量,就可以避免将所有的10修改为20。
◆ 因此在外部声明ArSize几乎没有任何实际用处,但包含多个函数的程序常常会受益于共享外部常量,因此我们现在就开始练习使用外部变量。
◆ 这个示例使用了using声明,而不是using编译指令。
◆ string类的size( )获得字符串中的字符数
◆ 对这种语句,C++没有定义正确的行为
◆ 首先,副作用(side effect)指的是在计算表达式时对某些东西(如存储在变量中的值)进行了修改;
◆ 在C++中,语句中的分号就是一个顺序点,这意味着程序处理下一条语句之前,赋值运算符、递增运算符和递减运算符执行的所有修改都必须完成。
◆ 在C++11文档中,不再使用术语“顺序点”了,因为这个概念难以用于讨论多线程执行。相反,使用了术语“顺序”,它表示有些事件在其他事件前发生。这种描述方法并非要改变规则,而旨在更清晰地描述多线程编程。
◆ 前缀格式和后缀格式的最终效果相同
◆ 用户这样定义前缀函数:将值加1,然后返回结果;但后缀版本首先复制一个副本,将其加1,然后将复制的副本返回
◆ 总之,对于内置类型,采用哪种格式不会有差别;但对于用户定义的类型,如果有用户定义的递增和递减运算符,则前缀格式的效率更高
◆ 将把指针的值增加其指向的数据类型占用的字节数,这种规则适用于对指针递增和递减
◆ 前缀运算符的从右到到结合规则意味着*++pt的含义如下:现将++应用于pt(因为++位于的右边),然后将应用于被递增后的p
◆ 因此*pt++的值为arr[2],即25.4,但该语句执行完毕后,pt的值将为arr[3]的地址
◆ 方法是用两个花括号来构造一条复合语句(代码块)
◆ 如果在语句块中定义一个新的变量,则仅当程序执行该语句块中的语句时,该变量才存在。执行完该语句块后,变量将被释放。
◆ 逗号运算符对表达式完成同样的任务
◆ 这是因为声明已经将逗号用于其他用途—分隔列表中的变量
◆ 在这种情况下,逗号只是一个列表分隔符,而不是逗号运算符
◆ 这样,temp在每轮循环中都将被分配和释放。这比在循环前声明temp的速度要慢一些。另一方面,如果在循环内部声明temp,则它将在循环结束后被丢弃。
◆ 首先,它确保先计算第一个表达式,然后计算第二个表达式(换句话说,逗号运算符是一个顺序点)。
◆ 逗号表达式的值是第二部分的值
◆ 在所有运算符中,逗号运算符的优先级是最低的
◆ 它能够对值进行比较,这种能力是计算机决策的基础
◆ 如果不小心遗漏了= =运算符中的一个等号,则for循环的测试部分将是一个赋值表达式,而不是关系表达式,此时代码仍是有效的。这是因为可以将任何有效的C++表达式用作for循环的测试条件。
◆ 所以始终为true
◆ 因此很多编译器都会发出警告,询问这是否是设计者的真正意图)
◆ 用引号括起的字符串常量也是其地址
◆ 两个字符串的地址是否相同呢?答案是否定的,虽然它们包含相同的字符。
◆ 应使用C-风格字符串库中的strcmp( )函数来比较
◆ 这意味着两个字符串即使被存储在长度不同的数组中,也可能是相同的:
◆ 但却可以用它们来比较字符,因为字符实际上是整型
◆ 因为类设计让您能够使用关系运算符进行比较
◆ 读者现在只需知道可以将关系运算符用于string对象即可。
◆ tring类重载运算符!=的方式让您能够在下述条件下使用它:至少有一个操作数为string对象,另一个操作数可以是string对象,也可以是C-风格字符串
◆ 在C++中,for和while循环本质上是相同的
◆ 省略for循环中的测试表达式时,测试结果将为true,因此下面的循环将一直运行下去
◆ 如果程序在显示其他内容之前等待5秒钟,情况将会好得多。while循环可用于这种目的。一种用于个人计算机的早期技术是,让计算机进行计数,以等待一段时
◆ 一种是使用预处理器:
◆ 编译程序时
◆ 第二种方法是使用C++(和C)的关键字typedef来创建别名
◆ 可以使用#define,不过声明一系列变量时,这种方法不适用
◆ 使得与使用#define相比,使用typedef是一种更佳的选择—有时候,这也是唯一的选择。
◆ C++11新增了一种循环:基于范围(range-based)的for循环
◆ 读取char值时,与读取其他基本类型一样,cin将忽略空格和换行符。因此输入中的空格没有被回显,也没有被包括在计数内。
◆ 按下回车键后,整个字符序列将被发送给程序,但程序在遇到#字符后将结束对输入的处理。
◆ 该程序回显了每个字符,并将全部字符计算在内,其中包括空格。输入仍被缓冲,因此输入的字符个数仍可能比最终到达程序的要多。
◆ 头文件iostream将cin.get(ch)的参数声明为引用类型,因此该函数可以修改其参数的值
◆ 因为该语言支持被称为函数重载的OOP特性
◆ 则可以使用一种功能更强大的技术—检测文件尾(EOF)
◆ <符号是Unix和Windows命令提示符模式的重定向运算符。
◆ 很多操作系统都允许通过键盘来模拟文件尾条件
◆ cin方法检测到EOF时,将设置cin对象中一个指示EOF条件的标记
◆ cin.clear( )方法可能清除EOF标记
◆ 如果使用cin.get( )来锁住屏幕直到可以阅读它,这将不起作用,因为检测EOF时将禁止进一步读取输入。
◆ 单5.14那样的计时循环来使屏幕停留一段时间
◆ 函数调用cin.get(ch1)返回一个cin对象,然后便可以通过该对象调用get(ch2)
◆ 对于二维数组来说,由于每个元素本身就是一个数组,因此可以使用与上述代码类似的格式来初始化每一个元素
◆ 在这个例子中,可以使用char数组的数组,而不是字符串指针数组
◆ 因此,从存储空间的角度说,使用指针数组更为经济
◆ 在希望字符串是可修改的情况下,string类自动调整大小的特性将使这种方法比使用二维数组更为方便。
第6章 分支语句和逻辑运算符
◆ 通常情况下,测试条件都是关系表达式,如那些用来控制循环的表达式
◆ 该程序清单还演示了限定符std::,这是编译指令using的替代品之一。
◆ 然而,很多编译器会发出警告,因此注意警告是明智的
◆ 由于||的优先级比关系运算符低,因此不需要在这些表达式中使用括号
◆ C++规定,||运算符是个顺序点(sequence point)
◆ 另外,如果左侧的表达式为true,则C++将不会去判定右侧的表达式,因为只要一个表达式为true,则整个逻辑表达式为true(读者可能还记得,冒号和逗号运算符也是顺序点)。
◆ 和||运算符一样,&&运算符也是顺序点,因此将首先判定左侧,并且在右侧被判定之前产生所有的副作用。
◆ double类型的精度足以存储典型的int值,且取值范围更大。另一种选择是,使用long long来存储输入的值,因为其取值范围比int大。
◆ 外,它们并不是C语言中的保留字,但C语言程序可以将它们用作运算符,只要在程序中包含了头文件iso646.h。C++不要求使用头文件
◆ 它是C++中唯一一个需要3个操作数的运算符
◆ 则整个表达式的值
◆ 每个标签都必须是整数常量表达式。最常见的标签是int或char常量(如1或'q'),也可以是枚举量
◆ 如果integer-expression不与任何标签匹配,则程序将跳到标签为default的那一行。Default标签是可选的,
◆ C++中的case标签只是行标签,而不是选项之间的界线。也是说,程序跳到switch中特定代码行后,将依次执行之后的所有语句,除非有明确的其他指示。程序不会在执行到下一个case处自动停止,要让程序执行完一组特定语句后停止,必须使用break语句。这将导致程序跳到switch后面的语句处执行。
◆ 通常,cin无法识别枚举类型(它不知道程序员是如何定义它们的),因此该程序要求用户选择选项时输入一个整数。
◆ 将int值和枚举量标签进行比较时,将枚举量提升为int
◆ 也会将枚举量提升为int类型
◆ 另外,这个值必须是整数(包括char
◆ 另外case标签值还必须是常量
◆ switch语句的效率更高。
◆ 虽然continue语句导致该程序跳过循环体的剩余部分,但不会跳过循环的更新表达式
◆ 使用goto语句不好,而应使用结构化控制语句(如if else、switch、continue等)来控制程序的流程。
◆ 非数字输入设置错误标记意味着必须重置该标记,程序才能继续读取输入。clear( )方法重置错误输入标记,同时也重置文件尾(EOF条件,参见第5章)。
◆ 着用cin将输入放到数组中
◆ 重置cin以接受新的输入
◆ 删除错误输入
◆ 提示用户再输入
◆ 该循环的第一条语句使用clear( )方法重置输入,如果省略这条语句,程序将拒绝继续读取输入
◆ 程序在while循环中使用cin.get( )来读取行尾之前的所有输入
◆ 使用cin进行输入时,程序将输入视为一系列的字节,其中每个字节都被解释为字符编码。不管目标数据类型是什么,输入一开始都是字符数据——文本数据。
◆ cin对象负责将文本转换为其他类型
◆ 输入行中的第一个字符被赋给ch。在这里,第一个字符是数字3,其字符编码(二进制)被存储在变量ch中。
◆ 在这种情况下,cin将不断读取,直到遇到非数字字符。也就是说,它将读取3和8,这样句点将成为输入队列中的下一个字符
◆ 在这种情况下,cin将不断读取,直到遇到第一个不属于浮点数的字符。也就是说,cin读取3、8、句点和5,使得空格成为输入队列中的下一个字符
◆ 空字符
◆ 在这种情况下,cin将不断读取,直到遇到换行符(示例输入行少于50个字符)
◆ 换行符被丢弃
◆ 输入一开始为文本。因此,控制台输入的文件版本是文本文件,即每个字节都存储了一个字符编码的文件。
◆ 另外,字处理文件中可能包含文本信息,但也可能包含用于描述格式、字体、打印机等的非文本数据
◆ 下面来复习一些有关将cout用于控制台输出的基本事实,为文件输出做准备
◆ 方法open( )接受一个C-风格字符串作为参数,这可以是一个字面字符串,也可以是存储在数组中的字符串
◆ 声明一个ofstream对象并将其同文件关联起来后,便可以像使用cout那样使用它。所有可用于cout的操作和方法(如<<、endl和setf( ))都可用于ofstream对象(如前述示例中的outFile和fout)。
◆ 声明一个ofstream对象后,便可以使用方法open( )将该对象特定文件关联起来
◆ 创建好ofstream对象(如outFile)后,便可以像使用cout那样使用它
◆ open( )将首先截断该文件,即将其长度截短到零——丢其原有的内容,然后将新的输出加入到该文件中。
◆ 检查文件是否被成功打开至关重
◆ 声明ifstream和ofstream对象,并将它们同文件关联起来后,便可以像使用cin和cout那样使用这些对象
第7章 函数——C++的编程模块
◆ 库函数是已经定义和编译好的函数,同时可以使用标准库头文件提供其原型,因此只需正确地调用这种函数即可。
◆ 在每个函数定义中,都使用了一条using编译指令,因为每个函数都使用了cout。另一种方法是,在函数定义之前放置一条using编译指令或在函数中使用std::cout。
◆ 没有返回值的函数被称为void函数
◆ 参数列表int n意味着调用函数cheers( )时,应将一个int值作为参数传递给它。
◆ 是其结果的类型必须为typeName类型或可以被转换为typeName
◆ 不能是数组,但可以是其他任何类型——整数、浮点数、指针,甚至可以是结构和对象
◆ 可以将数组作为结构或对象组成部分来返回
◆ 回函数和调用函数必须就该内存单元中存储的数据的类型达成一致
◆ 函数在执行遇到的第一条返回语句后结束
◆ ,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。
◆ C++的编程风格是将main( )放在最前面,因为它通常提供了程序的整体结构。
◆ 函数原型不要求提供变量名,有类型列表就足够了。
◆ 在C++中,括号为空与在括号中使用关键字void是等效的——意味着函数没有参数。
◆ 如果不正确,则转换为正确的类型(如果可能的话)
◆ 原型自动将被传递的参数强制转换为期望的类型。
◆ 第8章将介绍的函数重载可能导致二义性,因此不允许某些自动强制类型转换
◆ 仅当有意义时,原型化才会导致类型转换。例如,原型不会将整数转换为结构或指针。
◆ 用于接收传递值的变量被称为形参。传递给函数的值被称为实参。
◆ 在函数结束时,计算机将释放这些变量使用的内存
◆ 则必须分别指定每个参数的类型,而不能像声明常规变量那样,将声明组合在一起
◆ cin.get( )将读取后面的换行符。可以通过编程来避开这种麻烦,但比较简便的方法是像该程序那样使用cin。
◆ 下面来如何将数组和函数结合在一起
◆ 为使函数通用,而不限于特定长度的数组,还需要传递数组长度。这里唯一的新内容是,需要将一个形参声明为数组名。
◆ arr实际上并不是数组,而是一个指针!好消息是,在编写函数的其余部分时,可以将arr看作是数组。
◆ C++将数组名解释为其第一个元素的地址
◆ 该规则有一些例外。首先,数组声明使用数组名来标记存储位置;其次,对数组名使用sizeof将得到整个数组的长度
◆ 这证明这两个函数头都是正确的,因为在C++中,当(且仅当)用于函数头或函数原型中,int *arr和int arr [ ]的含义才是相同的。
◆ 在其他的上下文中,int * arr和int arr [ ]的含义并不相同。例如,不能在函数体中使用int tip[ ]来声明指针。
◆ sum_arr( )函数将cookies的地址赋给指针变量arr,将ArSize赋给int变量n
◆ 程序清单7.5实际上并没有将数组内容传递给函数,而是将数组的位置(地址)、包含的元素种类(类型)以及元素数目(n变量)提交给函数(参见图7.4)。
◆ 传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。
◆ 但这个值是一个地址,而不是数组的内容。
◆ 数组地址作为参数可以节省复制整个数组所需的时间和内存。
◆ 使用原始数据增加了破坏数据的风险
◆ cookies和arr的值相同。它还演示了指针概念如何使sum_arr函数比以前更通用
◆ 而sizeof arr只是指针变量的长度
◆ 必须显式传递数组长度
◆ 还可以提供假的数组起始位置:
◆ 由于接受数组名参数的函数访问的是原始数组,而不是其副本,因此可以通过调用该函数将值赋给数组元素。
◆ 该函数返回实际读取的元素数
◆ 还有另一个问题——确保显示函数不修改原始数组
◆ 这是由于C++按值传递数据,而且函数使用数据的副本
◆ 这意味着不能使用ar修改该数据,也就是说,可以使用像ar[0]这样的值,但不能修改。
◆ 这并不是意味着原始数组必须是常量,而只是意味着不能在show_array( )函数中使用ar来修改这些数据
◆ 这条消息提醒用户,C++将声明const double ar [ ]解释为const double *ar。因此,该声明实际上是说,ar指向的是一个常量值
◆ 由于这个函数将修改数组的值,因此在声明ar时,不能使用const。
◆ lboud + 20指向数组结尾后面的一个位置。将区间传递给函数将告诉函数应处理哪些元素。
◆ 第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。
◆ 该声明指出,pt指向一个const int(这里为39),因此不能使用pt来修改这个值。
◆ pt的声明并不意味着它指向的值实际上就是一个常量
◆ C++禁止第二种情况的原因很简单——如果将g_moon的地址赋给pm,则可以使用pm来修改g_moon的值,这使得g_moon的const状态很荒谬,因此C++禁止将const的地址赋给非const指针
◆ 假设有一个由const数据组成的数组:
◆ 禁止将常量数组的地址赋给非常量指针将意味着不能将数组名作为参数传递给使用非常量形参的函数:
◆ 但仍然不能使用pt来修改它指向的值(现在为80)。
◆ 这种声明格式使得finger只能指向sloth,但允许使用finger来修改sloth的值。
◆ 但允许将ps指向另一个位置。简而言之,finger和ps都是const,而finger和ps不是
◆ const的指针来保护数据
◆ 只要只有一层间接关系,就可以使用这种技术。例如,这里的数组元素是基本类型,但如果它们是指针或指向指针的指针,则不能使用const。
◆ 比较难处理的是如何正确地声明指针
◆ 4个指向int的指针组成的数组
◆ 它指向由4个int组成的数组。因此,指针类型指定了列数,这就是没有将列数作为独立的函数参数进行传递的原因。
◆ 由于指针类型指定了列数,因此sum( )函数只能接受由4列组成的数组
◆ 必须对指针ar2执行两次解除引用,才能得到数据。最简单的方法是使用方括号两次:ar2[r][c]。然而,如果不考虑难看的话,也可以使用运算符*两次:
◆ 将字符串作为参数时意味着传递的是地址,但可以使用const来禁止对字符串参数进行修改。
◆ 被设置为字符串的地址的char指针。
◆ 但实际传递的是字符串第一个字符的地址
◆ 字符串有内置的结束字符
◆ 这意味着不必将字符串长度作为参数传递给函数
◆ 不应
◆ 参数不一定必须是数组名,也可以是其他形式的指针
◆ str将指向结尾的空值字符,使得*str等于0——空值字符的数字编码,从而结束循环。
◆ 函数无法返回一个字符串,但可以返回字符串的地址,这样做的效率更高。
◆ 需要能够存储n + 1个字符的空间,以便能够存储空值字符
◆ pstr(而不是字符串)使用的内存将被释放。
◆ delete释放
◆ 读者将知道C++类如何使用构造函数和析构函数负责为您处理这些细节。
◆ 但在涉及到函数时,结构变量的行为更接近于基本的单值变量。
◆ 结构将其数据组合成单个实体或数据对象,该实体被视为一个整体
◆ 函数将使用原始结构的副本
◆ 使用结构编程时,最直接的方式是像处理基本类型那样来处理结构;也就是说,将结构作为参数传递,并在需要时将结构用作返回值使用。
◆ 按值传递结构有一个缺点。如果结构非常大,则复制结构将增加内存要求,降低系统运行的速度
◆ 许多C程序员倾向于传递结构的地址
◆ C++提供了第三种选择——按引用传递
◆ 距离和角度一起构成了极坐标
◆ 该函数接受一个rect参数,并返回一个polar结构
◆ 有些编译器仅当被明确指示后,才会搜索数学库
◆ 另外请记住,非数字输入将设置一个错误条件,禁止进一步读取输入。如果程序在输入循环后还需要进行输入,则必须使用cin.clear( )重置输入,然后还可能需要通过读取不合法的输入来丢弃它们
◆ string对象与结构的更相似
◆ 可以将一个结构赋给另一个结构,也可以将一个对象赋给另一个对
◆ 可以将结构作为完整的实体传递给函数,也可以将对象作为完整的实体进行传递。如果需要多个字符串,可以声明一个string对象数组
◆ 该程序像对待内置类型(如int)一样对待string对象。
◆ 在C++中,类对象是基于结构的,因此结构编程方面的有些考虑因素也适用于类。
◆ 函数处理的是原始对象的副本
◆ 模板array并非只能存储基本数据类型,它还可存储类对象
◆ std::限定array和string
◆ 函数fill()使用指针来直接处理原始对象,这避免了上述效率低下的问题
◆ 因此*pa为这种对象,而(*pa) [i]是该对象的一个元素。
◆ 运算符优先级的影响,其中的括号必不可少
◆ 然而,与C语言不同的是,C++不允许main( )调用自己
◆ 每个递归调用都创建自己的一套变量
◆ 函数的地址是存储其机器语言代码的内存的开始地址
◆ 为实现这种目标,采用的机制是,将程序员要使用的算法函数的地址传递给estimate( )
◆ 要将函数作为参数进行传递,必须传递函数名
◆ 一定要区分传递的是函数的地址还是函数的返回值
◆ 声明指向某种数据类型的指针时,必须指定指针指向的类型
◆ 这意味着声明应指定函数的返回类型以及函数的特征标
◆ 由于pam是函数,因此(*pf)也是函数。而如果(*pf)是函数,则pf就是函数指针。
◆ 正确地声明pf后,便可以将相应函数的地址赋给它:
◆ 函数指针的表示可能非常恐怖。下面通过一个示例演示使用函数指针时面临的一些挑战。首先,下面是一些函数的原型,它们的特征标和返回类型相同:
◆ 另一方面,函数定义必须提供标识符,因此需要使用const double ar [ ]或const double * ar
◆ 使用C++11的自动类型推断功能时,代码要简单得多
◆ 因此,pa是一个包含三个指针的数组,其中每个指针都指向这样的函数,即将const double *和int作为参数,并返回一个const double *。
◆ 自动类型推断只能用于单值初始化,而不能用于初始化列表
◆ 自动类型推断功能表明,编译器的角色发生了改变
◆ 自动类型推断确保变量的类型与赋给它的初值的类型一致
◆ 函数是C++的编程模块。要使用函数,必须提供定义和原型,并调用该函数。函数定义是实现函数功能的代码;
◆ string类的方法size( )可用于判断其存储的字符串的长度
◆ 这意味着可以按值传递结构,并将其用作函数返回类型。然而,如果结构非常大,则传递结构指针的效率将更高,同时函数能够使用原始数据。这些考虑因素也适用于类对象
第一十三章 第8章 函数探幽
◆ 新特性包括内联函数、按引用传递变量、默认的参数值、函数重载(多态)以及模板函数
◆ 内联函数是C++为提高程序运行速度所做的一项改进
◆ C++编译器如何将它们组合到程序中
◆ 来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。
◆ 内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码替换函数调用
◆ 内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本
◆ 将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。
◆ 内联函数不能递归
◆ 这使得C++的内联功能远远胜过C语言的宏定义
◆ 而是要指出,如果使用C语言的宏执行了类似函数的功能,应考虑将它们转换为C++内联函数
◆ 引用是已定义的变量的别名(另一个名称)
◆ 但引用变量的主要用途是用作函数的形参。
◆ 这样除指针之外,引用也为函数处理大型结构提供了一种非常方便的途径,同时对于设计类来说,引用也是必不可少的。
◆ 语句中的&运算符不是地址运算符,而是将rodents的类型声明为int &,即指向int变量的引用
◆ 将rodents加1将影响这两个变量。更准确地说,rodents++操作将一个有两个名称的变量加1。
◆ 差别之一是,必须在声明引用时将其初始化,而不能像指针那样,先声明,再赋值
◆ 引用更接近const指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它。
◆ 简而言之,可以通过初始化声明来设置引用,但不能通过赋值来设置
◆ 将rodents初始化为*pt使得rodents指向rats。接下来将pt改为指向bunnies,并不能改变这样的事实,即rodents引用的是rats
◆ 引用经常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名。这种传递参数的方法称为按引用传递
◆ 2024/03/21发表想法
本质上,依然是按值传递
原文:C语言只能按值传递。按值传递导致被调用函数使用调用程序的值的拷贝(参见图8.2)。当然,C语言也允许避开按值传递的限制,采用按指针传递的方式。
◆ C语言只能按值传递。按值传递导致被调用函数使用调用程序的值的拷贝(参见图8.2)。当然,C语言也允许避开按值传递的限制,采用按指针传递的方式。
◆ 这意味着按值传递变量将不管用,因为函数将交换原始变量副本的内容,而不是变量本身的内容。
◆ 另一个区别是指针版本需要在函数使用p和q的整个过程中使用解除引用运算符*。
◆ 函数调用使用实参初始化形参,因此函数的引用参数被初始化为函数调用传递的实参
◆ refcube( )函数修改了main( )中的x值,而cube( )没有,这提醒我们为何通常按值传递。
◆ 如果程序员的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用
◆ 编译器发现代码修改了ra的值时,将生成错误消息。
◆ 如果要编写类似于上述示例的函数(即使用基本数值类型),应采用按值传递的方式,而不要采用按引用传递的方式。当数据比较大(如结构和类)时,引用参数将很有用
◆ 按值传递的函数
◆ 在现代的C++中,这是错误的,大多数编译器都将指出这一点;而有些较老的编译器将发出这样的警告
◆ 由于x + 3.0不是double类型的变量,因此程序将创建一个临时的无名变量,并将其初始化为表达式x + 3.0的值
◆ 然后,ra将成为该临时变量的引用
◆ 如果实参与引用参数不匹配,C++将生成临时变量
◆ 则编译器将在下面两种情况下生成临时变量:实参的类型正确,但不是左值;实参的类型不正确,但可以转换为正确的类型。
◆ 左值参数是可被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针都是左值。
◆ 常规变量和const变量都可视为左值,因为可通过地址访问它们。但常规变量属于可修改的左值,而const变量属于不可修改的左值。
◆ 另一方面,参数7.0和side + 10.0的类型都正确,但没有名称,在这些情况下,编译器都将生成一个临时匿名变量,并让ra指向它。这些临时变量只在函数调用期间存在,此后编译器便可以随意将其删除。
◆ 这里的类型不匹配,因此编译器将创建两个临时int变量,将它们初始化为3和5,然后交换临时变量的内容,而a和b保持不变
◆ 简而言之,如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。
◆ 如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。
◆ 使用const使函数能够处理const和非const实参,否则将只能接受非const数据;
◆ 使用const引用使函数能够正确生成并使用临时变量。
◆ 因此,应尽可能将引用形参声明为const。
◆ C++11新增了另一种引用——右值引用(rvalue reference)。这种引用可指向右值,是使用&&声明的:
◆ 引用非常适合用于结构和类(C++的用户定义类型)。确实,引入引用主要是为了用于这些类型的,而不是基本的内置类型。
◆ 如果返回类型被声明为free_throws而不是free_throws &,上述返回语句将返回target(也就是team)的拷贝。但返回类型为引用,这意味着返回的是最初传递给accumulate()的team对象。
◆ 在第一条语句中,值4.0被复制到一个临时位置,然后被复制给m
◆ 如果accumulate()返回一个结构,而不是指向结构的引用,将把整个结构复制到一个临时位置,再将这个拷贝复制给dup。但在返回值为引用时,将直接把team复制到dup,其效率更高。
◆ 返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元引用
◆ 该函数返回一个指向临时变量(newguy)的引用,函数运行完毕后它将不再存在。第9章将讨论各种变量的持续性。同样,也应避免返回指向临时变量的指针。
◆ 返回一个作为参数传递给函数的引用
◆ 作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。
◆ 另一种方法是用new来分配新的存储空间。
◆ 也就是说,在赋值表达式中,左边的子表达式必须标识一个可修改的内存块。在这里,函数返回指向dup的引用,它确实标识的是一个这样的内存块,因此这条语句是合法的。
◆ 但为何常规函数返回值是右值呢?这是因为这种返回值位于临时内存单元中,运行到下一条语句时,它们可能不再存在。
◆ 假设您要使用引用返回值,但又不允许执行像给accumulate()赋值这样的操作,只需将返回类型声明为const引用:
◆ 现在返回类型为const,是不可修改的左值,因此下面的赋值语句不合法:
◆ 另外,您仍可以在赋值语句右边使用accumulate()。
◆ 创建一个函数,它将指定的字符串加入到另一个字符串的前面和后面
◆ 然而其中的一个存在非常大的缺陷,可能导致程序崩溃甚至不同通过编译。
◆ 这两个函数参数都是const引用。如果使用string对象作为参数,最终结果将不变
◆ 因此该函数的返回类型为string,这意味着temp的内容将被复制到一个临时存储单元中
◆ 假设实参的类型与引用参数类型不匹配,但可被转换为引用类型,程序将创建一个正确类型的临时变量,使用转换后的实参值来初始化它,然后传递一个指向该临时变量的引用。
◆ 该函数可以修改s1,因为不同于s2,s1没有被声明为const。
◆ 这个函数能够通过编译(但编译器会发出警告),但当程序试图执行该函数时将崩溃。
◆ 程序试图引用已经释放的内存。
◆ 使得能够将特性从一个类传递给另一个类的语言特性被称为继承,
◆ 派生类继承了基类的方法,这意味着ofstream对象可以使用基类的特性,如格式化方法precision( )和setf( )。
◆ 基类引用可以指向派生类对象
◆ 这种特征的一个实际结果是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。
◆ setf(ios_base::showpoint)将对象置于显示小数点的模式,即使小数部分为零。
◆ 方法setf( )返回调用它之前有效的所有格式化设置
◆ 程序员能够修改调用函数中的数据对象。通过传递引用而不是整个数据对象,可以提高程序的运行速度。
◆ 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
◆ 如果数据对象是结构,则使用引用或指针。如果数据对象是类对象,则使用引用。
◆ 函数调用left(“theory”, 3)将创建新字符串“the”,并返回一个指向该字符串的指针
◆ 但函数调用left(“theory”)不会出错,它认为第二个参数的值为1,并返回指向字符串“t”的指针。
◆ 对于带参数列表的函数,必须从右向左添加默认值。
◆ 要为某个参数设置默认值,则必须为它右边的所有参数提供默认值:
◆ 实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。
◆ 在设计类时您将发现,通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。
◆ 只有原型指定了默认值。函数定义与没有默认参数时完全相同。
◆ 默认参数让您能够使用不同数目的参数调用同一个函数,而函数多态(函数重载)让您能够使用多个同名的函数
◆ 可以通过函数重载来设计一系列函数——它们完成相同的工作,但使用不同的参数列表。
◆ 函数重载的关键是函数的参数列表——也称为函数特征标(function signature)。
◆ 条件是它们的特征标不同
◆ 没有匹配的原型并不会自动停止使用其中的某个函数,因为C++将尝试使用标准类型转换强制进行匹配
◆ 如果#2原型是print( )唯一的原型,则函数调用print(year, 6)将把year转换为double类型。
◆ 有3个将数字作为第一个参数的原型,因此有3种转换year的方式。在这种情况下,C++将拒绝这种函数调用,并将其视为错误。
◆ 为避免这种混乱,编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标
◆ dribble( )函数有两个原型,一个用于const指针,另一个用于常规指针,编译器将根据实参是否为const来决定使用哪个原型
◆ 而drivel( )函数可以与带const或非const参数的调用匹配
◆ 主要是由于将非const值赋给const变量是合法的,但反之则是非法的。
◆ 请记住,是特征标,而不是函数类型使得可以对函数进行重载
◆ 返回类型可以不同,但特征标也必须不同:
◆ 右值
◆ 如果没有定义函数stove(double &&),stove(x+y)将调用函数stove(const double &)。
◆ 下面添加另一个left()函数,它返回整数的前n位
◆ 仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。
◆ C++编译器将执行一些神奇的操作——名称修饰(name decoration)或名称矫正(name mangling)
◆ 它根据函数原型中指定的形参类型对每个函数名进行加密
◆ 函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如int或double)替换
◆ 通过将类型作为参数传递给模板,可使编译器生成该类型的函数
◆ 由于类型是用参数表示的,因此模板特性有时也被称为参数化类型(parameterized types)
◆ 假设要交换两个double值,则一种方法是复制原来的代码,并用double替换所有的int。如果需要交换两个char值,可以再次使用同样的技术。进行这种修改将浪费宝贵的时间,且容易出错
◆ 第一行指出,要建立一个模板,并将类型命名为AnyType。关键字template和typename是必需的,除非可以使用关键字class代替typename。
◆ 模板并不创建任何函数,而只是告诉编译器如何定义函数
◆ 然而,有大量代码库是使用关键字class开发的。在这种上下文中,这两个关键字是等价的。本书使用了这两种形式,旨在让您在其他地方遇到它们时不会感到陌生。
◆ 如果需要多个将同一种算法用于不同类型的函数,请使用模板
◆ 在文件的开始位置提供模板函数的原型,并在main( )后面提供模板函数的定义。
◆ 第一个Swap( )函数接受两个int参数,因此编译器生成该函数的int版本。也就是说,用int替换所有的T,生成下面这样的定义
◆ 程序员看不到这些代码,但编译器确实生成并在程序中使用了它们
◆ 函数模板不能缩短可执行程序。对于程序清单8.11,最终仍将由两个独立的函数定义,就像以手工方式定义了这些函数一样。最终的代码不包含任何模板,而只包含了为程序生成的实际函数
◆ 为满足这种需求,可以像重载常规函数定义那样重载模板定义
◆ 和常规重载一样,被重载的模板的函数特征标必须不同
◆ 原来的模板的特征标为(T &, T &),而新模板的特征标为(T [ ], T [ ],int)。注意,在后一个模板中,最后一个参数的类型为具体类型(int),而不是泛型。并非所有的模板参数都必须是模板参数类型。
◆ 但第二次调用将两个int数组和一个int值用作参数,这与新模板匹配。
◆ 代码假定可执行哪些操作。例如,下面的代码假定定义了赋值,但如果T为数组,这种假设将不成立
◆ 总之,编写的模板函数很可能无法处理某些类型。另一方面,有时候通用化是有意义的,但C++语法不允许这样做。
◆ C++允许您重载运算符+,以便能够将其用于特定的结构或类
◆ 可以提供一个具体化函数定义——称为显式具体化
◆ 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本。
◆ 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
◆ Swap中的是可选的
◆ 编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation
◆ 板并非函数定义,但使用int的模板实例是函数定义。
◆ 显式实例化(explicit instantiation)
◆ 将使用Swap( )模板生成一个使用int类型的实例。也就是说,该声明的意思是“使用Swap( )模板生成int类型的函数定义
◆ 显式具体化使用下面两个等价的声明之一
◆ 式具体化声明在关键字template后包含<>,而显式实例化没有
◆ 使用函数来创建显式实例化
◆ 但通过使用Add(x, m),可强制为double类型实例化,并将参数m强制转换为double类型,以便与函数Add(double, double)的第二个参数匹配。
◆ 对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析(overloading resolution)
◆ 注意,只考虑特征标,而不考虑返回类型。其中的两个候选函数(#4和#7)不可行,因为整数类型不能被隐式地转换(即没有显式强制类型转换)为指针类型。
◆ 什么是完全匹配?如果两个函数(如#3和#5)都完全匹配,将如何办呢?通常,有两个函数完全匹配是一种错误,但这一规则有两个例外。
◆ 正如您预期的,如果有多个匹配的原型,则编译器将无法完成重载解析过程;如果没有最佳的可行函数,则编译器将生成一条错误消息,该消息可能会使用诸如“ambiguous(二义性)”这样的词语。
◆ 如果只定义了函数#3和#4是完全匹配的,则将选择#3,因为ink没有被声明为const。
◆ const和非const之间的区别只适用于指针和引用指向的数据。也就是说,如果只定义了#1和#2,则将出现二义性错
◆ 也就是说,#2模板已经显式指出,函数参数是指向Type的指针,因此可以直接用blot标识Type
◆ Type已经被具体化为指针,因此说它“更具体”。
◆ 如果将模板B从程序中删除,则编译器将使用模板A来显示pd的内容,因此显示的将是地址,而不是值。
◆ 常规函数一样,通过在使用函数前提供模板函数定义,它让它也充当原型。
◆ 编译器应选择模板函数,而不是非模板函数
◆ x和y的值将被强制转换为int,该函数返回一个int值,
◆ 在C++98中,编写模板函数时,一个问题是并非总能知道应在声明中使用哪种类型。
◆ C++11新增的关键字decltype提供了解决方案。可这样使用该关键字
◆ decltype提供的参数可以是表达式,因此在前面的模板函数ft()中
◆ decltype比这些示例演示的要复杂些。为确定类型,编译器必须遍历一个核对表
◆ 如果expression是一个函数调用,则var的类型与函数的返回类型相同
◆ 并不会实际调用函数。编译器通过查看函数的原型来获悉返回类型,而无需实际调用函数。
◆ 如果expression是一个左值,则var为指向其类型的引用
◆ 一种显而易见的情况是,expression是用括号括起的标识符
◆ 请注意,虽然k和n都是引用,但表达式k+n不是引用;它是两个int的和,因此类型为int
◆ auto是一个占位符,表示后置返回类型提供的类型,这是C++11给auto新增的一种角色。这种语法也可用于函数定义
◆ 现在,decltype在参数声明后面,因此x和y位于作用域内,可以使用它们。
◆ 编译器不是让程序跳到独立的代码段,以执行函数,而是用相应的代码替换函数调用。只有在函数很短时才能采用内联方式
◆ 引用变量是一种伪装指针,它允许为变量创建别名(另一个名称)。引用变量主要被用作处理结构和类对象的函数的参数
◆ 只能在参数列表中从右到左提供默认参数。
◆ 则必须为该参数右边所有的参数提供默认值。
◆ 函数的特征标是其参数列表。程序员可以定义两个同名函数,只要其特征标不同。这被称为函数多态或函数重载。通常,通过重载函数来为不同的数据类型提供相同的服务。
◆ 编译器将为程序中使用的特定参数类型生成正确的函数定义
第一十四章 第9章 内存模型和名称空间
◆ C++为在内存中存储数据方面提供了多种选择。可以选择数据保留在内存中的时间长度(存储持续性)以及程序的哪一部分可以访问数据(作用域和链接)等
◆ 和C语言一样,C++也允许甚至鼓励程序员将组件函数放在独立的文件中。
◆ 一个文件(头文件)包含了用户定义类型的定义;另一个文件包含操纵用户定义类型的函数的代码
◆ 请不要将函数定义或变量声明放到头文件中。这样做对于简单的情况可能是可行的,但通常会引来麻烦。
◆ 如果在头文件包含一个函数定义,然后在其他两个文件(属于同一个程序)中包含该头文件,则同一个程序中将包含同一个函数的两个定义,除非函数是内联的,否则这将出错。
◆ 被声明为const的数据和内联函数有特殊的链接属性(稍后将介绍),因此可以将其放在头文件中,而不会引起问题
◆ 如果文件名包含在尖括号中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;但如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录(或其他目录,这取决于编译器)。
◆ 另外,不要使用#include来包含源代码文件,这样做将导致多重声明。
◆ 在同一个文件中只能将同一个头文件包含一次。
◆ 下面的代码片段意味着仅当以前没有使用预处理器编译指令#define定义名称COORDINH 时,才处理#ifndef和#endif之间的语句
◆ 这种方法并不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容。
◆ 两个编译器将为同一个函数生成不同的修饰名称
◆ 在链接编译模块时,请确保所有对象文件或库都是由同一个编译器生成的。如果有源代码,通常可以用自己的编译器重新编译源代码来消除链接错误。
◆ 这些方案的区别就在于数据保留在内存中的时间
◆ 静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态
◆ 如果变量是使用关键字thread_local声明的,则其生命周期与所属的线程一样长
◆ 用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。
◆ 链接性(linkage)描述了名称如何在不同单元间共享
◆ 链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享
◆ 静态变量的作用域是全局还是局部取决于它是如何被定义的
◆ 在名称空间中声明的变量的作用域为整个名称空间(由于名称空间已经引入到C++语言中,因此全局作用域是名称空间作用域的特例)。
◆ 常用的方法是留出一段内存,并将其视为栈,以管理变量的增减。
◆ 关键字register最初是由C语言引入的,它建议编译器使用CPU寄存器来存储自动变量:
◆ 在C++11中,这种提示作用也失去了,关键字register只是显式地指出变量是自动的。
◆ 外部链接性
◆ 内部链接性
◆ 无链接性
◆ 这3种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长
◆ 编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在
◆ 如果没有显式地初始化静态变量,编译器将把它设置为0。在默认情况下,静态数组和结构将每个元素或成员的所有位都设置为0。
◆ 要想创建链接性为外部的静态持续变量,必须在代码块的外面声明它
◆ 要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用static限定符
◆ 要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static限定符。
◆ 与llama不同的是,即使在funct1( )函数没有被执行时,count也留在内存中
◆ 由于global的链接性为外部,因此可以在程序的其他文件中使用它。
◆ 所有的静态持续变量都有下述初始化特征:未被初始化的静态变量的所有位都被设置为0。这种变量被称为零初始化的(zero-initialized)。
◆ 零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译器处理文件(翻译单元)时初始化变量。动态初始化意味着变量将在编译后初始化。
◆ 首先,所有静态变量都被零初始化,而不管程序员是否显式地初始化了它。
◆ 首先,x、y、z和pi被零初始化。然后,编译器计算常量表达式,并将y和z分别初始化为5和169。但要初始化pi,必须调用函数atan(),这需要等到该函数被链接且程序执行时。
◆ 常量表达式并非只能是使用字面常量的算术表达式。例如,它还可使用sizeof运算符
◆ 一种是定义声明(defining declaration)或简称为定义(definition),它给变量分配存储空间;另一种是引用声明(referencing declaration)或简称为声明(declaration),它不给变量分配存储空间,因为它引用已有的变量
◆ 关键字extern并非必不可少的,因为即使省略它,效果也相同
◆ 这种声明将被视为一个自动变量的定义,当程序执行自动变量所属的函数时,该变量将位于作用域内。
◆ main( )和update( )都可以访问外部变量warming。注意,update( )修改了warming,这种修改在随后使用该变量时显现出来了。
◆ 函数update()使用关键字extern重新声明了变量warming,这个关键字的意思是,通过这个名称使用在外部定义的变量。由于即使省略该声明,update( )的功能也相同,因此该声明是可选的。它指出该函数被设计成使用外部变量。
◆ C++比C语言更进了一步——它提供了作用域解析运算符(::)
◆ 放在变量名前面时,该运算符表示使用变量的全局版本。
◆ 全局变量很有吸引力——因为所有的函数能访问全局变量,因此不用传递参数。
◆ 可以让多个函数可以使用同一个数据块(如月份名数组或原子量数组)。外部存储尤其适于表示常量数据,因为这样可以使用关键字const来防止数据被修改。
◆ 第一个const防止字符串被修改,第二个const确保数组中每个指针始终指向它最初指向的字符串
◆ 将static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的
◆ 但常规外部变量都具有外部链接性,即可以在其他文件中使用
◆ 但如果文件定义了一个静态外部变量,其名称与另一个文件中声明的常规外部变量相同,则在该文件中,静态变量将隐藏常规外部变量
◆ 这没有违反单定义规则,因为关键字static指出标识符errors的链接性为内部
◆ 使用该变量的其他文件必须使用关键字extern声明它
◆ 可使用外部变量在多文件程序的不同部分之间共享数据;可使用链接性为内部的静态变量在同一个文件中的多个函数之间共享数据
◆ 如果将作用域为整个文件的变量变为静态的,就不必担心其名称与其他文件中的作用域为整个文件的变量发生冲突。
◆ 无链接性的局部变量。这种变量是这样创建的,将static限定符用于在代码块中定义的变量。在代码块中使用static时,将导致局部变量的存储持续性为静态的。
◆ 但它在该代码块不处于活动状态时仍然存在
◆ 存储说明符
◆ 但在C++11中,auto用于自动类型推断。关键字register用于在声明中指示寄存器存储,而在C++11中,它只是显式地指出变量是自动的
◆ 关键字extern表明是引用声明,即声明引用在其他地方定义的变量
◆ 关键字thread_local指出变量的持续性与其所属线程的持续性相同。thread_local变量之于线程,犹如常规静态变量之于整个程序
◆ 它表明,内存被初始化后,程序便不能再对它进行修改。
◆ 该关键字的作用是为了改善编译器的优化能力
◆ 2024/03/25发表想法
这说明这个 volatile 关键字发挥了它的作用。其实不只是内嵌汇编操纵栈"这种方式属于编译无法识别的变量改变,另外更多的可能是多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。
原文:将变量声明为volatile,相当于告诉编译器,不要进行这种优化。
◆ 将变量声明为volatile,相当于告诉编译器,不要进行这种优化。
◆ 现在回到mutable。可以用它来指出,即使结构(或类)变量为const,其某个成员也可以被修改。
◆ veep的const限定符禁止程序修改veep的成员,但access成员的mutable说明符使得access不受这种限制。
◆ 但const全局变量的链接性为内部的。也就是说,在C++看来,全局const定义(如下述代码段所示)就像使用了static说明符一样
◆ 假设将一组常量放在头文件中,并在同一个程序的多个文件中使用该头文件。那么,预处理器将头文件的内容包含到每个源文件中后,所有的源文件都将包含类似下面这样的定义
◆ 要为某个文件使用一组定义,而其他文件使用另一组声明
◆ 内部链接性还意味着,每个文件都有自己的一组常量,而不是所有文件共享一组常量
◆ 这样,只要在两个源代码文件中包括同一个头文件,则它们将获得同一组常量。
◆ 则可以使用extern关键字来覆盖默认的内部链接性
◆ 定义常规外部变量时,不必使用extern关键字
◆ 和C语言一样,C++不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都自动为静态的,即在整个程序执行期间都一直存在。
◆ 要让程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译,或者是由链接程序搜索的库文件
◆ 使之只能在一个文件中使用。必须同时在原型和函数定义中使用该关键字
◆ 这意味着该函数只在这个文件中可见,还意味着可以在其他文件中定义同名的的函数。和变量一样,在定义静态函数的文件中,静态函数将覆盖外部定义,因此即使在外部定义了同名的函数,该文件仍将使用静态函数。
◆ 但使用该函数的每个文件都应包含其函数原型。
◆ 这意味着如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本,而不是库函数
◆ 链接程序要求每个不同的函数都有不同的符号名
◆ 为满足内部需要,C语言编译器可能将spiff这样的函数名翻译为_spiff。这种方法被称为C语言链接性(C language linkage)
◆ 这种方法被称为C++语言链接(C++ language linkage)
◆ 第4章介绍过,动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制。
◆ 编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,另外一块用于动态存储
◆ 如果希望另一个函数能够使用这80个字节中的内容,则必须将其地址传递或返回给该函数。
◆ 由new分配的内存通常都将被释放,不过情况也并不总是这样
◆ 最佳的做法是,使用delete来释放new分配的内存。
◆ 这种括号语法也可用于有合适构造函数的类
◆ 这些函数被称为分配函数(alloction function),它们位于全局名称空间中。
◆ new运算符还有另一种变体,被称为定位(placement)new运算符,它让您能够指定要使用的位置。
◆ 要使用定位new特性,首先需要包含头文件new,它提供了这种版本的new运算符的原型
◆ 上述代码从buffer1中分配空间给结构chaff,从buffer2中分配空间给一个包含20个元素的int数组。
◆ 定位new运算符使用传递给它的地址,它不跟踪哪些内存单元已被使用,也不查找未使用的内存块。
◆ 而delete只能用于这样的指针:指向常规new运算符分配的堆内存。
◆ 如果buffer是使用常规new运算符创建的,便可以使用常规delete运算符来释放整个内存块
◆ 基本上,它只是返回传递给它的地址,并将其强制转换为void *,以便能够赋给任何指针类型。
◆ 变量、函数、结构、枚举、类以及类和结构的成员
◆ 用户可能希望使用一个库的List类,而使用另一个库的Tree类。这种冲突被称为名称空间问题。
◆ 先复习一下C++中已有的名称空间属性,并介绍一些术语,让读者熟悉名称空间的概念
◆ 声明区域
◆ 可以在函数外面声明全局变量,对于这种变量,其声明区域为其声明所在的文件
◆ 从声明点开始,到其声明区域的结尾
◆ C++关于全局变量和局部变量的规则定义了一种名称空间层次
◆ C++新增了这样一种功能,即通过定义一种新的声明区域来创建命名的名称空间,这样做的目的之一是提供一个声明名称的区域。
◆ 名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的
◆ 全局名称空间(global namespace)
◆ 名称空间是开放的(open),即可以把名称加入到已有的名称空间中。
◆ 未被装饰的名称(如pail)称为未限定的名称(unqualified name);包含名称空间的名称(如Jack::pail)称为限定的名称(qualified name
◆ using声明使特定的标识符可用,using编译指令使整个名称空间可用。
◆ using声明将特定的名称添加到它所属的声明区域中。
◆ 由于using声明将名称添加到局部声明区域中,因此这个示例避免了将另一个局部变量也命名为fetch。
◆ 在函数的外面使用using声明时,将把名称添加到全局名称空间中:
◆ using声明使一个名称可用,而using编译指令使所有的名称都可用。
◆ 它使名称空间中的所有名称都可用,而不需要使用作用域解析运算符:
◆ 如果有名称空间jack和jill,并在代码中使用作用域解析运算符,则不会存在二义性
◆ 编译器不允许您同时使用上述两个using声明,因为这将导致二义性
◆ 使用using编译指令导入一个名称空间中所有的名称与使用多个using声明是不一样的,而更像是大量使用作用域解析运算符
◆ 使用using编译指令时,将进行名称解析,就像在包含using声明和名称空间本身的最小声明区域中声明了名称一样。
◆ 如果使用using编译指令导入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名,就像隐藏同名的全局变量一样。
◆ 一般说来,使用using声明比使用using编译指令更安全,这是由于它只导入指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示
◆ 名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。
◆ 这将名称空间std中的所有内容导出到全局名称空间中。
◆ 由于Jill::fetch现在位于名称空间myth(在这里,它被叫做fetch)中,因此可以这样访问它
◆ using编译指令是可传递的。如果A op B且B op C,则A op C,则说操作op是可传递的
◆ 可以使用这种技术来简化对嵌套名称空间的使用:
◆ 在该名称空间中声明的名称的潜在作用域为:从声明点到该声明区域末尾
◆ debts名称空间也包含一些原型。
◆ 因此定义和声明必须位于同一个名称空间中
◆ 通过包含namesp.h(参见程序清单9.11)导入了原来的名称空间。然后该文件将函数定义添加入到两个名称空间中
◆ 如果函数被重载,则一个using声明将导入所有的版本
◆ another( )函数使用using声明和作用域解析运算符来访问具体的名称
◆ 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量。
◆ 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
◆ C++当前提倡将标准函数库放在名称空间std中,这种做法扩展到了来自C语言中的函数
◆ 对于using声明,首选将其作用域设置为局部而不是全局。
◆ 使用头文件来定义用户类型,为操纵用户类型的函数提供函数原型
◆ 并将函数定义放在一个独立的源代码文件中
◆ 将main( )和其他使用这些函数的函数放在第三个文件中。
◆ 没有使用说明符时,变量将默认为自动的
◆ 使用extern且不初始化
◆ 但使用关键字static限定的函数的链接性为内部的,被限制在定义它的文件中。
◆ 可以通过使用作用域解析运算符、using声明或using编译指令,来使名称空间中的标识符可用。
第10章 对象和类
◆ 面向对象编程(OOP)是一种特殊的、设计程序的概念性方法,C++通过一些特性改进了C语言,使得应用这种方法更容易。
◆ 抽象;封装和数据隐藏;多态;继承;代码的可重用性。
◆ 特殊
◆ 运算符重载(另一种多态)和继承,它们是代码重用的基础。
◆ 首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需的操作。
◆ 将问题的本质特征抽象出来,并根据特征来描述解决方案
◆ 因此,将变量声明为int或float指针时,不仅仅是分配内存,还规定了可对变量执行的操作
◆ 决定数据对象需要的内存数量;
◆ 决定如何解释内存中的位(long和float在内存中占用的位数相同,但将它们转换为数值的方法不同);
◆ 决定可使用数据对象执行的操作或方法。
◆ 对于内置类型来说,有关操作的信息被内置到编译器中。但在C++中定义用户自定义的类型时,必须自己提供这些信息。付出这些劳动换来了根据实际需要定制新数据类型的强大功能和灵活性。
◆ 它将数据表示和操纵数据的方法组合成一个整洁的包
◆ 类声明:以数据成员的方式描述数据部分
◆ 以成员函数(被称为方法)的方式描述公有接口
◆ 简单地说,类声明提供了类的蓝图,而方法定义则提供了细节。
◆ 接口是一个共享框架,供两个系统(如在计算机和打印机之间或者用户或计算机程序之间)交互时使用
◆ 类设计禁止公共用户直接访问类,但公众可以使用方法size( )
◆ C++程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中
◆ 使用类对象的程序都可以直接访问公有部分
◆ 类设计尽可能将公有接口与实现细节分开
◆ 数据隐藏(将数据放在类的私有部分中)是一种封装,将实现的细节隐藏在私有部分中,就像Stock类对set_tot( )所做的那样,也是一种封装。封装的另一个例子是,将类函数定义和类声明放在不同的文件中。
◆ main( )函数只需定义这个结构类型的变量
◆ 使得数据只能被授权的函数访问
◆ 但由于隐藏数据是OOP主要的目标之一,因此数据项通常放在私有部分,组成类接口的成员函数放在公有部分
◆ 不必在类声明中使用关键字private,因为这是类对象的默认访问控制
◆ 作用域解析运算符
◆ 作用域解析运算符确定了方法定义对应的类的身份
◆ 而,在类声明和方法定义之外使用update( )时,需要采取特殊的措施,稍后将作介绍
◆ 它只能在类作用域中使用
◆ 编译器禁止这样做
◆ 其定义位于类声明中的函数都将自动成为内联函数,因此Stock::set_tot( )是一个内联函数。类声明常将短小的成员函数作为内联函数,set_tot( )符合这样的要求。
◆ 如果愿意,也可以在类声明之外定义成员函数,并使其成为内联函数。
◆ 内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义
◆ 在OOP中,调用成员函数被称为发送消息
◆ OOP程序员常依照客户/服务器模型来讨论程序设计
◆ 这意味着客户(客户程序员)唯一的责任是了解该接口
◆ 指定类设计的第一步是提供类声明
◆ 声明有私有部分,在其中声明的成员只能通过成员函数进行访问
◆ 成员函数被放在公有部分中
◆ C++通过类使得实现抽象、数据隐藏和封装等OOP特性很容易。
◆ 单独提供函数定义(除非函数很小
◆ 名称Retort的作用域为整个类,因此在类声明和类方法之外使用该名称时,需要使用作用域解析运算符进行限定。
◆ 应为类提供被称为构造函数和析构函数的标准函数
◆ 不能像上面这样初始化Stock对象的原因在于,数据部分的访问状态是私有的,这意味着程序不能直接访问数据成员。
◆ 因此需要设计合适的成员函数,才能成功地将对象初始化
◆ 但使数据成为公有的违背了类的一个主要初衷:数据隐藏
◆ 一般来说,最好是在创建对象时对它进行初始化
◆ 类构造函数,专门用于构造新对象、将值赋给它们的数据成员
◆ C++为这些成员函数提供了名称和使用语法,而程序员需要提供方法定义。名称与类名相同
◆ 构造函数的原型和函数头有一个有趣的特征——虽然没有返回值,但没有被声明为void类型。实际上,构造函数没有声明类型
◆ (第4个值,total_val成员,是根据shares和share_val计算得到的,因此不必为构造函数提供这个值。)
◆ 这是错误的。构造函数的参数表示的不是类成员,而是赋给类成员的值。因此,参数名不能与类成员相同,否则最终的代码将是这样的
◆ 两种使用构造函数来初始化对象的方式
◆ 每次创建类对象(甚至使用new动态分配内存)时,C++都使用类构造函数。
◆ 对象没有名称,但可以使用指针来管理该对象
◆ 默认构造函数是在未提供显式初始值时,用来创建对象的构造函数
◆ 当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数
◆ 如果提供了非默认构造函数(如Stock(const char *co, int n, double pr)),但没有提供默认构造函数,则下面的声明将出错
◆ 另一种方式是通过函数重载来定义另一个构造函数——一个没有参数的构造函数
◆ 通常应提供对所有类成员做隐式初始化的默认构造函数。
◆ 隐式地调用默认构造函数时,不要使用圆括号
◆ 对象过期时,程序将自动调用一个特殊的成员函数,该函数的名称令人生畏——析构函数
◆ delete来释放这些内存
◆ 析构函数也可以没有返回值和声明类型
◆ 析构函数没有参数
◆ 原型
◆ 因此可以将它编写为不执行任何操作的函数
◆ 如果创建的是静态存储类对象,则其析构函数将在程序结束时自动被调用。如果创建的是自动存储类对象(就像前面的示例中那样),则其析构函数将在程序执行完代码块时(该对象是在其中定义的)自动被调用
◆ 当使用delete来释放内存时,其析构函数将自动被调用
◆ 如果没有这些大括号,代码块将为整个main(),因此仅当main()执行完毕后,才会调用析构函数
◆ 另一种方式是允许调用构造函数来创建一个临时对象,然后将该临时对象复制到stock2中,并丢弃它。如果编译器使用的是这种方式,则将为临时对象调用析构函数,因此生成下面的输出
◆ 这是通过让构造程序创建一个新的、临时的对象
◆ 第一条语句是初始化,它创建有指定值的对象,可能会创建临时对象(也可能不会);第二条语句是赋值。像这样在赋值语句中使用构造函数总会导致在赋值前创建一个临时对象。
◆ 只要提供与某个构造函数的参数列表匹配的内容,并用大括号将它们括起
◆ 因为show( )的代码无法确保调用对象不被修改——调用对象和const一样,不应被修改
◆ 需要一种新的语法——保证函数不会修改调用对象。
◆ 就像应尽可能将const引用和指针用作函数形参一样,只要类方法不修改调用对象,就应将其声明为const。从现在开始,我们将遵守这一规则。
◆ 接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值:[插图]这种特性可能导致问题,但正如第11章将介绍的,可关闭这项特性。
◆ 否则,必须自己提供默认构造函数。
◆ 但有时候方法可能涉及到两个对象,在这种情况下需要使用C++的this指针。
◆ topval( )方法使用一个类型为const Stock &的参数。
◆ 该函数不会修改被隐式地访问的对象
◆ 更为清晰。可以使用运算符重载(参见第11章)完成这项工作
◆ 使用被称为this的特殊指针。this指针指向用来调用成员函数的对象(this被作为隐藏参数传递给方法)
◆ 置为stock2对象的地址。一般来说,所有的类方法都将this指针设置为调用它的对象的地址
◆ 每个成员函数(包括构造函数和析构函数)都有一个this指针。this指针指向调用对象。
◆ *this
◆ 要返回的并不是this,因为this是对象的地址,而是对象本身,即*this
◆ 在这种情况下,将使用不执行任何操作的隐式默认构造函数),要么定义了一个显式默认构造函数(就像这个例子那样)
◆ 如果类包含多个构造函数,则可以对不同的元素使用不同的构造函数
◆ 由于该声明只初始化了数组的部分元素,因此余下的7个元素将使用默认构造函数进行初始化。
◆ 初始化对象数组的方案是,首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。
◆ 定义成员函数时,必须使用作用域解析运算符
◆ 因此创建一个由所有对象共享的常量是个不错的主意
◆ C++11提供了成员初始化,但不适用于前述数组声明
◆ 这将创建一个名为Months的常量,该常量将与其他静态变量存储在一起,而不是存储在对象中。因此,只有一个Months常量,被所有Bakery对象共享
◆ 这将无法通过编译,因为egg Small和t_shirt Small位于相同的作用域内,它们将发生冲突。为避免这种问题,C++11提供了一种新枚举,其枚举量的作用域为类。
◆ 也可使用关键字struct代替class。无论使用哪种方式,都需要使用枚举名来限定枚举量:
◆ 使用OOP方法解决编程问题的第一步是根据它与程序之间的接口来描述数据,从而指定如何使用数据。
◆ 类声明(包括由函数原型表示的方法)应放到头文件中。
◆ 类是用户定义的类型,对象是类的实例
◆ 由于this指针被设置为调用对象的地址,因此*this是该对象的别名。
第一十六章 第11章 使用类
◆ 本章将进一步探讨类的特征,重点是类设计技术,而不是通用原理。
◆ 运算符重载
◆ 然后介绍友元,这种C++机制使得非成员函数可以访问私有数据。
◆ 这被称为函数重载或函数多态,旨在让您能够用同名的函数来完成相同的基本操作
◆ C++允许将运算符重载扩展到用户定义的类型,例如,允许使用+将两个对象相加。编译器将根据操作数的数目和类型决定使用哪种加法定义。重载运算符可使代码看起来更自然。
◆ 这种简单的加法表示法隐藏了内部机理,并强调了实质,这是OOP的另一个目标。
◆ op必须是有效的C++运算符,不能虚构一个新的符号
◆ 现在将其推广,采用一个使用方法来处理加法的Time类。
◆ 但由于sum对象是局部变量,在函数结束时将被删除,因此引用将指向一个不存在的对象
◆ 总之,最后的返回值为t1、t2和t3之和,这正是我们期望的
◆ 重载的运算符(有些例外情况)不必是成员函数,但必须至少有一个操作数是用户定义的类型。
◆ [插图]
◆ C++控制对类对象私有部分的访问。通常,公有类方法提供唯一的访问途径,但是有时候这种限制太严格,以致于不适合特定的编程问题。在这种情况下,C++提供了另外一种形式的访问权限:友元
◆ 通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。
◆ 记住,左侧的操作数是调用对象
◆ ,编译器不能使用成员函数调用来替换该表达式。
◆ 非成员函数不是由对象调用的,它使用的所有值(包括对象)都是显式参数。
◆ 与下面的非成员函数调用匹配
◆ 2024/03/27发表想法
这是定死的
原文:对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算符表达式右边的操作数对应于运算符函数的第二个参数。而原来的成员函数则按相反的顺序处理操作数,也就是说,double值乘以Time值。
◆ 对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算符表达式右边的操作数对应于运算符函数的第二个参数。而原来的成员函数则按相反的顺序处理操作数,也就是说,double值乘以Time值。
◆ 非成员函数不能直接访问类的私有数据,至少常规非成员函数不能访问。然而,有一类特殊的非成员函数可以访问类的私有成员,它们被称为友元函数
◆ 虽然operator *( )函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用;虽然operator *( )函数不是成员函数,但它与成员函数的访问权限相同。
◆ 类的友元函数是非成员函数,其访问权限与成员函数相同。
◆ 通过使用友元函数和类方法,可以用同一个用户接口表达这两种操作。
◆ 只有类声明可以决定哪一个函数是友元,因此类声明仍然控制了哪些函数可以访问私有数据。总之,类方法和友元只是表达类接口的两种不同机制。
◆ 按下面的方式对定义进行修改(交换乘法操作数的顺序),可以将这个友元函数编写为非友元函数
◆ 个版本将Time对象t作为一个整体使用,让成员函数来处理私有值,因此不必是友元。然而,将该版本作为友元也是一个好主意。
◆ 其次,如果以后发现需要函数直接访问私有数据,则只要修改函数定义即可,而不必修改类原型。
◆ 如果要为类重载运算符,并将非类的项作为其第一个操作数,则可以用友元函数来反转操作数的顺序。
◆ 可以对<<运算符进行重载,使之能与cout一起来显示对象的内容。
◆ 之所以可以这样做,是因为<<是可被重载的C++运算符之一。
◆ 这是因为对于每种基本类型,ostream类声明中都包含了相应的重载的operator<<( )定义。
◆ 看看函数代码就会发现,尽管该函数访问了Time对象的各个成员,但从始至终都将ostream对象作为一个整体使用
◆ 但由于它并不直接访问ostream对象的私有成员,所以并不一定必须是ostream类的友元
◆ 但这种实现不允许像通常那样将重新定义的<<运算符与cout一起使用
◆ 正如iosream中定义的那样,<<运算符要求左边是一个ostream对象
◆ 其中包括operator*( )和operator<<( )这两个友元函数。它将第一个友元函数作为内联函数,因为其代码很短。
◆ 只有在类声明中的原型中才能使用friend关键字。除非函数定义也是原型,否则不能在函数定义中使用该关键字
◆ 一般来说,非成员函数应是友元函数,这样它才能直接访问类的私有数据。
◆ 对于友元版本来说,两个操作数都作为参数来传递。
◆ 非成员版本的重载运算符函数所需的形参数目与运算符使用的操作数数目相同;而成员版本所需的参数数目少一个,因为其中的一个操作数是被隐式地传递的调用对象。
◆ 因为这两种格式都与同一个表达式匹配,同时定义这两种格式将被视为二义性错误,导致编译错误
◆ 像下面这样的调用无法通过编译,因为诸如2等整数不能隐式地转换为枚举类型
◆ 这正是重载的乘法运算符要完成的工作
◆ 可以像Time示例那样,使用一个内联友元函数来处理double与Vector相乘
◆ 这意味着将从隐式矢量参数减去以显式参数传递的矢量
◆ operator-( )有两种不同的定义。这是可行的,因为它们的特征标不同。可以定义−运算符的一元和二元版本,因为C++提供了该运算符的一元和二元版本。
◆ 下面介绍类的另一个主题——类型转换。
◆ 将一个标准类型变量的值赋给另一种标准类型的变量时,如果这两种类型兼容,则C++自动将这个值转换为接收变量的类型。
◆ 上述赋值语句都是可行的,因为在C++看来,各种数值类型都表示相同的东西——一个数字,同时C++包含用于进行转换的内置规则。
◆ 上述语句将10强制转换为int指针类型(即int *类型),将指针设置为地址10。这种赋值是否有意义是另一回事
◆ 程序将使用构造函数Stonewt(double)来创建一个临时的Stonewt对象,并将19.6作为初始化值。
◆ 这一过程称为隐式转换,因为它是自动进行的,而不需要显式强制类型转换。
◆ 只有接受一个参数的构造函数才能作为转换函数。下面的构造函数有两个参数,因此不能用来转换类型
◆ 然而,如果给第二个参数提供默认值,它便可用于转换int:
◆ 将构造函数用作自动类型转换函数似乎是一项不错的特性。然而,当程序员拥有更丰富的C++经验时,将发现这种自动特性并非总是合乎需要的,因为这会导致意外的类型转换。
◆ 如果使用关键字explicit限定了这种构造函数,则它只能用于显示转换,否则也可以用于隐式转换。
◆ 将Stonewt对象初始化为double值时。将double值赋给Stonewt对象时。将double值传递给接受Stonewt参数的函数时。返回值被声明为Stonewt的函数试图返回double值时。
◆ 由于没有找到这样的构造函数,因此编译器寻找接受其他内置类型(int可以转换为这种类型)的构造函数
◆ 要进行相反的转换,必须使用特殊的C++运算符函数——转换函数。
◆ 如果定义了从Stonewt到double的转换函数,就可以使用下面的转换
◆ 转换函数必须是类方法;转换函数不能指定返回类型;转换函数不能有参数。
◆ 注意,虽然没有声明返回类型,这两个函数也将返回所需的值。
◆ 在缺少信息时,编译器将指出,程序中使用了二义性转换。该语句没有指出要使用什么类型。
◆ 在C++中,int和double值都可以被赋给long变量,所以编译器使用任意一个转换函数都是合法的。编译器不想承担选择转换函数的责任。然而,如果删除了这两个转换函数之一,编译器将接受这条语句
◆ 原则上说,最好使用显式转换,而避免隐式转换。
◆ 用一个功能相同的非转换函数替换该转换函数即可,但仅在被显式地调用时,该函数才会执行。
◆ typeName是对象将被转换成的类型。将类对象赋给typeName变量或将其强制转换为typeName类型时,该转换函数将自动被调用。
◆ 这一次,每个调用中都有一个参数(kennyD)是double类型的,因此将调用Stonewt(double)构造函数,将该参数转换为Stonewt对象
◆ 在这种情况下,如果定义了operator double( )成员函数,将造成混乱,因为该函数将提供另一种解释方式。编译器不是将kennyD转换为double并执行Stonewt加法,而是将jennySt转换为double并执行double加法。过多的转换函数将导致二义性。
◆ 如果程序经常需要将double值与Stonewt对象相加,则重载加法更合适;如果程序只是偶尔使用这种加法,则依赖于自动转换更简单,但为了更保险,可以使用显式转换。
第12章 类和动态内存分配
◆ 本章将介绍如何对类使用new和delete以及如何处理由于使用动态内存而引起的一些微妙的问题
◆ 通常,最好是在程序运行时(而不是编译时)确定诸如使用多少内存等问题。
◆ 在这种情况下,析构函数将是必不可少的,而不再是可有可无的。
◆ 这个类并没有什么错误,但忽略了一些不明显却必不可少的东西
◆ 这意味着类声明没有为字符串本身分配存储空间,而是在构造函数中使用new来为字符串分配空间
◆ num_strings成员可以记录所创建的对象数目
◆ 不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存
◆ 这是因为静态类成员是单独存储的,而不是对象的组成部分。
◆ 初始化是在方法文件中,而不是在类声明文件中进行的,这是因为类声明位于头文件中,程序可能将头文件包括在其他几个文件中。如果在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误。
◆ 静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来指出静态成员所属的类。但如果静态成员是整型或枚举型const,则可以在类声明中初始化。
◆ 构造函数将len加1
◆ 要理解这种方法,必须知道字符串并不保存在对象中。字符串单独保存在堆内存中,对象仅保存了指出到哪里去查找字符串的信息。
◆ 删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存
◆ 首先,将headline2作为函数参数来传递从而导致析构函数被调用。其次,虽然按值传递可以防止原始参数被修改,但实际上函数已使原始字符串无法识别,导致显示一些非标准字符(显示的文本取决于内存中包含的内容)。
◆ 在为每一个创建的对象自动调用析构函数时,情况更糟糕
◆ 当您使用一个对象来初始化另一个对象时,编译器将自动生成上述构造函数(称为复制构造函数,因为它创建对象的一个副本)。
◆ 这个例子说明的所有问题都是由编译器自动生成的成员函数引起的
◆ 复制构造函数,如果没有定义;赋值运算符,如果没有定义;地址运算符,如果没有定义。
◆ 如,如果您将一个对象赋给另一个对象,编译器将提供赋值运算符的定义。
◆ StringBad类中的问题是由隐式复制构造函数和隐式赋值运算符引起的。
◆ 默认析构函数不执行任何操作,因此这里也不讨论,但需要指出的是,这个类已经提供默认构造函数。
◆ 移动构造函数
◆ 移动赋值运算符
◆ 编译器将提供一个不接受任何参数,也不执行任何操作的构造函数(默认的默认构造函数),这是因为创建对象时总是会调用构造函数
◆ 如果定义了构造函数,C++将不会定义默认构造函数。如果希望在创建对象时不显式地对它进行初始化,则必须显式地定义默认构造函数。
◆ 带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。
◆ 第二个声明既与构造函数#1(没有参数)匹配,也与构造函数#2(使用默认参数0)匹配。
◆ 包括按值传递参数
◆ 而不是常规的赋值过程中
◆ 类的复制构造函数原型通常如下
◆ 新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。
◆ 最后一种声明使用motto初始化一个匿名对象,并将新对象的地址赋给pstring指针
◆ 每当程序生成了对象副本时,编译器都将使用复制构造函数
◆ 编译器生成临时对象时,也将使用复制构造函数。
◆ 当按值传递和返回对象时,都将调用复制构造函数
◆ 与下面的代码等效(只是由于私有成员是无法访问的,因此这些代码不能通过编译
◆ 如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。
◆ 原因可能是程序确实使用默认的复制构造函数另外创建了两个对象。
◆ 解决办法是提供一个对计数进行更新的显式复制构造函数
◆ 原因在于隐式复制构造函数是按值进行复制的
◆ 析构函数StringBad释放str指针指向的内存
◆ sports.str指向的内存已经被sailor的析构函数释放,这将导致不确定的、可能有害的后果。程序清单12.3中的程序生成受损的字符串,这通常是内存管理不善的表现。
◆ 另一个症状是,试图释放内存两次可能导致程序异常终止
◆ 解决类设计中这种问题的方法是进行深度复制(deep copy)。
◆ 复制构造函数应当复制字符串并将副本的地址赋给str成员,而不仅仅是复制字符串地址。
◆ Stringbad的其他问题:赋值运算符
◆ 赋值运算符
◆ 而C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的
◆ 初始化总是会调用复制构造函数,而使用=运算符时也可能调用赋值运算符。
◆ 数据受损。这也是成员复制的问题,即导致headline1.str和knot.str指向相同的地址。因此,当对knot调用析构函数时,将删除字符串“Celery Stalks at Midnight”;当对headline1调用析构函数时,将试图删除前面已经删除的字符串。
◆ 赋值运算符(进行深度复制)定义。
◆ 因为返回值是一个指向StringBad对象的引用,因此参数类型是正确的。
◆ 如果地址不同,函数将释放str指向的内存,这是因为稍后将把一个新字符串的地址赋给str。如果不首先使用delete运算符,则上述字符串将保留在内存中。由于程序中不再包含指向该字符串的指针,因此这些内存被浪费掉。
◆ 赋值操作并不创建新的对象,因此不需要调整静态数据成员num_strings的值。
◆ 上面两种方式分配的内存量相同,区别在于前者与类析构函数兼容,而后者不兼容。
◆ C++11提供了更好的解决方案:引入新关键字nullptr,用于表示空指针。
◆ 在C++中,两个中括号组成一个运算符——中括号运算符,可以使用方法operator 来重载该运算符。
◆ 将返回类型声明为char &,便可以给特定元素赋值。
◆ 但由于operator 是类的一个方法,因此能够修改数组的内容。
◆ 原因是answer是常量,而上述方法无法确保不修改数据(实际上,有时该方法的工作就是修改数据,因此无法确保不修改数据)。
◆ 因此可以提供另一个仅供const String对象使用的operator版本:
◆ 首先,不能通过对象调用静态成员函数;实际上,静态成员函数甚至不能使用this指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。
◆ 由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。
◆ 假设要将常规字符串复制到String对象中。
◆ 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete。new和delete必须相互兼容。new对应于delete,new[ ]对应于delete[ ]。如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或C++11中的nullptr),这是因为delete(无论是带中括号还是不带中括号)可以用于空指针。
◆ 但正如前面指出的,C++11提供了关键字nullptr,这是一种更好的选择。
◆ 具体地说,复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。
◆ 另外,这里使用的new不带中括号,这与另一个构造函数的正确格式不一致。
◆ 默认的逐成员复制和赋值行为有一定的智能。如果您将一个Magazine对象复制或赋值给另一个Magazine对象,逐成员复制将使用成员类型定义的复制构造函数和赋值运算符。
◆ 如果Magazine类因其他成员需要定义复制构造函数和赋值运算符,情况将更复杂;在这种情况下,这些函数必须显式地调用String和string的复制构造函数和赋值运算符
◆ 首先,返回对象将调用复制构造函数,而返回引用不会。
◆ 第二个版本所做的工作更少,效率更高
◆ 如果使用返回类型ostream,将要求调用ostream类的复制构造函数,而ostream没有公有的复制构造函数。
◆ 2024/03/30发表想法
返回引用不会调用复制构造函数
原文:返回一个指向cout的引用不会带来任何问题,因为cout已经在调用函数的作用域内。
◆ 返回一个指向cout的引用不会带来任何问题,因为cout已经在调用函数的作用域内。
◆ 如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回它,因为在被调用函数执行完毕时,局部对象将调用其析构函数。
◆ 在这种情况下,应返回对象而不是引用
◆ 返回值不能是指向在调用函数中已经存在的对象的引用。相反,在Vector::operator+( )中计算得到的两个矢量的和被存储在一个新的临时对象中,该函数也不应返回指向该临时对象的引用,而应该返回实际的Vector对象
◆ 存在调用复制构造函数来创建被返回的对象的开销,然而这是无法避免的。
◆ 而返回语句引发的对复制构造函数的隐式调用创建一个调用程序能够访问的对象。
◆ 其次,这种代码之所以可行,是因为复制构造函数将创建一个临时对象来表示返回值。因此,在前面的代码中,表达式force1 + force2的结果为一个临时对象。在语句1中,该临时对象被赋给net;在语句2和3中,net被赋给该临时对象。
◆ 如果Vector::operator+()的返回类型被声明为const Vector,则语句1仍然合法,但语句2和语句3将是非法的。
◆ 在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须返回一个指向这种对象的引用
◆ 这种特殊的语法意味着使用对象saying [choice]来初始化新的String对象,这将调用复制构造函数,因为复制构造函数(const String &)的参数类型与初始化值(saying [choice])匹配。
◆ 然后,当程序不再需要该对象时,使用delete删除它。对象是单个的,因此,程序使用不带中括号的delete。
◆ 如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。
◆ 如果对象是静态变量(外部、静态、静态外部或来自名称空间),则在程序结束时将调用对象的析构函数。
◆ 如果对象是用new创建的,则仅当您显式使用delete删除对象时,其析构函数才会被调用。
◆ 定位new运算符让您能够在分配内存时能够指定内存位置。
◆ 首先,在创建第二个对象时,定位new运算符使用一个新对象来覆盖用于第一个对象的内存单元。
◆ 2024/03/30发表想法
没必要
原文:然而,将delete[]用于buffer时,不会为使用定位new运算符创建的对象调用析构函数。
◆ 然而,将delete[]用于buffer时,不会为使用定位new运算符创建的对象调用析构函数。
◆ 原因在于delete可与常规new运算符配合使用,但不能与定位new运算符配合使用。
◆ 指针pc3没有收到new运算符返回的地址,因此delete pc3将导致运行阶段错误。
◆ delete pc1也将释放buffer,而不是pc1。
◆ 这种问题的解决方案是,显式地为使用定位new运算符创建的对象调用析构函数。
◆ 对于使用定位new运算符创建的对象,应以与创建顺序相反的顺序进行删除。
◆ 晚创建的对象可能依赖于早创建的对象。另外,仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区。
◆ 该程序使用定位new运算符在相邻的内存单元中创建两个对象,并调用了合适的析构函数。
◆ 使用转换函数时要小心。可以在声明构造函数时使用关键字explicit,以防止它被用于隐式转换。
◆ 如果析构函数通过对指针类成员使用delete来释放内存,则每个构造函数都应当使用new来初始化指针,或将它设置为空指针。
◆ 应定义一个分配内存(而不是将指针指向已有内存)的复制构造函数。这样程序将能够将类对象初始化为另一个类对象。这种构造函数的原型通常如下:
◆ 通过将Node声明放在Queue类中,可以使其作用域为整个类
◆ 在类声明中声明的结构、类或枚举被称为是被嵌套在类中,其作用域为整个类。这种声明不会创建数据对象,而只是指定了可以在类中使用的类型。
◆ 问题在于qsize是常量,所以可以对它进行初始化,但不能给它赋值。
◆ 因此,调用Queue(int qs)构造函数将导致程序首先给4个成员变量分配内存。
◆ 对于const数据成员,必须在执行到构造函数体之前,即创建对象时进行初始化
◆ 它叫做成员初始化列表(member initializer list)
◆ 只有构造函数可以使用这种初始化列表语法。如上所示,对于const类成员,必须使用这种语法。
◆ 正如第14章将介绍的,对于本身就是类对象的成员来说,使用成员初始化列表的效率更高。
◆ 这种格式只能用于构造函数;
◆ 必须用这种格式来初始化非静态const数据成员(至少在C++11之前是这样的);
◆ 必须用这种格式来初始化引用数据成员。
◆ 数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关。
◆ 不能将成员初始化列表语法用于构造函数之外的其他类方法。
◆ 在这种情况下,实际列表将覆盖这些默认初始值
◆ 2024/03/30发表想法
队列生命周期结束时,结点不一定会全部释放掉,因此必须手动写析构函数,释放掉所有的结点
原文:通过删除节点的方式,dequeue( )方法确实可以清除节点,但这并不能保证队列在到期时为空。
◆ 通过删除节点的方式,dequeue( )方法确实可以清除节点,但这并不能保证队列在到期时为空。
◆ 使用new的类通常需要包含显式复制构造函数和执行深度复制的赋值运算符
◆ 更糟的是,只有副本的尾指针得到了更新,从原始对象的角度看,这将损坏链表。
◆ 要克隆或复制队列,必须提供复制构造函数和执行深度复制的赋值构造函数。
◆ 因此,最好还是提供复制构造函数和赋值运算符,尽管目前并不需要它们。
◆ 这就是将所需的方法定义为伪私有方法
◆ 指出这些方法是不可访问的。另外,在定义其对象不允许被复制的类时,这种方法也很有用。
◆ C++11提供了另一种禁用方法的方式——使用关键字delete
◆ 复制构造函数还被用于创建其他的临时对象,但Queue定义中并没有导致创建临时对象的操作,例如重载加法运算符。
◆ 则将一个对象初始化为另一个对象,或将一个对象赋给另一个对象时,也会出现问题。
◆ C++逐个对成员进行初始化和赋值,这意味着被初始化或被赋值的对象的成员将与原始对象完全相同。
◆ 这种语法包括冒号和由逗号分隔的初始化列表,被放在构造函数参数的右括号后,函数体的左括号之前。
第13章 类继承
◆ 面向对象编程的主要目的之一是提供可重用的代码。
◆ 然而,C++提供了比修改代码更好的方法来扩展和修改类。这种方法叫作类继承,它能够从已有的类派生出新的类,而派生类继承了原有类(称为基类)的特征,包括方法。
◆ 从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。
◆ 这将首先为firstname调用string的默认构造函数,再调用string的赋值运算符将firstname设置为fn,但初始化列表语法可减少一个步骤,它直接使用string的复制构造函数将firstname初始化为fn。
◆ 但构造函数的形参类型被声明为const string &。这导致类型不匹配
◆ string类有一个将const char *作为参数的构造函数,使用C-风格字符串初始化string对象时,将自动调用这个构造函数
◆ 将前者作为参数时,将调用接受const string &作为参数的string构造函数,而将后者作为参数时,将调用接受const char *作为参数的string构造函数。
◆ 冒号指出RatedPlayer类的基类是TableTennisplayer类。上述特殊的声明头表明TableTennisPlayer是一个公有基类,这被称为公有派生。
◆ 基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问(稍后将介绍保护成员)。
◆ 派生类对象存储了基类的数据成员(派生类继承了基类的实现);派生类对象可以使用基类的方法(派生类继承了基类的接口)。
◆ 派生类需要自己的构造函数。派生类可以根据需要添加额外的数据成员和成员函数。
◆ 在第一个RatedPlayer构造函数中,每个成员对应一个形参;而第二个Ratedplayer构造函数使用一个TableTennisPlayer参数
◆ 具体地说,派生类构造函数必须使用基类构造函数。
◆ 创建派生类对象时,程序首先创建基类对象
◆ C++使用成员初始化列表语法来完成这种工作
◆ 其中:TableTennisPlayer(fn,ln,ht)是成员初始化列表
◆ 必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数
◆ 否则应显式调用正确的基类构造函数。
◆ 因此将调用基类的复制构造函数。基类没有定义复制构造函数,但第12章介绍过,如果需要使用复制构造函数但又没有定义,编译器将自动生成一个
◆ 首先创建基类对象;派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;派生类构造函数应初始化派生类新增的数据成员。
◆ 即首先执行派生类的析构函数,然后自动调用基类的析构函数。
◆ 派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。
◆ 初始化器列表机制将值传递给基类构造函数
◆ 其中derived是派生类,base是基类,x和y是基类构造函数使用的变量。
◆ 成员初始化列表只能用于构造函数。
◆ 派生类与基类之间有一些特殊关系。其中之一是派生类对象可以使用基类的方法,条件是方法不是私有的
◆ 另外两个重要的关系是:基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象
◆ 2024/04/02发表想法
做了限制
原文:然而,基类指针或引用只能用于调用基类方法,因此,不能使用rt或pt来调用派生类的ResetRanking方法。
◆ 然而,基类指针或引用只能用于调用基类方法,因此,不能使用rt或pt来调用派生类的ResetRanking方法。
◆ C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是例外。然而,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针:
◆ 派生类引用能够为基对象调用派生类方法,这样做将出现问题
◆ 形参rt是一个基类引用,它可以指向基类对象或派生类对象,所以可以在Show( )中使用TableTennis参数或Ratedplayer参数:
◆ 类定义中没有这样的构造函数,但存在隐式复制构造函数
◆ 形参是基类引用,因此它可以引用派生类。这样,将olaf2初始化为olaf1时,将要使用该构造函数,它复制firstname、lastname和hasTable成员。
◆ 同样,也可以将派生对象赋给基类对象:
◆ 在这种情况下,程序将使用隐式重载赋值运算符
◆ C++有3种继承方式:公有继承、保护继承和私有继承。
◆ 继承可以在基类的基础上添加属性,但不能删除基类的属性。在有些情况下,可以设计一个包含共有特征的类,然后以is-a或has-a关系,在这个类的基础上定义相关的类。
◆ 因此,还是坚持使用is-a关系吧。
◆ 这种较复杂的行为称为多态——具有多种形态,即同一个方法的行为随上下文而异。
◆ Brass类和BrassPlus类都声明了ViewAcct( )和Withdraw( )方法,但BrassPlus对象和Brass对象的这些方法的行为是不同的;
◆ 这些方法被称为虚方法(virtual method);
◆ 程序将使用对象类型来确定使用哪个版本
◆ 对于在两个类中行为相同的方法(如Deposit( )和Balance( )),则只在基类中声明。
◆ 如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。
◆ 这里两个引用的类型都是Brass,但b2_ref引用的是一个BrassPlus对象,所以使用的是BrassPlus::ViewAcct( )。
◆ 经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法。
◆ 第四点是,基类声明了一个虚析构函数。这样做是为了确保释放派生对象时,按正确的顺序调用析构函数。
◆ 派生类并不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据。
◆ 派生类构造函数在初始化基类私有数据时,采用的是成员初始化列表语法
◆ 这几个构造函数都使用成员初始化列表语法,将基类信息传递给基类构造函数,然后使用构造函数体初始化BrassPlus类新增的数据项。
◆ 在派生类方法中,标准技术是使用作用域解析运算符来调用基类方法。
◆ 如果代码没有使用作用域解析运算符,编译器将认为ViewAcct( )是BrassPlus::ViewAcct( ),这将创建一个不会终止的递归函数——这可不好。
◆ 因为派生类没有重新定义该方法,代码不必对Balance( )使用作用域解析运算符。
◆ 如果数组成员指向的是Brass对象,则调用Brass::ViewAcct( );如果指向的是BrassPlus对象,则调用BrassPlus::ViewAcct( )。如果Brass::ViewAcct( )被声明为虚的,则在任何情况下都将调用Brass::ViewAcct( )。
◆ 因此,使用虚析构函数可以确保正确的析构函数序列被调用。
◆ 将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)
◆ 然而,C/C++编译器可以在编译过程完成这种联编。在编译过程中进行联编被称为静态联编(static binding
◆ 所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding)
◆ 正如您看到的,指向基类的引用或指针可以引用派生类对象,而不必进行显式类型转换
◆ 将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),这使公有继承不需要进行显式类型转换
◆ 也是如此。向上强制转换是可传递的
◆ 相反的过程——将基类指针或引用转换为派生类指针或引用——称为向下强制转换(downcasting)。如果不使用显式类型转换,则向下强制转换是不允许的。
◆ 隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++使用虚成员函数来满足这种需求。
◆ 指针类型在编译时已知,因此编译器在编译时,可以将ViewAcct( )关联到Brass::ViewAcct( )。总之,编译器对非虚方法使用静态联编。
◆ 在这个例子中,对象类型为BrassPlus,但通常(如程序清单13.10所示)只有在运行程序时才能确定对象的类型。
◆ 总之,编译器对虚方法使用动态联编。
◆ 由于静态联编的效率更高,因此被设置为C++的默认选择。
◆ C++的指导原则之一是,不要为不使用的特性付出代价(内存或者处理时间)。
◆ 不将该函数设置为虚函数,有两方面的好处:首先效率更高;其次,指出不要重新定义该函数。这表明,仅将那些预期将被重新定义的方法声明为虚的。
◆ 如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法。
◆ 每个对象都将增大,增大量为存储地址的空间;对于每个类,编译器都创建一个虚函数地址表(数组);对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址
◆ 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。
◆ 构造函数不能是虚函数。
◆ 如果使用默认的静态联编,delete语句将调用Employee( )析构函数。这将释放由Singer对象中的Employee部分指向的内存,但不会释放新的类成员指向的内存。但如果析构函数是虚的,则上述代码将先调用Singer析构函数释放由Singer组件指向的内存,然后,调用~Employee( )析构函数来释放由Employee组件指向的内存。
◆ 这意味着,即使基类不需要显式析构函数提供服务,也不应依赖于默认构造函数,而应提供虚析构函数,即使它不执行任何操作
◆ 通常应给基类提供一个虚析构函数,即使它并不需要析构函数。
◆ 重新定义不会生成函数的两个重载版本,而是隐藏了接受一个int参数的基类版本。总之,重新定义继承的方法并不是重载
◆ 第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变(covariance of return type)
◆ 因为允许返回类型随类类型的变化而变化:
◆ 注意,这种例外只适用于返回值,而不适用于参数。
◆ 第二,如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。
◆ 如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们
◆ 关键字protected与private相似,在类外只能用公有类成员来访问protected部分中的类成员
◆ 派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。
◆ Brass类被设计成只能通过Deposit( )和Withdraw( )才能修改balance。但对于BrassPlus对象,Reset( )方法将忽略Withdraw( )中的保护措施,实际上使balance成为公有变量
◆ 最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据。
◆ 然而,对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能使用的内部函数。
◆ 接下来更为复杂的是抽象基类(abstract base class,ABC)
◆ Circle和Ellipse类有很多共同点,将它们分别定义则忽略了这一事实。
◆ 便可以使用基类指针数组同时管理Circle和Ellipse对象,即可以使用多态方法)
◆ 这里的理念是,包含纯虚函数的类只用作基类
◆ 要成为真正的ABC,必须至少包含一个纯虚函数
◆ 也许所有的基类方法都与Move( )一样,可以在基类中进行定义,但您仍需要将这个类声明为抽象的。在这种情况下,可以将原型声明
◆ 在原型中使用=0指出类是一个抽象基类,在类中可以不定义该函数。
◆ 由于Circle和Ellipse对象的基类相同,因此可以用BaseEllipse指针数组同时管理这两种对象
◆ ABC描述的是至少使用一个纯虚函数的接口,从ABC派生出的类将根据派生类的具体特征,使用常规虚函数来实现这种接口。
◆ 至少应有一个虚函数是纯虚函数,这样才能使AcctABC成为抽象类。
◆ AcctABC类还有两个纯虚函数,所以它确实是抽象类。
◆ 保护方法FullName( )和AcctNum( )提供了对数据成员fullName和acctNum的只读访问,使得可以进一步定制每个派生类的ViewAcct( )。
◆ 在处理继承的问题上,RatedPlayer示例使用的方法比较随意,而ABC方法比它更具系统性、更规范。
◆ 可以将ABC看作是一种必须实施的接口。
◆ 析构函数、复制构造函数和重载赋值运算符。
◆ 实际上,派生类的默认构造函数总是要进行一些操作:执行自身的代码后调用基类析构函数。
◆ lacksDMA类的默认复制构造函数使用显式baseDMA复制构造函数来复制lacksDMA对象的baseDMA部分。
◆ 类的默认赋值运算符将自动使用基类的赋值运算符来对基类组件进行赋值。因此,默认赋值运算符也是合适的。
◆ 必须为派生类定义显式析构函数、复制构造函数和赋值运算符
◆ 派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的进行清理。
◆ hasDMA析构函数必须释放指针style管理的内存,并依赖于baseDMA的析构函数来释放指针label管理的内存。
◆ BaseDMA的复制构造函数遵循用于char数组的常规模式,即使用strlen( )来获悉存储C-风格字符串所需的空间、分配足够的内存(字符数加上存储空字符所需的1字节)并使用函数strcpy( )将原始字符串复制到目的地
◆ 因此它必须调用baseDMA复制构造函数来处理共享的baseDMA数据
◆ 需要注意的一点是,成员初始化列表将一个hasDMA引用传递给baseDMA构造函数
◆ 因为复制构造函数baseDMA有一个baseDMA引用参数,而基类引用可以指向派生类型
◆ baseDMA复制构造函数将使用hasDMA参数的baseDMA部分来构造新对象的baseDMA部分。
◆ 然而,派生类的显式赋值运算符必须负责所有继承的baseDMA基类对象的赋值,可以通过显式调用基类赋值运算符来完成这项工作
◆ 所以使用后面的代码时,编译器将使用hasDMA ::operator=( ),从而形成递归调用。
◆ 当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数、赋值运算符都必须使用相应的基类方法来处理基类元素。
◆ 对于析构函数,这是自动完成的
◆ 对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成的
◆ 如果不这样做,将自动调用基类的默认构造函数
◆ 对于赋值运算符,这是通过使用作用域解析运算符显式地调用基类的赋值运算符来完成的。
◆ 作为hasDMA类的友元,该函数能够访问style成员。
◆ 答案是使用baseDMA类的友元函数operator<<( )。
◆ 这个问题的解决方法是使用强制类型转换,以便匹配原型时能够选择正确的函数。
◆ 默认构造函数要么没有参数,要么所有的参数都有默认值。
◆ 自动生成的默认构造函数的另一项功能是,调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数。
◆ 如果定义了某种构造函数,编译器将不会定义默认构造函数。在这种情况下,如果需要默认构造函数,则必须自己提供。
◆ 将新对象初始化为一个同类对象;按值将对象传递给函数;函数按值返回对象;编译器生成临时对象。
◆ 如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;
◆ 如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。
◆ 默认的赋值运算符用于处理同类对象之间的赋值。不要将赋值与初始化混淆了。如果语句创建新的对象,则使用初始化;如果语句修改已有对象的值,则是赋值
◆ 编译器不会生成将一种类型赋给另一种类型的赋值运算符。
◆ 构造函数不同于其他类方法,因为它创建新的对象,而其他类方法只是被现有的对象调用。这是构造函数不被继承的原因之一。
◆ 对于基类,即使它不需要析构函数,也应提供一个虚析构函数。
◆ 使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换。
◆ 将可转换的类型传递给以类为参数的函数时,将调用转换构造函数。
◆ 要将类对象转换为其他类型,应定义转换函数
◆ C++11支持将关键字explicit用于转换函数。与构造函数一样,explicit允许使用强制类型转换进行显式转换,但不允许隐式转换。
◆ 通常,编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。
◆ 按引用传递对象的另外一个原因是,在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。
◆ 有些类方法返回对象。您可能注意到了,有些成员函数直接返回对象,而另一些则返回引用。有时方法必须返回对象,但如果可以不返回对象,则应返回引用。
◆ 其次,应返回引用而不是返回对象的的原因在于,返回对象涉及生成返回对象的临时副本,这是调用函数的程序可以使用的副本。
◆ 返回对象的时间成本包括调用复制构造函数来生成副本所需的时间和调用析构函数删除副本所需的时间
◆ 直接返回对象与按值传递对象相似:它们都生成临时副本。
◆ 在这种情况下,应返回对象,以生成一个调用程序可以使用的副本。
◆ 使用const时应特别注意。可以用它来确保方法不修改参数
◆ 可以使用const来确保方法不修改调用它的对象
◆ 该方法返回对this或s的引用。因为this和s都被声明为const,所以函数不能对它们进行修改,这意味着返回的引用也必须被声明为const。
◆ 注意,如果函数将参数声明为指向const的引用或指针,则不能将该参数传递给另一个函数,除非后者也确保了参数不会被修改。
◆ 最好的方法可能是创建包含纯虚函数的抽象数据类,并从它派生出其他的类。
◆ 构造函数是不能继承的,也就是说,创建派生类对象时,必须调用派生类的构造函数。
◆ 派生类构造函数通常使用成员初始化列表语法来调用基类构造函数,以创建派生对象的基类部分。
◆ 如果基类有默认析构函数,编译器将为派生类生成默认析构函数。通常,对于基类,其析构函数应设置为虚的。
◆ 赋值运算符是不能继承的,原因很简单。派生类继承的方法的特征标与基类完全相同,但赋值运算符的特征标随类而异,这是因为它包含一个类型为其所属类的形参。
◆ 如果编译器发现程序将一个对象赋给同一个类的另一个对象,它将自动为这个类提供一个赋值运算符。
◆ 编译器将使用基类赋值运算符来处理派生对象中基类部分的赋值。
◆ 如果类构造函数使用new来初始化指针,则需要提供一个显式赋值运算符。因为对于派生对象的基类部分,C++将使用基类的赋值运算符
◆ 例如,baseDMA类显式地定义了赋值,但派生类lackDMA使用为它生成的隐式赋值运算符。
◆ 如果派生类使用了new,则必须提供显式赋值运算符。
◆ 将派生类对象赋给基类对象将会如何呢?(注意,这不同于将基类引用初始化为派生类对象。)
◆ 其中左边的对象是Brass对象,因此它将调用Brass ::operator =(const Brass &)函数。is-a关系允许Brass引用指向派生类对象,如Snips。赋值运算符只处理基类成员,所以上述赋值操作将忽略Snips的maxLoan成员和其他BrassPlus成员。总之,可以将派生对象赋给基类对象,但这只涉及基类的成员。
◆ 然而,派生类引用不能自动引用基类对象,因此上述代码不能运行
◆ 转换构造函数可以接受一个类型为基类的参数和其他参数,条件是其他参数有默认值:
◆ 如果有转换构造函数,程序将通过它根据gp来创建一个临时BrassPlus对象,然后将它用作赋值运算符的参数
◆ 另一种方法是,定义一个用于将基类赋给派生类的赋值运算符:
◆ 总之,问题“是否可以将基类对象赋给派生对象?”的答案是“也许”。如果派生类包含了这样的构造函数,即对将基类对象转换为派生类对象进行了定义,则可以将基类对象赋给派生对象。如果派生类定义了用于将基类对象赋给派生对象的赋值运算符,则也可以这样做
◆ 对派生类而言,保护成员类似于公有成员;但对于外部而言,保护成员与私有成员类似。
◆ 使用私用数据成员比使用保护数据成员更好,但保护方法很有用。
◆ 如果希望派生类能够重新定义方法,则应在基类中将方法定义为虚的,这样可以启用晚期联编(动态联编
◆ 如果不希望重新定义方法,则不必将其声明为虚的,这样虽然无法禁止他人重新定义方法,但表达了这样的意思:您不希望它被重新定义。
◆ 当通过指向对象的基类指针或引用来删除派生对象时,程序将首先调用派生类的析构函数,然后调用基类的析构函数,而不仅仅是调用基类的析构函数。
◆ 您可能希望派生类的友元函数能够使用基类的友元函数。
◆ 可以通过强制类型转换将,派生类引用或指针转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数
◆ 派生类对象自动使用继承而来的基类方法,如果派生类没有重新定义该方法。派生类的构造函数自动调用基类的构造函数。派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数。派生类构造函数显式地调用成员初始化列表中指定的基类构造函数。派生类方法可以使用作用域解析运算符来调用公有的和受保护的基类方法。派生类的有元函数可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后使用该引用或指针来调用基类的友元函数。
◆ 作为is-a模型的一部分,派生类继承基类的数据成员和大部分方法,但不继承基类的构造函数、析构函数和赋值运算符。
◆ 如果希望派生类可以重新定义基类的方法,则可以使用关键字virtual将它声明为虚的
◆ 不一定非得定义纯虚方法。对于包含纯虚成员的类,不能使用它来创建对象。纯虚方法用于定义派生类的通用接口。
第14章 C++中的代码重用
◆ C++的一个主要目标是促进代码重用。
◆ 本身是另一个类的对象。这种方法称为包含(containment)、组合(composition)或层次化(layering)
◆ 另一种方法是使用私有或保护继承。通常,包含、私有继承和保护继承用于实现has-a关系,即新的类将包含另一个类的对象
◆ 多重继承使得能够使用两个或更多的基类派生出新的类,将基类的功能组合在一起。
◆ 第10章介绍了函数模板,本章将介绍类模板——另一种重用代码的方法。类模板使我们能够使用通用术语定义类,然后使用模板来创建针对特定类型定义的特殊类。
◆ 例如,可以使用一个String类(参见第12章)或标准C++ string类的对象来表示姓名。
◆ valarray类是由头文件valarray支持的
◆ 使用valarray类来声明一个对象时,需要在标识符valarray后面加上一对尖括号,并在其中包含所需的数据类型
◆ 第4章介绍vector和array类时,您见过这种语法,它非常简单。这些类也可用于存储数字,但它们提供的算术支持没有valarray多。
◆ 在C++11中,也可使用初始化列表
◆ operator :让您能够访问各个元素。size( ):返回包含的元素数。sum( ):返回所有元素的总和。max( ):返回最大的元素。min( ):返回最小的元素。
◆ 对于这种情况,通常被描述为Student类获得了其成员对象的实现,但没有继承接口
◆ 使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。
◆ 不继承接口是has-a关系的组成部分。
◆ 将该typedef放在类定义的私有部分意味着可以在Student类的实现中使用它,但在Student类外面不能使用。
◆ 可以用一个参数调用的构造函数将用作从参数类型到类类型的隐式转换函数;但这通常不是好主意
◆ C++包含让程序员能够限制程序结构的特性——使用explicit防止单参数构造函数的隐式转换,使用const限制方法修改数据,等等。这样做的根本原因是:在编译阶段出现错误优于在运行阶段出现错误。
◆ 在前面的一些例子中,构造函数用这种语法来初始化内置类型的成员
◆ 对于继承的对象,构造函数在成员初始化列表中使用类名来调用特定的基类构造函数。对于成员对象,构造函数则使用成员名。
◆ 初始化列表中的每一项都调用与之匹配的构造函数,即name(str)调用构造函数string(const char *),scores(pd, n)调用构造函数ArrayDb(const double *, int)
◆ C++要求在构建对象的其他部分之前,先构建继承对象的所有成员对象。
◆ 则name成员仍将首先被初始化,因为在类定义中它首先被声明。对于这个例子来说,初始化顺序并不重要,但如果代码使用一个成员的值作为另一个成员的初始化表达式的一部分时,初始化顺序就非常重要了。
◆ 被包含对象的接口不是公有的,但可以在类方法中使用它。
◆ 该方法内部使用了valarray的方法size( )和sum( )。
◆ operator<<(ostream & os, const Student & stu)必须是Student类的友元函数,这样才能访问name成员
◆ C++还有另一种实现has-a关系的途径——私有继承
◆ 使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。
◆ 但可以在派生类的成员函数中使用它们。
◆ 总之,派生类将继承基类的接口;这是is-a关系的一部分
◆ 使用私有继承,基类的公有方法将成为派生类的私有方法。总之,派生类不继承基类的接口。正如从被包含对象中看到的,这种不完全继承是has-a关系的一部分。
◆ 包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。我们将使用术语子对象(subobject)来表示通过继承或包含添加的对象。
◆ 因此私有继承提供的特性与包含相同:获得实现,但不获得接口
◆ 要进行私有继承,请使用关键字private而不是public来定义类(实际上,private是默认值,因此省略访问限定符也将导致私有继承)
◆ 使用多个基类的继承被称为多重继承(multiple inheritance,MI)。通常,MI尤其是公有MI将导致一些问题,必须使用额外的语法规则来解决它们
◆ 对于继承类,新版本的构造函数将使用成员初始化列表语法,它使用类名而不是成员名来标识构造函数:
◆ 在这里,ArrayDb是std::valarray的别名。成员初始化列表使用std::string(str),而不是name(str)。这是包含和私有继承之间的第二个主要区别。
◆ 然而,私有继承使得能够使用类名和作用域解析运算符来调用基类的方法
◆ 使用包含时将使用对象名来调用方法,而使用私有继承时将使用类名和作用域解析运算符来调用方法。
◆ 由于Student类是从string类派生而来的,因此可以通过强制类型转换,将Student对象转换为string对象;结果为继承而来的string对象。
◆ 由于既可以使用包含,也可以使用私有继承来建立has-a关系,那么应使用种方式呢?大多数C++程序员倾向于使用包含
◆ 其次,继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题,如包含同名方法的独立的基类或共享祖先的独立基类。
◆ 2024/04/04发表想法
所以通过私有继承来实现 has-a 的关系,本身就是反人类的。它不利于人类按照常规思维来实现 has-a 的关系。
原文:而继承则只能使用一个这样的对象(当对象都没有名称时,将难以区分)。
◆ 而继承则只能使用一个这样的对象(当对象都没有名称时,将难以区分)。
◆ 通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
◆ 使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。
◆ 使用using重新定义访问权限
◆ 使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。
◆ 这样Student对象便能够调用Student::sum( ),后者进而将valarray::sum( )方法应用于被包含的valarray对象(
◆ 另一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使采用的是私有派生。
◆ 注意,using声明只使用成员名——没有圆括号、函数特征标和返回类型。
◆ MI描述的是有多个直接基类的类。与单继承一样,公有MI表示的也是is-a关系
◆ 请注意,必须使用关键字public来限定每一个基类。这是因为,除非特别指出,否则编译器将认为是私有派生
◆ 由于这个原因,很多C++用户强烈反对使用MI,一些人甚至希望删除MI;而喜欢MI的人则认为,对一些特殊的工程来说,MI很有用,甚至是必不可少的;也有一些人建议谨慎、适度地使用MI。
◆ 因为Singer和Waiter都继承了一个Worker组件,因此SingingWaiter将包含两个Worker组件
◆ 通常,这种赋值将把基类指针设置为派生对象中的基类对象的地址。但ed中包含两个Worker对象,有两个地址可供选择,所以应使用类型转换来指定对象
◆ 这将使得使用基类指针来引用不同的对象(多态性)复杂化。
◆ C++引入多重继承的同时,引入了一种新技术——虚基类(virtual base class),使MI成为可能。
◆ 虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如,通过在类声明中使用关键字virtual,可以使Worker被用作Singer和Waiter的虚基类(virtual和public的次序无关紧要)
◆ 现在,SingingWaiter对象将只包含Worker对象的一个副本。从本质上说,继承的Singer和Waiter对象共享一个Worker对象,而不是各自引入自己的Worker对象副本
◆ 因为SingingWaiter现在只包含了一个Worker子对象,所以可以使用多态。
◆ 2024/04/07发表想法
丧心病狂,连关键字都重载
原文:有点像关键字重载。
◆ 有点像关键字重载。
◆ 使用虚基类时,需要对类构造函数采用一种新的方法。
◆ 如果Worker是虚基类,则这种信息自动传递将不起作用
◆ 为避免这种冲突,C++在基类是虚的时,禁止信息通过中间类自动传递给基类。
◆ 上述构造函数将初始化成员panache和voice,但wk参数中的信息将不会传递给子对象Waiter
◆ 在上述情况下,编译器将使用Worker的默认构造函数。
◆ 上述代码将显式地调用构造函数worker(const Worker &)。请注意,这种用法是合法的,对于虚基类,必须这样做;但对于非虚基类,则是非法的。警告:
◆ 如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
◆ 在多重继承中,每个直接祖先都有一个Show( )函数,这使得上述调用是二义性的。
◆ 多重继承可能导致函数调用的二义性。例如,BadDude类可能从Gunslinger类和PokerPlayer类那里继承两个完全不同的Draw( )方法。
◆ 然而,这种递增的方式对SingingWaiter示例无效。下面的方法将无效,因为它忽略了Waiter组件
◆ 可以通过同时调用Waiter版本的Show( )来补救:
◆ 然而,这将显示姓名和ID两次,因为Singer::Show( )和Waiter::Show( )都调用了Worker::Show( )。
◆ 与此相似,其他Show( )方法可以组合适当的Data( )组件。
◆ 对象仍可使用Show( )方法。而Data( )方法只在类内部可用,作为协助公有接口的辅助方法
◆ 如果Data( )方法是保护的,则只能在继承层次结构中的类中使用它,在其他地方则不能使用。
◆ 另一种办法是将所有的数据组件都设置为保护的,而不是私有的,不过使用保护方法(而不是保护数据)将可以更严格地控制对数据的访问。
◆ 在祖先相同时,使用MI必须引入虚基类,并修改构造函数初始化列表的规则。另外,如果在编写这些类时没有考虑到MI,则还可能需要重新编写它们。
◆ 如果基类是虚基类,派生类将包含基类的一个子对象
◆ 当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。
◆ 使用非虚基类时,规则很简单。如果类从不同的类那里继承了两个或更多的同名成员(数据或方法),则使用该成员名时,如果没有用类名进行限定,将导致二义性。但如果使用的是虚基类,则这样做不一定会导致二义性
◆ 在这种情况下,如果某个名称优先于(dominates)其他所有名称,则使用它时,即便不使用限定符,也不会导致二义性。
◆ 派生类中的名称优先于直接或间接祖先类中的相同名称
◆ 类C中的q( )定义优先于类B中的q( )定义,因为类C是从类B派生而来的。
◆ 任何一个omg( )定义都不优先于其他omg( )定义,因为C和E都不是对方的基类。
◆ 首先复习一下不使用虚基类的MI。这种形式的MI不会引入新的规则
◆ 如果一个类通过多种途径继承了一个非虚基类,则该类从每种途径分别继承非虚基类的一个实例。在某些情况下,这可能正是所希望的,但通常情况下,多个基类实例都是问题
◆ 从虚基类的一个或多个实例派生而来的类将只继承了一个基类对象。为实现这种特性,必须满足其他要求:
◆ 有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的;
◆ 这种复杂性主要是由于派生类通过多条途径继承同一个基类引起的。避免这种情况后,唯一需要注意的是,在必要时对继承的名称进行限定。
◆ 继承(公有、私有或保护)和包含并不总是能够满足重用代码的需要。
◆ 容器类设计用来存储其他对象或数据类型。
◆ 然而,与其编写新的类声明,不如编写一个泛型(即独立于类型的)栈,然后将具体的类型作为参数传递给这个类。
◆ 例如,将类型名int传递给Queue模板,可以让编译器构造一个对int进行排队的Queue类。
◆ 将使用模板定义替换Stack声明
◆ 使用模板成员函数替换Stack的成员函数
◆ 关键字template告诉编译器,将要定义一个模板。尖括号中的内容相当于函数的参数列表。可以把关键字class看作是变量的类型名,该变量接受类型作为其值,把Type看作是该变量的名称
◆ 较新的C++实现允许在这种情况下使用不太容易混淆的关键字typename代替class
◆ 每个函数头都将以相同的模板声明打头
◆ 如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符
◆ 程序清单14.13列出了类模板和成员函数模板。
◆ 模板的具体实现——如用来处理string对象的栈类——被称为实例化(instantiation)或具体化(specialization)。
◆ 能将模板成员函数放在独立的实现文件中
◆ 由于模板不是函数,它们不能单独编译。模板必须与特定的模板实例化请求一起使用。
◆ 最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
◆ 仅在程序包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名
◆ 下面的代码创建两个栈,一个用于存储int,另一个用于存储string对象
◆ 编译器将按Stack模板来生成两个独立的类声明和两组独立的类方法。
◆ 当然,使用的算法必须与类型一致。
◆ 但对于数组则不成立。
◆ 泛型标识符——例如这里的Type——称为类型参数(type parameter),这意味着它们类似于变量,但赋给它们的不能是数字,而只能是类型
◆ 必须显式地提供所需的类型,这与常规的函数模板是不同的,因为编译器可以根据函数的参数类型来确定要生成哪种函数
◆ 毕竟,这种指针是处理C-风格字符串的内置方式。答案是可以创建指针栈,但如果不对程序做重大修改,将无法很好地工作。
◆ 但每次执行压入操作时,加入到栈中的的地址都相同
◆ 具体地说,栈并没有保存每一个新字符串,因此没有任何用途。
◆ 使用指针栈的方法之一是,让调用程序提供一个指针数组,其中每个指针都指向不同的字符串。
◆ 原型将赋值运算符函数的返回类型声明为Stack引用,而实际的模板函数定义将类型定义为Stack。
◆ 即可以在模板声明或模板函数定义内使用Stack,但在类的外面,即指定返回类型或使用作用域解析运算符时,必须使用完整的Stack。
◆ 把字符串压入栈实际上是新建一个指向该字符串的指针,即创建一个指针,该指针的值是现有字符串的地址
◆ 栈的析构函数对字符串有何影响呢?没有。构造函数使用new创建一个用于保存指针的数组,析构函数删除该数组,而不是数组元素指向的字符串。
◆ 模板常用作容器类,这是因为类型参数的概念非常适合于将相同的存储方案用于不同的类型
◆ 确实,为容器类提供可重用代码是引入模板的主要动机
◆ 一种方法是在类中使用动态数组和构造函数参数来提供元素数目,最后一个版本的Stack模板采用的就是这种方法。
◆ 使用模板参数来提供常规数组的大小
◆ 关键字class(或在这种上下文中等价的关键字typename)指出T为类型参数,int指出n的类型为int。
◆ 这将导致编译器定义名为ArrayTP<double, 12>的类,并创建一个类型为ArrayTP<double, 12>的eggweight对象。定义类时,编译器将使用double替换T,使用12替换n。
◆ 模板代码不能修改参数的值,也不能使用参数的地址。
◆ 外,实例化模板时,用作表达式参数的值必须是常量表达式。
◆ 构造函数方法使用的是通过new和delete管理的堆内存,而表达式参数方法使用的是为自动变量维护的内存栈。这样,执行速度将更快,尤其是在使用了很多小型数组时。
◆ 2024/04/09发表想法
这可能会生成大量的类文件
原文:表达式参数方法的主要缺点是,每种数组大小都将生成自己的模板。
◆ 表达式参数方法的主要缺点是,每种数组大小都将生成自己的模板。
◆ 但下面的声明只生成一个类声明,并将数组大小信息传递给类的构造函数
◆ 模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数
◆ 例如,可以使用数组模板实现栈模板,也可以使用数组模板来构造数组——数组元素是基于栈模板的栈。
◆ 另一个模板多功能性的例子是,可以递归使用模板。例如,对于前面的数组模板定义,可以这样使用它
◆ 模板可以包含多个类型参数。例如,假设希望类可以保存两种值,则可以创建并使用Pair模板来保存两个不同的值(标准模板库提供了类似的模板,名为pair)。
◆ 类模板的另一项新特性是,可以为类型参数提供默认值
◆ 第16章将讨论的标准模板库经常使用该特性,将默认类型设置为类。
◆ 类模板与函数模板很相似,因为可以有隐式实例化、显式实例化和显式具体化,它们统称为具体化(specialization)。模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明
◆ 编译器在需要对象之前,不会生成类的隐式实例化:
◆ 第二条语句导致编译器生成类定义,并根据该定义创建一个对象。
◆ 当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化(explicit instantiation)。
◆ 面的声明将ArrayTP<string,100>声明
◆ 在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。和隐式实例化一样,也将根据通用模板来生成具体化。
◆ 如果T表示一种类,则只要定义了T::operator>( )方法,这也管用
◆ 现在,当请求const char *类型的SortedArray模板时,编译器将使用上述专用的定义,而不是通用的模板定义
◆ 关键字template后面的<>声明的是没有被具体化的类型参数
◆ 则<>内将为空,这将导致显式具体化
◆ 如果有多个模板可供选择,编译器将使用具体化程度最高的模板
◆ 如果没有进行部分具体化,则第二个声明将使用通用模板,将T转换为char *类型。如果进行了部分具体化,则第二个声明将使用具体化模板,将T转换为char
◆ 模板可用作结构、类或模板类的成员
◆ 该模板类将另一个模板类和模板函数作为其成员
◆ hold模板是在私有部分声明的,因此只能在beta类中访问它。beta类使用hold模板声明了两个数据成员
◆ 而q成员是基于T类型(beta模板参数)的hold对象。在main( )中,下述声明使得T表示的是double,因此q的类型为hold
◆ 模板可以包含类型参数(如typename T)和非类型参数(如int n)。模板还可以包含本身就是模板的参数,这种参数是模板新增的特性,用于实现STL
◆ 模板参数是template class Thing,其中template class是类型,Thing是参数
◆ 为使上述声明被接受,模板参数King必须是一个模板类,其声明与模板参数Thing的声明匹配
◆ 总之,模板参数Thing将被替换为声明Crab对象时被用作模板参数的模板类型。
◆ Crab类的声明对Thing代表的模板类做了另外3个假设,即这个类包含一个push( )方法,包含一个pop( )方法,且这些方法有特定的接口。
◆ 可以混合使用模板参数和常规参数,例如,Crab类的声明可以像下面这样打头
◆ 现在,成员s1和s2可存储的数据类型为泛型,而不是用硬编码指定的类型。这要求将程序中nebula的声明修改成下面这样
◆ 模板参数T表示一种模板类型,而类型参数U和V表示非模板类型。
◆ 上述声明使counts( )函数成为模板所有实例化的友元。例如,它将是类hasFriend和HasFriend的友元。
◆ 答案是不可以。原因是不存在HasFriend这样的对象,而只有特定的具体化,如HasFriend。
◆ 注意,report( )本身并不是模板函数,而只是使用一个模板作参数。这意味着必须为要使用的友元定义显式具体化
◆ HasFriend模板有一个静态成员ct。这意味着这个类的每一个特定的具体化都将有自己的静态成员
◆ 可以修改前一个示例,使友元函数本身成为模板。
◆ 然后,在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化
◆ 因此必须使用模板参数语法()来指明其具体化
◆ 程序必须满足的第三个要求是,为友元提供模板定义。
◆ 正如您看到的,counts和counts报告的模板大小不同,这表明每种T类型都有自己的友元函数count( )。
◆ C++11新增了一项功能——使用模板提供一系列别名,如下所示
◆ C++11允许将语法using =用于非模板。用于非模板时,这种语法与常规typedef等价:
◆ C++提供了几种重用代码的手段。
◆ 无论使用哪种继承,基类的公有接口都将成为派生类的内部接口。
◆ 但并不继承接口,因为派生类对象不能显式地使用基类的接口。
◆ 可以使用类限定符来解决名称二义性的问题,使用虚基类来避免继承多个基类对象的问题。
◆ T是类型参数,用作以后将指定的实际类型的占位符(这个参数可以是任意有效的C++名称,但通常使用T和Type)。
◆ 2024/04/10发表想法
动态的,只有在声明、生成对象时,才会具体化。
原文:类定义(实例化)在声明类对象并指定特定类型时生成
◆ 类定义(实例化)在声明类对象并指定特定类型时生成
◆ 使用关键字template声明类的特定具体化时,将发生显式实例化
◆ 2024/04/10发表想法
这是显式实例化
原文:在这种情况下,编译器将使用通用模板生成一个int具体化——Ic,虽然尚未请求这个类的对象。
◆ 在这种情况下,编译器将使用通用模板生成一个int具体化——Ic,虽然尚未请求这个类的对象。
◆ 可以提供显式具体化——覆盖模板定义的具体类声明。方法是以template<>打头,然后是模板类名称,再加上尖括号(其中包含要具体化的类型)
◆ 面这样的声明将为chic使用专用定义,而不是通用模板
◆ 类模板可以指定多个泛型,也可以有非类型参数
◆ 模板类可用作其他类、结构和模板的成员。
◆ 2024/04/10发表想法
这里的复杂度太高,不一定有利于编程
原文:所有这些机制的目的都是为了让程序员能够重用经过测试的代码,而不用手工复制它们。这样可以简化编程工作,提供程序的可靠性。
◆ 2024/04/10发表想法
但是这太过复杂
原文:所有这些机制的目的都是为了让程序员能够重用经过测试的代码,而不用手工复制它们。这样可以简化编程工作,提供程序的可靠性。
◆ 所有这些机制的目的都是为了让程序员能够重用经过测试的代码,而不用手工复制它们。这样可以简化编程工作,提供程序的可靠性。
第15章 友元、异常和其他
◆ C++异常处理提供了处理特殊情况的机制,如果不对其进行处理
◆ RTTI是一种确定对象类型的机制
◆ 友元类的所有方法都可以访问原始类的私有成员和保护成员
◆ 也可以做更严格的限制,只将特定的成员函数指定为另一个类的友元
◆ 相反,它们提高了公有接口的灵活性。
◆ 很明显,这两个类之间应当存在某种关系
◆ 友元声明可以位于公有、私有或保护部分,其所在的位置无关紧要。
◆ 除构造函数外,所有的Romote方法都将一个Tv对象引用作为参数,这表明遥控器必须针对特定的电视机。
◆ 这个练习的主要目的在于表明,类友元是一种自然用语,用于表示一些关系。如果不使用某些形式的友元关系,则必须将Tv类的私有部分设置为公有的,或者创建一个笨拙的、大型类来包含电视机和遥控器。这种解决方法无法反应这样的事实,即同一个遥控器可用于多台电视机。
◆ 唯一直接访问Tv成员的Remote方法是Remote::set_chan( ),因此它是唯一需要作为友元的方法
◆ 然而,要使编译器能够处理这条语句,它必须知道Remote的定义。否则,它无法知道Remote是一个类,而set_chan是这个类的方法。
◆ 避开这种循环依赖的方法是,使用前向声明(forward declaration)
◆ 还有一个麻烦。程序清单15.1的Remote声明包含了内联代码
◆ 但正如看到的,该声明位于Remote声明的后面
◆ 使Remote声明中只包含方法声明,并将实际的定义放在Tv类之后。
◆ 检查该原型时,所有的编译器都需要知道Tv是一个类,而前向声明提供了这样的信息
◆ 通过在方法定义中使用inline关键字,仍然可以使其成为内联方法
◆ 内联函数的链接性是内部的
◆ 也可以将定义放在实现文件中,但必须删除关键字inline,这样函数的链接性将是外部的
◆ 让整个Remote类成为友元并不需要前向声明,因为友元语句本身已经指出Remote是一个类
◆ 新的方案将受益于相互的友情,一些Remote方法能够像前面那样影响Tv对象,而一些Tv方法也能影响Remote对象。
◆ 其原型可在Remote类声明之前声明,但必须在Remote类声明之后定义,以便编译器有足够的信息来编译该方法
◆ 它可以是一个类的成员,同时是另一个类的友元,但有时将函数作为两个类的友元更合理。
◆ 前向声明使编译器看到Probe类声明中的友元声明时,知道Analyzer是一种类型。
◆ 在另一个类中声明的类被称为嵌套类(nested class),它通过提供新的类型类作用域来避免名称混乱。
◆ 而仅当声明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符
◆ 对类进行嵌套通常是为了帮助实现另一个类,并避免名称冲突。
◆ 由于结构是一种其成员在默认情况下为公有的类,所以Node实际上是一个嵌套类,但该定义并没有充分利用类的功能。
◆ 上述代码创建Node后,显式地给Node成员赋值,这种工作更适合由构造函数来完成。
◆ 并将next指针设置为0,这是使用C++编写空值指针的方法之一
◆ 个例子在类声明中定义了构造函数。假设想在方法文件中定义构造函数,则定义必须指出Node类是在Queue类中定义的。
◆ 嵌套类的公有部分、保护部分和私有部分控制了对类成员的访问。在哪些地方可以使用嵌套类以及如何使用嵌套类,取决于作用域和访问控制
◆ 类的默认访问权限是私有的
◆ 但是程序的其他部分甚至不知道存在Node类
◆ 如果嵌套类是在另一个类的公有部分声明的,则允许后者、后者的派生类以及外部世界使用它,因为它是公有的。然而,由于嵌套类的作用域为包含它的类,因此在外部世界使用它时,必须使用类限定符。
◆ 要在Team类的外面创建Coach对象,可以这样做
◆ 很多程序员都使用公有枚举来提供可供客户程序员使用的类常数。
◆ 类声明的位置决定了类的作用域或可见性。类可见后,访问控制规则(公有、保护、私有、友元)将决定程序对嵌套类成员的访问权限
◆ Node是利用通用类型Item来定义的
◆ 这两个Node类将在两个独立的QueueTP类中定义,因此不会发生名称冲突。
◆ C++异常为处理这种情况提供了一种功能强大而灵活的工具
◆ 于被零除的情况,很多新式编译器通过生成一个表示无穷大的特殊浮点值来处理,cout将这种值显示为Inf、inf、INF或类似的东西;
◆ 如果其中一个参数是另一个参数的负值,则调用abort( )函数
◆ 它还返回一个随实现而异的值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程),处理失败。
◆ 如果愿意,也可以使用exit( ),该函数刷新文件缓冲区,但不显示消息。
◆ 在hmean( )中调用abort( )函数将直接终止程序,而不是先返回到main( )。
◆ ostream类的get(void)成员通常返回下一个输入字符的ASCII码,但到达文件尾时,将返回特殊值EOF
◆ 可使用指针参数或引用参数来将值返回给调用程序,并使用函数的返回值来指出成功还是失败。
◆ 另外还给该函数增加了第三个参数,用于提供答案
◆ 第三参数可以是指针或引用。对内置类型的参数,很多程序员都倾向于使用指针,因为这样可以明显看出是哪个参数用于提供答案。
◆ 传统的C语言数学库使用的就是这种方法,它使用的全局变量名为errno。当然,必须确保其他函数没有将该全局变量用于其他目的。
◆ 对异常的处理有3个组成部分:引发异常;使用处理程序捕获异常;使用try块。
◆ throw语句实际上是跳转,即命令程序跳到另一条语句
◆ throw关键字表示引发异常,紧随其后的值(例如字符串或对象)指出了异常的特征。
◆ 然后是一个用花括号括起的代码块,指出要采取的措施。catch关键字和异常类型用作标签,指出当异常被引发时,程序应跳到这个位置执行。异常处理程序也被称为catch块。
◆ try块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个catch块。
◆ 表明需要注意这些代码引发的异常。
◆ 如果其中的某条语句导致异常被引发,则后面的catch块将对异常进行处理。如果程序在try块的外面调用hmean( ),将无法处理异常。
◆ 异常类型可以是字符串(就像这个例子中那样)或其他C++类型;通常为类类型,本章后面的示例将说明这一点。
◆ 在程序清单15.9中,该函数是调用函数。稍后将有一个沿函数调用序列后退多步的例子。另外,在这个例子中,throw将程序控制权返回给main( )。程序将在main( )中寻找与引发的异常类型匹配的异常处理程序(位于try块的后面)。
◆ 关键字catch表明这是一个处理程序,而char*s则表明该处理程序与字符串异常匹配。s与函数参数定义极其类似,因为匹配的引发将被赋给s。另外,当异常与该处理程序匹配时,程序将执行括号中的代码。
◆ 则程序跳过try块后面的catch块,直接执行处理程序后面的第一条语句。
◆ 程序向后搜索时发现,hmean( )函数是从main( )中的try块中调用的,因此程序查找与异常类型匹配的catch块。
◆ 命令程序跳过while循环的剩余部分,跳到起始位置。continue使程序跳到循环的起始处,这表明处理程序语句是循环的一部分,而catch行是指引程序流程的标签(参见图15.2)。
◆ 而没有try块或没有匹配的处理程序时,将会发生什么情况。在默认情况下下,程序最终将调用abort( )函数
◆ 可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常。另外,对象可以携带信息,程序员可以根据这些信息来确定引发异常的原因。同时,catch块可以根据这些信息来决定采取什么样的措施。
◆ 调用构造函数bad_hmean( ),以初始化对象,使其存储参数值
◆ 如果函数hmean( )引发bad_hmean异常,第一个catch块将捕获该异常;如果gmean( )引发bad_gmean异常,异常将逃过第一个catch块,被第二个catch块捕获。
◆ 有时候,一种理念看似有前途,但实际的使用效果并不好
◆ 最好不要使用这项功能。而C++11也建议您忽略异常规范。
◆ 假设try块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序流程将从引发异常的函数跳到包含try块和处理程序的函数。
◆ 另外,函数调用将函数参数放到栈中。在栈中,这些函数参数被视为自动变量
◆ 依此类推,同时每个函数都在结束时释放其自动变量。如果自动变量是类对象,则类的析构函数(如果有的话)将被调用。
◆ 随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句
◆ 如果没有栈解退这种特性,则引发异常后,对于中间函数调用放在栈中的自动类对象,其析构函数将不会被调用。
◆ 一般而言,重新引发的异常将由下一个捕获这种异常的try-catch块组合进行处理,如果没有找到这样的处理程序,默认情况下程序将异常终止。
◆ 假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将与任何派生类对象匹配。
◆ 假设有一个异常类层次结构,并要分别处理不同的异常类型,则使用基类引用将能够捕获任何异常对象;而使用派生类对象只能捕获它所属类及从这个类派生而来的类的对象。引发的异常对象将被第一个与之匹配的catch块捕获。这意味着catch块的排列顺序应该与派生顺序相反:
◆ 如果有一个异常类继承层次结构,应这样排列catch块:将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面。
◆ 在这种情况下,仍能够捕获异常,即使不知道异常的类型。方法是使用省略号来表示异常类型,从而捕获任何异常
◆ 可以将上述捕获所有异常的catch块放在最后面,这有点类似于switch语句中的default
◆ 可以创建捕获对象而不是引用的处理程序。在catch语句中使用基类对象时,将捕获所有的派生类对象,但派生特性将被剥去,因此将使用虚方法的基类版本
◆ 总之,异常是这样一种特性:类似于类,可以改变您的编程方式。
◆ 例如,为支持该语言,exception头文件(以前为exception.h或except.h)定义了exception类,C++可以把它用作其他异常类的基类。
◆ 有一个名为what( )的虚拟成员函数,它返回一个字符串,该字符串的特征随实现而异。
◆ 有一个名为what( )的虚拟成员函数,它返回一个字符串,该字符串的特征随实现而异
◆ 否则,可以分别捕获它们。C++库定义了很多基于exception的异常类型。
◆ 该文件定义了logic_error和runtime_error类,它们都是以公有方式从exception派生而来的
◆ 每个类独有一个类似于logic_error的构造函数,让您能够提供一个供方法what( )返回的字符串。
◆ 很多代码都是在new在失败时返回空指针时编写的。
◆ 异常、类和继承以三种方式相互关联。
◆ 如果它是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型与这个类及其派生类的对象匹配),否则称为意外异常(unexpected exception)
◆ 如果异常不是在函数中引发的(或者函数没有异常规范),则必须捕获它。如果没被捕获(在没有try块或没有匹配的catch块时,将出现这种情况),则异常被称为未捕获异常(uncaught exception)。在默认情况下,这将导致程序异常终止。
◆ 知道应捕获哪些异常很有帮助,因为默认情况下,未捕获的异常将导致程序异常终止。
◆ 如果发生意外异常,程序将调用unexpected( )函数(您没有想到是unexpected( )函数吧?谁也想不到!)。
◆ string类采用动态内存分配。通常,当函数结束时,将为mesg调用string的析构函数。虽然throw语句过早地终止了函数,但它仍然使得析构函数被调用,这要归功于栈解退。
◆ 解退栈时,将删除栈中的变量ar。但函数过早的终止意味着函数末尾的delete[ ]语句被忽略。指针消失了,但它指向的内存块未被释放,并且不可访问。总之,这些内存被泄漏了。
◆ RTTI是运行阶段类型识别(Runtime Type Identification)的简称。
◆ 如何知道指针指向的是哪种对象呢?
◆ 在这种情况下,只要该函数是类层次结构中所有成员都拥有的虚函数,则并不真正需要知道对象的类型。
◆ 只能将RTTI用于包含虚函数的类层次结构,原因在于只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针。
◆ RTTI只适用于包含虚函数的类。
◆ 类型转换#2就是不安全的,因为它将基数对象(Grand)的地址赋给派生类(Magnificent)指针。
◆ 虚函数确保了将这3种指针中的任何一种指向Magnificent对象时,都将调用Magnificent方法。
◆ 问题“类型转换是否安全”更通用,也更有用。
◆ 首先,它定义了3个类,名称为Grand、Superb和Magnificent。Grand类定义了一个虚函数Speak( ),而其他类都重新定义了该虚函数。
◆ 循环将该指针赋给Grand *变量pg,然后使用pg调用Speak( )函数。因为这个函数是虚拟的,所以代码能够正确地调用指向的对象的Speak( )版本。
◆ 然而,可以使用dynamic_cast运算符来检查是否可将pg的类型安全地转换为Superb指针。如果对象的类型为Superb或Magnificent,则可以安全转换。
◆ 顺便说一句,有些编译器可能会对无目的赋值(在if条件语句中,通常使用= =运算符)提出警告。
◆ 即使编译器支持RTTI,在默认情况下,它也可能关闭该特性。如果该特性被关闭,程序可能仍能够通过编译,但将出现运行阶段错误。
◆ 即应尽可能使用虚函数,而只在必要时使用RTTI
◆ 正如您看到的,只为Superb和Magnificent类调用了Say( )方法(每次运行时输出都可能不同,因为该程序使用rand( )来选择对象类型)。
◆ 没有与空指针对应的引用值,因此无法使用特殊的引用值来指示失败。
◆ dynamic_cast将引发类型为bad_cast的异常,这种异常是从exception类派生而来的,它是在头文件typeinfo中定义的。
◆ 类名;结果为对象的表达式。
◆ type_info类重载了= =和!=运算符,以便可以使用这些运算符来对类型进行比较。
◆ 但包含一个name( )成员,该函数返回一个随实现而异的字符串:通常(但并非一定)是类的名称。
◆ C++界有很多人对RTTI口诛笔伐,他们认为RTTI是多余的,是导致程序效率低下和糟糕编程方式的罪魁祸首。
◆ 使用typeid来显示地测试每个类型时,必须修改for循环的代码,添加一个else if,但无需修改原来的版本。
◆ 如果发现在扩展的if else语句系列中使用了typeid,则应考虑是否应该使用虚函数和dynamic_cast。
◆ dynamic_cast;const_cast;static_cast;reinterpret_cast。
◆ 可以根据目的选择一个适合的运算符,而不是使用通用的类型转换。这指出了进行类型转换的原因,并让编译器能够检查程序的行为是否与设计者想法吻合。
◆ 该运算符的用途是,使得能够在类层次结构中进行向上转换(由于is-a关系,这样的类型转换是安全的),而不允许其他转换。
◆ const_cast运算符用于执行只有一种用途的类型转换,即改变值为const或volatile
◆ 除了const或volatile特征(有或无)可以不同外,type_name和expression的类型必须相同。
◆ 它删除const标签
◆ const_cast不是万能的。它可以修改指向一个值的指针,但修改const值的结果是不确定的。
◆ const_cast运算符可以删除const int* pt中的const,使得编译器能够接受change( )中的语句
◆ 因此可用来修改指向的值,但仅当指向的值不是const时才可行。
◆ 同理,由于无需进行类型转换,枚举值就可以被转换为整型,所以可以用static_cast将整型转换为枚举值。同样,可以使用static_cast将double转换为int、将float转换为long以及其他各种数值转换。
◆ reinterprete_cast运算符并不支持所有的类型转换。例如,可以将指针类型转换为足以存储指针表示的整型,但不能将指针转换为更小的整型或浮点型。另一个限制是,不能将函数指针转换为数据指针,反之亦然。
◆ 可能需要使用前向声明,需要特别注意类和方法声明的顺序,以正确地组合友元。
◆ C++异常机制为处理拙劣的编程事件,如不适当的值、I/O失败等,提供了一种灵活的方式。
◆ 但C++11摒弃了这项功能。未被捕获的异常(没有匹配的catch块的异常)在默认情况下将终止程序,意外异常(不与任何异常规范匹配的异常)也是如此。
◆ Typeid运算符返回一个type_info对象。可以对两个typeid的返回值进行比较,以确定对象是否为特定的类型,而返回的type_info对象可用于获得关于对象的信息。
第16章 string类和标准模板库
◆ 接下来介绍标准模板库(STL),它是一组用于处理各种容器对象的模板
◆ STL演示了一种编程模式——泛型编程;
◆ 头文件string.h和cstring支持对C-风格字符串进行操纵的C库字符串函数,但不支持string类
◆ 其中包括了若干构造函数,用于将字符串赋给变量、合并字符串、比较字符串和访问各个元素的重载运算符以及用于在字符串中查找字符和子字符串的工具等
◆ 使用了string的7个构造函数
◆ 然后列出了C++11新增的两个构造函数
◆ string类将string::npos定义为字符串的最大长度,通常为unsigned int的最大值
◆ 2024/04/19发表想法
C like 字符串和 string 类的字符串完全不是一个东西
原文:表格中使用缩写NBTS(null-terminated string)来表示以空字符结束的字符串——传统的C字符串
◆ 表格中使用缩写NBTS(null-terminated string)来表示以空字符结束的字符串——传统的C字符串
◆ 程序清单16.1中程序还使用了重载+=运算符,它将一个字符串附加到另一个字符串的后面;重载的=运算符用于将一个字符串赋给另一个字符串;重载的<<运算符用于显示string对象;重载的[ ]运算符用于访问字符串中的各个字符。
◆ 同样,=运算符也被重载,以便可以将string对象、C-风格字符串或char值赋给string对象
◆ 重载[ ]运算符(就像第12章的String示例那样)使得可以使用数组表示法来访问string对象中的各个字符:
◆ 原因在于,对象名(不同于数组名)不会被看作是对象的地址,因此five不是指针,所以five + 6是没有意义的。
◆ 构造函数string(string && str)类似于复制构造函数,导致新创建的string为str的副本。但与复制构造函数不同的是,它不保证将str视为const。
◆ 构造函数string(initializer_list il)让您能够将列表初始化语法用于string类
◆ 两个版本的getline( )都有一个可选参数,用于指定使用哪个字符来确定输入的边界
◆ 在设计方面的一个区别是,读取C-风格字符串的函数是istream类的方法,而string版本是独立的函数。这就是对于C-风格字符串输入,cin是调用对象;而对于string对象输入,cin是一个函数参数的原因。
◆ string版本的operator>>( )函数的行为与此类似,只是它不断读取,直到遇到空白字符并将其留在输入队列中
◆ 将:指定为分界字符后,换行符将被视为常规字符。
◆ 可以比较字符串。String类对全部6个关系运算符都进行了重载。
◆ size( )和length( )成员函数都返回字符串中的字符数
◆ length( )成员来自较早版本的string类,而size( )则是为提供STL兼容性而添加的
◆ nd( )方法的4个版本。
◆ 每当程序将一个字母附加到字符串末尾时将发生什么呢?不能仅仅将已有的字符串加大,因为相邻的内存可能被占用了。因此,可能需要分配一个新的内存块,并将原来的内容复制到新的内存单元中。如果执行大量这样的操作,效率将非常低,因此很多C++实现分配一个比实际字符串大的内存块,为字符串提供了增大空间。
◆ 超过了内存块的大小,程序将分配一个大小为原来两倍的新内存块,以提供足够的增大空间,避免不断地分配新的内存块
◆ open( )方法要求使用一个C-风格字符串作为参数;幸运的是,c_str( )方法返回一个指向C-风格字符串的指针,该C-风格字符串的内容与用于调用c_str( )方法的string对象相同。因此可以这样做:[插图]
◆ 本节将string类看作是基于char类型的。事实上,正如前面指出的,string库实际上是基于一个模板类的
◆ 当出现异常时,delete将不被执行,因此也将导致内存泄漏。
◆ 当remodel( )这样的函数终止(不管是正常终止,还是由于出现了异常而终止),本地变量都将从栈内存中删除——因此指针ps占据的内存将被释放。
◆ 因此,ps的问题在于,它只是一个常规指针,不是有析构函数的类对象。如果它是对象,则可以在对象过期时,让它的析构函数删除指向的内存。
◆ 这三个智能指针模板(auto_ptr、unique_ptr和shared_ptr)都定义了类似指针的对象,可以将new获得(直接或间接)的地址赋给这种对象。
◆ 因此,如果将new返回的地址赋给这些对象,将无需记住稍后释放这些内存:在智能指针过期时,这些内存将自动被释放。
◆ 本书前面说过,throw( )意味着构造函数不会引发异常;与auto_ptr一样,throw()也被摒弃。因此,请求X类型的auto_ptr将获得一个指向X类型的auto_ptr
◆ new double是new返回的指针,指向新分配的内存块。它是构造函数auto_ptr的参数,即对应于原型中形参p的实参。同样,new string也是构造函数的实参。
◆ 注意到智能指针模板位于名称空间std中
◆ 每个智能指针都放在一个代码块内,这样离开代码块时,指针将过期。
◆ 所有智能指针类都一个explicit构造函数,该构造函数将指针作为参数。因此不需要自动将指针转换为智能指针对象
◆ 由于智能指针模板类的定义方式,智能指针对象的很多方面都类似于常规指针。
◆ 如果ps是一个智能指针对象,则可以对它执行解除引用操作(* ps
◆ pvac过期时,程序将把delete运算符用于非堆内存,这是错误的。
◆ 如果ps和vocation是常规指针,则两个指针将指向同一个string对象。这是不能接受的,因为程序将试图删除同一个对象两次——一次是ps过期时,另一次是vocation过期时。要避免这种问题,方法有多种。
◆ 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数(reference counting)。
◆ 仅当最后一个指针过期时,才调用delete。这是shared_ptr采用的策略。
◆ 消息core dumped表明,错误地使用auto_ptr可能导致问题(这种代码的行为是不确定的,其行为可能随系统而异)。这里的问题在于,下面的语句将所有权从films[2]转让给pwin:
◆ 却发现这是一个空指针,这显然讨厌的意外。
◆ 这次pwin和films[2]指向同一个对象,而引用计数从1增加到2。在程序末尾,后声明的pwin首先调用其析构函数,该析构函数将引用计数降低到1。然后,shared_ptr数组的成员被释放,对filmsp[2]调用析构函数时,将引用计数降低到0,并释放以前分配的空间。
◆ 与auto_ptr一样,unique_ptr也采用所有权模型。但使用unique_ptr时,程序不会等到运行阶段崩溃,而在编译器因下述代码行出现错误
◆ 但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。
◆ 编译器认为语句#6非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全(编译阶段错误比潜在的程序崩溃更安全)。
◆ 这没有问题,因为ps拥有了string对象的所有权。但这里的另一个好处是,demo( )返回的临时unique_ptr很快被销毁,没有机会使用它来访问无效的数据。换句话说,没有理由禁止这种赋值。神奇的是,编译器确实允许这种赋值!
◆ 总之,程序试图将一个unique_ptr赋给另一个时,如果源unique_ptr是个临时右值,编译器允许这样做;如果源unique_ptr将存在一段时间,编译器将禁止这样做:
◆ 语句#2不会留下悬挂的unique_ptr,因为它调用unique_ptr的构造函数,该构造函数创建的临时对象在其所有权转让给pu后就会被销毁。
◆ 答案是它使用了C++11新增的移动构造函数和右值引用,这将在第18章讨论。
◆ 可将unique_ptr存储到STL容器中,只要不调用将一个unique_ptr复制或赋给另一个的方法或算法(如sort( ))
◆ 其中的push_back( )调用没有问题,因为它返回一个临时unique_ptr,该unique_ptr被赋给vp中的一个unique_ptr。
◆ 如果按值而不是按引用给show( )传递对象,for_each( )语句将非法,因为这将导致使用一个来自vp的非临时unique_ptr初始化pi,而这是不允许的
◆ 模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptr。shared_ptr将接管原来归unique_ptr所有的对象。
◆ STL提供了一组表示容器、迭代器、函数对象和算法的模板。
◆ STL容器是同质的,即存储的值的类型相同
◆ 迭代器能够用来遍历容器的对象,与能够遍历数组的指针类似,是广义指针;
◆ STL不是面向对象的编程,而是一种不同的编程模式——泛型编程(generic programming)。这使得STL在功能和方法方面都很有趣。
◆ 在计算中,矢量(vector)对应数组
◆ 所以vector类提供了与第14章介绍的valarray和ArrayTP以及第4章介绍的array类似的操作,即可以创建vector对象,将一个vector对象赋给另一个对象,使用[ ]运算符来访问vector元素。要使类成为通用的,应将它设计为模板类,STL正是这样做的——在头文件vector(以前为vector.h)中定义了一个vector模板。
◆ 与string类相似,各种STL容器模板都接受一个可选的模板参数,该参数指定使用哪个分配器对象来管理内存。
◆ 如果省略该模板参数的值,则容器模板将默认使用allocator类。这个类使用new和delete。
◆ 所有的STL容器都提供了一些基本方法,其中包括size( )——返回容器中元素数目、swap( )——交换两个容器的内容、begin( )——返回一个指向容器中第一个元素的迭代器、end( )——返回一个表示超过容器尾的迭代器。
◆ 事实上,它可以是指针,也可以是一个可对其执行类似指针的操作——如解除引用(如operator*( ))和递增(如operator++( ))——的对象。
◆ 什么是超过结尾(past-the-end)呢?它是一种迭代器,指向容器最后一个元素后面的那个元素。
◆ 只是空字符是一个值,而“超过结尾”是一个指向元素的指针(迭代器)
◆ end( )成员函数标识超过结尾的位置
◆ vector模板类也包含一些只有某些STL容器才有的方法。push_back( )是一个方便的方法,它将元素添加到矢量末尾。
◆ 它接受两个迭代器参数,这些参数定义了要删除的区间。了解STL如何使用两个迭代器来定义区间至关重要。第一个迭代器指向区间的起始处,第二个迭代器位于区间终止处的后一个位置。
◆ 第一个参数指定了新元素的插入位置,第二个和第三个迭代器参数定义了被插入区间,
◆ 顺便说一句,对于这种情况,拥有超尾元素是非常方便的,因为这使得在矢量尾部附加元素非常简单。
◆ STL从更广泛的角度定义了非成员(non-member)函数来执行这些操作,即不是为每个容器类定义find( )成员函数,而是定义了一个适用于所有容器类的非成员函数find( )。
◆ 即使有执行相同任务的非成员函数,STL有时也会定义一个成员函数。这是因为对有些操作来说,类特定算法的效率比通用算法高,因此,vector的成员函数swap( )的效率比非成员函数swap( )高,但非成员函数让您能够交换两个类型不同的容器的内容。
◆ 最后一个是指向函数的指针(更普遍地说,最后一个参数是一个函数对象,函数对象将稍后介绍
◆ Random_shuffle( )函数接受两个指定区间的迭代器参数,并随机排列该区间中的元素
◆ 与可用于任何容器类的for_each不同,该函数要求容器类允许随机访问,vector类可以做到这一点。
◆ sort( )函数也要求容器支持随机访问。
◆ 并使用为存储在容器中的类型元素定义的<运算符,对区间中的元素进行操作。例如,下面的语句按升序对coolstuff的内容进行排序,排序时使用内置的<运算符对值进行比较
◆ 如果容器元素是用户定义的对象,则要使用sort( ),必须定义能够处理该类型对象的operator<( )函数。
◆ 可以使用另一种格式的sort()。它接受3个参数,前两个参数也是指定区间的迭代器,最后一个参数是指向要使用的函数的指针(函数对象
◆ 与operator<( )相比,WorseThan( )函数执行的对Review对象进行排序的工作不那么完整。如果两个对象的title成员相同,operator<( )函数将按rating进行排序,而WorseThan( )将它们视为相同。第一种排序称为全排序(total ordering),第二种排序称为完整弱排序(strict weak ordering)。在全排序中,如果a<b和b<a都不成立,则a和b必定相同。在完整弱排序中,情况就不是这样了。它们可能相同,也可能只是在某方面相同,如WorseThan( )示例中的rating成员。所以在完整弱排序中,只能说它们等价,而不是相同。
◆ 基于范围的for循环是为用于STL而设计的。
◆ 循环体使用指定的变量依次访问容器的每个元素。
◆ 根据book的类型(vector),编译器将推断出x的类型为Review,而循环将依次将books中的每个Review对象传递给ShowReview( )。
◆ 不同于for_each( ),基于范围的for循环可修改容器的内容,诀窍是指定一个引用参数。
◆ 面向对象编程关注的是编程的数据方面,而泛型编程关注的是算法。它们之间的共同点是抽象和创建可重用代码,但它们的理念绝然不同。
◆ 模板让这一切成为可能,但必须对元素进行仔细地设计。为解模板和设计是如何协同工作的,来看一看需要迭代器的原因。
◆ 模板使得算法独立于存储的数据类型,而迭代器使算法独立于使用的容器类型
◆ 从实现细节上看,这两个find函数的算法是不同的:一个使用数组索引来遍历元素,另一个则将start重置为start->p_next。但从广义上说,这两种算法是相同的:将值依次与容器中的每个值进行比较,直到找到匹配的为止。
◆ 泛型编程旨在使用同一个find函数来处理数组、链表或任何其他容器类型
◆ 要实现find函数,迭代器应具备哪些特征呢?
◆ 应能够对迭代器执行解除引用的操作,以便能够访问它引用的值。即如果p是一个迭代器,则应对*p进行定义。
◆ 应能够将一个迭代器赋给另一个。即如果p和q都是迭代器,则应对表达式p=q进行定义。
◆ 应能够将一个迭代器与另一个进行比较,看它们是否相等。即如果p和q都是迭代器,则应对p= =q和p!=q进行定义。
◆ 应能够使用迭代器遍历容器中的所有元素,这可以通过为迭代器p定义++p和p++来实现。
◆ 实际上,STL按功能的强弱定义了多种级别的迭代器,这将在后面介绍。顺便说一句,常规指针就能满足迭代器的要求,因此,可以这样重新编写find_arr( )函数:
◆ 其中的一个指向数组的起始位置,另一个指向数组的超尾
◆ 为区分++运算符的前缀版本和后缀版本,C++将operator++作为前缀版本,将operator++(int)作为后缀版本;其中的参数永远也不会被用到,所以不必指定其名称。
◆ find_ar( )函数使用超尾迭代器,而find_ll( )使用存储在最后一个节点中的空值。除了这种差别外,这两个函数完全相同
◆ 首先,每个容器类(vector、list、deque等)定义了相应的迭代器类型。
◆ 其次,每个容器类都有一个超尾标记,当迭代器递增到超越容器的最后一个值后,这个值将被赋给迭代器。
◆ 下面的代码行将pr的类型声明为vector类的迭代器
◆ 使用C++11新增的自动类型推断可进一步简化:对于矢量或列表,都可使用如下代码
◆ 来总结一下STL方法。首先是处理容器的算法,应尽可能用通用的术语来表达算法,使之独立于数据类型和容器类型。
◆ 不同的算法对迭代器的要求也不同。例如,查找算法需要定义++运算符,以便迭代器能够遍历整个容器;它要求能够读取数据,但不要求能够写数据(它只是查看数据,而并不修改数据)。而排序算法要求能够随机访问,以便能够交换两个不相邻的元素。如果iter是一个迭代器,则可以通过定义+运算符来实现随机访问,这样就可以使用像iter + 10这样的表达式了。另外,排序算法要求能够读写数据。
◆ 这5种迭代器分别是输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器
◆ 都可以执行解除引用操作(即为它们定义了*运算符),也可进行比较,看其是相等(使用= =运算符,可能被重载了)还是不相等(使用!=运算符,可能被重载了)。如果两个迭代器相同,则对它们执行解除引用操作得到的值将相同。
◆ 术语“输入”是从程序的角度说的,即来自容器的信息被视为输入,就像来自键盘的信息对程序来说是输入一样
◆ 具体地说,对输入迭代器解除引用将使程序能够读取容器中的值,但不一定能让程序修改值。
◆ 需要输入迭代器的算法将不会修改容器中的值。
◆ 2024/04/23发表想法
没有重载--运算符
原文:基于输入迭代器的任何算法都应当是单通行(single-pass)的,不依赖于前一次遍历时的迭代器值,也不依赖于本次遍历中前面的迭代器值。注意,输入迭代器是单向迭代器,可以递增,但不能倒退。
◆ 基于输入迭代器的任何算法都应当是单通行(single-pass)的,不依赖于前一次遍历时的迭代器值,也不依赖于本次遍历中前面的迭代器值。注意,输入迭代器是单向迭代器,可以递增,但不能倒退。
◆ STL使用术语“输出”来指用于将信息从程序传输给容器的迭代器,因此程序的输出就是容器的输入
◆ 与输入迭代器和输出迭代器相似,正向迭代器只使用++运算符来遍历容器,所以它每次沿容器向前移动一个元素;
◆ 双向迭代器具有正向迭代器的所有特性,同时支持两种(前缀和后缀)递减运算符。
◆ 像a+n这样的表达式仅当a和a+n都位于容器区间(包括超尾)内时才合法,
◆ 随机访问迭代器具有正向迭代器的全部功能,同时还有自己的功能
◆ 这样,通过使用级别最低的输入迭代器,find( )函数便可用于任何包含可读取值的容器。而sort( )函数由于需要随机访问迭代器,所以只能用于支持这种迭代器的容器。
◆ 各种迭代器的类型并不是确定的,而只是一种概念性描述。正如前面指出的,每个容器类都定义了一个类级typedef名称——iterator,因此vector类的迭代器类型为vector ::interator。然而,该类的文档将指出,矢量迭代器是随机访问迭代器,它允许使用基于任何迭代器类型的算法,因为随机访问迭代器具有所有迭代器的功能。
◆ 原因在于,正向迭代器是一系列要求,而不是类型
◆ 概念的具体实现被称为模型(model)。因此,指向int的常规指针是一个随机访问迭代器模型,也是一个正向迭代器模型,因为它满足该概念的所有要求。
◆ 迭代器是广义指针,而指针满足所有的迭代器要求。迭代器是STL算法的接口,而指针是迭代器,因此STL算法可以使用指针来对基于指针的非STL容器进行操作
◆ C++确保了表达式Receipts + n是被定义的,只要该表达式的结果位于数组中。因此,C++支持将超尾概念用于数组,使得可以将STL算法用于常规数组。由于指针是迭代器,而算法是基于迭代器的,这使得可将STL算法用于常规数组。同样,可以将STL算法用于自己设计的数组形式,只要提供适当的迭代器(可以是指针,也可以是对象)和超尾指示器即可
◆ 前两个参数必须是(或最好是)输入迭代器,最后一个参数必须是(或最好是)输出迭代器。Copy( )函数将覆盖目标容器中已有的数据,同时目标容器必须足够大,以便能够容纳被复制的元素
◆ 用STL的话说,该模板是输出迭代器概念的一个模型,它也是一个适配器(adapter)——一个类或函数,可以将一些其他接口转换为STL使用的接口。
◆ 构造函数的第一个参数(这里为cout)指出了要使用的输出流,它也可以是用于文件输出的流(参见第17章);最后一个字符串参数是在发送给输出流的每个数据项后显示的分隔符。
◆ 这意味着将dice容器的整个区间复制到输出流中,即显示容器的内容。
◆ vector类有一个名为rbegin( )的成员函数和一个名为rend( )的成员函数,前者返回一个指向超尾的反向迭代器,后者返回一个指向第一个元素的反向迭代器
◆ rbegin( )和end( )返回相同的值(超尾),但类型不同(reverse_iterator和iterator)。同样,rend( )和begin( )也返回相同的值(指向第一个元素的迭代器),但类型不同
◆ 必须对反向指针做一种特殊补偿。
◆ 反向指针通过先递减,再解除引用解决了这两个问题。即rp将在rp的当前值之前对迭代器执行解除引用。
◆ 请采用后者。后一种方法要做的工作较少,人为出错的机会也较少。
◆ 另外三种迭代器(back_insert_iterator、front_insert_iterator和insert_iterator)也将提高STL算法的通用性。很多STL函数都与copy( )相似,将结果发送到输出迭代器指示的位置。
◆ 这些值将覆盖dice中以前的内容,且该函数假设dice有足够的空间,能够容纳这些值,即copy( )不能自动根据发送值调整目标容器的长度。
◆ 三种插入迭代器通过将复制转换为插入解决了这些问题。插入将添加新的元素,而不会覆盖已有的数据,并使用自动内存分配来确保能够容纳新的信息。back_insert_iterator将元素插入到容器尾部,而front_insert_iterator将元素插入到容器的前端
◆ 这三个插入迭代器都是输出容器概念的模型。
◆ 这里存在一些限制。back_insert_iterator只能用于允许在尾部快速插入的容器(快速插入指的是一个时间固定的算法,将在本章后面的“容器概念”一节做进一步讨论),vector类符合这种要求。front_insert_iterator只能用于允许在起始位置做时间固定插入的容器类型,vector类不能满足这种要求,但queue满足。insert_iterator没有这些限制,因此可以用它把信息插入到矢量的前端。然而,front_insert_iterator对于那些支持它的容器来说,完成任务的速度更快。
◆ 必须声明容器类型的原因是,迭代器必须使用合适的容器方法
◆ 该方法有这样的权限。
◆ 第一个copy( )从s1中复制4个字符串到words中。
◆ 如果程序试图使用words.end( )和words.begin( )作为迭代器,将s2和s3复制到words中,words将没有空间来存储新数据,程序可能会由于内存违规而异常终止。
◆ copy( )只是是使用输出迭代器的若干STL函数之一,因此这些预定义迭代器也增加了这些函数的功能。
◆ STL具有容器概念和容器类型。概念是具有名称(如容器、序列容器、关联容器等)的通用类别;容器类型是可用于创建具体容器对象的模板。
◆ 1个容器类型分别是deque、list、queue、priority_queue、stack、vector、map、multimap、set、multiset和bitset(本章不讨论bitset,它是在比特级处理数据的容器);C++11新增了forward_list、unordered_map、unordered_multimap、unordered_set和unordered_multiset,且不将bitset视为容器,而将其视为一种独立的类别。因为概念对类型进行了分类,下面先讨论它们。
◆ 没有与基本容器概念对应的类型,但概念描述了所有容器类都通用的元素。它是一个概念化的抽象基类——说它概念化,是因为容器类并不真正使用继承机制。换句话说,容器概念指定了所有STL容器类都必须满足的一系列要求。
◆ 容器是存储其他对象的对象。被存储的对象必须是同一种类型的,它们可以是OOP意义上的对象,也可以是内置类型值。
◆ 这意味着当容器过期时,存储在容器中的数据也将过期(然而,如果数据是指针的话,则它指向的数据并不一定过期)。
◆ 不能将任何类型的对象存储在容器中,具体地说,类型必须是可复制构造的和可赋值的。
◆ 只要类定义没有将复制构造函数和赋值运算符声明为私有或保护的,则也满足这种要求
◆ 基本容器不能保证其元素都按特定的顺序存储,也不能保证元素的顺序不变,但对概念进行改进后,则可以增加这样的保证
◆ 如果复杂度为编译时间,则操作将在编译时执行,执行时间为0。
◆ 即如果a和b都是容器,则a = = b具有线性复杂度,因为= =操作必须用于容器中的每个元素。实际上,这是最糟糕的情况。如果两个容器的长度不同,则不需要作任何的单独比较。
◆ 复杂度要求是STL特征,虽然实现细节可以隐藏,但性能规格应公开,以便程序员能够知道完成特定操作的计算成本。
◆ 在这个表中,rv表示类型为X的非常量右值,如函数的返回值。
◆ 复制构造和复制赋值以及移动构造和移动赋值之间的差别在于,复制操作保留源对象,而移动操作可修改源对象,还可能转让所有权,而不做任何复制。如果源对象是临时的,移动操作的效率将高于常规复制。
◆ 序列(sequence)是一种重要的改进,因为7种STL容器类型(deque、C++11新增的forward_list、list、queue、priority_queue、stack和vector)都是序列(本书前面说过,队列让您能够在队尾添加元素,在队首删除元素。
◆ 序列概念增加了迭代器至少是正向迭代器这样的要求,这保证了元素将按特定顺序排列,不会在两次迭代之间发生变化。array也被归类到序列容器,虽然它并不满足序列的所有要求。
◆ 序列还要求其元素按严格的线性顺序排列,即存在第一个元素、最后一个元素,除第一个元素和最后一个元素外,每个元素前后都分别有一个元素。
◆ 因为模板类deque、list、queue、priority_queue、stack和vector都是序列概念的模型,所以它们都支持表16.7所示的运算符。
◆ 在允许的情况下,它们的复杂度为固定时间。
◆ a[n]和a.at(n)都返回一个指向容器中第n个元素(从0开始编号)的引用。它们之间的差别在于,如果n落在容器的有效区间外,则a.at(n)将执行边界检查,并引发out_of_range异常。
◆ 为何为list和deque定义了push_front( ),而没有为vector定义?
◆ 简单地说,vector是数组的一种类表示,它提供了自动内存管理功能,可以动态地改变vector对象的长度,并随着元素的添加和删除而增大和缩小。
◆ vector还是可反转容器(reversible container)概念的模型。这增加了两个类方法:rbegin( )和rend( ),前者返回一个指向反转序列的第一个元素的迭代器,后者返回反转序列的超尾迭代器。
◆ 这两种方法返回的迭代器都是类级类型reverse_iterator。对这样的迭代器进行递增,将导致它反向遍历可反转容器。
◆ 从deque对象的开始位置插入和删除元素的时间是固定的,而不像vector中那样是线性时间的。
◆ 因此,尽管二者都提供对元素的随机访问和在序列中部执行线性时间的插入和删除操作,但vector容器执行这些操作时速度要快些
◆ list模板类(在list头文件中声明)表示双向链表。
◆ list在链表中任一位置进行插入和删除的时间都是固定的(vector模板提供了除结尾处外的线性时间的插入和删除,在结尾处,它提供了固定时间的插入和删除)。因此,vector强调的是通过随机访问进行快速访问,而list强调的是元素的快速插入和删除。
◆ list不支持数组表示法和随机访问
◆ 指向某个元素的迭代器仍然指向该元素,但它链接的元素可能与以前不同。
◆ 程序清单16.12演示了这些方法和insert( )方法(所有模拟序列的STL类都有这种方法)的用法。
◆ insert( )将原始区间的副本插入到目标地址,而splice( )则将原始区间移到目标地址。
◆ splice( )方法执行后,迭代器仍有效。也就是说,如果将迭代器设置为指向one中的元素,则在splice( )将它重新定位到元素three后,该迭代器仍然指向相同的元素。
◆ unique( )只能将相邻的相同值压缩为单个值。程序执行three.unique( )后,three中仍包含不相邻的两个4和两个6。但应用sort( )后再应用unique( )时,每个值将只占一个位置。
◆ sort( )、merge( )和unique( )方法还各自拥有接受另一个参数的版本,该参数用于指定用来比较元素的函数
◆ 因此forward_list只需要正向迭代器,而不需要双向迭代器。因此,不同于vector和list,forward_list是不可反转的容器。相比于list,forward_list更简单、更紧凑,但功能也更少。
◆ 可以将元素添加到队尾、从队首删除元素、查看队首和队尾的值、检查元素数目和测试队列是否为空。
◆ priority_queue模板类(在queue头文件中声明)是另一个适配器类,它支持的操作与queue相同。两者之间的主要区别在于,在priority_queue中,最大的元素被移到队首(生活不总是公平的,队列也一样)。内部区别在于,默认的底层类是vector。
◆ greater< >( )函数是一个预定义的函数对象
◆ 与queue相似,stack(在头文件stack——以前为stack.h——中声明)也是一个适配器类,它给底层类(默认情况下为vector)提供了典型的栈接口。
◆ stack模板的限制比vector更多。它不仅不允许随机访问栈元素,甚至不允许遍历栈。它把使用限制在定义栈的基本操作上,即可以将压入推到栈顶、从栈顶弹出元素、查看栈顶的值、检查元素数目和测试栈是否为空。
◆ 如果要使用栈中的值,必须首先使用top( )来检索这个值,然后使用pop( )将它从栈中删除。
◆ 第4章介绍过,模板类array是否头文件array中定义的,它并非STL容器,因为其长度是固定的。因此,array没有定义调整容器大小的操作,如push_back( )和insert( ),但定义了对它来说有意义的成员函数,如operator [] ()和at( )。可将很多标准STL算法用于array对象,如copy( )和for_each()。
◆ 关联容器(associative container)是对容器概念的另一个改进。关联容器将值与键关联在一起,并使用键来查找值。
◆ 对于容器X,表达式X::value_type通常指出了存储在容器中的值类型。对于关联容器来说,表达式X::key_type指出了键的类型。
◆ 与序列相似,关联容器也允许插入新元素,但不能指定元素的插入位置。原因是关联容器通常有用于确定数据放置位置的算法,以便能够快速检索信息。
◆ 关联容器通常是使用某种树实现的。树是一种数据结构,其根节点链接到一个或两个节点,而这些节点又链接到一个或两个节点,从而形成分支结构。
◆ STL提供了4种关联容器:set、multiset、map和multimap。
◆ 最简单的关联容器是set,其值类型与键相同,键是唯一的,这意味着集合中不会有多个相同的键。
◆ 在map中,值与键的类型不同,键是唯一的,每个键只对应一个值。multimap与map相似,只是一个键可以与多个值相关联。
◆ 与其他容器相似,set也有一个将迭代器区间作为参数的构造函数(参见表16.6)。这提供了一种将集合初始化为数组内容的简单方法。
◆ STL提供了支持这些操作的算法。它们是通用函数,而不是方法,因此并非只能用于set对象。
◆ 函数set_intersection( )和set_difference( )分别查找交集和获得两个集合的差,它们的接口与set_union( )相同。
◆ 这样做旨在简化表示方式。这些示例使用了名称空间std中非常多的元素,如果使用using声明或作用域运算符,代码将变得混乱
◆ 与set相似,multimap也是可反转的、经过排序的关联容器,但键和值的类型不同,且同一个键可能与多个值相关联。
◆ 在默认情况下,将使用模板less< >(稍后将讨论)
◆ 实际的值类型将键类型和数据类型结合为一对
◆ 为此,STL使用模板类pair<class T, class U>将这两种值存储到一个对象中。
◆ 而datatype是存储的数据类型,则值类型为pair<const keytype, datatype
◆ 对于pair对象,可以使用first和second成员来访问其两个部分了:
◆ 成员函数equal_range( )用键作为参数,且返回两个迭代器,它们表示的区间与该键匹配。为返回两个值,该方法将它们封装在一个pair对象中,这里pair的两个模板参数都是迭代器。
◆ 无序关联容器是对容器概念的另一种改进。
◆ 无序关联容器也将值与键关联起来,并使用键来查找值。但底层的差别在于,关联容器是基于树结构的,而无序关联容器是基于数据结构哈希表的,这旨在提高添加和删除元素的速度以及提高查找算法的效率。
◆ 很多STL算法都使用函数对象——也叫函数符(functor)
◆ 函数符是可以以函数方式与( )结合使用的任意对象。这包括函数名、指向函数的指针和重载了( )运算符的类对象(即定义了函数operator()( )的类)。
◆ 重载的( )运算符将使得能够像函数那样使用Linear对象
◆ 第3个参数可以是常规函数,也可以是函数符
◆ 不能把它声明为函数指针,因为函数指针指定了参数类型。
◆ STL通过使用模板解决了这个问题。
◆ Function参数可以表示具有重载的( )运算符的类类型
◆ 如果最后的for_each( )参数是一个对象,则f( )将是调用其重载的( )运算符的对象。
◆ 正如STL定义了容器和迭代器的概念一样,它也定义了函数符概念。
◆ 生成器(generator)是不用参数就可以调用的函数符。一元函数(unary function)是用一个参数可以调用的函数符。二元函数(binary function)是用两个参数可以调用的函数符。
◆ 例如,提供给for_each( )的函数符应当是一元函数,因为它每次用于一个容器元素。
◆ 一些STL函数需要谓词参数或二元谓词参数。例如,程序清单16.9使用了sort( )的这样一个版本,即将二元谓词作为其第3个参数:
◆ list模板有一个将谓词作为参数的remove_if( )成员
◆ 如果谓词返回true,则删除这些元素。
◆ 类函数符适
◆ 然而,如果设计一个TooBig类,则可以使用类成员而不是函数参数来传递额外的信息
◆ 一个函数符(f100)是一个声明的对象,而另一个函数符(TooBig(200))是一个匿名对象,它是由构造函数调用创建的。
◆ 简而言之,类函数符TooBig2是一个函数适配器,使函数能够满足不同的接口。
◆ 在该程序清单中,可使用C++11的初始化列表功能来简化初始化。
◆ STL定义了多个基本函数符,它们执行诸如将两个值相加、比较两个值是否相等操作。提供这些函数对象是为了支持将函数作为参数的STL函数。
◆ 很明显,使用的函数符必须是接受单个参数的函数符。
第二十二章 16.5.3 自适应函数符和函数适配器
◆ 更好的办法是定义一个模板(除非STL已经有一个模板了,这样就不必定义)。头文件functional(以前为function.h)定义了多个模板类函数对象,其中包括plus< >( )
◆ 2024/04/24发表想法
默认构造函数,产生一个匿名对象,也就是一个函数符
原文:这里,代码没有创建命名的对象,而是用plus构造函数构造了一个函数符,以完成相加运算(括号表示调用默认的构造函数,传递给transform( )的是构造出来的函数对象)
◆ 这里,代码没有创建命名的对象,而是用plus构造函数构造了一个函数符,以完成相加运算(括号表示调用默认的构造函数,传递给transform( )的是构造出来的函数对象)
◆ 对于所有内置的算术运算符、关系运算符和逻辑运算符,STL都提供了等价的函数符。
◆ 老式C++实现使用函数符名times,而不是multiplies。
◆ 表16.12列出的预定义函数符都是自适应的
◆ 接受一个自适应函数符参数的函数可以使用result_type成员来声明一个与函数的返回类型匹配的变量。
◆ multiplies( )函数符可以执行乘法运行,但它是二元函数。因此需要一个函数适配器,将接受两个参数的函数符转换为接受1个参数的函数符。
◆ 即f1(x)等价于f2(val, x),只是前者是一元函数,而不是二元函数
◆ 可以问其提供用于构建binder1st对象的函数名称和值,它将返回一个这种类型的对象。
◆ C++11提供了函数指针和函数符的替代品——lambda表达式,这将在第18章讨论。
◆ 对于算法函数设计,有两个主要的通用部分。首先,它们都使用模板来提供泛型;其次,它们都使用迭代器来提供访问容器中数据的通用表示。
◆ 因为指针是一种特殊的迭代器,因此诸如copy( )等STL函数可用于常规数组。
◆ 可以用= =来比较不同类型的容器,如deque和vector。之所以能够这样做,是因为容器重载的= =运算符使用迭代器来比较内容,因此如果deque对象和vector对象的内容相同,并且排列顺序也相同,则它们是相等的。
◆ 前3组在头文件algorithm(以前为algo.h)中描述,第4组是专用于数值数据的,有自己的头文件,称为numeric(以前它们也位于algol.h中)。
◆ 例如,find( )和for_each( )就属于这一类。
◆ STL函数使用迭代器和迭代器区间。
◆ 区间参数必须是输入迭代器或更高级别的迭代器,而指示结果存储位置的迭代器必须是输出迭代器或更高级别的迭代器。
◆ 在sort( )函数完成时,结果被存放在原始数据的位置上,因此,sort( )是就地算法(in-place algorithm);而copy( )函数将结果发送到另一个位置,所以它是复制算法(copying algorithm)。
◆ eplace_copy( )的返回类型为OutputIterator。对于复制算法,统一的约定是:返回一个迭代器,该迭代器指向复制的最后一个值后面的一个位置。
◆ string类虽然不是STL的组成部分,但设计它时考虑到了STL。例如,它包含begin( )、end( )、rbegin( )和rend( )等成员,因此可以使用STL接口。
◆ 有时可以选择使用STL方法或STL函数。通常方法是更好的选择。
◆ 调用该方法后,链表中所有值为4的元素都将被删除,同时链表的长度将被自动调整。
◆ 还有一个名为remove( )的STL算法(见附录G),它不是由对象调用,而是接受区间参数。因此,如果lb是一个list对象,则调用该函数的代码如下
◆ 由于该remove( )函数不是成员,因此不能调整链表的长度
◆ 并返回一个指向新的超尾值的迭代器。这样,便可以用该迭代器来修改容器的长度。
◆ 最后4个元素可任意处理,因为其中每个元素要么为4,要么与已经移到链表开头的值相同。
◆ STL是一个库,其组成部分被设计成协同工作。
◆ 集合自动对其内容进行排序,因此无需调用sort( );
◆ 另外,STL设计者就是非常关心效率的算法人员,算法是经过仔细选择的,并且是内联的。
◆ 第14章介绍了头文件valarray提供的模板类valarray。这个类模板被设计成用于表示数值数组,支持各种数值数组操作,例如将两个数组的内容相加、对数组的每个元素应用数学函数以及对数组进行线性代数运算。
◆ 这些类是由不同的小组开发的,用于不同的目的。
◆ 而valarray类模板是面向数值计算的,不是STL的一部分。例如,它没有push_back( )和insert( )方法,但为很多数学运算提供了一个简单、直观的接口
◆ Array表示长度固定的数组,因此不支持push_back( )和insert( ),但提供了多个STL方法,包括begin( )、end( )、rbegin( )和rend( ),这使得很容易将STL算法用于array对象。
◆ 然而,valarray类重载了所有算术运算符,使其能够用于valarray对象,因此您可以这样做:
◆ valarray类重载了这种数学函数,使之接受一个valarray参数,并返回一个valarray对象,因此您可以这样做:
◆ valarray类还提供了方法sum( )(计算valarray对象中所有元素的和)、size( )(返回元素数)、max( )(返回最大的元素值)和min( )(返回最小的元素值)。
◆ 总之,与vector类相比,valarray类关注的东西更少,但这使得它的接口更简单。
◆ 使用数字填充该数组后,能够将STL sort( )函数用于该数组吗?valarray类没有begin( )和end( )方法,因此不能将它们用作指定区间的参数
◆ 但valarray没有定义下标超过尾部一个元素的行为。这并不一定意味着使用&vadp[10]不可行。事实上,使用6种编译器测试上述代码时,都是可行的;
◆ 为解决这种问题,C++11提供了接受valarray对象作为参数的模板函数begin( )和end( )。因此,您将使用begin(vad)而不是vad.begin。
◆ slice类对象可用作数组索引,在这种情况下,它表的不是一个值而是一组值。
◆ 对于valarray对象(如valint)和单个int元素(如valint[1]),定义了运算符+;但正如程序清单16.21指出的,对于使用slice下标指定的valarray单元,如valint[slice(1, 4, 3),并没有定义运算符+。因此程序使用slice指定的元素创建一个完整的valint对象
◆ valarray类提供了用于这种目的的构造函数。
◆ vector包含一个将initializer_list作为参数的构造函数
◆ 这里显式地将列表指定为构造函数参数。
◆ 通常,考虑到C++11新增的通用初始化语法,可使用表示法{}而不是()来调用类构造函数:
◆ 答案是,如果类有接受initializer_list作为参数的构造函数,则使用语法{}将调用该构造函数。因此在这个示例中,对应的是情形B。
◆ 所有initializer_list元素的类型都必须相同,但编译器将进行必要的转换
◆ 所以19和89被转换为double。
◆ 但不能进行隐式的窄化转换:
◆ 在下面的声明中,类包含三个数据成员,因此没有提供initializer_list作为参数的构造函数
◆ 可按值传递initializer_list对象,也可按引用传递,如sum()和average()所示。
◆ 也可能是一个指针和一个表示元素数的整数,因此采用的传递方式不会带来重大的性能影响。STL按值传递它们
◆ initializer_list的迭代器类型为const,因此您不能修改initializer_list中的值
◆ 然而,提供initializer_list类的初衷旨在让您能够将一系列值传递给构造函数或其他函数。
◆ 诸如auto_ptr以及C++11新增的shared_ptr和unique_ptr等智能指针模板使得管理由new分配的内存更容易。如果使用这些智能指针(而不是常规指针)来保存new返回的地址,则不必在以后使用删除运算符。智能指针对象过期时,其析构函数将自动调用delete运算符。
◆ STL是一个容器类模板、迭代器类模板、函数对象模板和算法函数模板的集合,它们的设计是一致的,都是基于泛型编程原则的。
◆ STL使用术语“概念”来描述一组要求。例如,正向迭代器的概念包含这样的要求,即正向迭代器能够被解除引用,以便读写,同时能够被递增。概念真正的实现方式被称为概念的“模型”。例如,正向迭代器概念可以是常规指针或导航链表的对象。基于其他概念的概念叫作“改进”。例如,双向迭代器是正向迭代器概念的改进。
◆ 诸如vector和set等容器类是容器概念(如容器、序列和关联容器)的模型。
◆ 容器类模板:vector、deque、list、set、multiset、map、multimap和bitset
◆ 适配器类模板queue、priority_queue和stack
◆ 但大量算法都被表示为通用的、非成员函数,这是通过将迭代器作为容器和算法之间的接口得以实现的
◆ 另一个优点是:STL算法可用于非STL容器,如常规数组、string对象、array对象以及您设计的秉承STL迭代器和容器规则的任何类。
◆ 而sort( )则要求随机访问迭代器,并非所有的容器类都支持这种迭代器
◆ STL还提供了函数对象(函数符),函数对象是重载了( )运算符(即定义了operator( )( )方法)的类。
◆ 模板类complex和valarray支持复数和数组的数值运算
第17章 输入、输出和文件
◆ 一方面,几乎每个程序都要使用输入和输出,因此了解如何使用它们是每个学习计算机语言的人面临的首要任务
◆ 用于文件输入和输出的C++工具都是基于cin和cout所基于的基本类定义,因此本章以对控制台I/O(键盘和屏幕)的讨论为跳板,来研究文件I/O。
◆ 但C和C++都没有将输入和输出建立在语言中。这两种语言的关键字包括for和if,但不包括与I/O有关的内容
◆ C语言最初把I/O留给了编译器实现人员。这样做的一个原因是为了让实现人员能够自由的设计I/O函数,使之最适合于目标计算机的硬件要求。
◆ 然而,C++依赖于C++的I/O解决方案,而不是C语言的I/O解决方案,前者是在头文件iostream(以前为iostream.h)和fstream(以前为fstream.h)中定义一组类。
◆ 首先,标准类库是一个非正式的标准,只是由头文件iostream和fstream中定义的类组成
◆ 字节可以构成字符或数值数据的二进制表示。
◆ 流充当了程序和流源或流目标之间的桥梁。这使得C++程序可以以相同的方式对待来自键盘的输入和来自文件的输入。C++程序只是检查字节流,而不需要知道字节来自何方
◆ 换句话说,输入流需要两个连接,每端各一个。
◆ 程序端连接将流的流出部分转储到程序中(文件端连接可以是文件,也可以是设备,如键盘)
◆ 缓冲区是用作中介的内存块,它是将信息从设备传输到程序或从程序传输给设备的临时存储工具。
◆ 缓冲区帮助匹配这两种不同的信息传输速率。
◆ 这种原理与水库在暴风雨中收集几兆加仑流量的水,然后以比较文明的速度给您家里供水是一样的(见图17.2)。
◆ 这被称为刷新缓冲区(flushing the buffer)。
◆ 键盘输入每次提供一个字符,因此在这种情况下,程序无需缓冲区来帮助匹配不同的数据传输速率。然而,对键盘输入进行缓冲可以让用户在将输入传输给程序之前返回并更正
◆ C++程序通常在用户按下回车键时刷新输入缓冲区。这是为什么本书的例子没有一开始就处理输入,而是等到用户按下回车键后再处理的原因。
◆ C++程序通常在用户发送换行符时刷新输出缓冲区。
◆ 但iostream(以前为iostream.h)文件中包含一些专门设计用来实现、管理流和缓冲区的类。
◆ streambuf类为缓冲区提供了内存,并提供了用于填充缓冲区、访问缓冲区内容、刷新缓冲区和管理缓冲区内存的类方法;
◆ iostream类是基于istream和ostream类的,因此继承了输入方法和输出方法。
◆ 使用ostream对象(如cout)来处理输出。创建这样的对象将打开一个流,自动创建缓冲区,并将其与流关联起来,同时使得能够使用类成员函数。
◆ 为成为国际语言,C++必须能够处理需要16位的国际字符集或更宽的字符类型。因此,该语言在传统的8位char(“窄”)类型的基础上添加了wchar_t(“宽”)字符类型;
◆ 例如ios::fixed(现在为ios_base::fixed)。另外,ios_base还包含了一些老式ios中没有的选项。
◆ 例如,在程序中包含iostream文件将自动创建8个流对象(4个用于窄字符流,4个用于宽字符流)。
◆ 在默认情况下,这个流被关联到标准输入设备(通常为键盘)
◆ 这个流被关联到标准输出设备(通常为显示器)。
◆ cerr对象与标准错误流相对应,可用于显示错误消息。在默认情况下,这个流被关联到标准输出设备(通常为显示器)。
◆ 当iostream文件为程序声明一个cout对象时,该对象将包含存储了与输出有关的信息的数据成员,如显示数据时使用的字段宽度、小数位数、显示整数时采用的计数方法以及描述用来处理输出流的缓冲区的streambuf对象的地址。
◆ 总之,流的一端与程序相连,另一端与标准输出相连,cout对象凭借streambuf对象的帮助,管理着流中的字节流。
◆ 但很多操作系统(包括UNIX、Linux和Windows)都支持重定向,这个工具使得能够改变标准输入和标准输出。例如,假设有一个名为counter.exe的、可
◆ 命令行中的<oklahoma将标准输入与oklahoma文件关联起来,使cin从该文件(而不是键盘)读取输入。
◆ 命令行中的>cow_cnt将标准输出与cow_cnt文件关联起来,导致cout将输出发送给文件(而不是屏幕)
◆ DOS、Windows命令提示符模式、Linux和UNIX能自动识别这种重定向语法
◆ 例如,在UNIX和Linux中,运算符2>重定向标准错误。
◆ 因此,ostream类最重要的任务之一是将数值类型(如int或float)转换为以文本形式表示的字符流。
◆ ostream类将数据内部表示(二进制位模式)转换为由字符字节组成的输出流
◆ 但ostream类重新定义了<<运算符,方法是将其重载为输出。
◆ <<叫作插入运算符,而不是左移运算符(左移运算符由于其外观(像向左流动的信息流)而获得这种新角色)。插入运算符被重载,使之能够识别C++中所有的基本类型
◆ 对于上述每种数据类型,ostream类都提供了operator<<( )函数的定义(第11章讨论过,名称中包含运算符的函数用于重载该运算符)。
◆ 2024/04/26发表想法
这样可以保证这种连续插入起作用,插入第一个对象之后,返回一个 ostream 引用,从而继续可以插入。
原文:该原型还表明,函数返回一个指向ostream对象的引用,这使得可以将输出连接起来
◆ 该原型还表明,函数返回一个指向ostream对象的引用,这使得可以将输出连接起来
◆ ostream类还为下面的指针类型定义了插入运算符函数:const signed char *;const unsigned char *;const char *;void *。
◆ 方法使用字符串中的终止空字符来确定何时停止显示字符。
◆ 对于其他类型的指针,C++将其对应于void *,并打印地址的数值表示。如果要获得字符串的地址,则必须将其强制转换为其他类型,
◆ 函数定义指出,引用将指向用于调用该运算符的对象。
◆ 例如,cout << “potluck”返回的是cout对象。这种特性使得能够通过插入来连接输出。
◆ 这种设计技术确实是一项很好的特性,这也是前几章中重载<<运算符的示例模仿了这种技术的原因所在。
◆ cout是调用方法的对象,put( )是类成员函数。和<<运算符函数一样,该函数也返回一个指向调用对象的引用,因此可以用它将拼接输出
◆ 在原型合适的情况下,可以将数值型参数(如int)用于put( ),让函数原型自动将参数转换为正确char值
◆ cout.write( )调用返回cout对象。这是因为write( )方法返回一个指向调用它的对象的引用,这里调用它的对象是cout。
◆ 这使得可以将输出拼接起来,因为cout.write( )将被其返回值cout替换
◆ write( )方法并不会在遇到空字符时自动停止打印字符,而只是打印指定数目的字符,即使超出了字符串的边界!
◆ 因此在屏幕上,560031841将被显示为4个字符的组合,这很可能是乱码(也可能不是,请试试看)。
◆ 所以输出不会立即发送到目标地址,而是被存储在缓冲区中,直到缓冲区填满
◆ 通常,缓冲区为512字节或其整数倍。当标准输出连接的是硬盘上的文件时,缓冲可以节省大量的时间。
◆ 例如,将换行符发送到缓冲区后,将刷新缓冲区。
◆ 多数C++实现都会在输入即将发生时刷新缓冲区。
◆ 即使输出字符串中没有换行符。如果没有这种特性,程序将等待输入,而无法通过cout消息来提示用户
◆ 控制符flush刷新缓冲区,而控制符endl刷新缓冲区,并插入一个换行符。
◆ 然而,ostream类对<<插入运算符进行了重载,使得下述表达式将被替换为函数调用flush(cout)
◆ 对于char值,如果它代表的是可打印字符,则将被作为一个字符显示在宽度为一个字符的字段中。
◆ 新式:浮点类型被显示为6位,末尾的0不显示(注意,显示的数字位数与数字被存储时精度没有任何关系)
◆ 区域库(头文件locale)提供了用特定的风格影响(imbuing)输入或输出流的机制,所以同一个编译器能够提供多个区域选项。
◆ 1.200末尾的0没有显示出来,但末尾不带0的浮点值后面将有6个空格。另外,该实现将指数显示为3位,而其他实现可能为两位。
◆ ostream类是从ios类派生而来的,而后者是从ios_base类派生而来的
◆ 通过使用ios_base的成员函数,可以控制字段宽度和小数位数。由于ios_base类是ostream的间接基类,因此可以将其方法用于ostream对象(或子代),如cout。
◆ 现在,ios_base是ios的基类。在新系统中,ios是包含char和wchar_t具体化的模板,而ios_base包含了非模板特性。
◆ 例如,下面的函数调用将cout对象的计数系统格式状态设置为十六进制:
◆ ostream类重载了<<运算符,这使得上述用法与函数调用hex(cout)等价。
◆ 这是因为数字的字段宽度不相同。可以使用width成员函数将长度不同的数字放到宽度相同的字段中
◆ width( )方法只影响将显示的下一个项目,然后字段宽度将恢复为默认值。
◆ 由于width( )是成员函数,因此必须使用对象(这里为cout)来调用它。
◆ 12被放到宽度为12个字符的字段的最右边,这被称为右对齐。
◆ 显示所有的数据比保持列的整洁更重要。C++视内容重于形式
◆ 右对齐是默认的。
◆ 默认的字段宽度为0。由于C++总会增长字段,以容纳数据,因此这种值适用于所有的数据
◆ 在默认情况下,cout用空格填充字段中未被使用的部分,可以用fill( )成员函数来改变填充字符。
◆ 与字段宽度不同的是,新的填充字符将一直有效,直到更改它为止。
◆ 浮点数精度的含义取决于输出模式。在默认模式下,它指的是显示的总位数。
◆ 和width( )的情况不同,但与fill( )类似,新的精度设置将一直有效,直到被重新设置。
◆ 但ios_base类提供了一个setf( )函数(用于set标记),能够控制多种格式化特性。
◆ 2024/04/26发表想法
这不对,其实是 2.00000,五个0
原文:cout不会将2.00显示为2,而是将它显示为2.000000。
◆ cout不会将2.00显示为2,而是将它显示为2.000000。
◆ showpoint是ios_base类声明中定义的类级静态常量。类级意味着如果在成员函数定义的外面使用它,则必须在常量名前面加上作用域运算符(::)。因此ios_base::showpoint指的是在ios_base类中定义的一个常量
◆ setf( )方法控制了小数点被显示时其他几个格式选项,因此来仔细研究一下它。
◆ 打开一个标记称为设置标记(或位),并意味着相应的位被设置为1。位标记是编程开关,相当于设置DIP开关以配置计算机硬件。
◆ ex、dec和oct控制符调整控制计数系统的3个标记位。setf( )函数提供了另一种调整标记位的途径
◆ bitmask类型是一种用来存储各个位值的类型。它可以是整型、枚举,也可以是STL bitset容器。这里的主要思想是,每一位都是可以单独访问的,都有自己的含义。iostream软件包使用bitmask来存储状态信息。
◆ 由于这些格式常量都是在ios_base类中定义,因此使用它们时,必须加上作用域解析运算符。
◆ 修改将一直有效,直到被覆盖为止
◆ 仅当基数为10时才使用加号。C++将十六进制和八进制都视为无符号的,因此对它们,无需使用符号(然而,有些C++实现可能仍然会显示加号)
◆ 函数的这种重载格式用于设置由多位控制的格式选项。
◆ 具体地说,要修改基数,可以将常量ios_base::basefield用作第二参数,将ios_base ::hex用作第一参数。
◆ lm选项命令链接程序搜索数学库。同样,有些使用g++的Linux系统也要求这样做。
◆ 注意到精度3让默认的浮点显示(在这个程序中用于内部对齐)总共显示3位
◆ 如果系统支持这些控制符,请使用它们;否则,仍然可以使用setf( )。
◆ C++在头文件iomanip中提供了其他一些控制符,它们能够提供前面讨论过的服务,但表示起来更方便。3个最常用的控制符分别是setprecision( )、setfill( )和setw( ),它们分别用来设置精度、填充字符和字段宽度。
◆ 由于它们都是控制符,因此可以用cout语句连接起来。
◆ 有些C++系统不自动搜索数学库。前面说过,有些UNIX系统要求使用如下命令选项来访问数学库:
◆ 现在可以生成几乎完全对齐的列了。使用fixed控制符导致显示末尾的0。
◆ 通常情况下,通过键盘来生成这种字符流。
◆ 因此,抽取还涉及了类型转换。cin对象根据接收值的变量的类型,使用其方法将字符序列转换为所需的类型。
◆ value_holder为存储输入的内存单元,它可以是变量、引用、被解除引用的指针,也可以是类或结构的成员。cin解释输入的方式取决于value_holder的数据类型。
◆ stream类(在iostream头文件中定义)重载了抽取运算符>>
◆ 这些运算符函数被称为格式化输入函数(formatted input functions),因为它们可以将输入数据转换为目标指定的格式
◆ 参数和返回值都是引用。引用参数(参见第8章)意味着下面这样的语句将导致operator>>( )函数处理变量staff_size本身,而不是像常规参数那样处理它的副本
◆ 对于上述列出的各种类型的参数,抽取运算符将字符输入转换为指定类型的值。
◆ 顺便说一句,可以将hex、oct和dec控制符与cin一起使用,来指定将整数输入解释为十六进制、八进制还是十进制格式。
◆ 如果通过键入Liz来进行响应,则抽取运算符将把字符Liz\0放到name数组中(\0表示末尾的空值字符)。
◆ 其中,cin>>name返回的cin对象成了处理fee的对象。
◆ 不同版本的抽取运算符查看输入流的方法是相同的。它们跳过空白(空格、换行符和制表符),直到遇到非空白字符。
◆ 也就是说,它读取从非空白字符开始,到与目标类型不匹配的第一个字符之间的全部内容。
◆ Z将留在输入流中,下一个cin语句将从这里开始读取。与此同时,运算符将字符序列−123转换为一个整数值,并将它赋给elevation。
◆ 并返回0(如果istream对象的错误状态被设置,if或while语句将判定该对象为false
◆ 输入与预期格式不匹配反过来将导致表达式cin>>input的结果为false
◆ cin或cout对象包含一个描述流状态(stream state)的数据成员(从ios_base类那里继承的)
◆ 当全部3个状态位都设置为0时,说明一切顺利。
◆ 它们都重置状态,但采取的方式不同。clear()方法将状态设置为它的参数。因此,下面的调用将使用默认参数0,这将清除全部3个状态位(eofbit、badbit和failbit)
◆ 下面的调用将状态设置为eofbit;也就是说,eofbit将被设置,另外两个状态位被清除
◆ 下面的调用将设置eofbit,而不会影响其他位:
◆ setstate( )的主要用途是为输入和输出函数提供一种修改状态的途径。
◆ 在默认情况下,答案是否定的。但可以使用exceptions( )方法来控制异常如何被处理
◆ exceptions( )方法返回一个位字段,它包含3位,分别对应于eofbit、failbit和badbit。
◆ ios_base::failure异常类是从std::exception类派生而来的,因此包含一个what( )方法。
◆ exceptions( )的默认设置为goodbit,也就是说,没有引发异常。但重载的exceptions(iostate)函数使得能够控制其行为
◆ 位运算符OR(在附录E讨论)使得能够指定多位。例如,如果badbit或eofbit随后被设置,下面的语句将引发异常
◆ 然而,让这个程序在badbit位被设置时引发异常可能是合理的,因为这种情况是意外的。如果程序被设计成从一个数据文件中读取数据,直到到达文件尾,则在failbit位被设置时引发异常也是合理的,因为这表明数据文件出现了问题
◆ 只有在流状态良好(所有的位都被清除)的情况下,下面的测试才返回true
◆ 流将对后面的输入或输出关闭,直到位被清除
◆ 导致输入循环终止的不匹配输入仍留在输入队列中,程序必须跳过它
◆ 方法get(char&)和get(void)提供不跳过空白的单字符输入功能
◆ 函数get(char*, int, char)和getline(char*, int, char)在默认情况下读取整行而不是一个单词
◆ 它们被称为非格式化输入函数(unformatted input functions),因为它们只是读取字符输入,而不会跳过空白,也不进行数据转换
◆ get( )方法读取下一个输入字符,即使该字符是空格、制表符或换行符。get(char & ch)版本将输入字符赋给其参数
◆ 这里的重点是,通过使用get(ch),代码读取、显示并考虑空格和可打印字符。
◆ 由于抽取运算符跳过了换行符,因此代码不会将换行符赋给ch,所以while循环测试将不会终止循环。
◆ get( )和getline( )之间的主要区别在于,get( )将换行符留在输入流中,这样接下来的输入操作首先看到是将是换行符,而gerline( )抽取并丢弃输入流中的换行符。
第18章 探讨C++新标准
◆ 2024/05/07发表想法
到了激动人心的有趣的章节~
原文:本章首先复习前面介绍过的C++11功能,然后介绍如下主题:移动语义和右值引用。Lambda表达式。包装器模板function。可变参数模板。
◆ 本章首先复习前面介绍过的C++11功能,然后介绍如下主题:移动语义和右值引用。Lambda表达式。包装器模板function。可变参数模板。
◆ C++11扩大了用大括号括起的列表(初始化列表)的适用范围
◆ 然而,如果类有将模板std::initializer_list作为参数的构造函数,则只有该构造函数可以使用列表初始化形式
◆ 初始化列表语法可防止缩窄,即禁止将数值赋给无法存储它的数值变量。
◆ 然而,如果使用初始化列表语法,编译器将禁止进行这样的类型转换
◆ 但允许转换为更宽的类型
◆ 头文件initializer_list提供了对模板类initializer_list的支持。这个类包含成员函数begin( )和end( ),可用于获悉列表的范围。
◆ C++11将其用于实现自动类型推断(见第3章)。这要求进行显式初始化,让编译器能够将变量的类型设置为初始值的类型
◆ 关键字auto还可简化模板声明
◆ 关键字decltype将变量的类型声明为表达式指定的类型。下面的语句的含义是,让y的类型与x相同,其中x是一个表达式
◆ 这在定义模板时特别有用,因为只有等到模板被实例化时才能确定类型:
◆ C++11新增了一种函数声明语法:在函数名和参数列表后面(而不是前面)指定返回类型
◆ 这里解决的问题是,在编译器遇到eff的参数列表前,T和U还不在作用域内,因此必须在参数列表后使用decltype。这种新语法使得能够这样做。
◆ 差别在于,新语法也可用于模板部分具体化,但typedef不能:
◆ 正如第12章讨论的,C++11新增了关键字nullptr,用于表示空指针
◆ 例如,可将0传递给接受int参数的函数,但如果您试图将nullptr传递给这样的函数,编译器将此视为错误。
◆ C++11摒弃了auto_ptr,并新增了三种智能指针:unique_ptr、shared_ptr和weak_ptr
◆ 可能
◆ 然而,标准委员会认为,指出函数不会引发异常有一定的价值,他们为此添加了关键字noexcept
◆ 为解决这些问题,C++11新增了一种枚举。这种枚举使用class或struct定义
◆ 但随着编程经验的积累,程序员逐渐认识到,自动类型转换可能导致意外转换的问题。为解决这种问题,C++引入了关键字explicit,以禁止单参数构造函数导致的自动转换
◆ C++11拓展了explicit的这种用法,使得可对转换函数做类似的处理
◆ 很多首次使用C++的用户都会问,为何不能在类定义中初始化成员?现在可以这样做了,其语法类似于下面这样
◆ 如果构造函数在成员初始化列表中提供了相应的值,这些默认值将被覆盖,因此第三个构造函数覆盖了类内成员初始化。
◆ 本章前面提到了模板别名和适用于STL的智能指针。
◆ 基于范围的for循环
◆ 基于范围的for循环(第5章和第16章讨论过)可简化为它们编写循环的工作。这种循环对数组或容器中的每个元素执行指定的操作
◆ 一种更容易、更安全的方式是,使用auto来声明x,这样编译器将根据prices声明中的信息来推断x的类型
◆ 如果要在循环中修改数组或容器的每个元素,可使用引用类型
◆ 容器forward_list是一种单向链表,只能沿一个方向遍历;与双向链接的list容器相比,它更简单,在占用存储空间方面更经济。其他四种容器都是使用哈希表实现的。
◆ C++11还新增了模板array(这在第4和16章讨论过)。要实例化这种模板,可指定元素类型和固定的元素数
◆ 但array确实有方法begin( )和end( ),这让您能够对array对象使用众多基于范围的STL算法。
◆ 用于指定包含全部元素的区间。另外,这些新方法将元素视为const。
◆ 除传统的复制构造函数和常规赋值运算符外,STL容器现在还有移动构造函数和移动赋值运算符。
◆ C++11添加了两个函数(begin( )和end( )),它们都接受valarray作为参数,并返回迭代器,这些迭代器分别指向valarray对象的第一个元素和最后一个元素后面。这让您能够将基于范围的STL算法用于valarray(参见第16章)。
◆ 为避免与运算符>>混淆,C++要求在声明嵌套模板时使用空格将尖括号分开
◆ C++11不再这样要求:
◆ 右值引用可关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值
◆ r2关联到的是当时计算x + y得到的结果。也就是说,r2关联到的是23,即使以后修改了x或y,也不会影响到r2。
◆ 有趣的是,将右值关联到右值引用导致该右值被存储到特定的位置,且可以获取该位置的地址。也就是说,虽然不能将运算符&用于13,但可将其用于r1。通过将数据与特定的地址关联,使得可以通过右值引用来访问该数据。
◆ 引入右值引用的主要目的之一是实现移动语义,这是本章将讨论的下一个主题。
◆ C++11支持移动语义,这就提出了一些问题:什么是移动语义?C++11如何支持它?为何需要移动语义?
◆ 假设有一个函数,它返回一个vector对象
◆ 考虑到临时对象被删除了,如果编译器将对数据的所有权直接转让给vstr_copy2,不是更好吗?
◆ 这种方法被称为移动语义(move semantics)。有点悖论的是,移动语义实际上避免了移动原始数据,而只是修改了记录
◆ 它使用const左值引用作为参数,这个引用关联到左值实参,如语句#1中的vstr;另一个是移动构造函数,它使用右值引用作为参数,该引用关联到右值实参,如语句#2中allcaps(vstr)的返回值。
◆ 在将所有权转移给新对象的过程中,移动构造函数可能修改其实参,这意味着右值引用参数不应是const。
◆ 该构造函数随后将原来的指针设置为空指针,因为对空指针执行delete [ ]没有问题。这种夺取所有权的方式常被称为窃取(pilfering)
◆ 注意,由于修改了f对象,这要求不能在参数声明中使用const。
◆ 之所以知道这是临时对象,是因为其元素数和数据地址都是0。
◆ 虽然使用右值引用可支持移动语义,但这并不会神奇地发生。要让移动语义发生,需要两个步骤。首先,右值引用让编译器知道何时可使用移动语义
◆ 对象one是左值,与左值引用匹配,而表达式one + three是右值,与右值引用匹配。因此,右值引用让编译器使用移动构造函数来初始化对象four。实现移动语义的第二步是,编写移动构造函数,使其提供所需的行为。
◆ 总之,通过提供一个使用左值引用的构造函数和一个使用右值引用的构造函数,将初始化分成了两组。使用左值对象初始化对象时,将使用复制构造函数,而使用右值对象初始化对象时,将使用移动构造函数。程序员可根据需要赋予这些构造函数不同的行为。
◆ 在C++98中,下面的语句将调用复制构造函数
◆ 例如,下面演示了如何给Useless类编写复制赋值运算符和移动赋值运算符
◆ 与移动构造函数一样,移动赋值运算符的参数也不能是const引用,因为这个方法修改了源对象。
◆ 移动构造函数和移动赋值运算符使用右值。如果要让它们使用左值,该如何办呢
◆ 如果可以使用移动构造函数或移动赋值运算符来保留选定的对象,那该多好啊
◆ 为此,可使用运算符static_cast<>将对象的类型强制转换为Useless &&,但C++11提供了一种更简单的方式—使用头文件utility中声明的函数std::move( )
◆ 将one赋给three调用了复制赋值运算符,但将move(one)赋给four调用的是移动赋值运算符。
◆ 对大多数程序员来说,右值引用带来的主要好处并非是让他们能够编写使用右值引用的代码,而是能够使用利用右值引用实现移动语义的库代码
◆ STL类现在都有复制构造函数、移动构造函数、复制赋值运算符和移动赋值运算符。
◆ 在原有4个特殊成员函数(默认构造函数、复制构造函数、复制赋值运算符和析构函数)的基础上,C++11新增了两个:移动构造函数和移动赋值运算符
◆ 有一些例外。如果您提供了析构函数、复制构造函数或复制赋值运算符,编译器将不会自动提供移动构造函数和移动赋值运算符;如果您提供了移动构造函数或移动赋值运算符,编译器将不会自动提供复制构造函数和复制赋值运算符。
◆ 您提供了移动构造函数,因此编译器不会自动创建默认的构造函数、复制构造函数和复制赋值构造函数。在这些情况下,您可使用关键字default显式地声明这些方法的默认版本:
◆ 编译器将创建在您没有提供移动构造函数的情况下将自动提供的构造函数。
◆ 关键字delete可用于禁止编译器使用特定方法。例如,要禁止复制对象,可禁用复制构造函数和复制赋值运算符
◆ 可将复制构造函数和赋值运算符放在类定义的private部分,但使用delete也能达到这个目的,且更不容易犯错、更容易理解。
◆ 关键字default只能用于6个特殊成员函数,但delete可用于任何成员函数。delete的一种可能用法是禁止特定的转换。
◆ C++11允许您在一个构造函数的定义中使用另一个构造函数。这被称为委托,因为构造函数暂时将创建对象的工作委托给另一个构造函数。
◆ 上述默认构造函数使用第一个构造函数初始化数据成员并执行其函数体,然后再执行自己的函数体。
◆ 这让函数fn的所有重载版本都可用。也可使用这种方法让基类的所有非特殊成员函数对派生类可用。
◆ C2中的using声明让C2对象可使用C1的三个fn( ) 方法,但将选择C2而不是C1定义的方法fn(double)。
◆ C++11将这种方法用于构造函数。这让派生类继承基类的所有构造函数(默认构造函数、复制构造函数和移动构造函数除外),但不会使用与派生类构造函数的特征标匹配的构造函数
◆ 请注意,继承的基类构造函数只初始化基类成员;如果还要初始化派生类成员,则应使用成员列表初始化语法:
◆ 假设基类声明了一个虚方法,而您决定在派生类中提供不同的版本,这将覆盖旧版本。
◆ 在C++11中,可使用虚说明符override指出您要覆盖一个虚函数:将其放在参数列表后面。
◆ 说明符override和final并非关键字,而是具有特殊含义的标识符。这意味着编译器根据上下文确定它们是否有特殊含义;在其他上下文中,可将它们用作常规标识符,如变量名或枚举。
◆ 但lambda函数并不像看起来那么晦涩难懂,它们提供了一种有用的服务,对使用函数谓词的STL算法来说尤其如此。
◆ 2024/05/08发表想法
函数指针会自动转化为一个函数对象
原文:它使用三种方法给STL算法传递信息:函数指针、函数符和lambda
◆ 它使用三种方法给STL算法传递信息:函数指针、函数符和lambda
◆ 并将每个元素设置为第三个参数返回的值,而第三个参数是一个不接受任何参数的函数对象
◆ 函数符是一个类对象,并非只能像函数名那样使用它,这要归功于类方法operator( ) ( )
◆ 2024/05/08发表想法
使用默认构造函数,实例化了一个匿名对象
原文:参数f_mod(3)创建一个对象,它存储了值3
◆ 参数f_mod(3)创建一个对象,它存储了值3
◆ 名称lambda来自lambda calculus(λ演算)—一种定义和应用函数的数学系统。这个系统让您能够使用匿名函数—即无需给函数命名
◆ 在C++11中,对于接受函数指针或函数符的函数,可使用匿名函数定义(lambda)作为其参数
◆ 如果lambda不包含返回语句,推断出的返回类型将为void
◆ 仅当lambad表达式完全由一条返回语句组成时,自动类型推断才管用;否则,需要使用新增的返回类型后置语法
◆ 距离、简洁、效率和功能。
◆ lambda是理想的选择,因为其定义和使用是在同一个地方进行的
◆ 因为不能在函数内部定义其他函数,因此函数的定义可能离使用它的地方很远
◆ 但并非必须编写lambda两次,而可给lambda指定一个名称,并使用该名称两次
◆ 您甚至可以像使用常规函数那样使用有名称的lambda
◆ 2024/05/08发表想法
lambda 表达式会被转化为一个类的对象,换句话说它实际上是一个函数对象(函数符),并且重载了()符。
原文:您甚至可以像使用常规函数那样使用有名称的lambda
◆ 函数指针方法阻止了内联,因为编译器传统上不会内联其地址被获取的函数,因为函数地址的概念意味着非内联函数。
◆ 而函数符和lambda通常不会阻止内联。
◆ [=, &ed]让您能够按引用访问ed以及按值访问其他所有动态变量。
◆ 由于count13是按引用捕获的,因此在lambda对count13所做的任何修改都将影响原始count13。
◆ 在这里,[&]让您能够在lambad表达式中使用所有的自动变量,包括count3和count13。
◆ 典型的lambda是测试表达式或比较表达式,可编写为一条返回语句。这使得lambda简洁而易于理解,且可自动推断返回类型。
◆ C++提供了多个包装器(wrapper,也叫适配器[adapter])
◆ 好像F的类型都相同
◆ 其中的dub是一个函数的名称,该函数接受一个double参数并返回一个double值。函数名是指针,因此参数F的类型为double(*) (double):一个指向这样的函数的指针,即它接受一个double参数并返回一个double值。
◆ 因为将为这些F值实例化use_f( )模板两次
◆ 注意到程序清单18.7中的函数指针、函数对象和lambda表达式有一个相同的地方,它们都接受一个double参数并返回一个double值。可以说它们的调用特征标(call signature)相同。
◆ 它从调用特征标的角度定义了一个对象,可用于包装调用特征标相同的函数指针、函数对象或lambda表达式
◆ 下面的声明创建一个名为fdci的function对象,它接受一个char参数和一个int参数,并返回一个double值
◆ 所有可调用参数的调用特征标都相同:double (double)
◆ 从上述输出可知,count的地址都相同,而count的值表明,use_f( ) 被调用了6次。这表明只有一个实例,并调用了该实例6次,这缩小了可执行代码的规模
◆ 可变参数模板(variadic template)让您能够创建这样的模板函数和模板类,即可接受可变数量的参数。
◆ 模板参数包(parameter pack);函数参数包;展开(unpack)参数包;递归。
◆ C++11提供了一个用省略号表示的元运算符(meta-operator),让您能够声明表示模板参数包的标识符,模板参数包基本上是一个类型列表
◆ 而函数参数包基本上是一个值列表
◆ 。Args和T的差别在于,T与一种类型匹配,而Args与任意数量(包括零)的类型匹配。请看下面的函数调用
◆ 更准确地说,这意味着函数参数包args包含的值列表与模板参数包Args包含的类型列表匹配—无论是类型还是数量
◆ 相反,可将省略号放在函数参数包名的右边,将参数包展开
◆ 不幸的是,该函数调用与原始函数调用相同,因此它将使用相同的参数不断调用自己,导致无限递归
◆ 直到列表为空为止。
◆ 这样,当args包缩短到只有一项时,将调用这个版本,而它打印换行符而不是逗号。另外,由于没有递归调用show_list3( ),它也将终止递归。
◆ 在可变参数模板中,可指定展开模式(pattern)
◆ 2024/05/08发表想法
可以指定展开模式,例如指针,引用等等
原文:在可变参数模板中,可指定展开模式(pattern)