残局

不过在等杀死希望的最后一刀

0%

面向对象

本文是我在学习候捷《面向对象高级开发》课程时所做的笔记,在此分享给大家。

基于对象

类从思考方式上大致分为两类

  • 不带指针。(complex)复数。内容在创建的时候已经是确定的(实部、虚部及相关的函数)
  • 带指针。(string)字符串。只存了指针,当需要添加内容时,需要额外的创建空间。

设计重点

  1. 成员变量必须为private
  2. 参数尽可能的使用引用传递(const与否看情况)
  3. 尽可能的将返回值以引用形式传递
  4. 类本体里面注意应该加const的地方
  5. 尽量使用构造函数的初始化形式。
1
2
3
4
5
6
7
class A{
public:
A(int _a,int _b) : a(_a),b(_b) {}
private:
int a;
int b;
}

基于对象 && 面向对象

Object-Based。设计单一的class。
Object-Oriented。面对多个class,涉及class与class的关系。

头文件

防卫式声明(guard)

1
2
3
4
#ifndef __xxxx__
#define __xxxx__
.......
#endif

只有再第一次包含头文件时,才会导入库。

布局

在头文件里,大致分为三个部分。

  1. 前置声明
  2. 类-声明
  3. 类-定义

私有构造函数

当把构造函数设置在private区域时,表示该类不允许被创建对象。(设计模式-Singleton)

function() const {}

设计不会改变数据内容的成员函数时(如:打印private成员变量),声明为const

同一class的各个object互为friend

强制类型转换

在实现运算符重载时,可以按照(int)(30+10.5)的形式,来强制转成类的临时对象。(该行运行结束即销毁)Complex(5,4)

拷贝构造

类不写拷贝构造函数及拷贝赋值的话,会默认按bit进行拷贝。(带指针的类,需要自己去写)
如果没有写,只是复制了个指针(指针才是类内部的东西,字符数组是动态分配的)。

1
2
3
4
//使用默认拷贝
String a("HELLO");
String b("WORLD");
b = a;

上述代码中,对象a的指针指向HELLO,对象b的指针指向WORLD,当执行b=a时,对象b拷贝对象a,两个对象的指针都指向原来对象a所指向的HELLO地址(浅拷贝),而WORLD所在地址已经造成内存泄漏。

拷贝赋值

拷贝赋值时,要加上自我赋值检测(自我赋值delete->new时,delete删掉了相应的数据,无法new相同空间及拷贝内容)

1
2
if(this == &str)
return *this;

new/delete

[]的new要搭配有[]的delete使用。中括号表示的是数组(array)

1
2
int * ptr = new int[];
delete[] ptr;

有没有[]不影响这一个对象数组在内存中的删除,而是 有[]是表示删除整个数组,根据元素个数,调用多次分别释放各自动态分配的内存。而是数组中后面的指针所分配的内存没有被释放。

1
2
3
4
String* p = new String[3];
......
delete[] p;
delete p;

上述代码中,有无[].3个String所占的内存都会被释放,而有[]会调用3次析构函数,即3个String对象中指针所指向的内存都会被释放,而delete只会调用一次析构函数,String[1],String[2]这两个对象中的指针所指向的内存并没有被释放。

static

静态变量要在类外进行定义(类内的static进行声明,不属于对象)。
多个对象的成员函数只有一份
加在数据成员、成员函数前,表示为静态。所有对象共用一个静态成员。

1
2
3
4
5
cout <<c1.real();
cout <<c2.real();
//等价于
cout <<Complex::real(&c1);
cout <<Complex::real(&c2);

非静态的成员,以this pointer的形式去传,指针地址不同,调用不同的对象。
而静态的成员,属于类,是类负责存储。没有this pointer
e.g:银行系统,利率设置为静态。利息结算函数去处理静态的变量(利率)。
可以通过对象去调用静态变量,也可以通过类名直接调用。

1
2
3
4
5
6
7
8
9
10
class Account{
public:
static double m_rate;
static void set_rate(const double& x) {m_rate = x;}
}
double Account::m_rate = 5; //类外进行初始化
int main() {
Account::set_rate(3); //通过类名调用
Account a; a.set_rate(6); //通过对象调用
}

复合(Composition)

