听说C语言很难?怎么不来看看我这篇(五)指针

1.写在前面

计算机中我们所有写的程序以及变量都是通过对应的地址来找到,当你知道这个东西的地址和长度,那么你就能读取到这个东西。这个东西可以是函数,可以是变量等等。所以可以知道地址是多么重要的吧!那么C语言中有没有一个东西用来存储地址,很高兴的告诉你,有,就是指针,指针是一种保存变量地址的变量。

2.指针与地址

首先我们要知道计算机的存储是如何划分的。我们可以想把存储器按照一系列的编号来分配,所以我们的所了解的地址可以理解为编号,但是有了编号你就只知道这个变量的地址,但是要读取这个这个变量,你需要知道这个变量的长度,假如这个变量的长度是1个字节,你读取这个地址后面的两个字节,那么读出来的内容就是错的。

不管什么变量都是有两大要素,一个要素是变量的名(类似地址),另外一个要素就是变量类型(所占的长度)有了这些前置的知识,我们就能讲讲C语言中的指针和地址。

既然每个变量都是地址和对应的长度,那么我们我们可不可以将这个地址和长度存到一个变量中去,当然可以的,这就是指针。

讲了这么多,那么指针如何声明呢?具体的如下:

int x = 1,y = 2;
int *z = &x;
y =  *z; // y = 1

地址运算符&只能应用于内存中的对象,即变量与数组元素。(取对应的地址)

一元运算符*是间接寻址或者间接引用运算符。当它作用于指针时,将访问指针所指向的对象。

所以指针的变量还是没有逃脱上面两个规则,一个是地址,一个是长度。所以它也有名字和类型,不过这个类型存的是这个地址中变量的类型。

指针只能指向某种特定类型的对象,也就是说,每个指针都必须指向某种特定的数据类型(一个例外情况是指向void类型的指针可以存放指向任何类型的指针,但它不能间接引用其自身)

&就是取这个变量的地址,*就是取这个指针指向的变量的内容,指针变量进行运算的时候实际上是对指针变量中存的地址进行运算,但是如果加了*就对指针变量中地址所指向的变量的内容进行相应的运算了,具体的如下:

int a = 1;
int *p = &a;
*p += 1; // a就变成了2
p += 1; // 这个时候是a所在的地址加上4个字节

3.指针与函数参数

由于C语言是以传值的方式将参数值传递给被调用函数。因此,被调用函数不能直接修改主调函数中值。我们看一个交换的函数,具体的如下:

void swap(int x, int y) {
  int temp;
  temp = x;
  x = y;
  y = temp;
}

这样并不会改变交换的值,因为是值传递,所以是副本。我们可以采用指针的方式来进行交换数据,因为有了地址就可以修改原来的变量。具体的代码如下:

void swap(int *px, int *py){
  int temp;
  temp = *px;
  *px = *py;
  *py = temp;
}

我们再来看一个程序,具体的如下:

#include <stdio.h>
#include <ctype.h>

int getch(void);

void ungetch(int);

/* getint:  get next integer from input into *pn */
int getint(int *pn) {
    int c, sign;
    while (isspace(c = getch()))   /* skip white space */
        ;
    if (!isdigit(c) && c != EOF && c != '+' && c != '-') {
        ungetch(c);  /* it is not a number */
        return 0;
    }
    sign = (c == '-') ? -1 : 1;
    if (c == '+' || c == '-')
        c = getch();
    for (*pn = 0;isdigit(c);c = getch())
        *pn = 10 * *pn + (c - '0');
    *pn *= sign;
    if (c != EOF)
        ungetch(c);
    return c;
}

该版本的getint函数在到达文件结尾时返回EOF,当下一个输入不是数字时返回0,当输入中包含一个有意义的数字时返回一个正值。

4.指针与数组

先看如下的程序,具体的代码如下:

int strlen(char *s)
{
  int n;
  for (n = 0; *s != '\0'; s++)
    n++;
  return n;
}

