[C++]类的继承

news/2024/12/19 1:27:01/

一、什么是继承

1.定义

        在 C++ 中,继承是一种机制,允许一个类(派生类)继承另一个类(基类)的成员(数据和函数)。继承使得派生类能够直接访问基类的公有和保护成员,同时也可以对这些成员进行扩展或修改。继承是一种“是一个”的关系,它允许一个类从另一个类继承其属性和方法,从而实现代码的复用。派生类是基类的一种特殊类型。

如何理解“是一个”?

如"狗是动物","猫是动物",Cat,Dog这两个类都继承了Animal这个类,它们是属于的关系,是“是一个”的关系。

2.继承的作用

        继承的主要作用是实现代码的复用:派生类可以重用基类的代码,而不需要重复编写相同的功能。同时,继承也允许派生类扩展或修改基类的行为。


二、基类与派生类

1.基类

        定义通用功能的类,供其他类继承。

2.派生类

        从基类继承的类,可以继承、扩展或修改基类的功能。

3.简单示例

        用一个简单的动物类和狗类示例,展示基类和派生类的关系。

#include <iostream>
using namespace std;// 基类:动物
class Animal {
public:void eat() { cout << "Animal is eating" << endl; }
};// 派生类:狗(继承了动物的功能)
class Dog : public Animal {
public:void bark() { cout << "Dog is barking" << endl; }
};int main() {Dog dog;dog.eat(); // 狗继承了动物的吃饭功能dog.bark(); // 狗有自己的叫的功能return 0;
}

三、继承的访问控制

1.公有继承

        当派生类以 public 方式继承基类时,基类的公有成员和保护成员在派生类中保持原有的访问权限,而私有成员完全不可访问。

1.公有成员(public):可以通过派生类对象访问。

2.保护成员(protected):可以在派生类内部访问,但不能通过派生类对象直接访问。

3.私有成员(private):完全无法访问。

4.示例:

