【C++】《C++ Primer 5th》笔记-Chapter15-面向对象程序设计

笔记:
一、OOP:概述
1、面向对象程序设计基于三个基本概念(也是其核心思想):数据抽象、继承和动态绑定。
通过使用数据抽象,我们可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
2、对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数。
3、派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上virtual,但是并不是非得这么做。
3、C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个override关键字。
4、在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。

二、定义基类和派生类
1、作为继承关系中根节点的类通常都会定义一个虚析构函数。(基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此)。
2、基类通过在其成员函数的声明语句之前加上关键字virtual使得该函数执行动态绑定。
任何构造函数之外的非静态函数都可以是虚函数。
关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。
如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
3、派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明。
派生类经常(但不总是)覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。
4、在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键所在。
5、派生类必须使用基类的构造函数来初始化它的基类部分。每个类控制它自己的成员初始化过程。
首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
6、尽管从语法上来说我们可以在派生类构造函数体内给它的公有或受保护的基类成员赋值,但是最好不要这么做。和使用基类的其他场合一样,派生类应该遵循基类的接口,并且通过调用基类的构造函数来初始化那些从基类中继承而来的成员。
7、如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。
8、一条声明语句的目的是令程序知晓某个名字的存在以及该名字表示一个什么样的实体,如一个类、一个函数或一个变量等。
9、为了设计一种不希望其他类继承的类,C++11新标准提供了一种防止继承发生的方法,即在类名后跟一个关键字final。
10、和内置指针一样,智能指针类也支持派生类向基类的类型转换,这意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针内。
11、之所以存在派生类向基类的类型转换是因为每个派生类对象都包含一个基类部分,而基类的引用或指针可以绑定到该基类部分上。
12、如果在基类中含有一个或多个虚函数,我们可以使用dynamic_cast请求一个类型转换,该转换的安全检查将在运行时执行。同样,如果我们已知某个基类向派生类的转换是安全的,则我们可以使用static_cast来强制覆盖掉编译器的检查工作。
13、派生类向基类的自动类型转换只对指针或引用类型有效。
14、当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。
15、尽管自动类型转换只对指针或引用类型有效,但是继承体系中的大多数类仍然(显式或隐式地)定义了拷贝控制成员。因此,我们通常能够将一个派生类对象拷贝、移动或赋值给一个基类对象。不过需要注意的是,这种操作只处理派生类对象的基类部分。

三、虚函数
1、在C++语言中,当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。因为我们直到运行时才能知道到底调用了哪个版本的虚函数,所以所有虚函数都必须有定义。
2、动态绑定只有当我们通过指针或引用调用虚函数时才会发生。当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。
对象的类型是是确定不变的,我们无论如何都不可能令对象的动态类型与静态类型不一致。
3、当我们在派生类中覆盖了某个虚函数时,可以再一次调用virtual关键字指出该函数的性质。然而这么做并非必须,因为一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数。
4、派生类中虚函数的形参必须与基类函数匹配。
派生类中虚函数的返回类型也必须与基类函数匹配,但该规则存在一个例外,当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。
5、在C++11新标准中我们可以使用override关键字来说明派生类中的虚函数。这么做的好处是在使得程序员的意图更加清晰的同时让编译器可以为我们发现一些错误。
6、注意只有虚函数才能被覆盖。我们还能把某个函数指定为final,如果我们已经把函数定义成final了,则之后任何尝试覆盖该函数的操作都将引发错误。
7、final和override说明符出现在形参列表(包括任何const或引用修饰符)以及尾置返回类型之后。
8、和其他函数一样,虚函数也可以拥有默认实参。如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。
所以,如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
9、在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。使用作用域运算符可以实现这一目的,例如:
double lfDiscounted = pFather->AFather::getDiscounted();
该代码调用基类的getDiscounted函数,而不管pFather实际指向的对象类型到底是什么。该调用将在编译时完成解析。
通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。(通常是当一个派生类的虚函数调用它覆盖的基类的虚函数版本时)

四、抽象基类
1、一个纯虚函数无须定义。我们通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯需函数。其中,=0只能出现在类内部的虚函数声明语句处。
值得注意的是,我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。也就是说,我们不能在类的内部为一个=0的函数提供函数体。
2、含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。
我们不能(直接)创建一个抽象基类的对象。
抽象基类的派生类必须给出自己的覆盖基类纯虚函数的定义,否则它们仍将是抽象基类。
3、派生类构造函数只初始化它的直接基类。而且构造的顺序是先基类再派生类自己。

五、访问控制与继承
1、每个类控制它自己的成员初始化过程,与之类似,每个类还分别控制其成员对于派生类来说是否可访问。
2、和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。
派生类的成员和友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。(即,派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员;对于普通的基类对象中的成员不具有特殊的访问权限)
3、派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响。对基类成员的访问权限只与基类中的访问说明符有关。
4、派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限。
派生访问说明符还可以控制继承自派生类的新类的访问权限。

5、对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的;反之则不行。
6、就像友元关系不能传递一样,友元关系同样也不能继承。
当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效。
7、有时我们需要改变派生类继承的某个名字的访问级别,通过使用using声明可以达到这一目的。
通过在类的内部使用using声明语句,我们可以将该类的直接或间接基类中的任何可访问成员(例如,非私有成员)标记出来。using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。
派生类只能为那些它可以访问的名字提供using声明。
8、默认情况下,使用class关键字定义的派生类是私有继承;而使用struct关键字定义的派生类是公有继承的。

