第十章 对象和类

面向对象编程是一种特殊的、设计程序的概念性方法,C++通过一些特性改进了C语言,下边是最重要的OOP特性:

  • 抽象
  • 封装和数据隐藏
  • 多态
  • 继承
  • 代码的可重用性

采用OOP方法时,首先从用户的角度考虑对象,描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。

抽象和类

对于基本类型,编译器需要维护如下内容:

  • 决定数据对象需要的内存数量
  • 决定如何解释内存中的位
  • 决定可使用数据对象执行的操作或方法

对于C++中自定义的类,我们需要自己提供这些信息。

类中可以声明public和private及protected的成员。类设计尽可能的将公有接口与实现细节分开。公有接口表示设计的抽象组件,将实现细节放在一起并将它们与抽象分开被称为封装。数据隐藏(将数据放在类的私有部分中)是一种封装,将实现的细节隐藏在私有部分中,也是封装,封装的另一个例子是将类定义和类声明放在不同的文件中。

数据隐藏可以防止直接访问数据,还让开发者无需了解数据是如何被表示的。这样,只要接口不变,我们就可以对程序的细节进行修改。

数据项一般来说是私有对象,接口成员一般放在公共部分。对于实现的细节相关的函数,我们可以作为私有成员函数。对于类来说,成员默认是私有的。一般我们显式的指出private。

C++对于C的结构进行了扩充,与类的功能类似,唯一的区别是结构的默认访问方式是公有的。

对于类成员函数的定义,与普通函数有两点不同:

  • 定义成员函数时,使用作用域解析运算符来标识函数所属的类
  • 类方法可以访问类的private组件

对于类中的函数或成员,我们说这些标识符具有类作用域。

定义位于类声明中的函数都将自动成为内联函数。除此之外还可以在类函数声明中使用inline。

内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义。确保内联定义对多文件程序中的所有文件都可用。最简便的方法是:将内联定义放在定义类的头文件中。顺便说一句,根据改写规则,在类声明中定义方法,等同于用原型替换方法定义,然后在类声明的后边将定义改写为内联函数。

调用成员函数时,它将使用被用来调用它的对象的数据成员。

所创建的每个新对象都有自己的存储空间,用于存储其他内部变量和类成员。但同一个类的所有对象共享同一组类方法,即每种方法只有一个副本。

指定类设计的第一步是提供类声明。类声明类似结构声明,可以包含数据成员和函数成员。声明有私有部分,在其中声明的成员只能通过成员函数进行访问;声明还具有公有部分,在其中声明的成员可被使用类对象的程序直接访问,通常数据成员函数被放在私有部分中。

第二步是实现类成员函数。

类的构造函数和析构函数

构造函数

构造函数没有声明类型,其名字和类的名字一样。
有两种方式使用构造函数,显式和隐式的调用,如下:

1
2
Student s = Student("name");
Student t("name");

每次创建类对象,C++都使用构造函数。构造函数的使用不同于其他方法。

默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。如果没有提供任何构造函数,C++将提供默认构造函数,它是默认构造函数的显式版本,不做任何工作。我们可以通过显式的方式重载一个构造函数。

析构函数

对象过期时,程序将自动调用一个特殊的成员函数——析构函数。析构函数完成清理工作。析构函数的名字是在类名前面加上~。析构函数没有参数。如果没有提供析构函数,编译器将隐式的声明一个默认析构函数。

C++11的列表初始化可以用来初始化类。

有一种成员函数叫做const成员函数,这种函数不会修改调用对象,我们将const关键字放在函数括号的后边来实现这种函数。

1
2
3
4
5
6
void show() const;

void show() const
{

}

只要类方法不修改调用对象,就应将其声明为const

初始化对象有三种方式

1
2
3
Student a = Student("name");
Student b = Student("name");
Student c = "name";

接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值。这种特性可能会导致问题,我们可以关闭这种特性。

this指针

每个成员函数都包含一个this指针。this指针指向用来调用成员函数的对象,其作为隐藏参数传递给方法。const成员方法中的this不能用来修改调用对象的值。

对象数组

我们可以声明一个对象数组。并对其进行初始化,如果不进行初始化,则使用默认构造函数进行初始化。还可以使用初始化列表对对象数组进行初始化。

