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

简介: 三大函数——拷贝构造、拷贝赋值、析构函数 拷贝构造——接受的是自己这种东西   ctor和dtor构造函数和析构函数字符串有两种:一种是前面有一个常数,用于记录字符串的长度,此字符串的末尾没有结束符号。

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

 

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

 

 

 

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:一行一行的打开,不是全开,因为里面东西可能会很多

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

相关文章
|
2月前
|
存储 C++ UED
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展
本文介绍了如何通过四步实现C++插件化编程,实现功能定制与扩展。主要内容包括引言、概述、需求分析、设计方案、详细设计、验证和总结。通过动态加载功能模块,实现软件的高度灵活性和可扩展性,支持快速定制和市场变化响应。具体步骤涉及配置文件构建、模块编译、动态库入口实现和主程序加载。验证部分展示了模块加载成功的日志和配置信息。总结中强调了插件化编程的优势及其在多个方面的应用。
317 65
|
1月前
|
消息中间件 存储 安全
|
2月前
|
安全 程序员 编译器
【实战经验】17个C++编程常见错误及其解决方案
想必不少程序员都有类似的经历:辛苦敲完项目代码,内心满是对作品品质的自信,然而当静态扫描工具登场时,却揭示出诸多隐藏的警告问题。为了让自己的编程之路更加顺畅,也为了持续精进技艺,我想借此机会汇总分享那些常被我们无意间忽视却又导致警告的编程小细节,以此作为对未来的自我警示和提升。
211 8
|
2月前
|
存储 搜索推荐 C++
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
55 2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
|
2月前
|
安全 程序员 编译器
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
88 11
|
2月前
|
存储 C++ 容器
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器1
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
55 5
|
2月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
48 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
2月前
|
算法 编译器 C++
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
84 2
|
2月前
|
存储 编译器 C++
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
42 2
|
15天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
26 2