现在的位置: 首页 > 综合 > 正文

C++ Primer学习系列(5):复制控制/重载操作与转换/面向对象编程

2013年12月06日 ⁄ 综合 ⁄ 共 7428字 ⁄ 字号 评论关闭



13

复制控制

复制控制:复制构造函数、赋值操作、析构函数不显式给出时,编译器会自动定义

定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用复制构造函数;将该类型的对象传递给函数或从函数返回该类型的对象时,隐式使用复制构造函数

定义两个对象,将一个对象赋值给另一个对象时使用赋值操作符

对象超出作用域或动态分配的对象被删除时,使用析构函数

不管类是否定义了自己的析构函数,编译器都自动执行类中非
static数据成员的析构函数

赋值操作符:通过指定不同类型的右操作数而重载

有一种特别常见的情况需要定义自己的复制控制函数:类具有指针成员

13.1

复制构造函数:只有单个类类型的引用形参,且该形参是对本类类型对象的引用(常用
const修饰)

直接初始化(放在圆括号中)直接调用与实参匹配的构造函数,复制初始化(用
=号)总是调用复制构造函数

当非引用形参或返回值为类类型时,由复制构造函数进行复制

最佳实践:除非想使用容器元素的默认初始值,更有效的方法是分配一个空容器,然后将已知元素的值加入容器

合成的复制构造函数:由编译器自动提供。对每个非
static成员,执行逐个成员初始化(相当于每个成员在初始化列表中初始化),将新对象初始化原对象的副本。类类型成员使用其复制构造函数进行复制,内置类型成员直接复制其值

复制构造函数:当有指针成员(有成员表示在构造函数中分配的其他资源)或者在复制对象时有一些特定工作要做时,需要自己定义复制构造函数。它与类同名,没有返回值,可以使用初始化列表;
只有单个类类型的形参,且该形参必须是对本类类型对象的引用(常用

const修饰,也可不用)

如果要禁止类对象的复制,类必须显式声明其复制构造函数为
private
I/O
istream,ostream等就是这种情况。这样类的友元和成员函数仍可进行复制,要禁止这个,可以声明一个
private复制构造函数但不对其定义

不允许复制的类对象只能作为引用传递给函数或从函数返回,它们不能用作容器的元素

最佳实践:最好显式或隐式定义默认构造函数和复制构造函数。如果定义了复制构造函数,也必须定义默认构造函数(因为编译器不会再合成了)

13.2

操作符函数:可定义为成员函数或非成员函数,定义为成员函数时,第一个操作数隐式绑定到
this指针

合成的赋值操作符:由编译器自动提供,执行逐个成员赋值

一般而言,如果类需要复制构造函数,它也会需要赋值操作符,反之亦然

13.3

析构函数:用于撤销对象,完成资源回收,当然也可以做其他用户定义的操作。没有返回值,没有形参,因此不能重载析构函数,一个类只能有一个析构函数

动态分配的对象只有其指针用
delete删除后,才会运行该对象的析构函数以撤销对象,否则就不会运行析构函数,对象就一直存在,导致内存泄漏。

当对象的引用或指针超出作用域时,不会运行析构函数。注意只有两种情况下会导致析构函数被调用:用
delete删除指向动态分配
(new)对象的指针时;实际对象(而不是对象的引用)超出作用域时

注意:

撤销一个容器(不管是标准库容器还是内置数组)时,也会运行容器中的类类型元素的析构函数,容器中的元素总是按逆序撤销(因为它们按正序创建)

何时编写显式析构函数:要释放在构造函数或在对象生命期内获取的资源

三法则:如果类需要析构函数,则它也需要赋值操作符和复制构造函数。也即需要其中一个时,三个都需要。

合成析构函数:编译总是会合成一个析构函数。它按成员在类中声明次序的逆序撤销每个非
static成员,对类类型成员,它调用该成员的析构函数来撤销对象

析构函数与复制构造函数或赋值操作符的区别:即使我们编写了自己的析构函数,合成析构函数仍然运行,类在定义的析构函数结束之后运行合成的析构函数

编写自己的复制构造函数时,必须显式复制需要复制的任意成员。显式定义的复制构造函数不会进行任何自动复制

赋值操作符通常要做复制构造函数和析构函数也要完成的工作。在这种情况下,通用工作应放在
private实用函数中

13.5

