一、引言

在嵌入式 Linux 后台服务开发中,我们经常需要一些全局唯一的对象,如日志对象、配置对象、数据库连接对象等。如果使用全局变量,会破坏封装性,并且难以控制对象的创建和销毁时机。单例模式是解决这个问题的最佳方案,它确保一个类只有一个实例,并提供一个全局访问点。

本文将带你学习 static 关键字在类中的应用,掌握静态成员变量和静态成员函数的特性,然后重点学习单例模式的实现原理和适用场景,这是 Linux 后台服务中最常用的设计模式之一。


二、核心知识点

1、静态成员变量

静态成员变量是类的成员,而非对象的成员。它的主要特点如下:

  1. 声明与定义分离:在类内部声明静态成员变量时,需要使用 static 关键字,但不能直接初始化。必须在类外部(通常在 .cpp 文件中)进行定义和初始化。
  2. 属于类而非对象:静态成员变量是类级别的,所有该类的对象共享同一份静态成员变量。它不依赖于对象的创建,即使没有创建任何对象,静态成员变量也存在。
  3. 内存分配:静态成员变量存储在全局数据区(而非栈或堆),其生命周期贯穿整个程序运行期间。

 示例代码

// Example.h
class Example {
public:
    static int count; // 静态成员变量声明
    Example();
};

// Example.cpp
#include "Example.h"

int Example::count = 0; // 静态成员变量定义和初始化

Example::Example() {
    count++; // 每创建一个对象,count 加 1
}

2、静态成员函数

静态成员函数同样是类级别的函数,它的主要特点如下:

  1. 只能访问静态成员:静态成员函数没有 this 指针,因此只能访问类的静态成员变量和静态成员函数,无法访问非静态成员。
  2. 调用方式:可以通过类名直接调用(推荐),也可以通过对象调用。

示例代码

// Example.h
class Example {
public:
    static int count;
    static int getCount(); // 静态成员函数声明
    Example();
};

// Example.cpp
#include "Example.h"

int Example::count = 0;

int Example::getCount() { // 静态成员函数定义
    return count; // 只能访问静态成员变量
}

Example::Example() {
    count++;
}

3、单例模式

单例模式是一种创建型设计模式,其核心目标是确保一个类在整个程序运行期间只有一个实例,并提供一个全局访问点来获取该实例。

适用场景

  • 日志系统:确保所有模块使用同一个日志对象,统一日志格式和输出目标。
  • 配置管理:全局唯一的配置对象,避免配置不一致。
  • 数据库连接池:管理全局的数据库连接资源,避免重复创建连接。
  • 硬件资源管理:如嵌入式系统中的 GPIO、串口等独占资源。

实现方式

  1. 饿汉式单例:在程序启动时(即进入 main 函数前)就创建实例。

    • 优点:实现简单,线程安全(由编译器保证)。
    • 缺点:如果实例从未被使用,会造成资源浪费;如果单例对象依赖于其他资源,可能会出现初始化顺序问题。

    示例代码

    class Singleton {
    private:
        static Singleton instance; // 静态成员变量,程序启动时创建
        Singleton() {} // 私有构造函数
    public:
        static Singleton& getInstance() {
            return instance;
        }
        // 禁用拷贝构造和赋值
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    };
    
    // 类外部定义静态成员变量
    Singleton Singleton::instance;
    
  2. 懒汉式单例:在第一次调用 getInstance() 时才创建实例(延迟加载)。

    • 优点:按需创建,节省资源。
    • 缺点:在多线程环境下,原始的懒汉式实现不是线程安全的,可能会创建多个实例。
  3. 线程安全的懒汉式单例(C++11 及以上):C++11 标准规定:静态局部变量的初始化是线程安全的。利用这一特性,我们可以写出最简洁、高效的线程安全单例。

    示例代码

    class Singleton {
    private:
        Singleton() {} // 私有构造函数
    public:
        static Singleton& getInstance() {
            static Singleton instance; // 静态局部变量,第一次调用时初始化,线程安全
            return instance;
        }
        // 禁用拷贝构造和赋值
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    };
    

二、工程实战

1、练习题目

实现一个线程安全的单例日志类。要求:

  1. 确保日志类只有一个实例。
  2. 支持分级日志(DEBUG、INFO、WARN、ERROR)。
  3. 支持输出到控制台和文件。
  4. 支持设置日志级别(只有级别大于等于设置级别的日志才会输出)。
  5. 线程安全(多线程环境下不会出现日志错乱)。

