【C++】---继承

【C++】---继承

  • 一、继承的概念及定义
    • 1、继承的概念
    • 2、定义语法格式
    • 3、继承基类成员访问方式的变化
  • 二、基类 和 派生类 的对象之间的赋值转换
    • 1、赋值规则
    • 2、切片
      • (1)子类对象 赋值 给 父类对象
      • (2)子类对象 赋值 给 父类指针
      • (3)子类对象 赋值 给 父类引用
  • 三、继承中的作用域
    • 1、父类和子类都有独立的作用域
    • 2、隐藏的概念
    • 3、如果是成员函数的隐藏,只需要函数名相同 就构成 隐藏
    • 4、继承中最好不要定义同名成员
  • 四、派生类的默认成员函数
    • 1、规则①
    • 2、规则②
    • 3、规则③
    • 4、规则④
  • 五、继承和友元
  • 六、继承和静态成员
  • 七、菱形继承
    • 1、单继承
    • 2、多继承
    • 3、菱形继承
  • 八、菱形虚拟继承
    • 1、虚继承
    • 2、虚继承的解决数据冗余 和 二义性的原理
  • 九、继承和组合

一、继承的概念及定义

1、继承的概念

继承的概念:

继承机制是面向对象程序设计当中使代码复用的重要手段,它允许程序员,在保持原有类特性的基础上,进行拓展,增加功能,产生新的类,这个新的类就叫做:派生类。

继承的出现,呈现出面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

以前我们所接触到的复用都是函数的复用,而继承的出现则是体现了:类设计层次的复用。
在这里插入图片描述

2、定义语法格式

在这里插入图片描述

#include<iostream>
using namespace std;// 基类
class Person
{
public:void Print(){cout << "name->" << _name << endl;cout << "age->" << _age << endl;}
protected:string _name = "peter";int _age = 18;
};// 派生类:stu
class Student :public Person
{
protected:int stu_ID;
};
// 派生类:tea
class Teacher :public Person
{
protected:int job_ID;
};int main()
{Student s;Teacher t;s.Print();t.Print();return  0;
}

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

在这里插入图片描述
不需要记住总结上面的表
我给大家总结两点,只需要记住下面两点规律,一切都好说:

(1)基类private成员在派生类中无论以什么方式继承都是不可见的

(2)基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),访问权限,由大到小:public>protected>private。(Min(成员在基类的访问限定符,继承方式):意思是 取 两者访问权限较小的!)


注意:基类private成员在派生类中无论以什么方式继承都是不可见的

关于什么叫做不可见:

假如将父类成员改为private,那么子类继承了父类的私有成员,内存上,子类对象有这个成员,但是语法上不允许访问
可以理解为父类就是你的父亲,而父类的私有成员就是你父亲藏的私房钱。子类就是你自己,不可见的意思,就是说:你知道你父亲藏有私房钱,但是你找不到你见不到!

二、基类 和 派生类 的对象之间的赋值转换

1、赋值规则

(1)派生类对象可以赋值给:基类对象 / 基类的指针 / 基类的引用。这里有一个形象的说法叫做切割或者切片,就是将派生类中父类那一部分切割下来,赋值过去。(子可以给父)

(2)基类对象不能赋值给派生类对象。(父不能给子)

(3)基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTimeType Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面再讲解,这里先了解一下)

2、切片

(1)子类对象 赋值 给 父类对象

子可以赋值给父,但 父不可以赋值给子:
在这里插入图片描述

// 基类
class Person
{
public:void Print(){cout << "name->" << _name << endl;cout << "age->" << _age << endl;}string _name = "peter";int _age = 18;
};// 派生类:stu
class Student :public Person
{
public:int stu_ID;
};int main()
{Student s;// s是一个 子类对象Person p;// p是一个 父类对象s._name = "yjl";s._age = 20;s.stu_ID = 20221071;p = s;s = p;return  0;
}

在这里插入图片描述

(2)子类对象 赋值 给 父类指针

在这里插入图片描述

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


