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

C++基础 [十二] - 继承与派生

目录

前言

什么是继承 

继承的概念

继承的定义

基类与派生类对象的赋值转换 

继承的作用域 

派生类中的默认成员函数 

默认成员函数的调用 

构造函数与析构函数

拷贝构造

赋值运算符重载

显示成员函数的调用

构造函数

拷贝构造

赋值运算符重载

析构函数

继承与友元 

继承与静态成员

菱形继承 

继承和组合 


前言

在编程中,继承是面向对象编程(OOP)的核心概念之一。它通过让一个类(子类)继承另一个类(父类)的特性和行为,帮助我们实现代码复用、提高程序的扩展性和灵活性。那么,为什么继承在编程中如此重要呢?

什么是继承 

试想这样一个场景:假设有个App需要去获取不同类型用户的数据,并进行分类,那么就需要我们去写对应不同的类,比如说学生老师军人公司职工…………每个类都需要有名字、联系方式、家庭住址、年龄……,我们会发现这样每个类都要写一份,非常冗余,于是我们的祖师爷为了解决这个问题,设计出了继承的语法,比如说用户的共同点是都是用户,我们就可以写一个关于人的类,作为基类而不同类型用户就作为基类的派生类,去继承基类的成员,从而达到我们的目的。

继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。

  • 被继承对象:父类 / 基类 base
  • 继承方:子类 / 派生类 derived

继承的本质 就是 - 复用代码 

举个例子 : 假设我现在要设计一个校园管理系统,那么肯定会设计很多角色类,比如学生、老师、保安、保洁等等之类的。 

设计好以后,我们发现,有些数据和方法是每个角色都有的,而有些则是每个角色独有的。 

为了复用代码、提高开发效率,可以从各种角色中选出共同点。组成 基类,比如每个人都有姓名、年龄、联系方式等基本信息,而 教职工学生 的区别就在于 管理与被管理,因此可以在 基类 的基础上加一些特殊信息教职工号 表示 教职工,加上 学号 表示学生,其他细分角色设计也是如此

这样就可以通过 继承 的方式,复用 基类 的代码,划分出各种 子类 

像上面共同拥有的数据和方法我们可以重新设计一个类Person ,然后让 Student 和 Teacher 去继承它,如下:

// 大众类  --- 基础属性
class Person
{
public:Person(string name = string(), string tell = string(), int age = int()):_name(name),_tell(tell),_age(age){}void Print(){cout << "我的名字是 :" << _name << endl;cout << "我的电话是 :" << _tell << endl;cout << "我的年龄是 :" << _age << endl;}
protected:string _name; // 姓名string _tell; // 电话int _age; // 年龄
};// 学生类  --- 派生/子属性
class Student : public Person
{
public:Student(int stuId = 1578):Person("XAS","123456789",26),_stuId(stuId){cout << "我是一个学生" << endl;cout << "以下是我的个人信息 :" << endl;cout << endl;cout << "我的学号为 :" << _stuId << endl;}
protected:int _stuId; // 学号
};// 老师类  --- 派生/子属性
class Teacher : public Person
{
public:Teacher(int workId = 2024916):Person("xas","987654321",26),_workId(workId){cout << "我是一个老师" << endl;cout << "以下是我的个人信息 :" << endl;cout << endl;cout << "我的工号为 :" << _workId << endl;}
protected:int _workId; // 工号
};int main()
{Student s;s.Print();cout << "---------------------" << endl;cout << "---------------------" << endl;Teacher t;t.Print();return 0;
}

继承后父类的 Person 的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了  Student  和 Teacher 复用了 Person 

继承的定义

格式:Person是父类,也称作基类。Student是子类,也称作派生类

