欢迎光临
我们一直在努力

16. 实战1:C命令行通讯录 → C++ 安全版本重构

文章目录

  • 引言
  • 一、需求说明
  • 二、C 语言版本:完整实现
    • 2.1 数据结构
    • 2.2 实现
    • 2.3 主程序
  • 三、C++ 版本:用 class + STL + 智能指针重写
    • 3.1 数据结构
    • 3.2 实现
    • 3.3 主程序
  • 四、对比分析:同样的功能,不同级别的安全保证
    • 4.1 内存安全
    • 4.2 缓冲区溢出
    • 4.3 搜索返回值的语义精确性
    • 4.4 代码量对比
  • 五、逐项对照:C 的痛点 → C++ 的解药
  • 六、扩展:加上更多现代 C++ 特性
    • 6.1 用 lambda 实现条件搜索
    • 6.2 文件持久化
  • 总结

本系列为《C++深度修炼:基础、STL源码与多线程实战》第16篇 前置条件:了解 class(第2篇)、构造/析构(第3篇)、string(本系列将涉及)、vector(本系列将涉及)、智能指针(第11篇)

引言

本专栏的最初 15 篇文章,每一项都做了同一件事:对照 C 语言的局限,展示 C++ 的解法。 现在是时候把它们串联起来了。

本文从头实现两个版本的通讯录程序:

  • C 版本:命令行交互,用 struct + 动态数组管理联系人
  • C++ 版本:同样的功能,用 class + std::vector + std::string + std::unique_ptr 重写

你会看到,C++ 版本不是"多了什么功能"——而是相同的功能,用了更少的代码,更少的出错路径,以及只在 C++ 中才能实现的编译期安全保证。

这不是学术示例。两个版本都是可以运行、可以扩展的完整程序。


一、需求说明

