前端从零学C++(二):C++基础

120 阅读1小时+

一、背景

我是一个 C/C++ 都没实战经验的前端开发,C 在几年前有了解过一些语法知识,下面我按照《C++ Primer第五版》,跟着一起学习 C++ 基础知识。

注:本文要求至少有一门编程语言的基础。

我的电脑环境:

  • mac
  • vscode 1.88.1

往期精彩:

二、变量和基础类型

为什么每一门编程语言最先介绍的都是数据类型呢,因为数据类型是程序的基础,它告诉我们数据的意义以及我们能在数据上执行的操作。

1. 基本内置类型

在C++里,我们有几种基本的数据类型。首先是算术类型,它包括了:

  • 字符类型:用来存储单个字符,比如字母和符号。
  • 整型:用来存储整数,比如1、-42或1000。
  • 布尔值:用来表示真或假(true 或 false)。
  • 浮点数:用来存储带小数的数字,比如3.14或-0.001。

除了这些,C++还有一种叫做空类型void)。空类型特别的地方在于,它不存储任何具体的值。主要用在一些特殊的场合,例如当你定义一个函数,它没有返回值时,你就会用 void 作为返回类型。这表示这个函数只是执行操作,不会给出任何结果。

- 算术类型
    - 整型
        - bool 布尔 -- 最小尺寸未定义
        - char 字符 -- 最小尺寸 8 位
        - wchar_t 宽字符 -- 最小尺寸 16 位
        - char16_t Unicode 字符 -- 最小尺寸 16 位
        - char32_t Unicode 字符 -- 最小尺寸 32 位
        - short 短整型 -- 最小尺寸 16 位
        - int 整型 -- 最小尺寸 16 位
        - long 长整形 -- 最小尺寸 32 位
        - long long 长整形 -- 最小尺寸 64 位
    - 浮点型
        - float 单精度浮点数 -- 最小尺寸 6 位有效数字
        - double 双精度浮点数 -- 最小尺寸 10 位有效数字
        - longdouble 扩展精度浮点数 -- 最小尺寸 10 位有效数字
- 空类型

1.1 字符类型

C++ 里有好几种字符类型,其中很多都能支持国际化,这意味着它们可以用来表示全世界的各种语言和符号。

最基本的字符类型是char。一个char的大小刚好是机器的一个字节,它能存下机器基本字符集里的任意一个字符的数字值。

除了char,C++还有其他几种字符类型来处理更多的字符。

  • wchar_t,这个类型能确保存下机器最大扩展字符集里的任意一个字符。
  • char16_tchar32_t这两种类型,它们是为Unicode字符集服务的。

Unicode是一个标准,用来表示全世界所有自然语言中的字符,这样你就能在一个文本里混合使用不同语言的字符了。简单来说,这些扩展的字符类型让我们的程序能更轻松地处理国际化的文字和符号。

1.2 整型

在 C++ 里,有几种整数类型,它们的大小和相对关系是有规定的:

  1. int:这是一种常用的整数类型,它的大小至少要和 short 一样大。也就是说,int 至少得能存储和 short 一样多的数据。

  2. long:这个类型的大小至少得和 int 一样大。所以,long 至少得能存储和 int 一样多的数据。

  3. long long:这是C++11引入的一个新类型,它的大小至少得和 long 一样大。简单来说,long long 至少得能存储和 long 一样多的数据。

所以,基本上,这些整数类型的大小是逐级增加的,从 shortint,然后到 long,最后到 long longlong long 是最新加入的类型,它能存储更多的数据。

1.3 带符号类型和无符号类型

在 C++ 中,除了布尔型和一些特殊的字符类型,其他的整型都可以分为两类:带符号的(signed)和无符号的(unsigned)。

  • 带符号类型:可以表示正数、负数和0。常见的带符号类型包括 intshortlonglong long

  • 无符号类型:只能表示0及以上的值。你可以通过在这些带符号类型前加上 unsigned 来得到无符号类型,比如 unsigned int(常缩写为 unsigned)。其他的无符号类型包括 unsigned shortunsigned longunsigned long long

字符类型则稍有不同,分为三种:charsigned charunsigned char。这三种字符类型的表现形式主要取决于编译器。也就是说,char 的表现形式(是带符号还是无符号)由编译器决定。

无符号类型的所有比特都用来表示数值。例如,一个8比特的 unsigned char 可以表示从0到255的值。

带符号类型的表示方式没有硬性规定,但通常采用补码表示法。在这种表示方式下,一个8比特的 signed char 可以表示从 -128 到 127 的范围(尽管理论上可以表示 -127 到 127,实际应用中一般是 -128 到 127)。

1.4 如何选择类型

在 C++ 中,设计准则之一是尽量接近硬件,这也意味着它的算术类型可能会有点复杂,尤其是考虑到不同硬件的特性。为了简化选择数据类型的过程,程序员可以遵循一些经验法则:

  • 无符号类型:如果你知道一个数值绝对不会是负数,最好用无符号类型。这样可以避免不必要的负数范围,从而提高存储效率。

  • 使用 int:一般来说,执行整数运算时使用 int 是比较合适的。在实际应用中,short 类型可能过小,而 long 通常和 int 的大小相同。如果你的数值范围超出了 int 的表示范围,选择 long long 是个不错的选择。

  • 避免用 charbool 进行算术运算charbool 主要用来存储字符和布尔值,进行算术运算时容易出现问题。char 在不同的机器上可能是有符号的也可能是无符号的,因此它在算术运算中可能会引发意料之外的结果。如果你需要处理小整数,建议明确使用 signed charunsigned char

  • 浮点数运算选择 double:对于浮点数运算,建议使用 doublefloat 的精度通常不足,而 doublefloat 的计算代价差别不大。在某些机器上,double 的运算甚至比 float 更快。long double 提供了更高的精度,但在大多数情况下并不必要,而且它会增加运行时的开销。

通过这些原则,你可以更好地选择合适的数据类型,提高程序的效率和稳定性。

练习2.1

类型 int、long、long long 和 short 的区别是什么?无符号类型和带符号类型的区别是什么?float 和 double的区别是什么?

解:

  • C++ 规定 short 和 int 至少16位,long 至少32位,long long 至少64位。
  • 带符号类型能够表示正数、负数和 0 ,而无符号类型只能够表示 0 和正整数。
  • 浮点数的取值范围和精度不同,计算效率也有差异。
练习2.2

计算按揭贷款时,对于利率、本金和付款分别应选择何种数据类型?说明你的理由。

解:

使用double。需要进行浮点计算。

1.5 字面量常量

每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。

练习2.5

指出下述字面值的数据类型并说明每一组内几种字面值的区别:

(a) 'a', L'a', "a", L"a"
(b) 10, 10u, 10L, 10uL, 012, 0xC
(c) 3.14, 3.14f, 3.14L
(d) 10, 10u, 10., 10e-2

解:

  • (a): 字符字面值,宽字符字面值,字符串字面值,宽字符串字面值。
  • (b): 十进制整型,十进制无符号整型,十进制长整型,十进制无符号长整型, 八进制整型,十六进制整型。
  • (c): double, float, long double
  • (d): 十进制整型,十进制无符号整型,double, double
练习2.6

下面两组定义是否有区别,如果有,请叙述之:

int month = 9, day = 7;
int month = 09, day = 07;

解:

第一行定义的是十进制的整型,第二行定义的是八进制的整型。但是month变量有误,八进制不能直接写9。

练习2.7

下述字面值表示何种含义?它们各自的数据类型是什么?

(a) "Who goes with F\145rgus?\012"
(b) 3.14e1L
(c) 1024f
(d) 3.14L

解:

  • (a) Who goes with Fergus?(换行),string 类型
  • (b) long double
  • (c) 无效,因为后缀f只能用于浮点字面量,而1024是整型。
  • (d) long double

2. 变量

在 C++ 里,每个变量都有一个数据类型。这个数据类型决定了几个关键的东西:变量在内存中占用的空间大小、如何在这块空间里存储数据、以及这个变量能进行哪些计算。

练习2.9

解释下列定义的含义,对于非法的定义,请说明错在何处并将其改正。

  • (a) std::cin >> int input_value;
  • (b) int i = { 3.14 };
  • (c) double salary = wage = 9999.99;
  • (d) int i = 3.14;

解:

(a): 应该先定义再使用。

int input_value = 0;
std::cin >> input_value;

(b): 用列表初始化内置类型的变量时,如果存在丢失信息的风险,则编译器将报错。

double i = { 3.14 };

(c): 在这里wage是未定义的,应该在此之前将其定义。

double wage;
double salary = wage = 9999.99;

(d): 不报错,但是小数部分会被截断。

double i = 3.14;
练习2.10

下列变量的初值分别是什么?

std::string global_str;
int global_int;
int main()
{
    int local_int;
    std::string local_str;
}

解:

global_strglobal_int是全局变量,所以初值分别为空字符串和0。 local_int是局部变量并且没有初始化,它的初值是未定义的。 local_strstring 类的对象,它的值由类确定,为空字符串。

练习2.11

指出下面的语句是声明还是定义:

  • (a) extern int ix = 1024;
  • (b) int iy;
  • (c) extern int iz;

解:

(a): 定义 (b): 定义 (c): 声明

2.1 标识符

在 C++ 中,你可以定义多个不同的 int 变量,变量名可以包含字母、数字和下划线,但必须以字母或下划线开头,并且区分大小写。你给出的例子定义了四个不同的 int 变量,如下:

int some_name;
int someName;
int SomeName;
int SOME_NAME;

这些变量名都是有效的,因为它们都符合 C++ 的标识符命名规则:

  1. some_name:由小写字母和下划线组成。
  2. someName:由小写字母和大写字母组成,符合驼峰命名法。
  3. SomeName:由大写字母和小写字母组成,符合大驼峰命名法。
  4. SOME_NAME:由大写字母和下划线组成,符合全大写命名法。

由于 C++ 是区分大小写的,这些变量名被认为是不同的。

2.2 命名规范

变量命名规范在编程中非常重要,因为它们直接影响代码的可读性和可维护性。以下是一些广泛接受的变量命名规范:

  1. 标识符要能体现实际含义

    • 变量名应该具有描述性,能够清晰地表达变量的用途或它所代表的数据。
    • 例如,age 是一个好的变量名,因为它直接表示了年龄;而 ax1 则不够清晰。
  2. 变量名一般用小写字母

    • 小写字母更易于阅读,并且在多数编程风格中更受欢迎。
    • 例如,index 是一个符合这一规范的变量名。
    • 避免使用大写字母开头的变量名,如 Index,除非在特定上下文中(如类名或常量)。
  3. 用户自定义的类名一般以大写字母开头(注意这里是类名,不是变量名):

    • 在许多编程风格中,类名通常以大写字母开头,以区别于变量和方法名。
    • 例如,SalesItem 是一个符合这一规范的类名。
    • 变量名通常不使用这种大写开头的风格。
  4. 如果标识符由多个单词组成,则单词间应有明显区分

    • 当变量名由多个单词组成时,应该使用某种方式来区分这些单词,以提高可读性。
    • 常见的区分方式包括使用下划线(如 student_loan)或驼峰命名法(如 studentLoan)。
    • 避免使用不清晰的命名方式,如 studentl1oanstudetnloan

另外,需要注意的是,在编程中通常避免使用非字母数字的字符(如空格、连字符等)作为变量名的一部分,因为这些字符可能在不同的编程语言或环境中具有特殊含义或不被允许。

总结来说,好的变量命名规范应该遵循简洁、描述性、一致性和可读性的原则。通过遵循这些规范,可以提高代码质量,减少错误,并使得代码更易于被他人理解和维护。

练习2.12

请指出下面的名字中哪些是非法的?

  • (a) int double = 3.14;
  • (b) int _;
  • (c) int catch-22;
  • (d) int 1_or_2 = 1;
  • (e) double Double = 3.14;

解:

(a), (c), (d) 非法。

2.3 作用域

在编程中,作用域(Scope)是决定标识符(如变量、函数、类等)在代码中有效范围的一个重要概念。不同的作用域可以让同一个名字在不同的地方指向不同的实体。理解作用域的概念有助于避免命名冲突,并使代码更具可读性和可维护性。以下是作用域的几个关键点:

  1. 作用域的开始和结束

    • 开始:名字的作用域始于其声明语句。即,当你声明一个变量或其他标识符时,从这一声明开始,这个名字在其作用域中是有效的。
    • 结束:名字的作用域在声明语句所在的作用域的末端结束。作用域的末端通常由闭合的大括号 } 结束。
  2. 局部作用域与全局作用域

    • 局部作用域:局部作用域是在函数、代码块或类中的作用域。局部变量的作用范围仅限于其声明所在的函数或代码块内部。例如:
      void function() {
          int localVar = 10; // localVar 仅在 function 内部有效
      }
      
    • 全局作用域:全局作用域是在整个程序中有效的作用域。全局变量在所有函数和代码块中都是可见的,直到程序结束。例如:
      int globalVar = 20; // globalVar 在整个程序中有效
      
      void anotherFunction() {
          // 可以访问 globalVar
      }
      
  3. 嵌套作用域

    • 在 C++ 中,作用域是嵌套的,这意味着在一个作用域中声明的名字可以被嵌套的作用域所隐藏。例如:
      int x = 5; // 全局作用域
      
      void someFunction() {
          int x = 10; // 局部作用域,隐藏了全局的 x
          {
              int x = 15; // 更内层的局部作用域,隐藏了上一个 x
          }
      }
      
  4. 作用域链

    • 当一个标识符在当前作用域中没有找到时,编译器会向上查找(即作用域链),直到找到该标识符或到达全局作用域为止。如果在整个作用域链中都没有找到标识符,则会产生错误。
  5. 变量隐藏

    • 在嵌套作用域中,如果一个内层作用域声明了一个与外层作用域相同名字的变量,内层作用域的变量将“隐藏”外层作用域的变量。这种情况称为变量隐藏。

示例:

#include <iostream>

int globalVar = 100; // 全局作用域

void exampleFunction() {
    int localVar = 200; // 局部作用域

    std::cout << "Global variable: " << globalVar << std::endl;
    std::cout << "Local variable: " << localVar << std::endl;

    {
        int innerVar = 300; // 更内层作用域
        std::cout << "Inner variable: " << innerVar << std::endl;
    }
}

int main() {
    exampleFunction();
    // std::cout << innerVar; // 错误:innerVar 不在此作用域
    return 0;
}

在这个例子中,globalVar 在整个程序中有效,localVar 仅在 exampleFunction 中有效,而 innerVar 仅在其声明的块内有效。

理解和正确使用作用域规则能够帮助你编写更健壮、可维护的代码。

练习2.13

下面程序中j的值是多少?

int i = 42;
int main()
{
    int i = 100;
    int j = i;
}

解:

j的值是100,局部变量i覆盖了全局变量i

练习2.14

下面的程序合法吗?如果合法,它将输出什么?

int i = 100, sum = 0;
for (int i = 0; i != 10; ++i)
    sum += i;
std::cout << i << " " << sum << std::endl;

解:

合法。输出是 100 45 。

3. 复合类型

3.1 引用

什么是引用?

引用是一个已存在变量的别名。通过引用,你可以用另一种名称来访问变量。引用在定义时必须初始化,并且一旦与某个变量绑定,就不能再绑定到其他变量上。

int a = 10;
int& ref = a;  // ref 是 a 的引用

在这个例子中,refa 的引用,意味着对 ref 的任何操作实际上都是对 a 的操作。

为什么需要引用?
  1. 简化语法: 引用可以简化函数参数的传递,避免了指针的复杂语法。使用引用,代码更简洁、更易读。

    void modifyValue(int& x) {
        x = 20;  // 修改 x 的值,即修改传入的变量
    }
    
    int main() {
        int a = 10;
        modifyValue(a);  // 直接传递 a 的引用
        // a 的值现在是 20
    }
    
  2. 防止不必要的复制: 当你传递大对象或数据结构到函数时,使用引用可以避免复制整个对象,从而提高性能。

    void processLargeObject(LargeObject& obj) {
        // 处理大对象
    }
    
    int main() {
        LargeObject obj;
        processLargeObject(obj);  // 传递对象的引用,避免复制
    }
    
  3. 实现函数链式调用: 引用可以让你实现函数链式调用,即在一个函数中返回自身的引用,方便进行连续的操作。

    class MyClass {
    public:
        MyClass& setValue(int value) {
            this->value = value;
            return *this;
        }
    
    private:
        int value;
    };
    
    int main() {
        MyClass obj;
        obj.setValue(10).setValue(20);  // 链式调用
    }
    
什么时候用引用?
  1. 函数参数传递: 当你需要在函数中修改传入的变量时,使用引用参数可以方便地修改原始变量的值,而不需要返回修改后的结果。

  2. 避免对象复制: 对于大型对象或数据结构,使用引用可以避免复制,特别是在函数参数传递和返回值时。对于类成员函数,通常也用引用传递参数以提高性能。

  3. 实现运算符重载: 在实现类的运算符重载时,通常使用引用来接受和返回对象,以确保操作的效率。

    class MyClass {
    public:
        MyClass operator+(const MyClass& other) const {
            MyClass result;
            // 执行加法操作
            return result;
        }
    };
    
  4. 函数返回值: 当你需要从函数返回对某个对象的引用(而不是复制对象)时,可以使用引用作为返回值类型。但要确保返回的引用有效。

    MyClass& getObject() {
        return obj;  // 返回对对象的引用
    }
    
注意事项
  • 引用必须初始化:引用在声明时必须被初始化,并且不能被重新绑定到其他变量。

  • 避免悬空引用:返回局部变量的引用或使用已经释放内存的引用会导致悬空引用,可能引发未定义行为。

  • 不可修改引用:引用的绑定不能改变,一旦创建,引用就始终引用它初始绑定的变量。

引用是 C++ 提供的一个强大功能,能够提高程序的性能和可读性,但也需要谨慎使用以避免潜在的问题。

3.2 指针

在 C++ 中,指针是一种变量,用于存储另一个变量的内存地址。简单来说,指针就是指向某个变量或对象的地址。

什么是指针?

指针本质上是一个变量,但它不是存储数据的实际值,而是存储数据所在内存位置的地址。例如,假设我们有一个整型变量 int a = 10;,那么指针 int* ptr 可以用来存储变量 a 的地址。

int a = 10;
int* ptr = &a;  // ptr 存储 a 的地址
为什么需要指针?
  1. 动态内存管理: 指针可以用来分配和释放动态内存。通过 newdelete 操作符,你可以在程序运行时动态地管理内存,而不是在编译时确定内存大小。

    int* ptr = new int;  // 动态分配内存
    *ptr = 5;  // 使用动态分配的内存
    delete ptr;  // 释放内存
    
  2. 提高程序效率: 在函数调用中,如果传递大数据结构(如大型数组或对象),通过指针传递比通过值传递更高效。指针传递的是地址,不需要复制数据本身。

    void modifyValue(int* ptr) {
        *ptr = 20;  // 修改指针指向的值
    }
    
    int main() {
        int a = 10;
        modifyValue(&a);  // 传递 a 的地址
        // a 的值现在是 20
    }
    
  3. 实现复杂数据结构: 指针是实现链表、树、图等复杂数据结构的基础。这些数据结构通常需要节点之间的链接,指针在这里起到了连接作用。

    struct Node {
        int data;
        Node* next;  // 指向下一个节点的指针
    };
    
  4. 函数指针和回调函数: 指针可以指向函数,这在需要传递函数作为参数或实现回调机制时非常有用。

    void print(int x) {
        std::cout << x << std::endl;
    }
    
    void execute(void (*func)(int), int value) {
        func(value);  // 调用传递的函数
    }
    
    int main() {
        execute(print, 10);  // 输出 10
    }
    
什么时候用指针?
  1. 动态内存分配: 当你需要在运行时动态地分配内存时,比如处理不确定大小的数据时,使用指针是合适的。

  2. 优化性能: 在函数中传递大型数据结构时,使用指针可以减少内存复制,提高性能。

  3. 数据结构实现: 当你实现复杂的数据结构,如链表、树、图等时,指针是不可或缺的。

  4. 低级编程: 在需要与硬件或底层系统交互时,指针可以帮助你直接操作内存。

指针是 C++ 强大且灵活的特性,但也要小心使用,以避免内存泄漏、悬空指针等问题。适当的指针管理对于编写健壮的 C++ 代码至关重要。

3.4 const 限定符

在 C++ 中,const 关键字用于定义常量,即在程序运行期间值不会改变的变量。const 可以用于修饰变量、指针、函数参数以及函数返回值,目的是增强代码的安全性和可读性。

什么是 const
  1. 常量变量: 当 const 用于修饰变量时,该变量的值在初始化后不能被修改。例如:

    const int x = 10;
    // x = 20;  // 错误:x 是一个常量,不能被修改
    
  2. 常量指针和指针常量const 可以修饰指针,形成两种不同的情况:

    • 指向常量的指针const* 之前): 这种指针所指向的数据不能被修改,但指针本身可以指向其他位置。

      const int* ptr = &x;  // ptr 是一个指向常量整数的指针
      // *ptr = 20;  // 错误:不能通过 ptr 修改值
      ptr = &y;  // 可以改变 ptr 指向其他位置
      
    • 常量指针const* 之后): 这种指针的值(即指针的地址)不能被改变,但指针所指向的数据可以被修改。

      int* const ptr = &x;  // ptr 是一个常量指针
      *ptr = 20;  // 可以通过 ptr 修改值
      // ptr = &y;  // 错误:不能改变 ptr 指向的地址
      
    • 常量指针常量const* 之前和之后): 这种指针既不能改变指向的数据,也不能改变指针本身的地址。

      const int* const ptr = &x;  // ptr 是一个常量指针常量
      // *ptr = 20;  // 错误:不能通过 ptr 修改值
      // ptr = &y;  // 错误:不能改变 ptr 指向的地址
      
  3. 常量函数参数: 使用 const 修饰函数参数可以确保函数不会修改传入的参数。这对于保持数据的完整性非常重要。

    void printValue(const int x) {
        // x = 20;  // 错误:不能修改参数 x
        std::cout << x << std::endl;
    }
    
  4. 常量成员函数: 在类中,使用 const 修饰成员函数可以确保该成员函数不会修改对象的成员变量。常量成员函数只能调用其他常量成员函数。

    class MyClass {
    public:
        void show() const {
            // memberVariable = 10;  // 错误:不能修改成员变量
        }
        
    private:
        int memberVariable;
    };
    
  5. 常量返回值: 使用 const 修饰函数返回值可以防止返回的对象被修改。这对于防止函数外部修改返回对象非常有用。

    const int getValue() {
        return 10;
    }
    
什么时候使用 const
  1. 保护数据不被修改: 使用 const 可以确保某些数据或对象不会被意外修改,这在编写可靠和易维护的代码时非常重要。例如,函数参数如果不会被修改,就可以使用 const 修饰,以避免意外修改。

  2. 提升代码可读性和意图const 明确表达了某些数据在程序中的不变性,使代码更易于理解。它告诉程序员这些数据或对象不会改变,从而避免错误的使用方式。

  3. 优化代码: 在某些情况下,const 可以帮助编译器进行优化。比如,在函数参数或局部变量中使用 const,编译器可能会优化内存访问和缓存使用。

  4. 保护类成员函数: 对于不修改类成员数据的成员函数,使用 const 修饰可以确保对象的状态在调用这些函数时保持不变,从而增强对象的封装性。

  5. 避免不必要的拷贝: 对于传递大型对象(如容器或自定义类)到函数时,可以使用 const 引用来避免不必要的拷贝,同时保证对象在函数内部不会被修改。

    void processLargeObject(const LargeObject& obj) {
        // 使用 obj,但不会修改它
    }
    

const 是 C++ 中一个非常有用的特性,能够提高代码的安全性和可维护性。在合适的地方使用 const 可以帮助你编写更健壮和高效的程序。

4. 类型

在 C++ 中,类型别名、auto 类型说明符和 decltype 类型指示符是用于简化和增强代码的工具。它们各自的用途和使用方法如下:

1. 类型别名

定义和用途

类型别名用于给现有的类型创建一个新的名称。这在代码中可以提高可读性和可维护性,特别是在处理复杂的类型时。

  • typedef:C++98 和 C++03 中使用的定义类型别名的关键字。

    typedef unsigned long ulong;
    ulong x = 1000;
    
  • using:C++11 引入的新语法,更加现代和灵活。

    using ulong = unsigned long;
    ulong y = 2000;
    
什么时候使用
  • 简化复杂的类型:如复杂的模板类型或函数指针类型。

    // 使用 typedef 或 using 简化复杂的模板类型
    typedef std::map<std::string, std::vector<int>> StringIntVectorMap;
    using StringIntVectorMap = std::map<std::string, std::vector<int>>;
    
  • 提高代码可读性:使用有意义的类型别名替代长的类型名称,使代码更易于理解。

    using Iterator = std::vector<int>::iterator;
    

2. auto 类型说明符

定义和用途

auto 是 C++11 引入的类型说明符,用于自动推断变量的类型。编译器根据变量的初始化表达式自动确定变量的类型。

  • 基本用法

    auto x = 10;        // x 的类型被推断为 int
    auto y = 3.14;      // y 的类型被推断为 double
    
  • 用于迭代器

    std::vector<int> v = {1, 2, 3};
    for (auto it = v.begin(); it != v.end(); ++it) {
        std::cout << *it << std::endl;
    }
    
  • decltype 结合使用

    int a = 5;
    auto b = a;  // b 的类型与 a 相同,即 int
    
什么时候使用
  • 简化类型声明:对于复杂的类型,使用 auto 可以减少代码中的冗长和复杂性,尤其是模板类型或迭代器。

  • 保持代码与类型定义同步:如果类型定义发生变化,使用 auto 可以确保变量类型自动适应新的定义。

  • 提升代码可读性:特别是当类型声明非常冗长时,auto 可以使代码更简洁易读。

3. decltype 类型指示符

定义和用途

decltype 是 C++11 引入的关键字,用于推断表达式的类型。与 auto 不同,decltype 不会立即初始化变量,而是仅仅推断表达式的类型。

  • 基本用法

    int x = 10;
    decltype(x) y = 20;  // y 的类型是 int
    
  • 用于声明函数的返回类型

    auto func() -> decltype(x + y) {
        return x + y;
    }
    
  • 用于模板编程

    template <typename T>
    void print(const T& value) {
        decltype(value) copy = value;  // copy 的类型与 value 相同
        std::cout << copy << std::endl;
    }
    
什么时候使用
  • 推断复杂表达式的类型:特别是在处理复杂表达式或模板时,decltype 可以用来获取表达式的确切类型。

  • 确保与表达式类型一致:当你需要确保变量或函数的类型与某个表达式的类型一致时,decltype 非常有用。

  • 在模板编程中提高灵活性:在模板编程中,可以使用 decltype 确保变量的类型与模板参数的类型一致,从而提高代码的泛化能力。

总结

  • 类型别名 (typedefusing):用于创建现有类型的别名,简化复杂类型和提高代码可读性。

  • auto:用于自动推断变量的类型,减少类型声明的冗长,简化代码编写。

  • decltype:用于推断表达式的类型,适用于需要知道表达式类型的场景,例如模板编程和函数返回类型推断。

这三种工具可以结合使用,以提高代码的灵活性、可读性和维护性。

5. 自定义数据结构

在C++中,你可以通过定义类或结构体来自定义数据结构。以下是一个简单的例子,展示如何定义一个表示三维空间中的点的数据结构:

class Point3D {
public:
    float x;
    float y;
    float z;

    // 构造函数
    Point3D(float x = 0.0f, float y = 0.0f, float z = 0.0f) : x(x), y(y), z(z) {}

    // 成员函数,例如计算两点之间的距离
    float distanceTo(const Point3D& other) const {
        float dx = this->x - other.x;
        float dy = this->y - other.y;
        float dz = this->z - other.z;
        return sqrt(dx*dx + dy*dy + dz*dz);
    }
};

在这个例子中,我们定义了一个名为Point3D的类,它有三个公共成员变量xyz,分别表示点在三维空间中的坐标。我们还定义了一个构造函数,用于初始化点的坐标,以及一个成员函数distanceTo,用于计算当前点与另一个点之间的距离。

使用这个自定义数据结构的示例代码如下:

int main() {
    Point3D p1(1.0f, 2.0f, 3.0f);
    Point3D p2(4.0f, 5.0f, 6.0f);
    float distance = p1.distanceTo(p2);
    std::cout << "The distance between p1 and p2 is: " << distance << std::endl;
    return 0;
}

在这个示例中,我们创建了两个Point3D对象p1p2,并调用p1distanceTo函数来计算两点之间的距离。最后,我们将结果打印到控制台。

请注意,这只是一个简单的例子。在实际应用中,你可能需要为你的数据结构添加更多的成员变量和成员函数,以满足你的具体需求。

三、字符串、向量和数组

1. 命名空间的using声明

在 C++ 中,using 声明(或称为 using 指令)用于引入命名空间中的名称,以简化代码并避免在使用这些名称时需要每次都写出完整的命名空间限定符。using 声明允许你在当前作用域中直接使用命名空间中的名称,而不需要使用其完全限定名。

using 声明的基本用法

引入单个名称

你可以使用 using 声明引入命名空间中的单个名称,这样你可以在当前作用域中直接使用这个名称。

示例

#include <iostream>

// 定义一个命名空间
namespace MyNamespace {
    void myFunction() {
        std::cout << "Hello from MyNamespace!" << std::endl;
    }
}

int main() {
    // 使用 `using` 声明引入命名空间中的名称
    using MyNamespace::myFunction;

    // 直接调用函数,不需要再写命名空间限定符
    myFunction(); 

    return 0;
}

在这个例子中,我们使用 using MyNamespace::myFunction; 声明来引入 MyNamespace 命名空间中的 myFunction 函数,使得在 main 函数中可以直接使用 myFunction 而不需要写 MyNamespace::myFunction

引入整个命名空间

你也可以使用 using namespace 声明来引入整个命名空间,使得命名空间中的所有名称在当前作用域内都可以直接使用。这种方式可能会导致命名冲突,尤其是在引入的命名空间很大或有多个命名空间的情况下,因此通常需要谨慎使用。

示例

#include <iostream>

namespace MyNamespace {
    void function1() {
        std::cout << "Function 1" << std::endl;
    }
    
    void function2() {
        std::cout << "Function 2" << std::endl;
    }
}

int main() {
    // 使用 `using namespace` 引入整个命名空间
    using namespace MyNamespace;

    // 直接调用命名空间中的函数
    function1(); 
    function2(); 

    return 0;
}