// 基类
class Person
{
public:void Print(){cout << "name->" << _name << endl;cout << "age->" << _age << endl;}string _name = "peter";int _age = 18;
};// 派生类:stu
class Student :public Person
{
public:int stu_ID;
};int main()
{Student s;// s是一个 子类对象//Person p;// p是一个 父类对象Person* p;// p是一个 父类对象指针s._name = "yjl";s._age = 20;s.stu_ID = 20221071;p = &s;return  0;
}

在这里插入图片描述
② 若父类指针要赋值给子类指针,必须将父类指针强转为子类指针:

int main()
{//父类指针赋值给子类指针Person p;//父类对象Student *s;//子类指针p._name = "yjl";p._sex = "male";p._age = 6;Person* pp = &p;//父类指针s = (Student*)pp;//将父类指针强转为子类型指针后,再赋值给子类型指针return 0;
}

③ 指向父类对象的父类指针,强转为子类指针后赋值给子类指针可能会造成访问越界

int main()
{//指向父类对象的父类指针,强转为子类指针后赋值给子类指针可能会造成访问越界Person p;p._name = "yjl";p._sex = "male";p._age = 7;//s._No = 21530;Person* pp = &p;Student* ss = (Student*)pp;//ss->_No = 6;访问越界造成程序崩掉return 0;
}

(3)子类对象 赋值 给 父类引用

// 基类
class Person
{
public:void Print(){cout << "name->" << _name << endl;cout << "age->" << _age << endl;}string _name = "peter";int _age = 18;
};// 派生类:stu
class Student :public Person
{
public:int stu_ID;
};int main()
{Student s;// s是一个 子类对象//Person p;// p是一个 父类对象//Person* p;// p是一个 父类对象指针s._name = "yjl";s._age = 20;s.stu_ID = 20221071;Person& p=s;// p是一个 父类对象引用return  0;
}

在这里插入图片描述

三、继承中的作用域

1、父类和子类都有独立的作用域

2、隐藏的概念

(1)子类和父类中有同名成员,子类成员将屏蔽父类,对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)

(2)通过代码来演示:隐藏!

class Person
{
public:string _name = "yjl";int _age = 20;void Print1(){cout << "name->" << _name << endl;cout << "age->" << _age << endl;}
};class Student :public Person
{
public:int _age = 100;void Print2(){cout << "age->" << _age << endl;}
};int main()
{Person p;Student s;s.Print2();return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3、如果是成员函数的隐藏,只需要函数名相同 就构成 隐藏

// 父类
class Person
{
public:string _name = "yjl";int _age1 = 20;void Print(){cout << "name->" << _name << endl;cout << "age->" << _age1 << endl;}
};
// 子类
class Student :public Person
{
public:int _age2 = 100;void Print(int count){while (count){cout << "age->" << _age2 << endl;count--;}}
};
int main()
{Person p;Student s;// 这里的s.Print();与父类的Print  构成函数重载×××,构成函数隐藏√√√// 父子类函数构成隐藏,只需要满足一点即可(函数名相同)s.Print();// 这里认为你用子类对象调用的是子类的打印函数,而不是你想要调用的父类打印函数。return 0;
}

在这里插入图片描述

4、继承中最好不要定义同名成员

四、派生类的默认成员函数

(1)什么叫做默认成员函数?

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

(2)派生类的默认成员函数和普通类的默认成员函数的不同区别:

  1. 派生类的构造函数必须要调用基类的构造函数来初始化基类的那一部分成员。如果基类没有写默认的构造函数,那么就必须要在派生类的初始化列表里面对基类的构造函数进行显示调用。(构造:先父后子)
  2. 派生类的拷贝构造函数必须要调用基类的拷贝构造函数,来进行对基类成员的拷贝构造。
  3. 派生类的operator=必须要调用基类的operator=完成基类的赋值
  4. 派生类的析构函数,会在被调用完成之后,自动调用基类的析构函数来清理基类的成员。因为这样才能保证析构函数作用的时候是先析构子类的成员,最后再析构父类的成员的顺序(析构:先子后父)
  5. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加显示调用的情况下,子类析构函数和父类析构函数构成隐藏关系。

总之:派生类=基类+派生类成员(①把基类看作是一个自定义类型的整体->无论是构造机构,拷贝构造,基类就调用自己的默认写的!②派生类成员看作一个普通的类!)

// 父类
class Person
{
public: 构造//Person(const char* name = "peter")//	:_name(name)//{//	cout << "Person()" << endl;//} 拷贝构造//Person(const Person& p)//	:_name(p._name)//{//	cout << "Person(const Person& p)" << endl;//}// 析构~Person(){cout << "~Person()" << endl;}// 赋值运算符重载Person& operator=(const Person& p){cout << "Person& operator=()" << endl;if (this != &p){_name = p._name;}return *this;}
protected:string _name;
};// 子类
class Student :public Person
{
public:// 构造Student(const char* name,int num):Person(),_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=" << endl;if (this != &s){Person::operator=(s);_num = s._num;}return *this;}// 析构~Student(){cout << "~Student()" << endl;}
protected:int _num;// 学号
};void test()
{Person p1;Student s1("yjl", 2022);//Student s2("yjl666", 2024);//Student s3(s1);//Person p2(s3);}int main()
{test();return 0;
}

1、规则①

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

(1)回忆一下什么是默认构造函数?

  1. 我们没有写编译器,自动生成的。
  2. 我们自己写了无参的构造函数。
  3. 我们自己写了的,带参的全缺省构造函数。

(2)父类里面没有写默认的构造函数和拷贝构造函数:
在这里插入图片描述
当父类没有写默认的构造函数的时候,显式调用父类默认构造函数,是把当作一个整体去调父类的构造函数!
(3)父类里面写了默认的构造函数:

在这里插入图片描述
总结:
(1)父类有默认构造函数,子类只需要在构造函数中初始化子类自己的成员
(2)父类没有默认构造函数,子类要在构造函数初始化列表显式调用父类的构造函数,再初始化子类自己的成员

2、规则②

规则②:子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。

子类的拷贝构造函数该如何写呢?

调用父类拷贝构造函数时,将子类对象作为参数传给父类拷贝构造函数,这时会发生切片行为,将子类对象中父类成员部分切片赋值给父类成员,完成父类拷贝构造;再将子类自己的成员初始化为拷贝对象的子类成员值:

3、规则③

规则③ 子类的operator=必须要调用父类的operator=完成父类的赋值

如果子类的赋值运算符重载函数里只调用子类的operator=,那么会发生栈溢出:

// 赋值运算符重载Student& operator=(const Student& s){cout << "Student& operator=" << endl;if (this != &s){operator=(s);_num = s._num;}return *this;}void test()
{//Person p1;Student s1("yjl", 2022);Student s2("yjl666", 2024);s1 = s2;// 赋值的时候,会发生:栈溢出//Person p2(s3);}

在这里插入图片描述
这样写才对:

	// 赋值运算符重载Student& operator=(const Student& s){cout << "Student& operator=" << endl;if (this != &s){Person::operator=(s);_num = s._num;}return *this;}

4、规则④

规则④ 子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序
1、
(1)析构顺序:先子后父!

(2)构造顺序:先父后子!

在这里插入图片描述
2、注意:由于多态的缘故,对析构的统一处理

// 对析构的统一处理:~Student(){//Person::~Person();~Person();cout << "~Student()" << endl;}

在这里插入图片描述
这是由于父类的析构函数和子类的析构函数构成隐藏,奇不奇怪?

按照隐藏的定义,父类和子类的同名函数才会构成隐藏,为什么父类和子类的析构函数不同名也会构成隐藏呢?

这是因为多态的缘故,任何类的析构函数名都会被统一处理成destructor( )。所以父类和子类析构函数同名,会构成隐藏。解决方法就是加父类作用域,指定访问的是父类的析构函数就可以了:

	// 析构~Student(){Person::~Person();//~Person();cout << "~Student()" << endl;}

在这里插入图片描述

五、继承和友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

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;
}
void main()
{Person p;Student s;Display(p, s);
}

六、继承和静态成员

如果在父类里面定义了一个static静态成员,那么在整个继承体系当中,无论父类派生出多少个子类,整个体系只有这一个静态成员!

下面这些代码是对静态成员变量的诠释,在整个继承体系中,只要有static静态成员变量,那么它就是同一个!

#include<iostream>using namespace std;class Person
{
public:Person() { _count++; }
protected:string _name;
public:static int _count;
};int Person::_count = 0;class Student : public Person
{
protected:int _ID;
};class Graduate : public Student
{
protected:string _course;
};void TestPerson()
{Student s1;Student s2;Student s3;Graduate s4;// 在上面的继承体系中,它是单继承。// 创建子类对象的时候必须要先构造父类对象,所以进行到此处_count等于4。cout << "人数:" << Person::_count << endl;Student::_count = 0;// 因为已经说明了_count是一个静态的,在整个继承体系中它就是一个整体就是一个// 对_count修改后他就是0了。cout << "人数:" << Person::_count << endl;
}int main()
{TestPerson();return 0;
}

在这里插入图片描述

七、菱形继承

1、单继承

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

在这里插入图片描述

2、多继承

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

在这里插入图片描述

3、菱形继承

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

在这里插入图片描述

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 _course;
};void Test()
{Assistant a;a._name = "peter";// 这样会有二义性无法明确知道访问的是哪一个_name// 因为,a访问的_name没有指明是student继承person的_name,还是teacher继承person的_name。// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决a.Student::_name = "xxx";a.Teacher::_name = "xxx";
}

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

在这里插入图片描述

八、菱形虚拟继承

虚拟继承:可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。
需要注意的是,虚拟继承不要在其他地方去使用。

1、虚继承

虚继承的语法格式:在两个直接父类的继承方式访问限定符前加vitual:

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 _course;
};void Test()
{Assistant a;a._name = "peter";/*a.Student::_name = "xxx";a.Teacher::_name = "xxx";*/
}int main()
{Test();return 0;
}

