继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生的新的类,称为派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,而继承便是类设计层次的复用。
例子:
以下代码Student类和Teacher类继承了Person类,这样设置的原因是这两个类存在共同属性,也存在着不同的属性,例如这两个类都有姓名和年龄,但是Student类有特有属性学号,Teacher类有特有属性工号。
//父类
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "张三"; //姓名
int _age = 18; //年龄
};
//子类 对Person类进行继承
class Student : public Person
{
protected:
int _stuid; //学号
};
//子类 对Person类进行继承
class Teacher : public Person
{
protected:
int _jobid; //工号
};
继承的定义
上面的例子中,Person类称为父类,也称为基类,而Student类和Teacher类称为子类,也称为派生类。
继承受继承方式和访问限定符共同影响
继承基类成员后访问方式的变化
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
表中的读取方式为:结合基类的成员中的访问限定符与继承方式决定派生类的成员的访问限定符
例如: 基类的public成员,通过public继承,则在派生类中仍为public成员
1、基类private成员在派生类中无论以什么⽅式继承都是不可见的。这⾥的不可见是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类里面还是类外面都不能去访问它。
2、基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3、通过上方表格可以总结发现。基类的成员在派生类中的访问方式是 Min(成员在基类的访问限定符,继承方式),根据public>protected>private。即成员在基类的访问限定符和继承方式哪个比较小,那么成员在派生类中的访问方式就是哪个。
4、使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
5、在实际运用中⼀般使用的都是public继承,几乎很少使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
基类和派生类对象赋值转换
public继承的派生类对象可以赋值给基类的对象、基类的指针以及基类的引用。有个形象的说法叫切片或者切割。意味着把派生类中属于基类的那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。
例子:
//基类
class Person
{
protected:
string _name; //姓名
string _sex; //性别
int _age; //年龄
};
//派生类
class Student : public Person
{
protected:
int _stuid; //学号
};
int main()
{
Student s;
Person p = s; //派生类对象赋值给基类对象
Person* ptr = &s; //派生类对象赋值给基类指针
Person& ref = s; //派生类对象赋值给基类引用
// s = p; 基类对象不能赋值给派生类对象 这里会报错
return 0;
}
派生类对象赋值给基类对象:
派生类对象赋值给基类指针:
派生类对象赋值给基类引用:
注意:
1、基类对象不能赋值给派生类对象。
2、基类的指针可以通过强制类型转换赋值给派生类的指针,但是此时基类的指针必须是指向派生类的对象才是安全的。即将一个指向派生类的基类指针强制类型转换为派生类类型再赋值给派生类对象。
继承中的作用域
规则:
1、在继承体系中的基类和派生类都有独立的作用域。
2、派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
例子:
#include <iostream>
#include <string>
using namespace std;
//父类
class Person
{
protected:
int _num = 111;
};
//子类
class Student : public Person
{
public:
void Print()
{
cout << _num << endl;
}
protected:
int _num = 999;
};
int main()
{
Student s;
s.Print(); //999
return 0;
}
我们可以发现,上面的代码中,父类和子类都有成员_num。子类对象中访问成员_num时访问到的是子类当中的_num,即999
如果此时我们偏要访问父类当中的_num,可以使用作用域限定符进行指定访问。
void Print()
{
cout << Person::_num << endl; //指定访问父类当中的_num成员
}
需要注意的是,如果是成员函数的隐藏,只需要函数名相同就构成隐藏,与参数等无关。
例如:
#include <iostream>
#include <string>
using namespace std;
//父类
class Person
{
public:
void Print(int x)
{
cout << x << endl;
}
};
//子类
class Student : public Person
{
public:
void Print(double x)
{
cout << x << endl;
}
};
int main()
{
Student s;
s.Print(5.24); // 调用子类当中的成员函数Print
s.Person::Print(10); // 指定调用父类当中的成员函数Print
return 0;
}
注意: 虽然同名构成成员函数的隐藏,但不是构成函数重载。因为函数重载要求两个函数在同一作用域,而此时这两个fun函数并不在同一作用域。所以需要注意实际在继承体系当中最好不要定义同名的成员。
派生类中的默认成员函数
类当中有6个默认成员函数。
默认成员函数,即我们不写时,编译器会自动生成的函数。
示例:
Person类为基类
//基类
class Person
{
public:
//构造函数
Person(const string& 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;
}
private:
string _name; //姓名
};
用该基类派生出派生类Student类
//派生类
class Student : public Person
{
public:
//构造函数
Student(const string& name, int id)
:Person(name) //调用基类的构造函数初始化基类的那一部分成员
, _id(id) //初始化派生类的成员
{
cout << "Student()" << endl;
}
//拷贝构造函数
Student(const Student& s)
:Person(s) //调用基类的拷贝构造函数完成基类成员的拷贝构造
, _id(s._id) //拷贝构造派生类的成员
{
cout << "Student(const Student& s)" << endl;
}
//赋值运算符重载函数
Student& operator=(const Student& s)
{
cout << "Student& operator=(const Student& s)" << endl;
if (this != &s)
{
Person::operator=(s); //调用基类的operator=完成基类成员的赋值
_id = s._id; //完成派生类成员的赋值
}
return *this;
}
//析构函数
~Student()
{
cout << "~Student()" << endl;
//派生类的析构函数会在被调用完成后自动调用基类的析构函数
}
private:
int _id; //学号
};
根据以上示例可得出,派生类与普通类的默认成员函数的区别如下:
1、派生类的构造函数必须调用基类的构造函数对基类的成员进行初始化。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2、派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类成员的拷贝构造。
3、派生类的赋值运算符重载函数必须调用基类的赋值运算符重载函数完成基类成员的赋值。
4、派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。
5、派生类对象初始化时,会先调用基类的构造函数再调用派生类的构造函数。
6、派生类对象在析构时,会先调用派生类的析构函数再调用基类的析构函数。
编写派生类的默认成员函数时的注意点:
1、派生类和基类的赋值运算符重载函数因为函数名相同构成隐藏,因此在派生类当中调用基类的赋值运算符重载函数时,需要使用作用域限定符进行指定调用。
2、因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同。所以需要对析构函数特殊处理。任何类的析构函数名都会被统一处理为 destructor(); 所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。若是我们需要在某处调用基类的析构函数,那么就要使用作用域限定符进行指定调用。
3、在派生类的拷贝构造函数和operator=当中调用基类的拷贝构造函数和operator=的传参方式是一个切片行为,都是将派生类对象直接赋值给基类的引用。
说明:
1、基类的构造函数、拷贝构造函数、赋值运算符重载函数我们都可以在派生类当中自行进行调用,而基类的析构函数是当派生类的析构函数被调用后由编译器自动调用的,我们若是自行调用基类的构造函数就会导致基类被析构多次的问题。
2、创建派生类对象时是先创建的基类成员再创建的派生类成员,编译器为了保证析构时先析构派生类成员再析构基类成员的顺序析构,所以编译器会在派生类的析构函数被调用后自动调用基类的析构函数。
不能被继承的类
实现一个不能被继承的类有两种方法
方法一: 将基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。
⽅法2: C++11新增了⼀个final关键字,final修改基类,派生类就不能继承了。
例子:
// C++11的⽅法 final关键字
class Base final
{
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
// C++98的⽅法 将构造函数私有化
/*Base()
{}*/
};
class Derive :public Base
{
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
继承与友元
友元关系不能继承,也就是说基类的友元可以访问基类的私有和保护成员,但是不能访问派生类的私有和保护成员。
例子:
#include <iostream>
#include <string>
using namespace std;
class Student; // 先声明Student类
class Person
{
public:
//声明Display是Person的友元
friend void Display(const Person& p, const Student& s);
protected:
string _name; //姓名
};
class Student : public Person
{
protected:
int _id; //学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl; //可以访问
cout << s._id << endl; //无法访问
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
继承与静态成员
若基类当中定义了一个static静态成员变量,则在整个继承体系里面只有一个该静态成员。无论派生出多少个子类,都只有一个static成员实例。
class Person
{
public:
string _name;
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum;
};
int main()
{
Person p;
Student s;
// 这⾥的运⾏结果可以看到⾮静态成员 _name的地址是不⼀样的
// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份
cout << &p._name << endl;
cout << &s._name << endl;
// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的
// 说明派⽣类和基类共⽤同⼀份静态成员
cout << &p._count << endl;
cout << &s._count << endl;
// 公有的情况下,指定类域都可以访问静态成员
cout << Person::_count << endl;
cout << Student::_count << endl;
return 0;
}
继承的方式
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
多继承:一个子类有两个或两个以上直接父类时称这个继承关系为多继承。
菱形继承:菱形继承是多继承的一种特殊情况。包含了单继承和多继承
从菱形继承的模型构造就可以看出,菱形继承的继承方式存在数据冗余和二义性的问题。
例如:
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
string _name; //姓名
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; //职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; //主修课程
};
int main()
{
Assistant a;
a._name = "peter"; //二义性:无法明确知道要访问哪一个_name
return 0;
}
由于Student和Teacher都有一个成员_name。因此Assistant无法明确需要访问哪个类。需要显示指定。
//显示指定访问哪个父类的成员
a.Student::_name = "同学";
a.Teacher::_name = "老师";
// 还有一点点内容没写完 明天补