Chapter 10. Name Control
一、局部变量作为static
// c10: static_objects_inside_funcs.cpp
#include <iostream>
using namespace std;
class X {
int i;
public:
X(int ii = 0):i(ii) {}
~X() { cout << "X::~X()" << endl; }
};
void f()
{
static X x1(47);
static X x2;
}
int main()
{
f();
} ///:~
/* result:
X::~X()
X::~X()
*/
当局部变量声明为static的时候,此变量将不会放置在堆栈区内,而是放置在程序的静态内
存区。这就意味着static变量:
* 具有全局生命期
* 初始化只进行一次
* 此变量在声明函数外部不可见
来解析一下静态对象的析构方法:
// c10: static_destructors.cpp
// static object destructors
#include <fstream>
using namespace std;
ofstream out("statdest.out");
class obj {
char c;
public:
obj(char cc): c(cc) {
out << "obj::obj() for " << c << endl;
}
~obj() {
out << "obj::~obj() for " << c << endl;
}
};
obj a('a');
void f()
{
static obj b('b');
}
void g()
{
static obj c('c');
}
int main()
{
out << "inside main()" << endl;
f();
//g() not called
out << "leaving main()" << endl;
}
/* statdest.out:
obj::obj() for a
inside main()
obj::obj() for b
leaving main()
obj::~obj() for b
obj::~obj() for a
*/
静态变量具有全局生命期,这样静态对象会在main()函数退出时候,或者调用C标准函数
exit()的时候被自动调用析构方法。这意味着:如果你在静态对象的析构方法内exit,就会
造成一个死循环。这是很危险的。另外,调用abort()函数结束程序,将不会自动调用静态
对象的析构方法。
这个例子解释了一些基本概况,a是全局对象,b是f()内部的静态对象,他们的构造和析构
顺序正好相反:
cons a -> cons b -> des b --> des a
(在C++中,全局静态对象的构造函数是在main()之前被调用的)
二、全局变量为static
local_extern.cpp -- 第一个模块(main)
// c10: local_extern.cpp
#include <iostream>
int main()
{
extern int i;
std::cout << i << endl;;
}
/* result:
5
*/
local_extern2.cpp -- 第二个模块
// c10: local_extern2.cpp
int i = 5;
static 和 extern在 C++中是一对反义词。
* 默认全局变量是extern的,extern关键词标明次变量包含外部链接,可以在其他模块中引
用。而static则约束此变量为内部链接,不可外部引用。这对于“非成员”函数也同样适用。
file scope:
1) 全局变量声明默认为extern,而声明为static只会更改可视范围,这样变量只包含内部
链接————它仍旧会位于静态数据区域,无论static还是extern。
2) 局部变量声明默认为auto,若声明为static则会更改可视范围,并且更改存储区域(从
堆栈区更改到静态数据区);若声明为extern,则显示此变量位于别的模块。
3) 方法定义为static/extern,只会改变可视范围。
三、名字空间 namespaces
// c10: continuation.cpp
namespace mylib {
extern int x;
void f();
}
namespace mylib {
extern int y;
void g();
}
int main() {}
* 名字空间的定义必须在全局区内,或者被嵌套在其他名字空间内。
* 定义的末尾没有";"
* 可以在多个头文件里添加名字空间定义(原本看起来像重复定义)
* 名字空间可以被引用为其他名字,比如
namespace bob = bobs_super_duper_library;
* 对名字空间内部数据的引用类似于成员操作
* 你不能使用名字空间建立实例,他根本不是一个类型
四、using 指令
namespace_int.h -- 定义名字空间Int
// c10: namespace_int.h
#ifndef __NAMESPACEINT_H
#define __NAMESPACEINT_H
namespace Int {
enum sign { positive, negative };
class Integer {
int i;
sign s;
public:
Integer(int ii = 0): i(ii),
s(i >= 0 ? positive : negative)
{}
sign get_sign() const { return s; }
void set_sign(sign sgn) { s = sgn; }
}
}
#endif /// __NAMESPACEINT_H
namespace_math.h -- 定义名字空间Math
// c10: namespace_math.h
#ifndef __NAMESPACEMATH_H
#define __NAMESPACEMATH_H
#include "namespace_int.h"
namespace Math {
using namespace int;
Integer a, b;
Integer divide(Integer, Integer);
}
#endif /// __NAMESPACEMATH_H
arithmetic.cpp
// c10: arithmetic.cpp
#include "namespace_math.h"
void arithmetic()
{
using namespace Int;
Integer x;
x.set_sign(positive);
}
int main() {}
在arithmetic()的方法中,我们看到了using指令,如果没有他,任何命名空间的内容都需
要全名提供。
五、C++中的static数据成员
// c10: static_init.cpp
#include <iostream>
using namespace std;
int x = 100;
class with_static {
static int x;
static int y;
public:
void print() const {
cout << "with_static::x = " << x << endl;
cout << "with_static::y = " << y << endl;
}
};
int with_static::x = 1;
int with_static::y = x + 1;
// with_static::x not ::x
int main()
{
with_static ws;
ws.print();
}
/*
with_static::x = 1
with_static::y = 2
*/
类的static成员存在于一个独立的空间中,无论建立多少个对象,他们的static成员都是共
享的。更重要的是static成员只有在类对象中可见,它也可以受成员访问符约束。
在此例子中,在没有创建实例的时候,我们就对with_static::x进行初始化,这对于静态成
员是可行的。
// c10: static_array.cpp
class values {
static const int scsize;// = 100;
static const long sclong = 100;
static const int scints[];
static const long sclongs[];
static const float sctable[];
static const char scletters[];
static int size;
static const float scfloat; // differ from tic++
static float table[];
static char letters[];
};
int values::scsize = 100;
int values::size = 100;
const float values::scfloat = 1.1;
const int values::scints[] = { 99, 47, 33, 11, 7 };
const long values::sclongs[] = { 99, 47, 33, 11, 7 };
const float values::sctable[] = { 1.1, 2.2, 3.3, 4.4 };
const char values::scletters[] = { 'a','b','c','d','e','f','g','h','i','j',};
float values::table[4] = { 1.1, 2.2, 3.3, 4.4 };
char values::letters[10] = { 'a','b','c','d','e','f','g','h','i','j',};
int main() { values v; }
这里揭示了static初始化的可行性分析:
1) static 不可以在类中初始化,必须要求const。
2) static const 非原子类型,不可以在类中初始化。
3) static const 原子类型,可以而且必须在类中初始化。
下面来看嵌套类定义和局部类定义中的static情况————
// c10: local.cpp
// static member & local classes
#include <iostream>
using namespace std;
// Nested classes CAN have static data members;
class outer {
class inner {
static int i;
};
};
int outer::inner::i = 47;
//local class can not have static data members;
void f()
{
class local {
public:
//! static int i; // error
// (how could you define i?)
} x;
}
int main()
{
outer x;
f();
}
你可以发现嵌套类定义中的static完全可行,但是在局部类定义中却被禁止了。
我们怎么办呢?实际上,局部类定义是很少使用的。
六、static成员方法
// c10: static_member_funcs.cpp
class X {
int i;
static int j;
public:
X(int ii = 0): i(ii) {
// non-static member function can
// access static member function or data
j = i;
}
int val() const { return i; }
static int incr() {
//! i++; // Error: static member function
// cannot access non-static member data
return ++j;
}
static int f() {
//! val(); // Error: static member function
// cannot access non-static member function
return incr();
}
};
int X::j = 0;
int main()
{
X x;
X *xp = &x;
x.f();
xp->f();
X::f(); // only works with static members
} ///:~
同样,我们在C++中可以像定义成员数据那样定义一个static成员方法,在我们需要创建一
个仅仅作用于类或者对象的方法的时候,我们不希望他在全局区域内造成命名污染,因此,
这个念头就诞生了。
static方法可以直接被引用调用,不需要建立类实例(就像数据成员初始化一样)。有一点
要注意的是:static成员方法,他并不包含this指针,这注定了它:
* 不能够访问数据成员,也不能调用成员方法。
// c10: singleton.cpp
#include <iostream>
using namespace std;
class egg {
static egg e;
int i;
egg(int ii): i(ii) {}
egg(const egg&); // prevent copy-construction
public:
static egg* instance() { return &e; }
int val() const { return i; }
};
egg egg::e(47);
int main()
{
//! egg x(1); // private constructor
cout << egg::instance()->val() << endl;
}
这个例子非常有趣,除了egg::e,你将永远无法创造第二个egg对象。
1) 因为egg的构造函数设置为私有,你无法直接创建私有对象。
2) 使用static成员方法egg::instance(),你可以创建一个实例,这个实例是
static egg e;
3) 由于egg e本身也是e,那么所有的 instance()调用,都会返回这一个对象。其他对象创
建不能。
4) 此例还禁用了拷贝构造方法 egg(const egg&),这意味着,你甚至无法赋值对象,最后
一条路也被封锁了。
七、static 初始化依赖 static initialization dependency
initializer.h
// c10: initializer.h
#ifndef __INITIALIZER_H
#define __INITIALIZER_H
#include <iostream>
extern int x;
extern int y;
class initializer {
static int init_count;
public:
initializer() {
std::cout << "initializer()" << std::endl;
//initialize first time only
if (init_count++ == 0) {
std::cout << "performing initialization"
<< std::endl;
x = 100;
y = 200;
}
}
~initializer() {
std::cout << "initializer()" << std::endl;
//cleanup last time only
if (--init_count == 0) {
std::cout << "performing cleanup"
<< std::endl;
// any necessary cleanup here
}
}
};
static initializer init;
#endif//__INITIALIZER_H
initializer_defs.cpp -- 已知模块
// c10: initializer_defs.cpp
#include "initializer.h"
int x;
int y;
int initializer::init_count;
initializer.cpp -- 测试模块(main)
// c10: initializer.cpp
#include "initializer.h"
using namespace std;
int main()
{
cout << "inside main()" << endl;
cout << "leaving main()" << endl;
}
/* result:
initializer()
performing initialization
initializer()
inside main()
leaving main()
initializer()
initializer()
performing cleanup
*/
在extern的初始化过程中,我们会遇到这样的问题:
extern std::ofstream out;
class Oof {
public:
Oof() { std::out << "ouch"; }
} Oof;
这段代码引用到了其他文件的一个对象ofstream out,如果两个文件位于不同的模块,那么
这意味着out的定义和使用位于两个模块。在编译器编译的时候,往往不能完全决定哪一个
模块先编译,操作系统也没有提供确保初始化顺序的绝对方法。如果是此段代码先编译,那
么面对的问题就是,out还未构造就使用了,这会带来混乱。
这个问题针对的对象是(它们会在main()之前执行初始化):
1) 全局对象
2) 局部static对象
为了解决此问题的发生,我们给出三种方法:
1) 永远别这样做,避免此类问题发生是最好的选择
2) 如果必须这样做,把静态对象定义放在一个文件里,这样你可以安排预想的编译次序。
3) 如果不可避免的要把静态对象放在不同模块中,可以采用两种技巧来解决此问题。
本段例子就是技巧1,它是Jerry Schwarz在创建iostream库的时候提出的。
initializer.h *设计目标
initializer_defs.cpp *已知模块
initializer.cpp *已知模块
我们要求的目标是,无论initializer_defs.cpp和initializer.cpp哪一个先初始化,都可
以保证x,y的安全使用。
实现方法:首先在initializer.h中,extern引用x,y,然后定义为x,y专门的初始化类
initializer,根据static成员int_count是否为0,判断x,y是否已经被初始化,如果未初始
化,则实现初始化;否则不要动。以同样的方法定义析构方法。接着在文件末尾添加static
对象initializer init。
最后一步,在x,y对象定义的模块初始化 initializer::init_count = 0;
由于已知模块都已经包含了initializer.h头文件,那么他们将共同拥有init对象,init对
象保证了对象x,y的初始化;由于class initializer的处理,也将屏蔽x,y的重复初始化。
下面介绍技巧2————
dependency1.h
// c10: dependency1.h
#ifndef __DEPENDENCY1_H
#define __DEPENDENCY1_H
#include <iostream>
class dependency1 {
bool init;
public:
dependency1(): init(true) {
std::cout << "dependency1 construction"
<< std::endl;
}
void print() const {
std::cout << "dependency1 init:"
<< init << std::endl;
}
};
#endif//__DEPENDENCY1_H
dependency2.h
// c10: dependency2.h
#ifndef __DEPENDENCY2_H
#define __DEPENDENCY2_H
#include "dependency1.h"
class dependency2 {
dependency1 d1;
public:
dependency2(const dependency1 &dep1): d1(dep1) {
std::cout << "dependency2 construction ";
print();
}
void print() const { d1.print(); }
};
#endif//__DEPENDENCY2_H
dependency1statfun.h
// c10: dependency1statfun.h
#ifndef __DEPENDENCY1STATFUN_H
#define __DEPENDENCY1STATFUN_H
#include "dependency1.h"
extern dependency1 &d1();
#endif//__DEPENDENCY1STATFUN_H
dependency2statfun.h
// c10: dependency2statfun.h
#ifndef __DEPENDENCY2STATFUN_H
#define __DEPENDENCY2STATFUN_H
#include "dependency2.h"
extern dependency2 &d2();
#endif//__DEPENDENCY2STATFUN_H
dependency1statfun.cpp
// c10: dependency1statfun.cpp
#include "dependency1statfun.h"
dependency1 &d1()
{
static dependency1 dep1;
return(dep1);
}///:~
dependency2statfun.cpp
// c10: dependency2statfun.cpp
#include "dependency1statfun.h"
#include "dependency2statfun.h"
dependency2 &d2()
{
static dependency2 dep2(d1());
return(dep2);
}
technique2b.cpp
// c10: technique2b.cpp
// (L) dependency1statfun dependency2statfun
#include "dependency2statfun.h"
int main()
{
d2();
}///:~
/* result:
dependency1 construction
dependency2 construction dependency1 init:1
*/
这个方法显然更加干净清爽:)
dependency1.h * 已知头文件
dependency2.h * 已知头文件
dependency1statfun.h * 设计头文件
dependency2statfun.h * 设计头文件2
dependency1statfun.cpp * 设计模块
dependency2statfun.cpp * 设计模块2
technique2b.cpp *main函数,用来测试
我们发现class dependency2的对象实例必须通过拷贝构造函数来初始化,而且他需要借助
class dependency1的对象。因此以下的初始化总是可能存在的:
dependency2 dep2((dependency1)dep1);
如果dep1和此定义不再同一模块中,我们显然要面对静态对象的依赖问题。
技巧2是这样作的。他通过定义两个模块提供了两个函数d1()和d2(),他们分别包含一个静
态对象dep1和dep2(d1),如果要创建dependency2对象,只有通过d2()返回得到。实际上,
技巧2正是封锁了初始化方法,迫使初始化按照即定的顺序进行。
dependency2& -> d2() -> dep2(d1()) -> d1() -> dep1 -> dependency1: dependency1()
总结
我们看到static带来功能性的同时,也带来了困惑,因为在某些情况下,它用来控制存储位
置,有些时候控制可视范围或者名字的链接属性。
* static存储在静态数据内存区,拥有全局生命域
* 作用在局部变量上,static使得变量局部可见却拥有全局生命期。
* 作用在全局变量上,static控制可视范围,仅仅在本模块内可见。
* 作用在类的数据成员上,static既控制生命期,又限制该标识符仅仅在类中可见。
* 作用在类的成员方法上,static方法除了保留数据成员特性以外,还去除了指针。这意味
着,静态成员不能访问非static数据成员,也不能调用非static成员方法。
namespace 同样给予更加灵活的方法,让您在大工程内部控制代码的增加和繁衍。
class 内部的class是在程序中控制名字的另外一种方法,他不会和全局名字冲突,而在程
序内部享用独立的可视范围和访问控制。这会大大增强你代码的维护性能。
没有评论:
发表评论