“码” 上认亲!面向对象之继承:解锁代码的 “家族传承” 密码


个人主页

🎬 个人主页Vect个人主页

🎬 GitHubVect的代码仓库

🔥 个人专栏: 《数据结构与算法》《C++学习之旅》《计算机基础

⛺️Per aspera ad astra.


1.继承基础(公有继承基础下实现)

继承定义

在实现一个系统时,我们会经常遇到具有类似属性,但细节或行为存在细微差异的组件 。比如说外卖配送系统:最重要的三个角色:骑手、客户和商家,他们有共同的属性:地址、联系方式等等,也有每个类个性化的属性。

这种情况有两种解决方式:

  • 将每个组件声明位一个类,并在每个类中实现所有的属性,这会重复实现相同的属性
  • 继承: 从一个包含通用属性并且实现了通用功能基类派生出类似的类,并在类中覆盖基本功能,来实现让每个类都有独一无二的功能。

这便是面向对象的第二大特性:继承

如图:类之间的继承关系

在这里插入图片描述

根据上述介绍,可以总结出继承的定义:

继承是面向对象程序设计使代码可以复用的手段,允许设计者在保证原有类特性的基础上进行扩展,增加新的功能,产生新的类。继承呈现了面向对象程序设计的层次结构

继承语法

在这里插入图片描述

#include <iostream>
#include <string>
using namespace std;

// 鱼品种的基类
class Fish {
protected:
	bool isFreshWaterFish;
public:
	void swim() {
		if (isFreshWaterFish) cout << "在湖泊小河中生存的鱼" << endl;
		else cout << " 在海洋中生存的鱼" << endl;
	}
};

class Carp : public Fish {
public:
	Carp() { isFreshWaterFish = true; }
};

class Tuna : public Fish {
public:
	Tuna() { isFreshWaterFish = false; }
};

int main() {
	Carp myLunch;
	Tuna myDinner;

	cout << "我的食物:" << endl;
	cout << "午餐:";
	myLunch.swim();
	cout << "晚餐:";
	myDinner.swim();

	return 0;
}

注意protected这个限定符出现在基类中,它的意思是保护isFreshWaterFish只能在继承层次结构体系中访问和使用,而若没有protected,则能在继承层次结构体系外部访问和使用。

基类初始化——向基类传递参数

如果基类包含重载的构造函数,需要在实例化时给它提供实参,该如何办呢?创建派生对象时将如何实例化这样的基类?方法是使用初始化列表,并通过派生类的构造函数调用合适的基类构造函数,代码如下所示:

class Base {
public:
	Base(int someNumber){
		//...
	}
};
class Derived : public Base {
public:
	Derived()
		:Base(5) // 用基类走初始化列表
	{ }
};

对于我们定义的Fish类,通过给Fish的构造函数提供一个布尔值,来初始化Fish::isFreshWaterFish,强制每个派生类指出自己的品种,代码如下:

class Fish {
protected:
	bool isFreshWaterFish;
public:
	Fish(bool isFreshWaterFish)
		: isFreshWaterFish(isFreshWaterFish)
	{ }

	void swim() {
		if (isFreshWaterFish) cout << "在湖泊小河中生存的鱼" << endl;
		else cout << " 在海洋中生存的鱼" << endl;
	}
};

class Carp : public Fish {
public:
	Carp()
		:Fish(true)
	{ }
};

class Tuna : public Fish {
public:
	Tuna()
		:Fish(false)
	{ }
};

int main() {
	Carp myLunch;
	Tuna myDinner;

	cout << "我的食物:" << endl;
	cout << "午餐:";
	myLunch.swim();
	cout << "晚餐:";
	myDinner.swim();

	return 0;
}

Fish有一个构造函数,接受一个参数用于初始化Fish::isFreshWaterFish。因此,要创建Fish对象,必须提供一个用于初始化该保护成员的参数,这样Fish避免了保护成员包含随机值的情况,派生类CarpTuna被迫定义一个构造函数,使用合适的参数来实例化基类Fish

