C++面向对象程序设计

前言

面向对象有三个核心概念,抽象、继承、多态。

数据抽象就是多类事物的共同特征抽象出来,抽象出一个类,这个类定义了接口。

继承是描述类与类之间的关系,是一种包含关系。比如苹果和梨子都是水果,水果是基类,它包含苹果和梨子的共同特征(数据特征、行为特征),而苹果和梨是派生类,有它们自己独有的特征。

多态,c++主要指的是动态绑定,根据里氏替换原则,使用父类的地方都可以用子类去替换。在运行时,根据实际类型(c++称为动态类型)来调用实际类型的方法,已达到复用的目的。

我们在之前 的那一章已经分析过数据抽象的基本知识了。本文主要分析继承和动态绑定(更加严格来说)。

继承和动态绑定对程序有两方面的影响:

  1. 更容易的定义与其他类相似但不完全相同的新类 (继承)
  2. 在一定程度上忽略这些彼此相似的类编写程序(动态绑定)
// 继承
class Quote {
public:
    std::string isbn() const;

    // 希望派生类定义自己的版本
    virtual double net_price(int n) const;  
};

class Bulk_quote : public Quote {
public:
    // 1. virtual 可加可不加;2.override显示的告诉编译器这个方法重写了基类的虚函数
    virtual double net_price(int n) const override; 
};

// 动态绑定
double print_total(ostream &os, const Quote &item, int n) {
    // 使用基类引用编程,运行时根据引用的动态类型选择实际类型的虚函数
    double ret = item.net_price(n);
    cout << ret << endl;
}

Quote basic;
Bulk_quote bulk;

print_total(cout, basic, 20); // 调用 Quote::net_price
print_total(cout, bulk, 20); // 调用 Bulk_quote::net_price

注意:

c++ 中,使用基类的引用或者指针调用虚函数来发生动态绑定


1. 定义基类和派生类

  • 基类

基类集中了派生类共有的特征,通过将函数设置成虚拟函数来说明希望派生类定义自己的版本,而如果派生类没有定义自己的版本,则会继承基类的版本,并且隐式保持着虚拟的状态。

class Quote {
public:
    Quote() = default;
    virtual ~Quote() = default; // 对析构函数进行动态绑定 
};

基类通常需要定义一个虚析构函数,即使该函数不执行任何实际操作。因为,我们知道动态绑定需要基类定义虚函数,如果基类的析构函数不是虚拟的,用基类的引用或者指针来编程,并不会发生动态绑定。

任何构造函数之外的,非静态的函数都可以是虚函数。另外,virtual关键字只能出现在类的内部。

派生类继承基类的成员,但是派生类的成员函数不一定有权访问继承而来的成员。需要看基类定义的成员访问控制符。

  • 派生类

派生类通过类派生列表指明从哪个(哪些)基类继承而来。

class Bulk_quote : public Quote {
public:
    Bulk_quote() = default;
    double net_price() const override;
private:
    int min_qty = 0; // 可以定义派生类自己的数据成员
    double discount = 0.0;  
};

类派生列表包含了派生访问说明符,这个符号说明的是如何继承基类的成员,即基类的成员对于派生类的用户的可见性。和基类的成员访问符的作用没有关系。

派生类可以不覆盖继承的虚函数。另外,派生类中覆盖的函数不必使用virtual关键字。但是一定得记住的是,这个函数一定还是虚的。可以通过override显示告诉编译器,我覆盖了虚方法,请检查。

  • 派生类向基类的类型转换

派生类对象组成部分:

  1. 派生类自己定义的非静态成员子对象。
  2. 继承基类对应的子对象,如果是多继承,则有多个

正因为派生类对象中含有与其基类对应的组成部分,所以

  1. 可以把派生类对象当成基类对象来使用
  2. 能将基类的指针或引用绑定到派生类的对象上来
Quote item; // 基类
Bulk_quote bulk; // 派生类 

Quote *p = &item; // p 指向 Quote对象 
p = &bulk; // p 指向 bulk 的 Quote 部分
Quote &r = bulk; // r 绑定到 bulk 的 Quote 部分
  • 关于构造函数

记住一点:每个类控制它自己的成员初始化过程

