一、标识符的作用域与可见性
1、作用域
作用域是一个标识符在程序正文中有效的区域。有函数原型作用域、局部作用域(块作用域)、类作用域、文件作用域、命名空间作用域、限定作用域的enum枚举类。
1)函数原型作用域
在函数原型声明时形式参数的作用范围就是函数原型作用域。
double Area(double radius);
//radius的作用域仅在左右括号之间,不能用于程序正文其它地方,因而可有可无。
2)局部作用域
在块中声明的标识符,其作用域自声明处起,限于块中
void fun(int a)
{ int b(a);
cin>>b;
if (b>0)
{
int c;
......
}
}
- 函数形参列表中形参的作用域,从形参列表的声明开始,到整个函数体结束为止。
- 函数体内声明的变量,其作用域从声明开始,一直到声明所在块结束的花括号位置。
3)类作用域
类里面的成员有类作用域,假设类X里有成员m,m就具有类作用域,下面三种方式访问m
- 如果X的成员函数中没有声明同名的局部作用域标识符,那么在该函数内可以之间访问成员m。
- 通过x.m或X::m。
- 通过ptr->m这样的表达式,ptr为指向X类的一个指针。
4)文件作用域
不在前面各个作用域中出现的声明,就具有文件作用域,这样的声明标识符其作用开始于声明点,结束与文件尾。
5)命名空间作用域
为了避免同名变量、函数、类等命名冲突的情况,让编译器能够区分来自不同库的同名实体,C++引入了命名空间的概念。命名空间定义了实体所属的空间。命名空间定义使用namespace关键字。
namespace namespace_name{
//代码声明
}
某个命名空间中的函数、变量、类等实体,需要 命名空间::实体名 或通过using namespace namespace_name的方式。
6)限定作用域的enum枚举类
限定作用域的枚举类型的方式是enum class {...},即多了class或struct限定符。要通过enum_name::的方式来访问枚举元素
enum color {red, yellor, green} //不限定作用域的枚举类型
enum color1 {red, yellor, green} //错误,枚举元素重复定义
enum class color2 {red, yellor, green} //正确,限定作用域的枚举元素被隐藏了
color c = red; //正确,color的枚举元素在有效的作用域中
color2 c1 = red; //错误,color2的枚举元素不在有效的作用域中
color c2 = color::red; //正确,允许显式地访问枚举元素
color2 c3 = color2::red //正确,使用了color2的枚举元素
2、可见性
程序运行到某一点。能够引用到的标识符,就是该处可见的标识符。
- 标识符应声明在先,引用在后。
- 如果某个标识符在外层中声明,且在内层中没有同一标识符的声明,则该标识符在内层可见。
- 对于两个嵌套的作用域,如果在内层作用域内声明了与外层作用域中同名的标识符,则外层作用域的标识符在内层不可见。
- 在同一作用域内的对象名、函数名、枚举常量名会隐藏同名的类名或枚举类型名。
- 重载的函数可以有相同的函数名
二、对象生存期
1、静态生存期
如果对象的生存期与程序的运行期相同,我们称它具有静态生存期。
- 文件作用域中声明的对象都具有静态生存期。
- 如果要在函数内部的局部作用域中声明具有静态生存期的对象,则要使用关键字static。如:static int i; 这个i就是具有静态生存期的变量,也称为静态变量。它不会随着每次函数的调用而产生,也不会随着函数返回而失效,即使发生递归也是如此,该变量会在各次调用减共享。
2、动态生存期
除了上面的两种情况,其余对象都具有动态生存期。局部生存期对象诞生于声明点,结束与声明所在的块执行完毕之时。
三、类的静态成员
1、静态数据成员
在类里面使用static关键字声明静态成员。静态成员在每个类只有一份,由该类的所有对象共同维护和使用,从而实现了同一类的不同对象之间的数据共享。
- 静态数据成员具有静态生存期,一般用法是“类名::标识符”。
- 必须在类外定义和初始化,用“::”来指明所属的类。
2、静态函数成员
静态成员函数就是使用static关键字声明的函数成员,静态成员函数也属于整个类,由同一个类的所有对象共同拥有,为这些对象所共享。
- 类外代码可以使用类名和作用域操作符来调用静态成员函数。
- 静态成员函数只能引用属于该类的静态数据成员或静态成员函数。而访问非静态成员,必须通过对象名。
class A{
public:
static int m;
static void f(A a);
private:
int x
}
int A::m = 0; //静态数据成员必须在类外定义和初始化
void A:: f(A a){
cout << x; //对x的引用是错误的
cout << a.x; //正确
}
四、类的友元
友元关系提供了不同类或对象的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。友元关系就是一个类主动声明哪些其他类或函数是它的朋友,进而给他们提供对本类的访问特许。
在一个类中可以利用关键字friend将其他函数或类声明为友元。如果友元是一般函数或类的成员函数,称为友元函数;如果友元是一个类,则称为友元类,友元类的所有成员函数都自动成为友元函数。
1、友元函数
- 友元函数是在类中用关键字friend修饰的非成员函数。
- 友元函数的函数体可以通过对象名访问类的私有和保护成员。
- 作用:增加灵活性,使程序员可以在封装和快速性方面做合理选择。
#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;
};
float 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;
}
2、友元类
- 若A类为B类的友元类,则A类的所有成员函数都是B类的友元函数,都可以访问B类的私有和保护成员。
- 声明语法:将友元类名在另一个类中使用friend修饰说明
class B{
...
friend class A; //声明A为B的友元类
...
}
注意:1、友元关系不能传递。2、友元关系是单向的。
五、共享数据保护
1、常对象
常对象的数据成员值在对象的整个生存期间内不能被改变。也就是说,常类对象必须进行初始化,而且不能被更新。语法形式为:
const 类型说明符 对象名;//把const放在类型说明符后面也可以
//举例
class A{
public:
A(int m){
x = m;
}
private:
int x;
}
const A a;//a是常对象,不能被更新
为了避免成员函数会改变数据成员的值,语法规定不能通过常对象调用普通的成员函数,只能调用常成员函数。
2、const修饰的类成员
1)常成员函数
使用const关键字修饰的函数为常成员函数:
类型说明符 函数名(参数表) const;
- 函数定义也要加上const。
- 常对象只能调用它的常成员函数。(常对象唯一对外接口方式)
- 常成员函数调用期间,目的对象都被视为常对象,不能更新目的对象的数据成员
- const关键字可以用于对重载函数的区分。
- 对于无需改变对象状态的成员函数,都应当使用const。
2)常成员数据
- 类的成员数据也可以是常量,使用const说明的数据成员为常数据成员。
- 任何函数都不能对常数据成员赋值。
- 构造函数对其进行初始化,就只能通过初始化列表。
#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; //静态常数据成员在类外说明和初始化,因为是静态可以在外面说明
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();
}
3)常引用
- 用const修饰的引用就是常引用。
- 常引用所引用的对象不能被更新。
- 非const的引用只能绑定到普通对象,而不能绑定到常对象,但常引用可以绑定到普通对象和常对象。无论绑定到常对象还是普通对象,都不能通过这个给引用来更新对象。
- 由于上面这一点,对于函数中无须改变其值的参数,不宜使用普通引用方式加以传递,因为这会导致常对象无法传入。
六、多文件结构和编译预处理命令
1、编译预处理命令
在编译器对远程序进行编译之前,首先要由预处理器对程序文本进行预处理。预处理器提供了一组编译预处理指令和预处理操作符。所有预处理指令在程序中都是以“#”来引导。
1)#include 指令
#include 指令也称文件包含指令,其作用是将一个源文件嵌入到当前源文件中该点处。通常用来嵌入头文件,对应由两种格式:
- #include<文件名> 按标准方式搜索,文件位于系统目录的include子目录下。
- #include"文件名" 首先在当前目录中搜索,如果没有,再按标准方式搜索。
2)#define 和 #undef 指令
#define 用来定义符号常量如:#define PI 3.14 定义了一个符合常量PI的值为3.14。(注意不要加分号!)虽然可以这样定义符合常量,更好的方法还是使用const。
#define 还可以定义带参数宏,已被内联函数取代。
#undef的作用是删除由#define定义的宏,使之不再起作用。
3)条件编译指令
条件编译指令,可以限定程序中的某些内容要在满足一定条件的情况下才参与编译。常用的由以下几种:
#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。
2、C++程序的一般组织结构
通常一个项目至少划分为三个文件:类定义文件(*.h,头文件)、类实现文件(*.cpp文件)、类的使用文件(*.cpp,主函数文件)。
一般来说类实现文件和类的使用文件需要 #include" **.h ",即对应的头文件
决定一个声明放在源文件还是头文件的一般规则是:
- 将需要分配空间的定义放在源文件中,如函数的定义(需要为函数代码分配空间)、文件作用域中变量的定义(需要为变量分配空间)等。
- 不需要分配空间的声明放在头文件中,如类的声明、外部函数的原型声明、外部变量的声明、基本数据类型常量的声明等。内联函数,由于它的内容需要嵌入到每个调用它的函数之中,它的代码应该被各个编译单元可见,应当放在头文件。
- !!如果将分配了空间的定义写入头文件中,在多个源文件包含该头文件时,会导致编译在不同的编译单元中被分配多次,从而在连接时引发错误。
3、外部函数与外部变量
1)外部变量
如果一个变量除了在定义它的源文件中可以使用,还能被其他文件使用,那么就称这个变量是外部变量。
文件作用域中定义的变量,默认情况下都是外部变量,但在其他文件中使用这个变量的话,需要用extern关键字加以声明,如下:
//源文件1
int i = 3; //定义变量i
//源文件2
extern int i; //声明一个在其他文件中定义的外部变量,引用性声明。
2)外部函数
在所有类之外声明的函数(也就是非成员函数),都具有文件作用域,只要在调用之前进行引用性声明(声明函数原型)即可,也可以声明函数原型会定义函数时用extern定义(可加可不加),效果一样。
通常情况下,变量和函数的定义都放在源文件中,而外部变量和外部函数的引用性声明则放在头文件中。
3)将变量和函数限制在编译单元
文件作用域中声明的变量和函数,默认情况下都可以被其他的编译单元访问,但有时候并不希望一个源文件中定义的文件作用域的变量和函数被其他源文件引用。这样由两个作用:1、出于安全性考虑;2、减少名字冲突;
解决办法就是使用匿名的命名空间:
namespace { //匿名的命名空间
int n;
voif f(){
n++;;
}
}