int strlen(char s[])
{
    int n;
    for (n = 0; s[n] != '\0'; n++);
    return n;
}

上面的代码都是求数组的长度的,一个是通过指针的方式来实现,一个是通过数组的方式实现的。我们都知道数组是一个连续的空间,而第一个函数传入的时候数组的首地址,然后每次给这个首地址加1,然后计算对应的长度,当地址中的内容是’\0’的时候,表示到了数组的结尾了,这个时候就计算出来了数组的长度了。还有一种就是直接通过数组的下标计算出来的。

永远记住指针是一个变量,可以进行相应的计算,而数组名只是一个数组名,他无法对其进行相应的计算。

当把数组名传递给一个函数时,实际上传递的是该数组第一个元素的地址。

5.地址算术运算

前面我们说过了指针也是变量,所以对指针的计算是允许的,但是指针的运算是怎么样的呢?指针的运算是就根据指针的类型来的,如果你的类型是int的类型的话,那么就会加上4个字节,如果是char类型的话,那么就会加上1个字节。抓住这个内容的话,那么我们就先看一个例子吧。

我们先来看一个不完善的存储分配程序。它是由两个函数组成。第一个函数alloc(n)返回一个指向n个连续字符存储单元的指针,alloc函数的调用者可利用该指针存储字符序列。第二个函数afree(p)释放已分配的存储空间,以便以后重用。但是为什么说这两个函数是不完善的呢?因为这两个函数的调用必须要按照栈的方式来调用。

我们来看下这两个函数具体的如下:

#define ALLOCSIZE 10000 /* size of available space */

static char allocbuf[ALLOCSIZE]; /* storage for alloc */

static char *allocp = allocbuf;  /* next free position */

char *alloc(int n)    /* return pointer to n characters */
{
    if (allocbuf + ALLOCSIZE - allocp >= n) {  /* it fits */
        allocp += n;
        return allocp - n; /* old p */
    } else      /* not enough room */
        return 0;
}

void afree(char *p)  /* free storage pointed to by p */
{
    if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
        allocp = p;
}

通过上面的例子我们可以知道指针可以进行比较运算,但是比较运算有意义,只有两个指针指向是同一个数组的成员,这样的比较才是有意义的。假设我们有两个指针,分别是指针p和指针q,这两个指针同时指向一个数组,这个时候指针p指向的地址在指针q指向的地址之前,那么p<q就是成立的。

前面我们也看到指针有加减的方法,同时他们也是有效,例如指针p+1,这个时候如果指针p是int类型的,那么p+1就是在p原来的地址的上加上4个字节。这样我们就能反推出减法。这里我就不做过多的赘述了。

总结:有效的指针运算包括相同类型之间的赋值运算;指针同整数之间的加法或减法运算;指向相同数组中元素的两个指针间的减法或比较运算;将指针赋值为0或指针与0之间的比较运算。

6.字符指针与函数

我们都知道字符串常量是一个字符数组。同时字符数组的结尾有个’\0’。

我们先看如下的两个定义,具体的如下:

char amessage[] = "nw is the time";  /*定义一个数组*/
char *pmessage = "now is the time";  /*定义一个指针*/

上述的声明中,amessage是一个仅仅足以存放初始化字符串以及空字符‘\0’的一维数组。数组中的单个字符可以进行修改,但amessage始终指向同一个存储位置。另一方面,pmessage是一个指针,其初值指向一个字符串常量,之后它可以被修改以指向其它地址,但如果试图修改字符串的内容,结果是没有定义的

在这里插入图片描述

下面我们在看看如下的两个函数的不同的实现,具体的如下:

// 非指针的方式实现的复制
void strcpy(char *s, char *t)
   {
       int i;
       i = 0;
       while ((s[i] = t[i]) != '\0')
           i++;
   }

// 指针的方式实现的复制
void strcpy(char *s, char *t)
   {
       int i;
       i = 0;
       while ((*s = *t) != '\0') {
           s++;
           t++;
       }
   }

