海外社交网站开发,wordpress 游客评论,wordpress主题制作,开一个网站_只做同城交易1.继承概念
“继承”是面向对象语言的三大特性之一#xff08;封装、继承、多态#xff09;。
继承#xff08;inheritance#xff09;机制是面向对象程序设计使代码可以复用的最重要的手段#xff0c;它允许程序员在保持原有类特性基础上进行扩展#xff0c;增加功能封装、继承、多态。
继承inheritance机制是面向对象程序设计使代码可以复用的最重要的手段它允许程序员在保持原有类特性基础上进行扩展增加功能这样产生新的类称为“派生类/子类”。
继承呈现了面向对象程序设计的层次结构体现了由简单到复杂的认知过程。之前我们接触的复用都是函数复用继承是类设计层次的复用。
2.继承使用
继承发生在“基类/父类”和“派生类/子类”之间语法形式就是
class 父类名
{//方法//属性
};
class 子类名 : 继承方式 父类
{//...
};那这个继承方式又是什么呢在字面上和我们之前看到的public、protected、private是一样的但是意义有所不同。继承方式和访问限定符可以相互组合在父子类中有以下组合
类成员/继承方式public继承protected继承private继承子类的public成员子类中仍是public成员子类中变成protected成员子类中变成private成员子类的protected成员子类中仍是protected成员子类中仍是protected成员子类中变成private成员子类的private成员子类中不可见该成员为父类私有子类中不可见该成员为父类私有子类中不可见该成员为父类私有
实际上规律也简单公有保护私有在两个关键字中取小即可有点类似权限缩小的感觉当然您可以选择调用父类的函数来无视这些限定也就是间接访问。
下面我们就以“人Person”父类来生成子类“学生Student”和“老师Teacher”
class Person//父类
{
public: void Print() { cout name: _name endl; cout age: _age endl; }
protected: string _name peter;//姓名 int _age 18;//年龄
};
//继承后父类的Person的成员成员函数成员变量都会变成子类的一部分class Student : public Person//继承子类
{
protected: int _stuid;//学号
};
class Teacher : public Person
{
protected:int _jobid;//工号
};int main()
{Student s;Teacher t;s.Print();t.Print();return 0;
}注意1实际应用中继承方式大多使用public还有类内的访问限定符也大多使用public和protected。其他的继承方式和类内的访问限定符基本上都很少用并且有很多时候都不推荐使用。 注意2如果不写继承方式那么默认继承方式是private如果是结构体struct就默认public但是最好还是显式写出继承方式为好。 注意3父类的private成员在子类中无论以什么方式继承都是不可见的。这里的不可见是指父类的私有成员被继承到了子类对象中但是语法上限制子类不管在类里还是类外都不能去访问它。 #include iostream
#include string
using namespace std;
class Person//父类
{
public:void Print(){cout name: _name endl;cout age: _age endl;}
private:string _name peter;//姓名 int _age 18;//年龄
};
//继承后父类的Person的成员成员函数成员变量都会变成子类的一部分class Student : public Person//继承子类
{/*void function(){_name student;//失败_age 12;//失败}*/
};//不可以直接在类内使用从父类继承来的private变量包括子类创建出来的对象不可见但是依旧可以被Print()使用
class Teacher : public Person
{/*void function(){_name teacher;//失败_age 20;//失败}*/
};//不可以直接在类内使用从父类继承来的private变量包括子类创建出来的对象不可见但是依旧可以被Print()使用int main()
{Student s;Teacher t;/* 失败s._name;s._age;t._name;t._age;*/s.Print();t.Print();return 0;
}注意4由于父类的private成员在子类中是不能被访问如果子类成员不想在类外直接被访问但需要在子类中被访问就使用限定符为protected即可。从这点也可以看出保护成员限定符是因为继承才出现的。 #include iostream
#include string
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 function(){_name student;//成功_age 12;//失败}
};
class Teacher : public Person
{
public:void function(){_name teacher;//成功_age 20;//失败}
};int main()
{Student s;Teacher t;/* 失败语句s._name student;s._age 12;t._name teacher;t._age 20;*/s.function();t.function();s.Print();t.Print();return 0;
}3.切片赋值
公有类继承的子类可以直接赋值给父类这个过程形象的说法叫做“切割/切片”也有叫“赋值兼容转化”的。这个过程是纯天然的没有隐式类型转化的发生没有临时变量的产生类似int a 1; int b 10; a b的过程而这种“切片”的现象实际上在指针和引用上也有体现。
#include iostream
#include string
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
{
protected:int _stuid;
};class Teacher : public Person
{
protected:int _jobid;
};int main()
{//1.普通类型double a 1.1;int b a;//发生类型转化const int ra a;//中间会产生临时变量int* pa (int*)a;//2.子类切片给父类Student std;Person per std;//没有发生类型转化只是将子类中的成员变量拷贝给父类Person rper std;//没有产生临时变量并且使用这一特性就好像给子类做了“切片”rp本身还是子类但是却只引用/切出了子类中继承自父类的那一部分成员因此无需加上constPerson* pper std;//同理指针也是做了一些“切片”指针解引用只能看到子类从父类哪里继承的那一部分成员//3.父类赋值给子类//std per;//不允许//Student rstd per;//不允许//Student* pstd per;//不允许/* 这里我还暂时理解不了Student rstd (Student)per;//不允许pper std;Student* pstd (Student*)pper;//允许pper per;pstd (Student*)pper;//允许但是有可能会越界*/return 0;
}那反过来可以么一般是不可以的父类不可以直接赋值给子类因为容易造成子类的属性缺失有可能会出现问题。 补充父类的指针和引用可以通过“强制类型转化”赋值给子类的指针和引用但是必须是父类的指针是指向子类对象时才是安全的这里的父类如果是多态的就可以使用RTTI(Run-Time Type Information)的dynamic cast来进行识别然后进行安全转换这点我们以后再来细说。 4.隐藏父类 在继承中父类和子类都有自己独立的作用域 子类和父类有同名成员但是子类将屏蔽父对类同名对象的直接访问这种情况就叫“隐藏/重定义” 函数也有类似“隐藏”的情况注意需要和“重载区分在同一个作用域”只需要函数名相同就可以达到“隐藏父类成员函数” 尽可能不使用同名的成员构成隐藏不然有的时候会给自己挖坑…
#include iostream
#include string
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
{
protected:string _name student;//父类的_name被子类Student隐藏了int _stuid;
public:void Print()//父类的Print()被子类Student隐藏了{cout name: _name endl;cout age: _age endl;//_age没有被子类隐藏依旧打印从父类继承过来的_age}
};class Teacher : public Person
{
protected:string _name teacher;//父类的_name被子类Teacher隐藏了int _jobid;
public:void Print()//父类的Print()被子类Teacher隐藏了{cout name: _name endl;cout age: _age endl;//_age没有被子类隐藏依旧打印从父类继承过来的_age}
};int main()
{Student s;s.Print();Teacher t;t.Print();return 0;
}但是想要直接访问父类中被子类隐藏的成员怎么办呢使用作用域解析运算符即可因为父类的成员只是被隐藏而不是不存在了。
#include iostream
#include string
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
{
protected:string _name student;//父类的_name被子类Student隐藏了int _stuid;
public:void Print()//父类的Print()被子类Student隐藏了{cout name: _name endl;cout age: _age endl;//_age没有被子类隐藏依旧打印父类的_age}
};class Teacher : public Person
{
protected:string _name teacher;//父类的_name被子类Teacher隐藏了int _jobid;
public:void Print()//父类的Print()被子类Teacher隐藏了{cout name: _name endl;cout age: _age endl;//_age没有被子类隐藏依旧打印父类的_age}
};int main()
{Student s;s.Print();Teacher t;t.Print();s.Person::Print();t.Person::Print();return 0;
}5.子类成员 子类的构造函数必须调用父类的构造函数初始化继承自父类的那一部分成员而不能自己在子类中直接初始化。 如果父类有默认构造函数哪怕用户不写也会在子类构造函数的初始化列表处自动调用父类的默认构造函数。 如果父类没有默认构造函数则用户必须在子类构造函数的初始化列表阶段显示调用。因此子类对象初始化先调用父类构造函数再调用子类构造函数。 子类的析构函数会在被调用完成后自动调用基类的析构函数清理父类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序避免出现用户自己调用父类的析构函数父类成员先被释放了但是子类依旧可以使用父类的成员造成越界访问的现象。 因此在子类的析构函数内用户不能自己显式调用父类的析构函数否则会重复调用。 另外编译器会对析构函数名进行特殊处理处理成destrutor()所以父类析构函数不加virtual的情况下子类析构函数和父类析构函数构成隐藏关系。后续一些场景中析构函数需要构成重写重写的条件之一是函数名相同 子类的拷贝构造函数必须调用父类的拷贝构造完成继承自父类的那一部分成员的拷贝初始化。但是父类的拷贝构造函数需要传递一个父类对象过去如何把继承于父类的成员变量拿出来作为父类对象传给父类的拷贝构造函数呢使用切片即可这就是切片的实际应用 子类的operator必须要调用父类的operator完成继承自父类的那一部分成员的复制同理也是使用切片 注意子类是没有办法直接隐藏父类的特殊成员函数的这也体现了成员函数的特殊性。当然这除去了析构函数的特殊情况。 使用父类的成员函数最好一定要带上父类和作用域解析运算符否则就有可能因为隐藏而出现自己调用自己的情况就会变成无穷递归例如下述代码中子类的Person::Print()调用。
#include iostream
#include string
using namespace std;
//1.父类Person
class Person
{
public:Person(string name, int age) : _name(name), _age(age)//不是默认的构造函数{cout Person(string nume, int age) : _name(\peter\), _age(18) endl;}~Person(){cout ~Person() endl;}Person(Person p)//不是默认的拷贝构造函数{cout Person(Person p) endl;_name p._name;_age p._age;}Person operator(const Person p)//不是默认的赋值重载函数{cout Person operator(const Person p) endl;if (this ! p){_name p._name;_age p._age;}return *this;}void Print(){cout Print() endl;cout name: _name age: _age endl;}
protected:string _name;int _age;
};//2.子类Student
class Student : public Person
{
public:Student(string name, int age, int stuid 00000000) : Person(name, age), _stuid(stuid)//这里调用显示调用父类的构造函数并且没有明确指定父类初始化子类中从父类继承过来的成员然后才初始化子类自己的成员{cout Student(string name, int age, int stuid 00000000) : Person(name, age), _stuid(stuid) endl;}~Student(){cout ~Student() endl;//无需自己调用父类函数的析构函数编译器会自己帮助我们调用}Student(Student s) : Person(s), _stuid(s._stuid)//这里使用了切片显式调用父类的拷贝构造函数可以选择不指定父类初学者推荐一直显式调用有助于代码理解和可读性。{cout Student(Student s) : Person(s), _stuid(s._stuid) endl;}Student operator(const Student p){cout Student operator(const Student p) endl;if (this ! p){Person::operator(p);_stuid p._stuid;}return *this;}void Print()//构成成员函数隐藏了{//Print();//这就变成自己调用自己了此时就不可以不明确父类了Person::Print();//指明父类cout stuid: _stuid endl;}
protected:int _stuid;
};
int main()
{Student s1(limou, 18, 22130800);Student s2(s1);Student s3(daimou, 22, 22109988);s1 s3;s1.Print();s2.Print();s3.Print();return 0;
}6.不可继承
6.1.友元关系
友元关系是不会被继承的也就是说如果父类有一个友元函数那么这个友元函数只可以使用父类的成员变量而不可以使用从父类继承过来的子类的内部成员变量。
#include iostream
#include string
using namespace std;class Student;//这句是声明告诉编译器有一个类的存在让下面在Person内的函数可以通过编译
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;
}这种情况下只能再定义一个友元关系给子类。
#include iostream
#include string
using namespace std;class Student;//这句是声明告诉编译器有一个类的存在让下面在Person内的函数可以通过编译
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;//该语句不正确
}int main()
{Person p;Student s;Display(p, s);return 0;
}6.2 .静态成员
静态成员变量属于整个类不仅仅属于父类也属于子类因此不能说子类继承静态成员变量这么说是不准确的。
#include iostream
#include string
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;//学号
};class Graduate : public Student
{
protected:string _seminarCourse;//研究科目
};
void TestPerson()
{Student s1;Student s2;Graduate s3;Graduate s4;cout 人数: Student::_count endl;Person::_count 0;cout 人数: Graduate::_count endl;
}
int main()
{TestPerson();return 0;
}也就是说父类中使用的静态成员变量和子类的静态成员变量是一样的不过这其实也侧面说明从父类中的成员变量和子类中的成员变量只是拷贝关系各自的地址是不一样的。
7.虚拟继承
7.1.不可能棱形
我之前提到的继承都是“单个父类继承给多个子类”的“单继承”但是C在开发之初为了更加符合现实中“多个父类继承个单个子类”的现象设计了“多继承”。
但是没有想到因此出现了一个新的问题棱形继承具有“二义性”和”数据冗余“的两大缺陷。棱形继承是多继承的一种特殊情况其内容为”假设有一个父类其有两个子类而两个子类又通过多继承得出一个子子类这个子子类创建出来的对象会从两个子类继承那么会出现相同的成员也就是二义性不知道使用的是哪一个子类的成员和数据冗余有可能出现重复相同的成员变量“。 #include iostream
#include string
using namespace std;
class Person
{
public:Person(): _name(peter), _age(18){cout Person() endl;}
public:string _name;int _age;
};class Student : public Person
{
public:Student():Person(), _stuid(22103){}
public:int _stuid;
};class Teacher : public Person
{
public:Teacher():Person(), _jobid(88903){}
protected:int _jobid;
};
class Graduate : public Student, public Teacher//多继承
{
protected:int _graid;
};int main()
{Graduate g;//cout g._name endl;//运行失败具有二义性cout g.Student::_name endl;//运行成功成功解决二义性cout g.Teacher::_name endl;//运行成功成功解决二义性return 0;
}二义性我们尚且可以通过作用域解析运算符来解决但是数据冗余还需要使用virtual关键字才能解决。该关键字加在两个子类的继承方式处但是也因此变得很复杂… 补充因此多继承会让代码变得复杂所以Java直接移除了多继承的功能。 7.2.虚继承操作
为什么数据冗余使用virtual关键字就能解决呢我们来查看一下底层。
这里我们最好不要查看编译器的调试窗口而是使用内存窗口因为调试窗口为了方便展示又可能做了一些优化比如使用内联函数有些编译器的调试器会让您觉得这是在调用函数。
7.2.1.不加关键字
#include iostream
#include string
using namespace std;
class A//父类A
{
public:int _a;
};class B : public A//子类B
{
public:int _b;
};class C : public A//子类C
{
public:int _c;
};class D : public B, public C//多继承后的子子类D
{
public:int _d;
};int main()
{D d;d.B::_a 1;d.C::_a 2;d._b 3;//或者d.B::_b 3;d._c 4;//或者d.C::_c 4;d._d 5;return 0;
}可以看到每一个变量都存储在不同的地址上。
7.2.2.加上关键字
#include iostream
#include string
using namespace std;
class A//父类A
{
public:int _a;
};class B : virtual public A//子类B
{
public:int _b;
};class C : virtual public A//子类C
{
public:int _c;
};class D : public B, public C//多继承后的子子类D
{
public:int _d;
};int main()
{D d;d.B::_a 1;d.C::_a 2;d._b 3;//或者d.B::_b 3;d._c 4;//或者d.C::_c 4;d._d 5;return 0;
}因此我们成功解决了二义性和数据冗余的问题需要注意的是虚拟继承最好不要在其他地方去使用。
7.3.虚继承原理
通过上面的调试我们发现在解决二义性的时候好像D从B、C、A处继承的_a成员是在一个地址上的变量这个应该怎么理解呢让我们通过调试来理解虚继承的原理。 将B和C和D中同属于A的部分变成一个公共部分在继承自B和C的部分中多出两个指针各自指向一块表空间这些表内存储了指针偏移量信息。利用指针偏移量和表指针的地址即可找到一块公共空间消除二义性。
那为什么是指针指向一个偏移量表而不是直接使用指针指向公共空间呢这些后面多态再来讨论指针固然能解决这个问题但是使用偏移量还能再进一步解决多态的问题… 这样子子类在使用从两个子类继承过来的变量的时候实际上是同一个变量解决了冗余性和二义性。
既然棱形继承这么复杂那应该会很少使用吧实际上您有可能天天用这iostream类就是通过istream类和ostream类棱形继承来的… 注意上述两图来源于Reference - C Reference (cplusplus.com)和输入/输出库 - cppreference.com感兴趣您可以去查看一下。 下面有一道结合了虚拟继承的面试题目供您思考一下让您更加了解虚继承和初始化列表的初始化顺序。
#include iostream
#include string
using namespace std;
class A
{
public:A(const char* s){cout s endl;}~A() {}
};class B : virtual public A
{
public:B(const char* s1, const char* s2): A(s1){cout s2 endl;}
};class C : virtual public A
{
public:C(const char* s1, const char* s2): A(s1){cout s2 endl;}
};
class D : public B, public C//多继承
{
public:D(const char* s1, const char* s2, const char* s3, const char* s4): B(s1, s2), C(s1, s3), A(s1){cout s4 endl;}
};int main()
{D* p new D(class A, class B, class C, class D);//按照初始化列表的根据谁先定义先初始化的原则//先调用A(s1)然后调用B(s1, s2)然后调用C(s1, s3)然后打印class D//也就是先调用A(s1)然后打印class B然后调用A(s1)然后打印class C然后调用A(s1)然后打印class D//由于在D中从B和C中继承过来的A的成员是公共部分因此只会调用一次构造函数A(s1)并且这个构造函数A(s1)是在D中被调用的//于是得到A、B、C、Ddelete p;return 0;
}8.继承/组合
继承和组合都是一种复用以下是继承和组合的区别不过这里只讨论公有方式的继承
//继承is-a
class A1
{};
class B1 : public A1
{};//组合has-a
class A2
{};
class B2
{
private:A2 _a;
};那么这个is-a和has-a是什么呢一个是“是一个也就是继承”的关系一个是“拥有一个也就是组合”的关系。
从“关联关系/耦合度”上来看继承会更高B1可以直接使用A1的所有成员但是B2只能使用A2的函数成员间接调用其他的A2成员因此对象组合也是一种黑箱/黑盒风格。
但在软件设计中有一种设计理念就是“低耦合、高内聚”因此在实践中应该尽可能去用组合这样的代码维护性更好具体还要看实际。
如果是为了实现多态那就必须使用继承多态我们后面再谈如果描述类之间的关系时继承和组合都可以那么应该尽量使用组合