第三讲:C++对C的扩充(2)
本讲基本要求
掌握:函数默认参数的设置方法;变量引用的方法及作用;字符串变量的定义、应用;。
理解:作用域运算符的作用;new、delete运算符。
重点、难点:变量引用的方法及作用。
六、有默认参数的函数
一般情况下:在函数调用时形参从实参那里取得值,因此实参的个数应与形参相同。但是有时多次调用同一函数时用的是同样的实参值,c++提供简单的处理办法,给形参一个默认值,这样形参就不必一定要从实参取值了。如有一函数声明:
float area(float r=6.5);
指定r的默认值为6.5,如果在调用此函数时,确认r的值为6.5,则可以不必给出实参的值,如:
area(); //相当于area(6.5);
可以看到,在调用有默认参数的函数时,实参的个数可以与形参不同,实参未给定的,从形参的默认值得到值。利用这一特性,可以使函数的使用更加灵活。
在使用带有默认参数的函数时有两点要注意:
(1)如果函数的定义在函数调用之前,则应在函数定义中给出默认值。如果函数的定义在函数调用之后,则在函数调用之前需要有函数声明,此时必须在函数声明中给出默认值,在函数定义时可以不给出默认值。也就是说必须在函数调用之前将默认值的信息通知编译系统。由于编译是从上到下逐行进行的,如果在函数调用之前未得到默认值信息,在编译到函数调用时,就会认为实参个数与形参个数不匹配而报错。
如果在声明函数时已对形参给出了默认值,而在定义函数时又对形参给出默认值,有的编译系统会给出“重复指定默认值”的报错信息,有的编译系统对此不报错,甚至允许在声明时和定义时给出的默认值不同,此时编译系统以先遇到的为准。由于函数声明在函数定义之前,因此以声明时给出的默认值为准,而忽略定义函数时给出的默认值。
(2)一个函数不能既作为重载函数,又作为有默认参数的函数。因为当调用函数时如果少写一个参数,系统无法判定是利用重载函数还是利用默认参数的函数,会出现二义性,系统无法执行。
七、变量的引用:引用(reference)是C++对C的一个重要扩充。
1.引用的概念
在c++中,变量的“引用”就是变量的别名,因此引用又称为别名(alias)。建立“引用”的作用是为一个变量起另一个名字,以便在需要时可以方便、间接地引用该变量,这就是引用名称的由来。对一个变量的“引用”的所有操作,实际上都是对其所代表的(原来的)变量的操作。
假如有一个变量a,想给它起一个别名b,可以这样写:
int a;
int &b=a; //声明b是一个整型变量的引用变量,它被初始化为a
这就声明了b是a的“引用”,即a的别名。经过这样的声明后,使用a或b的作用相同,都代表同一变量。注意:在上述声明中,&是“引用声明符”,此时它并不代表地址。不要理解为“把a的值赋给b的地址”。对变量声明一个引用,并不另开辟内存单元,b和a都代表同一变量单元。在声明一个引用时,必须同时使之初始化,即声明它代表哪一个变量。
请注意:由于引用不是独立的变量,编译系统不给它单独分配存储单元,因此在建立引用时只有声明,没有定义,只是声明它和原有的某一变量的关系。
在声明一个变量的引用后,在本函数执行期间,该引用一直与其代表的变量相联系,不能再作为其他变量的别名。下面的用法不对:
int a1,a2;
int &b=a1; //使b成为变量a1的引用(别名)
int &b=a2; //又企图使b成为变量a2的引用(别名)是不行的
2.引用的简单使用
通过下面的例子可以了解引用的简单使用。
例9 了解引用和变量的关系。
#include <iostream>
using namespace std;
int main( )
{ int a=10;
int &b=a;//声明b是a的引用
a=a*a; //a的值变化了,b的值也应一起变化
cout<<a<<" "<<b<<endl;
b=b/5; //b的值变化了,a的值也应一起变化
cout<<b<<" "<<a<<endl;
return 0;
}
a的值开始为1O,b是a的引用,它的值当然也应该是10,当a的值变为100(a。a的值)时,b的值也随之变为 100,在输出a和b的值后,b的值变为20,显然a的值也应为20。
运行记录如下:
100 100
20 20![]()
3.关于引用的简单说明
(1)引用并不是一种独立的数据类型,它必须与某一种类型的数据相联系。声明引用时必须指定它代表的是哪个变量,即对它初始化。
(2)引用与其所代表的变量共享同一内存单元,系统并不为引用另外分配存储空间。 实际上,编译系统使引用和其代表的变量具有相同的地址。
(3)当看到&a这样的形式时,怎样区别是声明引用变量还是取地址的操作呢?请记住,当及a的前面有类型符时(如int盘a),它必然是对引用的声明;如果前面没有类型符(如int &a),此时的&是取地址运算符。
(4)对引用的初始化,可以用一个变量名,也可以用另一个引用。
(5)引用在初始化后不能再被重新声明为另一变量的别名。
实际上,在c++程序中很少使用独立变量的引用,如果要使用某一个变量,就直接使用它的原名,没有必要故意使用它的别名。前面举的例子只是为了说明引用的特征和基本的用法。那么有了变量名,为什么还需要一个别名呢?请见下面的介绍。
4.将引用作为函数参数
C++之所以增加“引用”,主要是利用它作为函数参数,以扩充函数传递数据的功能。
在C语言中,函数的参数传递有以下两种情况。
(1)将变量名作为实参。这时传给形参的是变量的值。传递是单向的,在执行函数期间形参值发生变化并不传回给实参,因为在调用函数时,形参和实参不是同一个存储单元。下面的程序无法实现两个变量的值互换。
例10 无法实现两个变量的值互换的程序。
#include <iostream>
using namespace std;
void swap(int a,int b)
{ int temp;
temp=a;
a=b;
b=temp; //实现a和b的值互换
}
int main( )
{ int i=3,j=5;
swap(i,j);
cout<<i<<","<<j<<endl; //i和j的值未互换
return 0;
}
输出i和j的值仍为3和5。见图2,图2(a)表示调用函数时的数据传递,图2(b)是执行swap函数体后的情况,a和b值的改变不会影响i和j的值。
图2:i、j值未交换, 图3:i、j值交换,
(2)传递变量的指针。为了解决上面这个问题,在c程序中可以用传递变量地址的方法。使形参得到一个变量的地址,这时形参指针变量指向实参变量单元。
例11 使用指针变量作形参,实现两个变量的值互换。
#include <iostream>
using namespace std;
void swap(int *p1,int *p2)
{ int temp;
temp=*p1;
*p1= *p2;
*p2=temp;
}
int main( )
{ int i=3,j=5;
swap(&i,&j);
cout<<i<<","<<j<<endl;
return 0;
}
(3)传送变量的别名。c++把变量的引用作为函数形参,就弥补了上面的不足。这就是向函数传递数据的第三种方法,即传送变量的别名。
例12 利用“引用形参”实现两个变量的值互换。
#include <iostream>
using namespace std;
void swap(int &a,int &b)
{ int temp;
temp=a;
a=b;
b=temp;
}
int main( )
{ int i=3,j=5;
swap(i,j);
cout<<"i="<<i<<" "<<"j="<<j<<endl;
return 0;
}输出结果为 i=5 j=3
图4:别名实现变量的交换
分析使用引用和使用指针变量作函数形参有什么不同? 可以发现:
①不必在swap函数中设立指针变量,指针变量要另外开辟内存单元,其内容是地 址。而引用不是一个独立的变量,不单独占内存单元,在本例中其值为一整数。
②在main函数中调用swap函数时实参不必在变量名前加&以表示地址。这种传递方式相当于Pascal语言中的“变量形参”,系统传送的是实参的地址而不是实参的值。
③使用指针变量时,为了表示指针变量所指向的变量,必须使用指针运算符。(如例11程序内swap函数中的。pl,*p2),而使用引用时,引用就代表该变量,不必使用指针运算符*(见例12程序内swap函数)。对比例11和例12中的swap函数,可以发现例12中的swap函数比例11中的swap函数简单。
④用引用能完成的工作,用指针也能完成。但引用比指针的使用直观、方便,直截了当,不必“兜圈子”,容易理解。有些过去只能用指针来处理的问题,现在可以用引用来代替,从而降低了程序设计的难度。
5.对引用的进一步说明
有了以上的初步知识后,再对使用引用的一些细节作进一步讨论。
(1)不能建立void类型的引用,如: void &a=9; //错误
因为任何实际存在的变量都是属于非void类型的,void的含义是无类型或空类型,void只是在语法上相当于一个类型而已。
(2)不能建立引用的数组。如
char c[6]="hello";
char &rc[6]=c; //错误
企图建立一个包含6个元素的引用的数组,这样是不行的,数组名c只代表数组首元素的地址,本身并不是一个占有存储空间的变量。
(3)可以将变量的引用的地址赋给一个指针,此时指针指向的是原来的变量,如
int a=3; //定义a是整型变量
int &b=a; //声明b是整型变量的别名
int *P=&b;//指针变量p指向变量a的引用b,相当于指向a,合法
相当于p指向变量a,其作用与下面一行相同,即: int *p=&a;
如果输出*p的值,就是b的值,也就是a的值。但是不能定义指向引用类型的指针变量,不能写成: int& *p=&a; //企图定义指向引用类型的指针变量P,错误
由于引用不是一种独立的数据类型,因此不能建立指向引用类型的指针变量。
(4)可以建立指针变量的引用,如
int i=5; //定义整型变量i,初值为5
int *p=&i //定义指针变量P,指向i
int * &pt=p;//pt是一个指向整型变量的指针变量的引用,初始化为p
从定义的形式可以看出,&pt表示pt是一个变量的引用,它代表一个int,类型的数据对象(即指针变量),如果输出。pt的值,就是*p的值5。
(5)可以用const对引用加以限定,不允许改变该引用的值。如
int i=5; //定义整型变量i,初值为5
const int &a=i; //声明常引用,不允许改变a的值
a=3; //企图改变引用a的值,错误
但是它并不阻止改变引用所代表的变量的值,如
i=3; //合法 此时输出i和a的值都是3。
这一特征在使用引用作为函数形参时是有用的,因为有时希望保护形参的值不被改变,在第3章中将会看到它的应用。
(6)可以用常量或表达式对引用进行初始化,但此时必须用const作声明。如
int i=5;
const &a=i+3; //合法
此时编译系统是这样处理的:生成一个临时变量,用来存放该表达式的值,引用是该临时变量的别名。系统将"const= &a=i+3"转换为: int temp=1+3; //先将表达式的值存放在临时变量temp中
const int &a=temp;//声明a是temp的别名
临时变量是在内部实现的,用户不能访问临时变量。
用这种办法不仅可以用表达式对引用进行初始化,还可以用不同类型的变量对之初始化(要求能赋值兼容的类型)。如
double d=3.1415926;//d是double类型变量
const int&a=d; //用d初始化a
编译系统将"const int &a=d;"转换为: int temp=d; //先将double类型变量d转换为int型,存放在temp中
const int &a=temp; //temp和a同类则
注意:此时如果输出引用a的值,将是3而不是3.1415926。因为从根本来说,只能对变量建立引用。
如果在上面声明引用时不用const,则会发生错误。如
double d=3.1415926; //d是double类型变量
int &a=d; //未加const,错误
为什么呢?若允许这样做的话,如果修改了引用a的值(例如a=6.28;),则临时变量temp的值也变为6.28,即修改了临时变量temp的值,但不能修改变量d的值,这往往不是用户所希望的,即存在二义性。与其允许修改引用的值而不能实现用户的目的,还不如不允许修改引用的值。这就是c++规定对这类引用必须加const的原因。
c++提供的引用机制是非常有用的,尤其用作函数参数时,比用指针简单、易于理 解,而且可以减少出错机会,提高程序的执行效率,在许多情况下能代替指针的操作。在本书的第3章和第4章中会有具体的使用例子,请读者认真阅读,并在今后的实践中进一步熟悉它的使用。
八、内置函数
调用函数时需要一定的时间,如果有的函数需要频繁使用,则累计所用时间会很长,从而降低程序的执行效率,,c++提供一种提高效率的方法,即在编译时将所调用函数的代码嵌入到主函数中。这种嵌入到主函数中的函数称为内置函数(inline function),又称内嵌函数。在有些书中把它译成内联函数。
指定内置函数的方法很简单,只须在函数首行的左端加一个关键字inline即可。
例13 将函数指定为内置函数。
#include <iostream>
using namespace std;
inline int max(int a,int b,int c) //这是一个内置函数,求3个整数中的最大者
{if (b>a) a=b;
if (c>a) a=c;
return a;
}int main( )
{int i=7,j=10,k=25,m;
m=max(i,j,k);
cout<<"max="<<m<<endl;
return 0;
}
由于在定义函数时指定它为内置函数,因此编译系统在遇到函数调用nmax(i,j,k)时,就用max函数体的代码代替max(i,j,k),同时将实参代替形参。这样,m=max(i,j,k)就被置换成
a=i;b=j;c=k;
if(b>a) a=b;
if(c>a) a=c;
m=a;
内置函数与用#define命令实现的带参宏定义有些相似,但不完全相同。宏定义是在编译前由预处理程序对其预处理的,它只作简单的字符置换而不作语法检查,往往会出现意想不到的错误。
例14 用带参宏定义实现求平方值,
#include <iostream>
using namespace std;
#define power(x) x*x
int main()
{ cout<<power(2)<<endl;
cout<<power(1+1)<<endl;
return 0;
}
本来程序编写者希望两个cout语句都输出2的平方值,但运行结果却是:
4 (输出power(g)的值)
3 (输出power(1+1)的值)
第2个结果显然不是程序设计者所希望的,原因是在进行宏替换时只是简单地将字符“1+l”取代x,因此power(1+1)被置换为1+1*l+1,结果为3。
如果不用#define而用内置函数,也可以达到同样的目的,但避免了上面的副作用。例14程序可改为:
例15 用内置函数实现求平方值。
#inclde<iostream>
useing namespace std;
inline int power(int x) //改用内置函数
{ return x*x;}
int main()
{ cout <<power(2) <<endl;
cout <<power(1+1)<<endl;
return O;
}运行结果是
4 (输出power(2)的值)
4 (输出power(1+1)的值)
可以看到:
1、 用带参宏定义和内置函数都可以实行置换,但具体的做法不同,用内置函数可以达到用#define宏置换的门的,但不会出现带参宏定义的副作用。
2、使用内置函数可以节省运行时间,但却增加了目标程序的长度。
九、作用域运算符
每一个变量都有其确有效的作用域,只能在变量的作用域内使用该变量,不能直接使用其他作用域中的变量。见例6。
例16 局部变量和全局变量同名。
#include <iostream>
using namespace std;
float a=13.5;
int main( )
{ int a=5;
cout<<a<<endl;
return 0; }
“::”a表示全局作用域中的变量a。请注意:不能用“::”访问函数中的局部变量。
十、字符串变量
除了可以使用字符数组处理字符串外,C++还提供了一种更方便的方法—用字符串类型(stung类型)定义字符串变量。
实际上,string并不是C++语言本身具有的基本类型(而char,int,float,double等是 C++本身提供的基本类型),它是在c++标准库中声明的一个字符串类,用这种类可以定义对象。
1.定义字符串变量
和其他类型变量一样,字符串变量必须先定义后使用,定义字符串变量要用类名strmg。如:
string stringl; //定义stringl为字符串变量
string string2="China";//定义string2同时对其初始化
可以看出,这和定义char,int,float,double等类型变量的方法是类似的。
应当注意:要使用string类的功能时,必须在本文件的开头将c++标准库中的“string”头文件包含进来,即应加上#include<string> //注意头文件名不是"string.h这一点是与定义基本数据类型变量所不同的。
2. 对字符串变量的赋值
在定义了字符串变量后,可以用赋值语句对它赋以一个字符串常量,
如: string1="Canada";
而用字符数组时是不能像下面这样做的
char str[10];
str="Hello!"; //错误
既可以用字符串常量给字符串变量赋值,也可以用一个字符串变量给另一个字符串变量赋值。如
string2=stringl; //假设string2.和stringl均已定义为字符串变量
可以对字符串变量中某一字符进行操作,如
string word="Then"; //定义并初始化字符串变量word
word[2]='a'; //修改序号为2的字符,修改后word的值为"Than"
前面已说明,字符串常量以“/o”作为结束符,但将字符串常量存放到字符串变量中时,只存放字符串本身而不包括“/o”。因此字符串变量word中的字符为"Than”(共4个字符)而不是"Than"再加“/0”。
3. 字符串变量的输入输出
可以在输入输出语句中用字符串变量名输入输出字符串,如
cin >>string1; //从键盘输入一个字符串给字符串变量stfingl
cout<<stHnS2; //将字符串stnng2输出
4. 字符串变量的运算
在用字符数组存放字符串时,字符串的运算要用字符串函数,如strcat(连接),strcmp(比较),strcpy(复制),而对stung类对象,可以不用这些函数,而直接用简单的运算符。
(1)用赋值运算符实现字符串复制
stringl=string2; 其作用与“strcpy(string1,string2);”相同。
(2)用加法运算符实现字符串连接
string string1="C++"; //定义string[并赋初值
string string2="Language"; //定义string2并赋初值
stnngl=string1十sane2; //连接string1和string2
连接后string1为"C++ Language"。
(3)用关系运算符实现字符串比较
可以直接用==(等于)、,(大于)、<(小于)、!=(不等于)、>=(大于或等于)、<=(小于或等于)等关系运算符来进行字符串的比较。
5.字符串数组
不仅可以用string定义字符串变量,也可以用string定义字符串数组。如
string name[5]; //定义一个字符串数组,它包含5个字符串元素
string name[5]={"Zhang","Li","Fun","Wang","Tan"}; //定义一个字符中数组并初始化 此时name数组的状况如图5所示。
图5
可以看到:
(1)在一个字符串数组中包含若干个(今为5个)元素,每个元素相当于—个字符串变量。
(2)并不要求每个字符串元素具有相同的K度,即使对同一个元素而言,它的长度也是可以变化的,当向某一个元素重新赋值时,其长度就可能发生变化,,
(3)在字符串数组的每一个元素中存放一个字符串,而不是一个字符,这是字符串数组与字符数组的区别。如果用字符数组存放字符串,一个元素只能存放一个字符,用一个一维字符数组存放一个字符申。
(4)每一个字符中元素中只包含字符串本身的字符而不包括"/0"。
可见用字符串数组存放字符串以及对字符串进行处理是很方便的,使用户感到更加直观,简化了操作,提高了效率。
例17 输入3个字符串,要求按字母由小到大顺序输出。
对于将3个整数按由小到大顺序输出,是很容易处理的。可以按照同样的算法将3个字符串按由小到大顺序输出。程序如下:
#include <iostream>
#include <string>
using namespace std;
int main()
{ string string1,string2,string3,temp;
cout<<"please input three strings:"; //这是对用户输入的提示
cin>>string1>>string2>>string3; //输入3个字符串
if(string2>string3) {temp=string2;string2=string3;string3=temp;} //使串2≤串3
if(string1<=string2) cout<<string1<<" "<<string2<<" "<<string3<<endl;
//如果串1≤串2,则串1≤串2≤串3
else if(string1<=string3) cout<<string2<<" "<<string1<<" "<<string3<<endl;
//如果串1>串2,且串1≤串3,则串2<串1≤串3
else cout<<string2<<" "<<string3<<" "<<string1<<endl;
//如果串1>串2,且串1>串3,则串2≤串3<串3
return 0;
}运行情况如下:
please input three strings:China U.S.A.Germany/
China Germany U.S.A.
这个程序是很好理解的。在程序中对字符串变量用关系运算符进行比较,如同对数值型数据进行比较一样方便。
十一、动态分配/撤销内存的运算符new和delete
在软件开发中,常常需要动态地分配和撤销内存空间。在C语言中是利用库函数malloc和free分配和撤销内存空间的。但是使用malloc函数时必须指定需要开辟的内存空间的大小。其调用形式为: malloc(size)。
此外,malloc函数只能从用户处知道应开辟空间的大小而不知道数据的类型,因此无法使其返回的指针指向具体的数据。其返回值一律为void。类型,必须在程序中进行强制类型转换,才能使其返回的指针指向具体的数据。
C++提供了简便而功能较强的运算符new和delete来取代malloc和free函数(为了与c语言兼容,仍保留这两个函数)。例如:
new int; //开辟一个存放整数的空间,返回一个指向整型数据的指针
new int(100); //开辟一个存放整数的空间,并指定该整数的初值为100
new char[10]; //开辟一个存放字符数组的空间,该数组有10个元素
//返回一个指向字符数据的指针
new int[5][4];//开辟一个存放二维整型数组的空间,该数组大小为5*4
float *P=new float(3.14159) //开辟一个存放实数的空间,并指定该实数的初值为3.14159,
//将返回的指向实型数据的指针赋给指针变量P
new运算符使用的一般格式为:
new类型[初值]; //用new分配数组空间时不能指定初值。
delete运算符使用的一般格式为:
delete[] 指针变量
例如要撤销上面用new开辟的存放实数的空间(上面第5个例子),应该用: delete p;
前面用newchar[10)开辟的空间,如果把返回的指针赋给了指针变量pt,则应该用以下形式的delete运算符撤销所开辟的空间:
delete [] pt; //在指针变量前面加一个方括号,表示对数组空间的操作
例18 开辟空间以存放一个结构体变量。
#include <iostream>
#include <string.h>
using namespace std;
struct Student
{ char name [10];
int num;
char sex;
};int main ( )
{ Student *p;
p=new Student;
strcpy(p->name,"Wang Fun");
p->num=10123;
p->sex='M';
cout<<p->name<<" "<<p->num<<" "<<p->sex<<endl;
delete p;
return 0;
}
注意:new和delete是运算符,不是函数,因此执行效率高。malloc函数要和free函数配合使用,new和delete配合使用,不要混合使用(例如用malloc函数分配空间,用delete撤销)。
综上所述,c++对c功能的扩展包括:
(1)允许使用以“//”开头的注释。
(2)对变量的定义可以出现在程序中的任何行(但必须在引用该变量之前)。
(3)提供了标准输入输出流cin和cout,它们不用指定输入输出格式符(如%d),使输入输出更加方便。
(4)可以用const定义常变量。
(5)可以利用函数重载实现用同一函数名代表功能类似的函数,以方便使用,提高可读性。
(6)可以利用函数模板,简化同一类型的函数的编程工作。
(7)可以使用带默认值的参数的函数,使函数的调用更加灵活。
(8)提供变量的引用类型,即为变量提供一个别名,将“引用”作为函数形参,可以实现通过函数的调用来改变实参变量的值。
(9)增加了内置函数(内嵌函数),以提高程序的执行效率。
(10)增加了单目的作用域运算符::,这样在局部变量作用域内也能引用全局变量。
(11)可以用string类定义字符串变量,使得对字符串的运算更加方便。
(12)用new和delete运算符代替malloc和free函数,使分配动态空间更加方便。
c++对c的扩展还有其他一些方面。这些扩充使得人们在进行程序设计时,更加方便和得心应手。