比如 队列、栈是基于双端队列来设计的。故队列、栈内部会包含双端队列这个类(内部存有其他数据结构)这个概念叫做复合。一些共用的功能,可以直接返回其底层容器的东西(empty()、size()函数),不需要的功能可以不开放,不引入进来(设计模式–Adapter)
Adapter是改装后的,不是被改装者。

1
2
3
4
5
6
7
8
9
template<typename T>
class queue {
protected:
deque<T> deq;
public:
void pop() {
deq.pop_front();
}
}

container &component

在上述过程中,container为队列(继承者),component为双端队列(被继承者)
Container构造时,先构造Component的构造函数,再构造自己的。析构时则相反。先调用自己的析构函数,再调用Component的析构函数

1
2
3
Container::Container(...):Component()(......)
Container::~Container(...){......~Component()}
//上述代码中 六个点表示Container相应函数的实现

委托(Delegation)

又叫 Composition by reference。
某个类的内部,成员变量中含有另一个类的对象的指针。(想用那个类的函数时,可以直接通过指针调用另一个类的函数,即将该功能委托给另一个类)。

Handle/Body
衍生设计模式:
* 类的对外接口在该类内部实现,类的底层在被委托者里具体实现。

好处:对外接口不变,内部通过指针指向不同的实现类,从而改变底层。(底层变动不影响接口,从而不影响客户端。)即,底层改变时,编译时候只需要编译底层,不需要编译对外接口等。(节省时间)
而且可以多个不同的对外接口文件共用同一份底层代码。

Composition & Delegation

  • 生命周期不同
    • Composition直接涵盖另一个类的源码,二者同生共死。
    • Delegation中另一个类对象的指针,只有调用时才创建。

继承&&虚函数

成员函数+virtual关键字。继承函数是继承的调用权
设计模式(Template Method)。比如 各种软件打开文件功能,其打开步骤都是相似的(打开对话框、选择文件、检查文件是否存在、在磁盘里读取)这些基础动作基本相同,不同的是以什么形式读取。可以直接封装一个类实现这些基础功能,读取的具体实现,取决于软件继承这个类后去重写。这个东西也叫作Application framework。(应用框架),微软MFC是一个典型例子。

继承+复合构造析构的顺序

派生类Component其余类对象时,派生类的构造/析构次序:基类构造->复合类构造->自身构造自身析构->复合类析构->基类析构。

继承+委托

设计模式(Observer)某个文件内部包括多个查看操作的委托,该文件支持多个人同时查看。(内部可以增加注册、注销等功能以便于管理。)

面向对象

实现对象和非对象的运算

conversion function

类对象—>非类的对象
转换函数conversion function。(比如分数类,实现分数和小数的转换)
operator开头,函数名为返回值类型。不需要写返回值类型。也不需要参数。通常加上const(因为不需要改变内容)

1
2
3
4
5
6
7
8
9
10
class Frac {
public:
Frac(double _y, double _x) : x(_x), y(_y){}
operator double() {
return (double)y / x;
}
private:
double y;
double x;
};

至此,可以用一个数字去这个分数类进行运算。(无需重载运算符)

non-explicit-one-argument ctor

非类的对象—>类对象
non-explicit-one-argument ctor。要重载运算符。(整数默认分母是1,可以使得分母默认为1,只输入一个实参便可以)如果只写了分数间加法时,当调用分数+小数(整数)时,会调用相应的构造函数转换成分数再加。

1
2
3
4
5
6
7
8
9
10
11
12
class Frac {
public:
Frac(double _y, double _x = 1) : x(_x), y(_y) {}
Frac operator+(const Frac &f) {
return Frac(...);
//...为具体实现的方式
}

private:
double y;
double x;
};
  • non-explicit-one-argument ctor与转换函数并存会产生二义性,报错。

explicit-one-argument ctor

explicit关键字放在构造函数前,告诉编译器,不会将非对象的参数转换为对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Frac {
public:
explicit Frac(double _y, double _x = 1) : x(_x), y(_y) {}
Frac operator+(const Frac &f) {
return Frac(...);
//...为具体实现的方式
}

private:
double y;
double x;
};
//运行会报错。

cpp的类大致分为两类,类的对象长得像指针的,长得像函数的。

