当前位置: 首页 > news >正文

[C++] 深入理解面向对象编程特性 : 继承

Kevin的技术博客.png

文章目录

  • 继承的概念与定义
    • 继承的定义
      • 定义格式
      • 不同继承方式与继承的基类中访问限定符间的影响
      • C++中的继承和访问控制总结
        • 父类的`private`成员在子类中的访问限制
        • `protected`成员的使用场景
        • 成员访问方式总结
        • 继承方式的默认值
        • 实际应用中的继承方式
      • 示例代码
    • OOP中类之间的关系
      • “is a” 关系
      • “has a” 关系
    • 类模板的继承
      • 类模板继承的基本语法
      • 访问控制和作用域解析
      • 名称查找和依赖名称
        • 名称查找和作用域解析示例
  • 父类和子类对象赋值兼容转换
    • 子类对象可以赋值给父类对象、父类指针或父类引用
    • 父类对象不能赋值给子类对象
    • 父类的指针或引用可以通过强制类型转换赋值给子类的指针或引用
      • 安全的类型转换
      • 强制类型转换
    • 总结
  • 继承中的作⽤域
    • 隐藏规则
    • 作⽤域相关知识考察
  • ⼦类的默认成员函数
    • 子类的构造函数
    • 子类的拷贝构造函数
    • 子类的赋值运算符
    • 子类的析构函数
    • 子类的赋值运算符重载
    • 不能被继承的类
  • 继承:友元&静态成员
    • 继承与友元
    • 继承与静态成员
  • 多继承与菱形继承
    • 继承模型
      • 单继承
      • 多继承
        • 多继承中指针偏移问题:
      • 菱形继承
    • 虚继承
      • 虚继承的原理
      • 虚继承的内存分布
      • 注意事项
  • 继承和组合
    • 继承(Inheritance)
    • 组合(Composition)
    • 继承与组合的比较
    • 继承与组合的使用原则
    • 实例分析
      • 示例 1:组合(has-a 关系)
      • 示例 2:继承(is-a 关系)
    • 组合与继承的实际应用
    • 综合示例


继承的概念与定义

面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类
cpp-inheritance-2020-12-15-1.png

继承的定义

定义格式

继承格式:class derived-class: access-specifier base-class

Person是⽗类,也称作基类。Student是⼦类,也称作派⽣类 :
image.png

不同继承方式与继承的基类中访问限定符间的影响

  • 类的继承有三种类型:公有继承(public)、保护继承(protected)和私有继承(private)。
  • C++中的访问限定符有publicprotectedprivate,它们分别控制成员的可访问性。

具体的继承后访问权限如下:

类成员/继承方式public继承protected继承private继承
父类的public成员子类的public成员子类的protected成员子类的private成员
父类的protected成员子类的protected成员子类的protected成员子类的private成员
父类的private成员在子类中不可见在子类中不可见在子类中不可见

C++中的继承和访问控制总结

父类的private成员在子类中的访问限制

父类的private成员在子类中是不可见的。这意味着,虽然子类对象中仍然包含父类的private成员,但语法上子类无法访问这些成员,无论是在子类的内部还是外部。

protected成员的使用场景

父类的private成员在子类中不能被访问。如果需要父类成员在类外不能直接访问,但在子类中能够访问,那么应该将这些成员定义为protectedprotected成员限定符主要是为了解决继承中的访问控制问题而出现的。

成员访问方式总结

通过继承方式和父类成员的访问限定符,可以总结出父类的其他成员在子类中的访问方式:

  • public > protected > private

子类对父类成员的访问权限是取父类成员的访问限定符与继承方式的最小值。

继承方式的默认值

在使用关键字class时,默认的继承方式是private。而使用关键字struct时,默认的继承方式是public。尽管如此,最好显式地写出继承方式以提高代码的可读性。

class Base {
private:int privateMember;
protected:int protectedMember;
public:int publicMember;
};class Derived : public Base {// 继承方式为public,访问权限如下:// privateMember:不可见// protectedMember:protected// publicMember:public
};
实际应用中的继承方式

在实际应用中,通常使用public继承,很少使用protectedprivate继承。原因在于protectedprivate继承的成员只能在子类内部使用,限制了代码的扩展性和可维护性。

示例代码

class Base {
private:int privateMember;
protected:int protectedMember;
public:int publicMember;
};class PublicDerived : public Base {// privateMember:不可见// protectedMember:protected// publicMember:public
};class ProtectedDerived : protected Base {// privateMember:不可见// protectedMember:protected// publicMember:protected
};class PrivateDerived : private Base {// privateMember:不可见// protectedMember:private// publicMember:private
};

OOP中类之间的关系

“is a” 关系

“is a”关系:通过继承(Inheritance)来表示,表示类之间的层次关系。

“is a”关系通常表示继承(Inheritance)关系,也就是一个类是另一个类的特殊类型。比如,狗(Dog)是动物(Animal)的一种,我们可以通过继承来表示这种关系:

class Animal {
public:void makeSound() {std::cout << "Animal sound" << std::endl;}
};class Dog : public Animal { // Dog is an Animal
public:void makeSound() {std::cout << "Bark" << std::endl;}
};

在这个例子中,Dog类继承自Animal类,这表明“狗是一种动物”(Dog is an Animal)。Dog类可以访问Animal类中的公共成员函数和变量。

“has a” 关系

“has a”关系:通过组合(Composition)或聚合(Aggregation)来表示,表示一个类拥有另一个类的实例。

“has a”关系通常表示组合(Composition)或聚合(Aggregation)关系,即一个类包含另一个类作为其成员。这种关系强调一个类拥有另一个类的实例。比如,汽车(Car)有一个引擎(Engine),可以用组合来表示这种关系:

class Engine {
public:void start() {std::cout << "Engine starts" << std::endl;}
};class Car { // Car has an Engine
private:Engine engine;public:void startCar() {engine.start();std::cout << "Car starts" << std::endl;}
};

在这个例子中,Car类包含一个Engine类的实例,这表明“汽车有一个引擎”(Car has an Engine)。Car类可以使用Engine类中的方法来实现其功能。

类模板的继承

类模板继承的基本语法

template<class T>
class Base {// 基类内容
};template<class T>
class Derived : public Base<T> {// 派生类内容
};

访问控制和作用域解析

  • 访问控制:继承时,基类的成员的访问权限在派生类中依旧遵循C++的访问控制规则,即publicprotectedprivate
  • 作用域解析:在派生类中访问基类的成员时,需要使用作用域解析符来明确调用基类的成员:
template<class T>
class Derived : public Base<T> {
public:void foo() {Base<T>::bar(); // 调用基类的bar函数}
};

名称查找和依赖名称

名称查找与依赖名称的问题主要源于模板的按需实例化机制和两阶段名称查找机制

两阶段名称查找
C++编译器对模板代码进行两次名称查找:

  1. 第一次名称查找:在模板定义时进行。编译器解析所有与模板参数无关的非依赖名称。
  2. 第二次名称查找:在模板实例化时进行。编译器解析依赖于模板参数的名称,即依赖名称。

依赖名称(Dependent Names)是指那些依赖于模板参数的名称。在第一次名称查找时,编译器无法确定这些名称的具体含义,只有在模板实例化时才能解析。

名称查找和作用域解析示例
template <typename T>
class Base {
public:void foo() {std::cout << "Base foo" << std::endl;}
};template <typename T>
class Derived : public Base<T> {
public:void bar() {// 问题点:编译器在第一次名称查找时不知道foo()是从Base<T>继承的// 因为foo()是依赖于模板参数T的名称// foo(); // 这会导致编译错误// 解决方法1:使用this指针this->foo();// 解决方法2:使用作用域解析符Base<T>::foo();}
};int main() {Derived<int> d;d.bar(); // 输出 "Base foo"return 0;
}

编译器会在第一次名称查找时尝试解析foo()。但是由于foo()是依赖于模板参数T的成员函数,编译器无法确定foo()是从基类继承的。这是因为模板是按需实例化的,编译器在第一次查找时并不知道派生类实例化时会包含哪些基类成员。
在使用Derived<int> d;初始化的时候会对构造函数进行实例化并调用构造函数,但是当使用d.bar();时,如果在bar()中为foo();即会编译错误,原因就如上述,无法确定从基类继承。
所以解决如下:

  1. 使用**this**指针
void bar() {this->foo(); // 正确
}

编译器会在第二阶段名称查找时解析foo(),并正确地找到基类中的foo()成员函数。这是因为this指针在类定义中总是已知的,并且它指向当前对象**(包括从基类继承的部分)**。

