文章目录
  1. 1. 继承与多态章节
    1. 1.1. 基类和派生类的类型转换
      1. 1.1.1. 基类向派生类的转换
      2. 1.1.2. 派生类向基类的转换
        1. 1.1.2.1. 转换方法
        2. 1.1.2.2. 转换条件
    2. 1.2. 虚析构函数
      1. 1.2.1. C++派生类的虚析构函数是如何工作的?
      2. 1.2.2. 外话:派生类的构造函数、赋值运算符呢?
    3. 1.3. 继承与隐藏(名字查找与作用域规则)
      1. 1.3.1. 一般的“名字查找与作用域”规则
      2. 1.3.2. 继承中的隐藏(“名字查找与作用域”规则)
    4. 1.4. 继承与重载

引言:学习C++继承与多态过程中有一些比较有意思的问题,就在这里记录一下吧。注意,下面的代码均遵循C++11,在VS2015运行正常。


继承与多态章节

基类和派生类的类型转换

虽然讲的是类型转换,但基类和派生类的对象之间并不能进行直接的类型转换,我们进行类型转换只能通过不同类型的指针或引用来进行。

基类向派生类的转换

  • 首先说明,基类向派生类的转换是不安全的。

    派生类均包含基类的一部分,而基类不一定包含派生类的部分,因此让基类向派生类转换是不安全的。

  • 如果确实有这种需求,并且使用者可以保证转换安全的话,可以通过static_cast进行强制转换。

派生类向基类的转换

转换方法
  • 假设Base是基类,B1是派生类,则我们可以通过下面两种方法进行转换:
1
2
3
B1 b1;
Base *baseP=&b1;
Base &baseRef=b1;
  • 另外,派生类向基类的转换存在隐式转换。如:
1
2
3
4
5
6
7
8
9
10
void func(Base base)
{
base.show();
}
int main(void)
{
B1 b1;
func(b1);//合法的,b1将隐式转换为Base类对象
return 0;
}
  • 类型转换之后的baseRef和*baseP将 “切掉” (sliced down)派生类的那部分(隐藏掉),仅保留基类的部分。
转换条件

转换也是有条件的,在《C++ Primer》书上写了三个晦涩难名的句子,归结起来是:“对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可行的;反之则不行。”

为了理解这句话,请看下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base {
public:
int func(int a, int b) { return a + b; };
double func(double a, double b) { return a*b; };
string func(string a, string b) { return a.append(b); }
};
class B1 :private Base {
public:
double func(double a, double b) { return a + b; }
};

int main(void)
{
B1 b1;
Base *baseP = &b1;
return 0;
}

编译器给了如下报错:不允许对不可访问的基类"Base"进行转换 ,就是说,在main函数体中这个节点,由于B1继承方式是private,因此用户对b1中的Base部分是不可见的,也就无法将b1对象转换到Base类。

为了更深地理解这个问题,请看下面一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {
public:
int func(int a, int b) { return a + b; };
double func(double a, double b) { return a*b; };
string func(string a, string b) { return a.append(b); }
};
class B1 :private Base {
public:
double func(double a, double b) { return a + b; }
void func() {
B1 b1;
Base &base = b1;
cout << "成功" << endl;
}
};

这段代码成功通过了编译并且输出“成功”二字。由于private继承方式将其所有成员均以private方式继承下来,对其B1类内可见,因此b1对象到Base的类型转换是可行的。

虚析构函数

所有会被用作基类的类都必须将析构函数声明为virtual。(如果不想用作基类,在声明类名时,在类名后final,表示其不会被继承,函数也是如此)

  • 如果析构函数不定以为虚函数的话,析构函数的定义将直接被继承,那么在delete一个派生类对象时将出现错误,因为delete一个指向派生类对象的基类指针将产生未定义的行为。

C++派生类的虚析构函数是如何工作的?

如果我的派生类定义了虚析构函数,那么我需不需要手动帮基类擦屁股?也就是说,派生类的虚析构函数需不需要做基类虚构函数要做的那部分事情?

假设Base是基类,B1是派生类。

1
2
B1 *b1 = new B1; 
delete b1;//先调用B1的析构,然后调Base的析构。

