Skip to content

Latest commit

 

History

History
1507 lines (1078 loc) · 77.9 KB

01.md

File metadata and controls

1507 lines (1078 loc) · 77.9 KB

一、理解语言特性

在本章中,您将深入学习各种语言特性来控制代码中的流程。

写 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 & R奥尔曼

克尼根和里奇(K&R)写了第一本,也是最有影响力的关于 C 语言的书(丹尼斯·里奇是 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  
        }

奥尔曼风格是微软的典型风格。

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

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

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

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

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

撰写陈述

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

语句以分号结束。除此之外,很少有关于如何格式化语句的规则。您甚至可以单独使用分号,这称为空语句。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. 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;

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

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

这三个都是合法的 C++,但是风格上第一个更好,因为更明显:变量是整数,叫做i,赋值 1。第三个看起来很混乱;它看起来像是一个函数的声明,实际上它是在声明一个变量。

第四章将涵盖类,你自己的自定义类型。自定义类型可能被定义为具有默认值,这意味着您可能决定在使用自定义类型的变量之前不初始化它。然而,这将导致较差的性能,因为编译器将用默认值初始化变量,随后您的代码将赋值,导致赋值被执行两次。

使用常数和文字

每种类型都有一个文字表示。整数是没有小数点的数字,如果是有符号的整数,文字也可以用加号或减号来表示符号。同样,实数可以有包含小数点的文字值,甚至可以使用包含指数的科学(或工程)格式。在代码中指定文字时,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表示以十六进制(7 为基数)显示。如果您希望看到前缀被打印,那么您可以使用流操纵器std::showbase(更多细节将在第 5 章使用标准库容器中给出)。

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);

在这段代码中,一个名为sqrtOf2的全局常数被声明,并使用std::sqrt函数赋值。因为这个常数是在函数外声明的,所以它对文件是全局的,可以在整个文件中使用。

这种方法的问题是预处理器做了一个简单的替换。对于用const声明的常量,C++ 编译器将执行类型检查,以确保常量被正确使用。

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

    int values[5];

这在堆栈上声明了一个由五个整数组成的数组,这些项通过values数组变量来访问。这里的5是一个常量表达式。当您在堆栈上声明一个数组时,您必须向编译器提供一个常量表达式,这样它就知道要分配多少内存,这意味着在编译时必须知道数组的大小。(您可以分配一个只有在运行时才知道大小的数组,但这需要动态内存分配,在第 2 章中解释了如何使用内存、数组和指针。)在 C++ 中,可以声明一个常量来做以下事情:

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

在代码的其他地方,当您访问values数组时,您可以使用size常量来确保您不会访问超过数组末尾的项目。由于size变量只在一个地方声明,如果您需要在稍后阶段更改数组的大小,您只有一个地方可以进行更改。const关键字也可用于指针和引用(参见第 2 章处理内存、数组和指针)以及对象(参见第 4 章);通常,你会看到它用在函数的参数上(见第三章使用函数)。这用于让编译器帮助确保指针、引用和对象按照您的意图得到适当的使用。

使用常量表达式

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是对编译器的指示,检查函数的使用情况,看它能否在编译时确定参数。如果是这种情况,编译器可以评估返回值,并比在运行时调用函数更有效地生成代码。如果编译器在编译时无法确定参数,该函数将被调用为正常。标有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 
    }

声明指针

由于我们讨论的是变量的使用,所以解释用于定义指针和数组的语法是值得的,因为存在一些潜在的陷阱。第 2 章使用内存、数组和指针对此进行了更详细的介绍,因此我们将只介绍语法,以便您熟悉它。

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

Don't worry if you find pointers confusing at this point. Chapter 2, 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_data或者utilities::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外部链接,即该名称在定义变量以外的文件中可见。version变量在print.cpp源文件中使用。在该文件中,使用的范围解析运算符(::)没有名称空间名称,因此指示变量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变量增加515值。

使用运算符

运算符用于根据一个或多个操作数计算值。下表以相等的优先级对所有运算符进行分组,并列出它们的关联性。表中的值越高,运算符在表达式中的执行优先级就越高。如果表达式中有几个运算符,编译器将在低优先级运算符之前执行高优先级运算符。如果表达式包含同等优先级的运算符,则编译器将使用结合律来决定操作数是与其左边的运算符还是右边的运算符分组。

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 :无关联性 | :: | | 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++ 自带广泛的内置运算符;大多数是算术或逻辑运算符,这将在本节中介绍。内存操作符将包含在第 2 章、*使用内存、数组和指针、*以及 第 4 章中的对象相关操作符中。

算术运算符

