第15章 面向对象编程OOP(Object-oriented programming)
面向对象编程基于三个基本概念: 数据抽象、继承和动态绑定。
面向对象编程基于三个基本概念: 数据抽象、继承和动态绑定。
在 C++ 中,用类进行数据抽象,用类派生从一个类继承另一个:派生类继承基类的成员。动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义
的函数。
的函数。
- private 成员
• 通过类对象无法访问类的private成员。
• 在派生类中不能访问基类的private成员。
• private 成员只能在当前类的作用域内访问,类的友元也可以访问类的private 成员。例如,在成员函数中可以访问private 成员,在成员函数中还可以通过自己类的对象来访问类的private 成员。类的作用域包括:类定义的{}之内,类定义之外的成员函数的函数体,形参列表等。
- protected 成员
• 通过类对象无法访问protected 成员。
• protected 成员可被public派生类(包括派生类的派生类,向下传递)访问,也就是说在派生类中可以使用基类的protected 成员。
• 派生类只能通过派生类对象访问其基类的 protected 成员,派生类无法访问其基类类型对象的 protected 成员。
- 派生类和虚函数
派生类一般会重定义所继承的虚函数。如果派生类没有重定义某个虚函数,则使用基类中定义的版本。
一般情况下,派生类中虚函数的声明必须与基类中的定义方式完全匹配,例外:返回对基类型A的引用(或指针)的虚函数。派生类中的虚函数可以返回类A的派生类的引用(或指针)。
一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用 virtual 保留字,也可以不使用。
- 在运行时确定 virtual 函数的调用
将基类类型的引用或指针绑定到派生类对象,那么在调用虚函数时,如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。只有通过引用或指针调用,虚函数才在运行时确定。只有在这些情况下,直到运行时才知道对象的动态类型。
- 虚函数与默认实参
虚函数也可以有默认实参。如果一个调用省略了具有默认值的实参,则所用的值由调用该函数的类型定义,与对象的动态类型无关。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。
- 友元关系与继承
友元关系不能继承:
(1)基类的友元对派生类的成员没有特殊访问权限。
(2)友元类的派生类不能访问授予友元关系的类。
- 继承与静态成员
如果基类定义 static 成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个 static 成员只有一个实例。
static 成员遵循常规访问控制:如果成员在基类中为 private,则派生类不能访问它。如果可以访问成员,则既可以通过基类访问 static 成员,也可以通过派生类访问 static 成员。一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。
- 派生类到基类的转换
指针或引用:(1)派生类型引用到基类类型引用,(2)派生类型指针到基类类型指针。反之是不行的。
对象:一般可以使用派生类型的对象对基类类型的对象进行初始化或赋值,但没有从派生类型对象到基类类型对象的直接转换,编译器不会自动将派生类型对象转换为基类类型
对象。
- 可以使用派生类型对象对基类对象进行赋值或初始化。
将派生类型的对象传给希望接受基类引用的函数,实际上实参是该对象的引用,对象本身未被复制。
将派生类对象传给希望接受基类类型对象(而不是引用)的函数时,该派生类对象的基类部分被复制到形参。
- 派生类构造函数
构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果类不定义自己的默认构造函数和复制控制成员,就将使用合成版本。
派生类的合成默认构造函数,除了初始化派生类的数据成员之外,它还初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化。
派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员。派生类构造函数只能通过将基类包含在构造函数初始化列表中来间接初始化继承成员。
class Bulk_item : public Item_base {
public:
Bulk_item(const std::string& book, double sales_price,std::size_t qty = 0, double disc_rate = 0.0):
Item_base(book, sales_price), min_qty(qty), discount(disc_rate) { }
// as before
};
构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。首先初始化基类,然后根据声明次序初始化派生类的成员。
一个类只能初始化自己的直接基类(通过将基类包含在构造函数初始化列表中来间接初始化基类)。
- 复制控制和继承
只包含类类型或内置类型数据成员、不含指针的类一般可以使用合成操作,复制、赋值或撤销这样的成员不需要特殊控制。具有指针成员的类一般需要定义自己的复制控制来管理这些成员。
- 派生类的复制构造函数
如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分:
class Base { /* ... */ };
class Derived: public Base
{
public:
Derived(const Derived& d):Base(d) { /*... */ }
};
- 派生类赋值操作符
赋值操作符通常与复制构造函数类似:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。
//Base::operator=(const Base&)
Derived &Derived::operator=(const Derived &rhs)
{
if (this != &rhs) //防止给自己赋值
{
Base::operator=(rhs); // 调用 Base 类的赋值操作符给基类部分赋值
……//为派生类Derived 的成员赋值
}
return *this;
}
- 派生类析构函数
派生类析构函数不负责撤销基类对象的成员。每个析构函数只负责清除自己的成员。
class Derived: public Base
{
public:
~Derived() { /* do what it takes to clean up derived members
*/ }
};
- 虚析构函数
如果层次中基类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数。
建议:即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。
- 构造函数和赋值操作符不是虚函数
在复制控制成员中,只有析构函数应定义为虚函数,构造函数不能定义为虚函数。
- 名字冲突与继承
与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。
可以使用作用域操作符访问被屏蔽的基类成员:
struct Derived : Base
{
int get_base_mem() { return Base::mem; }
};
设计派生类时,只要可能,最好避免与基类成员的名字冲突。
- 作用域与成员函数
在基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽。
- 重载函数
如果派生类重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员。
如果派生类想要通过自身类型来使用重载的版本,那么派生类必须重定义所有的重载版本,但这样会繁琐,可以通过给重载成员提供using 声明来达到简化的目的。
using Base::Func;//注意,将所有基类Base中的Func函数在本类中可见。
- 名字查找与继承
(1)首先确定进行函数调用的对象、引用或指针的静态类型。
(2)在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
(3)一旦找到了该名字,就进行常规类型检查,查看如果给定找到的定义,该函数调用是否合法。
(4)假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。
- 纯虚函数
在函数形参表后面写上 = 0 以指定纯虚函数。该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本决不会调用。
含有(或继承)一个或多个纯虚函数的类是抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象。
第16章
模板和泛型编程
泛型编程就是以独立于任何特定类型的方式编写代码。模板是泛型编程的基础。在泛型编程中,我们所编写的类和函数能够多态地用于跨越编译时不相关的类型。一个类或一个函数可以用来操纵多种类型的对象。
- 定义函数模板
函数模板是一个独立于类型的函数,可作为一种方式,产生函数的特定类型版本。下面是 compare 的模板版本:
// implement strcmp-like generic compare function
// returns 0 if the values are equal, 1 if v1 is larger, -1 if v1 is smaller
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
模板定义以关键字 template 开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。模板形参表不能为空。
- inline 函数模板
函数模板可以用与非模板函数一样的方式声明为 inline。说明符放在模板形参表之后、返回类型之前,不能放在关键字 template 之前。
template <typename T> inline T min(const T&, const T&);// ok: inline specifier follows template parameter list
inline template <typename T> T min(const T&, const T&);// error: incorrect placement of inline specifier
- 类模版
类模版是一个类定义,用来定义一组特定类型的类,类模板用template关键字后接用尖括号<>括住,以逗号分隔的一个或多个模板形参的列表来定义。
template <class Type> class Queue {
public:
Queue (); // default constructor
Type &front (); // return element from head of Queue
const Type &front () const;
void push (const Type &); // add element to back of Queue
void pop(); // remove element from head of Queue
bool empty() const; // true if no elements in the Queue
private:
// ...
};
与调用函数模板形成对比,使用类模板时,必须为模板形参显式指定实参:
Queue<int> qi; // Queue that holds ints
Queue< vector<double> > qc; // Queue that holds vectors of doubles
Queue<string> qs; // Queue that holds strings
- 模板形参
模板形参作用域:模板形参的名字可以在声明为模板形参之后直到模板声明或定义的末尾处使用。
模板形参遵循常规名字屏蔽规则。与全局作用域中声明的对象、函数或类型同名的模板形参会屏蔽全局名字。
- 限制
模板形参的名字不能在模板内部重用。
模板形参的名字只能在同一模板形参表中使用一次。
模板形参的名字可以在不同模板中重用。
- 模板声明
像其他任意函数或类一样,对于模板可以只声明而不定义。
同一模板的声明和定义中,模板形参的名字可以不相同。
- 模板类型形参
类型形参由关键字 class 或 typename 后接说明符构成。在模板形参表中,这两个关键字具有相同的含义,都指出后面所接的名字表示一个类型。
它可以用于指定返回类型或函数形参类型,以及在函数体中用于变量声明或强制类型转换。
- typename 与 class 的区别
在函数模板形参表中,关键字 typename 和 class 具有相同含义,可以互换使用,两个关键字都可以在同一模板形参表中使用。
关键字 typename 是作为标准 C++ 的组成部分加入到 C++ 中的,因此旧的程序更有可能只用关键字 class。
- 在模板定义内部指定类型
通过在成员名前加上关键字 typename 作为前缀,可以告诉编译器将成员当作类型。
template <class Parm, class U>
Parm fcn(Parm* array, U value)
{
typename Parm::size_type * p; // ok: declares p to be a pointer
}
- 非类型模板形参
模板形参不必都是类型。
template <class T, size_t N>
void array_init(T (&parm)[N])
{
for (size_t i = 0; i != N; ++i) {
parm[i] = 0;
}
}
- 编写泛型程序
在函数模板内部完成的操作限制了可用于实例化该函数的类型。所以,应该保证用作函数实参的类型实际上支持所用的任意操作,以及保证在模板使用哪些操作的环境中那些操作运行正常。
编写模板代码时,重要的原则是:实参类型的要求尽可能少。
- 编写泛型代码的两个重要原则(对实参类型的要求尽可能少):
• 模板的形参是 const 引用。
• 函数体中的测试只用 < 比较。
- 类的实例化
类模板的每次实例化都会产生一个独立的类类型。
想要使用类模板,就必须显式指定模板实参。
- 函数模板实例化
使用函数模板时,编译器通常会为我们推断模板实参。
- 类型形参的实参的受限转换,编译器只会执行两种转换:
• const 转换:接受 const 引用或 const 指针的函数可以分别用非 const 对象的引用或指针来调用,无须产生新的实例化。
• 数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。
- 类定义和函数声明放在头文件中,而函数定义和成员定义放在源文件中。
- 包含编译模型
在包含编译模型中,编译器必须看到用到的所有模板的定义。一般而言,可以通过在声明函数模板或类模板的头文件中添加一条 #include 指示使定义可用,该 #include 引入了包含相关定义的源文件。
- 分别编译模型
在分别编译模型中,编译器会为我们跟踪相关的模板定义。但是,我们必须让编译器记住给定的模板定义,可以使用 export 关键字来做这件事。
在一个程序中,一个模板只能定义为导出一次。
(1)函数模板
在函数模板的定义中指明函数模板为导出的,在关键字template 之前包含 export 关键字:
export template <typename Type>
Type sum(Type t1, Type t2) /* ...*/
这个函数模板的声明像通常一样应放在头文件中,声明不必指定 export。
(2)类模版
类声明必须放在头文件中,头文件中的类定义体不应该使用关键字 export,应该在类的实现文件中使用 export。
template <class Type> class Queue { ... };
export template <class Type> class Queue;
#include "Queue.h"
- 通常,当使用类模板的名字的时候,必须指定模板形参。这一规则有个例外:在类本身的作用域内部,可以使用类模板的非限定名。例如,在默认构造函数和复制构造函数的声明中,名字 Queue 是 Queue<Type> 缩写表示。
编译器不会为类中使用的其他模板的模板形参进行这样的推断,因此,在声明伙伴类 QueueItem 的指针时,必须指定类型形参: QueueItem<Type> *head; // pointer to first element in Queue
- 类模板成员函数
• 必须以关键字 template 开关,后接类的模板形参表。
• 必须指出它是哪个类的成员。
• 类名必须包含其模板形参。例如:
template <class Type> void Queue<Type>::destroy()
{
while (!empty())
pop();
}
- 将类模板设为友元
template <class Type> class Queue;
template <class Type> class QueueItem {
friend class Queue<Type>;
// ...
};
这个声明建立了想要一对一映射,只将与 QueueItem 类用同样类型实例化的
Queue 类设为友元。
- 将函数模板设为友元
template <class T>
std::ostream& operator<<(std::ostream&, const Queue<T>&);
template <class Type> class QueueItem {
friend class Queue<Type>;
// needs access to item and next
friend std::ostream&
operator<< <Type> (std::ostream&, const Queue<Type>&);
// ...
};
template <class Type> class Queue {
// needs access to head
friend std::ostream&
operator<< <Type> (std::ostream&, const Queue<Type>&);
};
- 成员模板
任意类(模板或非模板)可以拥有本身为类模板或函数模板的成员,这种成员称为成员模板,成员模板不能为虚。
- 在类外部定义成员模板
当成员模板是类模板的成员时,它的定义必须包含类模板形参以及自己的模板形参。首先是类模板形参表,后面接着成员自己的模板形参表。
template <class T> template <class Iter> //第一个模板形参表 template<class T> 是类模板的,第二个模板形参表 template<class Iter> 是成员模板的。
void Queue<T>::assign(Iter beg, Iter end)
{
destroy(); // remove existing elements in this Queue
copy_elems(beg, end); // copy elements from the input range
}
- 类模板的 static 成员
类模板可以像任意其他类一样声明 static 成员