跳转至

[C++ Basic] Grammar

概要

C++ 基础知识和语法,包括C++11,C++17,C++23的各种语言支持。

C++编程语言历史 和 设计思路

在纷繁多变的世界里茁壮成长:C++ 2006–2020

C 与 C++、java 的 区别

支持范式模板编程 (generic programming)

  1. 模板代码(增加泛型编程能力,类似python),
  2. 泛型编程是一种以通用性为中心的编程范式。在泛型编程中,程序通过使用参数化类型(或称为模板)来实现数据类型无关的算法和数据结构
  3. 强⼤的 Standard Template Library (STL) 标准库, 也是基于泛型编程的产物。
  4. 包括:容器、迭代器、算法和函数对象 四类
  5. 元编程(e.g., constexpr )编译时推导出变量是否为固定常数。
  6. 一些语法和关键字,增加了 new 和 delete,auto

支持面向对象编程 (object-oriented programming) 的拓展

  • 类和对象:C++允许定义类和创建对象。类是一种用户自定义的数据类型,可以包含成员变量和成员函数。对象是类的一个实例,可以通过类来创建多个对象。C语言中没有类和对象的概念,只能使用结构体和函数来组织数据和行为。
  • 封装:C++支持封装,可以将数据和相关的操作封装在一个类中,并使用访问修饰符来控制对类成员的访问权限。C语言没有封装的概念,所有的数据和函数都是公开的。
  • 继承:C++支持继承,允许创建派生类从基类继承属性和行为。继承可以实现代码重用和类的层次化。C语言没有继承的概念。
  • 多态:C++支持多态,允许通过基类指针或引用来调用派生类的虚函数,实现动态绑定和运行时多态性。C语言没有多态的概念。
  • 异常处理:C++提供异常处理机制,可以通过抛出和捕获异常来处理程序中的错误和异常情况。C语言没有内置的异常处理机制。
C++ 与 java 的区别
  1. 内存管理:C++中的内存管理是手动的,程序员需要显式地分配和释放内存。C++提供了new和delete关键字来进行动态内存分配和释放。Java中的内存管理是自动的,使用垃圾回收机制来自动管理内存,程序员不需要手动释放内存。
  2. 指针:C++支持指针操作,允许直接访问和修改内存地址。Java中没有指针的概念,所有的数据访问都是通过引用进行的。
  3. 运行环境:C++是一种编译型语言,源代码在编译后被转换为机器码,并直接在操作系统上运行。Java是一种解释型语言,源代码在编译后生成字节码,然后由Java虚拟机(JVM)解释执行。
  4. 平台依赖性:C++代码在不同的平台上需要重新编译,因为它直接与底层系统交互。Java代码是平台无关的,一次编译的字节码可以在任何支持Java虚拟机的平台上运行。

  5. C++更适合系统级编程、游戏开发等需要更高的性能和底层控制的场景。

  6. Java更适合企业级应用开发、网络编程等需要跨平台和可移植性的场景。

基础知识与坑

程序执行入口

The default program entry function is main, but can be changed in two situations:

  1. use stupid #define xxx main in header file to replace the name which maybe ignored by silly search bar in VSCODE.
  2. use -exxx compile flag

语句末尾的分号

  • 在 C++ 中,是否需要在语句的末尾使用分号(;)取决于上下文。
  • C++ 语法的基本规则是:分号用来标识一条语句的结束,而有些结构并不是严格的语句,因此不需要分号。

  • 声明(变量、类、结构体、枚举、函数原型、类型别名):都需要分号作为结束。

  • 函数定义控制语句(如 if, while, for)、复合语句{}) 不需要分号。
  • 预处理指令(如 #define, #include):不需要分号,因为它们不是 C++ 语法层面的内容。
  • 作用域结束的 } 不需要分号,但声明类或结构体时 } 后要加分号。

为什么类/结构体需要分号

C++ 中的类和结构体定义实际上是一种声明,它们的定义是一种复杂的声明语句,因此必须用分号来结束它们。

总结来说,分号用来结束语句,包括声明、表达式和执行体等,但当你定义一个复合结构(如函数定义、控制语句)时,不需要分号来结束复合结构的定义。

逗号运算符(Comma)

重名变量的优先级

