一、继承的概念及定义
1、继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类(基类或者又叫父类)特性的基础上进行扩展,增加功能,这样产生新的类,称派生类(子类)。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
看下面一段代码来演示继承的表现,我们只需要注重结果就行,关于代码的具体语法我们后面讲解。
//父类(基类) 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; // 学号 }; int main() { Student s; s.Print(); return 0; }
继承后父类的Person
的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student
和复用了Person
的成员。我们使用监视窗口查看Student
对象,可以看到变量的复用,调用Print可以看到成员函数的复用。
2、继承的定义格式
继承的语法很简单通常就是:
class
派生类 :
继承方式 基类
struct
派生类 :
继承方式 基类
注意:继承方式是可以省略的,使用关键字class
时默认的继承方式是private
,使用struct
时默认的继承方式是public
,不过最好显示的写出继承方式。
关于继承方式与访问限定符
3、继承基类成员访问方式的变化
总结:
- 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == 最小值(成员在基类的访问限定符,继承方式),这里的关系等级划分
public
>protected
>private
。 - 基类
private
成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
例如下面的private
成员
//父类(基类) class Person { public: void Print() { cout << "name:" << _name << endl; cout << "age:" << _age << endl; } private: string _name = "peter"; // 姓名 int _age = 18; // 年龄 }; //子类(派生类) class Student : public Person { void SPrint() { cout << "_stuid " << _stuid << endl; //报错无法访问 子类无法访问父类的private成员 cout << "_name " << _name << endl; cout << "_age " << _age << endl; } protected: int _stuid; // 学号 }; int main() { Student s; //s.SPrint(); //无法使用 s.Print(); //依然可以打印父类的private成员,因为我们使用的是继承下来的public的函数,使用的是间接访问的方式 return 0; }
运行这段代码我们就知道,在子类中确实继承了父类的private
成员,但是子类无法显示使用,想要访问的话我们也只能通过父类提供的一些public
或protected
函数来进行间接访问
- 基类
private
成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected
,可以看出保护成员限定符是因继承才出现的。 - 在实际运用中一般使用都是
public
继承,几乎很少使用protetced
/private
继承,也不提倡使用protetced
/private
继承,因为protetced
/private
继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
二、基类和派生类对象赋值转换
- 派生类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
利用此条性质,就可以实现这样的操作。
//父类(基类) 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; // 学号 }; int main() { Student s1; //赋值 Person p1 = s1; //指针 Person* pptr = &s1; //引用 Person& rp = s1; return 0; }
打开监视窗口我们可以看到:
- 基类对象不能赋值给派生类对象。
三、继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。在实际中在继承体系里面最好不要定义同名的成员。
看下面的代码,成员变量_num
与成员函数func
构成了隐藏。
//父类(基类) class Person { public: void func() { cout << "class Person func" << endl; } protected: string _name = "peter"; // 姓名 int _num = 123;//身份证号 }; //子类(派生类) class Student : public Person { public: void Print() { //父类的_name cout << "_name " << _name << endl; //两者构成隐藏,不加访问限定符,默认访问的是本类的成员 cout << "_num " << _num << endl; //加访问限定符,明确要访问的成员变量 cout << "_num " << Person::_num << endl; } //两个函数func构成隐藏,不是函数重载,函数重载的两个函数要求在同一作用域!!! void func(int i = 0) { cout << "class Student func" << endl; } protected: int _num = 111; //学号 }; int main() { Student s1; s1.Print(); //不加访问限定符,默认访问的是本类的成员 s1.func(); //调用父类的func函数 s1.Person::func(); return 0; }
运行结果:
四、派生类的默认成员函数
我们知道C++对于一个类,有6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?下面我们来一起探讨一下。
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
//基类 (父类) class Person { public: //构造函数,全缺省的默认构造 Person(const char* name = "peter", int age = 12) :_name(name) ,_age(age) { cout << "Person()" << endl; } protected: string _name; int _age; }; //派生类 (子类) class Student : public Person { public: Student(const char* name, int age, int stuid = 2000) :Person(name, age) //对于父类的成员,要用父类的构造函数进行初始化 ,_stuid(stuid) { cout << "Student()" << endl; } Student(int stuid = 2000) :_stuid(stuid) //对于父类的成员如果有默认的构造函数,此处不显示初始化,也会调用父类的默认构造 { cout << "Student()" << endl; } protected: int _stuid; //学号 }; int main() { Student s1(110); Student s2("xia", 15, 123); return 0; }
从运行结果中我们也可以知道,子类对象想要创建需要先创建其父类对象
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
//基类 (父类) class Person { public: //构造函数,全缺省的默认构造 Person(const char* name = "peter", int age = 12) :_name(name) ,_age(age) { cout << "Person()" << endl; } //拷贝构造 Person(const Person& p) :_name(p._name) ,_age(p._age) { cout << "Person(const Person& p)" << endl; } protected: string _name; int _age; }; //派生类 (子类) class Student : public Person { public: Student(const char* name, int age, int stuid = 2000) :Person(name, age) //对于父类的成员,要用父类的构造函数进行初始化 ,_stuid(stuid) { cout << "Student()" << endl; } Student(int stuid = 2000) :_stuid(stuid) //对于父类的成员如果有默认的构造函数,此处不显示初始化,也会调用父类的默认构造 { cout << "Student()" << endl; } //拷贝构造 Student(const Student& s) :Person(s) //这里使用了基类与派生类的赋值转化,将子类赋值给父类。 ,_stuid(s._stuid) { cout << "Student(const Student& s)" << endl; } protected: int _stuid; //学号 }; int main() { Student s1(110); Student s2(s1); return 0; }
一些特殊情况:
- 子类中的拷贝构造如果不写,默认生成的拷贝构造会对父类对象调用拷贝构造,对自己的的自定义类型调用它的默认构造,对内置类型完成值拷贝。
- 如果子类中写了拷贝构造,但是没有显示调用父类的拷贝构造,则会在初始化列表中调用父类的构造函数去初始化父类对象。
//拷贝构造 Student(const Student& s) :_stuid(s._stuid) //,Person(s) { cout << "Student(const Student& s)" << endl; }
- 派生类的
operator=
必须要调用基类的operator=
完成基类的复制。
//基类 (父类) class Person { public: //构造函数,全缺省的默认构造 Person(const char* name = "peter", int age = 12) :_name(name) ,_age(age) { cout << "Person()" << endl; } //拷贝构造 ...... //赋值重载 Person& operator=(const Person& p) { if (this != &p) { _name = p._name; _age = p._age; } cout << "Person& operator=(const Person& p)" << endl; return *this; } protected: string _name; int _age; }; //派生类 (子类) class Student : public Person { public: Student(const char* name, int age, int stuid = 2000) :Person(name, age) //对于父类的成员,要用父类的构造函数进行初始化 ,_stuid(stuid) { cout << "Student()" << endl; } Student(int stuid = 2000) :_stuid(stuid) //对于父类的成员如果有默认的构造函数,此处不显示初始化,也会调用父类的默认构造 { cout << "Student()" << endl; } //拷贝构造 ...... //赋值重载 Student& operator=(const Student& s) { if (this != &s) { //这里必须显示调用operator=(),父类与子类的operator=()构成隐藏。 Person::operator=(s);//这里使用了基类与派生类的赋值转化,将子类赋值给父类。 _stuid = s._stuid; } cout << "Student& operator=(const Student& s)" << endl; return *this; } protected: int _stuid; //学号 }; int main() { Student s1(110); Student s2(s1); return 0; }
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
- 派生类对象初始化先调用基类构造再调派生类构造。派生类对象析构清理先调用派生类析构再调基类的析构。
//基类 (父类) class Person { public: //构造函数,全缺省的默认构造 Person(const char* name = "peter", int age = 12) :_name(name) ,_age(age) { cout << "Person()" << endl; } //拷贝构造 ...... //赋值重载 ...... //析构函数 ~Person() { cout << "~Person()" << endl; } protected: string _name; int _age; }; //派生类 (子类) class Student : public Person { public: Student(const char* name, int age, int stuid = 2000) :Person(name, age) //对于父类的成员,要用父类的构造函数进行初始化 ,_stuid(stuid) { cout << "Student()" << endl; } Student(int stuid = 2000) :_stuid(stuid) //对于父类的成员如果有默认的构造函数,此处不显示初始化,也会调用父类的默认构造 { cout << "Student()" << endl; } //拷贝构造 ...... //赋值重载 ...... //析构函数 ~Student() { cout << "~Student()" << endl; } protected: int _stuid; //学号 }; int main() { Student s1(110); return 0; }