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语言的其他的内容