最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

從零實(shí)現(xiàn)一個(gè) C++ 輕量級(jí)日志系統(tǒng)原理與實(shí)踐指南

 更新時(shí)間:2026年05月13日 09:01:03   作者:Cx330?  
本文從零開始手搓一個(gè)簡(jiǎn)易C++日志工具,詳細(xì)介紹了日志系統(tǒng)的理念、設(shè)計(jì)、實(shí)現(xiàn)及優(yōu)化,核心包括:日志格式化時(shí)間和等級(jí),基于策略模式實(shí)現(xiàn)異步刷新,使用RAII機(jī)制實(shí)現(xiàn)自動(dòng)刷新,以及對(duì)線程安全和可重入性的深度考量,最后還提供了優(yōu)化方向,幫助讀者完善日志系統(tǒng)

前言:

日志系統(tǒng)可以說(shuō)是每個(gè)程序員都繞不開的話題。在大型C++項(xiàng)目中,日志打印和記錄幾乎是日常開發(fā)中最常用的功能之一 —— 排查Bug、追蹤調(diào)用鏈、監(jiān)控線上服務(wù)狀態(tài),都離不開一套好用的日志工具。

提到C++日志庫(kù),有一些人會(huì)想:日志系統(tǒng)到底是怎么實(shí)現(xiàn)的? 拋開spdlog這些現(xiàn)成的庫(kù),我們自己能不能從零手搓一個(gè)可用的日志組件出來(lái)?

其實(shí)手搓一個(gè)簡(jiǎn)易日志工具并不復(fù)雜,核心思路就是 將格式化后的信息輸出到不同的目的地,比如控制臺(tái)、文件等。在這個(gè)思路的指引下,我們可以一步步搭建自己的日志系統(tǒng),并順便搞懂:

  • 日志級(jí)別是怎么分類和管理的;
  • 日志消息如何格式化并分派到不同的輸出端;
  • 異步日志如何做到高性能;
  • RAII機(jī)制在日志系統(tǒng)中的應(yīng)用。

如果你曾對(duì)這些問題感到好奇,那這篇文章就是寫給你的。下面,我們就從零開始,手搓一個(gè)實(shí)用且可擴(kuò)展的C++日志工具。

一. 日志系統(tǒng)的設(shè)計(jì)理念

1.1 日志的核心組成要素

一條合格的工業(yè)級(jí)日志,必須包含必選字段可選擴(kuò)展字段,確保問題可追溯、狀態(tài)可監(jiān)控:

  • 必選核心字段
    • 時(shí)間戳:可讀性強(qiáng)的年月日時(shí)分秒格式,精準(zhǔn)定位事件發(fā)生時(shí)間
    • 日志等級(jí):區(qū)分事件嚴(yán)重程度,支持分級(jí)過濾與告警
    • 日志內(nèi)容:用戶自定義的業(yè)務(wù) / 調(diào)試信息
  • 可選擴(kuò)展字段
    • 進(jìn)程 PID / 線程 ID:多進(jìn)程 / 多線程環(huán)境下定位執(zhí)行流
    • 文件名與行號(hào):精準(zhǔn)定位日志打印的代碼位置
    • 自定義擴(kuò)展字段:如模塊名、用戶 ID 等業(yè)務(wù)信息

本文實(shí)現(xiàn)的日志格式如下,完全兼容主流日志庫(kù)的規(guī)范:

[2026-04-16 21:33:18] [DEBUG] [1030871] [Main.cc] [10] - hello world hello Cx330
         日期          日志等級(jí)    pid     源文件   行號(hào) -         內(nèi)容       root   

1.2 日志系統(tǒng)的兩大核心階段

日志的生命周期可拆分為兩個(gè)完全解耦的階段,這是我們?cè)O(shè)計(jì)的核心依據(jù):

  1. 日志形成階段:將時(shí)間戳、等級(jí)、文件名、行號(hào)、用戶內(nèi)容等信息,拼接成一條完整的格式化字符串,與日志輸出目的地?zé)o關(guān)
  2. 日志刷新階段:將格式化完成的日志字符串,寫入到指定目的地(控制臺(tái)、文件、數(shù)據(jù)庫(kù)、網(wǎng)絡(luò)等),僅關(guān)注寫入邏輯

兩個(gè)階段解耦后,我們可以獨(dú)立擴(kuò)展刷新邏輯,而無(wú)需修改日志格式化的核心代碼,這正是策略模式的最佳應(yīng)用場(chǎng)景。

1.3 為什么選擇策略模式?

  • 分離關(guān)注點(diǎn):日志類(上下文)只負(fù)責(zé)構(gòu)建格式化后的日志消息,具體的“寫到哪里”交由獨(dú)立的對(duì)象完成。
  • 運(yùn)行時(shí)切換:程序運(yùn)行時(shí)可隨時(shí)切換日志策略,比如調(diào)試階段用控制臺(tái)輸出,生產(chǎn)環(huán)境用文件持久化。
  • 擴(kuò)展性極佳:新增一種輸出方式(比如 UDP 網(wǎng)絡(luò)發(fā)送),只需實(shí)現(xiàn)一個(gè)新的類,日志類完全不需要改動(dòng)。
  • 支持組合:日志類可以持有多個(gè)對(duì)象,一條日志同時(shí)送給控制臺(tái)、文件、遠(yuǎn)程服務(wù)器,這正是策略模式在集合層面的靈活運(yùn)用。

二、代碼設(shè)計(jì):實(shí)現(xiàn)一個(gè)完整的日志庫(kù)

日志系統(tǒng)的核心前提是線程安全,同時(shí)需要時(shí)間戳、日志等級(jí)等基礎(chǔ)能力支撐,我們先實(shí)現(xiàn)這些底層模塊。

2.1 RAII 風(fēng)格互斥鎖封裝(線程安全基石)

多線程環(huán)境下,控制臺(tái)、日志文件都是臨界資源,多個(gè)線程同時(shí)寫入會(huì)導(dǎo)致內(nèi)容交錯(cuò)、亂序,必須通過互斥量保證臨界區(qū)的原子性。我們基于 Linux 原生的pthread_mutex封裝互斥鎖,并通過 RAII 機(jī)制管理鎖的生命周期,避免手動(dòng)解鎖導(dǎo)致的死鎖、內(nèi)存泄漏問題,這也是 C++11 std::lock_guard的核心實(shí)現(xiàn)原理。

  • Mutex.hpp
