面向懒惰程序员的 C++20 教程(五)

原文:C++20 for Lazy Programmers

协议:CC BY-NC-SA 4.0

十三、标准 I/O 和文件操作

我们玩得太开心了。是时候认真对待了。

或者,当你不使用图形和游戏库的时候,也许是时候学习如何编程了。毕竟,你通常不是。即使你是,你也可能需要访问文件(比如加载游戏关卡),在 C++ 中,我们处理文件就像我们处理基于文本的用户交互一样——就像我们到目前为止用sinsout所做的那样。

标准 I/O 程序

示例 13-1 是一个使用标准 I/O 的程序,可能看起来很熟悉。

// Hello, world! program
//      -- from _C++20 for Lazy Programmers_

// It prints "Hello, world!" on the screen.
//   Quite an accomplishment, huh?

#include <iostream>

using namespace std;

int main ( )
{
    cout << "Hello, world!" << endl;

    return 0;
}

Example 13-1“Hello, world!using C++ standard I/O

以下是 SSDL 的《你好,世界》的变化。,从底层开始,反向工作:

  • 是时候澄清一下了:ssinsout是编译器自带的内置cin(读作“C-in”)和cout(C-out)的廉价仿制品。cincout与 SDL 窗口不兼容,所以我们需要一个替代品。cincout就像ssinsout,但是 a)你不能设置光标——你只能向下移动屏幕——b)你不能设置字体或颜色。

  • 我们需要main拥有与 SDL 兼容的参数(int argc, char** argv);现在可以省略了。

  • using namespace std; : cout是“标准”名称空间的一部分,你必须告诉编译器使用它,否则它会抱怨它不知道cout是什么。

  • 我们加载的是<iostream>,而不是"SSDL.h",它就像<cmath><cstdlib>一样是编译器自带的。它定义了cincoutendl(输出'\n')、 1 等东西。

编译标准 I/O 程序

您可以像以前一样构建和运行 Visual Studio 程序:打开解决方案文件,右键单击要运行的项目,然后选择 Debug ➤ Start New Instance。但是要做自己的项目,不是抄basicSSDLProject而是抄basicStandardProject

Unix 或 **MinGW 中,**变得更加简单。进入项目文件夹,输入make。(再也不用抄Makefile.unix或者Makefile.mingw;一个Makefile现在服务于两个平台——这是不使用库的优势。)要运行,输入./a.out (Unix)或a.out (MinGW)。

要创建自己的项目,在资源库的newWork文件夹中复制一份basicStandardProject,并以同样的方式使用它:makea.out

从头开始构建项目(可选)

…在 Microsoft Visual Studio 中