继承有权限的概念,分别为:公有继承(public)、保护继承(protected)、私有继承(private没错,与  中的访问 限定修饰符 一样,不过这些符号在这里表示 继承权限 

简单回顾下各种限定符的用途

  • 公有 public:公开的,任何人都可以访问
  • 保护 protected:保护的,只有当前类和子类可以访问
  • 私有 private:私有的,只允许当前类进行访问

权限大小:公有 > 保护 > 私有
保护 protected 比较特殊,只有在 继承 中才能体现它的价值,否则与 私有 作用一样

我们会发现继承方式访问限定符都存在公有、保护、私有这三种继承模式。

 总结:

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

 2、基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。(所以我们不想让子类继承就写成私有,想让子类继承就写成保护。可以理解为private防外人也防儿子,但是protected防外人但是不防儿子。)

3、 上面表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private。

4、使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,我们常用的一般都是class,所以最好显示地写出继承方式。

5、在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强

如何证明呢? 通过一下的代码,我们来验证上面的结论!!

// 父类
class A
{
public:int _a;
protected:int _b;
private:int _c;
};// 子类
class B : public A
{
public:B(){cout << _a << endl;cout << _b << endl;cout << _c << endl;}
};int main()
{// 外部(子类对象)B b;      b._a;
}

下面我们一起去验证一下 

之所以说 C++ 的继承机制设计复杂了,是因为 protected 和 private 继承时的效果一样

其实 C++ 中搞这么多种情况(9种)完全没必要,实际使用中,最常见到的组合为 public : public  protected : public 

如何优雅的使用好 继承权限 呢

对于只想自己类中查看的成员,设为 private,对于想共享给子类使用的成员,设为 protected其他成员都可以设为 public

比如在张三家中,张三家的房子面积允许公开,家庭存款只限家庭成员共享,而个人隐私数据则可以设为私有

class Home
{
public:int area = 500;	//500 平米的大房子
};class Father : public Home
{
protected:int money = 50000;	//存款五万
private:int privateMoney = 100;	//私房钱,怎能公开?
};class Zhangsan : public Father
{
public:Zhangsan(){cout << "我是张三" << endl;cout << "我知道我家房子有 " << area << " 平方米" << endl;cout << "我也知道我家存款有 " << money << endl;cout << "但我不知道我爸爸的私房钱有多少" << endl;}
};class Xiaoming
{
public:Xiaoming(){cout << "我是小明" << endl;cout << "我只知道张三家房子有 " << Home().area << " 平方米" << endl;cout << "其他情况我一概不知" << endl;}
};int main()
{Zhangsan z;cout << "================" << endl;Xiaoming x;return 0;
}

基类与派生类对象的赋值转换 

在继承中,允许将 子类 对象直接赋值给 父类,但不允许 父类 对象赋值给 子类 

  • 这其实很好理解,儿子以后可以当父亲,父亲还可以当儿子吗?

并且这种 赋值 是非常自然的,编译器直接处理,不需要调用 赋值重载 等函数 

子类对象可以赋值给父类对象

//基类
class Person
{
public:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};//派生类
class Student : public Person
{
public:int _id;
};int main()
{Person p;Student s;s._name = "张三";s._sex = "男";s._age = 20;s._id = 8888;p = s; // 子类对象赋值给父类对象return 0;
}

通过调式可以看到,为什么没有把 id 赋值过去呢?

这里有个形象的说法叫切片或者切割,相当于把派生类中父类那部分切来赋值过去,如图所示: 

子类对象可以赋值给父类指针  

//基类
class Person
{
public:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};//派生类
class Student : public Person
{
public:int _id;
};int main()
{Student s;s._name = "张三";s._sex = "男";s._age = 20;s._id = 8888;Person* p = &s;return 0;
}

可以看到,当父类对象是一个指针的时候,照样可以赋值过去: 

子类对象赋值给父类指针切片图: 

子类对象可以赋值给父类引用 

//基类
class Person
{
public:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};//派生类
class Student : public Person
{
public:int _id;
};int main()
{Student s;s._name = "张三";s._sex = "男";s._age = 20;s._id = 8888;Person& rp = s;return 0;
}

可以看到,当父类对象是一个引用的时候,也可以赋值过去:

子类对象赋值给父类引用切换图片:

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

//基类
class Person
{
public:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};//派生类
class Student : public Person
{
public:int _id;
};int main()
{Student s;Person p;s = p;return 0;
}

编译会报错: 

继承的作用域 

在继承体系中 基类 和 派生类 都有独立的作用域,如果子类和父类中有同名成员子类成员将屏蔽父类对同名成员的直接访问,这种情况叫 隐藏,也叫重定义

代码示例: 

Student 的 _num  Person的 _num 构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆。

// 基类
class Person
{
protected:string _name = "Edison"; // 姓名int _num = 555; // 身份证号
};// 派生类
class Student : public Person
{
public:void Print(){cout << "姓名:" << _name << endl;cout << "学号:" << _num << endl;}
protected:int _num = 888; // 学号
};int main()
{Student s1;s1.Print();return 0;
}

运行可以看到,访问的是子类中的_num (类似于局部优先的原则)

这时候你肯定会有疑问了,那么如果我想访问父类中的_num 呢 ? 可以使用 基类 :: 基类成员显示 的去访问 

// 基类
class Person
{
protected:string _name = "Edison"; // 姓名int _num = 555; // 身份证号
};// 派生类
class Student : public Person
{
public:void Print(){cout << "姓名:" << _name << endl;cout << " 身份证号:" << Person::_num << endl;}
protected:int _num = 888; // 学号
};int main()
{Student s1;s1.Print();return 0;
}

可以看到,此时就是访问的父类中的 _num 

还有一点需要注意的是 : 如果是成员函数的隐藏,只需要函数名相同就构成隐藏 

// 基类
class A
{
public:void fun(){cout << "A::func()" << endl;}
};// 派生类
class B : public A
{
public:void fun(int i){cout << "B::func()" << endl;cout << "func(int i)->" << i << endl;}
};int main()
{B b;b.fun(10);return 0;
}

可以看到,默认是去调用子类的 fun() 函数,因为成员函数满足函数名相同就构成隐藏。

如果想调用父类的 fun() 还是需要指定作用域 

// 基类
class A
{
public:void fun(){cout << "A::func()" << endl;}
};// 派生类
class B : public A
{
public:void fun(int i){cout << "B::func()" << endl;cout << "func(int i)->" << i << endl;}
};int main()
{B b;b.A::fun();return 0;
}

运行可以看到,此时就是调用 父类的 fun()

注意 :B 中的 fun 和 A 中的 fun 不是构成函数重载,而是隐藏 !函数重载的要求是在同一作用域里面!!

另外,在实际中在继承体系里面最好不要定义同名的成员。

派生类中的默认成员函数 

6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  • 派生类的operator=必须要调用基类的operator=完成基类的复制。
  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  • 派生类对象初始化先调用基类构造再调派生类构造派生类对象析构清理先调用派生类析构再调基类的析构。

这里我们只以下面的两个类为基础,讨论四类默认成员函数:构造函数、拷贝构造、赋值运算符重载、析构函数 

class Person
{
public:Person(const std::string name = std::string(), const int age = 18, const int sex = 1): _name(name), _age(age), _sex(sex){std::cout << "Person()" << std::endl;}Person(const Person& p): _name(p._name), _age(p._age), _sex(p._sex){std::cout << "Person(const Person& p)" << std::endl;}Person& operator= (const Person& p){std::cout << "operator= (const Person& p)" << std::endl;if (this != &p){_name = p._name;}return *this;}~Person(){std::cout << "~Person()" << std::endl;}
protected:std::string _name;int _age = 18;int _sex = 1;
};class Student : public Person
{
public:
protected:long long _st_id;
};

默认成员函数的调用 

构造函数与析构函数

以下是main函数的逻辑

int main()
{Student st;return 0;
}

输出结果

Person()     // s1 的构造
~Person()    // s1 的析构

说明了:派生类的默认构造函数和默认析构函数都会 - 自动调用基类的构造和析构 

拷贝构造

int main()
{Student st1;Student st2(st1);return 0;
}

输出结果:

Person()                 // s1 的构造函数
Person(const Person& p)  // s2 拷贝构造
~Person()                // s2 的析构函数
~Person()                // s1 的析构函数

说明:派生类的默认拷贝构造会自动调用基类的拷贝构造  

赋值运算符重载

int main()
{Student st1, st2;st1 = st2;return 0;
}

输出结果 

Person()    // s1 的拷贝构造
Person()    // s2 的拷贝构造
operator= (const Person& p)    // 赋值重载
~Person()   // s2 的析构函数
~Person()   // s1 的析构函数

说明:派生类的默认赋值运算符重载会自动调用基类的赋值运算符重载 

实际上,我们可以将派生类的成员分成三部分:基类成员、内置类型成员、自定义类型成员 

  • 继承相较于我们以前学的类和对象,可以说就是多了基类那一部分

  • 当调用派生类的默认成员函数时,对于基类成员都会调用对应基类的默认成员函数来处理

显示成员函数的调用

构造函数

当实现派生类的构造函数时,就算不显示调用基类的构造,系统也会自动调用基类的构造

class Student : public Person
{
public:Student(long long st_id = 111): _st_id(st_id){std::cout << "Student()" << std::endl;}
protected:long long _st_id;
};
int main()
{Student st;return 0;
}

 输出结果

Person()
Student()
~Person()

我们会发现, 这里也没有定义父类对象,但是会调用父类的构造函数,因为C++有规定,派生类必须调用父类的构造函数初始化父类的成员。

如果需要显示的调用基类的构造函数,应该这样写:

Student(long long st_id = 111): _st_id(st_id), Person("xas", 18)
{std::cout << "Student()" << std::endl;
}

注意:如果基类没有提供默认构造函数(即没有没有参数的构造函数)并且派生类没有在构造函数初始化列表中显式调用基类的构造函数,则会导致编译错误。必须在派生类的构造函数中显式调用基类的构造函数。 

还有个问题,这个是Person先初始化还是id先初始化呢?答案是Person先初始化

因为基类的声明优先级是在派生类之上的。它是把父类看成了一个整体。

 派生类初始化的时候,可以初始化基类的成员变量吗?

 

在 C++ 中,当创建派生类对象时,派生类只能初始化派生类中的成员,而基类成员的初始化通常是通过基类的构造函数来完成的。基类的构造函数是在派生类构造函数执行之前调用的。

拷贝构造

派生类的拷贝构造也不能初始化基类的成员

如果我们不写基类的拷贝构造呢?会咋样

s2的peter就是父类默认构造的内容。如果基类没有提供拷贝构造。就会去调用基类的默认构造构造。要是基类默认构造也没那就会报错所以派生类也要显示调用拷贝构造

赋值运算符重载

在实现派生类的赋值运算符重载时,如果没有显式调用 基类的赋值运算符重载系统也不会自动调用基类的赋值运算符重载

class Student : public Person
{
public:Student(long long st_id = 111): _st_id(st_id), Person("xas", 18){std::cout << "Student()" << std::endl;}Student& operator= (const Student& s){std::cout << "operator= (const Student& s)" << std::endl;if (this != &s){_st_id = s._st_id;}return *this;}
protected:long long _st_id;
};
int main()
{Student st1, st2;st1 = st2;return 0;
}

输出结果

Person()
Student()
Person()
Student()
operator= (const Student& s)
~Person()
~Person()

 为什么呢,不是说 派生类的默认赋值运算符重载会自动调用基类的赋值运算符重载吗?

 其实这里是构成了隐藏。由于基类和派生类的赋值运算符重载构成隐藏,因此要用 :: 指定类域

Student& operator= (const Student& s)
{std::cout << "operator= (const Student& s)" << std::endl;if (this != &s){Person::operator=(s);	//由于基类和派生类的赋值运算符重载构成隐藏,因此要用 :: 指定类域_st_id = s._st_id;}return *this;
}

析构函数

在实现派生类的析构函数时,不要显式调用基类的析构函数,系统会在派生类的析构完成后自动调用基类的析构

class Student : public Person
{
public:Student(long long st_id = 111): _st_id(st_id), Person("xas", 18){std::cout << "Student()" << std::endl;}~Student(){std::cout << "~Student()" << std::endl;}
protected:long long _st_id;
};int main()
{Student st1;return 0;
}

输出结果

Person()
Student()
~Student()
~Person()

这时候可能还会有小伙伴说,为什么呢别的默认成员函数都需要显性调用,而析构函数不要显性呢?

 这是因为,如果显示调用了基类的析构,就会导致基类成员的资源先被清理,如果此时生类成员还访问了基类成员指向的资源就会导致野指针问题。因此,必须保证析构顺序为先子后父,保证数据访问的安全

继承与友元 

友元关系不能继承,好比你爸爸的朋友,并不是你的朋友。也就是说 基类友元不能访问子类私有和保护成员,只能访问自己的私有和保护成员。 

下面代码中,Display 函数是基类 Person 的友元,但是 Display 函数不是派生类 Student 的友元

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);return 0;
}

可以看到运行会报错,因为Display是爸爸的朋友,但是可不是儿子的朋友

如果想让 Display 函数也能够访问派生类Student 的私有和保护成员,只需要在派生类Student 当中进行友元声明。 

class Student;class Person
{
public:friend void Display(const Person& p, const Student& s); // 声明Display是Person的友元
protected:string _name; // 姓名
};class Student : public Person
{
public:friend void Display(const Person& p, const Student& s); // 声明Display是Student的友元
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);return 0;
}

继承与静态成员

友元不能继承,但是静态成员有使用权。如果基类中定义了static 静态成员,则整个继承体系里面只有一个这样的成员。 无论派生出多少个子类,都只有一个 static 成员实例

下面代码中,在基类 Person 当中定义了静态成员变量 _ count, 派生类 Student 和 Graduate 继承了Person 

// 基类
class Person
{
public:Person() { ++_count; }
protected:string _name; // 姓名
public:static int _count; // 统计人的个数。
};// 静态成员在类外面定义
int Person::_count = 0; // 派生类
class Student : public Person
{
protected:int _stuNum; // 学号
};// 派生类
class Graduate : public Student
{
protected:string _seminarCourse; // 研究科目
};int main()
{Student s1;Student s2;Student s3;Graduate s4;Person s;cout << " 人数 :" << Person::_count << endl;cout << " 人数 :" << Student::_count << endl;cout << " 人数 :" << s4._count << endl;return 0;
}

