[C++] C++面向对象高级开发:String类
Big Three:拷贝构造函数,拷贝赋值函数,析构函数
int main()
{
String s1();
String s2("hello");
String s3(s1); // 拷贝构造
cout << s3 << endl;
s3 = s2; // 赋值构造
cout << s3 << endl;
}
class String
{
public:
String (const char* cstr = 0); // 拷贝构造函数
String (const String& str); // 拷贝构造函数
String& operator = (const String&); // 拷贝赋值函数
~String (); // 析构函数
char* get_c_str () const { return m_data; }
private:
char* m_data;
}
注:
如果不写Big Three,编译器会自动创建一个,
在进行拷贝和赋值时,会将class中包含的数据进行拷贝和赋值,
对于含有指针的class,这样做是不正确的。
当被拷贝或赋值的对象释放时,
这个拷贝出来的指针就会指向错误的内存地址。
对析构函数也类似,编译器自动创建的析构函数只会释放对象中的数据,
而不会释放指针m_data
指向的数据。
所以,对于包含指针的class,我们应该自己实现Big Three。
构造函数和析构函数
inline
String::String (const char* cstr = 0)
{
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;
}
{
String s1();
String s2("hello");
String* p = new String("hello");
delete p;
}
拷贝构造函数
inline
String::String (const String& str)
{
m_data = new char [strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
}
注:
同类object之前互为friend,因此,str.m_data
可以直接取private数据。
{
String s1("hello");
String s2(s1); // 拷贝构造
}
拷贝赋值函数
inline String&
String::operator = (const String& str)
{
if(this == &str){ // 检测自我赋值
return *this;
}
delete[] m_data;
m_data = new char [strlen(str) + 1];
strcpy(m_data, str.m_data);
return *this;
}
注:
检测自我赋值,并不只是效率方面的考虑,还是为了保证程序的正确性,
如果不判断自我赋值,会首先释放掉内存delete[] m_data
,导致程序出错。
{
String s1("hello");
String s2;
s2 = s1; // 拷贝赋值
}
<< 运算符重载
#include <iostream.h>
ostream&
operator << (ostream& os, const String& str)
{ return os << str.get_c_str(); }
{
String s1("hello");
cout << s1;
}
栈(stack)和堆(heap)
(1)栈
Stack,是存在于某作用域(scope)的一块内存空间(memory space)。
例如,当你调用函数,函数本身即会形成一个stack用来放置它所接收的参数,以及返回地址。
在函数体(function body)内声明的任何变量,
其所使用的内存块都取自上述stack。
(2)堆
Heap,或称为system heap,是指由操作系统提供的一块global内存空间,
程序可动态分配(dynamic allocated)从其中获得若干区块(blocks)。
{
complex c1(1, 2); // stack
complex* p = new complex(3); // heap
}
注:
栈中的对象,在作用域结束后会自动被清理,
即自动调用其析构函数,
因此,栈中的对象又称为auto object(自动对象)。
而static局部变量则不会,
{
static complex c2(1, 2);
}
static局部变量,其生命在作用域结束后仍然存在,
直到整个程序结束后被清理。
全局对象
class complex { ... };
...
complex c3(1, 2); // 全局对象
int main ()
{
...
}
全局对象也是在整个程序结束后才被销毁,
可以把它视为一种static object,其作用域是整个程序。
heap object的生命周期
{
complex* p = new complex;
delete p; // 释放到p指向的内存
}
{
complex* p = new complex;
}
以上会出现内存泄漏(memory leak),
因为当作用域结束,p所指向的heap object仍然存在,
但是指针p的生命却结束了,作用域之外再也看不到p了,
也就没机会delete p
。
new原理
new
操作符的作用原理是,先分配内存,再调用构造函数。
complex* pc = new complex(1, 2);
编译器会转化为,
complex* pc;
void* mem = operator new (sizeof(copmlex)); // 分配内存
pc = static_case<complex*>(mem); // 类型转换
pc->complex::complex(1, 2); // 调用构造函数
其中,pc->complex::complex(1, 2)
,
会将pc
作为隐含的this
指针传入构造函数。
operator new
内部调用了C函数malloc(n)
来分配内存。
delete原理
complex* pc = new complex(1, 2);
delete pc;
编译器转化为,
complex::~complex(pc); // 调用析构函数
operator delete(pc); // 释放内存
其中,operator delete
内部调用了C函数free(pc)
来释放内存。
VC中动态分配所得的内存块
左边第一张图为complex对象,在VC debug模式下占用的内存。
其中,浅绿色部分是complex中的数据所占内存(2个double,2×4 bytes),
灰色部分,是VC在debug模式下增加的调试信息所占内存(上32 bytes下4 bytes),
红色部分,成为cookie,VC用来标志内存块的开始和结束(各4 bytes),
深绿色部分,是由于VC要求内存块大小必须为16的倍数,所进行的补齐。
此外,红色cookie的值,表示内存块大小,
由于内存块大小必为16的倍数,所以最后一位肯定是0,
例如,64 bytes是写成16进制是40h
。
这时VC用这一位作为标志位,1
表示操作系统分配出来的内存,
0
表示操作系统已回收的内存,
于是,cookie变成了41h
。
左边第二张图为complex对象,在VC release模式下占用的内存,
去掉了调试信息,
由于加上cookie后,内存大小已经是16的倍数,因此不需要补齐。
右边两张图为String对象分别在VC debug和release模式下的所占用的内存。
动态分配数组
如果动态分配了一个数组,例如,
m_data = new char [strlen(cstr)+1];
则除了为数组分配空间之外,还要增加一块内存区域,
用来表示数组的长度,
图中灰色区域上面的3
(4 bytes),表示了后面分配的数组长度。
array new 一定要搭配 array delete
由于delete[] p
和delete p
,都是删除p
指向的内存,
所以,以21h
开头和结尾的内存块,用这两种方式调用都会被删除。
但是,delete[] p
会根据数组的长度,自动调用3次析构函数,
而delete p
只会调用1次析构函数,
因此,String对象中m_data
所指向的内存区域,使用delete p
就没有被完全释放。
注:
如果对象中不包含指针,则delete[] p
和delete p
的作用就是相同的,
并不会造成内存泄漏。