类的性质
上文的例子中用到了类,也知道了类的定义方法,其实类还有更多的性质,这些更多的性质完整支持了面向对象编程。
封装
以前说过,程序就是数据和代码的组合。而C++又正好提供了对数据的封装功能,这就可以很好的完成数据和代码的组合。还是先上代码:
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
class A
{
public:
A()
{
_data = 10;
std::cout << "create A, _data is " << _data << std::endl;
}
~A()
{
std::cout << "destory A" << std::endl;
}
void setData( int v )
{
_data = v;
}
int getData()const
{
return _data;
}
protected:
int _data;
};
int main ( int argc, char *argv[] )
{
A a;
// 输出a中变量的值
std::cout << "a._data is " << a.getData() << std::endl;
a.setData( 20 ); // 改变值为20
std::cout << "a._data is " << a.getData() << std::endl;
A *a1 = new A; // 动态创建A的另一个变量
delete a1; // 删除这个变量,这时候应该有输出"destory A"
a1 = NULL;
return 0;
}
编译后输出:
create A, _data is 10
a._data is 10
a._data is 20
create A, _data is 10
destory A
destory A
这段简单的代码,定义了一个名叫A的类,此类中有一个int类型的变量,叫做_data,这个变量的访问属性为protected,说明在类的外部是无法访问此变量的。
构造函数
这个叫做A的类中有一个和类名相同的函数叫做A();这个函数叫做构造函数,在定义A的变量时候被调用,本例中在A a;和A *a1 = new A这两行调用。
要注意,构造函数没有返回类型。
如果没有手动定义构造函数,C++总是自动添加一个没有任何参数的默认构造函数,如下代码:
class B
{
public:
void setData(int v ){_data = v;};
int getData()const{return _data;};
protected:
int _data;
};
这个例子中没有B的构造函数,这时候_data没有被初始化,则里面的值不确定。可以使用
B b;
std::cout << b.getData() << std::endl;
做个测试,每次程序运行时,都会输出不同的值。
除了不带参数的构造函数,还可以再定义一个或多个带参数的构造,当然函数名还是和类名相同,如下代码:
class C
{
public:
C(){ _data = 0; };
C( int v ){ _data = v;};
int getData()const{ return _data;};
protected:
int _data;
};
int main( int argc, char *argv[] )
{
C c;
std::cout << c.getData() << std::endl;
C c1(10);
std::cout << c1.getData() << std::endl;
return 0;
}
编译后输出:
0
10
可以看到,C类有两个构造函数,一个是默认构造函数,一个是带参数的构造函数。带参数的构造函数把定义C时给的10放到了_data变量里。这时候就相当于给了另一个默认值10,很多时候都会用到带参数的构造函数。
析构函数
回到最开始的例子可以看到还有另一个“~A()”这个函数,叫做析构函数,此函数会在清理掉A的变量时候被调用,即被销毁的时候被调用。这个函数在类的定义过程中最多只有一个,或是不定义析构函数,这时候C++编译器也会添加一个默认的什么功能也没有的析构函数。一般情况下需要定义这个析构函数用于清理一些自己创建的变量,向第一个例子中main函数上一行中增加如下代码:
class C
{
public:
C(){
_a = new A;
};
~C(){
delete _a;
};
protected:
A *_a;
};
// 修改main函数代码如下
int main ( int argc, char *argv[] )
{
{
C c;
}
return 0;
}
编译运行:
create A, _data is 10
destory A
可以看到,变量_a的析构函数被调用,也就是_a被销毁了。
修改代码,注释掉
// ~C(){
// delete _a;
// };
编译后再运行:
create A, _data is 10
这时可以看到_a的析构函数没有被调用。
其实在这里主要是_a的生成方式不同。在这里是用的new操作符动态创建的。一般使用new操作符分配的变量,一定要在不用的时候配合delete操作符把分配的变量删除。C++并不知道什么时候删除无用的变量,一定要程序员手动添加删除代码。
成员函数
在定义类的变量的时候,_data的访问属性是protected类型,在上一节可以知道,这种类型的变量无法在类的外部访问,也就是没有办法直接使用这个变量,为了能访问它,在代码中添加了两个函数
- setData:此函数带一个参数,函数内部用这个参数给_data新值。
- getData:此函数功能更简单,只是把_data值返回给调用者。
而getData和setData的访问属性为public,也就是在类的外部可以调用。而在main函数中,也确实是调用成功了。这样程序就有了修改类A中_data变量的方法,但是也只能通过这两个函数来访问A中的变量。
到这里,所谓的封装可能也就很清楚了。即隐藏数据,提供访问方法。这样的思想在本例中可能很简单,但是放到更复杂的代码环境中就会发现使用这种封装的方法管理数据在逻辑上非常清楚。
继承
所谓的继承,也是面向对象编程中一个很重要的概念。先说一个为什么要继承。首先是在代码上可以重用,再就是功能上可以扩展,而再增加虚函数概念后会发现,通过继承还能改变类的行为。虚函数这个东西以后再说,本节先把继承搞清楚。
上一个简单的代码:
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
class A
{
public:
A()
{
_data = 10;
std::cout << "create A, _data is " << _data << std::endl;
}
~A()
{
std::cout << "destory A" << std::endl;
}
void setData( int v ){
_data = v;
};
int getData()const{
return _data;
};
protected:
int _data;
};
class C : public A
{
public:
C(){
_data2 = 0;
}
void setData2( int v ){
_data2 = v;
};
int getData2()const{
return _data2;
};
int sum()const{
return _data + _data2;
}
protected:
int _data2;
};
int main ( int argc, char *argv[] )
{
C c;
c.getData();
c.getData2();
std::cout << c.sum() << std::endl;
return 0;
}
观察代码,可以看到本例中定义了两个类, A和C。
其中C的定义后面有 “public A”。
这就是表示类C是从类A继承而来。
而“public”表示此继承是公有继承,同样,“public”可以换成"protecte"和"private"。在本例中不能换,换后的效果可以自己做测试。
然后在main函数中定义了一个C的变量c。
这时候使用此c变量可以成功调用A的getData函数和自己的getData2函数。说明类C拥有了类A的功能,这就是对类A功能的继承,同样里面的数据也一起继承了过来,即在C中可以直接操作A的成员变量_data。
因为继承这种从上到下的关系,一般会把A叫做C的父类, 类关系图如下:
多态
在初识C++程序前面的代码例子中已经看到过virtual这个关键字。当时是为了让某个函数有和父类函数不同的功能而添加。
虚函数
使用virtual修饰的类的函数,叫做虚函数,父类中的虚函数可以由子类重新实现,这样在外部调用时会找到子类的虚函数定义。
虚函数又分为两种,普通虚函数,纯虚函数,比如下面两个类的定义:
class A
{
public:
A(){};
virtual void fun(){printf("from a fun\n");};
};
class B : public A
{
public:
B(){};
virtual void fun(){printf("from b fun\n");};
}
在这里B类的fun函数会覆盖掉A的fun函数。如下的调用方式:
A *lpA = new B;
lpA->fun(); // 这里调用的是B的fun函数,虽然是用的A类型的指针,但是最终也是调用B的fun函数。
A *lpA2 = new A;
lpA2->fun(); // 这里调用的是A的fun函数。
需要注意的是,只要父类中的任何一个函数声明为虚函数,则子类中不管是否使用virtual修饰同样的函数,此函数都一直是虚函数。
虚函数的使用
更多的时候,虚函数的定义是为了提供一致的函数调用方法,也叫做定义接口。大多数时候程序员的工作是为了提供一个运行框架,而更多的功能是围绕这个框架添加,这时一致的接口调用就非常重要,在C的时候程序员可能会用函数指针来解决这个问题。而C++语言提供了虚函数这个特性,那么在不修改主框架代码的情况下,偷偷的替换里面的某一段代码功能就成为可能,比如下面这种情况:
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
class Computer
{
public:
Computer()
{
_a = 0;
_b = 0;
}
void setA( int a ){
_a = a;
}
void setB( int b ){
_b = b;
}
virtual void work()
{
printf("use Computer work\n");
_r = _a + _b;
};
void printResult()
{
printf("%d\n", _r);
}
protected:
int _a;
int _b;
int _r;
};
class Computer2 : public Computer
{
public:
Computer2()
{
}
virtual void work()
{
printf("use Computer2 work\n");
_r = _a * _b;
}
};
int main ( int argc, char *argv[] )
{
std::string workType = argv[1];
Computer *lpComputer = NULL;
int a = 10;
int b = 3;
if ( workType == "2" )
{
lpComputer = new Computer2( );
}else
{
lpComputer = new Computer( );
}
lpComputer->setA( a );
lpComputer->setB( b );
lpComputer->work();
lpComputer->printResult();
delete lpComputer;
return 0;
}
编译运行:
$ ./virtualclass.exe 1
use Computer work
13
Administrator@WIN-R9MT13JQHOK /e/workspace/book
$ ./virtualclass.exe 2
use Computer2 work
30
第一次运行时给程序带了一个外部参数“1”,第二次运行时给程序带了一个部分参数“2”。最终得到了两个不同的输出。
此代码在main函数中还能看到是通过判断不同的输入而生成了不同Computer实例。在这里表现的并不明显,可以接着做个改进,把创建Computer指针的代码封装成一个函数,如下:
Computer *createComputer( std::string const& t )
{
Computer *lpComputer = NULL;
if ( t == "2" )
{
lpComputer = new Computer2;
}else
{
lpComputer = new Computer;
}
return lpComputer;
}
int main ( int argc, char *argv[] )
{
std::string workType = argv[1];
Computer *lpComputer = createComputer( workType );
int a = 10;
int b = 3;
lpComputer->setA( a );
lpComputer->setB( b );
lpComputer->work();
lpComputer->printResult();
return 0;
}
在这个例子中,main函数的工作过程不变,但是当要切换计算方法时,还是只要给出不同的工作类型,2,或是1,就能让main函数的工作方式完全改变。
这时候,如果再给一个新的工作方式“3”,只需要修改createComputer的实现而不用修改主函数的任何代码。这个结构就给了对程序进行动态扩展的能力。比如把createComputer这个函数和计算类放到另一个动态库中,由其他人开发这个动态库,他只要在有新的功能时把这个动态库更新就可以给整个程序做功能升级。
纯虚函数的使用
纯虚函数的定义方法如下:
class A
{
public:
A(){};
virtual void fun() = 0;
}
这时候不能直接定义类A的变量,只能定义指针:
A a;///这一行在编译时报错.
A *lpA = NULL;// 这一行可以正确编译
在多数情况下纯虚函数只是为了规范一系列类的接口,比如有一个文件操作类:
class File
{
public:
File(){};
virtual ~File(){};
virtual bool open() = 0; //打开文件
virtual bool close() = 0;//关闭文件
virtual bool write() = 0;//写文件
virtual bool read() = 0;//读文件
};
这样一个文件操作类,使用者只知道这个类有四个可用的功能函数,而每个函数的具体实现,就要根据文件类型分别实现。使用者能做的就是使用文件指针调用这几个函数完成文件操作功能,而不必关心里面的实现,这在一定程度上也把程序的复杂度降了下来,又对细节做了封装隔离。
一般来说对它会有如下简单的扩展
使用这种方法隔离实现细节。
到这里,面向对象编程的三个常用的特性就都介绍完了,再重复一次:
如果学过C语言,就会知道在C语言中也可以自定义数据结构,就是使用叫做struct的关键字。在C++里面,可以把struct看成是所有内容都是public属性的类。