【C++ Primer Chapter 15 总结】面向对象编程

1.面向对象编程:数据抽象,继承,动态绑定。
2.数据抽象:接口和类实现分开。继承:可以建模相似类型之间的关系的类。动态绑定:使用不同类型的对象并忽略它们的不同。
3. 基类将依赖类型的函数与期望其派生类不加更改地继承的函数区分开来。 基类定义虚函数使得派生类可以自定义该函数。
4.类派生列表指定了派生类继承了哪些类。
5.通过在函数头声明override显示声明派生类重载了基类的虚函数。只有虚函数可以声明为override。
6.在使用基类之前,必须先定义类,而不仅仅是声明类。
class Quote {
public:
    std::string isbn() const;
    virtual double net_price(std::size_t n) const;
    virtual void fun(int);
    void fun2();
};
 
class Bulk_quote : public Quote {          // 类派生列表 
public:
    double net_price(std::size_t) const override;   
    void fun() override;                  // error:虚函数覆盖必须参数列表和返回值都和基类一样
    void fun2() override;                 // error: 只有虚函数可以声明为override
};  

  

7. 用作继承体系根的类应定义虚析构函数,使得删除指向动态分配对象的指针时执行正确的析构函数。(主要应对对象的静态类型与动态类型不一致的情况。)
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; 
};

  

8.virtual关键字只出现在类内部的声明上,不能用于出现在类体外部的函数定义上。
   在基类中声明为虚函数的函数在派生类中也隐式为虚函数。(可传递性)
   未声明为虚函数的成员函数在编译时解析,而不是在运行时解析。
 
9. 类派生列表:每个基类名之前都可以有一个可选的访问说明符(public,private,protected)。
10.派生对象包含多部分:1)派生类自己定义的成员,2)从基类继承的部分。
11. 派生类必须使用基类构造函数来初始化其基类部分。派生类构造函数使用其构造函数初始化列表将参数传递给基类构造函数。
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;    // virtual
private:
    std::size_t min_qty = 0; 
    double discount = 0.0; 
};
Bulk_quote(const std::string& book, double p, 
    std::size_t qty, double disc): Quote(book, p), min_qty(qty), discount(disc) { };

  

12.可以将对基类的引用或指针变量绑定到派生对象的基类部分。
Quote item;          // 基类对象
Bulk_quote bulk;     // 派生类对象
Quote *p = &item;    // p指向基类
p = &bulk;           // p指向派生类对象bulk的基类部分
Quote &r = bulk;     // r引用派生类对象bulk的基类部分
 
13. 如果一个基类定义了一个静态成员,那么在整个层级中只有一个这样的成员。
如果该成员在基类中是private,则派生类无法访问它。
class Base {
public:
    static void statmem();
};
class Derived : public Base {
    void f(const Derived&);
};
 
void Derived::f(const Derived &derived_obj) {
    Base::statmem();                // 通过基类访问限定符访问静态成员
    Derived::statmem();             // 通过派生类访问限定符访问静态成员
    derived_obj.statmem();          // 通过派生类对象访问静态成员
    statmem();                      // this->statmem();
}
 
14.最后的派生对象包含其直接基类的以及每个间接基类继承的部分。
class Base { /* ... */ } ;
class D1: public Base { /* ... */ };     // Base是D1的直接基类
class D2: public D1 { /* ... */ };       // Base是D2的间接基类,D1是D2的直接基类
 
15. 通过在类名后面加上final来防止类被用作基类。
class NoDerived final { /* ... */ };         // NoDerived不能被继承
class Base { /* ... */ };
 
class Last final : Base { /* ... */ };       // Last不能被继承
class Bad : NoDerived { /* ... */ };         // error: NoDerived不能被继承
class Bad2 : Last { /* ... */ };             // error: Last不能被继承
 
16. 可以将基类类型的指针或引用绑定到基类派生类型的对象上。
Bulk_quote bulk;                 // 派生类对象
Quote *itemP = &bulk;            // 指向基类的指针
 