#ifndef __MUTEX_HPP
#define __MUTEX_HPP
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock,nullptr);
    }    
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    pthread_mutex_t *Orgin()
    {
        return &_lock;
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};
// 鎖的開關(guān)
class LockGuard
{
public:
    LockGuard(Mutex *lockp):_lockp(lockp)
    {
        _lockp->Lock();
    }
    ~LockGuard()
    {
        _lockp->Unlock();
    }
private:
    Mutex *_lockp;
};
#endif

核心設(shè)計(jì)解析

  • 禁用拷貝:互斥量是系統(tǒng)資源,不允許拷貝和賦值,避免重復(fù)釋放、死鎖等問題
  • RAII 機(jī)制:LockGuard在對(duì)象構(gòu)造時(shí)加鎖,析構(gòu)時(shí)自動(dòng)解鎖,即使代碼中途拋出異常,也能保證鎖被釋放,徹底避免手動(dòng)解鎖的遺漏
  • 接口封裝:屏蔽原生pthread庫(kù)的接口細(xì)節(jié),提供更符合 C++ 面向?qū)ο蟮氖褂梅绞?/li>

2.2 格式化時(shí)間戳模塊

時(shí)間戳是日志的核心字段,我們需要實(shí)現(xiàn)秒級(jí)、可重入、格式化的時(shí)間戳獲取功能。

重點(diǎn)注意:C 標(biāo)準(zhǔn)庫(kù)的localtime函數(shù)是不可重入的,多線程環(huán)境下會(huì)出現(xiàn)數(shù)據(jù)錯(cuò)亂,因此必須使用可重入版本localtime_r,它由調(diào)用者提供結(jié)構(gòu)體緩沖區(qū),避免了全局靜態(tài)變量的競(jìng)態(tài)問題。

  • 時(shí)間戳實(shí)現(xiàn)代碼(在命名空間里面)
// 獲取當(dāng)前時(shí)間的字符串表示(格式:YYYY-MM-DD HH:MM:SS)
std::string GetTimeStamp()
{
    // 獲取從1970-01-01 UTC到當(dāng)前時(shí)刻的秒數(shù)(Unix時(shí)間戳)
    time_t timestamp = time(nullptr);
    // 定義tm結(jié)構(gòu)體用于存儲(chǔ)分解后的本地時(shí)間
    struct tm data_time;
    // 將時(shí)間戳轉(zhuǎn)換為本地時(shí)間(線程安全版本,localtime_r是POSIX標(biāo)準(zhǔn))
    localtime_r(&timestamp, &data_time);
    // 緩沖區(qū),用于存放格式化后的時(shí)間字符串
    char data_time_str[128];
    // 使用snprintf進(jìn)行格式化,限制最大寫入長(zhǎng)度,防止溢出
    // 格式:年-月-日 時(shí):分:秒,各部分不足兩位時(shí)補(bǔ)零
    snprintf(data_time_str, sizeof(data_time_str), "%4d-%02d-%02d %02d:%02d:%02d",
             // tm_year 從1900年開始計(jì)數(shù),需要加1900得到實(shí)際年份
             data_time.tm_year + 1900,
             // tm_mon 范圍0~11,需要加1轉(zhuǎn)換為實(shí)際月份
             data_time.tm_mon + 1,
             data_time.tm_mday,   // 日(1~31)
             data_time.tm_hour,   // 小時(shí)(0~23)
             data_time.tm_min,    // 分鐘(0~59)
             data_time.tm_sec);   // 秒(0~60,閏秒時(shí)可達(dá)60)
    // 返回std::string對(duì)象,自動(dòng)拷貝緩沖區(qū)內(nèi)容
    return data_time_str;
}

細(xì)節(jié)解析

  • 可重入性保障:使用localtime_r替代localtime,確保多線程環(huán)境下時(shí)間轉(zhuǎn)換不會(huì)出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)
  • 格式化補(bǔ)零:通過%02d確保月、日、時(shí)、分、秒始終是兩位數(shù)字,保證日志格式的一致性
  • 時(shí)間偏移修正:tm_year需要 + 1900 得到真實(shí)年份,tm_mon需要 + 1 得到真實(shí)月份,這是tm結(jié)構(gòu)體的標(biāo)準(zhǔn)規(guī)范

  • 測(cè)試代碼
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;
// 測(cè)試時(shí)間戳模塊
void testTime()
{
    for(int i = 0; i < 5; i++)
    {
        std::cout << GetTimeStamp() << std::endl;
        sleep(1);
    }
}
int main()
{
    // 1. 測(cè)試時(shí)間
    testTime();
    return 0;
}

2.3 類型安全的日志等級(jí)模塊

日志等級(jí)用于區(qū)分事件的嚴(yán)重程度,我們使用 C++11 的enum class實(shí)現(xiàn)類型安全的日志等級(jí),避免普通枚舉的隱式類型轉(zhuǎn)換問題,同時(shí)提供枚舉到字符串的轉(zhuǎn)換能力。

  • 日志等級(jí)實(shí)現(xiàn)代碼(在命名空間里面,我沒帶上)
// 日志等級(jí)枚舉,用于區(qū)分事件的嚴(yán)重程度
enum LogLevel
{
    DEBUG,   // 調(diào)試信息,僅開發(fā)階段使用,生產(chǎn)環(huán)境通常關(guān)閉
    INFO,    // 常規(guī)信息,如服務(wù)啟動(dòng)、正常業(yè)務(wù)流轉(zhuǎn)
    WARNING, // 警告信息,表示潛在問題,但系統(tǒng)仍可正常運(yùn)行
    ERROR,   // 運(yùn)行時(shí)錯(cuò)誤,某個(gè)功能可能受損,需要關(guān)注
    FATAL    // 致命錯(cuò)誤,程序即將終止(如內(nèi)存分配失?。?
};
// 將日志等級(jí)枚舉轉(zhuǎn)換為對(duì)應(yīng)的字符串(用于日志輸出)
std::string LogLevel2String(LogLevel level)
{
    switch (level)
    {
    case LogLevel::DEBUG:
        return "DEBUG";     // 調(diào)試等級(jí)
    case LogLevel::INFO:
        return "INFO";      // 信息等級(jí)
    case LogLevel::WARNING:
        return "WARNING";   // 警告等級(jí)
    case LogLevel::ERROR:
        return "ERROR";     // 錯(cuò)誤等級(jí)
    case LogLevel::FATAL:
        return "FATAL";     // 致命等級(jí)
    default:
        return "UNKNOWN";   // 未匹配到的等級(jí)(防護(hù)性代碼)
    }
}

