C语言笔记

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),*a0行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) 向文件读写字符

读写一个字符的函数

函数名调用形式功能返回值
fgetcfgetc(fp)从fp指向的文件读入一个字符读成功,则返回所读的字符;失败则返回文件结束标志EOF(即-1)
fputcfputc(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) 向文件读写一个字符串

读写一个字符串的函数

函数名调用形式功能返回值
fgetsfgets(str,n,fp)从fp指向的文件中读入一个长度为(n-1)的字符串(不包括字符串结束字符‘\0’)读成功,返回地址str;失败则返回NULL
fputsfputs(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:要输出到文件中的那段数据在内存中的存储区的地址;
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值