【C++】深入理解C++中的函数与运算符重载

发布于:2025-05-25 ⋅ 阅读:(22) ⋅ 点赞:(0)

前言

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 运算符重载的规则
  • 运算符重载必须是成员函数或者非成员函数
  • 运算符重载不会改变运算符的优先级和结合性。
  • 可以重载大多数的运算符,但有一些运算符不能被重载(如 ::sizeoftypeid 等)。
  • 运算符重载必须遵循特定的语法规则,通常通过类成员函数或全局函数实现。
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.0i

    return 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” 的作用?

  1. 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_int
    PRINT(3.14);      // 调用 print_double
    return 0;
}
  1. 变参函数

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;
}

网站公告

今日签到

点亮在社区的每一天
去签到