全景网站制作教程游戏制作软件app手机下载

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

全景网站制作教程,游戏制作软件app手机下载,知名企业名字,网站建设在哪【C】—— 继承#xff08;上#xff09; 1 继承的概念与定义1.1 继承的概念1.2 继承定义1.2.1 定义格式1.2.2 继承父类成员访问方式的变化 1.3 继承类模板 2 父类和子类对象赋值兼容转换3 继承中的作用域3.1 隐藏规则3.2 例题 4 子类的默认成员函数4.1 构造函数4.1.1 父类有… 【C】—— 继承上 1 继承的概念与定义1.1 继承的概念1.2 继承定义1.2.1 定义格式1.2.2 继承父类成员访问方式的变化 1.3 继承类模板 2 父类和子类对象赋值兼容转换3 继承中的作用域3.1 隐藏规则3.2 例题 4 子类的默认成员函数4.1 构造函数4.1.1 父类有默认构造4.1.2 父类没有默认构造 4.2 拷贝构造4.2.1 不需要自己显式写4.2.2 自己显式写 4.3 赋值重载4.4 析构函数4.4.1 重载4.4.2 顺序 4.5 实现不能被继承的类4.5.1 法一设为私有4.5.2 法二final 4.6 总结 1 继承的概念与定义 1.1 继承的概念 继承inheritance机制是面向对象设计使代码可以复用的最重要的手段它允许我们在保存原有特性的基础上进行扩展增加方法成员函数和属性成员变量这样产生新的类称派生类。继承呈现了面向对象程序设计的层次结构体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用继承是类设计层次的复用。 下面我们通过一个例子来初步感受一下继承 class Student {public :// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证void identity(){// …} // 学习void study(){// …} protected:string _name peter; // 姓名string _address; // 地址string _tel; // 电话int _age 18; // 年龄int _stuid; // 学号 };class Teacher { public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证void identity(){// …} // 授课void teaching(){//…} protected:string _name 张三; // 姓名int _age 18; // 年龄string _address; // 地址string _tel; // 电话string _title; // 职称 };上面我们看到没有继承之前我们设计了两个类 Student 和 Teacher S t u d e n t Student Student 和 T e a c h e r Teacher Teacher 都有 姓名 / 地址 / 电话 / 年龄 等成员变量都有 i d e n t i t y identity identity ⾝份认证的成员函数设计到两个类里面就是冗余的。当然他们也有⼀些独有的成员变量和函数比如老师独有成员变量是职称学生的独有成员变量是学号学⽣的独有成员函数是学习⽼师的独有成员函数是授课。 既然 S t u d e n t Student Student 和 T e a c h e r Teacher Teacher 两个类的设计有些冗余那我们能不能把公共的信息提取出来呢 下面我们公共的成员都放到 Person 中Student 和 Teacher 都继承 Person就可以复⽤这些成员就不需要重复定义了省去了很多麻烦。 class Person { public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证void identity(){cout void identity() _name endl;} protected:string _name 张三; // 姓名string _address; // 地址string _tel; // 电话int _age 18; // 年龄 };class Studen : public Person { public:// 学习void study(){// …} protected:int _stuid; // 学号 };class Teacher : public Person { public:// 授课void teaching(){//…} protected:string title; //职称 };虽然 S t u d e n t Student Student类 的成员变量看起来只有int _stuid;但它继承了Person类它还有string _name 、string _address;等等成员变量。成员函数也不止void study()还有void identity()       1.2 继承定义 1.2.1 定义格式 下面我们看到 Person 是父类也称作基类。Student 是子类也称作派生类。因为翻译的原因所以既叫父类/子类也叫基类/派生类 继承方式与访问限定符一样都有三个公有、保护、私有 1.2.2 继承父类成员访问方式的变化 类成员/继承方式 p u b l i c public public 继承 p r o t e c t e d protected protected 继承 p r e v a t e prevate prevate 继承基类的 p u b l i c public public 成员派生类的public成员派生类的 p r o t e c t e d protected protected 成员派生类的 p r i v a t e private private 成员基类的 p r o t e c t e d protected protected 成员派生类的 p r o t e c t e d protected protected 成员派生类的 p r o t e c t e d protected protected 成员派生类的 p r i v a t e private private 成员基类的 p r i v a t e private private 成员在派生类中不可见在派生类中不可见在派生类中不可见 父类的 private成员 在子类中无论以什么方式继承都是不可见的。这里的不可见是指父类的私有成员还是被继承到了子类对象中但是语法上限制子类对象不管在类里面还是类外我们都不能去访问它 子类想访问父类的 p r i v a t e private private 成员虽然不能直接访问但能间接访问。虽然在子类中是不能访问但在父类中并没有相关限制只用父类提供相关访问 p r i v a t e private private 成员变量的成员函数子类调用其函数就能间接访问。 父类 p r i v a t e private private 成员在子类中是不能被访问的如果父类成员不想在类外直接被访问但需要在子类中能访问就定义为 protected。可以看出保护成员限定符是因继承才出现的。 实际上面的表格我们进行一下总结会发现父类的私有成员在子类都是不可见。父类其他成员在子类的访问方式为 M i n Min Min成员在父类的访问限定符 继承方式 p u b l i c public public   p r o t e c t e d protected protected   p r i v a t e private private。 使用关键字 class 时默认的继承方式是private使用 struct 时默认的继承方式是 public不过最好显式的写出继承方式 class Student:Person //默认为private继承、struct Student:Person //默认为public继承 在实际运用中一般使用的都是 p u b l i c public public 继承几乎很少使用 p r o t e c t e d protected protected / p r i v a t e private private 继承也不提倡使用 p r o t e c t e d protected protected / p r i v a t e private private 继承因为 p r o t e c t e d protected protected / p r i v a t e private private 继承下来的成员都只能在子类的类里面使用实际中扩展维护性不强。这里可以认为是 C 过度设计了。 看起来上面的规则很复杂实际实践过程中是很简单的一般都是父类我们就用公有和保护继承方式我们就用公有。其他方式都很少使用。    1.3 继承类模板 上述都是一些普通类的继承那如果我们想继承类模板又该怎样呢 之前我们模拟实现栈使用的适配器模式其实还有一种方法继承 namespace ganyu {templateclass Tclass stack : public std::vectorT{public:void push(const T x){push_back(x);}void pop(){vectorT::popback();}const T top(){return vectorT::back();}bool empty(){return vectorT::empty();}}; }当基类是类模板时需要指定类域去访问否则会编译报错。普通类的继承不存在这个问题 int main() {ganyu::stackint st;st.push(1);st.push(2);st.push(3);while (!st.empty()){cout st.top() ;st.pop();}return 0; }为什么编译报错呢这与按需实例化有关系 ganyu::stackint st;这句代码实例化栈将 T T T 实例化成 i n t int int也间接将 v e c t o r vector vector 实例化严格来说只实例化了栈的构造函数。但我们将 v e c t o r vector vector 实例化时不会把 v e c t o r vector vector 中所有的成员函数都实例化我们调用谁才实例化谁。   我们调用 p u s h push push 函数时编译器去找 p u s h push push b a c k back back 函数在子类和父类中都找不到因为还没有实例化。所以我们要指定类域去访问表示调用的是 v e c t o r vector vector T T T 中的 p u s h push push_ b a c k back back此时编译器看到 T T T 已经被实例化成 i n t int int 了就会将 v e c t o r vector vector T T T 中的 p u s h push push_ b a c k back back 实例化出一份 i n t int int 版本的出来。 我们可以结合 #define能灵活更改 s t a c k stack stack 的底层容器达到类似适配器模式的效果 #define CONTAINER vectornamespace ganyu {templateclass Tclass stack : public std::CONTAINERT{public:void push(const T x){CONTAINERT::push_back(x);}void pop(){CONTAINERT::pop_back();}const T top(){return CONTAINERT::back();}bool empty(){return CONTAINERT::empty();}}; }2 父类和子类对象赋值兼容转换 p u b l i c public public继承的前提下子类对象可以赋值给父类的对象 / 父类的指针 / 父类的引用。这里有个形象的说法叫切片或者切割。寓意把子类中父类那部分切割开来赋值给父类对象/指针/引用但反过来就不成立父类对象不能赋值给子类对象 例如现在有一个 S t u d e n t Student Student 对象 S t u d e n t Student Student 对象可以赋值给父类对象 P e r s o n Person Person当然指针和引用也是可以的但反过来就不成立总不能无中生有出一个 _ N o No No 成员吧。 class Person { protected :string _name; // 姓名string _sex; // 性别int _age; // 年龄 };class Student : public Person { public :int _No; // 学号 };int main() {Student sobj;// 1.派⽣类对象可以赋值给基类的对象/指针/引⽤Person pobj sobj;Person* pp sobj;Person rp sobj;//2.基类对象不能赋值给派⽣类对象这⾥会编译报错//sobj pobj;return 0; }这里并没有发生类型转换。   虽然我们前面讲过不同类型的对象之间进行赋值支持的是类型转换 int i 0; double d i;将 i i i 赋值给 d d d 走的就是类型转换中间会生成一个临时对象。   但是切片并不是类型转换中间并没有产生临时变量这是一种特殊处理。 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型可以使用RTTI(Run-Time Type Information)的 dynamic_cast 来进行识别后进行安全转换。ps这个我们后面再单独专门介绍这里先提⼀下       3 继承中的作用域 3.1 隐藏规则 在继承体系中父类和子类都有独立的作用域子类和父类中有同名成员子类成员将屏蔽父类的同名成员的直接访问这种情况叫 隐藏。在子类成员函数中可以使用父类::父类成员 显式访问 class Person { protected :string _name 小帅; // 姓名int _num 111; // ⾝份证号 }; class Student : public Person { public :void Print(){cout 姓名: _name endl;cout 身份证号: Person::_num endl;//指定父类的类域进行访问cout 学号: _num endl;} protected:int _num 999; // 学号 };int main() {Student s1;s1.Print();return 0; }运行结果 如果是成员函数的隐藏只需要函数名相同就构成隐藏注意在实际中在继承体系里面最好不要定义重名的成员或函数 3.2 例题 class A {public :void fun(){cout func() endl;} }; class B : public A {public :void fun(int i){cout func(int i) i endl;} }; int main() {B b;b.fun(10);b.fun();return 0; };A A A 和 B B B 类中的两个 f u n c func func函数 构成什么关系 A. 重载   B. 隐藏  C.没关系 下面程序的编译运行结果是什么 A. 编译报错  B. 运行报错  C. 正常运行
第一题第一眼看上去他们构成重载关系函数名相同参数类型不同。但如果选 A 就错了这题选B。别忘了只有在同一作用域的函数才构成函数重载而隐藏是父类和子类中的函数名相同就构成隐藏第二题选A因为子类和父类的 f u n c func func函数 构成隐藏除非指定父类的作用域去调用否则同名成员或函数是不会去父类中查找的。b.fun(); 没有传递参数编译报错。 4 子类的默认成员函数 6 个默认成员函数意思是我们不写编译器会给我们自动生成。父类的默认成员函数与普通类没有任何差别但在派生类中这几个成员函数是如何生成的呢 4.1 构造函数 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果没有默认的构造函数则必须在派生类构造函数的初始化列表阶段显式调用。 4.1.1 父类有默认构造 class Person { public:Person(const char* name peter): _name(name){cout Person() endl;}protected:string _name; // 姓名 };class Student : public Person { public:protected:int _num; //学号string _sex; //性别 };首先我们来回忆一下普通类的默认生成的构造函数的行为 对内置类型默认生成的构造函数是不确定的对自定义类型会调用它的默认构造函数 现在比起之前多出来一部分父类成员 我们把继承的父类成员看成一个整体对象子类的默认构造会自动调用父类的默认构造完成父类成员的初始化 4.1.2 父类没有默认构造 class Person { public:Person(const char* name, double height): _name(name),_height(height){cout Person() endl;}protected:string _name; // 姓名double _height; //身高 };class Student : public Person { public:protected:int _num; //学号string _sex; //性别 };现在父类没有默认构造派生类还能默认生成构造函数吗. 可见默认生成的只能调用默认构造。这时就需要我们在子类显式写一个构造函数了 Student(const char* name, double height, int num, const char* sex):_name(name),_height(height),_num(num),_sex(sex) {}这样写可不可以呢   不可以。编译器不允许直接去初始化父类的成员子类要求必须调用父类的构造函数来初始化父类的成员要把父类当成一个整体。    显示调用父类方法如下 Student(const char* name, double height, int num, const char* sex):Person(name, height),_num(num),_sex(sex) {}int main() {Student s1(张三, 1.80, 1, 男);Student s2(李四, 1.70, 2, 未知);return 0; }有点像调用一个匿名对象一样。 4.2 拷贝构造 派生类的拷贝构造函数必须调用基类的拷贝构造完成基本的拷贝初始化    对默认生成的拷贝构造其行为也像上述构造函数一样分成三类 内置类型完成浅拷贝自定义类型调用其拷贝构造父类整体调用父类的拷贝构造 4.2.1 不需要自己显式写 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;}protected:string _name; // 姓名 };class Student : public Person { public://构造函数Student(const char* name, int num, const char* sex):Person(name), _num(num), _sex(sex){}//未写拷贝构造//···protected:int _num; //学号string _sex; //性别 };int main() {Student s1(张三, 1, 男);Student s2 s1;return 0; }严格来说 S t u d e n t Student Student类是不用我们自己写拷贝构造的默认生成的拷贝构造已经完成了我们的需求。前面我们通过学习知道拷贝构造、赋值重载、析构是一体的。一个不需要写三个都不需要写一个要写三个都要写。因此 S t u d e n t Student Student类 的赋值重载和析构函数都不需要自己写   如果有需要深拷贝的资源才需要自己实现 4.2.2 自己显式写 那假设 S t u d e n t Student Student 类中有指向的资源需要我们自己写拷贝构造又该怎么写呢 class Student : public Person { public:protected:int _num; //学号string _sex; //性别int* _ptr new int[10];//假设有指向的资源 };Student (const Student s):_num(s._num),_sex(s._sex),//显示调用父类的拷贝构造 {//深拷贝memcpy(_ptr, s._ptr, sizeof(int) * 10); }如何显式调用父类的拷贝构造呢 调用父类的拷贝构造需要传递父类的对象但现在没有父类的对象咋办呢   这时我们就可以运用前面学习的赋值兼容转换 Student(const Student s):_num(s._num),_sex(s._sex),Person(s) {//深拷贝memcpy(_ptr, s._ptr, sizeof(int) * 10); }Person(s) s s s 是子类对象的引用要拷贝父类那一部分需要将父类那一部分拿出来 怎么拿出来呢我把子类对象传给父类的引用这时父类的引用引用的是子类对象中切割出来的父类的那一部分。 这里有个小细节走初始化列表时编译器会先走Person(s)在走_num(s._num)和_sex(s._sex)   这是因为初始化列表初始化的顺序与成员在列表中的顺序无关只与声明的顺序有关。   所以继承以后它将父类对象当成一个整体而父类对象是最先被声明的 那如果不在初始化列表显示初始化父类呢 Student(const Student s):_num(s._num), _sex(s._sex) {//深拷贝 }我们说过所有成员都会走初始化列表父类 P e r s o n Person Person 没有显示调用也会走初始化列表。但此时编译器会调用 P e r s o n Person Person 的默认构造虽然编译能通过但很可能不符合你的需求如果 P e r s o n Person Person 没有默认构造那么编译报错 4.3 赋值重载 和拷贝构造一样 S t u d e n t Student Student 类严格来说不需要写赋值。   但如果我们需要显式写要怎么写呢 派生类的 o p e r a t o r operator operator 必须要调用基类的 o p e r a t o r operator operator 。 Student operator(const Student s) {if (this ! s){operator(s);_num s._num;_sex s._sex;}return *this; }复制拷贝与拷贝构造是类似的都是传递子类对象的引用给父类即可。 但是如果运行程序会发现程序陷入死循环。   为什么呢   子类中的同名函数与父类的构成了隐藏   operator(s);其实一直调的是子类的 o p e r a t o r operator operator因此程序陷入死循环 因此我们要指定调用定父类的 o p e r a t o r operator operator。 Student operator(const Student s) {if (this ! s){Person::operator(s);_num s._num;_sex s.sex;}return *this; }总结派生类的 o p e r a t o r operator operator 必须要调用基类的 o p e r a t o r operator operator 完成基类的赋值。需要注意的是派生类的 o p e r a t o r operator operator 屏蔽了基类的 o p e r a t o r operator operator所以显式调用基类的 operator需要指定基类作用域 4.4 析构函数 首先严格来说 S t u d e n t Student Student 并不需要我们显式写析构函数   那如果有需要显式释放的资源析构函数又该怎么写呢 4.4.1 重载 我们还是以 S t u d e n t Student Student类 为例   首先如果显式实现析构函数 n u m num num 和 _ s e x sex sex 是不用管的。因为int _num是内置类型而 string _sex会自己调用其析构。我们只需要管父类即可 ~Student() {~Person(); }但这样会报错 析构是可以显示调用的但为什么这里调不动呢 这里有个小知识点子类的析构会和父类的析构构成隐藏关系。   因为一些特殊的原因析构函数的函数名会被特殊处理成 d e s t r u c t o r destructor destructor()所以父类的析构函数和子类的析构函数构成隐藏关系。实际上并没有什么 ~ S t u d e n t Student Student() 和 ~ P e r s o n Person Person()只有 d e s t r u c t o r destructor destructor()。 所以我们要指定类域调用 ~Student() {Person::~Person(); }4.4.2 顺序 我们来尝试调用一下析构函数 class Person { public://成员函数//···~Person(){cout ~Person() endl;}protected:string _name; // 姓名 };class Student : public Person { public://成员函数//···~Student(){Person::~Person();}protected:int _num; //学号string _sex; //性别 };int main() {Student s1(张三, 1, 男);Student s2(李四, 2, 未知);return 0; }运行结果 大家有没有发现析构函数调的有点多啊我一个就两个对象你怎么就调用 4 次析构函数了呢 像构造、赋值重载等我们显式写的都需要显式调用父类的对应函数但析构不需要显式调用。调用了子类析构函数之后系统会自动调用父类的析构这点与自定义类型的成员很像。 为什么要这样的。这样可以保证析构顺序是先子后父。后定义的先析构而对象构造时是先构造初始化父类在初始化子类析构是就需要先析构子类在析构父类。如果显式调用就不能保证先子后父而是取决于实现的人。       4.5 实现不能被继承的类 要实现一个不能被继承的类有两种方法 4.5.1 法一设为私有 将父类的构造函数设置为私有 class Base { public :void func5() { cout Base::func5 endl; } protected:int a 1; private:// C98的⽅法Base(){} };class Derive :public Base {void func4() { cout Derive::func4 endl; }protected:int b 2; };为什么呢因为子类的构造函数不论是自动生成还是我们自己显式实现都必须调用父类的构造函数。但是父类的 p r i v a t e private private成员在子类中是不可见的因此子类调不到父类的构造函数。 但是这种方式不够明显如果不调用子类的对象编译器是不会报错的 4.5.2 法二final C11中新增了一个关键字 f i n a l final final   用 f i n a l final final 修饰一个类表示该类是最终类无法再被继承。 这种方式更直观一些不管子类定不定义直接报错 class Base final { public :Base(){}void func5() { cout Base::func5 endl; } protected:int a 1; };class Derive :public Base {void func4() { cout Derive::func4 endl; } protected:int b 2; };4.6 总结 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数则必须在派生类构造函数的初始化列表显示调用派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化派生类的 o p e r a t o r operator operator 必须要调用基类的 o p e r a t o r operator operator 完成基类的复制。需要注意的是派生类的 o p e r a t o r operator operator 隐藏了基类的 o p e r a t o r operator operator所以显示调用基类的 operator需要指定基类作用域派生类的析构函数会在被调用完成之后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员在清理基类成员的顺序派生类对象初始化先调用基类的构造再调派生类的构造派生类对象析构清理先调用派生类析构再调基类的析构因为多态中一些场景析构函数需要构成重写重写的条件之一是函数名相同。那么编译器会对析构函数进行特殊处理 处理成 d e s t r u c t o r destructor destructor()所以基类析构函数不加 virtual 的情况下派生类析构函数和基类析构函数构成隐藏关系大多数情况下派生类中拷贝构造、赋值、析构都是不需要自己写的如果需要那这个继承的设计太过复杂可以考虑重新设计。 好啦本期关于继承的知识就介绍到这里啦希望本期博客能对你有所帮助。同时如果有错误的地方请多多指正让我们在 C 的学习路上一起进步