《C++ Primer》读书笔记——第十五章_面向对象程序设计_2

OOP基于三个基本盖面:数据抽象、继承和动态绑定。 
以下的例子:基类Quote->派生类Bulk_quote 
对于某些类,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明称虚函数(virtual function)。


class Quote
{
public:
    std::string isbn() const;
    virtual double net_price(std::size_t) const;
}




派生类必须通过派生类列表来明确指出它是从哪个(哪些基类)继承而来的。 
每个基类前面可以有访问说明符public或private或protected
class Bulk_quote : public Quote  //Bulk_Quote公有继承了Quote
{
public:
    double net_price(std::size_t) const override;    
}




因为Bulk_quote公有继承Quote,我们完全可以把Bulk_quote的对象当成Quote的对象来用。 


如果派生类没有覆盖它继承的虚函数,那么派生类会直接继承其在基类中的版本。


派生类覆盖他继承的虚函数时,前面的virtual可有可无。
net_price后的override显式地注明这个函数将改写基类的虚函数。(可能派生类有多个同名函数,但只有声明override的那个是虚函数??)


override只是为了显式的注明将,增加可读性。还可以防止一些微小难以发现的错误:http://blog.csdn.net/liyuanbhu/article/details/43816371


动态绑定 
通过使用动态绑定,我们能用同一段代码分别处理Quote 和 Bulk_quote的对象。


void f(Quote &item){ }


因为item的形参是基类Quote的一个引用(引用或者指针才能实现多态/动态绑定),所以我们既能用基类的对象调用该函数,也能使用派生类的对象调用该函数。
Quote base;
Bulk_quote derived;
f(base); //调用base的f
f(derived); //调用derived的f


在运行时选择函数的版本成为动态绑定(dynamic binding),也成运行时绑定(run-time binding)。


用基类的引用或者指针调用一个虚函数时将发生动态绑定。


15.2 定义基类和派生类


class Quote
{
public:
    Quote() = default;
    Quote(const std::string &book, double sales_price) :
        bookNo(book), price(sales_price) { }
    std::string isbn() const {return bookNo;}
    //返回定数量的书籍的销售总额
    //派生类负责改写并使用不同的折扣计算算法
    virtual double net_price(std::size_t n) const
        { return n*price; }
    virtual ~Quote() = default;  //对析构函数进行动态绑定
private:
    std::string bookNo;
protected:
    double price = 0.0;
};


基类通常都应该定义一个虚析构函数,即使函数不执行任何操作也是如此(用于给派生类销毁派生类独有的那部分内存,否则只能销毁派生类中从基类继承而来的那部分内存)。
成员函数与继承


当遇到如net_price这样的和类型有关的操作时,派生类必须对其重新定义。换句话说,派生类需要对这些操作提供自己的新定义以覆盖(override)从基类继承而来的旧定义。


指针和引用才能动态绑定。


关键字virtual只能出现在类内的声明语句之前而不能用于类外部的函数定义。(static也不能)


如果基类把一个函数声明为virtual,则该函数在派生类中也是隐式virtual的。也可以显式在声明一次virtual。


虚函数的解析过程发生在运行时,非虚函数解析过程发生在编译时。


构造函数和static成员函数不能是virtual的。因为:


(1) static 函数和实例无关,只和类有关,可以把static成员看成某个namespace里的函数。


(2) virtual函数要用到虚函数表(vtable),而vtable是在构造函数中建立的。所以调用构造函数的时候还没有vtable,不能把构造函数设为virtual。






访问控制与继承:


protected成员: 能被派生类访问,不能被其他用户访问。






15.2.2


 定义派生类 


class Bulk_quote : public QUote
{
public:
    Bulk_quote() = default;
    Bulk_quote(const std::string&, double, std::size_t, double);
    //覆盖基类的函数版本以实现基类大量购买的折扣政策。
    double net_price(std::size_t) const override;
private:
    std::size_t min_qty = 0; //适用折扣政策的最低购买量
    double discount = 0.0;  //以小数表示的折扣额
};


