一、继承的定义
1.格式
struct A
{
protected:
int _a = 1;
};
//类后边加访问限定符+类名
struct B :public A
{
protected:
int _b = 2;
};
2.原则
通过继承可以复用类的成员,被继承的类叫做基类或者父类,继承的类叫做派生类或者子类.派生类可以根据继承方式和基类的访问限定符类型访问基类的成员.
继承方式有public,protected和private三种方式,基类中也有这三种访问限定符限定的成员.public方式继承基类成员,成员属性不变;protected方式继承,基类public成员在派生类中变protected属性,其余不变;private继承所有基类成员都变为派生类的private属性成员.除了public继承方式,其他继承范式不常用.
基类的private成员在派生类中不可直接使用,但是可以通过基类公共成员间接调用.基类的protected成员在类外面不可以使用,但是在派生类中可以直接调用.
友元函数不能被继承,静态成员变量不论被继承多少次,都只有一个实例.
#include<iostream>
#include<string>
using namespace std;
class person
{
public:
person(const string& name="张三")
:_name(name)
{}
protected:
void print()
{
cout << _name << endl;
}
private:
string _name;
};
class student :public person
{
private:
int _ib = 101;
};
int main()
{
student s;
//s嗲用person的保护成员
s.print();
//s._name;会报错
return 0;
}
二、基类与派生类赋值转换
派生类支持直接赋值给基类,中间没有发生隐式类型转换,是先天性支持.相当于派生类将与基类相同的内容切片(拷贝)给了基类.同样也支持基类对派生类的引用和指针赋值.
#include<iostream>
#include<string>
using namespace std;
struct person
{
person(const string& name="张三")
:_name(name)
{}
void print()
{
cout << _name << endl;
}
protected:
string _name;
};
struct student :public person
{
protected:
int _ib = 101;
};
int main()
{
person per;
student s;
per = s;
person* pper = &s;
person& ppers = s;
return 0;
}
三、继承中的作用域
基类和派生类都有独立的作用域,不同的作用域可以有重名的变量或者函数.重名的成员在继承中叫做隐藏或者重定义,函数只要函数名相同就构成隐藏.
子类访问成员时,会屏蔽父类中重名的成员.如果要访问父类中的隐藏成员需要加上基类::成员名访问.
#include<iostream>
#include<string>
using namespace std;
class person
{
public:
person(const string& name="张三")
:_name(name)
{}
void print()
{
cout << _name << endl;
}
private:
string _name;
};
class student :public person
{
public:
void print()
{
cout << _id << endl;
}
protected:
int _id = 101;
};
int main()
{
student s;
s.print();
s.person::print();
return 0;
}
四、继承类的默认成员函数
1.普通构造
派生类构造时会先自动调用基类的成员函数初始化属于基类的成员变量,然后再初始化自己的成员变量.派生类默认构造也会自动调用基类的构造函数.
2.拷贝构造
派生类拷贝构造时会先调用基类拷贝构造,派生类传参派生类类型在父类拷贝构造函数接收时发生赋值转换为父类类型.派生类默认拷贝构造会自动调用基类拷贝构造函数.
3.赋值构造
派生类赋值拷贝时会先调用基类赋值拷贝,派生类传参派生类类型在父类赋值拷贝接收时发生赋值转换为父类类型.因为派生了和基类的复制拷贝构成隐藏关系,所以调用基类的赋值构造需要用基类名::函数名显示调用.派生类默认赋值拷贝会自动调用基类拷贝构造函数.
4.析构
派生类和子类的析构函数名会被编译器解释为destructor构成隐藏函数.所以自定义时不支持显示调用基类析构,派生类只需要析构自己的成员即可,编译器自动调用基类析构.析构的顺序是先派生类后基类,因为派生类中有可能会复用基类的成员,而基类中不可能复用派生类的成员,所以先析构派生类.
#include<iostream>
#include<string>
using namespace std;
class person
{
public:
//构造
person(const string& name="张三")
:_name(name)
{
cout << "person(const string& name)" << endl;
}
//拷贝构造
person(const person& p)
{
cout << "person(const person& p)" << endl;
}
//赋值构造
person& operator=(const person& p)
{
_name = p._name;
cout << "person& operator=(const person& p)" << endl;
return *this;
}
//析构
~person()
{
cout << "~person()" << endl;
}
void print()
{
cout << _name << endl;
}
private:
string _name;
};
class student :public person
{
public:
//构造
student(const string& name)
:person(name)
,_id(1211)
{
cout << "student(const string& name)" << endl;
}
//拷贝构造
student(const student& s)
:person(s)
, _id(s._id)
{
cout << "student(const student& s)" << endl;
}
//赋值拷贝
student& operator=(const student& s)
{
person::operator=(s);
cout << "student& operator=(const student& s)" << endl;
_id = s._id;
return *this;
}
//析构
~student()
{
cout << "~student()" << endl;
}
void print()
{
cout << _id << endl;
}
protected:
int _id = 101;
};
int main()
{
student s("李四");
cout << endl;
student s1(s);
cout << endl;
student s2("王五");
s2 = s1;
cout << endl;
cout << endl;
return 0;
}
五、菱形继承
1.菱形继承概念
一个类可以被多个派生类继承,多个类也有可能被派生类继承,这种多继承方式,可能会导致派生类中重复继承了基类多次,也就是菱形继承,菱形继承会导致数据的冗余和二义性.
示例
基类A同时被派生类B和C继承,D又同时继承了B和C,这就导致了A中的成员_a存在数据冗余和二义性.
#include<iostream>
#include<string>
using namespace std;
class A
{
public:
int _a=1;
};
class B :public A
{
public:
int _b = 2;
};
class C :public A
{
public:
int _c = 3;
};
class D :public B, public C
{
public:
int _d = 4;
};
int main()
{
D d;
d.B::_a = 10;
d.C::_a = 20;
return 0;
}
对象d无法直接调用_a,因为d中有两个_a,一个是B继承A的一个是C继承A的,随意要指明使用B中还是C中的_a.
2.菱形虚拟继承
菱形虚拟继承很好的解决了数据的冗余和二义性,菱形虚拟继承下的D类对象中只存有一个_a,并且对_a的访问是通过偏移量访问的.菱形虚拟需要再访问限定符前加virtual
#include<iostream>
#include<string>
using namespace std;
class A
{
public:
int _a=1;
};
class B :virtual public A
{
public:
int _b = 2;
};
class C :virtual public A
{
public:
int _c = 3;
};
class D :public B, public C
{
public:
int _d = 4;
};
int main()
{
D d;
d._a=20;
return 0;
}
虚拟继承在对象中加了用于被菱形继承类的成员变量的相对偏移量._a存储在d对象的最后面,d对象中还有继承了B和C中的成员,B和C中都有_a,但是B和C在对象d中的存储位置不同.那么对于Bb=&d和Cc=&d这样的使用场景来说b和c都需要知道_a的位置,所以B和C在对象d中都各自需要一个指针存储对于自己在d中位置而言的相对偏移量.
这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
菱形虚拟继承是为了解决数据冗余和二义性的问题,但是对于数据较小的_a来说没有很明显,但是对于较大的数据成员类说,虚拟继承也就是增加了两个指针8个字节而已,指针指向的空间也完全可以忽略不计,因为所以对象都可以共用这些空间,而不是一个对象对标一个.
六、组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合
//继承
class person
{
public:
string _name;
};
class student :public person
{
public:
int _id=1;
};
//组合
class person
{
public:
string _name;
};
class student
{
public:
int _id = 1;
private:
person _p;
};
七、应用题
#include<iostream>
#include<string>
using namespace std;
class A
{
public:
A(const char* s)
{
cout << s << endl;
}
};
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");
delete p;
return 0;
}
初始化列表是谁先声明就先初始化谁,对于基类谁先被继承就是谁先被声明.根据菱形虚拟继承的规则,A在D中只有一份,所以A只会调用一次.