【C语言督学训练营 第五天】数组字符串相关知识

前言

今天的C语言训练营没有安排高维数组的讲解,因为考试中常考的是一维数组,老师根据一维数组分析了数组越界、数组在内存中的分布、缓冲区与字符数组等相关问题。

一、数组的定义

先看一个案例:

为了存放鞋子,假设你把衣柜最下面的一层分成了 10 个连续的格子。此时,让他人帮你拿鞋子就会很方便,例如你可直接告诉他拿衣柜最下面一层第三个格子中的鞋子。同样假设现在我们有 10 个整数存储在内存中,为方便存取,我们可以借助 C 语言提供的数组,通过一个符号来访问多个元素。

也就是在我们使用的时候,如果需要用到多个类型相同的变量,我们大可以选数组,来简化我们的操作。

1.一维数组

①.如何定义

一维数组的定义格式为

类型说明符 数组名 [常量表达式]

例如:定义一个整型数组,数组名为 a,它有 10 个元素

int a[10];

②.声明规则

注意:声明数组时要遵循以下规则:

  • (1)数组名的命名规则和变量名的相同,即遵循标识符命名规则。
  • (2)在定义数组时,需要指定数组中元素的个数,方括号中的常量表达式用来表示元素的个数,即数组长度。
  • (3)常量表达式中可以包含常量和符号常量,但不能包含变量。也就是说,C 语言不允许对数组的大小做动态定义,即数组的大小不依赖于程序运行过程中变量的值。
  • 能不用高维数组就不用高维数组(多一个维度多一点复杂,除非自己特别了解数组)

以下是错误的声明示例(最新的 C 标准支持,但是最好不要这么写):

int n;
scanf(“%d”, &n); /* 在程序中临时输入数组的大小 */
int a[n];

数组声明的其他常见错误如下:

① float a[0]; /* 数组大小为 0 没有意义 /
② int b(2)(3); /
不能使用圆括号 /
③ int k=3, a[k]; /
不能用变量说明数组大小*/

③.内存分布

数组元素在内存中是连续分布的,并且遵循小下标位于低地址的原则。数组中每一个元素占有的空间跟数组类型有关。
在这里插入图片描述

④.初始化方法

(1)在定义数组时对数组元素赋初值。例如,

int a[10]={0,1,2,3,4,5,6,7,8,9};
不能写成
int a[10];a[10]={0,1,2,3,4,5,6,7,8,9}

(2)可以只给一部分元素赋值。例如,

int a[10]={0,1,2,3,4};
定义 a 数组有 10 个元素,但花括号内只提供 5 个初值,这表示只给前 5 个元素赋初值,后 5 个
元素的值为 0。

(3)如果要使一个数组中全部元素的值为 0,那么可以写为

int a[10]={0,0,0,0,0,0,0,0,0,0};

int a[10]={0};

(4)在对全部数组元素赋初值时,由于数据的个数已经确定,因此可以不指定数组的长度。

例如,
int a[]={1,2,3,4,5};

2.二维数组

除了使用一维数组,还可以使用二维数组,二维数组定义方法与一维数组大致相同。

类型说明符 数组名 [常量表达式][常量表达式]

例如:定义一个整型数组,数组名为 a,它有 100 个元素

int a[10][10];

3.高维数组

C语言支持3维及以上维度的数组,不过我们一般用不到,所以不必深究,只需了解即可。使用数组的时候一定要避免数组越界这个问题,我会在下面介绍到。
先看看高维数组的使用:

//
// Created by Zhu Shichong on 2023/1/9.
//
#include<stdio.h>
main()
{
    
    
	int a[1][1][1];
    int b[1][1][1][1];
    int c[2][2][2][2][2];
    a[0][0][0]=1;
    b[0][0][0][0]=2;
    c[1][1][1][1][1]=3;
    printf("%d %d %d",a[0][0][0],b[0][0][0][0],c[1][1][1][1][1]);
    return 0;
}

数组元素的个数=定义时各维度下标大小连乘

二、访问数组元素相关问题

1.访问越界

