您的位置:首页 > 编程语言 > C语言/C++

重读C++ Primer,记录一些之前不易觉察的知识点(15. 面向对象编程)

2013-09-04 13:15 399 查看
1.基类和派生类

派生类必须对基类中想要重定义的每个继承成员进行声明。

一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用 virtual 保留字,但不是必须这样做。

派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。

C++ 中的函数调用默认不使用动态绑定。要触发动态绑定,满足两个条件:第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;第二,必须通过基类类型的引用或指针进行函数调用。

派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。

像其他任何函数一样,虚函数也可以有默认实参,并且基类和派生在声明时,都可以指定同一虚函数的参数默认值。但是在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。

可以进行派生类型到基类类型转换,包括:应用、指针和对象形式,但不要进行基类类型到派生类类型的任何形式转换。也不建议派生类转换为基类时采用对象形式进行初始化或赋值。进行转换操作时请使用static_cast和dynamic_cast。static_cast可以保证上行转换(把子类的指针或引用转换成基类表示)的安全性,但不保证下行转换安全性。如果非要进行下行转换,请使用dynamic_cast。

2.公用、私有和受保护的继承

如果是公用继承,基类成员保持自己的访问级别:基类的 public 成员为派生类的 public 成员,基类的 protected 成员为派生类的 protected 成员。

如果是受保护继承,基类的 public 和 protected 成员在派生类中为 protected 成员。

如果是私有继承,基类的的所有成员在派生类中为 private 成员。

使用 private 或 protected 派生的类不继承基类的接口,相反,这些派生通常被称为实现继承。

迄今为止,最常见的继承形式是 public。

派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松。

class Base {
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
};

class Derived : private Base {
public:
// maintain access levels for members related to the size of the object
using Base::size;
protected:
using Base::n;
// ...
};
为了使 size 在 Derived 中成为 public,可以在 Derived 的 public 部分增加一个 using 声明。这样改变 Derived 的定义,可以使 size 成员能够被用户访问,并使 n 能够被从 Derived 派生的类访问。

3.struct和class的区别

struct 和 class 保留字定义的类具有不同的默认访问级别,同样,使用哪个保留字定义派生类时,默认继承访问级别也不相同。使用 class 保留字定义的派生默认具有 private 继承,而用 struct 保留字定义的类默认具有 public 继承:

class Base { /* ... */ };
struct D1 : Base { /* ... */ };   // public inheritance by default
class D2 : Base { /* ... */ };    // private       inheritance by default
有一种常见的误解认为用 struct 保留字定义的类与用 class 定义的类有更大的区别。唯一的不同只是默认的成员保护级别和默认的派生保护级别,没有其他区别:

class D3 : public Base {
public:
/* ... */
};
// equivalent definition of D3
struct D3 : Base {      // inheritance public by default
/* ... */           // initial member access public by default
};
struct D4 : private Base {
private:
/* ... */
};
// equivalent definition of D4
class D4 : Base {   // inheritance private by default
/* ... */           // initial member access private by default
};
尽管私有继承在使用 class 保留字时是默认情况,但这在实践中相对罕见。因为私有继承是如此罕见,通常显式指定 private 是比依赖于默认更好的办法。显式指定可清楚指出想要私有继承而不是一时疏忽。

4.友元关系与继承

友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。

每个类控制对自己的成员的友元关系,基类的友元对从该基类派生的类型没有特殊访问权限。如果派生类想要将自己成员的访问权授予其基类的友元,派生类必须显式地这样做。同样,如果基类和派生类都需要访问另一个类,那个类必须特地将访问权限授予基类的和每一个派生类。

class Base {
friend class Frnd;
protected:
int i;
};
// Frnd has no access to members in D1
class D1 : public Base {
protected:
int j;
};
class Frnd {
public:
int mem(Base b) { return b.i; }  // ok: Frnd is friend to Base
int mem(D1 d) { return d.i; }    // error: friendship doesn't inherit
};
// D2 has no access to members in Base
class D2 : public Frnd {
public:
int mem(Base b) { return b.i; } // error: friendship doesn't inherit
};


5.继承与静态成员

