Java 开发者面对 C++ 的语法其实不会太陌生——它们都源自 C 语言家族。但细节上有很多值得注意的差异,尤其是现代 C++(C++11/14/17/20)引入的大量新特性,让 C++ 的写法发生了质变。本文从 Java 开发者的视角出发,快速梳理 C++ 的基础语法,重点突出现代 C++ 的最佳实践。
基本数据类型
C++ 的基本类型和 Java 几乎一样,但有一个关键区别:Java 严格规定了每种类型的字节数保证跨平台一致;C++ 只保证最小值,实际大小取决于平台(这也是 C++ 高性能的代价之一——直接映射到硬件)。
| C++ 类型 | 最小位数 | 典型大小(64位) | Java 对应 | 说明 |
|---|---|---|---|---|
char |
8 位 | 1 字节 | char |
C++ 的 char 可能是 signed 或 unsigned(由实现定义) |
short |
16 位 | 2 字节 | short |
不变 |
int |
16 位 | 4 字节 | int |
在 16 位系统上是 2 字节 |
long |
32 位 | 8 字节 | long |
64 位系统上 C++ long = 8 字节 |
long long |
64 位 | 8 字节 | - | C++11 引入,保证至少 64 位 |
float |
- | 4 字节 | float |
IEEE 754 单精度 |
double |
- | 8 字节 | double |
IEEE 754 双精度 |
bool |
- | 1 字节 | boolean |
注意 C++ 是 bool,Java 是 boolean |
用 sizeof 检查类型大小:
#include <iostream>
int main() {
std::cout << "int: " << sizeof(int) << " bytes\n";
std::cout << "long: " << sizeof(long) << " bytes\n";
std::cout << "double: " << sizeof(double) << " bytes\n";
// C++11 提供了固定宽度整数类型
std::cout << "int32_t: " << sizeof(int32_t) << " bytes\n";
std::cout << "int64_t: " << sizeof(int64_t) << " bytes\n";
// sizeof 是运算符,不是函数(可以不加括号)
int x = 42;
std::cout << sizeof x << '\n'; // 合法,返回 4
return 0;
}
注意:C++ 没有 byte 类型(Java 特有)。替代方案是 char 或 uint8_t(来自 <cstdint>)。
nullptr、NULL 与 0:C++11 的空指针革命
Java 中 null 就是空引用,没有歧义。C++ 的情况就复杂多了——而 nullptr 正是为了解决历史遗留问题诞生的。
问题的根源
在 C++98/03 中,NULL 通常被定义为 0(整数零)或 ((void*)0)。但 C++ 不允许 void* 隐式转换成其他指针类型,所以实际实现中 NULL 基本上就是 0。这就导致了一个严重问题:
#include <iostream>
void foo(char*) {
std::cout << "foo(char*) called\n";
}
void foo(int) {
std::cout << "foo(int) called\n";
}
int main() {
foo(0); // 调用 foo(int) —— 0 是整数
// foo(NULL); // 如果 NULL==0,调用的也是 foo(int),违反直觉!
foo(nullptr); // 调用 foo(char*) —— nullptr 是空指针类型
return 0;
}
nullptr 的类型是 std::nullptr_t,它可以隐式转换为任何指针或成员指针类型,但不能转换为整数类型(除了 bool)。这从根本上解决了空指针的歧义。
完整验证代码(引自 changkun 现代 C++ 教程)
#include <iostream>
#include <type_traits>
void foo(char*) {
std::cout << "foo(char*) is called\n";
}
void foo(int) {
std::cout << "foo(int) is called\n";
}
int main() {
// 验证 NULL 到底是什么
if (std::is_same<decltype(NULL), decltype(0)>::value)
std::cout << "NULL == 0\n";
if (std::is_same<decltype(NULL), decltype((void*)0)>::value)
std::cout << "NULL == (void*)0\n";
if (std::is_same<decltype(NULL), std::nullptr_t>::value)
std::cout << "NULL == nullptr\n";
foo(0); // 调用 foo(int)
// foo(NULL); // 可能调用 foo(int) —— 取决于实现
foo(nullptr); // 明确调用 foo(char*)
return 0;
}
在现代 C++ 编译器上,输出通常为:
NULL == 0
foo(int) is called
foo(char*) is called
- ✅ 好习惯:始终用
nullptr表示空指针,永远不要用NULL或0 - ❌ 坏味道:还在使用
NULL或0给指针赋值——这在重载场景下会导致意外的函数调用 - ✅ 好习惯:
nullptr可以和任何指针类型比较,也兼容bool判断
变量定义与初始化
声明 vs 定义
Java 中变量要么声明要么定义,区分不严格。C++ 严格区分:
- 声明:告诉编译器这个名字存在,不分配空间
- 定义:实际分配空间
extern int x; // 声明:x 在其他地方定义
int x; // 定义:分配空间(如果不在函数内,默认初始化为 0)
四种初始化方式
C++11 引入了统一初始化(Uniform Initialization),推荐使用花括号 {}:
int a = 10; // ① 等号初始化(C 风格/拷贝初始化)
int b(10); // ② 构造函数风格/直接初始化
int c{10}; // ③ C++11 统一初始化/列表初始化(推荐)
int d = {10}; // ④ 等号+花括号初始化
auto e = 10; // ⑤ 类型推导初始化
这四种方式在大多数情况下效果相同,但细节上有重要差异。
统一初始化 {} 的最大优点:防止窄化转换
花括号初始化最关键的特性是防止窄化转换(Narrowing Conversion)——即丢失精度的隐式类型转换:
int x{3.14}; // 编译错误或警告:double→int 丢失精度
int y = 3.14; // 通过编译,y = 3(静默丢失精度!)
double d{1.0f}; // OK:float→double 是拓宽转换,安全
float f{3.14}; // 警告或错误:double→float 丢失精度
// 窄化转换对整型同样有效
int a{42}; // OK
int b{100000L}; // 如果 long 大于 int,编译错误
char c{256}; // 编译错误:256 超出 char 范围
char d = 256; // 静默截断!未定义行为
// 更实际的例子
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec{1, 2, 3}; // 列表初始化 vector
// 注意陷阱:vector<int>(n, val) vs vector<int>{n, val}
std::vector<int> v1(10, 42); // 10 个 42
std::vector<int> v2{10, 42}; // 2 个元素:10 和 42(初始化列表优先!)
std::cout << "v1 size: " << v1.size() << '\n'; // 10
std::cout << "v2 size: " << v2.size() << '\n'; // 2
return 0;
}
- ✅ 好习惯:默认使用
{}初始化,获得窄化安全检查 - ⚠️ 注意事项:
auto和{}的交互——auto x{1}在 C++17 中推导为int(不是initializer_list<int>)
默认初始化
Java 中类的成员变量会自动初始化为 0/0.0/false/null,局部变量必须手动初始化。
C++ 的规则不同:
- 全局变量(函数外):自动初始化为 0
- 局部变量(函数内):不自动初始化! 值是不确定的
- 类成员:取决于构造函数是否显式初始化
#include <iostream>
int global_x; // 自动初始化为 0
int main() {
int local_y; // 未初始化!值不确定(可能是垃圾值)
static int static_z; // 静态局部变量自动初始化为 0
std::cout << global_x; // OK:输出 0
std::cout << local_y; // 危险!未定义行为(UB)
int safe_y{}; // ✅ 好习惯:用 {} 确保零初始化
std::cout << safe_y; // 输出 0
return 0;
}
- ❌ 坏味道:局部变量定义后不立即初始化
- ✅ 好习惯:定义变量时立即初始化,即使只是
int x{};
const:比 Java 的 final 更强大
Java 的 final 表示变量不可变。C++ 的 const 有更多用法:
const int MAX_SIZE = 100; // 类似 Java 的 final int MAX_SIZE = 100;
// MAX_SIZE = 200; // 编译错误
// const 指针:读法从右向左读
const int* ptr1; // 指针指向 const int(所指对象不可改)
int* const ptr2 = nullptr; // const 指针(指针本身不可改,指向可改)
const int* const ptr3 = nullptr; // 两者都不可改
// const 成员函数:承诺不会修改对象状态
class Person {
public:
std::string getName() const { return name_; } // const 成员函数
void setName(const std::string& name) { name_ = name; }
private:
std::string name_;
};
控制流
C++ 的控制流和 Java 基本一样,但有几处增强。
if/else、while、do-while、for
// if-else(和 Java 一致)
if (condition) {
// ...
} else if (condition2) {
// ...
} else {
// ...
}
// for 循环(和 Java 一致)
for (int i = 0; i < 10; ++i) { } // 注意前缀 ++ 是 C++ 习惯
// while / do-while
while (condition) { }
do { } while (condition);
C++17 if/switch 变量声明强化
C++17 允许在 if 和 switch 的条件中声明临时变量,限制其作用域:
#include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> scores = {
{"Alice", 95}, {"Bob", 87}
};
// ❌ C++17 之前:变量泄漏到外部作用域
auto it = scores.find("Alice");
if (it != scores.end()) {
std::cout << "Found: " << it->first << '\n';
}
// it 仍然在这个作用域中!
// 再次查找需要新变量名
auto it2 = scores.find("Bob");
if (it2 != scores.end()) {
std::cout << "Found: " << it2->first << '\n';
}
// ✅ C++17:变量声明在 if 内,作用域受限
if (auto it = scores.find("Alice"); it != scores.end()) {
std::cout << "Found: " << it->first << '\n';
}
// 这里的 it 已不可见
if (auto it = scores.find("Bob"); it != scores.end()) {
std::cout << "Found: " << it->first << '\n';
}
// 同一个变量名在下一个 if 中安全重用
// switch 也支持
switch (auto status = getStatus(); status) {
case 0: std::cout << "OK\n"; break;
case 1: std::cout << "Warning\n"; break;
default: std::cout << "Unknown: " << status << '\n'; break;
}
return 0;
}
int getStatus() { return 0; }
范围 for 循环(Range-based for loop)
C++11 引入了基于范围的 for 循环,类似 Java 的 for-each:
#include <iostream>
#include <vector>
#include <array>
#include <map>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 值拷贝(原始元素不变)
for (int x : vec) {
std::cout << x << ' ';
}
std::cout << '\n';
// 引用修改
for (int& x : vec) {
x *= 2;
}
// 常量引用(高效只读)
for (const auto& x : vec) { // auto 自动推导类型
std::cout << x << ' '; // 2 4 6 8 10
}
std::cout << '\n';
// C++17 结构化绑定 + 范围 for
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score << '\n';
}
// 也支持 C 风格数组
int arr[] = {10, 20, 30};
for (int x : arr) {
std::cout << x << ' ';
}
return 0;
}
范围 for 循环的最佳实践:
| 写法 | 语义 | 适用场景 |
|---|---|---|
for (auto x : c) |
值拷贝 | 小对象,不需要修改 |
for (auto& x : c) |
引用 | 需要修改元素 |
for (const auto& x : c) |
常量引用 | 大对象,只读(推荐) |
for (auto&& x : c) |
转发引用 | 泛型代码 |
- ✅ 好习惯:优先用范围 for 循环代替索引循环(更安全、更可读)
- ✅ 好习惯:用
const auto&作为范围 for 循环的默认选择
函数
函数定义
// 返回类型 函数名(参数列表)
int add(int a, int b) { return a + b; }
// 函数重载(和 Java 一致)
void print(int x);
void print(const std::string& s);
// 默认参数(Java 没有这个特性)
void greet(const std::string& name, const std::string& prefix = "Hello") {
std::cout << prefix << ", " << name << "!\n";
}
greet("Alice"); // "Hello, Alice!"
greet("Bob", "Hi"); // "Hi, Bob!"
传参方式
这是 C++ 和 Java 最关键的差异之一:
void by_value(int x); // 值传递(拷贝副本)
void by_ref(int& x); // 引用传递(直接操作原变量)
void by_const_ref(const int& x); // 常量引用(高效且不修改)
void by_pointer(int* x); // 指针传递(可以设为 nullptr)
// 完整对比
#include <iostream>
struct BigObject {
int data[1000]; // 大对象
};
void pass_by_value(BigObject obj) {
// 拷贝了整个 BigObject!(1000 个 int)
}
void pass_by_const_ref(const BigObject& obj) {
// 只是传递了引用(8 字节指针),高效!
// obj.data[0] = 42; // 编译错误:const 引用禁止修改
std::cout << obj.data[0] << '\n';
}
void pass_by_ref(BigObject& obj) {
obj.data[0] = 42; // 可以修改
}
int main() {
BigObject obj{};
pass_by_const_ref(obj); // ✅ 高效只读
pass_by_ref(obj); // ✅ 需要修改时
return 0;
}
C++ 传参最佳实践:
- 对大型对象用
const &传参(既高效又安全) - 需要修改原对象时用
& - 简单类型(int, double, 指针)用值传递
- 指针通常用智能指针传递所有权语义
函数返回值
int get_value(); // 返回值(拷贝)
const std::string& get_name(); // 返回引用(避免拷贝,但注意生命周期)
std::unique_ptr<Object> create(); // 返回智能指针(所有权转移)
// 现代 C++ 推荐:返回值优化(RVO)保证值返回高效
// 不要害怕返回大对象——编译器会自动优化拷贝
std::vector<int> create_large_vector() {
std::vector<int> result(1000000);
// ... 填充数据 ...
return result; // NRVO:不拷贝,直接构造到调用者栈上
}
命名空间
Java 用 package 组织类,C++ 用 namespace:
namespace com::example::util { // C++17 嵌套命名空间
int helper_function() { return 42; }
}
// 使用完全限定名
int x = com::example::util::helper_function();
// 使用声明(类似 import)
using com::example::util::helper_function;
// using 指令(类似 import package.*)
// using namespace com::example::util;
| Java | C++ |
|---|---|
import java.util.*; |
using namespace std; |
package com.example; |
namespace com::example {} |
import java.util.List; |
using std::vector; |
- ❌ 坏味道:在头文件中
using namespace std;——会导致命名空间污染 - ✅ 好习惯:只在
.cpp文件中且仅在必要时使用using namespace std; - ✅ 好习惯:或者使用更精准的
using std::cout; using std::vector;
字符串
#include <string>
#include <iostream>
int main() {
std::string s1 = "hello";
std::string s2 = "world";
// 拼接
std::string s3 = s1 + " " + s2; // "hello world"
// 长度(两种等价)
std::cout << s1.length(); // 5
std::cout << s1.size(); // 5
// 内容比较(直接 ==,和 Java 不同——Java 不能用 == 比较字符串内容)
if (s1 == "hello") {
std::cout << "Equal!\n";
}
// 原始字符串字面量(C++11),不用转义反斜杠
std::string path = R"(C:\Users\name\file.txt)";
std::string html = R"(<html>
<body>
<p>Hello</p>
</body>
</html>)";
// C++14 字符串字面量
auto s = "hello"s; // s 是 std::string,不是 const char*
auto hello = "hello"sv; // C++17 string_view
return 0;
}
- ✅ 好习惯:用
std::string代替 C 风格char*字符串 - ✅ 好习惯:只读字符串参数用
std::string_view(C++17)代替const std::string&
数组:std::array 代替 C 风格数组
C 风格数组的缺点
// ❌ 坏味道:C 风格数组
void bad_array() {
int arr[10]; // 未初始化!值是垃圾
arr[20] = 42; // 越界访问!没有边界检查,未定义行为
// int size = sizeof(arr) / sizeof(arr[0]); // 需要手动算大小
}
// 函数传参时 C 数组退化为指针
void process(int arr[]) { // 实际上是 int* arr
// sizeof(arr) 返回的是指针大小,不是数组大小!
}
std::array(C++11)——C++ 风格的静态数组
#include <array>
#include <iostream>
#include <algorithm>
int main() {
// ✅ 好习惯:用 std::array
std::array<int, 5> arr = {1, 2, 3, 4, 5};
// 知道自己的大小
std::cout << arr.size() << '\n'; // 5
// 边界检查(at() 方法抛异常)
// arr.at(10); // 抛出 std::out_of_range
// 范围 for 支持
for (const auto& x : arr) {
std::cout << x << ' ';
}
// 标准算法支持
std::sort(arr.begin(), arr.end());
// 和 C 数组兼容
int* raw_ptr = arr.data();
// 不自动初始化的问题
std::array<int, 5> empty{}; // ✅ 全部初始化为 0
// std::array<int, 5> bad; // ❌ 未初始化
return 0;
}
动态数组:std::vector 代替 Java 的 ArrayList
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3}; // 列表初始化
vec.push_back(4); // 尾部添加
vec.pop_back(); // 尾部移除
std::cout << vec.size() << '\n'; // 3
std::cout << vec.capacity() << '\n'; // 当前容量
// 预分配(避免反复扩容)
vec.reserve(1000);
// 访问
vec[0]; // 无边界检查(快)
vec.at(0); // 有边界检查(抛异常)
return 0;
}
- ✅ 好习惯:用
std::array替代 C 风格数组(编译期固定大小) - ✅ 好习惯:用
std::vector替代动态 C 数组 - ❌ 坏味道:仍在用 C 风格数组和
char*字符串 - ❌ 坏味道:在头文件中使用 C 风格数组作为函数参数(会退化为指针)
常用容器对照
| 操作 | Java | C++(现代 C++) |
|---|---|---|
| 动态数组 | ArrayList<E> |
std::vector<T> |
| 链表 | LinkedList<E> |
std::list<T> |
| 哈希表 | HashMap<K,V> |
std::unordered_map<K,V> |
| 有序映射 | TreeMap<K,V> |
std::map<K,V> |
| 哈希集合 | HashSet<E> |
std::unordered_set<T> |
| 有序集合 | TreeSet<E> |
std::set<T> |
| 双端队列 | ArrayDeque<E> |
std::deque<T> |
| 固定数组 | - | std::array<T,N> |
| 字符串 | String |
std::string |
好习惯与坏味道速查表
✅ 需要养成的好习惯
| 好习惯 | 说明 |
|---|---|
统一初始化 {} |
防止窄化转换,所有类型一致 |
nullptr 代替 NULL |
解决重载歧义,类型安全 |
const auto& + 范围 for |
高效、安全的循环 |
std::array / std::vector |
替代 C 风格数组 |
std::string |
替代 char* |
| 初始化列表 | 成员初始化优先用初始化列表 |
| 局部变量立即初始化 | 避免未定义行为 |
❌ 需要避免的坏味道
| 坏味道 | 问题 |
|---|---|
用 C 风格数组不用 std::array |
退化指针、无边界检查、无 size 方法 |
| 局部变量不初始化 | 未定义行为,难以调试 |
用 NULL 不用 nullptr |
重载歧义、类型不安全 |
滥用 using namespace std(尤其在头文件中) |
命名空间污染 |
new/delete 手动管理 |
内存泄漏风险,现代 C++ 用智能指针 |
| 函数体赋初值而不是初始化列表 | 两次初始化,降低效率 |
#define 定义常量 |
无类型检查,无作用域 |
编译运行
# 编译单个源文件
g++ -std=c++17 -Wall -Wextra main.cpp -o main
# 编译多个源文件
g++ -std=c++17 -Wall -Wextra main.cpp utils.cpp -o my_app
# 运行
./my_app
总结
本文从 Java 开发者的视角快速梳理了 C++ 基础语法,重点突出了现代 C++(C++11/14/17/20)的新特性。关键要点:
- 用
nullptr替代NULL和0表示空指针 - 用
{}统一初始化,获得窄化安全检查 - 用 C++17 的
if (init; condition)缩小变量作用域 - 用范围 for 循环写出更简洁的迭代逻辑
- 用
std::array/std::vector替代 C 风格数组 - 始终初始化局部变量,优先使用初始化列表
下一篇文章将深入 C++ 的面向对象特性,看看类、继承、多态和 Java 有什么不同。