1 标识符的作用域与可见性
作用域讨论的是标识符的有效范围,可见性讨论标识符是否可以被引用。
1-1 作用域
作用域是一个标识符在程序正文中有效的区域。
1-1-1 函数原型作用域
在函数原型声明时形式参数的作用范围就是函数原型作用域。
在double area(double radius);
中,标识符radius的作用范围在函数area形参列表的左右括号之间。
1-1-2 局部作用域
函数形参列表中形参的作用域,从形参列表中的声明处开始,到整个函数体结束之处为止。
函数体内声明的变量,其作用域从声明处开始,一直不对劲声明所在的块结束的花括号为止。
具有局部作用域的变量也称为局部变量。
1-1-3 类作用域
类可以被看成是一组有名成员的集合,类 X的成员 m 具有类作用域,对m 的访问方式有如下3种。
- 如果在X的成员函数中没有声明同名的局部作用域标识符,那么在该函数内可以直接访问成员m。也就是说m在这样的函数中都起作用。
- 通过表达式x.m或者 X::m。这正是程序中访问对象成员的最基本方法。X::m 的方式用于访问类的静态成员
- 通过 ptr>m这样的表达式其中 ptr 为指向X类的一个对象的指针。
1-1-4 文件作用域
不在前述各个作用域中出现的声明,就具有文件作用域,这样声明的标识符其作用域开始于声明点,结束于文件尾。
1-1-5 命名空间作用域
生活中存在重名现象,在 C++ 应用程序中,也存在同名变量、函数和类等情况,为避免重名冲突,使编译器能够区分来自不同库的同名实体,C++ 引人了命名空间的概念,它本质定义了实体所属的空间。命名空间定义使用 namespace 关键字,声明方式如下:
namespace namespace_name{
//代码声明
}
使用某个命名空间中的函数、变量等实体,需要命名空间::实体名称或通过using namespace namespace_name的方式。
1-2 可见性
程序运行到某一点,能够引用到的标识符,就是该处可见的标识符。
作用域可见性的一般规则是:
- 标识符要声明在前,引用在后。
- 在同一作用域中,不能声明同名的标识符。
- 在没有互相包含关系的不同的作用域中声明的同名标识符,互不影响。
- 如果在两个或多个具有包含关系的作用域中声明了同名标识符.则外层标识符在内层不可见。
2 生存期
2-1 静态生存期
如果对象的生存期与程序的运行期相同,我们称它具有静态生存期。
- 文件作用域中声明的对象都是具有静态生存期的。
- 在函数内部的局部作用域中声明具有静态生存期的对象需要使用关键字static。
局部作用域中静态变量的特点是,它并不会随着每次函数调用而产生一个副本,也不会随着函数返回而失效,也就是说,当一个函数返回后,下一次再调用时。该变量还会保持上回的值。即使发生了递归调用,也不会为该变量建立新的副本,该变量会在各次调用间共享。
在定义静态变量的同时也可以为它赋初值,例如:static int i=5;
这表示i会被以5初始化,而非每次执行函数时都将赋值为5。
细节:定义时未指定初值的基本类型静态生存期变量,会被以0值初始化,而对于动奇生存期变量,不指定初值意味着初值不确定。
2-2 动态生存期
除了上述两种情况其余的对象都具有动态生存期。在局部作用域中声明的具有动态生存期的对象,习惯上也被称为局部生存期对象。局部生存期对象诞生于声明点,结束于声明所在的块执行完毕之时。
3 类的静态成员
3-1 静态数据成员
如果某个属性为整个类所共有,不属于任何一个具体对象,则采用static 关健字来声明为静态成员。静态成员在每个类只有一份,由该类的所有对象共同维护和使用,从而实现了同一类的不同对象之间的数据共享。类属性是描达类的所有对象共同特征的一个数据项,对于任何对象实例,它的属性值是相同的。
静态数据成员具有静态生存期。
class Point{
static int i;
};
int Point::i=1;
3-2 静态函数成员
静态成员函数可以直接访问该类的静态数据和函数成员。而访问非静态成员,必须通过对象名。
一般情况下,它主要用来访问同一个类中的静态数据成员,维护对象之间共享的数据。
static 类型说明符 静态函数名();//声明
类型说明符 类名::静态函数名(){};//定义
类名::静态函数名();//调用
class Point{
static int i;
static void show(){};
};
void show(){
cout<<i<<endl;
}
int main(){
Point::show();
}
4 类的友元
友元关系提供了不同类或对象的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。
4-1 友元函数
友元函数是在类中用关键字friend修饰的非成员函数。在它的函数体中可以通过对象名访问类的私有和保护成员。
class Point{
public:
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));
}
4-2 友元类
若A类为B类的友元类,则A类的所有成员函数都是B类的友元函数,都可以访问B类的私有和保护成员。
注意:
- 友元关系是不能传递的。
- 友元关系是单向的。
class B
{
...
friend class A;
...
}
5 共享数据的保护
5-1 常对象
常对象的数据成员值在对象的整个生存期间内不能被改变。也就是说,常对象必须进行初始化,而且不能被更新。
声明常对象的语法形式为:const 类型说明符 对象名;
但写成类型说明符 const 对象名;
也是允许的,不过人们还是习惯把const写在前。
例如:const A a(3,4);
而为了保证常对象的值不被改变,语法限制了常对象的数据成员不能被赋值且常对象无法调用普通的成员函数(因为成员函数可能会改变数据成员的值)。那常对象没有成员函数可用还有什么用呢?这就跟常成员函数有关了。
5-2 用const修饰的类成员
5-2-1 常成员函数
使用const关键字修饰的函数为常成员函数,常成员函数声明的格式如下:
类型说明符 函数名(参数表) const;
class R{
public:
R(int r):r(r){}
void print();
void print() const;
private:
int r;
};
void R::print(){
cout<<r<<endl;
}
void R::print() const{
cout<<r<<endl;
}
int main(){
R a(5);
const R b(4);
a.print();//调用void print()
b.print();//调用void print() const
return 0;
}
5-2-2 常数据成员
使用const说明的数据成员为常数据成员。
常数据成员只能通过初始化列表来获得初值。
常数据成员声明的格式如下:
const 类型说明符 数据成员名;
class A{
public:
A(int i);
private:
const int a;
};
A::A(int i):a(i){}
int main(){
A a(100);
}
5-3 常引用
如果在声明引用时用const修饰,被声明的引用就是常引用。常引用所引用的对象不能被更新。
常引用的声明形式如:const 类型说明符 & 引用名;
非const的引用只能绑定到一个普通的对象,不能绑定到常对象,但常引用可以绑定到普通对象也可以绑定到常对象。如:
int a=1;
const int b=2;
int &c=a;//正确
int &c=b;//错误
const int &d=a;//正确
const int &d=b;//正确
通过常引用访问该对象时都只能把该对象当作常对象,不能给其赋值,修改数据成员或调用它的非const成员函数。
6 多文件结构和编译预处理命令
6-1 C++程序的一般组织结构
一个项目至少划分三个文件:类定义文件(.h文件)、类实现文件(.cpp文件)、类的使用文件(*.cpp文件,主函数文件)
决定一个声明放在源文件还是头文件中的一般原则是:
- 将需要分配空间的定义放在源文件中,例如函数的定义、文件作用域中变量的定义等;
- 将不需要分配空间的声明放在头文件中,例如类声明、外部函数的原型声明、外部变量的声明、基本数据类型的声明等。
- 内联函数比较特殊,由于它的内容需要嵌入到每个调用它的函数之中,所以对于需要被多个编译单元调用的内联函数,它们的代码应该被各个编译单元可见,这些内联函数的定义应当出现在头文件中。
6-2 外部变量与外部函数
6-2-1 外部变量
如果一个变量除了在定义它的源文件中可以使用外,还能被其他文件使用,那么就称这个变量是外部变量。文件作用域中定义的变量默认都是外部变量,但在其他文件中如果需要使用这一变量,需要用extern关键字加以声明。
//源文件1
int i=3;
//源文件2
extern int i;
int main(){
cout<<i<<endl;
}
6-2-2 外部函数
在所有类之外声明的函数,都是具有文件作用域的,如果没有特殊说明,这样的函数都可以在不同的编译单元中被调用,只要在调用之前进行引用性声明即可。当然也可以在声明函数原型或定义函数时用extern修饰,其效果与不加修饰的默认状态是一样的。
6-2-3 将变量和函数限制在编译单元内
文件作用城中声明的变量和函数,在默认情况下都可以被其他的编译单元访问,但有时并不希望一个源文件中定义的文件作用域的变量和函数被其他源文件引用,这种需求主要是出于两个原因,一是出于安全性考虑,不希望将一个只会在文件内使用的内部变量或函数暴露给其他编译单元,就像不希望暴露一个类的私有成员一样;二是对于大工程来说,不同文件中的、只在文件内使用的变量名很容易重名,如果将它们都暴露出来,在连接时很容易发生名字冲突。
对这一问题有两种方法:
- 使用static关键字,起到和extern相反的作用。(不推荐)
- 使用匿名的命名空间
namespace {//匿名的命名空间
int n;
void f(){
n++;
}
}
6-3 编译预处理
1.#include指令
文件包含指令有两种格式:
- #include<文件名>
按标准方式搜索,文件位于系统目录的include子目录下 - #include"文件名"
首先在当前目录中搜索,若没有再按标准方式搜索。
2.#define指令和#undef指令
#define MYHEAD_H
定义MYHEAD_H
#undef MYHEAD_H
删除MYHEAD_H定义的宏
3.条件编译指令
#if 常量表达式1
程序段1//当常量表达式1非零时编译
#elif 常量表达式2//可以省略
程序段2//当常量表达式1为零,常量表达式2非零时编译
#elif 常量表达式3
程序段3//当常量表达式1为零,常量表达式2为零,常量表达式3非零时编译
...
#else//可以省略
程序段n//其他情况下编译
#endif
#ifdef 标识符
程序段1//当标识符被#define定义过时编译
#else//可以省略
程序段2
#endif
#ifndef 标识符
程序段1//当标识符未被#define定义过时编译
#else//可以省略
程序段2
#endif
4.defined操作符
defined是一个预处理操作符,而不是指令,因此不要以#开头。
defined操作符使用的形式为:defined(标识符)
如果标识符在此前经#define定义过且没经#undef删除,则上述表达式为非0,否则为0。