C++--继承

文章目录

  • 继承
    • 1. 继承的概念及定义
      • 1.1 继承的概念
      • 1.2 继承的定义
        • 1.2.1 定义格式
        • 1.2.2 继承方式和访问限定符
        • 1.2.3 继承基类成员访问方式的变化
          • 1.2.3.1 基类成员访问方式的变化规则
          • 1.2.3.2 默认继承方式
      • 1.3 继承类模版
    • 2. 基类和派生类的转化
    • 3. 继承中的作用域
      • 3.1 隐藏
      • 3.2 经典面试题
    • 4. 派生的默认成员函数
      • 4.1 普通类的默认成员函数
      • 4.2 派生类的默认成员函数
        • 4.2.1 派生类的构造函数
        • 4.2.2 派生类的拷贝构造函数
        • 4.2.3 派生类的赋值重载函数
        • 4.2.4 派生类的析构函数
        • 4.2.5 派生类的4个成员函数的总结
      • 4.3 面试题:实现一个不能被继承的类
    • 5. 继承和友元
    • 6. 继承和静态成员
    • 7. 多继承和菱形继承问题
      • 7.1 继承模型
      • 7.2 虚继承
      • 7.3 虚拟继承的原理
      • 7.4 多继承面试题
    • 8. 继承和组合

继承

1. 继承的概念及定义

1.1 继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前接触的复用都是函数复用,而继承便是类设计层次的复用。

具体示例:

下面在没有学习到继承之前设计了两个类 StudentTeacherStudentTeacher 都有姓名、年龄等成员变量,都有 Print 这个成员函数,然而这些内容在这两个类中是重复出现的,设计到两个类里面就是冗余的。

当让这两个类中也有一些不同的成员变量,比如 Teacher 中独有的成员变量是工号,Student 中独有的成员变量是学号。

//Student类
class Student
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "张三";   //姓名int _age = 18;  		//年龄int _stuid;   			//学号
};//Teacher类
class Teacher
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "张三";   //姓名int _age = 18;  		//年龄int _jobid;   			//工号
};

在这里插入图片描述

下面是使用了继承,将公有的成员都放到了 Person 中,StudentTeacher 都继承Person,就可以复⽤这些成员,就 不需要重复定义了,省去了很多麻烦。

继承后,父类 Person 的成员,包括成员函数和成员变量,都会变成子类的一部分,也就是说,子类 StudentTeacher 复用了父类 Person 的成员。

//父类
class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "张三";   //姓名int _age = 18;     		//年龄
};//子类
class Student : public Person
{
protected:int _stuid;   //学号
};//子类
class Teacher : public Person
{
protected:int _jobid;   //工号
};

在这里插入图片描述

1.2 继承的定义

1.2.1 定义格式

下面看到 Person基类,也称作父类Student派生类,也称作子类。(因为翻译的原因,所以既叫基类/派生类,也叫父类/子类)

在这里插入图片描述

1.2.2 继承方式和访问限定符

访问限定符有以下三种:

  1. public访问
  2. protected访问
  3. private访问

继承的方式也有类似的三种:

  1. public继承
  2. protected继承
  3. private继承

在这里插入图片描述

1.2.3 继承基类成员访问方式的变化

基类当中被不同访问限定符修饰的成员,以不同的继承方式继承到派生类当中后,该成员最终在派生类当中的访问方式将会发生变化。

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

**总结:**可以认为三种访问限定符的权限大小为:public > protected > private,可以以这个为基准去理解表中的结果。

1.2.3.1 基类成员访问方式的变化规则
  1. 在基类当中的访问方式为 publicprotected 的成员,在派生类当中的访问方式变为:Min(成员在基类的访问方式,继承方式)
  2. 在基类当中的访问方式为 private 的成员,在派生类当中都是不可见的。

如何去理解基类的private成员在派生类当中不可见

这句话的意思是,无法在派生类当中访问基类的 private 成员。

例如,虽然 Student 类继承了 Person 类,但是无法在 Student 类当中访问 Person 类当中的 private 成员 _name。

