Scott's world.

C语言备课

Word count: 9.1kReading time: 34 min
2019/09/09 Share

C语言备课

函数

引导

编写一个程序

它将输入中包含特定模式将字符串的各行打印出来

该任务可以明确地划分成下列3部分:
while(未处理的行)
if(该行包含指定的模式)
打印该行

  • 尽管我们可以都放在主程序main中

    但更好的做法是利用其结构把每一部分设计成一个独立的函数。因为这样可以把不相关的细节隐藏在函数从而减少了不必要的相互影响的机会并且可以在其他程序中使用

那现在我们就来用一个独立的结构也就是函数来处理这一个问题

  • getline实现“未处理的行”
  • 我们只需要编写一个判定“该行包含指定的模式”的函数编写strindex(s,t)实现该目标。该函数返回字符串t在字符串s中出现的起始位置或索引即当s不包含t时返回值为-1(以此来表示失败的情况)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include<stdio.h>
#define maxline 1000
int getline(char line[],int max);
int strindex(char source[],char searchfor[]);
char pattern[]="ould";//查找的模式
int getline(char s[],int lim)//将行保存在s中并返回该行的长度
{
int c,i;
i=0;
while(--lim>0&&(c=getchar())!=EOF&&c!='\n')
s[i++]=c;
if(c=='\n')
s[i++]=c;
s[i]='\0';
return i;

}
int strindex(char s[],char t[])//返回t在s中的位置若未找到则返回-1
{
int i,j,k;
for(i=0;s[i]!='\0';i++)
{
for(j=i,k=0;t[k]!='\0'&&t[k]==s[j];k++,j++);//遍历的思想
if(k>0&&t[k]=='\0')
return i;
}
return -1;
}
main()//返回了一个状态即匹配的数目
{
char line[maxline];
int found=0;
while(getline(line,maxline)>0)//分成一段一段来判断
{
if(strindex(line,pattern)>=0)
{
printf("%s",line);
found++;
}

}
return found;
}

当完成了这一个程序,大家就应该暗暗地明白函数到底是怎么一个用法,利用的是怎么一个思想,也就是如何利用函数来解决实际问题.

下面我们就来介绍函数的相关定义

函数定义

1
2
3
4
返回值类型 函数名(参数声明表)
{
声明和语句
}

其中函数定义中的各构成部分都可以省略。最简单的函数如下所示

1
2
3
4
dummy() {}
//该函数不执行任何操作也不返回任何值
//它可以在程序开发期间用以保留位置(留待以后填充代码)
//如果函数定义省略了返回值类型则默认为int类型
  • 程序可以看成事变量定义和函数定义的集合,而被调用函数通过return语句向调用者返回值
1
2
3
return 表达式;(可以跟任何表达式)
//在必要时表达式将被转换为函数的返回值类型。表达式两边可加一对圆括号
//调用函数可以忽略返回值并且return语句后面也不一定需要表达式

如果某个函数从一个地方返回时有返回值而从另一个地方返回时没有返回值,该函数并不非法。但可能是一种出问题的征兆。在任何情况下函数若没有成功返回一个值则它的“值”肯定是无用的

我们可以来一个例子也就是改编一下上面我们的函数strrindex(s,t),这次我们需要返回的是字符串t在s中最右边出现的位置.如果不包含则返回-1

1
2
3
4
5
6
7
8
9
10
11
12
strrindex(char s[],char t[]);
strrindex(char s[],char t[])
{
int i,j,k;
for(i=strlen(s)-strlen(t);i>=0;i--)
{
for(j=i,k=0;t[k]!='\0'&&t[k]==s[j];k++,j++);
if(k>0&&t[k]!=='\0')
return i;
}
return -1;
}

返回非整型值的函数

首先我们来看一个函数

