惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
Y
Y Combinator Blog
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
腾讯CDC
T
The Blog of Author Tim Ferriss
P
Proofpoint News Feed
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
小众软件
小众软件
美团技术团队
Martin Fowler
Martin Fowler
爱范儿
爱范儿
有赞技术团队
有赞技术团队
博客园 - 【当耐特】
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
宝玉的分享
宝玉的分享
J
Java Code Geeks
B
Blog
V
V2EX
Stack Overflow Blog
Stack Overflow Blog
B
Blog RSS Feed
博客园 - Franky

博客园 - 海乐学习

VS2026创建.net8的控制台程序并能在银河麒麟V10的Liunx上运行 在麒麟系统中卸载RabbitMQ Java Maven 开发的常用命令 C语言开发的常用命令 三汇Linux配置Config说明 Java实现优雅的关闭程序并执行清理流程的写法 C语言开发中优雅的关闭程序并执行清理流程的写法 将C语言开发的程序做成 麒麟系统的服务(Systemd 标准服务,Linux 通用) 实现开机自启动 ZeroMQ的DEALER双帧结构(路由空帧 + 业务帧)(支持异步收发、主动推送事件)C写服务端 java写客户端 ZeroMQ中ZMQ_DEALER单帧数据(支持异步收发、主动推送事件)C写服务端 java写客户端 win10系统中 关闭专注助手 执行 xx.sh 脚本文件时出现: /bin/bash^M:解释器错误: 没有那个文件或目录 运行编译打包好的 xxx.jar 中没有主清单属性 出现这个错误 将Java编译的 .jar文件做成 麒麟系统的服务(Systemd 标准服务,Linux 通用) 实现开机自启动 将Java编译的 .jar文件做成windows服务 实现开机自启动 方法二 RabbitMQ在麒麟系统中离线安装说明 VMware启动虚似机后出现 无法获取快照信息: 锁定文件失败 模块“Snapshot”启动失败。未能启动虚拟机。 C语言在Linux中开发完整Demo包含读配置文件写日志和定时器Timer C语言在Linux中开发读取配置文件app.conf 麒麟V10 Server系统中搭建C语言开发环境 麒麟ServerV10 修改IP4地址 麒麟ServerV10 配置IP4 当系统中有两个版本的Maven时,用IDEA创建Maven有时会出错 麒麟ServerV10安装 espeak-ng 和 ffmpeg 方法 C语言在Linux中开发没有界面纯后台运行的Demo程序(含日志和Timer) C语言在Linux中开发带界面的程序(含每小时日志) C语言在Linux中开发第一个项目Hello Word 在apache-maven项目中使用log4写日志 在apache-maven项目中解决中文乱码问题 在apache-maven项目中读取配置文件
C语言在Linux中开发使用定时器Timer在界面上显示时间
海乐学习 · 2026-04-22 · via 博客园 - 海乐学习

效果图

image

 工程目录

image

 直接上完整代码

MyCServer.h

#include <stdio.h> //标准输入输出 终端/文件 I/O 基础
#include <stdlib.h> //通用工具与内存管理 进程控制、随机数、字符串转换
#include <string.h> //C 字符串与内存操作 不处理动态分配,仅操作已分配内存
#include <ctype.h>  //字符属性判断与转换 安全处理 ASCII/扩展字符
#include <locale.h> //本地化/国际化设置 影响数字格式、字符排序、ctype 行为
#include <stdarg.h> //可变参数函数支持 实现类似 printf 的自定义函数
#include <curses.h> //终端文本 UI(TUI)Linux 通常用 ncurses,Windows 用 PDCurses
#include <time.h>   // 必须包含时间头文件
#include <sys/time.h>  // 提供 gettimeofday()
#include <unistd.h>    // 提供 usleep()

/*====================== 显示常量 ======================*/
#define SKIP_LINE       3       // 显示起始行
#define LOOP_TIME       1000     // 定时器周期 (ms) 1000毫秒

/*====================== 全局变量声明 ======================*/
extern int              ymax, xmax;             // 屏幕尺寸
extern int              ypos, xpos;             // 光标位置
extern int              bSetTime;               // 定时器标志
extern pthread_t        Th;                     // 定时器线程