因此,我们很容易明白,派生类对象含有从基类继承而来的成员,但是派生类不能直接初始化它们。派生类必须使用基类的构造函数来初始化它们。

Bulk_quote(const string &book, double p, int qty, double disc)
        : Quote(book, p), min_qty(qty), discount(disc) {}
        // 使用基类构造函数初始化基类部分

另外,初始化的顺序是:

  1. 首先初始化基类的部分
  2. 按照声明的顺序依次初始化派生类的数据成员

如果没有指出基类的构造函数,执行基类的默认初始化。

  • 关于静态成员

如果基类定义了静态成员,在整个继承体系中只存在该成员的唯一定义。

  • 防止继承的发生 (final)

final 关键字有两个作用

  1. 定义类为final,不允许其他类继承它
  2. 定义类中的方法为final,不允许其它函数覆盖它

2. 类型转换与继承

理解类型转换非常重要。关键在于理解静态类型和动态类型的关系。

注意:

和内置指针一样,智能指针也支持派生类向基类的转换

变量有静态类型和动态类型。

  • 静态类型指变量声明的类型,编译期可以确定的
  • 动态类型指变量表示的对象的类型,是内存中对象的类型,运行时才可知

那么,容易知道,如果变量或者表达式不是引用也不是指针,那么它的动态类型与静态类型永远是一致的。只有指针或者引用才有可能静态类型与动态类型不一致。

不存在从基类向派生类的隐式类型转换(判断的关键:编译期来检查静态类型转换是否合法)

Quote base;
Bulk_quote *bulkp = &base; // 错误:不能将基类转换成派生类
Bulk_quote &bulkRef = base; // 错误:不能讲基类转换成派生类

Bulk_quote bulk;
Quote *itemp = &bulk; // 正确
Bulk_quote *bulkp = itemp; // 错误:不能将基类转换成派生类,即使动态类型是派生类

当然,如果转换是安全的,可以使用static_cast或者dynamic_cast强制转换

对象之间不存在类型转换,如果派生类转基类,会被切掉一部分,这些是由拷贝构造函数和赋值操作符来决定的。

Bulk_quote bulk;
Quote item(bulk); // 使用Quote::Quote(const Quote &) 拷贝构造函数 
item = bulk;  // 使用Quote::operator=(const Quote &) 赋值运算符

3. 虚函数

使用引用或者指针来发生动态绑定

非虚函数的调用发生在编译期,根据静态类型来调用

一旦函数声明成虚函数,它在所有派生类中都是虚函数

注意

1 派生类的函数如果覆盖继承来的虚函数,它的形参必须与被覆盖的基类虚函数完全一致

2 返回类型也必须与基类函数一致,有一个例外,但返回类型是类本身的指针或引用时,这种例外必须要从 D 到 B 的类型转换是可访问的。

关于默认实参

如果虚函数使用默认实参,基类和派生类的定义的默认实参最好一致。

回避虚函数机制

即使用特定版本的虚函数,使用作用域操作符 ::

double undiscounted = baseP -> Quote::net_price(42); // 强制使用基类的版本而不管动态类型

通常的使用场景是,基类的虚函数执行了一些共同的任务,派生类的版本需要直接调用,并执行一些自己的操作。

如果没有使用作用域操作符,派生类虚函数调用基类版本,会在运行时调用自己,从而导致无限递归。


4. 抽象基类

某些情况下,一些类并不需要被创建,它的作用只是定义一个接口,或者提供公共部分的特征。我们称这样的类为抽象类。c++中并没有像java那样可以声明类为抽象类。c++通过将虚函数声明成纯虚函数,来将一个类作为抽象类。

抽象类可以作为引用和指针使用。

class Disc_quote : public Quote { // Disc_quote 为抽象类,不能创建对象
public:
    Disc_quote() = default;
    double net_price(int) const = 0; // 声明为纯虚函数

protected:
    int quantity = 0;
    double discount = 0;    
};

// 纯虚函数的定义必须在类外
double Disc_quote::net_price(int t) {
    return discount;
}

注意 :

派生类必须要覆盖抽象基类的纯虚函数,否则它们仍然是抽象的。


5. 访问控制与继承

关于这个问题,分两部分来看。

一方面,基类使用访问控制符来表达基类的成员的是否可以访问,三个级别

  • public: 公有的,所有人可见
  • protected: 受保护的,仅派生类和友元可见(用户不可见!!!)
  • private: 私有的,仅友元可见

