C语言程序设计(Part Ⅶ)(指针 下)

C语言程序设计(Part Ⅶ 下)的整理笔记,若有错误,欢迎指正。
C语言程序设计(Part Ⅶ)(指针 上)

  • 用数组名作函数参数

在C语言程序设计(Part Ⅳ)中介绍过可以用数组名作函数的参数,当用数组名作参数时,如果形参数组中各元素的值发生变化,实参数组元素的值随之变化。实参数组名代表该数组首元素的地址,而形参是用来接收从实参传递过来的数组首地址的。因此,形参应该是一个指针变量(只有指针变量才能存放地址)。实际上,C编译都是将形参数组名作为指针变量来处理的。

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

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

!实参数组名代表一个固定的地址,或者说是指针常量,但形参数组并不是一个固定的地址值,而是作为指针变量,在函数调用开始后,它的值等于实参数组首元素的地址,在函数执行期间,它可以再被赋值。

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

#include <stdio.h>
int main()
{
	void inv(int x[], int n); //inv函数的声明
	int * p, i, a[10] = { 3,7,9,11,0,6,7,5,4,2 };
	printf("The original array: \n");
	for (i = 0; i < 10; i++)
		printf("%d ", a[i]);
	printf("\n");
	inv(a, 10);
	printf("The array has been inverted: \n");
	for (i = 0; i < 10; i++)
		printf("%d ", a[i]);
	printf("\n");
	return 0;
}
void inv(int x[], int n) //形参x是数组名
{
	int temp, i, j, m = (n - 1) / 2;
	for (i = 0; i < m; i++)
	{
	j = n - 1-i;
	temp = x[i]; x[i] = x[j]; x[j] = temp; 
	}
	return;
}

//输出结果:
The original array:
3 7 9 11 0 6 7 5 4 2
The array has been inverted:
2 4 5 7 6 0 11 9 7 3

程序分析:

在主函数中定义整型数组a,并赋予初值。函数inv的形参数组名为x,在inv函数中可以不指定数组元素的个数。因为形参数组名实际上是一个指针变量,并不是真正地开辟一个数组空间(定义实参数组时必须指定数组大小,因为要开辟相应的存储空间)。函数形参n用来接收实际上需要处理的元素的个数。

例:(正确✔) 以上程序主函数基本不改动(只改变对inv函数的声明),将inv函数改为:

void inv(int * x, int n)
{
	int * p, temp, * i, * j, m = (n - 1) / 2;
	i = x; j = x + n - 1; p = x + m;
	for (; i <= p; i++, j--)
	{
		temp = * i; * i = * j; * j = temp;
	}
	return;
}

//运行情况与前一程序相同。

!如果有一个实参数组,要想在函数中改变此数组中的元素的值,实参与形参的对应关系有以下4种情况:

  • 形参和实参都用数组名。
  • 实参用数组名,形参用指针变量。
  • 实参形参都用指针变量。先使实参指针变量p指向数组a,然后将p作实参,将&a[0]传给形参指针变量x。x的初始值也是&a[0]。通过x值的改变可以使x指向数组a
    的任一元素。
  • 实参为指针变量,形参为数组名。应该注意,必须先使实参指针变量有确定值,即指向数组一个元素。

例:选择法对整数排序

#include<stdio.h>
int main()
{
	void ascend(int * p, int n);
	void descend(int* p, int n);
	int a[100] ; //任意个数冒泡排列(个数在100以内)
	int  i,n,flag;

	printf("input a number :\n"); //输入需要进行冒泡排序的个数
	scanf("%d", &n);
	printf("\n");

	printf("input %d numbers :\n", n); //输入需要进行冒泡排序的具体数值
	for (i = 0; i < n; i++)
		scanf("%d", &a[i]);
	printf("\n");

	printf("choose 1(ascend) or 0(descend) :\n");
	//选择升序还是降序,1代表升序,0代表降序
	scanf("%d", &flag);
	printf("\n");

	if (flag == 1) //如果输入是1,则升序输出
		ascend(a, n);
	else //如果输入是0,则降序输出
		descend(a, n);

	printf("the sorted numbers :\n"); //输出排序后的结果
	for (i = 0; i < n; i++)
		printf("%d ", a[i]);
	printf("\n");
	return 0;
}

