Skip to content

Latest commit

 

History

History
233 lines (152 loc) · 10.2 KB

File metadata and controls

233 lines (152 loc) · 10.2 KB

四、字符串

在前面的探索中,您使用带引号的字符串作为每个输出操作的一部分。在这个探索中,你将开始学习如何通过对字符串做更多的处理来使你的输出更有趣。从阅读清单 4-1 开始。

import <iostream>;

int main()
{
   std::cout << "Shape\tSides\n" << "-----\t-----\n";
   std::cout << "Square\t" << 4 << '\n' <<
                "Circle\t?\n";
}

Listing 4-1.Different Styles of String Output

预测清单 中程序的输出 4-1 **。**你可能已经知道\t是什么意思了。如果是这样的话,这个预测很容易做出。如果你不知道,猜一猜。


现在检查你的答案。你是对的吗?那么 \t 是什么意思呢?


在字符串内部,反斜杠(\)是一个特殊的,甚至是神奇的字符。它改变了后面字符的含义。您已经看到了\n如何开始一个新行。现在您知道了\t是一个水平制表符:也就是说,它将随后的输出对齐到一个制表符位置。在典型的控制台中,每八个字符位置设置一个制表位。

应该如何打印字符串中的双引号字符?


写一个程序来测试你的假设,然后运行程序。你是对的吗?


将您的程序与清单 4-2 进行比较。

import <iostream>;

int main()
{
   std::cout << "\"\n";
}

Listing 4-2.Printing a Double-Quote Character

在这种情况下,反斜杠将特殊字符转换为普通字符。C++ 可以识别其他一些反斜杠字符序列,但这三个是最常用的。(当你读到《探索》中的角色时,你会学到更多。)

现在修改清单 4-1 以将三角形添加到形状列表中。

输出是什么样的?制表符不会自动对齐一列,而只是将输出定位在下一个制表符位置。要对齐列,您必须控制输出。一种简单的方法是使用多个制表符,如清单 4-3 所示。

 1 import <iostream>;
 2
 3 int main()
 4 {
 5    std::cout << "Shape\t\tSides\n" <<
 6                 "-----\t\t-----\n";
 7    std::cout << "Square\t\t" << 4 << '\n' <<
 8                 "Circle\t\t?\n"
 9                 "Triangle\t" << 3 << '\n';
10 }

Listing 4-3.Adding a Triangle and Keeping the Columns Aligned

我在列表 4-3 中捉弄了你。仔细观察第 8 行的结尾和第 9 行的开头。请注意,该程序缺少一个输出操作符(<<),该操作符通常用于分隔所有输出项。只要有两个(或更多)相邻的字符串,编译器就会自动将它们合并成一个字符串。这个技巧只适用于字符串,不适用于字符。因此,你可以用许多不同的方式写第 8 行和第 9 行,意思完全一样。

std::cout << "\nCircle\t\t?\n" "Triangle\t" << 3 << '\n';
std::cout << "\nCircle\t\t?\nTriangle\t" << 3 << '\n';
std::cout << "\n" "Circle" "\t\t?\n" "Triangle" "\t" << 3 << '\n';

选择你最喜欢的风格,坚持下去。我喜欢在每一个新行之后做一个清晰的分隔,这样阅读我的程序的人就可以清楚地区分每一行的结束和新的一行的开始。

您可能会问自己,为什么我要麻烦地分别打印数字,而不是打印一个大字符串。这个问题问得好。在真正的程序中,打印单个字符串是最好的,但在本书中,我想不断提醒您可以用各种方式编写输出语句。例如,想象一下,如果你事先不知道一个形状的名称和它的边数,你会怎么做。也许这些信息存储在变量中,如清单 4-4 所示。

 1 import <iostream>;
 2 import <string>;
 3
 4 int main()
 5 {
 6    std::string shape{"Triangle"};
 7    int sides{3};
 8
 9    std::cout << "Shape\t\tSides\n" <<
10                 "-----\t\t-----\n";
11    std::cout << "Square\t\t" << 4 << '\n' <<
12                 "Circle\t\t?\n";
13    std::cout << shape << '\t' << sides << '\n';
14 }

Listing 4-4.Printing Information That Is Stored in Variables

字符串的类型是std::string。你必须在程序的顶部附近有import <string>来通知编译器你正在使用std::string类型。第 6 行展示了如何给一个字符串变量一个初始值。有时,您希望变量以空开始。你认为如何定义一个空的字符串变量?



写一个程序来测试你的假设。

如果在验证字符串是否真的为空时遇到困难,请尝试在两个其他非空字符串之间打印该字符串。清单 4-5 给出了一个例子。

1 import <iostream>;
2 import <string>;
3
4 int main()
5 {
6    std::string empty;
7    std::cout << "|" << empty << "|\n";
8 }

Listing 4-5.Defining and Printing an Empty String

将您的程序与清单 4-5 进行比较。你更喜欢哪个?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

为什么呢?



第 6 行没有为变量empty提供初始值。您在 Exploration 3 中了解到,省略初始值会导致变量未初始化,这将是一个错误,因为没有其他值被赋给empty。不允许打印或访问未初始化变量的值。但是std::string不同。在这种情况下,缺少初始化式与空括号是一样的;也就是说,变量被初始化为一个空字符串。

当你定义一个没有初始值的字符串变量时,C++ 保证这个字符串最初是空的。修改清单 4-4 所以 shape sides 变量未初始化。预测程序的输出。



发生了什么事?解释一下。




你的程序应该如清单 4-6 所示。

 1 import <iostream>;
 2 import <string>;
 3
 4 int main()
 5 {
 6    std::string shape;
 7    int sides;
 8
 9    std::cout << "Shape\t\tSides\n" <<
10                 "-----\t\t-----\n";
11    std::cout << "Square\t\t" << 4 << '\n' <<
12                 "Circle\t\t?\n";
13    std::cout << shape << '\t' << sides << '\n';
14 }

Listing 4-6.Demonstrating Uninitialized Variables

当我运行清单 4-6 时,我会得到不同的答案,这取决于我使用的编译器和平台。大多数编译器会发出警告,但仍然会编译程序,所以你可以运行它。我得到的答案之一是这样的:

Shape          Sides
-----          -----
Square         4
Circle         ?
        4226851

用另一个平台上的另一个编译器,最后的数字是0。然而,另一个编译器的程序打印出最后的数字-858993460。有些系统甚至会崩溃,而不是打印出shapesides的值。

这难道不奇怪吗?如果没有为类型为std::string的变量提供初始值,C++ 会确保该变量以初始值开始,即空字符串。另一方面,如果变量的类型是int,你无法判断初始值实际上是什么,事实上,你甚至无法判断程序是否会运行。这就是所谓的未定义行为。该标准允许 C++ 编译器和运行时环境在遇到某些错误情况时做任何事情,绝对是任何事情,比如访问未初始化的变量。

C++ 的一个设计目标是,如果可以避免,编译器和库不应该做任何额外的工作。只有程序员知道什么值作为变量的初始值是有意义的,所以赋予初始值必须是程序员的责任。毕竟,当你正在对你的天气模拟器进行最后的润色时(这将最终解释为什么当我去海滩时总是下雨),你不希望内部循环被一个浪费的指令所负担。性能保证的另一面是程序员的额外负担,以避免出现导致未定义行为的情况。一些语言帮助程序员避免问题,但是这种帮助总是伴随着性能的损失。

那么std::string是怎么回事?简而言之,复杂类型(如字符串)不同于简单的内置类型。对于std::string这样的类型,C++ 库提供一个定义良好的初始值其实更简单。标准库中大多数有趣的类型都有相同的行为方式。

如果你不记得什么时候定义一个没有初始值的变量是安全的,为了安全起见,使用空括号:

std::string empty{};
int zero{};

我建议初始化每个变量,即使你知道程序很快就会覆盖它,比如我们之前使用的输入循环。以“性能”的名义省略初始化很少能提高性能,而且总是会损害可读性。接下来的探索展示了初始化每个变量的重要性。

OLD-FASHIONED INITIALIZATION

初始化所有变量的大括号风格是在 C++ 11 中引入的,所以早于 C++ 11 的代码(或由在 C++ 11 之前学习 C++ 并且还没有掌握新的初始化风格的程序员编写的新代码)使用不同的方法来初始化变量。

例如,初始化整数的常用方法是使用等号。它看起来像一个赋值语句,但它不是。它定义并初始化一个变量。

int x = 42;

您也可以使用括号:

int x(42);

许多标准库类型也是如此:

std::string str1 = "sample";
std::string str2("sample");

有些类型需要括号,并且没有等号。其他类型在 C++ 11 之前使用花括号。等号、括号和花括号都有不同的规则,初学者很难理解等号和括号在初始化时的细微差别。

因此,标准化委员会努力在 C++ 11 中定义一个单一的、统一的初始化风格,他们不得不在 C++ 14 中进行调整。不过,您还没有完全走出困惑区,因为您将会看到一些需要等号进行初始化的上下文,或者大括号不像您预期的那样工作。但是普通变量应该总是使用花括号,这是我在这次探索中提出的。