如果派生是公有的,那么基类的公有成员也是派生类接口的组成部分。
如果派生类是struct,那么默认从基类到派生类是public继承。
如果派生类是class,那么默认从基类到派生类是private继承。


如果一个派生类只继承自一个基类,那么称为单继承。


new返回的是指针
Foo f = new Foo(); //错误,类型不匹配
Foo *f = new Foo(); //正确



派生类不一定会覆盖他继承的虚函数,如果派生类没有覆盖其基类的某个虚函数,那么这个函数在派生类的表现和其他普通成员函数没有区别(功能和在基类中一样)。


c++11允许显式表示函数使用某个成员参数覆盖了它继承的虚函数,方法是在函数体前加override(其实不加也可以覆盖,override只是显式表示)。


c++标准并没有明确规定派生类的对象在内存中如何分布,但是我们可以认为如下




继承自基类的部分和派生类自定义的部分不一定是连续存储的,上图只是概念模型,而非物理模型。

Quote item; //基类对象
Bulk_quote bulk; //派生类对象
Quote *p  = &item;  //p指向Quote对象
p = &bulk;          //  p 绑定到bulk的Quote部分
Quote &r = bulk;  // r 绑定到bulk的Quote部分


这种转换通常称为派生类到基类的转换。
这种隐式特性意味着我们可以把派生类对象或者派生类对象的引用用在需要基类引用的地方,同样的,我们也可以把派生类对象的指针用在需要基类指针的地方。(但是只有引用和指针才能多态)。
在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键所在。
派生类并不能直接构造初始化基类的成员,而是要调用基类的构造函数来初始化它的基类部分。(不算是委托构造函数,委托构造函数是同类之间的)
struct Base
{
    int x;
    Base(int t):x(t){}
};



struct Derived : public Base
{
    int y;
    Derived():Base(10), y(20){}
};


如果派生类构造函数不写Base(10)的话,会报错,因为Derived默认构造函数会调用Base的默认构造函数,而Base的默认构造函数没了(被Base(int t) 隐藏了)。


首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。


派生类可以访问基类的公有成员和受保护成员。
要想与类的对象交互必须使用类的接口,即这个对象是派生类的基类部分也是如此。所以派生类不能直接初始化基类的成员。而是用调用基类的接口(构造函数)初始化基类。


继承和静态成员:
如果基类中定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义(但是基类能不能访问就是另一回事了,private)。不论从基类中派生出多少个派生类,对于每个静态成员来说都只存在唯一的实例。
静态成员也有public,private,protected之分。


class Base
{
public:
    static void statmem();
};
class Derived : public Base
{
    void f(const Derived&);
};




如果基类成员是private的,则派生类无权访问。假设某静态成员是可访问的,我们可以通过基类使用它,也可以通过派生类使用它。
class A
{
protected:
    static int x;
public:
    A(){cout << x << endl;}
};
int A::x = 1 ;                         //为什么这一行不报错???????????
class B : public A
{
  public:  B(){cout << x <<endl;}
};


protected的static成员要在类外声明才可用。private的在类外声明不了(相当于只能本类在用,继承也不行?)。




void Derived::f(const Derived &derived_obj)
{
    Base::statmem(); //正确
    Derived::statmem(); //正确
    derived_obj.statmem(); //通过Derived访问
    statmem();  //通过this对象访问
}



派生类的声明中包含类名,但是不包含它的派生列表。
class Bulk_quote : public Quote;  //错误:派生列表不能出现在这里
class Bulk_quote;   //正确;声明派生类的正确方式。
一条生命语句的目的是令程序知晓某个名字的存在以及该名字表示一个什么样的实体,如一个类、一个函数或一个变量等。派生列表以及与定义有关的其他细节必须与类的主题一起出现。