void ascend(int * p, int n) //升序
{

	int i, j, t,m;
	for (i = 0; i < n - 1; i++)
		for (j = i + 1; j < n; j++)
		{	
			m = i;
			if (*(p + m) > *(p + j))
			{
				m = j;
				t = *(p + i); *(p + i) = *(p + j); *(p + j) = t;
			}
		}
}

void descend(int* p, int n) //降序
{

	int i, j, t, m;
	for (i = 0; i < n - 1; i++)
		for (j = i + 1; j < n; j++)
		{
			m = i;
			if (*(p + m) < *(p + j))
			{
				m = j;
				t = *(p + i); *(p + i) = *(p + j); *(p + j) = t;
			}
		}
}

  • 通过指针引用字符串
    在C程序中,有两种方法访问一个字符串:
  1. 用字符数组存放一个字符串,然后用字符数组名和下标访问字符数组中的元素,也可以通过字符数组名用%s格式符输出一个字符串。
  2. 用字符指针指向一个字符串,可以不定义字符数组,而定义一个字符指针,用字符指针指向字符串中的字符。
    例:定义字符指针,使它指向一个字符串
//方法①:字符数组名和下标形式逐个输出字符元素(%c格式)
#include <stdio.h>
#include<string.h>
int main()
{
	char string[]="I love China!";
	for(int i=0;i<=strlen(string);i++)
		printf("%c", string[i]);
	return 0;
}

//方法②:直接输出一个字符串(%s格式)
#include <stdio.h>
int main()
{
	char string[]="I love China!";
	printf("%s\n", string);
		return 0;
}

//方法③:使用字符指针输出一个字符串
#include <stdio.h>
int main()
{
	char * string = "I love China!";
	printf("%s\n", string);
		return 0;
}

//输出结果:I love China!

程序分析:

  • 在程序③中没有定义字符数组,只定义了一个字符指针变量string,用字符串常量"I love China!"对它初始化。
  • C语言对字符串常量是按字符数组处理的,在内存中开辟了一个字符数组用来存放该字符串常量,但是这个数组是没有名字的,不能通过数组名来引用,只能通过指针变量来引用。
  • !注意:可以用指针指向字符串常量,但是不能通过指针变量对该字符串常量重新赋值,因为字符串常量是不能改变的。
  • 对字符指针变量string初始化,实际上是把字符串第1个元素的地址(即存放字符串的字符数组的首元素地址)赋给string。

其中:

char * string = "I love China!";
//等价于下面两行:
char * string;
string="I love China!";
  • 可以看到定义了一个指针变量string,其基类型为字符型。
  • !注意它只能指向一个字符变量或其他字符型数据,不能同时指向多个字符数据,更不是把"I love China!"这些字符存放到string中(指针变量只能存放地址),也不是把字符串赋给* string。只是把"I love China!"的第1个字符的地址赋给指针变量string。
  • %s是输出字符串时所用的格式符,在输出项中给出字符指针变量名string,则系统先输出它所指向的一个字符数据,然后自动使string加1,使之指向下一个字符,然后再输出一个字符…如此直到遇到字符串结束标志’\0’为止。
  • !注意,在内存中,字符串的最后被自动加了个’\0’,因此在输出时能确定字符串的终止位置。
    在这里插入图片描述

通过字符数组名或字符指针变量可以输出一个字符串。而对一个数值型数组,是不能企图用数组名输出它的全部元素的。
例:(错误❌)

#include <stdio.h>
int main()
{
	int a[] = {1,2,3,4,5};
	printf("%d ", a);
	return 0;
}
  • C语言中,用printf()输出数据时,只有字符串类型数据才可以通过数组名(数组首地址)将其中的数据完整的输出出来。
  • 因为字符串有一个规定,必须以’\0’结尾,这样,程序只需要判断是不是遇到’\0’而决定是否结束输出。
  • 对于其它类型的数组,因为没有标识可供机器判定输出结束,所以,只提供数组首地址,机器不知道何时输出结束,也就无法完成对数组整体进行输出了。
  • 关于输入数据,对于字符串类型,虽然是输入多个字符,只要是按了回车,系统就认为输入结束,这一堆字符都却只形成一个字符串,相当于只是输入了一个数据。
  • 对于其它类型数组的输入,完整输入则表示要输入多个数据,因为机器无法判定数组的大小,不知道输入到哪里算是数据输入结束,所以,无法根据数组名进行整体输入。

只能逐个元素输出。例:

#include <stdio.h>
int main()
{
	int a[] = {1,2,3,4,5};
	for(int i=0;i<5;i++)
		printf("%d ", a[i]);
	return 0;
}


有一个字符数组a,在其中存放字符串"I’m a girl.",要求把该字符串复制到字符数组b中。
例1:

//方法①:用下标法访问数组元素,按字符逐个输出
#include <stdio.h>
int main()
{
	char a[] = "I'm a girl.", b[20];
	int i;
	printf("string a is:%s \n", a);
	for (i = 0; a[i] != '\0'; i++)
		b[i] = a[i];
	b[i] = '\0';
	printf("string b is:");
	for (i = 0; b[i] != '\0'; i++)
		printf("%c", b[i]); //用下标法访问数组元素
	return 0;
}
//输出结果为:
string a is:I'm a girl.
string b is:I'm a girl.

程序分析:

在for语句中,先检查a[i]是否为’\0’,如果不等于’\0’,表示字符串尚未处理完,就将a[i]的值赋给b[i],如果等于’\0’,则循环结束。在for循环中将数组a中的元素值全部复制给了数组b,最后还应该在数组b的末位添加一个’\0’。

例2:

//方法②:用地址法访问数组元素,按字符串整个输出
#include <stdio.h>
int main()
{
	char a[] = "I'm a girl.", b[20];
	int i;
	printf("string a is:%s \n", a);
	for (i = 0; * (a + i) != '\0'; i++)
		* (b + i) = * (a + i); //用地址法访问数组元素
		* (b + i) = '\0'; //在已复制到b数组中的字符最后加'\0',表示字符串结束
	printf("string b is:%s", b);
	return 0;
}

程序分析:

定义a和b都是字符型数组,使用地址法访问数组元素时,(b+i)、(a+i)地址值都是+1个字节,此时a、b的下标是完全相同的,因此可以将数组a中的元素值正确复制到数组b中。

例3:

//方法③:用指针变量
#include <stdio.h>
int main()
{
	char a[] = "I'm a girl.",b[20];
	char * p1 ,* p2; //定义字符型指针变量
	p1 = a; //p1指向字符串a的第一个字符
	p2 = b; //p2指向字符串b的第一个字符
	printf("string a is:%s \n", a);
	for ( ; * p1 != '\0'; p1++, p2++)
		* p2 = * p1; //将p1指向的a串中的字符复制到p2所指向的b串中的位置
	*p2 = '\0'; //最后在p2所指的位置上赋予一个'\0'字符
	printf("string b is:%s", b);
	return 0;
}

程序分析:

p1++,p2++表示它们分别+1个字节,指向下一个元素,直到* p的值等于’\0’为止,注意:虽然p1和p2的值是在不断改变的,但它们是同步移动的。


  • 字符指针作函数参数

将一个字符串从一个函数“传递”到另一个函数,可以用地址传递的办法,即用字符数组名作参数,也可以用指向字符的指针变量作参数。在被调用的函数中可以改变字符串的内容,在主调函数中可以得到改变了的字符串。
例4