17.使用继承相关的指针/引用对象时,要区分指针/引用对象的静态类型(static type)动态类型(dynamic type)
静态类型是声明变量或表达式产生的类型。编译时确定。
动态类型是变量或表达式表示的对象在内存中的类型。运行时确定。
指向基类的指针或对基类的引用的静态类型可能不同于其动态类型。
Quote base;                       // 基类对象
Bulk_quote bulk;                 // 派生类对象
Quote *itemP = &base;            // itemP的静态类型:Quote&,itemP的动态类型:Quote,一样
itemP = &bulk;                  // itemP的静态类型:Quote&,itemP的动态类型:Bulk_quote,不一样
 
18.无法自动隐式地从基类类型转换到派生类类型。
编译器只查看指针或引用的静态类型,以确定转换是否合法。
Quote base;                      // 基类对象
Bulk_quote* bulkP = &base;       // error: 指向派生类的指针
Bulk_quote& bulkRef = base;      // error: 派生类的引用
 
Bulk_quote bulk;                 // 派生类对象
Quote *itemP = &bulk;            // 指向基类的指针可以指向派生类的基类部分
Bulk_quote *bulkP = itemP;       // error: bulkP的静态类型:指向派生类的指针,itemP的静态类型:指向基类的指针,无法实现自动转换
 
19. 可以将派生类型的对象复制、移动或赋值给基类型对象。
当从派生类型的对象初始化或赋值基类型对象时,只复制、移动或赋值派生对象的基类部分。对象的派生部分将被忽略。
Bulk_quote bulk;
Quote item(bulk);       // 使用Quote::Quote(const Quote&) constructor
item = bulk;            // 调用Quote::operator=(const Quote&)
 
20.虚函数。
 动态绑定只发生在通过基类的引用(或指针)调用虚函数时。 
a. 运行基类/派生类的虚函数只能在运行时阶段根据参数的类型决定,动态绑定有时也称为运行时绑定。
b. 当在具有非引用和非指针类型的表达式上调用虚函数时,该调用将在编译时绑定。
double print_total(ostream &os, const Quote &item, size_t n) {     // 基类对象的引用
    double ret = item.net_price(n);
    os << "ISBN: " << item.isbn() // calls Quote::isbn << " # sold: " << n << " total due: " << ret << endl;
    return ret;
}
 
Quote base("0-201-82470-1", 50);                       // 基类对象
print_total(cout, base, 10);                          // a. 动态绑定到基类对象上,print_total调用 Quote::net_price
Bulk_quote derived("0-201-82470-1", 50, 5, .19);      // 派生类对象
print_total(cout, derived, 10);                      // a. 动态绑定到派生类对象上,print_total调用 Bulk_quote::net_price
 
base = derived;                    // 调用Quote的拷贝赋值函数,拷贝derived的基类部分给base
base.net_price(20);                // b. 调用Quote::net_price,编译时决定

 

21.当派生类覆盖虚函数时,基类和派生类中的参数和返回值(除了返回值是自身类型的引用/指针)必须完全匹配。 
这样的返回类型要求从派生类到基类的派生到基类的转换是可访问的。
class A:{
public:
    virtual A& fun(int&, char);
};
class B: A{
public:
    B& fun(int&, char);
}
 
22. 如果调用虚函数使用的是默认实参,则使用的值是调用函数的静态类型所定义的值。
当通过基类的引用或指针进行调用时,默认实参将是基类中定义的实参。
即使在运行该函数的派生版本时,也将使用基类参数。
所以具有默认实参的虚函数应该在基类和派生类中使用相同的实参值。
 
23.纯虚函数。
纯虚函数不需要定义。我们通过将函数体写成= 0来指定虚函数是纯虚函数。
class Disc_quote : public Quote {                      // 抽象基类
public:
    Disc_quote() = default;
    Disc_quote(const std::string& book, double price, std::size_t qty, double disc): Quote(book, price), quantity(qty), discount(disc) { }
    double net_price(std::size_t) const = 0;          // 纯虚函数
};
 
24.抽象基类。
包含了纯虚函数的类是抽象基类,不能直接创造抽象基类类型的对象。
class Bulk_quote : public Disc_quote {      // 重构 :重新设计类层级,以便将操作和数据从一个类转移到另一个类。
public:
    Bulk_quote() = default;
    Bulk_quote(const std::string& book, double price, std::size_t qty, double disc): Disc_quote(book, price, qty, disc) { }
    double net_price(std::size_t) const override;
};

 

