MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++流运算符重载的实际案例

2021-09-301.5k 阅读

C++ 流运算符重载的基础概念

在 C++ 中,流(Stream)是一种处理输入输出的机制。iostream 库提供了 std::cinstd::cout 等对象来处理标准输入输出。流运算符 <<>> 分别用于输出和输入操作。例如,我们可以使用 std::cout << "Hello, World!"; 来将字符串输出到控制台。

流运算符是可以重载的,这使得我们能够自定义类型的输入输出行为。当我们定义了一个新的类,并且希望能够使用流运算符方便地对其对象进行输入输出操作时,就需要重载流运算符。

输出流运算符 << 的重载

输出流运算符 << 用于将数据输出到流中。为了重载这个运算符,我们通常将其定义为类的友元函数。原因在于,左操作数是流对象(如 std::ostream),而不是我们自定义类的对象,如果定义为成员函数,语法上会不太符合习惯。

下面是一个简单的例子,我们定义一个 Point 类来表示二维平面上的点,并重载 << 运算符以便能够方便地输出点的坐标:

#include <iostream>

class Point {
public:
    int x;
    int y;
    Point(int a, int b) : x(a), y(b) {}
    // 友元函数声明
    friend std::ostream& operator<<(std::ostream& os, const Point& p);
};

// 友元函数定义
std::ostream& operator<<(std::ostream& os, const Point& p) {
    os << "(" << p.x << ", " << p.y << ")";
    return os;
}

int main() {
    Point p(3, 4);
    std::cout << "The point is: " << p << std::endl;
    return 0;
}

在上述代码中,我们首先在 Point 类中声明了 operator<< 为友元函数。然后在类外定义这个函数,函数的参数 osstd::ostream 的引用,表示输出流对象,pPoint 类对象的常量引用。在函数体中,我们按照自定义的格式将点的坐标输出到流中,并返回 os,这样可以支持链式输出,例如 std::cout << "Point 1: " << p1 << " Point 2: " << p2;

输入流运算符 >> 的重载

输入流运算符 >> 用于从流中读取数据。与 << 运算符类似,通常也将其定义为友元函数。

继续以 Point 类为例,我们重载 >> 运算符来从输入流中读取点的坐标:

#include <iostream>

class Point {
public:
    int x;
    int y;
    Point() : x(0), y(0) {}
    // 友元函数声明
    friend std::istream& operator>>(std::istream& is, Point& p);
};

// 友元函数定义
std::istream& operator>>(std::istream& is, Point& p) {
    is >> p.x >> p.y;
    return is;
}

int main() {
    Point p;
    std::cout << "Enter the coordinates of the point (x y): ";
    std::cin >> p;
    std::cout << "The point you entered is: (" << p.x << ", " << p.y << ")" << std::endl;
    return 0;
}

在这个例子中,operator>> 函数从输入流 is 中读取两个整数,分别赋值给 Point 对象 pxy 成员变量。同样,函数返回 is,以支持连续输入,例如 std::cin >> p1 >> p2;

复杂类型的流运算符重载

自定义类包含动态内存的情况

当自定义类包含动态分配的内存时,流运算符重载需要特别小心,以避免内存泄漏等问题。

假设我们有一个 String 类,它模拟一个简单的字符串类,内部使用动态分配的字符数组来存储字符串:

#include <iostream>
#include <cstring>

class String {
private:
    char* str;
    int length;
public:
    String(const char* s = nullptr) {
        if (s == nullptr) {
            length = 0;
            str = new char[1];
            str[0] = '\0';
        } else {
            length = std::strlen(s);
            str = new char[length + 1];
            std::strcpy(str, s);
        }
    }
    ~String() {
        delete[] str;
    }
    // 拷贝构造函数
    String(const String& other) {
        length = other.length;
        str = new char[length + 1];
        std::strcpy(str, other.str);
    }
    // 赋值运算符重载
    String& operator=(const String& other) {
        if (this == &other) {
            return *this;
        }
        delete[] str;
        length = other.length;
        str = new char[length + 1];
        std::strcpy(str, other.str);
        return *this;
    }
    // 友元函数声明
    friend std::ostream& operator<<(std::ostream& os, const String& s);
    friend std::istream& operator>>(std::istream& is, String& s);
};