定义智能指针类:若类
A需要指针成员,可定义一个智能指针类
B来封装这些指针成员和使用计数,并有一个与之相关联的
A类友元,构造函数复制指针成员并初始化使用计数,析构函数删除它,
B中所有这些成员均为
private。这样
A类只要有一个指向
B类对象的指针成员即可,由
B类来管理
A的实际指针成员的使用。
A的复制构造函数从形参复制成员并增加
B对象成员中的使用计数,析构函数检查
B对象成员中的使用计数,若为
0则删除
B对象成员。

A的赋值操作符:要增加右操作数中的使用计数,减少左操作数中的使用计数并检查,若为
0则删除其中的
B对象成员

定义值型类:复制构造函数不再复制指针成员,而是分配一块新的空间,把被复制对象的成员中的值复制到这块空间中,这样每个对象只保存自己的成员副本。而析构函数将无条件删除指针成员。

总结:

复制构造函数、赋值操作符函数、析构函数、定义智能指针类、定义值型类

 



14

重载操作与转换

14.1

不能重载的操作符:作用域
::
点星号

.*, 点号
.,
条件操作符
?:
4个,其余的操作符均能重载

重载操作符必须具有至少一个类类型或枚举类型的操作数;操作符的优先级和结合性是固定的;不再具备短路求值特性

除了函数调用操作符
operator()之外,重载操作符时使用默认实参是非法的

重载操作符可以是类成员函数,也可以是非成员的普通函数

一般将算术和关系操作符定义为非成员函数,而将赋值操作符定义为成员函数

操作符定义为非成员函数时,通常必须将它们设置为所操作类的友元

重载操作符的设计:


1)不要重载具有内置含义的操作符:如逗号、取地址、逻辑与、逻辑或等具有内置的含义,如果定义了自己的版本,就不能再使用内置含义了


2)为类设计操作符,最好先设计类的公用接口,之后再考虑将哪些操作符定义为重载操作符


3)如果类有算术操作符或位操作符,则最好也提供相应的复合赋值操作符


4)用作关联容器键类型的类应定义小于操作符
<。一般地,类通常要定义相等操作符
==和小于操作符
<,因为很多算法需要类定义这些操作符,如
sort使用
<操作符,
find使用
==操作符。类也应该定义不等操作符
!=,全部四个关系操作符


5)赋值
=、下标
[]、调用
()、成员访问箭头
->等必须定义为成员函数;对称的操作符如算术、关系、位操作符等最好定义为普通非成员函数

14.2

一般而言,输出操作符应输出对象的内容,进行最小限度的格式化,不应该输出换行符;一般不对对象内容进行检查(如越界,错误处理等),只是直接输出内容即可。对象内容的检查由对象的其他函数来完成

为与标准
IO库一致,
IO操作符必须为非成员函数,类将
IO操作符设为友元

输出操作符
>>重载:与输出操作符
<<不同的是,它必须处理错误和文件结束的可能性,并要确定错误恢复措施,如将对象复位(创建空对象)

14.3

为了与内置操作符保持一致,加法操作符返回一个右值,而不是一个引用

算术操作符通常产生一个新值,该值是两个操作数的计算结果

14.4

赋值操作符函数可以重载为多个版本;赋值最好返回对
*this的引用

一般而言,赋值操作符与复合赋值操作符应返回左操作数的引用

14.5

定义下标操作符时,一般需要定义两个版本:一个为非
const成员并返回引用,另一个为
const成员并返回
const引用(这时不能作用赋值的目标)。返回非
const引用时就可得到左值,可用来赋值

14.6

解引用操作符
*和箭头操作符
->常用在实现智能指针的类中,重载时都没有显式形参,并且通常要提供
const和非
const两个版本

重载箭头操作符必须返回指向类类型的指针,或者返回定义了箭头操作符的类类型对象

14.7

C++不要求自增或自减操作符一定作为类的成员,但是因为这些操作符改变操作对象的状态,所以更倾向于将它们作为成员

为了与内置类型一致,前缀式自增自减操作符返回被增量或减量对象的引用。前缀式没有显式形参,为了与前缀式区别,后缀式有一个无用的
int型形参,编译器提供
0作为其默认实参。后缀式应返回旧值,且应作为值返回,而不是返回引用

14.8

可以为类类型的对象重载函数调用操作符,它必须声明为成员函数,可重载为多个版本

函数对象:定义了调用操作符的类。其对象是行为类似于函数的对象

标准库定义的函数对象类:在
<functional>

算术函数对象类型:
plus<Type>,
minus<Type>, multiplies<Type>, divides<Type>,
modulus<Type>, negate<Type>

关系函数对象类型:
equal_to<Type>,
not_equal_to<Type>, greater<Type>, greater_equal<Type>,
less<Type>, less_equal<Type>,