25.访问控制。
public成员:类使用者和派生类都可访问。
private成员:类使用者和派生类都不可访问。
protected成员:类使用者不可访问,派生类成员及其友元可以访问。
 
26. 派生类的成员或友元只能通过派生类对象访问基类的受保护成员。
class Base {
protected:
    int prot_mem;
};
 
class Sneaky : public Base {
    friend void clobber(Sneaky&);
    friend void clobber(Base&);
    int j;
};
 
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }        // ok
void clobber(Base &b) { b.prot_mem = 0; }                // error:不能直接访问基类的protected成员
 
 
27.类的三个用户:类的实现者,类对象使用者,派生类。
28.对类继承的成员的访问受2个组合控制:
  1. 基类中该成员的访问说明符; 控制派生类成员对基类成员的访问。
  2. (继承方式)派生类的派生列表中的访问说明符。 控制派生类的对象使用者(包括从派生类派生的其他类)对从Base继承的成员的访问。
对基类成员的访问只由基类本身中的访问说明符控制。派生列表的访问说明符对派生类的成员(和朋友)是否可以访问其自己直接基类的成员没有影响。
class Base {
public:
    void pub_mem();
protected:
    int prot_mem;
private:
    char priv_mem;
};
 
struct Pub_Derv : public Base {  
    int f() { return prot_mem; }          // ok
    char g() { return priv_mem; }         // error: 不能访问基类的private成员
};
 
struct Priv_Derv : private Base {
    int f1() const { return prot_mem; }      // ok: 可以访问基类的protected成员,对基类成员的访问只由基类本身中的访问说明符控制
};
 
Pub_Derv d1;        
Priv_Derv d2;
d1.pub_mem();     // ok
d2.pub_mem();     // error: Pub_Derv的pub_mem是private成员,不能直接访问
 
struct Derived_from_Public : public Pub_Derv {
    int use_base() { return prot_mem; }             // ok
};
struct Derived_from_Private : public Priv_Derv {
    int use_base() { return prot_mem; }             // error:Pub_Derv的pub_mem是private成员,不能直接访问
};

  

29.每个类控制其成员的访问权限,类的友元可以访问对象的所有成员。
  • 友元是不可遗传的。基类的友元没有对其派生类成员的特殊访问权,派生类的友元也没有对基类的特殊访问权。
  • 如果是基类的友元,则该友元不仅可以直接访问基类成员,也可以访问派生类对象的基类部分。
class Base {
    friend class Pal; 
};
class Pal {
public:
    int f(Base b) { return b.prot_mem; }          // ok
    int f2(Sneaky s) { return s.j; }             // error: Pal不是Sneaky的友元
    int f3(Sneaky s) { return s.prot_mem; }      //  ok: Pal是Base的友元,可以访问Base的派生类的基类部分
};

  

30.可以使用using声明改变派生类继承的某个成员的访问权限。
对using声明中指定的名称的访问只取决于using声明之前的访问说明符。
class Base {
public:
    std::size_t size() const { return n; }
protected:
    std::size_t n;
};
class Derived : private Base {    // 对于类的对象使用者和Derived的派生类来说,从Base继承来的成员都是private,不可访问的
public:
    using Base::size;       // using改变了Base::size的访问权限,从private到public
protected:
    using Base::n;          // using改变了Base::n的访问权限,从private到protected
};
 
31.class关键字默认private继承,struct默认public继承。
 
32. 名字查询是在编译时决定的。对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。
class Disc_quote : public Quote {
public:
    std::pair<size_t, double> discount_policy() const { return {quantity, discount}; }
};
 
Bulk_quote bulk;                        // 派生类对象
Bulk_quote *bulkP = &bulk;              // 静态类型和动态类型一样,都是Bulk_quote
Quote *itemP = &bulk;                   // itemP的静态类型:Quote,动态类型:Bulk_quote
bulkP->discount_policy();               // ok
itemP->discount_policy();               // error:itemP的静态类型是Quote,Quote没有discount_policy成员

  