我们定义了5个对象,那么每定义一个对象都会去调用一次++ _count ,打印以后可以看到,这几个对象里面的 _count 都是一样的:

同时,我们还可以打印一下地址,可以看到也是同一个: 

静态成员属于父类和派生类,在派生类中不会单独拷贝一份,它继承的是使用权 

如何计算该程序中一共创建了多少个对象??

设置一个静态成员变量,然后在基类的构造函数里++

如上图,我们可以发现如果把静态成员设为公有,那么就会出现可以修改的情况,所以我们最好把静态成员变量设成私有,然后用写一个静态成员函数getcount去获取结果。

 如何实现一个不能被继承的类 ?

 将基类的构造函数藏在私有作用域里,这样继承的时候子类不可见基类构造函数,就无法创建出子类对象了!(将析构函数设成私有也可以)

但是你可能会想,把构造函数放在私域,那A不是也创建不了对象了吗???确实是这样的!

但是也有一个解决的方法,就是利用静态成员函数去返回这个构造函数。

为什么用静态成员函数呢??

因为这涉及到先有鸡还是先有蛋的问题,我们不创建对象就调用不了这个函数,但是要调用这个函数又需要一个对象,所以为了解决这个问题,只能将该函数变成静态成员函数,这样我们可以通过类限定符去访问他 

菱形继承 

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

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

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

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
在Assistant的对象中Person成员会有两份。

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。

什么是虚继承呢,也就是在继承的中间加一个virtual  例如 class C : virtuai public A

需要注意的是,虚拟继承不要在其他地方去使用。为了能够更好地观察虚继承的过程,我们选择一个较为简单的模型。

class A
{
public:int _a;
};
// class B : public A
class B :  public A
{
public:int _b;
};
// class C : public A
class C :  public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

我们先看看没有进行虚继承时,内存中是怎样的

我们看看虚继承后是怎样的,为了能更好地观察,我们用32位环境

不是为了解决冗余问题吗??为什么这边的空间反而变大了??

解决数据冗余要付出存指针的代价,但是如果冗余的对象超过指针的大小的话,那么就赚了。哪怕真的损耗了一点空间。。至少二义性解决了

存的地址指向的空间难道不算空间消耗么??

动态布局的灵活:虚基类的位置是在最终派生类中才确定的,当存在多层虚继承时,不同中间类对同一虚基类的相对偏移可能不同,通过指针可以在运行时进行动态计算,避免了编译时固定偏移的局限。
共享基类的唯一性:虚基类在对象中仅存在一份实例时,若直接存储偏移量,那么每个中间派生类都要维护自己的偏移量,导致冗余,而通过指针可以指向一个共享位置,保证所有访问路径指向同一个实例!

所以如果我们创建了多个d对象,那么存地址的优势就更突出了!!因为偏移量都是一样的

 A有一个_a对象,所以有一个指针,如果A有多个对象是存多个指针吗?

所以哪怕A对象成员变量很多,也只会存一个地址。综合2和3我们可以得到一个结论,无论是创建特别多的d对象,还是说这个a的成员变量非常多,都只会存一个地址。所以虚继承显然能够解决数据冗余的问题。

既然原来b中的a空间存的是一个地址,那如果用b类型或者c类型的指针存d的地址,然后再分别访问_a   或者是用d赋值给b和c类型的对象得到的会是地址吗?

