C中指针总结

C中指针总结

Posted by Anriku on April 18, 2018

C/C++的精髓在于指针这一东西。在今天的这篇博客中,我对指针的一些基本的东西不会进行讲解,毕竟太基本的东西看书是最适合的。在这篇博客中我先对指针的一些稍微难一点的东西进行一下讲解。

主要有:

  • 函数指针
  • 指针和二维数组
  • 指针数组
  • 动态内存分配
  • 指针中常见的问题

函数指针

学习过面向对象程序设计的同学都知道,在面向对象程序设计的一个最大的一个特点之一就是面向抽象编程(也被称为面向接口编程)。这东西可以让你的程序应对各种的变化。简单来说就是一种替换的思想,就像我们为了想玩绝地求生了,但是你寝室的台式机配置不够了,于是你花掉你积蓄“多年”的私房钱买来了不错的CPU和显卡,非常兴奋的换到的你的电脑上,开始快乐的吃鸡之旅了。这个过程你发现了什么?你是不用把整台电脑拿去换掉,而是只用换你需要换的地方。省钱省力,面向抽象编程就是这么一个思想。

说了这么多,现在我们来看看函数指针,并且慢慢来体会我们前面提到的东西:

函数指针就是指向一个函数的指针,这个指针指向的是一个函数的入口地址(函数的第一条指令的地址)。同数组一样,一个函数的函数名(函数的名字,注意不带后面的参数)也就是一个值为函数的源代码在内存中的地址的指针常量

函数指针的定义:

返回值类型 (*函数指针名)(参数列表);

如:
int (*compare)(int a,int b);

函数执行的使用:

//第一种方式(通过解函数指针引用调用)
(*函数指针名)(参数);

如(用上面的示例定义):
(*compare)(a,b);

//第二种方式(直接通过函数指针)
函数指针名(参数);

如:
compare(a,b);

没错,函数指针就是这么简单。然后我们通过函数指针这个东西重要的是来学习一下上面的思想。下面来一个简单的实例进行更加深刻的理解。这是一个通过函数指针来方便的实现升序和降序两种方式的代码:

#include "stdio.h"

void sort(int score[], int n, int (*compare)(int a, int b));

int asc(int a, int b);

int desc(int a, int b);

int main() {
    int i;
    int score[] = {90, 100, 30, 50, 60, 78, 77, 89, 45, 88};
    
    //在这里进行排序的时候只需要传入不同的函数名就能进行不同的排序了
    sort(score, 10, asc);
    for (i = 0; i < 10; i++) {
        printf("%d\n", score[i]);
    }
    return 0;
}

/**
 * 冒泡排序
 * @param score 
 * @param n 
 * @param compare 
 */
void sort(int score[], int n, int (*compare)(int a, int b)) {
    int i, j, flag = 0;

    for (i = 0; i < n - 1 && flag == 0; i++) {
        flag = 1;
        for (j = 0; j < n - 1 - i; j++) {
            if ((*compare)(score[j], score[j + 1])){
                int tmp = score[j];
                score[j] = score[j + 1];
                score[j + 1] = tmp;
                flag = 0;
            }
        }
    }
}

/**
 * 升序函数
 * @param a 
 * @param b 
 * @return 
 */
int asc(int a, int b) {
    return a > b;
}

/**
 * 降序函数
 * @param a 
 * @param b 
 * @return 
 */
int desc(int a, int b) {
    return a < b;
}

指针和二维数组

下面是一张二维数组和指针之间的关系图,其中这是一个a[2][3]的数组

二维数组与指针

通过上面一图我们主要可以得到下面几点:

  • 二维数组名是一个存储二维数组第一个元素地址指针常量(这是一个int数组类型的指针常量)这个其实是叫做行指针,也就是说在这里每次对指针进行加1减1操作的时候地址移动的是下一行或者上一行的地址。具体的在后面会进行更加详细的介绍。
  • a[i]存储第i行首地址的指针常量(这是一个int类型的指针)。也可以通过*(a + i)来获得。这里需要特别注意的是,a的指针所存的地址和a[0]所存的地址都是a[0][0]元素所对应的地址,但是它们是不同的指针类型。a + 1所得到的指针的值是a[1][0]的地址,而a[0] + 1得到的是a[0][0]元素的地址。

由上面我们可以总结出一个获取i + 1行j + 1列元素的等价形式:

a[i][j] <==> *(a[i] + j) <==> *(*(a + i) + j) <==> (*(a + i))[j]

行指针

行指针就是一个指向一个数组的指针:

行指针的形式 数据类型 (*指针名)[数组长度];

//如:
int (*p)[4];

我们以上面的指向int[4]类型的指针来进行优先级分析:

p —> * —> [4] —> int

[]的优先级高于*但由于有圆括号,因此*在前面。然后,从左向右看,首先这是是一个指针,然后接着看这个个指针的基类型是一个int[4]的数组类型。

我们二维数组名比如说上面那个图中的a就是这个类型。

//行指针的赋值
p = a;

然后这是访问二维数组的方式就和a访问二维数组的方式是一样的,我就不重复了。

列指针

列指针就是一个指向一个具体数据类型的指针:

列指针的形式 数据类型 *指针名;

//如:
int *p;

没错列指针就是最简单的指针。

//列指针的赋值,这个时候是可以将二维数组中的任意一个元素进行赋值的。但是如果想要通过这个指针来进行怎个二维数组的访问的时候可以像下面一样进行赋值
p = a[0];

