【C++ -Day10】静态成员与单例模式 | 实现全局资源管理
一、引言
在嵌入式 Linux 后台服务开发中,我们经常需要一些全局唯一的对象,如日志对象、配置对象、数据库连接对象等。如果使用全局变量,会破坏封装性,并且难以控制对象的创建和销毁时机。单例模式是解决这个问题的最佳方案,它确保一个类只有一个实例,并提供一个全局访问点。
本文将带你学习 static 关键字在类中的应用,掌握静态成员变量和静态成员函数的特性,然后重点学习单例模式的实现原理和适用场景,这是 Linux 后台服务中最常用的设计模式之一。
二、核心知识点
1、静态成员变量
静态成员变量是类的成员,而非对象的成员。它的主要特点如下:
- 声明与定义分离:在类内部声明静态成员变量时,需要使用
static关键字,但不能直接初始化。必须在类外部(通常在.cpp文件中)进行定义和初始化。 - 属于类而非对象:静态成员变量是类级别的,所有该类的对象共享同一份静态成员变量。它不依赖于对象的创建,即使没有创建任何对象,静态成员变量也存在。
- 内存分配:静态成员变量存储在全局数据区(而非栈或堆),其生命周期贯穿整个程序运行期间。
示例代码:
// Example.h
class Example {
public:
static int count; // 静态成员变量声明
Example();
};
// Example.cpp
#include "Example.h"
int Example::count = 0; // 静态成员变量定义和初始化
Example::Example() {
count++; // 每创建一个对象,count 加 1
}
2、静态成员函数
静态成员函数同样是类级别的函数,它的主要特点如下:
- 只能访问静态成员:静态成员函数没有
this指针,因此只能访问类的静态成员变量和静态成员函数,无法访问非静态成员。 - 调用方式:可以通过类名直接调用(推荐),也可以通过对象调用。
示例代码:
// 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、串口等独占资源。
实现方式
-
饿汉式单例:在程序启动时(即进入
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; -
懒汉式单例:在第一次调用
getInstance()时才创建实例(延迟加载)。- 优点:按需创建,节省资源。
- 缺点:在多线程环境下,原始的懒汉式实现不是线程安全的,可能会创建多个实例。
-
线程安全的懒汉式单例(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、练习题目
实现一个线程安全的单例日志类。要求:
- 确保日志类只有一个实例。
- 支持分级日志(DEBUG、INFO、WARN、ERROR)。
- 支持输出到控制台和文件。
- 支持设置日志级别(只有级别大于等于设置级别的日志才会输出)。
- 线程安全(多线程环境下不会出现日志错乱)。
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 为空,从而创建两个对象。解决:
- (推荐)使用 C++11 及以上的静态局部变量初始化方式,编译器保证其线程安全。
- 使用
std::mutex加双重检查锁定(DCL, Double-Checked Locking),但注意内存序问题。
坑点 3:单例对象的析构顺序问题
现象:如果单例 A 依赖单例 B,而 B 在 A 之前被析构了,那么 A 在析构时访问 B 就会导致程序崩溃。解决:
- 尽量避免单例对象之间的相互依赖。
- 如果必须依赖,可以手动管理析构顺序,或者使用智能指针(如
std::shared_ptr)来控制生命周期。
四、今日总结
通过今天的学习,我们完成了以下目标:
- 掌握了静态成员变量和静态成员函数的特性和用法:理解了它们属于类而非对象的本质,以及在内存管理和访问限制上的特点。
- 理解了单例模式的实现原理和适用场景:区分了饿汉式和懒汉式的优劣,并重点掌握了利用 C++11 静态局部变量特性实现的线程安全单例。
- 实现了一个线程安全的单例日志类:在工程实战中,我们将
std::mutex与单例结合,实现了一个功能完善、支持多线程的日志组件。 - 学会了如何使用单例模式进行全局资源管理:这为我们后续开发复杂的 Linux 后台服务打下了坚实的基础。
五、后续预告
下一篇文章: 【C++ -Day11】Linux 通用日志组件实战:C++ 面向对象设计与工程化实现
我们将把今天的单例日志类扩展为一个更通用、更强大的 Linux 日志组件,支持异步写入、日志滚动、格式化输出等高级特性。
原创不易,如果本文对你有帮助,欢迎点赞、收藏、关注三连!有任何问题都可以在评论区留言,我会及时回复。
更多推荐



所有评论(0)