//方法④:用函数来实现
#include <stdio.h>
int main()
{
	void copy_string(char * from, char * to); //函数声明
	char * a= "I am a teacher.";//定义a为字符指针变量,指向一个字符串
	char b[20] = "You are a student."; //定义b为字符数组,内放一个字符串
	char * p = b; //字符指针变量p指向字符数组b的首元素
	printf("string a=%s\nstring b=%s\n", a, p);
	printf("\ncopy string a to string b:\n");
	copy_string(a, p); //指针变量作实参
	printf("string a=%s\nstring b=%s\n", a, b);
	return 0;
}
void copy_string(char * from, char * to) //形参是字符指针变量
{
	for ( ; * from != '\0'; from++, to++) //只要a数组没结束就复制到b数组
		* to = * from;
	* to = '\0';
}
//输出结果为:
string a=I am a teacher.
string b=You are a student.

copy string a to string b:
string a=I am a teacher.
string b=I am a teacher.

程序分析:

程序执行完以后,b数组的内容如图8.19(b)所示。可以看到,由于b数组原来的长度大于a数组,因此将a数组复制到b数组后,未能全部覆盖b数组原有内容,b数组最后3个元素仍保留原状。在输出b时由于按%s(字符串)输出,遇’\0’即宣告结束,因此第一个’\0’后的字符不输出。如果不采取%s格式输出,而用%c逐个字符输出是可以输出后面这些字符的。最后将’\0’赋给* to。
在这里插入图片描述

copy函数还可以改写为以下几种形式:

void copy_string(char * from, char * to)
{
	while((* to=* from)!='\0') //先赋值后判断
		{to++;from++}
} //此时先把'\0'复制过去,再进行判断,因此* to不用加'\0'


void copy_string(char * from, char * to)
{
	while((* to++ = * from++)!='\0') //先将* from赋给* to,然后使to和from增值
}

void copy_string(char * from, char * to)
{
	while(* from!='\0') //或者while(* from)
		* to++ = * from++;
	* to='\0'; 
}

例:有两个字符串,要求把字符串b连接到字符串a的后面。

#include <stdio.h>
int main()
{
	void link(char * p1, char * p2); //函数声明
	char a[40] = "I am a girl.", b[]="You are a boy.";
	 //定义a、b为字符数组,内存放一个字符串
	char * p1, * p2;
	p1 = a; //字符指针变量p1指向字符数组a的首元素
	p2 = b; //字符指针变量p2指向字符数组b的首元素
	printf("string a is:%s \n", a);
	link(p1, p2); //调用函数,指针变量作实参
	printf("string a is:%s", a);
	return 0;
}
void link(char * arr1, char * arr2)
{
	while (* arr1 != '\0') //将指针移动到'\0'处
		arr1++;
	for(arr2;* arr2!='\0';arr2++,arr1++) //只要a没结束就复制b数组
		* arr1 = * arr2;
	* arr1 = '\0'; //在复制完后加一个'\0'
}
//输出结果为:
string a is:I am a girl.
string a is:I am a girl.You are a boy.

程序分析:

  • 定义字符数组a,它的长度应能容纳两个字符串,现定义长度为40。字符数组b可不指定长度,它的长度由初始化时的字符串长度决定(它不必定义太大,因为不向它增加字符)。
  • 字符指针变量p1,p2分别指向字符串a和b的首元素。
  • 调用link函数时以p1,p2作为实参,将两个字符串的首元素地址传给形参(字符指针变量arr1和arr2)。这时实参p1和形参arr1都指向字符数组a的首元素(即a、p1、arr1都指向a[0]),实参p2和形参arr2都指向字符数组b的首元素(即b、p2和arr2都指向b[0])。
  • 首先使用while循环将p1指向字符串a的结尾,然后再使用for循环将字符串b复制到a串的后面。当复制完全部有效字符后,在a字符串的最后加一个’\0’。

调用函数时实参与形参的对应关系

