一.继承概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
继承呈现了面向对象程序设计的层次结构,相比以前我们接触的复用都是函数复用,继承是类设计层次的复用。
#define _CRT_SECURE_NO_WARNINGS 1
#include<string>
#include<iostream>
using namespace std;
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。调用Print可以看到成员函数的复用。
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
system("pause");
return 0;
}
1.1怎么定义
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。注意一般在写类成员时在前面加上 _
1.2继承关系和访问限定符
注意 访问限定符:
- protected: 类外不可见,但是在子类中可见。
- private: 类外不可见,在子类中不可见,父类的私有成员也会被子类继承,属于子类的一部分,但是不可见。
1.3继承基类访问方式变化
小结 :
基类的成员在子类中新的访问权限:
- 1.基类的私有成员在子类中不可见。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 2.访问权限由继承方式和原有的访问权限的最小权限决定。(public > protected > private)
- 3.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
- 4.继承代码常识: 【1】父类的成员变量的权限一般定义为protected:类外不可见,子类可见,既体现封装,也体现复用。【2】继承一般指公有继承(99%),私有保护继承不可用。 【3】公有继承体现的关系是 “是” is-a,即子类是父类,比如学生是人,老师是人。
二.基类和派生类对象赋值转换(切片)
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。
- 基类对象不能赋值给派生类对象,也不支持父类对象到子类对象的强制转换。
- 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。
三.继承中的作用域
局部域,子类作用域,基类作用域都存在同名变量,匹配原则:就近原则
注意:
- 成员函数的参数不要和成员变量同名,如果同名,只会匹配成员函数的参数。
- 如果基类和子类用相同名称的成员,在子类中父类的的成员就会被隐藏,简称同名隐藏,如果需要访问父类成员,需要加父类作用域: 父类::成员 ( 类内部访问方法) 这里的成员指的是所有的成员,包括成员变量/成员函数,如果是成员函数,只需要函数名相同,就会构成同名隐藏, 类外访问同名成员。
- 区分静态成员的访问方式: 类名::静态成员 (类外访问方法)
对比函数隐藏与函数重载
- 函数隐藏:函数名相同,参数无所谓 继承场景 --》不同作用域
- 函数重载:函数名相同,参数不同 ---》相同的作用域
四.派生类的默认成员函数
1.子类的构造函数
- 1.初始化列表,自动调用父类的默认构造函数,初始化从父类继承过来的成员,并不是创建父类对象。
- 2.父类的默认构造函数调用完之后,再去初始化子类新增的成员。
- 3.初始化顺序: 继承的父类成员 ,子类新增的成员。
- 4.如果父类没有默认构造函数,不能直接在初始化列表中初始化父类的成员的,需要显示指定调用父类的哪一个构造函数,完成父类成员的初始化。
2.子类的拷贝构造
- 1.编译器默认生成的子类拷贝构造函数会自动调用父类的构造函数。
- 2.显示定义子类的拷贝构造函数,默认情况下,会自动的调用父类的默认构造,但是可以指定调用父类的拷贝构造。
3.子类的赋值运算符成员函数(operator=)
- 1.编译器默认生成的子类赋值运算符成员函数会自动调用父类的赋值运算符成员函数。
- 2.显示定义了子类的赋值运算符成员函数,就会和父类的赋值运算符成员函数构成同名隐藏,不会调用父类的赋值运算符成员函数但是可以显示调用,通过父类 operator=()。
4.子类的析构函数
- 1.任何情况下都会最后调用父类的析构函数。
- 2.父类析构和子类析构 构成同名隐藏,底层编译器修改了析构函数的名称,导致子类父类析构函数同名。
- 3.一定不要在子类的析构函数中显示调用父类析构函数,否则父类的析构会执行两次,导致程序错误。
#include<string>
#include<iostream>
using namespace std;
class Person
{
public:
Person(int age = 10)//构造函数
:_age(age)
{
cout << "Person()" << endl;
}
Person(const Person& p)//拷贝构造
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)//赋值运算符重载
{
cout << "Person& operator=(const Person& p)" << endl;
return *this;
}
~Person()//底层 : destructor
{
cout << "~Person()" << endl;
}
protected:
int _age;
};
class Student : public Person
{
public:
Student(int age = 18 , int id = 2019)//先将从父类继承的成员 如_age 利用父类的构造函数初始化,再调用自己的构造函数完成其他成员初始化
:_id(id)
//, Person(age)
{
cout << "Student(int age, int id)" << endl;
}
Student(const Student& s)
:Person(s)
, _id(s._id)
{
cout << "Student(const Student& s) " << endl;
}
Student& operator=(const Student& s)
{
Person::operator=(s);
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
~Student()//底层 : destructor 父类析构和子类析构 构成一个同名隐藏
{
//Person::~Person();//子类析构一定不要显示调用父类的析构,编译器会自动调用
cout << "~Student()" << endl;
}
protected:
int _id;
};
void testStudent()
{
Student s;//自动调用子类的构造,子类构造会自动调用父类默认构造
//cout << "输出拷贝构造" << endl;
//Student copy(s);//自动调用父类拷贝构造
//cout << "operator= " << endl;
//s = copy;
}
int main()
{
testStudent();
//system("pause");
return 0;
}
练习 :实现一个不能继承的类---》基类构造函数私有化,或者C++11语法final关键字 即最终类不可扩展。
// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
// C++11给出了新的关键字final禁止继承
class NonInherit final
{};
五.继承和友元,继承与静态成员
5.1友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
5.2继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。静态成员不属于类对象,静态成员变量数据存放在数据段。
六.复杂的菱形继承及菱形虚拟继承
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。
菱形继承举例:
class Person
{
protected:
int _id; //身份证号
};
class Teacher : public Person
{
protected:
int _jobid; //职工编号
};
class Student : public Person
{
protected:
int _stuid; //学号
};
class Assitant :public Student, public Teacher
{
public:
int getId()
{
return _id;
}
protected:
int _aid;
};
void test()
{
Assitant ass;
cout << ass.getId() << endl;
}
int main()
{
test();
system("pause");
return 0;
}
因为ass中有两个_id ,一个是从Teacher继承的,一个是从Student继承的,所以编译器无法确定到底是哪一个。
要解决上面问题除了指定作用域之外还可以使用虚拟继承。
class Person
{
protected:
int _id = 10086; //身份证号
};
class Teacher :virtual public Person
{
protected:
int _jobid; //职工编号
};
class Student :virtual public Person
{
protected:
int _stuid; //学号
};
class Assitant :public Student, public Teacher
{
public:
int getId()
{
return _id;
}
protected:
int _aid;
};
void test()
{
Assitant ass;
cout << ass.getId() << endl;
}
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
这里可以分析出d对象中将A放到的了对象组成的最下面,这个_a同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。【虚基表中存的当前位置相对于公共成员的偏移量】通过偏移量可以找到下面的A。
七.组合与继承
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。 - 继承一定程度破坏了基类的封装,基类的改变,对派生类类有很大的影响。派生类和基类间的依
赖关系很强,耦合度高。而 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。 - 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。
// Car和BMW Car和Benz构成is-a的关系
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
};
class BMW : public Car{
public:
void Drive() { cout << "好开-操控" << endl; }
};
class Benz : public Car{
public:
void Drive() { cout << "好坐-舒适" << endl; }
};
// Tire和Car构成has-a的关系
class Tire{
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
Tire _t; // 轮胎
};
多继承例题:
class Base1{
public:
int _b1;
};
class Base2 {
public:
int _b2;
};
class Derive : public Base1, public Base2 {
public:
int _d;
};
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
// A. p1 == p2 == p3
// B. p1 < p2 < p3
// C. p1 == p3 != p2
// D. p1 != p2 != p3 选择C