通过atof(s)来说明函数返回非整型值的方法。该函数把字符串s转换为相应的双精度浮点数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <ctype.h>//这版本并不是高质量的一个版本,它占用了过多的空间
double atof(char s[])
{
double val,power;
int i,sign;
for(i=0;isspace(s[i]);i++)//跳过空白符而且局限在第一个遇到的空白符
;
sign=(s[i]=='-')?-1:1;
if(s[i]=='+'||s[i]=='-')i++;
for(val=0.0;isdigit(s[i]);val++)val=10.0*val+(s[i]-'0');
if(s[i]=='.')i++;
for(power=1.0;isdigit(s[i]);power++)
{
val=10.0*val+(s[i]-'0');
power*=10.0;
}
return sign*val/power;//返回的非整型值,为了达到该目的可在调用函数时显式声明atof但函数
//的声明与定义必须一致
}
  • 如果没有函数原型则函数将在第一次出现的表达式中被隐式声明
  • 如果先前没有声明过的一个名字出现在某个表达式中并且后面紧跟一个左圆括号那么下文就会认为该名字是一个函数名字且返回值假定为Int类型
  • 如果函数声明不包含参数,那么编译程序不会对参数作任何假设,并会关闭所有的参数检查(void进行声明可以)
1
2
3
4
5
6
//在正确声明atof函数基础上可以利用编写出atoi函数(将字符串转换为Int类型)
int atoi(char s[])
{
double atof(char s[]);
return (int) atof(s);//防止这种操作可能会丢失信息某些编译器可能会对此给出警告信息
}

练习:对atof函数进行扩充使他可以处理形如123.45e-6的科学表示方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
> double atof(char s[])
> {
> double val,power;
> int i,sign,exp;
> for(i=0;isspace(s[i]);i++)//跳过空白符而且局限在第一个遇到的空白符
> ;
> sign=(s[i]=='-')?-1:1;
> if(s[i]=='+'||s[i]=='-')i++;
> for(val=0.0;isdigit(s[i]);val++)val=10.0*val+(s[i]-'0');
> if(s[i]=='.')i++;
> for(power=1.0;isdigit(s[i]);power++)
> {
> val=10.0*val+(s[i]-'0');
> power*=10.0;
> }
> val=sign*val/power;
> if(s[i]=='e'||s[i]=='E')
> {
> sign=(s[++i]=='-')?-1:1;
> if(s[i]=='+'||s[i]=='-')i++;
> for(exp=0;isdigit(s[i]);i++)exp=10*exp+(s[i]-'0');
> if(sign==1)
> while(exp-->0) val*=10;//防止E或e后面没有数字
> else
> while(exp-->0) val/10;
> }
> return val;
> }
>
>

外部变量

  • C语言程序可以看成由一系列的外部对象构成,这些外部对象可能是变量或函数

外部变量和函数具有以下性质:

  • 通过同一个名字对外部变量的所有引用实际上都是引用同一个对象。(标准把这一性质称为外部链接)
  • 为外部变量可以在全局范围内访问,这就为函数之间的数据交换提供了一种可以代替函数参数与返回值的方式
  • 外部变量的用途还表现在它们与内部变量相比具有更大的作用域和更长的生存期,外部变量是永久存在的它们的值在一次函数调用到下一次函数调用之间保持不变。因此在需要函数共享某些数据时最方便的方式是把这些共享数据定义为外部变量而不是作为函数参数传递

接下来我们可以看一个例子

用逆波兰表示法表示数

在逆波兰表示法中所有运算符都跟在操作数后面,比如
(1-2)(4+5) == 12-45+

while(下一个运算符或操作数不是文件结束指示符)
if(是数)
将该数压入栈中
else if(是运算符)
弹出所需数目的操作数
执行运算
将结果压入到栈中
else if(是换行符)
弹出并打印栈顶的值
else
出错

  • 栈的压入与弹出操作比较简单,但是,如果把错误检测与恢复操作都加进来,该程序就很长,最好把它们设计成独立的函数,而不要把它们作为程序中重复的代码使用。另外还需要一个单独的函数来取下一个输入运算符或操作数
  • main函数不需要了解控制栈的变量信息,它只进行压入与弹出操作。因此可以把栈及相关信息放在外部变量中,并只提供push与pop函数访问,而不能被main函数访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include <stdio.h>