33.函数调用过程。obj.mem();
  1. (Name look up)在obj的静态类型中查找mem成员。如果没有,则往obj所属类的直接基类上找该成员,如果没找到,则继续往上找间接基类中的成员。
  2. (Type Checking)判断mem()函数是否可以调用。(参数列表和返回类型是否匹配)
  3. 编译器生成代码。
  • 如果mem是虚函数,并且调用是通过引用或指针进行的,则编译器会生成代码,以根据对象的动态类型在运行时确定要运行哪个版本。
  • 如果mem是非虚函数,或者调用是在一个对象上(不是引用或指针),编译器将生成一个普通函数调用。
 
34.名字查找在类型匹配之前。如果名字相同,即使函数有不同的形参表,基类成员也是隐藏的。
struct Base {
    int memfcn();
};
struct Derived : Base {
    int memfcn(int);      // 隐藏了Base的memfcn的成员
 
};
 
Derived d; 
Base b;
b.memfcn();                // 调用Base::memfcn
d.memfcn(10);              // 调用Derived::memfcn
d.memfcn();                // error:编译器首先根据d的静态类型执行名字查找找到Derived中的memfcn成员,然后执行类型匹配发现参数无法匹配无法调用,出错
d.Base::memfcn();          // 调用Base::memfcn
 
35.派生类重载虚函数必须参数列表和返回列表与基类的虚函数一样。
虚函数的调用要按步骤执行:名字查找(静态类型)->函数类型匹配->编译器生成代码在运行时调用具体的函数版本。
非虚函数的调用,只由指针指向对象的静态类型决定。
class Base {
public:
    virtual int fcn();
};
 
class D1 : public Base {
public:                           
    int fcn(int);                // 和Base::fcn()参数不同,D1会继承Base::fcn()
    virtual void f2();             
};
 
class D2 : public D1 {
public:
    int fcn(int);                  // 隐藏了D1::fcn(int) 
    int fcn();                     // 重写虚函数Base::fcn()
    void f2();                     // 重写虚函数D1::f2
};
 
//调用虚函数
Base bobj; 
D1 d1obj; 
D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn();                       // 虚函数调用Base::fcn  (运行时决定)
bp2->fcn();                       // 虚函数调用Base::fcn,因为D1没有重写虚函数fcn()
bp3->fcn();                      // 虚函数调用D2::fcn
D1 *d1p = &d1obj; 
D2 *d2p = &d2obj;
bp2->f2();                        // error:在Base类中名字查找f2,Base没有成员f2
d1p->f2();                        // 虚函数调用D1::f2()
d2p->f2();                        // 虚函数调用D2::f2()
 
//调用非虚函数
Base *pb = &d2obj;                 // 静态类型不一样,动态类型一样   
D1 *pd1 = &d2obj; 
D2 *pd2 = &d2obj;
pb->fcn(42);                       // error: Base没有fcn(int)成员
pd1->fcn(42);                      // 调用D1::fcn(int)
pd2->fcn(42);                      // 调用D2::fcn(int)

  

35. 如果基类没有默认、复制或移动构造函数,那么它的派生类通常也没有。
派生类的拷贝和移动构造函数要在初始化列表中显式地调用基类的拷贝和移动构造函数。
基类的复制构造函数将派生类的Base部分复制到正在创建的对象中。
class Base { /* ... */ } ;
class D: public Base {
public:
    D(const D& d): Base(d)
                   /* initializers for members of D */ { /* ... */ }
    D(D&& d): Base(std::move(d)) 
                  /* initializers for members of D */ { /* ... */ }
};
 
36.如果构造函数或析构函数调用虚函数,则运行的虚函数版本是与构造函数或析构函数本身的类型相对应的版本。
 
37. 类不会自动继承默认构造函数、复制和移动构造函数。
  • 派生类可以通过using声明来继承其基类构造函数。
  • 对于基类中的每个构造函数,编译器在派生类中生成具有相同形参列表的构造函数。
class Bulk_quote : public Disc_quote {
public:
    using Disc_quote::Disc_quote;           // 继承Disc_quote的构造函数
    double net_price(std::size_t) const;
};
 
// derived(parms) : base(args) { }
Bulk_quote(const std::string& book, double price, std::size_t qty, double disc): Disc_quote(book, price, qty, disc) { }
 
38. 如果基类构造函数有默认实参,这些实参不会被继承。相反,派生类获得多个继承的构造函数,其中每个带默认实参的形参都被连续省略。
 
posted @ 2021-05-29 13:42  萌新的学习之路  阅读(80)  评论(0编辑  收藏  举报