侯捷《C++面向对象编程》笔记(二)超经典-如何编出具有大家风范的类 一、三大函数:拷贝构造、拷贝复制、析构 二、堆、栈与内存管理 三、类模板、函数模板 完!!!

积土成山,风雨兴焉;积水成渊,蛟龙生焉;积善成德,而神明自得,圣心备焉。故不积跬步,无以至千里;不积小流,无以成江海。骐骥一跃,不能十步,驽马十驾功在不舍。锲而舍之,朽木不折;锲而不舍,金石可镂。蚓无爪牙之利,筋骨之强,上食埃土,下饮黄泉,用心一也。蟹六跪而二螯,非蛇蟮之穴无可寄托者,用心躁也。是故无冥冥之志者,无昭昭之明;无昏昏之事者,无赫赫之功

——荀子《劝学篇》

(1)string类里面存在指针,属于类的两种类型之一;有指针的类必须要有析构函数;不带指针的类,其拷贝构造操作可以用编译器自带的功能但是类里面有指针,则必须自己重写

int main()
{
    string s1();
    string s2("hello");
    
    string s3(s1);//拷贝构造
    cout<<s3<<endl;
    s3=s2;//拷贝赋值
    cout<<s3<<endl;
}

(2)因为string类的元素大小是不确定的,所以用指针

class string
{
public:
    string(const char* cstr = 0);//构造函数
//带指针的类,以下三个函数一定要写
    string(const string& str);//拷贝构造函数,接受自己这种类型的东西进行构造,所以是拷贝构造
    string& operator=(const string& str);//操作符重载,接受的也是自己这种东西,所以称为拷贝赋值;
    ~string();
//
    char* get_c_str() const { return m_data }
private:
    char* m_data;//指针
}

(3)字符串就是一个指针指向头,最后有一个结束符号(c&c++)。字符串的构造和析构函数为

对于没有指针的类,不需要清理,因为定义的内容会随着函数结束而自动清理掉;但是含有指针的类中,过程中做了

动态分配,占用了动态内存,不会随着函数结束而被清理,所以需要单独清理该内存,防止内存泄漏。

inline 
string::string(const char* cstr = 0)//构造函数
{
    if(cstr)
    {
        m_data = new char[strlen(cstr)+1];
        strcpy (m_data, cstr);//如果构造函数已赋值,则先分配一个大小为所赋值字符串的长度的内存加1,最后的1用来存结束符号。然后将所赋值cstr拷贝到m_data中。
    }
    else
    {
        m_data = new char[1];
        *m_data = ' ';//如果构造函数未赋值,则自动分配一个大小为1的内存用来存结束符号
    }
}

inline
string::~string()
{
    delete[] m_data;//析构函数,清理,注意delete[]的写法
}

(4)新建字符串的几种形式。动态分配内存一定要及时delete

//main.cpp
{
    string s1();
    string s2("hello");

    string* p = new string("hello");
    delete p;//动态分配内存,新建指向”hello“的变量,用完以后一定要及时删除
}

(5)类里面如果有指针,则一定要有拷贝构造和拷贝赋值!

下图中没有拷贝构造函数的b=a操作,会使得b直接指向与a同样的地址,而b原来指向的地址则成为野内容,内存泄漏,同时hello 被两个指针指向也存在危险性。

侯捷《C++面向对象编程》笔记(二)超经典-如何编出具有大家风范的类
一、三大函数:拷贝构造、拷贝复制、析构
二、堆、栈与内存管理
三、类模板、函数模板
完!!!
拷贝构造函数:

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);//拷贝构造
    s2=s1;//拷贝赋值
}

拷贝赋值函数:检测是否自我赋值 or 赋值

{
    string s1("hello");
    s2=s1;//拷贝赋值
}