注意

派生类的成员或友元,只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问权(这时派生类仅仅是一个用户)

class Base {
protected:
    int prot_mem;   
};

class Sneaky : public Base {
    friend void clobber(Sneaky &);
    friend void clobber(Base &);
    int j;  
};

// 因为是Sneaky的友元,可以访问Sneaky的private和protected
void clobber(Sneaky &s) {
    s.j = s.prot_mem = 0; // 正确,clobber能够访问Sneaky对象的private和protected成员
}

// 虽然是友元
void clobber(Base &b) {
    b.prot_mem = 0; // 错误,不能访问一个基类对象的protected成员
}

另一方面,继承有访问控制,分为公有继承、私有继承、受保护继承。这个继承说明的仅仅是基类的成员被派生类继承了之后,它的访问控制权限要如何改变。受到两个因素的影响,其一是在基类中的访问控制符,其二是继承的访问控制符。

  1. 公有继承,基类中访问控制符是什么样,照样是什么样
  2. 受保护继承,基类中是public,则变成protected,基类是protected,还是protected
  3. 私有继承,基类所有都变成private

一定要注意的是,这里仅仅指的基类的成员被继承之后,在派生类的控制访问符变成什么样。

派生访问说明符 对派生类的成员(或友元)能否访问到其直接基类的成员没有什么关系

注意,直接基类!!!

通过以上,我们知道,派生访问符目的是控制派生类用户(包括派生类的派生类)对于基类成员的访问权限。

关于派生类向基类转换的可访问性

记住一点,只有当D公有继承B时,用户代码才能使用派生类向基类的转换;如果D继承B的方式是受保护或者私有继承,则用户代码不能使用该转换

而不论D以什么方式继承B,D的成员函数和友元函数都能够使用派生类向基类的转换。

关于友元和继承

友元关系不传递,不继承

如何改变个别成员的可访问性

使用using声明

class Base {
public:
    int size() const {return n;}
protected:
    int n;  
};

class Derived : private Base { // 私有继承,默认应该是私有的,但。。。
public:
    using Base::size; // 基类size方法的访问权限由前面的public决定
protected:
    using Base::n; // 基类n数据成员由前面的protected决定
};

默认继承保护级别!!!!

  1. class 默认是私有继承
  2. struct 默认是公有继承

6. 继承中的类作用域

记住一点,派生类的作用域嵌套在其基类的作用域之内。

如果一个名字在派生类作用域无法解析,则在外层基类作用域中查找。

注意只会往当前派生类到继承链的顶端找,不会往继承链的末端找

名字查找优先于类型检查

因此,声明在内存作用域的函数并不会重载声明在外层作用域的函数,也就是说派生类的函数不会重载其基类的函数,只会隐藏基类的函数,即使派生类的成员和基类的成员形参列表不一致。(名字查找,只查找函数名)

但是,如前面所说,可以使用作用域操作符调用基类的成员

struct Base {
    int func(); 
};

struct Derived : Base {
    int func(int);  
};

Derived d;
Base b;

b.func(); // 调用 Base::func()
d.func(10); // 调用 Derived::func()
d.func(); // 错误:基类的func被隐藏了
d.Base::func(); // 正确:通过作用域操作符调用 Base::func()

关于虚析构函数

定义虚析构函数的意义在于,如果delete指向派生类对象的基类指针,根据动态绑定,调用的是派生类的析构函数,这正是我们想要的。

如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针会产生未定义的行为。

我们之前还说过一个三\五法则,如果一个类需要析构函数,那么它同样需要拷贝和赋值。注意,基类的析构函数并不遵循,这是一个重要的例外。

虚拟析构函数将阻止合成移动操作。

析构顺序(和构造顺序相反):

  1. 派生类的析构函数首先执行
  2. 然后是基类的析构函数(直到继承链的顶端)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
