极客班C++面向对象高级编程(上)第二周笔记
Classes的两个经典分类
Class without pointer member(s)
complex
Class with pointer member(s)
string
带指针的class
String class
它的Header的结构它将实现的功能:
三种构造方式:
无初值的、有初值的 和 拷贝构造。
两种操作符重载:
“<<”(输出到“cout”) & “=”(拷贝赋值)
Big Three 三个特殊函数
四个主要函数:
1.默认用指针构造;
2.拷贝构造,接受的是自身的类型的对象;
3.拷贝赋值,只有类带有指针,一定要包含有这个函数;
4.析构函数,当以这个类创建的对象,即将死亡的时候,它就会被调用。
其中2,3,4 被称为 Big Three .
ctor 和 dtor (构造函数 和 析构函数)
构造函数 ctor :
字符串会以头指针的形式传入,并以‘\0’为结尾;因此构造函数要适应不同长度的,所以用‘new’分配一块大小适合内存。
析构函数 dtor:
用于清理,清理动态分配的内存,不然就是内存泄漏了,用 delete[] 关键字释放动态分配的内存;
在调用的示例中:
指针p指向动态分配的内存,所以用 delete 释放内存;
当离开作用域的时候,会调用3次析构函数,包括以new动态分配的String对象的析构函数。
class with pointer members (带有指针的类)必须有 copy ctor (拷贝构造函数) 和 copy operator= (拷贝赋值)
如果不写拷贝赋值函数,而是直接使用原来‘=’把a的地址复制到b里去,b只会指向a所指的内存,原来所指的内存也将会丢失(造成内存泄漏),这叫浅拷贝,不是我们想要的结果,我们要的是把内容拷贝过来的深拷贝。
在设计中应该避免有别名出现,即多个指针指向同一块内存。
copy ctor (拷贝构造函数)
copy assignment operator(拷贝赋值函数)
经典的写法
① 释放原来的内存
② 重新申请一个足够大的内存
③ 将内容拷贝进来
然后,在最前面检测自我赋值的情况;如果出现自我赋值,而又没有检测自我赋值,这样会因为内存被释放而丢失原来的内容。
(这里的this是指向调用者的,而且这个函数它还是成员函数,所以可以直接更改)
output函数
operator<< 函数一定是全局函数,如果它是成员函数的话cout就要在右边,因为成员函数的左边一定是自身类的指针 *this
所谓stack(栈) ,所谓heap(堆)
Stack,是存在于作用域(scope)的一块内存空间(memory space)。例如当你调用函数,函数本身即会形成一个stack用来旋转它所接收的参数,以及返回地址。
在函数本体(function body)内声明的任何变量,其所使用的内存块都取自上述stack。
Heap,或调system heap,是指由操作系统提供的一块global(全局的)内存空间,程序可动态分配,从中获得若干块。
c1所占用的空间来自Stack;当离开作用域时,它的生命自然结束;
Complex(3)是个临时创建的对象,占用的空间是以new动态分配非得,并由heap提供;它必须需要手动delete掉。
关于生命期
stack object:
c1便是,其生命在作用域结束之际结束。又称为auto object,因为它会被[自动]清理。
static local object(静态对象):
c2便是所谓static object,其生命在作用域结束之后仍然存在,直到整个程序结束。
global object(全局对象):
c3便是所谓global object,它的生命在整个程序结束之后才结束。也可以把它被视为一种static object,其作用域是[整个程序]。
heap objects:
p所指的便是heap object,其生命在它被deleted之际结束。
在创建时,我们得到的是一个指针p,所以我们应该delete指针p,同时会调用被删除对象内的析构函数。
如果没把heap对象 delete掉,就会出现内存泄漏(memory leak),即是程序失去对内存块的控制(内存块再也找不回来了)
以上便会出现内存泄漏,因为当作用域结束,p所指的heap object仍然存在,但指针p的生命却结束了,作用域之外再也看不到p(也就没机会delete p)
动态分配所得的内存块(关于new和delete)
new其实也是一个函数
new调用时,先分配memory,再调用ctor
以上便是编译器将new转化的三个动作,实际上调了malloc()函数分配内存。
delete:先调用dtor,再释放memory
编译器转化的两个动作,它里面也调用了free()函数。
动态分配数组对象
以上的用法叫做array new和array delete;
array new一定要搭配array delete,因为delete只是会删除指针所指的内存,数组内的其它内存就会因此而丢失(内存泄漏)。
(注:动态分配的内存区块中,头尾也会有一个称为cookie标识的内存块,标识中用16进位表示状态,结尾为1表示“分配”,并且动态分配的内存区块一定是16的倍数)
扩展补充
关于static
一般的成员函数都会有this point
以上是从编译器角度看到的成员函数的调用。
如果一个数据在每个同类的对象都是相同的,这时候它就是应该变成static数据,在它的声明前加上static便是;
static数据要在class body外定义,上面黄色部分便是。
static函数:它只能处理static的数据,也没this point;它可以通过object调用(通过已经创建的对象调用),也可以通过class name调用(直接修改类的static数据,作用于所有通过这个类创建的对象)
关于把ctors放在private区
之前说过这种把构造函数放在private中的类,叫做Singleton(单体)
上面的便所谓的Singleton,因为它不能被外境构造,所以它须要在自身内被构造,并且一定是static的;
因为static函数可以通过class name调用(上面的小框便是),通过getInstance()调用‘a’,这样就解决了不能调用的问题。
但就是因为static的数据一被定义了就会直到整个程序结束,它的生命才会结束;
用上面的写法可以解决static object不被使用也一直存在的问题。
关于cout
cout属于右上角的class,而这个类型继承自ostream,所以它也是属于ostream。
从ostream里可以发现,它做了好多种operator<<的重载,这正是cout可以接受多种类型数据的原因。
class template(类模板)
如果在设计没有确定类里面的参数在使用时的类型,可以通过类模板在使用时再确定它们的数据类型。
写法如上:在类的前面声明T是一个关键字;用T替换参数类型的位置。
图中左下角便是它的调用方法。
function template(函数模板)
用法:正如上面的比大小函数,为了适用于所有不同类型的对象,所以它采用了函数模板,代替未知的对象类型;它的声明和类模板相似,要在函数之前用加上黄色的语句。
(使用这种方式时,不要忘了还要对相应的操作符进行重载)
namespace
它的作用是将它里面的东西包装起来,防止和其它人重名。(std是指标准库,标准库的所有东西都被包在std里)
用法:
最简单的用法就是using directive(全开)(写法如上),这样在下面的所以标准库的函数都不用写命名了(cin和cout的全名是std::cin和std::cout)。
using declaration(逐个打开‘声明’)
正如上面的,它只声明了cout,所以就是只有cout不用写全名。
更多拓展,此课程不再详述
Composition(复合),表示has-a
表示queue(队列,先进先出)包含deque(两端都可以进出的队列);
queue拥有deque的所有功能,这样便是所谓Composition,而queue自身没有实现功能只是改变功能的名字,这类情况叫做Adapter(改装);
Adapter:适用于已经存在一个类能实现所需的功能时,只是有一些情况不同(可能是接口不一样-函数名不同),它是复合中一种特殊的情况
Composition类的大小要加上它所包含的所有类的大小
Composition关系下的构造和析构
当Container包含Component时:
编译器会在Container的构造函数名之后,自动加上Component的默认构造(‘ : Component() ’),这便使它先要执行所含类的构造函数(如果需要调用所含类的其它构造函数,需要自己写上)然后才执行自己;
编译器也会在Container的析构函数中加上所含有类的析构函数,且是先执行自己。
Delegation (委托) . Composition by reference
这里是通过指针,指向功能实现的类,而自身却只是一个对外的接口,所有功能的实现都是通过指针委托‘功能实现类’完成的,这样它便有了高度的弹性(可以改变它功能,而不影响整体,也可以方便地增加它的功能)
这样的写法称为pimpl(private implementation),左边的是Handle,右边的是Body,其主要作用是解开类的使用接口和实现的耦合
(这里用到是指针,但为什么也叫by reference而不是by point?因为业界中没有by point这个说法,by point也叫by reference)
Inheritance(继承),表示is-a
(在C++中,struct其实是一种class)
语法:就是加黄色的一行;上面的是父类,下面的是子类(子类继承父类)。
C++的继承拥有三种方式,分别是public / private / protected,其中最重要的public。(而Java中只有public)
子class拥有自己的part(成份)同时,还涵盖了父class的prat。
图示,用空心三角形表示继承。
Inheritance(继承)关系下的构造和析构
与Composition的关系相似,但前提是父类中的构造函数一定要是虚函数(后面会解释虚函数),否则不会出图中下面是两个动作
Inheritance(继承)with virtual functions(虚函数)
父类的成员函数分三种类型:非虚函数、虚函数、纯虚函数。
虚函数的语法:在非虚函数之前加virtual。
非虚函数,子类不能重新定义,在父类中定义好,供子类使用。
虚函数,在父类中定义,子类也可以重新定义它。
纯虚函数,在父类中声明,在子类中定义。
Template Method(模板方法):二十三种设计模式之一。
示例中,通过子类的对象调用父类的函数,当调用虚函数时,编译器会检查子类是否有重新定义。
Inheritance + Composition关系下的构造和析构
在第一种情况中,Base part和Component part它两的构造的先后是顺序,而析构则是逆序的。
在第二种情况中,与之前的相类似,不多说。
Delegation(委托) + Inheritance(继承)
左边委托右边,右边作为父类,可以被继承。
这样的写法可以使左边被创建,内容可以被多个继承右边的子类观察。(就像一个文件在同一个软件里被打开,但有不同的查看窗口)