C++面向对象(三)String类
一、深浅拷贝
String.h
———————————————————————————————————————————————————————————————————————————————
| #ifndef __COMPLEX__ |
| #define __COMPLEX__ |
———————————————————————————————————————————————————————————————————————————————
0 | #include <cmath> -------------------------- |
| | forward declarations | |
| class ostream; | (前置声明 ) | |
| class String; -------------------------- |
| |
| |
———————————————————————————————————————————————————————————————————————————————
1 | class String{ ------------------------ |
| ... | class declarations | |
| }; | (类-声明 ) | |
| ------------------------ |
———————————————————————————————————————————————————————————————————————————————
2 | string::function(...) ... ---------------------- |
| Global-function(...) ... | class definition | |
| | (类-定义 ) | |
| ---------------------- |
———————————————————————————————————————————————————————————————————————————————
| #endif |
———————————————————————————————————————————————————————————————————————————————
string-test.cpp
int main(){
String s1();
String s2("hello");
String s3(s1); // 拷贝构造(copy ctor)
cout << s3 << endl;
s3 = s2; // 拷贝赋值(copy op=)
cout << s3 << endl;
}
注意:这里要知道 浅拷贝 和 深拷贝 的区别。浅拷贝一般是编译器默认的构造函数,即一个位一个位地拷贝过去,前面的complex例子便是用浅拷贝。 深拷贝 则是一般用在带指针的类上面(需要自己去定义编写的构造函数),因为如果只是用编译默认的函数去拷贝,则只会将相应的指针拷贝过去,进行的是浅拷贝,这样相当于两个类对象指针相同,且指向同一地址,容易出现问题。只要类是带指针的,则一定要自己编写拷贝赋值函数。
二、Big Three(三个特殊函数)
class String{
public:
String(const char* cstr = 0); // 构造函数应该跟class同名
String(const String& str); // 拷贝构造
String& operator=(cons String& str); // 拷贝赋值
~String(); //析构
char* ger_c_str() const{
return m_data;
}
private:
char* m_data;
}
- ctor 和 dtor(构造函数和析构函数)
注意:一个字符串有多长,有两种设计方法,一为在字符串后面+结束符号 ’\0’ ;一为在前面加标志长度的数值 ’len’ 。从C以来,一直延续一个概念,字符串是一个指针指进来指着头,指着一串,然后后面再有个 ’\0’ 的结束符号(C++也是)。
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';
}
}
class里面有指针多半是要做动态分配的(即new),所以在对象死亡之前必须调用下面的析构函数,将动态分配的内存释放掉,否则将会造成内存泄漏。
inline String::~String(){
delete[] m_data;
}
这因为前面对应new的是数组 ‘[ ]’ ,所以析构函数这边应该对应用 ‘delete[ ]’。其释放的是‘hello’对应的动态内存空间。而下面的例子中,因为new的不是 ‘[ ]’ ,而是动态分配了个指针内存空间,所以直接 ’delete p‘ 即可,无需 ‘delete[ ]’ 。
{
String s1();
String s2("hello");
String* p = new String("hello");
delete p;
}
class with pointer members 必须有 copy ctor(拷贝构造) 和 copy op=(拷贝赋值)。
- copy ctor(拷贝构造函数)
若需要操作 b=a; 时,当使用默认的copy ctor时,进行的是浅拷贝,即只是将a的point复制到b,a和b指向的都是同一个内存地址,这样当对a进行操作改变时,会让b也受到影响。而且原先b指针指向的内存块内存泄漏。而下面因为自写了拷贝构造函数,所以进行的是深拷贝。
inline String:: String(const String& str){
m_data = new char[strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
}
{
String s1("hello");
String s2(s1);
// String s2 = s1;
}
- copy op=(拷贝赋值函数)
inline String& String::operator=(const String& str){
if(this == &str)
return *this; // 检测自我赋值(self assignment)
delete[] m_data;
m_data = new char[strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
return *this;
}
{
String s1("hello");
String s2(s1);
String s2 = s1;
}
正常情况下,当两个对象的指针指向不是同一个内存地址时,不用自我检测也许不会出错,如:
但当存在自我赋值的情况时,则必须要编写
if(this == &str)
return *this;
否则一上来直接执行 ‘delete[ ]’ 则因为同一个指针地址指向的内存被释放掉了,无法进行后续的拷贝操作。
三、stack(栈)和heap(堆)
class Complex {
...
};
{
Complex c1(1,2);
Complex* p = new compelx(3);
}
c1,所占用的空间来自stack,当离开作用域{ }时,便自动释放掉了;
而 Complex(3) 是个临时对象,其所占用空间时以new自heap动态分配而得,并由p来指向。若离开作用域{ }时,此情况下,若不调用‘delete p’,则无法释放掉其动态分配的内存空间,而造成内存泄漏。(若是涉及字符串,即带指针的类的对象时,则一定要’delete[ ]’ ,再‘delete p’)
-Stack
存在于某作用域的一块内存空间。若在函数体内声明的任何变量其所使用的内存块都是存在于stack中的。
- stack objects的生命周期
{
Complex c1(1,2);
}
c1 称stack object又称auto object,生命再作用域(scope)结束时结束,会自动被清理掉。
- static local objects的生命周期
{
static Complex c1(1,2);
}
c2 称static object,生命再作用域(scope)结束时仍存在,直到整个程序结束(因为其析构函数在离开作用域时也没被调用,直到程序结束才调用)。
-Heap
操作系统提供的一块global内存空间,动态分配。
- heap objects的生命周期
{
Complex* p = new Complex;
…
delete p;
}
p指的是heap object,生命在调用delete时结束。在调用delete时,会先调用complex的默认析构函数,所以释放了new出来的内存空间,不存在内存泄漏。
{
Complex* p = new Complex;
}
没有使用delete,存在内存泄漏。因为当作用域结束时,p所指的那块空间(即heap object)仍然存在,但指针p的生命却结束了,作用域之外再也没机会delete p了。
- new:先分配memory,再调用ctor
Complex *pc = new Complex(1,2);
则编译器转化为:
- void* men = operator new(sizeof(Complex)); // 分配内存
内部调用malloc(n),在内存中开辟出double,double的空间:
- pc = static_cast<Complex*>(men); // 转型
将第一步的void型指针转为对应的Complex型指针。
- pc -> Complex::Complex(1,2); // 调用构造函数
注:这里的pc指针是不要写出来的,只是为了说明将其写出来而已。
----------------------------------------------------------------
⇓
\Downarrow
⇓ -------------------------------------------------
调用complex的构造函数,通过指针p找到对应内存地址,给内存空间double,double赋值。
- delete:先调用dtor,再释放memory
String *ps = new String("Hello");
...
delete ps;
则编译器转化为:
- String::~String(ps); // 析构函数
这里是先将指针ps所指向的动态分配的内存空间里的’Hello’先释放掉,即动态分配内存空间里的内容已经被清除了。
- operator delete(ps); // 释放内存
其内部相当于调用 free(ps),即将指针删除掉,对应释放掉的即是ps(字符串相当于一个指针而已)。
四、动态分配的内存空间
在C中,对于动态分配内存,使用的是malloc和free的指令。
- 动态分配所得的内存块
左图表示的是debuger模式下的,其除了有8byte的complex数据外(8),前面还有8个8byte(32)+后面的1个8byte的灰色信息部分(debugger信息部分),再加上头尾的2个8byte的cookie信息(4*2),总共为52byte,而要凑成16进制数,还需填补上深绿色的部分(4*3),所以52+12=64。另外cookie的作用是,标志内存空间的状态以及所分配的整块空间的大小,因为转成了16的倍数,即为00000040,而后面补1即00000041,表示这是系统给出去的空间,对应系统收回的即为00000040。
右图表示的是非debugger模式下的,则不许加上灰色数据的部分。下面String类对象也是类似情况:
- 动态分配所得的数组(array)
注意:array new 一定要搭配 array delete,否则很容易存在内存泄漏 (针对指针类对象的数组而言) 的情况。
-
非指针类对象的情况
这里最后面的4是表示在VC编译器下,虽有3来表示有三个(double, double),类似数组的长度。在该类对象的情况下,array new 不搭配 array delete即(‘delete [ ]’),不会内存泄漏,因为没有涉及到指针即不涉及动态分配的内存空间,在离开作用域时直接结束消亡。
-
指针类对象的情况
对比下图左边(array delete)和右边(no array delete)的代码,
左边的因为正确使用了array delete,调用了三次dtor而将动态分配的内存空间释放掉,然后再free掉指针,不会造成内存泄漏;
但右边的由于没有进行array delete,则只是调用一次dtor,而只释放了一个动态内存空间,剩余的其他两个动态分配的内存空间则没法释放,然后指针就被free掉了 (若在指针被free掉之前,没有将其中所分配得内存空间释放干净,则会导致内存泄漏) 。