/*====================== 函数声明 ======================*/
int     InitSys(void);
void    ExitSys(void);
void    InitWin(void);
void    UpInitWin(void);
void    OnTimer(void);
void    SetTimer(pthread_t *pTh);
void    KillTimer(pthread_t Th);
void    *timer_sr(void *timer_func);

MyCServer.c

#include "MyCServer.h"     // 主头文件(包含 TRUNK_CHANNEL 等结构体定义和系统头文件)
#include "Logger.h"          // 日志模块

/*====================== 全局变量 ======================*/
// 此处为全局变量实际分配内存
int     ymax, xmax;         // 屏幕尺寸
int     ypos, xpos;         // 光标位置
int     bSetTime = 0;       // 定时器标志
pthread_t Th = 0;           // 定时器线程(初始化为 0,表示未创建)
//底层使用全局缓冲区,多线程同时写屏会导致乱码。互斥锁是轻量级且必要的保护。
pthread_mutex_t g_scr_mutex = PTHREAD_MUTEX_INITIALIZER; // 保护 ncurses 屏幕刷新

int main(void){
    printf("Hello World\n");//// 此时只在内存中绘制

     // 初始化日志
    Log_Init();
    Log_Write("=======================================");
    Log_Write("======== MyCServer v1.88 启动 ==========");
    Log_Write("=======================================");

    if (InitSys() != 0)
    {
        Log_Write("[MyCServer][错误] 系统初始化失败");
        getch();
        clear();
        refresh();
        endwin();
        Log_Close();
        return -1;
    }

    Log_Write("[MyCServer][信息] 系统初始化成功");

    // 主循环:等待用户按键退出
    int nTmp = getch();
    while (tolower(nTmp) != 'q')
    {
        nTmp = getch();
    }

    Log_Write("[MyCServer][信息] 用户退出,关闭系统");
    ExitSys();

    return 0;
}

/*====================== 系统初始化 ======================*/
int InitSys(void)
{
    int nEnableTimer = 1;     // Timer 开关:1=启动,其他值=不启动

    // 1. 初始化界面
    InitWin();

    // 2. 根据开关决定是否启动定时器
    if (nEnableTimer == 1)
    {
        SetTimer(&Th);
    }

    //已经读配置文件等一系统初始化工作
    Log_Write("系统初始化完成===");

    refresh(); //将内存中绘制好的“虚拟屏幕”一次性输出到物理终端上。
    return 0;
}

/*====================== 系统退出 ======================*/
void ExitSys(void)
{
    Log_Write("正在关闭系统...");
    clear();//清空当前窗口(通常是整个终端屏幕),并将光标移动到左上角 (0,0)
            //它不会立刻清屏,而是将窗口标记为“需要完全重绘”。必须调用 refresh() 后,
            //清屏效果才会真正显示到终端。
    refresh();

    // 停止定时器(如果已启动)
    if (bSetTime == 1)
    {
        KillTimer(Th);
        Log_Write("[MyCServer][信息] Timer定时器已停止");
    }

    endwin();//标准退出清理函数。它的核心作用是安全关闭 ncurses 模式,并将终端完整恢复到程序运行前的原始状态。
             //与initscr()成对出现

}

/*====================== 窗口初始化 ======================*/
void InitWin(void)
{
      // setlocale 必须在 initscr() 之前调用
    if (setlocale(LC_ALL, "") == NULL)
    {
        setlocale(LC_ALL, "zh_CN.UTF-8");
    }
    //// 初始化 ncurses
    if (initscr() == NULL)
    {
        perror("Initscr");
        return;
    }

    // 强制下次刷新时清除整个屏幕
    clearok(stdscr, TRUE);
    cbreak();      // 禁用行缓冲
    noecho();      // 不显示输入字符
    curs_set(0);   // 隐藏光标

    UpInitWin();


}

