C++特性——引用与指针详解

简介: C++特性——引用与指针详解


引用

简单来说,引用就是给一个变量起一个别名。例如:

int a = 1;
int& b = a;

对于上面的代码,我们就说ba的别名,我们可以看看ba的地址:

我们可以发现,别名ba共用一块地址,不会开辟新的空间,我们可以将下面三段代码进行比较:

//代码一
int a = 1;
int& b = a;
//代码二
int a = 1;
int b = a;
//代码三
int a = 1;
int* b = &a;
  • 代码一:ba别名,前面已经讲过,这里不再赘述
  • 代码二:创建了一个新的整形变量b,并将a的值赋值给b
  • 代码三:创建了一个指针变量b指向变量a

我们可以画图来更加明确他们的关系:

1. 引用的作用

1.1 引用可以做函数参数:

既然我们知道引用就表示给一个变量取别名,那么我们就可以用引用做函数实参,来简化用C语言写时较为复杂的代码,例如:

  • 交换两个数的值Swap
/*
  形参a,b是两个别名,和实参共用一个地址。
  因此形参的改变就会影响实参的改变
  故可以不要像C语言一样传实参的地址
*/
void Swap(int& a, int& b)
{
  int temp = a;
  a = b;
  b = temp;
}
  • 无哨兵位单链表的尾插SLPushBack
typedef struct ListNode
{
  struct ListNode* next;
  int val;
}SL;
//如果不用别名,那么形参就应该是二级指针,只有这样才能改变原本为一级指针的实参
void SLPushBack(SL*& phead, int data)
{
  assert(phead);
  SL* newNode = (SL*)malloc(sizeof(SL));
  newNode->val = data;
  newNode->next = NULL;
  if (phead == NULL)
    phead = newNode;
  else
  {
    SL* cur = phead;
    while (cur->next)
      cur = cur->next;
    cur->next = newNode;
  }
}

总结:

引用做函数实参,有以下好处:

  1. 形参的改变可以影响实参,这样可以降低函数的复杂度并提高函数的可读性
  2. 对于大型的实参,如果将它的别名作为形参传入函数,就可以大大提高函数效率

1.2 引用做函数返回值:

先来看一串代码:

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

大家来想一想,这两次打印的ret的结果分别是什么?

第一次为3大家可能认为没问题,但为什么第二次明明ret没有接收函数Add(3, 4)的返回值,为什么他的值会变成7呢?

原因是:

  • Add函数的返回值类型是一个引用,即局部变量c的别名,而接收对象ret也是引用,因此ret也是局部变量c的别名
  • 所以尽管第二次ret没有接受返回值,但是c的改变也会影响其别名ret的改变,所以最后ret的值也会变为7。

其实,上面的分析并不完全正确

  • 应该清楚,随着函数栈帧的销毁,其函数内部的局部变量也会被销毁
  • 因此ret打印出的结果应该是不确定的
  • 这里之所以可以打印出看似正确的结果3和7,可能因为博主用的编译器并没有清理函数Add的栈帧

而要使代码正确,我们可以将变量c设置为静态变量,这样就不会随着函数栈帧的销毁而被清除了。

清楚了这一点,再来看下面的两串代码:

代码一:

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

代码二:

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

大家认为,这两串代码打印的结果一样吗?

如果想错了的小伙伴,可能忽略了一点:静态变量只能被初始化一次

因此,对于代码一,定义静态变量c时,将其初始化为a + b = 3,那么之后就不会被初始化,ret的值也就不会变了。

经过上面的一系列分析,可以得出结论:

如果将局部变量的引用作为返回值,那么出了函数作用域,返回对象就被销毁了,不能用引用返回,否则结果就是不正确的。

接下来我们举一个正确使用引用做返回值的例子:

//功能:将顺序表的每个值++
typedef struct SeqList
{
  int data[100];
  int size;
}SL;
int& SLPosModify(SL* sl, int pos)
{
  return (sl->data)[pos];
    //返回顺序表每个位置的别名,从而改变返回对象就可以改变顺序表内的内容
}
int main()
{
  SL* sl = (SL*)malloc(sizeof(SL));
  sl->size = 100;
  memset(sl->data, 0, sizeof(int) * sl->size);
  for (int i = 0; i < sl->size; i++)
    SLPosModify(sl, i)++;
  return 0;
}

