Skip to content

Latest commit

 

History

History
1322 lines (941 loc) · 65.1 KB

File metadata and controls

1322 lines (941 loc) · 65.1 KB

三、内置数据类型

概观

本章介绍了 C++ 提供的内置数据类型,包括它们的基本属性以及在向量和数组中的使用。我们将从识别和描述核心数据类型的选择开始,然后分别在向量和数组等容器中进行实现。然后,在我们将创建的注册应用中实现它们之前,我们将查看它们的生命周期和范围,作为本章的最后练习。

简介

在前一章中,我们研究了控制流,学习了许多通过应用操纵执行流的方法。在这一章中,我们将仔细研究如何使用不同的数据类型来表示这些信息;具体来说,就是 C++ 提供的内置数据类型。

我们以前用过一些。例如,我们知道整数代表数字,字符串代表单词和字符,但是让我们更详细地讨论一下。C++ 提供的核心类型集是我们稍后将创建的任何和所有用户定义类型的构建块,因此很好地理解我们可用的类型非常重要。我们将从查看它们存储的数据、它们是如何分配的以及它们的大小开始。然后,我们可以继续研究类型修饰符——允许我们修改其属性的关键字。将提供一个图表供将来参考。

接下来,我们将继续关注创建这些类型的数组。到目前为止,我们的大多数变量(如果不是全部的话)都是单一的——也就是说,一个数字或一个字符串。除了单独存储这些内容,我们还可以将这些内容的多个集合存储在一起。这些被称为数组,是一个重要的特性来理解和使用。

在阵列之后,我们将关注存储寿命或范围。这是变量归属的概念,以及变量可访问的时间。这是一个基本的主题,所以强有力的理解是关键,并将引导我们进入我们的最后一个主题——类和结构。这些对象封装了我们的数据和功能,是面向对象编程 ( OOP )的核心。这些将在第九章对象 - 面向 原则中详细介绍,因此我们在此的介绍仅构成简要介绍。

为了完成这一章,我们将通过创建一个真实的注册应用来测试我们所学的内容。这将是我们迄今为止创建的最大的应用,将允许用户注册一个系统,并通过一个标识查找现有记录。这不仅将利用本章中涵盖的概念,还将利用前面的所有概念。

当本章完成时,您不仅会对我们所使用的各种类型的属性有更深入的了解,还会了解它们的生命周期以及它们在我们的应用中是如何出现/消失的。

数据类型

到目前为止,我们已经在整本书中看到,我们将数据存储在变量中——用户的姓名、年龄或食品价格。鉴于这些是不同类型的数据——字母、数字等等,我们将它们存储在不同的变量类型中。我们现在要看的就是这些类型,因为为要存储的数据使用正确的变量类型很重要。

类型修饰符

然而,在我们了解基本数据类型本身之前,让我们先快速了解一下类型修饰符。最初在第 1 章你的第一个 C++ 应用中提到,当我们查看关键字时,类型修饰符允许我们更改整数类型的属性。我们可以使用以下修改器:

  • signed:关键字signed指定我们的变量可以同时保存正值和负值。这增加了最大的下限值,因为我们现在可以为负,但这样做会减少最大的上限值。这是因为变量可以容纳的值的范围不会改变;它只是移动,这意味着现在有一半的范围是负数。

  • unsigned:关键字unsigned指定我们的变量应该只保存正值。这增加了变量的上限,但降低了下限,因为上限为 0。

  • long:long关键字确保我们的变量至少有一个int那么大;通常,这将是 4 字节。在某些情况下,这将增加可以存储的值的范围。

  • long long(c++ 11):c++ 11 中增加的long long关键字,保证了我们的变量会比long更大;通常,这将是 8 字节。在大多数情况下,这将增加可存储的值的范围。

  • short: The short keyword ensures that our variable has the smallest memory footprint it can, whilst ensuring a size less than long; typically, this will be 4 bytes.

    注意

    数据类型的确切大小取决于一些因素,例如您正在使用的体系结构和设置的编译器标志,尽管典型的大小将很快在参考图表中显示。值得注意的是,C++ 标准并不保证类型的绝对大小,而是保证它们必须能够存储的最小范围。这意味着不同平台之间的修改类型也可能不同。

内置类型

现在我们已经有了关于修饰符的入门知识,我们可以看看 C++ 为我们提供的基本数据类型的核心集合。这些类型大部分时间都会服务于你的需求,你不需要做什么特别的事情来使用它们;它们是语言的一部分。这些内置类型如下:

  • bool:bool类型存储一个true(非零)或false (0)值,大小为一个字节。
  • int:int类型用于存储整数,通常大小为四个字节。
  • char:char类型用于存储单个字符。它以整数的形式存储,并根据使用的字符集(通常是 ASCII)解析为一个字符。这种数据类型的大小为一个字节。
  • float:float类型代表单精度浮点数,大小通常为 4 字节。
  • double:double类型代表双精度浮点数,大小通常为 8 字节。
  • void:void类型是表示空值的特殊类型。您不能创建void类型的对象。然而,它可以被指针和函数用来表示一个空值——例如,一个不指向任何东西的void指针,或者一个不返回任何东西的void函数。
  • wide character:wchar_t类型用于存储宽字符(Unicode UTF-16)。wchar_t的大小是特定于编译器的,尽管 C++ 11 引入了固定大小类型char16_tchar32_t

参考表

下面是一个由 C++ 提供的基本数据类型表,其中包含一些类型修饰符:

Figure 3.1: Table of C++ data types and their sizes

图 3.1:c++ 数据类型及其大小表

注意

这些类型的范围由它们的大小决定,不依赖于数据类型。此外,上表中的值仅适用于 Microsoft Visual C++。gcc 和 clang 中基本类型的大小不同于 visual studio 中的大小。

练习 13:声明数据类型

对于第一章的练习,我们将声明一些不同的变量,有和没有类型修饰符,并使用sizeof操作符打印出它们的大小。以下是完成练习的步骤:

注意

如果您使用的编译器与本书中的不同,如果您的大小不同,请不要惊慌。请记住,它们可以在不同的平台和体系结构上进行不同的调整。这个练习的代码文件可以在这里找到:https://packt.live/2rdD8Em

  1. 我们将从使用上表中的三种类型来定义一些变量开始:

        int myInt = 1;
        bool myBool = false;
        char myChar = 'a';
  2. sizeof运算符会以字节为单位给出变量的大小。对于之前定义的每个变量,添加一个output语句,该语句将打印其大小:

        std::cout << "The size of an int is " << sizeof(myInt) << ".\n";
        std::cout << "The size of a bool is " << sizeof(myBool) << ".\n";
        std::cout << "The size of a char is " << sizeof(myChar) << ".\n";
  3. 完整的代码如下:

    #include<iostream>
    using namespace std;
    int main()
    {
        int myInt = 1;
        bool myBool = false;
        char myChar = 'a';
        std::cout << "The size of an int is " << sizeof(myInt) << ".\n";
        std::cout << "The size of a bool is " << sizeof(myBool) << ".\n";
        std::cout << "The size of a char is " << sizeof(myChar) << ".\n";
        return 0;
    }
  4. 运行这段代码。您应该会看到我们的变量的大小被打印出来:

Figure 3.2: Using sizeof to determine the size of our variables

