C++的诗行:一文读懂C++的继承机制

发布于:2025-09-11 ⋅ 阅读:(16) ⋅ 点赞:(0)

目录

一、继承的概念及定义

1.1 继承的概念

1.2 继承的定义

1.2.1 定义格式

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

1.3 继承类模板

注意事项:

二、基类和派生类间的转化

三、隐藏机制

3.1 继承中的隐藏机制

3.2 重载与隐藏

四、派生类的默认成员函数

4.1 构造函数

4.2 拷贝构造函数

4.3 operator =

4.4 析构函数

五、继承与友元

5.1 什么是友元

5.2 继承中的友元关系

六、继承与静态成员

七、多继承与菱形继承

7.1 单继承与多继承

7.1.1 多继承中的指针偏移

7.2 菱形继承(钻石问题)

  7.2.1 虚继承

八、继承与组合

一、继承的概念及定义

1.1 继承的概念

在没有接触继承之前我们要设计两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity身份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有一些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学生生独有的成员函数是学习,老师独有的成员函数是教授。

class Student
{
public:
	void Study()
	{
		//学习
	}
	void identity()
	{
		//身份认证
	}
private:
	string _name;      //姓名
	string _address;   //地址
	string _tel;       //电话
	int _age;          //年龄

	int _stuid;        //学号
};

class Teacher
{
public:
	void teach()
	{
		//教授
	}
	void identity()
	{
		//身份认证
	}
private:
	string _name;      //姓名
	string _address;   //地址
	string _tel;       //电话
	int _age;          //年龄

	int _title;        //职工号
};

利用继承的方法我们可以将两个类中的公共部分提取出来封装成单独一个类Person,再使Teacher/Student分别继承Person。这样Teacher/Student中既有自己特有的成员变量(函数)也有公共的成员变量(函数),大大避免了代码的冗余。

#include<string.h>
class Person
{
public:
	void identity()
	{
		//身份认证
	}
	string _name;      //姓名
	string _address;   //地址
	string _tel;       //电话
	int _age;          //年龄
};

class Student:public Person  //Stuedent继承Person类
{
public:
	void Study()
	{
		//学习
	}
private:
	int _stuid;     //学号
};

class Teacher:public Person  //Teacher继承Person类
{
public:
	void teach()
	{
		//教授
	}
private:
	int _title;   //职工号
};

 

继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类叫做派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到到复杂的认知过程。以前我们接触的都是函数层面的复用,继承是类设计层次的复用。                                                             

1.2 继承的定义

1.2.1 定义格式

下面Person是基类,也称作父类;Student是派生类,也称作子类。

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

基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

基类private成员在派生类中不能访问,如果基类不想在类外直接访问,但还需要在派生类中可以访问,就定义成protected。可以看出保护成员限定符是因继承才出现的。

实际上面的表格我们总结一下就会发现,基类的私有成员在派生类中都是不可见的。基类的其他成员在派生类中的访问方式==Min(成员在基类的访问限定符,继承方式)public>protected>private。

使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显式地写出继承方式。

在实际运用中一般使用的都是public继承,几乎很少使用protected/private继承,也不提倡使用protected/private继承,因为protected/private继承下来的成员都只能在派生类中使用,实际中扩展维护性不强。

1.3 继承类模板

在 C++ 中,类模板可以被继承,这种方式称为 "继承类模板"。通过继承类模板,我们可以创建更具体或更特殊化的类,同时复用模板的通用逻辑。

继承类模板主要有两种场景:

  1. 普通类继承类模板的实例化版本
  2. 类模板继承另一个类模板

1.普通类继承类模板的实例化版本

当一个普通类继承自一个已经实例化的类模板时,需要指定模板参数:

// 定义一个类模板
template <typename T>
class Container {
protected:
    T data;
public:
    Container(T val) : data(val) {}
    T get() const { return data; }
    void set(T val) { data = val; }
};

