《C++ Primer》第2章 变量和基本类型(中)
2.3 复合类型
复合类型:基于其他类型定义的类型。
定义变量的格式为:
数据类型 变量名; //如int i;
更通用的:
数据类型 声明符;
之前接触的声明语句中,声明符就是变量名。其实有更复杂的声明符,它基于基本数据类型得到更复杂的类型,并把它指定给变量。
C++有几种复合类型,这里介绍其中两种:引用和指针。
2.3.1 引用
C++11新增了“右值引用”,将在第十三章详细介绍。严格来说,使用术语“引用”时,指的其实是“左值引用”。
引用:为对象起了另外一个名字。其声明符为&d
的形式,其中d
为变量名。
int ival = 1024;
int &refVal = ival; //refVal指向ival(是ival的另一个名字)
int &refVal2; //错误:引用必须被初始化
在定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另一个对象,因此引用必须初始化。
引用即别名
引用并非对象,它只是为一个已经存在对象起的另一个名字。
定义引用后,对其所有操作都是在与之绑定的对象上进行的。接上段程序:
refVal = 2; //把2赋给refVal指向的对象,即ival
int ii =refVal; //等价于ii = ival
int &refVal3 = refVal; //refVal3绑定到了那个与refVal绑定的对象上,即ival
因为引用本身不是一个对象,所以不能定义引用的引用。
引用的定义
允许一条语句定义多个引用,每个引用标识符都必须以&开头:
int i = 1, i2 = 2; //i和i2都是int
int &r = i, r2 = i2; //r是引用,与i绑定在一起;r2是int
除以下两种例外情况外,引用本身的类型必须与其所引用对象的类型一致。
int &refVal4 = 10; //错误:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval; //错误:此处引用类型的初始值必须是int型对象
例外情况一:在初始化常量引用时,允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值、一般表达式。(2.3节会讲)
例外情况二:可以将基类的引用绑定到派生对象上。(15.2节会讲)
2.3.2 指针
指针:其声明符为*d
的形式,其中d是变量名。
int *p1, *p2; //p1和p2都是指向int型对象的指针。
如果一条语句定义多个指针,则每个变量前都要有*。
指针与引用的异同点:
相似点:均实现了对其他对象的间接访问。
不同点:
1.指针本身就是一个对象,允许对指针赋值和拷贝,且在指针的生命周期内它可以先后指向几个不同的对象。
2.指针无须在定义时赋初值。与内置类型一样,在块作用域内定义的指针若未初始化,将拥有一个不确定的值。
获取对象的地址
指针存放某个对象的地址,用取地址符(&)获取该地址。
int ival =42;
int *p = &ival; //p存放变量ival的地址
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
与引用相似,除以下两种例外情况,指针本身的类型都要和它所指向的对象严格匹配。
声明语句中指针的类型实际上被用于指定它所指向对象的类型,所以二者必须匹配。
double dval;
double *pd = &dval; //正确:初始值是double型对象dval的地址
double *pd2 = pd; //正确:初始值是指向double对象的指针
int *pi = pd; //错误:pi和pd的类型不匹配
pi = &dval; //错误:试图把double型对象的地址赋给int型指针
例外情况一:允许令一个指向常量的指针指向一个非常量对象。(2.3节会讲)
例外情况二:可以将基类的指针绑定到派生对象上。(15.2节会讲)
指针值
指针的值(即地址)应属于下列4种状态之一:
- 指向一个对象。
- 指向紧邻对象所占空间的下一个位置。
- 空指针,意味着没有指向任何对象。
- 无效指针,即上述情况之外的其他值。
- 访问无效指针的后果无法预计。
- 虽然2和3的指针有效,但也没有指向具体对象,访问他们的后果也无法预计。
利用指针访问对象
对指针使用解引用符(*)可以访问该指针指向的对象。如:
int ival = 42;
int *p = &ival; //指针p指向int型对象ival
cout << *p; //由符号*得到p所指的对象,输出42
给解引用的结果赋值,实际上就是给指针所指的对象赋值。接上述代码:
*p = 0; //经由p为变量ival赋值
cout << ival; //输出0
解引用操作只适用于那些确实指向了某个对象的有效指针。
&和*的多重含义
在声明语句中,&和*是声明的一部分,用以组成复合类型。如:
int i = 42; int &r = i; //&紧随类型名出现,因此是声明的一部分,r是一个引用 int *p; //*紧随类型名出现,因此是声明的一部分,p是一个指针
而在表达式中,它们又变成运算符,&是取地址符,*是解引用符。如(接上):
p = &i; //&出现在表达式中,是取地址符 *p = i; //*出现在表达式中,是解引用符 int &r2 = *p; //&是声明的一部分,r2是一个引用;*是解引用符
综上,虽然是同一个符号,但在不同场景下其含义截然不同。
空指针
空指针:不指向任何对象
//以下三种方法均可生成空指针
int *p1 = nullptr; //方法#1
int *p2 = 0; //方法#2
//需要首先使用#include <cstdlib>
int *p3 = NULL; //方法#3
-
方法#1
nullptr
是C++11新标准刚引入的一种特殊类型的字面值。 -
方法#2、方法#3
NULL是以前常用的预处理变量,它在头文件
cstdlib
中定义,其值就是0。预处理器:运行于编译过程之前的一段程序。
当用到一个预处理变量时,预处理器会自动地把它替换为实际值。
用NULL初始化指针和用0初始化指针是一样的。现在,推荐使用
nullptr
,避免使用NULL
。把
int
变量直接赋给指针是错误的,即使int
值恰好为0也不行。
建议初始化所有指针
赋值和指针
引用本身不是对象,其一旦定义,便无法再令其绑定其他对象。
指针本身就是对象,它可以赋新值指向新的对象。
//区分是改变了指针的值还是改变了指针所指对象的值
int *pi = nullptr , ival = 1024;
pi = &ival; //改变了pi的值,pi现在指向ival
*pi = 0; //改变了ival的值,现在ival为0
其他指针操作
- 指针用在条件表达式中,如果指针为空指针,则条件取false。任何非0指针对应的条件值都是true。
- 两个类型相同的合法指针,可以用**==和!=**进行比较,比较结果为布尔值。
使用非法指针作为条件或进行比较会引发不可预计的后果。
void* 指针
void*:它是一种可用于存放任意对象的地址的指针。
不能直接操作void*指针所指的对象,因为并不知道这个对象是什么类型,也就无法确定能在这个对象上进行哪些操作。
2.3.3 理解复合类型的声明
一条语句可以定义出不同类型的变量(因为在同一条定义语句中,基本数据类型只有一个,但声明符却可以不同)。
int i = 2, *p = &i, &r = i; //i是int型的数,p是int型指针,r是int型引用
定义多个变量
有些人喜欢把类型修饰符(*
或&
)与基本数据类型紧挨着放,如:
int* p1 ,p2;
这会让人误认为p1
和p2
都是指针,其实不是,*
只修饰p1
,所以p1
是指针,而p2
是int型的数。
我们才用把*
或&
与变量名连在一起的方法,这样不容易引起新手的误会。
int *p1, p2;
指向指针的指针
指针是对象,它有实实在在的内存,所以可以定义指向指针的指针。
int ival = 1024;
int *pi = &ival; //pi指向ival
int **ppi = π //ppi指向pi
解引用也同理(接上述代码):
std::cout << ival //直接获取ival值
<< *pi //通过解引用pi获取ival值
<< **ppi //通过解引用ppi两次获取ival值
<< std::endl;
指向指针的引用
指针是对象,所以可以定义指向指针的引用。
int i = 42;
int *p; //p是int型指针
int *&r = p; //r是对指针p的引用
r = &i; //r引用了指针p,因此给r赋值&i就是令p指向i
*r = 0; //解引用r得到i,也就是p所指对象,将i的值改为
要理解r的类型到底是什么,最简单的办法就是从右向左读r的定义。离变量名最近的符号对变量的类型有最直接的影响。int *&r = p;
中,离r最近的是&
,所以r是一个引用;*
说明r引用的是一个指针;int
说明r引用的是一个int型指针。
2.4 const限定符
被关键字const修饰的变量的值不能改变,任何试图改变const对象值的行为都将引发错误。
const对象必须初始化,初始值可以是任意复杂的表达式。
const int bufSize = 512;
bufSize = 512; //错误:试图向const对象写值
const int i = get_size(); //正确:运行时初始化
const int j = 42; //正确:编译时初始化
const int k; //错误:k是一个未初始化的常量
初始化和 const
只能在const对象上执行不改变其内容的操作。
以下操作是允许的:
int i =42;
const int ci = i;
int j = ci;
ci
的常量特征仅仅在执行改变ci
的操作时才发挥作用。用ci
初始化j
并不会改变ci
的内容,所以允许这样操作。
默认状态下,const对象仅在文件内有效
默认状态下,const对象被设定为仅在文件内有效。当多个文件出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
那么怎么实现const对象在文件间的共享呢?
答:对于const变量不管是声明还是定义都添加extern关键字。
2.4.1 const 的引用
对常量的引用:与普通引用不同的是,对常量的引用不能用来修改它所绑定的对象。
const int ci = 1024;
const int &r1 = ci; //正确:引用及其绑定对象都是常量
r1 = 42; //错误:r1是对常量的引用
int &r2 = ci; //错误:试图让一个非常量引用指向一个常量对象
对const的引用 简称 常量引用
初始化和对const的引用
之前提到,引用的类型必须与其所引用对象的类型一致,但包含两个例外,这里说第一个例外情况:
在初始化常量引用时,允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是一个一般表达式。
小总结:
允许常量引用绑定非常量对象
不允许非常量引用绑定常量对象
int i = 42;
const int &r1 = i; //常量引用绑定非常量对象
const int &r2 = 42; //常量引用绑定非常量字面值
const int &r3 = r1 * 2; //常量引用绑定一般表达式
int &r4 = r1 * 2; //错误:r4为非常量引用
当一个常量引用被绑定到另外一种类型上时到底发生了什么?
double dval = 3.14;
const int &r1 = dval;
这里r1
引用的是一个int
型数,但dval
却是double
型。因此,为了确保r1
绑定了一个整数,编译器把上述代码变成如下形式:
const int temp = dval;
const int &r1 = temp;
即,r1
绑定了一个临时量temp
。temp
是编译器将double
通过类型转换转换为int
而来。
对const的引用可能引用一个并非const的对象
即常量引用绑定的对象不一定是个常量。
常量引用只对引用可参与的操作做了限定,对引用的对象本身是不是常量并未限定。
所以引用的对象也可能是个非常量,允许通过其他途径改变它的值,只是不能通过常量引用改变它的值罢了。
int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0; //r1为非常量引用,可以通过r1改变i的值,因此i修改为0
r2 = 0; //错误:r2为常量引用,不能通过它修改i的值
2.4.2 指针和 const
只有对象才可以定义为常量
因此对于引用,由于引用不是对象,所以只有对const的引用,我们所说的常量引用是对对const的引用的简称。
而对于指针,因为指针本身是对象,所以有指向常量的指针和常量指针。前者指这个指针指向常量,后者指这个指针本身就是常量。
指向常量的指针
与常量引用相似,指向常量的指针不能用于改变其所指对象的值。
要想存放常量对象的地址,只能使用指向常量的指针。
const double pi = 3.14; //pi是常量,其值不能改变
double *ptr = π //错误:普通指针不能指向常量对象
const double *cptr = π //正确:cptr是一个指向常量的指针
*cptr = 42; //错误:不能给*cptr赋值
但是有两个例外,这里说第一个:允许令一个指向常量的指针指向一个非常量对象。如:
double dval = 3.14;
cptr = &dval;
这样,与常量引用相似,指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
小总结:
指向常量的指针可以指向非常量。
普通指针不能指向常量。
const 指针(常量指针)
指针是对象,故可定为常量。常量指针必须初始化,初始化后其值不能改变,即存于指针内的那个地址不能改变,而不是指针指向的那个值。也就是说指针本身是常量,而不是说指针指向的对象是常量。
形式:在const前加*。
int errNumb = 0;
int *const curErr = &errNumb; //curErr是一个指向普通int对象的常量指针
const double pi = 3.14;
const double *const pip = π //pip是一个指向double型常量对象的常量指针
从右向左阅读法:
离curErr最近的是const,说明curErr本身是一个常量对象;下一个符号是*,说明curErr是一个常量指针;下一个是int,说明curErr是指向int型对象。综上,curErr是一个指向int型对象的常量指针。
可以通过curErr修改errNumb的值。
离pip最近的是const,说明pip本身是一个常量对象;下一个符号*,说明pip是一个常量指针;下一个是double,说明pip指向double型对象;下一个又是const,说明pip指向double型常量。综上,pip是一个指向double型常量对象的常量指针。
不可以通过pip修改pi的值。
2.4.3 顶层 const
顶层const:表示指针本身是一个常量。
底层const:表示指针所指的对象是个常量。
概念扩展:
顶层const:表示该数据类型的对象是常量。
底层const:与指针和引用等复合类型的基本类型部分有关。
指针类型既可以是顶层const也可以是底层const。
int i = 0;
int *const p1 = &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
在进行对象的拷贝操作中,常量是顶层const还是底层const区别明显。
由于拷贝不会改变被拷贝对象的值,所以对顶层const没什么影响。
而对于底层const,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换为常量,反之则不行:
//代码接上
int *p = p3; //错误:p3包含底层const,而p没有
p2 = p3; //正确:p2和p3均有底层const
int &r = ci; //错误:普通引用不能绑定常量对象
const int &r2 = i; //正确:常量引用可以绑定非常量对象
2.4.4 constexpr 和常量表达式
常量表达式:值不会改变 且 在编译过程就能得到计算结果的 表达式。
字面值属于常量表达式,用常量表达式初始化的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不是常量表达式
尽管staff_size
的初始值是字面值常量,但它的数据类型是普通int
而非const int
,所以它不是常量表达式。
尽管sz
本身是常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。
constexpr 变量
C++11新标准规定,允许将变量声明为**constexpr
**类型,以便由编译器来验证变量的值是否是一个常量表达式。
声明为constexpr
的变量一定是一个常量,且必须用常量表达式初始化:
constexpr int mf = 20; //20是常量表达式
constexpr int limit = mf + 1; //mf+1是常量表达式
constexpr int sz = size(); //只有当size是一个constexpr函数时才是一条正确的声明语句
constexpr函数应足够简单以使得编译时就可以得到计算结果,这样就可以用constexpr函数初始化constexpr变量。普通函数不能作为constexpr变量的初始值。
字面值类型
字面值类型:能声明成constexpr
的类型。
常见的如算数类型、引用、指针都属于字面值类型;
自定义类、IO库、string类等则不属于字面值类型,也就不能定义成constexpr。
尽管指针和引用都可定义成constexpr,但其初始值受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址的对象。
函数体内的变量一般不在固定地址,所以constexpr指针不能指向它们。
定义于所有函数体外的对象其地址固定不变,能用来初始化constexpr指针。
后边会讲到,允许函数定义一类有效范围超出函数本身的变量,它们也有固定地址,所以constexpr指针可以指向它们。
指针和 constexpr
必须明确一点,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指对象无关,其中的关键在于constexpr把它所定义的对象置为了顶层const
。
const int *p = nullptr; //p是指向整型常量的普通指针
constexpr int *q = nullptr; //q是一个指向整数的常量指针
与其他常量指针类似,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
6.1.1节会提到,函数体内定义的变量一般来说并非存放在固定地址中,因此
constexpr
指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr
指针。但是,允许函数定义一类有效范围超过函数本身的变量,这类变量也有固定的地址,故
constexpr
指针可以指向这类变量,constexpr
引用也可以绑定到这类变量上。