本章内容包括:
- 单独编译
- 存储持续性、作用域和链接性
- 定位new运算符
- 名称空间
C++也允许将组件函数放在独立的文件中。可以打拿督编译这些文件,然后将它们连接成可执行的程序。
C++编译器既编译程序,也负责链接器。
请不要将函数定义或变量声明放到头文件中。如果头文件中包含一个函数定义,有两个文件进行了include,那么一个程序中将包含同一个函数的两个定义(除非函数是inline)。
头文件中常包含的内容包括:
被声明为const的数据和内联函数有特殊的链接属性,因此可以放在头文件中,不会引起问题。
另外,在包含头文件时,如果文件名被包含在尖括号,比如
#include指令用来管理头文件,不应该使用include包含源代码文件。
在同一个文件中只能将同一头文件包含一次,但是很可能在不知情的状况下将头文件包含多次。使用预处理编译器指令#ifndef可以解决问题。
- #ifndef COORDIN_H_
- #define COORDIN_H_
-
- // place include file contents here
-
- #endif
-
编译器首次遇到该文件时,名称COORDIN_H_(一般用文件名加下划线)没有定义,此时编译器将会查看#ifndef和#endif之间的内容,并读取到定义#define一行。如果在同一文件中另一位置发现已经定义了COORDIN_H_,那么编译器就会跳到#endif后面的一行上。
使用这种方法并不能防止编译器将文件包含两次,只是会忽略掉第一次包含之外的所有内容。
C++每个编译器都会用自己的方式进行名称修饰,不同编译器创建的二进制模块可能会无法正确链接。所以在链接编译模块时,请确保所有对象文件或库都是由同一个编译器生成的。
C++使用三种(C++11为四种)不同的方式来存储数据,区别在于数据保留在内存中的时间。
作用域描述了名称在文件(翻译单元)的多大范围内可见。比如函数中定义的变量在该函数中可见。
链接性描述了名称如何在不同单元间共享。链接性为外部的名称可在文件之间共享,链接性为内部的名称只能有一个文件中的函数共享。自动变量的名称没有链接性,因为不能共享。
C++变量作用域有很多种。
作用域为局部的变量只在定义它的代码块(花括号括起来)中可用;作用域为全局(文件作用域)的变量在定义位置到文件结尾之间都可用;在名称空间中声明的变量的作用域为整个名称空间。
在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有磁链性。所以在不同函数中声明的相同名称的变量是独立的。
如果在代码块中声明了变量,在该代码块中又定义了新代码块,声明了相同的变量,那么新的定义会隐藏旧的定义,新定义可见,就定义暂时不可见,程序离开里代码块后,旧定义重新可见。
在旧版的C中,旧版本中的关键字auto,是于显示指出变量为自动存储的,但是用的人很少,所以在新版本C中用新的含义代替了老的使用方法。
自动变量和栈
C编译器运行时会对自动变量进行管理,留出一段内存,将其视为栈,用来管理变量的增减。
寄存器变量
关键字register最初是由C语言引入的,他建议编译器使用CPU寄存器来存储自动变量。
register int count_fast;
但是现在的register已经没有了作用,和以前的auto关键字作用是相同的。
C++为静态存储持续性变量提供了3种链接性:
静态变量名称的意义在于,静态变量在整个程序运行期间数目是不变的,因此程序不需要使用特殊的装置(比如栈)来进行管理。
编译器会将没有显式初始化的静态变量设置为0。
进行三种静态变量举例:
- ...
- int global = 100; // static, external linkage
- static int one_fle = 50; // static, internal linkage
- int main()
- {
- ...
- }
-
- void funct1(int n)
- {
- static int count = 0; // static , no linkage
- int llama = 0;
- ...
- }
-
- void funct2(int q)
- {
- ...
- }
三种静态变量在整个程序执行期间都存在。
5种变量存储方式
| 存储描述 | 持续性 | 作用域 | 磁链性 | 如何声明 |
|---|---|---|---|---|
| 自动 | 自动 | 代码块 | 无 | 在代码块中 |
| 寄存器 | 自动 | 代码块 | 无 | 在代码块中,使用register |
| 静态,无磁链性 | 静态 | 代码块 | 无 | 在代码块中,使用关键字static |
| 静态,外部磁链性 | 静态 | 文件 | 外部 | 不在任何函数内 |
| 静态,内部磁链性 | 静态 | 文件 | 内部 | 不在任何函数内,使用关键字static |
C++存在“单定义规则”,即变量只能有一次定义。
所以C++提供了两种变量声明:一种是定义声明,会给变量分配存储空间;一种是引用声明,不会分配空间,只是引用已有变量。
引用声明使用extern进行定义:
- double up; // 定义声明
- extern int blem; // 引用声明
- extern char gr = 'z'; // 定义声明,因为包含了初始化
-
- // 如果要在其他文件中使用另一个文件定义的变量,使用extern
-
- // file01.cpp
- int process_status = 0;
- int main()
- {
- ...
- }
-
-
- // file02.cpp
- extern int process_status;
- int work()
- {
- std::cout << ::process_status << endl;
-
- ...
-
- int process_status = 1;
- std::cout << process_status << endl;
-
-
- }
说明,定义与全局变量同名的局部变量后,局部变量将会隐藏全局变量。
C++提供了作用域解析运算符(::)。放在变量名前面时,表示该变量为全局版本变量。
外部存储尤其适用于表示常量数据。
将static作用于作用域为整个当前文件的变量时,磁链性为内部。
如果在一个文件中创建一个全局静态变量,另一个文件中想要创建一个同名变量,那么仅仅省略extern是不够的,这样会违反C++的单定义规则。
正确的做法是声明一个static变量,这样静态变量会隐藏常规外部变量。这样做,声明的变量磁链性为内部,不会违法单定义规则。
使用static作用于代码块内的变量,就可以创建无磁链性的局部变量。
变量会在函数运行之前存在,在函数运行结束之后也不会被回收。因此在两次函数调用之间,静态局部变量的值将会保持不变。
另外如果定义时进行初始化,只会在程序启动时进行一次初始化,之后再调用函数时,不会再次进行初始化。
存储说明符:
cv-限定符:
关键字volatile表示,即使程序代码没有对内存单元进行修改,值也可能会发生变化。比如硬件修改内容、两个程序共享数据等等。该关键字作用是为了改善编译器的优化能力。
关键字mutable表示,即使结构或类变量为const,其某个成员也可以被修改。
- struct data
- {
- char name[30];
- mutable int accesses;
- ...
- };
-
- const data veep = {"aiky",0,...};
-
- strcpy{veep.name, "john"}; //not allowed
- veep.accesses++; // allowed
关键字const。在默认情况下全局变量的链接性为外部的,但是const全局变量的链接性为内部的。会和static一样。
可以把const变量放到头文件中,由不同的文件包含。这时由于内部连接性,所以每个文件都会有自己的一组常量,而不是所有文件都共享一组常量。
如果希望某个常量的链接性为外部,那么可使用extern关键字:
extern const int states = 50; // definition with external linkage
此时常量为外部链接性定义。
函数和变量一样也有链接性,但是范围比较少。
由于C++和C默认不能再函数中定义另外一个函数,所以所有函数的存储持续性都自动为静态。
链接性上,函数默认链接性为外部,可以在文件之间共享。也可以使用extern关键字来指出函数为另一文件中定义,可选。
也可以使用static将函数的链接性默认为内部链接。此时意味着该函数只在当前文件中可见,其他文件中可以定义同名函数。
函数大多只能有一个定义,内联函数除外,所以内联函数可以放在头文件之中。
C语言中,一个名称只对应一个函数,spiff翻译成_spiff。
但是C++中,一个名称可能对应多个函数,spiff(int)可能翻译成_spiff_i,spiff(double,double)可能翻译成_spiff_d_d。
那么此时如果在C++程序寻找C中的函数,就找不到了。
为了解决这种问题,可以使用函数原型来指出要使用的约定。
- extern "C" void spiff(int); // use C protocol for name look-up
- extern void spoff(int); // use C++ protocol for name look-up
- extern "C++" void spaff(int); // use C++ protocol for name look-up
C和C++的链接性是C++标准指定的说明符,但实现可提供其他语言链接性的说明符。
C++为变量分配内存的5种方案是不适用于动态内存分配的。C++使用new,C使用malloc分配的内存为动态内存分配。
动态内存由new和delete控制,因此可以在一个函数中分配内存,在另一个函数中释放。
通常情况下,编译器使用三块独立的内存:一块用于静态变量(可再细分),一块用于自动变量,一块用于动态存储。
使用new运算符
C++98提供了内置类型的内存分配和初始化方法。
- int * pi = new int (6); // *pii set to 6
- double *pd = new double (99.9); // *pd set to 99.9
C++11提供了常规结构或者数组的初始化。
- struct where { double x; double y ; double z;};
- where * one = new where {2.5,5.3,7.2}; // C++11
- int * ar = new int [4] {2,4,6,7}; // C++11
-
- int * pin = new int {6}; // C++11
如果使用new失败,找不到请求的内存量。在之前C++会返回空指针,但现在会引发异常std::bad_alloc。
在C++内部中,new会调用void * operator new(std::size_t); 而new []会调用void * operator new[](std::size_t);这些函数被称为分配函数。delete会调用 void operator delete(void *); 而delete[]会调用 void operator delete[] (void *);这些函数被称为释放函数。
在代码中调用new和delete关键字时会被替换:
- int * pi = new int ;
- // 会被转化为
- int * pi = new(sizeof(int));
-
- int * pa = new int[40];
- // 会被转化为
- int * pa = new(40 * sizeof(int));
-
-
- delete pi;
- // 会被转化为
- delete (pi);
C++将这些函数称为可替换,可以根据需求,对其进行定制和替换函数。
通常,new会在堆中找到一个符合条件的内存块。new运算符还有另一种变体,被称为定位new运算符。
使用定位特性,需要包含头文件
- #include
- struct staff
- {
- char dross[20];
- int slag;
- };
-
- char buffer1[50];
- char buffer2[500];
- int main()
- {
- chaff *p1, *p2;
- int *p3, *p4;
-
- // first ,the regular forms of new
- p1 = new chaff; // place structure in heap
- p3 = new int[20]; // place int array in heap
-
- // now , the two forms of placement new
- p2 = new (buffer1) chaff; //place structure in buffer1
- p4 = new (buffer2) int[20]; // place int array in buffer2
- ...
- }
示例中,使用两个静态数组来为new运算符提供内存空间。从buffer1中分配空间给chaff;从buffer2中分配空间给int数组。
但是不能够使用delete进行内存释放,因为buffer是静态内存,delete只能够用于new分配的堆内存。
但如果buffer是使用new来创建的,就可以通过使用delete来释放了。
定位new运算符的另一种用法是,初始化后,将信息放在特定的硬件地址位置。
标准定位new会调用一个接收两个参数的new()函数:
- int * p2 = new int; // -> new(sizeof(int))
- int * p2 = new (buffer) int; // -> new(sizeof(int), buffer)
- int * p3 = new (buffer) int[40]; // -> new(40*sizeof(int), buffer)
随着项目越来越大,名称相互冲突的可能性也在增加。
使用多个厂商的类库时,可能会导致名称冲突。这种冲突被命名为名称空间问题。
C++提供了名称空间工具,方便更好的控制作用域。
声明区域:是可以在其中进行声明的区域。
潜在作用域:从声明点开始,到声明区域的结尾。潜在作用域比声明区域小。
C++新增功能,通过定义一种新的声明区域来创建命名的名称空间。
名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。
- namespace Jack {
- double pail;
- void fetch();
- int pal;
- struct Well { ... };
- }
-
- namespace Jil{
- double bucket(double n){ ... }
- double fetch;
- int pal;
- struct Hil {...};
- }
另一种名称空间——全局名称空间。对应于文件级声明区域。
名称空间是开放的,可以将新的名称加入到已有的名称空间中。也可以在一个文件中,使用名称空间Jil提供函数原型,然后在另一个文件中使用名称空间为其提供定义。
需要有一种方法来访问给定名称空间的名称,最简单的方式是,通过作用域解析运算符::
比如:
Jack::pail = 12.34;
Jill::Hill mole;
未被装饰的名称被称为未限定名称;包含名称空间的名称被称为限定名称。
using声明和编译指令
using声明能够使特定标识符可用,using编译指令能够使整个名称空间可用。
- // using声明 --------------
- // file1
- namespace Jill{
- double bucket(double n) { ... }
- double fetch;
- struct Hill { ... };
- }
-
- // file2
- char fetch;
- int main()
- {
- using Jill::fetch;
- cin >> fetch ; //read a value into Jill::fetch
- cin >> ::fetch; //read a value into global fetch
- }
-
-
-
- // using编译指令-------------
- using namespace Jack; // make all the names in Jack available
using编译指令不能同时使用,会导致二义性。
通常情况下,使用using声明会比编译指令更加安全。
名称空间的其他特性
可以将名称空间进行嵌套
- namespace elements
- {
- namesapce fire
- {
- int flame;
- ...
- }
- float water;
- }
-
- // 此时以下命令都是有效的
- using namespace elements::fire;
-
- elements::fire::flame = 10;
-
- // 在命名空间内也是可以包含using编译指令和using声明的
- namespace myth
- {
- using Jill::fetch;
- using namespace elements;
- using std::cin;
- }
using编译指令是可以传递的,如果A op B and B op C,那么A op C
未命名的名称空间
通过省略名称空间的名称来创建未命名的名称空间。
- namespace // unnamed namespace
- {
- int ice;
- int bandycoot;
- }
不能够在未命名名称空间所属文件之外的其他文件中,使用该名称空间中的名称。可以作为static的替代品。
当前的一些指导原则: