深挖C++知识点

智能指针

auto_ptr指针的问题

auto_ptr采用copy语义来转移指针资源,转移指针资源的所有权的同时将原指针置为NULL,这跟通常理解的copy行为是不一致的(不会修改原数据),而这样的行为在有些场合下不是我们希望看到的。

vector<auto_ptr<People>> peoples;
// 这里实例化多个people并保存到数组中
auto_ptr<People> one = peoples[5];
cout << peoples[5]->get_name() << endl; 

这行代码会将peoples[5]中的指针所有权转移了!即该变量中的指针已经为null了。后续对解引用是不正确的。

shared_ptr的线程安全问题

  1. shared_ptr线程安全问题
    1)指针指向对象的线程安全:需要注意的是指针本身的安全就是计数的安全性;而这里指的是对象的安全,所以使用的时候为了保证对象的安全还是需要使用锁。
    2)引用计数的安全:
    https://blog.csdn.net/D_Guco/article/details/80155323
    shared_ptr本身的线程不安全主要来自于引用计数有并发更新的风险。
    • 引用计数的本质:其实引用计数本身是使用指针实现的,也就是将计数变量存储在堆上,所以共享指针的shared_ptr 就存储一个指向堆内存的指针。
    • 线程安全的解决方案:引用计数的线程安全可以使用互斥锁或者原子操作实现。
      class shared_ptr{
      public:
           //引用计数安全的锁实现
          //是一个指向堆区的指针,采用new开辟
          int* ref_count;
          void use_count_add() {
              mutex_->lock();
              ++(*ref_count_);
              mutex_->unlock();
          }
          //引用计数安全原子操作实现
          atomic<int> ref_count;
          void use_count_add() {
              refCount.fetch_add(1, memory_order_relaxed);
          }
      };
    

shared_ptr的double free问题

造成double free的原因:使用同一个裸指针创建多个shared_ptr,而没有使用make_shared工厂函数,从而导致多个独立的引用计数。

int* p = new int(5);
shared_ptr<int> p1(p);
//shared_ptr<int> p2(p);//使用两个裸指针的时候会产生这个问题,
shared_ptr<int> p2(p1);//这样才没有问题。或者每次使用make_shared函数
cout << p1.use_count() << " " << p2.use_count() << endl;

unique_ptr的裸指针问题

#include<bits/stdc++.h>
using namespace std;
int main() {
    int* p = new int(5);
    unique_ptr<int> p1(p);
    unique_ptr<int> p2(p); 
    cout << *p1 << " " << *p2 << endl;
    return 0;
}

malloc\free与new\delete底层

https://csguide.cn/cpp/memory/malloc_free.html#malloc实现方案

malloc\free

  1. malloc的申请方式:
    1)brk:从堆区分配内存;

    2)mmap()系统调用的方式,从文件映射区偷取一块内存
  2. 具体的申请方式是怎么确定的。
    malloc() 源码里默认定义了一个阈值:
    1)如果用户分配的内存小于 128 KB,则通过 brk() 申请内存;
    2)如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存;
    注意,不同的 glibc 版本定义的阈值也是不同的。
  3. free释放内存之后会还给操作系统吗?
    1)malloc 通过 brk() 方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用;
    2)malloc 通过 mmap() 方式申请的内存,free 释放内存的时候,会把内存归还给操作系统,内存得到真正的释放;
  4. 为什么不直接都使用brk或者mmap呢?
    1)如果一直使用brk模式的话,那么在频繁申请小空间的时候,由于brk模式是暂时不会将内存归还给操作系统的,所以会造成许多零碎的空间。
    2)如果全部采用mmap模式的话,那么频繁的调用系统调用会导致内核态和用户态的切换,会导致程序的效率变低,还会发生缺页中断,cpu的消耗会变大。
  5. 为什么分配内存的时候明明是malloc(1)会分配更大的内存
    如果采用的是brk模式,频繁的申请和释放会导致很多碎片空间,一旦这些碎片空间太小,要重复利用就会很难,所以malloc(1)才会采用每次分配更大的空间。
  6. free怎么知道释放的空间的大小
  7. 缺页中断:
    缺页中断是操作系统中的一种异常情况,当程序访问的页面(页)不在主存(内存)中时,就会发生缺页中断。具体来说,当程序引用的虚拟地址对应的页面不在物理内存中,而在磁盘上或者其他存储介质中时,操作系统需要将缺失的页面加载到内存中,然后再继续执行程序。
    1)操作系统检测到缺页中断,并停止当前程序的执行。
    2)操作系统通过页表等数据结构确定缺失页面的位置,即在存储介质中的位置。
    3)操作系统将缺失页面从磁盘或其他存储介质中加载到内存中的空闲位置。
    4)更新页表,将虚拟地址与新加载页面的物理地址进行映射。
    5)恢复程序的执行,使其重新访问缺失页面,这次应该可以找到并继续执行了。

