继承是一种简化代码的手段之一,使代码可以实现复用。它允许在原来类的功能上进行扩展,衍生出新的功能。也可以把多个类的基本信息提取到某个类,减少代码的冗余。其中被继承的类通常叫做父类或基类,继承下来的类叫做子类或派生类
子类名:继承类型 继承的父类。例如:以人类和学生类做例子
class person
{
public:
int _name;
int _age;
};
class stu:public person
{
int_ id;
}
其中,stu就是派生类,而person是父类
其中继承类型有三种:public,protect,private继承。其中:
就像什么呢?爸爸和儿子住一间房子下,虽然家里的东西大家可以共用,但是爸爸总有隐私不能让儿子发现,比如爸爸的私房钱
总结:基类其它成员在子类中的访问方式=min(基类访问限定符,访问方式),权限大小:public>protected>private。但实际中基本用public继承
class Father
{
public:
int _a;
protected:
int _b;
private:
int _c;
};
class Son :public Father
{
public:
int _d;
};
上述代码中,外部只能访问a和d,子类还可以访问b,但c只有父类能访问
首先声明:切片不是强制类型转换,而是语法特性的一种
派生类可以赋值给父类,但父类不能赋值给子类
Father f1;
Son s1;
f1=s1;
为什么能这么赋值呢?
我们知道,子类中的成员肯定是不少于父类的成员的。在赋值时,子类只需要把父亲没有的成员给丢弃后赋值给子类,就像切面包一样一刀切下去,所以也叫做切片
那有没有基类赋值给派生类的方式呢?有的,用指针就行了
Son s1;
Father* f1=&s1;
但通过指针父类只能访问它有的成员,访问子类成员时程序就会崩溃
使用注意:只建议在public继承下使用
我们知道,程序以一对花括号来表示不同的作用域,例如:
int a=0;
{
int a=1;
cout<<a<<endl;//1
}
cout<<a<<endl;//2
1处的a输出1,2处的a输出0.因为1的作用域在a=1内部,而2的作用域在外部
父类和子类它们属于不同的作用域,在有同名变量被声明时,使用起来也会发生上面代码类型的情况
class Father
{
public:
int _a;
};
class Son:public Father
{
public:
int _a;
};
Father f1;
Son s1;
f1._a=1;
s1._a=2;
cout<<s1._a<<endl;
在上面的情况,父类的成员将会被屏蔽,子类将会直接访问属于它的成员,输出为2
虽然语法上支持这种写法,但是,这样写代码非常容易造成二义性混乱,所以写代码要规避这种情况的发生
条件:只要父类和子类有相同名字的函数就构成隐藏(忽略参数和返回值)
class Father
{
public:
void func()
{
cout<<"Father"<<endl;
}
};
class Son:public Father
{
public:
void func(int i)
{
cout<<"Son"<<endl;
}
};
Father f1;
Son s1;
s1.func();//这么写会报错,因为隐藏了父类的func函数,所以提示缺少参数
s1.func(10);//正常调用
我们知道,在我们写一个类的时候,编译器通常会给我们形成以下的默认成员函数:
那么,由于派生类其中有父类的存在。派生类的这些函数到底是继承父类的呢还是自己实现的呢?
c++中采用了比较简单的方式,就是各管各的
总的来说,在派生类生成的默认函数中,父类的成员部分由父类的函数自己完成,而派生类独有的成员需要在派生类中自己实现。
如果派生类中没有显示的定义默认函数,那么生成的函数中,父类部分调用父类的成员函数,子类的按普通类处理
普通类没有定义默认函数的处理方式:内置类型不处理(随机值),自定义类型调用它的默认函数
class Father
{
public:
Father()
:_a(1)
{
}
int _a;
};
class Son :public Father
{
public:
int _b;
};
cout<<Son()._a<<" "<<Son()._b<<endl;
在上述代码中,a输出1,因为派生类中默认调用的父类的构造。b输出随机值,因为派生类中未作处理
如果定义了,会有一个顺序问题,构造函数会先调用父类,而析构函数先调用派生类
如果父类没有默认构造或其它成员函数,那么在派生类中必须显示调用父类的一部分
class Father
{
public:
Father(int i)
:_a(i)
{
}
int _a;
};
class Son :public Father
{
public:
Son()
{
}
int _b;
};
这样的话会直接编译报错,在子类中显示调用就不会
而且在显示调用时,只能在初始化列表中调用
class Son :public Father
{
public:
Son()
:Father(1)
{
}
int _b;
};
c++中允许一个派生类继承自多个基类,连接继承关系时使用逗号
class A
{
public:
int _a;
};
class B
{
public:
int _b;
};
class C :public A, public B
{
public:
int _c;
};
c可以访问到a和b两个成员变量
但是多继承是c++的一个早期设计的大坑,它有可能会带来数据冗余和二义性的问题
看下面的代码
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;
};
它们的继承关系是这样的
我们可以发现,在D这个类中,_a有两份!
因为D分别继承B和C,而BC都继承了A,所以D自然继承了BC中都包含的A的那一部分,造成了数据的冗余
而在使用的时候,不能直接这样
D d;
d._a=1;
会提示数据不明确而报错
解决方式:在_a前加上限定符
d.B::_a=1;
这个就是菱形继承了
但这么写相当的奇怪,我们必须想办法解决
我们先观察一下D类的内存情况吧
D d1;
d1.B::_a = 1;
d1.C::_a = 5;
d1._b = 2;
d1._c = 3;
d1._d = 4;
其中谁先继承,谁就在内存的偏前位置
我们看到,圈出来的1和5都是属于A类的,确实冗余了
为了更正这个问题,后面提出了虚继承的概念
虚继承的使用,就是在可能会造成二义性的类前加入virtual修饰,(下面的B和C,因为同时继承了A,就可能造成二义性)这样就解决了数据二义性问题
class A
{
public:
int _a;
};
class B:virtual public A
{
public:
int _b;
};
class C :virtual public A
{
public:
int _c;
};
class D:public B,public C
{
public:
int _d;
};
cout<<D._a<<endl;//这样使用就不会出现不明确的问题了
那么,虚继承的原理是什么呢?同样先观察内存
我们发现,我们能找到我们定义的变量,但中间的一些奇怪的数字是啥呢?
看着结构有点像地址,我们再跟踪地址找找看吧
我们发现它指向了一个值一个14一个0c,因为这是16进制,我们把它转换为10进制
然后在加上B类和C类中的地址,我们发现它居然指向一个值,就是本章第一张图片下面的01
那么,结论就来了
虚继承中会在每个派生类类中加入一张虚基表。先把父类的成员放在一个公共区域,虚基表中存了公共区域相对于此类的偏移量,那么每个类就可以计算出父类成员的位置了