目录
1、继承的概念与定义
1.1 继承的概念
1.2 继承的定义
1.2.1 定义格式
1.2.2 继承基类成员访问方式的变化
2、基类和派生类对象赋值转换
3、继承中的作用域
4、派生类的默认成员函数
4.1 构造函数
4.2 拷贝构造
4.3 operator=
4.4 析构函数
面向对象的三大特性是封装、继承和多态,前面我们已经学习了封装,回顾一下
第一层封装是将变量与方法封装到一起,成了类
第二层封装是迭代器和适配器
面向对象就是关注类和类之间的关系
1、继承的概念与定义
1.1 继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
就比方说要实现一个外卖系统,那么这个外面系统需要有三个类,骑手、商家、顾客,而这三个类中会有一些共同的成员变量或成员函数,如姓名、地址、电话、上线下线函数等等。所以,可以将这些公共的封装成一个类,让骑手、商家、用户的类去继承。这个公共的类称为父类或基类,骑手、商家、顾客这三个类称为子类或派生类。
1.2 继承的定义
1.2.1 定义格式
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
像上面这个例子中,学生和老师都是人,都会有姓名和年龄,所以可以用一个Person类来存放共有的成员(成员变量+成员函数),然后再让Student类和Teacher类去继承Person类
继承的本质就是复用
注意:使用上面3个类来创建对象时,当创建出来的对象调用基类中的函数Print时,调用的是同一个函数,因为函数不存在于对象中,而是存在公共代码段。但是创建出来的对象里面的成员变量是单独的
1.2.2 继承基类成员访问方式的变化
基类中的成员在派生类中的访问形式是什么样的呢?
1. 看最下面一行,即基类中的private的成员,在派生类中都是不可见的。不可见是指基类的私有成员虽然被继承到了派生类中,但是无论是在派生类内还是在派生类外,都是无法被访问的。虽然不可见,但是派生类中的的确确是有这些私有成员的。若要访问,可通过调用基类中的成员函数来访问。实际上,基类中的private成员就是不想被继承
2. 基类的private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的
3. 看前面两行,会发现在派生类中的访问方式 = Min(成员在基类的访问限定符,继承方式),其中public > protecte > private
4. 写派生类时,继承方式是可以不写的,class默认继承方式是private,struct默认继承方式是public。但是最好还是协商
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强
2、基类和派生类对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里称为基类与派生类的赋值兼容转换,简称为切片或者切割。寓意把派生类中父类那部分切来赋值过去。
基类对象不能赋值给派生类对象
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面再讲解,这里先了解一下)。其实也就是说,只能将基类对象 = 派生类对象,不能 派生类对象 = 基类对象
void test_incise()
{
Person p;
Student s;
p = s; // 将派生类对象赋值给基类对象
Person* ptr = &s; // 将派生类对象的地址传给基类的指针
Person& ref = s; // 让基类引用派生类对象
}
注意,在将派生类对象赋值给基类对象中,不是用派生类的对象s去产生一个基类临时对象,再将这个临时对象赋值给p,而是直接将s中成员变量的值赋值给p中的成员变量。否则也无法解释为什么引用不需要加const
指针只是单纯指向派生类对象中基类的那一部分
引用也是引用的是派生类对象中基类的那一部分,单纯只是这一部分的别名
3、继承中的作用域
目前我们已经学习过四种域,局部域、全局域、命名空间域、类域,域会影响两个东西,语言编译查找规则和生命周期
其中命名空间与和类域不影响生命周期
1. 在继承体系中基类和派生类都有独立的作用域
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)。子类和父类之所以可以有同名成员,是因为不同域是可以有同名成员的
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 s;
s.Print();
}
结果是
所以,若是派生类中有和基类同名的成员变量,在派生类中直接访问,访问的是派生类中的成员变量,若在派生类中需要访问基类中同名的那个成员变量,则需要指定类域
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
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);
b.A::fun();
}
在上面这两个类当中,A中的成员函数fun和B中的成员函数fun就改成隐藏。注意,不构成函数重载,因为函数重载的前提是同一个作用域
与成员变量类似。若是派生类中有和基类同名的成员函数,派生类对象直接访问,访问的是派生类中的成员函数,若在派生类中需要访问基类中同名的那个成员变量,则需要指定类域
当派生类对象调用函数时,首先是去派生类的类域中找,没找到再到基类中找,还没找到就到全局域中找,还找不到就报错(一般查找都是最后到全局域中找)
上面是派生类的默认查找规则,无论是调用函数还是访问成员变量都是如此
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();
}
上面这个程序为什么会编译报错呢?
在编译检查语法时,发现B类中确实有一个fun函数,但是这个函数需要参数,没传参数所以报错,因为在子类已经找到了,所以不会去父类中寻找。注意,这里是根据fun这个函数名来找的,不是根据符号表,符号表是在链接时的查找规则,所以会直接在派生类B中找到
4、派生类的默认成员函数
前面说过,继承的本质就是复用
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num; //学号
string _address; //地址
};
4.1 构造函数
首先我们来看看子类中的默认构造函数是怎么实现的
那要如何自己实现一个子类的构造函数呢?
Student(const char* name = "", int num = 0, const char* address = "")
:Person(name)
,_num(num)
,_address(address)
{
cout << "Student()" << endl;
}
子类的构造不能直接去动父类的成员变量,需要手动调用父类的构造
父类的构造子类仍然可以用,所以需不需要自己写子类的构造,完全看情况。当然,如果父类没有默认构造函数,即父类的构造函数需要传参,那么就必须显示写一个子类的构造函数。因为子类的默认构造函数只能调用父类的默认构造函数
注意:构造函数一定要在初始化列表中调用父类的构造函数,否则子类对象中父类的那部分已经初始化过了,再去函数体中调用父类的构造函数就是错误的,下面的拷贝构造是同理的,也就是下面这样是错误的
Student(const char* Name = "", int num = 0, const char* address = "")
{
Person(Name); // 实际上此时子类对象中父类那一部分已经初始化过了
_address = address;
_num = num;
cout << "Student()" << endl;
}
下面这样子是可以的
Student(const char* Name = "", int num = 0, const char* address = "")
:Person(Name)
{
_address = address;
_num = num;
cout << "Student()" << endl;
}
4.2 拷贝构造
首先,我们来看看子类的默认拷贝构造是怎么实现的
那要如何自己实现一个子类的拷贝构造呢?
Student(const Student& st)
:Person(st)
,_num(st._num)
,_address(st._address)
{
cout << "Student(const Student& st)" << endl;
}
调用父类的拷贝构造需要将子类中父类的那一部分传给父类的拷贝构造函数,是将整个子类对象传过去,因为父类的拷贝构造函数会进行切割
所以在子类当中要调用父类的成员函数,并且这个成员函数还需要传一个父类对象时,直接将子类对象传过去即可,会自动切割,下面operator=中也有应用
拷贝构造只有当子类中设计到深拷贝才一定要自己实现,其他情况随意
4.3 operator=
默认的operator=与默认的拷贝构造函数是类似的,这里就不过多说明了
直接看如何自己实现一个
Student& operator=(const Student& st)
{
if (this != &st)
{
operator=(st);
_num = st._num;
_address = st._address;
}
return *this;
}
按照上面拷贝构造的思路,很容易写成这样子,实际上,这是错的,会造成无穷递归
因为子类的operator=和父类的operator=构成隐藏,而此时在子类中并没有指明类域,所以会一直调用子类中的operator=,导致无穷递归
Student& operator=(const Student& st)
{
if (this != &st)
{
Person::operator=(st);
_num = st._num;
_address = st._address;
}
return *this;
}
4.4 析构函数
首先,子类默认的析构是:
父类成员(整体) :调用父类的析构
子类自己的内置成员 :不处理
子类自己的自定义成员 :调用他的析构
自己实现:
此时发现会有报错,原因是,由于多态,析构函数的名字会被统一处理为destructor(),即~Person()和~Student()都被处理成destructor(),所以~Person()和~Student()构成隐藏,解决方法是指定类域
~Student()
{
Person::~Person();
cout << "~Student<()" << endl;
}
可这样为什么会调用两次父类的析构呢?
实际上,此时仍然是错的,因为父类析构不需要在子类中显示调用,子类的析构完成后,会自动调用父类的析构
那为什么前面3个可以显示调用父类相应的成员函数,析构就不行呢?
在子类的成员变量声明中,虽然看不见父类的部分,但是父类这部分是放在声明的最前面的。析构从后往前析构的,即先析构子类特有的成员变量,如何再调用父类的析构去析构掉子类从父类中继承的成员变量,因为在子类析构时,是有可能会去访问子类对象中从父类继承的那一部分成员变量的。而前面3个函数可以显示调用父类相应的成员函数,以构造函数为例。构造函数是从前往后构造的,因为子类中特有的那部分成员变量在构造时可能会利用子类中从父类继承下来的那部分成员变量来构造,所以需要从前往后构造。
构造函数是从前往后构造的,不过在构造函数的初始化列表中,不把Person(name)放在最前面也可以,因为初始化并不是按初始化列表走的,而是按照声明顺序走的
所以,析构函数可以写为:
~Student()
{
cout << "~Student()" << endl;
}