10、名字控制
在C++中有更多的机制让你控制名字的创建、可见性、名字存储的内存及如何链接。
Staic无论表示内存的物理位置还是文件中的可见性,其基本意义为保持其原有位置。
C中的静态成员
无论是C还是C++,static都具备两个基本意义,如下:
在固定的位置只分配一次内存,对象创建在静态数据区中而非堆栈中,这就是静态内存的概念。
对特定单元可见,static控制者名字的可见性,其同时描述了链接的概念,指明了链接器可以见到的名字。
函数内部的静态变量
当在函数内部创建局部变量时,若需要在下次调用时保存其上次的值,可以将其声明为全局的,但这样扩大了该变量的可见性;因此在函数内部可以用static声明变量。
//: C10:StaticVariablesInfunctions.cpp
#include "../require.h"
#include <iostream>
using namespace std;
char oneChar(const char* charArray = 0) {
static const char* s;
if(charArray) {
s = charArray;
return *s;
}
else
require(s, "un-initialized s");
if(*s == '/0')
return 0;
return *s++;
}
char* a = "abcdefghijklmnopqrstuvwxyz";
int main() {
// oneChar(); // require() fails
oneChar(a); // Initializes s to a
char c;
while((c = oneChar()) != 0)
cout << c << endl;
} ///:~
对于函数内的局部变量,必须初始化,否则对于内部数据类型,编译器将自动设置其为0。
static char* s = 0;
函数中的静态类对象
用户定义的数据类型必须调用构造函数进行初始化。若不指定构造函数的参数时,将调用默认的构造函数。
//: C10:StaticObjectsInFunctions.cpp
#include <iostream>
using namespace std;
class X {
int i;
public:
X(int ii = 0) : i(ii) {} // Default
~X() { cout << "X::~X()" << endl; }
};
void f() {
static X x1(47);
static X x2; // Default constructor required
}
int main() {
f();
} ///:~
静态对象的析构函数
当main函数退出或者显式的调用exit时,将调用静态对象(即具备静态内存的对象)的析构函数。因此在析构函数中不能调用exit,否则将陷入无限循环。当调用aboot退出程序时将不调用静态对象的析构函数。
可以通过atexit函数指定当离开main或者调用exit时所要执行的动作。其在所有的析构函数之前运行。
//: C10:StaticDestructors.cpp
// Static object destructors
#include <fstream>
using namespace std;
ofstream out("statdest.out"); // Trace file
class Obj {
char c; // Identifier
public:
Obj(char cc) : c(cc) {
out << "Obj::Obj() for " << c << endl;
}
~Obj() {
out << "Obj::~Obj() for " << c << endl;
}
};
Obj a('a'); // Global (static storage)
// Constructor & destructor always called
void f() {
static Obj b('b');
}
void g() {
static Obj c('c');
}
int main() {
out << "inside main()" << endl;
f(); // Calls static constructor for b
// g() not called
out << "leaving main()" << endl;
} ///:~
Obj::Obj() for a
inside main()
Obj::Obj() for b
leaving main()
Obj::~Obj() for b
Obj::~Obj() for a
静态对象的析构与对象的初始化顺序正好相反;只有创建了的对象才会调用其析构函数,C++中保存了已经构造的对象和其初始化顺序。全局对象将在进入main之前构造,在main退出时析构。若包含静态局部对象的函数未被调用,则也不进行析构。
控制链接
通常文件级的名字即不是在函数或类内部定义的名字对程序中的所有单元都是可见的,这就是通常所谓的外部链接external linkage,因为链接的时候,名字对于连接器随处可见。全局变量和通常的函数都是外部链接的。
但有时候你想控制文件级变量的可见范围visibility,可用static修饰,此时文件内部的所有函数可以访问,但其他文件的函数不能访问该变量,且可以在其他文件中采用相同的名字并不会导致冲突和链接错误。此时的static变量是内部链接的,其好处之一在于该名字可以放在头文件中,而不必担心链接时的冲突。常见的例子如const变量和内联函数。但在C中,const仍然是外部链接的。链接只涉及到有地址的元素,因此类的声明和局部变量不存在链接问题。
疑惑
Static所涉及的visibility和storage两个特性有时是交织的。
文件级的全局变量默认是静态内存,但是是全局可见的。
int a = 0;
因此上面的定义相当于,extern指明了其可见性。
extern int a = 0;
若用static修饰,则其只对内部模块可见,且是内部链接的
static int a = 0;
无论用extern还是static修饰全局变量,其总是存储在静态数据区,改变的只是其可见性。
当涉及到局部变量时,static改变的不是可见性而是存储域了,若声明一个局部变量为extern,则表明其内存存在于别处。
//: C10:LocalExtern.cpp
//{L} LocalExtern2
#include <iostream>
int main() {
extern int i;
std::cout << i;
} ///:~
//: C10:LocalExtern2.cpp {O}
int i = 5;
///:~
对于函数而言,static和extern改变的只是可见性,因此
extern void f()和void f()是一样的,但
static void f()表明其只在本文件内部可见。
名字空间
在大型的项目中,不对全局的名字空间进行控制可能会引发问题,因此程序员通常建立复杂冗长的名字来避免冲突。
在C++中,你可以用namespace关键词来将名字空间分成可管理的小片。如struct、class、enum、union一样,关键词namespace也将其管理的名字放在一个特定地方,其唯一的作用就是建立一个新的名字空间。
名字空间的创建
其创建和class类似,但也有显著差别:
1、 名字空间的定义必须出现在全局范围或者是其他namespace中。
2、 其定义的反括号后无须分号“;”
3、 其定义可以在多个头文件中继续定义而非通常认为的重复定义,如“
//: C10:MyLib.cpp
namespace MyLib {
// Declarations
}
int main() {} ///:~
//: C10:Header1.h
#ifndef HEADER1_H
#define HEADER1_H
namespace MyLib {
extern int x;
void f();
// ...
}
#endif // HEADER1_H ///:~
//: C10:Header2.h
#ifndef HEADER2_H
#define HEADER2_H
#include "Header1.h"
// Add more names to MyLib
namespace MyLib { // NOT a redefinition!
extern int y;
void g();
// ...
}
#endif // HEADER2_H ///:~
//: C10:Continuation.cpp
#include "Header2.h"
int main() {} ///:~
4、 可以对namespace进行别名更改,以避免冗长的名字
5、 不能象类那样创建名字空间的实例instance
无名的名字空间
无名名字空间确保了对于每个文件是唯一的,对于空间内的所有名字,无须用static修饰即保证了其内部链接internal linkage的特性。C++鼓励使用无名名字空间,避免用static关键词。
//: C10:UnnamedNamespaces.cpp
namespace {
class Arm
{ /* ... */ };
class Leg
{ /* ... */ };
class Head { /* ... */ };
class Robot {
Arm arm[4];
Leg leg[16];
Head head[3];
// ...
} xanthan;
int i, j, k;
}
int main() {} ///:~
朋友
可以在一个类中进行朋友声明而将其加入到名字空间中,如:
//: C10:FriendInjection.cpp
namespace Me {
class Us {
//...
friend void you();
};
}
int main() {} ///:~
此时函数you即成为名字空间Me的成员。
若在全局的名字空间中引入friend到某个类中,friend就是全局的了。
使用名字空间
可以通过三种方式引用名字空间的的名字:
运用scope resolution指定某个名字;
运用using指示符引入名字空间中的所有名字;
运用using声明引入名字
scope resolution
与类的操作方式类似,必须加上namespace的名字来引用。
//: C10:ScopeResolution.cpp
namespace X {
class Y {
static int i;
public:
void f();
};
class Z;
void func();
}
int X::Y::i = 9;
class X::Z {
int u, v, w;
public:
Z(int i);
int g();
};
X::Z::Z(int i) { u = v = w = i; }
int X::Z::g() { return u = v = w = 0; }
void X::func() {
X::Z a(1);
a.g();
}
int main(){} ///:~
运用using指示符
一次可以引入某个namespace中的所有名字
//: C10:NamespaceInt.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 getSign() const { return s; }
void setSign(sign sgn) { s = sgn; }
// ...
};
}
#endif // NAMESPACEINT_H ///:~
//: C10:NamespaceMath.h
#ifndef NAMESPACEMATH_H
#define NAMESPACEMATH_H
#include "NamespaceInt.h"
namespace Math {
using namespace Int;
Integer a, b;
Integer divide(Integer, Integer);
// ...
}
#endif // NAMESPACEMATH_H ///:~
//: C10:Arithmetic.cpp
#include "NamespaceInt.h"
void arithmetic() {
using namespace Int;
Integer x;
x.setSign(positive);
}
int main(){} ///:~
局部的定义可以覆盖namespace中的名字,此时引用namespace中的名字时需要namespace的名字
//: C10:NamespaceOverriding1.cpp
#include "NamespaceMath.h"
int main() {
using namespace Math;
Integer a; // Hides Math::a;
a.setSign(negative);
// Now scope resolution is necessary
// to select Math::a :
Math::a.setSign(positive);
} ///:~
若同时引入多个namespace,而其中包含相同的部分,则在引用而非声明相同的部分时,会出现模棱两可的情况,不能够正确引用。
//: C10:NamespaceOverriding2.h
#ifndef NAMESPACEOVERRIDING2_H
#define NAMESPACEOVERRIDING2_H
#include "NamespaceInt.h"
namespace Calculation {
using namespace Int;
Integer divide(Integer, Integer); //与MATH中重复了
// ...
}
#endif // NAMESPACEOVERRIDING2_H ///:~
//: C10:OverridingAmbiguity.cpp
#include "NamespaceMath.h"
#include "NamespaceOverriding2.h"
void s() {
using namespace Math;
using namespace Calculation;
// Everything's ok until:
//! divide(1, 2); // Ambiguity
}
int main() {} ///:~
使用using声明
Using指示符是全局有效的,而using声明只在当前范围有效,因此其可以覆盖using指示符引入的名字,
//: C10:UsingDeclaration.h
#ifndef USINGDECLARATION_H
#define USINGDECLARATION_H
namespace U {
inline void f() {}
inline void g() {}
}
namespace V {
inline void f() {}
inline void g() {}
}
#endif // USINGDECLARATION_H ///:~
//: C10:UsingDeclaration1.cpp
#include "UsingDeclaration.h"
void h() {
using namespace U; // Using directive
using V::f; // Using declaration
f(); // Calls V::f();
U::f(); // Must fully qualify to call
}
int main() {} ///:~
using声明指示给出了完整的名字标识符,并没有类型信息,若名字空间中含有重载函数时,using声明将引入所有的重载函数。Using声明只是一个别名,他允许你在不同的namespace中声明相同的函数,但引入多个namespace时并不会出现冲突。
如何使用名字空间
关键之处在于不要使用全局的using指示符,在源文件中若出现名字冲突可以使用显式名字或者用using声明的形式,但是在头文件中,由于多个文件可能都包含,因此冲突时不便于更改。因此在头文件中不能使用全局的using指示符,而应该用显式名字或者局部using指示符和声明。
C++中的静态成员
当需要一个类所所有实例共享一个内存单元时,在C中可以通过全局变量来实现,但其不安全,一是可以随意修改,而是在大型工程中可能存在名字冲突。因此理想的情况是数据象全局变量一样存储,同时隐藏在类内部并且和类相关联,在C++中是通过static成员实现的。
为静态数据成员分配内存
编译器不会为静态数据成员自动分配内存,链接器若发现静态数据成员只声明未定义的时候会报错。因此定义必须在类的外部并且不能内联。通常放在类的定义文件中。
如下:
class A {
static int i;
public:
//...
};
int A::i = 1;
你可能奇怪,i不是private的么?没错,但初始化静态成员的唯一合法地方就是在类的定义文件中,另外只有定义之后用户才不会第二次定义,因为链接器会报错。这样就确保了只会定义一次且在类的创建者的掌控之中。
//: C10:Statinit.cpp
// Scope of static initializer
#include <iostream>
using namespace std;
int x = 100;
class WithStatic {
static int x;
static int y;
public:
void print() const {
cout << "WithStatic::x = " << x << endl;
cout << "WithStatic::y = " << y << endl;
}
};
int WithStatic::x = 1;
int WithStatic::y = x + 1;
// WithStatic::x NOT ::x
int main() {
WithStatic ws;
ws.print();
} ///:~
静态数组的初始化
在第八章中,可以通过定义静态的const变量实现类中的常量。静态对象的数组创建原则是一样的,
//: C10:StaticArray.cpp
// Initializing static arrays in classes
class Values {
// static consts are initialized in-place:
static const int scSize = 100;
static const long scLong = 100;
// Automatic counting works with static arrays.
// Arrays, Non-integral and non-const statics
// must be initialized externally:
static const int scInts[];
static const long scLongs[];
static const float scTable[];
static const char scLetters[];
static int size;
static const float scFloat;
static float table[];
static char letters[];
};
对于整型的静态const变量可以在类中定义,其他的必须在类外部定义。Const变量是内部链接的,因此可以放在头文件中。
可以在类中定义静态const对象,但不能有内联的方式去初始化。
//: C10:StaticObjectArrays.cpp
// Static arrays of class objects
class X {
int i;
public:
X(int ii) : i(ii) {}
};
class Stat {
// This doesn't work, although you might want it to:
//! static const X x(100);
// Both const and non-const static class
// objects must be initialized externally:
static X x2;
static X xTable2[];
static const X x3;
static const X xTable3[];
};
X Stat::x2(100);
X Stat::xTable2[] = {
X(1), X(2), X(3), X(4)
};
局部类
不能在局部类即函数内部定义的类中使用static成员变量。因为没有办法定义初始化之。
//: C10:Local.cpp
// Static members & local classes
#include <iostream>
using namespace std;
// Nested class CAN have static data members:
class Outer {
class Inner {
static int i; // OK
};
};
int Outer::Inner::i = 47;
// Local class cannot have static data members:
void f() {
class Local {
public:
//! static int i;
// Error
// (How would you define i?)
} x;
}
int main() { Outer x; f(); } ///:~
静态成员函数
静态成员函数服务于整个类而非类的具体对象,将其放在类中,避免了全局函数污染名字空间,同时又和某个特定的类联系起来了。
因为其属于整个类,因此对象也可以象通常那样通过“。”或“>”来调用静态成员函数,但更习惯用范围解析符“::”来调用之。
静态成员函数没有This指针,所有其不能访问非静态成员变量和非静态成员函数。因为没有This指针的传递,其获得了一般全局函数的效率同时又封装在类里面,提供了安全性。
//: C10:StaticMemberFunctions.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(); // OK -- calls static
}
};
int X::j = 0;
int main() {
X x;
X* xp = &x;
x.f();
xp->f();
X::f(); // Only works with static members
} ///:~
静态初始化的依赖关系
在单个文件中,静态对象的初始化顺序是和其定义的顺序一致的,析构则反之。但是在多个文件中,则链接器无法保证各个静态对象的初始化顺序。当静态对象的初始化存在依赖关系时可能会导致问题。
// First file
#include <fstream>
std::ofstream out("out.txt");
// Second file
#include <fstream>
extern std::ofstream out;
class Oof {
public:
Oof() { std::out << "ouch"; }
} oof;
如果先初始化oof,那么其构造时std::out尚未构造,则会出错。
又如,当存在相互依赖关系时,最后的值可能是不稳定的。链接装载机制确保在用户调用的任何动态初始化之前将所有静态对象初始化为0。对于其他类型来说,初始化为0可能没有意义,但对于内建类型来说,0也是一种初始化。
extern int y;
int x = y + 1;
and in a second file you have at the global scope:
extern int x;
int y = x + 1;
上述存在相互依赖关系,若第一个先调用,由于x的初始化需要y的值,因此此时在y尚未定义时将其值强制为0,x值为1;随后由于对x的依赖关系,y又动态初始化为2
如何来解决这种静态对象初始化的依赖关系呢?
尽量避免;
将关键的静态对象的定义放在一个文件中,这样就确保了其初始化顺序;
若必须在多个文件中,那么有两种技巧来解决。
这需要在库的头文件中增添一个类来专门处理类中静态变量的初始化顺序。
//: C10:Initializer.h
// Static initialization technique
#ifndef INITIALIZER_H
#define INITIALIZER_H
#include <iostream>
extern int x; // Declarations, not definitions
extern int y;
class Initializer {
static int initCount;
public:
Initializer() {
std::cout << "Initializer()" << std::endl;
// Initialize first time only
if(initCount++ == 0) {
std::cout << "performing initialization"
<< std::endl;
x = 100;
y = 200;
}
}
~Initializer() {
std::cout << "~Initializer()" << std::endl;
// Clean up last time only
if(--initCount == 0) {
std::cout << "performing cleanup"
<< std::endl;
// Any necessary cleanup here
}
}
};
// The following creates one object in each
// file where Initializer.h is included, but that
// object is only visible within that file:
static Initializer init;
#endif // INITIALIZER_H ///:~
//: C10:InitializerDefs.cpp {O}
// Definitions for Initializer.h
#include "Initializer.h"
// Static initialization will force all these values to zero:
int x;
int y;
int Initializer::initCount;
///:~
若多个文件都包含这个头文件,无论编译的先后顺序,x和y都只会初始化一次且顺序固定。
//: C10:Initializer.cpp {O}
// Static initialization
#include "Initializer.h"
///:~
//: C10:Initializer2.cpp
//{L} InitializerDefs Initializer
// Static initialization
#include "Initializer.h"
using namespace std;
int main() {
cout << "inside main()" << endl;
第二种技巧是利用了函数内部的静态变量只会在初次调用时对其进行初始化,且顺序固定,其只依赖于代码的设计
改变链接规则
C++中为了支持函数重载和类型安全检查,会对编译后的函数名字进行装饰,如生成_f_int_char之类的。因此在C++中无法利用C库函数,需要使用链接交换指导符extern,告诉C++不要进行装饰。
float f(int a, char b);
extern "C" float f(int a, char b);
extern "C" {
float f(int a, char b);
double d(int a, char b);
}
extern "C" {
#include "Myheader.h"
}
总结
Static关键词有时控制内存有时控制链接时的可见性,比较混乱。Namespace的引进使得你可以更优化的控制大型项目中的名字冲突问题。
类中的静态成员名字和全局的名字不会冲突。