int getKthAncestor(int node, int k) {
    int node= getKthAncestor(saved[node],--k);  
    return node;
}
 //为什么第二行的node会提前被修改为0,导致传入函数getKthAncestor的saved[node]的node值为0
 //如下去掉int,也不会错。因为int node 会初始化node为0
 int getKthAncestor(int node, int k) {
    node= getKthAncestor(saved[node],--k);  
    return node;
}

根据C++的作用域规则,内层的局部变量会覆盖外层的同名变量。因此,在第二行的语句中,node引用的是函数参数中的node,而不是你想要的之前定义的node。

为了避免这个问题,你可以修改代码,避免重复定义变量名。例如,可以将第二行的变量名改为newNode或其他不同的名称,以避免与函数参数名冲突。

运算符优先级

运算符性质:

  • 接受的操作数,
  • 优先级,
    • 特殊:逻辑和(&&)先于逻辑或(||)、四则运算先于位运算
    • 位运算优先级低于判断符号,记得写括号。
    • 赋值(=)优先级最低
  • 结合性,
    • 左结合性: 大部分运算(加减乘除)
    • 右结合性:赋值运算符。程序会先计算它右边的表达式的值,然后再计算它左边的表达式的值
  • 返回值
    • 赋值运算符的返回值是赋值后左操作数的引用

变量类型以及Macro constants

https://en.cppreference.com/w/cpp/language/types

https://en.cppreference.com/w/cpp/types/integer

//返回与平台相关的数值类型的极值
std::numeric_limits<double>::max()
std::numeric_limits<int>::min()

#include<limits.h>
INT_MAX
FLT_MAX (or DBL_MAX ) 
-FLT_MAX (or -DBL_MAX ) 

关键词

extern 
const
constexpr //C++11引入的关键字,用于编译时的常量与常量函数。
volatile    //是指每次需要引用某个变量的数据时,都必须从内存原地址读取,而不是编译器优化后寄存器间接读取.(必须写回内存,为了多进程并发而设计的。)
inline 

static 关键字

static 作⽤:控制变量的存储⽅式和作用范围(可⻅性)。

  1. 修饰局部变量
    • 存放位置:栈区 -> 静态数据区(data段或者bss段)
    • 生命周期:程序结束才会释放
    • 作用域:还是局部代码块
  2. 修饰函数与全局变量
    • 使其作用范围由全工程文件可见变成了本文件可见
  3. 修饰类内函数
    • 静态成员函数:使用"static"修饰的成员函数称为静态成员函数。静态成员函数与类的对象无关,可以在没有创建对象的情况下直接通过类名调用。这意味着它们不需要通过类的对象来访问,而是属于整个类的。举例
    • 静态成员函数没有隐式的this指针,因此不能直接访问非静态成员变量和非静态成员函数。静态成员函数可以访问类的静态成员变量和其他静态成员函数。
    • static 成员函数不能被 virtual 修饰, static 成员不属于任何对象或实例,所以加上 virtual没有任何实际意义;
      • 静态成员函数没有 this 指针,虚函数的实现是为每⼀个对象分配⼀个vptr 指针,⽽ vptr 是通过 this 指针调⽤的,所以不能为 virtual;虚函数的调⽤关系,this->vptr->ctable->virtual function。
  4. 修饰类内的变量
    • 存放位置:栈区 -> 静态数据区(data段或者bss段)
    • 生命周期:程序结束才会释放
    • 意味着下一次调用函数时,静态局部变量将保持上一次调用时的值。
    • 由于不再属于某个类对象,可以直接通过类名初始化 int MyClass::staticVariable = 10;
静态成员函数
#include <iostream>

class MyClass {
public:
    static void staticFunction() {
        std::cout << "This is a static member function." << std::endl;
    }
};

int main() {
    MyClass::staticFunction(); // 直接通过类名调用静态成员函数
    return 0;
}

const 关键字

当const修饰基本数据类型时,可以将其放置在类型说明符的前面或后面,效果是一样的。const关键字用于声明一个常量,即其值在声明后不可修改。

const int constantValue1 = 10; // const在类型说明符前
int const constantValue2 = 20; // const在类型说明符后

