C++基本概念


  C++是一种面向对象的程序设计语言。从命名中可以看出它与C的渊源——通过在C中添加面向对象的特性,如类、多态和模板等而得到的一个C的超集。C作为C++的子集也自然得到C++开发环境的支持,而且还能够应用由C++带来的特性。
  面向对象程序设计的基本特性可概括为:封装性、继承性和多态性。在本章,我们将了解C++的输入输出、引用和异常处理等基本概念,以及C++中类、重载、继承和多态等抽象机制。

1、输入输出

  与C语言相比,C++引入了许多新的语言特性。我们从输入输出开始介绍名空间、引用、动态对象等。
  输入输出是程序与外界的通信接口。在默认方式下,键盘和显示器成为终端应用程序的标准输入和标准输出设备。C函数scanf和printf可用来完成标准输入输出设备对变量的访问,而且在C++编程环境中可以继续使用。除此之外,C++还提供了新的操作接口,即cin、cout、<<、>>等。

1	/*C program */
2	#include <stdio.h>
3	#include <stdlib.h>
4	
5	int main( ) {
6	  double x, y, z;
7	  printf("Enter the 1st: ");
8	  scanf("%lf", &x);
9	  printf("Enter the 2nd: ");
10	  scanf("%lf", &y);
11	  z = x + y;
12	  printf( "the sum is: %-6.2lf\n", z);
13	  return 014	
14 }	
1	// C++ program
2	#include <iostream>
3	#include <iomanip>
4	using namespace std;
5	int main( ) {
6	  double x, y, z;
7	  cout << "Enter the 1st: ";
8	  cin >> x;
9	  cout << "Enter the 2nd: ";
10	  cin >> y;
11	  z = x + y;
12	  cout << "the sum is: " << left << setw(6) << setprecision(2) << z << endl;
13	  return 014	
14	}

  上面两段代码完成同样的功能。在C++中不仅可以使用C风格的注释“/* */”,还可用“//”表示直到行末的单行注释,如第1行。
  第2行中的头文件iostream声明了cin和cout,注意iostream没有后缀“.h”。第3行的头文件iomanip声明了iostream用于实现格式控制的操纵符fixed、setw、left等。
  第3行的using指令用于操作名空间。名空间起到类似于磁盘文件系统中目录的作用。只要两个文件在不同的目录中,就允许它们有相同的文件名。main函数不在任何空间中,我们把它看做位于全局名空间中。在下面的代码中定义了名空间abc,其中包含两个函数。另有一个重名的函数与main共同位于全局名空间中。

namespace abc{
	void f( ) { }
	void g( ) { }
}
using abc::g;
void f( ) { }
int main( ) {
	f( );
	abc::f( );
	g( );
}

  现在可以观察到using指令的作用。它把abc名空间中的g输出到当前名空间中来,就好像g位于当前名空间中,从而访问g时不必再加上前缀。下列指令
using namespace abc
则输出了名空间abc中的全部名字。在本例中这会引起冲突,因为有两个函数都命名为f。这时可把对全局名空间中的f的访问改为“::f( )”,如下所示。

namespace abc{
	  void f( ) { }
	  void g( ) { }
}
using namespace abc;
void f( ) { }
int main( ) {
	::f( );
	abc::f( );
	g( );
}

  由于cin、cout、setw等在名空间std中声明,为了避免在代码出现大量的前缀,如语句12将被写成

std::cout << "the sum is: " << std::left << std::setw(6) 
			<< std::setprecision(2) << z << std::endl;

所以加上了using namespace std指令。注意主函数应位于全局名空间中。
第7行中的cout与标准输出相关联。<<是输出操作符,它把字符串"Enter the 1st:"输出到cout。第8行中的cin与标准输入相关联。>>是输入操作符,用来从标准输入读入数据。语句cin >> x;把读取的数据存入变量x。注意x前没有取地址运算符&。与标准错误相关联的是cerr,其用法与cout相似。
第12行中,C的格式描述符“%-6.2lf”分别通过“6”、“2”和“-”负号指出输出数据的宽度、精度和对齐方式。在C++中,这些控制可通过操作符setw、setprecision、left来实现。语句末尾的endl起到换行的作用。输出操作符可以连续使用,如本行中的代码。输入操作符也可以连接起来,如

cin >> x >> y;

  如果希望输入不定个数的值,可用如下方式实现。

int x, y = 0;
while (cin >> x)
	y += x;

  在while 循环中,每次迭代都从标准输入读入一个整数直到文件结束时条件(cin >> word)为假。对于键盘而言,在Windows中可按Z,在Linux中可按D键来输入文件结束符。

2、引用

  下面的例子并排列出了有着同等功能的C代码和C++代码。这两段都定义了交换参数的swap1和swap2函数,并在main函数中以间接方式改变x的值。