如果派生类中有和基类名字相同的成员,派生类的成员会把基类的隐藏。
class Base
{
public:
    int x;
    Base(int t):x(t){}
};
class Derived : public Base
{
public:
    int x;
    Derived(int t):Base(t){}
};
int main()
{
    Derived d(20);
    cout << d.x << endl;
    return 0;
}





会输出一个垃圾值,因为Derived(int t) : Base(t) {} 中初始化的是Base中的x,而不是Derived中的x。要想初始化Derived中的x就要Derived(int t) : x (t)  { } 




如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。(类定义之后才能被继承)因为派生类必须要知道基类成员是什么。还隐含的意思就是:一个类不能派生它本身。
class Base;
class Derived : public Base    //错误
{
};



一个类是派生类,同时他也可以是其他类的基类(派生链)。(基类和派生类的关系是相对的)
每个类都会继承直接积累的所有成员,所以继承链的顶端的那个派生类,将包含了它的直接基类的子对象以及每个间接基类的子对象。


在一个类定义中加入final可防止类被继承:在类名后接final 
c++ 11 之前的防止继承的方法   http://www.cnblogs.com/kingstarspe/archive/2013/06/06/virtualpublic.html
class NoDerived final { /* */ };      //派生链一开始就断了
class Base {/* */};
class Last final : Base {/* */};     、、派生链在在中间断
class Bad : NoDervied {/* */};
class Bad2 : Last {/* */}; 




15.2.3 类型转换与继承
通常情况下,如果我们想把引用或指针绑定到一个对象上,则引用或指针的类型应予对象的类型一致,或者对象的类型含有一个可接受的const类型转换规则。存在继承关系的类是一个重要的例外:我们可以将基类的指针或引用绑定到派生类对象中。

Base *p =  new Bulk_quote(); 
Bulk_quote bq;
Base &r = bq;


所以使用基类的指针和引用时,我们不知道该引用或指针所绑定对象的真实类型。该对象可能是基类的对象,也可能是派生类的对象。


静态类型与动态类型:
静态类型:编译时已知,是自身的类型。如:Base b;
动态类型:运行时才知,是该对象(所指向的)内存中的变量或表达式表示的内容。如Base *r = Derived();
变量是指针或者引用时,才可能有静态类型和动态类型的不一样。如果表达式既不是变量也不是指针,那么它的动态类型永远和静态类型一致(没有多态)。如Quote类型的变量永远是一个Quote对象。


因此,基类的指针或引用的静态类型可能与其动态类型不一致。


不存在基类向派生类的隐式类型转换。(不可由简单往复杂的转)。因为派生类一定包含基类的成分,而基类却不一定包含派生类的成分。否则我们可能将会访问到不存在的成员。


派生类对基类的类型转换只对指针和引用类型有效。


对象之间不存在类型转换(即 Base b = Derived();  b.f() 调用的还是 Base::f( ))


struct Base
{
    int x;
    Base() = default;
    Base(const Base& b){cout << "Base" << endl;}
};
struct Derived : Base
{
    int y;
    Derived() = default;
    Derived(const Derived& d) {cout << " Derived" << endl;}
};
int main()
{
    Derived d;
    Base b(d); //输出Base
    return 0;
}


即使我们给基类的拷贝构造函数传一个派生类的实参,拷贝构造函数执行的还是基类的那一个,该构造函数只能处理基类自己的成员。
Bulk_quote bulk;   //派生类对象
Quote item(bulk);   //执行Quote::Quote(const Quote&) ; 该函数只能处理基类中的成员,忽略派生类中的成员
item = bulk;      //执行 Quote::operator=(const Quote&); 该函数只能处理基类中的成员,忽略派生类中的成员
因为上述过程会忽略Bulk_quote部分,所以我们称bulk的Bulk_quote 部分被切掉(slice down)了。


