面向对象程序设计(Object-oriented programming)的核心思想是数据抽象,继承,和动态绑定。
1. 继承
在C++语言中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数(virtual function)。
1 class Quote { 2 public: 3 std::string isbn() const; 4 virtual double net_price(std::size_t n) const; 5 };
派生类必须通过使用派生列表(class derivation list)明确指出它是从哪个(哪些)基类继承而来的:
如下:
1 class Bulk_quote : public Quote { 2 public: 3 double net_price(std::size_t) const override; 4 };
派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上virtual关键字,但是并不是非得这么做。并且,C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在改函数的形参列表之后增加一个override关键字。
1.1 访问控制和继承

1 #include<iostream> 2 class A { 3 public: 4 int p = 0; 5 virtual void test(); 6 private: int p2 = 1; 7 protected: int p3 = 2; 8 }; 9 10 void A::test() 11 { 12 std::cout << this->p << this->p2 << this->p3 << std::endl; 13 } 14 15 class B:public A { 16 public: 17 int b = 3; 18 void test() { 19 std::cout << this->b << this->b2 << this->b3 << std::endl; 20 } 21 22 void test2() { 23 std::cout << this->p3 << std::endl; // 派生类可以访问基类的protect和public 24 } 25 26 friend void test3() { 27 std::cout << this-> << std::endl; 28 } 29 private: int b2 = 4; 30 protected: int b3 = 5; 31 }; 32 33 int main() 34 { 35 A a; 36 std::cout << a.p << std::endl;// 只能访问自己的public 37 a.test(); 38 39 B b; 40 std::cout << b.b << b.p << std::endl;// 派生类 只能访问自己的puiblic和基类的public 41 b.test(); 42 43 }
1.2 定义基类和派生类
1.定义基类。
Note:基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
1 class Quote { 2 public: 3 Quote() = default; 4 Quote(const std::string &book, double sales_price): 5 bookNo(book), price(sales_price){} 6 std::string isbn()const {return bookNo;} 7 //返回给定数量的书籍的销售总额 8 //派生类负责改写并使用不同的折扣计算算法 9 virtual double net_price(std::size_t n) const 10 {return n*price;} 11 virtual ~Quote() = default; //对析构函数进行动态绑定 12 private: 13 std::string bookNo; 14 protected: 15 double price = 0.0; //代表普通状态下不打折的价格 16 };
- 基类必须将它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数,基类通常将其定义为虚函数;另一种是基类希望派生类直接继承而不要改变的函数。
- 任何构造函数之外的非静态函数都可以是虚函数,关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。
- 如果基类把一个函数声明为虚函数,则该函数在派生类中隐式地也是虚函数。
- 成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时。
- 派生类能访问公有成员,而不能访问私有成员。
- 不过在某些时候,基类中还有一种成员,基类希望它的派生类有权访问该成员,同时禁止其他用户访问。我们用受保护的(protected)访问运算符说明这样的成员。
2. 定义派生类
1 class Bulk_quote : public Quote { 2 public: 3 Bulk_quote() = default; 4 Bulk_quote(const std::string&, double, std::size_t, double); 5 //覆盖基类的函数版本以实现基于大量购买的折扣政策 6 double net_price(std::size_t) const override; 7 private: 8 std::size_t min_qty = 0; //使用折扣政策的最低购买量 9 double discount = 0.0; //以小数表示的折扣额 10 };
- 派生类经常(但不总是)覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。
- C++新标准允许派生类显式地注明它使用某个成员函数覆盖了它继承的虚函数。具体做法是在函数后面加上关键字override。
- 在派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上。如下:
1 Quote item; //基类对象 2 Bulk_quote bulk; //派生类对象 3 Quote *p = &item; //p指向Quote对象 4 p = &bulk; //p指向bulk的Quote部分 5 Quote &r = bulk; //r绑定到bulk的Quote部分
3. 派生类构造函数
派生类可以继承基类的成员,但是不能直接初始化这些成员(每个类控制它自己的成员初始化过程)。如果没有在子类中对父类进行初始化,则父类必须有默认构造函数。
1 Bulk_quote(const std::string &book, double p, std::size_t qty, 2 double disc):Quote(book, p), min_qty(qyt), discount(disc) { }
4.派生类使用基类的成员
派生类可以访问基类的公有成员,和受保护成员
5.继承与静态成员
如果基类中有静态变量,则不论派生出多少类,对每个静态成员来说都只存在唯一实例。如果基类中静态成员是private,则派生类无权访问,假设派生类可以访问,则我们既能通过基类也能通过派生类使用它。
6.防继承
C++11新标准提供了一种防止继承发生的方法,即在类名后跟一个关键字final
1 class NoDerived final { /* */}; //NoDerived不能作为基类
7.不存在从基类向派生类的隐式类型转换
之所以存在派生类向基类的类型转换是因为每个派生类都包含了基类的一部分,而基类引用或者指针可以绑定到该基类部分上。但是因为一个基类对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换。
1 Quote base; 2 Bulk_quote* blukP = &base; //不合法 3 Bulk_quote& blukRef = base; //不合法
下面这种情况也是不允许的
1 Bulk_quote bulk; 2 Quote* itemP = &bulk; //合法,基类绑定派生类 3 Bulk_quote* blukRef = itemP; //不合法
如果基类中含有一个或多个虚函数,我么可以使用 dynamic_cast 请求类型转换。
8.对象之间不存在类型转换
派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型之间不存在这样的转换。很多时候,我们确实希望将派生类对象转换成基类类型,但是这种转换往往与我们所期望的不一样。
请注意,当我们初始化或赋值一个类类型的对象时,实际上实在调用某个函数。当执行初始化时,我们调用构造函数,而当执行赋值操作时,我们调用赋值运算符。
因为这些成员接受引用作为参数,多以派生类向基类的转换允许我们给基类的拷贝和移动操作传递一个派生类的对象。这些操作不是虚函数,当我们给基类的构造函数传递一个派生类对象时,实际运行的构造函数是基类中定义的那个,显然,该运算符只能处理基类自己的成员。
1 Bulk_quote bulk; // 派生类对象 2 Quote item(bulk);// 使用Quote::Quote(const Quote&)构造函数 3 item = bulk; // 调用Quote::opertator=(const Quote&)。同时忽略掉派生类中的部分成员
2. 动态绑定
在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。明晰派生类调用到底调用谁的print方法。
比如:
1 #include<iostream> 2 class A { 3 public: 4 A() = default; 5 virtual void print() { 6 std::cout << "a" << std::endl; 7 } 8 virtual ~A() { 9 std::cout << "destroy A" << std::endl; 10 }; 11 }; 12 13 class B :public A { 14 public: 15 B() = default; 16 void print() { 17 std::cout << "b" << std::endl; 18 } 19 ~B() { 20 std::cout << "destroy B" << std::endl; 21 } 22 23 }; 24 25 int main() 26 { 27 A a; 28 B b; 29 a.print(); 30 b.print(); 31 32 /// 动态绑定.如果基类中 print 方法不是虚函数,则以下结果均为a 33 A *a2 = &a; 34 A *b2 = &b; 35 a2->print();// a 36 b2->print();// b 37 38 A &a3 = a; 39 A &b3 = b; 40 a3.print();// a 41 b3.print();// b 42 43 /// 强行调用基类 44 //b.A::print(); // a 45 //b2->A::print(); // a 46 //b3.A::print(); // a 47 48 }
3. 虚函数
如第二点所述,当我们使用基类的引用或指针调用一个虚函数时会执行动态绑定。因为我们直到运行时,才能知道到底调用了哪个版本的虚函数(上面的例子能看出,但是编译器看不出),所以所有虚函数都必须定义。需要注意的,动态绑定必须通过指针,引用,调用虚函数才会发生。如果使用类类型【如:Quote base; Bulk_Quote derived; base=derived; base.net_price(21)。其中net_price是基类虚函数,派生类重写了。这个情况下编译时,会被解析成基类的net_price() 方法。】非虚函数在编译时进行绑定。
一个派生类的函数如果覆盖了某个继承来的虚函数,则它的形参类型必须与被覆盖的基类函数完全一致。
3.1 final和override说明符
派生类如果定义了一个和基类中名字相同但是形参列表不同,这也是合法的。编译器会认为这是两个函数与基类中原有的函数是相互独立的。但是这可能不是我们想要的,我们想要覆盖基类方法,但是编译器不报错。这是c++11 中新出了 overrride 关键字,这可以让编译器明白我们的用意,并为我们发现错误(形参是否写错了等)。
如果我们用 override 关键字标记了某个函数,但是该函数没有覆盖已存在的虚函数,此时编译器会报错。
如果使用 final 关键词标记函数,则不允许后序其他类覆盖该方法。
1 struct C { 2 virtual void f1() const; 3 }; 4 5 struct D:C 6 { 7 void f1() const final; // final 修饰虚函数 8 }; 9 void D::f1() const { 10 11 } 12 struct E:D 13 { 14 void f1() const ; // 错误 15 };
4.抽象基类
4.1 纯虚函数
为什么要有纯虚函数?因为在许多情况下,在基类中不能对虚函数给出有意义的实现(比如动物基类中“叫”的方法),而把它声明为纯虚函数,它的实现完全留给派生类去做。凡是含有纯虚函数的类叫做抽象类,这种类不能声明对象,只能作为基类为派生类服务。
1 virtual void f3()=0;
派生类构造函数只初始化它的直接基类。
5. 访问控制与继承
5.1 受保护的成员
类中的私有变量只能由该类的成员方法能访问。一个类使用protected关键字来声明哪些希望与派生类分享但是不想被其他公共访问使用的成员,protected可以看成是private 和 public的中和产物:
- 和私有成员类似,受保护的成员对于类的用户来说不可访问
- 和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的
- 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问权
1 class Base { 2 protected: 3 int prot_mem; //protected成员 4 }; 5 class Sneaky : public Base { 6 friend void clobber(Sneaky&); //能访问Sneaky::prot_mem 7 friend void clobber(Base&); //不能访问Base::prot_mem 8 int j; //j默认是private 9 }; 10 //正确:clobber能访问Sneaky对象的private和protected成员 11 void clobber(Sneaky &s) { s.j = s.prot_mem = 0; } 12 //错误:clobber不能访问Base的protected成员 13 void clobber(Base &b) { b.prot_mem = 0; }
- 在类的外部(比如main函数中),类的pirvate成员不管是对该类的对象还是该类派生类的对象,都是无访问权限的
5.2 公有,私有和受保护继承
1 #include<iostream> 2 using namespace std; 3 4 class base { 5 public: 6 int pub; 7 void pub_fun_base() { 8 cout << "base pub_fun" << endl; 9 } 10 private: 11 int pri; 12 void pri_fun_base() { 13 cout << "base pri_fun" << endl; 14 } 15 protected: 16 int pro; 17 void pro_fun_base() { 18 cout << "base pro_fun" << endl; 19 } 20 }; 21 22 struct pub_derived : public base 23 { 24 void f() { 25 cout << pub << pro << endl; // 不能访问父类的私有变量 pri 26 pub_fun_base(); 27 pro_fun_base(); 28 } 29 30 }; 31 32 struct pri_derived : private base // 派生类说明符private同public看似访问权限一样,那该说明符何用?(在main函数中解释) 33 { 34 void f() { 35 cout << pub << pro << endl; 36 pub_fun_base(); 37 pro_fun_base(); 38 } 39 }; 40 41 struct pro_derived : protected base 42 { 43 void f() { 44 cout << pub << pro << endl; 45 pub_fun_base(); 46 pro_fun_base(); 47 } 48 }; 49 50 int main() 51 { 52 base base1; 53 base1.pub; 54 base1.pub_fun_base(); // 说明基类对象能直接访问public成员,protect/private不可。在成员方法中均可 55 56 pub_derived pub_d; 57 pri_derived pri_d; 58 pro_derived pro_d; 59 pub_d.f(); 60 pri_d.f(); 61 pro_d.f(); 62 63 pub_d.pub_fun_base(); // 派生类说明符作用在此,控制基类中继承过来的公有成员是保持public还是改成private/public 64 //pri_d.pub_fun_base(); //非法 // 基类中的public成员通过private说明符继承,则该方法不能由派生类对象直接访问了,相当于变成了派生类中的private成员 65 //pro_d.pub_fun_base(); //非法 66 return 0; 67 }
5.3 虚析构函数
1 #include<iostream> 2 using namespace std; 3 4 struct base { 5 base() { cout << "base create" << endl; } 6 ~base(){ cout << "base destroy" << endl; } 7 }; 8 9 struct derived_base : public base { 10 derived_base() { cout << "derived_base create" << endl; } 11 ~derived_base() { cout << "derived_base destroy" << endl; } 12 }; 13 14 int main() 15 { 16 17 derived_base* db = new derived_base(); 18 delete db; 19 20 return 0; 21 }
下面更改下代码如下:
1 #include<iostream> 2 using namespace std; 3 4 struct base { 5 base() { cout << "base create" << endl; } 6 virtual ~base(){ cout << "base destroy" << endl; } 7 }; 8 9 struct derived_base : public base { 10 derived_base() { cout << "derived_base create" << endl; } 11 ~derived_base() { cout << "derived_base destroy" << endl; } 12 }; 13 14 int main() 15 { 16 base* b = new derived_base(); // 使用动态绑定 17 delete b; 18 19 return 0; 20 }
如果没有使用动态绑定,就是第一个例子。如果使用了动态绑定,但是如果没有第二个例子第六行在基类中定义虚析构函数,则没有截图中红色箭头销毁派生类的操作。为什么呢?因为我们知道第一个例子是静态绑定,第二个例子是动态绑定,动态绑定在运行时才能确定执行基类or派生类方法,动态绑定的条件时:①使用指针或引用②使用虚函数。所以如果没有在基类中定义虚函数,则析构函数没有采用动态绑定,而是使用静态绑定,则只会执行基类的析构,而不会执行派生类的虚构函数。