void UpInitWin(void)
{
    getmaxyx(stdscr, ymax, xmax);

    // 清除屏幕并重新绘制边框
    clear();
    box(stdscr, ACS_VLINE, ACS_HLINE);

    // 绘制标题
    mvhline(1, 1, ' ', xmax - 2);
    mvprintw(1, 30, "MyCServer v1.88 (");
    addch('q' | A_REVERSE);
    printw("-退出)");

    getyx(stdscr, ypos, xpos);

    // 绘制表头
    mvhline(2, 1, ' ', xmax - 2);
    mvprintw(2, 3, "CH   状态            来电号码            通道信息");

    // 清除通道显示区域
    for (int i = SKIP_LINE; i < ymax - 2; i++)
    {
        mvhline(i, 1, ' ', xmax - 2);
    }

    refresh();
    move(ypos, xpos);
    refresh();
}

/*
 * 模块名称: 软件定时器管理模块
 * 功能描述: 基于 POSIX 线程实现一个周期性执行的软件定时器。
 *           通过创建独立线程循环执行回调函数,并利用系统时间测量与补偿,
 *           尽量保证回调函数以固定的周期 (LOOP_TIME) 触发。
 * 依赖宏:   LOOP_TIME (定时器目标周期,单位:毫秒)
 * 依赖类型: LP_V_V (应为回调函数指针类型,如 typedef void (*LP_V_V)(void);)
 * 依赖变量: bSetTime (全局运行状态标志,控制线程循环与退出)
 */

/*====================== 定时器回调 ======================*/
/**
 * @brief 定时器周期触发的业务回调函数
 * @note  1. 此函数的执行耗时必须远小于 LOOP_TIME,否则会导致周期累积延迟甚至“跑飞”。
 *        2. 避免在此函数中执行阻塞型操作(如 sleep、网络请求、大内存分配等)。
 *        3. LOOP_TIME 1000    timer_sr 依赖此宏计算休眠时间。1000ms 保证回调每秒触发一次
 */
void OnTimer(void)
{
    //move(ypos, xpos);  // 示例:更新坐标/位置信息
    //refresh();         // 示例:刷新显示/渲染界面

     time_t now = time(NULL);
    struct tm tm_info;
    localtime_r(&now, &tm_info);  // 线程安全的时间转换

    char time_str[32];
    strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", &tm_info);

    // 🔒 锁定屏幕操作区,防止与主线程 getch()/refresh() 并发冲突
    pthread_mutex_lock(&g_scr_mutex);

    // 动态计算打印列坐标(第一行右侧),避免覆盖标题
    int col_pos = (xmax > 24) ? xmax - 24 : 1;
    
    // 末尾补两个空格,用于覆盖旧时间字符串可能残留的字符
    mvprintw(1, col_pos, "[%s]  ", time_str);

    refresh();  // 立即输出到物理终端
    pthread_mutex_unlock(&g_scr_mutex);

}

/*====================== 定时器管理 ======================*/
/**
 * @brief 创建并启动定时器线程
 * @param pTh [out] 传出参数,用于保存新创建的线程 ID
 * @note  需确保 pTh 指向有效内存。多次调用此函数会创建多个定时器线程,
 *        建议在外部做好状态控制,避免重复创建。
 */
void SetTimer(pthread_t *pTh)
{
    bSetTime = 1;  // 置位运行标志,使 timer_sr 中的 while 循环得以执行
    // 创建线程:默认属性(可连接),线程入口为 timer_sr,将 OnTimer 作为参数传入
    pthread_create(pTh, NULL, timer_sr, (void *)OnTimer);
}

/**
 * @brief 安全停止并销毁定时器线程
 * @param Th [in] 待销毁的线程 ID
 * @note  采用“标志位退出 + 强制取消 + 资源回收”的组合策略,确保线程安全终止。
 */
void KillTimer(pthread_t Th)
{
    if (Th == 0) return;  // 线程未创建或 ID 无效,直接返回
    
    if (bSetTime == 1)
    {
        pthread_cancel(Th);  // 发送线程取消请求(唤醒可能处于 usleep 的线程)
        bSetTime = 0;        // 清除运行标志,破坏 timer_sr 的循环条件
        pthread_join(Th, NULL); // 阻塞等待线程完全退出,回收线程资源
    }
}

//定义一个新类型 LP_V_V,它代表“一个无参数、无返回值的函数指针
typedef void (*LP_V_V)(void);//定义函数指针类型别名的标准写法

