当使用STL容器去存放数据时,是存放对象合适,还是存放对象指针(对象地址)合适?

发布于:2025-08-19 ⋅ 阅读:(12) ⋅ 点赞:(0)

目录

1、概述

2、考察深拷贝与浅拷贝的经典笔试题

3、详细阐述

4、使用结论

5、对于一些频繁使用的通用C++类,如果类中包含指针成员变量,则可能需要添加对深拷贝的支持


C++软件异常排查从入门到精通系列教程(核心精品专栏,订阅量已达8000多个,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C/C++实战专栏(重点专栏,专栏文章已更新500多篇,订阅量已达6000多个,欢迎订阅,持续更新中...)https://blog.csdn.net/chenlycly/article/details/140824370C++ 软件开发从入门到实战(重点专栏,专栏文章已更新300多篇,欢迎订阅,持续更新中...)https://blog.csdn.net/chenlycly/category_12695902.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_2276111.html       当我们把STL容器当做列勒表去存放数据时,大家有没有想过,是选择存对象合适,还是选择存对象指针合适呢?最近我们这边的新人在开发新功能的过程中就遇到这样的疑惑,我根据自己的理解与代码经验给予了详细的解答,这里面会牵涉到浅拷贝和深拷贝的相关概念。本文把相关的内容与细节分享出来,以供大家借鉴或参考。

1、概述

       常用的STL容器有vector、list、map等,它们基于模板实现的方式,方便我们存放各种类型的数据,我们在项目代码中会频繁地使用这些容器。有了这些容器,一般不再需要我们自己去实现一些数据结构了,这些容器基本能满足日常开发的需要,给我们带来了很大的便利,有效提高了编码的效率。   

此外,STL还提供了操作这些容器的算法函数,使用这些高效的算法函数,比直接遍历容器的效率要高很多,对于这一点,可以查看我之前根据项目中的使用实例与实际体验撰写的文章:  

如何使用C++ STL标准模板库中的算法函数(附源码)https://blog.csdn.net/chenlycly/article/details/139591764C++调用STL算法函数有效提升STL列表的搜索速度(附源码)https://blog.csdn.net/chenlycly/article/details/123943134

       当我们使用STL容器去存放数据时,是存放对象合适,还是存放对象指针(对象地址)合适?即,存放的元素到底是选择对象还是对象指针呢?大家有没有考虑这个问题?或者是在项目中遇到过这样的疑惑呢?刚入门的新人可能会遇到这样的问题,但即便大概了解这个使用规则的有经验的老鸟,可能没有深入去考虑过这个问题!

       根据多年编写代码的经验和代码实例,我先来说一下结论。一般来说,如果存放的元素是基本数据类型或者结构体,则可以直接存放结构体对象;如果存放的元素是类对象,则一般选择存放类对象指针(存放类对象地址)。

对于容器而言,在将对象存放到容器中时,走的是值拷贝(对象数据成员内存中的内容拷贝),属于浅拷贝,不是深拷贝。如果要存放的对象中包含指针成员变量时,该指针变量要指向一段内存,则需要考虑深拷贝。

2、考察深拷贝与浅拷贝的经典笔试题

       要搞清楚本文的主题问题,需要先搞清楚深拷贝与浅拷贝的概念。有个很经典的笔试题:

题目中给出了字符串类String的声明,让实现该类中的这些成员函数,主要用来考察对浅拷贝与深拷贝的理解,很多公司都用过这个笔试题。关于这个笔试题的详细说明,可以查看我的文章:

【C++经典面试题】字符串类String的接口代码实现(重点考察对浅拷贝与深拷贝的理解)https://blog.csdn.net/chenlycly/article/details/140344262

3、详细阐述

       对于结构体,一般没有指针变量成员,对于存放字符串的字段,一般都会用数组去实现,比如如下的结构体:

// 参与讨论人员信息
typedef struct tagDiscuMember
{
    char achUserId[BUF_64_LEN + 1];              ///< 用户uuid
    char achNickName[BUF_128_LEN + 1];      ///< 用户昵称
    char achAvatarUrl[BUF_256_LEN + 1];        ///< 用户头像url

public:
    tagDiscuMember() { memset(this, 0, sizeof(tagDiscuMember)); }
}TDiscuMember, *LPTDiscuMember;

在结构体中,一般存放字符串的数组都定位成定长的buffer,是不包含指针成员变量的,在进行值拷贝(赋值),即进行memcpy的内存拷贝操作是没问题的,不涉及到深拷贝。所以结构体存放到STL容器中时,直接放结构体对象就可以了。

       但是对于C++类,类中包含指针成员变量是很常见的,比如我们在C++窗口类中可能会定义多个指针成员变量(包含的子窗口可能会被定义成指针变量),一旦C++类中包含指针成员变量时,就需要注意深拷贝的概念了。对于包含指针成员变量的C++类,在赋值时默认进行的是值拷贝(内存拷贝),如果没有实现支持深拷贝的赋值函数,则赋值后会出现两个类对象指向同一段内存的情况。当一个对象析构时将指向的内存销毁了,则另一个类对象中的指针变量就指向了一个已经释放的内存了,这样该指针变量就变成野指针了,访问该野指针则会出问题,出现内存访问违例引发异常。

       对于包含指针成员变量的C++类(比如自定义的窗口类),一般不需要添加对深拷贝的支持我们要从代码使用上不让依赖深拷贝的操作出现。比如我们将包含指针成员变量的C++类放置到stl容器中存放时,我们不要直接存C++类对象,而是存放C++对象地址。比如在IM软件中可能会打开多个聊天窗口,聊天窗口类名称为CChatDlg,我们为了管理这些打开的聊天窗口管理起来,会把这些聊天窗口类对象放到STL列表中保存下来,我们保存的是类对象地址:

vector<CChatDlg*> vtChatDlgList;

而不能直接存放CChatDlg聊天窗口对象:

vector<CChatDlg> vtChatDlgList;

       在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)