算术运算符+-/*%除了除法和模数运算符之外,几乎不需要其他解释。所有这些运算符都作用于整数和实数类型,除了%,它只能用于整数类型。如果您混合了这些类型(比如,将一个整数加到一个浮点数上),那么编译器将执行自动转换。除法运算符/的行为与您对浮点变量的预期一样:它产生两个操作数的除法结果。当你在两个整数之间执行除法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的纵横比,而它应该是1.3333(或4 : 3)。为了确保执行浮点除法,而不是整数除法,可以将被除数或除数转换为浮点数。

递增和递减运算符

这些运算符有两个版本,前缀和后缀。顾名思义,前缀表示运算符放在操作数的左边(例如++ i),后缀运算符放在右边(i++ )。++ 运算符将增加操作数,--运算符将减少操作数。前缀运算符表示“在操作后返回数值*,后缀运算符表示“在操作前返回数值因此,下面的代码将增加一个变量,并使用它来分配另一个变量:*

    a = ++ b;

这里,使用前缀运算符,因此变量b增加,并且变量ab增加后被赋值。另一种表达方式是:

    a = (b = b + 1);

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

    a = b++ ;

这意味着变量b增加,但是变量ab增加之前被赋值。另一种表达方式是:

    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,因此它们被视为没有符号位。

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

    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,则按位“或”运算符|将返回值 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";

您可以使用&&(与)和||(或)运算符组合两个逻辑表达式。带有&&运算符的表达式只有在两个操作数都为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,则总的逻辑表达式将是true,而不考虑右操作数的值(这里,y < z)。所以测试第二个表达式没有意义。相应地,在&&表达式中,如果第一个操作数是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

按位移位运算符

按位移位运算符将左操作数整数中的位按指定方向移位右操作数中给定的指定位数。向左移动一位会将数字乘以 2,向右移动一位会除以 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已经乘以 2 8 ,或者 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 个字节)。

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

还可以对整数执行按位赋值操作:左移位(<<=)、右移位(>>=)、按位“与”(&=)、按位“或”(|=)和按位“异或”(^=)。通常只有将这些应用于无符号整数才有意义。所以,乘以 8 可以通过这两条线来实现:

    i *= 8; 
    i <<= 3;

控制执行流程

C++ 提供了许多测试值和循环代码的方法。

使用条件语句

最常用的条件语句是if。最简单的形式是,if语句在一对括号中取一个逻辑表达式,紧接着是语句,如果条件为true,则执行该语句:

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

也可以使用else语句捕捉条件为false的场合:

    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,并使用第一个表达式。请注意,整个运算符周围有一对括号。原因是流<<运算符重载,您希望编译器选择接受字符串的版本,这是运算符返回的类型,而不是表达式(number == 1)的类型bool

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

    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,如果是,则为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语句意味着脱离这个代码块,在循环语句whilefor中也是如此。还有其他方法可以突破switch。A case可以调用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;

您可以提供一个或多个循环语句,对于多个语句,您应该使用大括号提供一个代码块。循环表达式可以满足循环的目的,在这种情况下,您可能不希望执行循环语句;这里,您使用了 null 语句,;表示什么也不做

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

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

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

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

循环表达式可以是您喜欢的任何表达式,但通常它会增加或减少一个值。您不必将循环变量值更改 1;例如,您可以使用i -= 5作为循环表达式,在每个循环上将变量减少 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++ 标准库包含用于容器类的模板。这些类包含对象的集合,并以标准方式提供对这些项的访问。标准方法是使用迭代器对象迭代集合。关于如何做到这一点的更多细节将在 第 5 章使用标准库容器中给出;语法要求理解指针和迭代器,所以我们在这里不涉及它们。基于范围的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循环中,循环语句总是被调用至少一次。

跳跃的

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++ 语言特性

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

启动 Visual C++ 并单击“文件”菜单,然后单击“新建”,最后单击“文件”...选项来获取“新建文件”对话框。在左侧窗格中,单击 Visual C++,在中间窗格中,单击 C++ 文件(。cpp),然后单击打开按钮。在你做任何其他事情之前,保存这个文件。使用 Visual C++ 控制台(一个命令行,有 Visual C++ 环境),导航到Beginning_C++ 文件夹,创建一个名为Chapter_02的新文件夹。现在,在 Visual C++ 中,在“文件”菜单上,单击“将源 1.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函数,传递给你需要的字符的索引。(第 5 章使用标准库容器,将给出更多关于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. 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++ 中循环和分支代码的基本管道,以及内置运算符的工作原理。最后,您将所有这些放在一个简单的应用中,允许您在命令行执行简单的计算。

在下一章中,您将学习如何使用内存、数组和指针。**