1. 单独编译
(1)头文件
1)头文件中常包含的内容:
- 函数原型
- 使用#define或const定义的符号常量
- 结构声明
- 类声明
- 模板声明
- 内联函数
2)头文件的书写格式:
如果文件名包含在尖括号中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;但如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录(或其他目录,取决于编译器),如果没有找到再去标准位置查找。因此,在包含自己的头文件时,应使用双引号而不是尖括号。
3)系统将程序组合起来的步骤:
只需将源代码文件加入到项目中,而不用加入头文件,这是因为#include指令管理头文件。另外不要使用#include来包含源代码文件,这样做将导致多重声明。
在UNIX系统中编译由多个文件组成的C++程序:
- 编译两个源代码文件的UNIX命令:CC file1.cpp file2.cpp
- 预处理器将包含的文件与源代码文件合并,生成临时文件
- 编译器创建每个源代码文件的目标代码文件
- 链接程序将目标代码文件、库代码和启动代码合并,生成可执行文件
警告:在IDE中,不要将头文件加入到项目列表中,也不要在源代码文件中使用#include来包含其他源代码文件。
4)头文件管理:
在同一个文件中只能将同一个头文件包含一次,但可能使用包含了另一个头文件的头文件,这种规则就会被破坏。C/C++提供了一种标准技术来避免多次包含同一个头文件,即基于预处理器编译指令#ifndef(即if not defined)。
#ifndef COORDIN_H_
// ...
#endif
// 意味着仅当以前没有使用预处理器编译指令#define定义名称COORDIN_H_时,才处理#ifndef和#endif之间的语句。
这种方式并不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容。大多数标准C和C++头文件都使用这种防护方案。
(2)单独编译示例:
头文件coordin.h:
// coordin.h -- 结构模板和函数原型
// 结构模板
#ifndef COORDIN_H_
#define COORDIN_H_
struct polar {
double distance; // 距离坐标
double angle; // 角度坐标
};
struct rect {
double x;
double y; //直角坐标系
};
// 函数原型
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);
#endif // !COORDIN_H_
源文件:
// file1.cpp -- 3个文件的示例
#include <iostream>
#include "coordin.h"
using namespace std;
int main() {
rect rplace;
polar pplace;
cout << "Enter the x and y values: ";
while (cin >> rplace.x >> rplace.y) {
pplace = rect_to_polar(rplace);
show_polar(pplace);
cout << "Next two numbers (q to quit): ";
}
cout << "Bye!\n";
return 0;
}
// file2.cpp -- 包含file1.cpp的函数定义
#include <iostream>
#include <cmath>
#include "coordin.h"
// 将直角坐标转换为极坐标
polar rect_to_polar(rect xypos) {
using namespace std;
polar answer;
answer.distance = sqrt(xypos.x * xypos.x + xypos.y * xypos.y);
answer.angle = atan2(xypos.y, xypos.x);
return answer;
}
// 显示极坐标,将弧度表示的角度转换为角度
void show_polar(polar dapos) {
using namespace std;
const double Rad_to_deg = 57.29577951;
cout << "distance = " << dapos.distance;
cout << ", angle = " << dapos.angle * Rad_to_deg;
cout << " degrees.\n";
}
C++标准使用术语翻译单元(translation unit),而不是文件,进行单独编译,文件并不是计算机组织信息时的唯一方式。
多个库的链接:由于C++允许每个编译器设计人员以他认为合适的方式实现名称修饰,因此由不同编译器创建的二进制模块(对象代码文件)很可能无法正确地链接。在链接编译模块时,请确保所有对象文件或库都是由同一个编译器生成的。如果由源代码,通常可以用自己的编译器重新编译源代码来消除链接错误。
2. 存储持续性、作用域和链接性
(1)C++四种存储数据的方案(存储持续性):
(区别在于数据保留在内存中的时间)
- 自动存储持续性:在函数定义中声明的变量(包括函参),其存储持续性为自动。它们在程序开始执行其所属的函数或代码块时被创建,在执行完毕后其内存被释放。C++有2种存储持续性为自动的变量。
- 静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。C++有3种存储持续性为静态的变量。
- 线程存储持续性(C++11):当前,多核处理器很常见,这些CPU可以同时处理多个执行任务。这让程序能够将计算放在可并行处理的不同线程中。如果变量是使用关键字thread_local声明的,则其生命周期与所属的线程一样长。
- 动态存储持续性:用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或者程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储(free store)或堆(heap)。
(2)作用域和链接
1)作用域(scope):描述名称在文件(翻译单元)的多大范围内可见。
- 局部:只在定义它的代码块中可用。
- 全局(文件作用域):在定义位置到文件结尾之间都可用。
- 自动变量作用域为局部
- 静态变量的作用域取决于它是如何被定义的
- 函数原型作用域:使用的名称只在包含参数列表的括号内可用,因此这些名称是什么以及是否出现都不重要
- 类中声明的成员:作用域为整个类
- 在名称空间中声明的变量:作用域为整个名称空间(全局作用域是名称空间作用域的特例)
- 函数的作用域:整个类或整个名称空间,但不能是局部
2)链接性(linkage):描述名称如何在不同单元间共享。
- 外部:可在文件间共享
- 内部:只能由一个文件中的函数共享
(自动变量的名称没有链接性,因为它们不能共享)
(3)自动存储持续性
在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。
寄存器变量,关键字register
(4)静态持续变量
C/C++为静态存储持续性变量提供3种链接性:
- 外部链接性:可在其他文件中访问,创建时必须在代码块的外面声明它;
- 内部链接性:只能在当前文件中访问,创建时必须在代码块的外面声明它,并且使用static限定符;
- 无链接性:只能在当前函数或代码块中访问,创建时必须在代码块内声明它,并使用static限定符。
编译器将分配固定内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。
#include <iostream>
using namespace std;
int global = 1000; //静态持续性;全局作用域,从定义位置到文件结尾都可使用;外部链接性,可在其他文件中访问
static int one_file = 50; //静态持续性,全局作用域,内部链接性,只能在当前文件中访问
void funct1(int n);
int main() {
return 0;
}
void funct1(int n) {
static int count = 0; //静态持续性,作用域为局部,无链接性
int llama = 0;
}
下表总结了引入名称空间之前使用的存储特性,其列出了关键字static的两种用法,但含义不同:用于局部声明,以指出变量是无链接性的静态变量时,static表示的是存储持续性;而用于代码块外的声明时,static表示内部链接性,而变量已经是静态持续性了。
存储描述 | 持续性 | 作用域 | 链接性 | 如何声明 |
---|---|---|---|---|
自动 | 自动 | 代码块 | 无 | 在代码块中 |
寄存器 | 自动 | 代码块 | 无 | 在代码块中,使用关键字register |
静态,无链接性 | 静态 | 代码块 | 无 | 在代码块中,使用关键字static |
静态,外部链接性 | 静态 | 文件 | 外部 | 不在任何函数内 |
静态,内部链接性 | 静态 | 文件 | 内部 | 不在任何函数内,使用关键字static |
静态变量的初始化:
如果没有显式地初始化静态变量,编译器将把它设置为0。在默认情况下,静态数组和静态结构将每个元素和成员的所有位都设置为0。即未被初始化的静态变量所有位都被设置为0,这种变量被称为零初始化的(zero-initialized)。
静态初始化:零初始化+常量表达式初始化,这意味着在编译器处理文件(翻译单元)时初始化变量。
动态初始化:变量将在编译后初始化。
(5)静态持续性、外部链接性
链接性为外部的变量通常简称为外部变量,它们的存储持续性为静态,作用域为整个文件,外部变量又称为全局变量。
1)单定义规则
一方面在每个使用外部变量的文件中,都必须声明它。另一方面,C++的“单定以规则”要求变量只能有一次定义。C++提供了两种声明,一种是定义声明,一种是引用声明,引用声明使用关键字extern,且不进行初始化。
如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义,但在使用该变量的其他所有文件中,都必须使用关键字extern声明它。
注意,单定义规则不意味着不能有多个变量同名,局部变量可能隐藏同名的全局变量。
// 9.5 external.cpp -- external variables
// compile with suport.cpp
#include <iostream>
using namespace std;
// 外部变量
double warming = 0.3;
// 函数原型
void update(double dt);
void local();
int main() {
cout << "Global warming is " << warming << " degrees.\n";
update(0.1);
cout << "Global warming is " << warming << " degrees.\n";
local();
cout << "Global warming is " << warming << " degrees.\n";
return 0;
}
#include <iostream>
extern double warming;
void update(double dt);
void local();
using std::cout;
void update(double dt) {
extern double warming; // 意识是通过这个名称使用外部变量warming,由于update()中没有定义同名自动变量,因此该声明可选
warming += dt;
cout << "Updating global warming to " << warming;
cout << " degrees.\n";
}
void local() {
double warming = 0.8; // 局部变量隐藏全局变量
cout << "Local warming = " << warming << " degrees.\n";
cout << "But global warming = " << ::warming; // 作用域解析运算符(::)放在变量名称前表示使用变量的全局版本,用这个更明确更安全
cout << " degrees.\n";
}
上述程序示例关键点:作用域解析运算符(::)、局部变量隐藏全局变量
2)何时适合使用全局变量
计算经验表明,程序越能避免对数据进行不必要的访问,就越能保持数据的完整性。通常情况下应该使用局部变量,在需要知晓时才传递数据。
全局变量尤其适用于表示常量数据,搭配const来防止数据被修改:
const char* const months[12] = {
"January", "February", "March", "Apirl", "May",
"June", "July", "Augest", "September", "October",
"November", "December"
};
(6)静态持续性、内部链接性
将static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。如果文件定义了一个静态外部变量,其名称与另一个文件中声明的常规外部变量相同,则在该文件中,静态变量将隐藏常规外部变量。
(7)静态存储持续性、无链接性
将static限定符用于在代码块中定义的变量,将导致该局部变量的存储持续性变为静态。因此,在两次函数调用之间,静态变量的值将保持不变。(静态变量适用于再生——可以用它们将瑞士银行的密码账号传递到下一个要去的地方)。另外,如果初始化了静态变量,则程序只在启动时进行一次初始化,以后再调用函数时,将不会像自动变量那样再次被初始化。
利用静态自动变量读取总字数:
// static.cpp -- using a static local variable
#include <iostream>
// 常量
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);
// 处理输入可能长于目标数组的方法,方法cin.get(input, ArSize)将一直读取输入,直到到达行尾或者读取了ArSize-1为止。
// 它把换行符留在输入队列里
while (next != '\n')
cin.get(next); //丢弃余下的字符
strcount(input);
cout << "Enter next line (empty line to quit):\n";
cin.get(input, ArSize); // !!!!!试图使用get(char*, int)读取空行将导致cin为false
}
cout << "Bye\n";
return 0;
}
void strcount(const char* str) {
using namespace std;
static int total = 0;
int count = 0;
cout << "\"" << str << "\" contains ";
while (*str++)
count++;
total += count;
cout << count << " characters\n";
cout << total << " characters total\n";
}
(8)说明符和限定符
有些被称为存储说明符(storage class specifier)或cv-限定符(cv-qualifier)的C++关键字提供了其他有关存储的信息。
存储说明符:
- auto:指出变量为自动变量,但在C++11中,用于自动类型推断
- register:用于在声明中指示寄存器存储,C++11中只是显示地指出变量是自动的
- static
- extern:表明是引用声明,即声明引用在其他地方定义的变量
- thread local:指出变量的持续性与其所属的线程的持续性相同
- mutable
1)cv-限定符
- const
- volatile
关键字volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。可能是硬件修改或者两个程序互相影响共享数据。该关键字的作用是为了改善编译器的优化能力。
2)mutable
mutable指出,即使结构(或类)变量为const,其某个成员也可以被修改。
struct data {
char name[30];
mutable int accesses;
};
int main() {
const data veep = { "Claybourne Clodde", 0 };
strcpy(veep.name, "Joye Joux"); // 不允许
veep.accesses++; // 允许
return 0;
}
3)const
在C++中,const限定符对默认存储类型稍有影响。在默认情况下,全局变量的链接性为外部,而const全局变量的链接性为内部。C++修改了常量类型的规则,让程序员更轻松。例如,假如将一组常量放在头文件中,并在同一个程序的多个文件中使用该头文件。
如果出于某种原因,程序员希望某个常量的链接性为外部,则可以使用extern关键字来覆盖默认的内部链接性。
extern const int states = 90;
(9)函数和链接性
- 所有函数的存储持续性都自动为静态的
- 默认情况下,函数的链接性为外部,即可以在文件间共享(要让程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译,或者是由链接程序搜索的库文件)
- 可以使用关键字static将函数的链接性设置为内部的,使之只能在一个文件中使用。必须同时在原型和函数定义中使用该关键字。
- 同变量一样,静态函数可以覆盖外部定义
- 单定义规则使用于非内联函数
- 内联函数不受单定义规则的约束,则允许程序员将内联函数放在头文件中。这样,包含了头文件的每个文件都有内联函数的定义。然而,C++要求同一个函数的所有内联定义都必须相同。
- C++在哪里查找函数?如果函数是静态的,则只在该文件中查找函数定义;否则编译器(包括链接程序)将在所有程序文件中查找;如果在程序文件中没找到,则去查找库函数。(因此如果定义和库函数同名的函数,编译器优先使用程序员定义的版本)。
(10)语言链接性
C语言链接性、C++语言链接性(名称修饰 Cap8)
链接程序寻找与C++函数调用匹配的函数时,使用的方法与C语言不同。如果要在C++程序中使用C库中预编译的函数,将出现什么情况?
为解决这个问题,可以用函数原型来指出要使用的约定:
extern "C" void spiff(int);
extern void spiff(int);
extern "C++" void spiff(int);
C和C++链接性是C++标准指定的说明符。
(11)存储方案和动态分配
使用C++运算符new(或C函数malloc)分配的内存被称为动态内存,动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制。
通常,编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,一块用于动态存储。
1)使用new运算符初始化
int *pi = new int (6); // C++98
double *pd = new double(99.99); //C++98
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 };
int* pin = new int {6}; //C++11
double* pdo = new double {99.99};
2)new失败时
将引发异常std::bad_alloc
3)new:运算符、函数和替换函数
运算符new和new[]分别调用如下函数:
void* operator new(std::size_t); // std::size_t是一个typedef,对应于合适的整型
void* operator new[](std::size_t);
这些函数被称为分配函数,位于全局名称空间中。
例:
int* pi = new int; 被转换为:int* pi = new (sizeof(int));
运算符delete和delete[]分别调用如下函数:
void operator delete(void* );
void operator delete[](void* );
这些函数被称为释放函数,位于全局名称空间中。
4)定位new运算符
通常,new运算符负责在堆(heap)中找到一个足以能够满足要求的内存块。其另一种变体,定位new运算符,让程序员能够指定要使用的位置。程序员可能使用这种特性来设置其内存管理章程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象。
要使用定位new特性,首先要包含头文件new,它提供了这种版本的new运算符的原型;然后将new运算符用于提供了所需地址的参数。除需要指定参数外,句法与常规new运算符相同,具体地说,使用定位new运算符时,变量后面可以有方括号,也可以没有。
new运算符的4种用法:
// 此示例用两个静态数组来为定位new运算符提供内存空间
#include <new>
struct chaff {
char dross[20];
int slag;
};
char buffer1[50];
char buffer2[500];
int main() {
chaff* p1, * p2;
int* p3, * p4;
// new运算符的传统形式
p1 = new chaff; // 在堆中分配可以存储chaff变量的内存
p3 = new int[20]; // 在堆中分配可以存储int数组的内存
// 定位new运算符的2种形式
p2 = new (buffer1) chaff; // 将结构放在buffer1中
p4 = new (buffer2) int[20];// 将int数组放在buffer2中
}
程序示例:
// newplace.cpp -- using placement new
#include <iostream>
#include <new>
const int BUF = 512;
const int N = 5;
char buffer[BUF]; // 内存块
int main() {
using namespace std;
double* pd1, * pd2;
int i;
cout << "Calling new and placement new: \n";
pd1 = new double[N]; // 堆
pd2 = new (buffer) double[N]; // buffer数组
for (i = 0; i < N; i++)
pd2[i] = pd1[i] = 1000 + 20.0 * i;
cout << "Memory address:\n" << " heap: " << pd1
<< " static: " << (void*)buffer << endl; // 如果不进行强制转换,cout将显示一个字符串
cout << "Memory contents:\n";
for (i = 0; i < N; i++) {
cout << pd1[i] << " at " << &pd1[i] << "; ";
cout << pd2[i] << " at " << &pd2[i] << endl;
}
cout << "\nCalling new and placement new a second time:\n";
double* pd3, * pd4;
pd3 = new double[N]; // 找新的内存块
pd4 = new (buffer) double[N]; // 在旧地址重写
for (i = 0; i < N; i++)
pd4[i] = pd3[i] = 1000 + 40.0 * i;
cout << "Memory contents:\n";
for (i = 0; i < N; i++) {
cout << pd3[i] << " at " << &pd3[i] << "; ";
cout << pd4[i] << " at " << &pd4[i] << endl;
}
cout << "\nCalling new and placement new a third time:\n";
delete[] pd1;
pd1 = new double[N];
pd2 = new (buffer + N * sizeof(double)) double[N];
for (i = 0; i < N; i++)
pd2[i] = pd1[i] = 1000 + 60.0 * i;
cout << "Memory contents:\n";
for (i = 0; i < N; i++) {
cout << pd1[i] << " at " << &pd1[i] << "; ";
cout << pd2[i] << " at " << &pd2[i] << endl;
}
delete[] pd1;
delete[] pd3;
// buffer指定的是内存是静态内存,而delete只能用于这样的指针:指向常规new运算符分配的堆内存
return 0;
}
注意:detele只能用于这样的指针:指向常规new运算符分配的堆内存。
3. 名称空间
(1)传统的C++名称空间
术语:声明区域(declaration region),潜在作用域(potential scope),作用域(scope)。
(2)新的名称空间特性
C++提供这样一种功能:通过定义一种新的声明区域来创建命名的名称空间。
namespace Jack{
double pail;
void fetch();
int pal;
struct Well { ... };
}
namespace Jill{
double bucket(double n) { ... }
double fetch;
int pal;
struct Hill { ... };
}
名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量 ???)。
访问给定名称空间中的名称,最简单的方法是通过作用域解析符::,使用名称空间来限定该名称。
1)using声明和using编译指令
using声明:using Jill :: fetch;
using编译指令:using namespace Jill;
注意:假设名称空间和声明区域定义了相同的名称。如果试图使用using声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突,从而出错。using声明的作用域与其所在的声明区域相同。如果使用using编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间版本。一般情况下,使用using声明比使用using编译指令更安全。
2)名称空间的其他特性
可以用嵌套名称空间来创建一个包含常用using声明的名称空间。
未命名的名称空间提供了链接性为内部的静态变量的替代品。
3)名称空间示例
// namesp.h
#include <string>
// 建立pers和debts名称空间
namespace pers {
struct Person {
std::string fname;
std::string lname;
};
void getPerson(Person&);
void showPerson(const Person&);
}
namespace debts {
using namespace pers; // using 编译指令
struct Debt {
Person name;
double amount;
};
void getDebt(Debt&);
void showDebt(const Debt&);
double sumDebts(const Debt ar[], int n);
}
// namesp.cpp -- namespaces
#include <iostream>
#include "namesp.h"
namespace pers {
using std::cout;
using std::cin;
void getPerson(Person& rp) {
cout << "Enter first name: ";
cin >> rp.fname;
cout << "Enter last name: ";
cin >> rp.lname;
}
void showPerson(const Person& rp) {
std::cout << rp.lname << ", " << rp.fname;
}
}
namespace debts {
void getDebt(Debt& rd) {
getPerson(rd.name);
std::cout << "Enter debt: ";
std::cin >> rd.amount;
}
void showDebt(const Debt& rd) {
showPerson(rd.name);
std::cout << ": $" << rd.amount << std::endl;
}
double sumDebts(const Debt ar[], int n) {
double total = 0;
for (int i = 0; i < n; i++)
total += ar[i].amount;
return total;
}
}
// usenamesp.cpp -- using namespaces
#include <iostream>
#include "namesp.h"
void other(void);
void another(void);
int main() {
using debts::Debt;
using debts::showDebt; // 如果函数被重载,则一个using声明将导入所有版本
Debt golf = { {"Benny", "Goatsniff"}, 120.0 };
showDebt(golf);
other();
another();
return 0;
}
void other(void) {
using std::cout;
using std::endl;
using namespace debts; // 不太好的方法
Person dg = { "Doodles", "Glister" };
showPerson(dg);
cout << endl;
Debt zippy[3];
int i;
for (i = 0; i < 3; i++)
getDebt(zippy[i]);
for (i = 0; i < 3; i++)
showDebt(zippy[i]);
cout << "Total debt: $" << sumDebts(zippy, 3) << endl;
return;
}
void another(void) {
using pers::Person;
Person collector = { "Milo", "Rightshift" };
pers::showPerson(collector);
std::cout << std::endl;
}