图 3.2:使用 sizeof 来确定变量的大小

通过使用sizeof,我们可以快速看到我们变量的大小。同样,根据您使用的平台和编译器配置,您的里程可能会有所不同。继续使用前面参考图表中列出的其他一些数据类型,并查看您的大小是否与给定的大小匹配。了解关于我们的数据类型的信息是很好的,这样我们就可以确保在给定的场景中使用最合适的数据类型。

容器

现在我们已经了解了 C++ 提供的一些内置数据类型,让我们来看几个容器——允许我们将多个元素存储在一起的对象。它们有多种形状和大小,具体取决于您存储的数据以及您希望如何存储。在这一章的开头,我们将集中讨论两个基本容器——数组和向量。并非所有语言都提供这些类型;例如,Python 两者都没有,而是提供列表。然而,对于 C++,我们被宠坏了,无法选择。标准库包含了无数的集合来满足我们的需求,但是这两个是我们在本章中要关注的。

阵列

数组是对象的容器,因此我们可以存储许多数组,而不是在变量中存储单个值。这些都在内存中一个挨着一个,所以我们通过一个变量和一个索引来访问它们。当我们声明一个数组时,我们需要在编译时知道它的大小,因为它的内存是预先分配的:

Figure 3.3: An array diagram

图 3.3:一个数组图

例如,也许我们想存储一些客户的年龄;假设其中五个。我们可以做到以下几点:

    int customerAge1;
    int customerAge2;
    int customerAge3;
    int customerAge4;
    int customerAge5;

这给了我们五个值,但它采用了五个变量声明,每次我们想访问客户的年龄时,我们都需要知道我们需要使用哪个变量。但是,使用数组,我们可以将所有这些数据存储在一个变量中。此外,如果你把你的注意力放回到第二章,控制流,我们看到了如何使用循环来迭代数组,这是另一个非常有用的属性。因此,让我们将这些数据存储在一个数组中。

我们声明数组如下:

    type arrayName [numberOfElements]

因此,在前面的例子中,我们可以这样做:

    int customerAges[5];

请注意,这只是在内存中为五个int值创建了空间,以便并排放置。它还没有给这些整数中的任何一个赋值,这意味着此时它们将包含垃圾。如果我们在正确初始化数组之前尝试访问它的元素,我们可以看到这一点,如下面的代码片段所示:

注意

我们将很快介绍访问数组值,所以不要担心以下语法对您来说是否是新的。

    int customerAges[5];
    std::cout << customerAges[0] << std::endl;
    std::cout << customerAges[1] << std::endl;
    std::cout << customerAges[2] << std::endl;
    std::cout << customerAges[3] << std::endl;
    std::cout << customerAges[4] << std::endl;

如果我们运行这段代码,我们会得到垃圾数据,因为我们还没有给集合中的单个整数赋值:

Figure 3.4: Since our array is uninitialized, our values hold garbage data

图 3.4:由于我们的数组未初始化,我们的值保存垃圾数据

让我们看看如何补救。

初始化

为了用值初始化数组,C++ 给了我们许多选项,所有这些选项都使用了大括号{ }。当我们定义数组时,我们可以通过将每个元素放在大括号中并将其分配给我们的新数组来显式地赋予它们一个值:

    int customerAges[5] = {1, 2, 3, 4, 5};

这是一个完整的初始化,因为我们声明了一个包含五个元素的数组,并传递了五个值,每个值一个。如果我们重新运行前面的代码,我们将看到所有值现在都有效:

Figure 3.5: With the array properly initialized, we have valid data

图 3.5:数组正确初始化后,我们有了有效的数据

当我们像这样初始化一个数组,为每个元素传入一个值时,我们可以省略方括号中的大小,因为编译器能够为我们计算出来。在本例中,我们传入了五个元素,因此将创建一个这样大小的数组。这意味着以下两个数组声明是有效的,并导致相同的数组:

    int customerAges[5] = {1, 2, 3, 4, 5};
    int customerAges[] = {1, 2, 3, 4, 5};

我们还可以通过为我们的一些元素提供值来提供部分初始化,但不是全部:

    int customerAges[5] = {1, 2, 3};

如果我们进行这种更改,然后重新运行前面的代码和我们的三个初始化值,接着是最后两个包含垃圾的值,那么我们将会得到这样的结果:

Figure 3.6: With partial initialization, we have a mix of our defined values and a default

图 3.6:通过部分初始化,我们混合了我们定义的值和默认值

我们得到了初始化值和默认值的混合。这是因为 C++ 会将空大括号视为默认值,因此缺失的元素会被视为默认值。作为这种行为的扩展,我们甚至可以用一组空的括号来初始化数组,所有元素都将被赋予这个默认值:

    int customerAges[5] = {};

在这种情况下,输出如下:

Figure 3.7: All our elements have default values since we used empty brackets

图 3.7:因为我们使用了空括号,所以我们所有的元素都有默认值

这里需要注意的是,虽然传入的元素少于数组所能容纳的元素(在本例中是三个,而数组的大小是五),但反过来就不行了。也就是说,传入的元素不能超过数组所能容纳的数量。请考虑以下陈述:

    int customerAges[5] = {1, 2, 3, 4, 5, 6};

我们声明了一个大小为五的数组,但是试图初始化六个元素。值得庆幸的是,编译器会发出警告,我们的错误可以在造成损害之前得到纠正:

Figure 3.8: Trying to initialize too many elements throws a compiler error

图 3.8:试图初始化太多元素会引发编译器错误

最后,自 C++ 11 以来,我们已经能够直接用大括号初始化成员数组,这意味着不再需要=符号。实际上,这意味着以下两个数组声明是相同的,并且将产生相同的数组:

    int customerAges[5] = {1, 2, 3, 4, 5};
    int customerAges[5] {1, 2, 3, 4, 5};

访问元素

由于我们现在在一个集合中用一个变量名存储多个值,我们需要一种单独访问元素的方法。为此,我们使用指数。它们放在变量名后面的方括号中,表示我们要获取集合中的哪个元素:

    int myArray[5] {1, 2, 3, 4, 5};
    int mySecondValue = myArray[1];

需要注意的是,在 C++ 和大多数其他语言中,索引从0开始,而不是1。在前面的例子中,这意味着我们将输出数字 2,而不是 1。同样重要的是不要试图访问不存在的元素。例如,在前面的数组中,我们总共有 5 个元素,这意味着索引 0-4 是有效的。如果我们试图访问索引为 5 的元素,我们的应用将会崩溃。

让我们看看下面的片段:

    int myArray[5] {1, 2, 3, 4, 5};
    int mySecondValue = myArray[5];

在这段代码中,我们的数组只有五个元素,但是我们试图访问第六个元素。这将读取不属于我们阵列的内存,并且几乎总是会导致崩溃。因此,我们必须确保在访问元素时使用有效的索引。

有几种方法可以做到这一点。一种更经典的方法是找到整个数组的大小,找到一个元素的大小,然后对它们进行划分,计算它包含多少个元素:

    sizeof(myArray)/sizeof(myArray[0])

C++ 11 给了我们std::array,它的长度是可访问的。这可通过<array>标题访问:

    std::array<int, 5> myArray {1, 2, 3, 4, 5};
    std::cout << myArray.size() << std::endl;

最后,C++ 17 给了我们std::size(),一个返回两个标准容器或一个 C 风格数组的元素计数的函数:

    std::array<int, 5> myArray {1, 2, 3, 4, 5};
    std::cout << std::size(myArray) << std::endl;
    int myArray[5] = {1, 2, 3, 4, 5};
    std::cout << std::size(myArray) << std::endl;

注意

您的编译器必须启用 C++ 17 支持才能使用。

无论我们试图完成什么,我们通常都有多种选择;这一切都是为了找到最适合每个场景的。

阵列存储器

假设数组中的所有值都并排存储在内存中,我们可以通过指定一个索引轻松地获取其中的任何一个值。我们在 C++ 数组中的第一个索引总是 0,并且在内存中;这是我们数组结构的开始。下一个元素的索引为 1。所以,为了得到它,我们从 0 开始,通过元素的大小乘以我们的索引在内存中前进。在这种情况下,一个整数是 4 个字节,我们需要索引 1,所以我们将在数组的开始前寻找 4 个字节,在那里我们将找到我们的元素:

Figure 3.9: Memory access

图 3.9:内存访问

如果我们单独打印出元素的内存地址,我们就可以看到这一点。我们不打算在这里详细讨论——我们将在后面的章节中适当地讨论——但是 C++ 中的&运算符(&)获取它后面的对象的内存地址。我们可以用它来观察我们的元素在记忆中的位置。

以下代码是一个示例:

    int customerAges[] = {1, 2, 3, 4, 5};
    std::cout << &customerAges[0] << std::endl;
    std::cout << &customerAges[1] << std::endl;
    std::cout << &customerAges[2] << std::endl;
    std::cout << &customerAges[3] << std::endl;
    std::cout << &customerAges[4] << std::endl;

如果我们运行前面的代码,我们将看到每个元素的地址:

Figure 3.10: Printing the address of each element shows the addresses are incremented by 4 bytes

图 3.10:打印每个元素的地址显示地址增加了 4 个字节

内存地址以十六进制格式存储(基数 16),但是我们可以看到第一个地址,元素 0 ,以 50 结束。如果我们接着看下一个地址,元素 1,它以 54 结束,因为它的值增加了 4 个字节。4 字节是整数的大小,所以这是有意义的。如果我们再看下一个,元素 3,它的内存地址在 58 结束。这比元素 1 多了 4 个字节,比元素 0 多了 8 个字节,展示了我们的索引如何让我们导航内存来处理数组中的单个值。

练习 14:实现存储用户名的容器

在本练习中,我们将编写一个小应用,将用户名存储在一个数组中,并允许稍后再次获取用户名:

注意

这个练习的完整代码可以在这里找到:https://packt.live/35kFKix

  1. 我们将从定义一个宏开始,该宏将决定我们的系统将保存多少名称,我们将使用它来初始化一个大小正确的数组:

    // Arrays exercise.
    #include <iostream>
    #include <string>
    #define NAME_COUNT 5
    int main()
    {
        std::string names[NAME_COUNT];
  2. 接下来,我们有一点输入输出,我们想问我们的用户正确的名字数量。我们可以像在前面的练习中一样,对此使用for循环。当我们使用getline获取输入时,我们将使用for循环的索引将其直接放入我们的数组中:

        std::cout << "Please input usernames." << std::endl;
        for (int i = 0; i < NAME_COUNT; ++ i)
        {
            std::cout << "User " << i + 1 << ": ";
            std::getline(std::cin, names[i]);
        }
  3. 现在我们已经将用户名存储在阵列中,我们希望允许用户选择任意数量的用户名。我们在章节2控制 流程中看到了如何使用while循环来实现这一点,我们将在这里采用相同的方法。该循环将允许用户连续选择要查看的记录的索引,或者如果他们想要退出应用,可以输入-1 的索引。

        bool bIsRunning = true;
        while (bIsRunning)
        { 
            int userIndex = 0;
            std::string inputString = "";
            std::cout << "Enter user-id of user to fetch or -1 to quit: ";
            std::getline(std::cin, inputString);
            userIndex = std::stoi(inputString);
            if (userIndex == -1)
            {
                bIsRunning = false;
            }
  4. We're now at the final section of our application, where we want to fetch a user record based on the index. We need to be careful here to ensure that the index that the user has passed in is valid. We saw earlier in the chapter what happens if that's not the case.

    首先,我们知道我们能拥有的最低指数是0,所以任何低于这个数值都是无效的。我们还知道数组的大小NAME_COUNT,由于我们从0开始计数,我们的最大有效索引将是NAME_COUNT – 1。如果用户指定的索引符合这两个条件,那么很好,我们可以使用它。如果没有,我们将打印一个错误并让他们重新挑选:

            else
            {
                if (userIndex >= 0 && userIndex < NAME_COUNT)
                {
                    std::cout << "User " << userIndex << " = " 
                        << names[userIndex] <<std::endl;
                }
                else
                {
                    std::cout << "Invalid user index" << std::endl;
                }
            }
        }
    }

    这应该是一切。我们定义数组,收集用户记录,然后允许用户再次获取它们,确保他们提供给我们的索引是有效的。让我们运行应用并测试它:

Figure 3.11: Our small name records application, which allows users to store and fetch name records

图 3.11:我们的小名记录应用,它允许用户存储和获取姓名记录

在本练习中,我们使用了一个数组来动态存储名称。我们可以通过为我们的每个名称使用单独的字符串变量来实现类似的功能,但这不是动态的。我们必须单独实现额外的名称,而使用这种方法,我们只需要更改在应用顶部定义的宏。我们还仔细检查了我们在数组中使用的索引的健全性,这在这种情况下尤其重要,因为它是由用户提供的。

多维数组

我们已经看到了数组是如何用来存储对象集合的,没有什么能阻止我们存储数组的数组。这些被称为多维数组,起初可能会令人困惑,但它们非常有用。

到目前为止,我们使用的阵列都是一维的(1D);也就是说,它们的元素完全是线性的,可以用一行来表示,如下图所示:

Figure 3.12: A 1D array

图 3.12:1D 阵列

如果我们将数组看作一个值表(如上),要访问一个值,我们只需要指定列号。这是我们以前使用的单一索引。然而,有可能增加我们使用的行数,当我们这样做时,我们创建了一个二维(2D)数组:

Figure 3.13: A 2D array

图 3.13:2D 阵列

正如我们在这里看到的,我们现在必须使用多行,而不是将数据映射到一行。这意味着我们能够存储更多的数据,但是它引入了对第二个索引的需求,因为我们现在需要指定行和列。

在代码中声明 2D 数组非常类似于 1D 数组。不同的是,对于 2D 数组,我们需要提供两个大小值:一个用于行计数,另一个用于列计数。因此,上图中所示的数组定义如下:

    int myArray[3][5];

与 1D 数组一样,我们也可以在声明值的同时初始化它们。由于我们现在有多行,我们在它自己的花括号嵌套集中初始化每一行。初始化我们刚刚定义的数组如下所示:

    int myArray[3][5] { {1, 2, 3, 4, 5}, {1, 2, 3, 4, 5}, {1, 2, 3, 4, 5} };

理论上,数组可以由任意多个维度组成;它们不仅仅局限于两个。然而,在实践中看到二维以上的阵列并不常见,因为它们的复杂性和内存占用成为了影响因素。