// 普通类继承实例化的类模板
class IntContainer : public Container<int> {
public:
    IntContainer(int val) : Container<int>(val) {}
    
    // 新增方法
    void increment() { data++; }
};

2.类模板继承另一个类模板

类模板可以继承另一个类模板,此时可以使用自身的模板参数作为基类模板的参数:

#include <iostream>

// 基类模板
template <typename T>
class Base {
protected:
    T value;
public:
    Base(T v) : value(v) {}
    void print() const {
        std::cout << "Base value: " << value << std::endl;
    }
};

// 派生类模板继承基类模板
template <typename T, typename U>
class Derived : public Base<T> {
private:
    U extra;
public:
    // 注意初始化列表中需要显式指定基类模板
    Derived(T v, U e) : Base<T>(v), extra(e) {}
    
    void show() const {
        // 访问基类成员时,可能需要使用this指针或Base<T>::限定
        std::cout << "Derived value: " << this->value 
                  << ", Extra: " << extra << std::endl;
    }
};

int main() {
    Derived<int, std::string> obj(42, "example");
    obj.print();  // 调用基类方法
    obj.show();   // 调用派生类方法
    
    return 0;
}

注意事项:

  1. 访问基类成员:在派生类模板中访问基类模板的成员时,可能需要使用this->指针或Base<T>::限定符,帮助编译器识别成员。

  2. 模板参数传递:派生类模板可以将自身的模板参数传递给基类模板,也可以使用固定类型:

template <typename T>
class Derived : public Base<double> {  // 固定使用double类型
    // ...
};

二、基类和派生类间的转化

public继承的派生类对象可以赋值给基类的指针/基类的引用。这里有个形象的说法叫切片或者切 割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。

#include<iostream>
using namespace std;
class Person
{
protected:
	string _name; // 姓名
	string _sex;  // 性别
	int _age;  // 年龄

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

这里需要注意的是:基类对象不能赋值给派生类对象。

三、隐藏机制

3.1 继承中的隐藏机制

派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派生类成员函数中,要是需要访问基类中同名成员可以使用基类::基类成员显式访问)

class Person
{
protected:
	int _num = 111;
	string _name = "⼩李⼦"; 
};
class Student : public Person
{
public:
	void Print()
	{
		cout <<_num<<endl;//这里打印结果为999因为Person中的_num被隐藏了
	}
protected:
	int _num = 999; 
};
int main()
{
	Student s1;
	s1.Print();
	return 0;
};

需要注意的是对于成员函数来讲,只要函数名相同就构成了隐藏。也就是说,如果派生类与基类中的成员函数名相同,通过派生类的实例化对象调用该函数默认调用派生类中定义的重名函数,需要调用基类中的重名函数时需要指定类名:

class Person
{
public:
	void Print()
	{
		cout << "我是基类中的Print函数" << endl;
	}
};
class Student : public Person
{
public:
	void Print()
	{
		cout <<"我是派生类中的Print函数" << endl;
	}
};
int main()
{
	Student s1;
	s1.Print();
	return 0;
};

要是想显式调用基类中的Print函数,需要指定Person::Print();如下示例:

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

这也从侧面说明,对于继承关系中的隐藏机制,基类中的重名的成员函数或者成员变量确确实实被派生类继承下来了,只是由于隐藏机制默认访问或调用的是派生类中的成员变量或函数,需要访问基类中的时需要指明类域。

3.2 重载与隐藏

这里我们试着想一想下面基类与派生类func函数之间的关系:

A:重载 B:隐藏 

class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void fun(int i)
	{
		cout << "func(int i)" << i << endl;
	}
};
int main()
{
	B b;
	b.fun(10);
	return 0;
};

这里我们发现基类和派生类中都有一重名的func函数,根据上述提到的隐藏机制的形成条件基类的func函数与派生类中的func函数构成隐藏。

什么是重载?

