指针与引用区别

 

=====

来自:https://www.cnblogs.com/honernan/p/12175899.html

每日一问1:指针和引用的区别

 

  指针(pointer)和引用(references)都是对另一个变量的一种关联,即通过指针和引用可以使用另一个变量。而它们最大的区别在于:指针是一个变量,而引用不是。但在c++底层中,引用是通过指针实现的,所以,在实现层面上来说,引用就是指针,但是在c++语法上来说,c++编译器并不为引用类型分配内存,所以引用不能为空,必须被初始化,一旦初始化不能更改引用对象。所有对引用的操作都是对原始对象的操作 。

  指针是一个变量,它所存储的就是它所指向的变量的地址,而它同时也有着自己的地址。引用可以理解为变量的"小名”,它的地址也就是它引用的变量的地址。也可以说,指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。它们更细致的区别总结如下:

 


  后续查阅资料的过程中,发现了自己理解的错误,因为是学习的过程,所以不删除错误,引以为戒。在上面提到引用不是一个变量,这是错误的,引用是一个变量,它在底层的实现就是一个变量,一个常量指针。C++对常量指针进行一些包装后就成了引用。

==========

来自:https://csguide.cn/cpp/memory/difference_of_pointers_and_ref.html#google_vignette

区别
指针和引用在 C++ 中都用于间接访问变量,但它们有一些区别:

指针是一个变量,它保存了另一个变量的内存地址;引用是另一个变量的别名,与原变量共享内存地址。

指针可以被重新赋值,指向不同的变量;引用在初始化后不能更改,始终指向同一个变量。

指针可以为 nullptr,表示不指向任何变量;引用必须绑定到一个变量,不能为 nullptr。

使用指针需要对其进行解引用以获取或修改其指向的变量的值;引用可以直接使用,无需解引用。

下面的示例展示了指针和引用的区别:

#include <iostream>

int main() {
int a = 10;
int b = 20;

// 指针
int *p = &a;
std::cout << "Pointer value: " << *p << std::endl; // 输出:Pointer value: 10

p = &b;
std::cout << "Pointer value: " << *p << std::endl; // 输出:Pointer value: 20

// 引用
int &r = a;
std::cout << "Reference value: " << r << std::endl; // 输出:Reference value: 10

// r = &b; // 错误:引用不能被重新绑定
int &r2 = b;
r = r2; // 将 b 的值赋给 a,r 仍然引用 a
std::cout << "Reference value: " << r << std::endl; // 输出:Reference value: 20

return 0;
}
当然啦,上面说的都是很简单的一些概念,这里继续引用之前公众号发过的一篇文章: 指针和引用有什么区别(opens new window)

这篇文章中从汇编底层角度来解释 C++ 中引用的实现机制。

#从汇编看引用和指针
要是问引用和指针有什么区别,相信大多数学过c++的都能回答上几点: 指针是所指内存的地址,引用是别名,引用必须初始化。。。。。

但是引用是别名这是c++语法规定的语义,那么到底引用在汇编层面和指针有什么区别呢?

没区别。

对,引用会被c++编译器当做const指针来进行操作。

#汇编揭开引用面纱
先分别用指针和引用来写个非常熟悉的函数swap

// 指针版
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
// 引用版
void swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
直接gcc -S 输出汇编

# 引用版汇编
__Z4swapRiS_: ## @_Z4swapRiS_
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movq %rdi, -8(%rbp) # 传入的第一个参数存放到%rbp-8 (应该是采用的寄存器传参,而不是常见的压栈)
movq %rsi, -16(%rbp) # 第二个参数 存放到 %rbp-16
movq -8(%rbp), %rsi # 第一个参数赋给 rsi
movl (%rsi), %eax # 以第一个参数为地址取出值赋给eax,取出*a暂存寄存器
movl %eax, -20(%rbp) # temp = a
movq -16(%rbp), %rsi # 将第二个参数重复上面的
movl (%rsi), %eax
movq -8(%rbp), %rsi
movl %eax, (%rsi) # a = b
movl -20(%rbp), %eax # eax = temp
movq -16(%rbp), %rsi
movl %eax, (%rsi) # b = temp
popq %rbp
retq
.cfi_endproc
## -- End function
在来一个函数调用引用版本swap

void call() {
int a = 10;
int b = 3;
int &ra = a;
int &rb = b;
swap(ra, rb);
}
对应汇编:

__Z4callv: ## @_Z4callv
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
leaq -8(%rbp), %rax # rax中是b的地址
leaq -4(%rbp), %rcx # rcx中是a的地址
movl $10, -4(%rbp)
movl $3, -8(%rbp) # 分别初始化a、b
movq %rcx, -16(%rbp) # 赋给ra引用
movq %rax, -24(%rbp) # 赋给rc引用
movq -16(%rbp), %rdi # 寄存器传参, -16(%rbp)就是rcx中的值也就是a的地址
movq -24(%rbp), %rsi # 略
callq __Z4swapRiS_
addq $32, %rsp
popq %rbp
retq
清楚了! 我们可以看到给引用赋初值也就是把所绑定对象的地址赋给引用所在内存,和指针是一样的。

再来看看指针的汇编吧

__Z4swapPiS_: ## @_Z4swapPiS_
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq -8(%rbp), %rsi
movl (%rsi), %eax
movl %eax, -20(%rbp)
movq -16(%rbp), %rsi
movl (%rsi), %eax
movq -8(%rbp), %rsi
movl %eax, (%rsi)
movl -20(%rbp), %eax
movq -16(%rbp), %rsi
movl %eax, (%rsi)
popq %rbp
retq
.cfi_endproc
## -- End function
汇编我就不注释了,真的是完全一样!并不是我直接复制的引用汇编而是真的是一种方式实现的

指针版调用

void pointer_call() {
int a = 10;
int b = 3;
int *pa = &a;
int *pb = &b;
swap(pa, pb);
}
这次我特意改了下函数名,对应汇编:

__Z12pointer_callv: ## @_Z12pointer_callv
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
leaq -8(%rbp), %rax
leaq -4(%rbp), %rcx
movl $10, -4(%rbp)
movl $3, -8(%rbp)
movq %rcx, -16(%rbp)
movq %rax, -24(%rbp)
movq -16(%rbp), %rdi
movq -24(%rbp), %rsi
callq __Z4swapPiS_
addq $32, %rsp
popq %rbp
retq
还是几乎完全一样...。也没再注释

#简单总结
引用只是C++语法糖,可以看作编译器自动完成取地址、解引用的指针常量
引用区别于指针的特性都是编译器约束完成的,一旦编译成汇编就喝指针一样
由于引用只是指针包装了下,所以也存在风险,比如如下代码:
int *a = new int;
int &b = *a;
delete a;
b = 12; // 对已经释放的内存解引用
引用由编译器保证初始化,使用起来较为方便(如不用检查空指针等)
尽量用引用代替指针
引用没有顶层const即int & const,因为引用本身就不可变,所以在加顶层const也没有意义; 但是可以有底层const即 const int&,这表示引用所引用的对象本身是常量
指针既有顶层const(int * const--指针本身不可变),也有底层const(int * const--指针所指向的对象不可变)
有指针引用--是引用,绑定到指针, 但是没有引用指针--这很显然,因为很多时候指针存在的意义就是间接改变对象的值,但是引用本身的值我们上面说过了是所引用对象的地址,但是引用不能更改所引用的对象,也就当然不能有引用指针了。
指针和引用的自增(++)和自减含义不同,指针是指针运算, 而引用是代表所指向的对象对象执行++或--

作者: 编程指北
链接: https://csguide.cn/cpp/memory/difference_of_pointers_and_ref.html#%E5%8C%BA%E5%88%AB
来源: https://csguide.cn
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