new的底层

  1. new的底层用全局函数operator new来申请空间,注意这个operator new不是操作符重载,里面是实现了malloc+异常处理机制。
  2. 在空间申请之后调用构造函数实现空间中的值的初始化。

delete底层

  1. 调用析构函数,使得所有对象的内存能够正确释放。
  2. delete通过使用全局函数operator delete函数实现释放空间,注意operator delete不是操作符重载,内部是通过封装free实现的。

operator new(操作符重载)

https://blog.csdn.net/migu123/article/details/129049539?spm=1001.2014.3001.5501

  1. operator new可以实现在栈区和全局区申请空间。如何写一个类A的operator new和operator delete成员函数来取代系统的operator new和operator delete函数。特别说明一下:因为new和delete本身称为关键字或者操作符,所以类A中的operator new和operator delete叫作重载operator new和operator delete操作符,但是这里将重载后的operator new和operator delete称为成员函数也没问题,这方面并没有太严格的规定。

  2. 类实现自定义的operator new代码:

class A
{
public:
	static void* operator new(size_t size);		//应该为静态函数,但不写static似乎也行,估计是编译器内部有处理,因为new一个对象时还没对象呢,静态成员函数跟着类走,和对象无关
	static void operator delete(void* phead);
};
void* A::operator new(size_t size)
{
	cout << "A::operator new被调用了" << endl;
	A* ppoint = (A*)malloc(size);
	return ppoint;
}
void A::operator delete(void* phead)
{
	cout << "A::operator delete被调用了" << endl;
	free(phead);
}

注意如果是调用的类内写的自定的new和delete函数,调用的时候就是正常的new和delete,如果不想用自己定义的,应该:

A * pa = ::new A();
::delete pa;
  1. 实现全局的new和delete函数的重载
    需要注意的是这种重载的影响面很广,会影响整个new的变化,所以2中的针对某个类的重载比较常见。
void* operator new(size_t size) //重载全局operator new
{
	return malloc(size);
}
void* operator new[](size_t size)//重载全局operator new[]
{
	return malloc(size);
}
void operator delete(void* phead)//重载全局operator delete
{
	free(phead);
}
void operator delete[](void* phead)//重载全局operator delete[]
{
	free(phead);
}
class A
{
public:
	A() //构造函数
	{
		cout << "A::A()" << endl;
	}
	~A() //析构函数
	{
		cout << "A::~A()" << endl;
	}
}
int main()
{
	int* pint = new int(12);   //调用重载的operator new
	delete pint;               //调用重载的operator delete
	char* parr = new char[10]; //调用重载的operator new[]
	delete[] parr;             //调用重载的operator delete[]   	
	A* p = new A();            //调用重载的operator new ,之后也执行了类A的构造函数
	delete p;                  //执行了类A的析构函数,之后也调用了重载的operator delete
	A* pa = new A[3]();        //调用一次重载的operator new[],之后执行了三次类A的构造函数
	delete[] pa;               //执行了三次类A的析构函数,之后也调用了重载的operator delete[]  
}
  1. new的种类
    有三种,plainnew, nothrownew和placenew,其中plainnew就是我们通常说的new,nothrownew是不会抛出错误的new,错误返回NULL,重点是placenew(定位new)。定位new也和传统new处于同一个层次,但定位new的功能却是:在已经分配的原始内存中初始化一个对象。请注意这句话的两个重要描述点:
    1) 已经分配:意味着定位new并不分配内存,也就是使用定位new之前内存必须先分配好。
    2) 初始化一个对象,也就是初始化这个对象的内存,可以理解成其实就是调用对象的构造函数。
    placenew的使用:
#include <iostream>
#include <string>
using namespace std;
class ADT{
	int i;
	int j;
public:
	ADT(){
		i = 10;
		j = 100;
		cout << "ADT construct i=" << i << "j="<<j <<endl;
	}
	~ADT(){
		cout << "ADT destruct" << endl;
	}
};
int main()
{
	char *p = new(nothrow) char[sizeof ADT + 1];
	if (p == NULL) {
		cout << "alloc failed" << endl;
	}
	ADT *q = new(p) ADT;  //placement new:不必担心失败,只要p所指对象的的空间足够ADT创建即可
	//delete q;//错误!不能在此处调用delete q;
	q->ADT::~ADT();//显示调用析构函数
	delete[] p;
	return 0;
}
//输出结果:
//ADT construct i=10j=100
//ADT destruct
  1. operator new实现在栈区和全局区申请空间
    https://blog.csdn.net/happyjacob/article/details/104766843
  1. 在全局区分配空间
// 52-1.cpp
#include <iostream>
#include <string>
using namespace std;

class Test
{
    static const int MAX = 4;	// 最多存放的对象数量
    static char buffer[];					// 对象的存储空间
    static char used[];					// 每个存储对象位置是否空间,0表示空间可用
    //int m_value;
public:
    void* operator new (size_t size)
    {
        void* ret = NULL;
        for (int i = 0; i < MAX; i++)		// 找一个空位放对象
        {
            if ( !used[i] )
            {
                used[i] = 1;
                ret = buffer + i * sizeof(Test);
                cout << "succeed to allocate memory: " << ret << endl;
                break;
            }
        }
        return ret;
    }
    void operator delete(void* p)
    {
        if (p != NULL)
        {
            char* mem = reinterpret_cast<char*>(p);
            int index = (mem - buffer) / sizeof(Test);	// 第几个位置
            int flag = (mem - buffer) % sizeof(Test);		// 查看给出的位置是否正确
            if (flag == 0 && 0 <= index && index < MAX)
            {
                used[index] = 0;							// 标志置0,表示空间可用
                cout << "succeed to free memory: " << p << endl;
            }
        }
    }
};

char Test::buffer[sizeof(Test) * Test::MAX] = { 0 };
char Test::used[Test::MAX] = { 0 };

int main(int argc, char* argv[])
{
    cout << "==== Test Single Object ====" << endl;
    Test* pt = new Test;
    delete pt;
    cout << "==== Test Object Array ====" << endl;
    Test* pa[5] = { 0 };
    for (int i = 0; i < 5; i++)
    {
        pa[i] = new Test;
        cout << "pa[" << i << "]" << pa[i] << endl;
    }
    for (int i = 0; i < 5; i++)
    {
        cout << "delete " << pa[i] << endl;
        delete pa[i];
    }
    return 0;
}
  1. 在栈区分配空间
// 52-2.cpp
#include <iostream>
#include <string>
using namespace std;

class Test
{
    static int MAX;
    static char* buffer;
    static char* used;
    int m_value;
public:
    static bool SetMemorySource(char* memory, unsigned int size)
    {
        bool ret = false;
        MAX = size / sizeof(Test);
        ret = MAX && (used = reinterpret_cast<char*>(calloc(MAX, sizeof(char))));
        if (ret)
        {
            buffer = memory;
        }
        else
        {
            free(used);
            used = NULL;
            buffer = NULL;
            MAX = 0;
        }
        return ret;
    }

    void* operator new (size_t size)
    {
        void* ret = NULL;
        if (MAX > 0)
        {
            for (int i = 0; i < MAX; i++)
            {
                if (!used[i])
                {
                    used[i] = 1;
                    ret = buffer + i * sizeof(Test);
                    cout << "succeed to allocate memory: " << ret << endl;
                    break;
                }
            }
        }
        else
        {
            ret = malloc(size);
        }
        return ret;
    }

