首先我们得搞清楚几种继承关系
在面向对象设计的领域里,有若干种设计思路
,主要有如下三种:
is-a、has-a、like-a
is-a,顾名思义,是一个,代表继承
关系。如果A is-a B,那么B就是A的父类
。
has-a,顾名思义,有一个,代表从属关系
。如果A has a B,那么B就是A的组成部分
。
同一种类的对象,通过它们的属性的不同值来区别。
例如一台PC机的操作系统是Windows,另一台PC机的操作系统是Linux。操作系统是PC机的一个成员变量,根据这一成员变量的不同值,可以区分不同的PC机对象。
like-a,顾名思义,像一个,代表组合关系
。如果A like a B,那么B就是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函数,这就是动态联编的效果。
总结来说,静态联编在编译时确定函数的调用关系,效率高但灵活性低;动态联编在运行时确定函数的调用关系,灵活性高但效率相对较低。根据实际需要,可以选择使用静态联编或动态联编来实现多态性。