C语言指针

善于利用指针

    • 一、指针是什么
    • 二、指针变量
      • 指针变量作为函数参数
    • 三、通过指针引用数组
      • 数组元素的指针
      • 在引用数组元素时的指针运算
      • 利用指针引用数组元素
      • 用数组名作函数参数
      • 以变量名和数组名作为函数参数的比较
      • 通过指针引用多维数组
        • 多维数组的地址
        • 指向多维数组元素的指针变量
    • 四、通过指针引导字符串
        • 字符串的引用方式
    • 五、指向函数的指针
      • 什么是函数的指针
      • 用函数指针调用函数
      • 用指向函数的指针作为函数的参数
    • 六、返回函数指针的函数(指针函数)
    • 七、指针数组和多重指针
    • 八、动态内存分配与指向它的指针变量
      • 建立内存的动态分配
      • void指针

一、指针是什么

内存区的每一个字节都有一个编号,这就是“地址”。通过地址能找到所需的变量单元,可以说地址指向该变量单元。形象化的将地址称为“指针

数据是分类型的,对不同类型的数据,在内存中分配的存储单元的大小(字节数)和存储方式是不同的(如整数是以二进制补码的形式存放,实数则以指数形式存放)

C语言中的地址包括位置信息(内存编号、或称纯地址)和它所指向的数据的类型信息,或者说是“带类型的地址”

指针和指针变量是两个概念:指针是一个地址,而指针变量是存放地址的变量。

二、指针变量

所有的指针类型存储的都是内存地址,内存地址都是一个无符号十六进制整形数。(32位操作系统占4字节,64位操作系统占8字节)

定义指针变量:*类型名 指针变量名;左边的类型名是“基类型”,用来指定此指针变量可以指向的变量的类型。

指针变量作为函数参数

实例:按从大到小的顺序输出三个整数

#include
int main()
{
	void sort(int *a,int *b,int *c);
	int a,b,c;
	printf("please enter three numbers:");
	scanf("%d%d%d",&a,&b,&c);
	sort(&a,&b,&c);
	printf("The sorted numbers are:%d,%d,%d",a,b,c);
	return 0;
}
void sort(int *a,int *b,int *c)	//形参是指针变量,定义sort函数,对三个整数进行排序
{
	void swap(int *x,int *y);
	if(*a<*b)
		swap(a,b);
	if(*a<*c)
		swap(a,c);
	if(*b<*c)
		swap(b,c);
}
void swap(int *x,int *y)	//形参是指针变量,定义swap函数交换*x和*y的值
{
	int temp;		//整型变量temp作为临时辅助变量实现*x和*y的值交换
	temp=*x;
	*x=*y;
	*y=temp;
}	
/*
void swap(int *x,int *y)	
{
	int *temp;		//*temp是指针变量temp所指向的值,未给temp赋值(野指针),*temp指向的值不可预见
	*temp=*x;		//对*temp赋值是向一个未知的储存单元赋值,这个储存单元里面可能存着一个有用的数据,可能会破坏系统的正常工作状态
	*x=*y;
	*y=*temp;
}
void swap(int *x,int *y)	//不能企图改变指针形参的值而改变指针实参的值
{
	int *temp;
	temp=a;
	a=b;
	b=temp
}
*/

三、通过指针引用数组

数组元素的指针

数组元素的指针就是数组元素的地址,引用数组元素可以用下标法(如a[3]),也可以用指针法(通过数组元素的指针找到所需的元素)

数组名不能代表整个数组,只代表数组中首元素的地址,是一个指针型常量。

在引用数组元素时的指针运算

在指针已指向一个数组元素时可以进行以下运算

  • 加一个整数(用+或+=),如p+1;
  • 减一个整数(用-或-=),如p-1;
  • 自加运算,如p++,++p
  • 自减运算,如p–,–p
  • 两个指针相减,如p1-p2(只有p1和p2都指向同一数组中的元素时才有意义)
  1. 数组元素是float型,每个元素占四字节。p+1意味着使p的值(是地址)加四个字节,以使他指向下一个元素。
  2. p2-p1的结果是:p2-p1的值(两个地址之差)初以数组元素的长度

利用指针引用数组元素

设p开始时指向数组a的首元素(即p=a)

  1. p++;
    *p;
    

    p++使p指向下一元素a[1],然后在执行*p,则得到下一个元素a[1]的值

  2. *p++
    

    由于*和++同级优先,结合方向为自右向左,因此等价于*(p++)

    for(i=0;i<10;i++,p++)
        printf("%d",*p)
    等价于:
    for(i=0;i<10;i++)
        printf("%d",*p++)
    

    作用都是先输出*p的值,再使p加1

  3. *(p++)和*(++p)作用是不一样的,*(p++)是先取p的值,再使p加1。*(++p)是先使p加1,再取*p

  4. *(arr+i)和arr[i]是无条件等价的

