C++面向对象高级开发

引用标准库时,用尖括号(<>)

引用自己写的库时,用引号(“”)

延伸文件名(extension file name)不一定是.h或.cpp,也可能是.hpp或其他甚至无扩展名。

头文件防护式声明

complex.h

1
2
3
4
5
6
7
8
#ifndef __COMPLEX__
#define __COMPLEX__
...




#endif

可以防止重复include这个库

头文件的布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef __COMPLEX__
#define __COMPLEX__

#include <cmath>
class ostream;
class complex;
complex&
__doapl (complex* ths, const complex& r); //前置声明
class complex
{
...
}; //类-声明
complex::function ... //类—定义
#endif

class的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class complex           //class head
{ //class body
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&);
};

对于不同类型数据的需求,使用模板写class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
class complex
{
public:
complex (T r=0,T i=0)
: re(r),im(i)
{ }
complex& operator += (const complex&);
T real () const {return re;}
T imag () const {return im;}
private:
T re,im;

friend complex& __doapl (complex*, const complex&);
};

使用方法

1
2
3
4
5
6
{
complex<double> c1(2.5,1.5);
complex<int> c2(2,6);
...
}//在使用的时候再指定类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class complex           //class head
{ //class body
public:
complex (double r=0,double i=0)
: re(r),im(i)
{ }
complex& operator += (const complex&);//有些函数在此直接定义,另一些在body之外定义
double real () const {return re;}
double imag () const {return im;}
private:
double re,im;

friend complex& __doapl (complex*, const complex&);
};

inline(内联)函数

更快,有些函数不可以,因为编译器做不到(有些太复杂)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class complex
{
public:
complex (double r=0,double i=0)
: re(r),im(i)
{ } //函数在class本体里定义,就形成一种inline
complex& operator += (const complex&);
double real () const {return re;}
double imag () const {return im;}
private:
double re,im;

friend complex& __doapl (complex*, const complex&);
};

1
2
3
4
5
inline double
imag(const complex& x)
{
return x.imag();
}

access level(访问级别)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class complex
{
public:
complex (double r=0,double i=0)
: re(r),im(i)
{ } //函数在class本体里定义,就形成一种inline
complex& operator += (const complex&);
double real () const {return re;}
double imag () const {return im;}
private://通常是数据,因为要封装起来
double re,im;

friend complex& __doapl (complex*, const complex&);
};

使用方法

1
2
3
4
5
{   //错误示范                        {//正确示范
complex c1(2,1) complex c1(2,1)
cout << c1.re; cout << c1.real();
cout <<c1.im; cout << c1.imag();
} }

构造函数

在创建对象时,会自动调用该函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class complex
{
public:
complex (double r=0,double i=0)//默认参数
: re(r),im(i)//初值列、初始列(这里我在学校学的时候叫参数初始化表)
{ }
//下面这种写法相比于上面的写法效率低
//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&);
};
1
2
3
4
5
6
{
complex c1(2,1);
complex c2();
complex* p = new complex(4);
...
}

构造函数可以有很多个,overloading重载

常用于构造函数,重载函数虽然同名,但对与编译器来说有区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class complex
{
public:
complex (double r=0,double i=0)
: re(r),im(i)
{ }
//要注意写重载时不要和已经有的函数冲突,如complex():re(0),im(0){}与上面函数冲突,编译器不允许
complex& operator += (const complex&);
double real () const {return re;}
double imag () const {return im;}
private:
double re,im;

friend complex& __doapl (complex*, const complex&);
};

把构造函数放在private里

当需要不能创建对象的类时,这样写

里面有一个对象(只有一个)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A{
public:
static A& getInstance();
setup(){...}
private:
A();
A(const A& rhs);
...
};
A& A::getInstance()
{
static A a;
return a;
}

外部使用方法

1
A::getInstance().setup();

常量成员函数

函数不会改变数据的,加上const

1
2
3
4
5
6
7
8
9
10
11
12
13
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 cpmplex&);
};

使用者所写

