从0开撸C++(三)——继承和多态特性

从0开撸C++系列

往期地址:

本期主题:
c++中的继承与多态特性



1.继承体系下的构造/析构函数关系

我们知道继承指的是子类从父类继承对应的成员变量/函数方法,那么父类中的构造与析构函数,是否会被子类所继承呢?

class 派生类名: 访问控制 基类名1,访问控制 基类名2

看一个实际的例子。

基类:person
派生类:man

//person.h
class person
{
public:
	string name;
	person();   //添加父类的构造函数
	~person();  //添加父类的析构函数

private:

protected:

};

//man.h
class man: public person
{
public:
	void coding(void)
	{cout << "name :" << this->name << " here is man coding" << endl;};  
	man();   //添加子类的构造函数
	~man();  //添加子类的析构函数


private:

protected:
};

//main.cpp
int main(void)
{
    man jason;
    return 0;
}

gary@ubuntu:~/workspaces/cpp_study/2.3.constructor$ ./app 
here is person::person()
here is man::man()
here is man::~man()
here is person::~person()
gary@ubuntu:~/workspaces/cpp_study/2.3.constructor$ 

由此可以看出,调用的顺序是 调用父类的构造->调用子类的构造->调用子类的析构->调用父类的析构

2.子类和父类的类型兼容

派生类从基类继承而来,因此派生类和基类其实是有一定的关系的,这个关系可以被称为类型兼容规则,简单概括一下:

  • 派生类的对象可以隐含转换为基类对象
  • 派生类的指针可以隐含转换为基类的指针

2.1 派生类与基类的对象转换

int main(void)
{
    person person_gen;  //定义派生类对象
    man jason;          //定义基类对象
    person_gen = jason; //这里将派生类的对象赋值给基类的对象,按照一般理解此时person_gen已经变成了派生类的内容
    person_gen.print_age(); //调用发现,仍然是基类的方法
    return 0;
}
gary@ubuntu:~/workspaces/cpp_study/2.3.constructor$ ./app 
here is person::person()
here is person::person()
here is man::man()
person::print_age: 10
here is man::~man()
here is person::~person()
here is person::~person()
gary@ubuntu:~/workspaces/cpp_study/2.3.constructor$ 

2.2 派生类与基类的指针转换

int main(void)
{
   man *jason = new man();  //定义派生类的指针
   person *p_gen = jason;   //定义基类的指针,并且将派生类的指针赋值给这个指针
   p_gen->print_age();      //调用发现,仍然是基类的方法
   delete jason;
    return 0;
}
gary@ubuntu:~/workspaces/cpp_study/2.3.constructor$ ./app 
here is person::person()
here is man::man()
person::print_age: 10
here is man::~man()
here is person::~person()
gary@ubuntu:~/workspaces/cpp_study/2.3.constructor$ 

3.多继承

多继承就是一个子类具有多个父类

class A: xxx B,xxx C, xxx代表访问控制,即可以是public/private/protected

//test.h
class A
{
public:
    void helloA(void);
};

class B
{
public:
    void helloB(void);
};

class C: public A, public B //c类继承于A类和B类
{
public:
    
};

//test.cpp
void A::helloA()
{
    cout << "hello A" << endl;
}


void B::helloB()
{
    cout << "hello B" << endl;
}

int main(void)
{
    C testc;
    testc.helloA();  //调用继承自A的成员方法
    testc.helloB();  //调用继承自B的成员方法
    return 0;
}
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ ./app 
hello A
hello B

3.1 多继承的二义性问题

当C多继承自A和B时,当C去调用A和B中的同名成员时,即会有二义性问题,编译器并没有办法确认C想调用A和B中的哪个成员;
上面那个例子中,如果我们将A和B的成员方法同名,看会发生什么问题

gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ make
g++ test.cpp -o app
test.cpp: In function ‘int main():
test.cpp:20:11: error: request for member ‘hello’ is ambiguous //提示hello是含糊的,定义不清
     testc.hello();
           ^
test.cpp:12:6: note: candidates are: void B::hello()
 void B::hello()
      ^
test.cpp:6:6: note:                 void A::hello()
 void A::hello()
      ^

3.2 利用namespce指定调用

如果在调用时,指定了具体的父类,则不会有问题

子类对象.父类::方法

int main(void)
{
    C testc;
    testc.A::hello(); //这样指定父类的方法
    return 0;
}
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ ./app 
hello A

3.3 用虚继承来解决二义性问题

在代码中,存在这样一种情况,从一个父类A,继承了两个子类B1和B2,类型C又多继承了B1和B2,这样看起来就是一个菱形的结构,这种情况我们称为菱形继承,在这种情况下,如果子类C去调用A类中原有的方法就会报错

//test.h
class gen  //基类
{
public:
    void set(void);
};

class A: public gen
{
public:
    void hello(void);
};

