C++ 继承和多态


一、基类和派生类概述

1. 派生类的定义

派生类可以继承基类的成员,通过使用类派生列表明确指出继承的基类以及访问控制。
派生类定义的语法如下:

class 派生类名: 访问说明符 基类名1, 访问说明符 基类名2, ...
{
	// 成员变量和成员函数
};

示例如下:

class Animal
{
	//成员变量和成员函数
};

class Dog: public Animal
{
 	//成员变量和成员函数      
};

2. 派生类的声明

派生类的声明包含其类名但不包含它的派生列表:
class Dog; //正确
class Dog: public Animal; //错误

3. 被用作基类的类

如果一个类被用作基类,则这个类必须已经被定义。

4. 防止继承的发生

有时候会定义这样一种类,不希望其他类继承它,为实现这一目的,C++11新标准提供了一种防止继承发生的方法,定义类时,在类名后面加上关键字final

二、访问控制

1.protected访问说明符

派生类可以继承定义在基类的成员,但无法访问定义在基类的私有成员。基类中有一种成员,基类希望它的派生类有权访问而其他用户无权访问,此时使用protected访问说明符来说明这种成员。

示例代码:

class Animal
{
public:
    void speak()
    {
        cout << "animal apeak" << endl;
    }
    
protected:
    string name;
    int age;
};

class Dog: public Animal
{
public:
    void speak()
    {
        cout << name << "wangwang" << endl;
    }
};

若将Animal中的成员name改成private的,则会有如下报错:

In member function ‘void Dog::speak():
error: ‘std::string Animal::name’ is private within this context
|         cout << name << "wangwang" << endl;
|                 ^~~~

对于protected成员的说明:

  1. protected成员对于类的用户来说是不可访问的。
  2. protected成员对于派生类的成员和友元来说是可访问的。
  3. 派生类的成员和友元只能通过派生类对象来访问基类的protected成员,而不能通过基类对象访问。即派生类成员和友元只能访问派生类对象中从基类继承的protected成员,不能访问基类对象中的protected成员。
    代码示例:
#include <iostream>
#include <string>

using namespace std;

class Animal
{
protected:
    string name;
};

class Dog: public Animal
{
    friend void print_name(Dog&);   //能访问Dog::name
    friend void print_name(Animal&);//不能访问Aniaml::name
};

//正确,print_name可以访问Dog对象的name
void print_name(Dog &d) { cout << d.name << endl;}
//错误,print_name不能访问Animal对象的name
void print_name(Animal &a) { cout << a.name << endl;}

int main()
{
    
}

上述代码的编译结果如下

ain.cpp: In function ‘void print_name(Animal&):
main.cpp:21:40: error: ‘std::string Animal::name’ is protected within this context
   21 | void print_name(Animal &a) { cout << a.name << endl;}
      |                                        ^~~~
main.cpp:9:12: note: declared protected here
    9 |     string name;
      |          

如果派生类成员和友元可以访问基类对象的protected成员,则上面第二个print_name是合法的,这样我们可以定义一个类似Dog的类,从而简单规避掉基类中protected提供的访问保护了。

2.不同权限的继承方式

派生类访问说明符对派生类的成员及友元能否访问其直接基类的成员没什么影响,对基类成员的访问权限只与基类中的访问说明符有关。
派生类访问说明符控制派生类用户对派生类中基类成员的访问权限:

  • public继承:派生类保持基类原有访问级别
  • private继承:派生类中基类成员全变成private
  • protected继承:派生类中基类的public成员会变成protected,protected成员仍然为protected,private成员仍然为private

若继承时未描述权限,当派生类为class则默认为private继承;当派生类为struc则默认为public继承

3. 友元关系不能继承

就像友元关系不能传递一样,友元关系也不能继承。基类的友元在访问派生类成员时不具特殊性,同样,派生类的友元在访问继承成员时也不具特殊性。

友元关系不能继承,每个类负责控制各自成员的访问权限。

三、类型转换与继承