练习 15:使用多维数组存储更多数据

让我们扩展之前的应用,也存储用户的姓氏。我们将使用多维数组来实现这一点:

注意

这个练习的完整代码可以在这里找到:https://packt.live/33cBVuE

  1. 练习 14 的最终输出复制到代码窗口。

  2. 我们将首先更新保存名称的数组。我们希望这是二维的,其中每个记录有两个值——一个名字和一个姓氏:

         std::string names[NAME_COUNT][2] {""};
  3. 接下来,在我们当前从用户那里获取名字并将它们读入names[i]的地方,我们需要改为要求名字。我们还需要指定第二个索引,我们将在其中存储这个输入。既然是Forename,那就要指数0 :

        std::cout << "User " << i + 1 << " Forename: ";
        std::getline(std::cin, names[i][0]);
  4. 然后我们会再做同样的事情,这次是问姓。由于我们现在存储第二个元素,我们将希望使用索引1而不是0 :

        std::cout << "User " << i + 1 << " Surname: ";
        std::getline(std::cin, names[i][1]);
  5. 最后的for循环现在应该如下:

        for (int i = 0; i < NAME_COUNT; ++ i)
        {
            std::cout << "User " << i + 1 << " Forename: ";
            std::getline(std::cin, names[i][0]);
            std::cout << "User " << i + 1 << " Surname: ";
            std::getline(std::cin, names[i][1]);
        }
  6. 最后,我们需要更新我们的输出,以包括这两个名称。我们目前只是打印names[userIndex],所以我们需要更新它以同时使用第一和第二个指数— ForenameSurname,分别为:

        std::cout << "User " << userIndex << " = " << names[userIndex][0]              << " " << names[userIndex][1] << std::endl;
  7. 完整的代码如下:

    // Arrays exercise.
    #include <iostream>
    #include <string>
    #define NAME_COUNT 5
    int main() 
    {
        std::string names[NAME_COUNT][2] {""};
        std::cout << "Please input usernames." << std::endl;
        for (int i = 0; i < NAME_COUNT; ++ i) 
        {
            std::cout << "User " << i + 1 << " Forename: ";
            std::getline(std::cin, names[i][0]);
            std::cout << "User " << i + 1 << " Surname: ";
            std::getline(std::cin, names[i][1]);
        }
        bool bIsRunning = true;
        while (bIsRunning) 
        {
            int userIndex = 0;
            std::string inputString = "";
            std::cout << "Enter user-id of user to fetch or -1 to quit: ";
            std::getline(std::cin, inputString);
            userIndex = std::stoi(inputString);
            if (userIndex == -1) 
            {
                bIsRunning = false;
            } 
            else 
            {
                if (userIndex >= 0 && userIndex < NAME_COUNT) 
                {
                    std::cout << "User " << userIndex << " = "                           << names[userIndex][0] << "" ""                           << names[userIndex][1] << std::endl;
                } 
                else 
                {
                    std::cout << "Invalid user index" << std::endl;
                }
            }
        }
    }
  8. 运行应用。输入一些虚拟名称,并检查我们是否可以正确访问它们,显示两个名称:

Figure 3.14: We've stored more data by adding another dimension to our names array

图 3.14:通过向我们的名称数组添加另一个维度,我们存储了更多的数据

通过在我们的名字数组中添加第二个维度,我们能够存储更多的信息——在本例中是一个姓氏。

向量

向量类似于数组,因为它们在内存中连续存储元素集合,但向量具有动态大小。这意味着我们不需要在编译时知道它们的大小;我们可以定义一个向量,随意添加/删除元素。考虑到这一点,他们谨慎地管理自己的规模。

每次向量需要增长时,它必须找到并分配正确的内存量,然后将所有元素从原始内存位置复制到新的位置——这是一项繁重的任务。因此,它们不会随着每次插入而增长,而是会分配比实际需要更多的内存。这给了它们一个缓冲区,在这里可以添加许多元素,而不需要另一个增长操作。然而,当达到极限时,它们将不得不再次增长。

这种动态调整大小的能力使它们在需要存储的元素数量波动时比数组更受欢迎。以注册应用为例,其中将注册的用户数量未知。如果我们在这里使用数组,我们将不得不选择一个任意的上限,并声明一个这样大小的数组。除非应用已满,否则这将导致大量空间浪费。同样,如果我们将上限设置为 1000 个用户,而只注册 100 个,那就浪费了很多空间。我们还强制规定了可注册人数的绝对上限。如果我们在这种情况下使用向量,这些问题就会得到缓解。

声明向量的操作如下:

    std::vector<int> myVector;

在这一点上,向量不包含任何元素,但是在我们看如何添加它们之前,我们要看如何访问它们。这将为我们接下来要做的练习做好准备,我们将迭代向量,打印出每个元素。

访问元素

要访问向量中的元素,我们有几个选项。首先,由于向量将其元素连续存储在内存中,就像数组一样,我们可以使用[]运算符来访问它们:

    int myFirstElement = myVector[0];
    int mySecondElement = myVector[1];

请记住,元素从索引0开始,所以对于我们的第二个元素,我们想要索引1。我们还需要考虑与数组相同的问题,比如确保我们总是使用有效的索引。值得庆幸的是,向量为我们提供了一个at函数,它的行为非常类似于[]运算符,并增加了一个检查来确保索引有效。

例如,要像刚才那样获取第一个和第二个元素,但是使用at函数,我们将执行以下操作:

    int myFirstElement = myVector.at(0);
    int mySecondElement = myVector.at(1);

这里的关键区别在于,如果我们将一个越界索引传递给at函数,而不是未定义的行为,那么该函数将抛出一个异常。异常将在第 13 章异常**c++ 中进行处理,但是它们允许我们以安全的方式捕获和处理错误,而不会导致我们的应用崩溃。

现在,我们已经了解了如何访问向量的元素,让我们编写一个小应用,它可以循环遍历并打印出所有元素。这将是非常有用的,因为我们正在考虑添加和删除元素。

练习 16:在向量上循环

在本节中,我们将以各种方式与向量进行交互,因此能够通过单个函数调用来可视化向量将非常有用。在继续之前,让我们编写一个小应用来实现这一点。以下是完成练习的步骤:

注意

这个练习的代码文件可以在这里找到:https://packt.live/2QCGTxZ

  1. 首先,初始化一个int类型的向量:

    // Vector example.
    #include <iostream>
    #include <string>
    #include <vector>
    std::vector<int> myVector;
  2. 接下来,定义一个名为PrintVector的函数。我们将在这里编写打印矢量内容的功能:

    void PrintVector()
    {
    }
  3. 要访问向量中的元素,我们可以使用索引,就像我们以前访问数组一样。为此使用for循环,使用索引访问向量中的各种元素。在函数的最后,我们将打印出几行空白行作为间隔符:

    void PrintVector()
    {
        for (int i = 0; i < myVector.size(); ++ i)
        {
            std::cout << myVector[i];
        }
        std::cout << "\n\n";
    }
  4. 最后,在main中添加对我们新的PrintVector函数的调用:

    int main()
    {
        PrintVector();
    }
  5. 完整的代码如下:

    // Vector example.
    #include <iostream>
    #include <string>
    #include <vector>
    std::vector < int > myVector;
    void PrintVector() 
    {
        for (int i = 0; i < myVector.size(); ++ i) 
        {
            std::cout << myVector[i];
        }
        std::cout << "\n\n";
    }
    int main() 
    {
        PrintVector();
    }
  6. Run the program. We've not yet initialized myVector with any data, so there won't be any output, but we can confirm that it's compiling without errors:

    Figure 3.15: The program should compile without any errors

    图 3.15:程序应该编译没有任何错误

  7. 我们将很快使用这个应用,所以保持它在编译器中打开。现在,让我们将一些数据添加到向量中。

初始化

与数组一样,我们有许多选项可以用数据初始化向量。我们将研究的第一种方法是单独指定元素。下面的初始化将给出一个包含五个元素的向量,其值如下:12345:

    std::vector<int> myVector {1, 2, 3, 4, 5};

我们还可以指定向量的大小,以及每个元素的默认值。下面的初始化将给出一个包含三个元素的向量,所有元素的值都是1:

    std::vector<int> myVector(3, 1);

最后,可以从现有的数组和向量中创建向量。这是通过传入它们的起始和结束内存位置来实现的。这将分别按如下方式进行:

    std::vector<int> myVector(myArray, myArray + myArraySize);
    std::vector<int> myVector(myVector2.begin(), myVector2.end());

修改元素

与初始化一样,向量中的元素可以通过多种方式添加/移除。要在向量末尾添加一个元素,我们可以使用push_back()函数。同样,要从向量的末尾移除一个元素,我们可以使用pop_back():

    myVector.push_back(1);
    myVector.pop_back();

在这个片段中,我们将元素1添加到向量的后面,然后立即移除它。

我们还可以使用inserterase函数更精确地添加和移除矢量。这两种方法都使用迭代器来确定操作应该发生在数组的什么位置。我们现在不打算详细讨论迭代器,但是它们是允许我们遍历集合的对象。

要在特定位置添加和移除向量中的元素,我们将执行以下操作:

    myVector.erase(myVector.begin() + 1);
    myVector.insert(myVector.begin() + 2, 9);

在这个例子中,我们使用begin()方法,该方法返回指向向量中第一个元素的迭代器。然后,我们可以添加一个偏移量来获得我们想要的元素。记住索引从0开始,我们将删除索引1处的元素,然后添加一个索引为2的元素——向量中的第二个和第三个元素。

注意

迭代器是通过“指向”集合中的项目来帮助我们迭代集合的对象。它们包含在第 12 章、容器和迭代器中。您也可以参考以下文档了解更多详情:https://packt.live/37rHlVA

让我们使用这些函数用数据初始化一个向量,然后通过在不同的位置添加和移除元素来修改它。

练习 17:修改向量

在本练习中,我们将通过添加和移除元素来修改向量。我们将利用我们在前面的练习中创建的应用,在各个步骤之间打印出我们的矢量,这样我们就可以清楚地看到我们在做什么。以下是完成本练习的文件:

注意

这个练习的代码文件可以在这里找到:https://packt.live/2QEZAB4

  1. 将我们在练习 16、 中创建的程序复制到一个向量上,如果编译器窗口还没有的话。

  2. 将当前向量定义替换为同时用以下元素初始化向量的定义:12345 :

        std::vector<int> myVector {1, 2, 3, 4, 5};
  3. 接下来,在main函数中,调用PrintVector后,使用pop_back从向量中移除最后一个元素。立即拨打另一个电话至PrintVector() :

        myVector.pop_back();
        PrintVector();
  4. 使用push_back功能在向量后面添加一个值为6的新元素。同样,接下来请致电PrintVector() :

        myVector.push_back(6);
        PrintVector();
  5. erase函数去掉向量中的第二个元素。接下来再打一个电话给PrintVector() :

        myVector.erase(myVector.begin() + 1);
        PrintVector();
  6. 最后,使用插入操作符在第四个位置插入一个值为8的元素。接下来是对PrintVector() :

        myVector.insert(myVector.begin() + 3, 8);
        PrintVector();

    的最后一次通话

  7. 完整的代码如下:

    // Vector example.
    #include <iostream>
    #include <string>
    #include <vector>
    std::vector<int> myVector {1, 2, 3, 4, 5};
    void PrintVector()
    {
        for (int i = 0; i < myVector.size(); ++ i)
        {
            std::cout << myVector[i];
        }
        std::cout << "\n\n";
    }
    int main()
    {
        PrintVector();
        myVector.pop_back();
        PrintVector();
        myVector.push_back(6);
        PrintVector();
        myVector.erase(myVector.begin() + 1);
        PrintVector();
        myVector.insert(myVector.begin() + 3, 8);
        PrintVector();
    }
  8. Run the application and observe the state of the vector after each step.

    Figure 3.16: We've manipulated the elements in our vector by means of a number of methods

图 3.16:我们已经通过许多方法操纵了向量中的元素

在这个应用中,我们已经用值初始化了一个数组,然后通过许多方法修改了它们。我们有简单的push / pop功能来添加/移除数组后面的项目。我们还能够通过使用insert / erase功能更具体地说明在哪里添加/删除值。通过用for循环迭代向量,我们能够打印出每个阶段的元素,这样我们就可以清楚地看到我们所做的修改的效果。

还有其他可用的容器,如堆栈、树和链表,每种容器都有其优缺点。你用哪一个取决于你的情况,因为通常没有单一的正确答案。数组和向量是一个很好的起点,将为我们继续学习提供必要的工具。当你继续前进的时候,一定要分支到这些不同的容器中,看看它们是如何表现的,以及在给定的情况下它们是如何最好地为你服务的。

类/结构

C++ 提供的基本类型是一个很好的起点,但是这些是应用中唯一需要的变量类型是很少见的。当我们表示真实世界的信息时,例如用户记录或对象的各种属性,我们通常需要更复杂的数据类型来存储信息。C++ 允许我们在类和结构中创建这样的类型。在后面的章节中将会更详细地介绍类,但是现在,我们将简单地介绍一些关键的概念。

类是变量和功能的集合,封装在一个对象中。当我们定义一个类时,我们正在为这个对象创建一个蓝图。这意味着每次我们想要创建一个这种类型的对象时,我们都使用这个蓝图来构建我们的对象。类是 C++ 的核心部分;毕竟,C++ 最初被命名为 C,带有类

默认情况下,C++ 类中声明的成员(变量和函数)是私有的。这意味着它们只能被类本身访问,因此不能被外部类访问。然而,这可以通过使用访问修饰符来改变;我们很快会谈到这些。类也可以相互继承,但这将在8类和结构中介绍。

用 C++ 声明类的语法如下:

class MyClassName:
{
Access Modifier:
    data members.
    member functions.
}

使用这个语法,让我们定义一个简单的类:

// Class example. 
#include <iostream>
#include <string>
class MyClass 
{
    int myInt = 0;
public:
    void IncrementInt() 
    {
        myInt++ ;
        std::cout << "MyClass::IncrementInt: " << myInt;
    };
};
int main() 
{
    MyClass classObject;
    classObject.IncrementInt();
}

在这段代码中,我们定义了一个名为MyClass的小类,它包含一个变量和一个函数。第一个是私有的,因此只能通过类本身访问,另一个是公共的,因此可以从类所在的任何地方访问。

在我们的main函数中,我们声明了类的一个实例。这给了我们一个对象,classObject,它包含了我们在MyClass中定义的所有属性和功能。既然我们定义了一个public函数,IncrementInt,我们可以通过那个类对象来调用它:

Figure 3.17: Running our code, we can see that our member function was called

图 3.17:运行我们的代码,我们可以看到我们的成员函数被调用

结构

结构与类非常相似。两者的区别在于,默认情况下,类成员是私有的,而在结构中,它们是公共的。因此,我们倾向于使用结构来定义主要用于存储数据的对象。如果我们有一个存储数据的对象,但是它有许多相关的功能,那么它通常被定义为一个类。

良好使用结构的一个简单例子是存储坐标。包括一个x和一个y值,我们可以只定义两个独立的浮点变量。然而,这种方法要求每个坐标有两个变量。然后我们必须管理它们,保持它们在一起,等等。定义一个struct更容易,它将那些单独的变量封装并包含在一个逻辑单元中。

声明一个struct和声明一个类几乎一样,但是我们用struct替换class关键字:

// Struct example. 
#include <iostream>
#include <string>
struct Coordinate 
{
    float x = 0;
    float y = 0;
};
int main() 
{
    Coordinate myCoordinate;
    myCoordinate.x = 1;
    myCoordinate.y = 2;
    std::cout << "Coordinate: " << myCoordinate.x << ", "               << myCoordinate.y;
}

在这里,我们已经在struct中定义了我们的坐标,由于成员默认是公共的,所以我们不必担心访问修饰符。我们可以简单地声明该类的一个实例,并在代码中开始使用它的成员,而不必担心这个问题。下面是通过运行前面的代码获得的输出:

Figure 3.18: We're about to access our struct members by default as they're public

图 3.18:我们将默认访问我们的结构成员,因为它们是公共的

在定义和实例化类和结构方面,我们将把它留在那里。当我们深入研究 OOP 时,它们将在后面的章节中详细介绍,因此简单地熟悉它们的语法就足够了。我们在本章后面还有一个简短的练习,但是让我们看一下访问修饰符。

访问修饰符

如前所述,类和结构的区别在于成员变量和函数的默认可见性。这并不是说它们不能被改变。在声明这些成员时,我们有以下三个可用的访问修饰符:

  • Public—任何声明为 public 的成员都可以从该类所在的任何位置访问。

  • 私有—任何声明为私有的成员只对定义它们的类和友元函数可用。

  • Protected—Protected members are similar to private members, with the addition that child classes can access them.

    注意

    子类是从基类继承的类。这将在第 10 章高级面向对象原理中详细介绍。

通过用这些关键字定义我们的成员,我们可以控制它们对我们的应用的可见性。使用这些修饰符的语法如下:

class MyClass
{
public:
    // Any members declared from this point forth will be public.
protected:
    // Any members declared from this point forth will be protected.
private:
    // Any members declared from this point forth will be private.
};

我们通过调用public / protected / private关键字在可访问性组中定义成员,然后随后声明的成员将具有该可见性。您可以在类定义中多次使用这些修饰符;您不局限于像这样严格的组,但是如果成员被整齐地分组,它会使您的代码更易读。

练习 18:使用可访问性修饰符控制访问

作为一个简短的练习,让我们在前面的类模板中添加一些成员,看看这会如何影响我们如何使用它们。对于每个可见性修饰符,我们将定义一个整数变量,然后尝试访问它。这将向我们展示不同的可访问性修饰符将如何影响我们在实践中的变量:

注意

这个练习的完整代码可以在这里找到:https://packt.live/35nNi4h

  1. 分别声明myPublicIntmyProtectedIntmyPrivateIntpublicprotectedprivate变量:

    // Accessibility example.
    #include <iostream>
    #include <string>
    class MyClass
    {
    public:
        int myPublicInt = 0;
    protected:
        int myProtectedInt = 0;
    private:
        int myPrivateInt = 0;
    };
  2. 接下来,实例化MyClass类的一个实例,并尝试访问我们刚刚在cout语句中定义的每个成员:

    int main()
    {
        MyClass testClass;
        std::cout << testClass.myPublicInt << "\n";
        std::cout << testClass.myProtectedInt << "\n";
        std::cout << testClass.myPrivateInt << "\n";
    } 
  3. 完整的代码如下:

    // Accessibility example.
    #include <iostream>
    #include <string>
    class MyClass 
    {
    public:
        int myPublicInt = 0;
    protected:
        int myProtectedInt = 0;
    private:
        int myPrivateInt = 0;
    };
    int main() 
    {
        MyClass testClass;
        std::cout << testClass.myPublicInt << "\n";
        std::cout << testClass.myProtectedInt << "\n";
        std::cout << testClass.myPrivateInt << "\n";
    }
  4. 运行代码,让我们看看编译器给了我们什么:

Figure 3.19: Only our public member variables are accessible; the others throw errors

图 3.19:只有我们的公共成员变量可以访问;其他人抛出错误

我们可以看到只有我们的公共成员变量是可访问的。另外两个抛出错误,声明它们是受保护的和私有的;因此,我们不能像以前那样使用它们。在构建应用时,为成员提供正确的可访问性是一个很好的实践,这样我们就可以确保我们的数据只按照我们希望的方式使用和访问。

为了使用方便,有时将变量和函数公开会让人觉得很有诱惑力,尤其是当我们的应用变得更大、更复杂的时候;处理谁能从哪里看到什么可能会成为一个有点任务。但是,对我们的数据和功能的适当访问不应受到损害;这对于创建不会被滥用的安全系统至关重要。在第 9 章,面向对象原则中,我们将介绍getter / setter范例,通过该范例,我们定义了允许以安全和受控的方式访问私有类成员的函数。

构造函数/析构函数

当我们在 C++ 中实例化/销毁一个对象时,我们可能想要做某些事情。例如,当一个对象被实例化时,我们可能想要为该对象做一些设置;也许给一些变量默认值,或者从某个地方获取一些信息。同样,当我们想要销毁一个对象时,我们可能首先要做一些清理。也许我们已经创建了一个临时文件,我们想删除或取消分配一些内存。C++ 让我们通过给我们构造函数和析构函数来实现这一点,当一个对象被实例化或销毁时,这些函数如果被定义,就会自动运行。

对象的构造函数保证在对象被实例化时运行,但在它被用于任何地方之前。这使我们有机会执行对象正确操作所需的任何设置。为了定义一个构造函数,我们创建了一个公共函数,它的名字就是类的名字——例如,为我们的MyClass对象定义一个构造函数:

public:
    MyClass()

为了看到我们的构造函数在运行,我们可以添加一个 print 语句并初始化我们的myPublicInt变量。在我们的应用启动时添加一个 print 语句,我们可以看到执行的顺序:

#include <iostream>
#include <string>
class MyClass 
{
public:
    MyClass() 
    {
        std::cout << "My Class Constructor Called\n";
        myPublicInt = 5;
    }
    int myPublicInt = 0;
};
int main() 
{
    std::cout << "Application started\n";
    MyClass testClass;
    std::cout << testClass.myPublicInt << "\n";
}

注意

我们可以重载我们的构造函数,就像我们在前一章重载我们的普通函数一样。然而,我们不会在这一章讨论这个问题。这是进一步阅读的任务。

运行前面的代码片段后,您将获得以下输出:

Figure 3.20: We can see that our constructor is called at the point our object is created

图 3.20:我们可以看到,我们的构造函数是在创建对象时被调用的

对象的析构函数的操作方式与构造函数非常相似,只是在对象生命周期的另一端。这给了我们执行任何清理的机会,比如取消分配内存等等。析构函数的语法与构造函数的语法相同,但前面有一个波浪号字符:

~MyClass()

如果我们扩展前面的代码来声明一个析构函数,并在其中为自己打印另一条语句,我们可以看到它何时被调用。这发生在主函数结束时;应用关闭并自行清理,因此,我们的析构函数被调用,我们看到我们的语句:

~MyClass()
{
    std::cout << "My Class Destructor Called\n";
}

Figure 3.21: Our destructor is called at the end of the application as it performs cleanup, destroying the MyClass object

图 3.21:我们的析构函数在应用执行清理时被调用,销毁了 MyClass 对象

正如在本节开始时所提到的,在后面的章节中进行深入的检查之前,我们在这里只做一个简短的类和结构的介绍。我希望你从中得到的启示是:

  • 类和结构封装变量和行为。
  • 默认情况下,类成员是私有的,而在结构中,它们是公共的。
  • 我们可以使用访问修饰符修改成员的可见性。
  • 构造函数和析构函数可用于在对象生命周期的每一端调用代码。

练习 19:类别/结构

作为本节的一个简短练习,我们将在classstruct中封装相同的数据和功能,并再次观察它如何影响它们的使用。我们要封装的数据如下:

  • 整数变量

  • 到 bool 变量

  • A function that will return a string

    注意

    这个练习的完整代码可以在这里找到:https://packt.live/37rPd9B

以下是完成练习的步骤:

  1. 让我们从创建一个封装这个行为和数据的类开始:

    class MyClass
    {
        int myInt = 0;
        bool myBool = false;
        std::string GetString()
        {
            return "Hello World!";
        }
    };
  2. 接下来,对结构执行同样的操作。这里唯一的变化就是将class关键字替换为struct

  3. To test how accessible our variables are, instantiate an instance of our class and make calls to each member:

        MyClass classObject;
        std::cout << "classObject::myInt: " << classObject.myInt << "\n";
        std::cout << "classObject::myBool: " << classObject.myBool               << "\n";
        std::cout << "classObject::GetString: " << classObject.GetString()               << "\n"; 

    然后,我们将对结构执行同样的操作,并运行应用。我们将看到一些关于类中成员的错误,这些成员是不可访问的,但结构却不可访问:

    Figure 3.22: Inaccessible members due to the default private access that classes have

    图 3.22:由于类具有默认的私有访问权限,成员不可访问

  4. To fix this, we'll use the public access modifier to make our members accessible. The final code is as follows:

    注意

    本章前面已经说过,公开一切通常是不好的做法,这是站得住脚的;这是为了演示。在后面的章节中,当我们正确地看待 OOP 时,我们将讨论 getter/setter 架构,它允许我们以更可控的方式访问变量。

    // Classes/struct exercise.
    #include <iostream>
    #include <string>
    class MyClass 
    {
    public:
        int myInt = 0;
        bool myBool = false;
        std::string GetString() 
        {
            return "Hello World!";
        }
    };
    struct MyStruct 
    {
        int myInt = 0;
        int myBool = 0;
        std::string GetString() 
        {
            return "Hello World!";
        }
    };
    int main() 
    {
        MyClass classObject;
        std::cout << "classObject::myInt: " << classObject.myInt << "\n";
        std::cout << "classObject::myBool: " << classObject.myBool               << "\n";
        std::cout << "classObject::GetString: " << classObject.GetString()               << "\n"; 
        MyStruct structObject;
        std::cout << "\nstructObject::myInt: " << structObject.myInt               << "\n";
        std::cout << "structObject::myBool: " << structObject.myBool               << "\n";
        std::cout << "structbject::GetString: "               << structObject.GetString() << "\n";
    } 

最终输出如下:

Figure 3.23: With the addition of the public access modifier, our class members become accessible

图 3.23:通过添加公共访问修饰符,我们的类成员变得可访问

在本练习中,我们概括了如何使用类和结构来封装行为,并利用访问修饰符来确保它们的可见性。这种对两者之间根本区别的理解将为我们在前面到达面向对象的章节提供更多的背景。

储存寿命

到目前为止,在我们编写的应用和代码中,我们已经在我们的main函数中声明了所有的变量。因为我们所有的其他代码也存在于这个函数中,所以我们可以完全访问所有这些变量。然而,当我们开始构建更大的应用,并开始使用函数和类时,我们需要了解范围存储寿命

对象生存期是指一个对象对我们有效和可访问的时间。到目前为止,我们的大部分变量已经在我们的main函数中声明,它们的生命周期已经与我们正在编写的应用的生命周期相匹配,没有什么好担心的。只要我们想使用这个变量,它就一直存在并且有效,因为我们在同一个范围内工作。范围指的是一段代码,它表示在其中声明的对象的生存期,因此我们可以看到这些术语是如何关联的。

我们使用花括号来表示范围,可以是函数中的范围,也可以是单独的范围,如下面的代码片段所示:

void MyFunc()
{
    // scope 1
}
int main()
{
    // scope 2
    {
        // scope 3
    }
}

在这个例子中,我们有三个不同级别的范围:一个在MyFunc、(scope 1)、一个在main ( scope 2)以及另一个在它们自己的花括号中(scope 3)。我们在这个例子中声明变量的范围将直接影响我们何时何地可以使用这些数据,以及它的生命周期有多长。让我们看看这是怎么回事。

练习 20:存储寿命示例

为了清楚地了解范围如何影响我们的变量,让我们来做一个快速练习。在每个不同的范围内,我们将定义一个整数变量,并在main函数的末尾,尝试打印出每个变量的值。然后,我们可以检查输出以查看范围之间的差异:

注意

这个练习的完整代码可以在这里找到:https://packt.live/2XE9FzJ

  1. 将前面的代码片段复制到编译器中,用整数变量定义替换关于范围的各种注释:

    #include <iostream>
    void MyFunc()
    {
        int myInt1 = 1;
    }
    int main()
    {
        int myInt2 = 2;
        {
            int myInt3 = 3;
        }
  2. 接下来,编写三个输出语句,一个用于刚刚定义的每个变量,打印它们的值。然后,用最后一个“}”关闭main功能:

        // print values
        std::cout << myInt1 << std::endl;
        std::cout << myInt2 << std::endl;
        std::cout << myInt3 << std::endl;
    }
  3. 完整代码如下:

    #include <iostream>
    void MyFunc() 
    {
        int myInt1 = 1;
    }
    int main() 
    {
        int myInt2 = 2; 
        {
            int myInt3 = 3;
        }
        // print values
        std::cout << myInt1 << std::endl;
        std::cout << myInt2 << std::endl;
        std::cout << myInt3 << std::endl;
    }
  4. 运行代码。我们的编译器会向我们抛出一些错误和警告,如下所示:

Figure 3.24: Various variables we've tried to use are not within scope, so we get errors

图 3.24:我们试图使用的各种变量不在范围内,所以我们会得到错误