2、完整代码实现

Logger.h

#ifndef LOGGER_H
#define LOGGER_H

#include <string>
#include <fstream>
#include <mutex>

// Log level enumeration
enum LogLevel {
    DEBUG,
    INFO,
    WARN,
    ERROR
};

class Logger {
private:
    LogLevel currentLevel;       // Current log level threshold
    std::ofstream logFile;       // File stream for logging
    bool consoleOutputEnabled;   // Flag to enable/disable console output
    bool fileOutputEnabled;      // Flag to enable/disable file output
    mutable std::mutex logMutex; // Mutex for thread safety (mutable to allow locking in const functions)

    // Private constructor (Singleton pattern)
    Logger();

    // Private destructor
    ~Logger();

    // Get current timestamp as string
    std::string getCurrentTime() const;

    // Convert LogLevel enum to string
    std::string levelToString(LogLevel level) const;

public:
    // Disable copy constructor and assignment operator
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

    // Get the singleton instance
    static Logger& getInstance();

    // Set the log level threshold
    void setLevel(LogLevel level);

    // Enable or disable console output
    void enableConsoleOutput(bool enable);

    // Enable or disable file output
    // @param filename: Name of the log file
    // @param append: If true, append to the file; otherwise, overwrite
    void enableFileOutput(const std::string& filename, bool append = true);

    // Log a DEBUG message
    void debug(const std::string& message);

    // Log an INFO message
    void info(const std::string& message);

    // Log a WARN message
    void warn(const std::string& message);

    // Log an ERROR message
    void error(const std::string& message);

private:
    // Internal generic log function
    // @param level: Log level of the message
    // @param message: Message content
    void log(LogLevel level, const std::string& message);
};

#endif // LOGGER_H

Logger.cpp

#include "Logger.h"
#include <iostream>
#include <ctime>
#include <iomanip>
#include <sstream>

// Constructor implementation
Logger::Logger() 
    : currentLevel(INFO), 
      consoleOutputEnabled(true), 
      fileOutputEnabled(false) {
    std::cout << "[Logger] Singleton instance created." << std::endl;
}

// Destructor implementation
Logger::~Logger() {
    if (logFile.is_open()) {
        logFile.close();
    }
    std::cout << "[Logger] Singleton instance destroyed." << std::endl;
}

// Get singleton instance (Thread-safe in C++11+)
Logger& Logger::getInstance() {
    static Logger instance;
    return instance;
}

// Get current time in "YYYY-MM-DD HH:MM:SS" format
std::string Logger::getCurrentTime() const {
    std::time_t now = std::time(nullptr);
    std::tm localTime;
    
    // Thread-safe local time conversion
#ifdef _WIN32
    localtime_s(&localTime, &now);
#else
    localtime_r(&now, &localTime);
#endif

    std::ostringstream oss;
    oss << std::put_time(&localTime, "%Y-%m-%d %H:%M:%S");
    return oss.str();
}

// Convert LogLevel enum to human-readable string
std::string Logger::levelToString(LogLevel level) const {
    switch (level) {
        case DEBUG: return "DEBUG";
        case INFO:  return "INFO";
        case WARN:  return "WARN";
        case ERROR: return "ERROR";
        default:    return "UNKNOWN";
    }
}

// Set the log level threshold
void Logger::setLevel(LogLevel level) {
    std::lock_guard<std::mutex> lock(logMutex);
    currentLevel = level;
}

// Enable or disable console output
void Logger::enableConsoleOutput(bool enable) {
    std::lock_guard<std::mutex> lock(logMutex);
    consoleOutputEnabled = enable;
}

// Enable or disable file output
void Logger::enableFileOutput(const std::string& filename, bool append) {
    std::lock_guard<std::mutex> lock(logMutex);
    
    if (logFile.is_open()) {
        logFile.close();
    }

    std::ios_base::openmode mode = std::ios_base::out;
    if (append) {
        mode |= std::ios_base::app;
    } else {
        mode |= std::ios_base::trunc;
    }

    logFile.open(filename, mode);
    if (logFile.is_open()) {
        fileOutputEnabled = true;
    } else {
        fileOutputEnabled = false;
        std::cerr << "[Logger] Failed to open log file: " << filename << std::endl;
    }
}