通常情况下,将引用和指针绑定到一个对象上时,引用和指针的类型必须和对象类型一致或者对象类型存在一个可接受的const类型转换。存在继承关系的类是一个重要的例外,可以将基类的指针和引用绑定到派生类的对象中。

1. 静态类型和动态类型

使用存在继承关系的类型时,必须将静态类型和动态类型区分开来。
静态类型是变量声明的类型或表达式生成的类型,在编译时总是已知的。
动态类型是变量或表达式表示的内存中的对象类型,直到运行时才可知。

如果变量和表达式既不是指针也不是引用,则它的动态类型永远与静态类型一致。

2. 类型转换

  • 因为派生类对象中含有基类对应的组成部分,所以能把派生类对象当成基类来用,也能将基类的指针和引用绑定到派生类对象中,这种转换被称为派生类到基类的类型转换。将基类指针绑定到派生类对象时,该指针的静态类型是基类类型,动态类型是派生类类型,调用的成员函数是派生类的成员函数版本。
  • 派生类到基类的类型转换只对指针和引用有效,在对象之间不存在这样的转换。可以使用派生类对象给基类类型的变量赋值或初始化,但只对派生类的基类部分生效,且该变量的静态类型和动态类型仍然都是基类类型。
  • 不存在基类到派生类的类型转换。因为派生类的成员基类可能并不包含,如果将基类作为派生类使用,可能会调用一个不存在的成员。
  • 派生类到基类的类型转换可能会由于访问受限而变得不可行。

3. 派生类向基类转换的可访问性

这里的可访问性指的是转换是否可行,如前所述,派生类到基类的类型转换可能会由于访问受限而变得不可行。
派生类向基类转换的可访问性由使用该转换的代码决定,也受派生访问说明符的影响。假定D继承B:

  • 对于用户代码,只有D继承B的方式是public时,才能使用D到B的类型转换
  • 对于D的成员和友元,不管D以什么方式继承B,都可使用D到B的类型转换。
  • 对于D的派生类的成员的友元,只有D继承B的方式是public或protected时,才可使用D到B的类型转换。

3.1 用户代码

#include <iostream>
#include <string>

using namespace std;

class B 
{
public:
    int a;
};
class D: protected B
{
    
};

int main()
{
    D d;
    B *p = &d;
}

编译结果如下

main.cpp: In function ‘int main():
main.cpp:19:13: error: ‘B’ is an inaccessible base of ‘D’
   19 |     B *p = &d;
      |             ^           ^

使用私有继承时,也会得到同样的结果。

使用private继承或protected继承时,对于用户而言,派生类中的所有基类成员变得不可访问。用户通过基类对象访问的一些成员,无法再通过派生类对象访问,比如,用户可通过B类型的对象访问成员a,但不可通过D类型的对象访问成员a。因此,不能再把派生类当成基类来用,派生类向基类的类型转换不可行。

3.2 D的成员函数和友元

#include <iostream>
#include <string>

using namespace std;

class B 
{
public:
    int a;
};

class D: private B
{
public:
    void f(D &d)
    {
        B *pb = &d;
        cout << pb->a <<endl;
    }
    friend void friendf(D &d);
};
void friendf(D &d)
{
    B *pb = &d;
    cout << pb->a <<endl;
}

int main()
{
    
}

运行结果如下:

..Program finished with exit code 0
Press ENTER to exit console.

不管以什么方式继承,对于派生类的成员和友元,派生类中的成员都可访问,所以可以通过基类对象访问的成员都可通过派生类对象访问,因此可以将派生类当作基类用,可以由派生类转换成基类。

3.3 D的派生类的成员和友元

  1. protected继承
#include <iostream>
#include <string>

using namespace std;

class B 
{
public:
    int a;
};

class D: protected B
{
    
};
class E: private D 
{
    void f(D &d)
    {
        B *p = &d;
    }
};

int main()
{
    
}

运行结果如下

..Program finished with exit code 0
Press ENTER to exit console.
  1. private继承
#include <iostream>
#include <string>

using namespace std;

class B 
{
public:
    int a;
};

class D: private B
{
    
};
class E: private D 
{
    void f(D &d)
    {
        B *p = &d;
    }
};

