Skip to content

C++ 完整实践:RAII 宿主连接 POSIX C 库 · C++原生项目怎样判断构建与运行边界至资料与适用边界 ​

返回分篇目录

先具备 C++ 值语义、异常和 RAII 知识。本专题沿 ABI 契约、拥有关系、完整宿主、排错推进。示例未执行,仅完成静态文本/模板检查,不声称运行通过。

专题入口:C++ C ABI 边界:链接、不透明句柄与错误值 · C++ 原生资源所有权:接管、析构与借用 · C++ 完整实践:RAII 宿主连接 POSIX C 库 · C++ 原生边界排错:符号、释放器与异常政策

scan.c(同源文件,示例未执行)
c
#include "scan.h"
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

struct worker {
    struct scan *owner;
    char *path;
    int error;
};
struct scan {
    pthread_mutex_t mutex;
    struct worker jobs[2];
    uint64_t total;
    int attempted;
    int ready;
};

/* Broken synchronization means lifetime safety cannot be established. */
static void require_success(int code) {
    if (code != 0) abort();
}

static void *count_file(void *arg) {
    struct worker *w = arg;
    int fd;
    do { fd = open(w->path, O_RDONLY | O_NONBLOCK); }
    while (fd < 0 && errno == EINTR);
    if (fd < 0) { w->error = errno; return NULL; }
    struct stat info;
    if (fstat(fd, &info) != 0) w->error = errno;
    else if (!S_ISREG(info.st_mode)) w->error = EINVAL;
    uint64_t bytes = 0;
    unsigned char buffer[4096];
    while (w->error == 0) {
        ssize_t n = read(fd, buffer, sizeof buffer);
        if (n > 0) {
            if ((uint64_t)n > UINT64_MAX - bytes) w->error = EOVERFLOW;
            else bytes += (uint64_t)n;
        } else if (n == 0) break;
        else if (errno != EINTR) w->error = errno;
    }
    /* Never blindly retry close: the descriptor may already be released. */
    if (close(fd) != 0 && w->error == 0) w->error = errno;
    if (w->error == 0) {
        require_success(pthread_mutex_lock(&w->owner->mutex));
        if (bytes > UINT64_MAX - w->owner->total) w->error = EOVERFLOW;
        else w->owner->total += bytes;
        require_success(pthread_mutex_unlock(&w->owner->mutex));
    }
    return NULL;
}

int scan_create(const char *left, const char *right, scan **out) {
    if (out == NULL) return EINVAL;
    *out = NULL;
    if (left == NULL || right == NULL) return EINVAL;
    scan *s = calloc(1, sizeof *s);
    if (s == NULL) return ENOMEM;
    s->jobs[0].path = strdup(left);
    s->jobs[1].path = strdup(right);
    if (s->jobs[0].path == NULL || s->jobs[1].path == NULL) {
        free(s->jobs[0].path); free(s->jobs[1].path); free(s);
        return ENOMEM;
    }
    int code = pthread_mutex_init(&s->mutex, NULL);
    if (code != 0) {
        free(s->jobs[0].path); free(s->jobs[1].path); free(s);
        return code;
    }
    s->jobs[0].owner = s;
    s->jobs[1].owner = s;
    *out = s;
    return 0;
}

int scan_run(scan *s) {
    if (s == NULL) return EINVAL;
    if (s->attempted) return EALREADY;
    s->attempted = 1;
    pthread_t threads[2];
    size_t started = 0;
    int code = 0;
    for (; started < 2; ++started) {
        code = pthread_create(&threads[started], NULL, count_file,
                              &s->jobs[started]);
        if (code != 0) break;
    }
    for (size_t i = 0; i < started; ++i)
        require_success(pthread_join(threads[i], NULL));
    if (code != 0) return code;
    for (size_t i = 0; i < 2; ++i)
        if (s->jobs[i].error != 0) return s->jobs[i].error;
    s->ready = 1;
    return 0;
}

static int write_all(int fd, const char *data, size_t length) {
    size_t done = 0;
    while (done < length) {
        ssize_t n = write(fd, data + done, length - done);
        if (n > 0) done += (size_t)n;
        else if (n == 0) return EIO;
        else if (errno != EINTR) return errno;
    }
    return 0;
}

