您现在的位置是:首页 > 文章详情

C++面向对象高级编程(上) 第二周 侯捷

日期:2018-11-04点击:386

三大函数——拷贝构造、拷贝赋值、析构函数

 

拷贝构造——接受的是自己这种东西

 

 

 

ctor和dtor构造函数和析构函数

字符串有两种:

一种是前面有一个常数,用于记录字符串的长度,此字符串的末尾没有结束符号。

另一种是字符串的末尾有结束符号,字符串的开头没有用于记录字符串长度的常数。

new就是分配内存,分配了一个字符的内存。

分配了一个字符的内存,然后把结束符传进来,这样就形成了一个空字符串

strlen是一个函数,获取字符串的长度(strlen是计算机C语言函数,计算字符串s的(unsigned int型)长度,不包括'\0'在内)你的class里面有指针,你多半是要做动态分配。所以你要在他生命结束前,调用析构函数,把内存释放掉)

 

 

 

拷贝赋值函数

如图中的红框①②③,是要把右手里面的东西拷贝赋值给左边的步骤:

a)清空左边的东西

b)申请和右边一样大的内存空间

c)拷贝

如果没有上面那句检测自我赋值( ),会出现如下情况:

检测是否为自我赋值,不仅仅是为了效率,还是为了安全性。

 

 

output 函数

为了打印类中的东西,我们要重载操作符 "<<",由于成员函数有默认this指针,如果将重载"<<"设置成成员函数,那么变量的位置要发生改变,这不符合人们的使用规范,因此,重载"<<"要设置成全局函数。

ostream& operator<<(ostream& os, const String& str) { os << str.get_c_str(); return os; } 

任何东西,只要你能直接丢给cout,你就直接丢给他输出好了。先看一下整体代码:

#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) { m_data = new char[strlen(cstr)+1]; strcpy(m_data, cstr); } else { 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); } #include <iostream> using namespace std; ostream& operator<<(ostream& os, const String& str) { os << str.get_c_str(); return os; } #endif 

注意到 get_c_str函数的返回值是char *,刚刚好可以直接给cout进行输出,因此我们写了get_c_str函数来进行输出。

 

 

 

堆栈与内存管理

 

stack object 的生命周期

 

static local object

 

global object 的生命周期

 

heap object 的生命期

 

new——先分配内存,后调用构造函数

new的动作分解:

a)调用 operator new 函数来分配内存(operator new 底层调用的是malloc)。对应的,上图分配出

b)第二个动作把我们创建的变量做一个类型转换

c)通过指针调用构造函数Complex(注意:构造函数在类里面,所以是成员函数,会有this指针。谁调用成员函数,this指针就指向谁。因此,上图中的第三步完整的写法应该是如图所示的形式:

这里的this指针指向了pc)

 

delete:先调用析构函数,再释放内存

 

 

 

 

动态分配内存块 in VC

根据上图第一个矩形(debug模式下的情况)

1、new  一个复数会获得的内存是 8 byte(上图中第一个矩形草绿的部分)。

2、在调试模式下,你会得到灰色的部分,上面每一格是4byte(即),一共 4*8=32个字节。

