关注

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

本系列为《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. 退出程序

交互界面(两个版本一致):

=== 通讯录 ===
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 版本的关键问题清单

  1. 手动内存管理cb_initcb_destroy 必须成对调用,中间任何漏掉的 return 都可能跳过 cb_destroy
  2. 固定大小缓冲区name[MAX_NAME_LEN]——64 字节硬上限,名字长了就截断,短了就浪费
  3. 字符串操作的危险strncpy 容易忘写 \0strcmp 区分大小写,fgets 要手动去掉 \n
  4. 扩容代码分散cb_expand 的逻辑和普通添加混在一起
  5. 没有类型安全cb_remove 的索引判断是 C 风格的手动边界检查
  6. 代码量:约 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 + capacitystd::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::vectorstd::stringContactBook 析构时自动释放——无论函数如何退出(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
手动内存管理行~150
手动边界检查行~81
字符串缓冲区操作行~120(std::string 封装掉了)
初始化/清理配对手动自动(构造/析构)

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

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

本专栏文章对应特性在本项目中怎么用的
第2篇class vs structContactBook 是 class,Contact 是 private 内部 struct
第3篇构造/析构自动管理 vector<Contact> 的生命周期,不需要 init/destroy
第6篇命名空间两个版本各自独立,不会符号冲突(如果要合并的话)
第7篇iostreamcin/cout 代替 scanf/printf——类型安全,不需要 %d/%s
第9篇引用std::string_view 零拷贝传参,不像 C 的 const char* 要手动管理 \0
第11篇unique_ptr虽然本示例没直接用,但如果 Contact 含有需要独占所有权的资源,unique_ptr 就是答案
第12篇RAIIstd::stringstd::vectorContactBook 本身——三个都是 RAII 的直接应用
第13篇autoauto 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/fclosemalloc 行缓冲区、判断文件结尾——至少 50 行。


总结

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

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

下一篇——实战2:RAII文件事务管理器——我们将聚焦 C++ 最核心的资源管理哲学,实现一个在生产项目中可以直接使用的类。


📝 动手练习

  1. 编译并运行 C 版本的完整代码,用 Valgrind 或 AddressSanitizer 检测内存泄漏(故意在删除操作之前提前 return,验证泄漏确实存在)
  2. 编译并运行 C++ 版本,验证同样的提前 return 不会导致泄漏
  3. 为 C++ 版本添加 find_if 模板函数,用 lambda 查找所有名字长度大于 3 的联系人
  4. 为 C++ 版本添加 save/load 文件持久化功能
  5. 对比两个版本在搜索 10000 个联系人时的性能(用 <chrono> 测量)——证明 C++ 版本没有"隐藏开销"

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_42125125/article/details/161471837

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--