9.关于常量
- 程序中直接出现的各种进制数字、字符或字符串等也是一种常量,叫做字面常量
- 字面常量只能引用,不能修改,字面常量一般是保存在程序的符号表里而不是数据区。符号表是只读的。
- 除了字符串,无法获取一个字面常量的地址。int *p = &5;这类语句是非法的。
- 用标识符表示的常量称为符号常量。符号常量有两种:#define定义的宏常量和const定义的常量。其实对于宏常量来说,由于在编译阶段前,就已经被替换为所代表的字面常量了,所以宏常量本质上是字面常量。
- const定义的常量在C和C++中有些许不同。C中的const定义的常量是值不能修改的变量,是会分配内存空间的;而在C++中,如果是基本类型的const常量,编译器会把它们放到符号表中而不分配内存,对于构造类型的const对象,则会分配内存空间。
- 可以取一个const常量的地址,但对于基本类型的const常量,编译器其实是重新在内存中创建了一个它的拷贝,你通过地址访问的其实这个拷贝而非原始的符号常量。而对于构造类型的const常量,它其实是编译时不允许修改的变量,但其实有办法绕过编译器的安全检查机制,而在运行时修改它的值的。编译器“防君子不防小人”。
- 从理论上讲,只要手中握有一个对象的指针,就可以设法绕过编译器随意修改它的内容,除非该内存受到操作系统的保护。
- C语言中const符号常量是默认外连接的(分配内存),所以不能在两个及两个以上的编译单元中定义一个同名的const符号常量,也不能把const符号常量定义放在一个头文件中。但在C++中,const符号常量是默认内连接的,因此可以定义在头文件中,编译器会认为它们是不同的符号常量,每个编译单元编译时会分别为它们分配内存空间,而在连接时进行常量合并。
- 建议尽量使用含义直观的符号常量来表示那些将在程序中多次出现的数字或字符串。
- 在C++中将需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。可以把不同模块的常量集中存放在一个公用的头文件中。
- 如果某一常量与其他常量密切相关,应在定义中包含这种关系。比如:
const double RADIUS = 100;
const double DIAMETER = RADIUS*2;
- C++中应尽量使用const定义符号常量,包括字符串常量。因为const常量有数据类型,而宏常量没有数据类型。编译器可以对const常量进行静态类型安全检查,宏常量是没有安全检查的。另外,有些调试工具是可以对const常量进行调试的,而宏常量是不能调试的。const的用途还有很多,以后会提到。
- 在类中,用const修饰的非静态成员,只在某个对象的生存期限内是常量,而不同的对象却可以是不同的值,因此对整个类来说,它是可变的。
- 不能在类声明中初始化非静态的const数据成员。非静态的const数据成员只能在类的构造函数的初始化列表中进行初始化。
- 想要建立整个类中的常量,可以定义静态const常量,也就是static const常量。这个常量的作用范围是整个类;另外还有一种方法定义类范围的常量,就是利用枚举常量。在类声明中构造一个匿名枚举类型,内部写入常量定义即可。
class A
{
enum
{
SIZE1 = 100;
SIZE2 = 200;
};
int array1[SIZE1];
int array2[SIZE2];
};
枚举常量不会占用存储空间,它们在编译时就会被全部求值。缺点是不能表示浮点数和字符串。
-
当希望在多个编译单元定义统一的符号常量时,C和C++一般有如下方法。
C语言中:
方法一:在某个公用头文件中将符号常量定义为static并初始化,然后每个使用它的编译单元#include这个头文件,或者在头文件中使用宏定义。
方法二:在某个公用头文件中将符号常量声明为extern的,并在某个源文件中定义一次,然后每一个使用它的编译单元#include上述头文件。
方法三:如果是整型常量,在某个公用头文件中定义enum类型,然后每一个使用它的编译单元#include这个头文件。
C++中:
方法一:在某个公用头文件中直接在某个名字空间或全局名字空间中定义符号常量并初始化,有无static无所谓。然后每一个使用它的编译单元#include这个头文件。
方法二:在某个公用头文件并且在某个名字空间或者全局名字空间中将符号常量声明为extern,并在某个源文件中定义一次并初始化,然后每一个使用它的编译单元#include上述头文件。
方法三:如果是整型常量,在某个公用头文件中定义enum类型,然后每一个使用它的编译单元#include这个头文件。
方法四:定义为某一个公用类的static const数据成员并初始化,还活着定义为类内的枚举类型,然后每一个使用它的编译单元#include该类的定义。 -
上一条中,符号常量如果在头文件中定义并初始化(方法一和方法三),那么包含了该头文件的每一个编译单元会为每一个常量创建一个独立的拷贝内容,单元内访问的其实是那份拷贝,只有在连接时才会进行常量合并。而如果是在源文件或者类中初始化(方法二和方法四),那每个编译单元访问的都是同一个常量,而不会有拷贝一说。这点在一般情况下没什么感觉,但是当常量为字符串常量时,内存消耗区别可能会有影响。
10.关于函数
- 当你需要某种功能的函数时,首先应该查看现有的库是否提供了类似的函数。不要编写函数库中已有的函数,这不仅是重复劳动,而且自己编写的函数在各个质量属性方面一般都不如对应的库函数。
- 对于静态链接库的函数或类库,如果程序中调用了其中的函数,编译器才会从相应的库中提取这些函数的代码连接到你的程序中。否则编译器是不会把库文件的代码连接进来的。所以,不用担心仅使用库文件中一小部分功能却包含整个库而导致代码体积增大。
- 如果使用动态链接库(dll),则需要将所有dll都复制到运行环境的相应目录下。
- 早期的C语言有函数前置声明的概念,格式是:
函数返回类型 函数名();
函数前置声明并没有给出函数可接受的参数类型和个数。所以现在一般不用函数前置声明,而是使用函数原型。
87. 函数原型能够告诉编译器一个函数的作用域、返回值的数据类型、调用规范、连接规范、函数名、可以接受的参数个数与类型及其顺序,编译器可以进行静态类型安全检查。格式如下:
[作用域] [函数的连接规范] 返回类型 [函数的调用规范] 函数名(类型1 [形参名1],类型2[形参名2],...);
- 形参是指在函数原型或定义及catch语句的参数列表中被声明的对象或指针、宏定义中的参数、模板定义中的类型参数等。实参是指函数调用语句中以逗号分割的参数列表中的表达式、宏调用语句中以逗号分割的列表中一个或多个预处理标识符序列、throw语句的操作数、表达式的操作数、模板实例化时的实际类型参数等。函数调用的参数传递本质是用实参来初始化形参而不是替换形参。
- 建议在函数原型中写出形参名,虽然编译器会忽略它们。不要再函数体内定义于形参同名的局部变量,形参也被看做是本地变量。
- 函数定义的语法:
[函数的连接规范] 返回值类型 [函数的调用规范] 函数名(类型1 [形参名1],类型2[形参名2],...)
{
函数体语句序列;
}
该函数至少被调用一次,编译器才会为函数定义体生成对应的可执行代码。
-
函数调用是通过堆栈完成的。函数堆栈使用的是程序的堆栈段内存。虽然程序堆栈段是系统为程序分配的静态数据区,但函数堆栈是在调用时才动态分配的。函数堆栈在进入函数前保存环境变量和返回地址、在进入函数时保存实参的拷贝、在函数体内保存局部变量。
-
函数堆栈的使用与函数的调用规范相关。调用规范决定了函数的实参压栈、退栈及堆栈的释放方式,以及函数名改编方案。Windows下常用的调用规范包括:
__cdecl:C++/C函数的默认调用规范,参数从右向左依次传递压入堆栈,由调用函数负责堆栈的清退,可以传递个数可变的参数。
__stdcall:Win API函数使用的调用规范。参数从右向左依次传递压入堆栈,由被调用函数负责堆栈的清退。这个规范生成的函数代码比__cdecl小,但当函数参数个数可变时,会自动转为__cdecl规范。
__thiscall:是C++非静态成员函数的默认调用规范,不能使用个数可变的参数。调用的时候,this指针直接保存在ECX寄存器中。
__fastcall:该规范的函数实参会被传递到CPU的寄存器中而不是内存堆栈中。堆栈的清退由被调用函数负责。这个规范不能用于成员函数。 -
注意,程序的默认规范不同于环境的默认规范。如果某个DLL没有显式指定调用规范,它就会使用环境的默认规范,而你的程序使用自己的默认规范不同于环境的默认规范则可能编译和连接通过,运行时崩溃。
-
函数的参数和返回值传递方式包括:值传递和地址传递(即指针传递)。C++中增加了引用传递。
-
不论原型还是定义,都要明确写出每个参数的类型和名字。如果函数没有参数,应该使用void而不要空着。这是因为空的参数列表在C和C++中有不同的解释。在C中,空的参数列表被解释为可以接受任何类型和个数的参数,而在C++中则表示不可以接受任何参数。
-
参数列表中的参数名应命名恰当,顺序应合理。一般把输出参数放在前面,输入参数放在后面,不要交叉出现输入和输出参数。
-
如果参数是指针,且仅做输入用,则在类型前加const,可以防止指针指向的内存单元在函数体内无意中被修改。
-
应避免函数有太多的参数,参数个数应尽量控制在5个以内。如果参数太多,可以将这些参数封装在一个对象中,并采用地址传递或引用传递方式。
-
尽量不要使用类型和数目不确定的参数列表。
-
不要省略返回值的类型,如果确实没有返回值,应声明为void类型。
-
不要将正常值和错误标志混在一起返回。建议正常值使用输出参数获得,错误标志使用return返回。
-
如果函数返回值是一个对象,有些场合可以使用“返回引用”方式替换“返回对象值”,可以提高效率。但要注意函数返回时的局部变量会被销毁的行为动作。所以有些场合下,只能使用“返回对象之”而不能用“返回引用”。
-
return不可返回指向“堆栈内存”的指针或引用。因为该内存单元在函数体结束时会被释放。
-
要搞清楚返回的究竟是对象的值、对象的指针还是对象的引用。如果返回值是一个对象,需要考虑效率问题。
return (x + y);
和
temp = x + y;
return temp;
效率差很多,尤其x和y是复杂对象的情况,后者的temp会比前者多出拷贝构造和析构的开销。
- 函数功能要单一,一个函数只完成一件事,尽量控制在50行以内。
- 设计函数要检查输入参数的有效性,同时,需要检查通过其他途径进入函数体的变量的有效性,如全局变量,文件句柄等。
- 尽量避免函数带有“记忆”功能,相同的输入应当具有相同的输出。建议尽量少用static局部变量。
- 默认情况下,全局变量和全局函数的存储类型是extern的,能够被定义在它们之后的同一编译单元内的函数调用。如果被显式的加上extern声明,则其他编译单元中的函数也能调用它们。
- 局部变量默认auto存储属性,除非用static或register定义。不论如何,它们的作用域是程序块,连接类型是内连接,在进入函数时创建,退出时销毁。register和auto只能用于声明局部变量和局部常量。
- 全局变量默认存储类型是static的,要想在定义了它的编译单元外的其他编译单元中使用这个全局变量,需要显式使用extern声明。否则不能被访问。
- 局部符号常量默认auto类型。函数的形参是局部变量,最好不要声明为static的。
- 标号(label)是具有函数作用域的唯一一种标识符。一般用在goto语句中。局部变量的作用域其实是程序块({})。
- 内层变量会遮蔽外层同名变量。当局部变量与全局变量同名,函数内会遮蔽全局变量,恶意使用域解析运算符(::)来引用全部变量。
- 尽管语法允许,但不要再内层程序块中定义会遮蔽外层程序块中同名标识符。
- 复杂数据结构的所有成员具有类作用域。类的非静态成员函数可以直接访问类的其他任何成员。作用域外只能使用指针和点运算符访问。
- 成员函数中的局部变量与成员变量重名时,成员变量被屏蔽,需要使用this指针访问成员变量。
- 连接类型中外连接是指一个标识符能够在其他单元中或者定义它的单元中的其他范围内被调用。外连接标识符是需要分配运行时的存储空间的。
- 内连接能在定义它的编译单元中其他范围内被调用,但不能在其他编译单元中调用。无连接是仅能在声明它的范围内被调用。
- 递归函数是函数直接或间接调用自己。函数内必须首先检测返回条件。任何能够递归的问题都可以用迭代来实现。迭代运行的开销更小,而递归更能直接描述问题,两者需要取舍。
- 不要使用间接递归。会损害程序的清晰性。
- 在函数入口处建议使用断言(assert)来检测函数参数的有效性。
- assert语义为:表达式为0(假),则输出错误消息,并种植执行。表达式为真,则不进行任何操作。
- assert是宏,只在程序的debug版本中有效,在release版本中无效。使用assert时要加注释,告诉别人assert要干什么。
- 对于函数的输出参数,不能加const修饰,否则将失去输出能力。函数的输入参数如果采用“指针传递”,加const修饰可以防止该指针指向的内存被改动,起保护作用。
- 对于函数的“值传递”的输入函数,一般不加const修饰。对于复杂数据类型建议不使用“值传递”参数,而使用引用传递,并用const修饰,这样可以提高运行效率。