C++学习笔记----3、设计专业的C++程序(二)---- C++程序设计

1、C++程序设计

        当利用C++进行程序设计时需要记住如下几个方面:

  • C++提供了面向对象的能力。这意味着设计可以包含类层次结构,类接口以及对象交互。面向对象设计与像用于C及其他语言的面向过程的设计有很大的不同。
  • C++是一种多样式编程语言。除了在前面说的面向对象的能力之外,C++支持多种样式,像过程化。使用哪种样子,面向对象还是面向过程,是设计过程的一部分。
  • C++对设计通用及可重用的代码有大量的特色服务。除了面向对象与面向过程的能力之外,C++支持其他的像泛型编程的模板的语言特色服务。
  • C++提供了丰富的标准库。包含了字符串类,字符串格式化,输入输出特色服务,多线程构建块。大量通用数据结构与算法,还有,所有的这些特色服务均为C++编写。
  • C++包含了许多设计模式。换句话说,它支持通用方法解决问题。

        进行程序设计可能是令人震惊的。你可能会整天整天地在纸上写写划划,然后划掉,再写,再划,不停地重复这个过程。有时候这个过程是有用的,几天或者几周后,你做出了整洁有效的设计。有时候却令人很沮丧,毫无成果,但这并不是无用功。如果你需要把已经失败的设计重新来过,你可能会浪费更多的时间。重要的是保持清醒,是否在进步。如果你发现自己被困住了,可以采用如下行动之一:

  • 寻求帮助。咨询同事,导师,书籍,新闻组或者网上查找。
  • 干一会儿别的事儿。然后再回来进行设计。
  • 做出决定继续进行。即使不是理想的解决方案,先决定好在这个设计上进行。错误的选择马上就会显现。然而,也有可能表明是一个可以接受的解决方案。也有可能就是没有简洁的方式来完成你的设计。有时候如果只有现实的策略来完成需求,你不得不接受一个“丑陋“的解决方案。不管你如何决定,都要用文档记录下来以便站起来你或者其他人知道你为什么这么做。包含了你拒绝掉的设计以及拒绝背后的理由。

        记住好的设计是很难的,正确的设计需要不断的练习。不要想一夜成名,也不要奇怪,C++设计比C++编码难得多。

2、C++程序设计的两大规则

        当进行C++程序设计时,有两个基本的设计规则要遵循:抽象与重用。这些指导非常重要,可以被认为是C++设计的圭臬,会在本学习笔记中反复出现,会应用在高效C++设计的各个领域。

2.1、抽象

        抽象的原则通过一个真实世界的类比就比较好理解了。电视是在大部分家庭的一种科技产品。你对它的属性可能比较熟悉:你可以打开或者关闭电视,换台,调整音量,也可以添加外部组件像音箱,DVR,蓝光播放器等。然而,你可以解释这个黑盒子是如何工作的吗?也就是说,你知道它是怎么通过线缆接收信号,进行解码,然后显示到屏幕上的吗?大部分从一定解释不了电视是如何工作的,但使用起来却得心应手。这就是因为电视清晰地将其内部实现与外部接口进行了划分。我们通过接口与电视交互:电源开关,频道转换器,音量控制器。我们不知道,也不关心电视是如何工作的;我们不关心它是否是用电子射线管还是类似于外星人的技术在屏幕上产生图像。无关紧要,因为它不影响接口。

2.1.1抽象的好处

        抽象的原则与软件类似,你可以使用代码而无需知道其背后的实现。举一个简单的例子,你可以调用在<cmath>中声明的sqrt()函数而不必知道该函数中真正用于计算平方根的算法。实际上,平方根计算背后的实现不同库版本其实现可能不同,但只要其接口不变,对其的调用就有效。

        抽象的原则也可以延伸到类。我们知道vector类可以当作动态阵列使用,可以添加或删除任意多的元素,只要你喜欢。例如:

	vector<int> myVector;
	myVector.push_back(33);
	myVector.push_back(44);

        我们使用vector的接口对myVector添加了33与44这两个元素。然而,你不需要理解vector类是如何在内部管理内存的--这在C中可是个大麻烦。你需要知道的只是它的公共接口。vector背后的实现可以随意修改,只要暴露在外面的行为与接口保持不变就行。

2.1.2在设计中应用抽象

        应该按如下的方式来设计函数与类,其他程序员不必知道或者依赖背后的实现就可以使用它们。我们来看一下设计成暴露实现的与隐藏实现的区别,还是我们以前的国际象棋的例子。你可以用指向ChessPiece对象的二维数组指针来实现棋盘,代码如下:

	ChessPiece* chessBoard[8][8]{}; // Zero-initialized	array.
	...
	chessBoard[0][0] = new Rook{};

        然而,这种实现没有使用抽象的概念。每一个使用棋盘的程序员都知道它是使用一个二维数组来实现的。把它改成其他实现,如一维数组,将其大小扩大为64,这样做就很困难了,因为需要在整个程序中使用到棋盘的地方都需要修改。每个使用棋盘的程序员都要小心翼翼地对内存进行管理。这是没有将接口与实现分开的例子。

        更好的方式是将棋盘模型化为一个类,只需要暴露接口,隐藏实现细节。看下面的棋盘类的例子:

	class ChessBoard
	{
	public:
		void setPieceAt(std::size_t x, std::size_t y, ChessPiece* piece);
		ChessPiece* getPieceAt(std::size_t x, std::size_t y) const;
		bool isEmpty(std::size_t x, std::size_t y) const;
		// ...
	private:
		// Private implementation details...
	};

        注意这个接口就没有对任何背后的实现的展示。棋盘类可以使用二维数组,但接口并没有强制要求。改变实现不要求改变接口。还有,该实现可以实现额外的功能,如边界检测。这就是因为它严格地遵循了如下原则:

        所有的类成员必须为私有的。提供公用的get()与set()从外部对该数据进行访问。

        把所有的类成员设置成私有的通常被叫做数据隐藏。这个为什么重要?遵守如下原则,就对你的类进行了高度抽象化:

  • 改变背后的实现时不需要改变公共接口。
  • 允许外部代码只能通过get()与set()对数据成员进行访问使得你在数据被查询可修改时可以添加额外的步骤。例如,你可以添加安全检测以保证数据成员不会被赋于无效值,也可以当数据成员被修改时生成一个事件等等。
  • 使用调试工具,可以在get()与set()函数中设置断点,使得在查询或修改数据时比较容易和发现代码在做什么。

        希望这个例子能够让你相信抽象在C++编程中是一项很重要的技术。

2.2重用

        第二个在C++设计中的基本原则就是重用。还是用原来的方法,用现实世界中的类比来帮助大家理解这个概念。假设啊,你不喜欢编程了,想去搞烘焙。第一天工作,大厨让你去烘焙饼干。要把这个事儿干完,你找到巧克力饼干的配方,将原料混合,在饼干架子上做成型,把架子放到烤箱。大厨很满意你的结果。

        现在我来指出一些很明显但是会让你吃惊的东西:你没有自己建造烤箱来烘焙饼干,也没有搅拌奶油,也没有磨面粉,更没有制作巧克力。我可以听到你的内心:这还用说啊。如果你是一个厨师,这确实,但是如果你是一个写烘焙模拟游戏的程序员呢?那种情况下,你不会想去写程序的每一个部件,从巧克力到烤箱。或者,你会省下时间去找可重用的代码。也许有同事写了一个做饭的模拟游戏,有一些很好的有关烤箱的代码可用。可能并没有你所需要的所有功能,但是你可以在此基础上进行修改,加上必须的功能。天下程序一大抄嘛。

        你认为的理所当然的事情是按照饼干的配方制作而不是自己创造配方。当然,这不用说。然而,在C++编程中,这真的需要说。虽然有许多标准的途径来解决C++中不断出现的问题,但是还是有许多程序员坚持在每个程序设计中重新发明这些策略。

        使用既有的代码并不是什么新鲜事儿。从你使用std::println()来输出东西的第一天编码开始,你就在重用代码。你不会真的去写把数据输出到屏幕上的代码,你会使用既有的println()实现去干这个事儿。同样的,前面提到的C++标准库中的std::vector也在你的代码中做了很多事儿,

        不幸的是,并不是所有的程序员都会利用既有的代码,经常会重新造轮子。在设计地应该把既有代码考虑进去,合适的时候重用。

