内存分配与管理
内存分配方式
由C/C++编译的程序占用的内存分为以下几个部分
栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量等。其操作方式类似于数据结构中的栈。
堆区(heap):一般由程序员分配释放(malloc/free、new/delete),若程序员不释放,程序结束时可能由操作系统回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
全局区(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后由系统释放。
文字常量区:常量字符串就是放在这里的,程序结束后由系统释放。
程序代码区:存放函数体的二进制代码
堆和栈区别
申请方式不同。
栈由系统自动分配。
堆由程序员手动分配。
申请大小限制不同。
栈向低地址方向增长,栈顶和栈底是之前预设好的,大小固定。
堆向高地址方向增长,是不连续的内存区域,大小可以灵活调整。
申请效率不同。
栈,速度快,是一个先进后出的队列,进出一一对应,不会产生碎片。
堆,速度慢,频繁的new/delete会造成大量碎片,使程序效率降低。
动态内存
malloc、calloc、realloc、alloca
malloc:申请指定字节数的内存。申请到的内存中的初始值不确定。
calloc:为指定长度的对象,分配能容纳其指定个数的内存。申请到的内存的每一位(bit)都初始化为0
realloc:更改以前分配的内存长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定
alloca:在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca不宜使用在必须广泛移植的程序中,。C99中支持变长数组(VLA), 可以用来替代alloca()。
malloc、free
申请内存,确认是否申请成功
char *str = (char*) malloc(100); assert(str != nullptr);
释放内存后指针置空
free(p); p = nullptr;
new、delete
new/new[]:完成两件事,先底层调用malloc分了配内存,然后创建一个对象(调用构造函数)。
delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用free释放空间。
new在申请内存时会自动计算所需字节数,而malloc则需我们自己输入申请内存空间的字节数。
int main() { T* t = new T(); // 先内存分配 ,再构造函数 delete t; // 先析构函数,再内存释放 return 0; }
内存泄露
堆是动态分配内存的,并且可以分配很大的内存,使用不好会产生内存泄露。频繁使用malloc和free会产生内存碎片。
所谓内存泄漏是指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。一般常说的内存泄漏是指堆内存的泄露。内存泄露其实并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。内存泄漏与许多其他问题有着相似的症状,并且通常情况下只能由哪些可以获得程序源码的程序员才可以分析出来。
应用程序一般使用malloc、calloc、realloc、new等函数从堆中分配到一块内存,使用完后,程序必须负责响应地调用free、delete释放内存块,否则这块内存就不能再次使用,造成内存泄露。
new/delete和malloc/free区别
new/delete是c++关键字,需要编译器支持;malloc/free是库函数,需要头文件支持;
new能够自动计算需要分配的内存空间,而malloc需要手工计算字节数。例如,
int* p1=new int[2],int * p2=malloc(2*sizeof(int))
;new与delete直接返回具体类型的指针,而malloc与free返回void类型指针。
new是类型安全的,而malloc不是,例如,int * p=new float[2], 编译时就会报错;而
int * p=malloc(2*sizeof(int))
, 编译时编译器就无法指出错误来。new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。
指针相关
引用和指针
引用只是别名,不占用具体存储空间,只有声明没有定义;指针是具体变量,需要占用存储空间。
引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。
引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量。
不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。
指针数组和数组指针
指针数组,是指一个数组里面装着指针,也即指针数组是一个数组。一个有10个指针的数组,其中每个指针指向一个整型数,那么次数组定义位:
int *a[10];
数组指针,是指一个指向数组的指针,它其实还是指针,只不过它指向整个数组。一个指向有10个元素整形数组的指针定义为:
int (*a)[10];
字符串相关函数
strcmp
int Strcmy(const char* str1,const char* str2) { assert(str1!=NULL&&str2!=NULL); int ret = 0; while (!(ret = *(unsigned char*)str1 - *(unsigned char*)str2) && *str1) { str1++; str2++; } if (ret < 0) return -1; else if (ret > 0) return 1; else return 0; }
strcat,strcpy,strncpy
strcat(dest,scr)
把src所指字符串添加到dest尾处(覆盖dest结尾的'\')并添加'\0'
char * my_strcat(char *dest, const char *ptr) { char *temp = dest; while (*temp != '\0') { temp++; } while (*ptr != '\0') { *temp = *ptr; temp++; ptr++; } *temp = '\0'; return dest; }
strcpy(dest,src)把从scr地址开始且还有null结束符的字符串复制到以dest开始的地址空间
char *my_strcpy( char *dst, const char *scr ) { char *ret = dst;//保存返回地址 assert(dst); assert(scr); while( *dst++ = *scr++ );//实现拷贝 return ret; }
memcpy,memset
void *memcpy(void *dest,const void *src,size_t n);
功能:从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中。函数返回指向*dest的指针。
memcpy与strcpy区别
复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整形、结构体、类等。strcpy只能用于字符串复制,并且它不仅复制字符串内容之外还会复制字符串的结束符。memcpy对于需要复制的内容没有限制,因此用途更广。
复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符'\0'时才结束,所以容易溢出。memcpy则根据其第三个参数决定复制的长度。
用途不同。通常在复制字符串时使用strcpy,而需要复制其他数据时一般用memcpy。
void *memset(void *s,int ch,sizet n);
功能:将s中前n个字节用ch替换并返回s,作用是一段内存中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法,
关键字
const关键字
常量:
const限定符把一个对象转换成一个常量,因为常量定义后就不能修改,所以定义时必须初始化。
修饰类成员变量:
//用const修饰的类成员变量,只能在类的构造函数初始化列表中赋值,不能在类构造函数体内赋值。 class A { public: A(int x) : a(x) // 正确 { //a = x; // 错误 } private: const int a; };
修饰类成员函数:
//用const修饰的类成员函数,在该函数体内不能改变该类对象的任何成员变量, 也不能调用类中任何非const成员函数。 class A { public: int& getValue() const { // a = 10; // 错误 return a; } private: int a; // 非const成员变量 };
修饰类对象:
/*用const修饰的类对象,该对象内的任何成员变量都不能被修改。 因此不能调用该对象的任何非const成员函数,因为对非const成员函数的调用会有修改成员变量的企图。*/ class A { public: void funcA() {} void funcB() const {} }; int main { const A a; a.funcB(); // 可以 a.funcA(); // 错误 const A* b = new A(); b->funcB(); // 可以 b->funcA(); // 错误 }
static关键字
c语言中static的用法:
1、全局静态变量:
用法:在全局变量前加上关键字static,全局变量就定义成一个全局静态变量。 static int temp;
内存中的位置:静态存储区,在整个程序运行期间一直存在。
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
2、局部静态变量:
在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
内存中的位置:静态存储区;
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);且只在第一次被调用时定义;
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
3、静态函数:
在函数返回类型前加关键字static,函数就定义成静态函数。函数的定义和生命在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用;
c++中static的用法:
1、类的静态成员:
class A{ private: static int val; }; int A::val=0;
在cpp中必须对他进行初始化,初始化时使用作用域运算符来标明他所属类,其属于该类的所有成员共有,只有一个拷贝;
静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。在Example中,语句int A::val=0;是定义静态数据成员;(类内声明,类外定义)
2、类的静态成员函数:
class A{ private: static int func(int x); };
实现的时候也不需要static的修饰,因为static是声明性关键字;类的静态函数是该类的范畴内的全局函数,不能访问类的私有成员,只能访问类的静态成员,不需要类的实例即可调用、(非静态成员函数可以任意地访问静态成员函数和静态数据成员);实际上,他就是增加了类的访问权限的全局函数;
void A::func(int);
静态成员函数可以继承和覆盖,但无法是虚函数;
3、只在cpp内有效的全局变量:
在cpp文件的全局范围内声明:
static int val = 0;
这个变量的含义是该cpp内有效,但是其他的cpp文件不能访问这个变量;如果有两个cpp文件声明了同名的全局静态变量,那么他们实际上是独立的两个变量;
4、只在cpp内有效的全局函数:
函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;
volatile关键字
volatile int i = 10;
volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。
volatile关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
const 可以是 volatile (如只读的状态寄存器)
指针可以是 volatile
define和const定义常量
用
#define MAX 255
定义的常量是没有类型的,所给出的是一个立即数,编译器只是把所定义的常量值与所定义的常量的名字联系起来,define所定义的宏变量在预处理的时候进行替换,在程序中使用到该常量的地方都要进行拷贝替换;用
const float MAX = 255;
定义的常量有类型名字,存放在内存的静态区域中,在程序运行过程中const变量只有一个拷贝,而#define 所定义的宏变量却有多个拷贝,所以宏定义在程序运行过程中所消耗的内存要比const变量的大得多;用define定义的常量是不可以用指针变量去指向的,用const定义的常量是可以用指针去指向该常量的地址的;
用define可以定义一些简单的函数,const是不可以定义函数的.
具体来说,有以下几方面的区别:
编译器处理方式
define – 在预处理阶段进行替换 const – 在编译时确定其值
类型检查
define – 无类型,不进行类型安全检查,可能会产生意想不到的错误 const – 有数据类型,编译时会进行类型检查
内存空间
define – 不分配内存,给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大 const – 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝
strlen和sizeof
自定义函数实现strlen功能:
int strlen(const char *str){ assert(str!=NULL); int len=0; while((*str++)!='\0') len++; return len; } int strlen(const char *str){ assert(str!=NULL); return *str=='\0'? 0 : (1+strlen(++str)); }
sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。
sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是'\0'的字符串。
因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。
strlen("\0")=0;sizeof("\0")=2;
结构体共用体枚举
概念
共用体
结构体和共用体(联合)都是由不同的数据类型成员组成,但在任何同一时刻,共用体中只存放了一个被选中的成员,而结构体的所有成员都存在。对于共用体的不同成员赋值,将会对其他成员重写,原来的成员的值就不存在了,而对于结构体的不同成员赋值是互不影响的。
枚举
枚举(enum)是一种用户自定义的类型,定义的基本格式为:
enum 枚举类型名 {枚举常量1[=整形常数],枚举常量1[=整形常数],…} [变量名列表]
花括号中内容称为枚举表,其中的每一项称为枚举常量,换言之,枚举表是枚举常量的集合。枚举表中每项后的“=整形常数”是给枚举常量赋初值,用方括号代表可以省略,如果不给枚举常量赋值,编译器会给每一个枚举常量赋一个不同的整形值,第一个为0,第二个为1等。当枚举表中某个常量赋值后,其后的成员则按一次加1的规则确定其值。
空间计算
struct
遵循两个原则:
整体空间是占用空间最大的成员(的类型)所占字节数的整数倍,但在32位Linux+gcc(vs下不满足)环境下,若最大成员类型所占字节超过4,如double是8,则整体空间是4的倍数即可。
数据对齐原则——内存按结构体成员的先后顺序排列,当排到该成员变量时,其前面已摆放的空间大小必须是该成员类型大小的整数倍,如果不够则补齐,依次向后类推,但在Linux+gcc(vs下不满足)下,若某成员类型占字节数超过4,如double是8,则前面已摆放的空间大小是4的倍数即可,不够则补齐。
//struct空间计算例子,windows32环境 struct s1{ char a; double b; int c; char d; }; //sizeof(s1)=24; struct s2{ char a; char b; int c; double d; }; //sizeof(s2)=16;
位域
在结构体和类中,可以使用位域来规定某个成员所能占用的空间,所以使用位域能在一定程度上节省结构体占用的空间。
使用位域的主要目的是压缩存储,其大致规则为:
如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则其后面的字段将紧邻前一个字段存储,直到不能容纳为止;
如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
如果相邻的位域字段类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++与gcc采取压缩方式;
如果位与字段之间穿插着非位域字段,则不能进行压缩;
整个结构体的总大小为最宽基本类型成员大小的整数倍。
//Linux+gcc struct a{ int f1:3; //8位只占用前三位 char b; char c; }; //sizeof(a)=4; VS下参见规则3不压缩,8 struct b1{ char f1:3; char f2:4; //第一个字节可容纳f1和f2 char f3:5; } //sizeof(b1)=2;
“#pragma pack”
基本用法:#pragma pack(n),n为字节对齐数,其取值为1、2、4、8、16等,默认为8,如果这个值比结构体成员的sizeof值小,那么该成员的偏移量应该以此值为准,即结构体成员的偏移量应该取二者的最小值,公式如下:offsetof(item)=min(n,sizeof(item));
struct node{ char f; //0~3 int e; //4~7 short int a; //8~9 char b; //10 }; //sizeof(node)=12; 4的倍数 struct node{ char f; //0~1 int e; //2~5 #2位对齐,即是2的倍数地址,不能为4的倍数 short int a; //6~7 char b; //8 }; //sizeof(node)=10; 2的倍数 struct node{ char f; int e; short int a; char b; }; //sizeof(node)=8;
union
结构体在内存组织上是顺序式的,联合体是重叠式,各成员共享一段内存,所以整个联合体的sizeof也就是每个成员sizeof的最大值。但uninon也需要跟struct一样考虑对齐问题,原则同struct规则1.
union s1{ double b; }; union U{ int i; char c; s1 s; } //sizeof(U)=8 //考虑对齐问题 union{ char b[9]; int bh[2]; }c; //数组b占用9个字节,考虑对齐,须是4(int 占用空间数)的整数倍,故补齐为12