初始化对象数组的方案是首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中,这就说明要想创建对象数组,这个对象必须有默认构造函数。

类作用域

类作用域是c++特有的。其不同于全局作用域和局部作用域。在类中定义的名称的作用域都为类,作用域为整个类的名称只在类中是已知的,在类外不可知。不能从外部直接访问类的成员(必须通过类对象)。

使用类成员名时,必须根据上下文使用直接成员运算符(.),间接成员运算符(->)或作用域解析运算符。

作用域为类的常量

有两种方式,一种是在类内声明一个枚举。在类声明中声明的枚举的作用域为整个类,可以用枚举为整型常量提供作用域为整个类的符号名称。这种方式声明枚举并不会创建类数据成员。这种方式不比创建枚举类型的变量,因此不需要提供枚举名。

另一种方式是使用static关键字。这创建一个常量,其与其他静态变量存储在一起,而不是存储在对象中。如果不是const的方式,需要在类外进行初始化。在C++ 98中只能使用这种技术声明值为整数或枚举的静态常量,而不能存储double常量,C++ 11取消了这种限制。

1
2
3
4
5
class Test
{
public:
static const int num = 12;
};

作用域内枚举(C++ 11)

传统枚举可能会存在名称冲突,C++提供的新枚举,其枚举量的作用域为类。

1
2
enum class egg {Small, Large};
enum class t_shirt {Small, Large};

其中class可以替换为struct。使用时需要使用枚举名限定枚举量。如egg::Small。

C++ 11还提高了作用域内枚举的类型安全。在有些情况下常规枚举自动转换为整型,如将其赋值给int变量或用于比较表达式时,但作用域内枚举不能隐式地转换为整型。但可以进行显式转换。

枚举用某种底层整型类型表示,在C++ 98中如何选择取决于实现,其长度可能因系统而异。对于作用域内枚举,C++ 11消除了这种依赖性,默认情况下,C++ 11作用域内枚举的底层类型为int,另外,还提供了一种语法,可用于做出不同选择:

1
enum class: short pizza {Small, Large};

其底层类型为short。底层类型必须为整形。在C++ 11中也可以使用这种语法来指定常规枚举的底层类型,如果没有指定,编译器选择的底层类型将随实现而异。

抽象数据类型

ADT(abstract data type)以通用的方式描述数据类型,而没有引入语言或实现细节。类的概念非常适用于ADT方法。私有部分隐藏实现的细节,共有的方法提供相应的接口。

第十一章 使用类

学习C++的难点之一是需要记住大量的东西,但在拥有丰富的时间经验之前,根本不可能全部记住这些东西。“轻松的使用这种语言,不要觉得必须使用所用特性,不要在第一次学习时就试图使用所有特性”。

运算符重载

运算符重载是一种形式的C++多态。函数重载是使用同名的函数来完成相同的基本操作。同样C++允许运算符有用多种不同的含义。C++(C)很多运算符已经被重载。

重载运算符op的格式如下

1
operatorop(argument-list)

op必须是有效的C++运算符。编译器使用重载的运算符op时使用这种格式obj1.operatorop(obj2)

不要返回局部变量或临时变量的引用,函数执行完毕后,局部变量或临时引用将会消失

我们可以首先定义一个sum函数,然后将sum换成operator+就可以重载+运算符,其他运算符同理
有两种形式可以使用重载运算符

1
2
sum = a.operator+(b);
sum = a + b

可以使用重载运算符对多个操作数进行相加

重载限制

  1. 重载后的运算符必须至少有一个是用户定义的类型,这防止用户为标准类型重载运算符
  2. 使用运算符时不能违反运算符原来的句法规则,不能修改运算符的优先级
  3. 不能创建新的运算符
  4. 不能重载一些运算符,sizeof,.成员运算符,.*成员指针运算符,::作用域解析运算符,typeid一个RTTI运算符,const_cast,dynamic_cast,reinterpret_cast,static_cast强制类型转换运算符
  5. 大多数运算符都可以通过成员或非成员函数进行重载,下边的运算符只能通过成员函数进行重载,=赋值运算符,()函数调用运算符,[]下标运算符,->通过指针访问类成员运算符

友元

C++提供了另外一种形式的访问权限友元。分为三种友元函数,友元类,友元成员函数,通过让函数称为类的友元,可以赋予该函数与类成员函数相同的权限。