当我们用一个派生类对象为一个基类对象初始化或赋值时,只有给派生类对象中的基类部分会被拷贝、移动或赋值,他的派生类部分将被忽略掉。


派生类到基类的转换也可能由于访问受限而不可行(p544).


15.3
 虚函数
虚函数一定要有定义,或者标为 =0(纯虚函数)。
通常我们不用某个函数就不必要把它定义,声明即可。但虚函数除外!!!!!!!!!!因为连编译器都不知道用到了哪个虚函数(运行时才会报错,编译期不报错)。


对虚函数的调用可能在运行时才被解析。
动态绑定只有我们通过【指针或引用】调用【虚函数】时才会发生!(关键字:指针  引用  虚函数)
引用或指针的动态类型与静态类型不同是c++多态性的根本所在。
通过对象调用的虚函数或非虚函数也在编译期绑定。因为是对象,而不是指针或引用,对象的静态类型和动态类型相同。


派生类中的虚函数:
派生类中的虚函数前的virtual写不写都可,默认为虚函数了。
派生类中的函数如果覆盖某个继承而来的虚函数,则它的形参类型必须是与被它覆盖的基类函数完全一致。否则就是派生类中定义的新函数,且隐藏原函数,可用override防止不小心写错了参数列表或返回值这种错误,因为加了override之后就肯定是要覆盖基类中的虚函数了。


基类有同名的虚函数时,才能在派生类中写override。


派生类中虚函数的返回类型必须和基类函数相一致。但当类的虚函数返回类型是类本身的指针或引用时例外,上述规则无效。如果D由B派生得到,那么即使基类虚函数返回类型为B* 或者B& , 派生类虚函数返回类型也可以是D*或者D&。(条件是D到B的转换是可访问的。(貌似只有public继承可以), 类型转换的可访问性见p544)。


struct B
{
    virtual void f1(int) const;
    virtual void f2(){};
    void f3();
};




struct D1 : B
{
    void f1(int) const override; //正确
    //void f2(int) override;   //错误,没有const,找不到匹配,但又声明了override
    //void f3() override;    // 错误,B中的f3 不是虚函数
    //void f4() override;    //错误,B中无f4。
};


我们使用override的意思是我们希望覆盖该函数,而不是重新定义,并且隐藏原来那个。


override 不能再类外出现!!!!!!!!!!!!!!!!
struct D2 : B
{
    //从B继承f2() 和f3(), 覆盖f1(int)
    void f1(int) const final;


};
struct D3 : D2
{
    void f2();     //正确,覆盖从间接基类B继承而来的f2
    void f1(int) const; //错误,D2已经将f2声明为final
};




在派生类中,要么不写虚函数的声明(直接隐式使用基类的定义),一旦写了声明,就必须把定义也写了,否则虚函数表会出错。
定义与声明的本质区别:有无内存分配
final和override出现在形参列表(包括const和引用修饰符)以及尾置返回类之后


虚函数允许有默认实参,实参值由本次调用的静态类型决定。
Base ===》 virtual void show(int x = 0) { cout << x << endl;}
Derived ===》virtual void show(int x = 1) {cout << x << endl; cout << "sb" << endl;}
Base *b = new Derived();
b-show();    //输出静态类型的形参 0 和 字符 sb.  除了默认形参用的是静态类型的,其他函数体什么的都是用动态类型的。


回避虚函数的机制:
我们可以通过作用域运算符来回避虚函数:
Base *p = Derived();
p -> Base::f();   //执行的是基类的f().



一个派生类的虚函数调用基类的虚函数时,一定要加Base::符号,否则会导致派生类虚函数调用自己,无限递归,直至爆栈。


抽象基类:
将基类的某个成员函数定义为纯虚的(pure virtual),这个基类就是抽象类。
在类成员函数的声明语句前加上 = 0,就可以将一个虚函数声明为纯虚函数。和virtual、override和static一样,= 0 只能出现在类内部。
虽然一个抽象基类不能定义对象,但是这个基类的派生类的构造函数会调用基类的构造函数来初始化派生类中的基类部分。所以该定义的函数还是要正常定义(实现)。


