【程序设计基于C】第四章 数组和字符串 (完结)【期末复习】

本章速览

  • 本章内容:介绍了数组和字符串的定义、形式、一般形式和指针形式的输入输出。
  • 目标要求:掌握数组的定义和形式,对于同步学习或预习阶段要求能掌握数组一般形式的输入输出方式,而对于复习阶段则还要求掌握数组的指针形式调用方式。
  • 学习方式:希望能仔细的读文中所写的每一个字和每一个案例,再配合相关习题进行学习,一次花个几个小时搞懂,从此再也不为数组烦恼。

这部分的内容在个人心中是一个重点,因为在很多地方都会用得上,出现频率极高;并且在不会之前也是一个难点,甚至于在很长一段时间我都不敢写二维数组(因为不会)。不过好在某天下定决心,花了几个小时对各种代码进行了实验过后终于搞懂,从此以后再也不怕这类型的题了。

1. 数组的定义及数组元素的引用

1.1 一维数组的定义和元素引用方法

定义形式: 数组类型 数组名[常量表达式](以下以int x[10]为例)

  • 对于数组的定义,[]里必须要有常数或者是常量表达式

也就是说,在定义数组的时候需要首先确定数组的长度,并且不能定义空数组

这让从matlab开始接触程序的我非常的不适应,并且希望通过各种尝试来实现:可以根据自己数组数字的多少自动改变数组长度的功能。不过现在的我已经能坦然接受这个事实——我做不到。

在后来我学到可以申请一个较大空间的数组,来近似的达到这个效果。

1.1.1 一维数组的初始化

初始化int x[10] = {1,2,3}

  • 对于数组的初始赋值,必须要用{},数字通过,隔开。
  • 对于没有进行赋值的位置会用0进行占位【如果没有赋初值这个动作,则不会进行占位】

通过一个程序,对数组的赋初值进行更好的理解:

#include <stdio.h>

#define N 10
void Putnum(int *x)
{
    for (int i = 0; i < N; i++)
        printf("%d ", x[i]);
    printf("\n");
}
int main()
{
    int x1[N] = {1,2,3};
    int x2[N];

    Putnum(x1), Putnum(x2);
    return 0;
}

这个程序的输出结果为:

1 2 3 0 0 0 0 0 0 0
1 0 4233625 0 3 0 43 0 43 0
  • 上边一行为x1的值,由于对x1进行了赋初值处理,所以x1中剩下的位置全都是0
  • 下边一行为x2的值,由于未读x2进行赋初值处理,所以x2中所有元素全是随机数【不一定是0

1.1.2 一维数组的数组名和首地址