1
2
3
4
5
{
const complex c1(2,1);//表示这个复数不可以被修改
cout<<c1.real();
cout<<c1.imag();
}

如果定义函数时没有写const,在调用时编译器就会报错

参数传递 pass by value vs pass by reference (to const)

传内容 ,和传引用的区别

传内容整个的全都传过去,传引用只传四个字节的指针

对于内容较长的参数,传引用比传内容快

为了满足传递引用,且不修改引用的内容,有时在传递引用时加上const

若是函数修改参数,编译器会报错

下面是例子

1
2
3
4
5
6
7
8
9
10
11
12
13
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&);
};
1
2
3
4
5
6
ostream&
operator <<(ostream& os,const complex& x)
{
return '('<<real(x)<<','
<<imag(x)<<')';
}

同样返回值传递也有两种:return by value vs return by reference(to const)

friend(友元)

上面的friend函数定义

1
2
3
4
5
6
7
inline complex&
__doapl (complex* ths,const complex& r)
{
ths->re+=r.re;
ths->im+=r.im;
return *ths;
}

相同class的各个objects互为friends(友元)

1
2
3
4
5
6
7
8
9
10
11
class complex
{
public:
complex (double r=0,double i=0)
:re(r),im(i)
{}
int func(const complex& param)
{return param.re+param.im;}
private:
double re,im;
};
1
2
3
4
5
6
{
complex c1(2,1);
complex c2;

c2.func(c1);
}

回顾一下

在写一个类时会注意哪些地方

  1. 数据放在private里
  2. 参数尽可能是以refrence来传,看情况加const
  3. 返回值也尽量以refrence来传,不行的情况不用(下面说)
  4. 在类的body里要加const的就应该加上,不然使用者使用时会很麻烦,他会埋怨你
  5. 构造函数有一个特殊的语法是initialization list(参数初始化表)

class body外的各种定义(definitions)

什么情况下可以pass by reference

什么情况下可以return by reference

1
2
3
4
5
6
7
8
9
10
11
12
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 __dopal (this ,r);
}

先说不能return by refrence的情况

当使用的函数创建了一个新的内容,这时返回一个新内容的引用,在函数结束时,这个新内容就死亡了,这时引用里的内容也就死亡了,在去看时看到的不是想要的东西。

operator overlaoding (操作符重载之一,成员函数)this

1
2
3
4
5
{
complex c1(2,1);
complex c2(5);
c2+=c1;//执行这行时,编译器会编译成调用操作符重载函数,对应函数写在下面,是左边调用函数,因此this指向c2
}
1
2
3
4
5
6
7
8
9
10
11
12
inline complex&
__doapl(complex* ths, const complex& r)
{
ths->re +=r.re;
ths->im +=r.im;
return *ths;
}
inline complex&
complex::opeartor += (const complex& r)//但是this不能写在参数表里
{
return __doapl (this ,r);//这个this,是每个类函数自带的参数,谁调用了这个函数,这个this就指向谁
}

这里写了两个函数,先跳到重载函数,重载函数的返回地址是__doapl函数,这样写的目的可能是其他函数也用到右边加到左边的操作。

__doapl函数,是标准库里的名字,上面的代码是从标准库里拿出来删减所得

return by reference 语法分析

传递者无需知道接收者是以什么(reference)形式接收

1
2
3
4
5
6
7
8
9
10
11
inline complex&//接收者
__doapl(complex* ths,const complex& r)
{
...
return *ths;//传递者
}
inline complex&//接收者
complex::operator += (const complex& r)
{
return __doapl(this ,r);//传递者
}

那为了方便,反正使用函数之后也不用在乎返回值,把返回值类型设置为void可不可以呢?

不可以

当使用者这样使用时(c++允许连串赋值)

1
c3+=c2+=c1;//先c1加到c2,c2再加到c3

第一步c1加到c2的结果就不是无关紧要了,因为它需要作为右值参与下次计算。

class body之外的各种定义(definitions)

1
2
3
4
5
6
7
8
9
10
inline double
imag(const complex& x)
{
return x.imag();
}
inline double
real(const complex& x)
{
return x,real();
}
1
2
3
4
5
6
{
complex c1(2,1);

cout<<imag(c1);
cout<<real(c1);
}

