C++ primer摘要(13)---面向对象的程序设计

面向对象的程序设计

概述

  • 封装
- [x] 隐藏对象的属性和实现细节,仅仅对外提供接口和方法
    - 优点:`隔离变化` `便于使用` `提高重用性` `提高安全性`
    - 缺点:`如果封装太多,影响效率` `使用者不能知道代码的具体实现`
  • 继承
- [x] 一个对象直接使用另一个对象的属性和方法
    - 优点:`减少重复的代码` `继承是多态的前提` `继承增加了类的耦合性`
    - 缺点:`继承在编译时刻就定义,无法在运行时刻改变父类继承的实现` `父类通常至少定义了子类的部分行为,父类的改变都可能影响子类的行为` `入股继承下来的子类不适合解决新问题,父类必须重写或替换,那么这种依赖关系就限制了灵活性,最终限制了复用性`
  • 多态
- [x] C++中有两种多态,`动多态(运行期多态)`以及`静多态(编译器多态)`,而静多态主要通过模板实现,宏也是实现静多态的一种途径,动多态在C++中是通过虚函数实现的,即在基类中存在一些接口(一般为纯虚函数),子类必须重载这些接口,这样通过使用基类的指针或者引用指向子类的对象,就可以实现调用子类对应的函数的功能,动多态的函数调用机制是执行期才能进行确定,所以它是动态的
- [x] 接口的多种不同实现方式即为多态
    - 优点:`大大提高了代码的可复用性` 
    - 缺点:`易读性比较差,调试困难` `模板只能定义在.h文件中,当工程大了之后,编译时间较长` 
  • 对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数
class Quote{
public:
    std::string isbn() const;
    virtual double net_price(std::size_n n) const;

};
  • 派生类必须通过使用派生类列表明确指出它是从哪个基类继承而来
class Bulk_quote : public Quote{
public:
    double net_price(std::size_n) const override;
};
  • C++11允许派生类显式的注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个override关键字

virtual关键字解析

  • 在类base中加了virtual关键字的函数就是虚拟函数,基类的函数调用如果有virtual则根据多态性调用派生类,如果没有virtual则是正常的静态函数调用,还是调用基类的
  • virtual关键字与重载的区别
 - [x] 重载的几个函数必须在同一个类中;覆盖的函数必须在有继承关系的不同的类中
 - [x] 覆盖的几个函数必须函数名、参数、返回值都相同;重载函数必须函数名相同,参数不同,参数不同的目的就是为了在函数调用的时候编译器能够通过参数来判断程序是在调用哪个函数,这也就很自然地解释了为什么函数不能通过返回值不同来重载,因为程序在调用函数的时候很有可能不关心返回值,编译器就无法从代码中看出程序在调用哪个函数
 - [x] 覆盖的函数前必须加关键字virtual
  • 重载和virtual没有任何瓜葛,加不加都不影响重载的运作

定义基类和派生类

定义基类
  • 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此
  • 在C++语言中,基类必须将它的两种成员函数区分开来:
- [x] 一种是基类希望派生类进行覆盖的函数,对于这种函数,基类通常将其定义为`虚函数`
- [x] 另一种是基类希望派生类直接继承而不要改变的函数
  • 当我们使用指针或引用调用虚函数时,该调用将被动态绑定,根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本
  • 基类通过在其成员函数的声明语句之前加上关键字virtual使得该函数执行动态绑定,任何构造函数之外的非静态函数都可以是虚函数
  • 关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义
  • 如果基类把一个函数声明称虚函数,则该函数在派生类中隐式的也是虚函数
  • 成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时
  • 在某些时候基类中有这样一种成员,基类希望派生类有权访问该成员,同时禁止类外其他用户访问,称之为(受保护的protected)的成员
定义派生类
  • 派生类必须使用派生类列表明确指出它是从哪个(哪些基类继承而来的)
  • 因为派生类对象中含有基类对应的组成部分,所以我们能把派生类的对象当成基类来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上
  • 编译器会隐式地执行派生类到基类的转换
  • 这种隐式特性意味着我们可以把派生类对象或派生类对象的引用在需要基类引用的地方,同样的,也可以把派生类对象的指针用在需要基类的地方
  • 在派生类对象中含义与其基类对应的组成部分,这一事实是继承的关键所在
  • 每个类控制它自己的成员初始化过程
  • 派生类构造函数同样是通过构造函数初始化列表来将实参传递给基类构造函数的
  • 派生类首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员
  • 每个类负责定义各自的接口,要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分也是如此
  • 由上述原因,派生类对象不能直接初始化基类成员尽管从语法上来说我们可以在派生类构造函数体内给它的公有或私有或受保护的基类成员赋值,但最好不要这么做,和使用基类的其他场合一样,派生类应该组合寻基类的接口,并且通过调用基类的构造函数来初始化那些从基类中继承而来的成员
  • 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义,不论从基类中派生出多少个派生类,对于每个静态成员来说都只存在唯一的实例
  • 静态成员遵循通用的访问权限控制,如果基类中的成员是private的,则派生类无权访问它,假设某静态成员是可访问的,则我们既能通过某些基类使用它也能通过派生类使用它
  • 入股我们想将某个类用作基类,则该类必须已经定义而非仅仅声明
  • 一个类不能派生它自身
  • 一个类是基类,同时它也可以是一个派生类
  • 有时我们会定义这样一种类,我们不希望其他类继承它,或者不想考虑它是否适合作为一个基类,为了实现这一目的,C++11新标准提供了一种防止继承发生的方法,即在类名字后面跟一个关键字final
