Linux C++ 編程配置文件加載方法

前段時間研究了如何在Linux上讀取配置文件,基于手頭上的項目也成功實現了,主要通過在C++文件中用定義需從配置文件讀取的配置屬性為全局變量,然后通過讀取函數來更新配置屬性。
其中也附有日志等級函數的打印輸出。
————————————————————————

目錄

  1. 用宏定義配置文件的路徑
  2. 將配置屬性定義為全局變量
  3. 相關全局變量
  4. 配置文件讀取函數及其相關函數的初始化
  5. 函數的具體定義
  6. 配置文件作用方法
  7. 配置文件config.ini內容
    —————————————————————————

1. 用宏定義配置文件的路徑

//***********[START]相關宏定義[START]***************
//配置文件路徑
#define CONFIG_FILE_PATH "./config.ini"
//日志等級的宏定義
#define LOG_ERR -1
#define LOG_INF 0
#define LOG_DBG 1
//配置文件關鍵字長度限制
#define KEYVALLEN 256
//是(1)否(0)使用線程鎖,調試發現不使用鎖就不會有延遲
#define USE_PTHREAD_LOCK 0
//*************[END]相關宏定義[END]*****************

2. 將配置屬性定義為全局變量

//*****[START]從配置文件中獲取的全局變量及其默認值[START]******
//------日志配置參數------
//日志路徑,因為一般將結果保存在“result”文件夾中,所以要使用“result/***.log”的格式
char gconf_szLogPath[256] = "result/testResult.log";
//設置程序日志等級:1 表示打印所有(調試,一般,錯誤信息),0 表示打印運行(一般,錯誤)信息,-1 表示僅打印錯誤信息.
int gconf_nLogLevel = 1;
//設置程序日志大小上限(單位為bytes,0表示不設置),超過后日志清空,最大2,147,483,647,建議不要超過104,857,600(即100*1024*1024)
int gconf_lMaxLogSize = 104857600;
//程序開始運行時,日志是(1)否(0)清空
int gconf_nLogReset = 1;
//------其余配置參數------
//設置程序的運行時間,達到時間后終止程序,方便調試.(單位為秒,0表示一直運行),原為DURATION_TIME
int gconf_nProgramDurationTime = 0;
//*****[END]從配置文件中獲取的全局變量及其默認值[END]******

3. 相關全局變量

//********[START]相關全局變量[START]****************
//程序首次運行標記
bool g_bFirstRun = true;
//配置文件加載次數
int g_nCountLoadConfig = 0;
//用于獲取配置文件屬性
struct stat g_statConfigFile = {0};
//定義車牌及其數目容器
static vector<string> gs_szVecLicense;
static vector<int> gs_nVecCount;
//時間戳的全局變量
clock_t g_clockLast;
//線程ID
pthread_t g_pthreadRunIPCameraId;
//線程鎖
pthread_mutex_t mutex;
//**********[END]相關全局變量[END]******************

4. 配置文件讀取函數及其相關函數的初始化

//**********[START]函數的初始化[START]**************

//-------------結果目錄創建函數----------------
//-功能: 處理輸入的結果路徑,截取相應的目錄名,在工作目錄上創建相應目錄,用于保存結果.
int CreateDir(char* szPath,char* szDirPath);

//-------------日志打印函數--------------------
//-功能: 把日志信息保存到日志文件中,路徑為從配置文件中獲取的全局變量gconf_szLogPath所定義.
void PrintLOG(int nType,const char* ms, ... );

//-------------結果輸出函數--------------------
//-功能: 將結果識別結果存放到文本文件中,路徑為從配置文件中獲取的全局變量gconf_szPlateResultPath所定義.
void PrintResult(const char* ms, ... );

//---------配置文件讀取輔助函數,刪左空格-------
//-功能: 刪除從配置文件中讀取到的字符串的左空格.
char * l_trim(char * szOutput, const char *szInput);

//---------配置文件讀取輔助函數,刪右空格-------
//-功能: 刪除從配置文件中讀取到的字符串的右空格.
char *r_trim(char *szOutput, const char *szInput);

//---------配置文件讀取輔助函數,刪兩邊空格-------
//-功能: 刪除從配置文件中讀取到的字符串的兩邊空格.
char * a_trim(char * szOutput, const char * szInput);

