This page looks best with JavaScript enabled

C 语言学习笔记 - 指针和数组

 ·  ☕ 6 min read  ·  ✍️ 鱼子盖饭 · 👀... views
  1. C 语言中的数组名代表存放数组元素的连续存储空间的首地址,即指向数组中第一个元素的指针常量

  2. 数组的下标运算符 [] 实际上就是以指针作为其操作数的,例如 a[i] 被编译器解释为表达式 *(a+i)

  3. 指向整型数据的指针变量 p,并使其值为数组 a 的首地址,则:p+1p++ 本质上是两个不同的操作,虽然二者都对指针变量 p 进行加 1 运算,但 p+1 并不改变当前指针的指向p 仍然指向原来指向的元素;而 p++ 相当于执行 p=p+1,因此 p++ 操作改变了指针 p指向,表示将指针变量 p 向前移动了一个元素位置,即指向了下一个元素。此外,p++ 并非将指针变量 p 的值简单的加 1,而是加上 1*sizeof(基类型) 个字节;

  4. 指针也可以用下标表示:

    1
    2
    
    scanf("%d", &p[i]);  /* &p[i] 等价于 p+i */
    printf("%4d", p[i]);  /* p[i] 等价于 *(p+i) */
    
  5. 二维数组 a, a 为数组名,代表其第一个元素 a[0]地址&a[0]),其元素仍然为地址,而非具体的数据值;

  6. a[i][j] 的四种表示形式:

    • a[i][j]
    • *a[i]+j
    • *(*(a+i)+j)
    • (*(a+i))[j]
  7. 如下代码定义了一个可指向含有 4 个元素的以为整形数组的指针变量

    1
    
    int (*p)[4]
    
  8. 定义指针 p

    1
    
    int *p; p = a[0];
    

    为了能通过 p 引用二维数组 a 的元素 a[i][j],可将数组 a 看成一个由(m 行 * n 列)个元素组成的一维数组。由于 p 代表数组的第 0 行第 0 列寻址到数组的第 i 行第 j 列,中间需跳过 i*n+j 个元素,因此 p+i*n+j 代表数组的第 i 行第 j 列的地址,即 &a[i][j]。此时不能用 p[i][j] 来表示数组元素,因为此时是将二维数组等同于一维数组看待的;

  9. 在定义和使用二维数组的行指针时,必须显式地指定其所指向的一维数组的长度(即二维数组的列数),且不能用变量指定。因为在执行增 1(或减 1 )操作时,每次移动的字节数为:二维数组的列数 * 数组的基类型所占的字节数,如果不指定列数,则无法计算指针移动的字节数;

  10. 二维数组作函数形参时,必须显式地指定数组第 2 维(列)的长度;

  11. 指向数组的指针是一个指针变量,指针变量中保存的是一个数组的首地址;指针数组是一个数组,只不过是指针作为数组的元素,形成了指针数组;

  12. 定义字符指针数组 pStr

    1
    
    char *pStr[N];
    

    声明了一个有 N 个元素的字符指针数组 pStr,它的每个元素都是一个字符型指针。常量 N 规定了指针数组的长度,关键字 char 代表指针数组元素可指向的数据类型为字符型;

  13. 在使用指针数组之前必须对数组元素进行初始化

  14. 用二维数组对指针数组初始化之后,对指针数组排序,结果只改变了原来指针数组的元素指向,并不改变二维数组的排列顺序;

  15. 相对于使用二维数组实现物理排序的方法而言,使用指针数组实现索引排序的程序执行效率相对较高;

  16. 用于保存函数调用时的返回地址、函数的形参、局部变量及 CPU 的当前状态等程序运行信息是一个自由存储区,可利用 C 的动态内存分配函数来使用它;

  17. 变量的内存分配方式:

    • 从静态存储区分配全局变量静态变量在程序编译时就已经分配好了,仅在程序终止前,才被系统收回
    • 在栈上分配执行函数调用时局部变量形参分配内存,函数执行结束时释放。如果往栈中压入的数据超出预先给栈分配的容量,则会出现栈溢出
    • 从堆上分配程序运行期间用动态内存分配函数来申请的内存。调用 free() 释放已不再使用的内存;
  18. 指针的增 1 和减 1 运算速度很快,所以用指针变量来寻址数组元素可提高程序的执行效率;

  19. malloc()

    函数 malloc() 用于分配若干字节的内存空间,返回一个指向该内存首地址的指针。若系统不能提供足够的内存单元,函数将返回空指针 NULL

    1
    
    void *malloc(unsigned int size);
    

    指向 void 类型的指针:通用指针/无类型的指针,即类型未知的指针。用强转的方法将返回的指针强转为所需的类型,然后再进行赋值操作:

    1
    2
    
    int *pi = NULL;
    pi = (int *)malloc(2);
    

    若不能确定某种类型所占内存的字节数,则需使用 sizeof() 计算本系统中该类型所占内存的字节数,有利于提高程序的可移植性:

    1
    
    pi = (int *)malloc(sizeof(int));
    
  20. calloc()

    函数 calloc() 用于给若干同一类型的数据项分配连续的存储空间并赋值为 0:

    1
    
    void *calloc(unsigned int num, unsigned int size);
    

    调用成功则返回一个指向 void 类型的连续存储空间的首地址,否则返回空指针 NULL

    1
    2
    
    float *pf = NULL;
    pf = (float *)calloc(10, sizeof(float));
    

    安全角度考虑,使用 calloc() 更明智,calloc() 能自动将分配的内存初始化为 0

  21. free()

    函数 free() 释放向系统动态申请的由 p 指向的存储空间:

    1
    
    void free(void *p);
    

    形参 p 的地址只能是由 malloc()calloc() 申请内存时返回的地址

  22. realloc()

    函数 realloc() 改变原来分配的存储空间的大小:

    1
    
    void *realloc(void *p, unsigned int size);
    

    将指针 p 所指向的存储空间的大小改为 size 个字节,函数返回值是新分配的存储空间的首地址,与原来分配的首地址不一定相同

  23. 一旦改变了指针的指向,原来分配的内存及数据也就随之丢失了;

  24. 如果动态内存分配后返回的指针为 NULl,那么就说明内存分配为成功,必须调用 exit(1) 终止整个程序的执行。主要是为了避免使用空指针,空指针意味着它不指向任何对象,使用空指针将会导致系统崩溃;

  25. 常见的内存异常错误可分为非法内存访问错误因持续的内存泄漏导致系统内存不足

  26. 内存分配未成功就使用:

    在使用内存之前检查一下指向它的指针是否为空指针 NULL 即可

  27. 内存分配成功了,但是尚未初始化就使用:

    无论数组是以何种方式创建的,都不要忘记给它赋初值,即使是赋零值也不要省略。对于用 malloc()calloc() 动态分配的内存,最好用函数 memset() 进行清零操作。对于指针变量,即使后面有对其进行赋初值的语句,也最好在定义时就将其初始化为 NULL

  28. 内存发生了越界使用

  29. 忘记了释放内存,造成了内存泄漏

    向系统申请的动态内存是不会被自动释放的,应及时释放不再使用的内存。因为对于包含这类错误的函数,只要它被调用一次,就会丢失一块内存

  30. 降低内存泄漏错误发生概率的一般方法:

    • 仅在需要时才使用 malloc()
    • malloc()free() 集中在一个函数内,一个在入口处,一个在出口处
    • 如果 malloc()free() 无法集中在一个函数中,那么就要分别单独编写申请内存和释放内存的函数,然后使其配对使用
    • 重复利用 malloc() 申请到的内存
  31. 当前面的 malloc() 调用成功但后面的调用不成功时,直接退出函数将导致前面已分配的内存未被释放:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    void Init(void)
    {
        char *pszMyName = NULL, *pszHerName = NULL, *pszHisName = NULL;
        pszMyName = (char *)malloc(256);
        if (pszMyname == NULL)return;
        pszHerName = (char *)malloc(256);
        if (pszHerName == NULL)return;
        pszHisName = (char *)malloc(256);
        if (pszHisName == NULL)return;
        ...
        free(pszMyName);
        free(pszHerName);
        free(pszHisName);
        return;
    }
    

    修改如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    void Init(void)
    {
        char *pszMyName = NULL, *pszHerName = NULL, *pszHisName = NULL;
        pszMyName = (char *)malloc(256);
        if (pszMyName == NULL)  return;
        pszHerName = (char *)malloc(256);
        if (pszHerName == NULL)
        {
            free(pszMyName);
            return;
        }
        pszHisName = (char *)malloc(256);
        if (pszHisName == NULL)
        {
            free(pszMyName);
            free(pszHerName);
            return;
        }
        ...
        free(pszMyName);
        free(pszHerName);
        free(pszHisName);
        return;
    }
    

    以上代码可通过 goto 语句简化

  32. 释放内存后依然继续使用:

    编译时会有如下警告:

    function returns address of local variable
    

    函数内定义的动态局部变量是在栈上创建内存的,在函数调用结束后就被自动释放了,被释放了的内存是不能再继续使用的,因为释放后的内存中的数据将变成随机数

  33. 当指针指向的栈内存被释放以后,指向它的指针并未消亡;

  34. 形参不能返回函数中动态分配的内存的首地址给实参,这样实参仍为空指针。但利用 return 语句可返回动态分配的内存首地址给主调函数,这是因为动态分配的内存不会在函数调用结束后被自动释放,必须使用 free() 才能释放;

  35. 缓冲区溢出通常是因为 gets() scanf() strcpy() 等函数未对数组越界加以监视和限制,导致有用的堆栈数据被覆盖而引起的;

  36. 使用 strncpy() strncat() 等“n 族”字符处理函数,通过增加要给参数来限制字符串处理的最大长度,可防止发生缓冲区溢出。


鱼子盖饭
WRITTEN BY
鱼子盖饭
Get into trouble, make mistakes.


What's on this Page