/*====================== 定时器工作线程 ======================*/
/**
 * @brief 定时器线程主体函数
 * @param timer_func [in] 用户传入的周期回调函数指针
 * @return 线程退出状态指针
 * 
 * @逻辑流程:
 * 1. 记录执行回调前的系统时间
 * 2. 执行用户回调函数
 * 3. 记录执行后的系统时间,计算实际耗时
 * 4. 计算剩余时间 = 目标周期 - 实际耗时
 * 5. 若剩余时间 > 0,则休眠补足;否则直接进入下一轮周期
 */
void *timer_sr(void *timer_func)
{
    struct timeval old, now;
    long ns, nus;
    // 将通用 void 指针强转为实际的回调函数类型
    LP_V_V tm_sr = (LP_V_V)timer_func;

    // 循环执行条件:受全局标志 bSetTime 控制,KillTimer 中置 0 后退出
    while (bSetTime)
    {
        gettimeofday(&old, NULL); // 获取回调执行前的系统时间 (秒, 微秒)
        tm_sr();                  // 执行用户回调函数
        gettimeofday(&now, NULL); // 获取回调执行后的系统时间

        // 计算回调函数实际执行耗时
        ns  = now.tv_sec  - old.tv_sec;
        nus = now.tv_usec - old.tv_usec;

        // 处理微秒借位:若 now.tv_usec < old.tv_usec,需从秒位借 1 秒 (1000000 微秒)
        while (nus < 0)  // 此处实际只需 if 即可,while 同样安全
        {
            ns--;
            nus += 1000 * 1000;
        }

        // 计算本周期剩余需休眠的时间 (LOOP_TIME 为毫秒,需统一转为微秒)
        long remaining_us = LOOP_TIME * 1000 - (ns * 1000 * 1000 + nus);
        
        // 若剩余时间大于 0,则休眠补足周期;否则说明回调耗时已超周期,直接跳至下一轮
        if (remaining_us > 0)
        {
            usleep(remaining_us); // 微秒级休眠。此函数为 POSIX 标准的“可取消点”,
                                  // 此时调用 pthread_cancel 可立即唤醒线程。
        }
    }

    return (void *)0; // 线程正常退出
}

Logger.h

/*
 * ============================================================================
 * Logger.h - 日志模块头文件
 * ============================================================================
 */

#ifndef LOGGER_H
#define LOGGER_H

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/stat.h>
#include <pthread.h>

/*====================== 日志配置 ======================*/
#define LOG_BASE_PATH       "./logs"         // 日志根目录
#define LOG_MAX_SIZE        (5 * 1024 * 1024) // 单个日志文件最大大小 (5MB)
#define LOG_PATH_MAX        512              // 路径缓冲区大小
#define LOG_BUFFER_MAX      1024             // 日志内容缓冲区大小
#define LOG_TIME_FORMAT_MAX 64               // 时间戳格式缓冲区大小

/*====================== 日志句柄结构 ======================*/
typedef struct {
    FILE    *fp;                // 当前日志文件指针
    char    szFilePath[LOG_PATH_MAX];  // 当前日志文件路径
    time_t  tLastCheck;         // 上次检查时间
    long    lFileSize;          // 当前文件大小
    int     nLastHour;          // 上次小时
    int     nLastDay;           // 上次日期
} LOG_HANDLE;

/*====================== 全局变量声明 ======================*/
extern LOG_HANDLE       g_Log;          // 全局日志句柄
extern pthread_mutex_t  g_LogMutex;     // 日志互斥锁

/*====================== 函数声明 ======================*/

/**
 * 初始化日志系统
 * 创建日志目录并打开初始日志文件
 */
void Log_Init(void);

/**
 * 写入日志
 * @param szFormat 格式化字符串 (支持 printf 风格格式)
 * @param ... 可变参数
 */
void Log_Write(const char *szFormat, ...);

/**
 * 检查并切换日志文件 (按小时或文件大小轮转)
 */
void Log_CheckAndRotate(void);

/**
 * 关闭日志系统
 * 关闭日志文件并释放资源
 */
void Log_Close(void);

/**
 * 获取当前日志文件路径
 * @param szPath 输出路径缓冲区
 * @param size 缓冲区大小
 */
void Log_GetFilePath(char *szPath, size_t size);