//---------------配置文件檢查函數-------------
//-功能: 檢查配置文件是否被修改,如果被修改則返回1,沒變返回0,錯誤返回-1.
static int CheckConfigFileSizeTime();

//---------------配置值讀取函數----------------
//-功能: 從配置文件中讀取對應配置值.
int GetConfigValue(char *szLabelInfoName, char *szKeyName, char *szKeyVal );

//---------------配置值讀取函數(重載)---------
//-功能: 從配置文件中讀取對應配置值,為int型重載.
int GetConfigValue(char *szLabelInfoName, char *szKeyName, int *pnKeyVal );

//---------------配置文件作用函數--------------
//-功能: 從配置文件中讀取配置,并使之生效.
int LoadConfigFile();

//-------------配置文件重新作用函數------------
//-功能: 從配置文件中重新讀取配置,并通過線程的重啟來生效.
//       (此函數功能并未完善,程序也未調用此函數)
int ReloadConfigFile();

//************[END]函數的初始化[END]****************

5. 函數的具體定義


//**********[STAT]函數具體定義[STAT]**************

//-------------結果目錄創建函數----------------
//-功能: 處理輸入的結果路徑,截取相應的目錄名,在工作目錄上創建相應目錄,用于保存結果.
//-Input: 結果路徑
//-Output: 創建相應目錄
//-Return: 如果成功返回1,錯誤返回-1
//--------------------------------------------
int CreateDir(char* szPath,char* szDirPath)
{
    char* pTmpPathStart;
    char* pTmpPathEnd;
    pTmpPathStart = szPath;
    while(pTmpPathEnd = strchr(pTmpPathStart,'/'))
    {
        memset(szDirPath,0,sizeof(szDirPath));
        strncpy(szDirPath,szPath,(size_t)(pTmpPathEnd-szPath));
        pTmpPathStart = pTmpPathEnd + 1;
        //printf("%s\n",szDirPath);
        if(access(szDirPath,0)!=0)
        {
            if(mkdir(szDirPath,S_IRWXU)==-1)
            {
                //PrintLOG(LOG_INF,"創建相應目錄出錯:%d!請檢查路徑是否正確.",errno);
                return -1;
            }
        }

    }
    return 1;
}

//-------------日志打印函數--------------------
//-功能: 把日志信息保存到日志文件中,路徑為從配置文件中獲取的全局變量gconf_szLogPath所定義.
//-Input: 1.日志信息類型
//        2.與printf函數相似的格式化字符串
//-Output: 打印到日志文件的結果
//-Return: 無
//--------------------------------------------
void PrintLOG(int nLogType,const char* ms, ... )
{
    char wzLog[1024] = {0};
    char buffer[1024] = {0};
    char szNULL[256] = "";
    va_list args;
    va_start(args, ms);
    vsprintf( wzLog ,ms,args);
    va_end(args);

    struct stat statLogFile = {0};

    time_t now;
    time(&now);
    struct tm *local;
    struct timeval tv;
    local = localtime(&now);
    gettimeofday(&tv, NULL);

    //mkdir("result",S_IRWXU);
    CreateDir(gconf_szLogPath,szNULL);
    FILE* file = fopen(gconf_szLogPath,"a+");
    if(gconf_nLogReset == 1 && g_bFirstRun)
    {
        fclose(file);
        unlink(gconf_szLogPath);
        file = fopen(gconf_szLogPath,"a+");
        g_bFirstRun = false;
    }
    if (stat (gconf_szLogPath, &statLogFile) == -1)
    {
        PrintLOG(LOG_ERR,"獲取日志文件(%s)狀態失敗:%s.\n", gconf_szLogPath, strerror (errno));
        return;
    }
    if (S_ISDIR(statLogFile.st_mode))
    {
        PrintLOG(LOG_ERR,"此日志文件路徑(%s)是文件夾.錯誤代碼為:%s!", gconf_szLogPath, strerror (errno));
        return;
    }
    if (S_ISREG(statLogFile.st_mode))
    {
        ;//PrintLOG(LOG_INF,"日志文件(%s)大小為:%ld bytes, 最近修改時間為: %s.",gconf_szLogPath, statLogFile.st_size, ctime (&statLogFile.st_mtime));
    }
    if(statLogFile.st_size >= gconf_lMaxLogSize && gconf_lMaxLogSize != 0){
        fclose(file);
        //printf("日志文件已滿,程序終止/n.");
        //exit(1);
        unlink(gconf_szLogPath);
        file = fopen(gconf_szLogPath,"a+");
    }

    if(gconf_nLogLevel == 1){
        if(nLogType == 1){
            sprintf(buffer,"[%04d-%02d-%02d %02d:%02d:%02d.%03ld][DBG] %s\n",local->tm_year+1900, local->tm_mon,local->tm_mday, local->tm_hour, local->tm_min, local->tm_sec,tv.tv_usec/1000,wzLog);
        }
        if(nLogType == 0){
            sprintf(buffer,"[%04d-%02d-%02d %02d:%02d:%02d.%03ld][INF] %s\n",local->tm_year+1900, local->tm_mon,local->tm_mday, local->tm_hour, local->tm_min, local->tm_sec,tv.tv_usec/1000,wzLog);
        }
        if(nLogType == -1){
            sprintf(buffer,"[%04d-%02d-%02d %02d:%02d:%02d.%03ld][ERR] %s\n",local->tm_year+1900, local->tm_mon,local->tm_mday, local->tm_hour, local->tm_min, local->tm_sec,tv.tv_usec/1000,wzLog);
        }
    }
    if(nLogType <= gconf_nLogLevel){
        fwrite(buffer,1,strlen(buffer),file);
        printf("%s",buffer);
        fclose(file);
    }

    //  syslog(LOG_INFO,wzLog);
    return ;
}

