C++ 继承

继承

1.继承定义

1.1格式

在这里插入图片描述

1.2成员访问

继承方式

  • public继承
  • protected继承
  • private继承

访问限定符

  • public访问
  • protected访问
  • private访问
父类的成员\继承方式public继承protected继承private继承
public成员子类的public成员子类的protected成员子类的private成员
protected成员子类的protected成员子类的protected成员子类的private成员
private成员在子类中不可见在子类中不可见在子类中不可见

class子类 默认为private继承;struct子类默认为public继承

但一般显式写出继承方式;

1.3继承类模板

1.3.1类模板继承类模板
template<typename T>
class stack : public vector<T> {
public:
    void push(const T& x) {
        this->push_back(x);
    }
    void pop() {
        this->pop_back();
    }
    const T& top() const {
        return vector<T>::back();
    }
    bool empty() const {
        return vector<T>::empty();
    }
};
  • 子类模板声明时需指定父类模板的参数(如 public Base<T>)。

  • 由于按需实例化的原因,访问父类模板的成员时,通常需要用 this->Base<T>:: 限定(避免编译器歧义)

  • 子类类构造函数必须在初始化列表中显式调用父类模板的构造函数(如 Base<T>(v)

1.3.2普通类继承类模板的某个实例
// 基类模板
template <typename T>
class Base {
protected:
    T value;
public:
    Base(T v) : value(v) {}
    void print() { cout << "Base value: " << value << endl; }
};

// 普通类继承类模板的具体实例(指定T为int)
class Derived : public Base<int> {
private:
    string data;
public:
    // 基类此时已确定为Base<int>,直接传递int参数
    Derived(int v, string d) : Base<int>(v), data(d) {}
    void show() {
        cout << "Derived data: " << data << ", Base value: " << value << endl;
    }
};
  • 基类模板被实例化为具体类型(如 Base<int>),派生类是普通类,无需模板参数。
  • 此时基类成员的类型已确定(如 valueint),访问时无需 this-> 限定。
1.3.3类模板继承普通类

2.赋值兼容转换

在这里插入图片描述

  • public继承的子类对象(指针 / 引用) 可以赋值给父类对象(指针 / 引用),被称为切片。中间不会产生临时对象

  • 父类对象不能赋值给子类对象。

  • 父类的指针或引用可以通过强制类型转换赋值给子类的指针或引用,但是只有父类的指针是指向子类对象时才是安全的。这里父类如果是多态模型,可以使用RTTI的 dynamic_cast 来识别后进行安全转换。

3.继承中的作用域

3.1隐藏规则

  1. 在继承体系中父类和子类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将会屏蔽对父类同名成员的直接访问,这种情况叫隐藏。(在子类成员函数中,可以使用父类::父类成员显式访问)
  3. 如果是成员函数的隐藏,只要函数名相同就构成隐藏,而不是重载,这是因为函数重载要求在同一作用域
  4. 子类对象调用父类隐藏成员也需指定类域
class Person {
public:
    void identity() const {
        cout << "void identity()" << _name << endl;
    }
protected:
    string _name = "Bob";
    string _address;
    string _tel;
    int _age = 18;
};

class Student : public Person {
public:
    void study() {
        cout << "studying" << Person::_name << endl;
    }
protected:
    int _stuId;
    string _name = "Jack";
};

3.2选择题

  1. [外链图片转存中…(img-jgu6Ewbp-1753797211700)]

  2. 下面程序的编译运行结果是什么?

    A.编译报错 B.运行报错 C.正常运行 C

    class A
    {
    public:
        void fun()
        {
        	cout << "func()" << endl;
        }
    };
    class B : public A
    {
    public:
    	void fun(int i)
    	{
    		cout << "func(int i)" <<i<<endl;
    	}
    };
    

4.子类的默认成员函数

在这里插入图片描述

4.1四个常见的默认成员函数

默认成员函数编译器自动生成。

  1. 子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显式调用。
  2. 子类的拷贝构造函数必须调用父类的拷贝构造函数完成父类的拷贝初始化。
  3. 子类的赋值重载函数必须调用父类的赋值重载完成父类的赋值。需要注意的是,子类的赋值重载隐藏了父类的赋值重载,所以显式调用父类的赋值重载需要指定父类作用域。
  4. 子类的析构函数会在被动用完成后自动调用父类的析构函数清理父类成员。保证子类对象先清理子类成员再清理父类成员的顺序。
  5. 子类对象初始化先调用父类构造再调用子类构造。
  6. 子类对象析构清理先调用子类析构,再调用父类析构。

在这里插入图片描述

4.2不能被继承的类

  • **将该类的构造函数私有。**因为子类的构成必须调用父类的构造函数。(但是这必须要定义子类对象才能观察到报错)
  • C++11新增关键字final,将类声明为final,就不能被继承。

5.继承和友元

友元关系不能继承,即父类友元不能访问子类私有和保护成员。

要访问父类和子类需要在父类和子类都进行友元声明。

class Student; //前置声明

class Person{
public:
    friend void Display(const Person& p, const Student& s);
protected:
    string _name; // 姓名
};

class Student : public Person{
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;
}

6.继承和静态成员

父类定义了static静态成员,整个继承体系里面只有一个这样的成员,也就是共享同一个成员。可以理解为受类域限制的全局变量。

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;
	cout << &s._name << endl;
// 这里的运行结果可以看到静态成员_count的地址是⼀样的
// 说明子类类和父类共用同⼀份静态成员
	cout << &p._count << endl;
	cout << &s._count << endl;
// 公有的情况下,父类指定类域都可以访问静态成员
    cout << Person::_count << endl;
    cout << Student::_count << endl;
    
    return 0;
}