在重载二元运算符时,常常需要友元。对于重载了+的类来说,a+1和1+a是一样的。但我们无法将1+a转换为1.operator+(a)的形式。
这时候我们需要使用非成员函数来重载运算符,这种运算符的参数都是显式参数。这样就可以把1+a转换为operator+(1, a)。非成员函数不能访问a对象的私有成员,这时候就需要使用友元函数了。

创建友元的第一步是将函数原型放在类声明中,并在函数原型声明前加上关键字friend。
它说明虽然其实在函数中声明的,但它不是成员函数,因此不能使用成员运算符来调用
虽然它不是成员函数,但是它与成员函数的访问权限相同
不要在函数定义中使用关键字friend

友元和类方法都是表达类接口的两种不同机制。友元必须在类声明中出现,所以类声明仍然控制着哪些函数可以访问私有数据。

如果要为类重载运算符,并将非类的项作为第一个操作数,则可以用友元函数来反转操作数的顺序。

常用的友元是重载<<运算符,重载后可以将其与cout结合输出对象的内容。通用格式

1
2
3
4
5
ostream &operator<<(ostream &os, const c_name &obj)
{
os << ...;
return os;
}

只有在类声明中的原型中才能使用friend关键字,除非函数定义也是原型,否则不能在函数定义中使用该关键字

重载运算符:作为成员函数还是非成员函数

非成员函数版本的重载运算符函数所需要的形参数目与运算符使用的操作数数目相同,成员函数版本的参数少一个,因为其中一个操作数是被隐式的传递的调用对象。

定义运算符时,必须选择其中的一种格式,不能同时选用这两种格式。某些情况下成员函数是唯一的选择,其他情况下,这两种方式没有太大区别。有时使用友元的版本更好一点。

运算符重载是通过函数实现的,所以只要运算符函数的特征标不同,使用的运算符数量与相应的内置C++运算符相同,就可以多次重载同一个运算符

类的自动转换和强制类型转换

将一个标准的类型变量的值赋给另一种标准类型的变量时,如果这两种类型兼容,则C++自动将这个值转换为接收变量的类型。但不进行转换不兼容的类型

可以将类定义成与基本类型或另一个类相关,使得从一种类型转换为另一种类型是有意义的。这种情况下,程序员可以指示C++如何自动进行转换,或通过强制类型转换来完成。

在C++中接受一个参数的构造函数为将类型与该参数相同的值转换为类提供了蓝图。这种转换是自动进行的,而不需要显式强制类型转换。只有接受一个参数的构造函数才能作为转换函数(其他参数可以是默认值)。

1
Student c = "name";

我们也提供了explicit关键字用于关闭这种自动特性。但仍然允许显式转换。
隐式转换在下列情况下出现

  • 将对象初始化为构造函数参数类型时
  • 将构造函数参数类型赋值给对象时
  • 将构造函数类型传递给接受对象参数的函数时
  • 返回值被声明为对象的函数试图返回构造函数参数类型时
  • 在上述任意情况下,使用可自动转换为double类型的内置类型时

最后一点当且仅当不存在二义性时,才会进行这种转换

转换函数可以将用户定义的类型转换成某种类型,这是用户定义的强制类型转换,可以像使用强制类型转换一样使用它们。

1
operator typeName();

转换函数必须是类方法
转换函数不能指定返回类型
转换函数不能有参数

如果存在二义性转换,编译器不会进行自动转换。但我们可以通过强制类型转换来指出需要使用哪个转换函数

1
2
long gone = (double)a;
long gone = double(a);

在C++98中,explicit关键字不能用于转换函数,C++11消除了这种限制,在C++11中可以将转换函数声明为显式的

1
explicit operator int() const;

这样就必须使用强制类型转换来使用。另一种方式是定义一个功能相同的非转换函数替代,其必须被显式的调用。

C++提供了如下类型转换

  • 只有一个参数的构造函数用于将类型与该参数相同的值转换为类类型,使用explicit可以防止隐式转换,只能使用显式转换
  • 被称为转换函数的特殊类成员运算符函数,用于将类对象转换为其他类型。

实现加法的选择,可以使用友元+一个参数的构造函数,或者将加法运算符进行重载,它们各有优点,前者程序简短,但依赖隐式转换,出错少,但调用时间和效率不高,后者正好相反。具体要看使用场景