void strcpy(char *s, char *t)
   {
       while ((*s++ = *t++) != '\0')
           ;
   }

void strcpy(char *s, char *t)
   {
       while (*s++ = *t++)
           ;
   }

第一个版本是通过数组的方式的实现的,第二个版本是通过指针实现,主要读取的是指针指向的地址的内容进行对应的复制。然后对第二个版本进行了不断的升级,最后就写出了最精炼的最后的一个版本。

我们再来看另外一个函数,具体的如下:

// 比较两个字符的大小,非指针的方式实现
int strcmp(char *s, char *t)
   {
       int i;
       for (i = 0; s[i] == t[i]; i++)
           if (s[i] == '\0')
               return 0;
       return s[i] - t[i];
   }

// 比较两个字符的大小,指针的方式实现
 int strcmp(char *s, char *t)
   {
       for ( ; *s == *t; s++, t++)
           if (*s == '\0')
               return 0;
       return *s - *t;
   }

7.指针数组以及指向指针的指针

既然我们将变量的地址存起来的,这样就是指针变量。但是如果这个被存的变量是指针变量呢?那么不就是指向指针的指针。可以理解为二级指针。

我们需要写一个排序,针对不同的字符串的长度,进行排序。这个时候需要引入指针数组处理这个问题。如果待排序的文本行首尾相连地存储在一个长字符数组中,那么每个文本行可通过指向它的第一个字符的指针来访问。这样,将指向两个文本行的指针传递给函数strcmp就可实现对这两个文本行的比较。当交换次序颠倒的两个文本行时,实际上交换的是指正数组中与这两个文本行相对应的指针,而不是两个文本行本身。

在这里插入图片描述

那么可以分成如下三个步骤:

  • 读取所有输入行
  • 对文本行进行排序
  • 按次序打印文本行

具体的代码如下:

#include <stdio.h>
#include <string.h>

#define MAXLINES 5000     /* max #lines to be sorted */

// 表示lineptr是一个具有MAXLINES个元素的一维数组,其中数组的每个元素是一个指向字符类型对象的指针。
char *lineptr[MAXLINES];  /* pointers to text lines */
int readlines(char *lineptr[], int nlines);

void writelines(char *lineptr[], int nlines);

void qsort(char *lineptr[], int left, int right);

/* sort input lines */
main() {
    int nlines;     /* number of input lines read */
    if ((nlines = readlines(lineptr, MAXLINES)) >= 0) {
        qsort(lineptr, 0, nlines - 1);
        writelines(lineptr, nlines);
        return 0;
    } else {
        printf("error: input too big to sort\n");
        return 1;
    }
}

#define MAXLEN 1000  /* max length of any input line */

int getline(char *, int);

char *alloc(int);

/* readlines:  read input lines */
int readlines(char *lineptr[], int maxlines) {
    int len, nlines;
    char *p, line[MAXLEN];
    nlines = 0;
    while ((len = getline(line, MAXLEN)) > 0)
        if (nlines >= maxlines || (p = alloc(len)) == NULL)
            return -1;
        else {
            line[len - 1] = '\0';  /* delete newline */
            strcpy(p, line);
            lineptr[nlines++] = p;
        }
    return nlines;
}

/* writelines:  write output lines */
void writelines(char *lineptr[], int nlines) {
    int i;
    for (i = 0; i < nlines; i++)
        printf("%s\n", lineptr[i]);
}


然后我们再来看下排序的算法,具体的如下:

/* qsort:  sort v[left]...v[right] into increasing order */
   void qsort(char *v[], int left, int right)
   {
       int i, last;
       void swap(char *v[], int i, int j);
       if (left >= right)  /* do nothing if array contains */
           return;         /* fewer than two elements */
       swap(v, left, (left + right)/2);
       last = left;
       for (i = left+1; i <= right; i++)
           if (strcmp(v[i], v[left]) < 0)
               swap(v, ++last, i);
       swap(v, left, last);
       qsort(v, left, last-1);
       qsort(v, last+1, right);
   }

