一、作用域
作用域(Scope)是什么?其实很简单,就是字面的意思。就是在哪个范围内可以应用。不同的行业都有作用域的概念,它们的概念或多或少的都有不同。即使在计算机编程语言中,不同的语言,作用域的概念也是有所不同的。比如有的语言没有类的概念,有的语言没有名空间。同样,同一个语言在不周的时期或者说不同的版本本也作用域也会有不同,这个都需要开发者引起注意。
作用域一个重要作用就是控制可见性和编程单元(变量、类等)的生命周期,这个非常重要。毕竟,可见性和生命周期都能直接影响代码的安全性和健壮性。
二、C++中的作用域
重点分析一下C++中的作用域。按不同的划分可以有多种的作用域划分:
1、按整体划分
可分为全局作用域和局部作用域。这个好理解,整体作用域是指在整体的可见范围内都可以应用;而局部作用域则只能指定的局部范围内应用。请一定注意,这个全局和局部是相对的,不是绝对的。比如一个文件可能是一个全局,也可能是一个局域,这看程序的设计和结构。
2、按代码定义划分
可分为名空间作用域、类作用域和块(函数)作用域。前二者的划分非常好理解,毕竟都是有明显的特征控制范围的。但块作用域可能对不少程序员理解起来有点麻烦。主要原因在于块作用域的标记{}这对大括号的功能不断的增加变化,有可能让小白们最头疼。同时它代表函数时,内部往往会嵌套,就更让初学者难受。但只要把握了作用域的特点,应用起来还是比较容易看清楚的。
3、按文件划分
如果说整体局部是一种逻辑的存在,代码是内部显示定义的存在,那么文件来划分可以理解成一种形式上类似于物理的隔绝。开发者可以使用static定义静态的作用域,让编程单元只能在本文件中使用。
可能如果按其它的维度划分还有更细的划分情况,这里就不再一一展开。不同的划分情况适应不同的场景,它们之间是综合应用的,不是必然单独一种的。比如一个全局变量可以增加static就成为了一个文件作用域内的全局变量。另外可见性和生命周期与作用域也未必一一对等,比如一个局部静态变量,它的可见性只在局部域内,但生命周期却是全生命周期的。如此等等,开发者一定要引起重视。
作用域其实是接口划分和封装的一个重要的保障,既要保障编程单元的合理可见性和生命周期的安全性(比如使用RAII等),又要保持在某些特殊情况下的连续可见性和全生命周期性(如单例等)。但一个整体的原则是在合理的情况下,作用域要尽可能的小。毕竟作用域越大,被其它单元污染的可能性越大。这种污染,如果只在极少的情况下发生时,往往难于发现和解决。
三、设计中的应用
大家熟知的安全措施诸如加密、接口控制以及最常见的用户名和密码,其实也是一种作用域的控制,只不过它是针对应用层。而这里提到的作用域,是针对代码层的。在前面分析了很多种设计的情况,更多的是倾向于框架的设计。哪怕是到一个函数,一个类,侧重的是由外向内的划分。而作用域,侧更倾向于从内向外划分,即一般先有一个定义单元,然后再考虑它的应用范围。
或者也可以理解为前面的设计更倾向于逻辑上的划分,而作用域更倾向于内容的划分。当然,二者一定不是严格区分的,站在不同的角度上,当然也会有不同的看法。
那么在实际的应用中,就要在具体的场景下考虑是由上至下还是由下至上,或由内到外还是由外到内哪种更便捷、简单,更容易扩展。当然更有可能的是综合应用,不同的层次下或不同的模块中采用不同的方式。
作用域和粒度有相通之处,又有很大的不同。但作用域不光是城墙,还有城门。作用域有可能会需要延展到另外一个作用域,或者说作用域可以通过某种实现达到不同的作用域内。给大家举一个不太恰当的例子,大家就更清楚了,一个函数内的类变量作用返回值返回时,如果开启了优化,则有可能会直接在调用方构造,编译器其实是主动打破了作用域的范围。
打破这种作用域的方法很多,比如刚刚提到的static,lambada表达式的捕获,引用传递等。
也就是说,一定要弄清楚作用域与可见性、生命周期的关系。
基于上面的分析,在设计中就可以能过作用域来灵活的控制编程单元的可见性,并基于不同的情况利用其生命周期来实现一些设计模式和具体的应用。例如刚刚提到的RAII,就是利用局部类变量在析构时释放相关的资源来实现的。
同样,Pimpl也是一种安全的可访问实现,诸如这些通过作用域控制来实现功能的在C++中很多,大家可以留心总结一下。
四、例程
下面看一个简单的例程:
c
#define flour1 4
constexpr short flour2 = 5;//全局 作用域
static short s_tee = 0;//文件作用域
void testDraw(Point a/*函数原型声明作用域*/);
class A {
public:
void Display() {//函数作用域
std::cout << "this is test!" << std::endl;
this->Print();
}
//类作用域
void Print() { std::cout << "this is print!" << std::endl; }
};
namesapce OK{//名空间作用域
int a = 0;
}
void testInteritance(){//块作用域
Parent p;//局部作用域
p.eat(100);
InheritanceExample ie;
ie.eat(200);
}
int main() {
testInteritance();
auto ret = BigEndian();
A *a = nullptr;
a->Display();
return 0;
}
诸如RAII和单例等的例子,大家翻看前面的例程即可,此处不再重复赘述。
五、总结
简单的东西,往往意味着灵活。因为简单,所以应用的广泛,应用的广泛就意味着应用的场景丰富。不同的场景下的应用就有可能有细节的不同。而细节的丰富恰恰是C++的一个特点,也是为广大开发者觉得不容易把握的地方。
作用域看似简单,但它和代码编写直接结合了起来。往往代码的水平就可以通过作用域的控制窥见一斑。而作用域还有一个特点,即使设计的较差甚至非常差,在一些中小程序中对程序的运行也不会有什么影响。往往就会让大多数中低程序员将其忽视。
简单不代表容易把握,切记!