首页 > 代码库 > C++之面向对象编程总结

C++之面向对象编程总结

1. 面向对象编程的三个基本概念:数据抽象(类),继承(类继承)和动态绑定(运行时决定使用基类函数还是派生类函数)。面向对象编程的关键思想是多态性。

2. 派生类可以继承基类中定义的成员;派生类可以调用基类函数;派生类可以重定义基类的函数;派生类可以定义新的数据成员和函数成员。
3. 基类通过关键字virtual来指出希望派生类重新定义的函数(虚函数)。而基类希望派生类继承的函数不能使用virtual关键字。
4. 通过动态绑定可以使我们在继承层次中使用任意类型的对象,而不用关心对象的具体类型。使用这些类的程序无需区分函数是在基类中定义的还是子类中定义的。
5. 在继承层次中,根类一般都要定义虚析构函数
6. virtual的目的就是启用动态绑定。
7. 非虚函数的调用在编译时确定。
8. 除了构造函数外,任何非static函数都可以是virtual函数。
9. virtual关键字只能在类内部成员函数声明时使用,不能再类体外定义时使用
10. 基类函数通常应该将派生类函数需要重定义的任意函数定义为虚函数
11. 用户代码可以访问类的public成员,不能访问private和protected成员
12. 派生类可以访问基类的public成员和protected成员,不能访问private成员
13. private成员只能被当前类或者友元类访问
14. 在C++中,可以通过类的引用或者指针调用虚函数。即可以使用基类的引用或者指针调用派生类的函数。
15. 关于protected标签还有非常重要的一个属性:派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型的其它对象的protected成员没有特殊访问权限.例如对于以下代码:
class base_item {
protected:
    int price;
};
class bulk_item: public base_item {
public:
    void memfcn(const bulk_item& d, const base_item& b) {
           int p = price;   //正确
           p = d.price;     //正确
           p = b.price;     //不正确
    }
};
16. 使用类派生列表(class derivation list)指定基类,其格式如下:
      class classname: access-label base-class
      其中access-label为public,private或者protected.
17. 一般而言,派生类只定义那些与基类不同或扩展基类行为的方面
18. 派生类一般会重新定义某个虚函数,如果派生类没有重定义某个虚函数,则使用基类中定义的版本
19. 派生类的虚函数声明必须和基类中的定义方式完全匹配,但是有一个例外:返回对基类型的引用(或指针)的虚函数,派生类中可以返回派生类引用(或指针).例如B继承A,A类中有一个返回类型为A*的虚函数,B中重定义该函数的时候,可以返回A*或者B*
20. 派生类对象=派生类本身定义的非static成员+基类中定义的非static成员
21. c++语言并不要求天一起将对象的基类部分和派生类部分连续排列。
22. 派生类在访问基类的public和protected成员时,和访问自身的成员用法相同。
23. 以定义的类才可以用作基类,仅仅声明的类不能用作基类。这是因为每个派生类都包含并且可以访问其基类的成员,为了使用这些成员,派生类必须知道他们是什么。
24. 按照以上23中的规则,一个类不可能从自身派生出一个新的类。而基类本身可以是一个派生类。所以会有直接基类(immediate-base)和间接基类这两个概念。
25. 如果需要声明一个派生类,则不需要再声明时包含派生列表。
      class B: public A;  //错误,不应该包含声明列表
      class B; class A;   //正确
26. c++中的函数调用默认不使用动态绑定。要触发动态绑定必须满足两个条件:1.函数必须为虚函数。2.必须通过基类类型的引用或指针进行函数调用。
27. 基类类型的引用或指针可以引用基类类型对象,也可以应用派生类型对象,无论实际对象为何类型,编译器都将它看做基类类型对象。这种做法是安全的,因为每个派生类对象都拥有基类子对象。而且派生类继承基类的操作,任何可以在基类对象上执行的操作也可以通过派生类对象使用。
28. 积累类型引用和指针的关键点在于静态类型(static type,编译时确定类型)和动态类型(dynamic type,运行时确定类型)的不同。
29. 通过基类引用或者指针调用函数时,编译器将生成额外的代码,在运行时确定调用哪个函数,被调用的是与动态类型相对应的那个函数。
30. 引用和指针的静态类型和动态类型可以不相同,这是C++用以支持多态性的基石。另一方面,对象时非多态的,其类型总是已知且不变的。运行的函数是由对象的类型来确定的。
31. B继承自A,A中定义了一个非虚函数f,在B中可以重定义这个函数,编译器在调用f时总是根据实际指针或者引用的类型来确定执行函数的。所以非虚函数的在编译时便确定。
32. 覆盖虚函数机制的办法是使用与操作符(B集成自A,A定义了虚函数f,B中重定义了该函数):
      A * a = new B();
      int ret = a->A::f();
