和C语言一样,C++也允许甚至鼓励程序员将组件函数放在独立的文件中。可以单独编译这些文件,然后将它们链接成可执行的程序。(通常,C++编译器即编译程序,也管理链接器。)如果只修改了一个文件,则可以只重新编译该文件,然后将它与其他文件的编译版本链接。
UNIX 和 linux 系统都具有 make 程序,可以跟踪程序依赖的文件以及这些文件的最后修改时间。运行 make 时,如果它检测到上次编译后修改了源文件,make 将记住重新构建程序所需的步骤。大多数集成开发环境都在project菜单中提供了类似的工具。
头文件中常包含的内容:
将结构声明放在头文件中是可以的,因为它们不创建变量,而只是在源代码文件中声明结构变量时,告诉编译器如何创建该结构变量。同样,模板声明不是将被编译的代码,它们指示编译器如何生成与源代码中的函数调用相匹配的函数定义。被声明为const的数据和内联函数有特殊的链接属性,因此可以将其放在头文件中,而不会引起问题。
注意,在包含头文件时,使用"headfile.h",而不是
警告:在IDE中,不要将头文件加入到项目列表中,也不要在源代码文件中使用 #include 来包含其它源代码文件。
头文件管理:
#ifndef COORDIN_H_
#define COORDIN_H_
..
#endif
由于C++标准允许每个编译器设计人员以他认为合适的方式实现名称修饰。在链接编译模块时,请确保所有对象文件或库都是由同一个编译器生成的。如果有源代码,通常可以用自己的编译器重新编译源代码来消除链接错误。
C++11使用4种不同的方案来存储数据,这些方案的区别就在于数据保留在内存中的时间。
作用域(scope)描述了名称在多大范围内可见。
例如,函数中定义的变量可在该函数中使用,但不能在其它函数中使用;而在文件中的函数定义之前定义的变量可在所有函数中使用。
链接性(linkage)描述了名称如何在不同单元间共享。
链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量的名称没有链接性,因为它们不能共享。
C++ 变量的作用域有多种。作用域为局部的变量只在定义它的代码块中可用。代码块是由花括号括起的一系列语句。例如函数体就是代码块,但可以在函数体中嵌入其他代码块。作用域为全局(也叫文件作用域)的变量在定义位置到文件尾之间都可用。自动变量的作用域是局部,静态变量的作用域是全局还是局部取决于它是如何被定义的。在函数声明中使用的名称只在包含参数列表的括号内可用(这就是为什么这些名称是什么以及是否出现都不重要的原因)。在类中声明的成员的作用域为整个类。在名称空间中声明的变量的作用域为整个名称空间(由于名称空间已经引入到C++语言中,因此全局作用域是名称空间作用域的特例)。
C++函数的作用域可以是整个类或整个名称空间(包括全局的),但不能是局部的(因为不能在代码块内定义函数,如果函数的作用域为局部,则只对它自己是可见的,因此不能被其他函数调用。这样的函数将无法运行)。
不同的C++存储方式是通过存储持续性、作用域和链接性来描述的。
在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。当程序开始执行这些变量所属的代码块时,将为其分配内存;当函数结束时,这些变量都将消失。
如果在代码块中定义了变量,则该变量的存在时间和作用域将被限制在该代码块内。
如果内部代码块定义了和外部代码块同名的变量,那么在执行内部代码块时,该名称的变量将被解释为内部代码块定义的那个。可以形象地说,内部代码块的定义隐藏了以前的定义,在程序离开内部代码块时,原来的定义又重新可见。
可以使用任何在声明时其值为已知的表达式来初始化自动变量。
程序使用两个指针来跟踪栈,一个指针指向栈底——栈的开始位置,另一个指针指向栈顶——下一个可用内存单元。当函数被调用时,其自动变量将被加入到栈中,栈顶指针指向变量后面的下一个可用的内存单元。函数结束时,栈顶指针被重置为函数被调用前的值,从而释放新变量使用的内存。
关键字 register 最初是由C语言引入的,它建议编译器使用 CPU 寄存器来存储自动变量:
register int count_fast; // request for a register variable
这旨在提高访问变量的速度。
在 C++11 之前,这个关键字在 C++ 中的用法始终未变,只是随着硬件和编译器变得越来越复杂,这种提示表明变量用得很多,编译器可对其做特殊处理。在 C++11 中,这种提示作用也失去了,关键字 register 只是显式地指出变量是自动的。保留关键字 register 的重要原因是,避免使用了该关键字的现有代码非法。
C++为静态存储持续性变量提供了3种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块中访问)。
这3种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。
由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的装置(比如栈)来管理它们。编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。
另外,如果没有显式地初始化静态变量,编译器将把它们设置为0。在默认情况下,静态数组和结构将每个元素的所有位都设置为0。
下面介绍如何创建这3种静态持续变量,然后介绍它们的特点。
要创建链接性为外部的静态持续变量,必须在代码块的外面声明它;
要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用关键字static;
要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static限定符。
...
int gloabal = 1000; // static duration, external linkage
static int one_file = 50; // static duration, internal linkage
...
void func(int n){
static int count = 0; // static duration, no linkage
}
...
静态变量的初始化
除默认的零初始化外,还可对静态变量进行常量表达式初始化和动态初始化。
零初始化即将变量设置为零,对于标量类型,零将被强制转换为合适的类型。例如,在C++中,空指针用0表示,但内部可能采用非零表示,因此指针变量零初始化时将被设置为空指针相应的内部表示。结构成员被零初始化,且填充位都被设置为零。
零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译文件时初始化变量。动态初始化意味着变量将在编译后初始化。
首先,所有静态变量都被零初始化,然后,如果编译器仅根据文件内容就可计算表达式,编译器将执行静态初始化;但如果编译器没有足够的信息,变量将被动态初始化。
比如:
#include
const double pi = 4.0 * atan(1.0); // dynamic initialization
要初始化pi,必须调用函数atan(),这需要等到atan()被链接且程序被执行时。
但注意,使用sizeof运算符的表达式也可以是常量表达式,所以下面的语句执行的是静态初始化:
int enough = 2 * sizeof(long) + 1; // constant expression initialization
C++11新增了关键字 constexpr,这增加了创建常量表达式的方式。但本书不会更详细地介绍 C++11 新增的这项功能。
单定义规则
一方面,在每个使用外部变量的文件中,都必须声明它;另一方面,根据“单定义规则”,变量只能有以此定义。为满足这种需求,C++提供了两种变量声明,一种是定义声明(defining declaration),它给变量分配存储空间;另一种是引用声明,它不给变量分配存储空间,因为它引用已有的变量。引用声明使用关键字 extern,且不进行初始化;否则,声明为定义,导致分配存储空间:
double up; // definition, up is 0;
extern int blem; // blem defined elsewhere
extern char gr = 'z'; // definition because initialized
如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义(单定义规则),但在使用该变量的其他所有文件中,都必须使用关键字 extern 声明它:
// file01.cpp
extern int cats = 20; // definition because of initialization
int dogs = 22; // also a definition
int fleas; // also a definition
...
// file02.cpp
extern cats; // not definitions they use
extern dogs; // extern and have no initialization
...
// file03.cpp
extern int cats;
extern int dogs;
extern int fleas;
file02.cpp没有重新声明fleas,因此无法访问它。在file01.cpp中,省略关键字 extern 效果也相同。
另外,还可以在函数块内使用关键字extern来声明定义过的外部变量,如果不使用extern,则函数块内新定义的同名变量会隐藏外部变量。但相比于使用extern来声明外部变量然后使用,更好和更安全的选择是使用作用域解析运算符(:😃。
全局变量和局部变量
全局变量很有吸引力——因为所有的函数都能访问全局变量,因此不用传递参数。但易于访问的代价很大——程序不可靠。计算经验表明,程序越能避免对数据进行不必要的访问,就越能保持数据的完整性。通常情况下,应使用局部变量,应在需要知晓时才传递数据,而不应不加区分地使用全局变量来使数据可用。OOP在数据隔离方面又向前迈进了一步。
然而,全局变量也有它们的用处。例如,可以让多个函数使用同一个数据块。外部存储尤其适合常量数据,可以使用关键字 const 来防止数据被修改。
将static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。
如果要在其他文件中使用相同的名称来表示其他变量,只省略关键字extern是不可行的,因为它违反了单定义原则,还需要加上static以表示链接性为内部,只作用于所在文件。
静态持续家族中的第三个成员——无链接性的局部变量。这种变量是这样创建的,将 static 限定符用于在代码块中定义的变量。在代码块中使用 static 时,将导致局部变量的存储持续性为静态的。这意味着虽然该变量只在该代码块中可用,但它在该代码块处于不活动状态时仍然存在。因此在两次函数调用之间,静态局部变量的值将保持不变。另外,如果初始化了静态局部变量,则程序只在启动时进行一次初始化。以后再调用该函数时,将不会像自动变量那样再次被初始化。
static 被用在作用域为整个文件的声明中时,表示内部链接性;被用于局部声明中,表示局部变量的存储持续性为静态的。
extern 表明是引用声明,即声明引用在其他地方定义的变量。
thread_local 指出变量的持续性与其所属线程的持续性相同。thread_local 变量之于线程,犹如常规静态变量之于整个程序。
mutable的含义将根据const来解释
const;
volatile;
cv表示const和volatile。最常用的cv-限定符是const。
关键字volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。听起来似乎很神秘,实际上并非如此。例如,可以将一个指针指向某个硬件位置,其中包含了来自串行端口的时间或信息。在这种情况下,硬件(而不是程序)可能修改其中的内容。或者两个程序可能互相影响,共享数据。
该关键字的作用是为了改善编译器的优化能力。例如,假设编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在这两次使用之间不会变化。如果不将变量声明为 volatile,则编译器将进行这种优化;将变量声明为 volatile,相当于告诉编译器,不要进行这种优化。
mutable
现在回到 mutable。可以用它来指出,即使结构(或类)变量为 const,其某个成员也可以被修改。例如,请看下面的代码:
struct data{
char name[30];
mutable int accesses;
...
}
const data veep = {"Claybourne Clodde", 0, ... };
strcpy(veep.name, "Joye Joux"); // not allowed
veep.accesses++; // allowed
veep的const限定符禁止程序修改 veep 的成员,但 access 成员的 mutable 说明符使得 access 不受这种限制。
本书不使用 volatile 或 mutable,但将进一步介绍 const。
再谈 const
在C++中,const限定符对默认存储类型稍有影响——在默认情况下全局变量的链接性为外部的,但 const 全局变量的链接性为内部的。也就是说,在 C++ 看来,全局 const 定义(如下述代码所示)就像使用了 static 说明符一样。
const int fingers = 10; // same as static const int fingers = 10;
int main(){
...
假设将一组常量放在头文件中,并在同一个程序的多个文件中使用该头文件。那么,预处理器将头文件的内容包含到每个源文件中后,所有的源文件都将包含类似下面的定义:
const int fingers = 10;
const char * warning = "Wak!";
然而,由于外部定义的 const 数据的链接性为内部的,因此可以在所有文件中使用相同的声明。
内部链接性还意味着,每个文件都有自己的一组常量,而不是所有文件共享一组常量。每个定义都是其所属文件私有的。
这就是能够将常量定义放在头文件中的原因。这样,只要在两个源代码文件中包括同一个头文件,则它们将获得值相同但地址不同的常量。
出于某种原因,程序员希望某个常量的链接性为外部的,则可以使用 extern 关键字来覆盖默认的内部链接性:
extern const int states = 50; // definition with external linkage
在这种情况下,必须在所有使用该常量的文件中使用 extern 关键字来声明它。但由于定义和声明都使用了extern关键字,需要特别注意只有一个文件可对其进行初始化。
在函数或代码块中声明 const 时,其作用域为代码块,即仅当程序执行该代码中的代码时,该常量才是可用的。这意味着在函数或代码块中创建常量时,不必担心其名称与其他地方定义的常量发生冲突。
和C语言一样,C++ 不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都为静态的,即在整个程序执行期间都一直存在。在默认情况下,函数的链接性为外部的,即可以在文件间共享。实际上可以在函数声明中使用关键字 extern 来指出函数是在另一个文件中定义的,不过这是可选的(要让程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译,或者是由链接程序搜索的库文件)。还可以使用关键字 static 将函数的链接性设置为内部的,使之只能在一个文件中使用。必须在声明和函数定义中都使用该关键字:
static int private(double x);
...
static int private(double x){
...
}
这意味着该函数只在这个文件中可见,还意味着可以在其他文件中定义同名的函数。和变量一样,在定义静态函数的文件中,静态函数将覆盖外部定义,因此即使在外部定义了同名的函数,该文件仍将使用静态函数。
单定义原则也适用于非内联函数,因此对于每个非内联函数,程序只能包含一个定义。对于链接性为外部的函数来说,这意味着在多文件程序中,只能有一个文件(该文件可能是库文件,而不是程序员编写的)包含该函数的定义,但使用该函数的每个文件都应包含其函数声明。
内联函数不受这项规则的约束,这允许程序员能够将内联函数的定义放在头文件中。这样,包含了头文件的每个文件都有内联函数的定义。然而,C++ 要求同一个函数的所有内联定义都必须相同。
C++在哪里查找函数?
假设在程序的某个文件中调用一个函数,C++ 将到哪里去寻找该函数的定义呢?如果该文件中的函数声明指出该函数是静态的,则编译器将只在该文件中查找函数定义;否则,编译器(包括链接程序)将在所有的程序文件中查找。如果找到两个定义,编译器将发出错误消息,因为每个外部函数只能有一个定义。如果在程序文件中没有找到,编译器将在库中搜索。这意味着如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本,而不是库函数(然而,C++保留了标准库函数的名称,即程序员不应使用它们)。有些编译器-链接程序要求显式地指出要搜索哪些库。
另一种形式的链接性——称为语言链接性(language linking)也对函数有影响。首先介绍一些背景知识。链接程序要求每个不同的函数都有不同的函数名。在C语言中,一个名称只对应一个函数,因此这很容易实现。为满足内部需要,C语言编译器可能将 spiff 这样的函数名翻译为 _spiff。这种方法被称为C语言链接性(C language linkage)。但在 C++ 中,同一个名称可能对应多个函数,必须将这些函数翻译为不同的符号名称。因此C++编译器执行名称修饰,为重载函数生成不同的函数名称。例如,可能将 spiff(int) 转换为 _spoff_i,而将spiff(double,double)转换为_spiff_d_d。这种方法被称为C++语言链接(C++ language linkage)。
链接程序寻找与C++函数调用匹配的函数时,使用的方法与 C 语言不同。但如果要在 C++ 程序中使用 C 库中预编译的函数,将出现什么情况呢?例如,假设有下面的代码:
spiff(22); // want spiff(int) from a C library
它在 C 库文件中的符号名称为_spiff,但对于我们假设的链接程序来说,C++ 查询约定是查找符号名称_spiff_i。为解决这种问题,可以用函数声明来指出要使用的约定:
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++ 链接性是 C++ 标准指定的说明符,但实现可提供其他语言链接性的说明符。
动态内存由运算符 new 和 delete 控制,而不是由作用域和链接性规则控制。因此,可以在一个函数中分配动态内存,而在另一个函数中将其释放。与自动内存不同,动态内存不是LIFO,其分配和释放顺序要取决于 new 和 delete 在何时以何种方式被使用。通常,编译器使用三块独立内存:一块用于静态变量(可能再细分)、另一块用于动态存储。
虽然存储方案概念不适用于动态内存,但适用于用来跟踪动态内存的自动和静态指针变量。例如,假设在一个函数中包含下面的语句:
float * p_fees = new float [20];
由 new 分配的 80 个字节(假设 float 为 4 个字节)的内存将一直保留在内存中,直到使用delete运算符将其释放。但当包含该声明的语句块执行完毕时,p_fees 指针将消失。如果希望另一个函数能够使用这80个字节中的内容,则必须将其地址传递或返回给该函数。另一方面,如果在函数外声明 p_fees ,则文件中位于该声明后面的所有函数都可以使用它。另外,通过在另一个文件中使用下述声明,便可在其中使用该指针:
extern float * p_fees;
注意:在程序结束时,由 new 分配的内存通常都将被释放,不过情况也并不总是这样。例如,在不那么健壮的操作系统中,在某些情况下,请求大型内存块将导致该代码块在程序结束不会被自动释放。最佳的做法是,使用 delete 来释放 new 分配的内存。
使用 new 运算符初始化
如果要初始化动态分配的变量,该如何办呢?在 C++98中,有时候可以这样做,C++11 增加了其他可能性。
如果要为内置的标准类型(如int或double)分配存储空间并初始化,可在类型名后面加上初始值,并将其用括号括起:
int *pi = new int (6); // *pi set to 6
double *pd = new double (99.99); // *pd set to 99.99
这种括号语法也可用于有合适构造函数的类,这将在本书后面介绍。
然而,要初始化常规结构或数组,需要使用大括号的列表初始化,这要求编译器支持 C++ 11. 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
在 C++11 中,还可将列表初始化用于单值变量:
int *pin = new int {6}; // *pin set to 6
double * pdo = new double {99.99}; // *pdo set to 99.99
new 失败时
new 可能找不到请求的内存量。在最初的 10 年,C++ 在这种情况下让 new 返回空指针,但现在将引发异常 std::bad_alloc。
new: 运算符、函数和替换函数
运算符 new 和 new[] 分别调用如下函数:
void * operator new(std::size_t); // used by new
void * operator new(std::size_t); // used by new[]
这两个函数被称为分配函数,它们位于全局名称空间中。同样,也有由 delete 和 delete[]调用的释放函数:
void operator delete(void *);
void operator delete[](void *);
它们使用第11章将讨论的运算符重载语法。std::size_t 是一个 typedef,对应于合适的整型。对于下面这样的基本语句:
int * pi = new int;
将被转换为下面这样:
int *pi = new(sizeof(int));
而下面的语句:
int *pa = new int[40];
将被转换成下面这样:
int *pa = new(40*sizeof(int));
正如您知道的,使用运算符 new 的语句也可包含初始值,因此,使用 new 运算符时,可能不仅仅是调用 new() 函数。
同样,下面的语句:
delete pi;
将转换为如下函数调用:
delete(pi);
有趣的是,C++ 将这些函数称为可替换的(replaceable)。这意味着如果您有足够的知识和意愿,可为 new 和 delete 提供替换函数,并根据需要对其进行定制。例如,可定义作用域为类的替换函数,并对其进行定制,以满足该类的内存分配需求。在代码中,仍将使用 new 运算符,但它将调用您定义的 new() 函数。
定位 new 运算符
通常,new 负责在堆(heap)中找到一个足以能够满足要求的内存块。new 运算符还有另一种变体,被称为定位new运算符,它让您能够指定要使用的位置。程序员可能使用这种特性来设置其内存管理规程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象。
要使用定位new特性,首先需要包含头文件 new,它提供了这种版本的 new 运算符的声明;然后将 new 运算符用于提供了所需地址的参数。除需要指定参数外,句法与常规 new 运算符相同。具体地说,使用定位 new 运算符时,变量后面可以有方括号,也可以没有。下面的代码段演示了 new 运算符的4种用法:
#include
struct chaff{
char dross[20];
int slag;
};
char buffer1[50];
char buffer2[500];
int main(){
chaff *p1, *p2;
int *p3, *p4;
p1 = new chaff; // place structure in heap
p3 = new int[20]; // place int array in heap
p2 = new (buffer1) chaff; // place structure in buffer1
p4 = new (buffer2) int[20]; // place in array in buffer2
}