C++ 中重写重载和隐藏的区别

重写(override)、重载(overload)和隐藏(overwrite)在C++中是3个完全不同的概念。我们这里对其进行详细的说明

1、重写(override)是指派生类覆盖了基类的虚函数,这里的覆盖必须满足有相同的函数签名和返回类型,也就是说有相同的函数名、形参列表以及返回类型。

2、重载(overload)是指C++允许在同一作用域中声明几个功能类似的同名函数,这些函数的函数名相同,但是函数签名不同,也就是说有不同的形参。

3、隐藏(overwrite)是指基类成员函数,无论它是否为虚函数,当派生类出现同名函数时,如果派生类函数签名不同于基类函数,则基类函数会被隐藏。如果派生类函数签名与基类函数相同,则需要确定基类函数是否为虚函数,如果是虚函数,则这里的概念就是重写;否则基类函数也会被隐藏。另外,如果还想使用基类函数,可以使用using关键字将其引入派生类。

一、重写

函数重写的基本原则:在基类中,通过使用关键字 virtual 来声明一个虚函数,派生类可以通过重新定义基类中的虚函数来实现函数重写。

例如:

cpp 复制代码
class Father {
public:
     virtual void func() {
        cout << "Father" << endl;
    }
};

class Child: public Father {
public:
    void func() {
        cout << "Child" << endl;
    }
};

int main() {
    Father f;
    Child c;
    f.func();
    c.func();
}

以上代码实现了子类重写父类中的函数。所以父类子类调用同一个函数会有不同的实现。仿真如下:

1.1、重写引发的问题

重写虚函数很容易出现错误,原因是C++语法对重写的要求很高,稍不注意就会无法重写基类虚函数。且这些错误不易被发现,编译器可能也不会提示:

cpp 复制代码
class Base {
public:
    virtual void some_func() {}
    virtual void foo(int x) {}
    virtual void bar() const {}
    void baz() {}
};

class Derived : public Base {
public:
    virtual void sone_func() {}
    virtual void foo(int &x) {}
    virtual void bar() {}
    virtual void baz() {}
};

Derived的4个函数都没有触发重写操作。第一个派生类虚函数sone_func的函数名与基类虚函数some_func不同,所以它不是重写。第二个派生类虚函数foo(int &x)的形参列表与基类虚函数foo(int x)不同,所以同样不是重写。第三个派生类虚函数bar()相对于基类虚函数少了常量属性,所以不是重写。最后的基类成员函数baz根本不是虚函数,所以派生类的baz函数也不是重写。

1.2、使用override说明符

重写容易出错,尤其继承关系非常复杂的时候。所以C++11标准提供了一个非常实用的override说明符,明确告诉编译器这个虚函数需要覆盖基类的虚函数,一旦编译器发现该虚函数不符合重写规则,就会给出错误提示。

cpp 复制代码
class Derived : public Base {
public:
    virtual void sone_func() override {}
    virtual void foo(int &x) override {}
    virtual void bar() override {}
    virtual void baz() override {}
};

如果没有override说明符,则修改基类虚函数将面临很大的风险,因为编译器不会给出错误提示,我们只能靠测试来检查问题所在。

1.3、使用final说明符

可以为基类声明纯虚函数来迫使派生类继承并且重写这个纯虚函数。但是一直以来,C++标准并没有提供一种方法来阻止派生类去继承基类的虚函数。C++11标准引入final说明符解决了上述问题,它告诉编译器该虚函数不能被派生类重写。final说明符也需要声明在虚函数的尾部。

cpp 复制代码
class Father {
public:
     virtual void func() final {
        cout << "Father" << endl;
    }
};

class Child: public Father {
public:
	// 报错,不能重写final修饰的函数
    void func() {
        cout << "Child" << endl;
    }
};

最后要说明的是,final说明符不仅能声明虚函数,还可以声明类。如果在类定义的时候声明了final,那么这个类将不能作为基类被其他类继承

cpp 复制代码
class Base final {
public:
    virtual void foo(int x) {}
};

// 报错,不能继承final修饰的类
class Derived : public Base {
public:
    void foo(int x) {};
};

1.4、override和final的特别之处

在C++11标准中,override和final并没有被作为保留的关键字,其中override只有在虚函数尾部才有意义,而final只有在虚函数尾部以及类声明的时候才有意义,因此以下代码仍然可以编译通过:

cpp 复制代码
void override() {}
void final() {}

二、重载

函数重载的条件:

1、参数个数不同

2、参数类型不同

3、参数顺序不同