//-------------結果輸出函數--------------------
//-功能: 將結果識別結果存放到文本文件中,路徑為從配置文件中獲取的全局變量gconf_szPlateResultPath所定義.
//-Input: 1.與printf函數相似的格式化字符串
//-Output: 打印到結果文件的車牌識別結果
//-Return: 無
//--------------------------------------------
void PrintResult(const char* ms, ... )
{
    char wzLog[1024] = {0};
    char buffer[1024] = {0};
    char szNULL[256] = "";
    va_list args;
    va_start(args, ms);
    vsprintf( wzLog ,ms,args);
    va_end(args);

    time_t now;
    time(&now);
    struct tm *local;
    struct timeval tv;
    local = localtime(&now);
    gettimeofday(&tv, NULL);
    sprintf(buffer,"[%04d-%02d-%02d %02d:%02d:%02d.%03ld]%s\n",local->tm_year+1900, local->tm_mon,local->tm_mday, local->tm_hour, local->tm_min, local->tm_sec,tv.tv_usec/1000,wzLog);
    printf("%s",buffer);

    //mkdir("result",S_IRWXU);
    CreateDir(gconf_szPlateResultPath,szNULL);

    FILE* file = fopen(gconf_szPlateResultPath,"a+");
    fwrite(buffer,1,strlen(buffer),file);
    fclose(file);

    return ;
}

//---------配置文件讀取輔助函數,刪左空格-------
//-功能: 刪除從配置文件中讀取到的字符串的左空格.
//-Input: 1.被處理后的字符串指針
//        2.待處理的字符串指正
//-Output: 處理完的字符串
//-Return: 字符拷貝函數的返回結果
//--------------------------------------------
char * l_trim(char * szOutput, const char *szInput)
{
    assert(szInput != NULL);
    assert(szOutput != NULL);
    assert(szOutput != szInput);
    for   (NULL; *szInput != '\0' && isspace(*szInput); ++szInput){
        ;
    }
    return strcpy(szOutput, szInput);
}

//---------配置文件讀取輔助函數,刪右空格-------
//-功能: 刪除從配置文件中讀取到的字符串的右空格.
//-Input: 1.被處理后的字符串指針
//        2.待處理的字符串指正
//-Output: 處理完的字符串
//-Return: 字符拷貝函數的返回結果
//--------------------------------------------
char *r_trim(char *szOutput, const char *szInput)
{
    char *p = NULL;
    assert(szInput != NULL);
    assert(szOutput != NULL);
    assert(szOutput != szInput);
    strcpy(szOutput, szInput);
    for(p = szOutput + strlen(szOutput) - 1; p >= szOutput && isspace(*p); --p){
        ;
    }
    *(++p) = '\0';
    return szOutput;
}

