十、名字控制(Name Control)
九、名字控制(Name Control)
引言
你可以控制名称的创建 和可见性 ,控制这些名称的 存储 位置,以及名称的连接性。
- 创建(Creation):指名称(变量、函数等)什么时候创建,以及如何被创建
- 可见性(Visibility):指在程序的哪些地方可以使用这个名字
- 存储(Storage):指这个名字的数据存放在哪里,以及存活多久
- 连接性(Linkage):指同一个名字是否能在多个文件之间共享,即文字的跨文件访问能力。例如:
extern
声明的变量可以在不同文件之间链接。static
声明的变量在文件内部有内部链接(internal linkage),外部文件访问不到
10.1 静态元素(Static elements)
概述
所谓的静态对象(静态元素)是指: 在程序运行期间,存储在静态存储区(static storage area),
生命周期贯穿整个程序的对象。
常见的静态对象有:
类型 | 示例 | 说明 |
---|---|---|
全局变量 | int x = 5; | 存在于整个程序 |
静态局部变量 | static int y = 3; | 函数内部static,记住上次值 |
类的静态成员变量 | static int count; | 属于整个类 |
namespace作用域的静态对象 | 同样是静态存储 | |
静态类对象 | static MyClass obj; | 构造一次,用到最后 |
-
关键字
static
有两个基本含义:- 对象被创建在一个特殊的静态数据区(static data area) ,而不是每次函数调用时创建的**栈(stack)**上,这就是静态存储(static storage) 的概念。
static
控制名字的可见性(visibility),使得这个名字无法被外界的翻译器单元或者类看到。这也涉及了连接性(linkage) 的概念,即决定链接器(linker)能看到哪些名字。
#include <iostream> using namespace std; void f() {static int n = 1;// static int n;int x = 1;//int x;cout << "n = " << n++;cout << ",";cout << "x = " << x++ << endl; } void main(){f();f(); }
输出:
n = 1,x = 1 n = 2,x = 1
一个翻译单元就是
一个源文件.cpp
加上它包含的所有头文件.h
展开后组成的整体,
编译器是以“翻译单元”为单位来编译程序的。如果一个内建类型的
static
变量没有显示初始化器,编译器会保证这个变量被初始化为0。而普通内建类型则不会。比如这里如果采取后面注释的写法
#include <iostream> using namespace std; void f() {static int n;int x;cout << "n = " << n++;cout << ",";cout << "x = " << x++ << endl; } void main(){f();f(); }
编译器可能报错也可能继续运行,我的编译器则报错
这是因为编译器不会给int x;自动初始化,而是会给static int n;自动初始化,所以报错是
使用了未初始化的局部变量x
。如果正常运行,那么
n
都会初始化为0,而x
会是一个垃圾值。
函数内部的静态类对象
- 如果没有提供初始化器,静态的**内建类型(built-in-types)**会被自动化为零。
- 但是静态的用户自定义类型(user-defined types),必须通过构造函数调用来初始化。
内建类型(built-int types)
就是C++语言本身就定义好的基础数据类型,不需要用户自己额外设计。
常见的内建类型有:
int
(整数)float
(浮点数)double
(双精度浮点数)char
(字符)bool
(布尔值)short
、long
、long long
等各种整数扩展unsigned int
(无符号整数)- 指针类型(比如
int*
、char*
)
用户自定义(users-defined types)
就是程序员自己定义的复杂数据类型,通常用class
、struct
、union
、enum
等关键字定义。
示例
//C10:StaticObjectsInFunctions.cpp
#include <iostream>
using namespace std;
class X {int i;
public:X(int ii = 0) :i(ii) {}//Default~X() { cout << "i = " << i << endl; }
};
void f() {static X x1(47);static X x2;
}
void main() {f();f();X x3(10);
}
输出:
i = 10 //x3
i = 0 // x2
i = 47 // x1
不加static
#include <iostream>
using namespace std;
class X {int i;
public:X(int ii = 0) :i(ii) {}//Default~X() { cout << "i = " << i << endl; }
};
void f() {static X x1(47);X x2;
}
void main() {f();f();X x3(10);
}
输出:
i = 0 //x2
i = 0 //x2
i = 10 //x3
i = 47 //x1
可见
static
可以将变量的生命周期变为整个程序
静态对象的析构函数
- 全局对象的构造函数总是进入
main()
函数之前调用。 - 函数内部静态对象,只有这些函数被调用时才执行。就是说,静态对象虽然定义在函数里,但它不会随着程序启动就创建,只有在函数第一次被调用的时候才创建,而且只创建一次!
- 第一次调用这个函数:对象才被真正构造(调用构造函数)。
- 后续再调用这个函数:不会重新构造,复用第一次构造好的静态对象。
- 当
main()
退出时,所有已经构造的对象的析构函数会按照构造的相反顺序被依次调用。
示例
#include <iostream>
using namespace std;
class X {int i;
public:X(int ii = 0) :i(ii) {}//Default~X() { cout << "i = " << i << endl; }
};
X x0(5);
void f(){static X x1(47);static X x2;
}
void main(){f();f();X x3(10);
}
输出:
i = 10
i = 0
i = 47
i = 5
控制链接性(Controling linkage)
- 可见性(Visibility):外部链接、内部链接
- 外部链接(External linkage):文件作用域中的名字,对程序中所有翻译单元(
.cpp
文件)都是可见的。全局变量和普通函数具有外部链接性。 - 内部链接(Internal linkage,又叫文件静态):名字只在它所属的翻译单元 中可见。
static
、const
和inline
声明的名字默认具有内部链接性。
- 外部链接(External linkage):文件作用域中的名字,对程序中所有翻译单元(
- 不同存储类型(Storage type)的内存分配:静态数据区(static data area)、栈(stack)、堆(heap)
- 静态数据区:用于存储全局变量、静态变量
- 栈:用于局部变量
- 堆:通过
new
和delete
动态分配的内存
10.2 命名空间(Namespaces)
概述
-
尽管名字可以嵌套在类内部,但全局函数、全局变量和类的名字依然属于一个统一的全局命名空间。
-
static
关键字允许你对这种情况进行一定程度的控制,它可以让变量和函数具有内部链接性。但是,在大型项目中,如果无法有效管理全局命名空间,仍然会引发问题。
-
我们可以使用C++中的
namespace
特性,将全局命名空间划为更易管理的小块区域。
10.2.1 创建命名空间
//C10:MyLib.cpp
namespace MyLib{//members
}
void main(){}
与class
的区别:
-
namespace
只能出现在全局作用域,或者嵌套在另一个命名空间内部。 -
在闭合大括号
}
后,不需要加分号;
-
命名空间的名字(比如MyLib)可以在多个头文件中使用
-
命名空间的名字可以起别名,比如:
namespace MyLib = Lib;
-
不能创建命名空间的实例
10.2.2 使用命名空间
- 作用域解析运算符 是
::
using
指令:引入命名空间中的所有名字。using
声明:一次性引入命名空间的一个名字
示例:作用域解析运算符 ::
//ScopeResolution.cpp
namespace X
{class Y{static int i;public:void f();};class Z;void func();
}int X::Y::i = 9;
void X::Y::f(){}
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();
}
void main(){}
示例:using
指令
#include <iostream>
using namespace std;namespace calculator{double Add(double x,double y){return x+y;}void Print(double x){cout << x << endl;}
}using namespace calculator;//Using 指令
void main(){double a,b;cin >> a >> b;Print(Add(a,b));
}
如果命名空间内函数和其他函数重名
#include <iostream>
using namespace std;namespace calculator
{double Add(double x, double y) { return x + y; }void Print(double x) { cout << "Result is " << x << endl; }
}void Print(double x)
{cout << "This is an external function." << endl;
}using namespace calculator;void main()
{double a, b;cin >> a >> b;Print(Add(a, b)); // —— 这里调用 Print 时发生了歧义(ambiguous calling)
}
-
正确的调用方式:
calculator::Print(Add(a,b));
::Print(Add(a,b))
分表指明是命名空间calculator的
Print
还是全局函数Print
-
重要提醒:永远不要在头文件(header file)里使用
using namespace
指令!因为这会引起严重的名字冲突问题。原因:头文件(
.h
文件)是可以被很多不同源文件(.cpp
文件)包含 (#include
)的一旦你在头文件里写了:
using namespace std;
或者
using namespace calculator;
那么所有包含了这个头文件的
.cpp
文件,都会自动引入这个命名空间!这就可能导致严重的名字冲突问题!
示例:using
声明
- 当一个名字在命名空间外经常使用时,如果每次都写上命名空间会很麻烦。这时可以使用"using"来简化
#include <iostream>
using namespace std;
namespace calculator
{double Add(double x, double y) { return x + y; }void Print(double x) { cout << "Result is " << x << endl; }
}void main()
{using calculator::Add; // 使用声明,只引入 calculator 命名空间里的 Add 函数double a, b;cin >> a >> b;calculator::Print(Add(a, b)); // 使用 Add,不用再写 calculator::,但 Print 还是要写完整名字
}
- 这里只引入了
Add
,而没有把整个calculator
命名空间引入 - 所以用
Add(a,b)
可以直接写,而Print
仍然需要些calculator::Print(……)
10.3 静态成员(Static members)
概述
- 有时候我们需要一个单独的存储空间,供一个类的所有对象共同使用
- 虽然全局变量也可以做到,但全局变量不安全,因为:
- 任何人都能修改它。
- 在大型项目中,名字容易和其它全局冲突。
- 更好的做法是:
- 使用类内部的静态数据成员。
- 它们像全局变量一样共享一份数据,但作用域限制在类内部,受类的保护。
- 静态成员可以是public、private或protected,并且属于类本身,而不是某个特定对象。
10.3.1 静态数据成员
- 静态成员 是在类中声明,但会被类的所有对象共享。
- 静态数据成员必须进行初始化,并且需要遵循以下规则:
- 在类体外部初始化
- 初始化时不要再次写
static
关键字 - 初始化时需要类名限定
class Mycalss{static int obj;//静态数据成员
};//类外初始化
int Mycalss::obj = 8; //初始化
10.3.2 静态成员函数
- 使用
static
关键字声明。 - 只能直接访问静态数据成员
- 没有隐含的
this
指针 - 可以是
inline
函数
#include <iostream>
using namespace std;
class Myclass {
public:static void show();
private:static int obj;int x;
};
int Myclass::obj = 8;
void Myclass::show() {cout << obj << endl;//cout << x;//error
}
void main() {Myclass m;m.show();Myclass::show();
}
10.4 静态初始化顺序
- 在同一个翻译单元(specific translation unit)内,静态对象的初始化顺序保证按照对象定义出现的顺序进行。
- 而且,销毁程序保证是初始化顺序的逆序。
- 但是,跨翻译单元的静态对象初始化顺序没有任何保证。
- 解决方案:把静态对象的定义放在同一个源文件中。
比如说
// A.cpp
#include <iostream>
extern int b; // 声明B.cpp的b
int a = b + 1; // a的初始化依赖b
// B.cpp
#include <iostream>
extern int a; // 声明A.cpp的a
int b = a + 1; // b的初始化依赖a
问题:
- 编译器不能保证A.cpp和B.cpp谁先初始化!
- 有可能
b
初始化时,a
还没准备好,结果就成了未定义行为!
所以,正确示范:把a
和b
都写到一个.cpp
文件里
// AB.cpp
#include <iostream>int b = 1; // 先初始化b
int a = b + 1; // 再初始化aint getA() { return a; }
int getB() { return b; }