继承是面向对象复用的重要手段。通过继承定义一个类,继承是类型之间的关系建模,共享共有的东西,实现各自本质不同的东西。
成员访问限定符&继承关系:
这是一个简单的继承关系:
#include<iostream>
#include<Windows.h>
using namespace std;
class person
{
public:
char* name;
char* sex;
int age;
public:
void display()
{
cout << "name:" << name << " sex:" << sex << " age:" << age << endl;
}
};
class student :public person
{
public:
int stdnum;
public:
void show()
{
cout << "stdnum:" << stdnum << endl;
}
};
int main()
{
student s;
s.name = "xiaowang";
s.sex = "man";
s.age = 18;
s.stdnum = 2016;
s.display();
s.show();
system("pause");
return 0;
}
代码执行时的情况:
继承是一种复用手段,在继承关系基类的成员类的成员派生类的成员,从而达到复用多个目的。
三种继承关系下基类成员在派生类的访问关系变化:
总结:
- 基类的私有成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象都是一个父类对象。
- protected继承/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是has-s的原则,所以大多数情况下不使用这两种继承,绝大多数场景都是公有继承。
- 不管哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,基类的私有成员存在但是在子类中不可见,即不能访问。
- 使用关键字class默认继承方式是private,使用struct时默认的继承方式是public,最好显示出继承方式。
赋值兼容规则–public继承
- 子类对象可以赋值给父类对象(切割/切片)
- 父类对象不能赋值给子类对象
- 父类的指针/引用 可以指向子类对象
- 子类的指针/引用不能指向父类对象(可以通过强制类型转换完成,但不能调用成员函数,会崩溃)
class person
{
public:
char* name;
char* sex;
int age;
public:
void display()
{
cout << "name:" << name << " sex:" << sex << " age:" << age << endl;
}
};
class student :public person
{
public:
int stdnum;//学号
public:
void show()
{
cout << "stdnum:" << stdnum << endl;
}
};
int main()
{
person p;
p.name = "xiaozhang";
p.age = 18;
student s;
s.name = "xiaowang";
p.age = 20;
// 切割/切片
// 子类可以给父类赋值
p = s;
//父类不能给给子类赋值
//s=p;
//父类的指针可以指向子类对象
person* p1 = &s;
//父类的引用可以指向子类对象
person& r1 = s;
p1->name = "xx";
r1.name = "yy";
//子类的指针/引用不能指向父类(可以通过强制类型转换完成)
student* p2 = (student*)&p;
student& r2 = (student&)p;
p2->name = "xx";
r2.name = "yy";
system("pause");
return 0;
}
派生类的默认成员函数
在继承关系里面,在派生类中如果没有显示定义的这六个成员函数,编译系统则会默认合成这六个默认的成员函数
1.构造函数
子类的构造函数会自动调用父类的构造函数。
派生类构造函数必须使用基类构造函数。
构造函数不同于其他类的方法,应为它创建新的对象,要调用基类中的私有成员,因为派生类不能直接访问基类的私有成员,必须要调用基类方法进行访问,而其他类仅仅是被现有的对象调用而已,所以构造函数不能被继承;
基类的构造函数先被调用,后面是派生类的构造函数,其实,在main函数中,先调用的是派生类的构造函数,基类的构造函数是在派生类的初始化列表中调用的
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{
cout << "Person()" << endl;
}
}
class Student : public Person
{
public:
Student(const char* name, int age, int stunum)
:Person(name, age)
{
_stunum = stunum;
cout << "Student()" << endl;
}
}
2.拷贝构造函数
子类的拷贝构造函数会自动调用父类的拷贝构造函数。
拷贝构造会有切片。
在下列情况下使用拷贝构造:
- 将新的对象初始化为一个同类对象
- 按值将对象传递给函数
- 函数按值返回对象
- 编译器生成临时对象
如果程序没有显式定义拷贝构造函数,编译器将自动生成一个。
当然,如果想在派生类中构造基类对象,那么不仅仅可以用构造函数,也可以用拷贝构造函数
class Person
{
public:
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{
cout << "Person(const Person& p)" << endl;
}
}
class Student : public Person
{
public:
Student(const Student& s)
:Person(s)
{
_stunum = s._stunum;
}
}
3.析构函数
子类析构函数与父类析构函数会构成隐藏(虽然名字不同,但编译器会使他们重名,从而构成隐藏)
为了保证析构顺序,系统会自己调用析构函数,依据栈的规则,先call子类析构,在call父类析构
初始化时,父类先初始化,子类后初始化
析构时,子类先析构,父类后析构。栈的后进先出
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
}
class Student : public Person
{
public:
~Student() //隐藏了父类的析构函数
{
Person::~Person();
cout << "~Student()" << endl;
}
}
4.赋值操作符重载
子类对象可以赋值给父类对象
子类的赋值操作符重载会自动调用父类的赋值操作符重载。
赋值运算符是不能被继承的,原因很简单。派生类继承的方法的特征与基类完全相同,但赋值操作符的特征随类而异,因为它包含一个类型为其所属类的形参。
如果编译器发现程序将一个对象赋给同一个类的另一个对象,它将自动为这个类提供一个赋值操作符。这个操作符的默认版本将采用成员赋值,即将原对象的相应成员赋给目标对象的每个成员。
如果对象属于派生类,编译器将使用基类赋值操作符来处理派生对象中基类部分的赋值,如果显示的为基类提供了赋值操作符,将使用该操作符。
class Person
{
public:
Person& operator=(const Person& p)
{
if (this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}
}
class Student : public Person
{
public:
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_stunum = s._stunum;
}
return *this;
}
}