解决内存管理问题的最佳利器-valgrind

Valgrind是一个动态分析工具,能够自动检测许多内存管理问题、线程bug,  并且能够分析程序的状况。它内部支持多个工具集,包括内存错误检测器,线程错误检测器,缓存分析器、堆分析器等,默认使用的是内存检测器(memcheck),  它是使用最多的一个内存检测工具。当然,你也可以基于Valgrind自己建立新的工具。

Valgrind支持的平台有:x86/Linux、AMD/64Linux、PPC32/Linux、PPC64LE/Linux、S390X/Linux、ARM/Linux(supported since ARMv7)、ARM64/Linux、MIPS32/Linux、MIPS64/Linux、X86/Solaris、 AMD64/Solaris、 X86/illumos、 AMD64/illumos、X86/Darwin (10.10, 10.11)、 AMD64/Darwin (10.10, 10.11)、ARM/Android、ARM64/Android、 MIPS32/Android、X86/Android

Valgrind是开源免费的软件,基于GNU General Public License, version 2.

一、快速入门

Valgrind工具集中最受欢迎的是memcheck,  它满足大部分的场景。memcheck能够检测内存相关的错误,并且是采用C/C++编译的程序,程序运行过程中奔溃或者不可预料的行为都可以使用Valgrind中的memcheck来进行检测。

使用Valgrind前,采用-g选项编译程序,这样memcheck才能够提取到具体的行号信息,同时可以使用-O0优化选项,但是如果使用-O1选项,那么显示的行号信息可能就不准确;不推荐使用-O2选项,如果使用的话,memcheck偶尔上报不是真的存在的未初始化的错误信息

命令行一般的使用格式如下所示,–leak-check=yes是打开内存泄露的检测器,

valgrind --leak-check=yes myprog arg1 arg2

下面提供一个C++例子,该例子有内存泄露和访问不存在地址的两个错误

#include <string>

void f(void)
{
    int* x = new int[10](); 
    x[10] = 0; // 访问不存在地址       
} // 内存泄露,没有释放内存                   

int main(void)
{
    f();
    return 0;
}

错误信息描述如下,表示访问不存在地址,第一行“Invalid write of size 4”表明什么类型错误,写数据到内存中,而该内存是不应该访问的。1066表示进程id号。如果错误的堆栈信息显示不够显示,那么可以加上选项–num-callers,再加上层级数量,比如–num-callers=20。

