包含指针成员的类要特别注意复制控制的行为,默认行为种,复制指针只是复制指针地址,并不复制指针指向的对象。两个指针指向同一个对象,会有典型的浅拷贝行为,修改没有隔离,悬垂指针。
1.普通指针
普通指针,只复制指针地址,完全和普通指针一样。因为只复制地址,会导致两个对象共享基础对象。
class Pointer {
private:
int* a;
int b;
public:
Pointer(int *sa, int sb): a(sa), b(sb) {}
int get_b() {return b;}
void set_b(int b) {this->b = b;}
int* get_a() {return a;}
void set_a(int* a) {this->a = a;}
int get_a_val() {return *a;}
void set_a_val(int a) {(*this->a) = a;}
std::string str() {
std::ostringstream os;
os << "Pointer(a=" << *a << ",b=" << b << ")" << std::endl;
return os.str();
}
};
我们定义对象p1,从p1复制对象p2,数据成员b是独立的,数据成员a,如果修改p1的数据成员a的值,会导致p2的值也会跟着变化,因为他们指向同一对象
int main() {
int* pi = new int(1);
Pointer p1(pi, 2);
Pointer p2(p1);
// 非指针类型,两个数据是隔离的,p1对b的修改,不影响p2
p1.set_b(3);
std::cout << "p1:" << p1.str() << "p2:" << p2.str() << std::endl;
// 指针类型,因为指向同一个对象,p1对a的修改,p2的值也被改了
p1.set_a_val(100);
std::cout << "p1:" << p1.str() << "p2:" << p2.str() << std::endl;
}
因为指针指向的数据是共享的,给回收指针成员指向的对象造成了难度,同样是创建p1、p2,如果在析构函数里回收指针成员a,那么在p2超出作用域时,p1的指针成员就成了悬垂指针;如果不在析构函数里回收指针,那么在p1、p2都超出作用域里,没人确保指针成员a指向的对象能正确回收。
int* pi = new int(1);
Pointer p1(pi, 2);
{
Pointer p2(p1);
} // p2超出作用范围,会调用析构函数,指针类型数据,默认析构函数之后删除指针,不会删除指针指向的对象
std::cout << "p1:" << p1.str() << std::endl;
2.智能指针
智能指针一定程度上解决了普通指针的回收指针成员指向的对象时遇到的两难问题。智能指针是通过提供一个智能指针类,统计指向基础对象的引用数,提供析构函数,在引用数为0时在析构函数能回收指针指向的对象。
1.引用计数
智能指针类的作用就是将指针和引用计数绑定在一起。要处理的操作主要包括:
- 初始化时,用智能指针类保存实际数据的指针,引用计数置1
- 复制对象时,复制指针地址,将指针指向的智能指针类引用+1
- 赋值操作时,将左操作数的引用计数减1,如果引用计数减为0,回收左操作数的智能指针类数据;复制右操作数的指针,将引用计数+1
- 调用析构函数时,判断引用计数的值,如果为0,删除指针指向的基础对象
智能指针类内部持有数据的指针,保存对基础对象的引用计数,如RefCount类,为免被其他类访问,我们将所有成员设为private,将Pointer作为友元
class RefCount {
friend class Pointer;
private:
int* a;
unsigned count;
RefCount(int* p) : a(p), count(1) {}
~RefCount() {
delete a;
std::cout << "RefCount destruct" << std::endl;
}
};
Pointer类不再直接持有int*指针,改为持有RefCount对象的指针,从int*指针初始时,自动构建RefCount对象,RefCount构造函数将引用数默认为1
Pointer(int *sa, int sb): ref(new RefCount(sa)), b(sb) {}
此外我们需要定义复制构造函数,将ref复制给新对象,引用数+1
Pointer(Pointer &p1): ref(p1.ref), b(p1.b) {
ref->count++;
}
定义赋值操作时,要将左操作数的引用数-1,如果做操作引用数降为0,将ref对象删除;将右操作数赋值给左操作数,引用数+1
Pointer & operator=(Pointer p1) {
if(--ref->count == 0) {
delete ref;
}
ref = p1.ref;
b = p1.b;
ref->count++;
return *this;
}
析构的时候,要将引用计数减1,如果引用数为0时,删除RefCount指针
~Pointer() {
if(--ref.count == 0) {
delete ref;
}
}
至此对Pointer进行构造、复制、赋值、超出作用域析构都会更新引用数,在最后一个对象析构的时候,同时回收RefCount以及他指向的int *。
#include <iostream>
#include <string>
#include <ostream>
#include <sstream>
class RefCount {
friend class Pointer;
private:
int* a;
unsigned count;
RefCount(int* p) : a(p), count(1) {}
~RefCount() {
delete a;
std::cout << "RefCount destruct" << std::endl;
}
};
class Pointer {
private:
RefCount *ref;
int b;
public:
Pointer(int *sa, int sb): ref(new RefCount(sa)), b(sb) {}
Pointer(Pointer& p1) :ref(p1.ref), b(p1.b) {
ref->count++;
}
~Pointer() {
std::cout << "pointer destruct" << std::endl;
if (--ref->count == 0) {
delete ref;
}
}
Pointer& operator=(const Pointer &r) {
std::cout << "operator=" << std::endl;
r.ref->count++;
if (--ref->count == 0)
delete ref;
ref = r.ref;
b = r.b;
return *this;
}
};
int main() {
int* pi = new int(1);
{
Pointer p1(pi, 2);
Pointer p2(p1);
Pointer p3 = p1; // 复制构造函数
p3 = p2; // 赋值操作符
}
}
程序的输出时这样的,构建p1时自动创建RefCount对象的指针,引用计数设置为1;创建p2时,通过复制构造函数,引用计数+1;创建p3时复制构造函数,引用+1;最后使用赋值操作符,做操作引用-1,右操作数引用+1,最后输出见图
3.值型类
对值型类对象的修改会得到一个新的副本,不会修改原对象。复制指针成员的时候,必须复制指针所指向的对象。这样新创建的指针成员指向的基础对象就没有共享,不会相互影响。这样做的坏处是复制对象会消耗一些资源。
class Pointer {
private:
int *a;
int b;
public:
Pointer(int *sa, int sb): a(new int(*sa)), b(sb) {}
Pointer(Pointer& p1) :a(new int(*p1.a)), b(p1.b) {}
~Pointer() {
delete a;
}
Pointer& operator=(const Pointer &r) {
*a = *r.a;
b = r.b;
}
};