/**
 * 获取文件大小
 * @param szPath 文件路径
 * @return 文件大小 (字节)
 */
long Log_GetFileSize(const char *szPath);

/**
 * 创建目录 (递归)
 * @param szPath 目录路径
 */
void Log_CreateDir(const char *szPath);

#endif /* SH_LOG_H */

Logger.c

/*
 * ============================================================================
 * Logger.c - 日志模块实现
 * ============================================================================
 */

#include "Logger.h"
#include <stdarg.h>     // va_list, va_start, va_end

/*====================== 类型定义 ======================*/
#ifndef BOOL_DEFINED
#define BOOL_DEFINED
typedef int BOOL;
#ifndef TRUE
#define TRUE    1
#endif
#ifndef FALSE
#define FALSE   0
#endif
#endif

/*====================== 全局变量定义 ======================*/
LOG_HANDLE      g_Log;          // 全局日志句柄
pthread_mutex_t g_LogMutex;     // 日志互斥锁

/*====================== 日志函数实现 ======================*/

/**
 * 创建目录(递归)
 * @param szPath 目录路径
 */
void Log_CreateDir(const char *szPath)
{
    char szDir[LOG_PATH_MAX];
    char *p = szDir;

    strncpy(szDir, szPath, sizeof(szDir) - 1);
    szDir[sizeof(szDir) - 1] = '\0';

    for (p += 1; *p; p++)
    {
        if (*p == '/')
        {
            *p = '\0';
            mkdir(szDir, 0755);
            *p = '/';
        }
    }
    mkdir(szDir, 0755);
}

/**
 * 获取当前日志文件路径
 * 格式:./logs/YYYYMMDD/YYYYMMDD_HH.log
 * @param szPath 输出路径缓冲区
 * @param size 缓冲区大小
 */
void Log_GetFilePath(char *szPath, size_t size)
{
    time_t tNow = time(NULL);
    struct tm *tmNow = localtime(&tNow);

    snprintf(szPath, size, "%s/%04d%02d%02d/%04d%02d%02d_%02d.log",
             LOG_BASE_PATH,
             tmNow->tm_year + 1900, tmNow->tm_mon + 1, tmNow->tm_mday,
             tmNow->tm_year + 1900, tmNow->tm_mon + 1, tmNow->tm_mday,
             tmNow->tm_hour);
}

/**
 * 获取文件大小
 * @param szPath 文件路径
 * @return 文件大小 (字节),失败返回 0
 */
long Log_GetFileSize(const char *szPath)
{
    struct stat st;
    if (stat(szPath, &st) == 0)
    {
        return st.st_size;
    }
    return 0;
}

/**
 * 初始化日志系统
 * 创建日志目录并打开初始日志文件
 */
void Log_Init(void)
{
    memset(&g_Log, 0, sizeof(LOG_HANDLE));
    pthread_mutex_init(&g_LogMutex, NULL);

    // 创建日志根目录
    Log_CreateDir(LOG_BASE_PATH);

    // 打开日志文件
    Log_CheckAndRotate();
}

/**
 * 检查并切换日志文件 (按小时或文件大小轮转)
 * 触发条件:
 * 1. 小时变化
 * 2. 日期变化
 * 3. 文件大小超过 5MB
 */