通过数组的学习,我们需要至少知道两个东西,这是接下来理解的关键(对于int x[10]

  1. &表示对值取地址,*表示取地址对应的值;
  2. x&x[0]都表示数组x[10]的首地址;
  3. x表示首地址的时候,x+i不在是数学意义上的,而是x移动i个位置后的地址

通过一段程序,可以很好的理解这些东西:

#include <stdio.h>
int main()
{
    int x[5] = {2,8,6,5,7}; 
    printf("&x[0]:%d x:%d\n&x[1]:%d x+1:%d", &x[0], x, &x[1], x+1);
    return 0;
}

程序的运行结果为

&x[0]:6618624 x:6618624
&x[1]:6618628 x+1:6618628

不难看出:

  • &x[0]x对应的是同一个值
  • x+1也不再是数学意义上的x+1【如果是的话x+1 = 6618624+1 = 6618625与实际输出不符合】
  • 而且x+1的值与&x[1]的值相同,也就是x【首地址】,移动1位后的地址

在接受了上述结论之后,可以较好的理解接下来的程序。

1.1.3 一维数组的元素引用方法

引用方法:数组的引用方法,非常的多和繁杂,对于数组x[10]介绍两种引用方法

  1. 下标引用法:x[4]
  2. 指针引用法:*(x+4)

两者等价的原因是因为x&x[0]都表示数组x的首地址,而*表示取对应地址的值,并且可以通过以下代码进行验证:

#include <stdio.h>
#define N 10
void Putnum(int *x)
{
    for (int i = 0; i < N; i++)
        printf("%d ", x[i]);
    printf("\n");
}
int main()
{
    int x[N] = {1,2,3};
    Putnum(x);
    printf("%d %d", x[2], *x+2);
    return 0;
}

而对于关于如何通过函数使用一维数组,我则在另一篇博客中通过一道例题进行了详细的说明。

1.2 二维数组和多维数组

多维数组为二维数组的再拓展,只要能理解清楚二维数组,多维数组的操作基本雷同。因此此只对二维数组进行详细的说明。

定义形式数据类型 数组名[常量表达式][常量表达式]

1.2.1 二维数组的初始化

初始化:和一维数组的初始化基本相同,不过需要注意区分下边两种不同的形式

  • int x[3][3] = {1,2,3};
  • int x[3][3] = {{1},{2},{3}};

通过一段程序进行区分:

#include <stdio.h>
#define N 3

void Output(int *x, int n)
{
    for (int i = 0; i < n*n ; printf("%d ", *(x+i)), i++)
        if ((i)%n == 0)
            printf("\n");
    printf("\n");
}

int main()
{
    int x1[N][N] = {1, 2, 3};
    int x2[N][N] = {{1}, {2}, {3}};
    
    printf("x1:");
    Output(*x1, N);
    printf("x2:");
    Output(*x2, N);

    return 0;
}

输出的结果为:

x1:
1 2 3
0 0 0
0 0 0
x2:
1 0 0
2 0 0
3 0 0

可以知道的是:

  • 当内层没有大括号时(情况1):给一个一个的元素赋值

    也就是给第一个元素赋值为1,第二个元素赋值为2…

  • 当内层存在大括号是(情况2):每个括号内的元素代表一行(即一个数组)

    按照数组赋值:也就是给第一行元素赋值为1【由于只有一个,所以就只给第一行的第一个元素赋值)…

在接受此结论的基础上,可以尝试想想这个数组的结果:int x[N][N] = {{1}, 2, 3, 5, {3, 5}};

需要注意的是:如果两种表达方式混用,则需要按照元素赋值的时候占满一行,否则就会报错

如:
int x[3][3] = {1, 2, {3}, 4};【会报错:1,2只是两个元素,不能占满一行】
int x[3][3] = {1, 2, 3, {4}};【不报错:1,2,3是三个元素,能占满一行】

1.2.2 二维数组的数组名和首地址

相比于一维数组,二维数组稍微复杂一些:对于一维数组来说,数组名等于首地址,这个规则同样适用,但是需要稍作变化。

通过上一篇博客的内容,我们可以知道的是:数组名其实就是地址,而且是数组的首地址,不过对于二维数组来说,稍微特殊一点的是,首地址既是第一行的首地址,也是第一行第一列的首地址(可能暂时感觉不到什么区别),下边我们通过一段程序进行说明:

#include <stdio.h>

#define M 3
#define N 4

int main()
{

    int x[M][N];
    int *p1, (*p2)[N];
    p1 = *x; //一级指针
    p2 = x;  //二级指针
    printf("x:%d p1:%d p2:%d &x[0]:%d &x[0][0]:%d\n", x, p1, p2, &x[0], &x[0][0]);

    return 0;
}

此程序的输出为如下,其中:

  • x:表示该二维数组的首地址;
  • p1:表示该二维数组对应的一级指针;
  • p2:表示该二维数组对应的二级指针;
  • &x[0]:表示该二维数组第一行的首地址【第一行为一个一维数组】;
  • &x[0][0]:表示该二维数组第一行第一列元素的地址。
x:6618592 p1:6618592 p2:6618592 &x[0]:6618592 &x[0][0]:6618592

通过输出结果,不难看出,乱七八糟的东西,全部对应一个值,也就是对应一个地址。如果没有理解的很清楚,可能你现在已经有点晕了,不过这些都不重要,暂时只需要记住的是这些变量的值都相等,但是使用方法有所不同

各地址之间的区别

为便于指代,我们对x&x[0]&x[0][0]进行命名【名字是我胡乱取的,只是为了便于说明,如果有知道标准的叫法的朋友欢迎指出】

  1. 数组名:x
  2. 二维数组的一级首地址:&x[0][0]
  3. 二维数组的二级首地址:&x[0]

