文章目录
- 引言
- 一、需求说明
- 二、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 版本的关键问题清单:
三、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 版本:
| 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 代码量对比
| 总行数(不含空行和注释) | ~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 行。
总结
两个版本实现了相同的功能,但它们在"出错概率"上不在同一个数量级:
下一篇——实战2:RAII文件事务管理器——我们将聚焦 C++ 最核心的资源管理哲学,实现一个在生产项目中可以直接使用的类。
📝 动手练习:
