引用的知识点
1. 引用的声明与初始化
-
声明:
类型 &引用名 = 初始化值;
。引用必须在声明时初始化。 -
演示代码:
#include <iostream> using namespace std; int main() { int num1 = 10; int &ref1 = num1; // 引用声明并初始化 cout << "num1: " << num1 << endl; cout << "ref1: " << ref1 << endl; return 0; }
2. 引用的特性
-
特性:
- 引用声明时必须初始化。
- 引用的绑定关系不可更改(终身制)。
- 访问引用的值实际上是访问引用所引用变量的值。
- 对引用取地址实际是对引用所引用的变量取地址。
-
演示代码:
#include <iostream> using namespace std; int main() { int num1 = 10; int &ref1 = num1; cout << "Address of num1: " << &num1 << endl; cout << "Address of ref1: " << &ref1 << endl; // 地址相同 num1 = 100; cout << "num1: " << num1 << ", ref1: " << ref1 << endl; // 100 ref1 = 200; cout << "num1: " << num1 << ", ref1: " << ref1 << endl; // 200 return 0; }
3. 引用与指针的区别
-
区别:
- 指针可以在声明时不初始化,但引用必须初始化。
- 指针的指向关系可以随时改变,但引用的绑定关系一旦设定不能改变。
-
演示代码:
#include <iostream> using namespace std; int main() { int num1 = 10, num2 = 20; int *p1 = &num1; // 指针 int &ref1 = num1; // 引用 cout << "Address of num1: " << &num1 << endl; cout << "Address of p1: " << &p1 << endl; cout << "Address of ref1: " << &ref1 << endl; // 地址相同 p1 = &num2; // 改变指针的指向 cout << "Pointer p1 now points to: " << *p1 << endl; ref1 = 30; // 修改引用所绑定的变量的值 cout << "num1: " << num1 << endl; // 30 cout << "ref1: " << ref1 << endl; // 30 return 0; }
4. 引用的类型一致性
-
说明: 引用的类型与所引用变量的类型必须一致,不能将非
const
引用初始化为临时值或不同类型的值。 -
演示代码:
#include <iostream> using namespace std; int main() { int num1 = 10; const int &ref4 = 10; // 常量引用可以绑定到临时值 // ref4 = 20; // error: assignment of read-only reference // char &cref = num1; // error: invalid initialization of non-const reference of type 'char&' // int &ref3 = 10; // error: invalid initialization of non-const reference of type 'int&' cout << "ref4: " << ref4 << endl; return 0; }
5. 总结
- 引用 是一个变量的别名,一旦绑定到某个变量后就不能再改变其绑定关系。引用的使用方式与普通变量相同,但在声明时必须初始化,并且不能通过引用修改绑定的对象的类型。
引用与指针的使用区别
1. void
指针与引用
-
void
指针: 可以存在。void*
可以指向任何类型的数据,但需要在使用前转换为具体类型的指针。 -
void
引用: 不存在。C++ 不支持void
类型的引用。 -
演示代码:
#include <iostream> using namespace std; int main() { int num1 = 10; void *pv = &num1; // void &vref = num1; // error: 'void' cannot be used with a reference cout << "Address of num1: " << &num1 << endl; cout << "Address held by void pointer: " << pv << endl; return 0; }
2. 指针引用与引用指针
-
指针引用: 允许声明引用类型的指针,即指向指针的引用。例如
int *&
是一个指向int*
的引用。 -
引用指针: C++ 不允许声明指向引用的指针。例如
int &*
是非法的。 -
演示代码:
#include <iostream> using namespace std; int main() { int num1 = 10, num2 = 20; int *p1 = &num1; int *&pref = p1; // 指针引用 cout << "Address of num1: " << &num1 << endl; cout << "Pointer p1: " << p1 << endl; cout << "Pointer reference pref: " << pref << endl; cout << "Value pointed by p1: " << *p1 << endl; cout << "Value pointed by pref: " << *pref << endl; int &ref = num1; // int &*refp = &ref; // error: cannot declare pointer to 'int&' cout << "-------------" << endl; int arr[2][3] = {{1, 2, 3}, {4, 5, 6}}; int (*parr)[3] = arr; // 数组指针 for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) cout << parr[i][j] << "\t"; cout << endl; } int arr2[3] = {1, 2, 3}; int (&arrref)[3] = arr2; // 数组引用 for (int i = 0; i < 3; i++) cout << arrref[i] << "\t"; cout << endl; return 0; }
3. 总结
- C++ 允许
void*
指针,但不允许void
类型的引用。 - C++ 允许使用指针引用(例如
int *&
),但不允许声明引用指针(例如int &*
)。 - 数组指针和数组引用在 C++ 中的用法:
- 数组指针:
int (*parr)[3] = arr;
用于指向二维数组的行。 - 数组引用:
int (&arrref)[3] = arr2;
用于引用整个数组,保持数组的大小信息。
- 数组指针:
左值引用和右值引用
基本概念
- 左值(Lvalue):可以取地址的表达式,通常是变量、对象、返回左值引用的函数调用等。左值通常有名字,可以在内存中持续一段时间。
- 右值(Rvalue):不能取地址的表达式,通常是字面量、临时对象、返回非引用类型的函数调用等。右值通常没有名字,它们通常是临时的,在表达式结束后就不再存在。
引用概念
- 引用:在C++中,引用是一种特殊类型的别名,它绑定到一个已经存在的对象上。引用本身不占存储空间,它只是它所引用对象的一个别名。
左值引用
- 绑定规则:左值引用只能绑定到左值上。这意味着,左值引用提供了一个左值的别名。
- 生命周期:左值引用不会改变其所引用对象的生命周期。
- 用途:左值引用常用于函数参数传递,允许函数修改调用者的变量,而无需复制。
右值引用
- 绑定规则:右值引用只能绑定到右值上。C++11通过添加
&&
语法来表示右值引用。 - 生命周期:右值引用可以延长所引用的右值的生命周期,使其在引用的作用域内保持有效。
- 用途:右值引用主要用于实现移动语义,允许资源的所有权从一个对象转移到另一个对象,而不需要进行复制。
左值引用与右值引用的区别
- 绑定对象类型:左值引用绑定到左值,右值引用绑定到右值。
- 生命周期:左值引用不会改变其所引用对象的生命周期,而右值引用可以延长右值的生命周期。
- 重绑定能力:左值引用一旦绑定后不能更改,右值引用在初始化后也不能重新绑定。
- 用途:左值引用常用于参数传递和函数返回,右值引用主要用于实现移动语义,优化资源管理。
使用场景
- 当我们需要复制对象时,使用左值引用。
- 当我们需要移动对象以避免不必要的复制时,使用右值引用。
int func() {
return 5; // 返回的是右值
}
int main() {
int &lref = 10; // 错误,不能将左值引用绑定到右值
int &&rref = 10; // 正确,将右值引用绑定到右值
int a = 20;
int &lref2 = a; // 正确,将左值引用绑定到左值
// int &&rref2 = a; // 错误,不能将右值引用绑定到左值
return 0;
}
移动语义
右值引用的一个关键应用是支持移动语义,这在资源管理(如动态内存分配)中非常有用:
#include <iostream>
#include <utility>
class MyClass {
public:
MyClass() {
std::cout << "Constructor called" << std::endl;
}
MyClass(const MyClass& other) {
std::cout << "Copy constructor called" << std::endl;
}
MyClass(MyClass&& other) noexcept {
std::cout << "Move constructor called" << std::endl;
}
};
MyClass createObject() {
MyClass obj;
return obj; // 返回一个临时对象,是一个右值
}
int main() {
MyClass obj = createObject(); // 调用移动构造函数
return 0;
}
在这个例子中,createObject
函数返回一个临时对象,它是一个右值。当这个右值用于初始化obj
时,会调用移动构造函数而不是复制构造函数,这避免了不必要的复制,提高了性能。
std::move
std::move
是 C++11 引入的一个标准库函数,用于实现移动语义和完美转发。它的主要作用是将对象的资源“移动”到另一个对象,而不是复制它们,这对于提高程序性能尤为重要。
1. std::move
的基本概念
- 移动语义: 在传统的复制操作中,对象的资源(如动态内存、文件句柄等)会被复制给另一个对象,导致资源占用增加。而移动语义允许将资源的所有权转移给另一个对象,从而避免不必要的资源复制和释放。
- 左值和右值: 在 C++ 中,左值是指表达式结束后仍然存在的对象,而右值则是表达式结束后即将销毁的临时对象。
std::move
通过将左值强制转换为右值引用,使得可以对一个左值对象进行“移动”操作。
2. std::move
的使用场景
-
移动构造函数: 当一个对象通过
std::move
传递给另一个对象时,触发移动构造函数。移动构造函数将源对象的资源“移走”,而不进行资源复制。class MyClass { public: int* data; MyClass(int size) : data(new int[size]) {} // 移动构造函数 MyClass(MyClass&& other) noexcept : data(other.data) { other.data = nullptr; // 将源对象置为空 } ~MyClass() { delete[] data; } }; MyClass a(10); MyClass b(std::move(a)); // a 的资源被移动到 b
-
移动赋值运算符: 类似于移动构造函数,当对象通过
std::move
赋值给另一个对象时,触发移动赋值运算符。MyClass& operator=(MyClass&& other) noexcept { if (this != &other) { delete[] data; // 释放当前对象的资源 data = other.data; // 移动资源 other.data = nullptr; } return *this; }
-
完美转发: 在模板函数中,可以使用
std::move
将传入的参数完美转发给另一个函数,保持原有的值类别(左值或右值)。template<typename T> void forward(T&& arg) { process(std::forward<T>(arg)); // std::forward 保持 arg 的左值或右值属性 } void process(int& x) { std::cout << "Lvalue process: " << x << std::endl; } void process(int&& x) { std::cout << "Rvalue process: " << x << std::endl; } int main() { int a = 5; forward(a); // 输出 "Lvalue process: 5" forward(10); // 输出 "Rvalue process: 10" }
3. std::move
的实现原理
std::move
本质上并不执行任何移动操作,而是通过将传入对象转换为右值引用来指示该对象的资源可以被移动。它的定义通常如下:
template<typename T>
typename std::remove_reference<T>::type&& move(T&& arg) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(arg);
}
这个模板函数通过 static_cast
将 arg
转换为右值引用,从而使其可以与移动构造函数或移动赋值运算符匹配。
4. std::move
的注意事项
- 使用
std::move
后源对象状态不再可用: 在使用std::move
后,源对象的资源通常会被“移走”,处于无效或空的状态,因此不能再安全地使用该对象。 - 避免不必要的
std::move
: 不要对本身是右值的对象使用std::move
,如函数返回值,因为这样会导致不必要的性能损失。 - 与标准库容器的结合: 许多标准库容器(如
std::vector
,std::string
)在添加元素时,如果使用std::move
,可以避免复制操作,从而提高效率。
5. 示例
以下是一个完整的示例,展示了 std::move
的使用:
#include <iostream>
#include <vector>
#include <string>
int main() {
std::string str = "Hello, World!";
std::vector<std::string> v;
// 使用 std::move 将 str 移动到 vector 中
v.push_back(std::move(str));
std::cout << "After move, str is: " << str << std::endl; // str 可能为空
for (const auto& s : v) {
std::cout << "Vector contains: " << s << std::endl;
}
return 0;
}
万能引用
在模板代码中,T&&
的行为可能会有所不同。如果 T
是一个模板参数,那么 T&&
可以表示左值引用或右值引用,这取决于传入的实参是左值还是右值。在这种情况下,T&&
被称为“万能引用”或“转发引用”。
例子
template<typename T>
void bar(T&& x) {
// x 是一个万能引用
}
int main() {
int a = 10;
bar(a); // T 被推断为 int&,x 是 int& 类型的左值引用
bar(10); // T 被推断为 int,x 是 int&& 类型的右值引用
}
在这个例子中,bar
函数的参数 x
是一个万能引用。如果传入的是左值(如 a
),T
被推导为 int&
,因此 T&&
实际上是 int& &&
,根据引用折叠规则,它最终变为 int&
(即左值引用)。如果传入的是右值(如 10
),T
被推导为 int
,此时 T&&
就是 int&&
(即右值引用)。
引用折叠规则
在模板代码中,T&&
可以根据引用折叠规则进行折叠,具体规则如下:
T& &
折叠为T&
T& &&
折叠为T&
T&& &
折叠为T&
T&& &&
折叠为T&&
万能引用的应用:完美转发(Perfect Forwarding)
万能引用的主要用途是实现完美转发,即将函数参数“完美”地传递给另一个函数,保持参数的左值或右值属性。
template<typename T>
void forward(T&& arg) {
process(std::forward<T>(arg)); // std::forward 保持 arg 的左值或右值属性
}
void process(int& x) {
std::cout << "Lvalue process: " << x << std::endl;
}
void process(int&& x) {
std::cout << "Rvalue process: " << x << std::endl;
}
int main() {
int a = 5;
forward(a); // 输出 "Lvalue process: 5"
forward(10); // 输出 "Rvalue process: 10"
}
在这个例子中,std::forward<T>(arg)
保留了 arg
的值类别,确保在 process
函数中正确地调用相应的重载版本。
引用的本质与指针常量
引用的本质
- 引用在底层实现上类似于指针常量。
- 指针常量声明时必须初始化。
- 不能修改指针常量的指向。
- 编译器在处理引用时,会在访问引用的值时自动解引用。
演示代码
#include <iostream>
using namespace std;
int main() {
int num1 = 10, num2 = 20;
// int *const pc; // 错误:未初始化的常量指针
int *const pc = &num1; // 指针常量声明时必须初始化
// pc = &num2; // 错误:不能修改常量指针的指向
int &ref = num1; // 引用
// 输出引用和指针常量的地址及值
cout << "Address of ref: " << &ref << endl;
cout << "Address of pc: " << &pc << endl;
cout << "Value of ref: " << ref << endl;
cout << "Value of pc: " << *pc << endl;
// 输出引用的大小
cout << "Size of ref: " << sizeof(ref) << endl;
return 0;
}
输出示例
Address of ref: 0x7ffee4b8dabc
Address of pc: 0x7ffee4b8dac0
Value of ref: 10
Value of pc: 10
Size of ref: 4
解释
- 引用的本质:
- 引用在底层实现上类似于指针常量,必须在声明时初始化,且不能更改其绑定关系。
- 编译器在处理引用时,会自动在引用前加上
*
以获取实际值。(解引用)
- 指针常量:
- 指针常量 (
int *const
) 在声明时必须初始化,且不能更改其指向。 - 尝试更改指针常量的指向会导致编译错误。
- 指针常量 (
- 引用与指针常量的比较:
- 引用的大小与普通变量相同,因为编译器在访问引用时会自动处理解引用。
- 指针常量的大小与普通指针相同(通常为4或8个字节,具体取决于系统架构)。
C++ 引用作用
引用的作用及分类
- 引用作为参数:
- 一般类型作参数:
- 优点:声明、实现、调用都简单。
- 缺点:不能修改实参的值,占用更多内存,执行效率低。
- 指针作参数:
- 优点:能修改实参的值,节省内存,执行效率高。
- 缺点:声明、实现、调用稍复杂。
- 引用作参数:
- 优点:能修改实参的值,节省内存,执行效率高,实现和调用简单。
- 缺点:声明稍复杂。
- 一般类型作参数:
void swap_1(int num1, int num2) {
int temp = num1;
num1 = num2;
num2 = temp;
cout << "swap_1() : " << num1 << " " << num2 << endl;
}
void swap_2(int *num1, int *num2) {
int temp = *num1;
*num1 = *num2;
*num2 = temp;
cout << "swap_2() : " << *num1 << " " << *num2 << endl;
}
void swap_3(int &num1, int &num2) {
int temp = num1;
num1 = num2;
num2 = temp;
cout << "swap_3() : " << num1 << " " << num2 << endl;
}
使用引用修改结构体成员
struct Student {
string name;
int age;
};
void setStu(Student &stu, const string &lname, int lage) {
stu.name = lname;
stu.age = lage;
}
void showStu(const Student &stu) {
cout << stu.name << " " << stu.age << endl;
}
int main() {
Student stu1;
showStu(stu1);
setStu(stu1, "zhangsan", 20);
showStu(stu1);
return 0;
}
引用作为返回值
- 返回引用只需拷贝一次,节省内存,效率高。
- 避免返回局部变量的引用:
- 局部变量在函数结束时会被释放,返回其引用相当于返回野指针。
- 解决办法:
- 使用全局变量(不推荐)。
- 使用静态局部变量。
- 使用堆内存。
- 返回输出型参数的引用。
struct Student {
string name;
int age;
};
// 返回一般对象
Student getStu() {
Student stu;
stu.name = "zhangsan";
stu.age = 20;
cout << "getStu(): " << stu.name << " " << stu.age << endl;
return stu;
}
// 返回引用,使用输出型参数
Student& getStu3(Student &stu) {
stu.name = "zhangsan";
stu.age = 20;
cout << "getStu(): &stu = " << &stu << " " << stu.name << " " << stu.age << endl;
return stu;
}
void showStu(const Student &stu) {
cout << stu.name << " " << stu.age << endl;
}
int main() {
Student stu;
Student &stu3 = getStu3(stu);
cout << "&stu3 = " << &stu3 << endl;
showStu(stu3);
return 0;
}
返回类中数据成员的引用
在类中返回数据成员的引用时,通常返回的是成员变量的引用,而不是局部变量的引用。这样可以确保引用在对象的生命周期内有效,避免返回无效的引用。
关键点
- 成员变量的引用:返回类中数据成员的引用时,返回的是类的成员变量。成员变量的生命周期与对象的生命周期相同。
- 对象的生命周期:在对象销毁之前,其成员变量不会被销毁。因此,返回成员变量的引用是安全的。
- 避免局部变量的引用:不要返回函数内定义的局部变量的引用,因为局部变量在函数结束时会被销毁。
class Student {
public:
string name;
int age;
string& getName() {
return this->name;
}
};
int main() {
Student stu;
stu.name = "zhangsan";
stu.age = 20;
string temp = stu.getName();
cout << temp << endl;
return 0;
}
总结
- 引用与指针各有优缺点,选择合适的方式可以提高程序效率。
- 避免返回局部变量的引用否则会导致未定义行为。
- 使用引用作为返回值可以节省内存,提高效率,但需注意引用的生命周期。