inline
string& string::operator = (const string& str)//拷贝赋值
{
    if(this == &str){ return *this;}//检测是不是自我赋值,这一步非常重要,如果不写,当使用者自我赋值时会出错,原因如下图
   
     //操作步骤
    delete[] m_data;//1.先清掉自己
    m_data = new char[ strlen(str.m_data)+1];//2.构造与左边值相同大小的空间
    strcpy(m_data, str.m_data);//3.把左边值复制进来
    return *this;
}

侯捷《C++面向对象编程》笔记(二)超经典-如何编出具有大家风范的类
一、三大函数:拷贝构造、拷贝复制、析构
二、堆、栈与内存管理
三、类模板、函数模板
完!!!

二、堆、栈与内存管理

(1)所谓堆(stack)和栈(heap)

1.1 定义

stack:存在于某作用域的一块内存空间。例如当你调用函数,函数本身即会形成一个stack用开放置它所接收的参数,以及返回地址。在函数内声明的任何变量,在其所用的内存块上都取自上述stack。

heap:操作系统提供的一块global内存空间,程序可动态分配,从中获得若干区块。

class Complex{....};

{
    Complex c1(1,2);//存储在栈中,c1的存储会随着函数结束而消失.local object
    Complex *p = new Complex(3);//动态获得堆的内存,因此不会随着函数结束而消失,必须手动delete掉.static object

}

1.2 stack生命周期

a, stack objects:离开作用域就会消失,又称为auto object(指其析构函数会自动调用)

b.static local objects:生命在作用域结束之后仍然存在,直到程序结束。

c.global objects: 全局对象,生命同样在整个程序结束之后消失。

{
    Complex c1(1,2);//stack object
    static Complex c2(1,2);//static object
}

Complex m(1,2)//global object

int main()
{
    ...
    return 0;
}

1.3 heap objects生命周期

class Complex {...};

{
    Complex* p = new Complex;
    ....
    delete p;
}
//p所指的便是heap object, 其生命在它被delete之后结束

//VS

{
    Complex* p = new Complex;
    ....
}
//以上出现内存泄漏,因为当作用域结束,p所指的heap object仍然存在,但指针p的生命却结束了,作用域之外再也看不到p了,因此没办法再delete p了。

没有delete相对应的指针所指的内容,那么就会占用内存,造成内存泄漏。而消除内存泄漏,只需delete p即可,而不用delete *p。

1.3.1动态分配所得的array,到底有多大?--侯捷独家

a.调试状态下:存储一个数据的内存大小是:数据本身大小+上下cookie+灰色填充(结果不是16的倍数时要向下分配内存直至满足16的倍数)

b.非调试状态下:只存储上下cookie和数据本身大小。

侯捷《C++面向对象编程》笔记(二)超经典-如何编出具有大家风范的类
一、三大函数:拷贝构造、拷贝复制、析构
二、堆、栈与内存管理
三、类模板、函数模板
完!!!

1.3.2 动态分配下的数组array存储

array new & array delete要一起搭配。

{
     Complex *p = new Complex[3];//指针指向复数数组头
     String *p = new String[3];//数组里面有三个指针
}

侯捷《C++面向对象编程》笔记(二)超经典-如何编出具有大家风范的类
一、三大函数:拷贝构造、拷贝复制、析构
二、堆、栈与内存管理
三、类模板、函数模板
完!!!

图片说明:对于内有3个元素的数组存储,调试模式下(左1)内存分配包括3个数组元素+上下Debugger Header+上下cookie+表数量(8*3+(32+4)+4*2+4),以及不满16倍内存,自动增加内存直至为16倍数;左二是非调试模式。

1.3.3 delete[] 与 delete 的区别

delete的动作分为两步,首先调用析构函数删除变量,然后删除其内存。所以如下调用delete的时候,系统会自动调用析构函数,然后根据上下cookie给出的内存大小进行整块内存删除。因此对于数组内存储的是非指针变量的array new,delete不加[],是不会造成内存泄漏的。但是!!!如果内部存储的是指针,那么没有加[],编译器会不知道需要需要调用多次析构函数,从而造成只删除第一个数组元素及其指向的数据,而不会删除后续的。内存泄漏的部分是剩余数组内指针元素指向的部分。