当const关键字位于指针变量或引用变量的左侧时,它用于修饰指针所指向的变量,即指针指向的内容为常量。当const关键字位于指针变量或引用变量的右侧时,它用于修饰指针或引用本身,即指针或引用本身是常量。

  1. 修饰指针指向的变量, 它指向的值不能修改:

    int x = 5;
    const int* ptr = &x;  // 指向常量整数的指针
    // *ptr = 10;        // 错误:不能通过const指针修改值
    x = 10;               // 合法:可以修改变量本身的值
    
  2. 修饰指针本身 ,它不能再指向别的变量,但指向(变量)的值可以修改。:

    const int y = 10;
    int* const ptr = &y;  // 常量指针指向整数
    // ptr = &x;         // 错误:不能修改指针本身
    // *ptr = 5;         // 合法:可以修改常量变量的值
    
  3. const int *const p3; //指向整形常量 的 常量指针 。它既不能再指向别的常量,指向的值也不能修改。

explicit

在C++, explicit 是一个关键字,用于修饰单参数构造函数,用于禁止隐式类型转换。

当一个构造函数被声明为 explicit 时,它指示编译器在使用该构造函数进行类型转换时只能使用显式调用,而不允许隐式的类型转换发生。

通过使用 explicit 关键字,可以防止一些意外的类型转换,提高代码的清晰性和安全性。它通常用于防止不必要的类型转换,特别是在单参数构造函数可能引起歧义或产生意外结果的情况下。

preprocessor directive

  • #include_next作用是 在寻找头文件时的头文件搜索优先级里,去除该文件所在的当前目录,主要是为C++头文件的重名问题提供一种解决方案。
    • 正确的用法:代码b.cpp想使用 自己拓展修改的stdlib.h, 那么在代码的目录下创建stdlib.h,并在该文件里#include_next "stdlib.h" 防止递归引用。

define、 const、 typedef、 inline

  • define:
    • define是一个预处理器指令,用于创建宏定义。它在编译之前对源代码进行简单的文本替换。可以用来定义常量、函数宏和条件编译等。
    • define的替换是简单的文本替换,没有类型检查和作用域限制。因此,它可能存在一些潜在的问题,如意外的副作用或命名冲突。
    • 例如:#define PI 3.14159,在代码中将PI替换为3.14159。
  • const:
    • const用于声明一个常量,指示标识符的值在程序执行期间不能被修改。
    • const可以用于变量、函数参数、函数返回类型和成员函数。使用const可以提高代码的可读性和安全性。
    • 例如:const int MAX_VALUE = 100;,声明一个名为MAX_VALUE的常量。
  • typedef:
    • typedef用于为数据类型创建别名。它可以用于为复杂的数据类型提供更简洁的名称,增强代码的可读性和可维护性。
    • typedef创建的别名可以像原始类型一样使用,并且不会引入新的类型,只是为已有类型提供了一个新的名称。
    • 例如:typedef int Age;,为int类型创建了一个别名Age。
  • inline:
  • inline用于声明内联函数,它是一种编译器的建议,用于将函数的定义直接插入到调用处,以避免函数调用的开销。
  • 内联函数通常在函数体较小且频繁调用的情况下使用,可以提高程序的执行效率。
  • inline关键字只是给编译器一个提示,编译器可以选择忽略该提示。在大多数情况下,编译器会自动进行内联优化。
  • 例如:inline int add(int a, int b) { return a + b; },声明了一个内联函数add。

  • define主要用于宏定义,const用于声明常量,typedef用于创建类型别名,inline用于内联函数的声明。

#ifndef & #pragma once

为了避免同一个文件被include多次,C/C++中有两种方式,一种是#ifndef方式,一种是#pragma once方式。在能够支持这两种方式的编译器上,二者并没有太大的区别,但是两者仍然还是有一些细微的区别

new & delete

  • new和delete 相对于 malloc/free 分配和释放堆空间。
  • 额外会执行构造函数和析构函数
#include <iostream>

class MyClass {
public:
    MyClass() {
        std::cout << "Constructing MyClass" << std::endl;
    }

    ~MyClass() {
        std::cout << "Destructing MyClass" << std::endl;
    }
};

int main() {
    // 使用new动态分配内存,并调用构造函数
    MyClass* obj = new MyClass();

    // 执行一些操作...

    // 使用delete释放内存,并调用析构函数
    delete obj;

    return 0;
}

namespace

namespace 会影响 typedef 的作用范围,但不会直接限制 #define 宏的作用范围。