int scan_report(const scan *s, int fd) {
    if (s == NULL || !s->ready) return EINVAL;
    char text[64];
    int n = snprintf(text, sizeof text, "bytes=%" PRIu64 "\n", s->total);
    if (n < 0 || (size_t)n >= sizeof text) return EOVERFLOW;
    return write_all(fd, text, (size_t)n);
}

void scan_destroy(scan *s) {
    if (s == NULL) return;
    require_success(pthread_mutex_destroy(&s->mutex));
    free(s->jobs[0].path);
    free(s->jobs[1].path);
    free(s);
}
scan.h(同源文件,示例未执行)
c
#ifndef BYTE_SCAN_H
#define BYTE_SCAN_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct scan scan;
/* 0 = success, otherwise an errno-style error number, never -1.
 * out must be non-null. Failure sets *out = NULL. Paths are copied.
 * The handle has one owner. No concurrent calls on the same handle.
 * Files must be regular, finite, stable during run; paths are trusted.
 */
int scan_create(const char *left, const char *right, scan **out);
/* Run exactly once. Joins all successfully created workers before return.
 * Failure publishes no result; repeat calls return EALREADY.
 * Cancellation, fork while running, and external pthread manipulation
 * are unsupported. Unexpected internal join/lock failure aborts.
 */
int scan_run(scan *s);
/* Borrow output fd; never closes it. Requires a successful run.
 * Output failure can leave a prefix written; caller handles SIGPIPE.
 */
int scan_report(const scan *s, int fd);
/* NULL is accepted; otherwise a valid exclusively owned handle required. */
void scan_destroy(scan *s);
#ifdef __cplusplus
}
#endif
#endif
TEST-SPEC.md(同源文件,示例未执行)
md
# 验收规格(全部未执行)

| 场景 | 操作与观测 | 预期 |
| --- | --- | --- |
| LF样本 | README 的命令 | bytes=7 LF;退出0 |
| 重复路径 | 两参数都用 left.txt | bytes=8 LF;退出0 |
| 空文件 | 将一个参数设为读者准备的零字节普通文件 | 只计另一个输入;退出0 |
| 参数缺失 | 不传参数 | usage 到 stderr;退出2 |
| 文件缺失 | 第二参数为不存在的路径 | run 的错误到 stderr;stdout空;退出1 |
| 非普通文件 | 一个参数为目录 | 错误到 stderr;stdout空;退出1 |
| 输出失败 | 将 stdout 设为无效/不可写描述符的受控测试环境 | report失败;退出1,可能已有前缀 |
| 短读 | 接口替身让 read 依次返回2、1、0 | 本文件计3字节,不能计4096 |
| EINTR | 接口替身让 read/write 首次 -1/EINTR 后成功 | 重试且不丢失已处理偏移 |
| 第二次创建线程失败 | 在受控测试版本替换 pthread_create 返回 EAGAIN | 先 join 第一个线程,再返回EAGAIN |
| 一次性状态 | C测试驱动 create/run/run/destroy | 第二次run为EALREADY;仅销毁一次 |
| 构造失败 | 受控分配替身使第二次strdup失败 | out为空,已分配块释放,无mutex销毁 |
| 溢出 | 受控内部计数替身接近UINT64_MAX | EOVERFLOW,无成功报告 |

test_scan.c 已提供基础 C 测试驱动;短读写、EINTR、线程创建与分配失败的
接口替身尚未实现,不宣称已有自动化故障注入。测试驱动也未编译、未执行。
普通磁盘文件不能可靠制造短写、线程创建失败或64位溢出。
先用真实文件核对正常/缺失/目录/重复项,再单独实现链接替身测试;
替身要保留真实 pthread 生命周期,不允许伪造成功线程ID。
手工检查依据:成功创建数 started 决定 join 范围;所有 join 在返回前完成;
worker只写自己的error,主线程只在join后读;total写受同一mutex保护;
ready只在全部成功后置位。任何不满足这些条件的修改须重新审查。
test_scan.c(同源文件,示例未执行)
c
/* Unexecuted API contract tests. No fault-injection substitutes implemented. */
#include "scan.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void expect(int condition, const char *message) {
    if (!condition) {
        fprintf(stderr, "contract failed: %s\n", message);
        exit(1);
    }
}