我们要实现一个命令行通讯录,支持以下操作:

  • 添加联系人:姓名 + 电话号码
  • 列出所有联系人:按添加顺序显示
  • 搜索联系人:按姓名搜索
  • 删除联系人:按编号删除
  • 退出程序
  • 交互界面(两个版本一致):

    === 通讯录 ===
    1. 添加联系人
    2. 列出所有联系人
    3. 搜索联系人
    4. 删除联系人
    5. 退出
    请选择操作: _


    二、C 语言版本:完整实现

    2.1 数据结构

    // contact_c.h
    #ifndef CONTACT_C_H
    #define CONTACT_C_H

    #include <stddef.h> // size_t

    #define MAX_NAME_LEN 64
    #define MAX_PHONE_LEN 20

    typedef struct {
    char name[MAX_NAME_LEN];
    char phone[MAX_PHONE_LEN];
    } Contact;

    typedef struct {
    Contact *contacts; // 动态数组
    size_t count; // 当前联系人数量
    size_t capacity; // 数组容量
    } ContactBook;

    // API
    int cb_init(ContactBook *cb);
    void cb_destroy(ContactBook *cb);
    int cb_add(ContactBook *cb, const char *name, const char *phone);
    void cb_list(const ContactBook *cb);
    int cb_search(const ContactBook *cb, const char *name);
    int cb_remove(ContactBook *cb, size_t index);
    size_t cb_size(const ContactBook *cb);

    #endif

    2.2 实现

    // contact_c.c
    #include "contact_c.h"
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    #define INITIAL_CAPACITY 4

    int cb_init(ContactBook *cb) {
    cb->contacts = malloc(INITIAL_CAPACITY * sizeof(Contact));
    if (!cb->contacts) return 1;
    cb->count = 0;
    cb->capacity = INITIAL_CAPACITY;
    return 0;
    }

    void cb_destroy(ContactBook *cb) {
    free(cb->contacts);
    cb->contacts = NULL;
    cb->count = 0;
    cb->capacity = 0;
    }

    static int cb_expand(ContactBook *cb) {
    size_t new_capacity = cb->capacity * 2;
    Contact *new_contacts = realloc(cb->contacts, new_capacity * sizeof(Contact));
    if (!new_contacts) return 1;
    cb->contacts = new_contacts;
    cb->capacity = new_capacity;
    return 0;
    }

    int cb_add(ContactBook *cb, const char *name, const char *phone) {
    if (strlen(name) >= MAX_NAME_LEN || strlen(phone) >= MAX_PHONE_LEN)
    return 1;

    if (cb->count >= cb->capacity) {
    if (cb_expand(cb) != 0) return 1;
    }

    Contact *c = &cb->contacts[cb->count];
    strncpy(c->name, name, MAX_NAME_LEN 1);
    c->name[MAX_NAME_LEN 1] = '\\0';
    strncpy(c->phone, phone, MAX_PHONE_LEN 1);
    c->phone[MAX_PHONE_LEN 1] = '\\0';
    cb->count++;
    return 0;
    }

    void cb_list(const ContactBook *cb) {
    if (cb->count == 0) {
    printf("通讯录为空\\n");
    return;
    }
    printf("%-4s %-20s %-20s\\n", "编号", "姓名", "电话");
    printf("—- ——————– ——————–\\n");
    for (size_t i = 0; i < cb->count; ++i) {
    printf("%-4zu %-20s %-20s\\n", i + 1, cb->contacts[i].name, cb->contacts[i].phone);
    }
    }

    int cb_search(const ContactBook *cb, const char *name) {
    for (size_t i = 0; i < cb->count; ++i) {
    if (strcmp(cb->contacts[i].name, name) == 0) {
    printf("找到: %s – %s (编号 %zu)\\n", cb->contacts[i].name, cb->contacts[i].phone, i + 1);
    return (int)i;
    }
    }
    printf("未找到: %s\\n", name);
    return 1;
    }

    int cb_remove(ContactBook *cb, size_t index) {
    if (index >= cb->count) return 1;
    // 把后面的元素往前搬
    for (size_t i = index; i < cb->count 1; ++i) {
    cb->contacts[i] = cb->contacts[i + 1];
    }
    cb->count;
    return 0;
    }

    size_t cb_size(const ContactBook *cb) {
    return cb->count;
    }

    2.3 主程序

    // main_c.c
    #include "contact_c.h"
    #include <stdio.h>
    #include <stdlib.h>

    static void print_menu() {
    printf("\\n=== 通讯录 ===\\n");
    printf("1. 添加联系人\\n");
    printf("2. 列出所有联系人\\n");
    printf("3. 搜索联系人\\n");
    printf("4. 删除联系人\\n");
    printf("5. 退出\\n");
    printf("请选择操作: ");
    }

    static void clear_input() {
    int c;
    while ((c = getchar()) != '\\n' && c != EOF) {}
    }

    int main() {
    ContactBook cb;
    if (cb_init(&cb) != 0) {
    printf("初始化通讯录失败\\n");
    return 1;
    }

    int running = 1;
    while (running) {
    print_menu();
    int choice;
    if (scanf("%d", &choice) != 1) {
    printf("无效输入\\n");
    clear_input();
    continue;
    }
    clear_input();

    switch (choice) {
    case 1: {
    char name[MAX_NAME_LEN], phone[MAX_PHONE_LEN];
    printf("姓名: ");
    if (!fgets(name, sizeof(name), stdin)) break;
    name[strcspn(name, "\\n")] = '\\0';
    printf("电话: ");
    if (!fgets(phone, sizeof(phone), stdin)) break;
    phone[strcspn(phone, "\\n")] = '\\0';
    if (cb_add(&cb, name, phone) == 0) {
    printf("添加成功\\n");
    } else {
    printf("添加失败\\n");
    }
    break;
    }
    case 2:
    cb_list(&cb);
    break;
    case 3: {
    char name[MAX_NAME_LEN];
    printf("搜索姓名: ");
    if (!fgets(name, sizeof(name), stdin)) break;
    name[strcspn(name, "\\n")] = '\\0';
    cb_search(&cb, name);
    break;
    }
    case 4: {
    cb_list(&cb);
    printf("输入要删除的编号: ");
    int idx;
    if (scanf("%d", &idx) != 1) {
    printf("无效编号\\n");
    clear_input();
    break;
    }
    clear_input();
    if (cb_remove(&cb, (size_t)(idx 1)) == 0) {
    printf("删除成功\\n");
    } else {
    printf("删除失败(编号无效)\\n");
    }
    break;
    }
    case 5:
    running = 0;
    break;
    default:
    printf("无效选项,请重新输入\\n");
    break;
    }
    }

    cb_destroy(&cb);
    return 0;
    }

    C 版本的关键问题清单:

  • 手动内存管理:cb_init → cb_destroy 必须成对调用,中间任何漏掉的 return 都可能跳过 cb_destroy
  • 固定大小缓冲区:name[MAX_NAME_LEN]——64 字节硬上限,名字长了就截断,短了就浪费
  • 字符串操作的危险:strncpy 容易忘写 \\0,strcmp 区分大小写,fgets 要手动去掉 \\n
  • 扩容代码分散:cb_expand 的逻辑和普通添加混在一起
  • 没有类型安全:cb_remove 的索引判断是 C 风格的手动边界检查
  • 代码量:约 150 行(不含空行),其中至少 30 行是在做内存/字符串的安全管理

  • 三、C++ 版本:用 class + STL + 智能指针重写

    3.1 数据结构

    // contact_cpp.h
    #pragma once

    #include <string>
    #include <vector>
    #include <string_view>
    #include <optional>

    class ContactBook {
    public:
    void add(std::string name, std::string phone);
    void list() const;
    std::optional<size_t> search(std::string_view name) const;
    bool remove(size_t index);
    size_t size() const { return contacts_.size(); }

    private:
    struct Contact {
    std::string name;
    std::string phone;
    };
    std::vector<Contact> contacts_;
    };

    对比 C 版本:

    C 版本C++ 版本变化
    char name[64] / char phone[20] std::string name / std::string phone 动态大小,不浪费内存,没有 64 字符上限
    Contact *contacts + count + capacity std::vector<Contact> contacts_ 三合一,自动扩容,自动释放
    cb_init() / cb_destroy() 构造函数 / 析构函数(编译器自动生成) 不需要手动调用 init/destroy
    返回值 int(错误码) bool / std::optional<size_t> 类型精确表达语义
    搜索参数 const char* std::string_view 零开销的只读字符串视图

    3.2 实现

    // contact_cpp.cpp
    #include "contact_cpp.h"
    #include <iostream>
    #include <iomanip>
    #include <algorithm>

    void ContactBook::add(std::string name, std::string phone) {
    contacts_.push_back(Contact{std::move(name), std::move(phone)});
    }

    void ContactBook::list() const {
    if (contacts_.empty()) {
    std::cout << "通讯录为空\\n";
    return;
    }
    std::cout << std::left
    << std::setw(6) << "编号"
    << std::setw(22) << "姓名"
    << std::setw(22) << "电话" << '\\n'
    << "—— ———————- ———————-\\n";

    for (size_t i = 0; i < contacts_.size(); ++i) {
    std::cout << std::left
    << std::setw(6) << (i + 1)
    << std::setw(22) << contacts_[i].name
    << std::setw(22) << contacts_[i].phone << '\\n';
    }
    }

    std::optional<size_t> ContactBook::search(std::string_view name) const {
    for (size_t i = 0; i < contacts_.size(); ++i) {
    if (contacts_[i].name == name) {
    std::cout << "找到: " << contacts_[i].name
    << " – " << contacts_[i].phone
    << " (编号 " << (i + 1) << ")\\n";
    return i;
    }
    }
    std::cout << "未找到: " << name << '\\n';
    return std::nullopt;
    }

    bool ContactBook::remove(size_t index) {
    if (index >= contacts_.size()) return false;
    contacts_.erase(contacts_.begin() + static_cast<ptrdiff_t>(index));
    return true;
    }

    代码量对比:C 版本约 120 行实现逻辑,C++ 版本约 40 行——且没有一行是在做手动内存管理。

    3.3 主程序

    // main_cpp.cpp
    #include "contact_cpp.h"
    #include <iostream>
    #include <string>
    #include <limits>

    static void print_menu() {
    std::cout << "\\n=== 通讯录 ===\\n"
    << "1. 添加联系人\\n"
    << "2. 列出所有联系人\\n"
    << "3. 搜索联系人\\n"
    << "4. 删除联系人\\n"
    << "5. 退出\\n"
    << "请选择操作: ";
    }

    int main() {
    ContactBook cb; // 构造 = init——不需要手动调用 init()

    int running = 1;
    while (running) {
    print_menu();
    int choice;
    if (!(std::cin >> choice)) {
    std::cout << "无效输入\\n";
    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\\n');
    continue;
    }
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\\n');

    switch (choice) {
    case 1: {
    std::string name, phone;
    std::cout << "姓名: ";
    std::getline(std::cin, name);
    std::cout << "电话: ";
    std::getline(std::cin, phone);
    cb.add(std::move(name), std::move(phone));
    std::cout << "添加成功\\n";
    break;
    }
    case 2:
    cb.list();
    break;
    case 3: {
    std::string name;
    std::cout << "搜索姓名: ";
    std::getline(std::cin, name);
    cb.search(name);
    break;
    }
    case 4: {
    cb.list();
    std::cout << "输入要删除的编号: ";
    int idx;
    if (!(std::cin >> idx)) {
    std::cout << "无效编号\\n";
    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\\n');
    break;
    }
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\\n');
    if (cb.remove(static_cast<size_t>(idx 1))) {
    std::cout << "删除成功\\n";
    } else {
    std::cout << "删除失败(编号无效)\\n";
    }
    break;
    }
    case 5:
    running = 0;
    break;
    default:
    std::cout << "无效选项,请重新输入\\n";
    break;
    }
    }

    // 析构 = destroy——不需要手动调用 destroy()
    }


    四、对比分析:同样的功能,不同级别的安全保证

    4.1 内存安全

    C 版本的隐患:

    // 隐患 1:忘记调用 cb_destroy → 内存泄漏
    int main() {
    ContactBook cb;
    cb_init(&cb);
    if (error_condition) return 1; // ← cb_destroy 没有被调用!泄漏!
    cb_destroy(&cb);
    }

    // 隐患 2:realloc 失败导致原内存丢失
    // cb_expand 中:new_contacts = realloc(cb->contacts, …);
    // 如果 realloc 失败返回 NULL,cb->contacts 仍指向原内存——
    // 但许多 C 程序员写 cb->contacts = realloc(…) 直接覆盖,导致泄漏

    C++ 版本:std::vector 和 std::string 在 ContactBook 析构时自动释放——无论函数如何退出(return、异常、goto),编译器保证析构函数一定被调用。

    4.2 缓冲区溢出

    C 版本:

    char name[MAX_NAME_LEN]; // 64 字节硬上限
    // 如果有人输入了一个 100 字符的名字?截断——而且可能没有 \\0

    C++ 版本:

    std::string name;
    std::getline(std::cin, name); // 自动扩容——没有上限问题

    4.3 搜索返回值的语义精确性

    C 版本:

    int cb_search(...); // 返回 -1 表示未找到,返回 >=0 表示索引
    // -1 和索引混在同一个 int 里——使用者必须知道这个约定

    C++ 版本:

    std::optional<size_t> search(...); // std::nullopt 表示未找到,size_t 值表示索引
    // 类型系统强制执行"你使用前必须检查"——不可能忘了判空就用

    auto result = cb.search("alice");
    if (result) {
    std::cout << "索引: " << *result << '\\n'; // 类型安全
    }
    // 如果你直接写 *result 而不检查——std::optional 的 operator* 至少是定义明确的行为
    //(虽然也会崩,但比解引用 -1 的数组索引好排查一万倍)

    4.4 代码量对比

    维度C 版本C++ 版本
    总行数(不含空行和注释) ~150 ~90
    手动内存管理行 ~15 0
    手动边界检查行 ~8 1
    字符串缓冲区操作行 ~12 0(std::string 封装掉了)
    初始化/清理配对 手动 自动(构造/析构)

    五、逐项对照:C 的痛点 → C++ 的解药

    这个项目里,前面 15 篇文章讲的所有特性全部派上了用场:

    本专栏文章对应特性在本项目中怎么用的
    第2篇 class vs struct ContactBook 是 class,Contact 是 private 内部 struct
    第3篇 构造/析构 自动管理 vector<Contact> 的生命周期,不需要 init/destroy
    第6篇 命名空间 两个版本各自独立,不会符号冲突(如果要合并的话)
    第7篇 iostream cin/cout 代替 scanf/printf——类型安全,不需要 %d/%s
    第9篇 引用 std::string_view 零拷贝传参,不像 C 的 const char* 要手动管理 \\0
    第11篇 unique_ptr 虽然本示例没直接用,但如果 Contact 含有需要独占所有权的资源,unique_ptr 就是答案
    第12篇 RAII std::string、std::vector、ContactBook 本身——三个都是 RAII 的直接应用
    第13篇 auto auto result = cb.search("alice") ——推导为 std::optional<size_t>
    第14篇 lambda 虽然在主程序中没用到,但扩展功能(如按条件搜索)时就是 lambda 的主场

    这不是碰巧——这个实战的设计目标就是把前 15 篇文章的知识点串成一张网。


    六、扩展:加上更多现代 C++ 特性

    6.1 用 lambda 实现条件搜索

    在 C++ 版本中添加一个泛型搜索函数只需 5 行:

    // 添加到 ContactBook 类中
    template <typename Predicate>
    std::vector<const Contact*> find_if(Predicate pred) const {
    std::vector<const Contact*> result;
    for (const auto &c : contacts_) {
    if (pred(c)) result.push_back(&c);
    }
    return result;
    }

    // 使用:
    // 搜索所有电话以 "138" 开头的人
    auto result = cb.find_if([](const auto &c) {
    return c.phone.starts_with("138");
    });

    在 C 版本中实现同样的功能,你需要定义一个函数指针类型、写一个匹配函数、再把函数指针传进去——逻辑散落在三个地方。

    6.2 文件持久化

    给 C++ 版本加文件存储:

    #include <fstream>

    void ContactBook::save(const std::string &filepath) const {
    std::ofstream out(filepath);
    for (const auto &c : contacts_) {
    out << c.name << '\\t' << c.phone << '\\n';
    }
    // out 析构时自动关闭文件——不需要 out.close()
    }

    void ContactBook::load(const std::string &filepath) {
    std::ifstream in(filepath);
    std::string line;
    while (std::getline(in, line)) {
    auto tab = line.find('\\t');
    if (tab != std::string::npos) {
    add(line.substr(0, tab), line.substr(tab + 1));
    }
    }
    }

    C 版本需要手动 fopen/fclose、malloc 行缓冲区、判断文件结尾——至少 50 行。


    总结

    两个版本实现了相同的功能,但它们在"出错概率"上不在同一个数量级:

  • 内存管理:C 版本有 3+ 处可能的内存泄漏路径;C++ 版本为 0——std::vector 和 std::string 管理的所有内存都在析构函数中自动释放
  • 缓冲区溢出:C 版本有 64 字符硬上限,超长名字被静默截断;C++ 版本用 std::string 自动扩容,没有上限
  • 类型安全:C 版本的 cb_search 返回 int(-1 = 未找到);C++ 版本返回 std::optional<size_t>(类型系统防止你在值缺失时使用它)
  • 代码密度:C 版本约 150 行,C++ 版本约 90 行——少掉的那 60 行,恰好全是在做手动内存管理和字符串安全防护
  • 前 15 篇文章的知识全部落地:class、构造/析构、RAII、string、vector、auto、lambda——这不是噱头,是这个实际项目中每一项都在起作用
  • 下一篇——实战2:RAII文件事务管理器——我们将聚焦 C++ 最核心的资源管理哲学,实现一个在生产项目中可以直接使用的类。


    📝 动手练习:

  • 编译并运行 C 版本的完整代码,用 Valgrind 或 AddressSanitizer 检测内存泄漏(故意在删除操作之前提前 return,验证泄漏确实存在)
  • 编译并运行 C++ 版本,验证同样的提前 return 不会导致泄漏
  • 为 C++ 版本添加 find_if 模板函数,用 lambda 查找所有名字长度大于 3 的联系人
  • 为 C++ 版本添加 save/load 文件持久化功能
  • 对比两个版本在搜索 10000 个联系人时的性能(用 <chrono> 测量)——证明 C++ 版本没有"隐藏开销"
  • 赞(0)
    未经允许不得转载:171主机测评 » 16. 实战1:C命令行通讯录 → C++ 安全版本重构
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址