注:以上命名都是我瞎编的,不知道是否存在或者是否正确,目的是为了便于理解,请不要拘泥于名字

之所以需要对此加以区分,是在于虽然这三个变量均对应同一个值,但是在运算的时候存在差异,而理解其中的差异有助于后续内容的了解。在上文中已经进行过说明:当x表示一维数组名的时候,x+i已经不在具有数学意义上的加法,实际上表示的是x数组中的第i个元素对应的地址,【如果要说数值的话,应该是x+i*sizeof(int)】。

而我们还知道的是当x表示二维数组的时候,x+i表示的是二维数组第i行的首地址。换句话说,一维数组是按照元素移动的,二维数组是按照行数移动的。通过一个例子,可以更好的理解刚才的内容:

#include <stdio.h>

#define M 3
#define N 4

int main()
{
    int x[M][N];
    printf("x:%d x+1:%d &x[0]+1:%d &x[0][0]+1:%d\n", x, x+1, &x[0]+1, &x[0][0]+1);
    return 0;
}

此程序的输出结果为:

x:6618608 x+1:6618624 &x[0]+1:6618624 &x[0][0]+1:6618612

由于我们知道,x[3][4]是一个三行四列的数组,也就是说每一行占3*sizeof(int) = 12位。对比一下输出结果,不难看出:

  • x+1&x[0]+1是相同的,都是移动了一行(12位)
  • &x[0][0]+1则与另外两个不同,只是移动了一个(4位)
#include <stdio.h>

#define M 3
#define N 4

int main()
{

    int x[M][N];
    int *p1, (*p2)[N];
    p1 = *x; //一级指针
    p2 = x;  //二级指针
    printf("x+1:%d p1+1:%d p2+1:%d &x[0]+1:%d &x[0][0]+1:%d\n", x+1, p1+1, p2+1, &x[0]+1, &x[0][0]+1);

    return 0;
}

如果运行上述代码,输出结果为:

x+1:6618608 p1+1:6618596 p2+1:6618608 &x[0]+1:6618608 &x[0][0]+1:6618596

可以发现:

  • 一级指针(p1)和一级首地址(&x[0][0])的移动情况相同,符合一维数组的移动规则。
  • 二级指针(p2)和二级首地址(&x[0])的移动情况相同,符合二维数组的移动规则。

换句话说,也就是虽然这些变量对应的都是同一个值,但是数据类型的不同,会导致其运算规则的不同。而指针和同级首地址之间的差别在于:指针变量对应的值能够改变,而首地址的值不能变,就比如说:

    p1++, p2++; //正确
    &x[0][0]++, &x[0]++; //错误:因为不能改变

相当于 &x[0][0]的类型为int const *p【注意和const * int p以及const int *p区分】

1.2.3 二维数组的元素引用方法

在知道一维数组的元素引用方法之后,其实二维数组的元素引用就会变得比较简单,直接上结论:二维数组的函数调用,可以分为两种情况:

以下内容是我在另一篇博客中的例子,主要用于介绍方法和思路,如果觉得看起来比较突兀的可以点击了解一下题目的背景。

  1. 将二维数组当做一维数组的形式(一级指针、地址)调用:【用法和一维数组相同】
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define M 3
#define N 4

void Input(int x[], int m, int n) //x[]形式
{
    srand(time(NULL));
    for (int i = 0; i < m; i++)
        for (int j = 0; j < n; j++)
            x[i * n + j] = rand() % 10; //对应输出:x[i]形式
    //x[i][j] = x[i*n+j]对应位置输入
}

void Output(int *x, int m, int n) //*x形式
{
    for (int i = 0; i < m * n; i++)
    {
        printf("%d ", *(x + i)); //无脑输出:*(x+i)形式
        if ((i + 1) % n == 0)    //输出n个就换一行
            printf("\n");
    }
}

int main()
{

    int x[M][N];
    int *p1;
    p1 = *x; 

    Input(*x, M, N);  //函数名调用【注意“*”】
    Output(p1, M, N); //指针调用
    printf("\n");
    Output(&x[0][0], M, N); //一维首地址调用
    printf("\n");

    return 0;
}

