首先谈一下这一讲的主要内容。
- 单独编译
- 存储连续性、作用域和链接性
- 定位new运算符
为何要讲这块知识??C++为在内存中存储数据方面提供了多种选择。通常大型程序都由多个源代码文件组成,这些文件可能共享一些数据。这样的程序涉及到程序文件的单独编译,这些我们都要学习。
【单独编译】
我们常常将组件函数放在独立的文件中。
之前我们学习到,可以单独编译这些文件,然后将它们链接成可执行的程序。(通常,C++编译器既编译程序,也管理链接器。)如果只修改了一个文件,则可以只重新编译该文件,然后将它与其他文件的编译版本链接。这使得管理大程序更便捷。
此外,大多数C++环境都提供了其他工具来帮助管理。例如,UNIX和Linux系统都具有make程序,可以跟踪程序依赖的文件以及这些文件的最后修改时间。运行make时,如果它检测到上次编译后修改了源文件,make将记住重新构建程序所需的步骤。大多数集成开发环境(包括Microsoft Visual C++、Apple Xcode和Freescale CodeWarrior)都在Project菜单中提供了类似的工具。
现在我们来看一个简单的示例。我们不是要从中了解编译的细节(这取决于实现),而是要重点介绍更通用的方面,如设计。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // strctfun.cpp -- functions with a structure argument 2 #include <iostream> 3 #include <cmath> 4 5 // structure declarations 6 struct polar 7 { 8 double distance; // distance from origin 9 double angle; // direction from origin 10 }; 11 struct rect 12 { 13 double x; // horizontal distance from origin 14 double y; // vertical distance from origin 15 }; 16 17 // prototypes 18 polar rect_to_polar(rect xypos); 19 void show_polar(polar dapos); 20 21 int main() 22 { 23 using namespace std; 24 rect rplace; 25 polar pplace; 26 27 cout << "Enter the x and y values: "; 28 while (cin >> rplace.x >> rplace.y) // slick use of cin 29 { 30 pplace = rect_to_polar(rplace); 31 show_polar(pplace); 32 cout << "Next two numbers (q to quit): "; 33 } 34 cout << "Done. "; 35 return 0; 36 } 37 38 // convert rectangular to polar coordinates 39 polar rect_to_polar(rect xypos) 40 { 41 using namespace std; 42 polar answer; 43 44 answer.distance = 45 sqrt( xypos.x * xypos.x + xypos.y * xypos.y); 46 answer.angle = atan2(xypos.y, xypos.x); 47 return answer; // returns a polar structure 48 } 49 50 // show polar coordinates, converting angle to degrees 51 void show_polar (polar dapos) 52 { 53 using namespace std; 54 const double Rad_to_deg = 57.29577951; 55 56 cout << "distance = " << dapos.distance; 57 cout << ", angle = " << dapos.angle * Rad_to_deg; 58 cout << " degrees "; 59 }
上面这段程序将直角坐标转换为极坐标,然后显示结果。
假设我们要分解上述程序,将支持函数放在一个独立的文件中。我们不能简单地以main()之后的虚线为界,将原来的文件分为两个。
问题在于,main()和其他两个函数使用了同一个结构声明,因此两个文件都应该包含该声明。简单地将它们输入进去无疑是自找麻烦,即使正确地复制了结构声明,如果以后要作修改,则必须记住对这两组声明都进行修改。简而言之,将一个程序放在多个文件中将引出新问题。
显然,我们所有人都不希望麻烦,所以我们想出了提供#include来处理这种情况。哈哈!!与其将结构声明加入到每一个文件中,不如将其放在头文件中,然后在每一个源代码文件中包含该头文件。这样,要修改结构声明时,只需在头文件中做一次改动即可。另外,也可以将函数原型放在头文件中。
所以,原来的程序将分为三部分:
- 头文件:包含结构声明和使用这些结构的函数的原型
- 源代码文件:包含与结构有关的函数的代码
- 源代码文件:包含调用与结构相关的函数的代码
别小看它,这是一种非常有用的组织程序的策略。例如,如果编写另一个程序时,也需要使用这些函数,则只需包含头文件,并将函数文件添加到项目列表或make列表中即可。另外,这种组织方式也与OOP方法一致。一个文件(头文件)包含了用户定义类型的定义;另一个文件包含操纵用户定义类型的函数的代码。这两个文件组成了一个软件包,可用于各种程序中。
请不要将函数定义或变量声明放到头文件中。因为这样做对于简单的情况可能是可行的,但通常会引来麻烦。例如,如果在头文件包含一个函数定义,然后在其他两个文件(属于同一个程序)中包含该文件,则同一个程序中将包含同一个函数的两个定义,除非函数是内联的,否则这将出错。
下面列出了头文件中常包含的内容:
- 函数原型
- 使用#define或const定义的符号常量
- 结构声明
- 类声明
- 模板声明
- 内联函数
将结构放在头文件中是可以的,因为它们不创建变量,而只是在源代码文件中声明结构变量时,告知编译器如何创建该结构变量。同样,模板声明不是将被编译的代码,它们指示编译器如何生成与源代码中的函数调用相匹配的函数定义。被声明为const的数据和内联函数有特殊的链接属性(下面将介绍),因此可以放在头文件中而不会引起问题。
下面3个程序是将上面那个程序分成几个独立部分后得到的结果:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // coordin.h -- structure templates and function prototypes 2 // structure templates 3 #ifndef COORDIN_H_ 4 #define COORDIN_H_ 5 6 struct polar 7 { 8 double distance; // distance from origin 9 double angle; // direction from origin 10 }; 11 struct rect 12 { 13 double x; // horizontal distance from origin 14 double y; // vertical distance from origin 15 }; 16 17 // prototypes 18 polar rect_to_polar(rect xypos); 19 void show_polar(polar dapos); 20 21 #endif
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // file1.cpp -- example of a three-file program 2 #include <iostream> 3 #include "coordin.h" // structure templates, function prototypes 4 using namespace std; 5 int main() 6 { 7 rect rplace; 8 polar pplace; 9 10 cout << "Enter the x and y values: "; 11 while (cin >> rplace.x >> rplace.y) // slick use of cin 12 { 13 pplace = rect_to_polar(rplace); 14 show_polar(pplace); 15 cout << "Next two numbers (q to quit): "; 16 } 17 cout << "Bye! "; 18 // keep window open in MSVC++ 19 /* 20 cin.clear(); 21 while (cin.get() != ' ') 22 continue; 23 cin.get(); 24 */ 25 return 0; 26 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // file2.cpp -- contains functions called in file1.cpp 2 #include <iostream> 3 #include <cmath> 4 #include "coordin.h" // structure templates, function prototypes 5 6 // convert rectangular to polar coordinates 7 polar rect_to_polar(rect xypos) 8 { 9 using namespace std; 10 polar answer; 11 12 answer.distance = 13 sqrt( xypos.x * xypos.x + xypos.y * xypos.y); 14 answer.angle = atan2(xypos.y, xypos.x); 15 return answer; // returns a polar structure 16 } 17 18 // show polar coordinates, converting angle to degrees 19 void show_polar (polar dapos) 20 { 21 using namespace std; 22 const double Rad_to_deg = 57.29577951; 23 24 cout << "distance = " << dapos.distance; 25 cout << ", angle = " << dapos.angle * Rad_to_deg; 26 cout << " degrees "; 27 }
将上面两个源代码文件(file1、file2)和新的头文件一起进行编译和链接将生成一个可执行程序。
顺便说一句,虽然我们讨论的是根据文件进行单独编译,但为保持通用性,C++标准使用了术语翻译单元,而不是文件;文件并不是计算机组织信息时的唯一方式。出于简化的目的,我们使用术语文件,但我们可以将其解释为翻译单元。
注意,在包含头文件时,我们使用“coordin.h”,而不是<coordin.h>。为啥呢?现在我们来复习一下这两种用法的区别。
如果文件名包含在尖括号中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;但如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录(或其他目录,这取决于编译器)。如果没有在那里找到头文件,则将在标准位置查找。
因此在包含自己的头文件时,应使用引号而不是尖括号。
- 注意,只需将源代码文件加入到项目中,而不用加入头文件。这是因为#include指令管理头文件。另外,不要使用#include来包含源代码文件,这样做将导致多重声明。
既然谈到这个方面了,我们就仔细探讨一下头文件管理。
在同一个文件中只能将同一个头文件包含一次。记住这个规则很容易,但我们很可能在不知情的情况下将头文件包含多次。例如,可能使用包含了另外一个头文件的头文件。有一种标准的C/C++技术可以避免多次包含同一个头文件。它是基于预处理器编译指令#ifndef(即 if not defined)的。下面的代码片段意味着仅当以前没有使用预处理器编译指令#define定义名称COORDIN_H_时,才处理#ifndef和#endif之间的语句:
#ifndef COORDIN_H_ ... #endif
我们通常使用#define语句来创建符号常量(#define MAXIMUM 4096),但只要将#define用于名称,就足以完成该名称的定义,如下所示:
#define COORDIN_H_
上面连续3个程序当中的第一个程序使用这种技术是为了将文件内容包含在#ifndef中:
#ifndef COORDIN_H_ #define COORDIN_H_ //place include file contents here #endif
编译器首次遇到该文件时,名称COORDIN_H_没有定义(我们根据include文件名来选择名称,并加上一些下划线,以创建一个在其他地方不太可能被定义的名称)。在这种情况下,编译器将查看#ifndef和#endif之间的内容(这正是我们希望的),并读取定义COORDIN_H_的一行。如果在同一个文件中遇到其他包含coordin.h的代码,编译器将知道COORDIN_H_已经被定义了,从而跳到#endif后面的一行上。注意,这种方法并不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容。大多数标准C和C++头文件都使用这种防护方案。否则,可能在一个文件中定义同一个结构两次,这将导致编译错误。
下图简要地说明了在UNIX系统中将程序组合起来的步骤:
在这里,注意只需执行编译命令CC即可,其他步骤将自动完成。g++和gpp命令行编译器以及Borland C++命令行编译器(bcc32.exe)的行为类似。Apple Xcode、Microsoft Visual C++和Embarcadero C++ Builder基本上执行同样的步骤,但它们启动这个过程的方式不同——使用能够创建项目并将其与源代码文件关联起来的菜单。
【存储持续性、作用域和链接性】
上面介绍的是多文件程序,接下来我们讨论内存方案,即存储类别如何影响信息在文件间的共享。
我们来复习一下有关内存的知识。C++使用三种(在C++11中是四种)不同的方案来存储数据,这些方案的区别就在于数据保留在内存中的时间。
- 自动存储持续性:在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。C++有两种存储持续性为自动的变量。
- 静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。C++有3种存储持续性为静态的变量。
- 线程存储持续性(C++11):当前,多核处理器很常见,这些CPU可同时处理多个执行任务。这让程序能够将计算放在可并行处理的不同线程中。如果变量是使用关键字thread_local声明的,则其生命周期与所属的线程一样长。我们将不探讨并行编程。
- 动态存储持续性:用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储或堆。
下面我们介绍其他内容(包括关于各种变量何时在作用域内或可见(可被程序使用)以及链接性的细节)。
首先是作用域和链接。
作用域不用多说,我们在C语言中学过。它描述了名称在文件(翻译单元)的多大范围内可见。
链接性描述了名称如何在不同单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量的名称没有链接性,因为它们不能共享。
C++变量的作用域有多种。作用域为局部的变量只在定义它的代码块中可用。代码块是由花括号括起的一系列语句。例如函数体就是代码块,但可以在函数体内嵌入其他的代码块。作用域为全局(也叫文件作用域)的变量在定义位置到文件结尾之间都可用。自动变量的作用域为局部,静态变量的作用域是全局还是局部取决于它是如何被定义的。在函数原型作用域中使用的名称只在包含参数列表的括号内可用(这就是为什么这些名称是什么以及是否出现都不重要的原因)。在类中声明的成员的作用域为整个类。在名称空间中声明的变量的作用域为整个名称空间(由于名称空间已经引入到C++语言中,因此全局作用域是名称空间作用域的特例)。
C++函数的作用域可以是整个类或整个名称空间(包括全局的),但不能是局部的,因为不能在代码块内定义函数,如果函数的作用域为局部,则只对它自己是可见的,因此不能被其它函数调用。这样的函数将无法运行。
学到这里,我们要知道不同的C++存储方式是通过存储持续性、作用域和链接性来描述的。下面来看看各种C++存储方式的这些特征。
首先探讨最常见的自动存储连续性:
在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性(只能在当前函数或代码块中访问)。
具体地说,当程序开始执行这些变量所属的代码块时,将为其分配内存(其作用域的起点为其声明位置);当函数结束后,这些变量都将消失。
其他一些细节我们都已经耳熟能详了,在此不做赘述。我们只要知道,自动变量只在包含它们的函数或代码块中可见。
自动变量存在栈中。
如果使用关键字register,则表明将用CPU寄存器来存储自动变量,目的是提高访问变量的速度。只有自动变量可以使用register关键字。
熟悉了自动变量,我们再来探讨静态持续变量:
C++为静态存储持续性变量提供了3种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性。
这3种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。
由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的装置(如栈)来管理它们。
编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。
另外,如果没有显示地初始化静态变量,编译器将把它设置为0。
下面我们编写一段程序实现这3种变量:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
由程序段,我们获悉要想创建链接性为外部的静态持续变量,必须在代码块的外面声明它;要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用static限定符;要想创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static限定符。
几点说明:正如前面指出的,所有静态持续变量(程序段中的global、one_file和count)在整个程序执行期间都存在。在f1()中声明的变量count的作用域为局部,没有链接性,这意味着只能在f1()函数中使用它,就像自动变量llama一样。然而,与llama不同的是,即使在f1()函数没有被执行时,count也留在内存中。global和one_file的作用域都为整个文件,即在从声明位置到文件结尾的范围内都可以被使用。由于one_file的链接性为内部,因此只能在包含上述代码的文件中使用它;由于global的链接性为外部,因此可以在程序的其他文件中使用它。
在此,我省略了一些静态变量相关的细节,如果日后我想再探索,自会补充。
下面讲的就是静态持续性&外部链接性、静态持续性&内部链接性、静态存储持续性&无链接性
具体内容在书本p310~p317。
上面讲了一系列不同类型的变量以及变量的几种链接性。这些知识并不能完全覆盖变量的存储信息,我们接下来将学学说明符和限定符。
有一些关键字,它们被称为存储说明符或cv-限定符,提供了其他有关存储的信息。
首先列举存储说明符:
- auto(在C++11中不再是说明符)
- register
- static
- extern
- thread_local(C++11新增的)
- mutable
这些说明符中的大部分我们已经接触过甚至掌握了,在同一个声明中不能使用多个说明符(thread_local除外,它可与static或extern结合使用)。
auto:在C++11之前,可以在声明中使用关键字auto指出变量为自动变量;但在C++11中,auto用于自动类型推断。
register:用于在声明中指示寄存器存储,在C++11中,它只是显示地指出变量是自动的。
static:被用在作用域为整个文件的声明中时,表示内部链接性;被用于局部声明中,表示局部变量的存储持续性为静态的。
extern:表明是引用声明,即声明引用在其他地方定义的变量。
thread_local:指出变量的持续性与其所属线程的持续性相同。thread_local变量之于线程,犹如常规静态变量之于整个程序。
mutable:指出即使结构(或类)变量为const,其某个成员也可以被修改。
cv-限定符:
- const
- volatile
const:表明内存被初始化后,程序便不能再对它进行修改。
volatile:表明即使程序代码没有对内存单元进行修改,其值也可能发生变化。(具体解释可百度或参考书本p317)
详细谈谈const:
在C++中,const限定符对默认存储类型稍有影响。在默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的。
也就是说,在C++看来,全局const定义就像使用了static说明符一样:
const int fingers = 10; //等价于 static const int fingers = 10; int main() { ...
C++修改了常量类型的规则,这让程序员更轻松。
例如,假设将一组常量放在头文件中,并在同一个程序的多个文件中使用该头文件。那么预处理器将头文件的内容包含到每个源文件中后,所有的源文件都将包含类似下面这样的定义:
const int fingers = 10; const char *warning = "Wak!";
如果全局const声明的链接性像常规变量那样是外部的,则根据单定义规则,这将出错。也就是说,只能有一个文件可以包含前面的声明,而其他文件必须使用extern关键字来提供引用声明。另外,只有未使用extern关键字的声明才能进行初始化:
//如果const有外部链接性,那么extern是必要的 extern const int fingers; //不能被初始化 extern const char *warning;
因此,需要为某个文件使用一组定义,而其他文件使用另一组声明。然而,由于外部定义的const数组的链接性是内部的,因此可以在所有文件中使用相同的声明。
内部链接性还意味着,每个文件都有自己的一组常量,而不是所有文件共享一组常量。每个定义都是其所属文件私有的,这就是能够将常量定义放在头文件中的原因。这样,只要在两个源代码文件中包括同一个头文件,则它们将获得同一组常量。
如果出于某种原因,程序员希望某个常量的链接性为外部的,则可以使用extern关键字来覆盖默认的内部链接性:
extern const int states = 50; //定义 with 外部链接性
在这种情况下,必须在所有使用该常量的文件中使用extern关键字来声明它。这与常规外部变量不同,定义常规外部变量时,不必使用extern关键字,但在使用该变量的其他文件中必须使用extern。然而,请记住,鉴于单个const在多个文件之间共享,因此只有一个文件可对其进行初始化。
在函数或代码块中声明const时,其作用域为代码块,即仅当程序执行该代码块中的代码时,该常量才是可用的。这意味着在函数或代码块中创建常量时,不必担心其名称与其他地方定义的常量发生冲突。
下面讲述函数链接性和语言链接性(p318-p319)
最后一块骨头来了,这是大骨头啊。。关于存储方案和动态分配。
前面介绍C++用来为变量(包括数组和结构)分配内存的5种方案(线程内存除外),它们不适用使用C++运算符new分配的内存,这种内存被称为动态内存。
p320.
温故而知新
1.对于下面的情况,应使用哪种存储方案?
a。homer是函数的形参。
b。secret变量由两个文件共享。
c。topsercret变量由一个文件中所有函数共享,但对于其他文件来说是隐藏的。
d。beencalled记录包含他的函数被调用的次数。
答:a。自动变量;b。外部变量;c。静态内部变量;d。无链接性的静态变量。
c补充:也可以在一个未命名的名称空间中定义
2。using声明和using编译指令有何区别?
答:
①using声明只使用名称空间中的一个名称,using编译指令使用名称空间中的所有名称;
②using声明在遇见同名内部变量(但外部变量不会发生这种情况)时,可能导致名称冲突(使用外部时,同名外部会被隐藏);但using编译指令不会,他会隐藏外部,或者被内部隐藏。
补充③using声明其作用域与using声明所在的声明区域相同(我知道,但我觉得没必要强调,就相当于声明了一个变量一样)。using编译指令就像在一个包含using声明和名称空间本身的最小声明区域中声明了这些名称一样。
3。重新编写下面的代码,使其不使用using声明和using编译指令。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> using namespace std; int main() { double x; cout << "Enter value: "; while (! (cin>>x) ){ cout << "Bad input. Please enter a number: "; cin.clear(); while (cin.get() != ' ' ) continue; } cout << "Value = " << x << endl; return 0; }
答:修改为:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> int main() { double x; std::cout << "Enter value: "; while (! (std::cin>>x) ){ std::cout << "Bad input. Please enter a number: "; std::cin.clear(); while (std::cin.get() != ' ' ) continue; } std::cout << "Value = " << x << std::endl; return 0; }
4。重新编写下面的代码,使之使用using声明,而不是using编译指令。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> using namespace std; int main(){ double x; cout << "Enter value: "; while (! (cin>>x) ){ cout << "Bad input. Please enter a number: "; cin.clear(); while (cin.get() != ' ' ) continue; } cout << "Value = " << x << endl; return 0; }
答:修改为:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> using std::cin; using std::cout; using std::endl; int main() { double x; cout << "Enter value: "; while (! (cin>>x) ){ cout << "Bad input. Please enter a number: "; cin.clear(); while (cin.get() != ' ' ) continue; } cout << "Value = " << x << endl; return 0; }
5。在一个文件中调用average(3,6)函数时,它返回两个int参数平均值,在同一个程序的另一个文件中调用时,它返回两个int参数的double平均值。应如何实现?
答:需要将两个函数的链接性变为内部。具体方式为:
第一个文件,使用函数原型:static int average(int a,int b);然后在该文件中加入函数定义;
第二个文件,使用函数原型:static double average(int a,int b); 然后写对应的函数定义,并加入到该文件之中。
补充:也可以在未命名的名称空间中包含定义
6.下面的程序由两个文件组成,该程序显示什么内容?
//file1.cpp
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> using namespace std; void other(); void another(); int x = 10; int y; int main() { cout <<x <<endl; { int x = 4; cout << x << endl; cout << y << endl; } other(); another(); return 0; } void other() { int y = 1; cout << "Other: " << x << ", " << y << endl; }
//file2.cpp
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> using namespace std; extern int x; namespace { int y = -4; } void another() { cout << "another() " << x << ", " << y << endl; }
答:显示为:
10
4
0
Other: 10, 1
another(): 10, -4
7。下面的代码将显示什么内容?
#include <iostream> using namespace std; void other(); namespace n1 { int x = 1; } namespace n2 { int x = 2; } int main() { using namespace n1; cout << x << endl; { int x = 4; cout << x << ", " << n1::x << ", " << n2::x << endl; } using n2::x; cout << x << endl; other(); return 0; } void other() { using namespace n2; cout << x << endl; { int x = 4; cout << x << ", " << n1::x << ", " << n2::x << endl; } using n2::x; cout << x <<endl; }
显示:
1
4, 1, 2
2
2
4, 1, 2
2