继承是为了复用代码,成员的变量或者成员函数
class Person
{
public:
protected:
string _name="li";
int _age=1;
};
class Student :public Person
{
public:
void print()
{
cout << _age;
}
protected:
int _stuid=2;
};
子类student公有继承基类Person,子类,继承Person所有成员变量,相当于copy一份,成员函数在公有,但子类可以访问。
子类的访问受到继承方式和父类访问限定符的影响
父类的public | 父类的protected | 父类的private | |
public | 子类public | 子类的protected | 子类private |
protect | 子类protected | 子类protected | 子类private |
private | 不可见 | 不可见 | 不可见 |
不可见指的是类外都不能访问,private相比,至少类中可以访问。
实际上子类的访问=min(父类的访问限定符,继承方式)
protected 用于存放可以私有且子类可以访问。
不加继承方式,class默认私有继承,struct默认公有继承。
父类不能直接赋值子类(向下转换),子类可以给父类(向上转换),这是一种赋值兼容,把子类中父类的部分切割赋值给父类,而不是强制类型转换
student=person;//报错
person=student;
子类重定义/隐藏父类成员
子类和父类同名成员变量,成员函数,编辑器优先选择子类,通过父类域可以选择父类的变量,函数,编译器优先顺序:作用域>类域>全局域。
class Student:public Person
........
void Print()
{
cout<< num;
cout<< Person::num;
}
*注意不是重载关系,因为重载要在同一个作用域,而子类和父类分别为两个类域。
派生类的构造,拷贝构造,赋值重载
//基类
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;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
//子类
class Student :public Person
{
public:
Student()
:_id(0)
{
}
protected:
int _id;
};
子类Student继承了父类Person,继承相当于copy了一份父类的成员变量,因此我们理解子类中有_name和_id两个变量。但不能在初始化列表中初始化_name,但可以在函数内部中。
初始化列表相当于在定义时初始化,可以理解为编译器在基类的定义找不到父类中的_name声明。
实际上继承的父类的成员变量,在初始化列表,默认调用父类构造函数来初始化。
// 子类构造函数
Student()
:_id(0)
,Person("xiaoli")
{
_name="xixiaong"
}
注意Person的初始化优先于,_id的初始化
理解成父类成员变量的声明先于子类成员变量。
派生类的拷贝构造
Student(const Student& s)
:Person(s)
,_id(s._id)
{
}
用父类的拷贝构造,子类作为参数,拷贝构造父类的成员变量,我们此前所说的向上转换,子类给父类是指出的,再拷贝构造基类自身成员变量。
如果不显示调用拷贝构造,会调用默认构造。
派生类的赋值重载,除了子类向上构造,还有注意operator=()重定义的问题。
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_id = s._id;
}
return *this;
}
析构函数不用显式调用父类析构函数
析构函数名被特殊处理destructor,在子类析构调用后,会自动调用父类析构函数。来保证先子后父
~Student()
{
}
先子后父的原因:初始化列表时,先调用父类构造函数,所以结束时后调用父类析构函数。子类可能用到父类的函数,成员变量,如果父类被析构了,那么就会报错
友元关系不能继承。
静态成员变量属于子父类公有,子父类的静态成员变量是共有的,而不是复制的关系
class Person
......
protected:
string _name;
static int _count;
};
int Person::_count = 0;
多继承和菱形继承
Assiatant类继承了Student和Teacher两个类
class Assistant :public Student, public Teacher
{
protected:
string _majorCourse;
};
菱形继承存在的问题,student可能和teacher同时从Person那里继承了age,使得assistant类中有两个年龄造成数据冗余和二义性
虚继承解决冗余,二义性问题
// Student和Teacheer类虚继承了Person
class Student :virtual public Person
{
protected:
int _num;
};
class Teacher :virtual public Person
{
protected:
int _id;
};
二义性的证明
class A
{
public:
int _a;
};
class B : public A
{
public: int _b;
};
class C : public A
{
public: int _c;
};
class D : public C, public B
{
public: int _d;
};
由图可得_a有两个分别在B,C中。并且类A,B,C的变量依次排列
虚继承virtual
_a 的位置存放着一串地址
对应地址的下个位置的数值为20,12,恰好就是B,C到A的偏移量。
本质上保存的是_a的相对位置,那么为什么不直接保存A的地址,或者偏移量而是要保存指向一块空间的地址?如果存在多个D对象,因为相对位置是固定的,所以可以直接复用。d2 和d1的绝对地址是不一样的,但是相对位置是一样的。
右边的表又叫为虚基表,专门保存偏移量。
总结:把它单独划分出来便于复用。
虚继承后B,C,D 都报错相似的结构,有虚基表
偏移量的运用
当ptr无论指向B 还是C 都可以通过偏移量,找到A