在派生类中覆盖基类的方法

派生类用完全相同的函数签名重写基类的函数,相当于覆盖了基类的这个函数,

如下展示:

// 在派生类中覆盖基类的方法
class Base {
public:
	void func() {
		// ...
	}
};
class Derived : public Base {
public:
	void func() {
		// ...
	}
};

如果使用Derived类的实例化对象调用Func,调用不是Base类中的Func

我们换一组新的继承层次关系演示:

class Livestock {
protected:
    bool isWoolProducer;
public:
    Livestock(bool isWoolProducer)
        : isWoolProducer(isWoolProducer)
    {
    }

    void introduce() {
        if (isWoolProducer) cout << "以羊毛为主要产出的畜牧动物" << endl;
        else cout << "以奶/肉为主要产出的畜牧动物" << endl;
    }
};

class Cow : public Livestock {
public:
    Cow()
        : Livestock(false) 
    { }
    void introduce() {
        cout << "体重基数大,肉质紧实" << endl;
    }
};

class Sheep : public Livestock {
public:
    Sheep()
        : Livestock(true)  
    { }
    void introduce() {
        cout << "体重基数小,肉质软嫩" << endl;
    }
};

int main() {
    Cow myLunch;
    Sheep myDinner;

    cout << "我的食物:" << endl;
    cout << "午餐:";
    myLunch.introduce();
    cout << "晚餐:";
    myDinner.introduce();

    return 0;
}

Sheep::introduce覆盖了Livestock::introduce,想要调用Livestock::introduce,只能在main()中使用域作用做限定符显式调用

在派生类中调用基类

// 在派生类中调用基类的方法 ::显式调用
class Livestock {
protected:
    bool isWoolProducer;
public:
    Livestock(bool isWoolProducer)
        : isWoolProducer(isWoolProducer)
    {
    }

    void introduce() {
        if (isWoolProducer) cout << "以羊毛为主要产出的畜牧动物" << endl;
        else cout << " 以奶/肉为主要产出的畜牧动物" << endl;
    }
};

class Cow : public Livestock {
public:
    Cow()
        : Livestock(false) 
    { }
    void introduce() {
        cout << " 体重基数大,肉质紧实" << endl;
    }
};

class Sheep : public Livestock {
public:
    Sheep()
        : Livestock(true)  
    { }
    void introduce() {
        cout << "体重基数小,肉质软嫩" << endl;
    }
};

int main() {
    Cow myLunch;
    Sheep myDinner;

    cout << "我的食物:" << endl;
    cout << "午餐:";
    myLunch.Livestock::introduce();
    cout << "晚餐:";
    myDinner.Livestock::introduce();

    return 0;
}

在派生类中隐藏基类

覆盖的一种极端情况是隐藏

派生类里出现了同名成员(变量或函数),会把基类同名成员全部挡住——不论参数是否相同。这是名字查找规则导致的,属于编译期静态绑定问题

注意:隐藏只要是同名成员即是隐藏,这里函数隐藏和函数重载一定要区分开

  • 函数隐藏:出现在继承层次结构中,基类和派生类出现同名函数,派生类会隐藏基类的函数
  • 函数重载:同一作用域里,同名但参数列表不同的一组函数
// 隐藏:派生类中出现和基类同名的成员 隐藏基类成员
class Livestock {
protected:
    bool isWoolProducer;
public:
    Livestock(bool isWoolProducer)
        : isWoolProducer(isWoolProducer)
    {
    }


    void introduce(bool isWoolProducer) {
        if (isWoolProducer) cout << "以羊毛为主要产出的畜牧动物" << endl;
        else cout << " 以奶/肉为主要产出的畜牧动物" << endl;
    }
    void introduece() { cout << "......" << endl; }
};

class Cow : public Livestock {
public:
    Cow()
        : Livestock(false) 
    { }
    void introduce() {
        cout << " 体重基数大,肉质紧实" << endl;
    }
};

