C语言学习_内存分区

qingsongzdq 2019-06-28

1.1 数据类型
  • 数据类型的本质:固定内存大小的别名
  • 数据类型的作用:编译器预算对象(变量)分配的内存空间大小

    int a;  // 告诉编译器分配4个字节的内存
  • 数据类型可以通过typedef起别名
  • 数据类型可以通过sizeof()测类型大小
  • void数据类型(无类型,万能类型)

    • 如果函数没有返回值,必须用void修饰

      // 对函数返回的限定
      void fun(int a);
    • 如果函数没有参数,参数可以用void修饰

      // 对函数参数的限定
      int fun(void);
    • 不能定义void类型的普通变量

      void a; // error,不能确定分配内存空间的大小
    • 万能指针

      void * p; // ok, 万能指针,指针类型都是4个字节,函数参数,函数有返回值
      //1. void* 可以指向任何类型的数据,被称为万能指针
      void test03()
      {
        int a = 10;
        void* p = NULL;
        p=&a;
        printf("a:%d\n",*(int*)p);
        char c = 'a';
        p=&c;
        printf("c:%c\n",*(char*)p);
      }
      //2. void* 常用于数据类型的封装
      void test04()
      {
        void * memcpy(void * _Dst, const void * _Src, size_t _Size);
      }
  • sizeof操作符注意事项

    • sizeof返回的占用空间大小是为这个变量开辟的大小,而不只是它用到的空间。所以对结构体用的时候,大多情况下就得考虑字节对齐的问题了;
    • sizeof返回的数据结果类型是unsigned int
    • 要注意数组名和指针变量的区别。通常情况下,我们总觉得数组名和指针变量差不 多,但是在用sizeof的时候差别很大:

      • 对数组名用sizeof返回的是整个数组的大小
      • 对指针变量进行操作的时候返回的则是指针变量本身所占得空间,在32位机的条件下一般都是4。
      • 当数组名作为函数参数时,在函数内部,形参也就是个指针,所以不再返回数组的大小

示例代码:

//1. sizeof基本用法
void test01()
{
  int a = 10;
  printf("len:%d\n", sizeof(a));
  printf("len:%d\n", sizeof(int));
  printf("len:%d\n", sizeof a);
}

//2. sizeof 结果类型
void test02()
{
  unsigned int a = 10;
  if (a - 11 < 0)
  {
    printf("结果小于0\n");
  }
  else
  {
    printf("结果大于0\n");
  }
  int b = 5;
  if (sizeof(b) - 10 < 0)
  {
    printf("结果小于0\n");
  }
  else
  {
    printf("结果大于0\n");
  }
}

//3. sizeof 碰到数组
void TestArray(int arr[])
{
  printf("TestArray arr size:%d\n",sizeof(arr));
}
void test03()
{
  int arr[] = { 10, 20, 30, 40, 50 };
  printf("array size: %d\n",sizeof(arr));

  //数组名在某些情况下等价于指针
  int* pArr = arr;
  printf("arr[2]:%d\n",pArr[2]);
  printf("array size: %d\n", sizeof(pArr));

  //数组做函数函数参数,将退化为指针,在函数内部不再返回数组大小
  TestArray(arr);
}
1.2 变量
1.2.1 变量的概念

​既能读又能写的内存对象,称为变量;

若一旦初始化后不能修改的对象则称为常量。

变量定义的形式:类型 标识符,标识符,…,标识符
1.2.2 变量的本质
  • 变量名的本质:一段连续内存空间的别名;
  • 程序通过变量来申请和命名内存空间int a = 0
  • 通过变量名访问内存空间;
  • 不是向变量名读写数据,而是向变量所代表的内存空间中读写数据;
  • 变量的三要素:名称、大小、作用域
  • 变量的生命周期:结合内存四区模型来看

修改变量的两种方式:

void test()
{
    int a = 10;

    //1. 直接修改
    a = 20;
    printf("直接修改,a:%d\n",a);

    //2. 间接修改
    int* p = &a;
    *p = 30;

    printf("间接修改,a:%d\n", a);
}
1.3 程序的内存四区模型
1.3.1 内存分区
  • 程序运行之前:

    C程序编译过程

    1)预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法

    2)编译:检查语法,将预处理后文件编译生成汇编文件

    3)汇编:将汇编文件生成目标文件(二进制文件)

    4)链接:将目标文件链接为可执行程序

    可执行程序内部已经分好3段信息,分别为代码区text、数据区data和未初始化数据区bss 3个部分(有些人直接把databss合起来叫做静态区或全局区)。

    总体来讲说,程序源代码被编译之后主要分成两种段:程序指令(代码区)和程序数据(数据区)。代码段属于程序指令,而数据域段和.bss段属于程序数据。

  • 程序运行之后:

    程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,操作系统把物理硬盘程序load(加载)到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区堆区