用数组名作函数参数

函数的形参会退化为指针,失去精度(不知道数组元素个数了)

fun(int arr[],int n)
{
    ……
}
等价于
fun(int *arr,int n)
{
    ……
}

以变量名和数组名作为函数参数的比较

实参类型 变量名 数组名
要求形参的类型 变量名 数组名或指针变量
传递的信息 变量的值 实参数组首元素的地址
通过函数调用是否能改变实参的值 不能改变实参变量的值 能改变实参数组的值

例:将数组arr中的n个整数按相反顺序存放

#include
int main()
{
	void exchange(int arr[],int n);
	int arr[]={10,9,8,7,6,5,4,3,2,1};
	int i;
	printf("The original array:\n");
	for(i=0;i<10;i++)
		printf("%d ",arr[i]);
	exchange(arr,10);		//数组名是数组首元素地址,注意arr不是指针变量,是一个指针类型常量
	printf("\nThe array have been exchanged:\n");
	for(i=0;i<10;i++)
		printf("%d ",arr[i]);
	return 0;
}

void exchange(int arr[],int n)	//exchange函数中的n用来接收需要处理的元素的个数,形参用数组名表示
    							//函数原型还可以写成void exchange(int *arr,int n),形参用指针变量表示
{
	int i,j,temp;
	for(i=0,j=9;i<10;i++,j--)
	{temp=0;
		if(i<j)
		{
			temp=arr[i];
			arr[i]=arr[j];
			arr[j]=temp;
		}else
			break;
	}
}

如果有一个实参数组,要想在函数中改变此数组中的元素的值,实参与形参有以下四种对应关系。(实际上都是地址的传递)

  1. 实参和形参都用数组名
  2. 实参用数组名,形参用指针变量
  3. 实参形参都用指针变量
  4. 实参为指针变量,形参为数组名

例:用指针的方法对十个整数按由大到小的顺序排序

#include
#define N 10
int main()
{
	void sort(int *x,int n);
	int arr[N]={6,3,9,2,5,7,4,8,1,10};
	int i,*p;
    p=arr;		//指针变量指向a[0]
	printf("\n The original numbers are:\n");
	for(i=0;i<10;i++)
	printf("%d ",arr[i]);
	sort(p,N);
	printf("\n The sorted numbers are:\n");
	for(i=0;i<10;i++)
	printf("%d ",arr[i]);

	return 0;
}
void sort(int x[],int n)	//定义sort函数,x是形参数组名
{
	int i,j,temp;
	for(i=0;i<N-1;i++)
	{temp=0;
		for(j=0;j<N-i-1;j++)
		{
			if(x[j]<x[j+1])
			{
				temp=x[j];
				x[j]=x[j+1];
				x[j+1]=temp;
			}
		}
	}
}

通过指针引用多维数组

多维数组的地址
int a[3][4]={{1,3,5,7},{9,11,13,17},{19,23,27,31}};

a是二维数组名。a包含3行,即3个行元素:a[0],a[1],a[2]。每个行元素又是一个一维数组,它包含4个元素(即4个列元素),可以认为二维数组是“数组的数组”。

从二维数组角度看a代表的是二维数组首元素的地址,a+1代表序号为1的行的起始地址(即a[1]),a+2代表a[2]的起始地址。假设a=2000,则a+1=a[1]=2016,a+2=a[2]=2032

a[0],a[1],a[2]是一维数组名,代表首元素地址

a[0]+0,a[0]+1,a[0]+2,a[0]=+3 分别是a[0][0],a[0][1],a[0][2],a[0][3]元素的地址(即&a[0][0],&a[0][1],&a[0][2],&a[0][3])

a[0]和*(a+0)等价,a[1]和*(a+1)等价,a[i]和*(a+i)等价。因此a[0]+1和*(a+0)+1等价都是&a[0][1]

(a[0]+1)是a[0][1]的值,同理*(*(a+0)+1)或*(*a+1)也是a[0][1]的值,即**(a[i]+j)或*(*(a+i)+J)是a[i][j]的值**