class B: public gen
{
public:
    void hello(void);
};

class C: public A, public B
{
public:
    
};
//test.cpp
int main(void)
{
    C testc;
    testc.set(); //调用共同的方法,会报错
    return 0;
}
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ make
g++ test.cpp -o app
test.cpp: In function ‘int main():
test.cpp:24:11: error: request for member ‘set’ is ambiguous
     testc.set();
           ^
test.cpp:6:6: note: candidates are: void gen::set()
 void gen::set()
      ^
test.cpp:6:6: note:                 void gen::set()
Makefile:2: recipe for target 'all' failed
make: *** [all] Error 1

可以用虚继承来解决这个问题,注意:这里的虚继承和虚函数并没有任何关系,(就有点像雷锋和雷峰塔的关系,除了名字相近,实际上并没有联系)

//在继承时加上 virtual即可解决这个问题
//test.h
class gen  //基类
{
public:
    void set(void);
};

class A: virtual public gen  //添加上virtual,代表虚继承
{
public:
    void hello(void);
};

class B: public gen  //添加上virtual,代表虚继承
{
public:
    void hello(void);
};

4.多态

面向对象编程的三大特征,封装、继承和多态

4.1 多态的概念

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

  • 在基类中声明方法为virtual,代表其为虚函数
  • 在派生类中重新实现同名方法,以实现多态,这就是override(重写、覆盖)
//test.h
class gen
{
public:
    void set(void);
    virtual void hello(void);  //这里用virtual声明为虚函数
};

class A: public gen
{
public:
    void hello(void);
};

class B: public gen
{
public:
    void hello(void);
};
//test.cpp

int main(void)
{
    gen *testgen = new gen();
    testgen->hello();      //调用基类的方法

    A *testa = new A();  //新建派生类指针
    testgen = testa;     //将派生类指针赋值给基类指针,这句可以理解为多态特性

    testgen->hello();    //如果没有virtual,实际上打印还是发现是基类的成员方法,如果有virtual,就会实现多态的特性,实现派生类的方法
    //B testb;
    //testb.hello();
    return 0;
}
//基类的方法中未加virtual的结果
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ ./app 
hello gen
hello gen
//加上virtual的结果
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ ./app 
hello gen
hello A

4.2 纯虚函数

前面用virtual表达的是虚函数,其实还有一种称为纯虚函数,纯虚函数就是在基类中只有原型而没有实体的一种虚函数

virtual xxx = 0 // =0 定义了是个纯虚函数

纯虚函数的特点在于:

  • 纯虚函数所在的类无法实例化对象
  • 纯虚函数并不占用内存
//当想用纯虚函数的类来实例化对象时:
//test.h
class gen
{
public:
    void set(void);
    virtual void hello(void) = 0;  //纯虚函数
};
//test.cpp
int main(void)
{
	gen testgen;
	return 0;
}

gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ make
g++ test.cpp -o app
test.cpp: In function ‘int main():
test.cpp:35:9: error: cannot declare variable ‘testgen’ to be of abstract type ‘gen’
     gen testgen;

4.3 抽象类

带有纯虚函数的类被称为抽象类,抽象类只能作为基类来派生新类,不可实例化对象。
抽象类的意义:

  • 由于派生类必须实现基类的纯虚函数后,才能去实例化对象,因此基类就规定了派生类的行为
  • 具有接口的概念,定义了一套访问的规则,像java语言中的 interface关键词;

4.4 虚析构函数

  • 1.定义:
    在析构函数前加virtual,则析构函数变成虚析构函数;
  • 2.为什么需要虚析构函数:
    观察父类/子类的析构函数调用顺序,前面提到父类和子类的构造、析构函数的 调用的顺序是 调用父类的构造->调用子类的构造->调用子类的析构->调用父类的析构
//test.h
class gen
{
public:
    void set(void);
    virtual void hello(void) = 0;
    virtual ~gen();//虚析构函数
};
class A: public gen
{
public:
    void hello(void);
    ~A();
};

//test.cpp
gen::~gen()  //父类析构
{
    cout << "~gen" << endl;
}

A::~A()      //子类析构
{
    cout << "~A" << endl;
}
int main(void)
{
//1.写法1,分配在栈上
    gen *testgen;
    A testa;          //分配在栈上,符合预期
    testgen = &testa;
//2.写法2,分配在堆上
    gen *testgen = new A();
    delete testgen;

    return 0;
}
//写法1
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ ./app 
~A
~gen
//写法2
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ ./app 
~gen

能看出上面的结果,当分配在栈上时,调用顺序符合预期,而分配在堆上时,却与预期不一致;
加上在析构函数前加上virtual时,分配在堆上的代码在析构时也符合预期;

virtual虚函数的意义:
成员函数在运行时动态解析和绑定具体执行的函数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值