核心設(shè)計(jì)解析

  • 類型安全:enum class不會(huì)隱式轉(zhuǎn)換為整型,避免了錯(cuò)誤的等級(jí)賦值,編譯期即可發(fā)現(xiàn)類型問題
  • 等級(jí)分層:遵循業(yè)界通用的 5 級(jí)日志規(guī)范,覆蓋從調(diào)試到致命錯(cuò)誤的全場(chǎng)景
  • 字符串轉(zhuǎn)換:通過 switch 語(yǔ)句實(shí)現(xiàn)枚舉到字符串的映射,確保日志中輸出可讀性強(qiáng)的等級(jí)名稱,而非整型數(shù)字

  • 測(cè)試代碼
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;
// 測(cè)試日志類枚舉類型轉(zhuǎn)字符類型模塊
void testEnum()
{
    std::cout << LogLevel2String(LogLevel::DEBUG) << std::endl;
    std::cout << LogLevel2String(LogLevel::INFO) << std::endl;
    std::cout << LogLevel2String(LogLevel::WARNING) << std::endl;
    std::cout << LogLevel2String(LogLevel::ERROR) << std::endl;
    std::cout << LogLevel2String(LogLevel::FATAL) << std::endl;
}
int main()
{
    // 2. 測(cè)試枚舉類轉(zhuǎn)字符串類型
    testEnum();
    return 0;
}

三. 基于策略模式的日志刷新核心實(shí)現(xiàn)

基于策略模式的設(shè)計(jì),我們先定義抽象的刷新策略基類,再分別實(shí)現(xiàn)控制臺(tái)和文件兩種具體的刷新策略,后續(xù)可無(wú)限擴(kuò)展其他策略。

3.1 抽象策略基類 LogStrategy

抽象基類定義了所有刷新策略必須實(shí)現(xiàn)的純虛接口,同時(shí)使用虛析構(gòu)函數(shù)確保子類對(duì)象能正確析構(gòu)。

namespace LogModule 
{
    // 3. 刷新策略
    // 基類: 策略模式 (Strategy Pattern)
    // 核心思想:將“日志的產(chǎn)生”與“日志的刷新目的地”解耦。
    // 通過定義統(tǒng)一的接口,使得程序可以在運(yùn)行時(shí)動(dòng)態(tài)決定將日志輸出到控制臺(tái)、文件、數(shù)據(jù)庫(kù)或網(wǎng)絡(luò)。
    class LogStrategy
    {
    public:
        // 虛析構(gòu)函數(shù):在多態(tài)體系中,基類必須擁有虛析構(gòu)函數(shù)。
        // 這樣當(dāng)我們通過基類指針刪除派生類對(duì)象時(shí),才能確保調(diào)用到子類的析構(gòu)函數(shù),防止內(nèi)存泄漏。
        virtual ~LogStrategy() = default; // 不在這里析構(gòu)
        // 核心刷新接口:這是一個(gè)純虛函數(shù)。
        // 純虛函數(shù)的核心作用是定義一種“契約”,強(qiáng)制派生類(子類)必須實(shí)現(xiàn)具體的邏輯。
        // 不同的子類可以根據(jù)自己的策略(如 ConsoleStrategy 或 FileStrategy)來(lái)實(shí)現(xiàn)不同的刷新行為。
        virtual void SyncLog(const std::string &message) = 0; // 強(qiáng)制子類對(duì)其進(jìn)行重寫
    };
}

設(shè)計(jì)說(shuō)明

  • 純虛函數(shù)SyncLog定義了策略的統(tǒng)一接口,入?yún)⑹歉袷交瓿傻娜罩咀址宇愔恍桕P(guān)注具體的寫入邏輯
  • 虛析構(gòu)函數(shù)是 C++ 多態(tài)的基礎(chǔ)規(guī)范,避免通過基類指針釋放子類對(duì)象時(shí)出現(xiàn)內(nèi)存泄漏

3.2 控制臺(tái)日志策略 ConsoleLogStrategy

控制臺(tái)策略負(fù)責(zé)將日志輸出到標(biāo)準(zhǔn)錯(cuò)誤流(stderr),核心是保證多線程環(huán)境下的輸出原子性,避免日志交錯(cuò)。用到了我們自己的互斥鎖記得包含對(duì)應(yīng)頭文件,我這里就不寫了

namespace LogModule
{
    // 策略1: 控制臺(tái)日志策略
    // 子類:繼承自策略基類,用于將日志直接刷新到標(biāo)準(zhǔn)輸出(顯示器),常用于本地開發(fā)與調(diào)試 
    class ConsoleLogStrategy: public LogStrategy
    {
    public:
        // 構(gòu)造函數(shù)與析構(gòu)函數(shù):當(dāng)前策略不涉及復(fù)雜資源申請(qǐng),故使用默認(rèn)實(shí)現(xiàn)即可 
        ConsoleLogStrategy(){}
        ~ConsoleLogStrategy(){}
        /**
         * @brief 實(shí)現(xiàn)具體的日志同步邏輯——刷新到控制臺(tái)
         * @param message 組裝好的完整日志字符串
         */
        void SyncLog(const std::string &message) override // 檢查重寫的錯(cuò)誤
        {
            // 【核心原理】顯示器(stdout)在多線程環(huán)境下屬于“臨界資源”。
            // 如果不加保護(hù),多個(gè)線程同時(shí)調(diào)用 std::cout 會(huì)導(dǎo)致各條日志的字符在屏幕上發(fā)生“交織”或亂碼 。
            // 使用自定義的 LockGuard 配合互斥鎖,確保這一系列操作的原子性 。
            LockGuard logGuard(&_mutex);
            std::cout << message << std::endl;
        }
    private:
        // 互斥鎖:專門用于保護(hù)當(dāng)前控制臺(tái)輸出的原子性,防止并發(fā)打印時(shí)消息錯(cuò)亂 
        Mutex _mutex;
    };
}

