1 c++程序编译过程
编译分为四个过程:编译(编译预处理、编译、优化),汇编,链接
编译预处理将代码中#开头的代码的字面量 进行判断条件或宏替换
编译优化将.cpp编译成.s的汇编代码
汇编将汇编代码.s翻译成机器指令.o
链接 .o文件无法执行,因为.cpp中的某个函数可能使用了另个.cpp中定义的符号(变量or函数)或者调用了某个库中的函数。链接的目的就是将这些文件与对应的目标文件连接成一个整体,从而生产可执行的程序.exe
其中链接分为两种
静态链接(.lib,.a):静态链接是将程序调用的库一起打包到可执行文件中,在可执行程序被执行时静态库中代码会被一同装入此运行进程的虚拟地址空间中,这样执行时就不需要调用别的库了,执行时直接调用速度快,但是链接的时候可能同一个库链接了好几次(多个程序都使用了一个静态库,则在这多个程序运行时,每个进程的地址空间中就都会存在这个静态库,但实际只需要一个静态库就够了),导致空间浪费,而且如果该库更新了的话,整个程序需要重新编译。
动态链接(.dll,.so):动态库的代码(或某个共享的目标文件)不会被包到可执行程序中,而是以.so or .dll文件的形式存在。程序链接时只在可执行程序中记录了动态库的一些信息,动态链接库在代码执行时动态链接库的全部内容会被映射到运行经常的对应虚拟地址空间中(且整个内存上只有一份,其他的进程需要这个动态库也时同样将动态库的内容映射到其虚拟地址空间中(注意映射不是拷贝,映射只是进程中记录让执行进程记录了动态库的地址,要调用时去相应地址调用)。节省内存、更新方便,但是每次执行可执行程序都需要先链接到动态库,相比静态链接会有一定的性能损失。
2 c++内存分区(虚拟内存空间)
c++内存分区:堆、栈、全局/静态存储区、常量存储区、代码区
Linux虚拟内存地址空间分配如下从低地址(下)到高地址(上)的分别是
.text段(代码区 存放代码,不允许修改但可以执行,编译后的可执行文件存放在这)
.rodata段(常量存储区,下图中没画出来,位置在.text上.data下,与.text同样属于ro(read only),存放常量不允许修改,程序运行结束自动释放)
.data段,.bss段(全局静态存储区 存放全局和静态变量,程序运行结束自动释放,被其中去哪句or静态变量被初始化了的放在.data段中,没被初始化的放在.bss中)
堆区 (存放用户通过malloc申请的内存块,由程序员控制其分配和释放,程序结束后os自动释放回收)
在堆和栈中间这部分空间存放的是未使用空间以及程序中动态链接库的内存映射
栈区存放函数局部变量,函数参数,返回地址由编译器自动分配和释放
env 下图没画出来,用于存放当前的环境变量
int g_var = 0; // g_var 在全局区(.data 段)
char *gp_var; // gp_var 在全局区(.bss 段)
int main()
{
int var; // var 在栈区
char *p_var; // p_var 在栈区
char arr[] = "abc"; // arr 为数组变量,存储在栈区;"abc"为字符串常量,存储在常量区
char *p_var1 = "123456"; // p_var1 在栈区;"123456"为字符串常量,存储在常量区
static int s_var = 0; // s_var 为静态变量,存在静态存储区(.data 段)
p_var = (char *)malloc(10); // 分配得来的 10 个字节的区域在堆区
free(p_var);
return 0;
}
3 堆和栈的区别
最大的区别
栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的。
申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片。
4 变量的区别
全局变量:外部链接性、静态持续变量。全局作用域,一个文件中定义,同属于一个程序的其他文件之中使用extern声明后也可使用。
静态全局变量:内部链接性、静态持续变量。文件作用域,不同文件之间不共享。
局部变量:无链接性、静态持续变量。局部作用域,函数体执行完被销毁,存放在栈区。函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回
静态局部变量:自动存储持续性的变量。它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。局部作用域,整个程序执行结束后被销毁。
静态变量和全局变量的区别:静态变量用 static 告知编译器,自己仅仅在变量的作用范围内可见。
如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能在头文件中定义全局变量,只能在同文件中extern声明全局变量。
ifndef只防止某与1个.c重复include同一头文件
不同.c去include同一头文件是可以的;如果这个头文件里定义了全局变量,每个include该头文件的.c都会生成各自的同名全局变量,导致重复定义
最好就是在cpp中定义全局变量,在.h中声明全局变量是extern的这样其他文件就能使用这个全局变量。
5 如何限制类对象只能在堆or栈上创建
c++对象的建立分为两种,静态建立和动态建立。静态建立就是A a;动态建立就是A* pa = new A();使用new关键字创建对象,new关键字会先调用operator new在堆上空间申请合适足够的内存,然后在这块内存上根据new对象时传入的参数调用对应构造函数构造相应的对象。
限制对象只能在堆上建立:
避免直接调用类的构造函数,因为静态建立对象时就是直接调用类的构造函数创建对象。但是直接将类的构造函数设为私有,这样不仅无法外部直接调用,而且new关键字中也会调用构造函数(也是外部调用) 也无法构造。
解决方法1 将析构函数设为私有
静态对象建立在栈上,由编译器分配和释放内存空间,编译器为对象分配空间时,会对类的非静态函数进行检查,即编译器会检查析构函数的访问性,当析构函数被设为私有时,编译器创建的对象就无法通过析访问构函数(析构函数为私有,只能在类内调用,无法在外部调用)来释放对象的内存空间,因此编译器就不会在栈上为对象分配内存而是直接报错。
class A
{
public:
A() {}
void destory()// 写一个公用的方法析构对象
{
delete this;//delete会调用析构函数
// 此时析构函数是私有函数 只能内部调用
// 而此函数就在内部 所以delete可以成功调用析构函数
// delete先析构再释放(free)对应的空间,
// 所以需要A对象是堆上变量,才可以用free释放空间
}
private:
~A()// 析构函数设为私有
{
}
};
但是这种修改导致这个类A无法被继承,A类作为父类就需要将析构函数设置成virtual,然后子类继承去重写析构函数来实现多态,但此时析构函数是私有的(只能自己类内访问,子类也无法访问),子类中无法访问。
优化解决将构造和析构函数都设置为protected(子类和自己类中可以访问)
首先将构造设置成protected,在我们主函数中不能调用构造函数在栈上或堆上创建对象,但是提供了公有create接口,限制了在外部(子类和自己外)只能在堆上创建对象,达到了我们的目的。并且因为是protected的子类中可以使用符类的构造函数,对于多态继承特性没有影响。 同时将析构也设置成protected,创建公有destory接口,对象的析构在外部只能通过这个接口实现,为是protected的子类中可以使用符类的析构函数,对于多态继承特性没有影响。
class A
{
protected:
A() {}
~A() {}
public:
static A *create()// 注意是静态函数,不需要对象就能调用,否则我们会陷入 想构造一个对象的前提是已经有一个对象的困境。
{
return new A();
}
void destory()
{
delete this;
}
};
限制对象只能建立在栈上
堆上对象的构造通过new关键词来实现,new关键词中又会调用类中定义(类中没定义就是调用全局默认的)operator new函数,当我们将类的operator new函数设置为私有,就无法在外部通过new 类名来创建一个堆上对象(编译会直接报错),防止了在堆上构造对象,但是对栈上构造对象没有影响。
class A
{
private:
void *operator new(size_t t) {} // 注意函数的第一个参数和返回值都是固定的
void operator delete(void *ptr) {} // 重载了 new 就需要重载 delete
public:
A() {}
~A() {}
};
6 内存对齐
linux下
long在64位下是8字节,32位下是4字节
指针也是64位下是8字节,32位下是4字节
在 64 位 Linux 下,结构体字段默认(在没有#pragma pack宏的情况下)按 8 字节对齐;32 位 Linux 下,默认 4 字节对齐
https://www.cnblogs.com/flyinggod/p/8343478.html
什么是内存对齐?可以理解为在内存中规整存放数据的方式。
内存对齐原则?针对普通变量,变量的首地址应该被其类型大小与对齐基数(pragma pack规定的对齐数)的较小者整除。针对结构体,结构体中各个成员按照它们被声明的顺序在内存中顺序存储。
1 保存结构体变量的首地址能够被其最宽基本类型成员大小与【#pragma pack指定的数值】中的较小者所整除;
2 结构体每个成员(除第一个成员变量 第一个成员变量地址由1约束)相对于结构体首地址的偏移量(offset 该成员首地址-结构体首地址)都是【该数据成员所占内存】与【#pragma pack指定的数值】中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding);
3结构体的总大小为结构体最宽基本类型成员大小与【#pragma pack指定的数值】中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节 (trailing padding)。
#pragma pack(4)
struct Test1
{
char c; // 起始地址0
short sh; // 成员变量与结构体变量首地址的偏移量2
int a; // 成员变量与结构体变量首地址的偏移量4
float f; // 成员变量与结构体变量首地址的偏移量8
int *p; // 成员变量与结构体变量首地址的偏移量12
char *s; // 成员变量与结构体变量首地址的偏移量16
double d; // 成员变量与结构体变量首地址的偏移量20
};
总共占28Bytes。 c的偏移量为0,占1个Byte。sh占2个Byte,它的对齐模数是2(2<4,取小者),存放起始地址应该是2的整数倍,因此c后填充1个空字符,sh的起始地址是2。a占4个Byte,对齐模数是4,因此接在sh后存放即可,偏移量为4。f占4个字节,对齐模数是4,存放地址是4的整数倍,起始地址是8。p,s的起始地址分别是12,16。d占8个字节,对齐模数是4(4<8),d从偏移地址为20处存放。存放后结构体占28个字节,是4的整数倍不用补空字符。
32位计算机的虚拟内存地址空间的每个地址是32位的,每个内存地址所指向的内存空间能保存8位(1字节)的数据。假设此类变量的首地址为10000
10000 c 1字节 10001 null
10002 sh———|10003 2字节
10004 a———|10007 4字节
10008 f———|10011 4字节
10012 p———|10015 4字节
10016 s———|10019 4字节
10020 d———|10027 8字节
// 64位 win—long为四字节,按4字节对齐
struct A
{
short var; // 2 字节
int var1; // 8 字节 (内存对齐原则:填充 2 个字节) [2 (short) + 2 (填充) var1的偏移地址] + 4 (int)= 8
long var2; // 12 字节 8 + 4 (long) = 12
char var3; // 16 字节 (内存对齐原则:填充 3 个字节)12 + 1 (char) + 3 (填充) = 16
string s; // 48 字节 16 + 32 (string) = 48
};
假设此类变量的首地址为10000(结构体首地址可整除对齐数4)
10000 var———|10001 2字节
10002 null 10003 null
10004 var1———|10007 4字节var1的首地址与结构体首地址的偏移量,需要是4(int大小)与4(对齐数)中较小数的整数倍
10008 var2———|10011 4字节
10012 var3 1字节 偏移量,需要是1(char大小)与4(对齐数)中较小数的整数倍
10013 null 10014 null 10015 null
10016 s———|10047 32字节 偏移量,需要是32(string大小)与4(对齐数)中较小数的整数倍
A结构体变量占总空间为 10047-10000+1 = 48是结构体最宽基本类型(string 32)与【#pragma pack指定的数值—4】中的较小者的整数倍,所以结构体后不需要填充
写结构体时,将各个变量按所占内存从小到大排列所占结构体所占内存较小
静态变量的存放位置与结构体实例的存储地址无关,是单独存放在静态数据区的,因此用siezof计算其大小时没有将静态成员所占的空间计算进来。
空类是会占用内存空间的,而且大小是1(类的大小=sizeof(类名)),原因是C++要求每个实例在内存中都有独一无二的地址。(空类是最特殊的一种情况)
(一)类内部的成员变量:
普通的变量:是要占用内存的,但是要注意对齐原则(这点和struct类型很相似)。
static修饰的静态变量:不占用内容,原因是编译器将其放在全局变量区。
(二)类内部的成员函数:
普通函数:不占用内存。
虚函数:只要有虚函数类中就需要一个4个字节(32位下)的虚函数表指针。所以一个类的虚函数所占用的地址是不变的,和虚函数的个数是没有关系的
(三)子类所占内存大小
= 父类+自身成员变量的值。特别注意的是,子类与父类共享同一个虚函数表头指针,因此当子类新声明一个虚函数时,不必在对其保存虚函数表指针入口。
某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;
某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作(在读取的过程中数据无法被其他程序修改);
相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间;
某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignment trap);
某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。
在16位的系统中(比如8086微机) 1字 (word)= 2字节(byte)= 16(bit)
在32位的系统中(比如win32) 1字(word)= 4字节(byte)=32(bit)
在64位的系统中(比如win64)1字(word)= 8字节(byte)=64(bit)
内存对齐优点?提高可移植性,提高存取效率。有些平台每次读都是从偶数地址开始,如果一个 int 型(假设是 32 位)如果存放在偶数地址开始的地方,那么一个时钟周期就可以读出。而如果是存放在一个奇数地址开始的地方,就可能会需要 2 个时钟周期,并对两次读出的结果的高低字节进行拼凑才能得到该 int 型数据。
7 内存泄露
堆内存泄漏:new/mallc分配内存,未使用对应的delete/free回收
系统资源泄漏, Bitmap, handle,socket等资源未释放
没有将子类析构函数定义称为虚函数,(使用基类指针或者引用指向派生类对象时)派生类对象释放时将不能正确释放派生对象部分。
析构函数定义成虚函数是为了防止内存泄漏,因为当父类的指针或者引用指向或绑定到派生类的对象时(Base* p = new Derive()),如果未将基类的析构函数定义成虚函数,会调用基类的析构函数,那么只能将基类的成员所占的空间释放掉,派生类中特有的就会无法释放内存空间导致内存泄漏。
8 如何防止内存泄露
1 内部封装,如果类对象需要用到堆内存,则对堆内存的申请只在类内进行,在类的析构函数中做堆内存是否被释放的检查,如果没有被释放则在析构的时候释放
class A
{
private:
char *p;
unsigned int p_size;
public:
A(unsigned int n = 1) // 构造函数中分配内存空间
{
p = new char[n];
p_size = n;
};
~A() // 析构函数中释放内存空间
{
if (p != NULL)
{
delete[] p; // 删除字符数组
p = NULL; // 防止出现野指针
}
};
char *GetPointer()
{
return p;
};
};
但是这种做法当两个类对象都拥有同一资源指针时会调用两次析构函数,两次释放内存造成出错。比如 A a1; A a2=a1;//拷贝构造 直接对成员变量赋值a2也获得了a1.p上的资源指针,在析构的时候a1先析构将p1指针资源释放了,但a2并不知道a2.p此时不为null,a2析构时会再一次对相同的a1.p==a2.p进行delete,对同一块空间free两次报错
*2 优化,添加引用计数机制
class A
{
private:
char *p;
unsigned int p_size;
int *p_count; // 计数变量 注意这个计数变量是个指针,对于由同一对象拷贝构造出的所有对象以及拷贝构造对象的拷贝构造对象.....,是全部共享这一指针变量,共同维护这同一计数的!!!!
// 发生正常构造会为变量分配 成员变量堆空间,计数变量空间
// 发生拷贝构造,被构造对象的资源指针和计数指针 与 其拷贝的对象是指向同一空间的
public:
A(unsigned int n = 1) // 在构造函数中申请内存
{
p = new char[n];
p_size = n;
p_count = new int;
*p_count = 1;
cout << "count is : " << *p_count << endl;
};
A(const A &temp)// 拷贝构造
{
// 先将当前p给处理了
(*p_count)--; // 析构时,计数变量 -1
cout << "count is : " << *p_count << endl;
if (*p_count == 0) // 只有当计数变量为 0 的时候才会释放该块内存空间
{
cout << "buf is deleted" << endl;
if (p != NULL)
{
delete[] p; // 删除字符数组
p = NULL; // 防止出现野指针
if (p_count != NULL)
{
delete p_count;
p_count = NULL;
}
}
}
// 拷贝右值传入的资源,并将资源指针++
// 注意p,p_size,p_count等属性都是私有的
// 传入类被同函数中,在这个函数内部(类内部)可以访问此类temp的私有成员
p = temp.p;
p_size = temp.p_size;
p_count = temp.p_count;
(*p_count)++; // 复制时,计数变量 +1
cout << "count is : " << *p_count << endl;
}
~A()
{
(*p_count)--; // 析构时,计数变量 -1
cout << "count is : " << *p_count << endl;
if (*p_count == 0) // 只有当计数变量为 0 的时候才会释放该块内存空间
{
cout << "buf is deleted" << endl;
if (p != NULL)
{
delete[] p; // 删除字符数组
p = NULL; // 防止出现野指针
if (p_count != NULL)
{
delete p_count;
p_count = NULL;
}
}
}
};
char *GetPointer()
{
return p;
};
};
智能指针是 C++ 中已经对内存泄漏封装好了一个工具,其中shared_ptr实现的就是上面的这种功能。
下面介绍一下valgrind这种调试工具
valgrind 是一套 Linux 下,开放源代码(GPL V2)的仿真调试工具的集合,包括以下工具:
Memcheck:内存检查器(valgrind 应用最广泛的工具),能够发现开发中绝大多数内存错误的使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。
Callgrind:检查程序中函数调用过程中出现的问题。
Cachegrind:检查程序中缓存使用出现的问题。
Helgrind:检查多线程程序中出现的竞争问题。
Massif:检查程序中堆栈使用中出现的问题。
Extension:可以利用 core 提供的功能,自己编写特定的内存调试工具。
Memcheck 能够检测出内存问题,关键在于其建立了两个全局表:
Valid-Value 表:对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits(位) ;对于 CPU 的每个寄存器,也有一个与之对应的 bit 向量。这些 bits 负责记录该字节或者寄存器值是否具有有效的、已初始化的值。
Valid-Address 表:对于进程整个地址空间中的每一个字节(byte),还有与之对应的 1 个 bit,负责记录该地址是否能够被读写。
检测原理:
当要读写内存中某个字节时,首先检查这个字节对应的 Valid-Address 表中对应的 bit。如果该 bit 显示该位置是无效位置,Memcheck 则报告读写错误。
内核(core)类似于一个虚拟的 CPU 环境,这样当内存中的某个字节被加载到真实的 CPU 中时,该字节在 Valid-Value 表对应的 bits 也被加载到虚拟的 CPU 环境中。一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则 Memcheck 会检查 Valid-Value 表对应的 bits,如果该值尚未初始化,则会报告使用未初始化内存错误。
9 智能指针
c++11在
共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数,可以通过 unique_ptr、weak_ptr 来构造,调用 release() 释放资源的所有权,它们自己被销毁,或者它们的值因赋值操作或显式调用 shared_ptr::reset 而改变时,就会释放它们共同拥有的对象的所有权,计数减一,当计数减为 0 时,会自动释放内存空间(shared_ptr也可以规定删除器,在释放资源前做一些操作),从而避免了内存泄漏。
同一个shared_ptr被多个线程读,是线程安全的;
同一个shared_ptr被多个线程写,不是线程安全的;
共享引用计数的不同的shared_ptr被多个线程写,是线程安全的。
注意当一个资源被两个shared_ptr所有(这两个shared_ptr之间不是共享关系),当其中一个释放时会导致另外一个shared_ptr所拥有的资源无效。
shared_ptr 对象只能通过复制它们的值or reset来共享所有权
此外,shared_ptr 对象可以共享一个指针的所有权,同时指向另一个对象。这种能力被称为别名(参见构造函数),通常用于在拥有成员对象时指向成员对象。
std::shared_ptr
std::shared_ptr
template
一个 shared_ptr 可能与两个指针相关
一个存储指针(get返回的那个指针),即它所指向的指针,以及它用 operator, operator->, operator[]所取内容的指针,bool==判断所用的指针—p,operator<<输出的指针
一个控制块指针(release放弃的是这个指针)(可能是共享的),它是所有权组负责在某个时间点删除的指针,就是共享的那个资源指针—x
通常,存储指针和所有者指针指向同一个对象,但别名 shared_ptr 对象(使用别名构造函数及其副本构造的对象)可能指向不同的对象,所以有可能存在空控制块指针和非真存储指针的共享指针。
reset 释放资源所有权,并接收传入的资源指针(如果reset()没有传入资源,也就是没有任何对象的所有权, 此时仅释放资源所有权,reset()会同时释放x和p)
出于安全原因,shared_ptr不支持指针算术
std::shared_ptr<int> p1;//引用计数0
std::shared_ptr<int> p2 (nullptr);//引用计数0
std::shared_ptr<int> p3 (new int);//引用计数1
std::shared_ptr<int> p4 (new int, std::default_delete<int>());//引用计数1
std::shared_ptr<int> p5 (new int, [](int* p){delete p;}, std::allocator<int>());//引用计数2(5,7)
std::shared_ptr<int> p6 (p5);// 引用计数0,后面被move了
std::shared_ptr<int> p7 (std::move(p6));// 引用计数2
std::shared_ptr<int> p8 (std::unique_ptr<int>(new int));// 引用计数1
std::shared_ptr<C> obj (new C);
std::shared_ptr<int> p9 (obj, obj->data);//引用计数2(9,obj) 别名构造函数
shared_ptr<Test> ptest(new Test("123"));
cout<<ptest2->getStr()<<endl;// 取内容Test的成员函数
cout<<ptest2.use_count()<<endl;// shared_ptr的成员函数.use_count
// 注意上面说的引用计数指的是在所有shared_ptr构造完后,再用use_count去看指针上对应的计数
独占指针(unique_ptr):禁止拷贝语义, 只有移动语义来实现。独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用 move() 函数),即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,可以通过该方法进行赋值。当然当独占指针的声明周期结束在其析构中会自动析构堆上资源
get 获得资源指针
release 放弃内部对象的所有权,将内部指针置为空, 返回所内部对象的指针, 注意只是放弃此资源,资源在release后并没有被释放。此指针需要手动释放
reset 释放当前指针内保存的资源,并接收传入的资源指针(如果reset()没有传入资源,也就是没有任何对象的所有权, 此时仅将内部对象释放, 并置为空)
swap 交换两个unique_ptr中的资源指针(相当于交换资源),以及两个unique_ptr的deleter
无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作
unique_ptr<Test> fun()
{
return unique_ptr<Test>(new Test("789"));// 返回的是局部变量,但是这个局部变量拥有堆上资源 返回类型为对象
}
int main()
{
unique_ptr<Test> ptest(new Test("123"));
unique_ptr<Test> ptest2(new Test("456"));
//下面这样构造也能达到同样的效果
//std::unique_ptr<Test> ptest;
//ptest.reset(new Test(“123”));
//注意 ptest = nullptr;//显式销毁所指对象,同时智能指针变为空指针。与ptest.reset()等价
ptest2 = std::move(ptest);//不能直接ptest2 = ptest
//std::unique_ptr<Test> ptest2(ptest1); // 错误, unique_ptr 不支持拷贝
//std::unique_ptr<Test> ptest2= ptest1; // 错误, unique_ptr 不支持赋值
//std::unique_ptr<Test> ptest2(ptest1.release());// 正确
if(ptest == NULL)cout<<"ptest = NULL\n";
Test* p = ptest2.release();
ptest.reset(p);
ptest2 = fun(); //这里可以用=,返回纯右值(局部变量)调用了移动构造函数 去构造ptest2
// 在构造前会检查是否存在原资源,如果有先将原资源释放,和reset的行为一样
// 注意当我们将unique_ptr作为实参传值时也需要(传引用不需要) foo(std::move(ptest))
return 0;
}
并且unique_ptr还支持管理对象数组
std::unique_ptr ups(new A[10]);// 可以使用ups[i]来访问数组元素
unique_ptr的删除器可以自定义
有两种定义方法
class Connecter
{
void Disconnect() { PRINT_FUN(); }
};
void Deleter(Connecter* obj)
{
obj->Disconnect(); // 做其它释放或断开连接等工作
delete obj; // 删除资源指针
}
std::unique_ptr<Connecter, decltype(Deleter)*> up(new Connecter, Deleter);
// 在构造时可以提供删除器的类型,reset时也可以up.reset(new Connecter, Deleter)
//另一种定义方法
class Deleter
{
public:
void operator() (Connecter* obj)
{
obj->Disconnect();
delete obj;
}
};
std::unique_ptr<Connecter, Deleter> up1(new Connecter);
std::unique_ptr<Connecter, Deleter> up2(new Connecter, up1.get_deleter());
unique_ptr 可以实现如下功能:
1、为动态申请的内存提供异常安全
2、将动态申请的内存所有权传递给某函数
3、从某个函数返回动态申请内存的所有权
4、在容器中保存指针(容器中无法保存unique_ptr的早期版本auto_ptr)
https://www.jianshu.com/p/234b818f289a
弱指针(weak_ptr):指向 share_ptr 指向的对象,能够解决由shared_ptr带来的循环引用问题。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。弱引用不持有对象,当对象不存在时弱引用能够检测到,从而避免非法访问,弱引用也不会修改对象的引用计数。这意味这弱引用它并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。(weak_ptr指针内会保存构造它的shared_ptr的共享控制块指针,以及共享计数指针,weak_ptr无法修改计数指针,但是可以读取计数值知道当前有多少指针拥有此共享资源,当计数为0,weak_ptr就直到共享的资源已经被置为null)
std::shared_ptr<int> sp1(new int(10));
std::shared_ptr<int> sp2(sp1);
std::weak_ptr<int> wp(sp2);// weak_ptr可由shared_ptr以及weak_ptr构造
wp.use_count();// 查看和当前 weak_ptr 指针相同的 shared_ptr 指针的数量
wp.expired();//查看当前弱指针指向资源是否过期(为null已被释放) 计数为0返回true
wp.lock();
//如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针
wp.reset();// 将当前weak_ptr置为空
weak_ptr没有重载*和->因此weak_ptr只能通过lock获得其指向的shared_ptr(lock会构造出一个shared_ptr临时对象,会使引用计数+1,lock()成功时会延长shared_ptr对象的生命周期),再通过这个shared_ptr访问其堆资源。
除lock构造出的shared_ptr,其他指向相同的shared_ptr都已经析构,如果std::weak_ptr的lock()成功的指针还存在,那么这时候再一次进行lock()的代码调用,引用计数仍然递增
weak_ptr只能拥有shared_ptr的观测权,没有使用权,想要使用的时候,就将weak_ptr转为shared_ptr,这个时候新产生的shared_ptr对象是不会引起循环引用的问题,因为它在它的作用域结束后(lock返回的是一个临时shared_ptr对象,在lock这句话结束后这个临时对象就析构了)肯定要释放的
强引用循环:A对象强引用了B对象,B对象也强引用了A。因为都是强引用,也就是无论是A是B都要在对方的引用断了后才能销毁,但要断了引用,就必须对方对象销毁。就会出现这种僵局,为了避免出现这种情况,就应该有一个对象“示弱”,使其为“弱引用”。由于弱引用不更改引用计数,类似普通指针,只要把循环引用的一方使用弱引用,即可解除循环引用。
class A
{
public:
void Register(const std::shared_ptr<B>& sp)
{
m_spb = sp;
}
private:
std::shared_ptr<B> m_spb;// weak_ptr
};
class B
{
public:
void Register(const std::shared_ptr<A>& sp)
{
m_spa = sp;
}
private:
std::shared_ptr<A> m_spa;
};
int main(){
{
std::shared_ptr<A> pa(new A);
std::shared_ptr<B> pb(new B);
pa->Register(pb);// B的资源引用计数为2
pb->Register(pa);// A的资源引用计数为2
}
// A(pa,m_spa) B(pb,m_spb)
}
离开作用域后pa,pb析构。但是这个堆上的A,B因为在堆上所以不会被析构,则堆上A,B的m_spa,m_spb两个共享指针都不会析构,B的资源引用计数为1,A的资源引用计数为1。都不为0,都不会释放堆上资源。这就是循环引用导致的问题,只有当main结束后才能释放资源。
A对象强引用了B对象,B对象也强引用了A。因为都是强引用,也就是无论是A是// B都要在对方的引用断了后才能销毁,但要断了引用,就必须对方对象销毁。就会出现这// 种僵局,
解决循环引用只需要将m_spb,m_spa其中一个共享指针改为weak_ptr即可。比如将m_spb改为weak_ptr,这样在pa->Register(pb);后B资源的引用计数依旧是1,A(pa,m_spa) B(pb),在出作用域pb析构时B资源的引用计数减到0,B资源就释放了,pb析构其中的成员变量m_spa自然也析构了,A资源的引用计数为1,pa出作用域pa析构时A资源的引用计数减到0,A资源就释放了
std::shared_ptr 和 std::weak_ptr的用法以及引用计数的循环引用问题
用于在延迟使用主导权的场景, 比如线程A是重要,A完成之后B,C线程可做可不做.这时候B,C就可以使用weak_ptr指针来控制.
虽然通过弱引用指针可以有效的解除循环引用,但这种方式必须在程序员能预见会出现循环引用的情况下才能使用,也可以是说弱引用仅仅是一种编译期的解决方案,如果程序在运行过程中出现了循环引用,还是会造成内存泄漏的。因此,不要认为只要使用了智能指针便能杜绝内存泄漏。
10 C++和其他语言的区别
c和c++的区别
C 语言是面向过程的编程,它最重要的特点是函数,通过 main 函数来调用各个子函数。程序运行的顺序都是程序员事先决定好的。C++ 是面向对象的编程,类是它的主要特点,在程序执行过程中,先由主 main 函数进入,定义一些类,根据需要执行类的成员函数,过程的概念被淡化了(实际上过程还是有的,就是主函数的那些语句。),以类驱动程序运行,类就是对象,所以我们称之为面向对象程序设计。面向对象在分析和解决问题的时候,将涉及到的数据和数据的操作封装在类中,通过类可以创建对象,以事件或消息来驱动对象执行处理,C++ 对 C 的“增强”,表现在以下几个方面:类型检查更为严格。增加了面向对象的机制、泛型编程的机制(Template)、异常处理、运算符重载、标准模板库(STL)、命名空间(避免全局命名冲突)。
java和c++的区别
指针:C++ 可以直接操作指针,容易产生内存泄漏以及非法指针引用的问题;Java 并不是没有指针,虚拟机(JVM)内部还是使用了指针,只是编程人员不能直接使用指针,不能通过指针来直接访问内存,并且 Java 增加了内存管理机制。
多重继承:C++ 支持多重继承,允许多个父类派生一个类,虽然功能很强大,但是如果使用的不当会造成很多问题,例如:菱形继承;Java 不支持多重继承,但允许一个类可以继承多个接口,可以实现 C++ 多重继承的功能,但又避免了多重继承带来的许多不便。
数据类型和类:Java 是完全面向对象的语言,所有函数和变量部必须是类的一部分。除了基本数据类型之外,其余的都作为类对象,包括数组。对象将数据和方法结合起来,把它们封装在类中,这样每个对象都可实现自己的特点和行为。而 C++ 允许将函数和变量定义为全局的。
垃圾回收:
Java 语言一个显著的特点就是垃圾回收机制,编程人员无需考虑内存管理的问题,可以有效的防止内存泄漏,有效的使用空闲的内存。
Java 所有的对象都是用 new 操作符建立在内存堆栈上,类似于 C++ 中的 new 操作符,但是当要释放该申请的内存空间时,Java 自动进行内存回收操作,C++ 需要程序员自己释放内存空间,并且 Java 中的内存回收是以线程的方式在后台运行的,利用空闲时间。
应用场景:
Java 运行在虚拟机上,和开发平台无关,C++ 直接编译成可执行文件,是否跨平台在于用到的编译器的特性是否有多平台的支持。
C++ 可以直接编译成可执行文件,运行效率比 Java 高。
Java 主要用来开发 Web 应用。
C++ 主要用在嵌入式开发、网络、并发编程的方面。
python和c++的区别
python是脚本语言不需要编译
首先,Python 中涉及的内容比 C++ 多,经过了更多层,Python 中甚至连数字都是 object ;其次,Python 是解释执行的,和物理机 CPU 之间多了解释器这层,而 C++ 是编译执行的,直接就是机器码,编译的时候编译器又可以进行一些优化。