我们也可以为纯虚函数提供定义,不过函数体必须定义在泪外部。
含有(或者未经覆盖直接继承)抽象函数的类是抽象基类。
抽象基类负责定义接口。派生类可以覆盖该接口。不能创建基类对象,因为纯虚函数没有实现。
派生类的构造函数只初始化它的直接基类。


假设有:
struct Base
{
    int x;
    Base(int t) : x(t) {} 
    void f(){}
};
struct D1:public Base  //抽象类
{
    int y;
    virtual void f() = 0;
}; 
struct D2 : public D1{};



D2没有自己的数据成员,但它依然需要一个接受一个参数的构造函数,该函数将他的实参传递给D1的构造函数,随后D1的构造函数继续调用Base的构造函数,Base的构造函数首先初始化Base的x,当Base的构造函数结束后,开始运行D1的构造函数并初始化y成员,最后运行D2的构造函数,该函数无须执行实际的初始化操作。
protected :只有 【自身】、【派生类】和【友元】能访问pritected,
private: 只有【自身】和【友元】可访问。
public:谁都能访问。
派生类能访问protected指的是能访问自身从基类继承下来的protected部分。而不是说给一个派生类的函数传进去一个基类的对象就能直接访问基类的pritected成员。
class Base
{
protected:
    int x;
};


class Derived : public Base
{
    friend void f(Derived &d); //能访问Derived::x;
    friend void f(Base &b); //不能访问Base::x;
};
void f(Derived &d){cout << d.y << endl;} //正确
void f(Base &b){cout << b.x << endl;}  //错误


原因很简单,如果不这样规定的话,我们只要随便公有继承一个类,就可以访问它的protected对象,从而破坏封装性。
总结:派生类的成员和友元只能访问派生类对象中基类部分的受保护成员;对于普通的基类中的成员不具有特殊的访问性。




三种继承方法:
某个类对其继承而来的成员的访问权限收两个因素影响:(1)该成员在基类中的访问说明符。(2)派生类的派生列表中的访问说明符。
类似 Z = min(X, Y),X和Y分别为在基类中的访问权限和派生类型对应的访问权限。只有protected 和 public能被访问,一旦X 或 Y其中一个为private,  就不可访问了。 在派生类中就是private的了。(可访问,但只有派生类的成员函数和友元可访问)
如果D继承B的方法是private的,那么,B的成员或友元还是可以访问原来是public和protect的成员的,但从D继续派生出去的类就不可访问了,无论是成员还是友元。
class A
{
X:  //.......
}
class B : Y A
{
Z = min(X, Y)
Z表示成员在B中的可访问性。
}

class Base
{
public:
    int x;
protected:
    int y;
private:
    int z;
};




struct Pub_Derv : protected Base
{
    int f1() {return x;}//正确
    int f2() {return y;}//正确
    int f3() {return z;}//错误
};
struct Priv_Derv : private Base
{
    // private不影响派生类的访问权限。但在之后,xyz都变成private的了。
    int f1() {return y;}
};




派生类访问说明符对于派生类的成员(及友元)能否访问其【直接基类】没有什么影响。对基类成员的访问权限只与基类中的访问说明符有关。


Pub_Derv d1;  //继承自Base的成员是public的
Priv_Derv d2;  //继承自Base的成员是private的
d1.pub_mem(); //正确
d2.pub_mem(); //错误,只能通过公有成员或友元访问private部分。