3、还会得到草绿色矩形下面的那一个 4byte(即

4、上下两个砖红色的矩形区域是cookie

内存一共需要  8+(32+4)+(4*2)=52,而VC给你分配的内存块一定是16的倍数(现在不提为什么),因此填补了三个深绿色的填补物pad

看起来分配很浪费,但是这是必要的浪费,因为回收的时候,操作系统需要依据这里的信息来进行回收。

 

 

 

根据上图中第二个矩形(release 模式):

分配内存8byte,加上上下cookie刚刚好16byte,因此无需调整,无需添加填补物pad。

 

上下cookie的作用

记录整块给你的内存大小,便于系统回收,让系统知道回收多大内存

让我们看一下cookie上记录的数字——000000041,对于第一个矩形,内存一共分配了64byte,64的16进制表示是40,而cookie上的数字是41,为什么呢?40借助最后一个bit,最后一位,标志我这块内存是给出去了还是收回来了。这里是给出去了,对于程序来说是获得了,所以最后一位标志位1,因此是41。

同理,对于第二个矩形,系统给出的内存是16byte,16的16进制是10,这里cookie上写的是11,因为这是程序获得的内存。

 

为什么可以借最后一个bit来标志这一块内存是给出了还是收回了?

因为分配的内存都是16的倍数16的倍数最后四个bit都是0,我们就可以借一位来表示内存的状态。

 

 

 

 

 

为什么array new 要搭配 array delete

 

分析上图中第一个矩形(debug模型下的array new分配的内存分析)

1、复数申请的数组长度是3,因此申请了三块内存(

2、在调试模式下要加上那个header,上面是32下面是4(即

3、加上向下cookIe(

4、在VC中(别的编译器不明),会用一个4byte的内存记录数组的长度,即图中的

因此内存分配为 8*3+32+4+4*2+4=72 ,凑16的倍数,所以分配内存为80。

 

 

 

 

array new要搭配array delete

否则会造成内存泄漏。让我们看看是哪一种内存泄漏

内存泄漏的是动态分配的内存。

写不写 [],清空的内存大小是相等的,因为cookie上有记录。但是不写 [] ,编译器不知道要对每一个对象进行分别的析构。析构的时候,先分别析构各个内存对象,然后再析构母体的那个地方()。发生内存泄漏的是这里(

 

 

 

 

复习string的实现过程

设计一个class,我总是要去思考我需要什么样的数据。由于不知道字符串的长度,所以大部分人设计字符串这种类中的数据都是在里面放一根指针,将来要分配多大的字符串内容,就动态地去分配字符串的大小,用new的方式去动态分配一块内存(在32位的平台里面,一根指针占内存是4字节,所以不管你里面字符有多长,字符串本身就4个字节的内存)

              

 

 

Class里面带指针,所以我要关注三个重要的函数:

拷贝构造:他是一个构造函数,所以没有返回值。他要有一个拷贝蓝本,蓝本就是他自己(传入reference是可以的,又因为我们不会改变蓝本,所以前面可以加一个const)

拷贝赋值:赋值是要把来源段的拷贝到目的端,所以涞源段的内容和拷贝构造是相同的(所以他传入的参数和拷贝构造的参数是相同的)

因为传入的值我们不打算去改变他,所以前面加一个const。

拷贝赋值的返回值(要不要return by reference,要看函数执行所返回的结果是不是放在里local object中,只要不是local object,就可以传reference)

析构函数:

辅助函数:我们希望把最后的结果丢给cout来输出到屏幕上(加了const是因为不会改变数据)

拷贝赋值函数:

涞源段拷贝到目的端,目的端是已经存在的东西,所以

第一个动作应该是把目的端的内存清空

第二个动作是重新分配一块够大的空间:

第三个动作是把来源端拷贝到目的端:

接下来要思考赋值之后的返回值(如果不写返回值的话,连串的赋值行为就会受限)

&str得到的是一根指针

String&是引用

 

 

扩展补充:类模板、函数模板以及其他

 

进一步补充:static

谁调用我,谁就是那个this pointer,所以c的地址就是this pointer

成员函数有一个隐藏的参数this pointer,但是我们不能写进去,这个是编译器帮我们写进去

静态数据:加入了static的数据,就跟对象脱离了,他不属于对象,他在内存的某一个区域单独有一份,我们不必知道他在那里,反正后面的代码能够找得到就好了

静态函数:他的身份跟一般的成员函数字内存中是相同的,我们所指的相同指的是他也在内存中只有一份。函数在内存中当然只有一份,不可能因为你创建了好几个对象,就有好几个函数

静态函数跟一般函数的差别就在于,静态函数没有this pointer。因此静态函数如果去处理数据,他只能去处理静态的数据。

例子:

 

 

进一步补充:把ctors(构造函数)放在private区

当我们希望写的class只产生一个对象的时候,可以这么用。

把构造函数写在private里面,这样外界就无法再创建对象。

这么写有个缺陷,就是如果外界不需要这个数据,这个数据依然存在,这样会造成内存的浪费。更好的写法如下:

 

 

进一步补充:cout

为什么cout可以接受任何类型的数据,因为里面重载了很多

 

 

进一步补充:类模板

模板会造成代码的膨胀,但是这并不是缺点,因为你确实是需要这种类型的函数,即使不用模板,你也要写出来

 

 

进一步补充:function template,函数模板

类模板在用的时候要明确指出类型 ),函数模板则不需要,因为编译器会做实参的推导(argument deduction)

 

 

进一步补充:namespace

namespace等同于你把你的东西都封锁在这个命名空间里了,这样就不会打架。

Using directive(使用命令):等同于你把封锁打开,调用的时候就用写全名(e.g std::cin)了,可以直接写cin

Using declaration:一行一行的打开,不是全开,因为里面东西可能会很多

或者是都不打开,就每一步都规规矩矩的写全名

原文链接:https://yq.aliyun.com/articles/681152
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章