C++学习笔记之函数与变量

200 阅读34分钟

⚠️考虑到目前很多编译器以及编译环境的版本不高,或者对于 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布尔类型,表示真或假1truefalse
char字符类型,通常用于存储 ASCII 字符1-128 到 127 或 0 到 255
signed char有符号字符类型1-128 到 127
unsigned char无符号字符类型10 到 255
wchar_t宽字符类型,用于存储 Unicode 字符2 或 4取决于平台
char 16_t16 位 Unicode 字符类型(C++11 引入)20 到 65,535
char 32_t32 位 Unicode 字符类型(C++11 引入)40 到 4,294,967,295
short短整型2-32,768 到 32,767
unsigned short无符号短整型20 到 65,535
int整型4-2,147,483,648 到 2,147,483,647
unsigned int无符号整型40 到 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 引入)80 到 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容器初始化

为什么列表初始化是最佳选择?

  1. 通用性:它几乎可以用于所有类型的初始化,包括普通变量、数组、STL容器、自定义类等,语法非常统一。
  2. 安全性(防止类型收窄):它不允许“收窄转换”(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};
  1. 解决“最令人头疼的解析” (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 += ba = a + b
-=减后赋值a -= ba = a - b
*=乘后赋值a *= ba = a * b
/=除后赋值a /= ba = a / b
%=取模后赋值a %= ba = a % b
&=按位与后赋值a &= ba = a & b
|=按位或后赋值a |= ba = a | b
^=按位异或后赋值a ^= ba = a ^ b
<<=左移后赋值a <<= ba = a << b
>>=右移后赋值a >>= ba = 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::vectorstd::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 链的一种替代形式,特别适用于 根据一个整型(或可转换为整型的类型,如 charenum)变量的不同常量值来执行不同操作 的场景。

语法:

switch (表达式) {
    case 常量值1:
        // 代码块1
        break; // 非常重要!
    case 常量值2:
        // 代码块2
        break;
    // ... 更多 case
    default:
        // 如果表达式的值与所有case都不匹配,执行这里的代码 (可选)
        break;
}

关键点:

  1. break 语句switch 语句有一个特殊的“穿透”(fall-through)行为。如果没有 break,程序在执行完一个 case 的代码后会继续执行下一个 case 的代码,直到遇到 break 或 switch 结束。这通常不是我们想要的行为,所以几乎每个 case 的末尾都需要 break.
  2. 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 语句本身中声明和初始化一个变量。这个变量的作用域被限制在该判断语句的内部(包括所有 ifelse ifelse 块),有助于写出更整洁、更安全的代码。

语法:

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: 表示函数不返回任何值。

  • 具体类型 (如 intdoublestd::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,写不惯),以及指针和应用等