C++ Primer知识点学习(四)

第四部分
1、面向对象编程
定义为virtual的函数是基类期待派生类重新定义的。
在C++中,通过基类的引用(或指针)调用虚函数,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)的调用虚函数在运行时确定。被调用的函数是引用(或指针)所指对象的实际类型所定义的。
有类class Base;和类 class D:public Base;,在基类中有虚函数fcn,在派生类中有继承该虚函数fcn。则如下代码:
D d;
Base *b = &d;
b->fcn();
此时在运行时调用的是派生类中的虚函数,因为虽然指针类型是Base,但是其指针指向的实际对象是派生类对象。

保留字virtual的目的是启用动态绑定。成员默认为非虚函数,对非虚函数的调用在编译时确定。除了构造函数之外,任意非static成员函数都可以是虚函数。

派生类对象由多个部分组成:派生类本身定义的(非static)成员加上由基类(非static)成员组成的子对象。派生类可以访问其基类的public和protected成员,就好像那些成员是派生类自己的成员一样。派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象的protected成员没有特殊访问权限。
void Child::memfcn(const Child & c,const Base &b)
{
    //price成员在基类中声明,被派生类继承,使得其派生类也有price成员。
    double ret = price;//ok,use this->price
    ret = c.price;//ok,use price from Child object
    ret = b.price;//error,no access to price from an Base
}
类的protected部分可以被类成员、友元和派生类成员访问,但类的普通用户不能访问protected成员。只有类本身和友元可以访问基类的private部分。
派生类中虚函数的声明必须与基类中的定义方式完全匹配,但当返回的是对基类型的引用(或指针)的虚函数,派生类中的虚函数可以选择返回基类函数所返回类型的派生类的引用(或指针)。

【派生类到基类的转换】
因为每个派生类对象都包含基类部分,所以可以将基类类型引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象。(将派生类对象当作基类对象是安全的,因为每个派生类对象都拥有基类子对象。)
静态类型:在编译时可知的引用类型或指针类型
动态类型:指针或引用所绑定的对象的类型,这是仅在运行时可知的。
【基类类型的指针和引用可以绑定到派生类型的对象,在这种情况下,静态类型是基类引用(或指针),但动态类型是派生类引用(或指针)】
如果调用非虚函数(在编译时根据调用该函数的对象、引用或指针的类型而确定),则无论实际对象是什么类型,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。
派生类虚函数调用基类版本时,必须显示使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。
在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。

基类本身指定对成员的最小访问控制。如果成员在基类中为private,则只有基类和基类的友元可以访问该成员。派生类不能访问基类的private成员,也不能使自己的用户能够访问那些成员。
公用继承:基类成员保持自己的访问级别:基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员。(接口继承)
受保护继承:基类的public和protected成员在派生类中为protected成员。(实现继承)
私有继承:基类的所有成员在派生类中为private成员。【此时派生类对象不能访问基类成员,即此时的基类成员对派生类来说都为private成员】(实现继承)【可使用using声明访问基类中的名字】

struct保留字定义的类【默认public】与用class定义的类【默认private】唯一的不同只有默认的成员保护级别和默认的派生保护级别,没有其他区别。
友元关系不能继承,即基类的友元对派生类的成员没有特殊访问权限。
如果基类定义了static成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个static成员只有一个实例

【引用转换不同于转换对象】
派生类对象传给希望接受基类引用的函数时,引用直接绑定到该对象,并且,转换不会在任何方面改变派生类型对象,该对象仍是派生类型对象。【将派生类对象转换为基类类型引用,再调用基类复制构造函数或赋值操作符
派生类对象传给希望接受基类类型对象的函数时,此时形参的类型是固定的,在编译时和运行时形参都是基类类型对象。如果用派生类型对象调用这样的函数,则该派生类对象的基类部分被复制到形参。【用派生类对象对基类对象进行初始化或赋值,即初始化时调用构造函数,赋值时调用赋值操作符。将派生类对象复制到基类对象时,派生类对象将被切掉

【基类到派生类的转换】
从基类到派生类的自动转换是不存在的。编译器确定转换是否合法,只看指针或引用的静态类型。在这些情况下,如果知道从基类到派生类的转换是安全的,就可以使用static_cast强制编译器进行转换或者使用dynamic_cast申请在运行时进行检查。

