C++从入门到精通——引用()

C++的引用

  • 前言
  • 一、C++引用概念
  • 二、引用特性
    • 交换
      • 指针
      • 引用
  • 三、常引用
    • 保证值不变
    • 权限的方法
      • 权限的放大
      • 权限的缩小
      • 权限的平移
      • 类型转换
        • 临时变量
  • 四、引用的使用场景
    • 1. 做参数
    • 2. 做返回值
  • 五、传值、传引用效率比较
    • 值和引用的作为返回值类型的性能比较
  • 六、引用和指针的区别
    • 引用和指针的注意点
    • 引用和指针的不同点
  • 七、测试代码展示
    • &.cpp


前言

C++的引用是别名,它为已存在的对象提供了另一个名称。一旦引用被初始化指向一个对象,它就不能再指向其他对象。引用必须在声明时初始化,并且必须初始化为有效的对象或字面量。引用通常用于函数参数和返回值,以实现按引用传递和返回。此外,它们也常用于大型对象和数组,以避免复制的开销。C++11引入了右值引用和移动语义,允许更高效的资源管理和性能优化。总的来说,C++的引用是一种强大的工具,能够增强代码的可读性和性能。


一、C++引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。

在这里插入图片描述
在C++中,引用是一个别名,用于已经存在的变量或对象。引用提供了对变量的间接访问,通过引用,可以通过不同的名称来访问同一变量。

类型& 引用变量名(对象名) = 引用实体;

void TestRef()
{
    int a = 10;
    int& ra = a;//<====定义引用类型
    printf("%p\n", &a);
    printf("%p\n", &ra);
}

在这里插入图片描述

注意:引用类型必须和引用实体是同种类型的

引用的定义使用&符号,如下所示:

int x = 10;
int &ref = x;

在这个例子中,refx的引用,它是x的别名。现在,refx可以互换使用,任何对ref的更改将反映在x上,反之亦然。

二、引用特性

void TestRef()
{
   int a = 10;
   // int& ra;   // 该条语句编译时会出错
   int& ra = a;
   int& rra = a;
   printf("%p %p %p\n", &a, &ra, &rra);  
}

在这里插入图片描述

引用有以下几个特点:

  1. 引用必须在定义时进行初始化,一旦初始化完成后,就无法更改引用的绑定。

  2. 引用必须与其所引用的对象具有相同的类型。

  3. 引用可以作为函数的参数和返回值,通过引用参数传递参数可以避免复制大型对象的开销。

  4. 一个变量可以有多个引用

引用与指针不同,指针是一个对象,可以指向任何其他对象,而引用始终指向同一个对象。另外,引用在使用时不需要解引用操作符(*),因为它本身就是对象的别名。

引用的使用可以简化代码并提高可读性,它常用于函数参数传递、函数返回值、以及在循环中使用。

void increment(int& i) {
    i++;
}

int main() {
    int x = 10;
    increment(x);
    cout << x; // 输出11
    return 0;
}

在这里插入图片描述

在上面的例子中,increment函数接受一个引用参数i,对该引用进行递增操作。在main函数中,将变量x传递给increment函数后,x的值被递增为11。因为参数是引用类型,所以对i的修改会直接影响到x

交换

指针