//---------配置文件讀取輔助函數,刪兩邊空格-------
//-功能: 刪除從配置文件中讀取到的字符串的兩邊空格.
//-Input: 1.被處理后的字符串指針
//        2.待處理的字符串指正
//-Output: 處理完的字符串
//-Return: 字符拷貝函數的返回結果
//--------------------------------------------
char * a_trim(char * szOutput, const char * szInput)
{
    char *p = NULL;
    assert(szInput != NULL);
    assert(szOutput != NULL);
    l_trim(szOutput, szInput);
    for   (p = szOutput + strlen(szOutput) - 1;p >= szOutput && isspace(*p); --p){
        ;
    }
    *(++p) = '\0';
    return szOutput;
}


//---------------配置文件檢查函數-------------
//-功能: 檢查配置文件是否被修改,如果被修改則返回1,沒變返回0,錯誤返回-1.
//-Input: 無
//-Output: 無
//-Return: 如果被修改則返回1,沒變返回0,錯誤返回-1
//--------------------------------------------
static int CheckConfigFileSizeTime ()
{
    struct stat statTmpConfigFile = {0};
    if (stat (CONFIG_FILE_PATH, &statTmpConfigFile) == -1)
    {
        PrintLOG(LOG_ERR,"獲取配置文件(%s)狀態失敗:%s.\n", CONFIG_FILE_PATH, strerror (errno));
        return (-1);
    }
    if (S_ISDIR(statTmpConfigFile.st_mode))
    {
        PrintLOG(LOG_ERR,"此配置文件路徑(%s)是文件夾. 錯誤代碼為:%s!", CONFIG_FILE_PATH, strerror (errno));
        return (-1);
    }
    if (S_ISREG (statTmpConfigFile.st_mode))
    {
        ;//PrintLOG(LOG_INF,"配置文件(%s)大小為:%ld bytes, 最近修改時間為:%s",CONFIG_FILE_PATH, statTmpConfigFile.st_size, ctime (&statTmpConfigFile.st_mtime));
    }
    //如果配置文件未被修改,則返回0
    if(statTmpConfigFile.st_size == g_statConfigFile.st_size && statTmpConfigFile.st_mtime == g_statConfigFile.st_mtime){
        return 0;
    }
    //如果配置文件被且是首次運行程序修改,則打印相應提示,且調用配置文件讀取函數讀取相應配置值
    else if(g_bFirstRun){
        PrintLOG(LOG_INF,"加載配置文件.");
        g_statConfigFile = statTmpConfigFile;
        return 1;
    }
    //如果配置文件被且不是首次運行程序修改,則打印提示,且調用配置文件讀取函數讀取相應配置值
    else{
        PrintLOG(LOG_INF,"重新加載被修改的配置文件.");
        g_statConfigFile = statTmpConfigFile;
        return 1;
    }
}

