简单的举一个例子:
发现两者有共同的部分:姓名,年龄
所以我们可以定义一个基类包含姓名,年龄;使得学生类和教师类都继承此基类。
#include
#include
using namespace std;
//基类
class person
{
public :
person(const char *name="zhangsan", int age = 0)
:_name(name),
_age(age)
{
}
string _name;
int _age;
};
// 派生类
class student :public person
{
public:
student(const char* name="zhangsan", int age=0, int id=0)
:person(name,age),
_stu_id(id)
{
}
private:
int _stu_id;
};
// 派生类
class teacher :public person
{
public:
teacher(const char* name="zhangsan", int age = 0, int id = 0)
:person(name, age),
_tea_id(id)
{
}
private:
int _tea_id;
};
格式就是:

所以只需要有:

派生类继承基类的方式有:public,protected,private。但是基类中又有public,protected,private的权限限制。所以事情就搞复杂了。比如:用public方式继承,对于基类中的public,protected,private成员,是如何继承的?是有对应的关系的。
(1) 先看看public继承:
class A
{
public:
void Print_pubilc()
{
cout << "pubilc ok" << endl;
}
protected:
void Print_protected()
{
cout << "protected ok" << endl;
}
private:
void Print_private()
{
cout << "private ok" << endl;
}
int _private_A;
};
class B :public A
{
public:
};
在main()函数中,调用看看情况:
int main()
{
B hh;
hh.Print_pubilc();
}

可以看到pubilc继承,声明的对象可以pubic权限来访问继承的继承;protect继承下来的如何使用呢?可以在派生类中通过函数来使用,所以在上面的接口中加入一个函数:
class B :public A
{
public:
void Bprint_protected()
{
Print_protected();
}
};
再在main()函数中使用:
int main()
{
B hh;
hh.Print_pubilc();
hh.Bprint_protected();
}


还有一个private成员,pubilc方式继承,会是什么情况?发现根本看不到继承下来的private成员,也就是不可见。
(2) protected方式继承
class B :protected A
{
public:
};

可以看到之前可以直接使用继承下来的public不能通过派生类对象直接使用了。那么protected方式继承后,基类的public成员会变为派生类的何种权限成员呢?猜测是protected权限,我们在派生类中使用函数看看是否能访问。
class B :protected A
{
public:
void Bprint_protected()
{
Print_protected();
Print_pubilc();
}
};
int main()
{
B hh;
hh.Bprint_protected();
}

可以看到,确实是变为protected权限了。基类的private成员还是不可见。
(3) private方式继承
派生类以private的方式继承了基类的public,protected权限成员后,它俩的权限都会变成private权限。当然在派生类中,依旧可以调用继承下来的这两种权限的成员,貌似和protected继承的区别不大。但是如果有孙子呢?再来一个派生类去继承上面的派生类,是不是就会导致从爷爷那里继承的成员变成不可见,无论以何种方式继承。
(4) 继承方式的总结

注意:实际上,一般使用public方式继承,那俩种继承方式很少用
基类和派生类可以互相赋值吗?其实也可以,派生类可以赋值给基类,但是基类不能够赋值给派生类。这个也好理解,派生类一般大于基类,兼容基类的内容,所以可以完成赋值;基类不能够完全兼容派生类的内容,所以不能够赋值给派生类。
可以举个例子:
用上面的student类举例:
int main()
{
person hh;
student ly("张三",21,2002040207);
hh = ly;
return 0;
}
我们再可以试试 将hh赋值给ly;
ly =hh;
报错了:

如果是指针和引用呢?
int main()
{
person ll;
student ww("小李子", 20, 200202202);
// 子类给父类
person* ptr = &ww;
person& tr = ww;
/
// 父类给子类
student* str = (student*)≪
return 0;
}
上面的内容讲如何使用,接下来,来揭晓一下赋值转换的原理:
子类对象可以赋值给父类对象/指针/引用,这种叫做切片或者切割。

用person指针去指向student,会切片,也就是只会指向继承了person的那一部分。

person引用,也只会引用继承了的那部分。
student指针指向person也是可以的,不过需要强制,还有越界的风险。

这个用法也是会被用到的,不过用的少,毕竟有风险嘛。
在讲之前,先聊聊作用域:基类和派生类的作用域是不同的。所以基类和派生类中函数名相同,并不构成重载,而是构成隐藏,作用域不同。
隐藏并不是没有继承,还是继承下来的,我们都知道作用域中有个原则叫做就近原则。
比如:全局变量,局部变量
#include
using namespace std;
int a = 10;
int main()
{
int a = 0;
cout << a << endl;
}
认为是打印出哪个值呢?