// Internal log function (handles actual output)
void Logger::log(LogLevel level, const std::string& message) {
    std::lock_guard<std::mutex> lock(logMutex); // RAII lock for thread safety

    if (level < currentLevel) {
        return; // Skip if message level is below threshold
    }

    // Format log message: [Time] [Level] Message
    std::ostringstream oss;
    oss << "[" << getCurrentTime() << "] "
        << "[" << levelToString(level) << "] "
        << message;

    std::string formattedMessage = oss.str();

    // Output to console if enabled
    if (consoleOutputEnabled) {
        if (level == ERROR) {
            std::cerr << formattedMessage << std::endl;
        } else {
            std::cout << formattedMessage << std::endl;
        }
    }

    // Output to file if enabled
    if (fileOutputEnabled && logFile.is_open()) {
        logFile << formattedMessage << std::endl;
        logFile.flush(); // Ensure data is written to disk immediately
    }
}

// Public DEBUG log interface
void Logger::debug(const std::string& message) {
    log(DEBUG, message);
}

// Public INFO log interface
void Logger::info(const std::string& message) {
    log(INFO, message);
}

// Public WARN log interface
void Logger::warn(const std::string& message) {
    log(WARN, message);
}

// Public ERROR log interface
void Logger::error(const std::string& message) {
    log(ERROR, message);
}

main.cpp (测试示例)

#include "Logger.h"
#include <thread>

void testLogFunction() {
    Logger& logger = Logger::getInstance();
    for (int i = 0; i < 5; ++i) {
        logger.info("Thread log message #" + std::to_string(i));
    }
}

int main() {
    // Get logger instance
    Logger& logger = Logger::getInstance();

    // Configure logger
    logger.setLevel(DEBUG);
    logger.enableFileOutput("app.log", false);

    // Test basic logging
    logger.debug("This is a DEBUG message.");
    logger.info("This is an INFO message.");
    logger.warn("This is a WARN message.");
    logger.error("This is an ERROR message.");

    // Test thread safety
    std::thread t1(testLogFunction);
    std::thread t2(testLogFunction);
    t1.join();
    t2.join();

    return 0;
}

三、今日踩坑指南

坑点 1:单例模式的拷贝构造和赋值运算符没有被禁用,导致可以创建多个实例

现象:如果你不小心写了 Logger log2 = Logger::getInstance();,编译器可能会生成默认的拷贝构造函数,从而创建出第二个实例。解决:将拷贝构造函数和赋值运算符声明为 delete(C++11 及以上),或者将其声明为私有且不实现。

坑点 2:懒汉式单例模式在多线程环境下不是线程安全的

现象:在 C++11 之前,如果两个线程同时第一次调用 getInstance(),可能会同时判断 instance 为空,从而创建两个对象。解决

  1. (推荐)使用 C++11 及以上的静态局部变量初始化方式,编译器保证其线程安全。
  2. 使用 std::mutex 加双重检查锁定(DCL, Double-Checked Locking),但注意内存序问题。

坑点 3:单例对象的析构顺序问题

现象:如果单例 A 依赖单例 B,而 B 在 A 之前被析构了,那么 A 在析构时访问 B 就会导致程序崩溃。解决

  1. 尽量避免单例对象之间的相互依赖。
  2. 如果必须依赖,可以手动管理析构顺序,或者使用智能指针(如 std::shared_ptr)来控制生命周期。

四、今日总结

通过今天的学习,我们完成了以下目标:

  1. 掌握了静态成员变量和静态成员函数的特性和用法:理解了它们属于类而非对象的本质,以及在内存管理和访问限制上的特点。
  2. 理解了单例模式的实现原理和适用场景:区分了饿汉式和懒汉式的优劣,并重点掌握了利用 C++11 静态局部变量特性实现的线程安全单例。
  3. 实现了一个线程安全的单例日志类:在工程实战中,我们将 std::mutex 与单例结合,实现了一个功能完善、支持多线程的日志组件。
  4. 学会了如何使用单例模式进行全局资源管理:这为我们后续开发复杂的 Linux 后台服务打下了坚实的基础。

五、后续预告

下一篇文章: 【C++ -Day11】Linux 通用日志组件实战:C++ 面向对象设计与工程化实现

我们将把今天的单例日志类扩展为一个更通用、更强大的 Linux 日志组件,支持异步写入、日志滚动、格式化输出等高级特性。


原创不易,如果本文对你有帮助,欢迎点赞、收藏、关注三连!有任何问题都可以在评论区留言,我会及时回复。

 

更多推荐