[C++]类的继承

一、什么是继承

1.定义

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

如何理解"是一个"?

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

2.继承的作用

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


二、基类与派生类

1.基类

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

2.派生类

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

3.简单示例

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

cpp 复制代码
#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.示例:

cpp 复制代码
#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.示例:

cpp 复制代码
#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.示例:

cpp 复制代码
#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 不能访问

四、 抽象类与纯虚函数

  • 抽象类:一个不能直接实例化的类,通常包含纯虚函数。

  • 纯虚函数:没有实现的函数,要求派生类实现。

  • 示例 :展示如何定义抽象类,并解释其在接口设计中的应用。

    cpp 复制代码
    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. 子类(孙子类):继承自派生类(父类)的类。

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

例如:

cpp 复制代码
#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 访问 B
    obj.showC();  // 通过 C 访问 C
    return 0;
}

/*输出
    Class A
    Class B
    Class C
*/

在这个例子中:

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

2. 特性

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

3. 访问控制

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

2.多重继承

1. 定义

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

cpp 复制代码
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. 实例

cpp 复制代码
#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 访问 A
    obj.showB();  // 通过 C 访问 B
    obj.showC();  // 通过 C 访问 C
    return 0;
}
//输出:Class A Class B Class C

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

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

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

1.钻石问题的例子
cpp 复制代码
#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 只有一个副本。

cpp 复制代码
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 { ... }

六、虚函数与多态

  • 虚函数:基类中的函数被声明为虚函数时,派生类可以重写该函数,实现运行时多态。

  • 多态:解释静态多态和动态多态的区别。

  • 示例 :展示虚函数如何实现动态绑定,通过基类指针或引用调用派生类的函数。

    cpp 复制代码
    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. 构造函数的继承行为

基本规则:
  • 派生类的构造函数会调用基类的构造函数。这意味着在派生类的构造函数中,基类的构造函数会先被调用,然后才会执行派生类的构造代码。
  • 基类的构造函数被自动调用,即使你没有显式调用它。默认情况下,如果基类有无参构造函数,那么它会在派生类构造函数中自动调用。
  • 如果基类有带参构造函数,派生类必须显式调用基类的构造函数(使用初始化列表)。
示例:构造函数继承行为
cpp 复制代码
#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. 析构函数的继承行为

基本规则:
  • 派生类的析构函数会先执行。当对象销毁时,析构的顺序是:首先调用派生类的析构函数,然后再调用基类的析构函数。
  • 基类的析构函数应该是虚拟的:
    • 这是为了确保正确地析构派生类对象,避免资源泄漏。当你有一个基类和一个派生类,并且你通过基类指针去删除派生类对象时,如果基类的析构函数不是虚拟的 ,那么在删除对象时,程序就只会执行基类的析构函数派生类的析构函数就不会执行
    • 这种情况下,派生类对象在销毁时可能会留下未释放的资源(比如动态分配的内存、打开的文件或其他重要的资源),导致资源泄漏,即这些资源被占用但没有被正确释放。
    • 而如果基类的析构函数是虚拟的 ,程序就知道在删除派生类对象时,先执行派生类的析构函数,释放派生类分配的资源 ,然后再执行基类的析构函数,释放基类的资源。这样就能确保资源得到完全释放,避免浪费
示例:析构函数继承行为
cpp 复制代码
#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 被销毁时,先调用派生类的析构函数,再调用基类的析构函数。
虚拟析构函数

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

cpp 复制代码
#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.示例

cpp 复制代码
#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. 综合示例

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

cpp 复制代码
#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();  // 继承自 Animal
    dog.walk(); // 组合的功能
    dog.bark(); // Dog 类的独特功能
    return 0;
}

3.最后:

  • 在设计复杂系统时,应尽量优先使用组合而非继承,因为组合更加灵活并且降低了类之间的耦合性。
  • 如果需要扩展父类的功能或表示一种"类型"的关系,则继承是合理的选择。
相关推荐
逆袭之路6662 分钟前
c++死锁调试 ,gdb pstack
c++
gb42152876 分钟前
java中sha256和md5某个字符串实例代码
java·开发语言·哈希算法
十年一梦实验室26 分钟前
【C++】sophus : test_macros.hpp 用于单元测试的宏和辅助函数 (四)
开发语言·c++·单元测试
Irises`35 分钟前
flex自适应页面
开发语言·前端·javascript
不想当程序猿_40 分钟前
【蓝桥杯每日一题】扫描游戏——线段树
c++·算法·蓝桥杯·线段树·模拟
老猿讲编程1 小时前
在 macOS 下安装和使用 Clang Static Analyzer
c++·macos
明月看潮生1 小时前
青少年编程与数学 02-004 Go语言Web编程 01课题、Web应用程序
开发语言·青少年编程·编程与数学·goweb
cwtlw1 小时前
SpringMVC的使用
java·开发语言·笔记·学习·其他
Jambo!1 小时前
Visual studio中C/C++连接mysql
c语言·c++·mysql·visual studio
Mr.131 小时前
如何实现对象的克隆?如何实现单例模式?
c++·单例模式