要制作自己的项目而不引用basicStandardProject,在启动 Visual Studio 时,告诉它创建一个新项目(图 13-1 ,然后选择控制台 App(图 13-2 )。在下一个窗口中(图 13-3 ),Visual Studio 更愿意将你的项目放在一个很难找到的名为“repos”的文件夹中;我用台式机。我也强烈建议您选中“将解决方案和项目放在同一个目录中”——否则,您会得到多个调试文件夹,并且清理文件会更加困难。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-3

在 Visual Studio 中配置新项目

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-2

在 Visual Studio 中创建新项目

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-1

正在启动 Visual Studio

默认的控制台项目是微软的 Hello,world!(图 13-4 )。你可以删除它,输入你自己的代码。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-4

新项目

如果你想使用 C++20 的最新特性(谁不想呢?),可能需要告诉 Visual Studio。需求很可能会改变,所以请查看本书源代码的newWork文件夹,了解当前的指令。

现在你已经准备好输入你的程序了,但是在运行它之前,我建议你先阅读一下反欺诈部分。

防错法

以下是您在使用 Microsoft Visual Studio 迁移到标准 I/O 时可能会发现的一些问题:

  • 在你有机会看到任何东西之前,程序就关闭了。解决方案:在工具➤选项➤调试下,取消勾选调试停止时自动关闭控制台。这将适用于所有控制台项目,直到你改变它。

    之后,如果您正在运行它,并试图运行另一个副本,该后续副本可能会在您看到之前自动关闭。一次只运行一个副本。

  • 编译器找不到 _WinMain@16 **,预编译头文件,或者其他让你去“嗯?”**您可能选择了错误的项目类型(图 13-2 )。简单的解决方法是重新开始并选择控制台应用程序。

…使用 g++

要创建自己的程序,为它创建一个文件夹,在该文件夹中创建main.cpp,并使用以下命令进行编译:

g++ -std=gnu++2a -g main.cpp -o myProgram

就这样。-std=gnu++2a表示“尽可能使用 g++ 支持的 C++20 标准,加上一些额外的‘GNU’(g++)特性”;-g表示“支持用gdbddd调试”;-o myProgram的意思是“命名可执行文件myProgram如果您不选择-o选项,可执行文件将是a.out (Unix)或a.exe (MinGW)。

要运行,请输入./myProgram (Unix)或myProgram (MinGW)。

要调试,使用ddd myProgram & 2 (Unix)或gdb myProgram (Unix 和 MinGW)。在 MinGW,我们习惯用break``SDL_main;自从 SDL 走了之后,break main取而代之。

Extra

为什么我们要把?/在 Unix 中的程序名前?

当您键入一个命令时,Unix 会查看一个名为 PATH 的目录列表:它认为可执行程序应该在其中的目录。如果当前目录(在 Unix 中称为.,单个句点)不在路径中,它不会在那里查找,所以如果您键入当前目录中的程序名,Unix 不会找到它。

我不喜欢那样,所以让我们把.(“点”)放在路径中。

假设这是它检查的第一个目录。然后,如果一个坏人可以让一个恶意程序进入你的目录,并将其命名为一个普通命令,如ls,他可以让你做可怕的事情:你输入ls,它就会删除操作系统或其他东西。

好的,那么我们将使它成为最后一个被检查的目录。现在,如果您键入ls,Unix 将在/bin(或任何地方)中查找,找到正确的ls,并运行它。

但是如果坏人猜出人们犯了什么错别字,并命名他的邪恶程序sl而你的手指错过了…他就抓住你了。

我不知道最后一种情况发生的可能性有多大,但如果你担心的话,这是一个让你置身事外的理由。

防错法
  • **(gdb/ddd)调试器显示没有找到调试符号。**在ddd中,它也给出一个空白窗口。使用-g选项重新编译,或者使用basicStandardProject并键入make

Exercises

  1. Write a program which prints all 99 verses of “99 bottles of beer on the wall.” In case you missed this cultural treasure, it goes like this:

    99 bottles of beer on the wall
    99 bottles of beer;
    Take one down, pass it around,
    98 bottles of beer on the wall!
    
    

    最后一节是以 0 个瓶子结尾的。

文件 I/O(可选)

除了家庭作业之外,有用的程序通常需要访问文件。因此,让我们看看如何做到这一点:首先是简单的方法(使用cincout),然后是更普遍适用的方法。

cincout作为文件

从某种意义上说,我们已经在使用文件了,至少有两样东西 C++ 认为是文件:cincout

cin是一个输入文件。它只是一个输入文件,当你输入的时候从键盘上获取信息。cout是输出文件:即你电脑屏幕的输出文件。定义的延伸?也许吧,但是很快我们将使用cincout作为实际的文件。

为此,我们必须知道如何使用命令提示符。Unix 和 MinGW 用户可以跳过下一节;你已经知道了。

在 Windows 中设置命令提示符

打开 Windows 命令提示符(单击开始菜单并键入cmd)并转到您正在使用的项目的文件夹。这里有一个简单的方法:在该文件夹的窗口中,单击地址栏左侧的文件夹图标,该部分显示类似于... > ch13 > 1-hello的内容。当你这样做时,它将被一个高亮显示的路径所取代,如图 13-5 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-5

在 Windows 中获取用于命令提示符的路径

如果你在命令窗口提示中看到的驱动器与你刚才复制的地址左边的不同(在我的例子中是C:),从地址输入驱动器,如图 13-6 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-6

cmd中切换到项目的驱动器和目录。这里,我们从D:驱动器转到C:,然后从cd转到包含1-hello项目的目录

现在,在命令窗口中,键入cd并粘贴到您复制的路径中,然后按回车键。

Visual Studio 将您的可执行文件<your project>.exe放在一个子文件夹中,可能是DebugReleasex64,或者是它们的某种组合。找到它,并将其复制到与你的.vcxproj相同的文件夹中,它将能够找到你放在那里的任何文件。

在命令提示符下重定向 I/O

要让您的程序从in.txt而不是键盘获得输入,请键入

myProgram < in.txt #3The < goes from file to program; makes sense

要让它也将其输出发送到out.txt,而不是屏幕,请键入

myProgram < in.txt > out.txt

尝试将 Hello 程序的输出发送到一个文件,看看会发生什么。

Online Extra

“通过 I/O 重定向为你自己和你的用户节省一些时间”:在 YouTube 频道“以懒惰的方式编程”,或者在 www.youtube.com/watch?v=zQ3TY6oSAcQ 找到它。

while (cin)

我听说在英语文本中出现最频繁的字母是,以超级频繁的 E 开头,ETAOINSHRDLU。让我们看看这是不是真的,给程序一些巨大的文本,也许是关闭gutenberg.org,并计算频率:

make an array of frequencies for letters, all initially zero

while there are characters left
    read in a character // we won't prompt the user;
                        // it's all coming from a file
    if it's a letter add 1 to frequency for that letter

print all those frequencies

我知道如何创建数组,给int加 1,并读入字符。但是我怎么知道还有字符呢?

while (cin) ...会做到的。如果你把cin放在某个你期望有bool的地方,它会被评估为类似于“如果cin没有出错”cin通常会出错的地方是到达输入文件的末尾。

示例 13-2 是结果程序。

// Program to get the frequencies of letters
//      -- from _C++20 for Lazy Programmers_

#include <iostream>

using namespace std;

int main ()
{
    // make an array of frequencies for letters, all initially zero
    constexpr int LETTERS_IN_ALPHABET = 26;
    int frequenciesOfLetters[LETTERS_IN_ALPHABET] = {}; // all zeroes

    // read in the letters
    while (cin)              // while there are letters left
    {
        char ch; cin >> ch;  // read one in
        ch = toupper(ch);    // capitalize it

        if (cin)             // Still no problems with cin, right?
            if (isalpha(ch)) //   and this is an alphabetic letter?
                ++frequenciesOfLetters[ch - 'A'];
                             // A's go in slot 0, B's in slot 1...
    }

    // print all those frequencies
    cout << "Frequencies are:\n";
    cout << "Letter\tFrequency\n";
    for (char ch = 'A'; ch <= 'Z'; ++ch) // for each letter A to Z...
        cout << ch << '\t' << frequenciesOfLetters[ch - 'A'] << '\n';

    return 0;
}

Example 13-2Counting frequencies of letters in a text file

a.out < in.txt > out.txt (g++)或2-frequencies < in.txt > out.txt (Visual Studio)试试这个,你会得到一个类似于out.txt的文件

Frequencies are:
Letter   Frequency
A        40
B        5
C        9
D        20
E        63
...

如果字母来自一个文件,while (cin)会在到达文件末尾时停止。但是实际的键盘输入没有文件结尾。您可以通过按 Ctrl-Z 和 Enter (Windows)或 Ctrl-D (Unix)来模拟它。必须是该行的第一个字符,否则可能不起作用。

读入字符,包括空格

新任务:读入一个文件,每个字符,并大写一切。这是我们的输入文件。

这看起来应该可以工作,但却不能:

while (cin)               // for each char in file
{
    char ch;  cin >> ch;  //    read in char
    ch = toupper (ch);    //    capitalize
    if (cin) cout << ch;  //    cin still OK? Then print
}

使用这个输入-Twinkle, twinkle, little bat! How I wonder what you're at!–我们得到这个输出:TWINKLE,TWINKLE,LITTLEBAT!HOWIWONDERWHATYOU'REAT!

跳过空白。对于用户交互和 ETAOIN SHRDLU 程序来说很好,但是这里我们需要空白。

解决方法:ch = cin.get();cin.get()返回下一个字符,即使是空格、制表符(\t,或者行尾(\n)。

示例 13-3 读入一个文件并生成一个全大写版本。要执行,请键入a.out < in.txt > out.txt (g++)或3-capitalizeFile < in.txt > out.txt (Visual Studio)。

// Program to produce an ALL CAPS version of a file
//      -- from _C++20 for Lazy Programmers_

#include <iostream>
#include <cctype>             // for toupper

using namespace std;

int main ()
{
    while (cin)               // for each char in file
    {
        char ch = cin.get();  //   read in char
        ch = toupper (ch);    //   capitalize
        if (cin) cout << ch;  //   cin still OK? Then print
    }

    return 0;
}

Example 13-3Capitalizing a file, character by character

防错法
  • You told it to stop at the end of file , but it goes too far :

    /// get an average -- buggy version //
    double total = 0.0;      // initialize total and howMany
    int  howMany = 0;
    
    while (cin)              // while there are numbers in file
    {
        int num; cin >> num; //   read one in
    
        total += num;        //   keep running total
        ++howMany;
    }
    
    

    Your input file is

    1
    2
    
    

    而你的平均分是…1.6667。啊?

    用调试器跟踪它。它读入 1,将其相加,并递增howMany。它读入 2,将其相加,并再次递增howMany。用while (cin)测试文件结尾;它继续前进。

    但是我们不是在文件的末尾吗?也许不是。可能还有另一个\n 或者空间什么的。

    所以程序继续进行。它读入下一个数字,但是没有,所以它把 num 保持为 2,再加一次(!),并再次递增。一个错误诞生了。

    It couldn’t know there wasn’t going to be another number till it tried to read it. So the solution is to test the input file after every attempt to read, to ensure it didn’t run out of input while reading:

    int num; cin >> num; // read one in
    
    if (cin)             // still no problems with cin, right?
    {
        total += num;    // keep running total
        ++howMany;
    }
    
    

    average见源代码,ch13文件夹/解决方案,是本程序的完整正确版本。

Exercises

在所有这些练习中,使用标准 I/O。

  1. 从文件中读入一系列数字,并以相反的顺序打印出来。你不知道有多少,但你知道不超过,比如说,100。(这样你可以声明一个足够大的数组。)

  2. 计算文件中的字符数。

  3. …不包括空格或标点符号。

使用文件名

一直重定向 I/O 工作量太大。也许我有多个输入文件——它们不可能都是cin。或者,也许我只是想让程序记住文件名,而不是期望我在命令提示符下键入它。

假设我有一个游戏,游戏中愤怒的机器人四处游荡,试图与我的玩家发生冲突。播放器从屏幕的左边开始,我的工作是让它到右边,没有任何碰撞。

如果我把机器人放在特定的位置,设计一个比一个更难的关卡,可能会让游戏更有趣。我们将从三个机器人开始第一关,所以有三个位置。

如果我从cin得到这个(太烦人了,但是我们马上会改变它),代码可能看起来像例子 13-4 。要运行这个例子,输入六个整数,它会把它们返回给你。令人兴奋,是的,我知道。

// A (partial) game with killer robots
//    meant to demonstrate use of file I/O
// This loads 3 points and prints a report
//       -- from _C++20 for Lazy Programmers_

#include <iostream>

using namespace std;

struct Point2D { int x_=0, y_=0; };

int main ()
{
                    // an array of robot positions
    constexpr int MAX_ROBOTS = 3;
    Point2D robots[MAX_ROBOTS];

    int whichRobot = 0;

    // while there's input and array's not full...
    while (cin && whichRobot < MAX_ROBOTS)
    {
        int x, y;
        cin >> x >> y;        // read in an x, y pair

        if (cin)              // if we got valid input (not at end of file)
        {
            robots[whichRobot] = {x, y}; //  store what we read
            ++whichRobot;                //  and remember there's 1 more robot
        }
    }

    for (int i = 0; i < MAX_ROBOTS; ++i)
        cout        << robots[i].x_ << ' '
                    << robots[i].y_ << endl;

    return 0;
}

Example 13-4Code to read in several points from cin

现在,让程序在不重定向 I/O 的情况下获取文件。下面是使用命名输入文件必须做的事情:

  1. #include < fstream >,其中有我需要的定义。

  2. ifstream inFile;声明我的输入文件。ofstream用于输出文件。

  3. inFile.open``("level1.txt");–打开一个文件会将它与一个文件名相关联,并确保没有问题。

  4. 验证打开的文件没有错误。如果是输入文件,错误可能是该文件不存在或者不在您认为的文件夹中。如果是输出文件,可能是磁盘有问题或者是只读文件。以下是如何验证:

  5. 在您想要使用新文件的任何地方,将cin更改为inFile。如果是输出文件,将cout改为outFile

  6. 完成后,关闭文件: inFile.close ();。这告诉操作系统忘记inFileinput.txt之间的关联,从而让其他可能需要它的程序使用它。不可否认,当你的程序结束时,它引用的所有文件都将被关闭——但是,当你用完了你的玩具,我指的是文件,养成收起来的习惯是明智的。你妈妈会为你骄傲的。

if (! inFile) // handle error

示例 13-5 是程序的更新版本。

// A (partial) game with killer robots
//    meant to demonstrate use of file I/O
// This loads 3 points and prints a report
//       -- from _C++20 for Lazy Programmers_

#include <iostream>
#include <fstream>  // 1\. include <fstream>

using namespace std;

struct Point2D { int x_=0, y_=0; };

int main ()
{
                    // an array of robot positions
    constexpr int MAX_ROBOTS = 3;
    Point2D robots[MAX_ROBOTS];

                    // 2\. Declare file variables.
                    // 3\. Open the files.
                    //  Here's two ways to do both; either's fine
    ifstream inFile; inFile.open("RobotGameLevel1.txt");
    ofstream outFile ("RobotSavedGame1.txt");

                    // 4\. Verify the files opened without error
    if (! inFile)
    {
        cout << "Can't open RobotGameLevel1.txt!\n"; return 1;
                    // 1 is a conventional return value for error
    }
    if (! outFile)
    {
        cout << "Can't create file RobotSavedGame1.txt!"; return 1;
    }

    int whichRobot = 0;

                   // 5\. Change cin to inFile, cout to outFile

    // while there's input and array's not full...
    while (inFile && whichRobot < MAX_ROBOTS)
    {
        int x, y;
        inFile >> x >> y;    //  read in an x, y pair

        if (inFile)           // if we got valid input (not at end of file)
        {
            robots[whichRobot] = {x, y}; // store what we read
            ++whichRobot;                // and remember there's 1 more robot
        }
    }

    for (int i = 0; i < MAX_ROBOTS; ++i)
        outFile     << robots[i].x_ << ' '
                    << robots[i].y_ << endl;

                   // 6\. When done, close the files
    inFile.close(); outFile.close();

                   // can still use cout for other things
    cout << "Just saved RobotSavedGame1.txt.\n";

    return 0;
}

Example 13-5Program that reads an input file and prints to an output file

这奏效了;它将RobotSavedGame1.txt保存在与.vcxproj文件相同的文件夹中。

程序启动时会删除RobotSavedGame1.txt中的所有内容,并用新内容替换它们。

程序中可以有多个输入和输出文件。您也可以将文件传递给函数:

void readFile (ifstream& in,  double numbers[], int& howManyWeGot);
void writeFile(ofstream& out, double numbers[], int  howMany);

Exercises

  1. 写一个程序来判断两个文件是否相同。

  2. 写入和测试函数,以读取和打印到一个Point2D s 的文件。

  3. 掷出两个骰子 100 次,并将结果存储在一个文件中…

  4. …然后加载该文件并打印一个柱状图:一个柱状图显示你得到 2 的次数,另一个柱状图显示你得到 3 的次数,以此类推。在 SSDL 这样做(使用basicSSDLProject;继续使用文件变量;只是不要指望cincout管用);或者在屏幕上打印 x,显示每个值出现的次数——类似于:

    1  : 
    2  : X
    3  : XXXXXXX
    4  : XXXXXXXX
    ...
    
    
  5. 制作你自己的密码:一个字母方案,如 A 代表 R,B 代表 D,等等。然后使用您的加密方案对消息进行编码。还要写一个解密程序,验证一切正常。

  6. (用力)地球变暖了吗?

    在本章的示例代码中有一个文件temperature.txt 4 ,它包含给定年份的年份和估计的全球平均温度。(给出的温度是相对于 1910-2000 年估计平均温度的摄氏度数)。

    那么我们能从中学到什么呢?

    The degrees increase per year, which is

    )

    x 是年份,y 是温度。σx 读作“x 的总和”,意思是“所有 x 的总和”m 是与数据最匹配的直线 y = mx + b 的斜率。

    How closely the yearly temperatures actually match this line. This is

    )

    如果 R 为–1 或 1,则相关性很强。如果 R 接近 0,就很弱。负 R 意味着温度随时间下降(但我们已经从 m 知道了)。

    写一个程序,读取文件,并为用户提供每年增加的学位和 r。你需要什么功能?在给出你的答案之前,充分测试他们以确保你信任他们。

    当然,相关性并不能证明因果关系。比如喝咖啡的人滑雪多(比方说)。这是否意味着咖啡会导致滑雪?也许是滑雪小屋提供免费咖啡。或者,喜欢找乐子的人更可能去滑雪和喝咖啡。对于因果关系,我们需要更多一点的(人类)思考。

endl还告诉 C++ 立即将输出发送到屏幕——刷新它。我们通常不在乎,只要它最终到达那里,所以我倾向于使用’\n’

2

&的意思是“马上再给我一个命令提示;不要等待ddd结束”——这是一个好习惯。

3

Unix 注释标记。在 Windows 中,使用& REM(或者干脆不放注释)。

4

来源:www . ncdc . NOAA . gov/CAG/global/time-series

十四、字符数组和动态内存

字符数组——也称为“字符串”或文本——对许多任务都很重要。本章展示了如何处理它们,以及如何在事先不知道大小的情况下创建这些或其他数组。在这个过程中,我们将尽可能以最有效的方式学习标准库的字符数组函数:通过构建它们。

字符数组

我们从一开始就使用了char数组。我们从第一章和第十三章引用的"Hello, world!"是一个字符数组,内容如图 14-1 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-1

“你好,世界!”字符数组文字

“空字符”是一个标记,告诉 C++ 这是我们的字符串结束的地方。cout不是在到达分配空间的末端时停止打印——它不知道也不关心分配了多少空间——而是在到达'\0'时停止打印。

让我们看看除了打印之外,我们还能用char数组做什么。

这里有两种方法来初始化一个字符数组:

char A[] = {'d','o','g','\0'}; // they both

mean the same thing
char A[] = "dog";              // but this one's easier to read,
                               //   don't you think?

你也可以从cin或输入文件中把一个单词读入字符数组。我们需要确保我们声明的数组有足够的空间容纳输入的内容。我们通过分配比我们可能需要的更多的字符来做到这一点:

constexpr int MAX_STRING_SIZE = 250;
char      name[MAX_STRING_SIZE];
cout << "What's your name? "; cin >> name;

那段代码读起来一个字。如果你想让读取整行(也许你想让用户输入名字和姓氏),你需要cin. getline (name, MAX_STRING_SIZE);

我们可以将数组传递给函数。在示例 14-1 中,我们有一个函数打印一个问题并得到一个有效的是或否的答案。我们不打算改变数组,所以我们把它作为const传递。

bool getYorNAnswer (const char question[])
{
   char answer;

   do
   {
        cout << question;          // print a question
        cin  >> answer;            //   ...and get an answer
        answer = toupper (answer); //   capitalized, so we can compare to Y, N
    }
    while (answer != 'Y' && answer != 'N');
                                   // keep asking till we get Y or N
    return answer == 'Y';          // "true" means "user said Y"
}

Example 14-1A function that takes a char array as a parameter. Since it’s short, it and Example 14-2 are together in source code folder ch14, in project/folder 1-and-2-charArrays

如果我们这样称呼getYorNAnswer

getYorNAnswer ("Ready to rumble (Y/N)? ")   // same question, and reasoning,
                                            // as in Chapter 5's
                                            // section on while and do-while

我们的交互可能看起来像这样

Ready to rumble (Y/N)? x
Ready to rumble (Y/N)? 7
Ready to rumble (Y/N)? y

…此时getYorNAnswer将返回true

现在让我们看看一个字符串有多长——不是分配的内存,而是正在使用的部分,直到空字符:

where = 0
while the whereth char isn't the null character (not at end of string)
  add 1 to where

例 14-2 是完整版。

unsigned int myStrlen (const char str[]) // "strlen" is the conventional
                                         //    name for this function
{
    int where = 0;

    while (str[where] != '\0')   // count the chars
        ++where;

    return where;                // length is final "where"
}

Example 14-2The myStrlen function

. Find it and Example 14-2 in source code folder ch14, in project/folder 1-and-2-charArrays

该功能和其他功能已经在包含文件cstring中提供。表 14-1 列出了最常用的。

表 14-1

一些cstring功能,为清晰起见进行了简化

| `unsigned int` 1`strlen (const char myArray[]);` | 返回`myArray`中字符串的长度(到空字符为止有多少个字符)。 | | `void strcpy (char destination[],``const char source[]);` | 将`source`的内容复制到`destination`中。 | | `void strcat (char destination[],``const char source[]);` | 将`source`的内容复制到`destination`的末尾。如果您在包含`"Mr."`和`"Goodbar"`的参数上调用`strcat`,那么得到的目的地将是`"Mr.Goodbar"`。 | | `int strcmp (const char a[],``const char b[]);` | return-1 如果按字母顺序`a`在`b`之前,如`strcmp ("alpha", "beta")`;如果相同,则为 0;如果`a`在`b,`之后,如`strcmp ("beta", "alpha")`所示,则为 1。 |

Note

如果 Microsoft Visual Studio 看到strcpystrcat等等,它可能会给出警告:

warning C4996: 'strcpy':此函数或变量可能不安全。考虑改用strcpy_suse _CRT_SECURE_NO_WARNINGS禁用折旧。有关详细信息,请参见联机帮助。

strcpy_sstrcat_sstrcpystrcat的版本,它们试图阻止你写超出数组的界限。听起来很明智,但这通常不会流行起来。我不使用它们,因为我希望代码可以在编译器之间移植。或者我只是喜欢生活在边缘。22

要取消警告,请将这一行放在引用strcpy的任何文件的顶部,依此类推:

#pragma warning (disable:4996) // disable warning about // strcpy, etc.

防错法

  • 在调试器 中,你看到你的 char **数组看似合理,但到了最后却充满了随机字符。**没事;无论如何都不会打印或使用超过'\0'的内容。可以忽略。

  • 你看到你的 char 数组中合理的字符被打印出来,然后后面跟着多余的垃圾。字符串缺少最后一个'\0'。解决方案:在末尾插入'\0'

  • It acts like it’s gotten some of your input before you had a chance to type it. (If using file I/O, it skips part of the file.) Here’s an example:

    do
    {
        cout << "Enter a line and I'll tell you how long it is.\n";
        cout << "Enter: "; cin.getline (line, MAX_LINE_LENGTH);
    
        cout << "That's " << strlen (line) << " characters.\n";
    
        letsRepeat = getYOrNAnswer ("Continue (Y/N)? ");
    }
    while (letsRepeat);
    
    

    第一次,你很棒。之后每次你说要再来一次,它就说你的线长是 0,问你要不要继续。

    想象一下cin提供了一个从键盘发送到程序的字符序列,如图 14-2 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-2

cin缓冲区 3 ,以用户键入的任何内容作为其内容

在(a)中,你已经输入了你的第一行,TO BEcin.getline通过'\n'获得所有这些。

在(b)中,您已经输入了对"Continue (Y/N)?"问题的回答。

©中,getYOrNAnswer做了 input: cin>>answer. answer变成了“y”,getYOrNAnswer做了。

我们准备再次开始循环并获得更多的输入……但是看看在cin缓冲区中留下了什么:一个供cin.getline读取的字符串。这是一个空字符串,就像你什么也没输入就按了 Enter 键,但它仍然是一个字符串。所以cin.getline不会等你打字;它继续读取那个空字符串…然后我们回到getYOrNAnswer被询问是否要继续。

我们需要在cin.getline被愚弄之前甩掉剩下的'\n' getYOrNAnswer。这里有两种方法:

  • cin.getline再来,就是为了摆脱'\n'

  • cin.ignore (MAX_LINE_LENGTH, '\n');。这忽略了从MAX_LINE_LENGTH到第一个\n的所有char,以先出现的为准。

我觉得还是放在getYOrNAnswer里吧;是提出问题的函数给我们带来了麻烦,所以它应该自己清理:

  • cin >> 读入一个 char 数组,但是走得太远,覆盖了其他变量。让你的char数组变大,希望没人输入超长的东西。C++20 通过只接受最大可用大小来防止这个问题,但是你的编译器可能还不支持这个特性。

  • cin **不会读入作为参数传入的数组。**见下一节最后一个反欺诈条目。

bool getYOrNAnswer (const char question[])
{
    ...

    cin.ignore (MAX_LINE_LENGTH, '\n'); // dump rest of this line

    return answer == 'Y';
}

Online Extra

“当你在字符串末尾发现垃圾的那一刻”:在 YouTube 频道“以懒惰的方式编程”,或者在 www.youtube.com/watch?v=57jkYKwN9hg

Exercises

  1. 编写并测试myStrcpy,你自己版本的strcpy函数,如表 14-1 所述。(本章后面有答案。)为了测试它是否真的把'\0'放在了末尾,在复制之前用 x 填充目标数组(比如说)。

  2. 编写并测试你的strcat版本。

  3. …和strcmp

  4. 询问用户的姓名,并重复一遍。如果第一个字母是小写的,那就大写。

  5. (使用文件 I/O)确定给定文件中的行数。

  6. (使用文件 I/O)…以及它们的平均长度。

  7. (使用文件 I/O)编写一个程序,找出并打印两个给定文件之间的共同点。假设每个单词在一个文件中最多出现一次。

数组的动态分配

有时直到程序运行时你才知道数组应该有多大。但这是行不通的:

int size;
// calculate size somehow
int A[size];// compiler should complain: size must be a constant value

以下是应该做的事情。

首先,声明数组,但不为其元素分配内存:

int* A;

A不是一组int的集合,而是一些int的“指针”的地址。这就像我们用[]声明它时一样,但是还没有为那些int存储。

接下来,给它需要的内存:


A = new int [size];

这就要求 C++ 的一部分,“堆管理器”给我们这么多的int中的一部分。有一整堆的内存可用于此,堆管理器可以在你需要的任何时候给你其中的一部分。这被称为“动态分配”,因为它是在程序运行时发生的。到目前为止,我们在编译时进行分配的方式是“静态分配”(以这些方式分配的内存称为“动态”或“静态”)

我们像以前一样使用数组。完成后,我们告诉堆管理器它可以取回它,因此:


delete [] myArray;

对堆管理器来说,这是一个提醒,你所返回的是一个数组。如果你忘记了[],唉,编译器不会告诉你——你得记住你自己。

总之,要“动态分配”任何<type>的数组:

  1. <type>* myArray = new <type> [size];

  2. 像平常一样使用数组。

  3. 当你完成后。

很容易忘记delete []。有关系吗?当然可以。如果你一直分配内存并且从来不归还——如果你一直做****【内存泄漏】——内存最终耗尽,程序崩溃。以后我们会有一种方法让删除变得更容易记忆。

**示例 14-3 展示了如何使用动态分配。

// Program to generate a random passcode of digits

//       -- from _C++20 for Lazy Programmers_

#include <iostream>
#include <cstdlib> // for srand, rand
#include <ctime>   // for time

using namespace std;

int main ()
{
    srand ((unsigned int) time(nullptr));//  start random # generator

    int codeLength;                      //  get code length
    cout<< "I'll make your secret passcode. How long should it be? ";
    cin >> codeLength;

    int* passcode = new int[codeLength]; //  allocate array

    for (int i = 0; i <codeLength; ++i)  //  generate passcode
        passcode[i] = rand () % 10;      //  each entry is a digit

    cout << "Here it is:\n";             //  print passcode
    for (int i = 0; i < codeLength; ++i)
        cout << passcode[i];
    cout << '\n';
    cout << "But I guess it's not secret any more!\n";

    delete [] passcode;                  //  deallocate array

    return 0;
}

Example 14-3A program that dynamically allocates, uses, and deletes an array of ints

以下是一个会话示例:

I'll make your secret passcode. How long should it be? 5
Here it is:
14524
But I guess it's not secret any more!

Extra

1992 年,人工智能研究员 Edmund Durfee 在全国人工智能会议(AAAI-92)上做了一次受邀演讲,“你的计算机需要知道的,你在幼儿园就学会了”——引用了罗伯特·富尔亨的畅销书我真正需要知道的一切我在幼儿园就学会了。以下是 Durfee 所说的你的电脑从你的早期儿童教育中需要的:

  • 分享一切。

  • 公平竞争。

  • 不要打人。

  • 把东西放回原处。

  • 收拾你自己的烂摊子。

  • 不要拿不属于你的东西。

  • 当你伤害别人时,说对不起。

  • 同花。

  • 当你走向世界时,注意交通,手拉手,团结在一起。

  • 最终,一切都会消亡。

    其中许多在操作系统中是有用的。也许你有一个占用内存和 CPU 时间的程序,所以如果你想和一个不同的程序交互,你不能。(分享一切。)也可能只有占满整个屏幕才能运行。(公平竞争。)

    就记忆而言,我们需要“把东西放回你发现它们的地方。”就像蜡笔和玩具一样,如果我们总是把它们放回去,就更容易找到我们需要的东西。

防错法

动态内存最常见的问题是程序崩溃。可能是什么原因造成的?

  • **忘记初始化:**如果你还没有初始化myArray,它的地址指向某个随机的位置。它几乎肯定会崩溃。这比得到错误的输出却不知道要好。

防止这种情况的两种方法如下:

int* A = new myArray[size]; // initialize as soon as you // declare

或者

int* A = nullptr; // we'll initialize to something // sensible later

按照惯例,nullptr的意思是“不指向任何地方,所以不要考虑查看任何元素。”在老的程序中,不是nullptr而是NULL

  • 忘记 删除 : 长时间这样做,程序会耗尽内存并崩溃。

  • 忘记使用 delete []中的 [] :这会导致“未定义的行为”,这意味着它可能会崩溃,表现完美,或者引发第三次世界大战。我不会冒这个险。

