C 编程中,类内使用裸指针是极其常见也是常规用法,但是类内指针使用不当易导致崩溃。
如以下代码定义的对象,在使用时会引发崩溃
代码语言:javascript复制#pragma once
class ShallowCopyWithRawPointer {
private:
int* data;
public:
ShallowCopyWithRawPointer(int value) {
data = new int;
*data = value;
}
ShallowCopyWithRawPointer(const ShallowCopyWithRawPointer& other) {
data = other.data;
}
// 赋值运算符(浅拷贝)
ShallowCopyWithRawPointer& operator=(const ShallowCopyWithRawPointer& other) {
if (this != &other) {
data = other.data;
}
return *this;
}
~ShallowCopyWithRawPointer() {
if (data)
{
delete data;
data = nullptr;
}
}
int getValue() const {
return *data;
}
};
void using_shallow_raw_pointer()
{
ShallowCopyWithRawPointer obj1(100);
ShallowCopyWithRawPointer obj2 = obj1; // 浅拷贝
std::cout << "Object 1 value: " << obj1.getValue() << std::endl;
std::cout << "Object 2 value: " << obj2.getValue() << std::endl;
}
//运行时崩溃
由以上代码可知,在拷贝构造赋值和拷贝复制后,新旧对象内的指针指向同一块内存,如此当新旧对象析构时,会对同一块内存delete两次(delete after free),出现崩溃。
如上的新旧对象内的指针指向同一块内存即拷贝构造和拷贝赋值时执行的浅拷贝。所谓浅拷贝是指将一个对象的值复制到另一个对象,但是对于指向动态分配内存的指针,只是简单地拷贝了指针的值,而不是拷贝指针指向的内容。故两个对象会共享同一块内存空间,一旦其中一个对象修改了内存中的值,另一个对象也会受到影响。
与浅拷贝相对应的为深拷贝,所谓深拷贝是指将一个对象的值以及其指向的动态分配内存中的内容都复制到另一个对象,使得两个对象拥有各自独立的内存空间。这样,即使其中一个对象修改了内存中的值,另一个对象也不会受到影响。
可采用深拷贝优化如上的代码,解决崩溃问题。代码如下:
代码语言:javascript复制#pragma once
#include<iostream>
class DepthCopyWithRawPointer {
private:
int* data;
public:
DepthCopyWithRawPointer(int value) {
data = new int;
*data = value;
}
DepthCopyWithRawPointer(const DepthCopyWithRawPointer& other) {
data = new int(*(other.data));
}
DepthCopyWithRawPointer& operator=(const DepthCopyWithRawPointer& other) {
if (this != &other) {
delete data;
data = new int(*(other.data));
}
return *this;
}
~DepthCopyWithRawPointer() {
if (data)
{
delete data;
data = nullptr;
}
std::cout<<__FUNCTION__<<std::endl;
}
int getValue() const {
return *data;
}
};
由以上代码可以看到,在拷贝复制和拷贝赋值函数内,均申请了新的内存,即新旧对象的指针指向不同的内存。在对象析构时,每个对象析构自身指向的内存,不会导致崩溃。同时,由于指针指向的是两块独立的内存,所以执行深拷贝后,对于指针的修改也是互不影响的。
进一步的,可以在使用裸指针时,禁止拷贝操作,便不会存在新旧对象指向同一块内存,也就不会出现因释放同一块内存导致的崩溃了。代码示例如下:
代码语言:javascript复制#pragma once
class NoCopyWithRawPointer {
private:
int* data;
public:
NoCopyWithRawPointer(int value) {
data = new int;
*data = value;
}
~NoCopyWithRawPointer() {
if (data)
{
delete data;
data = nullptr;
}
}
int getValue() const {
return *data;
}
//禁用所有的赋值和复制
NoCopyWithRawPointer(const NoCopyWithRawPointer& other) =delete;
NoCopyWithRawPointer& operator=(const NoCopyWithRawPointer& other) =delete;
NoCopyWithRawPointer(const NoCopyWithRawPointer&& other) = delete;
NoCopyWithRawPointer&& operator=(const NoCopyWithRawPointer&& other) = delete;
};
进一步的,如果希望两个对象指向同一块内存,可以借助共享指针,类内不再使用裸指针,而是使用共享指针,借助共享指针的引用计数方案,拷贝赋值和拷贝复制时引用计数加一,引用计数为零时释放内存。
示例代码如下:
代码语言:javascript复制#pragma once
#include<memory>
class ShallowCopyWithSharedPointer {
private:
std::shared_ptr<int> data{nullptr};
public:
// 构造函数
ShallowCopyWithSharedPointer(int value) {
data = std::make_shared<int>(value);
}
// 复制构造函数(浅拷贝)
ShallowCopyWithSharedPointer(const ShallowCopyWithSharedPointer& other) {
data = other.data;
}
// 赋值运算符(浅拷贝)
ShallowCopyWithSharedPointer& operator=(const ShallowCopyWithSharedPointer& other) {
if (this != &other) {
data = other.data;
}
return *this;
}
~ShallowCopyWithSharedPointer() {
}
int getValue() const {
return *data;
}
};
进一步的,可以借助引用计数的思想,自己实现一个避免多次释放同一块内存的类,示例代码如下:
代码语言:javascript复制#pragma once
#include<atomic>
#include<iostream>
class ShallowCopyWithUserCount {
private:
int* data;
static std::atomic<int> user_count;
public:
ShallowCopyWithUserCount(int value) {
data = new int;
*data = value;
user_count ;//初始值为0,构造时加一
}
ShallowCopyWithUserCount(const ShallowCopyWithUserCount& other) {
data = other.data;
user_count ;//拷贝复制时加一
}
ShallowCopyWithUserCount& operator=(const ShallowCopyWithUserCount& other) {
if (this != &other) {
data = other.data;
user_count ;////拷贝赋值时加一
}
return *this;
}
~ShallowCopyWithUserCount() {
user_count--;//析构时减一
if (user_count.load() == 0)
{
if (data)
{
delete data;
data = nullptr;
std::cout << " free memory" << std::endl;
}
}
std::cout<<__FUNCTION__<<std::endl;
}
int getValue() const {
return *data;
}
};
std::atomic<int> ShallowCopyWithUserCount::user_count{0};
总结
只要类内存在裸指针,如果只是用浅拷贝会极易导致崩溃,基于此,本文提出了四种解决方案:
- 使用裸指针时,禁止类的拷贝构造、拷贝赋值、移动构造和移动赋值
- 使用裸指针时,使用深拷贝,使得每个对象内部的指针指向不同的内存块
- 类内使用指针时,不再使用裸指针,使用共享指针
- 类内使用裸指针时,基于基于引用计数的思想,赋值/复制时引用计数加一,析构时引用计数减一,当引用计数为零时释放内存。