1. C 和 C++的区别
1. 核心区别
| 维度 | C | C++ |
|---|---|---|
| 设计目标 | 底层控制,最小抽象 | 高性能 + 高级抽象 |
| 代码组织 | 函数为中心 | 类/泛型为中心 |
| 内存管理 | 手动(malloc/free) | RAII(智能指针、容器) |
| 错误处理 | 返回值/errno | 异常 + noexcept |
| 多态支持 | 函数指针模拟 | 虚函数 + 模板 |
| 类型安全 | 弱(void*强转常见) | 强(模板/类型推导) |
| 编译期计算 | 宏(预处理期) | constexpr + 模板元编程 |
| 标准库 | 基础(stdio.h, stdlib.h) | 强大(STL容器、算法、线程等) |
| 适用场景 | 内核、嵌入式、硬件交互 | 大型应用、游戏、高频交易 |
2. 深度对比
(1) 内存管理方式
C 方式:
int *arr = malloc(100 * sizeof(int));
if (!arr) { /* 错误处理 */ }
free(arr); // 必须手动释放
问题:
C++ 方式:
// 方案一:智能指针
std::unique_ptr<int[]> arr(new int[100]); // 自动释放
// 方案二:STL容器
std::vector<int> arr(100); // 自动管理内存+边界检查
// 方案三:类似C方式,使用new,delete或者C系列的内存管理函数
(2) 多态实现
C方式(通过函数指针模拟)
声明函数指针的语法:
// 返回类型 (*指针变量名)(参数类型列表)
int (*func_ptr)(int, int);
给函数指针赋值:
int add(int a, int b) {
return a + b;
}
func_prt = add; // 也可以写作 func_ptr = &add;
接下来,我们可以将一个函数指针作为参数传递给另一个函数(注册回调函数),比如:
int cmp(const void* a, const void* b) {
return (*(int*)a - *(int*)b);
}
qsort(arr, n, sizeof(int), cmp); // cmp 就是函数指针
也可以实现简单的多态/策略模式:
int op_add(int a, int b) { return a + b; }
int op_mul(int a, int b) { return a * b; }
void compute(int x, int y, int (*op)(int, int)) {
printf("Result: %d\n", op(x, y));
}
缺点:
- 需要手动维护函数指针,类型不安全
- 无法实现运行时类型识别
注意事项:
- 函数名本身可以当作函数指针使用(不加
&) - 函数指针不能指向内联函数或宏
- 在使用
typedef时尤其能大幅提升可读性 - 函数指针的调用在某些平台(嵌入式)中需要特别注意 ABI 和对齐
C++方式(面向对象)
class Animal {
public:
virtual void speak() = 0; // 纯虚函数
};
class Dog : public Animal {
void speak() override { std::cout << "Woof!\n"; }
};
Animal *animal = new Dog();
animal->speak(); // 动态绑定
delete animal; // 需注意内存管理(可用智能指针优化)
优势:
- 真正的运行时多态(虚函数表vtable支持)。
- 类型安全(override关键字防止错误重载)。
总结:
C++的虚函数机制虽然有小性能开销(vtable查找),但在大型项目中,清晰的接口抽象比微优化更重要。我们通过final和override关键字进一步确保多态安全性。
(3) 泛型编程
泛型编程 是一种编程范式,其核心目标是:编写与类型无关、可复用、可扩展的代码,依赖抽象而非具体类型。
其核心理念在于:
- 类型参数化:通过将类型作为参数,使得代码对类型不敏感
- 编译期多态(静态多态):与传统面向对象的运行时多态(如虚函数)不同,泛型编程依赖于模板和类型推导,实现在编译期间的多态行为
- 基于抽象操作而非接口继承:比如要求类型支持“+”运算、迭代等,而不要求某个特定的基类
C方式(宏展开或代码复用):
// 用宏实现泛型max
#define MAX(a,b) ((a) > (b) ? (a) : (b))
C++方式(模板)
template <typename T>
T max(T a, T b) { return a > b ? a : b; }
// 使用(隐式)
auto val = max(3, 5); // 编译期实例化int版本
auto val2 = max(3.14, 2.0); // 实例化double版本
优势:
- 编译期类型安全。
- 零运行时开销(模板在编译期展开)
(4) 类型系统
C 的类型系统
基本类型
int,char,float,double,void- 修饰符:
signed,unsigned,short,long
派生类型
- 指针(
int*)、数组(int arr[10])、函数指针、结构体(struct)、共用体(union)
用户定义类型
typedef(类型别名)struct/union/enum
特性
- 类型系统是弱类型 + 静态类型:允许大量隐式转换,兼容性强但不够安全。
- 函数参数不支持重载。
- 不支持面向对象(没有类、继承、虚函数等)。
- 类型检查较为宽松(比如函数声明不匹配时的默认行为更宽容)。
C++的类型系统
C++ 继承了 C 的类型系统,但大幅扩展,主要体现在:
类类型(Class Types)
-
类(
class)、结构体(struct)本质上是相同的,但支持:- 成员函数
- 继承
- 多态(虚函数)
- 封装(private/protected/public)
- 构造/析构函数
引用类型
int&引用类型是 C++ 独有,表现为别名(非指针)- C++11 起加入 右值引用(
int&&),用于移动语义和完美转发
模板与泛型
- C++ 的类型系统可以在编译期生成类型安全的代码
- 支持类型参数化(模板类、模板函数)
template<typename T>
类型推导
auto(C++11):自动推导变量类型decltype:根据表达式推导类型template deduction: 编译器在模板实例化时推导类型
类型安全增强
- 强制函数重载区分参数类型
- 更严格的转换规则(如
explicit构造函数) - 引入了
static_cast,dynamic_cast,reinterpret_cast,const_cast
常量表达力更强
const用法更严格、适用更广- C++11 引入
constexpr支持编译期常量求值
类型擦除(Type Erasure)
- 如
std::function、std::any、std::variant等实现了运行时多态和类型抽象
3. 常见问题
(1) C++比C慢吗?
“C++的抽象通常是零成本的(如STL容器、模板)。但错误使用(如滥用虚函数、异常)可能导致开销。关键点:
- 默认情况下,C++性能≈C。
- RAII和智能指针甚至能优化C的手动管理错误。
- 游戏引擎(Unreal)和金融系统(高频交易)用C++证明其性能。”
(2) 什么场景下该用C而不是C++?
“三类场景优先C:
- 极端受限环境(单片机固件、内核开发)。
- 跨编译器ABI兼容(动态库接口需兼容不同编译器)。
- 遗留系统维护(如Linux驱动开发)。
- 其他情况,C++的工程优势更明显。”
(3) C++的缺点有哪些?
- 语言复杂度过高,导致学习成本增加。C++ 是一个“多范式混合体”:过程式 + 面向对象 + 泛型编程 + 函数式风格(C++11起)+ 元编程 + 并发 + 模块化(C++20)等。
- 编译慢 且 可执行文件。首先模板机制(尤其是STL + 元编程)导致大量代码膨胀;其次头文件包含机制导致编译重复冗余。
- ABI不稳定 & 模板库难以封装。模板类/函数只能在头文件中实现,无法封装成 .so/.dll 提供 ABI;不同编译器/版本生成的二进制接口不兼容
- 安全问题依然严重。尽管C++提供了面向对象、RAII、智能指针等机制,但依然保留了指针与裸内存操作、手动new/delete、未定义行为,易导致悬垂指针、缓冲区溢出、资源泄露。
- 错误信息晦涩(尤其是模板)。泛型编程错误信息极长,且难以理解,Concepts 改善了部分情况,但编译期调试仍不友好。
- STL 与抽象代价。- STL 容器设计追求通用性,有时带来抽象开销,如:
std::map(红黑树)插入性能低于手写哈希表、std::function存在 type erasure 与动态分配开销,迭代器风格虽统一但不总是直观。
2. C++独有的特性
1. 移动语义(C++11起)
std::vector<int> createLargeData() {
std::vector<int> data(1'000'000);
return data; // 触发移动构造(非拷贝)
}
auto v = createLargeData(); // 零成本传递所有权
为什么重要:
- 避免深拷贝大对象(如容器、字符串)
- 实现资源所有权转移(如
std::unique_ptr) - 面试提示:解释
std::move与右值引用(区别于拷贝)
2. Lambda表达式(C++11起)
std::vector<int> nums = {1, 2, 3};
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b; // 降序排序
});
为什么重要:
- 简化回调、STL算法定制(如
std::for_each) - 替代函数对象(Functor),代码更紧凑
- 面试提示:捕获列表(
[=]、[&])的作用域控制
3. 类型推导(auto/decltype)
auto x = 42; // 推导为int
decltype(x) y = x; // y的类型与x相同
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) { return t + u; }
为什么重要:
- 减少模板代码冗余
- 配合泛型编程提升可读性
- 面试提示:
auto与模板类型推导规则的差异
4. 范围for循环(C++11起)
for (const auto& num : nums) { // 遍历容器
std::cout << num << " ";
}
为什么重要:
- 比C风格的
for (int i=0; i<n; i++)更安全(无越界风险) - 支持所有STL容器和自定义迭代器
- 面试提示:对比
for (auto&& x : range)的性能优化
5. 模板元编程(TMP)
template<int N>
struct Factorial {
static const int value = N * Factorial<N-1>::value;
};
template<>
struct Factorial<0> { static const int value = 1; };
int x = Factorial<5>::value; // 编译期计算120
为什么重要:
- 编译期计算(零运行时开销)
- 标准库(如
std::tuple)的基石 - 面试提示:C++11后的
constexpr更易用,但TMP仍是面试高频题
6. 并发支持(C++11起)
std::mutex mtx;
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back([&mtx, i] {
std::lock_guard<std::mutex> lock(mtx);
std::cout << "Thread " << i << "\n";
});
}
for (auto& t : threads) t.join();
为什么重要:
- 原生支持线程、互斥锁、条件变量
- 比C的
pthread更类型安全(RAII管理锁) - 面试提示:
std::async与线程池的取舍
7. 用户定义字面量(C++11起)
auto duration = 250ms; // 来自std::chrono
auto size = 64_KB; // 自定义字面量
为什么重要:
- 提升代码可读性(如单位明确化)
- 标准库应用(
std::string、std::complex) - 面试提示:如何实现自定义
_KB后缀
8. 结构化绑定(C++17起)
std::map<int, std::string> m = {{1, "one"}, {2, "two"}};
for (const auto& [key, value] : m) { // 直接解构
std::cout << key << ": " << value << "\n";
}
为什么重要:
- 简化多返回值处理(替代
std::tie) - 支持pair、tuple、结构体等
9. 概念(Concepts,C++20起)
template<typename T>
concept Addable = requires(T a, T b) { a + b; };
template<Addable T>
T sum(T a, T b) { return a + b; }
为什么重要:
- 取代SFINAE,提升模板可读性
- 编译期类型约束(如限制模板参数必须可相加)
10. 协程(C++20起)
generator<int> range(int start, int end) {
for (int i = start; i < end; ++i)
co_yield i; // 协程挂起
}
for (int n : range(1, 10)) { /* 惰性求值 */ }
为什么重要:
- 简化异步代码(如网络IO)
- 比回调或Promise更直观