专栏1:该精品技术专栏的订阅量已达到10000多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,已经更新到210篇以上!欢迎订阅!)

C++软件调试与异常排查从入门到精通系列文章汇总https://blog.csdn.net/chenlycly/article/details/125529931

本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法详细讲述了C++软件的调试方法与手段详细介绍分析C++软件问题的常用分析工具,以图文并茂的方式给出具体的项目问题实战分析实例(详细讲述分析排查过程,很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!

考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!

专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到300篇以上!

专栏2:(本专栏涵盖了C++多方面的内容,是当前重点打造的专栏,订阅量已达8000多个,专栏文章已经更新到500多篇,持续更新中...)

C/C++实战进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html

以多年的开发实战为基础,总结并讲解一些的C/C++基础与项目实战进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域多个方面的内容,包括C++基础及编程要点(模版泛型编程、STL容器及算法函数的使用等)、数据结构与算法C++11及以上新特性(开源代码中可能会用到很多新特性(比如WebRTC开源库),日常编码中也会用到部分新特性,面试时也会频繁地涉及到,学习新特性很有必要)、常用C++开源库的介绍与使用(比如SQLite、libcurl、libwebsockets、libevent、jsoncpp/RapidJson、Redis、RabbitMQ、MongoDB、MQTT、ZooKeeper、OpenCV、FFmpeg、SDL、GStreamer、Live555、ReactOS等)、代码分享(调用系统API、使用开源库)、常用编程技术(动态库、多线程、多进程、数据库及网络编程等)、软件UI编程(Win32/duilib/QT/MFC)、C++软件调试技术(引发C++软件异常的常见原因分析与总结、排查C++软件异常的手段与方法、分析C++软件异常的基础知识、使用常用软件分析工具分析C++软件问题、多个项目实战问题分析案例分享等)、设计模式(单例模式、工厂模式、观察者模式、状态模式等)、网络基础知识与网络问题分析进阶内容(实战问题分析实例分享)等。本专栏的内容都是建立在项目实践的基础上,来源于项目实战,服务于项目实战,很有实战参考价值!

专栏3:  

C++常用软件分析工具从入门到精通案例集锦汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795

常用的C++软件辅助分析工具有SPY++、PE工具、Dependency Walker、GDIView、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro等,本专栏详细介绍如何使用这些工具去巧妙地分析和解决日常工作中遇到的问题,很有实战参考价值!

专栏4:   

VC++常用功能开发汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/124272585

将10多年C++开发实践中常用的功能,以高质量的代码展现出来。这些常用的高质量规范代码,可以直接拿到项目中使用,能有效地解决软件开发过程中遇到的问题。

专栏5: 

C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12695902.html

根据多年C++软件开发实践,详细地总结了C/C++软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。


4、使用结论

       所以,当我们要将对象存放到STL容器中时,有如下的结论:

1)如果存放的是结构体,结构体中一般只会包含数组buffer,不会包含指针成员变量,比如:

typedef struct tagDiscuMember
{
    char achUserId[BUF_64_LEN + 1];              ///< 用户uuid
    char achNickName[BUF_128_LEN + 1];      ///< 用户昵称
    char achAvatarUrl[BUF_256_LEN + 1];        ///< 用户头像url

public:
    tagDiscuMember() { memset(this, 0, sizeof(tagDiscuMember)); }
}TDiscuMember, *LPTDiscuMember;

vector<TDiscuMember> vtDiscuMemberList;

不会涉及到深拷贝的场景,所以直接将结构体对象存放到STL容器中

2)如果存放的是C++类对象,C++类中可能包含指针成员变量,可能会牵涉到深拷贝的场景,我们要尽量依赖深拷贝的使用场景规避掉(规避的办法是,直接使用对象指针,不直接使用类对象),直接存放C++对象地址,比如:

vector<CChatDlg*> vtChatDlgList;

5、对于一些频繁使用的通用C++类,如果类中包含指针成员变量,则可能需要添加对深拷贝的支持

       对于一些频繁使用的通用C++类,如果类中包含指针成员变量,则可能需要添加对深拷贝的支持。比如MFC中常用的处理字符串的CString类,会频繁出现赋值等依赖深拷贝的场景,所以直接实现了支持深拷贝的赋值等专用函数,比如如下的赋值函数的实现:

// CString对象赋值给另一个CString对象,为了不指向同一块内存,实现了支持深拷贝的赋值函数
const CString& CString::operator=(const CString& stringSrc)
{
    if (m_pchData != stringSrc.m_pchData)
    {
        if ((GetData()->nRefs < 0 && GetData() != _strDataNil) ||
            stringSrc.GetData()->nRefs < 0)
        {
            // actual copy necessary since one of the strings is locked
            AssignCopy(stringSrc.GetData()->nDataLength, stringSrc.m_pchData);
        }
        else
        {
            // can just copy references around
            Release();
            assert(stringSrc.GetData() != _strDataNil);
            // 指向同一块内存,然后计数加1
            // 此处指针赋值,和AssignCopy中的memcpy不同,AssignCopy中的memcpy不改变指针的
            // 指向,却改变指针指向的内存中的值,即修改了所有指向同一块内存的CString对象
            // 的值,当然AllocBeforeWrite会保证修改所有指向同一块内存的CString对象的值的
            // 情况不会发生
            m_pchData = stringSrc.m_pchData;
            InterlockedIncrement(&GetData()->nRefs);
        }
    }
    return *this;
}

上面只给出了一个支持深拷贝的赋值函数的实现,支持深拷贝会涉及到多个函数,此处就不一一列举了。CString字符串类的内部实现可能稍显复杂,支持深拷贝的实例,可以直接查看考察深拷贝与浅拷贝的经典面试题:

【C++经典面试题】字符串类String的接口代码实现(重点考察对浅拷贝与深拷贝的理解)https://blog.csdn.net/chenlycly/article/details/140344262

      对于不支持深拷贝的C++类,使用时直接去new对象即可,然后保存时使用C++类指针变量保存类对象地址即可。

6、最后

       对于本篇文章讲到的这个细节问题,新人在问到我时,我才下意识地去好好思考这个问题,思考后才给新人一个相对完整的解释。其实很多看似理所当然的问题,我们深入一点思考,可能可以发现一些不一样的细节和知识点,可以进一步的扩大自己的理解和认知!


网站公告

今日签到

点亮在社区的每一天
去签到