也就是说,先调用派生类的析构函数,然后再调用基类的析构函数。为了验证,我们写了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
Base() { p = new int; }
virtual ~Base() { cout << "Base析构" << endl; delete p; p = nullptr; };
protected:
int *p;
};
class B1 final:public Base{
public:
B1() { p2 = new int; };
~B1() override { cout << "B1析构" << endl; delete p2; p2 = nullptr; }
private:
int *p2;
};
int main()
{
B1 *b1 = new B1;
delete b1;
return 0;
}

运行结果为:

1
2
B1析构
Base析构

那如果我将b1的类型转换为Base后再delete,将会发生什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base {
public:
Base() { p = new int; }
virtual ~Base() { cout << "Base析构" << endl; delete p; p = nullptr; };
protected:
int *p;
};
class B1 final:public Base{
public:
B1() { p2 = new int; };
~B1() override { cout << "B1析构" << endl; delete p2; p2 = nullptr; }
private:
int *p2;
};
int main()
{
B1 *b1 = new B1;
Base *base = b1;
delete base;
return 0;
}

运行结果为:

1
2
B1析构
Base析构

与之前的结果一模一样。

那么可以得出结论了:

  1. 自己的事情自己做。派生类的析构函数只需对自己类中特有的部分负责即可,析构函数只负责销毁派生类自己分配的资源
  2. 析构顺序是自底而上的,先调用派生类的析构函数,再调用直接基类的构造函数,再调用直接基类的直接基类的构造函数……(从前有座山,山上有座庙……)

外话:派生类的构造函数、赋值运算符呢?

与析构函数不同的是,构造函数、赋值运算符除了负责自有成员外,还需要负责基类部分成员。

虽然要负责全部成员,派生类的构造函数通常使用冒号语法调用基类的构造函数来初始化一部分成员,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base {
public:
Base() = default;
Base(int i, string s, bool b) :integer(i),str(s),boolean(b){}
protected:
int integer;
string str;
bool boolean;
};
class B1 final :public Base {
public:
B1() = default;
B1(int i, string s, bool b, int k) :Base(i, s, b), key(k) {}//调用Base的构造函数依次用i,s,b初始化interger,str,boolean
private:
int key;
};

而赋值运算符呢,也可以类似地做这样的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
public:
Base & operator=(Base & x) = default;
protected:
int integer;
string str;
bool boolean;
};
class B1 final :public Base {
public:
B1 & operator=(B1 x)
{
Base::operator=(x);
key = x.key;
return *this;
}//当然,这里只是来演示用法,由于没有堆空间的申请,所以也可以用default
private:
int key;
};

以上的写法是推荐的。

继承与隐藏(名字查找与作用域规则)

这个规则是十分重要的。

一般的“名字查找与作用域”规则

  • 关于作用域有这样一句话:声明在内层作用域的函数不会重载声明在外层作用于的函数,而是直接隐藏作用域的声明。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    int func(int);
    int func(int, int);
    {
    void func(char *);//此次声明不会对func进行重载,而是隐藏掉int func(int)和int func(int,int)
    int a=func(1);//错误
    func(1, 2);//错误
    func("yes")//正确
    }
    }

    有一个特例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    template<typename TYPE>
    inline void swap(Vector<TYPE> a, Vector<TYPE> b)//先定义友元swap用于交换Vector<TYPE>
    {
    using std::swap;
    swap(a.num, b.num);
    swap(a.p, b.p);
    }
    template<typename T>
    inline Vector<T>& Vector<T>::operator=(const Vector<T>& v)//用swap
    {
    using std::swap;//声明使用std中的swap(强行写在这里罢了,其实代码不需要这一行)
    Vector<T> vec=v;//copy and swap
    swap(vec,*this)
    return *this;
    }

    在上个例子中,重载赋值运算符函数中声明使用std中的swap,但是并不会隐藏掉友元swap,这是为什么呢?因为在C++名字查找规则中,即使std标准库的函数是可见的,仍然放在最后查找(简单理解就是using std::swap只是告诉编译器:我这个作用域里面可以使用std里面的这个swap罢了,不想隐藏外层啥声明,别想那么复杂)。

