《C++ 工具库实战》gflags 命令行参数解析库详解

🔥小叶-duck:个人主页

❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》

《Linux系统从入门到实践》《Linux网络从入门到实践》

《Qt 方寸极境》 《MySQL》

✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

[一、gflags 介绍](#一、gflags 介绍)

[1.1 gflags 核心特性](#1.1 gflags 核心特性)

[1.2 gflags 安装](#1.2 gflags 安装)

[二、gflags 使用](#二、gflags 使用)

[2.1 包含头文件与编译链接](#2.1 包含头文件与编译链接)

[4.2 定义参数](#4.2 定义参数)

[4.3 访问参数](#4.3 访问参数)

[4.4 跨文件访问参数](#4.4 跨文件访问参数)

[4.5 初始化解析所有参数](#4.5 初始化解析所有参数)

[4.6 运行时命令行设置参数](#4.6 运行时命令行设置参数)

[4.6.1 string /int 类型参数](#4.6.1 string /int 类型参数)

[4.6.2 bool 类型参数](#4.6.2 bool 类型参数)

[4.7 配置文件的使用](#4.7 配置文件的使用)

[4.8 内置特殊参数标识](#4.8 内置特殊参数标识)

三、完整示例讲解

[3.1 main.cc](#3.1 main.cc)

[3.2 Makefile](#3.2 Makefile)

[3.3 main.conf(配置文件)](#3.3 main.conf(配置文件))

[3.4 编译与多种运行方式](#3.4 编译与多种运行方式)

[3.5 跨文件访问参数演示](#3.5 跨文件访问参数演示)

结束语


前言

在 C++ 服务端开发中,程序启动时经常需要传入端口、监听 IP、日志级别等各类配置参数。如果手动解析argc/argv,需要自己编写大量字符串判断、类型转换的逻辑,代码繁琐且容易出错。而 gflags 是谷歌开源的 C++ 命令行参数解析库,可以直接定义不同类型的参数,自动完成命令行解析,同时支持配置文件批量加载参数,大幅简化服务启动配置的开发工作。

本篇文章将从 gflags 基础特性、环境安装开始,一步步讲解参数定义、参数读取、跨文件访问参数、命令行传参、配置文件加载以及内置参数等核心用法。最后搭配完整工程示例,结合 Makefile 构建脚本,演示多种程序启动方式,帮助你掌握 gflags 在 C++ 项目中的落地使用。

一、gflags 介绍

gflags 是谷歌推出的开源 C++ 命令行参数解析库 ,专门用来完成命令行配置项的声明、定义与解析工作。借助 gflags ,我们可以很方便地为程序增加各类运行时配置参数,程序启动时通过命令行传入不同 flag,就能动态调整程序行为,无需修改源码重新编译。

1.1 gflags 核心特性

gflags具备下面这些核心特性:

  • 使用门槛低: gflags 对外暴露简洁清晰的 API,新增命令行参数只需要一行宏定义,不需要手动解析argc/argv,新增配置项十分轻松。
  • **自动生成帮助文档:**定义 flag 时可以附带说明文字,程序运行时支持自动输出帮助信息,使用者可以快速了解每个参数的含义与用法。
  • **类型安全:**支持布尔、整型、字符串等多种基础类型的配置项,库内部自带类型校验与类型转换逻辑,减少手动类型转换带来的错误。
  • **跨平台兼容:**可在 Linux、Windows、macOS 多操作系统下编译运行,跨平台项目可以统一使用这套命令行参数方案。
  • **良好的可扩展能力:**支持开发者自行扩展参数注册、解析逻辑,可根据业务需求做定制化封装。

1.2 gflags 安装

Ubuntu 直接命令安装:

bash 复制代码
sudo apt-get install libgflags-dev

CentOS 直接命令安装:

CentOS 系统可以通过 yum 包管理器直接安装开发库:

bash 复制代码
# CentOS7 / CentOS8
sudo yum install gflags-devel

说明: CentOS 的包名后缀是-devel,和 Ubuntu 的-dev命名习惯不一样。

源码安装(全平台通用,Ubuntu / CentOS 都可以用):

如果软件源自带版本过低,或者需要自定义编译选项,可以源码编译安装:

bash 复制代码
# 下载源码
git clone https://github.com/gflags/gflags.git
# 切换目录
cd gflags/
mkdir build
cd build/
# 生成Makefile
cmake ..
# 编译代码
make
# 安装
make install

补充提示:源码安装完成后,CentOS 如果出现链接找不到库的问题,需要更新动态链接库缓存:

bash 复制代码
sudo ldconfig

安装完成结果:

二、gflags 使用

2.1 包含头文件与编译链接

想要使用 gflags 库完成命令行参数定义与解析,首先要引入对应的头文件。

cpp 复制代码
#include <gflags/gflags.h>

编译程序时,需要手动链接 gflags 库:

bash 复制代码
g++ main.cc -o main -lgflags

为什么编译程序时,需要手动链接 gflags 库?

头文件只提供函数与类型声明 ,没有具体实现。gflags 是第三方库 ,它的代码实现存放在libgflags.so库文件中,C++ 编译器不会默认加载第三方库 ,需要通过-lgflags告知链接器去加载该库,否则链接阶段会报未定义引用错误。

4.2 定义参数

gflags 提供一系列宏 用来声明命令行参数 。这类宏一共接收三项参数 :命令行参数名称、参数默认值、该参数的说明文字。

cpp 复制代码
DEFINE_bool(reuse_addr, true, "是否开启网络地址重用选项");
DEFINE_int32(log_level, 1, "日志等级:1-DEBUG,2-WARN,3-ERROR");
DEFINE_string(log_file, "stdout", "日志输出位置设置,默认为标准输出");

gflags支持多种基础类型的参数定义,常用宏如下:

  • DEFINE_bool:布尔类型参数
  • DEFINE_int32:32 位整型参数
  • DEFINE_int64:64 位整型参数
  • DEFINE_uint64:无符号 64 位整型参数
  • DEFINE_double:浮点型参数
  • DEFINE_string:字符串类型参数

4.3 访问参数

参数定义完成后,代码中 可以直接使用 FLAGS_参数名 当作普通全局变量 读取参数。

以上面例子为例:

对于 reuse_addr 这个参数对应的普通全局变量 就是FLAGS_reuse_addr; log_level 对应的就是 FLAGS_log_level; log_file 对应的就是 **FLAGS_log_file,**即可获取命令行传入的值。

4.4 跨文件访问参数

如果需要在其他源文件 使用当前文件定义的 flag,不能重复 DEFINE,需要使用**DECLARE_*系列宏** 做外部声明 。

以reuse_addr为例:

cpp 复制代码
DECLARE_bool(reuse_addr);

DECLARE 宏 本质等价于extern声明(extern FLAGS_reuse_addr),告诉编译器这个全局变量在别的文件中定义,实现跨文件共享参数。

4.5 初始化解析所有参数

参数定义完毕后,需要在main 函数 调用解析接口 ,读取argc/argv,给所有FLAGS_*变量赋值。

cpp 复制代码
int main(int argc, char *argv[])
{
    google::ParseCommandLineFlags(&argc, &argv, true);
    return 0;
}
  • **argc、argv:**就是 main 函数自带的命令行参数。
  • 第三个参数 remove_flags :
    • **设置为true:**解析完成后,会从 argv 中移除已经识别的 flag 参数,同步修改 argc。
    • **设置为false:**保留 argv 全部内容,仅调整参数顺序,把识别到的 flag 放到前面。

所以,第三个参数不管是设置为 true 还是 false,都会对我们的 argv 数组进行修改,我们就根据实际需求进行设置即可。

如果我们没有特殊需求的话,这个参数设置哪个都可以,大多数情况只要我们能获取到所需要的参数即可。

4.6 运行时命令行设置参数

程序启动时,可以通过命令行 传入 flag,支持长短横线两种写法。

4.6.1 string /int 类型参数

bash 复制代码
# log_file和"./main.log"用等号连接
./exec --log_file="./main.log"
./exec -log_file="./main.log"
# 也可以不使用等号连接
./exec --log_file "./main.log"
./exec -log_file "./main.log"

4.6.2 bool 类型参数

布尔类型参数用法比较特殊,布尔类型参数不能 像上面的string /int 类型参数那样不使用等号连接,而不选择等号连接的写法是:不带值代表开启,--no前缀代表关闭:

bash 复制代码
./exec --reuse_addr        # 等价 true,开启地址重用
./exec --noreuse_addr      # 等价 false,关闭地址重用
./exec --reuse_addr=true
./exec --reuse_addr=false

补充:-- 可以终止 flag 解析。例如 ./exec -f1 1 -- -f2 2,-f1会被识别为 flag,-f2不会被 gflags 解析,作为普通业务参数。

针对上面不管是 string /int 类型参数 还是 bool 类型参数,我们都推荐的写法是:

bash 复制代码
./exec --log_file="./main.log"
./exec --reuse_addr=true

因为这样我们就不需要对两种参数的写法进行区分了,统一都是这种形式,而这种形式我们也可以理解为是一种Key-Value 的形式,便于我们记忆使用。

4.7 配置文件的使用

当参数数量很多,每次启动手动输入命令行参数会很麻烦。gflags 支持从配置文件批量加载参数,一次编写配置文件,后续直接复用。

注意:配置文件内的参数名 必须和代码 DEFINE 定义的名字完全一致 ,字符串 值不需要加双引号。

示例 main.conf:

cpp 复制代码
-reuse_addr=true
-log_level=3
-log_file=./log/main.log

启动时通过--flagfile指定配置文件路径:

bash 复制代码
./exec --flagfile=main.conf

4.8 内置特殊参数标识

gflags 自带内置 flag,无需自己定义,直接命令行使用:

  • --help:打印所有参数的简要帮助信息
  • --helpfull:输出完整详细的帮助文档
  • --helpshort:仅打印当前文件内定义的参数
  • --helpxml:以 xml 格式输出帮助信息,便于程序自动化处理
  • --version:打印程序版本信息
  • --flagfile:从指定文件读取命令行配置

举例:

bash 复制代码
./exec --help

说明: --help、--helpfull、--helpshort、--helpxml、--version 都是查看类指令 ,执行后程序打印信息就直接退出,不会执行业务逻辑。

三、完整示例讲解

3.1 main.cc

cpp 复制代码
/*
    gflags使用:
    1、包含头文件 <gflags/gflags.h>
    2、定义要捕获的参数信息,以及设置默认值
    3、解析参数,初始化参数数据
    4、获取数据
*/
#include <iostream>
#include <gflags/gflags.h>
#include <string>

//定义gflags参数
DEFINE_bool(reuse_addr, true, "是否启用地址复用选项");
DEFINE_int32(listen_port, 8080, "服务器监听端口");
DEFINE_string(listen_ip, "0.0.0.0", "服务器监听IP");
DEFINE_double(pi, 3.14, "圆周率");

int main(int argc, char *argv[])
{
    //解析命令行参数
    google::ParseCommandLineFlags(&argc, &argv, true);

    //解析完参数,进行获取(注意:获取的是变量,变量名称是参数名称加上 FLAGS_ 前缀)
    std::cout << FLAGS_reuse_addr << std::endl;
    std::cout << FLAGS_listen_port << std::endl;
    std::cout << FLAGS_listen_ip << std::endl;
    std::cout << FLAGS_pi << std::endl;
    return 0;
}

3.2 Makefile

cpp 复制代码
main:main.cc
	g++ -o $@ $^ -lgflags

.PHONY:clean
clean:
	rm -rf main
  • $@:目标文件(main)
  • $^:所有依赖文件(main.cc)
  • -lgflags:链接 gflags 第三方库

3.3 main.conf(配置文件)

cpp 复制代码
-reuse_addr=false
-listen_port=8888
-listen_ip=127.0.0.1
-pi=3.1415926

3.4 编译与多种运行方式

  • 编译
bash 复制代码
[ply@iZbp12ear9ufvimc78fddkZ gflags_test]$ make
g++ -o main main.cc -lgflags
  • 使用默认参数运行(不传递任何 flag)
bash 复制代码
[ply@iZbp12ear9ufvimc78fddkZ gflags_test]$ ./main 
1
8080
0.0.0.0
3.14
  • 命令行传参,覆盖默认值
bash 复制代码
[ply@iZbp12ear9ufvimc78fddkZ gflags_test]$ ./main --reuse_addr=false --listen_port=8888 --listen_ip=127.0.0.1 --pi=3.1415
0
8888
127.0.0.1
3.1415

这里我们会发现如果直接通过命令行传参的形式修改默认值,写起来是非常繁琐的,我们需要对每个 flags 参数都进行重新赋值,这也是为什么有配置文件的存在:通过配置文件加载参数,我们只需要写一项就可以将所有我们需要访问的参数都进行设置。

  • 通过配置文件加载参数
bash 复制代码
[ply@iZbp12ear9ufvimc78fddkZ gflags_test]$ ./main --flagfile=main.conf
0
8888
127.0.0.1
3.14159

不管我们定义了多少个 gflags 参数,只需要把这些参数都设置到配置文件中,通过 --flagfile=main.conf,我们就不再需要一个一个在命令行中进行设置了。

  • 配置文件 + 命令行参数(命令行优先级更高,覆盖配置文件)
bash 复制代码
[ply@iZbp12ear9ufvimc78fddkZ gflags_test]$ ./main --flagfile=main.conf --listen_port=9090
0
9090
127.0.0.1
3.14159
  • 查看帮助信息(内置 flag)
bash 复制代码
[ply@iZbp12ear9ufvimc78fddkZ gflags_test]$ ./main --help
main: Warning: SetUsageMessage() never called

  Flags from ./src/gflags.cc:
    -flagfile (load flags from file) type: string default: ""
    -fromenv (set flags from the environment [use 'export FLAGS_flag1=value'])
      type: string default: ""
    -tryfromenv (set flags from the environment if present) type: string
      default: ""
    -undefok (comma-separated list of flag names that it is okay to specify on
      the command line even if the program does not define a flag with that
      name.  IMPORTANT: flags in this list that have arguments MUST use the
      flag=value format) type: string default: ""

  Flags from ./src/gflags_completions.cc:
    -tab_completion_columns (Number of columns to use in output for tab
      completion) type: int32 default: 80
    -tab_completion_word (If non-empty, HandleCommandLineCompletions() will
      hijack the process and attempt to do bash-style command line flag
      completion on this value.) type: string default: ""

  Flags from ./src/gflags_reporting.cc:
    -help (show help on all flags [tip: all flags can have two dashes])
      type: bool default: false currently: true
    -helpfull (show help on all flags -- same as -help) type: bool
      default: false
    -helpmatch (show help on modules whose name contains the specified substr)
      type: string default: ""
    -helpon (show help on the modules named by this flag value) type: string
      default: ""
    -helppackage (show help on all modules in the main package) type: bool
      default: false
    -helpshort (show help on only the main module for this program) type: bool
      default: false
    -helpxml (produce an xml version of help) type: bool default: false
    -version (show version and build info and exit) type: bool default: false



  Flags from main.cc:
    -listen_ip (服务器监听IP) type: string default: "0.0.0.0"
    -listen_port (服务器监听端口) type: int32 default: 8080
    -pi (圆周率) type: double default: 3.1400000000000001
    -reuse_addr (是否启用地址复用选项) type: bool default: true

代码要点小结:

  1. DEFINE_* 在全局域定义参数,不能写在函数内部。
  2. google::ParseCommandLineFlags 放在 main 开头,解析后才能读取FLAGS_*变量。
  3. 配置文件参数不需要引号,参数名和 DEFINE 保持一致。
  4. 优先级:命令行参数 > 配置文件参数 > 代码内默认值。

3.5 跨文件访问参数演示

child.h:

cpp 复制代码
extern void Print();

child.cc(错误写法):

cpp 复制代码
#include <iostream>
#include <gflags/gflags.h>

void Print()
{
    //跨文件访问gflags参数
    std::cout <<  FLAGS_listen_ip << std::endl;
}

main.cc:

cpp 复制代码
#include <iostream>
#include <gflags/gflags.h>
#include <string>

#include "child.h"

//定义gflags参数
DEFINE_bool(reuse_addr, true, "是否启用地址复用选项");
DEFINE_int32(listen_port, 8080, "服务器监听端口");
DEFINE_string(listen_ip, "0.0.0.0", "服务器监听IP");
DEFINE_double(pi, 3.14, "圆周率");

int main(int argc, char *argv[])
{
    //解析命令行参数
    google::ParseCommandLineFlags(&argc, &argv, true);

    //解析完参数,进行获取(注意:获取的是变量,变量名称是参数名称加上 FLAGS_ 前缀)
    std::cout << FLAGS_reuse_addr << std::endl;
    std::cout << FLAGS_listen_port << std::endl;
    std::cout << FLAGS_listen_ip << std::endl;
    std::cout << FLAGS_pi << std::endl;

    Print();
    return 0;
}

Makefile:

cpp 复制代码
main:main.cc child.cc
	g++ -o $@ $^ -lgflags

.PHONY:clean
clean:
	rm -rf main

运行错误结果演示:

child.cc(正确写法):

cpp 复制代码
#include <iostream>
#include <gflags/gflags.h>

DECLARE_string(listen_ip);

void Print()
{
    //跨文件访问gflags参数
    std::cout <<  FLAGS_listen_ip << std::endl;
}

运行结果演示:

bash 复制代码
[ply@iZbp12ear9ufvimc78fddkZ gflags_test]$ make
g++ -o main main.cc child.cc -lgflags
[ply@iZbp12ear9ufvimc78fddkZ gflags_test]$ ./main 
1
8080
0.0.0.0
3.14
0.0.0.0

最后我们再拿曾经基于UDP协议写的一个Echo回显服务器,对代码进行修改,加上 gflags 第三方库,我们就可以直观感受到 gflags 带来的效果:

UdpServer.cc:

cpp 复制代码
#include "UdpServer.hpp"
#include <iostream>
#include <cstdio>
#include <memory>

#include <gflags/gflags.h>

DEFINE_int32(port, 8080, "端口号");

//  ./udpserver port
int main(int argc, char *argv[])
{
    // if (argc != 2)
    // {
    //     std::cerr << "Usage: " << argv[0] << " port" << std::endl;
    //     return 1;
    // }

    // gflags 替代了手动检查 argc
    google::ParseCommandLineFlags(&argc, &argv, true);

    ENABLE_CONSOLE_LOG_STRATEGY();
    // uint16_t port = std::stoi(argv[1]); // 字符串转成uint16_t,获取客户传入的端口号

    // 无需手动解析 argv,无需处理 char* 到其他类型的转换
    // 类型自动转换:它内部会根据你定义的宏(_int32, _double, _string 等),
    // 自动把命令行传进来的字符串(char*)转换成对应的 C++ 类型
    uint16_t port = FLAGS_port;

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, calculate_handler);

    usvr->Init();
    usvr->Start();

    return 0;
}

UdpClient.cc:

cpp 复制代码
// 各自头文件包含...
#include <gflags/gflags.h>

DEFINE_string(ip, "127.0.0.1", "IP地址");
DEFINE_int32(port, 8080, "端口号");

// ./udpclient ip port
int main(int argc, char *argv[])
{
    // if (argc != 3)
    // {
    //     std::cerr << "Usage: " << argv[0] << " ip port" << std::endl;
    //     return 1;
    // }

    google::ParseCommandLineFlags(&argc, &argv, true);

    // 1、创建socket套接字
    int socketfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (socketfd < 0)
    {
        std::cerr << "socket created error" << std::endl;
        return 2;
    }

    //  填写服务器信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));

    server.sin_family = AF_INET;
    // server.sin_port = htons(std::stoi(argv[2]));
    // server.sin_addr.s_addr = inet_addr(argv[1]);
    
    server.sin_port = htons(FLAGS_port);
    server.sin_addr.s_addr = inet_addr(FLAGS_ip.c_str());

    // 读写IO处理...
    return 0;
}

Makefile:

cpp 复制代码
.PHONY:all
all:udpserver udpclient

udpserver:UdpServer.cc
	g++ -o $@ $^ -std=c++17 -lgflags

udpclient:UdpClient.cc
	g++ -o $@ $^ -std=c++17 -lgflags

.PHONY:clean
clean:
	rm -rf udpserver udpclient

运行结果展示:

结束语

到这里,gflags 的基础用法与工程实战就全部讲解完毕。我们从库的特性、安装入手,依次学习了参数定义、参数访问、跨文件共享参数、命令行传参、配置文件加载以及内置特殊标识等核心能力,并通过一套完整示例工程,演示了编译、多种运行方式的实际效果。

gflags 凭借简洁的 API、多类型参数支持,成为 C++ 后端项目中非常常用的命令行配置工具。需要注意的是,gflags 更适合存放程序启动时的静态配置,不适合运行时动态频繁修改业务参数。

后续你可以在自己的网络服务项目中集成 gflags,把端口、IP、日志开关等配置交由它管理,替代硬编码,提升项目的灵活性。

相关推荐
狂奔的sherry2 小时前
C++ 宏生成访问函数的代价:访问权限、调试和类型检查
c++·宏·编译·预处理器·类型检查
划破黑暗的第一缕曙光3 小时前
[C++]:5.STL:string(2)
java·c++·redis
小超人诞生日记6 小时前
C++ 异常机制详解:从抛出捕获到异常安全
c++·算法
多弗朗皮卡丘6 小时前
C++的IO流
开发语言·c++·io流·文件io流
xiangyun617 小时前
C++ 手撕 shared_ptr:引用计数与控制块到底长什么样?
开发语言·c++·算法·面试·stl
SunkingYang7 小时前
C++14 vs C++17:新特性对比与用法指南
c++·新特性·用法·c++14·示例·c++17
SunkingYang8 小时前
C++ Lambda 表达式原理详解
c++·lambda·理论·原理·表达式
xiaoye-duck9 小时前
《C++ 工具库实战》GTest 单元测试:断言、全局环境、局部夹具与底层原理
c++·单元测试·第三方库
学逆向的9 小时前
C++ 匿名命名空间
开发语言·c++·网络安全