掘根宝典之C++多态公有继承:is-a,has-a,like-a,虚函数,静态联编和动态联编

几种常见类设计思路

首先我们得搞清楚几种继承关系

1、is-a,has-a,like-a是什么

在面向对象设计的领域里,有若干种设计思路,主要有如下三种:
is-a、has-a、like-a

2、is-a是什么

is-a,顾名思义,是一个,代表继承关系。
如果A is-a B,那么B就是A的父类

3、has-a是什么

has-a,顾名思义,有一个,代表从属关系
如果A has a B,那么B就是A的组成部分
同一种类的对象,通过它们的属性的不同值来区别。
例如一台PC机的操作系统是Windows,另一台PC机的操作系统是Linux。操作系统是PC机的一个成员变量,根据这一成员变量的不同值,可以区分不同的PC机对象。

4、 like-a是什么

like-a,顾名思义,像一个,代表组合关系
如果A like a B,那么B就是A的接口
新类型有老类型的接口,但还包含其他函数,所以不能说它们完全相同
例如一台手机可以说是一个微型计算机,但是手机的通讯功能显然不是计算机具备的行为,所以手机继承了计算机的特性,同时需要实现通讯功能,而通讯功能需要作为单独接口,而不是计算机的行为。

5、is-a,has-a,like-a如何应用

如果你确定两件对象之间是is-a的关系,那么此时你应该使用继承;比如菱形、圆形和方形都是形状的一种,那么他们都应该从形状类继承。

如果你确定两件对象之间是has-a的关系,那么此时你应该使用聚合比如电脑是由显示器、CPU、硬盘等组成的,那么你应该把显示器、CPU、硬盘这些类聚合成电脑类

如果你确定两个对象之间是like-a的关系,那么此时你应该使用组合;比如空调继承于制冷机,但它同时有加热功能,那么你应该把让空调继承制冷机类,并实现加热接口

多态公有继承

派生类对象使用基类的方法时,而未作任何修改。然而可能会遇到这种情况,

#include
using namespace std;
class AA
{
private:
	int a_;
public:
	AA(int a = 9)//默认构造函数
	{
		a_ = a;
	}
	void print1()
	{
		cout << a_ << endl;
	}
};
class BB :public AA
{
private:
	int b_;

public:
	BB(int b)
	{
		b_ = b;
	}
};
int main()
{
	BB t(3);
	t.print1();
}

结果是9,打印的是基类的

难道我们不能使用print1函数打印t对象的值吗?

即希望同一个方法在派生类和基类的方法是不同的。换句话来说,方法的行为应取决于调用的对象,这被叫做多态。

我们通过在派生类重新定义基类的方法,使用虚方法等来实现多态公有继承。

虚函数

什么是虚函数?特点是什么?

在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,
virtual函数声明格式为:

virtual 函数返回类型 函数名(参数表) {函数体};

虚函数的定义不需要使用关键字virtual;
实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。
虚函数特点
:如果一个基类的成员函数定义为虚函数,那么它在派生类中也保持为虚函数;即使在派生类中省略了virtual关键字,也仍然是虚函数。(世世代代虚函数)


作用

虚函数的作用主要是实现了多态的机制。基类定义虚函数,子类可以重写该函数;在派生类中对积累定义的虚函数进行重写时,需要在派生类中声明该方法为虚方法。

如果要在派生类里重新定义基类的办法,通应该把基类方法声明为虚的。为基类声明一个虚析构函数是必要的。

当子类重新定义了父类的虚函数后,当父类的指针指向子类对象的地址时,[即B b; A a = & b;] 父类指针根据赋给它的不同子类指针,动态的调用子类的该函数,而不是父类的函数(如果不使用virtual方法,请看后面★),且这样的函数调用发生在运行阶段,而不是发生在编译阶段,称为动态联编。而函数的重载可以认为是多态,只不过是静态的。

注意,非虚函数静态联编,效率要比虚函数高,但是不具备动态联编能力。
1.如果使用了virtual关键字,程序将根据引用或指针指向的对象类型来选择方法,

2.如果没有使用关键字virtual,程序使用引用类型或指针类型来选择方法。

动态联编性

class A{
private:
    int i;
public:
    A();
    A(int num) :i(num) {};
    virtual void fun1();
    virtual void fun2();

};

class B : public A
{
private:
    int j;
public:
    B(int num) :j(num){};
    virtual void fun2();// 重写了基类的方法
};

// 为方便解释思想,省略很多代码
A a(1);
B b(2);
A *a1_ptr = &a;
A *a2_ptr = &b;

// 当派生类“重写”了基类的虚方法,调用该方法时
// 程序根据 指针或引用 指向的  “对象的类型”来选择使用哪个方法
a1_ptr->fun2();// call A::fun2();
a2_ptr->fun2();// call B::fun1();
// 否则
// 程序根据“指针或引用的类型”来选择使用哪个方法
a1_ptr->fun1();// call A::fun1();
a2_ptr->fun1();// call A::fun1();


可以让成员函数操作一般化,用基类的指针指向不同的派生类的对象时,基类指针调用其虚成员函数,则会调用其真正指向对象的成员函数,而不是基类中定义的成员函数(只要派生类改写了该成员函数)。

若不是虚函数,则不管基类指针指向的哪个派生类对象,调用时都会调用基类中定义的那个函数

注意事项

重新定义不是重载

我们得明白,在基类重新定义虚函数不会生成函数的两个·重载版本。因此如果重新定义继承的方法,应确保与原来的原型完全相同,即返回类型,函数名,函数参数必须相同