类型转换与继承
  • 当使用基类的引用(或指针)时,实际上我们并不清楚该引用(或指针)所绑定的对象的真实类型,该对象可能是基类的对象,也可能是派生类的对象
  • 之所以存在派生类向基类的类型转换是因为每个派生类对象都包含一个基类部分,而基类的引用或指针可以绑定到该基类部分上
  • 一个基类的对象既可以以独立的形式存在,也可以作为派生类对象的一部分存在有,如果基类对象不是派生类的一部分,则它只含有基类定义的成员,而不含有派生类定义的成员
  • 因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换
  • 即使一个基类指针或引用绑定在一个派生类对象上,也不能执行从基类向派生类的转换
  • 派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和积累类型之间不存在这样的转换
  • 当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将会被忽略掉
  • 存在继承关系的类型间的转换规则
- [x] 从派生类向基类的类型转换只对指针或引用类型有效
- [x] 基类向派生类不存在隐式类型转换
- [x] 和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行

虚函数

  • 当某个虚函数通过指针或引用被调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数,被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个
  • 动态绑定只有当我们通过指针或引用调用虚函数时才会发生
  • OOP的核心思想是多态性,我们把具有继承关系的多个类型成为多态类型,因为我们能使用这些类型的多种形式而无需在意它们的差异,引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在
  • 当且仅当通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同
  • 当我们在派生类中覆盖了某个虚函数时,可以再一次使用virtual关键字指出该函数的性质,但并不必须,因为一旦某个函数被声明为虚函数,则在所有派生类中它都是虚函数
  • 一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致,同样,派生类中虚函数的返回类型也必须与基类函数匹配(当类的虚函数返回类型是类本身的指针或引用时上述规则无效)
  • 基类中的虚函数在派生类中隐含地也是一个虚函数,当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配
  • 在C++11中我们可以使用override关键字来来说明派生类中的虚函数
  • 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致
  • 通常情况等下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制
  • 如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归

抽象基类

  • 在虚函数体的位置(即在声明语句的分号之前)书写=0就可以将一个函数说明为纯虚函数,其中=0只能出现在类内部的虚函数声明语句处
  • 含有纯虚函数的类是抽象基类
- [x] `抽象基类`负责定义接口,而后续的其他类可以覆盖该接口
  • 我们不能直接创建一个抽象基类的对象
  • 派生类构造函数只初始化它的直接基类

访问控制与继承

  • protected说明符可以看作是publicprivate中和后的产物
- [x] 和私有成员类似,受保护的成员对于类的用户来说是不可访问的
- [x] 和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的
- [x] 派生类的成员或友元只能通过派生类对象来凡哥维纳基类的受保护成员,派生类对于一个基类对象中的受保护成员没有任何访问特权
  • 某个类对其继承而来的成员的访问权限受到两个因素影响
- [x] 在基类中该成员的访问说明符
- [x] 在派生类的派生列表中的访问说明符
  • 对于代码中的某个给定节点来说,如果1基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的,反之则不行
  • 就像友元关系不能传递一样,友元关系同样也不能继承,基类的友元在访问派生类成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员
  • 不能继承友元关系,每个类各自负责控制各自成员的访问权限
  • 通过在类的内部使用using声明语句,我们可以将该类的直接或间接基类中的任何可访问成员标记出来,using声明语句中名字的访问权限由该using声明语句之前的访问说明符决定
class Base{
public:
    std::size_t size() const {return n;}
protected:
    std::size_t n;
};

class Derived : private Base{   //private继承
public:
    using Base::size;   
protected:
    using Base::n;
};
  • 派生类只能为那些它可以访问的名字提供using声明
  • 默认的继承保护级别
