继承
继承与派生
- 继承是一种创建新类的方式,新建的类可以继承一个或多个类。
- 所以继承描述的是类和类之间的关系
- 新建的类被称之为派生(子)类,之前就存在的类被称之为基(父)类
继承分为单继承和多继承。
继承是在子类的角度来看,派生是在父类的角度来看叫派生。
举例:
class A //父类
{
int a;
public:
void setA(int a)
{
this->a = a;
}
};
class B : public A //子类,通过父类派生出来的
{
int b;
public:
void setB(int b)
{
this->b = b;
}
};
void main()
{
printf("%d\n", sizeof(A));
printf("%d\n", sizeof(B));
}
观察以上结果可知,B类继承A类时会增大内存,增大的内存即为A类的内存。
问题:如何在B类中访问A的数据成员?
答:在以上示例中,A类中的a成员为私有属性,将其改为保护属性或公有属性即可在B类中访问。(保护属性在类外不能访问,在类内可以访问)
继承方式
继承方式有三种:
- 公有继承
- 私有继承
- 保护继承
先看下面一个表:
基类中成员的访问属性 | 公有继承 | 保护继承 | 私有继承 |
---|---|---|---|
公有 | 公有 | 保护 | 私有 |
保护 | 保护 | 保护 | 私有 |
私有 | 私有 | 私有 | 私有 |
不管是什么方式继承,父类的数据成员都会被继承到子类中。访问属性和继承方式谁更严格,在子类中按更严格的属性来。
继承之后
- 派生类会继承除基类的构造析构函数之外的所有成员变量和成员函数。
- 可以在派生类中添加新的成员,通过派生类对象来调用。
- 如果派生类中添加的成员名和基类成员名相同,那么派生类会隐藏基类的成员。
举例:
#include<iostream>
class A //父类
{
public: //保护属性
int valA;
public:
A()
{
valA = 0;
}
};
class B : public A //子类,通过父类派生出来的
{
public:
int ValB;
public:
int ValA;
B()
{
ValB = 1;
}
};
void main()
{
B b; //定义一个B类型的对象
B *pb = &b; //定义一个B类型的对象指针,指向b这个对象的首地址
pb->ValA = 80;
int c = 0;
}
本例结果对应以上第三点。
- 常见错误:基类不能赋值给派生类。原因:派生类是八字节,基类是四字节,把一个四字节赋值给八字节,剩下的四字节没有填充,发生二义性问题。
- 继承的构造和析构顺序:
构造顺序:先基类再子对象,再派生类
析构顺序:先派生再子对象,再基类
#include<iostream>
class A //父类
{
public:
A()
{
printf("A\n");
}
~A(){ printf("~A\n"); }
};
class C
{
public:
C()
{
printf("C\n");
}
~C(){ printf("~C\n"); }
};
class B : public A //子类,通过父类派生出来的
{
public:
C cc; //子对象
public:
B(){ printf("B\n"); }
~B(){ printf("~B\n"); }
};
void main()
{
B b; //定义一个B类型的对象
int c = 0;
}
- 如果有多个基类,按声明顺序从左至右顺序构造;如果有多个子对象,按从上往下顺序构造。
- 在构造过程中,基类和子对象需要带参构造,可以通过成员初始化列表来传参。
#include<iostream>
class A //父类
{
public:
A(int a)
{
printf("A\n");
}
~A(){ printf("~A\n"); }
};
class C
{
public:
C(int c)
{
printf("C\n");
}
~C(){ printf("~C\n"); }
};
class B : public C, public A //子类,通过父类派生出来的
{
public:
B(int a, int c) :A(a), C(c) //成员顺序表
{ printf("B\n"); }
~B(){ printf("~B\n"); }
};
void main()
{
B b(1,2); //定义一个B类型的对象
int c = 0;
}
小结:
- 派生类对象在实例化的时候是会调用基类的构造函数的,先基类后派生类。
- 如果是多继承与单继承中构造顺序一致,区别在于,在构造基类时有多个基类,那么会按照基类在初始化列表的声明顺序来依次调用基类的构造函数。
- 所以在写继承的时候,要确保基类有可以调用的构造函数。
菱形继承
举例:
#include<iostream>
class A
{
public:
int a;
public:
A(){ a = 1; }
};
class B : public A //8字节
{
public:
int b;
public:
B(){ b = 2; }
};
class C : public A
{
public:
int c;
public:
C(){ c = 3; }
};
class D :public B, public C
{
public:
int d;
public:
D(){ d = 4; }
}
void main()
{
printf("A::size=%d\n", sizeof(A));
printf("B::size=%d\n", sizeof(B));
printf("C::size=%d\n", sizeof(C));
printf("D::size=%d\n", sizeof(D));
}
观察以上代码可知,D类继承于B、C类,而这两个类继承同样的A类,这样就构成了菱形继承。但是,因为B、C类都继承于A类,当给A类中的数据成员赋值时,会出现二义性问题。
- 解决方法1:通过域访问去访问菱形继承里面的两个一样的基类成员。
- 解决方法2:通过虚继承来解决,让菱形继承里面不会产生两个一样的基类。
关键字:virtual。类中有virtual关键字,会增加4字节,会导致虚基类在内存中只有一根拷贝。
虚基类优先于基类构造。(构造顺序:虚基类->基类->子对象->派生类)
virtual的在以上例子中的使用过程解析:在构造D类时继承链里有虚基类,所以先构造虚基类A,构造B类时,继承链中有虚基类,先构造虚基类,但是在构造D类时已经构造了,所以这个virtual关键字就是告诉B、C类,A类已经被构造了。