C++学习:第六章Linux高级编程-(一)课程介绍、内存结构、堆结构、内存...
2023年6月22日发(作者:)
C++学习:第六章Linux⾼级编程-(⼀)课程介绍、内存结构、堆结构、内存分配函数、函数。。。⼀、约定1. 作业完成.2. 50-200 lines codes.⼆、课程体系1. 语⾔ C C++2. 算法 算法 数据结构3. 基础(系统核⼼(Kernel)编程) Linux/Unix Window MacOSX PC机 服务器 ARM
任何⼀个操作系统都应该具备以下能⼒: 内存管理 ⽂件⽬录 IO 进程管理
进程的创建 进程的控制 进程的通信 进程的同步 线程管理 线程创建 线程同步(含控制) 线程通信 应⽤ ⽹络 数据库(pro*c/c++) UI shell 定位:提供编程的能⼒,为设备驱动与Window应⽤奠定基础.三、 内存管理1. 硬件层次 内存结构管理2. 内核层次 内存映射 堆扩展3. 语⾔层次 c :malloc // #include c++ :new4. 数据结构 STL Standard Template Library 申请⼀个容器⽤来存放数据,数据存放在什么位置不需要管理,其实这⾥⾯隐藏着对内存的托管,其解决的就是内存中的⼀些细节问题。但其有两个瓶颈效应:1. 对多线程⽀持存在问题。2. 对共享内存的实现存在问题 智能指针
5. Linux对内存的结构描述5.1 proc⽬录 在Linux 下,根⽬录/proc/${pid 进程ID}/这个⽬录下存放这每个程序运⾏的所有信息,包括内存结构。exe执⾏程序⽂件,它指向我们的执⾏程序。cwd指向当前的⼯作路径,这是个链接⽅式fd 是这个程序打开的所有的⽂件cpuset存放的cpu信息cmdline 命令⾏environ 环境参数和环境变量maps 程序运⾏的所有内存结构 查看程序maps结构 在第⼀个终端下运⾏下⾯程序,得到pid=4270
#include #include int main(){ printf("%dn", getpid()); getchar(); return 0;} 在第⼆个终端下cd到根⽬录下,打开 可以看到程序运⾏的内存结构以及各部分的地址分配。任何程序的内存空间分成4个基本部分1)代码区2)全局栈区3)堆4)局部栈5.2 内存数据结构运⾏代码#include #include #include int add(int a, int b){return a+b;}int a1 = 1;static int a2 = 2;const int a3 = 3;int main(){ int b1=4; static int b2=5; const int b3=6; int* p1 = malloc(4); printf("a1=%pn", &a1); printf("a2=%pn", &a2); printf("a3=%pn", &a3); printf("b1=%pn", &b1); printf("b2=%pn", &b2); printf("b3=%pn", &b3); printf("p1=%pn", p1); printf("mian=%pn", main); printf("func=%pn", add); printf("pid=%dn", getpid()); getchar(); return 0;} 可以看到代码区 --- 存放函数地址 + 全局 const 对象全局栈区 --- 存放全局变量 + static 修饰对象局部栈区 --- 存放局部变量 + 局部 const 对象堆 --- 存放 malloc 对象 + new 对象(这⾥没举例⼦) 5.3 ⾃加载器 解决代码拷贝到内存中的问题,⾃加载器负责调⽤程序⽂件,找到main函数,拷贝代码到指定位置。 5.4 malloc 与 new ⽤堆分配空间,分配4字节空间占却⽤16字节(我这边事64位ubuntu 占⽤32字节)的实际空间:因为堆是靠链表来维护的。链表中不仅仅包含当前分配空间⼤⼩的信息,分配的空间/上⼀个空间数据/下⼀个空间/空间⼤⼩等信息,所以占⽤空间⽐分配空间要更⼤。
#include #include int main(){ // 这⾥不需要进⾏类型转换 // 因为C语⾔和ObjectC语⾔是⼀样的,是弱类型语⾔,很多类型他是⾃⼰去判定 int *p1 = malloc(4); int *p2 = malloc(4); int *p3 = malloc(4); int *p4 = malloc(4); int *p5 = malloc(4); printf("%pn", p1); printf("%pn", p2); printf("%pn", p3); printf("%pn", p4); printf("%pn", p5); return 0;}#include #include int main(){ int *p1 = new int; int *p2 = new int; int *p3 = new int; int *p4 = new int; int *p5 = new int; printf("%pn", p1); printf("%pn", p2); printf("%pn", p3); printf("%pn", p4); printf("%pn", p5); return 0;} 运⾏可以看到地址分配的间隔 32 字节 malloc 和 new 的关系 相同:new的实现使⽤的是malloc来实现的。 区别:new使⽤malloc后,还要初始化空间、基本类型、直接初始化成默认值。UDT(⽤户⾃定义类型)类型,调⽤指定的构造器 delete调⽤free实现:delete负责调⽤析构器,然后在调⽤free。 new与new[]区别 1)new只调⽤⼀个析构器初始化. 2)new[]循环对每个区域调⽤构造器. delete 与delete[] 1)delete 只调⽤⼀个构造器然后在调⽤free。 2)delete[]循环对每个区域调⽤构造器、free。.5.5 堆和栈
#include #include #include int main(){ int a1 = 10; int a2 = 20; int a3 = 30; int * p1 = malloc(0); int * p2 = malloc(0); int * p3 = malloc(0); printf("a1=%pn", &a1); printf("a2=%pn", &a2); printf("a3=%pn", &a3); printf("p1=%pn", p1); printf("p2=%pn", p2); printf("p3=%pn", p3); printf("pid=%dn", getpid()); getchar(); return 0;} a1、a2、a3在局部栈,p1、p2、p3在堆,观察地址变化和递增⼤⼩。 5.6 函数调⽤栈空间的分配与释放#include int add(int a,int b){ return a+b;}int main(){ int (*func)(int) = (int(*)(int))add; int r = func(20); printf("%dn", r); return 0;}1)函数执⾏的时候有⾃⼰的临时栈。2)函数的参数就在临时栈中,如果函数传递实参,则⽤来初始化临时参数变量。3)通过寄存器返回值,(使⽤返回值返回数据)4)通过参数返回值,(参数必须是指针),指针指向的区域必须事先分配。5)如果参数返回指针,参数就是双指针.__stdcall __cdecl __fastcall1)决定函数栈压栈的参数顺序.2)决定函数栈的清空⽅式3)决定了函数的名字转换⽅式.拓展6.5.2 拓展__stdcall__cdecl__fastcall左边开始的两个不⼤于4字节参数传递⽅式右->左 压栈右->左 压栈(DWORD)的参数分别放在ECX和EDX寄存器,其余的参数仍旧⾃右向左压栈传送被调⽤函数清理(即函数⾃⼰清理),多数据情况使⽤这个清理栈⽅调⽤者清理被调⽤者清理栈适⽤场合C编译修饰约定(它们均不改变输出函数名中的字符⼤⼩写)Win APIc/C++MFC默认⽅式可变参数的时候使⽤速度快约定在输出函数名前加上⼀个下划线前缀,后⾯加上⼀个“@”符号和其参数的字节数,格式为_functionname@number约定仅在输出函数名前加上⼀个下划线前缀,格式为_functionname调⽤约定在输出函数名前加上⼀个“@”符号,后⾯也是⼀个“@”符号和其参数的字节数,格式为@functionname@number。 1. 修饰名(Decoration name) “C”或者“C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时⽣成的字符串。有些情况下使⽤函数的修饰名是必要的,如在模块定义⽂件⾥头指定输出“C++”重载函数、构造函数、析构函数,⼜如在汇编代码⾥调⽤“C””或“C++”函数等。 修饰名由函数名、类名、调⽤约定、返回类型、参数等共同决定。 2. 名字修饰约定随调⽤约定和编译种类(C或C++)的不同⽽变化。函数名修饰约定随编译种类和调⽤约定的不同⽽不同,下⾯分别说明。 a、C编译时函数名修饰约定规则: __stdcall调⽤约定在输出函数名前加上⼀个下划线前缀,后⾯加上⼀个“@”符号和其参数的字节数,格式为_functionname@number。 __cdecl调⽤约定仅在输出函数名前加上⼀个下划线前缀,格式为_functionname。 __fastcall调⽤约定在输出函数名前加上⼀个“@”符号,后⾯也是⼀个“@”符号和其参数的字节数,格式为@functionname@number。 它们均不改变输出函数名中的字符⼤⼩写,这和PASCAL调⽤约定不同,PASCAL约定输出的函数名⽆任何修饰且全部⼤写。 b、C++编译时函数名修饰约定规则: __stdcall调⽤约定: 1. 以“?”标识函数名的开始,后跟函数名; 2. 函数名后⾯以“@@YG”标识参数表的开始,后跟参数表; 3. 参数表以代号表⽰: X--void, D--char, E--unsigned char, F--short, H--int, I--unsigned int, J--long, K--unsigned long, M--float, N--double, _N--bool, .... PA--表⽰指针,后⾯的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,⼀个“0”代表⼀次重复; 4、参数表的第⼀项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前; 5、参数表后以“@Z”标识整个名字的结束,如果该函数⽆参数,则以“Z”标识结束。 其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如 int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z” void Test2()-----“?Test2@@YGXXZ” __cdecl调⽤约定: 规则同上⾯的_stdcall调⽤约定,只是参数表的开始标识由上⾯的“@@YG”变为“@@YA”。 __fastcall调⽤约定: 规则同上⾯的_stdcall调⽤约定,只是参数表的开始标识由上⾯的“@@YG”变为“@@YI”。 VC++对函数的省缺声明是“__cedcl“,将只能被C/C++调⽤.注意: 1、_beginthread需要__cdecl的线程函数地址,_beginthreadex和CreateThread需要__stdcall的线程函数地址。C++修饰 2、⼀般WIN32的函数都是__stdcall。⽽且在Windef.h中有如下的定义: #define CALLBACK __stdcall #define WINAPI __stdcall 3、extern "C" _declspec(dllexport) int __cdecl Add(int a, int b); typedef int (__cdecl*FunPointer)(int a, int b); 修饰符的书写顺序如上。 4、extern "C"的作⽤:如果Add(int a, int b)是在c语⾔编译器编译,⽽在c++⽂件使⽤,则需要在c++⽂件中声明:extern "C" Add(int a, int b),因为c编译器和c++编译器对函数名的解释不⼀样(c++编译器解释函数名的时候要考虑函数参数,这样是了⽅便函数重载,⽽在c语⾔中不存在函数重载的问题),使⽤extern "C",实质就是告诉c++编译器,该函数是c库⾥⾯的函数。如果不使⽤extern "C"则会出现链接错误。 ⼀般象如下使⽤: #ifdef _cplusplus #define EXTERN_C extern "C" #else #define EXTERN_C extern #endif #ifdef _cplusplus extern "C"{ #endif EXTERN_C int func(int a, int b); #ifdef _cplusplus } #endif 5、MFC提供了⼀些宏,可以使⽤AFX_EXT_CLASS来代替__declspec(DLLexport),并修饰类名,从⽽导出类,AFX_API_EXPORT来修饰函数,AFX_DATA_EXPORT来修饰变量 AFX_CLASS_IMPORT:__declspec(DLLexport) AFX_API_IMPORT:__declspec(DLLexport) AFX_DATA_IMPORT:__declspec(DLLexport) AFX_CLASS_EXPORT:__declspec(DLLexport) AFX_API_EXPORT:__declspec(DLLexport) AFX_DATA_EXPORT:__declspec(DLLexport) AFX_EXT_CLASS:#ifdef _AFXEXT AFX_CLASS_EXPORT #else AFX_CLASS_IMPORT 6、DLLMain负责初始化(Initialization)和结束(Termination)⼯作,每当⼀个新的进程或者该进程的新的线程访问DLL时,或者访问DLL的每⼀个进程或者线程不再使⽤DLL或者结束时,都会调⽤DLLMain。但是,使⽤TerminateProcess或TerminateThread结束进程或者线程,不会调⽤DLLMain。 7、⼀个DLL在内存中只有⼀个实例 DLL程序和调⽤其输出函数的程序的关系: 1)、DLL与进程、线程之间的关系 DLL模块被映射到调⽤它的进程的虚拟地址空间。 DLL使⽤的内存从调⽤进程的虚拟地址空间分配,只能被该进程的线程所访问。 DLL的句柄可以被调⽤进程使⽤;调⽤进程的句柄可以被DLL使⽤。 DLLDLL可以有⾃⼰的数据段,但没有⾃⼰的堆栈,使⽤调⽤进程的栈,与调⽤它的应⽤程序相同的堆栈模式。 2)、关于共享数据段 DLL定义的全局变量可以被调⽤进程访问;DLL可以访问调⽤进程的全局数据。使⽤同⼀DLL的每⼀个进程都有⾃⼰的DLL全局变量实例。如果多个线程并发访问同⼀变量,则需要使⽤同步机制;对⼀个DLL的变量,如果希望每个使⽤DLL的线程都有⾃⼰的值,则应该使⽤线程局部存储(TLS,Thread Local Strorage)。5.7 far near huge指针 near 16位指针 far 32位指针 huge 综合四、 虚拟内存1. 介绍1. ⼀个程序不能访问另外⼀个程序的地址指向的空间.2. 每个程序的开始地址0x800840003. 程序中使⽤的地址不是物理,⽽是逻辑地址(虚拟内存)。逻辑地址仅仅是编号,编号使⽤int 4字节整数表⽰ 4294967296 每个程序提供了4G的访问能⼒4. 逻辑地址与物理地址关联才有意义:映射过程称为内存映射.5. 虚拟内存的提出:禁⽌⽤户直接访问物理存储设备,有助于系统的稳定。虚拟地址与物理地址映射的时候有⼀个基本单位:【 4k=1000(16进制) =内存页】.段错误:⽆效访问,访问的虚拟内存地址没有映射到实际物理内存上。合法访问: ⽐如malloc分配的空间之外的空间可以访问,虽然不会有段错误但访问⾮法。2. 虚拟内存的分配栈上的数据:编译器⾃动⽣成代码维护堆上的数据:地址是否映射,映射的空间是否被管理。1)brk/sbrk 内存映射函数补充:帮助⼿册man + 节 + 关键字1-8节:不指定节会⾃动搜索1 = Linux系统(shell)指令2 = 系统函数3 = 标准C函数4 = 特殊⽂件,例如设备和驱动程序5 = ⽂件格式,包括配置⽂件,⽹络服务列表,可⽤的shell列表等6 = 游戏和屏幕保护程序7 = 系统编程帮助8 = 系统管理命令,超级⽤户可能需要⽤到它们。int brk(void *end);
移动虚拟内存指针的绝对位置如果新位置(地址)⼤于当前位置(地址 ),则地址拓展部分被分配空间,可以使⽤。如果新位置(地址)⼩于当前位置(地址 ),则地址覆盖部分被擦除,不可以使⽤。void *sbrk(int size);
返回当前地址位置。如果size 等于 0;返回当前地址,系统会分配⼀个分页(4k)的空⽩空间,此时此时没有映射到物理内存;如果size 不等于 0,先返回当前地址,再把地址偏移size⼤⼩,此时此时映射到物理内存,⼤⼩等于size。原笔记内容 应⽤: 1.使⽤sbrk分配空间 2.使⽤sbrk得到没有映射的虚拟内存⾸地址. 3.使⽤brk分配空间 4.使⽤brk释放空间 理解: sbrk(int size)sbrk与brk后台系统维护⼀个指针,指针默认是null。调⽤sbrk,判定指针是否是0。是:得到⼤块空闲空间的⾸地址初始化并返回指针;否:先返回指针地址,再把指针位置+size。注意:如果是第⼀次运⾏该函数,会有如下区别 1. size==0?。为0,则直接返回⾸地址,但内存页没有映射到物理内存;否,完成⼀个内存页的虚拟内存到物理内存的映射,但只是完成size⼤⼩的内存空间,此时在⼀页(1024字节,即4k)内可以越界访问,不会有段错误,但是会有危险。 2. sbrk 与 brk 本质上市⼀样的,区别在于移动相对位置和绝对位置。测试代码// break.c#include #include #include int main(){ printf("p1=%pn", sbrk(0)); printf("p2=%pn", sbrk(0)); printf("p3=%pn", sbrk(0)); printf("p4=%pn", sbrk(32)); printf("p5=%pn", sbrk(0)); printf("p6=%pn", sbrk(0)); printf("p7=%pn", sbrk(0)); printf("%dn", getpid()); getchar(); return 0;}// break2.c#include #include #include int main(){ int * p1 = sbrk(0); int * p2 = sbrk(0); int * p3 = sbrk(0); int * p4 = sbrk(32); int * p5 = sbrk(0); int * p6 = sbrk(0); int * p6 = sbrk(0); int * p7 = sbrk(0); printf("p1=%pn", p1); printf("p2=%pn", p2); printf("p3=%pn", p3); printf("p4=%pn", p4); printf("p5=%pn", p5); printf("p6=%pn", p6); printf("p7=%pn", p7); printf("%dn", getpid()); getchar(); return 0;}// break3.c#include #include #include int main(){ printf("p1=%pn", (int*)sbrk(0)); printf("p2=%pn", (int*)sbrk(0)); printf("p3=%pn", (int*)sbrk(0)); printf("p4=%pn", (int*)sbrk(32)); printf("p5=%pn", (int*)sbrk(0)); printf("p6=%pn", (int*)sbrk(0)); printf("p7=%pn", (int*)sbrk(0)); printf("%dn", getpid()); getchar(); return 0;}运⾏结果,⼏次运⾏结果不⼀样,不知道为什么。
练习代码#include #include int isPrimer(int a){
int i; for(i=2;i } } return 0;}main(){ int i=2; int b; int *r; int *p; p=sbrk(0); r=p; for(;i<100;i++) { b=isPrimer(i); if(b==0) { brk(r+1); *r=i; r=sbrk(0); } }
i=0; r=p; while(r!=sbrk(0)) { printf("%dn",*r); r++; } brk(p);//释放内存
} 总结: 智能指针 stl new malloc brk / sbrk 异常处理 int brk(void*) void *sbrk(int); 如果成功.brk返回0、sbrk返回指针,失败 brk返回-1、sbrk返回(void*)-1 Unix函数错误,修改内部变量: errno 字符串函数 string.h cstring 内存管理函数 malloc memset mamcmp memcpy bzero 等等 错误处理函数
标准IO函数
时间函数
类型转换函数
#include #include #include #include #include extern int errno;int main(){ void *p=sbrk(1000000000*2); if(p==(void*)-1) { //C语⾔⼏种错误打印⽅式 //perror("Hello:"); //printf("Memory:%mn"); printf("::%sn",strerror(errno)); }
}
发布者:admin,转转请注明出处:http://www.yc00.com/news/1687424815a9088.html
评论列表(0条)