C++ 完整实践:RAII 宿主连接 POSIX C 库 · C++原生项目怎样判断构建与运行边界至资料与适用边界
先具备 C++ 值语义、异常和 RAII 知识。本专题沿 ABI 契约、拥有关系、完整宿主、排错推进。示例未执行,仅完成静态文本/模板检查,不声称运行通过。
专题入口:C++ C ABI 边界:链接、不透明句柄与错误值 · C++ 原生资源所有权:接管、析构与借用 · C++ 完整实践:RAII 宿主连接 POSIX C 库 · C++ 原生边界排错:符号、释放器与异常政策
scan.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(同源文件,示例未执行)
#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
#endifTEST-SPEC.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(同源文件,示例未执行)
/* 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 编译开关。文章采用独立组织的边界与失败推演,外链用于溯源,必要答案已在正文。
继续阅读
- 知识专题
- 排错
- 知识专题