目录
前言
一、概念与定义
Ⅰ、是什么?
Ⅱ、定义
1.定义格式:
2.继承方式和访问限定符
3.基类(父类)成员访问方式的变化
二、父类与子类的赋值转化
基本认识
原理
三、 继承中的作用域
四、子类(派生类)的默认成员函数
构造函数
析构函数
拷贝构造/赋值重载
五、继承与友元
六、继承与静态成员
前言
前面我们对STL库中的一些简单的容器进行了学习,也算是初级内容,从这里开始,我们将更加深入去感受C++的魅力!相信我,你会更喜欢C++!!!
一、概念与定义
Ⅰ、是什么?
先从字面上看,继承继承,无非就相当于现实生活中继承家业,把父亲的事业传给儿子,同时,儿子也能够在此基础上创造出属于自己的东西,也能算作一种改进或者拓展!
而在C++中继承是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用!
举个例子:
//一个人统一的信息
class Person
{
public:
void Print()
{
cout << "Maybe i am father!" << endl;
cout << "name:" << _name << endl;
}
protected:
string _name="Stephen Curry";// 姓名
};
//子类继承了父类Person
class Student : public Person
{
protected:
int _stuid; // 学号
};
int main()
{
Student s;
s.Print();
return 0;
}
上面就是一种继承关系,Student类继承了父类Person,继承后,子类中都会存在一份父类的数据(成员函数+成员变量),这就是体现Student子类复用父类的成员!
所以对于上面这段代码,当子类去调用print函数并且他自己没有和父类相同名字的成员函数时,他实际上就是在调用父类的print函数(原因在下文的作用域)
Ⅱ、定义
1.定义格式:
2.继承方式和访问限定符
继承方式 | public继承 |
private继承 | |
protected继承 | |
访问限定符 | public |
private | |
protected |
3.基类(父类)成员访问方式的变化
public继承 | protected继承 | private继承 | |
基类中的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类中的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类中的private成员 | 派生类不可见 | 派生类不可见 | 派生类不可见 |
总结:实际上访问限定不能存在权限的放大,只能是缩小,也就是满足一个公式:
基类的其他成员在子类的访问方式 == Min(成员在父类的访问限定符,继承方式),public > protected > private。
正如上表:该成员在父类是public,子类是以private的方式继承父类,那么取最小权限,该父类成员在子类的访问方式就变成了private的!!
注意:
①派生类不可见,不是说子类没有继承,而是子类也会继承父类的私有成员,但是子类不能直接访问父类的私有成员,可以间接使用(调用父类的方法即可),这就好像是你不能直接用你爸爸的私房钱一样。
②如果父类成员不想在类外直接被访问,但是需要给子类直接访问,就定义该成员为protected。。可见protected因继承而生!
③实际使用中,一般采用public继承,几乎很少使用protected/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
二、父类与子类的赋值转化
基本认识
- 子类对象可以赋值给父类对象,父类指针,父类引用。
- 父类对象不能赋值给子类对象
如下:父类和子类代码
class Person
{
protected:
string _name = "Stephen Curry";// 姓名
string _sex="man";
};
//子类继承了父类Person
class Student : public Person
{
private:
int _stuid=30; // 学号
};
上述代码可以存在以下赋值关系:
Student s;
Person p = s;//直接赋值给父类对象
Person& refp = s;//赋值给父类引用
Person* p = &s;//赋值给父类指针
不能存在如下关系:
Student st;
Person pe;
st = pe;//父类不能赋值给子类对象
原理
实际上对于上面的赋值关系,有一个形象的说法叫做切片或者切割,也就是说子类会将继承父类的那一部分切割下来赋值给父类。
子类赋值给父类
子类赋值给父类引用,子类切割出的父类那部分的别名!
子类赋值给父类的指针,指针指向的是子类切割出的父类那一部分的首地址!
不难看出,每个子类对象实际上都是一个特殊的父类对象!!!是一种 is-a的关系!
注意:父类的指针或引用可以通过强制类型转换赋值给子类的指针或引用,但是父类的指针是指向子类对象是才是安全的。(原因后续讲解)
Student st;
Person pobj;
//父类指针指向子类对象再强转赋值是安全
Person* pp = &st;
Student* pst1 = (Student*)pp;//这种情况是可以的,安全
//不安全
Person* pp = &pobj;
Student* pst2 = (Student*)pp;//这种情况虽然可以,但是会存在越界访问的问题,不安全。
三、 继承中的作用域
- 继承体系中,子类和父类都有独立的作用域
- 子类和父类若存在同名成员(成员函数/成员变量),子类成员将屏蔽父类的同名成员,这种情况叫隐藏,也叫重定义。(在子类中若想访问父类的同名成员,可以使用父类::父类成员的方式访问)
同名成员变量隐藏
class Father
{
protected:
int _num = 0;
};
//子类继承了父类Person
class Son : public Father
{
public:
void func()
{
cout << _num << endl;//隐藏父类的同名成员,输出子类的成员,1
cout << Father::_num << endl;//0
}
protected:
int _num = 1;
};
同名成员函数的隐藏
class Father
{
public:
void func()
{
cout << "Father func()" << endl;
}
};
class Son : public Father
{
public:
void func(int i)
{
cout << "Son func(int i)->" << i<< endl;
Father::func();//类内调用
}
};
int main()
{
Son s;
s.func(1);
s.Father::func();//类外调用
return 0;
}
①以上代码的父类和子类并不是重载关系,因为两个同名函数并不在同一个作用域!
②子类的func和父类的func构成隐藏,成员函数只要是同名就能构成隐藏!
因为独立作用域,所以相当于就近原则访问,一个全局一个局部,当然优先局部!
注意: 实际上在继承体系中最好不要有同名成员!!
四、子类(派生类)的默认成员函数
默认成员函数,"默认"意思就是我们不写编译器自动生成一个,那么子类是如何生成的呢?
首先我们得先明确子类对象的模型,子类对象实际上包含了父类的成员变量+自己的成员变量
构造函数
由上述对象模型可以得出:父类在子类中就像是一个自定义类型!父类当成是一个整体!(复用)
- 父类成员变量,会调用父类自己的默认构造函数去初始化(复用)!如果父类没有合适的默认构造函数,则必须在子类构造函数的初始化列表阶段显示调用。
显示调用
注意:若不想显示调用,那父类就必须提供默认构造函数(无参,全缺省,编译器自动生成)!!
如下:
- 子类自己的成员变量,还是一样(类和对象一样),对内置类型不做处理,自定义类型还是会去调用对应的构造!
注意:继承体系中构造的顺序是:先构造父类再构造子类!
析构函数
- 子类的析构函数会在调用完成之后自动调用父类的析构函数去清理父类的成员变量!所以我们一般不显示调用父类的析构函数!为了保证析构顺序!
- 子类对象的析构顺序:先析构子类再析构父类!!
演示:
可以看到,如果显示调用父类的析构,那么在程序结束时,又会自动调用父类的析构,万一父类的析构中存在释放申请的内存空间等问题,那必然会引起同一块空间连续多次释放的问题!同时这也不满足要求的析构顺序:先子后父!
为啥一定要先子后父?
因为如果显示写父类析构,那就会先析构父类,父类成员变量被清理,此时子类就不能访问了,万一子类要访问时,且该成员变量是指针,那必然会造成野指针问题!所以一定要遵循先子后父!不要在子类中显示调用父类的析构!
此外,子类能够自动调用的原因,那就是子类隐藏了父类的析构函数,实际上在类中的析构函数在编译器看来都是一个名字---destrutor(),在继承体系中,只要是同名函数,就能构成隐藏!!
拷贝构造/赋值重载
- 拷贝构造:和构造类似,父类必须调用父类的拷贝构造完成初始化
- 赋值重载:operator=必须要调用基类的operator=完成基类的复制。
演示:
拷贝构造:
五、继承与友元
注意:友元关系不能继承,也就是父类友元不能访问子类私有和保护变量 !
理解:就好像你爸爸的朋友不是你的朋友一样!
class Father
{
public:
friend void Show(const Father& p, const Son& s);
private:
string _name;
};
class Son : public Father
{
protected:
int _num;
string _sex;
};
void Show(const Father& p, const Son& s)
{
//父类的友元可以访问父类的私有
cout << p._name << endl;
//不能访问子类的私有/保护
cout << s._num << endl;
}
int main()
{
Father f;
Son s;
Show(f, s);
return 0;
}
若想访问子类的私有变量,可以在子类中声明友元!
六、继承与静态成员
父类定义了static静态成员,则整个继承体系里面只有一个这样的成员。也就是说父类的静态成员属于当前类,也属于当前父类的所有子类,都是共用一个static变量!
它的用处就是可以去统计父亲有多少个儿子!只需在父类的构造中++静态成员即可!
好了,兄弟们,今天的分享就到这里,如果对你有帮助,欢迎三连!!你的支持是我前进的动力!