1. 单独编译
C++允许甚至鼓励程序员将组件函数放在独立的文件中。另外,大多数C++环境都提供了其他工具来帮助管理。
提供了#include
来处理文件代码的包含问题。程序可以分成三部分:
- 头文件:包含结构声明和使用这些结构的函数的原型
- 源代码文件:包含与结构有关的函数的代码
- 源代码文件:包含调用与结构相关的函数的代码
头文件中常包含的内容:函数原型;使用#define
或const
定义的符号常量;结构声明;类声明;模板声明;内联函数。
如果文件名包含在尖括号中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;
如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或者源代码目录。
// coordin.h -- structure template and function prototype
// structure templates
#ifndef COORDIN_H_
#define COORDIN_H_
struct polar{
double distance;
double angle;
};
struct rect{
double x;
double y;
};
// prototype
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);
#endif
在同一个文件中只能将同一个头文件包含一次。
避免多次包含同一个头文件的标准C/C++技术:预处理器编译指令#ifndef
。
C++标准允许每个编译器设计人员以他认为合适的方式实现名称修饰,因此由不同编译器创建的二进制模块(对象代码文件)很可能无法正确的链接。
2. 存储持续性、作用域和链接性
C++使用三种(C++11是四种)不同的方案存储数据,这些方案的区别在于数据保留在内存中的时间:
- 自动存储持续性:函数定义中声明的变量。它们在程序开始执行其所属函数或代码块时被创建,执行完成后它们使用的内存被释放。C++有2中存储持续性为自动的变量。
- 静态存储持续性:函数定义外定义的变量和使用关键字
static
定义的变量。它们在程序整个运行过程都存在。C++有3中存储持续性为静态的变量。 - 线程存储持续性(C++11)
- 动态存储持续性:用
new
运算符分配的内存。有时被称作自由存储或堆。
2.1 作用域(scope)和链接(linkage)
作用域描述了名称在文件(翻译单元)的多大范围内可见。
链接性描述了名称如何在不同的单元之间共享。
C++变量的作用域有多种。
- 局部:只在定义它的代码块中可用
- 全局(文件作用域):在定义位置到文件结尾都可用。
- 函数原型作用域:只在包含参数列表的括号内可用
- 类中声明的成员作用域:整个类
- 名称空间中声明的变量作用域:整个名称空间
C++函数的作用域可以是整个类或者整个名称空间,但不能是局部的。
2.2 自动存储持续性
默认情况下,函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。
如果在代码块中定义了变量,则变量的存在时间和作用域将被限制在该代码块内。且同名的新定义将会隐藏以前的定义(旧定义暂时不可见)。
编译器对自动变量管理的常用方法是留出一段内存视作为栈。
2.3 静态持续变量
C++为静态存储持续变量提供3中链接性:外部链接性(可在其他文件中访问)、内部链接性(只在当前文件访问)以及无链接性(只能在当前函数或代码块中访问)。
由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的装置管理他们。
编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。
int global = 1000; // static duration, external linkage
static int one_file = 50; // static duration, internal linkage
int main(){
//...
}
void funct1(int n){
static int count = 0; // static duration, no linkage
int llama = 0;
}
在funct1()
中声明的变量count
的作用域为局部,没有链接性,意味着只能在该函数中使用;但是,即使在funct1()
没有执行时,count
也留在内存中。
静态变量的初始化
除了默认的零初始化外,还可对静态变量进行常量表达式初始化和动态初始化。
零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译器处理文件时初始化变量。动态初始化意味着变量将在编译后初始化。
首先,所哟静态变量都被零初始化。接下来,如果使用常量表达式初始化了变量,且编译器仅根据文件内容就可以计算表达式,编译器将执行常量表达式初始化;必要时,编译器执行简单计算。如果没有足够的信息,变量将被动态初始化。
#include <cmath>
int x; // zero initialization
int y = 5; // constant-expression initialization
long z = 13 * 13; // constant-expression initialization
int enough = 2 * sizeof(long) + 1; // constant-expression initialization
const double pi = 4.0 * atan(1.0); // dynamic initialization
静态持续性、外部链接性
单定义规则(One Definition Rule, ODR):变量只有一次定义。
为满足以上要求,C++提供两种变量声明:
- 定义声明(defining declaration),给变量分配存储空间
- 引用声明(referencing declaration),不给变量分配存储空间,引用已有变量。
引用采用关键字extern
,且不进行初始化;否则声明为定义。如果要在多个文件中使用外部变量,只需在一个文件中包含该变来那个的定义,但在使用该变量的其他所有文件中,都必须使用关键字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 int cats; // not definition because they use extern
extern int dogs; // and have not initialization
// use external variable
#include <iostream>
extern duble warming; // use warming from another file
// function prototype
void update(double dt);
void local();
using std::cout;
void update(double dt) // modifies global variable
{
extern double warming; // optional redeclaration using global warming
warming += dt;
cout << "Updating global warming to " << warming << " degrees.\n";
}
void local() // uses local variable
{
double warming = 0.8; // new variable hides external one
cout << "Local warming = " << warming << " degrees.\n";
// Access global variable with the scope resolution operator
cout << "But global warming = " << ::warming << " degrees.\n";
C++提供了作用域解析运算符::
。如果放在变量名之前,该运算符表示使用变量的全局版本。
所有函数都能访问全局变量,因此不用传递参数。但易于访问的代价是程序不可靠。程序越能避免对数据进行不必要的访问,就越能保持数据的完整性。
静态持续性、内部连续性
将static
限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。链接性为内部的变量只能在其所属的文件中使用。
可使用链接性为内部的静态变量在同一个文件中的多个函数之间共享数据。如果将作用域为整个文件的变量变为静态的,就不必担心其名称与其他文件中的作用域为整个文件的变量发生冲突。
//file1.cpp variables with external and internal linkage
#include <iostream>
int tom = 3; // external variable definition
int dick = 30; // external variable definition
static int harry = 300; // static, internal linkage
void remote_access();
int main(){
using namespace std;
cout << "main() reports the following addresses:\n";
cout << &tom << " = &tom, " << &dick << " = &dick, ";
cout << &harry << " = &harry\n";
remote_access();
return 0;
}
//file2.cpp
#include <iostream>
extern int tom; // tom defined elsewhere
static int dick = 10; // overrides external dick
int harry = 200; // external variable definition, no conflict with file1 harry
void remote_access(){
using namespace std;
cout << "remote_access() reports the following addresses.\n";
cout << &tom << " = &tom, " << &dick << " = &dick, ";
cout << &harry << " = &harry\n";
}
静态存储持续性、无链接性
将static
限定符用于在代码块中定义的变量。该变量在该代码块不处于活动状态时仍然存在,但作用域是该代码块。因此,在两次函数调用之间,静态局部变量的值保持不变。另外,如果初始化了静态局部变量,则程序只在启动时进行一次初始化。
// using a static local variable
#include <iostream>
//constants
const int ArSize = 10;
void strcount(const char *str);
int main(){
using namespace std;
char input[ArSize];
char next;
cout << "Enter a line:\n";
cin.get(input, ArSize);
while(cin){
cin.get(next);
while(next != '\n')
cin.get(next);
strcout(input);
cout << "Enter next line (empty line to quit):\n";
cin.get(input, ArSize);
}
cout << "Bye\n";
return 0;
}
void strcount(const char * str){
using namespace std;
static int total = 0; // static local variable
int count = 0; // automatic local variable
cout << "\"" << str << "\" contains ";
while(*str++)
count++;
total += count;
cout << count << " characters\n";
cout << total << " characters total\n";
}
2.4 说明符和限定符
存储说明符(storage class classifier):
auto
(C++11中不再是说明符)register
static
extern
thread_local
(C++11新增)mutable
cv-限定符:const
和volatile
关键词volatile
表面即使程序代码没有对内存单元进行修改,其值也可能发生变化。该关键字的作用是为了改善编译器的优化能力。
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
在默认情况下全局变量的链接性为外部的,但const
全局变量的链接性是内部的。内部链接性意味着每个文件都有自己的一组常量,而不是所有文件共享一组常量。如果处于某种原原因,程序员希望某个常量的链接性为外部的,则可以使用extern
关键字覆盖默认内部链接性。
2.5 函数和链接性
C++不允许在一个函数中定义另一个函数,因为所有函数的存储连续性都自动为静态的。默认情况,函数的链接性为外部的。还可以使用static
将函数的链接性设置为内部的,使之只能在一个文件中使用;必须同时在原型和函数定义中使用该关键字。
C++在哪查找函数
如果文件原型表明函数是静态的,则编译器只在该文件中查找函数定义;
否则,编译器(包括链接程序)将在所有的程序文件中查找。
如果找到两个定义,编译器将发出错误信息;如果在程序文件中没有找到,编译器将在库中搜索。
2.6 语言的链接性(language linking)
2.7 存储方案和动态分配
通常编译器使用三块独立的内存,分别用于静态变量、自动变量和动态存储。
存储方案概念不适用于动态内存,但适用于用来跟踪动态内存的自动和静态指针变量。
使用new
运算符初始化
C++98通过类型名后加上括号括起的初始值为内置的标量类型分配存储空间并初始化。
int *pi = new int (6);
这种括号语法也适用于有合适构造函数的类。
如果要初始化常规结构或数组,则需要使用大括号列表初始化(要求支持C++11)
int * ar = new int [4] {2,4,6,7}; // C++11
new
失败时
new
可能找不到请求的内存量,则将引发异常std::bad_alloc
new
:运算符、函数和替换函数
运算符new
和new []
分别调用以下分配函数(allocation function)(位于全局名称空间中):
void * operator new(std::size_t); // used by new
void * operator new [] (std::size_t); // used by new []
同样也有delete
和delete []
调用的释放函数(deallocation function)。
定位new
运算符
new
运算符的一种变体,能够指定要使用的位置。定位new
运算符只是返回传递给它的地址,并强制转换为void *
,以便能够赋给任何指针类型。
// using placement new
#include <iostream>
#include <new> // for placement new
const int BUF = 512;
const int N = 5;
char buffer[BUF]; // chunk of memory
int main(){
using namespace std;
double *pd1, *pd2;
int i;
cout << "Calling new and placement new:\n";
pd1 = new double[N]; // use heap
pd2 = new (buffer) double[N]; // use buffer array
for (i=0; i<N; i++}
pd2[i] = pd1[i] = 1000 + 20.0 * i;
cout << "Memory address:\n" << " heap: " << pd1;
cout << " static: " << (void *) buffer << endl; //强行转换Buffer
cout << "Memory contents:\n";
for(i=0; i<N; i++){
cout << pd1[i] << " at " << &pd1[i] << "; ";
cout << pd2[i] << " at " << &pd2[i] << endl;
}
}
3. 名称空间
3.1 传统的C++名称空间
声明区域(declaration region):可以在其中进行声明的区域。
潜在作用域(potential scope):变量的潜在作用域从声明点开始,到其声明区域的结尾。变量并非在其潜在作用域内的任何位置都是可见的。
作用域(scope):变量对程序而言可见的范围。
C++关于全局变量和局部变量的规则定义了一种名称空间层次。每个声明区域都可以声明名称,这些名称独立于在其他声明区域中声明的名称。
3.2 新的名称空间特性
C++新增:通过定义一种新的声明区域来创建命名的名称空间。namespace
namespace Jack{
double pail;
void fetch();
int pal;
struct Well {};
}
namespace Jill{
double bucket(double n){}
double fetch;
int pal;
struct Hill{};
}
名称空间可以是全局的,也可以位于另一个名称空间,但不能位于代码块中。默认情况下,在名称空间中声明的名称的链接性为外部的。
除了用户定义的名称空间外,还存在全局名称空间(global namespace),对应于文件级声明区域,全局变量位于其中。
名称空间是开放的,即可以把名称加入到已有的名称空间中。
访问给定名称空间中名称最简单的方法是通过作用域解析运算符::
使用名称空间限定名称。Jack::pail = 12.34;
为被未被装饰的名称被称为未限定的名称;包含名称空间的名称称为限定的名称。
using
声明和using
编译命令
C++提供两种机制简化对名称空间中名称的使用。using
声明使特定标识符可用,using
编译指令使整个名称空间可用。它们增加了名称冲突的可能性。
using Jill::fetch; // a using declaration
// 将特定的名称添加到它所属的声明区域,使一个名称可用
using namespace Jack; // make all the names in Jack available
// using编译指令
如果某个名称已经在函数中声明了,则不能使用using
声明导入相同的名称。使用using
编译指令将进行名称解析,局部名称将隐藏名称空间名。
可以将名称空间声明进行嵌套。而且using
编译指令是可传递的。
可以给名称空间创建别名。namespace mvft = my_very_favorite_things;
可以通过省略名称空间的名称来创建未命名的名称空间。
// namesp.h
#include <string>
// create the pers and debts namespace
namespace pers
{
struct Person{
std::string fname;
std::string lname;
};
void getPerson(Person &);
void showPerson(const Person &);
}
namespace debts
{
using namespace pers;
struct Debt{
Person name;
double amount;
};
void getDebt(Debt &);
void showDebt(const Debt &);
double sumDebts(const Debt ar[], int n);
}
3.3 名称空间及其前途
使用在已命名的名称空间中声明的变量,而不是使用外部全局变量。
使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
如果开发了一个函数库或者类库,放在一个名称空间中。
仅将编译指令using
作为一种将旧代码转换为使用名称空间的权宜之计
不要再头文件中使用using
编译指令。
导入名称时,首选使用作用域解析运算符或者using
声明
对using
声明,首选将其作用域设置为局部而不是全局。