void Log_CheckAndRotate(void)
{
    time_t tNow = time(NULL);
    struct tm *tmNow = localtime(&tNow);
    char szNewPath[LOG_PATH_MAX];

    // 检查是否需要切换文件(小时变化或文件过大)
    BOOL bNeedRotate = FALSE;

    if (g_Log.fp == NULL)
    {
        bNeedRotate = TRUE;
    }
    else if (g_Log.nLastHour != tmNow->tm_hour || g_Log.nLastDay != tmNow->tm_mday)
    {
        bNeedRotate = TRUE;  // 小时或日期变化
    }
    else if (g_Log.lFileSize >= LOG_MAX_SIZE)
    {
        bNeedRotate = TRUE;  // 文件超过 5MB
    }

    if (!bNeedRotate)
    {
        return;
    }

    // 关闭旧文件
    if (g_Log.fp != NULL)
    {
        fclose(g_Log.fp);
        g_Log.fp = NULL;
    }

    // 生成新文件路径
    Log_GetFilePath(szNewPath, sizeof(szNewPath));

    // 创建日期目录
    char szDirPath[LOG_PATH_MAX];
    time_t tNow2 = time(NULL);
    struct tm *tmNow2 = localtime(&tNow2);
    snprintf(szDirPath, sizeof(szDirPath), "%s/%04d%02d%02d",
             LOG_BASE_PATH,
             tmNow2->tm_year + 1900, tmNow2->tm_mon + 1, tmNow2->tm_mday);
    Log_CreateDir(szDirPath);

    // 打开新文件
    g_Log.fp = fopen(szNewPath, "a");
    if (g_Log.fp != NULL)
    {
        strncpy(g_Log.szFilePath, szNewPath, sizeof(g_Log.szFilePath) - 1);
        g_Log.lFileSize = Log_GetFileSize(szNewPath);
        g_Log.nLastHour = tmNow->tm_hour;
        g_Log.nLastDay = tmNow->tm_mday;
        g_Log.tLastCheck = tNow;
    }
}

/**
 * 写入日志
 * @param szFormat 格式化字符串 (支持 printf 风格格式)
 * @param ... 可变参数
 */
void Log_Write(const char *szFormat, ...)
{
    va_list args;
    char szBuffer[LOG_BUFFER_MAX];
    char szTime[LOG_TIME_FORMAT_MAX];
    time_t tNow = time(NULL);
    struct tm *tmNow = localtime(&tNow);

    // 生成时间戳
    snprintf(szTime, sizeof(szTime), "%04d-%02d-%02d %02d:%02d:%02d",
             tmNow->tm_year + 1900, tmNow->tm_mon + 1, tmNow->tm_mday,
             tmNow->tm_hour, tmNow->tm_min, tmNow->tm_sec);

    // 生成日志内容
    va_start(args, szFormat);
    int nLen = vsnprintf(szBuffer, sizeof(szBuffer), szFormat, args);
    va_end(args);

    // 加锁
    pthread_mutex_lock(&g_LogMutex);

    // 检查是否需要切换文件
    Log_CheckAndRotate();

    // 写入日志
    if (g_Log.fp != NULL)
    {
        fprintf(g_Log.fp, "[%s] %s\n", szTime, szBuffer);
        fflush(g_Log.fp);
        g_Log.lFileSize += nLen + strlen(szTime) + 3;
    }

    // 解锁
    pthread_mutex_unlock(&g_LogMutex);
}

/**
 * 关闭日志系统
 * 关闭日志文件并释放资源
 */
void Log_Close(void)
{
    pthread_mutex_lock(&g_LogMutex);

    if (g_Log.fp != NULL)
    {
        fclose(g_Log.fp);
        g_Log.fp = NULL;
    }

    pthread_mutex_unlock(&g_LogMutex);
    pthread_mutex_destroy(&g_LogMutex);
}

Makefile

# ==========================================
# 编译器与变量定义
# 原生单 *.c 文件的的编译命令  gcc MyCServer.c -o MyCServer
# 有Makefile文件的 清理并编译  make clean && make
# 运行  ./MyCServer
# ==========================================

# 定义 C 语言编译器为 gcc
CC = gcc
# 定义 C++ 编译器为 g++ (虽然本文件主要编译 .c,但预留变量是个好习惯)
CXX = g++

# 编译选项 (CFLAGS)
# -Wall                  : 开启几乎所有警告,帮助发现潜在代码问题
# -O2                    : 开启二级优化,提高程序运行速度
# -D_XOPEN_SOURCE_EXTENDED : 宏定义,启用 X/Open 标准扩展,通常用于支持宽字符和高级终端功能
# -D_GNU_SOURCE          : 宏定义,启用 GNU C 库的扩展功能,确保能使用一些非标准的系统调用
CFLAGS = -Wall -O2 -D_XOPEN_SOURCE_EXTENDED -D_GNU_SOURCE

