> 文章列表 > C++ 继承:概念定义、对象的赋值转换、继承作用域及派生类的默认成员函数

C++ 继承:概念定义、对象的赋值转换、继承作用域及派生类的默认成员函数

C++ 继承:概念定义、对象的赋值转换、继承作用域及派生类的默认成员函数

目录

一. 继承的概念和定义

1.1 继承的概念

1.2 继承的定义格式

1.3 继承关系和访问限定符

二. 基类和派生类对象的之间的赋值转换

三. 继承体系中的作用域

四. 派生类的默认成员函数

4.1 构造函数 

4.2 拷贝构造函数

4.3 赋值运算符重载函数

4.4 析构函数

4.5 取地址运算符重载函数

附录:派生类6个默认成员函数的实现完整代码


一. 继承的概念和定义

1.1 继承的概念

继承是面向对象编程的三大特性之一(封装、继承、多态),继承使得程序可以在原有类的基础上进行扩展,在保留原有类功能的同时增加具有新的功能的新类新增的类称为派生类/子类,原有的类称为基类/父类。继承体现了面向对象程序设计的层次结构,是类层次设计的体现。

假设要定义一个学生类Student,学生类中要记录学生的相关信息(姓名、年龄、学号等),其中姓名和年龄为每个人都具备的基础信息,因此,可以单独定义一个Person类,其中包含姓名信息和年龄信息。Student类基础Person类,这样在Student类中,就包含了Person类的成员变量,同时可以调用Person类的成员函数。在这一继承体系中,Student为派生类/子类,Person为父类/基类。

图1.1 Student类和Person类的继承关系

1.2 继承的定义格式

定义继承体系,要包含三个关键要素:基类、派生类和继承关系。

图1.2 继承的定义方式

1.3 继承关系和访问限定符

通过类和对象的学习,可知访问限定符有三种:private(私有)、public(公有)、protected(保护)。同时,继承关系对应三种访问限定符,也可分为三种:private、public、protected。

图1.3 访问限定符和继承关系

基类中成员的访问限定符和继承关系,共同影响子类对基类成员的访问权限,表1.1为不同的基类成员访问方式和继承方式下,子类对基类成员的访问权限。

表1.1 子类对基类成员的访问权限
基类成员访问方式/继承方式 public protected private
pubilc public protected private
protected protected protected private
private 基类成员对子类不可见 基类成员对子类不可见 基类成员对子类不可见

根据表1.1,总结出以下规律:

  1. 基类中的private成员,对子类是不可见的,不可见指在子类中无法访问基类成员。
  2. 基类的protected(保护)成员不能在类外部被直接访问,但它的派生类可以访问基类的protected成员,如果希望基类的某成员不能被随意访问但可通过派生类访问,那么应该定义为protected成员。
  3. 派生类对基类成员的访问权限为:Min(基类成员访问方式,继承方式),其中:public > protected > private。

另有两点需要注意:

  1. class的默认继承方式为private继承,struct的默认继承方式为public继承,这里的class和struct是指定义派生类时使用的关键字,而不是基类。但是,一般要求继承方式要显示地写出,不可省略。
  2. 在绝大部分实际项目中,都是使用public继承,protected继承和private继承几乎用不到。

二. 基类和派生类对象的之间的赋值转换

关于基类和派生类对象之间的相互赋值,有以下几条关系:

  1. 派生类对象可以给基类对象赋值,但基类对象不能给派生类对象赋值。
  2. 派生类指针可以赋值给基类指针,基类指针通过强制类型转换可以赋值给派生类指针。但是基类指针给派生类指针赋值时,极易产生越界访问问题。
  3. 派生类对象可以赋值给基类引用,但基类对象的引用不能赋值给派生类对象。

派生类对象(指针)之所以可以赋值给基类对象(指针),是因为编译器对派生类对象进行了切割,将派生类和基类共有的部分从派生类中分离出来,赋值给基类。

图2.1 派生类给基类赋值时的切割关系示意图
class Person
{
public:std::string _name;int _age;
};class Student: public Person
{
public:int _stuNum;  //学号int majorId;  //专业代号
};int main()
{Person p1;Student s1;p1 = s1;   //派生类对象赋值给基类对象Person* pp1 = &s1;   //派生类指针赋值给基类指针Person& rp1 = s1;    //派生类赋值给基类引用Person p2;Student s2;//s2 = p2;   //基类对象不能赋值给派生类Student* pp2 = (Student*)&p1;  //基类指针通过强制类型转换可赋给派生类指针//pp2->_stuNum = 1;  //越界访问,程序崩溃//Student& rs2 = p1;  //基类不能赋给派生类引用return 0;
}

三. 继承体系中的作用域

关于继承体系中的类作用域,有下面几条语法规则:

  1. 基类和派生类成员都有自己单独的作用域,不可认为一个派生类中继承下来的基类成员位于这个派生类的作用域中。
  2. 如果基类和派生类中含有同名的成员变量,那么派生类成员会将基类成员覆盖,即默认先访问派生类成员。如果想访问基类中同名的成员变量,那么就应当指定作用域,语法格式为:基类名称::基类成员名。
  3. 基类和派生类中的函数,只要函数名相同,就存在覆盖问题,基类和派生类的成员函数无法构成重载,因为重载函数要求定义在同一作用域内。
  4. 在继承体系中,基类和派生类中应尽量避免出现同名成员。
