菜单

Administrator
发布于 2026-05-22 / 0 阅读
0
0

Java 开发者学 C++(二):基础语法速通(瘦身版)

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 特有)。替代方案是 charuint8_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 表示空指针,永远不要用 NULL0
  • 坏味道:还在使用 NULL0 给指针赋值——这在重载场景下会导致意外的函数调用
  • 好习惯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 允许在 ifswitch 的条件中声明临时变量,限制其作用域:

#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)的新特性。关键要点:

  1. nullptr 替代 NULL0 表示空指针
  2. {} 统一初始化,获得窄化安全检查
  3. 用 C++17 的 if (init; condition) 缩小变量作用域
  4. 用范围 for 循环写出更简洁的迭代逻辑
  5. std::array / std::vector 替代 C 风格数组
  6. 始终初始化局部变量,优先使用初始化列表

下一篇文章将深入 C++ 的面向对象特性,看看类、继承、多态和 Java 有什么不同。


评论