C++的重载与虚函数
其实真正要说的是虚函数,不过其中要扯倒重载,所以顺便也说了下重载
1. 重载
1.1 简单重载
在C++中,是允许同名函数的存在
int add(int i,int j);
float add(float i,float); 而在c中,函数名是唯一的,所以为了区分int和float版本的add,你需要给它们起不同的名字,比如将int的命名为add_int,将float的命名为add_float,这样做的坏处就是程序员要记住很多的函数名,虽然这些函数的功能是一样的,而且也不直观。
重载函数的存在,使得这种情况不在存在,它可以根据参数的类型,自动调用合适的重载函数,程序员只要记住要使用加法是调用add函数就可以,不同再像C中那样猜测int版的add函数函数名是怎样的,float版的函数名又是怎样的。实际上,这两个版本的add函数名在编译后是不一样的,编译器自动为它们进行了修饰,比如int的修饰成add_int_int,float的修饰成add_float_float,不过这都只是编译生成后的结果,对程序员来说,他只需知道add这个函数,但他要调用的时候,比如使用了int参数,编译器根据参数推出该调用的版本,这里就是add_int,所有这一切程序员都是看不见的,也不需要关心,从而减轻了程序员的工作。
从上面的说法也可以看出,函数重载也不是能乱重载的,重载的要求是:
1. 函数的参数类型不一样,像上面的int和float的
2. 函数的参数个数不一样
这是因为编译器在修饰生成的函数名时,一般用所有的参数类型来进行修饰,比如void add(int,float) 修饰成add_int_float,void add(int,int,int) 修饰成add_int_int_int,这样符合上面2条要求的重载函数最后生成的函数名是不一样的。
可能有人会认为,为什么不用返回值来区分,如果编译器能推测出函数调用该返回什么值那自然没什么问题,但很多时候,往往只是调用函数,使用函数的副作用,而不要求返回值,这个时候编译器就推测不出了,比如
void f();
int f();
int main()
{
f();
}
这个时候编译器怎么知道调用哪个函数
1.2 类中的重载函数
不仅仅是全局函数可以重载,类中的函数也可以重载
class Base
{
public:
int f() const{
cout<<"Base:f()"<<endl;
return 1;
}
int f(string) const{
return 1;
}
};
这看起来跟全局的没什么区别,但是当涉及到继承的时候,事情就变得麻烦起来
class Derived1: public Base
{
public:
//Redefinition
int f() const{
cout<<"Derived1:f()"<<endl;
return 1;
}
};
class Derived2: public Base
{
public:
//change Return type
void f()const
{
cout<<"Derived2:f()"<<endl;
}
}
class Derived3: public Base
{
public:
//change argument list
int f(int) const
{
cout<<"Dervide3:f()"<<endl;
return 1;
}
}
子类中定义了跟父类同名的函数,这个时候该如何办?其实说起来也很简单,只要子类定义了跟父类同名的函数,不管是重写了函数内容(Dervied1),改变了返回类型(Derived2),还是改变了参数列表(Derived3),结果都一样,子类中的同名函数将父类中的同名函数给隐藏了,只要子类中的函数是可见的,通过子类的对象调用父类的同名函数是不合法的,只能调用子类自身的同名函数。这就是所谓的名字隐藏。
2. 重写与虚函数
2.1 基本知识
虚函数在多态中经常用到。你只要有一个基类的指针或引用,编译器会为你调用该指针真正对应的函数
class Base
{
public;
virtual void f()
{
cout<<"Base:f()"<<endl;
}
};
class Derived:public Base
{
public:
//You can also ingore virtual here
virtual void f()
{
cout<<"Derived:f()"<<endl;
}
};
int main()
{
Base* p=new Derived();
p->f();
delete p;
}
程序输出Derived::f(),这就是虚函数的作用。你可以不用关心基类指针到底指向那个子类,编译器会为你调用正确的函数。
这是因为编译器使用了晚捆绑的缘故。
当一个类中有一个虚函数时(可以是因为在类中声明了一个虚函数,也可以是因为基类中有虚函数,通过继承得到)。编译器就为这个类创造一个虚表(VTABLE),它当中的虚函数位置是固定的,即使被继承到子类中也一样。当定义了一个这个类的对象时,编译器会在这个对象中放入一个虚指针(VPTR)指向这个表。当调用虚函数时,编译器在汇编代码中插入
一段代码,这个代码首先找到虚表,然后在通过偏移调用正确的函数。
当一个带有虚函数的基类被继承时,这个VTABLE会被完整赋值,当然对应的函数地址会改成子类中的函数地址。如果子类另外声明了虚函数,就会在原来的虚函数后面添加上新的条目。
重写其实就是在子类中对父类的虚函数进行重定义,因为一般子类有自己的特性。
2.2 虚函数与重载
如果子类中只是改写了父类中虚函数的内容,这就只是重写(overriding),函数前面的virtual可以忽略掉
但如果子类中改变了父类中虚函数的参数类型或个数,那么父类中的同名函数就会被隐藏掉,这同普通的重载一样,有一点不一样的是,不可以通过改变返回类型来隐藏父类中的同名函数。
2.3 切片
当用子类对象来初始化父类时(如函数中的call by value),新生成的父类对象会正确初始化它自身的vtable,而不会使用子类的vtable。
注:
虽然通过基类指针调用虚函数,最后调用的是子类的函数,但是如果使用的确实基类的默认参数
class Base
{
public:
virtual void f(int i=0)
{
cout<<i<<endl;
}
};
class Derived: public Base
{
public:
virtual void f(int i=1)
{
cout<<i<<endl;
}
};
int main()
{
Base* p=new Derived();
p->f(); //输出的是0
}
注2:
发生在private继承时的问题,父类中的虚函数是private的,当它被private继承时,子类是无法访问到这个函数的,不过子类仍然可以override这个函数
class Base
{
public:
void nvi()
{
vfun();
}
private:
virtual void vfun()
{
cout<<"Base::vfun()"<<endl;
}
};
class Derived1:private Base
{
public:
void df()
{
nvi(); //调用base的nvi,由于这里没有override vfun,所以输出的是Base:vfun()
}
//事实上,这里不能直接调用base中的vfun,因为它是private继承来的
};
class Derived2:private Base
{
public:
void df()
{
nvi();//调用了Dervied2的vfun
}
private:
void vfun();//要想override,必须重新声明
};
void Derived2::vfun()
{
cout<<"Derived2::vfun()"<<endl;
}