自学内容网 自学内容网

[C++]类的继承

一、什么是继承

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 访问 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++ 允许的继承方式。例如:

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 访问 A
    obj.showB();  // 通过 C 访问 B
    obj.showC();  // 通过 C 访问 C
    return 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();  // 继承自 Animal
    dog.walk(); // 组合的功能
    dog.bark(); // Dog 类的独特功能
    return 0;
}

3.最后:

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

原文地址:https://blog.csdn.net/2302_80281315/article/details/144332632

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!