前言
Java 和 C++ 都用new创建对象,语法看起来差不多,但背后的内存模型、生命周期和类型系统差别很大。把 Java 的习惯直接搬到 C++,最常见的后果是忘记释放内存;把 C++ 的习惯搬到 Java,则会误以为能控制对象何时销毁。
需要先澄清一个说法:Java 的new返回的是一个引用(reference),对象本身由 JVM 管理,通常分配在堆上;C++ 的new返回指针,指向一块由程序员负责释放的堆内存。两者都不是"返回值本身"。
另一个常见误解是"Java 有析构函数"。Java 没有 C++ 意义上的析构函数,finalize早已被弃用,且不保证执行;资源清理要靠try-with-resources、AutoCloseable或java.lang.ref.Cleaner。本文按内存、生命周期、引用语义、初始化顺序四条线对比。
一、对象放在哪里
Java 的对象由 JVM 分配,语言层面不规定必须在堆上;主流的 HotSpot 实现通常在堆上分配,逃逸分析等优化可能把某些对象拆解或分配到栈上,但这是实现细节,不是语言保证。
C++ 的对象可以放在三个地方:
- 栈上:
User u("alice");,作用域结束自动析构; - 堆上:
new User("alice"),返回指针,需要delete; - 静态存储区:全局对象、
static对象。
new只是堆分配的一种方式,现代 C++ 更推荐用std::make_unique/std::make_shared把裸new包起来。
// 适用于 Java 17
public class Demo {
static class User {
private final String name;
User(String name) {
this.name = name;
}
}
public static void main(String[] args) {
User u = new User("alice");
User v = u; // 复制引用,不是复制对象
System.out.println(v == u); // true,两个引用指向同一对象
}
}// 适用于 C++17
#include <iostream>
#include <memory>
#include <string>
class User {
public:
explicit User(std::string name) : m_name(std::move(name)) {}
private:
std::string m_name;
};
int main()
{
auto u = std::make_unique<User>("alice");
User *v = u.get(); // 观察指针,不拥有
std::cout << (v == u.get()) << "\n"; // 1
User w("bob"); // 栈对象,离开作用域自动析构
return 0;
}二、生命周期与释放
Java 的对象由垃圾回收器(garbage collector, GC)回收。只要还有可达的引用,对象就不会被回收;没有任何引用时,GC 会在某个不确定的时刻回收它。程序员不能指定回收时间,也不能强制立即回收。
C++ 的堆对象不会自动回收。new必须配对delete,new[]必须配对delete[]。忘记释放就是内存泄漏;重复释放或释放后使用是 UB。现代做法是用 RAII:std::unique_ptr独占所有权,std::shared_ptr共享所有权,容器和std::string自己管理内部内存。
// 适用于 C++17
#include <memory>
#include <string>
void scoped()
{
std::unique_ptr<std::string> p = std::make_unique<std::string>("hi");
// 离开作用域,p 析构,字符串自动释放
}std::shared_ptr用引用计数决定何时释放,但计数本身是原子操作,且循环引用会导致对象永远不释放,需要用std::weak_ptr打破环。这一点和 Java 的 GC 不同:Java 能处理循环引用,C++ 的引用计数不能。
三、引用语义
Java 的引用可以为null,可以重新绑定到另一个对象,不能做指针算术。方法传参是值传递,传的是引用的副本,所以在方法里让参数指向新对象,不会影响调用方:
// 适用于 Java 17
static void reassign(User u) {
u = new User("bob"); // 只改了局部引用
}C++ 的引用是别名,必须在初始化时绑定,之后不能改绑,也不能为 null(合法程序中)。指针可以为空、可以改指向、可以做算术,但需要自己保证有效性。
C++ 按值传参会发生拷贝,除非用引用或指针。多态传参通常按const Base&或Base*,按值传会发生对象切片。
四、初始化顺序
Java 创建对象的顺序大致是:分配内存并零初始化字段;调用父类构造;按声明顺序执行字段初始化器和实例初始化块;执行构造体。
C++ 的顺序是:分配内存;按基类声明顺序调用基类构造;按成员声明顺序调用成员构造;执行构造体。成员初始化列表的顺序不决定实际顺序,真正决定顺序的是成员在类中的声明顺序。
这个差异会导致一个经典错误:C++ 的初始化列表里写了m_b(b),但m_b声明在m_a之前,实际先用未初始化的m_a去初始化m_b。编译器通常会警告,但不会阻止。
五、对象比较
Java 的==比较引用身份,内容相等要用equals。C++ 的==可以被重载,对类类型通常比较内容;比较指针才是比较地址。
Java 的String是类,==比较引用,必须用equals。C++ 的std::string重载了==,直接比较内容。这是两种语言习惯差异最明显的地方之一。
六、对照表
| 维度 | Java | C++ |
|---|
new返回 | 对象引用 | 对象指针 |
| 对象位置 | 由 JVM 决定,通常堆上 | 栈、堆、静态区可选 |
| 释放 | GC 自动回收 | 手动delete或 RAII |
| 析构 | 无(finalize已弃用) | 确定性析构函数 |
| 引用能否为空 | 能 | 引用不能,指针能 |
| 引用能否改绑 | 能 | 引用不能 |
| 对象比较 | ==比身份,equals比内容 | ==可重载 |
| 循环引用 | GC 可处理 | shared_ptr会泄漏,需weak_ptr |
七、异常路径上的资源释放
C++ 的析构函数在栈展开(stack unwinding)时自动调用,所以 RAII 对象即使在中途抛异常也能释放资源,这是 C++ 资源管理的基石:
// 适用于 C++17
#include <fstream>
#include <stdexcept>
void writeFile()
{
std::ofstream out("data.txt"); // 构造即打开
if (!out) {
throw std::runtime_error("open failed");
}
out << "hello\n";
// 无论正常返回还是抛异常,out 析构时都会关闭文件
}Java 没有确定性析构,资源清理要显式写。try-with-resources会在离开代码块时调用close(),即使抛出异常也会调用:
// 适用于 Java 17
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
void writeFile() throws IOException {
try (BufferedWriter out = new BufferedWriter(new FileWriter("data.txt"))) {
out.write("hello");
}
}两者的效果类似,但机制不同:C++ 靠对象生命周期,Java 靠语言层面的try结构。因此 C++ 的 RAII 可以用于任意资源(锁、socket、临时状态),Java 的try-with-resources要求资源实现AutoCloseable。
常见坑点
- ❌ 把 Java 的引用当成 C++ 的引用,以为可以为空。
✅ C++ 引用必须绑定有效对象;可空语义用指针或std::optional。
- ❌ 在 C++ 里
new之后不释放,指望有 GC。
✅ 用std::unique_ptr/std::shared_ptr,或把对象放栈上。
- ❌ 认为 Java 对象一定在堆上。
✅ 语言不保证;JVM 的具体优化由实现决定。
- ❌ Java 里用
==比较字符串内容。
✅ 用equals;==比较的是引用身份。
- ❌ 认为 Java 的
finalize会可靠执行。
✅ 它已弃用且不保证时机;用try-with-resources或Cleaner。
- ❌ C++ 返回局部对象的指针或引用。
✅ 返回对象值(依赖拷贝省略/移动),或返回智能指针。
- ❌ Java 方法里给参数重新赋值,以为能改变调用方的引用。
✅ 传的是引用的副本;要修改对象内容,调用对象的方法。
- ❌ C++ 通过基类指针
delete派生对象,而基类析构不是virtual。
✅ 多态基类析构必须virtual,否则是 UB。
总结
| 问题 | Java 的答案 | C++ 的答案 |
|---|
| 谁分配 | JVM | 程序员选择栈或堆 |
| 谁释放 | GC | 析构函数 / 智能指针 |
| 何时释放 | 不确定 | 确定性,作用域或引用计数归零 |
| 能否控制 | 不能 | 能 |
| 主要风险 | 内存占用、GC 停顿 | 泄漏、悬垂、重复释放 |
两种语言的new只是拼写相同。Java 把内存管理的责任交给运行时,换来的是不可控的回收时机;C++ 把控制权交给程序员,换来的是确定性,也换来了必须自己保证正确性的负担。理解这一点,就不会把一边的习惯生搬到另一边。