5.1 标识符的作用域与可见性
5.1.1 作用域
函数原形的作用域:
- 函数原型中的参数,其作用域始于 "(",结束于")"。
- 例如:double area(double radius); 标识符radius的作用(或称有效)范围就在函数area形参列表的左右括号之间.
在程序的其他地方不能引用这个标识符。因此标识符radius的作用域称做函数原型作用域。
块作用域(局部作用域):在块中声明的标识符,其作用域自声明处起,限于块中,例如:
类作用域:
- 类作用域作用于特定的成员名。
- 类X的成员M具有类作用域,对M的访问方式如下:
- 如果在X的成员函数中没有声明同名的局部作用域标识符,那么在该函数内可以访问成员M。
- 通过表达式x.M或者X::M访问。
- 通过表达式prt->M
文件作用域:
- 不在前述各个作用域中出现的声明,具有文件作用域
- 这样声明的标识符的作用域开始于声明点,结束于文件尾。
命名空间作用域:具有命名空间作用域的变量也称为全局变量。
5.1.2 可见性
- 可见性是从对标识符的引用的角度来谈的概念
- 可见性表示从内层作用域向外层作用域“看”时能看见什么。
- 如果标识在某处可见,则就可以在该处引用此标识符。
- 标识符应声明在先,引用在后。
- 如果某个标识符在外层中声明,且在内层中没有同一标识符的声明,则该标识符在内层可见。
- 对于两个嵌套的作用域,如果在内层作用域内声明了与外层作用域中同名的标识符,则外层作用域的标识符在内层不可见。
同一作用域中的同名标识符:
- 在同一作用域内的对象名、函数名、枚举常量名会隐藏同名的类名或枚举类型名。
- 重载的函数可以有相同的函数名。
例:作用域与可见性
#include<iostream> using namespace std; int i; //文件作用域 int main() { i=5; { int i; //块作用域 i=7; cout<<"i="<<i<<endl; //输出7 } cout<<"i="<<i; //输出5 return 0; }
5.2 对象的生存期
5.2.1 静态生存期
- 对象的生存期与程序的运行期相同。
- 在文件作用域中声明的对象具有这种生存期。
- 在函数内部声明静态生存期对象,要冠以关键字static 。
5.2.2 动态生存期(局部生存对象)
- 块作用域中声明的,没有用static修是的对象是动态生存期的对象(习惯称局部生存期对象)。
- 开始于程序执行到声明点时,结束于命名该标识符的作用域结束处。
#include<iostream> using namespace std; void fun(); void main() { fun(); fun(); } void fun() { static int a=1; int i=5; a++; i++; cout<<"i="<<i<<",a="<<a<<endl; }
运行结果:
i=6, a=2
i=6, a=3
i是动态生存期
a是静态生存期
例:变量的生存期与可见性
#include<iostream> using namespace std; int i=1; // i 为全局变量,具有静态生存期。 void other(void) { static int a=2; static int b; // a,b为静态局部变量,具有全局寿命,局部可见。 //只第一次进入函数时被初始化。 int c=10; // C为局部变量,具有动态生存期, //每次进入函数时都初始化。 a=a+2; i=i+32; c=c+5; cout<<"---OTHER--- "; cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl; b=a; } void main(void) { static int a; // 静态局部变量,有全局寿命,局部可见。 没有初始化默认为0 int b=-10; // b, c为局部变量,具有动态生存期。 int c=0; void other(void); cout<<"---MAIN--- "; cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl; c=c+8; other(); cout<<"---MAIN--- "; cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl; i=i+10; other(); }
运行结果:
---MAIN---
i: 1 a: 0 b: -10 c: 0
---OTHER---
i: 33 a: 4 b: 0 c: 15
---MAIN---
i: 33 a: 0 b: -10 c: 8
---OTHER---
i: 75 a: 6 b: 4 c: 15
例:具有静态和动态生存期对象的时钟程序
#include<iostream> using namespace std; class Clock //时钟类声明 {public: //外部接口 Clock(); void SetTime(int NewH, int NewM, int NewS); //三个形参均具有函数原型作用域 void ShowTime(); ~Clock(){} private: //私有数据成员 int Hour,Minute,Second; }; //时钟类成员函数实现 Clock::Clock() //构造函数 { Hour=0; Minute=0; Second=0; } void Clock::SetTime(int NewH, int NewM, int NewS) { Hour=NewH; Minute=NewM; Second=NewS; } void Clock::ShowTime() { cout<<Hour<<":"<<Minute<<":"<<Second<<endl; } Clock globClock; //声明对象globClock, //具有静态生存期,文件作用域 void main() //主函数 { cout<<"First time output:"<<endl; //引用具有文件作用域的对象: globClock.ShowTime(); //对象的成员函数具有类作用域 globClock.SetTime(8,30,30); Clock myClock(globClock); //声明具有块作用域的对象myClock cout<<"Second time output:"<<endl; myClock.ShowTime(); //引用具有块作用域的对象 }
程序的运行结果为:
First time output:
0:0:0
Second time output:
8:30:30
5.3 类的静态成员
5.3.1 静态数据成员
- 用关键字static声明
- 静态数据成员是一个类的所有对象共同维护和拥有的一个成员,不是该类的每一个对象所独有的
- 必须在类外定义和初始化,用(::)来指明所属的类。
例:具有静态数据成员的Point类
#include <iostream> using namespace std; class Point //point类定义 {public: Point(int x=0, int y=0):x(x),y(y) {//构造函数 count++; //在构造函数中对count累加,所有对象共同维护一个count } Point(Point &p){//复制构造函数 x=p.x; y=p.y; count++; } ~Point(){count--;} int getX() {return x;} int getY() {return y;} void showCount() {//输出静态数据成员 cout<<" Object id="<<count<<endl; } private: int x,y; static int count;//静态数据成员声明,用于记录点的个数 }; int Point::count=0; //静态数据成员定义和初始化,使用类名限定 void main() { Point a(4,5); //定义对象a,其构造函数会使count增1 cout<<"Point A,"<<a.getX()<<","<<a.getY(); a.showCount(); Point b(a); //定义对象b,其构造函数会使count增1 cout<<"Point B,"<<b.getX()<<","<<b.getY(); b.showCount(); }
上面例子有一个问题当你没有构造点的时候count=0,但是你没有办法在主函数中查到当前count的值。showCount没有对象,无法引用。
5.3.2 静态函数成员
- 类外代码可以使用类名和作用域操作符来调用静态成员函数。
- 态成员函数只能引用属于该类的静态数据成员或静态成员函数。
#include<iostream> using namespace std; class Application { public: static void f(); static void g(); private: static int global; }; int Application::global=0; void Application::f() { global=5; } void Application::g() { cout<<global<<endl; } int main() { Application::f(); Application::g(); return 0; } class A { public: static void f(A a); private: int x; }; void A::f(A a) { cout<<x; //对x的引用是错误的 cout<<a.x; //正确 } //具有静态数据、函数成员的Point类 #include <iostream> using namespace std; class Point //Point类声明 {public: //外部接口 Point(int xx=0, int yy=0) {X=xx;Y=yy;countP++;} Point(Point &p); //拷贝构造函数 int GetX() {return X;} int GetY() {return Y;} static void GetC() {cout<<" Object id="<<countP<<endl;} private: //私有数据成员 int X,Y; static int countP; } Point::Point(Point &p) { X=p.X; Y=p.Y; countP++; } int Point::countP=0; void main() //主函数实现 { Point A(4,5); //声明对象A cout<<"Point A,"<<A.GetX()<<","<<A.GetY(); A.GetC(); //输出对象号,对象名引用 Point B(A); //声明对象B cout<<"Point B,"<<B.GetX()<<","<<B.GetY(); Point::GetC(); //输出对象号,类名引用 }
5.4 类的友元
- 友元是C++提供的一种破坏数据封装和数据隐藏的机制。
- 通过将一个模块声明为另一个模块的友元,一个模块能够引用到另一个模块中本是被隐藏的信息。
- 可以使用友元函数和友元类。
- 为了确保数据的完整性,及数据封装与隐藏的原则,建议尽量不使用或少使用友元。
5.4.1 友元函数
- 友元函数是在类声明中由关键字friend修饰说明的非成员函数,在它的函数体中能够通过对象名访问 private 和 protected成员
- 作用:增加灵活性,使程序员可以在封装和快速性方面做合理选择。
- 访问对象中的成员必须通过对象名。
例:使用友元函数计算两点距离
#include <iostream> #include <cmath> using namespace std; class Point //Point类声明 { public: //外部接口 Point(int xx=0, int yy=0) {X=xx;Y=yy;} int GetX() {return X;} int GetY() {return Y;} friend float Distance(Point &a, Point &b); //友元函数 private: //私有数据成员 int X,Y; }; double Distance( Point& a, Point& b) { double dx=a.X-b.X; double dy=a.Y-b.Y; return sqrt(dx*dx+dy*dy); } int main() { Point p1(3.0, 5.0), p2(4.0, 6.0); double d=Distance(p1, p2); cout<<"The distance is "<<d<<endl; return 0; }
5.4.2 友元类
- 若一个类为另一个类的友元,则此类的所有成员都能访问对方类的私有成员。
- 声明语法:将友元类名在另一个类中使用friend修饰说明。
例:
class A
{ friend class B; //定义友元类,授权B可以访问A类中的私有成员
public:
void Display()
{cout<<x<<endl;}
private:
int x;
}
class B
{ public:
void Set(int i);
void Display();
private:
A a;
};
void B::Set(int i)
{
a.x=i;
}
void B::Display()
{
a.Display();
}
友元关系是单向的
- 如果声明B类是A类的友元,B类的成员函数就可以访问A类的私有和保护数据,但A类的成员函数却不能访问B类的私有、保护数据。
5.5 共享数据的保护
常类型: 常类型的对象必须进行初始化,而且不能被更新。
- 常对象:必须进行初始化,不能被更新。
- 常成员:常数据成员和常函数成员
- 常引用:被引用的对象不能被更新。
- 常数组:数组元素不能被更新。
- 常指针:指向常量的指针。
5.5.1 常对象
常对象必须进行初始化,而且不能被更新。
const 类名 对象名 或 类名 const 对象名
class A
{
public:
A(int i,int j) {x=i; y=j;}
...
private:
int x,y;
};
A const a(3,4); //a是常对象,不能被更新
5.5.2 用const修饰的类成员
1、常成员函数:使用const关键字修饰的函数
类型说明符 函数名(参数表)const;
#include<iostream> using namespace std; class R { public: R(int r1, int r2){R1=r1;R2=r2;} void print(); void print() const; //常成员函数 ,重载函数 private: int R1,R2; }; void R::print() { cout<<R1<<":"<<R2<<endl; } void R::print() const //常成员函数,绝不改变成员的状态 { cout<<R1<<";"<<R2<<endl; } void main() { R a(5,4); a.print(); //调用void print() const R b(20,52); b.print(); //调用void print() const }
#include<iostream> using namespace std; class R { public: R(int r1, int r2){R1=r1;R2=r2;} void print(); void print() const; //常成员函数 ,重载函数 private: int R1,R2; }; void R::print() { cout<<R1<<":"<<R2<<endl; } void R::print() const //常成员函数,绝不改变成员的状态 { cout<<R1<<";"<<R2<<endl; } void main() { R a(5,4); a.print(); //调用void print() const R b(20,52); b.print(); //调用void print() const }
2、常数据成员
#include<iostream> using namespace std; class A {public: A(int i); void print(); const int& r; private: const int a; static const int b; //静态常数据成员 }; const int A::b=10; //b初始化后,就再也不许改变了 A::A(int i):a(i),r(a) {} void A::print() { cout<<a<<":"<<b<<":"<<r<<endl; } void main() {/*建立对象a和b,并以100和0作为初值,分别调用构造函数,通过构造函数的初始化列表给对象的常数据成员赋初值*/ A a1(100),a2(0); a1.print(); a2.print(); }
5.5.3 常引用
被引用的对象不能被更新。相当于是只读的,不能去修改,只能读取。
const 类型说明符 &引用名
例:
#include <iostream> #include <cmath> using namespace std; class Point{//Point类定义 public://外部接口 Point(int x=0,int y=0):x(x),y(y){} int getX(){return x;} int getY(){return y;} friend float dist(const Point &p1,const Point &p2); private: //私有数据成员 int x,y; }; float dist(const Point &p1,const Point &p2){//只能访问,不能修改 double x=p1.x-p2.x; double y=p1.x-p2.y; return static_cast<float>(sqrt(x*x+y*y)); } int main(){ const Point myp1(1,1),myp2(4,5); cout<<"The distance is:"; cout<<dist(myp1,myp2)<<endl; return 0; }
#include<iostream> using namespace std; void display(const double& r); int main() { double d(9.5); display(d); return 0; } void display(const double& r) //常引用做形参,在函数中不能更新 r所引用的对象。 { cout<<r<<endl; }
5.6 多文件结构和编译预处理命令
5.6.1 C++程序的一般组织结构
- 一个工程可以划分为多个源文件,例如
- 类声明文件(.h文件)
- 类实现文件(.cpp文件)
- 类的使用文件(main()所在的.cpp文件)
- 利用工程来组合各个文件。
例: 多文件的工程
//文件1,类的定义,Point.h class Point { //类的定义 public: //外部接口 Point(int x = 0, int y = 0) : x(x), y(y) { count++; } Point(const Point &p); ~Point() { count--; } int getX() const { return x; } int getY() const { return y; } static void showCount(); //静态函数成员 private: //私有数据成员 int x, y; static int count; //静态数据成员 };
//文件2,类的实现,Point.cpp #include "Point.h" #include <iostream> using namespace std; int Point::count = 0; //使用类名初始化静态数据成员 Point::Point(const Point &p) : x(p.x), y(p.y) { count++; } void Point::showCount() { cout << " Object count = " << count << endl; }
//文件3,主函数,5_10.cpp #include "Point.h" #include <iostream> using namespace std; int main() { Point a(4, 5); //定义对象a,其构造函数使count增1 cout <<"Point A: "<<a.getX()<<", "<<a.getY(); Point::showCount(); //输出对象个数 Point b(a); //定义对象b,其构造函数回使count增1 cout <<"Point B: "<<b.getX()<<", "<<b.getY(); Point::showCount(); //输出对象个数 return 0; }
5.6.2 外部变量与外部函数
外部变量:
- 如果一个变量除了在定义它的源文件中可以使用外,还能被其它文件使用,那么就称这个变量是外部变量。
- 文件作用域中定义的变量,默认情况下都是外部变量,但在其它文件中如果需要使用这一变量,需要用extern关键字加以声明。
外部函数:
- 在所有类之外声明的函数(也就是非成员函数),都是具有文件作用域的。
- 这样的函数都可以在不同的编译单元中被调用,只要在调用之前进行引用性声明(即声明函数原型)即可。也可以在声明函数原型或定义函数时用extern修饰,其效果与不加修饰的默认状态是一样的。
- 将变量和函数限制在编译单元内
- 使用匿名的命名空间:在匿名命名空间中定义的变量和函数,都不会暴露给其它的编译单元。
- namespace { //匿名的命名空间
int n;
void f() {
n++;
}
}
这里被“namespace { …… }”括起的区域都属于匿名的命名空间。
5.6.3 标准C++库
标准C++类库是一个极为灵活并可扩展的可重用软件模块的集合。
标准C++类与组件在逻辑上分为6种类型:
- 输入/输出类
- 容器类与抽象数据类型
- 存储管理类
- 算法
- 错误处理
- 运行环境支持
5.6.4 编译预处理
- #include 包含指令
- 将一个源文件嵌入到当前源文件中该点处。
- #include<文件名>
- 按标准方式搜索,文件位于C++系统目录的include子目录下
- #include"文件名"
- 首先在当前目录中搜索,若没有,再按标准方式搜索。
- #define 宏定义指令
- 定义符号常量,很多情况下已被const定义语句取代。
- 定义带参数宏,已被内联函数取代。
- #undef
- 删除由#define定义的宏,使之不再起作用。
条件编译指令——#if 和 #endif:
#if 常量表达式 //当“ 常量表达式”非零时编译
程序正文
#endif
......
条件编译指令——#else:
#if 常量表达式
//当“ 常量表达式”非零时编译
程序正文1
#else
//当“ 常量表达式”为零时编译
程序正文2
#endif
条件编译指令——#elif
#if 常量表达式1
程序正文1 //当“ 常量表达式1”非零时编译
#elif 常量表达式2
程序正文2 //当“ 常量表达式2”非零时编译
#else
程序正文3 //其它情况下编译
#endif
条件编译指令
#ifdef 标识符
程序段1
#else
程序段2
#endif
如果“标识符”经#defined定义过,且未经undef删除,则编译程序段1,否则编译程序段2。
#ifndef 标识符
程序段1
#else
程序段2
#endif
如果“标识符”未被定义过,则编译程序段1,否则编译程序段2。
5.7 综合实例-个人银行账户管理程序
5.8 深度探索
实验课:
clinet.h
clinet.cpp
Client.cpp
//文件2,类的实现,Point.cpp
#include "Point.h"
#include <iostream>
using namespace std;
int Point::count = 0; //使用类名初始化静态数据成员
Point::Point(const Point &p) : x(p.x), y(p.y) {
count++;
}
void Point::showCount() {
cout << " Object count = " << count << endl;
}