继承的概念和定义
- 继承的概念
** 继承**(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的。
- 继承使用,实例代码
class Person
{
public:
void print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
//c++11的补丁
string _name = "peter";
int _age = 18;
};
class Student : public Person
{
protected:
int _stuid; //学号
};
class Teacher : public Person
{
protected:
int _jobid; //工号
};
int main()
{
//Student 和 Teacher中都没有Print函数,但是他们继承了Person类,所以也可以调用,相当于有了
Student s;
Teacher t;
s.print();
t.print();
return 0;
}
- 继承的定义
- 继承关系和访问限定符
从这里可以看出,共有3*3 = 9种组合方式。那么,这些方式有什么区别呢?
类成员/继承方式 | public 继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 派生类中不可见 | 派生类中不可见 | 派生类中不可见 |
总结:
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义protected。可以看出保护成员限定符是因继承才出现的。
- 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他
成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected
private。- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
代码示例演示:
- 继承方式为public,访问限定符为public
- 继承方式是public,基类成员有protected和private
这里涉及到一个问题:当基类成员是protected的时候,派生类该如何访问?
当基类成员是protected的时候,派生类该如何修改?
-
继承方式是public,访问限定符是private
-
继承方式是public,访问限定符是protected
因为访问限定符是protected,就代表,最多只支持在派生类中访问,不支持在类外访问。
这也证明了上面的总结,最后的权限由继承方式和访问限定符中小的那个决定。
如果一直用public继承和public的权限,就不会有那么多麻烦了,最多还会用到protected,private很少用到。
基类和派生类对象的赋值转换
派生类对象可以赋值给基类的对象/基类的引用/基类的指针,这里有一个形象的说法叫做切片或者切割,寓意把派生类中基类的那一部分切来赋值过去。
基类对象不能赋值给派生类对象。
基类的指针/基类的引用可以通过强制类型转换赋值给派生类的指针/派生类的引用。
演示代码1:
class Person
{
protected:
string _name;
int _age;
string _sex;
};
class Student : public Person
{
public:
int _No;
};
int main()
{
Student s;
//1.子类的对象可以赋值给基类的对象/引用/指针
Person p = s;
Person& rp = s;
Person* pp = &s;
//2.基类对象不能赋值给子类的对象
//s = p;
//3.基类的指针可以通过强制类型转换赋值给子类的指针
pp = &s; //pp是基类的指针,但是派生类赋值给pp了
Student* ps1 = (Student*)pp; // 强制类型转换了
ps1->_No = 10;
pp = &p; //pp是基类的指针,指向自己
Student* ps2 = (Student*)pp; //这种情况虽然可以,但是会存在越界访问
//ps2->_No = 20;
return 0;
}
演示代码2:
class Person
{
public:
void print()
{
cout << "_name: " << _name << endl;
cout << "_sex: " << _sex << endl;
cout << "_age: " << _age << endl;
}
string _name = "zhangsan";
string _sex = "男";
int _age = 18;
};
class Student : public Person
{
public:
int _NO;
};
int main()
{
Student s;
Person p;
p.print();
cout << "///" << endl;
s._name = "lisi";
Person* pp = &s; //赋值给基类的指针
pp->print();
cout << "///" << endl;
s._age = 90;
Person& rp = s; //赋值给基类的引用
rp.print();
return 0;
}
运行结果:
继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
构成隐藏的条件:
- 在不同的作用域
- 函数名/成员名相同
隐藏和重载的区别是:函数重载需要在同一个作用域,函数重载是相同的函数名,不同的参数列表和返回值,而隐藏只需要函数名相同即可.
- 成员名相同构成的隐藏
//Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected:
string _name = "小李子"; // 姓名
int _num = 111;// 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 身份证号:" << Person::_num << endl;
cout << " 学号:" << _num << endl;
}
protected:
// 这里Student中的_num和Person中的_num构成隐藏
int _num = 999; // 学号
};
void Test()
{
Student s1;
s1.Print();
};
int main()
{
Test();
return 0;
2. 函数名相同构成的隐藏
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
void fun()
{
cout << "A::func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "B::func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun(10); // 在b作用域中就默认调用b中的函数
b.A::fun();//指定类域之后,调用指定的
};
int main()
{
Test();
return 0;
}
派生类的默认成员函数
构造函数
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
什么是默认构造函数?
- 无参的
- 全缺省的
- 我们不写,编译器默认生成的
第一个版本,有默认构造函数的时候,派生类该怎么写构造函数?
class Person
{
public:
//构造函数 --> 默认构造函数
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << _name << endl;
}
string _name;
};
class Student : public Person
{
public:
//这里Person存在默认构造,所以Student写构造函数的时候就可以不显式调用
Student(int num = 19)
:_num(num)
{
cout << "Student()" << _name << endl;
}
void print()
{
cout << _name << " " << _num << endl;
}
int _num;
};
int main()
{
//这里的声明,因为Student的的构造函数有缺省值,是默认构造
//所以可以直接写Student s,这样就会调用缺省值
//但是不能写Student("张三", 19);因为根本没有传_name进去,所以不能这样初始化
//Student s(19);这样的初始化形式是可以的,因为传了_num进去
Student s;
s._name = "张三";
s.print();
return 0;
}
运行结果:
第二个版本,没有默认构造的时候,派生类该怎么写构造函数?
错误的写法:
正确的写法:
class Person
{
public:
//构造函数 --> 不是默认构造函数
Person(const char* name)
: _name(name)
{
cout << "Person()" << _name << endl;
}
string _name;
};
class Student : public Person
{
public:
//这里Person没有默认构造,所以Student写构造函数的时候就必须显式调用
Student(const char* name, int num = 19)
:Person(name)
,_num(num)
{
cout << "Student()" << _name << endl;
}
int _num;
};
int main()
{
//这里之所以可以传"张三"和"19",是因为Student的构造函数中传了name和num
Student s("张三", 19);
}
运行结果:
从两次的运行结果可以看出,无论是否有默认构造函数,无论是否显式调用,最后调用派生类的时候,都是先调用父类的构造函数再调用派生类的构造函数
满足一下好奇心:基类有默认构造函数,派生类可以不显式写,但是派生类非要写呢?会报错吗?
不会
拷贝构造函数
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
class Person
{
public:
Person(const char* name = "zahngsan")
{}
Person(const Person& p)
:_name(p._name)
{
cout << "Person的拷贝构造函数" << endl;
}
string _name;
};
class Student : public Person
{
public:
Student(const char* name, int num)
{}
//调用父类的拷贝构造函数,直接传对象, 通过“切片”拿到父类的那一部分
Student(const Student& s)
:Person(s)
,_num(s._num)
{
cout << "Student的拷贝构造函数" << endl;
}
int _num;
};
int main()
{
Student s("zhangsan", 19);
//调用拷贝构造
Student s1(s);
return 0;
}
运行结果:
上述那样写是为了代码能跑起来,更直观的是:
赋值运算符重载
派生类的operator=必须要调用基类的operator=完成基类的复制。
代码:
class Person
{
public:
Person(const char* name = "zahngsan")
{}
Person(const Person& p)
:_name(p._name)
{}
//返回的*this,所以返回类型是Person&
Person& operator=(const Person& p)
{
//s != *this是错误的写法,没有这样的比较方法
if (&p != this)
{
cout << "Person的operator=函数" << endl;
_name = p._name;
}
return *this;
}
string _name;
};
class Student : public Person
{
public:
Student(const char* name = "peter", int num = 12)
{}
//调用父类的拷贝构造函数,直接传对象, 通过“切片”拿到父类的那一部分
Student(const Student& s)
:Person(s)
,_num(s._num)
{}
Student& operator=(const Student& s)
{
if (&s != this)
{
//注意这里必须使用域作用限定符,如果不用的话,operator=是一个隐藏
//在Student作用域里,会优先调用自己作用域的函数,所以会无穷递归
//也就是说,写成operator=(s)是错误的
Person::operator=(s);
_num = s._num;
cout << "Studnet的operator=函数" << endl;
}
return *this;
}
int _num;
};
int main()
{
Student s("zhangsan", 19);
Student s1;
//调用赋值重载函数
s1 = s;
return 0;
}
注意:上述的代码是为了能跑起来,更直观的代码如下:
析构函数
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
class Person
{
public:
Person(const char* name = "zahngsan")
{}
~Person()
{
cout << "Person的析构函数" << endl;
}
string _name;
};
class Student : public Person
{
public:
Student(const char* name = "peter", int num = 12)
{}
//在派生类中不要显式写基类的析构函数,
//因为编译器要先调用基类的析构函数再调用派生类的析构函数
//因为构造函数先调用派生类的再调用基类的
~Student()
{
cout << "Student的析构函数" << endl;
}
int _num;
};
int main()
{
Student s;
return 0;
}
运行结果:
注意:在派生类的析构函数中,不要自己调用,有两个原因:
- 由于多态的一些原因,编译器会对析构函数名进行特殊处理,处理成destructor(),所以父类的析构函数在不加virtual的情况下,会和子类的析构函数构成隐藏关系。所以如果要自己调用,必须用域作用限定符,Person :: ~Person()
- 由于栈的特性,构造函数调用时先调父类再调子类,所以析构时先析构子类,再析构父类,编译器为了能保证这个特性,默认在子类析构完成后调用父类的析构函数,所以不需要在子类的析构函数中显式地调用父类的析构函数。
所以,我们在子类的析构函数中显式调用析构函数会造成父类析构两次的情况。
总结:
继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
继承与静态成员
**基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。**无论派生出多少个子类,都只有一个static成员实例 。
//这段代码的意思是:Person::_count为4,然后赋值Student::_count = 0,
//如果不是静态成员,那么最后输出Person::_count应该还是4,但是最后输出为0,
// 意味着Person中的_count和Student中的_count是同一个_count
class Person
{
public:
Person() { ++_count; }
protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
void TestPerson()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
cout << " 人数 :" << Person::_count << endl;
Student::_count = 0;
cout << " 人数 :" << Person::_count << endl;
}
int main()
{
TestPerson();
return 0;
}
运行结果:
如果为非静态成员,会发生什么?
菱形继承与菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承::菱形继承是多继承的一种特殊情况。
菱形继承的问题
从上面这张图可以看出来,菱形继承具有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。
二义性的问题好解决,只需要手动的指定类域就行了。
class Person
{
public:
string _name;
};
class Student : public Person
{
protected:
int _num;
};
class Teacher : public Person
{
protected:
int _id;
};
class Assistant : public Teacher, public Student
{
string _majorCourse; // 主修课程
};
int main()
{
Assistant as;
//会有二义性,定义不明确
//as._name = "li ming";
//指定才不会有二义性的问题,但是代码冗余的问题并没有解决
as.Student::_name = "zhangsan";
as.Teacher::_name = "lisi";
return 0;
}
虚继承可以解决这两个问题
虚继承
//虚拟继承只能写在腰部的位置
//Student继承Person Teacher继承Person
//Assistant继承Student和Teacher
//这种情况下,在Student继承Person和Teacher继承Person的时候写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 Teacher, public Student
{
string _majorCourse; // 主修课程
};
int main()
{
Assistant as;
//虚继承之后,不会有二义性
as._name = "li ming";
return 0;
}
虚继承只能写在腰部的位置
简化模型观察虚继承
这是有虚继承的代码
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
上述代码是使用了虚继承的代码,不存在数据冗余和二义性的问题。
下面的图是没有使用虚继承的代码的内存图,存在数据冗余和二义性的问题。
下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?
这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
总结:菱形继承绝对是一个大坑,非常麻烦,所以,为了解决这个问题,最好的办法就是,不用菱形继承。