C++中的继承——第一篇
一、继承本质
1.1继承的版式
继承的本质是复用
版式:
class Student : public Person
{
public:
int stuid;
int major;
}
1.2继承中发生的事
与实际生活当中的继承不同,此处的继承部分与原来的不是同一份,而是会把成员变量与成员函数的声明拷贝下来一份,即修改子类的时候,父类不受影响(当然,函数还是同一个)
1.2补:
父类与子类的构造函数不是同一个,仅仅是子类构造中可以调用父类的构造
二、继承的访问
2.1访问的分类
由三种继承方式和三种访问限定符共同组成,共有九种。
实际上,___继承也可以理解为继承为派生类的___
2.2继承情况的表格
其中最常用的是
2.2补: “不可见”是什么
意思是“在子类中,但是子类用不了”,即父类私有成员变量虽然被继承了下来,但是不论子类的类内部还是类外部都不可以直接访问使用
不过若是执意要用也可以通过调用父类成员函数来间接使用
2.3关于访问的总结
①基类的private成员不管在什么继承都是不可见的,它虽然被继承到了派生类类当中,但是不论派生类的类内部还是类外部都不可以直接访问使用
②保护限定符因继承而出现:在一个类中,保护限定符与私有限定符唯一的区别就是此类的派生类类可不可以直接访问
③基类的私有成员在子类中全都不可见,其他成员基类中其他成员继承到子类中的访问方式是继承方式和访问限定符中较“小”的一种(假设大小关系为:public>protected>private)
④继承关键字可以省略,在使用class关键字的时候,默认继承方式是private,使用struct关键字的时候,默认继承方式public
⑤在实际使用当中,一般只会使用public继承,几乎很少使用protected和private继承
三、基类和派生类对象的赋值转换(赋值兼容转换,仅限public继承)
3.1区分C语言中截断与提升(类型转换)
3.1.1截断与提升使用举例
int i=1234;
printf("%x\n",i);
char ch=i;//发生截断
printf("%x\n",ch);
i=ch;//发生提升
printf("%x\n",i);
//打印结果:
//4d2
//ffffffd2
//ffffffd2
int类型的i有四个字节的大小,将其赋值给一个字节大小的char会发生截断,只把i中一个字节存储的内容赋值给ch,以16进制整形打印时候会在高位补f(即16进制中的15)
而之后提升的时候,因为ch只有一个字节,所以把这一个字节的内容赋值给i之后也会默认高位补f,因此打印结果相同
3.1.2赋值兼容转换使用举例
//基类
Person p;
//派生类
Student s;
p=s;//赋值兼容转换
Person* ptr=&s;
Person& ref=s;
//这三句语句都可以编译通过,且借由他们都可以直接改变s
//中属于p的那一部分,但是p不会变
s._name += "张三";
ptr->_name += "李四";
ref._name += "王五";
//此时会改变p中的值,s中属于p的那一部分不会变
p._name+="赵六";
//但是此时若是再进行一次赋值兼容转换
p=s;
//运行后s中的p与p中_name就会变得一样
3.1.3对比
截断与提升的本质是发生类型转换,会产生临时变量,所以我们可以:
//在i还是1234的时候
const char& c_ref=i;
这里的const是必须要加上的,但是在赋值兼容转换中,
Person& ref=s;
我们可以很容易地得出一个结论:
赋值兼容转换与类型转换的机制不一样,中间是没有临时变量产生的
3.2兼容赋值转换的图示
3.3总结
兼容赋值转换可以理解为分割/切片
①派生类对象可以赋值给基类的对象/基类的指针/基类的引用,切片,顾名思义,会把派生类中基类的那一部分切下来赋值过去
②基类对象不能赋值给派生类对象(有特殊方法可以达到,暂时不探究)
四、继承中的作用域
4.1域的影响
①影响查找规则
②影响生命周期
但并非全部的域都会影响这两个,如图:
命名空间域和类域对于生命周期是没有影响的
4.2隐藏/重定义(针对父类子类有同名变量或函数)
4.2.1访问的优先级
像在局部域中访问变量,优先级是先局部再全局,子类与父类的访问优先级也是先子类后父类,
若要执意访问父类,可以选择指定类域的方式,如
Person::_name
4.2.2区分重载
只有在一个作用域中,同名函数才可以称之为重载
而父类与子类中出现同名时,构成的是隐藏(又名重定义),如:
class A
{
public:
void func()
{}
};
class B :public A
{
public:
void func(int i)
{}
};
此时B中的void func(int i)与A中的void func()构成隐藏
注:
构成隐藏的时候,如果main函数中写了
B b;
b::func();
编译时便会报错,除非
B b;
b.A::func();
否则会找不到函数
4.3总结
①基类和派生类都有自己的独立作用域
②只要子类成员变量或函数与父类中的名字相同就会构成隐藏,此时子类成员将屏蔽对父类成员的直接访问
③在实际应用中,最好不去定义同名成员。
五、子类的六个默认成员函数
关于子类的六个默认成员函数,我们只需要关注构造,析构,拷贝构造,赋值重载四个即可,剩下的两个取地址运算符重载和const取地址运算符重载一般不会取显式实现
5.1子类的构造函数
5.1.1不显示实现会发生什么?
我们将子类中的成员分为三类:
①父类成员(视作一个整体)
②子类自己的内置成员
③子类自己的自定义成员
情况如图:
5.1.2显示实现举例
需要注意:无论是否显式写,都必须调用父类的构造
当父类没有默认构造或者有特殊的构造需求时,我们需要进行子类构造函数的显示实现:
假如父类Person中有保护成员变量_name
子类Student中有保护成员变量_age,_address
构造:
Student(const char* name,int* age,const char* address)
:Person(name)//注意此处我们习惯性考虑_name(name),但这是不可以的
//规定不允许子类直接操作父类成员,需要复用父类构造
,_age(age)
,_address
{}
5.1.2补:如果父类没有默认构造函数怎么处理
若是父类没有默认构造,我们需要给出全缺省的形式,避免调用“Student s1;”的时候出现无默认构造的问题
Student(const char* name = "zhangsan",int* age = 18,const char* address = "shandong")
5.2子类的拷贝构造函数
5.2.1不显示实现会发生什么?
5.2.2何时需要显式实现以及显式实现举例
只有当成员变量涉及到深浅拷贝问题的时候,需要自己实现,实现规则与构造函数类似:
Student(const Student& st)
:Person(st)//赋值兼容转换
,_age(st._age)
,_address(st._address)
{}
总的来说,子类中父类那一部分视为一个整体,只能复用父类的函数
5.3子类的赋值重载函数
依旧是涉及到深浅拷贝的时候才需要显示实现,如
Student& operator=(const Student& st)
{
if(this!=&st)//避免自己给自己赋值的情况
{
Person::operator=(st);//千万注意父类中即便不显式写赋值重载,也会默认生成operator=
//因此在这里,该函数构成了隐藏,我们必须指定类域
_age=st._age;
_address=st._address;
}
return *this;
}
这里隐藏的出现是需要注意的点
5.4子类的析构函数
5.4.1不显示实现会发生什么?
5.4.2显示实现相关问题
按照常理,我们会这样写
~Student()
{
~Person();
delete[] _ptr;//假设在子类成员变量中属于子类特有的部分我们进行了资源配置
_ptr=nullptr;
}
运行会发现编不过,为什么呢?
实际上,由于多态的原因,析构函数的名字会被统一处理成destructor(),因此父类的析构与子类的析构构成隐藏,修改后应该是这样的:
~Student()
{
Person::~Person();
delete[] _ptr;//假设在子类成员变量中属于子类特有的部分我们进行了资源配置
_ptr=nullptr;
}
运行后,结果还是没编过,这啥情况?
原来,在子类析构函数显示实现的规则中规定:我们必须先析构子类对象,再析构父类对象,编译器为了避免我能显示实现的时候出现失误,已经为显示实现的析构添加了一条“结束后自动调用父类析构”的规则,此处我们已无需显式写出析构函数了
最后正确代码如下:
~Student()
{
delete[] _ptr;//假设在子类成员变量中属于子类特有的部分我们进行了资源配置
_ptr=nullptr;
}
5.4.3在子类构造与析构时,其中父类部分与子类部分构造/析构顺序的问题
在子类对象中,有部分属于父类,有部分为子类特有,他们的构造/析构顺序有一些规定
①构造顺序:先父后子
子类特有的成员在初始化的时候,有时会用上父类中的成员,如果此时父类没有初始化,那么对应成员为随机值,导致无法正常初始化子类
②析构顺序:先子后父
子类特有的成员在析构时也有可能会用到父类成员,先父后子会出现问题,所以为保证顺序,弗雷德析构会在子类析构结束后自动调用
5.5特殊问题说明
①final关键字修饰的类不可以被继承
②若是父类有默认构造函数,那么在显示实现子类构造的时候无需在初始化列表显式调用父类构造函数,当然,父类的构造一定会被调用的
③静态成员属于整个类,不是与任何对象,静态成员函数也可以被继承