C++如何设计一个类2(含指针的类)
C++如何设计一个类2(含指针的类)
本文预览:
- BigThree:拷贝构造、拷贝复制、析构
- Stack(栈) Heap(堆)及生命周期
- new delete 操作符内部实现
- 物理内存模型 in VC
BigThree:拷贝构造、拷贝复制、析构
带有指针的类必须要有拷贝构造、拷贝复制
C++ STL中String的实现是典型的带指针类,成员只有一根char类型的指针,为什么不用char类型的数组,这个考量在于,我们不能确定用户创建的字符串到底有多少个字符,数组在分配内存空间的时候必须是确定的,多了造成浪费,少了空间不足,所以数组不适合这样的需求。使用指针的好处在于,我们是动态分配的内存空间,大小可控。
- 接口设计
class String{
public:
String(const char* cstr=0);
String(const String& str); //拷贝构造
String& operator=(const String& str); //拷贝复制
~String(); //析构
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
- 构造和析构函数的实现
我们在每一个函数定义上面加了inline,这样是否合适,有人说复杂的函数加了inline多此一举,是的,因为是否是inline这是由编译器决定的,我们可以把所有的成员函数都加上inline,这样写是没有问题的,至于是不是,让编译器去决定
#include <cstring> //使用C的函数
inline
String::String(const char* cstr)
{
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = '\0';
}
}
inline
String::~String()
{
delete[] m_data; //注意,析构函数delete动态分配的数组
}
- 拷贝构造函数的实现
根据传入的字符串长度开辟相同大小的内存空间,然后执行拷贝,由于包含‘\0’结束符,所以长度需要加1
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ]; //m_data虽然是private的,同类之间的对象互为friend
strcpy(m_data, str.m_data);
}
- 拷贝复制
- 从右值复到左值,清空左值之前的数据,然后开辟新的内存空间,执行拷贝
- 自检为什么是必须的,当是同一个对象的时候,而没有自检,那么会发生什么?
两个指针指向同一块内存空间,这一块内存空间先delete掉了,再取的时候另一个就变成了野指针
inline
String& String::operator=(const String& str)
{
if (this == &str) //自检不是多余的,一定要有
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
Stack(栈) Heap(堆)及生命周期
- Stack
Stack 是存在于某作用域的一块内存空间。例如当你调用一个函数,函数本身就会形成一个stack,用来存放接收的参数以及返回地址。在函数本体内声明的任何变量,其所使用的内存块都取自上述 Stack
- Heap
Heap *或者叫做默认System Heap, 是指由操作系统提供的一块global内存空间,程序可动态分配,从中获取若干区块(blocks) *
Stack objects的生命期
- 分析一段代码
class Complex {...};
...
Complex a3(5,6);
int main()
{
Complex a1(1,2); //a1所占用的内存来之stack
static Complex a2(1,3);
Complex* p = new Complex(2,3); //Complex(2,3)是个临时对象,它所占用的内存是以new自Heap动态分配获得,并由p指向
}
- Stack Objects的生命期
上述代码示例中,a1便是所谓的Stack Object, 其生命在作用域结束之际结束。这种作用域的Object,又被称为auto object, 因为他们会被自动清理。
- Stack local Object的生命期
a2便是 static object,它的生命在作用域结束之后仍然存在,直到整个程序结束。
- global objects的生命期
a3便是所谓的 global object,其生命在整个程序结束之后才结束,也可以把它理解为一种static,其作用域是整个程序
Heap Objects的生命期
- 代码
class Complex {...};
...
{
Complex* p = new Complex();
delete p;
}
p所指的便是Heap Object,其生命期在被deleted之际结束。如果不写delete p,会出现内存泄漏,p所指向的Heap object仍然存在,但p的生命期结束了,作用域外再也看不到p了,也就没有机会delete p了。
new delete操作符内部实现
内存分配这块非常重要,其分析的内存模型在很多资料上都是找不到的,内存模型是基于VC的,其他编译器也应该大同小异吧。
new:先分配memory,再调用ctor()
- 不包含指针
new内部分解为三个步骤:
- 调用 operator new函数(内部malloc)分配内存
- 转型
- 调用构造函数,赋初始值
- 包含指针
三个步骤是相同的,在这个例子里,operator new分配了4个字节的空间给指针,然后转型,第三步调用构造的函数的时候,又动态分配了6个字节的空间给hello并把地址返回给指针ps
delete:先调用析构,再释放内存
delete的内部实现delete内部实现分为两步:
- 调用析构函数
- operator delete释放内存
物理内存块模型 in VC
- 动态内存分配的对象
在实际的VC编译器中,一个Complex对象是8个字节,需要包含4*2个字节的cookie(delete回收的时候是根据cookie来进行回收的),一共是16字节,在调试模式下,需要额外的32+4个字节的信息。
- 动态内存分配的array
Complex数组连续分配三个对象空间,并且多了一个4字节存放数组的大小内存,在没有调试模式下,83 + 42 +4 = 36,序列化必须是16的倍数,所以,在实际的内存中是48字节。String的数组看起来会更小一点,但是还要在堆里面的内存。
- 为什么array new 一定要搭配 array delete
这个问题就在于delete[] 会多次调用析构函数,而不加[]只会调用一次析构函数,所以,在这个例子中,最后两个对象内部动态分配的内存是被泄漏了,这个内存模型分为两部分,对象数组部分和对象动态内存,他们都是在堆里的,那么我们调用delete p的时候到底泄漏了多少内存呢?答案是后两个对象的动态内存,对象数组本身的内存是delete根据cookie进行释放的。所以,如果我的类是没有指针的,那么我直接调用delete p是不会造成内存泄漏的。