如果基类定义 static 成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个 static 成员只有一个实例。static 成员遵循常规访问控制:如果成员在基类中为 private,则派生类不能访问它。假定可以访问成员,则既可以通过基类访问 static 成员,也可以通过派生类访问 static 成员。一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。

struct Base {
static void statmem(); // public by default
};
struct Derived : Base {
void f(const Derived&);
};
void Derived::f(const Derived &derived_obj)
{
Base::statmem();      // ok: Base defines statmem
Derived::statmem();   // ok: Derived in herits statmem
// ok: derived objects can be used to access static from base
derived_obj.statmem();     // accessed through Derived object
statmem();                 // accessed through this class


6.尊重基类接口

构造函数只能初始化其直接基类的原因是每个类都定义了自己的接口。一旦类定义了自己的接口,与该类对象的所有交互都应该通过该接口,即使对象是派生类对象的一部分也不例外。同样,派生类构造函数不能初始化基类的成员且不应该对基类成员赋值。如果那些成员为 public 或 protected,派生构造函数可以在构造函数函数体中给基类成员赋值,但是,这样做会违反基类的接口。派生类应通过使用基类构造函数尊重基类的初始化意图,而不是在派生类构造函数函数体中对这些成员赋值。

7.复制控制和继承

构造函数和复制控制成员不能继承。

类是否需要定义复制控制成员完全取决于类自身的直接成员。基类可以定义自己的复制控制成员,而派生类使用默认生成的版本,反之亦然。

如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分:

class Base { /* ... */ };
class Derived: public Base {
public:
// Base::Base(const Base&) not invoked automatically
Derived(const Derived& d):
Base(d) /* other member initialization */ { /*... */ }
};
初始化函数 Base(d) 将派生类对象 d 转换为它的基类部分的引用,并调用基类复制构造函数。如果省略基类初始化函数,如下代码:

// probably incorrect definition of the Derived copy constructor
Derived(const Derived& d) /* derived member initizations */
{/* ... */ }
效果是运行 Base 的默认构造函数初始化对象的基类部分。假定 Derived 成员的初始化从 d 复制对应成员,则新构造的对象将具有奇怪的配置:它的 Base 部分将保存默认值,而它的 Derived 成员是另一对象的副本。

赋值操作符通常与复制构造函数类似:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。

// Base::operator=(const Base&) not invoked automatically
Derived &Derived::operator=(const Derived &rhs)
{
if (this != &rhs) {
Base::operator=(rhs); // assigns the base part
// do whatever needed to clean up the old value in the derived part
// assign the members from the derived
}
return *this;
}
析构函数的工作与复制构造函数和赋值操作符不同:派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员。对象的撤销顺序与构造顺序相反:首先运行派生析构函数,然后按继承层次依次向上调用各基类析构函数。

要保证运行适当的析构函数,基类中的析构函数必须为虚函数。像其他虚函数一样,析构函数的虚函数性质都将继承。因此,如果层次中根类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用默认析构函数,派生类析构函数都是虚函数。

基类析构函数是三法则的一个重要例外。三法则指出,如果类需要析构函数,则类几乎也确实需要其他复制控制成员。基类几乎总是需要构造函数,从而可以将析构函数设为虚函数。如果基类为了将析构函数设为虚函数则具有空析构函数,那么,类具有析构函数并不表示也需要赋值操作符或复制构造函数。

8.纯虚函数

将函数定义为纯虚能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本决不会调用。重要的是,用户将不能创建该类型的对象,如果试图创建,将发生编译时错误。含有(或继承)一个或多个纯虚函数的类是抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象。

9.继承情况下的 作用域

名字查找是在编译时发生,所以使用基类类型的引用或指针访问派生类型的成员时会发生编译时错误。

与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。虽然可以使用作用域操作符访问被屏蔽的基类成员,但是最好避免派生类和基类成员的名字冲突。

像其他任意函数一样,成员函数(无论虚还是非虚)也可以重载。如果派生类重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员。即派生类如果想通过自身类型使用重载版本,则派生类必须要么重定义所有重载版本,要么一个也不重定义。派生类不用重定义所继承的每一个基类版本的方法是:为重载成员提供
using 声明。即通过using 声明将基类中该函数的所有重载实例加到派生类的作用域,然后派生类只需要重定义本类型确实必须定义的那些函数,对其他版本可以使用继承的定义。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: