第五章 数据的共享与保护
5.1 标识符的作用域与可见性
5.1.1 作用域
作用域:一个标识符在程序正文中的有效的区域
**C++中标识符的作用域:**函数原型作用域、局部作用域(块作用域)、类作用域、文件作用域、命名空间作用域
1.函数原型的作用域
double Area(double radius);
radius的作用域仅在“(”和“)”之间,不能用于程序正文其他地方,因而可有可无。但是考虑到程序可读性,通常需要给出形参名。
2.局部作用域(块作用域)
自声明处起,限于块中
e.g. 函数形参列表中形参的作用域
函数体内声明的变量的作用域
具有局部作用域的变量也称为局部变量
3.类作用域
①如果在X的成员函数中没有声明同名的局部作用域标识符,那么在该函数内可以直接访问成员m
②访问对象成员 X.m 或 X::m(访问类中静态成员)
③ptr->m (ptr为指向对象的一个指针)
4.文件作用域
开始于声明点,结束于文件尾
全局变量
5.命名空间作用域
作用:为了避免重名冲突,使编译器能够区分来自不同库的同名实体,将不同的标识符集合在一个命名作用域(named space)内,是全局作用域的细分,本质上定义了实体所属的空间.
一个命名空间确定了一个命名空间作用域
声明方式:
namespace namespace_name{
//代码声明
}
**使用方式:**使用某个命名空间中的函数、变量等实体,需要 命名空间::实体名称 或 using namespace namespace_name的方式;
用using来指定命名空间:
using MYNS::Clock; //只把Clock暴露在当前作用域内
using namespace MYNS; //把命名空间中的所有标识符暴露在当前作用域内
两类特殊的命名空间:
1.全局命名空间:默认的命名空间,没有命名空间的标识符都处于全局命名空间中
2.匿名命名空间:需要显示声明但没有名字的命名空间
namespace{
各种声明
}
在多文件编程中,该空间常常被用来屏蔽不希望暴露给其他源文件的标识符
6.限定作用域的enum枚举类
方式:
enum class{...};//多了class或struct限定符,此时枚举元素的名字遵循常规的作用域准则,即类作用域
枚举类分为:
不限定作用域:枚举元素的作用域与枚举类型本身的作用域相同
限定作用域:枚举元素具有类作用域
- enum color{ red,yellow,green};
- enum color1{ red,yellow,green}; //错误,枚举元素重复定义
- enum class color2{ red,yellow,green}; //正确,限定作用域的枚举元素被隐藏了
- color c = red;
- color c1=color::red;
5.1.2可见性
标识符的可见性:从标识符引用的角度来看标识符的有效范围,即标识符是否可以被引用。表示从内层作用域向外层作用域“看”时能看见什么,如果标识符在某处可见,则就可以在该处引用此标识符
可见性的规则
- 标识符要先声明,后使用
- 在同一作用域中,不能声明同名的标识符(重载函数除外)
- 在没有相互包含关系的不同的作用域中声明的同名标识符,互不影响
- 如果在两个或多个具有包含关系的作用域中声明了同名标识符,则外层标识符在内层不可见
- 如果某个标识符在外层中声明,且在内层中没有同一标识符的声明,则该标识符在内层中可见
5.2对象的生存期
对象从产生到结束的这段时间就是它的生存期,在对象的生存期内,对象将保持它的值(数据成员的值),直到被更新为止。
5.2.1静态生存期
静态生存期:如果对象的生存期与程序运行的生存期相同,我们就称它具有静态生存期
5.2.2动态生存期
注意;禁止使用class类型的全局变量
5.3 类的静态成员
- 类实现共享与隐藏两全
- 类内部的函数之间实现了共享:类中的数据成员可以被同一类中的任何一个函数访问
- 设置访问控制属性:共享是受限的,限制在类中
- 如何实现对象之间的共享?
5.3.1 静态数据成员
静态数据成员的必要性
类属性:描述类的所有对象共同特征的一个数据项,对于任何对象实例,它的属性值是相同的
若要实现类属性,如果在类中增加新的数据成员,则即冗余,又在各个对象中不一致;如果再声明一个类外变量,则不能实现数据的隐藏
静态成员
- 解决了同类的不同对象之间数据和函数的共享问题,由所有对象共同维护和使用
- 静态数据成员:属于类内部的合法成员,在类外储存,独立于具体的类对象而存在
- 使用时·必须初始化,且必须类外初始化
- 静态常量数据成员(constexpr/const):
- constexpr static int count=0;
- 在类内初始化,此时仍可在类外定义该静态成员,但不可再初始化
class Employee{
public:
//成员函数略
private:
int empNo;
char *name;
static int count; //静态数据成员,引用性说明
//说明静态数据成员count的访问权限和作用域
};
int Employee ::count=0; //定义性说明
//为count分配内存和初始化
int main(void){
}
问: count是私有数据成员,为什么可在类外使用?
答:设定static数据成员初值时,不受如何存取权限束缚
问:为什么类的静态成员需要在类定义之外再加以定义?
答:需要以这种方式专门为它们分配空间。非静态数据成员无须以此方式定义,是因为它们的空间是与它们所属对象的空间同时分配的。
静态数据成员与普通数据成员的区别
类的普通数据成员
- 在声明类的时候被创建
- 在类的每个对象中都拥有一个副本
- 具有类作用域和动态生存期
- 引用方式:
- 对象.数据成员
- 对象指针->数据成员
类的静态数据成员
- 在编译时被创建
- 所有对象共享一个副本,且与该对象是否存在无关,由该类的所有对象共同维护和使用
- 静态数据成员是类的成员,而不是对象的成员
- 实现了同一类的不同对象之间的数据共享
- 具有全局的生存周期(静态生存期)
- 静态数据成员的引用:
- 类::静态数据成员
- 对象.静态数据成员
- 对象指针->静态数据成员
静态数据成员的使用
- 保存该类对象的个数
- 作为一个标志,指定一个特定的动作是否发生
- 保存指向链表的第一关或醉后应该成员的指针
- 管理该类的各个对象共享的资源
5.3.2 静态函数成员
如何管理静态数据成员?
可以用Point::showCount();输出静态数据成员的初始值吗?
漏,大漏特漏!
静态函数成员的声明
static 返回值类型 函数成员名(参数表);
静态函数成员的调用
<类名>::<静态函数成员>(参数表)
或
<对象名>.<静态函数成员>(参数表)
习惯:一般习惯于通过类名调用。因为即使通过对象名调用,起作用的也只是对象的类型信息,与所使用的具体对象毫无关系。
静态成员函数可以直接访问该类的静态数据和函数成员。而访问非静态成员,必须通过对象名。
class A{
public:
static void f(A a);
private:
int x;
};
void A::f(A,a){
cout<<x; //对x的引用是错误的
cout<<a.x; //正确
}
静态函数成员注意事项
- 静态函数不在于信息共享,数据沟通,而在于管理静态数据成员,完成对静态数据成员的封装。
- 调用静态函数成员的访问可以不依附于任何对象
- 只能直接访问属于该类的静态数据成员和静态函数成员
- 静态函数成员没有this指针,不能直接访问类的非静态数据成员
- 如果要访问非静态数据成员,必须通过参数传递对象名,然后通过对象名访问非静态数据成员
- 非静态成员函数可以直接访问静态成员函数
- 访问静态数据成员的函数,其函数体的定义应该与静态成员的初始化在同一个源文件中
- 静态函数成员具有自己类的作用域,但具有全局生命周期(静态生存期)
静态成员函数与一般成员函数的区别
静态成员函数
- 声明方式不同:加static
- 使用限制不同:能直接访问静态成员,不能直接访问非静态数据成员
- 调用方式不同:Point::showCount(); 或者 a.showCount();
一般成员函数
- 声明方式不同:不加static
- 使用限制不同:均可访问
- 调用方式不同:a.showCount();
5.4 类的友元
封装与隐藏是面向对象的两个主要特性,可以保证按所需要的方式正确地使用数据成员,但有时需要让外部函数访问私有数据成员。例如,两个不同类的对象可能要向同一个函数提供数据,有矛盾吗?
e.g.计算任意两点间距离都函数
1.放在类外:Dist(p1,p2);
不能体现出这个函数与“点"之间的关系,且类外函数不可以直接引用点的坐标
2.设计为类的函数成员:p1.Dist(p2);
体现不出距离是点与点之间的关系
3.类的组合:
- 线段是点的组合
- 若有许多点,并且经常需要计算两点的距离时都需要构造线段,影响程序的可读性
友元关系
友元关系就是一个类主动声明哪些其他类或函数是他的朋友,进而给他们提供对本类的访问特许
注意:
- 友元关系在类外,但与类有特殊的关系
- 是一种解除访问控制约束的机制
- 提供了数据共享的机制
- 不同类或对象的函数成员之间
- 类的函数成员与一般成员之间
友元有三种:友元函数(普通函数)、友元成员函数、友元类
5.4.1 友元函数
- 一种定义在当前类外的普通函数
- 该函数可以访问这个类的私有成员
- 友元函数不能是自称的友元,需通过friend在当前类内部声明
- 友元函数不是当前类的函数成员
e.g.
class Point{
public: //外部接口
Point(int x=0,int y=0) : x(x),y(y) {}
int getX() {return x;}
int getY() {return y;}
friend float dist(Point &p1,Point &p2); //友元函数声明
private:
int x,y;
}
float dist(Point &p1,Point &p2){ //友元函数的实现
double x=p1.x-p2.x;
double y=p1.y-p2.y;
return static_cast<float>(sqrt(x*x+y*y)); //强制类型转换
}
注意事项
- 友元延伸了(但没有打破)类的封装界限
- 通过让函数成为类的友元,可以赋予该函数与类的函数成员相同的访问权限
- 被访问的类负责声明谁是友元
- 友元是声明我允许谁访问我的私有成员
- 为了确保数据的完整性,及数据封装与隐藏的原则,建议尽量不使用或少使用友元,如果使用太多友元函数,则应该考虑重新设计程序
具有相同友元的两个类
假设函数在操作俩个无关类的对象,函数的参数是两个类对象,在函数中处理其私有数据,则这两个类都要向该函数提供友元关系。
友元成员函数
类可以向其他类的成员提供友元关系,即一个类的友元函数可以是另一个类的函数成员
friend void Date::show(const Time &t); //友元成员函数
5.4.2 友元类
一个类可以成为另一个类的友元
若B类为A类的友元,B类的所有成员函数都自动成为A类的友元函数,B类的所有成员函数中都能直接访问A类的私有成员和保护成员。
声明语法
class B{
...
friend class A; //声明A为B的友元类
...
};
友元关系注意事项
- 友元关系是不能传递的
- 友元关系是单向的
- 友元关系是不能继承的
5.5 共享数据的保护
虽然数据隐藏保证了数据的安全性,但各种形式的数据共享却又不同程度地破坏了数据的安全。因此,对于既需要数据共享又需要防止改变的数据应该声明为常量。因为常量在程序运行期间是不可改变的,所有可以有效地保护数据。
常类型
常类型的对象必须进行初始化,而且不能被更新
- 常对象
- 常类成员
- 常引用
- 常数组:数组元素不能被更新 类型说明符 const 数组名[大小]
- 常指针:指向常量的指针
“Use const whenever you need.”
5.5.1 常对象
常对象定义
常对象就是这样的对象,它的数据成员值在对象的整个生存期内不能被改变。也就是说,常对象必须进行初始化,而且不能被更新。
常对象的数据成员被视为常量
声明方式
const 类型说明符 对象名;
在声明常对象时,把const关键字放在类型名后面也是允许的,不过人们更习惯前者.
注意
- 常对象必须进行初始化
- 常对象的数据成员在对象的整个生存期内不能被改变
问题:如何保障常对象的值不被改变?
答:
- 直接访问:通过对象名访问数据成员。常对象的数据成员被视为常量,通过常对象访问数据成员不允许被复制
- 间接访问:通过成员函数改变数据成员。不能通过常对象调用普通成员函数,常对象只能调用常成员函数
注意*:在定义一个变量或者常量时为它指定初值叫作初始化*
在定义一个变量或者常量以后使用赋值运算符修改它的值叫作赋值
5.5.2 用const修饰的类成员
1.常成员函数
声明方式
类型说明符 函数名(参数表) const
实现方式
int Point::GetX(void) const{
return X;
}
注意事项
-
const是函数类型的一个组成部分,因此在函数的定义部分也要带const关键字
-
如果将一个对象说明为常对象,则通过该常对象只能调用它的常成员函数,而不能调用其他成员函数(这就是C++从语法机制上对常对象的保护,也是常对象唯一的对外接口方式)。
-
无论是否通过常对象调用常成员函数,在常成员函数调用期间,目的对象都被视同为常对象,因此常成员函数不能更改目的对象的数据成员,也不能针对目的对象调用该类中没有用const修饰的成员函数(这就保证了在常成员函数中不会更改目的对象的数据成员的值)。
-
const关键字可以用于对重载函数的区分,例如,如果在类中这样声明:
void print(); void print() const;
这是对print的有效重载。
提示:如果仅以const关键字为区分对成员函数重载,那么通过非const的对象调用该函数,两个重载的函数都可以与之匹配,这时编译器将选择最近的函数——不带const关键字的函数。
e.g.
class Stack{
public:
void Push(int elem);
int Pop(void);
int GetCount(void) const;
private:
int num;
int data[100];
};
int Stack::GetCount(void) const{
++num; //编译2错误,企图修改数据成员num
Pop(); //编译错误,企图调用非const函数
return num;
}
习惯:对于无须改变对象状态的成员函数,都应当使用const
2.常数据成员
- 使用const说明的数据成员
- 如果在一个类中声明了常数据成员,那么任何函数都不能对该成员赋值
- 构造函数对常数据成员的初始化,只能通过构造函数的初始化列表
e.g.
class A{
public:
A(int i);
void print();
private:
const int a; //常数据成员
static const int b; //静态常数据成员 (直接定义:static const int b=10;)
}
const int A::b=10; //静态常数据成员在类外说明和初始化
A::A(int i) : a(i) {} //常数据成员只能通过初始化列表来获得初值
细节:类成员中的静态变量和常量都应当在类体之外加以定义,但C++标准规定了一个例外:类的静态变量如果具有整数型或枚举类型,那么可以直接在类定义中为它指定常量值。这时,不必在类定义外定义A::b,因为会将程序中对A::b的所有引用都替换成数值10,一般无须再为A::b分配空间。但也有例外。p169
5.5.3 常引用
声明
const 类型说明符 &引用名;
注意
- 常引用所引用的对象不能被更新‘
- 非const的引用只能绑定到普通的对象,而不能绑定到常对象
- 常引用可以绑定到常对象或者普通对象
- 常引用无论是否绑定到常对象,均将该对象当作常对象,这意味着,对于基本数据类型的引用,则不能为该数据赋值,对于类类型的引用,则不能修改它的数据成员,也不能调用它的非const成员函数。
- 从效率上说,”传引用调用“参数要优于”传值调用“残数。对于简单类型,效率上的差异可忽略不计;对于类参数,效率上的区别非常明显
- 参数修饰符const适用于任何参数,但它通常用于以类为参数的传引用调用
- 如果输入参数采用”指针传递“,那么加const修饰可以防止意外地改动该指针,起到保护作用
- 要么全部使用修饰符const,要么根本不要使用 ,如果为特定类型的一个参数使用了const,那么对于其他所有参数,只要他们具有相同的类型,而且不由函数调用更改,那么也应该使用const
void guarantee(const Money &price){
cout<<"Money="<<2*price.get_value(); //get_value应该是const函数
}
//如果不为成员函数加上const修饰符,那么大多数编译器可能报错,因为在get_value函数申明中不包含const,编译器就假定调用对象会被修改,所以一旦为Money类型的参数使用了修饰符const,那么针对不会修改调用对象值的所有Money成员函数,都应该使用const,所以在get_value函数的申明中必须包含const
习惯:对于在函数中无须改变其值的参数,不宜使用普通引用方式加以传递,因为那会使得常对象无法被传入,采用传值方式或传递常引用的方式可以避免这一问题。对于大对象来说,传值耗时较多,因此传递常引用为宜。复制构造函数的参数一般也宜采用常引用传递。
常引用和const引用的区别
- c++ primer中有一条规定: 引用所绑定的类型和所引用的对象类型需严格匹配。除了两个例外(下面再说),我们先看看普通的引用情况:
① int i = 3;
int &ri = i; //正确,引用绑定到int 变量i上
② double d = 3.1415;
int &rd = d; //错误, 引用类型为 int ,所绑定对象类型为 double,类型不一致
③ int & rm = 3; //错误, 普通引用必须绑定到对象,不能绑定至常量
const引用,属于1中所说的一种例外,初始化 const引用时允许用任意表达式,只要该表达式的结果能转换为 引用类型即可。
也就是说,允许为一个const引用 绑定 非常量对象、字面值、甚至是一般表达式。
① const int &ci = 3; //正确,整型字面值常量绑定到 const引用
② int i = 1;
const int &cj = i; //正确,非常量对象绑定到 const引用
③ const int i = 4;
const int &ck = i; //正确,常量对象绑定到 const引用
④ const int i = 5;
int &r = i; //错误,常量对象绑定到非const引用
————————————————
版权声明:本文为CSDN博主「c+」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qnavy123/article/details/82183586
5.6 多文件结构和编译预处理
5.6.1 C++程序的一般组织结构
- C++源程序的组成:类定义、类的成员的实现、主函数
- 在规模较大的项目中,往往需要多个源程序文件,每个源程序文件称为一个编译单元
- 多文件编程时注意;
- 类的定义必须出现在所有使用该类的编译单元中
- 将类的定义写在头文件中,使用该类的编译单元则包含这个头文件
- 通常一个项目至少划分为三个文件:类定义文件(.h)、类实现文件(.cpp)、类的使用文件(.cpp,主函数)
- 若更复杂,则每个类都有单独的定义和实现
- 可以对不同的文件进行单独编写、编译,最后再链接
- 声明放在源文件中还是头文件中的原则
- 将需要分配空间的定义放在源文件中,例如 函数的定义、命名空间作用域中变量的定义
- 将不需要分配空间的声明放在头文件中,例如 类声明、外部函数的原型声明、外部变量的声明、基本数据类型变量的声明
- 内联函数比较特殊,由于它的内容需要嵌入到每个调用它的函数之中,所以对于需要被多个编译单元调用的内联函数,他们的代码应该被各个编译单元可见,这些内联函数的定义应当出现在头文件中
习惯:如果误将分配了空间的定义写入头文件中,在多个源文件包含该头文件时,会导致空间在不同的编译单元中被分配多次,从而在连接时引发错误
5.6.2 外部变量与外部函数
1.外部变量
定义
如果一个变量除了在定义它的源文件中可以使用外,还能被其他文件使用,那么就称这个变量是外部变量。
文件作用域中定义的变量,默认情况下都是外部变量,但是在其他文件中如果需要使用这一变量,需要用extern关键字加以声明
声明
①定义性声明:在声明的同时定义(分配内存,初始化)
②引用性声明:引用在别处定义的变量
在文件作用域中,不用extern关键字声明的变量,都是定义性声明;用extern关键字声明的变量,如果同时指定了初值,则是定义性声明,否则是引用性声明
2.外部函数
定义
在所有类之外定义的函数(也就是非成员函数),都是具有文件作用域的,如果没有特殊说明,这样的函数都可以在不同的编译单元中被调用,只要在调用之前进行引用性说嘛(即声明函数原型)即可。也可以在声明函数原型或定义函数时用extern修饰,其效果与不加修饰的默认状态是一样的。
3.将变量和函数限制在编译单元内
原因
安全、避免重名
方法
①在定义时使用static关键字修饰
②使用匿名的命名空间,在匿名空间中定义的变量和函数,都不会暴露给其他的编译单元
习惯:应当将不希望被其他编译单元引用的函数和变量放在匿名的命名空间中
5.6.3 标准C++库
标准C++库 大部分C语言系统函数、预定义的模板和类
标准C++类和组件 输入/输出类、容器类与ADT(抽象数据类型)、存储管理类、算法、错误处理、运行环境支持
对库中预定义内容的声明分别存在于不同的头文件中,要使用这些预定义的成分,就要将相应的头文件包含到源程序中。当包含了必要的头文件后,就可以使用其中预定义的内容了。
使用标准C++库时,还需要加入下面这一语句来将指定命名空间的名称引入当前作用域中:
using namespace ste;
如果不使用上述方法,就需要在使用std命名空间中的标识符时冠以命名空间“std::”
习惯:通常情况下,using namespace语句不宜放在头文件中,因为这会使一个命名空间不被察觉地对一个源文件开放。
5.6.4 编译预处理
预处理命令不是C++语言的一部分,它只是用来扩充C++程序设计的环境
所有预处理指令在程序中都是以#
来引导,每一条预处理指令单独占一行,不要用分号结束。
预处理指令可以根据需要出现在程序中的任何位置。
- #include 指令
- #include<文件名>
- #include"文件名"
- #define 指令 和 #undef指令
- 条件编译指令
p175~p176