侯捷《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 = '\0';//如果构造函数未赋值,则自动分配一个大小为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\0被两个指针指向也存在危险性。
拷贝构造函数:
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;
}
二、堆、栈与内存管理
(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和数据本身大小。
1.3.2 动态分配下的数组array存储
array new & array delete要一起搭配。
{
Complex *p = new Complex[3];//指针指向复数数组头
String *p = new String[3];//数组里面有三个指针
}
图片说明:对于内有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[]/
三、类模板、函数模板
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
完!!!
posted on 2019-06-16 23:03 Nancy_Fighting 阅读(248) 评论(0) 编辑 收藏 举报