可以看到,引用做函数返回值有以下好处:

  1. 可以直接修改返回对象
  2. 可以简化代码,提高代码可读性
  3. 可以提高效率

2 常引用

常引用(const reference)是一种引用变量的方式,用于表示引用的目标对象不可被修改

需要注意以下几点:

  1. 取别名时不能将原来的权限放大,只能权限平移或者权限缩小。例如:
const int a = 1;
int& b = a; //权限放大
  1. 会有如下报错信息:

    而下面的写法就是正确的:
int a = 1;
const int& b = a; //权限缩小
const int& c = b; //权限平移
  1. 可以给常量取别名。例如:
const int& d = 10;
  1. 当引用涉及到类型转换时,要注意临时变量的常性。例如:
int a = 1;
double& b = a;
  1. 错误的。而
int a = 1;
const double& b = a;
  1. 就是正确的
  • 这是由于

当给整型变量a取一个浮点型的别名b时,就会发生隐式转换,中间就会产生一个临时变量

而由于临时变量具有常性(const),因此由上面我们所说的取别名不能放大权限,因此正确的写法为const double& b = a;

3 引用和指针

在上面的介绍中,我们例举了很多引用替代指针来实现指针功能的例子。那么我们是否可以这样认为:C++中,引用可以完全取代指针?

答案是:不能!

首先我们应该清除引用有两点特性:

  1. 引用在定义时必须被初始化
  2. 引用不能被修改指向,只能修改其值(例如,如果c原来是a的引用,那么c就只能是a的引用)

例如:

而我们又清楚,指针可以随意改变它的指向,这是引用无法做到的,因此在C++中,引用是无法完全取代只针的,应该说,引用和指针是互补的。

3.1 引用和指针在语法层面和底层的异同

  1. 语法上引用就是给一个变量取别名,不会开辟额外空间,和原来的变量共用一个地址。而指针就会开辟空间用来存储这个指针变量。
  2. 底层上,引用和指针一样,都是用汇编实现的。因此我们可以说,实际上声明和指针都是传地址

那么日常情况下,我们是以语法为主还是底层为主呢?

答案是以语法为主。我们来看一个例子:

char a = 1;
char& b = a;
cout << sizeof(b) << endl;

32位系统,如果输出的是4,那就说明以底层为主,如果输出为1,那就说明以语法为主。

我们可以看到,输出的结果是1,因此日常情况下,以语法为主

3.1 引用和指针的不同

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址
  2. 引用在定义时必须初始化,指针没有要求
  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  4. 没有 NULL 引用,但有 NULL 指针
  5. 在 sizeof 中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  7. 有多级指针,但是没有多级引用(一个变量可以有多个引用)
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 引用比指针使用起来相对更安全,但不是绝对安全

本篇完。

相关文章
|
2月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
134 59
|
1月前
|
安全 编译器 C++
【C++11】新特性
`C++11`是2011年发布的`C++`重要版本,引入了约140个新特性和600个缺陷修复。其中,列表初始化(List Initialization)提供了一种更统一、更灵活和更安全的初始化方式,支持内置类型和满足特定条件的自定义类型。此外,`C++11`还引入了`auto`关键字用于自动类型推导,简化了复杂类型的声明,提高了代码的可读性和可维护性。`decltype`则用于根据表达式推导类型,增强了编译时类型检查的能力,特别适用于模板和泛型编程。
25 2
|
10天前
|
存储 程序员 C++
深入解析C++中的函数指针与`typedef`的妙用
本文深入解析了C++中的函数指针及其与`typedef`的结合使用。通过图示和代码示例,详细介绍了函数指针的基本概念、声明和使用方法,并展示了如何利用`typedef`简化复杂的函数指针声明,提升代码的可读性和可维护性。
38 0
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
109 4
|
2月前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
2月前
|
存储 搜索推荐 C语言
如何理解指针作为函数参数的输入和输出特性
指针作为函数参数时,可以实现输入和输出的双重功能。通过指针传递变量的地址,函数可以修改外部变量的值,实现输出;同时,指针本身也可以作为输入,传递初始值或状态。这种方式提高了函数的灵活性和效率。
|
2月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
65 1
|
2月前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
51 2
|
2月前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
44 0
|
2月前
|
算法 C++
【算法】双指针+二分(C/C++
【算法】双指针+二分(C/C++