#include<math.h>
#include <string.h>
#include<stdlib.h>
#include<ctype.h>
#define MAXOP 100//操作数或运算符的最大长度
#define NUMBER '0'//标识找到了一个数
void push(double);//把f压入到值栈中
double pop(void);//弹出并返回栈顶的值
int getop(char []);//截取下一个运算符或数值操作数
int getch(void);//读取下一个待处理的字符
int ungetch(int);//用于把字符放回到输入中
//程序中经常会出现这样的情况:程序不能确定它已经读入的输入是否足够,除非超前多读入一些输入
void clear(void);
int main()
{
int type;
double op1,op2;
char s[MAXOP];
while((type=getop(s))!=EOF){
switch(type){
case NUMBER:
push(atof(s));
break;
case '+':
push(pop()+pop());
break;
case '*':
push(pop()*pop());
break;
case '-'://不满足交换律运算符左右操作数必须加以区分
op2=pop();
push(pop()-op2);
break;
case '/':
op2=pop();
if(op2!=0.0)
push(pop()/op2);
else
printf("error:zero dibisor\n");
break;
case '%':
op2=pop();
if(op2!=0.0)
push(fmod(pop(),op2))
else printf("error: zero divisor\n");
case '\n':
printf("\t%.8g\n",pop());
break;
case '?'://在不弹出元素的情况下打印栈顶元素
op2=pop();
printf("\t%.8g\n",op2);
push(op2);
break;
case 'c'://清空栈
clear();
break;
case 's'://交换栈顶两个元素
op1=pop();
op2=pop()
push(op1);
push(op2);
break;
case 'd'://复制栈顶元素
op2=pop();
push(op2);
push(op2);
break;
default:
printf("error:unknown command %s\n",s);
break;
}
}
return 0;
}
//-------------------------------------------------
#define BUFSIZE 100
#define maxval 100//val的最大深度
int sp=0;//下一个空闲栈位置
double val[maxval];//值栈
void push(double f)//把f压入到值栈中
{
if(sp<maxval)val[sp++]=f;
else printf("error:stack full %g failed\n",f);
}
double pop(void)//弹出并返回栈顶的值
{
if(sp>0) return val[--sp];
else
{ printf("error:stack empty\n");
return 0.0;
}
void clear(void)//清空栈
{
sp=0;
}
}
//-------------------------------------------------


//-------------------------------------------------
char buf[BUFSIZE];//用于ungetch函数的缓冲区
int bufp=0;//buf中下一个空闲位置
int getch(void)
{
return (bufp>0)?buf[--bufp]:getchar();
}
int ungetch(int c)
{
if(bufp>=BUFSIZE) printf("ungetch: too many characters\n");
else buf[bufp++]=c;
}
//-------------------------------------------------
int getop(char s[])//截取下一个运算符或数值操作数
{
int i,c;
while((s[0]=c=getch())==' '||c=='\t');
s[1]='\0';
if(!isdigit(c)&&c!='.'&&c!='-')return c;//不是数
i=0;
if(c=='-')//考虑负数的情况
{
if(isdigit(c=getch())||c=='.')
s[++i];
}
if(isdigit(c))//收集整数部分
while(isdigit(s[++i]=c=getch()));
if(c=='.')//收集小数部分
while(isdigit(s[++i]=c=getch()));
s[i]='\0';
if(c!=EOF)ungetch(c);
return NUMBER;
}
//-------------------------------------------------

作用域规则

构成C语言程序的函数与外部变量可以分开进行编译.

一个程序可以存放在几个文件中,原先已编译过的函数可以从库中进行加载

我们这里或许对以下几个问题感兴趣:

  • 如何进行声明才能确保变量在编译时被正确声明
  • 如何安排声明的位置才能确保程序在加载时各部分能够正确连接
  • 如何组织程序中的声明才能确保只有一份副本
  • 如何初始化外部变量

现在我们可以回想一下我们刚刚写的计算器程序,将它分散到多个文件中.

从实践的角度讲,计算器程序较小,不值得分成几个文件存放,但通过它可以很好地说明较大的程序中遇到的类似问题


名字的作用域是指程序中可以使用该名字的部分

  • 对于在函数开头声明的自动变量来说,其作用域是声明该变量名的函数.
  • 而不同函数中声明的具有相同名字的各个局部变量之间没有任何关系.

而函数的参数也是这样的,实际上可以将它看作是局部变量


然后我们来看我们之前在计算器中声明的几个函数和外部变量.

1
2
3
4
5
6
7
8
main(){...}

int sp=0;
double val[MAX];

void push(double f){...}

double pop(void){...}

那么我们现在知道

pushpop两个函数不需要进行任何声明就可以通过名字访问变量spval

但是,这两个变量名都不能用在main函数中,pushpop函数也不能用在main中

另一方面,如何要在外部变量的定义之前使用该变量,或者外部变量的定义与变量的使用不在同一个源文件中,则必须在相应的变量声明中强制使用关键字extern