关于一维的*xx[]的传入子函数形式,以及x[i]*(x+i)的元素调用形式已经说得很详细了,此处不再赘述。

再啰嗦一点:由于x是二维数组的数组名(对应二级地址),需要将格式搞成一维数组数组名的格式(对应一级地址),需要通过*来实现。

个人的理解为:*表示取值,取了一次值,就相当于地址少了一级

  1. 按照二维数组的形式(二级指针,地址)调用:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define M 3
#define N 4

void Input(int x[][N], int m, int n) //x[][M]形式
{
    srand(time(NULL) + 1);
    for (int i = 0; i < m; i++)
        for (int j = 0; j < n; j++)
            *(*(x + i) + j) = rand() % 10; //对应输入:*(*(x+i)+j)形式

    //x+i:代表x的第i行的首地址,二维形式【按行移动】 ---> 加上*以后变为一维形式【按列移动】
    //*(x+i)+j:代表x的第i行第j列的地址:先移动到i行,再向右移动j列
    //*(*(x+i)+j):上述地址对应的值,即:x[i][j]
}

void Output(int x[M][N], int m, int n) //x[M][N]
{
    for (int i = 0; i < m; i++, printf("\n"))
        for (int j = 0; j < n; j++)
            printf("%d ", *(x[i] + j)); //对应输出:*(x[i] + j)
    //和上一种方法类似,只不过将 x[i] <--->  *(x+i)
}

int main()
{

    int x[M][N];
    int (*p2)[N];
    p2 = x;  //二级指针

    Input(x, M, N);   //二维数组的二维输入形式
    Output(p2, M, N); //二维数组的二维输出形式
    printf("\n");
    Output(&x[0], M, N);

    return 0;
}

这里需要进行几点说明:

  1. 二维数组x[M][N]中,M可以缺省,但是N不能,就像一维数组中的N不能缺省一样。
  2. 关于数组中元素的调用,最常见的还是x[i][j]的调用方法,和例子中的*(*(x + i) + j)*(x[i] + j)等价,且更加简便。

2. 字符数组和字符串

在理解数组之后,其实对于字符串的理解就会变得相对容易。在我看来二者本质上差别不大,无非就是一个存放的是一堆数字,一个存放的是一堆字符。

2.1 字符数组的定义和初始化

定义char 数组名[长度];

而定义中的规则和要求和数组一样,不过值得注意的一点是:字符串的结尾会有一位\0

也就是说,如果你希望这个字符串能放下10个字符,那么需要将其的长度设置为11

初始化:字符串的初始化,可以和数组一样一个一个复制,也可以用双引号直接初始化

  1. char str1[10] = {'H', 'e', 'l', 'l', 'o', '\0'};
  2. char str2[10] = "Hello";

如果末尾不加\0,请放心,系统会自动补上

2.2 字符数组的输入输出

2.2.1 字符数组的输入

字符数组的输入分为,和初始化一样,分为按照单个字符输入,和整体输入两种形式

  • 单个字符输入:利用scanf("%c", str[i]);或者str[i] = getchar;加上循环,可以实现
  • 整个字符串输入:利用scanf("%s", str);或者gets(str);可以实现字符串的整体输入

需要注意的是:

  • scanf("%s", str); 遇到空格、Tab、换行符都会停止读入,也就是说,一次可以输入多个字符串
  • gets(str); 读完所有的(一行),遇到换行符,才会停止读入,一次只能输入一个字符串

通过以下程序可以更好的理解,二者之间的差异

测试程序1:

#include <stdio.h>

int main() {
	char str1[80], str2[80];

	scanf("%s %s", str1, str2);
	printf("str1:%s\nstr2:%s", str1, str2);

	return 0;
}

测试程序2:

#include <stdio.h>

int main() {
	char str1[80], str2[80];

	gets(str1);
	gets(str2);
	printf("str1:%s\nstr2:%s", str1, str2);

	return 0;
}

输入:这里需要多一个换行符

test1 test2 test3

程序1输出:

str1:test1
str2:test2

程序2输出:

str1:test1 test2 test3
str2:

2.2.2 字符数组的输出

输出和输入一样,同样可以分为按照单个字符和按照字符串的情况进行输出:

  • 单个字符输出:利用printf(str[i]);或者getchar(str[i]);加上循环,可以实现
  • 整个字符串输出:利用printf("%s", str);或者puts(str);可以实现字符串的整体输入

二者之间的区别在于:

  1. puts输出结束后会自动换行,而printf在输出之后则不会换行
  2. printf("%s %s", str1, str2)在输出的时候可以输出多个字符串(也可以用多个puts实现)

2.3 字符串的功能的写法

关于字符串指针和地址相关的东西和数组的一样的,因此在这里就不再做过多的解释

下边分享几个常用的功能的实现方法,以及其对应的库函数。

2.3.1 字符串长度的统计

  • strlen(s):返回字符串s的长度,错误返回0

    • 标准库函数:#include <string.h>
    • 函数原型:``size_t strlen(const char*s);`
  • 代码实现:通过循环,一直跑到字符串的末尾,记录跑的步数

#include <stdio.h>
#include <string.h>

int main() {
	
	char str[80] = "Hello";
	int i = 0;
	
	while(str[i] != '\0')
		i++;
	int len1 = i;
	
	for(i = 0; str[i]; i++); //也可以写str[i]  != '\0'
	int len2 = i;
	
	int len3 = strlen(str);
	printf("len1 = %d\nlen2 = %d\nlen3 = %d", len1, len2, len3);

	
	return 0;
}

输出结果:

len1 = 5
len2 = 5
len3 = 5

2.3.2 字符串复制(赋值)

  • strcpy(str1,str2):将str2复制给str1(用str2str1赋值),失败返回NULL
    • 标准库函数:#include <string.h>
    • 函数原型:char * strcpy(char *str1, char *str2);
  • 代码实现:遍历整个字符串,一个字符一个字符的赋值
#include <stdio.h>
#include <string.h>

int main() {
	
	char str1[80], str2[80] = "Hello", str3[80];
	
	int i;
	for(i = 0; str2[i]; i++)
		str1[i] = str2[i];
	str1[i] = '\0';
	
	strcpy(str3,str2);
	printf("str1:%s\nstr3:%s", str1, str3);

	
	return 0;
}

输出结果:

str1:Hello
str3:Hello
  • 在此多说一句:在字符串的结尾加上'\0'是个好习惯:

如果原本str1str2更长,如果不加\0则会导致覆盖不完全,比如说下边的程序

#include <stdio.h>

int copy(char *str1, char *str2)
{
	int i;
	for(i = 0; str2[i]; i++)
		str1[i] = str2[i];
		
	return i;
} 

int main() {
	
	char str[80] = "Hello";
	char str1[80] = "newhello", str2[80] = "newhello";
	int i;
	
	i = copy(str1, str);  //加上结尾
	str1[i] = '\0';
	i = copy(str2, str);  //不加结尾

	printf("str1:%s\nstr2:%s", str1, str2);
	return 0;
}	

输出结果:

str1:Hello
str2:Hellollo

2.3.3 字符串的连接

  • strcat(str1,str2):将str2连接到str1末尾,失败返回NULL
    • 标准库函数:#include <string.h>
    • 函数原型:char * strcat(char *str1, char *str2);
  • 代码实现:类似复制,只不过在字符串末尾开始赋值
#include <stdio.h>
#include <string.h>

int length(char *str)
{
	int i;
	for(i = 0; str[i]; i++);
	return i;
} 

int main() {
	char str[80] = "word";
	char str1[80] = "Hello ", str2[80] = "New ";
	int len = length(str1); //找到str1的尾部
	
	for(int i = 0; str[i]; i++)
		str1[len+i] = str[i];
		
	strcat(str2,str);
	
	printf("str1:%s\nstr2:%s", str1, str2);
	return 0;
}	

此处默认为字符串长度已经会求

输出结果:

str1:Hello word
str2:New word

其他的话

最近忙于期末复习,更新有点慢,下周过后加快更新!!

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值