前面介绍过数组定义之后在内存中是连续分布的,并且100个元素的数组最大数组下标为99,那么我们访问下标为100的元素会有什么问题呢?大致有以下两种情况:

  • 数组内存中接下来的空间被其余变量占用:会直接导致程序异常。
  • 数组内存中接下来的空间没有被其余变量占用:会读出内存中的脏值(也就是乱码。内存中有什么将会读什么),最终亦会导致程序异常。

由此可见,数组越界对我们的程序影响之大。

可能文字不如图片给的感觉强烈,下面给出一个案例:大家可以自行尝试

//
// Created by Zhu Shichong on 2023/1/9.
//
#include <stdio.h>
//数组越界
int main()
{
    
    
    int a[5]={
    
    1,2,3,4,5}; //定义数组时,数组长度必须固定
    int j=20;
    int i=10;
    a[5]=6; //越界访问
    a[6]=7; //越界访问会造成数据异常
    printf("i=%d\n",i); //i 发生改变
    return 0;
}

在这里插入图片描述
有这种情况的原因是:
编译器并不检查程序对数组下标的引用是否在数组的合法范围内。这种不加检查的行为有好处也有坏处,好处是不需要浪费时间对有些已知正确的数组下标进行检查,坏处是这样做将无法检测出无效的下标引用。
一个良好的经验法则是:
如果下标值是通过那些已知正确的值计算得来的,那么就无须检查;如果下标值是由用户输入的数据产生的,那么在使用它们之前就必须进行检查,以确保它们位于有效范围内。

2.数组的传递

先看一个案例:

//
// Created by Zhu Shichong on 2023/1/9.
//
#include <stdio.h>
#include<string.h>
//一维数组的传递,数组长度无法传递给子函数
//C 语言的函数调用方式是值传递(将地址的值传递过去)
void print(int b[],int len)
{
    
    
    int i;
    for(i=0;i<len;i++)
    {
    
    
        printf("%3d",b[i]);
    }
    b[4]=20; //在子函数中修改数组元素
    printf("\n");
}
//数组越界
//一维数组的传递
#define N 5
int main()
{
    
    
    int a[5]={
    
    1,2,3,4,5}; //定义数组时,数组长度必须固定
    print(a,5);
    printf("a[4]=%d\n",a[4]); //a[4]发生改变
    return 0;
}

如果在print数组时不传递len过去,将会导致无法获取数组的长度,从而无法访问到数组中所有的元素,想要在函数中访问数组中的所有元素,必要的一点就是将数组的长度通过参数的方式传进去。
原因如下:
这是因为一维数组在传递时,其长度是传递不过去的,所以我们通过 len来传递数组中的元素个数。实际数组名中存储的是数组的首地址,在调用函数传递时,是将数组的首地址给了变量 b(其实变量 b 是指针类型,具体原理会在指针节讲解),在 b[]的方括号中填写任何数字都是没有意义的。这时我们在 print 函数内修改元素 b[4]=20,可以看到数组 b 的起始地址和 main 函数中数组 a 的起始地址相同,即二者在内存中位于同一位置,当函数执行结束时,数组 a 中的元素 a[4]就得到了修改。
在这里插入图片描述
在这里插入图片描述

三、Scanf与字符数组

1.字符数组初始化

字符数组的定义方法与前面介绍的一维数组类似。例如,

char c[10];

字符数组的初始化可以采用以下方式。
(1)对每个字符单独赋值进行初始化。例如,

c[0]=‘I’;c[1]=’ ‘;c[2]=‘a’;c[3]=‘m’;c[4]=’';c[5]=‘h’;c[6]=‘a’;c[7]=‘p’;c[8]=‘p’;c[9]=‘y’;

(2)对整个数组进行初始化。例如,

char c[10]={‘I’,‘a’,‘m’,‘h’,‘a’,‘p’,‘p’,‘y’}

工作中一般不用以上两种初始化方式,因为字符数组一般用来存取字符串。通常采用的初始化方式是 char c[10]= “hello”。因为 C 语言规定字符串的结束标志为’\0’,而系统会对字符串常量自动加一个’\0’,为了保证处理方法一致,一般会人为地在字符数组中添加’\0’,所以字符数
组存储的字符串长度必须比字符数组少 1 字节。例如,char c[10]最长存储 9 个字符,剩余的 1 个字符用来存储’\0’。如果末尾没有存储\0将会可能导致程序出现以下错误(一直沿着字符数组打印,直到内存中存储\0为止)
在这里插入图片描述
在这里插入图片描述

