变量和基本类型(二)
之前学过的基础性的东西我将不会在此再做重复,如果有一些我之前从未注意过或从未深思过的事情以及一些重点,我都将会在此写下。
默认状态下,const 对象仅在文件内有效。
如果想在多个文件之间共享 const 对象,必须在变量的定义之前添加 extern 关键字。
即:某些时候有这样一种 const 变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。我们想让这类 const 对象像其他(非 常量)对象一样工作,也就是说,只在一个文件中定义 const,而在其他多个文件中声明并使用它。
解决的办法是,对于 const 变量不管是声明还是定义都添加 extern 关键字,这样只需定义一次就可以了:
// file_1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn () ;
// file_ 1.h头文件
extern const int bufSize; //与 file_1.cc 中定义的 bufSize 是同一个
引用的类型必须与其所引用对象的类型一致,但是有两个例外。
第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式:
int i = 42;
const int &r1 = i; //允许将const int&绑定到一个普通int对象上
const int &r2 = 42; //正确:r1是一个常量引用
const int &r3 = r1 * 2;//正确:r3是一个常量引用
int &r4 = r1 * 2; //x //错误:r4是一个普通的非常量引用
要想理解这种例外情况的原因,最简单的办法是弄清楚当一个常量引用被绑定到另外一种类型上时到底发生了什么:
double dval = 3.14;
const int &ri = dval;
此处 ri 引用了一个 int 型的数。对 ri 的操作应该是整数运算,但 dval 却是一个双精度浮点数而非整数。因此为了确保让 ri 绑定一个整数,编译器把上述代码变成了如下形式:
const int temp = dval; //由双精度浮点数生成一个临时的整型常量
const int &ri = temp; //让ri绑定这个临时量
在这种情况下,ri 绑定了一个临时量(temporary)对象。所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。C++程序员们常常把临时量对象简称为临时量。
指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。
顶层 const(top-level const)表示指针本身是个常量,
底层 const(low-level const)表示指针所指的对象是一个常量。
更一般的,顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层 const 则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层 const 也可以是底层 const,这一点和其他类型相比区别明显。
当执行对象的拷贝操作时,顶层 const 不受什么影响。执行拷贝操作并不会改变被拷贝对象的值,因此,拷入和拷出的对象是否是常量都没什么影响。
另一方面,底层 const 的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:
int i = 0;
int *const pl = &i;
//不能改变p1的值,这是一个顶层const
const int ci = 42;
//不能改变ci的值,这是一个顶层const
const int *p2 = &ci;
//允许改变p2的值,这是一个底层const
const int *const p3 = p2;
//靠右的const是顶层const,靠左的是底层const
const int &r = ci;
//用于声明引用的const都是底层const
int *p = p3;
//错误:p3包含底层const的定义,而p没有
p2 = p3;
//正确:p2和p3都是底层const
p2 = &i;
//正确:int*能转换成const int*
int &r = ci;
//错误:普通的int&不能绑定到int常量上
const int &r2 = i;
//正确:const int&可以绑定到一个普通int上
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的 const 对象也是常量表达式。
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如:
const int max_files = 20; // max_files是常量表达式
const int limit = max_files + 1;// limit是常量表达式
int staff_size = 27; // staff_size不是常量表达式
const int sz = get_size ();// sz不是常量表达式
尽管 sz 本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。
C++11标准规定,允许将变量声明为 constexpr 类型以便由编译器来验证变量的值是否是一个常量表达式。声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式初始化。
一般来说,如果你认定变量是一个常量表达式,那就把它声明成 constexpr 类型。
特殊的:C++11标准允许定义一种特殊的 constexpr 函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用 constexpr 函数去初始化 constexpr 变量了。
算术类型、枚举类型、引用和指针都属于字面值类型。
自定义类、 IO库、string 类型则不属于字面值类型,也就不能被定义成 constexpr。
数据成员都是字面值类型的聚合类(第七章 7.5.5)也是字面值常量类。
如果一个类不是聚合类,但它符合下述要求,则它也是一个字面值常量类:
- 数据成员都必须是字面值类型。
- 类必须至少含有一个 constexpr 构造函数。
- 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式:或者如果成员属于某种类类型,则初始值必须使用成员自己的 constexpr 构造函数。
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
指针和引用都能定义成 constexpr,但它们的初始值却受到严格限制。
一个 constexpr 指针的初始值必须是 nullptr、0 或者是存储于某个固定地址中的对象。
函数体内定义的变量一般来说并非存放在固定地址中,因此 constexpr 指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始 constexpr 指针。还有,允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址,因此,constexpr 引用能绑定到这样的变量上,constexpr 指针也能指向这样的变量。
必须明确一点,在 constexpr 声明中如果定义了一个指针,限定符 constexpr 仅对指针有效,与指针所指的对象无关:
const int *p = nullptr;
// p是一个指向整型常量的指针
constexpr int *q = nullptr;
// q是一个指向整数的常量指针
p 和 q 的类型相差甚远,p 是一个指向常量的指针,而 q 是一个常量指针,其中的关键在于 constexpr 把它所定义的对象置为了顶层 const 。
与其他常量指针类似,constexpr 指针既可以指向常量也可以指向一个非常量:
constexpr int *np = nullptr; // np是一个指向整数的常量指针,其值为空
int j=0;
constexpr int i = 42; // i的类型是整型常量
// i和j都必须定义在函数体之外
constexpr const int *p = &i; // p是常量指针,指向整型常量i
constexpr int *p1 = &j; // p1 是常量指针,指向整数j
类型别名(type alias)是一个名字,它是某种类型的同义词。
传统的方法是使用关键字 typedef,C++11标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名:
using SI = Sales_item;
// SI是Sales_ item的同义词
如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。例如下面的声明语句用到了类型 pstring,它实际上是类型 char * 的别名:
typedef char *pstring;
const pstring cstr = 0;
//cstr是指向char的常量指针
const pstring *ps;
// ps是一个指针,它的对象是指向char的常量指针
上述两条声明语句的基本数据类型都是 const pstring,和过去一样, const 是对给定类型的修饰。pstring 实际上是指向 char 的指针,因此,const pstring 就是指向 char 的常量指针,而非指向常量字符的指针。
遇到一条使用了类型别名的声明语句时,人们往往会错误地尝试把类型别名替换成它本来的样子,以理解该语句的含义:
const char *cstr = 0;
//是对const pstring cstr的错误理解
再强调一遍:这种理解是错误的。声明语句中用到 pstring 时,其基本数据类型是指针。可是用 char* 重写了声明语句后,数据类型就变成了 char,* 成为了声明符的一部分。
这样改写的结果是,const char 成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向 char 的常量指针,而后者则声明了一个指向 const char 的指针。
auto 一般会忽略掉顶层 const ,同时底层 const 则会保留下来,
如果希望推断出的auto类型是一个项层const,需要明确指出:
const auto f = ci; //ci的推演类型是int,f是const int
还可以将引用的类型设为 auto,此时原来的初始化规则仍然适用:
auto &g = ci;
// g是一个整型常量引用,绑定到ci
auto &h = 42;
//错误:不能为非常量引用绑定字面值
const auto &j = 42;
//正确:可以为常量引用绑定字面值
要在一条语句中定义多个变量,切记,符号 & 和 * 只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须是同一种类型:
auto k = ci, &X = i; // k是整数,X是整型引用
auto &m = ci, *p = &ci; // m是对整型常量的引用,p是指向整型常量的指针
auto &n = i, *p2 = &ci; // 错误: i的类型是int而&ci的类型是const int
有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求,C++11标准引入了第二种类型说明符 decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
编译器并不实际调用函数 f,而是使用当调用发生时f的返回值类型作为 sum 的类型。换句话说,编译器为sum指定的类型是假如 f 被调用的话将会返回的那个类型。
decltype 处理顶层 const 和引用的方式与 auto 有些许不同。如果 decltype 使用的表达式是一个变量,则 decltype 返回该变量的类型(包括顶层 const 和引用在内):
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的类型是const int
decltype(cj) y = x; // y的类型是const int&,y绑定到变量x
decltype(cj) z; // 错误:z是一个引用,必须初始化
因为 cj 是一个引用,decltype(cj) 的结果就是引用类型,因此作为引用的 z 必须被初始化。
需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在 decltype 处是一个例外。
如果 decltype 使用的表达式不是一个变量, 则 decltype 返回表达式结果对应的类型。
// decltype的结果可以是引用类型
int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // 正确:加法的结果是int,因此b是一个(未初始化的) int
decltype(*p) c; // 错误: c是int&,必须初始化
因为 r 是一个引用,因此 decltype(r)的结果是引用类型。如果想让结果类型是 r 所指的类型,可以把 r 作为表达式的一部分,如 r + 0,显然这个表达式的结果将是一个具体值而非一个引用。
另一方面,如果表达式的内容是解引用操作,则 decltype 将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p) 的结果类型就是 int&,而非 int。
切记:decltype((variable))(注意是双层括号)的结果永远是引用,而 decltype(variable) 结果只有当 variable 本身就是一个引用时才是引用。
一般来说, 最好不要把对象的定义和类的定义放在一起。这么做无异于把两种不同实体的定义混在了一条语句里,一会儿定义类,一会儿又定义变量,显然这是一种不被建议的行为。
对类内初始值的限制:
或者放在花括号里,或者放在等号右边,记住不能使用圆括号。