【C++】继承

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

本文是小编巩固自身而作,如有错误,欢迎指出!

一.面向对象三大属性

大家之前应该都多少听说过面向对象的三大特性,封装、继承、多态。其中的封装我们在前面已经学习过了其主要就是

封装是将数据(属性)和操作数据的方法(行为)捆绑在一起,并对外隐藏内部实现细节。通过访问修饰符(如privateprotectedpublic)控制对外暴露的接口,避免外部直接访问内部数据。

而我们今天学习的就是继承,多态会在后续更新。

二.继承

(1)继承的定义

继承是面向对象编程(OOP)中的核心概念之一,指一个类(子类)可以基于另一个类(父类)的属性和方法进行扩展。子类会自动获得父类的成员(字段和方法),同时可以添加新的成员或重写父类的行为。继承的主要目的是实现代码复用和层次化建模。

简单来说就是父类的成员你能用且你还可以添加新的成员。

(2)继承的格式

下列代码就是一个简单的继承演示

class Person
{
//protected:
public:
	string _name = "张三"; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};

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

我们在其中就可以看到,子类可以使用父类的成员(public情况下),而父类不能使用子类新添加的成员。

(3)继承的方式

继承的方式就如我们上文所提到分为public、protected、private三种,可以如下图所示

1. 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员 还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它。

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

 3. 实际上面的表格我们进行⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。

 4. 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public。

(4)继承类模版

继承类模版主要一点要注意就是:在子类中使用父类类模板的方法时,如果参数是不确定的,要指定一下父类的类域(才能实例化)

namespace yiming
{
	
	template<class T>
	class stack : public std::vector<T>
	{
	public:
		void push(const T& x)
		{
			// 基类是类模板时,需要指定⼀下类域, 
// 因为stack<int>实例化时,也实例化vector<int>了 
 // 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到 
	/*		vector<T>::push_back(x);*/
			push_back(x);
		}
		void pop()
		{
			vector<T>::pop_back();
		}
		const T& top()
		{
			return vector<T>::back();
		}
		bool empty()
		{
			return vector<T>::empty();
		}
	};
}
int main()
{
	yiming::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
	return 0;
}

更改后结果如下

(5)基类和派生类的转换

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

• 基类对象不能赋值给派⽣类对象。

简单点说就是成员多的能赋值给成员少的,因为可以切割多的出去不用,而成员少的不能赋值个成员多的,因为少一个成员无法弥补。

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

(6)继承中的作用域

1. 在继承体系中基类和派⽣类都有独⽴的作⽤域。

2. 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。

3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。

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

class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout <<" 学号:"<< _num << endl;//优先访问子类
		cout <<" 身份证号:"<< Person::_num << endl;
	}
protected:
	int _num = 999; // 学号
};

int main()
{
	Student s1;
	s1.Print();
	

	return 0;
};

我们可以看到其调用的是派生类的函数而非基类函数。

想要使用就得在前面加上类域。

(7)派生类的默认成员函数

默认成员函数,大家都很熟悉,就是指我们不写,编译器会变我们⾃动⽣成⼀个,那么在派⽣类中,这几个成员函数是如何⽣成的呢?

首先默认成员函数是以下几个

我们要注意的就是以下几点

1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。

如果基类没有默认的构造 函数,则必须在派⽣类构造函数的初始化列表阶段显示调用。

1)有默认构造函数的情况

#include<iostream>
using namespace std;

// 基类
class Base {
public:
    // 基类的默认构造函数
    Base() {
        cout << "Base default constructor called" << std::endl;
    }
};

// 派生类
class Derived : public Base {
public:
    // 派生类的构造函数
    Derived() {
       cout << "Derived constructor called" << std::endl;
    }
};

int main() {
    Derived d;
    return 0;
}

我们可以看到只使用了derived但是还是调用了base的默认构造函数。

2)没有默认构造函数的情况

#include <iostream>

// 基类
class Base {
public:
    // 基类的构造函数,需要一个参数
    Base(int value) {
        std::cout << "Base constructor called with value: " << value << std::endl;
    }
};

// 派生类
class Derived : public Base {
public:
    // 派生类的构造函数,在初始化列表中显式调用基类的构造函数
    Derived(int value) : Base(value) {
        std::cout << "Derived constructor called" << std::endl;
    }
};

int main() {
    Derived d(10);
    return 0;
}

如果我们不显示调用则会报错不存在默认构造函数

2. 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。

3. 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的 operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域 

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

5. 派⽣类对象初始化先调⽤基类构造再调派⽣类构造。

 6. 派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。

上述几点就和第一条类似。

#include <iostream>

// 基类
class Base {
private:
    int baseData;
public:
    // 基类构造函数
    Base(int data = 0) : baseData(data) {
        std::cout << "Base 构造函数被调用,baseData = " << baseData << std::endl;
    }

    // 基类拷贝构造函数
    Base(const Base& other) : baseData(other.baseData) {
        std::cout << "Base 拷贝构造函数被调用,baseData = " << baseData << std::endl;
    }

    // 基类赋值运算符重载
    Base& operator=(const Base& other) {
        if (this != &other) {
            baseData = other.baseData;
        }
        std::cout << "Base operator= 被调用,baseData = " << baseData << std::endl;
        return *this;
    }

    // 基类析构函数
    ~Base() {
        std::cout << "Base 析构函数被调用,baseData = " << baseData << std::endl;
    }
};

