C语言基础 Day05 二维数组、字符串、函数
1.多维数组
前面的一维数组在定义的时候数组名后面会跟一个中括号去说明数组的长度,而多维数组则是在数组名后跟多个中括号,下面主要以二维数组为主,三维数组为辅,说一下多维数组的用法。
1.1 二维数组
二维数组的使用与一维数组的类似,其数组名后面有两个中括号,一般我们习惯用一个表的形式去描述一个二维数组,第一个中括号称为行,第二个中括号称为列。如定义一个二维数组int arr[2][3]={{2, 5, 3}, {3, 5, 2}};
则可以用下面一个2行3列的表格去描述:
2 | 5 | 3 |
---|---|---|
3 | 5 | 2 |
与一维数组一样,我们对下标的计数也是从0开始。也就是第0行、第1行;第0列、第1列、第2列去描述。数组的定义即为<数据类型> 数组名[行数][列数];
。
下面说说关于二维数组的定义与初始化,有以下形式:
// 第一种方式常规初始化: 给定行列数,赋值相应个数的元素
int arr[3][5] = {{2, 3, 54, 56, 7 }, {2, 67, 4, 35, 9}, {1, 4, 16, 3, 78}};
// 第二种方式不完全初始化: 未赋值的元素默认是0。
int arr[3][5] = {{2, 3}, {2, 67, 4, }, {1, 4, 16, 78}}; // 未被初始化的数值为 0 。
int arr[3][5] = {0}; // 初始化一个初值全为0的二维数组。
int arr[3][5] = {2, 3, 2, 67, 4, 1, 4, 16, 78}; // 系统自动分配行列。
// 第三种方式不完全指定行列初始化: 最高维的长度编译器自动计算
int arr[][2] = { 1, 3, 4, 6, 7 }; // 可以不指定行值
关于二维数组的行列数以及数组大小等的计算可以使用以下一些语句:
// 大小单位均为字节
整个数组的大小: sizeof(arr);
数组一行的d大小: sizeof(arr[0]);
一个元素的大小: sizeof(arr[0][0]);
行数:row = sizeof(arr)/ sizeof(arr[0]);
列数:col = sizeof(arr[0])/ sizeof(arr[0][0]);
在二维数组中,数组的首地址、数组的首元素地址、数组的首行地址三者是相等的。关于二维数组,给出以下示例代码:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
void test1()
{
int arr[3][4] = {
{ 2, 4, 5, 6 },
{ 2, 4, 5, 3},
{2, 4, 5, 7}
};
for (int i = 0; i < 3; i++) // 行
{
for (int j = 0; j < 4; j++) // 列
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
printf("数组的大小为: %u\n", sizeof(arr));
printf("数组行的大小: %u\n", sizeof(arr[0]));
printf("数组一个元素的大小: %u\n", sizeof(arr[0][0]));
printf("行数=总大小/一行大小: %d\n", sizeof(arr) / sizeof(arr[0]));
printf("列数=行大小/一个元素大小: %d\n", sizeof(arr[0]) / sizeof(arr[0][0]));
printf("arr = %p\n", arr);
printf("&arr[0][0] = %p\n", &arr[0][0]);
printf("arr[0] = %p\n", arr[0]);
}
// 二维数组的初始化
void test2()
{
int arr[][3] = { 2, 3, 4, 2, 6, 8, 3, 5, 3, 6 };
int row = sizeof(arr) / sizeof(arr[0]);
int col = sizeof(arr[0]) / sizeof(arr[0][0]);
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
putchar('\n');
}
}
int main(int argc, char* argv[])
{
// test1();
test2();
system("pause");
return 0;
}
#endif
【统计成绩】给定一个成绩表格,表格内有多干学生的三门课的成绩,计算每个学生三门功课的总成绩和每门功课的总成绩。
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
int main(int argc, char* argv[])
{
int scores[][3] = { 56, 78, 92, 45, 67, 93, 29, 83, 88, 93, 56, 89, 72, 83, 81 };
int row = sizeof(scores) / sizeof(scores[0]);
int col = sizeof(scores[0]) / sizeof(scores[0][0]);
// 获取学生三门功课的成绩
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", scores[i][j]);
}
putchar('\n');
}
// 求一个学生的总成绩
for (int i = 0; i < row; i++)
{
int sum = 0;
for (int j = 0; j < col; j++)
{
sum += scores[i][j];
}
printf("第%d个学生的总成绩为: %d\n", i + 1, sum);
}
// 求一门功课的总成绩
for (int i = 0; i < col; i++)
{
int sum = 0;
for (int j = 0; j < row; j++)
{
sum += scores[j][i];
}
printf("第%d门功课的总成绩为: %d\n", i + 1, sum);
}
system("pause");
return 0;
}
#endif
1.2 三维数组
三维数组,可以理解为多个二维表的叠加。如果说二维数组代表了笛卡尔坐标系中的x-y平面,那么三维数组中的第一个中括号就是该坐标系中的z轴。所以三维数组的定义是<数据类型> 数组名[层数][行数][列数];
。 更高维的数组以此类推,定义并初始化的时候第一个中括号都可以省略,由编译器自动计算。下面给出一个关于三维数组的示例:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
int main(int argc, char* argv[])
{
int a[3][4][3] = {
{
{2, 3, 4},
{ 2, 54, 4 },
{ 2, 3, 43 },
{ 2, 83, 4 }
},
{
{ 2, 34, 4 },
{ 72, 3, 4 },
{ 22, 33, 4 },
{ 27, 37, 4 }
},
{
{ 62, 3, 4 },
{ 2, 39, 4 },
{ 52, 3, 40 },
{ 2, 3, 49 }
}
};
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
for (int k = 0; k < 3; k++)
{
printf("%d ", a[i][j][k]);
}
printf("\n");
}
printf("\n\n");
}
system("pause");
return 0;
}
#endif
2.字符数组和字符串
2.1 字符数组和字符串的区别
关于这两个概念前面提到过,此处重点说一下两者的区别,字符串是以’\0’为结尾标志的,而字符数组未必有’\0’为结尾标志。所以字符串是一个特殊的字符数组。在使用printf()函数用%s输出字符串的时候,必须碰到’\0’才会结束,所以当打印字符数组的时候使用%s可能会发生内存溢出,而使用字符串则不会有这个问题。下面给出关于字符数组和字符串的一些示例,其中test1是演示两者用%s输出的区别,test2是统计字符串中每个字符出现次数的例子。
#if 1
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
void test1()
{
char str[6] = { 'h', 'e', 'l', 'l', 'o', '!' };
char str2[] = "world";
printf("%s\n", str2);
printf("%s\n", str);
}
// 统计字符串中每个字符出现的次数
void test2()
{
char str[11] = { 0 };
// scanf("%s", str);
for (int i = 0; i < 10; i++)
{
scanf("%c", &str[i]);
}
int count[26] = { 0 }; // 代表26个英文字母出现的次数
for (int i = 0; i < 10; i++)
{
int index = str[i] - 'a'; // 用户输入字符串在count中的下标
count[index] ++;
}
for (int i = 0; i < 26; i++)
{
if (count[i] != 0)
{
printf("%c字符在字符串中出现 %d 次\n", i + 'a', count[i]);
}
}
}
int main(int argc, char* argv[])
{
// test1();
test2();
system("pause");
return 0;
}
#endif
2.2 字符串获取函数scanf
前面关于输入讲解了scanf函数,scanf函数输入字符串的时候遇到换行或者是空格就会结束,且输入字符串的时候用于存储字符串的空间必须足够大,不然会发生溢出。那么怎么样才能够输入带换行或者是带回车的字符串呢?那么就需要用到正则表达式。借助正则表达式,我们可以输入想要输入格式的字符串。用法为scanf("%[正则表达式]",字符串存储空间地址);
。下面以一个输入空格但是以换行结束的scanf为例。
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
int main(int argc, char* argv[])
{
char str[100];
// scanf("%s", str);
scanf("%[^\n]s", str);
printf("%s\n", str);
system("pause");
return 0;
}
#endif
在代码中用到了正则表达式[^\n]
,其中^
表示除…之外,意思就是除了换行之外的都读取。在使用正则表达式时,%[正则表达式]s
可以省略掉s,写为%[正则表达式]
是一样的。
2.3 字符串操作函数
这一小节主要说一下关于字符串的输入与输出的一些函数以及一个求字符串长度的函数。主要有以下一些函数:
函数名 | 函数原型 | 返回值 | 参数 | 作用 |
---|---|---|---|---|
gets | char *gets(char *s) | 实际获取到的字符串首地址 | s: 用于存储字符串的空间地址 | 获取一个字符串,返回字符串的首地址,可以获取带有空格的字符串【不安全】 |
fgets | char *fgets(char *s, int size, FILE *stream) | 实际获取到的字符串首地址 | s: 用于存储字符串的空间地址 size: 空间大小 stream: 读取字符串的位置 | 获取一个字符串,会预留’\0’的存储空间。空间足够会读取’\n’,空间不足则舍弃 |
puts | int puts(const char *s) | 是否成功,成功返回一个非负数,失败返回-1 | s: 待写出到屏幕的字符串 | 将一个字符串写出到屏幕,输出字符串后自动添加’\n’ |
fputs | int fputs(const char *s, FILE *stream) | 是否成功,成功返回一个非负数,失败返回-1 | s: 待写出的字符串 stream: 写出的位置 | 将一个字符串写出到指定位置,输出字符串后不添加’\n’ |
strlen | size_t strlen(const char *s) | 有效的字符个数 | s: 待求长度的字符串 | 求字符串的长度,遇到’\0’结束 |
关于输出或者输入的位置,此处先说两个:
- stdin: 标准输入,键盘
- stdout: 标准输出,屏幕
关于上面操作的示例代码如下:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
// gets
void test1()
{
char str[100];
printf("获取的字符串为: %s\n", gets(str));
}
// fgets
void test2()
{
char str[10];
printf("获取的字符串为: %s\n", fgets(str, sizeof(str), stdin));
}
// puts
void test3()
{
char str[] = "helllo world\n";
int ret = puts(str); // puts("hello world");
printf("ret = %d\n", ret);
}
// fputs
void test4()
{
char str[] = "hello world\n";
// int ret = fputs(str, stdout);
int ret = fputs("hello world\n", stdout);
printf("ret = %d\n", ret);
}
// strlen()函数: 获取字符串的有效长度,不包含\0
void test5()
{
char str[] = "hello\0world";
printf("sizeof(str) = %u\n", sizeof(str));
printf("strlen(str) = %u\n", strlen(str));
}
// 实现strlen函数
void test6()
{
char str[] = "hello world";
int i = 0;
while (str[i] != 0)
{
i++;
}
printf("%d\n", i);
}
int main(int argc, char* argv[])
{
// test1();
// test2();
// test3();
// test4();
// test5();
test6();
system("pause");
return 0;
}
#endif
对于strlen函数,我们也可以手动编写代码实现,主要用到循环和字符数组,代码如下:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
int main(int argc, char* argv[])
{
char str1[] = "hello";
char str2[] = "world";
char str3[100];
int i = 0; // 循环str1
while (str1[i] != '\0')
{
str3[i] = str1[i]; // 循环着将str1中的每一个元素交给str3
i++;
} // str3=[h e l l o]
int j = 0; // 循环str2
while (str2[j]) // 等价于 while(str2[j] != 0),也等价于 while(str2[j] != '\0')
{
str3[i + j] = str2[j];
j++;
} // str3=[h e l l o w o r l d]
// 手动添加\0字符串结束标记
str3[i + j] = '\0';
printf("%s\n", str3);
system("pause");
return 0;
}
#endif
3.函数
3.1函数的基础
函数主要分为系统库函数和用户自定义函数。系统库函数也就是标准C库(libc)。使用系统库函数主要有两个步骤,第一步是引入相应的头文件,头文件中有该函数的申明。第二步是根据函数原型进行调用。而用户自定义函数除了需要提供函数原型之外,还需要有函数的实现。
使用函数,可以提高代码的复用率,同时可以提高程序模块化组织性。下面谈一谈关于函数的定义、申明、调用。
函数的定义包含函数原型(返回值类型、函数名、形参列表)和函数体(大括号、具体代码实现)。其中形参列表里面一定要包含形参的类型名和形参名,下面以两个数加法为例定义一个add函数进行加法运算:
int add(int a, int b)
{
return a + b;
}
其中int add(int a, int b)
就是函数原型,而{return a + b; }
就是函数体。
函数调用是调用该函数的功能,调用函数的格式是函数名(实参列表)
,实参是实际的参数。在调用的时候,必须严格按照形参进行填充参数,要遵循形参的个数、类型、顺序,不要添加类型描述。若要进行4和5的加法计算并保存到sum变量中,调用方式如下:
int sum = add(4, 5);
在函数调用之前,编译器必须见过函数定义,则否就需要函数声明。函数的声明包含函数原型,函数的声明格式是<数据类型> 函数名(形参列表);
,如对add函数进行声明如下:
int add(int a,int b);
如果没有声明,编译器会进行隐式声明,默认编译器在做隐式声明时函数的返回值类型都是int类型,编译器会根据调用的语句补全函数名和形参列表。下面给出一个关于函数的定义、声明、调用的例子:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
// 函数申明
void bubble_sort(int arr[]);
void print_arr(int arr[]);
int main(int argc, char* argv[])
{
printf("add = %d\n", add(4, 3));
int arr[] = { 2, 3, 2, 6, 34, 7, 23, 6, 2, 53 };
bubble_sort(arr);
print_arr(arr);
system("pause");
return 0; // 底层调用_exit() 退出
}
void bubble_sort(int arr[])
{
int tmp;
for (int i = 0; i < 10 - 1; i++)
{
for (int j = 0; j < 10 - 1 - i; j++)
{
if (arr[j] < arr[j + 1])
{
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
void print_arr(int arr[])
{
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int add(int a, int b)
{
return a + b;
}
#endif
3.2 exit()函数
exit()函数位于stdlib.h头文件中,该函数的作用是退出当前的程序,也就是结束整个程序。该函数的使用和关键字return有区别,return是返回当前函数调用,将返回值返回给调用者,结束的只是被调用函数。exit函数中传入的是一个int类型的。关于exit()函数的使用示例如下:
#if 1
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
int func(int a, char ch);
int main(int argc, char* argv[])
{
int ret = func(20, 'a');
printf("ret = %d\n", ret);
system("pause");
return 0;
}
int func(int a, char ch)
{
printf("a = %d\n", a);
printf("ch = %c\n", ch);
// return 10;
exit(10);
}
#endif
3.3 多文件编程
将多个含有不同函数功能的C语言文件编译到一起,生成一个可执行文件就叫做多文件编程。下面给出一个关于加减乘法多文件编程的例子。首先创建main.c、head.h、add.c、sub.c和mul.c这几个文件,如下:
首先在add.c、sub.c和mul.c分别写加减乘法的实现。
add.c文件:
int add(int a, int b)
{
return a + b;
}
sub.c文件:
int sub(int a, int b)
{
return a - b;
}
mul.c文件:
int mul(int a, int b)
{
return a * b;
}
接下来在head头文件写出该三个功能的函数声明。
head.h文件:
#ifndef __HEAD_H__
#define __HEAD_H__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
#endif
在编写头文件的时候,我们需要防止头文件的重复包含,也就是需要头文件卫士。关于头文件卫士,主要用两种写法,如下:
/// 写法一
#pragma once
// 写法二
#ifndef 头文件名宏
#define 头文件名宏
头文件内容
#endif
头文件宏名主要是将头文件的名字全大写,然后在两边写两个下划线,并把点也替换为下划线。如头文件名为head.h,则宏名应该写为__HEAD_H__
,如果头文件名loveletter.h,则宏名应该写为__LOVELETTER_H__
。在上面两个写法中,写法一只适合于Windows系统,其它系统环境应该使用写法二。
main.c文件:
#if 1
#define _CRT_SECURE_NO_WARNINGS
#include "head.h"
int main(int argc, char* argv[])
{
int a = 32;
int b = 43;
printf("%d + %d = %d\n", a, b, add(a, b));
printf("%d - %d = %d\n", a, b, sub(a, b));
printf("%d * %d = %d\n", a, b, mul(a, b));
system("pause");
return 0;
}
#endif
在main.c文件中,我们包含头文件用的是双引号,双引号""是用于包含用户自定义头文件的,而如果使用尖括号<>的话则无法找到自定义的头文件。