【C++】深入理解C++中的函数与运算符重载
文章目录
- 前言
- 一、什么是重载?
- 1.1 函数重载
- 1.1.1 函数重载的规则
- 1.1.2 示例:函数重载
- 1.2 运算符重载
- 1.2.1 运算符重载的规则
- 1.2.2 示例:运算符重载
- 1.2.3 运算符重载的注意事项
- 二、重载的注意事项
- 2.1 重载的二义性
- 2.2 默认参数和重载
- 2.3 运算符重载的成员函数与非成员函数
- 2.4 重载与继承
- 虚函数与重载
- 三、重载的一些问题
- 3.1 函数重载如何确定应该调用哪个函数?在编译期间还是运行期间?
- 3.2 C语言支持函数重载吗?为什么?
- 3.3 C++底层怎么支持函数重载的?
- 3.4 为什么仅仅返回值不同不能形成重载?
- 3.5 extern "C" 的作用?
- 3.6 如何在C语言中写出函数重载的效果?
前言
C++中 重载 主要体现在两种形式:函数重载和运算符重载。下面会分别进行介绍
一、什么是重载?
重载(Overloading) 指在同一个作用域内,允许使用相同名称的函数或运算符,但参数列表或类型不同。
参数列表指的是 参数类型、参数个数、参数类型次序不同;
1.1 函数重载
函数重载是指在同一个作用域中,允许存在多个函数名相同但参数列表不同的函数。编译器会根据函数调用时传入的参数个数、类型等信息来区分并调用相应的函数。
1.1.1 函数重载的规则
- 函数名相同:重载的函数必须具有相同的函数名。
- 参数个数或类型不同:重载的函数必须有不同的参数个数、类型或顺序。参数的类型、数量或顺序任何一种发生变化,都会被视作不同的函数重载。
- 返回类型无关:返回类型不能作为区分重载函数的依据,重载的函数必须通过不同的参数列表来区分。
1.1.2 示例:函数重载
#include <iostream>
using namespace std;class Printer {
public:// 打印整数void print(int i) {cout << "打印整数: " << i << endl;}// 打印浮点数void print(double d) {cout << "打印浮点数: " << d << endl;}// 打印字符串void print(const string& s) {cout << "打印字符串: " << s << endl;}
};int main() {Printer p;p.print(10); // 打印整数p.print(3.14); // 打印浮点数p.print("Hello!"); // 打印字符串return 0;
}
输出:
打印整数: 10
打印浮点数: 3.14
打印字符串: Hello!
对于上面的代码,print
函数通过参数类型的不同实现了重载。编译器根据传入的参数类型来选择对应的函数版本。
1.2 运算符重载
运算符重载是指为自定义类型(如类)定义运算符的行为,使得这些类型可以像内置类型一样使用常见的运算符(例如 +
、-
、*
等)。
1.2.1 运算符重载的规则
- 运算符重载必须是成员函数或者非成员函数。
- 运算符重载不会改变运算符的优先级和结合性。
- 可以重载大多数的运算符,但有一些运算符不能被重载(如
::
、sizeof
、typeid
等)。 - 运算符重载必须遵循特定的语法规则,通常通过类成员函数或全局函数实现。
1.2.2 示例:运算符重载
#include <iostream>
using namespace std;class Complex {
private:double real;double imag;public:// 构造函数Complex(double r, double i) : real(r), imag(i) {}// 重载+运算符Complex operator+(const Complex& other) {return Complex(real + other.real, imag + other.imag);}// 输出复数void print() const {cout << real << " + " << imag << "i" << endl;}
};int main() {Complex c1(3.0, 4.0);Complex c2(1.0, 2.0);Complex c3 = c1 + c2; // 使用重载的+运算符c3.print(); // 输出: 4.0 + 6.0ireturn 0;
}
输出:
4.0 + 6.0i
对于上面的代码,定义了一个Complex
类表示复数,并重载 +
运算符使可以通过 +
来相加两个复数对象。重载后的 +
运算符实现了两个复数的实部和虚部相加。
1.2.3 运算符重载的注意事项
- 运算符重载只是改变了运算符的行为,而不改变运算符的基本语义。
- 并非所有运算符都可以被重载,C++标准规定了一些不能被重载的运算符,比如
::
(作用域解析运算符)、sizeof
(大小运算符)、typeid
(类型信息运算符)等。 - 重载运算符时需要特别注意运算符的结合性和优先级,这些是固定的,无法改变的。
二、重载的注意事项
在使用重载时,有一些注意事项需要特别留意:
2.1 重载的二义性
有时重载可能会导致编译器无法确定使用哪个函数或运算符。这种情况通常称为二义性。例如,假设有两个重载函数,它们的参数类型很相似,编译器可能无法决定调用哪一个函数。
void func(int x);
void func(double x);
如果此时传入一个 float
类型的参数,编译器可能不知道该调用 int
版本还是 double
版本的函数。这种情况下可以通过强制类型转换来解决。
2.2 默认参数和重载
默认参数不能作为区分重载的依据。如果两个函数的参数只有默认值不同,那么它们仍然会被认为是同一个函数,而不能构成重载。
void func(int x = 5);
void func(int x = 10); // 错误:参数默认值不同,不构成重载
2.3 运算符重载的成员函数与非成员函数
-
运算符可以通过成员函数或者非成员函数来重载。成员函数可以访问类的私有数据,而非成员函数(通常是全局函数)需要通过传参来访问类的数据。
-
通常来说,如果运算符重载涉及到修改类内部状态,则使用成员函数;如果只是执行操作,则可以使用非成员函数。
2.4 重载与继承
重载的函数或运算符如果在基类和派生类中都有定义,派生类可以覆盖基类的重载版本。
比如对于下面的代码:
#include <iostream>
using namespace std;// 基类
class Shape {
public:// 基类中的重载函数virtual void draw(int radius) {cout << "Drawing a circle with radius: " << radius << endl;}virtual void draw(double side) {cout << "Drawing a square with side: " << side << endl;}// 虚拟函数,允许派生类重写virtual ~Shape() {}
};// 派生类
class Circle : public Shape {
public:// 重写基类的重载函数void draw(int radius) override {cout << "Circle class: Drawing a circle with radius: " << radius << endl;}
};class Square : public Shape {
public:// 重写基类的重载函数void draw(double side) override {cout << "Square class: Drawing a square with side: " << side << endl;}
};int main() {Shape* shape1 = new Circle();Shape* shape2 = new Square();// 使用基类指针调用派生类的覆盖版本shape1->draw(5); // 调用 Circle 类中的 draw(int)shape2->draw(3.5); // 调用 Square 类中的 draw(double)delete shape1;delete shape2;return 0;
}
会有如下的输出结果:
Circle class: Drawing a circle with radius: 5
Square class: Drawing a square with side: 3.5
虚函数与重载
如果重载的函数是虚函数,那么编译时选择哪个重载版本并不会受到虚函数机制的影响。虚函数机制(多态性)是基于运行时的动态绑定来确定调用哪个函数,而重载是根据函数签名在编译时进行解析的。
三、重载的一些问题
3.1 函数重载如何确定应该调用哪个函数?在编译期间还是运行期间?
- A:函数重载的选择是在 编译期间 确定。具体的说,编译器根据函数的参数列表来决定应该调用哪个重载版本。这个过程被称为静态绑定或早期绑定。
3.2 C语言支持函数重载吗?为什么?
- C语言不支持,C语言编译器种没有名字修饰(Name Mangling)机制,C++ 编译器可能会将 func(int) 和 func(double) 分别变成 func_int 和 func_double,从而使它们在符号表中具有不同的名字。
- 而C 的符号表通常只有函数的名称,而没有参数类型的附加信息。
3.3 C++底层怎么支持函数重载的?
- 同上一问,C++编译器会将函数的参数信息也加载到名字中;在符号表中的名字就不同了。
3.4 为什么仅仅返回值不同不能形成重载?
- 同上面的问题,编译器不会将函数的返回值加载到名字中,仅仅返回值不同,符号表上依然是相同的
3.5 extern “C” 的作用?
- extern “C” 是用于 C++ 中的声明,作用是 告诉编译器以 C 语言的方式来处理函数或变量的链接方式,即 禁用 C++ 的名字修饰(name mangling)
使用 extern "C"
修饰函数或变量,可以确保这些符号按照 C 的规则进行链接,使得 C++ 编译器生成与 C 编译器兼容的符号,从而允许 C++ 和 C 代码混合编程,进行跨语言调用。
3.6 如何在C语言中写出函数重载的效果?
C语言中可以通过利用特性实现函数重载的效果,比如 宏 与 变参函数
- 宏
可以利用宏根据不同的参数类型调用不同的函数:
#include <stdio.h>#define PRINT(x) _Generic((x), \int: print_int, \double: print_double)(x)void print_int(int x) {printf("Integer: %d\n", x);
}void print_double(double x) {printf("Double: %f\n", x);
}int main() {PRINT(10); // 调用 print_intPRINT(3.14); // 调用 print_doublereturn 0;
}
- 变参函数
C 语言允许定义变参函数,即函数的参数数量和类型可以在调用时动态变化。例如:
#include <stdio.h>
#include <stdarg.h>void print(const char *format, ...) {va_list args;va_start(args, format);if (strcmp(format, "int") == 0) {int x = va_arg(args, int);printf("Integer: %d\n", x);} else if (strcmp(format, "double") == 0) {double x = va_arg(args, double);printf("Double: %f\n", x);}va_end(args);
}int main() {print("int", 10);print("double", 3.14);return 0;
}