学懂C++(五十):深入详解 C++ 陷阱:对象切片(Object Slicing)

2024-09-03 04:44

本文主要是介绍学懂C++(五十):深入详解 C++ 陷阱:对象切片(Object Slicing),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

对象切片基本概念

什么是对象切片?

对象切片(Object Slicing)是 C++ 中的一个常见陷阱,发生在将派生类对象赋值给基类对象时。由于基类对象无法存储派生类特有的数据和行为,因此派生类对象的特有部分会被“切掉”,只保留基类部分。

为什么会发生对象切片?

        对象切片发生的原因在于 C++ 的赋值操作是基于值语义的。当你将一个派生类对象赋值给一个基类对象时,实际上是创建了一个新的基类对象,而这个新的基类对象只能包含基类的成员变量和方法。因此,派生类对象的特有部分(即派生类独有的数据和方法)就会被丢弃,这就是对象切片。

示例代码

让我们通过一个具体的例子来深入理解对象切片。

#include <iostream>//基类
class Base {
public:Base(int a): a_(a) {}virtual void show() const {std::cout << "Base class, a: " << a_ << std::endl;}
private:int a_;
};//派生类
class Derived : public Base {
public:Derived(int a, int b): Base(a), b_(b) {}virtual void show() const override {Base::show();std::cout << "Derived class, b: " << b_ << std::endl;}
private:int b_;
};int main() {Derived d(5, 10);Base b = d; // 对象切片,b_ 被“切掉”b.show();   // 只调用 Base 类的 show(),不会显示 b_return 0;
}

 运行结果

Base class, a: 5

如上所示,派生类的 show() 方法没有被调用,派生类对象的成员变量 b_ 也没有被显示。这是因为 bBase 类型的对象,无法存储 Derived 类型的特有部分。对象切片发生在将派生类对象 d 赋值给基类对象 b 时,导致派生类对象的特有数据 b_ 被丢弃。

如何避免对象切片?

有多种方法可以避免对象切片,以下是几种常见的方法。

1. 使用指针或引用

使用指针或引用可以避免对象切片,因为它们不会创建基类对象的副本。这种方法确保派生类的特有部分不会被“切掉”。

int main() {Derived d(5, 10);Base* pb = &d; // 使用指针pb->show();   // 调用 Derived 类的 show() 方法Base& rb = d; // 使用引用rb.show();    // 调用 Derived 类的 show() 方法return 0;
}
运行结果
Base class, a: 5
Derived class, b: 10
Base class, a: 5
Derived class, b: 10

在这段代码中,pbrb 分别是指向 d 的指针和引用。通过它们调用 show() 方法时,会根据虚函数机制动态绑定到 Derived 类的 show() 方法。

2. 使用多态和虚函数

确保基类中的方法是虚函数,这样即使通过基类指针或引用调用方法,也能动态绑定到派生类的实现。注意:虚函数不能避免对象切片,只能保证在使用指针或引用时实现多态行为。

class Base {
public:Base(int a): a_(a) {}virtual void show() const {  // 使用 virtual 关键字std::cout << "Base class, a: " << a_ << std::endl;}virtual ~Base() {} // 确保基类有虚析构函数
private:int a_;
};class Derived : public Base {
public:Derived(int a, int b): Base(a), b_(b) {}virtual void show() const override {Base::show();std::cout << "Derived class, b: " << b_ << std::endl;}
private:int b_;
};int main() {Derived d(5, 10);Base b = d; // 对象切片仍然发生b.show();   // 只调用 Base 类的 show(),不会显示 b_Base* pb = &d; // 使用指针pb->show();   // 调用 Derived 类的 show() 方法Base& rb = d; // 使用引用rb.show();    // 调用 Derived 类的 show() 方法return 0;
}
 运行结果
Base class, a: 5
Base class, a: 5
Derived class, b: 10
Base class, a: 5
Derived class, b: 10

这里,第一行结果来自于对象切片操作,仅调用了 Base 类的 show() 方法,而没有显示派生类的特有数据 b_。后续使用指针或引用来调用 show() 方法时,动态多态性确保了调用 Derived 类的 show() 方法。

3. 使用智能指针

使用智能指针(如 std::unique_ptrstd::shared_ptr)来管理动态分配的对象,可以有效避免对象切片,同时更好地管理内存。

#include <memory>
#include <iostream>class Base {
public:Base(int a) : a_(a) {}virtual void show() const {std::cout << "Base class, a: " << a_ << std::endl;}
private:int a_;
};class Derived : public Base {
public:Derived(int a, int b) : Base(a), b_(b) {}virtual void show() const override {Base::show();std::cout << "Derived class, b: " << b_ << std::endl;}
private:int b_;
};int main() {std::unique_ptr<Base> pb = std::make_unique<Derived>(5, 10); // 使用智能指针管理动态分配的对象pb->show(); // 调用 Derived 类的 show() 方法std::shared_ptr<Base> spb = std::make_shared<Derived>(5, 10); // 使用智能指针spb->show(); // 调用 Derived 类的 show() 方法return 0;
}
运行结果
Base class, a: 5
Derived class, b: 10
Base class, a: 5
Derived class, b: 10

