面向对象是C引入的重要机制,提高了开发效率.C03作为早期面向对象语言以及保持对C的兼容,C++并没有达到一切都是对象的程度.
C++的class关键字或struct关键字可以定义类. 类是一种自定义数据类型, 包含属性和成员函数(OOP中通常称为方法).
class Circle {private: double radius; const double pi; static int count;public: Circle(): radius(0), pi(3.14) {}; double get_radius(void) { return radius; } void set_radius(double); static void add_count(void); double get_pi(void) const;};int Circle::count = 0;void Circle::set_radius(double radius) { this->radius = radius;}double Circle::get_pi()const { return pi;}void Circle::add_count(void) { count++;}int main(void) { Circle c = Circle(); c.set_radius(3); cout << c.get_pi() << '\n' << c.get_radius() << endl;}
上述示例展示了如何定义一个类并进行实例化(即根据类定义创建对象).可以看出C++的class与C的struct在语法有诸多相似之处.
private:
和public:
是访问控制符, 将在成员访问控制中详细说明.
class Circle {private: double radius;public: Circle(): radius(0), pi(3.14) {}; double get_radius(void); void set_radius(double);};
class必须先声明再使用, 可以只进行class Circle
声明而滞后定义.方法的定义可以在class {};
也可以在类外. 只需要使用命名空间运算符::
指明其所属的类即可.
void Circle::set_radius(double radius) { this->radius = radius;}
因为类的引入, C++的作用域级别变为:namespace
/ 文件 - class
- 函数. 在查找标志符含义时, 先检查当前作用域, 然后检查上一级作用域.
每个成员函数中都有一个特殊的this
指针,它指向调用成员函数的类对象。在函数作用没有定义同名标识符时, 可以直接使用成员名访问当前对象的成员. 必要时, 可以显式使用this
指针.
double Circle::get_radius(void) { return radius;}void Circle::set_radius(double radius) { this->radius = radius;}
一个对象所占的空间仅与属性有关与成员函数无关。函数的目标代码是存储在对象的存储空间之外的,不同对象调用的都是同一段函数代码。
成员访问控制
C++提供了private
, prortected
和public
三个成员访问控制符(member access specifier).
类定义中private
, prortected
和public
可以出现多次,它们作用到下一个访问限定符或类定义结束.
被
private
修饰的成员只能被本类的方法或友元函数引用.被
public
修饰的成员可以本类方法或类作用域内的其它函数引用被
protected
修饰的成员不能被类外访问但可以被派生类的成员函数访问。
C++在增加了class
之后仍保留了struct
关键字,struct也可以包含属性和成员函数.
在没有访问限定符修饰的情况下struct默认为public,而class默认为private. 此外,两者的默认继承也有类似的区别.
构造函数
因为类的封装性和信息隐蔽,所以很难对类对象(private, protected)进行初始化。C++提供了构造函数来实现对象的初始化.
构造函数(constructor)是一类特殊的成员函数,它没有任何类型(也不是void类型)也没有返回值.构造函数函数名与类名相同,访问限定符通常为public.
与其它成员函数不同,它在对象建立后自动执行,以完成对象的初始化和其它程序员想要在使用对象前完成的操作.
Circle::Circle(): radius(0), pi(3.14) { cout << 'construction done' << endl;}
Circle(): radius(0), pi(3.14)
相当于一组在函数首部的赋值语句.这种语法是唯一可以初始化const成员的方法和调用基类非默认构造函数的语法.
构造函数与其它函数一样也可以重载和设置默认参数,在定义类对象时,对象名后加构造函数的实参表即可调用相应的构造函数进行初始化:
string str("Hello world!");
在没有显式初始化类对象时,C++将调用无参数的默认构造函数(void)进行初始化.
C++中有三种特殊的构造函数:
默认构造函数
无参数的构造函数称为默认构造函数,当没有显式初始化类成员时C++将自动调用默认构造函数进行初始化.
在程序员未定义默认构造函数时,C++编译系统将自动建立默认构造函数,它将对本类中的其它类对象成员调用其默认构造函数初始化,基本类型不进行初始化.
复制构造函数
复制构造函数只有一个本类的对象或引用作为参数Circle (const Circle&);
.
除了初始化类对象外,复制构造函数在类对象作为参数进行传值调用,或作为返回值被传回时会被自动调用.
如果程序员没有定义复制构造函数,C++编译系统会自动生成复制构造函数和赋值运算符,它们将直接拷贝每一个非static属性(包括指针和类对象),也就是所谓浅拷贝.
它们会将数组中的每一个元素都进行复制而不是仅复制首地址.
由于类对象含有指针和动态分配的成员(new运算符生成)在复制时需要考虑重新分配问题,否则会使指针指向同一内存单元(这样的复制就像在C中通过将数组名赋给指针来试图复制数组一样并不是真正的复制).
考虑到除属性简单赋值之外的对象复制(主要是内存的重新分配)就可以称作深拷贝了. 赋值运算符默认只进行浅拷贝(调用默认复制构造函数),如果想要它进行深拷贝则需显式地对赋值运算符进行重载.
转换构造函数
转换构造函数只有一个其他类型的对象或引用作为参数Circle (const Ellipse&);
.
定义了转换构造函数后,编译系统会自动重载本类的类型转换运算符,若不想执行自动重载则需在声明构造函数时使用explicit关键字,在类外定义时不需要重复该关键字.
如果多种类型的数据转换为本类的代码相同,转换构造函数也可以使用模板而不需要定义多个几乎完全相同的函数.
相反地,如果想要将本类对象转换成已有类型就需要对目标类型的类型转换运算符进行重载.重载函数格式为operator Eliipse (void) {}
注意operator关键字与类型名之间的空格,函数没有类型,没有参数但要返回一个目标类型的数据.
析构函数
析构函数(destructor)是另一类特殊的成员函数,其作用与构造函数正好相反.析构函数在对象生存期结束时被自动执行,它的作用并不是删除对象而是完成在撤销对象前的清理工作, 比如释放动态内存, 关闭数据库连接等.
析构函数的函数名是~Circle()
类似constructor没有返回值类型,~
是C/C++中的位取反运算符,由此可见它与构造函数的作用相反.
在调用顺序上,先构造的对象后析构,后构造的先析构.
C++建议将析构函数定义为虚函数virtual ~Circle()
.因为在实现多态时,使用基类引用操作派生类,使用虚函数防止只析构基类而不析构派生类的状况发生。
const 成员
在类声明中用const关键字声明的属性称为常属性const double pi;
C++ 03中只能用constructor进行初始化 Circle(): pi(3.14) {};
常属性必须初始化其所在类才可以实例化, 常属性一旦初始化其值不可能更改.
用const关键字修饰的成员函数被称为常成员函数double Circle::get_pi()const {}
.
注意const是放在后面的,放在前面则是对函数返回值进行限制.
如果将成员函数声明为常成员函数,则只能引用本类中的属性(包括非const成员),而不能修改它们的值.为了防止意外修改成员值,常成员函数只能调用常成员函数不能调用其它成员函数.
在声明时用const关键字修饰的对象称为常对象,常对象只能由构造函数对其进行初始化.除了系统隐式调用的构造函数与析构函数外, 只能够调用其常成员函数.
static 成员
在类声明时使用static关键字修饰的属性称为静态属性static int count;
.
静态属性不为某一对象所拥有,在内存中只占用一份空间,不随着对象的建立与销毁而建立与销毁.
静态属性只能在类内声明并在类定义外初始化int Circle::count = 0;
.
不能利用构造函数的对静态属性进行初始化,类外定义时也不需要写static关键字.
可以通过c.count
来引用静态属性,不论是否有类对象存在公用的静态属性都还可以用Circle::count
来引用.
以static声明的成员函数称为静态成员函数static void add_count(void);
,与静态属性公用的静态成员函数可以用Circle::add_count();
来调用,实际上也允许通过对象来调用静态成员函数.
静态成员函数没有this指针,也就是说它不能直接引用对象中的属性.所以静态成员函数只能访问静态属性而不能访问非静态属性.
如果一个属性被声明为const static那么只能在类内声明时进行初始化,count static double pi=3.14;
friend友元
除了类的成员函数外,友元函数也可以访问类的私有成员.
在类定义中用friend void watch();
形式声明一个函数为友元函数,然后可以在此类外定义该函数.
友元函数不是此类的成员函数,在定义时不能加类名与作用域运算符::
.
除了全局函数外,其他类的成员函数也可以作为友元函数,只不过要用friend void watchdog::watch();
形式来声明.
除了函数外也可以将另一个类B声明为类A的友元类,类B中所有成员函数均为类A的友元函数.声明格式为class B; class A { friend B;}
.
内部类和局部类
C++允许在类中定义另一个类,称为内部类或嵌套类.
内部类可以用访问控制符修饰但不能使用static修饰.
在拥有访问权限的代码块中可以使用 Outer::Inner obj
来定义对象,如public内部类可以在任意处定义对象,private内部类只能在外部类及其方法内定义对象.
与Java不同,C内部类没有外部类对象指针,不依赖外部类对象(这与Java的嵌套类相似),C内部类对于外部类的private、public成员没有访问权限.
简单地说,C++内部类只是扩展了命名空间.
在函数中定义的类称为局部类,它们只能在本函数中定义对象.
继承
在一个已定义类的基础上通过新增或重写成员来定义新的类这就是类的继承(inheritance)机制.通过继承产生的新类称作派生类或子类.
继承最简单的情况是一个基类产生一个派生类,这称为单继承.C++也允许一个类从多个基类继承成员,这就是多重继承.
因为多重继承可能产生二义性的问题,所以一些OOP语言(如Java)不支持多重继承.
被用做基类的类必须进行了定义,只进行了声明而没有定义的类不能作为基类.
派生类不继承构造函数,析构函数和赋值运算符的重载函数(operator=
).除此之外, 派生类必须继承基类的其它所有属性和方法, 不能只继承一部分.
继承机制只能继承属性和方法,友元关系不能被继承.
如果基类中存在static成员则基类和派生类共享同一个static实例.
class ExtendedClass: public BaseClass {};
示例中的public
用于指定继承方式. 派生类对基类成员的访问权限由基类成员的访问控制符和继承方式共同决定.
无论以何种方式继承,基类中的private都会成为派生类中的不可访问成员.
以private方式继承则基类中的所有成员, 派生类均无法访问.
以protected方式继承则基类中public和protected成员均变为protected.
以public方式继承则, 基类中public和protected成员访问属性不变.
从封装程度来看, 三种访问控制由强到弱为:
- private 仅本类访问
protected 仅本类和派生类访问
public 公开
继承不能破坏封装性, 所以基类成员的访问属性自然的选择原访问属性和继承方式中更强的那个.
通过在派生类中定义与基类声明完全相同的属性或方法可以覆盖基类的成员, 被覆盖的基类成员必须用::
指定基类作用域才能访问.
在实例化派生类对象时, C++将先执行基类的默认构造函数,再逐个执行派生类的构造函数.派生类可以在构造函数中为基类构造函数提供参数, 从而指定要调用的基类构造函数.
class ExtendedClass: public BaseClass { ExtendedClass(): BaseClass(0) {}};
多重继承与虚继承
在多重继承的情况下,如果派生类D继承了B,C, 而B,C继承了共同的基类A.理论上, 两条继承路径均会继承共同基类的成员, 并在构造时两次调用A的构造方法.
C++提供了虚继承以解决这个问题:
class A {};class B: virtual public A {};class C: virtual public A {};class D: public B,public C {};
则派生类D只会保留一份共同基类A的属性.
虚继承可以部分的解决多继承带来的问题, 若B,C对A的同一个函数进行了不同重写仍然是未定义行为.
作者比较喜欢Java的多重继承处理方式, 即只继承一个实体类, 但可以继承多个接口类.
多态性与虚函数
多态性是指不同对象接收到同一个消息时进行不同的响应, C++把多态定义为调用基类和派生类的同名函数时, 调用不同的实现.
派生类重写基类方法可以实现上述要求, 但是要调用的对象和函数实现在编译期就已指定(即静态绑定),缺乏灵活性.为了在运行期动态绑定对象和函数,我们需要使用函数指针.
因为派生类中包含基类的所有成员, 所以使用基类指针访问派生类对象是安全的. 这一兼容性是实现多态的重要基础.
C++提供的了虚函数作为动态多态性的解决方案.简单的说,虚函数允许在派生类中定义与基类中同名(参数表一致,否则成了函数重载)的函数.当基类指针指向基类对象时调用基类中的相应函数,基类指针指向派生类对象时调用派生类中定义的同名函数.
将一个成员函数声明为虚函数只需要在基类函数头部最前用virtual关键字修饰,其在派生类中的同名函数也将自动成为虚函数.通常为了提高代码可读性,习惯上将所有虚函数前都使用virtual关键字修饰.
当派生类中没有重定义虚函数时,派生类将继承直接基类的同名函数.在基类中声明的虚函数在类外定义时不需要再使用virtual关键字修饰.
class BaseStream {public: virtual BaseStream& operator>>();};class FileStream: public BaseStream {public: virtual string read();};class StringStream: public BaseStream {public: virtual string read();};void func(BaseStream *) {}
上述示例模仿了std::stream的结构, func函数不需要关心传入的Stream指针到底是FileStream还是StringStream, 只要调用read就可以读取数据.
C++允许只声明而不定义虚函数, 含有未定义的虚函数的类称为抽象类, 抽象类不能实例化. 抽象类的派生类如果没有定义所有未定义虚函数则仍旧是抽象类.
当一个类带有虚函数时,编译系统会为该类构造一个叫做虚函数表(vtable, Virtual Function Table)的指针数组,以存放指向不同虚函数定义的函数指针.
vtable会增加一定的空间消耗,但是响应速度很快不必担心时间消耗的问题.