cpp 复制代码
void fun(int i) {
	cout << "打印整数: " << i << endl;
}
void fun(int i, int j) {
	cout << "打印两个整数: " << i << " 和 " << j << endl;
}
void fun(float f) {
	cout << "打印浮点数: " << f <<endl;
}

fun(4);				// 调用第一个 fun 函数  
fun(2, 3);			// 调用第二个 fun 函数
fun(1.5f);          // 调用第三个 fun 函数  

**注意:**返回值不同不是函数重载的判断标准

cpp 复制代码
void fun(int i) {
	cout << "打印整数: " << i << endl;
}

int fun(int i) {
	cout << "打印整数: " << i << endl;
	return 0;
}

fun(4);             // 报错,并不知道调用哪个函数

2.1、函数重载的底层原理

为什么C++支持函数重载而C不支持?C语言中同名函数编译完还是同名的,两个重名函数的地址都是有效值,所以在重定位的时候就会产生冲突和歧义。而C++会对函数名进行修饰,例如:

cpp 复制代码
void f(int a, double b) { printf("%d %lld", a, b) }

函数名 f 会被修正为 _Z1fid,Linux下的命名规则为

函数名被修饰为:_Z + 函数名长度 + 函数名 + 各个形参类型首字母的小写

这也就解释了为什么函数重载和返回值无关,为什么和参数个数,类型,顺序不同就可以重载,因为他们修饰完后的函数名就是不同的。

三、隐藏

隐藏指在某些情况下,派生类 中的函数屏蔽了基类中的同名函数:

1、两个函数参数相同,但是基类不是虚函数。和重写的区别在于基类函数是否是虚函数

2、两个函数参数列表不同,无论基类函数是否虚函数,基类函数都将被覆盖。和重载的区别在于两个函数不在同一个类中

下面举例说明:

cpp 复制代码
class Base {
public:
	void funA(){cout<<"funA()"<<endl;}
	virtual void funB(){cout<<"funB()"<<endl;} 
};

class Heri:public Base {
public:
    // 隐藏,基类中同名函数不是虚函数
	void funA(){cout<<"funA():Heri"<<endl;}
    // 隐藏,参数列表不同,无论基类是否是虚函数,基类函数都将被覆盖
	void funA(int a){cout<<"funA(int a):heri"<<a<<endl;}
    // 重写,基类是虚函数
	void funB(){cout<<"funB():heri"<<endl;}
};

隐藏使用的时候记住一句,派生类的指针或引用,对象调用子类和父类同名的函数,父类的同名函数被子类隐藏,调用的是子类的函数

看下面代码:

cpp 复制代码
class Base {
public:
    void fun1() { cout<<"base:fun1()"<<endl; fun(); }
    virtual void fun() { cout<<"base:fun()"<<endl; }
};

class Deriverd:public Base {
public:
    virtual void fun1() { cout<<"deriverd:fun1()"<<endl; }
    void fun() { cout<<"deriverd:fun()"<<endl; }
};

int main() {
    Base *pb = new Deriverd;
    pb->fun1();
    return 0;
}

输出结果为:

main函数中创建了父类的指针指向了子类的对象,然后通过父类的指针调用具有隐藏关系的fun1()函数,我们会以为pb->fun1()调用的是子类的函数fun1(),实际并不是,隐藏关系的函数,谁调用就用谁的函数,按照正常的函数调用使用便可得正确的结果,这里是父类指针调用,就用父类的函数fun1()。这就是隐藏和重写的区别。

四、重写与隐藏的区别

看下面的代码:

cpp 复制代码
class Base {
public:
    virtual void foo(int x) { cout << "Base: " << x << endl;}
    void foo(int x, int y) { cout << "Base: " << x << ' ' << y << endl; }
};

// 报错,不能继承final修饰的类
class Derived : public Base {
public:
    void foo(int x) { cout << x << endl; };
    void foo(int x, int y) {cout << x << ' ' << y << endl; }
};
int main() {
    Base *pb = new Derived;
    pb->foo(1);
    pb->foo(1, 2);

    return 0;
}

其中 foo(int x, int y) 函数发生了隐藏,void foo(int x)函数发生了重写, Base *pb = new Derived;发生了父类指针指向子类对象,隐藏由于是父类指针,所以调用了父类的实现,重写由于是子类对象,所以调用了子类实现。

相关推荐
Theodore_10221 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
‘’林花谢了春红‘’3 小时前
C++ list (链表)容器
c++·链表·list
----云烟----3 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024063 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic4 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it4 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康4 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神5 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
机器视觉知识推荐、就业指导5 小时前
C++设计模式:建造者模式(Builder) 房屋建造案例
c++
宅小海5 小时前
scala String
大数据·开发语言·scala