文章目录
- 复合类型
- 引用
- 概念与使用
- 引用的定义
- 注意
- 指针
- 概念
- 声明方式
- 取地址符
- 指针值
- 空指针
- 利用指针访问对象
- 赋值和指针
- void* 指针
- 指向指针的指针
- 指向指针的引用
- 初始化所有指针
- 有多重含义的某些符号
- const限定符
- 概念
- const的引用
- 指针和const
- 顶层const和底层const
- constexpr和常量表达式
- constexpr 变量
- 字面值类型
- 指针和constexpr
复合类型
复合类型是指基于其他类型定义的类型。引用和指针都是复合类型。
引用
概念与使用
引用: 引用并非对象,只是为一个已经存在的对象起了另一个名字,引用即别名。
一般在初始化变量时,初始值会被拷贝到新建的对象中。定义引用时,程序把引用和它的初始值绑定(bind) 在一起,而非将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。
因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
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 = 1024, i2 = 2048; // i和i2都是int
int &r = i, r2 = i2; // r是一个引用,与i绑定在一起,r2是int
int i3 = 1024, &ri = i3; // i3是int,ri是一个引用,与i3绑定在一起
int &r3 = i3, &r4 = i2; // 一条语句定义多个引用
大多数情况下(详情见下文注意), 引用的类型都要和与之绑定的对象严格匹配。而且引用只能绑定在对象上,不能与字面值霍某个表达式的计算结果绑定在一起。
int &refVal4 = 10; // warning:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval;
// warning:引用类型要和与之绑定的对象严格匹配,此处引用类型的初始值必须是int型对象
注意
上文说的大多数情况是指除了:
- 初始化常量引用时允许用任意表达式作为初始值
- 基类引用可以绑定到派生类对象上
第一点将在下文说明,第二点将在别的博文中说明。
指针
概念
指针: 与引用类似,指针也实现了对其他对象的间接访问。
不同点在于:
- 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
- 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
声明方式
经常有一种观点会误认为,在定义语句中,类型修饰符(*和&)作用于本次定义的全部变量。
int* p1, p2; // p1是指向int的指针,p2是int
上述代码中,基本数据类型是int而非int*。*仅仅是修饰了p1而已,对该声明语句中的其他变量,例如p2并不产生任何作用。
取地址符
指针存放某个对象的地址,想获取该地址,需要使用取地址符(操作符&):
int ival = 1024;
int *p = &ival; // p存放变量ival的地址,或者说p是指向变量ival的指针
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
同样的,**大部分情况下,**指针的类型都要和它所指向的对象严格匹配:
double dval;
double *pd = &dval; // 正确:初始值是double型对象的地址
double *pd2 = pd; // 正确:初始值是指向double对象的指针int *pi = pd; // warning:指针pi的类型和pd的类型不匹配
pi = &dval; // warning:double型对象的地址赋给int型指针
因为在声明语句中指针的类型实际上被用于指定它所指向对象的类型,所以二者必须匹配。如果指针指向了一个其他类型的对象,对该对象的操作将发生错误。
指针值
指针的值(即所指向的地址),应属于下列4种状态之一:
- 指向一个对象。
- 指向紧邻对象所占空间的下一个位置。
- 空指针,意味着指针没有指向任何对象。
- 无效指针,也就是上述情况之外的其他值。
试图拷贝或以其他方式访问无效指针的值都将引发错误。 编译器并不负责检查此类错误,这一点和试图使用未经初始化的变量是一样的。访问无效指针的后果无法预计,因此程序员必须清楚任意给定的指针是否有效。
尽管第2种和第3种形式的指针是有效的,但其使用同样受到限制。显然这些指针没有指向任何具体对象, 所以试图访问此类指针(假定的)对象的行为是不被允许的。如果这样做了,后果也无法预计。
空指针
生成空指针的方法:
int *p1 = nullptr; // 等价于int *p1 = 0;
int *p2 = 0; // 直接将p2初始化为字面常量0
int *p3 = NULL; // 需要#include cstdlib
得到空指针最直接的办法就是用字面值nullptr 来初始化指针。nullptr 是一种特殊类型的字面值,可以被转换成任意其他的指针类型。
用名为NULL的预处理变量来给指针赋值,NULL在头文件cstdlib中定义,它的值就是0。
在C++11新标准下,程序最好使用nullptr,同时尽量避免使用NULL。
注意:不能将int变量的直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行。
int zero = 0;
pi = zero; // warning:不能把int变量直接赋给指针
利用指针访问对象
允许使用解引用符(操作符*)来访问该对象:
int ival = 42;
int *p = &ival; // p存放着变量ival的地址,或者说p是指向变量ival的指针
cout << *p; //由符号*得到指针p所指的对象,输出42
cout << p; //得到指针p所指对象的地址,输出ival的地址
对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值:
*p = 0; //由符号*得到指针p所指的对象,即可经由p为变量ival赋值
cout << *p; // 输出0
解引用操作仅适用于那些确实指向了某个对象的有效指针。
赋值和指针
想要搞清楚一条赋值语句到底改变了指针的值还是改变了指针所指对象的值,最好的办法就是记住赋值永远改变的是等号左侧的对象。
pi = &ival; // pi的值被改变,现在pi指向了ival
上述代码的意思是为指针pi赋一个新的值,也就是改变了那个存放在pi内的地址值。
*pi = 0; // ival的值被改变,指针pi并没有改变
上述代码的意思是为指针pi所指对象赋一个新的值,也就是改变了pi所指对象的值。
void* 指针
void*是一种特殊的指针类型,可用于存放任意对象的地址。但是我们对该地址中到底是个什么类型的对象并不了解:
double obj = 3.14,*pd = &obj;void *pv = &obj; // 正确:void*能存放任意类型对象的地址,obj可以是任意类型的对象
pv = pd; // pv可以存放任意类型的指针
利用void指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上作哪写操作。
概括来说,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。
指向指针的指针
一般来说,声明符中修饰符的个数并没有限制。当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。以指针为例,指针式内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
通过*的个数可以区分指针的级别。也就是说,** 表示指向指针的指针, ***表示指向指针的指针的指针,以此类推:
int ival = 1024;
int *pi = &ival; // pi指向一个int型的数
int **ppi = π // ppi指向一个int型的指针
解引用int型指针会得到一个int型的数,同样,解引用指向指针的指针会得到一个指针。为了访问最原始的那个对象,需要对指针的指针做两次解引用。
ival
*pi
**ppi
上述三种方式输出的都是ival的值。
指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
int i = 42;
int *p; // p是一个int型指针
int *&r = p; // r是一个对指针p的引用r = &i; // r引用了一个指针,因此给r赋值&i就是令p指向i
*r = 0; // 解引用r得到i,也就是p指向的对象,将i的值改为0
要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号对变量的类型有最直接的影响。对r来讲就是(int *&r)中的&,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号 * 说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针。
初始化所有指针
在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占的内存空间的当前内容将被看作一个地址值。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。如果指针所占内存空间中恰好有内容,而这些内容又被当作了某个地址,我们就很难分清它到底是合法的还是非法的了。
良好的编程习惯应该是初始化所有指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了。
有多重含义的某些符号
像&和*这样的符号,既能用作表达式里的运算符,也能作为声明的一部分出现,符号的上下文决定了符号的意义:
int i = 42;
int &r = i; // &紧随类型名出现,因此是声明的一部分,r是个引用
int *p; // *紧随类型名出现,因此是声明的一部分,p是个指针
p = &i; // &出现在表达式中,是一个取地址符
*p = i; // *出现在表达式中,是一个解引用符
int &r2 = *p; // &是生命的一部分,*是一个解引用符
在声明语句中, &和*用于组成复合类型;在表达式中, 又作为运算符。在不同场景下出现的虽然是同一个符号,但是由于含义截然不同,所以我们完全可以把它们当作不同的符号看待。
const限定符
概念
const可以对变量的类型加以限定,使得它的值不能被改变。
由于const对象一旦创建后其值就不能再改变,因此const对象必须初始化。
const int i = get_size(); // 正确,运行时初始化
const int j = 42; // 正确,编译时初始化
const int k; // 错误,k是一个未经初始化的常量
const的引用
像其他对象一样,可以把引用绑定到const对象上,称之为对常量的引用。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。
const int ci = 1024; //
const int &r1 = ci; // 正确:引用及其对应的对象都是常量
r1 = 42; // 错误:r1是对常量的引用
int &r2 = ci; // 错误:试图让一个非常量的引用指向一个常量对象
因为不允许直接为ci赋值,当然也就不能通过引用去改变ci。因此,对r2的初始化是错误的。换种方式理解:假设该初始化合法,则可以通过r2来改变它引用对象的值,这显然是不正确的。
通常引用的类型必须与其所引用对象的类型一致, 但有两个例外,一种即在初始化常量时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。例如,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式:
int i = 42;
const int &r1 = i; // 允许将const int&绑定到一个普通int对象上
const int &r2 = 42; // 正确:r2是一个常量引用
const int &r3 = r1 * 2; // 正确:r3是一个常量引用
int &r4 = r1 * 2; // 错误:r4是一个普通的非常量引用
r3 和 r4采用了同样的初始化方法却出现了不同的结果。为什么呢?这需要弄清楚当一个常量引用被绑定到另外一种类型上时到底发生了什么:
double dval = 3.14;
const int &ri = dval; // 正确
此处ri引用了一个int型的数。对ri的操作应该是整数运算,但dval却是一个双精度浮点数而非整数。因此为了确保让ri绑定一个整数,编译器把上述代码变成了如下形式:
const int temp = dval; // 由双精度浮点数生成一个临时的整型常量
const int &ri = temp; // 让ri绑定这个临时量
这里简单介绍临时量(temporary)对象。所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。
接下来简单探讨当 ri 不是常量时,如果执行了类似上面的初始化过程将带来什么样的后果。如果 ri 不是常量,则允许对 ri 赋值,这样就会改变 ri 所引用对象的值。注意,此时绑定的对象是一个临时量而非dval。因此赋值不会修改dval,而是修改了temp。程序员既然让 ri 引用dval,是想让 ri 改变dval,不会想着把引用绑定到临时量上,C++自然也就把这种行为归为非法。 ri 是常量的话不允许对其赋值,自然也就没有修改temp而不是dval的隐患啦~
常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。对象若是非常量,允许通过其他途径改变它的值:
int i = 42;
int &r1 = i; // 引用ri绑定对象i
const int &r2 = i; // r2也绑定对象i,但是不允许通过r2修改i的值
r1 = 0; // r1并非常量,i的值修改为0
r2 = 0; // 错误:r2是一个常量引用
r2 绑定非常量整数 i 是合法行为,但是不允许通过 r2 修改 i 的值。 但 i 的值仍然允许通过其他途径修改,既可以直接给 i 赋值,也可以通过 r1 一样绑定到 i 的其他引用来修改。
PS:有时候经常会遇到将“对const的引用”称作“常量引用”的情况,但严格来说并不存在常量引用,因为引用本身不是一个对象, 所以我们没法让引用本身恒定不变。但事实上,由于C++并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算常量。引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。
指针和const
指针亦可指向常量或非常量。指向常量的指针(pointer to const)不能用于改变其所指对象的值,想要存放常量对象的地址,只能使用指向常量的指针:
const double pi = 3.14; // pi是个常量,其值不可更改
double *ptr = π // 错误:ptr是个普通指针
const double *cptr = π // 正确:cptr可以指向一个双精度常量
*cptr = 42; // 错误:不能给*cptr赋值
通常来讲指针的类型必须和其所指对象的类型一致。但是有两种例外情况,其中之一便是允许一个指向常量的指针指向一个非常量对象:
double dval = 3.14;
cptr = &dval; // 正确:但是不能通过cptr改变dval的值
和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。 所谓指向常量的指针,仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
指针是对象而引用不是,因此允许将指针本身定为常量。常量指针 (const pointer)必须初始化。而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把 * 放在const关键字之前用以说明指针是一个常量,即不变的是指针本身的值而非指向的那个值:
int errNumb = 0;
int *const curErr = &errNumb; // curErr将一直指向errNumb
const double pi = 3.14159;
const double *const pip = π // pip是一个指向常量对象的常量指针
还是重提一下如何明晰声明的含义,拿curErr来讲,离curErr最近的符号是const,意味着curErr本身是一个常量对象,声明符中下一个符号是*,意思是curErr是一个常量指针。最后,该声明语句的基本数据类型部分确定了常量指针指向的是一个int对象。同理可得,pip是一个常量指针,指向的对象是一个双精度浮点型常量。
指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型。例如,pip是要给指向常量的常量指针,则不论是pip所指的对象值,还是pip自己存储的那个地址都不能改变。相反的,curErr指向的是一个一般的非常量整型,那么就完全可以用curErr去修改errNumb的值:
*pip = 2.72; // 错误:pip是一个指向常量的指针
*curErr = 3; // 正确:将curErr所指的对象的值改为3
顶层const和底层const
由于指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。用名词顶层 const(top-level const)表示指针本身是个常量,而用名词底层 const(low-level const)表示指针所指的对象是一个常量。
引申来讲,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层const则与指针和引用等复合类型的基本类型部分有关。指针类型既可以是顶层也可以是底层,这一点和其他类型相比区别明显。
const
在 */&
左边的是顶层,在右边的是底层:
int i = 0;
int *const pi = &i; // 不能改变p1的值,这是一个顶层const
const int ci = 42; // 不能改变ci的值,这是一个顶层的const
const int *p2 = &ci; // 允许改变p2的值,这是一个底层const
const int *const p3 = p2; // 靠右的const是顶层const,靠左的const是底层const
const int &r = ci; // 用于声明引用的const都是底层const
当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。顶层const不受什么影响:
i = ci; // 正确:拷贝ci的值,ci是一个顶层const,拷贝操作不会更改ci的值
p2 = p3; // 正确:p2和p3指向的对象类型相同,都是指向常量的指针
拷贝操作并不改变被拷贝对象的值,因此,拷入和拷出的对象是否是常量都没什么影响。
但底层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的引用,不过是指针或引用觉得自己指向了常量,所以自觉地不去改变所指对象的值。所以指向(绑定)常量,也可以指向(绑定)非常量。
constexpr和常量表达式
定义:常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。
显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如:
const int max_files = 20; // max_files是常量表达式
const int limit = max_files; // limit是常量表达式
int i = 30; // i不是常量表达式
const int sz = get_size(); //要在运行阶段才能初始化,sz不是常量表达式
从定义我们可以知道常量表达式必须具备两个特征:
- 值不会改变
- 编译过程就能得到计算结果
因此尽管 i 的初始值是个字面值常量,满足第二点,但是它的数据类型只是一个普通的int而非const int,所以它不属于常量表达式。
constexpr 变量
在一个复杂系统中,几乎肯定不能分辩一个初始值到底是不是常量表达式。当然可以定义一个const变量并把它初始值设为我们认为的某个常量表达式,但在实际使用时,尽管如此要求却常常发现初始值并非常量表达式的情况。在此种情况下,对象的定义和使用根本就是两码事儿。
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时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为 “字面值类型”(literal type)。
到目前为止接触过的数据类型中,算术类型、引用和指针都属于字面值类型。自定义类、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr。
尽管指针和引用都能定义成constexpr,但他们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。
函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。同时,C++允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用可以绑定到这样的变量上,constexpr指针也可以指向这样的变量。
指针和constexpr
constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:
const int *p = nullptr; // p是一个指向整型常量的指针
constexpr int *q = nullptr; // q是一个指向整数的常量指针
q和p类型相差甚远,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