头文件

  • 相互引用,前置声明
  • include头文件其实就是将对应的头文件内容贴在include的位置

A.h, B.h 都需要string.h的头文件,然后B.h 会include A.h,那么我在B.h里是不是可以省略include string.h

不应该省略,

  1. 防止代码变更引发的问题: 如果某天 A.h 中移除了 #include ,而 B.h 依赖 A.h 提供的 #include ,那么 B.h 将会因找不到 std::string 而编译失败。因此,显式包含依赖的头文件可以避免这种隐含依赖引发的问题。
  2. 提高可读性和自包含性: 每个头文件应该尽量做到自包含,意思是每个头文件应该独立地包含所有它所需要的头文件。这样做的好处是,任何其他文件都可以安全地单独包含 B.h,而无需额外关心它依赖于哪些头文件。
  3. 减少隐式依赖: 隐式依赖(依赖另一个头文件帮你包含所需的头文件)可能导致维护性问题。显式 #include 可以让代码更具可预测性和可维护性。

include的位置有什么规则和规律吗,头文件和cpp文件前都可以吗?

在编写代码时,往往A.cpp需要include A.h。那A.cpp需要的头文件,我是写在A.cpp里还是A.h里?

  1. 头文件 (A.h):只包含声明所需的头文件,不包含仅在实现中需要的头文件。
  2. 源文件 (A.cpp):包含所有实现需要的头文件,特别是那些仅在实现部分用到的头文件。此外,A.cpp 应该总是包含 A.h。6.

函数的特殊写法

函数传参

  1. 值传递
  2. 引用传递
  3. 指针传递
//值传递
change1(n);
void change1(int n){
    n++;
}

//引用传递,操作地址就是实参地址 ,只是相当于实参的一个别名,在符号表里对应是同一个地址。对它的操作就是对实参的操作
    change2(n);
    void change2(int &n){
        n++;
    }
    //特殊对vector
    void change2(vector<int> &n)
    //特殊对数组
    void change2(int (&n)[1000])

//指针传递,其实是地址的值传递
change3(&n);
void change3(int *n){
    *n=*n+1;
}

引用传递和指针传递的区别:

  • 引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
  • 不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)。
  • 一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。

指针传递和引用传递的使用情景:

  1. 函数内部修改参数并且希望改动影响调用者。
  2. 当一个函数实际需要返回多个值,而只能显式返回一个值时,可以将另外需要返回的变量以指针/引用传递

闭包、匿名函数

闭包是捕获并持有了外部作用域变量的函数。

闭包(Closure)是指在程序中,函数可以捕捉并记住其作用域(环境)中的变量,即使在函数执行完成后,这些变量依然保存在内存中,并能在后续的函数调用中被使用。闭包的一个重要特性是,它不仅保存了函数本身的逻辑,还“闭合”了函数执行时的上下文环境(即该函数所在的作用域)。

闭包通常用于实现函数内部的状态保持、回调函数等场景。在 C++ 中,闭包通过 lambda 表达式 实现,lambda 表达式可以捕获外部变量并在其内部使用。

例子

auto add = [](int x) {
    return [x](int y) {
        return x + y;
    };
};

auto add5 = add(5);
std::cout << add5(3); // 输出 8
在上面的例子中,add 函数返回了一个闭包,捕获了变量 x 的值。即使 x 在原作用域中不再可用,返回的闭包仍然可以访问并使用 x 的值。

  • 匿名函数是一种没有被绑定标识符的函数
  • lambda 是一种匿名函数
  • lambda 可以表示闭包

匿名函数(lambda)和闭包的关系就如同类和类对象的关系

匿名函数和类的定义都只存在于源码(代码段)中,而闭包和类对象则是在运行时占用内存空间的实体;

类型变参模板

template<typename T>
void swap(T& t1, T& t2)
{
    T temp = t2;
    t2 = t1;
    t1 = temp;
}
swap<int>(a,b);
条件模板类

假设我们有一个模板类 Wrapper,我们希望禁止 VirtualGuardImpl 类型作为模板参数:

template <
    typename T,
    typename U = T,
    typename = typename std::enable_if<!std::is_same<U, VirtualGuardImpl>::value>::type>
class Wrapper {
public:
    void function() {
        // 实现
    }
};

