String函数为例的C++学习笔记
拷贝构造,拷贝赋值,析构函数
这三个函数被称为BIG Three
string.h的防卫式声明
一般根据主函数来确定需要编写的函数
s3(s1)拷贝构造和s3=s2拷贝赋值。由此可知我们需要一个拷贝的动作和一个操作符<<的重载
如果class中带指针一定要自己写构造函数
一般而言都会先创建一个指针,只有当需要传入字符串的时候才会申请空间存放字符串。在本函数中m_data充当这个指针。
char指针指向字符串的解释
对于语句 char *a=“hello”;
对于这个声明方式,会造成的误解是:声明了一个字符指针(它会指向一个位置),将“字符串”赋值给 指针表达式"*a"所指向的地址。
但正解是:声明了一个字符指针后,并用字符串常量的第一个字符的地址赋值给指针变量a。
即正确顺序是:
1.分配内存给字符指针;
2.分配内存给字符串;
3.将字符串首地址赋值给字符指针;
被圈起来的函数属于自定义函数声明。第一个函数是构造函数,第二个构造函数就是拷贝构造,第三个是一个操作符重载,用于拷贝赋值。只要类带着指针一定要写拷贝构造和拷贝赋值函数
第四个函数是析构函数,以此类做出来的对象当它死亡时,析构函数就会被调用
析构函数和构造函数
strlen函数会从首地址读到结束地址并返回个数
构造函数
if(cstr)表示判断传入的指针是否为0,如果为0则将传到else。if(cstr)等同于if(cstr!=0)
strlen(cstr)+1是为了存放结束符号
对于char[]=“chinese”,在计算数组长度时包不包括结束标志’\0’,那么对于char[]={‘c’,‘a’,‘t’}这种数组呢?
所有计算字符串的长度都不包括结束标志’\0’,第2种情况同样。
对于前一种情况,如果用strcpy将一个数组复制到另一个数组时,会否将结束标志也复制过去呢?
会
如果是,那么在输出复制后的数组时遇到该结束标志是否会终止?为什么?
会终止,因为有结束符。
如果不终止,那么是输出0还是空格呢?为什么?
会终止
析构函数是为了清理动态分配的内存
默认拷贝(浅拷贝)的坏处
会造成两个object指向同一块空间。
拷贝构造函数(深拷贝)
拷贝赋值函数
需要先把左边清空然后分配和右边一样大的空间容纳右边的内容
s2是调用操作符=的对象所以它是this,s1是传入的str
如果不写自我赋值结果还可能会出错
output函数不可以是成员函数只能是全局函数,如果它是成员函数那么cout方向会相反即变成s1<<cout;
成员函数的第一个参数是默认的this
堆,栈与内存管理
Heap由关键字new动态分配获得,当你获得Heap时你有责任去delete它
stack objects的生命期
static local objects的生命期
global objects的生命期
全局对象是存在于任何大括号之外的
heap objects的生命期
关键字new
new关键字来创建对象时,先会得到一块空间然后调用构造函数
绝大部分new会被分解成上图三个动作,operator new是一个函数的名称(由C++提供,malloc是C里面调用内存的函数)
第三步中pc在构造函数中this
关键字delete
先调用析构函数清除自己动态分配的内存,再释放字符串本身(指针)内存
动态分配所得的内存块,在VC下
以Complex为例
Complex对象有两个double申请的空间占用8个byte即8个字节,上图每一格是4个字节上面会多得到32个字节,下面会多得到4个字节(灰色的部分即多得的部分),砖红色是cookie,因为VC下分配的空间一定是16的倍数所以最靠近52字节的16的倍数就是64。图中的pad就是填补物。
第二个图是不会进入调试模式(debugger module),不会加上debugger header(上32,下4)但一定要加上 上下cookie。
上下cookie最主要的工作是要记录给你的区块大小
第一个图中的上cookie是41但本来应该是40即4*16,以16进制的方式存储。40借用最后一位即0或1来表现这一块是给出去还是收回来,因为是操作系统给出来了所以是41。为什么可以借最后一位来使用,因为是16的倍数所以一般最后一位都是0。
以String为例
字符串本身只内含一个指针,所以指针的大小是4byte
动态分配的array
array new要搭配array delete
在调试模式下3个complex上要加32bytes,下要加4byte,上下cookie,最后加的4bytes是因为是一个数组用一个整数来记录有3的东西
h代表16进制
有无中括号都不影响删除,因为上下cookie记录了内存块大小
发生内存泄漏是因为如果不写中括号编译器就不知道是数组,只会调用一次析构函数,析构函数执行完毕后才会执行删除母体。内存泄漏发生在2和3指向的空间不是母体。如果发生在指针函数里是否是delete []都可以。
复习String的实现过程
String使用的是C风格的字符串即char指针指向首字符
String& operator=(const String& str);
//执行的结果放到什么地方去,如果放的结果不是local object那么就可以选择传reference
char* get_c_str() const { return m_data; }
//此函数内没有改变任何数据因此可以加上const,如果需要加上const需要加上函数名称后面
构造函数和析构函数
//构造函数
inline
String::String(const char* cstr)
{
if (cstr) { //cstr存在?
m_data = new char[strlen(cstr)+1];//如果cstr指针不是0,new的形式创建足够大的空间,+1是加上字符串的结束符号
strcpy(m_data, cstr);
}
else { //为指定初值,cstr为0
m_data = new char[1];
*m_data = '\0';//结束符号
}
}
//析构函数
inline
String::~String()
{
delete[] m_data;//我们分配了空间(不管哪一种情况)
}
拷贝构造函数
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];//足够大的空间,一定要+1
strcpy(m_data, str.m_data);
}
拷贝赋值函数
inline
String& String::operator=(const String& str)//这里的&叫reference,放在typename后面
{
if (this == &str)//这里的&叫做取地址,放在object前面
return *this; //必须先查询是否是自己赋值自己即来源端是否和目的端相同
//如果没有这个检查那么会先清除自己导致错误
delete[] m_data; //目的端要清除自己
m_data = new char[ strlen(str.m_data) + 1 ];//目的端要分配足够大的空间
strcpy(m_data, str.m_data);
return *this;//如果设置成void 某种情况下可以某种情况下不行。
//如果是连串的赋值例如s1=s2=s3=“hello”,那返回类型就不能是void。
}