其他问题包括:

  • 错误声明 一行多指针:很奇怪,但是

    int* myArray1, myArray2;

    没有创建两个指针。它创建了一个int指针myArray1和另一个(单个)int myArray2。为什么这么困惑?这是 C 标准遗留下来的东西。解决方案:

    int* myArray1;

    int* myArray2;

  • 在不必要的时候使用动态内存:这不是错误,但会导致错误。动态记忆有更多可能出错的地方;你必须记住用new分配,用delete解除分配。如果你没有任何收获(比方说,如果你在编译时知道你的数组有多大),给自己省点事;按老办法分配。

  • cin 不会读入你动态分配的数组或者你的数组作为函数参数传入:

void read (char str [])
{
    cin >> str;                                   // compile-time //   error
}

int main ()
{
    char  s1 [FIXED_SIZE];             cin >> s1; // No problem
    char* s2 = new char[someSize];     cin >> s2; // compile-time //   error
...
}

正如前面的反阻塞部分所提到的:当cin知道它要读入的数组的大小时,就像它对s1所做的那样,它只读入它有空间的数组。 4 但是如果给它一个动态数组什么的传入作为参数,它不知道;它可能读入太多,并超过数组界限。C++20 标准通过说“不,你不能这样做”来解决这个问题,因此导致了一个新的问题:编译时错误。

公平地说,这是一个巨大的安全漏洞。

我的解决方法是总是读入一个本地声明的固定大小的数组,并从该数组复制到动态数组或传入的数组中:

static char buffer [REALLY_BIG_NUMBER];
if there is room in str to store what we get, then
    strcpy (str, buffer);
else
    complain

Exercises

  1. 询问用户的姓名。您需要一个足够长的缓冲区来存储任何合理的名称。然后把它存储在一个足够长的数组里。

  2. (使用 SSDL)询问用户要画多少颗星星;生成随机星阵列;画出来。根据需要使用函数。

  3. (使用文件 I/O)编写一个程序,计算文件中的行数(参见上一节中的练习 5),动态分配一个数组来存储这些行,并将它们全部读入。提示:您可以打开文件,计算行数,关闭它,然后再打开它。

  4. (更难)动态分配一个游戏板,像棋盘但大小可变。我不能只分配一个二维数组,所以我们只能用一维数组来凑合。决定它的大小以及如何访问第,第的位置。

  5. (使用 SSDL;硬)自己写位图:动态分配的数组,每个数组包含图像中一个像素的颜色。和前面的练习一样,我们需要使用一个一维数组。

    提供渲染功能;给定位图、屏幕上的起始位置以及位图的宽度和高度,在该位置显示位图。要绘制像素,请使用SSDL_SetRenderDrawColorSSDL_RenderDrawPoint

    位图作为一个struct,包含数组加上宽度和高度,这是一个好主意吗?

使用*符号

我们已经使用了*来声明动态分配的数组:

double* myArray = new double[sizeOfArray];

我们也可以用它来指代单个元素。*A的意思是A[0],因为*A的意思是“什么A指向什么”,而A指向第 0 个元素。

*(A+1)的意思是A[1]。编译器足够聪明,知道A+1意味着下一个元素的地址。(像这样给指针加上一些东西叫做“指针算术”。)

A[1]*(A+1)更容易阅读——那么为什么要用这种新的符号呢?一个原因是让你为我们稍后将和*一起做的事情做好准备。

另一个是这个有趣的遍历数组的新方法。考虑本章“字符数组”一节的练习 1 中的myStrcpy函数。下面的代码片段是在ch14/strcpyVersions中收集和编译的。

void myStrcpy (char destination [], const char source[])
{
    int i = 0;

    while (source[i] != '\0')
    {
        destination[i] = source[i];
        ++i;
    }

    destination[i] = '\0'; // put that null character at the end
}

这里有一个不使用[]的版本:

void myStrcpy (char* destination, const char* source)
{
    int i = 0;

    while (*(source + i) != '\0')
    {
        *(destination + i) = *(source+i);
        ++i;
    }

    *(destination + i) = '\0'; //  put null character at the end
}

没有明显的改善,但它会起作用。接下来,我们取消使用i,直接更新sourcedestination:

void myStrcpy (char* destination, const char* source)
{
    while (*source != '\0')
    {
        *destination = *source;
        ++source; ++destination;
    }

    *destination = '\0'; //  put null character at the end
}

有用吗?是的。现在我们每次遍历循环时都要给source加 1——所以每次,它都指向它的下一个元素(对于destination也是如此)。当source到达空字符时,循环停止。

记住测试条件时,0 表示false,其他都表示true(第四章)。所以while (*source != '\0')可以写成

while (*source) // if *source is nonzero -- "true" -- we continue

因此,我们可以将函数写成

void myStrcpy (char* destination, const char* source)
{
    while (*source)
    {
        *destination = *source;
        ++source; ++destination;
    }

    *destination = '\0'; // put null character at the end
}

这是我真正应该停止的地方。习惯了就可读了;它很短,给了我们用*符号的练习,我们会在第二十章及以后经常用到。但是现在退出太有趣了。

回忆第五章中的后增量运算符(如X++)。Y=X++;真正的意思是Y=X; X=X+1;。获取值,然后递增。

在这里,我们可以将它用于目的地和源——因为我们使用它们的值进行赋值,然后递增。

void myStrcpy (char* destination, const char* source)
{
    while (*source)
        *destination++ = *source++;

    *destination = '\0'; // put null character at the end
}

还记得,X=Y的值是分配的值——在*destination++ = *source++的情况下就是简单的*source。只要这个非零,我们就想继续下去:

void myStrcpy (char* destination, const char* source)
{
    while (*destination++ = *source++); *destination = '\0';
}

这近乎邪恶和粗鲁。我不想这样写我的代码,但是它的确展示了我们通过使用*获得的灵活性。

Note

被称为“解引用”操作符——因为它接受一个对象的引用(地址)并给出对象本身。

&是它的反义词:“reference”运算符。它接受一个对象并给你地址:

int x;
int* addressOfX = &x;