//基类
class Person
{
private:string _name = "张三"; //姓名
};
//派生类
class Student : public Person
{
public:void Print(){//在派生类当中访问基类的private成员,报错!cout << _name << endl; }
protected:int _stuid;   //学号
};

也就是说,基类的 private 成员无论以什么方式继承,在派生类中都是不可见的,这里的不可见是指基类的私有成员虽然被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它

  1. 因为规则2中规定基类的private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就需要定义为 protected ,由此可以看出,protected 限定符是因继承才出现的。

**注意:**在实际运用中一般使用的都是 public 继承,几乎很少使用 protectedprivate 继承,也不提倡使用 protectedprivate 继承,因为使用 protectedprivate 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

1.2.3.2 默认继承方式

在使用继承的时候也可以不指定继承方式,使用关键字 class 时默认的继承方式是 private ,使用 struct 时默认的继承方式是 public

在关键字为class的派生类当中,所继承的基类成员_name的访问方式变为private。

//基类
class Person
{
public:string _name = "张三"; //姓名
};//派生类
class Student : Person //默认为private继承
{
protected:int _stuid;   //学号
};

在关键字为struct的派生类当中,所继承的基类成员_name的访问方式仍为public。

//基类
class Person
{
public:string _name = "张三"; //姓名
};//派生类
struct Student : Person //默认为public继承
{
protected:int _stuid;   //学号
};

注意: 虽然继承时可以不指定继承方式而采用默认的继承方式,但还是最好显示的写出继承方式。

1.3 继承类模版

下面是利用继承,通过 vector<int> 作为基类继承给了 stack 从而达到快速开发的目的。

namespace bit
{//template<class T>//class vector//{};// stack和vector的关系,既符合is-a,也符合has-a template<class T>class stack : public std::vector<T>{public:void push(const T& x){// 基类是类模板时,需要指定⼀下类域, // 否则编译报错:error C3861: “push_back”: 找不到标识符 // 因为stack<int>实例化时,也实例化vector<int>了 // 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到 vector<T>::push_back(x);//push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};
}int main()
{bit::stack<int> st;st.push(1);st.push(2);st.push(3);while (!st.empty()){cout << st.top() << " ";st.pop();}return 0;
}

**补充:**第18行,使用了类模版中还未实例化的成员函数一定要指定类域才可以调用。

2. 基类和派生类的转化

在继承关系中,派生类对象可以直接赋值给基类的对象/基类的指针/基类的引用,而不产生类型转换。这个赋值的过程也被形象的叫做切片或者切割,寓意把派生类中父类那部分切来赋值过去。

在这里插入图片描述

如图所示:派生类对象赋值给基类对象时是直接将派生类中属于基类那一部分切割给基类。引用和指针也是一样,基类的引用是派生类中属于基类那一部分成员的别名,基类的指针指向派生类中属于基类的那一部分

具体示例:

//基类
class Person
{
protected:string _name; //姓名string _sex;  //性别int _age;     //年龄
};//派生类
class Student : public Person
{
protected:int _stuid;   //学号
};int main()
{Student s;Person p = s;     //派生类对象赋值给基类对象,可以Person* ptr = &s; //派生类对象赋值给基类指针,可以Person& ref = s;  //派生类对象赋值给基类引用,可以s = p;	//基类对象不可以赋值给派生类,这里会编译错误return 0;
}

派生类对象赋值给基类指针图示:

在这里插入图片描述

派生类对象赋值给基类引用图示:

在这里插入图片描述

**注意:**基类对象不能赋值给派生类对象,基类的指针可以通过强制类型转换赋值给派生类的指针,但是此时基类的指针必须是指向派生类的对象才是安全的。

3. 继承中的作用域

3.1 隐藏

在继承体系中的基类和派生类都有独立的作用域。若派生类和基类中有同名成员(成员变量、成员函数),派生类成员将屏蔽基类对自己作用域中同名成员的直接访问,这种情况叫隐藏

具体示例:

#include <iostream>
#include <string>
using namespace std;//父类
class Person
{
protected:int _num = 111;
};//子类
class Student : public Person
{
public:void fun(){cout << _num << endl;}
protected:int _num = 999;
};int main()
{Student s;s.fun(); return 0;
}
//运行结果:999

对于以上代码,访问成员函数 fun 会打印子类中的成员变量 _num ,但是父类和子类中均有成员变量 _num ,但是这里会访问子类中的 _num,也就是打印 999

补充:

