电子产业一站式赋能平台

PCB联盟网

搜索
查看: 30|回复: 0
收起左侧

为什么多态一定需要指针或者引用才可触发?

[复制链接]

558

主题

558

帖子

6588

积分

高级会员

Rank: 5Rank: 5

积分
6588
发表于 昨天 09:01 | 显示全部楼层 |阅读模式
点击上方“C语言与CPP编程”,选择“关注/置顶/星标公众号
干货福利,第一时间送达!
最近有小伙伴说没有收到当天的文章推送,这是因为微信更改了推送机制,导致没有星标公众号的小伙伴刷不到当天推送的文章,无法接收到一些比较实用的知识和资讯。所以建议大家加个星标??,以后就能第一时间收到推送了。

rfqkwuzue5r6405092007.png

rfqkwuzue5r6405092007.png


看一个例子:
#include
#include
class Base {
protected:
    int m_value{};
public:
    Base(int value) : m_value{ value } { }
    virtual std::string_view getName() const { return"Base"; }
    int getValue() const { return m_value; }
};
class Derived :public Base {
public:
    Derived(int value) : Base{ value } { }
    std::string_view getName() const override { return"Derived"; }
};
int main() {
    Derived derived{ 5 };
    std::cout "derived is a " " and has value " '
';
    Base& ref{ derived };
    std::cout "ref is a " " and has value " '
';
    Base* ptr{ &derived };
    std::cout "ptr is a " getName() " and has value " getValue() '
';
    return 0;
}
在上面的例子中,ref 引用并 ptr 指向 derived,derived 有一个 Base 部分和一个 Derived 部分。
因为 ref 和 ptr 是 Base 类型的,所以 ref 和 ptr 只能看到 derived 的 Base 部分,而derived 的 Derived 部分仍然存在,但通过 ref 或 ptr 无法看到。
然而,通过使用虚函数,我们可以访问派生版本的函数。因此,上面的程序输出:
derived is a Derived and has value 5
ref is a Derived and has value 5
ptr is a Derived and has value 5
但是,如果我们不是将 Derived 对象设置为 Base 引用或指针,而是简单地将 Derived 对象赋值给 Base 对象,会发生什么呢?
int main() {
    Derived derived{ 5 };
    Base base{ derived }; // 这里会发生什么?
    std::cout "base is a " " and has value " '
';
    return 0;
}
记住,derived 有一个 Base 部分和一个 Derived 部分。当我们把 Derived 对象赋值给 Base 对象时,只有 Derived 对象的 Base 部分被复制。Derived 部分不会被复制。
在上面的例子中,base 接收了 derived 的 Base 部分的副本,但没有接收 Derived 部分。这个 Derived 部分实际上被“切掉”了。因此,将 Derived 类对象赋值给 Base 类对象被称为对象切片
因为 base 是并且仍然只是一个 Base,Base 的虚指针仍然指向 Base。因此,base.getName() 解析为 Base::getName()。上面的例子输出:
base is a Base and has value 5
如果谨慎使用,切片可能是无害的。然而,如果使用不当,切片可能会以多种方式导致意想不到的结果。
现在,你可能会认为上面的例子有点傻。毕竟,你为什么要像那样把 derived 赋值给 base 呢?你可能不会。然而,切片更有可能在函数中意外发生。看看以下函数:
void printName(const Base base) // 注意:base 是按值传递,而不是引用
{
    std::cout "I am a " '
';
}
这是一个非常简单的函数,带有一个 const base 对象参数,该参数是按值传递的。如果我们这样调用这个函数:
int main() {
    Derived d{ 5 };
    printName(d); // 糟糕,调用时没有注意到这是按值传递的
    return 0;
}
当你编写这个程序时,你可能没有注意到 base 是一个值参数,而不是引用。因此,当调用 printName(d) 时,虽然我们可能期望 base.getName() 调用虚函数 getName() 并打印“I am a Derived”,但这并不是发生的事情。
相反,Derived 对象 d 被切片,只有 Base 部分被复制到 base 参数中。当 base.getName() 执行时,即使 getName() 函数是虚函数,也没有 Derived 部分供它解析。因此,这个程序输出:
I am a Base
在这种情况下,很明显发生了什么,但如果你的函数实际上没有像这样打印任何标识信息,追踪错误可能会很困难。
当然,这里的所有切片都可以通过将函数参数改为引用而不是按值传递来轻松避免(这是为什么按引用而不是按值传递类的另一个好理由)。
void printName(const Base& base) // 注意:base 现在是按引用传递
{
    std::cout "I am a " '
';
}
int main() {
    Derived d{ 5 };
    printName(d);
    return 0;
}
这个程序输出:
I am a Derived
另一个新手程序员在使用 std::vector 实现多态性时遇到麻烦的地方是切片。看看以下程序:
#include
int main() {
    std::vector[B] v{};
    v.push_back(Base{ 5 }); // 向我们的向量添加一个 Base 对象
    v.push_back(Derived{ 6 }); // 向我们的向量添加一个 Derived 对象
    // 打印出我们向量中的所有元素
    for (const auto& element : v)
        std::cout "I am a " " with value " '
';
    return 0;
}
这个程序编译得很好。但运行时,它输出:
I am a Base with value 5
I am a Base with value 6
与前面的例子类似,因为 std::vector 被声明为 Base 类型的向量,当 Derived(6) 被添加到向量中时,它被切片了。
修复这个问题有点困难。许多新程序员尝试创建一个对象引用的 std::vector,像这样:
std::vector[B] vec;
不幸的是,这无法编译。std::vector 的元素必须是可赋值的,而引用不能被重新赋值(只能初始化)。
解决这个问题的一个方法是创建一个指针的向量:
std::vector[B] vec;  
vec.push_back(new Base(5));  
vec.push_back(new Derived(6));
这将打印:
I am a Base with value 5  
I am a Derived with value 6
它奏效了!关于这一点,有几点需要注意。首先,nullptr 现在是一个有效的选项,这可能或可能不是你想要的。
其次,你现在必须处理指针语义,这可能会很笨拙。但好处是,使用指针允许我们将动态分配的对象放入向量中(只是不要忘记显式删除它们)。
另一个选择是使用 std::reference_wrapper,这是一个模拟可重新赋值引用的类:
std::vectorstd::reference_wrapper[B]> vec;  
Base b(5);  
Derived d(6);  
vec.push_back(b);  
vec.push_back(d);
在上面的例子中,我们看到了切片导致错误结果的情况,因为派生类被切掉了。现在让我们来看另一个危险的情况,即使派生对象仍然存在!
考虑以下代码:
Derived d1;  
Derived d2;  
Base& b = d2;  
b = d1;
前 3 行代码很直接。创建两个 Derived 对象,并将一个 Base 引用指向第二个对象。
第 4 行代码是问题所在。因为 b 指向 d2,而我们将 d1 赋值给 b,你可能会认为结果是 d1 被复制到 d2 中——如果 b 是一个 Derived 类型的,这确实会发生。但 b 是一个 Base 类型的,C++ 为类提供的赋值运算符默认不是虚函数。因此,复制一个 Base 的赋值运算符被调用,只有 d1 的 Base 部分被复制到 d2 中。
因此,你会发现 d2 现在有了 d1 的 Base 部分和 d2 的 Derived 部分。在这个特定的例子中,这不是问题(因为 Derived 类没有自己的数据),但在大多数情况下,你刚刚创建了一个法兰肯斯坦对象——由多个对象的部分组成。
更糟糕的是,没有简单的方法可以防止这种情况发生(除了尽可能避免这种赋值)。
看到这里,你是否清楚了多态为什么一定要指针或者引用才能触发?

现在不少朋友都在准备校招,常规的技术学习只是提高了代码能力,还没有提升从 0 到 1 整体做项目和解决问题的能力!
为此我们特别推出了C++训练营,带着你从 0 到 1 做 C++ 项目(你可以从项目池中任选项目),帮助你提升做项目的能力,提升从0到1的能力,熟悉做项目的完整流程,比如开发环境、编译脚本、架构设计、框架搭建、代码发布、问题调试、单元测试
除了上面的,还有需求分析、项目规划、架构设计、任务拆解、时间规划、版本管理。另外做项目的过程中必然会遇到种种问题,可以逐步提升你的调试能力,分析问题的能力,掌握更多的调试手段。
遇到棘手的问题,我们还有专门的导师1V1答疑解惑,给出具体建议。
项目池里面的项目,是导师团队花费大量时间完成的,不仅有完整的代码及清晰的注释还有详细的文档和视频,并且有专门的项目导师答疑,完全不用担心自己学不会。
相信我,这些项目绝对能够让你进步巨大!下面是其中某三个项目的说明文档
回复

使用道具 举报

发表回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则


联系客服 关注微信 下载APP 返回顶部 返回列表