我在 C++ 里不经常用;它在 C 中更有用,C 中缺少我们的引用参数,引用参数也使用符号&。这让生活更加混乱。啊,好吧。如果*可以表示取整(*addressOfX ) 相乘(x*y,我想&也可以表示不止一个意思。

防错法

  • 编译器抱怨你正在用一个字符串常量初始化一个 char* ,就像在char* str = "some string";中一样。

    改为说char str[] = "some string";

Exercises

在所有这些练习中,使用*符号,而不是[]符号。

  1. strcmp

  2. …对我们的其他基本字符数组函数做同样的事情。

  3. (Harder) Write and test a function contains which tells if one character string contains another. For example,

    contains ("'Twas brilling, and the slithy toves"
              " did gyre and gimble in the wabe",
              "slithy")
    
    

    会返回 true。

  4. (Harder) Write a function myStrtok which, like the strtok in cstring, gets the next word (“token”) in a character array. It might be called thus:

    char myString[] = "Mary Mary\nQuite contrary";
    const char* nextWord = myStrtok (myString, " \t\n\r");
                          // I use space, tab, return, and
                          // the less-used carriage return \r
                          // as "delimiters": separators
                          // between words
    while (nextWord)
    {
        cout << "Token:\t" << nextWord << '\n';
        nextWord = myStrtok (nullptr, " \t\n\r");
    }
    
    

    预期产出:

    Token: Mary

    Token: Mary

    Token: Quite

    Token: contrary

    当你第一次为一个给定的字符串调用它时,它应该返回一个指向第一个单词的指针。(如果没有,则返回nullptr。)然后,每次你传入nullptr,它会给出你之前使用的字符串中的下一个单词——用完时返回nullptr

    您将需要上一个练习中的contains函数。您还需要一个static局部变量。

    就像编译器的strtok一样,您可以随心所欲地修改输入字符串。通常的方法是将'\0'放在您希望当前返回的令牌结束的地方,覆盖一个空白字符。

这实际上是size_t:源自unsigned int的一种类型,用于变量的大小,尤其是数组。它不是内置的,但是包含在各种头文件中——包括cstring——我们在这里不使用,所以我现在忽略它。

2

还有另一组函数——这是标准的一部分——试图阻止你在字符数组中走得太远:

char* strncpy (char *destination, const char *source, size_t n);

char* strncat (char *destination, const char* source, size_t n);

...

当您复制n字符时,这些功能会退出。我很愿意使用它们,但是它们也可能会出错:如果source太长,它会在destination的末尾加上 no ’\0’,打印它会得到一些奇怪的结果。

在第十七章,我们将得到一个更好、更安全的处理字符串的方法。

3

缓冲区:临时存储器,尤其是用于 I/O。

4

但是数组不会记录它们的大小,不管它们是如何被分配的,这是真的吗?没错,但是在编译时,编译器知道并且能够设置好,所以cin >>使用了正确的界限——这是一个高级的话题。

**

十五、类

到目前为止,我们所介绍的基本上都是 C 语言,只是做了一些调整,特别是cincout。现在是时候添加在 C++ 中放置+的东西了:类。他们不会给我们新的能力,比如cin / cout、SSDL 函数,或者像循环这样的控制结构。他们做的是帮助我们保持事情有条理,这样我们就不会混淆;尽量减少错误;让表达事情变得更简单——这样我们就可以信任我们写的代码,并将其用于更大更好的项目。

例如,这里有一个存储日历日期的类类型:

class Date
{
    int day_;
    int month_;
    int year_;
};
...
Date appointment; // Variables of a class type are called "objects"
                  // Using the term makes you sound smart at job interviews

我们可以用一个struct来完成。就像使用struct s 一样,我们可以声明这种类型的变量,将它们作为参数传递,使用.(点号)来获取部分,等等——看起来非常相似。但是我们即将找到重用代码和避免错误的新方法。

第一种方式:我不想让day_month_year_在程序的任何部分都可用;他们可能会搞砸。我将允许某些函数访问它们,称为成员函数

这个安全措施来自一个可能更容易记住的隐喻。考虑物理世界中的物体。一个物体——比如说,一个橡胶球——有一些特征:也许它是红色的,有弹性,有一定的质量和成分。你不能只是在你想要的时候把那些特征设置成你想要的。可以把真球上的色域设置成蓝色吗?叫它轻如鸿毛?相反,对象本身提供了与它交互的方式。不能设置色域,但是可以上色。你不能直接改变它的质量,但是你可以做一些事情来改变它的质量,比如切掉它或者烧掉它。你不能把它的位置设置成 90 公里直上,但你可以扔出去,看它能飞多远。

我们将以同样的方式创建我们的类:用特征(成员变量)和方法(成员函数)与这些特征进行交互。

那么,如何处理Date才是合适的呢?首先,您可以打印它:

class Date

{
public:
    void print (ostream&);1

private:
    int day_;
    int month_;
    int year_;
};

...

Date appointment;

...

appointment.print (cout); // or we could pass in a file variable -- see //   Chapter 13

public 部分是为外界(比如main)可以访问的东西准备的:也就是说,main可以告诉一个Date打印自己。私有部分是只有Date可以直接访问的部分。(如不注明,均为私人。) 2

一个类的 BNF 大致是

class <name>
{
public:
  <function declarations, variables, and types;
  usually declarations, almost never variables>
private:
  <function declarations, variables, types; usually variables>
};

看前面的print调用。为什么我们不告诉appointment日、月和年?它已经知道-它包含了它们!它不知道我们是想打印到cout还是一个文件,所以我们必须告诉它。

我还没说怎么打印呢。以下是如何:

void Date::print (std::ostream& out)
{
    out << day_ << '-' << month_ << '-' << year_;
}

Date ::”告诉编译器,“这不仅仅是一个名为print的函数——它是属于Date的函数。”

当你调用它的时候——appointment.print (cout);——它会打印谁的day_appointment夏侯惇

如果你正在使用一个友好的编辑器,比如微软的 Visual Studio,当你输入appointment和一个句号(见图 15-1 ,编辑器会列出可用的成员函数——到目前为止,只有print,但是我们很快会有更多的。你可以点击一个,它会为你粘贴。加上开头部分,它会提醒你它期望什么样的论点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15-1

Microsoft Visual Studio 提示函数声明信息

Date::也一样。它会列出可用的成员。

如果没有,不用担心;有时编辑会感到困惑。

构造器

我们已经知道初始化变量是明智的。在类中,我们有一个特殊类型的函数叫做构造器(常见的缩写是“ctor”)来做这件事(见例子 15-1 中突出显示的部分)。

// A program to print an appointment time, and demo the Date class
//   ...doesn't do that much (yet)
//       -- from _C++20 for Lazy Programmers_

#include <iostream>

using namespace std;

class Date
{
public:
    Date (int theDay, int theMonth, int theYear); // constructor declaration

    void print (std::ostream& out);

private:
    int day_;
    int month_;
    int year_;
};

Date::Date (int theDay, int theMonth, int theYear) :     // ...constructor //   body
    day_ (theDay), month_ (theMonth), year_ (theYear)
    // theDay is the parameter passed into the Date constructor
    //   function.  day_ is the member that it will initialize.
{
}

void Date::print (std::ostream& out)
{
    out << day_ << '-' << month_ << '-' << year_;
}

int main ()
{
    Date appointment (31, 1, 2595);3

    cout << "I'll see in you in the future, on ";
    appointment.print (cout);
    cout << " . . . pencil me in!\n";

    return 0;
}

Example 15-1The Date class, with a constructor, and a program to use it

构造器的名字总是和它的类一样。当你声明一个类Date的变量时,它调用这个函数来初始化成员变量。(没有返回类型;本质上,构造器“返回”对象本身。)

函数定义的第二行

day_ (theDay), month_ (theMonth), year_ (theYear)

告诉它将day_初始化为等于theDay等等。当我们到达{}的时候,已经没什么可做的了,所以这次{}是空的。

相反,您可以在函数体中初始化,使用=:

Date::Date (int theDay, int theMonth, int theYear)
{
    day_ = theDay; month_ = theMonth; year_ = theYear;
}

然而,带有()的成员初始化语法更常见,更不容易出错(参见反调试部分),并且在某些情况下是必要的,所以现在养成这种习惯是比较懒惰的。

为了形象化成员函数如何与数据成员交互,考虑示例 15-1 中发生的情况。main的第一个动作是为appointment分配空间,调用其构造器(图 15-2 ),传入参数。我把构造器画在了main之外,因为它一个单独的函数……但它是appointment的一部分,所以我用虚线把它和数据成员统一起来。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15-2

调用Date构造器

构造器将theDay复制成day_theMonth复制成month_theYear复制成year_(图 15-3 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15-3

Date构造器初始化appointment的数据成员

完成后,构造器离开(图 15-4 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15-4

Date构造器完成

这显示了(例如)成员day_和构造器参数theDay的不同角色:day_是持久的并且记住你的约会的日期部分;theDayDate::Date用来将信息从main传递到day_的参数,当构造器Date::Date完成时,该参数消失。

此后,main继续,打印"I'll be getting up at ",然后进入appointmentprint功能,该功能了解day_month_year_(图 15-5 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15-5

调用appointmentprint函数

Golden Rule of Member Function Parameters

不要传入对象的数据成员。该函数已经知道它们。

防错法

  • **构造器被调用,但数据成员从未被初始化。**如果我们使用这个构造器

    Date::Date (int theDay, int theMonth, int theYear)
    {
         theDay   = day_;
         theMonth    = month_;
         theYear   = year_;
    }
    
    

也许我们会得到奇怪的输出

I'll see in you in the future, on -858993460--858993460--858993460...pencil me in!

我已经将day_theDay交换了位置,所以我正在从复制我想要初始化的数据成员(显然其中有-858993460——有未初始化的变量,你永远不知道)有我想要的值的参数(图 15-6 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15-6

一个完全颠倒的构造器

如果你在{前使用()的初始化方法,就不会发生这种情况,例如 15-1 。如果你试图初始化错误的东西,它会报告一个错误。

Exercises

  1. 写一个Time类,用于记住早上什么时候起床,什么时候午睡等等。包括相关的数据成员、打印函数以及适当的构造器。

  2. 编写并测试一个函数Time currentTime()。它会调用time(就像我们初始化随机数生成器一样),获取 1970 年 1 月 1 日以来的秒数。我们只关心从午夜开始的秒数。将其转换为秒、分和小时,并返回当前的Time当前不是的成员Time

  3. 像前一个问题一样,用调用time的函数Date currentDate ()来扩充Date程序,并获得当前的Date。该功能是而不是Date成员。我之前假设时间从 1970 年 1 月 1 日开始,在你的机器上正确吗?

  4. (更难)添加一个函数Date::totalDays (),该函数返回自公元前 1 年 12 月 31 日以来的天数。你需要处理闰年。项目2-date-bestSoFar中的示例代码中有一个解决方案。

  5. (更难)添加一个函数Date::normalize (),如果Date有一个或多个字段超出范围,该函数将进行修正:例如,Date tooFar (32, 12, 1999);将使tooFar成为日期 1-1-2000。它应该由构造器调用。一个解决方案就在本书的样本代码项目2-date-bestSoFar中。

有没有更简单的方法写normalizetotalDays

const对象,const成员函数…

考虑以下代码:

const Date PEARL_HARBOR_DAY (7, 12, 1941);

cout << "A date which will shall live in infamy is ";
PEARL_HARBOR_DAY.print (cout);
cout << ".\n";

认为PEARL_HARBOR_DAY是一个常量是合理的,因为它永远不会改变(除非你有一台工作的时间机器)。然而,如果我们把它设为const,代码将不再编译。为什么不呢?

C++ 区分了能改变对象的成员函数和不能改变对象的成员函数。这是防止错误的一种方法。如果print是那种可以改变Date的东西,我们就不应该允许它在常量Date上被调用。

由于print 对于const对象来说是安全的,我们将这样告诉 C++:

class Date
{
   ...

   void print (std::ostream&) const;

   ...
};

void Date::print (std::ostream& out) const
{
   out << day_ << '-' << month_ << '-' << year_;
}

()后面的单词const告诉编译器这个函数可以用于常量对象。它还告诉编译器,在编译print时,如果对数据成员做了任何更改,就会产生一个错误。

如果你遇到很多错误,有时很容易将单词const从你的程序中完全删除。抵制这种诱惑。这个特性确实可以保护我们免受真正的错误。

防错法

  • 你会得到关于从转换到const的错误。这通常意味着您忘记了成员函数声明末尾的const(以及其函数体的第一行)。

  • 它说你的成员函数体和声明不匹配,但是它们看起来确实一样。检查他们都是或者都不是。

…和const参数

假设我们想将一个Date传递给一个函数fancyDisplay,它以一种可爱的方式打印时间:

void fancyDisplay (Date myDate, ostream& out)
{
   cout << "*************\n";
   cout << "* "; myDate.print (out); cout << " *\n";
   cout << "*************\n";
}

我没有用&来表示myDate,所以myDate本身不是传入的,而是一个副本。

在某种程度上,这很好,因为我们不想改变myDate。但是复制的成本比仅仅int要高——三倍多,因为它有三个int。当我们创建更大的类时,我们可能会发现它会减慢我们的程序。

以下是部分解决方案:

void fancyDisplay (Date& myDate, ostream& out);

这并不完美,因为现在我们允许fancyDisplay改变myDate!这更好:

void fancyDisplay

(const Date& myDate, ostream& out);

现在fancyDisplay不会花时间去复制myDate也改不了。

*Golden Rule of Objects as Function Parameters

如果你想改变一个作为参数传入的对象,把它作为TheClass& object传递。

如果没有,作为const TheClass& object传递。

多个构造器

没有必要将我们自己局限于一个构造器。我们可能想用其他方式来创造Date s:

Date d (21, 12, 2000);         // using our old constructor...
Date e (d);                    // e is now exactly the same as d
Date f;                        // now one with no arguments
Date dateArray [MAX_DATES];    // still no arguments
Date g (22000);                // 22,000 days -- nearly a lifetime

让我们一个一个来看。

复制构造器

Date的复制构造器将另一个Date作为唯一参数。我们称之为这个是因为它复制了(咄):

Date::Date (const Date& other) :     // "copy" constructor
    day_(other.day_), month_(other.month_), year_(other.year_)
{
}

这份宣言使用了它

Date e (d);

这个也是

Date e {d};

还有这个:

Date e = d; // Looks like =, but it's really calling the copy ctor

这种形式被称为“语法糖”:让代码更具可读性的不必要的东西。

复制构造器还有一些特殊之处。如果 C++ 需要复制一个Date,它会隐式地调用**,也就是说,不用你告诉它。这里有两个例子:**

void doSomethingWithDate (Date willBeCopied);
                           // I'd rarely do this, but if I did...
Date currentDate ();       // No &, so it returns a copy

不写复制构造器怎么办?C++ 会对如何复制做出最好的猜测,而这种猜测有时是危险的错误。一个好的规则:总是指定复制构造器

默认构造器

Date::Date () :     // "default" ctor

    day_ (1), month_ (1), year_ (1)    // default is Jan 1, 1 AD
{
}

...

Date f; // or: Date f {};
Date dateArray[MAX_DATES];

如果你不知道如何初始化你的Date,你什么也不告诉它,它使用它的默认构造器:不带参数的那个。它还需要这个来初始化日期数组的元素。 4 所以我们总是写默认的构造器。

转换构造器

此构造器调用

Date g (22000); // or Date g {22000}; -- or Date g = 22000;

需要将这些太多的日子转换成更传统的日、月、年安排。我将使用前面练习 5 中的函数normalize;如果我们给它 22,000 天,它会将其转换为 26 天、3 个月和 61 年:

Date::Date (int totalDays) :    // conversion ctor from int
    day_ (totalDays), month_ (1), year_ (1)
{
    normalize ();
}

由这个函数和任何需要它的构造器调用的normalize函数应该放在私有部分。函数会在需要的时候调用它,所以其他人不需要。 5

一个只有一个参数的构造器(不是一个Date))被称为转换构造器,因为它从其他类型(比如int)转换成我们正在编写的类。

就像复制构造器一样,如果 C++ 需要一个Date,但是你给了它一个int,它会隐式地调用它。假设你调用了void fancyDisplay (const Date& myDate, ostream& out);,但是传入了一个int : fancyDisplay (22000, cout);。C++ 会把 22000 转换成一个Date,把fancyDisplay转换成那个Date。很好!

摘要

每个构造器都有责任确保数据成员处于某种可接受的状态。不幸的是,C++ 的基本类型让你不用初始化就可以声明它们。但是我们可以构造我们的类,这样 C++ 可以初始化所有的数据成员。

由于前面提到的问题,我推荐以下准则:

Golden Rule of Constructors

总是指定默认复制构造器。

Extra

使用{}来初始化变量和老式的()的 and =:

Date g (22000.5);   // No problem: casts from int to double

Date g {22000.5};   // WRONG: "narrowing" conversion loses data                    //   -- compile-time error

这可能会防止我们无意中做一些愚蠢的事情。

此外,如果你不使用()的,你不能混淆使用()的调用构造器(如在Date d (21, 12, 2000);中)和使用()的函数声明——参见下面的反调试部分。

让事情变得更复杂的是,还有initializer_list s,它支持将{}用于另一种类型的构造器(见第二十三章)。

我倾向于像我们已经做的那样使用{}——对于struct和数组初始化——部分是为了清晰。但是其他人,包括 C++ 创始人比雅尼·斯特劳斯特鲁普本人,建议所有初始化都使用{},原因如下。

防错法

  • You declared an object, using the default constructor, but the compiler doesn’t recognize it :

    Date z  ();
    z.print (); // Error message says z is a Date() (?)
                // or at least isn't of a class type
    
    

    使用()会让编译器认为你在为返回Date的函数z写声明。我是说,它怎么能说不是这个意思呢?

    The solution is to ditch the parens or replace them with {}'s:

    Date z1;
    Date z2 {};
    
    
  • **“非法复制构造器”或“无效构造器”**带此复制构造器声明:Date (Date other);

    假设编译器让你调用这个函数。因为没有&,它要做的第一件事就是复制other。怎么做?通过调用复制构造器,这意味着它必须复制一个other, w,这意味着它调用复制构造器,等等,直到你用完内存。

这就是偶然递归,也就是一个函数在你不希望的时候调用自己。还好编译器(Visual Studio 或 g++)捕捉到了这个问题。解决方法:const &

代码重用的默认参数

我们可以通过告诉 C++ 来节省更多的工作,如果我们不给我们的一些函数参数,它应该适当地填充它们。

比如我已经厌倦了在myDate.print (cout);中指定cout。一般不都是cout吗?但是我不想将cout硬编码到函数中,因为我以后可能想要打印到一个文件中。

所以我改声明:void print (ostream& out = cout) const;

现在我可以只说myDate.print();,编译器会想,他没说,所以他一定要 cout

我有一个带三个int的构造器,一个带一个,一个不带任何东西。如果我使用默认值,我可以将它们合并成一个函数:

class Date
{
public:
    Date (int theDay=1, int theMonth=1, int theYear=1);
        // Defaults go in the declaration
   ...
};

...

Date::Date (int theDay, int theMonth, int theYear) :
   day_ (theDay), month_ (theMonth), year_ (theYear)
{
   normalize ();
}

现在我可以用 0 到 3 个参数来调用它:

Date Jan1_1AD;
Date convertedFromDays   (22000);
const Date CINCO_DE_MAYO (5, 5);         // Got a new one free that takes //   day & month
Date doomsday            (21, 12, 2012); // Doomsday? Well, that didn't //   happen

默认参数也适用于非成员函数:

void fancyDisplay (const Date& myDate, ostream& out = cout);
                   //if not specified, print with cout

如果有些参数有默认值,而有些没有,那么那些有默认值的参数会放在最后。编译器不想混淆你打算省略哪些参数。

Date程序(目前为止)

这里是我们所拥有的(例子 15-2 )。

我添加了一个枚举类型Month和对isLeapYear以及另外两个函数的声明。他们与日期有关,但他们不是Date的成员。

我可以让Month成为会员。但是为了引用(比如说)JUNE,我不得不写Date::Month::JUNE……那已经长得离谱了。

isLeapYear成为Date的成员没有意义:不是你对一个Date做的事,而是你对一年int做的事。它不需要访问Date的数据成员day_month_year_。它属于 Date,但我不会让它成为成员。

Tip

如果一个函数不能被合理地认为是对一个对象做了一些事情(包括“暴露它的秘密”,像一个访问函数),它可能就不应该是一个成员。

main的目的是测试类和相关的函数。当设计一个新的类时,这是一个基本的实践(在第十七章和第十九章中再次展示,在这两章中,我们在使用新的类来创造一些有趣的东西之前测试它们)。

// A program to test the Date class
//         -- from _C++20 for Lazy Programmers_

#include <iostream>

using namespace std;

enum class Month {JANUARY=1, FEBRUARY, MARCH, APRIL, MAY, JUNE,
                  JULY, AUGUST, SEPTEMBER, OCTOBER, DECEMBER};

bool isLeapYear   (int year);
int  daysPerYear  (int year);
int  daysPerMonth (int month, int year);
                              // We have to specify year in case month
                              //   is FEBRUARY and it's a leap year
class Date
{

public:
    Date (int theDay=1, int theMonth=1, int theYear=1);
                              // Because of default parameters,
                              //   this serves as a ctor taking 3 ints;
                              //   a new one taking days and months;
                              //   the conversion from int constructor;
                              //   and the default constructor

    Date (const Date&);       // copy constructor

    void print (std::ostream& out = std::cout) const;

    int totalDays   () const; // total days since Dec 31, 1 B.C.
private:
    int day_;
    int month_;
    int year_;

    void normalize  ();
};

Date::Date (int theDay, int theMonth, int theYear) :
    day_ (theDay), month_ (theMonth), year_ (theYear)
{
    normalize ();
}

Date::Date (const Date& other) :
     day_ (other.day_), month_ (other.month_), year_ (other.year_)
{
}

void Date::print (std::ostream& out) const
{
    out << day_ << '-' << month_ << '-' << year_;
}

// Date::totalDays and Date::normalize from earlier exercises
//   as well as isLeapYear, daysPerYear, and daysPerMonth
//   are omitted here, but they're in the book's sample code

void fancyDisplay (const Date& myDate, ostream& out = std::cout)
{

    cout << "*************\n";
    cout << "* "; myDate.print (); cout << " *\n";
    cout << "*************\n";
}

int main ()
{
    constexpr int MAX_DATES = 10;

    Date d (21, 12, 2000);          // using our old constructor...
    Date e = d;                     // e is now exactly the same as d
    Date f;                         // now one with no arguments
    Date dateArray [MAX_DATES];     // still no arguments
    Date g (22000);                 // 22,000 days, nearly a lifetime

    cout << "This should print 26-3-61 with lots of *'s:\n";
    fancyDisplay (22000);           // tests conversion-from-int constructor

    return 0;
}

Example 15-2A program using the Date class

Exercises

  1. 更新Time类以使用你在本章剩余部分学到的知识。

ostream&,不是ostream,因为iostream的设计者禁用了ostreams的复制,大概理由是没有意义。如果你忘记了,编译器会提醒你。

2

这是structclass的唯一区别:如果你不指定(我们总是这样做,对于类),class成员是私有的,struct成员是公共的。我们通常将struct用于小型、简单的分组,没有成员函数,一切都是公共的。那是因为struct s 在类被发明之前就已经存在于 C 中了,C 就是这样使用它们的。

3

也可以用{}表示法:Date appointment {2595, 1, 31};。它调用同一个构造器。

4

除非你用{}的,比如在Date myDates[] = {{31,1,2595},{1,2,2595}};里。

5

私有部分中的函数被称为“实用”函数,因为它们执行对其他公共函数有用的任务。我们并不真的需要这个术语,但使用它让我听起来很聪明,所以我当然会使用它。

***

十六、类·续

更多的事情使你的类工作,并且工作得很好,特别是把你的程序的一部分放在多个文件中。

inline提高效率的功能

考虑一下第八章和第十五章的函数调用图。它们展示了计算机的功能。它创建函数的新副本,即“激活记录”,包含函数实例需要的所有内容,尤其是局部变量。它将参数复制到内存中该函数可以访问的部分。它存储它需要知道的关于它所在函数的信息(CPU 中寄存器的状态——如果你不知道那是什么,不用担心)。最后,它将控制权转移给新功能。

完成后,它反转这个过程:丢弃函数及其变量的副本,并恢复旧函数的状态。

在运行时,计算机上的工作量很大。那我们该怎么办?停止使用函数?

解决方案是内联函数。一个内联函数被写成一个函数,就程序员而言,它的行为就像一个函数,并且看起来像是作为一个函数编译的,但是编译器做了一些偷偷摸摸的事情:它用一段代码代替函数调用来做同样的事情。这里有一种方法可以让一个函数内联——只要在它前面加上inline:

inline
void Date::print (std::ostream& out) const
{
   out << day_ << '-' << month_ << '-' << year_;
}

当你写这个的时候

d.print (cout);

编译器会像你说的那样处理它

cout << d.day_ << '-' << d.month_ << '-' << d.year_;
    // but there's no problem with these members being private

从而节省了函数调用的开销。

如果一个函数足够大,那么调用的时间开销与花在函数本身上的时间相比并不显著,所以帮助不大。并且inline引入了新的开销:内联扩展的大函数的多个副本将占用大量内存。以下是如何知道是否应该内联一个函数。

Golden Rule

inline功能

**一个函数应该是内联的,如果它

  • 适合单行。

  • 不包含循环(for、while 或 do-while)。

扩展仅仅是对编译器的一个建议,而不是命令。如果编译器认为函数不应该扩展,它会否决你。我没意见;在这种情况下,编译器最清楚。

这里有一个快速、简单的方法来使成员函数内联——将整个事情放在类定义中:

class Date
{
    ...

   void print (ostream& out) const
           // inline, because it's inside the class definition
   {
        out << day_ << '-' << month_ << '-' << year_;
   }

   ...
};

访问功能

有时我们希望世界上的其他人能够看到我们的数据成员,但不能改变它们。这就像一个时钟:你必须通过适当的控件(成员函数)来设置它,但是你可以随时看到时间。

事情是这样的:

class Date
{
public:
   ...

    // Access functions -- all const, as they don't change data, just //   access it
    int day     () const { return day_;   }
    int month   () const { return month_; }
    int year    () const { return year_;  }

    ....
};

调用它们的方法与使用print相同——使用.:

cout << myBirthday.year () << " is the year of the lion. Fear me.\n";

使用访问函数通常是个好主意,甚至在成员函数中。假设我决定转储day_month_year_,并且只有一个数据成员totalDays,成员函数可以根据需要从中计算日、月和年。引用day_的函数将不再编译!但是如果它指的是仍然存在的days(),那就好了。

这就是为什么我们在数据成员名称:day_和其他之后使用下划线。这就是为什么我用了有趣的名字theDaytheMonth等等,作为我写的第一个构造器的参数。如果我这么做了

class Date
{
public:
    Date(int day, int month, int year) :
        day(day),month(month),year(year)
    {
    }
    int day() const { return day; }
    ....
private:
    int day, months, years;
};

我有太多的东西要命名,我永远也不会把它们分类! 1 也不会编译。名称必须不同。

单独的编译和包含文件

现在我们的程序足够长了,我们应该把它们分成多个文件。这里有一些通用指南,这样你就知道在哪里可以找到东西了:

  • 我们通常给每个类一个自己的文件。

  • …让每一组明确相关的函数共享一个文件。例如,如果你在写三角函数正弦、余弦、正切等等,你可以把它们放在一起。

  • 我们给main它自己的文件,可能与main调用的函数共享,这些函数对其他程序没有用。比方说,如果你正在为扑克写一个程序,与叫牌相关的函数可能会放在main的文件中(因为只有扑克才进行扑克式的叫牌),但是与洗牌和发牌相关的函数会放在其他地方(因为许多游戏都涉及到牌)。

然而,就让这一切发挥作用而言,还有一个问题。这个新文件需要知道某些事情(比如类定义!)—main也会如此。这些信息必须共享。

幸运的是,我们已经知道如何做到这一点:包含文件。

在单独编译中会发生什么

假设我创建了这些文件:myclass.h(如“header”中的“h”),包含类定义;myclass.cpp,包含成员功能;和main.cpp,包含主程序。(我给.h.cpp文件起了和类一样的名字,小写。习俗各不相同;保持一致。)我是这样收录的:

#include "myclass.h" // <-- Use "" not <>; and let the file end in .h

下面是编译器构建程序的几个阶段。

首先,它编译c++“源”文件(你的.cpp文件;见图 16-1 。当它遇到一个#include指令时,它停止读取源文件,读取你包含的.h文件,然后返回源文件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16-1

构建程序的编译阶段

编译器用机器语言为每个源文件生成一个“目标”文件。

如果没有错误,编译器准备好链接(图 16-2 )。目标文件知道如何做它们要做的事情,但是它们不知道在哪里可以找到函数引用,无论是从彼此之间还是从系统库中。链接阶段通过解析引用将这些文件“链接”在一起,并生成一个可执行文件。如果您使用的是 Visual Studio,可执行文件将以.exe结尾;g++ 是灵活的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16-2

构建程序的链接阶段

看到这个过程使我们能够准确地理解什么应该和不应该进入包含文件。

写你的。h 文件

以下是包含文件中可以包含的内容(到目前为止):

  • 类型,包括类定义和枚举类型

  • 函数声明

  • 任何事情

以下是不应该的:

  • 功能

  • 常量或变量声明(除了inline–继续阅读)

原因如下。如果你把一个函数(或变量声明)放在一个包含文件中,它将被包含到不同的.cpp文件中。当你编译这些文件时,你会得到同一个函数的多个副本。当你调用这个函数的时候,编译器不知道使用哪个副本,也不知道它们是相同的。你会得到一个错误,说它有个重复的定义

如果你想让你的函数出现在包含文件中…就把它设为inline

如果您想在包含文件中包含变量、const s 或constexprs…也让它们成为inline:

inline constexpr int DAY_PER_WEEK = 7;
inline const SSDL_Color  BABY_BLUE = SSDL_CreateColor (137, 207, 240);2

不仅仅是为了提高函数调用的效率——它还防止了重复定义的问题。也许不是最清晰的关键字,但是allowInIncludeFilesWithoutDuplicateDefinitionError太难输入了。

只包含一次. h 文件

假设time.h (来自第十五章的习题;它定义了新的类Stopwatch需要类Time。我们需要#include "time.h",这样我们就可以声明start_stop_:

// stopwatch.h: defines class Stopwatch
//        -- from _C++20 for Lazy Programmers_

#include "time.h"                // trouble ahead...

class Stopwatch
{
public:
    Stopwatch () {}
private:
    Time start_, stop_;
};

然后我们到了main.cpp

// Program that uses Stopwatches and Times
//       -- from _C++20 for Lazy Programmers_

#include "time.h"
#include "stopwatch.h"

int main (int argc, char** argv)
{
    Time duration;
    Stopwatch myStopwatch

;

    // ...

    return 0;
}

Example 16-1A program that includes time.h and stopwatch.h. Since this and Example 16-2 work together, they’re in the same project in source code’s ch16 folder; it’s named 1-2-stopwatch

编译时main.cpp

首先,编译器包含了time.h,它定义了类Time

然后包括stopwatch.h。做的第一件事*是#include "time.h",它定义了类Time。*又来了。编译器抱怨:类Time的重复定义!

解决方案是告诉编译器只在还没有被读取的时候才读取一个.h文件。有一个常用的技巧:在.h文件中定义一些东西;然后在整个文件周围放些东西,说“如果你从来没听说过,就只看这个。”

// time.h: defines class Time
//        -- from _C++20 for Lazy Programmers_

#ifndef TIME_H // If TIME_H is not defined...
#define TIME_H

class Time
{
    // ...
};

#endif //TIME_H

Example 16-2time.h, written so it will only be processed once. Part of the 1-2-stopwatch project in source code’s ch16

第一次通过时,它从未听说过TIME_H,所以它读取了.h文件。这定义了类别TimeTIME_H

下一次,它听说过TIME_H,所以它跳到#endif。阶级Time没有被重新定义。任务完成。

为了防止这个问题,我对所有的包含文件都这样做。对常量(MYFILE_H)使用相同的形式意味着我总是记得我如何拼写它,并防止名称冲突。

避免包含文件中的using namespace std;

using namespace std ;不应该在你的包含文件里。如果有人包含了您的文件,但不想使用std名称空间怎么办?为了避免强加给他们,跳过using宣言。为了让 C++ 仍然能够识别cincout以及std名称空间中的其他东西,在它们前面加上std::,就像在std::cin >> x;中一样。

备份多文件项目

在 Unix 中,要备份目录myproject,请输入以下命令:cp -R myproject myprojectBackup1

在 Windows 中,复制并粘贴整个文件夹,忽略不会复制的内容。

防错法

循环包括制造一个奇怪的错误。让我们修改time.h,这样它就需要Stopwatch:

#include "stopwatch.h"

class Time
{
    void doSomethingWithStopwatch (const Stopwatch&);
};

假设某个.cpp文件包含了time.h。这定义了TIME_H,然后包括stopwatch.h(图 16-3 ,左)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16-3

time.h包括stopwatch.h,其中包括time.h——会有麻烦的

所以它暂时停止读取time.h,读取stopwatch.h(图 16-3 ,中间)。这定义了STOPWATCH_H,然后又包含了time.h(图 16-3 ,右图)。

因为已经定义了TIME_H,所以#ifndef让我们跳过内容。我们回到stopwatch.h,当它到达第Time start_, stop_;行时,它对我们大喊它从来没有读过Time的定义,这是真的。所以程序不会编译。

一个包含文件可以包含另一个——但是它们不能包含彼此的*。*

*一些修复:

  • 重新考虑这个功能是否应该在Time中。Time真的要靠Stopwatch吗?难道不应该反过来吗?(那是这个代码的最佳答案。)

  • 如果那样不行…只要代码不需要细节,你可以在不知道是什么的情况下参考Time中的Stopwatch。只告诉time.hStopwatch是一个班:

    class Stopwatch;
    class Time
    {
           void doSomethingWithStopwatch (const Stopwatch&);
    };
    
    

问题解决了。

下一个问题:如果你有很多文件,却不记得把其中一个函数放在哪里,该怎么办?

  • Visual Studio:右击函数名。“转到申报”将带您到申报;“转到定义”将带你到函数本身,如果它可用的话。

  • Unix:尽管有一些软件包可以帮助解决这个问题(emacs 的 ggtags 就是其中之一),但不能保证它们就在您的系统上。这个命令是一个快速的 3 的方法来找到函数和对它的所有引用:grep functionIWant *

  • MinGW:我用 Windows Grep——在网上找——来搜索函数名。

Microsoft Visual Studio 中的多文件项目

**要添加新文件,**进入项目菜单,选择添加新项目。您可以通过这种方式添加任何您需要的.h.cpp文件;它会把它们放在正确的地方。

然后像往常一样构建并运行您的项目。

Extra

现在您已经有了多个源文件,您可能想要一种更简单的方法来清理 Visual Studio 创建的额外文件。 4

在项目所在的文件夹中,使用记事本或其他编辑器创建一个文件clean.txt。把它放在一个文件夹里,里面只有你的工作(也许还有我的),没有任何不可替代的东西。这里面应该有些什么:

REM Erase folders you don't want -- here's my picks
for /r . %%d in (Debug,.vs) do @if exist "%%d" rd /s/q "%%d"
REM Erase other files -- here's my picks.
REM /s means "in subfolders too"
del /s *.obj    REM Not needed, but now you know how to
                REM   erase all files with a particular extension

保存您的文本文件,并将其名称从clean.txt改为clean.bat。(看不到.txt?取消选中隐藏已知文件类型的扩展名–参见第一章。)在图 16-4 的警告对话框中点击是。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16-4

Microsoft Windows 关于更改文件扩展名的警告

…并获取新的clean.bat文件。

每当您想要抹掉额外的文件时,您可以双击这个“批处理”文件,即一个命令文件。被警告:del永久删除东西。 5

g++ 中的多文件项目

要在 g++ 项目中使用多个文件,只需将它们添加到从basicStandardProject(或basicSSDLProject)复制的项目文件夹中。make将照常建造一切。

不管有没有自己的 Makefiles,都要自己做,请继续阅读。

命令行:多打字,少思考

您可以使用以下命令构建程序:

g++ -g -std=gnu++2a -o myprogram myprogram.cpp myclass.cpp

您可以将编译和链接阶段分开:


g++ -g -std=gnu++2a -c myprogram.cpp   #-c means "compile only -- don't link
g++ -g -std=gnu++2a -c myclass.cpp
g++ -g -std=gnu++2a6 -o myprogram myprogram.o myclass.o #now link

Makefiles:多思考,少打字(可选)

Makefiles 跟踪项目中已更改的文件。当你make的时候,它只会重建它需要的部分。这减少了编译时间。输入make比输入g++ -g -o myprogram file1.cpp file2.cpp...更好

Makefiles 并不容易,但对于大型项目或使用大量库的项目来说,它们是必不可少的。本节将向您展示如何制作它们,从最简单的版本(例如 16-3 )到最复杂但也是最常用的版本(例如 16-5 )。

简单的版本
#This is a basic Makefile, producing one program from 2 source files

myprogram:   myclass.o main.o       #link object files to get myprogram
      g++ -std=gnu++2a -g -o myprogram myclass.o main.o

main.o:      main.cpp myclass.h     #create main.o
      g++ -std=gnu++2a -g -c main.cpp

myclass.o:   myclass.cpp myclass.h  #create myclass.o
      g++ -std=gnu++2a -g -c myclass.cpp

clean:
      rm -f myprogram           # for Unix; with MinGW, rm -f myprogram.exe
      rm -f *.o

Example 16-3A simple Makefile. It’s in source code, ch16/3-4-5-makefiles, as Makefile.Ex-16.3\. To use it, copy it to Makefile and type make

第一行是注释,因为它以#开头。

为了简单起见,我将把事情打乱。这条线

main.o:   main.cpp myclass.h

说要编译 main 的.o(对象)文件,你需要main.cppmyclass.h。如果其中任何一个发生变化,make就会重建main.o。(make根据文件的修改时间检测更改。)

下一行g++ -std=gnu++2a -g -c main.cpp是编译它的命令。如果失败,make会停止,以便您可以纠正错误。

myclass.o的理解是一样的。

让我们回到顶部:

myprogram:   myclass.o main.o
             g++ -std=gnu++2a -g -o myprogram myclass.o main.o

这确定了myprogram依赖于myclass.omain.o,并告诉如何创建它。

因为这是 Makefile 中的第一件事,所以当你键入make时,这就是计算机试图构建的东西。

clean很好:如果你说make clean,它会清除可执行文件和所有的.o文件。-f选项是这样的,如果没有错误,它不会报告错误,因为这不是问题。注意,Windows 将.exe附加到它的可执行文件中,所以 MinGW 版本的clean需要删除myprogram.exe

更好的版本

Makefile 的工作量太大了:我们必须指定每个.cpp文件以及它所依赖的.h文件。我们现在将创建一个 Makefile 文件,它应该适用于您将在本文剩余部分和其他地方遇到的大多数项目。

# Makefile for a program with multiple .cpp files

PROG   = myprogram             # What program am I building?
                               #  MinGW: make this myprogram.exe
SRCS   = $(wildcard *.cpp)     # What .cpp files do I have?
OBJS   = ${SRCS:.cpp=.o}       # What .o   files do I build?

$(PROG):  $(OBJS)              # Build the program
      g++ -std=gnu++2a -g -o $@ $^

%.o:      %.cpp                # Make the .o files
      g++ -std=gnu++2a -g -o $@ -c $<

clean:                         # Clean up files
      rm -f $(PROG)
      rm -f *.o

Example 16-4A Makefile for any project of .cpp source files – first attempt. It’s in source code, ch16/3-4-5-makefiles, as Makefile.Ex-16-4.unix and Makefile.Ex-16.4.mingw. To test, copy the one for your platform to Makefile and make

首先,我们定义一些变量。

之后,我们看到我们的程序依赖于目标文件(和以前一样)。注意变量用$()括起来。然后第一个 g++ 命令告诉我们如何从目标文件创建程序。

$@表示“上面的:左边的东西”,$^表示“:右边的一切”——即所有的对象文件。

产生.o文件的部分为每个.cpp文件制作一个。$<的意思是“下一个文件右边有什么*,在这种情况下,下一个.cpp文件。*

(如果你想知道如何使用这些看起来很奇怪的结构的一切,互联网是你的。如果你只是想找点有用的……互联网还是你的。我就是这么做的:查阅教程,看看什么能解决我的问题。)

要查看这些变量是如何被翻译成实际命令的,输入make——它会在执行命令时打印出命令。

完整的版本

Makefile 仍然有一个大错误(除了看起来像是用埃及象形文字写的)。它不引用任何.h文件。如果你改变了一个.h文件,make不会知道在此基础上重新编译——它应该知道。

所以我们在 Makefile 的末尾添加了一个神奇的咒语:

%.dep:   %.cpp                         # Make the .dep files
      g++ -MM -MT "$*.o $@" $< > $@

ifneq ($(MAKECMDGOALS),clean)          # If not cleaning up...
-include $(DEPS)                       #  bring in the .dep files
endif

第一部分说对于我们拥有的每个.cpp文件,我们需要一个.dep文件,它将包含依赖关系的信息。g++ -MM行生成它。main.dep文件可能看起来像这样

main.o main.dep: main.cpp myclass.h

意思是“每当main.cppmyclass.h改变时,重新制作main.omain.dep

-MT "$*.o $@"选项指定了:左边的内容——它应该包含相关的.o文件main.o,加上main.dep,这里指定为$@。我们放置main.dep的原因是,如果main.cppmyclass.h中有任何变化(比方说,我们添加另一个#include ), main.dep也会更新。

$<是相关的.cpp文件。

>表示将输出存储在一个文件中,特别是$@,这是我们正在创建的.dep文件。

include $(DEPS)表示将这些规则包含到 Makefile 中。最初的-表示如果有错误就不要报告,比如文件不存在,这将在clean之后第一次运行make时发生。而ifneq...说,如果你无论如何都要更新.dep文件,那就不要担心它们。

是的,这很复杂,但这是突破口。

这是我们的结果。它应该不加改变地为新项目工作;如果您想要不同的可执行文件名称,请更改myprogram

# Makefile for a program with multiple .cpp files

PROG     = myprogram             #What program am I building?
                                 # MinGW: make this myprogram.exe
SRCS     = $(wildcard *.cpp)     #What .cpp files do I have?
OBJS     = ${SRCS:.cpp=.o}       #What .o   files do I build?
DEPS     = $(OBJS:.o=.dep)       #What .dep files do I make?

##########################################################

all:        $(PROG)

$(PROG):    $(OBJS)                        # Build the program
      g++ -std=gnu++2a -o $@ -g $^

clean:              # Clean up files
      rm -f $(PROG)
      rm -f *.o
      rm -f *.dep

%.o:    %.cpp        # Make the .o files
      g++ -std=gnu++2a -g -o $@ -c $<

%.dep:  %.cpp        # Make the .dep files
      g++ -MM -MT "$*.o $@" $< > $@

ifneq ($(MAKECMDGOALS),clean)              # If not cleaning up...
-include $(DEPS)                           #  bring in the .dep files
endif

Example 16-5A complete Makefile. It’s in source code, ch16/3-4-5-makefiles, as Makefile.Ex16-5.unix and Makefile.Ex16-5.mingw. Copy the version for your platform into any folder where you have a project, naming it Makefile, and run by typing make

防错法

  • **Makefile:16: ***** 缺少分隔符 。停下来。

    不开玩笑:这是因为在指定的行上,你用空格而不是制表符缩进。解决方案:哼哼,翻白眼,或者其他什么,使用制表符。

最终Date程序

示例 16-6 至 16-8 显示了Date的完成程序,如前所述,该程序被分成文件。main是驱动程序,也就是设计用来测试类的程序。

// A "driver" program to test the Date class
//        -- from _C++20 for Lazy Programmers_

#include <iostream>
#include "date.h"

using namespace std;

int main ()
{

    Date t (5,11,1955); // Test the 3-int constructor

    // ... and print
    cout << "This should print 5-11-1995:\t";
    t.print (cout);
    cout << endl;

    // Test access functions
    if (t.day () != 5 || t.month () != 11 || t.year () != 1955)
    {
        cout << "Date t should have been 5-11-1955, but was ";
        t.print ();
        cout << endl;
    }

    Date u = t;         // ...the copy constructor
    if (u.day () != 5 || u.month () != 11 || u.year () != 1955)
    {
        cout << "Date u should have been 5-11-1955, but was ";
        u.print ();
        cout << endl;
    }

    const Date DEFAULT; // ...and the default constructor
                        // I do consts to test const functions
    if (DEFAULT.day  () != 1 || DEFAULT.month () != 1 ||
        DEFAULT.year () != 1)
    {
        cout << "Date v should have been 1-1-1, but was ";
        DEFAULT.print ();
        cout << endl;
    }

    // ...and total days
    constexpr int DAYS_FOR_JAN1_5AD = 1462; // I found this number myself
                                            //   with a calculator
    Date Jan1_5AD (1, 1, 5);
    if (Jan1_5AD.totalDays () != DAYS_FOR_JAN1_5AD)
        cout << "Date Jan1_5AD should have had 1462 days, but had "
             << DAYS_FOR_JAN1_5AD << endl;

    // Test normalization
    const Date JAN1_2000 (32, 12, 1999);
    if (JAN1_2000.day  () != 1 || JAN1_2000.month () != 1 ||
        JAN1_2000.year () != 2000)
    {

        cout << "Date JAN1_2000 should have been 1-1-2000, but was ";
        JAN1_2000.print ();
        cout << endl;
    }

    cout << "If no errors were reported, "
         << " it looks like class Date works!\n";

    return 0;
}

Example 16-8A driver program for class Date. It’s in source code, ch16, as part of the 6-7-8-date project/folder

// class Date -- functions
//        -- from _C++20 for Lazy Programmers_

#include "date.h"

bool isLeapYear (int year)
{

//...
}

int daysPerYear (int year)
{
    //...
}

int daysPerMonth (int month, int year)
{
    //...
}

void Date::normalize ()
{
    //...
}

int Date::totalDays () const
{
    //...
}

Example 16-7date.cpp, abbreviated for brevity. A complete version is in source code, ch16 folder, as part of the 6-7-8-date project/folder

// class Date
//       -- from _C++20 for Lazy Programmers_

#ifndef DATE_H
#define DATE_H

#include <iostream>

enum class Month { JANUARY=1, FEBRUARY, MARCH, APRIL, MAY, JUNE,
                   JULY, AUGUST, SEPTEMBER, OCTOBER, DECEMBER};

bool isLeapYear   (int year);
int  daysPerYear  (int year);
int  daysPerMonth (int month, int year);// Have to specify year,
                                        // in case month is FEBRUARY
                                        //  and we're in a leap year
class Date
{
public:
    Date(int theDay=1, int theMonth=1, int theYear=1) :
       day_(theDay), month_(theMonth), year_(theYear)
    {
        normalize();
    }
    // Because of its default parameters, this 3-param
    //  ctor also serves as a conversion ctor
    //  (when you give it one int)
    //  and the default ctor (when you give it nothing)

    // Default is chosen so that the default day
    //  is Jan 1, 1 A.D.

    Date(const Date& otherDate) : // copy ctor
           day_  (otherDate.day_  ),
           month_(otherDate.month_),
           year_ (otherDate.year_ )
    {

    }

    // Access functions
    int days        () const { return day_;   }
    int months      () const { return month_; }
    int years       () const { return year_;  }

    int totalDays () const; // convert to total days since Dec 31, 1 BC

    void print (std::ostream& out = std::cout) const
    {
        out << day_ << '-' << month_ << '-' << year_;
    }

private:
    int day_;
    int month_;
    int year_;

    void normalize  ();
};
#endif //DATE_H

Example 16-6date.h. It’s in source code, ch16, as part of the 6-7-8-date project/folder

输出是

This should print 5-11-1995: 5-11-1955
If no errors were reported, looks like class Date works!

为了对用户友好,如果一切正常,驱动程序给出尽可能少的输出——测试时需要费力处理的输出也越少。

Exercises

在这些练习中,使用单独的编译;为您的类提供适当的构造器,在有用的地方使用默认参数;以及相关的写访问函数和内联函数。

如果结果表明.cpp文件中没有任何内容(可能发生!),不需要写一个:

  1. 更新Time类以使用本章中所涉及的内容。

    添加一个常量Time MIDNIGHT,如果你的编译器支持的话,使用inline让它对main.cpp,可用。

  2. 添加Time函数sumdifference,返回这个Time与另一个Time的和/差。它的返回值也是一个Time

  3. 你想在你的墓碑上刻什么?创建一个Tombstone类,包含出生日期、死亡日期、姓名和墓志铭。除了一个成员函数print之外,给它一个lifespan函数,它返回这个人生命的持续时间作为一个Date

  4. 将示例 16-1 和 16-2 中的Stopwatch具体化,以使用本章涵盖的内容。还要添加函数startstop,它们启动和停止Stopwatch(您可能已经完成了第十五章第一组练习中的练习 2:将Time设置为当前系统时间),以及duration,它们返回差值。然后使用Stopwatch记录用户按 Enter 键的速度。

  5. 创建一个类Track,它包含一段音乐的标题、艺术家和持续时间(a Time)。

    现在创建一个类Album,它包含一个标题和一个Track的数组。在你知道你将需要的其他函数中,包括一个函数duration(),它是所有Track持续时间的总和。

或者我可以有一个数据成员days和一个访问函数getDays ();有些人使用这个惯例。我觉得《??》太难读了——你说了算。

2

如果由于某种原因(可能是不兼容的编译器)这种方法对您不起作用,您可以采用这种更麻烦的形式:

extern const 类型 myConstant//在中。h 文件

常量类型 myConstant =…;//在相应的。cpp 文件

extern 的意思是“这个变量将在别处被声明——你会在链接时找到它的位置。”

3

轻松,不优雅。

4

Visual Studio 中还有一个选项:构建➤清洁解决方案。不疼,但是在我的机器上留下了一个文件Browse.VC.db,可能超过 5 MB。所以我不依赖它。

5

如果使用来自教科书源代码的clean.bat,Windows 可能会给出“运行此应用程序可能会使你的电脑面临风险”的警告,并建议你不要这样做。Windows 是对的:clean.bat在错误的地方可能会擦除错误的东西。知道没关系,但是……自己做吧。

6

可能不需要在这个“link”命令中放入-STD = gnu++ 2a——但是万一发生变化,我会把它留在这里。

***

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值