# 链接选项 (LDFLAGS)
# -lpthread              : 链接 pthread 库,支持多线程编程
# -lshpa3                : 链接 shpa3 库 (可能是你的私有库或特定硬件库)
# -lncursesw             : 链接 ncursesw 库,'w' 代表 wide-character,用于在终端支持中文和宽字符显示
# -lstdc++               : 链接 C++ 标准库 (因为可能混编了 C++ 代码或依赖库是用 C++ 写的)
# -lzmq                  : 链接 ZeroMQ 库,用于高性能网络消息通信
LDFLAGS = -lpthread -lshpa3 -lncursesw -lstdc++ -lzmq

# ==========================================
# 2. 目标文件与源文件定义
# ==========================================

# 最终生成的可执行文件名
#TARGET = ShPbxServer
TARGET = MyCServer

# 源文件列表 (所有 .c 文件)
#SRCS = ShPbxServer.c Logger.c ZeroMQServer.c ShBoard.c ShMemRecord.c ZeroMQAudioPush.c
SRCS = MyCServer.c Logger.c

# 目标文件列表 (自动将 SRCS 中的 .c 替换为 .o)
# 例如: ShPbxServer.c -> ShPbxServer.o
OBJS = $(SRCS:.c=.o)

# ==========================================
# 3. 伪目标声明
# ==========================================

# 声明 .PHONY 告诉 make,all 和 clean 不是文件名,而是动作标签
# 这样可以避免当前目录下有名为 "all" 或 "clean" 的文件时产生冲突
.PHONY: all clean install uninstall

# ==========================================
# 4. 默认目标
# ==========================================

# all 是默认执行的第一个目标,它依赖于 $(TARGET)
# 执行 make 时,会先尝试构建 $(TARGET)
all: $(TARGET)

# ==========================================
# 5. 链接规则 (生成可执行文件)
# ==========================================

# 规则:生成 $(TARGET) 依赖于所有的 $(OBJS)
$(TARGET): $(OBJS)
    # 注意:下面这行开头必须是一个 Tab 键,不能是空格!
    # 执行链接命令:将所有的 .o 文件链接在一起,生成最终的可执行文件
    $(CC) $(CFLAGS) -o $(TARGET) $(OBJS) $(LDFLAGS)

# ==========================================
# 6. 编译规则 (生成 .o 文件)
# ==========================================

# 模式规则:告诉 make 如何将任意 .c 文件编译成 .o 文件
# % 是通配符,表示匹配任意文件名
%.o: %.c
    # 执行编译命令:
    # -c 表示只编译不链接
    # $< 代表第一个依赖文件 (即源文件 .c)
    # $@ 代表目标文件 (即 .o 文件)
    $(CC) $(CFLAGS) -c $< -o $@

# ==========================================
# 7. 清理规则
# ==========================================

# 清理构建产物
clean:
    # 删除所有的 .o 中间文件和最终的可执行文件
    # -f 参数表示强制删除,不提示确认
    rm -f $(OBJS) $(TARGET)

# ==========================================
# 8. 安装与卸载 (可选)
# ==========================================

# 安装目标:将编译好的程序复制到系统路径,使其可以在任意目录运行
install: $(TARGET)
    cp $(TARGET) /usr/local/bin/

# 卸载目标:删除系统路径中的程序
uninstall:
    rm -f /usr/local/bin/$(TARGET)

#================几个关键点解释=================== 
#1、-lncursesw 的作用:
#你的程序中使用了 ncursesw 而不是普通的 ncurses。这说明你的程序界面需要显示中文或者特殊符号。如果编译报错找不到库,通常是因为系统没装 libncursesw-dev。
#2、$< 和 $@:
#这是 Makefile 的自动变量。
#$<:代表依赖列表中的第一个文件(在这里是 .c 源文件)。
#$@:代表目标文件(在这里是 .o 对象文件)。
#使用它们可以让规则通用,不用为每个文件单独写一行编译命令。
#3、Tab 键缩进:
#在 Makefile 中,所有命令(如 $(CC)... 或 rm...)前面必须使用 Tab 键 进行缩进,不能使用空格。这是新手最容易遇到的报错原因(missing separator)。
#4、$(SRCS:.c=.o):
#这是一个字符串替换函数。它会自动把 SRCS 变量里所有的 .c 后缀替换成 .o,这样你添加新源文件时,只需要改 SRCS 列表,不用手动去改 OBJS。
#================================================

进入工程目录

执行  清理且编译 命令

 运行命令

image