从该图我们可以看到并不会, d赋值给b和c的时候,他们会先通过这个地址找到存放偏移量的空间,然后再回来找到_a,最后也是按照存地址的方式去展现。而B C类型的指针存放d地址的时候,也会通过这个地址找到偏移量,然后再回过头来找_a。所以在这个过程中他们自己会进行处理。从这里我们也可以看到虚继承的访问会存在一定的时间消耗,因为已经不是单纯地解引用了,而是经过了多一层的解引用和计算。

为什么虚基表的前四个字节位置是空出来的?? 

       这是为了后面的虚函数表做准备的,用来存从虚基表找到虚函数表的偏移量(菱形继承多态)。在多态那一环节会去验证。

继承和组合 

public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象。 
而 组合 是一种 has-a 的关系。假设 B 组合了 A,每个 B对象中都有一个 A对象。

继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse)术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见可以直接访问被继承对象的成员变量)。

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口(只能通过接口去访问被组合对象的成员变量)。这种复用风格被称为黑箱复用(black-box reuse)因为对象的内部细节是不可见的对象只以“黑箱”的形式出现。

继承就好比是全包的旅游团,组合就好比是半包旅游自由团

举个例子: 轿车和奔驰就构成 is-a 的关系,所以可以使用继承。 

// 车类
class Car
{
protected:string _colour = "黑色"; // 颜色string _num = "川A66688"; // 车牌号
};// 奔驰
class Benz : public Car
{
public:void Drive(){cout << "好开-操控" << endl;}
};

再举个例子:汽车和轮胎之间就是 has-a 的关系,它们之间则适合使用组合。 

// 轮胎
class Tire {
protected:string _brand = "Michelin"; // 品牌size_t _size = 17; // 尺寸};// 汽车
class Car {
protected:string _colour = "黑色"; // 颜色string _num = "川A66688"; // 车牌号Tire _t; // 轮胎
};

实际项目中,更推荐使用 组合 的方式,这样可以做到 解耦,避免因父类的改动而直接影响到子类