在这个例子中,如果用户尝试创建 Wrapper<VirtualGuardImpl>Wrapper<VirtualGuardImpl, VirtualGuardImpl> 的实例,编译器将报错,因为 std::enable_if 的条件不满足。但如果使用其他类型,比如 int 或自定义类型,就可以正常编译。

这段代码是 C++ 中的一个模板函数或模板类模板参数的定义,它使用了模板默认参数、std::enable_if 条件编译技术以及类型萃取(type traits)。下面是对这段代码的详细解释:

  1. 模板参数 U:

    • typename U = T 定义了一个模板类型参数 U,并给它一个默认值 T。这意味着如果在使用模板时没有指定 U 的话,它将默认使用模板参数 T 的值。
  2. std::enable_if:

    • std::enable_if 是一个条件编译技术,它只在给定的布尔表达式为 true 时启用某个模板。
    • 在这个例子中,std::enable_if 后面的布尔表达式是 !std::is_same<U, VirtualGuardImpl>::value。这意味着只有当 U 不等于 VirtualGuardImpl 类型时,这个模板参数才有效。
  3. typename 关键字:

    • typename 关键字用于告诉编译器 std::enable_if 的结果是一个类型。std::enable_if 返回的是一个类型,如果条件为 true,它返回一个空的类型,否则会导致编译错误。
  4. std::is_same:

    • std::is_same<U, VirtualGuardImpl>::value 是一个编译时检查,用于判断 UVirtualGuardImpl 是否是相同的类型。::value 是类型特征 std::is_same 的一个成员,它是一个布尔值,如果类型相同则为 true,否则为 false
  5. 组合解释:

    • 这段代码的意思是:定义一个模板参数 U,默认值为 T,并且这个模板参数只有在 U 不是 VirtualGuardImpl 类型时才有效。
    • 这是一种常见的模板编程技巧,用于约束模板参数的类型,以确保它们符合特定的要求。

个数变参模板

#include <stdarg.h>
void Error(const char* format, ...)
{
    va_list argptr;
    va_start(argptr, format);
    vfprintf(stderr, format, argptr);
    va_end(argptr);
}

VA_LIST 是在C语言中解决变参问题的一组宏,变参问题是指参数的个数不定,可以是传入一个参数也可以是多个;可变参数中的每个参数的类型可以不同,也可以相同;可变参数的每个参数并没有实际的名称与之相对应,用起来是很灵活。

(1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针; (2)然后用VA_START宏初始化变量刚定义的VA_LIST变量; (3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用VA_ARG获取各个参数); (4)最后用VA_END宏结束可变参数的获取。

系统提供了vprintf系列格式化字符串的函数,用于编程人员封装自己的I/O函数。

int vprintf  / vscanf   (const char * format, va_list ap);                  // 从标准输入/输出格式化字符串 
int vfprintf / vfsacanf (FILE * stream, const char * format, va_list ap);   // 从文件流 
int vsprintf / vsscanf  (char * s, const char * format, va_list ap);        // 从字符串

返回多个数

使用结构体

struct RowAndCol { int row;int col; };

RowAndCol r(string fn) {
    /*...*/
    RowAndCol result;
    result.row = x;
    result.col = y;
    return result;
}

左值与右值

在 C++ 中,左值(lvalue)右值(rvalue) 是两个重要的概念,用来描述表达式的值和内存的关系。它们帮助开发者理解变量的生命周期、赋值和对象管理,特别是在现代 C++ 中引入了右值引用后,优化了移动语义和资源管理。

1. 左值(lvalue)

左值(lvalue,locatable value) 是指在内存中有明确地址、可持久存在的对象,可以对其进行赋值操作。通俗地说,左值是能够取地址的值,可以出现在赋值操作符的左边。

特点:

  • 左值具有持久的内存地址。
  • 左值可以取地址(使用 & 运算符)。
  • 左值通常表示已经存在的变量或对象。

示例

int x = 10;   // x 是左值
int* p = &x;  // 可以取 x 的地址

x = 20;       // 可以对左值进行赋值

在这个例子中,x 是一个左值,因为它表示了内存中的某个对象,并且可以通过赋值语句修改它的值。

2. 右值(rvalue)

右值(rvalue,readable value) 是没有明确地址、临时存在的对象,不能对其进行赋值操作。它们通常是字面值常量或表达式的结果。右值只能出现在赋值操作符的右边,表示一个临时对象或数据。

特点:

  • 右值是临时的,通常会在表达式结束时销毁。
  • 右值不能取地址(即不能使用 & 获取右值的地址)。
  • 右值表示表达式的计算结果或临时对象。

示例

int y = 10;       // 10 是右值
int z = y + 5;    // y + 5 是右值表达式

在这个例子中,10y + 5 是右值,因为它们表示计算出的临时数据,并且不能直接对这些值进行赋值操作。

3. 现代 C++ 中的右值引用(rvalue reference)

C++11 引入了 右值引用,即通过 && 符号表示。这使得右值也能通过引用进行操作,特别是在实现移动语义(move semantics)和避免不必要的拷贝时非常有用。右值引用允许我们通过右值管理资源,避免性能上的损失。

示例:右值引用与移动语义

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec1 = {1, 2, 3};
    std::vector<int> vec2 = std::move(vec1);  // vec1 资源移动到 vec2

    std::cout << "vec1 size: " << vec1.size() << std::endl;
    std::cout << "vec2 size: " << vec2.size() << std::endl;

    return 0;
}

