一、继承的概念及定义
1.继承的概念
继承机制:面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
原有的类称为父类或基类,而继承了该类的类称为子类或派生类。
2.继承定义
继承格式:
class [派生类名] : [继承方式] [基类名]
例如下面代码,Student类继承Person类
class Person
{
private:
string _name = "lisa";
int age = 20;
};
class Student : public Person
{
private:
string _id = "1111";
};
Person类就成了Student的一部分。
继承方式
继承方式:public继承、private继承、protected继承。
public继承
class Person
{
private:
string _name = "lisa";
int age = 20;
};
class Student : public Person
{
private:
string _id = "1111";
};
- 父类的private私有成员被子类继承,但是子类不能访问父类的私有成员
2. 除过父类的private成员不可见,其他成员的访问权限不会发生变化
private继承
class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
class Person
{
public:
int age = 20;
private:
string _name = "lisa";
};
//class默认私有继承
//class Student : Person
class Student : private Person
{
public:
void Print()
{
cout << age << endl;
}
private:
string _id = "1111";
};
父类中的所有成员,除过父类的private成员不可见,其他成员的访问权限在类外都是私有的。
保护继承
保护继承:基类成员不能在类外直接被访问,但在派生类中能访问。 可以看出保护成员限定符是因继承才出现的。
class Person
{
public:
int age = 20;
private:
string _name = "lisa";
};
class Student : protected Person
{
public:
void Print()
{
cout << age << endl;
}
private:
string _id = "1111";
};
父类中的所有成员,除过父类的private成员不可见,访问权限为public会变成protected,在子类中可以被访问,类外不能访问。
总结:
基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 = Min(成员在基类的访问限定符,继承方式),public > protected > private。
3.基类和派生类对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
class Person
{
private:
string _name = "lisa";
int _age = 20;
};
class Student : public Person
{
private:
string _id;
};
void test()
{
Person p;
Student s;
//切片
p = s;
Person &rs = s;
Person *ps = &s;
}
1.父类对象不能赋值给子类对象,也不能进行强制类型转换
2.父类引用、指针不能直接赋值给子类的引用、指针,但是可以通过强制 类型转换赋值
void test()
{
Person p;
Student s;
s = p; //错误,父类对象不能直接赋值给子类对象
//父类指针可以通过强制类型转换赋值给子类指针
Person *pc = &p;
Student *ps = (Student*)pc;
//父类引用可以通过强制类型转换赋值给子类引用
Person &rc = p;
Student &rs = (Student&)rc;
}
这种情况下转换时虽然可以,但是会存在越界访问的问题。
pc是一个父类对象,其中包含了_name和_age变量,并没有_id这个值,_id这个变量没有被创建出来,所以访问_id相当于非法访问。
4.继承中的作用域
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
Person中的_age与Student中的_age构成同名隐藏,访问的是子类的成员变量。
解决方法:在子类中可以使用 基类::基类成员 显示访问
如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
如果需要访问父类的同名函数,需要加父类作用域
Person中的fun和Student中的fun,不构成重载,因为不是在同一作用域。
注意在实际中在继承体系里面最好不要定义同名的成员。
二、派生类的默认成员函数
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
构造函数
子类的默认构造会自动调用父类的构造函数
父类的成员需要调用父类构造完成初始化,不能在子类的初始化列表中直接初始化父类的成员。
如果父类有默认构造,初始化列表中可以不用显示调用父类的构造函数,编译器会自动调用父类的默认构造;如果没有默认构造,必须显示调用父类的一个构造函数。
总结: 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
拷贝构造
子类的默认拷贝构造自动调用父亲的拷贝构造
如果子类显示定义了拷贝构造,没有显示调用父类的拷贝构造或父类的构造函数,子类的拷贝构造会自动调用父类的默认构造;若没有默认构造,就会出错。
子类的显示拷贝构造,初始化列表中可以显示调用父类的构造,也可以调用父类的拷贝构造。
总结: 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
赋值运算符
子类的默认赋值运算符会自动调用父类的赋值运算符
子类显示定义的赋值运算符不会自动调用父类的赋值运算符。
析构函数
子类的默认析构会自动调用父类的析构
子类的显示析构也会调用父类的析构
子类析构和父类析构是同名隐藏,若要在子类析构中显示调用父类析构,需要加上作用域。
~Student()
{
Person::~Person();
cout << "~Student" << endl;
}
但是编译器在任何情况下都会自动调用父类的析构函数,不需要显示调用父类析构,否则可能会出现二次释放的问题。
子类成员的初始化顺序: 一定是先初始化父类成员,再初始化子类定义的新成员。
子类析构的顺序: 先析构子类,再析构父类。
继承与友元:
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
三.继承与静态成员
基类的static成员在整个的继承体系中,被所有对象共享。
class A
{
public:
void fun()
{
++_a;
}
public:
static int _a;
};
int A::_a = 0;
class B : public A
{};
class C :public B
{};