//---------------配置值讀取函數----------------
//-功能: 從配置文件中讀取對應配置值.
//-Input: 1.配置文件中待讀取的標簽信息名
//        2.配置文件中指定標簽中的待讀取關鍵字名
//        3.配置文件中指定標簽和關鍵字的對應配置值(char型指針)
//-Output: 相應配置值
//-Return: 如果讀取成功返回1,錯誤返回-1
//--------------------------------------------
int GetConfigValue(char *szLabelInfoName, char *szKeyName, char *szKeyVal )
{
    char szTmpLabelInfoName[32],szTmpKeyName[32];
    char *buf,*c;
    char buf_i[KEYVALLEN], buf_o[KEYVALLEN];
    FILE *fp;
    int found=0; /* 1 szLabelInfoName 2 szKeyName */
    if( (fp=fopen( CONFIG_FILE_PATH,"r" ))==NULL ){
        printf( "openfile [%s] error [%s]\n",CONFIG_FILE_PATH,strerror(errno) );
        return(-1);
    }
    fseek( fp, 0, SEEK_SET );
    memset( szTmpLabelInfoName, 0, sizeof(szTmpLabelInfoName) );
    sprintf( szTmpLabelInfoName,"[%s]", szLabelInfoName );

    while( !feof(fp) && fgets( buf_i, KEYVALLEN, fp )!=NULL ){
        l_trim(buf_o, buf_i);
        if( strlen(buf_o) <= 0 )
            continue;
        buf = NULL;
        buf = buf_o;

        if( found == 0 ){
            if( buf[0] != '[' ) {
                continue;
            } else if ( strncmp(buf,szTmpLabelInfoName,strlen(szTmpLabelInfoName))==0 ){
                found = 1;
                continue;
            }

        } else if( found == 1 ){
            if( buf[0] == '#' ){
                continue;
            } else if ( buf[0] == '[' ) {
                break;
            } else {
                if( (c = (char*)strchr(buf, '=')) == NULL )
                    continue;
                memset( szTmpKeyName, 0, sizeof(szTmpKeyName) );

                sscanf( buf, "%[^=|^ |^\t]", szTmpKeyName );
                if( strcmp(szTmpKeyName, szKeyName) == 0 ){
                    sscanf( ++c, "%[^\n]", szKeyVal );
                    char *szTmpKeyVal_o = (char *)malloc(strlen(szKeyVal) + 1);
                    if(szTmpKeyVal_o != NULL){
                        memset(szTmpKeyVal_o, 0, sizeof(szTmpKeyVal_o));
                        a_trim(szTmpKeyVal_o, szKeyVal);
                        if(szTmpKeyVal_o && strlen(szTmpKeyVal_o) > 0)
                            strcpy(szKeyVal, szTmpKeyVal_o);
                        free(szTmpKeyVal_o);
                        szTmpKeyVal_o = NULL;
                    }
                    found = 2;
                    break;
                } else {
                    continue;
                }
            }
        }
    }
    fclose( fp );
    if( found == 2 )
        return(1);
    else
        return(-1);
}

//---------------配置值讀取函數(重載)---------
//-功能: 從配置文件中讀取對應配置值,為int型重載.
//-Input: 1.配置文件中待讀取的標簽信息名
//        2.配置文件中指定標簽中的待讀取關鍵字名
//        3.配置文件中指定標簽和關鍵字的對應配置值(int型指針)
//-Output: 相應配置值
//-Return: 如果讀取成功返回1,錯誤返回-1
//--------------------------------------------
int GetConfigValue(char *szLabelInfoName, char *szKeyName, int *pnKeyVal )
{
    char szTmpLabelInfoName[32],szTmpKeyName[32],szTmpKeyVal[32];
    char *buf,*c;
    char buf_i[KEYVALLEN], buf_o[KEYVALLEN];
    FILE *fp;
    int found=0; /* 1 szLabelInfoName 2 szKeyName */
    if( (fp=fopen( CONFIG_FILE_PATH,"r" ))==NULL ){
        printf( "openfile [%s] error [%s]\n",CONFIG_FILE_PATH,strerror(errno) );
        return(-1);
    }
    fseek( fp, 0, SEEK_SET );
    memset( szTmpLabelInfoName, 0, sizeof(szTmpLabelInfoName) );
    sprintf( szTmpLabelInfoName,"[%s]", szLabelInfoName );

    while( !feof(fp) && fgets( buf_i, KEYVALLEN, fp )!=NULL ){
        l_trim(buf_o, buf_i);
        if( strlen(buf_o) <= 0 )
            continue;
        buf = NULL;
        buf = buf_o;

        if( found == 0 ){
            if( buf[0] != '[' ) {
                continue;
            } else if ( strncmp(buf,szTmpLabelInfoName,strlen(szTmpLabelInfoName))==0 ){
                found = 1;
                continue;
            }

        } else if( found == 1 ){
            if( buf[0] == '#' ){
                continue;
            } else if ( buf[0] == '[' ) {
                break;
            } else {
                if( (c = (char*)strchr(buf, '=')) == NULL )
                    continue;
                memset( szTmpKeyName, 0, sizeof(szTmpKeyName) );

                sscanf( buf, "%[^=|^ |^\t]", szTmpKeyName );
                if( strcmp(szTmpKeyName, szKeyName) == 0 ){
                    sscanf( ++c, "%[^\n]", szTmpKeyVal );
                    char *szTmpKeyVal_o = (char *)malloc(strlen(szTmpKeyVal) + 1);
                    if(szTmpKeyVal_o != NULL){
                        memset(szTmpKeyVal_o, 0, sizeof(szTmpKeyVal_o));
                        a_trim(szTmpKeyVal_o, szTmpKeyVal);
                        if(szTmpKeyVal_o && strlen(szTmpKeyVal_o) > 0)
                            strcpy(szTmpKeyVal, szTmpKeyVal_o);
                        free(szTmpKeyVal_o);
                        szTmpKeyVal_o = NULL;
                    }
                    found = 2;
                    break;
                } else {
                    continue;
                }
            }
        }
    }
    *pnKeyVal = atoi(szTmpKeyVal);
    fclose( fp );
    if( found == 2 )
        return(0);
    else
        return(-1);
}