逻辑函数对象类型:
logical_or<Type>,
logical_and<Type>, logical_not<Type>

函数对象的函数适配器:绑定器
bind1st,bind2nd;求反器
not1,not2

14.9

转换操作符:
operator
type();是类成员函数,将其类类型值转换成由
type指定的类型值。
type表示内置类型名、类类型名或由类型别名所定义的名字。对任何可作为函数返回类型(除了
void)都可以定义转换函数。

转换函数形参必须为空,不能指定返回类型,但是必须显示返回一个
type类型的值
,通常是
const成员函数

一般而言,给出一个类与两个内置类型之间的转换是不好的做法。

如果两个转换操作符都可用在一个调用中,而且在转换函数之后存在标准转换,则根据该标准转换的类别选择最佳匹配

避免二义性最好的方法是避免编写互相提供隐式转换的成对的类

当一个函数既可以用标准转换,又可以用类类型转换时,标准转换优于类类型转换

类类型转换:转换操作符定义的类类型到操作符所指定类型的转换,单形参的非
explicit构造函数定义的形参类型到类类型的转换

既为算术类型提供转换函数,又为同一类类型提供重载操作符,可能会导致重载操作符和内置操作符之间的二义性

总结:

重载操作符(难点赋值
=,输入
>>和输出
<<,下标
[],解引用
*,成员访问
->,自增
++和自减
--,调用
()操作符)、函数对象、函数适配器(
bind1st,bind2nd,not1,not2)、转换操作符、类类型转换与标准转换

 



15

面向对象编程

15.1

面向对象的三个基本概念:数据抽象、继承、动态绑定

15.2

基类希望派生类重新定义的函数要定义为
virtual函数,基类期望派生类继承的函数不能定义为虚函数。对非虚函数的调用在编译时确定;对虚函数的调用在运行时确定

重新定义原型必须一样,但返回类型可以是基类版本中返回类型的子类

通过其类的引用(或指针)调用虚函数时,发生动态绑定。引用或指针既可以指向基类对象也可以指向派生类对象,被调用的虚函数在运行时确定,是运行时的实际对象类型定义的函数

继承层次的根类一般都要定义虚析构函数;除构造函数外,任意非
static成员函数都可以是虚函数。派生类也可不重新定义所继承的虚函数,这时使用基类中定义的版本。

发生动态绑定的两个条件:成员函数为虚函数;通过基类类型的引用或指针进行函数调用

如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数(即使运行时是子类且定义了同名函数)。如果调用虚函数,则直到运行时才能确定调用哪个函数

覆盖虚函数机制:要强制调用基类中虚函数(不管运行时是子类还基类对象),可使用作用域操作符,以覆盖虚函数机制

只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制

派生类虚函数调用基类版本时,必须显式使用作用域操作符。否则运行时如果是派生类对象,会导致无穷递归的调用

通过基类的引用或指针调用虚函数时,默认实参是在基类虚函数声明中指定的值。通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中的值。这主要是因为默认实参值是编译期确定的。

接口继承:
public继承

实现继承:
private继承和
protected继承

使用
protected
private派生的类可以恢复继承成员原有的访问级别,方法是在派生类的
public部分增加一个继承成员
using声明

如果派生时没有指定派生级别,则
class默认为
private继承,
struct默认为
public继承,因此在派生类时,我们常用
public继承,要加上
public关键字

记住:友元关系不能继承。这很好理解,我跟你是朋友时,我跟你的儿子、你跟我的儿子、我的儿子跟你的儿子都不一定是朋友

如果基类定义了
static成员,则整个继承层次中只有一个这样的成员。派生类不能访问基类中
private
static成员

15.3

派生类对象包括一个基类部分的子对象

派生类的引用或指针可以自动转换成基类的引用或指针,对对象则没有类似转换,但可以用派生类对象对基类对象进行赋值或初始化

用派生类对象对基类对象进行初始化或赋值:只用派生类对象中的基类部分来初始化或赋值,结果基类对象中只包含基类中定义的成员,不包含由任意派生类型定义的成员

不存在基类到派生类的转换,甚至对指针或引用也不行

Bulk_item
bulk;

Item_base
*itemP=&bulk;

Bulk_item
*bulkP=itemP; //error:不能转换

这种情况,如果知道转换是安全的,可使用
static_cast强制编译器进行转换,或者可用
dynamic_cast在运行时进行检查

15.4

