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<stdlib.h>

              c++ :new

4. 数据结构

              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  <stdio.h>
#include <unistd.h>

int main()
{
    printf("%d\n", getpid());
    getchar();
    return 0;
}

        在第二个终端下cd到根目录下,打开

 可以看到程序运行的内存结构以及各部分的地址分配。

任何程序的内存空间分成4个基本部分

1)代码区

2)全局栈区

3)堆

4)局部栈

5.2 内存数据结构

运行代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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=%p\n", &a1);
    printf("a2=%p\n", &a2);
    printf("a3=%p\n", &a3);
    printf("b1=%p\n", &b1);
    printf("b2=%p\n", &b2);
    printf("b3=%p\n", &b3);
    printf("p1=%p\n", p1);
    printf("mian=%p\n", main);
    printf("func=%p\n", add);

    printf("pid=%d\n", getpid());
    getchar();

    return 0;
}

 可以看到

代码区  ---   存放函数地址 + 全局 const 对象

全局栈区  ---  存放全局变量 + static 修饰对象

局部栈区  ---   存放局部变量 + 局部  const 对象

堆   ---   存放 malloc 对象 + new 对象(这里没举例子)

 5.3 自加载器

        解决代码拷贝到内存中的问题,自加载器负责调用程序文件,找到main函数,拷贝代码到指定位置。

 5.4 malloc 与 new

        用堆分配空间,分配4字节空间占却用16字节(我这边事64位ubuntu 占用32字节)的实际空间:因为堆是靠链表来维护的。链表中不仅仅包含当前分配空间大小的信息,分配的空间/上一个空间数据/下一个空间/空间大小等信息,所以占用空间比分配空间要更大。 

#include <stdio.h>
#include <stdlib.h>

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("%p\n", p1);
    printf("%p\n", p2);
    printf("%p\n", p3);
    printf("%p\n", p4);
    printf("%p\n", p5);

    return 0;
}
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *p1 = new int;
    int *p2 = new int;
    int *p3 = new int;
    int *p4 = new int;
    int *p5 = new int;

    printf("%p\n", p1);
    printf("%p\n", p2);
    printf("%p\n", p3);
    printf("%p\n", p4);
    printf("%p\n", 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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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=%p\n", &a1);
	printf("a2=%p\n", &a2);
	printf("a3=%p\n", &a3);
	printf("p1=%p\n", p1);
	printf("p2=%p\n", p2);
	printf("p3=%p\n", p3);


	printf("pid=%d\n", getpid());
	getchar();

	return 0;
}

         a1、a2、a3在局部栈,p1、p2、p3在堆,观察地址变化和递增大小。

  5.6 函数调用栈空间的分配与释放

#include <stdio.h>
int add(int a,int b)
{
    return a+b;
}

int main()
{
	int (*func)(int) = (int(*)(int))add;
	int r = func(20);
	printf("%d\n", r);

	return 0;
}

1)函数执行的时候有自己的临时栈。

2)函数的参数就在临时栈中,如果函数传递实参,则用来初始化临时参数变量。

3)通过寄存器返回值,(使用返回值返回数据)

4)通过参数返回值,(参数必须是指针),指针指向的区域必须事先分配。

5)如果参数返回指针,参数就是双指针.

__stdcall   __cdecl   __fastcall

1)决定函数栈压栈的参数顺序.

2)决定函数栈的清空方式

3)决定了函数的名字转换方式.

拓展

6.5.2 拓展

__stdcall

__cdecl

__fastcall

参数传递方式

-> 压栈

-> 压栈

左边开始的两个不大于4字节(DWORD)的参数分别放在ECXEDX寄存器,其余的参数仍旧自右向左压栈传送

清理栈方

被调用函数清理(即函数自己清理),多数据情况使用这个

调用者清理

被调用者清理栈

适用

场合

Win API

c/C++MFC默认方式

可变参数的时候使用

速度快

C编译修饰约定(它们均不改变输出函数名中的字符大小写)

约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为

