继承是面向对象中很重要的特性,今天就来讲讲C++中的继承。
文中不足错漏之处望请斧正!
是一种类的复用,可以让B类继承,从而使B类获得A类的所有成员。
A类叫做父类或基类,B类叫做子类或派生类。
而继承分为单继承和多继承。
子类只继承一个父类。
class 父类
{};
class 子类 : 继承方式 父类
{};
子类可通过派生类列表明确从哪个类而来。
class Base
{
int _b;
};
class Derive : public Base
{
int _d;
};
int main()
{
Base b;
Derive d;
return 0;
}
Derive类后跟冒号,冒号后是类派生列表,public是继承方式,Base是要继承的类。
通过继承我们能看见,Derive对象确实成功继承了Base类的成员。
但父类的不同成员,按照继承方式在子类中应该会得到不同的访问限定符吧?那限定符继承后到底是什么样的,有什么规律吗?
有的。
子类中父类成员的访问权限限定符规则,可看作一个min函数的调用结果:
min(父类成员在父类中的访问权限, 继承方式)
。
*private < protected < public
*父类的private成员同样会继承到子类,只是不可见
从这里其实我们也可以看出,C++想覆盖尽可能多的继承场景,但是实际上,除了public继承外,其他都不怎么用:
这里我们就可以谈谈protected有什么用了。
其实就是能被子类访问的private成员。
父类的
子类对象可单向赋值给父类对象,父类对象会拿到子类对象中父类的一部分,没有类型转换。
“父类对象会拿到子类对象中父类的一部分”,就像把子类对象中的父类部分切出来给父类一样,所以这种行为叫做切片。
注意:是子类单向可赋值给父类,父类并不能赋给子类,不然子类多出的一部分哪里来。
场景:
隐藏是一种子类对父类同名成员的屏蔽。
*若想访问父类同名成员,指定父类类域。
这里容易和重载弄混:
一句话:父子类部分分开处理。
构造和析构是先处理父类还是子类?
还有一点很特殊的,这是为了兼容多态而添加的一个特性。
父子类的析构,函数名都会被处理成destructor,是因为多态需要父子类析构同名,才有机会构成重写(不重写析构可能内存泄漏)。
因为父子类析构构成隐藏,所以调用父类析构是要指定类域的,不过一般不用调,因为子类析构调用完后会自动调用父类析构。
友元关系不会被继承,父类的友元不是子类的友元(你爹的朋友不一定是你的朋友)。
和以前的概念吻合:static成员在整个继承体系中只有一个。
这里强调一下类指针的意义,在继承中容易搞混。
struct A {
int _a;
void func() { cout << "func called..." << endl;}
static int _s;
};
int A::_s = 999;
int main() {
A *ptr = nullptr;
cout << ptr->_a << endl; //err
ptr->func();
cout << ptr->_s << endl;
return 0;
}
主要看有没有解引用找属性。
一个子类继承于多个父类多继承。
有场景:我既需要A类的属性,也需要B的属性
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._a = 10;
return 0;
}
err:
non-static member '_a' found in multiple base-class subobjects of type 'A':
class D -> class B -> class A
class D -> class C -> class A
d._a = 10;
^
_a
的时候,不确定是B::_a
还是C::_a
那么如何解决这两个问题呢?
虚拟继承的核心思想是共享公共基类的成员。
通过virtual继承方式派生出的派生类,实际上并不拥有虚基类成员,而只能通过偏移量访问。
*被派生类使用虚拟继承来继承的基类称为虚基类。
class A {
public:
int _a;
};
class B: virtual public A { //通过偏移量访问_a
public:
int _b;
};
class C: virtual public A { //通过偏移量访问_a
public:
int _c;
};
class D: public B, public C {
public:
int _d;
};
int main() {
D d;
d._a = 10;
return 0;
}
本来冗余的_a,不再被派生类拥有,而是通过偏移量访问了。
通过偏移量访问,它是怎么个访问法?
派生类虚拟继承自基类,派生类实际上会得到一个指针,这个指针指向一个关于A类的偏移量表。
偏移量表中有偏移量,还有A类部分的地址,因此通过偏移量表就能访问A类成员。这个偏移量表就叫做虚基表。
int main() {
D d;
d._b = 1;
d._c = 2;
d._d = 3;
d._a = 4;
return 0;
}
*32位机
&d,在前4个字节,我们首先看到的就是一个指针。查看指针指向的内容,发现了前四个字节是零值(应该是空指针),而后就是一个0x14,即20,这就是d对象要访问A类成员_a需要的偏移量。你数数,从d的第一个字节开始,往后20个字节,就是_a。
继承是一种“is-a”的感觉,比如Student是Person的复用。这种复用称为白箱复用,会暴露底层细节。
组合是一种“has-a”的感觉,比如B类中有A类的对象。这种复用称为黑箱复用,不暴露底层细节。
其中,继承耦合度 > 组合耦合度。
class B {public: int b;};
class C1: public B {public: int c1;};
class C2: public B {public: int c2;};
class D : public C1, public C2 {public: int d;};
A.D总共占了20个字节
B.B中的内容总共在D对象中存储了两份
C.D对象可以直接访问从基类继承的b成员
D.菱形继承存在二义性问题,尽量避免设计菱形继承
解:
A.C1中b和c1共8个字节,C2中c2和b共8个字节,D自身成员d 4个字节,一共20字节
B.由于菱形继承,最终的父类B在D中有两份
C.子类对象不能直接访问最顶层基类B中继承下来的b成员,因为在D对象中,b有两份,一份是从C1中继承的,一份是从C2中继承的,直接通过D的对象访问b会存在二义性问题,在访问时候,可以加类名::b,来告诉编译器想要访问C1还是C2中继承下来的b。
D.菱形继承存在二义性问题,尽量避免设计菱形继承,如果真有需要,一般采用虚拟继承减少数据冗余
今天的分享就到这里了,感谢您能看到这里。
这里是培根的blog,期待与你共同进步!