菱形继承不好理解,建议自己看完代码再回想一遍
目录
1.继承的理解
继承是类设计定义层次的复用,允许程序员在保持原有类特性的基础上进行扩展,呈现了面向对象
程序设计的层次结构
比如Student类 继承 Person类 ,Person类叫做父类 对应地 Student叫子类,或者,Person类叫做基类 对应地 Student叫派生类
派生这个词很形象
我们可以把派生类看成一种基类的延伸
派生类的内部有基类成员
2.继承的格式书写
可以看到,派生类的内部有基类成员
3.继承关系和访问限定符
访问限定符:public protected private
继承方式:public private protected
两个的限定程度 public>protected>private
基类成员访问方式:
助记:
基类的private肯定类外的都不可见,这是private设计初衷,即使继承也看不见
当继承方式和访问限定符权限不统一时,取权限小的
- 不写继承方式时,class默认私有继承,struct默认公有继承
4.基类和派生类对象赋值转换
派生对象可以赋值给基类对象,但是基类对象不能赋值给派生对象(向上转化,不允许向下转)
现在子类有个变量想赋值给父类,这个看起来很合理
这个叫切片,把父类需要的变量切走
但是父类的变量赋值给子类
这样一看就很荒谬,因为父类的成员不可能比子类还多,如果多,那也是私有成员,不能被继承的
回顾内置类型的隐式类型转化
把a赋值给b,会发生隐式类型转换(具体内容看内含隐式类型转换),但是把a赋值给c会报错
因为a会先产生临时变量,该变量是double类型,然后在把临时变量赋给c这个引用
但是临时变量有常性,赋值给c相当于权限放大(临时变量只读,但是c可读可写)
加上const对c限制只读就行了
可以看到只能子类给父类,不能反向
可以看到 pp和rp都完成了切片,并且没有报错,说明并没有产生有常性的临时变量
因此基类和派生类的赋值转化 中间不存在类型转化,没有产生中间变量
虽然基类不能赋值给派生类(绝大多数一般情形下),但是基类指针可以通过类型强转赋值给派生类
父类指针强转后赋值给子类,可能会越界
5.继承中的作用域
基类和派生类都有独立的作用域
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义
隐藏只要是同名函数即可,对参数列表没有要求,所以他可以伪装得很像重载
两个fun构成什么关系:重写,重定义,重载,编译报错
肯定不是重载,重载要求两个函数在同一个作用域,但是基类和派生类有独立的作用域
重写目前不讲,肯定也不是他
那一定就是重定义
第一个函数调用的结果表是什么?打印A,打印B,编译报错
答案是编译报错,因为子类B不能直接访问父类A的同名成员函数,并且B::fun()没有缺省情况
第二个函数调用没有问题
想访问父类的同名成员函数怎么搞?指定作用域显示访问
建议:实际中在继承体系里面最好不要定义同名的成员,不给自己找麻烦
6.派生类的默认成员函数
普通类:
构造/析构:内置类型不处理,自定义类型调用自己的析构/构造
拷贝构造/赋值重载:内置类型浅拷贝,自定义类型调这个成员的拷贝/赋值
派生类的成员变量:
0.基类对象——————————>调用过基类的对应函数实现四个功能(构造/析构/拷贝/赋值)
1.派生类自己的内置类型————————————>和普通类保持一致
2.派生类自己的自定义类型———————————>和普通类保持一致
特殊地
- 析构函数
1.子类析构函数和父类的析构函数默认构成重定义,由于多态的关系需求,所有析构函数都会特殊处理成destructor(函数名)
2.子类先析构,父类先构造,派生析构函数中无需显示调用基类析构,派生类析构之后自动调用父类析构
很好理解,构造肯定先有基类,析构肯定不能一上来先把基类弄没
class A
{
public:
void fun()
{
cout << "A" << endl;
}
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
int _age;
};
class B:public A
{
public:
void fun(int i=0)
{
cout << "B" << endl;
}
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
int _num;
};
int main()
{
B b;
return 0;
}
- 友元关系不能继承,也就是说基类友元函数不能访问子类私有和保护成员
- 静态成员
普通成员:子类和父类不是一份
静态成员:属于整个类的所有对象,同属于派生类及其对象,子类和父类是一份
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;
Student s3;
Graduate s4;
cout << " 人数 :" << Person::_count << endl;
Student::_count = 0;
cout << " 人数 :" << Person::_count << endl;
}
最后的结果:4 0
三个类构成单继承关系,每次创建一个子类对象都会被调用父类的构造函数,count++
最后只把Student类的count置空整个类的count都改变,说明三个类使用的一个count,因为他是常量在静态区,和之前的知识不矛盾
7.单继承和多继承
多继承中有一种特殊的继承——菱形继承(继承的形状像菱形)
菱形继承有问题
class Person
{
public:
string _name; // 姓名
// id 家庭住址 身份证号码
};
class Student : virtual public Person
{
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
// 数据冗余/二义性
int main()
{
Assistant a;
a._name = "小张";
a.Student::_name = "张三";
a.Teacher::_name = "张老师";
return 0;
}
在Assistant的对象中,Person成员有两份,可以发现,每次改变name,Person的名字就会变,如果Person里面的成员变量是int id表示身份证,难道小张,张老师,张三会有三个id?(二义性)
class A
{
public:
int _a;
};
// class B : public A
class B : public A
{
public:
int _b;
};
// class C : public A
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;
}
很明显看到代码冗余
怎么解决菱形继承的坑?虚继承 virtual
虚继承只能在菱形的腰部
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;
}
变成了这样,发现A位置改变了,并且BC里面有一个奇怪的地址,是B和C的两个指针 的地址,指针指向一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量
可以找到下面的A
为什么要找到BC自己的A?
出现这样的情况,只有通过d找到BC自己的A才能顺利赋值
- 组合
这个是我们学的继承,特点是:白箱复用,耦合度高:任何一个成员调整都可能影响B
这个是组合,特点:黑箱复用,耦合度低:只有M中的公有函数调整才会影响N
8.总结
优先使用对象组合,而不是类继承
多继承可以认为是C++的缺陷之一