Chireiden

地霊殿

地霊殿,充满幻想与希望的殿堂


夢も希望も無い、毎日がそんな生活だった。

面向对象入门

If you think C++ is not overly complicated, just what is a protected abstract virtual base pure virtual private destructor and when was the last time you needed one?
Tom Cargill

本文通篇将会构造一个类,并以此类的构造过程为讲解顺序。

本文需求的先修知识:cpp iostream (std::cin,std::cout & std::endl),cpp class defination, c struct, c pointer and cpp keywords (const, new, delete, class, public, private [protected, pure, virtual])


编程目标

在命令行窗口编程时,我们有时候会希望输出一些特定的格式,如:(每行前面有一个空格)

 Hello,
 World!

像这样的,基于字符构造的,有特定格式、内容的整体,我们姑且称之为 “字符图像”。 我们可以给字符图像加上边框:

.-------.
| Hello,|
| World!|
'-------'

更多地,可能会希望支持两个这样的字符图拼接,形成新的图像。

.-------.
| Hello,| 你好,
| World!| 世界!
'-------'

以及希望能够修改边框的样式等等······


面向过程可以吗?

如我们所学过的 C/Cpp, 我们可能会想到这样做:

int main()
{
    const char *content[] = {"Hello,", "World!"};
    int left_padding = 1;
    for(int i=0; i<2; ++i)
    {
        for(int j=0; j< left_padding; ++j) printf(" ");
        printf("%s", content[i]);
        printf("\n");
    }
}

运行可以得到输出:

 Hello,
 World!

但是很显然,这样无法(很难)对多个字符图进行操作。

C 语言也能面向对象吗?

我们需要将一个图视为一个对象,一个物体,才能方便地管理、输出两个(及以上)的图。

C 语言面向对象

如果下面的例程编译错误,请修改文件后缀为 cpp 或添加编译选项 -std=c99 或 -std=c11

我们用 C 语言来试试看:

struct tagPicture
{
	char **m_content;
	int m_padding_left, m_padding_top;
	int m_height, m_width;
};

typedef struct tagPicture Picture;

在这个结构体 struct tagPicture (或记作Picture)中,我们使用 m_content 来保存图像内容,m_height, m_width 表示图像的宽与高,m_padding_leftm_padding_top 表示图像左侧和上侧内边界大小。

怎么创建一个 Picture 呢? 在 C 语言中,我们需要定义一个函数来创建该类型:

// 此处需要库文件 stdlib.h 和 string.h
Picture getPicture(const char **_ArrayOfCharArray, int _Lines)
{
	Picture t;
	t.m_content = (char **) malloc(_Lines *sizeof(char *));
	t.m_padding_left = t.m_padding_top = 0;
	t.m_height = _Lines;
	t.m_width = 0;
	for(int i=0; i<_Lines; ++i)
	{
		int n = _ArrayOfCharArray[i] ? strlen(_ArrayOfCharArray[i]) : 0;
		if( t.m_width < n ) t.m_width = n;
	}
	for(int i=0; i<_Lines; ++i)
	{
		t.m_content[i] = (char *) malloc((t.m_width+1) * sizeof(char));
		strcpy(t.m_content[i], _ArrayOfCharArray[i]);
	}
	return t;
}

还需要设定一个输出用的函数:

// 此处需要库文件 stdio.h
void printPicture(Picture _pic)
{
	for(int i=0; i<_pic.m_height; ++i)
	{
		if(!(i < _pic.m_padding_top))
		{
			for(int j=0; j<_pic.m_width; ++j)
			{
				if(j < _pic.m_padding_left) printf(" ");
				else if(_pic.m_content[j-_pic.m_padding_left])
					printf("%c", _pic.m_content[i-_pic.m_padding_top][j-_pic.m_padding_left]);
				else break;
			}
		}
		if(i+1 != _pic.m_height) printf("\n");
	}
}

接下来在主函数调用试试:

int main()
{
	const char *content[] = {"Hello,", "World!"};
	Picture a = getPicture(content, 2);
//  a.m_padding_left = 1;
	printPicture(a);
}

编译运行后可以得到这样的输出:

Hello,
World!

我们在主函数中将 a.m_padding_left 设置为 1,则会看到:

 Hello
 World

有何缺陷?

首先,我们的这个图像单个输出、存储都没有问题,但是如果想要拼接,尤其是横向拼接,就很难做,其次,进行其他复杂操作时,不能较容易地存储结构信息。

C语言课后习题

  • 课后习题 1 : 经过上面的示例,我们可以发现纵向拼接很容易,在此处不做详解,将 C 语言面向对象实现图像的纵向拼接这一任务留作习题。
  • 课后习题 2 : 纵向拼接实现很轻松,横向拼接相对来说难度略大,但聪明的读者一定能解决这个问题,请用 C 语言面向对象实现图像的横向拼接。
  • 课后习题 3 : 请用 C 语言面向对象实现给图像加边框,边框样式需要作为函数参数,如可以指定用 ‘*’ 来画边框。
  • 课后习题 4 : 请用 C 语言面向对象实现图像修改边框样式,例如将 ‘*’ 样式的边框改为 ‘+’ 样式。

Cpp 面向对象

Cpp 的 class、struct 与 C 中的 struct 相似。

C structCpp structCpp class
Plain Old Datatruefalsefalse
default access rights/publicprivate
member function enablefalsetruetrue

简单用法在此不做赘述。 不妨先实现上面程序的内容,让我们看看能否更加优雅。

构造和输出功能

我们姑且以此结构构造程序:

src
 |-- main.cpp
 |-- Picture.h
 '-- Picture.cpp

首先得定义这个类,并声明其方法。

#ifndef __PICTURE__INC
#define __PICTURE__INC

class Picture
{
public:
	Picture(const char **_Strs, int _Lines);
	void print(void);
private:
	char **m_content;
	int m_width, m_height;
	int m_padding_left, m_padding_top;
};

#endif

接下来我们在主函数内直接定义 Picture 的实例 pic,并以此初始化字符串数组初始化这个实例。

此后,我们调用其输出方法。

//main.cpp
#include "picture.h"

const char *initstrs[] = {"Hello,", "World!"};

int main()
{
	Picture pic(initstrs, 2);
	pic.print();
}

最后,我们实现这两个函数。

// picture.cpp
#include "picture.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>


Picture::Picture(const char **_Strs, int _Lines)
{
	m_content = (char **) malloc(_Lines *sizeof(char *));
	m_padding_left = m_padding_top = 0;
	m_height = _Lines;
	m_width = 0;
	for(int i=0; i<_Lines; ++i)
	{
		int n = _Strs[i] ? strlen(_Strs[i]) : 0;
		if( m_width < n ) m_width = n;
	}
	for(int i=0; i<_Lines; ++i)
	{
		m_content[i] = (char *) malloc((m_width+1) * sizeof(char));
		strcpy(m_content[i], _Strs[i]);
	}
}

void Picture::print(void)
{
	for(int i=0; i<m_height; ++i)
	{
		if(!(i < m_padding_top))
		{
			for(int j=0; j<m_width; ++j)
			{
				if(j < m_padding_left) printf(" ");
				else if(m_content[j-m_padding_left])
					printf("%c", m_content[i-m_padding_top][j-m_padding_left]);
				else break;
			}
		}
		if(i+1 != m_height) printf("\n");
	}
}

管理内存

对于 Picture 来说,管理内存比较容易,只需要给其添加一个析构函数即可。

// picture.h
@@ -5,6 +5,7 @@
 {
 public:
        Picture(const char **_Strs, int _Lines);
+       ~Picture(void);
        void print(void);
 private:
        char **m_content;
@@ -23,6 +23,13 @@
        }
 }