void Swap(int* a, int* b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
int main{
	int x = 0, y = 10;
	cout << x << "  " << y << endl;
	Swap(&x, &y);
	cout << x << "  " << y << endl;
	return 0;
	}

在这里插入图片描述

引用

void _Swap(int& a, int& b)
{
    int tmp = a;
    a = b;
    b = tmp;
}
int main{
	int x = 0, y = 10;
	cout << x << "  " << y << endl;
	_Swap(x, y);
	cout << x << "  " << y << endl;
	return 0;
	}

在这里插入图片描述

三、常引用

保证值不变

C++中的常引用是指使用const关键字修饰的引用,即被引用的对象不能被修改。常引用和普通引用的主要区别在于,常引用所引用的对象在引用过程中不能被修改。

常引用的语法形式如下:

const T& ref;

其中,T是被引用对象的类型。常引用可以指向任何类型的对象,包括基本类型、自定义类型、指针等。

常引用在函数参数传递中很常用,可以用于避免拷贝大对象,同时又不希望对对象进行修改。在函数定义时,使用常引用作为参数,可以防止函数对参数进行修改。

需要注意的是,引用作为函数参数时,函数内部对引用的修改也会反映到函数外部的变量上。

void print(const int& i) {
    cout << i;
}

int main() {
    int x = 10;
    print(x);
    return 0;
}

在这里插入图片描述

在上述例子中,print函数接受一个const引用参数i,这意味着i不可修改。在main函数中,将变量x传递给print函数后,print函数无法修改x的值。这样做可以确保函数不会意外地修改传递给它的参数。

权限的方法

void TestConstRef()
{
    const int a = 10;
    //int& ra = a;   // 该语句编译时会出错,a为常量
    const int& ra = a;
    // int& b = 10; // 该语句编译时会出错,b为常量
    const int& b = 10;
    double d = 12.34;
    //int& rd = d; // 该语句编译时会出错,类型不同
    const int& rd = d;
}

权限的放大

const int a = 10;
int& ra = a;   // 该语句编译时会出错,a为常量
int p = a;

这样是可取的,因为这是赋值语句

如上const int a = 10;按照语法来理解是可读但不可写,我们使用一个可读可写的int类型来引用就会报错,所以这种方法是不可取的

const int* a = &m;
int* ra = a;   

这也是权限的放大,不可以

权限的缩小

int a = 10;
const int& ra = a;   

如上 int a = 10;按照语法来理解是可读可写,我们使用一个可读的const int类型来引用是没有问题的,所以这种方法是可取的

int p = a;//p = ra;

这样是可取的,因为这是赋值语句

注意a++ra也会++,因为raa的别名,但是ra不能++,因为ra是常量,总结来说就是不能通过ra来修改

 int* a = &m;
const int* ra = a;   

这也是权限的缩小,可以

权限的平移

 	int a = 10;
   int& ra = a;
   int& rra = a;
const int b = 10;
const int& rb = b;

如上 int a = 10;按照语法来理解是可读可写,我们使用一个可读可写的int类型来引用是没有问题的,所以这种方法是可取的,同理const的修饰也是一样的

类型转换

 double d = 12.34;
 //int& rd = d; // 该语句编译时会出错,类型不同
 const int& rd = d;

如上为什么int& rd = d;不行,而 const int& rd = d;确可以,是因为类型转换会生成临时变量,类型转换是将一个数据类型的值转换为另一个数据类型的值,而不是直接修改原始值。因此,在执行类型转换时,会创建一个新的变量来存储转换后的值,并且可以在需要的地方使用。这时的临时变量是具有常性的,使用int& rd = d; 就相当于权限的放大,会报错,需要使用const修饰

临时变量
int x = 0,y = 1;
//int& p = x + y;是不可以的
const int& p = x + y;//是可以的,和上面一样是临时变量的原因

除了类型转换之外,还有以下几种情况会生成临时变量:

  1. 函数返回值:当一个函数返回一个临时变量时,编译器会在函数结束时生成一个临时变量,并将其复制到函数返回的地方。

  2. 表达式计算:在进行表达式计算时,如果表达式中包含临时变量的创建和销毁,编译器会在需要的地方生成临时变量。

  3. 函数调用:当调用函数时,会将实参传递给形参。如果实参的类型与形参的类型不匹配,编译器可能会生成临时变量来进行类型转换。

  4. 对象初始化:当创建对象时,如果使用了拷贝构造函数,编译器会生成一个临时变量来初始化新对象。

  5. 运算符重载:当重载一个运算符时,可能会生成临时变量来进行操作。

需要注意的是,编译器为了优化性能可能会对临时变量进行优化,比如使用编译器自动生成的构造函数、析构函数等。因此,生成临时变量并不一定会带来显著的性能损耗。

四、引用的使用场景

1. 做参数

void Swap(int& left, int& right)
{
   int temp = left;
   left = right;
   right = temp;
}

2. 做返回值

int& Count()
{
   static int n = 0;
   n++;
   // ...
   return n;
}

下面代码输出什么结果?为什么?

int& Add(int a, int b)
{
    int c = a + b;
    return c;
}
int main()
{
    int& ret = Add(1, 2);
    Add(3, 4);
    cout << "Add(1, 2) is :"<< ret <<endl;
    return 0;
}

在这里插入图片描述

在这里插入图片描述

注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

五、传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
    A a;
    // 以值作为函数参数
    size_t begin1 = clock();
    for (size_t i = 0; i < 10000; ++i)
        TestFunc1(a);
    size_t end1 = clock();
    // 以引用作为函数参数
    size_t begin2 = clock();
    for (size_t i = 0; i < 10000; ++i)
        TestFunc2(a);
    size_t end2 = clock();
    // 分别计算两个函数运行结束后的时间
    cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
    cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
    TestRefAndValue();
    return 0;
}