operator overloading (操作符重载之二,非成员函数) 无this

为了应对client(客户)的三种可能用法,这里写了三个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,double y)//复数加实数
{
return complex (real (x) + y,imag (x) );
}
inline complex
operator + (double x,complex& y)//实数加复数
{
return complex (x + real(y) ,imag (y) );
}
//当然可以加虚数,这里不写

下面对应三种用法

1
2
3
4
5
6
7
{
complex c1(2,1);
complex c2;
c2=c1+c2;
c2=c1+5;
c2=7+c1;
}

temp object(临时对象) typename();

上面三个加函数

绝对不能return by reference,

因为,他们返回的必定是一个local object.

和之前return一样,返回的东西是一个新创建的,函数结束时就会死亡,那么传递的引用就是错误的东西。

typename();表示创建一个临时对象,这个对象生命很短且没有名字,使用方法就是一个数据类型比如int后面跟一个小括号,括号里放给对象的内容。

在标准库里常用

operator overloading (操作符重载),非成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//判断等不等
inline bool
operator == (const complex& x,const complex& y)
{
return real (x)==real(y)&&imag(x)==imag(y);
}
inline bool
operator==(const complex& x,double y)
{
return real(x)==y&&imag(x)==0;
}
inline bool
operator ==(double x,const complex& y)
{
return x==real(y)&&imag(y)==0;
}
1
2
3
4
5
6
7
{//操作用例
complex c1(2,1);
complex c2;
cout<<(c1==c2);
cout<<(c1==2);
cout<<(0=c2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
inline complex
conj (const complex& x)//共轭复数
{
return complex (real(x),-imag(x));
}
#include<iostream.h>
ostream&//不能改为void,因为使用者会连续输出(例子最下面的用法)
operator <<(ostream& os,const complex& x)//这个<<操作符不能重载为成员函数
{//操作符都是作用到左边上的,作用在cout上,cout不是类里的东西
//这里ostream前面不能加const,加了os不可以被改变,但是实际上每次输出时它都在改变状态
return os<<'('<<real(x)<<','
<<imag(x)<<')';
}
1
2
3
4
5
{
complex c1(2,1);
cout<<conj(c1);=>(2,1)
cout<<c1<<conj(c1);=>(2,1)(2,-1)//合法,输出c1和它的共轭复数
}//先执行输出c1,结果还要能接收<<,所以cout<<c1的返回值类型应为ostream

整理

  1. 构造函数用initaliazation list初值列(参数初始化表)
  2. 函数该不该加const
  3. 参数的传递尽量考虑pass by reference
  4. 数据要尽可能的放在private里,函数绝大部分要放在public里
  5. 防卫式声明

class 的两个经典分类

之前所记是无指针的

现在来写有指针的

1
2
3
4
5
6
7
8
9
10
11
12
//用户可能会使用的用法
int main()
{
String s1()
String s2("hello");//初始化,带有默认参数的

String s3(s1);//拷贝s1到s3,要写一个拷贝构造,前面写的复数没写是编译器自动生成的
//但是有指针时候,拷贝过来的指针也是指向同一个位置,所以这个函数要自己写一个
cout<<s3<<endl;//输出s3,要重载操作符<<
s3=s2;//赋值(assign)操作,拷贝赋值函数
cout<<s3<<endl;
}

三个特殊函数,有些书把三个新增函数叫做Big Three

1
2
3
4
5
6
7
8
9
10
11
class String
{
public:
String(const char* cstr=0);//带参数的构造函数
=>1 String(const String& str);//拷贝构造函数
=>2 String& operator=(const String& str);//拷贝赋值函数
=>3 ~String();//析构函数,当创建的对象死亡时(比如离开他的作用域,离开{})
char* get_c_str() const { return m_data; }
private:
char* m_adta;
};

ctro和dtor(构造函数和析构函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
inline 
String::String(const char* cstr=0)
{
if(cstr){
m_adta=new char[strlen(cstr)+1];//后面会细说new是啥,现在知道是分配内存
strcpy(m_adta,cstr);
}
else{//未指定初值,即传进来的参数是0
m_data=new char[1];
*m_adta='/0';
}
}
inline
String::~String()//他的作用是将动态分配的内存给释放掉,如果不释放就会造成内存泄露
{
delete[] m_data;
}

class with pointer memeber 必须有copy ctor(copy constructor)和copy op= (copy assignment operator)

对于string类,他的对象中的数据只有一个指针,指针所指向的字符串是不属于data的

没有特地为带有指针的对象进行赋值时,就会出现两个指针指向同一个地址的情况,这很危险(因为修改其中任意一个都会将字符串修改,复制出来的指针原来指向的地址也不会被释放还会造成内存泄漏),这种叫做浅拷贝

1
2
3
4
5
6
inline 
String::String(const String& str)//像这种动态分配内存的叫做深复制或深拷贝
{
m_adta =new char[strlen(str.m_data)+1];
strcpy(m_data,str,m_data);
}
1
2
3
4
5
//使用例子
{
String s1("Hello");
String s2(s1);
}

copy assignment operator(拷贝赋值函数)

1
2
3
4
5
6
7
8
9
10
inline 
String& String::operator=(const String& str)//这里是pass by reference
{
if(this==&str)//检测自我赋值(self assignment),这里表示取str地址
return *this;
delete[] m_data;
m_data=new char[strlen(str.m_data)+1];
strcpy(m_data,str.m_data);
return *this;
}

这个检测自我赋值很重要(我觉得),虽然用到它需要你忘记了已经把两个指针指向了同一地址(这是可能发生的)

可以想一下,如果真的传入的参数所指向的地址和要被赋值的指针指向的地址相同,最开始为了防止内存泄露的delete一执行就全删掉了

output函数

1
2
3
4
5
6
#include<iostream.h>
ostream& operator<<(ostream& os,const String& str)
{
os<<str.get_c_str();//前面写了这个函数
return os;
}
1
2
3
4
{
String s1("hello");
cout<<a1;
}

所谓stack(栈)、所谓heap(堆)

Stack,是存在某个作用域(scope)的一块内存空间(memory space)。例如当你调用函数,函数本身即会形成一个stack用来放置它所接收的参数,返回地址以及local object。(侯捷老师说,不完全是,除了函数之外还有其他的用到栈的地方)

在函数本身(function body)内声明的任何变量,其所使用的内存块都取自上述stack。

Heap,或称system heap,是指由操作系统提供的一块global内存空间,程序可动态分配(dynamic allocated)从中获得若干区块(blocks)。

1
2
3
4
5
6
class Complex {...};//声明一个类
...//相关定义
{//一个作用域
Complex c1(2,1);//创建的对象会保存在栈中
Complex* p=new Complex(3);//创建的对象会保存在堆中,在用完要离开作用域时需要自己delete
}

stack objects(本地对象)的生命期

1
2
3
4
5
class Complex {...}
...
{
Complex c1(1,2);
}

c1便是所谓的stack object,其生命在作用域(scope)结束时结束。

这种作用域内的object,又称为auto object,因为它会”自动“清理自己。

static local objects(静态本地对象)的生命期

1
2
3
4
5
class Complex {...}
...
{
static Complex c2(1,2);
}

c2便是所谓static object,其生命在作用域(scope),结束之后仍然存在,直到整个程序结束。

global objects(全域/全局对象)的生命期

1
2
3
4
5
6
7
class Complex {...}
...
Complex c3(1,2);
int main()
{
...
}

c3便是所谓的global object,其生命在整个程序结束之际才结束。可以把他设为一种static object,其作用域是“整个程序”。

heap object的生命期

1
2
3
4
5
6
7
8
9
//正确写法
class Complex {...}
...
{
Complex* p=new Complex;
...
delete p;//在离开作用域之际释放p
}

1
2
3
4
5
6
//错误写法
class Complex {...}
...
{
Complex* p=new Complex;
}

错误写法中没有写delete,会造成内存泄露

内存泄漏指你本来有一块内存,可是经过某些时间或某些作用域之后,你对他失去了控制

关于new和delete都可以在c++语法书里查到

new:先分配memory,再调用ctor(构造函数)

1
2
3
4
5
6
Complex* pc=new Compelx(1.2);
//编译器可能会转化为下面的代码(大部分编译器)
Complex *pc;
void* mem=operator new(sizeof(Complex));//分配内存operator new是c++的特殊函数,其内部调用malloc(n),malloc是c的分配内存函数
pc =static_cast<Compelx*>(mem); //转型void* -> Complex*
pc->Complex::Complex(1,2); //构造函数Complex::Complex(pc,1,2);这个pc是this

delete:先调用dtor,再释放memory

1
2
3
4
5
6
Complex* pc=new Compelx(1,2);
...
delete pc;
//编译器转化为
Complex::~Complex(pc);//析构函数
operator delete(pc); //释放内存,opeartor delete函数也是c++特殊函数,其内部调用free(pc),将字符串本身指针杀掉
1
2
3
4
5
6
7
8
9
class String
{
public:
~String()
{delete[] m_data;}//将字符串里面的动态分配的那一块内存杀掉,字符串本身只是一个指针
...
private:
char* m_data;
}

动态分配所得到的内存块(memory block),in vc(一种编译器)

以之前new一个复数为例,调试模式下分配的内存

一个复数,实部虚部两个double八个字节(应该是16字节,但是侯捷老师说是8个字节,咱们先按照8个来讲,可能是32位的要8字节吧)

但是动态分配给他的不止有八个字节

图中上部分灰色部分(一个是4字节)有4*8=32字节,首尾砖头颜色的是cookie(小甜饼干,先不说是干嘛的,一个四字节)4乘2=8。

所以动态分配一个复数会分配8+(32+4)+(4*2)=复数+灰色+红色=52,但是vc分配内存块一定是16的倍数(有原因,现在不说),所以就有了深绿色的部分(填充物),将分配的内存补充至十六的倍数64(离52最近的16的倍数)

这看起来很浪费内存,但是这是必要的,回收内存的时候需要这些东西

没有进入调试模式分pride动态内存是这样的

没有灰色部分(包括复数前面的八个和两侧的debug header)一定要加cookie,这是一定的。

8+(4*2)=16,正好是16的倍数,不需要加pad(填充)

cookie的作用:记住给你的整块的内存的大小,因为在delete的时候只知道一个指针,他不知道要回收多大,在写malloc和free在写的时候就规定好,在分配好的内存上头写一个cookie来确定大小。

注意:调试模式分配出去的内存大小是64=0x40,而cookie记录的是0x41,这是为什么?

最后那个bit是用来标记这块内存区域有没有被分配出去,0和1,0代表收回来,1代表给出去(这里感觉设计的好厉害)

对于非调试模式下的也同样(16=0x10)。

这里也解释了为什么分配出去的内存块大小一定是16的倍数,这样cookie的后四位就一定是0,可以借一位来表示内存是否属于操作系统

同样,String被分配内存时

调试模式

非调试模式

new 带有[]叫array new ,delete带有[]叫做array delete,array new要与array delete搭配使用,不然会出错

动态分配所得的array

8*3是三个复数(一个复数俩double),32+4是橙色(之前是灰色,debug header,非调试模式没有),4乘2是cookie,最后那个4(counter)是vc编译器的做法(别的不一定)用一个整数表示这里有几个东西(内容)

同理,String的

array new一定要搭配array delete

1
2
3
4
5
6
7
8
//正确示范
String* p=new String[3];
...
delete[] p;//会唤起三次dotr(析构函数)
//错误示范
String* p=new String[3];
...
delete p;//唤起一次dtor

错误示范没有加[],第一个对象会被delete掉后面两个不会,后面两个就造成内存泄露了。

进一步扩充:static(静态)

在类的数据或者函数前,加上static,他就变成了静态成员或静态成员函数

加上static之后,数据就与类分离了,它没有this,存储在另一个地方(可以找到,我们不知道)

侯捷老师以银行账户体系为例子:创建出一百万个账户,其中利率与账户无关,这种情况下,就应该将利率设计为静态数据

静态函数同样没有this,不能处理其他普通的数据,只能处理静态的数据

1
2
3
4
5
6
7
8
9
10
11
class Account{
public:
static double m_rate;//声明没有分配内存
static void set_rata(const double& x) {m_adta =x;}
};
double Accout::m_rata=8.0;//静态数据必须在类外定义(获得内存叫定义),曾经考试时因为忘记静态数据只能在类外定义没编出程序
int main() {//调用static函数的额方式有二
Account::set_rate(0.5);//通过lass nameo调用,在没有对象时可以用这种方法
Accout a;
a.set_rate(7.0);//通过cobject调用
}

进一步补充:把ctors放在private里

1
2
3
4
5
6
7
8
9
10
11
//Singleton单体单例
class A{
public:
static A& getInstance(return a;)//这个函数就是A的唯一接口
setup(){...}
private:
A();
A(const A& rhs);
static A a;
...
}//使用方法A::getInstance().setup();

但是如果外界没有需要用到a的地方,就造成了浪费。所以有更好的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
::getInstance().setup();class A {
public:
static A& getInstance();
setup();{...}
private:
A();
A(const A& rhs);
...
};
A& A::getInstance()
{
static A a;
return a;
}//使用方法仍然是A::getInstance().setup();

进一步补充:cout

为什么cout可以接受那么多的类型?是因为重载了很多类型?是的

关于cout的定义

1
2
3
4
5
class _IO_ostream_withassign//从ostream继承过来的
:public ostream{
。。。
};
extern _IO_ostream_withassign cout;

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

这里只是部分,

1
2
3
4
5
6
template <calss T>//比大小的函数模板
inline
const T& min(const T& a,const T& b)
{
return b<a?b:a;
}
1
2
stone r1(2,3),r2(1,4),r3;
r3=min(r1,r2);//注意这里的min没有写类型,编译器会对function template进行引述(实参)推到(argument deduction)

这样编译器就会将min中的T替换为stone去比较大小。

遇到小于号时(<)会去到stone里找有没有小于号的重载,如果有就执行,没有就报错

进一步扩充:namespace

using directive

1
2
3
4
5
6
7
8
#include<iostream.h>
using namespace std;
int main()
{
cin<<...;
cout<<...;
return 0;
}

using declaration

1
2
3
4
5
6
7
8
#include<iostream.h>
using std::cout;//可以防止造成混乱,不全打开
int main()
{
std::cin<<...;
cout<<...;
return 0;
}

不开的

1
2
3
4
5
6
7
#include<iostream.h>
int main()
{
std::cin<<...;
std::cout<<...;
return 0;
}

Composition(复合) ,表示has-a(有一个)

1
2
3
4
5
6
7
template <class T,class Sequence =deque<T> >//这里说了
class queue{
...
protected:
Sequence c;//上面有说
public:
}

侯捷老师将他替换过来了//Sequence —–>deque<⁢T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <class T>//deque英语读法daike(拼音(笑死),我们读dq是一个两端可以进出的队列
class queue{//queue是队列,先进先出(一端进另一端出)读法q
...
protected:
deque<T> c;//底层容器
public:
//完全用c的操作函数完成,这个类没有写函数,所有的功能都是借用c的功能
bool empty()const{return c.empty();}
size_type size()const {return c.front();}
reference front(){return c.front();}
reference back(){return c,back();}
//
void push(const value_type& x){ c.push_back(x);}
void pop(){c.pop_front();}
};

一个类里面有另n个类,叫复合

老师说要学会用图的形式

黑色菱形这一端就是容器,容纳了另外一个东西

这种写法叫做adapter,改造适配,这里queue就是adapter

可以看到,在deque里也有两个adapter,可以看出大小上的关系

Composition(复合)关系下的构造和析构

container是容器的意思,component是成分的意思

container一定大于等于component

构造要从内而外

container的构造函数首先调用component的default(默认)构造函数,然后才执行自己。

1
Container::Container(...):Component(){...};//Component()是编译器自己加上去的,因为编译器不知道要调用哪一个构造函数,所以调用默认的,如果默认的不行,就需要自己写了

析构从外而内

container的析构函数首先执行自己,然后调用component的析构函数。

1
Container::~Container(...){...~Component()};//同样~Component()也是编译器自己加上去的

Delegantion(委托).Composition by reference.

1
2
3
4
5
6
7
8
9
10
11
//file String.hpp
class StringRep;
class String{
public:
String();
String(const char *s);
String &operator=(const String& s);
...
private:
StringRep* rep;//pimpl
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//file String.cpp
#include "String.hpp"
namespace{
class StringRep{
friend class String;
StringRep(const char* s);
~StringRep();
int count;
char* rep;
};
}
String::String(){...}
...

菱形没有涂黑表示关系指针指向(并不扎实,还没有拥有,不知到什么时候会拥有)

指针指向,他们的生命就不一样了

这个写法叫做point to implementation(有一根指针指向实现所有功能的类),另外一个名字handle/body(调用者是handle)

这跟指针可以指向任何一个类(又叫编译防火墙,每次修改只需修改类)

这是根据上面代码总结的图,reference counting(共享,有多个指针指向同一位置)

但是要注意的是:如果共享就不要轻易修改。解决方式也很简单,让要修改的位置copy一份修改。

inherited(继承),表示is-a(是一个)

1
2
3
4
5
6
7
8
9
10
11
struct _List_code_bace
{
_List_code_base* _M_next;
_List_dode_base* _M_prev;
};
template<typename _Tp>
struct _List_node
:public _List_node//使用语法,老师说class可以,但是有特殊情况可能会出错
{
_TP _M_data;
};

inherited(继承)关系下的构造和析构

与Composition(复合)类似

构造由内而外

Derived的构造函数首先调用Base的defult的构造函数然后才执行自己。

1
Derived::Derived(...):Base(){...}

析构由外而内

Derived的析构函数先执行自己,然后调用base 的析构函数。

1
Derived::~Derived(...){... ~Base};

base calss的dtor必须是virtual,否则会出现undefined behavior(先放在这里,后面会说)

现在可以理解为一个良好的习惯,你认为你的class会变成一个父类,或者将来会成为一个父类,那就把析构函数设置成虚函数。

Inheritance(继承)with virtual functions(虚函数)

语法:在函数前加上virtual就变成虚函数

继承,数据可以继承,函数也可以继承,但是数据继承过来占用内存,那函数继承从内存角度怎么理解,不能从内存的角度去看,函数的继承其实继承的时调权。

non-virtal函数:你不希望derived class (子类)重新定义(override,覆写)它。这个override术语不能乱用,只能在虚函数被重新定义才能叫override。

virtual函数:你希望derived class 重新定义(override,覆写)它,且你对它已经有了默认定义。

pure virtual 纯虚函数:你希望derived class 一定要重新定义它,你对它没有默认定义。

1
2
3
4
5
6
7
8
9
class Shape{//形状(世界上没有叫形状的形状)
public:
virtual void draw()const=0;//前面有virtual 后面跟着=0的函数,是纯虚函数
virtual void error(const std::string& msg);//只有virtual的函数,是虚函数
int objectID()const;//非虚函数
...
};
class Rectangle: public Shape{...};//矩形
class Ellipse:public Shape{...};//椭圆形

error的作用:当运行出错,可以调出是哪个图形的错误。

纯虚函数与空函数不同:纯虚函数要求子类必须覆写,而空函数即使子类不定义也会通过编译。

右边的上方是子类,下方是调用用例,左边是父类。

当主函数,调用myDoc.OnFileOpen();时会先去调用OnFileOpen函数,当执行到Serizalize时,会去找子类的补充定义(覆写)

主函数里写出了函数全名,括号里的参数是this指针(隐藏的不用写出来),而接下来调用内部的纯虚函数也是通过this指针(看左边的指出去的地方),this指针指向了子类里的定义的函数。

这个写法叫做template method (不是c++里的模板只是借用这个模板,在Java里method叫做函数)大名鼎鼎的23个设计模式之一

Inheritance+Composition关系下的构造和析构

在内存中的存储方式谁上谁下对我们编程没多打影响。但是我们要知道当我们创建一个这样的(从父类继承,又和另一个类复合)对象时,构造函数的调用顺序是怎样的呢

老师把这个留给我们自己完成当然给了实验方法

各写三个类,构造函数打印一些内容(主要不要是一样的),根据先后顺序就可以直到先调用了谁

这是我写的test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<iostream>
using namespace std;
class base {
private:
int a;
public:
base()
{
cout << "base" << endl;
}
};
class component {
public:
component() { cout << "component" << endl; }
private:
int b;
};
class derived :public base{
public:
component b;
derived() { cout << "derived" << endl; }
};
int main()
{
derived a;
return 0;
}

附运行结果

他是先调用了父类的构造函数,后调用复合类的构造函数

析构的时候呢

test修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<iostream>
using namespace std;
class base {
private:
int a;
public:
base(){}
~base() { cout << "base" << endl; }
};
class component {
public:
component(){}
~component() { cout << "component" << endl; }
private:
int b;
};
class derived :public base {
public:
component b;
derived(){}
~derived() { cout << "derived" << endl; }
};
int main()
{
derived a;
return 0;
}

运行结果

就很好想了,先调用父类构造函数然后调用复合类的构造函数

1
2
3
4
5
class Observer
{
public:
virtual void update(Subject* sub,int value)=0;
}

Delegantion(委托)+Inheritance(继承)

一个数据多个窗口观察(之前说的这个指针指向的模式用处不多,因为会因为任意一个修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Subjet
{
int m_value;
vetor<Observer*>m_views;//这里创建了一个容器(向量),是Observer*类型。
public:
void attach(Observer* obs)//注册功能,
{
m_view.push_back(obs);
}
void ste_val(int value)
{
m_value =value;
notify();
}
void notify()//遍历容器,通知所有观察(类型)要刷新界面
{
for(int i=0;i<m_views.size();i++)
m_views[i]->update(this,m_value);
}
}

Delegantion(委托)+Inheritance(继承)续

Composite

我们要写一个文件系统(以此为例)

Primitive的意思是单体、基础物的意思

Composite(组合物)

两者同是conmponent的子类,右边使用了c++的容器,容器只可以存储一样大小的东西,所以对象不能直接放进去,这里用了指针

add函数(下右),既要加左边又要能加右边的类型。

这种叫Composite(23个模式之一)

1
2
3
4
5
6
7
class Componst//父类
{
int value;
public:
Component(int cal){value=val;}
virtual void add(Component*){}//这里不能写成纯虚函数,那样Primitive就必须定义,而它又无法定义,比如文件系统共中,文件里不能加文件
};

Prototype(一种设计模型),想要创建未来出现的子类(不太理解)

需要每个将要创建的子类先自己创建一个自己的蓝本(原型)让框架去有办法看到原型,复制他创建它(一点没懂)

画图的时候先写object名字,再写typename

有下划线的是静态函数,带负号的是私有函数,带#的是protected函数,

LandSatImage()是私有的构造函数(但是是自己调用自己所以可以被调用)(我们用它调用addprototype把自己传到框架里)

调用父类的addProtetype将this作为参数,也就是将自己创建一份传到父类。

所有子类(未来的)还需要准备clone函数(克隆)就是new一个自己(自己写的),这样做出了一个副本,是在父类里使用刚才传过去的原型调用这个克隆函数。(父类有这个函数的纯虚函数)

如果没有原型,就不能调用clone函数,但是将clone设置成静态也能调用它?

调用静态需要classname,但是我们不知道calssname(未来的东西)

对于这种方式:写一个静态函数调用自己,又要写一个克隆自己的函数,这样产生的开销合理吗?

合理,为了使用别人写好的框架,需要让别人知道自己写的是什么(不太理解。。)