=======================

c++ 引用 例子:

在C++中,引用(reference)是一种轻量级的变量别名。引用必须在声明时进行初始化,并且一旦绑定到一个对象后,就不能再绑定到其他对象。以下是一些关于C++引用的示例:

基本引用示例

cpp
#include <iostream> int main() { int x = 10; int& ref = x; // ref 是 x 的引用 std::cout << "x = " << x << ", ref = " << ref << std::endl; ref = 20; // 修改 ref 也会修改 x std::cout << "x = " << x << ", ref = " << ref << std::endl; return 0; }

在这个示例中:

  • int& ref = x; 创建了一个引用ref,它是变量x的别名。
  • 修改引用ref的值,会直接影响到原变量x

引用作为函数参数

使用引用作为函数参数,可以避免复制大对象,提高性能,同时还可以修改传入的参数。

cpp
#include <iostream> void increment(int& value) { value++; // 直接修改传入的参数 } int main() { int a = 5; increment(a); // 传递 a 的引用 std::cout << "a = " << a << std::endl; // a 被修改为 6 return 0; }

在这个示例中:

  • void increment(int& value) 函数接受一个整数引用value
  • 调用increment(a)时,传入a的引用,函数内部对value的修改会直接影响到a

引用作为返回值

函数可以返回引用,但要小心,返回引用的变量必须在函数调用后依然有效。

cpp
#include <iostream> int& getElement(int* array, int index) { return array[index]; // 返回数组元素的引用 } int main() { int arr[5] = {1, 2, 3, 4, 5}; getElement(arr, 2) = 10; // 修改数组中第三个元素 for (int i = 0; i < 5; ++i) { std::cout << arr[i] << " "; } std::cout << std::endl; return 0; }

在这个示例中:

  • int& getElement(int* array, int index) 函数返回数组中某个元素的引用。
  • getElement(arr, 2) = 10; 通过引用直接修改数组元素。

常量引用

常量引用(const reference)用于避免修改传入的参数,同时仍然使用引用传递的性能优势。

cpp
#include <iostream> void printValue(const int& value) { std::cout << "Value: " << value << std::endl; } int main() { int a = 42; printValue(a); // 传递 a 的常量引用 return 0; }

在这个示例中:

  • void printValue(const int& value) 函数接受一个整数的常量引用,保证函数内部不会修改传入的参数。

引用与指针的区别

虽然引用和指针有一些相似之处,但它们也有显著的区别:

  • 引用必须在声明时初始化,而指针可以在声明后再赋值。
  • 引用一旦绑定到一个对象,就不能再改变绑定对象,而指针可以重新指向不同的对象。
  • 语法上,引用使用起来更像变量本身,而指针需要解引用操作符*

示例:引用和指针的区别

cpp
#include <iostream> int main() { int x = 10; int y = 20; int& ref = x; // 引用,必须初始化 int* ptr = &x; // 指针,可以重新赋值 ref = y; // 修改 ref 的值,x 变为 20,而 ref 仍然指向 x ptr = &y; // 重新指向 y std::cout << "x = " << x << ", ref = " << ref << std::endl; std::cout << "y = " << y << ", *ptr = " << *ptr << std::endl; return 0; }

在这个示例中:

  • ref是一个引用,绑定到x,无法重新绑定到y
  • ptr是一个指针,最初指向x,然后重新指向y

通过这些示例,可以更好地理解C++中的引用及其用法。

==============

二、引用作为函数返回值

说明:

(1)以引用返回函数值,定义函数时需要在函数名前加&