    void operator delete(void* p)
    {
        if (p != NULL)
        {
            if (MAX > 0)
            {
                char* mem = reinterpret_cast<char*>(p);
                int index = (mem - buffer) / sizeof(Test);
                int flag = (mem - buffer) % sizeof(Test);
                if (flag == 0 && 0 <= index && index < MAX)
                {
                    used[index] = 0;
                    cout << "succeed to free memory: " << p << endl;
                }
            }
            else
            {
                free(p);
            }
        }
    }
};
int Test::MAX = 0;
char* Test::buffer = NULL;
char* Test::used = NULL;

int main(int argc, char* argv[])
{
    char buffer[12] = { 0 };
    Test::SetMemorySource(buffer, sizeof(buffer));
    cout << "==== Test Single Object ====" << endl;
    Test* pt = new Test;
    delete pt;
    cout << "==== Test Object Array ====" << endl;
    Test* pa[5] = { 0 };
    for (int i = 0; i < 5; i++)
    {
        pa[i] = new Test;
        cout << "pa[" << i << "]" << pa[i] << endl;
    }
    for (int i = 0; i < 5; i++)
    {
        cout << "delete " << pa[i] << endl;
        delete pa[i];
    }
    return 0;
}

多态的底层

虚表与虚指针

  1. 虚函数的调用过程,this->v_ptr->v_table->function_dir,即通过虚指针指向的虚表中记录的虚函数的地址,然后调用该函数。
    需要注意的是继承之后的虚表中的函数地址如果发生重写地址是会
    修改的,比如B重写了A的vfunc1()函数,那么该函数的地址由0x401ED0->0x401F80
  2. 虚指针的初始化是在构造函数调用的时候来初始化的,由编译器来初始化指向虚表,而this指针在编译期的时候就确定下来了。
  3. 下图中的B继承了A,其中vfunc1()函数就展示了重写的过程,func2()函数展示了函数的覆盖。
  4. 如果基类里面有虚函数,那么子类一定会有虚函数,也会有虚表
  5. B继承了A,那么B的内存中先后顺序应该是虚指针->A的成员变量->B的成员变量。

虚函数与纯虚函数

  1. 作用:
    定义为虚函数是为了允许用基类的指针来调用子类的这个函数。定义一个函数为纯虚函数,才代表函数没有被实现,要求子类必须重写它。
  2. 为什么要有纯虚函数?
    在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

静态函数为什么只能访问静态成员变量

https://blog.csdn.net/vonmax007/article/details/77151718

  1. 类的静态成员(变量和方法)属于类本身,在类加载的时候就会分配内存,可以通过类名直接去访问;非静态成员(变量和方法)属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,然后通过类的对象(实例)去访问。
  2. 在创建一个类的对象时,this指针就被初始化指向该对象。想要访问成员变量必须通过this,也就是在实例化对象之后,所以静态成员函数出来的时候还没有实例化对象,所以自然不能访问。

定位内存泄漏

C++ 内存泄漏-原因、避免、定位
https://blog.csdn.net/weixin_46645965/article/details/137088374?spm=1001.2014.3001.5506
在linux下使用Valgrind(瓦尔葛林德)来解决内存泄露的问题,具体的可以看csdn的链接
可以检测的问题有:
1)访问非法内存;
2)内存重叠错误;
3)内存泄漏问题;
4)非法释放内存;

gdb调试

单线程调试

https://www.cnblogs.com/hankers/archive/2012/12/07/2806836.html

命令 描述
backtrace(或bt) 查看各级函数调用及参数
finish 连续运行到当前函数返回为止,然后停下来等待命令
frame(或f) 帧编号 选择栈帧
info(或i) locals 查看当前栈帧局部变量的值
list(或l) 列出源代码,接着上次的位置往下列,每次列10行
list 行号 列出从第几行开始的源代码
list 函数名 列出某个函数的源代码
next(或n) 执行下一行语句
print(或p) 打印表达式的值,通过表达式可以修改变量的值或者调用函数
quit(或q) 退出gdb调试环境
set var 修改变量的值
start 开始执行程序,停在main函数第一行语句前面等待命令
step(或s) 执行下一行语句,如果有函数调用则进入到函数中

多线程调试

。。。。。。。。。。。。。。

仿函数以及其应用

