文章目录
1.头文件与类的声明
c语言中,data和函数都是分别定义,根据类型创建的。这样创建出的变量,是全局的,会有很大影响。
cpp中,将数据data和处理数据的函数都包含在一起(class),创建出一个对象,即为面向对象。数据和函数(类的方法)都是局部的,不是全局的。
class的两个经典分类:无指针成员的类(complex,复数)、有指针成员的类(string)。
如果include过了,就不要在声明了。防卫式声明,如果没定义这个名词,那么就定义一下。ifndef+define。(这样如果程序是第一次引用它,则定义,后续则不需要重复定义,不需要重复进入下面的过程)
1是要写的类的声明,
2是要写类的具体定义,
写12的时候发现有一些东西需要提前声明,写在0处。
模板类型,这里用符号T表示。
这里的意思是,因为实部和虚部的类型不确定,可能是double、float、int,定义起来比较费劲。我自己定义一个模板类型叫做T来满足这个要求。
将T作为一个类型参数来传入,在调用的时候就可以指定类型了。
通过在定义类的前面加入一行代码template来实现。
2.构造函数
- 定义类的时候,可以直接在body中定义函数(inline函数,在body中定义完成),也可以只是在body中声明函数。
如果在外头定义就不是inline - inline内联函数。如果定义的函数是内联函数,那么会比较好,运行比较快,尽可能定义为内联函数。
- 在body外,通过inline关键字来指定该函数为inline函数。
- 注意的是,上面所有的inline函数,都只是我们指定的,希望它为inline,具体是不是,要看编译器来决定。
数据应该被定为private,这样外界看不到。函数应该定义为public,被外界使用。
- 通过构造函数来创建对象。会自动调用构造函数进行创建。
- 构造函数名称需要与类的名称一样。函数的参数可以有默认参数。构造函数没有返回类型。
- 注意,不要使用赋值的方法来写构造函数,使用构造函数的特殊的方法来写,更规范。使用初值列、初始值。
1.构造函数可以有很多个,可以重载。但是上面的12两个构造函数冲突了,右面的调用方式对两个构造函数都适用,冲突。
2.同名的函数可以有多个,编译器会编成不同的名称,实际调用哪个会根据哪个适用。
1.通常构造函数不要放在private中,这样外界没法调用,也就无法创建对象。
在设计模式Singleton(单体)中,将构造函数放在了private中。这个class只有一份,外界想要调用的时候,只能使用定义的2.getinstance函数来取得这一份;外界无法创建新的对象。
3.参数传递与返回值
1.定义函数的时候,函数名后面➕const,对于不会改变数据内容的函数,一定要加上const。
2.对于上面右侧调用方式,我们创建一个常量复数然后输出实部虚部,**如果上面real和img函数定义的时候,没有加const,那么这里函数默认的意思是可能会改变数据,与我们的常量复数就矛盾了。**编译器会报错。因此,对于不会改变数据内容的函数,一定一定要加const。
- 参数传递,传递value是把整个参数全传过去,double4字节。尽量不要直接value传递。pass by value是**直接压到堆栈,**但是当参数太大的时候,可以传递地址(指针,4字节)
- 尽可能传递引用reference,传引用相当于传指针,很快,形式又很漂亮。
- 传引用过去,修改之后,都会改变;如果只是为了提升速度,不向改变数据,那么传const引用。这样传进去的东西,不能被修改。
- 返回值的传递,也尽量返回引用。
- 1中操作符重载的声明中,没有写变量名,也可以写上。c++中,声明函数的时候,可以不写变量名,实现的时候必须写。
1.友元:friend,修饰在函数定义之前,表示这个函数可以直接拿该类对象的private数据。
2.如上面所示,声明为friend之后,函数可以直接取到re和im,如果不被声明为friend,只能通过调用real和imag函数来得到,效率较低,而朋友是直接拿。
相同class的不同对象objects互为友元,即可以直接拿到另一个object的data。
4.操作符重载与临时对象
1.第一种方式,写成成员函数。所有的成员函数都带有一个隐藏的参数this(是一个指针),this表示(指向)调用这个函数的调用者。(谁调用这个函数,那个谁就是this)
2.定义函数的时候,在参数列中不能写出来this,直接用即可。
2是成员函数
1.传递者无需知道接受者是以引用形式接受。
2.这里面虽然返回值需要的是引用,但是代码中写的返回值可以是value。
3.+=操作符中,定义的参数是引用,但是传进去的c1也可以是value。
4.接收端使用什么形式接收与传递者无关。
5.上面的操作符,进行操作之后,c2改变了,返回了c2的引用。因此感觉上,将操作符写为void函数也可以,但实际上,为了可以兼容c3+=c2+=c1的形式,写成返回引用更好。
1.非成员函数的操作符重载。(没有this)
2.应对客户的三种方法,写出三种方式,使用时进行重载。
3.非成员函数是local函数。(之前的成员函数的例子是吧左边加到右边已经存在的this,现在是在这个函数里面创建一个东西来存放结果,离开当前函数就死掉了,所以不能传递引用出去)
4.这些函数不能返回引用,必须是一个local object。因为这里面的操作符中二者不是一个加到另一个上,是两个对象相加,因此返回必须是一个对象,不是引用。
5.typename(),创建一个typename类型的临时对象(和int()的用法类似)。
靠参数的个数可以用来区分正号和加号的不同
- operator <<要构造成非成员函数,因为c1<<cout这种方法违反使用习惯。(cout是一个对象)
- 直觉上,cout输出以后就没人在意函数返回类型了, 完全可以是void,但是为了应对连续输出的情况,可以输出引用
5.主要代码
complex.h
#ifndef __MYCOMPLEX__
#define __MYCOMPLEX__
class complex;
complex&
__doapl(complex* ths, const complex& r);
class complex
{
public:
complex(double r = 0, double i = 0) :re(r), im(i) { }
complex& operator += (const complex&);
double real() const { return re; }
double imag() const { return im; }
private:
double re, im;
friend complex& __doapl(complex*, const complex&);
};
//友元
inline complex&
__doapl(complex* ths, const complex& r)
{
ths->re += r.re;
ths->im += r.im;
return *ths;
}
//成员函数
inline complex&
complex::operator +=(const complex& r)
{
return __doapl(this, r);
}
inline double
real(const complex& x)
{
return x.real();
}
inline double
imag(const complex& x)
{
return x.imag();
}
inline complex
operator +(const complex& x, const complex& y)
{
return complex(real(x) + real(y), imag(x)+imag(y));
}
inline complex
operator +(const complex& x)
{
return x;
}
inline complex
operator -(const complex& x)
{
return complex(-real(x),-imag(x));
}
inline bool
operator == (const complex& x, double y)
{
return real(x) == y && imag(x) == 0;
}
#include <cmath>
inline complex
polar(double r,double t)
{
return complex(r * cos(t), r * sin(t));
}
#endif
complex_text.cpp
#include <iostream>
#include "complex_2.h"
using namespace std;
ostream&
operator <<(ostream& os, const complex& x)
{
return os << '(' << real(x) << ',' << imag(x) << ')';
}
int main()
{
complex c1(2, 1);
complex c2(4, 0);
cout << c1 << endl;
cout << c2 << endl;
cout << (c1 += c2) << endl;
cout << c1 + c2 << endl;
cout << polar(4, 3) << endl;
}
7.三大函数:拷贝构造,拷贝复制,析构
1.同样进行防卫式的声明。
2.string s3(s1)就是拷贝构造,s3=s是拷贝赋值。
3.不写的话,会使用编译器默认的拷贝构造赋值(一个bit一个bit的复制)。对于没有指针的complex,可以使用编译器提供的拷贝。针对带有指针的,编译器默认的只是拷贝了指针,而不是指针指向的数据。因此,如果类中有指针,需要写这两个函数(拷贝构造和拷贝赋值)。
1.因为字符串的长度未知,不能直接设定一个xx长度的数组,这样会导致内存浪费。
2.因此数据应该是一个指向字符的指针,给出字符串之后,可以动态的调整占用内存。
3.第二行是拷贝构造函数(因此参数就是stri ng类)。
4.~String是析构函数。这个类对象死亡的时候,会自动调用。
1.字符串是一个指针,最后有结束符号\0。
2.如果传入的是0,说明是空字符串,则只有一个结束符号。
3.析构函数,释放指针指向的内存。
类中有指针,必须写拷贝构造和拷贝赋值,不然会内存泄漏。
- 右下角的“world\0”还在,但是没有指针指着他了,变成孤儿了,发生内存泄漏
- 默认的是浅拷贝。总结:如果属性有在堆区开辟的(比如指针),一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
浅拷贝的问题:p2的析构函数调用以后已经释放堆区0x0011,p1的析构函数再次调用会造成内重复释放;另外一个问题就是上例中的内存泄漏问题。
1.我们需要的是深拷贝。
2.拷贝另一个string指针指向的字符串内容。
1.拷贝赋值,如果两边目前都有东西(比如左边拷贝到右边),需要先把右边清空,然后右边创建同样大小的内存,然后把左边内容拷贝到右边来。
2.然后先把数据清空,然后新建指定大小的数组。然后把字符串内容复制过来。
3.如上图所示,在重载“=”赋值运算符时需要检查自我赋值,原因如下:
如果没有自我赋值检测,那么自身对象的m_data将被释放,m_data指向的内容将不存在,所以该拷贝会出问题。
8.堆,栈与内存管理
1.栈是存在于某作用域的一块内存空间。
2.堆是由操作系统提供的一块全局内存空间,用new来动态取得。在栈中的,作用域结束,则释放了;在堆中的,需要手动释放。
结束之后,会自动调用析构函数。
加上static之后,会存在到整个程序结束。程序结束之后才会调用析构函数。
- 全局对象:在任何大括号之外,不属于任何作用域
- 寿命期的存在或者消失是考虑到其构造函数和析构函数是什么时候被调用
**指针在离开作用域之后,其自身已经“死亡”,右边却还保存其内存。**导致内存泄漏,没有办法吧内存还给任何人或者系统,内存是非常宝贵的资源
new:
1.先分配了内存,将指针转型,通过指针调用构造函数。
2.内部使用malloc分配内存。
complex函数没必要调用析构函数,反正离开作用域就死亡了,但是string函数不行,因为包含指针
1.先调用析构函数(释放指针所指的数据,堆区),再释放内存(释放指针自身,栈区)。
2.内部使用free来释放。
array new要搭配array delete
1.Vc调试模式下,前后都带着灰色的内存部分(上面32,下面4),还有头尾的cookie(每个cookie4字节)。分配的内存都是16的倍数,因此填充到64字节。
2.执行模式下,没有灰色的,则占用16字节。
3.cookie表示使用了多少字节,每一位是4位bit,(之后系统回收的时候,你只给他一个指针,他需要cookie知道有多大)因为内存必须是16的倍数,因此最后四位bit一定都是0,借用最后的一位1表示占用内存,0表示释放内存。
左边的,最后加的4字节,保存数组的长度。
右边的同理。
1.array new一定要搭配array delete,否则会内存泄漏。因为普通的delete只调用一次析构函数。内存泄漏会发生在剩下两个,因为剩下两个没有调用析构函数。
2.这种情况主要发生在有指针的类,因为如果没有指针的类(比如之前的复数),没有动态分配内存new,因此也就不需要调用自己写的析构函数来杀掉。
9. string实现
10.扩充:类模板,函数模板及其他
1.static
1.在数据或函数前加static关键字,则变为静态函数/数据。
2.一个成员函数要处理很多个数据,需要靠某个东西告诉他处理谁,就是this pointer。来告诉他处理从c1、c2、c3。成员函数有一个隐藏的this pointer参数。(右下角黄色部分可写可不写,real后面的()里面一定不能写)
3.加上static之后,跟对象就脱离了,这个数据/函数就不属于这个对象了,跟这个对象脱离。他在内存的某个区域单独有一份
4. 静态数据举例:银行有很多户头,但是利率是一样的(静态)
5.静态函数 没有this pointer参数,因此不能直接处理普通的对象,只能处理静态数据。
1.这个例子中,利率mrate是静态数据,set_rate是静态函数。脱离于对象,他是属于类class
2.静态函数set_rate只能处理静态数据。
3.静态函数可以通过对象object来调用,也可以通过class name来调用。
4.静态成员:类内声明,类外初始化
1.构造函数放在private中,不想让外界创建。
2.设计一个静态函数,来返回唯一的那一份,这个静态函数是外界取得这一份的唯一方法。
3.调用这个静态函数之后,才开始创建这唯一的一份。
1.cout是一种ostream。
2.设计了很多种<<的操作符重载
2.类模板
1.使用T来代替某种类型,类模板。
2.使用的时候,<>中写明类型,编译器就会把T全部替换为这种类型。
1.这里面的min,比较的类型用T来表示。
2.这样比较的时候,<符号就会使用T类型中重载的<符号来进行。
3.用的时候不需要用<>绑定类型,编译器会根据传进去的对象类型 推导而自动绑定 T类型。
3.namespace
11.组合与继承
探讨 类与类之间的关系,面向对象的思想(之前是基于对象)
1.复合
1.表示这个class queue中,有一个这种sequence类东西。 我有一/几个(has a)
2.这个sequence是deque
3.queue里面所有的功能,都是调用c的功能来完成的。
1.queue中有deque,deque的源代码中,还有另一个复合,Itr。
2.从内存的角度看,queue占用40字节。
1.左边拥有右边。
2.复合情况下的构造函数,由内而外,析构函数,由外而内。代码中红色的部分,是编译器来完成的。编译器会调用内部的默认的构造函数或析构函数。如果不希望调用默认的,那么就需要自己写代码。
2.委托
1.引用方式的复合,即左边has a右边类的指针。
2.即可以通过该指针,把任务委托给右边的类。
3.复合中,内部和外部是一起出现的,即调用二者的构造函数;而委托的话,因为是指针,是不同步的,当需要右边的时候,才创建这个。 我有一个指针指向实现所有功能的类
4.应用实例:右面的类为具体的实现,左边只是调用的接口。
3.继承
1.黄的的一行为使用public继承的语法,表示继承_List_node_base类。
2.继承,表示is-a,是一种。
3.父类的数据会被完整继承下来。
子类拥有自己的以及父类的数据。
4.继承和虚函数搭配是关键
1.子类的对象中有父类的成分。
2.构造时,先调用父类的构造函数,然后再调用自己的。
3.析构时,先析构自己,然后析构父类的。
4.编译器会自动完成。父类的析构函数必须是虚函数,否则。。。
12.虚函数与多态
1.搭配虚函数来完成继承。
2.在任何成员函数之前加上virtual关键字,即为虚函数。
3.子类可以调用父类的数据和函数,即继承了函数(实际上是继承了函数的调用权)。(数据的继承可以从内存的角度来理解,函数的继承不是从内存的角度,而是从调用权的角度)
4.非虚函数,是不希望子类重新定义(override)的函数。
5.虚函数,希望子类重新定义它,且已有默认定义。
6.纯虚函数,希望子类重新定义它,且目前没有默认定义,一定要去定义。即函数定义后面直接=0。
7.上图中,定义了一个父类shape,其中定义了几种成员函数。objectID是非虚函数,不需要重新定义。error是虚函数,有默认定义,可以重新定义。draw函数是纯虚函数,没有默认定义,必须要子类来重新定义。
1.父类中其他可以通用,读文件这个函数Serialize设置为虚函数,需要override。
2.我们定义一个读文档的类,那么serialize函数就要override成读文档的函数。
3.调用serialize时,通过隐藏的this pointer来调用,因为myDoc.OnFileOpen,因此this就是myDoc,因此调用的是我们override之后的serialize函数。
4.这就是设计模式,template method
复合+继承
1.继承+复合。
2.构造函数,首先调用父类的构造函数,然后调用复合的构造函数,然后调用自己的构造函数。(最里面的先被调用)
3.析构函数相反。
委托+继承
1.observer来观察subject的数据。一个subject数据可以有多个observer来观察。observer是一个父类,可以定义子类来继承,因此可以有不同的观察方法。
2.当数据改变的时候,observer也需要更新,即notify函数,来将目前所有的observer更新。
13.委托相关设计
1.设计一种类似窗口的类,窗口中可以有其他窗口,窗口中有其他类对象。
2.primitive是对象个体,composite是一种窗口容器,特殊点在于放的可能是其他对象,也可能是窗口。
3.因此把primitive和composite都继承自component,然后composite容器存放的是指向component对象的指针即可(因为vector里面只能放相同大小的东西,所以放指针)。这样composite中存放的可能是窗口,也可能是对象。
4.这就是设计模式:composite。
5.component中add是虚函数,不能是纯虚函数,因为primitive无法override add函数。composite需要override add函数,使得容器可以存放窗口,也可以存放对象。
1.想要创建未来才会出现的子类(下面是派生的子类)。
2.子类中,安排一个静态对象(_LAST),然后把它放到父类之前开辟出的一个空间中,这样父类就可以看到新创建的子类。
3.这个静态对象创建的时候,调用自己私有的构造函数,调用addPrototype,这样就把自己放到了父类中。
4.子类中,还需要准备一个clone函数。这样父类就可以通过调用clone方法来创建这种子类的副本。
静态变量要到本体外部去定义,这样才是分配内存。
多态首先是建立在继承的基础上的,先有继承才能有多态。多态是指不同的子类在继承父类后分别都重写覆盖了父类的方法,即父类同一个方法,在继承的子类中表现出不同的形式。多态成立的另一个条件是在创建子类时候必须使用父类new子类的方式。
未使用虚函数:
使用虚函数:
多态存在的三个必要条件
一、要有继承;
二、要有重写;
三、父类引用指向子类对象。