C++ | 面向对象 | 继承

👻 继承种类

👾单继承

  • 已有的类称为基类 ,继承它的类称为派生类
  • 派生 Derive继承 Inheritance 是角度不同的一个概念
  • 继承是儿子接收父亲的产业,派生是父亲传承产业给儿子

👽语法格式

c++ 复制代码
class Derived(派生类): access-specifier(继承方法) Base(基类)

👽示例代码

c++ 复制代码
// 基类
class Shape {
	public:
		void setWidth(int w) { width = w; }
		void setHeight(int h) { height = h; }
	protected:
		int width;
		int height;
};

// 派生类
class Rectangle: public Shape {
	public:
		int getArea() { return (width * height); }
};

...
     
Rectangle Rect;
Rect.setWidth(5);
Rect.setHeight(7);
int area = Rect.getArea();

👾多继承

  • 一个类继承自多个基类 1:n,使用类派生列表来指定基类

👽语法格式

c++ 复制代码
class Derived : access-specifier Base1, access-specifier Base2 , ...

👽示例代码

c++ 复制代码
// 基类 Shape
class Shape {
	public:
		void setWidth(int w) { width = w; }
		void setHeight(int h) { height = h; }
	protected:
		int width;
		int height;
};

// 基类 PaintCost
class PaintCost {
	public:
		int getCost(int area) { return area * 70; }
};

// 派生类
class Rectangle: public Shape, public PaintCost {
	public:
		int getArea() { return (width * height); }
};

...

Rectangle Rect;
// 使用继承自 Shape 类的方法 setWidth()、setHeight()
Rect.setWidth(5);
Rect.setHeight(7);
// 使用继承自 PaintCost 类的方法 getCost(area)
cout << "Cost = " << Rect.getCost(Rect.getArea()) << endl;

👾菱形继承

派生类通过间接或直接的方式多次继承同一个祖先基类 ,不仅占用较多的存储空间,还容易产生命名冲突。

类 A 派生 B 和 C ,类 D 继承自 B 和 C

  • 数据冗余 :类 A 中的成员继承到类 D 中变成两份,来自 A-->B-->DA-->C-->D ,存储两份 m_a,造成数据冗余
  • 命名冲突 :在类 D 中直接访问 a 会产生歧义,编译器不知道它究竟来自 A-->B-->D ,还是 A-->C-->D
c++ 复制代码
// 间接基类A
class A {
	protected:
		int m_a;
};

// 直接基类B
class B: public A {
	protected:
		int m_b;
};

// 直接基类C
class C: public A {
	protected:
		int m_c;
};

// 派生类D
class D: public B, public C {
	public:
		void seta(int a) { m_a = a; } // 命名冲突
		void setb(int b) { m_b = b; } // 正确
		void setc(int c) { m_c = c; } // 正确
		void setd(int d) { m_d = d; } // 正确
	private:
		int m_d;
};

...
D d;
[Error] reference to 'm_a' is ambiguous

为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:

c++ 复制代码
void seta(int a){ B::m_a = a; } // 使用类 B 的 m_a
或
void seta(int a){ C::m_a = a; } // 使用类 C 的 m_a

👾环状继承

环状继承并不是一个合法的继承方式,而是指在继承关系中,类之间形成了一个闭环,导致编译器无法正确解析继承关系。环状继承在C++中是不被允许的,会导致编译错误。

  • 编译错误:环状继承会导致编译器无法确定类的定义顺序,因此会报错。
  • 逻辑错误:从逻辑上讲,环状继承没有意义,因为类A和类B相互依赖,无法确定谁是基类,谁是派生类。
cpp 复制代码
class A : public B {};  // A继承自B
class B : public A {};  // B继承自A

👻 虚继承

👾概念

  • 让某个类做出声明,承诺愿意共享它的基类,被共享的基类就称为虚基类。
  • 不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员

👾语法格式

在继承方式前面加上 关键字 virtual 实现虚继承

c++ 复制代码
class Derived(派生类): virtual(虚继承) access-specifier(继承方法) Base(基类)

👾示例代码

c++ 复制代码
// 间接基类A
class A {
	protected:
		int m_a;
};

// 直接基类B
class B: virtual public A { //虚继承
	protected:
		int m_b;
};

// 直接基类C
class C: virtual public A { //虚继承
	protected:
		int m_c;
};

// 派生类D
class D: public B, public C {
	public:
		void seta(int a) { m_a = a; }   // 正确
		void setb(int b) { m_b = b; }   // 正确
		void setc(int c) { m_c = c; }   // 正确
		void setd(int d) { m_d = d; }   // 正确
	private:
		int m_d;
};

