[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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/398019.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PLSQL导入导出ORACLE数据提示失败问题修改PLSQL配置

oracle中plsql导入提示无法导入问题 1.首先看下是否环境变量已经配置(具体配置看下面环境变量配置) 2.plsql数据导入中tools-->Preferences中配置如下框中的内容 3.设置 tnsnames.ora文件中看下是否设置有问题 4.PLSQL乱码问题 NLS_LANG SIMPLIFIED CHINESE_CHINA.ZHS16…

性能测试工具之JMeter

JMeter Apache JMeter应用程序是开源软件,是一个100%纯Java应用程序,旨在负载测试功能行为和衡量性能。它最初是为测试Web应用程序而设计的,但后来扩展到其他测试功能。 JMeter是一个免费、开源、跨平台的性能测试工具,于20世纪90年代后期面世。这是一个成熟、健全且具有…

湖南(市场调查)源点咨询 构建多元样本是如何改善调研结果的?

湖南&#xff08;市场调研&#xff09;源点咨询认为&#xff0c;大多数市场研究从业者更倾向于从单一数据源获取调研样本&#xff0c;然而在很多情况下&#xff0c;仅从单个数据源很难获得真正具有代表性的样本。 使用多元样本源有两大好处&#xff1a; ①能够捕获不愿加入样…

2024110读书笔记|《飞花令·月》——长安一片月,万户捣衣声,独出前门望野田,月明荞麦花如雪

2024110读书笔记|《飞花令月》——长安一片月&#xff0c;万户捣衣声&#xff0c;独出前门望野田&#xff0c;月明荞麦花如雪 《飞花令月》素心落雪 编著&#xff0c;飞花令得名于唐代诗人韩翃《寒食》中的名句“春城无处不飞花”&#xff0c;类似于行酒令&#xff0c;是文人们…

Java数组篇[5]:数组的排序和查找

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云/阿里云/华为云/51CTO&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互…

1960-2020中国1km分辨率年均气温数据

数据简介 中国1km分辨率年均气温数据是在中国大陆2400多个站点的气温年统计结果的基础上&#xff0c;融合了NOAA Gsod中包括港澳台在内亚洲地区1300个站点的数据&#xff0c;使用Ansuplin插值软件生成的1960-2020年0.01&#xff08;约1km&#xff09;的网格数据。 Ansuplin基…

shell外壳与Linux权限

&#x1f308;个人主页&#xff1a;Yui_ &#x1f308;Linux专栏&#xff1a;Linux &#x1f308;C语言笔记专栏&#xff1a;C语言笔记 &#x1f308;数据结构专栏&#xff1a;数据结构 文章目录 1.shell命令以及运行原理2. Linux权限的概念3.Linux权限管理3.1 文件访问者的分类…

vue3前端开发-小兔鲜项目-添加购物车操作第一步

首先&#xff0c;呢&#xff0c;告诉大家一个坏消息&#xff0c;官方媒体的案例代码已经被他们删除了。如图所示。 也就是说&#xff0c;大家已经看不到官方的代码文件了。 那么既然如此&#xff0c;我们自己写的这个博客记录日志&#xff0c;就显得尤为重要了。继续今天的内容…

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

制作仪表板 引入数据模型 仪表板所需模型已经在数据模块中准备好&#xff0c;可以将对应模型表添加到数据模型中。提供了两种添加方式&#xff1a; 在数据栏中点击添加按钮&#xff0c;在弹出框中通过搜索或直接在其所在目录下选中该模型&#xff0c;点击确定。 点击数据按钮…

一篇讲清楚什么是密码加密和加盐算法 | 附Java代码实现

目录 前言&#xff1a; 一、密码加密 1. MD5介绍 2.彩虹表攻击 3.测试复杂密码是否能被攻破 二、加盐算法 1.对密码123456演示加盐算法 2.盐值的储存 3.密码加盐思想总结 三、Java代码实现 前言&#xff1a; 早些年&#xff0c;数据泄露屡见不鲜&#xff0c;每个班上总…

【Web前端】vue3整合eslint约束代码格式

一、整合eslint 整合eslint的两种方式&#xff1a; 在已有项目中整合eslint&#xff1a;# 安装eslint及其vue插件即可 npm i -D eslint eslint-plugin-vue创建项目时整合eslint&#xff1a; 提示 是否引入ESLint用于代码质量检测 时选择 是# 创建vue3项目 npx create-vue # 下…

.NET8使用VS2022打包Docker镜像

NET8使用VS2022打包Docker镜像 1. 项目中添加Docker支持文件2. 自定义镜像名称3. 发布Docker镜像3.1 安装Docker3.2 控制台切换到项目根目录,执行以下命令发布镜像 3.3 修改镜像名称4. 保存镜像到本地 1. 项目中添加Docker支持文件 2. 自定义镜像名称 项目文件PropertyGroup节…

vue3中 ref 和 reactive 的区别

相同&#xff1a;均是声明响应式对象。且声明的响应式对象是深层的 1. 数据类型不同&#xff1a;ref用于包装JavaScript基本类型的数据&#xff08;如字符串、数字、布尔值等&#xff09;&#xff0c;而reactive可以用于包装JavaScript对象和数组等复杂类型的数据。 2.访问方式…

力扣45:跳跃游戏2题解

题目链接&#xff1a; https://leetcode.cn/problems/jump-game-ii/description/?envTypestudy-plan-v2&envIdtop-100-liked 参考的代码随想录里面的题解&#xff1a; 题目 难度 中等&#xff0c;标的是中等难度&#xff0c;而且我之前做过这道题&#xff0c;但是我没写…

AI大模型赋能开发者|海云安创始人谢朝海受邀在ISC.AI 2024大会就“大模型在软件开发安全领域的应用”主题发表演讲

近日&#xff0c;ISC.AI 2024 第十二届互联网安全大会在北京国家会议中心盛大开幕。作为全球规格最高、规模最大、影响力最深远的安全峰会之一&#xff0c;本次大会以“打造安全大模型 引领安全行业革命”为主题&#xff0c;聚焦安全与AI两大领域&#xff0c;吸引了众多行业领袖…

【NLP】文本张量表示方法【word2vec、词嵌入】

文章目录 1、文本张量表示2、one-hot词向量表示2.1、one-hot编码代码实现&#xff1a;2.2、onehot编码器的使用2.3、one-hot编码的优劣势 3、word2vec模型3.1、模型介绍3.2、CBOW模式3.3、skipgram模式3.4、word2vec的训练和使用3.4.1、获取训练数据3.4.2、训练词向量3.4.3、查…

学习笔记第二十三天

1.程序与进程 程序&#xff08;Program&#xff09;&#xff1a;是静态的&#xff0c;它是一组指令的集合&#xff0c;这些指令被存储硬盘上&#xff0c;&#xff0c;程序本身 不占用CPU或内存资源&#xff0c;直到它被加载到内存中执行。 程序---静态---硬盘 进程&#xff08…

C++速学day4

类的继承关系 蛇 和 爬行类动物 //这两个类就是继承关系 子类 父类 //它们的关系相当于 派生类 基类 继承的作用 1、 吸收基类成员 2、改造基类成员 3、添加新的成员 class Dervid…

docker基本管理和应用

docker是一个开源的应用容器引擎&#xff0c;基于go语言开发的 docker是运行在linux的容器化工具&#xff0c;可以理解为轻量级的虚拟机 可以在任何主机上&#xff0c;轻松创建的一个轻量级&#xff0c;可移植的&#xff0c;自给自足的容器 鲸鱼--------->宿主机 集装箱…

vulnstack-7(红日靶场七)

环境配置 vlunstack是红日安全团队出品的一个实战环境&#xff0c;具体介绍请访问&#xff1a;漏洞详情http://vulnstack.qiyuanxuetang.net/vuln/detail/9/ 添加两个网卡 DMZ区域&#xff1a; 给Ubuntu (Web 1) 配置了两个网卡&#xff0c;一个可以对外提供服务&#xff1b;…