==1066== Invalid write of size 4
==1066==    at 0x100000F55: f() (example_02.cpp:6)
==1066==    by 0x100000F83: main (example_02.cpp:11)
==1066==  Address 0x100dea808 is 0 bytes after a block of size 40 alloc'd
==1066==    at 0x1000AC086: malloc (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1066==    by 0x100179627: operator new(unsigned long) (in /usr/lib/libc++abi.dylib)
==1066==    by 0x100000F33: f() (example_02.cpp:5)
==1066==    by 0x100000F83: main (example_02.cpp:11)

内存泄露的错误信息提示描述如下, 它会告诉你内存分配的位置,但是它不能告诉你内存为什么泄露。

==1122== 40 bytes in 1 blocks are definitely lost in loss record 14 of 42
==1122==    at 0x1000AC086: malloc (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1122==    by 0x100179627: operator new(unsigned long) (in /usr/lib/libc++abi.dylib)
==1122==    by 0x100000F33: f() (example_02.cpp:5)
==1122==    by 0x100000F83: main (example_02.cpp:11)

一般有几种内存泄露的类型,比较重要的两种是definitely lost和possibly lost,definitely lost是确定内存泄露,需要修复它,possibly lost可能存在内存泄露,需要仔细确认。

==1122== LEAK SUMMARY:
==1122==    definitely lost: 40 bytes in 1 blocks
==1122==    indirectly lost: 0 bytes in 0 blocks
==1122==      possibly lost: 72 bytes in 3 blocks
==1122==    still reachable: 200 bytes in 6 blocks
==1122==         suppressed: 18,127 bytes in 153 blocks
==1122== Reachable blocks (those to which a pointer was found) are not shown.
==1122== To see them, rerun with: --leak-check=full --show-leak-kinds=all

另外memcheck比较经常会上报没有初始化值的信息,但是要定位到错误信息的根本原因是比较困难的,对此,可以添加参数–track-origins=yes来获取更多的信息,但是,这样会使得memcheck运行的更慢。

Conditional jump or move depends on uninitialised value(s)

二、memcheck的错误信息

memcheck是内存错误的检测器,他可以检测C/C++常见的下列错误问题

  1. 访问不应该访问的内存,例如堆溢出、栈溢出、访问已经释放的内存
  2. 使用没有定义的值,例如值没有初始化
  3. 不正确的释放堆内存,例如重复释放内存,malloc/new/new[] 和 free/delete/delete[]没有一一对应使用
  4. 使用memcpy函数,源地址和目的地址重叠
  5. 向内存分配函数中,传递一个不正确的参数,例如负数
  6. 内存泄露
  • 非法读写错误,例如读取已经释放内存的地址,为了获取更多的信息,可以加上–read-var-info=yes的选项
==1178== Invalid read of size 16
==1178==    at 0x101321A50: qstricmp(char const*, char const*) (in /Users/lizijun/Qt5.13.0/5.13.0/clang_64/lib/QtCore.framework/Versions/5/QtCore)
==1178==    by 0x101539A81: QTimerInfoList::activateTimers() (in /Users/lizijun/Qt5.13.0/5.13.0/clang_64/lib/QtCore.framework/Versions/5/QtCore)
  • 使用没有定义的值,例如定义了变量,但是没有初始化,如果信息不够详细,可以添加参数–track-origins=yes来获取更多的信息
#include <string>
#include <iostream>

int main(void)
{
    int i_number;
    std::cout << i_number << std::endl;
    return 0;
}
==1189== Conditional jump or move depends on uninitialised value(s)
==1189==    at 0x1003D83C5: __vfprintf (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1003FF058: __v2printf (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1003E434A: _vsnprintf (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1003E43A7: vsnprintf_l (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1003D53B2: snprintf_l (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1000D4D22: std::__1::num_put<char, std::__1::ostreambuf_iterator<char, std::__1::char_traits<char> > >::do_put(std::__1::ostreambuf_iterator<char, std::__1::char_traits<char> >, std::__1::ios_base&, char, long) const (in /usr/lib/libc++.1.dylib)
==1189==    by 0x1000C8F27: std::__1::basic_ostream<char, std::__1::char_traits<char> >::operator<<(int) (in /usr/lib/libc++.1.dylib)
==1189==    by 0x100000D0D: main (example_03.cpp:7)
  • 非法释放地址,例如重复释放内存
#include <string>
#include <iostream>

int main(void)
{
    char *p_data = new char[64]();
    delete []p_data;
    delete []p_data;
    return 0;
}
==1212== Invalid free() / delete / delete[] / realloc()
==1212==    at 0x1000AC463: free (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1212==    by 0x100000F7D: main (example_04.cpp:8)
==1212==  Address 0x100dea7e0 is 0 bytes inside a block of size 64 free'd
==1212==    at 0x1000AC463: free (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1212==    by 0x100000F62: main (example_04.cpp:7)
==1212==  Block was alloc'd at
==1212==    at 0x1000AC086: malloc (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1212==    by 0x100179627: operator new(unsigned long) (in /usr/lib/libc++abi.dylib)
==1212==    by 0x100000F2A: main (example_04.cpp:6)
  • 调用申请和释放内存的方法不匹配,例如malloc申请内存,但是使用delete来释放,对某些系统来说是不允许的,因此,为了保证程序健壮,使用malloc,那么对应使用free; 使用new,那么对应使用delete; 使用new [], 那么对应使用delete []。
Mismatched free() / delete / delete []

三、Valgrind调用QtCreator程序

mac系统通过QtCreator创建程序之后,也可以采用Valgrind在终端上检测QtCreator生成的程序。

首先进入QtCreator编译生成的文件目录

接着选择build开头的目录,右键弹出的列表选择“服务”->”新建位于文件夹位置的终端窗口”来启动终端,  终端输入如下所示的命令来使用Valgrind测试QtCreator编译生成的程序JQtTestStudy.app

四、局限性

  1. Memcheck并不完美,它也会出现误报,但是它有99%的准确性,对于它提示的信息我们应该警惕。
  2. memcheck不能检测每一种内存错误,比如它不能检测到对静态分配或堆栈上的数组的超出范围的读写,但是它还是能够检测出使得你程序奔溃的错误,例如段错误segmentation fault

    五、总结

程序开发过程中,可能会遇到崩溃的问题,如果代码量很多的时候,我们可能会使用gdb来查看coredump信息,但是有时候gdb的信息比较简单,没有更加详细的堆栈信息,那么就可以考虑使用Valgrind进行分析。最近,工作中遇到一个问题,程序运行过程中,会偶发崩溃问题,使用gdb查看coredump信息,显示是重复释放内存,但是堆栈信息很少,一直找不到位置,后来使用Valgrind来查看程序,仔细查看从Valgrind提供的堆栈信息,很快找到问题的位置,原因确实是重复释放内存。

温馨提示:Valgrind经常上报了很多错误提示信息,这个可能是同样一个地方调用了多次,所以,如果解决了一个地方的问题,错误提示信息就会全部消失,需要耐心仔细。

结合生产消费者模式实现异步日志功能

软件程序开发过程中,日志是诊断bug必不可少的功能,日志功能通常是将每条日志信息按照一定的格式写入指定的文件,但是,实时将日志信息写入文件,必定耗费时间,对于性能要求比较高的机器来说,可能是无法接受的,并且由于时间差问题可能会带来无法预料的问题。

基于上面的原因,解决方案是将日志信息临时存储内存,然后启动线程来将内存中的日志写入文件,因此,本文将结合生产消费者模式来实现异步写入日志的功能。

生产者消费者模式,顾名思义,就是生产者生成数据,消费者处理数据。首先,将通过例子来说明生产者消费者的模式,然后再介绍异步写入日志的功能,其功能代码虽然简单,但是对于日志功能要求场景不多的人来说,却是相当实用的。

一、生产者消费者模式

1、实现简单的生产消费者管理类,生产者即函数Product, 它首先加锁,然后将数据写入队列,最后通过条件变量来唤醒消费者来处理数据;消费者即函数Consume,  它首先加锁,调用条件变量的wait等待接受信号,如果接受到信号,那么从队列中取出数据然后处理,这里需要注意的是取出数据之后,可以提前解锁,以便生产者能够尽快处理数据,另外wait函数添加的匿名函数,它判断队列是否为空,如果不为空,才继续往下处理数据,如果为空,那么继续等待,   这样做的原因是wait返回有可能不是因为接受到生产者发送的信号。

#include <iostream>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <queue>

class JDataManager
{
public:
    //生产者
    void Product()
    {
        for(int i = 0; i < 5; i++)
        {
            std::unique_lock<std::mutex> lock(m_mutex);
            m_queue.push(string("hello"));
            m_condition.notify_one();
            std::cout << "Product  i =  " << i << std::endl;
        }
    }
    
    //消费者
    void Consume()
    {
        int i = 0;
        do
        {
            std::unique_lock<std::mutex> lock(m_mutex);
            m_condition.wait(lock, [&]{ return !m_queue.empty(); } );
            std::string str = m_queue.front();
            m_queue.pop();
            std::cout << "Consume i = " << i << std::endl;
            i++;
        }while(true);
    }
private:
    std::mutex m_mutex;
    std::queue<std::string> m_queue;
    std::condition_variable m_condition;
};

2、上面实现了生产消费者管理类,接下来测试下其运行效果,首先启动线程用于执行消费者函数, 休眠两秒,再启动第二个线程用于执行生产者函数

JDataManager data_manager;

std::thread consumer(&JDataManager::Consume, &data_manager);

std::this_thread::sleep_for(std::chrono::seconds(2));

std::thread productor(&JDataManager::Product, &data_manager);


if(productor.joinable())
{
    productor.join();
}
if(consumer.joinable())
{
    consumer.join();
}

3、最后运行打印的信息如下图所示,生产者生成的数据,消费者对应的提取出来。这先启动消费者,再启动生产者后,运行的效果是合理的。

4、上面是先启动消费者,再启动生产者,如果反过来呢,即先启动生产者,再启动消费者

std::thread productor(&JDataManager::Product, &data_manager);

std::this_thread::sleep_for(std::chrono::seconds(2));

std::thread consumer(&JDataManager::Consume, &data_manager);

5、最后运行打印的信息如下图所示,先启动生产者再启动消费者,消费者也能够正常处理生产者生成的数据。

二、异步日志功能

1、日志是程序中每个模块都会使用到的功能,所以,考虑采用单例模式来实现日志的基本框架。

/// 类定义
class JMyLog
{
public:
    ~JMyLog();
    static JMyLog* Instance(void);

private:
    JMyLog();
);

private:
    static JMyLog* m_pMyLog;
};


/// 类实现
JMyLog* JMyLog::m_pMyLog = nullptr;

JMyLog::JMyLog()
{
}


JMyLog::~JMyLog()
{
    if (m_pMyLog)
    {
        delete m_pMyLog;
        m_pMyLog = nullptr;
    }
}

JMyLog* JMyLog::Instance(void)
{
    if (m_pMyLog == nullptr)
    {
        m_pMyLog = new JMyLog();
    }
    return m_pMyLog;
}

2、实现将每条日志信息写入队列的函数接口,这个相当于生产者, 它负责将写入的每条日志写入队列,再通过条件变量通知消费者处理数据。

/// 类定义
class JMyLog
{
public:
    ~JMyLog();
    static JMyLog* Instance(void);
    // 每条日志信息写入队列
    void WriteLog(int iLogLevel, const std::string &strFileName, int iLineNum
                  , const std::string &strFunName, const char *pFmt, ...);
private:
    JMyLog();

    std::string GetSysTimeToMs();
    std::string GetFirstLog();
    std::string GetLevelInfo(int iLevel);
    std::string GetThreadId();

private:
    static JMyLog* m_pMyLog;
    std::mutex  m_mutex;
    std::deque<std::string> m_deque;
    int m_iLogLevel;
    std::condition_variable m_condVariable;
};


/// 函数实现
static const std::string LOG_DEBUG = "DEBUG";

void JMyLog::WriteLog(int iLogLevel, const std::string &strFileName, int iLineNum
              , const std::string &strFunName, const char *pFmt, ...)
{

    std::unique_lock<std::mutex> lock(m_mutex);
    va_list vaa;
    va_start(vaa, pFmt);
    char ac_logbuf[1024];
    std::memset(ac_logbuf, 0x00, sizeof(ac_logbuf));

    snprintf(ac_logbuf, sizeof (ac_logbuf) - 2, "[%s][%s:%d:%s][%s][%s]"
             , GetSysTimeToMs().c_str()
             , strFileName.c_str()
             , iLineNum
             , strFunName.c_str()
             , GetThreadId().c_str()
             , GetLevelInfo(iLogLevel).c_str());

    size_t ilog_len = strlen(ac_logbuf);
    vsnprintf(ac_logbuf + ilog_len, sizeof(ac_logbuf) - ilog_len -2, pFmt, vaa);
    ilog_len = strlen(ac_logbuf);
    ac_logbuf[ilog_len] = '\n';
    m_deque.push_back(ac_logbuf);
    va_end(vaa);
    m_condVariable.notify_one();
    lock.unlock();
}

std::string JMyLog::GetSysTimeToMs()
{
    time_t timep;
    struct timeb tb;

    time (&timep);
    char tmp[128] ={0};
    strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M:%S",localtime(&timep) );

    char tmp2[128] ={0};
    ftime(&tb);
    snprintf(tmp2,sizeof(tmp2),"%d",tb.millitm);

    std::ostringstream buffer;
    buffer << tmp << "." << tmp2 ;

    return buffer.str();
}

std::string JMyLog::GetLevelInfo(int iLevel)
{
    std::string str_level_info;
    switch (iLevel)
    {
        case E_LOG_DEBUG:
        {
            str_level_info = LOG_DEBUG;
            break;
        }
        default:
        {
            str_level_info = LOG_DEBUG;
            break;
        }
    }
    return str_level_info;
}

std::string JMyLog::GetThreadId()
{
    std::ostringstream thread_id;
    thread_id << std::hex << std::this_thread::get_id();
    return thread_id.str();
}

std::string JMyLog::GetFirstLog()
{
    std::string str = m_deque.front();
    m_deque.pop_front();
    return str;
}

3、上面实现了生产者的日志生成功能后,接下来就是实现消费者的日志处理功能,由于考虑的是异步的模式,所以,消费者需要在线程中运行。下面实现的消费者是从队列中取出数据,然后将日志信息打印到终端,后面将添加日志写入文件的功能。

/// 类定义
class JMyLog
{
public:
    ~JMyLog();
    static JMyLog* Instance(void);
    // 每条日志信息写入队列
    void WriteLog(int iLogLevel, const std::string &strFileName, int iLineNum
                  , const std::string &strFunName, const char *pFmt, ...);
private:
    JMyLog();

    std::string GetSysTimeToMs();
    std::string GetFirstLog();
    std::string GetLevelInfo(int iLevel);
    std::string GetThreadId();

    // 启动线程
    void StartThread();
    // 线程执行函数
    void ThreadExce();

private:
    static JMyLog* m_pMyLog;
    std::mutex  m_mutex;
    std::deque<std::string> m_deque;
    int m_iLogLevel;
    std::condition_variable m_condVariable;
};

/// 函数实现
void JMyLog::StartThread()
{
    std::thread thread_obj(&JMyLog::ThreadExce, this);
    thread_obj.detach();
}

void JMyLog::ThreadExce()
{
    while(true)
    {
        std::unique_lock<std::mutex> lock_log(m_mutex);
        m_condVariable.wait(lock_log, [&] { return !m_deque.empty();});
        std::string str = GetFirstLog();
        lock_log.unlock();
        if (!str.empty())
        {
            std::cout << str;
        }
        str.clear();
    }
}

4、为了用户更加方便的调用,我们定义了如下所示的宏

enum E_LOG_LEVEL
{
    E_LOG_DEBUG = 1,
};

#define MyLogD(pFmt, ...) \
    JMyLog::Instance()->WriteLog(E_LOG_DEBUG, __FILE__, __LINE__, __func__, pFmt, ##__VA_ARGS__);

5、测试代码如下所示,调用者按照类似printf的格式使用定义好的宏MyLogD

MyLogD("%s", "this is mylog1.");
MyLogD("%s", "this is mylog2.");

6、日志输出的格式如下图所示

[2019-11-24 16:27:28.950][../JQtTestStudy/debbugtest/jdebugcppattr.cpp:259:TestMyLog][0x7fffa4c16380][DEBUG]this is mylog1.
[2019-11-24 16:27:28.951][../JQtTestStudy/debbugtest/jdebugcppattr.cpp:260:TestMyLog][0x7fffa4c16380][DEBUG]this is mylog2.

7、 为了将日志信息写入文件,封装日志文件写入类,该写入类主要实现打开文件、关闭文件,日志写入文件以及刷新文件的四个函数。

#include <string>
#include <fstream>

/// 类定义
class JLogFileHandler
{
public:
    JLogFileHandler(const std::string &strFilePath);
    ~JLogFileHandler();

    void Open();
    void Close();
    void Write(const std::string &strInfo);
    void Flush();

private:
    void InitFilePath();

private:
    std::string m_strFilePath;
    std::ofstream m_outfstream;
};

/// 类实现
JLogFileHandler::JLogFileHandler(const std::string &strFilePath)
    : m_strFilePath(strFilePath)
{
    InitFilePath();
}


JLogFileHandler::~JLogFileHandler()
{

}

void JLogFileHandler::InitFilePath()
{

}

void JLogFileHandler::Open()
{
    if (!m_outfstream.is_open())
    {
        m_outfstream.open(m_strFilePath);
    }
}

void JLogFileHandler::Close()
{
    if (m_outfstream.is_open())
    {
        m_outfstream.close();
    }
}

void JLogFileHandler::Write(const std::string &strInfo)
{
    m_outfstream << strInfo;
}

void JLogFileHandler::Flush()
{
    m_outfstream.flush();
}

8、JMyLog类添加文件处理者对象,然后线程执行函数中将日志信息写入文件,并且执行刷新功能

/// 类定义
class JMyLog
{
public:
    ~JMyLog();
    static JMyLog* Instance(void);
    // 每条日志信息写入队列
    void WriteLog(int iLogLevel, const std::string &strFileName, int iLineNum
                  , const std::string &strFunName, const char *pFmt, ...);
private:
    JMyLog();

    std::string GetSysTimeToMs();
    std::string GetFirstLog();
    std::string GetLevelInfo(int iLevel);
    std::string GetThreadId();

    // 启动线程
    void StartThread();
    // 线程执行函数
    void ThreadExce();

    // 初始化文件
    void InitFile();

private:
    static JMyLog* m_pMyLog;
    std::mutex  m_mutex;
    std::deque<std::string> m_deque;
    int m_iLogLevel;
    std::condition_variable m_condVariable;
    std::shared_ptr<JLogFileHandler> m_FileHandler; // 文件处理者

};

/// 函数实现
JMyLog::JMyLog()
{
    InitFile();
    StartThread();
}

void JMyLog::ThreadExce()
{
    while(true)
    {
        std::unique_lock<std::mutex> lock_log(m_mutex);
        m_condVariable.wait(lock_log, [&] { return !m_deque.empty();});
        std::string str = GetFirstLog();
        lock_log.unlock();
        if (!str.empty())
        {
            std::cout << str;

            m_FileHandler->Write(str);
            m_FileHandler->Flush();
        }
        str.clear();
    }
}

void JMyLog::InitFile()
{
    std::string str_file = "../../../log/test.log";
    m_FileHandler = std::make_shared<JLogFileHandler>(str_file);
    m_FileHandler->Open();
}

9、再次运行测试代码,查看日志目录下生成了日志文件test.log,并且日志信息也成功写入到文件

三、总结

最后再来总结异步日志功能的实现步骤,首先采用单例模式实现日志的基本框架,接着实现日志生产者,即提供写入日志信息接口,然后再实现日志消费者,日志消费者运行在线程中,并且收到信号才开始处理数据,最后实现文件处理者,将日志信息写入文件。至此,结合生产者消费者模式的异步日志功能完成了。

C++容器中实用的查找功能

C++标准中std提供了几种容器,它们包括顺序容器,比如vector, list, deque, queue, stack等,关联容器 ,比如map, set等,其中使用频率比较高的容器是vecotor向量容器、map键值对容器,我们经常会使用这两个容器来存储数据,然后根据不同的场景来查找获取容器内的值。而本文接下来将说明从这两类容器中快速查找获取数据的方法。

一、vector容器查找功能

vector容器自身没有提供查找函数,这里借助标准模版库algorithm提供的find,  使用的时候需要包含该头文件。

1、首先定义vector容器变量,然后存入数据,接着遍历打印容器内的所有数据,最后调用algorithm提供的函数find从vector向量中查找数据,algorithm提供的函数find需要输入三个入参数,第一个参数是容器开始查找的迭代器变量,第二个变量是容器结束查找的迭代器变量,第三个参数是需要查找的数据。

#include <vector>
#include <algorithm>

// 定义vecotor,然后存入数据
std::vector<std::string> vec_str;
vec_str.push_back("abc");
vec_str.push_back("def");
vec_str.push_back("fhj");
vec_str.push_back("123");
vec_str.push_back("456");

// 遍历打印vector容器内的数据
std::vector<std::string>::iterator iter = vec_str.begin();
for(iter = vec_str.begin(); iter != vec_str.end(); iter++)
{
    LOG(INFO) << *iter;
}
LOG(INFO) << "======";

// 调用find函数查找,内容为“def”的信息
iter = find(vec_str.begin(),vec_str.end(), "def");
if (iter != vec_str.end())
{
    LOG(INFO) << "find info: " << *iter;
}
LOG(INFO) << "======";

2、运行程序,输出的内容如下图所示,容器内存在需要查找的数据,返回迭代器变量,我们根据迭代器变量输出数据内容

二、map容器查找功能

map容器自身提供了查找功能,同时它也支持使用标准模版库algorithm提供的find函数。

1、首先定义map容器变量,写入数据,再遍历输出容器内的数据,接着调用map容器自身提供的find函数来查找key为2的数据,返回迭代器变量,然后根据这个迭代器变量输出键值,接下来调用algorithm提供的find函数,需要注意的是第三个参数输入的是迭代器的取值,最后也是返回迭代器变量。

#include <map>
#include <algorithm> 

// 首先定义map容器变量,写入数据
std::map<int, std::string> map_str;
map_str[1] = "aa";
map_str[2] = "bb";
map_str[3] = "cc";

// 遍历输出容器内的所有内容
std::map<int, std::string>::iterator iter_map;
for(iter_map = map_str.begin(); iter_map != map_str.end(); iter_map++)
{
    LOG(INFO) << "key: " << iter_map->first <<" value "<< iter_map->second;
}
LOG(INFO) << "======";

// 调用map容器自身提供的find函数,查找key为2的数据
iter_map = map_str.find(2);
if (iter_map != map_str.end())
{
    LOG(INFO) << "key: " << iter_map->first <<" value: "<< iter_map->second;
}
LOG(INFO) << "======";

// 调用algorithm提供的find函数来查找数据,注意find的第三个参数输入的是迭代器的取值
std::map<int, std::string>::iterator iter_map_ret;
iter_map_ret = find(map_str.begin(), map_str.end(), *iter_map);
if (iter_map_ret != map_str.end())
{
    LOG(INFO) << "result, key: " << iter_map_ret->first <<" value: "<< iter_map_ret->second;
}

2、运行程序,输出的内容如下图所示

三、键自定义的map容器查找功能

map容器使用过程中,有时候为了程序的可维护性以及降低代码的复杂度,需要自定义类作为map的键,在这种场景下,上面的查找方法是否也能够生效呢?接下来让我们进行验证。

1、首先自定义类JKeyPair来作为map的key,  如果自定义对象要作为map的键,那么需要重载operator<运算符,而如果要使用algorithm中的find,需要重载operator==运算符

#include <iostream>

/// 类的定义
class JKeyPair
{
public:
    JKeyPair(const std::string &strName, int iIndex);
    ~JKeyPair();

    std::string GetName(void);
    int GetIndex(void);

    // 自定义map中的key,需要重载operator<运算符(一定要记得加上const, 否则调用出错)
    bool operator<(const JKeyPair &rhs) const;

    // 自定义map中的key,使用algorithm中的find,需要重载operator==运算符(一定要记得加上const, 否则调用出错)
    bool operator==(const JKeyPair& rhs) const;
private:
    std::string m_strName;
    int m_iIndex;
};


/// 类的实现
JKeyPair::JKeyPair(const std::string &strName, int iIndex)
    :m_strName(strName),m_iIndex(iIndex)
{}

JKeyPair::~JKeyPair()
{}

std::string JKeyPair::GetName(void)
{
    return m_strName;
}

int JKeyPair::GetIndex(void)
{
    return m_iIndex;
}

bool JKeyPair::operator<(const JKeyPair &rhs) const
{
    if (m_iIndex < rhs.m_iIndex)
    {
        return true;
    }
    else if ((m_iIndex ==  rhs.m_iIndex)
             && (m_strName < rhs.m_strName))
    {
        return true;
    }
    return false;
}


bool JKeyPair::operator==(const JKeyPair& rhs) const
{
    if ((m_iIndex ==  rhs.m_iIndex)
            && (m_strName == rhs.m_strName))
    {
        return true;
    }
    return false;
}

2、验证调用map自身提供的find函数,其测试代码如下图所示

// 定义map容器变量,key是自定义类型,然后写入数据
std::map<JKeyPair, std::string> map_info;
JKeyPair key_pair_1("one", 1);
map_info[key_pair_1] = "value_one";
JKeyPair key_pair_2("two", 2);
map_info[key_pair_2] = "value_two";
JKeyPair key_pair_3("three", 2);
map_info[key_pair_3] = "value_three";

// 循环遍历容器内数据
std::map<JKeyPair, std::string>::iterator iter;
for(iter = map_info.begin(); iter != map_info.end(); iter++)
{
    JKeyPair key_pair = iter->first;
    LOG(INFO) << "name : " << key_pair.GetName();
    LOG(INFO) << "index : " << key_pair.GetIndex();
    LOG(INFO) << "value : " << iter->second;
}
LOG(INFO) << "======";

// 通过调用map自身提供的函数find,来查找键为key_pair_2的数据
iter = map_info.find(key_pair_2);
if (iter != map_info.end())
{
    LOG(INFO) << "find, value : " << iter->second;
}

3、从运行的结果看,使用自定义对象作为key值,map容器提供的find函数能够正确运行,并且从实际操作过程中,可以得出,map容器提供的find函数,自定义对象不需要重载operator==运算符。

4、接着再验证algorithm中的find,从实际操作过程中,自定义对象需要重载operator==运算符,测试代码段如下图所示,其中find中第三个参数是上一步骤中调用map自身的find返回的迭代器变量

// 使用alogrithm提供的find来查找map容器的数据
std::map<JKeyPair, std::string>::iterator iter_map_key;
iter_map_key = std::find(map_info.begin(), map_info.end(), *iter);
if (iter_map_key != map_info.end())
{
    JKeyPair key_pair = iter_map_key->first;
    LOG(INFO) << "result, self key: " << key_pair.GetName()  << "/" << key_pair.GetIndex()
              <<" value: "<< iter_map_key->second;
}

5、运行后打印的结果信息看,能够正确调用algorithm中的find来查找map中的数据

四、总结

到这里,我们已经将容器vector,map的查找功能介绍完成。接下来梳理总结。vector容器自身没有提供查找函数,因此,需要调用algorithm中的find来快速查找数据。map容器自身既提供了查找函数,也支持使用algorithm中的find来快速查找数据。而自定义对象作为map容器的键的情况下,如果需要支持上面的场景,那么自定义类需要重载operator<和operator==运算符。

自制智能指针

一、智能指针的原理

智能指针就是封装了创建对象的指针,并且可以在对象过期的时候,让析构函数自动删除指向的内存。

二、代码实现

为了防止拷贝和赋值,我们将拷贝函数和赋值构造函数放到private私有区域内。智能指针的类模版的实现方式:

template <typename T>
class SmartPtr
{
public:
    // explicit防止隐式转换
    explicit SmartPtr(T *pObj);
    ~SmartPtr(void);

public:
    T* operator->();
    T* Get();
    void Reset(T* pObj);
    
private:
    // 防止拷贝和赋值
    SmartPtr(const SmartPtr &);
    SmartPtr & operator= (const SmartPtr &);
    
    T* m_pObj;
};

template<typename T>
SmartPtr<T>::SmartPtr(T *pObj):m_pObj(pObj)
{
    
}

template<typename T>
SmartPtr<T>::~SmartPtr(void)
{
    delete m_pObj;
    m_pObj = 0;
}

template<typename T>
T* SmartPtr<T>::operator->()
{
    return m_pObj;
}

template<typename T>
T* SmartPtr<T>::Get()
{
    return m_pObj;
}

template<typename T>
void SmartPtr<T>::Reset(T* pObj)
{
    delete m_pObj;
    m_pObj = m_pObj;
}

三、测试验证

为了验证智能指针在退出的时候是否会自动调用delete来释放动态创建的对象,我们定义实现ObjectA对象。具体的实现细节如下所示:

class ObjectA
{
public:
    // 构造函数
    ObjectA(int iNum);
    ObjectA();
    // 析构函数
    ~ObjectA();
 
private:
    int m_num;
};
ObjectA::ObjectA(int iNum):m_num(iNum)
{
    cout << "ObjectA constructor" << endl;
}

ObjectA::ObjectA()
{
    cout << "ObjectA constructor, no param" << endl;
}

ObjectA::~ObjectA()
{
    cout << "ObjectA destructor" << endl;
}

完成上面ObjectA对象的定义和实现后,接下来就开始进行验证,验证代码如下:

ObjectA *pobj = new ObjectA;
SmartPtr<ObjectA> smartPtr(pobj);

通过调试运行,最后输出的结果为:

ObjectA constructor, no param
ObjectA destructor

这说明智能指针调用自身的析构函数,而析构函数中又回去删除动态创建对象的内存。

四、注意事项

因为模版不是函数,不能单独编译,所以实现智能指针的模版类的时候,需要将所有模版信息放入头文件中,其他文件使用的时候,再包含该文件。

五、总结

智能指针的机制就是利用本地变量在退出函数之后会自动删除的特性。首先保存动态创建对象的指针,当本地变量的生命周期结束的时候,自动调用其析构函数,而析构函数则会释放保存的指针指向的内存,从而达到避免调用new而忘记delete导致出现内存泄露的问题。

字符串string操作效率的思考

平常开发的时候,我们使用字符串string进行各种操作的过程中,有时候为了清晰的逻辑或者操作的简便,经常喜欢将字符串拷贝到另一个字符串中,再进行各种操作,在数据量少或者操作次数比较少的情况下,一般对程序的效率是不会有太大的影响,但是当数数据量大或者操作次数大的情况下,情况会是怎样呢?话不多说,首先,给出两个分隔字符串的函数,一个是没有优化的,其中采用字符串的赋值,另一个经过了优化。

1> 没有优化的分隔字符串函数

// 按照指定的分隔符,分隔字符串
// 例如split_string_test("a|b|c","|", vec);
// 得到的结果vec = ["a","b","c"]
void Public::split_string_test(const string &src, const string &split, vector<string> &vec_ret)
{
    vec_ret.clear();
    string tmp = src;
    
    while(1)
    {
        // 注意:size_t是unsigned, 而ssize_t是有符号整型,这里必须使用ssize_t
        // 32位系统上 size_被定义为 typedef unsigned innt size_t;  ssize_t等价于int
        ssize_t iPos = tmp.find(split);
        if (iPos <= 0)
        {
            vec_ret.push_back(tmp);
            break;
        }
        vec_ret.push_back(tmp.substr(0,iPos));
        tmp = tmp.substr(iPos + split.size());
    }
}

2> 经过优化的分隔字符串函数

// 按照指定的分隔符,分隔字符串
// 例如split_string("a|b|c","|", vec);
// 得到的结果vec = ["a","b","c"]
void Public::split_string(const string &src, const string &split, vector<string> &vec_ret)
{
    vec_ret.clear();
    const string &tmp = src;
    ssize_t iPos_l = 0;
    ssize_t iPos_r = 0;
    while(1)
    {
        // 从iPos_l位置开始查找分隔符split
        iPos_r = tmp.find(split,iPos_l);
        if (iPos_r <= 0)
        {
            vec_ret.push_back(tmp.substr(iPos_l));
            break;
        }
        vec_ret.push_back(tmp.substr(iPos_l,iPos_r - iPos_l));
        iPos_l = iPos_r + split.size();
    }
}

从上面两个分隔字符串函数中,可以看出第一个分隔字符串函数将传入的源字符串拷贝到临时的变量中进行操作,而第二个分隔字符串函数通过引用和位置的索引的方式来操作源字符串。接下来,我们通过实验来看看两者之间在效率上有多大差异。

主函数输入如下所示的代码段来进行实验

int i = 0;
string src = "a|b|c";
string split = "|";
vector<string> vec_ret;
unsigned long long ull_start_time;
unsigned long long ull_end_time;
int icount = 100;

// 没有优化的函数
ull_start_time = Public::get_system_time();
for (i = 0; i < icount; i++)
{
    Public::split_string_test(src, split, vec_ret);
}
ull_end_time = Public::get_system_time();
cout << "[split_string_test] time: " << ull_end_time - ull_start_time << " ms" <<endl;
   
// 经过优化的函数
ull_start_time = Public::get_system_time();
for (i = 0; i < icount; i++)
{
    Public::split_string(src, split, vec_ret);
}
ull_end_time = Public::get_system_time();
cout << "[split_string] time: " << ull_end_time - ull_start_time << " ms" <<endl;

一、 正常情况

字符串src = “a|b|c”;设置操作的次数为icount = 100; 运行调试结果如下所示

[split_string_test] time: 0 ms
[split_string] time: 0 ms

从上面运行的结果看,两个函数基本上没有耗时,看不出任何差异

二、 操作次数增加的情况

字符串src = “a|b|c”;设置操作的次数为icount = 10000; 运行调试结果如下所示

[split_string_test] time: 10 ms
[split_string] time: 0 ms

从上面运行的结果看,操作次数增加的情况下,第一个函数比第二个函数多耗时10ms

三、数据量增加的情况

字符串src = “a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j”;设置操作的次数为icount = 100; 运行调试结果如下所示

[split_string_test] time: 20 ms
[split_string] time: 10 ms

从上面运行的结果看,数据量增加的情况下,第一个函数比第二个函数多耗时10ms

四、数据量增加,操作次数增加情况

字符串src = “a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j|a|b|c|d|e|f|g|h|i|j”;设置操作的次数为icount = 10000; 运行调试结果如下所示

[split_string_test] time: 1710 ms
[split_string] time: 600 ms

从上面运行的结果看,数据量增加,操作次数增加的情况下,第一个函数比第二个函数竟然多耗时1.11s, 这可是相当可观的数据。

五、总结

从上面实验结果看,字符串的赋值拷贝操作,数据量大、操作次数多的情况下,能够明显看出比较耗时的。因此,我们在进行字符串的操作的时候,尽量采用引用和指针的方式来提高效率。

 

两种隐式类型转换的详解

operator除了操作符的重载之外,还可以进行隐式类型的转换,这与构造函数的隐式类型转换是有区别的。本文将通过例子说明operator的隐式类型转换,再说明构造函数的隐式类型转换,最后再总结他们之间的区别。

一、 operator隐式类型转换

operator隐式类型转换是当前对象生成其他类型的对象。首先实现ObjectA对象,用于说明operator的隐式类型转换。

class ObjectA
{
public:
    // 构造函数
    ObjectA(int iNum);
    // 析构函数
    ~ObjectA();
    // operator的隐式类型转换,转换的类型是int 
    operator int();
    
private:
    int m_num;
};
ObjectA::ObjectA(int iNum):m_num(iNum)
{
    cout << "ObjectA constructor" << endl;
}

ObjectA::~ObjectA()
{
    cout << "ObjectA destructor" << endl;
}
    
ObjectA::operator int()
{
    cout << "operator int " << endl;
    return m_num;
}

1、主程序输入如下代码段

ObjectA objA(3);
cout << objA + 4 << endl;

2、 调试运行输出的结果看,ObjA虽然不是int类型,但是与整数类型4进行相加的时候,会调用到operator int函数,该函数返回的结果是int类型

ObjectA constructor   // 调用构造函数
operator int          // 调用隐式类型转换
7                     // 计算结果
ObjectA destructor    // 调用析构函数

二、构造函数的隐式类型转换

构造函数的隐式类型转换,是用其他类型来构造当前类型的临时对象。为了说明构造函数的隐式类型转换,首先先实现ObjectB对象。

class ObjectA;

class ObjectB
{
public:
    // 无参数构造函数
    ObjectB();
    // 参数类型为对象ObjectA
    ObjectB(ObjectA objA);
    // 析构函数
    ~ObjectB();
    // 打印函数,注意入参类型是ObjectB
    int print(ObjectB B);  
};
ObjectB::ObjectB()
{
    cout << "ObjectB constructor" << endl;
}

ObjectB::ObjectB(ObjectA objA)
{
    cout <<":ObjectB(ObjectA objA)" << endl;
}
    
ObjectB::~ObjectB()
{
    cout << "ObjectB destructor" << endl;
}
    
int ObjectB::print(ObjectB B)
{
    cout << "ObjectB print" << endl;
    return 0;
}

1、主程序中输入如下代码段

ObjectA objA(3);
cout << objA + 4 << endl;

cout << "====" << endl;
ObjectB objB;
objB.print(objA);   // 等价于 objB.print(ObjectB(objA));
cout << "====" << endl;

2、 调试运行输出结果看,print函数入参类型是ObjectB,  但是传递的类型是ObjectA,但是ObjectB有单一形参类型为ObjectA的构造函数,程序会隐式的将objA,转换为ObjectB(objA)

ObjectA constructor
operator int 
7
====
ObjectB constructor
:ObjectB(ObjectA objA)
ObjectB print
ObjectB destructor
ObjectA destructor
====
ObjectB destructor
ObjectA destructor

三、总结

1、operator隐式类型转换是当前对象生成其他类型的对象

2、构造函数的隐式类型转换,是用其他类型来构造当前类型的临时对象, 与operator隐式类型转换是相反的。另外,如果想禁用构造函数的隐式类型转换,那么构造函数前添加explicit (例如 explicit ObjectB(ObjectA objA); )

 

C++三种new操作符的详解

工作开发过程中,一般申请创建内存,使用的是new方法。但是new存在三种操作符,其含义和应用的场景都不同。这三种操作符分别是new operator, operator new, placement new,  本文将针对这三种操作符结合例子进行说明。最后再总结它们的区别和联系。

一、 new operator 操作符

new operator指的就是new操作符,它经历两个阶段的操作:(1)调用::operator new申请内存(operator new后面将进行详细说明,这里理解为C语言中的malloc),(2)  调用类的构造函数。

定义类Func,  用于后面的验证测试

class FUNC
{
public:
    FUNC();
    virtual ~FUNC();
};
#include "Func.hpp"
#include <iostream>

FUNC::FUNC()
{
    std::cout << __func__ << ": call constructor"  << std::endl;
}

FUNC::~FUNC()
{
    std::cout << __func__ << ": call destructor" << std::endl;
}

1、 new操作符的一般调用方法

std::string *pstr = new std::string("hello, world");
std::cout << *pstr << std::endl;
delete pstr;

2、 new操作符调用自定义类Func

FUNC *pfunc = new FUNC;
delete pfunc;

最后终端输出结果如下图所示, 可以看出调用new操作符会调用对象的构造函数,而调用delete操作符会调用对象的析构函数。

FUNC: call constructor
~FUNC: call destructor

3、 new操作符不能被重载

二、 operator new 操作符

operator new操作符单纯申请内存,并且是可以重载的函数。(注意:::operator new 和 ::operator delete前面加上::表示全局)

1、operator new操作符的一般调用方法

调用operator new申请内存,内存申请的大小为自定义类Func的大小,经过调试发现,并没有输出类Func的构造函数,也没有调用Func的析构函数

FUNC *pfunc2 = (FUNC *)::operator new(sizeof(FUNC));
::operator delete(pfunc2);

2、重载operator new操作符

1) 首先FUNC类中添加如下信息

void* operator new(size_t size);

void operator delete(void* ptr);
void* FUNC::operator new(size_t size)
{
    cout << "operator new" << endl;
    // 全局operator new
    return ::operator new(size);
}


void FUNC::operator delete(void* ptr)
{
    cout << "operator delete" << endl;
    // 全局operator delete
    ::operator delete(ptr);
}

2) 主程序中调用new创建FUNC对象,然后调用delete释放对象

FUNC *pfunc = new FUNC;
delete pfunc;

3) 运行调试之后的结果信息如下所示,new调用到重载的函数operator new, 同样的,delete也调用到重载的函数operator delete

operator new
FUNC: call constructor
~FUNC: call destructor
operator delete

3、重载operator new操作符的第二种版本

1)首先FUNC类中添加如下信息

void* operator new(size_t size, string str);
void* FUNC::operator new(size_t size, string str)
{
    cout << "operator new version 2: " << str << endl;
    return  ::operator new(size);
}

2)主程序中调用new创建FUNC对象,然后调用delete释放对象

FUNC *pfunc = new("this is my world") FUNC;
delete pfunc;

3)运行调试之后的结果信息如下所示,new调用到重载的函数operator new的第二个版本

operator new version 2: this is my world
FUNC: call constructor
~FUNC: call destructor
operator delete

三、 placement new操作符

placement new操作符是重载operator new的一个版本,该函数的执行忽略了size_t参数,只返还第二个参数,该函数允许在已经构建好的内存中创建对象

// placement new
void *operator new( size_t, void * p ) throw() { return p; }

// 调用格式
pi = new(ptr) int

1、placement new操作符的使用方法

// 提前申请创建内存
char *buf = new char[sizeof(FUNC)];
// 创建对象FUNC将对象指向已经创建好的内存地址,注意这里需要使用::, 否则可能会调用到重载的operator new
FUNC *pfunc = ::new(buf) FUNC;
// 这里使用使用变量pfunc调用类FUNC中的功能
// ...
// 使用完成调用析构函数
pfunc->~FUNC();
// 释放已经创建的内存
delete []buf;

2、 终端输出打印信息如下所示, 从中可以发现placement new会调用到对象的构造函数

FUNC: call constructor
~FUNC: call destructor

四、总结

从上面实验的结果,进行总结,具体如下:

1、 new operator即new操作符,不能被重载,调用的时候,先申请内存,再调用构造函数,这是常用的调用方式。

2、 operator new操作符,能够被重载,单纯申请内存,相当于C语言中的malloc, 如果重载了operator new操作符,又需要调用原来的函数,那么需要在操作符前面加上::(即 ::operator new),重载该操作符通常是为了实现不同的内存分配方式。

3、placement new操作符,仅仅返回已经申请好内存的指针,它通常应用在对效率要求高的场景下,提前申请好内存,能够节省申请内存过程中耗费的时间。

UML类图关系

看类图的时候,理清类与类之间的关系是很重要的。类的关系一般分为泛化,实现,依赖,一般,聚合,组合这六种关系。本文首先给出六种类关系示意图来帮助记忆!然后再分别进行介绍说明!希望能够能够帮助到大家!

一、类图的3个基本组件

类名、属性、方法。 而属性和方法,可以设置三种访问权限,+表示公共,#表示保护,-表示私有

二、 基本概念

1、类图的三大组件:类名、属性、方法。用来表示系统中的类,接口以及它们的静态结构和关系。

2、泛化关系:是一种继承的关系,是is-a关系,子类继承父类,箭头从子类指向父类。

3、实现关系:接口和实现的关系,箭头从实现类指向接口。

4、组合关系:整体与部分的关系,是contains-a的关系,是强的包含关系,部分不能离开整体而存在。实心菱形指向整体

5、聚合关系:整体与部分的关系,但是是较弱的关系,部分可以离开整体单独存在,空心菱形指向整体

6、关联关系:是一种拥有关系,可以是双向或者单向,双向关联有两个箭头,单向关联只有一个箭头。箭头指向被拥有者。

7、依赖关系:是一种使用关系,对象间最弱的一种关系,通过调用被依赖类的方法或者参数等来完成指责。箭头指向被依赖者。

三、六种关系的强弱顺序

泛化 = 实现 -> 组合 -> 聚合 -> 关联 ->依赖