int main()
{
    
}

运行结果如下

main.cpp: In member function ‘void E::f(D&):
main.cpp:20:9: error:class B B::B’ is private within this context
   20 |         B *p = &d;
      |         ^
main.cpp:12:7: note: declared private here
   12 | class D: private B
      |       ^
main.cpp:20:17: error:Bis an inaccessible base ofD20 |         B *p = &d;
      |      

使用公有继承时,派生类中的基类成员访问权限不变。对于D的派生类E来说,通过基类B的对象可访问的成员,通过派生类D的对象也可访问,因此可以将派生类当作基类用,可以由派生类转换成基类。
使用受保护继承时,派生类中基类公有成员的访问权限变为protected。对于D的派生类E来说,仍然是可访问的,所以通过基类B的对象可访问的成员,通过派生类D的对象也可访问,因此可以将派生类当作基类用,可以由派生类转换成基类。
使用私有继承时,派生类中的基类公有成员的访问权限变为private。对于D的派生类E来说,变得不可访问,所以通过基类B的对象可访问的成员,通过派生类D的对象不可访问,因此不能将派生类当作基类用,不能由派生类转换成基类。

四、虚函数

1. 虚函数概念

基类中有以下两种成员函数

  1. 基类希望派生类直接继承而不要改变的函数。
  2. 基类希望派生类进行覆盖的函数,这类函数,基类通常将其声明为虚函数
    基类通过在其成员函数声明语句前加上关键字virtual将该成员函数声明为虚函数。除构造函数、静态成员函数外,其他任何函数都可以是虚函数。

2. 动态绑定

当使用指针或引用调用虚函数时,该调用发生动态绑定,其解析过程发生在运行时,根据指针或引用绑定的对象类型不同,该调用会执行对象对应的函数版本。
动态绑定只有当通过指针或引用调用虚函数时才会发生。成员函数如果没被声明成虚函数,则其解析过程发生在编译时,其调用在编译时绑定。如果是通过对象调用函数,其调用也是在编译时绑定。

3. 虚函数的继承

如果基类把一个函数声明为虚函数,则其所有派生类中这个函数也是虚函数。
派生类经常(但不总是)覆盖它继承的虚函数,如果派生类没有覆盖其基类中的虚函数,则会直接继承其在基类的版本。
一个派生类如果覆盖其继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。
派生类中的虚函数返回类型也必须与基类函数的返回类型匹配。一个例外情况是:当类的返回是类本身的引用或指针时,基类和派生类的函数返回各自类型的引用或指针(前提是派生类向基类的转换是可访问的)

4. override说明符

派生类如果定义了一个与基类虚函数同名但形参或返回类型不同的函数,仍是合法的,编译器会认为这是个新定义的函数,而不是覆盖基类的版本。这个函数与基类的虚函数是相互独立的,且在派生类中会隐藏基类的虚函数。就实际的编程习惯而言,这往往意味着发生了错误,因为有可能我们是想覆盖基类中的虚函数,这种错误往往难以调试和排查。
c++11新标准中,可以用override关键字说明派生类中虚函数,指明覆盖意图,当不能发生覆盖时会报编译错误,使我们可以在编译阶段发现这种错误。

代码示例

#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
    virtual void speak()
    {
        cout << "animal speak" << endl;
    }
    virtual void walk()
    {
        cout << "animal walk" <<endl;
    }
};

class Dog: public Animal
{
public:
    void speak()  override   //正确
    {
        cout << "wangwang" << endl;
    }
    void walk(int n) override  //错误:和虚函数的形参不同
    {
        cout << "dog walk" << endl;
    }
    int walk() override  //错误:和虚函数的返回类型不同
    {
        cout << "dog walk" << endl; 
    }
    void run() override  //错误:run不是虚函数
    {
        cout << "dog run" << endl; 
    }
    
};

int main()
{
    
}

以上代码编译结果如下:

main.cpp:26:10: error:void Dog::walk(int)’ marked ‘override, but does not override
   26 |     void walk(int n) override  //错误:和虚函数的形参不同
      |          ^~~~