当构造、复制、赋值和撤销派生类对象时,也会构造、复制、赋值和撤销其基类子对象

派生类的合成默认构造函数:先初始化基类部分的成员(调用基类的默认构造函数),再初始化自己的数据成员。如果基类没有默认构造函数,则派生类必须在初始化列表中向基类构造函数传递实参来初始化基类部分的成员

向基类构造函数传递实参:派生类构造函数通过将基类包含在构造函数初始化列表中来间接初始化继承成员

一个类只能直接初始化自己的直接基类

如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分。对赋值操作符也类似

析构函数则不同:派生类析构函数不负责撤销基类对象的成员,编译器会显式调用派生类对象基类部分的析构函数

如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。因此基类中的析构函数必须定义为虚函数,这样通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同

基类的虚析构函数具有继承性:这时派生类析构函数在任何情况下都将是虚的(无论派生类显式定义析构函数还是使用合成析构函数)

最佳实践:即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数

如果在构造函数或析构函数中调用虚函数,则运行的是构造函数或析构函数自身类型定义的版本

在继承情况下,派生类的作用域嵌套在基类作用域中。正因为这样,我们才能在派生类中直接访问基类的成员

用基类指针或引用指向派生类对象时,只能访问对象的基类部分,不能访问对象自己定义的特有部分。因为这时编译是在基类中查找函数,若找到,就检查实参是否与形参匹配。这也是为什么虚函数必须在基类和派生类中拥有同一原型(返回类型可以不同),否则就没办法通过动态绑定调用派生类函数(即在基类中找原型,执行的是派生类中的代码,因为内存中只含有派生类版本的代码)。如果基类的虚函数在派生类中没有重定义,则会调用基类中的版本(因为派生类对象没有相应代码)

隐藏:与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。可用使用作用域操作符访问被屏蔽成员

对于基类和派生类中同名的成员函数,即使原型不同,基类成员也会被屏蔽,被屏蔽的成员可以使用在成员名前加使用域操作符的方式访问

关键:编译器在作用域中查找名字,一旦找到就不再继续往上找了

局部作用域中声明的函数不会重载全局作用域中字义的函数。同样,派生类中定义的函数也不重载基类中定义的成员。通过派生类对象调用函数时,实参必须与派生类中定义的版本相匹配,只有在派生类根本没有定义该函数时,才考虑基类函数

抽象基类:含有(或继承)一个或多个纯虚函数的类是抽象基类,不能用它来创建对象

15.7

对基类类型的容器,添加派生类型的对象时,只将对象的基类部分保存在容器中。记住,将派生类复制到基类对象时,派生类对象将被切掉。因此比较可行的办法是使用容器保存对象的指针

15.8

句柄类:存储和管理基类的指针,这些指针可以指向基类对象也可以指向派生类对象,用户通过句柄类访问继承层次的操作。它类似于智能指针或值型类。

指针型句柄:类似于智能指针。数据成员有关联对象的指针、使用计数的指针,操作有有默认构造函数、复制构造函数、接受关联对象的构造函数(形参是引用或指针)、赋值操作符、解引用操作符
*、箭头操作符
->

对于接受关联对象的构造函数,由于有动态绑定,句柄类需要在不知道关联对象的确切类型时分配其新副本,故关联类需要从基类开始,在继承层次的每个类型中增加
clone操作(用动态分配的方式构造本类的一个副本并返回)。基类中
clone定义为虚函数。这样在接受关联对象的那个构造函数初始化列表中,可以在形参上调用
clone函数来初始化句柄类的成员

关联容器有自己的默认比较函数,但我们也可提供一个比较函数(或函数对象)覆盖它。如对
multimap<K,V>,可把
V指定为比较函数指针(是一个复合类型),这个比较函数接受两个
key_type类型(即
K类型)的对象并返回
bool

typedef
bool (*Comp)(const Sales_item&,const Sales_item&);

multiset<Sales_item,Comp>
items(compare); //容器将用比较器
compare函数来比较

//Sale_item对象

值型句柄:类似于值型类。保存继承层次中一个类型对象的指针及其使用计数,并对其进行管理

总结:

面向对象编程概念(数据抽象,继承,动态绑定)、虚函数、派生类、虚函数的动态绑定、继承级别(公有,受保护,私有)、友元关系与继承、静态成员与继承、基类与派生类之间的转换、派生类构造函数(初始化基类部分,派生类部分)及复制控制、虚析构函数、继承情况下的作用域、纯虚函数、容器与继承、句柄类与继承

 

【上篇】
【下篇】

抱歉!评论已关闭.