继承说明符只是指明了在继承之后,类中的成员的可访问性(只能“越来越”private,不能“越来越”public)
如果在派生链的某一个节点,某个成员变了private,那么后面的任何子派生类都不能再访问该成员了。否则会破坏封装性。
派生类向基类转换的可访问性(特指指针或者引用指向派生类对象,注意:类不可相互转换,即使能赋值):
只有当D公有继承B时,用户代码才能使用派生类向基类的转化;如果D继承B的方式是受保护或者私有的,则用户代码不能使用该转换。
不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
如果D继承B的方式是公有的或者受保护的,则D的派生类的成员和友元可以使用D向B的转换;反之,如果D继承B的方式是私有的,则不能使用。
总结:转换的可见性
用户代码:公有继承,转换才可见。
派生类的成员和友元:怎么继承都可见。
派生类的子派生类:public和protected才可见。
友元和继承:
class Base
{
    friend class Pal;
private:
    int xb;
};


class Sneaky : public Base     //如果不是Base就不行了
{
private:
    int xs;
};
class Pal
{
public:
    int f1(Base b) { return b.xb; } //正确,友元
    int f2(Sneaky s){ return s.xs; } //错误,xs是Sneaky新的成员。
    int f3(Sneaky s) { return s.xb; } //正确,xb部分是属于Base类,Pal是Base的友元
};


基类的友元对基类的可访问性会随着基类的public派生链延续下去。即所有public派生类中的基类成员都能被友元访问。
但是Pal类的派生类不能访问Base类。即被Base声明为友元的类的派生类,对Base不具有可访问性。否则容易破坏封装性。
class D2 : public Pal
{
public:
    int mem(Base b)
    {
        return b.xb;//错误
    }
};





当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效。对于原来那个类来说,其友元的基类或者派生类不具有特殊的访问能力。
总结:友元可访问一个基类及该基类在所有派生类中的部分,但继承链中的每一步都必须是public 继承。


改变个别成员的可访问性:
可通过using声明改变派生类继承的某个名字的访问级别。
class Base
{
public:
    std::size_t size() const {return n;}
protected:
    std::size n;
};


class Derived : private Base   //注意,是private继承,如果不处理的话,下面就全部默认private了
{
public:
    //保持对象尺寸相关的成员的访问级别
    using Base::size;


protected:
    using Base::n;
};


因为Derived使用了私有继承,所以继承而来的成员size和n默认是Derived的私有成员。然而,我们使用using声明改变了这些成员的可访问性。改变之后Derived的用户可以使用size成员(因为是public),而Derived的派生类能够使用n(因为是protected)。


派生类只能为哪些它可以访问的名字提供using声明。
struct : ①默认成员public;
      ②默认public继承(当一个派生类是struct时)


class:  ①默认成员private;
    ②默认private继承(当一个派生类是class时)


继承中的类作用域
当使用一个成员时,先在派生类中寻找,找不到再找它的直接基类(寻找方向 和继承方向相反  从派生类到基类)。




在编译时进行名字查找。
一个对象、引用或指针的静态类型。决定了该对象的哪些成员是可见的。即使静态成员和动态成员可能不一致(基类指针或引用绑定到派生类对象中时)。但是我们能使用哪些成员是由静态类型(也就是说只能用静态类型拥有的成员,虚函数也算基类拥有,因为名字相同,而只不过是实现不同)决定的。但是如果是虚函数,而派生类又没有重载的话,就由动态类型决定。
如果静态类型和动态类型不同,而动态类型覆盖了静态类型的成员函数,那么还是会调用静态类型的哪个被覆盖了的函数。







class Base
{
public:
    void bf(){}
};
class Derived : public Base
{
public:
    void df(){}
};
int main()
{
    Base *p = new Derived;
    p->df();
    return 0;
}





总结:【有没有】是由静态类型决定,而在都有的情况下虚函数【用哪个】由动态类型决定。


名字冲突与继承
派生类可以重用基类的名字,会把基类的同名成员隐藏。(先找派生类,再找基类)
struct A
{
    int x = 1;
};
struct B : public A
{
};




struct C : public B
{
    int x = 3;
    void show_x(){cout << B::x << endl;}
};