在这里插入图片描述

值和引用的作为返回值类型的性能比较

#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
    // 以值作为函数的返回值类型
    size_t begin1 = clock();
    for (size_t i = 0; i < 100000; ++i)
        TestFunc1();
    size_t end1 = clock();
    // 以引用作为函数的返回值类型
    size_t begin2 = clock();
    for (size_t i = 0; i < 100000; ++i)
        TestFunc2();
    size_t end2 = clock();
    // 计算两个函数运算完成之后的时间
    cout << "TestFunc1 time:" << end1 - begin1 << endl;
    cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
    //TestRefAndValue();
    TestReturnByRefOrValue();
    return 0;
}

在这里插入图片描述

通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。

六、引用和指针的区别

引用和指针的注意点

在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

int main()
{
 	int a = 10;
 	int& ra = a;
 
 	cout<<"&a = "<<&a<<endl;
	 cout<<"&ra = "<<&ra<<endl;
	 return 0;
}

在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

int main()
{
	 int a = 10;
 
 	int& ra = a;
 	ra = 20;
 
 	int* pa = &a;
	 *pa = 20;
 
	 return 0;
}

我们来看下引用和指针的汇编代码对比:
在这里插入图片描述

引用和指针的不同点

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。

  2. 引用在定义时必须初始化,指针没有要求

  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

  4. 没有NULL引用,但有NULL指针

  5. sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)

  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

  7. 有多级指针,但是没有多级引用

  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理

  9. 引用比指针使用起来相对更安全

注意:

int* ptr = NULL;
int& r = ptr;

这样编译器是不会报错的
在这里插入图片描述

int main()
{
    int* ptr = NULL;
    int& r = *ptr;
    cout << r << endl;
    return 0;
}

在这里插入图片描述
这样才会报错,因为指针的解引用是在使用的阶段,在底层是r存放的是ptr的地址

七、测试代码展示

&.cpp