// 派生类
class Derived : public Base {
private:
    int derivedData;
public:
    // 派生类构造函数
    Derived(int baseData, int derivedData) : Base(baseData), derivedData(derivedData) {
        std::cout << "Derived 构造函数被调用,derivedData = " << derivedData << std::endl;
    }

    // 派生类拷贝构造函数
    Derived(const Derived& other) : Base(other), derivedData(other.derivedData) {
        std::cout << "Derived 拷贝构造函数被调用,derivedData = " << derivedData << std::endl;
    }

    // 派生类赋值运算符重载
    Derived& operator=(const Derived& other) {
        if (this != &other) {
            // 显式调用基类的 operator=
            Base::operator=(other);
            derivedData = other.derivedData;
        }
        std::cout << "Derived operator= 被调用,derivedData = " << derivedData << std::endl;
        return *this;
    }

    // 派生类析构函数
    ~Derived() {
        std::cout << "Derived 析构函数被调用,derivedData = " << derivedData << std::endl;
    }
};

int main() {
    // 派生类对象初始化,先调用基类构造再调用派生类构造
    Derived d1(10, 20);

    // 派生类拷贝构造函数调用基类拷贝构造函数
    Derived d2(d1);

    // 派生类 operator= 调用基类 operator=
    Derived d3(30, 40);
    d3 = d1;

    return 0;
}

(8)继承与友元

友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员

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

我们看到派生类是不能使用父类友元函数的。

(9)静态成员的继承

基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都 只有⼀个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;
}

(10)多继承

多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型 是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。

简而言之就是一个类继承了两个类或者更多,就大概如下图所示

但是多继承的存在很容易造成一种问题也就是菱形继承,什么是菱形继承呢?

菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以 看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中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;
}

而要解决这问题就需要涉及到下一个知识,虚继承。

(11)虚继承

在菱形继承结构中,如果存在多层继承关系,使得一个派生类从两个或多个基类中继承了相同的成员,就会产生数据冗余和二义性问题。

虚拟继承就是为了解决这种问题而引入的,通过在继承关系中使用virtual关键字,使得在最终的派生类中只保留一份共享的基类子对象。


虚继承

class Person
{
public:
	string _name; // 姓名 
	
};
// 使⽤虚继承Person类 
class Student : virtual public Person
{
protected:
	int _num; //学号 
};
// 使⽤虚继承Person类 
class Teacher : virtual public Person
{
protected:
	int _id; // 职⼯编号 
};
// 教授助理 
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程 
};
int main()
{
	// 使⽤虚继承,可以解决数据冗余和⼆义性 
	Assistant a;
	a._name = "peter";
	cout << a._name << endl;
	return 0;
}

就如上述代码所示,我们在有可能继承产生不确定性的person时加上virtual。

(12)继承与组合

在C++中,继承和组合是两种重要的代码复用机制。继承描述的是“是一个(is - a)”的关系,而组合描述的是“有一个(has - a)”的关系

简而言之,就是继承就是什么是什么,组合是什么有什么和什么,就像我们所熟知的数据结构

stack及其底层vector,我们就可以说stack是一种vector也可以是stack可以由vector组成,也就是他们既是has-a也是is-a的关系。下面我们看一个组合的例子

#include <iostream>
#include <string>

// 基类:服装
class Clothing {
public:
    // 设置服装的材质
    void setMaterial(const std::string& material) {
        this->material = material;
    }

    // 设置服装的颜色
    void setColor(const std::string& color) {
        this->color = color;
    }

    // 展示服装信息
    void displayInfo() const {
        std::cout << "材质: " << material << ", 颜色: " << color;
    }

private:
    std::string material;
    std::string color;
};

// 派生类:T恤,继承自 Clothing
class TShirt : public Clothing {
public:
    // 设置T恤的图案
    void setPattern(const std::string& pattern) {
        this->pattern = pattern;
    }

    // 重写展示信息的函数,加入图案信息
    void displayInfo() const {
        Clothing::displayInfo();
        std::cout << ", 图案: " << pattern << std::endl;
    }

private:
    std::string pattern;
};

// 类:裤子
class Pants {
public:
    // 设置裤子的款式
    void setStyle(const std::string& style) {
        this->style = style;
    }

    // 设置裤子的长度
    void setLength(const std::string& length) {
        this->length = length;
    }

    // 展示裤子信息
    void displayInfo() const {
        std::cout << "款式: " << style << ", 长度: " << length << std::endl;
    }

private:
    std::string style;
    std::string length;
};

// 类:穿搭,通过组合 TShirt 和 Pants
class Outfit {
public:
    // 获取 T 恤对象
    TShirt& getTShirt() {
        return tShirt;
    }

    // 获取裤子对象
    Pants& getPants() {
        return pants;
    }

    // 展示整套穿搭信息
    void displayOutfitInfo() const {
        std::cout << "整套穿搭信息:" << std::endl;
        std::cout << "上衣:";
        tShirt.displayInfo();
        std::cout << "裤子:";
        pants.displayInfo();
    }

private:
    TShirt tShirt;
    Pants pants;
};


int main() {
    Outfit outfit;

    // 设置 T 恤信息
    outfit.getTShirt().setMaterial("棉质");
    outfit.getTShirt().setColor("白色");
    outfit.getTShirt().setPattern("卡通图案");

    // 设置裤子信息
    outfit.getPants().setStyle("直筒裤");
    outfit.getPants().setLength("长裤");

    // 展示整套穿搭信息
    outfit.displayOutfitInfo();

    return 0;
}

本次分享就到这里结束了,后续会继续更新,感谢阅读!


网站公告

今日签到

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