_functionname@number

约定仅在输出函数名前加上一个下划线前缀,格式为_functionname

调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为

@functionname@number

C++

修饰

     1. 修饰名(Decoration name)

          “C”或者“C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出“C++”重载函数、构造函数、析构函数,又如在汇编代码里调用“C””“C++”函数等。

          修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。

     2. 名字修饰约定随调用约定和编译种类(CC++)的不同而变化。函数名修饰约定随编译种类和调用约定的不同而不同,下面分别说明。

          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的线程函数地址,_beginthreadexCreateThread需要__stdcall的线程函数地址。

     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。但是,使用TerminateProcessTerminateThread结束进程或者线程,不会调用DLLMain

     7、一个DLL在内存中只有一个实例

          DLL程序和调用其输出函数的程序的关系:

          1)、DLL与进程、线程之间的关系

               DLL模块被映射到调用它的进程的虚拟地址空间。

               DLL使用的内存从调用进程的虚拟地址空间分配,只能被该进程的线程所访问。

               DLL的句柄可以被调用进程使用;调用进程的句柄可以被DLL使用。

               DLLDLL可以有自己的数据段,但没有自己的堆栈,使用调用进程的栈,与调用它的应用程序相同的堆栈模式。

          2)、关于共享数据段

               DLL定义的全局变量可以被调用进程访问;DLL可以访问调用进程的全局数据。使用同一DLL的每一个进程都有自己的DLL全局变量实例。如果多个线程并发访问同一变量,则需要使用同步机制;对一个DLL的变量,如果希望每个使用DLL的线程都有自己的值,则应该使用线程局部存储(TLSThread Local Strorage)

5.7 far near huge指针

        near       16位指针

        far          32位指针

        huge      综合

四、 虚拟内存

1. 介绍

  • 1. 一个程序不能访问另外一个程序的地址指向的空间.
  • 2. 每个程序的开始地址0x80084000
  • 3. 程序中使用的地址不是物理,而是逻辑地址(虚拟内存)。逻辑地址仅仅是编号,编号使用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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
	printf("p1=%p\n", sbrk(0));
	printf("p2=%p\n", sbrk(0));
	printf("p3=%p\n", sbrk(0));
	printf("p4=%p\n", sbrk(32));
	printf("p5=%p\n", sbrk(0));
	printf("p6=%p\n", sbrk(0));
	printf("p7=%p\n", sbrk(0));

	printf("%d\n", getpid());
	getchar();
	return 0;
}


// break2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 * p7 = sbrk(0);

	printf("p1=%p\n", p1);
	printf("p2=%p\n", p2);
	printf("p3=%p\n", p3);
	printf("p4=%p\n", p4);
	printf("p5=%p\n", p5);
	printf("p6=%p\n", p6);
	printf("p7=%p\n", p7);
	printf("%d\n", getpid());

	getchar();
	return 0;
}


// break3.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
	printf("p1=%p\n", (int*)sbrk(0));
	printf("p2=%p\n", (int*)sbrk(0));
	printf("p3=%p\n", (int*)sbrk(0));
	printf("p4=%p\n", (int*)sbrk(32));
	printf("p5=%p\n", (int*)sbrk(0));
	printf("p6=%p\n", (int*)sbrk(0));
	printf("p7=%p\n", (int*)sbrk(0));

	printf("%d\n", getpid());
	getchar();

	return 0;
}

运行结果,几次运行结果不一样,不知道为什么。 

 

 

              练习代码

#include <stdio.h>
#include <unistd.h>
int isPrimer(int a)
{	
	int i;
	for(i=2;i<a;i++)
	{
		if(a%i==0)
		{
			return 1;			
		}
	}
	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("%d\n",*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 <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
extern int errno;
int main()
{
	void *p=sbrk(1000000000*2);
	if(p==(void*)-1)
	{
		//C语言几种错误打印方式
		//perror("Hello:");
		//printf("Memory:%m\n");
		printf("::%s\n",strerror(errno));
	}	
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值