2.scanf读取字符

最明显的就是scanf碰见空白字符会直接阻断,导致一句话有空格时没办法直接用scanf把那句话读进字符数组。可以通过对下面一个小程序输入 how are you 体会体会。

//
// Created by Zhu Shichong on 2023/1/9.
//
#include <stdio.h>
//scanf 读取字符串时使用%s
int main()
{
    
    
    char c[10];
    char d[10];
    scanf("%s",c);
    printf("%s\n",c);
    scanf("%s%s",c,d);
    printf("c=%s,d=%s\n",c,d);
    return 0;
}

最好的解决方法就是使用gets,puts函数

gets 函数类似于 scanf 函数,用于读取标准输入。前面我们已经知道 scanf 函数在读取字符串时遇到空格就认为读取结束,所以当输入的字符串存在空格时,我们需要使用 gets 函数进行读取。

gets 函数的格式如下:
char *gets(char *str);

gets 函数从 STDIN(标准输入)读取字符并把它们加载到 str(字符串)中,直到遇到换行符(\n)。如下例所示,执行后,我们输入"how are you",共 11 个字符,可以看到 gets 会读取空格,同时可以看到我们并未给数组进行初始化赋值,但是最后有’\0’,这是因为 gets 遇
到\n 后,不会存储\n,而是将其翻译为空字符’\0’。

puts 函数类似于 printf 函数,用于输出标准输出。puts 函数的格式如下:
int puts(char *str);

函数 puts 把 str(字符串)写入 STDOU(标准输出)。puts 会将数组 c 中存储的"how are you"字符串打印到屏幕上,同时打印换行,相对于 printf 函数,puts 只能用于输出字符串,同时多打印一个换行符,等价于 printf(“%s\n”,c)

四、字符数组相关函数

str 系列字符串操作函数主要包括 strlen、strcpy、strcmp、strcat 等。strlen 函数用于统计字符串长度,strcpy 函数用于将某个字符串复制到字符数组中,strcmp 函数用于比较两个字符串的大小,strcat 函数用于将两个字符串连接到一起。
函数原型如下:

头文件 #include <string.h>
size_t strlen(char *str);
char *strcpy(char *to, const char *from);
int strcmp(const char *str1, const char *str2);
char *strcat(char *str1, const char *str2);

对于传参类型 char*,直接放入字符数组的数组名即可。

注意以下几点:

  • strlen 函数的计算原理是通过判断结束符来确定字符串的长度。
  • strcpy 函数用来将字符串中的字符逐个地赋值给目标字符数组。例中我们将 c 复制给 d,就是将 c 中的每个字符依次赋值给 d,也会将结束符赋值给 d。注意,目标数组一定要大于字符串大小,即 sizeof(d)>strlen(c),否则会造成访问越界
  • strcmp 函数用来比较两个字符串的大小(首先比较首字母ASCII值,第一个相同比较第二个依次类推),由于字符数组 c 中的字符串与 d 相等,所以这里的返回值为 0。如果 c 中的字符串大于 d,那么返回值为c与d中不同位字符相减即 c-d(正值);如果 c 中的字符串小于 d,那么返回值依旧为c-d(负值)。如何比较两个字符串的大小呢?具体操作是从头开始,比较相同位置字符的 ASCII码值,若发现不相等则直接返回,否则接着往后比较。例如,strcmp(“hello”,“how”)的返回值是−1,即"hello"小于"how",因为第一个字符 h 相等,接着比较第二个位置的字符,e 的 ASCII 码值小于 o 的,然后返回e-o。不同的标准中有可能返回值不同,但一定是两字符串相同返回0,前面小后面大返回负值,否则返回正值。
  • strcat 函数用来将一个字符串接到另外一个字符串的末尾。例中字符数组 c 中存储的是"hello",我们将 d 中的"world"与 c 拼接,最终结果为"helloworld"。注意,目标数组必须大于拼接后的字符串大小,即 sizeof(c)>strlen(“helloworld”)

今天的分享到此结束!!!

猜你喜欢

转载自blog.csdn.net/apple_51931783/article/details/128732898