7.多继承及其菱形继承问题

7.1继承模型

  • 单继承:一个子类只有一个直接父类。
  • 多继承:一个子类有两个或以上的直接父类。多继承对象在内存中的模型是,先继承的父类在前面,后即成的父类在后面。子类成员放在最后面。
  • 菱形继承:菱形继承是多继承的一种特殊情况。

菱形继承存在数据冗余和二义性的问题,在Assistant的对象中Person的成员会有两份。支持多继承就一定会有菱形继承,像 Java 就直接不支持多继承,规避了这样的问题,所以事件中不建议设计出菱形继承这样的模型。

class Person{
public:
    string _name; // 姓名
};

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

class Teacher : public Person {
protected:
    int teaId;  // 职工编号
};

class Assistant : public Student, public Teacher {
protected:
    string _course  // 课程
};

int main(){
// 编译报错:error C2385: 对“_name”的访问不明确
	Assistant a;
    a._name = "peter";
// 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决
    a.Student::_name = "xxx";
    a.Teacher::_name = "yyy";
    return 0;
}

7.2虚继承

使用虚继承可以解决菱形继承中数据冗余和二义性的问题。

在 C++ 中,虚继承声明的位置非常关键,只需在 **“直接继承共享父类” 的子类 ** 中声明即可,无需在整个继承链的所有层级都添加。

具体来说,假设存在如下菱形继承结构:

     A(共享基类)
    / \
   B   C (直接继承A的派生类)
    \ /
     D (间接继承A的最终派生类)

此时,虚继承只需在 B 和 C 继承 A 时声明,代码如下:

class A { ... };

// 在直接继承A的类中声明虚继承
class B : virtual public A { ... };  // 关键:B虚继承A
class C : virtual public A { ... };  // 关键:C虚继承A

// D继承B和C时,无需再对A声明虚继承
class D : public B, public C { ... };

这样就能保证:

  1. 最终派生类 D 中只包含一份 A 的实例(避免数据冗余)
  2. 访问 A 的成员时不会产生歧义(如 D obj; obj.a; 不会报错)
class Person {
public:
    string _name;
};
class Student : virtual public Person {
protected:
    string _stuId = "2023005756";
};
class Teacher : virtual public Person {
protected:
    string _title;
};
class Assistant : public Teacher, public Student {
protected:
    int _num;
};

在这里插入图片描述

对于复杂的菱形继承,一定是继承体系中谁会产生生冗余和二义性,谁的直接子类虚继承即可。

在这里插入图片描述

而没有菱形继承问题,即不需要虚继承的地方,就不要加虚继承。

7.3多继承中指针的偏移问题

下面说法正确的是()C

A.p1 == p2 == p3
B.p1<p2<p3
C.p1==p3!=p2
D.p1!=p2!=p3

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;
}

和继承的顺序有关系,先继承的在上面(低地址)。栈向下增长。

8.继承和组合

  • public 继承是一种 is-a 的关系。也就是说每个子类对象都是一个父类对象。
  • 组合是一种 has-a 的关系。假设B组合了A,每个B对象中都有一个A对象。
  • 继承允许根据父类的实现来定义子类的实现。这种通过生成子类的复用通常被称为白盒复用。在继承方式中,父类的内部细节对子类可见。继承一定程度破坏了父类的封装,父类的改变,对子类有很大的影响。子类和父类间的依赖关系很强,耦合度高
  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑盒复用,因为对象的内部细节是不可见的。对象只以“黑盒”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于保持每个类被封装。
  • 优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。
// Tire(轮胎)和Car(车)更符合has-a的关系
class Tire {
protected:
    string _brand = "Miqilin"; // 品牌
    size_t _size = 17; // 尺寸
};
class Car {
protected:
    string _colour = "白色"; // 颜色
    string _num = "晋JABC001"; // 车牌号
    Tire _t1; // 轮胎
    Tire _t2; // 轮胎
    Tire _t3; // 轮胎
    Tire _t4; // 轮胎
};
class BMW : public Car {
public:
	void Drive() { cout << "好开-操控" << endl; }
};
// Car和BMW/Benz更符合is-a的关系
class Benz : public Car {
public:
	void Drive() { cout << "好坐-舒适" << endl; }
};

// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public vector<T>
{};

template<class T>
class stack
{
public:
	vector<T> _v;
};

组合:deque是queue的一个成员。

template<typename T, typename Container = deque<T>>
    class queue {
    public:
        void push(const T& x) {_con.push_back(x);}
        void pop(){_con.pop_front();}
        const T& front() const {return _con.front();}
        const T& back() const {return _con.back();}
        size_t size() const {return _con.size();}
        bool empty() const {return _con.empty();}

    private: Container _con;
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

等你涅槃重生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值