using 声明的作用

  • 简化代码:通过引入命名空间中的名称,减少了重复书写长的命名空间限定符,使代码更加简洁。

  • 避免命名冲突:虽然 using namespace 可以引入整个命名空间,但要小心命名冲突。在大型项目或多个命名空间时,最好仅引入需要的名称以减少潜在的冲突。

  • 增加可读性:对于短小的函数或类型,使用 using 声明可以使代码更易于阅读和理解。

using 声明与using类型别名的区别

  • using 声明:用于引入命名空间中的名称,使得在当前作用域中可以直接使用这些名称。

  • using 类型别名:用于创建类型的别名,功能类似于 typedef

    示例

    using IntPtr = int*; // 定义一个类型别名 IntPtr,表示 int* 类型
    
    IntPtr ptr = nullptr; // 使用 IntPtr 来定义一个 int* 类型的指针
    

总结

  • using 声明:用于引入命名空间中的一个或多个名称,简化代码书写。
  • using namespace:用于引入整个命名空间,使得该命名空间中的所有名称在当前作用域中可用。
  • using 类型别名:用于创建类型的别名,与 typedef 类似。

在实际编程中,建议在局部范围内使用 using 声明来引入特定名称,避免全局范围内使用 using namespace,以减少命名冲突和提高代码的可维护性。

2. 标准库类型string

C++ 的标准库提供了 std::string 类型,它是一个非常常用的字符串类,用于处理文本数据。std::string 是 C++ 标准库 <string> 头文件中的一部分,并且封装了 C 风格字符串(以 null 结尾的字符数组)的一些常见操作,使字符串处理变得更加方便和安全。

基本特性

std::string 提供了一些基本的功能,包括但不限于:

  • 构造和赋值:可以使用不同的构造函数创建 std::string 对象,也可以通过赋值操作将一个字符串赋给另一个字符串。
  • 连接:可以使用 + 操作符或者 append 方法将两个字符串连接起来。
  • 比较:支持使用关系操作符(如 ==<> 等)对字符串进行比较。
  • 访问和修改字符:可以使用下标运算符 []at() 方法访问和修改字符串中的字符。
  • 查找和替换:提供了查找子字符串、替换子字符串等功能。
  • 大小写转换:可以将字符串转换为大写或小写。
  • 子串:可以提取字符串的子串。

示例代码

以下是一些常见操作的示例:

#include <iostream>
#include <string>

int main() {
    // 创建字符串
    std::string s1 = "Hello";
    std::string s2("World");

    // 字符串连接
    std::string s3 = s1 + " " + s2;  // s3 现在是 "Hello World"
    std::cout << s3 << std::endl;

    // 字符串赋值
    std::string s4;
    s4 = s3;  // s4 现在是 "Hello World"

    // 访问和修改字符
    s4[6] = 'w';  // 修改 s4 中的字符,变成 "Hello wOrld"
    std::cout << s4 << std::endl;

    // 查找子字符串
    std::size_t pos = s4.find("wOrld");
    if (pos != std::string::npos) {
        std::cout << "Found 'wOrld' at position " << pos << std::endl;
    }

    // 提取子串
    std::string sub = s4.substr(6, 5);  // 提取从位置 6 开始的 5 个字符
    std::cout << "Substring: " << sub << std::endl;

    // 比较字符串
    if (s1 == "Hello") {
        std::cout << "s1 is equal to 'Hello'" << std::endl;
    }

    return 0;
}

常用成员函数

  • 构造函数:用于初始化字符串,可以从 C 风格字符串、其他 std::string 对象等初始化。
  • append():将一个字符串或字符追加到当前字符串的末尾。
  • find():查找子字符串在当前字符串中的位置,如果找不到则返回 std::string::npos
  • substr():提取并返回字符串的子串。
  • length()size():返回字符串的长度。
  • empty():检查字符串是否为空。
  • c_str():返回一个 C 风格字符串的指针。

安全性

std::string 类提供了更安全的字符串处理方式,避免了 C 风格字符串的常见问题,如缓冲区溢出和未初始化字符串的使用。它会自动管理内存,确保字符串在操作过程中不会出现内存泄漏或其他内存问题。

总结

std::string 是 C++ 标准库中用于处理字符串的强大工具。它提供了丰富的功能,能够方便地创建、操作、修改和处理字符串数据。通过 std::string,你可以更加安全和高效地处理字符串,而不需要担心底层的内存管理问题。

3. 标准库类型vector

C++ 标准库中的 std::vector 是一个动态数组容器,定义在 <vector> 头文件中。它是 C++ 标准模板库(STL)中的一种序列容器,用于存储同类型的元素,并且可以在运行时动态调整其大小。

基本特性

std::vector 提供了一系列功能,使得动态数组的管理变得方便和高效:

  • 动态大小std::vector 可以在运行时自动调整大小,支持动态增加或减少元素。
  • 随机访问:提供常数时间的随机访问能力,通过下标运算符 []at() 方法访问元素。
  • 自动内存管理std::vector 自动处理内存分配和释放,避免了手动管理内存的问题。
  • 灵活的插入和删除:可以在容器的任意位置插入和删除元素,虽然在末尾操作是最有效的。
  • 元素拷贝:支持元素的拷贝和移动,提供高效的资源管理。

示例代码

以下是一些常见的 std::vector 操作示例:

#include <iostream>
#include <vector>

int main() {
    // 创建一个空的 vector,存储 int 类型的元素
    std::vector<int> vec;

    // 向 vector 中添加元素
    vec.push_back(10);
    vec.push_back(20);
    vec.push_back(30);

    // 访问元素
    std::cout << "First element: " << vec[0] << std::endl;
    std::cout << "Second element: " << vec.at(1) << std::endl;

    // 修改元素
    vec[1] = 25;

    // 遍历 vector 中的元素
    for (const auto& elem : vec) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    // 插入元素
    vec.insert(vec.begin() + 1, 15); // 在位置 1 插入 15

    // 删除元素
    vec.erase(vec.begin() + 2); // 删除位置 2 的元素

    // 获取 vector 的大小
    std::cout << "Size of vector: " << vec.size() << std::endl;

    // 清空 vector
    vec.clear();
    std::cout << "Size after clear: " << vec.size() << std::endl;

    return 0;
}

常用成员函数

  • 构造函数:可以从其他 std::vector、C 风格数组或指定大小和初始值的情况下构造 std::vector 对象。
  • push_back():在容器末尾添加一个元素。
  • pop_back():移除容器末尾的元素。
  • insert():在指定位置插入一个或多个元素。
  • erase():移除指定位置的元素。
  • clear():移除容器中的所有元素。
  • size():返回容器中元素的数量。
  • empty():检查容器是否为空。
  • at():访问指定位置的元素,并进行边界检查。
  • front():返回第一个元素。
  • back():返回最后一个元素。
  • data():返回指向容器内部数组的指针。

性能特性

  • 随机访问:提供常数时间的随机访问,支持高效的元素读取和写入。
  • 插入和删除:在末尾插入或删除元素的时间复杂度为常数时间(摊销复杂度),但在其他位置插入或删除可能会涉及到元素的移动,性能会有所下降。

使用场景

std::vector 是一种通用且高效的动态数组实现,适用于需要频繁访问、动态调整大小以及快速插入和删除操作的场景。它是 C++ 标准库中最常用的容器之一,广泛应用于各种编程任务中。

总结来说,std::vector 提供了灵活且强大的动态数组功能,结合了高效的随机访问和自动内存管理,适合在需要动态调整大小的场景下使用。

4. 迭代器介绍

在C++中,迭代器(Iterator)是一种设计模式,它允许顺序访问一个聚合对象(如列表、集合、树、图等数据结构)中的各个元素,而又不暴露该对象的内部表示。迭代器提供了一种方法来访问容器中的元素,而无需了解容器底层的实现细节。

迭代器的基本概念

  • 迭代器类型:C++标准库中的容器(如std::vector, std::list, std::map等)都提供了自己的迭代器类型。这些迭代器类型通常与容器紧密相关,以提供高效的元素访问。
  • 开始和结束迭代器:每个容器都提供了begin()end()成员函数,分别返回指向容器第一个元素和“尾后元素”(即容器最后一个元素之后的位置)的迭代器。
  • 迭代器失效:当容器结构发生变化时(如插入、删除元素),可能会导致迭代器失效。因此,在对容器进行修改后,应谨慎使用之前获取的迭代器。

迭代器的分类

C++中的迭代器主要分为以下几类:

  1. 输入迭代器(Input Iterator):只能向前移动,可以读取元素但不能修改。例如,std::istream_iterator就是一个输入迭代器。

  2. 前向迭代器(Forward Iterator):可以向前移动,并能多次遍历同一元素。它们提供比输入迭代器更多的功能,但仍然不能后退。

  3. 双向迭代器(Bidirectional Iterator):可以向前和向后移动。std::liststd::set的迭代器就是双向迭代器。

  4. 随机访问迭代器(Random Access Iterator):提供最高级别的功能,可以在常量时间内跳转到容器的任意位置。std::vectorstd::deque的迭代器就是随机访问迭代器。

迭代器的使用