总的来说,内存分区模型:

代码区:可执行代码段,不可修改。

未初始化数据区(BSS):全局未初始化,静态未初始化数据,数据的生存周期为整个程序运行过程 。

全局初始化数据区/静态数据区(data segment):全局初始化,静态初始化数据,文字常量(只读),数据的生存周期为整个程序运行过程。

栈区(stack):先进后出的内存结构,由编译器自动分配释放 ,存放函数的参数值、返回值、局部变量等

堆区(heap):容量要远远大于栈,用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。

1.3.2 分区模型
  • 栈区

    由系统进行的内存管理,主要存放函数的参数以及局部变量,函数完成后系统自动释放。

    #char* func()
    {
      char p[] = "hello world!"; //在栈区存储 乱码
      printf("%s\n", p);
      return p;
    }
    void test()
    {
      char* p = NULL;
      p=func();  
      printf("%s\n",p);
    }
  • 堆区

    手工申请,手工释放

    char* func()
    {
      char* str = malloc(100);
      strcpy(str, "hello world!");
      printf("%s\n",str);
      return str;
    }
    
    void test01()
    {
      char* p = NULL;
      p=func();
      printf("%s\n",p);
    }
    
    void allocateSpace(char* p)
    {
      p=malloc(100);
      strcpy(p, "hello world!");
      printf("%s\n", p);
    }
    
    void test02()
    {
      char* p = NULL;
      allocateSpace(p);
    
      printf("%s\n", p);
    }

    C语言学习_内存分区

    堆分配内存API:

    void *calloc(size_t nmemb, size_t size);
    /*
    * 功能:
    * 在内存动态存储区中分配nmemb块长度为size字节的连续区域。calloc自动将分配的内存置0。
    * 参数:
    * nmemb:所需内存单元数量
    * size:每个内存单元的大小(单位:字节)
    * 返回值:
    *  成功:分配空间的起始地址
    *  失败:NULL
    */
  • 全局/静态区

    全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储全局变量静态变量常量

    int v1 = 10;        //全局/静态区
    const int v2 = 20;  //常量,一旦初始化,不可修改
    static int v3 = 20; //全局/静态区
    char *p1;           //全局/静态区,编译器默认初始化为NULL
    
    //那么全局static int 和 全局int变量有什么区别?
    
    void test()
    {
      static int v4 = 20; //全局/静态区
    }
    
    // 加深理解
    char* func()
    {
      static char arr[] = "hello world!"; //在静态区存储 可读可写
      arr[2] = 'c';
      char* p = "hello world!"; //全局/静态区-字符串常量区 
      //p[2] = 'c'; //只读,不可修改 
      printf("%d\n",arr);
      printf("%d\n",p);
      printf("%s\n", arr);
      return arr;
    }
    void test()
    {
      char* p = func();
      printf("%s\n",p);
    }

总结:

数据区包括:堆,栈,全局/静态存储区。
全局/静态存储区包括:常量区,全局区、静态区。
常量区包括:字符串常量区、常变量区。
代码区:存放程序编译后的二进制代码,不可寻址区。

可以说,C/C++内存分区其实只有两个,即代码区和数据区。

1.3.3 函数调用模型
int func(int a,int b)
{
  int t_a = a;
  int t_b = b;
  return t_a + t_b;
}

int main()
{
  int ret = 0;
  ret = func(10, 20);
  return EXIT_SUCCESS;
}

C语言学习_内存分区

1.3.4 栈的生长方向和内存存放方向

C语言学习_内存分区

//1. 栈的生长方向
void test01()
{

  int a = 10;
  int b = 20;
  int c = 30;
  int d = 40;

  printf("a = %d\n", &a);
  printf("b = %d\n", &b);
  printf("c = %d\n", &c);
  printf("d = %d\n", &d);

  //a的地址大于b的地址,故而生长方向向下
}

//2. 内存生长方向(小端模式)
void test02()
{
  //高位字节 -> 地位字节
  int num = 0xaabbccdd;
  unsigned char* p = &num;

  //从首地址开始的第一个字节
  printf("%x\n",*p);
  printf("%x\n", *(p + 1));
  printf("%x\n", *(p + 2));
  printf("%x\n", *(p + 3));
}

相关推荐