1	//C
2	#include <stdio.h> 
3	
4	void swap1(int a, int b) {
5	  int t = a; a = b; b = t;
6	}
7	void swap2(int *a, int *b) {
8	  int t = *a; *a = *b; *b = t;
9	}
10	int main( ) {
11	  int x = 1, y = 2, z = x; 
12	  int *q = &x;
13	  z = 3;
14	  printf("%d,%d\n", x, z);
15	  *q = 3;
16	  printf("%d\n", x);
17	  swap1(x, y);
18	  printf("%d,%d\n", x, y);
19	  swap2(&x, &y);
20	  printf("%d,%d\n", x, y);
21	}

运行结果
1,3
3
3,2
2,3

1	//C++
2	#include <iostream>
3	using namespace std;
4	void swap1(int a, int b) {
5	  int t = a; a = b; b = t;
6	}
7	void swap2(int &a, int &b) {
8	  int t = a; a = b; b = t;
9	}
10	int main( ) {
11	  int x = 1, y = 2, z = x;
12	  int &p = x;
13	  z = 3;
14	  cout << x << ',' << z << endl;
15	  p = 3;
16	  cout << x << endl;
17	  swap1(x, y);
18	  cout << x << ',' << y << endl;
19	  swap2(x, y);
20	  cout << x << ',' << y << endl;
21	}

运行结果
1,3
3
3,2
2,3
  在第11行中定义了变量z。z初始化为x意味着把x的值拷贝到z中去。因为z与x是两个不同的变量,所以在第13行把z的值改为3时,x的值不受影响。所以第14行显示1,3。

  第12行C代码中定义了指向x的指针q,第15行修改*q时也就是在修改x的值。所以第16行显示3。在C++代码中定义了指向x的引用p。我们可以把p看做是x的别名,所以访问p就是访问x。例如取p的地址实际上得到的是x的地址,即“&p == &x”为真。

  第4行定义了swap1函数。第17行调用swap1时,形参a、b的值是实参x、y的拷贝。由于形参和实参是不同的变量,所以swap1交换了形参a、b,但实参不受影响。所以第18行显示3,2。

  第7行定义了swap2函数。C代码的形参a、b为指针,虽然它们是实参的拷贝,但a、b与实参&x, &y都指向x和y变量,所以交换a与b也就是交换x、y的值。C++代码声明了引用参数a、b。它们不是实参的拷贝而是实参本身,所以交换a、b实际上交换了实参。因此第20行代码打印2,3。

3、动态对象

  动态对象没有变量名,只能通过指针来对它们进行操作。对它们的分配与释放由程序员负责完成,相对静态对象来说容易出错。

1	int *p = (int *)malloc(sizeof(int));
2	free(p);
3	p = (int *)calloc(1, sizeof(int));
4	free(p);
5	p = (int *)malloc(sizeof(int) * 10);
6	free(p);	

1	int *p = new int;
2	delete p;
3	p = new int(0);
4	delete p;
5	p = new int[10];
6	delete []p;

  C++的new和delete运算符用于申请和释放动态对象。
  第1行申请了一个int对象,与C的动态分配函数相比,它不即需要指定对象占用空间的大小,也不用对申请的结果进行类型转换。注意所申请对象的值未经初始化。

  第3行调用new时带有参数,它把int初始化为0,也相当于语句int x(0)中0的作用。calloc有两个参数,第一个参数表示个数,第二个参数表示单个对象的大小。calloc把所申请的内存初始化为0。如果在调用new时把整数初始化为其它值,如new int(2),在C终究没有对应的语句了。

  第5行申请了一个包含10个元素的数组。释放对象时,delete可释放所申请的单个对象,delete[]则用于释放一个数组,这一点需要留意。

4、异常处理

  异常是程序运行过程中不可预测的情况,如没有访问文件的权限,或者申请内存失败等。C函数处理异常的通常做法是(1)忽略异常;(2)返回错误代码或在全局变量中设置错误代码;(3)退出程序;(4)调用一个预先设定好的错误处理函数等。

  在现实生活中生病找医生是常识。在程序中,这意味着要从发生异常的代码处跳转到处理错误的代码处,而且这种跳转可跨越函数调用,如f调用g,g又调用h,当在h中发生异常时应能跳转到f中的异常处理代码处。这有点像一个能够穿越函数的goto语句。C++的异常处理机制就提供了这样的一种处理方式。

  异常处理机制主要包括一个try语句块以及一个或者多个catch语句块。把有可能出现异常的语句写在try语句块中,catch语句块则是处理异常的代码。一旦try语句块中的语句或这些语句调用的函数在运行过程中检测到发生了异常时,就执行throw语句抛出一个异常对象,throw之后的其它语句将不再执行,控制转移到try之后的catch语句块中。待catch语句块结束后,再继续执行try-catch结构之后的语句。