pointer-like class

像指针的类。内部一定含有指针,实现一些比指针更高级的功能(智能指针)。作用在指针上的运算符(*->等)要进行重载。
shared_ptr(智能指针)、 iterator(迭代器)

  • shared_ptr。在不同类中,实现基本一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename T>
class shared_ptr {
public:
T &operator*() const {
return *px;
}
T &operator->() const {
return px;
}
shared_ptr(T *ptr) : px(ptr){}

private:
T *px;
};
shared_ptr<Foo> sp(new Foo);
Foo f(*sp);
sp->function(); // <==> px->function();
  • iterator。根据功能不同会改动,且会重载其他运算符(如 自增、自减)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <class T, class Ref, class Ptr>
struct __list_iterator {
typedef __list_iterator<T, Ref, Ptr> self;
typedef Ptr pointer;
typedef Ref reference;
typedef __list_node<T> *link_type;
link_type node;
...
reference operator*() const { return (*node).data; }
pointer operator->() const { return &(operator*()); }
...
};

list<Foo>::iterator iter;
*iter; //获取Foo对象
iter->function(); //调用Foo::function();
// iter->function() <==> *(iter).function() <==> (&(*iter))->function()

function-like class

像函数的类。仿函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<class T>
struct identity {
const T &operator()(const T &x) const { return x; }
};
template<class T>
struct select1st {
const typename Pair::first_type &operator()(const Pair &x) const { return x.first; }
};
template<class T>
template<class T>
struct select2nd {
const typename Pair::second_type &operator()(const Pair &x) const { return x.second; }
};

namespace

命名空间。用于分隔开不同开发团队的成果(防止名字冲突,具有二义性)

模板

class template

类模板。参考复数类。成员变量类型、成员函数的返回值类型可以用模板替换。

1
2
3
4
5
6
7
8
9
10
11
template <typename T>
class Complex {
public:
Complex(double x, double y) : re(x), im(y){}
Complex() : re(0), im(0){}
T real() const { return re; }
T imag() const { return im; }
private:
T re, im;
};
Complex<int> c(3, 4);

function template

函数模板。有一些函数,所实现的功能很通用。可以直接封装成函数模板。(一些其他类重载运算符)编译器会根据传入值自动推导相应的类型

1
2
3
4
template<class T>
inline const T& min (cosntT& a,constT& b) {
return b < a ? b : a;
}

member function

成员模板。模板里面嵌套模板。常用于标准库中,用某一个数据类型的变量来初始化另一个数据类型的变量。
比如,现在封装了一个鱼类,以及一个鲫鱼类。
我们声明了一个鲫鱼对象,用他去初始化一个鱼类对象。(构造函数更有弹性)

1
2
3
4
5
6
7
8
9
10
11
12
template<class T1,class T2>
struct pair {
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair() : first(T1()), second(T2()){}
pair(const T1 &a, const T2 &b) : first(a), second(b){}
temppalte<class U1, classU2>
pair(const pair<U1, U2> &p) : first(p.first), second(p.second){}
};

模板特化

specialization。通过模板实现了泛化,而其中一些东西可能并不太适用,会有些不同,需要特殊处理(特化)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//通过模板泛化
template<class Key>
struct hash{};
//当输入类型为指定类型时,进行特化处理
template<>
struct hash<char>
{
size_t operator()(char x) const { return x; }
};
template<>
struct hash<int>
{
size_t operator()(int x) const { return 2 * x; }
};

偏特化

局部特化,可以从个数、范围上产生差异。

  • 个数。比如模板的参数有2个,可以绑定部分参数。(bool类型用特定的底层做容器容易浪费,偏特化处理)
1
2
3
4
5
6
7
8
9
10
11
template <typename T,typename Alloc=...>
class vector
{
...//具体实现略
};
template<typename Alloc=...>
class vector<bool,Alloc>
{
...//具体实现略
};

  • 范围。模板支持任意类型,偏特化为 任意指针类型。(其余类型使用泛化模板,指针使用偏特化)
1
2
3
4
5
6
7
8
9
10
11
template <typename T>
class C
{
...//具体实现略
};
template<typename T>
class C<T*>
{
...//具体实现略
};

