[华为云在线课程][C语言基础][五][指针与数组][学习笔记]
指针是一种保存变量地址的变量。在C语言中,指针的使用非常广泛,原因之一是,指针常常是表达某个计算的唯一途径,另外,对比其他方法使用指针通常可以生成更高效、更紧凑的代码。指针与数组的关系十分密切。
指针和goto语句一样,会导致程序难以理解。如果使用粗心,指针很容易就指向了错误的地方。如果使用谨慎,可以写出简单、清晰的程序。
ANSI C的一个最重要的变化是,明确指定了操纵指针的规则。ANSI C使用类型void *(指向void的指针)代替char *作为通用指针的类型。
1.指针与地址
通常的机器都有一系列连续编号或编址的存储单元,存储单元可以单个进行操纵,也可以以连续成组的方式操纵。
通常,1个字节可以存放一个char类型的数据,2个相邻的字节存储单元可存储一个short(短整型)类型的数据,4个相邻的字节存储单元可存储一个long(长整型)类型的数据。指针是能够存放一个地址的一组存储单元(通常是2个或4个字节)。
一元运算符 & 用于取得一个对象的地址。
// 将把c的地址赋值给变量p,称p为指向c的指针。地址运算符&只能应用于内存中的对象,即变量与数组元素。不能用于表达式、常量或register类型的变量。
p = &c;
一元运算符*是间接寻址或间接引用运算符。当作用于指针时,将访问指针所指向的对象。
// 假定x和y是整数,ip是指向int类型的指针
int x=1,y=2,z[10];
int *ip; /* ip是一个指向int类型的指针 */
ip=&x; /* ip指向x */
y=*ip; /* y变成1 */
*ip=0; /* x变成0 */
ip=&z[0]; /* ip指向z[0]位置 */
int *ip;
这个声明为了方便记忆。表明表达式 *ip 的结果是int类型。同样的,对函数的声明也是同样方式。double *dp, atof(char *);
表明 *dp 和 atof(s) 的值都是double类型,且 atof 的参数是一个指向char类型的指针。
注意:指针只能指向某种类型的对象,每个指针必须指向某种特定的数据类型。(一个例外情况是指向void类型的指针可以存放指向任何类型的指针,但它不能间接引用其自身)
如果指针ip指向整型变量,那么 *ip = *ip + 10;
将把 *ip 的值加10。一元运算符 * 和 & 的优先级比算术运算符的优先级高,因此赋值语句 y = *ip + 1
将把 *ip 指向的对象的值取出并加1,然后再将结果赋值给y。
*ip += 1
将ip指向的对象的值加1,等同于 ++*ip
或 (*ip)++
。语句 (*ip)++
的括号是必须的,否则,该表达式将对ip进行加1运算,而不是对ip指向的对象进行加1运算,这是因为,类似于 * 和 ++ 这样的一元运算符遵循从右至左的结合顺序。
指针也是变量,可以在程序中直接使用。如果 iq 是另一个指向整型的指针,那么语句 iq = ip
将把 ip 中的值拷贝到 iq 中,这样,指针 iq 也将指向 ip 指向的对象。
2.指针与函数参数
由于C语言是以传值的方式将参数值传递给被调用参数。被调用函数不能直接修改主调用函数中变更的值。
// 排序函数可能会使用一个名为 swap 的函数来交换两个次序颠倒的元素。
void swap(int x,int y)
{
int temp;
temp=x;
x=y;
y=temp;
}
// 以上方法无法实现 x 和 y 两个元素交换
// 因为由于参数传递采用传值方式,上述 swap 函数不会影响到参数 a 和 b 的值。仅仅交换了 a 和 b 的副本的值。
// 那么如何实现目标交换两个元素的值,可以使主调程序将指向所要交换的变量的指针传递给被调用函数
swap(&a,&b);
由于一元运算符 & 用来取变量的地址,这样 &a 就是一个指向变量 a 的指针。swap函数的所有参数都声明为指针,并且通过这些指针来间接访问所指向的操作数。
// interchange *px and *py
void swap(int *px,int *py)
{
int temp;
temp=*px;
*px=*py;
*py=temp;
}
// 指针参数使得被调用函数能够访问和修改主调函数中对象的值。
3.指针与数组
在C语言中,通过数组下标所能完成的任何操作都可以通过指针来实现。一般来说,用指针编写的程序比用数组下标编写的程序执行速度快,但同时指针实现的程序理解稍微困难。
int a[10];
定义了一个长度为10的数组a。也就是定义了一个由10个对象组成的集合,这10个对象存储在相邻的内存区域中,名字分别为a[0]、a[1]、…、a[9]。
a[i]表示该数组的第i个元素。如果pa的声明为 int *pa;
则说明是一个指向整型对象的指针,那么赋值语句 pa = &a[0];
可以将指针pa指向数组a的第0个元素,pa的值为数组元素a[0]的地址。这样赋值语句 x = *pa;
就是将数组元素a[0]的内容复制到变量x中。
以此类推,pa+i 就是指向pa所指向数组元素之后的第i个元素,而pa-i指向pa所指向数组元素之前的第i个元素。如果指针pa指向a[0],那么*(pa+i)就是数组a[i]的地址。无论数组a中元素的类型或数组长度是什么,pa+i 指向pa所指向的对象之后的第i个对象。
数组类型的变量或表达式的值是该数组第0个元素的地址。 pa = &a[0];
表示pa和a具有相同的值。因为数组名所代表的就是该数组最开始的一个元素的地址,所以等价于 pa = a;
。对数组元素a[i]引用可以写成*(a+i)这种形式,在计算数组元素a[i]的值时,C语言实际上先将其转换为*(a+i)的形式然后再进行求值。两个等价的表达式分别加上地址运算符&,&a[i]和a+i也是等价的。a+i是a之后第i个元素的地址。如果pa是个指针,pa[i]与*(pa+i)等价。一个通过数组和下标实现的表达式可等价地通过指针和偏移量实现。数组名和指针之间不同是,指针是一个变量,数组名不是变量。
当把数组名传递给一个函数时,实际上传递的是该数组第一个元素的地址。在被调用函数中,此参数是一个局部变量,所以必须是一个指针,也就是一个存储地址值的变量。
// 利用指针编写一个strlen函数,用于计算一个字符串的长度
int strlen(char *s)
{
int n;
for(n=0 ; *s!='\0' ; s++)
{
n++;
}
return n;
}
// 因为s是一个指针,所以对其执行自增是合法的。执行s++运算不会影响strlen函数调用者的字符串,仅对该指针在strlen函数的私有副本运算。
在函数定义中, char s[];
和 char *s;
是等价的。后面的表达式更直观表明该参数是一个指针。如果a是一个数组,那么 f(&a[2])
和 f(a+2)
都把起始于a[2]的子数组的地址传递给函数f。f(int arr[]){...}
和 f(int *arr){...}
也是等价的。但要注意的是,引用数组边界之外的对象是非法的。
4.字符指针与函数
字符串常量是一个字符数组,例如"I am a string",在字符串的内部表示中,字符数组以空字符’\0’结尾,程序通过检查空字符找到结尾。所以,字符串占用的存储空间比双引号内的大1。
字符串常量最常用的用法就是作为函数参数,例如:printf("xxx");
,实际上是通过字符指针访问该字符串的。printf接受的是一个指向字符数组第一个字符的指针。字符串常量可通过一个指向其第一个元素的指针访问。
以标准库中strcpy(s,t)函数为例说明指针和数组的不同实现。
/*
函数strcpy(s,t)把指针t指向的字符串复制到指针s指向的位置。
如果使用语句s=t实现该功能,实质上只是拷贝了指针,并没有复制字符。
为了完成字符的复制,需要使用一个循环语句。
*/
// 通过数组方法实现
void strcpy(char *s, char *t)
{
int i;
i = 0;
while ((s[i] = t[i]) != '\0')
{
i++;
}
}
// 通过指针方法实现
void strcpy1(char *s, char *t)
{
int i;
i = 0;
while ((*s = *t) != '\0')
{
s++;
t++;
}
}
// 因为参数都是值传递的,所以在strcpy函数中可以任意使用参数s和t。每循环一次,指针就沿着相应的数组前进一个字符,知道将t中的结束符复制到s位置。
练习:
/*
用指针方式实现函数strcat
函数strcat(s,t)将t指向的字符串复制到s指向的字符串的尾部。
*/
#include<stdio.h>
#include<stdlib.h>
void strcpy(char *s, char *t);
void strcat(char *s, char *t);
int main()
{
char S1[1024] = "String One";
char S2[1024] = "String Two";
printf("字符串1为: %s\n", S1);
printf("字符串2为: %s\n", S2);
strcat(S1, S2);
printf("合并后的字符串为: %s\n", S1);
system("pause");
return 0;
}
void strcpy(char *s, char *t)
{
while ((*s = *t) != '\0')
{
s++;
t++;
}
}
void strcat(char *s, char *t)
{
while (*s)
{
++s;
}
strcpy(s, t);
}
/*
编写函数strend(s,t)。
如果字符串t出现在字符串s的尾部,该函数返回1,否则返回0。
*/
#include<stdio.h>
#include<stdlib.h>
int strend(char *s, char *t);
int strlen(char *s);
int strcmp(char *s, char *t);
int main()
{
char S1[1024] = "this is a string.";
char S2[1024] = "ng.";
char S3[1024] = "ng";
if (strend(S1, S2))
{
printf("字符串 %s 存在 %s 在尾部.\n", S1, S2);
}
else
{
printf("字符串 %s 不存在 %s 在尾部.\n", S1, S2);
}
if (strend(S1, S3))
{
printf("字符串 %s 存在 %s 在尾部.\n", S1, S3);
}
else
{
printf("字符串 %s 不存在 %s 在尾部.\n", S1, S3);
}
system("pause");
return 0;
}
int strlen(char *s)
{
int n;
for (n = 0; *s != '\0'; s++)
{
n++;
}
return n;
}
int strcmp(char *s, char *t)
{
for (; *s == *t; s++, t++)
{
if (*s == '\0')
{
return 0;
}
}
return *s - *t;
}
int strend(char *s, char *t)
{
int result = 0;
int s_length = 0;
int t_length = 0;
// 获取字符串长度
s_length = strlen(s);
t_length = strlen(t);
// 检查字符串t的长度是否能够匹配字符串s
if (t_length <= s_length)
{
// 移动指针使字符串t在字符串s开始位置
s += s_length - t_length;
// 对两个字符串进行比较
if (0 == strcmp(s, t))
{
result = 1;
}
}
return result;
}
5.多维数组
把某月某日这种日期表示形式转换为某年中第几天的表示形式,例如:3月1日是非闰年的第60天,是闰年的第61天。我们定义两个函数以进行日期转换:函数 day_of_year 将某月某日的日期表示形式转为某年中的第几天的表示形式,函数 month_day 执行相反的转换。因为后一个函数要返回两个值,所以在函数 month_day 中,月和日两个参数使用指针的形式。例如:month_day(1988,60,&m,&d);
将把m的值设置为2,把d的值设置为29。(2月29日)。
例如:都要用到一张记录每月天数的表。对闰年和非闰年来说,每个月的天数不同,所以要分别放在一个二维数组进行判断比较。
// 将数组daytab的第一列元素设置为0,月份的值为1-12,而不是0-11.
static char daytab[2][14] = {
{ 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
{ 0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
};
int day_of_year(int year, int month, int day)
{
int i, leap;
leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
for (i = 1; i < month; i++)
{
day += daytab[leap][i];
}
return day;
}
int month_day(int year, int yearday, int *pmonth, int *pday)
{
int i, leap;
leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
for (i = 1; yearday > daytab[leap][i]; i++)
yearday -= daytab[leap][i];
*pmonth = i;
*pday = yearday;
}
6.指针数组的初始化
编写一个函数 month_name(n) ,返回一个指向第n个月名字的字符串的指针。包含一个私有的字符串数组,当被调用时,返回一个指向正确元素的指针。
// 返回第几个月的名字
char *month_name(int n)
{
static char *name[] = {
"Illegal month", "January", "February",
"March", "April", "May", "June",
"July", "August", "September", "October",
"November", "December"
};
};
name的声明是一个一维数组,数组的元素为字符指针。name数组的初始化通过一个字符串列表实现,列表中的每个字符串赋值给数组相应位置的元素。第i个字符串存在存储器第i个位置,指向它的指针存在name[i]中。由于没有指明name的长度,编译器编译时将对初值个数进行统计,并将准确数字填入数组的长度。
7.指针与多维数组
以下面一个例子说明二维数组和指针数组之间的区别:
int a[10][20];
int *b[10];
// a是一个真正的二维数组,分配了200个int类型长度的存储空间,并通过常规的矩阵下标计算公式20*row+col计算得到元素a[row][col]的位置。
// b的定义为仅仅分配了10个指针,并没有对它们进行初始化,初始化必须采用显式方式进行,比如静态初始化或通过代码初始化。
// 指针数组的优点在于,数组每行长度可以不同。
- 点赞
- 收藏
- 关注作者
评论(0)