std::ostream& operator<<(std::ostream& os, const String& s) {
    os << s.str;
    return os;
}

std::istream& operator>>(std::istream& is, String& s) {
    char temp[100]; // 假设输入字符串长度不超过 99
    is >> temp;
    delete[] s.str;
    s.length = std::strlen(temp);
    s.str = new char[s.length + 1];
    std::strcpy(s.str, temp);
    return is;
}

int main() {
    String s1;
    std::cout << "Enter a string: ";
    std::cin >> s1;
    std::cout << "You entered: " << s1 << std::endl;
    return 0;
}

String 类中,我们有一个动态分配的字符数组 str。在重载 << 运算符时,直接输出 str 即可。而在重载 >> 运算符时,我们先读取到一个临时数组 temp,然后释放原来 sstr 占用的内存,再重新分配内存并复制 temp 中的内容到 s.str

嵌套自定义类的情况

当自定义类中包含其他自定义类对象时,流运算符重载需要考虑嵌套结构。

假设有一个 Rectangle 类,它由两个 Point 对象表示左上角和右下角的顶点:

#include <iostream>

class Point {
public:
    int x;
    int y;
    Point(int a, int b) : x(a), y(b) {}
    friend std::ostream& operator<<(std::ostream& os, const Point& p) {
        os << "(" << p.x << ", " << p.y << ")";
        return os;
    }
    friend std::istream& operator>>(std::istream& is, Point& p) {
        is >> p.x >> p.y;
        return is;
    }
};

class Rectangle {
public:
    Point topLeft;
    Point bottomRight;
    Rectangle(const Point& tl, const Point& br) : topLeft(tl), bottomRight(br) {}
    friend std::ostream& operator<<(std::ostream& os, const Rectangle& r) {
        os << "Top Left: " << r.topLeft << ", Bottom Right: " << r.bottomRight;
        return os;
    }
    friend std::istream& operator>>(std::istream& is, Rectangle& r) {
        is >> r.topLeft >> r.bottomRight;
        return is;
    }
};

int main() {
    Rectangle r(Point(1, 1), Point(5, 5));
    std::cout << "Rectangle: " << r << std::endl;
    Rectangle r2;
    std::cout << "Enter rectangle coordinates (top left x y bottom right x y): ";
    std::cin >> r2;
    std::cout << "You entered rectangle: " << r2 << std::endl;
    return 0;
}

Rectangle 类中,我们重载 <<>> 运算符。<< 运算符通过调用 Point 类重载的 << 运算符来输出两个顶点的信息。>> 运算符同样通过调用 Point 类重载的 >> 运算符来读取两个顶点的坐标。

流运算符重载与模板

模板类的流运算符重载

当我们使用模板定义类时,也可以重载流运算符。以一个简单的 Stack 模板类为例,它实现了一个栈结构:

#include <iostream>
#include <stdexcept>

template <typename T>
class Stack {
private:
    T* data;
    int topIndex;
    int capacity;
public:
    Stack(int cap = 10) : capacity(cap), topIndex(-1) {
        data = new T[capacity];
    }
    ~Stack() {
        delete[] data;
    }
    void push(const T& value) {
        if (topIndex == capacity - 1) {
            throw std::overflow_error("Stack is full");
        }
        data[++topIndex] = value;
    }
    T pop() {
        if (topIndex == -1) {
            throw std::underflow_error("Stack is empty");
        }
        return data[topIndex--];
    }
    bool isEmpty() const {
        return topIndex == -1;
    }
    // 友元函数声明
    template <typename U>
    friend std::ostream& operator<<(std::ostream& os, const Stack<U>& s);
};

template <typename T>
std::ostream& operator<<(std::ostream& os, const Stack<T>& s) {
    os << "Stack: ";
    for (int i = 0; i <= s.topIndex; ++i) {
        os << s.data[i];
        if (i < s.topIndex) {
            os << ", ";
        }
    }
    return os;
}