(2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。

例如:

#include <iostream.h>
float temp; //定义全局变量temp
float fn1(float r); //声明函数fn1
float &fn2(float r); //声明函数fn2
float fn1(float r) //定义函数fn1,它以返回值的方法返回函数值
{
 temp=(float)(r*r*3.14);
 return temp;
}
float &fn2(float r) //定义函数fn2,它以引用方式返回函数值
{
 temp=(float)(r*r*3.14);
 return temp;
}
void main() //主函数
{
 float a=fn1(10.0); //第1种情况,系统生成要返回值的副本(即临时变量)
 float &b=fn1(10.0); //第2种情况,可能会出错(不同 C++系统有不同规定)
 //不能从被调函数中返回一个临时变量或局部变量的引用
 float c=fn2(10.0); //第3种情况,系统不生成返回值的副本
 //可以从被调函数中返回一个全局变量的引用
 float &d=fn2(10.0); //第4种情况,系统不生成返回值的副本
 //可以从被调函数中返回一个全局变量的引用
 cout<<a<<c<<d;
}
引用作为返回值,必须遵守以下规则:

(1)不能返回局部变量的引用。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。

(2)不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。

(3)可以返回类成员的引用,但最好是const。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常 量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/caomin1hao/article/details/82227317

 ==============

c++ 引用作为函数参数 返回 class

在C++中,可以使用引用作为函数参数和返回值来提高性能和实现某些功能。特别是对于大型对象或类实例,使用引用可以避免不必要的拷贝开销。

使用引用作为函数参数和返回值的示例

假设我们有一个简单的类Person,我们将演示如何使用引用来作为函数参数和返回值。

cpp
#include <iostream> #include <string> class Person { public: std::string name; int age; Person(const std::string& name, int age) : name(name), age(age) {} void print() const { std::cout << "Name: " << name << ", Age: " << age << std::endl; } }; // 函数接受 Person 对象的引用作为参数,并返回该引用 Person& updatePerson(Person& person, const std::string& newName, int newAge) { person.name = newName; person.age = newAge; return person; // 返回修改后的对象引用 } int main() { Person john("John Doe", 30); john.print(); // 使用函数更新 Person 对象 Person& updatedJohn = updatePerson(john, "John Smith", 35); updatedJohn.print(); // 验证原对象是否被修改 john.print(); return 0; }

在这个示例中:

  1. 定义类

    cpp
    class Person { public: std::string name; int age; Person(const std::string& name, int age) : name(name), age(age) {} void print() const { std::cout << "Name: " << name << ", Age: " << age << std::endl; } };
  2. 引用作为函数参数

    cpp
    Person& updatePerson(Person& person, const std::string& newName, int newAge) { person.name = newName; person.age = newAge; return person; // 返回修改后的对象引用 }

    updatePerson函数接受一个Person对象的引用并修改它,然后返回该对象的引用。这样就避免了对象的拷贝,并且函数可以直接修改传入的对象。

  3. 在主函数中调用

    cpp
    int main() { Person john("John Doe", 30); john.print(); // 使用函数更新 Person 对象 Person& updatedJohn = updatePerson(john, "John Smith", 35); updatedJohn.print(); // 验证原对象是否被修改 john.print(); return 0; }
    • 首先创建一个Person对象john并打印其信息。
    • 然后调用updatePerson函数更新john的姓名和年龄,并将返回的引用赋给updatedJohn
    • 打印updatedJohn的信息,验证其已经被更新。
    • 最后,打印原始对象john的信息,确认其确实被修改。

常量引用

如果不需要修改传入的对象,可以使用常量引用(const reference),以确保函数不会意外地修改对象:

cpp
void printPerson(const Person& person) { person.print(); } int main() { Person john("John Doe", 30); printPerson(john); // 通过常量引用传递 return 0; }

使用常量引用,可以避免拷贝,提高性能,并且保证函数内部不会修改传入的对象。

通过这些示例,可以看到如何使用引用作为函数参数和返回值来处理类实例,避免不必要的对象拷贝,从而提高性能和代码效率。

==============

参考:

posted @ 2024-05-30 15:18  redrobot  阅读(15)  评论(0编辑  收藏  举报