C语言学习笔记
函数
函数的概念及意义
- 函数也叫做模块化编程,是用于完成特定任务的程序代码单元,就是把一个小功能封装成一个独立的代码段。封装前与封装后的执行结果是完全一样的。
- 函数的作用:1、增加了代码的复用性(可被重复使用)
2、增加了代码的可读性,便于其他人读懂,也便于快速找到问题所在,便于修改维护。 - 使用注意点:通常会把一个功能封装成一个函数,是专门某一功能的。
- 函数的封装无关于代码量。
- 在程序的执行过程中,从main开始,顺序向下执行,如果遇到了函数,则先执行完函数内的所有内容,接着返回,继续在主函数内向下执行。经过这两步跳转,会降低一点效率(微乎其微)。
函数的类型
- 简单分为四类:无参数无返回值,无参数有返回值,有参数无返回值,有参数有返回值。
无参数无返回值的函数
- 简单小程序:
#include <stdio.h>
void fun(void)
{
printf ("我是函数\n");
}
int main()
{
printf ("我是函数\n");
fun();//函数的调用
return 0;
}
这样的程序就可以展示出无参数无返回值的具体作用了,但是要注意,第二行后面一定不要有分号,像主函数后面不带分号一样,其次,调用时要加上小括号,不过无须写入任何其他的数据。
- 函数的定义包括了函数头和函数体,**返回值 函数名(参数列表)**的写法,但是如果在返回值的地方写上了
void
,表示函数没有返回值,或者我们不使用返回值,但是如果我们最后加上了return
,编译器就会报错。返回值在C中可以不写,默认是int
但是C++会报错,C++不支持默认的int
。
函数头
- 函数名:名字就是标识符,相当于
int a;
中的a,这是个变量名。其实函数的本质也是个变量,只不过是比我们基本数据类型的变量复杂一点。 - 函数名注意点:
1、起始必须是英文字母或者下划线,后面的是以数字,字母或者下划线任意组成,但是不能以数字开头。
2、不要用与系统重名的函数,比如printf
,会报错“printf重定义”
3、函数的名字尽量把函数的功能体现出来,原则是将函数功能描述的越详细越好,可以使用尽量多的关键词。长度没有限制,但是描述的清晰程度直接影响了代码的可读性,尽量不要简写,让别人也能读懂。
4、规范写法,其实就是为了增加可读性。 - 函数的参数列表:标准规定,如果函数没有参数,一定要加上void,跟主函数一样。如果不写表示参数个数不确定,写上表示不接受任何参数。但是C++没有这层意思,C++中不写==void。
函数体
- 函数实体,也就是代码部分,跟主函数一样,没有特殊的,但是要记得花括号
- 自定义函数与主函数:主函数是函数,自定义函数也是函数,没有区别,但是主函数是由操作系统调用的。
- 自定义函数与系统函数:
printf``scanf
等等函数都叫系统函数,是微软提前定义好了的,我们可以直接使用,所以就起了个系统函数的名字,这些函数本质上和我们自定义的函数是一样的。 - 形式:
void fun(void){printf("zxczxc")}
这是对于代码非常少的函数,更加简洁明了。这里有一个误区:行数少的函数并不意味着执行的快。
函数调用
- 调用形式:函数名+(),小括号内是装参数的,所以没有参数就不用写,返回值的void也不要加(加上了之后是函数声明)。
- 函数调用的本质是函数地址+(参数列表),函数名字就是函数地址,对函数名字&即&fun也是函数地址,但是其实
fun==&fun
这一点非常重要(可以在编译器上写上这一行代码,如果级别不一样编译器就会报错),而且还要分辨fun
是函数地址,fun()
和(&fun)()
都是函数调用。注意不要自己调用自己
函数声明/函数原型
- 使用一个变量前,一定要定义,或者声明这个变量,同样的,在函数调用前,一定要出现函数定义,或者声明。
- 声明一个函数仅需要在主函数前或者主函数内部写
void fun(void);
要加上分号,表示仅声明了函数没有定义,写法还是返回值+函数名(参数列表)。 - 如果声明函数且调用但没有定义的话,编译器会报错
- 函数声明可以多次,不会对程序造成影响,但是定义只能有一次,否则也会报错。
无参数有返回值的函数
- 写法:
int fun(void)
{
printf("我是函数fun\n");
return 4;
}
- void是无返回值,有具体类型的就是有返回值
- 这里
int
的意义:函数的返回值类型/函数类型,我们可以说,函数就可以理解成一个复杂点的变量,当有返回值时,该函数就会有一个值,int a = 2;
a的值就是2,函数的值由return
的值决定,那这个函数的值就是4,也就是说int b = fun()
,b的值就是4,不加return 4
编译器会报错。 - 但是在上述过程中,一定要明确一点,只要出现了fun(),编译器就认为调用了一次该函数。因此
int a = 2;
的输出结果是:我是函数fun
4
- 在实际应用中,就可以完完全全的把他当作4来理解和使用,比如参与运算。返回值不一定必须要被使用,也就是说,他比无返回值的函数功能更强,但是不一定要被体现。有返回值的函数是一个表达式,表达式就会有一个结果,结果就是return的值,类型由返回值类型决定。
return详解
return 数据
:用于有返回值的函数,终止所在函数的执行,并返回指定数据。也就是说,当函数进行到return时,函数就结束了,不再执行下面的代码,直接跳到花括号。可以有多个return,但是只执行代码逻辑顺序中的第一个,比如说用到了if,goto等等,要根据逻辑来判断。而且所有逻辑都一定要有返回值。return;
用于无返回值的函数中,终止函数的执行。- 返回多个值:首先明确概念:return一个只能返回一个值,可以返回一段连续空间的首地址,这段空间里装着多个值,也就是返回一个数组的首地址。
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
int *fun(void)
{
int *p = (int*)malloc(8);//动态内存分配
*p = 4;
p[1] = 5;
return p;
}
int main(void)
{
int *a = fun();
printf("%d,%d\n",a[0],a[1]);
free(a);
return 0;
}
- 这样的代码块就是return返回多个值的经典实例,输出结果是4,5。
- 为什么不返回栈区空间,因为它是局部变量、空间,会出问题。
有参数无返回值函数
- 函数定义:
void fun(int a,double b){printf("我是fun%d,%lf\n",a,b)}
- 返回值是void,即无返回值。
- 参数列表:作用:函数与外界交互的接口,将函数外部的数据传输到函数内,也能将函数内部数据传递出去。
写法:连续定义几个变量,用逗号隔开,注意在定义时,个数由需求所定;不要用成分号;变量不要初始化(C++有初始化的语法,但是c语言没有);名字不能冲突 - 函数调用:类型要对应上、个数对应上、顺序对应上。函数头的参数列表是形参(形式参数)、函数调用处是实参(实际参数)。
- 函数原型:
void fun(int a,int b);
函数头加个分号,其他注意点和上述一模一样,这也是形参;或者void fun(int , double );
这两个在函数的声明是作用是完全一样的。注意每个参数都要有类型,void fun(int a,b);
的声明是非法的。 - 通过参数把数据传递到外部(通过函数修改外部数据的值):首先声明,直接在函数内部修改形参的数据,无法改变实参的值。正确的操作方式是:将实参的地址传递到函数内部,再在函数中操作实参的地址的空间,这样就可以实现改变实参的值。
#include <stdio.h>
void fun(int *p)
{
printf("b = %d\n",*p);
*p = 12;
printf("b = %d\n",*p);
}
int main (void)
{
int a = 2; //int *p = &a;
printf("a = %d\n",a);
fun(&a);
printf("a = %d\n",a);
return 0;
}
上述程序的输出结果是
a = 2
b = 2
b = 12
a = 12
- 也就是说,在进行通过参数修改外部的值时,要注意分辨传值和传址,传值只能将值传递给形参,最终操作的是形参,而传址是通过地址来间接操作a本身,最终修改了a。
通过参数修改指针的指向
- 也就是说怎么通过函数修改指针变量
#include <stdio.h>
void fun1(int* *p)
{
printf("p = %p\n",*p);
*p = NULL;
printf("p = %p\n",*p);
}
int main()
{
int a = 12;
int *p1 = &a;
printf("%p\n",p1);
fun1(&p1);
printf("%p\n",p1);
return 0;
}
总结:要注意修改谁,就要传谁的地址,并不是说传个地址就是传值。
一维数组做参数
- 首先声明一点:
void fun(int a[10]) == void fun *a
这是因为编译器会自动将[]当成*处理,并且忽略数字10 - 所以更加合适的将一维数组做参数的做法是:
void fun(int *p , int nlength)
也就是将数组名和数组长度都要传递到函数中,这样在函数中可以更方便地遍历。
#include <stdio.h>
void fun (int *p , int nlength)
{
int i = 0;
for (i = 0 ; i < nlength ; i++)
{
printf ("%d\n",p[i]);
}
}
int main()
{
int a[5] = {1,2,3,4,5};
int a1[7] = {1,2,3,4,5,6,7};
fun (a,5);
fun (a1,7);
return 0;
}
- 要注意的是在进行函数调用的时候,a是一个指针类型的变量,就是首元素的首地址。之所以不直接将整个数组都传递进来,是因为1、传的是数组名,就是元素类型的地址;2、数组做函数,函数调用就会定义一个很大空间的数组,非常浪费空间;3、数组做参数,数组的大小就固定了,造成了使用上的局限性。
二维数组做参数
- 先复习怎么定义一个一维数组指针
int a[2][3] = {{1,2,3},{4,5,6}};
int (*p)[3] = a;
- 这样就是传递了一个一维数组的地址,将其完全的复制到函数头做参数就可以实现。
#include <stdio.h>
// int p[2][3]和下面的效果完全一样,甚至可以直接用*p来遍历
void fun(int (*p)[3], int hang,int lie)
{
int i,j;
for (i = 0 ; i < hang ; i++)
{
for (j = 0 ; j < lie ; j++)
{
printf ("%d\n",p[i][j]);
}
}
}
int main()
{
int a[2][3] = {{1,2,3},{4,5,6}};
//int (*p) = a;
fun (a,2,3);
return 0;
}
- 如果用二维数组指针来装的的话可以使用下面的程序
#include <stdio.h>
void fun(int (*p)[2][3] , int hang , int lie)
{
int i,j;
for (i = 0 ; i < hang ; i++)
{
for (j = 0 ; j < lie ; j++)
{
printf("%d\n",(*p)[i][j]);
}
}
}
int main()
{
int a[2][3] = {{1,2,3},{4,5,6}};
fun (&a,2,3);
return 0;
}
有参数有返回值的函数
- 可以参照前面的介绍,只是加上了返回值而已,如以下的程序找到数组内的最大值:
#include <stdio.h>
int Findmax (int *p , int n , int *p1)
{
int i = 0;
int maxnum = p[0];
for (i = 1 ; i < n ; i++)
{
if (maxnum < p[i])
{
maxnum = p[i];
}
}
*p1 = maxnum;
return maxnum;
}
int main()
{
int a[6] = {12,34,32,45,32,21};
int n = 0;
printf("%d\n",Findmax(a,6,&n));
n = Findmax(a,6);
printf("%d\n",n);
printf("%d\n",n);
return 0;
}
### 函数地址
- fun == &fun
- **定义一个指针指向函数的地址方法**:首先把函数名去掉,再在相应位置上补充`(*p)`,然后删去变量名(这是因为有些编译器会认为他没用提出警告,可以忽略),再将函数的地址赋值给他,也就是说`int (*p)(int) = fun`
- 如何通过指针来进行函数调用:函数地址(函数名)+小括号,括号内写参数,无参数就什么都不用写。
----------
#### 本周总结
本周主要学习了函数的内容,这一段学习过程还是比较轻松的,但是也暴露出了很多问题,前面的指针,数组内容以及它们之间的结合在函数这一块还是比较难理解的。美中不足的是本周其实也并没有完全完成制定的学习目标,递归函数还没有开始学习。
<br/>下周希望能将函数的内容全部学完,并且字符的内容学完。