运算符重载
格式如下:
例如这将重载“+”运算符。如果A、B、C都是类D的对象就可以这样用:。其中,运算符左侧的对象是调用对象,右侧的则是作为参数被传递的对象。
重载限制:
- 重载后的运算符必须至少有一个操作数是用户定义的类型
- 使用运算符时不能违反运算符原来的句法规则,同样,不能修改运算符的优先级
- 不能创建新运算符
- 不能重载某些运算符
- 某些运算符只能通过成员函数重载的运算符
类的自动转换和强制类型转换
例1
- 自动转换 int i=3.3 最终i会自动转换成3
- 强制类型转换 int i=int(3.3) i最终也为3.3
对于类来说也是如此, 将类放到与int,double相同的高度上,规则大体相同。
例2
假设类有一个构造函数
现在我们将类对象转换成特定的数据类型,那么,反过来呢?
我们需要转换函数
注意:
- 转换函数必须是类方法
- 转换函数不能指定返回类型
- 转换函数不能有参数
例3
它有一个特点,无论创建了多少个对象,程序都只创建一个静态类变量副本,也就是说所有的类对象共用一个静态成员。它不能在类中声明,因为这样会分配内存而类不可,不好在头文件中声明因为这会导致多次声明,最好在表示方法的文件中声明。
Such as:
很少为人所知的是,编译器会帮我们生成许多自动生成的成员函数,从而造成一些意料之外的结果。如:复制构造函数。
复制构造函数用于将一个对象复制到新创建的对象中,它的原型通常如下:
为什么隐式复制构造函数在一些情况下会出问题?
首先, 对于一些静态储存类,复制构造函数的执行不会对它造成改变,而有的时候正是需要
它的改变,这时候就会出问题。其次,一些类成员使用的是new初始化的,指向数据的指针,而不是数据本身。所以当对对象使用delete操作时,新构造出的函数很容易就会受到牵连。
下面给出一个复制构造函数的例子:
有关默认的赋值运算符的问题
与复制构造函数类似,当存在两个已经初始化的对象,我们想将一个对象的值赋给另一个时,默认的赋值运算符就会起作用,其作用机理与复制构造函数相同,产生的问题也大致相同,解决的方法也相似,下面提供赋值运算符(进行深度复制)定义。
e.g:
比较成员函数
e.g:
使用中括号表示法访问字符
总的说来就是重载[]运算符,为了确保不修改数据,可以用的形式来声明函数。
静态类成员函数
我们可以将成员函数声明为静态的。这样做的后果是,不能通过对象来调用此函数,此函数也只能使用静态数据成员。
使用方法:;其中String为类名,Howmany为函数名
使用指向对象的指针
ps:cpp经常使用指向对象的指针
e.g:
再谈定位new运算符
可以用定位new运算符声明对象
Such as:
但要注意的是,如果是在同一块内存块上声明对象,不要使它们相互重叠而造成错误。还有就是对于用定位new运算符创建的对象要显示的调用析构函数
e.g:为pc3所指向的类的名称。
还需注意的是晚创建的应该先删除
几种继承的方法
- 公有继承(is-a关系):
eg:
如何继承?
即可,当然其中构造函数有些许的不同。
稍特殊情况:多态公有继承
即方法的行为取决于调用该方法的对象
两种方法:
- 在派生类中重新定义基类的方法
- 使用虚方法
- 私有继承
基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们
e.g
- 保护继承
保护继承是私有继承的变体。保护继承在列出基类时使用关键字protected:
e.g:
使用保护继承时,基类的公有成员都将成为派生类的保护成员。
关于公有继承的具体细节
- 派生类构造函数必须使用基类构造函数
- 基类指针可以在不进行显示类型转换的情况下指向派生类对象;基类引用可以在不进行显示
类型转换的情况下引用派生类对象,反之,派生类指针不能指向基类对象。但基类指针或引用
只能用于调用基类方法。 - 关于多态公有继承的问题:在派生类中声明了一个基类中已经存在的方法,如show()函
数,在函数中又调用基类的函数,这很自然,可以减少工作量,要记得使
用作用域解析运算符,不然会是一个无穷尽的递归函数。 - 虚析构函数的重要性
若派生类中有动态分配内存的一些操作,就需要基类有虚析构函数,这样可以确保程序首先调
用派生类中的析构函数,再调用基类的析构函数,使内存可以正常的释放。 - 关于重新定义将隐藏方法的问题
e.g:
下面的showperks函数将隐藏上面的,这是很自然的。但是有时候很容易出现问题。当你想
调用上面的某个函数时,你就发现不能调用了。所以,如果要保证不出现此类问题,最好的方
法是把上一个类的虚方法的所有重载的版本在这个类中也全部写一遍。
访问控制:protected
我们已经用过public和private来控制对类成员的访问,其实还存在一种访问类别,那就是protected。protected与private较为相似。对于外部世界来说(相对的),它们都是不可访问的。但是派生类的成员可以直接访问protected中的成员。
警告:最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派
生类能够访问基类数据。
抽象基类
抽象基类也是为了表示一种特定的关系而存在着的。比如我们有篮球和足球这两种球,它们可以独立成两个类别,但它们有很多共性,那么我们就可以使用一个叫做‘球’的类来作为抽象基类,再通过球类来派生出篮球类和足球类。
e.g
其中AcctABC是抽象类,Brass和BrassPlus是具体类。AcctABC包含Brass和BrassPlus类共有的所有方法和数据成员,而那些在BrassPlus类和Brass类中的行为不同的方法应被声明为虚函数。至少应有一个虚函数是纯虚函数,这样才能使AcctABC成为抽象类。抽象基类的特点,抽象基类的作用相对于公有继承的基类来说作用有点单调,它只能提供一个垫板,本身作为类并不能声明对象。并且它一定要有纯虚函数的存在,它的纯虚函数可以定义也可以不定义,如果它的派生类没有定义纯虚函数,那么它也成为了抽象基类,知道有的类定义了这些函数,那么它就成为了实体类,可以声明对象。
继承和动态分配内存的细节
- 派生类不使用new
这种方式的复杂度可以说是最低了(如果有更低的,原谅我的孤陋寡闻),不需要进行一些所谓的额外的操作。 - 派生类使用new
在这种情况下,必须为派生类定义显示析构函数、复制构造函数和赋值运算符。派生类析构函数自动调用基类的析构函数,故其自身的职责是清除自身调用的内存。派生类的复制构造函数必须调用基类的复制构造函数来处理基类的数据。对于显示赋值运算符也是如此。
如何使用基类的友元函数?
解决方法是使用强制类型转换
示例代码块:
如上例所示,想要使用的基类的重载<<运算符的友元函数,只要加上即可。
包含、组合或层次化
使用这样的类成员,本身是另一个类的对象
e.g:
使用构造函数时,对于对象成员的处理:使用初始化列表,调用对象名即可而不是对象的类名。假定scores是一个对象成员,要调用该成员的方法,只需即可。
私有继承
使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味这基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。
使用构造函数:
e.g:
使用私有继承时将使用类名和作用域解析运算符来调用方法。
访问基类对象(强制类型转换)
访问基类的友元函数
用类名显示地限定函数名不适合友元函数,这是因为友元不属于类。然而,可以通过显示地转换为基类来调用正确的函数。
e.g:
保护继承
保护继承是私有继承的变体。使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类中派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。
使用来重新定义访问权限
e.g
这使得可用,就像它们是student的公有方法一样
多重继承的麻烦
比如说有一个worker的基类,从其中派生出singer类和waiter类,至此,整个代码是没有问题的,singer和waiter都获得了worker的一些数据成员,它们统称为work。当我们从singer和waiter中派生出一个singerwaiter类时,问题就出现了。首先,singerwaiter将有两个有关于worker的数据,那么,进行赋值时要把值赋给谁呢?cpp解决了这个问题,引入了虚基类(virtual base class)
e.g:
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。特别地使用构造函数
worker是虚基类,则singerwriter使用构造函数时要显式的使用worker的构造函数。还有就是方法的问题,如果singer和waiter都使用了show()方法,那么singerwaiter要使用哪个show()方法呢?
可以用作用域解析运算符来澄清编者的意图
还有一种方法可以解决这个问题
e.g
这样,singingwaiter的show函数就能完美地完成任务了。但要记得将上述基类的data函数
设置为受保护的,这样可以放置不被当前的类对象调用。
类模板
e.g
上述提供了类模板的一个例子,类模板函数最好与类在同一个文件中。
使用类模板的例子:
如果要使用指针和涉及动态内存分配的东西,可能需要重新设计一下类模板。我们在上面已经可以看到的存在了,实际上,类模板可以使用的不仅仅时这一个参数,比如,它可以是,n可以称为时表达式参数。表达式参数有一些限制。表达式参数可以是整型、枚举、引用或指针。另外,模板代码不能修改参数的值,也不能使用参数的地址。
模板多功能性
e.g
可以使用多个类型参数
e.g
可以为类型参数提供默认值
模板具体化
- 隐式实例化
不做额外的操作 - 显示实例化
声明必须位于模板定义所在的名称空间中
E.g
Template class ArrayTP<string, 100>;
在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。 - 显示具体化
提供一个为具体类型定义的模板
e.g
这样就可以专门地为该种数据类型设计函数。
4. 部分具体化
即部分限制模板的通用性
e.g
- 成员模板
一个模板类将另一个模板类和模板函数作为其成员
将模板用作参数
e.g
- 模板类和友元
- 非模板友元
都可以使用,在引入变量时要指定模板类的具体的参数类型。 - 模板类的约束模板友元函数
在类定义的前面声明每个模板函数
然后,在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化。
在写函数的时候不用再指定具体的参数类型了。
3.模板类的非约束模板友元函数
可以使用多个不同的类具体化
模板别名
可以使用typedef为模板具体化指定别名:
当然,也可以这么做:
定义好以后,就可以这样使用了:
注意:与是等价的
友元
- 友元类
所谓的友元类,就是在类头部把另一个类声明为有友元关系的类。这样,另一个类就可以在自己类的作用域中访问原始类的私有成员和保护成员。
e.g:
- 友元成员函数
作用与友元类相同,只是起作用的范围缩小了。
e.g:
- 前向声明
前向声明的出现是为了满足特定的需求的。比如说,remote中的一个方法被声明为Tv的一个友元成员函数后,那么在构建这两个函数的过程中,remote中的方法提到了Tv类,意味这Tv类必须放在remote类的前面,但Tv类中
remote成员函数意味着它必须先知道remote的定义,这样它才能知道remote是一个类,这样就有了一种矛盾的关系,即它们交叉引用了,为了拯救它们,前向声明出现了。
e.g:
可不可以是
呢?答案是否定的,因为,这样做对于Tv类来说它能看到的信息是remote是一个类,但是没有得到友元成员函数的定义,所以它就很迷惑了。所以就是不可以的。
- 其他友元关系
- 让两个类成为彼此的友元
作用是共享它们的数据,当然可以完成更宽泛的操作了。 - 共同的友元
可以将一个友元函数作为两个类的友元函数。
嵌套类
有的时候,为了方便,我们可以将一个类声明在另一个类中,这个类其实就相当于一种自定义的数据结构,而且这种类的作用域依实现而不同,是一种很好的封装的方法。
e.g:
异常
异常的出现是为了更好地检测出代码中潜在的问题,但异常也使代码变得更复杂了,可谓有一利有一弊吧。
系统有两种方式处理错误,这里简单的介绍一下
- abort()。其典型实现是向标准错误流(即cerr使用的错误流)发送消息abnormal program termination(程序异常中止),然后中止程序。
- 返回错误码。可以用一个函数检验,如果输入错误,则返回false,让用户可以重新输入。
- 我们自己设计的异常机制。一共有三个模块,try块,throw块和catch块。try块执行预计会发生错误的代码,期间对于不同的错误使用不同的throw代码,跳转到相应的catch块。throw块和catch块具有参数匹配的特性。
- 异常规范。异常规范是在C98提出来的一项特性,但实践证明它并不好用,C11保留了它,但以后可能会
被删除。它的功能就是让编译器添加执行运行阶段检查的代码,检查是否违反了异常规范。另外,有一个关键字noexcept,它告诉编译器函数不会引发异常。 - 栈解退。所谓的栈解退就是程序在按一定的顺序执行时,当它检测到异常以后,会反方向寻找解决问题
的catch方法,不管有多远,期间该调用析构函数的调用,该清理内存的清理,不会受到任何
影响。 - 其他异常特性。引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用。
但指定引用也有它的好处,比如我们指定一个基类的引用,这样它就可以作用用基类和派生类等的多个类了。
如果不知道将会引发哪些异常,可以这样写。这样它就可以捕获任何异常。
- 异常规范。异常规范是在C98提出来的一项特性,但实践证明它并不好用,C11保留了它,但以后可能会
- exception类。就是cpp提供了许多的类,它们的为检测不同的错误而生,我们可以使用它们,但实现的方
法因人而异。
异常何时会迷失方向:
- 有的时候,可能没有捕获到异常。这样,不会导致程序立刻异常中止。相反,程序将首先调用函数。在默认情况下,调用函数。当然,我们也可以指定要调用的函数。
e.g:
- 原则上,异常规范应包含函数调用的其他函数引发的异常。但有时并没有,这个时候就需要进行补救。这个时候就需要进行补救方法就是在异常规范中加入一个默认的异常。使其它可能引发的异常都指向这个异常。
- 有关异常的注意事项
有的时候动态分配了内存,在它被释放之前就引发了异常,导致了内存的泄露,这是可以解决的,比如在catch块中加入相应处理的代码, 但这会使得代码变复杂,可能还会引发许多其他的问题。
RTTI
RTTI是运行阶段类型识别(Runtime Type Identification)的简称。
cpp有三个支持RTTI的元素
- 如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指
针;否则,该运算符返回0——空指针。 - 空指针typeid运算符返回一个指出对象的类型的值
- type_info结构储存了有关特定类型的值
1.dynamic_cast运算符
它是最常用的RTTI组件。它不能回答“指针指向的是哪类对象“这样的问题,但能够回答”是否可以安全地将对象的地址赋给特定类型的指针“这样的问题。
e.g:
这提出了一个问题 ,指针pg的类型是否可被安全地转换为?如果可以,运算符将返回对象的地址,否则返回一个空指针。也可以将这个运算符用于引用啦,但是没有一个指示错误的空指针,所以只能用try块和catch块在检测了。
2. typeid运算符和type_info类
Typeid运算符使得能够确定两个对象是否为同种类型。它与sizeof有些相像,可以接受两种参
数。
- 类名
- 结果为对象的表达式
返回一个对type_info对象的引用
e.g
如果类型为真则为true,否则为false。若pg为空指针,则会引发bag_typeid异常。
类型转换运算符
四个类型转换运算符
- dynamic_cast
- const_cast
- static_cast
- reinterpret_cast
Dynamic_const之前已经介绍过了,这里不再赘述。Const_cast用于执行只有一种用途的类型转换,即改变值为const或volatile,其语法与dynamic_const相同。就是去const或volatile性。
Static_const