1.输入a和b两个整数,按先大后小的顺序输出a和b
方法一: 利用改变指针变量的值来实现(即改变指针的指向)
#include <stdio.h>
#include <stdlib.h>
/*
输入a和b两个整数,按先大后小的顺序输出a和b;
方法一:利用改变指针变量的值来实现(即改变指针的指向)
*/
int main() {
int *p1,*p2,a,b;
int *temp;
printf("Please enter two numbers:");
scanf("%d %d",&a,&b);//注意在运行时,输入数据的格式要和scanf中的格式相同;
p1=&a;
p2=&b;
if(a<b)
{
temp=p1;
p1=p2;
p2=temp;
}
printf("Max=%d,Min=%d",*p1,*p2);
return 0;
}
方法二: 利用交换指针变量指向的变量值(即交换a和b的值)
#include <stdio.h>
#include <stdlib.h>
/*
方法二:利用交换指针变量指向的变量值(即交换a和b的值)
*/
int main(){
int *p1,*p2,a,b;
int temp;
printf("Please enter two numbers:");
scanf("%d,%d",&a,&b);
p1=&a;
p2=&b;
if(a>b)
{
// *p1和*p2实际就是表示指针变量p1和 p2指向的变量(即a 和 b)
temp=*p1;
*p1=*p2;
*p2=temp;
}
printf("%d %d",a,b);
return 0;
}
方法三: 使用函数处理,将指向两个整型变量的指针变量作为实参传递给swap函数的形参指针变量,在函数中通过指针变量实现交换两个变量的值。
注:
(1)在函数调用时,将实参变量的值传递给形参变量,采取的依然是单向传送的“值传递”方式。(实参—>形参)
(2)不能企图通过改变指针形参的值而改变指针实参的值。因为 C语言中实参变量和形参变量之间的数据传递是单向的"值传递"方式。用指针变量作为函数参数依然要遵循这一规则。
(3)不可能通过执行调用函数来改变实参指针变量的值,但是可以改变实参指针变量所指向变量的值
(1)下面这段代码就是试图通过实参变量的值传入形参变量,然后再改变形参变量的值,然后又传回到实参变量。这显然是无法实现的
int main(){
void swap(int *p1,int *p2);
int *pointer_1,*pointer_2,a,b;
printf("Please enter two numbers:");
scanf("%d,%d",&a,&b);
pointer_1=&a;
pointer_2=&b;
if(a>b)
{
swap(pointer_1,pointer_2);
}
printf("%d %d",*pointer_1,*pointer_2);
return 0;
}
void swap(int *p1,int *p2)
{
//交换形参指针变量的值
int *temp;
temp=p1;
p1=p2;
p2=temp;
}
结果并不能实现a和b的值交换,因为改变形参指针变量p1和p2的值并不能改变实参指针变量pointer_1和pointer_2的值。
(2)通过执行调用函数来改变实参指针变量所指向变量的值。(可以实现两个输入的整数按从小到大的顺序输出)
/*
方法三:通过执行调用函数来改变实参指针变量所指向变量的值,从而实现a和b的值交换*/
int main(){
void swap(int *p1,int *p2);
int *pointer_1,*pointer_2,a,b;
printf("Please enter two numbers:");
scanf("%d,%d",&a,&b);
pointer_1=&a;
pointer_2=&b;
if(a>b)
{
swap(pointer_1,pointer_2);
}
printf("%d %d",*pointer_1,*pointer_2);//通过执行调用函数可以实现改变实参指针变量所指向变量的值。
// printf("%d %d",a,b); //两种输出效果一样,因为*pointer_1和a是一个意思
return 0;
}
void swap(int *p1,int *p2)
{
int temp;
temp=*p1;
*p1=*p2;
*p2=temp;
}
注:
函数的调用可以(且只可以)得到一个返回值(即函数值),而使用指针变量作参数,可以得到多个变化了的值。
拓展:
输入3个整数a,b,c, 要求按照从小到大的顺序将它们输出,用函数实现。
#include <stdio.h>
#include <stdlib.h>
/*
输入3个整数a,b,c,要求按照从小到大的顺序输出,利用函数的形式
*/
int main() {
void exchange(int *p1,int *p2,int *p3);
int *pointer_1,*pointer_2,*pointer_3,a,b,c;
printf("Please enter three numbers:");
scanf("%d %d %d",&a,&b,&c);
pointer_1=&a;
pointer_2=&b;
pointer_3=&c;
printf("%d\n",pointer_1);
exchange(pointer_1,pointer_2,pointer_3); //注意:exchange函数中的参数都是指针变量
printf("%d %d %d\n",*pointer_1,*pointer_2,*pointer_3);
printf("%d\n",pointer_1); //可以发现实参指针变量在执行完调用函数后依然没有改变
return 0;
}
void exchange(int *p1,int *p2,int *p3)
{
void swap(int *p_1,int *p_2);
if(*p1>*p2)
swap(p1,p2); //swap函数中的参数是指针变量,但其中交换的是指针变量指向的变量的值
if(*p2>*p3)
swap(p2,p3);
if(*p1>*p3)
swap(p1,p3);
}
void swap(int *p_1,int *p_2)
{
int temp;
temp=*p_1;
*p_1=*p_2;
*p_2=temp;
}
注意:
main函数中的3个实参指针变量的值在执行完调用函数并未改变,这就正好说明前面所说的**“执行完调用函数,并不能改变实参指针变量的值,但是可以改变实参指针变量指向的变量的值”**
2. 如何遍历数组中的全部元素
引用一个数组元素主要有以下两种方法:
(1)下标法:如 a[i];
(2)指针法:
格 式 | 具体说明 |
---|---|
*(a+i) | a是数组名,通过数组名和元素序号计算元素地址,从而找到该元素 |
*(p+i) | p是指向数组元素的指针变量,其初值为p=a |
注:
(1)其实上面两者与a[i]是等价的;
(2)*(a+i)即使按数组首元素的地址加上相对位移量得到要找的元素的地址,然后找出该地址的内容。
相对位移量=i * 基本类型的字节长度;(如int类型一般就是4个字节,而char型一般就是1个字节)
输出一个拥有10个元素的整形数组的全部元素。
方法 一:利用数组下标的方式访问数组元素
int main() {
int a[10];
int i,j,length;
length=sizeof(a)/sizeof(int); //求出数组的长度
for(i=0;i<length;i++)
{
scanf("%d",&a[i]);
}
for(j=0;j<length;j++)
{
printf("%d\n",a[j]);
}
return 0;
}
方法二:通过计算数组名计算数组元素地址,从而找出元素的值。
int main()
{
int a[10];
int i,length;
length=sizeof(a)/sizeof(int);
for(i=0;i<length;i++)
{
scanf("%d",&a[i]);
}
for(i=0;i<length;i++)
{
printf("%d\t",*(a+i)); //通过数组名和元素序号计算元素地址,再找到该元素。
}
return 0;
}
方法三:利用指针变量指向数组元素的方式访问数组元素 (这样不必每次都重新计算地址)
int main()
{
int a[10];
int *p;
//p=a; //与p=&a[0]等价
for(p=a;p<(a+10);p++)
{
scanf("%d",p);
}
for(p=a;p<(a+10);p++)
{
printf("%d\t",*p);
}
return 0;
}
注意一个易错点:
int main()
{
int a[10];
int i,length,*p;
length=sizeof(a)/sizeof(int);
p=a;
for(i=0;i<length;i++)
{
scanf("%d",p++);
}
p=a; //注意:这里一定要重新将 &a[0]的初始值赋给p,不然此时的p在下一个for循环的初始值就为 &a[10]了。
for(i=0;i<length;i++,p++)
{
printf("%d\t",*p); //通过数组名和元素序号计算元素地址,再找到该元素。
}
return 0;
}
上面的第二个for循环也可以稍作修改,效果完全一样
for(i=0;i<length;i++) //注意修改的部分
{
printf("%d\t",*p++); //通过数组名和元素序号计算元素地址,再找到该元素。
}
return 0;
3. 通过指针引用数组元素的一些注意事项
(1) 指向数组的指针变量也可以带下标,如p[i],.因为在程序编译时,对下标的处理方法是转换为地址的,对p[i]处理成*(p+i),因此如果p是指向一个整型数组元素a[0],则p[i]代表a[i].
(2) *p++ (注意理解)
由于++和*同优先级,且结合方向为自右向左,因此它等价于*(p++)。先引用p的值,实现*p的运算,然后再使p自增1。
*p++相当于*(p++),也就是下面两步的组合:
第一步:*p
第二步:p++
//先取*p,再使p值加1;
*(++p)则是下面两步的组合:
第一步:p++
第二步:*p
//先使p值加1,再取*p;
++(*p)则表示p所指向的元素值加1,如果p=a,则++(*p)相当于++a[0].
(3)如果p当前指向a数组中第i个元素a[i],则下列表达形式一一对应:
第一种表达方式 | 对应的第二种表达方式 |
---|---|
*(p- -) | a[i++] |
*(++p) | a[++i] |
*(- -p) | a[- -i] |
4. 用数组名作函数参数和用变量名作函数参数
(1)当用数组名作函数参数时,如果形参数组中各元素的值发生变化,则实参数组元素的值也随之发生变化。(常使用这种方法改变实参数组的值)
实参数组名代表数组首元素的地址,而形参是用来接收来自实参传递过来的数组首元素地址的。因此,形参应该是一个指针变量(只有指针变量才能存放地址)。实际上,C编译都是将形参数组名作为指针变量来处理的。
例:
函数fun的参数写成数组形式
fun(int arr[],int n)
//arr为形参数组名
但程序在编译时是将形参数组名arr按指针变量处理的,相当于将fun的首部写成
fun(int *arr,int n)
这两种写法完全等价。
(2) 以变量名和数组名作为函数参数的比较
实参类型 | 变量名 | 数组名 |
---|---|---|
要求形参的类型 | 变量名 | 数组名或指针变量 |
传递的信息 | 变量的值 | 实参数组首元素的地址 |
通过函数调用能否改变实参的值 | 不能实参变量的值 | 能改变实参数组的值 |
注:
实参数组名代表一个固定的地址,或者说是指针常量,但形参数组名并不是一个固定的地址,而是按指针变量处理。
(3) 用数组名作函数参数
/*
将数组a中n个整数按相反顺序存放。 (两头的元素相互交换)
*/
int main() {
void inv(int *arr,int n);
int a[10]={1,5,6,9,7,8,3,2,0,4};
int i,length;
length=sizeof(a)/sizeof(int);
printf("The original order is:\n");
for(i=0;i<length;i++)
{
printf("%d\t",a[i]);
}
inv(a,length);//两头交换顺序
printf("\n");
printf("The new order is:\n");
for(i=0;i<length;i++)
{
printf("%d\t",a[i]);
}
return 0;
}
void inv(int arr[],int n) //写成int *arr也可以
{
int i=0,j,temp;
int flag=n/2;
while(i<flag)
{
j=n-i-1;
temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
i++;
}
return;
}
(4) 用指针变量作函数参数
int main()
{
void inv(int *arr,int n);
int a[10]={1,5,6,9,7,8,3,2,0,4};
int i,length;
length=sizeof(a)/sizeof(int);
printf("The original order is:\n");
for(i=0;i<length;i++)
{
printf("%d\t",a[i]);
}
inv(a,length);//两头交换顺序
printf("\n");
printf("The new order is:\n");
for(i=0;i<length;i++)
{
printf("%d\t",a[i]);
}
return 0;
}
//改进之后
void inv(int *arr,int n)
{
int *i,*j,*p,temp;
int flag=n/2;
i=arr;
j=arr+n-1;
p=arr+flag;
for(;i<p;i++,j--)
{
temp=*i;
*i=*j;
*j=temp;
}
return;
}
(5) 如果用指针变量作实参,必须先使指针变量有一个确定值,指向一个已定义的对象。
如下面是不正确的:
int main()
{
void f(int *x,int n);
int *p;//指针变量未指向数组元素(或者说一个已定义的对象)。
...
f(p,10);
...
}
void f(int *x,int n)
{
...
return;
}
编译时出错,原因是指针变量p没有确定值,谈不上指向哪个变量。
5. 使用指针引用多维数组
(1) 二维数组a的有关指针
表示形式 | 含义 | 地址 |
---|---|---|
a | 二维数组名,指向一维数组a[0],即0行首地址 | 2000 |
a[0],*(a+0),*a | 0行0列元素地址 | 2000 |
a+1,&a[1] | 1行首地址 | 2016 |
a[1],*(a+1) | 1行0列的地址,即&a[1][0] | 2016 |
a[1]+2,*(a+1)+2,&a[1][2] | 1行2列元素地址,即&a[1][2] | 2024 |
*(a[1]+2),*(*(a+1)+2),a[1][2] | 1行2列元素的值,即a[1][2] | 元素值为13 |
注:
(1) 二维数组a的定义如下:
int a[3][4]={{1,3,5,7},{9,11,13,15},{17,19,21,23}}
且假设二维数组a的首行的首地址为2000.
(2) C语言规定了数组名代表数组首元素的地址。所以
a为二维数组名,指向一维数组a[0](二维数组的首元素),即0行首地址;
a[0]为一维数组名,指向一维数组的首元素即a[0][0],即0行0列元素地址;
(3) a+1是二维数组a中序号为1的行的首地址(序号从0算起),即1行首地址,因为a是二维数组名,指向一维数组a[0],表示0行首地址。千万不要和a[0]+1搞混, a[0]+1表示0行1列元素的地址。
(4) *(a+1)表示1行0列元素地址,即&a[1][0],和a[1]等价。
(5) 二维数组名(如a)是指向行的。因此a+1中的‘1’代表一行中全部元素所占的字节数(一行有4个整形元素,所以占16个字节)。一维数组名(如a[0],a[1])是指向列元素的。因此a[0]+1中的‘1’代表一个元素所占的字节数(一个整形元素,所以占4个字节)。
(6) 在指向行的指针前面加一个*号,就转换为指向列的指针。
(7) 在指向列的指针前面加一个&号,就转换为指向行的指针。
(8) a[0]指向0行0列元素的指针,&a[0]则指向二维数组的0行,与a等价。因为a[0]与*(a+0)等价,因此&a[0]与&*a等价,也就是与a等价,它指向二维数组的0行。
(9) &a[i]和a[i]的值虽然一样,但它们的含义不一样。&a[i]或a+i指向行,而a[i]或*(a+i)指向列。
6. 指向多维数组元素的指针变量
(1)指向数组元素的指针变量
/*
指向多维数组元素的指针变量
(1)指向数组元素的指针变量
*/
//用指向元素的指针变量输出二维数组各元素的值。
int main() {
int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};
int *p;
for(p=a[0];p<a[0]+12;p++) //注意:p是指向整形元素的,如这里p初始指向a[0][0];
{
if((p-a[0])%4==0) //p移动4次后换行
{
printf("\n");
}
//(1) %4d表示输出的整形数据宽度为4位,且右对齐,如果整形数据不够4位则前面补空格;
//(2) 如果要左对齐,则在%后加"-"号即可,如"%-4d" ;
printf("%-4d",*p);
}
return 0;
}
(2)指向由m个元素组成的一维数组的指针变量
/*
指向m个元素组成的一维数组的指针变量
*/
int main()
{
int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};
int (*p)[4],i,j;
p=a; // p的值是一维数组a[0]的起始地址;
printf("Please enter row and column:\t");
scanf("%d,%d",&i,&j);
printf("a[%d,%d]=%d\n",i,j,*(*(p+i)+j));
return 0;
}
注:
(1)int (*p)[4]表示定义p为一个指针变量,它指向包含4个整形元素的一维数组.
(2)*p两侧的括号不可以缺少, 若写成*p[4]则表示指针数组。(因为方括号[ ]的运算优先级别高)
(3)
格式 | 含义 |
---|---|
int a[4] | a有4个元素,每个元素都是整型 |
int (*p)[4] | *p有4个元素,每个元素为整型。也即p所指的对象是有4个整型元素的数组,即p是指向一维数组的指针。 |
(4)int (*p)[4]中的p的类型不是int *类型,而是int (*)[4]型,p被定义为指向一维整形数组的指针变量,一维数组有4个元素,因此p的基类型为一维数组,其长度是16字节。
(3) 一个需要注意的地方
int main()
{
int a[4]={1,3,5,7}; //这里是一维数组
int (*p)[4];
//不能写成 p=a,因为这样写表示p的值是&a[0],指向a[0],一维数组名表示首元素的地址;
p=&a; //表示p指向一维数组(行)
printf("%d\n",(*p)[2]);//表示访问p所指向一维数组(行)中的序号为2的元素;
return 0;
}
7. 引用一个字符串的方法
(1) 用字符数组存放一个字符串,可以通过数组名和下标引用字符串中一个字符,也可通过**数组名和格式声明"%s"**输出该字符串。
char string[]="I love China!";
printf("%s\n",string);//用%s格式声明输出string,可以输出整个字符串。
printf("%c\n",string[3]);//使用%c格式输出一个字符数组元素。
(2)用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常量。
//指针变量string指向字符串的第1个字符
char *string="I love China!";//定义指针变量并初始化。
printf("%s\n",string);
注:
(1)C语言中只有字符变量,没有字符串变量。
(2)这两者是等价的
char *string="I am a student";
char *string;
string="I am a student";
上述语句都是将字符串的第一个字符的地址赋给指针变量string,此后string就指向“I am a student”.
当然也可以对string重新赋值,从而改变string的指向。
(3)%s是输出字符串时所用的格式符,在输出项中给出字符指针变量名string,则系统会输出string所指向的第一个字符,然后自动使string加1,使之指向下一个字符,再输出该字符…如此直到遇到字符串结束标志’\0’为止。
8.复制一个字符数组到另外一个字符数组(字符串间的复制)—— 一种效果的多种实现方式
int main()
{
void copy_string(char *a,char *b);
char a[]="I am a teacher.";
char b[]="You are a student.";
int i;
printf("String a is: %s\n",a);
printf("String b is: %s\n",b);
printf("copy string a to string b:\n");
copy_string(a,b);
printf("String a is: %s\n",a);
printf("String b is: %s\n",b);
return 0;
}
void copy_string(char *a,char *b)
{
for(;*b++=*a++;);
}
上述copy_string函数中可以有多种不同方式而实现相同的效果。
(1)
while(*a!='\0')
{
*b++=*a++;
}
*b='\0';
(2)
while((*b=*a)!='\0')
{
b++;
a++;
}
(3)
while((*b++=*a++)!='\0');
(4)
//注意"\0"的ASCII码为0;
while((*b++=*a++)!=0);//千万不要忘了加";"
也可以写成
while(*b++=*a++);
注:
不等于0也就是表示为真, 这里表示先把*a的值赋值给*b,再判断*b是否为0(’\0’的ASCII值为0),为0则跳出while循环,否则继续将a数组中的未复制的字符复制到b数组中去。
(5)
for(;*b++=*a++;);
或
for(;(*b++=*a++)!=0;);
9. 用字符指针作为函数参数时,实参与形参的类型的对应关系有如下几种:
实参 | 形参 |
---|---|
字符数组名 | 字符数组名 |
字符数组名 | 字符指针变量 |
字符指针变量 | 字符指针变量 |
字符指针变量 | 字符数组名 |
10. 字符指针变量和字符数组的比较(七个小点)
(1) 字符数组由若干个元素组成,每个元素中放一个字符。而字符指针变量中存放的是地址(字符串中第一个字符的地址)。
(2)赋值方式: 可以对字符指针变量赋值,但不能对数组名赋值。
char *a;//定义a为字符指针变量
a="I love China!";//将字符串首元素的地址赋给指针变量。合法。
char str[14];
str[0]='I';//对字符数组元素赋值,合法。
str="I love China!";//数组名是地址,是常量,不能被赋值,非法。
(3) 初始化
字符指针变量的赋值
char *a="I love China!";//定义并赋值。
等价于
char *a;//先定义
a="I love China!"; //再赋值
数组的初始化
char str[14]="I love China!";//定义字符数组str,并把字符串赋给数组中各元素。合法。
不等价于(下面这种字符数组的初始化是非法的)
char str[14];
str[]="I love China!";//企图把字符串赋给数组中各元素。错误。
注:
数组可以在定义时对各元素赋初值,但不能用赋值语句对字符数组中全部元素整体赋值。
(4)存储单元的内容。编译时字符数组分配若干存储单元,以存放各元素的值;而对字符指针变量,只分配一个存储单元(Visual C++为指针变量分配4个字节)。
(5)如果定义了字符数组,但未对它赋值,这时数组中的元素的值是不可预料的。可以引用(如输出)这些值,结果显然是无意义的,但不会造成严重的后果,容易发现和改正。
如果定义字符指针变量,应当及时把一个字符变量(或字符数组元素)的地址赋值给它,使它指向一个字符型数据,如果未对它赋予一个地址值,它并未具体指向一个确定的对象。此时如果向该指针变量所指向的对象输入数据,可能会造成严重的后果。(定义了字符指针变量应及时赋值)
char *a;//定义字符指针变量a
scanf("%s",a);//企图从键盘输入一个字符串,使a指向该字符串。错误。
注意: 此时a的值是不可预料的,它可能指向内存中空白的(未用的)用户存储区(好的情况),也有可能指向内存中已存储指令或数据的有用内存段,这就会破坏程序或有用数据,甚至破坏了系统,会造成严重的结果。
因此在定义指针变量后,及时指定其指向,如:
char *a,str[10];
a=str;//使a指向str数组的首元素
scanf("%s",a);//从键盘输入一个字符串存放到a所指向的一段存储单元中。正确。
(5) 指针变量的值是可改变的;而数组名代表一个固定的值(数组元素的地址),是不可改变的。
改变指针变量的值
char *a="I love China!";
a=a+7;//改变指针变量的值,使a指向字符串中的第7个字符。
printf("%s",a);//输出的结果为“China!”
下面是错误的
char str[]={"I love China!"};
str=str+7;//错误
printf("%s",str);
(6) 字符数组中各元素的值是可以改变的(可以对它们再赋值);但字符指针变量指向的字符串常量中的内容是不可以被取代的(不能对它们再赋值)。
char a[]="House";
char *b="House";
a[2]='r';//合法,r取代a数组元素a[2]的原值u.
b[2]='r';//非法。字符串常量不能改变。
(7) 用指针变量指向一个格式字符串,可以用它代替printf函数中的格式字符串。
char *format;
format="a=%d,b=%f\n";//使format指向一个字符串。
printf(format,a,b);//这种printf函数称为可变格式输出函数。
11.用函数指针变量调用函数
//方法一:通过函数名调用函数
//略......
//方法二:通过函数指针变量调用函数
int main()
{
int max(int x,int y);
int a,b,c;
//定义p是一个指向函数的指针变量,它可指向函数的类型为整型且有两个整型参数的函数。
int (*p)(int x,int y); //p的类型用int(*)(int,int)表示。
printf("Please enter a and b:");
scanf("%d,%d",&a,&b);
//可通过改变p的指向,并结合if...else...或switch...case...,从而根据不同情况调用不同的函数。
p=max;//使p指向max函数,函数名代表函数入口地址。
c=(*p)(a,b);//通过指针变量调用max函数。
printf("a=%d\nb=%d\nmax=%d\n",a,b,c);
return 0;
}
int max(int x,int y)
{
int max;
if(x>y)
max=x;
else
max=y;
return max;
}
注:
(1)*p的括号不能省去,这表示p先与*号结合,是指针变量,然后再与后面的()结合,()表示函数,即该指针变量不是指向一般的变量,而是指向函数,最前面的int表示函数值(函数返回值为整型)。
int (*p)(int,int);//p是指向函数的指针。
(2)
int *p(int,int);//p是一个返回int*型指针的函数。
由于()优先级高于*号,它相当于
int *(p(int,int));
就变成了声明一个p 函数(这个函数的返回值是指向整型变量的指针)。
(3)用函数名调用函数,只能调用一个指定的函数;而通过指针变量调用函数比较灵活,可根据不同情况调用不同的函数。因为可以改变指针变量的值从而使其指向不同的函数。
12. 指向m个元素组成的一维数组的指针变量与指向数组元素的指针变量(难点)
(1)
int *p;
p是指向整形数据的,p+1所指向的元素是p所指向的列元素的下一元素(按在内存中的存储的下一个整型元素)。
(2)
int (*p)[4];
p是指向一个包含4个元素的一维数组。如果这时p指向a[0](即p=&a[0]),则p+1不是指向a[0][1],而是指向a[1](第一行的首地址,是指向行),p的值以一维数组的长度为单位。所以“p+1”是加上一个一维数组的长度。
13. 指针数组和多重指针
(1)指针数组:一个其数组元素均为指针型数据的数组。即指针数组中每一个元素都存放一个地址,相当于一个指针变量。如定义一个指针数组:
int *p[4];
由于[ ]比*优先级高,所以p先与[4]结合,形成p[4]形式,这显然是数组形式,。然后再与p前面的 * 结合,*表示此数组是指针类型的,每个数组元素(相当于一个指针变量)都可指向一个整型变量。
注意不要写成
int (*p)[4];//这是指向一维数组的指针变量。
(2) 要用到指针数组的情况:指针数组比较适合用来指向若干字符串(各字符串的长短不一)。
a. 按一般方法,字符串本身就是一个字符数组。因此要设计一个二维的字符数组才能存放多个字符串,但定义二维数组时要指定列数,也就是二维数组中每一行包含的元素个数(即列数)相等。而实际各字符串(如书名)的长短一般是不一样的。如按最长的字符串来定义列数,则会浪费许多内存单元。
b. 使用指针数组的方法则可以很灵活地对字符串进行处理:可分别定义一些字符串,然后用指针数组中的元素分别指向各字符串。如:name[0]中存放字符串"Follow me"的首字符的地址,name[1]中则存放字符串"BASIC"的首字符的地址…如果想对字符串排序,只须改变指针数组中各元素的指向(即改变各元素的值,这些值是各字符串的首地址)。
**(3)**定义指针数组,并赋初值。如:
//定义指针数组name,它有5个元素,其初值分别为 "Python","Java","C#",".NET","JavaScript"的首字符的地址。
char *name[]={"Python","Java","C#",".NET","JavaScript"};
(4)字符比较函数strcmp使用时一个需注意的地方
注意:
(1) strcmp(str1,str2),所以应分别传入的是两个字符串的首字符的地址。
如:strcmp("Hello","World"),其中实质分别传入的是"Hello"、"World"这两个字符串的首字符的地址。
(2) 其原型为extern int strcmp(char *s1,char *s2);
(3) 使用字符串处理函数时,应在程序头文件上加上
"#include <string.h>"
(5)指向指针数据的指针
char **p;
相当于
char *(*p);
可把它分为两个部分来看:
char * 和 (*p)
前面的char *表示p指向的是char *型的数据,也就是说p指向一个字符指针变量(这个字符指针变量指向一个字符型数据)。
如果引用*p,就得到p所指向的字符指针变量的值。如:
char *name[]={"Python","Java","C#",".NET","JavaScript"};
p=name+2;
printf("%d\n",*p);//输出name[2]的值(一个地址)
printf("%s\n",*p);//输出字符串“C#”
(6)小结
利用指针变量访问另一个变量就是"间接访问"。
如果在一个指针变量中存放一个目标变量的地址,这就是“单级间址”;
指向指针数据的指针用的则是"二级间址"。
从理论上说,间址方法可以延伸到更多的即,即多重指针,即多级间接访问。
14. 内存的动态分配
(1) 全局变量是分配在内存中的静态存储区的;非静态的局部变量(包括形参)是分配在内存中的动态存储区的,这个存储区是一个称为栈(Stack)的区域。
(2) 此外,C语言还允许建立内存动态分配区域,以存放一些临时用的数据,这些数据不必在程序的声明中定义,也不必等到函数结束时才释放,而是需要时随时开辟,不需要时随时释放。这些数据临时存放在一个特别的自由存储区,称为堆区。
可以根据需要,向系统申请所需大小的空间。由于未在声明部分定义它们为变量或数组,因此不能通过变量名或数组名去引用这些数据,只能通过指针来引用。
15. 建立内存的动态分配
对内存的动态分配是通过系统提供的库函数来实现的,主要有malloc,calloc,free,realloc这4个函数。
以上四个函数的声明在stdlib.h头文件中,在用到这些函数时,应当用"#include <stdlib.h>"指令。
(1)malloc函数
其函数原型是
void *malloc(unsigned int size);
其作用是在内存的动态存储区中分配一个长度为size的连续空间。
形参size的类型定为无符号整型(不允许为负数)。
此函数的值(即“返回值”)是所分配区域的第一个字节的地址,或者说此函数是一个指针函数,返回的地址指向该分配区域的开头位置。如:
malloc(100);//开辟100字节的临时分配区域,函数值为其第1个字节的地址。
注意:
指针的基类型为void,即不指向任何类型的数据,只提供一个地址。
(2)calloc函数
其函数原型为:
void *calloc(unsigned int n,unsigned int size);
其作用是在内存的动态存储区中分配n个长度为size的连续存储空间,这个空间一般比较大,足以保存一个数组。
用calloc函数可以为一维数组开辟动态存储空间,n为数组元素个数,每个元素长度为size。这就是 动态数组。函数返回指向所分配的起始位置的指针;如果分配不成功,返回NULL。如:
p=callol(50,4);//开辟50*4个字节的临时分配域,把起始地址赋给指针变量p。
(3)free函数
其函数原型为:
void free(void *p);
其作用是释放指针变量p所指向的动态空间,使这部分空间能重新被其他变量使用。
p应是最近一次调用calloc或malloc函数时得到的函数返回值。如:
free(p);//释放指针变量p所指向的已分配的动态空间。free函数无返回值。
(4)realloc函数
其函数原型为:
void *realloc(void *p,unsigned int size);
若已通过malloc或calloc函数获得了动态空间,可用realloc函数对动态空间大小进行重新分配。
(5)一个值得注意的点
以前的C版本提供的malloc和calloc函数得到的是指向字符型数据的指针,其原型为:
char *malloc(unsigned int size);//返回字符型数据的指针
因此若开辟的空间用来存放整数,则要进行类型转换,如:
int *pt;
pt=(int *)malloc(100);//将指向字符数据的指针转换为指向整型数据的指针。
注意:
(1)类型转换只是产生了一个临时的中间值赋给了pt,但没有改变malloc函数本身的类型。
(2)C99标准把malloc,calloc,realloc函数的基类型定为void类型,这种指针称为无类型指针,即不指向哪一种具体的类型数据,只表示用来指向一个抽象的类型的数据,即仅提供一个纯地址,而不能指向任何具体的对象。
16. void指针类型
可以定义一个基类型为void的指针变量(即void *型变量),它不指向任何类型的数据。
请注意:
(1)不要把"指向void类型"理解为能指向"任何的类型"的数据,而应该理解成**“指向空类型"或"不指向确定的类型”**的数据。
(2)在将void指针的值赋给另一指针变量时由系统对它进行类型转换,使之适合于被赋值的变量的类型。
17. 指针的优点
(1) 提高程序效率;
(2) 在调用函数时当指针指向的变量的值改变时,这些值能够为主调函数使用,即可以从函数调用得到多个可改变的值;
(3) 可以实现动态存储分配。
18. 宏定义(易错点)
宏定义:用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名。
注意:
(1)宏名在宏展开时被直接替换为宏定义时宏名后面的字符串。
(2)字符串中可以含任何字符,可以是常数,也可以是表达式。
(3)宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。
#define N 2
#define M N+1
#define NUM 2*M+1
//这时候NUM=2*N+1+1=6,即用N+1直接替换M
int main()
{
int i;
printf("N=%d\n",N);
printf("M=%d\n",M);
printf("NUM=%d\n",NUM);
for(i=1;i<=NUM;i++)
printf("%d\n",i);
return 0;
}
宏定义是由源程序中的宏定义命令#define完成的,宏代换是由预处理程序完成的。
宏定义的一般形式为:
#define 宏名 字符串
表示这是一条预处理命令,所有的预处理命令都以#开头。define是预处理命令。宏名是标识符的一种,命名规则和标识符相同。字符串可以是常数、表达式等。
这里所说的字符串是一般意义上的字符序列,不要和C语言中的字符串等同,它不需要双引号。
19. 定义结构体
(1) 先声明结构体类型,再定义该类型的变量。
struct Student
{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
//struct Student为结构体类型名,student1,student2是结构体变量名。
struct Student student1,student2;
(2) 在声明类型的同时定义变量
struct Student
{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
} student1,student2;
(3)不指定类型名而直接定义结构体类型变量
struct
{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
} student1,student2;
(4) 结构体的初始化需注意的一个地方
struct Data
{
int i;
char ch;
float f;
} a;
//a.i = 65;//错误,“a.d = 65;”是一个可执行代码,需放在函数中才能执行。
int main()
{
a.i = 65;//正确
return 0;
}
struct Data
{
int i;
char ch;
float f;
};
struct Data c;//错误的,为可执行代码,应放在函数中
struct Data a = {65,'H',30.0};//正确
struct Data b = {.i= 0, .ch = 'H',.f = 30.0};//正确
int main()
{
//不能使用赋值语句对结构体变量中的全部成员整体赋值。(字符数组也是这个道理)
/*
struct Data d;
d={86,'K',63.0};//错误
*/
struct Data d={86,'K',63.0};
return 0;
}
/*
struct Data a;
a.i= 0; is not a simple initializer, it is executable code; it cannot occur outside of a function. Use a proper initializer for a.
struct Data a = {65,'H',30.0};
or with named initializer syntax (not available in all compilers, and as yet only in C):
struct Data a = {.i= 0, .ch = 'H',.f = 30.0};
*/
20. 结构体指针
(1) 指向结构体对象的指针变量既可以指向结构体变量,也可以指向结构体数组中的元素。
(2) 3种访问结构体变量的成员的方法:
如果p指向一个结构体变量stu,下列三种用法等价:
1. stu.成员名 (如stu.name)
2. (*p).成员名 (如(*p).name)
3. p->成员名 (如p->name)
(3)用结构体变量和结构体变量的指针作函数参数
a. 用结构体变量的成员作实参;
如用stu[1].name作实参,将实参值传给形参。
b. 用结构体变量作实参;
注意:在函数调用期间形参也要占用内存单元,空间和时间开销较大。
c. 用指向结构体变量(或数组元素)的指针作实参,将结构体变量(或数组元素)的地址传给形参;
21. 用指针处理链表
(1) 静态链表
(2) 动态链表
22. 共用体类型
(1) 几个不同类型的变量共享同一段内存的结构,称为“共用体”类型的结构。
(2) 结构体和共用体的区别
结构体变量所占内存长度是各成员占的内存长度之和,每个成员分别占有其自己的内存单元。
而共用体变量所占的内存长度等于最长的成员所占的内存长度,每个成员存储的起始地址是相同的。
23. 共用体数据类型的特点
(1) 同一个内存段可以用来存放几种不同数据类型的成员,但在每一瞬间只能存放其中一个成员,而不是同时存放几个。即每一瞬间,共用体变量中只能存放一个值。
#include <stdio.h>
#include <stdlib.h>
union Data
{
int i;
char ch;
float f;
};
//a.i=65;//错误的
//union Data a = {65,'H',30.0};//注意:不能同时初始化共同体的3个成员,因为它们占用同一段存储单元。
int main()
{
a.i=65;//注意赋值不能在函数外 ,因为"a.i=65"是一段可执行的代码,必须放在函数中才能执行。
printf("%d\n",a.i);//输出整数65
printf("%c\n",a.ch);//输出字符‘a’
printf("%f\n",a.f);//输出实数0.000000(难点)
return 0;
}
(2) 共用体初始化表中只能有一个常量。
union Data
{
int i;
char ch;
float f;
} a={1,'a',2.5};//错误,不能同时初始化3个成员,因为它们占用同一段存储单元。
union Data b={2,'b','1.5'};//也是错误,理由同上。
union Data c={16};//正确,对第一个成员初始化
union Data d={.ch='h'};//正确,C99允许对指定的一个成员初始化。
(3) 共用体变量中其作用的成员是最后一次被赋值的成员(前面的赋值会被后面的赋值所覆盖)。
a.ch='a';
a.f=2.5;
a.i=66;
在完成上述3个赋值运算后,变量存储单元存放的是最后存入的40,原来的’a’和1.5都被覆盖了。
如果此时用
printf("%c",a.ch);//输出的是字符'b'(因为ASCII码为66,正好为字符'b')
(4) 共用体变量的地址和它的各成员的地址都是同一地址(起始地址相同)。
(5) C99允许同类型的共用体变量相互赋值。如:
b=a;//a和b是同类型的共用体变量,合法
(6) 以前的的C规定不能用共用体变量作为函数参数,但可以使用指向共用体变量的指针作函数参数。C99允许用共用体变量作为函数参数。
(7) 共用体类型可以出现在结构体类型定义中,也可以定义共用体数组。反之,结构体也可以出现在共用体类型定义中,数组也可以作为共用体的成员。
24. 枚举类型
如果一个变量只有几种可能的值,则可以定义为枚举类型。所谓枚举就是把可能的值一一列举出来,变量的值只限于列举出来的值的范围内。
(1) C编译对枚举类型的枚举元素按常量处理,故称枚举常量。不要因为它们是标识符(有名字)而把它们看作变量,不能对它们赋值。
enum Weekday{sun,mon,tue,wed,thu,fri,sat};
sun=7;//错误,不能对枚举元素赋值。
mon=1;//错误,不能对枚举元素赋值。
(2) 每一个枚举元素都代表一个整数,C语言编译按定义时的顺序默认为它们的值为0,1,2,3,4,5…。如:
enum Weekday{sun,mon,tue,wed,thu,fri,sat};
这里默认sun的值为0,mon的值为1,…sat的值为6。
如果有赋值语句:
workday=mon;
相当于
workday=1;
也可以人为地指定枚举元素的数值,在定义枚举类型时显式地指定,例如:
enum Weekday{sun=7,mon=1;tue,wed,thu,fri,sat} workday,week_end;
指定枚举常量sun的值为7,mon为1,以后顺序加1,sat为6。
注:
由于枚举型变量的值是整数,因此C99把枚举类型也作为整型数据中的一种,即用户自行定义的整型类型。
(3) 枚举类型可以用来作判断比较。例如:
if(workday==mon)...
if(workday>sun)...
25. 用typedef声明新类型名
(1) 简单地用一个新的类型名代替原有的类型名
typedef int Integer;//指定用Integer为类型名,作用与int相同。
typedef float Real;//指定Real为类型名,作用与float相同。
下面两行等价:
int i,j; float a,b;
Integer i,j; Real a,b;
(2) 命名一个简单的类型名代替一个复杂的类型表示方法。
a. 命名一个新的类型名代表结构体类型
typedef struct
{
int month;
int day;
int year;
} Date;
以上声明了一个新类型名Date,代表上面的一个结构体类型。
Date birthday;//定义结构体变量birthday,不要写成struct Date birthday;
Date *p;//定义结构体指针变量p,指向此结构体类型数据。
b. 命名一个新的类型名代表数组类型
typedef int Num[100];//声明Num为整型数组类型名。
Num a;//定义a为整型数组名,其包括100个元素。相当于int a[100];
c. 命名一个新的类型名代表指针类型
typedef char * String;//声明String为字符指针类型名
String p,s[10];//定义p为字符指针变量,s为字符指针数组。
d. 命名一个新的类型名代表指向函数的指针类型
typedef int (*Pointer)();//声明Pointer为指向函数的指针类型名,该函数返回int类型的数值。
Pointer p1,p2;//定义p1,p2为指向函数的指针变量。
注:
声明一个新的类型名的方法就是:
按定义变量的方式,把变量名换成新的类型名,并且在最前面加上一个typedef,就声明了新类型名代表原来的类型。
(3) typedef 与 #define表面上有相似之处,但是两者是不同的
typedef int Count;
和
#define Count int
从表面上看,它们都是用Count代表int。但事实上,它们二者是有区别的。
a. #define 是在预编译时处理的,它只能作简单的字符串替换。
#define Count int
Count a;
这里只是简单地用Count这个字符来代替int而已。
b. typedef是在编译阶段处理的,它并不是作简单的字符串替换。
typedef int Num[10];
Num a;
并不是用“Num[10]”去代替“int”,而是如同定义变量的方法那样先生成一个类型名,然后再去定义变量。
(4) 当不同源文件中用到同一类型数据(尤其是像数组、指针、结构体、共用体等类型数据)时,常用typedef声明一些数据类型。可以把所有的typedef名称声明单独放到一个头文件中,然后在需要用到它们的文件中用**#include 指令把它们包含在文件中。这样就不需要在各文件中自己定义typedef名称**了。
(5) 使用typedef名称有利于程序的通用和移植。
例如有的计算机系统int类型数据占用两个字节(假设为A系统),有的系统则占用4个字节(假设为B系统)。如果把一个C程序从B系统移植到A系统中,按照一般方法则是要将定义变量中的每个int改为long,如果程序中有多处用int定义变量,则需要修改多处。(4个字节表示一个整数,其数值范围为-21亿到+21亿,要想它在2个字节表示一个整数的系统中存储下来,则必须提前将int类型改为long型(在2个字节表示一个整数的系统long型占4个字节,能够容纳下-21亿到+21亿的数))。(注意理解)
如果使用Integer来替代int,
typedef int Integer;
程序中所有整型变量都用Integer定义。在移植时只须修改typedef定义体一处即可:
typedef long Integer;
26. 文件(难点、抽象)
(1) 两种类型的文件
- 程序文件
- 源程序文件(.c)
- 目标文件(.obj)
- 可执行文件(.exe)
- 数据文件
- 按数据的组织类型分为
- ASCII文件(文本文件):每个字节放一个字符的ASCII代码
- 二进制文件(映像文件):存储在内存的数据的映像
(2) 数据在磁盘上如何存储
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以用二进制形式存储。
(3) 文件指针
一种C编译环境提供的stdio.h头文件中有以下的文件类型声明:
typedef struct
{
short level;//缓冲区“满”或“空”的程度
unsigned flags;//文件状态标志
char fd;//文件描述符
unsigned char hold;//如缓冲区无内容不读取字符
short bsize;//缓冲区的大小
unsigned char * buffer;//数据缓冲区的位置
unsigned char * curp;//指针当前的指向
unsigned istemp;//临时文件指示器
short token;//用于有效性检查
} FILE;
(1)不同的C编译系统的FILE类型包含的内容不完全相同,但大同小异。
(2)在程序中可以直接用FILE类型定义变量。
(3)每一个FILE类型变量对应一个文件的信息区,在其中存放该文件的有关信息。
(4)一般不对FILE类型变量命名,也就是不通过变量的名字来引用这些变量,而是设置一个指向FILE类型变量的指针变量,然后通过它来引用这些FILE类型变量。
FILE * fp;//定义一个指向文件类型数据的指针变量;
(5)每一个指针变量指向一个FILE类型变量,每一个FILE类型变量对应一个文件的信息区。指向文件的指针变量是指向内存中的文件信息区的开头。
(4) 打开和关闭文件
(1)打开文件就是指为文件建立相应的信息区(用来存放有关文件的信息)和文件缓冲区(用来暂时存放输入输出的数据)。
fopen(文件名,使用文件方式);//fopen函数的返回值是一个指向文件类型的指针变量。
例如:
fopen("f1","r");
表示要打开名字为“f1”的文件,使用文件方式为“读入”,fopen函数的返回值是指向文件f1的指针(即f1文件信息区的起始地址)。
使用文件方式
文件使用方式 | 含义 | 如果指定的文件不存在 |
---|---|---|
r(只读) | 为了输入数据,打开一个已存在的文本文件 | 出错 |
w(只写) | 为了输出数据,打开一个文本文件 | 建立新文件 |
a(追加) | 向文本末尾添加数据 | 出错 |
rb(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
wb(只写) | 为了输出数据,打开一个二进制文件 | 建立新文件 |
ab(追加) | 在二进制文件末尾添加数据 | 出错 |
r+(读写) | 为了读和写,打开一个文本文件 | 出错 |
w+(读写) | 为了读和写,建立一个新的文本文件 | 建立新文件 |
a+(读写) | 为了读和写,打开一个文本文件 | 出错 |
rb+(读写) | 为了读和写,打开一个二进制文件 | 出错 |
wb+(读写) | 为了读和写,建立一个新的二进制文件 | 建立新文件 |
ab+(读写) | 为了读和写,打开一个二进制文件 | 出错 |
注:
“w”(只写):如果原来不存在该文件,则在打开文件前建立一个以指定的名字命名的文件。如果原来存在该文件,则先删除,然后重新建立一个新文件。
常用下面的方法打开一个文件:
if((fp=fopen("file1","r"))==NULL)
{
printf("cannot open this file\n");
exit(0);//关闭所有文件,终止正在执行的程序。
}
(2)关闭文件就是指撤销文件信息区和文件缓冲区,使文件指针变量不再指向该文件。
fclose(文件指针);
fclose函数的返回值为:
- 当成功执行了关闭操作,则返回值为0;
- 反之,返回值为EOF(-1);
例如:
fclose(fp);//fp为指向文件的指针变量;
(5) 向文件读写字符
读写一个字符的函数
函数名 | 调用形式 | 功能 | 返回值 |
---|---|---|---|
fgetc | fgetc(fp) | 从fp指向的文件读入一个字符 | 读成功,则返回所读的字符;失败则返回文件结束标志EOF(即-1) |
fputc | fputc(ch,fp) | 把字符ch写到fp指向的文件中 | 输入成功,则返回值是输出的字符;输出失败,则是返回EOF(即-1) |
注:
C系统已把fputc和fgetc函数定义为宏名putc和getc:
#define putc(ch,fp) fputc(ch,fp)
#define getc(ch,fp) fgetc(ch,fp)
这是在stdio.h文件中定义的。因此,在程序中用putc和fputc作用是一样的,用getc和fgetc的作用也是一样的。
(6) 向文件读写一个字符串
读写一个字符串的函数
函数名 | 调用形式 | 功能 | 返回值 |
---|---|---|---|
fgets | fgets(str,n,fp) | 从fp指向的文件中读入一个长度为(n-1)的字符串(不包括字符串结束字符‘\0’) | 读成功,返回地址str;失败则返回NULL |
fputs | fputs(str,fp) | 将str指向的字符串输出到fp指向的文件中 | 输出成功,返回0;否则返回非0值 |
注:
如果在读完n-1个字符之前遇到换行符‘\n’或文件结束符EOF,读入即结束,但将所遇到的换行符‘\n’也作为一个字符读入。
(7) 用格式化的方式读写文件
fprintf(文件指针,格式字符串,输出表列);//格式化写函数(写入文件)
fscanf(文件指针,格式字符串,输入列表);//格式化读函数(读文件)
注:
- fprintf: 输出时要将内存中的二进制形式转化成字符。
- fscanf: 读入时要将文件中的ASCII码转换为二进制形式,再保存在内存变量中。
上面两种转换要花费一定的时间,因此,在内存与磁盘频繁交换数据的情况下,最好不用fprintf和fscanf函数,而用下面介绍的fread和fwrite函数进行二进制的读写。
(8) 用二进制方式对文件进行读写
fread(buffer,size,count,fp);//执行成功时返回形参count的值(int),即读入数据项的个数;
- buffer:表示从文件中读入的数据要存入到内存中的存储块地址;
- size:要读写的字节数(每个数据项的大小);
- count:要读写的数据项数;(每个数据项的长度为size)
- fp: FILE类型指针;
fwrite(buffer,size,count,fp);//执行成功时返回形参count的值(int),即输出数据项的个数;
- buffer:要输出到文件中的那段数据在内存中的存储区的地址;