探索C++继承机制

继承的概念

继承机制是面向对象程序设计使代码可以复用的重要手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生的类叫做派生类

继承定义格式

下方代码中,Person是基类,也称作父类。Student是派生类,也称作子类。

cpp 复制代码
class Student : public Person
{
public:
    int _stuid;    //学号
    int _major;    //专业
}

继承基类成员访问方式的变化

|----------------|-----------------|-----------------|---------------|
| 类成员/继承方式 | public继承 | protected继承 | private继承 |
| 基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |

  • 基类的private成员在派生类中无论以什么方式继承都是不可见的。 这里的不可见是指基类的私有成员还是被继承到了派生类成员中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
  • 基类的private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。
  • 基类的其他成员在派生类的访问方式==Min(成员在积累的访问限定符,继承方式),public>protected>private
  • 使用关键字class默认的继承方式是private,使用struct关键字时默认的继承方式是public ,不过最好显示的写出继承方式
cpp 复制代码
class Student : Person  //private继承
struct Student : Person  //public继承

继承类模板

cpp 复制代码
//#define CONTAINER std::vector  
#define CONTAINER std::list  //使用宏方便替换

namespace bit
{
    //template<class T>
    //class vector
    //{};
    
    // stack和vector的关系,既符合is - a,也符合has - a
    template<class T>
    class stack : public CONTAINER<T>
    {
    public:
        void push(const T& x)
        {
            // 基类是类模板时,需要指定⼀下类域,帮助实例化
            // 否则编译报错: error C3861 : "push_back":找不到标识符
            // 因为stack<int>实例化时,也实例化vector<int>了
            // 但是模版是按需实例化, push_back等成员函数未实例化,所以找不到
            CONTAINER<T>::push_back(x);
            //push_back(x);
        }
        void pop()
        {
            CONTAINER<T>::pop_back();
        }
        const T& top()
        {
            return CONTAINER<T>::back();
        }
        bool empty()
        {
            return CONTAINER<T>::empty();
        }
    };
}

int main()
{
    bit::stack<int> st;
    st.push(1);
    st.push(2);
    st.push(3);
    while (!st.empty())
    {
        cout << st.top() << " ";
        st.pop();
    }
}

基类和派生类的转换

  • **public继承的派生类对象可以赋值给基类对象/基类的指针/积累的引用。**把Person有的那部分切出来给Person,有个形象的说法叫切片或切割。
  • 基类对象不能赋值给派生类对象
  • **基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但必须是积累的指针是指向派生类对象时才是安全的。**这里基类如果是多态类型,可以使⽤RTTI(Run-TimeType Information)的dynamic_cast 来进⾏识别后进⾏安全转换。
cpp 复制代码
class Person
{
protected:
    int _age;  //  年龄
    string _name; // 姓名
    string _sex;  // 性别
};

class Student : public Person
{
public:
    int _No; // 学号
};

int main()
{
    Student sobj;
    // 1.派⽣类对象可以赋值给基类的指针/引⽤,注意这里不是类型转换
    Person* pp = &sobj;
    Person& rp = sobj;
    Person pobj = sobj;
    // 派⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的
    
    //2.基类对象不能赋值给派⽣类对象,这⾥会编译报错
    //sobj = pobj;
    
    return 0;
}

继承中的作用域

隐藏规则

  • 在继承体系中基类和派生类都有独立的作用域(函数重载要求在同一作用域,注意和隐藏进行区分)
  • 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫做隐藏 。(在派生类成员函数中,可以使用基类:基类成员访问基类成员)
  • 如果是成员函数 的隐藏,只需要函数名相同就构成隐藏
  • 在继承体系里面最好不要定义同名的成员
cpp 复制代码
class Person
{
protected:
    int _num = 111;// 身份证号
    string _name = "小李子"; // 姓名
};

class Student : public Person
{
public:
    void Print()
    {
        cout << " 姓名:"<<_name<< endl;   //姓名:小李子
        cout << " 身份证号: "<<Person::_num<< endl; //身份证号 : 111
        cout << " 学号: "<<_num<<endl;    //    学号 : 999
    }
protected:
    int _num = 999; // 学号
};

int main()
{
    Student s1;
    s1.Print();
    return 0;
}

