//相当于a=a+226/178
递增和递减Increaseanddecrease
书写简练的另一个例子是递增(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在增加前所存储的值被用在了表达式的计算中。注意以下两个例子的不同:
例1B=3;A=++B;
//A的值为4,B的值为4A=B++;
//A的值为3,B的值为4
在第一个例子中,B在它的值被赋给A之前增加1。而在第二个例子中B原来的值3被赋给A然后B的值才加1变为4。
B=3;
例2
关系运算符Relationaloperators(==,!=,>,<,>=,<=)
27/178
我们用关系运算符来比较两个表达式。如ANSI-C++标准中指出的,关系预算的结果是一个bool值,根据运算结果的不同,它的值只能是真true或false。
例如我们想通过比较两个表达式来看它们是否相等或一个值是否比另一个的值大。以下为C++的关系运算符:
==
相等Equal
!=不等Different>大于Greaterthan<小于Lessthan>=<=
大于等于Greaterorequalthan小于等于Lessorequalthan
下面你可以看到一些实际的例子:(7==5)将返回false.(5>4)(3!=2)
将返回true.将返回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.
28/178
注意:运算符=(单个等号)不同于运算符==(双等号)。第一个是赋值运算符(将等号右边的表达式值赋给左边的变量);第二个(==)是一个判断等于的关系运算符,用来判断运算符两边的表达式是否相等。因此在上面例子中最后一个表达式((b=2)==a),我们首先将数值2赋给变量b,然后把它和变量a进行比较。因为变量a中存储的也是数值2,所以整个运算的结果为true。
在ANSI-C++标准出现之前的许多编译器中,就像C语言中,关系运算并不返回值为真true或假false的bool值,而是返回一个整型数值最为结果,它的数值可以为0,代表\"false\"或一个非0数值(通常为1)来代表\"true\"。
逻辑运算符Logicoperators(!,&&,||)
运算符!等同于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
truetruefalsefalse第二个操作数b
truefalsetruefalse结果a&&b
truefalsefalsefalse结果a||btruetruetruefalse例如:
((5==5)&&(3>6))返回false(true&&false).
29/178
((5==5)||(3>6))返回true(true||false).
条件运算符Conditionaloperator(?)
条件运算符计算一个表达式的值并根据表达式的计算结果为真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:ba>b?a:b
返回a,因为5大于3.返回较大值,a或b.
//条件运算符例子
#includeusingnamespacestd;intmain(){
inta,b,c;
a=2;b=7;
c=(a>b)?a:b;
cout<30/178return0;}7
上面的例子中a的值为2,b的值为7,所以表达式(a>b)运算值为假(false),所以整个表达式(a>b)?a:b要取分号后面的值,也就是b的值7。因此最后输出c的值为7。
逗号运算符(,)
逗号运算符(,)用来分开多个表达式,并只取最右边的表达式的值返回。
例如有以下代码:a=(b=3,b+2);
这行代码首先将3赋值给变量b,然后将b+2赋值给变量a。所以最后变量a的值为5,而变量b的值为3。
位运算符BitwiseOperators(&,|,^,~,<<,>>)
位运算符以比特位改写变量存储的数值,也就是改写变量值的二进制表示:opasm&AND|OR^XOR~NOT<<>>
Description逻辑与LogicAND逻辑或LogicOR
逻辑异或LogicalexclusiveOR
对1取补(位反转)Complementtoone(bitinversion)SHLSHR
左移ShiftLeft右移ShiftRight
变量类型转换运算符Explicittypecastingoperators
变量类型转换运算符可以将一种类型的数据转换为另一种类型的数据。在写C++中有几种方法可以实现这种操作,最常用的一种,也是与C兼容的一种,是在原转换的表达式前面加用括号()括起的新数据类型:
31/178
inti;
floatf=3.14;i=(int)f;
以上代码将浮点型数字3.14转换成一个整数值(3)。这里类型转换操作符为(int)。在C++中实现这一操作的另一种方法是使用构造函数constructor的形式:在要转换的表达式前加变量类型并将表达式括在括号中:
i=int(f);
以上两种类型转换的方法在C++中都是合法的。另外ANSI-C++针对面向对象编程(objectorientedprogramming)增加了新的类型转换操作符(参考Section5.4,Advancedclasstype-casting).
sizeof()
这个运算符接受一个输入参数,该参数可以是一个变量类型或一个变量自己,返回该变量类型(variabletype)或对象(object)所占的字节数:
a=sizeof(char);
这将会返回1给a,因为char是一个常为1个字节的变量类型。sizeof返回的值是一个常数,因此它总是在程序执行前就被固定了。
其它运算符
在本教程后面的章节里我们将看到更多的运算符,比如指向指针的运算或面向对象编程特有的运算,等等,我们会在它们各自的章节里进行详细讨论。
运算符的优先度Precedenceofoperators
当多个操作数组成复杂的表达式时,我们可能会疑惑哪个运算先被计算,哪个后被计算。例如以下表达式:
a=5+7%2
我们可以怀疑它实际上表示:
a=5+(7%2)结果为6,还是a=(5+7)%2结果为0?
32/178
正确答案为第一个,结果为6。每一个运算符有一个固定的优先级,不仅对数算符(我们可能在学习数学的时候已经很了解它们的优先顺序了),所有在C++中出现的运算符都有优先级。从最从最高级到最低级,运算的优先级按下表排列:
优先级Level
操作符
Operator说明DescriptionGrouping1::
范围
从左到右结合方向
2()[].->++--dynamic_caststatic_castreinterpret_castconst_casttypeid后缀从左到右3++--~!sizeofnewdelete一元(前缀)*&+-指针和取地址一元符号
从右到左
从左到右
从右到左
4(type)类型转换
5.*->*指向成员的指针
6*/%乘、除、取模从左到右7+-加减
从左到右从左到右关系操作符
从左到右
8<<>>位移9<><=>=
10==!=等于、不等于从左到右11&12^13|14&&
按位与运算
从左到右
按位异或运算从左到右按位或运算逻辑与运算
从左到右从左到右
33/178
15||16?:
逻辑或运算条件运算
从左到右从右到左
从右到左
17=*=/=%=+=-=>>=<<=&=^=|=赋值运算18,
逗号
从左到右
结合方向Grouping定义了当有同优先级的多个运算符在一起时,哪一个必须被首先运算,最右边的还是最左边的。
所有这些运算符的优先级顺序可以通过使用括号parenthesissigns(和)来控制,而且更易读懂,例如以下例子:
a=5+7%2;
根据我们想要实现的计算的不同,可以写成:a=5+(7%2);或者a=(5+7)%2;
所以如果你想写一个复杂的表达式而不敢肯定各个运算的执行顺序,那么就加上括号。这样还可以使代码更易读懂。
1.5控制台交互(Communicationthroughconsole)
控制台(console)是电脑的最基本交互接口,通常包括键盘(keyboard)和屏幕(screen)。键盘通常为标准输入设备,而屏幕为标准输出设备。
在C++的iostream函数库中,一个程序的标准输入输出操作依靠两种数据流:cin给输入使用和cout给输出使用。另外,cerr和clog也已经被实现——它们是两种特殊设计的数据流专门用来显示出错信息。它们可以被重新定向到标准输出设备或到一个日志文件(logfile)。
因此cout(标准输出流)通常被定向到屏幕,而cin(标准输入流)通常被定向到键盘。
通过控制这两种数据流,你可以在程序中与用户交互,因为你可以在屏幕上显示输出并从键盘接收用户的输入。
输出Output(cout)
34/178
输出流cout与重载(overloaded)运算符<<一起使用:
cout<<\"Outputsentence\";//打印Outputsentence到屏幕上cout<<120;//打印数字120到屏幕上cout<运算符<<又叫插入运算符(insertionoperator)因为它将后面所跟的数据插入到它前面的数据流中。在以上的例子中,字符串常量Outputsentence,数字常量120和变量x先后被插入输出流cout中。注意第一句中字符串常量是被双引号引起来的。每当我们使用字符串常量的时候,必须用引号把字符串引起来,以便将它和变量名明显的区分开来。例如,下面两个语句是不同的:cout<<\"Hello\";//打印字符串Hello到屏幕上
cout<插入运算符insertionoperator(<<)可以在同一语句中被多次使用:cout<<\"Hello,\"<<\"Iam\"<<\"aC++sentence\";上面这一行语句将会打印Hello,IamaC++sentence到屏幕上。插入运算符(<<)的重复使用在我们想要打印变量和内容的组合内容或多个变量时有所体现:
cout<<\"Hello,Iam\"<必须注意,除非我们明确指定,cout并不会自动在其输出内容的末尾加换行符,因此下面的语句:cout<<\"Thisisasentence.\";cout<<\"Thisisanothersentence.\";将会有如下内容输出到屏幕:
Thisisasentence.Thisisanothersentence.
虽然我们分别调用了两次cout,两个句子还是被输出在同一行。所以,为了在输出中换行,我们必须插入一个换行符来明确表达这一要求。在C++中换行符可以写作\\n:
cout<<\"Firstsentence.\\n\";
35/178
cout<<\"Secondsentence.\\nThirdsentence.\";将会产生如下输出:Firstsentence.Secondsentence.Thirdsentence.
另外,你也可以用操作符endl来换行,例如:cout<<\"Firstsentence.\"<当操作符endl被用在bufferedstreams中时有一点特殊:它们被flushed。不过cout默认为unbuffered,所以不会被影响。你可以暂时不管这一点。你可以使用\\n或endl来指定cout输出换行,请注意前面所讲的两者的不同用法。
输入Input(cin)
C++中的标准输入是通过在cin数据流上重载运算符extraction(>>)来实现的。它后面必须跟一个变量以便存储读入的数据。例如:
intage;cin>>age;
声明一个整型变量age然后等待用户从键盘输入到cin并将输入值存储在这个变量中。
cin只能在键盘输入回车键(RETURN)后才能处理前面输入的内容。因此即使你只要求输入一个单独的字符,在用户按下回车键(RETURN)之前cin将不会处理用户的输入的字符。
在使用cin输入的时候必须考虑后面的变量类型。如果你要求输入一个整数,extraction(>>)后面必须跟一个整型变量,如果要求一个字符,后面必须跟一个字符型变量,如果要求一个字符串,后面必须跟一个字符串型变量。
36/178
//i/oexample
#includeintmain(){inti;cout<<\"Pleaseenteranintegervalue:\";cin>>i;
cout<<\"Thevalueyouenteredis\"<}Pleaseenteranintegervalue:702
Thevalueyouenteredis702anditsdoubleis1404.
使用程序的用户可以使引起错误的原因之一,即使是在最简单的需要用cin做输入的程序中(就像我们上面看到的这个程序)。因为如果你要求输入一个整数数值,而用户输入了一个名字(一个字符串),其结果可能导致程序产生错误操作,因为它不是我们期望从用户处获得的数据。当你使用由cin输入的数据的时候,你不得不假设程序的用户将会完全合作而不会在程序要求输入整数的时候输入他的名字。后面当我们看到怎样使用字符串的时候,我们将会同时看到一些解决这一类出错问题的办法。
你也可以利用cin要求用户输入多个数据:cin>>a>>b;等同于:cin>>a;cin>>b;
在以上两种情况下用户都必须输入两个数据,一个给变量a,一个给变量b。输入时两个变量之间可以以任何有效的空白符号间隔,包括空格,跳跃符tab或换行。
cin和字符串
我们可以像读取基本类型数据一样,使用cin和>>操作符来读取字符串,例如:cin>>mystring;
37/178
但是,cin>>只能读取一个单词,一旦碰到任何空格,读取操作就会停止。在很多时候这并不是我们想要的操作,比如我们希望用户输入一个英文句子,那么这种方法就无法读取完整的句子,因为一定会遇到空格。
要一次读取一整行输入,需要使用C++的函数getline,相对于是用cin,我们更建议使用getline来读取用户输入。
例如:
//读取字符串例子#include#includeusingnamespacestd;intmain(){
stringmystr;
cout<<\"What'syourname?\";getline(cin,mystr);
cout<<\"Hello\"<cout<<\"Ilike\"<}What'syourname?AquaHelloAqua.Whatisyourfavoritecolor?blueIlikebluetoo!
你可能注意到在上面的例子中,两次调用getline函数我们都是用了同一个字符串变量(mystr)。在第二次调用的时候,程序会自动用第二次输入的内容取代以前的内容。
字符串流(stringstream)
38/178
标准头文件定义了一个叫做stringstream的类,使用这个类可以对基于字符串的对象进行像流(stream)一样的操作。这样,我们可以对字符串进行抽取和插入操作,这对将字符串与数值互相转换非常有用。例如,如果我们想将一个字符串转换为一个整数,可以这样写:stringmystr(\"1204\");intmyint;
stringstream(mystr)>>myint;
这个例子中先定义了一个字符串类型的对象mystr,初始值为\"1204\",又定义了一个整数变量myint。然后我们使用stringstream类的构造函数定义了这个类的对象,并以字符串变量mystr为参数。因为我们可以像使用流一样使用stringstream的对象,所以我们可以像使用cin那样使用操作符>>后面跟一个整数变量来进行提取整数数据。这段代码执行之后变量myint存储的是数值1204。
//字符串流的使用示例#include#include#includeusingnamespacestd;intmain(){
stringmystr;floatprice=0;intquantity=0;
cout<<\"Enterprice:\";getline(cin,mystr);
stringstream(mystr)>>price;cout<<\"Enterquantity:\";getline(cin,mystr);
stringstream(mystr)>>quantity;
39/178
cout<<\"Totalprice:\"<}Enterprice:22.25Enterquantity:7Totalprice:155.75在这个例子中,我们要求用户输入数值,但不同于从标准输入中直接读取数值,我们使用函数getline从标注输入流cin中读取字符串对象(mystr),然后再从这个字符串对象中提取数值price和quantity。
通过使用这种方法,我们可以对用户的输入有更多的控制,因为它将用户输入与对输入的解释分离,只要求用户输入整行的内容,然后再对用户输入的内容进行检验操作。这种做法在用户输入比较集中的程序中是非常推荐使用的。
第二章控制结构和函数
(ControlstructuresandFunctions)1.控制结构ControlStructures2.函数IFunctionsI3.函数IIFunctionsII
2.1控制结构(ControlStructures)
一个程序的语句往往并不仅限于线性顺序结构。在程序的执行过程中它可能被分成两支执行,可能重复某些语句,也可能根据一些判断结果而执行不同的语句。因此C++提供一些控制结构语句(controlstructures)来实现这些执行顺序。
为了介绍程序的执行顺序,我们需要先介绍一个新概念:语句块(blockofinstructions)。一个语句块(Ablockofinstructions)是一组互相之间由分号semicolons(;)分隔开但整体被花括号curlybracketsigns:{and}括起来的语句。
本节中我们将看到的大多数控制结构允许一个通用的statement做参数,这个statement根据需要可以是一条语句,也可以是一组语句组成的语句块。如果我们只需
40/178
要一条语句做statement,它可以不被括在花括号({})内。但如果我们需要多条语句共同做statement,则必须把它们括在花括号内({})以组成一个语句块。
条件结构Conditionalstructure:ifandelse
条件结构用来实现仅在某种条件满足的情况下才执行一条语句或一个语句块。它的形式是:
if(condition)statement
这里condition是一个将被计算的表达式(expression)。如果表达式值为真,即条件(condition)为true,statement将被执行。否则,statement将被忽略(不被执行),程序从整个条件结构之后的下一条语句继续执行。
例如,以下程序段实现只有当变量x存储的值确实为100的时候才输出\"xis100\":if(x==100)cout<<\"xis100\";
如果我们需要在条件condition为真true的时候执行一条以上的语句,我们可以花括号{}将语句括起来组成一个语句块:
if(x==100){
cout<<\"xis\";cout<我们可以用关键字else来指定当条件不能被满足时需要执行的语句,它需要和if一起使用,形式是:if(condition)statement1elsestatement2例如:if(x==100)cout<<\"xis100\";else
cout<<\"xisnot100\";
41/178
以上程序如果x的值为100,则在屏幕上打出xis100,如果x不是100,而且也只有在x不是100的时候,屏幕上将打出xisnot100。
多个if+else的结构被连接起来使用来判断数值的范围。以下例子显示了如何用它来判断变量x中当前存储的数值是正值,负值还是既不正也不负,即等于0。
if(x>0)
cout<<\"xispositive\";elseif(x<0)
cout<<\"xisnegative\";else
cout<<\"xis0\";
记住当我们需要执行多条语句时,必须使用花括号{}将它们括起来以组成一个语句块blockofinstructions。
重复结构Iterationstructures或循环loops
循环Loops的目的是重复执行一组语句一定的次数或直到满足某种条件。while循环格式是:
while(表达式expression)语句statement
它的功能是当expression的值为真true时重复执行statement。例如,下面我们将用while循环来写一个倒计数程序://customcountdownusingwhile#includeintmain(){intn;cout<<\"Enterthestartingnumber>\";cin>>n;while(n>0){
42/178
cout<cout<<\"FIRE!\";return0;}Enterthestartingnumber>88,7,6,5,4,3,2,1,FIRE!
程序开始时提示用户输入一个倒计数的初始值。然后while循环开始,如果用户输入的数值满足条件n>0(即n比0大),后面跟的语句块将会被执行一定的次数,直到条件(n>0)不再满足(变为false)。
以上程序的所有处理过程可以用以下的描述来解释:从main开始:
1.用户输入一个数值赋给n.
2.while语句检查(n>0)是否成立,这时有两种可能:otrue:执行statement(到第3步)
ofalse:跳过statement.程序直接执行第5步.3.执行statement:cout<(将n的值打印在屏幕上,然后将n的值减1).4.语句块结束,自动返回第2步。5.继续执行语句块之后的程序:打印FIRE!,程序结束。
我们必须考虑到循环必须在某个点结束,因此在语句块之内(loop的statement之内)我们必须提供一些方法使得条件condition可以在某个时刻变为假false,否则循环将无限重复下去。在这个例子里,我们用语句--n;使得循环在重复一定的次数后变为false:当n变为0,倒计数结束。
do-while循环
43/178
格式:
do语句statementwhile(条件condition);
它的功能与while循环一抹一样,除了在do-while循环中是先执行statement然后才检查条件condition,而不想while循环中先检查条件然后才执行statement。这样,即使条件condition从来没有被满足过,statement仍至少被执行一次。例如,下面的程序重复输出(echoes)用户输入的任何数值,直到用户输入0为止。
//numberechoer#includeintmain(){unsignedlongn;do{
cout<<\"Enternumber(0toend):\";cin>>n;
cout<<\"Youentered:\"<}Enternumber(0toend):12345Youentered:12345Enternumber(0toend):160277Youentered:160277Enternumber(0toend):0Youentered:0
do-while循环通常被用在判断循环结束的条件是在循环语句内部被决定的情况下,比如以上的例子,在循环的语句块内用户的输入决定了循环是否结束。如果用户永远不输入0,则循环永远不会结束。
for循环格式是:
for(initialization;condition;increase)statement;
44/178
它的主要功能是当条件condition为真true时重复执行语句statement,类似while循环。但除此之外,for还提供了写初始化语句initialization和增值语句increase的地方。因此这种循环结构是特别为执行由计数器控制的循环而设计的。
它按以下方式工作:
1.执行初始化initialization。通常它是设置一个计数器变量(countervariable)的初始值,初始化仅被执行一次。
2.检查条件condition,如果条件为真true,继续循环,否则循环结束循环中语句statement被跳过。
3.执行语句statement。像以前一样,它可以是一个单独的语句,也可以是一个由花括号{}括起来的语句块。
4.最后增值域(increasefield)中的语句被执行,循环返回第2步。注意增值域中可能是任何语句,而不一定只是将计数器增加的语句。例如下面的例子中计数器实际为减1,而不是加1。
下面是用for循环实现的倒计数的例子://countdownusingaforloop#includeintmain(){for(intn=10;n>0;n--){cout<cout<<\"FIRE!\";return0;}10,9,8,7,6,5,4,3,2,1,FIRE!
初始化initialization和增值increase域是可选的(即可以为空)。但这些域为空的时候,它们和其他域之间间隔的分号不可以省略。例如我们可以写:for(;n<10;)来表示没有初始化和增值语句;或for(;n<10;n++)来表示有增值语句但没有初始化语句。
45/178
另外我们也可以在for循环初始化或增值域中放一条以上的语句,中间用逗号coma(,)隔开。例如假设我们想在循环中初始化一个以上的变量,可以用以下的程序来实现:
for(n=0,i=100;n!=i;n++,i--){
//whateverhere...}
如果n和i在循还内部都不被改变的话,这个循环将被执行50次:
n初始值为0,i初始值为100,条件是(n!=i)(即n不能等于i)。因为每次循环n加1,而且i减1,循环的条件将会在第50次循环之后变为假false(n和i都等于50)。
分支控制和跳转(Bifurcationofcontrolandjumps)break语句
通过使用break语句,即使在结束条件没有满足的情况下,我们也可以跳出一个循环。它可以被用来结束一个无限循环(infiniteloop),或强迫循环在其自然结束之前结束。例如,我们想要在倒计数自然结束之前强迫它停止(也许因为一个引擎故障):
//breakloopexample#includeintmain(){intn;for(n=10;n>0;n--){cout<cout<<\"countdownaborted!\";break;46/178
}return0;
}10,9,8,7,6,5,4,3,countdownaborted!continue语句
continue语句使得程序跳过当前循环中剩下的部分而直接进入下一次循环,就好像循环中语句块的结尾已经到了使得循环进入下一次重复。例如,下面例子中倒计数时我们将跳过数字5的输出:
//continueloopexample#includeintmain(){for(intn=10;n>0;n--){if(n==5)continue;cout<cout<<\"FIRE!\";return0;}10,9,8,7,6,4,3,2,1,FIRE!goto语句
通过使用goto语句可以使程序从一点跳转到另外一点。你必须谨慎只用这条语句,因为它的执行可以忽略任何嵌套。
跳转的目标点可以由一个标示符(label)来标明,该标示符作为goto语句的参数。一个标示符(label)由一个标识名称后面跟一个冒号colon(:)组成。
通常除了底层程序爱好者使用这条语句,它在结构化或面向对象的编程中并不常用。下面的例子中我们用goto来实现倒计数循环:
//gotoloopexample#includeintmain(){47/178
intn=10;loop:
cout<if(n>0)gotoloop;cout<<\"FIRE!\";return0;}10,9,8,7,6,5,4,3,2,1,FIRE!exit函数
exit是一个在cstdlib(stdlib.h)库中定义的函数。
exit的目的是一个特定的退出代码来结束程序的运行,它的原型(prototype)是:voidexit(intexitcode);
exitcode是由操作系统使用或被调用程序使用。通常exitcode为0表示程序正常结束,任何其他值表示程序执行过程中出现了错误。
选择结构TheselectiveStructure:switch
switch语句的语法比较特殊。它的目标是对一个表达式检查多个可能常量值,有些像我们在本节开头学习的把几个if和elseif语句连接起来的结构。它的形式是:
switch(expression){caseconstant1:blockofinstructions1break;
caseconstant2:blockofinstructions2break;...
48/178
default:
defaultblockofinstructions}
它按以下方式执行:
switch计算表达式expression的值,并检查它是否与第一个常量constant1相等,如果相等,程序执行常量1后面的语句块blockofinstructions1直到碰到关键字break,程序跳转到switch选择结构的结尾处。
如果expression不等于constant1,程序检查表达式expression的值是否等于第二个常量constant2,如果相等,程序将执行常量2后面的语句块blockofinstructions2直到碰到关键字break。
依此类推,直到最后如果表达式expression的值不等于任何前面的常量(你可以用case语句指明任意数量的常量值来要求检查),程序将执行默认区default:后面的语句,如果它存在的话。default:选项是可以省略的。
下面的两段代码段功能相同:switchexampleswitch(x){case1:
cout<<\"xis1\";break;case2:
cout<<\"xis2\";break;default:
cout<<\"valueofxunknown\";}if(x==1){cout<<\"xis1\";}
elseif(x==2){cout<<\"xis2\";
if-elseequivalent
49/178
}else{
cout<<\"valueofxunknown\";}
前面已经提到switch的语法有点特殊。注意每个语句块结尾包含的break语句。这是必须的,因为如果不这样做,例如在语句块blockofinstructions1的结尾没有break,程序执行将不会跳转到switch选择的结尾处(}),而是继续执行下面的语句块,直到第一次遇到break语句或到switch选择结构的结尾。因此,不需要在每一个case域内加花括号{}。这个特点同时可以帮助实现对不同的可能值执行相同的语句块。例如:
switch(x){case1:case2:case3:
cout<<\"xis1,2or3\";break;default:
cout<<\"xisnot1,2nor3\";}
注意switch只能被用来比较表达式和不同常量的值constants。因此我们不能够把变量或范围放在case之后,例如(case(n*2):)或(case(1..3):)都不可以,因为它们不是有效的常量。如果你需要检查范围或非常量数值,使用连续的if和elseif语句。
2.2函数I(FunctionsI)
通过使用函数(functions)我们可以把我们的程序以更模块化的形式组织起来,从而利用C++所能提供的所有结构化编程的潜力。
一个函数(function)是一个可以从程序其它地方调用执行的语句块。以下是它的格式:
typename(argument1,argument2,...)statement
50/178
这里:
?type是函数返回的数据的类型?name是函数被调用时使用的名
?argument是函数调用需要传入的参量(可以声明任意多个参量)。每个参量(argument)由一个数据类型后面跟一个标识名称组成,就像变量声明中一样(例如,intx)。参量仅在函数范围内有效,可以和函数中的其它变量一样使用,它们使得函数在被调用时可以传入参数,不同的参数用逗号(comma)隔开.
?statement是函数的内容。它可以是一句指令,也可以是一组指令组成的语句块。如果是一组指令,则语句块必须用花括号{}括起来,这也是我们最常见到情况。其实为了使程序的格式更加统一清晰,建议在仅有一条指令的时候也使用花括号,这是一个良好的编程习惯。
下面看一下第一个函数的例子://functionexample#includeintaddition(inta,intb){intr;r=a+b;return(r);}intmain(){intz;
z=addition(5,3);
cout<<\"Theresultis\"<51/178记得在我们教程开始时说过:一个C++程序总是从main函数开始执行。因此我们从那里开始。
我们可以看到main函数以定义一个整型变量z开始。紧跟着我们看到调用addition函数。我们可以看到函数调用的写法和上面函数定义本身十分相似:
参数有明显的对应关系。在main函数中我们调用addition函数,并传入两个数值:5和3,它们对应函数addition中定义的参数inta和intb。
当函数在main中被调用时,程序执行的控制权从main转移到函数addition。调用传递的两个参数的数值(5和3)被复制到函数的本地变量(localvariables)inta和intb中。
函数addition中定义了新的变量(intr;),通过表达式r=a+b;,它把a加b的结果赋给r。因为传过来的参数a和b的值分别为5和3,所以结果是8。
下面一行代码:return(r);
结束函数addition,并把控制权交还给调用它的函数(main),从调用addition的地方开始继续向下执行。另外,return在调用的时候后面跟着变量r(return(r);),它当时的值为8,这个值被称为函数的返回值。
函数返回的数值就是函数的计算结果,因此,z将存储函数addition(5,3)返回的数值,即8。用另一种方式解释,你也可以想象成调用函数(addition(5,3))被替换成了它的返回值(8)。
接下来main中的下一行代码是:cout<<\"Theresultis\"<你必须考虑到变量的范围只是在定义该变量的函数或指令块内有效,而不能在它的函数或指令块之外使用。例如,在上面的例子里就不可能在main中直接使用变量a,b或r,因为它们是函数addition的本地变量(localvariable)。在函数addition中也不可能直接使用变量z,因为它是main的本地变量。52/178
因此,本地变量(localvariables)的范围是局限于声明它的嵌套范围之内的。尽管如此,你还可以定义全局变量(globalvariables),它们可以在代码的任何位置被访问,不管在函数以内还是以外。要定义全局变量,你必须在所有函数或代码块之外定义它们,也就是说,直接在程序体中声明它们。
这里是另一个关于函数的例子://functionexample#includeintsubtraction(inta,intb){intr;r=a-b;return(r);}intmain(){
intx=5,y=3,z;z=subtraction(7,2);
cout<<\"Thefirstresultis\"<cout<<\"Thesecondresultis\"<cout<<\"Thefourthresultis\"<}Thefirstresultis5Thesecondresultis5Thethirdresultis2Thefourthresultis6在这个例子中,我们定义了函数subtraction。这个函数的功能是计算传入的两个参数的差值并将结果返回。
53/178
在main函数中,函数subtraction被调用了多次。我们用了几种不同的调用方法,因此你可以看到在不同的情况下函数如何被调用。
为了更好的理解这些例子,你需要考虑到被调用的函数其实完全可以由它所返回的值来代替。例如在上面例子中第一种情况下(这种调用你应该已经知道了,因为我们在前面的例子中已经用过这种形式的调用):
z=subtraction(7,2);
cout<<\"Thefirstresultis\"<如果我们把函数调用用它的结果(也就是5)替换,我们将得到:z=5;cout<<\"Thefirstresultis\"<cout<<\"Thesecondresultis\"<与前面的调用有同样的结果,但在这里我们把对函数subtraction的调用直接用作cout的参数。这可以简单想象成我们写的是:cout<<\"Thesecondresultis\"<<5;因为5是subtraction(7,2)的结果。
在cout<<\"Thethirdresultis\"<第四种调用也是一样的。只要知道除了z=4+subtraction(x,y);我们也可以写成:z=subtraction(x,y)+4;它们的结果是完全一样的。注意在整个表达式的结尾写上分号semicolonsign(;)。它并不需要总是跟在函数调用的后面,因为你可以有一次把它们想象成函数被它的结果所替代:
z=4+2;z=2+4;
/178
没有返回值类型的函数,使用void.如果你记得函数声明的格式:
typename(argument1,argument2...)statement
就会知道函数声明必须以一个数据类型(type)开头,它是函数由return语句所返回数据类型。但是如果我们并不打算返回任何数据那该怎么办呢?
假设我们要写一个函数,它的功能是打印在屏幕上打印一些信息。我们不需要它返回任何值,而且我们也不需要它接受任何参数。C语言为这些情况设计了void类型。让我们看一下下面的例子:
//void函数示例#includeusingnamespacestd;voidprintmessage(){
cout<<\"I'mafunction!\";}
intmain(){
printmessage();return0;}I'mafunction!
void还可以被用在函数参数位置,表示我们明确希望这个函数在被调用时不需要任何参数。例如上面的函数printmessage也可以写为以下形式:
voidprintmessage(void){
cout<<\"I'mafunction!\";}
55/178
虽然在C++中void可以被省略,我们还是建议写出void,以便明确指出函数不需要参数。
你必须时刻知道的是调用一个函数时要写出它的名字并把参数写在后面的括号内。但如果函数不需要参数,后面的括号并不能省略。因此调用函数printmessage的格式是
printmessage();
函数名称后面的括号就明确表示了它是一个函数调用,而不是一个变量名称或其它什么语句。以下调用函数的方式就不对:
printmessage;
2.3函数II(FunctionsII)
参数按数值传递和按地址传递(Argumentspassedbyvalueandbyreference)到目前为止,我们看到的所有函数中,传递到函数中的参数全部是按数值传递的(byvalue)。也就是说,当我们调用一个带有参数的函数时,我们传递到函数中的是变量的数值而不是变量本身。例如,假设我们用下面的代码调用我们的第一个函数addition:
intx=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并没有把它们自己传递给函数,而只是传递了他们的数值。
但在某些情况下你可能需要在一个函数内控制一个函数以外的变量。要实现这种操作,我们必须使用按地址传递的参数(argumentspassedbyreference),就象下面例子中的函数duplicate:
//passingparametersbyreference#includevoidduplicate(int&a,int&b,int&c)
56/178
{a*=2;b*=2;c*=2;}
intmain(){
intx=1,y=3,z=7;duplicate(x,y,z);
cout<<\"x=\"<}x=2,y=6,z=14第一个应该注意的事项是在函数duplicate的声明(declaration)中,每一个变量的类型后面跟了一个地址符ampersandsign(&),它的作用是指明变量是按地址传递的(byreference),而不是像通常一样按数值传递的(byvalue)。
当按地址传递(passbyreference)一个变量的时候,我们是在传递这个变量本身,我们在函数中对变量所做的任何修改将会影响到函数外面被传递的变量。
用另一种方式来说,我们已经把变量a,b,c和调用函数时使用的参数(x,y和z)联系起来了,因此如果我们在函数内对a进行操作,函数外面的x值也会改变。同样,任何对b的改变也会影响y,对c的改变也会影响z>。
这就是为什么上面的程序中,主程序main中的三个变量x,y和z在调用函数duplicate后打印结果显示他们的值增加了一倍。
如果在声明下面的函数:
voidduplicate(int&a,int&b,int&c)时,我们是按这样声明的:voidduplicate(inta,intb,intc)
57/178
也就是不写地址符ampersand(&),我们也就没有将参数的地址传递给函数,而是传递了它们的值,因此,屏幕上显示的输出结果x,y,z的值将不会改变,仍是1,3,7。
这种用地址符ampersand(&)来声明按地址\"byreference\"传递参数的方式只是在C++中适用。在C语言中,我们必须用指针(pointers)来做相同的操作。
按地址传递(Passingbyreference)是一个使函数返回多个值的有效方法。例如,下面是一个函数,它可以返回第一个输入参数的前一个和后一个数值。
//morethanonereturningvalue#includevoidprevnext(intx,int&prev,int&next){
prev=x-1;next=x+1;}
intmain(){
intx=100,y,z;prevnext(x,y,z);
cout<<\"Previous=\"<}Previous=99,Next=101参数的默认值(Defaultvaluesinarguments)
当声明一个函数的时候我们可以给每一个参数指定一个默认值。如果当函数被调用时没有给出该参数的值,那么这个默认值将被使用。指定参数默认值只需要在函数声明时把一个数值赋给参数。如果函数被调用时没有数值传递给该参数,那么默认值将被使用。但如果有指定的数值传递给参数,那么默认值将被指定的数值取代。例如:
//defaultvaluesinfunctions#include58/178
intdivide(inta,intb=2){intr;r=a/b;return(r);}
intmain(){cout<我们可以看到在程序中有两次调用函数divide。第一次调用:divide(12)只有一个参数被指明,但函数divide允许有两个参数。因此函数divide假设第二个参数的值为2,因为我们已经定义了它为该参数缺省的默认值(注意函数声明中的intb=2)。因此这次函数调用的结果是6(12/2)。
在第二次调用中:divide(20,4)
这里有两个参数,所以默认值(intb=2)被传入的参数值4所取代,使得最后结果为5(20/4).
函数重载(Overloadedfunctions)
两个不同的函数可以用同样的名字,只要它们的参量(arguments)的原型(prototype)不同,也就是说你可以把同一个名字给多个函数,如果它们用不同数量的参数,或不同类型的参数。例如:
//overloadedfunction#include59/178
intdivide(inta,intb){return(a/b);}
floatdivide(floata,floatb){return(a/b);}
intmain(){intx=5,y=2;floatn=5.0,m=2.0;cout<在这个例子里,我们用同一个名字定义了两个不同函数,当它们其中一个接受两个整型(int)参数,另一个则接受两个浮点型(float)参数。编译器(compiler)通过检查传入的参数的类型来确定是哪一个函数被调用。如果调用传入的是两个整数参数,那么是原型定义中有两个整型(int)参量的函数被调用,如果传入的是两个浮点数,那么是原型定义中有两个浮点型(float)参量的函数被调用。为了简单起见,这里我们用的两个函数的代码相同,但这并不是必须的。你可以让两个函数用同一个名字同时完成完全不同的操作。
Inline函数(inlinefunctions)
inline指令可以被放在函数声明之前,要求该函数必须在被调用的地方以代码形式被编译。这相当于一个宏定义(macro)。它的好处只对短小的函数有效,这种情况下因
60/178
为避免了调用函数的一些常规操作的时间(overhead),如参数堆栈操作的时间,所以编译结果的运行代码会更快一些。
它的声明形式是:
inlinetypename(arguments...){instructions...}
它的调用和其他的函数调用一样。调用函数的时候并不需要写关键字inline,只有在函数声明前需要写。
递归(Recursivity)
递归(recursivity)指函数将被自己调用的特点。它对排序(sorting)和阶乘(factorial)运算很有用。例如要获得一个数字n的阶乘,它的数学公式是:
n!=n*(n-1)*(n-2)*(n-3)...*1更具体一些,5!(factorialof5)是:5!=5*4*3*2*1=120
而用一个递归函数来实现这个运算将如以下代码://factorialcalculator#includelongfactorial(longa){
if(a>1)return(a*factorial(a-1));elsereturn(1);}
intmain(){longl;
cout<<\"Typeanumber:\";cin>>l;
cout<<\"!\"<61/178}Typeanumber:9!9=362880
注意我们在函数factorial中是怎样调用它自己的,但只是在参数值大于1的时候才做调用,因为否则函数会进入死循环(aninfiniterecursiveloop),当参数到达0的时候,函数不继续用负数乘下去(最终可能导致运行时的堆栈溢出错误(stackoverflowerror)。
这个函数有一定的局限性,为简单起见,函数设计中使用的数据类型为长整型(long)。在实际的标准系统中,长整型long无法存储12!以上的阶乘值。
函数的声明(Declaringfunctions)
到目前为止,我们定义的所有函数都是在它们第一次被调用(通常是在main中)之前,而把main函数放在最后。如果重复以上几个例子,但把main函数放在其它被它调用的函数之前,你就会遇到编译错误。原因是在调用一个函数之前,函数必须已经被定义了,就像我们前面例子中所做的。
但实际上还有一种方法来避免在main或其它函数之前写出所有被他们调用的函数的代码,那就是在使用前先声明函数的原型定义。声明函数就是对函数在的完整定义之前做一个短小重要的声明,以便让编译器知道函数的参数和返回值类型。
它的形式是:
typename(argument_type1,argument_type2,...);它与一个函数的头定义(headerdefinition)一样,除了:
?它不包括函数的内容,也就是它不包括函数后面花括号{}内的所有语句。?它以一个分号semicolonsign(;)结束。
?在参数列举中只需要写出各个参数的数据类型就够了,至于每个参数的名字可以写,也可以不写,但是我们建议写上。
例如:
//声明函数原型#includevoidodd(inta);
62/178
voideven(inta);
intmain(){inti;do{
cout<<\"Typeanumber:(0toexit)\";cin>>i;odd(i);}while(i!=0);return0;}
voidodd(inta){
if((a%2)!=0)cout<<\"Numberisodd.\\n\";elseeven(a);}
voideven(inta){
if((a%2)==0)cout<<\"Numberiseven.\\n\";elseodd(a);
}Typeanumber(0toexit):9Numberisodd.
Typeanumber(0toexit):6Numberiseven.
Typeanumber(0toexit):1030Numberiseven.
Typeanumber(0toexit):0Numberiseven.
63/178
这个例子的确不是很有效率,我相信现在你已经可以只用一半行数的代码来完成同样的功能。但这个例子显示了函数原型(prototypingfunctions)是怎样工作的。并且在这个具体的例子中,两个函数中至少有一个是必须定义原型的。
这里我们首先看到的是函数odd和even的原型:voidodd(inta);voideven(inta);
这样使得这两个函数可以在它们被完整定义之前就被使用,例如在main中被调用,这样main就可以被放在逻辑上更合理的位置:即程序代码的开头部分。
尽管如此,这个程序需要至少一个函数原型定义的特殊原因是因为在odd函数里需要调用even函数,而在even函数里也同样需要调用odd函数。如果两个函数任何一个都没被提前定义原型的话,就会出现编译错误,因为或者odd在even函数中是不可见的(因为它还没有被定义),或者even函数在odd函数中是不可见的。
很多程序员建议给所有的函数定义原型。这也是我的建议,特别是在有很多函数或函数很长的情况下。把所有函数的原型定义放在一个地方,可以使我们在决定怎样调用这些函数的时候轻松一些,同时也有助于生成头文件。
第三章高级数据类型(AdvancedData)1.数组Arrays2.字符序列
CharactersSequences3.指针Pointers4.动态内存分配Dynamicmemory5.数据结构DataStructures6.自定义数据类型
/178
Userdefineddatatypes3.1数组(Arrays)
数组(Arrays)是在内存中连续存储的一组同种数据类型的元素(变量),每一数组有一个唯一名称,通过在名称后面加索引(index)的方式可以引用它的每一个元素。
也就是说,例如我们有5个整型数值需要存储,但我们不需要定义5个不同的变量名称,而是用一个数组(array)来存储这5个不同的数值。注意数组中的元素必须是同一数据类型的,在这个例子中为整型(int)。
例如一个存储5个整数叫做billy的数组可以用下图来表示:
这里每一个空白框代表数组的一个元素,在这个例子中为一个整数值。白框上面的数字0到4代表元素的索引(index)。注意无论数组的长度如何,它的第一个元素的索引总是从0开始的。
同其它的变量一样,数组必须先被声明然后才能被使用。一种典型的数组声明显示如下:
typename[elements];
这里type是可以使任何一种有效的对象数据类型(objecttype),如int,float...等,name是一个有效地变量标识(identifier),而由中括号[]引起来的elements域指明数组的大小,即可以存储多少个元素。
因此,要定义上面图中显示的billy数组,用一下语句就可以了:intbilly[5];
备注:在定义一个数组的时候,中括号[]中的elements域必须是一个常量数值,因为数组是内存中一块有固定大小的静态空间,编译器必须在编译所有相关指令之前先能够确定要给该数组分配多少内存空间。
初始化数组(Initializingarrays)
当声明一个本地范围内(在一个函数内)的数组时,除非我们特别指定,否则数组将不会被初始化,因此它的内容在我们将数值存储进去之前是不定的。
如果我们声明一个全局数组(在所有函数之外),则它的内容将被初始化为所有元素均为0。因此,如果全局范围内我们声明:
65/178
intbilly[5];
那么billy中的每一个元素将会被初始化为0:
另外,我们还可以在声明一个变量的同时把初始值付给数组中的每一个元素,这个赋值用花括号{}来完成。例如:
intbilly[5]={16,2,77,40,12071};这个声明将生成如下数组:
花括号中我们要初始化的元素数值个数必须和数组声明时方括号[]中指定的数组长度相符。例如,在上面例子中数组billy声明中的长度为5,因此在后面花括号中的初始值也有5个,每个元素一个数值。
因为这是一种信息的重复,因此C++允许在这种情况下数组[]中为空白,而数组的长度将有后面花括号{}中数值的个数来决定,如下例所示。
intbilly[]={16,2,77,40,12071};
存取数组中数值(AccesstothevaluesofanArray)
在程序中我们可以读取和修改数组任一元素的数值,就像操作其他普通变量一样。格式如下:
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]。同样的原因,最后一个元素是
66/178
billy[4]。如果我们写billy[5],那么是在使用billy的第6个元素,因此会超出数组的长度。
在C++中对数组使用超出范围的index是合法的,这就会产生问题,因为它不会产生编译错误而不易被察觉,但是在运行时会产生意想不到的结果,甚至导致严重运行错误。超出范围的index之所以合法的原因我们在后面学习指针(pointer)的时候会了解。
学到这里,我们必须能够清楚的了解方括号[]在对数组操作中的两种不同用法。它们完成两种任务:一种是在声明数组的时候定义数组的长度;另一种是在引用具体的数组元素的时候指明一个索引号(index)。我们要注意不要把这两种用法混淆。
intbilly[5];//声明新数组(以数据类型名称开头)billy[2]=75;//存储数组的一个元素其它合法的数组操作:
billy[0]=a;//a为一个整型变量billy[a]=75;b=billy[a+2];
billy[billy[a]]=billy[2]+5;
//arraysexample#includeintbilly[]={16,2,77,40,12071};intn,result=0;
intmain(){
for(n=0;n<5;n++){result+=billy[n];}
cout<67/178return0;}12206
数组(MultidimensionalArrays)
数组(MultidimensionalArrays)可以被描述为数组的数组。例如,一个2维数组(bidimensionalarray)可以被想象成一个有同一数据类型的2维表格。
jimmy显示了一个整型(int)的3x5二维数组,声明这一数组的的方式是:intjimmy[3][5];
而引用这一数组中第2列第4排元素的表达式为:jimmy[1][3]
(记住数组的索引总是从0开始)。
数组(Multidimensionalarrays)并不局限于2维。如果需要,它可以有任意,虽然需要3维以上的时候并不多。但是考虑一下一个有很的数组所需要的内存空间,例如:
charcentury[100][365][24][60][60];
给一个世纪中的每一秒赋一个字符(char),那么就是多于30亿的字符!如果我们定义这样一个数组,需要消耗3000M的内存。
数组只是一个抽象的概念,因为我们只需要把各个索引的乘积放入一个简单的数组中就可以获得同样的结果。例如:
intjimmy[3][5];效果上等价于intjimmy[15];(3*5=15)
唯一的区别是编译器帮我们记住每一个想象中的维度的深度。下面的例子中我们就可以看到,两段代码一个使用2维数组,另一个使用简单数组,都获得同样的结果,即都在内存中开辟了一块叫做jimmy的空间,这个空间有15个连续地址位置,程序结束后都在相同的位置上存储了相同的数值,如后面图中所示:
//multidimensionalarray#include#defineWIDTH568/178
#defineHEIGHT3
intjimmy[HEIGHT][WIDTH];intn,m;
intmain(){
for(n=0;nreturn0;}//pseudo-multidimensionalarray#include#defineWIDTH5#defineHEIGHT3intjimmy[HEIGHT*WIDTH];intn,m;
intmain(){
for(n=0;njimmy[n*WIDTH+m]=(n+1)*(m+1);}return0;}上面两段代码并不向屏幕输出,但都向内存中的叫做jimmy的内存块存入如下数值:
69/178
我们用了宏定义常量(#define)来简化未来可能出现的程序修改,例如,如果我们决定将数组的纵向由3扩大到4,只需要将代码行:
#defineHEIGHT3修改为:#defineHEIGHT4
而不需要对程序的其他部分作任何修改。
数组参数(Arraysasparameters)
有时候我们需要将数组作为参数传给函数。在C++中将一整块内存中的数值作为参数完整的传递给一个函数是不可能的,即使是一个规整的数组也不可能,但是允许传递它的地址。它们的实际作用是一样的,但传递地址更快速有效。
要定义数组为参数,我们只需要在声明函数的时候指明参数数组的基本数据类型,一个标识后面再跟一对空括号[]就可以了。例如以下的函数:
voidprocedure(intarg[])
接受一个叫做arg的整型数组为参数。为了给这个函数传递一个按如下定义的数组:
intmyarray[40];其调用方式可写为:procedure(myarray);下面我们来看一个完整的例子://arraysasparameters#includevoidprintarray(intarg[],intlength){for(intn=0;n70/178cout<<\"\\n\";}
intmain(){
intfirstarray[]={5,10,15};intsecondarray[]={2,4,6,8,10};printarray(firstarray,3);printarray(secondarray,5);return0;}51015246810
可以看到,函数的第一个参数(intarg[])接受任何整型数组为参数,不管其长度如何。因此,我们用了第2个参数来告知函数我们传给它的第一个参数数组的长度。这样函数中打印数组内容的for循环才能知道需要检查的数组范围。
在函数的声明中也包含数组参数。定义一个3维数组(tridimensionalarray)的形式是:
base_type[][depth][depth]
例如,一个函数包含数组参数的函数可以定义为:voidprocedure(intmyarray[][3][4])
注意第一对括号[]中为空,而后面两对不为空。这是必须的,因为编译器必须能够在函数中确定每一个增加的维度的深度。
数组作为函数的参数,不管是数组还是简单数组,都是初级程序员容易出错的地方。建议阅读章节3.3,指针(Pointers),以便更好的理解数组(arrays)是如何操作的。
3.2字符序列(CharacterSequences)
前面基础知识部分讲C++变量类型的时候,我们已经提到过C++的标准函数库提供了一个string类来支持对字符串的操作。然而,字符串实际就是一串连续的字符序列,所以我们也可以用简单的字符数组来表示它。
71/178
例如,下面这个数组:charjenny[20];
是一个可以存储最多20个字符类型数据的数组。你可以把它想象成:
理论上这数组可以存储长度为20的字符序列,但是它也可以存储比这短的字符序列,而且实际中常常如此。例如,jenny在程序的某一点可以只存储字符串\"Hello\"或者\"Merrychristmas\"。因此,既然字符数组经常被用于存储短于其总长的字符串,就形成了一种习惯在字符串的有效内容的结尾处加一个空字符(nullcharacter)来表示字符结束,它的常量表示可写为0或'\\0'。
我们可以用下图表示jenny(一个长度为20的字符数组)存储字符串\"Hello\"和\"MerryChristmas\":
注意在有效内容结尾是如何用空字符nullcharacter('\\0')来表示字符串结束的。后面灰色的空格表示不确定数值。
初始化以空字符结束的字符序列(Initializationofnull-terminatedcharactersequences)
因为字符数组其实就是普通数组,它与数组遵守同样的规则。例如,如果我们想将数组初始化为指定数值,我们可以像初始化其它数组一样用:
charmystring[]={'H','e','l','l','o','\\0'};
在这里我们定义了一个有6个元素的字符数组,并将它初始化为字符串Hello加一个空字符(nullcharacter'\\0')。
除此之外,字符串还有另一个方法来进行初始化:用字符串常量。
在前几章的例子中,字符串常量已经出现过多次,它们是由双引号引起来的一组字符来表示的,例如:
\"theresultis:\"
是一个字符串常量,我们在前面的例子中已经使用过。
与表示单个字符常量的单引号(')不同,双引号(\")是表示一串连续字符的常量。由双引号引起来的字符串末尾总是会被自动加上一个空字符('\\0')。
72/178
因此,我们可以用下面两种方法的任何一种来初始化字符串mystring:charmystring[]={'H','e','l','l','o','\\0'};charmystring[]=\"Hello\";
在两种情况下字符串或数组mystring都被定义为6个字符长(元素类型为字符char):组成Hello的5个字符加上最后的空字符('\\0')。在第二种用双引号的情况下,空字符('\\0')是被自动加上的。
注意:同时给数组赋多个值只有在数组初始化时,也就是在声明数组时,才是合法的。象下面代码现实的表达式都是错误的:
mystring=\"Hello\";mystring[]=\"Hello\";
mystring={'H','e','l','l','o','\\0'};
因此记住:我们只有在数组初始化时才能够同时赋多个值给它。其原因在学习了指针(pointer)之后会比较容易理解,因为那时你会看到一个数组其实只是一个指向被分配的内存块的常量指针(constantpointer),数组自己不能够被赋予任何数值,但我们可以给数组中的每一个元素赋值。
在数组初始化的时候是特殊情况,因为它不是一个赋值,虽然同样使用了等号(=)。不管怎样,牢记前面标下画线的规则。
给字符序列的赋值
因为赋值运算的lvalue只能是数组的一个元素,而不能使整个数组,所以,用以下方式将一个字符串赋给一个字符数组是合法的:
mystring[0]='H';mystring[1]='e';mystring[2]='l';mystring[3]='l';mystring[4]='o';mystring[5]='\\0';
但正如你可能想到的,这并不是一个实用的方法。通常给数组赋值,或更具体些,给字符序列赋值的方法是使用一些函数,例如strcpy。strcpy(stringcopy)在函数库cstring(string.h)中被定义,可以用以下方式被调用:
73/178
strcpy(string1,string2);
这个函数将string2中的内容拷贝给string1。string2可以是一个数组,一个指针,或一个字符串常量constantstring。因此用下面的代码可以将字符串常量\"Hello\"赋给mystring:
strcpy(mystring,\"Hello\");例如:
//settingvaluetostring#include#includeintmain(){charszMyName[20];
strcpy(szMyName,\"J.Soulie\");cout<注意:我们需要包括头文件才能够使用函数strcpy。虽然我们通常可以写一个像下面setstring一样的简单程序来完成与cstring中strcpy同样的操作:
//settingvaluetostring#includevoidsetstring(charszOut[],charszIn[]){intn=0;do{
szOut[n]=szIn[n];}while(szIn[n++]!='\\0');}
74/178
intmain(){charszMyName[20];
setstring(szMyName,\"J.Soulie\");cout<另一个给数组赋值的常用方法是直接使用输入流(cin)。在这种情况下,字符序列的值是在程序运行时由用户输入的。当cin被用来输入字符序列值时,它通常与函数getline一起使用,方法如下:cin.getline(charbuffer[],intlength,chardelimiter='\\n');
这里buffer是用来存储输入的地址(例如一个数组名),length是一个缓存buffer的最大容量,而delimiter是用来判断用户输入结束的字符,它的默认值(如果我们不写这个参数时)是换行符newlinecharacter('\\n')。
下面的例子重复输出用户在键盘上的任何输入。这个例子简单的显示了如何使用cin.getline来输入字符串:
//cinwithstrings#includeintmain(){charmybuffer[100];
cout<<\"What'syourname?\";cin.getline(mybuffer,100);
cout<<\"Hello\"<cout<<\"Ilike\"<}What'syourname?JuanHelloJuan.75/178
Whichisyourfavouriteteam?InterMilanIlikeInterMilantoo.
注意上面例子中两次调用cin.getline时我们都使用了同一个字符串标识(mybuffer)。程序在第二次调用时将新输入的内容直接覆盖到第一次输入到buffer中的内容。
你可能还记得,在以前与控制台(console)交互的程序中,我们使用extractionoperator(>>)来直接从标准输入设备接收数据。这个方法也同样可以被用来输入字符串,例如,在上面的例子中我们也可以用以下代码来读取用户输入:
cin>>mybuffer;
这种方法也可以工作,但它有以下局限性是cin.getline所没有的:
?它只能接收单独的词(而不能是完整的句子),因为这种方法以任何空白符为分隔符,包括空格spaces,跳跃符tabulators,换行符newlines和回车符arriagereturns。
?它不能给buffer指定容量,这使得程序不稳定,如果用户输入超出数组长度,输入信息会被丢失。
因此,建议在需要用cin来输入字符串时,使用cin.getline来代替cin>>。
字符串和其它数据类型的转换(Convertingstringstoothertypes)
鉴于字符串可能包含其他数据类型的内容,例如数字,将字符串内容转换成数字型变量的功能会有用处。例如一个字符串的内容可能是\"1977\",但这一个5个字符组成序列,并不容易转换为一个单独的整数。因此,函数库cstdlib(stdlib.h)提供了3个有用的函数:
?atoi:将字符串string转换为整型int?atol:将字符串string转换为长整型long?atof:将字符串string转换为浮点型float
所有这些函数接受一个参数,返回一个指定类型的数据(int,long或float)。这三个函数与cin.getline一起使用来获得用户输入的数值,比传统的cin>>方法更可靠:
//cinandato*functions#include76/178
#includeintmain(){charmybuffer[100];floatprice;intquantity;
cout<<\"Enterprice:\";cin.getline(mybuffer,100);price=atof(mybuffer);cout<<\"Enterquantity:\";cin.getline(mybuffer,100);quantity=atoi(mybuffer);
cout<<\"Totalprice:\"<}Enterprice:2.75Enterquantity:21Totalprice:57.75字符串操作函数(Functionstomanipulatestrings)
函数库cstring(string.h)定义了许多可以像C语言类似的处理字符串的函数(如前面已经解释过的函数strcpy)。这里再简单列举一些最常用的:
?strcat:char*strcat(char*dest,constchar*src);//将字符串src附加到字符串dest的末尾,返回dest。
?strcmp:intstrcmp(constchar*string1,constchar*string2);//比较两个字符串string1和string2。如果两个字符串相等,返回0。
?strcpy:char*strcpy(char*dest,constchar*src);//将字符串src的内容拷贝给dest,返回dest。
?strlen:size_tstrlen(constchar*string);//返回字符串的长度。注意:char*与char[]相同。
77/178
关于这个函数库的更多信息,参阅C++Reference。
3.3指针(Pointers)
我们已经明白变量其实是可以由标识来存取的内存单元。但这些变量实际上是存储在内存中具体的位置上的。对我们的程序来说,计算机内存只是一串连续的单字节单元(1bytecell),即最小数据单位,每一个单元有一个唯一地址。
计算机内存就好像城市中的街道。在一条街上,所有的房子被顺序编号,每所房子有唯一编号。因此如果我们说芝麻街27号,我们很容易找到它,因为只有一所房子会是这个编号,而且我们知道它会在26号和28号之间。
同房屋按街道地址编号一样,操作系统(operatingsystem)也按照唯一顺序编号来组织内存。因此,当我们说内存中的位置1776,我们知道内存中只有一个位置是这个地址,而且它在地址1775和1777之间。
地址操作符/去引操作符Address/dereferenceoperator(&)
当我们声明一个变量的同时,它必须被存储到内存中一个具体的单元中。通常我们并不会指定变量被存储到哪个具体的单元中—幸亏这通常是由编译器和操作系统自动完成的,但一旦操作系统指定了一个地址,有些时候我们可能会想知道变量被存储在哪里了。
这可以通过在变量标识前面加与符号ampersandsign(&)来实现,它表示\"...的地址\"(\"addressof\"),因此称为地址操作符(adressoperator),又称去引操作符(dereferenceoperator)。例如:
ted=&andy;
将变量andy的地址赋给变量ted,因为当在变量名称andy前面加ampersand(&)符号,我们指的将不再是该变量的内容,而是它在内存中的地址。
假设andy被放在了内存中地址1776的单元中,然后我们有下列代码:andy=25;fred=andy;ted=&andy;
其结果显示在下面的图片中:
78/178
我们将变量andy的值赋给变量fred,这与以前我们看到很多例子都相同,但对于ted,我们把操作系统存储andy的内存地址赋给它,我们想像该地址为1776(它可以是任何地址,这里只是一个假设的地址),原因是当给ted赋值的时候,我们在andy前面加了ampersand(&)符号。
存储其它变量地址的变量(如上面例子中的ted),我们称之为指针(pointer)。在C++中,指针pointers有其特定的优点,因此经常被使用。在后面我们将会看到这种变量如何被声明。
引用操作符Referenceoperator(*)
使用指针的时候,我们可以通过在指针标识的前面加星号asterisk(*)来存储该指针指向的变量所存储的数值,它可以被翻译为“所指向的数值”(\"valuepointedby\")。因此,仍用前面例子中的数值,如果我们写:beth=*ted;(我们可以读作:\"beth等与ted所指向的数值\")beth将会获得数值25,因为ted是1776,而1776所指向的数值为25。
你必须清楚的区分ted存储的是1776,但*ted(前面加asterisk*)指的是地址1776中存储的数值,即25。注意加或不加星号*的不同(下面代码中注释显示了如何读这两个不同的表达式):
beth=ted;//beth等于ted(1776)
beth=*ted;//beth等于ted所指向的数值(25)
地址或反引用操作符Operatorofaddressordereference(&)
它被用作一个变量前缀,可以被翻译为“…的地址”(\"addressof\"),因此:&variable1可以被读作variable1的地址(\"addressofvariable1\")。
引用操作符Operatorofreference(*)
它表示要取的是表达式所表示的地址指向的内容。它可以被翻译为“…指向的数值”(\"valuepointedby\")。
*mypointer可以被读作\"mypointer指向的数值\"。
79/178
继续使用上面开始的例子,看下面的代码:andy=25;ted=&andy;
现在你应该可以清楚的看到以下等式全部成立:andy==25&andy==1776ted==1776*ted==25
第一个表达式很容易理解,因为我们有赋值语句andy=25;。第二个表达式使用了地址(或反引用)操作符(&)来返回变量andy的地址,即1776。第三个表达式很明显成立,因为第二个表达式为真,而我们给ted赋值的语句为ted=&andy;。第四个表达式使用了引用操作符(*),相当于ted指向的地址中存储的数值,即25。
由此你也可以推断出,只要ted所指向的地址中存储的数值不变,以下表达式也为真:
*ted==andy
声明指针型变量Declaringvariablesoftypepointer
由于指针可以直接引用它所指向的数值,因此有必要在声明指针的时候指明它所指向的数据类型。指向一个整型int或浮点型float数据的指针与指向一个字符型char数据的指针并不相同。
因此,声明指针的格式如下:type*pointer_name;
这里,type是指针所指向的数据的类型,而不是指针自己的类型。例如:int*number;char*character;float*greatnumber;
它们是3个指针的声明,每一个指针指向一种不同数据类型。这三个指针本身其实在内存中占用同样大小的内存空间(指针的大小取决于不同的操作系统),但它们所指向的数据是不同的类型,并占用不同大小的内存空间,一个是整型int,一个是字符型char,还有一个是浮点型float。
80/178
需要强调的一点是,在指针声明时的星号asterisk(*)仅表示这里声明的是一个指针,不要把它和前面我们用过的引用操作符混淆,虽然那也是写成一个星号(*)。它们只是用同一符号表示的两个不同任务。
//myfirstpointer#includeintmain(){
intvalue1=5,value2=15;int*mypointer;mypointer=&value1;*mypointer=10;mypointer=&value2;*mypointer=20;
cout<<\"value1==\"<}value1==10/value2==20注意变量value1和value2是怎样间接的被改变数值的。首先我们使用ampersandsign(&)将value1的地址赋给mypointer。然后我们将10赋给mypointer所指向的数值,它其实指向value1的地址,因此,我们间接的修改了value1的数值。
为了让你了解在同一个程序中一个指针可以被用作不同的数值,我们在这个程序中用value2和同一个指针重复了上面的过程。
下面是一个更复杂一些的例子://morepointers#includeintmain(){
intvalue1=5,value2=15;int*p1,*p2;
p1=&value1;//p1=addressofvalue1
81/178
p2=&value2;//p2=addressofvalue2*p1=10;//valuepointedbyp1=10
*p2=*p1;//valuepointedbyp2=valuepointedbyp1p1=p2;//p1=p2(valueofpointercopied)*p1=20;//valuepointedbyp1=20
cout<<\"value1==\"<}value1==10/value2==20上面每一行都有注释说明代码的意思:ampersand(&)为\"addressof\",asterisk(*)为\"valuepointedby\"。注意有些包含p1和p2的表达式不带星号。加不加星号的含义十分不同:星号(*)后面跟指针名称表示指针所指向的地方,而指针名称不加星号(*)表示指针本身的数值,即它所指向的地方的地址。
另一个需要注意的地方是这一行:int*p1,*p2;
声明了上例用到的两个指针,每个带一个星号(*),因为是这一行定义的所有指针都是整型int(而不是int*)。原因是引用操作符(*)的优先级顺序与类型声明的相同,因此,由于它们都是向右结合的操作,星号被优先计算。我们在section1.3:Operators中已经讨论过这些。注意在声明每一个指针的时候前面加上星号asterisk(*)。
指针和数组Pointersandarrays
数组的概念与指针的概念联系非常解密。其实数组的标识相当于它的第一个元素的地址,就像一个指针相当于它所指向的第一个元素的地址,因此其实它们是同一个东西。例如,假设我们有以下声明:
intnumbers[20];int*p;
下面的赋值为合法的:p=numbers;
这里指针p和numbers是等价的,它们有相同的属性,唯一的不同是我们可以给指针p赋其它的数值,而numbers总是指向被定义的20个整数组中的第一个。所以,
82/178
p只是一个普通的指针变量,而与之不同,numbers是一个指针常量(constantpointer),数组名的确是一个指针常量。因此虽然前面的赋值表达式是合法的,但下面的不是:
numbers=p;
因为numbers是一个数组(指针常量),常量标识不可以被赋其它数值。由于变量的特性,以下例子中所有包含指针的表达式都是合法的://morepointers#includeintmain(){intnumbers[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(intn=0;n<5;n++)cout<}10,20,30,40,50,在数组一章中我们使用了括号[]来指明我们要引用的数组元素的索引(index)。中括号[]也叫位移(offset)操作符,它相当于在指针中的地址上加上括号中的数字。例如,下面两个表达式互相等价:
83/178
a[5]=0;//a[offsetof5]=0*(a+5)=0;//pointedby(a+5)=0
不管a是一个指针还是一个数组名,这两个表达式都是合法的。
指针初始化Pointerinitialization
当声明一个指针的时候我们可能需要同时指定它们指向哪个变量,intnumber;
int*tommy=&number;这相当于:intnumber;int*tommy;tommy=&number;
当给一个指针赋值的时候,我们总是赋给它一个地址值,而不是它所指向数据的值。你必须考虑到在声明一个指针的时候,星号(*)只是用来指明它是指针,而从不表示引用操作符referenceoperator(*)。记住,它们是两种不同操作,虽然它们写成同样的符号。因此,我们要注意不要将以上的代码与下面的代码混淆:
intnumber;int*tommy;*tommy=&number;
这些代码也没有什么实际意义。
在定义数组指针的时候,编译器允许我们在声明变量指针的同时对数组进行初始化,初始化的内容需要是常量,例如:
char*terry=\"hello\";
在这个例子中,内存中预留了存储\"hello\"的空间,并且terry被赋予了向这个内存块的第一个字符(对应’h’)的指针。假设\"hello\"存储在地址1702,下图显示了上面的定义在内存中状态:
这里需要强调,terry存储的是数值1702,而不是'h'或\"hello\",虽然1702指向这些字符。
84/178
指针terry指向一个字符串,可以被当作数组一样使用(数组只是一个常量指针)。例如,如果我们的心情变了,而想把terry指向的内容中的字符'o'变为符号'!',我们可以用以下两种方式的任何一种来实现:
terry[4]='!';*(terry+4)='!';
记住写terry[4]与*(terry+4)是一样的,虽然第一种表达方式更常用一些。以上两个表达式都会实现以下改变:
指针的数算Arithmeticofpointers
对指针进行数算与其他整型数据类型进行数算稍有不同。首先,对指针只有加法和减法运算,其它运算在指针世界里没有意义。但是指针的加法和减法的具体运算根据它所指向的数据的类型的大小的不同而有所不同。
我们知道不同的数据类型在内存中占用的存储空间是不一样的。例如,对于整型数据,字符char占用1的字节(1byte),短整型short占用2个字节,长整型long占用4个字节。
假设我们有3个指针:char*mychar;short*myshort;long*mylong;
而且我们知道他们分别指向内存地址1000,2000和3000。因此如果我们有以下代码:mychar++;myshort++;mylong++;
就像你可能想到的,mychar的值将会变为1001。而myshort的值将会变为2002,mylong的值将会变为3004。原因是当我们给指针加1时,我们实际是让该指针指向下一个与它被定义的数据类型的相同的元素。因此,它所指向的数据类型的长度字节数将会被加到指针的数值上。以上过程可以由下图表示:
85/178
这一点对指针的加法和减法运算都适用。如果我们写以下代码,它们与上面例子的作用一抹一样:mychar=mychar+1;
myshort=myshort+1;mylong=mylong+1;
这里需要提醒你的是,递增(++)和递减(--)操作符比引用操作符referenceoperator(*)有更高的优先级,因此,以下的表达式有可能引起歧义:
*p++;*p++=*q++;
第一个表达式等同于*(p++),它所作的是增加p(它所指向的地址,而不是它存储的数值)。
在第二个表达式中,因为两个递增操作(++)都是在整个表达式被计算之后进行而不是在之前,所以*q的值首先被赋予*p,然后q和p都增加1。它相当于:
*p=*q;p++;q++;
像通常一样,我们建议使用括号()以避免意想不到的结果。
指针的指针Pointerstopointers
C++允许使用指向指针的指针。要做到这一点,我们只需要在每一层引用之前加星号(*)即可:
chara;char*b;char**c;a='z';b=&a;c=&b;
假设随机选择内存地址为7230,8092和10502,以上例子可以用下图表示:
86/178
(方框内为变量的内容;方框下面为内存地址)
这个例子中新的元素是变量c,关于它我们可以从3个方面来讨论,每一个方面对应了不同的数值:
c是一个(char**)类型的变量,它的值是8092*c是一个(char*)类型的变量,它的值是7230**c是一个(char)类型的变量,它的值是'z'
空指针voidpointers
指针void是一种特殊类型的指针。void指针可以指向任意类型的数据,可以是整数,浮点数甚至字符串。唯一个是被指向的数值不可以被直接引用(不可以对它们使用引用星号*),因为它的长度是不定的,因此,必须使用类型转换操作或赋值操作来把void指针指向一个具体的数据类型。
它的应用之一是被用来给函数传递通用参数://integerincreaser#includevoidincrease(void*data,inttype){switch(type){
casesizeof(char):(*((char*)data))++;break;casesizeof(short):(*((short*)data))++;break;casesizeof(long):(*((long*)data))++;break;}}
intmain(){chara=5;shortb=9;longc=12;
87/178
increase(&a,sizeof(a));increase(&b,sizeof(b));increase(&c,sizeof(c));
cout<<(int)a<<\\"<sizeof是C++的一个操作符,用来返回其参数的长度字节数常量。例如,sizeof(char)返回1,因为char类型是1字节长数据类型。
函数指针Pointerstofunctions
C++允许对指向函数的指针进行操作。它最大的作用是把一个函数作为参数传递给另外一个函数。声明一个函数指针像声明一个函数原型一样,除了函数的名字需要被括在括号内并在前面加星号asterisk(*)。例如:
//pointertofunctions#includeintaddition(inta,intb){return(a+b);}
intsubtraction(inta,intb){return(a-b);}
int(*minus)(int,int)=subtraction;
intoperation(intx,inty,int(*functocall)(int,int)){intg;
g=(*functocall)(x,y);return(g);
88/178
}
intmain(){intm,n;
m=operation(7,5,addition);n=operation(20,m,minus);cout<在这个例子里,minus是一个全局指针,指向一个有两个整型参数的函数,它被赋值指向函数subtraction,所有这些由一行代码实现:int(*minus)(int,int)=subtraction;
这里似乎解释的不太清楚,有问题问为什么(intint)只有类型,没有参数,就再多说两句。
这里int(*minus)(intint)实际是在定义一个指针变量,这个指针的名字叫做minus,这个指针的类型是指向一个函数,函数的类型是有两个整型参数并返回一个整型值。
整句话“int(*minus)(int,int)=subtraction;”是定义了这样一个指针并把函数subtraction的值赋给它,也就是说有了这个定义后minus就代表了函数subtraction。因此括号中的两个intint实际只是一种变量类型的声明,也就是说是一种形式参数而不是实际参数。
3.4动态内存分配(Dynamicmemory)
到目前为止,我们的程序中我们只用了声明变量、数组和其他对象(objects)所必需的内存空间,这些内存空间的大小都在程序执行之前就已经确定了。但如果我们需要内存大小为一个变量,其数值只有在程序运行时(runtime)才能确定,例如有些情况下我们需要根据用户输入来决定必需的内存空间,那么我们该怎么办呢?
答案是动态内存分配(dynamicmemory),为此C++集成了操作符new和delete。
/178
操作符new和delete是C++执行指令。本节后面将会介绍这些操作符在C中的等价命令。
操作符new和new[]
操作符new的存在是为了要求动态内存。new后面跟一个数据类型,并跟一对可选的方括号[]里面为要求的元素数。它返回一个指向内存块开始位置的指针。其形式为:
pointer=newtype或者
pointer=newtype[elements]
第一个表达式用来给一个单元素的数据类型分配内存。第二个表达式用来给一个数组分配内存。
例如:int*bobby;bobby=newint[5];
在这个例子里,操作系统分配了可存储5个整型int元素的内存空间,返回指向这块空间开始位置的指针并将它赋给bobby。因此,现在bobby指向一块可存储5个整型元素的合法的内存空间,如下图所示。
你可能会问我们刚才所作的给指针分配内存空间与定义一个普通的数组有什么不同。最重要的不同是,数组的长度必须是一个常量,这就将它的大小在程序执行之前的设计阶段就被决定了。而采用动态内存分配,数组的长度可以常量或变量,其值可以在程序执行过程中再确定。
动态内存分配通常由操作系统控制,在多任务的环境中,它可以被多个应用(applications)共享,因此内存有可能被用光。如果这种情况发生,操作系统将不能在遇到操作符new时分配所需的内存,一个无效指针(nullpointer)将被返回。因此,我们建议在使用new之后总是检查返回的指针是否为空(null),如下例所示:
int*bobby;bobby=newint[5];
90/178
if(bobby==NULL){
//errorassigningmemory.Takemeasures.};
删除操作符delete
既然动态分配的内存只是在程序运行的某一具体阶段才有用,那么一旦它不再被需要时就应该被释放,以便给后面的内存申请使用。操作符delete因此而产生,它的形式是:
deletepointer;或
delete[]pointer;
第一种表达形式用来删除给单个元素分配的内存,第二种表达形式用来删除多元素(数组)的内存分配。在多数编译器中两种表达式等价,使用没有区别,虽然它们实际上是两种不同的操作,需要考虑操作符重载overloading(我们在后面的section4.2节中将会看到)。
//rememb-o-matic#include?iostream.h?#include?stdlib.h?
intmain(){charinput[100];inti,n;long*l;
cout<<\"Howmanynumbersdoyouwanttotypein?\";cin.getline(input,100);i=atoi(input);l=newlong[i];if(l==NULL)exit(1);for(n=0;n91/178
l[n]=atol(input);}
cout<<\"Youhaveentered:\";for(n=0;n}Howmanynumbersdoyouwanttotypein?5Enternumber:75Enternumber:436Enternumber:1067Enternumber:8Enternumber:32
Youhaveentered:75,436,1067,8,32,
这个简单的例子可以记下用户想输入的任意多个数字,它的实现归功于我们动态地向系统申请用户要输入的数字所需的空间。
NULL是C++库中定义的一个常量,专门设计用来指代空指针的。如果这个常量没有被预先定义,你可以自己定以它为0:
#defineNULL0
在检查指针的时候,0和NULL并没有区别。但用NULL来表示空指针更为常用,并且更易懂。原因是指针很少被用来比较大小或被直接赋予一个除0以外的数字常量,使用NULL,这一赋值行为就被符号化了。
ANSI-C中的动态内存管理DynamicmemoryinANSI-C
操作符new和delete仅在C++中有效,而在C语言中没有。在C语言中,为了动态分配内存,我们必须求助于函数库stdlib.h。因为该函数库在C++中仍然有效,并且在一些现存的程序仍然使用,所以我们下面将学习一些关于这个函数库中的函数用法。
92/178
函数malloc
这是给指针动态分配内存的通用函数。它的原型是:void*malloc(size_tnbytes);
其中nbytes是我们想要给指针分配的内存字节数。这个函数返回一个void*类型的指针,因此我们需要用类型转换(typecast)来把它转换成目标指针所需要的数据类型,例如:
char*ronny;
ronny=(char*)malloc(10);
这个例子将一个指向10个字节可用空间的指针赋给ronny。当我们想给一组除char以外的类型(不是1字节长度的)的数值分配内存的时候,我们需要用元素数乘以每个元素的长度来确定所需内存的大小。幸运的是我们有操作符sizeof,它可以返回一个具体数据类型的长度。
int*bobby;
bobby=(int*)malloc(5*sizeof(int));
这一小段代码将一个指向可存储5个int型整数的内存块的指针赋给bobby,它的实际长度可能是2,4或更多字节数,取决于程序是在什么操作系统下被编译的。
函数calloc
calloc与malloc在操作上非常相似,他们主要的区别是在原型上:void*calloc(size_tnelements,size_tsize);
因为它接收2个参数而不是1个。这两个参数相乘被用来计算所需内存块的总长度。通常第一个参数(nelements)是元素的个数,第二个参数(size)被用来表示每个元素的长度。例如,我们可以像下面这样用calloc定义bobby:
int*bobby;
bobby=(int*)calloc(5,sizeof(int));
malloc和calloc的另一点不同在于calloc会将所有的元素初始化为0。
函数realloc
93/178
它被用来改变已经被分配给一个指针的内存的长度。void*realloc(void*pointer,size_tsize);
参数pointer用来传递一个已经被分配内存的指针或一个空指针,而参数size用来指明新的内存长度。这个函数给指针分配size字节的内存。这个函数可能需要改变内存块的地址以便能够分配足够的内存来满足新的长度要求。在这种情况下,指针当前所指的内存中的数据内容将会被拷贝到新的地址中,以保证现存数据不会丢失。函数返回新的指针地址。如果新的内存尺寸不能够被满足,函数将会返回一个空指针,但原来参数中的指针pointer及其内容保持不变。
函数free
这个函数用来释放被前面malloc,calloc或realloc所分配的内存块。voidfree(void*pointer);
注意:这个函数只能被用来释放由函数malloc,calloc和realloc所分配的空间。你可以参考C++referenceforcstdlib获得更多关于这些函数的信息。
3.5数据结构(DataStructures)
一个数据结构是组合到同一定义下的一组不同类型的数据,各个数据类型的长度可能不同。它的形式是:
structmodel_name{type1element1;type2element2;type3element3;..
}object_name;
这里model_name是一个这个结构类型的模块名称。object_name为可选参数,是一个或多个具体结构对象的标识。在花括号{}内是组成这一结构的各个元素的类型和子标识。
94/178
如果结构的定义包括参数model_name(可选),该参数即成为一个与该结构等价的有效的类型名称。例如:
structproducts{charname[30];floatprice;};
productsapple;productsorange,melon;
我们首先定义了结构模块products,它包含两个域:name和price,每一个域是不同的数据类型。然后我们用这个结构类型的名称(products)来声明了3个该类型的对象:apple,orange和melon。
一旦被定义,products就成为一个新的有效数据类型名称,可以像其他基本数据类型,如int,char或short一样,被用来声明该数据类型的对象(object)变量。
在结构定义的结尾可以加可选项object_name,它的作用是直接声明该结构类型的对象。例如,我们也可以这样声明结构对象apple,orange和melon:
structproducts{charname[30];floatprice;
}apple,orange,melon;
并且,像上面的例子中如果我们在定义结构的同时声明结构的对象,参数model_name(这个例子中的products)将变为可选项。但是如果没有model_name,我们将不能在后面的程序中用它来声明更多此类结构的对象。
清楚地区分结构模型model和它的对象的概念是很重要的。参考我们对变量所使用的术语,模型model是一个类型type,而对象object是变量variable。我们可以从同一个模型model(type)实例化出很多对象objects(variables)。
在我们声明了确定结构模型的3个对象(apple,orange和melon)之后,我们就可以对它们的各个域(field)进行操作,这通过在对象名和域名之间插入符号点(.)来实现。例如,我们可以像使用一般的标准变量一样对下面的元素进行操作:
apple.nameapple.price
95/178
orange.nameorange.pricemelon.namemelon.price
它们每一个都有对应的数据类型:apple.name,orange.name和melon.name是字符数组类型char[30],而apple.price,orange.price和melon.price是浮点型float。
下面我们看另一个关于电影的例子://exampleaboutstructures#include?iostream.h?#include?string.h?#include?stdlib.h?
structmovies_t{chartitle[50];intyear;}mine,yours;
voidprintmovie(movies_tmovie);
intmain(){charbuffer[50];
strcpy(mine.title,\"2001ASpaceOdyssey\");mine.year=1968;cout<<\"Entertitle:\";cin.getline(yours.title,50);cout<<\"Enteryear:\";cin.getline(buffer,50);yours.year=atoi(buffer);
cout<<\"Myfavouritemovieis:\\n\";
96/178
printmovie(mine);cout<<\"Andyours:\\n\";printmovie(yours);return0;}
voidprintmovie(movies_tmovie){cout<cout<<\"(\"<这个例子中我们可以看到如何像使用普通变量一样使用一个结构的元素及其本身。例如,yours.year是一个整型数据int,而mine.title是一个长度为50的字符数组。注意这里mine和yours也是变量,他们是movies_t类型的变量,被传递给函数printmovie()。因此,结构的重要优点之一就是我们既可以单独引用它的元素,也可以引用整个结构数据块。
结构经常被用来建立数据库,特别是当我们考虑结构数组的时候。//arrayofstructures#include?iostream.h?#include?stdlib.h?
#defineN_MOVIES5
structmovies_t{chartitle[50];
97/178
intyear;
}films[N_MOVIES];
voidprintmovie(movies_tmovie);
intmain(){charbuffer[50];intn;
for(n=0;ncout<<\"\\nYouhaveenteredthesemovies:\\n\";for(n=0;nvoidprintmovie(movies_tmovie){cout<cout<<\"(\"<Entertitle:AlienEnteryear:1979Entertitle:BladeRunnerEnteryear:198298/178
Entertitle:MatrixEnteryear:1999Entertitle:RearWindowEnteryear:19Entertitle:TaxiDriverEnteryear:1975
Youhaveenteredthesemovies:Alien(1979)BladeRunner(1982)Matrix(1999)RearWindow(19)TaxiDriver(1975)
结构指针(Pointerstostructures)
就像其它数据类型一样,结构也可以有指针。其规则同其它基本数据类型一样:指针必须被声明为一个指向结构的指针:
structmovies_t{chartitle[50];intyear;};
movies_tamovie;movies_t*pmovie;
这里amovie是一个结构类型movies_t的对象,而pmovie是一个指向结构类型movies_t的对象的指针。所以,同基本数据类型一样,以下表达式正确的:
pmovie=&amovie;
下面让我们看另一个例子,它将引入一种新的操作符://pointerstostructures#include?iostream.h?
99/178
#include?stdlib.h?
structmovies_t{chartitle[50];intyear;};
intmain(){charbuffer[50];
movies_tamovie;movies_t*pmovie;pmovie=&amovie;
cout<<\"Entertitle:\";cin.getline(pmovie->title,50);cout<<\"Enteryear:\";
cin.getline(buffer,50);pmovie->year=atoi(buffer);
cout<<\"\\nYouhaveentered:\\n\";cout<title;cout<<\"(\"<year<<\")\\n\";return0;
}Entertitle:MatrixEnteryear:1999
Youhaveentered:
100/178
Matrix(1999)
上面的代码中引入了一个重要的操作符:->。这是一个引用操作符,常与结构或类的指针一起使用,以便引用其中的成员元素,这样就避免使用很多括号。例如,我们用:
pmovie->title来代替:(*pmovie).title
以上两种表达式pmovie->title和(*pmovie).title都是合法的,都表示取指针pmovie所指向的结构其元素title的值。我们要清楚将它和以下表达区分开:
*pmovie.title它相当于*(pmovie.title)
表示取结构pmovie的元素title作为指针所指向的值,这个表达式在本例中没有意义,因为title本身不是指针类型。
下表中总结了指针和结构组成的各种可能的组合:表达式
描述
等价于
结构pmovie的元素title
指针pmovie所指向的结构其元素title的值(*pmovie).title
pmovie.titlepmovie->title
结构pmovie的元素title作为指针所指向的值*pmovie.title
*(pmovie.title)
结构嵌套(Nestingstructures)
结构可以嵌套使用,即一个结构的元素本身又可以是另一个结构类型。例如:structmovies_t{chartitle[50];intyear;}
101/178
structfriends_t{charname[50];charemail[50];
movies_tfavourite_movie;}charlie,maria;
friends_t*pfriends=&charlie;
因此,在有以上声明之后,我们可以使用下面的表达式:charlie.name
maria.favourite_movie.titlecharlie.favourite_movie.yearpfriends->favourite_movie.year(以上最后两个表达式等价)
本节中所讨论的结构的概念与C语言中结构概念是一样的。然而,在C++中,结构的概念已经被扩展到与类(class)相同的程度,只是它所有的元素都是公开的(public)。在后面的章节4.1“类”中,我们将进一步深入讨论这个问题。
3.6自定义数据类型(Userdefineddatatypes)
前面我们已经看到过一种用户(程序员)定义的数据类型:结构。除此之外,还有一些其它类型的用户自定义数据类型:
定义自己的数据类型(typedef)
C++允许我们在现有数据类型的基础上定义我们自己的数据类型。我们将用关键字typedef来实现这种定义,它的形式是:
typedefexisting_typenew_type_name;
这里existing_type是C++基本数据类型或其它已经被定义了的数据类型,new_type_name是我们将要定义的新数据类型的名称。例如:
typedefcharC;
typedefunsignedintWORD;
102/178
typedefchar*string_t;typedefcharfield[50];
在上面的例子中,我们定义了四种新的数据类型:C,WORD,string_t和field,它们分别代替char,unsignedint,char*和char[50]。这样,我们就可以安全的使用以下代码:
Cachar,anotherchar,*ptchar1;WORDmyword;string_tptchar2;fieldname;
如果在一个程序中我们反复使用一种数据类型,而在以后的版本中我们有可能改变该数据类型的情况下,typedef就很有用了。或者如果一种数据类型的名称太长,你想用一个比较短的名字来代替,也可以是用typedef。
联合(Union)
联合(Union)使得同一段内存可以被按照不同的数据类型来访问,数据实际是存储在同一个位置的。它的声明和使用看起来与结构(structure)十分相似,但实际功能是完全不同的:
unionmodel_name{type1element1;type2element2;type3element3;..
}object_name;
union中的所有被声明的元素占据同一段内存空间,其大小取声明中最长的元素的大小。例如:
unionmytypes_t{charc;inti;floatf;
103/178
}mytypes;定义了3个元素:mytypes.cmytypes.imytypes.f
每一个是一种不同的数据类型。既然它们都指向同一段内存空间,改变其中一个元素的值,将会影响所有其他元素的值。
union的用途之一是将一种较长的基本类型与由其它比较小的数据类型组成的结构(structure)或数组(array)联合使用,例如:
unionmix_t{longl;struct{shorthi;shortlo;}s;charc[4];}mix;
以上例子中定义了3个名称:mix.l,mix.s和mix.c,我们可以通过这3个名字来访问同一段4bytes长的内存空间。至于使用哪一个名字来访问,取决于我们想使用什么数据类型,是long,short还是char。下图显示了在这个联合(union)中各个元素在内存中的的可能结构,以及我们如何通过不同的数据类型进行访问:
匿名联合(Anonymousunion)
在C++我们可以选择使联合(union)匿名。如果我们将一个union包括在一个结构(structure)的定义中,并且不赋予它object名称(就是跟在花括号{}后面的名字),这个union就是匿名的。这种情况下我们可以直接使用union中元素的名字来访问该元素,而不需要再在前面加union对象的名称。在下面的例子中,我们可以看到这两种表达方式在使用上的区别:
unionstruct{
anonymousunion
104/178
chartitle[50];charauthor[50];union{floatdollars;intyens;}price;}book;
struct{
chartitle[50];charauthor[50];union{floatdollars;intyens;};}book;
以上两种定义的唯一区别在于左边的定义中我们给了union一个名字price,而在右边的定义中我们没给。在使用时的区别是当我们想访问一个对象(object)的元素dollars和yens时,在前一种定义的情况下,需要使用:
book.price.dollarsbook.price.yens
而在后面一种定义下,我们直接使用:book.dollarsbook.yens
再一次提醒,因为这是一个联合(union),域dollars和yens占据的是同一块内存空间,所以它们不能被用来存储两个不同的值。也就是你可以使用一个dollars或yens的价格,但不能同时使用两者。
枚举Enumerations(enum)
枚举(Enumerations)可以用来生成一些任意类型的数据,不只限于数字类型或字符类型,甚至常量true和false。它的定义形式如下:
105/178
enummodel_name{value1,value2,value3,..
}object_name;
例如,我们可以定义一种新的变量类型叫做color_t来存储不同的颜色:enumcolors_t{black,blue,green,cyan,red,purple,yellow,white};
注意在这个定义里我们没有使用任何基本数据类型。换句话说,我们创造了一种的新的数据类型,而它并没有基于任何已存在的数据类型:类型color_t,花括号{}中包括了它的所有的可能取值。例如,在定义了colors_t列举类型后,我们可以使用以下表达式:
colors_tmycolor;mycolor=blue;
if(mycolor==green)mycolor=red;
实际上,我们的枚举数据类型在编译时是被编译为整型数值的,而它的数值列表可以是任何指定的整型常量。如果没有指定常量,枚举中第一个列出的可能值为0,后面的每一个值为前面一个值加1。因此,在我们前面定义的数据类型colors_t中,black相当于0,blue相当于1,green相当于2,后面依此类推。
如果我们在定义枚举数据类型的时候明确指定某些可能值(例如第一个)的等价整数值,后面的数值将会在此基础上增加,例如:
enummonths_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。
第四章面向对象编程
106/178
Object-orientedProgramming
1.类,构造函数和析构函数,类的指针
Classes.ConstructorsandDestructors.Pointerstoclasses.2.操作符重载,this,静态成员
OverloadingOperators.this.Staticmembers3.类之间的关系
Relationshipsbetweenclasses:friend.Inheritance4.虚拟成员,抽象,多态
VirtualMembers.Abstraction.Polymorphism4.1类(Classes)
类(class)是一种将数据和函数组织在同一个结构里的逻辑方法。定义类的关键字为class,其功能与C语言中的struct类似,不同之处是class可以包含函数,而不像struct只能包含数据元素。
类定义的形式是:classclass_name{permission_label_1:member1;permission_label_2:member2;...
}object_name;
其中class_name是类的名称(用户自定义的类型),而可选项object_name是一个或几个对象(object)标识。Class的声明体中包含成员members,成员可以是数据或函数定义,同时也可以包括允许范围标志permissionlabels,范围标志可以是以下三个关键字中任意一个:private:,public:或protected:。它们分别代表以下含义:
?private:class的private成员,只有同一个class的其他成员或该class的“friend”class可以访问这些成员。
107/178
?protected:class的protected成员,只有同一个class的其他成员,或该class的“friend”class,或该class的子类(derivedclasses)可以访问这些成员。
?public:class的public成员,任何可以看到这个class的地方都可以访问这些成员。
如果我们在定义一个class成员的时候没有声明其允许范围,这些成员将被默认为private范围。
例如:
classCRectangle{
intx,y;public:
voidset_values(int,int);intarea(void);}rect;
上面例子定义了一个classCRectangle和该class类型的对象变量rect。这个class有4个成员:两个整型变量(x和y),在private部分(因为private是默认的允许范围);以及两个函数,在public部分:set_values()和area(),这里只包含了函数的原型(prototype)。
注意class名称与对象(object)名称的不同:在上面的例子中,CRectangle是class名称(即用户定义的类型名称),而rect是一个CRectangle类型的对象名称。它们的区别就像下面例子中类型名int和变量名a的区别一样:
inta;
int是class名称(类型名),而a是对象名objectname(变量)。
在程序中,我们可以通过使用对象名后面加一点再加成员名称(同使用Cstructs一样),来引用对象rect的任何public成员,就像它们只是一般的函数或变量。例如:
rect.set_value(3,4);myarea=rect.area();
108/178
但我们不能够引用x或y,因为它们是该class的private成员,它们只能够在该class的其它成员中被引用。晕了吗?下面是关于classCRectangle的一个复杂的例子:
//classesexample#includeclassCRectangle{intx,y;public:
voidset_values(int,int);intarea(void){return(x*y);}};
voidCRectangle::set_values(inta,intb){x=a;y=b;}
intmain(){CRectanglerect;rect.set_values(3,4);
cout<<\"area:\"<area:12上面代码中新的东西是在定义函数set_values().使用的范围操作符(双冒号::)。它是用来在一个class之外定义该class的成员。注意,我们在CRectangleclass内部已经定义了函数area()的具体操作,因为这个函数非常简单。而对函数set_values(),在class内部只是定义了它的原型prototype,而其实现是在class之外定义的。这种在class之外定义其成员的情况必须使用范围操作符::。
范围操作符(::)声明了被定义的成员所属的class名称,并赋予被定义成员适当的范围属性,这些范围属性与在class内部定义成员的属性是一样的。例如,在上面的例
109/178
子中,我们在函数set_values()中引用了private变量x和y,这些变量只有在class内部和它的成员中才是可见的。
在class内部直接定义完整的函数,和只定义函数的原型而把具体实现放在class外部的唯一区别在于,在第一种情况中,编译器(compiler)会自动将函数作为inline考虑,而在第二种情况下,函数只是一般的class成员函数。
我们把x和y定义为private成员(记住,如果没有特殊声明,所有class的成员均默认为private),原因是我们已经定义了一个设置这些变量值的函数(set_values()),这样一来,在程序的其它地方就没有办法直接访问它们。也许在一个这样简单的例子中,你无法看到这样保护两个变量有什么意义,但在比较复杂的程序中,这是非常重要的,因为它使得变量不会被意外修改(这里意外指的是从object的角度来讲的意外)。
使用class的一个更大的好处是我们可以用它来定义多个不同对象(object)。例如,接着上面classCRectangle的例子,除了对象rect之外,我们还可以定义对象rectb:
//classexample#includeclassCRectangle{
intx,y;public:
voidset_values(int,int);intarea(void){return(x*y);}};
voidCRectangle::set_values(inta,intb){x=a;y=b;}
intmain(){
CRectanglerect,rectb;
110/178
rect.set_values(3,4);rectb.set_values(5,6);
cout<<\"rectarea:\"<rectarea:12rectbarea:30
注意:调用函数rect.area()与调用rectb.area()所得到的结果是不一样的。这是因为每一个classCRectangle的对象都拥有它自己的变量x和y,以及它自己的函数set_value()和area()。
这是基于对象(object)和面向对象编程(object-orientedprogramming)的概念的。这个概念中,数据和函数是对象(object)的属性(properties),而不是像以前在结构化编程(structuredprogramming)中所认为的对象(object)是函数参数。在本节及后面的小节中,我们将讨论面向对象编程的好处。
在这个具体的例子中,我们讨论的class(object的类型)是CRectangle,有两个实例(instance),或称对象(object):rect和rectb,每一个有它自己的成员变量和成员函数。
构造函数和析构函数(Constructorsanddestructors)
对象(object)在生成过程中通常需要初始化变量或分配动态内存,以便我们能够操作,或防止在执行过程中返回意外结果。例如,在前面的例子中,如果我们在调用函数set_values()之前就调用了函数area(),将会产生什么样的结果呢?可能会是一个不确定的值,因为成员x和y还没有被赋于任何值。
为了避免这种情况发生,一个class可以包含一个特殊的函数:构造函数constructor,它可以通过声明一个与class同名的函数来定义。当且仅当要生成一个class的新的实例(instance)的时候,也就是当且仅当声明一个新的对象,或给该class的一个对象分配内存的时候,这个构造函数将自动被调用。下面,我们将实现包含一个构造函数的CRectangle:
//classexample#include111/178
classCRectangle{intwidth,height;public:
CRectangle(int,int);
intarea(void){return(width*height);}};
CRectangle::CRectangle(inta,intb){width=a;height=b;}
intmain(){
CRectanglerect(3,4);CRectanglerectb(5,6);
cout<<\"rectarea:\"<rectarea:12rectbarea:30
正如你所看到的,这个例子的输出结果与前面一个没有区别。在这个例子中,我们只是把函数set_values换成了class的构造函数constructor。注意这里参数是如何在class实例(instance)生成的时候传递给构造函数的:
CRectanglerect(3,4);CRectanglerectb(5,6);
同时你可以看到,构造函数的原型和实现中都没有返回值(returnvalue),也没有void类型声明。构造函数必须这样写。一个构造函数永远没有返回值,也不用声明void,就像我们在前面的例子中看到的。
析构函数Destructor完成相反的功能。它在objects被从内存中释放的时候被自动调用。释放可能是因为它存在的范围已经结束了(例如,如果object被定义为一个
112/178
函数内的本地(local)对象变量,而该函数结束了);或者是因为它是一个动态分配的对象,而被使用操作符delete释放了。
析构函数必须与class同名,加水波号tilde(~)前缀,必须无返回值。
析构函数特别适用于当一个对象被动态分别内存空间,而在对象被销毁的时我们希望释放它所占用的空间的时候。例如:
//exampleonconstructorsanddestructors#includeclassCRectangle{int*width,*height;public:
CRectangle(int,int);~CRectangle();
intarea(void){return(*width**height);}};
CRectangle::CRectangle(inta,intb){width=newint;height=newint;*width=a;*height=b;}
CRectangle::~CRectangle(){deletewidth;deleteheight;}
intmain(){
113/178
CRectanglerect(3,4),rectb(5,6);
cout<<\"rectarea:\"<rectarea:12rectbarea:30
构造函数重载(OverloadingConstructors)
像其它函数一样,一个构造函数也可以被多次重载(overload)为同样名字的函数,但有不同的参数类型和个数。记住,编译器会调用与在调用时刻要求的参数类型和个数一样的那个函数(Section2.3,Functions-II)。在这里则是调用与类对象被声明时一样的那个构造函数。
实际上,当我们定义一个class而没有明确定义构造函数的时候,编译器会自动假设两个重载的构造函数(默认构造函数\"defaultconstructor\"和复制构造函数\"copyconstructor\")。例如,对以下class:
classCExample{public:inta,b,c;
voidmultiply(intn,intm){a=n;b=m;c=a*b;};};
没有定义构造函数,编译器自动假设它有以下constructor成员函数:?Emptyconstructor
它是一个没有任何参数的构造函数,被定义为nop(没有语句)。它什么都不做。CExample::CExample(){};?Copyconstructor
它是一个只有一个参数的构造函数,该参数是这个class的一个对象,这个函数的功能是将被传入的对象(object)的所有非静态(non-static)成员变量的值都复制给自身这个object。
114/178
CExample::CExample(constCExample&rv){a=rv.a;b=rv.b;c=rv.c;}
必须注意:这两个默认构造函数(emptyconstruction和copyconstructor)只有在没有其它构造函数被明确定义的情况下才存在。如果任何其它有任意参数的构造函数被定义了,这两个构造函数就都不存在了。在这种情况下,如果你想要有emptyconstruction和copyconstructor,就必需要自己定义它们。
当然,如果你也可以重载class的构造函数,定义有不同的参数或完全没有参数的构造函数,见如下例子:
//overloadingclassconstructors#includeClassCRectangle{intwidth,height;public:
CRectangle();CRectangle(int,int);
intarea(void){return(width*height);}};
CRectangle::CRectangle(){width=5;height=5;}
CRectangle::CRectangle(inta,intb){width=a;height=b;}
115/178
intmain(){
CRectanglerect(3,4);CRectanglerectb;
cout<<\"rectarea:\"<rectarea:12rectbarea:25
在上面的例子中,rectb被声明的时候没有参数,所以它被使用没有参数的构造函数进行初始化,也就是width和height都被赋值为5。
注意在我们声明一个新的object的时候,如果不想传入参数,则不需要写括号():CRectanglerectb;//rightCRectanglerectb();//wrong!类的指针(Pointerstoclasses)
类也是可以有指针的,要定义类的指针,我们只需要认识到,类一旦被定义就成为一种有效的数据类型,因此只需要用类的名字作为指针的名字就可以了。例如:
CRectangle*prect;
是一个指向classCRectangle类型的对象的指针。
就像数据机构中的情况一样,要想直接引用一个由指针指向的对象(object)中的成员,需要使用操作符->。这里是一个例子,显示了几种可能出现的情况:
//pointertoclassesexample#includeclassCRectangle{intwidth,height;public:
voidset_values(int,int);
intarea(void){return(width*height);}
116/178
};
voidCRectangle::set_values(inta,intb){width=a;height=b;}
intmain(){
CRectanglea,*b,*c;
CRectangle*d=newCRectangle[2];b=newCRectangle;c=&a;
a.set_values(1,2);b->set_values(3,4);d->set_values(5,6);d[1].set_values(7,8);
cout<<\"aarea:\"<area()<area()<aarea:2*barea:12*carea:2d[0]area:30d[1]area:56
以下是怎样读前面例子中出现的一些指针和类操作符(*,&,.,->,[]):
117/178
?*x读作:pointedbyx(由x指向的)?&x读作:addressofx(x的地址)
?x.y读作:memberyofobjectx(对象x的成员y)
?(*x).y读作:memberyofobjectpointedbyx(由x指向的对象的成员y)?x->y读作:memberyofobjectpointedbyx(同上一个等价)?x[0]读作:firstobjectpointedbyx(由x指向的第一个对象)?x[1]读作:secondobjectpointedbyx(由x指向的第二个对象)?x[n]读作:(n+1)thobjectpointedbyx(由x指向的第n+1个对象)
在继续向下阅读之前,一定要确定你明白所有这些的逻辑含义。如果你还有疑问,再读一遍这一笑节,或者同时参考小节\"3.3,指针(Pointers)\"和\"3.5,数据结构(Structures)\".
由关键字struct和union定义的类
类不仅可以用关键字class来定义,也可以用struct或union来定义。
因为在C++中类和数据结构的概念太相似了,所以这两个关键字struct和class的作用几乎是一样的(也就是说在C++中struct定义的类也可以有成员函数,而不仅仅有数据成员)。两者定义的类的唯一区别在于由class定义的类所有成员的默认访问权限为private,而struct定义的类所有成员默认访问权限为public。除此之外,两个关键字的作用是相同的。
union的概念与struct和class定义的类不同,因为union在同一时间只能存储一个数据成员。但是由union定义的类也是可以有成员函数的。union定义的类访问权限默认为public。
4.2操作符重载(Overloadingoperators)
C++实现了在类(class)之间使用语言标准操作符,而不只是在基本数据类型之间使用。例如:
inta,b,c;
118/178
a=b+c;
是有效操作,因为加号两边的变量都是基本数据类型。然而,我们是否可以进行下面的操作就不是那么显而易见了(它实际上是正确的):
struct{charproduct[50];floatprice;}a,b,c;a=b+c;
将一个类class(或结构struct)的对象赋给另一个同种类型的对象是允许的(通过使用默认的复制构造函数copyconstructor)。但相加操作就有可能产生错误,理论上讲它在非基本数据类型之间是无效的。
但归功于C++的操作符重载(overload)能力,我们可以完成这个操作。像以上例子中这样的组合类型的对象在C++中可以接受如果没有操作符重载则不能被接受的操作,我们甚至可以修改这些操作符的效果。以下是所有可以被重载的操作符的列表:
+
-*
/
=
<
>
+=-=*=/=<<>>
&
^
!
|
<<=>>===!=<=>=++--%~
&=^=|=&&||%=[]()newdelete
要想重载一个操作符,我们只需要编写一个成员函数,名为operator,后面跟我们要重载的操作符,遵循以下原型定义:
typeoperatorsign(parameters);
这里是一个操作符+的例子。我们要计算二维向量(bidimensionalvector)a(3,1)与b(1,2)的和。两个二维向量相加的操作很简单,就是将两个x轴的值相加获得结果的x轴值,将两个y轴值相加获得结果的y值。在这个例子里,结果是(3+1,1+2)=(4,3)。
//vectors:overloadingoperatorsexample#includeclassCVector{public:intx,y;CVector(){};CVector(int,int);
119/178
CVectoroperator+(CVector);};
CVector::CVector(inta,intb){x=a;y=b;}
CVectorCVector::operator+(CVectorparam){CVectortemp;temp.x=x+param.x;temp.y=y+param.y;return(temp);}
intmain(){CVectora(3,1);CVectorb(1,2);CVectorc;c=a+b;
cout<4,3如果你迷惑为什么看到这么多遍的CVector,那是因为其中有些是指class名称CVector,而另一些是以它命名的函数名称,不要把它们搞混了:
CVector(int,int);
//函数名称CVector(constructor)
CVectoroperator+(CVector);//函数operator+返回CVector类型的值
120/178
ClassCVector的函数operator+是对数学操作符+进行重载的函数。这个函数可以用以下两种方法进行调用:
c=a+b;
c=a.operator+(b);
注意:我们在这个例子中包括了一个空构造函数(无参数),而且我们将它定义为无任何操作:
CVector(){};
这是很必要的,因为例子中已经有另一个构造函数,CVector(int,int);
因此,如果我们不像上面这样明确定义一个的话,CVector的两个默认构造函数都不存在。
这样的话,main()中包含的语句CVectorc;将为不合法的。
尽管如此,我已经警告过一个空语句块(no-opblock)并不是一种值得推荐的构造函数的实现方式,因为它不能实现一个构造函数至少应该完成的基本功能,也就是初始化class中的所有变量。在我们的例子中,这个构造函数没有完成对变量x和y的定义。因此一个更值得推荐的构造函数定义应该像下面这样:
CVector(){x=0;y=0;};
就像一个class默认包含一个空构造函数和一个复制构造函数一样,它同时包含一个对赋值操作符assignationoperator(=)的默认定义,该操作符用于两个同类对象之间。这个操作符将其参数对象(符号右边的对象)的所有非静态(non-static)数据成员复制给其左边的对象。当然,你也可以将它重新定义为你想要的任何功能,例如,只拷贝某些特定class成员。
重载一个操作符并不要求保持其常规的数学含义,虽然这是推荐的。例如,虽然我们可以将操作符+定义为取两个对象的差值,或用==操作符将一个对象赋为0,但这样做是没有什么逻辑意义的。
虽然函数operator+的原型定义看起来很明显,因为它取操作符右边的对象为其左边对象的函数operator+的参数,其它的操作符就不一定这么明显了。以下列表总结了不同的操作符函数是怎样定义声明的(用操作符替换每个@):
121/178
Expression@aa@
Operator(@)Functionmember
A::operator@()
Globalfunctionoperator@(A)
+-*&!~++--
++--A::operator@(int)operator@(A,int)
a@b+-*/%^&|<>==!=<=>=<<>>&&||,A::operator@(B)operator@(A,B)a@b-=+=-=*=/=%=^=&=|=<<=>>=[]
A::operator()(B,C...)-A::operator->()
-A::operator@(B)
a(b,c...)()a->b
->
*这里a是classA的一个对象,b是B的一个对象,c是classC的一个对象。从上表可以看出有两种方法重载一些class操作符:作为成员函数(memberfunction)或作为全域函数(globalfunction)。它们的用法没有区别,但是我要提醒你,如果不是class的成员函数,则不能访问该class的private或protected成员,除非这个全域函数是该class的friend(friend的含义将在后面的章节解释)。
关键字this
关键字this通常被用在一个class内部,指正在被执行的该class的对象(object)在内存中的地址。它是一个指针,其值永远是自身object的地址。
它可以被用来检查传入一个对象的成员函数的参数是否是该对象本身。例如://this
#includeclassCDummy{public:
intisitme(CDummy¶m);};
intCDummy::isitme(CDummy¶m){if(¶m==this)return1;elsereturn0;
122/178
}
intmain(){CDummya;CDummy*b=&a;if(b->isitme(a))cout<<\"yes,&aisb\";return0;}
yes,&aisb
它还经常被用在成员函数operator=中,用来返回对象的指针(避免使用临时对象)。以下用前面看到的向量(vector)的例子来看一下函数operator=是怎样实现的:
CVector&CVector::operator=(constCVector¶m){x=param.x;y=param.y;return*this;}
实际上,如果我们没有定义成员函数operator=,编译器自动为该class生成的默认代码有可能就是这个样子的。
静态成员(Staticmembers)
一个class可以包含静态成员(staticmembers),可以是数据,也可以是函数。一个class的静态数据成员也被称作类变量\"classvariables\",因为它们的内容不依赖于某个对象,对同一个class的所有object具有相同的值。
例如,它可以被用作计算一个class声明的objects的个数,见以下代码程序://staticmembersinclasses#includeclassCDummy{
123/178
public:staticintn;
CDummy(){n++;};~CDummy(){n--;};};
intCDummy::n=0;
intmain(){CDummya;CDummyb[5];
CDummy*c=newCDummy;cout<cout<7