**C语言的地址信息中既包含位置信息(内存编号),还包含他所指向的数据的类型信息。**a[0]是一维数组名,它是一维数组中起始元素的地址;a是二维数组名,它是二维数组首行起始地址,二者的纯地址是相同的,但它们的基类型是不同的,即它们所指向的数据的类型不同,前者是整型数据,后者是一维数组。如果用一个指针变量pt来指向此一维数组,应当这样定义:int (*pt)[4];

在二维数组中,a+i,a[i],*(a+i),&a[i],&a[i][0]的值相同,都表示同一地址,但基类型不同。

例:输出二维数组有关数据(地址和元素的值)

#include
int main()
{
	int a[3][4]={{1,3,5,7},{9,11,13,17},{19,23,27,31}};
	printf("%d,%d\n",a,*a);					//0行起始地址和0行0列元素地址
	printf("%d,%d\n",a[0],*(a+0));			//0行0列元素地址
	printf("%d,%d\n",&a[0],&a[0][0]);		//0行起始地址和0行0列元素地址
	printf("%d,%d\n",a[1],a+1);				//1行0列元素地址和1行起始地址
	printf("%d,%d\n",&a[1][0],*(a+1)+0);	//1行0列元素地址
	printf("%d,%d\n",a[2],*(a+2));			//2行0列元素地址
	printf("%d,%d\n",&a[2],a+2);			//2行起始地址
	printf("%d,%d\n",a[1][0],*(*(a+1)+0));	//1行0列元素的值
	printf("%d,%d\n",*a[2],*(*(a+2)+0));	//2行0列元素的值
	return 0;
}
            /*结果是:
            6422000,6422000
            6422000,6422000
            6422000,6422000
            6422016,6422016
            6422016,6422016
            6422032,6422032
            6422032,6422032
            9,9
            19,19
            */
指向多维数组元素的指针变量
  1. 指向数组元素的指针变量

    int *p;

  2. 指向由m个元素组成的一维数组的指针变量

    int (*P)[m];

#include
int main()
{
    int a[4]={1,3,5,7};		//定义一维数组a,里面包含4个元素
    int (*p)[4];			//定义指向包含4个元素的一维数组的指针变量
    p=&a;			//使p指向一维数组,注意不要写成p=a;这样写p的值是&a[0],指向首元素a[0]
    printf("%d\n",(*p)[3]);//输出a[3],输出整数7
    return 0;
}
  1. 用指向数组的指针作函数参数

用指针变量作为形参,以接受实参数组名传递来的地址。有两种方法:1、用指向变量的指针变量。2、用指向一维数组的指针变量。

#include
int main()
{
	void average(float *arr,int n);
	void search(float (*p)[4],int n);
	float score[3][4]={{65,67,70,60},{80,87,90,81},{90,99,100,98}};
	average(*score,12);		//*score即score[0],也就是&score[0][0]
	search(score,2);
	return 0;
}
void average(float *arr,int n)	//形参是指向整型变量的指针变量
{
	float *p_end;
	float sum=0,aver;
	p_end=arr+n-1;
	for(;arr<=p_end;arr++)
		sum=sum+(*arr);
	aver=sum/n;
	printf("average=%5.2f\n",aver);
}
void search(float (*p)[4],int n)	//形参p的基类型是float (*)[4]
{
	int i;
	printf("The score of No.%d are:\n",n);
	for(i=0;i<4;i++)
	printf("%5.2f ",*(*(p+n)+i));	//*(*(p+n)+i)是search[n][i]的值
	printf("\n");
}

四、通过指针引导字符串

C语言只有字符变量,没有字符串变量。字符串是存放在字符数组中的。还要注意的是字符串是以’\0’结束

字符串的引用方式

1、字符数组存放一个字符串,可以通过数组名和下标引用字符串中的一个字符,也可以通过数组名和格式声明“%s”输出该字符串。

字符指针做函数参数:实参和形参可以是指针变量也可以是字符数组名

例:函数调用实现字符串的复制

#include
int main()
{
	void copy_string(char x[],char b[]);
	char a[]="I am chinese!";
	char b[]="I love china!";
	printf("string a:%s\nstring b:%s\n",a,b);
	copy_string(b,a);
	printf("\ncopy staing b to string a:\n");
	printf("string a:%s\nstring b:%s\n",a,b);
	return 0;
}
void copy_string(char from[],char to[])	//等效于void copy_string(char *from,char *to)
{
	int i=0;
	while (from[i]!='\0')
	{
		to[i]=from[i];
		i++;
	}
	to[i]='\0';
	/*函数体可以换为
	while(*to++=*from++);*/
	/*函数体可以换为
	while(*from!='\0';
	*to++=*from++;
	*to='\0'*/
}