#include <iostream>
using namespace std;
void h( ) {
	cout << "h() begin\n";
	throw "throw";
	cout << "h() end\n";
}

void g( ) {
	cout << "g() begin\n";
	h( );
	cout << "g() end\n";
}

int main( ) {
	try {
		cout << "main() begin\n";
		g( );
	} catch (const char *p) {
		cerr << "msg = " << p << endl;
	} catch (int eno) {
		cerr << "eno = " << eno << endl;
	} catch (...) {
		cerr << "..." << endl;
  	}
	cout << "main() end\n";
	return 0;
}

  运行结果
main() begin
g() begin
msg = throw
main() end

  throw可抛出各种类型的异常对象。它所抛出的异常对象就是throw的参数。每一个catch捕获一种类型的异常,异常类型参数指定。如catch (const char *p) {…}处理字符串异常,catch (int eno) {…}处理整型异常。catch后的形参就是所捕获的异常对象。把上例中throw "throw"改为throw 20,运行结果的第3行将显示为eno = 10。最后一个catch的参数为“…”,意指在此捕获所有未被处理的异常,类似于switch的default。

  从例中还可以看到,一旦执行throw抛出异常后,控制将转移到try后面的catch语句中,不再执行throw之后的语句。

5、类

  类是C++最重要基本的概念,它是C++的封装性、继承性和多态性等面向对象特性的基础。

  类是抽象概念在C++中的具体表示,也是程序员定义新类型的工具。每一个类型都有一个取值的集合和操作的集合,如整型的取值范围为整数,可以做算术运算和关系运算。另外数据类型还有实现和接口两个方面。实现像是表壳内的齿轮,它包含数据存储等所有必要的细节,接口则类似于表盘上的指针,它规定了所有可用的操作方式。

  在C中虽然可以定义struct类型,但操作它们的函数只能在struct的定义之后单独完成。C没有一个语言成分使得变量连同操作这些变量的函数都是它的组成部分。在C++中,一个类就是一个新的数据类型类,由成员变量和成员函数组成。类的成员变量组成了数据,成员函数规定了操作。从形式上看,类的定义与C的结构相似,只不过是把函数写到了类的里面。下列代码分别用结构和类定义了矩形rect。

