面向零基础初学者的现代 C++ 教程(全)
一、介绍
亲爱的读者:
恭喜你选择学习 C++ 编程语言,感谢你拿起这本书。我叫 Slobodan Dmitrovi,是一名软件开发人员和技术作家,我将尽我所能向您介绍一个 C++ 的美丽世界。
这本书致力于以一种结构化的、简单的和友好的方式向读者介绍 C++ 编程语言。我们将尽可能使用“足够的理论和大量的例子”的方法。
对我来说,C++ 是人类智慧的奇妙产物。这些年来,我确实认为它是一件美丽而优雅的事情。C++ 是一种与众不同的语言,它的复杂性令人惊讶,但在许多方面都非常圆滑和优雅。它也是一种不能通过猜测来学习的语言,一种容易出错并且很难正确掌握的语言。
在本书中,我们将首先熟悉语言基础。然后,我们将转向标准库。一旦我们了解了这些,我们将更详细地描述现代 C++ 标准。
在每一节之后,都有源代码练习来帮助我们更有效地采用所学的材料。让我们开始吧!
二、什么是 C++?
C++ 是一种编程语言。一种标准化的、通用的、面向对象的编译语言。C++ 附带了一组称为 C++ 标准库的函数和容器。比雅尼·斯特劳斯特鲁普创造了 C++ 作为 C 编程语言的扩展。尽管如此,C++ 已经发展成为一种完全不同的编程语言。
让我们强调一下:C 和 C++ 是两种不同的语言。C++ 开始是“带类的 C”,但现在它是一种完全不同的语言。所以,C++ 不是 C;C++ 不是有类的 C;它只是 C++。而且也没有 C/C++ 编程语言这种东西。
C++ 广泛用于所谓的系统编程和应用编程。C++ 是一种语言,它允许我们深入到底层,如果需要的话,我们可以执行底层的例程,或者通过模板和类等抽象机制高飞。
2.1 C++ 标准
C++ 由 ISO C++ 标准管理。这里按时间顺序列出了多个 ISO C++ 标准:C++03、C++11、C++14、C++17,以及即将推出的 C++20 标准。
从 C++11 开始的每一个 C++ 标准都被称为“现代 C++”。现代 C++ 是我们将在本书中教授的内容。
三、C++ 编译器
C++ 程序通常是分布在一个或多个源文件中的 C++ 代码的集合。C++ 编译器编译这些文件,并将它们转换成目标文件。目标文件通过链接器链接在一起,以创建可执行文件或库。在撰写本文时,一些比较流行的 C++ 编译器是:
-
g++ 前端(作为 GCC 的一部分)
-
Visual C++(作为 Visual Studio IDE 的一部分)
-
Clang(作为 LLVM 的一部分)
3.1 安装 C++ 编译器
以下部分解释了如何在 Linux 和 Windows 上安装 C++ 编译器,以及如何编译和运行我们的 C++ 程序。
Linux 上的 3.1.1
要在 Linux 上安装 C++ 编译器,请在终端中键入以下命令:
sudo apt-get install build-essential
为了编译 C++ 源文件 source.cpp ,我们输入:
g++ source.cpp
该命令将生成一个默认名称为 a.out 的可执行文件。要运行可执行文件,请键入:
./a.out
为了针对 C++11 标准进行编译,我们添加了-std=c++11 标志:
g++ -std=c++11 source.cpp
为了启用警告,我们添加了 -Wall 标志:
g++ -std=c++11 -Wall source.cpp
为了生成自定义的可执行文件名称,我们添加了 -o 标志,后跟一个可执行文件名称:
g++ -std=c++11 -Wall source.cpp -o myexe
同样的规则也适用于 Clang 编译器。用 clang++ 替换 g++ 。
3.1.2 在 Windows 上
在 Windows 上,我们可以安装一个免费的 Visual Studio。
选择创建新项目,确保选择了 C++ 语言选项,选择- 空项目-点击下一步,点击创建。转到解决方案资源管理器面板,右击项目名称,选择添加–新项目–c++ 文件(。cpp) ,输入文件名( source.cpp ),点击添加。按 F5 运行程序。
我们也可以这样做:选择创建一个新项目,确保选择了 C++ 语言选项,选择—控制台 App—点击下一步,点击创建。
如果创建新项目按钮不可见,选择文件-新建-项目,重复其余步骤。
四、我们的第一个项目
让我们使用我们选择的文本编辑器或 C++ IDE 创建一个空白文本文件,并将其命名为 source.cpp 。首先,让我们创建一个空的 C++ 程序,它什么也不做。 source.cpp 文件的内容是:
int main(){}
函数main
是主程序入口点,我们程序的开始。当我们运行我们的可执行文件时,main
函数体内的代码被执行。一个函数的类型是int
(并向系统返回一个结果,但是我们先不要担心这个)。保留名main
是一个函数名。它后面是圆括号内的参数列表()
,后面是用大括号标记的函数体{}
。标记函数体开始和结束的大括号也可以在不同的行上:
int main()
{
}
这个简单的程序什么也不做,圆括号中没有列出任何参数,函数体中也没有任何语句。理解这是主程序签名是很重要的。
还有另一个main
函数签名接受两个不同的参数,用于操作命令行参数。现在,我们将只使用第一种形式。
4.1 评论
C++ 中的单行注释以双斜线//
开头,编译器会忽略它们。我们使用它们来注释或记录代码,或者将它们用作注释:
int main()
{
// this is a comment
}
我们可以有多个单行注释:
int main()
{
// this is a comment
// this is another comment
}
多行注释以/*
开始,以*/
结束。它们也被称为 C 风格的注释。示例:
int main()
{
/* This is a
multi-line comment */
}
4.2 Hello World 示例
现在我们已经准备好第一次看到我们的“Hello World”示例。下面的程序是最简单的“Hello World”例子。它打印出 Hello World。在控制台窗口中:
#include <iostream>
int main()
{
std::cout << "Hello World.";
}
信不信由你,这个例子的详细分析和解释长达 15 页。我们现在可以深入研究它,但是在这一点上我们不会更明智,因为我们首先需要知道什么是头、流、对象、操作符和字符串文字。别担心。我们会到达那里。
简短的解释
#include <iostream>
语句通过#include
指令将iostream
头包含到我们的源文件中。iostream
标题是标准库的一部分。我们需要包含它来使用std::cout
对象,也称为标准输出流。<<
操作符将 Hello World 字符串文字插入到输出流中。字符串文字用双引号括起来""
。;
标志着语句的结束。语句是 c++ 程序中被执行的部分。在 C++ 中,语句以分号;
结尾。std
是标准库名称空间,而::
是范围解析操作符。对象cout
在std
名称空间内,为了访问它,我们需要在调用前加上std::
。我们将在本书的后面部分更加熟悉所有这些,尤其是std::
部分。
简要说明
简而言之,std::cout <<
是 C++ 中向标准输出/控制台窗口输出数据的自然方式。
我们可以通过用多个<<
操作符分隔它们来输出多个字符串文字:
#include <iostream>
int main()
{
std::cout << "Some string." << " Another string.";
}
为了在新行上输出,我们需要输出一个新行字符\n
文本。字符用单引号括起来'\n'
。
示例:
#include <iostream>
int main()
{
std::cout << "First line" << '\n' << "Second line.";
}
\
代表一个转义序列,一种输出某些特殊字符的机制,比如换行字符'\n'
、单引号字符'\''
或双引号字符'\"'
。
字符也可以是单个字符串文字的一部分:
#include <iostream>
int main()
{
std::cout << "First line\nSecond line.";
}
不使用 using 命名空间 std
web 上的许多例子通过using namespace std;
语句将整个 std 名称空间引入当前范围,只是为了能够键入cout
而不是std::cout
。虽然这可能会让我们少打五个额外的字符,但由于许多原因,这是错误的。我们不希望将整个 std 名称空间引入当前范围,因为我们希望避免名称冲突和歧义。记住:不要通过using namespace std;
语句将整个std
名称空间引入当前范围。所以,与其用这种错误的方法:
#include <iostream>
using namespace std; // do not use this
int main()
{
cout << "A bad example.";
}
使用以下内容:
#include <iostream>
int main()
{
std::cout << "A good example.";
}
对于驻留在std
名称空间内的对象和函数的调用,在需要的地方添加std::
前缀。
五、类型
每个实体都有一个类型。什么是类型?类型是一组可能的值和操作。类型的实例称为对象。对象是内存中具有特定类型值的某个区域(不要与也称为对象的类的实例相混淆)。
5.1 基本类型
C++ 有一些内置类型。我们经常称它们为基本类型。声明是将名称引入当前范围的语句。
布尔型
让我们声明一个类型为bool
的变量b
。这种类型保存true
和false.
的值
int main()
{
bool b;
}
这个例子声明了一个类型为bool
的变量b
。就是这样。变量没有初始化,在构造时没有给它赋值。为了初始化一个变量,我们使用一个赋值操作符=,后跟一个初始化器:
int main()
{
bool b = true;
}
我们也可以使用大括号{}
进行初始化:
int main()
{
bool b{ true };
}
这些例子声明了一个bool
类型的(局部)变量b
,并将其初始化为值true
。我们的变量现在保存了一个值true
。所有局部变量都应该初始化。访问未初始化的变量会导致未定义的行为,缩写为 UB。在接下来的章节中会有更多的介绍。
字符类型
类型char
,简称字符类型,用于表示单个字符。该类型可存储'a'
、'Z'
等字符。字符类型的大小正好是一个字节。在 C++ 中,字符文字用单引号''
括起来。为了声明和初始化一个char
类型的变量,我们写:
int main()
{
char c = 'a';
}
现在我们可以打印出 char 变量的值:
#include <iostream>
int main()
{
char c = 'a';
std::cout << "The value of variable c is: " << c;
}
声明并初始化后,我们可以访问变量并更改其值:
#include <iostream>
int main()
{
char c = 'a';
std::cout << "The value of variable c is: " << c;
c = 'Z';
std::cout << " The new value of variable c is: " << c;
}
char
类型在内存中的大小通常是一个字节。我们通过一个sizeof
操作符获得类型的大小:
#include <iostream>
int main()
{
std::cout << "The size of type char is: " << sizeof(char) << " byte(s)";
}
还有其他字符类型,比如用于保存 Unicode 字符集字符的wchar_t
,用于保存 UTF-16 字符集的char16_t
,但是现在,让我们坚持使用类型char
。
字符文字是用单引号括起来的字符。例如:'a'
、'A'
、'z'
、'X'
、'0'
等。
字符集中的每个字符都由一个整数表示。这就是为什么我们可以给我们的char
变量分配数字文字(直到某个数字)和字符文字:
int main()
{
char c = 'a';
// is the same as if we had
// char c = 97;
}
我们可以写成:char c = 'a';
也可以写成char c = 97;
(大概)是一样的,因为 ASCII 表中的'a'
字符是用97
的数字来表示的。在大多数情况下,我们将使用字符来表示 char 对象的值。
整数类型
另一种基本类型是int
称为整型。我们用它来存储整数值(整数),包括负数和正数:
#include <iostream>
int main()
{
int x = 123;
int y = -256;
std::cout << "The value of x is: " << x << ", the value of y is: " << y;
}
这里我们声明并初始化了两个类型为int
的变量。int
的大小通常是 4 个字节。我们也可以用另一个变量来初始化这个变量。它将收到其值的副本。我们在内存中仍然有两个独立的对象:
#include <iostream>
int main()
{
int x = 123;
int y = x;
std::cout << "The value of x is: " << x << " ,the value of y is: " << y;
// x is 123
// y is 123
x = 456;
std::cout << "The value of x is: " << x << " ,the value of y is: " << y;
// x is now 456
// y is still 123
}
一旦我们声明了一个变量,我们就只通过变量名来访问和操作变量名,而不用类型名。
整数可以是十进制、八进制和十六进制。八进制文字以前缀0,
开始,十六进制文字以前缀0x
开始。
int main()
{
int x = 10; // decimal literal
int y = 012; // octal literal
int z = 0xA; // hexadecimal literal
}
所有这些变量都被初始化为由不同的整数文字表示的值10
。在大多数情况下,我们将使用十进制文字。
还有其他整数类型如int64_t
等,但我们暂时坚持使用int
。
浮点类型
C++ 中有三种浮点类型:float
、double, long double,
,但我们将坚持使用类型double
(双精度)。我们用它来存储浮点值/实数:
#include <iostream>
int main()
{
double d = 3.14;
std::cout << "The value of d is: " << d;
}
一些浮点文字可以是:
int main()
{
double x = 213.456;
double y = 1.;
double z = 0.15;
double w = .15;
double d = 3.14e10;
}
类型空隙
类型void
是没有值的类型。如果我们不能拥有那种类型的对象,那么这种类型的目的是什么?问得好。虽然我们不能拥有类型为void
的对象,但是我们可以拥有类型为void
的函数。不返回值的函数。我们也可以有一个标有void*
的void
指针类型。在后面的章节中,当我们讨论指针和函数时,会有更多的介绍。
5.2 类型修饰符
类型可以有修饰符。一些修饰符是signed
和unsigned
。signed
(如果省略,则为默认值)意味着该类型可以保存正值和负值,而unsigned
意味着该类型具有无符号表示。其他的修饰符是关于大小的:short
- type 的宽度至少是 16 位,long
- type 的宽度至少是 32 位。此外,我们现在可以组合这些修饰符:
#include <iostream>
int main()
{
unsigned long int x = 4294967295;
std::cout << "The value of an unsigned long integer variable is: " << x;
}
类型int
默认为signed
。
5.3 变量声明、定义和初始化
在当前作用域中引入一个名字叫做声明。从现在开始,在当前范围内,我们让世界知道有一个某种类型的名称(例如,一个变量)。在声明中,我们在变量名前面加上一个类型名。声明示例:
int main()
{
char c;
int x;
double d;
}
我们可以在同一行声明多个名称:
int main()
{
int x, y, z;
}
如果存在一个对象的初始化器,那么我们称之为初始化。我们将一个对象声明并初始化为一个特定的值。我们可以用多种方式初始化一个对象:
int main()
{
int x = 123;
int y{ 123 };
int z = { 123 };
}
变量定义是在内存中为一个名字设置一个值。这个定义是确保我们可以在我们的程序中访问和使用这个名字。粗略地说,它是一个声明,后跟一个初始化(对于变量)和一个分号。定义也是一种声明。定义示例:
int main()
{
char c = 'a';
int x = 123;
double d = 456.78;
}
六、练习
6.1 Hello World 和评论
写一个有注释的程序,输出“Hello World”在一行上写着“C++ 太棒了!”在新的一行。
#include <iostream>
int main()
{
// this is a comment
std::cout << "Hello World." << '\n';
std::cout << "C++ rocks!";
}
6.2 声明
编写一个程序,在main
函数中声明三个变量。变量有char
、int
和double.
类型,变量的名字是任意的。因为我们不使用任何输入或输出,所以我们不需要包含
int main()
{
char mychar;
int myint;
double mydouble;
}
6.3 定义
编写一个在main
函数中定义三个变量的程序。变量有char
、int
和double.
类型,变量的名字是任意的。初始化器是任意的。
int main()
{
char mychar = 'a';
int myint = 123;
double mydouble = 456.78;
}
6.4 初始化
编写一个在main
函数中定义三个变量的程序。变量有char
、int
和double.
类型,变量的名字是任意的。初始化器是任意的。使用初始化列表执行初始化。之后打印这些值。
#include <iostream>
int main()
{
char mychar{ 'a' };
int myint{ 123 };
double mydouble{ 456.78 };
std::cout << "The value of a char variable is: " << mychar << '\n';
std::cout << "The value of an int variable is: " << myint << '\n';
std::cout << "The value of a double variable is: " << mydouble << '\n';
}
七、运算符
7.1 赋值运算符
赋值运算符=
为变量/对象赋值:
int main()
{
char mychar = 'c'; // define a char variable mychar
mychar = 'd'; // assign a new value to mychar
int x = 123; // define an integer variable x
x = 456; // assign a new value to x
int y = 789; // define a new integer variable y
y = x; // assing a value of x to it
}
7.2 算术运算符
我们可以使用算术运算符进行算术运算。其中一些是:
+ // addition
- // subtraction
* // multiplication
/ // division
% // modulo
示例:
#include <iostream>
int main()
{
int x = 123;
int y = 456;
int z = x + y; // addition
z = x - y; // subtraction
z = x * y; // multiplication
z = x / y; // division
std::cout << "The value of z is: " << z << '\n';
}
在我们的例子中,整数除法产生值0.
,这是因为两个操作数都是整数的整数除法的结果被截断为零。在表达式x / y
中,x
和y
是操作数,/
是运算符。
如果我们想要一个浮点结果,我们需要使用类型double
并确保至少有一个除法操作数也是类型double
:
#include <iostream>
int main()
{
int x = 123;
double y = 456;
double z = x / y;
std::cout << "The value of z is: " << z << '\n';
}
同样,我们可以有:
#include <iostream>
int main()
{
double z = 123 / 456.0;
std::cout << "The value of z is: " << z << '\n';
}
结果是一样的。
7.3 复合赋值运算符
复合赋值操作符允许我们执行算术运算,并用一个操作符赋值一个结果:
+= // compound addition
-= // compound subtraction
*= // compound multiplication
/= // compound division
%= // compound modulo
示例:
#include <iostream>
int main()
{
int x = 123;
x += 10; // the same as x = x + 10
x -= 10; // the same as x = x - 10
x *= 2; // the same as x = x * 2
x /= 3; // the same as x = x / 3
std::cout << "The value of x is: " << x;
}
7.4 递增/递减运算符
递增/递减运算符递增/递减对象的值。这些操作符是:
++x // pre-increment operator
x++ // post-increment operator
--x // pre-decrement operator
x-- // post-decrement operator
一个简单的例子:
#include <iostream>
int main()
{
int x = 123;
x++; // add 1 to the value of x
++x; // add 1 to the value of x
--x; // decrement the value of x by 1
x--; // decrement the value of x by 1
std::cout << "The value of x is: " << x;
}
前递增和后递增操作符都将1
加到我们对象的值上,前递减和后递减操作符都将我们对象的值减一。除了实现机制(非常宽泛地说)之外,两者的区别在于,使用预递增运算符时,首先添加一个值1
。然后在表达式中计算/访问该对象。使用后增量,首先对对象进行求值/访问,然后添加1
的值。对于接下来的下一个陈述来说,这并没有什么不同。无论使用什么版本的运算符,对象的值都是相同的。唯一的区别是表达式中使用它的时间。
八、标准输入
C++ 提供了接受用户输入的工具。我们可以把标准输入想象成我们的键盘。接受一个整数并将其打印出来的简单例子是:
#include <iostream>
int main()
{
std::cout << "Please enter a number and press enter: ";
int x = 0;
std::cin >> x;
std::cout << "You entered: " << x;
}
std::cin
是标准的输入流,它使用>>
操作符提取已经读入变量的内容。std::cin >> x;
语句的意思是:从一个标准输入读入一个 x 变量。cin
对象驻留在std
名称空间中。因此,std::cout <<
用于输出数据(到屏幕),而std::cin >>
用于输入数据(从键盘)。
我们可以从标准输入中接受多个值,用多个>>
操作符将它们分开:
#include <iostream>
int main()
{
std::cout << "Please enter two numbers separated by a space and press enter: ";
int x = 0;
int y = 0;
std::cin >> x >> y;
std::cout << "You entered: " << x << " and " << y;
}
我们可以接受不同类型的值:
#include <iostream>
int main()
{
std::cout << "Please enter a character, an integer and a double: ";
char c = 0;
int x = 0;
double d = 0.0;
std::cin >> c >> x >> d;
std::cout << "You entered: " << c << ", " << x << " and " << d;
}
九、练习
9.1 标准输入
编写一个程序,从标准输入中接受一个整数,然后打印该数字。
#include <iostream>
int main()
{
std::cout << "Please enter a number: ";
int x;
std::cin >> x;
std::cout << "You entered: " << x;
}
9.2 两个输入
编写一个程序,从标准输入中接受两个整数,然后打印出来。
#include <iostream>
int main()
{
std::cout << "Please enter two integer numbers: ";
int x;
int y;
std::cin >> x >> y;
std::cout << "You entered: " << x << " and " << y;
}
9.3 多输入
编写一个程序,分别从标准输入中接受类型为char
、int
和double
的三个值。之后打印出这些值。
#include <iostream>
int main()
{
std::cout << "Please enter a char, an int and a double: ";
char c;
int x;
double d;
std::cin >> c >> x >> d;
std::cout << "You entered: " << c << ", " << x << ", and " << d;
}
9.4 输入和算术运算
编写一个程序,接受两个int
数,将它们相加,并将结果赋给第三个整数。随后打印出结果。
#include <iostream>
int main()
{
std::cout << "Please enter two integer numbers: ";
int x;
int y;
std::cin >> x >> y;
int z = x + y;
std::cout << "The result is: " << z;
}
9.5 后加薪和复合派任
编写一个程序,定义一个名为x
的int
变量,其值为123
,在下一条语句中后递增该值,并在下面的语句中使用复合赋值运算符添加20
的值。随后打印出该值。
#include <iostream>
int main()
{
int x = 123;
x++;
x += 20;
std::cout << "The result is: " << x;
}
9.6 整数和浮点除法
编写一个程序,将数字 9 和2
相除,并将结果赋给一个int
和一个double
变量。然后修改其中一个操作数,使其为double
类型,并观察浮点除法的不同结果,其中至少有一个操作数为double
类型。之后打印出这些值。
#include <iostream>
int main()
{
int x = 9 / 2;
std::cout << "The result is: " << x << '\n';
double d = 9 / 2;
std::cout << "The result is: " << d << '\n';
d = 9.0 / 2;
std::cout << "The result is: " << d;
}
十、数组
数组是相同类型的对象序列。我们可以如下声明一个类型为char
的数组:
int main()
{
char arr[5];
}
此示例声明了一个包含 5 个字符的数组。要声明一个包含五个元素的类型为int
的数组,我们可以使用:
int main()
{
int arr[5];
}
要初始化一个数组,我们可以使用初始化列表{}:
int main()
{
int arr[5] = { 10, 20, 30, 40, 50 };
}
我们示例中的初始化列表{ 10, 20, 30, 40, 50 }
用大括号和逗号分隔的元素标记。这个初始化列表用列表中的值初始化我们的数组。第一个数组元素现在有一个值10
;第二个数组元素现在的值为20
等。最后一个(第五个)数组元素现在的值为50
。
我们可以通过下标操作符和索引来访问单个数组元素。第一个数组元素的索引为 0,我们通过以下方式访问它:
int main()
{
int arr[5] = { 10, 20, 30, 40, 50 };
arr[0] = 100; // change the value of the first array element
}
由于索引从 0 而不是 1 开始,最后一个数组元素的索引为 4:
int main()
{
int arr[5] = { 10, 20, 30, 40, 50 };
arr[4] = 500; // change the value of the last array element
}
所以,在声明一个数组的时候,我们写下想要声明多少个元素,但是在访问数组元素的时候,我们需要记住索引是从 0 开始,以元素数–1结束。也就是说,在现代 C++ 中,我们应该更喜欢std::array
和std::vector
容器而不是原始数组。在后面的章节中会有更多的介绍。
十一、指针
对象驻留在内存中。到目前为止,我们已经学会了如何通过变量来访问和操作对象。另一种访问内存中对象的方法是通过指针。内存中的每个对象都有自己的类型和地址。这允许我们通过指针访问对象。因此,指针是可以保存特定对象地址的类型。仅出于说明的目的,我们将声明一个未使用的指针,它可以指向一个int
对象:
int main()
{
int* p;
}
我们说p
属于类型int*
。
为了声明一个指向char
(对象)的指针,我们声明一个类型为char
*的指针:
int main()
{
char* p;
}
在我们的第一个例子中,我们声明了一个类型为int*.
的指针,让它指向内存中一个现有的int
对象,我们使用了地址操作符&
。我们说p
指向 x
。
int main()
{
int x = 123;
int* p = &x;
}
在我们的第二个例子中,我们声明了一个类型为char*
的指针,类似地,我们有:
int main()
{
char c = 'a';
char* p = &c;
}
要初始化一个不指向任何对象的指针,我们可以使用nullptr
文字:
int main()
{
char* p = nullptr;
}
据说 p 现在是一个空指针。
指针是变量/对象,就像任何其他类型的对象一样。它们的值是对象的地址,即存储对象的内存位置。要访问指针指向的对象中存储的值,我们需要解引用指针。通过在指针(变量)名称前添加一个解引用操作符*:
来完成解引用
int main()
{
char c = 'a';
char* p = &c;
char d = *p;
}
要打印出解引用指针的值,我们可以使用:
#include <iostream>
int main()
{
char c = 'a';
char* p = &c;
std::cout << "The value of the dereferenced pointer is: " << *p;
}
现在,解引用指针*p
的值就是'a'
。
类似地,对于一个整数指针,我们有:
#include <iostream>
int main()
{
int x = 123;
int* p = &x;
std::cout << "The value of the dereferenced pointer is: " << *p;
}
在这种情况下,解引用指针的值是123
。
我们可以通过一个解引用的指针来改变被指向对象的值:
#include <iostream>
int main()
{
int x = 123;
int* p = &x;
*p = 456; // change the value of pointed-to object
std::cout << "The value of x is: " << x;
}
我们将讨论指针,尤其是在讨论动态内存分配和对象生存期等概念时,我们将讨论智能指针。
十二、引用
另一个(有点)类似的概念是引用类型。引用类型是内存中现有对象的别名。必须初始化引用。我们将引用类型描述为type_name
后面跟一个&符号&
。示例:
int main()
{
int x = 123;
int& y = x;
}
现在我们有了两个不同的名字来指代内存中的同一个int
对象。如果我们给它们中的任何一个赋予不同的值,它们都会改变,因为我们在内存中有一个对象,但是我们使用了两个不同的名称:
int main()
{
int x = 123;
int& y = x;
x = 456;
// both x and y now hold the value of 456
y = 789;
// both x and y now hold the value of 789
}
另一个概念是const
-引用,它是某个对象的只读别名。示例:
int main()
{
int x = 123;
const int& y = x; // const reference
x = 456;
// both x and y now hold the value of 456
}
我们将在学习函数和函数参数时更详细地讨论引用和const
-reference。现在,让我们假设它们是一个别名,一个现有对象的不同名称。
重要的是不要混淆在指针类型声明(如int* p;
)中使用*
和在解引用指针(如*p = 456.
)时使用*
。虽然是相同的星形字符,但在两种不同的上下文中使用。
重要的是不要混淆引用类型声明中的&符号&
的用法,比如int& y = x;
和作为地址操作符int* p = &x.s
的&符号的用法。同一个文字符号用于两种不同的东西。
十三、字符串简介
前面,我们提到了通过以下方式将一个字符串文字如"Hello World
."
打印到标准输出:
std::cout << "Hello World.";
我们可以将这些文字存储在std::string
类型中。C++ 标准库提供了一个名为string
或者更确切地说是std::string
的复合类型,因为它是std
名称空间的一部分。我们用它来存储和操作字符串。
13.1 定义字符串
要使用std::string
类型,我们需要在程序中包含<string>
头:
#include <string>
int main()
{
std::string s = "Hello World.";
}
要在标准输出中打印出这个字符串,我们使用:
#include <iostream>
#include <string>
int main()
{
std::string s = "Hello World.";
std::cout << s;
}
13.2 连接字符串
我们可以使用复合运算符+=,将字符串文字添加到字符串中:
#include <iostream>
#include <string>
int main()
{
std::string s = "Hello ";
s += "World.";
std::cout << s;
}
我们可以使用+=运算符向字符串中添加一个字符:
#include <iostream>
#include <string>
int main()
{
std::string s = "Hello";
char c = '!';
s += c;
std::cout << s;
}
我们可以使用+运算符将另一个字符串添加到我们的字符串中。我们说我们连接字符串:
#include <iostream>
#include <string>
int main()
{
std::string s1 = "Hello ";
std::string s2 = "World.";
std::string s3 = s1 + s2;
std::cout << s3;
}
类型string
就是所谓的类——模板。它是使用模板实现的,我们将在后面讨论。现在,我们将只提到这个字符串类提供了一些处理字符串的功能(成员函数)。
13.3 访问字符
字符串中的单个字符可以通过下标操作符[]或成员函数来访问。 指数】。索引从0
开始。示例:
#include <iostream>
#include <string>
int main()
{
std::string s = "Hello World.";
char c1 = s[0]; // 'H'
char c2 = s.at(0); // 'H';
char c3 = s[6]; // 'W'
char c4 = s.at(6); // 'W';
std::cout << "First character: " << c1 << ", sixth character: " << c3;
}
13.4 比较字符串
使用等号==
操作符,可以将一个字符串与字符串文字和其他字符串进行比较。比较字符串和字符串文字:
#include <iostream>
#include <string>
int main()
{
std::string s1 = "Hello";
if (s1 == "Hello")
{
std::cout << "The string is equal to \"Hello\"";
}
}
使用相等运算符==
将一个字符串与另一个字符串进行比较:
#include <iostream>
#include <string>
int main()
{
std::string s1 = "Hello";
std::string s2 = "World.";
if (s1 == s2)
{
std::cout << "The strings are equal.";
}
else
{
std::cout << "The strings are not equal.";
}
}
13.5 字符串输入
接受来自标准输入的字符串的首选方式是通过将std::cin
和我们的字符串作为参数的std::getline
函数:
#include <iostream>
#include <string>
int main()
{
std::string s;
std::cout << "Please enter a string: ";
std::getline(std::cin, s);
std::cout << "You entered: " << s;
}
我们使用std::getline
是因为我们的字符串可以包含空格。如果我们单独使用std::cin
函数,它将只接受字符串的一部分。
std::getline
函数有如下签名:std::getline(read_from, into);
函数将标准输入(std::cin
)中的一行文本读入一个字符串(s
)变量。
一个经验法则:如果我们需要使用std::string
类型,就明确地包含<string>
头。
13.6 指向字符串的指针
字符串有一个成员函数。c_str(),返回指向第一个元素的指针。据说它返回一个指向空字符数组的指针,我们的字符串是由这个数组组成的:
#include <iostream>
#include <string>
int main()
{
std::string s = "Hello World.";
std::cout << s.c_str();
}
这个成员函数的类型是const char*
,当我们想把我们的std::string
变量传递给一个接受const char*
参数的函数时,这个函数很有用。
13.7 子字符串
为了从一个字符串创建一个子串,我们使用了。substr() 成员函数。该函数返回一个从主字符串中的某个位置开始并具有一定长度的子字符串。函数的签名是:。子串(起始位置,长度)。示例:
#include <iostream>
#include <string>
int main()
{
std::string s = "Hello World.";
std::string mysubstring = s.substr(6, 5);
std::cout << "The substring value is: " << mysubstring;
}
在本例中,我们有保存“Hello World”值的主字符串然后我们创建一个只有“World”值的子串。子串从主串的第六个字符开始,长度为五个字符。
13.8 查找子字符串
为了在字符串中找到子串,我们使用了。find() 成员函数。它在字符串中搜索子字符串。如果找到子字符串,函数将返回第一个找到的子字符串的位置。这个位置是主字符串中子字符串开始的字符位置。如果没有找到子串,该函数将返回一个值 std::string::npos 。函数本身的类型是 std::string::size_type 。
为了在“这是一个 Hello World 字符串”字符串中找到子字符串“Hello ”,我们编写:
#include <iostream>
#include <string>
int main()
{
std::string s = "This is a Hello World string.";
std::string stringtofind = "Hello";
std::string::size_type found = s.find(stringtofind);
if (found != std::string::npos)
{
std::cout << "Substring found at position: " << found;
}
else
{
std::cout << "The substring is not found.";
}
}
这里我们有一个主字符串和一个要查找的子字符串。我们将子字符串提供给。find()函数作为参数。我们将函数的返回值存储到变量 found 中。然后我们检查这个变量的值。如果值不等于 std::string::npos ,则找到子串。我们打印消息和主字符串中某个字符的位置,也就是找到子字符串的位置
十四、自动类型推导
我们可以使用auto
描述符自动推断对象的类型。自动描述符根据对象的初始值设定项类型推断对象的类型。
示例:
auto c = 'a'; // char type
这个例子推断出c
的类型为char
,因为初始化器'a'
的类型为char
。
同样,我们可以有:
auto x = 123; // int type
这里,编译器将x
推断为int
类型,因为整数文字123
是int
类型。
该类型也可以基于表达式的类型来推导:
auto d = 123.456 / 789.10; // double
这个例子将d
推断为double
类型,因为整个123.456 / 789.10
表达式的类型是double
。
我们可以使用auto
作为引用类型的一部分:
int main()
{
int x = 123;
auto& y = x; // y is of int& type
}
或者作为常量类型的一部分:
int main()
{
const auto x = 123; // x is of const int type
}
当类型(名称)很难手动推导或者由于长度原因键入起来很麻烦时,我们使用 auto 描述符。
十五、练习
15.1 数组定义
编写一个程序,定义并初始化一个由五个双精度值组成的数组。更改并打印第一个和最后一个数组元素的值。
#include <iostream>
int main()
{
double arr[5] = { 1.23, 2.45, 8.52, 6.3, 10.15 };
arr[0] = 2.56;
arr[4] = 3.14;
std::cout << "The first array element is: " << arr[0] << '\n';
std::cout << "The last array element is: " << arr[4] << '\n';
}
15.2 指向对象的指针
编写一个定义 double 类型对象的程序。定义一个指向该对象的指针。通过取消对指针的引用来打印所指向对象的值。
#include <iostream>
int main()
{
double d = 3.14;
double* p = &d;
std::cout << "The value of the pointed-to object is: " << *p;
}
15.3 参考类型
编写一个程序,定义一个名为mydouble
的 double 类型的对象。定义一个名为myreference
的引用类型的对象,并用mydouble
初始化它。改变myreference
的值。使用引用和原始变量打印对象值。改变mydouble
的值。打印两个对象的值。
#include <iostream>
int main()
{
double mydouble = 3.14;
double& myreference = mydouble;
myreference = 6.28;
std::cout << "The values are: " << mydouble << " and " << myreference << '\n';
mydouble = 9.45;
std::cout << "The values are: " << mydouble << " and " << myreference << '\n';
}
15.4 弦
写一个定义两个字符串的程序。将它们连接在一起,并将结果赋给第三个字符串。打印出结果字符串的值。
#include <iostream>
#include <string>
int main()
{
std::string s1 = "Hello";
std::string s2 = " World!";
std::string s3 = s1 + s2;
std::cout << "The resulting string is: " << s3;
}
标准输入的 15.5 个字符串
编写一个程序,使用std::getline
函数从标准输入中接受名字和姓氏。将输入存储在一个名为fullname
的字符串中。打印出字符串。
#include <iostream>
#include <string>
int main()
{
std::string fullname;
std::cout << "Please enter the first and the last name: ";
std::getline(std::cin, fullname);
std::cout << "Your name is: " << fullname;
}
15.6 创建子字符串
编写一个程序,从主字符串创建两个子字符串。主字符串由名和姓组成,等于“John Doe”第一个子字符串是名字。第二个子串是姓氏。之后打印主字符串和两个子字符串。
#include <iostream>
#include <iostream>
int main()
{
std::string fullname = "John Doe";
std::string firstname = fullname.substr(0, 4);
std::string lastname = fullname.substr(5, 3);
std::cout << "The full name is: " << fullname << '\n';
std::cout << "The first name is: " << firstname << '\n';
std::cout << "The last name is: " << lastname << '\n';
}
15.7 查找单个字符
编写一个程序,用值“Hello C++ World”定义主字符串。并检查是否在主字符串中找到单个字符“C”。
#include <iostream>
#include <string>
int main()
{
std::string s = "Hello C++ World.";
char c = 'C';
auto characterfound = s.find(c);
if (characterfound != std::string::npos)
{
std::cout << "Character found at position: " << characterfound << '\n';
}
else
{
std::cout << "Character was not found." << '\n';
}
}
15.8 查找子字符串
编写一个程序,用值“Hello C++ World”定义主字符串。并检查在主字符串中是否找到子字符串“C++”。
#include <iostream>
#include <string>
int main()
{
std::string s = "Hello C++ World.";
std::string mysubstring = "C++";
auto mysubstringfound = s.find(mysubstring);
if (mysubstringfound != std::string::npos)
{
std::cout << "Substring found at position: " << mysubstringfound << '\n';
}
else
{
std::cout << "Substring was not found." << '\n';
}
}
“C”字符和“C++”子字符串在主字符串中的相同位置开始。这就是为什么两个示例都产生值 6。
我们没有为我们的 characterfound 和 mysubstringfound 变量键入冗长的 std::string::size_type 类型,而是使用 auto 描述符为我们自动推导出类型。
15.9 自动类型扣除
编写一个程序,根据使用的初始化器自动推导出char
、int
和double
对象的类型。之后打印出这些值。
#include <iostream>
int main()
{
auto c = 'a';
auto x = 123;
auto d = 3.14;
std::cout << "The type of c is deduced as char, the value is: " << c << '\n';
std::cout << "The type of x is deduced as int, the value is: " << x << '\n';
std::cout << "The type of d is deduced as double, the value is: " << d << '\n';
}
十六、语句
之前,我们将语句描述为命令,即按某种顺序执行的代码片段。以分号结尾的表达式是语句。C++ 语言自带一些内置语句。我们将从选择语句开始。
16.1 选择声明
选择语句允许我们检查使用条件,并基于该条件执行适当的语句。
if 语句
当我们想基于某种条件执行一条或多条语句时,我们使用if
-语句。if
-声明的格式为:
if (condition) statement
仅当条件为true
时,statement
才会执行。示例:
#include <iostream>
int main()
{
bool b = true;
if (b) std::cout << "The condition is true.";
}
如果条件是true
,为了执行多个语句,我们使用块范围{}
:
#include <iostream>
int main()
{
bool b = true;
if (b)
{
std::cout << "This is a first statement.";
std::cout << "\nThis is a second statement.";
}
}
另一种形式是if-else
语句:
if (condition) statement else statement
如果条件为true
,则执行第一条语句,否则执行else
关键字后的第二条语句。示例:
#include <iostream>
int m.ain()
{
bool b = false;
if (b) std::cout << "The condition is true.";
else std::cout << "The condition is false.";
}
为了在if
或else
分支中执行多条语句,我们使用括号括起来的块{}
:
#include <iostream>
int main()
{
bool b = false;
if (b)
{
std::cout << "The condition is true.";
std::cout << "\nThis is the second statement.";
}
else
{
std::cout << "The condition is false.";
std::cout << "\nThis is the second statement.";
}
}
条件表达式
简单的 if 语句也可以写成条件表达式。下面是一个简单的 if 语句:
#include <iostream>
int main()
{
bool mycondition = true;
int x = 0;
if (mycondition)
{
x = 1;
}
else
{
x = 0;
}
std::cout << "The value of x is: " << x << '\n';
}
为了使用一个条件表达式重写前面的例子,我们写:
#include <iostream>
int main()
{
bool mycondition = true;
int x = 0;
x = (mycondition) ? 1 : 0;
std::cout << "The value of x is: " << x << '\n';
}
条件表达式的语法如下:
(condition) ? expression_1 : expression_2
条件表达式使用一元?运算符,检查条件的值。如果条件为真,则返回表达式 _1 。如果条件为假,则返回 expression_2 。它可以被认为是一种用一行程序代替简单的 if-else 语句的方法。
逻辑运算符
逻辑运算符对其操作数执行逻辑与、【or】、非运算。第一个是&&
运算符,这是一个逻辑 AND 运算符。如果两个操作数都是true
,则带有两个操作数的逻辑与条件的结果是true
。示例:
#include <iostream>
int main()
{
bool a = true;
bool b = true;
if (a && b)
{
std::cout << "The entire condition is true.";
}
else
{
std::cout << "The entire condition is false.";
}
}
下一个操作符是||,
,它是一个逻辑 or 操作符。逻辑 OR 表达式的结果总是true
,除非两个操作数都是false
。示例:
#include <iostream>
int main()
{
bool a = false;
bool b = false;
if (a || b)
{
std::cout << "The entire condition is true.";
}
else
{
std::cout << "The entire condition is false.";
}
}
下一个逻辑运算符是由!
表示的否定运算符。它对其唯一右侧操作数的值求反。它将true
的值转换为false
,反之亦然。示例:
#include <iostream>
int main()
{
bool a = true;
if (!a)
{
std::cout << "The condition is true.";
}
else
{
std::cout << "The condition is false.";
}
}
16.1.3.1 比较运算符
比较运算符允许我们比较操作数的值。比较运算符小于,大于等于> =,等于==,不等于!=.
我们可以使用相等运算符==
来检查操作数的值是否相等:
#include <iostream>
int main()
{
int x = 5;
if (x == 5)
{
std::cout << "The value of x is equal to 5.";
}
}
其他比较运算符的用例:
#include <iostream>
int main()
{
int x = 10;
if (x > 5)
{
std::cout << "The value of x is greater than 5.";
}
if (x >= 10)
{
std::cout << "\nThe value of x is greater than or equal to 10.";
}
if (x != 20)
{
std::cout << "\nThe value of x is not equal to 20.";
}
if (x == 20)
{
std::cout << "\nThe value of x is equal to 20.";
}
}
现在,我们可以在相同的条件下使用逻辑运算符和比较运算符:
#include <iostream>
int main()
{
int x = 10;
if (x > 5 && x < 15)
{
std::cout << "The value of x is greater than 5 and less than 15.";
}
bool b = true;
if (x >5 && b)
{
std::cout << "\nThe value of x is greater than 5 and b is true.";
}
}
任何可隐式转换为true
或false
的文字、对象或表达式都可以用作条件:
#include <iostream>
int main()
{
if (1) // literal 1 is convertible to true
{
std::cout << "The condition is true.";
}
}
如果我们使用一个整数变量,其值不是0
,那么结果将是true
:
#include <iostream>
int main()
{
int x = 10; // if x was 0, the condition would be false
if (x)
{
std::cout << "The condition is true.";
}
else
{
std::cout << "The condition is false.";
}
}
在if
-语句分支内使用代码块{}
是一个很好的实践,即使只有一条语句要执行。
开关声明
switch 语句类似于拥有多个 if 语句。它检查条件的值(必须是整数或枚举值),并根据该值执行一组给定事例标签中的一个标签内的代码。如果没有一个 case 语句符合条件,则执行默认标签中的代码。常规语法:
switch (condition)
{
case value1:
statement(s);
break;
case value2etc:
statement(s);
break;
default:
statement(s);
break;
}
一个简单的示例,它检查整数 x 的值并执行适当的 case 标签:
#include <iostream>
int main()
{
int x = 3;
switch (x)
{
case 1:
std::cout << "The value of x is 1.";
break;
case 2:
std::cout << "The value of x is 2.";
break;
case 3:
std::cout << "The value of x is 3."; // this statement will be // executed
break;
default:
std::cout << "The value is none of the above.";
break;
}
}
break
语句退出 switch 语句。如果没有 break 语句,代码将跳转到下一个 case 语句,并在那里执行代码,而不考虑 x 值。我们需要在所有的情况:和默认:开关中放置断点。
16.2 迭代语句
如果我们需要一些代码执行多次,我们使用迭代语句。迭代语句是在循环中执行某些代码的语句。循环中的代码执行 0 次、1 次或多次,具体取决于语句和条件。
16.2.1 声明
for
-语句循环执行代码。执行取决于条件。for
-语句的一般语法是:for (init_statement; condition; iteration_expression) { // execute some code }.
一个简单的例子:
#include <iostream>
int ma.in()
{
for (int i = 0; i < 10; i++)
{
std::cout << "The counter is: " << i << '\n';
}
}
这个例子执行了十次for
循环中的代码。init_statement
是int i = 0;
,我们将计数器初始化为0
。condition
是:i < 10; and the iteration_expression is i++;
简单解释:
将计数器初始化为0
,检查计数器是否小于 10,执行代码块中的std::cout << "The counter is: " << i << '\n';
语句,并将计数器i
加 1。因此,只要i < 10
条件为true
,代码块中的代码就会继续执行。一旦计数器变为10
,条件不再是true,
,并且for
循环终止。
如果我们希望某个东西执行 20 次,我们将设置一个不同的条件:
#include <iostream>
int main()
{
for (int i = 0; i < 20; i++)
{
std::cout << "The counter is: " << i << '\n';
}
}
while 语句
while
-语句执行代码,直到条件变为false
。while
循环的语法是:
while (condition) { // execute some code }
只要条件是true
,while
-循环就会继续执行代码。当condition
变成false,
时,while
循环终止。示例:
#include <iostream>
int main()
{
int x = 0;
while (x < 10)
{
std::cout << "The value of x is: " << x << '\n';
x++;
}
}
本例中的代码执行十次。每次迭代后,对条件x < 10
进行求值,只要等于true
,代码块中的代码就会一直执行。一旦条件变为false
,则while
循环终止。在这个例子中,我们在每次迭代中增加x
的值。而一旦变成10
,循环终止。
做陈述
do
-语句类似于while
-语句,但是条件在主体之后。do
语句中的代码保证至少执行一次。语法是:
do { // execute some code } while (condition);
如果我们使用前面的例子,代码将是:
#include <iostream>
int main()
{
int x = 0;
do
{
std::cout << "The value of x is: " << x << '\n';
x++;
} while (x < 10);
}
很少使用并且最好避免使用do
-语句。
请注意,还有一个名为的迭代语句,即语句的范围。我们稍后讨论容器时会谈到它。
十七、常量
当我们想要一个只读对象或者保证不改变当前作用域中某个对象的值时,我们就把它设为常量。C++ 使用const
类型限定符将对象标记为只读。我们说我们的对象现在是不可变的 ??。例如,要定义一个值为 5 的整数常量,我们可以这样写:
int main()
{
const int n = 5;
}
我们现在可以在诸如数组大小的地方使用该常量:
int main()
{
const int n = 5;
int arr[n] = { 10, 20, 30, 40, 50 };
}
常量是不可修改的,尝试这样做会导致编译时错误:
int main()
{
const int n = 5;
n++; // error, can’t modify a read-only object
}
不能给声明为const
的对象赋值;它需要初始化。所以,我们不能有:
int main()
{
const int n; // error, no initializer
const int m = 123; // OK
}
值得注意的是const
修改了整个类型,而不仅仅是对象。所以,const int
和int
是两种不同的类型。第一个据说是const
——合格。
另一个 const 限定符是名为constexpr
的常量表达式。它是一个可以在编译时计算的常量。常量表达式的初始值设定项可以在编译时计算,并且本身必须是常量表达式。示例:
int main()
{
constexpr int n = 123; //OK, 123 is a compile-time constant // expression
constexpr double d = 456.78; //OK, 456.78 is a compile-time constant // expression
constexpr double d2 = d; //OK, d is a constant expression
int x = 123;
constexpr int n2 = x; //compile-time error
// the value of x is not known during // compile-time
}
十八、练习
18.1 简单的 if 语句
写一个程序,定义一个值为假的布尔变量。使用变量作为 if 语句中的条件。
#include <iostream>
int main()
{
bool mycondition = false;
if (mycondition)
{
std::cout << "The condition is true." << '\n';
}
else
{
std::cout << "The condition is not true." << '\n';
}
}
18.2 逻辑运算符
写一个程序,定义一个 int 类型的变量。将值 256 赋给变量。检查此变量的值是否大于 100 且小于 300。然后,定义一个值为 true 的布尔变量。检查 int 数是否大于 100,或者 bool 变量的值是否为 true。然后定义第二个 bool 变量,其值将是第一个 bool 变量的反值。
#include <iostream>
int main()
{
int x = 256;
if (x > 100 && x < 300)
{
std::cout << "The value is greater than 100 and less than 300." << '\n';
}
else
{
std::cout << "The value is not inside the (100 .. 300) range." << '\n';
}
bool mycondition = true;
if (x > 100 || mycondition)
{
std::cout << "Either x is greater than 100 or the bool variable is true." << '\n';
}
else
{
std::cout << "x is not greater than 100 and the bool variable is false." << '\n';
}
bool mysecondcondition = !mycondition;
}
18.3 转换声明
写一个程序,定义一个值为 3 的简单整数变量。使用 switch 语句检查该值是否在[1..4]范围。
#include <iostream>
int main()
{
int x = 3;
switch (x)
{
case 1:
std::cout << "The value is equal to 1." << '\n';
break;
case 2:
std::cout << "The value is equal to 2." << '\n';
break;
case 3:
std::cout << "The value is equal to 3." << '\n';
break;
case 4:
std::cout << "The value is equal to 4." << '\n';
break;
default:
std::cout << "The value is not inside the [1..4] range." << '\n';
break;
}
}
18.4 for 循环
写一个程序,使用 for 循环打印计数器 15 次。计数器从 0 开始计数。
#include <iostream>
int main()
{
for (int i = 0; i < 15; i++)
{
std::cout << "The counter is now: " << i << '\n';
}
}
18.5 数组和 for 循环
写一个定义 5 个整数的数组的程序。使用 for 循环打印数组元素及其索引。
#include <iostream>
int main()
{
int arr[5] = { 3, 20, 8, 15, 10 };
for (int i = 0; i < 5; i++)
{
std::cout << "arr[" << i << "] = " << arr[i] << '\n';
}
}
说明:这里,我们定义了一个包含 5 个元素的数组。数组从零开始索引。因此第一个数组元素 3 的索引为 0。最后一个数组元素 10 的索引为 4。我们使用 for 循环迭代数组元素,并打印它们的索引和值。我们的 for 循环从计数器 0 开始,以计数器 4 结束。
18.6 常量类型限定符
编写一个程序,分别定义三个类型为 const int、const double 和 const std::string 的对象。定义第四个 const int 对象,并用第一个 const int 对象的值初始化它。打印出所有变量的值。
#include <iostream>
int main()
{
const int c1 = 123;
const double d = 456.789;
const std::string s = "Hello World!";
const int c2 = c1;
std::cout << "Constant integer c1 value: " << c1 << '\n';
std::cout << "Constant double d value: " << d << '\n';
std::cout << "Constant std::string s value: " << s << '\n';
std::cout << "Constant integer c2 value: " << c2 << '\n';
}
十九、函数
19.1 简介
我们可以将 C++ 代码分成更小的块,称为函数。函数在声明中有一个返回类型、一个名称、一个参数列表,在定义中还有一个额外的函数体。一个简单的函数定义是:
type function_name(arguments) {
statement;
statement;
return something;
}
19.2 函数声明
要声明一个函数,我们需要指定一个返回类型、一个名称和一个参数列表,如果有的话。要声明一个不接受任何参数的类型为void
的名为myfunction
的函数,我们编写:
void myvoidfunction();
int main()
{
}
Type void
是一个表示 nothing 的类型,是一组空的值。要声明一个接受一个参数的类型为int
的函数,我们可以写:
int mysquarednumber (int x);
int main()
{
}
要声明一个类型为int
的函数,例如,它接受两个int
参数,我们可以写:
int mysum(int x, int y);
int main()
{
}
仅在函数声明中,我们可以省略参数名,但是我们需要指定它们的类型:
int mysum(int, int);
int main()
{
}
19.3 函数定义
要在程序中被调用,必须首先定义一个函数。函数定义拥有函数声明所拥有的一切,再加上函数体。它们是返回类型、函数名、函数参数列表(如果有的话)和函数体。示例:
#include <iostream>
void myfunction(); // function declaration
int main()
{
}
// function definition
void myfunction() {
std::cout << "Hello World from a function.";
}
要定义一个接受一个参数的函数,我们可以写:
int mysquarednumber(int x); // function declaration
int main()
{
}
// function definition
int mysquarednumber(int x) {
return x * x;
}
要定义一个接受两个参数的函数,我们可以写:
int mysquarednumber(int x); // function declaration
int main()
{
}
// function definition
int mysquarednumber(int x) {
return x * x;
}
为了在我们的程序中调用这个函数,我们指定函数名后跟空括号,因为这个函数没有参数:
#include <iostream>
void myfunction(); // function declaration
int main()
{
myfunction(); // a call to a function
}
// function definition
void myfunction() {
std::cout << "Hello World from a function.";
}
要调用接受一个参数的函数,我们可以使用:
#include <iostream>
int mysquarednumber(int x); // function declaration
int main()
{
int myresult = mysquarednumber(2); // a call to the function
std::cout << "Number 2 squared is: " << myresult;
}
// function definition
int mysquarednumber(int x) {
return x * x;
}
我们通过名字调用函数mysquarednumber
,提供一个值2
来代替函数参数,并将函数的结果赋给我们的myresult
变量。我们传递给函数的内容通常被称为函数参数。
要调用一个接受两个或更多参数的函数,我们使用函数名,后跟一个左括号,再跟一列用逗号分隔的参数,最后是右括号。示例:
#include <iostream>
int mysum(int x, int y);
int main()
{
int myresult = mysum(5, 10);
std::cout << "The sum of 5 and 10 is: " << myresult;
}
int mysum(int x, int y) {
return x + y;
}
19.4 退货声明
函数属于某种类型,也称为返回类型、,它们必须返回值。返回值由一个return
-语句指定。类型为void
的函数不需要return
语句。示例:
#include <iostream>
void voidfn();
int main()
{
voidfn();
}
void voidfn()
{
std::cout << "This is void function and needs no return.";
}
其他类型的函数(main 函数除外)需要一个return
-语句:
#include <iostream>
int intfn();
int main()
{
std::cout << "The value of a function is: " << intfn();
}
int intfn()
{
return 42; // return statement
}
如果需要,一个函数可以有多个return
-语句。一旦任何一个return
-语句被执行,函数就停止,函数中的其余代码被忽略:
#include <iostream>
int multiplereturns(int x);
int main()
{
std::cout << "The value of a function is: " << multiplereturns(25);
}
int multiplereturns(int x)
{
if (x >= 42)
{
return x;
}
return 0;
}
19.5 传递参数
向函数传递参数有不同的方式。在这里,我们将描述三个最常用的。
19.5.1 按值/副本传递
当我们将一个参数传递给一个函数时,如果函数的参数类型不是一个引用,那么我们将复制该参数并传递给函数。这意味着原始参数的值不会改变。将制作一份该参数的副本。示例:
#include <iostream>
void myfunction(int byvalue)
{
std::cout << "Argument passed by value: " << byvalue;
}
int main()
{
myfunction(123);
}
这被称为通过值传递参数或者通过拷贝传递参数。
通过引用传递
当函数参数类型是引用类型时,实际的实参被传递给函数。该函数可以修改参数的值。示例:
#include <iostream>
void myfunction(int& byreference)
{
byreference++; // we can modify the value of the argument
std::cout << "Argument passed by reference: " << byreference;
}
int main()
{
int x = 123;
myfunction(x);
}
这里我们传递了一个引用类型的参数int&
,所以这个函数现在使用实际的参数,并且可以改变它的值。当通过引用传递时,我们需要传递变量本身;我们不能传入表示值的文字。最好避免通过引用传递。
19.5.3 通过常量引用
优选的是通过常量引用 ,也称为对常量的引用来传递参数。通过引用传递参数可能更有效,但为了确保它不被更改,我们将它设为 const 引用类型。示例:
#include <iostream>
#include <string>
void myfunction(const std::string& byconstreference)
{
std::cout << "Arguments passed by const reference: " << byconstreference;
}
int main()
{
std::string s = "Hello World!";
myfunction(s);
}
出于效率的原因,我们使用通过常量引用传递,并且const
修饰符确保参数的值不会被改变。
在最后三个例子中,我们省略了函数声明,只提供了函数定义。虽然函数定义也是一个声明,但是您应该同时提供声明和定义,如下所示:
#include <iostream>
#include <string>
void myfunction(const std::string& byconstreference);
int main()
{
std::string s = "Hello World!";
myfunction(s);
}
void myfunction(const std::string& byconstreference)
{
std::cout << "Arguments passed by const reference: " << byconstreference;
}
19.6 函数重载
我们可以有多个同名但参数类型不同的函数。这叫做函数重载。一个简单的解释:当函数名相同,但参数类型不同时,我们就有了重载函数。函数重载声明的示例:
void myprint(char param);
void myprint(int param);
void myprint(double param);
然后我们实现函数定义并调用每个函数定义:
#include <iostream>
void myprint(char param);
void myprint(int param);
void myprint(double param);
int main()
{
myprint('c'); // calling char overload
myprint(123); // calling integer overload
myprint(456.789); // calling double overload
}
void myprint(char param)
{
std::cout << "Printing a character: " << param << '\n';
}
void myprint(int param)
{
std::cout << "Printing an integer: " << param << '\n';
}
void myprint(double param)
{
std::cout << "Printing a double: " << param << '\n';
}
当调用我们的函数时,会根据我们提供的参数类型选择适当的重载。在对myprint('c'),
的第一次调用中,选择了一个char
重载,因为文字'c'
是类型char.
在第二次函数调用myprint(123),
中,选择了一个整数重载,因为参数123
是类型int.
最后,在我们的最后一次函数调用myprint(456.789),
中,编译器选择了一个双重载,因为参数456.789
是类型double
。
是的,C++ 中的文字也有类型,C++ 标准精确地定义了它是什么类型。一些文字及其对应的类型:
'c' - char
123 - int
456.789 - double
true - boolean
"Hello" - const char[6]
二十、练习
20.1 函数定义
编写一个程序,定义一个名为printmessage()
的void
类型的函数。该函数在标准输出上输出一条"Hello World from a function."
消息。从main
调用函数。
#include <iostream>
void printmessage()
{
std::cout << "Hello World from a function.";
}
int main()
{
printmessage();
}
20.2 单独声明和定义
编写一个程序,声明并定义一个名为printmessage()
的void
类型的函数。该函数在标准输出上输出一条"Hello World from a function."
消息。从main
调用函数。
#include <iostream>
void printmessage(); // function declaration
int main()
{
printmessage();
}
// function definition
void printmessage()
{
std::cout << "Hello World from a function.";
}
20.3 函数参数
写一个程序,它有一个类型为int
的函数,名为multiplication
,通过值接受两个int
参数。该函数将这两个参数相乘,并将结果返回给自身。调用 main 中的函数,并将函数的结果赋给一个本地int
变量。在控制台中打印结果。
#include <iostream>
int multiplication(int x, int y)
{
return x * y;
}
int main()
{
int myresult = multiplication(10, 20);
std::cout << "The result is: " << myresult;
}
20.4 传递参数
编写一个程序,该程序有一个名为custommessage
的void
类型的函数。该函数通过引用类型std::string
的const
来接受一个参数,并使用该参数的值在标准输出上输出一个定制消息。用本地字符串调用 main 中的函数。
#include <iostream>
#include <string>
void custommessage(const std::string& message)
{
std::cout << "The string argument you used is: " << message;
}
int main()
{
std::string mymessage = "My Custom Message.";
custommessage(mymessage);
}
20.5 函数重载
写一个有两个函数重载的程序。这些函数被称为division,
,都接受两个参数。它们对参数进行除法运算,并将结果返回给自己。第一个函数重载的类型是int
,并且有两个类型为int
的参数。第二个重载类型为double
,接受两个类型为double
的参数。调用main
中适当的重载,首先提供整数参数,然后是double
参数。观察不同的结果。
#include <iostream>
#include <string>
int division(int x, int y)
{
return x / y;
}
double division(double x, double y)
{
return x / y;
}
int main()
{
std::cout << "Integer division: " << division(9, 2) << '\n';
std::cout << "Floating point division: " << division(9.0, 2.0);
}
二十一、范围和生存期
当我们声明一个变量时,它的名字只在源代码的某些部分有效。而源代码的那一段(部件、部分、区域)叫做范围。它是可以访问名称的代码区域。有不同的范围:
21.1 当地范围
当我们在函数中声明一个名字时,这个名字有一个局部作用域。它的作用域从声明点到标有}的函数块的末尾。
示例:
void myfunction()
{
int x = 123; // Here begins the x's scope
} // and here it ends
我们的变量x
是在myfunction()
体中声明的,它有一个局部范围。我们说 x 这个名字是 ?? 本地的。它只存在于(可以被访问)函数的作用域内,而不存在于其他地方。
21.2 区块范围
block-scope 是由以{开始,以}结束的代码块标记的一段代码。示例:
int main()
{
int x = 123; // first x' scope begins here
{
int x = 456; // redefinition of x, second x' scope begins here
} // block ends, second x' scope ends here
// the first x resumes here
} // block ends, scope of first x's ends here
还有其他的作用域,我们将在本书后面介绍。在这一点上引入作用域的概念来解释对象的生存期是很重要的。
21.3 生存期
对象的生存期是对象在内存中花费的时间。生存期由所谓的存储持续时间决定。有不同种类的存储持续时间。
21.4 自动存储持续时间
自动存储持续时间是在代码块开始时自动分配对象的内存,并在代码块结束时释放内存的持续时间。这也被称为一个栈存储器;对象被分配到栈中。在这种情况下,对象的生存期由其范围决定。所有本地对象都有这个存储持续时间。
21.5 动态存储持续时间
动态存储持续时间是对象的存储器被手动分配和手动解除分配的持续时间。这种存储通常被称为堆内存。用户决定何时为对象分配内存,何时释放内存。对象的生存期不是由定义该对象的范围决定的。我们通过运算符新和智能指针来完成。在现代 C++ 中,我们应该更喜欢智能指针工具而不是新操作符。
21.6 静态储存持续时间
当一个对象声明被加上一个static
描述符时,这意味着静态对象的存储在程序开始时被分配,在程序结束时被释放。这种对象只有一个实例,并且(除了少数例外)当程序结束时,它们的生命周期也就结束了。它们是我们可以在程序执行的任何时候访问的对象。我们将在本书的后面讨论静态描述符和静态初始化。
21.7 运算符新增和删除
我们可以动态地为我们的对象分配和释放存储,并让指针指向这个新分配的内存。
操作符new
为一个对象分配空间。对象被分配在自由存储上,通常称为堆或堆内存。必须使用操作符delete
取消分配已分配的内存。它用一个操作符new
释放先前分配的内存。示例:
#include <iostream>
int main()
{
int* p = new int;
*p = 123;
std::cout << "The pointed-to value is: " << *p;
delete p;
}
此示例在自由存储上为一个整数分配空间。指针p
现在指向为我们的整数新分配的内存。我们现在可以通过取消引用一个指针来给新分配的 integer 对象赋值。最后,我们通过调用操作符delete
来释放内存。
如果我们想为一个数组分配内存,我们使用操作符 new[]。为了释放分配给数组的内存,我们使用操作符delete[]
。指针和数组是相似的,经常可以互换使用。指针可以被下标操作符[]取消引用。示例:
#include <iostream>
int main()
{
int* p = new int[3];
p[0] = 1;
p[1] = 2;
p[2] = 3;
std::cout << "The values are: " << p[0] << ' ' << p[1] << ' ' << p[2];
delete[] p;
}
这个例子为三个整数分配空间,一个三个整数的数组使用操作符new[].
我们的指针p
现在指向数组中的第一个元素。然后,使用下标操作符[],我们取消引用并给每个数组元素赋值。最后,我们使用操作符delete[]
释放内存。记住:永远是你new
选择的delete
,永远是你new[]
选择的delete[]
。
记住:比起操作符new
,更喜欢智能指针。在自由存储上分配的对象的生存期不受定义对象的作用域的限制。我们手动为对象分配和释放内存,从而控制对象何时被创建,何时被销毁。
二十二、练习
22.1 自动存储持续时间
编写一个程序,在main
函数范围内定义两个类型为int
的变量,并自动存储持续时间(放在栈上)。
#include <iostream>
int main()
{
int x = 123;
int y = 456;
std::cout << "The values with automatic storage durations are: " << x << " and " << y;
}
22.2 动态存储持续时间
编写一个程序,定义一个类型为int*
的变量,该变量指向一个具有动态存储持续时间的对象(放在堆上) :
#include <iostream>
int main()
{
int* p = new int{ 123 };
std::cout << "The value with a dynamic storage duration is: " << *p;
delete p;
}
解释
在这个例子中,对象p
只指向具有动态存储持续时间的对象。p
对象本身有一个自动存储持续时间。要删除堆上的对象,我们需要使用删除操作符。
22.3 自动和动态存储持续时间
编写一个程序,定义一个名为 x 的类型为int
的变量,自动存储持续时间,以及一个指向具有动态存储持续时间的对象的类型为 int*的变量。两个变量在同一范围内:
#include <iostream>
int main()
{
int x = 123; // automatic storage duration
std::cout << "The value with an automatic storage duration is: " << x << '\n';
int* p = new int{ x }; // allocate memory and copy the value from x to it
std::cout << "The value with a dynamic storage duration is: " << *p << '\n';
delete p;
} // end of scope here
二十三、类——简介
类是用户定义的类型。一个类由成员组成。成员是数据成员和成员函数。一个类可以被描述为数据和数据上的一些功能,打包成一个。一个类的实例称为对象。为了只声明一个类名,我们写:
class MyClass;
为了定义一个空类,我们添加了一个用大括号{}
标记的类体:
class MyClass{};
为了创建一个类的实例,一个对象,我们使用:
class MyClass{};
int main()
{
MyClass o;
}
解释
我们定义了一个名为MyClass
的类。然后我们创建了一个类型为MyClass
的对象 o。据说o
是一个对象,一个类实例。
23.1 数据成员字段
一个类可以包含一组数据。这些被称为成员字段。让我们向我们的类添加一个成员字段,并使其类型为char
:
class MyClass
{
char c;
};
现在我们的类有了一个名为c
的char
类型的数据成员字段。现在让我们再添加两个类型为int
和double
的字段:
class MyClass
{
char c;
int x;
double d;
};
现在我们的类有三个成员字段,每个成员字段都有自己的名字。
23.2 成员功能
类似地,一个类可以存储函数。这些被称为成员函数。它们主要用于对数据字段执行一些操作。要声明一个名为dosomething()
的 void 类型的成员函数,我们编写:
class MyClass
{
void dosomething();
};
有两种方法来定义这个成员函数。第一种是在类内部定义它:
class MyClass
{
void dosomething()
{
std::cout << "Hello World from a class.";
}
};
第二个是在类外定义。在这种情况下,我们首先编写函数类型,然后是类名,接着是 scope resolution :: operator,然后是函数名、参数列表(如果有)和函数体:
class MyClass
{
void dosomething();
};
void MyClass::dosomething()
{
std::cout << "Hello World from a class.";
}
这里,我们在类内部声明了一个成员函数,并在类外部定义了它。
一个类中可以有多个成员函数。为了在一个类中定义它们,我们应该这样写:
class MyClass
{
void dosomething()
{
std::cout << "Hello World from a class.";
}
void dosomethingelse()
{
std::cout << "Hello Universe from a class.";
}
};
要在类内声明成员函数并在类外定义它们,我们应该写:
class MyClass
{
void dosomething();
void dosomethingelse();
};
void MyClass::dosomething()
{
std::cout << "Hello World from a class.";
}
void MyClass::dosomethingelse()
{
std::cout << "Hello Universe from a class.";
}
现在我们可以创建一个既有数据成员字段又有成员函数的简单类:
class MyClass
{
int x;
void printx()
{
std::cout << "The value of x is:" << x;
}
};
这个类有一个名为x,
的int
类型的数据字段,还有一个名为printx()
的成员函数。这个成员函数读取 x 的值并打印出来。这个例子是对成员访问描述符或类成员可见性的介绍。
23.3 访问描述符
如果有一种方法可以禁止访问成员字段,但允许访问对象的成员函数和其他访问类成员的实体,这不是很方便吗?这就是访问描述符的用途。它们为类成员指定访问权限。有三种访问描述符/标签:公共、受保护和私有:
class MyClass
{
public:
// everything in here
// has public access level
protected:
// everything in here
// has protected access level
private:
// everything in here
// has private access level
};
如果没有访问描述符,则类的默认可见性/访问描述符是private
:
class MyClass
{
// everything in here
// has private access by default
};
另一种写类的方法是写一个struct
。一个结构也是一个class
,默认情况下成员拥有public
访问权限。因此,struct
与class
是一回事,但默认情况下带有一个public
访问描述符:
struct MyStruct
{
// everything in here
// is public by default
};
现在,我们将只关注public
和private
访问描述符。公共访问成员可以在任何地方访问。例如,其他类成员和我们类的对象都可以访问它们。为了从一个对象中访问一个类成员,我们使用点。运算符。
让我们定义一个类,其中所有成员都有公共访问权。要用公共访问描述符定义一个类,我们可以写:
class MyClass
{
public:
int x;
void printx()
{
std::cout << "The value of x is:" << x;
}
};
让我们实例化这个类并在我们的主程序中使用它:
#include <iostream>
class MyClass
{
public:
int x;
void printx()
{
std::cout << "The value of data member x is: " << x;
}
};
int main()
{
MyClass o;
o.x = 123; // x is accessible to object o
o.printx(); // printx() is accessible to object o
}
我们的对象o
现在可以直接访问所有成员字段,因为它们都被标记为 public。无论访问描述符是什么,成员字段总是可以相互访问。这就是为什么成员函数printx()
可以访问成员字段x
并打印或更改其值。
私有访问成员只能被其他类成员访问,而不能被对象访问。附有完整注释的示例:
#include <iostream>
class MyClass
{
private:
int x; // x now has private access
public:
void printx()
{
std::cout << "The value of x is:" << x; // x is accessible to // printx()
}
};
int main()
{
MyClass o; // Create an object
o.x = 123; // Error, x has private access and is not accessible to // object o
o.printx(); // printx() is accessible from object o
}
我们的对象o
现在只能访问类的公共部分中的成员函数printx()
。它不能访问类的私有部分中的成员。
如果我们希望类成员可以被我们的对象访问,那么我们将把它们放在public:
区域内。如果我们不希望类成员被我们的对象访问,那么我们将把它们放入private:
区域。
我们希望数据成员拥有私有访问权限,而函数成员拥有公共访问权限。这样,我们的对象可以直接访问成员函数,但不能访问成员字段。还有另一个访问描述符叫做protected:
,我们将在本书后面学习继承时讨论它。
23.4 施工人员
构造器是与类同名的成员函数。为了初始化一个类的对象,我们使用构造器。构造器的目的是初始化一个类的对象。它构造一个对象,并可以为数据成员设置值。如果一个类有一个构造器,那么该类的所有对象都将被一个构造器调用初始化。
默认构造器
没有参数或者设置了默认参数的构造器称为默认构造器。它是一个可以不带参数调用的构造器:
#include <iostream>
class MyClass
{
public:
MyClass()
{
std::cout << "Default constructor invoked." << '\n';
}
};
int main()
{
MyClass o; // invoke a default constructor
}
默认构造器的另一个例子是带有默认参数的构造器:
#include <iostream>
class MyClass
{
public:
MyClass(int x = 123, int y = 456)
{
std::cout << "Default constructor invoked." << '\n';
}
};
int main()
{
MyClass o; // invoke a default constructor
}
如果代码中没有显式定义默认构造器,编译器将生成默认构造器。但是当我们定义一个我们自己的构造器,一个需要参数的构造器时,默认的构造器被移除,并且不是由编译器生成的。
对象初始化时调用构造器。它们不能被直接调用。
构造器可以有任意参数;在这种情况下,我们可以称它们为用户提供的构造器:
#include <iostream>
class MyClass
{
public:
int x, y;
MyClass(int xx, int yy)
{
x = xx;
y = yy;
}
};
int main()
{
MyClass o{ 1, 2 }; // invoke a user-provided constructor
std::cout << "User-provided constructor invoked." << '\n';
std::cout << o.x << ' ' << o.y;
}
在这个例子中,我们的类有两个类型为int
的数据字段和一个构造器。构造器接受两个参数,并将它们赋给数据成员。我们通过用MyClass o{ 1, 2 };
在初始化列表中提供参数来调用构造器
构造器没有返回类型,它们的目的是初始化其类的对象。
23.4.2 成员初始化
在前面的例子中,我们使用了一个构造器体和赋值来给每个类成员赋值。一个更好、更有效的初始化类对象的方法是在构造器的定义中使用构造器的成员初始化列表:
#include <iostream>
class MyClass
{
public:
int x, y;
MyClass(int xx, int yy)
: x{ xx }, y{ yy } // member initializer list
{
}
};
int main()
{
MyClass o{ 1, 2 }; // invoke a user-defined constructor
std::cout << o.x << ' ' << o.y;
}
成员初始值设定项列表以冒号开头,后面是成员名及其初始值设定项,其中每个初始化表达式用逗号分隔。这是初始化类数据成员的首选方式。
复制构造器
当我们用同一个类的另一个对象初始化一个对象时,我们调用一个复制构造器。如果我们不提供我们的复制构造器,编译器会生成一个默认的复制构造器来执行所谓的浅层复制。示例:
#include <iostream>
class MyClass
{
private:
int x, y;
public:
MyClass(int xx, int yy) : x{ xx }, y{ yy }
{
}
};
int main()
{
MyClass o1{ 1, 2 };
MyClass o2 = o1; // default copy constructor invoked
}
在这个例子中,我们用相同类型的对象o1
初始化对象o2
。这将调用默认的复制构造器。
我们可以提供自己的复制构造器。复制构造器有一个特殊的参数签名MyClass(const MyClass& rhs).
用户定义的复制构造器示例:
#include <iostream>
class MyClass
{
private:
int x, y;
public:
MyClass(int xx, int yy) : x{ xx }, y{ yy }
{
}
// user defined copy constructor
MyClass(const MyClass& rhs)
: x{ rhs.x }, y{ rhs.y } // initialize members with other object's // members
{
std::cout << "User defined copy constructor invoked.";
}
};
int main()
{
MyClass o1{ 1, 2 };
MyClass o2 = o1; // user defined copy constructor invoked
}
在这里,我们定义了自己的复制构造器,在该构造器中,我们用其他对象的数据成员显式初始化数据成员,并在控制台/标准输出中打印出一条简单的消息。
请注意,默认的复制构造器不能正确地复制某些类型的成员,比如指针、数组等。为了正确地制作副本,我们需要在复制构造器中定义自己的复制逻辑。这被称为深度复制。例如,对于指针,我们需要创建一个指针,并在我们的用户定义的复制构造器中为它所指向的对象赋值:
#include <iostream>
class MyClass
{
private:
int x;
int* p;
public:
MyClass(int xx, int pp)
: x{ xx }, p{ new int{pp} }
{
}
MyClass(const MyClass& rhs)
: x{ rhs.x }, p{ new int {*rhs.p} }
{
std::cout << "User defined copy constructor invoked.";
}
};
int main()
{
MyClass o1{ 1, 2 };
MyClass o2 = o1; // user defined copy constructor invoked
}
这里我们有两个构造器,一个是用户提供的常规构造器,另一个是用户自定义的复制构造器。第一个构造器初始化一个对象,并在这里调用:main
函数中的MyClass o1{ 1, 2 };
。
第二,用户定义的复制构造器在这里被调用:MyClass o2 = o1;
这个构造器现在正确地复制了来自int
和int*
成员字段的值。
在这个例子中,我们将指针作为成员字段。如果我们忽略了用户定义的复制构造器,而依赖于默认的复制构造器,那么只有int
成员字段会被正确地复制,而指针不会。在本例中,我们对此进行了纠正。
除了复制,还有一个移动语义,数据从一个对象移动到另一个对象。这个语义通过一个移动构造器和一个移动赋值操作符来表示。
23.4.4 复制转让
到目前为止,我们已经使用复制构造器用一个对象初始化另一个对象。我们也可以在初始化/创建对象后将值复制到对象中。为此,我们使用了一个拷贝赋值。简单地说,当我们在同一行使用=操作符用另一个对象初始化一个对象时,复制操作使用复制构造器:
MyClass copyfrom;
MyClass copyto = copyfrom; // on the same line, uses a copy constructor
当在一行上创建一个对象,然后将其分配给下一行时,它使用复制分配操作符从另一个对象复制数据:
MyClass copyfrom;
MyClass copyto;
copyto = copyfrom; // uses a copy assignment operator
复制赋值运算符具有以下签名:
MyClass& operator=(const MyClass& rhs)
要在类中定义用户定义的复制赋值操作符,我们使用:
class MyClass
{
public:
MyClass& operator=(const MyClass& rhs)
{
// implement the copy logic here
return *this;
}
};
注意重载的=操作符必须在末尾返回一个解引用的 this 指针。为了在类外定义一个用户定义的复制赋值操作符,我们使用:
class MyClass
{
public:
MyClass& operator=(const MyClass& rhs);
};
MyClass& MyClass::operator=(const MyClass& rhs)
{
// implement the copy logic here
return *this;
}
类似地,还有一个移动赋值操作符,我们将在本书后面讨论。在接下来的章节中会有更多关于操作符重载的内容。
移动构造器
除了复制,我们还可以将数据从一个对象移动到另一个对象。我们称之为移动语义。移动语义是通过移动构造器和移动赋值操作符实现的。从中移动数据的对象处于某种有效但未指定的状态。就执行速度而言,移动操作是高效的,因为我们不必制作副本。
Move 构造器接受名为的右值引用作为参数。
每个表达式都可以在赋值操作符的左边或右边找到自己。可以在左边使用的表达式称为左值,如变量、函数调用、类成员等。可以在赋值运算符右侧使用的表达式称为右值,如文字和其他表达式。
现在,move 语义接受对该右值的引用。右值引用类型的签名是带有双引用符号的T&、。因此,移动构造器的签名是:
MyClass (MyClass&& rhs)
为了将某些内容转换为右值引用,我们使用了 std::move 函数。这个函数将对象转换为一个右值引用。它不会移动任何东西。调用移动构造器的示例:
#include <iostream>
class MyClass { };
int main()
{
MyClass o1;
MyClass o2 = std::move(o1);
std::cout << "Move constructor invoked.";
// or MyClass o2{std::move(o1)};
}
在这个例子中,我们定义了一个名为o1
的MyClass
类型的对象。然后我们初始化第二个对象 o2,将对象o1
中的所有内容移动到o2.
中。为此,我们需要将o2
转换为带有std::move(o1)
的右值引用。这又调用了o2
的MyClass
移动构造器。
如果用户不提供移动构造器,编译器会提供隐式生成的默认移动构造器。
让我们指定我们自己的、用户定义的移动构造器:
#include <iostream>
#include <string>
class MyClass
{
private:
int x;
std::string s;
public:
MyClass(int xx, std::string ss) // user provided constructor
: x{ xx }, s{ ss }
{}
MyClass(MyClass&& rhs) // move constructor
:
x{ std::move(rhs.x) }, s{ std::move(rhs.s) }
{
std::cout << "Move constructor invoked." << '\n';
}
};
int main()
{
MyClass o1{ 1, "Some string value" };
MyClass o2 = std::move(o1);
}
此示例定义了一个具有两个数据成员和两个构造器的类。第一个构造器是一些用户提供的构造器,用于用提供的参数初始化数据成员。
第二个构造器是用户定义的 move 构造器,它接受一个名为rhs
的类型为MyClass&&
的右值引用参数。这个参数将成为我们的std::move(o1)
参数/对象。然后在构造器初始化列表中,我们也使用std::move
函数将数据字段从o1
移动到o2
。
移动分配
当我们声明一个对象,然后试图给它赋值一个右值引用时,调用移动赋值操作符。这是通过移动分配运算符完成的。移动赋值操作符的签名是:MyClass& operator=(MyClass&& otherobject)
。
要在类中定义用户定义的移动赋值操作符,我们使用:
class MyClass
{
public:
MyClass& operator=(MyClass&& otherobject)
{
// implement the copy logic here
return *this;
}
};
与任何赋值操作符重载一样,我们必须在最后返回一个解引用的 this 指针。为了在类外定义一个移动赋值操作符,我们使用:
class MyClass
{
public:
MyClass& operator=(const MyClass& rhs);
};
MyClass& MyClass::operator=(const MyClass& rhs)
{
// implement the copy logic here
return *this;
}
改编自移动构造器示例的移动赋值运算符示例如下:
#include <iostream>
#include <string>
class MyClass
{
private:
int x;
std::string s;
public:
MyClass(int xx, std::string ss) // user provided constructor
: x{ xx }, s{ ss }
{}
MyClass& operator=(MyClass&& otherobject) // move assignment operator
{
x = std::move(otherobject.x);
s = std::move(otherobject.s);
return *this;
}
};
int main()
{
MyClass o1{ 123, "This is currently in object 1." };
MyClass o2{ 456, "This is currently in object 2." };
o2 = std::move(o1); // move assignment operator invoked
std::cout << "Move assignment operator used.";
}
这里我们定义了两个对象,分别叫做o1
和o2
。然后我们通过使用std::move(o1)
表达式给对象o2
分配一个(对象o1
的)右值引用,试图将数据从对象o1
移动到o2
。这调用了我们的对象 o2 中的移动赋值操作符。移动赋值操作符实现本身使用std::move()
函数将每个数据成员转换为一个右值引用。
运算符超载
类的对象可以在表达式中作为操作数使用。例如,我们可以这样做:
myobject = otherobject;
myobject + otherobject;
myobject / otherobject;
myobject++;
++myobject;
这里一个类的对象被用作操作数。为此,我们需要重载复杂类型(如类)的操作符。据说我们需要重载它们来提供对一个类的对象的有意义的操作。有些运算符可以为类重载;有些不能。我们可以重载以下运算符:
算术运算符、二元运算符、布尔运算符、一元运算符、比较运算符、复合运算符、函数和下标运算符:
+ - * / % ^ & | ~ ! = < > == != <= >= += -= *= /= %= ^= &= |= << >> >>= <<= && || ++ -- , ->* -> () []
当重载类时,每个操作符都带有自己的签名和规则集。有些运算符重载是作为成员函数实现的,有些是作为非成员函数实现的。让我们为类重载一元前缀++ 操作符。它的签名是:
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass()
: x{ 0 }, d{ 0.0 }
{
}
// prefix operator ++
MyClass& operator++()
{
++x;
++d;
std::cout << "Prefix operator ++ invoked." << '\n';
return *this;
}
};
int main()
{
MyClass myobject;
// prefix operator
++myobject;
// the same as:
myobject.operator++();
}
在这个例子中,当在我们的类中调用时,重载的前缀 increment ++ 操作符将每个成员字段递增 1。我们也可以通过调用一个.operator
actual_operator_name
(
parameters_if_any
);
来调用一个操作符,比如.operator++();
通常运算符是相互依赖的,并且可以根据其他运算符来实现。为了实现后缀运算符 ++ ,我们将根据前缀运算符来实现它:
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass()
: x{ 0 }, d{ 0.0 }
{
}
// prefix operator ++
MyClass& operator++()
{
++x;
++d;
std::cout << "Prefix operator ++ invoked." << '\n';
return *this;
}
// postfix operator ++
MyClass operator++(int)
{
MyClass tmp(*this); // create a copy
operator++(); // invoke the prefix operator overload
std::cout << "Postfix operator ++ invoked." << '\n';
return tmp; // return old value
}
};
int main()
{
MyClass myobject;
// postfix operator
myobject++;
// is the same as if we had:
myobject.operator++(0);
}
请不要过于担心操作符重载的有些不一致的规则。记住,每个(一组)操作符都有自己的重载规则。
让我们来霸王一个二元运算符 += :
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd)
: x{ xx }, d{ dd }
{
}
MyClass& operator+=(const MyClass& rhs)
{
this->x += rhs.x;
this->d += rhs.d;
return *this;
}
};
int main()
{
MyClass myobject{ 1, 1.0 };
MyClass mysecondobject{ 2, 2.0 };
myobject += mysecondobject;
std::cout << "Used the overloaded += operator.";
}
现在,myobject
成员字段x
的值为 3,成员字段d
的值为 3.0。
让我们根据 += 运算符实现算术 + 运算符:
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd)
: x{ xx }, d{ dd }
{
}
MyClass& operator+=(const MyClass& rhs)
{
this->x += rhs.x;
this->d += rhs.d;
return *this;
}
friend MyClass operator+(MyClass lhs, const MyClass& rhs)
{
lhs += rhs;
return lhs;
}
};
int main()
{
MyClass myobject{ 1, 1.0 };
MyClass mysecondobject{ 2, 2.0 };
MyClass myresult = myobject + mysecondobject;
std::cout << "Used the overloaded + operator.";
}
总结:
当我们需要对一个类的对象执行算术、逻辑和其他操作时,我们需要重载适当的操作符。重载每个操作符都有规则和签名。某些运算符可以根据其他运算符来实现。关于运算符重载规则的完整列表,请参考位于 https://en.cppreference.com/w/cpp/language/operators
的 C++ 参考。
23.6 解构器
正如我们前面看到的,构造器是一个成员函数,当对象初始化时被调用。类似地,析构函数是一个在对象被销毁时被调用的成员函数。析构函数的名称是波浪号~后跟一个类名:
class MyClass
{
public:
MyClass() {} // constructor
~MyClass() {} // destructor
};
析构函数不带参数,每个类只有一个析构函数。示例:
#include <iostream>
class MyClass
{
public:
MyClass() {} // constructor
~MyClass()
{
std::cout << "Destructor invoked.";
} // destructor
};
int main()
{
MyClass o;
} // destructor invoked here, when o gets out of scope
当一个对象超出范围或者一个指向对象的指针被删除时,析构函数被调用。我们不应该直接调用析构函数。
析构函数可以用来清理被占用的资源。示例:
#include <iostream>
class MyClass
{
private:
int* p;
public:
MyClass()
: p{ new int{123} }
{
std::cout << "Created a pointer in the constructor." << '\n';
}
~MyClass()
{
delete p;
std::cout << "Deleted a pointer in the destructor." << '\n';
}
};
int main()
{
MyClass o; // constructor invoked here
} // destructor invoked here
这里我们在构造器中为指针分配内存,在析构函数中释放内存。这种类型的资源分配/解除分配被称为 RAII,或者资源获取是初始化。不应直接调用析构函数。
Important
new
和delete
的使用,以及现代 C++ 中原始指针的使用,不鼓励。我们应该使用智能指针来代替。我们将在本书的后面讨论它们。让我们为这节课的介绍部分做一些练习。*
二十四、练习
24.1 类实例
编写一个程序,定义一个名为 MyClass 的空类,并在主函数中创建一个 MyClass 的实例。
class MyClass
{
};
int main()
{
MyClass o;
}
24.2 具有数据成员的类
编写一个程序,定义一个名为 MyClass 的类,该类有三个类型为char
、int,
和bool
的数据成员。在主函数中创建该类的一个实例。
class MyClass
{
char c;
int x;
bool b;
};
int main()
{
MyClass o;
}
24.3 具有成员函数的类
编写一个程序,用一个名为printmessage()
的成员函数定义一个名为MyClass
的类。在类中定义printmessage()
成员函数,并让它输出“Hello World”字符串。创建该类的一个实例,并使用该对象调用该类的成员函数。
#include <iostream>
class MyClass
{
public:
void printmessage()
{
std::cout << "Hello World.";
}
};
int main()
{
MyClass o;
o.printmessage();
}
24.4 具有数据和函数成员的类
编写一个程序,用一个名为printmessage()
的成员函数定义一个名为MyClass
的类。在类外定义printmessage()
成员函数,并让它输出"Hello World."
字符串。创建该类的一个实例,并使用该对象调用成员函数。
#include <iostream>
class MyClass
{
public:
void printmessage();
};
void MyClass::printmessage()
{
std::cout << "Hello World.";
}
int main()
{
MyClass o;
o.printmessage();
}
24.5 类访问描述符
编写一个程序,用一个名为 x 的类型为int
的私有数据成员和两个成员函数定义一个名为MyClass
的类。名为setx(int myvalue)
的第一个成员函数将把 x 的值设置为其参数myvalue
。第二个成员函数名为getx()
,类型为int
,返回值为 x 。创建类的实例,并使用对象来访问这两个成员函数。
#include <iostream>
class MyClass
{
private:
int x;
public:
void setx(int myvalue)
{
x = myvalue;
}
int getx()
{
return x;
}
};
int main()
{
MyClass o;
o.setx(123);
std::cout << "The value of x is: " << o.getx();
}
24.6 用户定义的默认构造器和析构函数
编写一个程序,用用户定义的默认构造器和析构函数定义一个名为MyClass
的类。在类外定义构造器和析构函数。两个成员函数都将在标准输出上输出一个自由选择的文本。在函数 main 中创建一个类的对象。
#include <iostream>
class MyClass
{
public:
MyClass();
~MyClass();
};
MyClass::MyClass()
{
std::cout << "Constructor invoked." << '\n';
}
MyClass::~MyClass()
{
std::cout << "Destructor invoked." << '\n';
}
int main()
{
MyClass o;
}
24.7 构造器初始化列表
编写一个程序,定义一个名为MyClass,
的类,它有两个类型为int
和double
的私有数据成员。在类外部,定义一个用户提供的接受两个参数的构造器。构造器使用初始值设定项用参数初始化两个数据成员。在类外部,定义一个名为printdata()
的函数,它打印两个数据成员的值。
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd);
void printdata();
};
MyClass::MyClass(int xx, double dd)
: x{ xx }, d{ dd }
{
}
void MyClass::printdata()
{
std::cout << " The value of x: " << x << ", the value of d: " << d << '\n';
}
int main()
{
MyClass o{ 123, 456.789 };
o.printdata();
}
24.8 用户定义的复制构造器
编写一个程序,用任意数据字段定义一个名为MyClass
的类。使用初始化数据成员的参数编写用户定义的构造器。编写一个用户定义的复制构造器来复制所有成员。创建一个名为 o1 的类的对象,并用值初始化它。创建一个名为 o2 的类的另一个对象,并用对象 o 初始化它。
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd); // user-provided constructor
MyClass(const MyClass& rhs); // user-defined copy constructor
void printdata();
};
MyClass::MyClass(int xx, double dd)
: x{ xx }, d{ dd }
{}
MyClass::MyClass(const MyClass& rhs)
: x{ rhs.x }, d{ rhs.d }
{}
void MyClass::printdata()
{
std::cout << "X is: " << x << ", d is: " << d << '\n';
}
int main()
{
MyClass o1{ 123, 456.789 }; // invokes a user-provided constructor
MyClass o2 = o1; // invokes a user-defined copy constructor
o1.printdata();
o2.printdata();
}
24.9 用户定义的移动构造器
编写一个程序,用两个数据成员定义一个类,一个用户提供的构造器,一个用户提供的移动构造器和一个打印数据的成员函数。在主程序中调用 move 构造器。打印移动到的对象数据字段。
#include <iostream>
#include <string>
class MyClass
{
private:
double d;
std::string s;
public:
MyClass(double dd, std::string ss) // user-provided constructor
: d{ dd }, s{ ss }
{}
MyClass(MyClass&& otherobject) // user-defined move constructor
:
d{ std::move(otherobject.d) }, s{ std::move(otherobject.s) }
{
std::cout << "Move constructor invoked." << '\n';
}
void printdata()
{
std::cout << "The value of doble is: " << d << ", the value of string is: " << s << '\n';
}
};
int main()
{
MyClass o1{ 3.14, "This was in object 1" };
MyClass o2 = std::move(o1); // invokes the move constructor
o2.printdata();
}
24.10 重载算术运算符
写一个重载算术运算符的程序——用一个复合算术运算符-=。打印出结果对象成员字段的值。
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd)
: x{ xx }, d{ dd }
{
}
void printvalues()
{
std::cout << "The values of x is: " << x << ", the value of d is: " << d;
}
MyClass& operator-=(const MyClass& rhs)
{
this->x -= rhs.x;
this->d -= rhs.d;
return *this;
}
friend MyClass operator-(MyClass lhs, const MyClass& rhs)
{
lhs -= rhs;
return lhs;
}
};
int main()
{
MyClass myobject{ 3, 3.0 };
MyClass mysecondobject{ 1, 1.0 };
MyClass myresult = myobject - mysecondobject;
myresult.printvalues();
}
二十五、类——继承和多态
在这一章中,我们将讨论一些面向对象编程的基本构件,比如继承和多态。
25.1 继承
我们可以从现有的类构建一个类。据说一个类可以从一个已有的类派生。这被称为继承,是面向对象编程的支柱之一,缩写为 OOP。为了从现有的类中派生出一个类,我们编写:
class MyDerivedClass : public MyBaseClass {};
一个简单的例子是:
class MyBaseClass
{
};
class MyDerivedClass : public MyBaseClass
{
};
int main()
{
}
在这个例子中,MyDerivedClass
继承了MyBaseClass
。
让我们把术语抛开。据说MyDerivedClass
是从MyBaseClass
派生出,或者说MyBaseClass
是MyDerivedClass
的基类。也有人说MyDerivedClass
就是 ??。它们的意思都一样。
现在这两个阶层有了某种关系。这种关系可以通过不同的命名约定来表达,但是最重要的一个是继承。派生类和派生类的对象可以访问基类的public
成员:
class MyBaseClass
{
public:
char c;
int x;
};
class MyDerivedClass : public MyBaseClass
{
// c and x also accessible here
};
int main()
{
MyDerivedClass o;
o.c = 'a';
o.x = 123;
}
以下示例引入了名为protected:
的新访问描述符。派生类本身可以访问基类的protected
成员。protected
访问描述符允许访问基类和派生类,但不允许访问对象:
class MyBaseClass
{
protected:
char c;
int x;
};
class MyDerivedClass : public MyBaseClass
{
// c and x also accessible here
};
int main()
{
MyDerivedClass o;
o.c = 'a'; // Error, not accessible to object
o.x = 123; // error, not accessible to object
}
派生类无法访问基类的private
成员:
class MyBaseClass
{
private:
char c;
int x;
};
class MyDerivedClass : public MyBaseClass
{
// c and x NOT accessible here
};
int main()
{
MyDerivedClass o;
o.c = 'a'; // Error, not accessible to object
o.x = 123; // error, not accessible to object
}
派生类继承基类的公共和受保护成员,并且可以引入自己的成员。一个简单的例子:
class MyBaseClass
{
public:
char c;
int x;
};
class MyDerivedClass : public MyBaseClass
{
public:
double d;
};
int main()
{
MyDerivedClass o;
o.c = 'a';
o.x = 123;
o.d = 456.789;
}
这里我们从MyBaseClass
类继承了一切,并在MyDerivedClass
中引入了一个新的成员字段,名为d
。所以,有了MyDerivedClass
,我们正在扩展MyBaseClass
的能力。字段d
仅存在于MyDerivedClass
中,并且可被派生类及其对象访问。对于MyBaseClass
类,它是不可访问的,因为它不存在于那里。
请注意,还有其他继承类的方法,比如通过受保护的和私有的继承,但是像class MyDerivedClass : public MyBaseClass
这样的公共继承是最广泛使用的,我们现在将坚持使用它。
派生类本身可以是基类。示例:
class MyBaseClass
{
public:
char c;
int x;
};
class MyDerivedClass : public MyBaseClass
{
public:
double d;
};
class MySecondDerivedClass : public MyDerivedClass
{
public:
bool b;
};
int main()
{
MySecondDerivedClass o;
o.c = 'a';
o.x = 123;
o.d = 456.789;
o.b = true;
}
现在我们的类拥有了MyDerivedClass
所拥有的一切,这包括了MyBaseClass
所拥有的一切,外加一个额外的bool
字段。据说继承产生了一个特殊的层次类。
当我们想要扩展类的功能时,这种方法被广泛使用。
派生类与基类兼容。指向派生类的指针与指向基类的指针兼容。这允许我们利用多态性,,我们将在下一章讨论。
25.2 多态性
据说派生类是一个基类。它的类型与基类类型兼容。此外,指向派生类的指针与指向基类的指针兼容。这很重要,所以让我们重复一下:指向派生类的指针与指向基类的指针是兼容的。与继承一起,这被用来实现被称为多态性的功能。多态性意味着对象可以变成不同的类型。C++ 中的多态性是通过一个称为虚函数的接口实现的。虚函数是其行为可以在后续派生类中被重写的函数。我们的指针/对象将变成不同的类型来调用适当的函数。示例:
#include <iostream>
class MyBaseClass
{
public:
virtual void dowork()
{
std::cout << "Hello from a base class." << '\n';
}
};
class MyDerivedClass : public MyBaseClass
{
public:
void dowork()
{
std::cout << "Hello from a derived class." << '\n';
}
};
int main()
{
MyBaseClass* o = new MyDerivedClass;
o->dowork();
delete o;
}
在这个例子中,我们有一个简单的继承,其中MyDerivedClass
从MyBaseClass
派生而来。
MyBaseClass
类有一个名为dowork()
的函数,带有一个virtual
描述符。虚拟意味着该函数可以在后续的派生类中被重写/重定义,并且适当的版本将通过多态对象被调用。派生类中有一个同名的函数和相同类型的参数(在我们的例子中没有)。
在我们的主程序中,我们通过基类指针创建了一个MyDerivedClass
类的实例。使用箭头操作符->
我们调用函数的适当版本。这里,o 对象将变成不同的类型来调用适当的函数。这里它调用派生版本。这就是这个概念被称为多态性的原因。
如果派生类中没有dowork()
函数,它将调用基类版本:
#include <iostream>
class MyBaseClass
{
public:
virtual void dowork()
{
std::cout << "Hello from a base class." << '\n';
}
};
class MyDerivedClass : public MyBaseClass
{
public:
};
int main()
{
MyBaseClass* o = new MyDerivedClass;
o->dowork();
delete o;
}
通过在函数声明的末尾指定= 0;
,函数可以是纯虚拟的。纯虚函数没有定义,也叫接口。纯虚函数必须在派生类中重新定义。至少有一个纯虚函数的类被称为抽象类,不能被实例化。它们只能用作基类。示例:
#include <iostream>
class MyAbstractClass
{
public:
virtual void dowork() = 0;
};
class MyDerivedClass : public MyAbstractClass
{
public:
void dowork()
{
std::cout << "Hello from a derived class." << '\n';
}
};
int main()
{
MyAbstractClass* o = new MyDerivedClass;
o->dowork();
delete o;
}
需要补充的一点是,如果要在多态场景中使用基类,它必须有一个virtual
析构函数。这确保了通过继承链适当地释放通过基类指针访问的对象:
class MyBaseClass
{
public:
virtual void dowork() = 0;
virtual ~MyBaseClass() {};
};
请记住,在现代 C++ 中,不鼓励使用运算符 new 和原始指针。我们应该使用智能指针。在这本书的后面会有更多的内容。
因此,面向对象编程的三个支柱是:
-
包装
-
继承
-
多态性
例如,封装就是将字段分组到不同的可见区域,对用户隐藏实现,并暴露接口。
继承是一种机制,我们可以通过从基类继承来创建类。继承创建了一定的类层次结构和它们之间的关系。
多态性是一种在运行时对象转变成不同类型的能力,确保调用正确的函数。这是通过继承、虚函数和重写函数以及基类和派生类指针来实现的。*
二十六、练习
26.1 继承
写一个程序,定义一个叫 Person 的基类。该类有以下成员:
-
名为名为的类型为 std::string 的数据成员
-
单参数,用户定义的构造器,初始化名
-
一个类型为 std::string 的 getter 函数,名为 getname(),,返回名字的值
然后,编写一个名为 Student、的类,它继承了类 Person 。班级学生有以下成员:
-
名为学期的整数数据成员
-
用户提供的构造器,用于初始化名称和学期字段
-
一个类型为 int 的 getter 函数,名为getsteam(),,返回学期的值
简而言之,我们将有一个基类 Person ,并在派生的 Student 类中扩展它的功能:
#include <iostream>
#include <string>
class Person
{
private:
std::string name;
public:
explicit Person(const std::string& aname)
: name{ aname }
{}
std::string getname() const { return name; }
};
class Student : public Person
{
private:
int semester;
public:
Student(const std::string& aname, int asemester)
: Person::Person{ aname }, semester{ asemester }
{}
int getsemester() const { return semester; }
};
int main()
{
Person person{ "John Doe." };
std::cout << person.getname() << '\n';
Student student{ "Jane Doe", 2 };
std::cout << student.getname() << '\n';
std::cout << "Semester is: " << student.getsemester() << '\n';
}
说明:我们有两个类,一个是基类(Person),一个(Student)是派生类。单参数构造器应该用explicit
标记,以防止编译器进行隐式转换。人用户提供的单参数构造器就是这种情况:
explicit Person(const std::string& aname)
: name{ aname }
{}
不修改成员字段的成员函数应该标记为 const 。成员函数中的 const 修饰符保证函数不会修改数据成员,并且更易于编译器优化代码。两个 getname() 都是这种情况:
std::string getname() const { return name; }
和get 学期()成员函数:
int getsemester() const { return semester; }
学生类继承自人类和 ads 附加数据字段学期和成员函数get 学期()。Student类拥有基类所拥有的一切,并且通过添加新的字段扩展了基类的功能。学生的用户提供的构造器使用其初始化列表中的基类构造器来初始化名称字段:
Student(const std::string& aname, int asemester)
: Person::Person{ aname }, semester{ asemester }
{}
在 main()程序中,我们实例化了两个类:
Person person{ "John Doe." };
以及:
Student student{ "Jane Doe", 2 };
并调用它们的成员函数:
person.getname();
以及:
student.getname();
student.getsemester();
Important
在本书的后面,当我们讨论智能指针时,我们将做一个多态性练习。这是因为我们不想使用new
和delete
以及原始指针。
二十七、静态描述符
static
描述符表示对象将有一个静态存储持续时间。静态对象的内存空间在程序启动时分配,在程序结束时释放。程序中只存在一个静态对象的实例。如果一个局部变量被标记为 static,那么它的空间在程序控件第一次遇到它的定义时被分配,当程序退出时被释放。
要在函数中定义局部静态变量,我们使用:
#include <iostream>
void myfunction()
{
static int x = 0; // defined only the first time, skipped every other // time
x++;
std::cout << x << '\n';
}
int main()
{
myfunction(); // x == 1
myfunction(); // x == 2
myfunction(); // x == 3
}
这个变量在程序第一次遇到这个函数时被初始化。该变量的值在函数调用中保持不变。这是什么意思?我们对其进行的最后一次更改保持不变。它不会在每次函数调用时都初始化为 0,只有在第一次调用时才会初始化。
这很方便,因为我们不必将值存储在某个全局变量 x 中。
我们可以定义静态类成员字段。静态类成员不是对象的一部分。它们独立于一个类的对象而存在。我们在类内部声明一个静态数据成员,在类外部只定义一次:
#include <iostream>
class MyClass
{
public:
static int x; // declare a static data member
};
int MyClass::x = 123; // define a static data member
int main()
{
MyClass::x = 456; // access a static data member
std::cout << "Static data member value is: " << MyClass::x;
}
这里我们声明了一个类中的静态数据成员。然后我们在类外定义了它。当在类外定义静态成员时,我们不需要使用静态描述符。然后,我们通过使用MyClass::data_member_name
符号访问数据成员。
为了定义一个静态成员函数,我们在函数声明前添加了 static 关键字。类外的函数定义不使用静态关键字:
#include <iostream>
class MyClass
{
public:
static void myfunction(); // declare a static member function
};
// define a static member function
void MyClass::myfunction()
{
std::cout << "Hello World from a static member function.";
}
int main()
{
MyClass::myfunction(); // call a static member function
}
二十八、模板
模板是支持所谓的通用编程的机制。泛型广义上意味着我们可以定义一个函数或类,而不用担心它接受什么类型。
我们使用一些通用类型来定义这些函数和类。当我们实例化它们时,我们使用一个具体的类型。所以,当我们想要定义一个几乎可以接受任何类型的类或函数时,我们可以使用模板。
我们通过键入以下内容来定义模板:
template <typename T>
// the rest of our function or class code
这与我们使用:
template <class T>
// the rest of our function or class code
这里代表一个类型名。哪种类型?嗯,任何类型。这里 T 的意思是,对于所有类型的 T 。
让我们创建一个可以接受任何类型参数的函数:
#include <iostream>
template <typename T>
void myfunction(T param)
{
std::cout << "The value of a parameter is: " << param;
}
int main()
{
}
为了实例化一个函数模板,我们通过提供一个用尖括号括起来的特定类型名来调用一个函数:
#include <iostream>
template <typename T>
void myfunction(T param)
{
std::cout << "The value of a parameter is: " << param;
}
int main()
{
myfunction<int>(123);
myfunction<double>(123.456);
myfunction<char>('A');
}
我们可以把 T 看作一个特定类型的占位符,就是我们在实例化一个模板时提供的类型。因此,我们现在把我们的具体类型。整洁,哈?这样,我们可以对不同的类型使用相同的代码。
模板可以有多个参数。我们简单地列出模板参数,并用逗号分隔它们。接受两个模板参数的函数模板示例:
#include <iostream>
template <typename T, typename U>
void myfunction(T t, U u)
{
std::cout << "The first parameter is: " << t << '\n';
std::cout << "The second parameter is: " << u << '\n';
}
int main()
{
int x = 123;
double d = 456.789;
myfunction<int, double>(x, d);
}
为了定义一个类模板,我们使用:
#include <iostream>
template <typename T>
class MyClass {
private:
T x;
public:
MyClass(T xx)
:x{ xx }
{
}
T getvalue()
{
return x;
}
};
int main()
{
MyClass<int> o{ 123 };
std::cout << "The value of x is: " << o.getvalue() << '\n';
MyClass<double> o2{ 456.789 };
std::cout << "The value of x is: " << o2.getvalue() << '\n';
}
这里,我们定义了一个简单的类模板。这个类接受 t 类型。我们在类中任何合适的地方使用这些类型。在我们的主函数中,我们用具体的类型int
和double
实例化这些类。我们只需使用一个模板,而不必为两个或更多不同的类型编写相同的代码。
要在类外定义一个类模板成员函数,我们需要通过在成员函数定义前添加适当的模板声明来使它们成为模板。在这样的定义中,必须用模板参数调用类名。简单的例子:
#include <iostream>
template <typename T>
class MyClass {
private:
T x;
public:
MyClass(T xx);
};
template <typename T>
MyClass<T>::MyClass(T xx)
: x{xx}
{
std::cout << "Constructor invoked. The value of x is: " << x << '\n';
}
int main()
{
MyClass<int> o{ 123 };
MyClass<double> o2{ 456.789 };
}
让我们把它变得简单些。如果我们有一个只有一个 void 成员函数的类模板,我们可以写:
template <typename T>
class MyClass {
public:
void somefunction();
};
template <typename T>
void MyClass<T>::somefunction()
{
// the rest of the code
}
如果我们有一个带有 T 类型的单个成员函数的类模板,我们将使用:
template <typename T>
class MyClass {
public:
T genericfunction();
};
template <typename T>
T MyClass<T>::genericfunction()
{
// the rest of the code
}
现在,如果我们在一个类中有它们,并且我们想在类范围之外定义它们,我们将使用:
template <typename T>
class MyClass {
public:
void somefunction();
T genericfunction();
};
template <typename T>
void MyClass<T>::somefunction()
{
// the rest of the code
}
template <typename T>
T MyClass<T>::genericfunction()
{
// the rest of the code
}
模板专门化
如果我们希望我们的模板对于一个特定的类型有不同的行为,我们提供了所谓的模板专门化。如果参数是某种类型的,我们有时需要不同的代码。为此,我们在函数或类前加上:
template <>
// the rest of our code
为了将模板函数特殊化为类型 int ,我们编写:
#include <iostream>
template <typename T>
void myfunction(T arg)
{
std::cout << "The value of an argument is: " << arg << '\n';
}
template <>
// the rest of our code
void myfunction(int arg)
{
std::cout << "This is a specialization int. The value is: " << arg << '\n';
}
int main()
{
myfunction<char>('A');
myfunction<double>(345.678);
myfunction<int>(123); // invokes specialization
}
二十九、枚举
枚举,简称 enum ,是一种类型,其值是用户自定义的命名常量,称为枚举器。
有两种枚举:未划分范围的枚举和 ?? 范围的枚举。未划分的枚举类型可以用以下内容定义:
enum MyEnum
{
myfirstvalue,
mysecondvalue,
mythirdvalue
};
为了声明一个枚举类型的变量MyEnum
,我们写:
enum MyEnum
{
myfirstvalue,
mysecondvalue,
mythirdvalue
};
int main()
{
MyEnum myenum = myfirstvalue;
myenum = mysecondvalue; // we can change the value of our enum object
}
每个枚举数都有一个基础类型的值。我们可以改变这些:
enum MyEnum
{
myfirstvalue = 10,
mysecondvalue,
mythirdvalue
};
这些未划分的枚举让它们的枚举器泄漏到一个外部作用域,在这个作用域中定义了枚举类型本身。旧枚举最好避免。比起这些老派的、未分类的枚举,我更喜欢范围内的枚举。限定范围的枚举不会将其枚举数泄漏到外部范围,也不能隐式转换为其他类型。为了定义一个限定了作用域的枚举,我们编写:
enum class MyEnum
{
myfirstvalue,
mysecondvalue,
mythirdvalue
};
要声明枚举类(作用域枚举)类型的变量,我们编写:
enum class MyEnum
{
myfirstvalue,
mysecondvalue,
mythirdvalue
};
int main()
{
MyEnum myenum = MyEnum::myfirstvalue;
}
为了访问枚举器值,我们在枚举器前面加上枚举名称和范围解析操作符::比如MyEnum::myfirstvalue, MyEnum:: mysecondvalue,
等。
使用这些枚举,枚举数名称仅在枚举内部范围内定义,并隐式转换为基础类型。我们可以指定作用域枚举的基础类型:
enum class MyCharEnum : char
{
myfirstvalue,
mysecondvalue,
mythirdvalue
};
我们还可以通过指定值来更改枚举数的初始基础值:
enum class MyEnum
{
myfirstvalue = 15,
mysecondvalue,
mythirdvalue = 30
};
摘要:比起旧的简单的未划分的枚举,更喜欢枚举类枚举(限定了作用域的枚举)。当我们的对象需要一组预定义的命名值中的一个值时,使用枚举。
三十、练习
30.1 静态变量
写一个程序来检查一个函数被主程序调用了多少次。为此,我们将在函数中使用一个静态变量,该变量将在 main()中每次调用该函数时递增:
#include <iostream>
void myfunction()
{
static int counter = 0;
counter++;
std::cout << "The function is called " << counter << " time(s)." << '\n';
}
int main()
{
myfunction();
myfunction();
for (int i = 0; i < 5; i++)
{
myfunction();
}
}
30.2 静态数据成员
编写一个程序,用一个 std::string 类型的静态数据成员定义一个类。将数据成员公开。在类外定义静态数据成员。从 main()函数中更改静态数据成员值:
#include <iostream>
#include <string>
class MyClass
{
public:
static std::string name;
};
std::string MyClass::name = "John Doe";
int main()
{
std::cout << "Static data member value: " << MyClass::name << '\n';
MyClass::name = "Jane Doe";
std::cout << "Static data member value: " << MyClass::name << '\n';
}
30.3 静态成员函数
编写一个程序,用一个静态成员函数和一个常规成员函数定义一个类。公开这些函数。在类外定义这两个成员函数。在 main()中访问这两个函数:
#include <iostream>
#include <string>
class MyClass
{
public:
static void mystaticfunction();
void myfunction();
};
void MyClass::mystaticfunction()
{
std::cout << "Hello World from a static member function." << '\n';
}
void MyClass::myfunction()
{
std::cout << "Hello World from a regular member function." << '\n';
}
int main()
{
MyClass::mystaticfunction();
MyClass myobject;
myobject.myfunction();
}
30.4 功能模板
编写一个程序,为两个数相加的函数定义一个模板。数字具有相同的泛型类型 T,并作为参数传递给函数。使用 int 和 double 类型实例化 main()中的函数:
#include <iostream>
template <typename T>
T mysum(T x, T y)
{
return x + y;
}
int main()
{
int intresult = mysum<int>(10, 20);
std::cout << "The integer sum result is: " << intresult << '\n';
double doubleresult = mysum<double>(123.456, 789.101);
std::cout << "The double sum result is: " << doubleresult << '\n';
}
30.5 课程模板
编写一个程序,定义一个简单的类模板,该模板包含一个泛型数据成员、一个构造器、一个泛型 getter 函数和一个 setter 成员函数。在 main()函数中为 int 和 double 类型实例化一个类:
#include <iostream>
template <typename T>
class MyClass
{
private:
T x;
public:
MyClass(T xx)
: x{ xx }
{}
T getx() const
{
return x;
}
void setx(T ax)
{
x = ax;
}
};
int main()
{
MyClass<int> o{123};
std::cout << "The value of the data member is: " << o.getx() << '\n';
o.setx(456);
std::cout << "The value of the data member is: " << o.getx() << '\n';
MyClass<double> o2{ 4.25 };
std::cout << "The value of the data member is: " << o2.getx() << '\n';
o2.setx(6.28);
std::cout << "The value of the data member is: " << o2.getx() << '\n';
}
30.6 作用域枚举
编写一个程序,定义一个代表一周中各天的作用域枚举。创建该枚举的对象,为其赋值,检查该值是否为周一,如果是,将对象值更改为另一个枚举值:
#include <iostream>
enum class Days
{
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
};
int main()
{
Days myday = Days::Monday;
std::cout << "The enum value is now Monday." << '\n';
if (myday == Days::Monday)
{
myday = Days::Friday;
}
std::cout << "Nobody likes Mondays. The value is now Friday.";
}
30.7 开关中的枚举
写一个定义枚举的程序。当在 switch 语句中使用枚举对象时,创建它。使用 switch 语句打印对象的值:
#include <iostream>
enum class Colors
{
Red,
Green,
Blue
};
int main()
{
Colors mycolors = Colors::Green;
switch (mycolors)
{
case Colors::Red:
std::cout << "The color is Red." << '\n';
break;
case Colors::Green:
std::cout << "The color is Green." << '\n';
break;
case Colors::Blue:
std::cout << "The color is Blue." << '\n';
break;
default:
break;
}
}
三十一、组织代码
我们可以将 C++ 代码分成多个文件。按照惯例,有两种类型的文件可以存储我们的 C++ 源代码:头文件(头文件)和源文件。
31.1 头文件和源文件
头文件是我们通常放置各种声明的源代码文件。头文件通常有。h (或者。 hpp 分机。源文件是我们可以存储定义和主程序的文件。他们通常有。cpp (或)。cc 扩展名。
然后我们使用#include
预处理指令将头文件包含到源文件中。为了包含一个标准的库头文件,我们使用了#include
语句,后跟一个不带扩展名的头文件名称,用尖括号<headername>
括起来。示例:
#include <iostream>
#include <string>
// etc
为了包含用户定义的头文件,我们使用#include 语句,后跟一个完整的头文件名称,扩展名用双引号括起来。示例:
#include "myheader.h"
#include "otherheader.h"
// etc
现实情况是,有时我们需要同时包含标准库头和用户定义的头:
#include <iostream>
#include "myheader.h"
// etc
编译器将头文件和源文件中的代码缝合在一起,产生一个所谓的翻译单元。然后编译器使用这个文件创建一个目标文件。然后链接器将目标文件链接在一起,创建一个程序。
我们应该将声明和常量放在头文件中,将定义和可执行代码放在源文件中。
31.2 顶盖防护装置
多个源文件可能包含同一个头文件。为了确保我们的头在编译过程中只被包含一次,我们使用了一种叫做头保护的机制。它确保我们的头内容在编译过程中只包含一次。我们用以下宏将头文件中的代码括起来:
#ifndef MY_HEADER_H
#define MY_HEADER_H
// header file source code
// goes here
#endif
这种方法确保头文件中的代码在编译阶段只包含一次。
31.3 名称空间
到目前为止,我们已经看到了如何将 C++ 代码的各个部分分组到单独的文件中,这些文件被称为头文件和源文件。还有另一种方法可以对 C++ 的各个部分进行逻辑分组,那就是通过命名空间。命名空间是有名称的作用域。为了声明一个名称空间,我们编写:
namespace MyNameSpace
{
}
为了在名称空间中声明对象,我们使用:
namespace MyNameSpace
{
int x;
double d;
}
为了在名称空间之外引用这些对象,我们使用它们的完全限定名。这意味着我们使用名称空间名称::我们的对象符号。我们在声明对象的名称空间之外定义对象的示例:
namespace MyNameSpace
{
int x;
double d;
}
int main()
{
MyNameSpace::x = 123;
MyNameSpace::d = 456.789;
}
要将整个名称空间引入当前范围,我们可以使用using
-指令:
namespace MyNameSpace
{
int x;
double d;
}
using namespace MyNameSpace;
int main()
{
x = 123;
d = 456.789;
}
如果我们的代码中有几个名称相同的独立名称空间,这意味着我们在扩展那个名称空间。示例:
namespace MyNameSpace
{
int x;
double d;
}
namespace MyNameSpace
{
char c;
bool b;
}
int main()
{
MyNameSpace::x = 123;
MyNameSpace::d = 456.789;
MyNameSpace::c = 'a';
MyNameSpace::b = true;
}
现在,在我们的MyNameSpace
名称空间中有了 x、d、c 和 b。我们是在扩展的MyNameSpace
,而不是重新定义它。
一个命名空间可以分布在多个文件中,包括头文件和源文件。我们经常会看到生产代码被包装到名称空间中。将代码按逻辑分组到名称空间是一种很好的机制。
两个不同名称的命名空间可以保存一个同名的对象。由于每个名称空间都是一个不同的作用域,它们现在用相同的名称声明了两个不同的不相关的对象。它可以防止名称冲突:
#include <iostream>
namespace MyNameSpace
{
int x;
}
namespace MySecondNameSpace
{
int x;
}
int main()
{
MyNameSpace::x = 123;
MySecondNameSpace::x = 456;
std::cout << "1st x: " << MyNameSpace::x << ", 2nd x: " << MySecondNameSpace::x;
}
三十二、练习
32.1 头文件和源文件
写一个在头文件中声明任意函数的程序。头文件叫做 myheader.h 在主程序源文件 source.cpp 里面定义这个函数。main
函数也位于 source.cpp 文件中。将头文件包含到我们的源文件中并调用函数。
myheader.h:
void myfunction(); //function declaration
source.cpp:
#include "myheader.h" //include the header
#include <iostream>
int main()
{
myfunction();
}
// function definition
void myfunction()
{
std::cout << "Hello World from multiple files.";
}
32.2 多个源文件
写一个在头文件中声明任意函数的程序。头文件叫做 mylibrary.h 在源文件里面定义一个叫做 mylibrary.cpp 的函数。main
函数位于第二个源文件 source.cpp 文件中。在两个源文件中包含头文件并调用函数。
mylibrary.h .:
void myfunction(); //function declaration
my library . CPP:my library .我的产品目录:
#include "mylibrary.h"
#include <iostream>
// function definition
void myfunction()
{
std::cout << "Hello World from multiple files.";
}
source.cpp:
#include "mylibrary.h"
int main()
{
myfunction();
}
说明:
这个程序有三个文件:
-
一个名为 mylibrary.h 的头文件,我们在其中放置了函数声明。
-
一个名为 mylibrary.cpp 的源文件,我们将函数定义放在这里。我们将头文件 mylibrary.h 包含到 mylibrary.cpp 源文件中。
-
主程序所在的名为 source.cpp 的源文件。我们还在这个源文件中包含了 mylibrary.h 头文件。
因为我们的头文件包含在多个源文件中,所以我们应该将头文件保护宏放入其中。 mylibrary.h 文件现在看起来像这样:
#ifndef MY_LIBRARY_H
#define MY_LIBRARY_H
void myfunction();
#endif // !MY_LIBRARY_H
用 g++ 编译一个有多个源文件的程序,我们使用:
g++ source.cpp mylibrary.cpp
Visual Studio IDE 自动处理多文件编译。
32.3 名称空间
编写一个程序,在命名空间内声明一个函数,在命名空间外定义该函数。调用主程序中的函数。命名空间和函数名是任意的。
#include <iostream>
namespace MyNameSpace
{
void myfunction();
}
void MyNameSpace::myfunction()
{
std::cout << "Hello World from a function inside a namespace.";
}
int main()
{
MyNameSpace::myfunction();
}
32.4 嵌套命名空间
编写一个程序,定义一个名为 A 的命名空间和嵌套在命名空间 A 中的另一个名为 B 的命名空间,在命名空间 B 中声明一个函数,并在两个命名空间之外定义该函数。调用主程序中的函数。然后,将整个名称空间 B 引入当前范围,并调用该函数。
#include <iostream>
namespace A
{
namespace B
{
void myfunction();
}
}
void A::B::myfunction()
{
std::cout << "Hello World from a function inside a nested namespace." << '\n';
}
int main()
{
A::B::myfunction();
using namespace A::B;
myfunction();
}
三十三、转换
类型可以转换为其他类型。例如,内置类型可以转换为其他内置类型。这里我们将讨论隐式和显式转换。
33.1 隐式转换
有些值可以隐式地相互转换。所有内置类型都是如此。我们可以把char
转换成int
,int
转换成double
等等。示例:
int main()
{
char mychar = 64;
int myint = 123;
double mydouble = 456.789;
bool myboolean = true;
myint = mychar;
mydouble = myint;
mychar = myboolean;
}
我们也可以隐式地将double
转换成int
。但是,有些信息会丢失,编译器会警告我们这一点。这叫做:
int main()
{
int myint = 123;
double mydouble = 456.789;
myint = mydouble; // the decimal part is lost
}
当更小的整数类型如char
或short
用于算术运算时,它们被提升/转换为整数。这就是所谓的积分提升 。例如,如果我们在一个算术运算中使用两个字符,它们都被转换成一个整数,整个表达式的类型是 int。这种转换只发生在算术表达式中:
int main()
{
char c1 = 10;
char c2 = 20;
auto result = c1 + c2; // result is of type int
}
任何内置类型都可以转换为布尔值。对于这些类型的对象,除 0 之外的任何值都被转换为布尔值true
,等于 0 的值被隐式转换为值false
。示例:
int main()
{
char mychar = 64;
int myint = 0;
double mydouble = 3.14;
bool myboolean = true;
myboolean = mychar; // true
myboolean = myint; // false
myboolean = mydouble; // true
}
相反,布尔类型可以转换为int
。true
的值转换为整数值 1,false
的值转换为整数值 0。
任何类型的指针都可以转换成void*
类型。我们将整数指针转换为空指针的示例:
int main()
{
int x = 123;
int* pint = &x;
void* pvoid = pint;
}
虽然我们可以将任何数据指针转换为空指针,但是我们不能取消对空指针的引用。为了能够访问 void 指针所指向的对象,我们需要先将 void 指针转换为其他类型的指针。为此,我们可以使用下一章中描述的显式转换函数static_cast
:
#include <iostream>
int main()
{
int x = 123;
int* pint = &x;
void* pvoid = pint; // convert from int pointer
int* pint2 = static_cast<int*>(pvoid); // cast a void pointer to int // pointer
std::cout << *pint2; // dereference a pointer
}
数组可以隐式转换为指针。当我们给指针分配一个数组名时,指针指向数组中的第一个元素。示例:
#include <iostream>
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
int* p = arr; // pointer to the first array element
std::cout << *p;
}
在这种情况下,我们有一个从类型 int[] 到类型 int* 的隐式转换。
当用作函数参数时,数组被转换为指针。更准确地说,它被转换成指向数组中第一个元素的指针。在这种情况下,数组失去了它的维度,据说它衰减为指针。示例:
#include <iostream>
void myfunction(int arg[])
{
std::cout << arg;
}
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
myfunction(arr);
}
这里, arr 参数被转换成指向数组中第一个元素的指针。因为 arg 现在是一个指针,打印它输出一个类似于 012FF6D8 的指针值。而不是它所指向的值要输出它所指向的值,我们需要取消对指针的引用:
#include <iostream>
void myfunction(int arg[])
{
std::cout << *arg;
}
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
myfunction(arr);
}
话虽如此,重要的是采用以下:首选 std:vector 和 std::array 容器,而不是原始数组和指针。
33.2 显式转换
我们可以显式地将一种类型的值转换成另一种类型。让我们从static_cast
函数开始。该函数在隐式可转换类型之间进行转换。该函数的一个特征是:
static_cast<type_to_convert_to>(value_to_convert_from)
如果我们想从一个double
转换到int
,我们写:
int main()
{
auto myinteger = static_cast<int>(123.456);
}
比起隐式转换,更喜欢这个冗长的函数,因为static_cast
是在可转换类型之间转换的惯用方式。该函数执行编译时转换。
下面的显式转换函数应该谨慎使用很少使用。他们是dynamic_cast
和reintepret_cast
。dynamic_cast
函数将基类的指针转换成派生类的指针,反之亦然。示例:
#include <iostream>
class MyBaseClass {
public:
virtual ~MyBaseClass() {}
};
class MyDerivedClass : public MyBaseClass {};
int main()
{
MyBaseClass* base = new MyDerivedClass;
MyDerivedClass* derived = new MyDerivedClass;
// base to derived
if (dynamic_cast<MyDerivedClass*>(base))
{
std::cout << "OK.\n";
}
else
{
std::cout << "Not convertible.\n";
}
// derived to base
if (dynamic_cast<MyBaseClass*>(derived))
{
std::cout << "OK.\n";
}
else
{
std::cout << "Not convertible.\n";
}
delete base;
delete derived;
}
如果转换成功,结果是一个指向基类或派生类的指针,这取决于我们的用例。如果转换不能完成,结果是一个值为nullptr
的指针。
要使用这个函数,我们的类必须是多态的,这意味着我们的基类应该至少有一个虚函数。要尝试将一些不相关的类转换为继承链中的一个类,我们可以使用:
#include <iostream>
class MyBaseClass {
public:
virtual ~MyBaseClass() {}
};
class MyDerivedClass : public MyBaseClass {};
class MyUnrelatedClass {};
int main()
{
MyBaseClass* base = new MyDerivedClass;
MyDerivedClass* derived = new MyDerivedClass;
MyUnrelatedClass* unrelated = new MyUnrelatedClass;
// base to derived
if (dynamic_cast<MyUnrelatedClass*>(base))
{
std::cout << "OK.\n";
}
else
{
std::cout << "Not convertible.\n";
}
// derived to base
if (dynamic_cast<MyUnrelatedClass*>(derived))
{
std::cout << "OK.\n";
}
else
{
std::cout << "Not convertible.\n";
}
delete base;
delete derived;
delete unrelated;
}
这将失败,因为dynamic_cast
只能在继承链内的相关类之间转换。实际上,我们在现实世界中几乎不需要使用dynamic_cast
。
第三种也是最危险的类型是reintrepret_cast
.
这种类型最好避免,因为它不提供任何形式的保证。考虑到这一点,我们将跳过它的描述,进入下一章。
重要提示:static_cast
函数可能是我们大部分时间会用到的唯一类型。*
三十四、异常
如果我们的程序出现错误,我们希望能够以某种方式处理它。一种方法是通过异常。异常是我们试图在 try{}块中执行一些代码的机制,如果出现错误,就会抛出异常。然后,控制被转移到 catch 子句,该子句处理该异常。try/catch 块的结构应该是:
int main()
{
try
{
// your code here
// throw an exception if there is an error
}
catch (type_of_the_exception e)
{
// catch and handle the exception
}
}
一个简单的 try/catch 示例是:
#include <iostream>
int main()
{
try
{
std::cout << "Let's assume some error occurred in our program." << '\n';
std::cout << "We throw an exception of type int, for example." << '\n';
std::cout << "This signals that something went wrong." << '\n';
throw 123; // throw an exception if there is an error
}
catch (int e)
{
// catch and handle the exception
std::cout << "Exception raised!." << '\n';
std::cout << "The exception has a value of " << e << '\n';
}
}
说明:这里我们尝试执行try
块中的代码。如果出现错误,我们会抛出一个异常,表示出现了问题。我们例子中的异常是 int 类型的,但是它可以是任何类型的。当抛出异常时,控制被转移到一个catch
子句,该子句处理异常。在我们的例子中,它处理类型int
的异常。
我们可以抛出不同类型的异常,std::string
例如:
#include <iostream>
#include <string>
int main()
{
try
{
std::cout << "Let's assume some error occured in our program." << '\n';
std::cout << "We throw an exception of type string
, for example." << '\n';
std::cout << "This signals that something went wrong." << '\n';
throw std::string{ "Some string error" }; // throw an exception // if there is an error
}
catch (const std::string& e)
{
// catch and handle the exception
std::cout << "String exception raised!." << '\n';
std::cout << "The exception has a value of: " << e << '\n';
}
}
我们可以有/引发多个异常。它们可以是不同的类型。在这种情况下,我们有一个 try 和多个 catch 块。每个 catch 块处理不同的异常。
#include <iostream>
#include <string>
int main()
{
try
{
throw 123;
// the following will not execute as
// the control has been transferred to a catch clause
throw std::string{ "Some string error" };
}
catch (int e)
{
std::cout << "Integer exception raised! The value is " << e << '\n';
}
catch (const std::string& e)
{
// catch and handle the exception
std::cout << "String exception raised!." << '\n';
std::cout << "The exception has a value of: " << e << '\n';
}
}
这里我们在 try 块中抛出了多个异常。第一个是int
类型,第二个是std::string
类型。当第一个异常被抛出时,程序的控制权被转移到一个 catch 子句。这意味着 try 块中的剩余代码将不会被执行。
更现实的情况是:
#include <iostream>
#include <string>
int main()
{
try
{
bool someflag = true;
bool someotherflag = true;
std::cout << "We can have multiple throw exceptions." << '\n';
if (someflag)
{
std::cout << "Throwing an int exception." << '\n';
throw 123;
}
if(someotherflag)
{
std::cout << "Throwing a string exception." << '\n';
throw std::string{ "Some string error" };
}
}
catch (int e)
{
// catch and handle the exception
std::cout << "Integer exception raised!." << '\n';
std::cout << "The exception has a value of: " << e << '\n';
}
catch (const std::string& e)
{
// catch and handle the exception
std::cout << "String exception raised!." << '\n';
std::cout << "The exception has a value of: " << e << '\n';
}
}
这里我们在 try 块中抛出了多个异常。出于说明的目的,它们依赖于一些条件。当遇到第一个异常时,控制被转移到适当的 catch 子句。
三十五、智能指针
智能指针是拥有它们所指向的对象的指针,并且一旦指针超出范围,就自动销毁它们所指向的对象并释放内存。这样,我们不必像使用 new 和 delete 操作符那样手动删除对象。
智能指针在<memory>
头中声明。我们将讨论以下智能指针——唯一的和共享的。
35.1 唯一指针
名为std::unique_ptr
的唯一指针是一个拥有它所指向的对象的指针。指针不能被复制。一旦对象超出作用域,唯一指针就删除该对象并为其释放内存。为了声明一个简单 int 对象的唯一指针,我们写:
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> p(new int{ 123 });
std::cout << *p;
}
这个例子创建了一个指向类型为int
的对象的指针,并将值123
赋给这个对象。使用*p
符号,唯一指针可以像普通指针一样被解引用。一旦 p 超出范围,对象就会被删除,在本例中,p 位于右括号}
处。不需要显式使用删除运算符。
一个更好的初始化唯一指针的方法是通过一个std::make_unique<some_type>(some_value)
函数,其中我们在尖括号中指定对象的类型,在括号中指定对象指针指向的值:
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> p = std::make_unique<int>(123);
std::cout << *p;
}
在 C++14 标准中引入了std::make_unique
函数。确保使用 -std=c++14 标志进行编译,以便能够使用该函数。
我们可以创建一个指向一个类的对象的唯一指针,然后使用它的->操作符来访问对象成员:
#include <iostream>
#include <memory>
class MyClass
{
public:
void printmessage()
{
std::cout << "Hello from a class.";
}
};
int main()
{
std::unique_ptr<MyClass> p = std::make_unique<MyClass>();
p->printmessage();
}
一旦 p 超出范围,对象就会被销毁。所以,比起原始指针和它们的新删除机制,更喜欢一个唯一的指针。一旦 p 超出范围,类的指向对象就会被销毁。
我们可以使用一个唯一的指针来利用多态类:
#include <iostream>
#include <memory>
class MyBaseClass
{
public:
virtual void printmessage()
{
std::cout << "Hello from a base class.";
}
};
class MyderivedClass: public MyBaseClass
{
public:
void printmessage()
{
std::cout << "Hello from a derived class.";
}
};
int main()
{
std::unique_ptr<MyBaseClass> p = std::make_unique<MyderivedClass>();
p->printmessage();
}
整洁哈?不需要显式删除分配的内存,智能指针为我们做了。因此有了智能部分。
35.2 共享指针
我们可以让多个指针指向一个对象。我们可以说它们都拥有我们指向的对象,也就是我们的对象拥有共享所有权。只有当最后一个指针被销毁时,我们指向的对象才会被删除。这就是共享指针的用途。多个指针指向一个对象,当所有指针都超出作用域时,该对象就会被销毁。
共享指针定义为std::shared_ptr<some_type>
。可以使用std::make_shared<some_type>(some_value)
函数进行初始化。共享指针可以复制。要让三个共享指针指向同一个对象,我们可以写:
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> p1 = std::make_shared<int>(123);
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3 = p1;
}
当所有指针都超出范围时,所指向的对象被销毁,并且它的内存被释放。
唯一指针和共享指针之间的主要区别是:
-
对于唯一指针,我们有一个指针指向并拥有一个对象,而对于共享指针,我们有多个指针指向并拥有一个对象。
-
唯一指针不能被复制,而共享指针可以。
如果您想知道使用哪一个,假设 90%的时间,您将使用唯一指针。共享指针可以用来表示数据结构,比如图形。
智能指针本身就是类模板,这意味着它们有成员函数。我们将简单地提到它们也可以接受自定义删除器,这是一个当它们超出范围时被执行的代码。
注意,对于智能指针,我们不需要指定<some_type*>
,我们只需要指定<some_type>.
重要!
比起原始指针,更喜欢智能指针。有了智能指针,我们不必担心是否正确地匹配了对new
的调用和对delete
的调用,因为我们不需要它们。我们让智能指针做所有繁重的工作。
三十六、练习
36.1 静态转换
编写一个使用 static_cast 函数在基本类型之间转换的程序。
#include <iostream>
int main()
{
int x = 123;
double d = 456.789;
bool b = true;
double doubleresult = static_cast<double>(x);
std::cout << "Int to double: " << doubleresult << '\n';
int intresult = static_cast<int>(d); // double to int
std::cout << "Double to int: " << intresult << '\n';
bool boolresult = static_cast<bool>(x); // int to bool
std::cout << "Int to bool: " << boolresult << '\n';
}
36.2 一个简单的唯一指针:
编写一个程序,定义一个唯一的整数值指针。使用 std::make_unique 函数创建一个指针。
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> p = std::make_unique<int>(123);
std::cout << "The value of a pointed-to object is: " << *p << '\n';
}
36.3 指向类对象的唯一指针
编写一个程序,用两个数据成员、一个用户定义的构造器和一个成员函数定义一个类。创建一个指向类对象的唯一指针。使用智能指针访问成员函数。
#include <iostream>
#include <memory>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd)
: x{ xx }, d{ dd }
{}
void printdata()
{
std::cout << "Data members values are: " << x << " and: " << d;
}
};
int main()
{
std::unique_ptr<MyClass> p = std::make_unique<MyClass>(123, 456.789);
p->printdata();
}
36.4 共享指针练习
编写一个程序,定义三个共享指针,指向同一个类型为 int 的对象。通过 std::make_shared 函数创建第一个指针。通过复制第一个指针来创建其余的指针。通过所有指针访问所指向的对象。
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> p1 = std::make_shared<int>(123);
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3 = p1;
std::cout << "Value accessed through a first pointer: " << *p1 << '\n';
std::cout << "Value accessed through a second pointer: " << *p2 << '\n';
std::cout << "Value accessed through a third pointer: " << *p3 << '\n';
}
简单多态性
写一个用纯虚拟成员函数定义基类的程序。创建一个派生类,该派生类重写基类中的虚函数。通过指向基类的唯一指针创建派生类的多态对象。通过唯一指针调用重写的成员函数。
#include <iostream>
#include <memory>
class BaseClass
{
public:
virtual void dowork() = 0;
virtual ~BaseClass() {}
};
class DerivedClass : public BaseClass
{
public:
void dowork() override
{
std::cout << "Do work from a DerivedClass." << '\n';
}
};
int main()
{
std::unique_ptr<BaseClass> p = std::make_unique<DerivedClass>();
p->dowork();
} // p1 goes out of scope here
这里的覆盖描述符明确声明了派生类中的 dowork() 函数覆盖了基类中的虚函数。
这里,我们使用唯一的指针来创建并自动销毁对象,并在指针超出 main() 函数的范围时释放内存。
36.6 多态性 II
写一个用纯虚拟成员函数定义基类的程序。从基类派生两个类,并重写虚函数行为。创建两个基类类型的唯一指针,指向这些派生类的对象。使用指针来调用适当的多态行为。
#include <iostream>
#include <memory>
class BaseClass
{
public:
virtual void dowork() = 0;
virtual ~BaseClass() {}
};
class DerivedClass : public BaseClass
{
public:
void dowork() override
{
std::cout << "Do work from a DerivedClass." << '\n';
}
};
class SecondDerivedClass : public BaseClass
{
public:
void dowork() override
{
std::cout << "Do work from a SecondDerivedClass." << '\n';
}
};
int main()
{
std::unique_ptr<BaseClass> p = std::make_unique<DerivedClass>();
p->dowork();
std::unique_ptr<BaseClass> p2 = std::make_unique<SecondDerivedClass>();
p2->dowork();
} // p1 and p2 go out of scope here
36.7 异常处理
编写一个抛出并捕获整数异常的程序。处理异常并打印其值:
#include <iostream>
int main()
{
try
{
std::cout << "Throwing an integer exception with value of 123..." << '\n';
int x = 123;
throw x;
}
catch (int ex)
{
std::cout << "An integer exception of value: " << ex << " caught and handled." << '\n';
}
}
36.8 多重例外
编写一个可以在同一个 try 块中抛出 integer 和 double 异常的程序。为这两个异常实现异常处理块。
#include <iostream>
int main()
{
try
{
std::cout << "Throwing an int exception..." << '\n';
throw 123;
std::cout << "Throwing a double exception..." << '\n';
throw 456.789;
}
catch (int ex)
{
std::cout << "Integer exception: " << ex << " caught and handled." << '\n';
}
catch (double ex)
{
std::cout << "Double exception: " << ex << " caught and handled." << '\n';
}
}
三十七、输入/输出流
我们可以将对象转换成字节流。我们也可以将字节流转换回对象。I/O 流库提供了这样的功能。
流可以是输出流和输入流。
还记得性病::cout 和性病::cin 吗?那些也是溪流。例如,std::cout 是一个输出流。它接受我们提供给它的任何对象,并将它们转换成字节流,然后进入我们的监视器。相反,std::cin 是一个输入流。它从键盘获取输入,并将输入转换成我们的对象。
I/O 流有不同的种类,这里我们将解释两种:文件流和字符串流。
37.1 文件流
我们可以读取文件,也可以写入文件。标准库通过文件流提供这样的功能。这些文件流在<fstream>
头中定义,它们是:
-
std::ifstream
–从文件中读取 -
std::ofstream
–写入文件 -
std::fstream
–读取和写入文件
std:
:fstream
既可以读取文件,也可以写入文件,所以让我们使用它。为了创建一个std::fstream
对象,我们使用:
#include <fstream>
int main()
{
std::fstream fs{ "myfile.txt" };
}
这个例子创建了一个名为fs
的文件流,并将其与磁盘上的文件名myfile.txt
相关联。要逐行读取此类文件,我们使用:
#include <iostream>
#include <fstream>
#include <string>
int main()
{
std::fstream fs{ "myfile.txt" };
std::string s;
while (fs)
{
std::getline(fs, s); // read each line into a string
std::cout << s << '\n';
}
}
一旦与文件名相关联,我们就使用文件流从屏幕上读取每行文本并将其打印出来。为此,我们声明一个字符串变量 s,它将保存我们读取的文本行。在 while 循环中,我们从文件中读取一行到一个字符串。这就是为什么std::getline
函数接受文件流和字符串作为参数。一旦阅读完毕,我们就在屏幕上输出文本行。当我们到达文件末尾时,while 循环终止。
要从一个文件中一次读取一个字符,我们可以使用文件流的>>
操作符:
#include <iostream>
#include <fstream>
int main()
{
std::fstream fs{ "myfile.txt" };
char c;
while (fs >> c)
{
std::cout << c;
}
}
这个例子将文件内容一次一个字符地读入我们的 char 变量。默认情况下,这会跳过空格的读取。为了纠正这一点,我们在上面的例子中添加了std::noskipws
操纵器:
#include <iostream>
#include <fstream>
int main()
{
std::fstream fs{ "myfile.txt" };
char c;
while (fs >> std::noskipws >> c)
{
std::cout << c;
}
}
为了写入文件,我们使用文件流操作符<<
:
#include <fstream>
int main()
{
std::fstream fs{ "myoutputfile.txt", std::ios::out };
fs << "First line of text." << '\n';
fs << "Second line of text" << '\n';
fs << "Third line of text" << '\n';
}
我们将一个fs
对象与一个输出文件名相关联,并提供一个额外的std::ios::out
标志,它打开一个文件进行写入,并覆盖任何现有的myoutputfile.txt
文件。然后,我们使用<<
操作符将文本输出到文件流中。
为了向现有文件追加文本,我们在文件流构造器中包含了std::ios::app
标志:
#include <fstream>
int main()
{
std::fstream fs{ "myoutputfile.txt", std::ios::app };
fs << "This is appended text" << '\n';
fs << "This is also an appended text." << '\n';
}
我们还可以使用文件流的<<
操作符将字符串输出到我们的文件中:
#include <iostream>
#include <fstream>
#include <string>
int main()
{
std::fstream fs{ "myoutputfile.txt", std::ios::out };
std::string s1 = "The first string.\n";
std::string s2 = "The second string.\n";
fs << s1 << s2;
}
37.2 字符串流
类似地,有一个流允许我们读写一个字符串。它在<sstream>
头中定义,有三种不同的字符串流:
-
std::stringstream
-从字符串中读取的流 -
std::otringstream
-要写入字符串的流 -
std::stringstream
-读取和写入字符串的流
我们将描述std::stringstream
类模板,因为它可以读写字符串。为了创建一个简单的字符串流,我们使用:
#include <sstream>
int main()
{
std::stringstream ss;
}
此示例使用默认构造器创建一个简单的字符串流。要创建一个字符串流并用字符串文字初始化它,我们使用:
#include <iostream>
#include <sstream>
int main()
{
std::stringstream ss{ "Hello world." };
std::cout << ss.str();
}
这里我们创建了一个字符串流,并在构造器中用一个字符串初始化它。然后我们使用字符串流的.str()
成员函数来打印流的内容。成员函数获取流的字符串表示。要用一个字符串初始化一个字符串流,我们使用:
#include <iostream>
#include <sstream>
int main()
{
std::stringstream ss;
ss << "Hello World.";
std::cout << ss.str();
}
我们使用字符串流的成员函数.str()
将字符串流的内容赋给一个字符串变量:
#include <iostream>
#include <string>
#include <sstream>
int main()
{
std::stringstream ss{ "Hello World from a string stream." };
std::string s = ss.str();
std::cout << s;
}
为了将数据插入到字符串流中,我们使用了格式的输出操作符< < :
#include <iostream>
#include <string>
#include <sstream>
int main()
{
std::string s = "Hello World.";
std::stringstream ss{ s };
std::cout << ss.str();
}
我们还可以使用格式化的输出操作符<<
将基本类型的值插入到字符串流中:
#include <iostream>
#include <sstream>
int main()
{
char c = 'A';
int x = 123;
double d = 456.78;
std::stringstream ss;
ss << c << x << d;
std::cout << ss.str();
}
为了使输出更具可读性,我们可以在变量之间插入文本:
#include <iostream>
#include <sstream>
int main()
{
char c = 'A';
int x = 123;
double d = 456.78;
std::stringstream ss;
ss << "The char is: " << c << ", int is: "<< x << " and double is: " << d;
std::cout << ss.str();
}
为了将流中的数据输出到对象中,我们使用了>>
操作符:
#include <iostream>
#include <sstream>
#include <string>
int main()
{
std::string s = "A 123 456.78";
std::stringstream ss{ s };
char c;
int x;
double d;
ss >> c >> x >> d;
std::cout << c << ' ' << x << ' ' << d << ' ';
}
这个例子从一个字符串流中读取/输出数据到我们的变量中。字符串流对于格式化的输入/输出以及当我们想要从内置类型转换为字符串以及从字符串转换为内置类型时非常有用。
三十八、C++ 标准库和友元
C++ 语言伴随着一个叫做的库 C++ 标准库。它是容器和有用函数的集合,我们通过包含适当的头文件来访问它们。C++ 标准库中的容器和函数是在 std 命名空间中定义的。还记得前面提到的 std::string 类型吗?它也是标准库的一部分。标准库是通过类模板实现的。长话短说:对于日常任务,更喜欢使用标准库而不是用户提供的库。
本章解释的一些功能,比如基于范围的 for 循环和 lambda 表达式是语言本身的一部分,而不是标准库。我们把它们放在这里的原因是它们通常与标准库设施一起使用。
38.1 集装箱
容器是我们存放物品的地方。容器有不同的类别,这里我们提到两种:
-
序列容器
-
关联容器
顺序容器按顺序存储对象,在内存中一个挨着一个。
标准::矢量
Vector 是在<vector>
头中定义的容器。向量是连续元素的序列。你可能会问,是什么类型的?任何类型的。vector 和所有其他容器被实现为类模板,允许存储(几乎)任何类型。为了定义一个向量,我们使用如下:std::vector<some_type>.
一个初始化 5 个整数的向量的简单例子:
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
}
这里,我们定义了一个名为v,
的 5 个整数元素的向量,并使用括号初始化来初始化向量。当我们在 Vector 中插入和删除元素时,vector 可以自己增长和收缩。为了在向量的末尾插入元素,我们使用向量的。push_back()成员函数。示例:
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
v.push_back(10);
}
这个例子在向量的末尾插入一个值 10。现在我们有一个包含 6 个元素的容器: 1 2 3 4 5 10 。
向量元素被索引,第一个元素的索引为 0。单个元素可以通过下标操作符[element_index]
或成员函数at(element_index
来访问:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
std::cout << "The third element is:" << v[2] << '\n';
std::cout << "The fourth element is:" << v.at(3) << '\n';
}
向量的大小为若干个元素,可以通过一个.size()
成员函数获得:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
std::cout << "The vector's size is: " << v.size();
}
向量是一个连续的容器。它按顺序存储元素。其他顺序容器有:
-
std::list
–双向链表 -
std::forward_list
–单链表 -
std::deque
–双头队列
那么,用哪个呢?如有疑问,请使用 std::vector。每个容器都有不同的插入和查找时间,每个容器都有不同的用途。然而,就序列容器而言,std::vector
是我们大部分时间想要使用的容器。
标准::数组
数组是 C 风格数组的一个薄薄的包装。当用作函数参数时,数组被转换成指针,我们应该更喜欢 std::array 包装器,而不是老式的 C 风格数组。std::array 的签名如下: std::array < type_name,array _ size>;一个简单的例子:
#include <iostream>
#include <array>
int main()
{
std::array<int, 5> arr = { 1, 2, 3, 4, 5 };
for (auto el : arr)
{
std::cout << el << '\n';
}
}
这个例子使用 std::array 容器创建了一个包含 5 个元素的数组,并将它们打印出来。让我们再次强调这一点:更喜欢 std::array 或 std::vector,而不是旧的/原始的 C 风格数组。
标准::设置
集合是一个保存唯一的、已排序的对象的容器。这是一个排序对象的二叉树。要使用集合,我们必须包含<set>
标题。为了定义一个集合,我们使用了std::set<type> set_name
语法。要初始化一组 5 个整数,我们可以写:
#include <iostream>
#include <set>
int main()
{
std::set<int> myset = { 1, 2, 3, 4, 5 };
for (auto el : myset)
{
std::cout << el << '\n';
}
}
为了将一个元素插入到集合中,我们使用集合的.insert(value)
成员函数。为了插入两个新元素,我们使用:
#include <iostream>
#include <set>
int main()
{
std::set<int> myset = { 1, 2, 3, 4, 5 };
myset.insert(10);
myset.insert(42);
for (auto el : myset)
{
std::cout << el << '\n';
}
}
由于该集合包含唯一值,插入重复值的尝试将不会成功。
标准::地图
map 是一个保存键值对的关联容器。关键字已排序并且是唯一的。一个映射也被实现为一个平衡的二叉树/图。所以现在,不是每个元素一个值,而是两个。要使用地图,我们需要包含标题。为了定义一个映射,我们使用了std::map<type1, type2> map_name
语法。这里的type1
代表键的类型,type2
代表值的类型。例如,为了初始化一个int char
对的映射,我们可以写:
#include <map>
int main()
{
std::map<int, char> mymap = { {1, 'a'}, {2, 'b'}, {3,'z'} };
}
在这个例子中,整数是键,字符是值。每个地图元素都是一对。该对的第一个元素(键)通过第一个()成员变量访问,第二个元素(值)通过第二个成员函数变量访问。要打印出我们的地图,我们可以使用:
#include <iostream>
#include <map>
int main()
{
std::map<int, char> mymap = { {1, 'a'}, {2, 'b'}, {3,'z'} };
for (auto el : mymap)
{
std::cout << el.first << ' ' << el.second << '\n';
}
}
我们也可以通过它的默认构造器和它的关键下标操作符[]来构造一个映射。如果通过下标操作符访问的键不存在,则整个键-值对被插入到一个映射中。示例:
#include <iostream>
#include <map>
int main()
{
std::map<int, char> mymap;
mymap[1] = 'a';
mymap[2] = 'b';
mymap[3] = 'z';
for (auto el : mymap)
{
std::cout << el.first << ' ' << el.second << '\n';
}
}
要插入到地图中,我们可以使用.insert()
成员函数:
#include <iostream>
#include <map>
int main()
{
std::map<int, char> mymap = { {1, 'a'}, {2, 'b'}, {3,'z'} };
mymap.insert({ 20, 'c' });
for (auto el : mymap)
{
std::cout << el.first << ' ' << el.second << '\n';
}
}
为了在 map 中搜索特定的键,我们可以使用 map 的.find(key_value)
成员函数,它返回一个迭代器。如果没有找到键,这个函数返回一个值为.end()
的迭代器。如果找到了关键字,函数将返回指向包含所搜索关键字对的迭代器:
#include <iostream>
#include <map>
int main()
{
std::map<int, char> mymap = { {1, 'a'}, {2, 'b'}, {3,'z'} };
auto it = mymap.find(2);
if (it != mymap.end())
{
std::cout << "Found: " << it->first << " " << it->second << '\n';
}
else
{
std::cout << "Not found.";
}
}
迭代器现在指向地图元素。映射元素是由第一个元素(键)和第二个元素(值)组成的对。要使用迭代器访问这些,首先我们必须使用箭头操作符->
取消对迭代器的引用。然后我们调用对的成员变量first
作为键,调用second
作为值。
标准::对
std::pair 类模板是一个可以表示一对值的包装器。要使用 std::pair,我们需要包含 <实用程序> 头。为了访问一对值中的第一个值,我们使用了。第一个成员变量。为了访问一对值中的第二个值,我们使用了。第二个成员变量。示例:
#include <iostream>
#include <utility>
int main()
{
std::pair<int, double> mypair = { 123, 3.14 };
std::cout << "The first element is: " << mypair.first << '\n';
std::cout << "The second element is: " << mypair.second << '\n';
}
创建配对的另一种方法是通过 std::make_pair 函数:
#include <iostream>
#include <utility>
int main()
{
int x = 123;
double d = 3.14;
std::pair<int, double> mypair = std::make_pair(x, d);
std::cout << "The first element is: " << mypair.first << '\n';
std::cout << "The second element is: " << mypair.second << '\n';
}
其他容器
在标准库中还有其他较少使用的容器。我们将提到其中的几个:
-
STD::forward _ list–单向链表
-
STD::list–一个双向链表
-
STD::deque–一个双头容器,允许在两端插入和删除
38.2 基于范围的 for 循环
现在是引入基于范围的for
循环的绝佳时机,它允许我们迭代容器/范围内容。基于范围的for
循环的语法如下:
for (some_type element_name : container_name)
{
}
我们解读为:对于container_name
内some_type
的每一个element_name
(在代码块{}内做一些事情)。为了迭代向量的元素,我们可以使用:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
v.push_back(10);
for (int el : v)
{
std::cout << el << '\n';
}
}
el
名称代表了 vector 的每个元素的副本。如果我们想对实际的向量元素进行操作,我们使用一个引用类型:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
v.push_back(10);
for (int& el : v)
{
std::cout << el << '\n';
}
}
现在,el
是实际的向量元素,所以我们在 el 上做的任何更改都将是对实际向量元素的更改。
我们也可以使用auto
描述符,让编译器推断出容器中元素的类型:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
v.push_back(10);
for (auto el : v)
{
std::cout << el << '\n';
}
}
为了迭代字符串向量,我们将使用const auto&
描述符,因为出于性能原因,我们应该通过 const 引用传递字符串:
#include <iostream>
#include <vector>
#include <string>
int main()
{
std::vector<std::string> v = { "Hello", "World,", "C++"};
v.push_back("Is great!");
for (const auto& el : v)
{
std::cout << el << '\n';
}
}
38.3 迭代器
容器有迭代器。迭代器就像指向容器元素的指针。指向向量第一个元素的迭代器通过一个.begin()
成员函数来表示。指向最后一个元素之后的迭代器(不是最后一个,而是)通过一个.end()
成员函数来表达。迭代器可以递增或递减。让我们使用迭代器打印一个向量内容:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
for (auto it = v.begin(); it!=v.end(); it++)
{
std::cout << *it << '\n';
}
}
只要我们向量的迭代器it
不等于v.end(),
,我们就继续迭代向量.
,当当前迭代器it
等于 v.end()时,for
循环终止。v.end()表示已经到达容器的末尾(不是最后一个元素,而是倒数第二个元素)。人们开始欣赏基于范围的 for 循环的易用性,而不是在 for 循环中使用这种老式的迭代器。
现在我们知道了迭代器,我们可以用它们来删除向量中的元素。假设我们想删除第三个元素。我们将迭代器定位到第三个元素,并使用.erase(iterator_name)
成员函数:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
auto it = v.begin() + 3;
v.erase(it);
for (auto el : v)
{
std::cout << el << '\n';
}
}
我们还提到了另一组容器,叫做关联容器。这些容器被实现为二叉树。它们允许快速搜索,并且这些容器中的数据是经过排序的。这些关联容器是 std::set 和 std::map 。Set 保存唯一的值。Map 保存成对的键值元素。地图拥有唯一的键。请注意,还有另一组允许重复值的关联容器。分别是 std::multi_set 和 std::multi_map 。
38.4 算法和实用程序
C++ 标准库提供了一组位于<algorithm
>
头文件中的有用函数。这些函数允许我们在容器上执行各种操作。
标准分类
例如,如果我们想对我们的容器进行排序,我们可以使用std::sort
函数。为了按升序排列我们的向量,我们使用:
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 1, 5, 2, 15, 3, 10 };
std::sort(v.begin(), v.end());
for (auto el : v)
{
std::cout << el << '\n';
}
}
函数对一系列元素进行排序。它接受表示范围开始和结束的参数(确切地说,是范围结束后的一个参数)。这里我们传入了整个向量的范围,其中v.begin()
代表范围的开始,v.end()
代表范围的结束。
为了对容器进行降序排序,我们传递了一个名为比较器的额外参数。有一个名为std::greater,
的内置比较器,它使用运算符>进行比较,并允许 std::sort 函数按升序对数据进行排序。示例:
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
int main()
{
std::vector<int> v = { 1, 5, 2, 15, 3, 10 };
std::sort(v.begin(), v.end(), std::greater<int>());
for (auto el : v)
{
std::cout << el << '\n';
}
}
比较器或比较函数是在<functional>
头内定义的所谓的函数对象。我们可以通过所谓的未命名函数定义我们的自定义函数对象,这些函数称为λ函数或λ函数。本书后面会有更多的介绍。
std::sort
函数的第三个参数通常被称为谓词。谓词是返回true
或false.
标准库函数的函数或函数对象,比如 std::sort 接受谓词作为它们的参数之一。是的,有很多文本和理论,但是暂时不要担心。只要记住标准库中有内置函数,学习如何使用它们才是关键。通过例子,一切都会变得清楚。
标准::查找
为了通过值找到某个元素并返回指向该元素的迭代器,我们使用了 std::find 函数。为了在我们的向量中搜索值 5,我们使用:
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 1, 5, 2, 15, 3, 10 };
auto result = std::find(v.begin(), v.end(), 5);
if (result!=v.end())
{
std::cout << "Element found: " << *result;
}
else
{
std::cout << "Element not found.";
}
}
如果找到了元素,函数返回一个迭代器,指向容器中第一个找到的元素。如果没有找到值,函数返回一个.end()
迭代器。
而不是使用容器的。begin()
和。end()
成员函数,我们也可以使用独立的std::begin(container_name)
和std::end(container_name)
函数:
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
int main()
{
std::vector<int> v = { 1, 5, 2, 15, 3, 10 };
auto result = std::find(std::begin(v), std::end(v), 5);
if (result!=std::end(v))
{
std::cout << "Element found: " << *result;
}
else
{
std::cout << "Element not found.";
}
}
还有一个接受谓词的条件函数std::find_if
。根据谓词值,该函数对谓词返回true
的元素执行搜索。当我们在后面的章节中讨论λ表达式时,会有更多的相关内容。
标准::副本
函数将元素从一个容器复制到另一个容器。它可以将起始容器中标有[starting _ poisition _ iterator、ending _ position _ iterator]的一系列元素复制到目标容器中标有(destination _ position _ iterator)的特定位置。该函数在 <算法> 头内声明。在复制元素之前,我们需要通过向 vector 的构造器提供大小来在目标 vector 中保留足够的空间。示例:
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> copy_from_v = { 1, 2, 3, 4, 5 };
std::vector<int> copy_to_v(5); // reserve the space for 5 elements
std::copy(copy_from_v.begin(), copy_from_v.end(), copy_to_v.begin());
for (auto el : copy_to_v)
{
std::cout << el << '\n';
}
}
说明:我们定义了一个名为 copy_from_v 的源向量,并用一些值初始化它。然后我们定义一个copy _ to _ vdestination vector,并通过向它的构造器提供数字 5 来为它保留足够的空间来保存 5 个元素。然后,我们将从源向量的开头到结尾的所有元素复制到目的向量的开头。
为了只复制前 3 个元素,我们将使用标有 copy_from_v.begin() 和 copy_from_v.begin() + 3 的适当范围。我们只需要为目的向量中的 3 个元素保留空间:
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> copy_from_v = { 1, 2, 3, 4, 5 };
std::vector<int> copy_to_v(3);
std::copy(copy_from_v.begin(), copy_from_v.begin() + 3, copy_to_v.begin());
for (auto el : copy_to_v)
{
std::cout << el << '\n';
}
}
最小和最大元素
为了找到容器中最大的元素,我们使用了在<algorithm>
头中声明的 std::max::element 函数。该函数返回容器中 max 元素的迭代器:
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
auto it = std::max_element(std::begin(v), std::end(v));
std::cout << "The max element in the vector is: " << *it;
}
类似地,为了找到容器中的最小元素,我们使用 std::min_element 函数,该函数返回容器或范围中最小元素的迭代器:
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
auto it = std::min_element(std::begin(v), std::end(v));
std::cout << "The min element in the vector is: " << *it;
}
38.5λ表达式
Lambda 表达式,简称 lambdas,就是所谓的:匿名函数对象。一个函数对象,或者一个仿函数,是一个可以作为函数调用的类的对象。为了能够像调用函数一样调用对象,我们必须为我们的类重载函数调用操作符():
#include <iostream>
class MyClass
{
public:
void operator()()
{
std::cout << "Function object called." << '\n';
}
};
int main()
{
MyClass myobject;
myobject(); // invoke the function object
}
函数对象可以有一个或多个参数;在这种情况下,有一个名为 x 的参数:
#include <iostream>
class MyClass
{
public:
void operator()(int x)
{
std::cout << "Function object with a parameter " << x << " called.";
}
};
int main()
{
MyClass myobject;
myobject(123); // invoke the function object
}
function 对象也可以返回值。例如,下面的函数对象检查参数是否为偶数:
#include <iostream>
class MyClass
{
public:
bool operator()(int x)
{
if (x % 2 == 0)
{
return true;
}
else
{
return false;
}
}
};
int main()
{
MyClass myobject;
bool isEven = myobject(123);
if (isEven)
{
std::cout << "The number is even." << '\n';
}
else
{
std::cout << "The number is odd." << '\n';
}
}
据说函数对象携带它们的值。因为它们是一个类的对象,所以它们可以携带数据成员。这将它们与常规函数区分开来。
正如我们所看到的,如果我们想要的只是一个简单的函数对象,那么重载 operator()并编写整个类会有些麻烦。这就是 lambda 表达式发挥作用的地方。Lambda 表达式是匿名/未命名的函数对象。λ表达式签名是:
captures{lambda_body};
为了定义和调用一个简单的 lambda,我们使用:
#include <iostream>
int main()
{
auto mylambda = []() {std::cout << "Hello from a lambda"; };
mylambda();
}
这里,我们将一个 lambda 表达式的结果:[]() {std::cout << "Hello from a lambda";
}赋给一个变量mylambda
。然后我们通过使用函数调用操作符()
来调用这个 lambda。因为 lambda 是未命名的函数,这里我们给它命名为mylambda,
,以便能够从 lambda 表达式本身调用代码。
为了能够在定义 lambda 的范围内使用变量,我们需要先用捕获。标有[]
的捕获段可以通过复制的方式捕获局部变量:
#include <iostream>
int main()
{
int x = 123;
auto mylambda = [x]() { std::cout << "The value of x is: " << x; };
mylambda();
}
这里,我们通过值捕获了局部变量 x,并在我们的 lambda 主体中使用它。另一种捕获变量的方法是通过引用,这里我们使用[&name]
符号。示例:
#include <iostream>
int main()
{
int x = 123;
auto mylambda = [&x]() {std::cout << "The value of x is: " << ++x; };
mylambda();
}
为了捕获多个变量,我们在捕获列表中使用逗号操作符:[var1, var2]
。例如,为了通过值捕获两个局部变量,我们使用:
#include <iostream>
int main()
{
int x = 123;
int y = 456;
auto mylambda = [x, y]() {std::cout << "X is: " << x << ", y is: " << y; };
mylambda();
}
为了通过引用捕获这两个局部变量,我们使用:
#include <iostream>
int main()
{
int x = 123;
int y = 456;
auto mylambda = [&x, &y]() {std::cout << "X is: " << ++x << ", y is: " << ++y; };
mylambda();
}
Lambdas 可以在括号内有可选参数:[](param1, param2){}
。示例:
#include <iostream>
int main()
{
auto mylambda = [](int x, int y)
{
std::cout << "The value of x is: " << x << ", y is: " << y;
};
mylambda(123, 456);
}
Lambdas 最常用作标准库算法函数中的谓词。例如,如果我们想计算容器中偶数元素的数量,我们可以向一个std::count_if
函数提供一个 lambda。示例:
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30 };
auto counteven = std::count_if(std::begin(v), std::end(v),
[](int x) {return x % 2 == 0; });
std::cout << "The number of even vector elements is: " << counteven;
}
这里我们有一个 lambda 函数,它检查一个参数是否是偶数,如果是,就返回true
。这个 lambda 随后被用作std::count_if
函数中的谓词。这个函数只计算谓词(我们的 lambda 表达式)返回true
的数字。std::count_if
函数遍历所有 vector 元素,每个元素都成为一个 lambda 参数。
我们可以在其他标准库算法函数中使用 lambdas,接受名为 callables 的表达式。可调用的例子有 lambdas 和 function 对象。
通过使用 lambdas,我们可以更清楚地表达自己,而不必编写冗长的类函数对象。Lambdas 是在 C++11 标准中引入的。
三十九、练习
39.1 基本矢量
写一个定义整数向量的程序。将两个元素插入向量。使用基于范围的循环打印出矢量内容。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = { 10, 5, 8, 4, 1, 2 };
v.push_back(15); // insert the value 15
v.push_back(30); // insert the value of 30
for (auto el : v)
{
std::cout << el << '\n';
}
}
39.2 删除单个值
写一个定义整数向量的程序。从向量中删除第二个元素。使用基于范围的循环打印出矢量内容。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = { 10, 5, 8, 4, 1, 2 };
v.erase(v.begin() + 1); // erase the second element which is 5
for (auto el : v)
{
std::cout << el << '\n';
}
}
39.3 删除一系列元素
写一个定义整数向量的程序。从矢量的开头开始擦除 3 个元素的范围。使用基于范围的循环打印出矢量内容。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = { 10, 5, 8, 4, 1, 2 };
v.erase(v.begin(), v.begin() + 3); // erase the first 3 elements
for (auto el : v)
{
std::cout << el << '\n';
}
}
在这种情况下。erase()函数重载接受两个参数。一个是要删除的范围的开始。在我们的例子中,它用 v.begin()标记。第二个参数是要删除的范围的结尾。在我们的例子中,它是 v.begin() + 3 迭代器。请注意不是。begin()成员函数我们可以使用一个独立的 std::begin(v)函数。
39.4 寻找向量中的元素
编写一个程序,使用 std::find()算法函数搜索矢量元素。如果已经找到该元素,则将其删除。打印出矢量内容。
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 10, 5, 8, 4, 1, 2 };
int findnumber = 4;
auto foundit = std::find(std::begin(v), std::end(v), findnumber);
if (foundit != std::end(v))
{
std::cout << "Element found. Deleting the element." << '\n';
v.erase(foundit);
std::cout << "Element deleted." << '\n';
}
else
{
std::cout << "Element not found." << '\n';
}
for (auto el : v)
{
std::cout << el << '\n';
}
}
39.5 基本设置
写一个定义一组整数的程序。打印出集合内容并观察以下内容:不管我们如何定义集合,数据都被排序。这是因为在内部,std::set 是一个保存唯一值的排序容器。
#include <iostream>
#include <set>
int main()
{
std::set<int> myset = { -10, 1, 3, 5, -20, 6, 9, 15 };
for (auto el : myset)
{
std::cout << el << '\n';
}
}
39.6 设置数据操作
编写一个程序,定义一个集合并使用该集合的插入两个新值。insert()成员函数。然后,使用集合的从集合中删除一个任意值。erase()成员函数。之后打印出设定的内容。
#include <iostream>
#include <set>
int main()
{
std::set<int> myset = { -10, 1, 3, 5, 6, 9, 15 };
myset.insert(-5); // inserts a value of -5
myset.insert(30); // inserts a value of 30
myset.erase(6); // deletes a value of 6
for (auto el : myset)
{
std::cout << el << '\n';
}
}
39.7 设置成员函数
编写一个程序,定义一组整数,并利用集合的成员函数来检查集合的大小,检查它是否为空,并清除集合的内容。
#include <iostream>
#include <set>
int main()
{
std::set<int> myset = { -10, 1, 3, 5, 6, 9, 15 };
std::cout << "The set's size is: " << myset.size() << '\n';
std::cout << "Clearing the set..." << '\n';
myset.clear(); // clear the set's content
if (myset.empty())
{
std::cout << "The set is empty." << '\n';
}
else
{
std::cout << "The set is not empty." << '\n';
}
}
39.8 在集合中搜索数据
编写一个程序,使用集合中的。find()成员函数。如果找到该值,则将其删除。打印出设定的内容。
#include <iostream>
#include <set>
int main()
{
std::set<int> myset = { -10, 1, 3, 5, 6, 9, 15 };
int findvalue = 5;
auto foundit = myset.find(findvalue);
if (foundit != myset.end())
{
std::cout << "Value found. Deleting the value..." << '\n';
myset.erase(foundit);
std::cout << "Element deleted." << '\n';
}
else
{
std::cout << "Value not found." << '\n';
}
for (auto el : myset)
{
std::cout << el << '\n';
}
}
39.9 基本地图
编写一个程序,定义一个映射,其中键的类型为 char,值的类型为 int。打印出地图内容。
#include <iostream>
#include <map>
int main()
{
std::map<char, int> mymap = { {'a', 1}, {'b', 5}, {'e', 10}, {'f', 10} };
for (auto el : mymap)
{
std::cout << el.first << ' ' << el.second << '\n';
}
}
解释。地图元素是键值对。这些对由一个可以存储对的 std::pair 类模板表示。所以地图元素的类型是 std::pair < char,int >。在地图容器中,键是唯一的,值不必是唯一的。我们用初始化列表{}中的键值对初始化映射。使用基于范围的 for 循环,我们遍历地图元素。为了访问密钥对中的密钥,我们使用密钥对的。第一个成员函数,表示一对元素中的第一个元素,在我们的例子中是键。类似地,我们使用对的访问第二个元素。第二个成员函数,代表地图元素值。
39.10 插入地图
写一个定义字符串和整数映射的程序。使用地图的将元素插入到地图中。 insert() 成员函数。然后使用映射的操作符[]将另一个键值元素插入到映射中。之后打印地图内容。
#include <iostream>
#include <map>
#include <string>
int main()
{
std::map<std::string, int> mymap = { {"red", 1}, {"green", 20}, {"blue", 15} };
mymap.insert({ "magenta", 4 });
mymap["yellow"] = 5;
for (const auto& el : mymap)
{
std::cout << el.first << ' ' << el.second << '\n';
}
}
使用映射的[]运算符时,有两种情况。[]运算符中的键存在于映射中。这意味着我们可以用它来改变一个元素的值。该密钥不存在。在这种情况下,当使用映射的操作符[]时,键值被插入到映射中。我们的:mymap["yellow"] = 5;
语句就是这种情况。请记住,地图是图表,地图的元素是根据关键字排序的。因为我们的键是字符串,所以顺序不一定是我们在初始化列表中提供的顺序。
例如,如果我们有一个 int 和 strings 的映射,并且我们在 initializers 列表中提供了排序的 int 键,那么在打印出元素时,顺序将是相同的:
#include <iostream>
#include <map>
#include <string>
int main()
{
std::map<int, std::string> mymap = { {1, "First"}, {2, "Second"}, {3, "Third"}, {4, "Fourth"} };
for (const auto& el : mymap)
{
std::cout << el.first << ' ' << el.second << '\n';
}
}
39.11 从地图中搜索和删除
写一个定义整数和字符串映射的程序。使用地图的键搜索元素。find() 成员函数。如果找到该元素,则将其删除。打印出地图内容。
#include <iostream>
#include <map>
#include <string>
int main()
{
std::map<int, std::string> mymap = { {1, "First"}, {2, "Second"}, {3, "Third"}, {4, "Fourth"} };
int findbykey = 2;
auto foundit = mymap.find(findbykey);
if (foundit != mymap.end())
{
std::cout << "Key found." << '\n';
std::cout << "Deleting the element..." << '\n';
mymap.erase(foundit);
}
else
{
std::cout << "Key not found." << '\n';
}
for (const auto& el : mymap)
{
std::cout << el.first << ' ' << el.second << '\n';
}
}
39.12λ表达式
写一个定义整数向量的程序。使用 std::sort 函数和用户提供的 lambda 函数作为谓词,对向量进行降序排序。
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 5, 10, 4, 1, 3, 15 };
std::sort(std::begin(v), std::end(v), [](int x, int y) {return x > y; });
for (const auto& el : v)
{
std::cout << el << '\n';
}
}
写一个定义整数向量的程序。使用 std::count_if 函数和用户提供的 lambda 函数只计算偶数。
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 5, 10, 4, 1, 3, 8 };
int mycount = std::count_if(std::begin(v), std::end(v), [](int x) {return x % 2 == 0; });
std::cout << "The number of even numbers is: " << mycount;
}
编写一个定义局部 lambda 表达式的程序,该表达式可以捕获和修改 main()函数中定义的变量:
#include <iostream>
int main()
{
int x = 123;
std::cout << "The value of a local variable is: " << x << '\n';
auto mylambda = [&x](){ x++; };
mylambda();
std::cout << "Lambda captured and changed the local variable to: " << x << '\n';
}
四十、C++ 标准
C++ 是一种 ISO 标准化编程语言。有不同的 C++ 标准:
一切以 C++11 开始的都被称为“现代 C++”。这些标准在技术细节上定义了这种语言。它们也可以作为 C++ 编译器作者的手册。这是一套令人难以置信的规则和规范。可以购买 C++ 标准,或者免费下载草稿版本。这些草案非常类似于最终的 C++ 标准。当 C++ 代码可以在不同平台(机器或编译器)上成功迁移和编译,并且 C++ 实现紧密遵循标准时,我们说代码是可移植的。这就是通常所说的可移植 C++ 。
用大括号括起来的标准代表所谓的“现代 C++”每个标准都描述了这种语言,并引入了新的语言和库特性。它还可能对现有规则进行修改。我们将描述每个标准的显著特征。
40.1 C++11
C++11 是 ISO C++ 标准,发布于 2011 年。为了按照这个标准进行编译,如果使用 g++ 或 clang 进行编译,那么在命令行编译字符串中添加 -std=c++11 标志。如果使用 Visual Studio,选择项目/选项/配置属性/ C/C++ /语言/C++ 语言标准,选择 C++11 。新的 Visual Studio 版本已经支持这个现成的标准。我们已经在前面的章节中描述了 C++11 的显著特性,在这里我们将再次简要回顾一下,并介绍几个新特性:
40.1.1 自动类型推断
该标准引入了auto
关键字,它根据变量的初始化式来推断变量的类型:
int main()
{
auto mychar = 'A';
auto myint = 123 + 456;
auto mydouble = 456.789;
}
40.1.2 基于范围的循环
基于范围的循环允许我们在范围内迭代,例如 C++ 标准库容器:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = { 10, 20, 40, 5, -20, 75 };
for (auto el : v)
{
std::cout << el << '\n';
}
}
基于范围的 for 循环如下:for (type element : container)
。对于容器中的每个元素来说,这读作(做点什么)。
初始化列表
用大括号{ }表示的初始化列表允许我们以统一的方式初始化对象。我们可以初始化单个对象:
int main()
{
int x{ 123 };
int y = { 456 };
double d{ 3.14 };
}
和容器:
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
}
列表初始化还可以防止收缩转换。如果我们试图在初始化列表中用 double 值初始化 integer 对象,编译将会失败:
int main()
{
int x = { 123.45 }; // Error, does not allowing narrowing
}
在初始化我们的对象时,我们应该更喜欢初始化列表{},而不是旧式的括号()。
移动语义
C++ 11 标准引入了类的移动语义。我们可以通过移动其他对象的数据来初始化我们的对象。这是通过移动构造器和移动赋值操作符实现的。两者都接受所谓的右值引用作为参数。 Lvalue 是一个可以用在赋值操作左侧的表达式。右值是可以在赋值的右边使用的表达式。右值引用的签名为 some_type & & 。为了将表达式转换为右值引用,我们使用了 std::move 函数。简单的移动构造器和移动赋值签名是:
class MyClass
{
public:
MyClass(MyClass&& otherobject) // move constructor
{
//implement the move logic here
}
MyClass& operator=(MyClass&& otherobject) // move assignment operator
{
// implement the copy logic here
return *this;
}
};
40 . 1 . 5λ表达式
Lambda 表达式是匿名函数对象。它们允许我们编写一小段代码,用作标准库函数谓词。lambda 有一个捕获列表,标记为[ ],在这里我们可以通过引用或复制来捕获局部变量,有可选参数的参数列表标记为( ),还有一个 lambda 主体,标记为{ }。空的 lambda 看起来像;。使用 lambda 作为谓词只计算集合中偶数的简单示例:
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
auto counteven = std::count_if(std::begin(v), std::end(v),
[](int x) {return x % 2 == 0; }); // lambda expression
std::cout << "The number of even vector elements is: " << counteven;
}
constexpr 描述符
constexpr 描述符承诺变量或函数可以在编译时被求值。如果在编译时不能对表达式求值,编译器会发出一个错误:
int main()
{
constexpr int n = 123; //OK, 123 is a compile-time constant // expression
constexpr double d = 456.78; //OK, 456.78 is a compile-time constant // expression
constexpr double d2 = d; //OK, d is a constant expression
int x = 123;
constexpr int n2 = x; //compile-time error
// the value of x is not known during // compile-time
}
范围枚举器
C++11 标准引入了作用域枚举器。与旧的枚举器不同,作用域枚举器不会将它们的名字泄漏到周围的作用域中。作用域枚举具有以下签名:枚举类 Enumerator_Name {value1,value2 等} 签名。作用域枚举的一个简单示例是:
enum class MyEnum
{
myfirstvalue,
mysecondvalue,
mythirdvalue
};
int main()
{
MyEnum myenum = MyEnum::myfirstvalue;
}
40.1.8 智能指针
智能指针指向对象,当指针超出范围时,对象被销毁。这使得它们变得智能,因为我们不必担心手动释放已分配的内存。智能指针为我们做了所有的重活。
有两种智能指针,带有 std::unique_ptr <类型> 签名的唯一指针和带有 std::shared_ptr <类型> 签名的共享指针。两者的区别在于我们只能有一个唯一的指针指向对象。相反,我们可以有多个共享指针指向一个对象。当唯一指针超出范围时,对象被销毁,内存被释放。当指向我们的对象的最后一个共享指针超出范围时,对象就会被销毁。内存被释放。
一个独特的指针示例:
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> p(new int{ 123 });
std::cout << *p;
} // p goes out of scope here, the memory gets deallocated, the object gets // destroyed
唯一指针不能被复制,只能被移动。要让多个共享指针指向同一个对象,我们应该写:
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> p1(new int{ 123 });
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3 = p1;
} // when the last shared pointer goes out of scope, the memory gets // deallocated
共享指针可以复制。据说他们共享该物体的所有权。当最后一个共享指针超出范围时,指向的对象被销毁,内存被释放。
std::unordered_set
std::unordered_set 是一个允许常量时间插入、搜索和删除元素的容器。这个容器被实现为一个链表存储桶的数组。计算每个元素的哈希值,并根据哈希值将对象放入适当的桶中。对象本身不按任何特定顺序排序。为了定义一个无序集合,我们需要包含<unordered_set>
头。示例:
#include <iostream>
#include <unordered_set>
int main()
{
std::unordered_set<int> myunorderedset = { 1, 2, 5, -4, 7, 10 };
for (auto el : myunorderedset)
{
std::cout << el << '\n';
}
}
这些值没有排序,但却是唯一的。为了将单个或多个值插入到一个 unordered_set 中,我们使用。insert()成员函数:
#include <iostream>
#include <unordered_set>
int main()
{
std::unordered_set<int> myunorderedset = { 1, 2, 5, -4, 7, 10 };
myunorderedset.insert(6); // insert a single value
myunorderedset.insert({ 8, 15, 20 }); // insert multiple values
for (auto el : myunorderedset)
{
std::cout << el << '\n';
}
}
要从无序集中删除一个值,我们使用。erase()成员函数:
#include <iostream>
#include <unordered_set>
int main()
{
std::unordered_set<int> myunorderedset = { 1, 2, 5, -4, 7, 10 };
myunorderedset.erase(-4); // erase a single value
for (auto el : myunorderedset)
{
std::cout << el << '\n';
}
}
标准::无序 _ 映射
与 std::unordered_set 类似,还有一个 std::unordered_map,这是一个由具有唯一键的键值对组成的无序容器。该容器还允许快速插入、搜索和移除元素。容器和数据也以桶的形式实现。什么元素进入什么桶取决于元素的键哈希值。为了定义一个无序映射,我们包含了<unordered_map>
头。示例:
#include <iostream>
#include <unordered_map>
int main()
{
std::unordered_map<char, int> myunorderedmap = { {'a', 1}, {'b', 2}, {'c', 5} };
for (auto el : myunorderedmap)
{
std::cout << el.first << ' '<< el.second << '\n';
}
}
这里我们用键值对初始化一个无序映射。在基于范围的 for 循环中,我们打印键和值。地图元素是成对的。Pairs 有成员函数。第一个用于访问密钥和。第二个用于访问一个值。要在地图中插入一个元素,我们可以使用 member 函数。insert()成员函数:
#include <iostream>
#include <unordered_map>
int main()
{
std::unordered_map<char, int> myunorderedmap = { {'a', 1}, {'b', 2}, {'c', 5} };
myunorderedmap.insert({ 'd', 10 });
for (auto el : myunorderedmap)
{
std::cout << el.first << ' '<< el.second << '\n';
}
}
我们还可以使用映射的操作符[]来插入一个元素。通常,该运算符用于通过键访问元素值。但是,如果键不存在,操作者将在映射中插入一个新元素:
#include <iostream>
#include <unordered_map>
int main()
{
std::unordered_map<char, int> myunorderedmap = { {'a', 1}, {'b', 2}, {'c', 5} };
myunorderedmap['b'] = 4; // key exists, change the value
myunorderedmap['d'] = 10; // key does not exist, insert the new element
for (auto el : myunorderedmap)
{
std::cout << el.first << ' ' << el.second << '\n';
}
}
标准:元组
虽然 std::pair 只能保存两个值,但是 std::tuple 包装器可以保存两个以上的值。要使用元组,我们需要包含 <元组> 头。为了访问某个元组元素,我们使用了STD::get
#include <iostream>
#include <utility>
#include <tuple>
int main()
{
std::tuple<char, int, double> mytuple = { 'a', 123, 3.14 };
std::cout << "The first element is: " << std::get<0>(mytuple) << '\n';
std::cout << "The second element is: " << std::get<1>(mytuple) << '\n';
std::cout << "The third element is: " << std::get<2>(mytuple) << '\n';
}
我们可以使用 std::make_tuple 函数创建一个元组:
#include <iostream>
#include <tuple>
#include <string>
int main()
{
auto mytuple = std::make_tuple<int, double, std::string>(123, 3.14, "Hello World.");
std::cout << "The first tuple element is: " << std::get<0>(mytuple) << '\n';
std::cout << "The second tuple element is: " << std::get<1>(mytuple) << '\n';
std::cout << "The third tuple element is: " << std::get<2>(mytuple) << '\n';
}
我们没有键入冗长的元组类型,即 std::tuple < int,double,std::string > ,而是使用 auto 描述符来为我们推导类型名称。
静态断言
static_assert 指令在编译时检查静态(constexpr)条件。如果条件为 false,则指令编译失败,并显示一条错误消息。示例:
int main()
{
constexpr int x = 123;
static_assert(x == 456, "The constexpr value is not 456.");
}
这里,static_assert 检查编译时 x 的值是否等于 456。因为不是,编译将失败,并显示一条"The constexpr value is not 456."
消息。我们可以将 static_assert 视为在编译时测试代码的一种方式。这也是测试 constexpr 表达式的值是否是我们期望的值的一种简洁方法。
40.1.13 并发性介绍
C++11 标准引入了处理线程的工具。为了启用线程,我们需要在命令行上用 g++ 和 clang 编译时添加 -pthreads 标志。示例:
g++ -std=c++11 -Wall -pthread source.cpp
铿锵声响起:
clang++ -std=c++11 -Wall -pthread source.cpp
当我们编译和链接我们的源代码程序时,就会产生一个可执行文件。当我们启动可执行程序时,程序被载入内存并开始运行。这个正在运行的程序被称为进程。当我们启动多个可执行文件时,我们可以有多个进程。每个进程都有自己的内存,自己的地址空间。在一个进程中,可以有多个线程。有哪些线索?线程或执行线程是一种操作系统机制,它允许我们并发/同时执行多段代码。
例如,我们可以使用线程同时执行多个功能。从更广泛的意义上来说,concurrently 也可以指与平行。线程是进程的一部分。一个进程可以产生一个或多个线程。线程共享同一个内存,因此可以使用这个共享内存相互通信。
为了创建一个线程对象,我们使用来自一个 <线程> 头文件的 std::thread 类模板。一旦被定义,线程就开始执行。为了创建一个在函数中执行代码的线程,我们将函数名作为参数提供给线程构造器。示例:
#include <iostream>
#include <thread>
void function1()
{
for (int i = 0; i < 10; i++)
{
std::cout << "Executing function1." << '\n';
}
}
int main()
{
std::thread t1{ function1 }; // create and start a thread
t1.join(); // wait for the t1 thread to finish
}
这里我们定义了一个名为 t1 的线程,它执行一个函数 function1 。我们将函数名作为第一个参数提供给 std::thread 构造器。在某种程度上,我们的程序现在有一个主线程,它是 main() 函数本身,还有一个 t1 线程,它是从主线程创建的。。join() 成员函数说:“嘿,主线程,请等我完成我的工作再继续你的。”如果我们忽略了。join()函数,主线程将在 t1 线程完成其工作之前完成执行。我们通过将子线程连接到主线程来避免这个问题。
如果我们的函数接受参数,我们可以在构造 std::thread 对象时传递这些参数:
#include <iostream>
#include <thread>
#include <string>
void function1(const std::string& param)
{
for (int i = 0; i < 10; i++)
{
std::cout << "Executing function1, " << param << '\n';
}
}
int main()
{
std::thread t1{ function1, "Hello World from a thread." };
t1.join();
}
通过构造多个 std::thread 对象,我们可以在程序/进程中产生多个线程。例如,我们有两个线程并发/同时执行两个不同的功能:
#include <iostream>
#include <thread>
void function1()
{
for (int i = 0; i < 10; i++)
{
std::cout << "Executing function1." << '\n';
}
}
void function2()
{
for (int i = 0; i < 10; i++)
{
std::cout << "Executing function2." << '\n';
}
}
int main()
{
std::thread t1{ function1 };
std::thread t2{ function2 };
t1.join();
t2.join();
}
这个例子创建了两个线程,同时执行两个不同的函数。
function1 代码在线程 t1、中执行,而 function2 代码在名为 t2 的单独线程中执行。
我们还可以让多个线程同时执行同一个函数的代码:
#include <iostream>
#include <thread>
#include <string>
void myfunction(const std::string& param)
{
for (int i = 0; i < 10; i++)
{
std::cout << "Executing function from a " << param << '\n';
}
}
int main()
{
std::thread t1{ myfunction, "Thread 1" };
std::thread t2{ myfunction, "Thread 2" };
t1.join();
t2.join();
}
线程有时需要访问同一个对象。在我们的例子中,两个线程都在访问全局 std::cout 对象,以便输出数据。这可能是个问题。同时从两个不同的线程访问 std::cout 对象允许一个线程向它写一点,然后另一个线程跳进去向它写一点,我们可以在控制台窗口中结束一些奇怪的文本:
执行。执行功能 1。执行功能 2 。
这意味着我们需要以某种方式同步对一个共享的 std::cout 对象的访问。当一个线程向它写入数据时,我们需要确保该线程不会向它写入数据。
我们通过锁定和解锁互斥体来做到这一点。互斥体由来自 <互斥体> 头的 std::mutex 类模板表示。互斥是一种在多个线程之间同步访问共享对象的方式。一旦线程锁定了互斥体,它就拥有了互斥体,然后执行对共享数据的访问,并在不再需要访问共享数据时解锁互斥体。这确保了当时只有一个线程可以访问一个共享对象,在我们的例子中是 std::cout 。
下面是一个例子,其中两个线程执行相同的函数,并通过锁定和解锁互斥体来保护对 std::cout 对象的访问:
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
std::mutex m; // will guard std::cout
void myfunction(const std::string& param)
{
for (int i = 0; i < 10; i++)
{
m.lock();
std::cout << "Executing function from a " << param << '\n';
m.unlock();
}
}
int main()
{
std::thread t1{ myfunction, "Thread 1" };
std::thread t2{ myfunctiosn, "Thread 2" };
t1.join();
t2.join();
}
我们可以忘记手动解锁互斥锁。更好的方法是使用 std::lock_guard 函数。它锁定互斥体,一旦超出范围,就自动解锁互斥体。示例:
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
std::mutex m; // will guard std::cout
void myfunction(const std::string& param)
{
for (int i = 0; i < 10; i++)
{
std::lock_guard<std::mutex> lg(m);
std::cout << "Executing function from a " << param << '\n';
} // lock_guard goes out of scope here and unlocks the mutex
}
int main()
{
std::thread t1{ myfunction, "Thread 1" };
std::thread t2{ myfunction, "Thread 2" };
t1.join();
t2.join();
}
删除的和默认的功能
如果我们不提供默认的构造器,编译器会为我们生成一个,这样我们就可以写:
class MyClass
{
};
int main()
{
MyClass o; // OK, there is an implicitly defined default constructor
}
但是,在某些情况下,不会隐式生成默认构造器。例如,当我们为类定义一个复制构造器时,默认构造器被隐式删除。示例:
#include <iostream>
class MyClass
{
public:
MyClass(const MyClass& other)
{
std::cout << "Copy constructor invoked.";
}
};
int main()
{
MyClass o; // Error, there is no default constructor
}
为了强制编译器生成的默认构造器的实例化,我们在其声明中提供了 =default 描述符。示例:
#include <iostream>
class MyClass
{
public:
MyClass() = default; // defaulted member function
MyClass(const MyClass& other)
{
std::cout << "Copy constructor invoked.";
}
};
int main()
{
MyClass o; // Now OK, the defaulted default constructor is there
MyClass o2 = o; // Invoking the copy constructor
}
当在成员函数上使用时, =default 描述符意味着:无论语言规则如何,我都希望这个默认成员函数存在。我不希望它被隐式禁用。
类似地,如果我们想禁止一个成员函数出现,我们使用 =delete 描述符。要禁用复制构造器和复制赋值,我们应该编写:
#include <iostream>
class MyClass
{
public:
MyClass()
{
std::cout << "Default constructor invoked.";
}
MyClass(const MyClass& other) = delete; // delete the copy constructor
MyClass& operator=(const MyClass& other) = delete; // delete the copy // assignment operator
};
int main()
{
MyClass o; // OK
MyClass o2 = o; // Error, a call to deleted copy constructor
MyClass o3;
o3 = o; // Error, a call to deleted copy assignment operator
}
这些描述符主要用于我们想要:
-
当我们使用 =default 时,强制或实例化隐式定义的成员函数,如构造器和赋值操作符;表情
-
使用 =delete 禁用隐式定义的成员函数的实例化;表情
这些表达式也可以用于其他函数。
类型别名
类型别名是用户为现有类型提供的名称。如果我们想对现有类型使用不同的名称,我们写:使用 my _ type _ name = existing _ type _ name;举例:
#include <iostream>
#include <string>
#include <vector>
using MyInt = int;
using MyString = std::string;
using MyVector = std::vector<int>;
int main()
{
MyInt x = 123;
MyString s = "Hello World";
MyVector v = { 1, 2, 3, 4, 5 };
}
40.2 C++14
C++14 是 2014 年发布的 ISO C++ 标准。它为语言和标准库带来了一些补充,但主要是补充和修正 C++11 标准。当我们说要使用 C++11 标准时,我们实际上想要的是 C++14 标准。下面是 C++14 的一些新特性。
要针对 C++14 进行编译,如果使用 g++ 或 clang 编译器,请在命令行编译字符串中添加 -std=c++14 标志。在 Visual Studio 中,选择项目/选项/配置属性/ C/C++ /语言/C++ 语言标准,选择 C++14 。
二进制文字
值由文字表示。到目前为止,我们已经提到了三种不同的二进制文字:十进制、十六进制和八进制,如下例所示:
int main()
{
int x = 10;
int y = 0xA;
int z = 012;
}
这三个变量具有相同的值 10,由不同的数字文字表示。C++14 标准引入了第四种整型文字,称为二进制文字。使用二进制文字,我们可以用二进制形式表示值。文字有一个0b
前缀,后跟一系列表示值的 1 和 0。为了用二进制文字表示数字 10,我们写:
int main()
{
int x = 0b101010;
}
二进制形式的著名数字 42 是:
int main()
{
int x = 0b1010;
}
Important to remember
价值观就是价值观;它们是内存中一些位和字节的序列。可以不同的是值表示法。该值有十进制、十六进制、八进制和二进制表示形式。同一事物的这些不同形式可能与我们人类相关。对一台机器来说,它全是比特和字节、晶体管和电流。
40.2.2 数字分隔符
在 C++14 中,我们可以用单引号分隔数字,使其可读性更好:
int main()
{
int x =100'000'000;
}
编译器会忽略引号。这里的分隔符只是为了我们的利益,例如,将大量内容分成可读性更强的部分。
自动 for 功能
我们可以根据返回语句的值推断出函数类型:
auto myintfn() // integer
{
return 123;
}
auto mydoublefn() // double
{
return 3.14;
}
int main()
{
auto x = myintfn(); // int
auto d = mydoublefn(); // double
}
40.2.4 通用 Lambdas
我们现在可以在 lambda 函数中使用auto
参数。参数的类型将从提供给 lambda 函数的值中推导出来。这也被称为通用λ:
#include <iostream>
int main()
{
auto mylambda = [](auto p) {std::cout << "Lambda parameter: " << p << '\n'; };
mylambda(123);
mylambda(3.14);
}
40.2.5 标准::制作 _ 唯一
C++14 引入了一个用于创建唯一指针的 std::make_unique 函数。它是在一个<memory>
头中声明的。创建唯一指针时,使用此函数优于原始new
运算符:
#include <iostream>
#include <memory>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd)
: x{ xx }, d{ dd } {}
void printdata() { std::cout << "x: " << x << ", d: " << d; }
};
int main()
{
auto p = std::make_unique<MyClass>(123, 456.789);
p->printdata();
}
40.3 C++17
C++17 标准引入了新的语言和库特性,并更改了一些语言规则。
嵌套的名称空间
还记得我们说过可以有嵌套的名称空间吗?我们可以把一个名称空间放到另一个名称空间中。我们使用了以下嵌套命名空间:
namespace MyNameSpace1
{
namespace MyNameSpace2
{
namespace MyNameSpace3
{
// some code
}
}
}
C++17 标准允许我们使用名称空间解析操作符来嵌套名称空间。上面的例子现在可以重写为:
namespace MyNameSpace1::MyNameSpace2::MyNameSpace3
{
// some code
}
40.3.2 Constexpr Lambdas
Lambdas 现在可以是一个常量表达式,这意味着它们可以在编译时进行计算:
int main()
{
constexpr auto mylambda = [](int x, int y) { return x + y; };
static_assert(mylambda(10, 20) == 30, "The lambda condition is not true.");
}
我们将constexpr
描述符放入 lambda 本身的一个等价例子是:
int main()
{
auto mylambda = [](int x, int y) constexpr { return x + y; };
static_assert(mylambda(10, 20) == 30, "The lambda condition is not true.");
}
在早期的 C++ 标准中,情况并非如此。
结构化绑定
结构化绑定将变量名绑定到编译时已知表达式的元素,如数组或映射。如果我们想让多个变量接受表达式元素的值,我们使用结构化绑定。语法是:
auto [myvar1, myvar2, myvar3] = some_expression;
一个简单的例子是,我们将三个变量绑定为三个数组元素的别名:
int main()
{
int arr[] = { 1, 2, 3 };
auto [myvar1, myvar2, myvar3] = arr;
}
现在我们已经定义了三个整型变量。这些变量的数组元素值分别为 1、2、3。这些变量是数组元素的副本。对变量进行更改不会影响数组元素本身:
#include <iostream>
int main()
{
int arr[] = { 1, 2, 3 };
auto [myvar1, myvar2, myvar3] = arr;
myvar1 = 10;
myvar2 = 20;
myvar3 = 30;
for (auto el : arr)
{
std::cout << el << ' ';
}
}
我们可以使用 auto&语法进行引用类型的结构化绑定。这意味着变量现在是数组元素的引用,对变量进行更改也会更改数组元素:
#include <iostream>
int main()
{
int arr[] = { 1, 2, 3 };
auto& [myvar1, myvar2, myvar3] = arr;
myvar1 = 10;
myvar2 = 20;
myvar3 = 30;
for (auto el : arr)
{
std::cout << el << ' ';
}
}
这是将多个变量引入并绑定到一些类似容器的表达式元素的一种很好的方式。
标准::文件系统
std::filesystem 库允许我们在系统上处理文件、路径和文件夹。这个库是通过一个
#include <iostream>
#include <filesystem>
int main()
{
std::filesystem::path folderpath = "C:\\MyFolder\\";
if (std::filesystem::exists(folderpath))
{
std::cout << "The path: " << folderpath << " exists.";
}
else
{
std::cout << "The path: " << folderpath << " does not exist.";
}
}
类似地,我们可以使用 std::filesystem::path 对象来检查文件是否存在:
#include <iostream>
#include <filesystem>
int main()
{
std::filesystem::path folderpath = "C:\\MyFolder\\myfile.txt";
if (std::filesystem::exists(folderpath))
{
std::cout << "The file: " << folderpath << " exists.";
}
else
{
std::cout << "The file: " << folderpath << " does not exist.";
}
}
为了迭代文件夹元素,我们使用了STD::file system::directory _ iterator迭代器:
#include <iostream>
#include <filesystem>
int main()
{
auto myfolder = "C:\\MyFolder\\";
for (auto el : std::filesystem::directory_iterator(myfolder))
{
std::cout << el.path() << '\n';
}
}
这里我们遍历目录条目,并使用。path()成员函数。
对于 Linux,我们需要调整路径并使用以下内容:
#include <iostream>
#include <filesystem>
int main()
{
auto myfolder = "MyFolder/";
for (auto el : std::filesystem::recursive_directory_iterator(myfolder))
{
std::cout << el.path() << '\n';
}
}
为了递归地迭代文件夹元素,我们使用STD::file system::recursive _ directory _ iterator。这允许我们递归地遍历一个文件夹中的所有子文件夹。在 Windows 上,我们将使用:
#include <iostream>
#include <filesystem>
int main()
{
auto myfolder = "C:\\MyFolder\\";
for (auto el : std::filesystem::recursive_directory_iterator(myfolder))
{
std::cout << el.path() << '\n';
}
}
在 Linux 和类似的操作系统上,我们将使用以下路径:
#include <iostream>
#include <filesystem>
int main()
{
auto myfolder = "MyFolder/";
for (auto el : std::filesystem::directory_iterator(myfolder))
{
std::cout << el.path() << '\n';
}
}
下面是 std::filesystem 名称空间中一些有用的实用函数:
-
std::filesystem::create_directory
用于创建目录 -
std::filesystem::copy
用于复制文件和目录 -
std::filesystem::remove
用于删除文件或空文件夹 -
std::filesystem::remove_all
用于删除文件夹和子文件夹
std::string_view
就 CPU 使用率而言,复制数据可能是一项开销很大的操作。将子字符串作为函数参数传递需要复制子字符串。这是一个昂贵的手术。string_view 类模板试图纠正这种情况。
string_view 是字符串或子字符串的非所有者视图。它是对内存中已经存在的东西的引用。它被实现为指向某个字符序列的指针加上该序列的大小。有了这种结构,我们可以高效地解析字符串。
std::string_view 在<string_view>
头文件中声明。要从现有字符串创建 string_view,我们编写:
#include <iostream>
#include <string>
#include <string_view>
int main()
{
std::string s = "Hello World.";
std::string_view sw(s);
std::cout << sw;
}
要为前五个字符的子字符串创建 string_view,我们使用不同的构造器重载。这个 string_view 构造器接受一个指向第一个字符串元素和子字符串长度的指针:
#include <iostream>
#include <string>
#include <string_view>
int main()
{
std::string s = "Hello World.";
std::string_view sw(s.c_str() , 5);
std::cout << sw;
}
一旦我们创建了一个 string_view,我们就可以使用它的成员函数。为了从 string_view 中创建一个子串,我们使用了.substr()
成员函数。为了创建子串,我们提供起始位置索引和长度。为了创建前五个字符的子串,我们使用:
#include <iostream>
#include <string>
#include <string_view>
int main()
{
std::string s = "Hello World";
std::string_view sw(s);
std::cout << sw.substr(0, 5);
}
string_view 允许我们解析(而不是更改)已经在内存中的数据,而不必复制数据。该数据属于另一个字符串或字符数组对象。
标准::任何
std::any 容器可以保存任何类型的单个值。这个容器是在头文件中声明的。示例:
#include <any>
int main()
{
std::any a = 345.678;
std::any b = true;
std::any c = 123;
}
为了以安全的方式访问 std::any 对象的值,我们使用 std::any_cast 函数将其转换为我们选择的类型:
#include <iostream>
#include <any>
int main()
{
std::any a = 123;
std::cout << "Any accessed as an integer: " << std::any_cast<int>(a) << '\n';
a = 456.789;
std::cout << "Any accessed as a double: " << std::any_cast<double>(a) << '\n';
a = true;
std::cout << "Any accessed as a boolean: " << std::any_cast<bool>(a) << '\n';
}
重要的是,如果我们试图将 123 转换为 double 类型,那么 std::any_cast 将抛出一个异常。此函数仅执行类型安全转换。另一个 std::any 成员函数是。has_value() 检查 std::any 对象是否包含值:
#include <iostream>
#include <any>
int main()
{
std::any a = 123;
if (a.has_value())
{
std::cout << "Object a contains a value." << '\n';
}
std::any b{};
if (b.has_value())
{
std::cout << "Object b contains a value." << '\n';
}
else
{
std::cout << "Object b does not contain a value." << '\n';
}
}
40.3.7 标准::变体
C++ 中还有一种数据叫做 union 。联合是一种类型,其不同类型的数据成员占用相同的内存。一次只能访问一个数据成员。内存中联合的大小是其最大数据成员的大小。数据成员在某种意义上是重叠的。要在 C++ 中定义联合类型,我们编写:
union MyUnion
{
char c; // one byte
int x; // four bytes
double d; // eight bytes
};
这里我们声明了一个联合类型,它可以保存字符、整数或双精度数。该联合的大小是其最大数据成员 double 的大小,可能是 8 个字节,这取决于实现。尽管 union 声明了多个数据成员,但它在任何给定时间只能保存一个成员的值。这是因为所有的数据成员共享相同的内存位置。我们只能访问最后写入的成员。示例:
#include <iostream>
union MyUnion
{
char c; // one byte
int x; // four bytes
double d; // eight bytes
};
int main()
{
MyUnion o;
o.c = 'A';
std::cout << o.c << '\n';
// accessing o.x or o.d is undefined behavior at this point
o.x = 123;
std::cout << o.c;
// accessing o.c or o.d is undefined behavior at this point
o.d = 456.789;
std::cout << o.c;
// accessing o.c or o.x is undefined behavior at this point
}
C++17 引入了一种使用来自 < variant > 头的 std::variant 类模板处理联合的新方法。这个类模板提供了一种存储和访问联合的类型安全方式。要使用 std::variant 声明一个变量,我们应该写:
#include <variant>
int main()
{
std::variant<char, int, double> myvariant;
}
此示例定义了一个可以保存三种类型的变量。当我们初始化变量或给变量赋值时,就选择了一个合适的类型。例如,如果我们用一个字符值初始化一个变量,这个变量当前将保存一个 char 数据成员。此时访问其他成员将引发异常。示例:
#include <iostream>
#include <variant>
int main()
{
std::variant<char, int, double> myvariant{ 'a' }; // variant now holds // a char
std::cout << std::get<0>(myvariant) << '\n'; // obtain a data member by // index
std::cout << std::get<char>(myvariant) << '\n'; // obtain a data member // by type
myvariant = 1024; // variant now holds an int
std::cout << std::get<1>(myvariant) << '\n'; // by index
std::cout << std::get<int>(myvariant) << '\n'; // by type
myvariant = 123.456; // variant now holds a double
}
我们可以使用STD::get
#include <iostream>
#include <variant>
int main()
{
std::variant<int, double> myvariant{ 123 }; // variant now holds an int
std::cout << "Current variant: " << std::get<int>(myvariant) << '\n';
try
{
std::cout << std::get<double>(myvariant) << '\n'; // exception is // raised
}
catch (const std::bad_variant_access& ex)
{
std::cout << "Exception raised. Description: " << ex.what();
}
}
我们定义了一个变量,既可以保存 int 也可以保存 double。我们用一个 int 类型的 123 字面值初始化变量。所以现在我们的变量保存了一个 int 数据成员。我们可以使用索引 0 或提供给 std::get 函数的类型名来访问该成员。然后我们试图访问 double 类型的错误数据成员。会引发异常。并且该异常的特定类型是 std::bad_variant_access 。在 catch 块中,我们通过解析名为 ex 的参数来处理异常。参数的类型是 std::bad_variant_access,,它有一个。what() 提供异常简短描述的成员函数。
40.4 C++20
C++ 20 标准承诺给这种语言带来一些大的补充。据说它对现有标准的影响就像 C++11 对 C++98/C++03 标准的影响一样大。在撰写本文时,C++20 标准将在 2020 年 5 月左右获得批准。编译器中的完整实现和支持应该紧随其后。乍一看,下面的一些事情似乎有些吓人,尤其是在开始学 C++ 的时候。但是,不要担心。在撰写本文时,没有一个编译器完全支持 C++20 标准,但这种情况即将改变。一旦编译器完全支持 C++20 标准,测试这些例子就容易多了。记住这一点,让我们来看看一些最令人兴奋的 C++20 特性。
模块
模块是 C++20 的新特性,旨在消除将代码分成头文件和源文件的需要。到目前为止,在传统的 C++ 中,我们使用头文件和源文件来组织我们的源代码。我们将声明/接口保存在头文件中。我们将定义/实现放在源文件中。例如,我们有一个带有函数声明的头文件:
mylibrary.h
#ifndef MYLIBRARY_H
#define MYLIBRARY_H
int myfunction();
#endif // !MYLIBRARY_H
这里我们声明了一个名为myfunction().
的函数,我们用头文件保护将代码包围起来,这确保了头文件在编译过程中不会被多次包含。我们有一个包含函数定义的源文件。这个源文件包括我们的头文件:
mylibrary.cpp
#include "mylibrary.h"
int myfunction()
{
return 123;
}
在我们的 main.cpp 文件中,我们还包含了上面的头文件并调用了函数:
#include "mylibrary.h"
int main()
{
int x = myfunction();
}
我们多次包含同一个标题。这增加了编译时间。模块只包含一次,我们不必将代码分成接口和实现。一种方法是拥有一个单独的模块文件,例如, mymodule.cpp ,我们在其中提供这个函数的完整实现和导出。
为了创建一个实现并导出上述函数的简单模块文件,我们编写:
mymodule.cpp:
export module mymodule;
export int myfunction() { return 123; }
说明:export module mymodule;
行表示在这个文件中有一个名为 mymodule 的模块。在第二行,函数上的导出描述符意味着一旦模块被导入到主程序中,该函数将是可见的。
我们通过编写import mymodule;
语句将模块包含在主程序中。
main.cpp :
import mymodule;
int main()
{
int x = myfunction();
}
在我们的主程序中,我们导入模块并调用导出的myfunction()
函数。
模块也可以提供一个实现,但是需要导出它。如果我们不希望函数对主程序可见,我们将省略模块中的导出描述符。这使得实现专用于模块:
export module mymodule;
export int myfunction() { return 123; }
int myprivatefunction() { return 456; }
如果我们有一个包含名称空间的模块,并且该名称空间内的声明被导出,则整个名称空间被导出。在该命名空间中,只有导出的函数是可见的示例:
mymodule2.cpp :
export module mymodule2;
namespace MyModule
{
export int myfunction() { return 123; }
}
main2.cpp:
import mymodule2;
int main()
{
int x = MyModule::myfunction();
}
概念
还记得提供泛型类型 T 的类模板和函数模板吗?如果我们希望我们的模板参数 T 满足某些要求,那么我们使用概念。换句话说,我们希望我们的 T 满足某些编译时标准。一个概念的特征是:
template <typename T>
concept concept_name = requires (T var_name) { reqirement_expression; };
第二行定义了一个概念名,后面是一个保留字requires
,后面是一个可选的模板参数 T 和一个局部变量名称,后面是一个 bool 类型的 const exprrequirement_expression
。
简而言之,概念谓词指定了模板参数在模板中使用时必须满足的要求。有些需求我们可以自己写,有些是已经预先做好的。
我们可以说,概念将类型约束到某些需求。它们也可以被看作是我们模板类型的一种编译时断言。
例如,如果我们想让模板参数递增 1,我们将为它指定概念:
template <typename T>
concept MustBeIncrementable = requires (T x) { x += 1; };
为了在模板中使用这个概念,我们编写:
template<MustBeIncrementable T>
void myfunction(T x)
{
// code goes in here
}
将概念包含到模板中的另一种方法是:
template<typename T> requires MustBeIncrementable <T>
void myfunction(T x)
{
// code goes in here
}
完整的工作示例是:
#include <iostream>
#include <concepts>
template <typename T>
concept MustBeIncrementable = requires (T x) { x ++; };
template<MustBeIncrementable T>
void myfunction(T x)
{
x += 1;
std::cout << x << '\n';
}
int main()
{
myfunction<char>(42); // OK
myfunction<int>(123); // OK
myfunction<double>(345.678); // OK
}
这个概念确保了 T 类型的参数 x 必须能够接受 operator ++,并且参数必须能够增加 1。这种检查是在编译时执行的。对于类型char
、int
和double
来说,这个要求确实是正确的。如果我们使用了一个不满足要求的类型,编译器会发出一个编译时错误。
我们可以结合多个概念。例如,我们有一个概念,要求 T 参数是偶数或奇数。
template <typename T>
concept MustBeEvenOrOdd = requires (T x) { x % 2; };
现在我们的模板可以包含MustBeIncrementable
和MustBeEvenOrOdd
概念:
template<typename T> requires MustBeIncrementable<T> && MustBeEvenNumber<T>;
void myfunction(T x)
{
// code goes in here
}
关键字requires
既用于概念中的表达式,也用于将概念包含到我们的模板类/函数中。
包括两个概念要求的完整计划将是:
#include <iostream>
#include <concepts>
template <typename T>
concept MustBeIncrementable = requires (T x) { x++; };
template <typename T>
concept MustBeEvenOrOdd = requires (T x) { x % 2; };
template<typename T> requires MustBeIncrementable<T> && MustBeEvenOrOdd<T>
void myfunction(T x)
{
std::cout << "The value conforms to both conditions: " << x << '\n';
}
int main()
{
myfunction<char>(123); // OK
myfunction<int>(124); // OK
myfunction<double>(345); // Error, a floating point number is not even // nor odd
}
在这个例子中,如果在编译期间两个概念需求都被评估为真,那么模板将被实例化。只有myfunction<char>(123);
和myfunction<int>(124);
函数可以被实例化并通过编译。char 和 int 类型的参数确实是可递增的,可以是偶数也可以是奇数。然而,声明myfunction<double>(345
);不通过编译。原因是第二个要求MustBeEvenOrOdd
没有满足,因为浮点数既不是奇数也不是偶数。
重要!这两个概念都表示:对于 T 类型的每个 x,代码块{ }中的语句进行编译,仅此而已。它只是编译。如果它可以编译,就满足了该类型的要求。
例如,如果我们希望我们的类型 T 有一个成员函数。 empty() 我们希望该函数的结果可转换为 bool 类型,我们编写:
template <typename T>
concept HasMemberFunction requires (T x)
{
{ x.empty() } -> std::convertible_to(bool);
};
在 C++20 标准中有多个预定义的概念。他们检查类型是否满足某些要求。这些预定义的概念位于
-
STD::integral–指定类型应该是整数类型
-
STD::boolean–指定该类型可以用作布尔类型
-
STD::move _ constructible–指定可以使用移动语义来构造特定类型的对象
-
STD::mobile–指定可以移动特定类型 T 的对象
-
STD::signed _ integral–表示该类型既是整型又是有符号整型
40 . 4 . 3λ模板
我们现在可以在 lambda 函数中使用模板语法。示例:
auto mylambda = []<typename T>(T param)
{
// code
};
例如,要使用模板化的 lambda 表达式打印输出泛型类型名,我们应该编写:
#include <iostream>
#include <vector>
#include <typeinfo>
int main()
{
auto mylambda = []<typename T>(T param)
{
std::cout << typeid(T).name() << '\n';
};
std::vector<int> v = { 1, 2, 3, 4, 5 };
mylambda(v); // integer
std::vector<double> v2 = { 3.14, 123.456, 7.13 };
mylambda(v2); // double
}
40 . 4 . 4[可能]和[不太可能]的属性
如果我们知道一些执行路径比其他路径更有可能被执行,我们可以通过放置属性来帮助编译器优化代码。我们在更有可能被执行的语句前使用[[可能]] 属性。我们也可以把[[不太可能]] 属性放在不太可能执行的语句前面。例如,属性可以用在开关语句内的 case 分支上:
#include <iostream>
void mychoice(int i)
{
switch (i)
{
[[likely]] case 1:
std::cout << "Likely to be executed.";
break;
[[unlikely]] case 2:
std::cout << "Unlikely to be executed.";
break;
default:
break;
}
}
int main()
{
mychoice(1);
}
如果我们想在 if-else 分支上使用这些属性,我们写:
#include <iostream>
int main()
{
bool choice = true;
if (choice) [[likely]]
{
std::cout << "This statement is likely to be executed.";
}
else [[unlikely]]
{
std::cout << "This statement is unlikely to be executed.";
}
}
40.4.5 范围
一般来说,范围是指一系列元素的对象。新的 C++20 范围特性是在一个<ranges>
头文件中声明的。范围本身通过 std::ranges 名称访问。对于 std::vector 这样的经典容器,如果我们想对数据进行排序,我们可以使用:
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
std::sort(v.begin(), v.end());
for (auto el : v)
{
std::cout << el << '\n';
}
}
std::sort 函数接受 vector 的。begin() 和 end() 迭代器。对于范围,就简单多了,我们只提供范围的名称,没有迭代器:
#include <iostream>
#include <ranges>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 3, 5, 2, 1, 4 };
std::ranges::sort(v);
for (auto el : v)
{
std::cout << el << '\n';
}
}
范围有一个称为适配器的特性。范围适配器之一是视图。视图适配器通过视图不拥有的std::ranges::views.
访问。它们不能更改基础元素的值。也有人说他们被懒洋洋地处决了。这意味着来自视图适配器的代码将不会被执行,直到我们迭代这些视图的结果。
让我们创建一个示例,通过创建一个范围视图,使用范围视图过滤出偶数,只打印向量中的奇数:
#include <iostream>
#include <ranges>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
auto oddnumbersview = v | std::views::filter([](int x) { return x % 2 == 1; });
for (auto el : oddnumbersview)
{
std::cout << el << '\n';
}
}
说明:我们有一个简单的向量和一些元素。然后,我们在该向量上创建一个视图范围适配器,它过滤该范围内的数字。为此,我们使用管道操作符|。只包括谓词为真的数字。在我们的例子中,这意味着偶数被排除在外。然后我们迭代过滤后的视图并打印出元素。
重要的是要注意,底层向量的元素不受影响,因为我们是在视图上操作,而不是在向量上操作。
让我们创建一个示例,该示例创建一个只返回大于 2 的数字的视图:
#include <iostream>
#include <ranges>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
auto greaterthan2view = v | std::views::filter([](int x) { return x > 2; });
for (auto el : greaterthan2view)
{
std::cout << el << '\n';
}
}
现在,让我们通过使用多个管道|操作符将两个视图分开,将它们合并成一个大视图:
#include <iostream>
#include <ranges>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
auto oddandgreaterthan2 = v | std::views::filter([](int x) { return x % 2 == 1; })
| std::views::filter([](int x) { return x > 2; });
for (auto el : oddandgreaterthan2)
{
std::cout << el << '\n';
}
}
此示例创建一个包含大于 2 的奇数的视图范围适配器。我们通过将两个不同的范围视图合并为一个来创建此视图。
另一个范围适配器是算法。这个想法是让算法超载范围。要调用算法适配器,我们使用:std::ranges::algorithm_name(parameters)
。使用 std::ranges::reverse()算法的示例:
#include <iostream>
#include <ranges>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
std::ranges::reverse(v);
for (auto el : v)
{
std::cout << el << '\n';
}
}
与视图不同,范围算法修改实际的矢量内容。
协程程序
协程是一个可以挂起和恢复的函数。如果普通函数在其函数体中使用以下任何运算符,则该函数是协同例程:
-
co _ await——暂停协程的执行,直到执行了其他一些计算,也就是说,直到协程本身恢复
-
co _ yield–挂起一个协同程序,并向调用者返回值
-
co _ return–从协程返回并停止执行
40.4.7 标准::跨度
一些容器和类型按顺序存储它们的元素,一个挨着一个。数组和向量就是这种情况。我们可以用指向第一个元素的指针加上容器的长度来表示这样的容器。一个来自 < span > 头的 std::span 类模板就是这样。对一系列连续容器元素的引用。使用 std::span 的一个原因是它的构建和复制成本很低。Span 不拥有它引用的向量或数组。但是,它可以改变元素的值。为了从向量创建跨度,我们使用:
#include <iostream>
#include <vector>
#include <span>
int main()
{
std::vector<int> v = { 1, 2, 3 };
std::span<int> myintspan = v;
myintspan[2] = 256;
for (auto el : v)
{
std::cout << el << '\n';
}
}
这里,我们创建了一个引用向量元素的 span。然后我们使用 span 来改变向量的第三个元素。使用 span,我们不必担心传递指针和长度,我们只需使用 span 包装器的简洁语法。因为向量的大小可以改变,我们说我们的跨度有一个动态范围。我们可以从固定大小的数组创建固定大小的跨度。我们说我们的跨度现在有一个静态范围。示例:
#include <iostream>
#include <span>
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
std::span<int, 5> myintspan = arr;
myintspan[4] = 10;
for (auto el : arr)
{
std::cout << el << '\n';
}
}
数学常量
C++20 标准引入了一种表示一些数学常量的方法。要使用它们,我们需要包含 <数字> 头。常量本身位于 std::numbers 名称空间中。下面的例子展示了如何使用数字 pi 和 e ,对数函数的结果以及数字 2 和 3 的平方根:
#include <iostream>
#include <numbers>
int main()
{
std::cout << "Pi: " << std::numbers::pi << '\n';
std::cout << "e: " << std::numbers::e << '\n';
std::cout << "log2(e): " << std::numbers::log2e << '\n';
std::cout << "log10(e): " << std::numbers::log10e << '\n';
std::cout << "ln(2): " << std::numbers::ln2 << '\n';
std::cout << "ln(10): " << std::numbers::ln10 << '\n';
std::cout << "sqrt(2): " << std::numbers::sqrt2 << '\n';
std::cout << "sqrt(3): " << std::numbers::sqrt3 << '\n';
}
标签:std,教程,main,cout,int,---,初学者,include,函数
From: https://www.cnblogs.com/apachecn/p/18343094