很明显打印的是局部变量 a的值,这就是就近原则,当然隐藏也是同理,我们在调用子类的同名函数时默认调用的时子类的函数,而不是父类的函数,如果想要调用父类的函数需要指定作用域。回到上面的程序,想要打印全局变量a的值,指定作用域就好了。
int main()
{
int a = 0;
cout << :: a << endl;
}

我们来举一个 类 的例子:
#include
using namespace std;
class A
{
public:
void Print()
{
cout << "i am father" << endl;
}
};
class B : public A
{
public:
void Print()
{
cout << "i am child" << endl;
}
};
int main()
{
B s;
s.Print();
//指定域名就好了
s.A::Print();
return 0;
}

重写就是对继承下来的函数进行了覆盖,也就说:子类重写实现的功能和父类的功能完全不一样,这个多用于多态。
重写满足的条件有两个:
基类有默认成员函数,关键的是:构造,析构,拷贝构造,赋值重载。子类会继承父类的默认成员函数,所以一般情况下,子类是不需要管父类继承下来的成员的。
什么情况下必须自己写?
我可以举一个例子:
#include
using namespace std;
class A
{
public:
A(int a=0)
:_a(a)
{}
~A()
{
cout << "~A()" << endl;
}
int _a;
};
class B:public A
{
public:
B(int b=0)
:_b(b)
{}
~B()
{
cout << "~B()" << endl;
}
int _b;
};
int main()
{
B nb;
return 0;
}
我创建了一个B类型的对象,继承了A类型,但是A类型有默认的构造函数。不过我们来查看一下,析构的情况。看到了,会自动析构父类的资源。

(1) 第一种情况:父类没有默认的构造函数。
#include
using namespace std;
class A
{
public:
A(int a)
:_a(a)
{}
~A()
{
cout << "~A()" << endl;
}
int _a;
};
class B:public A
{
public:
B(int a,int b=0)
:A(a),
_b(b)
{}
~B()
{
cout << "~B()" << endl;
}
int _b;
};
int main()
{
B nb(2);
return 0;
}
可以看到上面的A类没有默认的构造函数,需要显示的传参,所以子类B的构造,必须包括给A的传参。

(2) 第二种情况:子类有资源需要释放,就需要自己显示写析构
子类有资源需要释放,当然需要手动的写析构,但我有个疑问,父类的析构需要我们显示的调用嘛?
#include
using namespace std;
class A
{
public:
A(int a)
:_a(a)
{}
~A()
{
cout << "~A()" << endl;
}
int _a;
};
class B:public A
{
public:
B(int a,int b=0)
:A(a),
_b(b)
{
arry = new int[a];
}
~B()
{
delete[] arry;
A::~A();
cout << "~B()" << endl;
}
int _b;
private:
int * arry;
};
int main()
{
B nb(2);
return 0;
}
我在B类中加了一个私有成员,它需要开辟空间,所以也需要手动写析构函数,于是我还显示的调用了父类的析构函数,但是有一个细节A::~A();,我指定了作用域才显示的调用了~A(),难道它是构成了隐藏嘛?
看一看结果:

父类被析构了两次,这是明显错误的,所以我们根本不需要手动的调用父类的析构函数,我们在子类的析构之后会默认的调用的父类析构函数,这是c++的优化,为了控制析构的顺序,先析构子类的独有资源,再析构父类的资源(方式就是调用父类的析构函数)。
解释一下:上面的细节,我指定了类域才能显示的调用父类的析构,这确实构成了隐藏,虽然父类和子类的析构名不同,但是编译时,都会被看作成一个函数名为delete(),这也是C++大佬的设计思想。
(3) 第三种情况:子类存在深拷贝问题,就需要自己实现拷贝构造和赋值解决深拷贝问题
对于深拷贝问题,只需要子类中,处理自己的深拷贝问题就可以了,父类去调用自己拷贝构造和赋值构造即可。
#include
using namespace std;
class A
{
public:
A(int a)
:_a(a)
{}
~A()
{
cout << "~A()" << endl;
}
A& operator =(const A& nb)
{
this->_a = nb._a;
}
int _a;
};
class B:public A
{
public:
B(int a,int b=0)
:A(a),
_b(b)
{
arry = new int[a];
}
~B()
{
delete[] arry;
cout << "~B()" << endl;
}
B(const B& nb)
:A(nb),
_b(nb._b)
{
arry = new int[nb._a];
memcmp(arry, nb.arry, sizeof(nb.arry));
}
B& operator =(const B& nb)
{
if (this != &nb)
{
A::operator=(nb);
_b = nb._b;
delete[] arry;
arry = new int[nb._a];
memcmp(arry, nb.arry, sizeof(nb.arry));
}
}
int _b;
private:
int * arry;
};
int main()
{
B nb(2);
B wb(nb);
return 0;
}