main.cpp:30:9: error: conflicting return type specified forvirtual int Dog::walk()30 |     int walk() override  //错误:和虚函数的返回类型不同
      |         ^~~~
main.cpp:13:18: note: overridden function is ‘virtual void Animal::walk()13 |     virtual void walk()
      |                  ^~~~
main.cpp:34:10: error:void Dog::run()’ marked ‘override, but does not override
   34 |     void run() override  //错误:run不是虚函数
      |          ^~~

5. final说明符

我们可以将一个虚函数用final说明符进行说明,则在之后继承的派生类则不能再覆盖此函数。
代码示例:

#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
    virtual void speak()
    {
        cout << "animal speak" << endl;
    }
};

class Dog: public Animal
{
public:
    void speak()  override  final 
    {
        cout << "wangwang" << endl;
    }
};

class corgi: public Dog
{
public:
    void speak()
    {
        cout << "wang~~~" <<endl;  //错误:Dog中已将其声明为final
    }
};

class cat: public Animal
{
public:
    void speak()
    {
        cout << "miaomiao" <<endl; //正确:cat直接继承Animal而非Dog
    }
};

int main()
{
    
}

以上代码编译结果如下:

main.cpp:27:10: error: virtual function ‘virtual void corgi::speak()’ overriding final function
   27 |     void speak()
      |          ^~~~~
main.cpp:18:10: note: overridden function is ‘virtual void Dog::speak()18 |     void speak()  final
      |          ^~~~~

6. 虚函数与默认实参

和其他函数一样,虚函数也可以有默认实参。如果某次调用使用了默认实参,则该次实参值由本次调用的静态类型决定。换句话说,如果通过基类的引用或指针调用函数,无论本次调用实际绑定的是派生类版本还是基类版本,都使用基类中定义的默认实参。

如果虚函数有默认实参,最好使基类和派生类中的默认实参保持一致。

7. 强制使用指定版本的虚函数

在某些情况下,我们希望对虚函数的调用不发生动态绑定,而是强制执行虚函数的某个特定版本,这个是可以使用作用域运算符实现这一目的。

    Dog d;
    Animal *p = &d;
    p->speak();  //动态绑定
    p->Animal::speak();   //强制使用Animal的函数版本

通常情况下,只有成员函数或友元中的代码才需要使用作用域运算符来回避虚函数的机制,从而指定使用虚函数版本。
比如派生类的虚函数通常调用它的基类虚函数版本完成继承层次中所有类型都要执行的共同任务,之后再实现与特定派生类型相关的操作。
示例如下:

#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
    virtual void speak()
    {
        cout << "animal speak" << endl;
    }
};

class Dog: public Animal
{
public:
    void speak()
    {
        Animal::speak(); //先调用基类的speak版本完成共同操作。
        cout << "wangwang" << endl;
    }
};

int main()
{
    Dog d;
    d.speak(); 
}

如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。 但是在实际运行中未出现无限递归!

五、抽象基类

1. 纯虚函数

对于某一些类,里面的虚函数只是用于提供接口,不执行具体实现,具体实现由其派生类实现,这种类不希望用户创建具体对象,这时可以将虚函数声明为纯虚函数
在类内虚函数声明的分号前书写=0即可将虚函数声明为纯虚函数,其中,=0只能出现在类内的虚函数声明语句处。
代码示例:

#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
    virtual void speak() = 0;
};

class Dog: public Animal
{
public:
    void speak()
    {
        cout << "wangwang" << endl;
    }
};

int main()
{
    Dog d;
    d.speak(); 
}

值得注意的是,我们也可以给纯虚函数提供定义,但函数体必须定义在类的外部。也就是说,我们不能在类内为一个=0的函数提供函数体。

2. 抽象基类

含有纯虚函数的类或者未覆盖直接继承纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。我们不能创建一个抽象基类的对象。

六、继承中的类作用域

每个类都会定义它自己的作用域,在类的作用域之外,类的普通数据和函数成员只能由对象、引用或指针使用成员运算符来访问,类的类型成员由作用域运算符访问。
存在继承关系时,派生类的作用域嵌套在基类作用域之内,如果一个名字在派生类作用域内无法正确解析,则编译器会继续在外层的基类作用域中寻找该名字的定义。正是因为类作用域的这种继承嵌套关系,才使得派生类能像使用自己的成员一样使用基类的成员。

1. 名字查找发生在编译时

一个对象、引用和指针的静态类型决定了对象的哪些成员可见,即使静态类型和动态类型不一致,其可见成员仍是由静态类型决定,因为名字查找发生在编译时。
具体见代码示例:

#include <iostream>
#include <string>

using namespace std;

class Animal
{

};

class Dog: public Animal
{
public:
    void speak()
    {
        cout  << "wangwang" << endl;
    }
};

int main()
{
    Dog d;
    Animal *p = &d;
    p->speak(); //错误:虽然p的动态类型Dog中有speak成员,但p的静态类型是Animal中没有speak,所以无法通过p访问speak
}

运行结果如下

main.cpp: In function ‘int main():
main.cpp:24:8: error:class Animal’ has no member named ‘speak’
   24 |     p->speak();
      |    

2. 名字冲突与继承

和其他作用域一样,派生类也能重定义其直接基类或间接基类中的名字,此时,定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字。

2.1 成员数据冲突

代码示例如下

#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
    Animal(int i): age(i) {} //构造函数
protected:
    int age;
};

class Dog: public Animal
{
public:
    Dog(int i): Animal(1),age(i) {} //用i初始化Dog::age,用1初始化Aniaml::age。 构造函数与继承的介绍详见下一节。
    int getAge()
    {
        return age;                 //返回Dog::age
    }
protected:
    int age;
};

int main()
{
    Dog d(42);
    cout << d.getAge() << endl;
}

运行结果如下:

42
...Program finished with exit code 0
Press ENTER to exit console.

可以看到返回的是Dog类型对象的age。
可以通过作用域运算符来使用隐藏的基类成员。将上述代码中的getAge改写如下:

int getAge()
{
	return Animal::age;         //返回Aniaml::age
}

得到的运行结果如下

1
...Program finished with exit code 0
Press ENTER to exit console.

2.2 成员函数名字冲突

声明在内层作用域的函数并不会重载声明在外层作用域的函数,因此,定义在派生类中的函数也不会重载基类中的成员函数。如果派生类中的成员函数与基类中的成员函数重名,则派生类将在其作用域内隐藏该基类成员函数。即使派生类成员函数和基类成员函数的形参列表不一致,基类成员也会被隐藏掉。

#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
    void speak(string s)
    {
        cout << s << endl;
    }
};

class Dog: public Animal
{
public:
    void speak()
    {
        cout << "wangwang~" << endl; //隐藏Animal中的speak
    }
};

int main()
{
    Animal a;
    Dog d;
    a.speak("aa~~");    //正确:调用Animal::speak
    d.speak();          //正确:调用Dog::speak
    d.speak("wang~~");  //错误:入参为string的speak成员函数被隐藏了
}

运行结果如下

main.cpp: In function ‘int main():
main.cpp:30:12: error: no matching function for call to ‘Dog::speak(const char [7])30 |     d.speak("wang~~");  //错误:入参为string的speak成员函数被隐藏了
      |     ~~~~~~~^~~~~~~~~~
main.cpp:18:10: note: candidate:void Dog::speak()18 |     void speak()
      |          ^~~~~
main.cpp:18:10: note:   candidate expects 0 arguments, 1 provided

2.3 函数调用的解析过程

理解函数调用的解析过程对于理解C++的继承至关重要,假定我们调用p->mem()或者obj.mem,则依次执行以下4个步骤:

  1. 首先确定p或obj的静态类型。因为我们调用的是一个成员,所以该类型必须是一个类类型
  2. 在p或obj对应的静态类型中寻找成员mem,如果没有找到,则继续依次在其基类中寻找,直到找到或达到继承链的顶端,如果最后仍未找到,则编译器报错。
  3. 如果找到mem,则进行函数参数的类型检查,以确认对于找到mem,本次调用是否合法。
  4. 如果调用合法,则编译器根据调用方式以及调用的是否是虚函数产生不同的代码:
    • 如果是虚函数且通过指针和引用调用,则编译器产生的代码将在运行时依据对象的动态类型确定运行虚函数的哪个版本。
    • 如果不是虚函数或者不是通过指针和引用调用,则编译器产生一个常规函数调用。