...
D d;
  • 会发现虚继承的一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作 。在上图中,当定义 D 类时才出现了对虚派生的需求 ,如果 B 类和 C 类不是从 A 类虚派生得到的,那么 D 类还是会保留 A 类的两份成员。换个角度讲,虚派生只影响从指定虚基类的派生类中进一步派生出来的类,它不会影响派生类本身
  • 在实际开发中,位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。
    虚基类成员二义性

假设 A 定义了一个名为 x 的成员变量,当在 D 中直接访问 x 时,会有三种可能性:

  • 如果 B 和 C 中都没有 x 的定义, x 将被解析为 A 的成员,不存在二义性
  • 如果 B 或 C 其中的一个类定义了 x,派生类的 x 比虚基类的 x 优先级更高,不存在二义性
  • 如果 B 和 C 中都定义了 x,直接访问 x 将产生二义性问题

不提倡在程序中使用多继承

👻 访问控制

👾成员变量

  • 派生类可以访问基类中所有的非私有成员 。基类如不想被派生类访问,应声明为 private
访问 public protected private
同一个类
派生类(继承) ×
外部类 × ×

可以通过 调用基类提供的公有成员函数入口 来访问基类的私有成员变量

c++ 复制代码
class Base {
	private:
		int privateVar;
	public:
		Base() : privateVar(10) {}
		void accessPrivateVar() {
			cout << "Base privateVar: " << privateVar << endl;
		}
};

class Derived : public Base {
	public:
		void accessBasePrivateVar() {
			accessPrivateVar(); // 通过调用基类的公有成员函数来访问基类的私有成员变量
		}
};

...

Derived d;
d.accessBasePrivateVar(); // 输出 Base privateVar: 10

👾成员函数

派生类继承所有的基类方法,以下情况例外:

  • 基类的构造函数、析构函数、拷贝构造函数
  • 基类的重载运算符
  • 基类的友元函数

👻 继承类型

当一个类派生自基类,可被继承为 public、protectedprivate 类型,遵循规则:

  • 公有继承(public):
    • 基类的公有 成员也是派生类的公有成员
    • 基类的保护 成员是派生类的保护成员,
    • 基类的私有 成员不能直接被派生类访问,但可以通过调用基类的公有保护成员访问
  • 保护继承(protected): 基类的公有保护 成员将成为派生类的保护成员
  • 私有继承(private):基类的公有保护 成员将成为派生类的私有成员

👻 向上转型

在类中

  • 向上转型(Upcasting)--- 将派生类 赋值给基类 ,包括将派生类对象、指针、引用 赋值给基类,由编译器自动完成,非常安全
  • 向下转型(Downcasting)--- 将基类 赋值给派生类 ,由程序员手动干预,有风险

👾派生类对象 ---> 基类对象

c++ 复制代码
// 基类
class A {
	public:
		int m_a;
		A(int a): m_a(a) { }
		void display() { cout<<"Class A: m_a = "<<m_a<<endl; }
};

// 派生类
class B: public A {
	public:
		int m_b;
		B(int a, int b): A(a), m_b(b) { }
		void display() { cout<<"Class B: m_a = "<<m_a<<", m_b = "<<m_b<<endl; }
};

...

A a(1); B b(5, 10);
a.display(); b.display();  // 赋值前,输出 Class A: m_a = 1
							   // Class B: m_a = 5, m_b = 10
a = b;
a.display(); b.display();  // 赋值后,输出 Class A: m_a = 5
							   // Class B: m_a = 5, m_b = 10
  • 赋值不会影响成员函数this 指针 。即 a.display() 始终调用的都是 A 类的 display() 函数,不存在成员函数的赋值。

  • 赋值时,会舍弃派生类新增的成员。即 B 类的 m_b 变量被舍去,不存在 a.m_b , a 不包含成员 m_b

👾派生类指针 ---> 基类指针

c++ 复制代码
// 基类A
class A {
	public:
		A(int a): m_a(a) { }
		void display(){ cout << "Class A: m_a = " << m_a << endl; } 
	protected:
		int m_a;
};

// 中间派生类B
class B: public A {
	public:
		B(int a, int b): A(a), m_b(b) { }
		void display(){ cout<<"Class B: m_a = "<< m_a << ", m_b =" << m_b << endl; } 
	protected:
		int m_b;
};

// 基类C
class C {
	public:
		C(int c): m_c(c) { }
	public:
		void display(){ cout << "Class C: m_c = " << m_c << endl; } 
	protected:
		int m_c;
};

// 最终派生类D
class D: public B, public C {
	public:
		D(int a, int b, int c, int d): B(a, b), C(c), m_d(d) { }
		void display(){
			cout << "Class D: m_a = " << m_a << ", m_b = " << m_b << ", m_c = " 
                    << m_c << ", m_d = " << m_d << endl;
		}
	private:
		int m_d;
};

...