总结: 综上就一个原则: 继承父类的资源由父类管,子类的资源由子类管。
这里简单的说一下:父类的友元函数不是子类的友元函数。
所以父亲的朋友不是儿子的朋友,简单记一下就ok了。
类的静态成员属于类,不是属于具体某个对象的,所以派生类也可以继承父类的静态成员,也可以修改静态成员,因为静态成员属于所有类对象以及继承了静态成员的派生类,都共用一份。
静态成员的初始化,需要在类外,而且是基类来初始化,派生类是做不到初始化的。
如果想要修改静态成员的值,可以封装到函数中进行修改。
举个例子:
#include
using namespace std;
class A
{
public:
static int _sta;
A()
{
_sta++;
}
};
int A::_sta = 0;
class B :public A
{
};
int main()
{
B _a;
cout << _a._sta << endl;
B _b;
cout << _b._sta << endl;
B _c;
cout << _c._sta << endl;
return 0;
}

菱形继承是什么?其实我们应该避免设计出菱形继承,这真是很复杂。但是面试会问,笔试会考就来讲讲。
(1) 单继承:一个子类只有一个直接父类时称这个继承关系为单继承

(2) 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

(3) 菱形继承:菱形继承是多继承的一种特殊情况。

数据冗余:代码中继承了两份爷爷对吧,我只想要继承一份,但是却继承了两份,这是数据冗余
二义性:这是因为继承了两份爷爷里面的内容,导致内容重名了。
可以举个例子:
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;
};
这就是个简单的菱形继承,我们来使用一下吧!
(1) 二义性的测试
int main()
{
D dd;
dd._a = 1;
}
报错了,对_a的访问不明确,不知道是在访问谁的_a,B和C都继承了A中的_a,然后由D全继承下来。


解决方案: 可以指定作用域对吧,当然还有一个解决方案,一会讲。
int main()
{
D dd;
dd.B::_a=1;
dd.C::_a = 2;
}
(2) 数据冗余
为了展示,我将类A中的成员,变成一个数组:
class A
{
public :
int _a[100000];
};
测试一下大小:
int main()
{
D dd;
cout << sizeof(dd) << endl;
}
可以看到相当的大,我只想要一份的,但是给了两份(也有可能想要两份):

解决方案: 虚拟继承就可以解决数据冗余,在腰部也就是直接和爷爷继承的地方,搞成虚拟继承。

通过内存窗口以及监视窗口,我们来具体的观察一下这个实现过程:
将类A还原:
class A
{
public :
int _a;
};
int main()
{
D dd;
dd.B::_a = 2;
dd.C::_a = 4;
dd._c = 1;
dd._b = 3;
dd._d = 16;
}
取到D对象dd的地址,我们来查看一下:


可以看到,这个菱形继承是这样继承的,以及储存的。
先继承的B,所以B在上面;如果想让上面是C的话,可以调整一下继承的顺序。然后D的数据在最下面。

有了这个理解后,我们来正式解决问题:用虚继承,其余不变,方便查看
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};

我们来看看,虚继承之后的结果:

虚继承下来后,从爷爷A那里继承的数据,放到了最底部,本来在B处存_a,以及C处存的_a变成了不知道的数据。这样就解决了数据冗余。只保留了一份。
那么现在就只剩下一个问题:本来在B处存_a,以及C处存的_a的位置,存的是什么?看的像一个地址,我们来试一试:


可以看到:一个是12,一个是20,这是十六进制。保存的就是B和C距离_a偏移量,可以算算两地址的差值,发现就是一个是12。一个是20。
多继承真的有的复杂,导致后面出来的语言都砍掉了多继承这个功能,现在来讲讲继承和组合的优缺点。
一般情况下,优先考虑组合,因为组合对类的封装保护的很好,继承的话一定程度上破环了父类的封装。所以记住这个原则:能用继承也能用组合,那就用组合。