系统地讲述c语言的基础知识、基本语法以及编程方法,并且结合c阐述面向对象程序设计思想,使读者在掌握c语言语法知识的同时,能够解决现实生活中较简单的问题,并用计算机语言进行描述。本书每一章中都用大量实用性较强的例题阐述基本知识点,同时在每章的最后都提供一个有一定难度且趣味性较强的综合实例,将本章中多个知识点有机地结合起来,力求读者能把理论与实践紧密结合,体会解决实际问题的过程。全书内容精练,重点突出,从实例出发提出概念,突出应用,趣味性强。 语言表达严谨、文字通俗易懂,同时配有题型多样的典型习题,适用于c高级语言程序设计的初学者,可以作为普通高等院校中电子信息专业程序设计基础的教材,也适宜有兴趣学习c的非计算机专业学生作为辅助教材,同时也适合自学。 目录编辑 第1章c简单程序设计1 1.1概述1 1.1.1c语言的发展历史1 1.1.2c程序设计的初步知识2 1.1.3字符集5 1.1.4词法记号5 1.2基本数据型6 1.2.1基本数据型7 1.2.2变量8 1.2.3常量10 1.3运算符和表达式13 1.3.1运算符的优先级14 1.3.2算术运算符及其表达式15 1.3.3关系运算符与逻辑运算符15 1.3.4增1、减1运算符及其表达式17 1.3.5赋值运算符、复合的赋值运算符及其表达式17 1.3.6条件运算符18 1.3.7位操作运算符18 1.3.8其他运算符20 1.3.9数据型转换21 1.3.10型别名22 1.4面向对象设计思想及其实例23 1.4.1程序设计语言的发展23 1.4.2面向过程的程序设计(pop)思想23 1.4.3面向对象程序设计(oop)思想24 1.4.4面向对象思想分析实例——卖报亭24 1.5c上机实践26 1.5.1c程序的实现过程26 1.5.2cbuilder可视化编程环境27 本章小结30 习题31 第2章数据的输入/输出与控制结构34 2.1键盘输入34 2.2屏幕显示输出35 2.3字符数据的输入输出36 2.3.1字符数据的输入与输出36 2.3.2字符串的输入与输出37 2.4程序基本控制结构38 2.4.1语句的概念38 2.4.2算法的基本控制结构41 2.5选择结构42 2.5.1if-else语句42 2.5.2switch语句46 2.6循环结构48 2.6.1while语句与do-while语句48 2.6.2for循环语句50 2.6.3ifgoto实现循环功能51 2.6.4循环的嵌套52 2.7跳转语句53 2.7.1break语句54 2.7.2continue语句54 2.7.3break语句与continue语句的比较54 2.7.4goto语句55 2.8编程实例——水果收银机55 本章小结56 习题57 第3章数组61 3.1数组的基本概念61 3.2数组的定义与数组元素的表示法62 3.2.1数组的定义格式62 3.2.2数组元素的表示方法62 3.3数组的赋值63 3.3.1数组赋初值63 3.3.2数组赋值65 3.4字符数组66 3.4.1字符数组的定义格式66 3.4.2字符数组的赋值66 3.4.3字符数组的输入输出操作67 3.4.4字符串处理函数68 3.5编程实例——选择法排序和josephus问题70 3.5.1选择法排序70 3.5.2josephus问题71 本章小结72 习题72 第4章函数76 4.1函数的定义与调用76 4.1.1函数的定义76 4.1.2函数的声明和调用77 4.2函数的调用方式和参数传递79 4.2.1函数的调用过程79 4.2.2函数的传值调用80 4.2.3函数的引用调用81 4.2.4数组作为函数参数83 4.3函数的嵌套调用和递归调用85 4.3.1函数的嵌套调用85 4.3.2函数的递归调用86 4.4带默认形参值的函数90 4.5内联函数和重载函数92 4.5.1内联函数92 4.5.2重载函数93 4.6编程实例——二进制与十进制的转换94 本章小结95 习题95 第5章程序结构100 5.1全局变量与局部变量100 5.1.1全局变量100 5.1.2局部变量101 5.2静态变量102 5.3存储型103 5.4作用域与生存期104 5.4.1作用域104 5.4.2可见性105 5.4.3生存期105 5.5编译预处理105 5.5.1文件包含106 5.5.2宏定义106 5.5.3条件编译108 5.6多文件结构109 本章小结110 习题110 第6章指针114 6.1指针的概念114 6.1.1指针变量的声明115 6.1.2指针变量的初始化与引用115 6.2指针运算116 6.2.1运算符“ [1]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值