经验表明,最好不要依赖这种隐式转换函数。

第十二章 类和动态内存分配

C++使用new和delete运算符来动态控制内存,但在类中使用这些运算符将导致许多新的编程问题,这种情况下,析构函数将是必不可少的,有时候还必须重载赋值运算符,保证程序的正确性。

静态类成员只创建一个静态类变量副本。不能在类声明中初始化静态成员变量,静态类成员可以在类声明外用单独的语句进行初始化,初始化语句中指出了类型,并使用了作用域运算符,但没有使用关键字static。初始化不能放在头文件中。静态数据成员为整型或枚举型const可以在类声明中初始化。

在构造函数中使用new来分配内存时,必须在相应的析构函数中使用delete来释放内存,如果使用new[]来分配内存,则应使用delete[]来释放内存。

具体来说,C++自动提供了下面这些成员函数

  • 默认构造函数,如果没有定义构造函数
  • 默认析构函数,如果没有定义
  • 复制构造函数,如果没有定义
  • 赋值运算符,如果没有定义
  • 地址运算符,如果没有定义,返回this的值

C++11还提供了另外两个特殊成员函数:移动构造函数(move constructor)和移动赋值运算符(move assignment operator)

带有参数的构造函数也可以是默认构造函数,只要所有参数都有默认值

复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中,它用于初始化过程中,包括按值传递参数,而不是常规的赋值过程,其原型如下

1
class_name(const class_name &c);

需要知道何时调用和有何功能

何时调用复制构造函数

新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用

1
2
3
4
5
string a;
string s(a);
string s = a;
string s = string(a);
string s = new string(a);

中间的两种声明可能会使用复制构造函数直接创建对象,也可能使用复制构造函数生成一个临时对象,然后将临时对象内容赋给变量,这取决于具体实现。

每当程序生成了对象副本时,编译器都将使用复制构造函数。例如函数按值传递对象,函数返回对象,生成临时对象(如将对象相加)时。

按引用传递可以节约时间和存储空间。

默认的复制构造函数的功能

默认的复制构造函数逐个复制非静态成员(成员复制也叫浅复制),复制的是成员的值。如果成员本身就是类对象,则使用这个类的复制复制函数来复制成员对象。静态函数不受影响,因为它们属于整个类,而不是各个对象。

如果类中包含这样的静态数据成员,即其值将在新对象创建时发生变化,则应该提供一个显式复制构造函数来处理计数问题。

处理类遇到常用的问题就是进行深度复制(deep copy)。应该复制内容(new初始化的,指向数据的指针)而不是地址。

赋值运算符

赋值运算符的原型如下

1
class_name &class_name::operator=(const class_name &c);

它接受并返回一个指向类对象的引用。

赋值运算符的功能以及何时使用它

将已有的对象赋值给另一个对象时,将使用赋值运算符,初始化对象时不一定会使用赋值运算符。
与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制,如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。

解决赋值运算符的问题

仍然是提供赋值运算符的定义,进行深度复制,需要注意的是

  • 由于目标对象可能引用了以前分配的数据,所以可能需要释放之前的内存
  • 函数应该避免将对象赋给自身,需要进行特别判定
  • 函数返回一个指向调用对象的引用

C++空指针,在C++98中,字面值0有两个含义:可以表示数字值零,也可以表示空指针。C语言用NULL表示空指针,C++11提供了关键字nullptr,表示空指针。但仍保留了使用0的方式

对于操作符[],有两个操作数。重载[]时,C++将区分常量和非常量函数的特征标。后者就可以读const对象了

1
2
char &opeartor[](int idx);
const char &operator[](int idx) const;

静态类成员函数

不能通过对象调用静态成员函数;静态成员函数不能使用this指针,可以通过类名和作用域解析运算符来调用它。
静态类成员函数只能使用静态数据成员。

在构造函数中使用new时应注意的事项

  • 如果在构造函数中使用new([])来初始化指针成员,则应该在析构函数中使用delete([])
  • 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象
  • 应定义一个赋值运算符,通过深度复制想一个对象复制给另一个对象

返回对象的说明

  • 返回指向const对象的引用
  • 返回指向非const对象的引用
  • 返回对象
  • 返回const对象,使用+运算符的时,否则可能出现a+b=c情况