使用字符数组和字符指针都能实现字符串的存储和运算,但是二者之间是有差别的。

  1. 字符数组是有若干元素组成的,每个元素中存放一个字符,而字符指针变量中存放的是地址(字符串第一个字符的地址),绝不是将字符串放到字符指针变量中。
  2. 赋值方式:可以对字符指针变量赋值,但不能对数组名赋值。
  3. 编译时为字符数组分配若干存储单元,对指针变量只分配一个存储单元。
  4. 指针变量的值是可以改变的,而字符数组名代表的是一个固定的值(数组首元素的地址),不能改变。
  5. 字符数组中各元素的值是可以改变的,但字符变量指向的字符串常量中的内容是不可以被取代的。
  6. 如果定义了指针变量p是指向数组a的首元素,可以用*(p+5)引用a[5]的值。
  7. 用指针变量指向一个格式字符串,可以用它替代printf函数中的格式字符串。

五、指向函数的指针

什么是函数的指针

在程序中定义一个函数,在编译时会把函数的源代码转为可执行代码并分配一段存储空间。这段内存空间有一个起始地址,也被称为入口地址,调用函数时都从该地址入口开始执行此段函数代码。函数名代表函数的起始地址(类似于数组名代表数组的起始地址)。

用函数指针调用函数

#include
int main()
{
    max(int a,int b);
    int (*p)(int,int);	//指针变量p的类型是int (*p)(int,int),每个函数都有自己的类型,这点注意一下
    p=max;//将函数max的入口地址赋给指针变量p
    int a=3,b=6,c;
    c=(*p)(a,b);//通过指针变量调用max函数
    printf("max=%d\n",c);
}
int max(int x,int y)
{
    return (x?y:x>y);
}

在许多应用程序中常用菜单提示输入一个数字,然后根据输入的值调用不同的函数实现不同的功能,就可以使用指向函数的指针变量来实现。当然通过if或switch语句进行判断,调用不同的函数也能实现。

例:输入两个整数,然后让用户输入1和2。1代表输出两个数中较大的,2代表输出两个数中较小的

#include
int main()
{
	int max(int x,int y);
	int min(int x,int y);
	int a,b,c,n;
	int (*p)(int,int);				//定义指向函数的指针变量p
	printf("请输入两个整数:\n");
	scanf("%d%d",&a,&b);
	printf("输入1求最大值,输入2求最小值:\n");
	scanf("%d",&n);
	if(n!=1&&n!=2)
		printf("输入有误!!");
	else if(n==1)
		p=max;
	else if(n==2) p=min;
		c=(*p)(a,b);				//调用p指向的函数
	if(n==1)
		printf("max=%d\n",c);
	else printf("min=%d\n",c);
	return 0;
}
int max(int x,int y)
{
	return(x?y:x>y);
}
int min(int x,int y)
{
	return(x?y:x<y);
}

用指向函数的指针作为函数的参数

指向函数的指针变量的一个重要用途是把函数的入口地址作为函数参数传递到其他函数,这样就可以在其他函数中调用本函数。当然,通过函数原型也可以实现。

例:输入两个整数,然后让用户输入1、2或3。1代表输出两个数中较大的,2代表输出两个数中较小的,,3代表输出两数之和

#include
int main()
{
	int fun(int x,int y,int(*p)(int,int));	//fun函数声明
	int max(int x,int y);
	int min(int x,int y);
	int add(int x,int y);
	int a,b,n;
	int (*p)(int,int);	//定义指向函数的指针变量p
	printf("请输入两个整数:\n");
	scanf("%d%d",&a,&b);
	printf("输入1求最大值,输入2求最小值,输入3求两者之和:\n");
	scanf("%d",&n);
	if(n!=1&&n!=2&&n!=3)
		printf("输入有误!!");
	else if(n==1)
		fun(a,b,max);	//fun函数调用
	else if(n==2)
		fun(a,b,min);
	else if(n==3)
		fun(a,b,add);
	return 0;
}
int fun(int x,int y,int(*p)(int,int))	//定义指向函数的指针变量p做fun函数的形参,可以指向的函数有max函数、min函数、add函数
{
	int result;
	result=(*p)(x,y);
	printf("result is:%d\n",result);
}
int max(int x,int y)
{
	return(x?y:x>y);
}
int min(int x,int y)
{
	return(x?y:x<y);
}
int add(int x,int y)
{
	return (x+y);
}

六、返回函数指针的函数(指针函数)

定义返回指针的函数(指针函数)的原型一般形式:

*类型名 函数名(参数列表);

例:3个学生,4门成绩,要求输入学生序号以后能输出该学生的全部成绩。

