⾯向对象程序设计(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 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和public24 }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;// 只能访问⾃⼰的public37 a.test();38 39 B b; 40 std::cout << b.b << b.p << std::endl;// 派⽣类 只能访问⾃⼰的puiblic和基类的public41 b.test();42 43 } View Code 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) const10 {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 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 ⽅法不是虚函数,则以下结果均为a33 A *a2 = &a;34 A *b2 = &b;35 a2->print();// a36 b2->print();// b37 38 A &a3 = a;39 A &b3 = b;40 a3.print();// a41 b3.print();// b42 43 /// 强⾏调⽤基类44 //b.A::print(); // a45 //b2->A::print(); // a46 //b3.A::print(); // a47 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:D13 { 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 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; // 不能访问⽗类的私有变量 pri26 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 base42 { 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 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 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派⽣类⽅法,动态绑定的条件时:①使⽤指针或引⽤②使⽤虚函数。所以如果没有在基类中定义虚函数,则析构函数没有采⽤动态绑定,⽽是使⽤静态绑定,则只会执⾏基类的析构,⽽不会执⾏派⽣类的析构函数。 因篇幅问题不能全部显示,请点此查看更多更全内容