  • 若此时就是要访问父类当中的 _num 成员,可以使用作用域限定符进行指定访问

    void fun()
    {cout << Person::_num << endl; //指定访问父类当中的_num成员
    }
    

3.2 经典面试题

题目描述:

**问题1:**下面两个 func 是什么关系?A. 重载 B. 重写 C.没关系

**问题2:**下面这段程序编译运行的结果是什么?A. 编译报错 B. 运行报错 C.正常运行

class A
{
public:void func(){cout << "func()" << endl;}
};class B : public A
{
public:void func(int i){cout << "func(int i)" <<i<<endl;}
};int main()
{B b;b.fun();return 0;
}

问题1:

虽然 A 类中的 func 函数和 B 类中的 func 函数同名且参数不同,但是它们不构成重载,因为它们的作用域不同,重载函数一定是在同一个作用域中的。并且根据隐藏的规则,成员函数的隐藏,只需要函数名相同就构成隐藏,可知,这两个函数构成隐藏,选择A。

问题2:

所以两个 func 的关系是隐藏,因为 B 继承自 A,这里通过实例化 B 的对象 b 来调用 fun 函数,但是 B 中的 fun 函数需要参数,所以这里的语法出现问题,会编译报错。选择A。

如果这里想调用父类中的 fun 函数,需要在指定父类作用域。

总结:

  • 针对成员变量,派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派生类成员函数中,可以使用基类 :: 基类成员显示访问)
  • 如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  • 注意在实际中在继承体系里面最好不要定义同名的成员。

4. 派生的默认成员函数

4.1 普通类的默认成员函数

在学习派生类的默认成员函数之前,先来回顾一下普通类的默认成员函数:C++中成员变量的类型一共可以分为两类:内置类型和自定义类型,各个默认成员函数对它们的处理可以用下面两个图片概括:

在这里插入图片描述

在这里插入图片描述

**注意:**由于取地址重载和 const 取地址重载这两个默认成员函数一般使用编译器自动生成的即可,所以在这里不考虑它们。

4.2 派生类的默认成员函数

和普通类的默认成员函数一样,这里只讨论构造函数、析构函数、拷贝构造函数和赋值重载函数这四个成员函数。

4.2.1 派生类的构造函数

在均使用默认构造的前提下对于派生类的成员变量中的内置类型(有缺省值就用,没有就由编译器初始化)、自定义类型(使用默认构造)和父类成员(调用父类默认构造)。

若要自行实现构造函数,如果基类有默认的构造函数,派生类的构造函数无须调用基类的构造函数直接初始化子类的成员变量即可。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用基类的构造函数,再对剩余派生类中的成员进行构造。

//父类
class Person 
{
public://父类构造函数Person(const char* name)//非默认构造: _name(name){cout << "Person()" << endl;}
protected:string _name; // 姓名
}//子类
class Student : public Person 
{
public://自行实现的子类构造函数Student(const char* name, int num): Person(name)  //显示调用父类构造, _num(num){cout << "Student()" << endl;}protected:int _num; //学号
}

补充:上述代码中的父类中没有构造函数,所以其子类 Student 的构造函数就必须先显示调用父类 Person 的构造函数再初始化子类中的成员变量。如果父类 Person 中的构造函数有默认构造那么子类的构造函数在默认情况下就无需显式调用父类的构造函数,只需要对子类的成员函数进行初始化即可。

4.2.2 派生类的拷贝构造函数

在均使用默认拷贝构造的前提下对于派生类的成员变量中的内置类型(浅拷贝)、自定义类型(此类型的拷贝构造)和父类成员(调用父类的拷贝构造)。

如果要自行实现拷贝构造函数,派生类的拷贝构造函数必须先调用基类的拷贝构造完成基类的拷贝初始化。再对子类中的成员变量进行拷贝构造。

