第 5章 程序结构第 5章 程序结构
5.1 作用域与可见性
5.2 生存期
5.3 局部变量和全局变量
5.4 静态成员
5.5 友元
5.6 常类型
5.7 多文件结构
5.8 编译预处理第 5章 程序结构
5.1 作用域与可见性
5.1.1 作用域作用域是一个标识符在程序正文中有效的区域 。
C++的作用域有函数原型作用域,块作用域 ( 亦称局部作用域 ) 和文件作用域 。
1,函数原型作用域函数原型作用域是 C++程序中最小的作用域。前面介绍过,在函数原型的声明中一定要包含参数的类型声明。
第 5章 程序结构这里参数的声明就属于函数原型作用域,它开始于函数原型声明的左括号,(,,结束于右括号,),。
例如,有如下函数声明:
double Area(double length,double width);
由于形参 length和 width只在括号之内有效,在程序的其它地方无法引用这个标识符,如果要引用,必须重新定义 。 例如:
double Area(double length,double width); //函数声明
length=10;
width=5;
第 5章 程序结构这段代码会引起无定义的标识符编译错误 。 所以,
对于这种情况,标识符 length和 width实际上是可有可无的,省去它,也决不会影响到程序的编译和运行结果 。
例如:
double Area(double,double);
但是,考虑到程序的可读性,还是要在函数原型声明时,给形参指定一个有意义的标识符,而且一般总是与该函数定义时声明的参数标识符一致。
第 5章 程序结构例如:
double Area(double length,double width); //函数声明
//...
double Area(double length,double width) //函数定义
{
//...
}
第 5章 程序结构
2,块作用域块作用域又称局部作用域 。 当标识符的声明出现在由一对花括号,{}”所括起来的程序 ( 块 ) 内时,则此块中声明的标识符的作用域从声明处开始,一直到块结束的花括号为止 。 为了理解块作用域,我们来看一个例子:
#include<iostream.h>
void main( )
{
int n;
第 5章 程序结构
for(int i=0;i<5;i++)
{
int m;
if(i%2) m作用域 i作用域 n作用域
n++;
}
m=n/2; //错误,m未定义
n=i;
}
第 5章 程序结构在这个例子中,函数体 main( )是一个块,for语句之后的循环体又是一个较小的块 。 变量 n和 i的作用域从声明处开始,到它所在的块,即整个函数体 main( )
结束处为止 。 变量 m的作用域从声明处开始到它所在的块,即循环体结束为止 。 因此,编译时 m=n/2会出错,
因为它试图在 m作用域之外引用 m。 而 n=i是允许的,
因为此时还在 i的作用域内 。 如果在上面的程序中加入一个函数 fun1( ),情况会怎么样呢?
第 5章 程序结构
#include<iostream.h>
void main( )
{
void fun1( ); //fun1( )函数声明
int n;
for(int i=0;i<5;i++)
{
int m;
if(i%2)
n++;
第 5章 程序结构
}
m=n/2; //错误,m未定义
fun1( ); //fun1( )函数调用
n=i;
}
void fun1( )
{
cout<<"i="<<i<<endl; //错误,i未定义
}
第 5章 程序结构
3,文件作用域具有文件作用域的标识符是在所有函数定义之外声明的,其作用域从声明点开始,一直延伸至文件尾 。
一般情况下,程序中所声明的全局变量都具有文件作用域,它们在整个文件中都有效 。
【 例 5-1】 文件作用域例题 。
#include<iostream.h>
int k;
void main( )
{
k=5;
第 5章 程序结构
{
k++;
}
cout<<"k="<<k<<endl;
}
程序运行结果为
k=6
这个例子中,在主函数之前声明的变量 k具有文件作用域,它的有效作用范围是整个源代码文件 。
第 5章 程序结构
5.1.2 可见性作用域指的是标识符有效的范围,可见性从另一个角度表现标识符的有效范围 。 标识符的可见性是指在程序的某个位置,该标识符可以被有效地引用,因此,
形象地称为可见性 。 可见性遵循的一般规则如下:
① 标识符在引用前必须先声明 。
② 在互相没有包含关系的不同作用域中声明同名的标识符时,两标识符互不影响 。
③ 如果在两个或多个具有包含关系的作用域中声明了同名标识符,则外层标识符在内层不可见 。
第 5章 程序结构
【 例 5-2】 具有包含关系的作用域中可见性例题 。
#include<iostream.h>
int k;
void main( )
{
k=10;
{
int k=5;
cout<<"k="<<k<<endl;
第 5章 程序结构
}
cout<<"k="<<k<<endl;
}
程序运行结果为
k=5
k=10
在这个例子中,主函数之前声明的变量 k具有文件作用域,它的有效作用范围是整个源代码文件;主函数内声明的变量 k具有块作用域,它的作用范围在内层的花括号内,k的块作用域被完全包含在 k的文件作用域中 。
第 5章 程序结构图 5-1描述了 k的两个作用域的包含关系。根据作用域可见性的规则,在具有包含关系的作用域中声明同名标识符,外层标识符在内层不可见。程序的运行结果验证了这一点。
第 5章 程序结构图 5-1 k的作用域示意图图5 - 1 k 的作用域示意图
k 的文件作用域
k 的块作用域第 5章 程序结构
5.2 生存期
5.2.1 静态生存期此生存期与程序的运行期相同。静态生存期的变量只要程序一开始运行,它就存在,直到程序运行结束,此变量的生存期也就结束了。具有文件作用域的变量具有静态生存期。如果要在函数内部的块作用域中声明具有静态生存期的变量,则要使用关键字 static。
例如,下列语句声明的变量 k便是具有静态生存期的变量,也称为静态变量。
第 5章 程序结构
static int k;
具有静态生存期的变量在固定的数据区域内分配空间 。 如果具有静态生存期的变量未初始化,则自动初始化为 0。 全局变量,静态全局变量和静态局部变量都具有静态生存期 。
第 5章 程序结构
5.2.2 局部生存期在块作用域中声明的变量具有局部生存期 。 此生存期诞生于声明点,而终止于其作用域的结束处 。 因此,具有局部生存期的变量都具有块作用域 。 但反之则不然,一般具有块作用域的变量都具有局部生存期,
但当在块作用域内将变量说明为静态变量时,该变量则具有静态生存期 。 例如:
void main( )
{
static int k;
//...
}
第 5章 程序结构
5.2.3 动态生存期动态生存期由程序中特定的函数 (malloc( )和 free( ))
调用或由操作符 ( new和 delete) 创建和释放,这部分内容将在第 6章中介绍 。 具有动态生存期的变量在内存的堆区分配空间 。
第 5章 程序结构
5.3 局部变量和全局变量
5.3.1 局部变量局部变量具有局部作用域 。 因此,在不同函数体内的局部变量是互相不可见的,这就很好地实现了函数之间的数据隐蔽 。 这也是结构化程序设计中实现数据隐蔽的唯一办法 。 局部变量包括自动 ( auto) 变量,内部静态 ( static) 变量和函数参数 。 自动变量是在函数体或分程序内声明的变量,具有块作用域 。 声明时,变量前可以加 auto,也可以不加,程序中没有特别说明的变量都是自动变量 。
第 5章 程序结构自动变量以堆栈方式占用内存空间。因此,当程序运行到此类变量声明处时,会立刻为它分配内存空间,而一旦其生存期结束,系统立即收回这个堆栈,
此变量也就立即消失。内部静态变量在前面已介绍过,
它具有文件作用域和静态生存期,系统在固定的内存区 —— 数据区为它分配空间。函数参数实质上就是自动变量。
第 5章 程序结构局部变量能够在调用和被调用函数之间通过参数进行数据传递。如果把数据存储在局部变量中,函数在不同的块之间只能通过参数传递来共享数据。我们在前面学习函数的时候曾经深入讨论过这个问题。这种方法比较适合共享少量数据的情况,而且这种共享只能在主调函数与被调函数之间进行。
第 5章 程序结构
5.3.2 全局变量全局变量具有文件作用域 。 在整个程序中,除了在定义有同名局部变量的块中之外,其它地方都可以直接访问全局变量 。 将数据存放在全局变量中,不同的函数在不同的地方对同一个全局变量进行访问,实现了这些函数之间的数据共享 。 请看下面的程序:
#include<iostream.h>
int n;
void f( )
{ n=5; }
第 5章 程序结构
void g( )
{ cout<<"n="<<n<<endl; }
void main( )
{
f( );
g( );
}
程序运行结果为
n=5
这样的共享方法,使用相当方便,但是副作用也不可低估。
第 5章 程序结构
【 例 5-3】 局部变量和全局变量例题 。
#include<iostream.h>
int i=1;
void main( )
{
static int a;
int b= -10;
int c=0;
void other(void);
cout<<"----main----"<<endl;
第 5章 程序结构
cout<<"i="<<i<<" a="<<a<<" b="<<b<<" c="<<c<<endl;
c=c+8;
other( );
cout<<"----main----"<<endl;
cout<<"i="<<i<<" a="<<a<<" b="<<b<<" c="<<c<<endl;
other( );
}
void other(void)
{
static int a=1;
第 5章 程序结构
static int b;
int c=5;
i=i+2;a=a+3;c=c+5;
cout<<"----other----"<<endl;
cout<<"i="<<i<<" a="<<a<<" b="<<b<<" c="<<c<<endl;
b=a;
}
第 5章 程序结构程序运行结果为
----main----
i=1 a=0 b= -10 c=0
----other----
i=3 a=4 b=0 c=10
----main----
i=3 a=0 b= -10 c=8
----other----
i=5 a=7 b=4 c=10
第 5章 程序结构
【 例 5-4】 局部对象和全局对象例题 。
#include<iostream.h>
class Clock
//时钟类声明
{
private:
int Hour,Minute,Second;
public:
Clock( )
//构造函数
{
第 5章 程序结构
Hour=0;
Minute=0;
Second=0;
}
void SetTime(int NewH,int NewM,int NewS);
//设置时间函数
void ShowTime( );
~Clock( ){}
};
void Clock::SetTime(int NewH,int NewM,int NewS)
{
第 5章 程序结构
Hour=NewH;
Minute=NewM;
Second=NewS;
}
void Clock::ShowTime( )
{
cout<<Hour<<":"<<Minute<<":"<<Second<<endl;
}
Clock globClock; //声明具有静态生存期,文件作用域的对象 globClock
void main( )
{
第 5章 程序结构
cout<<"First time output:"<<endl;
globClock.ShowTime( );
globClock.SetTime(10,30,45);
Clock myclock(globClock); //声明具有块作用域的对象 myclock
cout<<"Second time output:"<<endl;
myclock.ShowTime( );
}
第 5章 程序结构程序运行结果为
First time output:
0,0,0
Second time output:
10,30,45
程序中包含了具有各种作用域的变量和对象。其时钟类声明中,函数成员 SetTime的三个形参具有函数原型作用域,对象 globClock具有文件作用域,对象
myclock具有块作用域。在主函数中,这些变量、对象及其公有成员都是可见的。
第 5章 程序结构
5.4 静态成员静态成员用于解决同一个类的不同对象之间的数据和函数的共享问题 。 例如,我们可以抽象出学生的共性,设计如下的学生类:
class Student
{
private:
int StudentNo;
int ID;
第 5章 程序结构
char *name; //字符指针,将在第 6章介绍
//…
};
如果需要统计学生总数,这个数据存放在什么地方呢?若以类外的变量来存储总数,不能实现数据的隐藏。若在类中增加一个数据成员用以存放总数,必然在每一个对象中都存储一个副本,不仅冗余,而且每个对象分别维护一个“总数”,势必造成数据的不一致。因此,比较理想的方案是使类的所有对象共同拥有一个用于存放总数的数据成员,这就是下面要介绍的静态数据成员。
第 5章 程序结构
5.4.1 静态数据成员类的普通数据成员在类的每一个对象中都拥有一个拷贝,也就是说,每个对象的同名数据成员可以分别存储不同的数值,这也是每个对象拥有自身特征的保证。
而静态数据成员是类的数据成员的一种特例。
每个类只有一个静态数据成员拷贝,它由该类的所有对象共同维护和使用,从而实现了同一个类的不同对象之间的数据共享 。 静态数据成员具有静态生存期 。 在静态数据成员的声明和使用时,应注意以下几点:
第 5章 程序结构
① 静态数据成员声明时,应在前面加 static关键字来说明 。 例如,static int n
② 静态数据成员必须初始化,并且一定要在类外进行 。 其初始化的形式如下:
<类型标识符 > <类名 >::<静态数据成员名 >=<值 >
例如,int point::n=0;
③ 静态数据成员属于类,而不属于任何一个对象,
所以,在类外只能通过类名对它进行引用 。 静态数据成员引用的一般形式如下:
<类名 >::<静态数据成员名 >;
第 5章 程序结构静态数据成员同一般数据成员一样要服从访问控制限制 。 当静态数据成员被声明为私有成员时,只能在类内直接引用它,在类外无法引用 。 但当静态数据成员被声明为公有成员或保护成员时,可以在类外通过类名对它进行引用 。 下面,我们通过一个例题来说明静态数据成员的使用 。
第 5章 程序结构
【 例 5-5】 含有静态数据成员例题 。
#include<iostream.h>
class Test
{
private:
int k;
public:
static int n; //静态数据成员
Test(int kk){k=kk;n++;}
void Display( )
第 5章 程序结构
{cout<<"n="<<n<<"k="<<k<<endl;}
};
int Test::n=0; //静态数据成员初始化
void main( )
{
Test t1(10);
t1.Display( );
Test t2(20);
t2.Display( );
Test::n++;
t2.Display( );
}
第 5章 程序结构上面的例题中,类 Test中声明 n为公有静态数据成员,
用来给 Test类的对象计数,每声明一个新对象,n的值就相应加 l。 静态数据成员 n的定义和初始化在类外进行 。
n的值在类的构造函数中被引用 。 t1对象生成时,调用构造函数 Test( ),n值由 0变为 1;当 t2对象生成时,又调用构造函数 Test( ),这次,n值由 1变为 2。 两次调用均访问的是 t1和 t2共同维护的该静态成员的拷贝,这样实现了在 t1和 t2两个对象间数据共享 。 另外,在主函数中,
通过类名对它进行引用,n值又一次被加 1。
第 5章 程序结构例 5-5的程序运行结果为
n=1 k=10
n=2 k=20
n=3 k=20
如果我们保持程序的其它部分不变,只将主函数中前四条语句改为
Test t1(10),Test t2(20);
t1.Display( );
t2.Display( );
读者可以想一想,程序运行结果又会怎样 。
第 5章 程序结构
5.4.2 静态函数成员所谓静态函数成员,就是使用 static关键字声明的函数成员 。 同静态数据成员一样,静态函数成员也属于整个类,由同一个类的所有对象共同维护,为这些对象所共享 。 静态函数成员可以直接引用该类的静态数据和函数成员,而不能直接引用非静态数据成员,
如果要引用,必须通过参数传递的方式得到对象名,
然后再通过对象名来引用 。
作为成员函数,静态函数成员的访问属性可以受到类的严格控制。对于公有的静态函数成员,可以通过类名或对象名来调用;而对于一般的非静态函数成员,只能通过对象名来调用。
第 5章 程序结构
【 例 5-6】 使用静态函数成员例题 。
#include<iostream.h>
class point
{
private:
int x,y;
static int countP; //静态数据成员
public:
point(int xx=0,int yy=0) //构造函数
{x=xx;y=yy;countP++;}
第 5章 程序结构
point(point &p); //拷贝构造函数
int get_x( ) {return x;}
int get_y( ) {return y;}
static void get_c( ) //静态函数成员
{cout<<"Object id="<<countP<<endl;}
};
point::point(point &p)
{
x=p.x;
y=p.y;
countP++;
第 5章 程序结构
}
int point::countP=0; //静态数据成员初始化
void main( )
{
point::get_c( ); //利用类名引用静态函数成员
point a(4,5);
cout<<"point a,"<<a.get_x( )<<","<<a.get_y( );
a.get_c( ); //利用对象名引用静态函数成员
point b(a);
第 5章 程序结构
cout<<"point b,"<<b.get_x( )<<","<<b.get_y( );
point::get_c( ); //利用类名引用静态函数成员
}
在这个例题中,类 point的数据成员 countP被声明为静态,用来给 point类的对象计数,每声明一个新对象,
countP的值就相应加 l。静态数据成员 countP的定义和初始化在类外进行,countP的值是在类的构造函数中引用的。
第 5章 程序结构例 5-6的程序运行结果为
Object id=0
point a,4,5 Object id=1
point b,4,5 Object id=2
如果上述程序中,采用非静态函数成员 get_c( ),
程序其余的部分保持不变,程序代码如下:
#include<iostream.h>
class point
{
private:
int x,y;
第 5章 程序结构
static int countP; //静态数据成员
public:
point(int xx=0,int yy=0) //构造函数
{x=xx;y=yy;countP++;}
point(point &p); //拷贝构造函数
int get_x( ) {return x;}
int get_y( ) {return y;}
void get_c( ) //非静态函数成员
{cout<<" Object id="<<countP<<endl;}
第 5章 程序结构
};
point::point(point &p)
{
x=p.x;
y=p.y;
countP++;
}
int point::countP=0; //静态数据成员初始化
void main( )
{
第 5章 程序结构
point a(4,5);
cout<<"point a,"<<a.get_x( )<<","<<a.get_y( );
a.get_c( ); //利用对象名引用非静态函数成员
point b(a);
cout<<"point b,"<<b.get_x( )<<","<<b.get_y( );
b.get_c( ); //利用对象名引用非静态函数成员
}
仔细比较这两个例子,在类的声明中,只是把原来的静态函数成员 get_c( )改写为普通函数成员,其余的部分保持不变。
第 5章 程序结构这时要输出静态数据成员 countP,只能通过 point类的某个对象来调用函数 get_c( )。 上述程序的运行输出结果与例 5-6的完全相同 。 相比而言,例 5-6采用静态函数成员的好处是可以不依赖于任何对象,直接引用静态数据 。
如果此时采用类名调用 get_c( )会怎么样呢? 现在尝试将上述主函数改写如下:
void main( )
{
point a(4,5);
cout<<"point a,"<<a.get_x( )<<","<<a.get_y( );
第 5章 程序结构
a.get_c( ); //利用对象名引用静态函数成员
point b(a);
cout<<"point b,"<<b.get_x( )<<","<<b.get_y( );
point::get_c( );//出错,试图利用类名引用静态函数成员
}
结果编译时出错 。 由此可见,对普通函数成员的调用不能用类名,必须用对象名 。 前面这段程序说明,
通过非静态函数成员同样可以输出静态数据成员,但在有些情况下是不行的 。
第 5章 程序结构例如,在所有对象声明之前,countP的初始值已经为 0,这时想要输出这个初始值,只能通过静态函数成员 。 现在请看下面的程序段:
#include<iostream.h>
class A
{
private:
int x;
public:
static void f(A a);
};
void A::f(A a)
第 5章 程序结构
{
cout<<x; //对 x的引用是错误的
cout<<a.x; //正确
}
可以看到,通过静态函数成员引用非静态成员是相当麻烦的 。 一般情况下,静态函数成员主要用来引用全局变量或同一个类中的静态数据成员,特别是和后者一起使用时,可以达到对同一个类中对象之间的共享数据进行维护的目的 。
第 5章 程序结构
5.5 友元先来看一段程序 。
#include<iostream.h>
class A
{
private:
int x;
public:
第 5章 程序结构
void display( ) {cout<<x<<endl;}
int get( ) {return x;}
};
class B
{
private:
A a;
public:
void set(int);
void display( );
};
第 5章 程序结构这是类组合的情况,B类中内嵌了 A类的对象,但是 B的成员函数却无法直接访问 A的私有成员 x。 从数据的安全角度来说,这无疑是最安全的,内嵌的部件相当于一个黑盒 。 但是使用起来多少有些不便 。 例如,
按如下形式实现 B的成员函数 set( )会引起编译错误:
void B::set(int k) {a.x=k;}
第 5章 程序结构由于 A的对象内嵌于 B中,能否让 B的函数直接访问 A的私有数据呢? 这就需要寻求一种使得类以外的对象或函数能够访问类中的私有成员的方法 。 C++为上述这种需求提供了语法支持,这就是友元 。
友元提供了不同类或对象的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。也就是说,通过友元的方式,一个普通函数或者类的成员函数可以访问到封装于某一个类中的数据。这相当于给类的封装挖了一个小孔,通过它,外界可以看到类内部的一些属性。
第 5章 程序结构
5.5.1 友元函数如果友元是普通函数或类的成员函数,则称为友元函数 。 友元函数是在类声明中由关键字 friend修饰的非成员函数 。
普通函数声明为友元函数的形式如下:
friend <类型标识符 > <友元函数名 >(参数表 )
成员函数声明为友元函数的形式如下:
friend <类型标识符 > <类名 >::<友元函数名 >(参数表 )
第 5章 程序结构说明:
① 友元函数的声明可以在类声明中的任何位置,
既可在 public区,也可在 protected区,意义完全一样 。
② 友元函数的定义一般放在类的外部,最好与类的其它成员函数定义放在一起 。 如果是普通函数作为友元,也可以放在类中 。
友元函数不是本类的成员函数,但是它可以通过对象名访问类的所有成员,包括私有和保护成员,我们先来看一个例子 。
第 5章 程序结构
【 例 5-7】 普通函数作友元函数例题 。
这是使用普通函数作友元函数计算两点距离的例题 。 这里声明了一个 point类,两点的距离用普通函数
distance来计算 。 这个函数需要访问 point类的私有数据成员 x和 y,为此,将 distance声明为 point类的友元函数 。
程序代码如下:
#include<iostream.h>
#include<math.h>
class point
{
第 5章 程序结构
private:
double x,y;
public:
point(double xx=0,double yy=0) {x=xx;y=yy;}
double get_x( ) {return x;}
double get_y( ) {return y;}
friend double distance(point &p1,point &p2);
//普通函数作 point的友元
};
double distance(point &p1,point &p2)
第 5章 程序结构
{
return (sqrt((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-
p2.y)));
}
void main( )
{
point myp1(1,1),myp2(4,5);
cout<<"The distance is:"<<distance(myp1,myp2)<<endl;
}
第 5章 程序结构上述程序中,point类在声明友元函数时,只给出友元函数原型,友元函数 distance的定义是在类外进行的 。 在友元函数中通过对象名直接引用 point类中的私有数据成员 x和 y。
程序运行结果为
The distance is,5
本例中的友元函数是一个普通函数 。 其实这个函数也可以是另外一个类的成员函数 。 有时候把成员函数作友元的情况称为友元成员 。 这种友元成员函数的使用和一般友元函数的使用基本相同,只是在使用该友元成员时要通过相应类的对象名来访问 。
第 5章 程序结构
【 例 5-8】 成员函数作友元函数例题 。
现在使用成员函数作友元函数计算两点距离 。 我们只要将例 5-7中相应的地方改为成员函数作友元就可以了 。 程序代码如下:
#include<iostream.h>
#include<math.h>
class point; //前向声明 point类
class A
{
public:
double distance(point &p1,point &p2);
第 5章 程序结构
//...
};
class point
{
private:
double x,y;
public:
point(double xx=0,double yy=0) {x=xx;y=yy;}
double get_x( ) {return x;}
double get_y( ) {return y;}
第 5章 程序结构
friend double A::distance(point &p1,point &p2); //A 类的成员函数作 point的友元
};
double A::distance(point &p1,point &p2)
{
return (sqrt((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-
p2.y)));
}
void main( )
{
point myp1(1,1),myp2(4,5);
第 5章 程序结构
A obj; //必须声明一个 A类的对象
cout<<"Thedistance
is:"<<obj.distance(myp1,myp2)<<endl;
}
上述程序中,distance( )为 A类的成员函数,因此,
在声明友元函数时,必须在 distance( )前加,A::”,以便确认 distance( )属于哪个类 。 另外,在主函数中一定要声明一个 A类的对象 。 只有这样,才能通过对象名调用友元函数 。 这时,程序的运行结果与例 5-7的完全相同 。
第 5章 程序结构
5.5.2 友元类如果友元是一个类,则称为友元类 。 友元类的声明形式如下:
friend class <友元类名 >
说明:
① 友元类的声明同样可以在类声明中的任何位置 。
② 友元类的所有成员函数都成为友元函数 。
例如,若 A类为 B类的友元类,则 A类的所有成员函数都是 B类的友元函数,都可以访问 B类的私有和保护成员 。
第 5章 程序结构现在我们用友元类的方法重做例 5-8。 程序代码如下:
#include<iostream.h>
#include<math.h>
class point; //前向声明 point类
class A
{
public:
double distance(point &p1,point &p2);
//...
};
class point
{
第 5章 程序结构
private:
doublex,y;
public:
point(double xx=0,double yy=0) {x=xx;y=yy;}
doubleget_x( ) {return x;}
doubleget_y( ) {return y;}
friend class A; //A类为友元类
};
doubleA::distance(point &p1,point &p2)
{
return (sqrt((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y)));
第 5章 程序结构
}
void main( )
{
point myp1(1,1),myp2(4,5);
A obj; //必须声明一个 A类的对象
cout<<"The distance
is:"<<obj.distance(myp1,myp2)<<endl;
}
从上面的程序可以看到,这里只需将例 5-8中的
friend double A::distance(point &p1,point &p2)语句改为
friend double A,程序其它部分没有作任何变动 。 此时,
程序运行结果与例 5-7和例 5-8的都完全相同 。
第 5章 程序结构通过友元类声明,友元类的成员函数可以通过对象名直接访问到隐藏的数据,达到高效协调工作的目的 。
在较为复杂的程序中,实现不同类之间的数据共享,友元类的使用是很必要的 。 现在将本节开头部分的程序段修改成如下形式,以便实现 B类的成员函数直接访问 A类私有成员 x的目的 。
#include<iostream.h>
class A
{
private:
第 5章 程序结构
int x;
public:
void display( ) {cout<<x<<endl;}
int get( ) {return x;}
friend class B;
};
class B
{
private:
A a;
public:
第 5章 程序结构
void set(int);
void display( );
};
void B::set(int k)
{
a.x=k;
}
第 5章 程序结构关于友元,还有两点需要注意:
① 友元关系是不能传递的 。 B类是 A类的友元,C
类是 B类的友元,C类和 A类之间如果没有声明,就没有任何友元关系,不能进行数据共享 。
② 友无关系是单向的 。 如果声明 B类是 A类的友元,
B类的成员函数就可以访问 A类的私有和保护数据,但
A类的成员函数却不能访问 B类的私有和保护数据 。
第 5章 程序结构
5.6 常类 型
5.6.1 常引用使用 const关键字声明的引用称为常引用 。 常引用所引用的对象不能被更新 。 常引用的声明形式如下:
const <类型标识符 > &<引用名 >
现在来看下面的例题 。
第 5章 程序结构
【 例 5-9】 常引用例题 。
#include<iostream.h>
void main( )
{
int n=5;
const int &m; //声明 m为常引用
m=10;
cout<<"m="<<m<<endl;
}
第 5章 程序结构这段程序编译时有两个错误:
error C2530,'m',references must be initialized
error C2166,l-value specifies const object
将上述程序稍作修改,程序代码如下:
#include<iostream.h>
void main( )
{
int n=5;
const int &m=n; //声明 m为常引用
cout<<"m="<<m<<endl;
}
这时,程序运行结果为
m=5
第 5章 程序结构
【 例 5-10】 常引用作形参例题 。
#include<iostream.h>
void display(const double &r); //常引用作形参
void main( )
{
double d(6.5);
display(d);
}
void display(const double &r)
{cout<<"r="<<r<<endl;}
第 5章 程序结构常引用做形参时,函数不能更新 r所引用的对象,
因此,对应的实参就不会被破坏 。
程序运行结果为
r=6.5
第 5章 程序结构
5.6.2 常对象使用 const关键字声明的对象称为常对象 。 常对象的声明形式如下:
const <类名 > <对象名 >
或 <类名 > const <对象名 >
声明常对象的同时,也要进行初始化,而且该对象以后不能再被更新 。 现在来看下面的程序:
class A
{
private:
int x,y;
第 5章 程序结构
public:
A(int i=0,int j=0) {x=i;y=j;}
//...
};
const A cobj1;
A const cobj2(3,4);
//...
在上面的程序中,分别用两种形式声明了两个常对象 cobj1和 cobj2,并在声明的同时对它们进行了初始化 。
第 5章 程序结构那么语法如何保障常对象的值不被改变呢? 改变对象的数据成员值有两个途径:一是在类外通过对象名访问其公有数据成员,这时语法会限制不能再赋值;
二是在类的成员函数中改变数据成员的值 。 然而几乎无法预料和统计哪些成员函数会改变数据成员的值 。
因此,语法只好规定不能通过常对象调用普通的成员函数 。 可是这样一来,常对象不是没有任何可用的对外接口了吗? 其实办法是有的,在下一小节中,我们将介绍专门为常对象定义的常成员函数 。
第 5章 程序结构
5.6.3 常成员函数使用 const关键字声明的函数称为常成员函数 。 常成员函数声明的形式如下:
<类型标识符 > <函数名 >(参数表 ) const;
说明:
① const是加在函数声明后面的类型修饰符,它是函数类型的一个组成部分,因此,在实现部分也要带
const关键字 。
第 5章 程序结构
② const关键字可以被用于对重载函数的区分 。 例如,可以在类中这样声明:
void fun( );
void fun( ) const;
③ 常成员函数不能更新对象的数据成员,也不能调用该类中没有用 const修饰的成员函数 。
④ 如果将一个对象说明为常对象,则通过该对象只能调用它的常成员函数,而不能调用其它成员函数 。
第 5章 程序结构
【 例 5-11】 常成员函数例题 。
#include<iostream.h>
class A
{
private:
int x,y;
public:
A(int i=0,int j=0) {x=i;y=j;}
void fun( ) //成员函数第 5章 程序结构
{cout<<"成员函数,x="<<x<<",y="<<y<<endl;}
void fun( ) const //常成员函数
{cout<<"常成员函数,x="<<x<<",y="<<y<<endl;}
};
void main( )
{
A obj1(1,2); //A类对象
obj1.fun( );
const A obj2(3,4); //A类常对象
obj2.fun( );
}
第 5章 程序结构在 A类中说明了两个同名成员函数 fun( ),其中一个是常成员函数 。 在主函数中说明了两个对象 obj1和
obj2,其中对象 obj2是常对象 。 通过对象 obi1调用的是一般的成员函数,而通过对象 obj2调用的是用 const修饰的常成员函数 。
程序运行结果为成员函数,x=1,y=2
常成员函数,x=3,y=4
第 5章 程序结构
5.6.4 常数据成员使用关键字 const不仅可以说明成员函数,还可以说明数据成员 。 如果在一个类中说明了常数据成员
( 包括常引用,常对象等 ),由于常数据成员不能被更新,因此,在类中说明常数据成员时,只能用成员初始化列表的方式通过构造函数对该数据成员进行初始化 。
第 5章 程序结构
【 例 5-12】 常数据成员例题 。
#include<iostream.h>
class A
{
private:
const int x; //常数据成员
static const int y; //静态常数据成员
public:
const int &r; //常引用第 5章 程序结构
A(int i):x(i),r(x){} //常数据成员通过初始化列表获得初值
void fun( )
{cout<<"x="<<x<<",y="<<y<<",r="<<r<<endl;}
};
const int A::y=5; //静态常数据成员的初始化
void main( )
{
A obj1(1),obj2(2);
obj1.fun( );
obj2.fun( );
}
第 5章 程序结构上述程序中说明了三个常数据成员:
const int x; //常数据成员
static const int y; //静态常数据成员
const int &r; //常引用程序中对静态常数据成员 y的初始化是在类外进行的,其它两个常数据成员 x和 r的初始化则通过构造函数实现 。 注意构造函数中,冒号后面的就是数据成员的初始化列表 。
程序运行结果为
x=1,y=5,r=1
x=2,y=5,r=2
第 5章 程序结构
5.7 多文件结构前面我们已经学习了很多完整的 C++源程序实例,
这些程序大部分都属于小程序,它们建立单一的源程序 (,cpp) 经过编译后形成一个目标文件 (,obj),然后再经过连接器生成可执行程序 (,exe) 。 但是,大型程序都要分成多个文件,其理由是:
① 将相关类和函数放在一个特定的文件中,这样可以对不同的文件进行单独编写、编译,最后再连接,
避免重复工作。
第 5章 程序结构
② 使程序更容易管理 。 按逻辑功能将程序分解成各源文件,便于程序员的任务安排 。 大型程序基本上都是由三个部分构成:类的声明,类成员的实现和主函数 。 因此,在实际程序设计中一个源程序至少要划分为三个文件:类声明文件 ( *.h),类实现文件
( *.cpp) 和类的使用文件 ( *.cpp) ( 即主函数文件 ) 。
对于更为复杂的程序,每一个类都有单独的定义和实现文件 。 这样可以充分利用类的封装特性,在程序的调试,修改时,只对其中某一个类的定义和实现进行操作,而其余部分根本就不用改动 。
第 5章 程序结构
【 例 5-13】 多文件结构例题 。
下面是一个具有静态数据,函数成员的 point类的例题 。 相关的内容在前面都已经介绍过,现在我们用多文件结构的形式来实现 。 程序代码如下:
//文件 1,类的声明,point.h
#include<iostream.h>
class point
{
private:
int x,y;
static int countP;
第 5章 程序结构
public:
point(int xx=0,int yy=0) {x=xx;y=yy;countP++;}
point(point &p);
int get_x( ) {return x;}
int get_y( ) {return y;}
static void get_c( ) {cout<<" Object id="<<countP<<endl;}
};
//文件 2,类的实现,point.cpp
#include"point.h"
point::point(point &p)
第 5章 程序结构
{
x=p.x;
y=p.y;
countP++;
}
//文件 3,主函数,fmian.cpp
#include<iostream.h>
#include"point.h"
int point::countP=0;
void main( )
第 5章 程序结构
{
point a(4,5);
cout<<"point a,"<<a.get_x( )<<","<<a.get_y( );
a.get_c( );
point b(a);
cout<<"point b,"<<b.get_x( )<<","<<b.get_y( );
point::get_c( );
}
分析整个源程序的结构,由三个单独的源文件构成,
它们的相互关系和编译,连接过程可以用图 5-2表示 。
第 5章 程序结构图 5-2 多文件组织结构图
p o i n t,c p p
# i n c l u d e " p o i n t,h "
p o i n t,h
# i n c l u d e " i o s t r e a m,h "
f m a i n,c p p
# i n c l u d e " p o i n t,h "
编 译编 译连 接可执行程序f m a i n,e x e
p o i n t,o b j f m a i n,o b j
第 5章 程序结构在多文件结构中,point.h文件包含 iostream.h。 在前面曾经提到过,输入,输出操作是在 iostream.h中定义的,要想使用它们,就必须把这个由系统提供的标准头文件包含到源程序中 。 在两个,cpp文件中都包含我们自己定义的头文件 point.h。 从图 5-2可以看到,两个,cpp文件被分别编译生成各自的目标文件,obj,然后再连接成为可执行文件,exe。 如果我们只修改了类的成员函数的实现部分,那么重新编译 point.cpp并连接即可,
其余的文件可以不改动 。
第 5章 程序结构采用多文件结构的形式,我们同样可以得到与前面一样的运行结果:
point a,4,5 Object id=1
point b,4,5 Object id=2
第 5章 程序结构
5.8 编 译 预 处 理预处理程序又称为预处理器,它包含在编译器中 。
预处理程序提供了一组编译预处理指令和预处理操作符 。 编译器在对源程序进行编译之前,首先要由预处理程序对程序文本进行预处理,然后编译器接受预处理程序的输出,并将源代码转化成用机器语言编写的目标文件 。
预处理指令实际上不是 C++语言的一部分,它只是用来扩充 C++程序设计的环境。
第 5章 程序结构使用预处理指令需要注意以下几点:
① 所有的预处理指令在程序中都是以,#”来引导 。
② 每一条预处理指令单独占用一行,不需要用分号结束 。
③ 预处理指令可以根据需要出现在程序中的任何位置 。
第 5章 程序结构
1,#include指令
#include指令也称文件包含指令,其作用是将另一个源文件嵌入到当前源文件中该点处 。 我们在前面各章中已经用过 #include指令来嵌入头文件 。 文件包含指令有两种格式 。
格式 1:#include<文件名 >
功能:按标准方式搜索 C++系统目录下的 include子目录 。
格式 2,#include“文件名,
功能:首先在当前目录中搜索,若没有,再按标准方式搜索 。
第 5章 程序结构
2,#define和 #undef指令在 C语言中,#define用来定义符号常量和带参数的宏 。 例如:
#define PI 3.14 //定义了一个符号常量 PI
#define MAX(a,b) ((a)>(b)?(a):(b)) //定义一个带参数的宏
MAX(a,b)
在 C++中,虽然仍可以这样定义,但是定义符号常量已经被 const所替代;定义带参数的宏也被 inline内嵌函数所代替 。 #undef的作用是删除由 #define定义的宏,
使之不再起作用 。
第 5章 程序结构
3,条件编译指令使用条件编译指令,可以限定程序中的某些内容要在满足一定条件的情况下才参与编译 。 因此,利用条件编译可以使同一个源程序在不同的编译条件下产生不同的目标代码 。 常用的条件编译语句形式如下 。
形式 1:
#ifdef 标识符程序段 1
[#else
程序段 2]
#endif
第 5章 程序结构形式 1表示:如果,标识符,已经定义过,则编译程序段 1,否则编译程序段 2;如果没有程序段 2,则
#else可以省略 。
形式 2,#ifndef 标识符程序段 1
[#else
程序段 2]
#endif
形式 2表示:如果“标识符”未被定义过,则编译程序段 1,否则编译程序段 2;如果没有程序段 2,则
#else可以省略。