代码改变世界

C++的那些事:面向对象

2014-06-11 11:23  ☆Ronny丶  阅读(2102)  评论(0编辑  收藏  举报

1 OOP概述

面向对象基于三个基本概念:数据抽象、继承和动态绑定。通过使用数据抽象,我们可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。

1.1 继承

继承是派生类与基类之间的关系,它们共享了一些公共的东西,而派生类特化了一些本质不同的东西。类与类之间的继承关系构成了继承层次。在C++中,基类必须指定希望派生类重定义哪些函数,定义为virtual的函数是基类期待派生类重新定义的,需要在派生类中继承的不能定义为虚函数。

派生类重新定义的虚函数前面可以加virtual,但这不是必要的,在C++11中,允许派生类显式地注明它将使用哪个成员函数改写基类虚函数,只用在该函数形式参数列表后增加一个override关键字。

1.2 动态绑定

当函数接口定义了基类的引用(或指针)时,在函数内调用虚函数时,发生动态绑定。因为这时候函数实参可以为基类类型也可以为派生类类型,虚函数只有在运行阶段才能确定需要调用哪个定义。

2 定义基类与派生类

2.1 定义基类

基类成员函数中希望在派生类中重新定义的函数前面加了virtual,这类函数在调用时,在程序运行阶段确定。任何构造函数之外的非静态函数都可以是虚函数。virtual关键字只需要在基类定义的时候加在需要重定义的函数前面,实现的时候不需要。

基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。

派生类可以继承定义在基类中的成员,但是派生类的成员函数不能访问从基类继承而来的private成员,只能访问public和protected成员。而用户则只能通过派生类访问从基类继承而来的public成员。

pubic:用户可访问的类成员,继承类也可以访问它们。

private:类的成员和友元可以访问,继承类不能访问。

protected:用户不能访问,但是可以被类的成员或派生类的成员访问。

2.2 定义派生类

1)派生类的成员变量分两种:一是从基类继承到的成员变量,另一种是显示自己特殊化的变量或者为特殊化接口而准备的变量。

2)一般来说派生类都要重新定义基类中声明的虚函数,但如果没有重新定义,则延用基类里的定义。

3)派生类中的虚函数定义与基类应该完全一致,如果基类返回基类类型的引用,派生类则返回派生类类型的引用。

4)派生类中定义虚函数时,可以不保留virtual。

5)作为基类的类必须是已经定义的,不能仅仅是声明,因为它的成员将被用到,所以一个类不能从自身派生出一个类。

2.3 virtual与其他成员函数

发生动态绑定的两个条件:

1)虚函数

2)基类类型的引用或指针进行函数调用

在任何需要基类的地方都可以用派生类对象去代替,所以指向基类的指向或引用可以指向派生类,因为派生类有所有它需要的成员或成员函数。

对象是非多态的——对象类型已知且不变。对象的动态类型是与静态类型相同,这一些与引用或指针相反。运行的函数是由对象的类型定义的。
如果派生类需要调用虚函数的基类版本,则需要用作用域操作符来覆盖虚函数机制。

不要在基类和派生类中为虚函数定义不同的默认实参,因为实参是静态绑定的,在编译时期确定,所以当你用指向派生类对象的基类类型的指针访问虚函数时,实际上用的是基类成员函数的默认实参。

2.4 访问控制与继承

派生列表中使用访问标号用来决定使用派生类的用户和对基类成员访问的权限。

首先,基类中只有public和protected的成员可以被派生类访问。

1) 如果派生类是公用继承(public inheritance)

派生类的成员或友员可以访问基类中的public和protected成员,用户通过派生类可以访问基类中的public成员,基类中的private只有基类的成员可以访问。

class Bulk_item:public Item_base{...};

那么基类中的public与protected就像派生类中的public和protected类型一样。

2)如果派生类是受保护继承(protected inheritance)

class Bulk_item:protected Item_base{...};

基类中的public和protected成员 就像派生类的中protected一样

3)如果派生类是私有继承(private inheritance)

class Bulk_item:private Item_base{...};

基类中的public和protected成员 就像派生类的中private一样,用户不能通过派生类对象访问基类中的任何成员。

public派生类继承了基类的接口,可以在需要基类的地方使用public派生类。而private和protected派生类则不可以。

可以用using 声明恢复基类成员在派生类中的访问级别。

class和struct在定义继承类的,默认的继承类别也不同。

class Base{...};

class D1:Base{...} 等价于 class D1:private Base{...}

struct D2:Base{...} 等价于 class D2:public Base{...}

class 与 struct 用来定义类除了访问级别的不同外,其他没有任何不同。

 

派生类的成员函数中不能直接访问基类类型对象的protected成员,但是可以通过派生类对象访问基类的protected成员。

void Bulk_item::memfcn(const Bulk_item &d, const Item_base &b)
{
    // price is protected 
    double ret = price; //ok
    ret = d.price;//ok
    ret = b.price;//error
}

2.5 继承与静态成员

如果基类定义了static成员,则在整个继承层次中只有一个这样的成员。

2.6 防止继承的发生

有时候我们会定义这样一种类,我们不希望其他类继承它,或者不想考虑它是否适合作为一个基类。C++11中允许在类名后加一个关键字final来防止继承。

Class NoDerived final{/* */ }
Class Bad : public NoDerived{/* */ } // error!

3 派生类与基类之间的转换

可以将基类的指针或引用绑定到派生类对象上有一层极为重要的含义:当使用基类的引用(或指针)时,实际上我们并不清楚该引用(或指针)所绑定对象的真实类型。该对象可能是基类的对象,也可能是派生类的对象。

3.1 静态类型与动态类型

表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型,动态类型直到运行才可知。如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。

3.2 派生类转换为基类

引用转换不同于对象转换

1)将派生类对象传递给希望接受基类引用的函数,实际上传递进去的就是原来的派生类对象,这个对象没有发生任何变化。

2)而如果将派生类对象传递给希望接受基类对象的函数,实际上是将实参派生类对象中基类部分复制出来,创建了一个临时的基类对象。

3)派生类对象转换为基类对象实际上像是做了“裁切”操作。

3.3 基类转换为派生类

没有从基类类型到派生类型的(自动)转换,因为派生类中很可能包含了基类中没有成员。甚至当基类指针或引用实际绑定到派生类对象时,从基类到派生类的转换也存在限制。

Bulk_item bulk;
Item_base *itemP = &bulk; // ok
Bulk_item *bulkP = itemP; // error

4 虚函数

1)引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。
2)基类中的虚函数在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。

3)可以是函数的形参表后加override说明函数是派生类中的虚函数,而用final用于说明不希望在继承类中覆盖该函数。

4)如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。

double undiscounted = baseP->Quote::net_price(42);

5)如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。

5 抽象基类

1)在虚函数的形参表后加=0会让虚函数变为纯虚函数,纯虚函数本身不需要定义,它只是为派生类提供一个接口用于表示抽象普适的意义。值得注意的是,我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。

2)含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。我们不能定义一个抽象基类的对象。

3)派生类构造函数只初始化它的直接基类。

class Disc_quote :public Quote
{
public:
    Disc_quote() = default;
    Disc_quote(const string& book, double price, size_t qty, double disc) :
        Quote(book, price), quantity(qty), discount(disc){}
    double net_price(size_t)const = 0;
protected:
    size_t quantity = 0;
    double discount = 0.0;
};
class Bulk_quote :public Disc_quote
{
public:
    Bulk_quote() = default;
    Bulk_quote(const string &book, double price, size_t qty, double disc) :
        Disc_quote(book, price, qty, disc){}
    double net_price(size_t)const override;
};

6 继承情况下的类作用域

1)名字的查找发生在编译阶段,也就是说不论是对象,指针还是引用,都只能访问它们静态类型的成员。

