一、类的继承与派生
继承与派生是C++编程过程中经常用到的一个非常重要的功能。可以复用以前开发好的功能,站在巨人的肩膀上,节省时间,提高开发效率。不用重复造轮子了。这就是C++面向对象编程中的继承与派生。
例如:定义的学生类 CStudent,里面包含了一些普通学生的信息:姓名、学号、性别、年龄。那么接下来呢,我要定义一个小学生的类,又要定义一个中学生的类,还要定义一个大学生的类。不管是小学生、中学生还是大学生,肯定也包含学生的姓名、学号、性别、年龄等基本信息,如果都分别新定义一个类的话,跟之前的学生类 CStudent 貌似有点重复,同样的代码写了很多次。所以,我们可以通过C++中的继承的机制来解决这个问题。
1.1 举例:继承
CStudent 学生类的信息可以作为所有学生的基础信息,任何一个学生都包含姓名、学号、性别、年龄等基本信息。那么我们在定义小学生、中学生或者大学生的类的时候就可以用 CStudent 作为父类进行派生子类:
#include "Student.h" //基类(public CStudent)声明在此文件中
class CXiaoStudent : public CStudent //小学生的类继承(继承基类)
{
public:
int yuwen_score;
int shuxue_score;
int english_score;
};
class CZhongStudent : public CXiaoStudent //中学生的类继承(继承小学生类)
{
public:
int wuli_score;
int huaxue_score;
};
上述中,这里面的 public 是指的继承方式,即:父类中的成员在子类中的继承方式,一般也包含三种:public公有继承、private私有继承、protedted受保护继承。
通过子类可以访问基(父)类的成员,如下:
CZhongStudent zhong_1;
zhong_1.wuli_score = 90; //调用本类的成员变量
zhong_1.yuwen_score = 100; //调用父类的成员变量
zhong_1.age = 15; //调用爷爷类的成员变量
1.2 继承的方式
①、public公有继承:
父类的公有成员和受保护成员在子类中保持原有的访问属性,其私有成员仍为父类私有,在子类中是访问不了的,即使通过子类的共有成员函数也访问不了;
②、private私有继承:
父类的公有成员和受保护的成员在子类中变成了私有成员,其私有成员仍为父类私有, 在子类中是访问不了的,即使通过子类的共有成员函数也访问不了;
③、protected受保护继承:
父类的公有成员和受保护的成员在子类中变成了受保护成员,其私有成员仍为父类私有, 在子类中是访问不了的,即使通过子类的共有成员函数也访问不了;
#include "Student.h"
class CXiaoStudent : public CStudent
{
public:
int yuwen_score;
int shuxue_score;
int english_score;
private:
int flag_private;
protected:
int flag_protected;
};
class CZhongStudent : public CXiaoStudent
{
public:
int wuli_score;
int huaxue_score;
public:
int get_flag_1()
{
//return flag_private;
return flag_protected;
}
};
继承访问关系表:
基类数据成员 | 继承方式 | 基类数据成员在派生类中的访问属性 |
---|---|---|
private | public | 不可直接访问 |
private | protected | 不可直接访问 |
private | private | 不可直接访问 |
protected | public | protected |
protected | protected | protected |
protected | private | private |
public | public | public |
public | protected | protected |
public | private | private |
总而言之(基类数据private除外):基类数据成员在派生类中的访问属性 是 基类数据成员 与 继承方式 的交集。取他们之间数据限制最严格的那一部分。
1.3 子类的构造函数与析构函数
构造函数
子类可以继承父类所有的成员变量和成员函数,但不能继承父类的构造函数。因此,在创建子类对象的时候,为了初始化从父类继承来的数据成员,系统需要调用其父类的构造函数。
-
父类构造函数的调用规则:
-
①. 如果子类没有定义构造函数,则调用父类的无参数的构造函数;
②. 如果子类定义了构造函数,不论是无参数还是带参数,在创建子类的对象的时候,首先执行父类无参数的构造函数,然后执行自己的构造函数;
③. 在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数,则会调用父类的无参构造函数(优先使用自定义无参构造函数,否则使用默认无参构造函数);
④. 在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数且父类只定义了自己的有参构造函数,则会出错(如果父类只有有参数的构造函数,则子类必须显示调用此带参构造函数);
⑥. 如果子类调用父类带参数的构造函数,需要用初始化父类成员对象的方式
简而言之:
如果子类没有显示的调用父类的构造函数,那么默认会调用父类无参的构造函数。
如果父类只提供了有参数的构造函数,那么子类在默认情况下调用父类的无参构造函数时就会报错!
class CXiaoStudent : public CStudent
{
public:
int yuwen_score;
int shuxue_score;
int english_score;
CXiaoStudent() : CStudent("zhangsan", 'm', 1001, 20)
{
yuwen_score = 2;
shuxue_score = 0;
english_score = 0;
flag_private = 0;
flag_protected = 0;
}
private:
int flag_private;
protected:
int flag_protected;
};
析构函数
跟父类的构造函数一样,子类也一样不能继承父类的析构函数,也需要通过派生子类的析构函数去调用父类的析构函数。在执行子类的析构函数时,系统会自动调用父类的析构函数和子对象的析构函数,对父类和子对象进行清理工作。
调用的顺序跟构造函数正好相反:先执行子类自己的析构函数,对派生类新增加的成员进行清理,之后调用子对象的析构函数,对子对象进行清理,最后调用父类的的析构函数,对基类进行清理。
二、运算符重载
一般的变量我们可以直接赋值等运算,但是对于复杂变量,或者对象,就不能简单的进行赋值或运算,比如:向量的加减等。
在复制构造函数中,我们会对对象进行复制初始化,我们需要对不同的成员变量进行不同的操作,这就涉及到重载。
运算符的重载实际上就是函数的重载,即,定义一个重载运算符的函数。使指定的运算符不仅能实现原有的功能,而且在函数中还能实现新的自定义的功能。
CStudent& CStudent::operator=(const CStudent& stud)
{
if (p_name)
{
delete[] p_name;
p_name = NULL;
}
int name_len = strlen(stud.p_name) + 1;
p_name = new char[name_len];
memset(p_name, 0, name_len);
strcpy(p_name, stud.p_name);
sex = stud.sex;
num = stud.num;
age = stud.age;
return *this;
}
2.1 重载运算符的规则
①、C++不允许用户自己定义新的运算符,只能对已有的C++运算符进行重载:
例如,在python中**
运算符,例如:3**5,表示3的5次方,如果你想在C++中创建**
运算符并实现重载,那是不行的。
可重载的运算符列表:
名称 | 详情 |
---|---|
双目算术运算符 | +(加),-(减),*(乘),/(除),% (取模) |
关系运算符 | ==(等于),!= (不等于),< (小于),> (大于>,<=(小于等于),>=(大于等于) |
逻辑运算符 | ||(逻辑或),&&(逻辑与),!(逻辑非) |
单目运算符 | + (正),-(负),*(指针),&(取地址) |
自增自减运算符 | ++(自增),–(自减) |
位运算符 | | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移) |
赋值运算符 | =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>= |
空间申请与释放 | new, delete, new[ ] , delete[] |
其他运算符 | () (函数调用),-> (成员访问),, (逗号),[] (下标) |
-
下面是不可重载的运算符列表:
-
.
成员访问运算符
.*
,->*
成员指针访问运算符
::
域运算符
sizeof
长度运算符
?:
条件运算符
#
预处理符号
②、重载运算符不能改变运算符操作对象的个数:例如 > 和 < 运算符,本身是比较用的双目运算符,也就是说要有2个操作数,如果你重载完了之后变成不是2了,那肯定不行;
③、重载运算符不能改变运算符的优先级别,乘除 */ 运算符的优先级别大于±,如果你重载完了之后导致优先级改变了,那肯定也不行;
④、重载运算符不能改变运算符的结合性,例如,赋值运算符=是从右向左开始结合的,如果你重载完了之后导致从左往右了,那肯定也不行;
⑤、重载运算符的函数不能有默认的参数:不然就改变了运算符参数的个数,与第③点矛盾了;
⑥、重载运算符的函数参数至少有一个是本类的对象或引用,不能全部都是C++的基本类型,例如:CStudent& CStudent::operator+(int a, int b) 这种也是不行的;
⑦、重载运算符应该遵循运算符原有的含义,不能把+重载之后变成-的功能,虽然语法上没有错误,但是违背了重载运算符的初衷!