C++中重载与重写函数区别及虚函数

C++虚函数详解
本文详细介绍了C++中虚函数的概念、使用方法及其背后的工作原理。包括虚函数的基础知识、多态性的实现、虚函数的语法特点以及一些高级用法如纯虚函数和虚析构函数等。
C++中的虚函数(virtual function)
1.简介
虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次:
class A
{
public:
virtual void foo() { cout << "A::foo() is called" << endl;}
};
class B: public A
{
public:
virtual void foo() { cout << "B::foo() is called" << endl;}
};
那么,在使用的时候,我们可以:
A * a = new B();
a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。
虚函数只能借助于指针或者引用来达到多态的效果,如果是下面这样的代码,则虽然是虚函数,但它不是多态的:
class A
{
public:
virtual void foo();
};
class B: public A
{
virtual void foo();
};
void bar()
{
A a;
a.foo(); // A::foo()被调用
}
1.1 多态
在了解了虚函数的意思之后,再考虑什么是多态就很容易了。仍然针对上面的类层次,但是使用的方法变的复杂了一些:
void bar(A * a)
{
a->foo(); // 被调用的是A::foo() 还是B::foo()?
}
因为foo()是个虚函数,所以在bar这个函数中,只根据这段代码,无从确定这里被调用的是A::foo()还是B::foo(),但是可以肯定的说:如果a指向的是A类的实例,则A::foo()被调用,如果a指向的是B类的实例,则B::foo()被调用。
这种同一代码可以产生不同效果的特点,被称为“多态”。
1.2 多态有什么用?
多态这么神奇,但是能用来做什么呢?这个命题我难以用一两句话概括,一般的C++教程(或者其它面向对象语言的教程)都用一个画图的例子来展示多态的用途,我就不再重复这个例子了,如果你不知道这个例子,随便找本书应该都有介绍。我试图从一个抽象的角度描述一下,回头再结合那个画图的例子,也许你就更容易理解。
在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构成类层次。这个类层次的使用者在使用它们的时候,如果仍然在需要基类的时候写针对基类的代码,在需要派生类的时候写针对派生类的代码,就等于类层次完全暴露在使用者面前。如果这个类层次有任何的改变(增加了新类),都需要使用者“知道”(针对新类写代码)。这样就增加了类层次与其使用者之间的耦合,有人把这种情况列为程序中的“bad smell”之一。
多态可以使程序员脱离这种窘境。再回头看看1.1中的例子,bar()作为A-B这个类层次的使用者,它并不知道这个类层次中有多少个类,每个类都叫什么,但是一样可以很好的工作,当有一个C类从A类派生出来后,bar()也不需要“知道”(修改)。这完全归功于多态--编译器针对虚函数产生了可以在运行时刻确定被调用函数的代码。
1.3 如何“动态联编”
编译器是如何针对虚函数产生可以再运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?Lippman在深度探索C++对象模型[1]中的不同章节讲到了几种方式,这里把“标准的”方式简单介绍一下。
我所说的“标准”方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写,针对1.1中的例子:
void bar(A * a)
{
a->foo();
}
会被改写为:
void bar(A * a)
{
(a->vptr[1])();
}
因为派生类和基类的foo()函数具有相同的VTABLE索引,而他们的vptr又指向不同的VTABLE,因此通过这样的方法可以在运行时刻决定调用哪个foo()函数。
虽然实际情况远非这么简单,但是基本原理大致如此。
1.4 overload和override
虚函数总是在派生类中被改写,这种改写被称为“override”。我经常混淆“overload”和“override”这两个单词。但是随着各类C++的书越来越多,后来的程序员也许不会再犯我犯过的错误了。但是我打算澄清一下:
override是指派生类重写基类的虚函数,就象我们前面B类中重写了A类中的foo()函数。重写的函数必须有一致的参数表和返回值(C++标准允许返回值不同的情况,这个我会在“语法”部分简单介绍,但是很少编译器支持这个feature)。这个单词好象一直没有什么合适的中文词汇来对应,有人译为“覆盖”,还贴切一些。
overload约定成俗的被翻译为“重载”。是指编写一个与已有函数同名但是参数表不同的函数。例如一个函数即可以接受整型数作为参数,也可以接受浮点数作为参数。
2. 虚函数的语法
虚函数的标志是“virtual”关键字。
2.1 使用virtual关键字
考虑下面的类层次:
class A
{
public:
virtual void foo();
};
class B: public A
{
public:
void foo(); // 没有virtual关键字!
};
class C: public B // 从B继承,不是从A继承!
{
public:
void foo(); // 也没有virtual关键字!
};
这种情况下,B::foo()是虚函数,C::foo()也同样是虚函数。因此,可以说,基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字。
2.2 纯虚函数
如下声明表示一个函数为纯虚函数:
class A
{
public:
virtual void foo()=0; // =0标志一个虚函数为纯虚函数
};
一个函数声明为纯虚后,纯虚函数的意思是:我是一个抽象类!不要把我实例化!纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,我的派生类都会有这个函数。
2.3 虚析构函数
析构函数也可以是虚的,甚至是纯虚的。例如:
class A
{
public:
virtual ~A()=0; // 纯虚析构函数
};
当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子:
class A
{
public:
A() { ptra_ = new char[10];}
~A() { delete[] ptra_;} // 非虚析构函数
private:
char * ptra_;
};
class B: public A
{
public:
B() { ptrb_ = new char[20];}
~B() { delete[] ptrb_;}
private:
char * ptrb_;
};
void foo()
{
A * a = new B;
delete a;
}
在这个例子中,程序也许不会象你想象的那样运行,在执行delete a的时候,实际上只有A::~A()被调用了,而B类的析构函数并没有被调用!这是否有点儿可怕?
如果将上面A::~A()改为virtual,就可以保证B::~B()也在delete a的时候被调用了。因此基类的析构函数都必须是virtual的。
纯虚的析构函数并没有什么作用,是虚的就够了。通常只有在希望将一个类变成抽象类(不能实例化的类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。
2.4 虚构造函数?
构造函数不能是虚的。
3. 虚函数使用技巧 3.1 private的虚函数
考虑下面的例子:
class A
{
public:
void foo() { bar();}
private:
virtual void bar() { ...}
};
class B: public A
{
private:
virtual void bar() { ...}
};
在这个例子中,虽然bar()在A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public或者protected的虚函数一样产生多态的效果。并不会因为它是private的,就发生A::foo()不能访问B::bar()的情况,也不会发生B::bar()对A::bar()的override不起作用的情况。
这种写法的语意是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用这个函数。
3.2 构造函数和析构函数中的虚函数调用
一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。例如:
class A
{
public:
A() { foo();} // 在这里,无论如何都是A::foo()被调用!
~A() { foo();} // 同上
virtual void foo();
};
class B: public A
{
public:
virtual void foo();
};
void bar()
{
A * a = new B;
delete a;
}
如果你希望delete a的时候,会导致B::foo()被调用,那么你就错了。同样,在new B的时候,A的构造函数被调用,但是在A的构造函数中,被调用的是A::foo()而不是B::foo()。
3.3 多继承中的虚函数 3.4 什么时候使用虚函数
在你设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现。通过这样的方法,就可以将对象的行为抽象化。
以设计模式[2]中Factory Method模式为例,Creator的factoryMethod()就是虚函数,派生类override这个函数后,产生不同的Product类,被产生的Product类被基类的AnOperation()函数使用。基类的AnOperation()函数针对Product类进行操作,当然Product类一定也有多态(虚函数)。
另外一个例子就是集合操作,假设你有一个以A类为基类的类层次,又用了一个std::vector<A *>来保存这个类层次中不同类的实例指针,那么你一定希望在对这个集合中的类进行操作的时候,不要把每个指针再cast回到它原来的类型(派生类),而是希望对他们进行同样的操作。那么就应该将这个“一样的操作”声明为virtual。
现实中,远不只我举的这两个例子,但是大的原则都是我前面说到的“如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的”。这句话也可以反过来说:“如果你发现基类提供了虚函数,那么你最好override它”。
<think> 首先,用户的问题是:“我想了解C++重载重写区别 请问C++函数重载函数重写区别”。用户参考了站内引用,这些引用提供了相关信息。我必须基于这些引用我的知识来回答。 回顾系统级指令: - 所有行内数学表达式用$...$。 - 独立公式用$$...$$。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 在引用的段落末尾自然地添加引用标识。 - 回答结构清晰,逐步解决问题。 - 保证回答真实可靠。 从引用中提取关键点: - 引用[1]:简述了区别,但未完成。 - 引用[2]:解释了重载是参数列表不同;重写是子类重写父类的virtual函数。 - 引用[3]:重载是参数列表不同,返回值类型不关心;给出了示例代码。 - 引用[4]:重载是在同一个类或命名空间,参数列表不同;给出了Calculator类的示例。 - 引用[5]:重载不能仅靠返回类型区分;重写的定义未完全给出。 用户上一次的问题也是关于这个区别,所以我需要结构化回答。 结构化回答: 1. **定义函数重载(Function Overloading)**: - 在同一个作用域内(如类或命名空间),定义多个同名函数,但参数列表不同(类型、个数、顺序)。 - 返回值类型不用于区分重载。 - 示例:如引用[4]中的Calculator类。 2. **定义函数重写(Function Overwriting)**: - 在继承关系中,子类重写父类的虚函数(virtual function)。 - 函数签名相同(包括名称、参数列表返回类型),但实现不同。 - 运行时多态的关键。 3. **对比区别**: - 作用域:重载在同一个作用域;重写在继承链中。 - 函数签名:重载参数列表不同;重写签名相同。 - 返回值:重载不关心返回值;重写通常要求相同(C++中,协变返回类型允许某些变化)。 - 目的:重载提供多个同名函数处理不同参数;重写实现多态。 4. **添加引用标识**:在解释中自然地添加。例如,提到关键点时引用相关来源。 5. **使用LaTeX**:如果有数学表达式,但这里可能没有直接数学,不过可以用于表示参数列表等。例如,函数参数可以用$a, b$等表示。 6. **生成相关问题**:在回答末尾添加相关问题。 7. **中文回答**:整个回答用中文。 回答结构: - 先简要概述区别。 - 然后详细解释重载。 - 接着详细解释重写。 - 最后对比总结。 - 添加代码示例以清晰说明(参考引用中的代码)。 - 末尾添加相关问题。 相关问题生成: 基于主题,问题如: 1. 重载重写在性能上有何影响? 2. C++中如何避免重载歧义? 3. 重写虚函数的关系是什么? 4. 实际编程中,何时使用重载,何时使用重写? 确保回答真实可靠:基于标准C++知识。 最终回答框架: - 开头:直接回答用户问题。 - 主体:分点解释。 - 结尾:总结。 - 相关问题。 </think> ### C++函数重载函数重写区别C++ 中,函数重载(Function Overloading)函数重写(Function Overwriting)是两个核心概念,它们都涉及同名函数的不同实现,但应用场景、规则目标截然不同。下面我将一步步解释两者的定义、区别示例,确保回答清晰可靠。所有解释基于标准 C++ 规范,并结合了站内引用提供的信息[^2][^3][^4]。 #### 1. **函数重载(Function Overloading)** - **定义**:函数重载发生在同一个作用域(如类内部或命名空间内),定义多个同名函数,但参数列表不同(包括参数的类型、个数或顺序)。编译器根据调用时的参数选择匹配的函数。 - 返回值类型不能用于区分重载函数(否则会导致编译错误)[^3][^5]。 - 目的:提供函数的多版本实现,提高代码可读性灵活性。例如,处理不同类型的数据输入。 - **示例代码**: ```cpp #include <iostream> using namespace std; class Calculator { public: // 重载 add 函数:参数列表不同 int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; } void add(int a, int b, int c) { cout << "Sum: " << (a + b + c) << endl; } }; int main() { Calculator calc; cout << calc.add(2, 3) << endl; // 调用 int add(int, int) cout << calc.add(2.5, 3.5) << endl; // 调用 double add(double, double) calc.add(1, 2, 3); // 调用 void add(int, int, int) return 0; } ``` - 在这个例子中,`add` 函数重载了三次,参数列表不同,但函数名相同[^4]。 #### 2. **函数重写(Function Overwriting)** - **定义**:函数重写发生在继承关系中,子类重新定义父类的虚函数(virtual function),函数签名(包括名称、参数列表返回类型)必须完全相同。 - 通过虚函数表(vtable)实现运行时多态,允许程序在运行时动态选择调用哪个版本的函数。 - 目的:支持多态性,使子类能自定义父类行为[^2][^3]。 - **关键点**: - 父类函数必须声明为 `virtual`(否则是函数隐藏,而非重写)。 - 返回值类型通常相同,但 C++ 允许协变返回类型(如父类返回基类指针,子类返回派生类指针)[^5]。 - **示例代码**: ```cpp #include <iostream> using namespace std; class Animal { public: virtual void speak() { // 虚函数声明 cout << "Animal sound" << endl; } }; class Dog : public Animal { public: void speak() override { // 重写父类虚函数 cout << "Woof!" << endl; } }; int main() { Animal* animal = new Dog(); animal->speak(); // 输出 "Woof!",运行时多态 delete animal; return 0; } ``` - 这里,`Dog` 类重写了 `Animal` 的虚函数 `speak`,签名相同但实现不同[^2]。 #### 3. **主要区别对比** 下表总结了函数重载重写的核心差异: | 特性 | 函数重载 (Overloading) | 函数重写 (Overwriting) | |--------------------|------------------------------------------------|----------------------------------------------| | **作用域** | 同一个类或命名空间内 | 继承关系中(子类父类之间) | | **函数签名要求** | 参数列表必须不同(类型、个数、顺序) | 签名完全相同(名称、参数、返回类型) | | **返回值类型** | 返回值不影响重载(相同返回类型但参数不同可重载)| 返回类型通常相同,协变返回类型允许有限变化 | | **多态性** | 编译时多态(静态绑定) | 运行时多态(动态绑定,通过虚函数表) | | **关键字** | 无需特殊关键字 | 父类函数需 `virtual`,子类可选 `override` | | **目的** | 处理不同参数输入,简化接口 | 实现子类自定义行为,支持多态 | | **示例场景** | 多个同名函数处理整数、浮点数等不同数据类型 | 子类重写父类的方法,如不同动物发声 | - **关键区别举例**:重载关注参数差异(如 `add(int, int)` vs `add(double, double)`),而重写关注继承中的行为覆盖(如 `Animal::speak()` vs `Dog::speak()`)。如果尝试仅通过返回值区分重载(如 `int func()` `void func()`),编译器会报错[^3][^5]。 #### 为什么重要? 理解重载重写是掌握 C++ 多态性的基础。重载优化了代码复用(例如数学运算),而重写实现了面向对象的设计模式(如工厂模式)。在实际开发中,正确使用它们能提升代码的扩展性可维护性[^2][^4]。 相关问题: 1. C++函数重载是否支持默认参数,如何避免歧义? 2. 重写虚函数时,为什么推荐使用 `override` 关键字? 3. 函数重写虚函数表(vtable)的机制是什么? 4. 在实际项目中,如何选择使用重载还是重写
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值