2.4 虚函数与作用域

现在可以理解为什么基类和派生类的虚函数必须有相同的形参列表了。如果派生类的虚函数形参和基类的不同,则无法通过基类的引用或指针来调用派生类的虚函数版本。(从2.3解析过程可以看出,在用基类指针或引用调用派生类的虚函数版本时,会因为形参类型不匹配而报错)。

#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
    virtual void speak()
    {
        cout << "Animal" << endl;
    }
};

class Dog: public Animal
{
public:
	// 隐藏了基类的speak(string)
	// Dog继承了Animal::speak(string)的定义,但因为名字被隐藏,所以通过Dog类型的对象无法调用speak(string),但仍可通过基类指针和引用调用,也可被Dog的派生类所继承。
    void speak(string s)
    {
        cout << "Dog " << "wangwang~" << s << endl;
    }
};

class corgi: public Dog
{
public:
    //覆盖基类的虚函数版本
    void speak()
    {
        cout << "corgi " << "wang~~~" <<endl;
    }
};

int main()
{
    Dog d;
    corgi c;
    Animal *p = &d, *pc = &c;
    d.speak();          //错误:在派生类中,Animal::speak名字被隐藏,名字查找是找到的是speak(string),会因为形参不匹配报错
    p->speak();         //正确:调用Animal::speak,派生类的虚函数表中仍有speak()版本的虚函数。
    p->speak("test");   //错误:在基类中,没有speak(string)函数
    pc->speak();        //正确:调用corgi::speak
}

对于派生类中隐藏的虚函数,派生类对象无法调用,绑定派生类对象的基类指针或引用可以调用。

七、构造函数和析构函数

1. 构造函数与继承

  • 尽管派生类对象含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员,派生类必须使用基类的构造函数类初始化它的基类部分。每个类控制自己的成员初始化过程。
  • 派生类并不继承基类的构造函数,所以在派生类中必须定义自己的构造函数,并使用基类的构造函数初始化其基类部分。

初始化时先初始化基类部分,再按声明的顺序依次初始化派生类部分
派生类构造函数只初始化它的直接基类,它的直接基类如果是继承的其他类,则它的直接基类再继续初始化基类的基类。

  1. 构造函数代码示例如下:
#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
    Animal() {cout << "Animal()" << endl;}
    Animal(string aname, int anage) : name(aname), age(anage) {cout << "Animal(string aname, int anage)" << endl;}
    string getName()
    {
        cout << "name: " << name << endl;
        return name;
    }
    
protected:
    string name;
    int age;
};

class Dog: public Animal
{
public:
    Dog() {cout << "Dog()" << endl;}
    Dog(string aname, int anage): Animal(aname, anage) {cout << "Dog(string aname, int anage)" << endl;}
};

int main()
{
    Dog dog1 ; 
    Dog dog2("laifu", 2);
    dog1.getName();
    dog2.getName();
}

结果如下:

Animal()
Dog()
Animal(string aname, int anage)
Dog(string aname, int anage)
name: 
name: laifu
  1. 如果将上述构造函数示例代码中的Dog类的构造函数改成如下形式
class Dog: public Animal
{
public:
    Dog() {}
    Dog(string aname, int anage): name(aname) age(anage) {}
};

则会有如下报错。

main.cpp: In constructor ‘Dog::Dog(std::string, int):
main.cpp:26:35: error: class ‘Dog’ does not have any field named ‘name’
   26 |     Dog(string aname, int anage): name(aname) age(anage) {}
      |                                   ^~~~
