继承是类设计层次的复用
1.根据表中我们可以看到 基类的私有成员在子类不可见,但还是被继承了下来
2.根据继承方式和成员在基类的访问限定符小的那个来决定了子类访问基类成员的访问方式
例如如果是public继承,那么基类中protected成员继承到子类中访问限定符就是protected(类外不可访问,类内可以访问)
因为private和protected在类和对线阶段他们没什么区别,都是类内可以访问,类外不可访问,但是到了继承这里,private成员的不可见,导致proteced的出现
是的,他们互相独立
但单只成员变量,不是指的成员函数,成员函数在代码段(常量区)
子类是可以赋值给父类的,称为向上转换
并且这种转换是天然支持的,而且不生成临时空间
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
//protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。调用Print可以看到成员函数的复用。
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
ps = st;发生了什么?
st把自己父类的部分拷贝给给了ps,并且没有开临时空间
person& rp = st;
这句没加const即可证明没开临时空间
rp是直接引用student的一部分,这样的话随着rp的改变,子类也会被改变
如果是指针的话,同样只想子类中父类的那一部分
我们可以看到用指针引用修改对象,他们父类部分都会跟着改变
对象的转换都是不可以的
但如果是本身就指向子类的父类指针,再把这个父类指针转换回子类是可以的
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
成员函数和成员变量都会发生隐藏
这种隐藏遵循了局部域优先,子类里面找到了就不会去父类里找
// 两个fun构成什么关系?
// a、隐藏/重定义 b、重载 c、重写/覆盖 d、编译报错
// 答案:a (父子类域中,成员函数名相同就构成隐藏)
重载是在同一作用域中利用函数名修饰规则来区分不同的函数,如果没有函数名修饰规则的话就无法区分。
隐藏是在不同的类域中直接就可以区分出来不同函数。
总结:
尽量不要使用隐藏,搞出同名成员
如果隐藏了只能指定类域访问成员
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
{
先父后子
C++规定了子类的构造必须调用父类的构造函数初始化父类的成员(在初始化列表调用)
他把父类整体当成一个对象成员来处理,父类的成员交给父类的构造,派生类初始化自己的成员
编译器规定你不能在初始化列表显示初始化基类成员,但在函数体内可以,这就是规定
同样禁止直接初始化父类
需要注意的是调用父类拷贝构造需要一个父类对象的引用,这里没有,但是直接传s就可以,因为会发生切片,把s1的父类部分引用给s2,那么拷贝构造就正常走了
注意如果不写Person(s),那么默认构造调用父类的默认构造,与预期拷贝s的父类不符
理念就是父亲干父亲的活,孩子干孩子的活
先子后父
涉及子类使用了父类成员,就必须先析构子类再析构父类
父类的析构函数编译器自动帮助我们调用了
.
涉及了多态导致析构函数名都被统一处理destructor,子类和父类的析构函数发生了隐藏
class Person
{
public:
string _name; // 姓名
int _age;
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
数据冗余的本质是浪费空间
二义性是指的我们不知道要访问哪一个,访问谁不确定
在腰部位置加上virtual,变成虚继承,让_age只有一份,就可以解决数据冗余二义性
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : 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里面有个B在包含,监视窗口只是方便看
监视窗口看起来有三份,已经不准了
我们还是看内存窗口
图中关于虚基表中预留位置00 00 00 00 是给谁预留的呢?图中说的不对
这个位置应该是给A类的父类预留的,具体的继承结构图是这样
class AA
{
public:
int _aa=7;
};
class A : virtual public AA
{
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
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
d._a = 0;
return 0;
}
它利用了虚基表存储了A的偏移量,让A的成员变量始终只有一份
那存储这个偏移量有什么意义呢?
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
{
public:
int _d;
};
//int main()
//{
// D d;
// d.B::_a = 1;
// d.C::_a = 2;
// d._b = 3;
// d._c = 4;
// d._d = 5;
// d._a = 0;
//
// return 0;
//}
int main()
{
D d;
d._a = 1;
B b;
b._a = 2;
b._b = 3;
B* ptr = &b;
ptr->_a++;
ptr = &d;
ptr->_a++;
return 0;
}
值得注意的是,B对象在虚继承后也发生了变化,在访问_a时也利用虚基表偏移量来访问
B* ptr 作为一个基类指针,它既可以指向B,也可以指向派生类D。
但是当B* ptr指向D时,发生切片,对于B* ptr来讲指向的还是一个B对象,所以它不知道指向B还是D
但是有了虚基表和偏移量,不管ptr指向谁,我都按照偏移量来修改成员变量
菱形继承大小 20 菱形虚拟继承大小 24
我们看到菱形虚拟继承反而大于菱形继承
此时花费了2个4字节指针,使得原来2个_a变成1个_a,收益是4,这肯定亏了,原因在于A对象成员大小不够大
我们将A对象成员改成数组,这样在菱形虚拟继承时,只有一份数组,而不是两份数组,D对象大小减少大概一半
菱形继承会有性能损失
搞出菱形继承就需要菱形虚拟继承,在写构造函数时就尤为复杂
多继承谨慎使用,避免搞出菱形继承