派生类的默认成员函数

4个常见默认成员函数

  • **派生类的构造函数必须调用基类的构造函数来初始化基类的那一部分成员。**如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
  • 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制 ,要注意的是派⽣类的operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域
  • 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。 因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序。
  • 派⽣类对象初始化先调⽤基类构造再调派⽣类构造
  • 派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构
  • 基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系
cpp 复制代码
class Person
{
public:
    Person(const char* name = "peter")
        : _name(name)
    {
        cout << "Person()" << endl;
    }

    Person(const Person & p)
        : _name(p._name)
    {
        cout << "Person(const Person& p)" << endl;
    }

    Person& operator=(const Person & p)
    {
        cout << "Person operator=(const Person& p)" << endl;
        if (this != &p)
            _name = p._name;

        return *this;
    }

    ~Person()
    {
        cout << "~Person()" << endl;
    }
protected:
    string _name; // 姓名
};

class Student : public Person
{
public:
    //默认生成的构造函数的行为
    //1.内置类型-》不确定
    //2.自定义类型-》调用默认构造
    //3.继承的基类成员看作一个整体对象,调用基类的默认构造
    Student(const char* name, int num, const char* address)
        :Person(name)
        ,_num(num)
        , _address(address)
    { }

    //严格说Student拷贝构造函数默认生成的就够用
    //如果有需要深拷贝的资源,才需要自己实现
    Student(const Student& s)
        :Person(s)
        ,_num(s._num)
        , _address(s._address)
    {
        //深拷贝
    }

    //严格说Student赋值重载函数默认生成的就够用
    //如果有需要深拷贝的资源,才需要自己实现
    Student& operator=(const Student& s)
    {
        if (this != &s)
        {
            //父类和子类的operator=构成隐藏关系,需要指定调用
            Person::operator=(s);
            _num = s._num;
            _address = s._address;
        }

        return *this;
    }

    //严格说Student析构函数默认生成的就够用
    //如果有需要显示释放的资源,才需要自己实现
    //析构函数都会被特殊处理成destructor()
    ~Student()
    {
        //子类的析构和父类的析构函数也构成隐藏关系
        //不需要显式调用,子类析构函数之后,会自动调用父类析构
        //这样保证析构顺序,先子后父,显示调用取决于实现的人,不能保证
        // 先子后父
        //Person::~Person();
    }

protected:
    int _num; //学号
    string _address="山东省";
};


int main()
{
    Student s1("张三",1,"山东省");
    Student s2(s1);
    Student s3("李四", 2, "河北省");
    s1 = s3;
    return 0;
}

实现一个不能被继承的类

  • 方法1:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。 ----》 缺点:不定义对象的话不会报错
  • 方法2:C++11新增了⼀个final关键字,final修饰基类,派⽣类就不能继承了。
cpp 复制代码
//方法 1:
class Base 
{
public:
    void func5() { cout << "Base::func5" << endl; }
protected:
    int a = 1;
private:   //private的成员在子类无法使用
    // C++98 的⽅法
    Base()
    {}
};

//方法2
class Base final
{
public:
    void func5() { cout << "Base::func5" << endl; }
protected:
    int a = 1;
private:
    // C++98 的⽅法
    Base()
    {}
};

class Derive :public Base
{
    void func4() { cout << "Derive::func4" << endl; }
protected:
    int b = 2;
};

继承与友元

  • 友元关系不能被继承,也就是说基类友元不能访问派生类私有和保护成员
cpp 复制代码
class Student;  //前置声明

class Person
{
public:
	//友元关系不能被继承
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};

class Student : public Person
{
	//解决方法
	friend void Display(const Person& p, const Student& s);
protected:
	int _stuNum; // 学号
};

void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl;	//
}

int main()
{
	Person p;
	Student s;
	// 编译报错:error C2248 : "Student::_stuNum" :⽆法访问protected成员
	// 解决⽅案:Display也变成Student的友元即可
	Display(p, s);
	return 0;
}

继承与静态成员

  • 基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都只有⼀个static成员实例
cpp 复制代码
class Person
{
public:
	string _name;
	static int _count;
};

int Person::_count = 0;

class Student : public Person
{
protected:
	int _stuNum;
};

