继承
继承允许我们依据另外一个类来定义一个新的类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
继承方式
当继承一个类时,可以选择公有继承(public)、私有继承(private)和保护继承(protected)
公有继承 | 私有继承 | 保护继承 | |
---|---|---|---|
公有成员 | 公有的 | 私有的 | 保护的 |
保护成员 | 保护的 | 私有的 | 保护的 |
私有成员 | 不可见 | 不可见 | 不可见 |
- 基类的私有成员无论采用什么继承方式在子类都是不可见的。
- 基类中的公有成员和保护成员在子类中的方式为min(基类中的类型,继承方式)。取权限小的。
- class的默认是成是private,struct的默认继承方式是public;
- 私有和保护的区别,在继承中,私有的成员无论采用什么继承方式在基类中是不可见的,保护成员在基类中是可以访问的,在类外面都不可以访问。
派生类对象可以直接赋值给基类的指针、对象和引用,这种方式叫做切片。
基类的对象是不能直接赋值给子类的。
基类的指针可以通过强制类型转换将基类指针转换为子类指针,但是转换的前提必须是安全的。
作用域
当基类和子类中存在同名的函数或者变量时,子类将屏蔽对基类成员的访问,称为隐藏或重定义。
当基类和子类中存在同名的函数,即使参数不同,也构成隐藏。
#include <iostream>
using namespace std;
class A
{
public:
int a = 10;
};
class B :public A
{
public:
int a = 20;//和基类构成隐藏
void Print()
{
cout << a <<endl;
}
};
int main()
{
B b;
b.Print();
return 0;
}
当想要访问基类的a时候需要就需要指定作用域
void Print()
{
cout<<"访问基类的a "<<A::a<<endl;
cout << a <<endl;
}
成员函数构成隐藏
#include <iostream>
using namespace std;
class A
{
public:
void Print()
{
cout<<"基类的Print"<<endl;
}
};
class B :public A
{
public:
void Print(int i)
{
cout <<"派生类的Print"<<endl;
}
};
//虽然基类和子类的参数不同但是仍然构成隐藏。
int main()
{
B b;
b.A::Print();//指定调用基类中的Print
b.Print(1);//默认调用的是子类的。
return 0;
}
默认成员函数
默认生成的派生类的构造函数和析构函数会做什么?
- 对于继承下来的成员调用基类的默认构造和析构函数,对于自己的成员自定义类型调用自己的默认构造,内置类型不做处理。
- 拷贝构造和赋值同样也是。基类继承下来的调用基类的operator=和拷贝构造,自己的对于自定义类型调用自定义类型的,内置类型完成值拷贝。
什么情况下需要自己写?
- 父类没有默认的构造函数。
- 子类中有资源需要释放,就需要我们自己写析构
- 使用浅拷贝存在问题就需要我们自己实现拷贝构造和赋值。
怎么实现呢?
- 对于父类成员调用父类的构造、拷贝构造、赋值和析构(不需要显示调用)。
- 自己的成员按照普通类处理。
//子类析构函数不需要显示去调用父类的析构
#include <iostream>
#include <string>
using namespace std;
class A
{
public:
int* _ptr = new int[10];
~A()
{
cout << "~A()" << endl;
}
};
class B :public A
{
public:
~B()
{
A::~A();
}
};
int main()
{
B b;
return 0;
}
在子类的析构函数中,不需要我们显示去调用基类的析构函数,会自动调用。
在构造时,会先调用基类的构造函数,再调用子类的构造函数,同样在析构时,会先调用子类的析构函数,再调用基类的析构函数。
继承与友元
友元关系不能被继承。那就好比我和你爹是朋友,但我和你不一定是朋友。
静态成员
静态成员在整个继承中只会存在一份。
#include <iostream>
using namespace std;
class A
{
public:
static int count;
int b = 0;
A()
{
count++;
cout << "A的构造函数" << endl;
}
void Print()
{
printf("基类的 count %p\n", &count);
}
};
int A::count = 0;
class B :public A
{
public:
void Print()
{
printf("派生类 count %p\n", &count);
}
};
int main()
{
A a;
a.Print();
B b;
b.Print();
B c;
c.Print();
return 0;
}
可以看到他们的地址是相同的。
菱形继承
![image-20220404101256988](http://wkn-1256062000.cosgz.myqcloud.com/image-20220404101256988.png)
菱形继承就是一个基类被多个类继承,而这些派生类又同时被一个类继承。
#include <iostream>
using namespace std;
class A
{
public:
int _a;
};
class B :public A
{
public:
int _b = 10;
};
class C :public A
{
public:
int _c = 10;
};
class D :public B, public C
{
public:
int _d = 20;
void Print()
{
cout << _a << _b << _c << _d;
}
};
int main()
{
D d;
d.Print();
return 0;
}
这里面就会出现一个问题,B和C里面都有一个_a,当继承下来以后就有2份 —a,如果不指定类域就会出现调用不明确。并且造成了数据冗余和二义性。
虚继承
对于多继承可以会产生一些问题,C++给了一个解决方案叫做虚继承。
- 虚拟继承的目的是令某个类做出声明,承诺愿意共享它的基类,其中贡献的基类对象称为虚基类。在这种机制下,无论基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。
虚继承就是在继承是时候加上virtural关键字。
class B :virtual public A //虚继承
{
public:
int _b = 10;
};
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b = 10;
};
class C : virtual public A
{
public:
int _c = 10;
};
class D :public B, public C
{
public:
int _d = 20;
};
int main()
{
D d;
d._a = 1;
d._b = 2;
d._c = 3;
d._d = 4;
return 0;
}
通过查看d的内存可以看到,内存的布局是按照继承的先后顺序是一样的,在使用虚继承后可以看到,虚基类是共享的,只存在一份,在B和C里面各有一个地址,这个地址指向的是一个虚基表,虚基表里面存储了当前类到虚基类的偏移量。通过就可以找到A。
![image-20220404135632873](http://wkn-1256062000.cosgz.myqcloud.com/image-20220404135632873.png)
[外链图片转存中…(img-jqDNThby-1649052102493)]
通过查看d的内存可以看到,内存的布局是按照继承的先后顺序是一样的,在使用虚继承后可以看到,虚基类是共享的,只存在一份,在B和C里面各有一个地址,这个地址指向的是一个虚基表,虚基表里面存储了当前类到虚基类的偏移量。通过就可以找到A。
![image-20220404135632873](http://wkn-1256062000.cosgz.myqcloud.com/image-20220404135632873.png)