目录
本章速览
- 本章内容:介绍了数组和字符串的定义、形式、一般形式和指针形式的输入输出。
- 目标要求:掌握数组的定义和形式,对于同步学习或预习阶段要求能掌握数组一般形式的输入输出方式,而对于复习阶段则还要求掌握数组的指针形式调用方式。
- 学习方式:希望能仔细的读文中所写的每一个字和每一个案例,再配合相关习题进行学习,一次花个几个小时搞懂,从此再也不为数组烦恼。
这部分的内容在个人心中是一个重点,因为在很多地方都会用得上,出现频率极高;并且在不会之前也是一个难点,甚至于在很长一段时间我都不敢写二维数组(因为不会)。不过好在某天下定决心,花了几个小时对各种代码进行了实验过后终于搞懂,从此以后再也不怕这类型的题了。
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]
)
&
表示对值取地址,*
表示取地址对应的值;x
和&x[0]
都表示数组x[10]
的首地址;- 当
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]
介绍两种引用方法
- 下标引用法:
x[4]
- 指针引用法:
*(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]
进行命名【名字是我胡乱取的,只是为了便于说明,如果有知道标准的叫法的朋友欢迎指出】
- 数组名:
x
- 二维数组的一级首地址:
&x[0][0]
- 二维数组的二级首地址:
&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 二维数组的元素引用方法
在知道一维数组的元素引用方法之后,其实二维数组的元素引用就会变得比较简单,直接上结论:二维数组的函数调用,可以分为两种情况:
以下内容是我在另一篇博客中的例子,主要用于介绍方法和思路,如果觉得看起来比较突兀的可以点击了解一下题目的背景。
- 将二维数组当做一维数组的形式(一级指针、地址)调用:【用法和一维数组相同】
#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;
}
关于一维的
*x
和x[]
的传入子函数形式,以及x[i]
和*(x+i)
的元素调用形式已经说得很详细了,此处不再赘述。
再啰嗦一点:由于x
是二维数组的数组名(对应二级地址),需要将格式搞成一维数组数组名的格式(对应一级地址),需要通过*
来实现。
个人的理解为:
*
表示取值,取了一次值,就相当于地址少了一级
- 按照二维数组的形式(二级指针,地址)调用:
#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;
}
这里需要进行几点说明:
- 二维数组
x[M][N]
中,M
可以缺省,但是N
不能,就像一维数组中的N
不能缺省一样。 - 关于数组中元素的调用,最常见的还是
x[i][j]
的调用方法,和例子中的*(*(x + i) + j)
和*(x[i] + j)
等价,且更加简便。
2. 字符数组和字符串
在理解数组之后,其实对于字符串的理解就会变得相对容易。在我看来二者本质上差别不大,无非就是一个存放的是一堆数字,一个存放的是一堆字符。
2.1 字符数组的定义和初始化
定义:char 数组名[长度];
而定义中的规则和要求和数组一样,不过值得注意的一点是:字符串的结尾会有一位\0
也就是说,如果你希望这个字符串能放下
10
个字符,那么需要将其的长度设置为11
初始化:字符串的初始化,可以和数组一样一个一个复制,也可以用双引号直接初始化
char str1[10] = {'H', 'e', 'l', 'l', 'o', '\0'};
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);
可以实现字符串的整体输入
二者之间的区别在于:
puts
输出结束后会自动换行,而printf
在输出之后则不会换行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
(用str2
给str1
赋值),失败返回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'
是个好习惯:
如果原本str1
比str2
更长,如果不加\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
其他的话
最近忙于期末复习,更新有点慢,下周过后加快更新!!