类的继承:
我们一直以来接触的函数在一定程度上完成了代码的复用,这里介绍面向对象的重要复用机制—继承
继承机制是面向对象程序设计使代码可以复用的重要手段,他允许我们在保持原有类特性的基础上进行扩展,增加功能。这样产生的类,称为派生类。继承呈现了面向对象程序的层次性,体现了从简单到复杂的认知过程
继承的简单使用:
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成员 | 不可见只能通过基类接口访问 | 不可见只能通过基类接口访问 | 不可见只能通过基类接口访问 |
总结:
- 基类private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访 问,就定义为protected。可以看出保护成员限定符是因继承才出现的
- public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一 个父类对象
- protected/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承
- 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员, 基类的私有成员存在但是在子类中 不可见(不能访问)
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写 出继承方式
- 在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承
is-a原则:继承关系,例如正方形和长方形在性质层面是一种继承关系
has-a原则:组合关系,例如轮胎和车是一种组合关系
至于使用哪个原则,应该是具体场景具体对待,看两种事物之间是继承关系还是组合关系,是继承关系就用is-a,是组合关系就用has-a。
可能会有这样的场景,两种事物之间既可以是继承关系,又可以是组合关系,这时该用哪个我们初学者的确很难回答出来,多年的C++开发者建议用has-a,他们的实践经验是这种情况下is-a很难做下去。
切片 / 切割:
- 子类对象可以赋值给父类对象(切片)
- 父类对象不能赋值给子类对象
- 父类的指针/引用可以指向子类对象,但是他并不能访问所有成员,它只能访问到子类继承了自己的成员
- 子类的指针/引用不能指向父类对象
通过强转子类的指针/引用可以指向父亲对象,但是这种情况下父亲对象指针其实是一种越界访问行为,所以等到作用域结束后程序会崩溃。因为此时父亲对象指针指向的范围扩大了,而扩大的那一部分不属于它的合法指向范围(合法域),虽然那块区域的大小刚好是_num的大小。
class Person
{
public:
void Display()
{
cout<<_name<<endl;;
}
protected:
string _name; //姓名
};
class Student : public Person
{
public:
int _num; //学号
};
void test()
{
Person p;
Student s;
//子类对象可以赋值给父类对象(切片)
//p = s;
//父类对象不能赋值给子类对象
//s = p;
//父类指针可以指向子类对象
//Person* p1 = &s;
//子类指针不可以指向父类对象
//Student* p2 = &p;
//通过强转子类指针可以指向父类对象
Student* p3 = (Student*)&p;
}
继承中的作用域:
- 在继承体系中基类和派生类都有自己独立的作用域
- 子类和父类中有同名成员,子类成员将屏蔽掉父类对同名成员的直接访问(在子类成员函数中可使用基类::基类成员访问)--隐藏 --重定义
- 在实际中继承体系中最好不要定义同名的成员
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(); //加上Person::后输出子类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;
}
派生类对象的构造与析构:
继承体系下派生类和基类构造函数的调用次序
继承体系下派生类和基类析构函数的调用次序
有了对派生类构造函数和析构函数的认识,我们再来看看上面程序的运行结果:
说明:
基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表
基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数
基类定义了带有形参表构造函数,派生类就一定定义构造函数
问题:
如何实现一个不能被继承的类?
基类的构造函数访问权限设置成私有的。