1,继承的概念及定义
1.1,继承的概念
继承机制是面向对象程序设计使代码可以复用的重要手段,它允许我们在原有类的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的是函数层次的复用,这里继承是类层次的复用。
下面我们看看两个类,学生类student和老师类teacher,分别放着学生和老师的一些属性和方法。
class student
{
public:
void identity()
{
//...
}
void study()
{
cout << " void study()" << endl;
}
protected:
string _name = "张三";
string adderss; //地址
string _tel; //电话
int _age = 18; //年龄
int _id; //学号
};
class teacher
{
public:
void identity()
{
//...
}
void teaching()
{
cout << "teaching" << endl;
}
protected:
string _name = "张三";
string adderss; //地址
string _tel; //电话
int _age = 18; //年龄
string _title; //职称
};
从上面的代码可以看出,Student和Teacher都有姓名/地址/ 电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的。 当然他们 也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣ 的独有成员函数是学习,⽼师的独有成员函数是授课。
下⾯我们公共的成员都放到Person类中,Student和teacher都继承Person,就可以复⽤这些成员,就 不需要重复定义了,省去了很多⿇烦
class person
{
public:
void identity()
{
//...
}
protected:
string _name = "张三";
string adderss; //地址
string _tel; //电话
int _age = 18; //年龄};
class student :public person
{
public:
void identity()
{
//...
}
void study()
{
cout << " void study()" << endl;
}
protected:
int _id; //学号
};class teacher: public person
{
public:
void teaching()
{
cout << "teaching" << endl;
}
protected:string _title; //职称
};
1.2,继承定义
1.2.1,定义格式
1.2.2,继承基类的成员,派生类访问方式的变化
1. 基类private成员在派⽣类中无论以什么⽅式继承都是不可⻅的。这⾥的不可见是指基类的私有成员 还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类里面还是类外面都不能去访 问它。
2,基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类 中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3,实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可见。基类的其他成员 在派⽣类的访问⽅式==Min(成员在基类的访问限定符,继承方式),public >protected> private。
4. 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显示的写出继承⽅式。
5. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤ protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类里面使用,
2,基类和派生类间的转化
1, public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切 割。寓意把派⽣类中基类那部分切出来,基类指针或引用指向的是派⽣类中切出来的基类那部分。
2,基类对象不能赋值给派⽣类对象。
3,基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。
class Person
{
protected:
string _name;
string _sex;
int _age;
};
class Student: public Person
{
public:
int _No;
};
int main()
{
Student sobj;
//子类对象可以赋值给父类对象,父类指针,父类引用
Person pobj = sobj;
Person* p1 = &sobj;
Person& p2 = sobj;
Student* p3 = (Student*)p1;//父类指针可以通过强制类型转换赋值给派生类的指针
return 0;
}
3,继承中的作用域
3.1,隐藏规则
1,在继承体系中,基类和派生类都有独立的作用域。
2,派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫做隐藏,(在派生类的成员函数中,可以使用基类:基类成员 显示访问)
3,需要注意的是,只要基类和派生类的成员函数名相同,就构成隐藏。
class person
{
protected:
int _num=100;
string _name = "张三";
};
class student:public person
{
public:
void print()
{
cout << _name << endl;
//与person中的_num构成隐藏,这里访问就是派生类的_num
cout << _num << endl;
//要想拿到基类的_num,指定类域即可
cout << person::_num << endl;
}
protected:
int _num = 200;
};
int main()
{
student s;
s.print();
return 0;
}
4,派生类的默认成员函数
1,派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认的构造函数,则必须在派生类的初始化列表阶段显示调用。
2,派生类的拷贝构造函数必须调用基类的拷贝构造完成基类部分的拷贝初始化。
3,派生类的operator=必须调用基类的operator=完成基类的赋值,需要注意的是,基类的operator=和派生类的operator=会构成隐藏,所以在显示调用基类的operator=时3,需要指明基类的作用域。
4, 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派 ⽣类对象先清理派⽣类成员再清理基类成员的顺序。
5.,派⽣类对象初始化先调⽤基类构造再调派⽣类构造。6.,派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。
大致可以总结如下:
4.1,实现一个不能被继承的类
C++11新增了一个关键字final,final修改基类,派生类就不能继承了。
5,继承与友元
友元关系不能被继承,也就是说基类友元不能访问派生类的私有和保护成员。
//示例:
//声明
class student;class person
{
public:
friend void display(const person& p, const student& s);
protected:
string _name;
};
class student :public person
{
protected:
int _num;
};void display(const person& p, const student& s)
{
cout << p._name << endl;
cout << s._num << endl;
}int main()
{
person p;
student s;
display(p, s);
return 0;
}
6,基类与静态成员
基类中定义了static静态成员变量,则整个继承体系中只有一个这样的成员。无论派生出多少派生类,都只有一个static成员实例。
7,多继承以及菱形继承
单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承。
多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型 是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。
菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以 看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就 ⼀定会有菱形继承。
下面代码,对象a中包含了person两份信息
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 a;
//编译报错,对_name的访问不明确
//a._name = "张三";
cout << sizeof(a) << endl;
//需要显示指定哪个基类的成员才能解决二义性的问题,但是数据冗余问题无法解决
a.student::_name = "张三";
a.teacher::_name = "李四";
return 0;
}
通过监视窗口可以看出,对象a中包含了两份person的信息。
运行结果:
解决方法:虚拟继承
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 a;
a._name = "张三";
cout << sizeof(a) << endl;
cout << a._name << endl;
return 0;
}
通过监视窗口可以看出,对象a中虽然包含了三份person,但他们都是同一个。
所以虚拟继承可以解决菱形继承的数据冗余和二义性的问题。
8,继承和组合
1,public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。
2,组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
示例:
//轮胎和车更符合has-a的关系
class Tire
{
protected:
string _brand = "Michelin";//品牌
size_t _size = 17;//尺寸
};
class Car
{
protected:
string _car = "白色";
string _num; //车牌号
Tire _t1; //轮胎
Tire _t2; //轮胎
Tire _t3; //轮胎
Tire _t4; //轮胎
};
//BMW和Benz跟符合is-a的关系
class BMW :public Car
{
public:
void Drive()
{
//...
}
};
class Benz :public Car
{
public:
void Drive()
{
//...
}
};
总结:优先使⽤组合,而不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太 那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的 关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。