C++启蒙笔记(八)---类继承、动态内存分配

一、基本概念

  • 代码复用:继承的基本功能,封装起来的基类稳定性、安全性和效率都较高

1.1派生类

  • 派生类:继承了基类的数据成员和成员函数,派生类中只需写新增的数据成员和新增的成员函数即可
  • 派生类构造函数:
    • a、先调用基类构造函数,然后调用派生类构造函数
    • b、若未显式调用基类构造函数,则隐式调用
  • 派生类析构函数:先调用派生类的析构函数,然后调用基类的析构函数

1.2 继承关系

  • is-a关系
    • 全称:is-a-kind-of
    • 单向性:person包含所有singer,但singer不包含所有person
    • 扩展性:派生类可在基类基础上添加属性,但不可删减
  • has-a关系
    • 例如:meal包含fruit,但是不能从meal派生出fruit
    • 处理:将fruit类作为meal的一个数据成员

二、常规写法

  • 合并写法:可以把以下三个放到一个文件里
  • 头文件写法:分成三个文件,头文件、类实现文件、主程序文件,三者找地方放

2.1 头文件

  • person.h
    // 这个PERSON_H_是自己命名的,不必跟文件名一致
    #ifndef PERSON_H_
    #define PERSON_H_
    // 以下内容会被原封不懂的插入#include "person.h"语句处
    #include <iostream>
    #include <string>
    
    using namespace std;
    
    // 基类
    class Person
    {
    private:
        string _name;
        int _age;
        // 静态数据成员:见下注释,记录类创建了多少个
        static int count;
    
    public:
    	// const静态数据成员:见下注释,记录创建对象的基类名字
    	static const string  title;
    	// 常规构造函数,接收0、1、2个参数
        Person(const string &name = "未命名", int age = 0);
        ~Person();
        void show() const;
    };
    
    // 派生类:指明基类是Person,继承方式为public,
    //    可访问public,protected,不可访问private
    class Singer : public Person
    {
    private:
        string _style;
    
    public:
        Singer(const string &name = "未命名", int age = 0,
               const string &style = "无风格");
        void show() const;
    };
    #endif
    
    • 静态数据成员:
      • 功能:作为声明变量的类所创建的对象共享变量
      • 声明及初始化:类声明中声明,在类方法文件中初始化,防止多次引入头文件,导致的多次初始化
    • 静态const数据成员
      • 声明及初始化:同静态数据成员
      • 功能:创建的常量生命周期跟类同长,避免频繁创建销毁

2.2 类实现

  • person.cpp
    // 注意路径的写法,同级目录可只写文件名
    #include "./person.h"
    
    // 基类中静态成员初始化
    int Person::count = 0;
    // 基类中const静态成员初始化
    const string Person::title = "Person类";
    
    // 基类的成员函数定义
    // 类构造函数:分号后为成员初始化列表形式,只能用于构造函数
    //           分号前括号内为形参,分号后_name(name),意义同_name=name;
    Person::Person(const string &name, 
    			   int age) : _name(name), _age(age) { count += 1; }
    ~Person(){ count -= 1; };
    // 基类的常规成员函数
    void Person::show() const
    {
        cout << "姓名是:" << _name << endl;
        cout << "年龄是:" << _age << endl;
        cout << "当前Person个数:"<< count << endl;
    }
    
    // 派生类的成员函数定义
    // 派生类构造函数:成员初始化列表,且调用了基类的构造函数
    Singer::Singer(const string &name, int age,
                   const string &style) : Person(name, age), _style(style){};
    // 派生类的常规成员函数
    void Singer::show() const
    {	// 派生类中调用基类的成员函数方法
        Person::show();
        cout << "风格为:" << _style << endl;
        // 生成了15个*号
        string s(15, '*');
        cout << s << endl;
    }
    

2.3 主程序

  • main.cpp
    // 注意路径的写法,同级目录可只写文件名
    #include "./person.h"
    
    int main()
    {	
    	// 类数组列表初始化
        Singer singer[3] = {Singer("小明", 15, "hip-hop"),
                            Singer("小李", 13, "blue")};
        singer[0].show();
        singer[1].show();
        // 这里调用的是基类Person的show函数
        singer[2].Person::show();
    
    	// 调用默认构造函数,生成一个对象,count增加1
    	Singer temp;
    	temp.show();
    }
    

