一、什么是指针
在 C++ 中,指针是一种变量类型,它存储的是内存地址而不是实际的数据值。
指针的基本概念
声明指针:
- 指针变量必须声明其类型,这决定了它可以指向哪种类型的变量。
- 例如,
int* p;
声明了一个指向整型的指针p
。初始化指针:
- 指针可以被初始化为
nullptr
(C++11 及以后版本)或NULL
(C++98 和 C++03)。- 也可以指向一个已存在的变量的地址。
- 例如,
int x = 10; int* p = &x;
。解引用指针:
- 使用
*
运算符访问指针指向的内存位置的值。- 例如,
int value = *p;
将p
指向的整数值赋给value
。指针运算:
- 可以对指针进行加减运算,以指向数组中的不同元素。
- 例如,
int arr[10]; int* p = arr; p++;
使p
指向数组中的下一个元素。指针和数组:
- 在 C++ 中,数组名实际上是一个指向数组第一个元素的常量指针。
- 例如,
int arr[10]; int* p = arr;
使p
指向arr
的第一个元素。
二、什么是引用
在 C++ 中,引用(Reference)是一种特殊类型的变量,它充当另一个变量的别名。引用在声明时必须被初始化,并且一旦初始化后就不能改变引用的对象。引用通常用于传递大对象作为函数参数,返回多个值,或者作为类成员的替代品。下面详细介绍引用的概念和用法。
引用的基本概念
声明引用:
- 引用声明语法类似于指针,但不需要使用星号
*
。- 例如,
int& ref = x;
声明了一个引用ref
,它是整型变量x
的别名。初始化引用:
- 引用在声明时必须被初始化,并且不能指向
nullptr
。- 例如,
int x = 10; int& ref = x;
。解引用引用:
- 不需要使用
*
运算符来解引用引用,可以直接使用引用名。- 例如,
ref = 20;
直接修改了x
的值。引用和指针的区别:
- 引用不是真正的变量,而是一个别名。
- 一旦引用被初始化,就不能改变它所引用的对象。
- 引用不能被赋值为
nullptr
,而指针可以。
引用和指针做函数参数:
#include <iostream>
// 使用指针交换两个整数
void swapPtr(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
// 使用引用交换两个整数
void swapRef(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 10;
int y = 20;
// 使用指针交换
std::cout << "Before swap with pointers: x = " << x << ", y = " << y << std::endl;
swapPtr(&x, &y);
std::cout << "After swap with pointers: x = " << x << ", y = " << y << std::endl;
// 使用引用交换
x = 10; // 重置 x 和 y
y = 20;
std::cout << "Before swap with references: x = " << x << ", y = " << y << std::endl;
swapRef(x, y);
std::cout << "After swap with references: x = " << x << ", y = " << y << std::endl;
return 0;
}
Before swap with pointers: x = 10, y = 20
After swap with pointers: x = 20, y = 10
Before swap with references: x = 10, y = 20
After swap with references: x = 20, y = 10
指针作为返回值 vs.引用作为返回值 :
#include <iostream>
// 使用指针返回数组的最大值
int* findMaxPtr(int arr[], int size) {
int* maxPtr = &arr[0];
for (int i = 1; i < size; ++i) {
if (arr[i] > *maxPtr) {
maxPtr = &arr[i];
}
}
return maxPtr;
}
// 使用引用返回数组的最大值
int& findMaxRef(int arr[], int size) {
int& maxRef = arr[0];
for (int i = 1; i < size; ++i) {
if (arr[i] > maxRef) {
maxRef = arr[i];
}
}
return maxRef;
}
int main() {
int arr[] = {10, 20, 5, 15, 25};
int size = sizeof(arr) / sizeof(arr[0]);
// 使用指针返回最大值
int* maxPtr = findMaxPtr(arr, size);
std::cout << "Max value with pointer: " << *maxPtr << std::endl;
// 使用引用返回最大值
int& maxRef = findMaxRef(arr, size);
std::cout << "Max value with reference: " << maxRef << std::endl;
return 0;
}
Max value with pointer: 25
Max value with reference: 25
三、什么是右值
在 C++ 中,右值(R-value)是指表达式或对象在赋值操作中的右侧部分。右值通常代表一个临时值或一个即将消亡的对象。右值可以分为两种主要类型:普通右值和将逝右值(Xvalue)。
(一)右值的分类
普通右值:
- 普通右值通常指的是那些不能被取地址的对象。
- 这包括字面量(如
10
,"hello"
)、临时对象(如函数返回值或表达式的结果)和将要消亡的对象。将逝右值(Xvalue):
- 将逝右值是一种特殊的右值,表示一个即将消亡的对象,可以被移动。
- 在 C++11 中引入了移动语义,允许将逝右值被移动而非复制,以提高效率。
- 将逝右值可以出现在移动赋值操作符的左侧。
(二)右值示例
#include <iostream>
#include <string>
// 一个简单的类
class MyClass {
public:
MyClass() : value(0) {}
MyClass(int v) : value(v) {}
MyClass(const MyClass&) { std::cout << "Copy constructor called." << std::endl; }
MyClass(MyClass&& other) noexcept : value(other.value) {
std::cout << "Move constructor called." << std::endl;
other.value = 0; // 清空原对象
}
MyClass& operator=(MyClass&& other) noexcept {
std::cout << "Move assignment operator called." << std::endl;
value = other.value;
other.value = 0; // 清空原对象
return *this;
}
int value;
};
int main() {
// 普通右值
MyClass x = 10; // 使用字面量初始化
MyClass y = MyClass(20); // 使用临时对象初始化
// 将逝右值
MyClass z = std::move(x); // 使用 std::move 将 x 移动到 z
MyClass w = std::move(y); // 使用 std::move 将 y 移动到 w
// 输出结果
std::cout << "x.value: " << x.value << std::endl;
std::cout << "y.value: " << y.value << std::endl;
std::cout << "z.value: " << z.value << std::endl;
std::cout << "w.value: " << w.value << std::endl;
return 0;
}
Copy constructor called.
Move constructor called.
Move assignment operator called.
x.value: 0
y.value: 0
z.value: 10
w.value: 20
普通右值:
x = 10;
中的10
是一个普通右值。y = MyClass(20);
中的MyClass(20)
是一个普通右值。- 普通右值通常代表一个临时值或即将消亡的对象。
将逝右值:
z = std::move(x);
中的std::move(x)
是一个将逝右值。w = std::move(y);
中的std::move(y)
是一个将逝右值。- 将逝右值可以被移动而非复制,以提高效率。
四、右值引用是什么
右值引用是为一个临时变量取别名,它只能绑定到一个临时变量或表达式(将亡值)上。实际开发中我们可能需要对右值进行修改(实现移动语义时就需要)而右值引用可以对右值进行修改。
为什么:1.移动语义;2.完美转发;3.拓展可变参数模板,实现更加灵活的模板编程。
1.移动语义
右值引用可以绑定到临时对象、表达式等右值上,这些右值在生命周期结束后就会被销毁,因此可以在右值引用中窃取其资源,从而避免昂贵的复制操作,实现高效的移动语义。
#include <iostream>
#include <vector>
#include <string>
class Resource {
public:
Resource(int size) { std::cout << "Resource created with size " << size << "\n"; }
Resource(Resource&& other) noexcept { std::cout << "Resource moved\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
void processResource(Resource&& r) {
// 处理资源
std::cout << "Resource is being processed\n";
}
int main() {
Resource r(10); // 创建一个 Resource 对象
processResource(std::move(r)); // 将 r 转换为右值引用并传递给函数
// 此时 r 已经是一个无效对象,因为它的资源已经被移动走了
2.完美转发
右值引用可以绑定到任何类型的右值上,可以将其作为参数传递给函数,并在函数内部将其“转发”到其他函数中,从而实现完美转发。
#include <iostream>
#include <utility> // For std::forward
// 模拟一个可以接收左值引用和右值引用的函数
void process(int& x) {
std::cout << "process(int&) - Lvalue reference" << std::endl;
}
void process(int&& x) {
std::cout << "process(int&&) - Rvalue reference" << std::endl;
}
// 模板函数,使用完美转发将参数转发给process函数
template<typename T>
void wrapper(T&& arg) {
// 使用std::forward来转发参数,保持其原始的类型和值类别
process(std::forward<T>(arg));
}
int main() {
int a = 10;
wrapper(a); // 编译器将调用 process(int&),因为a是左值
wrapper(10); // 编译器将调用 process(int&&),因为10是临时对象(右值)
}