GeekBand C++ 第二周

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(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();
      
  • 5.辅助函数
    • 为了能够cout字符串,因此需要一个函数能够取出String类中的字符串。
      char* get_c_str() cosnt { return m_data; }
      
    • 因为函数简单,直接使用inline的方式。因为不会改变对象的成员变量,因此需要加上const。
  • 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;
      

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<<

GeekBand C++ 第二周_第1张图片
ostream

3.class template,类模板

在类的前面加上:

template
class complex{
...
};

用T吧具体的类代替,当实际使用时,根据实际的需要,生成具体的类代码。

{
    complex c1(2.5, 1.5); //用double代替T生成一份类的代码
    complex c2(2, 6); //用int代替T生成一份类的代码
    
}

4.function template,函数模板

template
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 << ...;
    }
    

6.更多的细节,仍需努力

GeekBand C++ 第二周_第2张图片
仍需努力的部分

你可能感兴趣的:(GeekBand C++ 第二周)