类的继承:
我们一直以来接触的函数在一定程度上完成了代码的复用,这里介绍面向对象的重要复用机制—继承
继承机制是面向对象程序设计使代码可以复用的重要手段,他允许我们在保持原有类特性的基础上进行扩展,增加功能。这样产生的类,称为派生类。继承呈现了面向对象程序的层次性,体现了从简单到复杂的认知过程
继承的简单使用:
class Person
{
public:
void Display()
{
cout << _name << "-" << _sex << "-" << _age << endl;
}
public:
const char* _name;
const char* _sex;
int _age;
};
class Student : public Person
{
public:
void BuyTicket()
{
cout << "买票半价" << endl;
}
};
void test()
{
Person p;
p._name = "鸟哥";
p._sex = "男";
p._age = 20;
p.Display();
Student s;
s._name = "鸟";
s._sex = "男";
s._age = 15;
s.Display();
s.BuyTicket();
}
继承后的效果:
继承的定义的格式:
继承的方式有三种:公有继承,私有继承,保护继承。三种继承关系下基类三种访问权限的成员在派生类中的访问权限如下:
基类成员/继承方式 | public继承 | protected继承 | privated继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的privated成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的privated成员 |
基类的private成员 | 不可见只能通过基类接口访问 | 不可见只能通过基类接口访问 | 不可见只能通过基类接口访问 |
总结:
is-a原则:继承关系,例如正方形和长方形在性质层面是一种继承关系
has-a原则:组合关系,例如轮胎和车是一种组合关系
至于使用哪个原则,应该是具体场景具体对待,看两种事物之间是继承关系还是组合关系,是继承关系就用is-a,是组合关系就用has-a。
可能会有这样的场景,两种事物之间既可以是继承关系,又可以是组合关系,这时该用哪个我们初学者的确很难回答出来,多年的C++开发者建议用has-a,他们的实践经验是这种情况下is-a很难做下去。
切片 / 切割:
通过强转子类的指针/引用可以指向父亲对象,但是这种情况下父亲对象指针其实是一种越界访问行为,所以等到作用域结束后程序会崩溃。因为此时父亲对象指针指向的范围扩大了,而扩大的那一部分不属于它的合法指向范围(合法域),虽然那块区域的大小刚好是_num的大小。
class Person
{
public:
void Display()
{
cout<<_name<
继承中的作用域:
class Person
{
public:
Person(const char* name,int id)
:_name(name)
,_num(id)
{}
void Display()
{
cout << _num << endl;
}
protected:
string _name; //姓名
int _num; //id
};
class Student : public Person
{
public:
Student(const char* name,int id,int num)
:Person(name,id)
,_num(num)
{}
void Display()
{
cout << "id:" << Person::_num << endl;
cout << "学号" << _num << endl;
}
protected:
int _num; //学号
};
void test()
{
Person p("鸟哥",1111);
Student s("鸟",2222,0000);
p.Display(); //输出父类id
s.Person::Display(); //输出子类id
s.Display(); //输出的子类id和学号
}
这里首次提到隐藏(重定义)的概念,注意它是在不同作用域,只要成员的名字一样就会被隐藏起来,与参数返回值都无关;而重载是同一作用域内当参数类型、参数个数或参数顺序不同时可以使用同名函数
派生类默认构造函数:
继承体系下,派生类中如果没有显式定义这六个默认成员函数,编译器则会合成(注意这里的用词:之前没学继承之前说的是生成)这六个默认的成员函数
生成::不依赖于任何东西,只是编译器根据类的定义简单生成基于基础类型的默认成员函数
合成::必须依赖于基类,编译器根据基类的相应成员函数的行为来合成派生类的默认成员函数
class Person //父类
{
public:
Person(const char* name)
:_name(name)
{
cout << "Person(const char* name)" << 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 //子类
{
public:
Student(const char* name,int num) //子类构造函数
:Person(name)
,_num(num)
{
cout << "Student(const char* name,int num)" << endl;
}
Student(const Student& s) //子类拷贝构造函数
:Person(s)
,_num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator = (const Student& s) //子类的赋值运算符重载函数
{
if (this != &s)
{
Person::operator=(s); //发生切片行为
_num = s._num;
}
cout << "Student& operator = (const Student& s)" << endl;
return *this;
}
~Student() //子类的析构函数
{
cout << "~Student()" << endl;
}
private:
int _num; //学号
};
void test()
{
Student s("鸟",1111);
Student s1 = s;
Student s2("小亮", 2222);
s2 = s;
}
派生类对象的构造与析构:
继承体系下派生类和基类构造函数的调用次序
继承体系下派生类和基类析构函数的调用次序
有了对派生类构造函数和析构函数的认识,我们再来看看上面程序的运行结果:
说明:
基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表
基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数
基类定义了带有形参表构造函数,派生类就一定定义构造函数
问题:
如何实现一个不能被继承的类?
基类的构造函数访问权限设置成私有的。
继承体系下派生类的对象模型
对象模型为对象中非静态成员变量在内存中的布局形式,与成员函数无关,因此以下类中只给出了成员变量
单继承:一个类只有一个直接父亲时称这个继承关系为单继承
多继承:一个类有两个或两个以上的直接父亲时称这个继承关系为多继承
菱形继承:
菱形继承的对象模型:
从上面的菱形继承的对象模型中,我们发现Assistant类中存在两份Person对象,因此在派生类中访问继承于基类的成员变量时,会存在数据冗余及二义性的问题
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; //主修课程
};
void test()
{
Assistant A;
//A._name = "鸟哥"; //错误提示:对_name的访问不明确
//显示指定访问那个父类的成员
A.Student::_name = "一号鸟";
A.Teacher::_name = "二号鸟";
}
虚拟继承
虚拟继承能够解决以上菱形继承的二义性和数据冗余问题
在继承权限前加上virtual关键字即可构成虚拟继承
虚继承的特点是,在任何派生类中的virtual基类总用同一个(共享)对象表示
class Person
{
public:
string _name; //姓名
};
class Student : virtual public Person
{
protected:
int _num; //学号
};
class Teacher :virtual public Person
{
protected:
int _id; //职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; //主修课程
};
void test()
{
Assistant A;
A._name = "鸟哥"; //对_name的访问没有问题
//显示指定访问父类的成员
A.Student::_name = "一号鸟";
A.Teacher::_name = "二号鸟";
}
从以上图中可以看到的是,不管你以那种方式对父类成员赋值,首先都是支持的并且后一次赋值会覆盖掉前面的赋值,这里面的原因是构成虚拟继承后,菱形继承的对象模型发生了本质上的一个变化。
虚基表: