[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.解释:
- 由于继承方式是
public
,Dog
类可以访问基类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.解释:
- 由于继承方式是
protected
,Dog
类可以访问基类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.解释:
- 由于继承方式是
private
,Dog
类可以访问基类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. 定义
多级继承是指类的继承关系形成一个层次结构(是逐级进行的,类与类之间有明确的层级关系,每一层只能继承一个父类),其中派生类不仅继承了基类的成员,还继承了另外一个派生类的成员。
具体来说,就是:
- 基类(祖父类):最顶层的类。
- 派生类(父类):继承自基类的类。
- 子类(孙子类):继承自派生类(父类)的类。
在这种情况下,子类(孙子类)不仅继承了派生类(父类)的成员,还间接继承了基类(祖父类)的成员。子孙类的继承就叫多级继承。
例如:
#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
类将拥有A
和B
类的成员。、
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
类继承了 A
和 B
两个类,所以它可以直接访问 A
和 B
的公有成员。
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
类继承了 B
和 C
,而 B
和 C
都继承了 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)!