//父类
class Person 
{
public://父类拷贝构造函数Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}
protected:string _name; // 姓名
}//子类
class Student : public Person 
{
public://自行实现的子类拷贝构造函数Student(const Student& s): Person(s)  //显示调用父类拷贝构造, _num(s._num){cout << "Student(const Student& s)" << endl;}protected:int _num; //学号
}

**补充:**针对拷贝构造无论父类如何,子类若要自行实现拷贝构造函数必须在初始化列表中显示调用父类的拷贝构造函数。

并且这里调用父类的拷贝构造时传递的参数直接传递子类的变量名即可,因为以指针的形式接收,父类的拷贝构造函数接受后会对其进行切割,这里涉及基类和派生类对象的赋值中的知识点。

4.2.3 派生类的赋值重载函数

在均使用默认赋值重载构造的前提下对于派生类的成员变量中的内置类型(浅拷贝)、自定义类型(此类型的拷贝构造)和父类成员(调用父类的拷贝构造)。与拷贝构造相同。

如果要自行实现赋值重载函数,其要求也与自行实现拷贝构造函数相同,必须要调用基类的operator=完成基类的复制。

//父类
class Person 
{
public://父类赋值重载函数Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}
protected:string _name; // 姓名
}//子类
class Student : public Person 
{
public://自行实现的子类赋值重载函数Student& operator = (const Student& s) {cout << "Student& operator= (const Student& s)" << endl;if (this != &s){Person::operator =(s);  //父类赋值重载_num = s._num;}return *this;}
protected:int _num; //学号
}

**补充:**针对赋值重载函数无论父类如何,子类若要自行实现赋值重载函数必须在初始化列表中显示调用父类的赋值重载函数。

同样这里的赋值重载函数也需要使用传子类的变量名作为参数给父类的赋值重载函数,因为因为以指针的形式接收,父类的赋值重载函数接受后会对其进行切割,使其变成子类对象中父类那部分的别名。

并且这里调用 operator = 时需要指定父类类域,因为如果不指定这里的 operator = 由于子类和父类中的函数名相同构成隐藏,就会一直调用子类的 operator = 最后造成栈溢出

4.2.4 派生类的析构函数
//父类
class Person 
{
public://父类析构函数~Person(){cout << "~Person()" << endl;}
protected:string _name; // 姓名
}//子类
class Student : public Person 
{
public://自行实现的子类赋值重载函数~Student() {cout << "~Student()" << endl;}
protected:int _num; //学号
}

补充:派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。所以在自行实现子类析构函时不需要显示调用父类的析构函数。

如果在平时代码中需要在子类中调用父类的析构函数需要使用类域指定析构函数,Person : ~Person()。这是因为派生类的析构和基类的析构构成隐藏关系。(由于多态关系需求,所有的析构函数的函数名都会被编译器处理为 destructor,因为函数名相同所以构成隐藏。)

4.2.5 派生类的4个成员函数的总结
  1. 派生类的成员变量分为三类:内置类型、自定义类型以及父类成员变量。其中派生类成员函数对内置类型和自定义类型的处理和普通类的成员函数一样,但是父类成员变量必须由父类成员函数来处理

  2. 派生类的析构函数非常特殊,它不需要我们显式调用父类的析构函数,而是会在子类析构函数调用完毕后自动调用父类的析构函数,这样做是为了保证子类成员先被析构,父类成员后被析构 (如果我们显式调用父类析构,那么父类成员变量一定先于子类成员变量析构)。同时,子类析构和父类析构构成隐藏在这里插入图片描述

  3. 派生类对象初始化先调用基类构造再调派生类构造,派生类对象析构清理先调用派生类析构再调基类的析构。

    并且派生类对象的析构函数在被调用完之后会自动调用基类的析构函数清理基类成员。因为只有这样才能保证派生类对象先清理派生类成员在清理基类成员的顺序。在这里插入图片描述

4.3 面试题:实现一个不能被继承的类

**方法1:**基类的构造函数私有,派生类的构成函数必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。