六、继承中的类作用域
1、当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。
2、一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致(当使用基类的引用或指针时会发生这种情况),但是我们能使用哪些成员仍然是由静态类型决定的。
3、派生类的成员将隐藏同名的基类成员。但我们可以通过使用作用域运算符来使用一个被隐藏的基类成员。
除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
4、和其他作用域一样,如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉。
5、假如基类与派生类的虚函数接受的实参不同,则我们就无法通过基类的引用或指针调用派生类的虚函数了。
6、和其他函数一样,成员函数无论是否是虚函数都能被重载。
7、如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有的版本,或者一个也不覆盖。
类内using声明的一般规则同样适用于重载函数的名字;基类函数的每个实例在派生类中都必须是可访问的。对派生类没有重新定义的重载版本的访问实际上是对using声明点的访问。

七、构造函数与拷贝控制
1、继承关系对基类拷贝控制最直接的影响是基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。
和其他函数一样,我们通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本。
只要基类的析构函数是虚函数,就能确保当我们delete基类指针时将运行正确的析构函数版本。
注意,如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
2、如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。
3、对于派生类的析构函数来说,它除了销毁派生类自己的成员外,还负责销毁派生类的直接基类;该直接基类又销毁它自己的直接基类,以此类推。
4、此外,某些定义基类的方式也可能导致有的派生类成员成为被删除的函数:
①如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或者不可访问,则派生类中对应的成员将是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作。
②如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分。
③和过去一样,编译器将不会合成一个删除掉的移动操作。当我们使用=default请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数将是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的。
5、派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。
6、和构造函数及赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源。
对象的成员是被隐式销毁的;类似的,派生类对象的基类部分也是自动销毁的。
7、当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。
8、在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数。
与拷贝和移动构造函数一样,派生类的赋值运算符也必须显式地为其基类部分赋值。
9、和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁派生类自己分配的资源。
对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行,然后是基类的析构函数,以此类推,沿着继承体系的反方向直至最后。
10、如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。
11、在C++11新标准中,派生类能够重用其直接基类定义的构造函数。
一个类只初始化它的直接基类,出于同样的原因,一个类也只继承其直接基类的构造函数。
类不能继承默认、拷贝和移动构造函数。如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。
12、派生类继承基类构造函数的方式是提供一条注明了(直接)基类名的using声明语句。
通常情况下,using声明语句只是令某个名字在当前作用域内可见。而当作用于构造函数时,using声明语句将令编译器产生代码。对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数。
如果派生类含有自己的数据成员,则这些成员将被默认初始化。
13、和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别。
而且,一个using声明语句不能指定explicit或constexpr。
14、当一个基类构造函数含有默认实参时,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。例如,如果基类有一个接受两个形参的构造函数,其中第二个形参含有默认实参,则派生类将获得两个构造函数:一个构造函数接收两个形参(没有默认实参),另一个构造函数只接受一个形参,它对应于基类中最左侧的没有默认值的那个形参。
15、继承的构造函数不会被作为用户定义的构造函数来使用,因此,如果一个类只含有继承的构造函数,则它也将拥有一个合成的默认构造函数。

八、容器与继承
1、当我们使用容器存在继承体系中的对象时,通常必须采取间接存储的方式。因为不允许在容器中保存不同类型的元素,所以我们不能把具有继承关系的多种类型的对象直接存放在容器当中。
2、当派生类对象被赋值给基类对象时,其中的派生类部分将被"切掉",因此容器和存在继承关系的类型无法兼容。
3、正如我们可以将一个派生类的普通指针转换成基类指针一样,我们也能把一个派生类的智能指针转换成基类的智能指针。

九、文本查询程序再探
1、当我们另一个类公有地继承另一个类时,派生类应当反映与基类的"是一种(is a)"关系。在设计良好的类体系当中,公有派生类的对象应该可以用在任何需要基类对象的地方。

一些术语:
1、在C++语言中,动态绑定只作用于虚函数,并且需要通过指针或引用调用。
2、基类通常都应该定义一个虚析构函数,即使基类根本不需要析构函数也最好这么做。将基类的析构函数定义成虚函数的原因是为了确保当我们删除一个基类指针,而该指针实际指向一个派生类对象时,程序也能正确运行。
3、抽象基类:含有一个或多个纯虚函数的类,我们无法创建抽象基类的对象。
4、可访问的:能被派生类对象访问的基类成员。可访问性由派生类的派生列表中所用的访问说明符和基类中成员的访问级别共同决定。例如,通过公有继承而来的一个公有成员对于派生类的用户来说是可访问的;而私有继承而来的公有成员是不可访问的。
5、派生类的作用域嵌套在基类作用域当中。
6、私有继承:在私有继承中,基类的公有成员和受保护成员是派生类的私有成员。
7、受保护的继承:在受保护的继承中,基类的公有成员和受保护成员是派生类的受保护成员。
8、纯虚函数:在类的内部声明虚函数时,在分号之前使用了=0。一个纯虚函数不需要(但是可以)被定义。含有纯虚函数的类是抽象基类。如果派生类没有对继承而来的纯虚函数定义自己的版本,则该派生类也是抽象的。
9、通常情况下,重构类的方式是将数据成员和函数成员移动到继承体系的高级别节点当中,从而避免代码冗余。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值