一、OOP:概述
面向对象程序设计的核心思想是数据抽象、继承和动态绑定。通过使用数据抽象,我们可以将类的接口和实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
1)继承
通过继承联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类,其他类则直接或间接地从基类继承而来,这些继承得到的类称为派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自的成员。
在C++语言中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数。
1 class Quote { 2 public: 3 std::string isbn()const { return bookno; } 4 virtual double net_price(std::size_t n) const { 5 std::cout << "Quote::net_price" << std::endl; 6 return price; 7 } 8 public: 9 std::string bookno; 10 double price; 11 };
派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有访问说明符。
1 class BulkQuote:public Quote{ 2 public: 3 virtual double net_price(std::size_t n) const override { 4 std::cout << "BulkQuote::net_price" << std::endl; 5 if (n > 10) 6 return 0.5 * price; 7 return price; 8 } 9 };
派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上virtual关键字,但是并不是非得这么做。C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个override关键字。
2)动态绑定
通过使用动态绑定,我们可以用同一段代码分别处理Quote和BulkQuote的对象。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class Quote { 9 public: 10 std::string isbn()const { return bookno; } 11 virtual double net_price(std::size_t n) const { 12 std::cout << "Quote::net_price" << std::endl; 13 return price; 14 } 15 public: 16 std::string bookno; 17 double price; 18 }; 19 20 class BulkQuote:public Quote{ 21 public: 22 virtual double net_price(std::size_t n) const override { 23 std::cout << "BulkQuote::net_price" << std::endl; 24 if (n > 10) 25 return 0.5 * price; 26 return price; 27 } 28 }; 29 30 void print_total(std::ostream &os, const Quote &item, std::size_t n) { 31 double price = item.net_price(n); 32 std::cout << "ISBN:" << item.isbn() << " 原价:" << item.price 33 << " 折后价:" << price << std::endl; 34 } 35 int main() 36 { 37 Quote q; 38 q.bookno = "233"; 39 q.price = 100; 40 BulkQuote bq; 41 bq.bookno = "123"; 42 bq.price = 100; 43 print_total(std::cout, q, 20); 44 std::cout << "------------------" << std::endl; 45 print_total(std::cout, bq, 20); 46 return 0; 47 }
因为上述过程中函数的运行版本由实参决定,即在运行时选择函数的版本,所以动态绑定有时又被称为运行时绑定。
在C++语言中,当我们使用基类的引用或指针调用一个虚函数时将发送动态绑定。
二、定义基类和派生类
1、定义基类
基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
1 class Quote { 2 public: 3 Quote() = default; 4 Quote(const std::string &_bookno, double _price) 5 :bookno(_bookno), price(_price) {} 6 std::string isbn()const { return bookno; } 7 virtual double net_price(std::size_t n) const { 8 std::cout << "Quote::net_price" << std::endl; 9 return n * price; 10 } 11 virtual ~Quote() = default; // 对析构函数进行动态绑定 12 private: 13 std::string bookno; // 书籍的ISBN编号 14 protected: 15 double price; // 代表普通状态下不打折的价格 16 };
1)成员与继承
在C++语言中,基类必须将它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数;另一种是基类希望派生类直接继承而不要改变的函数。对于前者,基类通常将其定义为虚函数。当我们使用指针或引用调用虚函数时,该调用将被动态绑定。根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。
基类通过在其成员函数的声明语句之前加上关键字virtual使得该函数执行动态绑定。任何构造函数之外的非静态函数都可以是虚函数。关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
2)访问控制与继承
派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。和其他使用基类的代码一样,派生类能访问公有成员,而不能访问私有成员。不过某些时候基类中还有这样一种成员,基类希望它的派生类有权访问该成员,同时禁止其他用户访问。我们用受保护的访问运算符说明这样的成员。
2、定义派生类
派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有以下三种访问说明符中的一个:public、protected、private。
派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明。
1 class BulkQuote :public Quote { 2 public: 3 BulkQuote() = default; 4 BulkQuote(const std::string &_bookno, double _price, std::size_t qty, double disc) 5 :Quote(_bookno,_price),min_qty(qty),discount(disc){} 6 virtual double net_price(std::size_t n) const override; 7 private: 8 std::size_t min_qty; // 适用折扣的最低购买量 9 double discount; // 折扣值 10 }; 11 12 double BulkQuote::net_price(std::size_t n) const { 13 std::cout << "BulkQuote::net_price" << std::endl; 14 if (n >= min_qty) 15 return n * (1 - discount) * price; 16 return n * price; 17 }
1)派生类中的虚函数
如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。
派生类可以在它覆盖的函数前使用virtual关键字,但不是非得这样做。C++11新标准允许派生类显式地注明它使用某个成员函数的覆盖了它继承的虚函数。具体做法是在形参列表后面、或者在const成员函数的const关键字后面、或者在引用成员函数的引用限定符后面添加一个关键字override。
2)派生类对象及派生类向基类的类型转换
一个派生类对象包含多个组成部分:一个派生类自己定义的非静态成员的子对象,以及一个与该派生类继承的基类对应的子对象,如果多个基类,那么这样的子对象也有多个。
C++标准并没有明确规定派生类的对象在内存中如何分布。
因为在派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到该派生类对象中的基类部分上。这种转换通常称为派生类到基类的类型转换。和其他类型转换一样,编译器会隐式地执行派生类到基类的转换。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class Quote { 9 public: 10 Quote() = default; 11 Quote(const std::string &_bookno, double _price) 12 :bookno(_bookno), price(_price) {} 13 std::string isbn()const { return bookno; } 14 virtual double net_price(std::size_t n) const { 15 std::cout << "Quote::net_price" << std::endl; 16 return n * price; 17 } 18 virtual ~Quote() = default; // 对析构函数进行动态绑定 19 private: 20 std::string bookno; // 书籍的ISBN编号 21 protected: 22 double price; // 代表普通状态下不打折的价格 23 }; 24 25 class BulkQuote :public Quote { 26 public: 27 BulkQuote() = default; 28 BulkQuote(const std::string &_bookno, double _price, std::size_t qty, double disc) 29 :Quote(_bookno,_price),min_qty(qty),discount(disc){} 30 virtual double net_price(std::size_t n) const override; 31 private: 32 std::size_t min_qty; // 适用折扣的最低购买量 33 double discount; // 折扣值 34 }; 35 36 double BulkQuote::net_price(std::size_t n) const { 37 std::cout << "BulkQuote::net_price" << std::endl; 38 if (n >= min_qty) 39 return n * (1 - discount) * price; 40 return n * price; 41 } 42 43 int main() 44 { 45 Quote item; 46 BulkQuote bulk; 47 Quote *p = &item; // p指向Quote对象 48 p = &bulk; // p指向bulk的Quote部分 49 Quote &r = bulk; // r绑定到bulk的Quote对象 50 return 0; 51 }
这种隐式特性意味着我们可以把派生类对象或者派生类对象的引用用在需要基类引用的地方;同样的,我们也可以把派生类对象的指针用在需要基类指针的地方。
3)派生类构造函数
尽管派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。和其他创建了基类对象的代码一样,派生类也必须使用基类的构造函数来初始化它的基类部分。
派生类对象的基类部分与派生类对象自己的数据成员都是在构造函数的初始化阶段执行初始化操作的。类似我们初始化成员的过程,派生类构造函数同样是通过构造函数初始化列表来将实参传递给基类的构造函数的。首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
除非我们特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化。如果想使用其他的基类构造函数,我们需要以类名加圆括号内的实参列表的形式为构造函数提供初始值。
4)派生类使用基类的成员
派生类可以访问基类的公有成员和受保护成员。
5)继承与静态成员
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一实例。静态成员遵循通用的访问控制规则,如果基类中的成员是private的,则派生类无权访问它。假设某静态成员是可访问的,则我们既能通过基类使用它也能通过派生类使用它。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class Base { 9 public: 10 static void statemem(); 11 }; 12 void Base::statemem() { 13 std::cout << __FUNCTION__ << std::endl; 14 } 15 class Derived :public Base { 16 public: 17 void f() { 18 Base::statemem(); // Base定义了statemem 19 Derived::statemem(); // Derived继承了statemem 20 this->statemem(); // 通过this对象访问 21 } 22 }; 23 int main() 24 { 25 Derived d; 26 d.f(); 27 return 0; 28 }
6)派生类的声明
派生类的声明与其他类差别不大,声明中包含类名但是不包含它的派生列表:
class BulkQuote : public Quote; // 错误:派生列表不能出现在这里
class BulkQuote; // 正确
7)被用作基类的类
如果我们想将某个类用作基类,则该类必须已经定义而非仅声明。这一规定的原因:派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类当然要知道它们是什么。
8)防止继承的发生
有时我们会定义这样一种类,我们不希望其他类继承它,或者不想考虑它是否适合作为一个基类。为了实现这一目的,C++11新标准提供了一种防止继承发生的方法,即在类名后跟一个关键字final。
1 class Base final{ 2 public: 3 static void statemem(); 4 }; 5 void Base::statemem() { 6 std::cout << __FUNCTION__ << std::endl; 7 }
3、类型转换与继承
可以将基类的指针或引用绑定到派生类对象上有一层极为重要的含义:当使用基类的引用(或指针)时,实际上我们并不清楚该引用(或指针)所绑定的对象的真实类型。该对象可能是指针,也可能是派生类的对象。
和内置指针一样,智能指针类也支持派生类向基类的类型转换,这意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针内。
1)静态类型与动态类型
表达式的静态类型在编译时是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。
如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。
2)不存在从基类向派生类的隐式转换
之所以存在派生类向基类的类型转换时因为每个派生类对象都包含一个基类部分,而基类的引用或指针可以绑定到该基类部分上。一个基类的对象既可以以独立的形式存在,也可以作为派生类对象的一部分存在。如果基类对象不是派生类对象的一部分,则它只含有基类定义的成员,而不含有派生类定义的成员。
因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换:
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class Quote { 9 public: 10 std::string isbn()const { return bookno; } 11 virtual double net_price(std::size_t n) const { 12 std::cout << "Quote::net_price" << std::endl; 13 return price; 14 } 15 virtual ~Quote() = default; 16 public: 17 std::string bookno; 18 double price; 19 }; 20 21 class BulkQuote :public Quote { 22 public: 23 virtual double net_price(std::size_t n) const override { 24 std::cout << "BulkQuote::net_price" << std::endl; 25 if (n > 10) 26 return 0.5 * price; 27 return price; 28 } 29 }; 30 31 int main() 32 { 33 Quote base; 34 BulkQuote *p = &base; //错误:不能将基类转换成派生类 35 BulkQuote &r = base; // 错误:不能将基类转换成派生类 36 return 0; 37 }
有一种情况比较特别,即使一个基类指针或引用绑定在一个派生类对象上,我们也不能执行从基类向派生类的转换:
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class Quote { 9 public: 10 std::string isbn()const { return bookno; } 11 virtual double net_price(std::size_t n) const { 12 std::cout << "Quote::net_price" << std::endl; 13 return price; 14 } 15 virtual ~Quote() = default; 16 public: 17 std::string bookno; 18 double price; 19 }; 20 21 class BulkQuote :public Quote { 22 public: 23 virtual double net_price(std::size_t n) const override { 24 std::cout << "BulkQuote::net_price" << std::endl; 25 if (n > 10) 26 return 0.5 * price; 27 return price; 28 } 29 }; 30 31 int main() 32 { 33 BulkQuote bulk; 34 Quote *p = &bulk; 35 BulkQuote *bulkp = p; // 错误 36 return 0; 37 }
编译器在编译时无法确定某个特定的转换在运行时是否安全,这是因为编译器只能通过检查指针或引用的静态类型来推断该转换是否合法。如果在基类中含有一个或多个虚函数,我们可以使用dynamic_cast请求一个类型转换,该转换的安全检查在运行时执行。同样,如果我们已知某个基类向派生类的转换是安全的,则我们可以使用static_cast来强制覆盖编译器的检查工作。
3)在对象之间不存在类型转换
当我们勇敢一个派生类对象为一个基类对象初始化或赋值时,只有派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分会被忽略掉。
三、虚函数
通常情况下,如果我们不使用某个函数,则无须为该函数提供定义。但是我们必须为每一个虚函数都提供定义,而不管它是否被用到了,这是因为连编译器也无法确定到底会使用哪个虚函数。
1)对虚函数的调用可能在运行时才解析
当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。
2)派生类中的虚函数
当我们在派生类中覆盖了某个虚函数时,可以再一次使用virtual关键字指出该函数的性质。然而这么做并非必须,因为一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数。
一个派生类中的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与它被覆盖的基类函数完全一致。
同样,派生类中虚函数的返回类型也必须与基类函数匹配。该规则有一个例外,当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。也就是说,如果D由B派生得到,则基类的虚函数可以返回B*而派生类的对应函数可以返回D*,只不过这样的返回类型要求从D到B的类型转换是可访问的。
3)final和override关键字
派生类如果定义了一个函数与基类中的虚函数名字相同但是形参列表不同,这仍然是合法的行为。编译器会认为新定义的这个函数与基类中原有的函数是相互独立的。这时,派生类的函数并没有覆盖掉基类中的版本。
在C++11新标准中我们可以使用override关键字来说明派生类中的虚函数。这么做的好处是在使得程序员的意图更加清晰的同时让编译器可以为我们发现一些错误,后者在编程实践中显得更加重要。如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。
我们还能把某个函数指定为final,如果我们已经把函数定义成final了,则之后任何尝试重新覆盖该函数的操作都将引发错误。
1 class Quote { 2 public: 3 std::string isbn()const { return bookno; } 4 virtual double net_price(std::size_t n) const final{ 5 std::cout << "Quote::net_price" << std::endl; 6 return price; 7 } 8 virtual ~Quote() = default; 9 public: 10 std::string bookno; 11 double price; 12 };
final和override关键字出现在形参列表(包括任何const或引用限定符)以及尾置返回类型之后。
4)虚函数与默认实参
虚函数也可以拥有默认实参。如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。换句话说,如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。此时,传入派生类函数的将是基类函数定义的默认实参。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class A { 9 public: 10 virtual void func(int x = 1) { 11 std::cout << "A:"; 12 std::cout << x << std::endl; 13 } 14 }; 15 class B :public A { 16 public: 17 virtual void func(int x = 10) override { 18 std::cout << "B:"; 19 std::cout << x << std::endl; 20 } 21 }; 22 int main() 23 { 24 B b; 25 A &a = b; 26 a.func(); 27 return 0; 28 }
5)回避虚函数的机制
在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。使用作用域运算符可以实现这一目的。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class A { 9 public: 10 virtual void func(int x = 1) { 11 std::cout << "A:"; 12 std::cout << x << std::endl; 13 } 14 }; 15 class B :public A { 16 public: 17 virtual void func(int x = 10) override { 18 std::cout << "B:"; 19 std::cout << x << std::endl; 20 } 21 }; 22 int main() 23 { 24 B b; 25 A &a = b; 26 a.A::func(); 27 return 0; 28 }
原文地址:https://www.cnblogs.com/ACGame/p/10315961.html