  1. 使用作用域解析符
void bar() {Base<T>::foo(); // 正确
}

Base<T>::foo()明确指出了foo()来自基类Base<T>,消除了编译器的名称查找歧义。

父类和子类对象赋值兼容转换

子类对象可以赋值给父类对象、父类指针或父类引用

在公有继承中,子类对象可以赋值给父类对象、父类指针或父类引用(把⼦类中⽗类那部分切来赋值过去)。这种转换称为向上转换(upcasting)。

class Base {
public:void baseMethod() {std::cout << "Base method" << std::endl;}
};class Derived : public Base {
public:void derivedMethod() {std::cout << "Derived method" << std::endl;}
};int main() {Derived derivedObj;Base baseObj = derivedObj;  // 子类对象赋值给父类对象Base* basePtr = &derivedObj;  // 子类对象的地址赋值给父类指针Base& baseRef = derivedObj;  // 子类对象赋值给父类引用baseObj.baseMethod();  // 可以调用父类的方法basePtr->baseMethod();  // 可以通过父类指针调用父类的方法baseRef.baseMethod();  // 可以通过父类引用调用父类的方法// 以下调用都会导致编译错误,因为父类对象/指针/引用不能访问子类特有的方法// baseObj.derivedMethod();// basePtr->derivedMethod();// baseRef.derivedMethod();return 0;
}

image.png

父类对象不能赋值给子类对象

父类对象不能赋值给子类对象,因为父类对象可能不包含子类对象所需的所有信息。这种转换会导致子类特有的数据丢失或变得不确定。

Base baseObj;
Derived derivedObj;// 以下赋值会导致编译错误
// derivedObj = baseObj;

父类的指针或引用可以通过强制类型转换赋值给子类的指针或引用

父类的指针或引用可以通过强制类型转换赋值给子类的指针或引用,但必须确保父类的指针实际上指向一个子类对象。这种转换称为向下转换(downcasting)

安全的类型转换

如果父类是多态类型,可以使用RTTI(运行时类型信息)中的dynamic_cast来进行安全转换。

Base* basePtr = new Derived;Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr) {derivedPtr->derivedMethod();  // 安全转换后可以调用子类方法
} else {// 转换失败,basePtr并不指向Derived对象
}

强制类型转换

虽然可以使用static_cast进行强制转换,但这种转换在父类指针不指向子类对象时是危险的。

Base* basePtr = new Derived;Derived* derivedPtr = static_cast<Derived*>(basePtr);
derivedPtr->derivedMethod();  // 需要确保basePtr实际指向Derived对象

总结

  • 子类对象可以赋值给父类对象、父类指针或父类引用,称为向上转换(upcasting),但会发生对象切片(slicing)。
  • 父类对象不能赋值给子类对象,因为父类对象缺乏子类特有的信息。
  • 父类指针或引用可以赋值给子类指针或引用,但必须确保指向实际的子类对象。可以使用dynamic_cast进行安全转换。

继承中的作⽤域

隐藏规则

