概述
指针是一种保存变量地址
的变量
在C语言中,指针的使用非常广泛,原因之一是,指针常常是表达某个计算的唯一途径,另一个原因是,同其他方法比较起来,使用指针通常可以生成更高效、更紧凑的代码
。
指针与数组
之间的关系十分密切
指针在一定程度上会导致程序难以理解。但是,如果谨慎地使用指针,便可以利用它写出简单、清晰的程序
ANSI C明确制定了操纵指针的规则
。ANSI C使用类型void*
(指向void的指针)代替char*
作为通用指针的类型
1、指针与地址
通常的机器都有一系列连续编号或编址的存储单元
,这些存储单元可以单个
进行操纵,也可以以连续成组
的方式操纵
通常情况下,机器的一个字节
可以存放一个char类型的数据
指针是能够存放一个地址
的一组存储单元(通常是4个或8个字节)
地址运算符&
只能应用于内存中的对象,即变量与数组元素
。它不能作用于表达式、常量或register类型的变量
一元运算符*
是间接寻址或间接引用运算符
注意指针的声明方式:int *p
;,这种声明变量的语法与声明该变量所在表达式的语法类似(不太懂)
指针只能指向某种特定类型的对象,也就是说,每个指针都必须指向某种特定的数据类型
(一个例外情况是指向void类型的指针可以存放指向任何类型的指针,但它不能间接引用其自身
)
2、指针与函数参数
由于C语言是以传值的方式
将参数值传递给被调用函数,因此,被调用函数不能直接修改主调函数中变量的值
指针参数可以使得被调函数能够访问和修改主调函数中可变对象的值
void swap(int *px, int *py)
{
int temp;
temp = *px;
*px = *py;
*py = temp;
}
getint函数接受自由格式的输入,并执行转换,将输入的字符流分解成整数
,且每次调用得到一个整数
#include <ctype.h>
int getch(void);
void ungetch(int);
//将输入中的下一个整型数赋值给*pn
int getint(int *pn)
{
int c, sign;
while(isspace(c = getch()))
;
if(!isdigit(c) && c != EOF && c != '+' && c != '-')
{
ungetch(c);
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;
}
该函数在到达文件结尾时返回EOF
,当下一个输入不是数字时返回0
,当输入中包含一个有意义的数字时返回一个正值
3、指针与数组
通过数组下标所能完成的任何操作都可以通过指针来实现
。一般来说,用指针编写的程序比用数组下标编写的程序执行速度快
,但另一方面,用指针实现的程序理解起来稍微困难一些
int a[10];
定义了一个长度为10的数组a。换句话说,它定义了一个由10个对象组成的集合
,这10个对象存储在相邻的内存区域中
如果pa指向数组a中的某个特定元素,那么pa+i
指向pa所指向的对象之后的第i个对象,而pa-i
则指向pa所指向数组元素之前的第i个元素(无论数组a中元素的类型或数组长度是什么,以上的结论都成立,不要越界就好
)
下标和指针运算
之间具有密切的对应关系,数组名(数组类型的变量)
所代表的就是该数组最开始的一个元素的地址
(数组名是一个地址,但它不是一个指针
)
a为数组名
,在C语言中,a[i]
与*(a+i)
等价(其实,前者是转化为后者进行计算的);&a[i]
与a+i
等价
如果pa是一个指针(如果该指针不是指向数组的元素,那么其偏移是没有什么意义的。因为只有连续存储,偏移才有意义)
,那么,在表达式中也可以在它的后面加下标
(pa[i]
与*(pa+i)
是等价的)
简而言之,一个通过数组和下标
实现的表达式可等价地通过指针和偏移量
实现
数组名和指针之间有一个不同之处:指针是一个变量
,数组名不是变量
当把数组名传递给一个函数时,实际上传递的是该数组第一个元素的地址。
在函数定义中,形式参数char s[]
和char *s
是等价的。我们通常更习惯于使用后一种形式,因为它比前者更直观地表明了该参数是一个指针
如果将数组名传递给函数,函数可以根据情况判定是按照数组处理还是按照指针处理,随后根据相应的方式操作该参数(没什么意义,本质都是一样的)
对于函数来说,其并不关心所引用的是否只是一个更大数组的部分元素
如果确信
相应的元素存在,也可以通过下标访问数组第一个元素之前的元素。类似于p[-1]、p[-2]这样的表达式在语法上都是合法的
,它们分别引用位于p[0]之前的两个元素。当然,引用数组边界之外的对象是非法的
(有意思,很危险哇)
4、地址算术运算
C语言中的地址算术运算
方法是一致且有规律的,将指针、数组和地址
的算术运算集成在一起是该语言的一大优点
一个不完善(以栈的方式分配)的存储分配程序(连续字符存储单元
,即字符块)
#define ALLOCSIZE 10000 //可用空间大小
static char allocbuf[ALLOCSIZE]; //使用的存储区
static char *allocp = allocbuf; //下一个空闲位置
char *alloc(int n)
{
if(allocbuf + ALLOCSIZE - allocp >= n)
{
allocp += n;
return allocp - n;
}
else
return 0;
}
void afree(char *p)
{
if(p >= allocbuf && p < allocbuf + ALLOCSIZE)
allocp = p;
}
指针也可以初始化
。通常,对指针有意义的初始化值只能是0
或者是表示地址的表达式
,对后者来说,表达式所代表的地址必须是在此前已定义的具有适当类型的数据的地址
C语言保证,0永远不是有效的数据地址
指针与整数之间不能相互转换
,但0是唯一的例外;常量0可以赋值给指针,指针也可以和常量0进行比较。程序中经常用符号常量NULL
代替常量0,这样便于更清晰地说明常量0是指针的一个特殊值。符号常量NULL
定义在标准头文件<stddef.h>
中
任何指针与0进行相等或不等的比较运算都有意义
。指向不同数组的元素的指针之间的算术或比较运算没有定义(这里有一个特例:指针的算术运算中可使用数组最后一个元素的下一个元素的地址)
p是指向某种类型对象的指针,在计算p+n时,n将根据p指向的对象的长度按比例缩放,而p指向的对象的长度则取决于p的声明(两指针相减的结果按其所指向的类型缩小了)
头文件<stddef.h>
中定义的类型ptrdiff_t
足以表示两个指针之间的带符号差值
size_t
是由运算符sizeof
返回的无符号整型
所有的指针运算都会自动考虑它所指向的对象的长度
有效的指针运算包括相同类型指针之间的赋值运算;指针同整数之间的加法或减法运算;指向相同数组中元素的两个指针间的减法或比较运算;将指针赋值为0或指针与0之间的比较运算。其他所有形式的指针运算都是非法的
5、字符指针与函数
字符串常量
是一个字符数组
程序通常通过检查空字符
找到字符数组的结尾
。字符串常量占据的存储单元数也因此比双引号内的字符数大于1
字符串常量可通过一个指向其第一个元素的指针访问
C语言没有提供将整个字符串作为一个整体进行处理的运算符
char amessage[] = "now is the time"; //定义一个数组
char *pmessage = "now is the time"; //定义一个指针
amessage
是一个仅仅足以存放初始化字符串以及空字符’\0’的一维数组
。数组中的单个字符可以进行修改,但amessage始终指向同一个存储位置。另一方面,pmessage是一个指针
,其初值指向一个字符串常量,之后它可以被修改以指向其他的地址,但如果试图修改字符串的内容,结果是没有定义的
字符串常量的生命周期???????????
strcpy函数
的不同实现版本
//数组方式
void strcpy(char *s, char *t)
{
int i;
i = 0;
while((s[i] = t[i]) != '\0')
i++;
}
//指针方式版本1
void strcpy(char *s, char *t)
{
while((*s = *t) != '\0')
{
s++;
t++;
}
}
//指针方式版本2
void strcpy(char *s, char *t)
{
while((*s++ = *t++) != '\0')
;
}
//指针方式版本3,C语言程序中经常会采用这种写法
void strcpy(char *s, char *t)
{
while((*s++ = *t++))
;
}
标准库<string.h>
中提供的函数strcpy把目标字符串作为函数值返回
strcmp函数
的不同实现版本
//数组方式
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)
{
int i;
for( ; *s == *t; s++, t++)
if(*s == '\0')
return 0;
return *s - *t;
}
*p++ = val; //将val压入栈
val = *--p; //将栈顶元素弹出到val中
是进栈和出栈的标准用法
6、指针数组以及指向指针的指针
通常情况下,最好将程序划分为若干个与问题的自然划分
相一致的函数,并通过主函数控制其他函数的执行
这里通过一个按字母顺序
对由文本行组成的集合进行排序的程序来深入说明指针数组(指向指针的指针)
的问题
#include <stdio.h>
#include <string.h>
#define MAXLINES 5000
char *lineptr[MAXLINES];
int readlines(char *lineptr[], int nlines);
void writelines(char *lineptr[], int nlines);
void qsort(char *lineptr[], int left, int right);
int main(int argc, char ** argv)
{
int nlines;
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
int getline(char *, int);
char *alloc(int);
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';
strcpy(p, line);
lineptr[nlines++] = p;
}
return nlines;
}
void writelines(char *lineptr[], int lines)
{
while(nlines-- > 0)
printf("%s\n", *lineptr++);
}
void qsort(char *v[], int left, int right)
{
int i, last;
void swap(char *v[], int i, int j);
if(left >= right)
return ;
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);
}
void swap(char *v[], int i, int j)
{
char *temp;
temp = v[i];
v[i] = v[j];
v[j] = temp;
}
7、多维数组
多维数组的使用并不广泛
在char类型的变量中存放较小的非字符整数也是合法的(建议指明char的符号)
在C语言中,二维数组实际上是一种特殊的一维数组
把某月某日这种日期表示形式转换为某年中的第几天的表示形式和相反功能
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}
};
//把某月某日这种日期表示形式转换为某年中的第几天的表示形式
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;
}
//把某年中的第几天的日期表示形式转换为某月某日的表示形式
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个整型元素的一维数组
一般来说,除数组的第一维可以不指定大小外,其余各维都必须明确指定大小
8、指针数组的初始化
指针数组
的初始化必须以显式的方式进行
一个函数,返回一个指向第n个月名字的字符串的指针
char *month_name(int n)
{
static char *name[] = {
"Illegal month",
"January", "Fabruary", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December"
};
return (n < 1 || n > 12) ? name[0] : name[n];
}
name数组的初始化通过一个字符串列表
实现
字符串常量的生存周期?????????????
9、指针与多维数组
int a[10][20]; //二维数组
int *b[10]; //指针数组
a[3][4]
和b[3][4]
都是对一个int对象的合法引用
,个人认为b[3][4]=*(b[3] + 4)
(不正确,因为数组中存储的指针的值可以不是连续的)
指针数组的一个重要优点在于,数组的每一行长度可以不同
可以说,指针数组更加的灵活
(但是我担心字符串常量的生命周期
)
10、命令行参数
在支持C语言的环境中,可以在程序开始执行时将命令行参数传递给程序
第一个参数argc的值表示运行程序时命令行中参数的数目(不正确);第二个参数argv是一个指向字符串数组的指针(其实一个存放字符指针的数组)
因为argc不用程序员来指定,所以对它的理解并不深刻
按照C语言的约定,argv[0]
的值是启动该程序的程序名,因此argc的值至少为1。如果argc的值为1,则说明程序名后面没有命令行参数
第一个可选参数为argv[1]
,而最后一个可选参数为argv[argc - 1]
,ANSI C标准要求argv[argc]的值必须为一空指针
echo程序的实现
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
for(i = 1; i < argc; i++)
printf("%s%s", argv[i], (i < argc - 1) ? " " : "");
printf("\n");
return 0;
}
int main(int argc, char *argv[])
{
while(--argc > 0)
printf("%s%s", *++argv, (argc > 1) ? " " : "");
printf("\n");
return 0;
}
//该函数的printf语句还可以换成printf((argc > 1) ? "%s" : "%s", *++argv);
//这说明printf的格式化参数也可以是表达式
UNIX系统中的C语言程序有一个公共的约定:以负号开头的参数
表示一个可选标志或参数
可选参数应该允许以任意次序
出现。此外,如果可选参数能够组合使用
,将会给使用者带来更大的方便
UNIX程序grep的实现
#include <stdio.h>
#include <string.h>
#define MAXLINE 1000
int getline(char *line, int max);
//打印与第一个参数指定的模式匹配的行
int main(int argc, char ** argv)
{
char line[MAXLINE];
int found = 0;
if(argc != 2)
printf("Usage: find pattern\n");
else
while(getline(line, MAXLINE) > 0)
if(strstr(line, argv[1]) != NULL)
{
printf("%s", line);
found++;
}
return found;
}
//改进grep程序,允许程序带两个可选参数,一个参数表示打印除匹配模式之外的所有行,另一个参数表示为每个打印的行加上相应的行号
int main(int argc, char ** argv)
{
char line[MAXLINE];
long lineno = 0;
int c, except = 0, number = 0, found = 0;
while(--argc > 0 && (*++argv)[0] == '-')
while(c = *++argv[0])
switch(c)
{
case 'x':
except = 1;
break;
case 'n':
number = 1;
break;
default:
printf("find: illegal option %c\n", c);
argc = 0;
found = -1;
break;
}
if(argc != 1)
printf("Usage: find -x -n pattern\n");
else
while(getline(line, MAXLINE) > 0)
{
lineno++;
if((strstr(line, *argv) != NULL) != except)
{
if(number)
printf("%ld:", lineno);
printf("%s", line);
found++;
}
}
return found;
}
//将上述实现看明白
很少有人使用特别复杂的指针表达式
。如果表达式太过复杂,则建议分步书写
11、指向函数的指针
在C语言中,函数本身不是变量,但可以定义指向函数的指针
。这种指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值
等等
排序程序
通常包括3部分:判断任何两个对象之间次序的比较操作
;颠倒对象次序的交换操作
;一个用于比较和交换对象直到所有对象都按正确次序排列的排序算法
。由于排序算法与比较、交换操作无关
,因此,通过在排序算法中调用不同的比较和交换函数,便可以实现按照不同的标准排序
参数的出错处理???????
函数名就是函数指针,函数指针却不是函数名(好像并不是这么简单的?????)
该程序在给定可选参数-n的情况下,按数值大小而非字典顺序对输入行进行排序
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAXLINES 5000
char *lineptr[MAXLINES];
int readlines(char *lineptr[], int nlines);
void writelines(char *lineptr[], int nlines);
void qsort(void *lineptr[], int left, int right, int (*comp)(void *, void *));
int numcmp(char *, char *);
int main(int argc, char ** argv)
{
int nlines;
int numeric = 0;
if(argc > 1 && strcmp(argv[1], "-n") == 0)
numeric = 1;
if((nlines = readlines(lineptr, MAXLINES)) >= 0)
{
qsort((void **)lineptr, 0, nlines - 1, (int (*)(void*, void*))(numeric ? numcmp : strcmp));
writelines(lineptr, nlines);
return 0;
}
else
{
printf("input too big to sort\n");
return 1;
}
}
void qsort(void *v[], int left, int right, int (*comp)(void *, void *))
{
int i, last;
void swap(void *v[], int, int);
if(left >= right)
return;
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 numcmp(char *s1, char *s2)
{
double v1, v2;
v1 = atof(s1);
v2 = atof(s2);
if(v1 < v2)
return -1;
else if(v1 > v2)
return 1;
else
return 0;
}
void swap(void *v[], int i, int j)
{
void *temp;
temp = v[i];
v[i] = v[j];
v[j] = temp;
}
任何类型的指针都可以转换为void *类型
,并且在将它转换回原来的类型时不会丢失信息。这种转换通常不会影响到数据的实际表示,但要确保编译器不会报错(将自动强转变为显式强转)
如果comp是一个指向函数的指针
,*comp
代表一个函数
12、复杂声明
C语言的语法力图使声明和使用相一致
尽管实际中很少用到过于复杂的声明,但是,懂得如何理解甚至如何使用这些复杂的声明是很重要的
创建复杂声明的方法:使用typedef关键字
或通过声明符的语法
进行解析
声明符的语法是递归定义的
,所以在识别一个声明的组成部分时,函数dcl和dirdcl是相互递归调用的
程序dcl将正确的C语言的声明转换为文字描述(文字描述从左向右
阅读)
//该程序是一个递归下降语法分析程序,目前该程序有诸多限制
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#define MAXTOKEN 100
enum {NAME, PARENS, BRACKETS};
void dcl(void);
void dirdcl(void);
int gettoken(void);
int tokentype;
char token[MAXTOKEN];
char name[MAXTOKEN];
char datatype[MAXTOKEN];
char out[1000];
int main(int argc, char ** argv)
{
while(gettoken() != EOF)
{
strcpy(datatype , token);
out[0] = '\0';
dcl();
if(tokentype != '\n')
printf("syntax error\n");
printf("%s: %s %s\n", name, out, datatype);
}
return 0;
}
void dcl(void)
{
int ns;
for(ns = 0; gettoken() == '*'; )
ns++;
dirdcl();
while(ns-- > 0)
strcat(out, " pointer to");
}
void dirdcl(void)
{
int type;
if(tokentype == '(')
{
dcl();
if(tokentype != ')')
printf("error: missing )\n");
}
else if(tokentype == NAME)
strcpy(name, token);
else
printf("error: expected name or (dcl)\n");
while((type = gettoken()) == PARENS || type == BRACKETS)
if(type == PARENS)
strcat(out, " function returning");
else
{
strcat(out, " array");
strcat(out, token);
strcat(out, " of");
}
}
int gettoken(void)
{
int c, getch(void);
void ungetch(int);
char *p = token;
while((c = getch()) == ' ' || c == '\t')
;
if(c == '(')
{
if((c = getch()) == ')')
{
strcpy(token, "()");
return tokentype = PARENS;
}
else
{
ungetch(c);
return tokentype = '(';
}
}
else if(c == '[')
{
for(*p++ = c; (*p++ = getch()) != ']'; )
;
*p = '\0';
return tokentype = BRACKETS;
}
else if(isalpha(c))
{
for(*p++ = c; isalnum(c = getch()); )
*p++ = c;
*p = '\0';
ungetch(c);
return tokentype = NAME;
}
else
return tokentype = c;
}
如果不在乎生成多余的圆括号,另一方向的转换要容易一些
undcl程序,将文字描述(简化的文字描述)转换为声明
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#define MAXTOKEN 100
enum {NAME, PARENS, BRACKETS};
int gettoken(void);
int tokentype;
char token[MAXTOKEN];
char name[MAXTOKEN];
char datatype[MAXTOKEN];
char out[1000];
int main(int argc, char ** argv)
{
int type;
char temp[MAXTOKEN];
while(gettoken() != EOF)
{
strcpy(out, token);
while((type = gettoken()) != '\n')
if(type == PARENS || type == BRACKETS)
strcat(out, token);
else if(type == '*')
{
sprintf(temp, "(*%s)", out);
}
else if(type == NAME)
{
sprintf(temp, "%s %s", token, out);
strcpy(out, temp);
}
else
printf("invalid input at %s\n", token);
printf("%s\n", out);
}
return 0;
}