C++当中的复合类型
最近开始系统地学习 C++ 的语法,参考的主要资料来自于 C++ Primer 第五版,对于学习过程中所遇到的较难理解的点,我会以blog的形式对问题和内容进行记录,并进行进一步地探讨。
这一部分的内容对应于参考资料 C++ Primer 的第二章第三节。
1. 引用
开篇说到,C++11 标准下新增了“右值引用”,这将会在之后进行探讨。目前本章所接触到的引用都是“左值引用”,也就是C++当中大家最常接触到的引用。以我个人的学习经验来说,目前所接触到的引用大多以函数形参出现,它将会作为传入参数的别名进行传址调用,部分代替了 C 语言当中的指针(作为函数形参出现时其代替指针的作用最为明显,因为不需要再对指针进行解除引用了,可以直接使用函数的参数进行传址调用)。
引用是变量的别名
引用(reference) 是为变量起的别名,在对引用类型的变量进行声明的时候,应当在变量名之前加上&
。在声明一个引用类型的变量时,必须要对引用变量进行初始化,这一点也非常好理解,因为如果不进行初始化的话,我们声明的引用变量不知道自己是谁的别名,当然会产生错误。
有关引用声明的一个例子如下:
#include <iostream>using namespace std;int main()
{int a = 23;int &b = a;cout << a << ' ' << b;return 0;
}
输出的结果是:
23 23
如果此时我们再声明一个整型的引用变量c
,并且不对它进行初始化,那么我们将得到以下错误信息:
'c' declared as reference but not initialized
需要注意的是,由于引用是变量的别名,它在初始化的时候已经与其它变量绑定,因此如果对引用进行赋值,实际上就是将右侧变量的值赋给了与引用绑定的对象,而不是重新指定了引用所绑定的对象。
引用自身不是一个对象,因此不能定义引用的引用。
总结一下,引用类型在声明的时候必须得到初始化,并且一经初始化,该引用将与变量永久绑定,可以对引用进行赋值,但是由于引用在初始化时与变量永久绑定,为引用的赋值相当于将=
的右值对应的值赋给了引用所绑定的对象。引用并非对象,它只是为一个已经存在的对象所起的另外一个名字。定义一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的,以下是一个例子:
#include <iostream>using namespace std;int main()
{int a = 23;int &b = a;int c = 32;b += c;cout << a << ' ' << b;return 0;
}
输出的结果当然是:
55 55
引用的定义
与指针相同,可以在一条语句中进行多个引用的定义,但是在每一个引用类型的声明之前都要加上&
,比如int &a = b, &c = d;
。将&
与变量名“紧挨”在一起可以避免歧义。
引用只能与对象进行绑定,因为它是对象的别名,而不能将引用绑定到表达式或是字面值上,因为引用不是变量。
2. 指针
指针(pointer) 是指向(point to)另外一种类型的复合类型。与引用类似,指针也可以完成对变量的间接访问,但是指针是一种更加复杂的复合类型,指针本身指向的是变量的地址,使用指针引用解除符号*
可以对指针所指向变量的值进行访问和修改。
指针与引用有着本质上的不同,方才已经提到,指针指向的是变量的地址,而引用只是变量的别名。指针和引用的区别可以总结如下:
- 指针本身也是一个对象,允许对指针进行赋值和拷贝,并且在指针的生命周期内可以先后指向多个不同的变量的地址。与之相比,引用只是变量的别名,它必须得到初始化(如果不对指针进行初始化,它将会指向一个随机的地址,这是危险的,可以使用字面值
nullptr
来使得指针指向空地址,这样的初始化方法是安全的),并且引用类型一经声明无法改变与其绑定的变量,所有对引用的赋值操作都是直接将值赋给了引用绑定的对象。 - 无需在指针定义时为指针赋值,指针与其它内置类型相似,在块作用域内定义的指针如果没有得到初始化,也将拥有一个不确定的值(但是这样做是危险的,正如方才所提到的)。
定义一个指针的方法是在变量名之前加上*
,比如int *d;
定义了一个整型变量的指针d
,由于没有进行初始化,它将指向随机值。与引用的声明类似,在声明多个指针类型的变量时,应该为每一个指针变量都加上*
,比如int *d, *b;
。
获取对象的地址
指针类型变量存放的是对象的地址,而想要获取对象的地址,需要使用取地址符&
。从外表上看来,变量的取地址符与引用类型的声明是一样的,但是正如这句话所说的,引用是在声明阶段需要使用&
来表示它是一个引用类型,而取变量的地址并不是在声明阶段,而可能出现在初始化或是赋值阶段,比如对于整型变量b
,它的地址是&b
。
一个有关指针的例子如下,在这里涉及到变量的取地址符、指针的赋值以及使用指针对变量的值进行修改:
#include <iostream>using namespace std;int main()
{int a = 23;int *b = &a; // 使用取地址符为变量赋值cout << *b << ' ' << a + 1 << endl;cout << *b << ' ' << a << endl;*b = *b + 1; // 解除指针, 对该地址对应的变量(也就是a)进行修改cout << a; // 得到修改后的结果return 0;
}
实验的结果是:
23 24
23 23
24
需要注意的是,刚才已经反复强调过,引用不是变量而是变量的别名,因此不能定义指向引用的指针。指针所指向的变量必须与指针所声明的类型相同,比如整型的指针必须指向整型变量,如果指向的是double型的变量就会报错。
指针值
指针的值应该属于以下四种状态之一:
- 指向一个对象(的地址);
- 指向紧邻对象所占空间的下一个位置;
- 空指针(nullptr);
- 无效指针,即上述三种情况之外的值;
试图拷贝或以其他方式访问无效指针的值都将发生错误,但是编译器不会检查这种错误。
利用指针访问对象
如果指针指向了一个对象,则允许使用解引用符*
来访问该对象。对指针解引用会得到所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所指向的对象赋值。一个实际的例子是调用C++的库函数max_element
或min_element
来得到可迭代对象(比如vector)当中的最大值或最小值的地址,使用解引用符可以直接得到对应的最大值/最小值,比如对于某个整型的vectorv
:int MAX = *max_element(v.begin(), v.end());
,int MIN = *min_element(v.begin(), v.end());
。
像&
和*
这样的符号既可以作为表达式当中的运算符,又可以作为声明的一部分出现,符号的上下文才能够决定符号的具体意义。例如:
int i = 42;
int &r = i; // 声明一个变量 i 的引用
int *p = nullptr;
p = &i; // 让整型的指针 p 指向变量 i 的地址
*p = i; // 为指针 p 所指向的值重新赋值为变量 i 的值
int &r = *p; // 声明一个整型引用 r, 它是 p 所指向的值的别名
需要注意的是,C++当中可以声明指针的引用,方法是int *&t = p;
,即可声明整型指针p
的别名t
。一个比较难理解的点是区分&*
和*&
,经过资料的查阅,较好的解决方案是从右向左对声明语句进行阅读。举个例子,对于int *&t = p;
,t
左侧最先出现的是&
,代表t
是一个引用,即当前声明的是某种类型的变量的别名,那么类型如何确定呢?继续向右读,发现*
,说明这是一个指针类型,再向右读可以确定这个引用是一个整型指针的别名。int *&t = p;
声明的是指针类型的引用。
而对于&*
,由于引用类型不具有地址,因此无法声明引用类型的指针,int &*t = p;
这条语句是不合法的。同样从右往左读,首先读到*
代表t
是指针类型,而再右侧是int &
,即整型的引用,引用没有地址,所以这条语句不合法。
空指针
空指针不指向任何值,nullptr
是C++11新标准引入的新的字面值,代表空指针。在新标准下,应该尽可能地使用nullptr
来表示空指针而非使用NULL
。
赋值和指针
引用和指针都可以对它们所指向的对象进行改变。二者本质的区别是引用本身不是一个对象,不具有地址,它只是它在被初始化时所绑定到其它变量上的别名。而指针是一个复合类型的对象,它具有自己的地址,它的值代表的是它所指向的值的地址,对指针变量使用*
可以解除引用,直接对指针所指向的对象进行访问。
指针所指向的对象可以修改,方法是对指针进行赋值,使它的值等于另一个变量的地址或空指针。
其它指针操作
可以使用==
和!=
比较两个合法的指针,返回值是bool类型,代表二者所指向的地址是否相等。
void* 指针
void*
是一种特殊的指针类型,可以存放任意对象的地址。与具有类型的指针相比,这种指针与它们最大的不同就是并不了解存放的是何种类型对象的地址。
使用void*
指针能做的事情非常有限,可以拿它和其它指针进行比较(地址是否相同)、作为函数的输入输出(传址),或是将它保存的地址赋给另外一个void*
类型的指针。
不能直接操作void*
指针所指的对象,因为我们不知道它的具体类型。
3. 理解符合类型的声明
变量的定义包括一个基本数据类型(base type)和一组声明符。在同一条声明语句当中,基本类型只能有一个,但是声明符的形式可以非常不同:
int i = 1024, *p = &i, &r = i; // 基本类型为 int,
// 声明了一个 int 型的变量 i, 一个 指向 int 类型变量的指针 p 并将它初始化为 i 的地址
// 以及声明了一个 int 类型的引用 r, 它是 i 的别名.
指向指针的指针
通常来说,声明符当中修饰符的数量不加以限制,因此可以定义“指向指针的指针”,比如:
int ival = 1024;
int *pi = &ival;
int **ppi = π
指向指针的引用
方才已经说过,指针变量是对象,它具有自己的地址,因此它可以有别名,称为“指向指针的引用”:
int i = 2048;
int *p = &i;
int *&r = p;
再次重复一下,想要理解r
究竟是什么,最好的做法是从右向左阅读。距离变量名最近的符号对变量的类型有着最直接的影响,因此r
首先是一个引用。声明符的其余部分确定了r
引用的是何种类型,因此可以确定r
是一个指向指针的引用。
不存在指向引用的指针,因为引用不是对象,没有地址。