一、继承的概念及定义
1.1继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
先用一个例子来提高对继承概念的理解
我们在对学校的学生、老师、保安等人员进行抽象成类的时候,他们都有相同的属性成员。如电话、姓名、性别等。同时他们都有自己独有属性特征,学生如成绩、学院、等老师有职称、学院等。保安有工龄、职位等。
如上图所示:他们都有一部分相同的成员。那么他们可以继承自一个基类。person类。
1.2继承的定义
1.2.1定义的格式
如下是我们设计的Person为父类,也可以称为基类。Student是子类,也可以称为派生类。
class Student :public Person
{
public:
int _stuId;
int _stuName;
....
};
1.2.2继承关系的访问限定符
继承方式分为:公有继承,保护继承,私有继承。对应的访问限定符不同。
1.2.3继承基类成员访问方式的变化
对基类成员访问方式变化的总结:
1.基类中的私有成员,在派生类中无论是公有继承还是保护继承、私有继承,都是不可访问的。
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3.除了基类的私有成员不能访问之外,基类的其他成员在子类中访问的方式是Min(成员在基类的访问限定符,继承方式),一般的public>protected>private。
4.关键词class默认的继承方式是private,而struct的默认的继承方式是pubilc。一般得我们最好写明继承的方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
#pragma once
#include<string>
#include<iostream>
using namespace std;
class person
{
public:
person(string name,int age,string sex)
:p_name(name)
,p_age(age)
,p_sex(sex)
{
}
void work()
{
cout << p_name << endl;
}
protected:
string p_name;
private:
int p_age;
string p_sex;
};
//公有继承
//class student :private person
//class student :protected person
class student :public person
{
public:
int _sid;
};
二、基类和派生类对象赋值转换
- 派生类对象 可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
用代码实现对上面基类和派生类之间对象赋值转换结论的验证
#pragma once
#include<string>
#include<iostream>
using namespace std;
class person
{
public:
person(string name,int age,string sex)
:p_name(name)
,p_age(age)
,p_sex(sex)
{
}
void work()
{
cout << p_name << endl;
}
protected:
string p_name;
private:
int p_age;
string p_sex;
};
//公有继承
//class student :private person
//class student :protected person
class student :public person
{
public:
student(string name, int age, string sex);
int _sid;
};
//派生类对象赋值给基类的对象
student s("小明",18,"男");
person p = s;
//派生类对象赋值给基类的指针
student s("小明",18,"男");
person* p = &s;
//派生类对象赋值给基类的引用
student s("小明",18,"男");
person& p = s;
//基类对象赋值给派生类对象
person p("小明", 18, "男");
student s=p;
提示不能从person类转换到student类。
三、继承中的作用域
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
3.1函数的重定义和重载的区别
函数的重定义是子类继承自父类后,子类定义了和父类同名的函数,只要是函数名相同就构成重定义。
函数的重载是两个函数名相同的函数,参数个数不同,参数类型不同即构成函数重载。
区别在于是不是在同一个定义域。
class Person
{
protected :
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout<<" 姓名:"<<_name<< endl;
cout<<" 身份证号:"<<Person::_num<< endl;
cout<<" 学号:"<<_num<<endl;
}
protected:
int _num = 999; // 学号
};
void Test()
{
Student s1;
s1.Print();
};
Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆。
#include"person.h"
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun(10);
};
int main()
{
Test();
return 0;
}
先调用的子类B的fun函数,然后在fun函数内,通过A::fun()直接显示的调用父类的fun函数。这里是直接屏蔽了A类的fun函数。构成了隐藏。
四、了解派生类中的默认成员函数
可以提前看一下我原来的文章:10分钟搞定!C++类中构造函数和析构函数的完全指南
默认的成员函数的意思就是我们不写,让编译器负重前行,它帮我们自动进行生成。探究派生类的几个成员函数是如何生成的?
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
对上面的结论的总结:
构造函数: 基类构造函数 ->派生类构造函数
拷贝构造函数:基类拷贝构造函数 -> 派生类拷贝构造函数
函数重载: (=) 调用基类的运算符重载函数
析构函数: 派生类析构函数 ->基类构造函数
#include"person.h"
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
};
int main()
{
B b;
return 0;
}
如上测试代码就验证了派生类默认成员函数的构造、析构等过程。