书签:《c++第6版》9.3 名称空间 p324
7、函数探幽
7.1 内联函数
C++内联函数 内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。
- 在函数声明前加上关键字inline;
- 在函数定义前加上关键字inline。
通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。
inline double square (double x) {
return x * x; }
注意到整个函数定义都在一行中,但并不一定非得这样做。然而,如果函数定义占用多行(假定没有使用冗长的标识符),则将其作为内联函数就不太合适。
7.2 引用变量
引用变量
//创建引用变量
int rats;
int & rodents = rats; //makes rodents an alias for rats
int * const pr = &rats;//第二行实际上是这一行的伪装表示,引用 rodents 扮演的角色与表达式*pr相同
其中,&
不是地址运算符,而是类型标识符的一部分。就像声明中的char*
指的是指向char
的指针一样,int&
指的是指向int
的引用。上述引用声明允许将rats
和rodents
互换——它们指向相同的值和内存单元。
将引用参数声明为常量数据的引用的理由有三个:
- 使用const可以避免无意中修改数据的编程错误;
- 使用const使函数能够处理const和非const实参,否则将只能接受非const数据;
- 使用const引用使函数能够正确生成并使用临时变量。
因此,应尽可能将引用形参声明为const。
假设有这样一个结构:
struct free_throws
{
std::string name ;
int made;
int attempts;
float percent;
}
//则可以这样编写函数原型,在函数中将指向该结构的引用作为参数:
void set_pc(free_throws & ft);// use a reference to a structure
//如果不希望函数修改传入的结构,可使用const:
void display(const free_throws & ft);// don't allow changes to structure
使用引用参数的原因:
- 程序员能够修改调用函数中的数据对象。
- 通过传递引用而不是整个数据对象,可以提高程序的运行速度。
使用指针或引用的指导原则:
1、对于使用传递的值而不作修改的函数。
- 如果数据对象很小,如内置数据类型或小型结构,则按值传递。
- 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向
const
的指针。 - 如果数据对象是较大的结构,则使用
const
指针或const
引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间。 - 如果数据对象是类对象,则使用
const
引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。
2、对于修改调用函数中数据的函数:
- 如果数据对象是内置数据类型,则使用指针。如果看到诸如
fixit(&x)
这样的代码(其中x是int
),则很明显,该函数将修改x。 - 如果数据对象是数组,则只能使用指针。
- 如果数据对象是结构,则使用引用或指针。
- 如果数据对象是类对象,则使用引用。
7.3 默认参数&函数重载
默认参数&函数重载
//默认参数
char * left(const char * str, int n = 1);//参数有默认值
//函数重载
char * left(const char * str,unsigned n);// two arguments
char left(const char * str) ;//one argument
7.4 函数模板
函数模板
template <class 类型参数1, class 类型参数2, ...>
返回值类型 模板名(形参表)
{
函数体
};
-------------------------------
// 函数模板例子,交换函数,int, double, ..
// myfirst.cpp
#include <iostream>
using namespace std;
//function template prototype
template <typename T> //or class T
void Swap(T &a, T &b);
template <typename T> //new template
void Swap(T *a, T *b, int n);
void Show(int a[]);
const int Lim = 8;
int main()
{
int i = 10;
int j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using compiler-generated int swapper:\n";
Swap(i, j);
cout << "Now i,j = " << i << ", " << j << ".\n";
int d1[Lim] = {
0,7,0,4,1,7,7,6 };
int d2[Lim] = {
0,7,2,0,1,9,6,9 };
cout << "Original arrays:\n";
Show(d1);
Show(d2);
Swap(d1, d2, Lim);
cout << "Swapped arrays:\n";
Show(d1);
Show(d2);
cin.get();//pause
return 0;
}
// function template definition
template <typename T> //or class T
void Swap(T &a, T &b)
{
T temp; // temp a variable of type T
temp = a;
a = b;
b = temp;
}
template <typename T>
void Swap(T a[], T b[], int n)
{
T temp;
for (int i = 0;i < n;i++)
{
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
void Show(int a[])
{
cout << a[0] << a[1] << "/";
cout << a[2] << a[3] << "/";
for (int i = 4;i < Lim;i++)
{
cout << a[i];
}
cout << endl;
}
函数模板可以重载,只要它们的形参表或类型参数表不同即可。
函数模板和函数的次序,在有多个函数和函数模板名字相同的情况下,编译器如下处理一条函数调用语句。
- 1)先找参数完全匹配的普通函数(非由模板实例化而得的函数)。
- 2)再找参数完全西配的模板函数。
- 3)再找实参数经过自动类型转换后能够匹配的普通函数。
- 4)上面的都找不到,则报错。
8、内存模型与名称空间
8.1 单独编译
一般将代码分为:
- 头文件:包含结构声明和使用这些结构的函数的原型。
- 源代码文件:包含与结构有关的函数的代码。
- 源代码文件:包含调用与结构相关的函数的代码。
头文件常包含的内容:
- 函数原型。
- 使用
#define
或const
定义的符号常量。 - 结构声明。
- 类声明。
- 模板声明。
- 内联函数。.
不要将函数定义和变量声明放到头文件中!在包含头文件时,我们使用
“coordin.h"
,而不是<coodin.h>
。如果文件名包含在尖括号中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;但如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录(或其他目录,这取决于编译器)。如果没有在那里找到头文件,则将在标准位置查找。因此在包含自己的头文件时,应使用引号而不是尖括号。
8.2 存储持续性、作用域、链接性
存储持续性
C++11使用四种不同的方案来存储数据,这些方案的区别就在于数据保留在内存中的时间。
- 自动存储持续性:在函数定义声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码时,它们使用的内存被释放。C++有两种存储持续性为自动的变量。
- 静态存储持续性:在函数外部定义的变量和使用的关键字static 定义的变量的存储持续性都为静态。它们在程序运行的过程中都存在。C++有三种持续性为静态的变量。
- 线程存储持续性(C++11):当前,多核处理器很常见,这些CPU可同时处理多个执行任务。这让程序能够将计算机放在可并行处理的不同线程中。如果变量是使用关键字thread_local声明的,则其生命周期与所属的线程一样长。
- 动态存储持续性:用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储(free strore)或堆(heap)。
作用域
- 作用域为全局也叫文件作用域。
- 自动变量的作用域为局部。
- 静态变量的作用域是全局还是局部取决于它是如何被定义的。
- 在函数原型作用域中使用的名称只在包含参数列表的括号内可用,这就是为什么这些名称是什么以及是否出现都不重要的原因。
- 在类中声明的成员作用域为整个类。
- 在名称空间中生命的变量作用域为整个名称空间,由于名称空间已经引入到C++语言中,因此全局作用域是名称空间作用域的特例。
不同的C++存储方式是通过存储持续性、作用域和连接性来描述的,在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。
关键字register
最初是由C语言引入的,它建议编译器使用CPU寄存器来存储自动变量:
register int count_fast;
这旨在提高访问变量的速度。
链接性
- 静态持续变量
和C语言一样,C++也为静态存储持续性变量提供了3种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块中访问)。这3种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的装置(如栈)来管理它们。编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。另外,如果没有显式地初始化静态变量,编译器将把它设置为0。在默认情况下,静态数组和结构将每个元素或成员的所有位都设置为0。
要想创建链接性为外部的静态持续变量,必须在代码块的外面声明它;要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用
static
限定符;要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static
限定符。
5种变量存储方式:
- 所有的静态持续变量都有下述初始化特征:未被初始化的静态变量的所有位都被设置为0。这种变量被称为零初始化的(zero-initialized)。
- 对静态变量初始化可采用:零初始化、常量表达式初始化、动态初始化。零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译器处理文件(翻译单元)时初始化变量。动态初始化意味着变量将在编译后初始化。零初始化>常量表达式初始化>动态初始化(可简单理解)
- 链接性为外部的变量通常简称伪外部变量,它们的存储持续性为静态,作用域为整个文件。
单定义规则
- 一方面,在每个使用外部变量的文件中,都必须声明它;另一方面,C++有“单定义规则”(One Definition Rule,ODR),该规则指出,变量只能有一次定义。为满足这种需求,C++提供了两种变量声明,一种是定义声明(defining declaration)或简称定义(definition),它给变量分配存储空间;另一种是引用声明(referencing declaration)或简称声明(declaration),它不给变量分配存储空间,因为它引用已有的变量。引用声明使用关键字
extern
,且不进行初始化;否则,声明为定义,导致分配存储空间。 - 如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义(单定义规则),但在使用该变量的其他所有文件中,都必须使用关键字extern声明它。
- 注意:在多文件程序中,可以在一个文件(且只能在一个文件)中定义一个外部变量。使用该变量的其他文件必须使用关键字extern声明它。
- 无链接性的局部变量:创建方式为在代码块中将
static
限定符作用在变量前。这意味着虽然该变量只在该代码块中可用,但它在该代码块不处于活动状态时仍然存在。因此在两次函数调用之间,静态局部变量的值将保持不变。以后再调用函数时,将不会像自动变量那样再次被初始化。
存储说明符有:auto
(在C++11中不再是说明符)、register
、static
、extern
、thread_local
(C++11新增的)、mutable
。在同一个声明中不能使用多个说明符,但thread_local
除外,它可与static
、extern
结合使用。
auto
:在C++11之前,可以在声明中使用关键字auto指出变量为自动变量;但在C++11中,auto用于自动类型判断。register
:关键字register用于在声明中指示寄存器存储,而在C++11中,它只是显示地指出变量是自动的。static
:关键字static被用在作用域为整个文件的声明中,表示内部链接性;被用于局部声明中,表示局部变量的存储持续性为静态的。extern
:关键字extern表明是引用声明,即声明引用在其他地方定义的变量。thread_local
:关键字thread_local指出变量的持续性与其所属线程的持续性相同。thread_local变量之于线程,犹如常规静态变量之于整个程序。mutable
:关键字mutable的含义将根据const来解释。可以用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
cv-限定符:c代表const,v代表volatile;它表明内存初始化之后,程序便不能对它进行修改了。
volatile
关键字volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。该关键字的作用是为了改善编译器的优化能力。例如,假设编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在这两次使用之间不会发生变化。如果不将变量声明为volatile,则编译器将进行这种优化;将变量声明为volatile,相当于告诉编译器,不要进行这种优化。const
:在C++(但不是在C语言)中,const限定符对默认存储类型稍有影响。在默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的。也就是说,在C++看来,全局const定义就像使用了static说明符一样。出于某种原因,程序员希望某个常量的链接性为外部的,则可以使用extern关键字来覆盖默认的内部链接性。
函数和链接性 和变量一样,函数也有链接性,虽然可选择的范围比变量小。C++不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都自动为静态的,即在整个程序执行期间都一直存在。
- 在默认情况下,函数的链接性为外部的,即可以在文件间共享。
- 还可以使用关键字static将函数的链接性设置为内部的,使之只能在一个文件中使用。必须同时在原型和函数定义中使用该关键字。这意味着该函数只在这个文件中可见,还意味着可以在其他文件中定义同名的函数。
- 和变量一样,在定义静态函数的文件中,静态函数将覆盖外部定义,因此即使在外部定义了同名的函数,该文件仍将使用静态函数。
- 单定义规则也适用于非内联函数,因此对于每个非内联函数,程序只能包含一个定义。对于链接性为外部的函数来说,这意味着在多文件程序中,只能有一个文件(该文件可能是库文件,而不是您提供的)包含该函数的定义,但使用该函数的每个文件都应包含其函数原型。
- 内联函数不受这项规则的约束,这允许程序员能够将内联函数的定义放在头文件中。这样,包含了头文件的每个文件都有内联函数的定义。然而,C++要求同一个函数的所有内联定义都必须相同。
- C++找函数的过程:若静态,在该文件中找->否则,在所有程序文件中查找。若找到两个定义,报错->若上一步没找到,在库文件中查找。c++保留了标准库函数的名称,程序员不应该使用他们。
语言链接性:链接程序要求每个不同的函数都有不同的符号名。
- C语言链接性:在C语言中,一个名称只对应一个函数,因此这很容易实现。为满足内部需要,c语言编译器可能将spiff这样的函数名翻译为_spiff。这种方法被称为C语言链接性(C language linkage)。
- C++语言链接性:在C++中,同一个名称可能对应多个函数,必须将这些函数翻译为不同的符号名称。因此,C++编译器执行名称矫正或名称修饰,为重载函数生成不同的符号名称。例如,可能将spiff(int)转换为_spiff_i,将spiff(double,double)转换为_spiff_d_d。这种方法被称为C++语言链接(C++ language linkage)
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++标准指定的说明符,但实现可提供其他语言链接性说明符。
通常情况下:编译器使用三块独立的内存:一块用于静态变量(可能再细分)、一块用于自动变量、另一块用于动态存储。最好用delete来释放new分配的内存。
定位new运算符
#include <new>
struct chaff
{
char dross[20];
int slag;
};
char buffer1[50];
char buffer2[500];
int main()
{
chaff *p1, *p2;
int *p3, *p4;
//first, the regular forms of new
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 int array in buffer2
}
delete 只能用于指向常规new 运算符分配的堆内存。不能用于静态内存。
定位new运算符的工作原理:基本上,它只是返回传递给它的地址,并将其强制转换为 void * ,以便能够赋给任何指针类型。这说的是默认定位new函数,C++允许程序员重载定位new函数。就像常规new 运算符调用一个接收一个参数一样,标准定位new调用一个接收两个参数的new()函数:
int *pi = new int; //invokes new(sizeof(int))
int *p2 = new(buffer) int; //invokes new(sizeof(int),buffer)
int *p3 = new(buffer) int[40] //invokes new[40*sizeof(int),buffer)
定位new函数不可替换,但可重载。它至少需要接收两个参数,其中第一个总是std::size_t,指定了请求的字节数。这样的重载函数都被称为定义new,即使额外的参数没有指定位置。
8.3 名称空间
下面先来看几个概念:
- 声明区域(declaration region):声明区域是可以在其中进行声明的区域。例如,可以在函数外面声明全局变量,对于这种变量,其声明区域为其声明所在的文件。对于在函数中声明的变量,其声明区域为其声明所在的代码块。
- 潜在作用域(potential scope):变量的潜在作用域从声明点开始,到其声明区域的结尾。因此潜在作用域比声明区域小,这是由于变量必须定义后才能使用。
- 作用域(scope):变量对程序而言可见的范围。
- C++新增了这样一种功能,即通过定义一种新的声明区域来创建命名的名称空间,这样做的目的之一是提供一个声明名称的区域。一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突,同时允许程序的其他部分使用该名称空间中声明的东西。名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的( 除非它引用了常量)。
- 除了用户定义的名称空间外,还存在另一个名称空间——全局名称空间( global namespace)。 它对应于文件级声明区域,因此前面所说的全局变量现在被描述为位于全局名称空间中。任何名称空间中的名称都不会与其他名称空间中的名称发生冲突。
通过作用域解析运算符::
来访问给定名称空间中的名称。如果每使用这样一个变量都要这样写,那会不会太麻烦,因此C++有了using声明和using 编译指令。using 声明使特定的标识符可用,using 编译指令使整个名称空间可用。
using Jill::fetch; //a using declaration
// 之后就可以用fetch 代替using Jill::fetch 了。
using namespace Jill; //make names available globally
// 之后名称空间Jill中的所有变量都可以简化使用了。
使用using编译指令导入一个名称空间中所有的名称与使用多个using声明是不一样的,而更像是大量使用作用域解析云玄素。使用using声明时,就好像声明了相应的名称一样。如果某个名称已经在函数中声明了,则不能用using声明导入相同的名称。然而使用using编译指令时,将进行名称解析,就像在包含using声明和名称空间本身的最小声明区域中声明了名称一样。
上面这段话比较费解,可以简单地理解为:假设名称空间和声明区域定义了相同的名称。如果试图使用using声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突,从而出错。如果使用using编译指令将该名称空间的名称导入该声明区域,则区域版本将隐藏名称空间版本。若使用区域解析符::
则还可以对名称空间的变量进行访问。
名称空间的其他特性:
- 名称空间可以进行嵌套;
- 可以在名称空间中使用using编译和using声明。
- using编译指令是可传递的,也就是说在A名称空间使用了
using B
,那么using A;
就相当于using A; using B;
- 可以给名称空间创建别名:
namespace mvft=my_very_favorite_things;
- 简化对嵌套名称的使用:
namespace MEF=myth::elements::fire;
using MEF::flame;
- 可以通过省略名称空间的名称来创建未命名的名称空间;不能在未命名名称空间所属文件之外的其他文件中,使用该名称空间中的名称。这提供了链接性为内部的静态变量的替代品。
namespace
{
int ice;
int bandycoot;
}
名称空间统一的使用规则
- 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量。
- 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
- 如果开发了一个函数库或类库,将其放在一个名称空间中。
- 仅将编译指令using作为一种将旧代码转换为名称空间的权宜之计。
- 不要在头文件中使用using编译指令。首先,这样做掩盖了要让哪些名称可用;另外,包含头文件的顺序可能影响程序的行为。如果非要使用编译指令using,应将其放在所有预处理器编译指令#include之后。
- 导入名称时,首选使用作用域解析运算符或using声明的方法。
- 对于using 声明,首选将其作用域设置为局部而不是全局。
- 使用名称空间的主旨是简化大型编程项目的管理工作。对于只有一个文件的简单程序,使用using编译指令并非什么大逆不道的事。
- 老式头文件(如
iostream.h
)没有使用名称空间,但新头文件iostream
使用了std
名称空间。