2.2.1写可重用的代码

        不光要重用既有代码,也要对所写代码应用重用的设计思想。设计程序时,要能够重用类、算法,以及数据结构。在目前的项目和将来的项目中大家都可以使用这瞟部件。一般来讲,应该避免设计出只能应用于目前项目的特定代码。

        写出C++通用代码的一个技巧就是使用模板。还记得我们前面讨论过的国际象棋的例子吗?现在考虑一下,我们需要有一个ChessBoard类来存储ChessPieces,一个CheckersBoard类来存储CheckersPieces。当然了,你可以写两个完全不相干的ChessBoard类与CheckersBoard类,但是这样做的话,就会有大量重复的代码,在C中我们干这个事儿是得心应手,但是对于面向对象的C++来讲,就不是一个好主意了。在C++中,可以写一个GameBoard类模板来避免这些重复的代码,只需要改变类声明就可以让棋子做为模板参数PieceType,而不是在界面中硬编码,类模板如下:

template <typename PieceType>
class GameBoard
{
public:
	void setPieceAt(std::size_t x, std::size_t y, PieceType* piece);
	PieceType* getPieceAt(std::size_t x, std::size_t y) const;
	bool isEmpty(std::size_t x, std::size_t y) const;
	// ...
private:
	// Private implementation details...
};

        先不用纠结于这些代码的语法,我们以后会学习到,只需要知道它是可重用的类模板就足够了。

        在接口中进行简单的修改,你就拥有了一个通用的棋盘类,可以用于任何二维的棋盘游戏。虽然修改代码很简单,但在设计阶段做出决定很重要,这样你就可以高效高能地使用这些代码了。

2.2.2重用设计

        学习C++语言与成为好的C++语言工程师是两个完全不同的事情。假如你坐下来,阅读C++标准,记住每一个语法点,你就可以像其他人一样懂C++了。然而,只有你通过阅读代码,编写代码获得相关经验,你才可能成为好的C++语言工程师。原因就是C++语言语法只是简单定义了它能做什么,而没有说明每个特点要怎么用。这个可以用语言文字来类比,就像你掌握了汉语语法,能够听说读写,但不能说你就是一个文学家,因为如何使用语法来写出文章,甚至写出好文章,与你掌握了基本语法是完全不同的两回事。

        就像烘焙案例所展示的,重新为每一种好的糕点重新发明配方是荒唐可笑的,而程序员在他们的设计中经常会犯相类似的错误,不去用既存的“配方”--模式去设计程序,而是在设计程序时每次都是重新发明这些技术。

        在使用C++语言越来越有经验之后,C++程序员就会用C++语言的特色去开发他们自己的方式。在C++社区中也建立了一些标准的应用该语言的方式,有正式的,也有非正式的。我们把这些重用的语言应用叫做设计技巧与设计模式。有些看起来很明显,因为它们只是一些解决方案的正式化。其它的揭示了你在过往编程中碰到的问题的新的解决方案。有些提供了新的方式来思考你的编程组织。

        例如,你可能想设计你自己的国际象棋程序,具有一个单独的ErrorLogger对象将不同部件产生的错误序列化到一个Log文件中。当你尝试设计ErrorLogger类时,你意识到你想要在你的程序中只有一个ErrorLogger类的实例。但是你也想要程序中的几个部件能够使用这个ErrorLogger实例,也就是说,这些部件都想用同样的ErrorLogger服务。实现这样一个服务功能的标准模式就是策略模式与依赖注入的结合。利用策略模式生成每一个服务的接口,然后对该接口进行多重实现。例如,你可以有多个Logger服务的实现,一种服务是将日志输出到文件中,另一个是将日志信息发送到互联网上的远程服务器等等。一旦你定义了这样的接口,你就可以使用依赖注入将部件所需的接口注入到该部件中去。这样,在这一点上一个好的设计就是要用策略模式加上依赖注入。

        熟悉这些模式与技巧非常重要,当一个特殊的设计问题需要这种解决方案时,你可以重新组织你的设计。有许多技巧与模式适用于C++,大家可以参阅相关书籍,有关此类的书可以说是汗牛充栋了,当然,也可以在网上寻找相关资料,真的不难找,我就不给大家提供相关书籍与链接了,免得有做广告的嫌疑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王俊山IT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值