(Boolan) C++面向对象高级编程(二)
之前写到了关于不带有指针的class的设计思路和注意事项,但是对于C/C++语言来说,还有一个非常重要的概念就是指针,为什么将指针作为class设计的分界点呢?那么,之前我讲了一个关于猫ヽ(=・ω・=)丿的故事,那么今天我再来讲一个关于国王和宝箱的故事吧 <( ‵□′)───C<─___-)||。
关于 指 针(钥匙)的一个小故事
语文老师教导,讲故事需要先说明时间地点人物发生了啥:
话说很久很久以前(发现词语好匮乏、、、),在遥远的国度有一个国王(突然想起了和尚),他有一件十分心爱的具备魔法的宝物,每次祭奠、仪式都会用到这个宝物的魔力。
而这个宝物由十个部件组成,每次只有将十个部件全部拼接在一起才会发挥他的魔力(少了任何一块都不行(¬_¬))。由于国王爱这个宝物爱的死去活来的,为了防止他过度沉溺在宝物的喜爱中无法自拔而耽误朝政((=。=)其实我想说,到底啥宝物这么有吸引力。。。故事设定。。。(▼皿▼#) ....就这样吧)。
国王的爹爹(居然没死╮(╯_╰)╭)将这个的碎片分别锁在了十个宝箱里面(ヾ(゚∀゚ゞ)这个故事背景我实在快编不下去了.......)。。。而每个箱子都对应一把独特的钥匙(废话。。。。。一把钥匙开一把锁━━( ̄ー ̄*|||━━),特殊之处在于
***这 个 钥 匙 不 但 可 以 打 开 箱 子 还 可 以 记 录 箱 子 在 哪 里 , ****。而国王的爹爹把这十把钥匙分别交给了十个大臣来保管,也就是只有当国王召集了十个大臣以后才能够一睹宝物的真面目。
ps: (● ̄(エ) ̄●)背景就交代在这吧,虽然不合逻辑的地方很多,但是为了说清楚这个事,也就当作合理吧。
**** 那 么 问 题 来 了 !!!****( ⊙ o ⊙ )
等到国王的爹爹死了,国王也长大了不少,决心要做一个好的国王,已经不那么迷恋那个宝物了。但是每次在使用宝物的时候,国王发现实在太麻烦了,每次都要十个大臣全来了才能取出宝物的每个部件。有好几次急用的时候(,,#゚Д゚),大臣由于各种原因不能来把要是交给国王。还有的时候,十个大臣都来了,但是谁也不知道该开哪个箱子,每次都要一个个是试也是很大无时间的。
***有 没 有 办 法 能 帮 助 国 王 来 管 理 他 这 些 箱 子 呢 ( ▔•з•▔ )?****
这个 精彩绝伦(漏洞百出)的故事讲完了,但是这里面其实引出来的问题值得我们探讨一下的。方法太多了,我来简单说说几个拙见吧。
-
方法一:
将十个宝箱和每个大臣手里面的钥匙按照对应关系编号[0-9],至于摆放顺序其实无所谓了,每个箱子上都有自己的编号,每次大臣来齐了就能很快的打开所有的箱子了。(好像和之前比只是加上了序号) -
总结:
1 能够方便快速的打开箱子(真的快速吗?!难道箱子是乱放的,难道不需要找吗)
2 但是每次打开箱子都必须十个大臣到场,如果少了其中某个就不能打开全部箱子了(而且每次去开箱子的时候都需要所有大臣在场,否则,去了五号箱子那,发现其余大臣都在,唯独五号大臣不在,就抓瞎了) -
方法二:
还是先将箱子和钥匙进行编号,然后将9号箱子的钥匙以及他所在位置放在8号箱子中,将8号箱子的钥匙以及他所在位置放在7号箱子中......将1号箱子的钥匙以及他所在位置放在0号箱子中。
完成了钥匙和位置的分配,现在只需要一个大臣拿着钥匙就能开启所有的箱子啦~ ~ ~ ~ ~ ~- 总结:
1 只需保存一个箱子的钥匙就可以管理所有的10个箱子啦(听起来不错,但真就这么简单吗?!)
2 不需要担心钥匙和箱子位置具体在哪的问题(真的吗?!)
3 拿着钥匙的那个大臣权利集中,一旦他丢了钥匙,所有箱子都无法打开
4 如果中间某个箱子的位置或者钥匙放错了或者忘记了放进去~~~~那么后续的箱子就等于丢了(拿到了第一把钥匙也打不开后面的锁了、、、、)
5 假设某一天国王想看看第十个箱子里面的东西的时候~ ~ ~那就麻烦了,得把前面几个箱子都打开才行(十个箱子好像好像还好,那假设有十万个或者十亿个箱子呢?他也许就只为了看看最后一个箱子就变成了一个开了一辈子箱子的国王(为啥突然想起来做木匠活的皇帝....))
6 如果某天国王想要在这群箱子中插入个箱子,只需要管理需要插入位置前后的箱子就行了(这个优点今天可能用不到(  ̄ー ̄)(  ̄ー ̄)就先放着吧)
- 总结:
-
方法三:
将十个箱子并排放在一起,[ 0 ][ 1 ][ 2 ][ 3 ]....[ 9 ],箱子之间会形成自然顺序。将下个箱子的钥匙放在上个箱子上(这时候不需要保存位置啦,因为就在他旁边嘛)。也就是将9号箱子的钥匙以及他所在位置放在8号箱子上,将8号箱子的钥匙以及他所在位置放在7号箱子上......将1号箱子的钥匙以及他所在位置放在0号箱子上(注意没有放在箱子里面!!!)。- 总结
1 箱子的位置不用担心了,都在这一排摆着呢(但是需要考虑的是万一、、、没有地方放下这么多连续的箱子可怎么办,也就是对空间的要求更高了,十个箱子好像还好~~但是加入十万个、十亿个箱子呢?)
2 保存一个钥匙就可以管理全部的箱子了,也存在权利过分集中的问题,如果,外面的那把钥匙丢了、、、、箱子全都报废了
3 如果这时候国王想要看看其中某个箱子,那么只需要按照序号去看看就行了,毕竟钥匙就放在之前的箱子上面(如果这里是十万个箱子,国王也可以不费劲的从里面找出他想要的那个~ ~ ~ ~)
4 突然某一天国王想插入一个箱子,如果箱子是插在最后没啥问题,很容易,但是、、、、如果国王想要插入的位置是第一个,那么国王得把所有的箱子往后诺一个位置才行,如果是十亿个箱子呢?又出现了个挪了一辈子箱子的国王
- 总结
-
你TM弄了这么久箱子!!(# ゚Д゚)到底想干嘛(# ゚Д゚)?!
其实这个故事虽然存在很多不合理的地方,但实际是想做一个关于*** 数 据 结 构*** 的比喻。
- 每个箱子里面保存的“宝藏”,就相当于——数据
- 箱子以及箱子所处的位置,就相当于——内存
C/C++毕竟是*** 直 接 面 对 内 存 ***的语言,所以程序员难免要考虑这些“箱子们和钥匙们” 的摆放和打开的问题###
***还 有 就 是 ,这 些 管 理 方 法 后 面 肯 定 是 有 用 的 ~ ~ ~ ~ ~ ~, 就 先 放 在 这 里 , 我 之 后 再 提 ***
回到之前的一个问题,为什么类要用带有指针和不带指针来进行区分呢???
那么假设你是国王,你希望自己的箱子被打不开或者被找不到吗?
对于带指针和不带指针的class最大的区别就是是否需要管理这些放在箱子里面的“宝藏”!!!如果管理不好,那么会出现丢失找不到的情况,但是宝藏其实就在那里只是我们再也找不到了,这时候,被丢失的宝藏就会出现一个问题,它会占用那些被我们找不到的空间。就像一个箱子的钥匙丢了,箱子里面全是好东西,但是我们拿不到,也砸不开那个箱子,但是他还在你家里面占了很多空间。。。。所以对于持有指针的class来说,手上拿到的指针,其实就相当于那把钥匙。管理钥匙的本质是为了管理箱子里的东西
class中带不带指针的问题关系着背后的数据(宝藏)该如何管理的问题
今天会用简化版本的string.h作为例子来说明带有指针的class应该如何设计,其实string相当于之前的方法三的管理方法,将所有的字符(char)在内存中有一排连续的空间(数组),将每个字母放在连续的空间中。就相当于国王的十个箱子里面分别一个字母组成[ h ][ e ][ l ][ l ][ o ][ w ][ o ][ r ][ l ][ d ]的一串字符串。这些“箱子”(内存单元)是连续的排列的方式组合在一起。这时候,我们只需要拿着[ h ]这个箱子的钥匙,就可以打开后面全部的箱子了,同时也可以根据每个箱子的位置来打开其中的箱子,取出其中的数据了(比如取出hello的[ o ],他位于这一排箱子的第五个位置,如果从0号开始排序,那么他就是相当于这串箱子的4号了,我们只需要拿着这个4号就能去走这个o了)。
(后面该适当严肃点了,但还会呼应之前的故事,来解释 我想解释的内容 (´◊ω◊`) )
带有指针的class需要解决的Big Three问题
class String
{
public:
//构造函数,接收一个字符串数组,通过字符串数组生成一个字符串(cstr相当于保管起来的那个钥匙)
String(const char* cstr=0);
//构造函数(拷贝构造,对于含有指针的class尤其要注意的)
//接收参数为一个String本身,也就是通过接收String本身来构造一个新的String对象
String(const String& str);
//操作符重载,(拷贝赋值,对于含有指针的class尤其要注意的)
//操作符左右两侧均为String对象,也就是将一个String对象赋值给另外一个已有的String对象
String& operator=(const String& str);
//析构函数,对象死亡之前会默认调用的函数
//防止指针失效了,但实际的数据还存在在内存中,无法拿到,也无法改变和删除
//会造成内存泄漏,在对象死亡前需要再次管理那块内存空间
~String();
//获取私有属性,获取时没有改变内容,需要注意const的修饰(找钥匙的方法)
char* get_c_str() const { return m_data; }
private:
char* m_data; //相当于管理这一串宝箱的第一把钥匙
};
-
析构函数
-
析构函数是在对象生命周期的最末尾也就是对象即将被销毁的时候进行调用的函数,主要是为了方便清楚带有指针变量的对象,能够在对象清楚之前,清理其指针所指向的那部分内存空间,以避免造成必要的内存泄漏。
inline String::~String() { //删除字符串的内存空间中保存的内容,释放内存 //字符串本质上为字符数组,所以再删除的时候,需要使用delete[] delete[] m_data;
}
-
-
复制拷贝问题
- 与不带指针的对比
- 对于不带指针的class
- 编译器会自动帮我们实现拷贝构造和拷贝复制两种方法
- 并且编译器会依照内存中所保存的数据,依照每个byte进行拷贝,相当于两份完全相同的数据
- 如果能够满足需求不需要重写这两种方法
- 对于带有指针的class
- 编译器会默认拷贝指针变量(配一把钥匙),但实际指针所指向的数据并未被复制一份,导致两个指针同时指向一个内存空间(两把一样的钥匙开的同一个箱子,但是实际箱子里面的内容依然只有一份,不要假装骗自己说配了两把钥匙就拥有了两份数据的道理相同)
- 如果使用了赋值符(A = B)的时候,相当于将钥匙A的钥匙改造成钥匙B,但是对于A原始可以管理的那个箱子就再也没办法打开了,而A和B可以同时管理同一个箱子,没办法实现通过赋值符来实现赋值一个和B相同的箱子。为了避免这个问题,实现deep copy,必须重写赋值符左右相同都是自己本身类型的操作符
- class中持有指针变量,那么对于赋值过程需要考虑重写拷贝构造和拷贝赋值
- 对于不带指针的class
- 拷贝构造(Copy Constructor)
-
通过传入和自己本身类型相同的对象,来构造一个新的自己类型的对象
String s1("hello"); String s(s1); //第一种写法,构造一个新的String对象s,初值为s1中的内容 String s = s1; //第二种写法,构造一个新的String对象s,初值为s1中的内容
-
拷贝过后会生成一个新的对象(s)
-
- 与不带指针的对比
inline
String::String(const String& str) //参数就是自己类型本身,可以使用引用,因为传入的数据不会被改动,需要使用const修饰
{
//因为是新创建的String对象,所以直接申请足够的内存空间,将内容拷贝过去即可
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data); //str.m_data:相同类型的对象互为友元,可以直接读取内部的private属性
}
- 拷贝赋值(Copy Assignment Operator)
-
将一个对象,复制给另外一个** 已 有 **的对象
-
思路:先将原对象中的内容清空,再将内容进行复制
{ String s1("hello"); String s2("hello"); s2 = s1; //将s1的内容复制给s2,s2在复制之前实际是一个已存在的对象 }
-
函数定义
inline
String& String::operator=(const String& str)
{
//自我赋值检查
//作用:
//1. 如果将s赋值给s,则无需操作,那么可以直接通过return来跳出该函数,减少多余操作,提高效率
//2. 如果此处不做判断,直接执行后续代码,会在复制之前先释放等待接受内容的指针所指向的空间,
// 因为操作符左右相同,会造成将被复制的对象那么部分给释放,最终破坏原始的数据
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.h的完整代码
#ifndef __MYSTRING__
#define __MYSTRING__
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;
};
#include <cstring>
//通过字符串数组来构造一个字符串对象的构造函数
inline
String::String(const char* cstr)
{
//需要对传入的参数进行检查是否有内容
if (cstr) {
//需要针对传入的字符串数组的长度(strlen(cstr)来获得),但字符串后需要放置一个结束符/0,所以长度需要+1
m_data = new char[strlen(cstr)+1];//在堆内存上,申请一块长度比传入字符数组长1的字符数组的内存空间
strcpy(m_data, cstr);//使用strcpy函数将传入的参数cstr的所指的内存中的数据,赋值到新申请的内存空间上去,使两块内存空间都同时具备相同的数据
}
else { //未指定初值的情况
//虽然传入的部分为空,但是由于在C/C++中,管理字符串是以\0作为结尾的,即使是空字符串实际并不完全为空
//所以在需要构造长度为一个单位的字符串数组,并在其中放入结束符/0
m_data = new char[1]; //申请空间
*m_data = '\0'; //保存结束符
}
}
inline
String::~String()
{
delete[] m_data;
}
inline
String& String::operator=(const String& str)
{
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;
}
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
//输出,<<符号重载,不能为成员函数,因为操作符左侧为ostream
#include <iostream>
using namespace std;
ostream& operator<<(ostream& os, const String& str)
{
os << str.get_c_str();
return os;
}
#endif
内存管理初探
-
堆(heap)和栈(stack)
- 栈
- 栈,是存在于某作用域(scope)的一块内存空间。例如,当你调用的函数,函数本身就会形成一个栈,用来存放所接收的参数,以及返回地址。在函数本体内声明的任何变量,其所使用的内存块都在栈上。
- 栈,其实是一种特殊的数据结构,他的特性是先入后出(后入先出),就像一摞放在桌子上面的碟子,我们清洗好是依次往上摞在一起的,而最先洗好的盘子位于最底下,我们取盘子的时候一般都是先从顶部直接拿就行了,所以造成了后洗好的盘子最先被取出来,也就是先入栈的,后出栈。对于函数来说无论接收的参数还是创建的参数,都是最先得到的最后才出栈(被释放,也就是对象死掉),后得到或创建的先出栈。
- 特点:
1 对于操作系统来说,程序员不需要管理栈上的变量,全部交给系统调度和处理,效率高
2 使用简单,但空间较小,每个程序、函数都有自己的栈,相对安全性高
- 堆
- 堆,是指有操作系统所提供的一块全局的内存空间,程序可以动态分配(dynamic allocated)从中获得若干区块(blocks)。
- 特点
1 系统共用区域,不同的程序都可以访问同一块区域,不释放内存空间会造成内存泄漏
2 需要自己手动来申请,同时也需要手动来释放,如果不释放,当处在栈中的俄变量被弹出,则这部分空间丢失,会造成内存泄漏(钥匙丢了找不到箱子的情况)
- 栈
-
对象的创建(TypeName代表类型名称,比如自定义类型String等等,params...代表传入的参数)
- 方式一:
TypeName t(params...);
- 所占用的空间来自于栈
- 方式二:
TypeName *pt = new TypeName(params...);
- 实际该语句所需要的空间来自栈和堆两部分,其中,实际存储的数据的内存空间来自于堆,而变量pt的保存空间实际是在栈的,而存在栈里面pt由于是一个指针,它记录了所申请的堆中空间的地址(也就是箱子的位置)。由于pt的存储空间为栈,所以当t出了所在的作用域之后,t会被栈弹出,而堆上的内容并不会被释放,并且永远再没有机会被delete了。如果在弹出前,未释放堆上的那部分空间,则就造成了内存泄漏了(其实内存泄漏就相当丢了钥匙的宝箱,知道他的存在,但是再也没办法拿到了,占用了不必要的系统资源,宝箱的花就相当于丢了自己的钱!!但明知这部分钱是自己的,但是你就是没办法找到和证明了!!!)。
- 方式一:
-
静态对象(static object):这个对象其实就是一个"超级宅男 "。一直被保存在内存中的静态区(用于保存全局(global)和静态(static)变量的内存区域),并且被static所修饰的对象,声明将会超过整个作用域(scope),毕竟他不在栈中。他的生命周期会贯穿程序的全生命周期。
-
全局对象(global object):定义在***任何 *** 作用域({ ... })之外的变量。他和静态对象一样保存在内存的静态区部分,他的生命周期也是在整个程序结束后才会结束。
-
new关键字:先分配memory,再调用Constructor
-
TypeName *pt = new TypeName(params...);
的编译器处理过程
1void *mem = operator new (sizeof(TypeName)); //分配所需的内存,operator new的内部调用了malloc(n)来分配空间
2pt = static_cast<TypeName *>(mem); //类型强转
3pt->TypeName::TypeName(params...); //调用class所对应的构造函数
-
-
delete关键字:先分配disconstructor,再释放内存
-
delete pt;
的编译器处理过程
1TypeName::~TypeName(pt); //调用析构函数
2operator delete(pt); //释放内存,operator delete内部调用了free(pt)
-
-
动态分配内存的结构:所分配空间的大小都是16byte的倍数
- debug模式
- cookie (4byte)
- debuger header(4*8byte=32byte)
- 对象所需的空间(n byte)
- no man land (4byte)
- pad区域(填补区域,当总内存不为16byte倍数时,不足为最近16byte倍数所占用的空间)
- cookie(4byte)
- release模式
- cookie (4byte)
- 对象所需的空间(n byte)
- pad区域(填补区域,当总内存不为16byte倍数时,不足为最近16byte倍数所占用的空间)
- cookie(4byte)
- cookie部分保存参数的意义
- 例如 cookie中保存为0x00000041:64byte的十六进制为0x40,则0x00000040表示这部内存空间的大小,而最低位1表示程序获得内存(系统给出的内存),以此来作为标识告诉系统这块空间的大小和状态,同样cookie也就变成了0x00000041。为了方便cookie能够使用十六进制最低位表示状态,高位表示控件大小,则内存控件需要时16byte的的倍数,因为只有16byte的倍数十六进制数打的最低位才为0,也就是16(DEC) = 0x10(HEX)
- 对于数组的动态分配的内存结构
- debug模式
- cookie (4byte)
- debuger header(4*8byte=32byte)
- 记录保存的对象个数(4byte)
- 对象1所需的空间(n byte)
- 对象2所需的空间(n byte)
- .....
- 对象n所需的空间(n byte)
- no man land (4byte)
- pad区域(填补区域,当总内存不为16byte倍数时,不足为最近16byte倍数所占用的空间)
- cookie(4byte)
- release模式
- cookie (4byte)
- 记录保存的对象个数(4byte)
- 对象1所需的空间(n byte)
- 对象2所需的空间(n byte)
- .....
- 对象n所需的空间(n byte)
- pad区域(填补区域,当总内存不为16byte倍数时,不足为最近16byte倍数所占用的空间)
- cookie(4byte)
- debug模式
- 由于分配的内存的结构,所以导致new [],要搭配对应delete[]来释放空间,否则会造成内存泄漏,对应的delete[]会调用析构函数多次,可以将需要释放的内存都释放掉
- debug模式
补充部分:
- static关键字
- static 和non-static
-
非静态对象:在内存中会创建多份
-
静态对象:仅在静态区创建一份
-
非静态成员函数
complex c1; cout << c1.real(); //调用complex中成员函数real()返回对应的实部 //实际相当于cout << complex::real(&c1); //调用该函数的对象c1的地址相当于成员函数中的this,传入到该函数中
-
静态的函数(无this)
- 由于静态的函数中没有this,所以只能被处理静态的属性
-
- static 和non-static
class Account{
public:
static double m_rate;
static void set_rate(const dounle& x){m_rate = x;};
};
double Account::m_rate = 8.0; //对于静态数据必须在函数外进行定义,设置不设置初始值都可以
int main()
{
Account::set_rate(5.0); //通过class name调用
Account a;
a.set_rate(7.0); //通过object调用,对象的地址不会将a的地址作为this传入函数中
}
-
Singleto的设计模式(将构造函数放在private中)
-
懒汉模式:先创建好对象,需要时直接返回该对象即可
class A { public: static A& getInstance(){ return a; } setup(){....} private: A(); A(const A& rhs); static A a; ... }
-
饿汉模式(Meyers Singleton):在调用的时候才创建所需要的对象,由于是静态,则内存中式中只有一份所需要的对象
class A { public: static A& getInstance(){ return a; } setup(){....} private: A(); A(const A& rhs); ... }; A& A::getInstance() { static A a; return a; }
-
-
class template,类模版
template <typename 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& __doapl(complex *, const complex *); };
{
complex<double> c1(2.5, 1.5);
complex<int> c2 (2, 6);
} -
function template,函数模版
class stone { public: stone(int w, int h, int we): _w(w), _h(h), _weight(we){} bool operator< (const stone & rhs) const {return _weight < rhs._weight; } private: int _w, _h, _weight; }; template <class T> inline const T& min(const T& a, const T& b) { return b<a? b: a; } ..... { stone r1(2, 3), r2(3, 3), r3; //无需明确指出,编译器对function template进行参数推到(argument deduction) //参数推倒结果,T为stone,于是替换所有的T为stone,调用stone::operator< r3 = min(r1, r2); }
-
namespace:相当于一块封闭的空间
namespace std { .... }
-
using directive:相当于打开全部的封锁
#include <iostream.h> using namespace std; int main() { cin << ...; return 0; }
-
using declaration:只是用某一条的声明
#include <iostream.h> using namespace std::cout; int main() { std::cin << ...; cout<<...; return 0; }
-
-
不使用using namespace
#include <iostream.h> using namespace std::cout; int main() { std::cin << ...; std::cout<<...; return 0; }