第 14章 C++对 C的扩充
C++与 C语言的关系,C语言是 C++的一个子集,C++包含了 C
语言的全部内容。
1,C++保持与 C语言的兼容,现有的许多 C代码不经修改就可以为 C++所用。
2,C++对 C语言作了很多改进:
①增加了一些新的运算符,使得 C++应用起来更加方便。
②改进了类型系统,增加了安全性。
③引进了“引用”概念,使用引用作函数参数带来了很大方便。
④允许函数重载,允许设置缺省参数,这些措施提高了编程的灵活性,减少冗余性。又引进了内联函数的概念,提高了程序的效率。
⑤对变量说明更加灵活了。可以根据需要随时对变量进行说明。
14.1 C++概述
14.2 C++程序结构
例 14.1
//This is a C++ program.
#include <iostream.h>
void main( )
{ double x,y;
cout<<"Enter two float numbers:";
cin>>x>>y;
double z=x+y;
cout<<"x+y="<<z<<endl;
}
运行结果:
Enter two float numbers:3.4 5.5
x+y=8.9
说明:
1) C++允许的新的注释形式以 //开始,直到本行的末尾的文字都是注释。
2) iostream.h是一个头文件,定义了标准的输入和输出操作,
包括对 cin和 cout的说明。
3) cout称为标准输出流,表示标准输出设备,一般指屏幕。
cin表示标准输入设备,一般指键盘。
4) <<和 >>是重载的运算符,<<将其右边的内容输出到屏幕上。 >>将键盘中输入的一个数,送到它右边的变量中保存起来。
5) endl表示输出新行。
C++程序的源文件约定使用扩展名,cpp或,cxx,头文件约定使用扩展名,h 或,hpp或,hxx。编辑好的源程序经过 C++编译器编译成目标文件,其扩展名是,obj,再经过 C++连接器,
将目标文件与库文件中的一些代码连接起来,生成一个可执行文件。程序被运行后,一般在屏幕上显示出运行结果。
14.3 C++的 I/O流 cin和 cout
在 C++中提供了新的输入 /输出方式。其主要目标是建立一个类型安全、扩充性好的输入 /输出系统。
C++的输入 /输出流库是建立在流的概念上。流类似于文件,可以把流看成是一个无限长的字符序列,
它可以被顺序访问。从流中获取数据的操作称为提取操作。向流中添加数据的操作称为插入操作。
C++的输入 /输出流库不是语言的一部分,而是作为一个独立的函数库提供的。因此,在使用时需要包含相应的头文件,iostream.h”。输出操作被认为是插入过程,由重载的插入符,<<”来实现。输入操作被认为是提取过程,由重载的提取符,>>”来实现。
最一般的屏幕输出是将插入符作用在流类的对象
cout上。例如:
#include <iostream.h>
main()
{ int a=5,b=12;
cout<<"a="<,a<<"b="<<b<<endl;}
最一般的键盘输入是将提取符作用在流类的对象 cin
上。例如:
#include <iostream.h>
{int a,b;
cin>>a>>b;
cout<<a<<b<<endl; }
提取符可以连续写多个,每个后面跟一个表达式,
该表达式通常是获得输入值的变量或对象。
14.4函数的重载
函数重载( overloading)是指一个函数可以和同一作用域中的其他函数具有相同的名字,即同一个函数名可以对应着多个不同的函数实现。 C++中允许两个或多个函数共用同一个函数名,但这些函数各自拥有可用于区分和唯一识别它们的参数表。它们之间有的是通过参数表中某个参数的类型不同来区别,有的是通过参数个数的不同加以区别。
1、参数类型上不同的重载函数
例 14.2给函数名 add()定义多个函数实现,该函数的功能是求和。其中,一个函数实现求两个整型数之和,另一个函数实现求两个浮点数之和。每种实现对应着一个函数体,这些函数的名字相同,但是函数的参数的类型不同。这就是函数重载的概念。程序如下:
#include <iostream.h>
int add(int,int);
double add(double,double);
void main( )
{ cout<<add(3,6)<<endl;
cout<<add(4.6,9.0)<<endl; }
int add(int a,int b)
{ return a+b; }
double add(double a,double b)
{ return a+b; }
程序运行结果如下:
9
13.6
2、参数个数上不同的重载函数
例 14.3找出几个 int型数中的最大者。
#include <iostream.h>
int max(int a,int b);
int max(int a,int b,int c);
void main( )
{ cout<<max(12,6)<<endl;
cout<<max(5,9,-12)<<endl; }
int max(int a,int b)
{ return a>b?a:b; }
int max(int a,int b,int c)
{ int t;
if (a>=b) t=a;
else t=b;
if (c>t) t=c;
return t; }
函数重载要求编译器能够唯一地确定调用一个函数时应执行哪个函数代码,即采用哪个函数实现。确定函数实现时,要求从函数参数的个数和类型上来区分。也就是说,进行函数重载时,要求同名函数在参数个数上不同,或者参数类型上不同。否则,将无法实现重载。
使用函数重载主要是为了处理一组完成相同或相似功能的任务,但处理的数据个数或类型不同,这样,编程时可以不必费力的给它们起名和记忆。
如果两个函数参数个数和类型完全相同,仅仅是返回值不同,
它们不是重载的函数。程序中出现这样两个函数,编译时将出错。
函数重载可以使某些具有相似功能的函数聚集起来共同使用一个通常具有特定语义的函数名,但是当聚集起来的函数并不执行相似的操作时,就不应采用函数重载。
14.5 引用
引用也是一种特殊类型的变量,它通常被认为是另一个变量的别名。定义引用变量的格式:
类型 &引用名 =变量名 ;
引用一般要立即进行初始化。无初始化的引用是无效的。
引用与被引用的实体具有相同的地址,引用本身不能改变,所有在引用上所施加的操作,实质上就是在被引用者上的操作。
例如,int i=5,&m =i;
可以将一个引用赋给某个变量,则该变量将具有被引用的变量的值。例如,int n=m;
这时,n具有被 m引用的变量 i的值,即 10。
例 14.4:
#include <iostream.h>
void main()
{ int i=5;
int &ri=i;
cout<<"add_i="<<&i<<"add_ri="<<&ri<<endl;
cout<<"i="<<i<<"ri="<<ri<<endl;
i*=3; // 改变变量
cout<<"i="<<i<<"ri="<<ri<<endl;
ri+=5;
cout<<"i="<<i<<"ri="<<ri<<endl; }
运行结果:
add_i=63f974 add_ri=63f974
i=5 ri=5
i=15 ri=15
i=20 ri=20
从运行结果可以看出变量 i 和引用 ri的地址都是 0x63f974,即这两个名字都标识的是地址为 0x63f974的存储空间。所以不论对 i改写,还是对 ri的改写,都是操作在同一个程序实体上,即变量 i和引用 ri所指称的值都是相同的。
例 14.5:
#include <iostream.h>
void main()
{ int i=5;
int *pi=&i;
int *&rpi=pi; //对指针的引用
cout<<"add_pi="<<pi<<"add_rpi="<<rpi<<endl;
cout<<"i="<<i<<endl;
*pi=10;
cout<<"i="<<i<<endl;
*rpi=20;
cout<<"i="<<i<<endl; }
程序运行结果:
add_pi=63f970 add_rpi=63f970
i=5
i=10
i=20
由程序的运行结果可见,指针变量 pi和引用 rpi
的地址是相同的( 0x63f970),也就是说,用于存储指针的地址 0x63f970具有两个字句 pi和 rpi。
不论是对变量 i操作,还是对 pi所指对象的操作以及对 rpi所指对象的操作,都是对同一个程序实体的操作。
C++使用引用的主要地方是建立函数变参。将引用作为函数的形参时,改变形参的值会影响实参的值。 C++使用引用参数另一个目的是为了效率。如果函数的参数是类或结构类型,
参数传递时将拷贝整个形参。使用引用可以只传递指向形参的指针。引用调用是 C++中的一种函数调用方式,C语言中没有这种调用方式。
例 14.6:函数参数是引用类型
#include <iostream.h>
void swap(int &m,int &n) //引用作函数的参数
{ int temp=m;
m=n;
n=temp; }
void main()
{ int a=2,b=4;
swap(a,b);
cout<<"a="<<a<<endl;
cout<<"b="<<b<<endl; }
运算结果:
a=4
b=2
使用引用作函数形参时,调用函数的实参要用变量名,将实参变量名赋给形参的引用,相当于在被调用函数中使用了实参的别名。于是在被调用函数中,对引用的改变,实质上就是直接地通过引用来改变实参的变量值。而且这种调用起到传址调用的作用,但它又比传址调用更方便、更直接。因此,在 C++中常常使用引用作函数形参来实现在被调用函数中改变调用函数的实参值。
使用引用作形参但不改变实参值,可以如下定义函数原型:
int myFunc(const int & param);
不能对 param赋值,否则编译出错。
引用可以作为函数的返回值。返回引用的函数可以作为左值,即它可以出现在赋值运算符的左边。
例 14.7:
#include <iostream.h>
int a[ ]={1,3,5,7,9};
int & elem(int i) //函数返回引用类型
{ return a[i];}
void main()
{ int j;
for(j=1;j<5;j++)
elem(0)+=elem(j); //返回引用的函数的函数调用可以出现在赋值运算符左边。
cout<<"elem(0)="<<a[0]<<endl; }
在实现返回引用的函数时,注意不要返回对该函数内的自动变量的引用,因为以后使用引用时,它所指向的变量已不存在。
14.6内联函数
引入内联函数的目的是为了解决程序中函数调用的效率问题。程序执行过程中,每调用一次函数,就要在调用与返回过程中付出一定的时间与空间代价用于处理现场。
当函数较小又反复使用时,处理现场的开销比重会急剧增大。若把函数体嵌入函数调用处,便可以大大提高运行速度,节省开销。内联函数就可以自动实现这一功能。
使用 C++中新的关键字 inline说明的函数称为内联函数。
编译器在遇到对内联函数调用时,将尽可能的用内联函数的函数体替换函数调用的表达式。因此会增加目标程序代码量,它是以目标代码的增加为代价来换取时间的节省。使用内联函数可以加快程序执行的速度。
例 14.8:编程求 1~10中各个数的平方。
#include <iostream.h>
inline int power_int(int x)
{ return (x)*(x); }
void main()
{ int i;
for(i=1;i<=10;i++)
{ int p=power_int(i);
cout<<i<<"*"<<i<<"="<<p<<endl; } }
在使用内联函数时,应注意如下几点:
1、在内联函数内不允许用循环语句和开关语句。
2、内联函数的定义必须出现在内联函数第一次被调用之前。
3、后面讲到的类结构中所有在类说明内部定义的函数都是内联函数。
14.7函数参数的缺省值
正确的缺省参数说明:
void fun1(int x,int y=0,int z=0);
void fun2(int x,int y=0);
C++允许定义或说明函数时为一个或多个形参指定缺省值。
缺省参数的说明必须在形参表的最右边开始,并且中间没有间隔的非缺省参数说明。缺省参数只能定义一次,如果在函数原型中已经指定了缺省参数,那么在函数定义时不能再次说明。
如果在函数调用时指定了形参对应的实参,则形参使用实参的值;如果未指定相应的实参,则形参使用缺省值。例如有如下的函数调用表达式:
fun1(10);
它与下列调用表达式是等价的:
fun1(10,0,0);
例 14.9:
#include <iostream.h>
void fun(int a=1,int b=3,int c=5)
{ cout<<"a="<<a<<","<<"b="<<b<<","<<"c="<<c<<endl; }
void main( )
{ fun( );
fun(7);
fun(7,9);
fun(7,9,11);
cout<<"OK!"; }
执行该程序,输出如下结果:
a=1,b=3,c=5
a=7,b=3,c=5
a=7,b=9,c=5
a=7,b=9,c=11
OK!
该程序中在函数的定义时设置了参数的缺省值,而在调用该函数时,有的无实参,有的实参数目不足,有的实参数目与形参相等,分若干不同情况来说明缺省值的使用。
例 14.10:分析下列程序的输出结果:
#include <iostream.h>
int m(8); /*等价于,int m=8; */
int add_int(int x,int y=7,int z=m);
void main( )
{ int a(5),b(15),c(20);
int s=add_int(a,b);
cout<<s<<endl; }
int add_int(int x,int y,int z)
{ return x+y+z; }
该程序中,在说明函数 add_int()时,给函数参数设置了缺省值,而其中一个参数的值被设置为一个已知变量( m)的值。
请读者自己分析该程序的输出结果。
14,8作用域运算符
,:是 C++定义的一个新的运算符,称为作用域运算符。使用作用域运算符可以访问当前作用域外部的标识符。当::作为单目运算符时,它的右操作数是一个标识符,它限定访问全局作用域范围内的该标识符。当::是双目运算符时,它的左操作数是类名,右操作数是类的成员。它限定访问指定类的某个成员。::
运算符最有用的地方是在派生类中访问基类的成员。尤其是当派生类定义的成员名字与基类中成员名字相同时(即派生类的成员名字覆盖基类的成员名字时)。
例 14.11:
int a,b; //全局作用域内定义的变量 a,b
int myClass::myFunc(int a)
{ myClass::a=b; //当名字存在二义性时,使用::限定访问的类的成员
,:a=b; } //访问的是全局作用域内定义的变量 a
注意:在成员函数内访问全局变量是不好的程序设计风格,应尽量避免。
14,9 const修饰符定义常量
使用类型修饰符 const说明的类型称为常类型,常类型变量的值是不能被更新的。因此,定义或说明常类型变量时必须进行初始化。
任何一种改变常类型变量的值的操作都将导致编译错误。例如:
int const m=15;
m=18;
这种赋值操作是错误的。因为前面定义了 m是一个常量,并且给它初始化了,即 m值为 15,因此,不能再改变 m的值了。
一个没有初始化的常类型变量定义也会导致编译错误。例如:
const double PI; //error ; uninitialized const
试图把一个常类型变量的地址赋给一个指针同样会使编译器生成编译错误。否则,常类型变量的值将通过指针间接地修改。例如:
const int a=4;
int *p=&a; // error C2440,'initializing',cannot convert from 'const
int *' to 'int *'
*p+=5;
1、一般常量
一般常量是指简单类型的常量。这种常量在定义时,修饰符
const可以用在类型说明符前,也可以用在类型说明符后。例如:
int const x=2; 与 const int x=2; 是一样的。
int const a[5]={1,2,3,4,5};
说明数组 a的各个元素是 int型常量,即数组元素的值是不能被更新的。
2、常指针
使用 const修饰指针时,由于 const的位置不同,而含意不同。
下面定义的是一个指向字符串的常量指针:
char *const ptr1=stringptr1;
其中,ptr1是一个常量指针。因此,下面赋值是非法的,
ptr1=stringptr2;
而下面赋值是合法的,
*ptr1='m';
因为指针 ptr1所指向的变量是可以更新的。
下面定义了一个指向字符串常量的指针:
const char *ptr2=stringptr1;
其中,ptr2是一个指向字符串常量的指针。 ptr2所指向的字符串是不能更新的,而 ptr2是可以更新的。
因此,
*ptr2='x';
是非法的。而
ptr2=stringstr2;
是合法的。
所以,在使用 const修饰指针时,应该注意 const的位置。定义一个指向字符串的指针常量和定义一个指向字符串常量的指针时,const修饰符的位置不同,
前者 const放在 *和指针名之间,后者 const放在类型说明符前。
例 14.12常指针作函数参数的例子。
#include <iostream.h>
const int N=6;
void print(const int *p,int n);
void main()
{ int array[N];
for(int i=0;i<N;i++)
cin>>array[i];
print(array,N); }
void print(const int *p,int n)
{ cout<<"{"<<*p;
for(int i=1;i<n;i++)
cout<<","<<*(p+i);
cout<<"}"<<endl; }
执行该程序输入如下信息:
1 2 3 4 5 6 ↙
输出结果如下:
{1,2,3,4,5,6}
说明:该程序中两处出现 const修饰符,一是使用 const定义一个
int型常量 N;二是使用 const定义一个指向常量数组的指针。该指针所指向的数组元素是不能被更新的。
该程序中有一个问题,print()函数中,实参 array是一个 int型数组名,形参是 const int的指针,显然类型不相同,但却没有出现类型错误。这是因为形参虽然是指向一个非 const int型数组
array,该数组是可以更新的,但在 print()函数中不能被更新。
因此,一个能够更新的变量使用在一个不能被更新的环境中是不破坏类型保护,所以不出现类型不匹配错。
3、使用 const也可以说明引用,被说明的引用为常引用,该引用所引用的对象不能被更新。
4、使用 const关键字进行说明的成员函数,称为常成员函数。
只有常成员函数才有资格操作常量或常对象,没有使用 const关键字说明的成员函数不能用来操作常对象。
14.10动态内存分配和撤消运算符 new和 delete
在 C++中,定义了两个新的运算符,new和 delete,专门进行动态内存申请和释放,而且 new能自动调用构造函数创建相应的类对象,delete能自动调用析构函数删除类对象。
new和 delete应匹配使用,如果 delete运算符作用到不是用
new返回的指针,可能引起程序运行错误。
动态申请保存一个 type类型的数据的内存:
p=new type;
p是指向类型 type的指针,type是数据类型名。
释放以前用 new申请的保存一个 type类型数据的内存:
delete p;
等号左边的类型必须与右边申请的类型一致,否则,编译出错。
new 返回分配的内存地址,应该将它保存在一个变量中,
以后用 delete释放。
例 14.13:
#include <iostream.h>
void main()
{ int *p;
p=new int;
*p=888;
cout<<"add_p="<<&p<<endl;
cout<<"add_m="<<p<<endl;
cout<<"value_*p="<<*p<<endl;
delete p; }
运行结果如下,
add_p=0x0065fdf4
add_m=0x00780da0
value_*p=888
new能自动计算它要分配的存储空间的大小,可以为更复杂的数据实体(如数组)分配空间,如语句:
int *pi=new int[8];
动态分配存放 8个整数的内存空间。释放 pi指向的数组存储区时,应使用下面的格式:
delete [] pi;
new可以在为简单变量动态分配内存空间的同时,
进行初始化。例如:
int *p=new int(10);
在动态分配内存的同时,将这个动态存储区中的值初始化为 10。但是,不能用 new为动态分配的数组存储区进行初始化。