C c;
c.show_x();  会打印1,因为现在B中找x,找不到,再往B的基类找,永远不会找到C::x。


作用域运算符将覆盖原有的查找规则,并指示编译器从Base类的作用域开始查找x。


名字查找的步骤:以p->mem()/obj.mem()为例:
首先确定p(或obj)的静态类型。因为我们调用一个的是一个成员,所以该类型必须是一个类类型。
在p(或obj)的静态类型对应的类中查找mem,如果找不到,则依次在直接基类中不断查找直至到达继承链的顶端。如果还是找不到,将报错。
一旦找到了mem,将进行常规的类型检查,以确定对于当前找到的mem,本次调用是否合法。
假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码: 
(1)如果mem是虚函数且我们是通过指针或引用进行的调用。则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据的是对象的动态类型。
(2)反之,如果mem不是虚函数或者我们是通过对象(而非引用或指针)进行的调用,则编译器产生一个常规函数调用。


名字查找先于类型检查:如果派生类的成员和基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致。


class Base
{
public:
    void f(){ }
};
class Derived : public Base
{
public:
    void f(int x){ }
};
Derived d;
Base b;
b.f();
d.f(10);
d.f();  //错误,被void f(int x) 隐藏了。



所以基类和派生类的虚函数必须要有相同的形参列表,否则我们将无法通过引用或指针调用派生类的虚函数了。


一个派生类中的新函数(形参类型不同)可以把基类中的虚函数隐藏,使它不可见,但还是可以用。p550
判断有没有一个函数时,以静态类型为准;如果找到函数,但要确定执行哪个版本时,以动态类型为准。
class Base
{
public:
    virtual void f(){ cout << "v-Base" << endl; }
};
class Derived : public Base
{
public:
    void f( ){ cout << "Derived" << endl; }   //跟上面的同名,还是那个虚函数,并不是新函数
};
Base *p = new Derived;
p->f();// 打印 "Derived"




这里说的隐藏:指的是同名不同参数的函数。


覆盖重载的函数:
如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有的版本,或者一个也不覆盖。否则如果只覆盖一部分的话,另一部分会被隐藏(报错)。但是如果加上”Base::”,就不会报错了。


也可以使用一个using语句指定一个名字而不指定形参列表。。此时派生类只需要定义其特有的函数就可以了。而无须为继承而来的其他函数重新定义。
using Base::f;
//后面再重载部分函数。


在一个类里面,同名函数也可以是部分为虚函数,部分非虚。只要形参不同即可......


15.7  构造函数与拷贝控制
如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
基类总是需要定义析构函数,是因为要显式地把析构函数设为virtual。
派生类的虚析构函数可以把比基类多出来的那部分成员数据delete掉。


虚析构函数将阻止合成移动操作。(?????)


15.7.2 合成拷贝控制与继承
构造函数执行的时候,先执行派生类的构造函数。再调用其直接基类的构造函数,直到继承链顶端。但是如果在函数体中显式的话,会先显示基类的(类似于递归中最后被调用那个函数的东西先显示)。

先执行基类构造函数,再执行派生类构造函数。 析构函数的顺序相反。







要使用成员的拷贝控制,就要求成员可访问,并且不是一个被删除的函数。




派生类中删除的拷贝控制与基类的关系:
(1)如果基类中的某个【合成的特殊函数】是派生类不可访问的或者是删除的,那么派生类中的对应的成员将是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值与销毁工作。
(2)如果在基类中有一个不可访问或删除掉的【析构函数】,则派生类中合成的默认和拷贝构造函数将是被删除的。原因同上。
(3)编译器不会合成一个删除掉的移动操作。。。。。。。。。。。。。。。?????????(待续)


移动操作与继承。。。。。。。。。。??????????待续