此例中,pbspb 是指向 Derived 类型对象的智能指针,因此调用了 Derived 类的 show() 方法,避免了对象切片。

总结

        对象切片是 C++ 中一个常见而隐蔽的陷阱,发生在将派生类对象按值赋给基类对象时。由于基类对象无法存储派生类的特有部分,这些特有数据和行为将会被丢弃。要避免对象切片,可以使用指针或引用来管理对象,或者使用智能指针来处理动态分配的对象。此外,通过使用虚函数,可以确保基类指针或引用的多态性行为,但这并不能避免对象切片本身。

        理解和避免对象切片是编写高质量 C++ 代码的关键。掌握这些技术和概念不仅可以提高代码的健壮性,还能使程序更具有扩展性和维护性,避免由于对象切片带来的各种潜在问题。

这篇关于学懂C++(五十):深入详解 C++ 陷阱:对象切片(Object Slicing)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1132060

相关文章

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

mac中资源库在哪? macOS资源库文件夹详解

《mac中资源库在哪?macOS资源库文件夹详解》经常使用Mac电脑的用户会发现,找不到Mac电脑的资源库,我们怎么打开资源库并使用呢?下面我们就来看看macOS资源库文件夹详解... 在 MACOS 系统中,「资源库」文件夹是用来存放操作系统和 App 设置的核心位置。虽然平时我们很少直接跟它打交道,但了

关于Maven中pom.xml文件配置详解

《关于Maven中pom.xml文件配置详解》pom.xml是Maven项目的核心配置文件,它描述了项目的结构、依赖关系、构建配置等信息,通过合理配置pom.xml,可以提高项目的可维护性和构建效率... 目录1. POM文件的基本结构1.1 项目基本信息2. 项目属性2.1 引用属性3. 项目依赖4. 构

Rust 数据类型详解

《Rust数据类型详解》本文介绍了Rust编程语言中的标量类型和复合类型,标量类型包括整数、浮点数、布尔和字符,而复合类型则包括元组和数组,标量类型用于表示单个值,具有不同的表示和范围,本文介绍的非... 目录一、标量类型(Scalar Types)1. 整数类型(Integer Types)1.1 整数字

Spring常见错误之Web嵌套对象校验失效解决办法

《Spring常见错误之Web嵌套对象校验失效解决办法》:本文主要介绍Spring常见错误之Web嵌套对象校验失效解决的相关资料,通过在Phone对象上添加@Valid注解,问题得以解决,需要的朋... 目录问题复现案例解析问题修正总结  问题复现当开发一个学籍管理系统时,我们会提供了一个 API 接口去

Java操作ElasticSearch的实例详解

《Java操作ElasticSearch的实例详解》Elasticsearch是一个分布式的搜索和分析引擎,广泛用于全文搜索、日志分析等场景,本文将介绍如何在Java应用中使用Elastics... 目录简介环境准备1. 安装 Elasticsearch2. 添加依赖连接 Elasticsearch1. 创

深入理解C语言的void*

《深入理解C语言的void*》本文主要介绍了C语言的void*,包括它的任意性、编译器对void*的类型检查以及需要显式类型转换的规则,具有一定的参考价值,感兴趣的可以了解一下... 目录一、void* 的类型任意性二、编译器对 void* 的类型检查三、需要显式类型转换占用的字节四、总结一、void* 的

Redis缓存问题与缓存更新机制详解

《Redis缓存问题与缓存更新机制详解》本文主要介绍了缓存问题及其解决方案,包括缓存穿透、缓存击穿、缓存雪崩等问题的成因以及相应的预防和解决方法,同时,还详细探讨了缓存更新机制,包括不同情况下的缓存更... 目录一、缓存问题1.1 缓存穿透1.1.1 问题来源1.1.2 解决方案1.2 缓存击穿1.2.1

PyTorch使用教程之Tensor包详解

《PyTorch使用教程之Tensor包详解》这篇文章介绍了PyTorch中的张量(Tensor)数据结构,包括张量的数据类型、初始化、常用操作、属性等,张量是PyTorch框架中的核心数据结构,支持... 目录1、张量Tensor2、数据类型3、初始化(构造张量)4、常用操作5、常用属性5.1 存储(st

Java如何通过反射机制获取数据类对象的属性及方法

《Java如何通过反射机制获取数据类对象的属性及方法》文章介绍了如何使用Java反射机制获取类对象的所有属性及其对应的get、set方法,以及如何通过反射机制实现类对象的实例化,感兴趣的朋友跟随小编一... 目录一、通过反射机制获取类对象的所有属性以及相应的get、set方法1.遍历类对象的所有属性2.获取