内存管理问题要占程序bug的70%,而内存管理问题往往由指针引起的。
正常情况下,我们申请指针并且释放
void Function()
{
Resource *ptr = new Resource; // Resource is a struct or class
// do stuff with ptr here
delete ptr;
}
哪怕我们心心念念着它。种种原因,我们依旧会忘了释放:
- 函数提前返回
#include <iostream>
void someFunction()
{
Resource *ptr = new Resource;
int x;
std::cout << "Enter an integer: ";
std::cin >> x;
if (x == 0)
return; // the function returns early, and ptr won’t be deleted!
// do stuff with ptr here
delete ptr;
}
-
程序抛出了异常
#include <iostream> void someFunction() { Resource *ptr = new Resource; int x; std::cout << "Enter an integer: "; std::cin >> x; if (x == 0) throw 0; // the function returns early, and ptr won’t be deleted! // do stuff with ptr here delete ptr; }如何解决这个问题呢?我们可能需要引如面向对象的思想。
std::unique_ptr, std::weak_ptr, std::shared_ptr
智能指针
由于类有析构函数,当对象出作用域的时候,析构函数就会自动执行,释放其占有的内存。
如果我们在构造函数中申请内存,并在析构函数中delete,那么就可以保证内存能够被正确释放~
#include <iostream>
template<class T>
class Auto_ptr1
{
T* m_ptr;
public:
// 通过构造函数将外
Auto_ptr1(T* ptr=nullptr)
:m_ptr(ptr)
{
}
// The destructor will make sure it gets deallocated
~Auto_ptr1()
{
delete m_ptr;
}
// Overload dereference and operator-> so we can use Auto_ptr1 like m_ptr.
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
};
// A sample class to prove the above works
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
void sayHi() { std::cout << "Hi!\n"; }
};
void Function()
{
Auto_ptr1<Resource> ptr(new Resource()); // ptr now owns the Resource
int x;
std::cout << "Enter an integer: ";
std::cin >> x;
if (x == 0)
return; // the function returns early
// do stuff with ptr here
ptr->sayHi();
}
int main()
{
Function();
return 0;
}
看一下这段程序是如何运行的。


首先,我们新建了一个Resource对象,并把指针作为参数传递给模版类Auto_ptr1的构造函数,从此时起,res变量就拥有了Resource对象。因为res是一个局部变量,作用域是main函数的一对打括号,当出了大括号,res变量就会被销毁。只要Auto_ptr1对象被定义为一个局部变量,不管函数如何结束,都可以保证Resource类被正确的析构。
像Auto_ptr1这种类称为smart pointer,智能指针是一个组合类,它被设计用来管理动态分配的内存,并确保当智能指针对象超出范围时内存被删除。(对应的,内置指针有时被称为"dumb pointer",因为它们不能自己清理)。
因为ptr是一个局部变量,函数结束时会自动调用ptr的析构函数,正常释放掉resource占用的内存。
此时,问题并没有得到完全解决。
因为我们没有提供拷贝构造函数,因此编译器会给我们提供一个默认的拷贝构造函数,这个默认的函数仅做浅拷贝。连同指针全盘复制。
int main()
{
Auto_ptr1<Resource> res1(new Resource());
Auto_ptr1<Resource> res2(res1);
//passByValue(res1); //问题同上
return 0;
}
所以在main函数中,我们用res1来初始化res2之后,res1和res2指向同一个Resource对象。当res2出了作用域时,会释放掉resource对象占用的内存,使res1称为一个野指针,当res1出了作用域时,它会尝试再次释放Resource对象,导致程序崩溃。

有一个办法是我们可以显式定义并且将拷贝构造函数和赋值运算符置为delete。这样从一开始就阻止了任何拷贝,当然也阻止了函数调用时的参数传值。看起来似乎完美解决了问题,但是,如果我们向从一个函数返回Auto_ptr1呢?
❓ generateResource()
{
Resource *r = new Resource();
return Auto_ptr1(r);
}
我们不能返回引用,因为Auto_ptr1是局部变量,出了作用域,就会被销毁掉。返回地址也是一样。看来我们只能通过传值返回了。
另外一个办法是自定义拷贝构造函数和赋值运算符,在这两个函数中进行深拷贝。这种方式至少可以保证不存在多个指针指向同一个资源的问题。但是拷贝是非常耗时的操作(不是我们想要),我们也不想仅仅因为需要从函数返回Auto_ptr而做一些毫无必要的拷贝。
所有的路都堵死了, 还有别的办法吗?
(未完待续...)
关于成员访问运算符重载
T& operator* () const { return *m_ptr; }
T* operator-> () const { return m_ptr; }
类成员访问运算符( -> )可以被重载。它被定义用于为一个类赋予"指针"行为。运算符 -> 必须是一个成员函数。如果使用了 -> 运算符,返回类型必须是指针或者是类的对象。
运算符 -> 通常与指针引用运算符 * 结合使用,用于实现"智能指针"的功能。这些指针是行为与正常指针相似的对象,唯一不同的是,当您通过指针访问对象时,它们会执行其他的任务。比如,当指针销毁时,或者当指针指向另一个对象时,会自动删除对象。
间接引用运算符 -> 可被定义为一个 一元后缀运算符。
给出一个类:
class Ptr{
//...
T * operator-> ()const{return m_ptr};
};
类 Ptr 的对象可用于访问类 X 的成员,使用方式与指针的用法十分相似。例如:
void f(Ptr p )
{
p->m = 10 ; // (p.operator->())->m = 10
}
语句 p->m 被解释为 ( p.operator->() )->m。即 **m_ptr->**m。
同理 * 为一元前缀运算符?
可重载运算符/不可重载运算符
下面是可重载的运算符列表:
| 双目算术运算符 | + (加),-(减),*(乘),/(除),% (取模) |
|---|---|
| 关系运算符 | ==(等于),!= (不等于),< (小于),> (大于),<=(小于等于),>=(大于等于) |
| 逻辑运算符 | ||(逻辑或),&&(逻辑与),!(逻辑非) |
| 单目运算符 | + (正),-(负),*(指针),&(取地址) |
| 自增自减运算符 | ++(自增),--(自减) |
| 位运算符 | | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移) |
| 赋值运算符 | =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>= |
| 空间申请与释放 | new, delete, new[ ] , delete[] |
| 其他运算符 | ()(函数调用),->(成员访问),,(逗号),[](下标) |
下面是不可重载的运算符列表:
- .:成员访问运算符
- .*, ->*:成员指针访问运算符
- :::域运算符
- sizeof:长度运算符
- ?::条件运算符
- #: 预处理符号
评论区