class Base{/***/};
struct D1 : Base {/***/};       //struct默认public继承
class D2 : Base {/***/};        //class默认private继承
  • 人们常常有一种错觉,认为在使用struct关键字和class关键字定义的类之间还有更深层次的差别,事实上,唯一的差别就是默认成员访问说明符以及默认派生访问说明符,除此之外,再无其他不同之处
  • 一个私有派生类最好显式的将private声明出来,而不要仅仅依赖于默认的设置,显式声明的好处是可以令私有继承关系清晰明了,不至于产生误会

继承中的类作用域

  • 除了覆盖继承而来的虚函数中,派生类最好不要重用其他定义在基类中的名字
  • 名字查找先于类型检查
  • 如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员,即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉
strquct Base{
    int memfcn();
};

struct Derived : Base{
    int memfcn(int );       //隐藏基类的memfcn
};

Derived d;
Base b;
b.memfcn();         //调用Base:memfcn
d.memfcn(10);       //调用Derived:memfcn
d.memfcn();         //错误:参数列表为空的me'mfcn被隐藏了
d.Base:memfcn();    //正确:调用Base:memfcn
  • 基类与派生类中的虚函数必须有相同的形参列表,假如基类与派生类的虚函数接受的实参不同,则我们无法通过基类的引用或指针调用派生类的虚函数了
  • 对于虚函数的执行,编译器产生的代码将在运行时确定使用虚函数的哪个版本,判断的根据是该指针所绑定对象的真实类型

构造函数与拷贝控制

虚析构函数
  • 在基类中将析构函数定义成虚函数可以确保执行正确的析构函数版本
  • 如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为
  • 一个基类总是需要析构函数,而且它能将析构函数设定为虚函数,如果一个类定义额析构函数,即使它通过=default的形式使用了合成版本,编译器也不会为这个类合成移动操作
合成拷贝控制与继承
  • 对于派生类的析构函数来说,它除了销毁派生类自己的成员外,还负责销毁派生类的直接基类;该基类又销毁它自己的直接基类,以此类推直至继承链的顶端
派生类的拷贝控制成员
  • 派生类的构造函数在其初始化阶段不但要初始化派生类自己的成员,还负责初始化派生类对象的基类部分,因此,派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员,类似的,派生类赋值运算符也必须为其基类部分的成员赋值
  • 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象
  • 在默认情况下,基类默认构造函数初始化派生类对象的基类部分,如果我们想拷贝或移动基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类地拷贝(或移动)构造函数
  • 与拷贝和移动构造函数一样,派生类地赋值运算符也必须显式地为其基类部分赋值
  • 如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对于地虚函数版本
继承的构造函数
  • 类不能继承默认、拷贝和移动构造函数,如果派生类没有定义这些构造函数,则编译器将为派生类合成它们
  • 派生类继承基类构造函数的方式提供了一条直接注明基类名using声明语句
class Bulk_quote : public Disc_quote{
public:
    using Disc_quote::Disc_quote;   //继承Disc_quote的构造函数
    double net_price() const;
};

容器与继承

  • 当派生类对象被赋值给基类对象时,其中的派生类部分将被'切掉',因此容器和存在继承关系的类型无法被兼容
  • 当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针(更好的选择是智能指针),和往常一样,这些指针所指对象的动态类型可能是基类类型,也可能是派生类类型

继承和组合

  • 当我们令一个类公有的继承另一个类时,派生类应当反应与基类的‘是一种(Is A)’关系,在设计良好的类体系中,公有派生类的对象应该可以用在任何需要基类对象的地方
  • 类型之间的另一种常见关系是‘有一个(Has A)’关系,具有这种关系的类暗含成员的意思

小结

  • 继承使得我们可以编写一些新的类,这些类既能共享其基类的行为,又能根据需要覆盖或添加行为
  • 动态绑定使得我们可以忽略类型之间的差异,其机理是在运行时根据对象的动态类型来选择运行哪个版本
  • 继承和动态绑定的结合使得我们能够编写具有特定类型行为但又独立于类型的程序
  • 在C++中,动态绑定只作用于虚函数,并且需要通过指针或引用调用
  • 在派生类对象中包含有与它的每个基类对应的的子对象,因为所有派生类对象都含有基类部分,所以我们能将派生类的引用或指针转换为一个可访问的基类引用或指针
  • 当执行派生类的构造、拷贝、移动和赋值操作时,首先构造、拷贝、移动和赋值其中的基类部分,然后才轮到派生类部分,析构函数的执行顺序刚好相反,先销毁派生类,接下来执行基类子对象的析构函数
  • 基类通常应该定义一个虚析构函数,即使基类根本不需要析构函数也最好这么做,将基类的析构函数定义为虚函数的原因是为了确保当我们删除一个基类指针,而该指针实际指向一个派生类对象时,程序也能正常运行
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357