在 C++ 中,它允许在同一个作用域内定义多个同名的函数或运算符,但这些同名的函数或运算符的参数列表(参数个数、参数类型或参数顺序)必须有所不同,返回值类型可以相同也可以不同。

根据概念我们就可以发现两者的区别,重载强调在同一作用域也就是同一个类中的同名函数之间构成重载,继承中的隐藏机制始终离不开基类与派生类两个作用域。

四、派生类的默认成员函数

4.1 构造函数

派生类的构造函数必须调用基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造 函数,则必须在派生类构造函数的初始化列表阶段显式调用。

默认构造函数是一种特殊的构造函数,它不需要任何参数,或者所有参数都有默认值。当创建类的对象时如果没有提供实参,编译器会自动调用默认构造函数。

class A
{
public:
	//这里基类没有默认构造函数需要传递参数
	A(string _name)
	{
		name = _name;
	}

	string name;
};
class B : public A
{
public:
	//派生类中需要显式调用基类构造函数初始化基类的成员变量
	B(int _n1,string _name)
		:A(_name)
		,n1(_n1)
	{}
	void Print()
	{
		cout << name << n1 << std::endl;
	}
private:
	int n1;
};
int main()
{
	B ss(18,"张三");
	ss.Print();
	return 0;
};

4.2 拷贝构造函数

派生类的拷贝构造函数必须调用基类的拷贝构造完成拷贝初始化。

在一般情况下,拷贝构造函数编译器自动生成的就够用了,但是如果类中有资源需要深拷贝时就需要我们手动实现拷贝构造函数

当创建派生类对象时,先调用基类的构造函数再调用派生类的构造函数(与析构函数的调用顺序完全相反)。

class A
{
public:
	//这里基类没有默认构造函数需要传递参数
	A(string _name)
	{
		name = _name;
	}

	A(const A& _a)
	{
		name = _a.name;
	}
	string name;
};
class B : public A
{
public:
	B(int _n1,string _name)
		:A(_name)
		,n1(_n1)
	{}
	B(const B& _b)
		:A(_b)//显式调用基类的拷贝构造完成构造初始化
		,n1(_b.n1)
	{}
	void Print()
	{
		cout << name << n1 << std::endl;
	}
private:
	int n1;
};
int main()
{
	B ss(18,"张三");
	B pp(ss);
	pp.Print();
	return 0;
};

4.3 operator =

派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的 operator=隐藏了基类的operator=,所以需要指定基类作用域显式调用基类的operator=。

与前面的拷贝构造一样,在一般情况下,赋值运算符重载编译器自动生成的就够用了,但是如果类中有资源需要深拷贝时就需要我们手动实现赋值运算符重载

这里我们也来演示一下:

class A
{
public:
	//这里基类没有默认构造函数需要传递参数
	A(string _name)
	{
		name = _name;
	}

	A& operator=(A& _a)
	{
		if (this != &_a)
		{
			name = _a.name;
		}
		return *this;
	}
	string name;
};
class B : public A
{
public:
	B(int _n1,string _name)
		:A(_name)
		,n1(_n1)
	{}
	B& operator=(B& _b)
	{
		if (this != &_b)
		{
            //这里不能是operator(_b)因为构成隐藏会递归调用B中的operator=导致栈溢出
			A::operator=(_b);
			n1 = _b.n1;
		}
		return *this;
	}
	void Print()
	{
		cout << name << n1 << std::endl;
	}
private:
	int n1;
};
int main()
{
	B ss(18,"张三");
	B pp=ss;
	pp.Print();
	return 0;
};

4.4 析构函数

当派生类对象销毁时,先调用派生类的析构函数再调用基类的析构函数(与构造函数的调用顺序相反)。

派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派 生类对象先清理派生类成员再清理基类成员的顺序。

一般来说,编译器自动生成的析构函数就已经够用了,如果有需要显式释放的资源,才需要自己实现:

#include <iostream>
class Base {
public:
    ~Base() { std::cout << "Base destructor" << std::endl; }
};

class Derived : public Base {
public:
    ~Derived() { std::cout << "Derived destructor" << std::endl; }
};

int main() {
    Derived d;  // 销毁时先调用 ~Derived(),再调用 ~Base()
    return 0;
}
// 输出:
// Derived destructor
// Base destructor

五、继承与友元

5.1 什么是友元

在 C++ 中,友元(Friend) 是一种特殊的访问权限机制,它允许一个类或函数访问另一个类中的私有(private) 和保护(protected) 成员,即使它们不属于该类的成员。

通常友元具有以下三种形式:


1. 友元函数

普通函数可以被声明为某个类的友元,从而访问该类的私有成员。

class MyClass {
private:
    int secret;
public:
    MyClass(int s) : secret(s) {}
    // 声明友元函数
    friend void printSecret(MyClass obj);
};

// 友元函数定义(无需加friend关键字)
void printSecret(MyClass obj) {
    // 可以直接访问私有成员
    cout << "Secret value: " << obj.secret << endl;
}

2. 友元类

一个类可以被声明为另一个类的友元,此时友元类的所有成员函数都能访问该类的私有成员。

class A {
private:
    int value;
public:
    A(int v) : value(v) {}
    // 声明B为A的友元类
    friend class B;
};

class B {
public:
    void showA(A a) {
        // B的成员函数可以访问A的私有成员
        cout << "A's value: " << a.value << endl;
    }
};

3. 类的成员函数作为友元

可以将一个类的特定成员函数声明为另一个类的友元(更精确的权限控制)。

class B; // 前向声明

class A {
public:
    void showB(B b);
};

class B {
private:
    int data;
public:
    B(int d) : data(d) {}
    // 仅将A的showB函数声明为友元
    friend void A::showB(B b);
};

// 实现A的showB函数
void A::showB(B b) {
    cout << "B's data: " << b.data << endl;
}

5.2 继承中的友元关系

这里只需要记住一句话:友元关系不可以继承,也就是说基类的友元不能访问派生类的私有或者保护成员:

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

六、继承与静态成员

静态成员(变量或函数)属于整个类,而非某个具体对象,它被该类的所有实例以及所有派生类的实例共享。
在继承关系中,基类的静态成员不会被派生类复制,而是在整个继承体系中只存在一份副本。

派生类可以直接访问基类的静态成员,但需遵循访问权限控制(public/protected/private):

  • 基类的public静态成员:派生类可直接访问(通过类名或对象)。
  • 基类的protected静态成员:派生类的成员函数中可访问,但类外不可直接访问。
  • 基类的private静态成员:派生类无法访问(即使是成员函数中)。
#include <iostream>
using namespace std;

class Base {
public:
    static int public_static;  // 公有静态成员
protected:
    static int protected_static;  // 保护静态成员
private:
    static int private_static;  // 私有静态成员
};

// 初始化基类静态成员
int Base::public_static = 0;
int Base::protected_static = 0;
int Base::private_static = 0;

class Derived : public Base {
public:
    void print() {
        // 访问基类的public静态成员
        cout << "public_static: " << public_static << endl;
        
        // 访问基类的protected静态成员(仅在派生类内部可访问)
        cout << "protected_static: " << protected_static << endl;
        
        // 错误:无法访问基类的private静态成员
        // cout << "private_static: " << private_static << endl;
    }
};

int main() {
    Derived d;
    d.print();  // 正确:通过派生类对象调用,访问基类的public/protected静态成员
    
    // 直接访问基类的public静态成员(通过基类名或派生类名)
    cout << Base::public_static << endl;    // 正确
    cout << Derived::public_static << endl; // 正确(派生类共享基类的静态成员)
    
    return 0;
}

七、多继承与菱形继承

7.1 单继承与多继承

单继承指一个派生类只从一个基类继承成员的方式。这是最简单、最常用的继承形式,逻辑清晰,不易产生歧义。