使用指向对象的指针

析构函数的调用

  • 如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数
  • 如果对象是静态变量(外部,静态,静态外部或来自名称空间),则在该程序结束时将调用该对象的析构函数
  • 如果对象是new创建的,则仅当您显式的使用delete删除对象时,其析构函数才会被调用

定位new运算符

delete不能与定位new运算符配合使用,因此在使用定位new运算符时,不能使用delete来删除对象,进而调用析构函数。解决方案是显式的为定位new运算符创建的对象调用析构函数。需要注意的一点是正确的删除顺序,对于使用定位new运算符创建的对象,应以创建顺序相反的顺序进行删除,原因在于晚创建的对象可能依赖于早创建的对象,另外,仅当所有对象被销魂后,才能释放用于存储这些对象的缓冲区。

其他

我们可以在类中嵌套结构或类声明。其作用域为整个类。公共的定义还可以通过类名和作用域解析运算符来使用。

在类中初始化非静态const或引用对象时,我们需要使用成员初始化列表(member initializer list),只有构造函数可以使用这种方式。对于本身就是类对象的成员来说,使用初始化列表效率更高。初始化的顺序与他们在类声明中出现的顺序相同,与初始化列表中的排列顺序无关。

C++11可以使用类内初始化初始化非静态const成员,在类中可以使用这样的语句int a = 10;在构造函数中的成员初始化列表会覆盖其值。

第十三章 类继承

C++提供了类继承,能够从已有的类派生出新的类,而派生类继承了原有类(称为基类)的特征,包括方法。
通过继承可以在已有类的基础上添加功能。可以给类添加数据。可以修改类方法的行为。

派生类对象将包含基类对象,使用共有派生,基类的共有成员将成为派生类的共有成员,基类的私有部分也将成为派生类的一部分,但只能通过基类的共有或保护方法访问。
派生类需要自己的构造函数,派生类可以根据需要添加额外的数据成员和成员函数。

派生类构造函数必须使用基类构造函数,创建派生类对象时,程序首先创建基类对象。基类对象应该在程序进入派生类构造函数之前被创建,在C++中成员初始化列表语法来完成这种工作。如果不调用基类构造函数将调用默认的基类构造函数,派生类构造函数应该初始化派生类新增的数据成员。派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。

派生类对象可以使用基类的方法,条件是方法不是私有的。
基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象。但是基类指针或引用只能调用基类方法。

有三种继承方式:共有继承,保护继承,私有继承。共有继承是一种is-a关系,派生类对象也是一个基类对象。其他需要区分的是has-a,is-like-a,is-implemented-as-a,uses-a关系。

多态共有继承

如果希望同一个方法在派生类和基类中的行为是不同的,方法的行为取决于调用该方法的对象,这种较复杂的行为称为多态——具有多种形态,即同一个方法的行为随上下文而异。可以通过两种重要的机制用于实现多态共有继承。在派生类中重新定义基类的方法或使用虚方法。

如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的,这样程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例。

为何需要虚析构函数。如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。因此使用虚析构函数可以保证正确的析构函数序列被调用。

静态联编和动态联编

将源代码中的函数调用解释为执行特定函数代码块被称为函数名联编(binding)。C或C++编译器可以在编译过程完成这种联编。在编译过程中进行联编称为静态联编(static binding)。然而虚函数必须使用动态联编(dynamic binding),必须生成能够在程序运行过程中选择正确虚函数方法的代码。

将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),这使得共有继承不需要进行显式类型转换。相反的过程,将基类指针或引用转换为派生类指针或引用称为向下强制转换(downcasting)。

为什么有两种类型的联编以及为什么默认为静态联编

我们需要考虑效率和概念模型。
为了能使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的开销。如果不需要重新定义方法,则使用静态联编更合理。因为其效率更高,因此被设置为C++的默认选择。一个原则是不要为不使用的特性付出代价(内存或处理时间)。仅当程序设计确实需要使用虚函数时,才使用他们。

另外在类设计时,可能包含一些不在派生类中重新定义的成员函数,不设置该函数为虚函数效率更高,另外也指出不要重新定义该函数。这表明仅将那些预期将被重新定义的方法声明为虚的。

虚函数的工作原理

