学习中发现了delete ptr和prt=nullptr的区别,遂作此篇。
delete
作用:delete 操作会释放指针所指向的内存,从而使这块内存可以被重新分配和使用。
后果:如果继续使用已删除的指针(悬空指针),会导致未定义行为,可能会引发崩溃或错误。删除指针后,它仍然指向原来的内存地址(但该地址已经被释放)。
将指针设为 nullptr
作用:将指针设为 nullptr 只是改变指针的值,使其不再指向任何有效的内存地址。
后果:nullptr 是一种特殊的指针值,表示指针不指向任何有效对象。将指针设为 nullptr 后,使用该指针是安全的,因为对 nullptr 的解引用操作通常会被检测到并处理(例如,抛出异常或引发崩溃)。
画个图
接下来是学习的智能指针部分的代码
auto_ptr1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
template <typename T>
class AutoPtr
{
public:
AutoPtr() :m_data(nullptr) {}
AutoPtr(T* data) : m_data(data) {}
//一个AutoPtr对象在任何时刻都拥有唯一一个资源.当AutoPtr对象被拷贝时,资源的所有权从源对象转移到目标对象
AutoPtr(AutoPtr& other): m_data(other.release()){}
~AutoPtr()
{
if (m_data != nullptr)
{
delete m_data;
m_data = nullptr;
}
}
T* get()
{
return m_data;
}
//将当前对象所管理的资源(指针)释放给调用者,同时将当前对象的指针置为 nullptr,以避免双重删除
T* release()
{
auto data = m_data;
m_data = nullptr;//注意避雷:不能直接删除,如果删除会导致返回一个无效指针
return data;
}
void reset(T* data = nullptr)
{
if (m_data != data)
{
delete m_data;
m_data = data;
}
}
T* operator -> ()
{
return m_data;
}
T& operator * ()
{
return *m_data;
}
AutoPtr& operator = (AutoPtr<T>& other)
{
if (this == &other)
{
return *this;
}
m_data = other.release();
return *this;
}
private:
T* m_data;
};
移动构造函数
在C++中,移动构造函数是一种特殊的构造函数,用于从另一个对象“移动”资源,而不是复制资源。移动构造函数通过接受一个右值引用(T&&)参数,将源对象的资源转移到目标对象,从而避免了昂贵的资源复制操作,提高了程序的性能。
用途:优化性能:通过转移资源而不是复制资源,减少不必要的内存分配和数据复制。
实现移动语义:配合右值引用,实现对象的高效转移,特别是在容器类(如std::vector、std::list)中。
移动构造函数的定义和实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class MyClass {
private:
int* data;
public:
// 默认构造函数
MyClass() : data(new int[10]) {
std::cout << "Default constructor called\n";
}
// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr; // 将源对象的指针置为空,防止资源重复释放
std::cout << "Move constructor called\n";
}
// 析构函数
~MyClass() {
delete[] data;
std::cout << "Destructor called\n";
}
// 其他成员函数...
};
int main() {
MyClass obj1; // 调用默认构造函数
MyClass obj2 = std::move(obj1); // 调用移动构造函数
return 0;
}
接受一个右值引用参数 MyClass&& other。
将other对象的资源转移到当前对象。
将other的指针置为 nullptr,以防止在 other 被销毁时重复释放资源。
输出一条消息,表示移动构造函数被调用。
移动构造函数的几个特点:
1、函数名和类名相同,没有返回值,因为它也是构造函数的一种;
2、第一个参数必须是一个自身类类型的右值引用(&&),且其他参数都有默认值。
3、第一个参数不能声明为 const 右值引用的原因是该引用在函数内会被修改(移动资源)。
4、移动构造函数执行后,需要保证右值引用的对象能够被正常销毁。
noexcept 关键字
在移动构造函数的定义中,通常会添加 noexcept 关键字,表示这个函数不会抛出异常。这样可以让标准库容器(如std::vector)在进行移动操作时优化性能。
右值引用(Rvalue Reference)和 std::move
为了触发移动构造函数,我们通常使用 std::move 将一个左值转换为右值引用。std::move 实际上不移动任何东西,它只是将左值强制转换为右值引用,以便调用移动构造函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class MyClass {
private:
int* data;
public:
// 默认构造函数
MyClass() : data(new int[10]) {
std::cout << "Default constructor called\n";
}
// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr;
std::cout << "Move constructor called\n";
}
// 析构函数
~MyClass() {
delete[] data;
std::cout << "Destructor called\n";
}
// 其他成员函数...
};
int main() {
std::vector<MyClass> vec;
vec.push_back(MyClass()); // 调用默认构造函数和移动构造函数
return 0;
}
在上面的示例中,push_back 调用 MyClass 的默认构造函数创建一个临时对象,然后调用移动构造函数将临时对象移动到 std::vector 中,而不是复制临时对象。这大大提高了效率。
通过引入移动构造函数,我们可以实现高效的资源管理和转移,从而优化程序性能。
什么是左值,右值,右值引用?
左值:左值:本质上是可以操作的一块内存区域,一般可以通过&取到该内存起始地址,这块内存的值可以被修(const对象除外)。除了const对象的其他左值都可以出现在赋值语句左边。
有点抽象?
举个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23int main()
{
int i = 10;
i = 1; // i是变量(对象),左值
const int ci = 5;
//ci = 6; // ci是const变量,是左值但不能出现在赋值号左边
int *pi = &i;
*pi = 1; // *pi 对指针解引用,左值
int arr[5] = {0,};
arr[0] = 1; // arr[0] 是数组元素,左值
struct {
int m_a;
int m_b;
} st, *pst=&st;
st.m_a = 1; // 结构体成员,左值
pst->m_b = 2;
return 0;
}
右值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26int get100()
{
return 100;
}
int main()
{
int i = 0, c=0;
i = 42; // 字面值42,右值
c = 'a'; // 字面值'a',右值
i = 1+2; // 算术运算符的求值结果,右值
i = (c!='a'); // 逻辑运算符的求值结果,右值
i = (c >= 'a'); // 关系运算符的求值结果,右值
i = get100(); // 函数的非引用返回值,右值
i>0?i:c = i>0?1:0; // 条件运算符的两个表达式都是左值,则可以做左值,本例的 i>0?i:c
// 条件运算符的两个表达式都是右值,则为右值,本例的 i>0?1:0;
i = c; // c是左值,左值的值可以当右值使用
return 0;
}
右值引用
右值引用其实就是给右值起了个别名,右值引用只能绑定到一个右值,右值要么是字面常量,要么是在表达式求值过程中创建的临时对象;
临时对象有两个特点:
1、该对象将要被销毁;
2、该对象没有其他用户再使用它。
这就意味着,右值引用的代码是最后使用这个对象的了,可以自由地接管所引用的对象的资源。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using namespace std;
int get100()
{
return 100;
}
void fun(int &&rri)// 右值引用作为函数形参
{
rri = 0;
}
int main()
{
// 1、右值引用必须被初始化为右值
int &&ri0 = 42; // 将 42 存到一个临时量,然后引用这个临时量
int &&ri1 = 'a'; // 将 'a' 存到一个临时量,然后引用这个临时量
int &&ri2 = 1+2; // 将 1+2 存到一个临时量,然后引用这个临时量
int &&ri3 = (ri1 != 'a');
int &&ri4 = (ri1 >= 'a');
int &&ri5 = get100(); // 函数的非引用返回值,右值
int &&ri6 = ri0>0?1:0;
// 2、右值引用的内容可以被修改
ri0 = 1;
cout << "ri0=" << ri0 << endl;
// 3、虽然没办法获取右值的地址,但可以获取右值引用的地址,并改变该地址的值
int *pi = &ri0;
*pi = 2;
cout << "pi=" << pi << ", *pi=" << *pi << ", ri0=" << ri0 << endl;
// 4、传入右值
fun(1+2);
return 0;
}
std::move()
虽然不能将一个右值引用直接绑定到一个左值上, 但我们可以显式地将一个左值转换为对应的右值引用类型。通过调用 std::move 的标准库函数可以获取绑定到左值上的右值引用,此函数定义在 utility 头文件中。1
2
3int i = 0;
int &&rri = std::move(i);
\\std::move 告诉编译器,我们有一个左值,但希望像右值一样处理它。