目录
前言
函数
函数的定义
函数的分类
库函数 为什么会有库函数?
自定义函数
函数参数
实际参数(实参)
形式参数(形参)
函数调用
传值调用
传址调用
函数的嵌套调用和链式访问
嵌套调用
链式访问(了解)
函数的声明和定义
函数的声明
函数递归(汉诺塔)
什么是递归?
递归的两个必要条件
数组
一维数组的创建和初始化
数组的创建
数组的初始化
一维数组的使用
一维数组在内存中的存储
二维数组的创建和初始化
二维数组的使用
二维数组在内存中的存储
数组越界
数组作为函数参数(传址调用)
什么是数组名
两个例外
操作符
全部操作符的介绍
算术操作符
移位操作符
位操作符
赋值操作符
单目操作符
关系操作符
逗号表达式
下标引用,结构体成员访问,函数调用
表达式求值
隐式类型转换
算数转换
操作符的属性
总结
前言
- 本期是C语言的第三期,需要看前面的几期框架可以点到我博客里看一下。
- 下一期博主就要对指针进行总结了。同时我还会不定时分享一些干货。期待你的回访哦!
❤️ :热爱编程学习,期待一起交流!
🙏:博主水平有限,如有发现错误,求告知,多谢!
函数
函数的定义
数学中我们常见到函数的概念。但是你了解C语言中的函数吗?
维基百科中对函数的定义:子程序
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码,
由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
函数的分类
- 库函数
- 自定义函数
库函数 为什么会有库函数?
我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上,用到printf函数。
或者输入一个数的时候,用到scanf函数。
向上面的描述的基础功能,我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员
进行软件开发。
简单的总结,C语言常用的库函数都有:
IO函数
字符串操作函数
字符操作函数
内存操作函数
时间/日期函数
数学函数
其他库函数
当我们使用一个库函数的时候不知道头文件怎么用?
这里强烈推荐中文版的一个文档http://zh.cppreference.com
用的时候只需要查找一下就可以了,不需要把库函数全部记住,只需要学会如何查找就好了。
自定义函数
- 如果库函数能干所有的事情,那还要程序员干什么?
- 所以更加重要的是自定义函数。
- 自定义函数和库函数一样,有函数名,返回值类型和函数参数。
- 但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
- 函数的组成格式:
返回类型 函数名(参数类型 形式参数1,参数类型 形式参数2) { 函数体; 返回值; }
ret_type fun_name(para1, * ) { statement;//语句项 } //ret_type 返回类型 //fun_name 函数名 //para1 函数参数
- 我们举个例子
- 写一个函数可以找出两个整数中的最大值。
#include <stdio.h> //get_max函数的设计 int get_max(int x, int y)//这里是get_max函数的定义,这里的x和y是形参,用来接收实参传来的值。 { return (x>y)?(x):(y);//返回值 } int main() { int num1 = 10; int num2 = 20; int max = get_max(num1, num2);//这里是调用get_max函数,num1和num2均为实参。 printf("max = %d\n", max); return 0; }
函数参数
实际参数(实参)
- 真实传给函数的参数,叫实参。
- 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形
参 - 实参可以是:常量、变量、表达式、函数等。
形式参数(形参)
像上面的实参(num1,num2)和形参(x,y)使用的不是同一空间。即他们的值所在的地址不同。
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内
存单元),所以叫形式参数。
形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有
效。
我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。
函数调用
传值调用
- 函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
- 写一个函数可以判断一个数是不是素数。
#include<stdio.h> #include<math.h> int is_prime(int n) { int i = 0; for (i = 2; i < sqrt(n); i ++) { if (n % i == 0) { return 0; } } return 1; } int main() { int n = 0; scanf("%d", &n); int ret = is_prime(n); if (ret == 1) { printf("是素数"); } else if(ret == 0) { printf("不是素数"); } return 0; }
传址调用
- 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
- 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操
作函数外部的变量。 - 传址调用需要特别注意传数组名。
函数的嵌套调用和链式访问
嵌套调用
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。
#include <stdio.h> 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; }
函数可以嵌套调用,但是不能嵌套定义。
链式访问(了解)
把一个函数的返回值作为另外一个函数的参数。
#include <stdio.h> #include <string.h> int main() { char arr[20] = "hello"; int ret = strlen(strcat(arr,"bit"));//这里介绍一下strlen函数 printf("%d\n", ret); return 0; } #include <stdio.h> int main() { printf("%d", printf("%d", printf("%d", 43))); //结果是啥? //注:printf函数的返回值是打印在屏幕上字符的个数 return 0; }
函数的声明和定义
函数的声明
告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数
声明决定不了。
函数的声明一般出现在函数的使用之前。要满足先声明后使用。
函数的声明一般要放在头文件中的。(.h结尾的文件)
###函数的定义
函数的定义是指函数的具体实现,交待函数的功能实现。
一般放在源文件中(.c结尾的文件)
函数递归(汉诺塔)
什么是递归?
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接 调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件
- 下面是递归的典型问题汉诺塔问题。图片来自百度。
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> int Hanio_twice(int num) { if(1 == num) return 1; else return 2 * Hanio_twice(num - 1) + 1; } int main() { int num = 0; scanf("%d", &num);//塔数 int ret = Hanio_twice(num); printf("完成%d层的汉诺塔需要%d步\n", num, ret); return 0; }
数组
一维数组的创建和初始化
- 数组是一组相同类型元素的集合。
数组的创建
- 数组的创建方式:
type_t arr_name [const_n]; //type_t 是指数组的元素类型 //const_n 是一个常量表达式,用来指定数组的大小。
数组创建的实例:
//代码1 int arr1[10]; //代码2 int count = 10; int arr2[count];//数组时候可以正常创建? //代码3 char arr3[10]; float arr4[1]; double arr5[20];
数组的初始化
- 数组的初始化是指,在创建数组的同时给数组的内容一些合理初始值(初始化)。
- 代码如下
int arr1[10] = {1,2,3}; int arr2[] = {1,2,3,4}; int arr3[5] = {1,2,3,4,5}; char arr4[3] = {'a',98, 'c'}; char arr5[] = {'a','b','c'}; char arr6[] = "abcdef";
- 数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定。
- 但是对于下面的代码要区分,内存中如何分配
char arr1[] = "abc";//内存里存放了四个元素 分别是'a''b''c''\0' char arr2[3] = {'a','b','c'};//而这里面存放的是'a''b''c'
字符串的结束标志是‘\0’
一维数组的使用
- 对于数组的使用我们之前介绍了一个操作符: [] ,下标引用操作符。它其实就数组访问的操作符。
我们来看代码:
#include <stdio.h> int main() { int arr[10] = {0};//数组的不完全初始化 int sz = sizeof(arr)/sizeof(arr[0]);//计算数组的元素个数 //对数组内容赋值,数组是使用下标来访问的,下标从0开始。所以: int i = 0;//做下标 for(i=0; i<10; i++) { arr[i] = i;//这里i写10,行吗?当然不行,写10意味着访问的是第11个元素,数组会越界。 } for(i=0; i<10; ++i) { printf("%d ", arr[i]); //输出数组的内容 } return 0; }
总结:
- 数组是使用下标来访问的,下标是从0开始。
- 数组的大小可以通过计算得到。
int arr[10]; int sz = sizeof(arr)/sizeof(arr[0]);//40/4=10;所以数组的长度是10
一维数组在内存中的存储
#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]);//我们将10个元素的地址打印出来。 } return 0; }
- 打印结果如下
- 由图片可知。
- 每个地址相差四位数,9C和A0差四位,后面都是四位(这些数字都是16进制)。
- 数组在内存中是连续存放的。
- 用数组名指向整个存储空间最小的地址
二维数组的创建和初始化
//数组创建 int arr[3][4]; char arr[3][5]; double arr[2][4];
//数组初始化 int arr[3][4] = {1,2,3,4}; int arr[3][4] = {{1,2},{4,5}}; int arr[][4] = {{2,3},{4,5}};//二维数组如果有初始化,行可以省略,列不能省略
二维数组的使用
- 二维数组的使用也是通过下标的方式。
- 代码如下,创建一个3*4的二维数组,并对数组初始化输入和输出。图形如下。
#include <stdio.h> int main() { int arr[3][4] = {0};//创建一个三行四列的二维数组 int i = 0; for(i=0; i<3; i++) { int j = 0; for(j=0; j<4; j++) { arr[i][j] = i*j;//循环赋值 } } for(i=0; i<3; i++) { int j = 0; for(j=0; j<4; j++) { printf("%d ", arr[i][j]);//循环输出 } } return 0; }
二维数组在内存中的存储
- 像一维数组一样,这里我们尝试打印二维数组的每个元素。
#include <stdio.h> int main() { int arr[3][4]; int i = 0; for(i=0; i<3; i++) { int j = 0; for(j=0; j<4; j++) { printf("&arr[%d][%d] = %p\n", i, j,&arr[i][j]); }//将3*4的二维数组的每个元素的地址打印出来,来研究其在内存中的存储。 } return 0; }
打印结果如下。
和上面的一维数组一样,每次跳过四位数,说明二维数组在内存中也是连续的。
数组越界
数组的下标是有范围限制的。
数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。
所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。
但是编译器本身并不会报错,所以此时需要程序员多加留意。
#include <stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10};//定义了10个元素。 int i = 0; for(i=0; i<=10; i++) { printf("%d\n", arr[i]);//当i等于10的时候,这时候访问的是第11个元素! } return 0; }
数组作为函数参数(传址调用)
- 我们下面以错误的冒泡排序算法来举例子。
#include <stdio.h> void bubble_sort(int arr[]) { int sz = sizeof(arr)/sizeof(arr[0]);//这一行代码代码错了.错误思维是应该是40/4=10 int i = 0;//但结果是4/4原因是因为sizeof(arr)算出来的是首元素的地址,首元素的地址大小看机器,以32位的来说是4字节。 for(i=0; i<sz-1; i++) { int j = 0; for(j=0; j<sz-i-1; j++) { if(arr[j] > arr[j+1]) { int tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp; } } } } int main() { int arr[] = {3,1,7,5,8,9,0,2,4,6}; bubble_sort(arr);//传过去的是数组名,默认是数组首元素的地址。 for(i=0; i<sizeof(arr)/sizeof(arr[0]); i++) { printf("%d ", arr[i]); } return 0; }
- 所以我们得出一个结论:数组作为函数参数的时候,不是把整个数组的传递过去。而传递的是数组首元素的地址。所谓传址调用
什么是数组名
#include <stdio.h> int main() { int arr[10] = {1,2,3,4,5}; printf("%p\n", arr);// printf("%p\n", &arr[0]); printf("%d\n", *arr); return 0; }
- 三个 输出结果可以证明:数组名arr就是首元素的地址!
两个例外
- 有两种情况数组名不代表首元素地址
1、sizeof(arr)单独放一个数组名的时候,sizeof计算的是整个数组的字节大小。
2、取地址加数组名表示整个数组的地址。
操作符
全部操作符的介绍
算术操作符
+ - * / %
- 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
- 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
- % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
移位操作符
对于移位操作符,和位操作符,操作的都是二进制序列。
- 左移操作符
移位规则:左边抛弃、右边补0
- 右移操作符
移位规则:
1. 逻辑移位 左边用0填充,右边丢弃
2. 算术移位 左边用原该值的符号位填充,右边丢弃
位操作符
& //按位与 | //按位或 ^ //按位异或 注:他们的操作数必须是二进制序列整数。
- 一道变态面试题
- 不能创建临时变量,交换a和b的值。
#include <stdio.h> int main() { int a = 10; int b = 20;//用到下面两个结论。 a = a^b;//1.两个相同的数异或得到的结果为0. b = a^b;//2.任何一个数和0异或的结果都为它本身。 a = a^b; printf("a = %d b = %d\n", a, b); return 0; }
赋值操作符
int a = 10; int x = 0; int y = 20;
复合赋值操作符
+=//a+=1相当于a = a+1 -= *= /= %= >>= <<= &= |= ^=
- 复合赋值操作符看起来更加简便。
单目操作符
! 逻辑反操作 - 负值 + 正值 & 取地址 sizeof 操作数的类型长度(以字节为单位) ~ 对一个数的二进制按位取反 -- 前置、后置-- ++ 前置、后置++ * 间接访问操作符(解引用操作符) (类型) 强制类型转换
关系操作符
> >= < <= != 用于判断“不相等” == 用于判断“相等”
- 要格外注意if语句判断的时候的双等号。分清==和=的区别。
逗号表达式
- 逗号表达式,就是用逗号隔开的多个表达式。
- 逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
下标引用,结构体成员访问,函数调用
[ ] 下标引用操作符 ( ) 函数调用操作符 . 结构体.成员名(点操作符) -> 结构体指针->成员名
表达式求值
- 表达式求值的顺序一部分是由操作符的优先级和结合性决定。
- 同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
隐式类型转换
- C的整型算术运算总是至少以缺省整型类型的精度来进行的。
- 为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型 提升。
//实例1 char a,b,c; ... a = b + c;//b和c的值被提升为普通整型,然后再执行加法运算。
算数转换
- 如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
float f = 3.14; int num = f;隐式转换,会有精度丢失。
操作符的属性
复杂表达式的求值有三个影响的因素。(按照以下顺序依次判断)
- 先判断操作符的优先级
- 再判断操作符的结合性
- 是否控制求值顺序。
- 总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
- 注.本图参考百度
总结
- 以上是函数,数组,操作符的详细内容,建议收藏反复复习哦。
- 最后,如果你觉得我的文章对你有帮助🎉欢迎关注🔎点赞👍收藏⭐️留言📝。