15.7.3派生类的拷贝控制成员。
派生类的除析构函数外的函数,既要处理派生类的成员,也要处理基类的成员(赋值运算符要在函数体内处理基类,而构造函数和拷贝构造函数在初始化列表处调用基类的构造函数)。而析构函数只需要销毁自己的成员,会隐式调用基类的析构函数来处理基类的成员。
当派生类定义了【拷贝】或【移动】操作时,该操作负责拷贝或移动包括基类在内的整个对象,但析构不用。


派生类赋值运算符和派生类构造函数:

class D : public Base  
{  
public:  
	D(const D &d) : Base(d)   // 拷贝基类成员  
	{  
		//D的(独有)成员的初始值  
	}   
	
	D(D&& d) : Base (std::move(d))   // 移动基类成员  
	{  
		//D的(独有)成员的初始值  
	}  
};  




如果不写拷贝基类成员那部分,会造成基类成员为默认值,而派生类成员为拷贝值的情况。


类似的:基类的赋值运算符不会被自动调用。

应该写成:

D& operator=(const D &rhs)
{
	Base::operator=(rhs); //为基类部分赋值
	//为派生类的成员赋值
	//酌情处理自赋值及释放已有资源等情况
	reuturn *this;
}




派生类的析构函数:

//Base::~Base()被自动调用执行

~D() 
{
    // 不用显式调用基类的析构函数。
    /*该处由用户定义清楚派生类成员的操作*/ 
}








对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行,然后是基类的析构函数,以此类推,沿着继承体系反方向直到基类的基类的基类……。




如果一个类对象处于正在构建的过程中,我们认为它的类型和当前执行的构造函数(析构函数)的类型相同。而把整个类构造的过程看成类型转化的过程。








在c++11中,我们可以用using语句直接使用基类的构造函数。
class Bulk_quote : public Disc_quote
{
	public:
	using Disc_quote::Disc_quote;    //继承Disc_quote的构造函数
	double net_price(std::size_t) const; 
};









通常,using声明语句只是令某个名字在当前作用域内可见。而当作用于构造函数时,using声明语句将令编译器产生代码。对于基类的每一个构造函数,编译器都生成一个与之对应的派生类构造函数。换句话说,对于基类的每个构造函数,编译期都在派生类中生成一个形参列表完全相同的构造函数。
作用类似于:
Derived(int x, int y)  : Base(x)  { }   // x是Base中的,y是Derived中的,如果派生类含有自己的数据成员,那么这些成员将被默认初始化








和普通成员的using 声明不一样,一个构造函数的using 声明不会改变该构造函数的访问级别。
using不能指明explicit或者constexpr,这些属性和在基类中一样。




当基类的构造函数含有默认实参,这些实参并不会被继承。相反??????????????????(p558)
默认、拷贝和移动构造函数不会被继承。




15.8  容器与继承
vector<Quote> basket;
basket.push_back(Quote("0-123-123", 50));
basket.push_back(Bulk_quote("0-123-123", 50, 10, 25)); //正确,但是只能把对象的Quote部分拷贝给basket
cout << basket.back().net_price(15) << endl; //调用Quote版本的函数。





向vector中添加一个Bulk_quote时,派生类部分会被忽略掉。




在容器中放置(智能)指针而非对象:
当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针(最好是智能指针)。这些指针所指向动态对象可能是基类类型,也可能是派生类类型。
vector<shared_ptr<Quote> > basket;
basket.push_back(make_shared<Quote>("0-123-123", 50));
basket.push_back(make_shared<Bulk_quote>("0-123-123", 50, 10, .25)); 
cout << basket.back()->net_price(15) << endl; //调用Bulk_quote版本的函数。





上述代码成立的原因是可以把派生类的指针转换成基类的指针。
类似于通过基类指针调用派生类的虚函数,在编译时并不知道该指针指向的类型,所以能够将Bulk_quote的指针存到Quote类型的容器中。尽管形式上有区别,但实际上basket的所有元素类型都是相同的。








15.8.1  编写Basket类


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值