//---------------配置文件作用函數--------------
//-功能: 從配置文件中讀取配置,并使之生效.
//-Input: 無
//-Output: 相應配置值
//-Return: 如果成功返回1,錯誤返回-1
//--------------------------------------------
int LoadConfigFile()
{
    bool bGetLogPath = false;
    bool bGetLogLevel = false;
    bool bGetMaxLogSize = false;
    bool bGetLogReset = false;

    int ret = 0;
    ret = CheckConfigFileSizeTime();
    if(ret != 1){
        return ret;
    }

    //從配置文件中獲取日志信息中的日志路徑
    if(GetConfigValue((char*)"Log_Info",(char*)"log_path",gconf_szLogPath) == -1){
        PrintLOG(LOG_ERR,"獲取日志路徑失敗!");
    }
    else
    {
        bGetLogPath = true;
        //PrintLOG(LOG_INF,"日志路徑為:%s",gconf_szLogPath);
    }
    //從配置文件中獲取日志信息中的程序日志級別
    if(GetConfigValue((char*)"Log_Info",(char*)"log_levle",&gconf_nLogLevel) == -1){
        PrintLOG(LOG_ERR,"獲取程序日志級別失敗!");
    }
    else
    {
        bGetLogLevel = true;
        //PrintLOG(LOG_INF,"程序日志級別為:%d",gconf_nLogLevel);
    }
    //從配置文件中獲取日志信息中的日志大小上限
    if(GetConfigValue((char*)"Log_Info",(char*)"log_max_size",&gconf_lMaxLogSize) == -1){
        PrintLOG(LOG_ERR,"獲取日志大小上限失敗!");
    }
    else
    {
        bGetMaxLogSize = true;
        //PrintLOG(LOG_INF,"日志大小上限為:%d",gconf_lMaxLogSize);
    }
    //從配置文件中獲取日志信息中的日志是(1)否(0)清空
    if(GetConfigValue((char*)"Log_Info",(char*)"log_reset",&gconf_nLogReset) == -1){
        PrintLOG(LOG_ERR,"獲取日志清空配置信息失敗!");
    }
    else
    {
        bGetLogReset = true;
        //PrintLOG(LOG_INF,"日志清空配置為:%d",gconf_nLogReset);
    }

    if(g_nCountLoadConfig == 0 &&
            bGetCameraIP && bGetUserName && bGetCameraPassword && bGetCameraPtzTurn && bGetCameraPtzSpeed &&
            bGetPtzTurnLevel && bGetAccuracyFrequency && bGetMaxPlateVectorSize && bGetIntervalOfClearPlateVector && bGetPlateResultPath &&
            bGetLogPath && bGetLogLevel && bGetMaxLogSize && bGetLogReset){
        g_nCountLoadConfig ++;
        PrintLOG(LOG_INF,"全部配置信息均從配置文件獲取.");
    }

    return 1;
}
//************[END]函數具體定義[END]****************

6. 配置文件作用方法

暫時是在主函數中設置循環,不斷調用配置文件讀取函數,使其不斷生效后執行相關操作。

7. 配置文件config.ini內容

[Log_Info]
#日志路徑,因為一般將結果保存在“result”文件夾中,所以要使用“result/***.log”的格式
log_path = result/testResult.log
#設置程序日志等級:1 表示打印所有(調試,一般,錯誤信息),0 表示打印運行(一般,錯誤)信息,-1 表示僅打印錯誤信息.
log_levle = 1
#設置程序日志大小上限(單位為bytes,0表示不設置),超過后日志清空,最大2147483647,建議不要超過104857600(100*1024*1024)
log_max_size = 104857600
#程序開始運行時,日志是(1)否(0)清空
log_reset = 1;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容