将外部变量的声明与定义严格区分开来很重要

  • 变量声明用于说明变量的属性(主要是变量类型)
  • 变量定义除此以外还将引起存储器的分配

在一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次,而其他文件可以通过extern声明来访问它(定义外部变量的源文件中也可以包含对该外部变量的extern声明)

外部变量的定义中必须指定数组的长度,但extern声明则不一定要指定数组的长度

外部变量的初始化只能出现在其定义中

假如函数pushpop定义在一个文件中,而变量在另一个文件中定义并被初始化(通常不大可能这样组织程序),则需要通过下面这些定义与声明把这些函数和变量”绑定在一起”

1
2
3
4
5
6
7
8
9
10
11
12
//在文件file1中:

extern int sp;
extern double val[];

void push(double f){...}

double pop(void){...}

//在文件file2zhong :
int sp=0;
double val[MAXVAL];

总结

其实我们知道函数的定义和使用语法都是非常简单的

我们学习函数这一种结构其实更重要的是如何利用函数的思想去思考问题,去解决问题.

  • 当我们考虑设计一个程序时,我们不可能一脑子写到底,想到哪写到哪,把所有东西都放在主函数里面,这样的程序就像一个定时炸弹,因为当它爆炸的时候,你的程序也就无法运行,而你想拆掉这一个炸弹,你需要从你设计的各个电路里面去寻找,当你终于找到了这一个线路,修改后,你可能会发现你的程序又会出现问题,因为你修改的这个线路极有可能影响到了其他的线路.所以我们可不可以在一开始设计的时候,用函数的思想,先将程序进行拆分,每一个模块都用一个函数来表示,这样下来你的逻辑架构就会非常的清晰,也会极大便利敲代码的效率
  • 当我们看到一段程序重复使用的时候,我们能不能想到将一段程序封装成一个函数,这样我们可以在各个位置调用,而且使用起来也非常简单和方便,对于使用函数的时候我们只需要考虑参数和返回值,而对于定义函数的时候我们只需要考虑这段程序的实现
  • 而且当我们重复使用的这段代码出现bug时,我们可能需要在不同的位置修改一样的代码,这样会让我们的效率大大降低,而且使我们流程及其复杂也就更加容易出错,而我们将这段代码封装成函数时,我们只需要考虑这个函数中出现的问题,而不受其他位置的影响,这样也会使我们的效率大大提升

在实际问题中函数不仅仅是我们想象的那么难,也不会是我们想象的那么简单,因为一种语法规则我们更多需要考虑地是如何使用这一种语法规则去使我们的代码或者是程序效率最大化,去使我们设计的逻辑架构更加清晰

它只是我们的工具,是我们支配它,而不是它支配我们

指针

说起指针,它是C/C++中极具特色的数据类型

有些复杂但也很实用

指针是什么,我先用一句话概括

一种特殊的数据类型,这种类型存储的是地址

  • 所以指针并不会是我们想象中的那么复杂,归根到底它也只是一种数据类型,而复杂地只是我们应该怎么使用这样一种数据类型,我们什么时候会用到这一个数据类型,这才是我们需要考虑的地方

要理解复杂类型其实很简单

一个类型里会出现很多运算符,他们也像普通的表达式一样,有优先级,其优先级和运算优先级一样,所以我总结了一下其原则:从变量名处起,根据运算符优先级结合,一步一步分析.下面让我们先从简单的类型开始慢慢分析吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int p; //这是一个普通的整型变量  


int *p; //首先从P 处开始,先与*结合,所以说明P 是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针


int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组

int *p[3]; //首先从P 处开始,先与[]结合,因为其优先级比*高,所以P 是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组

int (*p)[3]; //首先从P 处开始,先与*结合,说明P 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针

int **p; //首先从P 开始,先与*结合,说是P 是一个指针,然后再与*结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据.由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针.

int p(int); //从P 处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据

Int (*p)(int); //从P 处开始,先与指针结合,说明P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针

int *(*p(int))[3]; //可以先跳过,不看这个类型,过于复杂从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int 结合,说明函数有一个整型变量参数,然后再与外面的*结合,说明函数返回的是一个指针,,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与*结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数.