+Picture::~Picture(void)
+{
+       for(int i=0; i<m_height; ++i)
+               free(m_content[i]);
+       free(m_content);
+}
+
 void Picture::print(void)
 {
        for(int i=0; i<m_height; ++i)

怎么纵向连接?俯瞰继承关系

首先我们要知道,纵向连接后的结果也应该是一个 Picture,并能够继续纵向连接。

我们应该怎样设计呢?

这里引入一个概念:继承

如下,是图形之间的继承关系。 我们可以认为,如果 A 指向 B ,那么 B 属于 A。

这里涉及到面向对象设计的基本理念,目前主流思路有两种:is-a,has-a 和 like-a。

graph TB; Shape["Shape"]-->Polygen["Polygen"]; Shape["Shape"]-->Ellipse["Ellipse"]; Polygen["Polygen"]-->Rectange["Rectange"]; Ellipse["Ellipse"]-->Circle["Circle"]; Rectange["Rectange"]-->Square["Square"];

在我们这个例子中,我们有理由认为,Picture 是所有类型的基础。 我们可以想当然如此构造:

graph TB; Picture["Picture"] --> vCatPicture["vCatPicture"]; Picture["Picture"] --> hCatPicture["hCatPicture"];

这时候出现了一个问题:我们连接两个图像的时候,应该使用类型本身呢,还是使用其指针呢? 例如上面的图形例子,Rectangle 类型(通常)可以无损转化为 Square 类型,但是 Rectangle 类型的指针不能转换为 Square 类型的指针。相反的,Square 类型(通常)不能无损转换为 Rectangle 类型,但是 Square * 类型却可以实现到 Rectangle * 的转换。

指针类型并不方便,而值类型(及引用类型)并不能支持这样的转换。

如果采用指针类型的话,我们需要定制这样的框架:

graph TB; Picture["Picture"]-->BasicPicture["BasicPicture"]; Picture["Picture"]-->vCatPicture["vCatPicture"]; Picture["Picture"]-->hCatPicture["hCatPicture"]; Picture["Picture"]-->FramedPicture["FramedPicture"];

其中,BasicPicture 表示纯文本的图,vCatPicture 表示经过了一次垂直连接,hCatPicture 表示经过一次水平连接,FramedPicture 表示加框架的图。 在具体使用时,我们使用 Picture * 来表示所有可能的类型。

而值类型则不同,值类型只能从上游向下游转换(从亲代到子代),因此只能是这两种情况之一(或其组合形式)。

graph TB; A["A"]-->D["D"]; B["B"]-->D["D"]; C["C"]-->D["D"]; a["a"]==>b["b"]; b==>c["c"]; c==>d["d"];

显然,这两种构造都无法用 is-a 来解释,用 has-a 解释也很勉强。因此,这是一种不合理的构造方式。

因此,我们采用指针来进行运算,采用共同基类来实现


我们需要哪些类型呢?

BasicPicture :保存实际的图像(字符串)内容,及其宽度与高度。 vCatPicturehCatPicture :保存其所连接的两图像指针 FramedPicture :保存一个图像指针及其边框内容。

基于这些类型,我们可以容易地实现图像连接,图像解除连接,图像加边框,图像去边框以及修改内容。


我们怎样管理内存?

之前的例子里,我们采用构造函数分配内存,析构函数释放内存的方式来处理内存问题。

但是,如下所示的,在断点处,FramedPicture b 的内容物(a)已经被析构,因此输出 b 可能是一个非法操作。

FramedPicture b;
{
    BasicPicture a = ...;
    b = enframe(a);
}
// breakpoint

经过一系列思考,我们可以推理出引用计数的必要性。

类型的设计

Picture 类

经过思考,我们设计这样一个 Picture 类。

class Picture
{
public:
	Picture(void);
	Picture(const char* const *, int);
	Picture(const Picture&);
	~Picture(void);

	Picture& operator=(const Picture&);
	void reframe(int p, char c);
	void reframe(char c, char v, char h);
	Picture split(int id);
private:
	PictureEntity *ap ;
	Picture(PictureEntity* p);

	int width(void) const;
	int height(void) const;
	std::ostream& display(std::ostream&, int, int) const;
};

最上面的三个构造函数,分别有以下作用:

Picture::Picture(void); // 构造一个空的 Picture 对象
Picture::Picture(const char* const *, int); // 根据字符串指针和其行数构造一个 Picture
Picture::Picture(const Picture&); // 拷贝构造函数,例如

reframe 函数用来重设框架格式。 private 内容则是对外不可见的具体实现。 PictureEntity *ap 是一个指针,这里的 PictureEntity 即为上面设计中的所有类型的基类 。 剩余的函数姑且不考虑。

PictureEntity 基类

这个类型,作为所有行为的具体呈现者,并且是一个基类,我们如此定义:

class PictureEntity
{
public:
	PictureEntity(void);
	virtual ~PictureEntity(void) = 0;
	virtual void incuse(void) = 0;
	virtual int decuse(void) = 0;
	friend class Picture;
protected:
	int use;
	virtual int width(void) const = 0;
	virtual int height(void) const = 0;
	virtual ostream& display(ostream&, int, int) const = 0;
	ostream& fillEmpty(ostream&, int, int, char _ch = ' ') const;
	static int max(int a, int b);
	virtual void reframe(int pos, char c);
	virtual void reframe(char corner, char vlimit, char hlimit);
	virtual Picture split(int id) = 0;
}

首先看 public: 部分: 继承中的基类必须拥有虚析构函数,在这里,为了防止创建 PictureEntity 的实例,我们将其析构函数设置为纯虚析构函数。 其次,其拥有两个对引用进行处理的函数,且均为纯虚函数。

其次看 protecteduse 为引用计数器。 其他的函数作用如其名所示。

这个类无法被实例化。

BasicPicture

class BasicPicture : public PictureEntity
{
public:
	BasicPicture(const char* const*, int);
	~BasicPicture(void);
	void incuse(void);
	int decuse(void);

	int width(void) const;
	int height(void) const;
	ostream& display(ostream&, int, int)const;

	Picture split(int id);
private:
	char **m_data;
	int m_size;
	int *m_width;
	int m_maxwidth;
};

这个类型看起来更像是我们在前面 里面实现的类型。其用法也与之相似。

FramedPicture

class FramedPicture : public PictureEntity
{
public:
	FramedPicture(const Picture&, char c, char v, char h);
	FramedPicture(const Picture&, char ct, char cb, char v, char h);
	FramedPicture(const Picture&, char ct, char cb, char t, char b, char l, char r);
	FramedPicture(const Picture&, char c1, char c2, char c3, char c4, char t, char b, char l, char r);
	~FramedPicture(void);
	void incuse(void);
	int decuse(void);

	void reframe(int pos, char c);
	void reframe(char corner, char v, char h);

	int width(void) const;
	int height(void) const;
	ostream& display(ostream&, int y, int mw) const;

	Picture split(int id);

	friend Picture enframe(const Picture&);
private:
	Picture m_insidePic;
	char c1, c2, c3, c4;
	char l, r, t, b;
};

这个类型 public: 部分有大量构造函数,其余部分与上面的BasicPicture 相似。

VcatPicture

class VcatPicture : public PictureEntity
{
public:
	VcatPicture(const Picture&, const Picture&);
	~VcatPicture(void);
	void incuse(void);
	int decuse(void);

	int width(void)const ;
	int height(void)const ;
	ostream& display(ostream&, int y, int mw) const;
	Picture split(int id);

	friend Picture operator&(const Picture&, const Picture&);
private:
	Picture top, bottom;
};

这个类表示 topbottom 纵向连接。

HcatPicture

class HcatPicture :public PictureEntity
{
public:
	HcatPicture(const Picture&, const Picture&);
	~HcatPicture(void);
	void incuse(void);
	int decuse(void);

	int width(void)const ;
	int height(void)const ;
	ostream& display(ostream&, int y, int mw) const;
	Picture split(int id);

	friend Picture operator|(const Picture&, const Picture&);
private:
	Picture left, right;
};

总述及效果展示

通过上面从继承关系设计类型的具体设计 ,我们基本有了程序的框架。

在经过具体实现这些类型后,我们就得到了完整的代码

Cpp 课后习题

  • 课后习题 1 : 经过上面的示例,我们可以发现一个给定的图可以唯一确定一个(系列性的)输出动作,而 std::string 也可以唯一确定一个输出动作,请将一个给定的图转换为 std::string
  • 课后习题 2 : 请设计一个类,可以独立保存一张图及其结构信息。
  • 课后习题 3 : 习题 2 自然不难,能否将这个类的 std::cinstd::cout 的对应操作重载,使其能够被保存和读取。
  • 课后习题 4 : 如果将'*' 视为黑色,' ' 视为白色,那么任何一张只有黑白两色的图可以唯一确定一个字符图,请设计一个方法,能够根据一张图片(.jpg.png)生成一个字符图。(可以利用第三方库读入图片)
  • 课后习题 5 : 相信对于聪明的读者们,习题 4 比较容易。请在习题 4 的基础上,支持字符图的旋转、平移等图形操作。
  • 课后习题 6 : 其实根据字符密度,纯字符方式可以表现黑白灰色彩(单通道八位),请在习题 4 的基础上,生成一个可以生成黑白灰色彩的图。(如 ' ' 表示纯黑,'■' 表示纯白,'*' 表示一种灰色)

本节课将会讲三点内容,链表,栈以及二叉树。

发现问题

众所周知,设置 matplotlib 输出中文需要:

plt.rcParams['font.sans-serif']=['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False  # 用来正常显示负号

pythonmatplotlib Continue