文章目录
本系列为《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++ 版本没有"隐藏开销"
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_42125125/article/details/161471837



