C++学习笔记--引用和 const 关键字

文章详细介绍了C++中的引用和const关键字的概念、语法以及使用场景,包括引用作为函数参数、返回值和与指针的区别,以及const关键字在防止变量修改和提升代码可读性方面的应用。同时提及了EffectiveC++中的条款,提倡使用const和enum代替宏,以及以常引用传参优化效率。
摘要由CSDN通过智能技术生成


前言

  • 引用作为 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}
      

3. 条款20:宁以传常引用传参替换传值传参

  • 以引用接收可以减少对象的拷贝,提高效率,特别是对于自定义类型,将不会在调用函数时调用构造和析构函数(涉及类和对象,在后续章节会详细说明)
  • 加 const 修饰主要是为了操作 const 对象并告诉我们函数内部不会修改对象,同条款03
  • 以引用接收派生类对象可以保留该对象的特质,不会因为传值传参而使形参变为基类对象,便于后续的多态调用(这里涉及继承和多态,后面章节会详细说明)


总结

这个章节主要讲解了 const 关键字和 C++ 新特性引用在程序设计中的重要性

const 和引用的语法是我们编写程序的基础,而后续 Effective C++ 的条款则能让我们的编写的程序更加完善,值得我们学习!!!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值