Linux 学习iOS基础知识整理E_C/C++

C语言-内存管理基础

2017-01-07  本文已影响2057人  老板娘来盘一血

前言

C语言作为一门应用途广泛、功能强大、使用灵活的面向过程式编程语言。既可用于编写应用软件,又能用于编写系统软件。所以深入理解C语言的内存管理能够加深我们对程序的理解,有助于开发出更高质量的应用。本文就是笔者在学习C语言过程中对其内存管理的学习总结,分享出来希望能给正在学习C、Objective-C、C++等语言的小伙伴们一点启发。

内存定义

内存单位和编址

内存组成

对于一个由C语言编写的程序而言,内存主要可以分为以下5个部分组成:

C语言程序内存组成图
其中需要注意的是:代码段、数据段、BSS段在程序编译期间由编译器分配空间,在程序启动时加载,由于未初始化的全局变量存放在BSS段,已初始化的全局变量存放在数据段,所以程序中应该尽量少的使用全局变量以节省程序编译和启动时间;栈和堆在程序运行中由系统分配空间。下面简单介绍一下各个组成部分具体含义,重点介绍堆、栈。
#include <stdio.h>
int main(int argc, const char * argv[]) {
    int a = 100;
    int b = 100;
    printf("%p \n",&a); // 0x7fff5fbff79c
    printf("%p \n",&b); // 0x7fff5fbff798
    return 0;
}   
// a 变量的地址 0x7fff5fbff79c 比 b 变量的地址 0x7fff5fbff798 要大 

最后栈还具有“小内存、自动化、可能会溢出”的特点。栈顶的地址和栈的最大容量一般是系统预先规定好的,通常不会太大。由于栈中主要存放的是局部变量,而局部变量的占用的内存空间是其所在的代码段或函数段结束时由系统回收重新利用,所以栈的空间是循环利用自动管理的,一般不需要人为操作。如果某次局部变量申请的空间超过栈的剩余空间时就有可能出现 “栈的溢出”,进而导致意想不到的后果。所以一般不宜在栈中申请过大的空间,比如长度很大的数组、递归调用重复次数很多的函数等等。

#include <stdio.h>
int a = 0;                  // 全局初始化区
char p1;                    // 全局未初始化区
int main(int argc, const char * argv[]) {
      int b ;                 // 栈
      char s[] = "abc";       // 栈
      char p2 ;               // 栈
      char p3 = "123456";     // 123456在常量区,p3在栈上。
      static int c = 0 ;      // 全局(静态)初始化区
      p1 = (char )malloc(10); // 分配的10字节的区域就在堆区
      p2 = (char )malloc(20); // 分配的20字节的区域就在堆区
      printf("%p\n",p1);      // 0xffffffb0
      printf("%p\n",p2);      // 0xffffffc0
      return 0;                
//p1 变量的地址 0xffffffb0 比 p2 变量的地址 0xffffffc0 要小
}

C语言操作堆内存的函数

C语言中申请和释放堆内存有四个函数,分别是:

    int *a = malloc(4);  //申请4个字节的空间用于存放一个int类型的值
    char *b = malloc(2);  //申请2个字节的空间用于存放一个char类型的值
    int *c = calloc(10, sizeof(int)); 申请10个sizeof(int) 字节的空间
    char *d = calloc(2, sizeof(char)); 申请10个sizeof(char) 字节的空间
    char *d = calloc(2, sizeof(char));  //申请2个sizeof(char) 字节的空间
    char *f = realloc(d, 5 * sizeof(char));  //将原来变量d指向的2个sizeof(char) 字节的空间更改到5个sizeof(char) 字节的空间并由变量f指向。
    char *g = malloc(sizeof(char)); //申请sizeof(char)大小内存空间
    free(g);      //释放掉g指针指向的内存空间
    g = NULL;     //将g指针指向NULL

free()函数只能释放动态分配的内存,并不能释放任意分配的内存,比如:

    int h[10];    //在栈上申请的10乘以sizeof(int)大小的内存空间
    free(h);      //此处报错:不能释放栈上分配的空间

void 指针类型

在系统提供的内存分配的4个库函数malloccallocrecollocfree中,除了free的返回值为空外,其他三个函数的返回值均为void *类型,由于指针不是本文的重点,所以这里仅仅简单介绍一下关于void指针类型(即void *类型)。
《C语言程序设计》一书中对void指针类型是这样解释的:

C99(C语言的官方标准第二版)允许使用基类型为void的指针类型,可以定义一个基类型为void的指针变量,它不指向任何类型的数据。请注意:不要把 “指向void类型” 理解为能指向 “任何的类型” 的数据,而应该理解为 “指向空类型” 或者 “不指向确定” 的类型的数据。在将它的值赋给另一个指针变量时由系统对它进行类型转换,使之适合被赋值变量的类型。

例如下面的代码:

int main(int argc, const char * argv[]) {
    int a = 3;                   //定义a为整型变量
    int *p1 = &a;                //p1指向 int 型变量
    char *p2;                    //p2指向 char 型变量
    void *p3;                    //p3为无类型指针变量
    p3 = (void *)p1;             //将p1的值转换为void *类型,然后赋值给p3
    p2 = (char *)p3;             //将p3的值转换为char *类型,然后赋值给p2
    printf("%d\n", *p1);         //输出a的值 3
    p3 = &a;                    
    printf("%d", *p3);           //此处报错,p3无指向,不能指向a 
    return 0;
}

C99标准把上述三个函数的基类型定义为void类型,这种指针称之为无类型指针,即不指向哪一种具体的类型数据,只表示用来指向一个抽象类型的数据,仅仅提供一个纯地址,不能指向任何具体的对象。

变量的存储方式

resultAresultB都是全局变量,但是作用范围不同,在add()sub()main()函数中都可以使用resultA,但是add()不能使用resultB变量。

int main(int argc, const char * argv[]) {
    int a = 1;
    int b = 2;
    printf("%p\n",&a);  //0x7fff5fbff79b
    printf("%p\n",&b);  //0x7fff5fbff797
    return 0;
}
int型变量内存分配
局部变量存储细节:由于是a、b是临时变量,因此他们的内存空间分配在栈上,栈中内存寻址由高到低,所以 a 变量的地址比 b 变量的地址要大,其次由于是在64位编译环境中,int 型变量占据4个字节的空间,每一个字节由低到高依次对应着8位二进制数,四个8位二进制数就是十进制中的 1 或 2,而变量a、b的地址就是四个字节中最小值的内存地址。
全局变量存储细节:关于全局变量存储在前面介绍内存组成已经说明,这里不再赘述。
变量的存储类别:C的存储类别包括4种:auto(自动的)static(静态的)register(寄存器的)extern(外部的)。根据变量的存储类别可以得知其作用域和生命周期。

数组的存储

C语言中数组的存储和普通的变量不太一样,数值中存储的元素,是从所占用的低地址开始存储的。例如:

int main(int argc, const char * argv[]) {
    char chars[4] = {'l','o','v','e'};
    printf("chars[0] = %p\n",&chars[0]); //0x7fff5fbff79b
    printf("chars[1] = %p\n",&chars[1]); //0x7fff5fbff79c
    printf("chars[2] = %p\n",&chars[2]); //0x7fff5fbff79d
    printf("chars[3] = %p\n",&chars[3]); //0x7fff5fbff79e
    return 0;
}
字符数组的存储示意图

仅仅通过上面的字符数组例子还不能完全说明数组在内存中关于其元素存储和元素中值的存储关系。如果换用一个整型数组就能看出一些差别。

int main(int argc, const char * argv[]) {
    int nums[2] = {5, 6};
    printf("nums[0] = %p\n",&nums[0]); // 0x7fff5fbff7a0
    printf("nums[1] = %p\n",&nums[1]); // 0x7fff5fbff7a4
    return 0;
}
整型数组内存分配
通过字符数组和整型数组内存分配示意图可以看出:数组中的元素按照存放顺序依次从低地址到高地址存放,但是每个元素中的内容又是按高地址向低地址方向存储
数组在使用过程中遇到的最多的问题可能就是下标越界,下面的代码就是越界访问数组示例:
int main(int argc, const char * argv[]) {
    char charsOne[2] = {'a', 'b'};
    char charsTwo[3] = {'c', 'd', 'e'};
    charsTwo[3] = 'f';
    printf("charsOne[0] = %p\n",&charsOne[0]); // 0x7fff5fbff79e
    printf("charsTwo[0] = %p\n",&charsTwo[0]); // 0x7fff5fbff79b
    printf("charsOne[0] = %c\n",charsOne[0]); // f
    return 0;
}
数组下标越界示意图
结合下标越界示意图看上面的的代码会发现,由于越界设置charsTwo[3]元素的值,导致变相更改了charsOne[0]的值。
int main(int argc, const char * argv[]) {
    char charsOne[2] = {'a', 'b'};
    char charsTwo[3] = {'c', 'd', 'e'};
    charsOne[-1] = 'g';
    printf("charsTwo[2] = %c\n",charsTwo[2]); // g
    return 0;
}
下标越界示意图
同样的道理,由于越界访问charsOne[-1]元素,但是charsOne并不存在这个元素,编译器会向上寻找到charsTwo [2]的位置,导致变相更改了charsTwo[2]的值。

通过数组在内存中的分配图能够更好帮助我们理解数组越界问题的严重性,在开发中使用数组首先应该考虑的就是数组长度问题,尽量不要出现类似低级错误。

总结

以上就是笔者对于C语言内存管理基本概念的学习心得,非常的基础和入门,在接下来的文章中还可能会深入探讨关于“一般函数调用”、“递归函数调用”、“结构体和指针”等在C语言中内存的分配。

如果文中有任何纰漏或错误欢迎在评论区留言指出,本人将在第一时间修改过来;喜欢我的文章,可以关注我以此促进交流学习; 如果觉得此文戳中了你的G点请随手点赞;转载请注明出处,谢谢支持。

下一篇:C语言-内存管理深入

上一篇下一篇

猜你喜欢

热点阅读