#include <iostream>
using namespace std;
//void TestRef()
//{
//    int a = 10;
//    int& ra = a;//<====定义引用类型
//    printf("%p\n", &a);
//    printf("%p\n", &ra);
//}
void TestRef()
{
    int a = 10;
    // int& ra;   // 该条语句编译时会出错
    int& ra = a;
    int& rra = a;
    printf("%p %p %p\n", &a, &ra, &rra);
}
void increment(int& i) {
    i++;
}
void Swap(int* a, int* b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
void _Swap(int& a, int& b)
{
    int tmp = a;
    a = b;
    b = tmp;
}
//int main()
//{
//    //TestRef();
//    //int x = 10;
//    //increment(x);
//    //cout << x; // 输出11
//    int x = 0, y = 100;
//    cout << x << "  " << y << endl;
//    //Swap(&x, &y);
//    _Swap(x, y);
//    cout << x << "  " << y << endl;
//    return 0;
//}
//void print(const int& i) {
//    cout << i;
//}
//
//int main() {
//    int x = 10;
//    /*int a = 10;
//    const int& ra = a;
//    int p = a;*/
//    const int a = 10;
//    //int& ra = a;   // 该语句编译时会出错,a为常量
//    int p = a;
//    print(p);
//    return 0;
//}
//int& Add(int a, int b)
//{
//    int c = a + b;
//    return c;
//}
//int main()
//{
//    int& ret = Add(1, 2);
//    Add(3, 4);
//    cout << "Add(1, 2) is :" << ret << endl;
//    return 0;
//}
#include <time.h>
//struct A { int a[10000]; };
//void TestFunc1(A a) {}
//void TestFunc2(A& a) {}
//void TestRefAndValue()
//{
//    A a;
//    // 以值作为函数参数
//    size_t begin1 = clock();
//    for (size_t i = 0; i < 10000; ++i)
//        TestFunc1(a);
//    size_t end1 = clock();
//    // 以引用作为函数参数
//    size_t begin2 = clock();
//    for (size_t i = 0; i < 10000; ++i)
//        TestFunc2(a);
//    size_t end2 = clock();
//    // 分别计算两个函数运行结束后的时间
//    cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
//    cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
//}
//struct A { int a[10000]; };
//A a;
 值返回
//A TestFunc1() { return a; }
 引用返回
//A& TestFunc2() { return a; }
//void TestReturnByRefOrValue()
//{
//    // 以值作为函数的返回值类型
//    size_t begin1 = clock();
//    for (size_t i = 0; i < 100000; ++i)
//        TestFunc1();
//    size_t end1 = clock();
//    // 以引用作为函数的返回值类型
//    size_t begin2 = clock();
//    for (size_t i = 0; i < 100000; ++i)
//        TestFunc2();
//    size_t end2 = clock();
//    // 计算两个函数运算完成之后的时间
//    cout << "TestFunc1 time:" << end1 - begin1 << endl;
//    cout << "TestFunc2 time:" << end2 - begin2 << endl;
//}
//int main()
//{
//    //TestRefAndValue();
//    TestReturnByRefOrValue();
//    return 0;
//}
int main()
{
    int* ptr = NULL;
    int& r = *ptr;
    cout << r << endl;
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/505561.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

动态规划-最长回文子串

动态规划-最长回文子串 原题描述解答中心移动思想代码实现复杂度分析时间复杂度空间复杂度 动态规划思想代码实现复杂度分析时间复杂度空间复杂度 突然觉得很有必要将学过的内容记录下来&#xff0c;这样后续在需要用到的时候就可以避免从头进行学习&#xff0c;而去看自己之前…

调试技巧安全预编译头文件(C++基础)

调试 调试可以选择条件调试和操作调试&#xff1a; 条件调试来选择条件进入断点设置&#xff0c;操作调试来使达到断点条件后完成某些操作&#xff08;一般是output窗口输出&#xff09;。 在这里就只输出了小于6的条件。 安全 降低崩溃、内存泄露、非法访问等问题。 应该转…

GetSystemTimes:获取CPU占用率(WIN API)

原文链接&#xff1a;https://blog.csdn.net/qq_28742901/article/details/104960653 GetSystemTimes函数&#xff1a; BOOL WINAPI GetSystemTimes(__out_opt LPFILETIME lpIdleTime, // 空闲时间__out_opt LPFILETIME lpKernelTime, // 内核进程占用时间__out_opt LPFILETI…

【JavaWeb】Day29.SpringBootWeb请求响应——请求(二)

请求响应 4.数组集合参数 数组集合参数的使用场景&#xff1a;在HTML的表单中&#xff0c;有一个表单项是支持多选的(复选框)&#xff0c;可以提交选择的多个值。 4.1 数组 数组参数&#xff1a;请求参数名与形参数组名称相同且请求参数为多个&#xff0c;定义数组类型形参即…

C++取经之路(其一)——namespace(命名空间),cout,cin(输入输出流),缺省参数。

目录 目录&#xff1a; 前言&#xff1a; namespace(命名空间): 命名空间可以嵌套使用如&#xff1a; 相同的命名空间 cout cin输入输出 std命名空间的使用惯例&#xff1a; 缺省参数&#xff1a; 缺省类型&#xff1a; 前言&#xff1a; 最近开始学习C了&#xff0c;…

Web 前端性能优化之二:图像优化

1、图像优化 HTTP Archive上的数据显示&#xff0c;网站传输的数据中&#xff0c;60%的资源都是由各种图像文件组成的。 **图像资源优化的根本思想&#xff0c;可以归结为两个字&#xff1a;压缩。**无论是选取何种图像的文件格式&#xff0c;还是针对同一种格式压缩至更小的…

两种序列化的方式:fastjson 和 Jackson

public class TestMain {public static void main(String[] args) throws JsonProcessingException {//创建一个课表对象LearningLesson lesson new LearningLesson();lesson.setId(1L);lesson.setCourseId(2L);lesson.setStatus(LessonStatus.EXPIRED); //课程状态&#xff0…

网安基础2-Sniffer的使用与防范

1. 嗅探器sniffer的工作原理 能捕获经过该网络设备的报文&#xff0c;通过分析网络流量&#xff0c;找出关键信息&#xff0c;解决网络问题。 不同于键盘捕获程序&#xff0c;如keylogger利用中断或钩子技术&#xff0c;Sniffer将网络接口置成适当的模式&#xff0c;如杂收。…

Java中的集合(详细)

前言 java中自带一些集合类&#xff0c;可以帮助我们更方便地写程序&#xff0c;其中所有的集合类都在java.util包下。 集合有很多有优点&#xff0c;首先它的大小是可以变化的&#xff0c;不像数组一样大小不可变。再者集合可以存储引用数据类型。 HashSet 1.HashSet集合的…

YOLOv9 实现多目标跟踪

YOLOv9项目结合了YOLOv9的快速目标检测能力和DeepSORT的稳定跟踪能力&#xff0c;实现了对视频流中多个对象的实时、准确检测和跟踪。在具体应用中&#xff0c;该项目能够对视频中的行人、车辆或其他物体进行实时定位、识别和持续跟踪&#xff0c;即使在复杂环境、对象互相遮挡…

BUU UPLOAD COURSE 1 文件包含

1.页面是一个文件上传的接口&#xff0c;尝试上传一句话木马&#xff0c;上传成功&#xff0c;但是文件后缀被重命名。 ​​2.因为文件名被重命名就想到了使用%00截断&#xff0c;但是不行。就陷入了死区&#xff0c;老是在想怎么去改后缀。 3.注意到参数是file而且内容是一个…

计算机的浮点数表示法(IEEE 754)

这篇文章与一道题有关&#xff1a; /** floatScale2 - Return bit-level equivalent of expression 2*f for* floating point argument f.* Both the argument and result are passed as unsigned ints, but* they are to be interpreted as the bit-level representati…

一条SQL在MySQL中的执行过程

图解&#xff1a; 第⼀步&#xff1a;连接器 过程 1. 建⽴连接&#xff1a;与客户端进⾏ TCP 三次握⼿建⽴连接&#xff1b; 2. 校验密码&#xff1a;校验客户端的⽤户名和密码&#xff0c;如果⽤户名或密码不对&#xff0c;则会报错&#xff1b;3. 权限判断&#xff1a…

正多边形拓扑与泛函

&#xff08;原创&#xff1a;Daode3056&#xff09; 也许&#xff0c;关于“拓扑”&#xff0c;“泛函”几本书上的内容与实例都是大同小异&#xff0c;总是那么点内容&#xff0c;数学要开拓一些新领域与新内容才能满足不断发展的社会与工业各种需要。本文就以人工智能生成对…

【独立开发前线】Vol.29 专注于电子邮件签名,也可以依靠SEO年入70万美元

今天要给大家分享的案例是MySignature&#xff0c;一个专注于电子邮件签名的产品&#xff1b; 它的官网是&#xff1a;MySignature: Free Email Signature Generator 提到电子邮件签名&#xff0c;很多人想到的肯定是“那不是电子邮件结尾的几行图文介绍吗&#xff0c;这也能做…

CCF-CSP20<2020-09>-第1/2题

202009-1 对称检测点查询 题目&#xff1a;202009-1 题目分析&#xff1a; 给定一群点的坐标&#xff0c;求出距离某点最近的3个点的坐标。 纯模拟即可。 AC代码&#xff1a; // -*- coding:utf-8 -*-// File : 202009-1.cpp // Time : 2024/03/23 // Author …

pajamas 0 publish repo fst in gitee

0. 好久没有blog了&#xff0c;真的好久了&#xff0c;先交代一波 因为半年来发生了很多&#xff0c;计划有变&#xff0c;辞工作&#xff0c;出去耍&#xff0c;找工作&#xff0c;重新计划… 从半年前开始&#xff0c;就想好了&#xff0c;最近这两年应该优先会写代码 &…

uniapp开发App(二)开通 微信授权登录功能(应用签名、证书、包名 全明白)

前言&#xff1a;开发App肯定要包含登陆&#xff0c;常用登陆方式很多&#xff0c;我选择微信登陆。 一、如何获得微信的授权登陆 答&#xff1a;申请&#xff0c;根据uniapp官网的提示有如下三个步骤 开通 1. 登录微信开放平台区&#xff0c;添加移动应用并提交审核&#xf…

热电偶测温仪UT320D 拆机

性能应该还好吧&#xff0c;毕竟是便宜货。本来打算看看学习一下热电偶电路的前端设计&#xff0c;用什么滤波器、保护电路之类的&#xff0c;结果比较失望。 拆机 打开后盖的效果&#xff1a; PCB 另一面没有元件&#xff0c;打眼一看就能看出电路相当简单&#xff0c;功能全…

蓝桥备赛——矩阵读入

题目描述 如上图所示&#xff0c;是一道有关二维前缀和的问题&#xff0c;因为涉及到二维&#xff0c;肯定就是以矩阵的形式进行读入的。 为此&#xff0c;针对矩阵的读入形式进行总结&#xff0c;可以大致总结出两种类型如下&#xff1a; 二维列表推导式 n, m, k map(int…