A *pa = new A(1);  		// m_a = 1
C *pc = new C(3);		// m_c = 3
B *pb = new B(2, 20);	// m_a = 2, m_b = 20
D *pd = new D(4, 40, 400, 4000);	// m_a = 4, m_b = 40,m_a = 400, m_b = 4000

pa(基) = pd(派);  pa -> display();	// 输出 Class A: m_a = 4
pb(基) = pd(派);  pb -> display();	// 输出 Class B: m_a = 4, m_b = 40
pc(基) = pd(派);  pc -> display();	// 输出 Class C: m_c = 400

cout << pa << endl;  // 输出 0xcb6470
cout << pb << endl;  // 输出 0xcb6470
cout << pc << endl;  // 输出 0xcb6478
cout << pd << endl;  // 输出 0xcb6470
  • 继承关系:

  • 指针赋值不拷贝对象的成员,不修改对象本身的数据,仅仅是改变了指针的指向

  • 指针赋值后,虽然基类的成员变量值改为派生类变量值,但是仍然使用基类的成员方法

c++ 复制代码
// 基类People
class People {
	public:
		People(char *name, int age): m_name(name), m_age(age) { }
		void display() {
			cout << m_name << "今年" << m_age << "岁,无业游民" << endl;
		}
	protected:
		char *m_name;
		int m_age;
};


// 派生类Teacher
class Teacher: public People {
	public:
		Teacher(char *name, int age, int salary): People(name, age), m_salary(salary) { }
		void display() {
			cout << m_name << "今年" << m_age << "岁,教师,每月" << m_salary << "元收入" << endl;
		}
	private:
		int m_salary;
};

...

People *p = new People("小白", 23);
p -> display();		// 小白今年23岁,无业游民

p = new Teacher("小红", 45, 7000);
p -> display();		// 小红今年45岁,无业游民  

基类指针 p 指向派生类 Teacher 对象时,虽然使用 Teacher 的成员变量 m_namem_age ,但没有使用它的成员函数Teacher.display(),导致输出结果不伦不类(小红今年45岁,,教师,每月 7000 元收入)

解决方法是使用多态机制 (基类指针可调用基类方法 ,也可调用派生类方法,有多种形态)

👾派生类引用 ---> 基类引用

c++ 复制代码
// 基类A
class A {
	public:
		A(int a): m_a(a) { }
		void display(){ cout << "Class A: m_a = " << m_a << endl; } 
	protected:
		int m_a;
};

// 中间派生类B
class B: public A {
	public:
		B(int a, int b): A(a), m_b(b) { }
		void display(){ cout<<"Class B: m_a = "<< m_a << ", m_b =" << m_b << endl; } 
	protected:
		int m_b;
};

// 基类C
class C {
	public:
		C(int c): m_c(c) { }
	public:
		void display(){ cout << "Class C: m_c = " << m_c << endl; } 
	protected:
		int m_c;
};

// 最终派生类D
class D: public B, public C {
	public:
		D(int a, int b, int c, int d): B(a, b), C(c), m_d(d) { }
		void display(){
			cout << "Class D: m_a = " << m_a << ", m_b = " << m_b << ", m_c = " 
                    << m_c << ", m_d = " << m_d << endl;
		}
	private:
		int m_d;
};

...

D d(4, 40, 400, 4000);
A &ra = d;
B &rb = d;
C &rc = d;

ra.display();	//  输出 Class A: m_a = 4
rb.display();	//  输出 Class B: m_a = 4, m_b =40
rc.display();	//  输出 Class C: m_c = 400

[!CAUTION]

引用和指针的表现是一致的

相关推荐
溟洵1 小时前
【C/C++】理解C++内存与Linux虚拟地址空间的关系---带你通透C++中所有数据
linux·c语言·c++
Chandler243 小时前
C++11 智能指针:unique_ptr、shared_ptr和weak_ptr 功能特性 模拟实现
开发语言·c++
tianmu_sama5 小时前
[Linux高性能服务器编程] 多线程编程
linux·服务器·c++·算法
挨代码5 小时前
UE_C++ —— Gameplay Tags
c++·ue
敢嗣先锋5 小时前
鸿蒙5.0实战案例:基于WaterFlow的页面滑动加载
c++·移动开发·harmonyos·arkui·组件化·鸿蒙开发·页面布局
weixin_399264296 小时前
QT C++ QtConcurrent::run 异步任务 简单例子
开发语言·c++
御风@户外6 小时前
qt5的中文乱码问题,QString、QStringLiteral 为 UTF-16 编码
c++·qt·乱码
JKHaaa6 小时前
单链表的排序(C++)
数据结构·c++·算法
什码情况6 小时前
C++ 正则表达式分组捕获入门指南
c++·正则表达式·分组捕获