课件地址(C++ 面向对象程序设计 侯捷)
培养正规大气的编程习惯:
以良好的方式编写C++ Class(基于对象)
~class without pointer members
——complex
~class with pointer members
——string
学习Class之间的关系(面向对象)
——继承(inheritance)
——复合(composition)
——委托(delegation)
需要的代码(来源于标准库std)
complex.h、complex-test.cpp、string.h、string-test.cpp.
C++的演化
C++98、C++03、C++11、C++14
C++标准库用处很大。
C vs. C++,关于数据和函数
基于对象是面向Class的单一的设计
面向对想是多个Class的关系
C++ 代码的基本形式
include自己写的东西使用双引号,include标准库的东西使用尖括号。
output
头文件的防卫式声明
#program once(可以代替防卫式声明,这个是编译器的命令,就是执行一次)
防卫式声明可以让在其中的文件,只执行一次,避免在一个程序中反复调用。
class的声明
类首先包含函数头,类的几个部分如上图所示。
模板的简介
模板是当你的操作基本上一致的时候,就可以使用模板。使用template<typename T>
内联函数(inline)
inline的好处,如果函数是inline function,就会执行的很快,在其他的地方调用的时候函数会自动的在那个地方展开,但是不是所有设置的inline function都会成为真正的inline function,这要看编译器是否使其成为inline function。(只是对编译器的建议)一般简单的函数较容易成为inline。函数出现在class的体内会优先成为inline function。
private:只有class中可以看到
public:class外面同样可以看到
可以将private和public交叉在类中出现。上图的左边不可以访问私有的成员数据是不可行的,要通过公有的成员函数进行取用。访问级别。
构造函数
构造函数的名称首先要和类的名称一致,其次存在两种构造函数的写法,推荐带有初始化的写法(比较高级),如果采用complex (double r = 0, double i = 0){re = r , im = i},那么就是少了对数据进行初始化的步骤,直接对数据进行了赋值的操作,相比来说冒号后面的两个操作是及进行初始化之后再进行赋值,在操作中更加高级。
参数要包含默认值,就是在调用的时候如果没有指明例如:complex c2;这样就会使用默认值,如果指明的话那么就不用使用默认值:complex c1(2,1);【一个变量有两个阶段,一个是初始化,一个赋值】不是构造函数的属性,其他函数也可以。构造函数没有返回类型,就是用来创建类的这种东西。
构造函数的重载
构造函数可以有很多个,类等待外界创建对象,创建的方式可能会有很多种,就可以创建多个的构造函数。在C语言中是不允许的。构造函数的重载是可以根据参数的个数以及参数的类型不同来进行重载,其他的函数同样可以进行相应的重载操作,【因为函数重载取决于参数的类型、个数】我们虽然写的函数名称相同,但是函数在编译器中来看是不相同的。在调用的时候会匹配到最优的,但是存在重载的时候需要明确的定义,两者在调用的时候不能产生歧义。
将构造函数放在private的区域
如果将构造函数放在私有区域,那么只有类的内部才可以使用,不可以被外界调用,例子:单例模式。
设计的A外界只能用一份,外界会通过类的函数创建对象,如上图所示。
在函数的后面加const,在大括号的前面小括号的后面。在类里面有两种情况的成员函数,一种是改变数据的函数,一种是不会改变数据的函数就是只调用,只使用不改变【例如只打印出数据】,另一种就是类似于赋值或者其他需要改变数据的函数,不会改变的就必须要加上const,表示的是常量成员函数。想法就是我们在想创建某个类的时候,对这个类中的一些动作就是函数,会不会改变类的相应的数据或者类的本身。
需要加的时候,不加const的后果,如有上图右下角,如果我的对象或者变量是const的,当我调用函数的时候,加入我的函数是没有加const的,那么我们的编译器就会报错。
参数传递:pass by value & pass by reference(to const)
pass by value:就是将数据整个传过去,所有都复制一份过去,相当于一个副本。
pass by reference:就是只传递这个数据或这对象的标志,相当于一个一个傀儡,就是木偶一样,我们传递的只是一个能够代表这个数据或者这个对象的东西,之后我们可以通过这个东西调用或是改变相应的数据或者对象。(尽量pass by reference),引用在底部就相当于指针,但是reference的形式更加漂亮。建议所有的value都传递引用,在这里指针和引用的区别就是,指针必须需要赋初值,但是引用不用,引用可以什么都没有,指针必须要有值。
const reference的作用:普通的reference是只要我对这个reference产生的任何操作都会作用于这个reference所指代的东西,但是这样的改变有时候是我们不需要的,那么我们就在这个reference的前面加上const。【pass by to const】
返回值传参:return by value vs. return by reference(to const)
(在每一个影响细节的小节上都要考虑效率)
友元
在函数的前面加上friend,就可以声明友元函数,直接可以取得这个类中的成员函数和私有数据。
相同的class的各种object互为friend(友元)
如上图所示:我们可以通过函数func可以直接去取到私有数据,就可以用标题很好的解释。类中的object互为友元,下面的是使用。
什么情况下不能不用reference
如上图操作+=后会产生一个本来就存在的数据,那么我们本来就是存在的数据就可以在操作后,还依然能够保存这个数据,这个时候就可以return by reference,当我们操作后产生一个新的东西,之前不存在的比如两个参数相加的操作,产生的东西是新创建的,这样当我们函数结束的时候这个数值就会被删除,数据就不会存在,这个时候就不能return by reference。产生的是局部变量。
operator overloading(操作符重载-1,成员函数)this
对于数据的操作都会使用操作符,在C++中操作符就是一种函数,允许大家通过重写操作符来重新定义操作符的定义。
在所有的成员函数中(静态成员函数除外)中都会带有一个隐藏的this指针,在写代码的时候,不能写出来,成员函数的参数中不能把this写出来,可以在函数中使用this指针。(上面的代码都来自标准库),this指向调用者,谁调用成员函数就指向谁。
return by reference语法分析
传递者无需知道接受者是以什么形式接收,*ths送出去的是一个object,上图如果使用者使用c2 += c1,这样的话返回值也可以使用void,因为我无需知道接受端如何接受,但是如果我要是连串的赋值那么就不可以使用void,因为上面先执行c2 += c1的话那么由于接下来还要把这个结果在和c3进行一次+= 操作,那么我的第一次运算的返回值就很重要了,就不能使用void。
operator overloading(操作符重载-2,非成员函数)(无this)
在这个例子中,我们重载了三次我们的赋值操作符,然后调用的时候会根据最符合条件的进行调用。这里的const解释是,左边加右边并没有改变左边和右边的数据,而是把产生的新的东西放在了另一个地方,未来的函数里面都不会被改变,新的东西放在了临时对象中,然后在赋值给其他的。
class之外的各种定义(definition)
由于上面是+= 的操作,我们进行操作的东西都是已经存在的东西,但是这里我们进行的赋值的加的操作产生的结果是不存在,所以这里面不能使用reference。(这里面的产生的值就是local的变量)
临时对象(temp object) typename()【用来容纳执行的结果】
临时对象是类加上小括号,可以是没有名字的,这些东西的返回值必定是一个local object,在阶段执行完毕的后就会消失。临时对象一定要return by value。
操作os这个object,这个object的状态始终都在改变。所以不能用const。
小结
三大函数:拷贝构造、拷贝赋值、析构函数
如果编写的类中是含有指针的,那么就要改写拷贝构造和拷贝赋值函数
指针可以动态分配字符,但是现在就要自己写拷贝构造和拷贝赋值函数。拷贝构造:拷贝函数但是接受的是自己的那一类的东西。拷贝赋值:操作符重载,但是它里面的接收的也是自己的哪一种东西。析构函数:当类所产生的对象死亡后,就调用析构函数。
ctor和dtor(构造和析构函数)
构造函数需要先分配一个空间,析构函数:用来清理内存,如果不清理内存会造成内存泄漏,class中有指针就要做动态分配。指针需要进行delete,其他的对象结束后就完事。
上图的会造成内存泄漏,浅拷贝,深拷贝就是拷贝构造函数使用的。
拷贝构造函数
在里面分配出来的空间来存放拷贝后的东西,如果不构建拷贝构造函数,只使用默认的,那么就会将地址进行复制。
拷贝赋值函数
拷贝赋值函数:两个都存在的东西,我们要将一个值赋给另一个值,那么我们的流程就是,要先清空这个被赋值的空间,然后在开辟一个想要要赋值的大小的空间,然后进行复制,最后返回之前的这个object的this指针。注意最开始起手就要想到赋相同值的情况。
一定要检查是不是自己赋值的原因。
栈(stack)和堆(heap)
静态对象的特性
heap的生命周期
new会被分解以上的三个动作。上面的一分配一个内存空间使用malloc(),二是一个转型,三是构造函数。
内存分配的真实场景
在debug的模式就(图左一),带着个首尾连个cookie,和一系列的灰色的,然后内存的大小计算,,这是在vc下,还要填充到16的倍数,所以pad是填充物。cookie是方便回收,release的模式(图左二),cookie要记录长度。右边是string的内存的,cookie的41是64/16=4。
虽然不同的情况下可能不一定要使用delete来释放空间,但是为了培养良好的习惯,我们只要创建了arrary new[],就对应写出delete []。
成员函数可以处理很多的东西就是对象,可以通过this指针调用,黄色的this可以不写,编译器会帮助我们写。静态的数据只有一份,例如:银行账户,开户的人可能有很多,但是利率只有一份,就要设置为静态数据,静态函数只能处理静态数据,没有this指针。
如果是静态的数据那么就要在class的外面使用黄色进行定义。上图的下面时进行调用,可以使用对象调用,也可以使用classname来调用,之前对象调用普通函数会把地址传递给函数,但是静态的函数不会这样。
不想让外界创建a,外界只能通过函数调用A。一种设计模式,单例模式,但是现在不够完美,如果没人用,依然存在,所以下图优化后的结果,把static A a放入函数中,这样在函数中的静态的东西只有在调用函数的时候才会创建,离开函数还会存在。没人使用不会存在,有人用才会出现。
模板
类模板的创建和使用
所有的名字包装在std的名字空间中。
复合:我的里面有另一个东西
配接器:就是deque已经很好了但是客户还有其他的需求,所以根据deque产生一个queue,所以这歌queue就是adapter。
把复合的源代码展开来看,里面的内存就会很好的展现出来,前面的东西拥有一个中间的东西,中间又有两个后面。
在复合的关系中,我们会从里面到外面的次序使用构造函数,但是会从外到里使用析构函数来进行空间的释放。当我们在调用构造函数的时候编译器会为我们调用一个默认的构造函数,这个默认的构造函数就是上图的红色部分,如果我们想要调用其他的构造函数,那么我们就自己加上相应的参数。
指针指向一个类,只是有一个指针指向他,但是不确定什么时候指向它,但是什么时候要调用的时候就会随时调用。另外一个术语,composition by reference。使用指针相连的话寿命不一定,上面的写法就是很经典的方式,我们把想要进行的操作,放在指针所指的类里面。叫做pimpl/handle&body,这样具有弹性,左边永远不用重新写,也叫做编译防火墙。
继承的表达式;在了类的后面加上一个冒号,然后三种形式,public,private,protect加上父类的名字。继承表示的是一种的关系。代表is-a的关系。
子类的对象中有父类的情况。注意上面的话,bass class的析构函数必须是virtual的。
在成员函数之前加virtual就成为虚函数,不仅继承的是父类的数据,这是内存上的继承,而且调用函数的使用权。子类可以调用父类的函数,但是子类要不要重新定义函数,就是虚函数的讨论。如上例shape是一个抽象的概念,里面的objectID函数是不需要重新定义的,他只是一个表示shape的代号的函数,我们就定义为非虚函数,另外里面的error函数,这里面有一个默认的定义,但是为什么要定义为虚函数,就是我们如果以后有更加精细的想法可以进行重新的定义,最后是draw函数,不可能对对他有具体的默认值,一定要被其他所有的子类进行重新定义,每一个子类都要对他重新定义,多一定以为纯虚函数
根据上图中的路线图进行函数的调用和操作.上面的serialize函数可以定义为纯虚函数,也可以定义为虚函数,可以给个初始定义,或者直接写成空函数,但是如果是纯虚函数那么我们就一定要重写他,上面的大致流程是,前面的父类函数可能是别人很久之前写好的,我们写子类调用,先写子类创建对象,然后通过子类创建的对象调用父类的函数serialize,然后父类的函数发现我们在子类中进行了重写,就跑到子类中调用重写之后的函数,然后在回到父类继续执行完onFileopen函数的操作,最后返回结果。在父类中写不出来延缓到子类定义,这个函数的做法就叫做Template Method,不是模板,是一个经典的设计模式。就是一般性的动作有人写好,大大减轻了人们的工作量。上面的函数调用参数是使用this指针进行调用。
这是上面流程的源代码
上面第一个图应该是先父类在复合在子类的构造函数,而析构就是先子类、复合再父类的调用。
可以产生多个同样的东西,另一个是表示多个不同的窗口。
解决以上的问题的操作,我们使用一个容器来存放我们要调用类的指针,然后我们在需要调用的类的基础上派生出多个子类解决多窗口的问题,其中其中subject类的内容中要存在注册和注销的动作,attach就是注册的函数,还要有一个函数是要遍历通知observer类,然后进行更新。
要写一个文件管理系统,首先我们需要一个primitive(个体),另外需要一个组合物(composite)可以容纳很多个primitive,而且他自己还可以容纳他自己,还要一个容器即可以放左边一个以放右边的东西,创建了一个类component,在容器中的东西要一样大,所以容器中放了个指针指向父类component,上面就是用了继承和委托解决了复杂的问题。这个设计模式叫composite。根据情况不能定义add为纯虚函数,因为子类的primitive不能定义add。
解决创建未来派生出来的子类,让下面派生的子类创建一个自己,让框架可以看见这些子类,只要被看到就可以把这些当作一个蓝本。下划线表示静态,静态的自己last,负号是私有的,自己可以调用自己的私有函数,然后私有的构造函数执行add的动作,把下面的原型传递上去,然后就可以通过挂上去的类的原型调用clone函数,就可以clone多个副本出来。上面子类中clone中会返回一个new自己,但是我们不能在调用之前的构造函数,所以要再写一个构造函数,但是我们构造函数还是不能使用public,加上一个参数进行区别
以上源代码来自于右上角的书。
简单的进行一个总结,由于我是第一次写博客,没有什么经验,语言还是有点混乱的,我会逐渐的调整和改善,然后争取再把xia给写出来。