int main()
{
	Person p;
	Student s;
	// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的
	// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份
	cout << &p._name << endl;	//012FFD44
	cout << &s._name << endl;	//012FFD44

	// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的
	// 说明派⽣类和基类共⽤同⼀份静态成员
	cout << &p._count << endl;	//00B2E47C
	cout << &s._count << endl;	//00B2E47C

	// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员
	cout << Person::_count << endl;		//0
	cout << Student::_count << endl;	//0

	// 也可以通过对象访问静态成员
	cout << p._count << endl;		//0
	cout << s._count << endl;		//0

	return 0;
}

多继承及其菱形继承问题

继承模型

  • 单继承(左图):一个派生类只有一个直接基类时成这个继承为单继承
  • 多继承(有图):一个派生类有两个或以上直接基类时成这个继承关系为多继承。多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯
  • 菱形继承:是多继承的一种特殊情况。有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就⼀定会有菱形继承。
cpp 复制代码
class Person
{
public:
	string _name; // 姓名
	int _tel;
	int _age;
};

//使用虚继承继承Person类
class Student : public Person
{
protected:
	int _num; //学号
};

//使用虚继承继承Person类
class Teacher : public Person
{
protected:
	int _id; // 职⼯编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	// 编译报错:error C2385 :对"_name"的访问不明确
	Assistant a;
	//a._name = "peter";

	// 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决
	a.Student::_name = "李同学";
	a.Teacher::_name = "李老师";

	return 0;
}

虚继承

  • 最好不要设计出菱形继承
  • virtual - 虚拟,哪个类产生数据冗余和二义性,继承时用虚继承
  • 不要看到有同一个基类就用virtual,可能是同一个基类产生多个派生类
cpp 复制代码
class Person
{
public:
	string _name; // 姓名
	int _tel;
	int _age;
};

//使用虚继承继承Person类
class Student : virtual public Person  //在这里加virtual
{
protected:
	int _num; //学号
};

//使用虚继承继承Person类
class Teacher : virtual public Person  //在这里加virtual
{
protected:
	int _id; // 职⼯编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	// 使⽤虚继承,可以解决数据冗余和⼆义性
	Assistant a;
	a._name = "peter";
	return 0;
}

多继承中的指针偏移问题

  • 先继承的基类在上面(低地址),派生类自己的成员放在最下面(高地址)
cpp 复制代码
class Base1 { public:  int _b1; };
class Base2 { public:  int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;    
	Derive* p3 = &d;   //指向开始

	return 0;
}

继承和组合

  • public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。
  • 组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象
  • 继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤ (white-box reuse)。术语"⽩箱"是相对可视性⽽⾔:**在继承⽅式中,基类的内部细节对派⽣类可⻅。**继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度⾼。
  • 对象组合 是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为**⿊箱复⽤**(black-boxreuse), 因为对象的内部细节是不可⻅的。对象只以"⿊箱"的形式、、出现。组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
  • 优先使⽤组合,⽽不是继承。 实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外**要实现多态,也必须要继承。**类之间的关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。
相关推荐
郝学胜-神的一滴2 小时前
深入解析Mipmap层级判定原理:从理论到实践
c++·unity·godot·游戏程序·图形渲染·unreal engine
人道领域2 小时前
【零基础学java】(等待唤醒机制,线程池补充)
java·开发语言·jvm
名字不好奇2 小时前
在C++中 如何实现java中的Stream
java·c++
智算菩萨2 小时前
【Python自然语言处理】基于NLTK库的英文文本词频统计系统实现原理及应用
开发语言·python·自然语言处理
superman超哥2 小时前
Rust 异步并发核心:tokio::spawn 与任务派发机制深度解析
开发语言·rust·编程语言·rust异步并发核心·rust任务派发机制
喵星人工作室2 小时前
C++传说:神明之剑0.2.1
开发语言·c++·游戏
黎雁·泠崖2 小时前
Java入门之吃透基础语法:注释+关键字+字面量+变量全解析
java·开发语言·intellij-idea·intellij idea
Ashley_Amanda2 小时前
Python 常见问题梳理
开发语言·windows·python
UpgradeLink2 小时前
基于 Go 打造的升级链路管理平台:upgradelink 让设备升级更简单
开发语言·后端·golang