仿函数相对于普通的函数,具有更加复杂的代码设计,然而仿函数也有其过人之处,它具有如下的优点:

  1. 拥有状态
    仿函数的能力超越了operator(),仿函数可以拥有成员函数和成员变量,这就意味着仿函数可以同时拥有状态不同的两个实体。一般函数则达不到这样的能力。
  2. 拥有自己的型别
    仿函数(Functor)拥有自己的类型指的是仿函数本身也是一个类型,具体来说,它是一个类或者结构体,具有函数调用操作符 operator() 的重载。
  3. 仿函数速度更快
    就template概念而言,由于很多细节在编译期就已经确定,传入一个仿函数,就可能活动更好的性能
template <typename Func>
void process_data(const std::vector<int>& data, Func func) {
    for (const auto& item : data) {
        func(item);  // 可能被编译器内联优化
    }
}

vector的扩容

https://blog.csdn.net/qq_44918090/article/details/120583540

vector的扩容过程

当向vector中插入元素时,如果元素有效个数size与空间容量capacity相等时,vector内部会触发扩容机制:

  1. 开辟新空间
  2. 拷贝元素。
  3. 释放旧空间。
    注意:每次扩容新空间不能太大,也不能太小,太大容易造成空间浪费,太小则会导致频繁扩容而影响程序效率。

以什么方式扩容

如果我们先扩容达到预期的大小之后进行vector的push_back()操作,那么效率会高很多。

  1. 以线性的形式扩容:

    可以看出我们预期的是O(1)的复杂度直接变成O(n),即O(1)->O(n)
  2. 以倍数的机制扩容:

    可以看出我们以倍数增长的方式时间复杂度会变成正常的O(1)

为什么是以1.5或者2的倍数扩容

主要是为了使得先前申请的空间可以直接用,不用另外申请空间

可以看到,每次扩容时,前面释放的空间都不能使用。比如:第4次扩容时,前2次空间已经释放,第3次空间还没有释放(开辟新空间、拷贝元素、释放旧空间),即前面释放的空间只有1 + 2 = 3。

使用1.5倍(k=1.5)扩容时,在几次扩展以后,可以重用之前的内存空间了。

空间配置器

malloc、calloc、realloc函数

void* malloc(unsigned int num_size);
int *p = malloc(20*sizeof(int));申请20个int类型的空间;

void* calloc(size_t n,size_t size);//alloc申请的空间的值初始化为0;
int *p = calloc(20, sizeof(int));

void realloc(void *p, size_t new_size);//用于扩充容量

为什么要有二级配置器

https://blog.csdn.net/qq_44824574/article/details/124001624
内存分配过程中需要考虑的问题:
1)小块内存带来的内存碎片问题;
2)小块内存频繁申请释放带来的性能问题
为了解决该类问题,STL设计了双层级配置器,也就是第一级配置器和第二级配置器。

配置器

1)第一级配置器直接使用malloc和free;

过程为:调用malloc,如果调用失败,看是不是有自己实现的oom_malloc函数,如果没有设置,则配置失败,设置了循环调用该函数直到成功为止。
2)第二级配置器则视情况采用不同的策略:

  • 当配置区块大于128bytes,将其视作足够大,便调用第一级配置器;
  • 当配置区块小于128bytes,将其视作过小,为降低额外负担,便采用内存池的管理方式

    来到这里的时候说明是小的碎片(小于128字节),采用内存池+链表的形式管理该内存,链表的长度为16,编号0-15,最小8字节,以8字节递增,最大128字节。
    此时需要分以下的场景:
    ** list中存在对应的数据块,比如32字节大小,那么直接将该数据块的第一块内存给它,然后链表指向下一块地址

    ** list中没有对应的数据块,比如申请72的大小,那么从内存池中分配一段内存到list中,然后将第一个配置给它,然后指向下一个内存块
    ** list中没有对应的数据块,内存池中也没有,调用malloc重新分配内存,分配时会多分配一倍的内存,把相应的内存挂到list下,剩余的放到内存池中。

动态库的链接过程

为什么需要动态库

  1. 静态库太浪费空间。

  2. 另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.a更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)

posted @ 2024-06-22 22:22  铜锣湾陈昊男  阅读(61)  评论(0)    收藏  举报