static void expect_report(scan *s, const char *expected) {
    int fds[2];
    expect(pipe(fds) == 0, "pipe");
    /* Reports are at most 64 bytes; no full-pipe wait before the read. */
    expect(scan_report(s, fds[1]) == 0, "report");
    expect(close(fds[1]) == 0, "close report writer");
    char text[64];
    size_t used = 0;
    for (;;) {
        expect(used < sizeof text, "report fits buffer");
        ssize_t n = read(fds[0], text + used, sizeof text - used);
        if (n > 0) used += (size_t)n;
        else if (n == 0) break;
        else expect(errno == EINTR, "report read");
    }
    expect(close(fds[0]) == 0, "close report reader");
    expect(used == strlen(expected), "report length");
    expect(memcmp(text, expected, used) == 0, "report bytes");
}

int main(int argc, char **argv) {
    expect(argc == 3, "two LF fixture paths required");
    scan *s = NULL;
    expect(scan_create(argv[1], argv[2], NULL) == EINVAL, "null out");
    expect(scan_create(NULL, argv[2], &s) == EINVAL && s == NULL,
           "null path leaves out null");
    expect(scan_run(NULL) == EINVAL, "null run");
    expect(scan_report(NULL, 1) == EINVAL, "null report");
    scan_destroy(NULL);

    expect(scan_create(argv[1], argv[2], &s) == 0, "create");
    expect(scan_report(s, 1) == EINVAL, "report before run");
    expect(scan_run(s) == 0, "run");
    expect(scan_run(s) == EALREADY, "one-shot run");
    expect(scan_report(s, -1) == EBADF, "invalid borrowed fd");
    expect_report(s, "bytes=7\n");
    scan_destroy(s);
    s = NULL;

    expect(scan_create(argv[1], argv[1], &s) == 0, "duplicate create");
    expect(scan_run(s) == 0, "duplicate input accepted");
    expect_report(s, "bytes=8\n");
    scan_destroy(s);
    s = NULL;

    /* Empty path cannot name a file; no reliance on a magic missing name. */
    expect(scan_create(argv[1], "", &s) == 0, "copy empty path");
    expect(scan_run(s) == ENOENT, "open failure propagated");
    expect(scan_report(s, 1) == EINVAL, "failed run not published");
    scan_destroy(s);
    puts("scan contracts passed");
    return 0;
}

C++原生项目怎样判断构建与运行边界 ​

C++原生项目怎样判断构建与运行边界:README 的 CMake 命令先配置再构建,最后从仓库根目录运行 byte-report 并传两个输入路径。本轮未编译、未配置、未执行,只有文本契约与 Vue 页面模板的静态检查;不能把模板通过当成 C++ 编译通过。

同架构同工具链链接是当前假设。Linux/macOS 读者需预先具备编译器、标准库、POSIX SDK;WSL 需在 Linux 中构建和执行,产物不是 Windows 原生可执行文件。没有安装依赖或探测读者机器环境。

C++原生宿主应测试哪些异常路径 ​

C++原生宿主应测试哪些异常路径:分别制造 run 失败和 report 失败,检查退出1及 stderr 诊断,并通过分配/销毁观测证明句柄只释放一次。创建失败须保证 out 为空且没有运行线程;第二线程创建失败须在抛异常前已 join 第一个。不能只测试 catch 被进入而不检查资源责任。

完整 TEST-SPEC 给出了正常、重复路径、缺失、短读写和溢出的预期。接口替身尚未实现,故当前只能手工追踪控制流;后续运行测试时既要比较输出,也要核对析构、线程收拢与无泄漏条件。

独立练习 ​

run返回错误后从check抛出,到main捕获之间有哪些清理?若报告已写一半再失败,有没有强事务保证?

先写出状态和资源归属,再对照答案;这些是推演练习,没有执行示例。

参考解答与检查条件

owner先析构调用scan_destroy,run返回前已join。catch打印错误并退出1;已写前缀不能回滚,只有资源生命周期保证,没有输出事务保证。

资料与适用边界 ​

2026-10-17 核对 Linux read 手册、write 手册、close 的平台差异、pthread_create、pthread_join 与 POSIX mutex 规范镜像。Linux 手册中的实现差异不直接推广为 macOS 实测结果。The Open Group 的 POSIX 页面本次抓取被拒绝,不记为已成功核读。线程依赖的目标表达参考 CMake 3.20 FindThreads,没有执行它来探测本机。

C++ 链接与拥有关系参照 cppreference language linkage、unique_ptr;中文核对 Microsoft extern 说明,仅引用语言链接概念,不套用其 Windows 编译开关。文章采用独立组织的边界与失败推演,外链用于溯源,必要答案已在正文。

继续阅读