前言
通过前面的学习,我们已经知道了如何通过创建一个新的子类来重用现有的代码(继承)。
但是如果我们需要在基类里提供一个通用的函数,但在它的某个子类里需要修改这个方法的实现,在C++里,覆盖(overriding)就可以做到。
覆盖方法
C++可以让我们很容易实现这种既有共同特征又需要在不同的类里面有不同实现的方法。
我们需要做的就是在类里重新声明这个方法,然后再改写一下它的实现代码(就想它是一个增加的方法那样)就行了。
比如说修改一下以前的例题:为我们的Animal添加eat()方法,并在Pig和Turtle中覆盖。
#include <iostream>
#include <string>
class Animal
{
public:
Animal(std::string theName);
void eat();
void sleep();
void drool();
protected:
std::string name;
};
class Pig : public Animal
{
public:
Pig(std::string theName);
void climb();
void eat(); // new!
};
class Turtle : public Animal
{
public:
Turtle(std::string theName);
void swim();
void eat(); // new!
};
Animal::Animal(std::string theName)
{
name = theName;
}
void Animal::eat()
{
std::cout << "I'm eating!" << std::endl;
}
void Animal::sleep()
{
std::cout << "I'm sleeping!Don't disturb me!\n" << std::endl;
}
void Animal::drool()
{
std::cout << "我是公的,看到母的我会流口水,我正在流口水。。。\n" << std::endl;
}
Pig::Pig(std::string theName) : Animal(theName)
{
}
void Pig::climb()
{
std::cout << "我是一个只漂亮的小母猪猪,我会上树,我正在爬树,嘘。。。\n" << std::endl;
}
void Pig::eat()
{
Animal::eat();
std::cout << name << "正在吃鱼!\n\n"; // new!
}
Turtle::Turtle(std::string theName) : Animal(theName)
{
}
void Turtle::swim()
{
std::cout << "我是一只小甲鱼,当母猪想抓我的时候,我就游到海里。。哈哈。。\n" << std::endl;
}
void Turtle::eat()
{
Animal::eat();
std::cout << name << "正在吃东坡肉!\n\n"; // new!
}
int main()
{
Pig pig("小猪猪");
Turtle turtle("小甲鱼");
// std::cout << "这只猪的名字是: " << pig.name << std::endl; // 错误
// std::cout << "每只乌龟都有个伟大的名字: " << turtle.name << std::endl; // 错误
pig.eat();
turtle.eat();
pig.climb();
turtle.swim();
return 0;
}
I'm eating!
小猪猪正在吃鱼!I'm eating!
小甲鱼正在吃东坡肉!我是一个只漂亮的小母猪猪,我会上树,我正在爬树,嘘。。。
我是一只小甲鱼,当母猪想抓我的时候,我就游到海里。。哈哈。。
重载
简化编程工作和提高代码可读性的另一种方法就是对方法进行重载。
重载机制允许你可以定义多个同名的方法(函数),只是它们的输入参数必须不同。(因为编译器是依靠不同的输入参数来区分不同的方法)
重载并不是一个真正的面向对象特征,它只是可以简化编程工作的捷径,而简化编程工作正是C++的全部追求。
下面对eat()进行重载。
#include <iostream>
#include <string>
class Animal
{
public:
Animal(std::string theName);
void eat();
void eat(int eatCount);
void sleep();
void drool();
protected:
std::string name;
};
class Pig : public Animal
{
public:
Pig(std::string theName);
void climb();
};
class Turtle : public Animal
{
public:
Turtle(std::string theName);
void swim();
};
Animal::Animal(std::string theName)
{
name = theName;
}
void Animal::eat()
{
std::cout << "I'm eating!" << std::endl;
}
void Animal::eat(int eatCount)
{
std::cout << "我吃了" << eatCount << "碗馄饨!\n\n";
}
void Animal::sleep()
{
std::cout << "I'm sleeping!Don't disturb me!\n" << std::endl;
}
void Animal::drool()
{
std::cout << "我是公的,看到母的我会流口水,我正在流口水。。。\n" << std::endl;
}
Pig::Pig(std::string theName) : Animal(theName)
{
}
void Pig::climb()
{
std::cout << "我是一个只漂亮的小母猪猪,我会上树,我正在爬树,嘘。。。\n" << std::endl;
}
Turtle::Turtle(std::string theName) : Animal(theName)
{
}
void Turtle::swim()
{
std::cout << "我是一只小甲鱼,当母猪想抓我的时候,我就游到海里。。哈哈。。\n" << std::endl;
}
int main()
{
Pig pig("小猪猪");
Turtle turtle("小甲鱼");
// std::cout << "这只猪的名字是: " << pig.name << std::endl; // 错误
// std::cout << "每只乌龟都有个伟大的名字: " << turtle.name << std::endl; // 错误
pig.eat();
turtle.eat();
pig.eat(15);
pig.climb();
turtle.swim();
return 0;
}
I'm eating!
I'm eating!
我吃了15碗馄饨!我是一个只漂亮的小母猪猪,我会上树,我正在爬树,嘘。。。
我是一只小甲鱼,当母猪想抓我的时候,我就游到海里。。哈哈。。
Pay Attention
对方法进行覆盖时(注意区分覆盖和重载)一定要看仔细,因为只要声明的输入参数和返回值与原来的不一样,那你编写的就将是一个重载方法而不是覆盖方法。
对从基类继承来的方法不能进行重载!!
例如
#include <iostream>
#include <string>
class Animal
{
public:
Animal(std::string theName);
void eat();
void sleep();
void drool();
protected:
std::string name;
};
class Pig : public Animal
{
public:
Pig(std::string theName);
void climb();
void eat(int eatCount);
};
class Turtle : public Animal
{
public:
Turtle(std::string theName);
void swim();
};
Animal::Animal(std::string theName)
{
name = theName;
}
void Animal::eat()
{
std::cout << "I'm eatting!" << std::endl;
}
void Animal::sleep()
{
std::cout << "I'm sleeping!Don't disturb me!\n" << std::endl;
}
void Animal::drool()
{
std::cout << "我是公的,看到母的我会流口水,我正在流口水。。。\n" << std::endl;
}
Pig::Pig(std::string theName) : Animal(theName)
{
}
void Pig::climb()
{
std::cout << "我是一个只漂亮的小母猪猪,我会上树,我正在爬树,嘘。。。\n" << std::endl;
}
void Pig::eat(int eatCount)
{
std::cout << "我吃了" << eatCount << "碗馄饨!\n\n";
}
Turtle::Turtle(std::string theName) : Animal(theName)
{
}
void Turtle::swim()
{
std::cout << "我是一只小甲鱼,当母猪想抓我的时候,我就游到海里。。哈哈。。\n" << std::endl;
}
int main()
{
Pig pig("小猪猪");
Turtle turtle("小甲鱼");
// std::cout << "这只猪的名字是: " << pig.name << std::endl; // 错误
// std::cout << "每只乌龟都有个伟大的名字: " << turtle.name << std::endl; // 错误
pig.eat();
turtle.eat();
pig.eat(15);
pig.climb();
turtle.swim();
return 0;
}
error C2660: “Pig::eat”: 函数不接受 0 个参数
note: 参见“Pig::eat”的声明
因为 pig.eat()重载失败,因为它被有参数的pig.eat(15)声明的一个新的方法给覆盖掉了。所以只能在同一个类里进行重载,继承后的不能重载。
C++成员函数的重载、覆盖、隐藏区别
成员函数被重载的特征:
- 相同的范围(在同一个类中);
- 函数名字相同;
- 参数不同;
- virtual 关键字可有可无
覆盖是指派生类函数覆盖基类函数,特征是:
- 不同的范围(分别位于派生类和基类中);
- 函数的名字相同;
- 参数相同;
- 基类函数必须有virtual关键字。
以下示例中,函数Base::f(int)与Base::f(float)相互重载,而Base::g(void)被Derived::g(void)覆盖。
#include <iostream>
using namespace std;
class Base
{
public:
void f(int x)
{
cout << "Base::f(int) " << x << endl;
}
void f(float x)
{
cout << "Base::f(float) " << x << endl;
}
virtual void g(void)
{
cout << "Base::g(void)" << endl;
}
};
class Derived : public Base
{
public:
virtual void g(void)
{
cout << "Derived::g(void)" << endl;
}
};
int main(void)
{
Derived d;
Base *pb = &d;
pb->f(42); // Base::f(int) 42
pb->f(3.14f); // Base::f(float) 3.14
pb->g(); // Derived::g(void)
return 0;
}
Base::f(int) 42
Base::f(float) 3.14
Derived::g(void)
令人疑惑的隐藏规则
本来仅仅区分重载和覆盖并不算困难,但是C++的隐藏规则使问题复杂性陡然上升。
这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
- 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。(重载是要求在同一个类中同名、参数不同)
- 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。(覆盖是要求不同的范围,函数名字和参数相同,但必须有virtual关键字)
以下示例中:
(1)函数Derived::f(float) 覆盖了 Base::f(float)。
(2)函数Derived::g(int) 隐藏了 Base::g(float),而不是重载。
(3)函数Derived::h(float) 隐藏了 Base::h(float),而不是覆盖。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void f(float x)
{
cout << "Base::f(float) " << x << endl;
}
void g(float x)
{
cout << "Base::g(float) " << x << endl;
}
void h(float x)
{
cout << "Base::h(float) " << x << endl;
}
};
class Derived : public Base
{
public:
virtual void f(float x)
{
cout << "Derived::f(float) " << x << endl;
}
void g(int x)
{
cout << "Derived::g(int) " << x << endl;
}
void h(float x)
{
cout << "Derived::h(float) " << x << endl;
}
};
“隐藏”的发生可谓神出鬼没,常常产生令人迷惑的结果。
如以下示例中,bp 和 dp 指向同一地址,按理说运行结果应该是相同的,可事实并非这样。
int main(void)
{
Derived d;
Base *pb = &d;
Derived *pd = &d;
pb -> f(3.14f); // Derived::f(float) 3.14
pd -> f(3.14f); // Derived::f(float) 3.14
pb -> g(3.14f); // Base::g(float) 3.14
pd- > g(3.14f); // Derived::g(int) 3 (surprise!)
pb -> h(3.14f); // Base::h(float) 3.14 (surprise!)
pd -> h(3.14f); // Derived::h(float) 3.14
return 0;
}
Derived::f(float) 3.14
Derived::f(float) 3.14
Base::g(float) 3.14
Derived::g(int) 3
Base::h(float) 3.14
Derived::h(float) 3.14
摆脱隐藏
隐藏规则引起了不少麻烦。以下示例中,语句pd->f(10)的本意是想调用函数 Base::f(int),但是 Base::f(int) 不幸被 Derived::f(char *str) 隐藏了。由于数字10不能被隐式地转化为字符串,所以在编译时出错。
class Base
{
public:
void f(int x);
};
class Derived : public Base
{
public:
void f(char *str);
};
void Test(void)
{
Derived *pd = new Derived;
pd -> f(10); // error
}
error C2664: “void Derived::f(char *)”: 无法将参数 1 从“int”转换为“char *”
从整型强制转换为指针类型要求 reinterpret_cast、C 样式强制转换或函数样式强制转换
总结
上述内容是我在FishC的视频中以及浏览博客做的笔记,感谢鱼C大佬的讲解,以下附上视频地址以及博客地址。以后关于覆盖、重载以及隐藏有了自己的见解也会陆续加入其中。
只要不是重载和覆盖其他的都是隐藏,重载很好理解,覆盖就是多了个virtual,其他的一切情况都是隐藏,当然,编译出错情况例外。
参考地址
《C++快速入门--小甲鱼》https://www.bilibili.com/video/av7595819/?p=16
C++成员函数的重载、覆盖、隐藏区别 https://fishc.com.cn/blog-9-1122.html