【派生类的构造函数和复制构造】
基类部分可由基类的默认构造函数初始化。或者使用构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。首先初始化基类【基类的构造函数】,然后根据声明次序初始化派生类的成员只能初始化直接基类。
重构:重新定义类层次,将操作或数据从一个类移到另一个类。
赋值操作符必须防止自身赋值【因为赋值操作是先撤掉左边,再将右边赋给左边】
每个析构函数只负责清除自己的成员。对象的撤销顺序于构造顺序相反,首先运行派生类析构函数,然后按继承层次向上调用各基类析构函数。
要保证运行适当的析构函数,基类中的析构函数必须为虚函数,此时通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同。基类析构函数时三法则的一个重要例外。即如果基类为了将析构函数设为虚函数而具有空析构函数,那么具有析构函数并不表示也需要赋值操作符或复制构造函数。
在基类构造函数或析构函数中,将派生类对象当作基类类型对象对待。如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。

与基类成员同名的派生类成员将屏蔽对基类成员的直接访问,设计派生类时,只要可能,最好避免与基类成员的名字冲突。
【名字查找与继承】
确定函数调用遵循以下四个步骤:
1、首先确定进行函数调用的对象、引用或指针的静态类型。
2、在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
3、一旦找到了该名字,就进行常规类型检查,查看如果给定找到的定义,该函数调用是否合法。
4、假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。
【纯虚函数】
将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本将不会调用。含有(或继承)一个或多个纯虚函数的类是抽象基类,除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象
因为派生类对象在赋值给基类对象时会被“切掉”,所以容器与通过继承相关的类型不能很好地融合。

C++中一个通用的技术是定义包装类或句柄类(提供到其他类的接口的类)句柄类存储和管理基类指针。指针所指对象的类型可以变化,它既可以指向基类类型对象又可以指向派生类对象。用户通过句柄类访问继承层次的操作。因为句柄类使用指针执行操作,虚成员的行为将在运行时根据句柄实际绑定的对象的类型而变化
句柄类经常需要在不知道对象的确切类型时分配已知对象的新副本。解决这个问题的通用方法是定义虚操作进行复制,该操作称为clone。即从基类开始,在继承层次的每个类型中增加clone,且基类必须将该函数定义为虚函数。
使用带比较器的关联容器
typedef bool (*Comp)(const Sales_item&,const Sales_item&);//将Comp定义为函数类型指针的同义词
inline bool comapre(const Sales_item &lhs,const Sales_item &rhs)
{
     return lhs->book()< rhs->book();
}
std::multiset<Sales_item,Comp>  items(comple);
items是一个multiset,它保存Sales_item对象并使用Comp类型的对象比较它们。当在items中增加或查找元素时,将用compare函数对multiset进行排序。