2.4 编译及显示

  • 编译命令
    >> g++ main.cpp person.cpp person.h
    >> ./a.out
    
  • 显示
    姓名是:小明
    年龄是:15
    当前Person个数:3
    风格为:hip-hop
    ***************
    姓名是:小李
    年龄是:13
    当前Person个数:3
    风格为:blue
    ***************
    姓名是:未命名
    年龄是:0
    当前Person个数:3
    姓名是:未命名
    年龄是:0
    当前Person个数:4
    风格为:无风格
    

三、多态公有继承

  • 名词释义

    名称释义
    多态即具有多种形态,同一方法,在派生类和基类中的行为是不同的
    多态用途函数虚实参传递时可用这一特性,实参为派生类对象(singer)或地址(&singer),虚参为基类绑定(&rp)或指针(*pp)
    重载问题若有基类有重载声明,派生类不可单独重载其中一个,需全部重写
    派生类不可继承函数构造、析构、=(用目标对象修改已初始化对象) 函数
    能默认生成函数构造(空的)、析构(空的)、=(默认逐数据成员项复制,遇到指针浅浅复制)、& 函数
  • 多态调用指向(见2.3.1代码)

    • 合法指向:基类的指针或绑定可以指向派生类,通过等号右侧(指针或绑定指向的对象)的对象决定调用哪个类的方法
    • 限制指向:派生类指针或绑定不可以指向基类,除非显式的定义了构造函数派生类(&基类)

3.1 虚方法

  • 功能:若要在派生类中重新定义基类的方法,将它设置为虚方法,否则为非虚方法(效率高相对高)
  • 头文件person.h
    ...
    
    // 基类
    class Person
    {
    private:
        ...
    
    public:
    	...
    	// 只在基类增加virtual关键字即可
        virtual void show() const;
    	...
    	// 虚析构函数:基类内声明,规范析构函数调用顺序,
    	// 先调用派生类析构函数,再调用基类析构函数
    	// 若无此句,在main.cpp中,a、b完成后将只调用基类的析构函数
    	virtual ~Person(){}
    };
    
    class Singer : public Person
    {
    ...
    }
    ...
    #endif
    
  • 代码实现main.cpp
    #include "person.h"
    
    int main()
    {
        Singer singer("小明", 15, "hip-hop");
    
    	// 以下三个都是调用派生类的show函数,若person.h中无virtual
    	// 以下前两个调用的是基类的show函数,最后个还是派生类的show函数
    	// 注意体会将其应用在函数虚实参传递中的效果
    
        // a、基类引用可以绑定派生类
        Person &rp = singer;
        rp.show();
    
        // b、基类指针可以指向派生类
        Person *pp = &singer;
        pp->show();
    	
        singer.show();
    }
    

3.2 抽象基类

  • 定义:抽象基类(abstract base class,ABC),提取两个类的共性,形成抽象基类
  • 纯虚函数:只可在ABC中创建,且ABC中至少包含一个
  • 特点:不可创建ABC的对象,可应用虚函数技术指向派生类
  • 代码示例
    #include <iostream>
    #include <string>
    
    using namespace std;
    
    // 基类:构造、析构、复制函数均采用默认
    class Person
    {
    public:
    	// 纯虚函数:基类中,函数后加=0即可生成纯虚函数,同时将基类转换成ABC
    	// 允许:纯虚函数是否定义都可,=0也可以应用在非虚函数上
        virtual void show() const = 0;
        // 虚析构函数:规范析构函数调用顺序
        virtual ~Person(){};
    };
    
    // 派生类Singer:构造、析构、复制函数均采用默认
    class Singer : public Person
    {
    public:
    	// 派生类必须实现纯虚函数,否则报错
        void show() const { cout << "这是singer类" << endl; };
    };
    
    // 派生类Actor:构造、析构、复制函数均采用默认
    class Actor : public Person
    {
    public:
    	// 派生类必须实现纯虚函数,否则报错
        void show() const { cout << "这是actor类" << endl; };
    };
    
    // 主函数
    int main()
    {
        // ABC类指针数组:后续用于指向派生类
        Person *p[3];
        Singer singer;
        Actor actor;
    
        p[0] = &singer;
        // 输出:这是singer类
        p[0]->show();
    
        p[1] = &actor;
        // 输出:这是actor类
        p[1]->show();
    
        // 编译错误:ABC不可创建对象
        // p[2]->show();
    }
    