class Base 
{
public:void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private:// C++98的⽅法 Base(){}
};class Derive :public Base 
{void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};int main()
{Base b;Derive d;return 0;
}

上面这种是 C++98 给出的做法,它虽然阻止了子类创建对象,但是构造私有化也使得它本身也不能创建对象,因为创建对象需要调用构造函数。所以 C++11 提供了另外一种方式。

**方法2:**C++11新增了一个 final 关键字,final 修改基类,派生类就不能继承了。

//C++11的方法
class Base final
{
public:void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private:
};class Derive :public Base 
{void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};int main()
{Base b;Derive d;return 0;
}

5. 继承和友元

友元关系不能被继承,基类友元不能访问派生类私有和保护成员。

**简记:**你父亲的朋友并不是你的朋友。

class Student;
class Person 
{
public:friend void Display(const Person& p, const Student& s);  //友元函数
protected:string _name; // 姓名
};class Student : public Person 
{friend void Display(const Person& p, const Student& s);  //友元函数
protected:int _stuNum; // 学号
};void Display(const Person& p, const Student& s) 
{cout << p._name << endl;cout << s._stuNum << endl;
}

补充:这里的 Dispaly 函数分别调用了基类和派生类中的成员变量,如果需要访问的话需要再基类和派生类中都加上友元声明。

6. 继承和静态成员

在 类和对象介绍了类的静态成员变量具有如下特性:

  • 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区;
  • 静态成员变量必须在类外定义,定义时不添加 static 关键字,类中只是声明;
  • 静态成员变量的访问受类域与访问限定符的约束。

在继承中,如果父类定义了 static 静态成员,则该静态成员也属于所有派生类及其对象,即整个继承体系里面只有一个这样的成员,并且无论派生出多少个子类,都只有一个 static 成员实例。继承下来的静态成员变量都是指向同一块空间的。

class Person
{
public:string _name;static int _count;	//静态成员类内声明
};
int Person::_count = 0;	//静态成员类外定义class Student : public Person
{
protected:int _stuNum;
};

在这里插入图片描述

7. 多继承和菱形继承问题

7.1 继承模型

**单继承:**一个子类只有一个直接父类时称这个继承关系为单继承。

在这里插入图片描述

**多继承:**一个子类有两个或两个以上直接父类时称这个继承关系为多继承。

在这里插入图片描述

**菱形继承:**菱形继承是多继承的一种特殊情况。

在这里插入图片描述

从菱形继承的模型构造就可以看出,菱形继承的继承方式存在数据冗余二义性的问题。

例如,对于以上菱形继承的模型,当实例化出一个 Assistant 对象后,访问成员时就会出现二义性问题。

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"; //二义性:无法明确知道要访问哪一个_name,产生报错return 0;
}

补充: Assistant 对象是多继承的 StudentTeacher ,而 StudentTeacher 当中都继承了 Person ,因此 StudentTeacher 当中都有 _name 成员,若是直接访问 Assistant 对象的 _name 成员会出现访问不明确的报错。

如果想要访问 _name 中的数据,可以具体指定是哪个类域的 _name

//显示指定访问哪个父类的成员
a.Student::_name = "张同学";
a.Teacher::_name = "张老师";

虽然该方法可以解决二义性的问题,但仍然不能解决数据冗余的问题。因为在 Assistant的对象在 Person 成员始终会存在两份。

在这里插入图片描述

7.2 虚继承

为了解决菱形继承的二义性和数据冗余问题,出现了虚拟继承。如前面说到的菱形继承关系,在Student和Teacher继承Person是使用虚拟继承,即可解决问题

虚拟继承代码如下:

class Person
{
public:string _name; //姓名
};class Student : virtual public Person //虚拟继承
{
protected:int _num; //学号
};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;
}

此时就可以直接访问 Assistant 对象的 _name 成员了,并且之后就算指定访问 AssistantStudent 父类和 Teacher 父类的 _name 成员,访问到的都是同一个结果,解决了二义性的问题。而打印 AssistantStudent 父类和 Teacher 父类的 _name 成员的地址时,显示的也是同一个地址,解决了数据冗余的问题。

