面向对象程序设计——抽象基类,访问控制与继承,继承中的类作用域,拷贝函数与拷贝控制
Posted acgame
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面向对象程序设计——抽象基类,访问控制与继承,继承中的类作用域,拷贝函数与拷贝控制相关的知识,希望对你有一定的参考价值。
一、抽象基类
1)纯虚函数
和普通的虚函数不同,一个纯虚函数无须定义。我们通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数。其中,=0只能出现在类内部的虚函数声明语句处。
值得注意的是,我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。也就是说,我们不能在类的内部为一个=0的函数提供函数体。
2)含有纯虚函数的类是抽象基类
含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。我们不能直接创建一个抽象基类的对象。
3)派生类构造函数只初始化它的直接基类
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 ~Quote() = default; // 对析构函数进行动态绑定 15 private: 16 std::string bookno; // 书籍的ISBN编号 17 protected: 18 double price; // 代表普通状态下不打折的价格 19 }; 20 21 class DiscQuote :public Quote { 22 public: 23 DiscQuote() = default; 24 DiscQuote(const std::string &book, double price, 25 std::size_t qty, double disc): 26 Quote(book, price), quantity(qty), discount(disc){} 27 virtual double net_price(std::size_t n) const = 0; 28 protected: 29 std::size_t quantity; // 折扣适用的购买量 30 double discount; // 表示折扣 31 }; 32 33 class BulkQuote :public DiscQuote { 34 public: 35 BulkQuote() = default; 36 BulkQuote(const std::string &_bookno, double _price, std::size_t qty, double disc) 37 :DiscQuote(_bookno, _price, qty, disc){} 38 virtual double net_price(std::size_t n) const override; 39 private: 40 }; 41 42 double BulkQuote::net_price(std::size_t n) const { 43 std::cout << "BulkQuote::net_price" << std::endl; 44 if (n >= quantity) 45 return n * (1 - discount) * price; 46 return n * price; 47 } 48 49 int main() 50 { 51 return 0; 52 }
二、访问控制与继承
1)受保护的成员
一个类使用protected关键字来声明那些它希望与派生类分享但是不想被其他公共访问使用的成员。protected说明符可以看做是public和private中和后的产物:
- 和私有成员类似,受保护的成员对于类的用户来说是不可访问的。
- 和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。
- 派生类的成员或友元只能访问派生类对象中的基类部分的受保护成员;对于普通的基类对象中的成员不具有特殊的访问权限。
1 class A { 2 protected: 3 int mem; 4 }; 5 class B :public A { 6 friend void func(B&); 7 friend void func(A&); 8 }; 9 void func(B &item) { 10 item.mem = 233; // 可以访问mem 11 } 12 void func(A &item) { 13 item.mem = 233; // 不能访问mem 14 }
2)公有、私有和受保护继承
某个类对其继承而来的成员的访问权限受到两个因素影响:一是在基类中该成员的访问说明符,二是在派生类的派生列表中的访问说明符。
派生类说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限。
3)派生类向基类转换的可访问性
派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响。假定D继承B:
- 只有当D公有继承B时,用户代码才能使用派生类向基类的转换。
- 不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
- 如果D继承B的方式是公有的或者受保护的,则D的派生类的成员和友元可以使用D到B的类型转换;反之,如果D继承B的方式是私有的,则不能使用。
4)友元和继承
就像友元关系不能传递一样,友元关系同样也不能继承。基类的友元在访问派生类成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class A { 9 friend class C; 10 protected: 11 int a; 12 }; 13 class B :public A { 14 protected: 15 int b; 16 }; 17 class C { 18 public: 19 int f(A a) { return a.a; } 20 int f2(B b) { return b.b; } // 错误:C不是B的友元 21 //对基类的访问权限由基类本身控制,即使对于派生类的基类部分也是如此 22 int f3(B b) { return b.a; } // 正确:C是A的友元 23 }; 24 int main() 25 { 26 return 0; 27 }
5)改变个别成员的可访问性
通过在类的内部使用using声明语句,我们可以将该类的直接或间接基类中的任何可访问成员标记出来。using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。
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 A(int x=1024):mem(x){} 11 protected: 12 int mem; 13 }; 14 class B :public A { 15 public: 16 using A::mem; 17 B(int x = 1024):A(x){} 18 }; 19 int main() 20 { 21 B b; 22 std::cout << b.mem << std::endl; 23 return 0; 24 }
6)默认的继承级别
默认情况下,使用class关键字定义的派生类是私有继承的;而使用struct关键字定义的派生类是公有继承的。
三、继承中的类作用域
每个类定义自己的作用域,在这个作用域内我们定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。
1)在编译时进行名字查找
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致(当使用基类的引用或指针时会发生这种情况),但是我们能使用哪些成员仍然是由静态类型决定的。
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 }; 11 class B:public A { 12 public: 13 void f() { 14 std::cout << "hello" << std::endl; 15 } 16 }; 17 int main() 18 { 19 B b; 20 B *bp = &b; 21 A *ap = &b; 22 bp->f(); // 正确:bp的类型是B* 23 ap->f(); // 错误:ap的类型是A* 24 return 0; 25 }
ap的类型是A的指针,意味着对f的搜索从A开始,显然A不包含名为f的成员。
2)名字冲突与继承
和其他作用域一样,派生类也能重用定义在在其直接基类或间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字:
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 Base():mem(0){} 11 protected: 12 int mem; 13 }; 14 class Derived: public Base { 15 public: 16 Derived(int i):mem(i){} 17 void show() { 18 std::cout << mem << std::endl; 19 } 20 protected: 21 int mem; 22 }; 23 int main() 24 { 25 Derived d(1024); 26 d.show(); 27 return 0; 28 }
3)通过作用域运算符来使用隐藏的成员
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 Base():mem(0){} 11 protected: 12 int mem; 13 }; 14 class Derived: public Base { 15 public: 16 Derived(int i):mem(i){} 17 void show() { 18 std::cout << Base::mem << std::endl; 19 } 20 protected: 21 int mem; 22 }; 23 int main() 24 { 25 Derived d(1024); 26 d.show(); 27 return 0; 28 }
4)一如往常,名字查找先于类型检查
如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也会被隐藏掉。
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 void func() { 11 std::cout << "Base" << std::endl; 12 } 13 }; 14 class Derived: public Base { 15 public: 16 void func(int) { 17 std::cout << "Derived" << std::endl; 18 } 19 }; 20 int main() 21 { 22 Derived d; 23 d.func(1024); 24 //d.func(); // 错误:参数列表为空的func被隐藏了 25 d.Base::func(); // 正确:调用Base::func 26 return 0; 27 }
5)虚函数与作用域
假如基类与派生类的虚函数接受的实参不同,则我们就无法通过基类的引用或指针调用派生类的虚函数了。
6)覆盖重载的函数
和其他函数一样,成员函数无论是否是虚函数都能被重载。派生类可以覆盖重载函数的0个或多个实例。如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有的版本,或者一个也不覆盖。
有时一个类仅需覆盖集合中的一些而非全部函数,此时,如果我们不得不覆盖基类中的每一个版本的话,显然操作将极其繁琐。一种好的解决方案是为重载的成员提供一条using声明语句,这样我们就无须覆盖基类中的每一个重载版本了。using声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载版本实例添加到派生类作用域中。此时,派生类只需要定义其特有的函数就可以了,而无须为继承而来的其他函数重新定义。
类内using声明的一般规则同样适用于重载函数的名字,基类函数的每个实例在派生类中都必须是可访问的。对派生类没有重新定义的重载版本的访问实际上是对using声明点的访问。
四、拷贝函数与拷贝控制
1、虚析构函数
当我们继承一个动态分配的对象的指针将执行析构函数。如果该指针指向继承体系中的某个类型,则有可能出现指针的静态类型与被删除对象的动态类型不符的情况,我们通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本,和其他虚函数一样,析构函数的虚属性会被继承。因此,无论派生类使用合成的析构函数还是定义自己的析构函数,都将是虚函数。只要基类的析构函数是虚函数,就能确保当我们delete基类指针时将运行正确的析构函数版本。
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 virtual ~Base() { 11 std::cout << __FUNCTION__ << std::endl; 12 } 13 }; 14 class Derived: public Base { 15 public: 16 ~Derived() { 17 std::cout << __FUNCTION__ << std::endl; 18 } 19 }; 20 int main() 21 { 22 Base *p = new Derived; 23 delete p; 24 return 0; 25 }
1)虚析构函数将阻止合成移动操作
如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。
2、合成拷贝控制与继承
基类或派生类的合成拷贝控制成员的行为与其他合成的构造函数、赋值运算符或析构函数类似:它们对类本身的成员依次进行初始化、赋值或销毁的操作。此外,这些合成的成员还负责使用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁的操作。
对于派生类的析构函数来说,它除了销毁派生类自己的成员外,还负责销毁派生类的直接基类;该直接基类又销毁它自己的直接基类,以此类推至继承链的顶端。
1)派生类中删除的拷贝控制与基类的关系
某些定义基类的方式可能导致有的派生类成员称为被删除的函数:
- 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或不可访问的,则派生类中对应的成员将是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作。
- 如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分。
- 编译器将不会合成一个删除掉的移动操作。当我们使用=default请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数将是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是删除的。
2)移动操作与继承
如前所述,大多数基类都会定义一个虚析构函数。因此在默认情况下,基类通常不含有合成的移动操作,而且在它的派生类中也没有合成的移动操作。
因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当我们确实需要执行移动操作时应该首先在基类中定义。
3、派生类的拷贝控制成员
派生类构造函数在其初始化阶段中不但要初始化派生类自己的成员,还负责初始化派生类对象的基类部分。因此,派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。类似的,派生类赋值运算符也必须为其基类部分的成员赋值。
和构造函数及赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源。如前所述,对象的成员是被隐式销毁的;类似的,派生类的基类部分也是自动销毁的。
1)定义派生类的拷贝或移动构造函数
当为派生类定义拷贝或移动构造函数时,我们通常使用对应的基类构造函数初始化对象的基类部分。
在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数。
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 Base():x(0){} 11 Base(const Base &item):x(item.x){ 12 std::cout << "Base-copy" << std::endl; 13 } 14 Base(Base &&item):x(std::move(item.x)) { 15 std::cout << "Base-move" << std::endl; 16 } 17 virtual ~Base() { 18 //std::cout << __FUNCTION__ << std::endl; 19 } 20 protected: 21 int x; 22 }; 23 class Derived: public Base { 24 public: 25 Derived():Base(), y(0){} 26 Derived(const Derived &item) :Base(item), y(item.y) { 27 std::cout << "Derived-copy" << std::endl; 28 } 29 Derived(Derived &&item) :Base(std::move(item)), y(std::move(item.y)) { 30 std::cout << "Derived-move" << std::endl; 31 } 32 ~Derived() { 33 //std::cout << __FUNCTION__ << std::endl; 34 } 35 protected: 36 int y; 37 }; 38 int main() 39 { 40 Derived d; 41 Derived d2(d); 42 std::cout << "------------" << std::endl; 43 Derived d3(std::move(d)); 44 return 0; 45 }
2)派生类赋值运算符
派生类的赋值运算符也必须显式地为其基类部分赋值。
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 Base():x(0){} 11 Base(const Base &item):x(item.x){ 12 std::cout << "Base-copy" << std::endl; 13 } 14 Base(Base &&item):x(std::move(item.x)) { 15 std::cout << "Base-move" << std::endl; 16 } 17 Base &operator=(const Base &item) { 18 int temp = item.x; 19 x = temp; 20 std::cout << "Base-operator" << std::endl; 21 return *this; 22 } 23 virtual ~Base() { 24 //std::cout << __FUNCTION__ << std::endl; 25 } 26 protected: 27 int x; 28 }; 29 class Derived: public Base { 30 public: 31 Derived():Base(), y(0){} 32 Derived(const Derived &item) :Base(item), y(item.y) { 33 std::cout << "Derived-copy" << std::endl; 34 } 35 Derived(Derived &&item) :Base(std::move(item)), y(std::move(item.y)) { 36 std::cout << "Derived-move" << std::endl; 37 } 38 Derived &operator=(const Derived &item) { 39 Base::operator=(item); 40 int temp = item.y; 41 y = temp; 42 std::cout << "Derived-operator" << std::endl; 43 return *this; 44 } 45 ~Derived() { 46 //std::cout << __FUNCTION__ << std::endl; 47 } 48 protected: 49 int y; 50 }; 51 int main() 52 { 53 Derived d1, d2; 54 d1 = d2; 55 return 0; 56 }
3)派生类析构函数
如前所述,在析构函数体执行完成后,对象的成员会被隐式销毁。类似的,对象的基类部分也是隐式销毁的。因此,和构造函数及赋值运算符不同的是,派生类构造函数只负责销毁由派生类自己分配的资源。
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 Base():x(0){} 11 Base(const Base &item):x(item.x){ 12 std::cout << "Base-copy" << std::endl; 13 } 14 Base(Base &&item):x(std::move(item.x)) { 15 std::cout << "Base-move" << std::endl; 16 } 17 Base &operator=(const Base &item) { 18 int temp = item.x; 19 x = temp; 20 std::cout << "Base-operator" << std::endl; 21 return *this; 22 } 23 virtual ~Base() { 24 std::cout << __FUNCTION__ << std::endl; 25 } 26 protected: 27 int x; 28 }; 29 class Derived: public Base { 30 public: 31 Derived():Base(), y(0){} 32 Derived(const Derived &item) :Base(item), y(item.y) { 33 std::cout << "Derived-copy" << std::endl; 34 } 35 Derived(Derived &&item) :Base(std::move(item)), y(std::move(item.y)) { 36 std::cout << "Derived-move" << std::endl; 37 } 38 Derived &operator=(const Derived &item) { 39 Base::operator=(item); 40 int temp = item.y; 41 y = temp; 42 std::cout << "Derived-operator" << std::endl; 43 return *this; 44 } 45 ~Derived() { 46 std::cout << __FUNCTION__ << std::endl; 47 } 48 protected: 49 int y; 50 }; 51 int main() 52 { 53 Derived d; 54 return 0; 55 }
对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行,然后是基类的析构函数,以此类推,沿着继承体系的反方向直至最后。
4)在构造函数和析构函数中调用虚函数
如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。
3、继承的构造函数
以上是关于面向对象程序设计——抽象基类,访问控制与继承,继承中的类作用域,拷贝函数与拷贝控制的主要内容,如果未能解决你的问题,请参考以下文章