实参形参实参形参
字符数组名字符数组名字符指针变量字符指针变量
字符数组名字符指针变量字符指针变量字符数组名


  • 使用字符指针变量和字符数组的区别

虽然用字符数组和字符指针变量都能实现字符串的存储和运算,但它们二者之间是有区别的,不应混为一谈,主要有以下几点:

  1. 字符数组由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址(字符串第1个字符的地址),绝不是将字符串放到字符指针变量中。
  2. 赋值方式不同。对字符数组可以在初始化时赋值,也可以对逐个元素赋值。
    如:
char str[6]="I love";
//或者
char str[6];
str[0] = 'I', str[1] = ' ', str[2] = 'l', str[3] = 'o', str[4] = 'v', str[5] = 'e';;

               \;\;\;\;\;\;\; 不能用以下办法对字符数组赋值(错误❌):

char str[6];
str="I love";

               \;\;\;\;\;\;\; 而对字符指针变量,可以采用下面方法赋值:

char * a;
a="I love China!";

               \;\;\;\;\;\;\; 但应注意,赋给a的不是字符,而是字符串第一个元素的地址。

  1. 对字符指针变量赋初值:
char * a="I love China!";
//等价于
char * a;
a="I love China!";

               \;\;\;\;\;\;\; 而对数组的初始化

char ser[14]={"I love China!"};
//不能等价于
char str[14];
str[]="I love China!"

               \;\;\;\;\;\;\; 即数组可以在定义时整体赋初值,但不能在赋值语句中整体赋值。

  1. 如果定义了一个字符数组,在编译时为它分配内存单元,它有确定的地址。而定义一个字符指针变量时,给指针变量分配内存单元,在其中可以放一个字符变量的地址。也就是说,该指针变量可以指向一个字符型数据,但如果未对它赋予一个地址值,则它并未具体指向确定的字符数据。例如:
char str[10];
scanf("%s",str);

               \;\;\;\;\;\;\; 是可以的。而常有人用下面的方法

char * a;
scanf("%s", a);

               \;\;\;\;\;\;\; 目的是想输入一个字符串,编译时发出“警告”信息,提醒未给指针变量指定初始值。虽然也能勉强运行,但这种方法是危险的,绝不应提倡。因为编译时虽然给指针变量a分配了内存单元,变量a的地址(即&a)是已指定了,但a的值并未指定,在a单元中是一个不可预料的值。在执行 scanf函数时要求将一个字符串输入到a所指向的一段内存单元(即以a的值(地址)开始的一段内存单元)中。而a的值如今却是不可预料的,它可能指向内存中空白的(未用的)用户存储区中(这是好的情况),也有可能指向已存放指令或数据的有用内存段,这就会破坏了程序,甚至破坏了系统,会造成严重的后果。在程序规模小时,由于空白地带多,往往可以正常运行,而程序规模大时,出现上述“冲突”的可能性就太多了。应当这样

char * a, str[10];
a = str;
scanf("%s",a);

               \;\;\;\;\;\;\; 先使a有确定值,也就是使a指向一个数组的首元素,然后输入一个字符串,把它存放在以该地址开始的若干单元中。

  1. 指针变量的值是可以改变的
#include<stdio.h>
int main()
{
	char * a="I love China!";
	a=a+7;
	printf("%s\n",a);
	return 0;
}
输出结果为:China!

程序分析:

指针变量a的值是可以变化的,现用“a=a+7;”使a指向输出字符串中的第8个字符。从a当时所指向的单元开始输出各个字符,直到遇’\0’为止,这样就输出了“China!”

               \;\;\;\;\;\;\; 数组名虽然代表地址,但它是常量,它的值是不能改变的。下面是错的:

char str[]={"I love China!"};
str=str+7;
printf("%s",str);
  1. 前面已说明,若指针变量p指向数组a,则可以用指针变量带下标的形式引用数组元素,同理,若字符指针变量p指向字符串,就可以用指针变量带下标的形式引用所指的字符串中的字符。如有:char * a="I love China!";
    则a[5]的值是a所指向的字符串"I love China!"中第6个字符(序号为5),即字母’e’。
    虽然并未定义数组a,但字符串在内存中是以字符数组形式存放的。A[5]按* (a+5)处理,即从a当前所指向的元素下移5个元素位置,取出其单元中的值。
  2. 字符数组中各元素的值是可以改变的(可以对它们再赋值),但字符指针变量指向的字符串常量中的内容是不可以被取代的(不能对它们再赋值)。如:
char a[]="House";
char * b=" House";
a[2]='r'; //合法,r取代u
b[2]='r'; //非法,字符串常量不能改变

  • 多维数组的指针
    指针变量可以指向一维数组中的元素,也可以指向多维数组中的元素。假如已定义二维数组a。
int a[3][4]={{1,3,5,7},{9,11,13,15},{17,19,21,23}}
  • a是一个二维数组名,a数组包含3行,即有3个行元素:a[0]、a[1]、a[2]。而每个行元素又是一个一维数组,它包含4个元素(即4个列元素)。可以认为二维数组是“数组的数组”,即二维数组a是由3个一维数组所组成的。
  • a代表二维数组首元素的地址,现在的首元素不是一个简单的整型元素,而是由4个整型元素所组成的一维数组,因此a代表的是首行(即第0行)的首地址。a+1代表序号为1的行的首地址。如果二维数组的首行的首地址为2000,若一个整型数据占4个字节,则a+1的值应该是2000+4×4=2016,因为第0行有4个整型数据。a+1指向a[1],或者说,a+1的值是a[1]的首地址。a+2代表a[2]的首地址,它的值应该是2000+8×4=2032。
    在这里插入图片描述

定义指向一维数组的指针变量:int (* p)[4];

  • 表示p是指针变量,指向有4个元素的一维数组,数组元素为整型。也就是p所指的对象是有4个整型元素的数组,此时p只能指向一个包含4个元素的一维数组,p的值就是该一维数组的起始地址。如果有赋值语句p=a+1;
    此时p指向数组序号为1的行(即a[1])的开头。

  • 指向函数的指针

不仅变量有指针,可以定义指针变量指向一个变量,而且函数也有指针,可以定义一个指向函数的指针变量指向一个函数。

  • 如果在程序中定义了一个函数,在编译时,编译系统给这个函数代码分配一段存储空间,这段存储空间的起始地址(又称入口地址)称为这个函数的指针。可以定义一个指向函数的指针变量,用来存放某一函数的起始地址,这就意味着此指针变量指向该函数。

定义的一般形式:数据类型(* 指针变量名)(函数参数表列)
如:int (* p)(int,int)
指针变量名为p,它指向的函数的类型为int型,函数有两个int型的形参。

  • 如果要用指针调用函数,必须先使指针变量指向该函数。 如:p=max;这就把max函数的入口地址赋给了指针变量p。
  • 用函数指针变量调用函数时,只需将(p)代替函数名即可(p为指针变量名),在(* p)之后的括号中根据需要写上实参。例如:c=(* p)(a,b);
  • 由于p指向max函数,因此它相当于c=max(a,b);
  • 指向函数的指针变量的一个重要用途是把函数的入口地址作为实参传递给函数的形参,此时形参是指向函数的指针变量。这样就能够在被调用的函数中使用实参函数

  • 返回指针值的函数
    一个函数可以返回一个整型值、实型值或字符值等,也可以返回指针型的数据,即一个地址。
    例如: int *a(int x, int y);

a是函数名,这个函数名前面有一个*,表示此函数是指针型函数(函数值是指针)。最前面的int表示返回的指针是指向整型变量的,x和y是函数a的形参(整型)。调用它以后能得到一个指向整型数据的指针(地址)。

返回指针的函数的一般形式为:类型名 *函数名(参数列表)

  • 这种形式与定义指向函数的指针变量很相似。!注意:在* a的两侧没有括号,右括号时,就成了指向函数的指针变量了。
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值