.所以,为了保险起见,array new 一定要搭配 array delete[]/

侯捷《C++面向对象编程》笔记(二)超经典-如何编出具有大家风范的类
一、三大函数:拷贝构造、拷贝复制、析构
二、堆、栈与内存管理
三、类模板、函数模板
完!!!

三、类模板、函数模板

3.1  静态全局变量的作用域为单个源文件的区域;全局变量的作用域是整个工程文件的区域

(1) 静态数据:带有this pointer;

           使用场景:对于不同类名的某一属性,只能有一份,如银行系统的利率,百人都是同一利率

(2) 静态函数:没有this pointer;只能存取、处理静态数据

(3) 静态的数据在类外一定要进行定义,赋不赋初值都是可以的。

(4)调用static函数方式两种:

a.通过类名调用;(在还没有对象被建立的时候)

b.通过用户调用

class Account
{
public:
    static double m_rate;
    static void set_rate(const double & x) { m_rate = x;}
};
double Account::m_rate = 8.0;//!!!静态数据在class外一定要进行定义

int main()
{
    Account::set_rate(5.0);//调用static函数方式两种:a.通过类名调用;
    Account a;
    a.set_rate(7.0);//b.通过用户调用
}

3.2 把ctors放在private区(Singleton,利用静态实现该种设计模式)

初级版本一,缺陷:如果外界不需要A,那么已经被创建的a就会造成内存占用浪费

class A
{
public:
    static A& getInstance {return a; };
    setup(){......}
private:
    A();
    A(const A& rhs);
    static A a;//仅一份的创建
...
};

A::getInstance().setup();//通过这种方式来调用唯一的类的函数

升级版本二

class A
{
public:
    static A& getInstance {return a; };
    setup(){......}
private:
    A();
    A(const A& rhs);
...
};

A& A::getInstance()//当没有人使用A时,她就不会被创建,一旦有人使用,就会被创建,并且不会随着函数消失而消失
{
    static A a;
    return a;
}


A::getInstance().setup();//通过这种方式来调用唯一的类的函数

3.3 补充类模板,class template

template<typename T>//表明T为一个类型模板
class complex
{
public:
    complex (T r=0, T i=0):re(r), im(i) {}
    complex& operator += {const complex& };
    T real() const { return re; }
    T imag() const { return im; }
private:
    T re, im;

    friend complex& __doap1 (complex*, const complex& );
};

//用法
{
    complex<double> c1(2.5, 1.5);
    complex<int> c2(2,6);
    ...
}

3.4 函数模板 function template

template <class T>
inline
const T& min(const T& a, const T& b)
{
    return b<a? b:a;
}


class stone
{
public:
    stone();
    bool operator < (const stone& rhs) const
    { return  _weight < rhs._weight; }

private:
    int _w, _h, _weight;
};

//使用
stone r1(2,3), r2(2,3), r3;
r3 = min(r1, r2);

3.5 补充 namespace

namespace是把所有包在命名空间里,为了防止与别人定义的重名,则把你的内容包在namespace里。

using namespace std;//一次把标准库全打开。一些常见小程序用这种
{
cout<<...
}

//为了防止标准库全打开,会出现混乱,那么单独打开需要的内容
using std::cout;
{
cout<<..
}

//
{
std::cout<<...
}


3.6更多细节学习

  • Standard Library:需要好好利用
  • operator type() const;
  • explicit complex(...):initialization list {}
  • pointer-like object
  • function-like object
  • Namespace
  • template specialization
  • variadic template 
  • move ctor
  • Rvalue reference
  • auto
  • lambda
  • range-base for loop
  • unordered containers

完!!!