最后再来看下swap函数

void swap(char *v[], int i, int j)
   {
       char *temp;
	   temp = v[i];
       v[i] = v[j];
       v[j] = temp;
   }

8.多维数组

多维数组就是数组中的数组,我们可以先看如下的一个例子,一个日期转换的问题,把某月某日这种的日期表示形式转换为某年中第几天的表示形式。由于我们闰年和平年的二月的日期是不一样的,所以我们用一维数组表示每个月的天数我们是无法表示的,这儿我们就需要二维数组,这样可以同时存平年和闰年的每个月的天数。具体的代码如下:

static char daytab[2][13] = {
        {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
        {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};

/* day_of_year:  set day of year from month & day */
int day_of_year(int year, int month, int day) {
    int i, leap;
    leap = year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
    for (i = 1; i < month; i++)
        day += daytab[leap][i];
    return day;
}

/* month_day:  set month, day from day of year */
void month_day(int year, int yearday, int *pmonth, int *pday) {
    int i, leap;
    leap = year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
    for (i = 1; yearday > daytab[leap][i]; i++)
        yearday -= daytab[leap][i];
    *pmonth = i;
    *pday = yearday;
}

如果将二维数组作为参数传递给函数 ,那么在函数的参数声明中必须指明数组的列数。有如下三种的写法:

f(int daytab[2][13]);

f(int daytab[][13]);

f(int (*daytab)[13])

最后一种声明形式表示是参数是一个指针,它指向具有13个整形元素的一维数组。因为方括号[]的优先级高于*的优先级,所以上述声明中必须使用圆括号。

9.指针数组的初始化

具体的代码如下:

 char *month_name(int n)
   {
       static char *name[] = {
           "Illegal month",
           "January", "February", "March",
           "April", "May", "June",
           "July", "August", "September",
           "October", "November", "December"
       };

10.指针与多维数组

我们先来看下如下的定义,具体的如下:

int a[10][20];
int *b[10];

从语法的角度来说,a[3][4]和b[3][4]都是对一个int对象的合法引用。但是a是一个真正的二维数组,它分配了200个int的长度的存储空间。但是对于b来说,该定义仅仅分配了10个指针,并且没有对它们初始化,它们的初始化必须以现实的方式进行,比如静态初始化或通过代码的初始化。我们可以看看如下的两个声明。

char *name[]={"Illegal manth", "Jan", "Feb", "Mar"};

在这里插入图片描述

再来看下二维数组的声明,具体的如下:

char aname[][15] = { "Illegal month", "Jan", "Feb", "Mar" };

在这里插入图片描述

11.指向函数的指针

在C语言中,函数本身不是变量,但可以定义指向函数的指针,我们先来如下的排序的代码,具体的如下:

/* qsort:  sort v[left]...v[right] into increasing order */
   void qsort(void *v[], int left, int right,
              int (*comp)(void *, void *))
   {
       int i, last;
       void swap(void *v[], int, int);
       if (left >= right)    /* do  nothing if array contains */
           return;           /* fewer than two elements */
       swap(v, left, (left + right)/2);
       last = left;
       for (i = left+1; i <= right;  i++)
           if ((*comp)(v[i], v[left]) < 0)
               swap(v, ++last, i);
       swap(v, left, last);
       qsort(v, left, last-1, comp);
       qsort(v, last+1, right, comp);
   }

我们看下排序函数的第四个参数声明如下:

int (*comp)(void *, void *)

它表明comp是一个指向函数的指针,该函数具有两个void*类型的参数,其返回值类型为int

在下列的语句中

if((*comp)(v[i], v[left]) < 0)

comp的使用和其声明是一致的,comp是一个指向函数的指针,*comp代表一个函数。下列语句是对该函数进行调用:

(*comp)(v[i],v[left])

12.写在最后

本篇博客主要简单的介绍了下C语言的指针。后面会继续介绍C语言的其他的内容

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值