通常编译器处理虚函数的方法是给每个对象添加一个隐藏成员。隐藏成员保存了一个指向函数地址数组的指针,这种数组被称为(虚函数表,virtual function table,vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数地址也将被添加到vtbl中。
其成本包括

  • 每个对象都将增大,增大为存储地址的空间
  • 对于每个类,编译器都创建一个虚函数地址表(数组)
  • 对于每个函数调用,都要执行一项额外的操作,即到表中查找地址

虚函数的要点

  • 在基方法的生命中使用关键字virtual可使该方法在基类以及在所有的派生类(包括从派生类派生出来的类)中是虚的
  • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种行为非常重要,因为这样基类指针或引用可以指向派生类对象
  • 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的

构造函数不能是虚函数,创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后派生类的构造函数将使用基类的一个构造函数,这不同于继承。

析构函数应当是虚函数,除非类不用作基类。

友元不能是虚函数,因为友元不是成员函数

如果派生类没有重新定义函数,则将使用该函数的基类版本,如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。

重新定义将隐藏方法
重新定义继承的方法并不是重载,如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。

如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用指针,则可以修改为指向派生类的引用或指针,这种特性被称为返回类型协变(covariance of return type),因为允许返回类型随类类型的变化而变化。这种例外只适用于返回值,而不适用于参数。

如果基类声明重载了,则应在派生类中重新定义所有的基类版本。如果只重新定义一个版本,则其他的版本将被隐藏。如果不需要修改,则新定义可只调用基类版本。

访问控制:protected

关键字protected与private相似,在类外只能用共有类成员来访问protected部分中的类成员,private和protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员。因此,对于外部世界来说保护成员的行为与私有成员类似,但对于派生类来说,保护成员的行为与共有成员相似。

最好对类数据成员采用私有访问控制,不要使用保护访问控制,同时通过基类方法使派生类能够访问基类数据。对于成员函数来说,保护访问控制很有用,他让派生类能够访问公众不能使用的内部函数。

抽象基类

抽象基类比较复杂(abstract base class)。C++通过使用纯虚函数(pure virtual function)提供未实现的函数。纯虚函数声明的结尾处为=0

当类声明中包含纯虚函数的时候,则不能创建类对象。这里的理念是,包含纯虚函数的类只用作基类。在原型中使用=0指出类是一个抽象基类,再类中可以不定义该函数。但也可以定义不过好像没什么作用。

可以将ABC看作是一种必须实施的接口。ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循ABC设置的接口规则。

继承和动态内存分配

在继承中使用动态内存分配需要注意一些情况。

第一种情况:派生类不使用new

假设基类使用了动态内存分配,并且正确实现了析构函数,复制构造函数和重载赋值运算符。如果其派生类不使用动态内存分配,其是否需要显式的定义析构函数,复制构造函数,赋值运算符。答案是不用。

派生类的析构函数总要执行自身的代码后调用基类析构函数,因为派生类的析构函数不需要进行任何操作,所以不用显式提供析构函数。

默认复制构造函数执行成员复制,对于派生类来说没有问题。我们要考虑从基类继承过来的组件,复制类成员或继承的类组件时,则是使用该类的复制构造函数完成的。所以默认构造函数对于派生了也是合适的。赋值运算符也类似。

第二种情况:派生类使用了new

派生类使用了动态内存分配,则必须为派生类定义显式析构函数,复制构造函数,赋值运算符。

派生类析构函数自动调用基类的析构函数,故其职责是对派生类构造函数执行工作的进行清理,因此派生类的析构函数需要释放派生类分配的内存。复制构造函数需要深度复制派生类的动态内存分配部分,同时也要调用基类的复制构造函数,方法是使用成员初始化列表调用基类的复制构造函数。基类引用可以指向派生类对象。

赋值运算符需要处理派生类的动态内存分配,同时也需要调用基类的赋值运算符,我们必须显式调用基类的赋值运算符,BaseClass::operator=(c)。必须使用这种函数表示法,否则可能会造成递归调用。

友元函数并非类成员,因此不能继承,然而,您可能希望派生类的友元函数能够使用基类的友元函数,为此,可以通过强制类型转换,派生类引用或指针转换为基类的引用或指针,然后使用转换后的指针或引用来调用基类的友元函数。