3.3 多重继承MI

  • 定义:能够使用两个或多个基类派生出新的类,组合功能

  • 虚基类:使得从多个类(他们基类相同)派生出的对象只继承一个基类对象,防止二义性

  • 构造函数规则:对虚基类,除默认构造函数,否则必须显式调用虚基类的某个构造函数


  • 继承关系示例

  • 代码示例

    #include <iostream>
    
    using namespace std;
    
    class B{
    public:
        void show(){cout<<"B"<<endl;};
    };
    
    // 指定B为虚基类
    class C:virtual public B{
    public:
        void show(){cout<<"C"<<endl;};
        void cal(){cout<<"C"<<endl;};
    };
    
    class D:public C{};
    
    // 指定B为虚基类
    class E:virtual public B{
    public:
        void cal(){cout<<"E"<<endl;};
    };
    
    class F:public D,public E{};
    
    int main() {
        F m;
        // 用的是类C的
        m.show();
        // m.cal()会产生二义性,
        // 最好在类F中重写此方法
        m.D::cal();
        return 0;
    }
    

四、动态内存分配

  • 静态内存分配:程序启动瞬间,变量即被分配了内存空间,直到程序退出销毁
  • 动态内存分配:程序运行期间,根据需要,动态分配内存空间,期间动态分配、销毁

4.1 头文件

  • person.h
    #ifndef PERSON_H_
    #define PERSON_H_
    #include <iostream>
    #include <string>
    
    using namespace std;
    
    class Person
    {
    private:
        // 动态内存分配的核心:此处只声明指针,未分配内存
        char *_name;
        // 用于存储字符串长度,不包含结尾\0
        int len;
    
    public:
        // 默认构造函数
        Person();
        // 重载默认构造函数
        Person(const char *name);
        // 复制构造函数
        Person(const Person &p);
        // 析构函数
        ~Person();
    
        // 常规函数:显示对象名
        void show() const;
    
        // 重载赋值运算符=
        Person &operator=(const Person &p);
    };
    #endif
    

4.2 类实现

  • person.cpp
    #include "./person.h"
    
    Person::Person()
    {
        // 赋值静态数据成员,见2.1节
        static const char *s = "未命名";
        // 字符串长度:传入字符串指针
        len = strlen(s);
        // new分配内存空间,此处用[],对应于析构函数
        _name = new char[len + 1];
        // 字符串深拷贝
        strcpy(_name, s);
        cout << "默认构造函数被调用" << endl;
    };
    
    Person::Person(const char *name)
    {
        // 同构造函数
        len = strlen(name);
        _name = new char[len + 1];
        strcpy(_name, name);
        cout << _name << "的构造函数被调用" << endl;
    };
    
    Person::Person(const Person &p)
    {
        this->len = p.len;
        _name = new char[len + 1];
        strcpy(_name, p._name);
        cout << _name << "的复制构造函数被调用" << endl;
    };
    
    Person::~Person()
    {
        // 析构函数对应与构造函数
        delete[] _name;
        cout << _name << "的析构函数被调用" << endl;
    };
    
    Person &Person::operator=(const Person &p)
    {
        // 检查是否为自我复制,若是且无此判断语句
        // 则delete语句会先清空内存导致无值可赋
        if (this == &p)
            return *this;
        // 若非自我复制,清空_name指向的内存
        delete[] _name;
        // 以下同构造函数写法
        len = strlen(p._name);
        _name = new char[len + 1];
        strcpy(_name, p._name);
        cout << _name << "赋值运算符被调用" << endl;
        return *this;
    };
    
    void Person::show() const
    {
        cout << "姓名是:" << _name << endl;
    };
    
    

4.3 主程序

  • main.cpp
    // 注意路径的写法,同级目录可只写文件名
    #include "./person.h"
    
    int main()
    {
        // 复习下:在堆中创建对象,per?是指向对象Person的指针
    
        // a、调用默认构造函数
        Person *per1 = new Person;
        // b、调用重载的默认构造函数
        Person *per2 = new Person("per2");
        // c、调用复制构造函数:注意传入的是*per2即Person对象
        Person *per3 = new Person(*per2);
    
        // 指针调用类方法的写法
        per1->show();
        per2->show();
        per3->show();
    
        // 此处调用重载的赋值运算符=
        *per3 = *per1;
    
        // 销毁类对象指针指向的内存空间,
        // 即调用Person对象的析构函数
        delete per1;
        delete per2;
        delete per3;
    }
    

4.4 显示

  • 显示结果
    默认构造函数被调用
    per2的构造函数被调用
    per2的复制构造函数被调用
    姓名是:未命名
    姓名是:per2
    姓名是:per2
    未命名赋值运算符被调用
    未命名的析构函数被调用
    per2的析构函数被调用
    未命名的析构函数被调用
    

上一篇:C++启蒙笔记(七)—类运算符重载
综合专题列表

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值