我们列举到这里其实也差不多了,我们的任务也就这么多,理解了上面我所说的这几个类型,其他的类型对我们来说也就是一件很容易的事情,不过我们一般不会用太复杂的类型,那样会大大减小程序的可读性,请慎用,上面的类型已经足够我们使用了

下面我们就来细细说一下关于指针的东西


指针细节

指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。

要搞清一个指针需要搞清指针的四方面的内容:

指针的类型

指针所指向的类型

指针的值或者叫指针所指向的内存区

指针本身所占据的内存区。

接下来让我们分别说明

  • 我们先声明几个指针做例子

    1
    2
    3
    4
    5
    (1)int*ptr;  
    (2)char*ptr;
    (3)int**ptr;
    (4)int(*ptr)[3];
    (5)int*(*ptr)[4];

指针的类型

从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。

这是指针本身所具有的类型。让我们看看例一中各个指针的类型:

1
2
3
4
5
(1)int*ptr;//指针的类型是int*
(2)char*ptr;//指针的类型是char*
(3)int**ptr;//指针的类型是int**
(4)int(*ptr)[3];//指针的类型是int(*)[3]
(5)int*(*ptr)[4];//指针的类型是int*(*)[4]

这样来看我们可以很清晰容易地找出指针类型

指针所指向的类型

当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。

  • 从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。
1
2
3
4
5
(1)int*ptr; //指针所指向的类型是int
(2)char*ptr; //指针所指向的的类型是char
(3)int**ptr; //指针所指向的的类型是int*
(4)int(*ptr)[3]; //指针所指向的的类型是int()[3]
(5)int*(*ptr)[4]; //指针所指向的的类型是int*()[4]

在指针的算术运算中,指针所指向的类型会让对我们理解指针有极大的帮助

  • 指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。

当你对C 越来越熟悉时,你会发现,把与指针搅和在一起的”类型”这个概念分成”指针的类型”和”指针所指向的类型”两个概念,是精通指针的关键点之一。

指针的值

或者叫针所指向的内存区或地址

指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。

在32 位程序里,所有类型的指针的值都是一个32 位整数,因为32 位程序里内存地址全都是32 位长。

  • 指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。

  • 我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域

    我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。

    指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。

所以没当我们遇到一个指针,都应该思考一下:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?这才是指针最重要的三个点

指针本身所占据的内存区

指针本身占了多大的内存?

你只要用函数sizeof(指针的类型)就知道了。在32 位平台里,指针本身占据了4 个字节的长度。指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。

运算符&和*

  • &是取地址运算符
  • *是间接运算符

&a 的运算结果是一个指针

指针的类型就是*a

指针所指向的类型是a 的类型,指针所指向的地址,那就是a 的地址。

*a 的结果是a所指向的东西它的类型是a指向的类型,它所占用的地址是a所指向的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int a=12; int b; int *p; int **ptr;  

p=&a;
//&a 的结果是一个指针,类型是int*,指向的类型是
//int,指向的地址是a 的地址

*p=24;
//*p 的结果,在这里它的类型是int,它所占用的地址是
//p 所指向的地址,显然,*p 就是变量a
printf("p:%d\ta:%d\n",*p,a);

ptr=&p;
//&p 的结果是个指针,该指针的类型是p 的类型加个*,
//在这里是int **。该指针所指向的类型是p 的类型,这
//里是int*。该指针所指向的地址就是指针p 自己的地址
printf("ptr:%d\n",**ptr);

*ptr=&b;
//*ptr 是个指针,&b 的结果也是个指针,且这两个指针
//的类型和所指向的类型是一样的,所以用&b 来给*ptr 赋
//值就是毫无问题的了

**ptr=34;
//*ptr 的结果是ptr 所指向的东西,在这里是一个指针,
//对这个指针再做一次*运算,结果是一个int 类型的变量。
printf("b:%d\tptr:%d\n",b,**ptr);

-----out-----------------------------
p:24 a:24
ptr:24
b:34 ptr:34

指针表达式

一个表达式的结果是一个指针,那么这个表达式就叫指针表达式

1
2
3
4
5
6
7
8
int a,b;  
int array[10];
int *pa;
pa=&a; //&a 是一个指针表达式。
Int **ptr=&pa; //&pa 也是一个指针表达式。
*ptr=&b; //*ptr 和&b 都是指针表达式。
pa=array;
pa++; //这也是指针表达式。

