-
同一个方法在派生类和基类中的行为是不同的,即方法的行为应取决于调用该方法的对象,这种行为称为多态。有两种重要的机制可用于实现多态公有继承:在派生类中重新定义基类的方法;使用虚方法。
·虚函数:在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
·在基类中不对虚函数给出有意义的实现,即函数没有主体,此时的虚函数是纯虚函数。
·如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。 -
函数名联编(binding):程序调用函数时,将源代码中的函数调用解释为执行特定的函数代码块。
①在C++中,由于函数重载的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数。C/C++编译器可以在编译过程完成这种联编,被称为静态联编(static binding),又称为早期联编(early binding)。
②编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)。
·编译器对非虚方法使用静态联编,编译器对虚方法使用动态联编。 -
·将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使公有继承不需要进行显式类型转换。
·将基类指针或引用转换为派生类指针或引用,称为向下强制转换。如果不使用显式类型转换,则向下强制转换是不允许的。 -
编译器处理虚函数的方法是:给每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,这种数组称为虚函数表(virtual function table,vtbl)。
·虚函数表中存储了为类对象进行声明的虚函数的地址。
·基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。
·派生类对象将包含一个指向独立地址表的指针:如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到vtbl中。
·使用虚函数时,每个对象都将增大,增大量为存储地址的空间;对于每个类,编译器都创建一个虚函数地址表(数组);对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。 -
构造函数不能是虚函数;析构函数应该是虚函数,除非类不用作基类(通常应给基类提供一个虚析构函数,即使它并不需要析构函数);友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。
-
重新定义继承的方法并不是重载。如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。
①如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。
②如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。 -
抽象基类(abstract base class,ABC)描述的是至少使用一个纯虚函数的接口,从ABC派生出的类将根据派生类的具体特征,使用常规虚函数来实现这种接口。
·ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循ABC设置的接口规则。这种情况下,使用ABC使得组件设计人员能够制定“接口约定”,这样确保了从ABC派生的所有组件都至少支持ABC指定的功能。 -
若基类使用动态内存分配,并重新定义赋值和复制构造函数,对派生类的影响有如下两种。
①派生类不使用new:基类声明中包含了构造函数使用new时需要的析构函数、复制构造函数和重载赋值运算符,对派生类来说,使用默认析构函数、默认复制构造函数和默认重载赋值运算符是合适的。
②派生类使用new:必须为派生类定义显式析构函数、复制构造函数和赋值运算符。 -
当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数、赋值运算符都必须使用相应的基类方法来处理基类元素。
①对于析构函数,这是自动完成的。
②对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成的;如果不这样做,将自动调用基类的默认构造函数。
③对于赋值运算符,这是通过使用作用域解析运算符显式地调用基类的赋值运算符来完成的。 -
接受一个参数的构造函数可用于将类型与该参数相同的值转换为类(隐式转换)。只有接受一个参数的构造函数才能作为转换函数。
如果给第二个参数提供默认值,它便可用于转换int:
Stonewt(double 1bs);//template for double-to-Stonewt conversion
Stonewt(int stn,double 1bs);//not a conversion function
Stonewt(int stn,double 1bs=0);//int-to-Stonewt conversion
·自动特性可能会导致意外的类型转换,关键字explicit用于关闭这种自动特性这将关闭上述示例中介绍的隐式转换,但仍然允许显式转换,即显式强制类型转换。
explicit Stonewt(double 1bs);//no implicit conversions allowed
- 成员函数或独立的函数返回对象时,可以返回指向对象的引用、指向对象的const引用或const对象。
·返回指向const对象的引用:如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过返回引用来提高其效率。
Vector force1(50,60);
Vector force2(10,70);
Vector max;
max=Max(force1,force2);
//version 1
Vector Max(const Vector & vl, const Vector & v2){
if (vl. magval() > v2. magval())
return v1;
else
return v2;
}
//version 2
const Vector & Max(const Vector & v1, const Vector & v2){
if(v1. magval() > v2. magval())
return v1;
else
return v2;
}
①返回对象将调用复制构造函数,而返回引用不会。版本二所做的工作更少,效率更高。
②v1和v2都被声明为const引用,因此返回类型必须为const,这样才匹配。
··返回指向非const对象的引用:两种常见的返回非const对象情形是,重载赋值运算符以及重载与cout一起使用的<<运算符。
String s1("Good stuff");
String s2,s3;
s3=s2=s1;
cout<<s1<<"is coming!";
①返回String对象或String对象的引用都是可行的,通过使用引用,可避免该函数调用String的复制构造函数来创建一个新的String对象。例子中返回类型不是const,因为方法operator-0返回一个指向s2的引用,可以对其进行修改。
②operator<<(cout,s1)的返回值成为一个用于显示字符串“is coming!”的对象**,返回类型必须是ostream&,而不能仅仅是ostream**。如果使用返回类型ostream,将要求调用ostream类的复制构造函数,而ostream没有公有的复制构造函数。
·返回对象:如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回它。在被调用函数执行完毕时,局部对象将调用其析构函数,当控制权回到调用函数时,引用指向的对象将不再存在。在这种情况下,应返回对象而不是引用。通常,被重载的算术运算符属于这一类。
Vector force1(50,60);
Vector force2(10,70);
Vector net;
net = forcel + force2;
Vector Vector:: operator+(const Vector & b) const {
return Vector(x+b.x,y+b.y);
}
·返回const对象:例如将Vector::operator+()返回类型声明为const Vector,可避免一些误用和滥用。
- ·对象指针小结:
//①使用常规表示法来声明指向对象的指针。
String * glamour;
//②可以将指针初始化为指向已有的对象。
String * first =&sayings[0];
//③可以使用new来初始化指针,这将创建一个新的对象。
String * favorite =new String(sayings[choice]);
//④对类使用new将调用相应的类构造函数来初始化新创建的对象。
String * gleep=new String;//invokes default constructor
String * glop=new String("my my my");//invokes the String(const char*)constructor
//⑤可以使用->运算符通过指针访问类方法。
if(sayings[i].length()<shortest->length())
//⑥可以对对象指针应用解除引用运算符(*)来获得对象。
if(sayings[i]<*first)//compare object values
first=&sayings[i];//assign object address
- 在使用定位new运算符创建第二个对象时,定位new运算符使用一个新对象来覆盖用于第一个对象的内存单元。要使用不同的内存单元,需要提供两个位于缓冲区的不同地址,并确保这两个内存单元不重叠。
pc1=new(buffer)JustTesting;
pc3=new(buffer+sizeof(JustTesting))JustTesting("Better Idea",6);
-
默认构造函数:
①默认构造函数要么没有参数,要么所有的参数都有默认值。如果没有定义任何构造函数,编译器将定义默认构造函数以创建对象。
②如果派生类构造函数的成员初始化列表中没有显式调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。在这种情况下,如果基类没有构造函数,将导致编译阶段错误。
③如果定义了某种构造函数,编译器将不会定义默认构造函数。在这种情况下,如果需要默认构造函数,则必须自己提供。 -
编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。
①按值传递对象涉及到生成临时拷贝,即调用复制构造函数,然后调用析构函数。调用这些函数需要时间,复制大型对象比传递引用花费的时间要多得多。如果函数不修改对象,应将参数声明为const引用。
②在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。 -
返回对象和返回引用:
①在编码方面,直接返回对象与返回引用之间唯一的区别在于函数原型和函数头:
star nova1(const Star&);//returns a star object
star & nova2(const star&);//returns a reference to a Star
②应返回引用而不是返回对象的的原因在于,返回对象涉及生成返回对象的临时副本,这是调用函数的程序可以使用的副本。返回引用可节省时间和内存。直接返回对象与按值传递对象相似,都生成临时副本;返回引用与按引用传递对象相似,调用和被调用的函数对同一个对象进行操作。
③如果函数返回在函数中创建的临时对象,则不要使用引用;如果函数返回的是通过引用或指针传递给它的对象,则应按引用返回对象。
-
使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。两种方法可以让基类的方法在派生类外面可用:
①定义一个使用该基类方法的派生类方法;
②将函数调用包装在另一个函数调用中,即使用一个using声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使采用的是私有派生。 -
类模板:开头将声明用下面代码替换。可以把关建字class看作是变量的类型名,该变量接受类型作为其值,把Type看作是该变量的名称。
template <class Type>
template <typename Type>//newer choice
·不能将模板成员函数放在独立的实现文件中。**模板不能单独编译,必须与特定的模板实例化请求一起使用。**可以将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
- 类模板的具体化:隐式实例化、显式实例化、显式具体化。
①隐式实例化(implicit instantiation):声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义。编译器在需要对象之前,不会生成类的隐式实例化。
ArrayTP<int,100>stuff;//implicit instantiation
ArrayTP<double,30>*pt;//a pointer,no object needed yet
pt=new ArrayTP<double,30>;//now an object is needed
②显式实例化(explicit instantiation):当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。声明必须位于模板定义所在的名称空间中。
即使没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。和隐式实例化一样,也将根据通用模板来生成具体化。
template class ArrayTP<string,100>;//generate ArrayTP<string,100> class
③显式具体化(explicit specialization):需要在为特殊类型实例化时,对模板进行修改,使其行为不同,此时可以创建显式具体化。
//具体化类模板定义格式:
template<>class Classname<specialized-type-name>{..…};
SortedArrays<int>scores;//use general definition
SortedArrays<const char*> dates;//use specialized definition
④部分具体化(partial specialization):即部分限制模板的通用性。
template <class T1, class T2>class Pair{..…};//general template
template <class T1>class Pair<T1, int>{..…};//specialization with T2 set to int
- 模板类声明的友元分3类:非模板友元;约束(bound)模板友元,即友元的类型取决于类被实例化时的类型;非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元。
①非模板友元
在模板类中将一个常规函数声明为友元:
template <class T>
class HasFriend{
public:
friend void counts();//friend to all HasFriend instantiations
…
};
要提供模板类参数,必须指明具体化。
template <class T>
class HasFriend{
friend void report(HasFriend<T>&);//bound template friend
…
};
②约束模板友元
首先,在类定义的前面声明每个模板函数。
template <typename T> void counts();
template <typename T> void report(T&);
然后,在函数中再次将模板声明为友元,这些语句根据类模板参数的类型声明具体化。
template <typename TT>
class HasFriendT{
friend void counts<TT>();
friend void report<>(HasFriendT<TT>&);//也可以使用report<HasFriendT<TT> >(HasFriendT<TT>&)
};
最后,为友元提供模板定义。
③非约束模板友元
template <typename T>
class ManyFriend{
…
template <typename C,typename D>friend void show2(C&,D&);
};