[华为云在线课程][C语言基础][五][指针与数组][学习笔记]

举报
John2021 发表于 2022/10/18 07:33:54 2022/10/18
【摘要】 指针是一种保存变量地址的变量。在C语言中,指针的使用非常广泛,原因之一是,指针常常是表达某个计算的唯一途径,另外,对比其他方法使用指针通常可以生成更高效、更紧凑的代码。指针与数组的关系十分密切。指针和goto语句一样,会导致程序难以理解。如果使用粗心,指针很容易就指向了错误的地方。如果使用谨慎,可以写出简单、清晰的程序。ANSI C的一个最重要的变化是,明确指定了操纵指针的规则。ANSI 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个指针,并没有对它们进行初始化,初始化必须采用显式方式进行,比如静态初始化或通过代码初始化。
// 指针数组的优点在于,数组每行长度可以不同。
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。