面向懒惰程序员的 C++20 教程(九)
二十七、秘籍(不推荐)
我很少使用这些功能。这一章的部分原因是在那些罕见的情况下,它们是有用的。另一个是理解为什么他们不那么受欢迎。因为向前看很有趣,所以有两个 C++20 特性的预览,它们还不是很有用,但是应该在 C++23 中:模块和协程。
protected章节,protected传承
考虑类Phone。Phone有一个成员numCalls_,它记录任何一个Phone打过的所有电话。有一个函数可以改变它,但它是私有的,因为我们真的应该只在制作call ()时更新numCalls_。
class Phone
{
public:
void call() { /*do some stuff, and then */ incNumCalls(); }
static int numCalls() { return numCalls_; }
private:
void incNumCalls () { ++numCalls_; }
inline static int numCalls_ = 0;
};
但是现在我们已经到达了人类文明的黎明,出现了MobilePhone s。他们用不同的方式打电话,使用信号塔,但是他们也需要增加那个数字。他们无法访问Phone::incNumCalls();这是隐私。我们有充分的理由决定不公开。我们还能做什么?
C++ 提供了另一个部分:受保护的(参见示例 27-1 )。外界看不到(像 private),但是子类看得见。
class Phone
{
public:
void call() { /*do some stuff, and then */ incNumCalls(); }
static int numCalls() { return numCalls_; }
protected:
void incNumCalls () { ++numCalls_; }
private:
inline static int numCalls_ = 0;
};
Example 27-1The Phone class, ready to share a family secret with its child classes
现在MobilePhone将能够访问incNumCalls():
class MobilePhone : public Phone
{
public:
void call() { /* do stuff w cell towers, and */ incNumCalls(); }
};
应该用public还是private继承?比方说,当你打手机时,你需要一些额外的安全措施。所以在MobilePhone中,我将抛弃旧的call,增加一个新的功能secureCall:
class MobilePhone : public /*?*/ Phone
{
public:
void secureCall()
{
makeSecure ();
/* do cell tower stuff */
incNumCalls();
}
void makeSecure (); // however that's done
};
有了公共继承(图 27-1 ,继承的成员在子类中就像在父类中一样是公共的。这对MobilePhone不利:它让外界使用不安全的、继承的call函数。也许私人继承,如图 27-2 ,会更好?看起来是这样。
图 27-2
带有受保护节的私有继承
图 27-1
带有受保护节的公共继承
现在我将添加一个MobilePhone的子类:一个SatellitePhone。它的叫声不同:
class SatellitePhone : public MobilePhone
{
public:
void secureCall()
{
makeSecure ();
/* do satellite stuff */
incNumCalls();
}
// makeSecure is inherited from MobilePhone
};
问题:SatellitePhone不能用incNumCalls。私人继承把它放在MobilePhone的私人部分。
我们可以用保护 继承,如图 27-3 。这就像公共继承,除了继承的公共成员受到保护。
图 27-3
继承:一个解决问题的方法
现在,正如示例 27-2 中的程序可以验证的那样,子类是安全的,不会被旧的call函数、和使用,所有的类都根据需要访问incNumCalls()。
int main ()
{
Phone P; P.call();
MobilePhone MP; MP.secureCall();
SatellitePhone SP; SP.secureCall();
assert (Phone::numCalls() == 3); // If this assertion succeeds,
// incNumCalls got called // 3 times -- good!
return 0;
}
Example 27-2Verifying that no matter what kind of call we made, Phone::numCalls_ got updated
在拥有孙类之前,使用私有继承还是受保护继承根本无关紧要。即便如此,这可能也无关紧要。我几乎从不需要受保护的部分或受保护的继承。
为什么你不应该有
考虑一个使用地图的程序。它读入几个Area秒,如图 27-4 所示。每个Area都有一个名称和边界框(?? 向北、向南、向西和向东延伸多远)。然后它报告哪个Area在最北边。示例 27-3 和 27-4 显示源代码,为简洁起见省略了一些代码。
图 27-4
四个Area的地图,其中一个Area上显示一个边界框
// Program to read in regions from a file, and tell which
// is furthest north.
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <fstream>
#include <vector>
#include "area.h"
using namespace std;
int main ()
{
vector<Area> myAreas;
ifstream infile("regions.txt");
if (!infile)
{
cerr << "Can't open file regions.txt.\n"; return 1;
}
while (infile) // read in Areas
{
Area area; infile >> area;
if (infile) myAreas.push_back (area);
}
// find the northernmost Area
int northernmostIndex = 0;
for (unsigned int i = 1; i < myAreas.size(); ++i)
if (furtherNorthThan (myAreas[i],myAreas[northernmostIndex]))
northernmostIndex = i;
// print it
cout << "The northernmost area is "
<< myAreas [northernmostIndex]
<< endl;
return 0;
}
Example 27-4The map program, which identifies the Area furthest north
// Class Area
// Each Area is read in as
// <north bound> <south bound> <west bound> <east bound> <name>
// as in
// 8 2 1 4 Blovinia
// ...and that's what an Area contains
// -- from _C++20 for Lazy Programmers_
#ifndef AREA_H
#define AREA_H
#include <string>
#include <iostream>
class Area
{
public:
static constexpr int NORTH = 0,
SOUTH = 1,
EAST = 2,
WEST = 3;
static constexpr int DIRECTIONS = 4 ; // there are 4 directions
Area () {}
Area (const Area& other);
Area& operator= (const Area& other);
void read (std::istream& in );
void print (std::ostream& out) const { out << name_; }
private:
double boundingBox_[DIRECTIONS];
// the northernmost, southernmost, etc., extent of our Area
// bigger numbers are further north
// bigger numbers are further east
std::string name_;
};
inline
bool furtherNorthThan (const Area& a, const Area& b)
{
return a.boundingBox_[Area::NORTH] > b.boundingBox_[Area::NORTH];
}
#endif //AREA_H
Example 27-3area.h
我知道我已经在这里写了清晰的、注释良好的代码(我也很谦虚),所以我不会进一步解释。但是当furtherNorthThan试图访问boundingBox_时,编译器抱怨侵犯了隐私。没错:boundingBox_是的私有。
C++ 的朋友可以解决这个问题。如果一个函数与一个类联系如此紧密,以至于它也可能是一个成员——但是不方便使它成为一个成员——你可以让它访问所有成员,包括私有成员,就像它是成员一样。下面是方法:在类Area的某个地方(我把它放在顶部,所以它总是在同一个地方),放一个函数的friend声明(例子 27-5 )。
class Area
{
// "friend" keyword plus prototype of the trusted function
friend bool furtherNorthThan (const Area& a, const Area& b);
...
Example 27-5A friend for Area
现在程序应该编译好了,并报告 Morgravia 在最北边。
也可以做一个 a 类friend:
class Area
{
friend class OtherClassITrust;1
...
或者让其他类的成员函数成为朋友:
class Area
{
friend void OtherClassIPartlyTrust::functionIFullyTrust();
...
这是个好主意吗?
根据马歇尔·克莱恩和《C++ 超级常见问题解答》, 2 是的。他认为friend函数是公共接口的一部分,就像公共成员函数一样。它不违反安全性,但只是安全性的另一部分。
我明白他的意思,但我想不出一个不能用另一种方法来做的例子。在这个例子中,我们可以用bool Area::furtherNorthThan (const Area& b) const;代替bool furtherNorthThan (const Area& a, const Area& b);。这就是我们对像<这样的运营商所做的。为什么不也这样呢?
我曾经让流 I/O 操作符>>和<<成为他们打印/读取的类的朋友;现在我让它们调用成员函数print和read。使用friend可能会更容易,但不会更容易。
如果你想要它,就像专家建议的那样使用它:用于与所讨论的类紧密相关的事物,因此它们可以被认为是类与世界的接口的一部分。我打赌不会经常发生。
用户定义的转换(转换运算符)
我们是否应该根据需要添加一种从String隐式强制转换到const char*的方法?有道理。许多内置函数都希望有一个char*,你可能更喜欢myInFile.open (filename);而不是myInFile.open (filename.c_str()),尤其是在你输入第 100 次的时候。所以我们将这个运算符添加到String: operator const char* () const { return c_str (); } // called implicitly as needed。
对于对myInFile.open的调用很有效。然后我们尝试一个简单的字符串比较:
if (str1 == "END")
cout << "Looks like we've reached the END.\n";
它不再编译——抱怨含糊不清或过多重载。
没错。现在有两种方法来匹配运算符==的参数:隐式地将"END"转换为另一个String,并与String的==进行比较;用 cast 操作符将str1隐式转换为char*,并使用char*的==。
解决方法是在函数前面加上explicit这个词(例如 26-7 )。 3
class String
{
public:
...
explicit operator const char* () const { return c_str(); }
// will cast from String to const char* if explicitly called
...
};
Example 27-6Giving String a user-defined cast operator
现在我们可以选角了,但是我们必须说我们想要选角:
myInputFile.open ((const char*) (filename)); //old-style explicit // cast -- OK
或者
myInputFile.open (static_cast<const char*>(filename)); //modern explicit // cast -- OK
它起作用了,但是我们从说filename.c_str()中得到什么了吗?
我似乎从来没有找到一种既安全又省时的方法来使用这个功能。也许你会。
Exercises
在每个练习中,使用explicit来避免歧义。
-
向
Fraction类添加一个 cast-to-double 运算符。比如 1/2 的double版本是 0.5(当然)。 -
将 cast-to-double 运算符添加到前面练习中的
Point2D类中。一个Point2D的double版本就是星等:。
模块
我以外的程序员开始担心加载那些越来越长的.h文件所花费的时间。我们也回避了一些问题:你在一个.h文件中包含的一个#define可能会干扰另一个文件。我们试图通过命名这些定义的惯例来避免这种情况;我们可能会失败并得到可怕的错误信息。
模块是一个补丁。一个模块可以编译一次,而不是为每个使用它的.cpp文件重新编译,不像一个.h文件; 4 这样应该可以缩短编译时间。它还可以指定其作者希望与世界共享的内容,从而防止一些名称冲突。(一个.h文件让所有东西对它的 includer 可见,一个.cpp文件什么也不显示,但是一个模块可以选择。)和一个模块都可以在一个文件中——你不必在.cpp和.h文件中分开。
我相信会按计划进行的。但是标准本身并不完整:即将到来的 C++23 标准的“首要任务”之一是将标准库放在模块中,这意味着他们还没有这样做。 5 我会等,我也推荐你这么做。
但是我当然不能就此罢休——所以这里有一个在线的补充。
Online Extra: Using Modules Right Now
请参阅 github.com/Apress/cpp20-for-lazy-programmers,了解如何使用模块的最新演练,因为现在最好的编译器支持它们。
协同程序
一般来说,如果你第二次调用一个函数,它会从头开始。协程程序可以从中断的地方重新开始。
示例 27-6 使用协程来计算下一个阶乘。(关于阶乘的复习,参见第十八章的递归部分)是co_yield让 C++ 将它识别为协程。6std::experimental::``generator``<int>返回类型的意思是“设置这个,以便factorial可以生成int s。”
// Program to print several factorials using a coroutine
// -- from _C++ for Lazy Programmers_
#include <iostream>
#include <experimental/generator>
std::experimental::generator<int> factorial()
{
int whichOne = 0; // start with 0!
int result = 1; // 0! is 1
while (true)
{
co_yield result;
++whichOne; // go on to next one
result *= whichOne; // and calculate next result
}
}
int main ()
{
std::cout << "The first 8 factorials: \n";
for (int i : factorial())
{
static int whichOne = 0;
std::cout << whichOne << ": " << i << '\n';
++whichOne; // go on to next
if (whichOne > 8) break; // stop at 8
}
std::cout << std::endl;
return 0;
}
Example 27-7A program using a coroutine in Microsoft Visual Studio. g++ and the C++20 standard aren’t equipped for this yet
追踪它的动作:第一次调用它时,它将whichOne——我们要返回的阶乘——设置为 0。0 的result为 1。(您可以将源代码示例加载到ch26文件夹中,并在调试器中跟踪它。我就是这么做的。)
它进入循环。它要做的第一件事是给调用者main提供那个result,和co_yield,意思是“把result给调用者,当再次被调用时,在这里继续执行。”因此控制返回到main,它打印那个result。
当main再次调用它时,它从它停止的地方继续:在co_yield处。它继续将whichOne加 1(将whichOne改为 1),将result乘以whichOne(再次得到 1),进入循环的下一次迭代,然后co_yield就是result。
当再次被调用时,它将再次增加whichOne(得到 2),将result乘以whichOne(得到 2),然后co_yield得到结果。
下一次,whichOne会变成 3,result会变成 6。等等。
main被设置为在一个基于范围的 for 循环中一次又一次地调用这个函数,在 8 处中断(必须在某处停止)。
协程的一个优点是效率。每次我们调用factorial,它所做的只是一个增量、一个乘法和一个返回。是 O(1)!第十八章的版本是 O(N)。程序员还报告说,对于一些问题,协程更直观,也更容易编写。
目前最大的劣势是支持。如你所见,Visual Studio 认为它的generator模板是实验性的,而 g++ 根本没有。两个都支持协程——都有co_await、co_result和co_yield——但是generator不是标准的,我认为最好编写能在任何机器上工作的代码。在 g++ 中,你必须自己写,这并不容易。对于你可能想做的其他事情也是一样。我的希望是 C++23 能够解决这个问题,社区中也有关于这个 7 的讨论。
Exercises
-
改编示例 27-7 ,以便
factorial不仅返回result,还返回结构化绑定中带有whichOne的result—main不必独立跟踪自己的whichOne。这实际上不是用协程而是用结构化绑定来实现的,但我认为仍然值得一做。 -
编写另一个返回下一个质数的
generator函数,从 2 开始,和一个打印前 100 个质数的main版本。
如果已经声明了另一个类,那么可以在这里省略 class 这个词。
2
在写的时候, isocpp.org/wiki/faq/ 。
3
你也可以把explicit放在其他有时被隐式调用的函数前面,比如复制和转换构造器,来禁用隐式调用——但是我从来不这样做。
4
当然,预编译头也不需要重新编译。但是模块还有预编译头文件没有的其他好特性。
5
www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2145r0.html 总结了其中的一些计划。
6
这里没有提到的另外两个关键字co_await和co_return的存在也使一个函数成为一个协程。
7
www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2145r0.html 又来了。
二十八、C
如果你懂 C++,你就差不多懂 C 了。使用 C 的经验给了你另一个吹牛的机会——一个字符长,所以应该合适!–在你的简历上。c 是操作系统和嵌入式系统的流行语言,里面有很多库。
c 基本上是我们在上课前所学的内容,不包括
-
SDL/SSDL
-
cin和cout -
&参数 -
bool(用int代替) -
constexpr(用const代替)
没有类、异常、重载运算符、模板或命名空间。存在,但没有成员函数或公共/私有/受保护部分(都是公共的)。
还有一些较小的差异,包括以下几点:
-
铸造长这样,
(int) f,不是这样:int (f)。 -
structS {...};确实声明了一个名为S的struct,但是声明该类型的变量需要一个额外的单词:structS myStruct;
cplusplus.com 和 cppreference.com,不管名字如何,都是 C 和 C++ 的好资源。
编译 C
在 Visual Studio 中,你不能选择“C 文件”作为添加到你的项目中的东西,但是你可以选择“C++ 文件”并将你的命名为<something>.c。编译器会将其视为 C 文件。照常编译和运行。
在 Unix 或 MinGW 中,你可以将你的程序命名为<something>.c并用gcc命令进行编译,这就像g++只适用于 C 文件。示例代码将这一点构建到了它的 Makefiles 中。
这是必须的“你好,世界!”
// Hello, world! -- again! This time in C.1
// -- from _C++20 for Lazy Programmers_
#include <stdio.h>
int main ()
{
printf ("Hello, world!\n");
return 0;
}
Example 28-1“Hello, world!” in C
一些需要注意的事项:
-
Includ e 文件,无论是否属于系统,都以
.h结尾。那些从 C 继承而来的 C++ 将首字母c去掉了:stdlib.h而不是cstdlib;math.h不是cmath。 -
我们使用
printf打印–下一节将详细介绍。
Extra
如果您希望在同一个项目中同时包含 C++ 和 C 文件,这是可行的,但是需要一些技巧。您需要将main放在一个 C++ 文件中,并且对于要在 C++ 文件中使用的任何 C include 文件,这样包装 include:
extern "C"
{
#include "mycheader.h"
}
如果你用的是 gcc/g++,那就用 g++ 链接。
关于这方面的更多内容,在写作的时候,参见 C++ 超级 FAQ, isocpp.org/wiki/faq/mixing-c-and-cpp 。
输入-输出
所有这些 I/O 功能都在stdio.h中。
printf
代替cout >>,C 有函数printf(print-f,意为“带格式打印”):
printf ("Ints like %d, strings like %s, and floats like %f -- oh, my!\n",
12, "ROFL", 3.14159); // %d is for "decimal"
将打印
Ints like 12, strings like ROFL, and floats like 3.141590 -- oh, my!
%序列是“格式字符串”("Ints like %d...")中的占位符,显示每个连续参数的位置。你可以有尽可能多的论点。最常见的%序列有%d表示十进制整数、%f表示固定浮点、%s表示字符串,即字符数组。%%的意思是“仅仅是%字符。”
你可以把修饰语放在里面。例如,%.2f将小数点右边的两位数字。
scanf和地址-of ( &)运算符
scanf(“扫描-f”)取代了cin >>,如下所示:
scanf ("%f %s", &myDouble, myCharArray);
&的意思是“记下…的地址”C 和 C++ 都有这个操作符,但是 C 一直在用。scanf需要知道myDouble在哪里,这样它就可以修改它(下一节将详细介绍)。它不需要myCharArray的地址,因为myCharArray是地址。
如果你使用本章中的scanf或其他一些函数,Visual Studio 会给出一个警告,告诉你这个函数是不安全的,就像使用一些cstring函数一样(见第十四章)。如果您想禁用警告,请将此行放在main之前:
#pragma warning (disable:4996)
示例 28-2 演示printf和scanf。
// Program to test C's major standard I/O functions
// -- from _C++20 for Lazy Programmers_
#include <stdio.h>
int main ()
{
float number; // number we'll read in and print out
int age; // your age
enum {MAXSTR = 80};2 // array size
char name [MAXSTR]; // your name
// A printf showing float, and use of % sign
printf ("%3.2f%% of statistics are made up on the spot!\n\n",
98.23567894);
// printfs using decimal, hex, and char array
// %02d means pad number to a width of 2 with leading 0's
printf ("%d is 0x%x in hexadecimal.\n\n", 16, 16);
printf ("\"%s\" is a $%d.%02d word.\n\n", "hexadecimal",
5, 0);
// scanf needs & for the variables it sets
printf ("Enter a floating-point number: ");
scanf ("%f", &number);
printf ("%g is %f in fixed notation and %e in scientific.\n",
number, number, number);
printf ("...in scientific with a precision
of 2: %.2e.\n\n",
number);
// ...except arrays, since they're already addresses
printf ("Enter your name and age: ");
scanf ("%s %d", name, &age);
printf ("%s is %d years old!\n\n", name, age);
return 0;
}
Example 28-2printf and scanf
输出可能是这样的:
98.24% of statistics are made up on the spot!
16 is 0x10 in hexadecimal.
"hexadecimal" is a $5.00 word.
Enter a floating-point number: 2
2 is 2.000000 in fixed notation and 2.000000e+000 in scientific.
Here it is in scientific with a precision of 2: 2.00e+000.
Enter your name and age: Linus 7
Linus is 7 years old!
表 28-1 包含 printf 和 scanf 格式代码的部分列表。有关更多详细信息,请参见(在编写本报告时)cplusplus . com/reference/CST dio/printf/和cplusplus . com/reference/CST dio/scanf/。
fprintf和fscanf、fopen和fclose
C 中的文件 I/O 使用了printf和scanf的变体。考虑以下代码:
FILE* file = fopen ("newfile.txt", "w"); // open file
if (!file) { printf("Can't open newfile.txt!\n"); return 0; }
// did it work? if not, quit main
fprintf (file, "Avagadro's number is %.4e.\n", 6.023e+023);
// use it
fclose (file); // close it
为了打开文件进行写入,我们调用fopen(“f-open”),给它一个文件名,"w"表示“写入”(到一个输出文件)。文件信息存储在一个类型为FILE*的指针中,如果fopen失败的话,这个指针就是NULL,相当于 C++ 的nullptr。
关闭文件只是将文件指针发送到fclose,如图所示。
在这两者之间,通过添加file作为第一个参数,将printf修改为fprintf。
如果你想读而不是写,打开文件,用"r"表示“读”,并类似地修改scanf:
file = fopen ("newfile.txt", "r");
if (!file) { printf("Can't open newfile.txt!\n"); return 0; }
fscanf (file, "%s %s %s %e", word1, word2, word3, &number);
fclose (file);
如果成功,fscanf和scanf返回您给出的参数个数。如果号码不一样,那就是出了问题。可能您已到达文件末尾。诸如此类测试:
while (1) // while true
{
if (fscanf (file, "%d", number) != 1)
/* it didn't work -- handle that */;
//...
}
示例 28-3 演示了这些功能。
// Program to test C's major standard I/O functions
// -- from _C++20 for Lazy Programmers_
#include <stdio.h>
int main ()
{
FILE* file; // a file to write to or read from
float number; // number we'll read in and print out
enum { MAXSTR = 80 }; // array size
char junk [MAXSTR]; // a char array for reading in (and thus
// discarding) a word
// printing to file. The number gets 4 digits of precision
file = fopen ("newfile.txt", "w");
if (!file)
{
printf ("Can't open newfile.txt for writing!\n"); return 0;
}
printf ( "Avagadro's number is %.4e.\n", 6.023e+023);
fprintf (file, "Avagadro's number is %.4e.\n", 6.023e+023);
fclose (file);
// reading from a file
file = fopen ("newfile.txt", "r");
if (!file)
{
printf("Can't open newfile.txt for reading!\n"); return 0;
}
fscanf (file, "%s %s %s %e", junk, junk, junk, &number);
// Read in 3 words, then the number we want
fclose (file);
printf ("Looks like Avagadro's number is still %.4e.\n", number);
return 0;
}
Example 28-3fprintf, fscanf, fopen, and fclose
该文件将包含Avagadro's number is 6.0230e+023.,屏幕输出将为:
Avagadro's number is 6.0230e+023.
Looks like Avagadro's number is still 6.0230e+023.
sprintf和sscanf;fgets、fputs、puts
这里还有几个 I/O 函数。
-
sscanf**(“s-scan-f”)**从字符数组中读取。如果myCharArray是"2.3 kg",我们可以说sscanf (myCharArray, "%f %s", &myDouble, myWord); // myDouble gets 2.3, myWord gets "kg",在 C++ 中我们会说
sscanf (myCharArray, "%f %s", &myDouble, myWord); // myDouble gets 2.3, myWord gets "kg" -
sprintf打印到一个字符数组:sprintf (myCharArray, "%s %f", name, number);在 C++ 中我们会说
sstream myStringStream; myStringStream << name << number; string myString = myStringStream.str(); -
fgets从文件中读取一行文本:fgets (myCharArray, MAX_STRING, someFile); // read myCharArray, // which should // be no more than // MAX_STRING long, // from someFile
或者键盘:
fputs打印一个char数组到一个文件:
fgets (myCharArray, MAX_STRING, stdin);
fputs (myCharArray, someFile);
您可以将该文件命名为stdout,但通常我们会使用一个更短的版本:
puts (myCharArray); //sends to stdout
我们不会将fgets缩写为gets : gets存在,但被认为是不安全的,行为不像fgets,因此不被使用。
示例 28-4 演示了这些功能。
表 28-1
printf和scanf的格式代码
%序列
|
意义
|
| --- | --- |
| %d | 十进制格式的整数。 |
| %o | 无符号八进制整数(基数为 8)。 |
| %x/%X | 无符号十六进制整数(基数为 16)。十六进制 1f 将显示为0x1f/0X1F。 |
| %c | 性格。 |
| %s | 字符数组。 |
| %f | 定点浮点。 |
| %e/%E | 科学记数法浮点。如果你说%E,那么E将是大写的。 |
| %g/%G | 默认浮点。如果你说%G,那么E(如果有的话)将会是大写的。 |
| %p | 指针。 |
| %% | %人物本身。 |
// Program to test sprintf, sscanf, fgets, fputs, puts
// -- from _C++20 for Lazy Programmers_
#include <stdio.h>
int main ()
{
while (1) // forever, or until break...
{
enum {MAXLINE=256}; // array size for line
char line [MAXLINE]; // a line of text
enum {MAXSTR = 80}; // array size for word
char word [MAXSTR]; // your word
int number; // a number to read in
// get an entire line with gets; on end of file quit
printf("Enter a line with 1 word & 1 number, end of file to quit: ");
if (! fgets (line, MAXLINE, stdin)) break;
// repeat line with fputs
printf("You entered: ");
fputs (line, stdout); // You *can* use fputs with stdout; puts is // more usual
// Use char array as source for 2 arguments
if (sscanf (line, "%s %i", word, &number) != 2)
puts ("That wasn't a word and a number!\n");
else
{
// Print using sprintf and puts
sprintf(line, "The name was %s and the number was %i.\n",
word, number);
puts (line);
// If this weren't a demo of new functions, I'd say
// printf ("The name was %s and the number was %f.\n",
// name, number);
}
}
puts ("\n\nBye!\n");
return 0;
}
Example 28-4A program using sprintf, sscanf, fgets, and so on
样本输出:
Enter a line with 1 word and 1 numbers, end of file to quit: Mila 18
You entered: Mila 18
The name was Mila and the number was 18.
Enter a line with 1 word and 1 numbers, end of file to quit: Catch 22
You entered: Catch 22
The name was Catch and the number was 22.
Enter a line with 1 word and 1 numbers, end of file to quit: [Enter Ctrl-D or Ctrl-Z here]
Bye!
命令摘要
在表 28-2 中描述的函数中,如果我没有给出函数返回的含义,那是因为我们很少关心那个函数。有了fopen、fgets和scanf家族,我们做到了。
表 28-2
C 语言中常见的stdio函数
printf和变体
| |
| --- | --- |
| int``printf``...); | 根据formatString的规定,在formatString之后打印屏幕参数。 |
| int``fprintf``const char* formatString, ...); | 与printf相同,但打印到file。 |
| int``sprintf``const char* formatString, ...); | 与printf相同,但打印到str。 |
| scanf和变体 | |
| int``scanf``...); | 按照formatString的指定,读取formatString之后的参数。如果在读取任何文件之前到达EOF,则返回EOF(文件结束);成功读取的 else #个参数。 |
| int``fscanf``const char* formatString, ...); | 与scanf相同,但从file读取。 |
| int``sscanf``const char* formatString, ...); | 与scanf相同,但从str读取 |
| 打开/关闭文件 | |
| FILE*``fopen``const char* fileMode); | 打开由filename指定的文件并返回指针。常见的fileMode有"r"(读)"w"(写)"a"(追加)。 |
| int``fclose | 关闭文件。 |
| 读/写字符串 | |
| int``puts``int``fputs``FILE* file); | 打印str/打印str到file。 |
| char*``fgets``FILE* file); | 从file(可能是stdin)读取str,最多读取max-1个字符(所以str的大小应该是max或者更大)。失败时返回NULL。 |
防错法
-
scanf因缺少&:而失败scanf ("%f %s", myDouble, myCharArray);应该是
scanf ("%f %s",``&很容易忘记
&,编译器可能不会警告你。
用*传递参数
C 没有&参数,但和 C++ 一样,它认为一次函数调用不会改变其他参数。哦哦。
void swap (int arg1, int arg2)
{
int temp = arg1; arg1 = arg2; arg2 = temp;
}
int main ()
{
int x, y;
...
swap (x, y); // x, y will not be changed
...
}
c 希望您发送参数的地址:
int main ()
{
int x, y;
...
swap (&
x, &y); // x's and y's addresses are sent, not x and y
...
}
该函数获取该地址,并使用*来引用它所指向的东西,其中可以被改变:
void swap (int* arg1, int* arg2)
{
int temp = *arg1; *arg1 = *arg2; *arg2 = temp;
}
有效!但是它很笨重,并且引入了一个令人恼火的常见错误:忘记*的。
示例 28-5 展示了一个使用它的程序(并且不要忘记*)。
// Program to do statistics on some strings
// from _C++20 for Lazy Programmers_
#include <stdio.h> // for printf, scanf
#include <string.h> // for strlen
void updateLineStats (char line[], unsigned int* length,
float* averageLineLength);
int main ()
{
printf ("Type in a line and I'll reply. ");
printf ("Type the end-of-file character to end.\n");
while (1) // forever (or until a break) ...
{
enum { MAXSTRING = 256 }; // max line length
char line [MAXSTRING]; // the line
int length; // its current length
float averageLineLength;
// get line of input
if (!fgets (line, MAXSTRING, stdin)) break;
// do the stats. We send addresses, not variables, using &
updateLineStats (line, &length, &averageLineLength);
// give the result
printf ("Length of that line, ");
printf ("and average so far: %d, %.2f.\n",
length, averageLineLength);
}
return 0;
}
void updateLineStats (char line[], unsigned int* length,
float* averageLineLength)
{
static int totalLinesLength = 0; // have to remember these
static int linesSoFar = 0; // for next time
// length is a pointer, so *length is the length
*length = (unsigned int) strlen (line);
// casting from size_t to unsigned int
// fgets included the final \n, but I won't count that:
--(*length);
++linesSoFar;
totalLinesLength += *length;
*averageLineLength = // and averageLineLength needs its *, too
totalLinesLength / ((float) linesSoFar);
}
Example 28-5Program using parameter passing with *’s in C
示例会话:
Type in a line and I'll reply. Type the end-of-file character to end.
alpha
Length of that line, and average so far: 5, 5.00.
bet
Length of that line, and average so far: 3, 4.00.
soup
Length of that line, and average so far: 4, 4.00.
防错法
- **" <变量>的间接级别不同"或"无法从<类型>转换为<类型> * "或"在不强制转换的情况下从整数生成指针。"**有各种各样的抱怨方式,但底线是很难记住在函数调用中放入
&的,更难记住*的 *every!时间到了!*你使用传入的变量。我从未找到解决办法。至少你知道这可能是出错的原因。
动态存储器
忘记new、new []、delete和delete []。c 的动态内存更简单,虽然更难看:
#include <stdlib.h> // for malloc, free
...
someType* myArray = malloc (myArraySize * sizeof (someType));
// allocate a myArraySize element array of some type
...use the array...
free (myArray); // throw it back
没有析构函数来帮助你记住释放东西——你只能靠自己了。
示例 28-6 将示例 14-3(一个使用动态数组的早期程序)改编为 c。
// Program to generate a random passcode of digits
// -- from _C++20 for Lazy Programmers_
#include <stdio.h>
#include <stdlib.h> // for srand, rand, malloc, free
#include <time.h> // for time
int main ()
{
srand ((unsigned int) time(NULL));// start random # generator
// NULL, not nullptr
int codeLength; // get code length
printf ("I'll make your secret passcode. "
"How long should it be? ");
scanf ("%d", &codeLength);
// allocate array
int* passcode = malloc (codeLength * sizeof(int));
for (int i = 0; i < codeLength; ++i)// generate passcode
passcode[i] = rand () % 10; // each entry is a digit
printf ("Here it is:\n"); // print passcode
for (int i = 0; i < codeLength; ++i)
printf ("%d", passcode[i]);
printf ("\n");
printf ("But I guess it's not secret any more!\n");
free (passcode); // deallocate array
return 0;
}
Example 28-6A C program using dynamic memory
Exercises
做第十三章和第十四章的练习,不包括那些用 SSDL 的。
Footnotes 1如果你的编译器不理解像这样的注释//——“c++ 风格的注释”——改为注释/* C-style */。或者获得更新的编译器。
2
“enum hack”是声明MAXSTR的一种方式。它比const int MAXSTR=80需要更少的输入(以及内存和运行时间);。
二十九、继续 SDL
通过使用 SSDL,你已经朝着成为一名 SDL 程序员迈进了一大步。要坚持下去,你可以
-
甩掉 SSDL,找一个关于 SDL 的教程。你会看到很多你认识的东西。许多 SSDL 函数是 SDL 函数,前面有一个“S”(比如,
SDL_PollEvent变成了SSDL_PollEvent)。通常 SDL 函数需要多一个初始参数,通常是类型SDL_Window*或SDL_Renderer*,这两种类型你马上就能学会。您通常可以猜到需要什么(提示:名称中带有“Render”的函数可能需要SDL_Renderer*)。 -
或者,保留 SSDL,但扩展更多 SDL 功能,比如支持操纵杆。
无论哪种方式,看看 SSDL 背后隐藏的东西都是有用的。让我们从初始化和清理代码开始。
典型的 SDL 程序有一个版本的main,看起来像示例 29-1 。
图 29-1
来自示例 29-1 、 29-2 和 29-3 的 SDL 程序。值得吗?
// An SDL program that does nothing of interest (yet)
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include "SDL.h"
#include "SDL_image.h"
#include "SDL_mixer.h"
#include "SDL_ttf.h"
int main(int argc, char** argv)
{
// initialization
constexpr int DEFAULT_WIDTH = 640, DEFAULT_HEIGHT = 480;
if (SDL_Init (SDL_INIT_EVERYTHING) < 0) return -1;
SDL_Window* sdlWindow
= SDL_CreateWindow ("My SDL program!",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
DEFAULT_WIDTH, DEFAULT_HEIGHT,
0); // flags are 0 by default
if (!sdlWindow) return -1; // nope, it failed
int rendererIndex = -1; //pick first renderer that works
SDL_Renderer* sdlRenderer
= SDL_CreateRenderer (sdlWindow, rendererIndex,
0); // flags are 0 by default
if (!sdlRenderer) return -1; // nope, it failed
SDL_ClearError(); // Initially, no errors
static constexpr int IMG_FLAGS // all available types
= IMG_INIT_PNG | IMG_INIT_JPG | IMG_INIT_TIF;
if (! (IMG_Init (IMG_FLAGS) & IMG_FLAGS)) // start SDL_Image
return -1;
if (TTF_Init() == -1) return -1; // ...and SDL_TTF
// ...and SDL_Mixer
int soundsSupported = Mix_Init
(MIX_INIT_FLAC|MIX_INIT_MOD|MIX_INIT_MP3|MIX_INIT_OGG);
if (!soundsSupported) return -1;
int soundInitialized = (Mix_OpenAudio(88020, MIX_DEFAULT_FORMAT,
MIX_DEFAULT_CHANNELS, 4096) != -1);
if (!soundInitialized) SDL_ClearError();
// if it failed, we can still do the program
// -- just forget the error
// STUFF YOU ACTUALLY WANT TO DO GOES HERE
// cleanup -- we're about to end the program anyway, but it's considered nice anyway
if (soundInitialized) { Mix_AllocateChannels(0); Mix_CloseAudio(); }
Mix_Quit();
TTF_Quit();
IMG_Quit();
SDL_DestroyRenderer(sdlRenderer);
SDL_DestroyWindow (sdlWindow);
SDL_Quit();
return 0;
}
Example 29-1A simple SDL program
在 SSDL,例 29-1 中的初始化代码是由SSDL_Display和SSDL_SoundSystem的构造器完成的。示例 29-1 有所简化。一个大问题是,我们不能在没有 SSDL 的情况下发射SSDL_Exception(咄),所以我们用return -1;来处理发射失败。
看它做什么:初始化 SDL(这个必须先做);创建窗口(好!);创建绘制或粘贴图像所需的“渲染器”;初始化图像和字体所需的 SDL 图像和 SDL TTF。如果出了什么问题,我们就放弃,因为没有这些东西你真的走不下去。
如果可以的话,它还通过初始化 SDL 混合器来支持声音。
清理代码关闭助手库,关闭窗口和渲染器,最后关闭 SDL。
显然,我更喜欢我的方式,因为它有条理、整洁,而且不必在每个新程序中键入所有代码;但是既然我们看到了这一切混乱的内部,我想我们会像游戏程序员经常做的那样把它留在main里。至少我没有使用全局变量。
编写代码
那么我们能得到一个实际上能做些什么的程序吗?当然,但首先让我谈谈 SSDL 还隐瞒了什么:
-
许多 SSDL 类型代表 SDL 类型,通常是指针。
-
SSDL_Color本质上是一个SDL_Color。 -
SSDL_Display本质上是一个SDL_Renderer*和一个SDL_Window*。(如果您关心如何将它们传递给需要它们的 SDL 函数,请参见SSDL_Display类定义,特别是两个用户定义的转换或转换操作符。) -
SSDL_Font是一个TTF_Font*。 -
SSDL_Image是一个SDL_Texture*。 -
SSDL_Music是一个Mix_Music*。 -
SSDL_Sound是一个Mix_Chunk*和一个int(针对频道)。 -
SSDL_Sprite是一个SDL_Texture*加上许多字段,在一个复杂的调用中被发送到SDL_RenderCopyEx(见SSDL_RenderSprite)。这些类的存在主要是为了保护初学者免受指针的影响,并且每个人都不必自己进行动态分配和清理。
-
-
除了 RGB,
SDL_Color和SSDL_Color还有一个“alpha”成员,其范围也是从 0 到 255。0 表示完全透明,255 表示完全不透明。要使用它,你需要名字中带有“混合”的 SDL 函数。 -
忘记
ssin和sout;你将使用TTF_RenderText_Solid(见SSDL_Display::RenderTextLine)。 -
SDL 总是使用动态内存,但是你不能使用
new和delete。SDL 及其助手提供了自己的分配和释放函数,例如,SDL_CreateTexture和SDL_DestroyTexture;TTF_OpenFont和TTF_CloseFont。你必须使用它们。
好,让我们做点什么,看看 SSDL 是怎么作弊的。我会在屏幕上显示一个图像,然后等待有人按下一个键。呼-啊!
我将使用来自SSDL_LoadImage和SSDL_RenderImage的图像代码(在 SSDL 代码中搜索这些代码——它们一定在某个地方)。如果你在追寻自我,请这样做!–你会看到我省略了对SSDL_Display::Instance的调用(这只是为了确保初始化代码首先被调用,我们已经这样做了)。我们不会拉伸图像,所以我将省略对stretchWidth和stretchHeight的引用,使用图像的实际大小。我根据需要重命名变量。再进行一点清理,我得到了示例 29-2 中的代码,它紧跟在示例 29-1 中的初始化代码之后。
// Draw an image
SDL_Surface* sdlSurface = IMG_Load("media/pupdog.png");
if (!sdlSurface) return -1;
SDL_Texture* image = SDL_CreateTextureFromSurface
(sdlRenderer, sdlSurface);
if (!image) return -1;
SDL_FreeSurface(sdlSurface);
SDL_Rect dst; // dst is where it's going on screen
dst.x = 0; dst.y = 0;
SDL_QueryTexture(image, nullptr, nullptr, &dst.w, &dst.h);
// get width and height of image
SDL_RenderCopy(sdlRenderer, image, nullptr, &dst);
Example 29-2Displaying an image in SDL
等待一把钥匙…我读了SSDL_WaitKey,然后是它调用的东西,然后是它调用的东西*,最终可以构造出例子 29-3 中的怪物。它紧接在示例 29-2 中的图像显示代码之后。*
最后,我可以看到图 29-1 中显示的输出。
// Waiting for a response
SDL_Event sdlEvent;
SDL_RenderPresent(sdlRenderer); // display everything
bool isTimeToQuit = false;
while (!isTimeToQuit)
{
if (SDL_WaitEvent(&sdlEvent) == 0) return -1;
// handle quit messages
if (sdlEvent.type == SDL_QUIT) isTimeToQuit = true;
if (sdlEvent.type == SDL_KEYDOWN
&& sdlEvent.key.keysym.scancode == SDL_SCANCODE_ESCAPE)
isTimeToQuit = true;
if (sdlEvent.type == SDL_KEYDOWN)// Got that key? quit
isTimeToQuit = true;
}
Example 29-3Waiting for a keystroke in SDL
你知道吗,这很有效。而我只用了 100 行!
诚然,我在那里写了一些糟糕的代码:一切都在main中。但是我已经在建造 SSDL 库的时候做了很好的编码工作(我希望如此)。如果我要写好的 ?? 代码,我会说
int main (int argc, char** argv)
{
const SSDL_Image PUPPY = SSDL_LoadImage("media/pupdog.png");
SSDL_RenderImage(PUPPY, 0, 0);
SSDL_WaitKey();
return 0;
}
游戏程序因糟糕的实践而臭名昭著:像这样的长函数、全局变量和指向 wazoo 的指针。当你开始为 SDL 编程时,你可以向每个人展示如何正确地编程。
防错法
有时我会在程序结束时崩溃:SDL 或一个助手库在它的部分清理中失败。我可能不应该这样做,但是我承认,我注释掉了清理代码。反正节目结束后就无所谓了。
收集
在 Unix 或 MinGW,为你的平台(MinGW 或 Unix)选择一个支持 SSDL 的Makefile,并删除所有对 SSDL 的引用。
在 Microsoft Visual Studio 中,获取一个支持 SSDL 的项目(.vcxproj,加上.vcxproj.filters和.vcxproj.user),加载它,并删除对 SSDL 的所有引用——也就是说,在“项目属性”“➤配置属性”下,针对所有平台(如果可能)和所有配置....
-
C/C++ ➤通用➤附加包含目录:取出 SSDL 包含的路径。
-
链接器➤通用➤附加库目录:取出 SSDL 库的路径。
-
链接器➤输入➤附加依赖:取出 ssdl .lib
然后编译并运行,就像你处理 SSDL 项目一样。
更多资源
我认为最好的参考是 libsdl.org。关于 SDL 图像和其他的文档也在那里;你只需要找到它(我在网上搜索我想要的,它会带我去那里)。而且很难击败懒惰的福(lazyfoo.net)的教程。