Skip to content

Latest commit

 

History

History
1507 lines (1078 loc) · 79.8 KB

File metadata and controls

1507 lines (1078 loc) · 79.8 KB

二、了解语言特性

在上一章中,您安装了 C++ 编译器并开发了一个简单的应用。 您还探索了 C++ 项目的基本结构以及如何管理它们。 在本章中,您将更深入地学习语言,并学习控制代码流的各种语言功能。

编写 C++

在格式化和编写代码时,C++ 是一种非常灵活的语言。 它也是一种强类型语言,这意味着有声明变量类型的规则,您可以通过让编译器帮助您编写更好的代码来利用这些规则。 在本节中,我们将介绍如何格式化 C++ 代码以及声明变量和确定变量作用域的规则。

使用空格

除了字符串文字之外,您可以自由使用空格(空格、制表符、换行符),并且可以根据自己的喜好使用任意多或少的空格。 C++ 语句用分号分隔,因此在下面的代码中有三个语句,它们将编译和运行:

    int i = 4; 
    i = i / 2; 
    std::cout << "The result is" << i << std::endl;

整个代码可以写成如下所示:

    int i=4;i=i/2; std::cout<<"The result is "<<i<<std::endl;

在某些情况下需要空格(例如,声明变量时,类型和变量名之间必须有空格),但约定是尽可能明智地使代码可读。 虽然从语言角度讲,将所有语句放在一行(如 JavaScript)是完全正确的,但这会使代码几乎完全不可读。

If you are interested in some of the more creative ways of making code unreadable, have a look at the entries for the annual International Obfuscated C Code Contest (http://www.ioccc.org/). As the progenitor of C++, many of the lessons in C shown at IOCCC apply to C++ code too.

请记住,如果您编写的代码是可行的,那么它可能会被使用几十年,这意味着您可能需要在编写代码数年后才能重新使用该代码,这意味着其他人也会支持您的代码。 使您的代码可读不仅是对其他开发人员的礼貌,而且不可读的代码总是可能被替换的目标。

格式化代码

不可避免的是,无论您为谁编写代码,都将决定您如何格式化代码。 例如,如果您使用某种形式的预处理来提取代码和定义来为代码创建文档,这有时是有意义的。 在很多情况下,强加给你的风格是别人的个人喜好。

Visual C++ allows you to place XML comments in your code. To do this you use a three--slash comment (///) and then compile the source file with the /doc switch. This creates an intermediate XML file called an xdc file with a <doc> root element and containing all the three--slash comments. The Visual C++ documentation defines standard XML tags (for example, <param>, <returns> to document the parameters and return value of a function). The intermediate file is compiled to the final document XML file with the xdcmake utility.

在 C++ 中有两种广泛的风格:K&RAllman

Kernighan 和 Ritchie(K&R)写了第一本也是最有影响力的关于 C 语言的书(Dennis Ritchie 是 C 语言的作者)。 K&R 样式用于描述该书中使用的格式样式。 通常,K&R 将代码块的左大括号放在最后一条语句的同一行。 如果您的代码有嵌套语句(通常是嵌套的),那么这种风格可能会让人有点困惑:

    if (/* some test */) { 
        // the test is true  
        if (/* some other test */) { 
            // second test is true  
        } else { 
            // second test is false    
        } 
    } else { 
        // the test is false  
    }

此样式通常用于 Unix(和类 Unix)代码。

Allman 样式(以开发人员 Eric Allman 命名)将左大括号放在新行上,因此嵌套示例如下所示:

        if (/* some test */)  
        { 
            // the test is true  
            if (/* some other test */)  
            { 
                // second test is true   
            }  
            else  
            { 
                // second test is false     
            } 
        }  
        else  
        { 
           // the test is false  
        }

微软通常使用 Allman 风格。

请记住,您的代码不太可能出现在纸上,因此 K&R 更紧凑的事实不会拯救任何树。 如果你可以选择,你应该选择最具可读性的风格;对于这本书,这位作者的决定是,奥尔曼更具可读性。

如果您有多个嵌套块,缩进可以让您了解代码驻留在哪个块中。 然而,评论可能会有所帮助。 特别是,如果代码块包含大量代码,注释代码块的原因通常很有帮助。 例如,在if语句中,将测试结果放入代码块中会很有帮助,这样您就可以知道该块中的变量值是什么。 在测试的结束支撑上加一条注释也很有用:

    if (x < 0)  
    { 
       // x < 0 
       /* lots of code */ 
    }  // if (x < 0) 

    else  
    { 
       // x >= 0 
       /* lots of code */ 
    }  // if (x < 0)

如果您将测试放在右大括号上作为注释,这意味着您有一个可以用来查找导致代码块的测试的搜索词。 前面的几行使注释变得多余,但是当您的代码块包含数十行代码,并且具有多层嵌套时,这样的注释可能非常有用。

编写报表

语句可以是变量的声明、计算结果为值的表达式,也可以是类型的定义。 语句也可以是影响代码执行流的控制结构。

语句以分号结束。 除此之外,关于如何格式化语句几乎没有什么规则。 您甚至可以单独使用分号,这称为 NULL 语句。 NULL 语句不执行任何操作,因此使用过多的分号通常是有益的。

使用表达式

表达式是一系列运算符和操作数(变量或文字),它们产生一些值。 请考虑以下事项:

    int i; 
    i = 6 * 7;

右侧的6 * 7是一个表达式,赋值(从左侧的i到右侧的分号)是一个语句。

每个表达式都是左值右值。 您最有可能在错误描述中看到这些关键字。 实际上,左值是指某个内存位置表达式。 赋值左侧的项目必须为左值。 但是,左值可以出现在赋值的左侧或右侧。 所有变量都是左值。 右值是一个临时项,其存在时间不超过使用它的表达式;它将有一个值,但不能被赋值,因此它只能存在于赋值的右侧。 文字是右值。 下面显示了一个左值和右值的简单示例:

    int i; 
    i = 6 * 7;

在第二行中,i是左值,表达式6 * 7产生右值(42)。 以下代码将不会编译,因为左侧有一个右值:

    6 * 7 = i;

一般说来,当您在后面附加分号时,表达式就变成了语句。 例如,以下两个语句都是:

    42;
    std::sqrt(2);

第一行是右值42,但是因为它是临时的,所以没有效果。 C++ 编译器将对其进行优化。 第二行调用标准库函数来计算2的平方根。 同样,结果是一个右值,并且没有使用该值,因此编译器将对其进行优化。 但是,它说明可以在不使用其返回值的情况下调用函数。 虽然std::sqrt并非如此,但许多函数除了它们的返回值之外,还有其他持久的影响。 实际上,函数的全部意义通常是做一些事情,返回值通常仅用于指示函数是否成功;开发人员通常假设函数将成功,而忽略返回值。

使用逗号运算符

运算符将在本章后面介绍;不过,在这里介绍逗号运算符很有用。 您可以将由逗号分隔的一系列表达式作为单个语句。 例如,以下代码在 C++ 中是合法的:

    int a = 9;
    int b = 4;
    int c;
    c = a + 8, b + 1;

作者打算键入c = a + 8 / b + 1;:,他们按了逗号而不是/。 其目的是将c赋值为 9+2+1 或 12。此代码将编译并运行,变量c将赋值为 17(a + 8)。 原因是逗号将赋值的右侧分隔为两个表达式a + 8b + 1,并使用第一个表达式的值来赋值c。 在本章的后面部分,我们将了解运算符的优先顺序。 但是,在这里值得一提的是,逗号的优先级最低,+的优先级高于=,因此语句按照加法的顺序执行:赋值,然后是逗号运算符(结果是b + 1被丢弃)。

您可以使用圆括号对表达式进行分组,以更改优先顺序。 例如,输入错误的代码可能如下所示:

    c = (a + 8, b + 1);

该语句的结果是:变量c被赋值为 5(或b + 1)。 原因是,使用逗号运算符时,表达式是从左到右执行的,因此该组表达式的值是最紧凑的。 例如,在某些情况下,在for循环的初始化或循环表达式中,您会发现逗号运算符很有用,但正如您在这里看到的那样,即使有意使用,逗号运算符也会生成难以阅读的代码。

使用类型和变量

类型将在下一章中详细介绍,但在此提供基本信息会很有用。 C++ 是一种强类型语言,这意味着您必须声明所使用的变量类型。 这样做的原因是,编译器需要知道要为变量分配多少内存,它可以通过变量的类型来确定这一点。 此外,如果变量尚未显式初始化,编译器需要知道如何初始化该变量,并且要执行此初始化,编译器需要知道变量的类型。

C++ 11 provides the auto keyword, which relaxes this concept of strong typing, and it will be covered in the next chapter. However, the type checking of the compiler is so important that you should use type checking as much as possible.

C++ 变量可以在代码中的任何位置声明,只要它们是在使用之前声明的。 声明变量的决定如何使用它(这称为变量的作用域)。 通常,最好在最严格的范围内尽可能靠近您要使用的位置来声明变量。 这可以防止名称冲突*,在这种情况下,您必须添加附加信息来消除两个或更多变量的歧义。*

你可以,,也应该,给你的变量起描述性的名称。 这将使您的代码更具可读性,更易于理解。 C++ 名称必须以字母字符或下划线开头。 它们可以包含除空格以外的字母数字字符,但可以包含下划线。 因此,以下是有效名称:

    numberOfCustomers 
    NumberOfCustomers 
    number_of_customers

C++ 名称区分大小写,前2,048字符很重要。 变量名可以以下划线开头,但不能使用两个下划线,也不能使用后跟大写字母的下划线(这些是由 C++ 保留的)。 C++ 还保留了关键字(例如,whileif),显然您不能将类型名用作变量名,既不能使用内置类型名(intlong等),也不能使用您自己的自定义类型。

您可以在语句中声明变量,并以分号结尾。 声明变量的基本语法是指定变量的类型,然后指定名称,还可以指定变量的任何初始化。

在使用内置类型之前,必须先对其进行初始化:

    int i; 
    i++ ;           // C4700 uninitialized local variable 'i' used 
    std::cout << i;

基本上有三种初始化变量的方法。 您可以赋值,也可以调用类型构造函数(类的构造函数将在第 6 章中定义),或者可以使用函数语法初始化变量:

    int i = 1; 
    int j = int(2); 
    int k(3);

这三个都是合法的 C++,但在风格上第一个更好,因为它更明显:变量是一个整数,它被称为i,并且被赋值为 1。第三个看起来很混乱;它看起来像是一个函数的声明,而它实际上是声明一个变量。 下一章将展示使用初始化列表语法赋值的变体。 您想要这样做的原因将留到那一章。

第 6 章将介绍类,即您自己的自定义类型。 可以将自定义类型定义为具有默认值,这意味着您可以决定在使用自定义类型的变量之前不对其进行初始化。 但是,这将导致较差的性能,因为编译器将使用默认值初始化变量,随后您的代码将赋值,从而导致执行两次赋值。

使用常量和文字

每种类型都有一个文字表示形式。 整数将是不带小数点的数字,如果它是带符号的整数,则文字也可以使用加号或减号来表示符号。 同样,实数可以具有包含小数点的文字值,您甚至可以使用包含指数的科学(或工程)格式。 在代码中指定文字时,C++ 有各种规则可供使用,这些规则将在下一章中介绍。 下面显示了一些文字的示例:

    int pos = +1; 
    int neg = -1; 
    double micro = 1e-6; 
    double unit = 1.; 
    std::string name = "Richard";

请注意,对于unit变量,编译器知道文字是实数,因为该值有一个小数点。 对于整数,您可以在代码中提供十六进制文字,方法是在数字前面加上0x,这样0x100就是十进制的256。 默认情况下,输出流将以 10 为基数打印数值;但是,您可以在输出流中插入操纵器,以告知它使用不同的数字基数。 默认行为是std::dec,这意味着数字应该显示为基数 10,std::oct表示显示为八进制(基数为 8),std::hex表示显示为十六进制(基数16)。 如果您希望打印前缀,则可以使用流操纵器std::showbase(更多详细信息将在第 8 章使用标准库容器中给出)。

C++ 定义了一些文字。 对于逻辑类型bool,有truefalse常量,其中false是零,true是 1。还有nullptr常量,同样是零,它被用作任何指针类型的无效值。

定义常量

在某些情况下,您可能希望提供可在整个代码中使用的常量值。 例如,您可能决定为π声明一个常量。 不应允许更改此值,因为它会更改代码中的基础逻辑。 这意味着您应该将变量标记为常量。 执行此操作时,编译器将检查变量的使用情况,如果在更改变量值的代码中使用该变量,则编译器将发出错误:

    const double pi = 3.1415; 
    double radius = 5.0; 
    double circumference = 2 * pi * radius;

在这种情况下,符号pi被声明为常量,因此不能更改。 如果您随后决定更改常量,编译器将发出错误:

    // add more precision, generates error C3892 
    pi += 0.00009265359;

一旦您声明了一个常量,您就可以放心,编译器将确保它保持不变。 您可以为常量指定一个表达式,如下所示:

    #include <cmath> 
    const double sqrtOf2 = std::sqrt(2);

在这段代码中,使用std::sqrt函数声明了一个名为sqrtOf2的全局常量,并为其赋值。 由于该常量是在函数外部声明的,因此它对文件是全局的,并且可以在整个文件中使用。

在上一章中,您了解到声明常量的一种方法是使用#define符号。 这种方法的问题在于,预处理器只需进行简单的替换。 对于用const声明的常量,C++ 编译器将执行类型检查,以确保正确使用常量。

还可以使用const声明将用作常量表达式的常量。 例如,您可以使用方括号语法声明数组(更多详细信息将在第 4 章使用内存、数组和指针中给出):

    int values[5];

这在堆栈上声明了一个由五个整数组成的数组,这些项通过values数组变量访问。 这里的5是一个常量表达式。 当您在堆栈上声明一个数组时,您必须为编译器提供一个常量表达式,以便它知道要分配多少内存,这意味着在编译时必须知道数组的大小。 (您可以分配大小仅在运行时已知的数组,但这需要动态内存分配,如第 4 章使用内存、数组和指针中所述。 )在 C++ 中,您可以声明一个常量来执行以下操作:

    const int size = 5;  
    int values[size];

在代码的其他地方,当您访问values数组时,可以使用size常量来确保不会访问超出数组末尾的项。 由于size变量仅在一个位置声明,因此如果您需要在稍后阶段更改数组的大小,则只有一个位置可以进行此更改。

const关键字还可以用于指针和引用(参见第 4 章使用内存、数组和指针)和对象(参见第 6 章CLASS);通常,您会看到它用于函数的参数(参见第 5 章使用函数)。 这用于让编译器帮助确保正确使用指针、引用和对象,就像您希望的那样。

使用常量表达式

C++ 11 引入了一个称为constexpr的关键字。 这应用于表达式,并指示应在编译类型而不是在运行时计算该表达式:

    constexpr double pi = 3.1415; 
    constexpr double twopi = 2 * pi;

这类似于初始化使用const关键字声明的常量。 但是,也可以将关键字constexpr应用于返回可在编译时计算的值的函数,因此这允许编译器优化代码:

    constexpr int triang(int i) 
    { 
       return (i == 0) ? 0 : triang(i - 1) + i;
    }

在本例中,函数triang递归计算三角数。 代码使用条件运算符。 在圆括号中,测试函数参数以查看它是否为零,如果是,则函数返回零,实际上结束递归并将函数返回给原始调用方。 如果该参数不为零,则返回值为该参数的和,用该参数调用的triang的返回值将递减。

当在代码中使用文字调用此函数时,可以在编译时对其求值。 constexpr指示编译器检查函数的使用情况,以查看它是否可以在编译时确定参数。 在这种情况下,与在运行时调用函数相比,编译器可以更有效地计算返回值并生成代码。 如果编译器在编译时无法确定参数,则该函数将被调用为Normal。 标记有constexpr关键字的函数只能有一个表达式(因此在triang函数中使用条件运算符?:)。

使用枚举

提供常量的最后一种方法是使用enum变量。 实际上,enum是一组命名常量,这意味着您可以将enum用作函数的参数。 例如:

    enum suits {clubs, diamonds, hearts, spades};

它定义了一个名为suits的枚举,其中包含一副纸牌中花色的命名值。 枚举是整数类型,默认情况下编译器将采用int,但您可以通过在声明中指定整数类型来更改此类型。 因为纸牌花色只有四个可能的值,所以使用int(通常是4字节)是浪费内存,我们可以使用char(单字节):

    enum suits : char {clubs, diamonds, hearts, spades};

当您使用枚举值时,可以只使用名称;但是,通常使用枚举的名称来确定其范围,从而使代码更具可读性:

    suits card1 = diamonds; 
    suits card2 = suits::diamonds;

这两种形式都是允许的,但后一种形式更明确地表明该值是从枚举中获取的。 要强制开发人员指定范围,可以应用关键字class

    enum class suits : char {clubs, diamonds, hearts, spades};

使用此定义和前面的代码,可以编译声明card2的行,但不编译声明card1的行。 使用作用域为enum时,编译器将枚举视为新类型,并且没有从新类型到整数变量的内置转换。 例如:

    suits card = suits::diamonds; 
    char c = card + 10; // errors C2784 and C2676

enum类型基于char,但是当您将suits变量定义为作用域(使用class)时,第二行将不会编译。 如果枚举被定义为未限定作用域(没有class),则枚举值和char之间存在内置转换。

默认情况下,编译器将为第一个枚举数赋值 0,然后为后续枚举数递增该值。 因此,suits::diamonds的值为 1,因为它是suits中的第二个值。 您可以自己赋值:

    enum ports {ftp=21, ssh, telnet, smtp=25, http=80};

在这种情况下,ports::ftp的值为 21,ports::ssh的值为 22(递增 21),ports::telnet为 22,ports::smtp为 25,ports::http为 80。

Often the point of enumerations is to provide named symbols within your code and their values are unimportant. Does it matter what value is assigned to suits::hearts? The intention is usually to ensure that it is different from the other values. In other cases, the values are important because they are a way to provide values to other functions.

枚举在switch语句中很有用(请参见后面),因为命名的值比只使用整数更清楚。 您还可以将枚举用作函数的参数,从而限制通过该参数传递的值:

    void stack(suits card) 
    { 
        // we know that card is only one of four values 
    }

声明指针

由于我们讨论的是变量的使用,因此有必要解释一下用于定义指针和数组的语法,因为其中存在一些潜在的缺陷。 第 4 章使用内存、数组和指针更详细地介绍了这一点,因此我们将只介绍语法,以便您熟悉它。

在 C++ 中,您将使用类型化指针访问内存。 该类型指示保存在指向的存储器中的数据的类型。 因此,如果指针是(4字节)整数指针,它将指向可用作整数的四个字节。 如果整数指针递增,则它将指向下一个可用作整数的四个字节。

Don't worry if you find pointers confusing at this point. Chapter 4, Working with Memory, Arrays, and Pointers, will explain this in more detail. The purpose of introducing pointers at this time is to make you aware of the syntax.

在 C++ 中,指针使用*符号声明,您可以使用&运算符访问内存地址:

    int *p; 
    int i = 42; 
    p = &i;

第一行声明了一个变量p,该变量将用于保存整数的内存地址。 第二行声明一个整数并为其赋值。 第三行将一个值赋给指针p,使其成为刚刚声明的整数变量的地址。 需要强调的是,p的值不是42;它将是存储42值的存储器地址。

请注意,声明的变量名上有*。 这是普遍的惯例。 原因是,如果在一条语句中声明多个变量,则*仅适用于立即变量。 因此,例如:

    int* p1, p2;

最初,这看起来像是声明了两个整数指针。 但是,这一行没有这样做;它只声明了一个指向名为p1的整数的指针。 第二个变量是一个称为p2的整数。 前面的行相当于下面的内容:

    int *p1;  
    int p2;

如果希望在一条语句中声明两个整数,则应执行以下操作:

    int *p1, *p2;

使用名称空间

命名空间为您提供了一种模块化代码的机制。 命名空间允许您用唯一的名称标记类型、函数和变量,这样,使用作用域解析操作符,您就可以给出一个完全限定名称。 这样做的好处是,您可以确切地知道将调用哪个项。 缺点是使用完全限定名实际上是为重载函数关闭了 C++ 的参数相关查找机制,编译器将根据传递给函数的参数选择最合适的函数。

定义名称空间很简单:使用namespace关键字和为其指定的名称来修饰类型、函数和全局变量。 在下面的示例中,在utilities命名空间中定义了两个函数:

    namespace utilities 
    { 
        bool poll_data() 
        { 
            // code that returns a bool 
        } 
        int get_data() 
        { 
            // code that returns an integer 
        } 
    }

Do not use semicolon after the closing bracket.

现在,当您使用这些符号时,需要用名称空间限定名称:

    if (utilities::poll_data()) 
    { 
        int i = utilities::get_data(); 
        // use i here... 
    }

名称空间声明可能只声明函数,在这种情况下,实际的函数必须在其他地方定义,并且您需要使用限定名称:

    namespace utilities 
    { 
        // declare the functions 
        bool poll_data(); 
        int get_data(); 
    } 

    //define the functions 
    bool utilities::poll_data() 
    { 
        // code that returns a bool 
    } 

    int utilities::get_data() 
    { 
       // code that returns an integer 
    }

命名空间的一个用途是对代码进行版本化。 代码的第一个版本可能有一个副作用,该副作用不在您的功能规范中,从技术上讲是一个错误,但有些调用者会使用它并依赖它。 当您更新代码以修复错误时,您可以决定允许调用者选择使用旧版本,这样他们的代码就不会中断。 您可以使用命名空间执行此操作:

    namespace utilities 
    { 
        bool poll_data(); 
        int get_data(); 

        namespace V2 
        { 
            bool poll_data(); 
            int get_data(); 
            int new_feature(); 
        } 
    }

现在,需要特定版本的调用者可以调用完全限定名称,例如,调用者可以使用utilities::V2::poll_data使用较新的版本,使用utilities::poll_data使用较旧的版本。 当特定命名空间中的项调用同一命名空间中的项时,它不必使用限定名称。 因此,如果new_feature函数调用get_data,则会调用utilities::V2::get_data。 重要的是要注意,要声明嵌套的名称空间,您必须手动执行嵌套(如下所示);您不能简单地声明一个名为utilities::V2的名称空间。

编写了前面的示例,以便代码的第一个版本将使用命名空间utilities调用它。 C++ 11 提供了一种称为内联内联命名空间的工具,它允许您定义嵌套命名空间,但允许编译器在执行参数相关查找时将这些项视为父命名空间中的项:

    namespace utilities 
    { 
        inline namespace V1 
        { 
            bool poll_data(); 
            int get_data(); 
        } 

        namespace V2 
        { 
            bool poll_data(); 
            int get_data(); 
            int new_feature(); 
        } 
    }

现在要调用get_data的第一个版本,可以使用utilities::get_datautilities::V1::get_data

完全限定名可能会使代码难以阅读,尤其是在代码只使用一个命名空间的情况下。 要在这里提供帮助,您有几个选择。 您可以放置一条using语句来指示在指定名称空间中声明的符号可以在没有完全限定名称的情况下使用:

    using namespace utilities; 
    int i = get_data(); 
    int j = V2::get_data();

您仍然可以使用完全限定名称,但此语句允许您放宽要求。 请注意,嵌套的命名空间是命名空间的成员,因此前面的using语句意味着您可以使用utilities::V2::get_dataV2::get_data调用get_data的第二个版本。 如果您使用非限定名称,则意味着您将调用utilities::get_data

一个命名空间可以包含许多项,您可能决定只使用其中的几个项来放宽对完全限定名的使用。 为此,请使用using并给出项目的名称:

    using std::cout; 
    using std::endl; 
    cout << "Hello, World!" << endl;

这段代码说明,每当使用cout时,它都会引用std::cout。 您可以在函数中使用using,也可以将其作为文件作用域,并使意图对文件而言是全局的。

您不必在一个位置声明命名空间,您可以在多个文件中声明它。 以下内容可能位于与先前utilities声明不同的文件中:

    namespace utilities 
    { 
        namespace V2 
        { 
            void print_data(); 
        } 
    }

print_data函数仍然是utilities::V2命名空间的一部分。

您还可以将#include放入命名空间,在这种情况下,头文件中声明的项现在将成为命名空间的一部分。 前缀为c的标准库头文件(例如,cmathcstdlibctime)通过在std名称空间中包含适当的 C 头来访问 C 运行时函数。

命名空间的最大优点是能够使用可能常见但对其他不知道命名空间名称的代码隐藏的名称来定义项。 命名空间意味着您的代码仍然可以通过完全限定名使用这些项。 但是,这只有在使用唯一的名称空间名称时才有效,很可能名称空间名称越长,它就越可能是唯一的。 Java 开发人员通常使用 URI 命名他们的类,您也可以决定做同样的事情:

    namespace com_packtpub_richard_grimes 
    { 
        int get_data(); 
    }

问题是完全限定名变得相当长:

    int i = com_packtpub_richard_grimes::get_data();

您可以使用别名解决此问题:

    namespace packtRG = com_packtpub_richard_grimes; 
    int i = packtRG::get_data();

C++ 允许您定义没有名称的名称空间,即匿名名称空间。 如前所述,命名空间允许您防止在多个文件中定义的代码之间发生名称冲突。 如果您只想在一个文件中使用这样的名称,您可以定义一个唯一的名称空间名称。 但是,如果您必须对多个文件执行此操作,这可能会变得单调乏味。 没有名称的命名空间具有特殊含义,即它具有内部链接,即项目只能在当前翻译单元、当前文件中使用,而不能在任何其他文件中使用。

未在命名空间中声明的代码将是global命名空间的成员。 您可以在没有命名空间名称的情况下调用代码,但您可能希望使用没有命名空间名称的作用域解析操作符显式指示该项位于global命名空间中:

    int version = 42; 

    void print_version() 
    { 
        std::cout << "Version = " << ::version << std::endl; 
    }

C++ 变量作用域

正如您在上一章中看到的,编译器会将源文件编译为称为翻译单元的单个项目。 编译器将确定您声明的对象和变量以及您定义的类型和函数,一旦声明,您就可以在声明范围内的后续代码中使用这些内容中的任何一个。 在最广泛的情况下,您可以通过在将由项目中的所有源文件使用的头文件中声明项来在全局范围内声明项。 如果您不使用名称空间,那么使用这样的全局变量将它们命名为全局名称空间的一部分通常是明智的做法:

    // in version.h 
    extern int version; 

    // in version.cpp 
    #include "version.h"  
    version = 17; 

    // print.cpp 
    #include "version.h" 
    void print_version() 
    { 
        std::cout << "Version = " << ::version << std::endl; 
    }

这段代码包含两个源文件(version.cppprint.cpp)的 C++ 以及这两个源文件都包含的头文件(version.h)。 头文件声明全局变量version,这两个源文件都可以使用;它声明了变量,但没有定义它。 实际变量是在version.cpp中定义和初始化的;正是在这里,编译器将为变量分配内存。 头中声明中使用的extern关键字向编译器指示version具有外部链接,即该名称在定义变量的文件以外的其他文件中可见。 在print.cpp源文件中使用了version变量。 在此文件中,使用作用域解析操作符(::)时没有命名空间名称,因此表示变量version在全局命名空间中。

您还可以声明仅在当前翻译单元中使用的项目,方法是在使用前在源文件中声明它们(通常在文件顶部)。 这会产生一定程度的模块化,并允许您对其他源文件中的代码隐藏实现细节。 例如:

    // in print.h 
    void usage(); 

    // print.cpp 
    #include "version.h" 
    std::string app_name = "My Utility"; 
    void print_version() 
    { 
       std::cout << "Version = " << ::version << std::endl; 
    } 

    void usage() 
    { 
       std::cout << app_name << " "; 
       print_version(); 
    }

print.h头包含文件print.cpp中代码的接口。 只有在头文件中声明的函数才能被其他源文件调用。 调用者不需要知道usage函数的实现,正如您在这里看到的那样,它是通过调用名为print_version的函数实现的,该函数仅对print.cpp中的代码可用。 变量app_name是在文件范围内声明的,因此只有print.cpp中的代码才能访问它。

如果另一个源文件声明了一个文件作用域的变量,称为app_name,并且也是一个std::string,则该文件将被编译,但链接器在尝试链接目标文件时会出现错误。 原因是链接器将看到在两个位置定义的相同变量,并且它不知道使用哪个变量。

函数还定义了一个作用域;函数中定义的变量只能通过该名称访问。 函数的参数也作为变量包含在函数中,因此在声明其他变量时,必须使用不同的名称。 如果参数未标记为const,则可以在函数中更改该参数的值。

只要在使用之前声明变量,就可以在函数内的任何位置声明变量。 大括号({})用于定义代码块,它们还定义局部作用域;如果在代码块内声明变量,则只能在那里使用它。 这意味着您可以在代码块外声明同名变量,编译器将使用最接近其访问范围的变量。

在结束本部分之前,有必要提到 C++存储类的一个方面。 在函数中声明的变量意味着编译器将在为该函数创建的堆栈帧上为该变量分配内存。 当函数完成时,堆栈帧将被拆卸并回收内存。 这意味着,函数返回后,所有局部变量中的值都会丢失;再次调用该函数时,会重新创建该变量并再次初始化。

C++ 提供static关键字来更改此行为。 关键字static表示在程序启动时分配变量,就像在全局作用域中声明的变量一样。 将static应用于函数中声明的变量意味着该变量具有内部链接,也就是说,编译器将对该变量的访问限制为该函数:

    int inc(int i) 
    { 
        static int value; 
        value += i; 
        return value; 
    } 

    int main() 
    { 
        std::cout << inc(10) << std::endl; 
        std::cout << inc(5) << std::endl; 
    }

默认情况下,编译器会将静态变量初始化为0,但您可以提供初始化值,该值将在第一次分配变量时使用。 当该程序启动时,在调用main函数之前,value变量将被初始化为0。 第一次调用inc函数时,value变量递增到 10,由该函数返回并打印到控制台。 当inc函数返回时,将保留value变量,以便当再次调用inc函数时,将value变量递增5至值15

使用运算符

运算符用于从一个或多个操作数计算值。 下表将具有相等优先级的所有运算符分组,并列出它们的结合性。 表中的位置越高,运算符在表达式中的执行优先级就越高。 如果表达式中有多个运算符,编译器将在执行较低优先级运算符之前执行较高优先级运算符。 如果表达式包含优先级相等的运算符,则编译器将使用结合性来决定操作数是将运算符分组在其左侧还是右侧。

There are some ambiguities in this table. A pair of parentheses can mean a function call or a cast and in the table these are listed as function() and cast(); in your code you will simply use (). The + and - symbols are either used to indicate sign (unary plus and unary minus, given in the table as +x and -x), or addition and subtraction (given in the table as + and -). The & symbol means either "take the address of" (listed in the table as &x) or bitwise AND (listed in the table as &). Finally, the postfix increment and decrement operators (listed in the table as x++ and x--) have a higher precedence than the prefix equivalents (listed as ++ x and --x).

| 优先级和结合性 | 运算符 | | Колибри1Колибри1:无关联性 | :: | | 2:从左到右的关联性 | .-> [] function() {} x++ x-- typeid const_cast dynamic_cast reinterpret_cast static_cast | | 3:从右到左的关联性 | sizeof ++ x --x ~ ! -x +x &x * new delete cast() | | 4:从左到右的关联性 | .*->* | | 5:从左到右的关联性 | * / % | | 6:从左到右的关联性 | + - | | 7:从左到右的关联性 | << >> | | 8:从左到右的关联性 | < > <= >= | | 9:从左到右的关联性 | == != | | 10:从左到右关联性 | & | | 11:从左到右关联性 | ^ | | 12:从左到右关联性 | &#124; | | 13:从左到右关联性 | && | | 14:从左到右关联性 | &#124;&#124; | | 15:从右到左的关联性 | ? : | | 16:从右到左的关联性 | = *= /= %= += -= <<= >>= &= &#124;= ^= | | 17:从右到左的关联性 | throw | | 18:从左到右的关联性 | , |

例如,看一下下面的代码:

    int a = b + c * d;

这被解释为先执行乘法,然后执行加法。 编写相同代码的一种更清晰的方法是:

    int a = b + (c * d);

原因是*的优先级高于+,所以先执行乘法,然后执行加法:

    int a = b + c + d;

在这种情况下,+运算符具有相同的优先级,高于赋值的优先级。 由于+具有从左到右的关联性,因此该语句的解释如下:

    int a = ((b + c) + d);

也就是说,第一个操作是将bc相加,结果与d相加,该结果用于分配a。 这看起来可能并不重要,但请记住,加法可以在函数调用之间进行(函数调用的优先级高于+):

    int a = b() + c() + d();

这意味着按照bcd的顺序调用这三个函数,然后根据从左到右的关联性对它们的返回值求和。 这可能很重要,因为d可能取决于由其他两个函数更改的全局数据。

如果您通过使用圆括号对表达式进行分组来显式指定优先级,则会使您的代码更具可读性,也更易于理解。 写入b + (c * d)可以立即明确哪个表达式首先执行,而b + c * d意味着您必须知道每个运算符的优先顺序。

内置运算符是重载的,也就是说,无论操作数使用哪种内置类型,都使用相同的语法。 操作数必须是同一类型;如果使用不同的类型,编译器将执行一些默认转换,但在其他情况下(特别是在操作不同大小的类型时),您必须执行强制转换以明确表示您的意思。 下一章将更详细地解释这一点。

探索内置运算符

C++ 附带了广泛的内置运算符;大多数是算术或逻辑运算符,本节将介绍这些运算符。 强制转换操作符将在下一章中介绍;内存操作符将在第 4 章使用内存、数组和指针中介绍,与对象相关的操作符将在第 6 章中介绍。

算术运算符

算术运算符+-/*%可能只需要除法和模运算符的解释。 除了只能与整数类型一起使用的%之外,所有这些运算符都作用于整数和实数类型。 如果您混合了这两种类型(例如,将整数加到浮点数上),则编译器将执行自动转换,如下一章所述。 除法运算符/的行为与您对浮点变量的预期一样:它产生两个操作数的除法结果。 当您在两个整数a / b之间执行除法时,结果是被除数(a)中除数(b)的整数。 除法的余数由模数%得到。 因此,对于任何整数b(不是零),可以说,整数a可以表示如下:

    (a / b) * b + (a % b)

请注意,模运算符只能用于整数。 如果想要得到浮点除法的剩余部分,请使用标准函数std:;remainder

使用整数除法时要小心,因为小数部分会被丢弃。 如果需要小数部分,则可能需要将数字显式转换为实数。 例如:

    int height = 480; 
    int width = 640; 
    float aspect_ratio = width / height;

这会在本应为1.3333(或4 : 3)的情况下提供1的纵横比。 要确保执行浮点除法,而不是整数除法,您可以将被除数或除数任意(或两者)转换为浮点数,如下一章所述。

增量和减量运算符

这些运算符有两个版本,前缀和后缀。 顾名思义,前缀意味着运算符放在操作数的左边(例如,++ i),后缀运算符放在右边(i++ )。 ++ 运算符将递增操作数,--运算符将递减它。 前缀运算符的意思是“在运算之后返回值*”,后缀运算符的意思是“在运算之前返回值”。 因此,下面的代码将递增一个变量,并使用它为另一个变量赋值:*

    a = ++ b;

这里,使用前缀运算符,因此变量b递增,并将变量a赋给b递增后的值。 表达这一点的另一种方式是:

    a = (b = b + 1);

下面的代码使用后缀运算符赋值:

    a = b++ ;

这意味着变量b递增,但变量a被赋给b递增之前的值。 表达这一点的另一种方式是:

    int t; 
    a = (t = b, b = b + 1, t);

Note that this statement uses the comma operator, so a is assigned to the temporary variable t in the right-most expression.

递增和递减运算符可以应用于整数和浮点数。 运算符还可以应用于指针,因为它们有特殊的含义。 当您递增指针变量时,它意味着将指针递增运算符所指向的类型的大小。

位运算符

整数可视为一系列位01。 与其他操作数中相同位置的位相比,按位运算符对这些位起作用。 有符号整数使用一位来表示符号,但按位运算符作用于整数中的每一位,因此通常只对无符号整数使用位运算符。 在下面,所有类型都被标记为unsigned,因此它们被视为没有符号位。

&运算符是按位 AND 的,这意味着左操作数中的每一位都与右操作数中相同位置的位进行比较。 如果两者都为 1,则同一位置的结果位将为 1;否则,结果位为 0:

    unsigned int a = 0x0a0a; // this is the binary 0000101000001010 
    unsigned int b = 0x00ff; // this is the binary 0000000000001111 
    unsigned int c = a & b;  // this is the binary 0000000000001010 
    std::cout << std::hex << std::showbase << c << std::endl;

在本例中,将按位&0x00ff一起使用与提供掩码以屏蔽除最低字节之外的所有字节具有相同的效果。

如果同一位置中的任一位或两位均为 1,则按位 OR 运算符|将返回值 1;仅当两位均为 0 时,才返回值 0:

    unsigned int a = 0x0a0a; // this is the binary 0000101000001010 
    unsigned int b = 0x00ff; // this is the binary 0000000000001111 
    unsigned int c = a & b;  // this is the binary 0000101000001111 
    std::cout << std::hex << std::showbase << c << std::endl;

&运算符的一个用途是查看是否设置了特定的位(或特定的位集合):

    unsigned int flags = 0x0a0a; // 0000101000001010 
    unsigned int test = 0x00ff;  // 0000000000001111 

    // 0000101000001111 is (flags & test) 
    if ((flags & test) == flags)  
    { 
        // code for when all the flags bits are set in test 
    } 
    if ((flags & test) != 0) 
    { 
        // code for when some or all the flag bits are set in test  
    }

flags变量具有我们需要的位,而test变量是我们正在检查的值。 值(flags & test)将仅包含也在flags中设置的前test个变量中的那些位。 因此,如果结果为非零,则意味着test中至少有一位也被设置在flags中;如果结果与第二个flags中的变量完全相同,则flags中的所有位都被设置在test中。

异或运算符^用于测试比特何时不同;如果操作数中的比特不同,则结果比特为1,如果它们相同,则结果比特为0。 异或可用于翻转特定位:

    int value = 0xf1; 
    int flags = 0x02; 
    int result = value ^ flags; // 0xf3 
    std::cout << std::hex << result << std::endl;

最后的按位运算符是按位补码~。 此运算符应用于单个整数操作数,并返回一个值,其中每一位都是操作数中相应位的补码;因此,如果操作数位为 1,则结果中的位为 0;如果操作数中的位为 0,则结果中的位为 1。请注意,将检查所有位,因此需要注意整数的大小。

布尔运算符

==运算符测试两个值是否完全相同。 如果测试两个整数,则测试是显而易见的;例如,如果x是 2,y是 3,那么x == y显然是false。 然而,即使你这么认为,两个实数也可能不一样:

    double x = 1.000001 * 1000000000000; 
    double y = 1000001000000; 
    if (x == y) std::cout << "numbers are the same";

double类型是一个 8 字节的浮点类型,但对于这里使用的精度来说,这是不够的;存储在x变量中的值是1000000999999.9999(小数点后四位)。

!=运算符测试是否有两个值不为真。 运算符><测试两个值以查看左侧操作数是否大于或小于右侧操作数,>=运算符测试左侧操作数是否大于或等于右侧操作数,<=运算符测试左侧操作数是否小于或等于右侧操作数。 这些运算符可以在if语句中使用,类似于前面示例中使用==的方式。 使用运算符的表达式返回bool类型的值,因此您可以使用它们为布尔变量赋值:

    int x = 10; 
    int y = 11; 
    bool b = (x > y); 
    if (b) std::cout << "numbers same"; 
    else   std::cout << "numbers not same";

赋值运算符(=)比大于(>=)运算符具有更高的优先级,但我们使用了圆括号来明确表示,值在用于赋值之前进行了测试。 您可以使用!运算符对逻辑值求反。 因此,使用前面获得的b值,您可以编写以下代码:

    if (!b) std::cout << "numbers not same"; 
    else    std::cout << "numbers same";

可以使用&&(AND)和||(OR)运算符组合两个逻辑表达式。 带有&&运算符的表达式只有在两个操作数都是true时才为真,而带有||运算符的表达式只有在其中一个或两个操作数都是true时才是true

    int x = 10, y = 10, z = 9; 
    if ((x == y) || (y < z)) 
        std::cout << "one or both are true";

这段代码涉及三个测试;第一个测试xy变量是否具有相同的值,第二个测试变量y是否小于z,然后进行一个测试,看看前两个测试中是否有一个或两个都是true

在这样的||表达式中,第一个操作数(x==y)是true,无论右操作数(这里是y < z)的值是什么,总的逻辑表达式都是true。 因此,测试第二个表达式是没有意义的。 相应地,在&&表达式中,如果第一个操作数是false,则整个表达式必须是false,因此不需要测试表达式的右侧部分。 编译器将提供代码来执行此短路

    if ((x != 0) && (0.5 > 1/x))  
    { 
        // reciprocal is less than 0.5 
    }

此代码测试x的倒数是否小于 0.5(或者相反,x大于 2)。 如果x变量的值为 0,则测试1/x是错误的,但在这种情况下,表达式将永远不会执行,因为&&的左操作数是false

按位移位运算符

按位移位运算符按指定方向将左操作数整数中的位移位右操作数中给定的指定位数。 向左移位 1 位将数字乘以 2,向右移位 1 位将除以 2。以下是 2 字节整数的移位:

    unsigned short s1 = 0x0010; 
    unsigned short s2 = s1 << 8; 
    std::cout << std::hex << std::showbase; 
    std::cout << s2 << std::endl; 
    // 0x1000  
    s2 = s2 << 3; 
    std::cout << s2 << std::endl; 
    // 0x8000

在本例中,s1变量设置了第五位(0x0010或 16)。 第二个s2变量有这个值,向左移位 8 位,因此单个位被移位到第 13 位,底部 8 位全部设置为 0(0x10000或 4,096)。 这意味着0x0010已乘以 28,或 256,得到0x1000。 接下来,将该值再左移 3 位,结果为0x8000;设置最高位。

运算符将丢弃所有溢出的位,因此如果您设置了顶部位并将整数左移一位,则该顶部位将被丢弃:

    s2 = s2 << 1; 
    std::cout << s2 << std::endl; 
    // 0

最后向左移位一位将得到值 0。

重要的是要记住,当与流一起使用时,运算符<<意味着插入到流中,而当与整数一起使用时,它意味着位移位

赋值运算符

赋值运算符=在左边赋值(变量),在右边赋值结果(变量或表达式):

    int x = 10; 
    x = x + 10;

第一行声明一个整数并将其初始化为 10。第二行通过在变量上再加上 10 来改变变量,因此现在变量x的值为 20。这是赋值。 C++ 允许您使用简短语法根据变量值更改变量值。 前面的几行可以写成如下:

    int x = 10; 
    x += 10;

这样的递增运算符(和递减运算符)可以应用于整数和浮点类型。 如果运算符应用于指针,则操作数指示指针更改了多少整项地址。 例如,如果int是 4 字节,而您将10加到int指针上,则实际指针值会递增 40(10 乘以 4 字节)。

除了递增(+=)和递减(-=)赋值之外,还可以对乘法(*=)、除法(/=)和余数(%=)赋值。 除了最后一个(%=),所有这些都可以用于浮点类型和整数。 余数赋值只能用于整数。

您还可以对整数执行按位赋值操作:左移位(<<=)、右移位(>>=)、按位 AND(&=)、按位 OR(|=)和按位异或(^=)。 通常只有将这些应用于无符号整数才有意义。 因此,乘以 8 可以由这两行执行:

    i *= 8; 
    i <<= 3;

控制执行流程

C++ 提供了多种方法来测试值和遍历代码。

使用条件语句

最常用的条件语句是if。 在其最简单的形式中,if语句采用一对圆括号中的逻辑表达式,后面紧跟条件为true时执行的语句:

    int i; 
    std::cin >> i; 
    if (i > 10) std::cout << "much too high!" << std::endl;

当条件为false时,还可以使用else语句捕捉情况:

    int i; 
    std::cin >> i; 
    if (i > 10) std::cout << "much too high!" << std::endl; 
    else        std::cout << "within range" << std::endl;

如果要执行多个语句,可以使用大括号({})来定义代码块。

条件是一个逻辑表达式,C++ 将从数值类型转换为bool,其中 0 是false,任何不是 0 的都是true。 如果您不小心,这可能是错误的来源,不仅很难注意到,而且还可能产生意想不到的副作用。 考虑以下代码,它要求从控制台输入,然后测试用户是否输入-1:

    int i; 
    std::cin >> i; 
    if (i == -1) std::cout << "typed -1" << endl; 
    std::cout << "i = " << i << endl;

这是人为设计的,但您可能会要求循环中的值,然后对这些值执行操作,除非用户输入-1,此时循环结束。 如果键入错误,可能会出现以下代码:

    int i; 
    std::cin >> i; 
    if (i = -1) std::cout << "typed -1" << endl; 
    std::cout << "i = " << i << endl;

在这种情况下,使用赋值运算符(=)代替相等运算符(==)。 只有一个字符的区别,但是这段代码仍然是正确的 C++,编译器很乐意编译它。

结果是,无论您在控制台输入什么,变量i都被赋给-1,由于-1 不是零,所以if语句中的条件是true,因此执行该语句的 TRUE 子句。 由于变量被赋值为-1,这可能会进一步改变代码中的逻辑。 避免这个错误的方法是利用赋值时左边必须是左值的要求。 按如下方式执行测试:

    if (-1 == i) std::cout << "typed -1" << endl;

这里,逻辑表达式是(-1 == i),由于==运算符是可交换的(操作数的顺序无关紧要;您会得到相同的结果),这与您在前面的测试中的意图完全相同。 但是,如果您键入了错误的运算符,则会得到以下结果:

    if (-1 = i) std::cout << "typed -1" << endl;

在这种情况下,赋值在左侧有一个右值,这将导致编译器发出错误(在 Visual C++ 中,这是C2106 '=' : left operand must be l-value)。

允许在if语句中声明变量,并且变量的作用域在语句块中。 例如,可以按如下方式调用返回整数的函数:

    if (int i = getValue()) {    
        // i != 0    // can use i here  
    } else {    
        // i == 0    // can use i here  
    }

虽然这是完全合法的 C++,但您没有什么理由要这样做。

在某些情况下,可以使用条件运算符?:代替if语句。 运算符执行?运算符左侧的表达式,如果条件表达式为true,则执行?右侧的表达式。 如果条件表达式为false,则执行:右侧的表达式。 运算符执行的表达式提供条件运算符的返回值。

例如,下面的代码确定两个变量ab中的最大值:

    int max; 
    if (a > b) max = a; 
    else       max = b;

这可以用以下一条语句来表达:

    int max = (a > b) ? a : b;

主要的选择是代码中哪一个是最具可读性的。 显然,如果赋值表达式很大,最好在if语句中将它们分成几行。 但是,在其他语句中使用条件语句很有用。 例如:

    int number;  
    std::cin  >> number; 
    std::cout << "there " 
              << ((number == 1) ? "is " : "are ")  
              << number << " item"            
              << ((number == 1) ? "" : "s") 
              << std::endl;

此代码确定变量number是否为 1,如果是,则在控制台there is 1 item上打印。 这是因为在这两个条件中,如果第一个number变量的值为 1,则测试为true,并使用第一个表达式。 请注意,整个运算符周围有一对圆括号。 原因是流<<运算符是重载的,您希望编译器选择接受字符串的版本,它是运算符返回的类型,而不是bool,它是表达式(number == 1)的类型。

如果条件运算符返回的值是左值,则可以在赋值的左侧使用它。 这意味着您可以编写以下相当奇怪的代码:

    int i = 10, j = 0; 
    ((i < j) ? i : j) = 7; 
    // i is 10, j is 7 

    i = 0, j = 10; 
    ((i < j) ? i : j) = 7; 
    // i is 7, j is 10

条件运算符检查i是否小于j,如果小于j,则为i赋值;否则,为j赋值。 这段代码很简洁,但缺乏可读性。 在这种情况下,使用if语句要好得多。

选择

如果您想测试一个变量是否是几个值中的一个,那么使用多个if语句就会变得很麻烦。 C++ switch语句更好地实现了这一目的。 基本语法如下所示:

    int i; 
    std::cin >> i; 
    switch(i) 
    { 
        case 1:  
            std::cout << "one" << std::endl; 
            break; 
        case 2:  
            std::cout << "two" << std::endl; 
            break; 
        default: 
            std::cout << "other" << std::endl; 
    }

如果所选变量是指定值,则每个case实质上是关于要运行的特定代码的标签。 default子句用于不存在case的值。 您不必有default子句,这意味着您只针对指定的情况进行测试。 default子句可以用于最常见的情况(在这种情况下,案例过滤掉不太可能的值),也可以用于例外的值(在这种情况下,案例处理最可能的值)。

switch语句只能测试整数类型(包括enum),并且只能测试常量。 char类型是整数,这意味着您可以在case项中使用字符,但只能使用单个字符;您不能使用字符串:

    char c; 
    std::cin >> c; 
    switch(c) 
    { 
        case 'a':  
            std::cout << "character a" << std::endl; 
            break; 
        case 'z':   
            std::cout << "character z" << std::endl; 
            break; 
        default: 
            std::cout << "other character" << std::endl; 
    }

break语句指示为case执行的语句的结束。 如果不指定,执行将通过执行*,并且将执行以下case语句,即使它们已为不同的情况指定:*

    switch(i) 
    { 
        case 1:  
            std::cout << "one" << std::endl; 
            // fall thru 
        case 2:  
            std::cout << "less than three" << std::endl; 
            break; 
        case 3:  
            std::cout << "three" << std::endl; 
            break; 
        case 4: 
            break; 
            default: 
            std::cout << "other" << std::endl; 
    }

此代码显示了break语句的重要性。 值 1 将把oneless than three都打印到控制台,因为执行通过到前面的case,即使那个case是针对另一个值的。

对于不同的情况,通常会有不同的代码,因此最常见的情况是用break来结束case。 很容易错误地错过break,这将导致异常行为。 在故意遗漏break语句时记录代码是一种很好的做法,这样您就知道如果遗漏了break,很可能是一个错误。

您可以为每个case提供零条或多条语句。 如果有多条语句,则会针对该特定情况执行所有语句。 如果没有提供语句(如本例中的case 4),则意味着不会执行任何语句,即使是default子句中的语句也不会执行。

break语句表示脱离此代码块,它在 LOOP 语句whilefor中的行为也是这样的。 还有其他方法可以让你突破switchcase可以调用return来完成声明了switch的函数;它可以调用goto来跳转到标签,或者可以调用throw来抛出异常,该异常将被异常处理程序捕获到switch之外,甚至在函数之外。

到目前为止,这些案例都是按数字顺序排列的。 这不是必需的,但它确实使代码更具可读性,显然,如果您希望跳过case语句(如这里的case 1),则应该注意case项的顺序。

如果需要在case处理程序中声明临时变量,则必须使用大括号定义代码块,这将使变量的范围仅局限于该代码块。 当然,您可以在任何case处理程序中使用在switch语句外部声明的任何变量。

由于枚举常量是整数,因此可以在switch语句中测试enum

    enum suits { clubs, diamonds, hearts, spades }; 

    void print_name(suits card) 
    { 
        switch(card) 
        { 
            case suits::clubs: 
                std::cout << "card is a club"; 
                break; 
            default: 
                std::cout << "card is not a club"; 
        } 
    }

虽然这里的enum没有限定作用域(它既不是enum class也不是enum struct),但它不需要在case中指定值的作用域,但它使代码更明显地说明了常量所指的是什么。

环绕 / 绕行 / 打环 / 翻筋斗

大多数程序都需要遍历一些代码。 C++ 提供了几种方法来实现这一点,要么使用索引值迭代,要么测试逻辑条件。

使用迭代进行循环

for语句有两个版本,迭代和基于范围。 后者是在 C++ 11 中引入的。迭代版本的格式如下:

    for (init_expression; condition; loop_expression) 
        loop_statement;

您可以提供一个或多个循环语句,对于多个语句,应该使用大括号提供代码块。 循环的目的可能由 LOOP 表达式实现,在这种情况下,您可能不希望执行 LOOP 语句;在这里,您使用 NULL 语句;,这意味着什么都不做

括号内有三个用分号分隔的表达式。 第一个表达式允许您声明和初始化循环变量。 此变量的作用域为for语句,因此只能在for表达式或后面的循环语句中使用它。 如果需要多个循环变量,可以在此表达式中使用逗号运算符声明它们。

当条件表达式为true时,for语句将循环;因此,如果使用循环变量,则可以使用此表达式检查循环变量的值。 在调用 LOOP 语句之后,在循环结束时调用第三个表达式;在此之后,调用条件表达式以确定循环是否应该继续。 最后一个表达式通常用于更新循环变量的值。 例如:

    for (int i = 0; i < 10; ++ i)   
    { 
        std::cout << i; 
    }

在此代码中,循环变量为i,并将其初始化为零。 接下来,检查条件,由于i将小于 10,因此将执行该语句(将值打印到控制台)。 下一个动作是循环表达式;调用++ i,它递增循环变量i,然后检查条件,依此类推。 由于条件为i < 10,这意味着该循环将运行 10 次,值i介于 0 和 9 之间(因此您将在控制台上看到 0123456789)。

循环表达式可以是您喜欢的任何表达式,但通常它会递增或递减一个值。 您不必将循环变量值更改为 1;例如,您可以使用i -= 5作为循环表达式,在每个循环中将变量减去 5。 循环变量可以是您喜欢的任何类型;它不必是整数,甚至不必是数字(例如,它可以是指针,也可以是中描述的使用标准库容器迭代器对象),并且条件和循环表达式不必使用循环变量。 事实上,您根本不需要声明循环变量!

如果不提供循环条件,则循环将是无限的,除非您在循环中提供检查:

for (int i = 0; ; ++ i)  
{ 
   std::cout << i << std::endl; 
   if (i == 10) break; 
}

这使用了前面在switch语句中介绍的break语句。 它指示执行退出for循环,您也可以使用returngotothrow。 您很少看到使用goto结束的语句;但是,您可能会看到以下内容:

for (;;)  
{ 
   // code 
}

在这种情况下,没有循环变量,没有循环表达式,也没有条件。 这是一个永恒的循环,循环中的代码决定循环何时结束。

for语句中的第三个表达式,循环表达式,可以是您喜欢的任何东西;唯一的属性是它在循环结束时执行。 您可以选择更改此表达式中的另一个变量,甚至可以提供几个用逗号操作符分隔的表达式。 例如,如果您有两个函数,一个名为poll_data的函数在有更多数据可用时返回true,当没有更多数据时返回false,另一个名为get_data的函数返回下一个可用数据项,您可以按如下方式使用for(请记住,这是一个人为的示例,目的是为了说明问题):

for (int i = -1; poll_data(); i = get_data()) 
{ 
   if (i != -1) std::cout << i << std::endl; 
}

poll_data返回false值时,循环将结束。 需要if语句,因为第一次调用循环时尚未调用get_data。 更好的版本如下:

for (; poll_data() ;) 
{ 
   int i = get_data();  
   std::cout << i << std::endl; 
}

在下一节中,请牢记此示例。

还可以在for循环中使用另一个关键字。 在许多情况下,您的for循环将有多行代码,在某个时刻,您可能会决定当前循环已经完成,并且希望开始下一个循环(或者,更具体地说,执行循环表达式,然后测试条件)。 为此,您可以调用continue

for (float divisor = 0.f; divisor < 10.f; ++ divisor)  
{ 
   std::cout << divisor; 
   if (divisor == 0)  
   {  
      std::cout << std::endl; 
      continue; 
   } 
   std::cout << " " << (1 / divisor) << std::endl; 
}

在这段代码中,我们打印数字 0 到 9 的倒数(0.f是一个 4 字节的浮点文字)。 for循环中的第一行打印循环变量,下一行检查变量是否为零。 如果是,则打印新行并继续,即不执行for循环中的最后一行。 原因是最后一行打印的是倒数,将任何数字除以零都是错误的。

C++ 11 引入了另一种使用for循环的方法,该循环旨在与容器一起使用。 C++ 标准库包含容器类的模板。 这些类包含对象集合,并以标准方式提供对这些项的访问。 标准方法是使用迭代器对象遍历集合。 关于如何做到这一点的更多细节将在使用标准库容器中给出;语法要求理解指针和迭代器,所以我们不在这里讨论它们。 基于范围的for循环提供了一种简单的机制来访问容器中的项,而无需显式使用迭代器。

语法很简单:

for (for_declaration : expression) loop_statement;

首先要指出的是,只有两个表达式,它们之间用冒号分隔(:)。 第一个表达式用于声明循环变量,该变量属于要循环访问的集合中的项的类型。 第二个表达式提供对集合的访问。

In C++ terms, the collections that can be used are those that define a begin and end function that gives access to iterators, and also to stack-based arrays (that the compiler knows the size of).

标准库定义了一个名为vector的容器对象。 vector模板是一个包含尖括号(<>)中指定类型的项的类;在下面的代码中,vector以一种 C++ 11 中新的特殊方式初始化,称为列表初始化。 此语法允许您在大括号之间的列表中指定向量的初始值。 下面的代码创建并初始化vector,然后使用迭代for循环打印出所有值:

using namespace std; 
vector<string> beatles = { "John", "Paul", "George", "Ringo" }; 

for (int i = 0; i < beatles.size(); ++ i)  
{ 
   cout << beatles.at(i) << endl; 
}

Here a using statement is used so that the classes vector and string do not have to be used with fully qualified names.

vector类有一个名为size的成员函数(通过.运算符调用,意思是“在此对象上调用此函数”),它返回vector中的项目数。 使用传递项目索引的at函数访问每个项目。 这段代码的一个大问题是它使用随机访问,也就是说,它使用每个项目的索引来访问每个项目。 这是vector的一个属性,但其他标准库容器类型没有随机访问权限。 下面使用基于范围的for

vector<string> beatles = { "John", "Paul", "George", "Ringo" }; 

for (string musician : beatles)  
{ 
   cout << musician << endl; 
}

此语法适用于任何标准容器类型和堆栈上分配的数组:

int birth_years[] = { 1940, 1942, 1943, 1940 }; 

for (int birth_year : birth_years)  
{ 
   cout << birth_year << endl; 
}

在这种情况下,编译器知道数组的大小(因为编译器已经分配了数组),因此它可以确定范围。 基于范围的for循环将遍历容器中的所有项,但与前一个版本一样,您可以使用breakreturnthrowgoto离开for循环,并且可以使用continue语句指示应该执行下一个循环。

条件循环

在上一节中,我们给出了一个人为的示例,其中for循环中的条件轮询数据:

for (; poll_data() ;) 
{ 
   int i = get_data();  
   std::cout << i << std::endl; 
}

在本例中,条件中没有使用循环变量。 这是while条件循环的候选项:

while (poll_data()) 
{ 
   int i = get_data();  
   std::cout << i << std::endl; 
}

该语句将继续循环,直到表达式(本例中为poll_data)的值为false。 与for一样,可以使用breakreturnthrowgoto退出while循环,并且可以使用continue语句指示应该执行下一个循环。

第一次调用while语句时,会在执行循环之前测试条件;在某些情况下,您可能希望循环至少执行一次,然后测试条件(很可能取决于循环中的操作),以查看是否应该重复循环。 执行此操作的方法是使用do-while循环:

int i = 5; 
do 
{ 
   std::cout << i-- << std::endl; 
} while (i > 0);

注意while子句后面的分号。 这是必需的。

此循环将以相反的顺序打印 5 比 1。 原因是循环开始时i被初始化为 5。循环中的语句通过后缀运算符递减变量,这意味着递减之前的值被传递给流。 在循环结束时,while子句测试变量是否大于零。 如果此测试为true,则循环重复。 在将i赋值为 1 的情况下调用循环时,会将值 1 打印到控制台,并将变量减为零,while子句将测试一个为false的表达式,循环将结束。

这两种类型的循环的不同之处在于,在while循环中执行循环之前会测试条件,因此可能不会执行该循环。 在do-while循环中,条件在循环之后调用,这意味着对于do-while循环,LOOP 语句始终至少被调用一次。

跳;跃;跳跃;跳过;跃过;跨越;快速移动;突然移动

C++ 支持跳转,并且在大多数情况下,有更好的方法来分支代码;然而,为了完整性,我们将在这里讨论该机制。 跳转有两个部分:要跳转到的标记语句和goto语句。 标签的命名规则与变量相同;声明时使用冒号作为后缀,并且必须在语句之前。 使用标签的名称调用goto语句:

    int main() 
    { 
        for (int i = 0; i < 10; ++ i) 
        { 
            std::cout << i << std::endl; 
            if (i == 5) goto end; 
        } 

    end:
        std::cout << "end"; 
    }

标签必须与调用goto具有相同的功能。

跳转很少使用,因为它们鼓励您编写非结构化代码。 但是,如果您的例程包含高度嵌套的循环或if语句,那么使用goto跳转来清理代码可能更有意义,可读性也更好。

使用 C++ 语言功能

现在,让我们使用您在本章中学到的功能来编写应用。 本例是一个简单的命令行计算器;您键入一个表达式,如67*,应用将解析输入并执行计算。

启动 Visual C++ 并单击文件菜单,然后单击新建,最后单击新建文件...选项以获得新建文件对话框。 在左侧窗格中,单击 Visual C++,在中间窗格中,单击 C++ 文件(.cpp),然后单击打开按钮。 在执行任何其他操作之前,请保存此文件。 使用 Visual C++ 控制台(具有 Visual C++ 环境的命令行)导航到您在上一章中创建的Beginning_C++ 文件夹,并创建一个名为Chapter_02的新文件夹。 现在,在 Visual C++ 中的文件菜单上,单击将 Source1.cpp 另存为...。 并在另存文件为对话框中找到您刚刚创建的Chapter_02文件夹。 在文件名框中,键入 calc.cpp,然后单击保存按钮。

应用将使用std::coutstd::string;因此,在文件的顶部,添加定义这两个名称的标头,并添加一条using语句,这样您就不必使用完全限定名称:

    #include <iostream> 
    #include <string> 

    using namespace std;

您将通过命令行传递表达式,因此在文件底部添加一个接受命令行参数的main函数:

    int main(int argc, char *argv[]) 
    { 
    }

应用处理arg1 op arg2形式的表达式,其中op是运算符,arg1arg2是参数。 这意味着,当调用应用时,它必须有四个参数;第一个参数是用于启动应用的命令,后三个参数是表达式。 main函数中的第一个代码应确保提供正确数量的参数,因此在此函数的顶部添加一个条件,如下所示:

    if (argc != 4) 
    { 
        usage(); 
        return 1; 
    }

如果使用多于或少于四个参数调用命令,则调用函数usage,然后返回main函数,停止应用。

main函数之前添加usage函数,如下所示:

    void usage() 
    { 
        cout << endl; 
        cout << "calc arg1 op arg2" << endl; 
        cout << "arg1 and arg2 are the arguments" << endl; 
        cout << "op is an operator, one of + - / or *" << endl; 
    }

这里简单地解释了如何使用该命令,并解释了参数。 此时,您可以编译应用了。 由于您使用的是 C++ 标准库,因此编译时需要支持 C++ 异常,因此在命令行中键入以下内容:

C:\Beginning_C++ Chapter_02\cl /EHsc calc.cpp

如果您键入的代码没有任何错误,文件应该会编译。 如果从编译器获得任何错误,请检查源文件以查看代码是否与前面的代码完全相同。 您可能会收到以下错误:

'cl' is not recognized as an internal or external command,  
operable program or batch file.

这意味着控制台未设置为 Visual C++ 环境,因此要么将其关闭,然后通过 Windows 开始菜单启动控制台,要么运行 vcvarsall.bat 批处理文件。 上一章给出了完成这两项任务的步骤。

一旦代码编译完毕,您就可以运行它了。 首先使用正确的参数数(例如,calc 6 * 7)运行它,然后尝试使用错误的参数数(例如,calc 6 * 7 / 3)。 请注意,参数之间的空格很重要:

C:\Beginning_C++ Chapter_02>calc 6 * 7 

C:\Beginning_C++ Chapter_02>calc 6 * 7 / 3 

calc arg1 op arg2 
arg1 and arg2 are the arguments 
op is an operator, one of + - / or *

在第一种情况下,应用不执行任何操作,因此您看到的只是一个空行。 在第二个示例中,代码确定没有足够的参数,因此它将使用信息打印到控制台。

接下来,您需要对参数进行一些简单的解析,以检查用户是否传递了有效值。 在main函数的底部添加以下内容:

    string opArg = argv[2]; 
    if (opArg.length() > 1) 
    { 
        cout << endl << "operator should be a single character" << endl; 
        usage(); 
        return 1; 
    }

第一行使用第三个命令行参数初始化 C++ std::string对象,该参数应该是表达式中的运算符。 这个简单的示例只允许操作符使用单个字符,因此后续行检查以确保操作符是单个字符。 C++ std::string类有一个名为length的成员函数,它返回字符串中的字符数。

argv[2]参数的长度至少为一个字符(没有长度的参数不会被视为命令行参数!),因此我们必须检查用户键入的操作符是否超过一个字符。

接下来,您需要测试以确保该参数是允许的受限集之一,如果用户键入另一个操作符,则打印错误并停止处理。 在main函数的底部添加以下内容:

    char op = opArg.at(0); 
    if (op == 44 || op == 46 || op < 42 || op > 47) 
    { 
        cout << endl << "operator not recognized" << endl; 
        usage(); 
        return 1; 
    }

测试将在一个字符上进行,因此您需要从string对象中提取该字符。 此代码使用at函数,该函数将传递所需字符的索引。 (第 8 章使用标准库容器将提供有关std::string类成员的更多详细信息。)。 下一行检查该字符是否不受支持。 代码依赖于我们支持的字符的下列值:

| 字符 | | | + | 42 | | * | 43 | | - | 45 | | / | 47 |

正如您所看到的,如果字符小于42或大于47,它将是不正确的,但是在4247之间,还有两个我们也想拒绝的字符:,(44)和.(46)。 这就是为什么我们有前面的条件:“如果字符小于 42 或大于47,或者它是4446,则拒绝它。”

char数据类型是整数,这就是测试使用整数文字的原因。 您可以使用字符文字,因此以下更改同样有效:

 if (op == ',' || op == '.' || op < '+' || op > '/') 
    { 
        cout << endl << "operator not recognized" << endl; 
        usage(); 
        return 1; 
    }

你应该使用你认为最易读的任何一个。 由于检查一个字符是否大于另一个字符意义不大,本书将使用前者。

此时,您可以编译代码并对其进行测试。 首先尝试使用多于一个字符的运算符(例如,**),并确认您收到的消息是运算符应该是单个字符。 其次,使用不是可识别操作符的字符进行测试;尝试除+*-/之外的任何字符,但也值得尝试.,

请记住,命令提示符对某些符号有特殊操作,如“&”和“|”,甚至在调用代码之前,命令提示符可能会通过解析命令行给您一个错误。

接下来要做的是将参数转换为代码可以使用的形式。 命令行参数以字符串数组的形式传递给程序;但是,我们将其中一些参数解释为浮点数(实际上是双精度浮点数)。 C 运行时提供了一个名为atof的函数,该函数可通过 C++ 标准库获得(在本例中,<iostream>包括包含<cmath>的文件,其中声明了atof)。

It is a bit counter-intuitive to get access to a math function such as atof through including a file associated with stream input and output. If this makes you uneasy, you can add a line after the include lines to include the <cmath> file. As mentioned in the previous chapter, the C++ Standard Library headers have been written to ensure that a header file is only included once, so including <cmath> twice has no ill effect. This was not done in the preceding code, because it was argued that atof is a string function and the code includes the <string> header and, indeed, <cmath> is included via the files the <string> header includes.

将以下行添加到main函数的底部。 前两行将第二个和第四个参数(记住,C++ 数组是从零开始编制索引的)转换为double值。 最后一行声明一个变量来保存结果:

    double arg1 = atof(argv[1]); 
    double arg2 = atof(argv[3]); 
    double result = 0;

现在我们需要确定传递了哪个操作符并执行请求的操作。 我们将使用switch语句完成此操作。 我们知道op变量将是有效的,因此我们不必提供default子句来捕获我们没有测试过的值。 在函数的底部添加一条switch语句:

    double arg1 = atof(argv[1]); 
    double arg2 = atof(argv[3]); 
    double result = 0; 

    switch(op) 
    { 
    }

前三种情况+-*很简单:

    switch (op) 
    { 
 case '+': result = arg1 + arg2; break; case '-': result = arg1 - arg2; break; case '*': result = arg1 * arg2; break; 
    }

同样,由于char是整数,您可以在switch语句中使用它,但 C++ 允许您检查字符值。 在这种情况下,使用字符而不是数字会使代码更具可读性。

switch之后,添加最终代码以打印结果:

    cout << endl; 
    cout << arg1 << " " << op << " " << arg2; 
    cout << " = " << result << endl;

现在可以编译代码并使用涉及+-*的计算对其进行测试。

除法是个问题,因为被零除是无效的。 要测试这一点,请将以下行添加到switch的底部:

 case '/': result = arg1 / arg2; break;

编译并运行代码,将零作为最后一个参数:

C:\Beginning_C++ Chapter_02>calc 1 / 0 
1 / 0 = inf

代码成功运行,并打印出表达式,但它显示结果是inf的奇数值。 这是怎么回事?

除以零将result赋给NAN的值,该值是<math.h>中定义的常量(通过<cmath>包含),意思是“不是一个数字”。 cout对象的插入操作符的double重载测试数字是否具有有效值,如果数字具有值NAN,则打印字符串 inf。 在我们的应用中,我们可以测试零因子,并将传递零的用户操作视为错误。 因此,请将代码更改为如下所示:

    case '/': 
 if (arg2 == 0) { cout << endl << "divide by zero!" << endl; return 1; } else { 
        result = arg1 / arg2; 
 } 
    break;

现在,当用户传递零作为除数时,您将得到一条divide by zero!消息。

现在您可以编译完整的示例并对其进行测试。 该应用支持使用+-*/运算符的浮点运算,并将处理除以零的情况。

简略的 / 概括的 / 简易判罪的 / 简易的

在本章中,您学习了如何格式化代码,以及如何标识表达式和语句。 您已经了解了如何标识变量的作用域,以及如何将函数和变量的集合分组到命名空间中,以便防止名称冲突。 您还学习了 C++ 中循环和分支代码的基本知识,以及内置操作符的工作方式。 最后,将所有这些放在一个简单的应用中,该应用允许您在命令行执行简单的计算。

在下一章中,您将了解 C++ 类型以及如何将值从一种类型转换为另一种类型。