好了,当一个指针表达式的结果指针已经明确地具有了指针自身占据的内存的话,这个指针表达式就是一个左值,否则就不是一个左值。在例七中,&a 不是一个左值,因为它还没有占据明确的内存。ptr 是一个左值,因为ptr 这个指针已经占据了内存,其实ptr 就是指针pa,既然pa 已经在内存中有了自己的位置,那么ptr 当然也有了自己的位置。

数组和指针的关系

数组的数组名其实可以看作一个指针

1
2
3
4
int array[10]={0,1,2,3,4,5,6,7,8,9},value;  
value=array[0]; //也可写成:value=*array;
value=array[3]; //也可写成:value=*(array+3);
value=array[4]; //也可写成:value=*(array+4);
  • 上例中,一般而言数组名array 代表数组本身,类型是int[10],但如果把array 看做指针的话,它指向数组的第0 个单元,类型是int 所指向的类型是数组单元的类型即int。因此array 等于0 就一点也不奇怪了。同理,array+3 是一个指向数组第3 个单元的指针,所以*(array+3)等于3。其它依此类推。
1
2
3
4
5
6
7
8
9
char *str[3]={  
"Hello,thisisasample!",
"Hi,goodmorning.",
"Helloworld"
};
char s[80];
strcpy(s,str[0]); //也可写成strcpy(s,*str);
strcpy(s,str[1]); //也可写成strcpy(s,*(str+1));
strcpy(s,str[2]); //也可写成strcpy(s,*(str+2));

上例中,str是一个三单元的数组,该数组的每个单元都是一个指针,这些指针各指向一个字符串。把指针数组名str当作一个指针的话,它指向数组的第0 号单元,它的类型是char * ,它指向的类型是char

  • str 也是一个指针,它的类型是char ,它所指向的类型是char,它指向的地址是字符串”Hello,thisisasample!”的第一个字符的地址,即’H’的地址。

注意:字符串相当于是一个数组,在内存中以数组的形式储存,只不过字符串是一个数组常量,内容不可改变,且只能是右值.

  • str+1也是一个指针,它指向数组的第1 号单元,它的类型是char*,它指向的类型是char
  • *(str+1)也是一个指针,它的类型是char*,它所指向的类型是char,它指向”Hi,goodmorning.”的第一个字符’H’
  • 总结一下数组的数组名(数组中储存的也是数组)的问题:
    声明了一个数组TYPE array[n],则数组名称array 就有了两重含义:
    • 它代表整个数组,它的类型是TYPE[n];
    • 它是一个常量指针,该指针的类型是TYPE* ,该指针指向的类型是TYPE,也就是数组单元的类型,该指针指向的内存区就是数组第0 号单元,该指针自己占有单独的内存区,注意它和数组第0 号单元占据的内存区是不同的。该指针的值是不能修改的,即类似array++的表达式是错误的。在不同的表达式中数组名array 可以扮演不同的角色
      • 在表达式sizeof(array)中,数组名array 代表数组本身,故这时sizeof 函数测出的是整个数组的大小。
      • 在表达式*array 中,array 扮演的是指针,因此这个表达式的结果就是数组第0 号单元的值。sizeof(\*array)测出的是数组单元的大小。
      • 表达式array+n(其中n=0,1,2,…..)中,array 扮演的是指针,故array+n 的结果是一个指针,它的类型是TYPE *,它指向的类型是TYPE,它指向数组第n号单元。故sizeof(array+n)测出的是指针类型的大小。在32 位程序中结果是4

指针的算术运算

  • 在指针中如果加或减一个整数时

    这里代表的不是通常数组的加减运算,它的运算是以内存单元为单位

接下来让我们看一个例子:

1
2
3
char a[10];  
int *value=(int *)a; //强制类型转换并不会改变a的类型
value++;

就像上面我们说的,我们先来考虑这个指针value的三点

  • 类型是int*
  • 指向的类型是int
  • 被初始化指向整型变量a

然后指针value被加1,这时候编译器是如何处理的呢

它把指针value的值加上了sizeof(int),在32 位程序中,是被加上了4(因为在32 位程序中,int 占4 个字节)

由于地址是用字节做单位的,故value所指向的地址由原来的变量a 的地址向高地址方向增加了4 个字节。

由于char 类型的长度是一个字节,所以,原来value是指向数组a 的第0 号单元开始的四个字节,此时指向了数组a中从第4 号单元开始的四个字节。