2)当派生类中定义了同基类同名的成员时,基类中的成员将会被屏蔽,当然可以使用作用域操作符进行访问。

3)在派生类中定义的同名函数,就算形参不一样,也会被屏蔽。原理同:局部作用域定义的函数不会重载全局作用域中定义的函数。

4)如果想让派生类使用所有的重载成员,则在派生类里要么不定义,要么全定义。

7 构造函数与复制控制

因为派生类中实际上包含了基类的一些成员,所以这将会影响到派生类的构造、复制、移动、赋值和撤销。

7.1 构造函数与继承

构造函数与复制控制成员不能继承,每个类都必须有自己的构造函数和复制控制成员。

但是在C++11中,派生类能够和其直接基类定义的构造函数,但是也仅限于此,仍然不能继承默认、拷贝和移动构造函数。

派生类继承基类构造函数的方式是提供一条注明了基类名的using声明语句,这样对于基类的每个构造函数,编译器都生成一个与之对应的派生类的构造函数。

class Bulk_qute : public Disc_quote{
public:
    using Disc_quote::Disc_quote; // 继承Disc_quote的构造函数
    double net_price(size_t) const;
};

如果派生类定义的构造函数与基类构造函数具有相同的参数列表,则该构造函数将不会被继承。

7.2 基类的构造与复制控制函数

基类的构造与复制函数基本不受影响,唯一的影响是,在确定哪些构造函数时,需要考虑使用对象,可以在构造函数前加protected,让它只能在派生类中使用。

7.3派生类的构造函数

1)合成的派生类默认构造函数

两个步骤:1,调用基类的默认构成函数完成基类成员的初始化;2,用常规变量初始化规则初始化派生类的特有成员。

2)定义默认构造函数

可以只给派生类的特有成员赋初值,而函数会隐式调用基类的构造函数对基类成员进行定义。

3)向基类构造函数传递实参

派生类的构造函数初始化列表,不能直接初始化派生得到的成员,只能将基类包含在初始化列表中来间接初始化那些成员。

Bulk_item(const string& book, double sales_price, size_t qty = 0, double disc_rate = 0) : Item_base(book, sales_price), min_qty(qty), discount(dis_rate){}

4)只能初始化直接基类

在有多重继承的性况下,派生类的构造函数只能初始化自己的直接基类。

5)尊重基类接口

虽然可以在派生类的构造函数体中直接访问基类的public和protected成员,从而进行初始化,但是不要这样做,而是使用基类提供的构造函数接口。

7.3 复制控制和继承

7.3.1 定义派生类的复制构造函数

如果派生类显式定义了自己的复制构造函数或赋值操作符,则该定义将完全覆盖默认定义。基类的部分也必须由派生类定义的函数来完成,不能希望有隐式的复制和赋值(这一点与构造函数不同)。

在派生类复制构造函数中,会显式的调用基类的复制构造函数,因为它的形参是基类类型的引用,所以可以直接将派生类对象作为实参传入。

如果没有显式调用,那么将会隐式地调用基类的默认构造函数完成基类部分成员的定义,这样最终得到的派生类对象就很奇怪了。

7.3.2 派生类赋值操作符

与复制构造函数类似,必须显式地调用基类的基类的赋值函数。

Derived& Derived::operator=(const Derived &rhs)
{
    if (this != &rhs)
    {
        Base::operator=(rhs);
        // then do
    }
    return *this;
}
7.3.3 派生类的析构函数

派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员。

7.3.4 虚析构函数
Item_base *itemP = new Item_base;
delete itemP;
itemP = new Bulk_item;
delete itemP;

上面代码中,在delete指向类的指针时,将调用析构函数,可是这里itemP可能指向基类也可能指向派生类,所以这时候应该在运行阶段根据类型去调用不同的析构函数。

所以基类的析构函数为虚函数:virtual ~Item_base(){}

7.3.5 在构造函数和析构函数中调用虚函数

运行的是为构造函数或析构函数自身类型定义的版本。