1	// C
2	
3	struct rect {
4	  int width;
5	  int height;
6	};
7	
8	void init(rect *r, int w, int h) {
9	  r->width = 20;
10	  r->height = 30;
11	}  
12	int area(rect *r) {
13	  return r->height * r->width;
14	}
15	
16	int main( ) {
17	  rect r;
18	  init (&r, 20, 30);
19	  printf(%d\n", area(&r));
20	  return 0;21	};	

1	// C++ 
2	using namespace std;
3	class rect {
4	private:
5	  int width;
6	  int height;
7	public:
8	  rect (int w, int h) {
9	    height = h;
10	    width = w;
11	  }
12	  int area( ) {
13	    return height * width;
14	  }
15	};
16	int main( ) {
17	  rect r(20, 30);
18	
19	  cout << r.area() << endl;
20	  return 0;21	};

  C++代码中,第3行用关键字class开始类定义,其后是类名以及花括号中的变量和函数,这些变量和函数是类的成员。类定义的基本形式为“class X {…};”。注意最后的分号是必要的。它与结构的定义“struct X {…};”有相似的框架。

  第4行的private是对类成员的访问控制,它强调在此之后(直到下一个访问控制出现为止)声明的类成员是类的私有成员。这些私有成员属于类的实现细节,不允许类用户(使用类的程序员)对它们进行访问。换句话说,访问私有成员的代码只能位于class之后的那一对花括号中,而不能位于类定义之外。如在main函数中,r是一个rect类的对象, r.width会导致一个编译错误错误(无法访问 private 成员)。C++约定,在类定义的开始部分没有指定访问控制的那些成员默认属于类的私有成员,所以第4行的private可以被省略掉。

  第5、6行定义了两个变量,它们是类的成员变量。因为它们之前有private,所以是类的私有成员。

  第7行的public是另一种访问控制。它允许类用户访问此后定义的类成员,直到下一个访问控制出现为止。public实际上是在描述类接口。注意访问控制单词后面的冒号。

  第8-11行定义了一个构造函数。构造函数有两个显著特征,一是函数名与类名相同,而是函数没有返回值类型,也不能写void。构造函数用于初始化类的成员变量,它在声明类变量时自动被调用。声明中变量名后的参数列表会作为实参传递给构造函数。与C中显式调用init来初始化相比,对构造函数的调用是隐式的。如弟17行声明r时以参数20和30调用构造函数。

  第12-14行定义了一个类的成员函数area,用于计算矩形的面积。它没有参数,由于其代码位于类定义的里面,所以可以直接访问私有成员变量。C代码中的area函数需要参数来指出计算哪一个area对象的面积。

  第17行声明了一个rect类型的变量r。它是类作为类型而存在的具体体现。把整型x初始化100可写作

int x = 100;
int x(100);

这里声明r的格式相当于后者,通过自动调用构造函数完成初始化。

  第18行,C调用init来为对象赋值,而C++在第17行已通过构造函数完成了初始化。
  第19行调用了area函数。C代码把r作为area的参数area(&r),area当然知道在计算谁的面积。C++代码中,r.area()中的area是如何知道要算谁的面积呢?C++在调用对象的成员函数时会隐含地把指向对象的指针传给函数。因为总是需要传递这样一个指针,所以它作为一个隐含参数而没有在参数列表中出现。该指针被命名为this,所以C++的area的代码实际上是

int area( ) {
	return this->height * this->width;
}

因为编译器保证隐含传递this指针,所以area也就可以明确要计算谁的面积了。在调用r.area时,this指针将指向r。因此,即使有两个rect类型的变量r1和r2,

rect r1(10, 20), r2(30, 40);
	cout << r1.area( ) << ‘\n‘ << r2.area( ) << endl;

rear也能够分别计算出r1和r2的面积。对比C++的r.area()和C的area(&r),它们的区别只是r的位置有所改变,从括号里面移到了外面。

  最后看看析构函数。观察一段操作结构的代码,留意其中的指针字段。

struct A {
  int *p;
};
int f( ) {
  A *a = (A *)malloc(sizeof(A));
  a->p = (int*)malloc(sizeof(int));
  free(a);
  return 0;
}

  函数f中有内存漏洞。它做了两次内存申请,一次是为a,一次是为a->p。在释放内存时虽然释放了a本身所占的内存,但为a->p申请的内存并未释放。所以释放结构对象的内存时,应考虑释放为其成员单独申请的其它资源(分配的内存、打开的文件等)。在C++中,申请资源可在构造函数中完成,相应的还有一个析构函数用于释放这些资源。析构函数的书写格式要求函数名为:~+类名,并且不带参数,如~A( )。析构函数在对象失效或释放时自动被调用。

  下面是一个简单的类A。它有变量成员p、构造函数和析构函数。函数g用两种方式创建了A类型的对象。一种是直接声明A的变量a,然后通过a来访问其成员函数。我们还可以声明A类型的指针p,这与声明其它类型指针(如整型指针)的语法是一致的。

1	class A { 
2	  int *p;
3	public:
4	  A(int n) { p = new int(n); }
5	  ~A( ) { delete p; }
6	};
7	void g( ) {
8	  A a(2);
9	  A *q = new A(3);
10	  delete q;
11	}

  第8行中的a(2)以参数2调用构造函数。在构造函数中,排指向一个动态整型变量,该变量被初始化为2。

  第9行中的new A(3)创建了A类型的一个动态对象,并用3调用构造函数来完成初始化。在这次调用中,构造函数也创建了一个动态整型变量。

  第10行中的delete释放动态对象,它会自动调用A的析构函数~A()。在析构函数中调用delete p释放了动态成员对象。由此可见,程序员在使用类时,由于C++会自动调用构造与析构函数,在一定程度上减轻了程序员的负担。
第11行函数g结束。这时,g的局部变量也会被释放掉。由于a是A类型的对象,释放a时会自动调用A的析构函数,这样a中排指向的动态成员也就随着析构函数的执行而被释放掉了。

6、静态成员

  全局变量可用于在函数之间传递数据。自然,它也可用于在类的成员函数之间和类的不同对象之间传递数据。分析下列代码中变量count的作用。

int count;
class A {
public: 
	void print( ) { cout << count; }
	void f( ) { count++; }
};	
class A {
	int count;
public: 
	A( ) { count = 0; }
	void print( ) { cout << count; }
	void f( ) { count++; }
};	

class A {
	static int count;
public: 
 	static void print( ) { cout << count; }
	static void f( ) { count++; }
} 
int A::count;
void g() {
	A a, b, c;
	a.f();b.f();c.f();
}

  第一段代码中的count是全局变量,所以count变量只有一个。无论是调用a.f()、b.f()还是c.f(),它们访问的是同一个全局变量count,而且都对它做了加1操作。所以count记录了各对象调用f函数的总次数。

  中间代码中的count是类A的成员变量,每个A的对象都有自己的count变量。每次调用f函数时,如调用a对象的函数a.f(),f把a自己的count变量加1,而不会改变b.count和c.count。所以count记录了各对象自己调用f函数的次数。

  使用全局变量的优点是通信,不同的函数和对象通过它实现数据共享。使用成员变量的优点是安全,在类的外部不能访问类的私有成员。能否把这两种优点结合到一起,好比静态局部变量结合了全局变量的生存期和局部变量的作用域那样?这就是类的静态成员变量,如右边代码中的count。我们注意到count被声明了两次,一次是在全局名空间中声明为全局变量(代码的最后一行),同时又声明为类A的私有成员(第2行)。由此可见类的静态成员变量就是专门归该类使用的全局变量而已,它可看作是属于类的变量,而其它的非静态成员变量则可看作是属于对象的变量。从数量上来看,静态成员是一个为所有类对象共享的变量。其它非静态成员变量是每一个对象都有专属于自己的那一个,因而有多少个对象就有多少个变量。

  代码3中的count是静态成员变量,这一点由关键字static来标志。成员函数print和f前也有static关键字,它们是类的静态成员函数,专门用于访问静态变量,但是不能访问非静态的成员变量和成员函数。但是反过来,非静态成员函数可访问静态成员。例如把中间代码声明的count改为static int count,并且加上声明int A::count;后,它也能通过编译。但是仅把f函数改为静态成员就不能通过编译了。这时应为不允许它作为静态成员函数去访问非静态成员变量。这有点像个人可以使用公共资源,单反过来,公众不应该随意使用个人资源。

  在类外部访问其静态成员时,如访问A的成员print,可用语句A::print( )实现,即在函数名或变量名前加上类名及域操作符“::”即可。

7、重载

  当声明一个变量或创建一个对象时,有多种初始化方式。

1	int a(10);
2	int b = 20;
3	int c = a;
4	int d(a);
5	int e;
6	new int;
7	new int(10);
8	new int(a); 

  第1、2行直接给出初始数据,第3、4行利用已有对象a来初始化c和d。第5行没有用参数初始化e。对动态创建的对象也是如此,如6-8行。既然有多种方式来做初始化,所以也应该有多个完成初始化任务的构造函数。构造函数的函数名必须是类名,所以会出现多个同名函数。这就是C++中重载的来由。

  按照编写构造函数的要求,即名称相同且不写返回值,编写多个构造函数时它们之间的差异只能是参数列表有所不同。这种现象叫做函数的重载,意思是说用一个名字来命名多个函数。这些同名函数位于同一个类中,或者都在全局名空间中定义,而且参数列表必须有所不同,但对返回值没有要求。如可以在同一个类中定义如下5个函数:

void f( );
void f(int x, char *p);
void f(char *p, int x);
int f(int x);
double f(int x, int y);

  它们虽然有相同的函数名,但参数的个数,或者参数的类型,再或者参数的顺序两两不同。在程序中调用其中的一个函数时,编译器能够根据实参列表从这些同名函数中选择一个正确的版本。如对下列调用,

x = f(2, 3);
y = f();

编译器不难判断出前面5个函数中的哪一个是所需要的版本。

  重载指用一个名字代表多个函数。对运算符而言,如果把它们所代表的操作也看作是函数的话,同一个运算符也应该能够表示多种操作,也就可以用于我们新定义的类类型。例如 “+”运算符,既能用于整型,也能用于浮点型,如何让它能够用于我们新定义的A类型呢?

A a, b, c;
c = a + b;
c = a.add(b);

否则,对自定义的类类型就只好直接写函数调用。对比最后两行代码,可读性孰优孰劣一目了然。

  运算是函数功能的体现,如求和操作a + b相当于调用了一个函数some_add_fun(a, b)。C++把这个函数命名为operator+。计算a + b时,实际调用的是a.operator+(b),所以只需要在a的类型中定义operator+函数,然后就能编写a + b风格的代码了。C++可以重载四十余种运算符,包括算术运算、关系运算、位操作符、括号([]和( ))、箭头(->或->*)等。

  接下来我们尝试构造一个类似于数组的array类,使得它的对象a支持int x = a[“c”]及a[“c”] = 2形式的语句。其特点是用字符串作下标,数组元素是整型,正好与数组string b[10]的下标和元素类型相反。a的方括号操作a[“c”]是函数调用a.operator,因此需要在array中定义成员函数

int& operator[](string id);

  注意返回值的类型为引用。在赋值表达式中,等号左边总是要求一个左值。当函数f()返回一个整型指针时,*f() = 2是合法的表达式。当函数f()返回一个整型变量的引用时,f() = 2也是合法的表达式。如果把operator[]的返回值类型由int&改为int,x = a[“c”]仍旧是正确的表达式,但是a[“c”] = 2就不再合法了。

  array是一个从有限字符串集到整数集的映射。与数组不同的是,无法预先指定array下标的取值范围。访问a[“a”] = 2后,下次再访问a[“a”]时,它返回存储2的数据对象的引用。可用两个数组idx和val来实现这一点。首次访问a[“a”]时,把"a"添加到idx数组中。以后再访问a[“a”]时,从idx数组中查询出"a"的下标i,返回对val[i]元素的引用。通过该引用就能读写与"a"相对应的整数值了。如果要读取a[“c”]的值,但"c"又不在idx数组中时,返回一个缺省值defa。它作为参数在构造函数中指定。

  array最初把idx和val各初始化为大小为8 的数组。数组大小信息保存在max_len中。已经被利用的数组元素个数位于length中。当length == max_len时表示数组已满。这时如果出现对新串(不在idx中)的访问,则扩充idx和val的容量为原来的两倍。在释放array数组的对象时,析构函数将释放idx、val和所有字符串。

	#include <iostream>
	using namespace std;

	class array {
	protected:
		char **idx;		//存储字符串下标
		int *val;		//存储对应的整数值
		int defa;		//缺省值
		int max_len;	//数组的大小
	public:
		int length;		//实际利用的数组元素个数
		array(int defa_val) {
			max_len = 8;
			length = 0;
			val = new int[max_len];
			idx = new char*[max_len];
			defa = defa_val;
		}
		~array( ) {
			for (int i = 0; i < length; i++)
				delete[] idx[i];	//释放下标字符串
			delete[] idx;
			delete[] val;
		}
		int& operator[](char* id) {
			for (int i = 0; i < length; i++) {	//查找id是否出现过
				if (strcmp(idx[i], id) == 0)
					return val[i];	//id出现过,返回对应的val元素
			}
			//没找到id,添加进idx
			if (length == max_len) {	//数组满 
				max_len *= 2;
				int *val_tmp = new int[max_len];	//申请更大的数组
				char **idx_tmp = new char*[max_len];
				for (int i = 0; i < length; i++) {	//复制已有元素
					val_tmp[i] = val[i];
					idx_tmp[i] = idx[i];
				}
				delete val;	//释放原来已满的数组
				val = val_tmp;
				delete idx;
				idx = idx_tmp;
			}
			idx[length] = strdup(id);	//赋值下标字符串
			val[length] = defa;			//新下标对应缺省值
			return val[length++];		//返回对应的val元素
		}
	};

	int main( ) {
		array a(0);
		a["hello"] = 111;				//写数据
		cout << a["hello"] << endl;		//读数据
		return 0;
	}
8、派生类

  概念常常意味着分类,如图形处理系统中的矩形、三角形和圆形等,它们都是几何图形这一大类中的小类。又如学籍管理系统中研究生是学生,本科生也是学生。在用类表示概念时,派生和继承可用来表示类与类之间的关系。下面先说明继承的概念,然后再通过实例讨论引进继承的意义。

  继承有公有、私有和保护三种形式。

	class A {…};
	class B: public A {…};		//B公有继承A
	class B: protected A {…};	//B保护继承A
	class B: private A {…};	//B私有继承A

  B继承A时,A和B分别叫做基类和派生类。B(私有)继承A表示B是用A来实现的,它与在B中包含一个A类型的变量时所起的作用相似,体现了B“has a”A这样一种关系。偶尔会遇到这种用法。保护继承很少使用,所以下文提到继承时指公有继承。

  伴随继承未来的首先是成员函数的可访问性问题。其基本原则是B继承A时,A的公有成员成为B的公有成员,A的保护成员成为B的保护成员,A的私有成员在B中不可见。需要注意的是A的保护成员不能用于在派生类B中访问A类型的对象。

  假设class A定义了fa1(私有)、fa2(保护)和fa3(公有)三个函数。B继承了A,虽然B没有定义fa3函数,但因为B继承A,所以我们也就能访问B对象的fa3函数了,如第15行。

1	class A {
2	private: 
3		void fa1() { }
4	protected: 
5		void fa2() { }
6	public: 
7		void fa3() { }
8		virtual void f4() { cout << "A::f4” << endl; }
9	};
10	
11	class B: public A {	//B继承A
12	public:
13		void fb() { 
14			//fa1(); 	//派生类不能访问基类私有成员
15			fa2(); 		//在派生类函数中能够访问基类保护成员
16		}
17		void f4() { cout << "B::f4\n"; }
18	};
19	void f() {
20		B b;
21		b.fa3();
22		A *p = &b
23		P -> f4();
24	}

  第8行,基类定义了一个虚拟函数f4。第17行,派生类B也定义了有相同原型的函数f4,这种现象叫做重写。第22行,定义了指向派生类对象的基类指针p,并p调用f4函数。由于在A和B中各有一个f4函数,于是哪一个版本的函数被调用了?答案是派生类B的f4。这就是多态性。它的要点是(1)指向派生类对象的基类指针调用;(2)被派生类重写的;(3)基类虚拟函数;(4)导致派生类版本的函数被调用。

9、多态性

  可维护性是评价软件质量的重要指标之一,良好的设计应能控制变化带来的影响。考虑一个几何图形应用程序,它判断平面上一个点是否落在一组给定的圆形和矩形之中。如果在设计完成之后又需要添加三角形,怎样设计才能使系统富有弹性,不变动已有代码也能添加新功能?我们通过对比两种设计来体会继承和多态的作用。

  由于矩形和圆形都是几何图形,为了把这两种类型的对象统一存储在一个数组ps中,用一个类shape来统一表示它们,每个shape对象都是一个图形。矩形用左上角和右下角这一对坐标表示(x1, y1, x2, y2)。圆形用圆心坐标和半径(x1, y1, r)表示。枚举类型SHAPE用于标识图形的种类。append用于在数组ps中添加一个图形对象,find用于查找给定点是否位于数组中的某个图形之中。

	enum SHAPE {RECTANGLE=1, CIRCLE};
	class shape {
	public:
		int shape_id;
		double x1, y1, x2, y2, r; 
		
		shape(int id, double a, double b, double c, double d) {
			switch (id) {
				case RECTANGLE:
					shape_id = id; x1 = a; y1 = b; x2 = c; y2 = d; 
					break;
				case CIRCLE:
					shape_id = id; x1 = a; y1 = b; r = c; 
					break;
			}
		}
		//点(x, y)是否位于本图形中
		int in(double x, double y) {
			switch (shape_id) {
				case RECTANGLE:
					if (x1 <= x && x <= x2 && y1 <= y && y <= y2)
						return 1;
					break;
				case CIRCLE:
					if ((x - x1) * (x - x1) + (y - y1) * (y - y1) <= r * r)
						return 1;
			}
			return 0;
		}
		
		void print() {
			switch (shape_id) {
				case RECTANGLE:
					out << "矩形:" << x1 << "," << y1 << "," << x2 << "," << y2;
					break;
				case CIRCLE:
					out << "圆:" << x1 << "," << y1 << "," << r;
					break;
			}
			out << endl;
			return out;
		}
	};

	static const int MAX_LEN = 100;
	shape *ps[MAX_LEN];
	int pos;

	shape* find(double x, double y) {
		for (int i = 0; i < pos; i++)
			if (ps[i]->in(x, y))
				return ps[i]; 
		return NULL;
	}
	
	void append(shape *s) {
		if (pos < MAX_LEN)
			ps[pos++] = s;
	}
	
	int main( ) {
		append(new shape(RECTANGLE, 1, 1, 2, 2));
		append(new shape(CIRCLE, 0, 0, 2, 0));
		append(new shape(RECTANGLE, 3, 1, 2, 2));
		shape *p = find(1.2, 1.2);
		if (p) {
			cout << fixed << setprecision(2) << endl;
			p->print();
		} else
			cout << "没有图形包含点("<< x << ',' << y << ')' << endl;
		return 0;
	}

  代码中多次判断图形的种类,如构造函数shape、判断图形是否包含点的函数in等。如果要包含三角形,在所有的判断代码处都必须做调整,加上处理三角形的代码。如果没有这些判断语句就可在添加新图形时不必调整已有的程序。

  对于两个类A和B而言,A派生B也可说成是B继承A。当B继承A时,与日常生活中对继承的理解相似,B将拥有A定义的那些变量和函数,即A的代码在B中被重用了。B除了重用A的代码之外还有B自身的代码,所以继承是一种扩充。
B继承A意味着B的对象“is a”A的对象,有点种瓜得瓜之意。如果B和C都继承自A,A可作为B与C共同的接口。指向A的指针既可以指向B类型的对象,也可以指向C类型的对象。用一个A*类型的指针数组就能引用B和C两种类型的对象。简单地说就是用一个数组存两类数据。

  既然圆形和矩形都是图形,所以可以设计一个表示一般几何图形的基类shape,圆形(circle)、矩形(rectangle)等具体的几何图形由基类派生出来,它们的对象也由指向基类的指针来引用。通过基类指针调用虚拟函数时,系统会自动选择所引用的对象类型的重写函数版本。添加三角形时,一旦定义了三角形类(triangle)为shape的派生类,其对象就能被shape指针访问,所以能够被添加到ps数组中。在find查找几何对象时会调用in函数,对于三角形对象系统会自动选择调用triangle版本的in函数。如果某三角形对象满足条件(包含给定点),triangle版本的print函数又被调用,打印出三角形的顶点信息。概括来说就是根据对象选函数,不用我们再写判断类型的语句。

  这里多次提到多态性的含义,即用基类指针调用函数时能自动选择所指对象类型的函数版本。这个选择本来是用条件语句来完成的(设计一),而现在是由编译器来完成。所以原来的判断消失了。

	class shape {
	public:
		virtual bool in(double x, double y) { return false; }
		virtual void print() { }
	};

	class rectangle: public shape {
		double x1, y1, x2, y2; 
	public:
		rectangle(double x1, double x2, double y1, double y2) {
			this->x1 = x1; this->y1 = y1; this->x2 = x2; this->y2 = y2;
		}
		bool in(double x, double y) { 
			return x1 <= x && x <= x2 && y1 >= y && y >= y2;
		}
		void print( ) {
			out << "矩形:" << x1 << "," << y1 << "," << x2 << "," << y2;
		}
	};

	class circle: public shape {
		double cx, cy, r; 
	public:
		circle(double x, double y, double radius) {
			cx = x; cy = y; r = radius;
		}
		bool in(double x, double y) { 
			return (x - cx) * (x - cx) + (y - cy) * (y - cy) <= r * r;
		}
		void print( ) {
	 		cout << "圆:" << cx << "," << cy << "," << r << endl;
		}
	};

	//这些代码没有变化
	const int MAX_LEN = 100;
	shape *ps[MAX_LEN];
	int pos;
	shape* find(double x, double y) {
		for (int i = 0; i < pos; i++)
			if (ps[i]->in(x, y))
				return ps[i]; 
		return NULL;
	}
	void append(shape *s) {
		if (pos < MAX_LEN)
			ps[pos++] = s;
	}

  现在可以添加三角形了,但我们可以不改变已有的代码。

	class triangle: public shape {
		double xs[3], ys[3];
	public:
		triangle(double x1, double y1, 
			double x2, double y2, double x3, double y3){
			xs[0] = x1; ys[0] = y1; 
			xs[1] = x2; ys[1] = y2; 
			xs[2] = x3; ys[2] = y3;
		}
		//n1和n2为下标三角形中两顶点,从而确定一条直线
		//判断点在直线哪一侧。一侧返回真,另一侧为假。
		//由两点可以确定直线的两点式方程:
		//(y–y2)/(y2-y1)=(x-x2)/(x2-x1)
		//直线上的点使方程成立,直线一侧的点使得:左边>右边
		//另一侧的点使得:左边<右边
		bool calc_side(int n1, int n2, double x, double y) {
			return (y - ys[n1]) / (ys[n2] - ys[n1]) 
				 - (x - xs[n1]) / (xs[n2] - xs[n1]) >= 0;
		}

		//判断给定点与三角形某顶点(n3)是否位于另外两顶点所定直线的同侧。
		bool same_side(int n1, int n2, int n3, double x, double y) {
			return calc_side(n1, n2, xs[n3], ys[n3]) 
				== calc_side(n1, n2, x, y);
		}

		//给定点与三角形的三个顶点都同侧时,给定点位于三角形中
		bool in(double x, double y) {
			return same_side(0, 1, 2, x, y) 
				&& same_side(1, 2, 0, x, y) 
				&& same_side(2, 0, 1, x, y);
		}

		void print( ) {
			cout << "三角形:" 	<< '(' << x[0] << ',' << y[0] << "), "
				 << '(' << x[1] << ',' << y[1] << "), " 
				 << '(' << x[2] << ',' << y[2] << ')' << endl;
		}
	};

		int main() {
			//三角形测试数据
			append(new triangle(1, 0, 0, 1, -1, -1)); 	
			append(new rectangle(1, 2, 2, 1));
			append(new circle(2, 2, 1));
			double x = 0, y = 0;
			shape *p = find(x, y);
			if (p) {
				cout << fixed << setprecision(2) << endl;
				p->print();
			} else
				cout << "没有图形包含点("<< x << ',' << y << ')' << endl;

			system("pause");
			return 0;
		}

  对比几何图形问题的两种方案。第一种设计在需要增加新的图形时需要修改多处代码,尤其是那些判断图形种类的部分。尤其是图形操作种类更多的时候,如计算面积、周长、重心、旋转等,修改代码的工作量可想而知。第二种设计已经包含了三角形这一新的几何图形。即使再增加椭圆,所需的改动仅仅是添加椭圆类本身,其它部分任然不需做任何调整(main需要添加一些椭圆对象测试数据)。有人认为应该把所有需要条件语句的地方改为用多态性来实现,这有滥用多态之嫌。但这种说法的确从某种程度上反映了多态的用途,即封装可变性。

  最后需要说明的是这个例子中基类没有实现太多的功能。实际上在处理几何图形时,有很多操作是各类图形所共有的,如根据端点的坐标计算线段的长度,求点到直线的距离等。实现这些功能的函数可放到基类中,供所有派生类使用。这也是继承性的应用。

  深入学习C++可阅读《C++程序设计原理与实践》(C++之父的最新力作)。它不仅介绍了C++程序设计的基本原理,而且包含了对C++思想和历史的讨论以及经典实例(如矩阵运算、文本处理、测试以及嵌入式系统程序设计)的展示,为读者呈现了一幅程序设计的全景图。Stanley B.Lippman的《C++ Primer》。这本久负盛名的经典教程不仅适合于初学者,而且是一本可供随时查阅的参考书。 几乎在所有C++书籍的推荐名单上Scott Meyers著的《Effective C++》都会位居前三位。它适合于案头常备,反复阅读。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值