cout << a.Student::_name << endl; //运行结果:peter
cout << a.Teacher::_name << endl; //运行结果:petercout << &a.Student::_name << endl; //运行结果:0136F74C
cout << &a.Teacher::_name << endl; //运行结果:0136F74C

7.3 虚拟继承的原理

7.4 多继承面试题

多继承中指针偏移问题?下面说法正确的是()

A: p1 == p2 == p3
B: p1 < p2 < p3
C: p1 == p3 != p2
D: p1 != p2 != p3

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

解答:

在这里插入图片描述

首先创建一个 Derive 类的对象 d ,因为C++中规定在内存中先继承的存储在前面,所以这里的 Base1Base2 呈上图存储方式排列。

所以 p3 理所当然指向这块空间的起始地址,然而对于 p1p2 因为其类型为 Derive 的父类,所以在赋值的时候,需要进行切片,指向子类中父类那一块所属的空间。

p1 指向 Base1 的起始地址也就是和 p3 指向的空间一样,但是需要注意 p1p3 的含义并不一样,如果对 p3 解引用其空间包含 d 的整块空间,如果对 p1 解引用其空间则只包含 Base1 那一块。p2与以上类似,指向 Base2 的起始地址。

最后根据图示的地址大小可以得出答案为C。

8. 继承和组合

public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象,本质是子类对象是一种特殊的父类对象

组合是一种 has-a 的关系,假设 B 组合了 A,则每个 B 对象中都有一个 A 对象。

继承允许你根据基类的实现来定义派生类的实现,这种通过生成派生类的复用通常被称为白箱复用 (white-box reuse)。术语 “白箱” 是相对可视性而言:在继承方式中,基类的内部细节对子类可见,即派生类可以访问基类的 protected 成员 。所以继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响,派生类和基类间的依赖关系很强,耦合度高

对象组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口,这种复用风格被称为黑箱复用 (black-box reuse),因为对象的内部细节是不可见的,即组合只能访问对象的共有成员,对象只以 “黑箱” 的形式出现。组合类之间没有很强的依赖关系,耦合度低,优先使用对象组合有助于保持每个类被封装

所以如果既能用继承,也能用组合,优先使用组合,因为组合耦合度低,代码维护性好。对于继承来说,父类的任何一个非私有成员修改都可能会影响子类,而对于组合,只有公有成员修改才可能会影响;但在实际开发中基本上不会出现全部都是公有成员的类,所以优先使用组合。

不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态也必须要继承,只是说当类之间的关系即可以用继承,可以用组合时,优先使用组合。

**eg1:**车类和宝马类就是is-a的关系,它们之间适合使用继承。

class Car
{
protected:string _colour; //颜色string _num; //车牌号
};class BMW : public Car	//BWM是车,继承关系
{
public:void Drive(){cout << "this is BMW" << endl;}
};

**eg2:**车和轮胎之间就是has-a的关系,它们之间则适合使用组合。

class Tire
{
protected:string _brand; //品牌size_t _size; //尺寸
};class Car	//车有轮胎,组合关系
{
protected:string _colour; //颜色string _num; //车牌号Tire _t; //轮胎
};

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

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

相关文章

无REPOSITORY、TAG的docker悬空镜像究竟是什么?是否可删除?

有时候&#xff0c;使用docker images指令我们可以发现大量的无REPOSITORY、TAG的docker镜像&#xff0c;这些镜像究竟是什么&#xff1f; 它们没有REPOSITORY、TAG名称&#xff0c;没有办法引用&#xff0c;那么它们还有什么用&#xff1f; [rootcdh-100 data]# docker image…

创建一个基于YOLOv8+PyQt界面的驾驶员疲劳驾驶检测系统 实现对驾驶员疲劳状态的打哈欠检测,头部下垂 疲劳眼睛检测识别

如何使用Yolov8创建一个基于YOLOv8的驾驶员疲劳驾驶检测系统 文章目录 1. 数据集准备2. 安装依赖3. 创建PyQt界面4. 模型训练1. 数据集准备2. 模型训练数据集配置文件 (data.yaml)训练脚本 (train.py) 3. PyQt界面开发主程序 (MainProgram.py) 4. 运行项目5. 关键代码解释数据集…

