- 面向过程:代码和数据分离、分析解决问题的步骤再一步步实现
- 优点:性能高,能快速开发
- 缺点:难扩展,难维护,耦合度高
- 面向对象:找出问题中的对象,实现对象间的交互,每个对象只管自己的事,能继承重复的行为和属性
- 优点:易维护,易复用,易拓展,耦合度低
- 缺点:性能比面向对象低
- 虚函数:基类希望它的派生类自定义适合自身的版本的函数
- 动态绑定:函数形参使用基类的指针或引用,函数的实际执行版本由传入的实参确定,即运行时绑定
面向对象三大特征:
- 封装:将数据和方法集合在一个单元(类)中,故类可以成为一个功能独立的模块
- 继承:在基类的基础上创建派生类,派生类继承基类的功能,并能扩充修改,实现重用代码
- 多态:静态多态指函数重载,动态多态指传入基类指针或引用,根据实际所指向的对象执行相应函数,即通过一个接口实现多个方法。
在C++语言中,当我们使用基类引用或指针调用一个虚函数时,将发生动态绑定
NOTE: 基类通常定义一个虚析构函数,即使该函数不执行任何实际操作
因为如果基类的析构函数不是虚函数。则delete一个指向派生类的基类指针将产生未定义的行为!
class B{};//B是基类,会合成默认析构,但不是虚析构函数
class C : public B {}// C继承自B
C *c = new C;
B *t = c;//基类指针指向派生类
delete t;//t的动态类型是派生类,静态类型是基类指针,这里会产生未定义行为
派生类的析构函数只负责销毁派生类自己分配的成员,但在派生类的移动和拷贝操作中还要移动和拷贝基类部分的成员,可以像委托构造一样在函数圆括弧后面使用冒号,然后就调用基类的移动拷贝操作。
任何构造函数之外的非静态函数都可以是虚函数!(声明为虚函数无非是想使用基类的指针或引用来调用派生类的自定义操作,即该对象已经存在了没有构造的必要)
virtual只能用于类内声明无非用于类外部定义
class BasicTank{
public:
int x;
int y;
string name;
BasicTank(const char s[] = "sb") : name(s) {}
virtual ~BasicTank() = default;
virtual void move() {//基类的虚函数实现move为x,y每次加1
cout << "origin: " << getPosStr() << endl;
++x;
++y;
cout << "curr: " << getPosStr() << endl;
};
string getPosStr(){
char temp[100];
sprintf(temp,"x: %d, y: %d", x, y);
return temp;
}
};
class FlyTank : public BasicTank{//类派生列表中,基类前面的public表明从基类继承来的成员对派生类用户可见
public:
FlyTank(const char s[] = "sb") :BasicTank(s) { }//使用基类构造函数来辅助构造派生类
//一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型和返回类型应该与被它覆盖的基类函数一致(但当虚函数返回类型是类本身的引用或指针时,可以各返回各的,如D继承自B,B的虚函数返回B*,而对应的D的虚函数可以返回D*。
void move() override{// 重写后为每次x+2,y+3
cout << "origin: " << getPosStr() << endl;
x+=2;
y+=3;
cout << "curr: " << getPosStr() << endl;
}
};
void getNextStep(BasicTank &tank){
cout << tank.name << endl;
tank.move();//这里将根据tank的实参类型动态选择move函数
}
BasicTank a("hacker");
FlyTank b("monster");
getNextStep(a);//执行基类的move
getNextStep(b);//执行派生类的move
上面getNextStep函数传参时之所以存在派生类向基类类型的转换是因为每个派生类对象都包含一个基类部分,但不存在从基类到派生类的隐式类型转换!因此下面代码是不合法的:
BasicTank *t = &b;
FlyTank *pt = t;//非法,即使t绑在一个派生类的对象上,但t仍然是一个基类指针
但如果我们已知这个转换时安全的,那么可以用static_cast强制转换,跳过编译器检查
FlyTank *pt = static_cast<FlyTank*>(t);
override和final
- override:表示该函数是要重写基类相应的虚函数,因为派生类如果定义了一个函数,该函数与基类对应的虚函数名字相同但形参列表不同,则该函数与基类虚函数相互独立,是合法行为,编译器不会报错,而且因为与编程习惯不同,容易出错却不易排查,应该当我们要重写虚函数的时候,加上override修饰,编译器就会帮我检查这次重写行为是否正确。
- final:使用该说明符声明函数,表明后续不允许再覆盖该函数(使用final的时候也像override一样检查当前是否为合法的虚函数)
class FlyTank : public BasicTank{
public:
/* ... */
void move(int a) override {}//非法,参数对不上
void attack() override {}//非法,基类中不存在该函数
string getPosStr() {}//非法,该函数不是虚函数
/* ... */
};
class C: public BasicTank{
public:
/* ... */
void move() final {}//final能起到override的检查虚函数合法性的作用,同时如果继承C的类若要覆盖move则会出错
}
class D : public C{
public:
void move() {};//非法,final后仍试图重写
void move() override {};//非法,final后仍试图重写
}
回避虚函数机制
如果我想要在派生类重写虚函数时想在已有基类的虚函数上,多增加一点东西,那么可以使用作用域运算符在派生类的重写函数类调用基类的。(在你不确定基类指针或引用实际指向什么的时候也可以使用作用域运算符强制调用对应的虚函数)
class C: public BasicTank{
public:
/* ... */
void move() override {
this->BasicTank::move();//不管指针指向的对象是哪个派生类,直接调用基类的,这里要省去BasicTank::会出现无限递归
cout << "a new version" << endl;
}
}
抽象基类
- 纯虚函数:当前这个类不实现该虚函数,仅声明,由继承它的派生类去实现。
- 抽象基类:含有或未经覆盖直接继承纯虚函数的类是抽象基类
我们无法创建抽象基类的对象,但还是要有相应构造函数,因为尽管我们不能直接创建抽象基类的对象,但抽象基类的派生类构造函数还是会使用抽象基类的构造函数来构建对应的抽象基类成员。
访问控制与继承
派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。派生类和友元能访问基类的公有成员(public),受保护成员(protected),而不能访问基类的私有成员。
- 公有继承:派生类成员将遵循其基类原有的访问说明符
class BaseClass{
private:
int privateMem;
protected:
int protectedMem;
public:
int publicMem;
};
class AClass : public BaseClass{// 公有继承
void usePrivate() { cout << privateMem << endl; } //非法!privateMem是基类私有成员,无法访问
void useProtected() { cout << protectedMem << endl; }//protectedMem公有继承后仍是protected,友元和成员函数可以访问
void usePublic() { cout << publicMem << endl; }//publicMem公有继承后仍是public
};
- 受保护继承:基类中的公有成员和受保护成员在派生类中都变成了受保护成员,原来私有的现在还是私有
class DClass : protected BaseClass{//受保护继承
};
DClass d;
cout << d.publicMem << endl;//非法!继承过来后,本来在基类中的公有成员也变成受保护了,非友元和成员函数不可访问
- 私有继承:从基类中继承来的所有成员皆私有
在最顶上的代码中,FlyTank是公有继承的,所以getNextStep的参数存在派生类到基类的转换,但如果FlyTank是受保护继承,非成员函数和友元就无法实现该转换。私有继承更无法使用了
class EClass;
class BaseClass{
private:
int privateMem;
protected:
int protectedMem;
public:
int publicMem;
friend void func02(EClass &a);
};
class EClass : public BaseClass{
int t = 5;
};
void func02(EClass &a){
cout << a.t << endl;//非法,t是EClass的私有成员,而func02并非ECLass友元
cout << a.publicMem << endl;//正确,普通函数可以访问ECLass公有继承来的公有变量
cout << a.protectedMem << endl;//正确,func02是基类的友元,protectedMem属于基类的受保护成员,可以被基类友元访问
cout << a.privateMem << endl;正确,privateMem 是基类私有成员,同上
}
默认的继承保护级别
在不显示指定派生类的继承方式时
- struct关键字定义的派生类是公有继承
- class关键字定义的派生类是私有继承
struct FClass : BaseClass{// 相当于struct FClass : public BaseClass
};
class GClass : BaseClass{// 相当于struct FClass : private BaseClass
};
class和struct的唯二差别:默认成员访问说明符,默认派生访问说明符
虚表和虚指针
C++动态绑定或者说多态的实现依赖虚表。
当一个类定义了虚函数,那么该类就会有一个虚表,虚表记录的是虚函数的入口地址
当派生类继承了有虚表的基类或者自身定义了虚函数也会有虚表,当重写了基类的虚函数后,虚表中对应虚函数的地址就与基类不同了。
一个对象所属的类存在虚表,那么该对象也就会有一个指向虚表的指针,当使用基类指针或引用调用虚函数的时候,就会在实际虚表中找出自己那个虚函数。