指针
概述
- 通常所说的“指针”就是指 指针类型的变量,它保存了 内存中另一个变量的地址
- 通过这个指针保存的地址,我们可以
间接访问
指针指向的内存位置的数据 - 这个过程称为 解引用(
dereferencing
)
- 通过这个指针保存的地址,我们可以
示例
*ptr
- 表示 通过指针
ptr
访问其保存的地址所指向的数据 - 这个操作叫做 解引用
1 2 3 4 5 |
int x = 10; // 定义一个整数变量 x,并赋值为 10 int* ptr = &x; // 定义一个指针变量 ptr,保存 x 的地址 std::cout << "x 的值: " << x << std::endl; // 输出 10 std::cout << "ptr 指向的地址中的值: " << *ptr << std::endl; // 通过指针访问 x 的值,输出 10 |
引用
概述
C++
中的一个特性,它为变量创建了一个别名,可以通过这个别名直接操作原来的变量- 引用本质上并不像指针那样保存地址,而是直接绑定到一个已存在的变量
- 任何对引用的操作都会影响到原始变量
示例
- 在
C++
中,引用是通过在类型后面加上&
来声明的- 例如
int& ref
表示一个整型引用 - 引用必须在定义时初始化,并且一旦绑定到某个变量,就不能再更改引用的对象
- 例如
1 2 |
int x = 10; int& ref = x; // ref 是 x 的引用,是 x 的别名 |
局部变量的引用
- 一个函数返回 普通局部变量的引用,那么当函数结束后引用的内存将被释放,从而导致 未定义行为
- 示例:
1 2 3 4 5 6 7 8 9 10 |
int& getLocalValue() { int x = 10; // 局部变量 return x; // 返回局部变量 x 的引用(这是错误的) } int main() { int& ref = getLocalValue(); // ref 引用的是已销毁的 x std::cout << ref << std::endl; // 未定义行为,可能崩溃或输出垃圾值 return 0; } |
- 解决:返回静态变量的引用(需谨慎)
1 2 3 4 5 6 7 8 9 10 |
int& getStaticValue() { static int x = 10; // 静态变量 return x; // 返回静态变量的引用是安全的 } int main() { int& ref = getStaticValue(); std::cout << ref << std::endl; // 输出 10 return 0; } |
- 解决:返回堆上分配的内存(动态内存分配)
1 2 3 4 5 6 7 8 9 10 11 |
int* getHeapValue() { int* x = new int(10); // 在堆上分配内存 return x; // 返回指针 } int main() { int* ptr = getHeapValue(); std::cout << *ptr << std::endl; // 输出 10 delete ptr; // 记得释放堆内存 return 0; } |
- 解决:返回值传递(复制)
1 2 3 4 5 6 7 8 9 10 |
int getValue() { int x = 10; // 局部变量 return x; // 返回值,不是引用 } int main() { int val = getValue(); // 安全,返回的是值 std::cout << val << std::endl; // 输出 10 return 0; } |
- 解决:使用标准库容器(副本)
1 2 3 4 5 6 7 8 9 10 |
std::vector<int> getVector() { std::vector<int> vec = {1, 2, 3}; // 局部变量 return vec; // 返回容器的副本,安全 } int main() { std::vector<int> vec = getVector(); // 返回副本 std::cout << vec[0] << std::endl; // 输出 1 return 0; } |
指针和引用区别
- 引用是别名
- 引用只是一个现有变量的别名,它和原来的变量是同一个实体
- 指针是一个变量,它存储的是另一个变量的地址,需要通过解引用操作符
*
来访问它指向的值
- 引用不需要解引用
- 使用引用时,可以直接像使用原始变量一样操作,不需要解引用操作符
- 指针则需要通过
*
来解引用指向的数据
- 引用必须在定义时初始化
- 引用在声明时就必须绑定到一个变量,并且绑定后不能再改变引用的对象
- 指针可以在任何时候指向不同的变量或地址
- 引用不能为空
- 引用必须绑定到有效的对象,不能指向
nullptr
- 指针可以为空(
nullptr
),表示它不指向任何东西
- 引用必须绑定到有效的对象,不能指向
左值引用
左值
- 指的是内存中有明确存储地址的对象
- 简单来说,左值就是可以取地址的对象
- 在大多数情况下,等号左边的是左值
定义
- 左值引用的语法形式为:
T&
,其中T
是类型,&
表示引用
1 2 |
int x = 10; int& ref = x; // ref 是 x 的左值引用 |
特点
- 引用的别名
- 左值引用是某个左值的别名
- 通过左值引用操作时,实际上是对原对象的操作
- 必须绑定到左值
- 左值引用只能绑定到左值,即必须是具有确定存储位置的对象或变量
- 可以修改引用的值
- 通过左值引用可以修改原始对象的值
主要用途
- 减少拷贝
- 引用可以避免值的拷贝,特别是当传递或返回大型对象时,通过引用传递可以提高效率
- 用于函数参数传递
- 左值引用常用于函数参数传递,允许函数直接操作传入的对象而不是其副本
1 2 3 4 5 6 7 8 9 10 |
void modify(int& ref) { ref = 30; // 通过引用修改实参 } int main() { int x = 10; modify(x); // 传递 x 的引用 std::cout << x << std::endl; // 输出 30,因为 x 被修改了 return 0; } |
- 实现操作符重载
- 左值引用在实现操作符重载时非常有用,可以用来实现链式操作符(如
a = b = c;
)等
- 左值引用在实现操作符重载时非常有用,可以用来实现链式操作符(如
- 引用作为返回值
- 函数可以返回某个对象的左值引用,这样可以允许外部通过引用修改函数内部的对象
右值引用
右值
- 指的是无法取地址的临时值,通常是表达式的结果
概述
- 是
C++11
引入的一个新特性,用于解决与临时对象和资源管理相关的问题 - 它使得程序员能够更高效地操作和管理临时对象
- 尤其是在实现移动语义(
move semantics
)和完美转发(perfect forwarding
)时非常有用
- 尤其是在实现移动语义(
定义
- 右值引用的声明语法是
T&&
,其中T
是类型,&&
表示右值引用 - 右值引用只能绑定到 右值,而左值引用(
T&
)只能绑定到 左值 - 右值引用允许对右值(如临时对象)进行修改
- 这使得我们可以避免不必要的拷贝操作,从而提高程序性能
1 |
int&& rvalue_ref = 10; // 10 是右值,可以绑定到右值引用 rvalue_ref |
用途
- 移动语义
- 传统的
C++
编程中,传递或返回对象通常涉及拷贝操作
拷贝会产生性能开销,特别是对于大对象 - 通过右值引用,我们可以通过移动对象的资源(如动态内存、文件句柄等),而不是拷贝资源,来提高性能
- 移动语义允许我们 "偷" 临时对象的资源,而不必拷贝它
- 传统的
- 避免不必要的拷贝
- 对于临时对象,右值引用可以避免深拷贝,通过移动来减少性能开销
- 实现完美转发(
Perfect Forwarding
)- 完美转发是模板编程中的技术,结合右值引用和
std::forward
可以在函数模板中实现高效的参数传递,无论传递的是左值还是右值
- 完美转发是模板编程中的技术,结合右值引用和
移动的本质
- 移动 是对资源管理的一种思路
- 它的具体实现,由编译支持移动构造函数以及移动赋值操作符等特性
- 还需要由程序员在具体的移动构造函数以及移动赋值操作符里面,完成对具体资源的转移工作
- 所谓所有权转移
- 将原对象的资源(如堆内存的指针)赋值给新对象,并将原对象的资源指针置为
nullptr
或其他无效状态,防止原对象在析构时释放已经转移的资源
- 将原对象的资源(如堆内存的指针)赋值给新对象,并将原对象的资源指针置为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class MyClass { private: int* data; size_t size; public: // 构造函数 MyClass(size_t sz) : size(sz), data(new int[sz]) {} // 移动构造函数 MyClass(MyClass&& other) noexcept : data(nullptr), size(0) { // 将 other 的资源转移到当前对象 data = other.data; // 接管资源 size = other.size; // 将 other 置为无效状态 other.data = nullptr; other.size = 0; } // 析构函数 ~MyClass() { delete[] data; } }; |
对象的引用
- 本质上就是 对象的别名
指针的引用
概述
- 指针的传递是浅拷贝
- 传递一个指针作为函数参数(例如
TreeNode* root
),实际上是对指针 进行拷贝 - 也就是说,形参
root
是传入的指针的 副本,它保存了和实参相同的地址 - 这意味着形参和实参都指向同一个内存地址,是两个不同的指针变量指向同一个内存地址
- 传递一个指针作为函数参数(例如
- 使用
- 因为形参和实参指向的是同一块内存
- 因此在函数内部通过这个指针修改其指向的内存内容,外部是可以看到的
- 问题
- 如果你在函数内部修改指针本身(即让形参指向不同的地址)
- 这个修改只影响形参(即拷贝的副本),不会影响外部的实参指针
- 示例
- 虽然
root
是指针,但函数内部的root
只是实参的副本 - 修改了形参
root
的指向(让它指向一个新的TreeNode
),但外部的实参root
并没有改变
- 虽然
1 2 3 |
void modifyPointer(TreeNode* root) { root = new TreeNode(20); // 只修改了形参 root,实参不受影响 } |
指针的引用
- 如果希望在函数内部 修改传入的指针(即让外部的指针也指向一个新的对象),那么需要使用指针的引用
1 2 3 |
void modifyPointer(TreeNode*& root) { root = new TreeNode(20); // 现在修改了实参 root 的指向 } |
关键点
- 形参和实参是两个不同的变量
- 如果这两个指针变量指向的是同一个地址,那么通过这个地址去修改数据,是可以的
- 但是修改保存地址的变量(形参)的内容,并不会同步修改到外面的实参
其他
移动构造函数
- 使用
std::move
将vec
变成右值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <iostream> #include <vector> class MyClass { public: std::vector<int> data; // 移动构造函数 MyClass(std::vector<int>&& d) : data(std::move(d)) { std::cout << "Move constructor called" << std::endl; } }; int main() { std::vector<int> vec = {1, 2, 3, 4}; MyClass obj(std::move(vec)); // vec 是右值,通过 std::move 传递 // 此时 vec 变为空,资源被 "移动" 到 obj 中 std::cout << "vec size: " << vec.size() << std::endl; std::cout << "obj size: " << obj.data.size() << std::endl; return 0; } |
- 关于移动的本质,见上文
移动赋值操作符
1 2 3 4 5 6 7 8 9 10 11 |
class MyClass { public: std::vector<int> data; // 移动赋值操作符 MyClass& operator=(std::vector<int>&& d) { data = std::move(d); // 资源转移 std::cout << "Move assignment operator called" << std::endl; return *this; } }; |
std::move
1 2 3 4 |
_EXPORT_STD template <class _Ty> _NODISCARD _MSVC_INTRINSIC constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { return static_cast<remove_reference_t<_Ty>&&>(_Arg); } |
- 它将对象“转换”成右值,使得我们可以对它进行移动操作
std::move
不会移动对象的内容,它只是将对象转换为右值,从而允许移动构造函数或移动赋值操作符来“移动”对象的资源
1 2 |
std::vector<int> vec = {1, 2, 3, 4}; std::vector<int> new_vec = std::move(vec); // 将 vec 转换成右值,资源转移到 new_vec |
- 不要滥用
std::move
- 虽然
std::move
可以将左值转换为右值,但要小心使用它,因为移动之后,原来的对象可能处于无效或不完整的状态
- 虽然
std::forward
1 2 3 4 |
_EXPORT_STD template <class _Ty> _NODISCARD _MSVC_INTRINSIC constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept { return static_cast<_Ty&&>(_Arg); } |
- 概述
- 主要用于实现 完美转发(
perfect forwarding
) - 完美转发允许在模板函数中保持传入参数的“值类别”(即保持参数是左值还是右值的性质),并将其完美地传递给另一个函数
- 主要用于实现 完美转发(
- 使用场景
- 典型使用场景是 转发参数到另一个函数,尤其是在 函数模板 中
- 在函数模板中,
std::forward
可以根据模板参数的类型,将左值保持为左值,将右值保持为右值,实现 完美转发
- 为什么需要
std::forward
- 在模板编程中,我们常常需要将参数传递给另一个函数
- 例如,在构造对象或调用其他函数时,你希望根据调用者传入的参数类型,保持其左值或右值属性
- 然而,简单的传递参数(不使用
std::forward
或std::move
)会导致值类别的丢失——右值会变为左值,无法再高效地使用移动语义
std::forward
和 std::move
区别
std::move
- 将任何对象转换为右值引用,告诉编译器该对象可以被移动(即使传入的是左值)
std::forward
- 根据传入参数的类型和值类别,条件性地将参数转发为左值或右值
- 它只在 模板代码中 使用,以保持参数的原始值类别
完美转发
- 使用
std::forward
实现完美转发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> #include <utility> // std::forward void process(int& x) { std::cout << "Lvalue processed: " << x << std::endl; } void process(int&& x) { std::cout << "Rvalue processed: " << x << std::endl; } template <typename T> void forward_example(T&& arg) { process(std::forward<T>(arg)); // 使用 std::forward 保持原值类别 } int main() { int a = 10; forward_example(a); // 传入左值,输出 "Lvalue processed" forward_example(20); // 传入右值,输出 "Rvalue processed" return 0; } |
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ macOs 解析mach-o05/11
- ♥ C++并发编程_同步并发(Condition_variable)05/21
- ♥ 51CTO:C++语言高级课程三08/15
- ♥ C++标准模板库编程实战_序列容器12/06
- ♥ C++_运算符优先级&&相关性12/15
- ♥ C++并发编程 _ 无锁数据结构09/18