C++基础教程简介
怎样使用本教程
读者范围
本教程面向所有希望学习C++语言的读者。如果读者有其他编程语言背景或计算机相关基本知识可以帮助更好的理解教程内容,但这并非必须条件。 对于C语言熟悉的读者可将前三章(1.1 到 3.4)当作复习,因为这部分内容主要介绍C++中的C部分。
第四章讲述面向对象编程。
第五章主要介绍ANSI-C++标准中的新增的功能。
本教程结构
教程共分6章,每章分若干小节。你可以直接从主目录进入任意小节,并循每页底部的链接向后浏览。
很多小节含有一页例题介绍该章节主要知识点的使用。建议在进入下一章学习之前最好先阅读这些例题,理解每行代码。
学习和练习一种编程语言的最好办法是自己修改
书
关于书的成语关于读书的排比句社区图书漂流公约怎么写关于读书的小报汉书pdf
中例题程序,设法在程序中增加新的功能。不要不敢修改这些例题程序,这正是学习的方法。
兼容性备注
ANSI-C++标准近几年来被接受为国际标准。尽管C++语言从十九世纪80年代即存在,ANSI-C++在1997年才被发
表
关于同志近三年现实表现材料材料类招标技术评分表图表与交易pdf视力表打印pdf用图表说话 pdf
。因此很多编译器不支持ANSI-C++中的部分新功能,特别是那些在此标准发表前即被发布的编译器。
在本教程中,那些ANSI-C++中新增的而老一代C++编译器大多不支持概念将备用如下标志标出:
new in ANSI C++
同样对于C和C++在实现上有明显不同的概念,将备用如下标志标出:
different implementation in C and C++
编译器
本教程中所有例题程序均为console程序(控制台程序)。此类程序以文本形式与用户交换信息,显示结果。
所有C++编译器均支持console程序的编译。要了解更多关于如何编译的说明,请查询你的编译器用户使用手册。
第一章 C++ 基础知识 (Basics of C++) 1.1 C++程序结构 (Structure of a program) 下面我们从一个最简单的程序入手看一个C++程序的组成结构。 // my first program in C++ Hello World!
#include
using namespace std;
int main() {
cout << “Hello World!”;
return 0;
}
上面左侧显示了我们的第一个程序的源代码,代码文件名称为
hellowworld.cpp。右边显示了程序被编译执行后的输出结果。编辑和编译一个程序的方法取决于你用的是什么编译器,根据它是否有图形化的界面及版本的不同,编译方法也有可能不同,具体请参照你所使用的编译器的使用说明。 以上程序是多数初学者学会写的第一个程序,它的运行结果是在屏幕上打出”Hello World!”这句话。 虽然它可能是C++可写出的最简单的程序之一,但其中已经包含了每一个C++程序的基本组成结构。 下面我们就逐个分析其组成结构的每一部分:
// my first program in C++ 这是注释行。所有以两个斜线符号(//)开始的程序行都被认为是注释行,这些注释行是程序员写在程序源代码内,用来对程序作简单解释或描述的, 对程序本身的运行不会产生影响。在本例中, 这行注释对本程序是什么做了一个简要的描述。
# include < iostream.h > 以#标志开始的句子是预处理器的指示语句。它们不是可执行代码,只是对编译器作出指示。在本例中这个句子# include < iostream.h > 告诉编译器的预处理器将输入输出流的标准头文件(iostream.h)包括在本程序中。这个头文件包括了C++中定义的基本标准输入-输出程序库的声明。此处它被包括进来是因为在本程序的后面部分中将用到它的功能。 using namespace std;
C++标准函数库的所有元素都被声明在一个名空间中,这就是std名空间。因此为了能够访问它的功能,我们用这条语句来表达我们将使用标准名空间中定义的元素。这条语句在使用标准函数库的C++程序中频繁出现,本教程中大部分代码例子中也将用到它。
int main()
这一行为主函数(main function)的起始声明。main function是所有C++程序的运行的起始点。 不管它是在代码的开头,结尾还是中间 – 此函数中的代码总是在程序开始运行时第一个被执行。并且,由于同样的原因,所有C++程序都必须有一个main function。 main 后面跟了一对圆括号(),表示它是一个函数。C++中所有函数都跟有一对圆括号 (),括号中可以有一些输入参数。如例题中显示,主函数(main function)的内容紧跟在它的声明之后,由花括号 ({})括起来。
cout << “Hellow World!”;
这个语句在本程序中最重要。 cout 是C++中的标准输出流(通常为控制台,即屏幕),这句话把一串字符串(本例中为”Hello World”)插入输出流(控制台输出)中。cout 在的声明在头文件iostream.h中,所以要想使用cout 必须将该头文件包括在程序开始处。 注意这个句子以分号(;)结尾 。分号标示了一个语句的结束,C++的每一个语句都必须以分号结尾。 (C++ 程序员最常犯的错误之一就是忘记在语句末尾写上分号) 。 return 0;
返回语句(return) 引起主函数 main()执行结束,并将该语句后面所跟代码(在本例中为0) 返回。这是在程序执行没有出现任何错误的情况下最常见的程序结束方式。在后面的例子中你会看到所有C++程序都以类似的语句结束。
你可能注意到并不是程序中的所有的行都会被执行。程序中可以有注释行(以//开头),有编译器预处理器的指示行(以#开头),然后有函数的声明(本例中main函数),最后是程序语句(例如调用cout <<),最后这些语句行全部被括在主函数的花括号({})内。 本例中程序被写在不同的行中以方便阅读。其实这并不是必须的。例如,以下程序 int main ()
{
cout << " Hello World ";
return 0;
}
也可以被写成:
int main () { cout << " Hello World "; return 0; }
以上两段程序是完全相同的。
在C++中,语句的分隔是以分号(;)为分隔符的。分行写代码只是为了更方便人阅读。 以下程序包含更多的语句:
// my second program in C++ Hello World! I'm a C++ program
#include
int main ()
{
cout << "Hello World! ";
cout << "I'm a C++ program";
return 0;
}
在这个例子中,我们在两个不同的语句中调用了cout << 函数两次。再一次说明分行写程序代码只是为了我们阅读方便,因为这个main 函数也可以被写为以下形式而没有任何问题:
int main () { cout << " Hello World! "; cout << " I'm to C++ program "; return 0; }
为方便起见,我们也可以把代码分为更多的行来写:
int main ()
{
cout <<
"Hello World!";
cout
<< "I'm a C++ program";
return 0;
}
它的运行结果将和上面的例子完全一样。
这个规则对预处理器指示行(以#号开始的行)并不适用,因为它们并不是真正的语句。它们由预处理器读取并忽略,并不会生成任何代码。因此他们每一个必须单独成行,末尾不需要分号(;)
注释 (Comments)
注释(comments)是源代码的一部分,但它们会被编译器忽略。它们不会生成任何执行代码。 使用注释的目的只是使程序员可以在源程序中插入一些说明解释性的内容。 C++ 支持两中插入注释的方法:
// line comment
/* block comment */
第一种方法为行注释,它告诉编译器忽略从//开始至本行结束的任何内容。第二种为块注释(段注释),告诉编译器忽略在/*符号和*/符号之间的所有内容,可能包含多行内容。
在以下我们的第二个程序中,我们插入了更多的注释。
/* my second program in C++ Hello World! I'm a C++ program with more comments */
#include
int main ()
{
cout << "Hello World! "; // says Hello World!
cout << "I'm a C++ program"; // says I'm a C++ program
return 0;
}
如果你在源程序中插入了注释而没有用//符号或/*和*/符号,编译器会把它们当成C++的语句,那么在编译时就会出现一个或多个错误信息。
1.2 变量,数据类型,常量 (Variables. Data types.
Constants)
你可能觉得这个“Hellow World”程序用处不大。我们写了好几行代码,编译,然后执行生成的程序只是为了在屏幕上看到一句话。的确,我们直接在屏幕上打出这句话会更快。但是编程并不仅限于在屏幕上打出文字这么简单的工作。为了能够进一步写出可以执行更有用的任务的程序,我们需要引入变量(variable)这个的概念。
让我们设想这样一个例子,我要求你在脑子里记住5这个数字,然后再记住2这个数字。你已经存储了两个数值在你的记忆里。现在我要求你在我说的第一个数值上加1,你应该保留6 (即5+1)和2在你的记忆里。现在如果我们将两数相减可以得到结果4。
所有这些你在脑子里做的事情与计算机用两个变量可以做的事情非常相似。同样的处理过程用C++来表示可以写成下面一段代码:
a = 5;
b = 2;
a = a + 1;
result = a - b;
很明显这是一个很简单的例子,因为我们只用了两个小的整数数值。但是想一想
你的电脑可以同时存储成千上万这样的数值,并进行复杂的数学运算。
因此,我们可以将变量(variable)定义为内存的一部分,用以存储一个确定的
值。
每一个变量 (variable)需要一个标识,以便将它与其他变量相区别,例如,在
前面的代码中,变量标识是a, b, 和result。我们可以给变量起任何名字,只
要它们是有效的标识符。
标识(Identifiers)
有效标识由字母(letter),数字(digits)和下划线 ( _ )组成。标识的长度没有
限制,但是有些编译器只取前32个字符(剩下的字符会被忽略)。
空格(spaces),标点(punctuation marks)和符号(symbols) 都不可以出现在标
识中。 只有字母(letters),数字(digits) 和下划线(_)是合法的。并且变量标
识必须以字母开头。标识也可能以下划线(_)开头,但这种标识通常是保留给
为外部连接用的。标识不可以以数字开头。
必须注意的另一条规则是当你给变量起名字时不可以和C++语言的关键字或你
所使用的编译器的特殊关键字同名,因为这样与这些关键字产生混淆。例如,以
下列出标准保留关键字,他们不允许被用作变量标识名称:
asm, auto, bool, break, case, catch, char, class, const, const_cast, continue, default,
delete, do, double, dynamic_cast, else, enum, explicit, extern, false, float, for,
friend, goto, if, inline, int, long, mutable, namespace, new, operator, private,
protected, public, register, reinterpret_cast, return, short, signed, sizeof, static,
static_cast, struct, switch, template, this, throw, true, try, typedef, typeid,
typename, union, unsigned, using, virtual, void, volatile, wchar_t, while
另外,不要使用一些操作符的替代表示作为变量标识,因为在某些环境中它们可
能被用作保留词:
and, and_eq, bitand, bitor, compl, not, not_eq, or, or_eq, xor, xor_eq
你的编译器还可能包含一些特殊保留词,例如许多生成16位码的编译器(比如一些DOS编译器)把 far, huge和 near也作为关键字。
非常重要:C++语言是“ 大小写敏感”(“case sensitive”) 的,即同样的名
字字母大小写不同代表不同的变量标识。因此,例如变量RESULT,变量result和变量Result分别表示三个不同的变量标识.
基本数据类型(Fundamental Data types)
编程时我们将变量存储在计算机的内存中,但是计算机要知道我们要用这些变量存储什么样的值,因为一个简单的数值,一个字符,或一个巨大的数值在内存所占用的空间是不一样的。
计算机的内存是以字节(byte)为单位组织的。一个字节(byte)是我们在C++中能够操作的最小的内存单位。一个字节(byte)可以存储相对较小数据:一个单个的字符或一个小整数(通常为一个0到255之间的整数)。但是计算机可以同时操作处理由多个字节组成复杂数据类型,比如长整数(long integers)和小数(decimals)。以下列表总结了现有的C++基本数据类型,以及每一类型所能存储的数据范围:
数据类型(DATA TYPES)
名称 字节数 描述 范围
字符(character)或整数(integer ), 8有符号(signed): -128 到 127 1 char 位(bits)长无符号(unsigned): 0 到 255
有符号(signed): -32768 到 short int 2 短整数(integer )16位(bits)长 32767 (short) 无符号(unsigned): 0 到 65535
有符号(signed):-2147483648 到
2147483647 long int 4 长整数(integer )32位(bits)长 无符号(unsigned): 0 到 (long)
4294967295
有符号(signed): -2147483648 到
2147483647 4 整数(integer) int 无符号(unsigned): 0 到
4294967295
4 浮点数(floating point number) 3.4e + / - 38 (7 个数字(7digits)) float
双精度浮点数(double precision floating 8 1.7e + / - 308 (15 digits) double point number)
long 长双精度浮点数(long double precision 8 1.7e + / - 308 (15 digits) double floating point number)
布尔Boolean值。它只能是真(true)或假bool 1 true 或 false (false)两值之一。
宽字符(Wide character) 。这是为存储两
wchar_t 2 字节(2 bytes) 长的国际字符而设计的类一个宽字符(1 wide characters)
型。
* 字节数一列和范围一列可能根据程序编译和运行的系统不同而有所不同。这里列出的数值是多数32位系统的常用数据。对于其他系统,通常的说法是整型(int)具有根据系统结构建议的自然长度(即一个字one word的长度),而4中整型数据char, short, int, long的长度必须是递增的,也就是说按顺序每一类型必须大于等于其前面一个类型的长度。同样的规则也适用于浮点数类型float, double和 long double,也是按递增顺序。
除以上列出的基本数据类型外,还有指针(pointer)和void 参数表示类型,我们将在后面看到。
变量的声明(Declaration of variables)
在C++中要使用一个变量必须先声明(declare)该变量的数据类型。声明一个新变量的语法是写出数据类型标识符(例如int, short, float...) 后面跟一个有效的变量标识名称。例如:
int a;
float mynumber;
以上两个均为有效的变量声明(variable declaration)。第一个声明一个标识为a 的整型变量(int variable),第二个声明一个标识为mynumber 的浮点型变量(float variable)。声明之后,我们就可以在后面的程序中使用变量a和 mynumber 了。
如果你需要声明多个同一类型的变量,你可以将它们缩写在同一行声明中,在标识之间用逗号(comma) 分隔。例如:
int a, b, c;
以上语句同时定义了a、b、c 3个整型变量,它与下面的写法完全等同: int a;
int b;
int c;
整型数据类型 (char, short, long 和 int) 可以是有符号的(signed)或无符号的(unsigned ),这取决于我们需要表示的数据范围。有符号类型(signed)可以表示正数和负数,而无符号类型(unsigned)只能表示正数和0。在定义一个
整型数据变量时可以在数据类型前面加关键字 signed 或 unsigned 来声明数据的符号类型。例如:
unsigned short NumberOfSons;
signed int MyAccountBalance;
如果我们没有特别写出signed或 unsigned,变量默认为signed,因此以上第二个声明我们也可以写成:
int MyAccountBalance;
因为以上两种表示方式意义完全一样,因此我们在源程序通常省略关键字signed 。
唯一的例外是字符型(char)变量,这种变量独立存在,与signed char 和 unsigned char型均不相同。
short 和 long 可以被单独用来表示整型基本数据类型,short 相当于 short
long 相当于 long int。也就是说 short year; 和 short int year; 两int,
种声明是等价的。
最后,signed 和 unsigned 也可以被单独用来表示简单类型,意思分别同signed int 和 unsigned int 相同,即以下两种声明互相等同: unsigned MyBirthYear;
unsigned int MyBirthYear;
下面我们就用C++代码来解决在这一节开头提到的记忆问题,来看一下变量定义是如何在程序中起作用的。
// operating with variables 4
#include
using namespace std;
int main ()
{
// declaring variables:
int a, b;
int result;
// process:
a = 5;
b = 2;
a = a + 1;
result = a - b;
// print out the result:
cout << result;
// terminate the program:
return 0;
}
如果以上程序中变量声明部分有你不熟悉的地方,不用担心,我们在后面的章节中很快会学到这些内容。
变量初始化(Initialization of variables)
当一个本地变量( local variable)被声明时,它的值默认为未定(undetermined)。但你可能希望在声明变量的同时赋给它一个具体的值。要想达到这个目的,需要对变量进行初始化。C++中有两种初始化方法: 第一种,又叫做类C (c-like) 方法,是在声明变量的时候加上一个等于号,并在后面跟上想要的数值:
type identifier = initial_value ; 例如,如果我们想声明一个叫做a的int变量并同时赋予它0这个值,我们可以这样写:
int a = 0;
另外一种变量初始化的方法,又叫做构造函数(constructor)初始化, 是将初始值用小括号(parenthesis ())括起来:
type identifier (initial_value) ; 例如:
int a (0);
在C++.中以上两种方法都正确并且两者等同 。
变量的范围(Scope of variables)
所有我们要使用的变量都必须事先声明过。,和,++语言的一个重要区别是,在C++语言中我们可以在源程序中任何地方声明变量,甚至可以在两个可执行(excutable)语句的中间声明变量,而不象在C语言中变量声明只能在程序的开头部分。
然而,我们还是建议在一定程度上遵循C语言的习惯来声明变量,因为将变量声明放在一处对debug程序有好处。因此,传统的C语言方式的变量声明就是把变量声明放在每一个函数(function)的开头(对本地变量local variable)或直接放在程序开头所有函数(function)的外面(对全局变量global variable)。
全局变量Global
variables 可以在
程序中任何地方任
何函数(function)
中被引用,只要是在
变量的声明之后。
本地变量local
variables 的作用
范围被局限在声明
它的程序范围内。如
果它们是在一个函
数的开头被声明的
(例如main函数),
它们的作用范围就是整个main函数。在左图的例子中,这就意味着如果在main函数外还另有一个函数,main函数中声明的本地变量(Age, ANumber, AnotherOne) 不能够被另一个函数使用,反之亦然。
在C++中,本地变量(local variable)的作用范围被定义在声明它的程序块内(一个程序块是被一对花括号(curly brackets{})括起来的一组语句)。如果变量是在一个函数(function)中被声明的,那么它是一个函数范围内的变量,如果变量是在一个循环中(loop)中被声明的,那么它的作用范围只是在这个循环(loop)之中,以此类推。
除本地和全局范围外,还有一种外部范围,它使得一个变量不仅在同一源程序文件中可见,而且在其他所有将被链接在一起的源文件中均可见。
常量:字(Constants: Literals)
一个常量(constant)是一个有固定值的表达式。常量(constant)可以被分为整数(Integer Numbers), 浮点数(Floating-Point Numbers),字符(Characters)和字符串(Strings)。
整数(Integer Numbers)
1776
707
-273
他们是整型常数,表示十进制整数值。注意表示整型常数时我们不需要些引号(quotes ("))或任何特殊字符。毫无疑问它是个常量:任何时候当我们在程序中写1776,我们指的就是1776这个数值。
除十进制整数另外, C++还允许使用八进制(octal numbers)和十六进制(hexadecimal numbers)的字常量(literal constants)。如果我们想要表示一个八进制数,我们必须在它前面加上一个0字符(zero character),而表示十六进制数我们需要在它前面加字符0x (zero, x)。例如以下字常量(literal constants)互相等值:
75 // 十进制 decimal
0113 // 八进制 octal
0x4b // 十六进制 hexadecimal
所有这些都表示同一个整数: 75 (seventy five) ,分别以十进制数,八进制数和十六进制数表示。
[备注:你可以在文章Numerical radixes中看到更多关于十六进制和八进制表示方式的信息。]
浮点数(Floating Point Numbers) 浮点数以小数(decimals)和,或指数幂( exponents)的形式表示。它们可以包括一个小数点,一个e字符(表示"by ten at the Xth height",这里X是后面跟的整数值) ,或两者都包括。
3.14159 // 3.14159
6.02e23 // 6.02 x 1023
1.6e-19 // 1.6 x 10-19
3.0 // 3.0
以上是包含小数的以,++表示的,个有效数值。第一个是PI,第二个是Avogadro数之一,第三个是一个电子(electron)的电量(electric charge)(一个极小的数值) – 所有这些都是近似值。最后一个是浮点数字常量表示数3。 字符和字符串(Characters and strings)
此外还有非数字常量,例如:
'z'
'p'
"Hello world"
"How do you do?"
前两个表达式表示单独的字符(character),后面两个表示由若干字符组成的字符串(string) 。注意在表示单独字符的时候,我们用单引号(single quotes
(')),在表示字符串或多于一个字符的时候我们用双引号(double quotes ("))。 当以常量方式表示单个字符和字符串时,必须写上引号以便把他们和可能的变量标识或保留字区分开,注意以下例子:
x
'x'
x 指一个变量名称为 x, 而'x'指字符常量'x'。
字符常量和字符串常量各有特点,例如escape codes,这些是除此之外无法在源程序中表示的特殊的字符,例如换行符 newline (\n) 或跳跃符tab (\t)。所有这些符号前面要加一个反斜杠inverted slash (\)。这里列出了这些escape codes:
换行符newline \n
回车carriage return \r
跳跃符tabulation \t
垂直跳跃vertical tabulation \v
backspace \b
page feed \f
警告alert (beep) \a
单引号single quotes (') \'
双引号double quotes (") \"
问号question (?) \?
反斜杠inverted slash (\) \\
例如:
'\n'
'\t'
"Left \t Right"
"one\ntwo\nthree"
另外你可以数字ASCII 码表示一个字符,这种表示方式是在反斜杠(\)之后加以8进制数或十六进制数表示的ASCII 码。在第一种(八进制octal)表示中,数字必需紧跟反斜杠(例如\23或\40),第二种(十六进制hexacedimal),必须在数
(例如\x20或\x4A)。 字之前写一个x字符
[关于此类escape code的更多信息,请参阅文件ASCII Code]。 如果每一行代码以反斜杠inverted slash (\)结束,字符串常量可以分多行代码表示:
"string expressed in \
two lines"
你还可以将多个被空格blankspace、跳跃符tabulator、换行符newline或其他有效空白符号分隔开的字符串常量连接在一起:
"we form" "a single" "string" "of characters" 定义常量Defined constants (#define) 使用预处理器指令#define,你可以将那些你经常使用的常量定义为你自己取的名字而不需要借助于变量。它的格式是:
#define identifier value
例如:
#define PI 3.14159265
#define NEWLINE '\n'
#define WIDTH 100
以上定义了三个常量。一旦做了这些声明,你可以在后面的程序中使用这些常量,就像使用其它任何常量一样,例如:
circle = 2 * PI * r;
cout << NEWLINE;
实际上编译器在遇到#define指令的时候做的只是把任何出现这些 常量名(在前面的例子中为PI, NEWLINE或WIDTH)的地方替换成他们被定义为的代码(分别为3.14159265, '\n'和100)。因此,由#define定义的常量被称为宏常量macro constants。
#define 指令不是代码语句,它是预处理器指令,因此指令行末尾不需要加分号semicolon (;) 。如果你在宏定义行末尾加了分号(;) ,当预处理器在程序中做常量替换的时候,分号也会被加到被替换的行中,这样可能导致错误。 声明常量declared constants (const) 通过使用const前缀,你可以定义指定类型的常量,就像定义一个变量一样: const int width = 100;
const char tab = '\t';
const zip = 12440;
如果没有指定类型(如上面最后例子中最后一行),编译器会假设常量为整型int。
1.3 操作符/运算符(Operators)
前面已经学习了变量和常量,我们可以开始操作他们。C++提供一系列的运算符,它们是一组关键字或非字母但是在所有键盘上都有的符号。运算符是C++语言的基础,所以非常重要。
你不需要背下所有这一小节的内容,这些细节知识仅供你以后需要时参考 。 赋值Assignation (=)
赋值运算符的功能是将一个值赋给一个变量。
a = 5;
将整数5赋给变量a。= 运算符左边的部分叫做lvalue (left value),右边的部分叫做rvalue (right value)。lvalue 必须是一个变量,而右边的部分可以是一个常量,一个变量,一个运算(operation)的结果或是前面几项的任意组合。 有必要强调赋值运算符永远是将右边的值赋给左边,永远不会反过来。 a = b;
将变量b (rvalue)的值赋给变量a (lvalue),不论a当时存储的是什么值。同时考虑到我们只是将b的数值赋给a,以后如果b的值改变了并不会影响到a的值.
例如:如果我们使用以下代码(变量值的变化显示在绿色注释部分): int a, b; // a:? b:?
a = 10; // a:10 b:?
b = 4; // a:10 b:4
a = b; // a:4 b:4
b = 7; // a:4 b:7
以上代码结果是a的值为4, b的值为7。最后一行中b的值被改变并不会影响到a,虽然在此之前我们声明了a = b; (从右到左规则right-to-left rule)。 C++拥有而其他语言没有的一个特性是赋值符 (=) 可以被用作另一个赋值符的rvalue (或rvalue的一部分) 。例如:
a = 2 + (b = 5);
等同于:
b = 5;
a = 2 + b;
它的意思是:先将5赋给变量b,然后把前面对b的赋值运算的结果(即5)加上2再赋给变量a,这样最后a中的值为7。因此,下面的表达式在C++中也是正确的:
a = b = c = 5; //将5同时赋给3个变量a, b和c。
数学运算符Arithmetic operators ( +, -, *, /, % ) C++语言支持的5种数学运算符为:
, + 加addition
, - 减subtraction
, * 乘multiplication
, / 除division
, % 取模module
加减乘除运算想必大家都很了解,它们和一般的数学运算符没有区别。 唯一你可能不太熟悉的是用百分号(%)表示的取模运算(module)。取模运算是取两个整数相除的余数。例如,如果我们写a = 11 % 3;,变量a的值将会为结果2,因为2是11除以3的余数。
组合运算符Compound assignation operators (+=, -=, *=,
/=, %=, >>=, <<=, &=, ^=, |=) C++以书写简练著称的一大特色就是这些组合运算符compound assignation
operators (+=, -=, *= 和 /= 及其他) ,这些运算符使得只用一个基本运算符就可改写变量的值:
value += increase; 等同于 value = value + increase; a -= 5; 等同于 a = a - 5;
a /= b; 等同于 a = a / b;
price *= units + 1; 等同于 price = price * (units + 1);
其他运算符以此类推。
递增和递减Increase and decrease 书写简练的另一个例子是递增(increase)运算符 (++)和递减(decrease) 运
--)。它们使得变量中存储的值加1或减1。它们分别等同于+=1和-=1。因算符(
此:
a++;
a+=1;
a=a+1;
在功能上全部等同,即全部使得变量a的值加1。
它的存在是因为最早的C编译器将以上三种表达式的编译成不同的机器代码,不同的机器代码运行速度不一样。现在,编译器已经基本自动实行代码优化,所以以上三种不同的表达方式编译成的机器代码在实际运行上已基本相同。 这个运算符的一个特点是它既可以被用作prefix前缀,也可以被用作后缀suffix,也就是说它既可以被写在变量标识的前面(++a),也可以被写在后面(a++)。虽然在简单表达式如a++或++a中,这两种写法代表同样的意思,但当递增increase或递减decrease的运算结果被直接用在其它的运算式中时,它们就代表非常不同的意思了:当递增运算符被用作前缀prefix (++a) 时,变量a的值线增加,然后再计算整个表达式的值,因此增加后的值被用在了表达式的计算中;当它被用作后缀suffix (a++)时,变量a的值在表达式计算后才增加,因此a在增加前所存储的值被用在了表达式的计算中。注意以下两个例子的不同:
Example 1 Example 2
B=3; B=3;
A=++B; A=B++;
// A is 4, B is 4 // A is 3, B is 4
在第一个例子中,B在它的值被赋给A之前增加1。而在第二个例子中B原来的值3被赋给 A然后B的值才加1变为4。
关系运算符Relational operators ( ==, !=, >, <, >=, <= )
我们用关系运算符来比较两个表达式。如ANSI-C++ 标准中指出的,关系预算的结果是一个bool值,根据运算结果的不同,它的值只能是真true或false。 例如我们想通过比较两个表达式来看它们是否相等或一个值是否比另一个的值大。以下为C++的关系运算符:
== 相等Equal
!= 不等Different
> 大于Greater than
< 小于Less than
>= 大于等于Greater or equal than
<= 小于等于Less or equal than
下面你可以看到一些实际的例子:
(7 == 5) 将返回false.
(5 > 4) 将返回true.
(3 != 2) 将返回true.
(6 >= 6) 将返回true.
(5 < 5) 将返回false.
当然,除了使用数字常量,我们也可以使用任何有效表达式,包括变量。假设有a=2, b=3和c=6,
(a == 5) 将返回false.
(a*b >= c) 将返回true 因为它实际是(2*3 >= 6)
(b+4 > a*c) 将返回false因为它实际是(3+4 > 2*6)
((b=2) == a) 将返回true.
注意:运算符= (单个等号)不同于运算符== (双等号)。第一个是赋值运算符(将等号右边的表达式值赋给左边的变量);第二个(==)是一个判断等于的关系运算符,用来判断运算符两边的表达式是否相等。因此在上面例子中最后一个表达式((b=2) == a),我们首先将数值2赋给变量b,然后把它和变量a进行比较。因为变量a中存储的也是数值2,所以整个运算的结果为true。
-C++标准出现之前的许多编译器中,就像C语言中,关系运算并不返回在ANSI
值为真true或假false的bool值,而是返回一个整型数值最为结果,它的数值可以为0,代表"false"或一个非0数值(通常为1)来代表"true"。
逻辑运算符Logic operators ( !, &&, || ) 运算符 ! 等同于boolean 运算NOT (取非),它只有一个操作数(operand),写在它的右边。它做的唯一工作就是取该操作数的反面值,也就是说如果操作数值为真true,那么运算后值变为假false,如果操作数值为假false,则运算结果为真true。它就好像是说取与操作数相反的值。例如:
!(5 == 5) 返回false,因为它右边的表达式(5 == 5)为真true.
!(6 <= 4) 返回true因为(6 <= 4)为假false.
!true 返回假false.
!false 返回真true.
逻辑运算符&&和||是用来计算两个表达式而获得一个结果值。它们分别对应逻辑运算中的与运算AND 和或运算OR。它们的运算结果取决于两个操作数(operand)的关系:
第一个操作数 第二个操作数 结果 结果
a b a && b a || b
true true true true
true false false true
false true false true
false false false false
例如 :
( (5 == 5) && (3 > 6) ) 返回false ( true && false ). ( (5 == 5) || (3 > 6)) 返回true ( true || false ).
条件运算符Conditional operator ( ? ) 条件运算符计算一个表达式的值并根据表达式的计算结果为真true或假false而返回不同值。它的格式是:
condition ? result1 : result2 (条件,返回值1:返回值2)
如果条件condition 为真true,整个表达式将返回esult1,否则将返回result2。 7==5 ? 4 : 3 返回3,因为7不等于5.
7==5+2 ? 4 : 3 返回4,因为7等于5+2.
5>3 ? a : b 返回a,因为5大于3.
a>b ? a : b 返回较大值,a 或b.
位运算符Bitwise Operators ( &, |, ^, ~, <<, >> )
位运算符以比特位改写变量存储的数值,也就是改写变量值的二进制表示: op asm Description
& AND 逻辑与 Logic AND
| OR 逻辑或Logic OR
^ XOR 逻辑异或Logical exclusive OR
~ NOT 对1取补(位反转)Complement to one (bit inversion)
<< SHL 左移Shift Left
>> SHR 右移Shift Right
变量类型转换运算符Explicit type casting operators
变量类型转换运算符可以将一种类型的数据转换为另一种类型的数据。在写C++中有几种方法可以实现这种操作,最常用的一种,也是与C兼容的一种,是在原转换的表达式前面加用括号()括起的新数据类型:
int i;
float f = 3.14;
i = (int) f;
以上代码将浮点型数字3.14转换成一个整数值(3)。这里类型转换操作符为(int)。在C++中实现这一操作的另一种方法是使用构造函数constructor 的形式:在要转换的表达式前加变量类型并将表达式括在括号中: i = int ( f );
以上两种类型转换的方法在C++中都是合法的。另外ANSI-C++针对面向对象编程(object oriented programming)增加了新的类型转换操作符 (参考Section 5.4, Advanced class type-casting).
sizeof()
这个运算符接受一个输入参数,该参数可以是一个变量类型或一个变量自己,返回该变量类型(variable type) 或对象(object)所占的字节数: a = sizeof (char);
这将会返回1给a,因为char是一个常为1个字节的变量类型。 sizeof返回的值是一个常数,因此它总是在程序执行前就被固定了。
其它运算符
在本教程后面的章节里我们将看到更多的运算符,比如指向指针的运算或面向对象编程特有的运算,等等,我们会在它们各自的章节里进行详细讨论。
运算符的优先度Priority of operators 当多个操作数组成复杂的表达式时,我们可能会疑惑哪个运算先被计算,哪个后被计算。例如以下表达式:
a = 5 + 7 % 2
我们可以怀疑它实际上表示:
a = 5 + (7 % 2) 结果为6,还是 a = (5 + 7) % 2 结果为0? 正确答案为第一个,结果为6。每一个运算符有一个固定的优先级,不仅对数学运算符(我们可能在学习数学的时候已经很了解它们的优先顺序了),所有在C++中出现的运算符都有优先级。从最从最高级到最低级,运算的优先级按下表排列:
优先级 运算符 描述 结合方向 Priority Operator Description Associativity 1 :: 范围scope Left 2 () [ ] -> . sizeof Left
++ -- 递增/递减increment/decrement
~ 求补Complementto one (bitwise)
! 取非unary NOT
3 Right 指针Reference 和取地址Dereference & * (pointers)
(type) 数据类型转换Type casting
+ - Unary less sign
4 * / % 数学运算符arithmetical operations Left 5 + - 数学运算符arithmetical operations Left 6 << >> 位移bit shifting (bitwise) Left 7 < <= > >= 关系运算符Relational operators Left 8 == != 关系运算符Relational operators Left 9 & ^ | 位操作Bitwise operators Left 10 && || 逻辑运算符Logic operators Left 11 ?: 条件Conditional Right
= += -= *= /= %= >>= <<= 12 赋值运算符Assignation Right &= ^= |=
13 , 逗号Comma, 分隔号Separator Left 结合方向Associativity定义了当有同优先级的多个运算符在一起时,哪一个必须被首先运算,最右边的还是最左边的。
所有这些运算符的优先级顺序可以通过使用括号parenthesis signs (和)来控制,而且更易读懂,例如以下例子:
a = 5 + 7 % 2;
根据我们想要实现的计算的不同,可以写成:
a = 5 + (7 % 2); 或者
a = (5 + 7) % 2;
所以如果你想写一个复杂的表达式而不敢肯定各个运算的执行顺序,那么就加上括号。这样还可以使代码更易读懂。
1.4 控制台交互(Communication through
console)
控制台(console)是电脑的最基本交互接口,通常包括键盘(keyboard)和屏幕(screen)。键盘通常为标准输入设备,而 屏幕为标准输出设备。 在C++的iostream函数库中,一个程序的标准输入输出操作依靠两种数据流:cin 给输入使用和cout给输出使用。另外,cerr和clog也已经被实现――它们是两种特殊设计的数据流专门用来显示出错信息。它们可以被重新定向到标准输出设备或到一个日志文件(log file)。
因此cout (标准输出流)通常被定向到屏幕,而cin (标准输入流)通常被定向到键盘。
通过控制这两种数据流,你可以在程序中与用户交互,因为你可以在屏幕上显示输出并从键盘接收用户的输入。
输出Output (cout)
输出流cout与重载(overloaded)运算符<<一起使用:
cout << "Output sentence"; // 打印Output sentence到屏幕上
cout << 120; // 打印数字 120 到屏幕上
cout << x; // 打印变量 x 的值到屏幕上
运算符<<又叫插入运算符(insertion operator) 因为它将后面所跟的数据插入到它前面的数据流中。在以上的例子中,字符串常量Output sentence,数字常量120和变量x先后被插入输出流cout中。注意第一句中字符串常量是被双引
号引起来的。每当我们使用字符串常量的时候,必须用引号把字符串引起来,以便将它和变量名明显的区分开来。例如,下面两个语句是不同的:
cout << "Hello"; // 打印字符串Hello到屏幕上
cout << Hello; // 把变量Hello存储的内容打印到屏幕上
插入运算符insertion operator (<<)可以在同一语句中被多次使用: cout << "Hello, " << "I am " << "a C++ sentence"; 上面这一行语句将会打印 Hello, I am a C++ sentence 到屏幕上。插入运算符(<<) 的重复使用在我们想要打印变量和内容的组合内容或多个变量时有所体现:
cout << "Hello, I am " << age << " years old and my zipcode is " << zipcode;
如果我们假设变量age的值为24,变量zipcode的值为90064,以上句子的输出将为: Hello, I am 24 years old and my zipcode is 90064
必须注意,除非我们明确指定,cout并不会自动在其输出内容的末尾加换行符,因此下面的语句:
cout << "This is a sentence.";
cout << "This is another sentence."; 将会有如下内容输出到屏幕:
This is a sentence.This is another sentence. 虽然我们分别调用了两次cout,两个句子还是被输出在同一行。所以,为了在输出中换行,我们必须插入一个换行符来明确表达这一要求。在C++中换行符可以写作\n:
cout << "First sentence.\n ";
cout << "Second sentence.\nThird sentence."; 将会产生如下输出:
First sentence.
Second sentence.
Third sentence.
另外,你也可以用操作符endl来换行,例如:
cout << "First sentence." << endl; cout << "Second sentence." << endl; 将会输出:
First sentence.
Second sentence.
当操作符endl被用在buffered streams中时有一点特殊:它们被flushed。不过cout 默认为unbuffered,所以不会被影响。 你可以暂时不管这一点。 你可以使用\n或endl来指定cout输出换行,请注意前面所讲的两者的不同用法。
输入Input (cin)
C++中的标准输入是通过在cin数据流上重载运算符extraction (>>) 来实现的。它后面必须跟一个变量以便存储读入的数据。例如:
int age;
cin >> age;
声明一个整型变量age然后等待用户从键盘输入到cin并将输入值存储在这个变量中。
cin 只能在键盘输入回车键(RETURN)后才能处理前面输入的内容。因此即使你只要求输入一个单独的字符,在用户按下回车键(RETURN)之前cin将不会处理用户的输入的字符。
在使用cin输入的时候必须考虑后面的变量类型。如果你要求输入一个整数,extraction (>>) 后面必须跟一个整型变量,如果要求一个字符,后面必须跟一个字符型变量,如果要求一个字符串,后面必须跟一个字符串型变量。 // i/o example Please enter an integer value: #include 702
int main () The value you entered is 702
{ and its double is 1404.
int i;
cout << "Please enter an integer value: ";
cin >> i;
cout << "The value you entered is " << i;
cout << " and its double is " << i*2 << ".\n";
return 0;
}
使用程序的用户可以使引起错误的原因之一,即使是在最简单的需要用cin做输入的程序中(就像我们上面看到的这个程序)。因为如果你要求输入一个整数数
值,而用户输入了一个名字(一个字符串),其结果可能导致程序产生错误操作,因为它不是我们期望从用户处获得的数据。当你使用由cin 输入的数据的时候,你不得不假设程序的用户将会完全合作而不会在程序要求输入整数的时候输入他的名字。后面当我们看到怎样使用字符串的时候,我们将会同时看到一些解决这一类出错问题的办法。
你也可以利用cin 要求用户输入多个数据 :
cin >> a >> b;
等同于:
cin >> a;
cin >> b;
在以上两种情况下用户都必须输入两个数据,一个给变量a,一个给变量b。输入时两个变量之间可以以任何有效的空白符号间隔,包括空格,跳跃符tab或换行。
第二章 控制结构和函数
2.1 控制结构(Control Structures)
一个程序的语句往往并不仅限于线性顺序结构。在程序的执行过程中它可能被分成两支执行,可能重复某些语句,也可能根据一些判断结果而执行不同的语句。因此C++ 提供一些控制结构语句 (control structures) 来实现这些执行顺序。 为了介绍程序的执行顺序,我们需要先介绍一个新概念:语句块(block of instructions)。一个语句块(A block of instructions) 是一组互相之间由分号semicolons (;) 分隔开但整体被花括号curly bracket signs: { and }括起来的语句。
本节中我们将看到的大多数控制结构允许一个通用的statement做参数,这个statement根据需要可以是一条语句,也可以是一组语句组成的语句块。如果我们只需要一条语句做statement,它可以不被括在花括号 ({}) 内。但如果我们需要多条语句共同做statement,则必须把它们括在花括号内 ({}) 以组成一个语句块。
条件结构Conditional structure: if and else
条件结构用来实现仅在某种条件满足的情况下才执行一条语句或一个语句块。它的形式是:
if (condition) statement
这里 condition 是一个将被计算的表达式(expression)。如果表达式值为真,即条件(condition)为true,statement 将被执行。否则,statement 将被忽略(不被执行),程序从整个条件结构之后的下一条语句继续执行。 例如,以下程序段实现只有当变量x存储的值确实为100的时候才输出"x is 100":
if (x == 100)
cout << "x is 100";
如果我们需要在条件condition为真true的时候执行一条以上的语句,我们可以花括号{}将语句括起来组成一个语句块:
if (x == 100)
{
cout << "x is ";
cout << x;
}
我们可以用关键字else 来指定当条件不能被满足时需要执行的语句,它需要和if 一起使用,形式是:
if (condition) statement1 else statement2
例如:
if (x == 100)
cout << "x is 100";
else
cout << "x is not 100";
以上程序如果x的值为100,则在屏幕上打出x is 100,如果x不是100,而且也只有在x不是100的时候,屏幕上将打出x is not 100。
多个if + else 的结构被连接起来使用来判断数值的范围。以下例子显示了如何用它来判断变量 x中当前存储的数值是正值,负值还是既不正也不负,即等于0 。
if (x > 0)
cout << "x is positive";
else if (x < 0)
cout << "x is negative";
else
cout << "x is 0";
记住当我们需要执行多条语句时,必须使用花括号{}将它们括起来以组成一个语
句块block of instructions。
重复结构Repetitive structures 或循环loops 循环Loops 的目的是重复执行一组语句一定的次数或直到满足某种条件。 while 循环
格式是:
while (表达式expression) 语句statement
它的功能是当expression 的值为真true时重复执行statement。 例如,下面我们将用while循环来写一个倒计数程序: // custom countdown using while Enter the starting number > 8 #include 8, 7, 6, 5, 4, 3, 2, 1, FIRE! int main ()
{
int n;
cout << "Enter the starting number > ";
cin >> n;
while (n>0) {
cout << n << ", ";
--n;
}
cout << "FIRE!";
return 0;
}
程序开始时提示用户输入一个倒计数的初始值。然后while 循环开始,如果用
户输入的数值满足条件n>0 (即 n 比0 大),后面跟的语句块将会被执行一定的
次数,直到条件 (n>0) 不再满足(变为false)。 以上程序的所有处理过程可以用以下的描述来解释: 从main开始:
1. 用户输入一个数值赋给n.
2. while语句检查(n>0)是否成立,这时有两种可能:
o true: 执行statement (到第3步)
o false: 跳过statement. 程序直接执行第5步.
3. 执行statement:
cout << n << ", ";
--n;
(将n 的值打印在屏幕上,然后将n 的值减1).
4. 语句块结束,自动返回第2步。
5. 继续执行语句块之后的程序:打印 FIRE! ,程序结束。
我们必须考虑到循环必须在某个点结束,因此在语句块之内(loop的statement之内) 我们必须提供一些方法使得条件condition 可以在某个时刻变为假 false,否则循环将无限重复下去。在这个例子里,我们用语句--n;使得循环在重复一定的次数后变为false :当 n 变为0, 倒计数结束。 do-while 循环
格式:
do 语句statement while (条件condition);
它的功能与while 循环一抹一样,除了在do-while循环中是先执行statement 然后才检查条件condition ,而不想while循环中先检查条件然后才执行statement。这样,即使条件condition从来没有被满足过,statement 仍至少被执行一次。例如,下面的程序重复输出(echoes)用户输入的任何数值,直到用户输入0为止。
// number echoer Enter number (0 to end): 12345
#include You entered: 12345
int main () Enter number (0 to end): { 160277
unsigned long n; You entered: 160277
do { Enter number (0 to end): 0
cout << "Enter number (0 to end): "; You entered: 0
cin >> n;
cout << "You entered: " << n << "\n";
} while (n != 0);
return 0;
}
循环通常被用在判断循环结束的条件是在循环语句内部被决定的情况do-while
下,比如以上的例子,在循环的语句块内用户的输入决定了循环是否结束。如果用户永远不输入0,则循环永远不会结束。
for 循环
格式是:
for (initialization; condition; increase) statement;
它的主要功能是当条件condition 为真true时重复执行语句statement ,类似while 循环。但除此之外,for 还提供了写初始化语句initialization 和增值语句increase 的地方。因此这种循环结构是特别为执行由计数器控制的循环而设计的。
它按以下方式工作:
1. 执行初始化initialization 。通常它是设置一个计数器变量(counter variable)的初始
值,初始化仅被执行一次。
2. 检查条件condition ,如果条件为真true,继续循环,否则循环结束循环中语句
statement 被跳过。
3. 执行语句statement 。像以前一样,它可以是一个单独的语句,也可以是一个由花括
号{ }括起来的语句块。
最后增值域(increase field)中的语句被执行,循环返回第2步。注意增值域中可能4.
是任何语句,而不一定只是将计数器增加的语句。例如下面的例子中计数器实际为
减1,而不是加1。
下面是用for循环实现的倒计数的例子:
// countdown using a for loop 10, 9, 8, 7, 6, 5, 4, 3, 2, 1,
#include FIRE!
int main ()
{
for (int n=10; n>0; n--) {
cout << n << ", ";
}
cout << "FIRE!";
return 0;
}
初始化initialization 和增值increase 域是可选的(即可以为空)。但这些域为空的时候,它们和其他域之间间隔的分号不可以省略。例如我们可以写:for (;n<10;)来表示没有初始化和增值语句;或for (;n<10;n++) 来表示有增值语句但没有初始化语句。
另外我们也可以在for循环初始化或增值域中放一条以上的语句,中间用逗号 coma(,)隔开。例如假设我们想在循环中初始化一个以上的变量,可以用以下的程序来实现:
for ( n=0, i=100 ; n!=i ; n++, i-- ) {
// whatever here...
}
这个循环将被执行50 次,如果n 和i 在循还内部都不被改变的话:
n初始值为0,i初始值为100,条件是(n!=i)(即n不能等于i)。因为每次循环n加1,而且i减1,循环的条件将会在第50次循环之后变为假false(n 和 i 都等于50)。
分支控制和跳转(Bifurcation of control and jumps)
break 语句
通过使用break语句,即使在结束条件没有满足的情况下,我们也可以跳出一个循环。它可以被用来结束一个无限循环(infinite loop),或强迫循环在其自然结束之前结束。例如,我们想要在倒计数自然结束之前强迫它停止(也许因为一个引擎故障):
// break loop example 10, 9, 8, 7, 6, 5, 4, 3, #include countdown aborted!
int main ()
{
int i;
for (n=10; n>0; n--) {
cout << n << ", ";
if (n==3)
{
cout << "countdown aborted!";
break;
}
return 0;
}
continue 语句
continue语句使得程序跳过当前循环中剩下的部分而直接进入下一次循环,就好像循环中语句块的结尾已经到了使得循环进入下一次重复。例如,下面例子中倒计数时我们将跳过数字5的输出:
// continue loop example 10, 9, 8, 7, 6, 4, 3, 2, 1, FIRE!
#include
int main ()
{
for (int n=10; n>0; n--) {
if (n==5) continue;
cout << n << ", ";
}
cout << "FIRE!";
return 0;
}
goto 语句
通过使用goto语句可以使程序从一点跳转到另外一点。你必须谨慎只用这条语句,因为它的执行可以忽略任何嵌套限制。
跳转的目标点可以由一个标示符(label)来标明,该标示符作为goto语句的参数。一个标示符(label)由一个标识名称后面跟一个冒号colon (:)组成。 通常除了底层程序爱好者使用这条语句,它在结构化或面向对象的编程中并不常
用。下面的例子中我们用goto来实现倒计数循环: // goto loop example 10, 9, 8, 7, 6, 5, 4, 3, 2, 1,
#include FIRE!
int main ()
{
int n=10;
loop:
cout << n << ", ";
n--;
if (n>0) goto loop;
cout << "FIRE!";
return 0;
}
exit 函数
exit是一个在cstdlib (stdlib.h)库中定义的函数。 exit的目的是一个特定的退出代码来结束程序的运行,它的原型(prototype)
是:
void exit (int exit code);
exit code是由操作系统使用或被调用程序使用。通常exit code为0表示程序正常结束,任何其他值表示程序执行过程中出现了错误。
选择结构The selective Structure: switch switch 语句的语法比较特殊。它的目标是对一个表达式检查多个可能常量值,有些像我们在本节开头学习的把几个if 和else if 语句连接起来的结构。它的形式是:
switch (expression) {
case constant1:
block of instructions 1
break;
case constant2:
block of instructions 2
break;
.
.
.
default:
default block of instructions
}
它按以下方式执行:
switch 计算表达式expression 的值,并检查它是否与第一个常量constant1相等,如果相等,程序执行常量1后面的语句块block of instructions 1 直到碰到关键字break ,程序跳转到switch 选择结构的结尾处。 如果expression 不等于constant1,程序检查表达式expression 的值是否等于第二个常量constant2, 如果相等,程序将执行常量2后面的语句块block of instructions 2 直到碰到关键字break。
依此类推,直到最后如果表达式expression 的值不等于任何前面的常量(你可以用case语句指明任意数量的常量值来要求检查),程序将执行默认区default: 后面的语句,如果它存在的话。default: 选项是可以省略的。 下面的两段代码段功能相同:
switch example if-else equivalent
switch (x) { if (x == 1) {
case 1: cout << "x is 1";
cout << "x is 1"; }
break; else if (x == 2) {
case 2: cout << "x is 2";
cout << "x is 2"; }
break; else {
default: cout << "value of x unknown";
cout << "value of x unknown"; }
}
前面已经提到switch的语法有点特殊。注意每个语句块结尾包含的break语句。这是必须的,因为如果不这样做,例如在语句块block of instructions 1 的结尾没有break,程序执行将不会跳转到switch选择的结尾处 (}) ,而是继续执行下面的语句块,直到第一次遇到break语句或到switch选择结构的结尾。因此,不需要在每一个case 域内加花括号{ } 。这个特点同时可以帮助实现对不同的可能值执行相同的语句块。例如:
switch (x) {
case 1:
case 2:
case 3:
cout << "x is 1, 2 or 3";
break;
default:
cout << "x is not 1, 2 nor 3";
}
注意switch只能被用来比较表达式和不同常量的值constants。因此我们不能够把变量或范围放在case之后,例如 (case (n*2):) 或 (case (1..3):) 都不可以,因为它们不是有效的常量。 如果你需要检查范围或非常量数值,使用连续的if 和else if 语句。
2.2 函数I(Functions I)
通过使用函数(functions)我们可以把我们的程序以更模块化的形式组织起来,从而利用C++所能提供的所有结构化编程的潜力。
一个函数(function)是一个可以从程序其它地方调用执行的语句块。以下是它的格式:
type name ( argument1, argument2, ...) statement
这里:
, type 是函数返回的数据的类型
, name 是函数被调用时使用的名
, argument 是函数调用需要传入的参量(可以声明任意多个参量)。每个参量(argument)
由一个数据类型后面跟一个标识名称组成,就像变量声明中一样(例如,int x)。参量
仅在函数范围内有效,可以和函数中的其它变量一样使用, 它们使得函数在被调用
时可以传入参数,不同的参数用逗号(comma)隔开.
, statement 是函数的内容。它可以是一句指令,也可以是一组指令组成的语句块。如
果是一组指令,则语句块必须用花括号{}括起来,这也是我们最常见到情况。其实
为了使程序的格式更加统一清晰,建议在仅有一条指令的时候也使用花括号,这是
一个良好的编程习惯。
下面看一下第一个函数的例子:
// function example The result is 8
#include
int addition (int a, int b)
{
int r;
r=a+b;
return (r);
}
int main ()
{
int z;
z = addition (5,3);
cout << "The result is " << z;
return 0;
}
记得在我们教程开始时说过:一个C++程序总是从main函数开始执行。 因此我们从那里开始。
我们可以看到 main 函数以定义一个整型变量z 开始。紧跟着我们看到调用addition 函数。我们可以看到函数调用的写法和上面函数定义本身十分相似:
参数有明显的对应关系。在main 函数中我们调用addition 函数,并传入两个数值: 5 和3 , 它们对应函数addition 中定义的参数int a 和int b。 当函数在main 中被调用时,程序执行的控制权从main转移到函数addition。调用传递的两个参数的数值 (5 和3) 被复制到函数的本地变量(local variables) int a 和int b 中。
函数addition 中定义了新的变量(int r;),通过表达式r=a+b;, 它把a 加b 的结果赋给r 。因为传过来的参数a 和b 的值分别为5 和3 ,所以结果是8。 下面一行代码:
return (r);
,从调用addition结束函数addition,并把控制权交还给调用它的函数(main) 的地方开始继续向下执行。另外,return 在调用的时候后面跟着变量r (return (r);), 它当时的值为8, 这个值被称为函数的返回值。
函数返回的数值就是函数的计算结果,因此, z 将存储函数addition (5, 3)返回的数值, 即8。用另一种方式解释,你也可以想象成调用函数(addition (5,3)) 被替换成了它的返回值 (8)。
接下来main中的下一行代码是:
cout << "The result is " << z;
它把结果打印在屏幕上。
变量的范围(Scope
of variables)
你必须考虑到变量的
范围只是在定义该变
量的函数或指令块内
有效,而不能在它的
函数或指令块之外使
用。 例如,在上面的
例子里就不可能在
main 中直接使用变
量a, b 或 r ,因为
它们是函数addition
的本地变量(local
variable)。在函数addition中也不可能直接使用变量z,因为它是main的本地变量。 因此,本地变量 (local variables)的范围是局限于声明它的嵌套范围之内的。尽管如此,你还可以定义全局变量(global variables),它们可以在代码的任何位置被访问,不管在函数以内还是以外。要定义全局变量,你必须在所有函数或代码块之外定义它们,也就是说,直接在程序体中声明它们。 这里是另一个关于函数的例子:
// function example The first result is 5 #include The second result is 5 int subtraction (int a, int b) The third result is 2 { The fourth result is 6
int r;
r=a-b;
return (r);
}
int main ()
{
int x=5, y=3, z;
z = subtraction (7,2);
cout << "The first result is " << z << '\n';
cout << "The second result is " << subtraction (7,2) << '\n';
cout << "The third result is " << subtraction (x,y) << '\n';
z= 4 + subtraction (x,y);
cout << "The fourth result is " << z << '\n';
return 0;
}
在这个例子中,我们定义了函数subtraction。这个函数的功能是计算传入的两个参数的差值并将结果返回。
在 main 函数中,函数subtraction被调用了多次。我们用了几种不同的调用方法,因此你可以看到在不同的情况下函数如何被调用。
为了更好的理解这些例子,你需要考虑到被调用的函数其实完全可以由它所返回的值来代替。例如在上面例子中第一种情况下 (这种调用你应该已经知道了,因为我们在前面的例子中已经用过这种形式的调用):
z = subtraction (7,2);
cout << "The first result is " << z; 如果我们把函数调用用它的结果(也就是5)替换,我们将得到: z = 5;
cout << "The first result is " << z; 同样的
cout << "The second result is " << subtraction (7,2);
与前面的调用有同样的结果,但在这里我们把对函数subtraction 的调用直接用作cout的参数。这可以简单想象成我们写的是:
cout << "The second result is " << 5;
因为5 是subtraction (7,2)的结果。
在
cout << "The third result is " << subtraction (x,y);
中,与前面的调用唯一的不同之处是这里调用subtraction 时的参数使用的是变量而不是常量。这样用时毫无问题的。在这个例子里,传入函数subtraction 的参数值是变量x 和y中存储的数值,即分别为5 和3,结果为2。 第四种调用也是一样的。只要知道除了
z = 4 + subtraction (x,y);
我们也可以写成:
z = subtraction (x,y) + 4;
它们的结果是完全一样的。注意在整个表达式的结尾写上分号semicolon sign
(;)。它并不需要总是跟在函数调用的后面,因为你可以有一次把它们想象成函数被它的结果所替代:
z = 4 + 2;
z = 2 + 4;
没有返回值类型的函数,使用void.
如果你记得函数声明的格式:
type name ( argument1, argument2 ...) statement
就会知道函数声明必须以一个数据类型(type)开头,它是函数由return 语句所返回数据类型。但是如果我们并不打算返回任何数据那该怎么办呢, 假设我们要写一个函数,它的功能是打印在屏幕上打印一些信息。我们不需要它返回任何值,而且我们也不需要它接受任何参数。C语言为这些情况设计了void 类型。让我们看一下下面的例子:
// void function example I'm a function!
#include
void dummyfunction (void)
{
cout << "I'm a function!";
}
int main ()
{
dummyfunction ();
return 0;
}
虽然在C++ 中void可以被省略,我们还是建议写出void,以便明确指出函数不需要参数。
你必须时刻知道的是调用一个函数时要写出它的名字并把参数写在后面的括号内。但如果函数不需要参数,后面的括号并不能省略。因此调用函数dummyfunction 的格式是
dummyfunction ();
这就明确指出它是一个函数调用,而不是一个变量名称或其它什么。 2.3 函数II(Functions II)
参数按数值传递和按地址传递(Arguments passed by value and by
reference)
到目前为止,我们看到的所有函数中,传递到函数中的参数全部是按数值传递的(by value)。也就是说,当我们调用一个带有参数的函数时,我们传递到函数中的是变量的数值而不是变量本身。 例如,假设我们用下面的代码调用我们的第一个函数addition :
int x=5, y=3, z;
z = addition ( x , y );
在这个例子里我们调用函数addition 同时将x和y的值传给它,即分别为5和3,而不是两个变量:
这样,当函数addition被调用时,它的变量a和b的值分别变为5和3,但在函数addition内对变量a 或b 所做的任何修改不会影响变量他外面的变量x 和 y 的值,因为变量x和y并没有把它们自己传递给函数,而只是传递了他们的数值。
但在某些情况下你可能需要在一个函数内控制一个函数以外的变量。要实现这种操作,我们必须使用按地址传递的参数(arguments passed by reference),就象下面例子中的函数duplicate:
// passing parameters by reference x=2, y=6, z=14
#include
void duplicate (int& a, int& b, int& c)
{
a*=2;
b*=2;
c*=2;
}
int main ()
{
int x=1, y=3, z=7;
duplicate (x, y, z);
cout << "x=" << x << ", y=" << y << ", z=" << z;
return 0;
}
第一个应该注意的事项是在函数duplicate的声明(declaration)中,每一个变量的类型后面跟了一个地址符ampersand sign (&),它的作用是指明变量是按地址传递的(by reference),而不是像通常一样按数值传递的(by value)。 当按地址传递(pass by reference)一个变量的时候,我们是在传递这个变量本身,我们在函数中对变量所做的任何修改将会影响到函数外面被传递的变量。
用另一种方式来说,我们已经把变量a, b,c和调用函数时使用的参数(x, y和 z)联系起来了,因此如果我们在函数内对a 进行操作,函数外面的x 值也会改变。同样,任何对b 的改变也会影响y,对c 的改变也会影响z>。 这就是为什么上面的程序中,主程序main中的三个变量x, y和z在调用函数duplicate 后打印结果显示他们的值增加了一倍。
如果在声明下面的函数:
void duplicate (int& a, int& b, int& c)
时,我们是按这样声明的:
void duplicate (int a, int b, int c)
也就是不写地址符 ampersand (&),我们也就没有将参数的地址传递给函数,而是传递了它们的值,因此,屏幕上显示的输出结果x, y ,z 的值将不会改变,仍是1,3,7。
这种用地址符 ampersand (&)来声明按地址"by reference"传递参数的方式只是在C++中适用。在C 语言中,我们必须用指针(pointers)来做相同的操作。 按地址传递(Passing by reference)是一个使函数返回多个值的有效方法。例如,下面是一个函数,它可以返回第一个输入参数的前一个和后一个数值。
// more than one returning value Previous=99, Next=101 #include
void prevnext (int x, int& prev, int& next)
{
prev = x-1;
next = x+1;
}
int main ()
{
int x=100, y, z;
prevnext (x, y, z);
cout << "Previous=" << y << ", Next=" << z;
return 0;
}
参数的默认值(Default values in arguments)
当声明一个函数的时候我们可以给每一个参数指定一个默认值。如果当函数被调
用时没有给出该参数的值,那么这个默认值将被使用。指定参数默认值只需要在
函数声明时把一个数值赋给参数。如果函数被调用时没有数值传递给该参数,那
么默认值将被使用。但如果有指定的数值传递给参数,那么默认值将被指定的数
值取代。例如:
// default values in functions 6
#include 5
int divide (int a, int b=2) {
int r;
r=a/b;
return (r);
}
int main () {
cout << divide (12);
cout << endl;
cout << divide (20,4);
return 0;
}
我们可以看到在程序中有两次调用函数divide。第一次调用: divide (12)
只有一个参数被指明,但函数divide允许有两个参数。因此函数divide 假设第二个参数的值为2,因为我们已经定义了它为该参数缺省的默认值(注意函数声明中的int b=2)。因此这次函数调用的结果是 6 (12/2)。 在第二次调用中:
divide (20,4)
这里有两个参数,所以默认值 (int b=2) 被传入的参数值4所取代,使得最后结果为 5 (20/4).
函数重载(Overloaded functions) 两个不同的函数可以用同样的名字,只要它们的参量(arguments)的原型(prototype)不同,也就是说你可以把同一个名字给多个函数,如果它们用不同数量的参数,或不同类型的参数。例如:
// overloaded function 2
#include 2.5
int divide (int a, int b) {
return (a/b);
}
float divide (float a, float b) {
return (a/b);
}
int main () {
int x=5,y=2;
float n=5.0,m=2.0;
cout << divide (x,y);
cout << "\n";
cout << divide (n,m);
cout << "\n";
return 0;
}
在这个例子里,我们用同一个名字定义了两个不同函数,当它们其中一个接受两个整型(int)参数,另一个则接受两个浮点型(float)参数。编译器 (compiler)通过检查传入的参数的类型来确定是哪一个函数被调用。如果调用传入的是两个
整数参数,那么是原型定义中有两个整型(int)参量的函数被调用,如果传入的是两个浮点数,那么是原型定义中有两个浮点型(float)参量的函数被调用。 为了简单起见,这里我们用的两个函数的代码相同,但这并不是必须的。你可以让两个函数用同一个名字同时完成完全不同的操作。
Inline 函数(inline functions)
inline 指令可以被放在函数声明之前,要求该函数必须在被调用的地方以代码形式被编译。这相当于一个宏定义(macro)。它的好处只对短小的函数有效,这种情况下因为避免了调用函数的一些常规操作的时间(overhead),如参数堆栈操作的时间,所以编译结果的运行代码会更快一些。
它的声明形式是:
inline type name ( arguments ... ) { instructions ... }
它的调用和其他的函数调用一样。调用函数的时候并不需要写关键字inline ,只有在函数声明前需要写。
递归(Recursivity)
递归(recursivity)指函数将被自己调用的特点。它对排序(sorting)和阶乘(factorial)运算很有用。例如要获得一个数字n的阶乘,它的数学公式是: n! = n * (n-1) * (n-2) * (n-3) ... * 1
更具体一些,5! (factorial of 5) 是:
5! = 5 * 4 * 3 * 2 * 1 = 120
而用一个递归函数来实现这个运算将如以下代码:
// factorial calculator Type a number: 9
#include !9 = 362880
long factorial (long a){
if (a > 1) return (a * factorial (a-1));
else return (1);
}
int main () {
long l;
cout << "Type a number: ";
cin >> l;
cout << "!" << l << " = " << factorial (l);
return 0;
}
注意我们在函数factorial中是怎样调用它自己的,但只是在参数值大于1的时候才做调用,因为否则函数会进入死循环(an infinite recursive loop),当参数到达0的时候,函数不继续用负数乘下去(最终可能导致运行时的堆栈溢出错误(stack overflow error)。
这个函数有一定的局限性,为简单起见,函数设计中使用的数据类型为长整型(long)。在实际的标准系统中,长整型long无法存储12~以上的阶乘值。
函数原型(Prototyping functions)
到目前为止,我们定义的所有函数都是在它们第一次被调用(通常是在main中)之前,而把main 函数放在最后。如果重复以上几个例子,但把main 函数放在其它被它调用的函数之前,你就会遇到编译错误。原因是在调用一个函数之前,函数必须已经被定义了,就像我们前面例子中所做的。
但实际上还有一种方法来避免在main 或其它函数之前写出所有被他们调用的函数的代码,那就是通过定义函数原型(prototyping functions)。定义函数原型就是对函数在的完整定义之前做一个短小重要的声明,以便让编译器了解函数的参数和返回值类型。
它的形式是:
type name ( argument_type1, argument_type2, ...);
它与一个函数的头定义(header definition)一样,除了:
, 它不包括函数的内容, 也就是它不包括函数后面花括号{}内的所有语句。
, 它以一个分号semicolon sign (;) 结束。
, 在参数列举中只需要写出各个参数的数据类型就够了,至于每个参数的名字可以写,
也可以不写,但是我们建议写上。
例如:
// prototyping Type a number (0 to exit): 9
#include Number is odd.
Type a number (0 to exit): 6
void odd (int a); Number is even.
void even (int a); Type a number (0 to exit): 1030
Number is even.
int main () { Type a number (0 to exit): 0
int i; Number is even.
do {
cout << "Type a number: (0 to exit)";
cin >> i;
odd (i);
} while (i!=0);
return 0;
}
void odd (int a) {
if ((a%2)!=0) cout << "Number is odd.\n";
else even (a);
}
void even (int a) {
if ((a%2)==0) cout << "Number is even.\n";
else odd (a);
}
这个例子的确不是很有效率,我相信现在你已经可以只用一半行数的代码来完成同样的功能。但这个例子显示了函数原型(prototyping functions)是怎样工作的。并且在这个具体的例子中,两个函数中至少有一个是必须定义原型的。 这里我们首先看到的是函数odd 和even的原型:
void odd (int a);
void even (int a);
这样使得这两个函数可以在它们被完整定义之前就被使用,例如在main中被调用,这样main就可以被放在逻辑上更合理的位置:即程序代码的开头部分。 尽管如此,这个程序需要至少一个函数原型定义的特殊原因是因为在odd 函数里需要调用even 函数,而在even 函数里也同样需要调用odd函数。如果两个函数任何一个都没被提前定义原型的话,就会出现编译错误,因为或者odd 在even 函数中是不可见的(因为它还没有被定义),或者even 函数在odd函数中是不可见的。
很多程序员建议给所有的函数定义原型。这也是我的建议,特别是在有很多函数或函数很长的情况下。把所有函数的原型定义放在一个地方,可以使我们在决定怎样调用这些函数的时候轻松一些,同时也有助于生成头文件。
第三章 高级数据类型(Advanced Data )
3.1 数组 (Arrays)
数组(Arrays) 是在内存中连续存储的一组同种数据类型的元素(变量),每一数组有一个唯一名称,通过在名称后面加索引(index)的方式可以引用它的每一个元素。
也就是说,例如我们有5个整型数值需要存储,但我们不需要定义5个不同的变量名称,而是用一个数组(array)来存储这5个不同的数值。注意数组中的元素必须是同一数据类型的,在这个例子中为整型(int)。
例如一个存储5个整数叫做billy的数组可以用下图来表示:
这里每一个空白框代表数组的一个元素,在这个例子中为一个整数值。白框上面的数字0 到4 代表元素的索引(index)。注意无论数组的长度如何,它的第一个元素的索引总是从0开始的。
同其它的变量一样, 数组必须先被声明然后才能被使用。一种典型的数组声明显示如下:
type name [elements];
这里type 是可以使任何一种有效的对象数据类型(object type),如 int, float...等,name 是一个有效地变量标识(identifier),而由中括号[]引起来的elements 域指明数组的大小,即可以存储多少个元素。
因此,要定义上面图中显示的 billy 数组,用一下语句就可以了: int billy [5];
备注:在定义一个数组的时候,中括号[]中的elements 域必须是一个常量数值,因为数组是内存中一块有固定大小的静态空间,编译器必须在编译所有相关指令之前先能够确定要给该数组分配多少内存空间。
初始化数组(Initializing arrays) 当声明一个本地范围内(在一个函数内)的数组时,除非我们特别指定,否则数组将不会被初始化,因此它的内容在我们将数值存储进去之前是不定的。 如果我们声明一个全局数组(在所有函数之外),则它的内容将被初始化为所有元素均为0。因此 ,如果全局范围内我们声明:
int billy [5];
那么billy 中的每一个元素将会被初始化为0:
另外,我们还可以在声明一个变量的同时把初始值付给数组中的每一个元素,这个赋值用花括号{ }来完成。例如:
int billy [5] = { 16, 2, 77, 40, 12071 };
这个声明将生成如下数组:
花括号中我们要初始化的元素数值个数必须和数组声明时方括号[ ]中指定的数组长度相符。例如,在上面例子中数组billy 声明中的长度为5,因此在后面花括号中的初始值也有5个,每个元素一个数值。
因为这是一种信息的重复,因此C++允许在这种情况下数组[ ]中为空白,而数组的长度将有后面花括号{}中数值的个数来决定,如下例所示。 int billy [] = { 16, 2, 77, 40, 12071 };
存取数组中数值(Access to the values of an Array) 在程序中我们可以读取和修改数组任一元素的数值,就像操作其他普通变量一样。格式如下:
name[index]
继续上面的例子,数组billy 有5个元素,其中每一元素都是整型int,我们引用其中每一个元素的名字分别为如下所示:
例如,要把数值75存入数组billy 中第3个元素的语句可以是: billy[2] = 75;
又例如,要把数组billy 中第3个元素的值赋给变量a,我们可以这样写: a = billy[2];
因此,在所有使用中,表达式billy[2]就像任何其他整型变量一样。 注意数组billy 的第3个元素为billy[2],因为索引(index)从0开始,第1个元素是billy[0],第2个元素是billy[1],因此第3个是 billy[2]。同样的原因,最后一个元素是billy[4]。如果我们写billy[5],那么是在使用billy的第6个元素,因此会超出数组的长度。
在C++ 中对数组使用超出范围的index是合法的,这就会产生问题,因为它不会产生编译错误而不易被察觉,但是在运行时会产生意想不到的结果,甚至导致严重运行错误。超出范围的index 之所以合法的原因我们在后面学习指针(pointer)的时候会了解。
学到这里,我们必须能够清楚的了解方括号[ ]在对数组操作中的两种不同用法。它们完成两种任务:一种是在声明数组的时候定义数组的长度;另一种是在引用具体的数组元素的时候指明一个索引号(index)。我们要注意不要把这两种用法混淆。
int billy[5]; // 声明新数组(以数据类型名称开头)
billy[2] = 75; // 存储数组的一个元素
其它合法的数组操作:
billy[0] = a; // a为一个整型变量
billy[a] = 75;
b = billy [a+2];
billy[billy[a]] = billy[2] + 5;
// arrays example 12206
#include
int billy [ ] = {16, 2, 77, 40, 12071};
int n, result=0;
int main () {
for ( n=0 ; n<5 ; n++ ) {
result += billy[n];
}
cout << result;
return 0;
}
多维数组(Multidimensional Arrays)
多维数组(Multidimensional Arrays)可以被描述为数组的数组。例如,一个2维数组(bidimensional array)可以被想象成一个有同一数据类型的2维表格。
jimmy 显示了一个整型(int )的3x5二维数组,声明这一数组的的方式是: int jimmy [3][5];
而引用这一数组中第2列第4排元素的表达式为:jimmy[1][3]
(记住数组的索引总是从0开始)。
多维数组(Multidimensional arrays)并不局限于2维。如果需要,它可以有任意多维,虽然需要3维以上的时候并不多。但是考虑一下一个有很多维的数组所需要的内存空间,例如:
char century [100][365][24][60][60];
给一个世纪中的每一秒赋一个字符(char),那么就是多于30亿的字符~如果我们定义这样一个数组,需要消耗3000M的内存。
多维数组只是一个抽象的概念,因为我们只需要把各个索引的乘积放入一个简单的数组中就可以获得同样的结果。例如:
int jimmy [3][5]; 效果上等价于
int jimmy [15]; (3 * 5 = 15)
唯一的区别是编译器帮我们记住每一个想象中的维度的深度。下面的例子中我们就可以看到,两段代码一个使用2维数组,另一个使用简单数组,都获得同样的结果,即都在内存中开辟了一块叫做jimmy的空间,这个空间有15个连续地址位置,程序结束后都在相同的位置上存储了相同的数值,如后面图中所示: // multidimensional array // pseudo-multidimensional array
#include #include
#define WIDTH 5 #define WIDTH 5
#define HEIGHT 3 #define HEIGHT 3
int jimmy [HEIGHT][WIDTH]; int jimmy [HEIGHT * WIDTH];
int n,m; int n,m;
int main (){ int main (){
for (n=0;n for (n=0;n
for (m=0;m for (m=0;m
jimmy[n][m]=(n+1)*(m+1); jimmy[n * WIDTH + m]=(n+1)*(m+1);
} }
return 0;
return 0; }
}
上面两段代码并不向屏幕输出,但都向内存中的叫做jimmy的内存块存入如下数值:
我们用了宏定义常量(#define)来简化未来可能出现的程序修改,例如,如果我们决定将数组的纵向由3扩大到4,只需要将代码行:
#define HEIGHT 3
修改为 :
#define HEIGHT 4
而不需要对程序的其他部分作任何修改。
数组参数(Arrays as parameters)
有时候我们需要将数组作为参数传给函数。在C++ 中将一整块内存中的数值作为参数完整的传递给一个函数是不可能的,即使是一个规整的数组也不可能,但是允许传递它的地址。它们的实际作用是一样的,但传递地址更快速有效。 要定义数组为参数,我们只需要在声明函数的时候指明参数数组的基本数据类型,一个标识后面再跟一对空括号[]就可以了。例如以下的函数: void procedure (int arg[])
接受一个叫做arg的整型数组为参数。为了给这个函数传递一个按如下定义的数组:
int myarray [40];
其调用方式可写为:
procedure (myarray);
下面我们来看一个完整的例子:
// arrays as parameters 5 10 15
#include 2 4 6 8 10
void printarray (int arg[], int length) {
for (int n=0; n
cout << arg[n] << " ";
cout << "\n";
}
int main () {
int firstarray[ ] = {5, 10, 15};
int secondarray[ ] = {2, 4, 6, 8, 10};
printarray (firstarray,3);
printarray (secondarray,5);
return 0;
}
可以看到,函数的第一个参数(int arg[ ])接受任何整型数组为参数,不管其长度如何。因此,我们用了第2个参数来告知函数我们传给它的第一个参数数组的长度。这样函数中打印数组内容的for 循环才能知道需要检查的数组范围。 在函数的声明中也包含多维数组参数。定义一个3维数组 (tridimensional
array) 的形式是:
base_type[ ][depth][depth]
例如,一个函数包含多维数组参数的函数可以定义为:
void procedure (int myarray[ ][3][4])
注意第一对括号[ ]中为空,而后面两对不为空。这是必须的,因为编译器必须能够在函数中确定每一个增加的维度的深度。
数组作为函数的参数,不管是多维数组还是简单数组,都是初级程序员容易出错的地方。建议阅读章节3.3, 指针(Pointers),以便更好的理解数组(arrays)是如何操作的。
3.2 字符串 (Strings of Characters) 迄今为止我们看到的程序都是使用的数字型变量(numerical variables)。但除了数字变量,还有字符串变量,我们可以用来表示连续的字符,如词,句子,名称,文本等等。到目前为止我们只是使用常量表示它们,其实我们还没有考虑过用变量来存储他们。
在C++ 中并没有一个单独的变量类型来专门存储字符串。为了完成这项功能,我们使用由连续字符(char)元素组成的字符数组。记住字符型数据(char) 是用来存储一个单个字符的,因此它的数组通常被用来存储字符串。 例如,如下数组(或字符串):
char jenny [20];
可以存储一个最多20个字符长的字符串。你可以把它想象成:
这20 个字符并不一定总是满的。例如,jenny 在程序的某一点可以只存储字符串"Hello" 或者"Merry christmas"。因此,既然字符数组经常被用于存储短于其总长的字符串,就形成了一种习惯在字符串的有效内容的结尾处加一个空字符(null character)来表示字符结束,它的常量表示可写为0 或'\0'。 我们可以用下图表示jenny (一个长度为20的字符数组) 存储字符串"Hello" 和"Merry Christmas" :
注意在有效内容结尾是如何用空字符null character ('\0')来表示字符串结束的。 后面灰色的空格表示不确定数值。
字符串初始化(Initialization of strings) 因为字符串其实是普通数组,它与数组遵守同样的规则。例如,如果我们想将数组初始化为指定数值,我们可以像初始化其它数组一样用:
char mystring[] = { 'H', 'e', 'l', 'l', 'o', '\0' };
在这里我们定义了一个有6个元素的字符数组,并将它初始化为字符串Hello 加一个空字符(null character '\0')。
除此之外,字符串还有另一个方法来进行初始化:用字符串常量。 在前几章的例子中,字符串常量已经出现过多次,它们是由双引号引起来的一组字符来表示的,例如:
"the result is: "
是一个字符串常量,我们在前面的例子中已经使用过。
与表示单个字符常量的单引号(')不同,双引号 (")是表示一串连续字符的常量。由双引号引起来的字符串末尾总是会被自动加上一个空字符 ('\0') 。 因此,我们可以用下面两种方法的任何一种来初始化字符串mystring: char mystring [ ] = { 'H', 'e', 'l', 'l', 'o', '\0' };
char mystring [ ] = "Hello";
在两种情况下字符串或数组mystring都被定义为6个字符长(元素类型为字符char):组成Hello的5个字符加上最后的空字符('\0')。在第二种用双引号的情况下,空字符('\0')是被自动加上的。
注意:同时给数组赋多个值只有在数组初始化时,也就是在声明数组时,才是合法的。象下面代码现实的表达式都是错误的:
mystring = "Hello";
mystring[ ] = "Hello";
mystring = { 'H', 'e', 'l', 'l', 'o', '\0' }; 因此记住:我们只有在数组初始化时才能够同时赋多个值给它。其原因在学习了指针(pointer)之后会比较容易理解,因为那时你会看到一个数组其实只是一个指向被分配的内存块的常量指针(constant pointer),数组自己不能够被赋予任何数值,但我们可以给数组中的每一个元素赋值。
在数组初始化的时候是特殊情况,因为它不是一个赋值,虽然同样使用了等号(=) 。不管怎样,牢记前面标下画线的规则。
给字符串赋值(Assigning values to strings) 因为赋值运算的lvalue 只能是数组的一个元素,而不能使整个数组,所以,用以下方式将一个字符串赋给一个字符数组是合法的:
mystring[0] = 'H';
mystring[1] = 'e';
mystring[2] = 'l';
mystring[3] = 'l';
mystring[4] = 'o';
mystring[5] = '\0';
但正如你可能想到的,这并不是一个实用的方法。通常给数组赋值,或更具体些,给字符串赋值的方法是使用一些函数,例如strcpy。strcpy (string copy) 在函数库cstring (string.h) 中被定义,可以用以下方式被调用: strcpy (string1, string2);
这个函数将string2 中的内容拷贝给string1。string2 可以是一个数组,一个指针,或一个字符串常量constant string。因此用下面的代码可以将字符串常
赋给mystring: 量"Hello"
strcpy (mystring, "Hello");
例如:
// setting value to string J. Soulie
#include
#include
int main () {
char szMyName [20];
strcpy (szMyName,"J. Soulie");
cout << szMyName;
return 0;
}
注意:我们需要包括头文件才能够使用函数strcpy。 虽然我们通常可以写一个像下面setstring一样的简单程序来完成与cstring
中strcpy同样的操作:
// setting value to string J. Soulie #include
void setstring (char szOut [ ], char szIn [ ]) {
int n=0;
do {
szOut[n] = szIn[n];
} while (szIn[n++] != '\0');
}
int main () {
char szMyName [20];
setstring (szMyName,"J. Soulie");
cout << szMyName;
return 0;
}
另一个给数组赋值的常用方法是直接使用输入流(cin)。在这种情况下,字符串
的数值是在程序运行时由用户输入的。
当cin 被用来输入字符串值时,它通常与函数getline 一起使用,方法如下:
cin.getline ( char buffer[], int length, char delimiter = ' \n');
这里buffer 是用来存储输入的地址(例如一个数组名),length 是一个缓存buffer 的最大容量,而delimiter 是用来判断用户输入结束的字符,它的默认值(如果我们不写这个参数时)是换行符newline character ('\n')。 下面的例子重复输出用户在键盘上的任何输入。这个例子简单的显示了如何使用cin.getline来输入字符串:
// cin with strings What's your name? Juan
#include Hello Juan.
Which is your favourite team?
int main () { Inter Milan
char mybuffer [100]; I like Inter Milan too.
cout << "What's your name? ";
cin.getline (mybuffer,100);
cout << "Hello " << mybuffer << ".\n";
cout << "Which is your favourite team? ";
cin.getline (mybuffer,100);
cout << "I like " << mybuffer << " too.\n";
return 0;
}
注意上面例子中两次调用cin.getline 时我们都使用了同一个字符串标识 (mybuffer)。程序在第二次调用时将新输入的内容直接覆盖到第一次输入到buffer 中的内容。
你可能还记得,在以前与控制台(console)交互的程序中,我们使用extraction operator (>>) 来直接从标准输入设备接收数据。这个方法也同样可以被用来输入字符串,例如,在上面的例子中我们也可以用以下代码来读取用户输入: cin >> mybuffer;
这种方法也可以工作,但它有以下局限性是cin.getline所没有的:
, 它只能接收单独的词(而不能是完整的句子),因为这种方法以任何空白符为分隔符,
包括空格spaces,跳跃符tabulators,换行符newlines和回车符arriage returns。
, 它不能给buffer指定容量,这使得程序不稳定,如果用户输入超出数组长度,输入
信息会被丢失。
因此,建议在需要用cin来输入字符串时,使用cin.getline来代替cin >>。
字符串和其它数据类型的转换(Converting strings to other types)
鉴于字符串可能包含其他数据类型的内容,例如数字,将字符串内容转换成数字型变量的功能会有用处。例如一个字符串的内容可能是"1977",但这一个5个字符组成序列,并不容易转换为一个单独的整数。因此,函数库cstdlib (stdlib.h)
提供了3个有用的函数:
, atoi: 将字符串string 转换为整型int
, atol: 将字符串string 转换为长整型long
, atof: 将字符串string 转换为浮点型float
所有这些函数接受一个参数,返回一个指定类型的数据(int, long 或 float)。这三个函数与cin.getline 一起使用来获得用户输入的数值,比传统的cin>> 方法更可靠:
// cin and ato* functions Enter price: 2.75
#include Enter quantity: 21
#include Total price: 57.75
int main () {
char mybuffer [100];
float price;
int quantity;
cout << "Enter price: ";
cin.getline (mybuffer,100);
price = atof (mybuffer);
cout << "Enter quantity: ";
cin.getline (mybuffer,100);
quantity = atoi (mybuffer);
cout << "Total price: " << price*quantity;
return 0;
}
字符串操作函数(Functions to manipulate strings)
函数库cstring (string.h) 定义了许多可以像C语言类似的处理字符串的函数 (如前面已经解释过的函数strcpy)。这里再简单列举一些最常用的:
, strcat: char* strcat (char* dest, const char* src); //将字符串src 附加到字符
串dest 的末尾,返回dest。
, strcmp: int strcmp (const char* string1, const char* string2); //比较两个字
符串string1 和string2。如果两个字符串相等,返回0。
, strcpy: char* strcpy (char* dest, const char* src); //将字符串src 的内容拷贝
给dest,返回dest 。
, strlen: size_t strlen (const char* string); //返回字符串的长度。
注意:char* 与char[] 相同。
3.3 指针 (Pointers)
我们已经明白变量其实是可以由标识来存取的内存单元。但这些变量实际上是存储在内存中具体的位置上的。对我们的程序来说,计算机内存只是一串连续的单字节单元(1byte cell),即最小数据单位,每一个单元有一个唯一地址。 计算机内存就好像城市中的街道。在一条街上,所有的房子被顺序编号,每所房子有唯一编号。因此如果我们说芝麻街27号,我们很容易找到它,因为只有一所房子会是这个编号,而且我们知道它会在26号和28号之间。 同房屋按街道地址编号一样,操作系统(operating system)也按照唯一顺序编号来组织内存。因此,当我们说内存中的位置1776,我们知道内存中只有一个位置是这个地址,而且它在地址1775和1777之间。
地址操作符/去引操作符 Address/dereference operator (&)
当我们声明一个变量的同时,它必须被存储到内存中一个具体的单元中。通常我们并不会指定变量被存储到哪个具体的单元中—幸亏这通常是由编译器和操作系统自动完成的,但一旦操作系统指定了一个地址,有些时候我们可能会想知道变量被存储在哪里了。
这可以通过在变量标识前面加与符号ampersand sign (&)来实现,它表示"...的地址" ("address of"),因此称为地址操作符(adress operator),又称去引操作符(dereference operator)。例如:
ted = &andy;
将变量andy的地址赋给变量ted,因为当在变量名称andy 前面加ampersand (&)
符号,我们指的将不再是该变量的内容,而是它在内存中的地址。 假设andy 被放在了内存中地址1776的单元中,然后我们有下列代码: andy = 25;
fred = andy;
ted = &andy;
其结果显示在下面的图片中:
我们将变量andy 的值赋给变量fred,这与以前我们看到很多例子都相同,但对于ted,我们把操作系统存储andy的内存地址赋给它,我们想像该地址为1776 (它可以是任何地址,这里只是一个假设的地址),原因是当给ted 赋值的时候,我们在andy 前面加了ampersand (&) 符号。
如上面例子中的ted ),我们称之为指针(pointer)。存储其它变量地址的变量(
在C++ 中,指针pointers 有其特定的优点,因此经常被使用。在后面我们将会看到这种变量如何被声明。
引用操作符Reference operator (*) 使用指针的时候,我们可以通过在指针标识的前面加星号asterisk (*)来存储该指针指向的变量所存储的数值,它可以被
翻译
阿房宫赋翻译下载德汉翻译pdf阿房宫赋翻译下载阿房宫赋翻译下载翻译理论.doc
为“所指向的数值”("value
beth = *ted; (我pointed by")。因此,仍用前面例子中的数值,如果我们写:
们可以读作:"beth 等与ted所指向的数值") beth 将会获得数值25,因为ted 是1776,而1776 所指向的数值为25。
你必须清楚的区分ted 存储的是1776,但*ted (前面加asterisk * ) 指的是地址1776中存储的数值,即25。注意加或不加星号*的不同(下面代码中注释显示了如何读这两个不同的表达式):
beth = ted; // beth 等于 ted ( 1776 )
beth = *ted; // beth 等于 ted 所指向的数值 ( 25 )
地址或反引用操作符Operator of address or dereference (&)
它被用作一个变量前缀,可以被翻译为“„的地址”("address of"),因此:&variable1 可以被读作 variable1的地址("address of variable1" )。 引用操作符Operator of reference (*) 它表示要取的是表达式所表示的地址指向的内容。它可以被翻译为“„指向的数值” ("value pointed by")。
* mypointer 可以被读作 "mypointer指向的数值"。
继续使用上面开始的例子,看下面的代码:
andy = 25;
ted = &andy;
现在你应该可以清楚的看到以下等式全部成立:
andy == 25
&andy == 1776
ted == 1776
*ted == 25
第一个表达式很容易理解,因为我们有赋值语句andy=25;。第二个表达式使用了地址(或反引用)操作符(&) 来返回变量andy的地址,即 1776。第三个表达式很明显成立,因为第二个表达式为真,而我们给ted赋值的语句为ted = &andy;。第四个表达式使用了引用操作符 (*),相当于ted指向的地址中存储的数值,即25。
由此你也可以推断出,只要ted 所指向的地址中存储的数值不变,以下表达式也为真:
*ted == andy
声明指针型变量Declaring variables of type pointer
由于指针可以直接引用它所指向的数值,因此有必要在声明指针的时候指明它所指向的数据类型。指向一个整型int或浮点型float数据的指针与指向一个字符型char数据的指针并不相同。
因此,声明指针的格式如下:
type * pointer_name;
这里,type 是指针所指向的数据的类型,而不是指针自己的类型。例如: int * number;
char * character;
float * greatnumber;
它们是3个指针的声明,每一个指针指向一种不同数据类型。这三个指针本身其实在内存中占用同样大小的内存空间(指针的大小取决于不同的操作系统),但它们所指向的数据是不同的类型,并占用不同大小的内存空间,一个是整型int,一个是字符型char ,还有一个是浮点型float。
需要强调的一点是,在指针声明时的星号asterisk (*) 仅表示这里声明的是一个指针,不要把它和前面我们用过的引用操作符混淆,虽然那也是写成一个星号 (*)。它们只是用同一符号表示的两个不同任务。
// my first pointer value1==10 / value2==20 #include
int main ( ) {
int value1 = 5, value2 = 15;
int * mypointer;
mypointer = &value1;
*mypointer = 10;
mypointer = &value2;
*mypointer = 20;
cout << "value1==" << value1 << "/ value2==" << value2;
return 0;
}
注意变量value1 和 value2 是怎样间接的被改变数值的。首先我们使用 ampersand sign (&) 将value1的地址赋给mypointer 。然后我们将10 赋给 mypointer所指向的数值,它其实指向value1的地址,因此,我们间接的修改了value1的数值。
为了让你了解在同一个程序中一个指针可以被用作不同的数值,我们在这个程序中用value2 和同一个指针重复了上面的过程。
下面是一个更复杂一些的例子:
// more pointers value1==10 / value2==20 #include
int main () {
int value1 = 5, value2 = 15;
int *p1, *p2;
p1 = &value1; // p1 = address of value1
p2 = &value2; // p2 = address of value2
*p1 = 10; // value pointed by p1 = 10
*p2 = *p1; // value pointed by p2 = value pointed by p1
p1 = p2; // p1 = p2 (value of pointer copied)
*p1 = 20; // value pointed by p1 = 20
cout << "value1==" << value1 << "/ value2==" << value2;
return 0;
}
上面每一行都有注释说明代码的意思:ampersand (&) 为"address of",asterisk (*) 为 "value pointed by"。注意有些包含p1 和p2 的表达式不带星号。加不加星号的含义十分不同:星号(*)后面跟指针名称表示指针所指向的地方,而指针名称不加星号(*)表示指针本身的数值,即它所指向的地方的地址。 另一个需要注意的地方是这一行:
int *p1, *p2;
声明了上例用到的两个指针,每个带一个星号(*),因为是这一行定义的所有指针都是整型int (而不是 int*)。原因是引用操作符(*) 的优先级顺序与类型声明的相同,因此,由于它们都是向右结合的操作,星号被优先计算。我们在 section 1.3: Operators 中已经讨论过这些。注意在声明每一个指针的时候前面加上星号asterisk (*)。
指针和数组Pointers and arrays 数组的概念与指针的概念联系非常解密。其实数组的标识相当于它的第一个元素的地址,就像一个指针相当于它所指向的第一个元素的地址,因此其实它们是同一个东西。例如,假设我们有以下声明:
int numbers [20];
int * p;
下面的赋值为合法的:
p = numbers;
这里指针p 和numbers 是等价的,它们有相同的属性,唯一的不同是我们可以给指针p赋其它的数值,而numbers 总是指向被定义的20个整数组中的第一个。所以,p只是一个普通的指针变量,而与之不同,numbers 是一个指针常量(constant pointer),数组名的确是一个指针常量。因此虽然前面的赋值表达式是合法的,但下面的不是:
numbers = p;
因为numbers 是一个数组(指针常量),常量标识不可以被赋其它数值。 由于变量的特性,以下例子中所有包含指针的表达式都是合法的: // more pointers 10, 20, 30, 40, 50,
#include
int main () {
int numbers[5];
int * p;
p = numbers;
*p = 10;
p++;
*p = 20;
p = &numbers[2];
*p = 30;
p = numbers + 3;
*p = 40;
p = numbers;
*(p+4) = 50;
for (int n=0; n<5; n++)
cout << numbers[n] << ", ";
return 0;
}
在数组一章中我们使用了括号[]来指明我们要引用的数组元素的索引(index)。中括号[]也叫位移(offset)操作符,它相当于在指针中的地址上加上括号中的数字。例如,下面两个表达式互相等价:
a[5] = 0; // a [offset of 5] = 0 *(a+5) = 0; // pointed by (a+5) = 0 不管a 是一个指针还是一个数组名, 这两个表达式都是合法的。
指针初始化Pointer initialization 当声明一个指针的时候我们可能需要同时指定它们指向哪个变量, int number;
int *tommy = &number;
这相当于:
int number;
int *tommy;
tommy = &number;
当给一个指针赋值的时候,我们总是赋给它一个地址值,而不是它所指向数据的
只是用来指明它是指针,值。你必须考虑到在声明一个指针的时候,星号 (*)
而从不表示引用操作符reference operator (*)。记住,它们是两种不同操作,虽然它们写成同样的符号。因此,我们要注意不要将以上的代码与下面的代码混淆:
int number;
int *tommy;
*tommy = &number;
这些代码也没有什么实际意义。
在定义数组指针的时候,编译器允许我们在声明变量指针的同时对数组进行初始化,初始化的内容需要是常量,例如:
char * terry = "hello";
在这个例子中,内存中预留了存储"hello" 的空间,并且terry被赋予了向这个内存块的第一个字符(对应’h’)的指针。假设"hello"存储在地址1702,下图显示了上面的定义在内存中状态:
这里需要强调,terry 存储的是数值1702 ,而不是'h' 或 "hello",虽然1702 指向这些字符。
指针terry 指向一个字符串,可以被当作数组一样使用(数组只是一个常量指针)。例如,如果我们的心情变了,而想把terry指向的内容中的字符'o' 变为符号'!' ,我们可以用以下两种方式的任何一种来实现:
terry[4] = '!';
*(terry+4) = '!';
记住写 terry[4] 与*(terry+4)是一样的,虽然第一种表达方式更常用一些。以上两个表达式都会实现以下改变:
指针的数学运算Arithmetic of pointers 对指针进行数学运算与其他整型数据类型进行数学运算稍有不同。首先,对指针只有加法和减法运算,其它运算在指针世界里没有意义。但是指针的加法和减法的具体运算根据它所指向的数据的类型的大小的不同而有所不同。 我们知道不同的数据类型在内存中占用的存储空间是不一样的。例如,对于整型数据,字符char 占用1 的字节(1 byte),短整型short 占用2 个字节,长整型long 占用4个字节。
假设我们有3个指针:
char *mychar;
short *myshort;
long *mylong;
而且我们知道他们分别指向内存地址1000, 2000 和3000。
因此如果我们有以下代码:
mychar++;
myshort++;
mylong++;
就像你可能想到的,mychar的值将会变为1001。而myshort 的值将会变为2002,mylong的值将会变为3004。原因是当我们给指针加,时,我们实际是让该指针
指向下一个与它被定义的数据类型的相同的元素。因此,它所指向的数据类型的长度字节数将会被加到指针的数值上。以上过程可以由下图表示:
这一点对指针的加法和减法运算都适用。如果我们写以下代码,它们与上面例子的作用一抹一样: mychar = mychar + 1; myshort = myshort + 1;
mylong = mylong + 1;
这里需要提醒你的是,递增 (++) 和递减 (--) 操作符比引用操作符reference operator (*)有更高的优先级,因此,以下的表达式有可能引起歧义: *p++;
*p++ = *q++;
第一个表达式等同于*(p++) ,它所作的是增加p (它所指向的地址,而不是它存储的数值)。
在第二个表达式中,因为两个递增操作(++) 都是在整个表达式被计算之后进行而不是在之前,所以*q 的值首先被赋予*p ,然后q 和p 都增加1。它相当于: *p = *q;
p++;
q++;
像通常一样,我们建议使用括号()以避免意想不到的结果。
指针的指针Pointers to pointers
C++ 允许使用指向指针的指针。要做到这一点,我们只需要在每一层引用之前加星号(*)即可:
char a;
char * b;
char ** c;
a = 'z';
b = &a;
c = &b;
假设随机选择内存地址为7230, 8092 和10502,以上例子可以用下图表示:
(方框内为变量的内容;方框下面为内存地址)
这个例子中新的元素是变量c,关于它我们可以从3个方面来讨论,每一个方面对应了不同的数值:
c 是一个(char **)类型的变量,它的值是8092
*c 是一个(char*)类型的变量,它的值是7230
**c 是一个(char)类型的变量,它的值是'z'
空指针void pointers
指针void 是一种特殊类型的指针。void 指针可以指向任意类型的数据,可以是整数,浮点数甚至字符串。唯一个限制是被指向的数值不可以被直接引用(不可以对它们使用引用星号*),因为它的长度是不定的,因此,必须使用类型转换操作或赋值操作来把void 指针指向一个具体的数据类型。 它的应用之一是被用来给函数传递通用参数:
// integer increaser 6, 10, 13
#include
void increase (void* data, int type) {
switch (type) {
case sizeof(char) : (*((char*)data))++; break;
case sizeof(short): (*((short*)data))++; break;
case sizeof(long) : (*((long*)data))++; break;
}
}
int main () {
char a = 5;
short b = 9;
long c = 12;
increase (&a,sizeof(a));
increase (&b,sizeof(b));
increase (&c,sizeof(c));
cout << (int) a << ", " << b << ", " << c;
return 0;
}
sizeof 是C++的一个操作符,用来返回其参数的长度字节数常量。例如,
sizeof(char) 返回 1,因为 char 类型是,字节长数据类型。
函数指针Pointers to functions C++ 允许对指向函数的指针进行操作。它最大的作用是把一个函数作为参数传递
给另外一个函数。声明一个函数指针像声明一个函数原型一样,除了函数的名字
需要被括在括号内并在前面加星号asterisk (*)。例如: // pointer to functions 8
#include
int addition (int a, int b) {
return (a+b);
}
int subtraction (int a, int b) {
return (a-b);
}
int (*minus)(int,int) = subtraction;
int operation (int x, int y, int (*functocall)(int,int)) {
int g;
g = (*functocall)(x,y);
return (g);
}
int main () {
int m,n;
m = operation (7, 5, addition);
n = operation (20, m, minus);
cout <title,50);
cout << "Enter year: ";
cin.getline (buffer,50);
pmovie->year = atoi (buffer);
cout << "\nYou have entered:\n";
cout << pmovie->title;
cout << " (" << pmovie->year << ")\n";
return 0;
}
上面的代码中引入了一个重要的操作符:->。这是一个引用操作符,常与结构或
类的指针一起使用,以便引用其中的成员元素,这样就避免使用很多括号。例如,
我们用:
pmovie->title
来代替:
(*pmovie).title
以上两种表达式pmovie->title 和 (*pmovie).title 都是合法的,都表示取指
针pmovie 所指向的结构其元素title 的值。我们要清楚将它和以下表达区分
开:
*pmovie.title
它相当于
*(pmovie.title)
表示取结构pmovie 的元素title 作为指针所指向的值,这个表达式在本例中没有意义,因为title本身不是指针类型。
下表中总结了指针和结构组成的各种可能的组合:
表达式 描述 等价于 pmovie.title 结构pmovie 的元素title
pmovie->title 指针pmovie 所指向的结构其元素title 的值 (*pmovie).title *pmovie.title 结构pmovie 的元素title 作为指针所指向的值 *(pmovie.title)
结构嵌套(Nesting structures) 结构可以嵌套使用,即一个结构的元素本身又可以是另一个结构类型。例如: struct movies_t {
char title [50];
int year;
}
struct friends_t {
char name [50];
char email [50];
movies_t favourite_movie;
} charlie, maria;
friends_t * pfriends = &charlie; 因此,在有以上声明之后,我们可以使用下面的表达式: charlie.name
maria.favourite_movie.title
charlie.favourite_movie.year pfriends->favourite_movie.year (以上最后两个表达式等价)
本节中所讨论的结构的概念与C语言中结构概念是一样的。然而,在C++中,结构的概念已经被扩展到与类(class)相同的程度,只是它所有的元素都是公开的(public)。在后面的章节4.1“类”中, 我们将进一步深入讨论这个问题。
3.6 自定义数据类型 (User defined data types) 前面我们已经看到过一种用户(程序员)定义的数据类型:结构。除此之外,还有一些其它类型的用户自定义数据类型:
定义自己的数据类型 (typedef)
C++ 允许我们在现有数据类型的基础上定义我们自己的数据类型。我们将用关键字typedef来实现这种定义,它的形式是:
typedef existing_type new_type_name;
这里 existing_type 是C++ 基本数据类型或其它已经被定义了的数据类型,new_type_name 是我们将要定义的新数据类型的名称。例如: typedef char C;
typedef unsigned int WORD;
typedef char * string_t;
typedef char field [50];
在上面的例子中,我们定义了四种新的数据类型: C, WORD, string_t 和 field ,它们分别代替 char, unsigned int, char* 和 char[50] 。这样,我们就可以安全的使用以下代码:
C achar, anotherchar, *ptchar1; WORD myword;
string_t ptchar2;
field name;
如果在一个程序中我们反复使用一种数据类型,而在以后的版本中我们有可能改变该数据类型的情况下,typedef 就很有用了。或者如果一种数据类型的名称太长,你想用一个比较短的名字来代替,也可以是用typedef。
联合(Union)
联合(Union) 使得同一段内存可以被按照不同的数据类型来访问,数据实际是存储在同一个位置的。它的声明和使用看起来与结构(structure)十分相似,但实际功能是完全不同的:
union model_name {
type1 element1;
type2 element2;
type3 element3;
.
.
} object_name;
union 中的所有被声明的元素占据同一段内存空间,其大小取声明中最长的元素的大小。例如:
union mytypes_t {
char c;
int i;
float f;
} mytypes;
定义了3个元素:
mytypes.c
mytypes.i
mytypes.f
每一个是一种不同的数据类型。既然它们都指向同一段内存空间,改变其中一个元素的值,将会影响所有其他元素的值。
union 的用途之一是将一种较长的基本类型与由其它比较小的数据类型组成的结构(structure)或数组(array)联合使用,例如:
union mix_t{
long l;
struct {
short hi;
short lo;
} s;
char c[4];
} mix;
以上例子中定义了3个名称:mix.l, mix.s 和 mix.c,我们可以通过这3个名字来访问同一段4 bytes长的内存空间。至于使用哪一个名字来访问,取决于我们想使用什么数据类型,是long, short 还是 char 。下图显示了在这个联合(union)中各个元素在内存中的的可能结构,以及我们如何通过不同的数据类型进行访问:
匿名联合(Anonymous union)
在 C++ 我们可以选择使联合(union)匿名。如果我们将一个union包括在一个结构(structure)的定义中,并且不赋予它object名称 (就是跟在花括号{}后面的名字),这个union就是匿名的。这种情况下我们可以直接使用union中元素的名字来访问该元素,而不需要再在前面加 union对象的名称。在下面的例子中,我们可以看到这两种表达方式在使用上的区别:
union anonymous union
struct { struct {
char title[50]; char title[50];
char author[50]; char author[50];
union { union {
float dollars; float dollars;
int yens; int yens;
} price; };
} book; } book;
以上两种定义的唯一区别在于左边的定义中我们给了union一个名字price,而在右边的定义中我们没给。在使用时的区别是当我们想访问一个对象(object)的元素dollars 和yens 时,在前一种定义的情况下,需要使用: book.price.dollars
book.price.yens
而在后面一种定义下,我们直接使用:
book.dollars
book.yens
再一次提醒,因为这是一个联合(union),域dollars 和yens 占据的是同一块内存空间,所以它们不能被用来存储两个不同的值。也就是你可以使用一个dollars 或yens的价格,但不能同时使用两者。
枚举Enumerations (enum)
枚举(Enumerations)可以用来生成一些任意类型的数据,不只限于数字类型或字符类型,甚至常量true 和false。它的定义形式如下:
enum model_name {
value1,
value2,
value3,
.
.
} object_name;
例如,我们可以定义一种新的变量类型叫做color_t 来存储不同的颜色: enum colors_t {black, blue, green, cyan, red, purple, yellow, white}; 注意在这个定义里我们没有使用任何基本数据类型。换句话说,我们创造了一种的新的数据类型,而它并没有基于任何已存在的数据类型:类型color_t,花括号{}中包括了它的所有的可能取值。例如,在定义了colors_t 列举类型后,我们可以使用以下表达式 :
colors_t mycolor;
mycolor = blue;
if (mycolor == green) mycolor = red; 实际上,我们的枚举数据类型在编译时是被编译为整型数值的,而它的数值列表可以是任何指定的整型常量 。如果没有指定常量,枚举中第一个列出的可能值为0 ,后面的每一个值为前面一个值加1。因此,在我们前面定义的数据类型colors_t 中,black 相当于0, blue 相当于 1, green 相当于2 ,后面依此类推。
如果我们在定义枚举数据类型的时候明确指定某些可能值(例如第一个)的等价整数值,后面的数值将会在此基础上增加,例如:
enum months_t { january=1, february, march, april,
may, june, july, august,
september, october, november, december} y2k;
在这个例子中,枚举类型months_t的变量y2k 可以是12种可能取值中的任何一个,从january 到 december ,它们相当于数值1 到 12,而不是0 到 11 ,因为我们已经指定 january 等于1。
第六章 C++ 标准函数库
C++ Standard Library
文件的输入输出 (Input/Output with files)
C++ 通过以下几个类支持文件的输入输出:
, ofstream: 写操作(输出)的文件类 (由ostream引申而来)
, ifstream: 读操作(输入)的文件类(由istream引申而来)
, fstream: 可同时读写操作的文件类 (由iostream引申而来)
打开文件(Open a file)
对这些类的一个对象所做的第一个操作通常就是将它和一个真正的文件联系起来,也就是说打开一个文件。被打开的文件在程序中由一个流对象(stream object)来表示 (这些类的一个实例) ,而对这个流对象所做的任何输入输出操作实际就是对该文件所做的操作。
要通过一个流对象打开一个文件,我们使用它的成员函数open(): void open (const char * filename, openmode mode);
这里filename 是一个字符串,代表要打开的文件名,mode 是以下标志符的一个组合:
ios::in 为输入(读)而打开文件
ios::out 为输出(写)而打开文件
ios::ate 初始位置:文件尾
ios::app 所有输出附加在文件末尾
ios::trunc 如果文件已存在则先删除该文件
ios::binary 二进制方式
这些标识符可以被组合使用,中间以”或”操作符(|)间隔。例如,如果我们想要以二进制方式打开文件"example.bin" 来写入一些数据,我们可以通过以下方式调用成员函数open()来实现:
ofstream file;
file.open ("example.bin", ios::out | ios::app | ios::binary);
ofstream, ifstream 和 fstream所有这些类的成员函数open 都包含了一个默认打开文件的方式,这三个类的默认方式各不相同:
类 参数的默认方式
ofstream ios::out | ios::trunc
ifstream ios::in
fstream ios::in | ios::out
只有当函数被调用时没有声明方式参数的情况下,默认值才会被采用。如果函数被调用时声明了任何参数,默认值将被完全改写,而不会与调用参数组合。 由于对类ofstream, ifstream 和 fstream 的对象所进行的第一个操作通常都是打开文件,这些类都有一个构造函数可以直接调用open 函数,并拥有同样的参数。这样,我们就可以通过以下方式进行与上面同样的定义对象和打开文件的操作:
ofstream file ("example.bin", ios::out | ios::app | ios::binary); 两种打开文件的方式都是正确的。
你可以通过调用成员函数is_open()来检查一个文件是否已经被顺利的打开了: bool is_open();
它返回一个布尔(bool)值,为真(true)代表文件已经被顺利打开,假( false )则相反。
关闭文件(Closing a file)
当文件读写操作完成之后,我们必须将文件关闭以使文件重新变为可访问的。关闭文件需要调用成员函数close(),它负责将缓存中的数据排放出来并关闭文件。它的格式很简单:
void close ();
这个函数一旦被调用,原先的流对象(stream object)就可以被用来打开其它的文件了,这个文件也就可以重新被其它的进程(process)所有访问了。 为防止流对象被销毁时还联系着打开的文件,析构函数(destructor)将会自动调用关闭函数close。
文本文件(Text mode files) 类ofstream, ifstream 和fstream 是分别从ostream, istream 和iostream 中引申而来的。这就是为什么 fstream 的对象可以使用其父类的成员来访问数据。 一般来说,我们将使用这些类与同控制台(console)交互同样的成员函数(cin 和 cout)来进行输入输出。如下面的例题所示,我们使用重载的插入操作符<<:
// writing on a text file file example.txt
#include This is a line.
This is another line.
int main () {
ofstream examplefile ("example.txt");
if (examplefile.is_open()) {
examplefile << "This is a line.\n";
examplefile << "This is another line.\n";
examplefile.close();
}
return 0;
}
从文件中读入数据也可以用与 cin的使用同样的方法:
// reading a text file This is a line.
#include This is another line.
#include
#include
int main () {
char buffer[256];
ifstream examplefile ("example.txt");
if (! examplefile.is_open())
{ cout << "Error opening file"; exit (1); }
while (! examplefile.eof() ) {
examplefile.getline (buffer,100);
cout << buffer << endl;
}
return 0;
}
上面的例子读入一个文本文件的内容,然后将它打印到屏幕上。注意我们使用了一个新的成员函数叫做eof ,它是ifstream 从类 ios 中继承过来的,当到达文件末尾时返回true 。
状态标志符的验证(Verification of state flags) 除了eof()以外,还有一些验证流的状态的成员函数(所有都返回bool型返回值):
, bad()
如果在读写过程中出错,返回 true 。例如:当我们要对一个不是打开为
写状态的文件进行写入时,或者我们要写入的设备没有剩余空间的时候。
, fail()
除了与bad() 同样的情况下会返回 true 以外,加上格式错误时也返回
true ,例如当想要读入一个整数,而获得了一个字母的时候。
, eof()
如果读文件到达文件末尾,返回true。
, good()
这是最通用的:如果调用以上任何一个函数返回true 的话,此函数返回
false 。
要想重置以上成员函数所检查的状态标志,你可以使用成员函数clear(),没有参数。
获得和设置流指针(get and put stream pointers) 所有输入/输出流对象(i/o streams objects)都有至少一个流指针:
, ifstream, 类似istream, 有一个被称为get pointer的指针,指向下一个将被读取的元
素。
, ofstream, 类似 ostream, 有一个指针 put pointer ,指向写入下一个元素的位置。
, fstream, 类似 iostream, 同时继承了get 和 put
我们可以通过使用以下成员函数来读出或配置这些指向流中读写位置的流指针:
, tellg() 和 tellp()
这两个成员函数不用传入参数,返回pos_type 类型的值(根据ANSI-C++
标准) ,就是一个整数,代表当前get 流指针的位置 (用tellg) 或 put
流指针的位置(用tellp).
, seekg() 和seekp()
这对函数分别用来改变流指针get 和put的位置。两个函数都被重载为
两种不同的原型:
seekg ( pos_type position );
seekp ( pos_type position );
使用这个原型,流指针被改变为指向从文件开始计算的一个绝对位置。要
求传入的参数类型与函数 tellg 和tellp 的返回值类型相同。
seekg ( off_type offset, seekdir direction );
seekp ( off_type offset, seekdir direction );
使用这个原型可以指定由参数direction决定的一个具体的指针开始计
算的一个位移(offset)。它可以是:
ios::beg 从流开始位置计算的位移
ios::cur 从流指针当前位置开始计算的位移
ios::end 从流末尾处开始计算的位移
流指针 get 和 put 的值对文本文件(text file)和二进制文件(binary file)的计算方法都是不同的,因为文本模式的文件中某些特殊字符可能被修改。由于这个原因,建议对以文本文件模式打开的文件总是使用seekg 和 seekp的第一种原型,而且不要对tellg 或 tellp 的返回值进行修改。对二进制文件,你可以任意使用这些函数,应该不会有任何意外的行为产生。
以下例子使用这些函数来获得一个二进制文件的大小:
// obtaining file size size of example.txt is 40 bytes.
#include
#include
const char * filename = "example.txt";
int main () {
long l,m;
ifstream file (filename,
ios::in|ios::binary);
l = file.tellg();
file.seekg (0, ios::end);
m = file.tellg();
file.close();
cout << "size of " << filename;
cout << " is " << (m-l) << " bytes.\n";
return 0;
}
二进制文件(Binary files)
在二进制文件中,使用<< 和>>,以及函数(如getline)来操作符输入和输出数据,没有什么实际意义,虽然它们是符合语法的。
文件流包括两个为顺序读写数据特殊设计的成员函数:write 和 read。第一个函数 (write) 是ostream 的一个成员函数,都是被ofstream所继承。而read 是istream 的一个成员函数,被ifstream 所继承。类 fstream 的对象同时拥有这两个函数。它们的原型是:
write ( char * buffer, streamsize size ); read ( char * buffer, streamsize size ); 这里 buffer 是一块内存的地址,用来存储或读出数据。参数size 是一个整数值,表示要从缓存(buffer)中读出或写入的字符数。
// reading binary file The complete file is in a buffer
#include
#include
const char * filename = "example.txt";
int main () {
char * buffer;
long size;
ifstream file (filename,
ios::in|ios::binary|ios::ate);
size = file.tellg();
file.seekg (0, ios::beg);
buffer = new char [size];
file.read (buffer, size);
file.close();
cout << "the complete file is in a buffer";
delete[] buffer;
return 0;
}
缓存和同步(Buffers and Synchronization) 当我们对文件流进行操作的时候,它们与一个streambuf 类型的缓存(buffer)联系在一起。这个缓存(buffer)实际是一块内存空间,作为流(stream)和物理文件的媒介。例如,对于一个输出流,每次成员函数put (写一个单个字符)被调用,这个字符不是直接被写入该输出流所对应的物理文件中的,而是首先被插入到该流的缓存(buffer)中。
当缓存被排放出来(flush)时,它里面的所有数据或者被写入物理媒质中(如果是一个输出流的话),或者简单的被抹掉(如果是一个输入流的话)。这个过程称为同步(synchronization),它会在以下任一情况下发生:
, 当文件被关闭时: 在文件被关闭之前,所有还没有被完全写出或读取的缓存都将被
同步。
, 当缓存buffer 满时:缓存Buffers 有一定的空间限制。当缓存满时,它会被自动同步。
, 控制符明确指明:当遇到流中某些特定的控制符时,同步会发生。这些控制符包括:
flush 和endl。
, 明确调用函数sync(): 调用成员函数sync() (无参数)可以引发立即同步。这个函数返
回一个int 值,等于-1 表示流没有联系的缓存或操作失败。