使用FFmpeg将YUV编码为H.264并封装为MP4,通过api接口实现

YUV数据来源 摄像头直接采集的原始视频流通常为YUV格式&#xff08;如YUV420&#xff09;&#xff0c;尤其是安防摄像头和网络摄像头智能手机、平板电脑的摄像头通过硬件接口视频会议软件&#xff08;如Zoom、腾讯会议&#xff09;从摄像头捕获YUV帧&#xff0c;进行预处理&am…

tcpdump工具交叉编译

本文默认系统已经安装了交叉工具链环境。 下载相关版本源码 涉及tcpdump源码&#xff0c;以及tcpdump编译过程依赖的pcap库源码。 网站&#xff1a;http://www.tcpdump.org/release wget http://www.tcpdump.org/release/libpcap-1.8.1.tar.gz wget http://www.tcpdump.org/r…

神经网络中torch.nn的使用

卷积层 通过卷积核&#xff08;滤波器&#xff09;在输入数据上滑动&#xff0c;卷积层能够自动检测和提取局部特征&#xff0c;如边缘、纹理、颜色等。不同的卷积核可以捕捉不同类型的特征。 nn.conv2d() in_channels:输入的通道数&#xff0c;彩色图片一般为3通道 out_c…

在MATLAB中使用GPU加速计算及多GPU配置

文章目录 在MATLAB中使用GPU加速计算及多GPU配置一、基本GPU加速使用1. 检查GPU可用性2. 将数据传输到GPU3. 执行GPU计算 二、多GPU配置与使用1. 选择特定GPU设备2. 并行计算工具箱中的多GPU支持3. 数据并行处理&#xff08;适用于深度学习&#xff09; 三、高级技巧1. 异步计算…

【unitrix】 4.12 通用2D仿射变换矩阵(matrix/types.rs)

一、源码 这段代码定义了一个通用的2D仿射变换矩阵结构&#xff0c;可用于表示二维空间中的各种线性变换。 /// 通用2D仿射变换矩阵&#xff08;元素仅需实现Copy trait&#xff09; /// /// 该矩阵可用于表示二维空间中的任意仿射变换&#xff0c;支持以下应用场景&#xff…

android RecyclerView隐藏整个Item后,该Item还占位留白问题

前言 android RecyclerView隐藏整个Item后,该Item还占位留白问题 思考了利用隐藏和现实来控制item 结果实现不了方案 解决方案 要依据 model 的第三个参数&#xff08;布尔值&#xff09;决定是否保留数据&#xff0c;可以通过 ​filter 高阶函数结合 ​空安全操作符​ 实…

地图瓦片介绍与地图瓦片编程下载

前沿 地图瓦片指将一定范围内的地图按照一定的尺寸和格式&#xff0c;按缩放级别或者比例尺&#xff0c;切成若干行和列的正方形栅格图片&#xff0c;对切片后的正方形栅格图片被形象的称为瓦片[。瓦片通常应用于B/S软件架构下&#xff0c;浏览器从服务器获取地图数据&#xf…

手机屏亮点缺陷修复及相关液晶线路激光修复原理

摘要 手机屏亮点缺陷严重影响显示品质&#xff0c;液晶线路短路、电压异常是导致亮点的关键因素。激光修复技术凭借高能量密度与精准操控性&#xff0c;可有效修复液晶线路故障&#xff0c;消除亮点缺陷。本文分析亮点缺陷成因&#xff0c;深入探究液晶线路激光修复原理、工艺…

MySQL数据一键同步至ClickHouse数据库

随着数据量的爆炸式增长和业务场景的多样化&#xff0c;传统数据库系统如MySQL虽然稳定可靠&#xff0c;但在海量数据分析场景下逐渐显露出性能瓶颈。这时&#xff0c;ClickHouse凭借其列式存储架构和卓越的OLAP&#xff08;在线分析处理&#xff09;能力脱颖而出&#xff0c;成…