int main() {
    Cow myLunch;

    cout << "我的食物:" << endl;
    cout << "午餐:";
    myLunch.introduce();
    // error C2660: “Cow::introduce”: 函数不接受 1 个参数
    // myLunch.introduce(false);
    return 0;
}

注意看:这个版本Livestock实现了两个introduce的重载,Cow也实现了introduce,将基类的两个重载都隐藏了,如果取消注释36行,会发生编译报错。

那么,如何调用这两个重载呢?

  • main()中使用域作用限定符

myLunch.Livestock::introduce();

  • Cow类中,将Livestockintroduce
class Cow : public Livestock {
public:
    using Livestock::introduce;
    Cow()
        : Livestock(false) 
    { }
    void introduce() {
        cout << " 体重基数大,肉质紧实" << endl;
    }
};

2. 派生类中默认成员函数的行为

构造和析构

Cow是从Livestock派生而来的,创建Cow对象时,先调用Cow的构造函数还是Livestock的构造函数?实例化对象时,成员属性(Livestock::isWoolProducer)是调用构造函数之前实例化还是之后实例化?

基类对象在派生对象之前被实例化

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。若基类没有默认构造函数,则必须再派生类的构造函数初始化列表显式调用
  • 派生类对象初始化先调用基类的构造再调用派生类构造
  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员,保证先清理派生类成员再清理基类成员
  • 派生类对象析构清理先调用派生类析构再调用基类析构

拷贝构造和赋值重载

  • 派生类的拷贝构造必须调用基类的拷贝构造完成基类的拷贝初始化
  • 派生类的operator=必须要调用基类的operator=完成基类的复制

具体行为演示

// 派生类成员函数行为

// 打印名字,观察顺序
struct Tracker {
	string _name;
	Tracker(string n)
		:_name(std::move(n)) {
		cout << " 构造 " << _name << endl;
	}
	~Tracker() { cout << " 析构 " << _name << endl; }
};

class Livestock {
protected:
	bool _isWoolProducer;
	int _age;
	Tracker _baseInfo;

	Livestock(bool w,int age) 
		:_isWoolProducer(w) 
		,_age(age)
		,_baseInfo("Livestock.baseInfo")
	{
		cout << "Livestock构造" << endl;
	}

	Livestock(const Livestock& other) 
		:_isWoolProducer(other._isWoolProducer)
		,_age(other._age)
		,_baseInfo(other._baseInfo)
	{
		cout << "Livestock拷贝构造" << endl;
	}
	~Livestock() { cout << "Livestock析构" << endl; }

	Livestock& operator=(const Livestock& copy) {
		if (this != &copy) {
			_isWoolProducer = copy._isWoolProducer;
			_age = copy._age;
			_baseInfo = copy._baseInfo;
			cout << "Livestock赋值重载" << endl;
		}
		return* this;
	}

	void introduce(bool wool) {
		if (wool) cout << "已羊毛为主要产出的畜牧动物" << endl;
		else cout << "以奶/肉为主要产出的畜牧动物" << endl;
	}

	void introduce() { cout << "......" << endl; }

};

class Cow : public Livestock {
private:
	int _weight;
	Tracker _cowInfo;
	const int _ID;
	const int& _ageAlias;
	
	static int weightInitial(int baseAge) { return 400 + baseAge * 10; } // 瞎编的

public:
	Cow()
		:Livestock(false, 3)			// 构造基类子对象 永远在最先
		, _ID(1234)						// cosnt成员必须在初始化列表赋值
		, _cowInfo("Cow._cowInfo")		// 根据声明顺序走初始化列表
		, _ageAlias(_age)				// 用基类的_age 基类已经构造好 安全
		, _weight(weightInitial(_age))
	{
		cout << "Cow 构造" << endl;
		cout << "-->weight=" << _weight
			<< ",age=" << _age
			<< ",ID=" << _ID << endl;
	}

	Cow(const Cow& other)
		:Livestock(other)		 // 先拷贝构造基类子对象(按基类列表顺序)
		, _weight(other._weight)
		, _cowInfo(other._cowInfo)
		, _ID(other._ID)
		, _ageAlias(_age)          // 绑定自己本体的 _age 而不是 other._ageAlias
	{
		cout << "Cow拷贝构造" << endl;
	}

	Cow& operator=(const Cow& copy) {
		if (this != &copy) {
			Livestock::operator=(copy);  // 先赋基类子对象
			_weight = copy._weight;
			_cowInfo = copy._cowInfo;
			// _ID 是 const:不可赋值(保持原值)
			// _ageAlias 是引用:不可重新绑定(仍引用“本对象的 _age”)
			cout << "Cow赋值重载" << endl;
		}
		return *this;
	}

	~Cow() { cout << "Cow析构" << endl; }

	using Livestock::introduce;
	void introduce() { cout << "体重基数大,肉质紧实" << endl; }

};

int main() {
	cout << "=== 构造 ===" << endl;
	Cow myLunch;
	cout << "午餐:" << endl;
	myLunch.introduce(); // 调用Cow无参版本
	cout << "午餐(基类重载):" << endl;
	myLunch.introduce(false); // 有using 可以访问到基类


	cout << "=== 拷贝构造 ===" << endl;
	Cow copyLunch = myLunch;   // 调用 Cow(const Cow&)

	cout << "=== 拷贝赋值 ===" << endl;
	Cow assigned;              // 先默认构造一个
	assigned = myLunch;        // 调用 Cow::operator=(const Cow&)

	cout << "=== 析构 ===" << endl;
	return 0;

}

默认构造调用顺序

  1. 先构造基类子对象:调用 Livestock(false, 3);其成员按声明顺序初始化:_isWoolProducer → _age → _baseInfo,再执行基类构造函数体。
  2. 再构造派生成员(按 Cow 中的声明顺序,与初始化列表书写顺序无关):_weight → _cowInfo → _ID → _ageAlias
    其中 _ID 必须在初始化列表给值,_ageAlias 在初始化列表绑定到本对象的 _age
  3. 最后进入 Cow 构造函数体,打印“Cow 构造”。

拷贝构造调用顺序

  1. 先调用 Livestock(const Livestock&) 拷贝构造基类部分。
  2. 再按 Cow 的成员声明顺序拷贝构造成员:_weight, _cowInfo, _ID, _ageAlias
    _ID 在初始化列表拷贝,_ageAlias 绑定本对象_age(而非对方的引用)。
  3. 执行 Cow 拷贝构造函数体,打印“Cow拷贝构造”。

拷贝赋值调用顺序

  1. 先调用 Livestock::operator=(copy)基类部分赋值
  2. 再给派生的可赋值成员逐个赋值:_weight, _cowInfo
    _ID(const)不可赋值_ageAlias(引用)不可改绑

析构调用顺序

  1. 先执行 Cow 析构函数体并销毁其成员(逆声明顺序)。
  2. 再执行 Livestock 析构函数体并销毁其成员(逆声明顺序)。

函数可见性
using Livestock::introduce; 解除名字隐藏,因此既可调用 Cow::introduce(),也可调用基类重载 introduce(bool)

在这里插入图片描述

3. 私有继承和保护继承

私有继承

私有继承不同之处在于,指定派生类的基类时使用关键字private

class Base{
    // ...
};
class Derived: private Base{
    // ...
};

私有继承意味着在派生类的实例中,基类的公有成员和方法都是私有的——不能从外部访问。即是==即便是Base类的公有成员和方法,也只能被Derived类使用,而无法通过Derived实例来使用

保护继承

class Base{
    // ...
};
class Derived: protected Base{
    // ...
};

保护继承意味着:在 Derived 的实例里,Base 的公有成员和受保护成员在 Derived 中都变成了 protected
所以:

  • 外部代码:不能通过 Derived 实例来访问这些成员(对外不可见)。
  • **Derived 自己以及Derived 的子类:可以使用这些从 Base 继承来的成员(在类内/子类内可见)。
  • 对外也不能把 Derived 隐式当作 Base 用(不能隐式转换成 Base* / Base&)。