  1. 在继承体系中⽗类和⼦类都有独⽴的作⽤域。
  2. ⼦类和⽗类中有同名成员,⼦类成员将屏蔽⽗类对同名成员的直接访问,这种情况叫隐藏。(在⼦
    类成员函数中,可以使⽤⽗类::⽗类成员显式访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。

作⽤域相关知识考察

#include <iostream> // 不要忘记包含 iostream 头文件以使用 coutclass A {
public:void fun() {std::cout << "func()" << std::endl;}
};class B : public A {
public:void fun(int i) {std::cout << "func(int i)" << i << std::endl;}
};int main() {B b;b.fun(10); // 调用 B 类的 fun(int i)b.fun();   // 尝试调用 A 类的 fun(),但由于重载,实际上调用的是 B 类的 fun(int i) return 0;
}
  1. A和B类中的两个func构成什么关系?
  • 此时的A和B类构成的是隐藏的关系。
  1. 编译运⾏结果是什么?
  • 编译报错。(b.fun();

⼦类的默认成员函数

子类的构造函数

子类的构造函数必须调用父类的构造函数来初始化父类的那部分成员。如果父类没有默认构造函数,则必须在子类构造函数的初始化列表中显式调用父类的构造函数。

Student(const char* name, int num): Person(name), _num(num) {cout << "Student()" << endl;
}

在初始化列表中可以注意初始化顺序,先声明的先初始化,所以先声明的父类会先定义。

子类的拷贝构造函数

子类的拷贝构造函数必须调用父类的拷贝构造函数来完成父类部分的拷贝初始化。

Student(const Student& s): Person(s), _num(s._num) {cout << "Student(const Student& s)" << endl;
}

子类的赋值运算符

子类的赋值运算符必须调用父类的赋值运算符来完成父类部分的复制。需要注意的是,子类的赋值运算符会隐藏父类的赋值运算符,所以需要显式调用父类的赋值运算符。

Student& operator=(const Student& s) {cout << "Student& operator=(const Student& s)" << endl;if (this != &s) {// 构成隐藏,所以需要显式调用Person::operator=(s);_num = s._num;}return *this;
}

子类的析构函数

不用再子类析构函数中显式调用父类的析构函数,子类的析构函数在被调用完成后,会自动调用父类的析构函数来清理父类成员。这样可以保证子类对象先清理子类成员再清理父类成员的顺序。

~Student() {cout << "~Student()" << endl;
}

析构会按照后定义的先析,先调用子类析构,再调用父类析构。

多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以⽗类析构函数不加virtual的情况下,⼦类析构函数和⽗类析构函数构成隐藏关系。

子类的赋值运算符重载

⼦类的operator=必须要调⽤⽗类的operator=完成⽗类的复制。需要注意的是⼦类的operator=隐
藏了⽗类的operator=,所以显⽰调⽤⽗类的operator=,需要指定⽗类作⽤域

student& operator=(const student& s)
{if (this != &s){person::operator=(s);}
}

不能被继承的类

有两种方法可以使类不可以被继承:

  1. ⽗类的构造函数私有,⼦类的构成必须调⽤⽗类的构造函数,但是⽗类的构成函数私有化以后,⼦类看不⻅就不能调⽤了,那么⼦类就⽆法实例化出对象。
  2. C++11新增了⼀个final关键字,final修改⽗类,⼦类就不能继承了。
class Base final
{
public:
void func5() { cout << "Base::func5" << endl; }protected:
int a = 1;private:
// C++98的⽅法
/*Base(){}*/
}

继承:友元&静态成员

继承与友元

友元关系不继承:
在C++中,友元关系是特定于某个类的。一个函数或类如果是父类的友元,它不会自动成为子类的友元。因此,父类的友元函数不能访问子类的私有成员和保护成员。同样地,如果你希望某个函数既是父类的友元,又是子类的友元,也可以在子类中声明该友元函数。

class Student;class Person {
public:friend void Display(const Person& p, const Student& s); // 声明友元函数
protected:string _name; // 姓名
};class Student : public Person {
protected:int _stuNum; // 学号
};void Display(const Person& p, const Student& s) {cout << p._name << endl;cout << s._stuNum << endl;  // 尝试访问子类的保护成员,编译错误
}int main() {Person p;Student s;Display(p, s);  // 编译报错:error C2248: “Student::_stuNum”: 无法访问 protected 成员return 0;
}

Display函数是Person类的友元,因此它可以访问Person类的保护成员 _name。但是,当它尝试访问Student类的保护成员_stuNum时,会产生编译错误。原因是友元关系不继承:Display函数虽然是Person的友元,但它不是Student的友元,所以不能访问Student的保护成员。
Display在子类中声明即可解决该问题:

class Student : public Person {
public:friend void Display(const Person& p, const Student& s); // 友元函数也要声明在子类中
protected:int _stuNum; // 学号
};

这样,Display函数就能同时访问PersonStudent的保护成员了。

继承与静态成员

在C++中,静态成员是属于类而不是某个特定对象的。⽗类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员,这意味着即使类派生出了多个子类,它们都共享同一个静态成员实例。

class Person {
public:string _name;static int _count;
};int Person::_count = 0; // 静态成员初始化class Student : public Person {
protected:int _stuNum;
};int main() {Person p;Student s;// 非静态成员_name地址不同,说明子类继承后,父子类对象各有一份cout << &p._name << endl;cout << &s._name << endl;// 静态成员_count地址相同,说明子类和父类共用同一个静态成员cout << &p._count << endl;cout << &s._count << endl;// 公有的情况下,父子类都可以访问静态成员cout << Person::_count << endl;cout << Student::_count << endl;return 0;
}

运行结果:

0133FDE4
0133FDBC
0014E478
0014E478
0
0
  • _name是一个非静态成员,在PersonStudent对象中分别有独立的实例,所以它们的地址不同。
  • _count是一个静态成员,PersonStudent共享同一个静态成员实例,因此它们的地址相同。
  • 无论是通过父类还是子类,都可以访问静态成员。

多继承与菱形继承

继承模型

单继承

单继承是指一个子类只有一个直接父类。在这种情况下,子类继承父类的所有非私有成员,继承结构简单明了,访问成员变量也不存在歧义问题。

多继承

多继承是指一个子类有多个直接父类。C++支持多继承,这意味着一个子类可以从多个父类继承成员。在多继承中,C++规定在内存布局上,先继承的父类放在前面,后继承的父类放在后面,子类自己的成员放在最后。

class Person {
public:string _name; // 姓名
};class Student : public Person {
protected:int _num; // 学号
};class Teacher : public Person {
protected:int _id; // 职工编号
};class Assistant : public Student, public Teacher {
protected:string _majorCourse; // 主修课程
};
多继承中指针偏移问题:

问题:下⾯说法正确的是()
A:p1p2p3 B:p1<p2<p3
C:p1==p3!=p2 D:p1!=p2!=p3

class Base2 { public: int _b2; };
class Base1 { public: int _b1; };
class Derive : public Base1, public Base2 { public: int _d; };int main()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

image.png
继承的时候会按照生命顺序来进行分配空间,也就是继承顺序。上述例子中先继承的是Base1,后继承的是Base2,所以按照规则栈会先为继承的Base1的信息进行开辟空间(栈向下开辟空间),然后再为Base2开辟空间,所以空间图如上图所示。

Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;

以上是用了继承中基类对于派生类的向上转换(会进行类似切片操作,详见上文),所以此时的指向为下图:
image.png
此时的p1p3指向的是同一块地址,p2指向的之后分配的继承了Base2的空间。

正确答案为:p1 == p3 != p2

菱形继承

菱形继承是多继承中的一种特殊情况,发生在一个子类通过两个不同的路径继承自同一个基类时,形成菱形结构。

这种继承方式会带来数据冗余访问二义性的问题。

class Person {
public:string _name; // 姓名
};class Student : public Person {
protected:int _num; // 学号
};class Teacher : public Person {
protected:int _id; // 职工编号
};class Assistant : public Student, public Teacher {
protected:string _majorCourse; // 主修课程
};int main() {Assistant a;a._name = "peter"; // 编译报错:error C2385: 对“_name”的访问不明确a.Student::_name = "xxx"; // 需要显式指定访问哪个父类的成员a.Teacher::_name = "yyy"; // 但是数据冗余问题无法解决return 0;
}
  • 数据冗余:在Assistant类中,由于StudentTeacher都继承了Person,所以Assistant中会有两份Person的拷贝。换句话说,Assistant类中有两份_name成员,这样会导致内存上的浪费。
  • 访问二义性:当你在Assistant类中访问_name时,编译器无法确定你想访问的是从Student继承过来的_name,还是从Teacher继承过来的_name,因此会报错。
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";

可以通过显式的指定访问的是哪个父类的成员,或者使用虚继承,即可解决当前问题。

不推荐使用菱形继承

虚继承

虚继承(virtual inheritance)是C++中的一种特殊继承机制,用来解决多继承中的菱形继承问题,特别是避免数据冗余和访问二义性。
在多继承中,如果一个子类通过不同的路径从同一个基类继承,那么就会形成菱形继承。菱形继承会导致子类中存在多个基类实例,从而产生数据冗余和访问二义性的问题。虚继承通过修改基类在继承链中的存储方式,使得即使存在多重继承,所有子类中只会存在一个基类的实例,从而避免数据冗余和访问二义性。

class Person {
public:string _name; // 姓名
};// 使用虚继承Person类
class Student : virtual public Person {
protected:int _num; // 学号
};// 使用虚继承Person类
class Teacher : virtual public Person {
protected:int _id; // 职工编号
};class Assistant : public Student, public Teacher {
protected:string _majorCourse; // 主修课程
};int main() {// 使用虚继承,可以解决数据冗余和二义性Assistant a;a._name = "peter";return 0;
}

虚继承的原理

当一个类通过virtual关键字虚继承一个基类时,编译器确保在多继承链中该基类只会有一个实例。在上述示例中,StudentTeacher都虚继承自Person,因此在Assistant类中,Person的实例只会有一个。

虚继承的内存分布

在普通继承中,每个子类都会在其对象中包含父类的成员。但在虚继承中,编译器通过在子类中存储一个指向基类的指针来避免冗余。这个指针指向了唯一的基类实例,确保整个继承体系中只存在一个基类实例。

注意事项

  • 构造函数调用顺序:因为虚继承之后只存在一个实例,所以当使用虚继承时,基类的构造函数在最派生类(如Assistant)的构造函数中被调用,而不是在虚继承的直接派生类(如StudentTeacher)中。派生类的构造函数负责初始化基类的那部分。
class Assistant : public Student, public Teacher {
public:Assistant(const string& name) : Person(name), Student(), Teacher() {}
};

在这个例子中,由于Person是通过虚继承的,所以必须在Assistant的构造函数中显式地调用Person的构造函数来初始化_name

  • 虚继承的时候注意:进行虚继承的是那个产生数据冗余和二义性的公共基类的子类。

image.png

继承和组合

继承(Inheritance)和组合(Composition)是面向对象编程中两种重要的代码复用手段。它们在实际开发中各有优势和适用场景。

继承(Inheritance)

继承是一种is-a关系,表示子类是父类的一种特殊类型。通过继承,子类可以复用父类的属性和方法。
特点:

  • 代码复用:子类自动继承父类的所有成员变量和成员函数。
  • 多态性:子类可以重写父类的虚函数,提供不同的实现。
  • 强耦合:子类与父类之间有很强的依赖关系,父类的修改可能影响到所有子类。
class Car {
public:void Start() {cout << "Car starts." << endl;}
};class BMW : public Car {
public:void Drive() {cout << "BMW drives fast." << endl;}
};

在上面的代码中,BMW类继承了Car类,所以BMW类可以直接使用Car类中的Start方法。

组合(Composition)

组合是一种has-a关系,表示一个类拥有另一个类的实例。这种方式通过将一个对象作为另一个对象的成员变量来实现代码复用。
组合的特点:

  • 松耦合:组合关系中的类是独立的,一个类的修改不会影响到其他类。
  • 黑箱复用:组合对象的内部实现对外部不可见,只暴露必要的接口。
  • 灵活性:通过组合,可以动态地创建更复杂的对象结构。
class Engine {
public:void Start() {cout << "Engine starts." << endl;}
};class Car {
private:Engine engine; // Car has an Engine
public:void Start() {engine.Start();cout << "Car starts." << endl;}
};

在上面的代码中,Car类包含了一个Engine类的实例,Car类通过组合来复用Engine类的功能。

继承与组合的比较

  • 复用性:继承可以直接复用父类的实现,组合则通过使用已有类的实例来复用功能。
  • 耦合度:继承会导致子类与父类的紧密耦合,组合则保持类之间的独立性。
  • 可维护性:由于继承的强耦合性,父类的修改可能影响子类,从而降低了代码的可维护性。组合则更容易维护,因为它遵循单一职责原则,每个类只负责自己的部分。
  • 扩展性:组合更容易扩展,因为可以通过组合不同的类来创建新的功能,而继承则在层次结构上有更多的限制。

继承与组合的使用原则

  • 优先使用组合:在设计类结构时,优先考虑使用组合,因为它可以减少耦合,提高代码的灵活性和可维护性。
  • 适当使用继承:当子类确实是父类的一种类型(即符合is-a关系)时,可以考虑使用继承。继承的优势在于实现多态性,但过度使用继承可能导致复杂的继承层次结构和高耦合。

实例分析

示例 1:组合(has-a 关系)

class Tire {
protected:string _brand = "Michelin"; // 品牌size_t _size = 17; // 尺寸
};class Car {
protected:string _colour = "白色"; // 颜色string _num = "陕ABIT00"; // 车牌号Tire _t1, _t2, _t3, _t4; // 轮胎组合
};

在这里,Car类通过组合了四个Tire类的实例来实现车轮的功能,这就是一个典型的has-a关系。

示例 2:继承(is-a 关系)

class Car {
protected:string _colour = "白色"; // 颜色string _num = "陕ABIT00"; // 车牌号
};class BMW : public Car {
public:void Drive() { cout << "好开-操控" << endl; }
};class Benz : public Car {
public:void Drive() { cout << "好坐-舒适" << endl; }
};

组合与继承的实际应用

  • 继承主要用于需要复用父类的代码或实现多态性的时候。
  • 组合主要用于需要动态组合功能、减少类之间的耦合以及增强代码的灵活性时。

综合示例

在一些场景中,组合和继承可能会混合使用,例如在一个stack类中,既可以使用组合来包含一个vector对象,也可以通过继承来扩展vector类的功能。
**继承方式: **

template<class T>
class stack : public vector<T> {// stack继承自vector
};

**组合方式: **

template<class T>
class stack {
public:vector<T> _v; // 通过组合方式来包含一个vector对象
};

在实际设计时,建议优先考虑组合,这样可以保持类的封装性和独立性,从而提高代码的可维护性。

image.png

相关文章:

[C++] 深入理解面向对象编程特性 : 继承

文章目录 继承的概念与定义继承的定义定义格式不同继承方式与继承的基类中访问限定符间的影响C中的继承和访问控制总结父类的private成员在子类中的访问限制protected成员的使用场景成员访问方式总结继承方式的默认值实际应用中的继承方式 示例代码 OOP中类之间的关系“is a” …...

汇昌联信科技做拼多多电商怎么引流?

在互联网经济高速发展的今天&#xff0c;电商平台如雨后春笋般涌现&#xff0c;其中拼多多以其独特的社交电商模式迅速崛起。对于汇昌联信科技而言&#xff0c;如何在拼多多平台上有效引流&#xff0c;成为提升销量和品牌知名度的关键。本文将深入探讨汇昌联信科技在拼多多电商…...

公网ip和私网ip的区别

1.接入方式不同\n公网IP以公网连接Internet上的非保留地址&#xff0c;私网IP则是局域网上的IP&#xff0c;通过NAT才能够与公网进行通信。 2.特点不同\n公网IP由国际互联网络信息中心InterNIC负责,将IP地址分配给注册并向InterNIC提出申请的机构或组织。私网IP则是为节省可分…...

【开发踩坑】windows查看jvm gc信息

windows查看jvm gc信息 EZ 找出java进程PID 控制面板----搜索任务管理器---- 任务管理器----搜索 java----详细信息 这里PID是4856 cmd jstat gc面板 reference&#xff1a; jstat命令...

时间序列预测 | CEEMDAN+CNN+Transformer多变量时间序列预测(Python)

目录 效果一览基本介绍程序设计参考资料 效果一览 基本介绍 时间序列预测 | CEEMDANCNNTransformer多变量时间序列预测&#xff08;Python&#xff09; 时间序列预测 创新点 多尺度特征提取&#xff1a;CEEMDAN将复杂的时间序列分解成多个IMFs&#xff0c;使得CNN和Transforme…...

vue3--实现vue2插件JSONPathPicker的路径获取功能

背景 最近在进行vue2项目升级为vue3。 在项目中需要获取json某些字段的路径&#xff0c;vue2中使用JSONPathPicker &#xff0c;但是该插件不支持vue3&#xff0c;vue3中也没有相应的模块有该功能。 实现目标&#xff1a; 原vue2中JSONPathPicker实现的功能&#xff1a; 查…...

SuccBI+低代码文档中心 — 可视化分析(仪表板)(上)

有关仪表板的设计器&#xff1a; 查询设置 由于仪表板的设计器是所见即所得的&#xff0c;可以将当前制作的内容和数据的查询结果实时展示在界面中&#xff0c;当引入到仪表板的模型数据量较大时&#xff0c;为了提高设计器界面的查询性能&#xff0c;提供了以下两种方法&…...

P3156 【深基15.例1】询问学号

昨天我发布了关于数据结构线性表的学习知识&#xff08;【数据结构】顺序表-CSDN博客&#xff09;。所谓“纸上得来终觉浅”&#xff0c;光看不练可不行&#xff0c;下面我们来看一下顺序表的习题。 题目链接 【深基15.例1】询问学号 - 洛谷 题目解读 题目描述了一个场景&…...

详解Xilinx FPGA高速串行收发器GTX/GTP(5)--详解8B10B编解码

目录 1、8B/10B编码是什么? 2、8B/10B编码的规则 3、两个例子 4、GTX的8B/10B编码 5、Verilog实现 文章总目录点这里:《FPGA接口与协议》专栏的说明与导航 1、8B/10B编码是什么? 简单来说,8B/10B编码就是将原本是8bits的数据,按照一定的规则扩展编码到10b…...

python 画多盘的写放大曲线方法

在服务器测试中我们经常会遇见客户要求画出每个SSD的WAF曲线&#xff0c;也就是写放大&#xff0c;通常的做法就是我们每隔10分钟记录一下每个SSD的host写入量和nand写入量&#xff0c;下面我们介绍一下python处理多盘的WAF的做法 如图所示 假设这是一个记录多盘的写入量信息的…...

计算机网络TCP/UDP知识点

这是一些在学习过程中关于计算机网络八股文的一些知识点记录&#xff1a; TCP/UDP TCP怎么保证可靠性 1.序列号&#xff0c;确认应答&#xff0c;超时重传 数据到达接收方&#xff0c;接收方需要发出一个确认应答&#xff0c;表示已经收到该数据段&#xff0c;并且确认序号…...

JavaScript 文档元素获取

目录 通过id获取文档元素 任务描述 相关知识 什么是DOM 文档元素 节点树 通过id获取文档元素 编程要求 通过类名获取文档元素 任务描述 相关知识 通过类名获取文档元素 编程要求 通过标签名获取文档元素 任务描述 相关知识 通过标签的名字获取文档元素 获取标…...

docker pull实现断点续传

问题背景 在使用Docker拉取DockerHub的镜像时&#xff0c;经常会出现网络不稳定的问题&#xff0c;这就导致拉取到一半的镜像会重新拉取&#xff0c;浪费时间。例如下面这种情况&#xff1a; 第二次拉取 这是一个网络中断的场景&#xff0c;第二次重新拉取的时候&#xff0c;同…...

无字母数字webshell之命令执行

源码 题目限制&#xff1a; webshell长度不超过35位除了不包含字母数字&#xff0c;还不能包含$和_ 这里使用php5来解决 可以围绕以下两点展开&#xff1a; shell下可以利用.来执行任意脚本Linux文件名支持用glob通配符代替 .或者叫period&#xff0c;它的作用和source一样…...

华为OD笔试

机试总分400。三道题目。100&#xff0b;100&#xff0b;200 华为od考试时间为150分钟&#xff0c;共有三道编程题&#xff0c;分数分别为100、100和200。如果你是目标院校&#xff08;查看目标院校请戳&#xff09;的话&#xff0c;及格线是160分&#xff0c;非目标院校则不确…...

HAProxy理论+实验

目录 一、基于cookie的会话保持 1、配置选项 2、配置示例 3、验证cookie信息 二、IP透传 1、layer4 与 layer7 &#xff08;1&#xff09;四层:IPPORT转发 &#xff08;2&#xff09;七层:协议内容交换 三、haproxy的ACL应用 1、ACL配置选项 &#xff08;1&#xf…...

Spring Boot ⽇志

1. ⽇志概述 为什么要学习⽇志 ⽇志对我们来说并不陌⽣, 从JavaSE部分, 我们就在使⽤ System.out.print 来打印⽇志了. 通过打 印⽇志来发现和定位问题, 或者根据⽇志来分析程序的运⾏过程. 在Spring的学习中, 也经常根据控制台 的⽇志来分析和定位问题. 随着项⽬的复杂…...

最详细!教你学习haproxy七层代理

一、工作原理 &#xff08;1&#xff09;包括 监听端口&#xff1a;HAProxy 会在指定的端口上监听客户端的请求。 例如&#xff0c;它可以监听常见的 HTTP 和 HTTPS 端口&#xff0c;等待客户端连接。请求接收&#xff1a;当客户端发起请求时&#xff0c;HAProxy 接收到请求。…...

ElementUI 事件回调函数传参技巧与自定义参数应用

ElementUI 事件回调函数传参技巧与自定义参数应用 在使用elementUI时&#xff0c;事件回调函数传递参数是一个常见的需求。根据搜索结果&#xff0c;我们可以了解到两种主要的方法来传递自定义参数&#xff1a; 使用回调函数&#xff1a;当elementUI组件触发事件时&#xff0c…...

优化Next的webpack配置

众所周知&#xff0c;next的webpack打包实际上分成了两个部分&#xff0c;一个是服务器端、一个是客户端&#xff0c;我们这里的配置主要是针对客户端的配置。 目的在于降低_app.js包大小&#xff0c;合理划分基础包、工具包、常用方法包、拆分lodash按需引入效果。 拆分lodas…...

Q-Dir vs 传统文件管理器:为何开发者更偏爱这款神器?

前言 在这个信息爆炸的时代&#xff0c;我们每天都在与海量的文件和文件夹打交道&#xff1b;你是否曾经为了找一个文件而翻遍了整个硬盘&#xff1f;是否因为繁琐的文件夹操作而头疼不已&#xff1f;今天&#xff0c;就让我小江湖带你走进一个全新的世界——Q-Dir&#xff0c;…...

日常疑问小记录

1、在抢票过程中&#xff0c;有些人显示服务器崩溃而另一些人仍能访问&#xff0c;可能是由于以下几个原因&#xff1a; &#xff08;1&#xff09;负载均衡&#xff1a;服务器可能采用了负载均衡技术&#xff0c;将用户请求分配到多个服务器上。部分用户可能被引导到正常运行…...

Java Web —— 第四天(HTTP协议,Tomcat)

HTTP-概述 概念:Hyper Text Transfer Protocol&#xff0c;超文本传输协议&#xff0c;规定了浏览器和服务器之间数据传输的规则 特点: 1. 基于TCP协议:面向连接&#xff0c;安全 2.基于请求-响应模型的:一次请求对应一次响应 3. HTTP协议是无状态的协议: 对于事务处理没有…...

C++ list的基本使用

目录 1.list简要介绍 2. list的构造 3. list中迭代器的使用 (1). 双向迭代器与随机访问迭代器使用区别 4.判空、获取元素个数 5. list头、尾元素的访问 6. 插入与删除操作 (1). 头插头删&#xff0c;尾插尾删 (2). 插入&#xff0c;删除与清空 (3). 交换 7. list容器迭代…...

云中韧性:Spring Cloud服务调用重试机制深度解析

标题&#xff1a;云中韧性&#xff1a;Spring Cloud服务调用重试机制深度解析 在微服务架构中&#xff0c;服务间的调用可能会因为网络问题、服务不可达、资源竞争等原因失败。Spring Cloud作为微服务架构的主流实现框架&#xff0c;提供了一套完整的服务调用重试机制&#xf…...

83.SAP ABAP从前台找字段所在表的两种方法整理笔记

目录 方法1&#xff1a;F1查看技术信息 F1 技术信息 方法2&#xff1a;ST05开启跟踪 Activate Trace Input and save data Deactivate Trace Display Trace 分析你想要的表 方法1&#xff1a;F1查看技术信息 从前台找一个屏幕字段所在表&#xff0c;一般通过按F1来查找…...

docker为普通用户设置sudo权限

在 Linux 上使用 Docker 命令时&#xff0c;默认情况下需要 sudo 权限。这是因为 Docker 守护进程&#xff08;Docker daemon&#xff09;通常运行在 root 用户下&#xff0c;而访问和管理 Docker 守护进程的权限也默认被限制给 root 用户。 然而&#xff0c;为了方便日常使用…...

Nginx + PHP 8.0支持视频上传

在 Ubuntu 20.04 上配置 Nginx PHP 8.0 的架构以支持视频上传&#xff0c;需要从 Nginx 和 PHP 两个方面进行配置&#xff0c;以确保服务器能够处理大文件上传。以下是详细的步骤&#xff1a; 1. Nginx 配置 1.1 增加上传文件大小限制 默认情况下&#xff0c;Nginx 对上传文…...

MySQL基础详解(3)

文章目录 索引普通索引创建索引修改表结构(添加索引)创建表的时候直接指定删除索引的语法 唯一索引创建索引修改表结构创建表的时候直接指定 使用ALTER 命令添加和删除索引使用ALTER 命令添加和删除主键显示索引信息 约束非空约束&#xff1a;唯一约束&#xff1a;主键约束&…...

傅里叶变换结合数学形态学进行边缘增强和边缘提取

一、前言 傅里叶变换是图像变换的一种常用方法&#xff0c;傅里叶变换在图像处理中扮演着核心角色&#xff0c;它通过将图像从空间域转换到频率域&#xff0c;获取图像的频率成分&#xff0c;因为有些特征在空间域很难处理&#xff0c;但在频率域比较容易处理。这一转换有助于分…...