目录
一、引用的基本概念与特性
1.定义与声明
2.特性
二、引用的进阶用法
1.函数参数传递:
2.引用作为函数返回值(重点)
引用作为返回值的优点
引用作为返回值的注意事项
代码示例
注意事项的进一步说明
三、传值和传引用效率比较
1.值和引用的作为参数的性能比较
2.值和引用的作为返回值类型的性能比较
四、 引用和指针的区别
在C++编程的广阔天地中,引用是一种强大且独特的工具,它允许程序员为已存在的变量创建别名,通过这个别名可以直接访问和操作原始变量。引用的这一特性不仅简化了代码,提高了代码的可读性,还带来了性能上的优势。接下来,我们将对C++中的引用进行深入剖析,探讨其工作机制、应用场景以及需要注意的事项,并通过具体代码示例进行说明。
一、引用的基本概念与特性
1.定义与声明
- 引用是C++中对某一变量(目标变量)的别名。通过引用,我们可以直接访问和操作原始变量,而无需通过指针的间接访问方式。
- 在C++中,使用
&
符号来声明引用。其基本语法为:
类型标识符& 引用名 = 已存在的变量名;
int main()
{
int a = 10;
int& ra = a;//ra 就是 a 的别名
int& x = a;
int& y = x;
}
2.特性
- 必须初始化:引用在声明时必须被初始化,且一旦初始化后,其引用的对象不能改变。即引用不能重新绑定到另一个变量上。
- 不占内存:从概念上讲,引用本身不占用内存空间,因为它只是原始变量的一个别名。但在底层实现上,编译器通常会将引用实现为指向原始变量的常量指针(
const pointer
),因此实际上会占用指针大小的内存。 - 类型一致:引用的类型必须与它所引用的变量的类型一致。
二、引用的进阶用法
1.函数参数传递:
- 通过引用传递函数参数可以避免数据的复制:做输出型参数,对象比较大时减少拷贝从而提高函数的执行效率。
void swap(int& x, int& y) {
int temp = x;
x = y;
y = temp;
}
int main() {
int a = 5, b = 10;
swap(a, b);
std::cout << "a: " << a << ", b: " << b << std::endl; // 输出a: 10, b: 5
return 0;
}
2.引用作为函数返回值(重点)
引用作为返回值的优点
-
避免拷贝:当函数返回大型对象或容器时,如果直接返回对象本身,会导致对象的拷贝。而返回引用则可以避免这种不必要的拷贝,从而提高效率。
-
允许修改:通过返回引用,调用者可以修改被返回对象的状态。这在某些情况下是非常有用的,比如当你需要实现一个返回容器中某个元素的函数时。
-
支持链式操作:返回引用允许实现链式操作,即连续调用返回引用的成员函数。
引用作为返回值的注意事项
-
确保对象存在:返回引用时,必须确保被引用的对象在函数返回后仍然有效。如果返回的是局部变量的引用,那么当函数结束时,局部变量会被销毁,返回的引用将指向一个无效的内存位置,这会导致未定义行为。
-
避免返回非常量引用:除非有充分的理由,否则应避免返回非常量引用,因为这可能会允许调用者修改被返回对象的状态,从而引入潜在的错误和不可预测的行为。如果确实需要返回可修改引用,应确保调用者明白这一点,并小心处理。
-
考虑使用常量引用:当不需要修改被返回对象时,应优先考虑返回常量引用。这不仅可以保护对象不被修改,还可以提高代码的可读性和安全性。
代码示例
下面是一个返回常量引用的示例,该示例从std::vector
中查找并返回最大元素的引用(作为常量,因为不希望调用者修改它):
#include <iostream>
#include <vector>
#include <algorithm> // for std::max_element
const int& findMax(const std::vector<int>& vec)
{
// 使用std::max_element找到最大元素的迭代器
auto it = std::max_element(vec.begin(), vec.end());
// 返回最大元素的引用(作为常量)
return *it;
}
int main()
{
std::vector<int> nums = { 1, 3, 7, 2, 9, 5 };
const int& maxNum = findMax(nums);
std::cout << "The maximum number is " << maxNum << std::endl;
// 注意:不能修改maxNum,因为它是常量引用
// maxNum = 100; // 这会导致编译错误
return 0;
}
在这个例子中,
findMax
函数返回了一个常量引用,指向vector
中的最大元素。由于返回的是常量引用,调用者不能修改被返回的元素。
注意事项的进一步说明
-
避免返回局部变量的引用:如前所述,这是非常危险的,因为局部变量在函数结束时会被销毁。
-
考虑对象的生命周期:返回引用时,应确保被引用的对象在整个程序运行期间都是有效的。例如,如果函数返回了一个指向堆上分配对象的引用,那么调用者必须负责在适当的时候释放该对象。
-
使用智能指针:在某些情况下,使用智能指针(如
std::shared_ptr
或std::unique_ptr
)作为返回值可能是一个更好的选择。这可以自动管理对象的生命周期,并避免悬垂引用的问题。然而,这也增加了代码的复杂性,并可能引入其他潜在的问题,如循环引用和性能开销。因此,在使用智能指针时应谨慎考虑。
三、传值和传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
1.值和引用的作为参数的性能比较
#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 < 100000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++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;
}
从运行时间可以看出,传引用的效率更高。
2.值和引用的作为返回值类型的性能比较
#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;
}
通过上述代码的比较,我们可以发现传值和指针在作为传参以及返回值类型上效率相差很大。
四、 引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
指针和引用的功能是类似的,重叠的。
C++的引用,对指针使用比较复杂的场景进行一些替换,让代码更简单易懂,但是不能完全替代指针。
引用不能完全替代指针的原因:引用定义后,不能改变指向(在链表的实现中就需要经常改变指向)。
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、定义与基本概念
引用:
引用在C++等编程语言中是一个重要的概念,它相当于某个变量的别名。
引用必须在声明时被初始化,且一旦被初始化后,就不能再改变引用的对象(但对象的值可以改变)。
指针:
指针是一个变量,其存储的是另一个变量的内存地址。
指针可以在任何时候被初始化,且可以随时改变其指向的对象。
2、内存与访问方式
引用:
引用本身不占用内存空间,它只是对象的别名。
对引用的操作实际上是对原对象的直接操作。
指针:
指针本身占用一定的内存空间,用于存储地址信息。
通过指针访问对象时,需要先解引用(即使用“*”操作符),才能访问指针所指向的对象。
3、特性与安全性
引用:
引用不能为空,它必须与合法的存储单元关联。
引用是类型安全的,编译器会对引用进行类型检查。
由于引用不能改变指向的对象,因此它在一定程度上比指针更安全。
指针:
指针可以为空(即指向NULL),也可以指向非法的内存地址(野指针)。
指针的类型安全性不如引用,因为编译器不会对指针进行严格的类型检查。
指针的灵活性更高,但也更容易出错,因此在使用时需要更加小心。
4、使用场景与示例
引用:
引用常用于函数参数传递和返回值,以避免不必要的拷贝和提高效率。
示例:void swap(int &a, int &b),在这个函数中,a和b都是对实参的引用,交换它们的值实际上是在交换实参的值。
指针:
指针常用于动态内存分配、数组操作、链表等数据结构以及函数指针等高级用法。
示例:int *p = new int(10);,这里p是一个指向整数的指针,它指向了一个动态分配的整数对象。
5、汇编层面的实现
从汇编层面来看,引用和指针在实现上有一定的相似性。例如,在C++中,引用在底层通常是通过指针来实现的。但是,编译器会对引用进行特殊的处理,以确保其安全性和类型安全性。因此,尽管引用和指针在汇编层面可能有一定的相似性,但在高级语言层面,它们的使用方式和特性是有显著区别的。
综上所述,引用和指针在编程中各有其独特的特性和使用场景。理解它们的区别和联系对于编写高效、安全的代码至关重要。
如有错误之处,望评论区指正