3. C与C++的差异

好,咱们今天聊聊C和C++在动态内存管理和类型转换上的差异。这两个点,说白了就是C++在C的基础上做了“升级改造”。我刚开始从C转C++时,觉得不就是换个写法嘛,结果踩了不少坑。今天我把这些经验掰开揉碎了讲给你听。

3.1 动态内存管理:new/delete vs malloc/free

先问个问题:C语言里怎么分配内存?你肯定知道,malloc。那C++呢?new。看起来都是分配内存,但本质上有天壤之别。

3.1.1 本质区别:构造函数与析构函数

malloc只做一件事:从堆上划一块内存给你。它不关心这块内存里放的是什么。而new不一样,它先分配内存,然后调用对象的构造函数。同样,free只释放内存,delete先调用析构函数,再释放内存。

我举个例子你就明白了:

// C语言方式
struct Student* s = (struct Student*)malloc(sizeof(struct Student));
// 此时s指向的内存是"脏"的,成员变量未初始化
free(s);  // 只是释放内存,没有清理工作

// C++方式
Student* s = new Student();  // 分配内存 + 调用构造函数
delete s;                    // 调用析构函数 + 释放内存

我在项目中遇到过这样一个bug:有人用malloc给一个C++类对象分配内存,这个类内部有个std::string成员。结果程序运行时随机崩溃。为什么?因为malloc没调用构造函数,std::string的内部指针是野的。后来改成new,问题立刻消失。

警告: 永远不要用malloc给C++类对象分配内存!除非你清楚知道自己在做什么,并且愿意手动处理构造/析构。

3.1.2 使用方式对比

操作 C语言 (malloc/free) C++ (new/delete)
分配单个对象 int* p = (int*)malloc(sizeof(int)); int* p = new int;
分配数组 int* arr = (int*)malloc(n * sizeof(int)); int* arr = new int[n];
释放单个对象 free(p); delete p;
释放数组 free(arr); delete[] arr;
失败时返回 NULL 抛出std::bad_alloc异常
类型安全 需要强制类型转换 类型安全,无需转换

看到没?new和delete的语法更简洁,而且类型安全。你想想看,malloc返回的是void*,你得手动强转,多一步就多一个出错的机会。

3.1.3 数组分配:一个容易踩的坑

这里有个细节我必须强调:new和delete必须配对使用,new[]和delete[]必须配对使用。千万别混了!

int* arr = new int[100];
delete arr;  // 错误!应该是 delete[] arr;

我曾经犯过这个错。那时候刚用C++写一个数据处理模块,分配了一个数组,释放时用了delete而不是delete[]。程序跑起来没问题,但内存泄漏检测工具报了一堆错。后来查了半天才发现是这里的问题。嗯,自那以后我每次写delete都会下意识看一眼有没有方括号。

技巧: 现代C++中,建议优先使用智能指针(std::unique_ptr、std::shared_ptr)和容器(std::vector),它们会自动管理内存,省心很多。

3.2 强制类型转换:C风格 vs C++风格

C语言里类型转换就一种写法:(类型)表达式。简单粗暴,但也很危险。C++引入了四种命名的强制类型转换,每种都有明确的用途。

3.2.1 C风格转换的问题

C风格转换的问题在于:它太“万能”了。一个括号加个类型名,什么都能转。但这也意味着编译器不会帮你检查转换是否合理。

// C风格转换:什么都能转
int a = 10;
double* p = (double*)&a;  // 编译通过,但运行时可能出问题
*p = 3.14;  // 踩内存了!

这种代码在C语言里很常见,但在C++里,我们应该尽量避免。为什么?因为C++提供了更安全的替代方案。

3.2.2 C++的四种类型转换

C++的四种转换各有各的用途,我一个个说。

1. static_cast:最常用的转换

用于相关类型之间的转换,比如int转double、void*转具体指针等。编译器会做基本检查。

double d = 3.14;
int i = static_cast<int>(d);  // 显式转换,丢掉小数部分

void* p = malloc(100);
int* ip = static_cast<int*>(p);  // void* 转 int*

2. dynamic_cast:运行时类型检查

用于多态类型之间的向下转换(基类转派生类)。它会在运行时检查转换是否合法,如果不合法,返回nullptr(指针)或抛出异常(引用)。

class Base { virtual void foo() {} };
class Derived : public Base {};

Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b);
if (d) {
    // 转换成功,安全使用
} else {
    // 转换失败,b不是Derived类型
}

我个人习惯在涉及继承体系的代码中,优先使用dynamic_cast。虽然它有一点运行时开销,但换来的是安全性。

3. const_cast:去掉const属性

这个转换的唯一用途是去掉对象的const或volatile属性。说实话,我建议你尽量少用。如果你发现自己需要const_cast,多半是设计上有问题。

const int ci = 10;
int* pi = const_cast<int*>(&ci);
*pi = 20;  // 未定义行为!ci可能真的在只读存储区
警告: 用const_cast修改原本定义为const的对象,结果是未定义行为。我曾经在一个遗留代码里看到有人这么干,结果程序在某些编译器上正常,换了个编译器就崩溃。排查了两天才找到原因。

4. reinterpret_cast:最危险的转换

这个转换几乎不做任何检查,它只是重新解释内存中的比特位。说白了,就是告诉编译器:“我知道我在做什么,别管我。”

int a = 0x12345678;
char* p = reinterpret_cast<char*>(&a);
// 现在p指向a的第一个字节,可以用来检查字节序

这个转换在底层编程、驱动开发、序列化等场景中偶尔会用到。但日常业务代码中,我建议你尽量避开它。

3.2.3 如何选择?一张图说清楚

下面这张图是我自己总结的,每次做类型转换时我都会在心里过一遍:

C++类型转换选择流程图 需要类型转换? 去掉 const/volatile? 是 const_cast 否 多态向下转换? 是 dynamic_cast 否 底层比特重解释? 是 reinterpret_cast 否 static_cast 提示:日常开发中,90%的场景用 static_cast 就够了 dynamic_cast 需要类中有虚函数(多态) const_cast 和 reinterpret_cast 请谨慎使用

3.2.4 我的建议

说了这么多,总结一下我的个人经验:

  • 能用C++风格就别用C风格。C++风格转换意图明确,编译器能帮你做更多检查。
  • 优先用static_cast。它覆盖了绝大多数场景,而且安全。
  • 涉及继承体系用dynamic_cast。虽然有点性能开销,但安全第一。
  • 尽量别用const_cast和reinterpret_cast。如果非用不可,加个注释说明原因。

核心要点:

  • new/delete会调用构造/析构函数,malloc/free不会
  • new[]必须配delete[],new必须配delete
  • C++四种类型转换各有用途,优先用static_cast
  • 避免C风格类型转换,它太“万能”反而容易出错

好了,关于动态内存管理和类型转换,今天就聊到这儿。这两个知识点看似基础,但很多C++老手也会在这上面翻车。你写代码时多留个心眼,养成好习惯,后面会省很多事。