单独编译
C++鼓励程序猿将组件函数放在独立的文件中。make可以单独编译这些文件,并链接成可执行程序。make会跟踪程序依赖的文件以及这些文件的时间戳,运行make时,检测到上次编译后修改源文件,只需重新编译这些文件,再与其他编译版本链接。
使用#include
为了实现单独编译,C/C++提供了#include。使用#include可以将原来的程序分成三个部分。
- 头文件:包含结构声明和使用这些结构的函数的原型。
- 源代码文件:包含与结构有关的函数的代码。
- 源代码文件:包含调用与结构相关的函数的代码。
独立头文件,有利于编写其他程序的时候使用这些函数,只需要包含函数声明的头,并添加函数文件到make列表中。
同时这种组织方式也符合OOP方法:头文件包含用户定义类型的定义;函数文件包含操纵用户定义类型的函数的代码。头文件和函数文件组成软件包,可以被用于各种程序中。
不要将函数定义(内联函数除外)或者变量声明放在头文件中
如果一个头文件包含了一个函数定义,该头文件被同一程序中其他两个文件#include,这将会出现同一个函数重复定义。
头文件常包含
- 函数原型
- 使用#define或const定义的符号常量
- 结构声明:结构声明不创建变量,仅告诉编译器如何创建该结构变量。
- 类声明
- 模版声明:不是将被编译的代码,而是指示编译器如何生成与源代码中的函数调用相匹配的函数定义。
- 内联函数:const和内联函数有特殊的链接属性,因此放在头文件中不会出问题。
“”和 <>
包含自己定义的头文件使用#include “”而非 #include <>。使用<>,C++编译器抗灾存储标准头文件的主机系统的文件系统中查找。使用“”,编译器首先查找当前工作目录或者源码目录,找不到再去标准目录查找。
编译过程
//coordin.h
//structure templates
#ifndef COORDIN_H_
#define COORDIN_H_
struct polar {
double distance; //distance from origin
double angle; //distance from origin
};
struct rect {
double x; //horizontal distance from origin
double y; //vertical distance from origin
};
//prototypes
polar rect_to_polar(rect xypos);
void show_polar(polar capos);
#endif //COORDIN_H_
#ifndef COORDIN_H_
#define COORDIN_H_
#endif
在同一个文件中只能将同一头文件包含因此。预处理器编译指令#ifndef避免头文件多次包含。
在IDE中,不要将头文件加入到项目列表中,也不要在源代码文件中#include另一个源代码文件
注意多个库链接
不同的编译器可能会将同一个函数生成不同的修饰名词,因此创建的二进制模块大多数情况下都不能正确的链接。在链接编译模块时,需确保对象文件和库都是同一个编译器生成的。
内存分配的存储方案
首先复习一下内存的知识。C++使用三种(C++11中是四种)不同的方案来存储数据,这些方案的区别在于数据保留在内存中的时间。##存储持续性、作用域、链接性
- 自动存储持续性:在函数定义中声明的变量(包括函数形参)的存储连续性为自动。它们在程序开始执行其所属的函数或者代码块是被创建,在执行完函数或代码块时,它们使用的内存被释放。C++有两种存储持续性为自动的变量。
- 静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量。它们在程序运行过程中都存在。C++有三种这样的变量。
- 动态存储持续性:用new运算符分配的内存,在使用dekete释放之前一致存在。同时也可以被称为自由存储(free store)或堆(heap)。
作用域:局部和全部:局部仅在当前函数中或代码块里,全局在单前文件中定义的位置至结尾。
链接性分为外部和内部:外部可在文件间共享,内部仅由一个文件中的函数共享。
静态变量的作用域取决于是如何被定义的。
函数原型作用域中使用的名称旨在包含参数列表的括号内使用。
类中声明的成员的作用域是整个类。
在名称空间声明的变量作用域是整个名称空间(全局域是名称空间域的特例)。
C++函数的作用域可以是整个类或者整个名称空间,但不能是局部的。
C++存储方式是通过存储持续性、作用域和链接性描述。
自动存储
局部作用域。无链接性。
函数定义中的内部代码块重复定义同一个变量名
C++11 的auto用于自动类型推断。在C++11以前,auto用于显示的支出变量为自动存储。
保存在程序栈中,函数调用入栈,函数结束出栈。栈底指针指向栈的开始位置,栈顶置针指向下一个可用的内存单元。下图简化的描述了函数调用时的栈变化,实际函数调用还需要传递其他信息,如返回地址等。
寄存器变量
register int count_fast; //最初由C语言引入,建议编译器使用CPU寄存器来存储自佛难过变量。可提高访问变量的速度。目前该关键字已经失去了它的用途,现在和以前auto的用途一样显式的支出变量时自动的。
静态持续变量
静态存储持续性变量有三种链接性。
1.外部链接性:可在其他文件访问
2.内部链接性:仅在当前文件访问
3.无链接性:仅在当前代码段内访问
但是他们都在整个程序执行期间存在,编译器将其分配在固定的内存块中,为初始化的值默认为0。称之为零初始化。
...
int global = 1000; //static duration. external linkage。定义在main前面或者头文件中,统称外部变量,静态持续性的。
static int one_file = 50; //static duration, internal linkage
int main() {
...
}
void func1(int n) {
static int count = 0; //static duration, no linkage
}
...
静态变量的初始化
- 静态初始化:(1)零初始化;(2)常量表达式初始化。编译时初始化。
- 动态初始化:编译后厨时候。
所有静态变量都被0初始化,编译器将执行变量表达式初始化。
静态持续性、外部链接性
外部链接性的统称外部变量,是静态持续性的,作用域是整个文件。定义在函数外部,一般定义在main函数前面或者头文件中,可以在该文件的任何函数中使用。
1.单定义规则
在每一个使用外部变量的文件中都必须声明它;同时C++有单定义规则,即变量只能有一次定义。因此C++提供了两种变量声明。
- 定义声明,给变量分配存储空间。
- 引用声明,不分配空间,引用已有的变量。使用关键字extern,且不进行初始化。
double up; //定义声明
extern int blem; //引用声明
extern char gr = 'z'; //定义,因为这里有初始化
多个文件使用外部变量,一个文件定义声明,其他文件使用extern引用声明它。
//file01.cpp
extern int cats = 20; //extern 可有可无
int dogs = 22;
int fleas;
...
//file02.cpp
extern int cats;
extern int dogs;
...
//file03.cpp
extern int cats;
extern int dogs;
extern int fleas;
单定义规则不是不能同名声明变量,单定义是同一个地址,同名变量是不同的地址。局部变量同名全局变量,会自动隐藏全局变量。
//external.cpp
//define warming
double warming = 0.3;
int main() {
....
local();
update();
}
//support.cpp
#include <iostream>
extern double warming; //use warming from external.cpp
void local() {
double warming = 0.8;
cout << "Local warming = " << warming << endl;
cout << "External warming = " << ::warming << endl;
}
void update() {
extern double warming;
}
局部作用域中可使用作用域解析符(::)访问被同名局部变量自动隐藏的全局变量。
子函数中也可以使用extern访问外部变量。
局部和全局的选择
- 局部:数据隔离
- 全局:所有函数都能访问,无需参,易于访问。程序不可靠,无法保持数据完整性。适用于常量数据。
const char * const months[12] = {
};
定义了含有12个元素的数组,数组元素是字符指针。
const char //防止数组里的元素被修改。
char* const months[12] //防止每一个数组元素的指针始终指向最初的字符串。
静态持续性、内部链接性
当外部变量被static限定符修饰时,链接性将由外部转为内部,只能在其所属文件中使用。
``
//file01.cpp
int errors = 20; //external declaration
int main() {
...
test();
}
//file02.cpp
int errors = 20;//external declaration
void test() {
}
以上两个errors都是外部变量,这违反了单定义规则。可使用static解决问题。
//file01.cpp
int errors = 20; //external declaration
int main() {
...
test();
}
//file02.cpp
static int errors = 20;//external declaration
void test() {
}
``
此时files02.cpp中的errors是具有内部链接性的静态持续性变量,同局部变量一样可以隐藏同名的常规变量。
在多文件程序中,可以在一个文件(且只能在一个文件)中定义的外部变量,其他文件使用改变量的时候必须使用external引用声明它
静态存储持续性、无链接性
使用static修饰局部变量,存储连续性是静态的。改变了只能在代码块中使用,但是代码块不活跃状态时变量依然存在。
说明符和限定符
1.存储说明符
- auto(C++11中已不是说明符)
- register,声明寄存器存储,C++11中功能变成和以前的auto一样显示指出变量是自动的。
- static
- extern
- thread_local(C++新增),与static或extern结合使用。指出变量的持续性与其所属线程的持续性相同。thread_local之于线程犹如静态变量之于程序。
- mutable,指出const修饰的结构或类,其中的成员也是可以被修改的。
struct data {
char name[32];
mutable int accesses;
...
};
const data veep = {"claybourne clodde"} 0, ...;
strcpy(veer.name, "Joye Joux"); //not allowed
veep.ceeseees++; //allowed
cv限定符
- const 内存被初始化后,程序将不再对它修改。
默认全局变量具有外部链接性,使用const修饰后变为内部链接性。常量是文件私有的,不共享。如果有需求也是可以使用extern将const变成外部链接性。
extern const int states = 50;
- volatile 即使程序未对内存单元进行修改,其值也可能发生变化。比如说将指针指向某个硬件位置,可能是某个端口的信息,在这种情况下可能是硬件修改了数据。或者两个程序共享程序,互相影响。volatile作用是为了改善编译器的优化能力。
函数链接性
所有函数都是静态持续性,即在整个程序声明期间都存在。链接性默认是外部,可以在文件间共享。
函数也可以使用extern引用声明这是在另一个文件中定义的。
也可以使用static将函数变为内部的,仅在当前文件中使用。
静态函数覆盖extern外部函数。
寻找函数
原型声明是static仅在当前文件中寻找函数定义;否则将在所有的程序文件中查找。找到两个定义直接报错。若所有程序文件都没有找到,则在库中搜索。
语言链接性
C++具有多态性,所以同一个函数名可对应多个函数。因此C++编译器执行会进行名词矫正或者名词修饰,为重载函数生成不同的符号名称。例如,count(int) ——> _count_i ,count(double , double) ——> _count_d_d。这种修饰被称为C++语言链接。
C语言函数没有链接性,一个名称仅对应一个函数。
C++程序链接C语言库,函数原型要指出约定。
//file01.cpp
extern "C" void count(int);
extern void count(int);
extern "C++" void count(int);
以上介绍了C++为变量分配内存的五种方案(线程内存除外)
动态分配
通常编译器使用三块独立的内存:(1)静态变量;(2)自动变量;(3)动态存储
动态内存由运算符new和delete控制,而非作用域和链接性规则控制。new分配内存,delete释放内存。
通常程序结束时系统会自动回收new分配的内存,现实并不是总是会自动回收,所有new和delete必须成对出现,但不一定在同一个函数中出现。
存储方案不适用动态分配内存,但是可以用来分钟动态内存的自动和静态指针变量。
float * p_free = new float[20];
有new分配了80个字节(假定float占4bytes)的内存在程序结束或者调用delete之前一直保留在内存中。当包含该声明的代码块执行完毕后,p_free指针将消失。如果希望这80 bytes依旧可以被使用,必须将其地址返回。或者将p_free声明为外部,则该文件中p_free声明语句后面的代码都可以使用它。同时其他文件也可以使用extern float *p_free使用该指针。
new运算法
初始化动态分配的变量
1.内置标量分配内存并初始化
int *pi = new int (6);
double *pd = new double (99.99);
也可以给有构造函数的类的初始化。
2.初始化常规结构或数组,使用初始化列表
struct where {
double x;
double y;
double z;
};
where * one = new where{2.5, 5.3, 7.2};
int *ar = new int{1, 2, 3, 4};
new失败
new可能会找不到请求内存,以前会让new返回空指针,现在会引发异常std::bad_alloc。在后面的章节有演示示例。
new:运算符、函数和替换函数
new、new[]、delete、delete[]
void * operator new (std::size_t);
void * operator new[] (std::size_t);
void operator delete (std::size_t);
void operator delete[] (std::size_t);
使用了运算符重载。std::size_t是一个typedef。
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++中这些函数是可替换的(replaceable)。程序员可以为new和delete提供替换函数,并根据需求进行定制。可定制作用域为类的替换函数,在该类中使用new运算法符将调用本地定制的new()函数
定位new运算符
new和delete操作是在堆(heap)。
new还有一个变体被称为定位new运算符,能够指定要使用的位置,程序员可用这个特性来设置内存管理规范、处理需要通过特定地址进行访问的硬件或者在特定的位置创建对象。
使用定位new运算符:(1)包含头文件new;(2)将new运算符用于提供了所需地址的参数;(3)变量后面可以后方括号也可以没有。
#include <new>
struct chaff {
char dross[20];
int slag;
};
char buffer1[50];
char buffer2[500]; /使用静态数组为定位new运算符提供内存空间
int main() {
chaff *p1, *p2;
int *p3, *p4;
p1 = new chaff;
p3 = new int[20];
p2 = new (buffer1)chaff; //从buffer1中分配空间给chaff;
p4 = new (buffer2)int[20]; //从buffer2中分配空间给一个包含20个元素的int数组。
p5 = new (buffer2)int[20]; //overwrite p4所用内存
p6 = new (buffer + 20 * sizeof(int))int[20] ;//使用偏移量,紧跟着p5所占内存后面接着分配20个元素的int数组
}
new运算符工作原理,只返回传递给它的地址,并将其强制转换为void *,以便可以赋值给任何指针类型。
其他形式
int * p1 = 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);
定位new函数不可以替换,但是可以重载。至少需要两个参数,第一个是std::size_t,制定申请的字节数。
名称空间
using namespace std;
在C++中名称可以是变量、函数、结构、枚举、类及类和结构的成员。随着项目的增大,名称冲突的可能性越来越大。特别是使用多个厂商类库的时候,极容易产生名称冲突。为了解决这种冲突,C++标准提供了名称空间工具,易变更好的控制名称的作用域。
传统的C++名称空间
专业术语
-
声明区域:开始可以在其中进行声明的区域。如函数外部声明的全局变量的声明域是其声明所在的文件,函数中声明的变量声明域是其声明所在的代码块。
Screen Shot 2022-04-15 at 1.31.04 PM.png -
潜在作用域:从声明点开始到声明区域的结尾。
Screen Shot 2022-04-15 at 1.32.21 PM.png
因为变量是先定义后使用,所以潜在作用域比声明区域小。
但是变量在其潜在作用域内并不是任何位置都可见。可能会被嵌套的同名变量隐藏。这是作用域和潜在作用域的区别。
C++的名称空间层次
每一个声明区域都可以声明名称,这些名称独立域在其他声明区域中声明的名称。在一个函数中声明的局部变量不会与另一个函数中的局部变量冲突。
新的名称空间的特性
定义一种新的声明区域来创建命名的名称空间,目的是提供一个声明名称的区域。一个名称空间的名称与另一个名称空间的名称不会因同名而冲突。同时允许程序的其他部分使用该名称空间中声明的东西。
namespace Jack {
double pail;
void fetch();
int pal;
struct Well {
....
};
}
namespace Jill {
double bucket(double n) {}
double fetch;
int pal;
struct Hill {...};
}
名称空间可以是全局的,也可以嵌套在别的名称空间中,但不能位于代码块中。因此默认在名称空间声明的名称都为外部链接性的。
对于名称空间,对应与文件级声明区域,前面提到的全局变量现在被描述为位于全局名称空间中的。
名称空间中的声明和定义规则同全局声明和定义规则相同。
使用名称空间
1.使用作用域解析运算符::
Jack::pail = 13.23;
Jill:Hill mole;
Jack::fetch();
使用名称空间名修饰称为限定,pail称为未限定的名称,Jack::pail称为现代的名称。
2.using
通常不希望每次使用名称都对它进行限定,因此C++提供了using简化对名称空间中名称的使用。
- using声明
using Jill::fetch; //将特定的名称添加到它所属的声明区域中,之后使用fetch替代Jill::fetch。
局部使用using
全局使用using
- using编译
using namespace Jill; //可使用Jill名称空间里的所有名称。
using声明和using编译的区别
使用using声明有点像声明了相应的名称,如果函数中以及有了某个名称的声明,则不能用using声明导入相同的名称。
使用using编译会进行名称解析,就像在包含using声明和名称空间中的最下声明区域声明的名称一样。如果使用using编译导入名称空间,则局部名称将隐藏名称空间名。
namespace Jill {
double bucket(double n) {}
double fetch;
struct Hill {...};
}
char fetch; //全局名称空间
int main() {
using namespace Jill;
Hill Thrill;//Jill::Hill
double water = bucket(2); //JIll::bucket()
double fetch;//hide Jill::fetch & global fetch
cin >> fetch; //locl fetch
cin >> ::fetch;//global fetch
cin >> Jill::fetch; //Jill::fetch
}
虽然using编译指令将名称空间的名称视为在函数之外声明的,但作用域只在using编译指令所在的作用域内。
假设名称空间个声明区域定义了相同的名称,如果使用using声明将名称空间的名称导入该声明区域,则会发生冲突。如果使用using编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间的版本。
一般来说using声明更安全,出现冲突编译器会警示。
using编译导入名称和局部名称冲突,局部会隐藏名称空间,编译器无警告。
其他特性
嵌套
namespace elements {
namespace fire {
int flame;
...
}
float water;
}
elements::fire::flame
elements::water;
名称空间中使用using
namespace myth {
using Jill:fetch;
using name elements;
}
此时可以使用myth::fetch来访问Jill::fetch
1.名称空间传递
using namespace myth; //同时导入了myth和elements
2.名称空间别名
namespace newMyth = myth;
3.使用别名简化嵌套
namespace MEF = myth::elements::fire;
using MEF::flame;
未命名的名称空间
namespace {
int count;
}
作用域:从声明点到该声明区域末尾。
无法显示使用using,因此不能在未命名名称空间所属文件之外的其他文件中使用该名称空间中的名称。即提供了链接性为内部的静态变量的替代品。
static int counts
指导原则
- 使用在已命名的名称空间中声明的变量,而不是使用外部全部变量;
- 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量;
- 如果开发了一个函数库或者类库,将其放在一个名称空间中。C++当前提倡将标准函数库放在名称空间std中。
- 仅将编译指令using作为一种将旧代码转换为使用名称空间的权益之计。
- 不要在头文件中使用using编译指令。
- 导入名称时,应=首先使用作用域解析运算符或者using声明方法。
- 对于using声明,首选将其作用域设置为局部而非全局。
iostream.h未使用名称空间
iostream使用了std名称空间
总结
1.C++鼓励程序员在开发程序时使用多个文件。一种有效的组织策略是,使用头文件来定义用户类型,为操纵用户类型的函数提供函数原型;并将函数定义放在一个独立的源代码文件中。头文件和源代码文件一起定义和实现了用户定义的类型及其使用方式。最后,将main()和其他使用这些函数的函数放在第三个文件中。
2.C++的存储方案决定了变量保留在内存中的时间(储存持续性)以及程序的哪一部分可以访问它(作用域和链接性)。自动变量是在代码块(如函数或函数体中的代码块)中定义的变量,仅当程序执行到包含定义的代码块时,它们才存在,并且可见。自动变量可以通过使用存储类型说明符 register 或根本不使用说明符来声明,没有使用说明符时,变量将默认为自动的。register 说明符提示编译器,该变量的使用频率很高,但C++11摒弃了这种用法。
3.静态变量在整个程序执行期间都存在。对于在函数外面定义的变量,其所属文件中位于该变量的定义后面的所有函数都可以使用它 (文件作用域),并可在程序的其他文件中使用(外部链接性)。另一个文件要使用这种变量,必须使用 extern 关键字来声明它。对于文件间共享的变量,应在一个文件中包含其定义声明(无需使用 extern,但如果同时进行初始化,也可使用它),并在其他文件中包含引用声明(使用 extern且不初始化)。在函数的外面使用关键字static 定义的变量的作用域为整个文件,但是不能用于其他文件(内部链接性)。在代码块中使用关键字static 定义的变量被限制在该代码块内(局部作用域、无链接性),但在整个程序执行期间,它都一直存在并且保持原值。
4.在默认情况下,C++两数的链接性为外部,因此可在文件间共享;但使用关键字 static 限定的函数的链接性为内部的,被限制在定义它的文件中。动态内存分配和释放是使用 new 和delete 进行的,它使用自由存储区或堆来存储数据。调用 new 占用内存,而调用 delete 释放内存。程序使用指针来跟踪这些内存单元。
5.名称空间允许定义一个可在其中声明标识符的命名区域。这样做的目的是减少名称冲突,尤其当程序非常大,并使用多个厂商的代码时。可以通过使用作用域解析运算符、using 声明或 using 编译指令,来使名称空间中的标识符可用。