第八课¶
因为目前我们重点介绍了 C++ 语言的结构,并没有给予我们程序信息的输入输出足够的重视。那么这次我们将会做出改变,但是首先,我们需要快速的查看一下那些被我们所忽视的细微之处。
作用域¶
无须对牙刷感到奇怪,尽管考虑到某些开发人员的卫生习惯,不是一个坏主意。幸运的是,在网络上,没有人会知道你是个小猫咪。
在这里, 作用域 作用于变量。一个变量的作用域是它的存在范围。当一个变量声明之后,它具有一个确定的开始,并且在源代码中,它无法在声明之前使用。并且当运行到达它所声明的区域结束的地方时,它将会完全消失。
作用域有三种类型:局部的,全局的,以及静态的。局部变量具有最短的生命周期:它们在代码块中进行声明,如函数,for 循环,或者 case 语句中,并且当程序离开代码块时,它将会消失。
全局变量和静态变量在函数之外声明。它们总是可以被访问,但是它们初始化的顺序是不确定的,因此需要注意哪些依赖于其他全局变量值的全局变量。全局变量在任何地方都可以访问。静态变量和全局变量具有同样的声明周期,但是它们仅可以在其所定义的文件的代码中所使用。换句话说,其他文件中的代码无法访问它们。所有这些可能比较复杂,但是在代码中还是比较容易理解,我们且看下面的示例:
#include <stdio.h>
// 下面是一个全局变量,可以在任何地方进行访问
int gAppReturnValue = 0;
int AreaOfSquare(int size)
{
return size * size;
}
int main(void)
{
int returnValue = 1;
// 由于计算 0 英寸的平方比较简单,
// 我们在本循环中从 1 开始。
for (int i = 1; i <= 12; i++)
{
// i 仅在本循环中可以访问。
// 如果我们需要利用 returnValue,我们可以在这里使用。
int area = AreaOfSquare(i);
printf("The area of a square %d inches on a side is "
"%d square inches\n",i,area);
}
// 如果我们尝试在循环之外访问 area 变量,
// 编译器将会显示错误,其类似于
// "foo.cpp:30; error: 'area' was not declared in this scop"
return gAppReturnValue;
}
看了上面的例子,您应该注意到了您所定义的变量名。代码中这个长长的全局变量具有一个小写的前缀 ‘g’。它用于说明其为一个全局变量。静态变量则使用前缀 ‘s’ 而不是 ‘g’。尝试养成使用前缀的习惯可以避免许多头痛的事。
如果您和我相似,您可能会想知道我所说的让人头痛的事究竟为何事。请看下面的示例,然后猜测打印的结果是什么。
#include <stdio.h>
//这是一个全局变量,可以在文件中的任何地方进行访问。
static int returnValue = 1;
int main(void)
{
int returnValue = 2;
printf(" The return value of this program is %d\n",returnValue);
return returnValue;
}
如果您太懒而不想尝试该示例,我可以告诉你,其结果为 2。通过命令我们的静态变量为 sReturnValue ,我们可以非常简单的避免这种情况。当存在作用域冲突时,局部变量总是取得优先权。当然,也能够使用这种方式隐藏参数或预声明局部变量,例如 if 语句中的变量会对函数开头声明的变量隐藏。需要注意的是,不要使用函数中声明的变量隐藏其参数。
全局和静态变量非常像厨艺中的佐料:少量使用,它们确实可以使程序很简单;但是使用太多,它们将会起到相反的作用。当无法确定时,不要使用全局变量传递数据 - 而是应该使用函数参数。这样做可以很容易的实现代码的复用。总之,工作之中,多用智慧,减少难度。
常量¶
在 C 和 C++ 中并不是所有的内容都需要修改。有些时候,我们需要保证某些数据不被修改。在其他时候,我们希望使用变量来存储某些随意的数值 - 我们不关心这些值的变化,但是我们需要存储它的目的。当我们编写使用窗口和按钮的 Haiku 程序时,我们将会使用很多这些内容以指定所使用的控件的行为,例如它们的尺寸控制。
第一类常量是预处理程序定义。如果您不大记得第二课中所学,预处理器是编译源代码为可执行程序过程中所使用的第一个工具。预处理器移除注释,包括头文件,以及其他基础的插入和替代文本。它们如下所示:
#define SOMEDEF " I like cheese!"
#define STRACE(x) printf x
格式非常简单:#define 。它们是简单的文本替换,非常类似于字处理程序的替换,把所有的 SOMEDEF 替换为 “I like cheese!”。编译器所关心的就是,您是否输入了 SOMEDEF。您甚至可以使它们看起来像一个函数,如 STRACE。在本课结束,我们会对它有更加详细的讨论,因此敬请期待。
#define 需要谨慎的处理,比数组有过之而不及。在实际应用中,将其写为大写形式比较好,这样可以区别于函数。它们也是无需注意类型的,并且还有一个警告:无论它们如何祈求,无论它们如何哭泣, 不要,请不要在预处理定义之后添加分号 。
// 请不要这样做
#define THISISBAD 1;
这样做将会导致代码出错,但是可能并不会带来真正的问题。这种类型的错误可能会让您觉得自己太过仓促,尽管您并非如此。
常变量是推崇的存储随意值的方法,因为它们具有定义类型。可以在变量声明之前添加 const 关键字来实现。因为它们不允许修改,当其声明时,您总能够看到常量的初始化。
const int someConstIntValue = 3;
指针也可以成为常量,但是如果您不够谨慎,它很快就会让您产生疑惑。const 关键字可以作用于指针的地址,指针本身,或两者皆可。
// 这只是一个整型常量,我们将会将其用于下面的一些指针。
const int someVariable = 5;
// 以下两个都是到整型的指针,我们可以修改指针的地址,
// 但是无法修改它的值,因此它们无需进行初始化。
const int *ptrConstInt;
int const *anotherPtrConstInt;
// 这是一个常量指针。其本身的值可以修改,但是我们无法修改
// 指针所指向的地址。除非我们对它进行初始化,否则毫无用处。
int * const constPtrInt = &foo;
// 以下这些是指向常量值的常量指针。我们无法关于它的任何内容,
// 也就是说,无论用于何种用途,它都必须进行初始化。
const int * const ptrReallyConstInt = &someVariable;
int const * const anotherPtrReallyConstInt = &someVariable;
实在是太混乱了!有一个简单的规则可以解决所有的疑惑。const关键字作用于其左端的元素,如果其左端为空,那么其作用域其右端的元素 。在上述的前两个例子中,每次遇到 const int 或 int const,都意味着,指针本身可以修改,但是指针指向的地址中的值无法修改。每次您遇到 * const ,也就意味着指针的地址被锁定,但是指针地址中的值可以修改。最后两个例子综合了两种技术,使所有内容都为常量,包括地址和数值。如果您对此仍存疑惑,请无需担心太多 - 不仅仅您存在这种情况。这原本就是一个有难度的话题。
外部数据使用:文件操作¶
我们所知道的唯一为我们的程序获取信息的方法是 gets(),并且唯一输出信息的方法是 printf()。printf() 还算可以,但是 gets() 却非常危险,并且无论何时编译器遇到它,都会给我们以警告。其缘由就是,没有办法来强制限定传递给它的字符串中字符的数量。由于输入的字符数量多于给以的数组的容量时,程序很容易就会崩溃。因此我们需要一个更好的解决方案。
程序中信息的输入输出通常是通过使用数据流来实现的。信息流进或流出您的程序。用户的直接输入是一个数据流,并且屏幕也是 - 用于输出的数据流。控制台程序利用流来获取和打印信息,并且它们可以组织到一起:当我们运行称为 bash 的 Terminal 时所使用的程序具有非常难以置信的能力,它能够接受某个程序的输出,并且可以将供其他程序使用,或者将其导入文件,这些直接的通道称之为**管道**。
对于每个程序,都有三种主要的流可用:stdin,标准输入,stdout,标准输出,以及 stderr,错误输出。如果不做修改,那么程序将会通过 stdin 从用户获取输入,跟我们所使用的 gets() 相似,并且发送任何内容到 stdout 或者打印 stderr 到屏幕。
数据可以通过数据流的读入而进入我们的程序,而且可以通过写入数据流从而从我们的程序中输出。有些流是只读的,一些是只写的,还有一些同时允许读和写。stdin 是只读的,因此我们可以使用它用于获取数据,stdout 和 stderr 是只写的,因此我们仅打印它们,如果我们创建一个流来操作文件,我们可以选择其中之一或者同时使用两者。
每个流都有一个标识符,称为**句柄**。在编程时,句柄是随主观而定的 - 但是通常是独立的 - 数值,其用于区别不同种类的对象。对流的操作很简单,就是获取流的句柄,然后调用合适的函数。我们来看一下我们在日常的 C 编程中所使用的几个函数的声明。
int printf(const char *format, ...);
int fprintf(FILE *streamHandle, const char *format, ...);
较早的标准。给定一个定义了打印内容格式的字符串,以及其后恰当数量的参数,然后打印字符串到 stdout 。fprintf() 需要在格式字符串之前指定一个流处理操作 - 使它能够直接“打印”文件 - 否则其和 printf() 一样。当成功时,这两个函数返回打印字符的数量。负数值用于表示运行失败。
int ferror(FILE *streamHandle);
如果由 streamHandle 标识的流一切正常,返回的结果为 0,如果发生错误,那么返回的结果将为其他未指定的值。请确保 streamHandle 不为 NULL - 否则您的程序将会发生段错误。
int feof(FILE *streamHandle);
如果由 streamHandle 标识的流一切正常,返回的结果为 0;如果流到达了文件的结尾,那么返返回的结果将为其他未指定的值。请确保 streamHandle 不为 NULL - 否则您的程序将会发生段错误。
char * fgets(char *array, int arraySize, FILE *streamHandle);
fgets() 是 gets() 的安全版本。它从流句柄里读取文本直到遇到了换行符 (‘n’)或者它读取到的字符数量等于 arraySize,如果程序员产生了错误,将可能会产生段错误。和 gets() 相同,当成功时,fgets()返回 array 指针。如果出现错误,它将会返回 NULL 指针。如果 fgets() 到达了文件的结尾 - 仅从文件而不是 stdin 读取 - 数组中的内容将不会改变,但是将会返回 NULL 指针。无论何时 fgets() 返回了 NULL 指针,请使用 feof() 或者 ferror() 检查是否发生了数据溢出或者发生了某些错误。
FILE * fopen(const char *filePath, const char *mode);
fopen() 以流方式打开文件。如果成功,它将会返回其他函数使用的流句柄,并且最终必须使用 fclose() 进行关闭。模式字符串如下表:
模式字符串 | 功能 |
---|---|
“r” | 打开读取文件。该文件必须存在。 |
“w” | 打开写入文件。如果文件存在,其内容将被擦除,并且它被视为新的空文件。 |
“a” | 打开写入文件。任何写入的数据将添加到文件末尾。如果文件不存在,它将被创建。 |
“r+” | 打开更新文件,同时支持读取和写入。文件必须存在。 |
“w+” | 打开更新文件,同时支持读取和写入,如果文件存在,其内容将被擦除,它被视为新的空文件。 |
“a+” | 打开写入和追加文件。可以从文件任何地方进行读取,但是所有写入的内容将添加到文件末尾。 |
int fclose(FILE *streamHandle);
关闭打开的流句柄。
哇哦,有许多函数哟!比较骇人的部分是,这只是可用函数中很小的一部分。一个程序员必须时刻在学习。一旦他熟悉了一门语言,他总是在学习那些不太熟悉的可用函数,以及新的使用已知函数的方式。最好的学习如何使用不熟悉的函数的方式是使用它们。
让我们查看以下这个打印测试文件到 stdout 并创建需要文件的程序。
#include <stdio.h>
int
FileExists(const char *path)
{
// 该函数通过尝试打开文件来测试文件的存在性。
// 当然还有其它方式可以处理,但是这种方法对
// 我们来说暂时已经足够了。
if (!path)
return -1;
// 尝试打开读取文件
FILE *file = fopen(path, "r");
// 如果文件存在,返回值将会为 1 ,否则为 0。
// 如果打开文件出错,ferror() 将会返回非零结果,
// 如果文件打开正常,则返回 0 。
if (!file || ferror(file) != 0)
returnValue = 0;
else
{
fclose(file);
returnValue = 1;
}
return returnValue;
}
int
MakeTestFile(const char *path)
{
// 在处理字符串时,总是检查 NULL 指针。
if(!path)
return -1;
// 打开文件,如果其存在,则擦除其内容。
FILE *file = fopen(path,"w");
if (!file || ferror(file))
{
// 如果我们不创建文件,我们将会产生不同的错误代码。
// 这样可以让我们知道是否是因为传递 NULL 指针或者存在
// 文件相关的错误而出现问题。
fprintf(stderr, "Couldn't create the file %s\n", path);
return 0;
}
// 用于 stdout,stdin,和 stderr 的流句柄已经进行了定义,
// 因此我们可以直接进行使用,如在上述的 if() 条件,以及
// 下述的传递数据到我们的文件。
fprintf(file, "This is a file.\nThis is only a file.\n"
"Had this been a real emergency, do you think I'd "
"be around to tell you?\n");
fclose(file);
return 1;
}
int
main(void)
{
int returnValue = 0;
// 让我们使用 /boot/home 中的 MyTestFile.txt 测试文件。
const char *filePath = "/boot/home/MyTestFile.txt";
// 如果不存在,则创建测试文件。如果创建出现问题,则释放我们的程序。
if(!FileExist(filePath)
{
returnValue = MakeTestFile(filePath);
if (returnValue != 1)
return returnValue;
}
printf("Printing file %s:\n", filePath);
// 我们到现在这一步,可以安全的打印文件了。
FILE *file = fopen(filePath, "r");
if(!file || ferror(file))
{
fprintf(stderr, "Couldn't print the file %s\n", filePath);
return 0;
}
char inString[1024];
// 当 fgets 到达文件的末尾时,它将会返回一个 NULL 指针。因此这个小循环
// 将会打印整个文件,在末尾时退出。
while (fgets(inString, 1024, file))
fprintf(stdout, "%s", inString);
fclose(file);
return 0;
}
哎哟!这就是我们最长的代码示例了,也是最接近“真实”程序的代码。一些习惯,如 if (!file) ,对于 C 和 C++ 编程非常普遍,所以请不要见怪。阅读了该示例中的代码,请确保了解每行代码的用意。
在代码风格方面也有一些小的改变。对代码风格的关注是 Haiku 环境的一个怪癖。特别的是,Haiku 开发者尤其挑剔代码,始终遵循 OpenTracker 中的代码风格。风格需要部分的关注,但是好的代码风格也有助于代码调试和避免错误。不好的代码风格可以使之非常困难。在接下来的章节中,我们将要使用的风格可能会有别于官方的 Haiku 代码规范,但是它和它们非常的接近。
错误查找¶
错误 #1¶
代码:
#include <stdio.h>
#include <string.h>
char *ReverseString(const char *string)
{
// 该函数对字符串重新倒装排序。
// 如,abcdef -> fedcba
if (!string)
return NULL;
int length = strlen(string);
int count = length / 2;
for (int 1 = 0; i < count; i++)
{
char temp = string[length - i];
string[length - i] = string[i];
string[i] = temp;
}
return string;
}
int main(void)
{
char inString[1024];
printf("Type a string to reverse:");
gets(inString);
printf("The reversed string is %s\n", ReverseString(inString);
return 0;
}
错误:
foo.cpp: In function 'char* ReverseString(const char*)';
foo.cpp:18: error: assignment of read-only location '*(string + ((unsigned int)
((length + 0x00000000000000001) - i)))'
foo.cpp:19: error: assignment of read-only location '*(string + ((unsigned int)
i))'
foo.cpp:22: error: invalid conversion from 'const char*' to 'char*'