声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。
摘要
本文整理了“引用”与“指针”相关的知识点,为了说明绑定不同类型的对象,补充整理了“类型转换”相关知识点。
1、引用
1.1 引用即别名
引用(reference),为“已经存在的对象”起另外一个名字。
- 定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用;
- 一旦初始化完成,引用将和它的初始值对象“一直绑定”在一起;因为无法令引用重新绑定到另外一个对象,所以引用必须初始化;
- 定义引用后,对其进行的所有操作都是“在与之绑定的对象上进行的”;
- 除了 对常量的引用 和 继承关系类的引用 两种例外情况,引用的类型要和与之绑定对象的类型严格匹配;
- 引用(非对常量的引用)只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起,因为不能通过引用对其进行修改。
int ival = 1024;
// refVal 指向 ival,是 ival 的另一个名字
int &refVal = ival;
// 对引用进行的所有操作都是“在与之绑定的对象上进行的”
// refVal2 绑定到了那个与 refVal 绑定的对象上,即绑定到 ival 上
int &refVal2 = refVal;
// p 指向那个与 refVal 绑定的对象,即指向 ival
int *p = &refVal;
引用的一种用途是作为函数的形参:
- 在被调函数内部,对形参的修改会直接影响到实参;
- 通过传递引用而不是整个数据对象,可以提高程序的效率。
void swap(int &a, int &b) {
int temp;
temp = a;
a = b;
b = temp;
}
int main() {
int wallet1 = 300;
int wallet2 = 350;
swap(wallet1, wallet2);
cout << wallet1 << ", " << wallet2 << endl;
return 0;
}
1.2 对 const 的引用
可以把引用绑定到 const
对象上,称之为对常量的引用(reference to const),或者简称为常量引用。对常量的引用不能被用于修改它所绑定的对象。
const int ci = 1024;
const int &r = ci;
常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。
在初始化常量引用时,允许用任意表达式作为初始值,只要该表达式的结果能 转换 成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是一表般达式。
int i = 1024;
const int &rl = i; // 允许将 const int& 绑定到一个普通 int 对象
const int &r2 = 1024; // 允许将 const int& 绑定到字面值
const int &r3 = rl * r2; // 允许将 const int& 绑定到一般表达式
double dval = 3.14;
const int &ri = dval; // 允许将 const int& 绑定到可以转换到 int 类型的 double 对象
// 实现说明:
// 为了确保让 ri 绑定一个整数,编译器把上述代码变成了如下形式:
const int temp = dval; // 由双精度浮点数生成一个临时的整型常量对象
const int &ri = temp; // 让 ri 绑定这个临时量(temporary,临时创建的一个未命名的对象)
// 不允许对 ri 赋值,即不能改变临时量的值,更不能通过 ri 改变 dval 的值
// dval 的值如果改变,也不会影响 ri 的值,因为 ri 引用的是临时量,其值在通过 dval 初始化时确定
关于 const 类型对象
- 只能在
const
对象上执行不改变其内容的操作;- 因为
const
对象一旦创建后其值就不能再改变,所以const
对象必须初始化;- 默认情况下,
const
对象被设定为仅在文件内有效;如果需要只在一个文件中定义,并在其他多个文件中声明并使用,那么,对于const
对象,不管是声明还是定义都需要添加extern
关键字。
//file_1.cc 定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = 512;
//file_1.h 头文件
extern const int bufSize; //与 file_1.cc 中定义的 bufSize 是同一个
1.3 继承关系类的引用/指针
可以将基类的引用(或指针)绑定到派生类对象上;
不可以将派生类的引用(或指针)绑定到基类对象上。
Base b;
Derived d; // Derived 是 Base 的派生类
Base &rb = d; // 可以用 Base& 指向一个 Derived 对象
Base *pb = &d; // 可以把一个 Derived 对象的地址赋给一个 Base*
Derived &rd = b; // 错误,不可以用 Derived& 指向一个 Base 对象
Derived *pd = &b; // 错误,不可以把一个 Base 对象的地赋给一个 Derived*
-
之所以存在派生类向基类的 类型转换,是因为
- 每个派生类对象都包含一个基类部分,而基类的引用(或指针)可以绑定到该基类部分上。
-
之所以不存在从基类向派生类的 自动类型转换,是因为
- 一个基类的对象既可以以独立的形式存在,也可以作为派生类对象的一部分存在;
- 如果基类对象不是派生类对象的一部分,那么它只含有基类定义的成员,不含有派生类定义的成员,也就不能转换为派生类。
2、指针
2.1 指针即地址
指针(pointer),指向(point to)另外一种类型的复合类型。
- 与引用的相同点:
- 与引用的不同点:
- 指针是通过地址间接访问某个对象,而引用是通过别名直接访问某个对象;
- 指针本身就是一个对象,允许对指针赋值和拷贝;
- 在指针的生命周期内,它可以先后指向几个不同的对象;所以,可以在定义时不进行赋初值(不推荐)。
int ival = 1024;
int *p = &ival; // p 存放变量 ival 的地址,p 是指向变量 ival 的指针
cout << *p; // 使用解引用符(操作符 *)来访问指针指向的对象
int &ri = ival; // 引用不是对象,没有实际地址,不能定义指向引用的指针
int *p1 = nullptr; // 等价于 int *p1 = 0; C++11 标准,推荐
int *p2 = NULL; // 等价于 int *p2 = 0; 需要首先 #include cstdlib
// void* 可以存放任意对象的地址,因为不知道具体的对象类型,所以不能直接操作所指向的对象
void *pv = &p;
int **pp = static_cast<int **>(pv);
指针的值(即地址)应属下列 4 种状态之一:
- 指向一个对象;
- 指向紧邻对象所占空间的下一个位置;
- 空指针,意味着指针没有指向任何对象;
- 无效指针,上述情况之外的其他值。
关于指针的更多内容,回顾 C语言提高专题(上)=> 2、指针
2.2 指针和 const
与引用类似:
- 可以令指针指向常量或非常量;
- 指向常量的指针(pointer to const)不能用于改变其所指对象的值;
- 要想存放常量对象的地址,只能使用指向常量的指针;
- 指向常量的指针可以指向一个非常量对象。
const double pi = 3.14;
const double *cptr = π
double dval = 3.14;
cptr = &dval;
与引用“在某种意义上”类似:
- 指针本身可以是常量,即常量指针(const pointer);
- 常量指针必须初始化,而且一旦初始化完成,它的值(存放在指针中的地址)就不能再改变;
- 指针本身是一个常量并不意味着不能通过指针修改其所指向对象的值,能否这样做完全依赖于所指向对象的类型。
int errNumb = 0;
// 把 * 放在 const 关键字之前,说明指针是一个常量
int *const curErr = &errNumb; // curErr 将一直指向 errNumb
*curErr = 1; // 可以用 curErr 修改 errNumb 的值
const double pi = 3.14159;
const double *const pip = π // pip 是一个指向常量对象的常量指针
术语:常量引用
C++ 程序员们经常把词组“对 const 的引用”简称为“常量引用”。严格来说,并不存在常量引用。因为引用不是一个对象,所以没法让引用本身恒定不变。但由于 C++ 语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算是常量。引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。
3、类型转换
对象的类型定义了对象能包含的数据和能参与的运算,其中一种运算被大多数类型支持,就是将对象从一种给定的类型转换(convert)为另一种关联的类型。这样,当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代。
3.1 隐式转换
隐式转换(implicit conversion),编译器自动执行的转换:
- 在大多数表达式中,比
int
类型小的整型值首先提升为较大的整数类型。 - 在条件中,非布尔值转换成布尔类型。
- 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
- 函数调用时,如果实参与形参的类型不同,也会发生类型转换。
3.1.1 算术转换
算术转换(arithmetic conversion)的含义是把一种算术类型转换成另外一种算术类型。
- 算术类型之间的隐式转换被设计得尽可能避免损失精度。
- 算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型。
- 当表达式中既有浮点类型也有整数类型时,整数值将转换成相应的浮点类型。
3.1.1.1 赋值转换
给某种类型的对象强行赋另一种类型的值时,类型所能表示的值的范围决定了转换的过程:
// 把一个非布尔类型的算术值赋给布尔类型时,初始值为 0 则结果为 false,否则结果为 true
bool b = 42; // b 的值为 true
bool b2 = 3.14; // b2 的值为 true
// 把一个布尔值赋给非布尔类型时,初始值为 false 则结果为 0,初始值为 true 则结果为 1
int i = b; // i 的值为 1
// 把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分
i = 3.14; // i 的值为 3
// 把一个整数值赋给浮点类型时,小数部分记为 0;
// 如果该整数所占的空间超过了浮点类型的容量,精度可能有损失
double pi = i; // pi 的值为 3.0
// 当赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数
// 例如,8 比特大小的 unsigned char 可表以示 0 至 255 区间内的值,
// 如果我们赋了一个区间以外的值,则实际的结果是该值对 256 取模后所得的余数
// 建议:不要混用带符号类型和无符号类型
unsigned char c = -1; // 假设 char 占 8 比特,c 的值为 255
// 当赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)
// 此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据
// 建议:避免无法预知和依赖于实现环境的行为
signed char c2 = 256; // 假设 char 占 8 比特,c2 的值是未定义的
3.1.1.2 整提型升
整型提升(integral promotion),负责把小整数类型转换成较大的整数类型。
- 对于
bool
、char
、signed char
、unsigned char
、short
和unsigned short
等类型来说,只要它们所有可能的值都能存在int
里,它们就会提升成int
类型;否则,提升成unsigned int
类型。 - 较大的
char
类型(wchar_t
、char16_t
、char32_t
)提升成int
、unsigned int
、long
、unsigned long
、long long
和unsigned long long
中最小的一类种型,前提是转换后的类型要能容纳原类型所有可能的值。
3.1.1.3 运算对象转换
对于某个运算符的两个运算对象,首先,执行整型提升;然后,根据两个(提升后的)运算对象类型,选择类型转换方式:
-
如果类型匹配,无须进一步转换;
-
如果类型符号相同(都带符号/都无符号),则小类型的运算对象转换成较大的类型;
-
如果类型符号不同,
-
无符号类型 >= 带符号类型,那么带符号的运算对象转换成无符号的。
- 例如,假设两个类型分别是
unsigned int
和int
,则int
类型的运算对象转换成unsigned int
类型(注意int
类型的值为负情况)。
- 例如,假设两个类型分别是
-
无符号类型 < 带符号类型,此时转换的结果依赖于机器。
- 如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换成带符号类型。
- 如果不能,那么带符号类型的运算对象转换成无符号类型。
-
例如,如果两个运算对象的类型分别是
long
和unsigned int
,- 如果
int
和long
的大小相同,则long
类型的运算对象转换成unsigned int
类型; - 如果
long
类型占用的空间比int
更多,则unsigned int
类型的运算对象转换成long
类型。
- 如果
-
bool flag; char cval;
short sval; unsigned short usval;
int ival; unsigned int uival;
long lval; unsigned long ulval;
float fval; double dval;
3.14159L + 'a'; // 'a' 提升成 int,然后该 int 值转换成 long double
dval + ival; // ival 转换成 double
dval + fval; // fval 转换成 double
cval + fval; // cval 提升成 int,然后该 int 值转换成 float
sval + cval; // sval 和 cval 都提升成 int
cval + lval; // cval 转换成 1ong
ival + ulval; // ival 转换成 unsigned Long
usval + ival; // 根据 unsigned short 和 int 所占空间的大小进行提升
uival + lval; // 根据 unsigned int 和 long 所占空间的大小进行转换
3.1.2 数组转换成指针
在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针:
int ia[10]; // 含有 10 个整数的数组
int* ip = ia; // ia 转换成指向数组首元素的指针
不会发生转换的情况:
- 数组被用作
decltype
关字键的参数时; - 数组作为取地址符
&
、sizeof
及typeid
等运算符的运算对象时; - 用一个引用来初化始数组时。
3.1.3 指针的转换
指针转换方式:
- 常量整数值 0 或者字面值
nullptr
能转换成任意指针类型; - 指向任意非常量的指针能转换成
void*
; - 指向任意对象的指针能转换成
const void*
; - 有继承关系类型间的指针转换。
3.1.4 转换成布尔类型
存在一种从算术类型或指针类型向布尔类型自动转换的机制:
如果指针或算术类型的值为 0,转换结果是 false
;否则,转换结果是 true
。
3.1.5 转换成常量
允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。
如果 T 是一种类型,可以将指向 T 的指针或引用分别转换成指向 const T 的指针或引用。但不能反向转换,即,不能删除掉底层的 const。
int i;
const int &j = i; // 非常量转换成 const int 的引用
const int *p = &i; // 非常量的地址转换成 const 的地址
3.1.6 类类型定义的转换
类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。
// 在需要标准库 string 类型的地方使用 C 风格字符串
string s, t = "a value"; // 字符串字面值转换成 string 类型
// 条件 (cin >> s) 读入 cin 的内容,并将 cin(istream 类型)作为其求值结果。
// IO 库定义了从 istream 向布尔值转换的规则,如果最后一次读入成功,转换后的值是 true;否则,转换后的值是 false。
while (cin >> s) { // while 的条件部分把 cin 转换成布尔值
...
}
3.2 显式转换
显式地将对象强制转换成另外一种类型,即强制类型转换(cast)。强制类型转换干扰了正常的类型检查,应尽量避免使用。
3.2.1 命名的强制类型转换
转换形式:cast-name<type>(expression);
type
是转换的目标类型(如果type
是引用类型,则结果是左值);expression
是要转换的值;cast-name
是static_cast
、dynamic_cast
、const_cast
和reinterpret_cast
中的一种,指定了执行的是哪种转换。
3.2.1.1 static_cast
任何具有明确定义的类型转换,只要不包含底层 const
,都可以使用 static_cast
。
// 进行强制类型转换,以便执行浮点数除法
int i, j;
double d = static_cast<double>(j) / i;
// 把一个较大的算术类型赋值给较小的类型,不在乎潜在的精度损失
float f = static_cast<float>(d);
// 进行编译器无法自动执行的类型转换,需要确保转换后所得的类型就是指针 p 所指的类型
void* p = &d;
double* dp = static_cast<double*>(p);
3.2.1.2 dynamic_cast
dynamic_cast
运算符,支持运行时类型识别(run-time type identification,RTTI),用于将基类的指针或引用安全地转换成派生类的指针或引用。
在可能的情况下,最好定义虚函数而非直接接管类型管理的重任。因为与虚成员函数相比,使用 RTTI 运算符蕴含着更多潜在的风险,程序员必须清楚地知道转换的目标类型并且必须检查类型转换是否被成功执行。
使用形式:
dynamic_cast<type*>(e)
// e 必须是一个有效的指针
dynamic_cast<type&>(e)
// e 必须是一个左值
dynamic_cast<type&&>(e)
// e 不能是左值
-
type
必须是一个类类型,并且通常情况下该类型应该含有虚函数; -
e
的类型必须符合以下三个条件中的任意一个:e
的类型是目标type
的公有派生类e
的类型是目标type
的公有基类e
的类型就是目标type
的类型
-
如果符合,则类型转换可以成功;否则,转换失败。
- 如果转换目标是“指针类型”并且失败了,则结果为空指针;
- 如果转换目标是“引用类型”并且失败了,则运符算将抛出一个 bad_cast 异常。
// 假定:1)Base 类至少含有一个虚函数;2)Derived 是 Base 的公有派生类。
// 指针类型的 dynamic_cast
// 在运行时,将指向 Base 的指针 bp 转换成指向 Derived 的指针 dp
// 如果 bp 为空指针,那么 db 是 Derived 类型的空指针
if (Derived *dp = dynamic_cast<Derived *>(bp)) {
// 使用 bp 指向的 Derived 对象
}
else {
// 使用 bp 指向的 Base 对象
}
// 引用类型的 dynamic_cast
void f(const Base &b) {
try {
const Derived &d = dynamic_cast<const Derived &>(b);
// 使用 b 引用的 Derived 对象
}
catch (bad_cast) { // 定义在 typeinfo 标准库头文件中的 std::bad_cast 异常
// 处理类型转换失败的情况
}
}
3.2.1.3 const_cast
const_cast
只能改变运算对象的底层 const
,也只有 const_cast
能改变表达式的常量属性。
如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为;然而,如果对象是一个常量,再使用 const_cast
执行写操作就会产生未定义的后果。
const_cast
常常用于有函数重载的上下文中,在其他情况下使用 const cast
通常意味着程序存在某种设计缺陷。
// 比较两个 string 对象的长度,返回较短的那引个用
const string &shorterString(const string &sl, const string &s2) {
return sl.size() <= s2.size() ? sl : s2;
}
string &shorterString(string &s1, string &s2) {
auto &r = shorterString(const_cast<const string &>(s1),
const_cast<const string &>(s2));
return const_cast<string &>(r);
}
3.2.1.4 reinterpret_cast
reinterpret_cast
通常为运算对象的位模式提供较低层次上的重新解释。本质上依赖于机器。要想安全地使用 reinterpret_cast
,必需对涉及的类型和编译器实现转换的过程都非常了解。
int *ip;
char *pc = reinterpret_cast<char*>(ip);
// 必须牢记 pc 所指的真实对象是一个 int 而非字符,
// 如果把 pc 当成普通的字符指针使用,就可能在运行时发生错误。
string str(pc); // 可能导致异常的运行时行为。
3.2.2 旧式的强制类型转换
在早期版本的 C++ 语言中,显式地进行强制类转型换包含两种形式:
type(expr); // 函数形式的强制类型转换
(type)expr; // C 语言风格的强制类型转换
根据所涉及的类型不同,旧式的强制类型转换分别具有与 const_cast
、static_cast
或 reinterpret_cast
相似的行为。
- 在执行旧式的强制类型转换时,如果换成
const_cast
和static_cast
也合法,则其行为与对应的命名转换一致; - 如果替换后不合法,则旧式强制类转型换的功能与
reinterpret_cast
类似。
// 效果与使用 reinterpret_cast 一样
char *pc = (char*)ip; // ip 是指向整数的指针
与命名的强制类型转换相比,旧式的强制类型转换从表现形式上来说不那么清晰明了,容易被看漏,所以一旦转换过程出现问题,追踪起来也更加困难。
参考
- [美] Stanley B.Lippman著.C++ Primer 中文版(第5版).电子工业出版社.2013.
- [美] Stephen Prata著.C++ Primer Plus(第6版)中文版.人民邮电出版社.2012.
宁静以致远,感谢 Vico 老师。