模板模板参数

模板的一个参数也为模板。用来初始化时,容器实现底层所采用的数据类型。

1
template <typename T, template <typename T> class Container>

variadic templates

模板参数可变化。允许写任意个数的模板参数(用...来省略)(每次都分为1 + n进行运行)通过sizeof...()函数获取n值。

以下代码调用时,可以一直递归为1 + n直至运行1 + 0全部运行完,程序结束。

1
2
3
4
5
6
7
8
void print() {
//最后的0个参数时调用的函数,用来退出
}
template<typename T,typename... Types>
void print(const T& firstArg,const Types&...args) {
cout << firstArg << endl;
print(args...);
}

auto

自动推导相应的数据类型。比如容器的迭代器,写起来过长可以直接auto让编译器根据返回值类型自动推导相应的类型。再比如lambda表达式返回值比较复杂,可以auto推导。

for(decl:coll)

另一种遍历的形式。每次从右边的容器中取出值赋给左边的变量。默认为值传递,不会改变原来值。

1
2
3
vector<int> vec;
vector<int>::iterator iter = vec.begin();
auto iter = vec.begin();

引用

引用,给变量取一个别名,取出后可以去修改。
变量和其引用大小相同,地址相同。
指针可以重新指向其他元素,而引用一旦确定不能变。

1
2
3
4
5
6
int x = 0;
int *p = &x;
int &r = x;
int y = 5;
p = y; //指针p指向了y 再赋值改变的是y值,不会影响x。
r = y; //用y的值给r赋值 r = 5 x = 5;

指针传递、值传递、引用传递中,指针和引用可以改原始值,三者调用对象的方式有些差异。

1
2
3
void func1(Cls *pobj) { pobj->function(); }
void func2(Cls obj) { obj.function(); }
void func3(Cls &obj) { obj.function(); }

同名同类型一个值传递、一个引用传递的函数不能重载。(二义性)但一个const一个非const可以并存。

虚指针&虚表

vptr & vtbl。只要有虚函数,类内部就会有出现一个指针。虚指针用来指向虚表中的函数指针指向虚函数的地址。未重写的虚函数,基类和派生类共用。重写了的,各自虚表中指向重写后的地址。
下列代码中存在一些问题,可以不必纠结。(为了方便理解)。
通过指针,向上转型,指向虚函数。(*p->vptr[n])(p)
vptr+vtbl

this指针

通过对象调用函数时,对象的地址传this指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A{
public:
void func(){}
private:
int val;
};
class B:public A{
private:
int key;
};
B b;
A::func(&b);
b.func(); //等价
//this指针为&b

const

非const对象可以调用所有的成员函数,const对象只能调用const成员函数。当成语函数const和非cosnt同时存在时,const对象只能调用const函数,非const对象只能调用非const函数。

operator new/delete

是运算符,可以重载。(全局,类内部 都可以)

1
2
3
4
inline void *operator new(size_t size);
inline void *operator new[](size_t size);
inline void *operator delete(void* ptr);
inline void *operator delete[](void* ptr);

重载new/delete

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
28
29
30
31
32
33
34
35
36
#include<bits/stdc++.h>
using namespace std;
class Foo {
public:
int _id;
long _data;
string _str;
public:
Foo() : _id(0) { cout << "default ctor.this" << this << "id=" << _id << endl; }
Foo(int i):_id(i) { cout << "ctor.this" << this << "id=" << _id << endl; }
~Foo() { cout << "dtor.this" << this << "id=" << _id << endl; }
static void *operator new(size_t size);
static void operator delete(void *ptr, size_t size);
static void* operator new[](size_t size);
static void operator delete[](void *ptr, size_t size);
};
void* Foo::operator new(size_t size) {
Foo *p = (Foo *)malloc(size);
return p;
}
void Foo::operator delete(void *ptr, size_t size) {
free(ptr);
}
void* Foo::operator new[](size_t size) {
Foo *p = (Foo *)malloc(size);
return p;
}
void Foo::operator delete[](void *ptr, size_t size) {
free(ptr);
}
// 优先从类成员函数调用,如果没有就用全局。
Foo *pf = new Foo;
delete pf;
//默认使用全局函数
Foo *pf = ::new Foo;
::delete pf;