我们可以用一个指针和一个循环来遍历一个数组,看例子:

1
2
3
4
5
6
7
int array[20]={0};  
int *value=array;
for(int i=0;i<20;i++)
{
(*value)++;
value++;
}

这个例子将整型数组中各个单元的值加1。由于每次循环都将指针value加1 个单元,所以每次循环都能访问数组的下一个单元

接下来我们再来看一个例子

1
2
3
char a[20]="You_are_a_boy";  
int *value=(int *)a;
value+=5;

而这一段代码编译器是如何处理的呢

按照上面的思路我们可以知道

将指针value的值加上5 乘sizeof(int),在32 位程序中就是加上了5X4=20。由于地址的单位是字节,故现在的value所指向的地址比起加5 后的value所指向的地址来说,向高地址方向移动了20 个字节。
在这个例子中,没加5 前的value指向数组a 的第0 号单元开始的四个字节,加5 后,value已经指向了数组a 的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵活性。如果上例中,value是被减去5,那么处理过程大同小异,只不过value的值是被减去5 乘sizeof(int),新的value指向的地址将比原来的value所指向的地址向低地址方向移动了20 个字节。

这里我再举一个我们很容易错的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>  
int main()
{
char a[20]=" You_are_a_boy";
char *p=a;
char **ptr=&p;
//printf("p=%d\n",p);
//printf("ptr=%d\n",ptr);
//printf("*ptr=%d\n",*ptr);
printf("**ptr=%c\n",**ptr);
ptr++;
//printf("ptr=%d\n",ptr);
//printf("*ptr=%d\n",*ptr);
printf("**ptr=%c\n",**ptr);
}
  • 误区

    • 输出答案为Y 和o
      误解:ptr 是一个char 的二级指针,当执行ptr++;时,会使指针加一个sizeof(char),所以输出如上结果,这个可能只是少部分人的结果.

    • 输出答案为Y 和a

      误解:ptr 指向的是一个char 类型,当执行ptr++;时,会使指针加一个sizeof(char )(有可能会有人认为这个值为1,那就会得到误区一的答案,这个值应该是4,参考前面内容), 即&p+4; 那进行一次取值运算不就指向数组中的第五个元素了吗?那输出的结果不就是数组中第五个元素了吗?答案是否定的.

正解: ptr 的类型是char *,指向的类型是一个char 类型,该指向的地址就是p的地址(&p),当执行ptr++;时,会使指针加一个sizeof(char),即&p+4;那(&p+4)指向哪呢,这个你去问上帝吧,或者他会告诉你在哪?所以最后的输出会是一个随机的值,或许是一个非法操作.

总结

一个指针ptrold 加(减)一个整数n 后,结果是一个新的指针ptrnew,ptrnew 的类型和ptrold 的类型相同,ptrnew 所指向的类型和ptrold所指向的类型也相同。ptrnew 的值将比ptrold 的值增加(减少)了n 乘sizeof(ptrold 所指向的类型)个字节。就是说,ptrnew 所指向的内存区将比ptrold 所指向的内存区向高(低)地址方向移动了n 乘sizeof(ptrold 所指向的类型)个字节。指针和指针进行加减:两个指针不能进行加法运算,这是非法操作,因为进行加法后,得到的结果指向一个不知所向的地方,而且毫无意义。两个指针可以进行减法操作,但必须类型相同,一般用在数组方面,不多说了。

参考

https://blog.csdn.net/constantin_/article/details/79575638

CATALOG
  1. 1. C语言备课
    1. 1.1. 函数
      1. 1.1.1. 函数定义
      2. 1.1.2. 返回非整型值的函数
      3. 1.1.3. 外部变量
      4. 1.1.4. 作用域规则
      5. 1.1.5. 总结
    2. 1.2. 指针
      1. 1.2.1. 指针细节
        1. 1.2.1.1. 指针的类型
        2. 1.2.1.2. 指针所指向的类型
        3. 1.2.1.3. 指针的值
        4. 1.2.1.4. 指针本身所占据的内存区
      2. 1.2.2. 运算符&和*
      3. 1.2.3. 指针表达式
      4. 1.2.4. 数组和指针的关系
      5. 1.2.5. 指针的算术运算
        1. 1.2.5.1. 总结
    3. 1.3. 参考