莱州市建设局网站,安阳县面积,这样做自己公司的网站,现在1做啥网站流量大目录
介绍#xff1a;
一#xff0c;继承的访问权限
二#xff0c;基类和派生类对象赋值转换
三#xff0c;继承中的作用域
四#xff0c;派生类的默认成员函数
1#xff0c;构造函数
2#xff0c;析构函数
3#xff0c;拷贝构造和赋值运算符
五#xff0c;继…目录
介绍
一继承的访问权限
二基类和派生类对象赋值转换
三继承中的作用域
四派生类的默认成员函数
1构造函数
2析构函数
3拷贝构造和赋值运算符
五继承中的友元与静态成员
1继承与友元
2继承与静态成员
六复杂的菱形继承及菱形虚拟继承
七继承与组合 介绍 继承机制是面向对象程序设计使代码可以复用的最重要的手段它允许程序员在保持原有类特性的基础上进行扩展增加功能这样产生新的类称派生类。继承呈现了面向对象程序设计的层次结构体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用继承是类设计层次的复用。 这里被复用的类叫做基类或父类复用后产生新的类叫做派生类或子类。
普通继承 class Person { public: void Print() { cout name: _name endl; cout age: _age endl; } protected: string _name peter; int _age 18; }; class Student : public Person //子类Student继承父类Person的成员(即成员函数成员变量)继承后都会变成子类的一部分通过调试窗口即可见 { protected: int _stuid; }; class Teacher : public Person //同理继承父类Person { protected: int _jobid; }; int main() { Student s; Teacher t; //由于继承后将会成为子类的一部分这里可看作子类中的成员 s.Print(); t.Print(); cout sizeof(Student) endl sizeof(Teacher) endl; //发现占用的内存变大因为继承子类中包含父类内存变大 return 0; } 模板继承 //模板继承 template class T class B : public AT { ....... }; 以上继承中Person是父类(基类)。Student和Teacher是子类(派生类)。我们以Student为例如下 一继承的访问权限 下面我们谈谈这里的继承方式中的访问限定符和访问权限的问题。先来观察基类访问权限与派生类的关系。 当基类的访问权限为private类型时除非在自己类中运用自己的成员函数可进行访问派生类无论怎样都访问不了。 当基类的访问权限为protected类型时在派生类中可进行访问但在类外不可进行访问。也就是说在类外时如同private权限。 当基类的访问权限为public类型时无论在派生类中还是在类外都允许直接访问。 继承方式中的权限限制的就是基类中的权限。基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 Min(成员在基类的访问限定符与继承方式)其中public protected private。即如下 当继承方式是public时基类的private是privatepublic是publicprotected是protected。 当继承方式是protected基类的private是privatepublic是protectedprotected是protected 当继承方式是private时继承基类的所有权限都为private。 #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; }; class Student : public Person { public: void func() //继承方式是public权限访问时与基类Person中默认取最小权限 { cout _name endl; cout _age endl; Print(); } protected: int _stuid; }; int main() { Student s; s.func(); //没有私有权限都可被派生类访问 return 0; } 需说明一点于class而言若继承方式的权限不写默认为私有private于struct而言若继承方式的权限不写默认为公有public跟默认成员权限一样。 补在实际运用中一般使用都是public继承几乎很少使用protetced/private继承也不提倡使用protetced/private继承因为protetced/private继承下来的成员都只能在派生类的类里面使用实际中扩展维护性不强。 二基类和派生类对象赋值转换 派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用但基类对象不能赋值给派生类对象。因为子类对象(派生类)中都有父类(基类)成员所以子类对象可以赋值给父类对象但是子类的成员父类不一定拥有所以父类不能赋值给子类。 #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; }; class Student : public Person //子类(派生类) { public: void func() { cout _name endl; cout _age endl; Print(); } protected: int _stuid; }; int main() { Student s; Person p s; //子类对象s赋值父类的对象p Person* pp s; //子类s的地址赋值给父类的指针pp Person rp s; //子类对象s赋值给父类的引用rp //s p; //报错父类对象不能赋值给子类对象 return 0; } 注意这里重点说明一下不同类型赋值会产生临时变量赋予的数据也是临时变量但这里的子类赋值于父类是不会产生临时变量。这里的子类对象赋值给父类对象如同切割一样将子类中所包含父类的成员切割下来给父类对象即子类比父类所占用的空间大。如下图 父类(基类)向子类(派生类)的转换在一定条件下也可进行但这方面设计到一定的东西这里先不做过多研究后文会详细说明。 三继承中的作用域 1. 在继承体系中基类和派生类都有独立的作用域。 2. 由于子类和父类都有独立的作用域所以子类和父类中可以有同名成员。当在子类中访问同名成员时子类成员将屏蔽父类对同名成员的直接访问这种情况叫隐藏也叫重定义。若想访问父类中的同名成员时在子类成员函数中使用 基类::基类成员 显示访问基类成员成员成员函数和普通成员。 3. 需要注意的是隐藏关系是对于继承关系中子类与父类中同名成员的关系只要重名即构成隐藏。如果是成员函数的隐藏只需要函数名相同就构成隐藏返回值和参数可以不同。隐藏的本质是子类隐藏父类的同名成员。 4. 注意在实际中在继承体系里面最好不要定义同名的成员。 //A和B的fun函数是隐藏关系 //注意: 这里的fun不是重载关系重载关系是在同一作用域下的关系这里的基类与派生类是两个不同的作用域 //对于基类和派生类而言只要满足成员名相同即构成隐藏关系所以成员如果是函数函数名相同就构成隐藏关系 class A { public: void fun() { cout func() endl; } }; class B : public A { public: void fun(int i) { A::fun(); cout func(int i)- i endl; } }; int main() { B b; b.fun(1); //b.fun();//调用错误因为父类的fun被隐藏了这里调用的是派生类中的fun b.A::fun(); //调用基类A的fun需指定作用域 return 0; } 四派生类的默认成员函数 派生类一共存在6个默认构造函数这里我们只需要了解四个即可即构造函数、析构函数、拷贝构造函数、赋值运算符重载。在继承关系中父类的这些函数都是通过子类进行调用的。
1构造函数 对于构造函数由于构造函数的初始化列表是按照初始化对象的声明顺序进行初始化而子类是先继承父类所以这里先对父类初始化即先调用父类的构造函数然后再对子类进行初始化即后调用子类的构造函数。如以下 #include iostream using namespace std; class Person { public: Person(const char* name peter) //普通构造函数 : _name(name) { } protected: string _name; }; class Student : public Person { public: Student(const char* name, int num) //:_name(name) 错误这里不允许这样对父类成员初始化必须调用父类的构造函数对父类成员初始化 : Person(name) //调用父类构造函数若不写这里会自动先调用父类的匹配构造函数然后再调用子类的构造函数 //这里要注意若不显示调用父类的构造函数当没有与之默认匹配的构造函数时将会报错 , _num(num) { } protected: int _num; }; int main() { Student s(兔子, 1); return 0; } 父类的构造函数可直接手动调用也可让系统自动调用吗但这里建议手动调用因为当自动调用时若不存在匹配的构造函数时将会出错。如下 #include iostream using namespace std; class Person { public: Person(const char* name) //这里的构造函数默认情况下不能调用因为不匹配 : _name(name) { } protected: string _name; }; class Student : public Person { public: Student(const char* name, int num) //报错因为不存在默认的构造函数 : _num(num) { } protected: int _num; }; int main() { Student s(兔子, 1); return 0; } 2析构函数 析构函数系统默认会先调用子类的析构函数然后调用父类的析构函数这样做是为了避免安全隐患。因为子类包含父类若父类动态指向一块内存空间时若先析构父类时此空间已经被释放子类析构时会再次析构此空间。 众所周知构造函数不能手动调用但是析构函数可以所以说这里我们可强行先调用父类的析构函数。但是要注意的是子类的析构函数与父类的析构函数构成隐藏关系由于多态的原因析构函数被特殊处理不能在子类的析构函数中直接显示调用。这里跟隐藏关系调用的逻辑一样需指名作用域。 这里不建议手动调用析构函数这里的规则跟析构函数的规则一样系统会自动调用也就是说手动析构完之后系统也会再次调用。 #include iostream using namespace std; class Person { public: Person(const char* name peter) : _name(name) { cout Person() endl; } ~Person() { cout ~Person() endl; } protected: string _name; }; class Student : public Person { public: Student(const char* name, int num) : Person(name) , _num(num) { cout Student() endl; } ~Student() { Person::~Person();//若不显示调用父类的析构函数会默认先调用本类(子类)的析构函数然后再调用父类Person的析构函数 cout ~Student() endl; } protected: int _num; }; int main() { Student s(兔子, 1); return 0;//发现系统结束时输出了两次父类析构函数中的内容 } 3拷贝构造和赋值运算符 调用父类的赋值运算符时需注意指定作用域而拷贝构造是将子类对象拷贝构造给父类如同子类赋值父类般进行切割。 #include iostream using namespace std; 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 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(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; } protected: int _num; }; int main() { Student s(兔子, 1); Student s1(s); s1 s; return 0; } 总这里的所有知识点可理解为跟单独的类一样父类看作是子类的成员父类的所有基本功能都要靠子类所对应的基本功能来实现。 五继承中的友元与静态成员
1继承与友元 这里这里友元关系不能继承即基类的友元不能被子类继承也就是说基类友元不能访问子类私有和保护成员(当权限公有时有没有友元都一样直接可被访问)。要想访问子类成员这里必须也在子类中声明。 #include iostream using namespace std; class Student; //声明Student类因为下面Display要使用 class Person { public: friend void Display(const Person p, const Student s); string _name 张三; }; class Student : public Person { //friend void Display(const Person p, const Student s); 这里在子类中也声明友元可正常运行//public: 权限是公有在类外可直接访问不需要友元 protected: //权限为私有或保护由于友元不能被继承所以不可访问数据 int _stuNum 5; }; 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; } 2继承与静态成员 静态成员存储在静态区不在类中也就说当基类存在静态成员时子类不会将其直接继承下来复制一份而是跟基类一样直接调用静态区里的静态成员即整个继承体系里面只有一个这样的静态成员。无论基类有多少个子类都只有一个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 _stuNum; }; int main() { Person p; Student s; cout Person::_count endl; //输出2 cout Student::_count endl; //输出2 } 六复杂的菱形继承及菱形虚拟继承
单继承一个子类只有一个直接父类时称这个继承关系为单继承。以上所有的实例都为单继承。 多继承一个子类有两个或以上直接父类时称这个继承关系为多继承。 菱形继承菱形继承是多继承的一种特殊情况。 菱形继承存在很多问题结构比较复杂。我们先来观察以下代码样例。 #include iostream using namespace std; 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 pter;//这样会有二义性无法明确知道访问的是哪一个 //以下为正确写法需跟一般继承一样指名作用域 //这样虽暂时解决了二义性但没有解决本质问题。有时我们不需要存储两份这样会造成空间的浪费也就是数据冗余问题无法解决 a.Student::_name 张三; a.Person::_name 李四; return 0; } 菱形继承的问题从上面的对象成员模型构造可以看出菱形继承有数据冗余和二义性的问题。 在Assistant的对象中Person成员会有两份。存储结构如下图 虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系在Student和 Teacher的继承Person时使用虚拟继承即可解决问题。 虚拟继承的使用方法是在继承前加上关键字virtual此时所有加上关键字virtual的类将会特殊处理。此时可以理解它们所继承的父类共用一块空间。具体使用方法如下 #include iostream using namespace std; class Person { public: string _name; }; class Student : virtual public Person //增加关键字virtual,即虚继承 { protected: int _num; }; class Teacher : virtual public Person //同理增加虚继承 { protected: int _id; }; class Assistant : public Student, public Teacher //加上此位置后与此类同名成员会跟以上情况一样被特殊处理。 { public: string _majorCourse; string _name; }; int main() { Assistant a; a.Student::_name 张三; //在Student、Teacher类中所继承的父类成员做了特殊处理这里基类名称为_name的成员为张三即a.Person::_name 张三 a.Person::_name 李四; //同理都被处理成李四 a._name 小张; //只有Assistant类的_name被处理成小张因为此类没有增加虚继承 //注意若Assistant类里没有_name这里直接a._name会默认基类中的成员 return 0; } 下面我们通过内部来分别观察使用虚拟继承和不使用虚拟继承的情况。代码样例如下 #include iostream using namespace std; 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的地址进而找到A。注意内存中按照十六进制存放偏移量。原理如下图 通过以上逻辑图我们再观察以下代码有关虚继承的问题。 #include iostream using namespace std; class A { public: int _a;}; class B : virtual public A { public: int _b; }; class C : virtual public A { public: int _c; }; class D : public B, public C //先继承B后继承C即B在存储前面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; cout d._a endl; //输出2 D dd; //调试后发现dd里面的虚基表指针指向的地址跟d一样但虚基表指针本身的地址不一样即A中的数据不一样 cout dd._a endl; //随机值 B b; //调试后发现b里面的虚基表指针指向的地址与d和dd都不一样 B bb; //只与b里面的虚基表指针指向的地址一样但虚基表指针本身的地址不一样即A中的数据不一样 C c; //调试后发现c里面的虚基表指针指向的地址跟b、d、dd都不一样 C cc; //只与c里面的虚基表指针指向的地址一样但虚基表指针本身的地址不一样即A中的数据不一样 return 0; } 有些人可能会有疑问为什么D中B和C部分要去找属于自己的A而不是直接指向A不妨先想想在虚继承中若存在子类赋值给父类的情况这里就出现很多复杂情况了这里只需了解即可。在整个C体系中除了输入流istream和输出流ostream运用了虚拟继承其它很少使用虚拟继承。 最后说一下继承是C中的缺陷之一。有了多继承就存在菱形继承。菱形继承由于有数据冗余和二义性的问题所以就有了菱形虚拟继承到了这里底层实现就很复杂。所以一般不建议设计出多继承若设计出多继承切记一定不要设计出菱形继承。否则在复杂度及性能上都有问题。 七继承与组合 继承和组合都是类的复用不同的是继承可以说每个派生类对象都是一个基类对象。而组合是一种包含关系即假设B组合了A每个B对象中都有一个A对象。 class A { public: int _a; };//组合 class B { private: A _a; }; 通过以上实例可发现组合为低耦合(即两个模块关系不大)继承是高耦合(即两个模块关系大)。 耦合性平常也叫做可维护性。若代码的可维护性高在以后对代码的调整或更新的时候会很方便比如在此项目上继续添加东西或迭代更新。若代码的可维护性低这很不利于更新或填补bug。因此我们需要低耦合即可以用组合就用组合。若实在不行才考虑继承。