#include <iostream>
using namespace std;class Animal {
public:void eat() { cout << "Animal eats." << endl; }  // 公有成员
protected:void sleep() { cout << "Animal sleeps." << endl; }  // 保护成员
private:void walk() { cout << "Animal walks." << endl; }  // 私有成员
};class Dog : public Animal {  // 公有继承
public:void dogActions() {eat();   // 可以访问公有成员sleep(); // 可以访问保护成员// walk(); // 错误,不能访问私有成员}
};int main() {Dog d;d.dogActions();return 0;
}
/*输出:
Animal eats.
Animal sleeps.
*/
1.解释:
  • 由于继承方式是 publicDog 类可以访问基类 Animal 的公有成员 eat() 和保护成员 sleep()
  • 但是,Dog 类不能访问基类 Animal 的私有成员 walk()

2.保护继承

        当派生类以 protected 方式继承基类时,基类的公有成员和保护成员都变成保护成员,只能在派生类及其子类中访问,不能通过派生类对象直接访问。

1.公有成员(public)变为保护成员(protected)。

2.保护成员(protected)仍然是保护成员。

3.私有成员(private)完全不可访问。

4.示例:

#include <iostream>
using namespace std;class Animal {
public:void eat() { cout << "Animal eats." << endl; }  // 公有成员
protected:void sleep() { cout << "Animal sleeps." << endl; }  // 保护成员
private:void walk() { cout << "Animal walks." << endl; }  // 私有成员
};class Dog : protected Animal {  // 保护继承
public:void dogActions() {eat();   // 可以访问保护成员sleep(); // 可以访问保护成员// walk(); // 错误,不能访问私有成员}
};int main() {Dog d;d.dogActions();// d.eat(); // 错误,不能通过对象访问 public 成员return 0;
}
/*输出:
Animal eats.
Animal sleeps.
*/
1.解释:
  • 由于继承方式是 protectedDog 类可以访问基类 Animal 的公有成员 eat() 和保护成员 sleep(),但是这些成员不能通过派生类对象直接访问。
  • Dog 类不能访问 Animal 类的私有成员 walk()

3.私有继承

        当派生类以 private 方式继承基类时,基类的公有成员和保护成员都变成私有成员,只能在派生类内部访问,不能通过派生类对象访问。

1.公有成员(public)变为私有成员(private)。

2.保护成员(protected)变为私有成员(private)。

3.私有成员(private)仍然不可访问。

4.示例:

#include <iostream>
using namespace std;class Animal {
public:void eat() { cout << "Animal eats." << endl; }  // 公有成员
protected:void sleep() { cout << "Animal sleeps." << endl; }  // 保护成员
private:void walk() { cout << "Animal walks." << endl; }  // 私有成员
};class Dog : private Animal {  // 私有继承
public:void dogActions() {eat();   // 可以访问私有成员(但只能在类内部访问)sleep(); // 可以访问保护成员(但只能在类内部访问)// walk(); // 错误,不能访问私有成员}
};int main() {Dog d;d.dogActions();//通过派生类的成员函数访问基类的成员函数// d.eat(); // 错误,不能通过对象访问 private 成员return 0;
}
/*输出:
Animal eats.
Animal sleeps.
*/
1.解释:
  • 由于继承方式是 privateDog 类可以访问基类 Animal 的公有成员 eat() 和保护成员 sleep(),但是这些成员都变成了 private,所以不能通过派生类对象直接访问。
  • 由于 eat()sleep()Dog 类的成员函数中被调用,它们可以访问 Animal 类的公有成员和保护成员。
  • 但是,如果尝试从 Dog 类的对象外部调用 eat()sleep()(如 d.eat()d.sleep()),就会报错,因为它们被转换成了私有或保护成员,不能通过外部代码直接访问。但因为Dog类的dogActions()方法是对外公开的,而这个dogActions()方法可以访问到基类的eat()和sleep()方法,因此可以通过Dog类的公有成员方法间接访问到基类的eat和sleep方法。
  • 类外Dog 类不能访问 Animal 类的私有成员 walk()

4. 总结继承的访问控制

继承方式基类 public 成员基类 protected 成员基类 private 成员
public保持 public保持 protected不能访问
protected变为 protected变为 protected不能访问
private变为 private变为 private不能访问

四、 抽象类与纯虚函数

  • 抽象类:一个不能直接实例化的类,通常包含纯虚函数。
  • 纯虚函数:没有实现的函数,要求派生类实现。
  • 示例:展示如何定义抽象类,并解释其在接口设计中的应用。
    class Animal {
    public:virtual void sound() = 0;  // 纯虚函数
    };class Dog : public Animal {
    public:void sound() override { std::cout << "Bark" << std::endl; }
    };
    

关于抽象类与纯虚函数我的这篇笔记里有详细讲,大家可以转站这里


五、多级继承与多重继承

1.多级继承

1. 定义

多级继承是指类的继承关系形成一个层次结构(是逐级进行的,类与类之间有明确的层级关系,每一层只能继承一个父类),其中派生类不仅继承了基类的成员,还继承了另外一个派生类的成员。

具体来说,就是:

  1. 基类(祖父类):最顶层的类。
  2. 派生类(父类):继承自基类的类。
  3. 子类(孙子类):继承自派生类(父类)的类。

在这种情况下,子类(孙子类)不仅继承了派生类(父类)的成员,还间接继承了基类(祖父类)的成员。子孙类的继承就叫多级继承。

例如:

#include<iostream>
using namespace std;class A {  // 基类
public:void showA() { cout << "Class A" << endl; }
};class B : public A {  // 派生类 B 继承自 A
public:void showB() { cout << "Class B" << endl; }
};class C : public B {  // 子类 C 继承自 B(间接继承自 A)
public:void showC() { cout << "Class C" << endl; }
};int main() {C obj;obj.showA();  // 通过 C 访问 A(通过继承的方式)obj.showB();  // 通过 C 访问 Bobj.showC();  // 通过 C 访问 Creturn 0;
}/*输出Class AClass BClass C
*/

在这个例子中:

  • A 是基类。
  • B 是派生类,它直接继承自 A
  • C 是子类,它继承自 B,但间接继承了 A 的成员。
  • C 类继承自 B 类,B 类又继承自 A 类。因此,C 类间接继承了 A 类的成员,这就是“继承链条中,派生类继续继承另一个派生类,形成一个层次结构”。
  • 子类(如 C)不仅可以访问直接继承的父类(如 B)的成员,还可以访问间接继承的祖父类(如 A)的成员。

2. 特性

  • 在多级继承中,继承链条从基类开始,一层一层向下继承。
  • 子类可以直接访问祖先类的公有成员,但需要注意继承的访问权限。例如,C 类可以通过继承链访问 A 类的公有成员。

3. 访问控制

  • 基类成员的访问控制(公有、保护、私有)会影响到继承链条中的派生类对基类成员的访问。
  • 在多级继承中,派生类不仅能访问自己类的成员,还能访问祖先类的公有成员和保护成员。

2.多重继承

1. 定义

多重继承是指一个类可以同时继承多个基类。也就是说,派生类可以同时继承多个父类的成员,具有多个基类。这是 C++ 允许的继承方式。例如:

class A {
public:void showA() { cout << "Class A" << endl; }
};class B {
public:void showB() { cout << "Class B" << endl; }
};class C : public A, public B {  // C 同时继承 A 和 B
public:void showC() { cout << "Class C" << endl; }
};

在这个例子中:

  • C 类继承了 A 类和 B 类,意味着 C 类将拥有 AB 类的成员。、

2. 特性

  • 派生类可以有多个直接的父类。
  • 每个父类的成员都可以被派生类访问。
  • 可能会发生命名冲突,如果多个父类有同名的成员,派生类需要通过作用域解析符来明确指定使用哪个父类的成员。
  • 如果两个基类有相同的成员,可能会出现钻石问题,但可以通过虚拟继承解决。

3. 访问控制

  • 和多级继承类似,多重继承中基类成员的访问控制(公有、保护、私有)依然适用。
  • 如果两个基类有同名的成员(钻石问题),在派生类中需要通过作用域解析符来区分它们。

4. 实例

#include <iostream>
using namespace std;class A {
public:void showA() { cout << "Class A" << endl; }
};class B {
public:void showB() { cout << "Class B" << endl; }
};class C : public A, public B {
public:void showC() { cout << "Class C" << endl; }
};int main() {C obj;obj.showA();  // 通过 C 访问 Aobj.showB();  // 通过 C 访问 Bobj.showC();  // 通过 C 访问 Creturn 0;
}
//输出:Class A Class B Class C

在这个例子中,C 类继承了 AB 两个类,所以它可以直接访问 AB 的公有成员。

5. 潜在的问题(如钻石问题)

多重继承可能导致一些潜在问题,最典型的就是 钻石问题。例如,如果两个基类有相同的成员,派生类可能无法明确继承哪个成员。C++ 通过 虚拟继承 来解决这个问题。

1.钻石问题的例子
#include <iostream>
using namespace std;class A {
public:void showA() { cout << "Class A" << endl; }
};class B : public A {
public:void showB() { cout << "Class B" << endl; }
};class C : public A {
public:void showC() { cout << "Class C" << endl; }
};class D : public B, public C {  // D 同时继承 B 和 C
public:void showD() { cout << "Class D" << endl; }
};int main() {D obj;obj.showA();  // 错误:不明确的继承(钻石问题)return 0;
}

在这个例子中,D 类继承了 BC,而 BC 都继承了 A 类。这样,D 类有两个 A 类的副本,因此不明确的继承会导致编译错误。

2.解决钻石问题:虚拟继承

通过使用虚拟继承(virtual 关键字),C++ 会确保基类 A 只有一个副本。

class A {
public:void showA() { cout << "Class A" << endl; }
};class B : virtual public A {
public:void showB() { cout << "Class B" << endl; }
};class C : virtual public A {
public:void showC() { cout << "Class C" << endl; }
};class D : public B, public C {
public:void showD() { cout << "Class D" << endl; }
};int main() {D obj;obj.showA();  // 现在可以访问 A,因为 A 只有一个副本return 0;
}
//输出:Class A

3.总结

1. 多级继承

  • 一种继承方式,子类继承父类,父类又继承祖父类,形成继承链条。
  • 子类能够访问基类和祖父类的成员(根据访问权限)。

2. 多重继承

  • 一个子类可以继承多个父类。
  • 如果多个父类有同名成员,可能会导致命名冲突(钻石问题),需要使用作用域解析符来明确调用。
  • 可以通过虚拟继承来避免钻石问题。
特性多级继承多重继承
继承关系逐级的,类与类之间形成一个明确的层级关系类同时继承多个父类,父类之间不一定有层级关系
父类数量每一层只有一个父类一个派生类可以有多个直接的父类
结构继承链是单向的,层次化的继承关系是并列的,可以有多个父类
命名冲突不容易发生命名冲突如果父类有相同成员,可能会发生命名冲突
例子class C : public B { ... }class B : public A { ... }class C : public A, public B { ... }

六、虚函数与多态

  • 虚函数:基类中的函数被声明为虚函数时,派生类可以重写该函数,实现运行时多态。
  • 多态:解释静态多态和动态多态的区别。
  • 示例:展示虚函数如何实现动态绑定,通过基类指针或引用调用派生类的函数。
    class Animal {
    public:virtual void sound() { std::cout << "Animal makes a sound" << std::endl; }
    };class Dog : public Animal {
    public:void sound() override { std::cout << "Dog barks" << std::endl; }
    };int main() {Animal* animal = new Dog();animal->sound();  // 动态绑定,调用 Dog 类中的 sound()delete animal;return 0;
    }
    

虚函数的详细笔记 

多态的详细笔记


七、继承中的构造函数与析构函数

1. 构造函数的继承行为

基本规则:
  • 派生类的构造函数会调用基类的构造函数。这意味着在派生类的构造函数中,基类的构造函数会先被调用,然后才会执行派生类的构造代码。
  • 基类的构造函数被自动调用,即使你没有显式调用它。默认情况下,如果基类有无参构造函数,那么它会在派生类构造函数中自动调用。
  • 如果基类有带参构造函数,派生类必须显式调用基类的构造函数(使用初始化列表)。
示例:构造函数继承行为
#include <iostream>
using namespace std;class Base {
public:Base() {  // 无参构造函数cout << "Base class constructor" << endl;}Base(int x) {  // 带参构造函数cout << "Base class constructor with value: " << x << endl;}
};class Derived : public Base {
public:Derived() : Base(10) {  // 显式调用基类的构造函数cout << "Derived class constructor" << endl;}
};int main() {Derived d;  // 创建派生类对象return 0;
}/*输出:
Base class constructor with value: 10
Derived class constructor*/

解释:

  • 当创建派生类对象 d 时,首先会调用基类 Base 的构造函数(带参构造函数 Base(int x)),并传入参数 10,然后才会执行派生类的构造函数。

2. 析构函数的继承行为

基本规则:
  • 派生类的析构函数会先执行。当对象销毁时,析构的顺序是:首先调用派生类的析构函数,然后再调用基类的析构函数。
  • 基类的析构函数应该是虚拟的:
    • 这是为了确保正确地析构派生类对象,避免资源泄漏。当你有一个基类和一个派生类,并且你通过基类指针去删除派生类对象时,如果基类的析构函数不是虚拟的,那么在删除对象时,程序就只会执行基类的析构函数派生类的析构函数就不会执行
    • 这种情况下,派生类对象在销毁时可能会留下未释放的资源(比如动态分配的内存、打开的文件或其他重要的资源),导致资源泄漏,即这些资源被占用但没有被正确释放。
    • 而如果基类的析构函数是虚拟的,程序就知道在删除派生类对象时,先执行派生类的析构函数,释放派生类分配的资源,然后再执行基类的析构函数,释放基类的资源。这样就能确保资源得到完全释放,避免浪费
示例:析构函数继承行为
#include <iostream>
using namespace std;class Base {
public:Base() {cout << "Base class constructor" << endl;}~Base() {  // 基类的析构函数cout << "Base class destructor" << endl;}
};class Derived : public Base {
public:Derived() {cout << "Derived class constructor" << endl;}~Derived() {  // 派生类的析构函数cout << "Derived class destructor" << endl;}
};int main() {Derived d;  // 创建派生类对象return 0;
}/*输出
Base class constructor
Derived class constructor
Derived class destructor
Base class destructor
*/

解释:

  • 创建对象 d 时,首先调用基类的构造函数,然后调用派生类的构造函数。
  • d 被销毁时,先调用派生类的析构函数,再调用基类的析构函数。
虚拟析构函数

在多态情况下,派生类对象是通过基类指针删除的,这时必须将基类的析构函数声明为虚拟的,以确保派生类的析构函数得到调用。否则,可能会导致资源泄漏。

#include <iostream>
using namespace std;class Base {
public:virtual ~Base() {  // 基类的虚拟析构函数cout << "Base class destructor" << endl;}
};class Derived : public Base {
public:~Derived() {  // 派生类的析构函数cout << "Derived class destructor" << endl;}
};int main() {Base* b = new Derived();  // 基类指针指向派生类对象delete b;  // 通过基类指针删除派生类对象return 0;
}/*
Derived class destructor
Base class destructor
*/

解释:

  • delete b 时,基类的虚拟析构函数确保先调用派生类的析构函数,然后再调用基类的析构函数。否则,如果基类的析构函数没有声明为虚拟的,就只会调用基类的析构函数,导致派生类的析构函数没有执行,可能造成资源泄漏。

八、继承与组合的比较

1.组合

1.定义:

组合是表示类之间**“有一个(has-a)”**关系的机制。它通过将一个类的对象作为另一个类的成员来实现功能复用。

2.特点:

  • 组合实现了类与类之间的松散耦合关系。
  • 被组合的对象可以是其他类的实例。

3.组合与继承的区别:

  • 继承:适用于表示 "是一个" 关系的情况。
  • 组合:适用于表示 "有一个" 关系的情况。类之间通过成员对象组合实现功能。

4.示例

#include <iostream>
using namespace std;class Leg {
public:void walk() {cout << "腿在跑。" << endl;}
};class Dog {
private:Leg leg;  // Dog 有一个 Leg
public:void walk() {leg.walk();  // 通过组合对象调用其功能}
};int main() {Dog dog;dog.walk(); // 调用组合对象的方法return 0;
}

2.继承 vs 组合:如何选择?

1.组合与继承的对比

方面继承组合
关系类型是一个(is-a)有一个(has-a)
耦合程度紧密耦合松散耦合
灵活性低(子类依赖父类)高(可以动态修改组合关系)
代码复用子类复用父类代码通过组合成员实现功能复用
使用场景表示类之间是一种“类型”的关系,如动物与狗表示类之间有一个成员的关系,如狗与腿

2. 综合示例

下面是继承和组合的综合使用示例,展示它们的区别和使用场景:

#include <iostream>
using namespace std;// 父类:动物
class Animal {
public:void eat() {cout << "Animal is eating." << endl;}
};// 组合类:腿
class Leg {
public:void walk() {cout << "Leg is walking." << endl;}
};// 子类:狗
class Dog : public Animal { // 继承:狗是动物
private:Leg leg; // 组合:狗有一个腿
public:void walk() {leg.walk(); // 使用组合对象的功能}void bark() {cout << "Dog barks!" << endl;}
};int main() {Dog dog;dog.eat();  // 继承自 Animaldog.walk(); // 组合的功能dog.bark(); // Dog 类的独特功能return 0;
}

3.最后:

  • 在设计复杂系统时,应尽量优先使用组合而非继承,因为组合更加灵活并且降低了类之间的耦合性。
  • 如果需要扩展父类的功能或表示一种“类型”的关系,则继承是合理的选择。

http://www.ppmy.cn/news/1556258.html

相关文章

论文翻译 | ChunkRAG: Novel LLM-Chunk Filtering Method for RAG Systems

摘要 使用大型语言模型&#xff08;LLM&#xff09;的检索-增强生成&#xff08;RAG&#xff09;系统经常由于检索不相关或松散相关的信息而生成不准确的响应。现有的在文档级别操作的方法无法有效地过滤掉此类内容。我们提出了LLM驱动的块过滤&#xff0c;ChunkRAG&#xff0…

apache中的Worker 和 Prefork 之间的区别是什么?

文章目录 内存使用稳定性兼容性适用场景 Apache中的Worker和Prefork两种工作模式在内存使用、稳定性以及兼容性等方面存在区别 内存使用 Worker&#xff1a;由于使用线程&#xff0c;内存占用较少。Prefork&#xff1a;每个进程独立运行&#xff0c;内存消耗较大。 稳定性 W…

Rust关键字实例解析

Rust是一种注重安全性、并发性和性能的系统编程语言。在Rust中&#xff0c;关键字是保留的标识符&#xff0c;用于语言的特定语法结构。这些关键字不能用作普通的标识符&#xff0c;除非使用原始标识符&#xff08;raw identifiers&#xff09;。下面&#xff0c;我们将通过实例…

You need to call SQLitePCL.raw.SetProvider()

在.NET环境中使用Entity Framework Core&#xff08;EF Core&#xff09;连接SQLite数据库时&#xff0c;报错。 使用框架 .NET8 错误信息&#xff1a; Exception: You need to call SQLitePCL.raw.SetProvider(). If you are using a bundle package, this is done by calling…

开疆智能Ethernet/IP转Profinet网关连接纳博特控制器配置案例

该案例是西门子PLC通过开疆智能研发的Ethernet/IP转Profinet网关KJ-PNG-108连接纳博特控制器的配置案例首先下载控制器的EDS文件&#xff0c;解析出其中的ethernet参数. 将EDS文件导入解析软件&#xff0c;透过软件可以看到数据长度默认为32字节&#xff0c;连接点为150/100 打…

同一个局域网下的两台电脑实现定时或者实时拷贝数据

【亲测能用】 需求&#xff1a;从数据库服务器上将数据库备份文件*.bak&#xff0c;每天定时拷贝到局域网下另一台电脑上&#xff0c;实现异机备份。 本文中192.168.1.110是本机&#xff0c;192.168.1.130是异机&#xff08;备份机&#xff09;。需求是每天定时从192.168.1.1…

【JavaWeb后端学习笔记】Redis常用命令以及Java客户端操作Redis

redis 1、redis安装与启动服务2、redis数据类型3、redis常用命令3.1 字符串String3.2 哈希Hash3.3 列表List3.4 集合Set&#xff08;无序&#xff09;3.5 有序集合zset3.6 通用命令 4、使用Java操作Redis4.1 环境准备4.2 Java操作字符串String4.3 Java操作哈希Hash4.4 Java操作…

易语言鼠标轨迹算法(游戏防检测算法)

一.简介 鼠标轨迹算法是一种模拟人类鼠标操作的程序&#xff0c;它能够模拟出自然而真实的鼠标移动路径。 鼠标轨迹算法的底层实现采用C/C语言&#xff0c;原因在于C/C提供了高性能的执行能力和直接访问操作系统底层资源的能力。 鼠标轨迹算法具有以下优势&#xff1a; 模拟…