面向懒惰程序员的 C++20 教程(八)
二十四、构建更大的项目
有一天你可能想要建立一个更大的项目。本章介绍了一些有用的工具:名称空间、条件编译和库的构造。
命名空间
假设我为地理信息编写了一个库,用于地图、划分选区或其他用途。我创建了一些类:map s,vector s(用于图形的 XY 对),region s,等等。
然后我注意到我不能编译了,因为map和vector在 C++ 中已经有意义了。好的。称它们为GeoLib_map、GeoLib_vector等等,就像 SDL 和 SSDL 函数一样。
我使用的是第三方库,它碰巧将region定义为别的东西……这变得很乏味。有捷径吗?
当然可以。制作一个命名空间 GeoLib,把你的代码放进去,如图 24-1 。
图 24-1
一个命名空间可以包含来自不同文件的代码
程序员现在可以输入GeoLib :: map或std::map,编译器会知道它们的意思。
如果他们厌倦了反复输入GeoLib::,他们可以使用using:
using GeoLib::region;
// after this you can omit the GeoLib:: in GeoLib::region
using namespace GeoLib;
// now *all* GeoLib members can have GeoLib:: omitted
using namespace std;
// now all std:: members can have std:: omitted too
// If the compiler gripes, you can still use GeoLib::
// or std:: to clarify which you want
您还可以用普通的::来指定没有在的任何名称空间中声明的内容(因此在“全局”名称空间中),比如:::myNonNamespaceFunction();。
为了说明名称空间的构造,示例 24-3 和 24-4 示出了名称空间Cards的创建;示例 24-5 使用。
这是一个关于using namespace <whatever>;是否邪恶的争论问题,也就是说,不可原谅的可怕。我说你可以在你自己的.cpp文件中随心所欲地使用它,但不要把它放在别人可能包括的.h文件中,从而搞乱别人的文件。
条件编译
现在我正在使用我的GeoLib代码,我发现我的计算是错误的,错误的,错误的。很难说哪些功能搞砸了。我想生成这些计算的报告,以便我可以检查它们:
map::area(region) thinks area of block group 6709 is 672.4
dist to center is 356.2
map::area(region) thinks area of block group 6904 is 312.5
dist to center is 379.7
...
我不希望这段代码一直被打印出来——只是在调试的时候。
所以我在一个.h文件中创建了一个#define,其他所有文件都包含在其中(例如 24-1 )。
// debugSetup.h
#ifndef DEBUGSETUP_H
#define DEBUGSETUP_H
#define DEBUG_GEOLIB // Yes, that's the whole thing
#endif //DEBUGSETUP_H
Example 24-1A .h file containing #define DEBUG_GEOLIB, for conditional compilation
我在任何需要打印调试信息的地方都使用它
#ifdef DEBUG_GEOLIB
cout << " map::area(region) thinks area of block group "
<< bg->id() << " is " << bg->area() << endl;
cout << "dist to center is "
<< distance (region.loc(), bg->loc()) << endl;
#endif
并根据我是否想看到它来注释或取消注释# define DEBUG_GEOLIB。
库
库有两种风格,静态和共享。静态库的代码在链接时直接进入可执行程序;共享库在另一个文件中,在运行时加载。所以据说静态库运行起来更快(我从未注意到有什么不同),而且你不必担心你的共享库被移到哪里了,因为它总是在可执行文件中。但是共享可以节省空间,因为许多程序可以使用相同的代码,而且更容易更新。
我倾向于分享。这在 Unix 中很常见,似乎有助于编译器版本之间的可移植性。
在这里,我将对两种编译器都尝试这两种方法。在我的例子中,我将使用第十九章中的纸牌游戏代码,以及一般有用的类(Card、Deck等)。)走进库。蒙大拿的比赛将使用这个库。
您可能会创建一些其他的库。参见本章末尾的“练习”,或者自己选择。
g++
证明这一点的代码在源代码中,ch24/g++。库在子目录cardLibg++下,测试程序在子目录montana下。
编译库
要创建一个静态库,像往常一样编译目标文件
g++ -g -c deck.cpp
...
然后链接到
ar rcs libcards.a
deck.o card.o cardgroup.o
#ar for "archive"; rcs is needed program optionsShared libraries
共享库需要在内存中“可重定位”的目标文件,所以像这样编译它们:g++ -g -fPIC -c deck.cpp #PIC: "position independent code." All righty then ...
在 Unix 中,共享库以.so结尾,所以这样链接:g++ -shared -o libcards.so deck.o card.o cardgroup.o。
Windows 使用扩展名.dll,所以对于 MinGW,键入这个:g++ -shared -o libcards.dll deck.o card.o cardgroup.o。
链接库
g++ 需要知道在哪里可以找到包含文件,在哪里可以找到库文件,以及使用什么库。
我们用这些命令行选项告诉它:
-
-I<name of directory>查找包含文件; -
-L<name of directory>查找库文件; -
-l<library>表示我们想要链接的库。库名的首字母是lib,扩展名是.a、.so或.dll,去掉后,如下所示:
g++ -o montana -g montana.o io.o montana_main.o
\1
-I../cardLibg++ -L../cardLibg++ -lcards
#uses libcards.<something>
您可以拥有这些选项的任意多个副本。
运行使用动态库的程序
如果你使用了一个静态库,你可以像往常一样运行程序。
如果它是动态的,系统需要知道在哪里可以找到它。解决方案:
-
贿赂系统管理员将
.dll或.so文件放在系统路径中。如果多个程序使用它并且你的程序足够重要,这是有意义的。 -
将
.dll或.so复制到包含可执行文件的文件夹中——这对于单个项目来说很好,但如果您有很多文件夹并因此有很多副本,就不太好了。 -
设置环境变量,以便系统可以找到它。源代码文件夹里有这个的脚本(
runx、runw、gdbx等)。,正如 SSDL 的情况一样)。内容是这样的:
export LD_LIBRARY_PATH=../cardLibg++ #Unix
PATH="../cardLibg++:$PATH" #Windows
文件
这些长命令重复输入会变得很乏味,所以它们被打包成本章源代码中的文件:用于在 Unix 或 MinGW 中构建库的 Makefiles,就像 SSDL 一样;另一个用于构建使用该库的程序(它适用于两种平台);以及在后者的文件夹中,用于运行程序的脚本(见上文)。
要创建自己的库,请编辑构建库的 Makefile 文件,选择要创建的库的类型,然后编辑所有文件的路径、可执行文件名称以及您喜欢的任何内容。
工具
演示这一点的代码在源代码中。ch24/VisualStudio。cardLib*(有不同版本;创建库,uses*是使用这些库的项目。
静态库:简单的选择
若要在 Visual Studio 中生成静态库,请单击“创建新项目”并选择“静态库”。
当它创建项目时,它希望您使用“预编译头” 2 你可以:
- 通过在每个源文件的开头放置这一行来支持这一点:
#include "``pch.h
它必须在任何其他包含之前,否则你的代码将无法编译。
或者一步到位:
- 消除预编译头文件。在“项目属性”下,对于“所有配置/所有平台”(第一行),将配置属性“➤ C/C++ ➤预编译头➤预编译头”设置为“不使用预编译头”。您可以忽略编译器在项目中提供给您的文件。
然后建库。我这样做是为了调试和版本,x86,和 x64,所以我不需要想我做了什么。
现在创建一个使用该库的项目。它需要知道在哪里可以找到包含文件。在所有配置/所有平台的项目属性下,适当地设置配置属性➤ C/C++ ➤通用➤附加包含目录(见图 24-2 )。
图 24-2
在 Visual Studio 中告诉项目在哪里可以找到库包含文件
将库的路径添加到配置属性➤链接器➤通用➤附加库目录(图 24-3 )。它的位置将因配置和平台而异;在名为Debug、Release和x64的子文件夹中搜索。
图 24-3
告诉项目在 Visual Studio 中何处可以找到您的库
现在你必须告诉它什么是库。在项目属性下,所有配置/所有平台,在配置属性下添加库名➤链接器➤输入➤附加依赖项(图 24-4 )。会是<your library project>.lib。
图 24-4
在 Visual Studio 中添加库依赖项
动态链接库(dll):不那么容易的选择
要创建自己的 DLL,请回到上一节,但对于项目类型,请选择动态链接库(DLL)。但是先不要建!
在指导新程序使用你的库的时候,当你告诉项目属性关于这个库的时候(图 24-4 ,它仍然是<your project>.lib。我以为我们在创建一个 DLL?是的,但我们实际上是在创建两件事:包含运行时代码的.dll,以及在编译时告诉程序“你稍后将从 DLL 中导入这些东西”的.lib文件
这就是奇怪的地方。当编译器看到一个函数声明时,它需要知道是要编译和导出它(因为它正在编译库)还是从 DLL 导入它(因为它正在编译一个使用库的程序)。换句话说,就是在声明前面加上__declspec ( dllexport )或__declspec( dllimport )。__declspec的意思是“我马上要告诉你一些关于这个功能的事情”和dllexport / dllimport,嗯,那是显而易见的。
那么我们是不是应该为每个函数写两个版本,一个用于导入,一个用于导出?
这种常见的黑客手段意味着我们不必。
- 像示例 24-2 那样写一个
.h文件。
// Header to make DLL functions import or export
// -- from _C++20 for Lazy Programmers_
#ifndef CARDSSETUP_H
#define CARDSSETUP_H
# ifdef IM_COMPILING_MY_CARD_LIBRARY_RIGHT_NOW
# define DECLSPEC __declspec(dllexport)
# else
# define DECLSPEC __declspec(dllimport)
# endif //IM_COMPILING_MY_CARD_LIBRARY_RIGHT_NOW
#endif //CARDSSETUP_H
Example 24-2A .h file to help with DLL projects
现在DECLSPEC意味着“这将被导出”或“这将被导入”……这取决于我们是在编译库还是在使用它。刚刚好。
- 在库中的每个
.cpp文件中,写入这个#define:
#define IM_COMPILING_MY_CARD_LIBRARY_RIGHT_NOW
这就是它如何知道DECLSPEC应该是导出版本。
这必须在与您的项目相关的任何.h文件之前完成,这样他们就可以看到它,但是如果我们使用的是#include " pch.h "之后,因为它总是在前面。
-
将
DECLSPEC放在从.cpp文件导出的所有内容之前。 -
…以及在
.h文件中相应的函数声明之前。它们必须匹配。 -
根据需要,包括步骤 1 中的
.h文件,以定义整个过程中的DECLSPEC。我把它放在cards.h。
库文件将看起来像示例 24-3 和 24-4 。示例 24-5 展示了如何在montana.h中使用Cards成员;在montana.cpp中,我只是说了using namespace Cards;,没有做其他改动。
// class Montana, for a game of Montana solitaire
// -- from _C++20 for Lazy Programmers_
#include "gridLoc.h"
#include "cell.h"
#include "deck.h"
#ifndef MONTANA_H
#define MONTANA_H
class Montana
{
public:
static constexpr int ROWS = 4, CLMS = 13;
static constexpr int NUM_EMPTY_CELLS = 4;// 4 empty cells in grid
...
private:
...
// dealing and redealing
void deal (Cards::Deck& deck, Cards::Waste& waste);
void cleanup (Cards::Deck& deck, Cards::Waste& waste);
...
// data members
Cards::Cell grid_ [ROWS][CLMS]; // where the cards are
GridLoc emptyCells_ [NUM_EMPTY_CELLS];// where the empty cells are
};
#endif //MONTANA_H
Example 24-5Parts of montana.h, showing use of namespace Cards
// Card class
// -- from _C++20 for Lazy Programmers_
#include "pch.h
"
#define IM_COMPILING_MY_CARD_LIBRARY_RIGHT_NOW
// see cardsSetup.h. Must come before card
// related includes, after "pch.h" if any
#include "card.h"
using namespace std;
namespace
Cards
{
DECLSPEC void Card::read (std::istream &in )
{
try { in >> rank_ >> suit_; }
catch (BadRankException&) // if reading rank_ throw an exception
{
in >> suit_; // consume the suit as well
throw; // and continue throwing the exception
}
}
DECLSPEC istream& operator>> (istream& in, Suit& s)
{
...
}
...
} //namespace Cards
Example 24-4Part of card.cpp, set up to make a DLL and forming a namespace
// Card class
// -- from _C++20 for Lazy Programmers__
#ifndef CARD_H
#define CARD_H
#include <iostream>
#include "cardsSetup.h" // defines DECLSPEC
namespace
Cards
{
// Rank and Suit: integral parts of Card
// I make these global so that I don't have to forget
// "Card::" over and over when I use them.
enum class Rank { ACE=1, JACK=11, QUEEN, KING }; // Card rank
enum class Suit { HEARTS, DIAMONDS, CLUBS, SPADES }; // Card suit
enum class Color { BLACK, RED }; // Card color
inline
Color toColor(Suit s) // DECLSPEC isn't needed for inlines
{
if (s == HEARTS || s == DIAMONDS) return RED; else return BLACK;
}
// I/O on Rank and Suit
DECLSPEC std::ostream& operator<< (std::ostream& out, Rank r);
DECLSPEC std::ostream& operator<< (std::ostream& out, Suit s);
DECLSPEC std::istream& operator>> (std::istream& in, Rank& r);
DECLSPEC std::istream& operator>> (std::istream& in, Suit& s);
...
class Card
{
public:
Card (Rank r = Rank(0), Suit s = Suit(0)) :
rank_ (r), suit_ (s)
{
}
Card (const Card& other) : Card(other.rank_, other.suit_){}
...
DECLSPEC void read (std::istream &in );
private:
Suit suit_;
Rank rank_;
};
...
} //namespace Cards
#endif //CARD_H
Example 24-3Parts of card.h, set up to make a DLL, and forming a namespace
如果一切顺利,在使用你的库的程序中还有一件事需要设置:它需要在运行时找到 DLL。
最简单的方法是将 DLL 复制到项目文件夹中。或者将其放在系统路径中(这可能需要管理员访问)。
如果这不是你想要的,转到项目属性(图 24-5 ,并设置配置属性➤调试➤环境。它需要对 PATH 变量进行更新,不要忘记旧路径……如果 DLL 的位置是..\cardLibDLL\Debug,可以给它PATH=..\cardLibDLL\Debug\;%PATH%。
图 24-5
在 Visual Studio 中设置路径
防错法
-
您对项目属性进行了更改,但这些更改没有影响。很容易忽略项目属性窗口的第一行(图 24-5 )。有时你纠正了一个配置,但你用的是另一个。我更喜欢尽可能编辑所有配置/所有平台。
-
编译器抱怨像
cout**这样常见的东西不存在。**将#include "pch.h"放在之前,其他包含或停止使用预编译头;请参见本节的开头。 -
在运行时,程序找不到 DLL 。但是你设置了路径变量。
如果不是路径中的错别字,可能是你把
.user文件抹掉了。这是包含环境信息的内容。重建它应该能解决问题。 -
**运行时,程序不能启动;错误信息不清楚。**也许你的程序平台(Win32 或 x86 对 x64)与 DLL 不匹配。
Exercises
-
将
PriorityQueue、List、Time、Fraction或您在之前练习中创建的其他类放入它自己的库中,并将它链接到使用它的某个对象。 -
创建一个名为
shapes(在名称空间Shapes中)的库,使用第二十一章中的Shape层级,随意扩展;并将它链接到一个使用Shape的程序中。在basicSSDLProject的 vcxproj。
\表示“在下一行继续”
2
Microsoft Visual Studio 和 g++ 都有这种帮助编译时间的方法。这个想法是,不需要为包含它的每个源文件重新编译一个.h文件,您可以通过编译一次来减少编译时间。我没有感觉到需要,但是随着 STL 和最近的语言变化,头文件看起来确实在不断增长…
二十五、秘籍(推荐)
您现在已经掌握了 C++ 的基本知识以及更多。是时候添加铃铛和口哨了。 1
在做这一章和接下来的两章之前,回过头来做你可能跳过的“可选”部分是有意义的。我们尤其需要文件 I/O(来自第十三章)和移动语义(第 18 和 21 章)。
这些章节是这样组织的:
-
第二十五章:新功能——更好的输出格式,使用命令行参数(最后使用
int main (int argc, char** argv)中的argc和argv),以及位操作 -
第二十六章:对组织和安全有用的东西
-
第二十七章:更多的组织帮助,在不太常见的情况下有用
我们开始吧。
sstream:像cin / cout一样使用strings
假设您想要将游戏的详细信息打印到您的平视显示器上,居中显示在顶部:
Points: 32000 / Time left: 30.2 / Mood: Annoyed
您可以计算每个标签和每个值的宽度(使用可变宽度的字体,祝您好运),并从中计算打印每个项目的位置……如果您这样做了,现在就交出您的懒惰程序员徽章。
或者您可以进行大量的转换和字符串连接,并将其发送给SSDL_RenderTextCentered,如下所示。再一次,交出徽章。
string finalString = string ("Points: ")
+ to_string (points) // to_string is in #include <string>
+ "/ Time left: "
+ to_string (time)
+ " / Mood: " + mood;
SSDL_RenderTextCentered(finalString, SSDL_GetWindowWidth()/2, 10);
如果我们发送其他类型的变量,比如Point2D,我们将需要更多的字符串转换函数。那是许多工作。
输入stringstream。这就像是把cin、cout和string合二为一。您可以向它写入数据,然后提取产生的字符串,或者将一个字符串放入其中并从中读取。
要使用<<构建字符串,请执行以下操作:
-
#include <``sstream -
声明一个
stringstream。 -
使用
<<打印到stringstream。 -
使用
str()成员函数访问您构建的字符串。
如果你想再次使用它,你可以将它的内容重置为"",就像在myStringStream.str("")中一样。 2
示例 25-1 使用stringstream向SSDL_RenderTextCentered发送文本。输出如图 25-1 所示。
图 25-1
stringstream程序的输出
表 25-1
常用stringstream功能,简化
// Program that uses stringstream to center multiple things on one line
// -- from _C++20 for Lazy Programmers_
#include <sstream> // Step #1: #include <sstream>
#include "SSDL.h"
using namespace std;
int main(int argc, char** argv)
{
int points = 3200; // Some arbitrary data to test
double time = 30.2; // printing to HUD
char mood = "Annoyed";
stringstream out; // #2: Declare a stringstream
// #3: Print to stringstream
out << "Points: " << points << " / Time left: "
<< time << " / Mood: " << mood;
string result = out.str(); // #4: Access with str()
SSDL_RenderTextCentered(result.c_str(),
SSDL_GetWindowWidth()/2, 10);
SSDL_WaitKey(); // Wait for user to hit a key
return 0;
}
Example 25-1A rudimentary heads-up display (HUD) using stringstream
您也可以使用stringstream作为输入源——使用str设置字符串,然后使用>>从中提取:
dataLine.str ("Flourine 0.52 0.63");
dataLine >> elementName >> firstNumber >> secondNumber;
使用stringstream输入包括以下步骤:
-
#include <``sstream -
声明一个
stringstream。 -
用
str()成员函数初始化stringstream。 -
使用
>>从stringstream中读取。
如果您有可能耗尽输入,您可以调用clear(),就像在dataLine.clear()中一样,在重用之前清除错误条件。
Exercises
-
假设我们有一组文件:
file1.txt, file2.txt…file100.txt。使用stringstream为第 x 个文件构建文件名。 -
编写并测试一个函数模板,该模板接受一个变量,将其打印成一个字符串,然后返回该字符串。
-
用包含单词和数字的文本初始化一个字符数组,然后使用
stringstream将它的各个部分适当地读入变量。 -
读入一个数字文件,忽略注释标记
#之后的所有内容。方法如下:排成一行阅读;在你找到任何#后丢弃任何东西;然后从剩下的数字中读入所有数字,并将它们推入一个向量中。
带有格式字符串的格式化输出
假设我想按列打印表格。我如何把事情安排好?自己数空格?不可能!简单的方法是将它们放入一个格式的字符串:
cout << format("{0:10} -- {1:10}.\n", "Column 1", "Column 2");
cout << format("{0:10} -- {1:10}.\n", "col1 data", "col2 data");
...
第一行说取第 0 个参数("Column 1")和第 1 个参数("Column 2"),把它们放在字符串中,中间有--,然后把结果发送给cout。每个参数占位符中的:10表示其参数至少要占用十个字符;format会用空格填充每一个来实现这一点。如果我们打印的内容超过十个字符,它会根据需要占用尽可能多的空间。
由于第二行以同样的方式格式化它的参数,代码产生了均匀排列的列(见图 25-2 (a))。
图 25-2
format命令的输出。(a)两个十字符栏;(b)两列以定点格式显示浮点数,精确到两位数;(c)三列,左对齐、中对齐和右对齐
我通常省略参数的编号;它将按顺序执行这些操作:
cout << format("{:10} -- {:10}.\n", "Column 1", "Column 2");
如果要打印的东西是浮点数,我可能会添加一个f来指定我想要定点,而不是科学记数法(默认是“让format决定”)。我也可以指定小数点右边显示多少位数。在这个片段中
cout << format("{:6.2f}{:8.2f}", 21.0, 22.5) << "\n";
我希望第一个数字占用六个空格,精度两位数;第二个占用八个空格,也是两位数的精度。输出如图 25-2 (b)所示。
默认情况下,字符串在它们的列中是左对齐的,如图 25-2 (a)所示,但是数字默认情况下是右对齐的,如(b)所示。我可以通过要求左(<)、中(^)或右(>)对齐来覆盖默认值。所以cout << format("{:<7} {:⁷} {:>7}\n", "left", "center", "right");会打印出你在图 25-2 (c)中看到的内容。
示例 25-2 使用这些工具打印几个熟悉星球的气候条件。
// Program to print temp, pressure for some familiar planets
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <format>
3
using namespace std;
int main()
{
// planetary temperature and pressure
constexpr double VENUS_TEMP = 464; // celsius
constexpr double EARTH_TEMP = 15;
constexpr double MARS_TEMP = -62;
constexpr double VENUS_PRESSURE = 92000; // millibars
constexpr double EARTH_PRESSURE = 1000;
constexpr double MARS_PRESSURE = 1;
// Print a 3-column table
// Left column is 7 chars wide;
// char* is left-justified by default
// Other columns are 11 chars wide, right-justified
// Headers
cout << format("{:7} {:>11} {:>11}\n",
"Planet", "Temperature", "Pressure");
cout << format("{:7} {:>11} {:>11}\n\n",
"", "(celsius)", "(millibars)");
// Data
cout << format("{:7} {:11.1f} {:11.0}\n",
"Venus", VENUS_TEMP, VENUS_PRESSURE);
cout << format("{:7} {:11.1f} {:11.0}\n",
"Earth", EARTH_TEMP, EARTH_PRESSURE);
cout << format("{:7} {:11.1f} {:11.0}\n",
"Mars", MARS_TEMP, MARS_PRESSURE);
cout << "\n...I think I'll just stay home.\n\n";
return 0;
}
Example 25-2Program to neatly print a table of astronomical data using format strings
以下是示例 25-2 的输出:
Planet Temperature Pressure
(celsius) (millibars)
Venus 464.0 90000
Earth 15.0 1000
Mars -62.0 1
I think I'll just stay home.
示例 25-3 展示了如何用format做一些更酷的事情。想打印不同格式不同基数或浮点的int s?见下文。想打印到一个string,就像上一节一样?如图所示使用format_to。
// Program to illustrate further capabilities of C++20's format strings
// -- from _C++ for Lazy Programmers_
#include <iostream>
#include <string>
#include <format>
using namespace std;
int main()
{
// Print the same integer using base 2, 8, 16, and 10
cout << "Here's 15 written in...\n";
cout << format("{0:>8}{1:>8}{2:>8}{3:>8}\n",
"binary", "octal", "hex
", "decimal");
//(if you don't specify, you get decimal)
cout << format("{0:>8b}{0:>8o}{0:>8x}{0:>8}\n\n", 15);
// We used argument #0 four times; no law against that...
// Print the same item with different types of padding
cout << "Here's 15 padded with x's, .'s, and *'s: ";
cout << format
("{0:x>4} {0:.>4} {0:*>4}\n\n", 15);
// Print a floating point number with different formats
cout << "And here's 0.01234 in scientific, fixed, general, ";
cout << "and default format,\n";
cout << "showing how they interpret a precision of 2.\n";
cout << format("{0:>10.2e} {0:>10.2f} {0:>10.2g} {0:>10.2}\n\n",
0.01234);
// You can also print to a string with format_to:
string str;
format_to
(back_inserter(str), "The language of choice is {}.\n",
"C++");
cout << str;
return 0;
}
Example 25-3More format string tricks
输出是
Here's 15 written in...
binary octal hex decimal
1111 17 F 15
Here's 15 padded with x's, .'s, and *'s: xx15 ..15 **15
And here's 0.01234 in scientific, fixed, general, and default format,
showing how they interpret a precision of 2.
1.23e-02 0.01 0.012 0.012
The language of choice is C++.
一个论点的规范必须按照特定的顺序来完成。除了开头和结尾{},可以跳过任何或所有内容。顺序是
-
打开{
-
参数编号
-
:(如果有任何格式规范,则需要)
-
对准:
<、^或>。 -
宽度
-
精度:小数点后跟随所需精度的位数
-
浮点类型:
f、g或e;整数的b、o、x或d(二进制、八进制、十六进制、十进制) -
关闭}
这是我常用的,但还有更多。参见cppreference.com了解format能做的所有事情的完整列表。
Online Extra: I/O Manipulators
关于这在 C++20 之前是如何完成的细节,如果你在处理遗留代码的话很有用, 4 在 github.com/apress/cpp20-for-lazy-programmers 。
防错法
格式字符串的错误通常在运行时被检测到,因为直到它尝试,C++ 才知道字符串-参数组合是否有效。你会得到一大堆无法理解的错误信息。只要回到调用format的地方,使用调试器中的调用栈,看看哪个format有问题。
Exercises
-
使用
format打印某人可能填写的表格——可能是一份申请(填写姓名、地址等)。),可能是更有趣的东西。你决定。 -
打印一些二进制、八进制、十六进制、十进制的加法题,像这样:
1111 17 f 15 + 1 + 1 + 1 + 1 ----- ----- ----- ----- 10000 20 10 16 -
打印一个表格,显示你每年摔倒的次数,从 2 岁开始,到你现在的年龄结束。我希望它有所下降。弄干净点。
-
打印一份冲浪用品店各种物品的价格表:冲浪板、冲浪板包、滑冰鞋或其他任何东西。美元金额中的
.应该对齐。 -
打印练习 4 中物品重量的表格。
-
使用科学记数法,打印出在任何给定的一年中,这些事件发生的概率:有一个重大的政治丑闻;生命在火星上自发形成;一颗彗星撞击了尤卡坦半岛,让我们重蹈恐龙的覆辙;比用 C++ 编程更有趣的事情发生了(当然是最低概率)。使用科学符号。编造数字——其他人都是这么做的。
命令行参数
有时有必要,尤其是在 Unix 世界中,给程序提供参数:cd myFolder、gdb myProgram等等。
假设你想让一个程序检查文本文件的差异。该命令可能类似于
./mydiff file1.txt file2.txt #in Windows, leave off the ./
你需要这样写main的第一行:
int main (int argc
, char** argv)5
argc(“参数计数”)是参数的个数,argv(“参数向量”,但它是一个数组,而不是vector)是一个字符数组的数组,每个数组包含一个参数,以程序名开始。
所以如果你的命令是./mydiff file1.txt file2.txt,argc将是 3,argv将包含如图 25-3 所示的值。
图 25-3
可能的命令行参数
示例 25-4 显示了程序的代码。
首先,它确保我们有正确数量的参数。如果出了问题,通常会告诉用户预期会发生什么。argv[0]始终是程序名。(我们不硬编码为“./mydiff”,以防程序名改变。)
cerr类似于cout,但是没有被>重定向,所以它对错误消息很有用。但是cout也可以。
// Program to find the difference between two files
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <fstream>
#include <cstdlib> // for EXIT_FAILURE, EXIT_SUCCESS
#include <string>
using namespace std;
int main (int argc, char** argv)
{
// Did we get right # of arguments? If not, complain and quit
if (argc != 3) // 3 args: 1 program name, plus 2 files
{ // On failure, tell user what user should've entered:
cerr << "Usage: " << argv[0] << " <file 1> <file 2>\n";
return EXIT_FAILURE
;
}
// Load the 2 files
ifstream file1(argv[1]), file2(argv[2]); // open files
if (! file1) // On failure, say which file wouldn't load
{
cerr << "Error loading " << argv[1] << endl;
return EXIT_FAILURE;
}
if (!file2) // On failure, say which file wouldn't load
{
cerr << "Error loading " << argv[2] << endl;
return EXIT_FAILURE;
}
string line1, line2;
while (file1 && file2) // While BOTH files are not // finished
{
getline (file1, line1); // read line from file1
if (!file1) break;
getline (file2, line2); // ...from file2
if (!file2) // if file2's done but file 1 // wasn't
{
cout << "<: " << line1 << endl; // spit out last line read from// file 1
break;
}
if (line1 != line2) // if lines differ print them
{
cout << "<: " << line1 << endl; // < means "first file"
cout << ">: " << line2 << endl; // > means "second file"
// this is conventional
}
}
// If either file has more lines than the other, print remainder
while (file1)
{
getline(file1, line1);
if (file1) cout << "<: " << line1 << endl;
}
while (file2)
{
getline(file2, line2);
if (file2) cout << ">: " << line2 << endl;
}
// Clean up and return
file1.close(); file2.close();
return EXIT_SUCCESS;
}
Example 25-4A program using command-line arguments. In source code, the executable for g++ is named mydiff. Instructions on how to run and debug it follow the example
要从命令行运行它
在 Unix 中,键入./mydiff file1.txt file2.txt。
对于 MinGW,键入mydiff file1.txt file2.txt。
Visual Studio 用户应该首先将可执行文件从Debug/、Release/或x64/中的某个地方复制到项目文件夹中,这样它就可以找到文本文件。它的名字是4-cmdLineArgs.exe,所以打4-cmdLineArgs file1.txt file2.txt。或者将其重命名为mydiff并使用该命令。
有关在 Windows 中使用命令提示符的提示,请参阅第十三章的“在 Windows 中设置命令提示符”小节
在 Unix 中使用命令行参数进行调试
无论是在ddd还是gdb,在提示符下,输入set args file1.txt file2.txt,你就准备好run了。
在 Visual Studio 中使用命令行参数进行调试
如果在 Visual Studio 中启动程序,它会像没有参数一样运行,但实际上它没有。要解决这个问题,进入项目菜单➤属性➤配置属性➤调试,设置配置为所有配置和平台为所有平台,并添加您的参数到命令参数(图 25-4 )。
图 25-4
在 Microsoft Visual Studio 中设置命令参数
项目的命令行参数存储在.user文件中。如果您删除它,您将不得不再次添加它们。
Exercises
-
写一个程序
myGrep,一个 Unixgrep实用程序的简化版本。它应该重复标准输入中包含给定单词的所有行。所以如果你有一个带线条的文件input.txtalphabetaalphabet然后命令
myGrep(或./myGrep)alpha < input.txt应该在屏幕上打印出来:alphaalphabet -
在前面的练习中,添加一个选项
-n,如果存在,它会指示grep打印每行输出的行号。(以-开头的选项在 Unix 命令中很常见。)给定练习 1 的输入,命令myGrep -n alpha < input.txt应该打印出来1: alpha3: alphabet -
编写一个程序,在给定输入和列中的数字的情况下,将只包含指定列的版本打印到标准输出。例如,如果你给它参数
0 3并得到这样的输入1900 -0.06 -0.05 -0.05 -0.08 -0.07 -0.071901 -0.07 -0.21 -0.14 -0.06 -0.2 -0.13...输出应该只显示第 0 和第 3 列:
1900 -0.051901 -0.14...我推荐用
stringstream,format也没坏处。
位操作:&、|、~和<</>>
许多库,比如 SDL 和它的助手,要求你用单独的位 6 设置它们的一些特性,并以同样的方式报告特性。嵌入式系统、加密和文件压缩也受益于对单个位的操作。
要启动 SDL 图像库,调用IMG_Init,它接受一个类型为int的参数,告诉它支持什么图像格式。我们如何把它塞进一个int?SDL_Image.h提供标志(具有指定含义的位):IMG_INIT_JPG为 1,IMG_INIT_PNG为 2,IMG_INIT_TIF为 4,以此类推。我们必须一点一点地将它们组合成一个int,没有双关的意思(见图 25-5 )。
图 25-5
发送到IMG_Init的int是如何布局的:从右向左读,我们有位 1、位 2、位 4 等等。这个设置为支持 jpg 和 tiffs(位 1 和位 4)
有一些运算符——“按位”运算符——可以帮助我们处理位:
图 25-6
逐位算术
- 按位或,如
x|y:如果xy中的任一位为 1,x|y中的每一位都为 1(图 25-6 (a))。
** 按位与,如x & y:如果xy中的一位都为 1,那么x&y中的一位将为 1(图 25-6 (b))。
** 按位非,如在~ x中:所有的位都反转(图 25-6 (c))。
* 我们可以使用移位操作符`<<`和`>>`向左或向右移动位(现在为流 I/O 和位操作执行双重任务)。`x << 2`是否将`x`中的所有位左移两位(图 25-6 (d))。*
*(还有 xor,x^y,这里不介绍,因为它不常用。)
现在我们可以看到如何构造发送给IMG_Init的int。int flags = IMG_INIT_JPG |赋予我们IMG_INIT_TIF
IMG_INIT_JPG : 000000001
IMG_INIT_TIF : 000000100
flags : 000000101
我们这样传递它:IMG_Init (flags);。
为了更好地发挥这一点,我举了一个超级简化烤箱的例子(例子 25-5 , 25-6 )。你可以将它设置为烘焙和/或烧烤(每个 1 比特)。我想更好地控制上面的两个燃烧器,所以我让最右边的两位控制右边的燃烧器,接下来左边的两位控制左边的燃烧器。它们可以取值为 00 表示关,01 表示低,10 表示中,11 表示高。
我还有一个“火”的条件:如果两个燃烧器都在高温,烤箱设置为烘烤和烤。
// Program that controls a Super-Simple Demo Oven (SSDO) with flags
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <cassert>
class SSDO // A Super-Simple Demo Oven
{
public:
static constexpr int
RIGHT = 0b00000011, // This is how to write in binary in C++:
LEFT = 0b00001100, // precede with 0b or 0B
BAKE = 0b00010000,
BROIL = 0b00100000;7
// Leftmost two bits are unused
static constexpr int
OFF = 0b00,
LO = 0b01,
MD = 0b10,
HI = 0b11;
// Right burner bits are at the right -- no offset
static constexpr int RIGHT_BURNER_OFFSET = 0;
// But the left burners' are offset two to the left
static constexpr int LEFT_BURNER_OFFSET = 2;
static constexpr int FIRE = 0b00111111;
Example 25-5Program that uses bit manipulation to set and use flags, part 1
为了打开烘焙或烧烤,我对一个成员变量flags_使用了按位“或”|(见图 25-7 (a)): flags_ = flags_ | BAKE;。
图 25-7
在超级简单的烤箱中打开和关闭BAKE钻头
要关闭它,我需要保持所有其他位不变,但将其设置为 0。这样做:得到一个所有位都是 1 的数,除了BAKE;“和”与flags_(图 25-7 (b)):
flags_ = flags_ & ~BAKE;
要设置燃烧器,我必须将该燃烧器的钻头更换为所需的Condition–OFF、LO、MD或HI–移动到燃烧器的正确位置。为了将左燃烧器设置在HI,比方说,我使用~和&清除左燃烧器位,就像我对BAKE所做的那样(图 25-8 (a))。然后我取HI,0b11,用<<左移两位得到0b1100。我用按位或把它们放在一起。
图 25-8
打开LEFT燃烧器;检查FIRE状态
我通过说(flags() & FIRE) == FIRE来确定烤箱是否着火——也就是说,是否所有的东西都开着,燃烧器是否在高处。如果我只说flags() == FIRE,可能不行,因为我不知道最左边那两个没用的位是什么(图 25-8 (b))。防御性编程。
// We're still in class SSDO's public section...
// ctors and =
SSDO () { flags_ = '\0'; }
SSDO (const SSDO&) = delete;
SSDO& operator= (const SSDO&) = delete;
// the controls
void setBake() { flags_ |= BAKE; }
void clearBake() { flags_ &= ~BAKE; }
void setBroil() { flags_ |= BROIL; }
void clearBroil() { flags_ &= ~BROIL; }
void setLeftBurner (unsigned char c)
{
flags_ &= ~LEFT;
flags_ |= (c<< LEFT_BURNER_OFFSET);
}
void setRightBurner (unsigned char c)
{
flags_ &= ~RIGHT;
flags_ |= (c << RIGHT_BURNER_OFFSET);
}
void clearLeftBurner () { setLeftBurner (OFF);}
void clearRightBurner() { setRightBurner(OFF);}
// access functions
unsigned char flags () const { return flags_; }
bool isSelfCleaning () const // bake and broil -> self-cleaning mode
{
return (flags() & BAKE) && (flags() & BROIL);
}
bool isFireHazard () const // they're all on, high!
{
return (flags() & FIRE) == FIRE;
}
private:
unsigned char flags_; // unsigned char has at least 8 bits --
// that's plenty for us here
};
using namespace std;
int main ()
{
SSDO myOven;
// Turning the oven completely on; now it's in self-cleaning mode
myOven.setBake ();
myOven.setBroil();
assert(myOven.isSelfCleaning());
assert(myOven.flags() == 0b00110000);
// Playing with the right burner, checking the result...
myOven.setRightBurner (SSDO::LO);
myOven.clearRightBurner ();
assert ((myOven.flags() & SSDO::RIGHT) == 0);
// I probably shouldn't do this...
myOven.setRightBurner (SSDO::HI);
myOven.setLeftBurner (SSDO::HI);
if (myOven.isFireHazard())
cout << "Cut the power and call the fire department!\n";
return 0;
}
Example 25-6Program that uses bit manipulation to set and use flags, part 2
输出为Cut the power and call the fire department!。所有断言都成功。
在最后的assert,我需要在==之前的()。如果没有它们,它会将表达式解析为myOven.flags() & (SSDO::RIGHT == 0)。这既奇怪又没用。
现在我们已经讨论了位操作,我们应该能够设置标志,将信息发送给使用它们的库,并获得这样的信息,或者在我们自己的库中使用它们。
如果 C++20 兼容,你的编译器还提供了一个头文件<bit>,带有rotr(“向右旋转”)——像>>,除了最右边的位被复制到最左边:
unsigned char c = 0b00000001; //1 is in the rightmost bit
c = std::rotr (c, 1); //now it's in the leftmost
再加上rotl(“向左旋转”)和其他我没怎么用的功能。在写作的时候,你可以在 en.cppreference.com/w/cpp/numeric 找到一个列表。
防错法
- 你得到了一个比特操纵表达式的错误答案,但是你没有看到如何。也许你用
&&代表&或者用||代表|。我做那件事。或者也许你需要一些()。
Exercises
-
写一个函数,通过检查一个数的某一位来确定这个数是奇数还是偶数。
-
编写一个函数,通过打印每个单独的位来打印一个二进制数。你可能想要
sizeof。 -
写一个函数,找到一个
int的日志 2 ,用>>,不用/。 -
写一个函数来判断一个数中的位序列是否对称(像 11000011 但不是 11010011)。
有额外的东西真好。
2
stringstream也作为clear的功能,但不要被欺骗;它清除错误标志,而不是内容。
3
您的编译器可能还没有提供这个 C++20 特性,但是没问题。您可以使用源代码中包含的{fmt}库。参见源代码(ch25/2-格式和 ch25/3-格式)中的示例 25-2 和 25-3 ,了解您在自己的顶部需要什么。cpp 文件让他们工作。它在不兼容的编译器上不能工作,只有 #include <格式> 。
您可以像往常一样复制和使用 newWork 项目 basicSSDLProject 和 basicStandardProject 他们知道在哪里可以找到库,但是你仍然需要参考 ch25/2-format 或 ch25/3-format。cpp 文件。
如果您想从头开始构建您的{ fmt }-使用项目,在 g++ 中,添加编译器选项-I../../external/fmt-master/include;在 Visual Studio 中,设置项目属性➤配置属性➤ C/C++ ➤附加包含目录以包含../../external/fmt-master/include。
4
在最新标准之前编写的代码。您可能会遇到这种情况,不仅是在您更新项目时,而且是在您在线寻找某个问题的解决方案并找到使用该语言旧功能的示例时。
5
或者 int main (int argc,char* argv[]),更清楚的说明第二个参数是字符数组的数组。唉,char**版本似乎是更常见的写法。
6
一个比特是一个二进制(以 2 为基数)数字,0 或 1,是最小的信息。电脑用它们来代表一切。一个int的值是其被解释为二进制数的位(除了最左边的,这意味着正或负)。一个float的更复杂。在这一节中,我们将把比特序列解释为比特序列。
7
通常明智的做法是给每个变量以自己的声明结尾;。为什么呢?因为,和;看起来很像,很容易混淆和出错。
但是正如从乔治·奥威尔到比雅尼·斯特劳斯特鲁普的大师们所指出的,文体规则应该服从眼前的形势。我不能催促任何人打字
static const expr int RIGHT = 0b 00000011;
static const expr int LEFT = 0b 00001100;
静态 constexpr int BAKE = 0b00010000
static const expr int broir = 0b 00100000;
在一本鼓吹懒编程的书里。
**
二十六、秘籍(推荐)·续
更多的附加功能使你的程序更安全、更快捷、更容易编写。
默认的构造器和=
早先我避免使用构造器和 operator=的默认值,因为有时 C++ 的猜测是非常错误的;具体来说,它复制数组地址,而不是数组内容。
但有时候完全正确。默认值可以节省我们写第十九章的Card类的时间(见例子 26-1 )。
class Card
{
public:
Card () : rank_(Rank(0)), suit_(Suit(0)) {}
Card (const Card& other) = default;
Card& operator= (const Card& other) = default;
...
private:
Rank rank_; Suit suit_;
};
Example 26-1Class Card, its constructors slightly altered from Chapter 19 for simplicity, and with defaulted constructor and = added
如果我们要使用 C++ 的默认值,我们最好知道它们是做什么的!是这样的:
-
在默认(无参数)构造器中,调用所有父类和所有部件的默认构造器。但是
Suit和Rank没有默认的构造器,所以默认的Card ()不会做任何事情来初始化部件。我将让第一个构造器保持原样。 -
在复制构造器中,复制父类和成员(对于类,复制构造器,对于基本类型,
=)。 -
In =,对父类和所有部分调用=。
=的使用解释了为什么这不适用于数组,但对于许多其他事情却很好。
除了前面讨论的成员函数,您还可以默认或删除、移动操作符和析构函数。
constexpr和static_assert:将工作移至编译时间
C++ 大师们现在建议你尽可能多地使用constexpr:这在运行时会更快(当然);占用内存少;它还防止了“未定义的行为”(不可预测的结果),因为带有未定义行为的东西不应该被编译。(如果这行得通,我们能对语言的其他部分也这样做吗?求你了。)
尽管名字如此,constexpr实际上意味着“在编译时这样做”,尽管它也使事情保持不变。
我们也可以使用constexpr 函数,并在编译时使用它们来生成值,前提是这些函数的输入在编译时是已知的。
假设我们正在销售一种油漆,我们想要大量的颜色。每种颜色都有自己的 RGB 值以及可以通过这些值计算出来的东西,比如亮度(我将亮度定义为 R、G 和 B 的平均值)和补色(色轮上相反的颜色)。在编译时这样做可以加快运行时间,特别是如果我们有许多这样的函数和许多颜色。示例 26-2 显示了一个struct及其相关的函数和变量。
// A struct Color and associated constants and functions
// -- from _C++20 for Lazy Programmers_
#ifndef COLOR_H
#define COLOR_H
#include "SSDL.h"
namespace Palette
{
struct Color
{
constexpr Color (double r = 0, double g = 0, double b = 0)
: red_(r), green_(g), blue_(b), brightness_((r+g+b)/3)
{
}
constexpr Color (const Color&) = default;
constexpr Color& operator= (const Color& c) = default;
constexpr Color complement() const
{
return Color (1.0 - red_, 1.0 - green_, 1.0 - blue_);
}
double red_, green_, blue_; // Each ranges 0.0-1.0\. More fine // shades than
// if we used ints 0-255
double brightness_;
};
// Function to convert a Color to an SSDL_Color
constexpr SSDL_Color color2SSDL_Color (const Color& c)
{
return SSDL_Color (int (c.red_ * 255),
int (c.green_ * 255),
int (c.blue_ * 255));
}
inline constexpr Color BLACK (0.0,0.0,0.0), RED (1.0,0.0,0.0),
GREEN (0.0,1.0,0.0), BLUE(0.0, 0.0, 1.0),
FUSCHIA (1.0,0.0,1.0);
inline constexpr
Color COLORS[] = {Color (0.80,0.53,0.60), // puce
Color (1.00,0.99,0.82), // cream
Color (0.94,0.92,0.84)};// eggshell
}
#endif //COLOR_H
Example 26-2struct Color, using defaulted ctors and constexpr out the wazoo
由于Color的构造器是constexpr,我可以把constexpr Color s 做成类似BLACK、RED等等,或者把它们做成类似COLORS的constexpr数组。函数可以是构造器、其他成员、虚函数、非成员等等,只要这些函数在编译时足够简单。计算是可以的,文字值和其他constexpr,但没有什么是直到运行时才知道的,也没有对函数的调用,无论是否内置,它们本身都不是constexpr。所以在他们修好之前,没有sin、sqrt或strcpy。 1
例 26-2 也有constexpr功能color2SSDL_Color。它调用SSDL_Color的构造器。那能行吗?是的,因为SSDL_Color的构造者也是constexpr。
因为一个constexpr函数必须能够在编译时完成它的工作,编译器必须知道,一旦它找到对它的调用,它是如何做的——就像内联函数一样。它需要放在.h文件中,并且是隐式的inline。
它是否真的在编译时工作取决于它必须处理什么。如果我们给它constexpr s 和/或文字,它可以返回一个constexpr值:
constexpr Color CREAM = COLORS[1];
constexpr
SSDL_Color SSDL_CREAM = color2SSDL_Color(CREAM); // done at compile time
但是如果我们给它一些运行时才存在的东西,比如这里的PUCE,它就不能:
const Color PUCE = COLORS[0];
SSDL_Color SSDL_PUCE = color2SSDL_Color(PUCE);
// color2SSDL_Color is called at runtime because PUCE isn't constexpr
我们也有一个编译期版本的assert用于constexpr s。不像assert,它不需要任何包含文件。
constexpr Color ONYX = CREAM.complement(); // CREAM's opposite
static_assert (ONYX. brightness_ < 0.10); // onyx isn't bright
static_assert (CREAM.brightness_ > 0.90, // but cream is
"Isn't cream supposed to be bright?");
在编译时完成它的工作。如果你愿意,你可以给它一些东西打印,如果它失败了,如图所示。有了 Visual Studio,你甚至不需要编译——在static_assert上挥动你的鼠标指针,它会告诉你是否有问题。很好!
Extra
从我在网上看到的情况来看,有计划要在未来的标准中大大扩展编译时可以做的事情。这里有两个进一步推进编译时间的特性 edge,所以你的编译器可能还不喜欢它们。
constinit Color favoriteColor = RED;
这不是一个常量,而是在编译时由某个常量初始化的。
为什么要这样做?假设在不同的.cpp文件中有两个全局对象,其中一个依赖于另一个的初始值。如果它们以正确的顺序被初始化,那绝对是好运气,因为 C++ 没有指定哪个.cpp文件的“静态”变量(全局变量和其他一些变量,包括static类成员)首先被初始化。constinit通过拒绝编译时未知的=的任何权利来避免这种“静态初始化顺序的惨败”。
如果你想在一个函数中声明一个constinit,在前面加上static关键字。
还有“即时功能”:
consteval int someFunction (...args...) {...}
这就像一个constexpr函数,除了它不灵活:它必须在编译时执行。
Exercises
-
constexpr能在Fraction的都在,再算一些Fraction(用+、*什么的),全部constexpr。尽可能使用static_assert来验证您的功能。 -
constexpr你能在Point2D中的一切,并声明一些constexpr Point2D在constexpr表达式中使用。尽可能使用static_assert来验证你的功能。
结构化绑定和tuple s:一次返回多个值
在第七章,我可能给人的印象是一个函数只能返回一个东西。如果是的话…我撒谎了。
我们已经可以返回一个vector或list…但是这里有一种方法可以返回多个东西,而不会产生额外的开销。示例 26-3 展示了它的样子。
-
#包含<元组> **。**元组是值的序列,可能是不同的类型;这是我们要回报的。
-
让功能返回 自动。它实际上是返回
std::tuple<firstType,secondType,...>,但是为什么不让编译器来计算呢? -
用 Return STD::make _ tuple(value 1,value2,...);。
-
将返回值存储在“结构化绑定”中:
auto [variable1, variable2, ...] = functionCall (...);
这将声明variable1、variable2等等,并从函数返回的内容中初始化它们。
// Program to calculate the quadratic formula
// using structured bindings and tuples
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <cmath> // for sqrt
#include <tuple> // for tuple stuff // Step #1: #include <tuple>
#include <cassert>
using namespace std;
// If auto's going to work in main, we need the
// function body *above* main. Else there's no way main
// can know what type is to be returned
// #2: return auto
auto quadraticFormula (double a, double b, double c)
{
int numroots = 0;
double root1 = 0.0;
double root2 = 0.0;
double underTheRadical = b * b - 4 * a*c;
if (underTheRadical >= 0) // If we have to sqrt a neg #,
// no solution. Otherwise...
{
root1 = (-b + sqrt (underTheRadical)) / (2 * a);
root2 = (-b - sqrt (underTheRadical)) / (2 * a);
if (root1 == root2) numroots = 1; else numroots = 2;
}
// #3: return a tuple
return std::make_tuple (numroots, root1, root2);
}
int main ()
{
// Get user input
cout << "Enter the a, b, c from ax²+bx+c = 0: ";
double a, b, c; cin >> a >> b >> c;
// Get the results
// #4: store result in auto[...]
auto[howMany, r1, r2] = quadraticFormula (a, b, c);
// Print the results
switch (howMany)
{
case 0: cout << "No solution.\n"; break;
case 1: cout << "Solution is " <<r1<<endl; break;
case 2: cout << "Solutions are " <<r1<<' '<<r2<<endl; break;
default:cout << "Can't have " <<howMany<<" solutions!\n";
}
return 0;
}
Example 26-3Using structured bindings to get multiple values through a return statement
这种新的能力,加上我们之前所知道的,导致了第八章中关于这个主题的黄金法则的更新版本。
Golden Rule of Function Parameters and
return (新版)
-
如果函数提供了一个值...
-
如果很小,就退掉。对于倍数,返回一个元组。
-
如果没有,就路过
&。
-
-
如果它接受一个变量并改变它,则通过
&。 -
如果它接受它而不改变它,
-
如果是一个对象,作为
const TheClass&对象传递。 -
否则按值传递(否
&)。
-
-
如果是移动
=,则经过&&。…除了数组作为参数传入之外,根据内容是否改变,有或没有
const。
你也可以在其他地方使用元组,有点像pair,只是元素数量不同。为了得到零件,无论是改变它们还是使用它们,你可以使用std::get <>()。将您想要的元素放在<>和()之间的元组之间:
std::tuple<int, double, double> myTuple = std::make_tuple (0, 2.0, 3.0);
assert(std::get<0> (myTuple) == 0); // check the 0th value
std::get<0> (myTuple) = 1; // set the 0th value
如果能节省你的时间就好。但是它甚至没有那个有auto [...]的东西一半酷。
Exercises
在每个练习中,让main使用auto [...]来存储返回值:
-
编写一个函数
sortedTriple,它接受一个由三个元素组成的元组,将它们按顺序排列,然后返回新的版本。 -
从第十八章写一个版本的
factorial,不仅返回n!,还返回n。 -
(更难)写一个函数,给定一个
vector,返回最大值、最小值、平均值和标准差,所有这些都在一个元组中。标准差有时定义为。
现在为一个
list写一个做同样事情的。泛型编程,是的。
智能指针
C++ 最近更新背后的一个动机是防止指针错误破坏我们的代码。祝你好运!但是还是有进步的。
unique_ptr
主要的主力是std::unique_ptr。它维护一个指针,让您使用它,并在它超出范围时自动删除它。是的,你可以打破它,但是你必须试一试。它通常用make_unique初始化,它带参数来初始化你所指向的任何东西:
#include <memory
>
...
std::unique_ptr<int > p1 = std::make_unique<int>(27);
// new int ptr, value 27
std::unique_ptr<Date> pDate = std::make_unique<Date>(1, 1, 2000);
// Put the arguments for Date's constructor in
// and make_unique will take care of it
或者它取你想要创建的数组的大小:
std::unique_ptr<char[]> myChars = std::make_unique<char[]>(100);
之后,像平常使用指针一样使用它:
*p1 = 2; cout << *p1;
pDate->print(cout);
myChars[5] = 'A';
你可以用get()拿到里面的指针。当传递给一个函数:strcpy (myChars.get(), "totally unique");时,您可能需要它。
不需要记得清理;它会自动删除。而且对于谁做delete也没有混淆,因为unique_ptr不共享内存(因此有了单词“unique”)。
你能不能告诉它马上删除:
myChars.reset(); // the memory is deleted --
// myChars now thinks it's nullptr
或许可以重置成你想要的其他东西:
myChars.reset (new char [100]);2 // takes ownership of the new memory –-
// is responsible for deleting later
为什么要这么做?
-
错误预防:这样你就不会忘记初始化或删除,也不会忘记使用已经被删除的指针——它会自动设置为
nullptr,所以你不能这样做。 -
异常安全:当你离开一个函数时,它调用所有局部变量的析构函数。原始指针——我们一直在使用的那种——没有析构函数,所以它们的内存不会被抛出,但是
unique_ptrs 会把它们的内存放回它们的析构函数中。因此,如果抛出异常,使用unique_ptr可以防止内存泄漏。
我代码中的大多数指针都在类的私有部分,清理由析构函数处理,所以我认为它们是相当安全的。但是让我们看看unique_ptr是否能为我们省去麻烦。
我将从示例 21-6 的奥林匹克标志程序开始(此处更新为示例 26-4 )。
// Program to show, and move, the Olympic symbol
// It uses Circle, and a subclass of Shape called Text
// Adapted to use unique_ptr
// -- from _C++20 for Lazy Programmers_
#include <vector>
#include <memory> // for unique_ptr
#include "circle.h"
#include "text.h"
int main (int argc, char** argv)
{
SSDL_SetWindowSize (500, 300); // make smaller window
// Create Olympic symbol
std::vector<std::unique_ptr<Shape>> olympicSymbol;
// with Circles
constexpr int RADIUS = 50;
olympicSymbol.push_back
(std::make_unique<Circle> ( 50, 50, RADIUS));
olympicSymbol.push_back
(std::make_unique<Circle> (150, 50, RADIUS));
olympicSymbol.push_back
(std::make_unique<Circle> (250, 50, RADIUS));
olympicSymbol.push_back
(std::make_unique<Circle> (100, 100, RADIUS));
olympicSymbol.push_back
(std::make_unique<Circle> (200, 100, RADIUS));
// plus a label
olympicSymbol.push_back
(std::make_unique<Text> (150,150,"Games of the Olympiad"));
// color those circles (and the label)
SSDL_Color olympicColors[] = { BLUE,
SSDL_CreateColor (0, 255, 255), // yellow
BLACK, GREEN, RED, BLACK };
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
olympicSymbol[i]->setColor (olympicColors [i]);
// do a game loop
while (SSDL_IsNextFrame ())
{
SSDL_DefaultEventHandler ();
SSDL_RenderClear (WHITE); // clear the screen
// draw all those shapes
for (const auto& i : olympicSymbol) i->draw ();
// ranged-based for-loops ftw!
// move all those shapes
for (const auto& i : olympicSymbol) i->moveBy (1, 1);
}
// No longer needed:
// for (auto i : olympicSymbol) delete i;
return 0;
}
Example 26-4The Olympic symbol program from Example 21-6, now using unique_ptr
我对我挽救的工作并不兴奋。让我们看看它在类Shape中会做什么。
Shape有一个char指针description_。在现实生活中,我只是使用了一个string,但是这将帮助我们看到当一个类必须有一个指针时unique_ptr为我们做了什么(例子 26-5 和 26-6 )。
// Shape class, for use with the SSDL library
// -- from _C++20 for Lazy Programmers_
#include "shape.h"
// ctors
Shape::Shape(int x, int y, const char* text)
: description_(copy(text))
{
location_.x_ = x; location_.y_ = y;
}
Shape::Shape (const Shape& s) :
location_ (s.location()),
color_ (s.color ()),
description_(copy (s.description_.get()))
{
}
// I no longer have to write the move ctor
// ...or the move = operator
// operator=
Shape& Shape::operator= (const Shape& s)
{
location_ = s.location();
color_ = s.color ();
description_.reset (copy (s.description_.get()));
return *this;
}
// copy, used by = and copy ctor
char* Shape::copy (const char* text)
{
char* result = new char [strlen (text) + 1];
strcpy (result, text);
return result;
}
Example 26-6The Shape class from Example 21-1, now using unique_ptr
// Shape class, for use with the SSDL library
// -- from _C++20 for Lazy Programmers_
#ifndef SHAPE_H
#define SHAPE_H
#include <memory> // for unique_ptr
#include "SSDL.h"
struct Point2D // Life would be easier if this were a full-fledged class
{ // with operators +, =, etc. . . . but that
int x_, y_; // was left as an exercise.
};
class Shape
{
public:
Shape (int x = 0, int y = 0, const char* text = "");
Shape (const Shape& other);
Shape (Shape&&) = default;
virtual ~Shape() {} // No need to delete contents_ -- handled!
Shape& operator= (const Shape& s);
Shape& operator= (Shape&&) = default;
//...
const char* description() const { return description.get(); }
//...
private:
Point2D location_;
SSDL_Color color_;
std::unique_ptr<char> description_;
char* copy(const char*); // used for copying descriptions
// altered from original for clearer use
// with the new description_, but
// it's not really new stuff
};
#endif //SHAPE_H
Example 26-5The Shape class from Example 21-1, now using unique_ptr
我发现的主要优势是
-
无需在析构函数中编写任何代码;知道如何删除自己。我也不必在
operator=或复制构造器中删除内存——这是一个容易出错的活动。 -
我现在可以使用默认的移动功能。
location_和color_使用它们的复制扇区;description_知道如何用它的 move 构造器复制自己。不编写 move 构造器和 move=为我节省了大约八行代码。
shared_ptr
一个unique_ptr拥有它的内存,其他任何人都不应该改变或释放它。
一个shared_ptr让其他shared_ptr拥有相同的内存。它记录了有多少个shared_ptr正在使用它(这就是“引用计数”),只有当这个数字降到 0 时,内存才会被删除。
这里有一个可能的用途:我有一个可以从文件中加载的 3d 模型。这些东西往往很大。如果我有 20 个相同类型的怪物,我不想要所有图形数据的 20 个副本!
所以我将图形数据放在一个类型为GraphicsData的对象中,并为怪物的每个实例创建一个Monster对象。让Monster s 分享他们的GraphicsData。
class GraphicsData
{
...
private:
... lots of graphics info...
};
class Monster
{
...
Point3D location_;
shared_ptr<GraphicsData> _modelInfo;
};
如果GraphicsData包含一个指向Monster的指针,那么shared_ptr的引用计数可能会遇到一个问题,因此引用的数量不会降到零,也不会删除任何东西。weak_ptr,这里没有涉及,是处理那个问题的一种方式。
防错法
我发现智能指针的主要问题是忘记get():
strcpy (myChars, "totally unique"); // should be myChars.get()。
Exercises
- 重写
String类(使用第十八章,这样它会有移动复制和移动=)来使用unique_ptr。
static_cast等。
哪里曾经说过newtype (value),哪里就可以说static_cast <newtype> (value):
double dbl = double (intVal);
成为
double dbl = static_cast<double> (intVal);
和
((ChildClass*) (parentClassPtr))->childClassFunction ();
成为
(static_cast<ChildClass*> (parentClassPtr))->childClassFunction ();
为什么呢?所以当你想说
int* intArray = static_cast<int*> (myFloatArray); // huh?!
编译器不会让你这么做。指针安全。
Extra
我不推荐 C++ 提供的其他选角 运算符,如果你继续下一部分,我也不会责怪你。
还在吗?好的。以下是其他类型:
const_cast<type>:增加或减少constness:
((const_cast<const MyClass*> (someVar ))->print (cout);
// adds constness
((const_cast< MyClass*> (someConst))->alterMeInSomeWay();
// takes it away
…但是你不能安全地把它应用到最初声明的东西上const。你可以将它应用于作为const参数传入的东西。
我避免这样。如果是非const,我能用const做什么非const做不了的事?不多。如果是const,我真的不应该破坏那个安全。
-
dynamic_cast<type>:如果涉及到虚拟的话,这可以让你在继承层次中进行转换。我从来没用过。 -
reinterpret_cast<type>:这个不太“什么都行”——不影响constness,也不能施放自己想不通的东西,但是可以做类似前文施放myFloatArray到int*的诡异事情。我也没用过。但是也许我在不知道的情况下用了最后一个,那时候我们只有更老更简单的演员阵容
<type>()。当您这样做时,C++ 会按顺序尝试这些类型的转换: -
const_cast -
static_cast -
static_cast然后const_cast -
reinterpret_cast -
reinterpret_cast然后const_cast找一个能用的。如果失败了,你就不能做演员了。
在我看来,我们使用的是
static_cast,而不是老式的造型,主要是为了不让我们不知道它已经到达了reinterpret_cast。
用户定义的文字:测量系统之间的自动转换
1999 年 9 月 23 日,美国太空探测器火星气候轨道器在火星附近失踪。该计划中有关进入轨道的部分有一些英制计算和一些公制计算。该计划失败了,美国国家航空航天局从未从其价值 3 . 27 亿美元的宇宙飞船中得到任何东西。哎呀。
我完全理解。每次用 trig 函数写程序,我都用度数,C++ 用弧度。
当 NASA 编写这个软件时,现代 C++ 还不存在,但是如果有,他们可以让计算机在系统之间自动转换。以下是如何:
-
Write an operator to convert from your unit to some unit you want the calculations done in. I’ll convert miles to meters:
long double operator"" _mi (long double mi) { return mi * 1609.344; // 1 mi = 1609.344 meters }你需要主角
_。
** 这样称呼它: 10_mi 。C++ 将此视为将 10 传递给"" _mi操作符。
*
*在 BNF 中,操作符看起来是这样的(尽管您可以像constexpr一样给它添加限定符):
<return-type> operator "" _<operator name> (<parameter list>)
{
<body>
}
这样用: <值> _ <运算符名称> ,无空格*。它只适用于文字——你不能用它来转换变量。*
示例 26-7 将此与constexpr一起使用–为什么不呢?–进行一些简单的计算。
// Program to use user-defined literals
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <cmath>
using namespace std;
constexpr double PI = 3.14159;3
// "Literal" operators
constexpr long double operator"" _deg (long double degrees) // version for // double values
{
return degrees * PI/180;
}
constexpr long double operator"" _deg (unsigned long long degrees) // ...for int
{
return degrees * PI/180;
}
constexpr long double operator"" _m (long double m) { return m; }
// 1 m = 1 m (duh)
constexpr long double operator"" _mi (long double mi) { return mi * 1609.344;}
// 1 mi = 1609.344 m
int main ()
{
cout << "The speed of light is 186,000 miles per second.\n";
cout << "In metric, that's ";
cout << 186'000.0_mi4
<< " meters per second --\n"
<< " should be about " << 300'000'000.0_m << ".\n";
cout << "Oh, and sin (30 deg) is about 0.5
: " << sin (30_deg) << endl;
return 0;
}
Example 26-7A program using user-defined literals
输出:
The speed of light is 186,000 miles per second.
In metric, that's 2.99338e+008 meters per second --
should be about 3e+008.
Oh, and sin (30 deg) is about 0.5: 0.5
以下是关于参数的两个特性:
-
它不会在类型之间进行隐式转换(尽管它对像
unsigned或long这样的修饰符很灵活)。如果你给它186'000_mi,它就会失败,因为它期待的是double而不是int。要么添加.0,要么编写一个期望整数类型的运算符。 -
参数表必须是表 26-1 中的一组。我大多用
long double。
表 26-1
用户定义的文字运算符的可能参数列表
| `unsigned long long` | `long double` | | `char` | `const char*` | | | `const char*, std::size_t`5 | | `wchar_t` | `const wchar_t*, std::size_t` | | `char16_t` | `const char16_t*, std::size_t` | | `char32_t` | `const char32_t*, std::size_t` |Exercises
使用用户定义的文字,如果可能的话用constexpr...
-
火星的气压约为 6.1 毫巴,地球的气压约为每平方英寸 14.7 磅。金星的大概是 9.3 MPa。在网上查找这些单位进行转换,计算金星的压力比地球大多少倍,地球比火星大多少倍。
-
使用砝码,用户提供三个物体,分别是磅、千克和石头,哪一个最重。
一次性使用的 Lambda 函数
STL 的函数sort可以接受第三个参数,这是一个比较函数,如果认为第一个参数小于第二个参数,则返回true。假设我们想按名字或按人口对城市进行排序:
bool lessThanByName (const City& a, const City& b)
{
return a.name() < b.name();
}
bool lessThanByPop (const City& a, const City& b)
{
return a.population() < b.population();
}
...
ranges::sort (cities, lessThanByName);6
ranges::sort (cities, lessThanByPop);
如果一个比较函数只使用一次,也许我懒得为它创建一个完整的函数。我可以这样做:
ranges::sort (cities, [](const City& a, const City& b)
{
return a.name() < b.name();
});
这被称为“lambda”函数,这个术语是通过 LISP 编程语言从数学中借用的。BNF 就像一个函数,除了返回类型和函数名被替换为[]:
[] (<parameters, separated by commas>)
{
<thing to do -- variable declaration, action, whatever>*
}
[]是一种表示“函数名放在这里,只是这次我没有考虑名字。”
λ捕获
越来越奇怪了。假设我想根据我的City离某个特定地点的距离来排序。我不能把那个位置作为第三个参数传入-sort需要一个两个参数的比较函数!但是我可以告诉 lambda,“把这个变量从外面带进来”:
const City LA ("Los Angeles", 3'900'000, { 34_deg, -118_deg }); // name, pop, location
ranges::sort (cities, &LA
{
return distance (LA, a) < distance (LA, b);
});
我在上面加了一个&来说明,“提交一份参考资料,不要复制。”你不能说const &,但既然LA是const,就不会改变它。
如果 lambda 不改变值并且复制成本不高,我可以省略&:
// Find out if some bad letter is in my city's name
auto findBadLetter =
find_if (name, badLetter { return ch == badLetter; });
表 26-2 显示了我们可以放在[]之间的东西。通常我们不需要任何东西。但是我们可以列出具体的变量,有或者没有&。
表 26-2
λ捕捉。这些位于 lambda 函数的[]之间,允许访问不是 lambda 函数参数的非静态局部变量。它们可以组合:【arg1, &arg2, this ]
我们也可以用=或&来说,“让一切都进来。”这里的“一切”是指非全局变量(ack!)并且不是静态的。 7 全局变量和静态局部变量无论如何都可以被引用,而不需要在[]中列出
我避免裸露的&和=——确切地指定什么可以进入函数更安全。
lambda 函数的示例
示例 26-8 展示了 lambda 函数和捕获,在程序中以各种方式对城市进行排序。
// Program that uses lambda functions to order cities by different criteria
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <vector>
#include <cassert>
#include <algorithm>
#include "globalLoc.h" // provides user-defined literal operators _deg and _mi,
// struct GlobalLoc, latitude, longitude, and
// a distance function that works on a globe
using namespace std;
class City
{
public:
City (const std::string& n, int pop, const GlobalLoc& loc) :
name_ (n), population_ (pop), location_ (loc)
{
}
City (const City&) = default;
City& operator= (const City&) = default;
const std::string& name () const { return name_; }
int population () const { return population_; }
const GlobalLoc& location() const { return location_; }
private:
std::string name_;
int population_;
GlobalLoc location_;
};
inline
double distance (const City& xCity, const City& yCity)
{
return distance (xCity.location(), yCity.location());
}
int main()
{
// Some prominent party spots
vector<City> cities =
{
{"London", 10'313'000, { 51_deg, -5_deg}},
{"Hamburg", 1'739'000, { 53_deg, 10_deg}},
{"Paris", 10'843'000, { 49_deg, 2_deg}},
{"Rome", 3'718'000, { 42_deg, 12_deg}},
{"Rio de Janiero", 12'902'000, {-22_deg, -43_deg}},
{"Hong Kong", 7'314'000, { 20_deg, 114_deg}},
{"Tokyo", 38'001'000, { 36_deg, 140_deg}}
};
// Print those cities in different orderings:
cout << "Some major cities, in alpha order : ";
ranges::sort (cities, [](const City& a, const City& b)
{
return a.name() < b.name();
});
for (const auto& i : cities) cout << i.name() << " / ";
cout << endl;
cout << "Ordered by population: ";
ranges::sort (cities, [](const City& a, const City& b)
{
return a.population() < b.population();
});
for (const auto& i : cities) cout << i.name() << " / ";
cout << endl;
cout << "Ordered by how far they are from LA: ";
const City LA ("Los Angeles", 3'900'000, { 34_deg, -118_deg });
// & would work here too -- but &LA is a little more secure
ranges::sort (cities, &LA
{
return distance (LA, a) < distance (LA, b);
});
for (const auto& i : cities) cout << i.name() << " / ";
cout << endl;
return 0;
}
Example 26-8A program to sort cities, using lambdas. Parts are omitted for brevity
Exercises
当然,在所有这些情况下,都要使用 lambda 函数。我引用了我在文中没有描述过的函数;需要的话在网上查查。
-
按纬度对前面示例中的
City进行排序。 -
使用
for_each函数打印一个容器的每个元素,由/分隔。也许你会用它来代替示例 26-8 中打印的for。 -
使用
for_each将一个容器中的每一个柠檬大写。 -
使用
count_if函数查看容器中有多少整数是整数的平方。 -
使用
all_of来验证容器中的每个字符串都包含一些子字符串,由用户给出。
与本章的其他部分一样,还有更多需要了解的内容——但这将是你的开始。
2
但不是 myChars.reset("完全唯一");。它只能获取它可以拥有和删除的动态内存,而不是静态内存。
3
C++20 会让我们#include 并使用 std::numbers::pi,而不是声明我们自己的。在撰写本文时,支持是不稳定的。
4
您可以使用这些“数字分隔符”来使数字更容易阅读。我承认撇号在这里看起来很奇怪,但逗号正忙于其他事情。
5
这张看起来很奇怪,我来解释一下。您应该像这样编写函数头
string operator”” _fancify (const char* str, size_t n)
// make a char* into a fancy string, whatever that is
并这样称呼它:
string fancy = “string to be made fancy”_fancify;
它自动从字符数组中获取大小。
6
回想一下第二十三章:如果你的编译器还不支持 ranges,那就传入相关的 begin()和 end(),而不是容器:sort (cities.begin()、cities.end()、lesssthanbyname);。
7
正确的说法是“自动变量”
*