总言
C语言:函数、数组初步认识。
文章目录
- 总言
- 1、函数
- 1.1、是什么
- 1.1.1、基本介绍
- 1.1.2、库函数使用演示(strcpy、memset)
- 1.1.3、自定义函数使用演示
- 1.2、函数参数、传值调用和传址调用
- 1.3、相关练习
- 1.3.1、写一个函数:可以判断一个数是不是素数
- 1.3.2、写一个函数:判断一年是不是闰年
- 1.3.3、写一个函数:实现一个整形有序数组的二分查找
- 1.3.4、写一个函数:每调用一次这个函数,就会将 num 的值增加1
- 1.4、函数的嵌套调用和链式访问
- 1.4.1、函数嵌套调用
- 1.4.2、函数链式访问(printf返回值介绍)
- 1.5、函数声明和定义
- 1.6、函数递归
- 1.6.1、概念与递归的必要条件
- 1.6.2、相关练习演示
- 1.6.2.1、接受一个整型值(无符号),按照顺序打印它的每一位
- 1.6.2.2、编写函数不允许创建临时变量,求字符串的长度
- 1.6.2.3、求n的阶乘(不考虑溢出)
- 1.6.2.4、求第n个斐波那契数(不考虑溢出)
- 2、数组
- 2.1、一维数组
- 2.1.1、一维数组创建
- 2.1.2、一维数组初始化
- 2.1.3、一维数组使用与存储
- 2.2、二维数组
- 2.2.1、二维数组创建与初始化
- 2.2.2、二维数组的基本使用
- 2.2.3、二维数组在内存中的存储
- 2.3、数组越界
- 2.4、数组作为函数参数
- 2.4.1、数组名初步认识
- 2.4.2、冒泡排序设计1.0
1、函数
1.1、是什么
1.1.1、基本介绍
1)、C语言中函数的分类
库函数
自定义函数
库函数: 早期的C语言是没有库函数的,此时由于各有各的方法实现,会带来如下问题:功能类似,但代码冗余、开发效率低、不标准。因此把一些常用的功能实现为函数,集成为库,由C语言提供。
自定义函数: 由我们自己设计实现的函数,即为自定义函数,能满足我们个人需求。自定义函数和库函数一样,有函数名,返回值类型和函数参数。
ret_type fun_name(para1, * )
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
{} 函数体
1.1.2、库函数使用演示(strcpy、memset)
1)、库函数strcpy使用演示
演示要求:拷贝字符串,并验证结尾字符"\0"也一并拷贝了。
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[] = "abcdefg";
char arr2[20] = "xxxxxxxxxxx";
strcpy(arr2, arr1);
return 0;
}
2)、库函数memset使用演示
演示要求:将数组前五个元素值设置为“X”
#include<stdio.h>
#include<string.h>
int main()
{
char arr[] = "hello world!";
memset(arr, 'X', 5);
printf("%s\n", arr);
return 0;
}
1.1.3、自定义函数使用演示
1)、写一个函数:可以找出两个整数中的最大值
int get_max(int x, int y)
{
return (x > y) ? (x) : (y);
}
int main()
{
int num1 = 0;
int num2 = 0;
scanf("%d %d",&num1,&num2);
int max = get_max(num1, num2);
printf("max = %d\n", max);
return 0;
}
2)、写一个函数:可以交换两个整形变量的内容
#include <stdio.h>
void Swap1(int x, int y)//传值
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
void Swap2(int* x, int* y)//传引用
{
int tmp = 0;
tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap1(num1, num2);
printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
Swap2(&num1, &num2);
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
1.2、函数参数、传值调用和传址调用
1)、基本介绍
实际参数(实参):真实传给函数的参数
形式参数(形参):是指函数名后括号中的变量
实参:
①可以是常量、变量、表达式、函数等。
②无论实参是何种类型的量,在进行函数调用时都必须有确定的值,以便把这些值传送给形参。
形参:
①形式参数只有在函数被调用的过程中才实例化(分配内存单元)
②形式参数在函数调用完成之后就自动销毁。
2)、传值调用和传址调用
**传值调用:**
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
**传址调用:**
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
1.3、相关练习
1.3.1、写一个函数:可以判断一个数是不是素数
#include<math.h>
int is_prime(int val)
{
//试除法
//2~n-1 \ 2~sqrt(n)
for (int i = 2; i < sqrt(val); ++i)
{
if(val % i == 0)//说明有别的因数
{
return 0;
}
}
return 1;
}
int main()
{
//任务目标:判断100~200间的素数
for (int i = 101; i <= 200; i+=2)//首先排除偶数
{
if (is_prime(i) == 1)
{
printf("%d ", i);
}
}
return 0;
}
1.3.2、写一个函数:判断一年是不是闰年
int is_leap_year(int year)
{
//四年一闰,百年不闰;
//四百年一闰;
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return 1;
}
return 0;
}
int main()
{
//任务目标:打印1000~2000年之间的闰年
//产生年份
for (int y = 1000; y <= 2000; ++y)
{
//判断
if (is_leap_year(y) == 1)
{
printf("%d ", y);
}
}
printf("\n");
return 0;
}
1.3.3、写一个函数:实现一个整形有序数组的二分查找
int binary_search(int arr[],int key,int len)
{
int left = 0;
int right = len - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (arr[mid] < key)
{
left = mid + 1;
}
else if (arr[mid] > key)
{
right = mid - 1;
}
else
{
return mid;
}
}
return -1;
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };//有序数组
int key = 0;
printf("输入要查找的数字:");
scanf("%d", &key);
int len = sizeof(arr) / sizeof(arr[1]);
int ret=binary_search(arr, key, len);
if (-1 == ret)
{
printf("找不到\n");
}
else {
printf("找到了,下标为:%d\n", ret);
}
return 0;
}
一个问题:binary_search
能否不传递实参len,而是选择在函数体内计算int len = sizeof(arr) / sizeof(arr[1]);
?
binary_search(arr, key);
回答:arr数组传参,传递的不是整个数组,而是数组首元素地址。int arr[]
本质上是int* arr
。
1.3.4、写一个函数:每调用一次这个函数,就会将 num 的值增加1
void test(int* num)
{
(*num)++;//优先级问题
}
int main()
{
int num = 0;
int n = 5;//调用次数
while (n)
{
test(&num);//传址
n--;
}
printf("%d\n", num);
return 0;
}
1.4、函数的嵌套调用和链式访问
1.4.1、函数嵌套调用
1)、是什么
函数和函数之间可以根据实际的需求进行组合,也就是互相调用的。
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i = 0;
for (i = 0; i < 3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0;
}
衍生思考:函数是否能嵌套定义?
int main()//函数一
{
void three_line()//函数二
{
int i = 0;
for (i = 0; i < 3; i++)
{
void new_line()//函数三
{
printf("hehe\n");
}
}
}
return 0;
}
回答:不能。
1.4.2、函数链式访问(printf返回值介绍)
1)、是什么
把一个函数的返回值作为另外一个函数的参数,称之为链式访问。
#include<string.h>
int main()
{
int len = strlen("abcdef");
printf("%d\n", len);
printf("%d\n", strlen("abcdef"));
return 0;
}
2)、其它一些例子
如下,以下结果是什么?
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
1.5、函数声明和定义
函数声明:
1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3. 函数的声明一般要放在头文件中的。
函数定义:
函数的定义是指函数的具体实现,交待函数的功能实现。
演示举例:
//函数声明
int get_max(int x, int y);
int get_max(int , int );//可以把形参名字省略
int main()
{
int num1 = 10;
int num2 = 20;
int max = get_max(num1, num2);
printf("max = %d\n", max);
return 0;
}
//函数定义
int get_max(int x, int y)
{
return (x > y) ? (x) : (y);
}
1.6、函数递归
1.6.1、概念与递归的必要条件
1)、是什么
说明:递归即函数调用其本身,其主要思想在于把大事化小,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。类似于一个深层的嵌套(俄罗斯套娃)。
以下为一个演示例子:main函数自己调用其本身
//main函数自己调用自己
#include<stdio.h>
int main(void)
{
printf("hehe\n");
main();
return 0;
}
如上图,虽然是实现了递归,但其结果为程序终止,通过调试发现,造成程序终止的原因是栈溢出。
2)、递归的两个使用条件
1、递归存在限制条件,符合限制条件后,递归停止。
2、每次递归调用,都要朝这一限制条件发展。
1.6.2、相关练习演示
1.6.2.1、接受一个整型值(无符号),按照顺序打印它的每一位
例如:
输入:1234,输出 1 2 3 4.
1)、假如采用非递归法,实现如下:
要正位打印每一个数,可将其不同数位上的数一一剥离。通常来说十进制下,/10、%10
能做得到。但如果直接运用,得到的则是4321
逆序,所以我们可以将剥离下的每个位数先存放在数组中,再逆序打印。
int main()
{
unsigned int num = 0;
scanf("%u", &num);
int digit = 0;//统计num位数
int arr[10] = { 0 };//存储num每个位数,10为32位下无符号整型最大位数
while (num)
{
arr[digit] = num % 10;
num /= 10;
digit++;
}
for (int i = digit - 1; i >= 0; i--)//倒序打印
{
printf("%d ", arr[i]);
}
return 0;
}
2)、假如采用递归,实现如下:
在递归方式中,对于1234仍旧采用/10、%10
的方法,只是我们将数字分为两部分,对末位数,将其剥离打印,对非末位数,再次调用函数剥离。故有:
f(1234)=f(123)+4
f(123)=f(12)+3
f(12)=f(1)+2
void print(unsigned int num)
{
if (num > 9)//说明num目前大于个位
{
print(num / 10);
}
printf("%d ",(num % 10));
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);
print(num);
return 0;
}
上述print
中,if条件语句
和printf打印
二者顺序至关重要,其决定着输出结果顺序问题。由于一上来就满足if语句,所以进入递归调用函数,直到来到终止条件时,执行printf语句,然后再层层递归返回,完成相应递归层中没有执行的函数语句。
1.6.2.2、编写函数不允许创建临时变量,求字符串的长度
1)、使用库函数的情况如下:
arr
存储的是a、b、c、d、e、f、\0
,strlen
求得'\0'
前字符长度。
#include <stdio.h>
#include <string.h>
int main(void)
{
char arr[] = "abcdef";
printf("%d\n", strlen(arr));
return 0;
}
2)、创建临时变量的情况如下:
int mystrlen(char* arr)
{
int count = 0;
while (*arr != '\0')
{
count++;
arr++;
}
return count;
}
int main(void)
{
char arr[] = "abcdef";
printf("%d", mystrlen(arr));
return 0;
}
3)、不允许创建临时变量的情况如下
f("abc")=1+f("bc")
f("bc")=1+f("c")
f("c")=1+f(‘\0')
f(‘\0')=0
既有:f("abc")
=1+f("bc")
=1+1+f("c")
=1+1+1+f(‘\0')
=3
#include <stdio.h>
int mystrlen(char* arr)
{
if (*arr == '\0')
return 0;
return 1 + mystrlen(++arr);
}
int main(void)
{
char arr[] = "abcdef";
printf("%d", mystrlen(arr));
return 0;
}
1.6.2.3、求n的阶乘(不考虑溢出)
1)、使用非递归的方法
//非递归方式:
#include<stdio.h>
int main(void)
{
int n;
scanf("%d", &n);
int sum = 1;
while (n > 0)
{
sum *= n;
n--;
}
return 0;
}
2)、使用递归的方法
//用递归的方式:
int factorial(int n)
{
if (n <= 1)
return 1;
else
return n * factorial(n - 1);
}
int main(void)
{
int n;
scanf("%d", &n);
printf("%d", factorial(n));
return 0;
}
1.6.2.4、求第n个斐波那契数(不考虑溢出)
1)、使用递归的方法
//以递归方式实现:
#include<stdio.h>
int fib(int n)
{
if (n <= 2)
return 1;
return fib(n - 1) + fib(n - 2);
}
int main(void)
{
int n;
scanf("%d", &n);
printf("%d", fib(n));
return 0;
}
2)、使用非递归的方法
非递归的方法能解决此处的递归效率问题。
//以非递归的方式实现:
#include<stdio.h>
int fib(int n)
{
int a = 1;
int b = 1;
int c = 1;//此处为c赋值为1不影响后续计算,且能解决第一、第二个斐波那契数
while (n - 2)//头俩项不需要计算,从第三项开始,第三项需要循环1次,第四项循环2次,以此类推,第n项循环n-2次。
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main(void)
{
int n;
scanf("%d", &n);
printf("%d", fib(n));
return 0;
}
2、数组
2.1、一维数组
2.1.1、一维数组创建
1)、基本介绍
数组:一组相同类型元素的集合。
type_t arr_name [const_n];
//type_t 数组的元素类型
//arr_name 数组名
//const_n 常量表达式,用来指定数组的大小
2)、演示实例
一般情况如下:
int arr1[10];
关于数组元素大小是否能用变量的说明:
C99标准之前,不支持使用变量,只能是常量。C99中,新增了变长数组的概念,允许数组的大小是变量,而且要求编译器支持C99标准。
int count = 10;
int arr2[count];
同样的,即使用const修饰变量后为数组定义大小,因其是常变量,也要求编译器支持C99标准。
const int count = 10;
int arr2[count];
需要注意:
1、const
修饰的count
,在C语言中属于常变量,在C++中属于常量。因此假若我们用.cpp
文件来实现,在VS中是能运行成功的。
2、变长数组不能初始化。因为元素个数的不确定性,初始化数组是无意义的。比如,变长数组中元素个数是根据scanf函数来自设的,则初始化变长数组便是无意义之事。
int count = 10;
scanf("%d",&count);
int arr2[count];
2.1.2、一维数组初始化
数组的初始化,是指在创建数组的同时给数组的内容一些合理初始值。
1)、整型数组初始化相关演示
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//完全初始化
int arr2[10] = { 1,2,3,4};//不完全初始化
return 0;
}
假如数组不初始化,其内部放置的是随机值,根据编译器而定,有的会是0xcccccccc,有的会是0xcdcdcdcd,不唯一,局部变量同理。但要注意,全局变量不初始化时默认值为0,静态变量同理。
int a;//全局变量
int main()
{
int arr3[10];//不初始化
int b;//局部变量
static int c;//静态变量
printf("%d\n", a);
printf("%d\n", c);
return 0;
}
原因:内存空间被划分为不同区域,对于全局变量、静态变量,其放置在静态区,默认初始化为0;对于局部变量,一些形式参数,其放置在栈区,默认不初始化时为随机值。
2)、字符型数组初始化相关演示
int main()
{
char ch1[] = { 'a','b','c','d' };
char ch2[] = { 'a',98,'c','d' };//字符b的ASCII码值为98
char ch3[] = "abcd";//'a','b','c','d','\0'
char ch4[6]= { 'a','b','c','d' };//不完全初始化,默认0,对应ASCII为'\0'
return 0;
}
致于此处涉及的strlen相关介绍,我们在初识C语言那一章节中有讲述。
2.1.3、一维数组使用与存储
1)、如何访问一维数组元素:下标引用操作符
数组是使用下标来访问的,下标从0开始。
#include <stdio.h>
int main()
{
int arr[10] = { 0 };//数组的不完全初始化
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组的元素个数
int i = 0;//做下标
for (i = 0; i < sz; i++)//遍历赋值
{
arr[i] = i;
}
for (i = 0; i < sz; ++i)//遍历打印
{
printf("%d ", arr[i]);
}
return 0;
}
arr[i]
:需要注意的是,虽然我们说,在非C99标准下,不能使用变量来定义数组大小,但从未规定,在使用下标引用操作符访问数组时,我们不能使用变量。[ ]
左操作数是数组名,右操作数是下标大小。
2)、一维数组在内存中的存储
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; ++i)
{
printf("&arr[%d] = %p\n", i, &arr[i]);
}
return 0;
}
观察可得:
1、一维数组在内存中连续存放。
2、随着数组下标的增长,地址是由低到高变化的。(这样做就方便使用指针来操作)
int*p=arr;//数组首元素
p+1;//int* ,+1 得下一个元素,依此类推
为什么地址间相差4的原因:
整型元素内存大小是4字节,而一个字节有一个地址,四个字节就占据了四个地址,即用四个地址表示一个整型元素。
2.2、二维数组
2.2.1、二维数组创建与初始化
1)、二维数组创建
//数组创建
int arr[3][4];
char arr[3][5];
double arr[2][4];
2)、二维数组初始化
//数组初始化
int arr[3][4] = {1,2,3,4};
//1、先放行,行满跳下一列,值不够时默认为0
//2、不完全初始化,默认为0。
int arr[3][4] = {{1,2},{4,5}};
//1、若规定了行列具体元素,可用花括号分隔
int arr[][4] = {{2,3},{4,5}};
//1、二维数组如果有初始化,行可以省略,列不能省略
对于省略行的情况,一行溢出的数组元素可以换行排列。但如果省略行列,如下述情况,数组可能为arr[1][10]
,arr[2][5]
,arr[5][2]
, arr[10][1]
,故同时省略行和列是非法的。
int arr[ ][ ] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
2.2.2、二维数组的基本使用
二维数组的使用也是通过下标的方式。[][]
指定行、列。
#include <stdio.h>
int main()
{
int arr[][4] = { {1,2,3},{2,3,4},{3,4,5},{4,5,6} };
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
问题:
类似于int arr[][4]
,是否能算处单独的行、列?
//行:
int row = sizeof(arr) / sizeof(arr[0]);//整个数组大小/任意一行大小
//列:
int col = sizeof(arr[0]) / sizeof(arr[0][0]);//某一行整体元素大小/该行单个元素大小
2.2.3、二维数组在内存中的存储
#include <stdio.h>
int main()
{
int arr[][4] = { {1,2,3},{2,3,4},{3,4,5},{4,5,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("&arr[%d][%d] = %p\n", i, j, &arr[i][j]);
}
printf("\n");
}
return 0;
}
由上述结果可知,这也是为什么二维数组初始化时行可以省略,列不能省略的原因。因为二维数组连续存放,不知道行但知道了列,也能在内存区域中划分处二维数组具体行列大小。
另外,需要注意的是,对于二维数组,可以将每行视作一维数组。其数组名称为arr[0]
、arr[1]
、arr[2]
、arr[3]
。
对每行元素的访问,相当于arr[0][j]
、arr[1][j]
、arr[2][j]
、arr[3][j]
。
即二维数组是一维数组的数组:二维数组arr[4][4]
的元素个数有有三个,每个元素都是一个一维数组(以int为类型,元素个数为4个的数组)
2.3、数组越界
基本描述:
如下述代码:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
for (i = 0; i <= 10; i++)
{
printf("%d ", arr[i]);//当i等于10的时候,越界访问了
}
return 0;
}
需要注意的是,二维数组也存在越界。那么,二维数组越界在内存中的显示?
由于二维数组在内存中同一维数组一样是连续排列的,而且是每行排列完后接着下一行。由此可知,若二维数组越界,对于最后一行最后元素,越界会访问到数组外的内存空间,对于数组内部,越界会访问到后续行中元素。
2.4、数组作为函数参数
2.4.1、数组名初步认识
数组名是数组首元素地址。
但有两个例外:
1、sizeof(数组名)
,此时数组名表示整个数组,计算的是整个数组的大小。
2、&数组名
。此处数组名表示整个数组,取出的是整个数组的地址。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("arr: %p\n", arr);
printf("arr[0]: %p\n", &arr[0]);
printf("&arr: %p\n", &arr);
printf("\nsizeof(arr):%d\n\n",sizeof(arr));
printf("arr: %p\n", arr+1);
printf("arr[0]: %p\n", &arr[0]+1);
printf("&arr: %p\n", &arr+1);
return 0;
}
2.4.2、冒泡排序设计1.0
void bubble_sort(int arr[], int size)
{
//总趟数
for (int i = 0; i < size - 1; ++i)
{
int flag = 1;//用于判断数组本身是否已经有序
//单趟数
for (int j = 0; j < size - 1 - i; ++j)
{
if (arr[j] > arr[j + 1])//排升序
{
flag = 0;
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
if (flag == 1)//说明没有进入单趟交换
{
break;
}
}
}
int main()
{
int arr[] = { 10,9,8,7,6,5,4,3,2,1 };
//要求:写一个冒泡排序,将数组arr排为升序。
int size = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, size);
for (int i = 0; i < size; ++i)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}