继承中的隐藏(“名字查找与作用域”规则)

  • 抽象地说,派生类的作用域在基类之内,适用于如上“名字查找与作用域”规则。

  • 基类中的所有虚函数,派生类继承后均需要进行定义,如果不定义则直接继承基类中该虚函数的定义。派生类对继承而来的虚函数进行声明定义时可以不需要加virtual,但仍然是虚函数。

  • 如果基类与派生类的虚函数参数列表不同,那么将代表这是不同的两个函数,按照“名字查找与作用域”规则,这时将发生意外。

    如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Base {
    public:
    virtual int fcn() { cout << "Base:fcn()" << endl; return 0;};
    };
    class D1 :public Base {
    public:
    //未定义虚函数int fcn(),因此直接继承Base::fcn()的定义,但被隐藏。
    int fcn(int) { cout << "D1:fcn(int)" << endl; return 0;}//不会重载,而是隐藏了Base::fcn(),并且该fcn(int)不是虚函数
    virtual void f2() { cout << "D1:f2()" << endl; }
    };
    int main()
    {
    D1 a;
    a.fcn();//错误
    a.fcn(3);//合法
    return 0;
    }

    上例中虚函数虽然被继承了下来,但被隐藏了,因此类外无法调用a.fcn(),但调用a.fcn(3)是合法的。

  • 很多时候这种意外是难以察觉的。因此我们引进了override标识符,它显式告诉编译器,我这个函数是要覆盖基类的某个虚函数的。如此的话,编译器将能够检测到,增加了项目安全性。如,我如此书写int fcn(int) override { cout << "D1:fcn(int)" << endl; return 0;} 编译器将报错,因为没有与之相同形参的虚函数可以覆盖,帮助我们发现错误。

  • 为了验证int fcn()是否真的被D1继承下来了,我们编写如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class D2 :public D1 {
    public:
    int fcn(int) { cout << "D2:fcn(int)" << endl; return 0;}//隐藏D1中的fcn(int),非虚函数
    int fcn() override { cout << "D2:fcn()" << endl; return 0; }//覆盖D1中的虚函数fcn()
    void f2() override { cout << "D2:f2()" << endl; }
    };
    int main()
    {
    D2 a;
    a.fcn(3);
    a.fcn();
    return 0;
    }

    编译是通过的,所以说明D1中确实有一个int fcn()的虚函数。

    输出结果:

    1
    2
    D2:fcn(int)
    D2:fcn()

继承与重载

按照上面的规则,我们发现有些东西变得棘手了起来。比如基类中有三个重载函数,我在派生类中只想覆盖其中一个,但我只要定义一个同名函数,就直接将基类的三个重载函数隐藏了,这可怎么办啊?

我们先看如下代码:

1
2
3
4
5
6
7
8
9
10
class Base {
public:
int func(int a, int b) { return a + b; };
double func(double a, double b) { return a*b; };
string func(string a, string b) { return a.append(b); }
};
class D1 :public Base {
public:
double func(double a, double b) { return a + b; }
};

我们想在D1中覆盖double func(double,double) ,但最后我们却发现将另外两个func也给隐藏掉了,真可谓牵一发而动全身啊!(原因在上面已经讲过了)

  • C++为了解决这个问题,可以使用using声明语句。using声明语句不需要指定参数,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类的作用域中。

改进代码:

1
2
3
4
5
6
7
8
9
10
11
class Base {
public:
int func(int a, int b) { return a + b; };
double func(double a, double b) { return a*b; };
string func(string a, string b) { return a.append(b); }
};
class D1 :public Base {
public:
using Base::func;//当当当当!看这里!!
double func(double a, double b) { return a + b; }
};

完美解决。同样的,我们还可以用这个方法对func进行重载。

文章目录
  1. 1. 继承与多态章节
    1. 1.1. 基类和派生类的类型转换
      1. 1.1.1. 基类向派生类的转换
      2. 1.1.2. 派生类向基类的转换
        1. 1.1.2.1. 转换方法
        2. 1.1.2.2. 转换条件
    2. 1.2. 虚析构函数
      1. 1.2.1. C++派生类的虚析构函数是如何工作的?
      2. 1.2.2. 外话:派生类的构造函数、赋值运算符呢?
    3. 1.3. 继承与隐藏(名字查找与作用域规则)
      1. 1.3.1. 一般的“名字查找与作用域”规则
      2. 1.3.2. 继承中的隐藏(“名字查找与作用域”规则)
    4. 1.4. 继承与重载