7.三大函数:拷贝构造,拷贝赋值,析构
String s3(s1);//拷贝构造函数(s3刚刚出现)
String s4 = s1;//这种情况也是拷贝构造(虽然用的'=',但是S4刚刚出现)
s3 = s2;//拷贝赋值(s3已经出现)
无指针的类,不需要写拷贝构造和拷贝赋值。类内带指针,一定要写拷贝构造和拷贝赋值,不能用编译器自动生成的。
- 构造函数,参数类型是自身类型,则为拷贝构造函数。
- 拷贝赋值,重载=操作符,参数类型是自身类型。
- 和构造函数名称相同,前面加~,是析构函数,当类的对象死亡的时候,析构函数会被调用。
构造函数
inline
String::String(const char* cstr = 0)
{
if(cstr){
m_data = new char[strlen(cstr) + 1];
strcpt(m_data, cstr);
}else{
m_data = new char[1];
*m_data = '\0';
}
}
- c语言 字符串,以'\0'结尾,字符串长度,以'\0'标记来计算。另一种在字符串的前面有长度标示,后面没有结束符。
- 字符串长度为0,也要用一个字符,来保存'\0',为了析构函数统一析构,一个字符用 new char[1] 来创建。
- 当字符串长度不为0,则用strlen()计算出长度+1,用来保存最后的'\0'。
析构函数
inline
String::~String(){
delete[] m_data;//array delete配合 array new
}
- 析构函数的作用,清理,cleanup。
- 离开作用域时要释放内存。
big three
class with pointer members 必须有 copy ctor 和 copy operator=。如果没有使用,则极易造成内存泄露,且两个类中的指针指向同一块内存,改变A,则B也被改变。
copy ctor
inline
构造函数,接受参数类型为本身,则为拷贝构造函数。
深拷贝:首先创造足够的空间,然后把内容拷贝到新的对象中。
浅拷贝:则会造成两个‘人’在‘看’同一个东西。
copy assignment operator 拷贝赋值函数
右边的对象拷贝的左边,左右两边原本都有内容
- 1.首先要清空左边。
- 2.然后在左边分配和右边一样的空间。
- 3.再把右边的内容拷贝到左边。
- 4.特别要注意,要检测自我赋值,如果不检测,则自身在拷贝之前就被干掉了,造成内存错误。如果检测到自我赋值,则直接返回。不单单是为了效率,更是为了安全。
8.堆,栈与内存管理
8.1.Stack和Heap
Stack
是存在于某作用于(scope)的一块内存空间(memory space)。调用函数时,函数本身即会形成一个stack用来放置它所接收的参数,以及返回地址,以及local object。
Heap
是由操作系统提供的一块global内存空间,程序可以动态分配从其中获得若干区块,new出来的,必须手动delete掉。
stack objects的生命周期
stack object,即为local object,又称为 auto object,生命在作用域结束之后就结束了。对象的析构函数会被调用。
static local objects的生命周期
static object,其生命在作用域结束后仍然存在,直到整个程序结束。
global objects的生命周期
任何写在大括号之外的对象,其生命在main函数之前就存在,在程序结束之后才结束,作用域是“整个程序”。
heap objects的生命周期
new得到的对象,在使用完毕之后要delete掉。如果没有delete,则会造成内存泄露。
8.2 new delete
new:先分配memory,再调用ctor
Complex *pc = new Complex(1, 2);
编译器把new分解为三个动作:
- 1.分配内存 void* mem = operator new(sizeof(Complex)); // 内部调用malloc(n);
- 2.转型pc = static_cast<Complex*>(mem);
- 3.构造函数pc->Complex::Complex(1,2); // 其实际参数列表为 Complex::Complex(pc, 1, 2);
delete:先调用dtor,在释放memory
String *ps = new String("Hello");
...
delete ps;
编译器把delete分解为两个动作:
- 1.析构函数 String::~String(ps); // 析构函数会delete掉String类内部动态分配的空间
- 2.释放内存 operator delete(ps); // 其内部调用free(ps);
共计两次delete。
8.3 动态分配所得的内存块(memory block)
1.动态分配所得的对象
1.Complex *pc = new Complex(1, 2);
在debug模式下,class的前面有32字节,后面有4字节,前后cooky各4字节,cooky为0x41,共计:
8+(32+4)+(4*2)=52 -> 64
在release模式下,class本身8个字节,前后cooky各4个字节,cooky为0x11,共计:
8+(4*2)=16 -> 16
上下cooky的作用,记录整块给你的大小。采用16进制,如果是0,则代表系统回收,如果是1,则代表系统给出。在vs的编译器下,给的内存的大小为16的倍数,所以cooky在16进制时最后一位一直为0,所以可以用来标记内存的方向。
2.String *ps = new String("Hello");
在debug模式下,cooky为0x31,共计:
4+(32+4)+(4*2)=48 -> 48
在release模式下,cooky为0x11,共计:
4+(4*2)=12 -> 16
2.动态分配所得的 array
array new 要搭配 array delete,不然会出错。
1.Complex *p = new Complex[3];
在debug模式下,cooky为0x51,共计:
(8*3)+(32+4)+(4*2)+4=72 -> 80
在release模式下,cooky为0x31,共计:
(8*3)+(4*2)+4=36 -> 48
2.String *p = new String[3];
在debug模式下,cooky为0x41,共计:
(4*3)+(32+4)+(4*2)+4=60 -> 64
在release模式下,cooky为0x31,共计:
(4*3)+(4*2)+4=24 -> 32
3.array new 一定要搭配 array delete
String *p = new String[3];
...
delete[] p; // 调用3次dtor
memory | 解释 |
---|---|
21h | cooky记录内存大小 |
3 | 数组的大小 |
String[0] | 调用dtor |
String[1] | 调用dtor |
String[2] | 调用dtor |
000000000(pad) | 填充内存 |
21h | cooky记录内存大小 |
String *p = new String[3];
...
delete p; // 调用1次dtor
memory | 解释 |
---|---|
21h | cooky记录内存大小 |
3 | 数组的大小 |
String[0] | 调用dtor |
String[1] | 未调用dtor |
String[2] | 未调用dtor |
000000000(pad) | 填充内存 |
21h | cooky记录内存大小 |
对比发现,整块的内存并没有发生内存泄露,因为整块内存的大小记录在cooky当中。如果没有写array delete而写的是delete,编译器不知道下面有几个对象,因此只有第一个也就是String[0]调用了dtor,其余的对象并没有调用dtor。当调用玩dtor之后,再释放掉整块的内存。由此可以发现,如果此时的例子是Complex类的话,那么由于类内部没有指针,所以即使用array new,但没用array delete,也不会产生内存泄露。
但是在写代码时,我们应养成好的编码习惯,array new 一定要搭配 array delete。
9.复习String类的实现
- 1.防卫式声明
#ifndef _MYSTRING_ #define _MYSTRING_ class String{ ... }; #endif
- 2.如何去定义内部变量
- 放数组,但是数组的大小无法确定。
- 放指针,当需要时,动态分配(new)字符串的大小,在32位的系统中,一个指针是4byte,放在private中。
char *m_data;
- 3.ctor,放在public;
String(const char* cstr = 0);
- 只是接受字符串,不会改变,要加上const。
- 4.class with point member:
- copy ctor:
String(cosnt String& str);
- copy assignment operator:
String& operator=(const String& str);
- 对于copy ctor 和copy assignment函数,不会改变被拷贝的对象,所以要加上const。
- 返回拷贝的对象,因为返回结果不是放在local object中,目标本来存在,因此使用return by reference。
- dtor:
~String();
- copy ctor:
- 5.辅助函数
- 为了能够cout字符串,因此需要一个函数能够取出String类中的字符串。
char* get_c_str() cosnt { return m_data; }
- 因为函数简单,直接使用inline的方式。因为不会改变对象的成员变量,因此需要加上const。
- 为了能够cout字符串,因此需要一个函数能够取出String类中的字符串。
- 6.ctor,copy ctor,copy assignment 都不需要加const
- 7.ctor和dtor
- ctor
inline //建议编译器 String::String(cosnt char* cstr = 0){ if(cstr){ //以下两个函数为C函数,需要相应头文件 m_data = new char[strlen(cstr) + 1]; strcpy(m_data, cstr); }else{ m_data = new char[1]; *m_data = '\0'; } }
- dtor
inline //建议编译器 String::~String(){ delete[] m_data; //由于ctor使用了array new,因此这里也要使用array delete }
- copy ctor
inline String::String(cosnt String& str){ m_data = new char[strlen(str.m_data) + 1]; strcpy(m_data, str.m_data); }
- inline只是建议,不能inline的话也没关系。
- copy assignment operator
inline String& String::operator= (cosnt String& str){//此时&为reference //首先判断是否自我赋值,不单单是效率问题,更是正确与否的问题。 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; //传出去值不在乎用何种方式接受 }
- 关于返回值,当不需要连续赋值时,则不需要返回值,当需要连续赋值时,则需要返回值。
String s1, s2, s3("Hello"); s1 = s2 = s3;
- ctor
10.扩展补充:类模板,函数模板及其他
1.static
Class complex{
public:
double real() const{
return this->re;
}
private:
double re;
double im;
};
- C++的习惯写法
complex c1, c2, c3;
cout << c1.real();
cout << c2.real();
- 从C的角度考虑完成同上功能的写法
complex c1, c2 ,c3;
cout << complex::real(&c1);
cout << complex::real(&c2);
同一个函数real(),之所以能处理不同对象的数据,靠的就是this point。
static data members 在内存的单独位置,有且只有一份。
static member functions
同样在内存的单独位置,函数本身也仅仅只有一份。但是跟一般的成员函数有个区别,它没有this point。它只能去处理静态的数据。
静态的变量,在类的内部只是生命,需要在类外部定义。类型 类名称::变量名(初始化操作);
调用静态函数的方法有两种:
- 1)通过object调用。但是this指针不会被作为参数传入函数中。
- 2)通过class name调用。
单例模式,把ctors放在private区域。
class A{
public:
static A& getInstance(){ return a; }
setup()
private:
A();
A(const A& rhs);
static A a;
...
};
A::getInstance().setup();
外界想要使用a,只能用过:getInstance()获得。
meyers Singleton:
class A{
public:
static A& getInstance();
setup()
private:
A();
A(const A& rhs);
...
};
A& A::getInstance(){
static A a;
return a;
}
当外界不需要使用这个类时,a不会被创建,只有当外界需要使用这个类,调用了getInstance()函数,a才会被创建。
2.关于cout
查看标准库ostream代码,重载了很多的operator<<
3.class template,类模板
在类的前面加上:
template<typename T>
class complex{
...
};
用T吧具体的类代替,当实际使用时,根据实际的需要,生成具体的类代码。
{
complex<double> c1(2.5, 1.5); //用double代替T生成一份类的代码
complex<double> c2(2, 6); //用int代替T生成一份类的代码
}
4.function template,函数模板
template<class T>
inline
const T& min(const T& a, const T& b){
retuen a < b ? a : b;
}
所有比较大小都是这么操作,因此可以使用函数模板。实际比较时如何去比较,则依赖于需要比较大小的类。类似于这种函数,称之为算法。
{
complex c1(1, 2), c2(3, 4), c3;
c3 = min(c1, c2);//当调用min()函数时,编译器会进行实参推导(argument deduction),不必再使用的时候指定类型。
}
5.namespace
避免全局变量,函数以及类的同名,则需要namespace,如果每个人自己顶一个namespace,则不会造成冲突。
- using directive
using namespace std; { cin >> ...; cout << ...; }
- using declaration
using std::cout; { std::cin >> ...; cout << ...; }
- not use
{ std::cin >> ...; std::cout << ...; }