下面是一个简单的示例,展示了如何使用std::vector的迭代器来遍历容器中的元素:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    
    // 使用迭代器遍历vector
    for (std::vector<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " "; // 使用 * 操作符来解引用迭代器,获取元素值
    }
    std::cout << std::endl;
    
    // 使用C++11的范围for循环(底层也是使用迭代器)
    for (const auto& num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

在这个例子中,我们首先创建了一个包含整数的std::vector。然后,我们使用begin()end()成员函数获取迭代器的范围,并通过迭代器来遍历和打印容器中的每个元素。此外,我们还展示了如何使用C++11引入的范围for循环来简化遍历过程。

注意事项

  • 当使用迭代器时,要确保不要越界访问,即不要解引用end()返回的迭代器,因为它并不指向有效的元素。
  • 在对容器进行修改(如插入、删除元素)时,要注意迭代器可能会失效。通常,在修改操作后,最好重新获取迭代器。
  • 不同类型的容器提供不同类型的迭代器,要根据容器的特性选择合适的迭代器类型。

5. 数组

在 C++ 中,数组是一种用于存储同一类型多个元素的数据结构。数组的元素在内存中是连续存储的,因此可以通过下标快速访问。C++ 提供了两种主要的数组类型:C 风格数组和 C++ 标准库中的 std::array

C 风格数组

C 风格数组是 C++ 中的传统数组形式,具有以下特点:

  • 定义和初始化

    int arr[5];  // 定义一个包含 5 个整数的数组
    int arr2[3] = {1, 2, 3};  // 定义并初始化数组
    
  • 访问元素: 通过下标访问数组元素,下标从 0 开始。

    arr2[0] = 10;  // 将第一个元素设置为 10
    int x = arr2[1];  // 获取第二个元素的值
    
  • 大小: C 风格数组的大小是固定的,在定义时指定,并且不能在运行时改变。如果需要动态调整大小,通常需要使用动态内存分配(如 newstd::vector)。

  • 范围检查: C 风格数组不提供范围检查,因此访问超出有效范围的下标会导致未定义行为。

std::array

C++11 引入了 std::array,它是 C++ 标准库中的一个模板类,提供了比 C 风格数组更多的功能和安全性。std::array 位于 <array> 头文件中。

  • 定义和初始化

    #include <array>
    std::array<int, 5> arr;  // 定义一个包含 5 个整数的 std::array
    std::array<int, 3> arr2 = {1, 2, 3};  // 定义并初始化 std::array
    
  • 访问元素std::array 提供了与 C 风格数组类似的访问方式,但还提供了更安全的方法。

    arr2[0] = 10;  // 将第一个元素设置为 10
    int x = arr2.at(1);  // 使用 at() 方法获取第二个元素的值,提供范围检查
    
  • 大小std::array 的大小在编译时确定,不能在运行时改变,但可以通过 size() 成员函数获取数组的大小。

    std::cout << "Size of arr2: " << arr2.size() << std::endl;
    
  • 其他功能std::array 提供了额外的功能,如:

    • at():提供范围检查的访问方式。
    • front()back():分别返回数组的第一个和最后一个元素。
    • fill():用指定的值填充整个数组。
    • data():返回指向数组内存的指针。

示例代码

下面是一个展示如何使用 C 风格数组和 std::array 的示例:

#include <iostream>
#include <array>

int main() {
    // C 风格数组
    int c_array[3] = {4, 5, 6};
    
    std::cout << "C-style array elements:" << std::endl;
    for (int i = 0; i < 3; ++i) {
        std::cout << c_array[i] << " ";
    }
    std::cout << std::endl;
    
    // std::array
    std::array<int, 3> cpp_array = {7, 8, 9};
    
    std::cout << "std::array elements:" << std::endl;
    for (const auto& elem : cpp_array) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    
    // 使用 std::array 的成员函数
    cpp_array.at(1) = 10;  // 修改第二个元素
    cpp_array.fill(0);  // 用 0 填充整个 std::array
    
    std::cout << "Modified std::array elements after fill:" << std::endl;
    for (const auto& elem : cpp_array) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

总结

  • C 风格数组:简单直接,但缺乏安全性和灵活性。适合对性能有严格要求的场景,但需要小心处理数组越界等问题。
  • std::array:提供了更高的安全性和更多的功能,适用于编译时大小已知的数组。它支持范围检查、元素访问、填充等操作,是现代 C++ 编程中的推荐选择。

选择使用 C 风格数组还是 std::array 取决于具体的应用需求和对功能的要求。在现代 C++ 编程中,std::array 更受欢迎,因为它提供了更强的安全性和灵活性。

6. 多维数组

在 C++ 中,多维数组是指具有两个或更多维度的数组。多维数组可以用来存储表格数据或矩阵等结构。最常见的多维数组是二维数组,但 C++ 也支持更多维度的数组。下面是对多维数组的详细介绍:

定义和初始化

二维数组

二维数组是最常见的多维数组形式,可以看作是一个数组的数组。定义一个二维数组的语法如下:

type arrayName[rows][columns];

其中,type 是数组元素的类型,rows 是行数,columns 是列数。

示例:

int matrix[3][4];  // 定义一个 3 行 4 列的二维数组

二维数组也可以在定义时进行初始化:

int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};
三维及更高维度的数组

定义三维数组的语法如下:

type arrayName[depth][rows][columns];

示例:

int tensor[2][3][4];  // 定义一个 2 层 3 行 4 列的三维数组

同样,三维及更高维度的数组也可以在定义时进行初始化:

int tensor[2][2][3] = {
    {
        {1, 2, 3},
        {4, 5, 6}
    },
    {
        {7, 8, 9},
        {10, 11, 12}
    }
};

访问元素

可以使用下标操作符访问多维数组的元素。对于二维数组,访问方式如下:

matrix[row][column] = value;  // 设置指定位置的值
int value = matrix[row][column];  // 获取指定位置的值

对于三维及更高维度的数组,访问方式类似:

tensor[depth][row][column] = value;
int value = tensor[depth][row][column];

传递到函数

多维数组可以作为参数传递给函数,函数的声明和定义可以使用以下语法:

对于二维数组:
void printMatrix(int matrix[3][4]) {
    // 函数体
}

或者使用指针和数组形式:

void printMatrix(int (*matrix)[4], int rows) {
    // 函数体
}
对于三维数组:
void printTensor(int tensor[2][3][4]) {
    // 函数体
}

或者使用指针和数组形式:

void printTensor(int (*tensor)[3][4], int depth) {
    // 函数体
}

注意事项

  1. 内存布局:多维数组在内存中是连续存储的。例如,二维数组 matrix[3][4] 在内存中按行优先的顺序存储。

  2. 维度大小:在定义数组时,所有维度的大小(除了第一个维度)必须是常量表达式。如果维度大小不确定,可以使用动态分配(如 std::vector 或动态内存分配)来处理。

  3. 动态多维数组:C++ 标准库中没有直接支持动态多维数组,但可以使用 std::vector 嵌套来实现动态大小的多维数组。例如:

    #include <vector>
    
    std::vector<std::vector<int>> matrix(3, std::vector<int>(4));
    
  4. 内存消耗:多维数组可能会消耗大量内存,特别是在高维度或大规模数组时。要注意内存的管理和使用。

示例代码

下面是一个示例代码,演示了如何定义、初始化和访问二维数组:

#include <iostream>

int main() {
    // 定义并初始化一个二维数组
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    // 打印二维数组的元素
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 4; ++j) {
            std::cout << matrix[i][j] << " ";
        }
        std::cout << std::endl;
    }
    
    return 0;
}

这个代码定义了一个 3 行 4 列的二维数组,并通过嵌套循环遍历并打印数组的元素。

总结

多维数组在 C++ 中非常强大,适合处理复杂的表格数据或矩阵计算。理解如何定义、初始化、访问和传递多维数组是 C++ 编程的重要技能。

四、表达式

4.1 基础

4.2 算术运算符

在 C++ 中,算术表达式用于执行数学计算。它们是由运算符和操作数组成的表达式,这些运算符执行基本的数学操作,如加法、减法、乘法、除法等。算术表达式在 C++ 中具有非常重要的作用,几乎所有的程序都会用到这些表达式。以下是对 C++ 中算术表达式的详细介绍:

基本运算符
  1. 加法 (+)

    • 用于计算两个操作数的和。
    • 示例:a + b
  2. 减法 (-)

    • 用于计算两个操作数的差。
    • 示例:a - b
  3. 乘法 (*)

    • 用于计算两个操作数的积。
    • 示例:a * b
  4. 除法 (/)

    • 用于计算两个操作数的商。对于整数除法,如果结果有余数,则会丢弃余数。
    • 示例:a / b
  5. 取余 (%)

    • 用于计算两个操作数的余数,仅适用于整数类型。
    • 示例:a % b
  6. 自增 (++) 和 自减 (--)

    • 自增 (++):将变量的值增加 1。可以作为前置自增(++a)或后置自增(a++)。
    • 自减 (--):将变量的值减少 1。可以作为前置自减(--a)或后置自减(a--)。

    示例:

    int a = 5;
    int b = ++a;  // a 变为 6,b 也为 6
    int c = a--;  // a 变为 5,c 为 6
    
优先级和结合性

算术运算符在表达式中有优先级和结合性,决定了运算的顺序:

  • 优先级:运算符的优先级决定了在一个表达式中,哪个运算符先执行。一般情况下,乘法、除法和取余的优先级高于加法和减法。
  • 结合性:运算符的结合性决定了同一级别的运算符的执行顺序。大多数算术运算符的结合性是从左到右,即从左向右依次计算。

示例:

int result = 5 + 3 * 2;  // result 为 11,因为乘法的优先级高于加法
表达式的类型和转换
  • 整型表达式:如果所有操作数都是整数类型,结果也是整数类型。如果涉及除法,可能会丢失余数。
  • 浮点型表达式:如果操作数中有浮点数类型(floatdoublelong double),结果将是浮点数类型。浮点数除法会返回精确的商(包含小数部分)。

示例:

int a = 5;
double b = 2.0;
double result = a / b;  // result 为 2.5,因为浮点数除法
  • 类型转换:在混合整数和浮点数的运算时,整数会被转换为浮点数以进行计算,计算结果也将是浮点数。

示例:

int a = 5;
float b = 2.0;
float result = a / b;  // result 为 2.5,因为 a 被转换为 float
算术表达式的例子

示例 1:简单的算术表达式

#include <iostream>

int main() {
    int a = 10;
    int b = 20;
    int sum = a + b;
    int difference = a - b;
    int product = a * b;
    int quotient = a / b;
    int remainder = a % b;

    std::cout << "Sum: " << sum << std::endl;
    std::cout << "Difference: " << difference << std::endl;
    std::cout << "Product: " << product << std::endl;
    std::cout << "Quotient: " << quotient << std::endl;
    std::cout << "Remainder: " << remainder << std::endl;

    return 0;
}

示例 2:复合算术表达式

#include <iostream>

int main() {
    int a = 5;
    int b = 10;
    int c = 15;

    int result = (a + b) * c / 2 - b % 4;

    std::cout << "Result: " << result << std::endl;  // Result 的计算遵循优先级规则

    return 0;
}
总结
  • 算术运算符:包括加法、减法、乘法、除法、取余、自增、自减等,能够进行基本的数学运算。
  • 优先级和结合性:运算符的优先级决定了运算顺序,而结合性则决定了同优先级运算符的执行顺序。
  • 类型和转换:整数和浮点数的混合运算会涉及类型转换,影响结果的类型。

理解这些基本概念对于编写正确和高效的 C++ 代码至关重要。

4.3 逻辑和关系运算符

在C++中,逻辑和关系运算符用于比较操作数之间的关系,并根据这些关系返回布尔值(true或false)。这些运算符在编写条件语句(如if语句)和循环(如while循环)时非常有用,因为它们允许程序根据特定条件来执行代码块。

关系运算符

关系运算符用于比较两个操作数之间的关系,并返回一个布尔值。C++中的关系运算符包括:

  1. 等于 (==):检查两个操作数是否相等。
  2. 不等于 (!=):检查两个操作数是否不相等。
  3. 大于 (>):检查第一个操作数是否大于第二个操作数。
  4. 小于 (<):检查第一个操作数是否小于第二个操作数。
  5. 大于等于 (>=):检查第一个操作数是否大于或等于第二个操作数。
  6. 小于等于 (<=):检查第一个操作数是否小于或等于第二个操作数。

示例

int a = 5;
int b = 10;
bool isEqual = (a == b); // false
bool isNotEqual = (a != b); // true
bool isGreater = (a > b); // false
bool isLess = (a < b); // true
bool isGreaterOrEqual = (a >= b); // false
bool isLessOrEqual = (a <= b); // true

逻辑运算符

逻辑运算符用于组合或修改布尔表达式,并返回一个布尔值。C++中的逻辑运算符包括:

  1. 逻辑与 (&&):当且仅当两个操作数都为true时,结果才为true。
  2. 逻辑或 (||):当至少有一个操作数为true时,结果为true。
  3. 逻辑非 (!):对操作数进行逻辑非运算,即如果操作数为true,则结果为false;如果操作数为false,则结果为true。

示例

bool a = true;
bool b = false;
bool logicalAnd = (a && b); // false
bool logicalOr = (a || b); // true
bool logicalNot = (!a); // false

三、短路行为

在C++中,逻辑与(&&)和逻辑或(||)运算符具有短路行为。这意味着:

  • 对于逻辑与(&&),如果第一个操作数为false,则整个表达式已经确定为false,因此不会评估第二个操作数。
  • 对于逻辑或(||),如果第一个操作数为true,则整个表达式已经确定为true,因此不会评估第二个操作数。

这种短路行为可以用于优化性能或避免不必要的操作,例如访问无效的内存位置。

四、总结

逻辑和关系运算符是C++中编写条件逻辑的基础。通过组合这些运算符,您可以创建复杂的布尔表达式,以控制程序的流程和执行路径。

4.4 赋值运算符

赋值运算符用于将值赋给变量。在 C++ 中,赋值运算符主要是 =,但除此之外,还有其他一些复合赋值运算符,可以在赋值的同时进行其他运算。以下是 C++ 中赋值运算符的详细介绍。

基本赋值运算符

  1. 赋值 (=)
    • 将右侧表达式的值赋给左侧变量。
    • 示例:a = 5; 将值 5 赋给变量 a

复合赋值运算符

复合赋值运算符结合赋值和其他运算,可以让代码更加简洁。它们的形式通常为基本运算符与 = 的组合。常见的复合赋值运算符包括:

  1. 加法赋值 (+=)

    • 用于将右侧的值加到左侧的变量上,并将结果赋给左侧变量。
    • 示例:a += b; 等同于 a = a + b;
  2. 减法赋值 (-=)

    • 用于将右侧的值从左侧的变量中减去,并将结果赋给左侧变量。
    • 示例:a -= b; 等同于 a = a - b;
  3. 乘法赋值 (*=)

    • 用于将左侧的变量乘以右侧的值,并将结果赋给左侧变量。
    • 示例:a *= b; 等同于 a = a * b;
  4. 除法赋值 (/=)

    • 用于将左侧的变量除以右侧的值,并将结果赋给左侧变量。
    • 示例:a /= b; 等同于 a = a / b;
  5. 取余赋值 (%=)

    • 用于将左侧变量对右侧的值取余,并将结果赋给左侧变量。
    • 示例:a %= b; 等同于 a = a % b;
  6. 左移赋值 (<<=)

    • 用于将左侧变量的值左移指定的位数。
    • 示例:a <<= 2; 等同于 a = a << 2;
  7. 右移赋值 (>>=)

    • 用于将左侧变量的值右移指定的位数。
    • 示例:a >>= 2; 等同于 a = a >> 2;

示例代码

以下是使用赋值运算符的简单示例:

#include <iostream>

int main() {
    int a = 10;        // 使用赋值运算符
    int b = 5;

    a += b;           // 相当于 a = a + b; 现在 a 的值为 15
    std::cout << "a += b: " << a << std::endl;

    a -= b;           // 相当于 a = a - b; 现在 a 的值为 10
    std::cout << "a -= b: " << a << std::endl;

    a *= b;           // 相当于 a = a * b; 现在 a 的值为 50
    std::cout << "a *= b: " << a << std::endl;

    a /= b;           // 相当于 a = a / b; 现在 a 的值为 10
    std::cout << "a /= b: " << a << std::endl;

    a %= b;           // 相当于 a = a % b; 现在 a 的值为 0
    std::cout << "a %= b: " << a << std::endl;

    return 0;
}

总结

  • 赋值运算符用于将值赋给变量,基本的赋值运算符是 =
  • 复合赋值运算符结合了赋值与其他运算,常见的有 +=-=*=/=%= 等。
  • 使用复合赋值运算符可以使代码更简洁,提高可读性。

掌握赋值运算符是C++编程的基础,有助于更高效地进行变量赋值和运算。

4.5 递增和递减运算符

递增和递减运算符用于增加或减少变量的值。它们是 C++ 中的基本运算符,常用于循环控制、计数器更新等场景。C++ 中的递增和递减运算符包括 ++--,这两个运算符可以用作前置运算符或后置运算符。

递增运算符 (++)

递增运算符用于将变量的值增加 1。在 C++ 中,递增运算符有两种形式:

  1. 前置递增 (++a)

    • 在表达式求值之前,将变量 a 的值增加 1。
    • 示例:++a 等同于 a = a + 1,但在计算中 ++a 会在它自身的值被使用之前完成递增。
  2. 后置递增 (a++)

    • 在表达式求值之后,将变量 a 的值增加 1。
    • 示例:a++ 等同于 a = a + 1,但在计算中 a++ 会在它自身的值被使用之后进行递增。

示例

#include <iostream>

int main() {
    int a = 5;

    std::cout << "前置递增: " << ++a << std::endl; // a 先增加到 6,然后打印 6
    std::cout << "后置递增: " << a++ << std::endl; // 打印 6,然后 a 增加到 7
    std::cout << "当前值: " << a << std::endl;     // 打印 7

    return 0;
}

递减运算符 (--)

递减运算符用于将变量的值减少 1。与递增运算符类似,递减运算符也有前置和后置形式:

  1. 前置递减 (--a)

    • 在表达式求值之前,将变量 a 的值减少 1。
    • 示例:--a 等同于 a = a - 1,但在计算中 --a 会在它自身的值被使用之前完成递减。
  2. 后置递减 (a--)

    • 在表达式求值之后,将变量 a 的值减少 1。
    • 示例:a-- 等同于 a = a - 1,但在计算中 a-- 会在它自身的值被使用之后进行递减。

示例

#include <iostream>

int main() {
    int a = 5;

    std::cout << "前置递减: " << --a << std::endl; // a 先减少到 4,然后打印 4
    std::cout << "后置递减: " << a-- << std::endl; // 打印 4,然后 a 减少到 3
    std::cout << "当前值: " << a << std::endl;     // 打印 3

    return 0;
}

应用场景

  1. 循环控制

    • 递增和递减运算符常用于 forwhile 循环中,控制循环变量的值。

    示例

    for (int i = 0; i < 10; ++i) {
        std::cout << i << " "; // 打印 0 到 9
    }
    
  2. 计数器更新

    • 在处理计数器时,递增和递减运算符可以方便地更新计数器的值。

    示例

    int count = 0;
    count++; // count 增加到 1
    count--; // count 减少到 0
    

总结

  • 递增运算符 (++):用于将变量的值增加 1,可以作为前置或后置运算符,前置递增先更新值再使用,后置递增先使用值再更新。
  • 递减运算符 (--):用于将变量的值减少 1,也可以作为前置或后置运算符,前置递减先更新值再使用,后置递减先使用值再更新。
  • 应用场景:递增和递减运算符广泛用于循环控制和计数器更新等操作中。

了解并正确使用递增和递减运算符可以帮助你编写更加简洁和高效的代码。

4.6 成员访问运算符

成员访问运算符在 C++ 中用于访问类或结构体的成员(即变量和方法)。这些运算符包括 .(点运算符)和 ->(箭头运算符),它们用于不同的上下文中。了解这两个运算符的使用方式对于编写面向对象的 C++ 代码至关重要。

点运算符 (.)

点运算符用于访问对象的成员。当你有一个对象实例,并且想要访问它的成员(包括属性和方法)时,你会使用点运算符。

用法

  • object.member:用于访问对象 object 的成员 member

示例

#include <iostream>

class Person {
public:
    std::string name;
    int age;

    void display() {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

int main() {
    Person person;         // 创建一个 Person 对象
    person.name = "Alice"; // 使用点运算符访问并修改成员变量
    person.age = 30;       // 使用点运算符访问并修改成员变量
    person.display();     // 使用点运算符调用成员函数

    return 0;
}

在上面的例子中,person.nameperson.age 使用点运算符访问对象的成员变量,而 person.display() 调用成员函数。

箭头运算符 (->)

箭头运算符用于访问指针指向的对象的成员。当你有一个指向对象的指针时,你使用箭头运算符来访问该对象的成员。

用法

  • pointer->member:用于访问指针 pointer 指向的对象的成员 member

示例

#include <iostream>

class Person {
public:
    std::string name;
    int age;

    void display() {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

int main() {
    Person* personPtr = new Person(); // 创建一个指向 Person 对象的指针
    personPtr->name = "Bob";          // 使用箭头运算符访问并修改成员变量
    personPtr->age = 25;              // 使用箭头运算符访问并修改成员变量
    personPtr->display();            // 使用箭头运算符调用成员函数

    delete personPtr; // 释放动态分配的内存
    return 0;
}

在上面的例子中,personPtr->namepersonPtr->age 使用箭头运算符访问指针所指向的对象的成员,而 personPtr->display() 调用成员函数。

总结

  • 点运算符 (.):用于访问对象实例的成员。适用于直接使用对象实例的情况。
  • 箭头运算符 (->:用于访问指针所指向对象的成员。适用于使用指针访问对象的情况。

掌握这两种成员访问运算符的用法对于有效地操作对象和类非常重要,它们使得对象的属性和行为可以被正确地访问和修改。

4.7 条件运算符

C++ 中的条件运算符(也称为三目运算符)是一个非常实用的运算符,用于简化条件语句的编写。其语法和功能类似于其他语言中的条件运算符,主要用来在一个表达式中根据条件选择不同的值。

条件运算符的语法

condition ? expression1 : expression2;
  • condition: 这是一个布尔表达式,结果为 truefalse
  • expression1: 如果 conditiontrue,则选择并计算这个表达式的值。
  • expression2: 如果 conditionfalse,则选择并计算这个表达式的值。

使用示例

  1. 基本示例

    int a = 10, b = 20;
    int max = (a > b) ? a : b;
    // max 的值将是 20,因为 a <= b
    
  2. 作为函数返回值

    int getMax(int x, int y) {
        return (x > y) ? x : y;
    }
    
  3. 嵌套使用

    条件运算符可以嵌套使用,虽然这样可能会使代码变得难以阅读。

    int x = 5;
    int y = 10;
    int z = 15;
    int result = (x < y) ? ((y < z) ? y : z) : x;
    // result 的值将是 10,因为 x < y 且 y < z
    

条件运算符的特点

  • 简洁:条件运算符使代码更加简洁,尤其是对于简单的条件判断和赋值操作。
  • 优先级:条件运算符的优先级低于大多数其他运算符,所以在使用时可能需要使用括号来确保表达式按预期计算。
  • 结合性:条件运算符的结合性是从右到左。这意味着如果有多个条件运算符嵌套在一起,它们会从右向左计算。

注意事项

  • 可读性:虽然条件运算符使代码更简洁,但对于复杂的条件或操作,使用传统的 if-else 语句可能会使代码更加易于理解和维护。
  • 返回值:条件运算符返回的是 expression1expression2 中的值,而不是对这两个表达式的操作。因此,要确保在使用条件运算符时,两个表达式的类型一致,否则可能会出现类型不匹配的错误。

总结

条件运算符是一种有用的工具,可以在一个表达式中选择两个值之一。它的简洁性和灵活性使其在编写条件逻辑时非常方便,但也需要注意其可读性和优先级。

4.8 位运算符

在 C++ 中,位运算符用于对整数的二进制位进行操作。这些运算符直接对数据的二进制表示进行操作,非常适合低级编程、优化和硬件编程。以下是 C++ 中的主要位运算符及其用法介绍:

按位与 (&)

  • 作用:对两个操作数的每一位进行与操作。只有当两个对应的二进制位都为 1 时,结果位才为 1。
  • 语法
    result = a & b;
    
  • 示例
    int a = 12; // 二进制: 00001100
    int b = 7;  // 二进制: 00000111
    int result = a & b; // 二进制: 00000100,即 result = 4
    

按位或 (|)

  • 作用:对两个操作数的每一位进行或操作。只要两个对应的二进制位中至少有一个为 1,结果位就为 1。
  • 语法
    result = a | b;
    
  • 示例
    int a = 12; // 二进制: 00001100
    int b = 7;  // 二进制: 00000111
    int result = a | b; // 二进制: 00001111,即 result = 15
    

按位异或 (^)

  • 作用:对两个操作数的每一位进行异或操作。只有当两个对应的二进制位不相同时,结果位才为 1。
  • 语法
    result = a ^ b;
    
  • 示例
    int a = 12; // 二进制: 00001100
    int b = 7;  // 二进制: 00000111
    int result = a ^ b; // 二进制: 00001011,即 result = 11
    

按位取反 (~)

  • 作用:对操作数的每一位进行取反操作。即将每一位的 0 变为 1,1 变为 0。
  • 语法
    result = ~a;
    
  • 示例
    int a = 12; // 二进制: 00001100
    int result = ~a; // 二进制: 11110011,即 result = -13 (取决于数据类型的位数和符号表示)
    

左移 (<<)

  • 作用:将操作数的所有位向左移动指定的位数。移动后,右侧空出的位用 0 填充。
  • 语法
    result = a << n;
    
  • 示例
    int a = 3; // 二进制: 00000011
    int result = a << 2; // 二进制: 00001100,即 result = 12
    

右移 (>>)

  • 作用:将操作数的所有位向右移动指定的位数。对于无符号数,左侧空出的位用 0 填充。对于有符号数,左侧空出的位根据符号位填充(算术右移)。
  • 语法
    result = a >> n;
    
  • 示例
    int a = 12; // 二进制: 00001100
    int result = a >> 2; // 二进制: 00000011,即 result = 3
    

位运算符的应用

  1. 位标志: 位运算常用于设置、清除和检查位标志。通过按位与、按位或和按位异或运算,可以方便地操作二进制标志位。

    示例

    const int FLAG_A = 1 << 0; // 00000001
    const int FLAG_B = 1 << 1; // 00000010
    int flags = 0;
    
    // 设置 FLAG_A
    flags |= FLAG_A;
    
    // 清除 FLAG_B
    flags &= ~FLAG_B;
    
    // 检查 FLAG_A 是否设置
    if (flags & FLAG_A) {
        std::cout << "FLAG_A is set" << std::endl;
    }
    
  2. 位掩码: 使用位运算可以创建掩码,提取特定的位值。例如,从一个整数中提取某些位的值。

    示例

    int value = 0b10101010;
    int mask = 0b00001111;
    int result = value & mask; // 提取低 4 位,即 result = 0b00001010
    

总结

  • 按位与 (&):两位都为 1 时结果为 1。
  • 按位或 (|):任意一位为 1 时结果为 1。
  • 按位异或 (^):两位不同时结果为 1。
  • 按位取反 (~):将每一位取反。
  • 左移 (<<):向左移动位,右侧补 0。
  • 右移 (>>):向右移动位,根据类型和符号填充。

位运算符是高效的低级操作工具,适用于需要直接操作二进制位的场景,例如底层编程、优化和系统编程。

4.9 sizeof运算符

在 C++ 中,sizeof 运算符用于确定数据类型或对象在内存中占用的字节数。它是一个编译时运算符,这意味着它的计算是在编译时完成的,而不是在运行时。

基本用法

  1. 对基本数据类型的使用

    int a;
    std::cout << "Size of int: " << sizeof(int) << " bytes" << std::endl;
    std::cout << "Size of a: " << sizeof(a) << " bytes" << std::endl;
    

    这段代码会输出 int 类型和变量 a 占用的内存字节数。

  2. 对数组的使用

    int arr[10];
    std::cout << "Size of arr: " << sizeof(arr) << " bytes" << std::endl;
    std::cout << "Size of one element in arr: " << sizeof(arr[0]) << " bytes" << std::endl;
    std::cout << "Number of elements in arr: " << sizeof(arr) / sizeof(arr[0]) << std::endl;
    

    这里 sizeof(arr) 返回整个数组 arr 的大小,而 sizeof(arr[0]) 返回数组中单个元素的大小。通过这两个值可以计算数组中的元素数量。

  3. 对结构体和类的使用

    struct MyStruct {
        int a;
        double b;
    };
    
    std::cout << "Size of MyStruct: " << sizeof(MyStruct) << " bytes" << std::endl;
    

    sizeof 可以用来确定自定义结构体或类的大小。注意,结构体和类的大小可能会受到内存对齐和填充的影响。

sizeof 运算符的特点

  1. 编译时计算sizeof 是在编译时进行计算的,因此不会影响程序的运行效率。

  2. 作用于类型和对象

    • sizeof(type):获取类型 type 的大小。
    • sizeof(expression):获取表达式 expression 的大小。注意,对于 sizeof 作用于表达式的情况,它不会对表达式进行求值。
  3. 数组和指针

    • 对于数组,sizeof 返回整个数组的大小。
    • 对于指针,sizeof 返回指针本身的大小,而不是指针所指向的数据的大小。

    示例

    int arr[10];
    int* ptr = arr;
    
    std::cout << "Size of arr: " << sizeof(arr) << " bytes" << std::endl; // 数组总大小
    std::cout << "Size of ptr: " << sizeof(ptr) << " bytes" << std::endl; // 指针大小
    
  4. 对齐和填充: 结构体和类的大小可能会受到对齐和填充的影响。这是因为编译器可能会在结构体和类的成员之间插入填充字节,以使数据对齐到适当的内存边界。

    示例

    struct AlignedStruct {
        char a;
        int b;
    };
    
    std::cout << "Size of AlignedStruct: " << sizeof(AlignedStruct) << " bytes" << std::endl;
    

常见问题

  1. sizeof 运算符的结果与平台相关: 不同的平台和编译器可能会对数据类型的大小有所不同。例如,int 类型在不同的平台上可能占用不同的字节数。

  2. sizeof 和动态分配内存sizeof 不能用于动态分配内存的大小。例如,对于 new 分配的对象,sizeof 只能返回对象的类型大小,而不是运行时分配的实际大小。

    示例

    int* p = new int;
    std::cout << "Size of int*: " << sizeof(p) << " bytes" << std::endl;
    delete p;
    
  3. sizeof 对于虚拟函数的类: 对于包含虚拟函数的类,sizeof 可能会返回比没有虚拟函数的类更大的值,因为需要为虚拟表指针(vtable pointer)分配额外的内存。

    示例

    class Base {
        virtual void func() {}
    };
    std::cout << "Size of Base: " << sizeof(Base) << " bytes" << std::endl;
    

总结

  • sizeof 运算符用于获取数据类型或对象在内存中占用的字节数。
  • 它在编译时计算,适用于基本数据类型、数组、结构体、类等。
  • 结果可能会受到内存对齐和填充的影响,不同的平台和编译器可能会有不同的结果。
  • 对于动态分配的对象,sizeof 只能返回对象类型的大小,而不是分配的实际大小。

4.10 逗号运算符

在 C++ 中,逗号运算符(,)是一个非常少用但有用的运算符。它的主要作用是将多个表达式串联在一起,并依次求值。逗号运算符的基本语法如下:

expression1, expression2

逗号运算符的特点

  1. 依次求值: 使用逗号运算符时,表达式 expression1 会首先被求值,然后是 expression2。整个表达式的值是 expression2 的值。

  2. 返回值: 逗号运算符的返回值是最后一个表达式 expression2 的值。前面的表达式 expression1 的值会被计算,但不会影响整个表达式的值。

  3. 优先级和结合性: 逗号运算符的优先级较低,仅高于赋值运算符。它是左结合的,这意味着从左到右依次求值。

使用场景

逗号运算符主要用于需要在一个语句中执行多个操作的场景,特别是在 for 循环中和宏定义中。

1. 在 for 循环中

逗号运算符常用在 for 循环的初始化和更新部分,以便在同一语句中执行多个操作。

示例

#include <iostream>

int main() {
    int a = 0, b = 0;

    // 使用逗号运算符在 for 循环中执行多个操作
    for (int i = 0; i < 5; ++i, ++a, ++b) {
        std::cout << "i: " << i << ", a: " << a << ", b: " << b << std::endl;
    }

    return 0;
}

在这个例子中,逗号运算符允许在 for 循环的每次迭代中同时更新 ab 变量。

2. 在宏定义中

在宏定义中,逗号运算符可以用来执行多个操作,然后返回最后一个操作的结果。这对于某些复杂的宏定义很有用。

示例

#include <iostream>

#define MULTIPLY_AND_ADD(x, y) ( \
    { int temp = (x) * (y); temp + 10; } \
)

int main() {
    int result = MULTIPLY_AND_ADD(3, 4);
    std::cout << "Result: " << result << std::endl; // 输出 Result: 22
    return 0;
}

在这个宏定义中,逗号运算符用于在宏内部执行多个操作,然后返回结果。

注意事项

  1. 避免滥用: 虽然逗号运算符很方便,但不应滥用它,因为过多地使用逗号运算符可能会使代码变得难以阅读和理解。

  2. 与其他运算符的结合: 逗号运算符的优先级较低,它会在其他更高优先级的运算符之后进行求值。因此,确保理解逗号运算符的优先级,以避免意外的行为。

  3. 影响代码可读性: 使用逗号运算符可以使某些代码更加简洁,但可能会影响代码的可读性。在使用时要考虑代码的清晰性和可维护性。

总结

  • 逗号运算符 , 允许在一个表达式中依次执行多个操作,并返回最后一个操作的结果。
  • 它常用于 for 循环的初始化和更新部分,也可以用于宏定义中。
  • 使用逗号运算符时需要注意代码的可读性和理解其优先级。

4.11 类型转换

在 C++ 中,类型转换(type conversion)是将一个数据类型的值转换为另一个数据类型的过程。C++ 提供了几种类型转换的方法,包括隐式类型转换、显式类型转换(C 风格的类型转换和 C++ 风格的类型转换)。

隐式类型转换(Implicit Type Conversion)

隐式类型转换,又称为自动类型转换,是由编译器自动进行的。编译器根据上下文自动将一种数据类型的值转换为另一种数据类型,以满足操作的要求。

示例

#include <iostream>

int main() {
    int i = 10;
    double d = i; // int 转换为 double
    std::cout << "Double value: " << d << std::endl;
    return 0;
}

在这个例子中,int 类型的变量 i 被自动转换为 double 类型。

显式类型转换(Explicit Type Conversion)

显式类型转换要求程序员明确地指定转换操作。在 C++ 中,显式类型转换有两种主要形式:C 风格的类型转换和 C++ 风格的类型转换。

1. C 风格的类型转换

C 风格的类型转换使用括号来进行转换。这种方法类似于 C 语言中的类型转换,但其安全性较低,因为它可能会进行不安全的转换。

示例

#include <iostream>

int main() {
    double d = 9.57;
    int i = (int)d; // C 风格转换
    std::cout << "Integer value: " << i << std::endl;
    return 0;
}

这里,(int)ddouble 类型的 d 显式转换为 int 类型。

2. C++ 风格的类型转换

C++ 提供了四种类型转换运算符,用于更安全和明确的类型转换:

  1. static_cast: 用于进行静态类型转换,通常用于已知的转换类型,如基本数据类型之间的转换、类层次结构中的上行和下行转换。

    示例

    #include <iostream>
    
    int main() {
        double d = 9.57;
        int i = static_cast<int>(d); // 使用 static_cast 进行转换
        std::cout << "Integer value: " << i << std::endl;
        return 0;
    }
    
  2. dynamic_cast: 用于进行运行时类型转换,通常用于类层次结构中的多态转换(需要有虚函数)。它能够检查转换是否有效,如果无效,返回 nullptr

    示例

    #include <iostream>
    
    class Base {
    public:
        virtual ~Base() {}
    };
    
    class Derived : public Base {};
    
    int main() {
        Base* b = new Derived;
        Derived* d = dynamic_cast<Derived*>(b); // 使用 dynamic_cast 进行转换
        if (d) {
            std::cout << "Successful dynamic_cast" << std::endl;
        } else {
            std::cout << "Failed dynamic_cast" << std::endl;
        }
        delete b;
        return 0;
    }
    
  3. const_cast: 用于修改对象的常量属性。可以将 constvolatile 属性移除或添加。

    示例

    #include <iostream>
    
    void print(int* ptr) {
        *ptr = 100; // 修改指针指向的值
    }
    
    int main() {
        const int x = 42;
        int* p = const_cast<int*>(&x); // 使用 const_cast 移除 const 属性
        print(p);
        std::cout << "Modified value: " << x << std::endl;
        return 0;
    }
    
  4. reinterpret_cast: 用于进行低级别的类型转换,通常用于不同类型之间的转换,如将指针转换为整数等。这种转换不检查类型安全,使用时要小心。

    示例

    #include <iostream>
    
    int main() {
        int i = 65;
        char* p = reinterpret_cast<char*>(&i); // 使用 reinterpret_cast 进行转换
        std::cout << "Character value: " << *p << std::endl;
        return 0;
    }
    

类型转换的选择

  1. 选择 static_cast: 当你知道转换是安全的,并且不涉及多态时,使用 static_cast。它适用于基本数据类型之间的转换、非多态类层次结构中的转换等。

  2. 选择 dynamic_cast: 当你需要在多态类层次结构中进行安全的转换,并且需要在运行时检查转换的有效性时,使用 dynamic_cast

  3. 选择 const_cast: 当你需要添加或移除对象的 constvolatile 属性时,使用 const_cast

  4. 选择 reinterpret_cast: 当你需要进行低级别的类型转换,且理解可能的风险时,使用 reinterpret_cast

总结

  • 隐式类型转换 是由编译器自动进行的转换。
  • C 风格的类型转换 使用括号进行转换,但不够安全。
  • C++ 风格的类型转换 提供了四种运算符:static_castdynamic_castconst_castreinterpret_cast,用于不同场景下的类型转换,提供了更明确和安全的转换方式。

理解不同类型转换的用法和选择合适的转换方式是编写健壮 C++ 代码的关键。

4.12 运算符优先级表

在 C++ 中,运算符优先级决定了在表达式中运算符的求值顺序。理解运算符的优先级有助于正确解析复杂的表达式,并避免逻辑错误。下面是 C++ 中常用运算符的优先级表及其结合性:

1. 括号与成员访问运算符

  • ():函数调用
  • []:数组下标
  • .:成员访问(通过对象)
  • ->:成员访问(通过指针)
  • :::作用域解析运算符
  • typeid:类型识别运算符
  • const_cast, dynamic_cast, reinterpret_cast, static_cast:类型转换运算符
  • .->[] 是成员访问运算符和函数调用的优先级最高

2. 一元运算符

  • +-:正负号
  • ++--:前置递增/递减
  • !:逻辑非
  • ~:按位取反
  • *:解引用
  • &:取地址
  • sizeof:大小运算符
  • alignof:对齐运算符
  • noexcept:异常说明运算符
  • dynamic_caststatic_castconst_castreinterpret_cast:类型转换

3. 算术运算符

  • */%:乘法、除法、取模

4. 加法运算符

  • +-:加法、减法

5. 移位运算符

  • <<>>:左移、右移

6. 关系运算符

  • <<=:小于、小于等于
  • >>=:大于、大于等于

7. 相等运算符

  • ==!=:等于、不等于

8. 按位与运算符

  • &:按位与

9. 按位异或运算符

  • ^:按位异或

10. 按位或运算符

  • |:按位或

11. 逻辑与运算符

  • &&:逻辑与

12. 逻辑或运算符

  • ||:逻辑或

13. 条件运算符

  • ?::条件(三目)运算符

14. 赋值运算符

  • =:赋值
  • +=-=*=/=%=<<=>>=&=^=|=:复合赋值运算符

15. 逗号运算符

  • ,:逗号运算符

运算符优先级与结合性

运算符的优先级决定了它在表达式中的求值顺序,而结合性决定了具有相同优先级的运算符的求值顺序。

  • 优先级高 的运算符先被求值。例如,乘法 * 和除法 / 的优先级高于加法 + 和减法 -
  • 结合性 决定了运算符的求值顺序:
    • 左结合:从左到右,例如,加法 + 和减法 - 是左结合的。
    • 右结合:从右到左,例如,赋值运算符 = 和条件运算符 ?: 是右结合的。

优先级表示例

以下是 C++ 运算符优先级从高到低的简化表:

优先级运算符描述
1() [] . -> :: typeid const_cast dynamic_cast reinterpret_cast static_cast函数调用、成员访问、类型转换等
2+ - ++ -- ! ~ * & sizeof alignof noexcept一元运算符
3* / %乘法、除法、取模
4+ -加法、减法
5<< >>移位运算符
6< <= > >=关系运算符
7== !=相等运算符
8&按位与运算符
9^按位异或运算符
10|按位或运算符
11&&逻辑与运算符
12||逻辑或运算符
13?:条件运算符
14= += -= *= /= %= <<= >>= &= ^= |=赋值运算符、复合赋值运算符
15,逗号运算符

总结

  • 运算符优先级决定了表达式中运算符的求值顺序。
  • 运算符的结合性决定了具有相同优先级的运算符的求值顺序。
  • 理解运算符优先级和结合性有助于正确地编写和调试复杂的表达式。