⚠️考虑到目前很多编译器以及编译环境的版本不高,或者对于 STL 库的支持不是按么完善。 C++11 及以上版本的相关语法我会标注,但大部分语法会控制在 C++20 以内
编译环境
C++有三个主流的编译环境下面分别介绍,后续内容基于 Clang 17(VSCode)。环境以及 IDE 的安装此处不做记载
GCC (GNU Compiler Collection)
- 命令行工具:
g++ - 特点:开源世界的标准,历史最悠久,支持的平台极其广泛,是所有Linux发行版的默认编译器。
- 标准库:
libstdc++
Clang
- 命令行工具:
clang++ - 特点:作为LLVM项目的一部分,以其模块化设计、更快的编译速度和非常友好、清晰的错误提示信息而闻名。Apple的Xcode默认使用它,并且在Google等许多公司中也非常流行。
- 标准库:通常搭配
libc++(LLVM自己的标准库实现) 或libstdc++。
MSVC (Microsoft Visual C++)
- 命令行工具:
cl.exe - 特点:Windows平台的官方编译器,与Visual Studio IDE深度集成,对Windows API的支持是最好的。
- 标准库:有其自己的标准库实现。
除去这些编译器,cmake 作为构建 C++项目最强劲的构建系统在后面也会有所涉及。
变量
变量的定义与初始化
在C++中,定义一个变量就意味着为它分配内存。初始化则是在分配内存的同时给它赋一个初始值。一个好习惯是:在定义变量时立即进行初始化,以避免使用未定义的值。
C++的初始化方法随着语言的发展变得越来越丰富。
所有变量型
| 数据类型 | 描述 | 大小(字节) | 范围 / 取值示例 |
|---|---|---|---|
bool | 布尔类型,表示真或假 | 1 | true 或 false |
char | 字符类型,通常用于存储 ASCII 字符 | 1 | -128 到 127 或 0 到 255 |
signed char | 有符号字符类型 | 1 | -128 到 127 |
unsigned char | 无符号字符类型 | 1 | 0 到 255 |
wchar_t | 宽字符类型,用于存储 Unicode 字符 | 2 或 4 | 取决于平台 |
char 16_t | 16 位 Unicode 字符类型(C++11 引入) | 2 | 0 到 65,535 |
char 32_t | 32 位 Unicode 字符类型(C++11 引入) | 4 | 0 到 4,294,967,295 |
short | 短整型 | 2 | -32,768 到 32,767 |
unsigned short | 无符号短整型 | 2 | 0 到 65,535 |
int | 整型 | 4 | -2,147,483,648 到 2,147,483,647 |
unsigned int | 无符号整型 | 4 | 0 到 4,294,967,295 |
long | 长整型 | 4 或 8 | 取决于平台 |
unsigned long | 无符号长整型 | 4 或 8 | 取决于平台 |
long long | 长长整型(C++11 引入) | 8 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
unsigned long long | 无符号长长整型(C++11 引入) | 8 | 0 到 18,446,744,073,709,551,615 |
float | 单精度浮点数 | 4 | 约 ±3.4 e±38(6-7 位有效数字) |
double | 双精度浮点数 | 8 | 约 ±1.7 e±308(15 位有效数字) |
long double | 扩展精度浮点数 | 8、12 或 16 | 取决于平台 |
C++11 新增类型
| 数据类型 | 描述 | 示例 |
|---|---|---|
auto | 自动类型推断 | auto x = 10; |
decltype | 获取表达式的类型 | decltype (x) y = 20; |
nullptr | 空指针常量 | int* ptr = nullptr; |
std::initializer_list | 初始化列表类型 | std::initializer_list< int> list = {1, 2, 3}; |
std::tuple | 元组类型,可以存储多个不同类型的值 | std::tuple< int, float, char> t (1, 2.0, 'a'); |
1. C风格初始化 (C-style Initialization)
这是从C语言继承过来的方法,使用等号 =。它也被称为“拷贝初始化”(Copy Initialization)。
// 语法: type variable_name = value;
int age = 25;
char initial = 'J';
std::string name = "Alice";
double pi = 3.14159;
这种方式对于内置的基本数据类型非常直观和常见。
2. 函数风格初始化 (Function-style Initialization)
这种方式使用圆括号 (),看起来像一个函数调用。它也被称为“直接初始化”(Direct Initialization)。
// 语法: type variable_name(value);
int age(25);
char initial('J');
std::string name("Alice");
double pi(3.14159);
对于类类型的对象,这种方式更清晰地表达了调用构造函数的意图。
3. 列表初始化 (List Initialization) - (C++11及以后,推荐)
C++11引入了使用花括号 {} 的初始化方式,也称为“统一初始化”(Uniform Initialization)或“大括号初始化”(Brace Initialization)。这是目前最推荐的初始化方法。
它有两种形式:
- 直接列表初始化:
type variable_name {value}; - 拷贝列表初始化:
type variable_name = {value};
// 语法: type variable_name {value}; 或者 type variable_name = {value};
int age {25};
char initial {'J'};
std::string name {"Alice"};
double pi = {3.14159};
// 也可以用于更复杂的类型
int numbers[] {1, 2, 3, 4, 5}; // 数组初始化
std::vector<int> ages {22, 25, 30}; // STL容器初始化
为什么列表初始化是最佳选择?
- 通用性:它几乎可以用于所有类型的初始化,包括普通变量、数组、STL容器、自定义类等,语法非常统一。
- 安全性(防止类型收窄):它不允许“收窄转换”(Narrowing Conversion),即从一个可能丢失信息的类型转换。这可以帮助在编译阶段发现潜在的错误。
Eg:
int x = 7.8; // C风格,合法,但double被截断为int,x的值是7,小数部分丢失,编译器可能只给一个警告
int y(7.8); // 函数风格,同上,y的值是7
// int z {7.8}; // 列表初始化,不合法!编译器会直接报错,因为它阻止了从double到int的收窄转换
// int w = {7.8}; // 同上,也会报错
// 正确的做法
int z {7};
- 解决“最令人头疼的解析” (Most Vexing Parse): 在C++中,MyClass obj(); 不会被解析为一个对象定义,而是一个函数声明。但使用列表初始化 MyClass obj{}; 就没有这个歧义,它明确地定义了一个对象。
二、 特殊的定义方法
1. auto 类型推导 (C++11)
如果你在定义变量时就对其进行了初始化,可以使用 auto 关键字,让编译器自动推断出变量的类型。
auto i = 42; // i 被推断为 int
auto d = 3.14; // d 被推断为 double
auto s = std::string("C++"); // s 被推断为 std::string
auto numbers = {1, 2, 3}; // numbers 被推断为 std::initializer_list<int>
2. const 和 constexpr 定义常量
const:用于定义一个运行时常量。一旦被初始化,它的值就不能再被改变。constexpr(C++11):用于定义一个编译时常量。它的值必须在编译期间就能确定。
const int MAX_USERS = 100; // 运行时常量
// MAX_USERS = 200; // 错误!不能修改const变量
constexpr double G = 9.8; // 编译时常量
// constexpr int r = rand(); // 错误!rand()的值在运行时才能知道
三、 变量的赋值
赋值是指在变量已经定义并存在之后,使用赋值运算符 (=) 来改变它的值。
// 语法: variable_name = new_value;
int score; // 1. 定义一个名为 score 的变量,此时它的值是未定义的
score = 95; // 2. 第一次赋值:现在 score 的值是 95
score = 100; // 3. 第二次赋值:score 的值被更新为 100
std::string message; // 定义一个空字符串
message = "Hello, World!"; // 赋值
关键区别:初始化 vs 赋值
虽然它们都可能使用 = 符号,但意义完全不同:
std::string book = "The Lord of the Rings"; // 这是定义并初始化,调用构造函数
std::string movie; // 这是定义,调用默认构造函数
movie = "The Hobbit"; // 这是赋值,调用赋值操作符函数
当然,这是一个内容更详尽的版本,包含了每个修饰符的含义、用途和写法示例。
C++ 修饰符详细功能表
类型限定符 (Type Qualifiers)
这类修饰符用于限定类型的属性,影响变量的可修改性与编译器的优化行为。
| 修饰符 | 核心含义与用途 | 写法示例 | C++ 版本 / 状态 |
|---|---|---|---|
const | 含义: 将一个对象声明为常量,其值在初始化后不能被修改。 用途: 1. 创建不可变的常量,增强代码可读性和安全性。 2. 在函数接口中,通过 const引用或指针避免不必要的数据拷贝并保证数据不被修改。 3. 在类中,const成员函数承诺不修改对象的状态。 | // 常量变量 const double PI = 3.14159; // 指向常量的指针 (保护数据) void print(const std::string& msg); // const成员函数 class MyClass { int getValue() const; }; | C++98 |
volatile | 含义: 告知编译器,变量的值可能会在程序控制之外被意外更改(如硬件、另一线程)。 用途: 阻止编译器对该变量的读写操作进行优化(如缓存到寄存器),确保每次都从内存中读取/写入。主要用于嵌入式编程访问硬件寄存器或某些多线程同步场景。 | // 指向硬件状态寄存器的指针 volatile bool* is_data_ready = (volatile bool*)0x1234; // 在多线程中共享的标志位 volatile bool should_stop = false; | C++98 |
restrict | 含义: (用于指针) 向编译器提供的一个优化提示,表明该指针是访问其所指向内存区域的唯一途径。 用途: 帮助编译器消除对“指针别名”(aliasing)的担忧,从而生成更高效、更积极优化的机器码。 | // C++23 标准写法 void copy(int* restrict dest, const int* restrict src, size_t n); // 常见编译器扩展写法 void copy(int* restrict dest,...); | [C++23] (之前为常见编译器扩展) |
存储类说明符 (Storage Class Specifiers)
这类修饰符决定了变量/函数的生命周期(存在多久)和链接性(在哪些地方可见)。
| 修饰符 | 核心含义与用途 | 写法示例 | C++ 版本 / 状态 |
|---|---|---|---|
static | 含义: 有多种用途:1. 函数内: 变量的生命周期与程序相同,但作用域局部,其值在函数调用间保持。 2. 全局/命名空间: 变量或函数具有内部链接性,仅在当前源文件可见。 3. 类中: 成员为所有类实例共享。 | // 函数内: 调用计数器 void func() { static int count = 0; ++count; } // 文件作用域: 私有辅助函数 static void helper_func() { /.../ } // 类中: 共享的常量 class WebServer { static int port; }; | C++98 |
extern | 含义: 声明一个变量或函数是在其他源文件中定义的。 用途: 用于在多个源文件之间共享全局变量或函数。它只做声明,不分配内存。 | // file_A.cpp int g_counter = 100; // file_B.cpp extern int g_counter; // 使用在A中定义的g_counter void use_counter() { g_counter++; } | C++98 |
thread_local | 含义: 声明的变量在每个线程中都有一个独立的实例。 用途: 创建线程私有的全局或静态变量,避免在多线程中手动管理线程特定数据,也避免了竞态条件。 | // 每个线程都有自己的日志ID thread_local int g_log_id = 0; void thread_func(int id) { g_log_id = id; // 只修改本线程的副本 } | [C++11] |
mutable | 含义: 允许类的非静态成员变量在 const 成员函数中被修改。用途: 用于实现“逻辑常量性”,即对象对外表现为常量,但其内部某些状态(如缓存、互斥锁)需要修改。 | class DataCache { mutable std::mutex mtx_; mutable int cached_data_; public: int getData() const { std::lock_guardstd::mutex lock(mtx_); // OK cached_data_ = compute(); // OK return cached_data_; } }; | C++98 |
auto | 含义: 作为一个类型占位符,让编译器根据其初始化表达式自动推断出变量的实际类型。 用途: 简化代码,特别是当类型名称很长或复杂时(如STL迭代器、模板、lambda表达式)。 | // 代替 std::vector::iterator auto it = my_vec.begin(); // 自动推断lambda类型 auto add = [](int a, int b) { return a + b; }; | [C++11] (含义改变) |
register | 含义: (已废弃) 建议编译器将变量存储在CPU寄存器中以加速访问。 用途: 现代编译器已足够智能,能自动进行寄存器优化,此关键字已无实际作用。 | register int fast_counter; | C++98 (C++17 中移除) |
函数与类说明符 (Function and Class Specifiers)
这类修饰符用于修改函数或类的行为、属性和编译期检查。
| 修饰符 | 核心含义与用途 | 写法示例 | C++ 版本 / 状态 |
|---|---|---|---|
inline | 含义: 1. (建议) 建议编译器进行内联展开,减少函数调用开销。 2. (重要) 允许多次定义,允许在头文件中定义函数而不产生链接错误。 | // 在头文件中定义,避免链接错误 inline int max(int a, int b) { return a > b ? a : b; } | C++98 |
virtual | 含义: 在基类中声明一个成员函数为虚函数,允许它在派生类中被重写。 用途: 实现C++的核心特性——多态。通过基类指针或引用调用虚函数时,会根据对象的实际类型在运行时确定调用哪个版本。 | class Animal { public: virtual void makeSound(); }; class Dog : public Animal { ... }; | C++98 |
explicit | 含义: 禁止构造函数或类型转换运算符被用于隐式类型转换。 用途: 避免因意料之外的自动类型转换而导致的逻辑错误,使代码意图更清晰。 | class Handle { public: explicit Handle(int fd); }; void useHandle(Handle h); useHandle(10); // 错误! useHandle(Handle(10)); // 正确` | C++98 (C++11 增强) |
override | 含义: 在派生类函数后使用,明确表示该函数旨在重写基类的虚函数。 用途: 编译期安全检查。如果函数签名与任何基类虚函数都不匹配,编译器会报错,防止因拼写错误等导致的重写失败。 | class Dog : public Animal { public: void makeSound() override; // 确保正确重写 }; | [C++11] |
final | 含义: 1. 用于虚函数: 禁止在更深层的派生类中进一步重写。 2. 用于类: 禁止该类被继承。 用途: 控制继承体系,明确设计意图,也可能带来优化机会。 | // final类,不能被继承 class Sealed final {}; // final函数,不能被重写 class Base { virtual void lock() final; }; | [C++11] |
constexpr | 含义: 指定变量或函数的结果可以在编译期计算出来。 用途: 将计算从运行时转移到编译时,提升性能。可用于定义数组大小、模板参数等需要编译期常量的地方。 | constexpr int square(int x) { return x * x; } int arr[square(5)]; // 合法, 数组大小为25 | [C++11] |
noexcept | 含义: 声明一个函数保证不会抛出异常。 用途: 允许编译器进行更深入的优化。标准库中的某些操作(如移动构造函数)会根据此属性选择更高性能的实现。 | // 保证swap操作不会失败 void swap(MyType& a, MyType& b) noexcept; | [C++11] |
consteval | 含义: (立即函数) 强制函数必须在编译时求值,其结果必须是编译期常量。 用途: 比 constexpr更严格,用于确保某些工具函数只在编译期被调用,不能用于运行时。 | consteval int get_magic_number() { return 42; } constexpr int num = get_magic_number(); // OK int runtime_var = get_magic_number(); // 错误! | [C++20] |
constinit | 含义: 断言一个具有静态或线程存储期的变量是被常量初始化的。 用途: 用于解决“静态初始化顺序灾难”,它确保变量的初始化在所有动态初始化之前完成,且没有运行时依赖。 | // 确保g_logger在main函数执行前已完成初始化 constinit Logger g_logger("global"); | [C++20] |
运算符
好的,我们来全面地介绍一下C++中的运算符。
C++的运算符是一些特殊的符号,用于执行数据运算、功能调用和逻辑判断。理解它们是掌握C++编程的基础。
在介绍之前,有两个重要的概念需要了解:
-
优先级 (Precedence):决定了在多个不同运算符组成的表达式中,哪个运算符先被执行。例如,在
3 + 4 * 5中,乘法*的优先级高于加法+,所以先计算4 * 5。 -
结合性 (Associativity):当多个相同优先级的运算符连续出现时,决定其计算顺序。例如,
a = b = c,赋值运算符=是右结合的,所以等价于a = (b = c)。而10 - 5 - 2,减法-是左结合的,所以等价于(10 - 5) - 2。
下面我将C++的运算符分类,并用表格的形式进行详细介绍。
1. 算术运算符 (Arithmetic Operators)
用于执行基本的数学运算。
| 运算符 | 名称 | 示例 | 说明 |
|---|---|---|---|
+ | 加法 | a + b | 计算两个操作数的和。 |
- | 减法 | a - b | 计算两个操作数的差。 |
* | 乘法 | a * b | 计算两个操作数的积。 |
/ | 除法 | a / b | 计算a除以b的商。若操作数均为整数,则结果为整数(小数部分被截断)。 |
% | 取模 (取余) | a % b | 计算a除以b的余数,操作数必须为整数类型。 |
2. 赋值运算符 (Assignment Operators)
用于将一个值赋给一个变量。
| 运算符 | 名称 | 示例 | 等价于 |
|---|---|---|---|
= | 简单赋值 | a = b | 将b的值赋给a。 |
+= | 加后赋值 | a += b | a = a + b |
-= | 减后赋值 | a -= b | a = a - b |
*= | 乘后赋值 | a *= b | a = a * b |
/= | 除后赋值 | a /= b | a = a / b |
%= | 取模后赋值 | a %= b | a = a % b |
&= | 按位与后赋值 | a &= b | a = a & b |
|= | 按位或后赋值 | a |= b | a = a | b |
^= | 按位异或后赋值 | a ^= b | a = a ^ b |
<<= | 左移后赋值 | a <<= b | a = a << b |
>>= | 右移后赋值 | a >>= b | a = a >> b |
3. 比较运算符 (Comparison Operators)
用于比较两个值,结果为布尔值 true 或 false。
| 运算符 | 名称 | 示例 | 说明 |
|---|---|---|---|
== | 等于 | a == b | 如果a等于b,则为 true。 |
!= | 不等于 | a != b | 如果a不等于b,则为 true。 |
> | 大于 | a > b | 如果a大于b,则为 true。 |
< | 小于 | a < b | 如果a小于b,则为 true。 |
>= | 大于等于 | a >= b | 如果a大于或等于b,则为 true。 |
<= | 小于等于 | a <= b | 如果a小于或等于b,则为 true。 |
<=> | 三向比较 (飞船运算符) | a <=> b | [C++20] 一次性比较a和b的大小关系。返回一个对象,该对象可与0比较:若a<b则<0;若a>b则>0;若a==b则==0。 |
4. 逻辑运算符 (Logical Operators)
用于组合多个布尔表达式。
| 运算符 | 名称 | 示例 | 说明 |
|---|---|---|---|
&& | 逻辑与 | expr1 && expr2 | 当且仅当 expr1 和 expr2 都为 true 时,结果为 true。具有短路求值特性。 |
| ` | ` | 逻辑或 | |
! | 逻辑非 | !expr | 如果 expr 为 false,结果为 true;反之亦然。 |
短路求值 (Short-circuit Evaluation):
-
对于
expr1 && expr2,如果expr1为false,则expr2不会被求值。 -
对于
expr1 || expr2,如果expr1为true,则expr2不会被求值。
5. 位运算符 (Bitwise Operators)
直接对整数的二进制位进行操作。
| 运算符 | 名称 | 示例 | 说明 |
|---|---|---|---|
& | 按位与 | a & b | 对a和b的每个二进制位进行与操作 (都为1时结果为1)。 |
| | 按位或 | a | b | 对 a 和 b 的每个二进制位进行或操作 (任意为 1 时结果为 1)。 |
^ | 按位异或 | a ^ b | 对a和b的每个二进制位进行异或操作 (相异时结果为1)。 |
~ | 按位取反 | ~a | 翻转a的所有二进制位 (0变1,1变0)。 |
<< | 左移 | a << n | 将a的所有二进制位向左移动n位,右边补0。 |
>> | 右移 | a >> n | 将a的所有二进制位向右移动n位,左边对于无符号数补0,对于有符号数取决于实现(通常补符号位)。 |
6. 递增和递减运算符 (Increment and Decrement Operators)
| 运算符 | 名称 | 示例 | 说明 |
|---|---|---|---|
++ | 递增 | var++ 或 ++var | 将变量的值加1。 |
-- | 递减 | var-- 或 --var | 将变量的值减1。 |
前置 (Pre-increment/decrement) vs 后置 (Post-increment/decrement):
++var(前置): 先将var加1,然后返回新的值。var++(后置): 先返回var的原始值,然后再将var加1。- 除非需要使用原始值,否则推荐使用前置版本 (
++var),因为它通常效率更高(尤其对于复杂类型)。
7. 成员、指针及其他运算符
| 运算符 | 名称 | 示例 | 说明 |
|---|---|---|---|
. | 成员访问 | obj.member | 通过对象实例访问其成员。 |
-> | 指针成员访问 | ptr->member | 通过指向对象的指针访问其成员,等价于 (*ptr).member。 |
& | 取地址 | &var | 获取变量 var 的内存地址。 |
* | 解引用 | *ptr | 获取指针 ptr 所指向地址处的值。 |
[] | 下标 | arr[i] | 访问数组或容器中索引为 i 的元素。 |
:: | 作用域解析 | Class::member | 访问类或命名空间的静态成员或类型。 |
? : | 条件 (三元) | cond ? expr1 : expr2 | 如果 cond 为 true,表达式的值为 expr1;否则为 expr2。 |
, | 逗号 | expr1, expr2 | 先计算 expr1,然后计算 expr2,整个表达式的值是 expr2的值。 |
sizeof | 大小 | sizeof(type) | 计算一个类型或对象在内存中所占的字节数。 |
new / delete | 内存管理 | new int; delete ptr; | 在堆上动态分配和释放单个对象的内存。 |
new[] / delete[] | 内存管理 | new int[10]; delete[] arr; | 在堆上动态分配和释放数组的内存。 |
typeid | 类型识别 | typeid(var) | 返回一个描述变量或类型信息的对象。 |
8. 类型转换运算符 (Type Casting Operators)
C++提供了更安全的显式类型转换方式来替代C风格的 (type)expr。
| 运算符 | 名称 | 示例 | 说明 |
|---|---|---|---|
static_cast | 静态转换 | static_cast<type>(expr) | 用于“良性”和“常规”的转换,如整数和浮点数之间、指针的上下行转换(不进行运行时检查)。 |
dynamic_cast | 动态转换 | dynamic_cast<type>(expr) | 主要用于多态类型,在继承体系中进行安全的下行转换。会进行运行时检查,转换失败时返回空指针(对指针)或抛出异常(对引用)。 |
const_cast | 常量转换 | const_cast<type>(expr) | 用于添加或移除变量的 const 或 volatile 属性。这是唯一能移除const的转换。 |
reinterpret_cast | 重解释转换 | reinterpret_cast<type>(expr) | 用于底层的、风险较高的转换,如将指针转换为整数,或在不相关的指针类型间转换。 |
循环
1. for 循环:循环次数已知
语法结构
for (初始化表达式; 条件表达式; 迭代表达式) {
// 循环体代码
}
- 初始化表达式 (Initialization): 在循环开始前执行一次,通常用于声明和初始化循环控制变量(如
int i = 0;)。 - 条件表达式 (Condition): 在每次循环体执行之前进行判断。如果为
true,则执行循环体;如果为false,则退出循环。 - 迭代表达式 (Update/Increment): 在每次循环体执行之后执行,通常用于更新循环控制变量(如
i++)。
用途
当你需要精确控制循环的次数,例如遍历一个数组的索引、执行固定次数的操作时,for 循环是最佳选择。
示例
#include <iostream>
#include <vector>
int main() {
// 示例1: 打印 0 到 4
std::cout << "示例1: 基本for循环" << std::endl;
for (int i = 0; i < 5; i++) {
std::cout << "i = " << i << std::endl;
}
// 示例2: 使用for循环遍历数组
std::cout << "\n示例2: 遍历数组" << std::endl;
int numbers[] = {10, 20, 30, 40, 50};
// sizeof(numbers) / sizeof(numbers[0]) 是计算数组元素个数的常用方法
for (int i = 0; i < sizeof(numbers) / sizeof(numbers[0]); ++i) {
std::cout << "索引 " << i << " 的值为: " << numbers[i] << std::endl;
}
return 0;
}
2. 基于范围的 for 循环 (Range-Based for Loop) (C++11)
这是C++11引入的一种更现代化、更简洁的 for 循环。它专门用于 遍历一个容器或序列(如数组、std::vector、std::string等)中的每一个元素。
语法结构
for (元素声明 : 容器或序列) {
// 循环体代码
}
- 容器或序列 (Range): 你想要遍历的对象,例如一个数组或STL容器。
- 元素声明 (Element Declaration): 用于在每次迭代中存储当前元素的变量。
用途
当你需要处理一个容器中的所有元素,而不关心索引时,这是最推荐的方式。它更安全(不会越界)也更易读。
示例
#include <iostream>
#include <vector>
#include <string>
int main() {
std::vector<int> scores = {100, 95, 88, 76};
std::cout << "遍历vector中的分数:" << std::endl;
// 使用 const auto& 来只读访问,高效且安全
for (const auto& score : scores) {
std::cout << score << " ";
}
std::cout << std::endl;
std::string message = "Hello";
std::cout << "\n遍历string中的字符:" << std::endl;
// 使用 auto& 来修改元素
for (auto& ch : message) {
ch = std::toupper(ch); // 将每个字符转为大写
}
std::cout << message << std::endl; // 输出 "HELLO"
return 0;
}
3. while 循环
while 循环适用于 循环次数未知,但知道循环的持续条件 的情况。它会在循环开始前检查条件。
语法结构
while (条件表达式) {
// 循环体代码
// (通常需要在这里更新与条件相关的变量,否则可能造成死循环)
}
- 条件表达式 (Condition): 在每次迭代开始前检查。只要条件为
true,循环就继续。如果一开始就为false,循环体一次也不会执行。
用途
当你需要等待某个事件发生(如用户输入特定值)、处理不确定长度的数据流(如读取文件)时,while 循环非常有用。
示例
#include <iostream>
int main() {
int countdown = 5;
std::cout << "倒计时开始:" << std::endl;
while (countdown > 0) {
std::cout << countdown << "..." << std::endl;
countdown--; // 更新循环变量
}
std::cout << "发射!" << std::endl;
// 示例: 等待用户输入-1来结束程序
int input = 0;
while (input != -1) {
std::cout << "请输入一个数字 (-1 退出): ";
std::cin >> input;
std::cout << "你输入了: " << input << std::endl;
}
std::cout << "程序结束。" << std::endl;
return 0;
}
4. do-while 循环
do-while 循环与 while 循环非常相似,唯一的关键区别是它 先执行一次循环体,然后再检查条件。因此,do-while 循环的循环体至少会执行一次。
语法结构
do {
// 循环体代码
} while (条件表达式); // 注意这里有一个分号
用途
当需要先执行操作再判断是否重复时使用。最典型的例子是菜单驱动的程序,至少要显示一次菜单。
示例
#include <iostream>
int main() {
char choice;
do {
std::cout << "\n--- 菜单 ---" << std::endl;
std::cout << "1. 开始游戏" << std::endl;
std::cout << "2. 加载游戏" << std::endl;
std::cout << "Q. 退出" << std::endl;
std::cout << "请输入你的选择: ";
std::cin >> choice;
// ... 处理选择的代码 ...
} while (choice != 'Q' && choice != 'q');
std::cout << "感谢使用!" << std::endl;
return 0;
}
5. 循环控制语句
C++提供了两个语句来在循环内部进行更精细的控制:
break: 立即终止并跳出整个循环(仅跳出最内层循环)。continue: 跳过当前这次迭代中余下的代码,直接开始下一次迭代。
示例
#include <iostream>
int main() {
std::cout << "break 示例 (找到5就停止):" << std::endl;
for (int i = 0; i < 10; ++i) {
if (i == 5) {
std::cout << "找到了5!" << std::endl;
break; // 退出 for 循环
}
std::cout << "当前 i = " << i << std::endl;
}
std::cout << "\ncontinue 示例 (跳过偶数):" << std::endl;
for (int i = 0; i < 10; ++i) {
if (i % 2 == 0) {
continue; // 跳过本次迭代的剩余部分
}
std::cout << "奇数: " << i << std::endl;
}
return 0;
}
判断
1. if
a) 单一 if 结构
如果条件为真,则执行代码块;否则,直接跳过。 语法:
if (条件表达式) {
// 如果条件为真,执行这里的代码
}
- 条件表达式: 任何可以求值为布尔
true或false的表达式。在C++中,非零值被视为true,零值(包括nullptr)被视为false。
示例:
#include <iostream>
int main() {
int score = 85;
if (score > 60) {
std::cout << "恭喜,你及格了!" << std::endl;
}
return 0;
}
b) if-else 结构
提供两条执行路径:如果条件为真,执行 if 块;如果条件为假,执行 else 块。
语法:
if (条件表达式) {
// 如果条件为真,执行这里的代码
} else {
// 如果条件为假,执行这里的代码
}
示例:
#include <iostream>
int main() {
int number = 7;
if (number % 2 == 0) {
std::cout << number << " 是一个偶数。" << std::endl;
} else {
std::cout << number << " 是一个奇数。" << std::endl;
}
return 0;
}
c) if-else if-else 结构
用于处理多个互斥的条件。程序会从上到下依次检查每个条件,一旦找到为真的条件,就执行其对应的代码块,然后跳过整个链条的其余部分。
语法:
if (条件1) {
// 如果条件1为真,执行这里的代码
} else if (条件2) {
// 如果条件1为假,但条件2为真,执行这里的代码
} else if (条件3) {
// ...
} else {
// 如果以上所有条件都为假,执行这里的代码 (可选)
}
示例:根据分数评定等级。
#include <iostream>
int main() {
int score = 88;
if (score >= 90) {
std::cout << "等级:A" << std::endl;
} else if (score >= 80) {
std::cout << "等级:B" << std::endl;
} else if (score >= 70) {
std::cout << "等级:C" << std::endl;
} else if (score >= 60) {
std::cout << "等级:D" << std::endl;
} else {
std::cout << "等级:E (不及格)" << std::endl;
}
return 0;
}
2. switch 语句
switch 语句是 if-else if 链的一种替代形式,特别适用于 根据一个整型(或可转换为整型的类型,如 char, enum)变量的不同常量值来执行不同操作 的场景。
语法:
switch (表达式) {
case 常量值1:
// 代码块1
break; // 非常重要!
case 常量值2:
// 代码块2
break;
// ... 更多 case
default:
// 如果表达式的值与所有case都不匹配,执行这里的代码 (可选)
break;
}
关键点:
break语句:switch语句有一个特殊的“穿透”(fall-through)行为。如果没有break,程序在执行完一个case的代码后会继续执行下一个case的代码,直到遇到break或switch结束。这通常不是我们想要的行为,所以几乎每个case的末尾都需要break.default子句: 如果表达式的值不匹配任何case,则会执行default子句。它类似于if-else if链最后的else。
示例:
#include <iostream>
int main() {
int day = 4;
switch (day) {
case 1:
std::cout << "星期一" << std::endl;
break;
case 2:
std::cout << "星期二" << std::endl;
break;
case 3:
std::cout << "星期三" << std::endl;
break;
case 4:
std::cout << "星期四" << std::endl;
break;
case 5:
std::cout << "星期五" << std::endl;
break;
case 6:
case 7: // "穿透"行为的有意使用
std::cout << "周末!" << std::endl;
break;
default: //必须要有default
std::cout << "无效的日期" << std::endl;
break;
}
return 0;
}
3. 条件(三元)运算符 ? :
这是一个紧凑的 if-else 表达式,而不是语句。它根据一个条件返回两个值中的一个。
语法:
条件 ? 为真时的值 : 为假时的值;
示例:
找出两个数中的较大者。
#include <iostream>
int main() {
int a = 10;
int b = 20;
int max_val = (a > b) ? a : b; // 如果 a > b 为真,表达式的值为 a,否则为 b
std::cout << "最大值是: " << max_val << std::endl;
return 0;
}
4. 带初始化的 if 和 switch (C++17)
C++17 引入了一个新特性,允许在 if 和 switch 语句本身中声明和初始化一个变量。这个变量的作用域被限制在该判断语句的内部(包括所有 if, else if, else 块),有助于写出更整洁、更安全的代码。
语法:
if (初始化语句; 条件表达式) {
// ...
}
switch (初始化语句; 表达式) {
// ...
}
示例:
#include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> user_scores = {{"Alice", 100}, {"Bob", 90}};
// C++17 写法: it只在if-else语句中有效
if (auto it = user_scores.find("Alice"); it != user_scores.end()) {
std::cout << "找到了 Alice, 分数: " << it->second << std::endl;
} else {
std::cout << "未找到 Alice。" << std::endl;
}
// 在这里访问 it 会导致编译错误,因为 it 已经超出了作用域
// it->second; // ERROR!
return 0;
}
函数
好的,我们来系统地梳理一下C++中关于函数语法的所有核心内容,从基础到现代C++的特性。
1. 函数的基本概念与作用
不想写。由于我已经完成了 C++的学习(大一上)
2. 函数的声明与定义
在C++中,函数需要先声明后使用,并且必须有且仅有一个定义。
a) 函数声明 (Declaration)
函数声明(也叫函数原型)告诉编译器一个函数的名字、返回类型和参数列表,但不包含函数体。声明的目的是让编译器知道这个函数存在,可以被调用。
语法:
返回类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, ...);
- 参数名在声明中是可选的,但写上通常能增加可读性。
- 声明以分号
;结尾。
示例:
int add(int a, int b); // 声明一个名为 add 的函数
void printMessage(std::string message); // 声明一个无返回值的函数
b) 函数定义 (Definition)
函数定义提供了函数的具体实现,即函数体内的代码。
语法:
返回类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, ...) {
// 函数体: 包含具体的执行语句
// ...
return 返回值; // 如果返回类型不是 void
}
示例:
#include <iostream>
#include <string>
// add 函数的定义
int add(int a, int b) {
return a + b;
}
// printMessage 函数的定义
void printMessage(std::string message) {
std::cout << message << std::endl;
}
3. 函数的参数传递机制
函数如何接收外部数据是通过参数传递机制来控制的。
a) 按值传递 (Pass by Value)
这是默认的传递方式。函数接收的是实参的一个副本。在函数内部对参数的任何修改都不会影响原始的实参。
语法:
void modify(int x) {
x = 100; // 只修改了副本 x
}
特点:安全,不会意外修改外部数据。但对于大型对象(如结构体、类),拷贝开销较大。
b) 按引用传递 (Pass by Reference)
通过在参数类型后加 &,函数接收的是实参的引用(别名)。在函数内部对参数的修改将直接影响原始的实参。
语法:
void modify(int& x) {
x = 100; // 直接修改了原始变量
}
特点:高效,没有拷贝开销。可用于需要修改外部变量的场景。
c) 按常量引用传递 (Pass by Constant Reference)
结合了按值传递的安全性和按引用传递的高效性。函数接收引用,但承诺不会修改它。这是向函数传递大型只读对象的最佳实践。
语法:
void print(const std::string& message) {
// message = "new"; // 编译错误!不能修改 const 引用
std::cout << message << std::endl;
}
特点:高效且安全。
d) 按指针传递 (Pass by Pointer)
函数接收的是实参的内存地址。通过解引用指针,可以读取和修改原始数据。
void modify(int* p) {
if (p != nullptr) {
*p = 100; // 修改指针指向地址的值
}
}
特点:与引用类似,可以修改外部数据。此外,指针可以为 nullptr,传递“空”状态。
4. 函数的返回值
函数可以通过 return 语句向调用者返回一个值。
a) 返回类型
-
void: 表示函数不返回任何值。 -
具体类型 (如
int,double,std::string): 函数必须通过return语句返回一个该类型的值。 -
auto(C++14): 编译器可以根据return语句自动推断返回类型。
auto add(int a, int b) { // 编译器推断返回类型为 int
return a + b;
}
b) 返回方式
-
返回一个值:
return x;- 最常见的方式。函数返回一个值的副本。编译器通常会使用返回值优化 (RVO)来避免不必要的拷贝。
-
返回一个引用:
return x;(当函数返回类型为T&)- 警告:极其危险! 绝对不能返回对局部变量的引用,因为函数结束后局部变量被销毁,引用会变成“悬垂引用”。
- 通常只用于返回类成员或由调用者传入的对象的引用。
int& bad_function() {
int local_var = 10;
return local_var; // 灾难!返回了对已销毁变量的引用
}
5. 高级与现代C++函数语法
a) 默认参数 (Default Arguments)
可以为函数的参数指定默认值。带有默认值的参数必须放在参数列表的末尾。
void showMessage(const std::string& msg, const std::string& prefix = "Info") {
std::cout << prefix << ": " << msg << std::endl;
}
showMessage("File not found"); // 使用默认前缀 "Info"
showMessage("Critical error", "Error"); // 提供自定义前缀 "Error"
b) 函数重载 (Function Overloading)
允许在同一作用域内定义多个同名函数,只要它们的参数列表不同(参数个数或类型不同)。
int add(int a, int b);
double add(double a, double b);
void print(int x);
void print(const std::string& s);
c) 函数模板 (Function Templates)
用于创建“泛型”函数,可以处理任何数据类型,避免为每种类型都写一个重载版本。
template <typename T>
T max_val(T a, T b) {
return (a > b) ? a : b;
}
int m1 = max_val(10, 20); // T 被推断为 int
double m2 = max_val(3.14, 2.71); // T 被推断为 double
d) inline 内联函数(这里前面讲修饰符也有提及)
向编译器建议将函数体直接嵌入到调用处,以减少函数调用的开销。对于非常短小的函数可以提升性能。更重要的作用是允许在头文件中定义函数而不违反“单一定义规则”。
inline int square(int x) { return x * x; }
e) constexpr 函数 (C++11)(同上)
表示函数可以在编译期求值(如果传入的参数是编译期常量)。
constexpr long long factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
// 可用于需要编译期常量的地方
int arr[factorial(5)]; // 数组大小在编译时计算为 120
f) 尾返回类型 (Trailing Return Type) (C++11)
一种替代的函数声明语法,将返回类型放在函数名和参数列表之后。在泛型编程和复杂类型推断中非常有用。
template <typename T, typename U>
auto multiply(T a, U b) -> decltype(a * b) {
return a * b;
}
g) Lambda 表达式 (C++11)
用于创建匿名的、临时的函数对象。常用于STL算法。
基本语法:[捕获列表](参数列表) -> 返回类型 { 函数体 };
[ ]捕获列表:决定能不能访问外部变量,以及怎么访问(值/引用)。( )参数列表:和普通函数参数一样。->返回类型:通常可省略,除非类型难推断。{ }函数体:写逻辑。
#include <vector>
#include <algorithm>
std::vector<int> nums = {1, 2, 3, 4, 5};
// 查找第一个偶数
auto it = std::find_if(nums.begin(), nums.end(),
[](int n) -> bool { // 这是一个 Lambda 表达式
return n % 2 == 0;
}
);
h) noexcept 说明符 (C++11)(同上那两个)
向编译器保证该函数不会抛出异常,有助于编译器进行优化。
- 优化:编译器在生成代码时,可以假设调用这个函数不会抛异常,从而省略一些异常处理的开销。
- 接口保证:对使用者来说,看函数声明就知道它不会抛异常,写库的时候尤其重要。
- 容器移动优化:标准容器(如
std::vector)在扩容时会尝试移动元素,如果移动构造函数是noexcept的,就会用move,否则会退回到copy,性能可能下降。
void swap(int& a, int& b) noexcept {
int temp = a;
a = b;
b = temp;
}
6. 函数指针
C++允许创建指向函数的指针,可以像变量一样存储、传递和调用函数。
语法: 返回类型 (*指针名)(参数类型列表); 示例:
#include <iostream>
void sayHello() {
std::cout << "Hello!" << std::endl;
}
int main() {
// 声明一个函数指针 fp,指向一个无参数、返回void的函数
void (*fp)();
// 将 sayHello 函数的地址赋给指针
fp = &sayHello;
// 或者 fp = sayHello;
// 通过指针调用函数
(*fp)(); // C 风格调用
fp(); // C++ 风格调用 (更常用)
return 0;
}
现代替代方案:在现代C++中,std::function (在 <functional> 头文件中) 是一个更安全、更灵活的函数指针替代品。(后续 STL 会慢慢提及)
后续:下一篇是函数和变量的延续,将介绍数组,STL 中 String,对于 char 组形成的字符串不做介绍(因为我也没学 C,一开始就用的 string,写不惯),以及指针和应用等