举个例子: 在定义类的时候,内部的成员甚至成员函数都有相同之处,假设我定义,学生,老师,导员,院长,校长等等,他们都有共同的特性,名字,性别,年龄等等,不相同的有学号,工号等等。 那么如果像往常一样去定义,会写很多重复的内容。 这时候C++就提供了一种语法叫做继承。 继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。
#include<iostream>
using namespace std;
class information//信息类
{
public:
void print()
{
cout << "name:" << name << endl;
cout << "age:" << age << endl;
}
protected:
char name[10] = "Baiye";//名字
int age = 18;//年龄
};
class student:public information
{
protected:
int student_number;//学号
};
class teacher:public information
{
protected:
int job_number;//工号
};
int main()
{
student s1;
teacher s2;
s1.print();
s2.print();
return 0;
}
这里打印结果显示了年龄,名字,但是看原来的这两个类中的并没有打印函数,也没有这两个成员。
调试发现,学生类和教师类创建的对象中有信息类的成员和成员函数。
这里来说说定义的格式:
派生类也可以叫子类,基类也可以叫做父类。 这里就算子类继承了父类成员,他们也是两种不同的类,里面的成员也是各自的。 继承方式是什么呢? 就是将父类的内容以什么访问限定符继承在子类当中。 当然,父类的内容里面也是有三种访问限定符的,父类的访问限定符和继承方式也是有关系的 ,所以这样看来,就有9中方式了。
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
1.这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。 2.那么基类的成员又不想被外面访问,又想继承到子类,这个时候父类的内容就要用保护了,也可以看得出保护是因此而出现的,之前保护和私有是没什么去别的。 3.继承方式也可以不写,使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。 4.其实最常用的也只有这两种情况而已:
总结:继承和访问限定符的9种情况,其实就是取权限小的那一种情况。
我们平时在进行不同类型赋值的时候,都会产生临时变量,但是派生类和基类的对象却不会。(公有继承) 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。 基类对象不能赋值给派生类对象。 这里不是类型转换,是类似与切割的方式:
子类给父类赋值其实只是让子类中的父类成员给赋值,多余的就不要了,这就像切割一样。 引用和指针也是一样的:
#include<iostream>
using namespace std;
class information//信息类
{
public:
void print()
{
cout << "name:" << name << endl;
cout << "age:" << age << endl;
}
protected:
char name[10] = "Baiye";//名字
int age = 18;//年龄
};
class Student:public information
{
protected:
int student_number = 0;
};
int main()
{
Student s1;
information s2;
information& p1 = s1;//不用加const
return 0;
}
注意:父类不能赋给子类。
在继承体系中基类和派生类都有独立的作用域。 这说明命名是可以重复的,那么同名成员要怎么算呢?
#include<iostream>
using namespace std;
class information//信息类
{
protected:
int age = 18;
};
class Student :public information
{
public:
void print()
{
cout << age << endl;
}
protected:
int age = 28;
};
int main()
{
Student s1;
s1.print();
return 0;
}
这里打印的是谁呢? 通常都是采取就近原则,打印函数在子类,那就按照子类中的成员来:
子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。 如果想访问父类的成员可以指定作用域:
这里函数同名是不构成重载,重写,也是隐藏。
#include<iostream>
using namespace std;
class B
{
public:
void print()
{
cout << "B" << endl;
}
protected:
int age = 18;
};
class A :public B
{
public:
void print(int i)
{
cout << "A "<< i << endl;
}
protected:
int age = 28;
};
int main()
{
A s;
s.print(1);
return 0;
}
如果是调用父类的成员函数:
这里报错了。 如果想访问父类的也要加指定作用域:
所以在定义的时候尽量不要定义名字相同的。
#include<iostream>
using namespace std;
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << 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:
protected:
int _num;//学号
};
int main()
{
Student s;
return 0;
}
这里调用了一次构造,一次析构。 构造函数: 首先要知道,在子类当中,父类的成员要调用父类的构造函数才能初始化,这里父类的构造函数我们用的缺省值,如果将缺省值去掉,还是像这样去创建子类对象是会报错的。
那么如果在子类想写构造函数,初始化父类的成员函数必须去调用父类的构造函数,不然会报错。
#include<iostream>
using namespace std;
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << 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)
{ }
protected:
int _num;//学号
};
int main()
{
Student s("baiye",18);
return 0;
}
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。 拷贝构造:
因为都是内置类型,如果是子类的成员就调用子类的默认拷贝构造,父类的成员就去调用父类的拷贝构造。 那么如果遇到深拷贝的时候,子类就必须去写构造函数了。
在初始化列表中调用父类的拷贝构造就可以了。
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。 赋值重载:
派生类的operator=必须要调用基类的operator=完成基类的复制。 析构函数: 析构函数有两个特点,; 1.子类析构和父类析构默认是隐藏关系。(由于多态的关系,所有的析构函数都会被处理成destrutor) 2.先析构子类,后析构父类。
我们发现这里析构函数打印了两次。 因为这里子类析构调用完之后又回去调用父类析构,这是编译器的默认行为。 也就是说我们根本不用去显示的调用父类的析构函数。
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序。
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
#include<iostream>
using namespace std;
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;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例 。
#include<iostream>
using namespace std;
class Person
{
public:
Person() { ++_count; }
protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
int main()
{
Person s1;
Student s2;
cout << s1._count << endl;
cout << s2._count << endl;
cout << &s1._count << endl;
cout << &s2._count << endl;
return 0;
}
这是因为静态成员存储的区域不一样,是储存在静态区的。 这里还要注意一种情况:
这样子是可以的,因为虽然是*和->但是不会去真正的在这个对象里面找,因为静态区是不在这个对象中的,就像之前的成员函数一样,定义的内容也不是在对象内部,只是声明在类中,传递过去的也是空指针,并没有真正的去访问空指针。 其实这样子就相当于告诉你去哪个类里面找。
一个类继承了多个个类,这就是多继承。
最麻烦的就是这种菱形继承,因为数据会冗余。
#include<iostream>
using namespace std;
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; // 主修课程
};
int main()
{
Assistant s;
//s._name;//编译器不知道我们要访问哪个name,产生了二义性
s.Student::_name = "xxx";
s.Teacher::_name = "yyy";
return 0;
}
但是指定作用域去访问并没有彻底的解决这里的问题,所以有了一个关键字,virtual。 虚继承要在菱形继承中间的位置。
#include<iostream>
using namespace std;
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; // 主修课程
};
int main()
{
Assistant s;
s._name = "xxx";//编译器不知道我们要访问哪个name
//s.Student::_name = "xxx";
//s.Teacher::_name = "yyy";
return 0;
}
现在这三个域中的name就都是同一个name了。
先来看看普通类,因为编译器内部做了处理,这里只能用内存窗口去观察了。
#include<iostream>
using namespace std;
class A
{
public:
int _a;
};
class B : public A
//class B : virtual public A
{
public:
int _b;
};
class C : public A
//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地址。
然后再来看看虚继承的:
这是成员在内存中的分布位置。 那么橙框中的内容又是什么呢? 这里面其实是地址,地址在调用两个内存窗口看一下:
那么这里橙色框里面又是什么。
我们看一下距离,第一个距离20,第二个距离12,也就是说上面橙框里面的是找到虚基类对象的偏移量! 那么为什么要储存偏移量呢? 因为子类在给父类赋值的情况下,会发生切片,虚继承的成员因为是在最下面,所以中间如果少了内容位置就会不一样。
也就是说,虚继承之后的对象就不是在类内部了,而是放在下面某一处了,但是这个位置不确定,所以用偏移量来算才是准确的。 像最后一个赋值给父类,他又指向了原来d对象的位置,进入之后也是通过地址+偏移量找到虚继承对象。 那么虚继承看起来好像更加占用空间了,那么如果虚继承对象的成员很大,不用虚继承会有多份,但是用了虚继承就只有一份。 存偏移量的地方叫做虚继表,对象中储存了虚继表的地址。
继承与组合:
//继承
class A
{
int _a;
};
class B:public A
{
int _b;
};
//组合
class X
{
int _x;
};
class Y
{
X x;
int _y;
};
这两种都是复用。 但是继承是保护能用,组合不能用,所有就有人提出了两个概念。 继承叫做白箱复用,组合叫黑箱复用。 白箱意思就是能看清楚里面的内容,根据某些底层原理去实现对应的功能,黑箱是不注重底层的内容,只要能完成任务就行。 这里还有一个区别,继承的耦合度比组合的高。 我们一般追求的都是低耦合。 继承当中改了父类的内容,子类的内容大概率也会被修改,组合的概率就会小,因为继承跟保护和公有成员有联系,组合只和公有成员有联系。 继承:例如学生和人都有很多共同特征,可以说学生是一个人,这个人是学生。 组合:例如头和眼睛,眼睛应该在头上,但是他们细节没相似之处,不能说头是眼睛。 继承和组合要根据实际情况去使用,如果都差不多就用组合。