如果我们阅读编译窗口中的错误,我们可以看到我们有两个声明myInt1myInt3没有在这个范围内声明。当执行离开给定的范围时(例如,当执行从MyFunc函数返回时),其中声明的变量被销毁,内存被回收。一旦发生这种情况,变量就不再可访问。

注意

情况并非总是如此。我们将在以后考虑使用指针,这样就有可能不会发生这种情况,但是现在,我们可以在这样一个前提下工作,即当范围结束时,其中的所有变量都为我们整理好了。

考虑到这一点,我们可以看到为什么我们不能用我们现有的方式使用这些变量。我们的第一个变量myInt1的作用域是MyFunc函数,所以除此之外,它将是不可访问的。myInt3也是如此,因为它也在自己的范围内被宣布。我们能够使用的一个变量myInt2是在与使用它的代码相同的范围内声明的,所以没关系。

随着我们的前进,熟悉范围和对象生命周期非常重要。将我们所有的变量放在尽可能高的范围内,然后不必担心它们在哪里/从哪里不可访问,这很有诱惑力。不过,这是个糟糕的设计。我们的目标应该是在尽可能小的范围内声明变量,这样我们就不会有不使用的内存。

静态

Static 是 C++ 中的一个特殊关键字,它将对象的生存期限定在应用的生存期内。这意味着静态变量在应用中只初始化一次,因此始终保持它们的值。变量和函数都可以变成静态的,所以让我们快速看一个例子:

// Static example. 
#include <iostream>
#include <string>
int MyInt() 
{
    int myInt = 0;
    return ++ myInt;
}
int main() 
{
    for (int i = 0; i < 5; ++ i) 
    {
        std::cout << MyInt();
    }
}

在这个例子中,我们有一个函数,它将返回一个它定义的整数,然后我们打印它的值。如您所料,如果我们按原样运行这段代码,我们将得到五个相同值的输出。每次调用该函数时,变量都被重新初始化为值0,递增,然后返回:

Figure 3.25: Since our variable is re-initialized each time the function is called, our output is the same

图 3.25:因为我们的变量在每次调用函数时都被重新初始化,所以我们的输出是相同的

但是,如果我们对此应用进行更改,并将我们的myInt变量定义为static,那么我们的程序将表现得非常不同:

    static int myInt = 0;

我们的变量在应用的生命周期中只初始化一次——第一次遇到它。这意味着,虽然我们将该值初始化为0,但这只会被观察一次,从而允许myInt在不同的函数调用之间保持其值。让我们再次运行该应用:

Figure 3.26: With the variable now declared static, its value persists between function calls

图 3.26:现在变量被声明为静态的,它的值在函数调用之间保持不变

我们现在可以看到该值正在增加,这证实了这样一个事实,即由于 static 关键字,每次调用函数时,变量都不会被重新初始化。

活动 3:报名申请

在第三个活动中,我们将编写一个用户注册应用。这将允许用户向系统注册,提供他们的姓名和年龄,我们将以自己的自定义类型存储这些信息。我们还将为用户提供通过标识进行查找的能力,检索他们的信息。

完成活动后,您应该会获得类似以下内容的输出:

Figure 3.27: Our application allows the user to add records and then recall them via an ID

图 3.27:我们的应用允许用户添加记录,然后通过一个 ID 调用它们

本活动将对您在本章中学到的所有内容进行测试,扩展我们在查看容器时所做的练习。您还将依靠以前学习的技能,例如循环、分支和读取用户输入。我们开始吧。

注意

在这个应用中,我们将处理一个异常。到目前为止,这还不是我们所涉及的内容,因此,如果您觉得它与您无关,请不要担心。这个活动的完整代码可以在这里找到:https://packt.live/2KHdXRx

以下是完成活动的步骤:

  1. 首先包含应用需要的各种头文件。

  2. Next, define the class that will represent a record in the system. This is going to be a person, containing both a name and an age. Also, declare a vector of this type to store these records. A vector is used for the flexibility it gives in not having to declare an array size upfront.

    注意

    您可以参考*练习 19,类/结构,了解如何定义结构的提示,以及练习 16,在向量上循环,*了解向量初始化。

  3. 现在,您可以开始添加一些函数来添加和获取记录;首先,补充。记录由名字和年龄组成,所以编写一个函数,接受这两个作为参数,创建一个记录对象,并将其添加到我们的记录向量中。命名该功能Add Record

  4. 添加一个函数来获取记录。该函数应该接受一个参数(用户标识)并返回该用户的记录。命名该功能Fetch Record

  5. 进入main功能,启动应用主体。从一个外部main循环开始,就像您在上一章中使用的那样,并向用户输出一些选项。你会给他们三个选择:Add RecordFetch RecordQuit

  6. 将这些选项呈现给用户,然后捕获他们的输入。

  7. 现在有三种可能的分支,这取决于用户输入,我们将用switch语句来处理。案例 1 是添加一条记录,为此,您将从用户那里获得用户的姓名和年龄,然后调用我们的AddRecord功能。

  8. The next case is the user wanting to fetch a record. For this, you need to get a user ID from the user and then make a call to FetchRecord, outputting its result. This is where you'll be catching an exception, something we've not covered before, so the following code is provided:

    try
    {
        person = FetchRecord(userID);
    }
    catch (const std::out_of_range& oor)
    {
        std::cout << "\nError: Invalid UserID.\n\n";
        break;
    }

    注意

    前面代码片段中的函数和变量的名称可能会有所不同,这取决于您对它们的命名。

    调用这段代码后,您只需要输出记录细节。同样,如果您不熟悉这个语法,也不要担心,因为它将在后面的章节中介绍。

  9. 下一种情况是用户想要退出应用。这个相当简单;你只需要退出我们的main循环。

  10. 最后,添加一个默认案例。这将处理用户输入的无效选项。这里您要做的就是输出一条错误消息,并将它们发送回应用的开始。

  11. With all of this in place, the application should be ready to go.

注意

这个活动的解决方案可以在第 520 页找到。

总结

在本章中,我们重点介绍了 C++ 提供的各种数据类型,以及如何创建自己的更复杂的对象来表示数据和封装功能。从 C++ 提供的内置数据类型开始,我们更仔细地观察了它们,研究了它们的内存占用和不同的关键字修饰符,以便扩展和更改它们的行为和属性。

然后我们继续看数组和向量。这些派生类型允许我们将不同元素的集合存储在一个变量名下,但仍然使用索引对它们进行单独寻址。我们研究了固定数组(一个需要在编译时知道其大小的集合)和更灵活的向量,它可以动态地增长/收缩以满足我们的需求。正是后一个容器,我们在最终活动中使用它来创建我们的用户记录应用。

接下来,我们对类和结构进行了一次简短的参观。这些将是后面章节的重点,所以我们只讨论了基础知识,看看类和结构之间的区别,我们如何声明它们,以及构造函数和析构函数如何操作。最后,我们查看了存储生命周期和范围,以更好地了解我们的对象存在多长时间,以及何时/何地可以访问它们。

在第四章的第一部分,操作员中,我们将会看到操作员。到目前为止,我们已经在整个工作中使用了其中的一些,但我们将花一些时间来接近它们,并更深入地了解它们。操作符是我们操作对象和数据的方式,因此对它们的操作有深刻的理解是至关重要的。