33. A中定义了虚函数f,B继承自A后并未实现f,这时如果用A * a = new B(); 使用a调用f时将输出A的实现。同样B *b = ne w B(); 调用b->f时同样也输出A的实现。
34. 派生类虚函数调用基类版本时,必须显式的使用域操作符。
35. 派生类B中的虚函数f在调用A中f的版本时,必须加作用域操作符,负责导致无穷递归。
36. 虚函数可以定义默认实参。函数调用时,默认参数的值在编译时就确定了。函数使用的默认值由调用该函数的指针或者引用的对象确定的,和对象的动态类型无关。所以在为虚函数指定默认参数时几乎总是会出错,因为默认参数使用的是静态类型,而虚函数调用使用的是动态类型。
37. 如果是公用集成,基类成员保持自己的访问级别;如果是受保护的继承,集成的public和protected成员在派生类中为protected成员;如果是私有集成,基类的所有成员在派生类中为私有成员。
38. 无论派生列表中用什么访问标号,派生类对基类的成员的访问级别和基类成员本身的访问级别相同。派生访问标号是用于控制派生类用户对继承的Base成员的访问。最常见的继承形式为public.
39. public派生成为接口继承,protected和private称为实现继承。
40. 派生类可以恢复继承成员的访问级别,但是不能使访问级别比基类中原来制定的级别更严格或者更宽松。恢复的方法是使用using 声明。例如:
      class Base {
       public:
            std::size_t size() const {return n;}
       protected:
             std::size_t n;
       };
      class Derived: private Base {
      public:
           using Base::size;
      protected:
           using Base::n;
      };
      这样就可以在用户代码中访问Base类的size成员,同时可以在Derived的派生类中访问n了。
41. 如果不适用派生访问标号的话,会有默认的继承保护级别。根据定义类选用的是class还是struct来确定这个基本,如果是class则访问标号为private,如果是struct,则访问标号是public.
42. struct和class除了默认的成员保护级别和默认的派生保护级别不同以外,没有其他区别。
43. 友元可以访问类的private和protected数据,注意友元关系不能继承。例如:
      class Base { 
          friend class Frnd;
      protected:
          int i;
      };
      class D1: public Base  {
      protected:
          int j;
       };
       class Frnd {
       public:
            int mem(Base b) {return b.i;}    //正确
            int mem(D1 d) {return d.i;}     //错误
        };
        class D2 : public Frnd {
        public:
             int mem(Base b) {return b.i;}    //错误
        };
44. static成员在整个继承层次中只有一个这样的成员。static成员遵循常规的访问控制。
45. 存在派生类类型的引用或者指针到基类类型的引用或者指针的转换,反之则不存在。这是因为基类可以是派生类的一部分,也可以不是其一部分。
46. 一般可以用派生类类型的对象对基类类型对象进行初始化和赋值,但是没有从派生类类型对象到基类类型对象的直接转换。引用转换和对象转换是不同的,例如函数传参的时候,前者只传递引用,后者将发生拷贝过程。
47. 前面提到的使用基类对象进行初始化和赋值,实际上是调用函数:初始化时调用构造函数,赋值时调用赋值操作符。
48. 如果是public继承,则用户代码和后代类都可以使用派生类到基类的转换;如果类是使用private或者protected继承派生的,则用户代码不能将派生类型对象转换为基类对象。如果是private集成,则从private集成类派生的类不能转换为基类。如果是protected集成则后续派生类的成员可以转换为基类类型。
49. 不存在从基类指针,引用,对象到派生类指针,引用,对象的转换。而且当基类指针或者引用实际绑定的是派生类对象时,从基类到派生类的转换也存在限制。例如:
      Bulk_item bulk;
      Item_base * itemP = & bulk;
      Bulk_item * bulkP = itemP;  //错误,不能从基类转换为派生类。
50. 非派生类的基类,其构造函数和赋值控制函数基本上不受继承影响。
51. 派生类的构造函数除了初始化自己的数据成员之外,还要初始化基类。
52. 派生类的默认构造函数会首先执行基类的默认构造函数,在对自己的成员进行初始化。
53. 可以重写派生类的构造函数,如下:
      class Bulk_item: public Item_base {
      public:
            Bulk_item(): min_qty(0),discount(0.0) { }
      };
      这时依然会首先执行基类的默认构造函数,然后对当前自己的成员进行初始化。