换句话说——

即便是 Base 的公有成员和方法,在“保护继承”下,也只能被 Derived 以及它的派生类使用无法通过 Derived 实例在类外直接使用

以下是各种继承的关系:

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

    • 若派生类用public继承:在派生类中仍为public成员(派生类对象可直接访问,派生类的派生类也能按规则继承)。
    • 若派生类用protected继承:在派生类中变为protected成员(派生类对象不可直接访问,但派生类的成员函数和其派生类可访问)。
    • 若派生类用private继承:在派生类中变为private成员(仅派生类自己的成员函数可访问,其派生类无法访问)。
  2. 基类的protected成员

    • 若派生类用public继承:在派生类中仍为protected成员(规则同上,保持 “保护” 特性)。
    • 若派生类用protected继承:在派生类中仍为protected成员(继承后权限不变)。
    • 若派生类用private继承:在派生类中变为private成员(权限被 “收紧” 为私有)。
  3. 基类的private成员

    • 无论派生类用哪种方式继承(public/protected/private),基类的private成员在派生类中完全不可见(派生类的成员函数和对象都无法直接访问,只能通过基类提供的public/protected成员函数间接访问)。

    三种继承方式衍生出九种情况,这样的设计有点冗余,在实践中基本都是使用公有继承

    4. 基类和派生类对象赋值转换

    派生类对象可以赋值给基类的对象、基类的指针、基类的引用,这种行为称为切割,把派生类中基类的那一部分切割后赋值过去。

    但是,基类对象不能赋值给派生类对象

在这里插入图片描述

// 派生类对象可以赋值给基类的对象、指针、引用
class Person {
protected:
	int _age;
	string _sex;
	string _name;
};

class Student : public Person {
protected:
	int _score;
};
int main() {
	Student stuObj;
	Person pA = stuObj;
	Person& pB = stuObj;
	Person* pC = &stuObj;

	// 基类对象不能赋值给派生类对象 小->大 不行
	//stuObj = pA;

	return 0;
}

5. 多继承

多继承是指一个派生类会继承多个基类的属性

  • 好处:能把互不相干的能力(例如“可充电”“可联网”)按横向能力接口拆开,某个具体对象可以一次性“拿来就用”。

  • 风险:若多个基类间存在继承关系,可能出现菱形继承带来的二义性

就比如说生活中的共享电动车,它可充电可联网属于电动车

// 多继承
// 共享电动车: 可充电 可联网 是电动车

class eBike {
public:
	void name(){ cout << "是电动车" << endl; }
};
class chargeAble {
public:
	void canCharge() { cout << "可充电" << endl; }
};
class webConnectable {
public:
	void canConnectWeb() { cout << "可联网" << endl; }
};

class shareEBike : public eBike
				 , public chargeAble
				 , public webConnectable{
public:
	void bike() { cout << "是共享电动车" << endl; }
};

int main() {
	shareEBike bike;
	bike.bike();
	bike.name();
	bike.canConnectWeb();
	bike.canCharge();

	return 0;
}

6. final禁止基类被继承

C++11起,编译器支持限定符final。被声明为final的类不能用作基类。

例如:

class shareEBike final: public eBike
				 , public chargeAble
				 , public webConnectable{
public:
	void bike() { cout << "是共享电动车" << endl; }
};

