文章目录
前言
引用作为 C++ 的一个重要新特性,其语法和作用将在本文进行详细的介绍(本文介绍的引用为 C++98 的左值引用,不包含 C++11 提出的右值引用)
const 关键字在程序设计中的重要性也将在本文中讲解,之所以把它放在引用的章节,是因为两者在一些语法上关联度较高
本文会在最后引入 Effective C++ 的条款,希望对大家有所启发
一、引用
1. 概念
引用不是新定义一个对象,而是给已存在对象取了一个别名,代码如下:
int main()
{
int ival = 1024;
int &refval = 1024; // 给 ival 取了一个别名 refval
refval = 0;
cout << ival << endl; // ival : 0
cout << refval << endl; // refval : 0
return 0;
}
因为 refval 是 ival 的别名,所以对 refval 进行修改,ival 的值也会一起发生改变。这就跟我们每一个人都有大名和小名一样,妈妈在叫我们吃饭时,不管喊得是我们的大名还是小名,指向的对象都是我们
2. 语法
编译器不会为引用开辟内存空间,它和它引用的对象共用同一块内存空间,一个对象可以有多个引用
不能修改引用指向的对象
引用在定义的时候就要初始化
一般在初始化对象时,是把初始值拷贝放进对象中;而引用不一样,引用是将别名和它的初始值绑定在一起,这就决定了引用起的别名和引用的对象都是同一对象,指向同一空间。
一旦初始化完成,引用将和它的初始值对象一直绑定在一起,无法令引用重新绑定到另外一个对象。
定义引用时必须初始化告诉编译器该与哪一个对象进行绑定
允许在一条语句中定义多个引用,其中每个别名前都必须以 & 开头,代码如下:
int i1 = 1024, i2 = 2048;
int &r1 = i1, r2 = i2; // r1 是 i1 的别名,但 r2 不是 i2 的别名
int &r3 = i1, &r4 = i2; // r3 是 i1 的别名,r4 是 i2 的别名
3. 使用场景
通过上述对引用的介绍,其实细心的小伙伴可以发现:引用和指针非常相似。 实际上,引用在底层就是用指针实现的,这里就不详解了。因为引用和指针的相似性,两者的使用场景可以说是基本一样
作函数参数
- 作函数的输入型参数可以减少对象的拷贝,形参只是实参的别名,提高了效率
- 作函数的输出型参数可以修改函数外部定义的变量,否则对形参的修改无法影响实参,如交换函数:
void Swap(int& left, int& right) { int temp = left; left = right; right = temp; }
作返回值
- 当引用作为返回值时,一定要保证引用的对象在出了该函数作用域不会被销毁,否则会引发未定义的行为,如下代码为例:
int& Add(int a, int b) { int c = a + b; return c; // c 为在栈上创建的变量,出了函数作用域就会被销毁 } int main() { int res = Add(10, 20); // 会发生越界访问 return 0; }
- 如果是传值返回,会将返回值拷贝放进临时变量中,这个临时变量的生命周期为调用函数的表达式,故可以用变量来接收这个返回值(其实是进行赋值)
- 但如果是传引用返回,则不会将返回值拷贝到临时对象中,故在函数内部创建的临时变量,在出了函数作用域后,用变量接收返回值会越界访问(用已经销毁的变量来赋值当然会报错)
- 当想要返回一个在堆上的资源并对其进行修改时,引用的价值就体现出来了,如下是 map 重载运算符 [ ] 的函数接口,该函数返回 map 中 key 值绑定 value 的引用,可以对其进行修改
- 当引用作为返回值时,一定要保证引用的对象在出了该函数作用域不会被销毁,否则会引发未定义的行为,如下代码为例:
4. 指针和引用的联系与区别
- 联系:
- 引用在底层通过指针实现
- 两者的使用场景及其相似
- 区别:
- 概念上: 引用为一个对象的别名,指针存储一个对象的地址
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任意一个实体
- 在 sizeof 中含义不同:引用结果为引用类型的大小,但指针结果为存放地址的空间的大小 ( 4 或 8 个字节 )
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用则是编译器自己处理
- 引用比指针使用起来相对更安全
二、const 关键字
1. 概念
有时候我们希望定义这样一种变量,它的值不能被修改。并且还希望,后续不小心修改这个变量,能得到相关的反馈。我们将这样的变量用 const 修饰,代码如下:
int main()
{
const int x = 5;
x = 10; // 报错,const 修饰的对象无法被修改
return 0;
}
2. const 与指针
当我们想定义一个指针来指向常量字符串时,需要在 * 前加 const 修饰,表示该指针指向的对象不能被修改,代码如下:
int main()
{
const char *str = "hello world";
char *str = "hello world"; // 报错,禁止 char* 指向常量字符串
return 0;
}
实际上,在进行指针的赋值时,涉及权限的转移,权限只能平移和缩小,不能放大。常量字符串的权限为可读不可写,当用 char* 指向字符串时,权限变为可读可写,即可以对常量字符串进行修改了,编译器不会允许这种权限放大的行为,故会进行报错
const int x = 10;
int* px1 = &x; // 权限放大,错误
const int* px2 = &x; // 权限平移,正确
int y = 20;
int* py1 = &y; // 权限平移,正确
const int* py1 = &y; // 权限缩小,正确
注意:
const 在 * 前表示指针指向的内容不能被修改
const 在 * 后表示指针本身不能被修改
3. const 引用
第一部分引用的讲解中,被引用的实体都是变量,那引用是否可以指向字面常量或表达式呢?
C++ 可以用 const 来对常量进行引用,const 修饰引用表示引用指向的对象无法被修改,和指针一样,引用也涉及权限的转移,权限只能平移和缩小,不能放大。常量的权限为可读不可写,当用普通引用,权限变为可读可写,即可以修改常量,编译器不会允许这种权限放大的行为,故会进行报错;但用 const 引用,权限为可读不可写,权限平移,则不会报错
int & r1 = 10; // 权限放大,错误
const int & r2 = 10; // 权限平移,正确
隐式类型转换、匿名对象、表达式、传值返回,引用都需要加 const 修饰
int Add(int x, int y)
{
return x + y;
}
void TestConstRef()
{
// 隐式类型转换
double d = 12.34;
const int& rd = d; // 错误:int& rd = d;
// 匿名对象
const string &rs = string(); // 错误:string &rs = string();
// 表达式
int x = 10, y = 20;
const int &add = x + y; // 错误:int& add = x + y;
// 传值返回
const int &ret = Add(x,y); // 错误:int &ret = Add(x,y);
}
原因:隐式类型转换、匿名对象、表达式、传值返回的值都会放在一个临时变量中,这个变量具有常属性,引用即指向这个临时变量,故要加 const 修饰
三、Effective C++ 条款的引入
1. 条款02:尽量以 const、enum、inline 代替 #define
在上一章 C++ 函数新特性中,我们已经详解过,为什么 C++ 要用 inline 内联函数来代替宏,这一章我们将把 const、enum 代替宏的原因详细说明
该条款的实际含义是:“宁可以编译器替换预处理器”。当我们想定义一个不会改变的常量并且想在后续使用时,一般做法是用宏定义:#define ASPECT_RATIO 1.653
。然而,宏的本质是替换,这也是它的问题所在:用 #define 定义的记号名称在预处理阶段就被预处理器移走了,编译器在后续阶段中不会把 ASPECT_RATIO 放进符号表内
当我们后续运用此常量但获得一个错误的编译信息时,可能会带来困惑,因为这个错误信息也许会提到 1.653 而不是 ASPECT_RATIO。如果 ASPECT_RATIO 被定义在一个非我们所写的头文件中,就更难找到错误了。
更让程序员难以接受的是,宏不利于调试:调试界面所显示的是被替换的常量 1.653,而我们看到的是记号名称 ASPECT_RATIO。
解决之道是以一个语言常量替换上述的宏,因为作为一个语言常量,如 const、enum ,肯定会被编译器放进符号表内,如下代码为例:
// 定义一个缓冲区大小
const int BufferSize = 1024;
enum { BufferSize = 1024 };
2. 条款03:尽可能使用 const
当你确实要定义一个不该改动的对象,就应该加 const 修饰明确告诉编译器,也防止我们后续不小心修改这个对象
同时也增加了代码的可读性,当一个程序员看到该变量加了 const 修饰,就知道在后续的逻辑中,该变量可读不可写
当函数参数为引用/指针时,且函数内部不会修改该引用/指针指向的对象,尽可能加上 const,使函数能操作 const 对象
- const 语法中说过,引用/指针涉及权限的转移,即普通引用/指针无法指向具有常性的对象,这时我们想要通过类型转换或传一个匿名对象,普通引用/指针则无法通过,如下为例:
// 实现一个将数字字符串 string 转化为数字 int 的函数 // 引用传参且函数内部不会修改字符串,加 const 修饰 int stoi(const string &str) { int num = 0; // 转化逻辑 return num; } int main() { int ret1 = stoi("1234"); // "1234" 从 char* 隐式类型转换为 string 类型,引用指向临时变量,需要加 const 修饰才能对其引用 int ret2 = stoi(string("1234"); // string("1234") 为匿名对象,具有常性,需要加 const修饰才能对其引用 return 0; }
- const 语法中说过,引用/指针涉及权限的转移,即普通引用/指针无法指向具有常性的对象,这时我们想要通过类型转换或传一个匿名对象,普通引用/指针则无法通过,如下为例:
3. 条款20:宁以传常引用传参替换传值传参
以引用接收可以减少对象的拷贝,提高效率,特别是对于自定义类型,将不会在调用函数时调用构造和析构函数(涉及类和对象,在后续章节会详细说明)
加 const 修饰主要是为了操作 const 对象并告诉我们函数内部不会修改对象,同条款03
以引用接收派生类对象可以保留该对象的特质,不会因为传值传参而使形参变为基类对象,便于后续的多态调用(这里涉及继承和多态,后面章节会详细说明)
总结
这个章节主要讲解了 const 关键字和 C++ 新特性引用在程序设计中的重要性
const 和引用的语法是我们编写程序的基础,而后续 Effective C++ 的条款则能让我们的编写的程序更加完善,值得我们学习!!!