一、什么是继承?
定义: 继承机制就是面向对象设计中使代码可以复用的重要手段,它允许在程序员保持原有类特性的基础上进行扩展,增加功能。这样产生的新类,称为派生类(子类),被继承的类称为基类(父类)。
下面我们来看一下实力更深入的了解一下它吧。
下面是一个Student
类继承Person
类的具体实例:
#include<iostream>
using namespace std;
class Person
{
public:
void Print()
{
cout << _height << endl;
cout << _age << endl;
cout << _name << endl;
}
protected:
double _height = 1.85;//身高
int _age = 20;//年龄
string _name = "zhangyu";//姓名
};
class Student :public Person
{
private:
int _stuid = 123456;//学号
int _grade = 1;//年级
};
int main()
{
Person p;
Student s;
return 0;
}
看一下这张图片,就可以看出来,使用Student定义的s,就继承了Person的成员和函数。
当然不同继承方式的继承效果也就不同:
我们先来说一下关于private,对于基类中的private成员,即使派生来对象中,但是语法上限制派生类对象无论在类里面还是在类外面都无法访问它。看一段代码更好的去理解
class Person
{
//public:
/*void Print()
{
cout << _height << endl;
cout << _age << endl;
cout << _name << endl;
}*/
private:
double _height = 1.85;//身高
int _age = 20;//年龄
string _name = "zhangyu";//姓名
};
class Student :public Person
{
public:
void Print()
{
cout << _height << endl;
cout << _age << endl;
cout << _name << endl;
cout << _stuid << endl;
cout << _grade << endl;
}
这里我们可以看到对于被private修饰的对象是无法在子类中访问的
所以这里也是private和protected的区别之一。
所以我们这里可以得到以下几个理论:
我们在前面的学习知道相近类型之间是能够赋值,因为他们之间会发生隐式类型转换。
int a = 10;
char b = a;//隐式类型转换
char& c = a;//报错
const char& c = a;//正确
double& d = a;//报错
const double& d = a;
char& c = a;
和 double& d = a;
这两行代码是非法的。因为引用必须绑定到与其类型完全匹配的对象上,否则就会引起权限的放大,因为产生的临时对象具有常性,常性是一种编程中的约束和特性,所以不能将 int
类型的变量直接绑定到 char
引用或 double
引用。
那么基类与派生类直接是否也遵循这个转换规则呢,接下来让我们以Person
类与Student
类来验证一下吧。
派生类对象是可以赋值给基类对象的,因为派生类对象本就存在基类成员。相反,基类成员就无法赋值给派生类成员,因为有些成员派生类有,而基类没有。
所以就会报错
派生类对象的引用赋值能够给基类对象,其中引用不许需要const
,证明其赋值之间并没有发生隐式类型转换,产生临时对象。
Student s;
Person& rp = s;//ok
派生类对象的指针能够赋值给基类对象,这种情况与引用十分类似。
Student s;
Person* pp = &s;//ok
基类指针能够通过强转赋值给派生类指针,但是也可能造成越界访问
Person p;
Student *sp = (Student*) & p;//ok
看这里,我们基类与派生类都定义了height变量,那么这里输出谁呢?
这里我们可以看到输出的结果是1.9,要是想输出基类中的height如何做呢,我们就要加上域作用限定符,
void Print()//隐藏
{
cout <<Person:: _height << endl;
cout << _age << endl;
cout << _name << endl;
}
这样就输出1.85了
class Person
{
public:
void func()
{
cout << "func()" << endl;
}
protected:
double _height = 1.85;//身高
int _age = 20;//年龄
string _name = "zhangyu";//姓名
};
class Student :public Person
{
public:
void func(int i)
{
Person::func();
cout << "func(int i)->" << i << endl;
}
private:
double _height = 1.90;
int _stuid = 123456;//学号
int _grade = 1;//年级
};
int main()
{
Student s;
s.func(1);
return 0;
}
首先第一个问题,两个fun
函数之间是函数重载还是隐藏的关系?答案当然是隐藏关系,因为函数重载针对的是同一个作用域的函数,而基类与派生类直接作用域不同。
在隐藏关系中,同名函数默认调用的当前作用域的函数,如果想调用其他作用域的函数,则需要使用域作用限定符。
我们知道在类中有6个默认成员函数,如果不显示定义,编译会自动生成。那么如果在派生类中,这几个成员函数是如何生成的呢?
4.1:
class Person
{
public:
Person()
:_name("xzy")
, _height(1.85)
,_age(20)
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
double _height = 1.85;//身高
int _age = 20;//年龄
string _name = "zhangyu";//姓名
};
class Student :public Person
{
public:
Student()
:_height(1.80)
,_grade(2)
{
cout << "Student()" << endl;
}
~Student()
{
cout << "~Student" << endl;
}
private:
double _height = 1.90;
int _stuid = 123456;//学号
int _grade = 1;//年级
};
int main()
{
Student s;
return 0;
}
显而易见:派生类对象在调用构造函数时会先调用基类的构造函数,再调用派生类的构造函数。调用析构函数时会先调用派生类的析构函数,再调用基类的析构函数。
4.2:派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
class Person
{
public:
Person(const char* name)//没有默认构造
: _name(name)
{
}
Person(const Person& p)
: _name(p._name)
{
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(int num, const char* name)
:_num(num)
//,_name(name) error
, Person(name)//正确初始化
{
;
}
protected:
int _num; //学号
};
4.3编译器会对派生类与基类的析构函数名进行特殊处理,都会被处理成destrutor(),所以派生类与基类的析构函数构成隐藏关系。
Person(const char* name)//没有默认构造
: _name(name)
{
cout << "Person()" << endl;
}
~Person()//析构
{
cout << "~Person()" << endl;
}
Student(int num, const char* name)//构造
:_num(num)
//,_name(name) error
, Person(name)//正确初始化
{
cout << "Student()" << endl;
}
~Student()
{
//因为构成覆盖关系,所以指定域作用限定符
Person::~Person();
cout << "~Student()" << endl;
}
但是为什么Person
的析构函数会多调用一次呢?因为编译器为了保证基类的析构最后调用,所以在调用派生类析构函数之后会自动调用基类的构造函数。所以为了保证调用的正确顺序,派生类的析构函数我们不需要显示定义。
4.4拷贝构造与赋值重载必须调用基类的拷贝构造与赋值重载完成对基类的初始化。
//拷贝构造
Person(const Person& p)
: _name(p._name)
{
}
//赋值重载
Person& operator=(const Person& p)
{
if (this != &p)
_name = p._name;
return *this;
}
Student(const Student& s)//拷贝构造
:_num(s._num)
, Person(s)//派生类赋值给基类
{
;
}
//赋值重载
Student& operator = (const Student& s)
{
if (this != &s)
{
//加域作用限定,否则发生死循环
Person::operator =(s);
_num = s._num;
}
return *this;
}
派生类赋值重载调用基类赋值重载时记得加域作用限定符,否则就会发生死循环。
友元关系不能继承,也就是说父类的友元不是子类的友元,不能访问子类私有和保护成员。
class Student;//声明
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,静态成员被所有类对象包括起子类和子类的子类共享。无论派生出多少个子类,都只有一个static成员实例 。
我们可以通过下面这段代码验证:
class Person
{
public:
Person() { ++_count; }
string _name; // 姓名
static int _count; // 统计人的个数。
};
int Person::_count = 0;//静态成员初始化
class Student : public Person
{
protected:
int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
int main()
{
Person p;
Student s;
Graduate g;
cout << &(p._name) << endl;
cout << &(s._name) << endl;
cout << &(g._name) << endl;
cout << &(p._count) << endl;
cout << &(s._count) << endl;
cout << &(g._count) << endl;
return 0;
}
从上图我们就可以看出非静态成员在不同基类与派生类中地址不同,这就说明他们在不同类是独立存在的。而非静态成员却恰恰相反,地址相同,证明基类与派生类都是用同一个静态成员。
单继承:一个子类只有一个直接父类的继承关系为单继承
多继承:一个子类有两个或以上直接父类。
菱形继承:就是继承关系近似呈一个菱形形状,如下图:
菱形继承会造成两个问题:数据冗余和二义性。
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 = "peter"; 这样会产生二义性无法明确知道访问的是哪一个类
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
为了解决数据二义性与冗余的问题,C++引入虚拟继承。虚拟继承用法十分简单,直接在继承前加上一个关键字: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 = "peter";
}
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。基类的内部细节对子类可见。
class A
{};
class B : public A
{};
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为**白箱复用(white - box reuse)。**术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
组合是一种has - a的关系。假设B组合了A,每个B对象中都有一个A对象。
class A
{};
class B
{
A _aa;
};
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black - box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。
所以一般推荐优先使用对象组合,而不是类继承