基础语法
这一课你将能做到
- 声明 C++ 的基本类型与
std::string,并说清整型溢出、浮点精度这类 Java 里同样存在的坑 - 用
if/switch/while/for写出判分逻辑,理解 C++ 条件必须是布尔值的严格性 - 编写并调用自由函数,区分值传递与
const std::string&传参,知道为什么大对象要传引用
为什么要学
任何判题逻辑的骨架都是「读输入 → 判断 → 循环测试点 → 输出结论」,拆开全是变量、控制流、函数三件套。你之后要读判题沙箱源码、刷算法题、写评测脚本,全都建立在这一课的地基上。
和 Java 对着学最快:C++ 的类型系统、运算符、控制流语法和 Java 几乎同源(都继承了 C 的衣钵),但 C++ 更「较真」——Java 里 if (score = 60) 因为类型不符直接编译报错,C++ 里它会把 60 赋给 score 再当真值判断,埋一颗闷雷。本课会带你在真实代码里躲开这些差异点。
先建立图像
一道题的评测流程,就是基础语法的全景图:
读入用户 ID(字符串)→ 变量
逐个测试点比对输出 → 循环
全对算 AC,否则算 WA → 分支
把「比对一个测试点」抽成函数 → 函数
int score = 87;
┌────────────┬─────────┐
│ 类型 int │ 名字 │
│ 告诉编译器 │ score │
│ 占4字节、 │ 指向那块│
│ 按整数解释 │ 内存 │
└────────────┴─────────┘
C++ 变量 = 一块有类型的内存,类型错了不悄悄转换(对比 Java 的自动装箱)
跟着做
步骤 1:变量与基本类型
新建 basics.cpp,写一个「提交信息卡片」:
#include <iostream>
#include <string>
int main() {
std::string user = "nihao_2024"; // 字符串:std::string,对应 Java 的 String
int score = 87; // 整型:Java 的 int
double ratio = 0.875; // 双精度浮点:Java 的 double
bool accepted = false; // 布尔:Java 的 boolean
char level = 'B'; // 单字符:用单引号,Java 的 char
std::cout << user << " 得分 " << score
<< ",通过率 " << ratio << "\n";
const int MAX_SCORE = 100; // const = Java 的 final,不可再赋值
std::cout << "满分 " << MAX_SCORE << "\n";
return 0;
}
g++ -std=c++17 basics.cpp -o basics && ./basics
常用整型一览(面试与工程都会遇到):
| 类型 | 典型大小 | Java 近似 | 使用场景 |
|---|---|---|---|
int | 4 字节 | int | 一般计数,约 ±21 亿 |
long long | 8 字节 | long | 算法题防溢出首选 |
size_t | 8 字节 | 无 | 容器大小、数组下标(无符号) |
double | 8 字节 | double | 浮点计算 |
步骤 2:控制流——给提交定级
#include <iostream>
int main() {
int score = 87;
if (score >= 90) {
std::cout << "AC:全部通过\n";
} else if (score >= 60) {
std::cout << "PA:部分通过\n";
} else {
std::cout << "WA:答案错误\n";
}
// switch:按评测结果码分流
int verdict = 2; // 0=CE 编译失败 1=TLE 超时 2=WA 答案错 3=AC 通过
switch (verdict) {
case 0: std::cout << "编译失败,检查语法\n"; break;
case 1: std::cout << "超出时间限制\n"; break;
case 2: std::cout << "答案错误\n"; break;
case 3: std::cout << "通过!\n"; break;
default: std::cout << "未知状态\n"; break; // 忘写 break 会「贯穿」到下一分支
}
return 0;
}
步骤 3:循环——跑完所有测试点
#include <iostream>
int main() {
// 10 个测试点的用户耗时(毫秒)
int costs[] = {12, 8, 45, 30, 22, 19, 60, 3, 11, 7};
int n = 10;
// 经典 for:下标遍历
int total = 0;
for (int i = 0; i < n; ++i) {
total += costs[i];
}
std::cout << "总耗时 " << total << "ms\n";
// while:找到第一个超时的测试点
int limit = 40;
int idx = 0;
while (idx < n && costs[idx] <= limit) {
++idx;
}
if (idx < n) {
std::cout << "第 " << idx + 1 << " 个测试点超时,耗时 "
<< costs[idx] << "ms\n";
}
return 0;
}
输出:
总耗时 217ms
第 3 个测试点超时,耗时 45ms
步骤 4:函数——把「判定单个测试点」抽出来
#include <iostream>
#include <string>
// 返回 bool 的自由函数(不属于任何类的函数,Java 没有「类外函数」概念)
// expected / actual 用值传递:各复制一份,函数内改动不影响外面
bool judgeCase(const std::string& expected, const std::string& actual) {
// const std::string& :常量引用传递——不复制字符串,只「借」过来看
// 字符串越长,引用传递省下的复制开销越大;判题对输出逐字节比对时全是这种写法
return expected == actual;
}
int maxOf(int a, int b) {
return a > b ? a : b; // 三目运算符,和 Java 相同
}
int main() {
if (judgeCase("42\n", "42\n")) {
std::cout << "测试点通过\n";
}
std::cout << "最大耗时 " << maxOf(120, 87) << "ms\n";
return 0;
}
步骤 5:函数要先声明后使用
C++ 编译器是「从上往下」读文件的,调用前必须见过这个函数。拆成声明 + 定义:
#include <iostream>
int square(int x); // 声明(原型):只告诉编译器签名,不写实现
int main() {
std::cout << square(9) << "\n"; // 可以先调用
return 0;
}
int square(int x) { // 定义:真正的实现
return x * x;
}
Java 方法不用提前声明,因为编译器会扫整个类;C++ 的多文件工程全靠头文件里的声明互相认识——这就是上一课说 .hpp 放声明的原因。
讲清楚要点
if条件必须是布尔语境:if (score)在 Java 里直接编译错误,在 C++ 里任何非零值都算真。整数当条件用是 C++ 世纪 bug 之源(if (x = 5)是赋值且恒真)。好习惯:比较一律写==,编译时加-Wall让警告全开。- 整型溢出是未定义行为:Java 的 int 溢出会安静回绕,C++ 的有符号整型溢出是 UB(Undefined Behavior)——编译器可以生成任何行为。算法题里两个 10^9 相乘必须换
long long。 std::string是值类型:std::string b = a;会复制一份新字符串,两个对象互不影响;Java 的String是引用,赋值只是共享。想要 C++ 也共享,得用引用或指针(下一课)。- 值传递 = 复制:函数参数默认按值复制。小对象无所谓,
std::string、std::vector这类大对象用const T&传参避免复制——这是 C++ 代码评审里最常见的意见。 - 初始化风格:现代 C++ 推荐花括号初始化
int x{42};,它能拦截窄化转换(int x{3.14}直接报错,而int x = 3.14静默丢精度)。
你可能会懵的地方
-
int到底多少字节? 标准只保证int至少 16 位,主流平台是 32 位。要确定性宽度用<cstdint>里的int32_t、int64_t,判题沙箱计量字节数时全用定宽类型。 -
std::string和 C 风格char*什么关系?std::string内部管理一块字符内存,自动扩容、自动释放;char*是裸指针,指向一块要自己管的生命周期。跟 C 库交互时才需要s.c_str()拿到const char*。日常一律用std::string。 -
为什么
0.1 + 0.2 != 0.3? 和 Java 一样,浮点遵循 IEEE 754 二进制表示,十进制小数大多不能精确表示。浮点比较要写成std::abs(a - b) < 1e-9,判题里「浮点答案误差容忍 1e-6」就是这个原理。 -
++i和i++有区别吗? 结果一样,但++i不产生临时对象,对迭代器(后面 STL 课会讲)有性能差异。C++ 圈的习惯是循环里都写++i。 -
C++ 有三元运算符之外更优雅的条件写法吗? C++17 if 初始化语句:
if (int r = compute(); r > 60) { ... }——把临时变量的作用域锁死在 if 里,Java 没有这个能力。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
if (x = 5) | 赋值表达式恒为真,逻辑悄悄跑偏 | if (x == 5),并开 -Wall 警告 |
for (int i = 0; i <= n; ++i) 配下标 n-1 | 数组越界,UB | 循环边界用 i < n,多一次自测 |
char c = "A"; | 双引号是字符串(指针),不是字符 | 单引号 char c = 'A'; |
| 函数定义写在调用之后且无声明 | 编译报错「未声明」 | 先声明或调整定义顺序 |
大对象参数写 void f(std::vector<int> v) | 每次调用整体复制 | void f(const std::vector<int>& v) |
卡点急救
- 程序「闪退」看不到输出:终端窗口运行而不是双击 exe;或在
return 0;前加一行std::cin.get();等待回车。 - 计算结果变成负数/巨大数:整型溢出。把相关变量改
long long,乘法前先1LL * a * b强制按 64 位算。 switch每个分支都执行了:漏了break,分支发生贯穿。每个case结尾都写break,唯一例外是故意贯穿并加注释说明。
面试常问
-
C++ 的值传递和引用传递有什么区别?什么时候用哪个? 值传递复制实参,函数内修改不影响外部,小对象用;引用传递是实参的别名,无复制开销,函数内修改会反映到外部,大对象只读用
const T&、需要修改用T&。输出型参数现代 C++ 更倾向返回值或结构化对象。 -
什么是有符号整型溢出?C++ 和 Java 行为一样吗? Java 规定 int 溢出按二进制回绕(补码环绕);C++ 把有符号溢出定义为未定义行为,编译器可基于「假设不溢出」做激进优化,导致结果不可预测。工程上用定宽类型并在关键计算前检查范围。
-
const在 C++ 里有哪些用法? 修饰变量(不可修改,类似 Java final)、修饰指针(指向常量 / 指针本身常量)、修饰函数参数(承诺不修改)、修饰成员函数(承诺不改对象状态)。面试常考const int* p(指向的值不可改)与int* const p(指针本身不可改)的区别。
自测三问
- 为什么函数参数推荐
const std::string&而不是std::string? - C++ 有符号整型溢出会发生什么?和 Java 的差异是什么?
switch里漏写break会发生什么?
小结一句话
C++ 基础语法和 Java 同源但更较真:类型是真实的内存布局、条件必须布尔、溢出是 UB、大对象传参用 const 引用——把这几个差异点刻进肌肉记忆,后面的路就顺了。
延伸
- cppreference:C++ 语言基础
- cplusplus.com:控制结构教程
- 下一课:指针与引用——C++ 内存世界的钥匙。