7. 全文总结

  1. 继承基础(默认用公有继承
  • 动机/场景:多个组件“有共同属性细节不同”(外卖系统:骑手/客户/商家)。
    两种做法:
    ① 各写一套(重复);② 抽公共到基类,差异在派生类中扩展/覆盖(继承)。

  • 定义:继承让代码复用并形成层次结构,在保留原有类特性的基础上扩展新功能。

  • 语法/示例(Fish)
    class Derived : public Base {}protected成员仅类族可见

    class Fish { protected: bool isFreshWaterFish; public: void swim(){...} };
    class Carp : public Fish { public: Carp(){ isFreshWaterFish = true; } };
    class Tuna : public Fish { public: Tuna(){ isFreshWaterFish = false; } };
    
  • 基类初始化(向基类传参):派生类初始化列表调用合适的基构造,避免未初始化。

    class Fish{ protected: bool isFreshWaterFish; 
      public: Fish(bool b):isFreshWaterFish(b){} };
    class Carp: public Fish { public: Carp():Fish(true){} };
    class Tuna: public Fish { public: Tuna():Fish(false){} };
    

在派生类中覆盖/调用/隐藏基类方法

  • 覆盖:派生类用相同函数签名重写基类函数。

    class Base{ public: void func(){} };
    class Derived: public Base{ public: void func(){} }; // 覆盖
    
  • 显式调用基类版本对象.Base::func()

  • 名字隐藏:派生类出现同名成员遮挡基类同名成员(与参数是否相同无关)。
    解除隐藏:在派生类中 using Base::introduce;

  • 示例(Livestock/Cow/Sheep)

    • Sheep::introduce() 覆盖 Livestock::introduce()
    • 通过 Livestock::introduce()using 访问被遮挡的重载。
  1. 派生类中默认成员函数的行为(顺序与规则)
  • 构造顺序先基类 → 后派生成员(按声明顺序)→ 执行派生构造体。
    常量/引用成员必须在初始化列表赋值/绑定。
  • 拷贝构造先拷贝基类部分,再拷贝派生成员。
  • 赋值运算operator= 先赋基类,再赋派生成员;const/引用成员不可重新赋值/改绑。
  • 析构顺序:先析构派生,再析构基类(与构造相反)。
  • 可见性using Base::introduce; 可解除隐藏,保留基类重载的可见性。
  1. 继承方式:public / protected / private
  • public 继承(推荐):基类 public → 派生 public;语义清晰(is-a)。
  • protected 继承:基类 public/protected 在派生类中都变为 protected(仅类族可用,对外不可见)。
  • private 继承:基类 public/protected 在派生类中都变为 private(对外完全隐藏,更像实现复用)。
  • 共同点:基类的 private 成员对子类不可见(只能经由基类接口访问)。

实务上优先 public 继承,其余两种少用、语义不直观。

  1. 基类与派生类的赋值/转换
  • 派生 → 基类:允许(对象切片 / 指针或引用向上转型)。

  • 基类 → 派生:禁止(不安全)。

    Student stu; Person pa = stu; Person& pr = stu; Person* pp = &stu; // 反向不行
    
  1. 多继承
  • 定义:一个派生类可同时继承多个基类。

  • 优点:可把**互不相关的“能力”*独立为接口,进行*横向组合(比如“可充电”“可联网”)。

  • 风险:若多基类之间存在继承,易引出菱形与二义性

  • 生活化示例

    class eBike{ public: void name(){ cout<<"是电动车\n"; } };
    class chargeAble{ public: void canCharge(){ cout<<"可充电\n"; } };
    class webConnectable{ public: void canConnectWeb(){ cout<<"可联网\n"; } };
    class shareEBike : public eBike, public chargeAble, public webConnectable {
      public: void bike(){ cout<<"是共享电动车\n"; }
    };
    

    接口式多继承

  1. final 禁止被继承(C++11)
  • 作用于类:class X final { ... }; —— X 不可再作基类。
  • 你的示例:class shareEBike final : public eBike, ... { ... };
  1. is-ahas-a
  • is-a(是一个):语义上“子类就是基类的一种”。
    → 采用公有继承(如 Tuna : public FishshareEBike : public eBike)。
  • has-a(有一个/由……组成):整体拥有部件。
    → 采用组合/聚合(成员对象),不要用继承(如电动车电池:class EBike { Battery battery_; })。
  • 横向“能力”拼装(既非父子、也非组成):
    → 用接口式多继承(如 Rechargeable + NetworkConnectable
  • 概念从属 → public 继承(is-a);
  • 组成拥有 → 组合(has-a);
  • 能力叠加 → 接口式多继承(无菱形)。
评论 50
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值