string类:内部含有指针
如何去写一个类?
- 首先考虑类的内部的成员,主要是看类的数据成员是否包含pointer
- 其次考虑如何构造函数的参数和返回值(参数是否要添加const)
- 如果含有pointer的话,拷贝构造函数,拷贝赋值函数,以及析构函数都需要用户自己去定义,不能使用编译器提供的默认函数。
- 一般来说,构造函数,拷贝构造函数,拷贝赋值函数是不能被设置为const方法的,因为类的数据成员是要发生改变的。
- 之后思考每一个函数内部如何实现。
- 添加公共接口,用来和外界进行交互。
- 一般来说,都需要重写输入输出运算符,因为标准的输入输出运算符只能够输出内置类型,而用户自定义的类类型,需要用户自身去定义。
下面就来实操一下吧:
首先考虑string类的作用,可以构造字符串。它的private成员:最好被设置为指针,因为如果直接使用字符数组的话,并不知道输入的字符串的大小是多少,而采用指针的方法:可以进行动态管理的内存。所以这里将私有成员设为指针变量。
class my_string
{
public:
my_string(const char* cstr = 0)
{
if (cstr)
{
m_data = new char[strlen(cstr) + 1];
//将str中的元素拷贝到m_data中。
strcpy(m_data, cstr);
}
else
{
// 开辟内存空间
m_data = new char[1];
// 初始化
*m_data = '\0';
}
}
//拷贝构造函数,传入的参数是类的引用
my_string(const my_string& str);
//拷贝赋值函数
my_string& operator=(const my_string& str);
char* get_c_str() const { return m_data; }
~my_string();
private:
char* m_data;
};
inline my_string::~my_string()
{
delete[] m_data;
}
inline my_string::my_string(const my_string& str)
{
//开辟空间
m_data = new char[strlen(str.m_data) + 1];
//把元素放进去
strcpy(m_data, str.m_data);
}
my_string& my_string:: operator=(const my_string& str)
{
if (this == &str)
return *this;
//1.释放原本的空间
delete[] m_data;
// m_data开辟str.data大小的空间
m_data = new char[strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
return *this;
}
ostream& operator<<(ostream& out, const my_string& str)
{
out << str.get_c_str();
return out;
}
测试代码:
int main()
{
my_string a1("hello");
my_string a2(a1);
my_string a3("i am a student");
/*a3 = a1;*/
a3.operator=(a1);
//等价于a3=a1;
cout << a1 << ":" << a2 << ":" << a3 << endl;
}
几个重要的地方:
1.visual_2019规定必须在类内初始化成员变量:因此构造函数的话需要写在class内,而不能放在类外。
2.字符串进行拷贝之前,必须给字符串开辟空间,而不能直接strcpy。
3.自己写代码的时候:需要注意变量和返回值的类型,没有养成这个习惯。如果给定的参数不发生改变,最好加上const,防止在内部修改。
4.operator=:其实将可以将它看成函数名。this指针指向的是被调用对象的地址。
5.赋值运算符中,要预防自我赋值的原因不仅仅是为了提升效率,如果不预防自我赋值的话,程序就会出现错误,理由如下:
s1和s2指向同一块内存空间,在delete之后,m_data所指向的空间已经被销毁,因此计算strlen((s2.m_data))已经没有意义啦!
堆和栈的内存空间
1.栈:是存放在某作用域内的一块内存空间。
例如,当我们调用函数的时候,函数本身会形成一个stack来放置它接收的参数,以及它的返回地址。在函数体内声明的任何变量,在脱离了作用域之后会,会被系统自动回收。
2.堆:又称为system heap,它是由操作系统提供的一块全局内存空间,一般通过new或者malloc开辟的空间是堆中的内存空间,它需要用户自己手动的开辟和释放空间,否则将会造成内存泄漏。
class complex{.....};
.......
{
complex* p= new complex;
}
如果不进行delete操作的话,指针p是存放在栈区的,因此当作用域结束之后,p所指向的heap-object 仍然存在,然而p指针的生命会结束,相当于我们找不到存储堆对象的那块空间了,故而会出现内存泄漏。
3.stack object:这里的c1就是stack object,也称为auto object,因为它是在栈上开辟的一块空间,因此其生命在作用域(scope)结束之后而结束。栈内的数据会被系统自动清理掉。
{
complex c1(1,2);
}
4.static local object :c2即是静态对象,它的生命在scope结束之后仍然存在,直到整个程序结束。
{
static complex c2(1,2);
}
new和malloc内部的机制
new的机制
new的基本原理是:先分配内存,再调用类的构造函数。
内部的函数如上图所示:
首先调用operator new函数分配一块内存空间-operator new的内部会调用malloc函数来开辟内存空间。
其次通过static_cast,即将void类型的指针强转为类类型的指针
最后通过指针访问该对象,调用其构造函数。
delete的机制
当对象的生命周期结束之后,系统会自动的调用析构函数。析构函数主要完成对类的清理善后工作,在最开始的例子中,string对象中的成员变量m_data是一个指针,既然是指针,那么其所指向的内存空间需要在析构函数中被释放,因为该内存空间也是new出来的。
为什么array new一定要搭配array delete?
如果申请了3个string类的对象,释放的时候一定要加[],如果直接delete p,由p申请的这三个heap object是可以被释放掉的,因为这里的21h:2*16+1=32字节,加1是因为:标记该内存块被使用,申请的总的内存大小是32个字节。由于已经记录了堆空间所开辟的内存的大小。
真正的原因在于:delete p的时候,只会调用一次析构函数,这样导致的结果就是后面两个对象没有调用析构函数,因此m_data所指向的内存空间无法被释放,如果数据成员中不含有指针的话,delete p和delete[] p是没有区别的,但是由于含有指针,因此它开辟的堆空间也需要被释放掉。