main.cpp:26:47: error: expected ‘{’ before ‘age’
   26 |     Dog(string aname, int anage): name(aname) age(anage) {}
      |             
  1. 如果将上述构造函数示例代码中的Dog类的构造函数删除,改成如下
class Dog: public Animal
{
public:
    //Dog(): Animal() {}
    //Dog(string aname, int anage): Animal(aname, anage) {}
};

则会有如下报错,因为Dog类中没有定义有两个入参的构造函数,dog1()没报错是因为若类中没有定义任何一个构造函数时,编译器会生成一个默认构造函数。

main.cpp:33:24: error: no matching function for call to ‘Dog::Dog(const char [6], int)33 |     Dog dog2("laifu", 2);

2. 虚析构函数

当我们delete一个动态分配的对象指针时,会调用对象的析构函数。由于基类指针可能指向派生类对象,有可能出现指针的静态类型和实际绑定的对象动态类型不一致的情况,这时编译必须清楚它执行的实际绑定对象的析构函数。为了保证执行正确的析构函数版本,需要将析构函数声明成虚函数。

基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
如果基类的析构函数不是虚函数,则delete一个之乡派生类对象的基类指针将产生未定义的行为。

八、using声明

1. 使用using声明改变个别成员的可访问性

具体见代码示例:

#include <iostream>
#include <string>

using namespace std;

class B 
{
public:
    int geta() 
    {
        cout << a << endl;
        return a;
    }
protected:
    int a;
};

class D: private B
{
public:
    using B::geta;  //在public用using声明geta,使geta的访问权限变为public,用户可访问
protected:
    using B::a; //在public用using声明a,使a的访问权限变为protected,派生类可访问
};

int main()
{
    D d;
    d.geta();
}

派生类只能为那些它可访问的名字提供using声明。

2. 覆盖重载的函数

和其他函数一样,成员函数无论是否是虚函数都可以重载。派生类可以覆盖重载函数的一个或多个实例。如果派生类希望所有重载版本对它都是可见的,那么它要么覆盖所有重载版本,要么一个都不覆盖。如果只覆盖其中一部分实例,则只有这些版本可见,其他没覆盖的版本则不可见。
有时派生类需要所有版本可见,但只需覆盖基类中的某些重载版本,此时,我们不得不覆盖所有版本,实现及其繁琐。一种很好的解决方案是使用using声明语句,现在派生类中声明此重载函数,使得所有重载版本对派生类可见,再覆盖其需要覆盖的重载版本。

#include <iostream>
#include <string>

using namespace std;

class B
{
public:
    virtual void print(int n) {cout<< "B: " << n <<endl;}
    virtual void print(double n) {cout<< "B: " << n <<endl;}
    virtual void print(string s) {cout<< "B: " << s <<endl;}
};

class D: public B
{
public:
    using B::print;
    void print(string s) {cout<< "D: " << s <<endl;}
};

int main()
{
    D d;
    d.print(2);
}

3. 使用using在派生类中生存基类对应的构造函数

在派生类中使用using将令编译器生成代码对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数。这些派生类生成的构造函数形如:
derived(param): base(args) { }

#include <iostream>
#include <string>

using namespace std;

class B 
{
public:
    B(): val(0), s("") {}
    B(int v): val(v), s("") {}
    B(string str): val(0), s(str) {}
    B(int v, string str): val(v), s(str) {}
private:
    int val;
    string s;
};

class D: public B
{
public:
    //使用using语句生成基类中每个构造函数对应的派生类构造函数
    //编译器会生成如下代码:
    //D(): B() {}
    //D(int v): B(v) {}
    //D(string str): B(str) {}
    //D(int v, string str): B(v, str) {}
    using B::B; 
};

int main()
{
    D d1, d2(2), d3("test"), d4(2,"test");
}

九、静态成员与继承

如果基类定义了一个静态成员,则整个继承体系中只存在该成员的唯一定义和唯一实例。

十、容器与继承

当我们使用容器存放继承体系中的对象时,通常采用间接存储的方式,因为容器不允许存放不同类型的元素,所以不能把具有继承关系的多种类型的对象直接存放在容器中。
当希望在容器中保存继承体系中的多种类型的对象时,通常存放基类的指针,基类的指针可以指向派生类对象。


  • 34
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值