在这个例子中,std::movevec1 变为一个右值引用,使其内部的资源(如动态分配的内存)直接转移给 vec2,避免了拷贝。

4. 区分左值与右值

通常,左值是表示持久存在的对象,可以通过取地址符 & 获取其地址,而右值是临时的、短暂存在的值,不能直接获取其地址。理解这两者对于编写高效的 C++ 代码和使用现代特性(如右值引用和移动语义)非常重要。

常见误区

  • 字面值常量(如 42、'a')是右值
  • 表达式的结果(如 x + y)通常是右值。
  • 函数返回值若返回的是值,而不是引用,则该返回值是右值。
  • 左值(lvalue) 是可以取地址的值,通常是变量或持久的对象。
  • 右值(rvalue) 是临时值,通常是表达式的结果或字面量。
  • 右值引用(&&)是 C++11 引入的新特性,用来优化资源管理和避免不必要的拷贝操作。

C++11: 花括号初始化列表

使用

在C++98/03中我们只能对普通数组和POD(plain old data,简单来说就是可以用memcpy复制的对象)类型可以使用列表初始化,如下:

//数组的初始化列表: 
int arr[3] = {1,2,3}
//POD类型的初始化列表:
struct A
{
 int x;
 int y;
}a = {1,2};

在C++11中初始化列表被适用性被放大,可以作用于任何类型对象的初始化。如下:

X x1 = X{1,2};
X x2 = {1,2}; // 此处的'='可有可⽆
X x3{1,2};
X* p = new X{1,2};

//列表初始化也可以用在函数的返回值上
std::vector<int> func() {
    return {};
}

变量类型的适用范围

聚合类型可以进行直接列表初始化

聚合类型包括

  1. 普通数组,如int[5],char[],double[]等
  2. 一个类,且满足以下条件:
    1. 没有用户声明的构造函数
    2. 没有用户提供的构造函数(允许显示预置或弃置的构造函数)
    3. 没有私有或保护的非静态数据成员
    4. 没有基类
    5. 没有虚函数
    6. 没有{}和=直接初始化的非静态数据成员
    7. 没有默认成员初始化器

原理

对于一个聚合类型,使用列表初始化相当于使用std::initializer_list对其中的相同类型T的每个元素分别赋值处理,类似下面示例代码;

struct CustomVec {
    std::vector<int> data;
    CustomVec(std::initializer_list<int> list) {
        for (auto iter = list.begin(); iter != list.end(); ++iter) {
            data.push_back(*iter);
        }
    }
};

优势

  1. 方便,且基本上可以替代括号初始化
  2. 可以使用初始化列表接受任意长度
  3. 可以防止类型窄化,避免精度丢失的隐式类型转换

参考文献

  1. https://zh.cppreference.com/

  2. https://leetcode-cn.com/problems/path-with-maximum-gold/solution/huang-jin-kuang-gong-by-leetcode-solutio-f9gg/

  3. https://blog.csdn.net/qq_33221533/article/details/82119031

  4. ⼩贺 C++ ⼋股⽂ PDF 的作者,电⼦书的内容整理于公众号「herongwei」

  5. https://blog.csdn.net/hailong0715/article/details/54018002