在这里插入图片描述

2、虚继承的解决数据冗余 和 二义性的原理

虚拟继承解决数据冗余和二义性的原理:

为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型

class A
{
public:int _a;
};
// class B : public A
class B : virtual public A
{
public:int _b;
};
// class C : public A
class C : virtual 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;
}

下图是菱形继承的内存对象成员模型:这里可以看到数据冗余:
在这里插入图片描述

下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。

在这里插入图片描述
下面是上面的Person关系菱形虚拟继承的原理解释:

在这里插入图片描述

九、继承和组合

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
  3. 继承和组合:

(1)public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。

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

(3)优先使用对象组合,而不是类继承 :链接:优先使用对象组合,而不是类继承

(4)实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。

// Car和BMW Car和Benz构成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;}};// Tire和Car构成has-a的关系class Tire{protected:string _brand = "Michelin";  // 品牌size_t _size = 17;         // 尺寸};class Car{protected:string _colour = "白色"; // 颜色string _num = "陕ABIT00"; // 车牌号Tire _t; // 轮胎};

好了,今天的分享就到这里了
如果对你有帮助,记得点赞👍+关注哦!
我的主页还有其他文章,欢迎学习指点。关注我,让我们一起学习,一起成长吧!

在这里插入图片描述

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

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

相关文章

java多线程编码应用1——java多线程CompletableFuture使用技巧

在实际项目开发过程中&#xff0c;大部分程序的执行顺序都是按照代码编写的先后顺序&#xff0c;依次从上往下挨个执行的&#xff0c;但是对于统计或者批量操作数据时&#xff0c;是否有更好的方案呢&#xff1f;这时候就可以考虑使用多线程编程&#xff0c;异步并行执行多个任…

【管理咨询宝藏95】SRM采购平台建设内部培训方案

本报告首发于公号“管理咨询宝藏”&#xff0c;如需阅读完整版报告内容&#xff0c;请查阅公号“管理咨询宝藏”。 【管理咨询宝藏95】SRM采购平台建设内部培训方案 【格式】PDF版本 【关键词】SRM采购、制造型企业转型、数字化转型 【核心观点】 - 重点是建设一个适应战略采…

数据挖掘流程是怎样的?数据挖掘平台基本功能有哪些?

