一、继承关系
C++的三大特性:封装、继承、多态,封装是利用类、命名空间以及成员访问限定(指成员变量或成员函数属于公有成员?私有成员?还是保护成员?)来实现封装的。
数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
所说类实现复用实质是说C++的继承特性,一个类通过公有继承、私有继承、保护继承来使用另一个基类的数据或方法,减少数据冗余,实现复用。
继承类对于基类的访问关系的变化规则:
- 基类的私有成员在继承类是不可见的。
- 继承方式和基类成员访问限定符中取对于派生类限制更强的那个属性作为派生类的访问限定属性。public限制最弱,private限制最强。
- 不想被基类对象直接访问,但需要在继承类中能访问,就定义为保护成员,保护成员限定符因继承而生。
- 一般继承方式都会取public继承,保持继承原则,特殊情况采取private/protected继承,保持组合原则。
- 用class定义的类,默认继承方式是private;用struct定义的类,默认继承方式是public。
二、类继承和对象组合
- 所谓组合是指新类由现有类的对象合并而成,称这种构造类的方式为组合;
- 所谓继承是指通过扩展已有类来获得新功能的代码重用方法。
继承
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见。 继承一定程度破坏了基类的封装,基类的改变,对派生类类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
class Person
{
public:
Person(const char*name)
:_name(name)
{
}
protected:
string _name;
};
class Student:public Person
{
int _num;
public:
Student(const char*name,int num)
:Person(name)
, _num(num)
{
}
void Show()
{
cout << "Name:" << _name << endl << "Num:" << _num << endl;
}
};
组合
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
//发动机类
class Motor{
public:
Motor(const int displacement=5000)
:_displacement(displacement)
{
cout << "Motor construct" << endl;
}
protected:
int _displacement;
};
//轮子类
class Wheel{
public:
Wheel(const int&num)
:_num(num)
{
cout << "Wheel construct" << endl;
}
protected:
int _num;
};
//汽车类
class Car :private Motor, protected Wheel
{
public:
Car(const int& displacement,const int& num,const int& val)
:Motor(displacement)
, Wheel(num)
, _val(val)
{
cout << "Car construct" << endl;
}
void Show()
{
cout << "发动机排放量:" << _displacement << "\n轮胎数:" << _num << "\n价值" << _val << "万" << endl;
}
private:
int _val;
};
实际尽量多去用组合。组合的耦合度低,代码维护性好。 不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
三、切片行为
父类对象/指针/引用=子类对象/指针/引用
- 子类对象可以赋值给父类对象(切片),反之不允许(如果把父类对象赋值给子类,子类对象不完整) 。子类赋值给父类,自动完成切片行为,所谓切片行为就是指,子类只将父类的那一部分赋值给父类。
class Person{
protected:
string _name;
};
class Student:public Person
{
int _num;
};
int main()
{
Person p;
Student s;
p = s; //子类对象赋值給父类
return 0;
}
- 父类对象的指针/引用可以指向子类对象, 父类指针可以指向子类,自动完成切片,只指向子类中父类成员的那一部分。
Student s;
Person* pp = &s; //父类指针指向子类
Person& bp = s; //父类引用指向子类
- 如果想要子类指针/引用指向父类,则需要强制类型转换。子类指针指向的空间比父类指向空间大,调用这个sp或者x指针操作子类独有的
_num
成员就越界了。
Person p;
Student* sp = (Student*)&p; //子类指针指向父类,需要强转才可以,不安全的强转
Student& x = (Student&)p; //子类引用指向父类,需要强转才可以,不安全的强转
四、继承默认成员函数
- 继承构造/继承拷贝构造/继承赋值重载
子类独有的成员需要子类去构造/拷贝构造/赋值函数,子类继承的父类成员,可以调用父类的构造/拷贝构造/赋值函数。 例如上述3和5部分代码。
当然这些默认成员函数可以不写,系统会自动生成这些函数, 例如上述4的代码是没有问题的。 - 析构函数不需要显示调用,如果显示调用不符合栈帧规则。
栈帧规则:先创建者,后销毁,C++继承导致显示调用析构不符合栈帧。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<vector>
using namespace std;
class father
{
public:
father()
{
cout << "father construct" << endl;
}
~father()
{
cout << "father Deconstruct " << endl;
}
};
class son :public father
{
public:
son()
{
cout << "son construct" << endl;
}
~son()
{
cout << "son Deconstruct" << endl;
}
};
void Func()
{
father f;
son s;
}
int main()
{
Func();
system("pause");
}
father construct
father construct
son construct
son Deconstruct
father Deconstruct
father Deconstruct
请按任意键继续. . .
我们可以看到子类构造先调用父类的构造函数去构造父类部分,再调用子类的构造函数构造子类部分。析构先析构子类部分,再析构父类部分。
五、重载、重写、重定义
协变:父子类重写的返回值分别对应各自的指针/引用
重载
为什么C语言不能实现重载?C++可以实现重载?
因为C语言和C++的链接方式有所不同,在C语言中,函数仅仅通过函数名区分不同,所以函数名相同当然会出现问题,比如,说两个人都叫小明,那点名时候,编译器无法判断,是要叫哪个小明。
而在C++中是通过函数名和参数列表区别不同函数,就可以实现重载了。
double add(int a);
int add(int);
上述是两个一模一样的函数,并不构成重载。函数名都是add,参数都仅有一个int类型参数,而函数返回值并不是区分不同函数的要素。
double add(int,double);
int add(double,int);
上述函数构成重载,参数列表不同。
函数重载的作用
减少了函数名的数量,避免出现重定义,避免了命名污染;
提高程序可读性。
在编译期间根据参数的不同就能决定调用哪一个函数,叫做静态重载。
重定义(隐藏)
1、派生类函数名和基类(非虚)函数名相同,这种情况就叫做重定义(隐藏)。
2、子类的同名成员隐藏了父类的同名成员,这种情况也叫重定义。子类要想调用父类的这个同名成员需要显示指明父类。
#include<iostream>
using namespace std;
class Father{
public:
Father(const int& f)
:_f(f)
{}
void Show()
{
cout << _f << endl;
}
protected:
int _f;
};
class Son:public Father
{
public:
Son(const int& f,const int& s)
:Father(f)
, _s(s)
{}
void Show()
{
cout << _s << endl;
}
private:
int _s;
};
int main()
{
Son s(1000,500);
s.Show();
s.Father::Show();
system("pause");
}
程序结果:
500
1000
请按任意键继续. . .
重写(覆盖)
概念:派生类对基类同名虚函数的改写就叫函数重写。
- 函数重写发生在子类和父类之间
- 父类中含有虚函数(virtual修饰的函数)
- 子类中含有同父类虚函数相同原型的函数
- 只有在运行期间才能决定调用哪一个函数,叫做动态重载
#include<iostream>
using namespace std;
class Father {
public:
Father(const int& f)
:_f(f)
{}
virtual void Show()
{
cout <<"Father:"<< _f << endl;
}
protected:
int _f;
};
class Son :public Father
{
public:
Son(const int& f, const int& s)
:Father(f)
, _s(s)
{}
void Show()
{
cout <<"Son:"<< _f << "\t" << _s << endl;
}
private:
int _s;
};
void func(Father& fl)
{
fl.Show();
}
int main()
{
Father f(800);
Son s(1000, 500);
func(f);
func(s);
}
赋值兼容规则
基类的指针,引用可以指向派生类的对象;
派生类的对象可以赋值给基类,基类的对象不可以赋值给派生类;
基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。
Father:800
Son:1000 500