本周主要以complex类的构建为例,详细学习了不包含指针的类的创建的相关知识。
我认为不包含指针的类与包含指针类的区别在于其内存管理机制上的不同。本周所学习的不包含指针的类就不用考虑析构函数。
(1)类
面向对象编程基于三个基本概念:数据抽象、继承和动态绑定。在C++中,用类进行数据抽象,用类派生从一个类继承另一个类,派生类继承与基类的成员。动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。
在C语言中,数据是公开的,所有的函数都可以使用这些数据。在C++中,不但类内数据实现了封装,而且类内函数也被赋予不同的访问等级。
(2)C++代码的基本构成
对于一个简单的类,其中包含以下三种文件:
<1>class.h:头文件,其中包括类以及所有函数。
<2>class.cpp:头文件的实现文件。对于本周所创建的简单的类,其相关函数均被定义为inline函数,并且被定义在类的头文件中,所以此文件被省略。
<3>main.cpp:执行文件,其中完成类的使用。该文件中必须包含类的头文件。
注: include<>首先在计算机本地库里进行搜索,再搜索用户自己实现的库; include""在用户自己实现的库里进行搜索。
为了能够避免同一个头文件被多次包含,应当使用防卫式声明,其基本结构如下:
#ifndef __HEAD_H_
#define __HEAD_H_
定义的类
#endif
(3)内联(inline)
内联函数类似于宏,效率较高。 函数在类内定义时,自动转成内联的候选人。因此最好将inline函数定义在头文件中。
某些函数虽然定义为inline,但是对于过于复杂的函数,编译器也没有能力将其做成inline。定义的inline只是给编译器的一种inline建议,至于能否成为inline由编译器决定。 一般来说,inline用于优化规模小,流程直接,频繁调用的函数,而且很多编译器并不支持内联嵌套函数。
(4)访问级别
类的访问级别:public、private和protected。
被定义在public中的成员能够在整个程序内访问。一般,希望被外界调用的类内函数会被定义在public中。
被定义在private中的成员可以被类的成员函数访问,但是不能类的用户使用。一般类的成员数据必须保证定义在private中,保证数据的封装。
protected访问标号可以看成是private和public的混合:①像private成员一样,protected成员不能被类的用户访问;②像public成员一样,protected成员可以被类的派生类访问。
一个类可以包含0个或者多个访问说明符,而且对于某个访问说明符能出现多少次也没有严格限定。每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或者到达类的结尾为止。
一个类可以用struct或者class进行定义,区别便是struct的默认访问权限为public,而class的默认访问权限为private。
(5)友元
通过关键字friend,在函数声明语句处能够将该函数设定为类的友元。被设定为友元的函数或其他类能够访问该类中private与protected成员。友元函数如果定义在类的内部,则其是隐式内联的,相同种类的类互为友元。
(6)构造函数
控制类对象初始化的一个或者几个成员函数被称之为类的构造函数。每当类被创建时,构造函数就会被调用。在C++中,在构造函数中最好使用列表初始化。
Date(int nYear = 0, int nMonth = 0, int nDay = 0) :
yr(nYear),
mon(nMonth),
dy(nDay){ }
列表初始化有以下几点优势:
<1>在类中存在常量时,常量只能初始化,不能赋值。
<2>类成员中存在引用,同样只能初始化,不能赋值。
<3>类成员在基类或者成员类中没有定义默认构造函数。
<4>使用列表初始化可以提高代码效率。
由于构造函数会在外部被调用,所以一般会被定义在public区域。 构造函数也可以存在于private区域中,这种写法被称为单例(singleton)模式。singleton模式的主要特点不是根据用户程序调用生成一个新的实例,而是控制某个类型的实例唯一性。
singleton模式拥有一个私有构造函数,这确保用户无法通过new直接实例它。除此之外,该模式中包含一个静态私有成员变量instance与静态公有方法getInstance()。getInstance()方法负责检验并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。
singleton模式的优缺点如下:
优点:
<1>实例控制: 单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。
<2>灵活性: 因为类控制了实例化过程,所以类可以灵活更改实例化过程
缺点:
<1>开销: 虽然数量很少,但如果每次对象请求引用时都要检查是否存在类的实例,将仍然需要一些开销。可以通过使用静态初始化解决此问题。
<2>可能的开发混淆:使用单例对象(尤其在类库中定义的对象)时,开发人员必须记住自己不能使用new关键字实例化对象。因为可能无法访问库源代码,因此应用程序开发人员可能会意外发现自己无法直接实例化此类。
<3>对象生存期:不能解决删除单个对象的问题。在提供内存管理的语言中(例如基于.NET Framework的语言),只有单例类能够导致实例被取消分配,因为它包含对该实例的私有引用。在某些语言中(如 C++),其他类可以删除对象实例,但这样会导致单例类中出现悬浮引用。
(7)const修饰符
const修饰类的成员函数,则该成员函数不能修改类中任何非const成员函数。一般写在函数的最后来修饰。
int year() const { return yr; }
int month() const { return mon; }
int day() const { return dy; }
对于const类对象/指针/引用,只能调用类的const成员函数,因此,const修饰成员函数的最重要作用就是限制对于const对象的使用。
const常量与define宏定义的区别:
<1> 编译器处理方式不同:define宏是在预处理阶段展开;const常量是编译运行阶段使用。
<2>类型和安全检查不同:define宏没有类型,不做任何类型检查,仅仅是展开;const常量有具体的类型,在编译阶段会执行类型检查。
<3>存储方式不同:define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存;const常量会在内存中分配(可以是堆中也可以是栈中)。
一般而言,尽量以const、enum、inline替换#define。
在可以使用const的地方使用const可以大大提高程序的鲁棒性,和对象设计的封装性。
(8)pass by reference与pass by value
在参数传递时,能pass by reference的时候,就应该pass by reference。虽然C++默认为by value形式,因为pass by reference可以节省一次拷贝的开销,并且可以节约一个拷贝的空间,所以pass by reference更加高效。
如果害怕函数修改了不应该修改的引用参数,那么可以使用pass by reference to const。
在选择return by reference与return by value时,并不是绝对的return by reference,必须返回对象时,就不要妄想返回其reference。
(9)操作符重载
当默认操作符无法满足当前类的使用要求时,应当进行操作符的重载。