#include
int main()
{
	float *search(float (*pointer)[4],int n);
	float score[][4]={{60,70,80,90},{81,82,83,84},{91,92,93,94}};
	float *p;
	int i,k;
	printf("enter the number of student:\n");
	scanf("%d",&k);
	printf("The score of No.%d are:\n",k);
	p=search(score,k-1);
	for(i=0;i<4;i++)
		printf("%5.2f\t",*(p+i));
	printf("\n");

	return 0;
}
float *search(float (*pointer)[4],int n)
{
	float *pt;
	pt=*(pointer+n);
	return(pt);
}

七、指针数组和多重指针

指针数组是一个特殊的二维数组模型,定义一维指针数组的一般形式是:

*类型名 数组名[数组长度]

指针数组有一个很重要的用途是作为main函数的参数

int main()或者main(void),表名main函数没有参数,调用main函数时不用给出实参。在某些情况下main函数可以有参数,main函数是操作系统调用的,实参只能由操作系统给出。

int main(int argc,char *argv[]);

如果用带参数的main函数,其第一个形参必须是int型,用来接收形参的个数,第二个参数必须是字符指针数组,用来接收从操作系统命令行传来的字符串中首字符的地址。

命令行的一般形式是:

命令名 参数1 参数2 …参数n

命令行各参数之间用空格号隔开。命令名是可执行的文件名(此文件包含main函数),假设可执行文件的名称是file1.exe,把“program ”和“language”两个字符串传递给main函数的参数,命令行可以写成:

file1 program language

main中形参argc是指命令行中参数的个数(文件名file1也作为一个参数,现在argc的值等于3),main函数的第二个形参argv是一个指向字符串的指针数组(argv[0]指向字符串"file1"的首字符)

八、动态内存分配与指向它的指针变量

全局变量是分配在内存中的静态存储区的,非静态的局部变量(包括形参)是分配在内存中的动态存储区的,

理解堆(heap)和栈(stack)的概念

建立内存的动态分配

堆内存的动态分配是通过系统提供的库函数stdlib.h来实现的,主要有malloc,calloc,free,realloc四个函数。

  1. 用malloc函数开辟动态存储区

    函数原型是:*void malloc(unsigned int size);

    在内存的动态存储区中分配一个长度为size的连续空间。此函数的返回值是所分配区域的第一个字节的地址。此函数若未能成功执行(例内存空间不够)则返回空指针(NULL)

  2. 用calloc函数开辟动态存储区

    函数原型是:*void calloc(unsigned n,unsigned size);

    其作用是在内存的动态存储区分配n个长度为size的连续空间,这个空间一般比较大可以保存一个数组。函数返回指向所分配区域的第一个字节的指针;如果分配不成功返回空指针(NULL)。

    p=calloc(50,4);//开辟50×4个字节的临时分配域,把首地址赋给指针变量p

  3. 用realloc函数重新分配动态存储区

    函数原型:**void realloc(void p,unsiged int size);

    realloc (p,50);//将p所指向已分配的动态空间改为50字节

    已经通过malloc函数或者calloc函数获得动态空间,想改变其大小,可以用realloc函数重新分配。用realloc函数将p所指向的的动态空间的大小改变为size。p的值不变。如果重分配不成功,返回NULL

  4. 用free函数释放动态存储区

    函数原型:void free(void *ptr);

    free函数无返回值。作用是释放指针p所指向的动态空间,使这部分空间能重新被其他变量使用。p应该是最近一次使用calloc函数或者malloc函数所得到的函数返回值。

    释放p所指向的已分配的动态空间,free掉一段空间之后还要将ptr置为NULL,指针free之后,free函数只是把指针指向的内存空间释放了,即内存中存储的值,但是并没有将指针的值赋为NULL,指针仍然指向这块内存。而程序判断一个指针是否合法,通常都是使用if语句测试该指针是否为NULL来判断,导致指针成为所谓的“野指针”

void指针

基类型为void *类型的指针不能指向任何类型的数据。应该理解为“指向空类型”或“不指向确定的类型”的数据。

void *指针可以指向任意变量的内存空间:

	void *p = NULL;

	int a = 10;
	p = (void *)&a; //指向变量时,最好转换为void *

	//使用指针变量指向的内存时,转换为int *
	*( (int *)p ) = 11;
	printf("a = %d\n", a);

万能指针可以接受任意类型变量的数据,再通过万能指针修改变量的值时需要找到变量对应的指针类型(强制类型转换)。

你可能感兴趣的:(开发语言,c语言)