int main() {
    Stack<int> s;
    s.push(1);
    s.push(2);
    s.push(3);
    std::cout << s << std::endl;
    return 0;
}

在上述代码中,我们定义了一个 Stack 模板类。在重载 << 运算符时,我们同样使用了模板。注意,在类内声明友元函数时,需要再次声明模板参数 template <typename U>,这里的 U 与类模板参数 T 没有直接关联,但在实际使用中通常会保持一致。在函数定义中,我们遍历栈中的元素并输出。

通用模板流运算符重载

有时候,我们希望为不同类型的对象提供一个通用的流输出方式。例如,我们可以定义一个模板函数来输出数组:

#include <iostream>

template <typename T, size_t N>
std::ostream& operator<<(std::ostream& os, const T(&arr)[N]) {
    os << "Array: ";
    for (size_t i = 0; i < N; ++i) {
        os << arr[i];
        if (i < N - 1) {
            os << ", ";
        }
    }
    return os;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::cout << arr << std::endl;
    return 0;
}

这个模板函数 operator<< 可以接受任何类型的数组,并按照自定义格式输出数组元素。这种通用模板流运算符重载在处理各种数据结构时非常方便。

流运算符重载的实际应用场景

日志系统

在开发大型应用程序时,日志系统是非常重要的。我们可以使用流运算符重载来方便地记录日志。

假设我们有一个 Logger 类,它管理日志文件的写入:

#include <iostream>
#include <fstream>
#include <ctime>

class Logger {
private:
    std::ofstream logFile;
public:
    Logger(const char* filename) {
        logFile.open(filename, std::ios::app);
        if (!logFile.is_open()) {
            std::cerr << "Failed to open log file." << std::endl;
        }
    }
    ~Logger() {
        logFile.close();
    }
    template <typename T>
    Logger& operator<<(const T& value) {
        auto now = std::time(nullptr);
        auto tm_info = *std::localtime(&now);
        char timeStr[26];
        std::strftime(timeStr, 26, "%Y-%m-%d %H:%M:%S", &tm_info);
        logFile << "[" << timeStr << "] " << value << std::endl;
        return *this;
    }
};

int main() {
    Logger logger("app.log");
    logger << "Application started.";
    logger << "Performing some operation...";
    return 0;
}

Logger 类中,我们重载了 << 运算符模板,使得可以方便地将各种类型的数据记录到日志文件中。每次记录日志时,会添加当前的时间戳。

数据序列化与反序列化

在网络通信或数据存储中,经常需要将对象转换为字节流(序列化)以及从字节流恢复对象(反序列化)。流运算符重载可以在一定程度上简化这个过程。

例如,我们有一个 User 类,包含用户名和年龄,我们可以重载流运算符来实现简单的序列化和反序列化:

#include <iostream>
#include <sstream>

class User {
public:
    std::string username;
    int age;
    User(const std::string& un, int a) : username(un), age(a) {}
    friend std::ostream& operator<<(std::ostream& os, const User& u) {
        os << u.username << " " << u.age;
        return os;
    }
    friend std::istream& operator>>(std::istream& is, User& u) {
        is >> u.username >> u.age;
        return is;
    }
};

int main() {
    User user("John", 30);
    std::ostringstream oss;
    oss << user;
    std::string serialized = oss.str();
    std::cout << "Serialized data: " << serialized << std::endl;

    std::istringstream iss(serialized);
    User deserialized;
    iss >> deserialized;
    std::cout << "Deserialized user: " << deserialized.username << ", " << deserialized.age << std::endl;
    return 0;
}

在这个例子中,我们通过重载 <<>> 运算符,将 User 对象转换为字符串(序列化)以及从字符串恢复 User 对象(反序列化)。这里使用了 std::ostringstreamstd::istringstream 来处理字符串流。

流运算符重载的注意事项

异常处理

在重载流运算符时,特别是在输入流运算符 >> 重载中,需要注意异常处理。例如,当输入数据格式不正确时,应该抛出合适的异常或者设置流的错误状态。

#include <iostream>
#include <stdexcept>