重载new(),delete()

new()可以重载出多个版本,但第一参数必须为size_t。同时也可以重载对应的delete(),但其不会被调用。只有在new()抛出异常才会调用delete()

new && delete

二者对于基本数据类型的操作基本没有差异,而当为类的对象分配空间时,会有一些区别。
以下是候捷老师课程中关于malloc/new区别的代码以更好的区分二者。
malloc+new
假设我们现在已经有一个复数类,当我们用new进行分配时,首先自动获取对象所占空间,执行operator new,在其内部调用malloc分配相应的大小。然后调用static_cast显式的进行强制类型转换。最终再执行类的构造函数。
从图中,我们可以看出二者的一些区别:

  1. new无需强制类型转换和指定分配的大小,而malloc需要。
  2. new为一个对象开辟空间时,会调用构造函数,而malloc不会。(故,malloc为对象申请空间时,一些在构造函数里的初始化操作无法执行。)

free && delete

下面是关于free/delete的区别
free+delete
可以看出,当我们对一个对象执行delete操作时,首先要调用析构函数再通过operator delete执行free操作。
由此,我们可以知道

  • free使用不当会造成内存泄漏。

如果该类是不带指针的类(比如复数类,成员变量只有实部和虚部),不调用类的析构函数也没有任何问题,也会正常的释放掉为对象所分配的内存空间。而如果这个类带有指针(比如 string),free时虽然会释放掉对象所占的空间,但由于string存储的只是个指针,我们对字符串进行修改时,对象内部会为指针分配相应的空间来存储内容。我们执行free时,内存中为其具体内容分配的空间没有得到释放。(指针消亡,但指向的内存空间并没有被释放)。

delete && delete[]

当我们为一个数组new相应的空间时,使用delete释放也会造成内存泄漏。
delete
从图中可以看出,使用delete释放为数组开辟的空间时,我们会释放掉所分配的空间,同样的也会调用析构函数。这对于不带指针的类来说可能没有太大的区别,当我们的类带指针时,由于只能够调用一次析构函数。数组array中,只有array[0]所指向的内存空间被释放掉了,其余的几个指针指向的空间并没有被释放。
由此可以知道,当我们为数组new一片内存时(new xxx[]),也要使用相应的delete[] xxx

设计模式

Adapter

通过复合。某个类内部含有另一个类的对象,一些可复用的功能直接通过对象去调用。
实例:栈、队列从双端队列改写代码。已实现的功能直接调用,其余自己再写。

Handle/Body

通过委托。某个类内部含有另一个类 对象的指针。这个类可以用来提供对外借口,而底层具体实现在指针指向的对象内实现。(接口改变、底层改变可以只编译一部分)

Template Method

通过继承。有些东西在每个软件中所展示出的操作没有太大区别,我们可以将其相似的功能进行编写,细微差异部分以虚函数的形式留给所继承到具体的类中去实现。
实例:打开文件操作。各个软件打开文件的次序基本一致:选择文件,检查文件是否存在,在磁盘中进行读取,xxx,关闭文件。我们可以将其余功能都封装好,对外提供一个具体读取方式的接口。

Observer

通过继承+委托。文件类内部包括多个查看操作的委托,具体到某个文件时,支持多个人同时查看。(文件内部可以增加注册、注销等功能以便于管理。)

Composite

通过继承+委托。当某个类内部可以包含多种不同内容时(自身+其余的类),我们可以将自身及 要包含的类继承自同一个父类。然后该类的内部包含基类的委托。
e.g. 类A内部要可以包含A的指针也可以包含B的指针。我们可以将A B都继承自父类C(功能多少不重要,重要的是A中可以包含B的委托),A中包含父类的委托。

Prototype

通过继承+委托。父类可以创建未来将要出现的子类。(父类是发行商自己写的,子类是客户买回去自己写的相应的类)。
派生类构造函数设置为私有,构造函数内部实现将自身的指针返回给基类,保存在基类的容器中。派生类中含有一个clone()函数,以让基类调用,生成一个副本提供给父类。还需要另外一个的构造函数(以便clone函数构造自己时调用,防止再次调用上次的私有构造 陷入死循环)

-------------感谢阅读有缘再见-------------