C++教程1之编程范式概述
C++ Course 1998
Thomas Papanikolaou
这是一个不错的C++课程,也许不适合编程初学者!
这篇教程是作者给其他程序员的一些讲座,给那些有其他语言编程经验的程序员做一些C++的介绍。学习C++毕竟是要靠例子,而不是一些语言定义,对于没有编程经验的也可把这用作一个教程。
本课程基于Bjarne Stroustrup很著名的那篇关于OOP【3】的论文,本文保持了同样的组织结构,同时也增加了一些例子,重新组织了一些章节,提供了更多的信息,毕竟Bjarne Stroustrup的论文目的不是用来教授C++的(尽管读了之后,会不由自主去去学习C++:))。随时接受你对错误的指正,但请以建设性的方式(阅读的时候觉得挺容易,但发现真正翻译出来,原来不是那么容易的事情,所以我也随时接受对错误的指正,随便什么方式都行))。
如何使用本文本
本课程既不是关于C++完整的资源,也没有覆盖C++全部的语言特性。本教程的主要目的是很平滑地介绍C++,帮助你不需要太多先决知识就可以正确使用C++。透过阅读、尝试做、模仿等学会以C++思考问题,就如同学习一门外语。一开始,可能不理解这些符号,但是老实说,当你开始学说话的时候,又知道多少语法呢?
警示:教授一门编程语言是相当复杂的任务,有时我常常会漏了一些重要的东西。因此我高度推荐除了阅读本教程,在你手边还应该拥有象B. Stroustrup【2】和 S. Lippman【1】的一本标准的教材。就我个人看法,你应该拥有这些书!
第一章
I
Programming Paradigms 编程范式
面向对象编程是一种编程技术――为一系列问题编写好程序的一种范式
--Bjarne Stroustrup
这句话指出了通常程序员对面向对象编程的误解:使用面向对象语言编程,并不会自动意味着由此产生的程序是面向对象的。数学家也许会说,使用面向对象语言是必要的,但不是OOP的一个充分条件。
在这个讲座里,我将描述一下不同的编程范式。遵从Stroustrup【3】中的讲述,也同时指出使能面向对象的语言和支持面向对象的语言之间的区别。
I-1 Unstructured Programming 非结构化编程
这通常是编写小应用或者工具程序时采用的编程风格。这样的应用由一个独立的程序组成,包含一些操纵数据的语句序列。这些数据在整个程序里都是全局可见的。非结构化编程有时认为是高效率的,但是生命期很短暂。
I-2 Procedural Programming 过程式编程
过程式编程(PP)是最早的编程范式(可能仍在广泛使用――确实如此,平时我们写一些小的应用还不是以这种范式考虑问题?)。这种范式,程序根据使用的算法进行组织:每一个算法以一个独立的过程实现。这样,处理步骤的共用的序列被组织在一起。
因此,使用PP范式编程可以定义如下:
1.标识需要使用什么算法
2.以过程实现这些算法
3.写一个主程序,定义某种顺序来执行这些过程
I-2.1 Examples of procedural programming 过程式编程例子
举个典型的例子,sqrt子程序。给定一个实数参数x,该子程序使用牛顿迭代算法返回x的平方根。用C实现如下:
- double sqrt(double x)
- {
- double sqrt_x;
- // code to perform the Newton iteration ...
- return sqrt_x;
- }
在程序的其他过程里可以这么使用sqrt:
- void foobar()
- {
- double y = sqrt(2);
- ...
- // no return statement
- }
完整的例子:
- #include
- using namespace std;
- inline double my_abs(double x, double y)
- {
- return (x>y)?(x-y):(y-x);
- }
- ///Newton’s iteration
- double my_sqrt(double x, double err=0.001)
- {
- double x1, x2;
- x1 = x2=1;
- do {
- x2 = (x1 + x/x1)/2;
- if (my_abs(x1,x2)> x;
- cout << my_sqrt(x);
- return 0;
- }
比较复杂的例子,就是递归子程序,例如factorial。给定一个整数n,它返回n!:
- int factorial(int n)
- {
- if (n == 1)
- return 1;
- else
- return n * factorial(n - 1);
- }
I-2.2 Languages supporting Procedural Programming 支持过程式编程的语言
最早的过程式语言是Fortran(有所耳闻,但没有用过)。Algol,C以及Pascal是后来发明的同样风格的过程式语言。所有这些语言提供了传递参数给函数、从函数返回值的方法。相互不同的各种参数、各种函数(过程、子程序、宏等)。这种意义上讲,所有这些语言可以认为是支持PP范式。因为他们提供了足够的语言特性,使得使用过程式编程风格更方便(容易、安全和高效)。
I-3 Modular Programming 模块化编程
随着计算机越来越强大、程序越来越复杂。编程范式的重点从过程的设计转移到数据的组织。新的编程范式以这样的方式看待数据和过程:数据和操纵数据的过程是一个整体(因此称之为模块)。
以模块化编程范式编程涉及以下步骤:
1.将程序分解成模块
2.对于每一个模块提供一个用户接口;确保模块的数据只能经由用户接口被访问(这通常被称之为数据隐藏原则)
3.确保在第一次使用模块前初始化
I-3.1 Examples of Modular Programming 模块化编程的例子
最普遍的例子就是一个字符stack模块的定义了。首先提供用户接口,遵循C惯例,将定义包含在头文件stack.h里。
Definition of the stack user interface
// File: stack.h
// maximum number of elements that can be stored
const int stack_size = 100;
// returns true is the stack is empty, false otherwise
int stack_is_empty();
// returns the top element of the stack and removes it
char pop();
// puts a new element on the top of the stack
void push(char);
实现stack模块
接着我们实现模块的隐藏部分。还是遵循C惯例。将实现部分保存在源文件stack.cc:
- #include "stack.h" // include the stack definition file
- // we represent the stack by an array of size stack_size
- static char the_stack[stack_size];
- // the_top will always point at the top of the stack
- static char* the_top;
- int stack_is_empty()
- {
- return the_top == the_stack;
- }
- // check for underflow and pop
- char pop()
- {
- if (p < the_stack)
- error("the character stack underflows!");
- else
- {
- char c = *the_top;
- the_top--;
- return c;
- }
- }
- // check for overflow and push
- void push(char c)
- {
- // ...
- }
可以看到给出的stack接口函数,没有给出实际的表示――即我们如何在内存里保存stack的细节。因此,我们可以用单链表或者其他合适的数据结构替代数组,对于使用stack的程序却不用做什么修改。
为什么用户不能访问这representation?答案很简单:将the_stack和the_top声明为static。将这些变量变成对于文件或者模块是local的。这样的stack可以这样使用:
- void some_function()
- {
- if (stack_is_empty()) push(’+’);
- char c = pop();
- if (c != ’+’) error("impossible");
- }
I-3.2 Languages supporting Modular Programming 支持模块化编程的语言
Pascal(最早时的定义)不能提供很满意的机制来支持隐藏数据的表示:隐藏名字的唯一方式就是将该该名字对于一个过程是local的。然而,这导致很奇怪的过程欠套以及对全局数据的依赖。
正如我们所看到的,在C里,透过在一个源文件实现一个模块、透过声明变量为static隐藏数据表示,这样可以使用MP编程范式。因此,程序员有较好的能力获得高度的模块化。尽管如此,在C里没有显式象其他语言对模块化的支持,例如,Modula-2(Pascal的后继者)。在Modula-2里,模块的概念是语言构造的中心。Modula-2支持定义良好的模块声明,显式地控制名字的作用域(import/export),一个module的初始化机制以及一组很不错的容易接受的使用风格。从这意义上来说,C和Pascal使能模块化编程,而Modula-2支持模块化编程。
I-4 Object-Based Programming 基于对象编程
前面也看到了我们如何用C语言以MP编程范式来实现一个字符stack。我们的实现提供了一个单一的固定大小的stack。如何扩展我们的实现,处理多个且任意大小的stack?这是两个问题,因此先让我们开始最简单的。考虑I-3.1部分的stack定义。我们会以这种方式扩展它:每一个接口知道它操纵的是哪一个stack:
- // File: multiple_stack.h
- // stack_id will reference a stack
- // its definition is left out for simplicity
- // returns true is stack_id is empty, false otherwise
- int stack_is_empty(stack_id);
- // returns the top element of stack_id and removes it
- char pop(stack_id);
- // puts a new element on the top of stack_id
- void push(stack_id, char);
- 为了能创建和销毁一个给定大小的stack,引入两个函数:
- // create a stack_id of a give size
- stack_id create_stack(int);
- // destroy the stack_id
- void destroy_stack(stack_id);
- 这样我们的实现就允许我们以下面的方式使用多个且大小任意的stack:
- void some_function()
- {
- stack_id s;
- s = create_stack(200);
- push(s, ’a’);
- char c = pop(s);
- if (c != ’a’) error("impossible");
- destroy_stack(s);
- }
与前面的实现相比,这个多个stack的实现很明显是个很大的改善。尽管它有两个主要的缺点:首先,在使用stack之前,我们必须要显式用我们想处理的元素数目来初始化它。这是透过调用create stack(200)完成。如果我们忘记了调用这个函数会发生什么?也许,随后的push调用会导致程序abort。类似地,我们必须要手工释放分配的内存,否则程序里会出现内存泄漏。这么看来,stack_id并不象内建的类型,并不如内建类型得到的支持那样好。对于上面的错误,编译器没有机会侦测到。
象Ada或者C++语言解决了这个问题,这些语言允许用户定义能和内建类型行为一样的(几乎一样的)类型。这样的类型通常称之为抽象数据类型(Abstract Data Type-ADT)或者用户定义类型(User-Defined Type-UDT)。
例如,在C++里,stack类型可实现如下:
- class stack
- {
- char *the_stack;
- char *the_top;
- public:
- // construct a stack of size n
- stack(int n) { the_top = the_stack = new char[n]; }
- // destruct a stack
- ˜stack() { delete[] the_stack; }
- int is_empty() const
- {
- return the_top == the_stack;
- }
- void push(char c) { ... }
- char pop() { ... }
- };
这样使用这个类型会相当容易安全:
- void some_other_function()
- {
- stack s(100); // stack(int n) is called here
- s.push(’+’);
- char c = s.pop();
- if (c != ’a’) error("impossible");
- // ˜stack() is called here automatically
- }
编程ADT,通常称之为基于对象编程(Object-based ProgrammingOBP)或者叫使用数据隐藏范式编程(Data Hiding paradigm-DHP),使用DHP编程范式涉及下面一些步骤:
1.决定想要什么类型?
2.对每一个类型指定表示方法
3.对每一个类型提供一组完整的操作
这看起来和模块化编程范式相当类似,这是自然的,因为ADT是模块化的一般表现形式。通过提供一种机制――确保只有该类的一个对象存在(instance),一个类就可以很容易模拟一个模块。Consequently, where there is no need for more that one object of a type the Data Hiding programming style using modules suffices
I-4.1 Examples of Object-Based Programming
数学上的类型rational以及一些复数的例子是针对用户定义类型常举的一些例子:
- class rational
- {
- private:
- int num, den;
- public:
- rational() { num = 0; den = 1; }
- rational(int n) { num = n; den = 1; }
- rational(int n, int d) { ... }
- ˜rational() { } // yes, it is empty
- friend rational operator + (rational x, rational y);
- friend rational operator - (rational x, rational y);
- friend rational operator * (rational x, rational y);
- friend rational operator / (rational x, rational y);
- ...
- };
类rational声明(用户定义类型)指定了一个rational数字的表示(num, den)以及一组在这rational数字上的操作(构造函数、析构函数、算术操作等等)。rational的表示部分是私有的;num和den,只能透过rational类里声明的函数访问。这些函数可以如下所示定义:
- rational operator + ( rational x, rational y )
- {
- return rational ( x.num * y.den + x.den * y.num,
- // -----------------------------
- x.den * y.den);
- }
- 可以这样使用:
- rational x = 3;
- rational y = 1 / x;
- y += rational(2, 3); // means y = y + 2/3;
- ...
I-5 Object-Oriented Programming 面向对象编程
一个ADT就像是个黑盒子。一旦定义,它就不用和程序其它部分交互。除非修改其定义,否则是无法让其适应其他新的用途的。这导致有点缺乏灵活性。例如,在一个图形系统里,考虑定义一个shape类型。假设这时我们的系统需要支持circles,triangles以及squares。假设我们已有两个类point和color:
class point{ /* ... */ };
class color{ /* ... */ };
也许会这样定义一个类shape:
- // enumerate the different types of shapes
- enum kind { CIRCLE, TRIANGLE, SQUARE };
- class shape
- {
- private:
- point center;
- color col;
- kind k; // representation of shape
- public:
- point where() { return center; }
- void move(point to) { center = to; draw(); }
- void draw();
- void rotate(int);
- // more operations ...
- };
类型k对于draw()和rotate()操作是必须的,用于决定需要处理的shape类型(在Pascal类似语言里,也许会用一个带有tag k的变量)。函数draw()可以定义如下:
- void shape::draw()
- {
- switch (k)
- {
- case CIRCLE: // draw a circle
- break;
- case TRIANGLE: // draw a triangle
- break;
- case SQUARE: // draw a square
- break;
- }
- }
看到这种解决方案的问题了吗?首先所有像draw()函数必须知道所有shape种类。因此每当有一个新的shape增加到系统,这些函数就需要修改,每次就不断增大。如果定义了一个新的shape,每一次在其上面的操作就比需要检查和修改(如果需要的话)。除非你拥有每个操作的源代码,否则不可能增加一个新shape到系统里。因为增加一个新的shape到系统里,涉及到在图形上的每一个重要操作,这不光需要高超的技巧,还可能会引入bug到处理其他shape的代码里。对特定shape表示法的选择可能因为一些需求(至少有些表示法必须与一般类型shape定义提供的固定大小框架相匹配)从而导致更严重的难以理解。
问题是对于任何shape的一般属性(比如,一个shape有颜色,可以被绘制等等)和某个特定shape的属性(比如circle是个有半径,可以由圆绘制函数的一个shape)间没有本质的区别。为了表达这区别,利用其区别定义面向对象编程。带有这构建特性的语言支持描述使用该区别,就能够支持面向对象编程。而其他语言不行。
Simula和C++继承机制提供了一个解决方案。首先,指定一个类定义所有shape的通用属性:
- class shape
- {
- private:
- point center;
- color col;
- // ...
- public:
- point where() { return center; }
- void move(point to) { center = to; draw(); }
- virtual void draw();
- virtual void rotate(int);
- // ...
- };
对于可以定义用于所有调用接口的函数,但是不能为某一个特定shape定义实现的(shape里定义某个函数,能被所有类型shape使用,但是在shape里又不能定义针对所有特定shape的实现),可以定义为virtual。对于这种定义,我们能编写通用的操纵shape函数。
例如,下面函数旋转一个大小为size的vector所有成员,angle角度:
- void rotate_all(shape* v, int size, int angle)
- {
- for (int i = 0; i < size; i++) v[i].rotate(angle);
- }
为定义一个特定shape,首先它是个shape,再指定其特定的属性(包括virtual函数):
- class circle : public shape
- {
- private:
- int radius;
- public:
- void draw() { /* ... */ };
- void rotate(int) {} // yes, the null function
- };
在C++里,circle类继承于shape类,类shape是类circle的一个基类。另一个类似的术语可以这么说circle是subclass,shape是superclass。
面向对象编程范式(OOP)可以定义如下:
1.决定想要类
2.对每一个类提供完整的操作集
3.使用继承让共同性显式表现出来
注意到,OBP编程范式不能充分满足共用性。是用OBP还是OOP编程范式取决于在应用领域使用的类型之间可挖掘的显式共用性多寡。在一个系统里不同的类型之间寻找共用性不是个小工程。可挖掘的共用性的多寡受设计系统方式的影响。当设计一个系统时,共用性必须能积极地被捕捉到--透过特别将类设计为构建其他类型的块,以及检查类看看是否他们展示的相似性能否在一个共用的基类里挖掘出来。
I-6 Summary 小结
这一章展示了各种编程范式:过程式、模块化、基于对象的以及面向对象编程。每一种范式某种程度上可以看作知识分类(knowledge grouping)的一种自然演化。
在过程编程范式我们使用过程来分组共用的语句序列。在模块化编程范式里,我们把数据已经操作这些数据的过程看作一个单元。基于对象的编程引入了用户定义类型的需求--和内建类型行为表现类似。最后,面向对象编程引入类型特定属性和共用属性之间的区别。
从设计者的/理念的观点看来,每一种范式增加了更多智能到语言上,因此要求更强有力的语法。过程只是将语句组合在一起,透过过程调用激活,一个模块知道其操纵的数据,以及因此需要一些初始化。一个ADT也知道允许什么函数来操纵数据,因此要求访问控制和自动初始化和释放,类似于内建类型行为。最后(最智能的)面向对象类型继承一些来自基类的知识,也需要定义(或重定义)
它的特定操作。继承和重定义,这两种行为都得到支持。
随后的章节会详细描述C++语言提供的用于支持面向对象编程的语言机制。
I-7 Exercises 练习
完成stack和multiple_stack的实现。试试,如果stack没有初始化,把一个字符压入stack会发生什么?
不同编程范式之间概念上的差异是什么?
从生活中选一个例子(例如,泡咖啡)试着用不同的编程范式表达它。例如,对于过程式版本的泡咖啡可能是这样:取得咖啡、拿糖、倒水、混合、煮
参考书
[1] Stanley B. Lippman: C++ Primer, second edition, Addison-Wesley Publishing
Co., Reading, Mass., 1991
[2] Bjarne Stroustrup: The C++ Programming Language, third edition, Addison-
Wesley Publishing Co., ISBN 0-201-88954-4
[3] Bjarne Stroustrup: What is ”Object-Oriented Programming”?, Bell Labs, 1991
- 评论列表(网友评论仅供网友表达个人看法,并不表明本站同意其观点或证实其描述)
-