54. 派生类构造函数通过将基类包含在构造函数初始化列表中来简介初始化继承成员。如下所示:
      calss Bulk_item : public Item_base {
      public:
          Bulk_item(const std::string& book, double sales_price, std::size_t qty=0, double disc_rate=0.0) :
               Item_base(book,sales_price), min_qty(qty),discount(disc_rate) {}
      };
      上例还说明可以在构造函数中使用默认参数。
55. 一个类只能初始化自己的直接基类。例如C继承B,B继承A。C不能直接初始化A,以为B的作者已经规定了怎样构造和初始化B类型对象。
56. 派生类可以使用合成复制控制成员,合成操作对对象的基类部分联通派生部分的成员一起进行复制,赋值,或者撤销,其中使用基类的复制构造函数、赋值操作符或析构函数对基类部分进行复制,赋值或撤销。
57. 只包含类类型或者内置类型的素具成员,不含指针的类一般可以使用合成操作。
58. 如果派生类定义了自己的复制构造哈数,则该赋值构造函数一般应显式使用基类赋值构造函数初始化对象的基类部分。
59. 如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。例如:
      Derived & Derived::operator=(const Derived & rhs) {
          if(this != &rhs) {  
              Base::operator=(rhs)
          }
          return * this;
      }
60. 析构函数和赋值操作符以及复制构造函数不同,派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清楚自己的成员。对象的析构顺序和构造顺序相反:首先运行派生类析构函数,然后按继承层次依次向上调用各析构函数。
61. 析构函数应该定义为虚函数的原因是可能会删除指向派生类对象的基类类型指针。如果没有定义为虚函数,将调用基类的析构,而没有对派生类中的成员进行析构。
62. 如果层次中根类的一个函数为虚函数,则派生类中该函数也将是虚函数。而不管是否在派生类中加virtual关键字。
63. 构造函数不能定义为虚函数。如果在构造函数或者析构函数中调用了虚函数,则虚函数的版本调用的和当前构造函数或者析构函数是同一类型对象。例如:
      class A {
      public:
          A() {show();}
          ~A(){show();}
          virtual void show() {cout<<"A "; }
      };
      class B : public A {
      public:
          B() {show();}
          ~B(){show();}
          virtual void show() {cout<<"B "; }
      };
      int main ()
      {
          B * b = new B();
          delete b;
          A * a = new A();
          delete a;
          return 0;
      }
      输出结果为:A B B A A B B A
64. 基类类型的指针(引用或者对象)只能访问对象的基类部分
65. 与基类成员同名的派生类成员将屏蔽基类成员的直接访问,可以通过与操作符访问被屏蔽的基类成员。
66. 同数据成员一样,派生类的函数成员同样会覆盖基类的函数成员。只要派生类中的函数名和基类中的函数名相同,基类中的函数就会被覆盖。
67. 派生类中的函数可以重载(无论是虚函数还是非虚函数)派生类可以重定义的0个或者多个版本。
68. 规则66和规则67导致如果基类定义了一系列重载函数,如果想要在派生类中使用这些重载函数将不得不 再次定义一遍。可以使用using声明引入基类的函数名称,然后再派生类中之定义某些需要重载的函数。
69. 考虑下面这个例子:
     class Base {
     public:
         virtual int fcn();
     };
     class D1 : public Base {
     public:
         int fcn(int );
     };
     class D2: public D1{
     public:
         int fcn(int);
         int fcn();
     };
     Base bobj; D1 d1obj; D2 d2obj;
     Base * dp1 = & bobj, * bp2 = &d1obj,*bp3=&d2obj;
     dp1->fcn();    //正确,调用Base::fcn
     dp2->fcn();    //正确,调用Base::fcn
     dp3->fcn();    //正确,调用D2::fcn
70. 纯虚函数,在函数声明后加 = 0;这样没有重写该函数的类将不能创建对象。函数一个或者多个纯虚函数的类是抽象基类,除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象。
71. 在使用容器保存基类或者派生类对象时,在指定类型时最好使用基类类型指针。
72. 由于多态只能通过对象指针或者引用才能体现出来,句柄的作用就是通过封装指针可以使用对象来操作多态。

参考:Primer, 4th ed, chapter 15

C++之面向对象编程总结