2、模板与泛型编程
面向对象编程所依赖的多态性称为运行时多态性。应用于存在继承关系的类,能够忽略基类与派生类之间类型上的差异。只要使用基类的引用或指针,基类类型或派生类类型的对象就可以使用相同的代码。
泛型编程所依赖的多态性称为编译时多态性或参数式多态性。所编写的类和函数能够多态地用于跨越编译时不相关的类型。一个类或一个函数可以用来操纵多种类型的对象。
template  <typename T>】   【template  <class T>
模板定义以关键字template开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔模板形参表不能为空
inline函数模板:inline说明符应放在模板形参表之后、返回类型之前,不能放在关键字template之前。
模板形参遵循常规名字屏蔽规则,与全局作用域中声明的对象、函数或同类型同名的模板形参会屏蔽全局名字。用作模板形参的名字不能在模板内部重用,在同一形参表中也只能使用一次。每个模板类型形参前面必须带上关键字class或typename,每个非类型形参前面必须带上类型名字。
类型形参:表示类型。由关键字class或typename后接说明符构成。在模板形参表中,这两个关键字具有相同的含义,都指出后面所接的名字表示一个类型。
非类型形参表示常量表达式。模板定义内部的常量值。必须是编译时常量表达式。
在模板定义内部指定类型。通过在成员名前加上关键字typename作为前缀,可以告诉编译器将成员当作类型。
编写泛型代码的两个重要原则:1、模板的形参是const引用。2、函数体中的测试只用<比较
一般而言,不会转换实参以匹配已有的实例化,相反,会产生新的实例。除了产生新的实例化之外,编译器只会执行两种转换
1、const转换接受const引用或const指针的函数可以分别用非const对象的引用或指针来调用,无须产生新的实例化。如果函数接受非引用类型,形参类型和实参类型都忽略const,即无论传递const或非const对象给接受非引用类型的函数,都使用相同的实例化。
2、数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数的实参应用常规指针转换。数组实参将当作指向其第一个元素的指针,函数实参当作指向函数类型的指针。当形参为引用时,数组不能转换为指针。
为调用提供显示模板实参定义类模板的实例很类似,在以逗号分隔、用尖括号括住的列表【<,>】中指定显示模板实参。显示模板类型的列表出现在函数名之后,实参表之前。显示模板实参从左至右与对应模板形参相匹配,假如可以从函数形参推断,则只有结尾(最右边)形参的显示模板实参可以省略。
【模板编译】
模板要进行实例化,则编译器必须能够访问定义模板的源代码。针对类定义和函数声明放在头文件中,而函数定义和成员定义放在源文件中。标准C++为编译模板代码定义了两种模型:
1、包含编译模型:在声明函数模板或类模板的头文件中添加一条#include指示使定义可用,该#inlcude引入了包含相关定义的源文件。有可能存在编译器多余实例化的问题,使得编译性能降低。
2、分别编译模型:使用export关键字。一般在函数模板的定义中指明函数模板为导出的,声明不必指定export。而类模板的声明必须放在头文件,而且头文件中的类定义体不应该使用关键字export,如果在头文件使用了export,则该头文件只能被程序中的一个源文件使用。相反,应该在类的实现文件中使用export。
编译器的用户指南应该会说明系统怎样管理模板。
通常,当使用类模板的名字时,必须指定模板形参。但当在类本身的作用域内部,可以使用类模板的非限定名。
【类模板成员函数】
定义形式:
1、必须以关键字template开头,后接类的模板形参表。
2、必须指出它是哪个类的成员。
3、类名必须包含其模板形参
类模板成员函数本身也是函数模板。在实例化类模板成员函数时,编译器不执行模板实参推断,其形参由调用该函数的对象的类型确定,使得调用类模板成员函数比调用类似函数模板更灵活。类模板的成员函数只有为程序所用才进行实例化类模板的指针定义不会对类进行实例化,只有用到这样的指针时才会对类进行实例化
【类模板中的友元声明】
在类模板中可以出现三种友元声明:
1、普通友元:普通非模板类或函数的友元声明,将友元关系授予明确指定的类或函数。【该普通非模板类或函数可以访问模板类任意实例】
2、一般模板友元:类模板或函数模板的友元声明,授予对友元所有实例的访问权。【友元的任意实例都能访问该模板类的任意实例】
3、特定的模板友元关系:只授予对类模板或函数模板的特定实例的访问权的友元声明。【友元的特定实例才能访问该模板类的任意实例。可建立一对一映射,将两者进行同样类型实例化】
想要得到对特定实例化的友元关系时,必须在可以用于友元声明之前声明类或函数。如果没有事先告诉编译器该友元是一个模板,则编译器将认为该友元是一个普通非模板类或非模板函数。
【成员模板】
任意类(模板或非模板)可以拥有本身为类模板或函数模板的成员,这种成员称为成员模板成员模板不能为虚。
当在类模板作用域外部定义成员模板的时候,必须包含两个模板形参表,类模板形参表和成员模板形参表。
成员模板有两种模板形参:由类定义的和由成员模板本身定义的。类模板形参由调用函数的对象的类型确定,成员定义的模板形参的行为与普通函数模板一样。这些形参都通过常规模板实参推断而确定。

给定类型实例化的所有对象都共享一个static成员。通过类使用static成员时,必须引用实际的实例化。像使用任意其他static数据成员一样,必须在类外部出现数据成员的定义。在类模板含有static成员的情况下,成员定义必须指出它是类模板的成员

【泛型句柄类】
句柄能够动态分配和释放相关继承类的对象,并且将“实际”工作转发给继承层次中的底层类。句柄类的行为类似于指针:复制句柄对象将不会复制基础对象,复制之后,两个句柄对象将引用同一基础对象。
接受单个类型形参,并且分配和管理指向该类型对象的指针。句柄类定义了必要的复制控制成员,它还定义了解引用操作符和箭头操作符。

【模板特化】
模板特化是一种特化的定义,它定义了模板的不同版本,将一个或多个形参绑定到特定类型或特定值。在定义了被特化的模板之前,不能出现该模板的特化。
对用户而言,调用特化函数或使用特化类。与使用从通用模板实例化的版本无法区别。【特化:指定类型
模板特化定义:1、关键字template后面接一对空的尖括号(<>);2、再接模板名和一对尖括号(可选),尖括号中指定这个特化定义的模板形参;3、函数形参表;4、函数体
类特化外部定义成员时,成员之前不能加template<>标记。特化成员而不特化类,成员特化的声明与任何其他函数模板特化一样。
类模板的部分特化本身也是模板。部分特化的模板形参表是对应的类模板定义形参表的子集。
【重载与函数模板】
函数模板可以重载:可以定义有相同名字但形参数目或类型不同的多个函数模板,也可以定义与函数模板有相同名字的普通非模板函数。
当匹配同样好时,非模板版本优先。


阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页