继承
继承的一般语法为:
class 派生类名:[继承方式] 基类名{
派生类新增加的成员
};
继承方式包括 public(公有的)、private(私有的)和 protected(受保护的),此项是可选的,如果不写,那么默认为 private。
1) public继承方式
- 基类中所有 public 成员在派生类中为 public 属性;
- 基类中所有 protected 成员在派生类中为 protected 属性;
- 基类中所有 private 成员在派生类中不能使用。
2) protected继承方式
- 基类中的所有 public 成员在派生类中为 protected 属性;
- 基类中的所有 protected 成员在派生类中为 protected 属性;
- 基类中的所有 private 成员在派生类中不能使用。
3) private继承方式
- 基类中的所有 public 成员在派生类中均为 private 属性;
- 基类中的所有 protected 成员在派生类中均为 private 属性;
- 基类中的所有 private 成员在派生类中不能使用。
基类成员在派生类中的访问权限不得高于继承方式中指定的权限。
改变访问权限
使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public
1 | //基类People |
方法遮蔽
基类成员和派生类成员的名字一样时会造成遮蔽,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。
1 | //基类Base |
构造方法和析构方法
在派生类的构造方法中调用基类的构造方法:
1 | Student::Student(char *name, int age, float score): People(name, age), m_score(score){ } //顺序可以交换 |
析构函数的执行顺序和构造函数的执行顺序刚好相反。
多继承
多继承的语法也很简单,将多个基类用逗号隔开即可。例如已声明了类A、类B和类C,那么可以这样来声明派生类D:
1 | class D: public A, private B, protected C{ |
上面的 A、B、C、D 类为例,D 类构造函数的写法为:
1 | D(形参列表): A(实参列表), B(实参列表), C(实参列表){ |
当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::
,以显式地指明到底使用哪个类的成员,消除二义性。
1 | BaseA::show(); //调用BaseA类的show()函数 |
虚继承和虚基类
在继承方式前面加上 virtual 关键字就是虚继承
1 | //直接基类B |
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;而对于普通继承,就是按照构造函数出现的顺序依次调用的。
向上转型(将派生类赋值给基类)
赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题
可以用派生类对象给基类对象赋值,而不能用基类对象给派生类对象赋值。赋值后方法调用仍然是原来的。
1 | A a(10); |
除了可以将派生类对象赋值给基类对象(对象变量之间的赋值),还可以将派生类指针赋值给基类指针
1 | A *pa = new A(1); |
虚函数
虚函数对于多态具有决定性的作用,有虚函数才能构成多态。
-
只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
-
为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽(覆盖)关系的同名函数都将自动成为虚函数。
-
当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
-
只有派生类的虚函数遮蔽基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)
-
构造函数不能是虚函数。
-
析构函数可以声明为虚函数,而且有时候必须要声明为虚函数,
1 | //基类Base |
基类的方便不会被覆盖了
纯虚函数和抽象类
在C++中,可以将虚函数声明为纯虚函数,语法格式为:
virtual 返回值类型 函数名 (函数参数) = 0;
纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0
,表明此函数为纯虚函数。它只起形式上的作用,告诉编译系统“这是纯虚函数”。
包含纯虚函数的类称为抽象类。
typeid运算符
typeid 运算符用来获取一个表达式的类型信息。类型信息对于编程语言非常重要,它描述了数据的各种属性:
- 对于基本类型(int、float 等C++内置类型)的数据,类型信息所包含的内容比较简单,主要是指数据的类型。
- 对于类类型的数据(也就是对象),类型信息是指对象所属的类、所包含的成员、所在的继承关系等。
typeid 的操作对象既可以是表达式,也可以是数据类型,下面是它的两种使用方法:
typeid( dataType )
typeid( expression )
1 | class Base{ }; |
type_info 类的几个成员函数,下面是对它们的介绍:
- name() 用来返回类型的名称。
- raw_name() 用来返回名字编码(Name Mangling)算法产生的新名称。关于名字编码的概念,我们已在《C++函数编译原理和成员函数的实现》中讲到。
- hash_code() 用来返回当前类型对应的 hash 值
typeid 运算符经常被用来判断两个类型是否相等
1 | char *str; |
类型比较 | 结果 | 类型比较 | 结果 |
---|---|---|---|
typeid(int) == typeid(int) | true | typeid(int) == typeid(char) | false |
typeid(char*) == typeid(char) | false | typeid(str) == typeid(char*) | true |
typeid(a) == typeid(int) | true | typeid(b) == typeid(int) | true |
typeid(a) == typeid(a) | true | typeid(a) == typeid(b) | true |
typeid(a) == typeid(f) | false | typeid(a/b) == typeid(int) | true |
1 | class Base{}; |
类型判断结果为:
类型比较 | 结果 | 类型比较 | 结果 |
---|---|---|---|
typeid(obj1) == typeid(p1) | false | typeid(obj1) == typeid(*p1) | true |
typeid(&obj1) == typeid(p1) | true | typeid(obj1) == typeid(obj2) | false |
typeid(obj1) == typeid(Base) | true | typeid(*p1) == typeid(Base) | true |
typeid(p1) == typeid(Base*) | true | typeid(p1) == typeid(Derived*) | false |
运算符重载
运算符重载是通过函数实现的,它本质上是函数重载。
运算符重载的格式为:
返回值类型 operator 运算符名称 (形参表列){
//TODO:
}
1 | class complex{ |
在全局范围内重载运算符
在上面基础上
1 | //声明为友元函数 |
运算符重载的规则
- 并不是所有的运算符都可以重载。能够重载的运算符包括:
+ - * / % ^ & | ~ ! = < > += -= = /= %= ^= &= |= << >> <<= >>= == != <= >= && || ++ – , -> -> () [] new new[] delete delete[]
长度运算符sizeof
、条件运算符: ?
、成员选择符.
和域解析运算符::
不能被重载。
- 重载不能改变运算符的优先级和结合性。假设上一节的 complex 类中重载了
+
号和*
号,并且 c1、c2、c3、c4 都是 complex 类的对象,那么下面的语句:
c4 = c1 + c2 * c3;
等价于:
c4 = c1 + ( c2 * c3 );
-
重载不会改变运算符的用法,原有有几个操作数、操作数在左边还是在右边,这些都不会改变。
-
运算符重载函数不能有默认的参数,否则就改变了运算符操作数的个数,这显然是错误的。
-
运算符重载函数既可以作为类的成员函数,也可以作为全局函数。
-
箭头运算符
->
、下标运算符[ ]
、函数调用运算符( )
、赋值运算符=
只能以成员函数的形式重载。
函数模板
所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。这个通用函数就称为函数模板(Function Template)。
定义模板函数的语法:
1 | template <typename 类型参数1 , typename 类型参数2 , ...> 返回值类型 函数名(形参列表){ |
typename
关键字也可以使用class
关键字替代,它们没有任何区别。
举例:
1 | template<typename T> void Swap(T *a, T *b){ |
函数模板也可以提前声明,不过声明时需要带上模板头,并且模板头和函数定义(声明)是一个不可分割的整体,它们可以换行,但中间不能有分号。
1 | //声明函数模板 |
类模板
声明类模板的语法为:
1 | template<typename 类型参数1 , typename 类型参数2 , …> class 类名{ |
1 | template<typename T1, typename T2> //这里不能有分号 |