class A
{
public:void func(){std::cout << "func()" << std::endl;}int _a = 1;int _a1 = 2;
};class B : public A
{
public:void func(int x){std::cout << "func(int x)->" << x << std::endl;}int _a = 10;int _b = 20;
};int main()
{B b;std::cout << b._a << std::endl;  //10std::cout << b.A::_a << std::endl;  //通过指定作用域访问基类中同名成员 -- 1//b.func();  //默认访问派生类成员函数 -- 编译报错:函数参数不足b.func(10);  //func(int x)->10b.A::func(); //func()return 0;
}

四. 派生类的默认成员函数

继承中的派生类,与普通类一样,如果用户不显示定义,就会生成6个默认成员函数。

本文以Person和Student类为例,讲解如何通过自定义来实现派生类的六个默认成员函数。

class Person
{
private:std::string _name;  //姓名int _age;  //年龄
};class Student: public Person
{
private:int _stuNum;  //学号int _majorId;  //专业代号
};

4.1 构造函数 

编译器自动生成的派生类构造函数:

  1. 对于派生类本身的成员变量,处理方式与普通类对象一致(内置类型成员变量不进行处理,自定义类型成员变量调用它的默认构造函数)。
  2. 调用基类的构造函数,初始化基类的成员。
  3. 构造顺序为:先基类对象 -> 再派生类对象

注意:如果要自己定义派生类构造函数,应当显示地调用基类的构造函数,最好在初始化列表中就进行调用。同时,不可以在派生类的构造函数中直接操作基类成员,基类成员变量只能在基类构造函数中进行处理。

//基类构造函数
Person(const char* name = "zhang", int age = 20): _name(name), _age(age)
{}//派生类构造函数
Student(const char* name = "zhang", int age = 20, int stuNum = 1, int majorId = 0): Person(name, age)  //调用基类的默认构造函数, _stuNum(stuNum), _majorId(majorId)
{}

4.2 拷贝构造函数

编译的自动生成的派生类拷贝构造函数:

  1. 对于派生类本身的成员,与普通类一样,内置类型成员变量做浅拷贝,自定义类型成员变量调用它的拷贝构造函数。
  2. 调用基类拷贝构造函数创建基类对象。

如果要自己实现派生类拷贝构造函数,应当显示调用基类拷贝构造函数。

Student(const Student& s): Person(s), _stuNum(s._stuNum), _majorId(s._majorId)
{ }

4.3 赋值运算符重载函数

编译器自动生成的赋值运算符重载函数与自动生成的拷贝构造函数类似,进行的工作为:

  1. 对于派生类本身的内置类型成员变量进行浅拷贝,自定义类型调用其赋值operator=函数。
  2. 调用基类的operator=函数。

与构造函数和拷贝构造函数一致,如果要自己定义operator=函数,要显示调用基类的operator=函数。这里要注意:调用基类的operator=函数时要指定基类作用域,否则会因为函数名相同而产生覆盖,引发无限递归调用。

Student& operator=(const Student& s)
{Person::operator=(s);   //显示调用基类的赋值运算符重载函数_stuNum = s._stuNum;_majorId = s._majorId;return *this;  //返回类对象本身
}

4.4 析构函数

编译器自动生成的派生类析构函数会先处理派生类成员变量,然后调用基类的析构函数。

如果自己实现派生类析构函数,注意不需要显示调用基类的析构函数,编译器会自动调用。这里是为了保证派生类对象先于基类对象被析构。

~Student()
{//清理资源...(无动态开辟内存就无需额外处理)
}

4.5 取地址运算符重载函数

一般而言,不需要用户自己实现取地址运算符重载函数,编译器自动生成的就足以满足要求。但即使要自己实现派生类的operator&函数,也不需要显示地再去调用基类的operator&函数。

//对于普通派生类对象的取地址运算符重载函数
Student* operator&()  
{return this;
}//对于const派生类对象的取地址运算符重载函数
const Student* operator&() const  
{return this;
}

附录:派生类6个默认成员函数的实现完整代码

class Student: public Person
{
public:Student(const char* name = "zhang", int age = 20, int stuNum = 1, int majorId = 0): Person(name, age)  //调用基类的默认构造函数, _stuNum(stuNum), _majorId(majorId){}Student(const Student& s): Person(s), _stuNum(s._stuNum), _majorId(s._majorId){ }Student& operator=(const Student& s){Person::operator=(s);   //显示调用基类的赋值运算符重载函数_stuNum = s._stuNum;_majorId = s._majorId;return *this;  //返回类对象本身}~Student(){//清理资源...(无动态开辟内存就无需额外处理)}Student* operator&()  //对于普通派生类对象的取地址运算符重载函数{return this;}const Student* operator&() const  //对于const派生类对象的取地址运算符重载函数{return this;}private:int _stuNum;  //学号int _majorId;  //专业代号
};

戏剧知识