多继承指一个派生类同时从多个基类继承成员的方式。它能让派生类整合多个不同基类的功能,但也可能带来复杂性。

多继承的语法格式:

class 基类1 { ... };
class 基类2 { ... };

class 派生类 : 继承方式 基类1, 继承方式 基类2, ... {
    // 派生类成员
};

7.1.1 多继承中的指针偏移

这里我们先来看一道题:

例题:下面说法正确的是( )

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

要解答这道题,我们首先来看指针p3,p3指向整个Derive类:

再看p1与p2,因为p1比p2早声明所以p1<p2

接下来我们来考虑p1与p3的关系,我们发现p3与p1分别是派生类与基类的指针,并且p1指向的是第一个基类(最先声明的)。前面我们讲过派生类对象可以赋值给基类的指针/基类的引用,基类指针或引用指向的是派生类中切出来的基类那部分。如图:

所以正确结果是p2>p1==p3,也就是p1==p3!=p2选C。

7.2 菱形继承(钻石问题)

当两个基类继承自同一个间接基类,而派生类同时继承这两个基类时,会导致间接基类的成员在派生类中存在两份副本,引发歧义。

在这里Student类与Teacher类同时继承了Person类,此时Assistant又同时继承了Student类与Teacher类,这时会导致Person类中的成员在Assistant类中出现两次。当我们访问这类成员时编译器不知道我们访问的是Student中Person类的成员还是Teacher中Person类的成员:

如果出现这种情况,我们建议应该声明要访问的这类变量的类域让编译器知道我们要访问的是哪一个父类中的重名变量。         

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

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

};
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 = "xxx";
	a.Teacher::_name = "yyy";
	return 0;
}

  7.2.1 虚继承

虚继承是面向对象编程中的一种技术,主要用于解决多继承时可能出现的菱形继承问题(也称为钻石问题)。

当一个派生类同时继承自两个基类,而这两个基类又共同继承自同一个间接基类时,就会形成菱形继承结构。这时,派生类会包含间接基类的两份副本,可能导致数据冗余和二义性。

虚继承的作用就是让派生类只保留间接基类的一份副本,从而解决上述问题。

在 C++ 中,通过在继承时使用virtual关键字来实现虚继承,例如:

// 间接基类
class A {
public:
    int x;
};

// 虚继承自A
class B : virtual public A { };
class C : virtual public A { };

// 继承自B和C,此时A只会有一份副本
class D : public B, public C {
public:
    void func() {
        x = 10;  // 不会产生二义性,因为A只有一份
    }
};

通过虚继承,类 D 中只会包含 A 的一个实例,避免了数据重复和访问冲突问题。这种技术在需要实现复杂的多继承结构时特别有用,例如在一些大型框架的类层次设计中。

八、继承与组合

继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用
(white-box reuse)。术语“白箱”是相对可视性而言:在继方式中,基类的内部细节对派生类可
见。继承⼀定程度破坏了基类的封装,基类的改变,对派生类有很⼤的影响。派生类和基类间的依
赖关系很强,耦合度高。

对象组合是类继承之外的另⼀种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对 象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-boxreuse), 因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

// Tire(轮胎)和Car(⻋)更符合has - a的关系
class Tire {
protected:
	// 品牌
	string _brand = "Michelin";  
	// 尺⼨
	size_t _size = 17;
};
class Car {
protected:
	string _colour = "⽩⾊";           
	string _num = "陕ABIT00";         
	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; 
	}
};

template<class T>
class vector
{};
// stack和vector的关系,既符合is - a,也符合has - a
template<class T>
class stack : public vector<T>
{};
template<class T>
class stack
{
public:
	vector<T> _v;
};
int main()
{
	return 0;
}

优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太 那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的 关系既适合用继承(is-a)也适合组合(has-a),就用组合。


网站公告

今日签到

点亮在社区的每一天
去签到