  • 公有继承is-a —> 高耦合,可以直接使用父类成员
  • 组合:has-a —> 低耦合,可以间接使用父类成员

继承的缺点有以下几点:

①:父类的内部细节对子类是可见的。

②:子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。

③:如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。

组合的优点:

①:当前对象只能通过所包含的那个对象去调用其方法(只能用接口不能用成员变量),所以所包含的对象的内部细节对当前对象时不可见的。

②:当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。 

③:对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时刻还可以用一个对象来替代另一个对象。

组合的缺点:①:容易产生过多的对象。②:为了能组合多个对象,必须仔细对接口进行定义。

结论:优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。另一方面,基于对象组合的设计会有更多的对象 (而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。

这导出了我们的面向对象设计的第二个原则:优先使用对象组合,而不是类继承

相关文章:

C++基础 [十二] - 继承与派生

目录 前言 什么是继承 继承的概念 继承的定义 基类与派生类对象的赋值转换 继承的作用域 派生类中的默认成员函数 默认成员函数的调用 构造函数与析构函数 拷贝构造 赋值运算符重载 显示成员函数的调用 构造函数 拷贝构造 赋值运算符重载 析构函数 继承与…...

uniapp小程序,输入框限制输入(正整数、小数后几位)

<uv-input inputAlign"right" type"number" v-model"quantity" placeholder"请输入单价"border"none" input"e > handleInput(e, 2)"/>const quantity ref() const handleInput (e, decimals) >{//…...

KnowGPT知识图谱整合

KnowGPT是由香港理工大学研究团队开发的一种黑盒知识注入框架&#xff0c;旨在通过外部知识图谱&#xff08;KGs&#xff09;增强大型语言模型&#xff08;LLMs&#xff09;在专业领域的问答能力。以下是详细介绍&#xff1a; 一、定义与核心功能 KnowGPT是一种通过API将知识图…...

深入理解MySQL中的MVCC机制

目录 1. MVCC的基本概念 2. MVCC的工作原理 2.1 数据版本的管理 2.1.1 记录的格式 2.1.2 Undo Log的作用 2.2 事务的可见性 3. Read View的作用与实现 3.1 Read View的组成 3.2 Read View的可见性判断规则 3.3 Read View的创建时机 4. Undo Log版本链 4.1 Undo Log…...

基于springboot的在线文档管理系统(026)

摘 要 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;在线文档管理当然也不能排除在外。在线文档管理系统是以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&am…...

【MVCC的前世今生】

一、MVCC的前世今生 MVCC 一个让爪哇开发闻风丧胆的词&#xff0c;因为面试必问&#xff0c;既然大家都知道这个问题是必问的&#xff0c;那就看谁理解的透彻了。 在数据库系统的发展历程中&#xff0c;锁机制曾是处理并发的唯一选择。传统的行级锁虽然能保证数据一致性&#x…...

初阶数据结构(C语言实现)——6.1插入排序详解(思路图解+代码实现)

目录 1 插入排序基本思想&#xff1a;2 直接插入2.1 直接插入排序思想&#xff1a;2.2 直接插入排序代码实现&#xff1a;2.2.1 单趟直接插入排序实现2.2.2 整体直接插入排序实现 3 希尔排序( 缩小增量排序 )3.1希尔排序( 缩小增量排序 )思想3.2 希尔排序代码实现3.2.1单趟排序…...

JAVA 中的 HashSet 工作原理

‌1. 底层数据结构‌ ‌依赖 HashMap 存储元素‌&#xff1a; HashSet 内部维护了一个 HashMap 实例&#xff0c;元素作为 HashMap 的 ‌Key‌ 存储&#xff0c;而所有的 ‌Value‌ 统一指向一个静态的 PRESENT 对象&#xff08;占位符&#xff09;。 // HashSet 源码片段 pri…...

mysql连接池

本文主要探讨mysql连接池的实现。 readme *****************************************************mysql连接池 *****************************************************概述&#xff1a;高并发情况下&#xff0c;大量TCP三次握手、MySQL Server连接认证、MySQL Server关闭连…...

领码科技:在低代码技术浪潮中的分享与探索

前言&#xff1a; 25年的职业生涯&#xff0c;赋予了我深厚的技术积累与实践经验。从武汉大学的工测系毕业&#xff0c;到央企副总工的职位&#xff0c;我始终站在IT浪潮的最前沿。然而&#xff0c;离开企业后&#xff0c;我并未停止前行的脚步。从2024年11月起&#xff0c;我选…...

闻所闻尽:穿透声音的寂静,照见生命的本真

在《楞严经》的梵音缭绕中&#xff0c;"闻所闻尽"四个字如晨钟暮鼓&#xff0c;叩击着每个修行者的心门。这个源自观世音菩萨耳根圆通法门的核心概念&#xff0c;既是佛门修行的次第指引&#xff0c;更蕴含着东方哲学对生命本质的终极叩问。当我们穿越时空的帷幕&…...

蓝桥与力扣刷题(蓝桥 三角形面积)

题目&#xff1a; 如上图所示。图中的所有小方格面积都是 1。 那么&#xff0c;图中的三角形面积应该是多少呢&#xff1f; 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。 解题思路&#xff0b;代码&#xff1a; 代码&…...

Linux信号:一场内核与用户空间的暗战

在Linux系统的黑暗森林中&#xff0c;每个进程都是小心翼翼的猎人。当一束神秘的信号光划过天际&#xff0c;内核瞬间变身信号调度大师&#xff0c;在进程的生死簿上书写着命运。这场跨越用户空间与内核态的博弈&#xff0c;远比表面看到的更加惊心动魄。 一、 信号诞生的量子…...

Spring Boot 异步返回对象深度解析

前言 在现代高并发、高响应的应用场景中&#xff0c;Spring Boot 的异步处理能力是提升系统吞吐量和用户体验的关键技术之一。无论是实时数据推送、大文件传输&#xff0c;还是复杂异步任务调度&#xff0c;Spring Boot 提供了多种灵活的异步处理机制以满足不同需求。本文将从…...

Android Compose 基础布局之 Box 和 Stack 源码深度剖析(九)

Android Compose 基础布局之 Box 和 Stack 源码深度剖析 一、引言 1.1 Android 开发中布局的重要性 在 Android 应用开发里&#xff0c;布局是构建用户界面&#xff08;UI&#xff09;的关键环节。良好的布局设计能够提升用户体验&#xff0c;使应用界面更加美观、易用且具有…...

【强化学习】Reward Model(奖励模型)详细介绍

&#x1f4e2;本篇文章是博主强化学习&#xff08;RL&#xff09;领域学习时&#xff0c;用于个人学习、研究或者欣赏使用&#xff0c;并基于博主对相关等领域的一些理解而记录的学习摘录和笔记&#xff0c;若有不当和侵权之处&#xff0c;指出后将会立即改正&#xff0c;还望谅…...

UE5材质法线强度控制节点FlattenNormal

连法 FlattenNormal内部是这样的 FlattenNormal的作用是用来调整法线强度 连上FlattenNormal后 拉高数值...

<项目> 主从Reactor模型的高并发服务器

目录 Reactor 概念 分类 单Reactor单线程 单Reactor多线程 多Reactor多线程 项目介绍 项目规划 模块关系 实现 TimerWheel -- 时间轮定时器 定时器系统调用 时间轮设计 通用类型Any Buffer Socket Channel Poller EventLoop&#xff08;核心&#xff09; eventfd 设计思路 …...

python爬虫解析器bs4,xpath,pquery

0x00 bs4 解析器的作用就是可以直接解析html页面&#xff0c;可以直接从网页中提取标签中的内容&#xff0c;而不用在使用正则表达式进行提起数据 import requests from bs4 import BeautifulSoup html_content <li id123><a hrefdfsdf>123</a>789</l…...

分析K8S中Node状态为`NotReady`问题

在Kubernetes&#xff08;k8s&#xff09;集群中&#xff0c;Node状态为NotReady通常意味着节点上存在某些问题&#xff0c;下面为你分析正常情况下节点应运行的容器以及解决NotReady状态的方法。 正常情况下Node节点应运行的容器 1. kubelet kubelet是节点上的核心组件&…...

【最后203篇系列】021 Q201再计划

忙了一周&#xff0c;终于到周末有时间再细细想这个问题了。这周还是不经意的弥补了kv硬盘存储库这个小空白的&#xff0c;这样也有助于构建更好的Q201。 计划是到6.1再发版&#xff0c;之所以留那么长时间&#xff0c;一方面是因为平时的确忙&#xff0c;另一方面则是可以有更…...

CA 机构如何防止中间人攻击

在现代互联网中&#xff0c;中间人攻击&#xff08;Man-in-the-Middle Attack&#xff0c;简称 MITM&#xff09;是一种常见的网络攻击方式&#xff0c;攻击者通过拦截和篡改通信双方的信息&#xff0c;进而窃取敏感数据或执行恶意操作。为了防止中间人攻击&#xff0c;证书颁发…...

CUL-CHMLFRP启动器 windows图形化客户端

CUL-CHMLFRP启动器 windows图形化客户端 基于v2 api开发的chmlfrp ui版本的第三方客户端 CUL原名CHMLFRP_UI CUL顾名思义为CHMLFRP-UI-Launcher 下载地址&#xff1a;https://cul.lanzoul.com/b00pzv3oyj 密码:ff50 下载解压运行即可&#xff08;仅支持win7以上版本&#xf…...

C语言基础08

内容提要 数组 排序算法&#xff1a;冒泡排序 二维数组 字符数组 数组 冒泡排序 排序思想&#xff08;向前冒泡&#xff09; 一次只排好一个数&#xff0c;针对n个数&#xff0c;最差情况需要n-1次就可以排好 每次排序假定第一个元素是最大或者最小&#xff0c;用第一个…...

基于javaweb的SpringBoot儿童爱心管理系统设计与实现(源码+文档+部署讲解)

技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论…...

深度学习:从零开始的DeepSeek-R1-Distill有监督微调训练实战(SFT)

原文链接&#xff1a;从零开始的DeepSeek微调训练实战&#xff08;SFT&#xff09; 微调参考示例&#xff1a;由unsloth官方提供https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen2.5_(7B)-Alpaca.ipynbhttps://colab.research.google.com/git…...

JavaScript |(五)DOM简介 | 尚硅谷JavaScript基础实战

学习来源&#xff1a;尚硅谷JavaScript基础&实战丨JS入门到精通全套完整版 笔记来源&#xff1a;在这位大佬的基础上添加了一些东西&#xff0c;欢迎大家支持原创&#xff0c;大佬太棒了&#xff1a;JavaScript |&#xff08;五&#xff09;DOM简介 | 尚硅谷JavaScript基础…...

模型整合-cherry studio+mysql_mcp_server服务配置

一、什么是MCP MCP&#xff08;Model Context Protocol&#xff09;是模型上下文协议&#xff0c;它允许大型语言模型&#xff08;LLM&#xff09;通过协议与外部工具或服务交互&#xff0c;动态获取实时数据或执行操作。简单来说&#xff0c;它让模型不再局限于静态知识库&…...

【QA】装饰模式在Qt中有哪些运用?

在Qt框架中&#xff0c;装饰模式&#xff08;Decorator Pattern&#xff09;主要通过继承或组合的方式实现&#xff0c;常见于IO设备扩展和图形渲染增强场景。以下是Qt原生实现的装饰模式典型案例&#xff1a; 一、QIODevice装饰体系&#xff08;继承方式&#xff09; 场景 …...

window 设置自动开启/关闭程序(分享)

打开计算机管理 winr 输入 compmgmt.msc 找到任务计划程序创建任务 设置开启任务 常规&#xff1a;添加名称与描述 触发器&#xff1a;新建触发时间与次数 操作&#xff1a;新建执行程序 添加任务对应的位置 以便修改 设置关闭任务 添加批处理文件&#xff0c;写完后吧 .…...

QT布局笔记

在 Qt 中&#xff0c;如果你希望将一个 QGroupBox 放置在水平布局&#xff08;QHBoxLayout&#xff09;的上方&#xff0c;可以通过将它们添加到一个垂直布局&#xff08;QVBoxLayout&#xff09;中来实现。垂直布局会将子布局或子控件按垂直顺序排列&#xff0c;因此 QGroupBo…...

【LLM大模型】LangChain学习

大模型对话 from langchain.chat_models import ChatOpenAI # 内置对话模型 from langchain.schema import HumanMessage, SystemMessage, AIMessage # 用户提示词&#xff0c;系统提示词&#xff0c; AI响应chat ChatOpenAI(temperature0.7, openai_api_keyxxxxxxxxxxxx) #…...

SpringBoot实战(三十二)集成 ofdrw,实现 PDF 和 OFD 的转换、SM2 签署OFD

目录 一、OFD 简介 1.1 什么是 OFD&#xff1f;1.2 什么是 版式文档&#xff1f;1.3 为什么要用 OFD 而不是PDF&#xff1f; 二、ofdrw 简介 2.1 定义2.2 Maven 依赖2.3 ofdrw 的 13 个模块 三、PDF/文本/图片 转 OFD&#xff08;ofdrw-conterver&#xff09; 3.1 介绍&#xf…...

SolidWorks使用显卡教程

操作步骤&#xff1a; 打开注册表编辑器 按下键盘上的 Win R 组合键&#xff0c;输入 regedit 并按回车键&#xff0c;打开注册表编辑器。 导航到显卡信息路径 在注册表中依次展开以下路径&#xff1a; plaintext HKEY_CURRENT_USER\Software\SolidWorks\SOLIDWORKS 2021\Per…...

mysql 查询进程查看并释放

在MySQL中&#xff0c;查看和管理进程&#xff08;例如查询、连接等&#xff09;是数据库维护和性能调优的重要部分。以下是一些常用的方法来查看MySQL进程并释放它们。 1. 查看进程 你可以使用SHOW PROCESSLIST命令来查看当前MySQL服务器上的所有进程。这个命令会显示正在执…...

C++代码2-多目标算法求解车辆路径规划

为了解决车辆路径规划问题,我们需要在同一模型中同时考虑多个目标,其中一个目标是降低运营总成本,而另一个目标是降低总的碳排放量。使用组合算法,包括人工蜂群算法(Artificial Bee Colony, ABC)、模拟退火算法(Simulated Annealing, SA)、以及多目标优化算法MODAD(Mu…...

JAVA学习*接口

接口 在生活中我们常听说USB接口&#xff0c;那接口是什么呢&#xff1f; 在Java中&#xff0c;接口相当于多个类的一种公共规范&#xff0c;是一种引用数据类型。 定义接口 public interface IUSB {public static final String SIZE "small";public abstract vo…...

Matplotlib

一、Matplotlib快速入门 学习目标 了解什么是matplotlib 为什么要学习matplotlib matplotlib简单图形的绘制 1、什么是Matplotlib 是专门用于开发2D图表(包括3D图表) 以渐进、交互式方式实现数据可视化 2、为什么要学习Matplotlib 可视化是在整个数据挖掘的关键辅助工…...

新版frp-0.61.0 实现泛解析域名穿透 以及 https启用

需要在公网服务器的域名解析平台 泛域名 *.aa.com 解析到frp 公网服务器的ip x.x.x.x 对于frpc.toml 文件的 serverAddr 绑定的ip 需要公网服务器放行 bindPort 对于的端口 frpc.toml serverPort 对于的的是 frps.toml bindPort 端口 frps.toml bindPort 7000 vhostHTTPP…...

HTTPS 加密过程详解

HTTPS 详解及其加密过程流程框架 HTTPS&#xff08;Hypertext Transfer Protocol Secure&#xff09;是一种基于 HTTP 协议的安全通信协议&#xff0c;通过 SSL/TLS 协议对传输数据进行加密和身份验证&#xff0c;解决了 HTTP 明文传输的安全隐患。以下是其核心原理和加密流程的…...

lua垃圾回收

lua垃圾回收 lua 垃圾回收 lua 垃圾回收 collectgarbage(“count”)获取当前lua脚本占用内存字节数(单位为KB)。 collectgarbage(“collect”)执行一次垃圾回收。 xxxnil 将变量置为空&#xff0c;会释放内存。 lua中的机制和c#中回收机制很类似 解除羁绊(置为空)。 --垃圾回…...

springboot继承使用mybatis-plus举例相关配置,包括分页插件以及封装分页类

以下是使用 MyBatis-Plus 分页插件的完整配置和封装步骤&#xff0c;包括日志输出、驼峰转下划线、逻辑删除以及分页属性类的封装。 1. 引入依赖 确保在 pom.xml 中已经引入 MyBatis-Plus 的依赖&#xff1a; <XML> <dependency><groupId>com.baomidou<…...

智能汽车以太网系统测试:聚焦ETH链路高负载性能表现

引言 在智能汽车高速发展的今天&#xff0c;车载以太网作为车辆信息交互的“神经网络”&#xff0c;承担着传输海量数据的关键使命。它不仅能够满足车辆对高带宽、低延迟和高可靠性的通信需求&#xff0c;还为自动驾驶、智能座舱等复杂功能提供了强大的技术支持。然而&#xf…...

学习笔记:黑马程序员JavaWeb开发教程(2025.3.21)

10.7 案例-员工管理-分页查询-分析 形参的默认值可以使用注解来设置&#xff1a;RequestParam(default “1”) 10.8 案例-员工管理-分页查询-PageHelper插件 分页插件PageHelper帮助完成有关分页的所有操作&#xff0c;我们只要正常使用查询语句就可以了。插件会自动…...

计算机网络精讲day1——计算机网络的性能指标(上)

性能指标1&#xff1a;速率 概念1&#xff1a;比特 英文全称是binary digit&#xff0c;意思是一个二进制数字&#xff0c;因此一个比特就是二进制数字中的1或0&#xff0c;比特也是信息论中使用的信息量单位。 概念2&#xff1a;速率 网络中的速率指的是数据的传送速率&#…...

gin-路由handler封装思路

约束handler入参和返回为func(ctx, req) (resp, error)。通过反射&#xff0c;封装handler&#xff0c;在调用前后写入入参和返回的处理。 package testingimport ("context""fmt""reflect""strings""testing" )type ReqPa…...

【实战案例】用STAR+3W模型拆解电商支付系统设计文档

各位开发者朋友&#xff0c;上次分享了结构化写作的黄金公式后&#xff0c;很多同学反馈需要更具象的落地方法。今天通过真实电商支付系统案例&#xff0c;手把手教你用STAR3W模型写出可执行的设计文档&#xff01; 结构化写作的「黄金公式」 STAR原则 3W模型 Situation&…...

计算机组成原理和计算机网络常见单位分类及换算

计算机组成原理&#xff08;主要用于存储、内存、缓存等&#xff09; 计算机网络&#xff08;主要用于传输速率&#xff09; 直观对比...

线性筛法求素数

时间复杂度 o&#xff08;n&#xff09; int cnt, primes[N];//cnt用来记录素数的下标 bool st[N];//用来标记合数 int minp[N];//最小质因数 void get_primes(int n) {for(int i 2;i < n;i )//从2开始找数 {if(!st[i])//如果这个数没有被筛出去过&#xff0c;说明是一…...

触动精灵对某东cookie读取并解密--记lua调用C语言

在Mac上构建Lua扩展模块&#xff1a;AES解密与Base64解码实战 今天我要分享一个实用技术&#xff1a;如何在Mac系统上为Lua编写和编译C扩展模块&#xff0c;特别是实现一个某东iOS PIN码解密功能的扩展。这对于需要在Lua环境中执行高性能计算或使用底层系统功能的开发者非常有…...