数据挖掘是从大量的、不完全的、有噪声的、模糊的、随机的数据中提取隐含在其中的、人们事先不知道的、但又是潜在有用的信息和知识的过程。 数据挖掘的流程是&#xff1a; 清晰地定义出业务问题&#xff0c;确定数据挖掘的目的。 数据准备: 数据准备包括&am…

机器学习 | 时间序列预测中的AR模型及应用

自回归模型&#xff0c;通常缩写为AR模型&#xff0c;是时间序列分析和预测中的一个基本概念。它们在金融、经济、气候科学等各个领域都有广泛的应用。在本文中&#xff0c;我们将探索自回归模型&#xff0c;它们如何工作&#xff0c;它们的类型和实际例子。 自回归模型 自回…

2024 GESP6级 编程第一题 游戏

题目描述 你有四个正整数 &#xff0c;并准备用它们玩一个简单的小游戏。 在一轮游戏操作中&#xff0c;你可以选择将 减去 &#xff0c;或是将 减去 。游戏将会进行多轮操作&#xff0c;直到当 时游戏结束。 你想知道游戏结束时有多少种不同的游戏操作序列。两种游戏操作…

虚拟机jvm下

jvm原理与实践 java程序的跨平台特性 jvm基本结构 JVM类加载流程和内存结构总览 类加载 加载阶段 类加载 验证阶段 类加载 准备阶段 类加载 解析阶段 类加载 初始化阶段 程序计数器 虚拟机栈&本地方法栈 栈帧操作 堆 方法区 永久代 元空间 垃圾回收 可触及性

python与java用途区别有哪些

区别&#xff1a; 1.Python比Java简单&#xff0c;学习成本低&#xff0c;开发效率高。 2.Java运行效率高于Python&#xff0c;尤其是纯Python开发的程序&#xff0c;效率极低。 3.Java相关资料多&#xff0c;尤其是中文资料。 4.Java版本比较稳定&#xff0c;Python2和3不…

笔记2:cifar10数据集获取及pytorch批量处理

&#xff08;1&#xff09;cifar10数据集预处理 CIFAR-10是一个广泛使用的图像数据集&#xff0c;它由10个类别的共60000张32x32彩色图像组成&#xff0c;每个类别有6000张图像。 CIFAR-10官网 以下为CIFAR-10数据集data_batch_*表示训练集数据&#xff0c;test_batch表示测试…

Ubuntu22.04下安装kafka_2.11-0.10.1.0并运行简单实例

目录 一、版本信息 二、安装Kafka 1.将Kafka安装包移到下载目录中 2.下载Spark并确保hadoop用户对Spark目录有操作权限 三、启动Kafka并测试Kafka是否正常工作 1.启动Kafka 2.测试Kafka是否正常工作 一、版本信息 虚拟机产品&#xff1a;VMware Workstation 17 Pro 虚…

C++从入门到精通---模版

文章目录 泛型编程函数模版模版参数的匹配原则类模版类模版的定义格式类模版的实例化 总结 泛型编程 泛型编程是一种编程范式&#xff0c;旨在实现通用性和灵活性。它允许在编写代码时使用参数化类型&#xff0c;而不是具体的类型&#xff0c;从而使代码更加灵活和可重用。 在…

Autoxjs 实践-Spring Boot 集成 WebSocket

概述 最近弄了福袋工具&#xff0c;由于工具运行中&#xff0c;不好查看福袋结果&#xff0c;所以我想将福袋工具运行数据返回到后台&#xff0c;做数据统计、之后工具会越来越多&#xff0c;就弄了个后台&#xff0c;方便管理。 实现效果 WebSocket&#xff1f; websocket是…

图像处理之SVD检测显示屏缺陷(C++)

图像处理之SVD检测显示屏缺陷&#xff08;C&#xff09; 文章目录 图像处理之SVD检测显示屏缺陷&#xff08;C&#xff09;前言一、SVD算法简介二、代码实现总结 前言 显示屏缺陷检测是机器视觉领域的一处较广泛的应用场景&#xff0c;显示屏主要有LCD和OLED&#xff0c;缺陷类…