class Rational {
public:
    int numerator;
    int denominator;
    Rational(int num, int den) : numerator(num), denominator(den) {
        if (den == 0) {
            throw std::invalid_argument("Denominator cannot be zero");
        }
    }
    friend std::istream& operator>>(std::istream& is, Rational& r) {
        char slash;
        is >> r.numerator >> slash >> r.denominator;
        if (!is || slash != '/') {
            is.setstate(std::ios::failbit);
        } else if (r.denominator == 0) {
            throw std::invalid_argument("Denominator cannot be zero");
        }
        return is;
    }
};

int main() {
    Rational r(1, 2);
    std::cout << "Enter a rational number (format: num/den): ";
    try {
        std::cin >> r;
    } catch (const std::invalid_argument& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
    if (std::cin.fail()) {
        std::cerr << "Invalid input format." << std::endl;
    }
    return 0;
}

在上述 Rational 类的 operator>> 重载中,我们检查输入格式是否正确(是否包含 /),以及分母是否为零。如果格式不正确,我们设置流的错误状态 std::ios::failbit;如果分母为零,我们抛出 std::invalid_argument 异常。

性能考虑

在重载流运算符时,尤其是处理大量数据或复杂对象时,性能是一个重要的考虑因素。例如,在输出流运算符 << 重载中,如果频繁地进行字符串拼接等操作,可能会导致性能下降。

#include <iostream>
#include <string>

class BigObject {
private:
    std::string data[1000];
public:
    BigObject() {
        for (int i = 0; i < 1000; ++i) {
            data[i] = "Some data " + std::to_string(i);
        }
    }
    friend std::ostream& operator<<(std::ostream& os, const BigObject& obj) {
        // 性能较差的实现,频繁字符串拼接
        std::string result;
        for (int i = 0; i < 1000; ++i) {
            result += obj.data[i] + " ";
        }
        os << result;
        return os;
    }
};

class BigObjectBetter {
private:
    std::string data[1000];
public:
    BigObjectBetter() {
        for (int i = 0; i < 1000; ++i) {
            data[i] = "Some data " + std::to_string(i);
        }
    }
    friend std::ostream& operator<<(std::ostream& os, const BigObjectBetter& obj) {
        // 性能较好的实现,直接输出
        for (int i = 0; i < 1000; ++i) {
            os << obj.data[i] << " ";
        }
        return os;
    }
};

int main() {
    BigObject obj;
    BigObjectBetter objBetter;
    std::cout << "BigObject output: ";
    std::cout << obj << std::endl;
    std::cout << "BigObjectBetter output: ";
    std::cout << objBetter << std::endl;
    return 0;
}

BigObject 类的 operator<< 重载中,我们先进行字符串拼接,然后输出整个字符串,这会导致较多的内存分配和拷贝操作。而在 BigObjectBetter 类的 operator<< 重载中,我们直接逐个输出数据,避免了不必要的字符串拼接,从而提高了性能。

与标准库的兼容性

在重载流运算符时,要确保与 C++ 标准库的兼容性。例如,不要改变标准库中流对象的基本行为,并且要遵循标准库对流操作的规范。

#include <iostream>

// 错误示例,试图改变 std::cout 的基本行为
std::ostream& operator<<(std::ostream& os, const char* str) {
    // 这里改变了 std::cout 输出字符串的默认行为
    for (int i = 0; str[i] != '\0'; ++i) {
        os.put(str[i] + 1);
    }
    return os;
}

int main() {
    std::cout << "Hello"; // 预期输出 "Hello",但实际输出行为被改变
    return 0;
}

在上述错误示例中,我们重载了 std::ostreamconst char*<< 运算符,改变了其输出字符串的默认行为。这可能会导致与其他依赖标准输出行为的代码不兼容。正确的做法是仅在自定义类型上进行流运算符重载,而不要改变标准库中已有的流操作行为。

通过以上对 C++ 流运算符重载的详细介绍,包括基础概念、复杂类型的重载、与模板的结合、实际应用场景以及注意事项等方面,希望读者能够深入理解并熟练运用流运算符重载,在 C++ 编程中更好地处理输入输出操作,开发出更加健壮和高效的程序。