在 C++ 中,引用和指针都是用于间接访问对象的工具,但它们有一些重要的区别。
-
引用:
- 引用是变量的别名,使用
&
符号声明。 - 引用在声明时必须初始化,并且一旦初始化后,不能再重新绑定到其他对象。
- 引用在使用时类似于被引用对象的别名,因此可以直接操作被引用对象的值,无需使用解引用操作符(
*
)。 - 引用不存在空引用,即不存在指向空对象的引用。
- 引用是变量的别名,使用
-
指针:
- 指针是一个变量,存储了另一个对象的地址,使用
*
符号声明。 - 指针可以在任何时候被重新赋值,可以指向不同的对象或者指向空(null)。
- 使用指针需要通过解引用操作符(
*
)来访问所指向对象的值。
- 指针是一个变量,存储了另一个对象的地址,使用
下面是一些情况下更适合使用引用而不是指针的情况:
-
函数参数传递:
- 当函数需要传递参数并且不希望修改原始对象时,可以使用引用来传递参数。这样可以避免创建副本,并且可以直接修改原始对象。
#include <iostream> // 函数原型,使用引用作为参数 void increment(int& num) { num++; // 修改原始对象的值 } int main() { int value = 10; std::cout << "Original value: " << value << std::endl; // 调用函数并传递引用参数 increment(value); std::cout << "Value after increment: " << value << std::endl; return 0; }
在上面的示例中,
increment
函数接受一个整型引用作为参数,并在函数内部修改了原始对象的值。通过引用传递,increment
函数可以直接修改main
函数中定义的value
变量的值,而无需返回任何值。因此,输出结果将显示value
的值在增加后的结果。 - 如果函数需要修改原始对象,也可以使用引用参数,而不是指针参数,因为引用参数的语法更简洁。
- 当函数需要传递参数并且不希望修改原始对象时,可以使用引用来传递参数。这样可以避免创建副本,并且可以直接修改原始对象。
-
函数返回值:
- 当函数需要返回一个对象,并且该对象是函数内部创建的局部变量时,可以使用引用返回。这样可以避免对象的拷贝操作,提高性能。
- 注意,不要返回函数内部局部变量的引用,因为函数执行完毕后局部变量会被销毁,返回的引用将变成悬空引用。
#include <iostream> // 函数原型,返回引用类型 int& getMaximum(int& num1, int& num2) { // 返回两个数中的较大值的引用 return (num1 > num2) ? num1 : num2; } int main() { int a = 10; int b = 20; std::cout << "a = " << a << ", b = " << b << std::endl; // 调用函数并获取返回值的引用 int& maxRef = getMaximum(a, b); // 修改返回值的引用,将 b 的值修改为 30 maxRef = 30; std::cout << "After modification, a = " << a << ", b = " << b << std::endl; return 0; }
当函数需要返回一个对象,并且该对象是函数内部创建的局部变量时,可以使用引用返回。在上面的示例中,
getMaximum
函数返回两个整型参数num1
和num2
中较大值的引用。在main
函数中,我们调用getMaximum
函数并将返回值的引用赋给maxRef
,然后可以通过maxRef
修改原始对象a
和b
的值。在输出结果中,我们可以看到b
的值被修改为 30,这是通过返回值的引用对原始对象进行修改所导致的。
-
操作符重载:
- 在操作符重载中,通常使用引用作为参数和返回值,因为引用可以直接操作原始对象,而不需要通过解引用操作符(
*
)。#include <iostream> // 定义一个简单的向量类 class Vector2D { private: double x; double y; public: // 构造函数 Vector2D(double x = 0.0, double y = 0.0) : x(x), y(y) {} // 操作符重载,将两个向量相加 Vector2D& operator+=(const Vector2D& other) { x += other.x; y += other.y; return *this; // 返回自身的引用 } // 操作符重载,将两个向量相加 friend Vector2D operator+(Vector2D lhs, const Vector2D& rhs) { lhs += rhs; // 使用前面的操作符重载 return lhs; } // 输出向量的坐标 friend std::ostream& operator<<(std::ostream& out, const Vector2D& vec) { out << "(" << vec.x << ", " << vec.y << ")"; return out; } }; int main() { Vector2D v1(1.0, 2.0); Vector2D v2(3.0, 4.0); std::cout << "v1: " << v1 << std::endl; std::cout << "v2: " << v2 << std::endl; Vector2D sum = v1 + v2; // 使用操作符重载进行向量相加 std::cout << "Sum: " << sum << std::endl; return 0; }
在上面的示例中,我们定义了一个简单的二维向量类
Vector2D
,并对加法运算符+
进行了重载。在重载中,我们使用了引用作为参数和返回值,以便能够直接修改原始对象或者返回新的对象的引用。通过引用,我们可以避免不必要的对象拷贝,提高了效率。在
main
函数中,我们创建了两个向量v1
和v2
,并对它们进行了相加操作。使用操作符重载后,我们可以直接对向量进行相加操作,并获得相加后的结果。
- 在操作符重载中,通常使用引用作为参数和返回值,因为引用可以直接操作原始对象,而不需要通过解引用操作符(
总的来说,引用通常更适合在不需要重新绑定对象或者不需要指向空对象的情况下使用,因为引用的语法更简洁明了,而且更安全。而指针则更适合在需要动态分配内存、需要指向空对象、或者需要在运行时重新绑定对象的情况下使用。
引用和指针的其他区别
1) 引用必须在定义时初始化,并且以后也要从一而终,不能再指向其他数据;而指针没有这个限制,指针在定义时不必赋值,以后也能指向任意数据。
2) 可以有 const 指针,但是没有 const 引用。也就是说,引用变量不能定义为下面的形式:
int a = 20;
int & const r = a;
因为 r 本来就不能改变指向,加上 const 是多此一举。
3) 指针可以有多级,但是引用只能有一级,例如,int **p
是合法的,而int &&r
是不合法的。如果希望定义一个引用变量来指代另外一个引用变量,那么也只需要加一个&
,如下所示:
int a = 10;
int &r = a;
int &rr = r;
4) 指针和引用的自增(++)自减(--)运算意义不一样。对指针使用 ++ 表示指向下一份数据,对引用使用 ++ 表示它所指代的数据本身加 1;自减(--)也是类似的道理。请看下面的例子:
#include <iostream>
using namespace std;
int main (){
int a = 10;
int &r = a;
r++;
cout<<r<<endl;
int arr[2] = { 27, 84 };
int *p = arr;
p++;
cout<<*p<<endl;
return 0;
}
运行结果:
11
84
编译器会为const引用创建临时变量
引用不能绑定到临时数据,这在大多数情况下是正确的,但是当使用 const 关键字对引用加以限定后,引用就可以绑定到临时数据了。例如:
typedef struct{
int a;
int b;
} S;
int func_int(){
int n = 100;
return n;
}
S func_s(){
S a;
a.a = 100;
a.b = 200;
return a;
}
S operator+(const S &A, const S &B){
S C;
C.a = A.a + B.a;
C.b = A.b + B.b;
return C;
}
int main(){
int m = 100, n = 36;
const int &r1 = m + n;
const int &r2 = m + 28;
const int &r3 = 12 * 3;
const int &r4 = 50;
const int &r5 = func_int();
S s1 = {23, 45};
S s2 = {90, 75};
const S &r6 = func_s();
const S &r7 = s1 + s2;
return 0;
}
这段代码在 GCC 和 Visual C++ 下都能够编译通过,这是因为将常引用绑定到临时数据时,编译器采取了一种妥协机制:编译器会为临时数据创建一个新的、无名的临时变量,并将临时数据放入该临时变量中,然后再将引用绑定到该临时变量。注意,临时变量也是变量,所有的变量都会被分配内存。
为什么编译器为常引用创建临时变量是合理的,而为普通引用创建临时变量就不合理呢?
1) 我们知道,将引用绑定到一份数据后,就可以通过引用对这份数据进行操作了,包括读取和写入(修改);尤其是写入操作,会改变数据的值。而临时数据往往无法寻址,是不能写入的,即使为临时数据创建了一个临时变量,那么修改的也仅仅是临时变量里面的数据,不会影响原来的数据,这样就使得引用所绑定到的数据和原来的数据不能同步更新,最终产生了两份不同的数据,失去了引用的意义。
总起来说,不管是从“引用的语义”这个角度看,还是从“实际应用的效果”这个角度看,为普通引用创建临时变量都没有任何意义,所以编译器不会这么做。
2) const 引用和普通引用不一样,我们只能通过 const 引用读取数据的值,而不能修改它的值,所以不用考虑同步更新的问题,也不会产生两份不同的数据,为 const 引用创建临时变量反而会使得引用更加灵活和通用。
bool isOdd(const int &n){ //改为常引用
if(n/2 == 0){
return false;
}else{
return true;
}
}
int a = 100;
isOdd(a); //正确 编译器不会创建临时变量,会直接绑定到变量 a
isOdd(a + 9); //正确
isOdd(27); //正确
isOdd(23 + 55); //正确
对于最后三行,编译器会创建临时变量来存储临时数据。也就是说,编译器只有在必要时才会创建临时变量。