Android中Compose常用组件以及布局使用方法

一、基础控件详解 1. Text - 文本控件 Text(text "Hello Compose", // 必填&#xff0c;显示文本color Color.Blue, // 文字颜色fontSize 24.sp, // 字体大小&#xff08;注意使用.sp单位&#xff09;fontStyle FontStyle.Italic, // 字体样式&…

SCI一区黑翅鸢优化算法+三模型光伏功率预测对比!BKA-CNN-GRU、CNN-GRU、GRU三模型多变量时间序列预测

SCI一区黑翅鸢优化算法三模型光伏功率预测对比&#xff01;BKA-CNN-GRU、CNN-GRU、GRU三模型多变量时间序列预测 目录 SCI一区黑翅鸢优化算法三模型光伏功率预测对比&#xff01;BKA-CNN-GRU、CNN-GRU、GRU三模型多变量时间序列预测效果一览基本介绍程序设计参考资料 效果一览 …

创客匠人视角:创始人 IP 打造为何成为知识变现的核心竞争力

在互联网流量成本高企的当下&#xff0c;知识变现行业正经历从 “产品竞争” 到 “IP 竞争” 的范式迁移。创客匠人 CEO 老蒋指出&#xff0c;创始人 IP 已成为企业突破增长瓶颈的关键支点 —— 美特斯邦威创始人周成建首次直播即创下 1500 万元成交额&#xff0c;印证了创始人…

类图+案例+代码详解:软件设计模式----生成器模式(建造者模式)

生成器模式&#xff08;建造者模式&#xff09; 把复杂对象的建造过程和表示分离&#xff0c;让同样的建造过程可以创建不同的表示。 假设你去快餐店买汉堡&#xff0c;汉堡由面包、肉饼、蔬菜、酱料等部分组成。 建造者模式的角色类比&#xff1a; 产品&#xff08;Product…

UI前端与数字孪生融合探索:为智慧物流提供可视化解决方案

hello宝子们...我们是艾斯视觉擅长ui设计、前端开发、数字孪生、大数据、三维建模、三维动画10年经验!希望我的分享能帮助到您!如需帮助可以评论关注私信我们一起探讨!致敬感谢感恩! 在全球供应链数字化转型的浪潮中&#xff0c;智慧物流正从概念走向落地 —— 据 MarketsandMa…

远程办公与协作新趋势:从远程桌面、VDI到边缘计算,打造高效、安全的混合办公环境

一、引言 随着数字化转型的加速&#xff0c;越来越多的企业开始采用远程办公和混合办公模式&#xff0c;以提升员工的灵活性和企业的敏捷性。然而&#xff0c;异地办公也带来了诸如桌面环境不一致、安全风险增加、沟通协作效率降低等诸多挑战。因此&#xff0c;如何打造一致、…

算法总结篇:二叉树

二叉树解题整体框架&#xff1a; 1、确定当前题型是做高度还是深度还是搜索树还是其他 高度&#xff08;从下往上&#xff0c;求根深度、高度等&#xff09;&#xff1a; 使用后序遍历会更加简单&#xff0c;递归方法一般需要返回值返回上级&#xff0c;让上级对返回值进行判断…

【Elasticsearch】most_fields、best_fields、cross_fields 的区别与用法

most_fields、best_fields、cross_fields 的区别与用法 1.核心区别概述2.详细解析与用法2.1 best_fields&#xff08;最佳字段匹配&#xff09;2.2 most_fields&#xff08;多字段匹配&#xff09;2.3 cross_fields&#xff08;跨字段匹配&#xff09; 3.对比案例3.1 使用 best…

力扣网C语言编程题:在数组中查找目标值位置之暴力解法

一. 简介 本文记录一下力扣网上涉及数组的问题&#xff1a;排序数组中查找目标值的位置。主要以C语言实现。 二. 力扣网C语言编程题&#xff1a;在数组中查找目标值位置 题目&#xff1a;在排序数组中查找元素的第一个和最后一个位置 给你一个按照非递减顺序排列的整数数组 …