然后这个时候,你就不要把p想成是二维数组的第一个元素的指针了,把它想成是一个有2*3(这个是我们在开头定义那个数组)的一维数组的指针。也就是如果我们要得到a\[1\]\[1\]的值的话,我们就通过\*(p + 1*3 + 1)来进行获取。

指针数组

指针数组就是由若干个基类型相同的指针构成数组。

形式如下:

数据类型 *指针变量名[指针数组长度];

//如:
char *pStr[150];

对上面的列出的例子,我们来进行一下优先级的分析:

pStr —> [150] —> * —> char

从左向右看,首先这是一个容量为150的数组,然后继续这个数组的基类型是一个char类型的指针。

注意:指针数组和前面二维数组中行指针的差别。

其中指针数组的一个很重要的应用就是在对多个字符串进行排序的时候提高排序效率。那么是怎么进行的呢?这里我就不列出具体的代码了,给出一个示意图:

指针数组

可以从上面看到我们的排序并没有去改变二维char数组中字符串的物理位置,而是通过改变指针数组中的指针的指向来进行的排序的。修改指针的值的开销远小于物理位置的交换。

动态内存的分配

首先,我们来看一下C语言中的内存映像:

C语言中的内存映像

其中有三个内存分配方式:

  • 静态存储区分配:

    上图中所说的变量会在程序编译的时候就分配好,并且会始终占据着内存,在程序终止的时候进行回收。

  • 在栈上分配:

    在调用函数的时候,上述的一些内容会在栈上进行分配内存,在函数执行结束后就会自动回收这些内存。其中栈分配运算是内置于处理器的指令集中的,因此效率很高,但是容量是有限的。所以函数调用栈过深会造成栈溢出。

  • 在堆上分配:

    我们通过动态分配函数在堆上进行内存分配。堆上的内存不会自动回收,我们必须要同free函数来进行手动回收。

为什么需要内存分配?

我们知道下面这样根据m的值进行动态的数组分配是行不通的。但是我们恰恰经常会遇到这样的情况,在这种情况下我们进行动态内存分配就行。

//下面是行不通的
int m;
scanf("%d",&m);
int a[m];

动态内存分配以及内存回收:

  • 使用malloc函数来分配若干个内存单元,并返回一个通用指针(无类型指针),这个指针指向分配内存的首地址。这个指针常用来表示基类型未知的指针,因此我们在赋给具体的指针类型的时候要进行类型的强转。
//malloc的原型
void *malloc(unsigned int size);

//使用malloc分配8个int数据的内存,并让p指向分配的内存的首地址
int *p = (int *)malloc(sizeof(int)*8);
  • 使用calloc来进行内存分配。这个函数和malloc的区别是这个函数接受两参数,第1个是申请的内存空间的数量来决定一个一维数组的大小,第2个表示每个内存空间的大小来决定数组元素的类型。
//calloc函数的原型
void *calloc(unsigned int num,unsigned int size);

//使用calloc分配8个int数据的内存,并让p指向分配的内存的首地址
int *p = (int *)calloc(8,sizeof(int));
  • 使用free函数来进行动态申请的内存的释放。这一步很重要,如果我们动态分配的内存过多且没有被合理的释放很可能会造成内存泄漏。
//free函数原型
void free(void *p);

//用free来对上面申请的内存进行释放
free(p);
  • 使用realloc函数来重新分配内存空间的大小。注意的是我们重新分配的空间的首地址和之前的地址不一定相同。
//函数原型
void *realloc(void *p,unsigned int size);

//用realloc函数对上面申请的内存进行释放
p = realloc(p,20);

指针中常见的问题

指针在定义的时候,如果未赋值,要给指针变量赋值为NULL(也就是0)。因为如果不赋值的话它会随机的引用一个内存地址。

还有就是当对指针变量所执向的内存进行了free后,如果后面这个指针变量仍然可能被用到也要将其赋值为NULL。因为在进行了free操作后,这个指针变量还是指向的这个内存地址,只不过这个地址中存的值将会是随机值。

前面都是一些简单的问题。下面重点介绍一下,函数返回指针变量的问题。

#include "stdio.h"

char *input();

int main() {
    char *name;
    printf("%c",*name);
    name = input();
    printf("%s", name);
}

char *input() {
    char name[10];
    scanf("%s",name);
    return name;
}

我们看一下上面的代码,我们通过一个函数进行名字的输入,然后再返回到主函数中进行输出。结果我们会发现没有输出。截图我就不列出来了。

其中原因就是input函数中的name是一个局部变量,它存储在栈中。当input函数执行的时候,函数在栈中为这些东西分配好内存,然后我们输入的名字后面也存在name局部变量中。在input函数执行结束后,input函数在栈中的内存就被释放了。因此就没有将名字返回回去。

其中我们有两种解决方法:

  • 一种是通过传入一个字符指针。
  • 另外就是在input函数中进行动态内存分配,动态内存分配在堆中,而且释放是由程序员来进行调用的。

下面我们给出第二种解决方案的代码:

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

char *input();

int main() {
    char *name;
    name = input();
    printf("%s", name);
}

char *input() {
    char *name;
    name = malloc(sizeof(char)*10);
    scanf("%s",name);
    return name;
}

上面的动态分配就可以有效解决之前出现的问题了。

总结

今天的博客主要是简单的对指针的一些难点进行了总结了。大纲在开头就给出了,这里我就不重复总结了。希望对大家有所帮助。^..^

参考

C语言程序设计(第三版)

转载请注明链接