我们看个例子

#include
using namespace std;
class AA
{
private:
	int a_;
public:
	AA(int a)
	{
		a_ = a;
	}
	virtual int prin(int a, int b)
	{
		return a * b;
	}
};
class BB :public AA
{
private:
	int b_;
public:
	BB(int a, int b):AA(a),b_(b){}
	virtual void prin(int a, int b)//这是不行的,原型的返回类型必须与上面一致
	{
		cout << a * b << endl;
	}
};

但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用和指针

#include
using namespace std;
class AA
{
private:
	int a_;
public:
	AA(int a)
	{
		a_ = a;
	}
	virtual AA& prin(int a, int b)
	{
		return a * b;
	}
};
class BB :public AA
{
private:
	int b_;
public:
	BB(int a, int b):AA(a),b_(b){}
	virtual BB& prin(int a, int b)//可以
	{
		cout << a * b << endl;
	}
};

构造函数

构造函数不能是虚函数根据继承的性质,构造函数执行的顺序是:基类的构造函数->派生类的构造函数但是如果基类的构造函数是虚函数,且派生类中也出了构造函数,那么当下应该会只执行派生类的构造函数,不执行基类的构造函数,那么基类的构造函数就不能构造了

析构函数

析构函数应当是虚函数,除非类不用做基函数。

#include
using namespace std;
class AA
{
private:
	int a_;
public:
	AA(int a):a_(a){}
	virtual void h()
	{
		cout << "基类" << endl;
	}
	~AA()
	{
		cout << "基类析构" << endl;
	}
};
class BB :public AA
{
private:
	int b_;
public:
	BB(int a,int b):AA(a),b_(b){}
	virtual void h()
	{
		cout << "派生类" << endl;
	}
	~BB()
	{
		cout << "派生类" << endl;
	}
};
int main()
{
	AA* a = new AA(2);
	AA* b = new BB(2, 3);
	a->h();
	b->h();
	delete b;
}

结果是

基类
派生类
基类析构


如果析构函数不是虚的,就将只调用对应于指针类型的析构函数,这意味着只有AA类的虚构函数被调用,即使指针指向了一个BB类对象

如果我们析构函数是虚的,将调用相应类型的析构函数。因此AA指针指向了BB类对象,将先调用BB类的析构函数,再调用AA类的

#include
using namespace std;
class AA
{
private:
	int a_;
public:
	AA(int a):a_(a){}
	virtual void h()
	{
		cout << "基类" << endl;
	}
	virtual ~AA()
	{
		cout << "基类析构" << endl;
	}
};
class BB :public AA
{
private:
	int b_;
public:
	BB(int a,int b):AA(a),b_(b){}
	virtual void h()
	{
		cout << "派生类" << endl;
	}
	 ~BB()
	{
		cout << "派生类" << endl;
	}
};
int main()
{
	AA* a = new AA(2);
	AA* b = new BB(2, 3);
	a->h();
	b->h();
	delete b;
}

结果是

基类
派生类
派生析构
基类析构

因此虚析构函数可以确保正确的析构函数序列被调用


最后,给类的析构函数定义析构函数没有错,即使这个类不做基类

友元函数

友元函数不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数
为下面三大特性留下伏笔

总结

虚函数是指使用了修饰符virtual修饰过后的函数,而且定义虚函数的函数必须为类的成员函数,虚函数被继承后所继承的派生类都是为虚函数,友员函数不能被定义为虚函数,但是可以被定义为另外一个类的友员,析构函数可以定义为虚函数,但是构造函数却不能定义为虚函数。

有的人还想看纯虚函数?来http://t.csdnimg.cn/bkAUA

静态联编和动态联编

C++静态联编和动态联编是C++中实现多态性的两种方式。

静态联编(Static Binding)是在编译时确定函数的调用关系(要找到调用的函数)。静态联编主要通过函数重载和模板来实现。在静态联编中,编译器根据函数调用时提供的参数类型和个数,在编译期间就确定了调用哪个函数。静态联编的优点是效率高,因为在运行时不需要进行函数查找。但是静态联编的灵活性较低,无法实现运行时动态绑定。

动态联编(Dynamic Binding)是在运行时确定函数的调用关系(找到要调用的那个函数)。动态联编主要通过虚函数和虚函数表来实现。在动态联编中,基类中声明的虚函数可以在派生类中被重写,然后通过基类指针或引用调用函数时,根据实际的对象类型确定调用哪个函数。动态联编的好处是具有更高的灵活性,可以实现运行时动态绑定,但效率相对较低,因为在运行时需要进行函数查找和解析虚函数表。

可以使用virtual关键字来声明一个虚函数,使其在派生类中可以重写。例如:

class Base {
public:
    virtual void foo() {
        // do something
    }
};

class Derived : public Base {
public:
    void foo() override {
        // do something different
    }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->foo(); // 动态绑定,调用Derived中重写的foo函数
    delete basePtr;
    return 0;
}

在上面的例子中,通过基类指针basePtr调用foo函数时,根据实际的对象类型Derived,确定调用Derived中重写的foo函数,这就是动态联编的效果。

总结来说,静态联编在编译时确定函数的调用关系,效率高但灵活性低;动态联编在运行时确定函数的调用关系,灵活性高但效率相对较低。根据实际需要,可以选择使用静态联编或动态联编来实现多态性。

你可能感兴趣的:(c++,c++,开发语言)