核心細(xì)節(jié)解析

  • 線程安全保障:控制臺(tái)是全局臨界資源,通過互斥鎖保證同一時(shí)刻只有一個(gè)線程能執(zhí)行輸出操作,徹底避免多線程日志交錯(cuò)
  • stderr 輸出:使用std::cerr而非std::cout,因?yàn)?stderr 無(wú)緩沖區(qū),日志會(huì)實(shí)時(shí)輸出,避免程序崩潰時(shí)緩沖區(qū)日志丟失
  • RAII 鎖管理:使用LockGuard自動(dòng)管理鎖,無(wú)需手動(dòng)解鎖,代碼更健壯

    3.3 文件日志策略 FileLogStrategy

    文件策略負(fù)責(zé)將日志持久化到磁盤文件,核心功能包括:自動(dòng)創(chuàng)建日志目錄、追加模式寫入、線程安全保障,使用 C++17 的filesystem庫(kù)處理目錄和文件操作(記得帶上對(duì)應(yīng)頭文件)。

    #include <fstream>
    #include <filesystem>
    
    namespace LogModule 
    {
        // 定義全局默認(rèn)路徑與文件名常量 
        const static std::string gdefaultlogdir = "./log/";
        const static std::string gdefaultlogfilename = "log.txt";
    
        // 策略2:文件類日志策略
        // 子類:繼承自策略基類,實(shí)現(xiàn)將日志持久化到磁盤文件的邏輯 
        class FileLogStrategy: public LogStrategy
        {
        public:
            /**
             * @brief 構(gòu)造函數(shù):初始化日志路徑并確保目錄環(huán)境就緒
             * @param logdir 日志存儲(chǔ)目錄
             * @param logfilename 日志文件名稱
             */
            FileLogStrategy(const std::string &logdir = gdefaultlogdir, const std::string &logfilename = gdefaultlogfilename)
                :_logdir(logdir),
                _logfilename(logfilename)
            {
                // 【重點(diǎn)】構(gòu)造階段即進(jìn)行加鎖保護(hù)。
                // 理由:判斷目錄是否存在并創(chuàng)建目錄屬于“先檢查再執(zhí)行(Check-Then-Act)”模式,
                // 必須保證這一系列操作的原子性,防止多線程同時(shí)創(chuàng)建導(dǎo)致競(jìng)態(tài)沖突 。
                LockGuard lockGuard(&_mutex);
                
                // 使用 C++17 的 <filesystem> 庫(kù)進(jìn)行跨平臺(tái)路徑檢查 
                if(std::filesystem::exists(_logdir))
                {
                    return;
                }
                else 
                {
                    try 
                    {
                        // 遞歸創(chuàng)建目錄(類似于 Linux 命令 mkdir -p),如果路徑中包含多級(jí)不存在的目錄會(huì)一并創(chuàng)建 
                        std::filesystem::create_directories(_logdir);
                    } 
                    catch (std::filesystem::filesystem_error &e) 
                    {
                        // 捕獲文件系統(tǒng)異常(如權(quán)限不足、磁盤空間不足等)并輸出錯(cuò)誤信息 
                        std::cerr << e.what() << std::endl;
                    }
                }
            }
    
            // 析構(gòu)函數(shù):由于不涉及手動(dòng)管理的堆內(nèi)存或特殊文件句柄(使用局部變量流管理),故使用默認(rèn)實(shí)現(xiàn)
            ~FileLogStrategy(){}
    
            /**
             * @brief 執(zhí)行具體的日志落盤操作
             * @param message 待寫入的完整日志字符串
             */
            void SyncLog(const std::string &message) override
            {
                // 加鎖保護(hù):防止多線程同時(shí)寫入同一文件導(dǎo)致內(nèi)容交織(Interleaving)亂碼 
                LockGuard logGuard(&_mutex);
                
                // 構(gòu)造完整的目標(biāo)文件路徑
                std::string target = _logdir + _logfilename;
                
                // 以追加模式(std::ios::app)打開文件流:
                // 核心邏輯:保證每條新日志都寫在文件末尾,不會(huì)覆蓋已有日志內(nèi)容 。
                std::ofstream out(target, std::ios::app); // 追加
    
                if(!out.is_open()) // 打開文件檢查
                {
                    return; // 如果因權(quán)限或路徑問題打開失敗,則放棄本次寫入,防止程序崩潰
                }
                
                // 將消息流式寫入文件,并手動(dòng)添加換行符以符合日志排版規(guī)范 
                out << message << "\n"; // 流式寫入
                
                // 文件流離開作用域或顯式調(diào)用 close 會(huì)自動(dòng)觸發(fā)刷新并關(guān)閉文件
                out.close();
            }
    
        private:
            std::string _logdir;      // 存儲(chǔ)目錄路徑
            std::string _logfilename; // 存儲(chǔ)文件名稱
            Mutex _mutex;             // 用于保障當(dāng)前策略類實(shí)例在多線程環(huán)境下的線程安全 
        };
    }
    

    核心設(shè)計(jì)解析

    • 自動(dòng)目錄創(chuàng)建:構(gòu)造函數(shù)中檢查日志目錄是否存在,不存在則通過create_directories遞歸創(chuàng)建,避免手動(dòng)創(chuàng)建目錄的繁瑣
    • 追加模式寫入:使用std::ios::app打開文件,所有日志都會(huì)追加到文件末尾,不會(huì)覆蓋歷史日志,符合日志系統(tǒng)的通用規(guī)范
    • 異常處理:目錄創(chuàng)建時(shí)捕獲filesystem的異常,避免目錄創(chuàng)建失敗導(dǎo)致程序崩潰
    • 線程安全:文件寫入全程加鎖,保證多線程環(huán)境下不會(huì)出現(xiàn)半行日志、內(nèi)容交錯(cuò)的問題

    測(cè)試代碼

    四. 日志主體類與流式輸出設(shè)計(jì)

    完成基礎(chǔ)模塊和策略模式的實(shí)現(xiàn)后,我們來(lái)實(shí)現(xiàn)日志系統(tǒng)的主體類,核心目標(biāo)是:兼容 glog 的流式調(diào)用風(fēng)格、自動(dòng)拼接日志元信息、RAII 自動(dòng)觸發(fā)日志刷新

    4.1 Logger 主類的整體架構(gòu)

    Logger類是日志系統(tǒng)的對(duì)外入口,核心職責(zé)包括:

    • 管理當(dāng)前使用的日志刷新策略,支持動(dòng)態(tài)切換
    • 提供仿函數(shù)接口,生成日志消息對(duì)象
    • 封裝策略切換的便捷接口

    4.2 LogMessage 內(nèi)部類:RAII 實(shí)現(xiàn)日志自動(dòng)刷新

    LogMessageLogger的內(nèi)部類,是整個(gè)日志系統(tǒng)最巧妙的設(shè)計(jì):

    • 構(gòu)造函數(shù)中完成日志元信息(時(shí)間、等級(jí)、PID、文件名、行號(hào))的拼接
    • 重載 <<運(yùn)算符,支持流式拼接任意類型的日志內(nèi)容
    • 析構(gòu)函數(shù)中自動(dòng)觸發(fā)日志刷新,利用臨時(shí)對(duì)象的生命周期實(shí)現(xiàn) “寫完即刷新”

    4.3 完整的 Logger 類實(shí)現(xiàn)

    #include <memory>
    #include <sstream>
    #include <unistd.h>
    
    namespace LogModule
    {
        // 真正要的日志類
        class Logger
        {
        public: 
            Logger()
            {
                UseConsoleLogStrategy();
            }
            ~Logger(){}
            // 顯示器的刷新策略
            void UseConsoleLogStrategy()
            {
                _strategy = std::make_unique<ConsoleLogStrategy>();
            }
            // 文件的刷新策略
            void UseFileLogStrategy()
            {
                _strategy = std::make_unique<FileLogStrategy>();
            }
            // 內(nèi)部類:一條日志
            // 目標(biāo)是把一個(gè)類對(duì)象,變成一個(gè)string
            class LogMessage
            {
            public:
                LogMessage(LogLevel level,std::string &filename,int line,Logger &self)
                    :_level(level),
                     _curr_time(GetTimeStamp()),
                     _pid(getpid()),
                     _filename(filename),
                     _line(line),
                     _logger(self)
                {   
                    std::stringstream ss; 
                    ss << "[" << _curr_time << "] "
                       << "[" << LogLevel2String(_level) << "] "
                       << "[" << _pid << "] "
                       << "[" << _filename << "] "
                       << "[" << _line << "] "
                       << "- ";
                    _loginfo = ss.str();
                }
                template<typename T>
                LogMessage &operator << (const T &info) 
                {
                    std::stringstream ss;
                    ss << info;
                    _loginfo += ss.str();
                    return  *this;
                }
                ~LogMessage() // RAII風(fēng)格的日志刷新
                {
                    if(_logger._strategy)
                    {
                        _logger._strategy->SyncLog(_loginfo);
                    }
                }
            private:
                LogLevel _level;         // 日志等級(jí)
                std::string _curr_time;  // 當(dāng)前時(shí)間
                pid_t _pid;              // 進(jìn)程pid
                std::string _filename;   // 文件名
                int _line;               // 行號(hào)
                std::string _loginfo;    // 一條完整的日志
                Logger &_logger;         // 外部類的引用
            };
            // LogMessage 對(duì)象打印日志的時(shí)候,故意返回一個(gè)臨時(shí)的 LogMessage對(duì)象
            // 為什么要返回臨時(shí)內(nèi)部類對(duì)象?
            LogMessage operator()(LogLevel level,std::string filename, int line)
            {
                return LogMessage(level,filename,line,*this);
            }
        private:
            std::unique_ptr<LogStrategy> _strategy; // 刷新日志的策略
        };
        Logger logger;
        // 使用宏,包裝我們的日志打印過程,宏有一個(gè)特點(diǎn),#define A B,B替換成A
        #define LOG(level) logger(level,__FILE__,__LINE__)
        // 動(dòng)態(tài)調(diào)整日志策略
        #define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
        #define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy()
    }

    核心設(shè)計(jì)深度解析

    • 仿函數(shù)機(jī)制:重載operator()Logger對(duì)象可以像函數(shù)一樣調(diào)用,返回一個(gè)LogMessage臨時(shí)對(duì)象,這是實(shí)現(xiàn)流式調(diào)用的核心
    • RAII 自動(dòng)刷新:LogMessage是臨時(shí)對(duì)象,當(dāng)整條LOG(xxx) << "xxx"語(yǔ)句執(zhí)行完畢后,臨時(shí)對(duì)象會(huì)被析構(gòu),析構(gòu)函數(shù)中自動(dòng)調(diào)用策略的刷新接口,無(wú)需用戶手動(dòng)觸發(fā)刷新
    • 模板化流式運(yùn)算符:通過模板重載<<運(yùn)算符,支持 int、double、string、char 等任意可流輸出的類型,實(shí)現(xiàn)和std::cout一致的使用體驗(yàn)

    預(yù)定義宏封裝:

    • __FILE__:編譯期自動(dòng)替換為當(dāng)前源文件名
    • __FILE__:編譯期自動(dòng)替換為當(dāng)前代碼行號(hào)
    • LOG宏將繁瑣的參數(shù)傳遞封裝為極簡(jiǎn)的調(diào)用方式,完全對(duì)齊 glog 的使用風(fēng)格

    全局單例:定義全局的logger對(duì)象,整個(gè)程序共用一個(gè)日志實(shí)例,避免重復(fù)創(chuàng)建,同時(shí)保證策略切換全局生效

    五. 日志系統(tǒng)的線程安全與可重入性深度解析

    六、日志系統(tǒng)源碼

    6.1 完整Logger.hpp代碼

    #ifndef __LOGGER_HPP
    #define __LOGGER_HPP
    #include <iostream>
    #include <cstdio>
    #include <string>
    #include <ctime>
    #include <filesystem> // C++17
    #include "Mutex.hpp"
    #include <fstream>
    #include <sstream>
    #include <memory>
    #include <unistd.h>
    namespace LogModule
    {
        // 1.獲取時(shí)間
        std::string GetTimeStamp()
        {
            time_t timestamp = time(nullptr);
            struct tm data_time;
            localtime_r(&timestamp, &data_time);
            char data_time_str[128];
            snprintf(data_time_str, sizeof(data_time_str), "%4d-%02d-%02d %02d:%02d:%02d",
                     data_time.tm_year + 1900, // 從1900開始記的
                     data_time.tm_mon + 1,     // 默認(rèn)月份從0開始記的
                     data_time.tm_mday,
                     data_time.tm_hour,
                     data_time.tm_min,
                     data_time.tm_sec);
            return data_time_str;
        }
        enum LogLevel
        {
            DEBUG,
            INFO,
            WARNING,
            ERROR,
            FATAL
        };
        // 2.日志等級(jí)
        std::string LogLevel2String(LogLevel level)
        {
            switch (level)
            {
            case LogLevel::DEBUG: 
                return "DEBUG";
            case LogLevel::INFO: 
                return "INFO";
            case LogLevel::WARNING: 
                return "WARNING";
            case LogLevel::ERROR: 
                return "ERROR";
            case LogLevel::FATAL: 
                return "FATAL";
            default:
                return "UNKNOWN";
            }
        }
        // 3.日志刷新
        // 基類:策略基類,設(shè)置刷新策略的
        class LogStrategy
        {
        public:
            virtual ~LogStrategy() = default;
            virtual void SyncLog(const std::string &logmessage) = 0;
        };
        // 子類:繼承純虛接口類
        // 策略1
        class ConsoleLogStrategy : public LogStrategy
        {
        public:
            ConsoleLogStrategy(){}
            ~ConsoleLogStrategy(){}
            virtual void SyncLog(const std::string &logmessage) override
            {
                LockGuard lockguard(&_mutex); 
                std::cout<<logmessage<<std::endl;
            }
        private:
            Mutex _mutex;
        };
        static const std::string glogdir = "./log/";
        static const std::string glogfilename = "log.log";
        // 子類:繼承純虛接口類
        // 策略2
        class FileLogStrategy : public LogStrategy
        {
        public:
            FileLogStrategy(const std::string &dir = glogdir,const std::string &filename = glogfilename)
                :_logdir(dir),_logfilename(filename)
            {
                // log/log.txt
                LockGuard lockguard(&_mutex);
                if(std::filesystem::exists(_logdir))
                {
                    return;
                }
                else
                {
                    try
                    {
                        std::filesystem::create_directories(_logdir);
                    }
                    catch (const std::filesystem::filesystem_error &e)
                    {
                        std::cerr<< e.what() <<std::endl;
                    }
                }
            }
            ~FileLogStrategy()
            {}
            void SyncLog(const std::string &logmessage) override
            {
                std::string target = _logdir + _logfilename;
                std::ofstream out(target,std::ios::app); // 追加寫入文件
                if(!out.is_open())
                {
                    return;
                }
                // 方法1:
                // out.write(logmessage.c_str(), logmessage.size());
                // out.write("\n", 1); // 寫入換行符
                // 方法2:
                // std::string line = logmessage + '\n';
                // out.write(line.c_str(), line.size());
                // 方法3:
                out << logmessage << '\n';
                out.close();
            }
        private:
            std::string _logdir;
            std::string _logfilename; // ./log/XXX.log
            Mutex _mutex;
        };
        // 真正要的日志類
        class Logger
        {
        public: 
            Logger()
            {
                UseConsoleLogStrategy();
            }
            ~Logger(){}
            // 顯示器的刷新策略
            void UseConsoleLogStrategy()
            {
                _strategy = std::make_unique<ConsoleLogStrategy>();
            }
            // 文件的刷新策略
            void UseFileLogStrategy()
            {
                _strategy = std::make_unique<FileLogStrategy>();
            }
            // 內(nèi)部類:一條日志
            // 目標(biāo)是把一個(gè)類對(duì)象,變成一個(gè)string
            class LogMessage
            {
            public:
                LogMessage(LogLevel level,std::string &filename,int line,Logger &self)
                    :_level(level),
                     _curr_time(GetTimeStamp()),
                     _pid(getpid()),
                     _filename(filename),
                     _line(line),
                     _logger(self)
                {   
                    std::stringstream ss; 
                    ss << "[" << _curr_time << "] "
                       << "[" << LogLevel2String(_level) << "] "
                       << "[" << _pid << "] "
                       << "[" << _filename << "] "
                       << "[" << _line << "] "
                       << "- ";
                    _loginfo = ss.str();
                }
                template<typename T>
                LogMessage &operator << (const T &info) 
                {
                    std::stringstream ss;
                    ss << info;
                    _loginfo += ss.str();
                    return  *this;
                }
                ~LogMessage() // RAII風(fēng)格的日志刷新
                {
                    if(_logger._strategy)
                    {
                        _logger._strategy->SyncLog(_loginfo);
                    }
                }
            private:
                LogLevel _level;         // 日志等級(jí)
                std::string _curr_time;  // 當(dāng)前時(shí)間
                pid_t _pid;              // 進(jìn)程pid
                std::string _filename;   // 文件名
                int _line;               // 行號(hào)
                std::string _loginfo;    // 一條完整的日志
                Logger &_logger;         // 外部類的引用
            };
            // LogMessage 對(duì)象打印日志的時(shí)候,故意返回一個(gè)臨時(shí)的 LogMessage對(duì)象
            // 為什么要返回臨時(shí)內(nèi)部類對(duì)象?
            LogMessage operator()(LogLevel level,std::string filename, int line)
            {
                return LogMessage(level,filename,line,*this);
            }
        private:
            std::unique_ptr<LogStrategy> _strategy; // 刷新日志的策略
        };
        Logger logger;
        // 使用宏,包裝我們的日志打印過程,宏有一個(gè)特點(diǎn),#define A B,B替換成A
        #define LOG(level) logger(level,__FILE__,__LINE__)
        // 動(dòng)態(tài)調(diào)整日志策略
        #define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
        #define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy()
    }
    #endif

    6.2 完整測(cè)試代碼

    #include <iostream>
    #include <unistd.h>
    #include <pthread.h>
    #include "Logger.hpp" // 我們實(shí)現(xiàn)的日志頭文件
    using namespace LogModule;
    // 多線程測(cè)試函數(shù):10個(gè)線程同時(shí)打印日志
    void *thread_log_test(void *arg)
    {
        char *thread_name = (char *)arg;
        for (int i = 0; i < 5; i++)
        {
            LOG(LogLevel::INFO) << thread_name << " 執(zhí)行日志打印, 循環(huán)次數(shù): " << i;
            usleep(1000);
        }
        return nullptr;
    }
    int main()
    {
        // 1. 基礎(chǔ)控制臺(tái)日志輸出
        std::cout << "===== 控制臺(tái)日志測(cè)試 =====" << std::endl;
        ENABLE_CONSOLE_LOG_STRATEGY();
        LOG(LogLevel::DEBUG) << "這是DEBUG調(diào)試日志, 數(shù)值: " << 3.14159;
        LOG(LogLevel::INFO) << "這是INFO常規(guī)日志, 服務(wù)啟動(dòng)成功";
        LOG(LogLevel::WARNING) << "這是WARNING警告日志, 配置缺失, 使用默認(rèn)值";
        LOG(LogLevel::ERROR) << "這是ERROR錯(cuò)誤日志, 文件讀取失敗";
        LOG(LogLevel::FATAL) << "這是FATAL致命日志, 內(nèi)存耗盡, 服務(wù)退出";
        // 2. 切換為文件日志策略
        std::cout << "\n===== 文件日志測(cè)試 =====" << std::endl;
        ENABLE_FILE_LOG_STRATEGY();
        LOG(LogLevel::INFO) << "切換為文件日志策略, 日志將持久化到./log/log.txt";
        LOG(LogLevel::DEBUG) << "文件日志測(cè)試, 支持鏈?zhǔn)狡唇? " << "字符串 " << 1234 << " 浮點(diǎn)數(shù) " << 2.71828;
        // 3. 多線程線程安全測(cè)試
        std::cout << "\n===== 多線程日志測(cè)試 =====" << std::endl;
        pthread_t t1, t2, t3, t4;
        pthread_create(&t1, nullptr, thread_log_test, (void *)"thread-1");
        pthread_create(&t2, nullptr, thread_log_test, (void *)"thread-2");
        pthread_create(&t3, nullptr, thread_log_test, (void *)"thread-3");
        pthread_create(&t4, nullptr, thread_log_test, (void *)"thread-4");
        // 等待所有線程執(zhí)行完畢
        pthread_join(t1, nullptr);
        pthread_join(t2, nullptr);
        pthread_join(t3, nullptr);
        pthread_join(t4, nullptr);
        LOG(LogLevel::INFO) << "多線程日志測(cè)試完成, 無(wú)亂序、無(wú)交錯(cuò)";
        return 0;
    }

    6.3 優(yōu)化方向

    • 修復(fù)策略切換與日志記錄的數(shù)據(jù)競(jìng)爭(zhēng) – 對(duì) Logger::_strategy 的讀寫加鎖(如 shared_mutex + shared_ptr),避免多線程切換策略時(shí)崩潰。
    • 文件策略避免每次寫入打開/關(guān)閉文件 – 在 FileLogStrategy 構(gòu)造時(shí)持有一個(gè) ofstream 對(duì)象,減少系統(tǒng)調(diào)用開銷。
    • 增加日志等級(jí)過濾 – 在 LOG 宏或 LogMessage 構(gòu)造前檢查等級(jí)閾值,避免無(wú)效的字符串構(gòu)造。
    • 優(yōu)化字符串拼接效率 – 使用 ostringstream 成員變量或 fmt 庫(kù),減少臨時(shí)對(duì)象與內(nèi)存分配。
    • 單例化全局 Logger – 采用 Meyers Singleton,解決靜態(tài)初始化順序問題。
    • 完善文件策略的目錄創(chuàng)建 – 使用 std::call_once 或程序啟動(dòng)時(shí)統(tǒng)一創(chuàng)建,避免多實(shí)例并發(fā)創(chuàng)建目錄的競(jìng)態(tài)。
    • 規(guī)范異常處理與降級(jí)策略 – 文件打開失敗時(shí)記錄到 stderr 或拋異常,避免靜默丟失日志。
    • 支持異步日志(高吞吐場(chǎng)景) – 引入后臺(tái)線程與無(wú)鎖隊(duì)列,解耦業(yè)務(wù)線程與 I/O 操作。
    • 增強(qiáng)可重入性與信號(hào)安全 – 提供信號(hào)安全的專用接口(如 write 系統(tǒng)調(diào)用),或明確禁止在信號(hào)處理函數(shù)中使用。
    • 支持自定義日志格式 – 抽象 Formatter 接口,允許用戶定制輸出布局。

    寫在最后

    至此,我們完整剖析了一個(gè)現(xiàn)代 C++ 流式日志系統(tǒng)的核心設(shè)計(jì):從日志等級(jí)的枚舉定義,到策略模式解耦輸出目標(biāo),再到 RAII + 臨時(shí)對(duì)象實(shí)現(xiàn)的自動(dòng)刷新,以及線程安全與可重入性的深度考量。

    這個(gè)日志庫(kù)雖然只有短短幾百行代碼,卻凝聚了策略模式、RAII、智能指針、臨時(shí)對(duì)象生命周期、流式接口、宏與預(yù)定義標(biāo)識(shí)符等 C++ 關(guān)鍵思想。更重要的是,通過分析它的線程安全漏洞和性能瓶頸,我們更能理解并發(fā)編程的復(fù)雜性以及工程落地必須權(quán)衡的取舍。

    當(dāng)然,任何代碼都不是完美的。你已看到它的十項(xiàng)優(yōu)化方向——從修復(fù)數(shù)據(jù)競(jìng)爭(zhēng)到支持異步日志,每一點(diǎn)改進(jìn)都能讓這個(gè)輪子更滾得更遠(yuǎn)。如果你正在為自己項(xiàng)目的日志組件苦惱,不妨從這份代碼出發(fā),增加等級(jí)過濾、持久的文件流、可配置格式等特性,打造一個(gè)真正生產(chǎn)就緒的日志庫(kù)。

    最后,感謝你跟隨本文深入到這個(gè)看似簡(jiǎn)單卻暗藏玄機(jī)的模塊中。日志是系統(tǒng)的“黑匣子”,好的日志設(shè)計(jì)能讓你在排查問題時(shí)事半功倍。希望這份解析能為你帶來(lái)實(shí)實(shí)在在的啟發(fā),也歡迎在評(píng)論區(qū)分享你的日志系統(tǒng)實(shí)踐或疑問。

    到此這篇關(guān)于從零實(shí)現(xiàn)一個(gè) C++ 輕量級(jí)日志系統(tǒng)原理與實(shí)踐指南的文章就介紹到這了,更多相關(guān) C++ 輕量級(jí)日志系統(tǒng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

    相關(guān)文章

    • C++編程中使用設(shè)計(jì)模式中的policy策略模式的實(shí)例講解

      C++編程中使用設(shè)計(jì)模式中的policy策略模式的實(shí)例講解

      這篇文章主要介紹了C++編程中使用設(shè)計(jì)模式中的policy策略模式的實(shí)例講解,文章最后對(duì)策略模式的優(yōu)缺點(diǎn)有一個(gè)簡(jiǎn)單的總結(jié),需要的朋友可以參考下
      2016-03-03
    • C++使用gtest框架編寫單元測(cè)試的教程詳解

      C++使用gtest框架編寫單元測(cè)試的教程詳解

      gtest 是 Google 開發(fā)的一個(gè)用于 C++ 的測(cè)試框架,廣泛應(yīng)用于編寫和運(yùn)行單元測(cè)試,并且支持任何類型的測(cè)試,而不僅僅是單元測(cè)試,本文本文給大家介紹了C++使用gtest框架編寫單元測(cè)試的教程,需要的朋友可以參考下
      2024-08-08
    • C++之構(gòu)造函數(shù)默認(rèn)值設(shè)置方式

      C++之構(gòu)造函數(shù)默認(rèn)值設(shè)置方式

      這篇文章主要介紹了C++之構(gòu)造函數(shù)默認(rèn)值設(shè)置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
      2023-08-08
    • Qt+Live555搭建RTSP服務(wù)器的方法步驟

      Qt+Live555搭建RTSP服務(wù)器的方法步驟

      本文主要介紹了Qt+Live555搭建RTSP服務(wù)器的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
      2023-07-07
    • 模擬實(shí)現(xiàn)C語(yǔ)言中的內(nèi)存管理

      模擬實(shí)現(xiàn)C語(yǔ)言中的內(nèi)存管理

      這篇文章主要內(nèi)容是模擬C語(yǔ)言中的內(nèi)存管理,需要的朋友可以參考下
      2015-07-07
    • C++ clock()解析如何使用時(shí)鐘計(jì)時(shí)的應(yīng)用

      C++ clock()解析如何使用時(shí)鐘計(jì)時(shí)的應(yīng)用

      本篇文章是對(duì)c++中的clock()函數(shù)進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下
      2013-06-06
    • C語(yǔ)言實(shí)現(xiàn)直角坐標(biāo)轉(zhuǎn)換為極坐標(biāo)的方法

      C語(yǔ)言實(shí)現(xiàn)直角坐標(biāo)轉(zhuǎn)換為極坐標(biāo)的方法

      這篇文章主要介紹了C語(yǔ)言實(shí)現(xiàn)直角坐標(biāo)轉(zhuǎn)換為極坐標(biāo)的方法,涉及C語(yǔ)言進(jìn)行三角函數(shù)與數(shù)值運(yùn)算相關(guān)操作技巧,需要的朋友可以參考下
      2017-09-09
    • C++vector的insert函數(shù)用法小結(jié)

      C++vector的insert函數(shù)用法小結(jié)

      std::vector::insert是C++中用于在指定位置插入元素的函數(shù),支持插入單個(gè)元素、多個(gè)相同元素、一個(gè)范圍的元素或初始化列表中的元素,插入操作可能會(huì)使插入點(diǎn)之后的迭代器失效,并且時(shí)間復(fù)雜度為O(n),本文介紹C++vector的insert函數(shù)用法小結(jié),感興趣的朋友一起看看吧
      2025-03-03
    • win10+VS2017+Cuda10.0環(huán)境配置詳解

      win10+VS2017+Cuda10.0環(huán)境配置詳解

      這篇文章主要介紹了win10+VS2017+Cuda10.0環(huán)境配置詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
      2020-08-08
    • C語(yǔ)言使用sizeof和strlen計(jì)算數(shù)組和指針大小

      C語(yǔ)言使用sizeof和strlen計(jì)算數(shù)組和指針大小

      sizeof()一般是用來(lái)求取?變量?或者?類型?所占內(nèi)存空間的大小,strlen()是一個(gè)庫(kù)函數(shù)是專門用來(lái)計(jì)算?字符串?長(zhǎng)度的,下面我們就來(lái)看看C語(yǔ)言如何使用sizeof和strlen計(jì)算數(shù)組和指針大小吧
      2023-11-11

    最新評(píng)論

    临江市| 封开县| 马龙县| 英山县| 南开区| 新蔡县| 永兴县| 九江县| 潼关县| 钦州市| 弥渡县| 天全县| 叶城县| 永寿县| 两当县| 广西| 金寨县| 安庆市| 长沙市| 雅安市| 太康县| 乌拉特前旗| 阳西县| 耿马| 玛沁县| 岫岩| 高陵县| 平武县| 德清县| 新乐市| 苗栗县| 潼关县| 全椒县| 府谷县| 海安县| 台中县| 高青县| 儋州市| 棋牌| 敦煌市| 岳池县|