目录
前言
在数学的世界里,我们早已邂逅了函数这一概念,例如那条简洁的直线方程 y=kx+b,其中 k 和 b 是常数。只要你给出一个 x 值,它就能准确无误地返回对应的 y 值。这一概念在 C 语言的编程领域同样适用,函数(有时也被称作子程序)在这里扮演着至关重要的角色。在 C 语言中,函数是一段执行特定任务的代码,它遵循特定的编写规则和调用方式。
想象一下,一个复杂的软件程序,就像一座由无数块乐高积木搭建的城堡。每一块积木,就好比一个函数,负责完成一个具体的小任务。通过这种方式,庞大的计算任务被巧妙地分解成一系列较小、更易于管理的函数,每个函数都专注于自己的职责。这种模块化的方法不仅让程序更加清晰,也极大地提高了开发效率,因为一旦编写完成,这些函数就可以在不同的程序中重复使用。
在 C 语言的浩瀚星河中,我们主要会遇到两类函数:
-
库函数:这些是编程世界中已经预先构建好的“工具箱”,由经验丰富的开发者们精心打造,供我们直接使用。它们就像是我们编程工具箱中的瑞士军刀,包含了各种常用的功能,如输入输出、数学计算等。
-
自定义函数:这些则是我们根据特定需求亲自编写的函数。它们就像是我们为解决特定问题而定制的工具,提供了更高的灵活性和针对性。
标准库:C语言的百宝箱
C语言本身定义了一套严谨的语法规则,但它并不直接提供具体的功能实现——这便是标准库大显身手的地方。ANSI C国际标准为常用的功能设定了标准,而各编译器厂商则根据这些标准,提供了丰富的函数实现,这些预定义的函数就是我们所说的库函数。
想象一下,
printf
和scanf
这些我们耳熟能详的函数,就像是程序员的工具箱中现成的锤子和锯子,我们无需从零开始打造这些工具,只需了解如何使用它们,就能直接投入到编程的创作中。这不仅极大地提升了开发效率,也因为这些库函数经过了严格的测试和优化,其质量和执行效率都得到了保证。头文件:库函数的藏宝图
在C语言的宝库中,库函数被精心分类并存储在不同的头文件中,每个头文件都像是一张藏宝图,指引我们找到所需的函数和类型定义。无论是处理数学运算的
<math.h>
,还是操作字符串的<string.h>
,或是处理日期和时间的<time.h>
,这些头文件中都包含了丰富的函数和类型声明。点击这里,你将找到一份完整的C标准库头文件列表,它涵盖了从基础数学运算到复杂的多线程处理,无所不包。
学习库函数不必急于一时,我们可以一步步来,逐个掌握。随着对这些头文件的深入了解,你会发现,它们不仅是代码的组成部分,更是提升编程技艺的宝贵资源。
- C/C++官方文档:提供了全面的库函数参考,是学习和查阅库函数的权威资源。
- C标准库头文件
- cplusplus.com:提供了C语言库的详细描述和示例,适合快速查找和学习。
- C library - C++ Reference
实例分析:计算平方根的sqrt
函数
让我们以sqrt
函数为例,探讨如何使用库函数。sqrt
函数用于计算一个数的平方根,它定义在<math.h>
头文件中。
#include <stdio.h>
#include <math.h>
int main() {
double d = 16.0; // 待计算的数
double r = sqrt(d); // 调用sqrt函数计算平方根
printf("The square root of %lf is %lf\n", d, r);
return 0;
}
在这个例子中:
sqrt
是函数名,表示“平方根”。d
是函数的参数,表示调用sqrt
函数时需要传递的值,这里是一个double
类型的值。
功能描述
sqrt
函数的主要功能是计算一个数的平方根。它接受一个double
类型的参数,并返回该参数的平方根。
头文件包含的重要性
使用库函数时,必须包含对应的头文件。这是因为库函数的声明和相关类型定义都在这些头文件中。例如,要使用
sqrt
函数,你需要在代码中包含<math.h>
头文件。如果不包含,编译器将无法识别该函数,可能会导致编译错误。
库函数文档的一般格式
1. 函数原型 2. 函数功能介绍 3. 参数和返回类型说明 4. 代码举例 5. 代码输出 6. 相关知识链接
自定义函数:释放你的编程创造力
在掌握了库函数的使用之后,我们将注意力转向自定义函数——这是编程中更具创造性的部分。自定义函数不仅重要,而且它们让程序员能够根据自己的需求设计和实现特定的功能。
函数的语法形式
ret_type fun_name(parameter_list) {
// 函数体
}
ret_type
:指定函数返回值的数据类型。如果函数不返回任何值,可以使用void
。fun_name
:函数的名称,它应该反映函数的功能,以便于理解和使用。parameter_list
:函数的参数列表,用于传递输入值给函数。参数可以有零个或多个,每个参数都需要指明数据类型和名称。- 花括号
{}
内的部分称为函数体,这里包含了完成特定任务的代码。
函数的比喻
我们可以将函数比作一个小型的加工厂,工厂需要输入原材料,经过加工后产出产品。类似地,函数通常接收一些输入值(参数),在函数体内进行处理,最终产生输出(返回值)。
函数的举例
让我们通过一个简单的例子来演示如何编写一个自定义函数。假设我们需要编写一个加法函数,用于计算两个整型变量的和。
#include <stdio.h>
// 自定义加法函数
int Add(int x, int y) {
int z = 0;
z = x+y;
return z; // 直接返回计算结果
}
int main() {
int a, b;
// 输入
printf("Enter two integers: ");
scanf("%d %d", &a, &b);
// 调用加法函数并输出结果
int sum = Add(a, b);
printf("The sum is: %d\n", sum);
return 0;
}
在这个例子中,我们定义了一个名为
Add
的函数,它接收两个整型参数x
和y
,并返回它们的和。在main
函数中,我们从用户那里获取两个整数,调用Add
函数计算它们的和,并将结果打印出来。
简化函数定义
实际上,Add
函数可以进一步简化,直接在函数体内返回计算结果
int Add(int x, int y) {
return x + y;
}
函数的参数部分需要交代清楚:参数个数,每个参数的类型是啥,形参的名字叫啥。上⾯只是⼀个例⼦,未来我们是根据实际需要来设计函数,函数名、参数、返回类型都是可以灵活变化的。
形参和实参
在函数使⽤的过程中,把函数的参数分为,实参和形参。形参
在上⾯代码中,第2⾏定义函数的时候,在函数名 Add 后的括号中写的 x 和 y ,称为形式参数,简 称形参。
为什么叫形式参数呢?实际上,如果只是定义了 Add 函数,⽽不去调⽤的话, Add 函数的参数 x 和 y 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在函数被调⽤的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形式的实例化。 实参和形参的关系 虽然我们提到了实参是传递给形参的,他们之间是有联系的,但是形参和实参各⾃是独⽴的内存空 间。 这个现象是可以通过调试来观察的。请看下⾯的代码和调试演⽰#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x+y;
return z;
}
int main()
{
int a = 0;
int b = 0;
//输⼊
scanf("%d %d", &a, &b);
//调⽤加法函数,完成a和b的相加
//求和的结果放在r中
int r = Add(a, b);
//输出
printf("%d\n", r);
return 0;
}
实参
在上⾯代码中,第2~7⾏是 Add 函数的定义,有了函数后,再第17⾏调⽤Add函数的。 我们把第17⾏调⽤Add函数时,传递给函数的参数a和b,称为实际参数,简称实参。 实际参数就是真实传递给函数的参数。 形参 在上⾯代码中,第2⾏定义函数的时候,在函数名 Add 后的括号中写的 x 和 y ,称为形式参数,简 称形参。 为什么叫形式参数呢?实际上,如果只是定义了 Add 函数,⽽不去调⽤的话, Add 函数的参数 x 和 y 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在 函数被调⽤的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形式的实例化。实参和形参的关系
虽然我们提到了实参是传递给形参的,他们之间是有联系的,但是形参和实参各⾃是独⽴的内存空 间。 这个现象是可以通过调试来观察的。请看下⾯的代码和调试演⽰:#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 0;
int b = 0;
//输⼊
scanf("%d %d", &a, &b);
//调⽤加法函数,完成a和b的相加
//求和的结果放在r中
int r = Add(a, b);
//输出
printf("%d\n", r);
return 0;
}
我们在调试的可以观察到,x和y确实得到了a和b的值,但是x和y的地址和a和b的地址是不⼀样的,所以我们可以理解为形参是实参的⼀份临时拷⻉。
return 语句
在函数的设计中,函数中经常会出现return语句,这⾥讲⼀下return语句使⽤的注意事项。
- return后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执⾏表达式,再返回表达式的结果。
- return后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是void的情况。
- return返回的值和函数返回类型不⼀致,系统会⾃动将返回的值隐式转换为函数的返回类型。
- return语句执⾏后,函数就彻底返回,后边的代码不再执⾏。
- 如果函数中存在if等分⽀的语句,则要保证每种情况下都有return返回,否则会出现编译错误。
数组做函数参数
在使⽤函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进⾏操作。 ⽐如:写⼀个函数对将⼀个整型数组的内容,全部置为-1,再写⼀个函数打印数组的内容。 简单思考⼀下,基本的形式应该是这样的:#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
set_arr();//设置数组内容为-1
print_arr();//打印数组内容
return 0;
}
这⾥的set_arr函数要能够对数组内容进⾏设置,就得把数组作为参数传递给函数,同时函数内部在设置数组每个元素的时候,也得遍历数组,需要知道数组的元素个数。所以我们需要给set_arr传递2个参数,⼀个是数组,另外⼀个是数组的元素个数。仔细分析print_arr也是⼀样的,只有拿到了数组和元素个数,才能遍历打印数组的每个元素。
#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int sz = sizeof(arr)/sizeof(arr[0]);
set_arr(arr, sz);//设置数组内容为-1
print_arr(arr, sz);//打印数组内容
return 0;
}
数组作为参数传递给了set_arr 和 print_arr 函数了,那这两个函数应该如何设计呢?
这⾥我们需要知道数组传参的⼏个重点知识:
- 函数的形式参数要和函数的实参个数匹配
- 函数的实参是数组,形参也是可以写成数组形式的
- 形参如果是⼀维数组,数组⼤⼩可以省略不写
- 形参如果是⼆维数组,⾏可以省略,但是列不能省略
- 数组传参,形参是不会创建新的数组的
- 形参操作的数组和实参的数组是同⼀个数组
嵌套调⽤和链式访问
嵌套调⽤
嵌套调⽤就是函数之间的互相调⽤,每个函数就⾏⼀个乐⾼零件,正是因为多个乐⾼的零件互相⽆缝的配合才能搭建出精美的乐⾼玩具,也正是因为函数之间有效的互相调⽤,最后写出来了相对⼤型的程序。 假设我们计算某年某⽉有多少天?如果要函数实现,可以设计2个函数: is_leap_year():根据年份确定是否是闰年 get_days_of_month():调⽤is_leap_year确定是否是闰年后,再根据⽉计算这个⽉的天数int is_leap_year(int y)
{
if(((y%4==0)&&(y%100!=0))||(y%400==0))
return 1;
else
return 0;
}
int get_days_of_month(int y, int m)
{
int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int day = days[m];
if (is_leap_year(y) && m == 2)
day += 1;
return day;
}
int main()
{
int y = 0;
int m = 0;
scanf("%d %d", &y, &m);
int d = get_days_of_month(y, m);
printf("%d\n", d);
return 0;
}
这⼀段代码,完成了⼀个独⽴的功能。代码中反应了不少的函数调⽤:
main
函数调⽤
scanf
、
printf
、
get_days_of_month
get_days_of_month
函数调⽤
is_leap_yea
未来的稍微⼤⼀些代码都是函数之间的嵌套调⽤,但是函数是不能嵌套定义的。
链式访问
所谓链式访问就是将⼀个函数的返回值作为另外⼀个函数的参数,像链条⼀样将函数串起来就是函数的链式访问。 ⽐如:#include <stdio.h>
int main()
{
int len = strlen("abcdef");//1.strlen求⼀个字符串的⻓度
printf("%d\n", len);//2.打印⻓度
return 0;
}
前⾯的代码完成动作写了2条语句,把如果把strlen的返回值直接作为printf函数的参数呢?这样就是⼀个链式访问的例⼦了。
#include <stdio.h>
int main()
{
printf("%d\n", strlen("abcdef"));//链式访问
return 0;
}
在看⼀个有趣的代码,下⾯代码执⾏的结果是什么呢?
#include <stdio.h>
2 int main()
3 {
4 printf("%d", printf("%d", printf("%d", 43)));
5 return 0;
6 }
这个代码的关键是明⽩printf
函数的返回是啥?
int printf ( const char * format, ... );
printf函数返回的是打印在屏幕上的字符的个数。
上⾯的例⼦中,我们就第⼀个printf打印的是第⼆个printf的返回值,第⼆个printf打印的是第三个
printf的返回值。
第三个printf打印43,在屏幕上打印2个字符,再返回2
第⼆个printf打印2,在屏幕上打印1个字符,再放回1
第⼀个printf打印1
所以屏幕上最终打印:4321
函数的声明和定义
单个⽂件
⼀般我们在使⽤函数的时候,直接将函数写出来就使⽤了。 ⽐如:我们要写⼀个函数判断⼀年是否是闰年。#include <stido.h>
//判断⼀年是不是闰年
int is_leap_year(int y)
{
if(((y%4==0)&&(y%100!=0)) || (y%400==0))
return 1;
else
return 0;
}
int main()
{
int y = 0;
scanf("%d", &y);
int r = is_leap_year(y);
if(r == 1)
printf("闰年\n");
else
printf("⾮闰年\n");
return 0;
}
上⾯代码中橙⾊的部分是函数的定义,绿⾊的部分是函数的调⽤。
这种场景下是函数的定义在函数调⽤之前,没啥问题。
那如果我们将函数的定义放在函数的调⽤后边,如下:
#include <stido.h>
int main()
{
int y = 0;
scanf("%d", &y);
int r = is_leap_year(y);
if(r == 1)
printf("闰年\n");
else
printf("⾮闰年\n");
return 0;
}
//判断⼀年是不是闰年
int is_leap_year(int y)
{
if(((y%4==0)&&(y%100!=0)) || (y%400==0))
return 1;
else
return 0;
}
这个代码在VS2022上编译,会出现下⾯的警告信息:
这是因为C语⾔编译器对源代码进⾏编译的时候,从第⼀⾏往下扫描的,当遇到第7⾏is_leap_year
函数调⽤的时候,并没有发现前⾯有is_leap_year的定义,就报出了上述的警告。
把怎么解决这个问题呢?就是函数调⽤之前先声明⼀下is_leap_year这个函数,声明函数只要交代清楚:函数名,函数的返回类型和函数的参数。
如:int is_leap_year(int y);这就是函数声明,函数声明中参数只保留类型,省略掉名字也是可以
的。代码变成这样就能正常编译了。
#include <stido.h>
int is_leap_year(int y);//函数声明
int main()
{
int y = 0;
scanf("%d", &y);
int r = is_leap_year(y);
if(r == 1)
printf("闰年\n");
else
printf("⾮闰年\n");
return 0;
}
//判断⼀年是不是闰年
int is_leap_year(int y)
{
if(((y%4==0)&&(y%100!=0)) || (y%400==0))
return 1;
else
return 0;
}
函数的调⽤⼀定要满意,先声明后使⽤;
函数的定义也是⼀种特殊的声明,所以如果函数定义放在调⽤之前也是可以的。
多个文件
⼀般在企业中我们写代码时候,代码可能⽐较多,不会将所有的代码都放在⼀个⽂件中;我们往往会根据程序的功能,将代码拆分放在多个⽂件中。 ⼀般情况下,函数的声明、类型的声明放在头⽂件(.h)中,函数的实现是放在源⽂件(.c)⽂件中。 如下: add.c//函数的定义
int Add1(int x, int y)
{
return x + y;
}
add1.h
#pragma once
//函数的声明
int Add(int x, int y);
test.c
#include <stdio.h>
#include "add1.h"
int main()
{
int a = 10;
int b = 20;
//函数调⽤
int c = Add1(a, b);
printf("%d\n", c);
return 0;
}
有了函数声明和函数定义的理解,我们写代码就更加⽅便了。
static 和 extern
static 和 extern 都是C语⾔中的关键字。 static 是 静态的 的意思,可以⽤来: 修饰局部变量 修饰全局变量 修饰函数 extern 是⽤来声明外部符号的。 在讲解 static 和 extern 之前再讲⼀下:作⽤域和⽣命周期。 作⽤域(scope)是程序设计概念,通常来说,⼀段程序代码中所⽤到的名字并不总是有效(可⽤)的,⽽限定这个名字的可⽤性的代码范围就是这个名字的作⽤域。 1. 局部变量的作⽤域是变量所在的局部范围。 2. 全局变量的作⽤域是整个⼯程(项⽬)。 ⽣命周期指的是变量的创建(申请内存)到变量的销毁(收回内存)之间的⼀个时间段。 1. 局部变量的⽣命周期是:进⼊作⽤域变量创建,⽣命周期开始,出作⽤域⽣命周期结束。 2. 全局变量的⽣命周期是:整个程序的⽣命周期。static 修饰局部变量:
//代码1
#include <stdio.h>
void test()
{
int i = 0;
i++;
printf("%d ", i);
}
int main()
{
int i = 0;
for(i=0; i<5; i++)
{
test();
}
return 0
}
//代码2
#include <stdio.h>
void test()
{
//static修饰局部变量
static int i = 0;
i++;
printf("%d ", i);
}
int main()
{
int i = 0;
for(i=0; i<5; i++)
{
test();
}
return 0;
}
对⽐代码1和代码2的效果,理解 static 修饰局部变量的意义。 代码1的test函数中的局部变量i是每次进⼊test函数先创建变量(⽣命周期开始)并赋值为0,然后 ++,再打印,出函数的时候变量⽣命周期将要结束(释放内存)。 代码2中,我们从输出结果来看,i的值有累加的效果,其实 test函数中的i创建好后,出函数的时候是不会销毁的,重新进⼊函数也就不会重新创建变量,直接上次累积的数值继续计算。
结论
static修饰局部变量改变了变量的⽣命周期,⽣命周期改变的本质是改变了变量的存储类型,本来⼀个局部变量是存储在内存的栈区的,但是被 static 修饰后存储到了静态区。存储在静态区的变量和全局变量是⼀样的,⽣命周期就和程序的⽣命周期⼀样了,只有程序结束,变量才销毁,内存才回收。但是作⽤域不变的。
使⽤建议:未来⼀个变量出了函数后,我们还想保留值,等下次进⼊函数继续使⽤,就可以使⽤static 修饰。
static 修饰全局变量
代码1 add.c
int g_val = 2018;
test.c
#include <stdio.h>
extern int g_val;
int main()
{
printf("%d\n", g_val);
return 0;
}
代码2
add.c
static int g_val = 2018;
test.c
#include <stdio.h>
extern int g_val;
int main()
{
printf("%d\n", g_val);
return 0;
}
extern 是⽤来声明外部符号的,如果⼀个全局的符号在A⽂件中定义的,在B⽂件中想使⽤,就可以使⽤ extern
进⾏声明,然后使⽤。
代码1正常,代码2在编译的时候会出现链接性错误。
结论: ⼀个全局变量被static修饰,使得这个全局变量只能在本源⽂件内使⽤,不能在其他源⽂件内使⽤。 本质原因是全局变量默认是具有外部链接属性的,在外部的⽂件中想使⽤,只要适当的声明就可以使⽤;但是全局变量被 static 修饰之后,外部链接属性就变成了内部链接属性,只能在⾃⼰所在的源⽂件内部使⽤了,其他源⽂件,即使声明了,也是⽆法正常使⽤的。 使⽤建议:如果⼀个全局变量,只想在所在的源⽂件内部使⽤,不想被其他⽂件发现,就可以使⽤static修饰。
static 修饰函数
代码1
add.c
int Add(int x, int y)
{
return x+y;
}
test.c
#include <stdio.h>
extern int Add(int x, int y);
int main()
{
printf("%d\n", Add(2, 3));
return 0;
}
代码2
add.c
static int Add(int x, int y)
{
return x+y;
}
test.c
#include <stdio.h>
extern int Add(int x, int y);
int main()
{
printf("%d\n", Add(2, 3));
return 0;
}
代码1是能够正常运⾏的,但是代码2就出现了链接错误。 修饰全局变量是⼀模⼀样的,⼀个函数在整个⼯程都可以使⽤,被static修饰后,只能在本⽂件内部使⽤,其他⽂件⽆法正常的链接使⽤了。 本质是因为函数默认是具有外部链接属性,具有外部链接属性,使得函数在整个⼯程中只要适当的声明就可以被使⽤。但是被 static 修饰后变成了内部链接属性,使得函数只能在⾃⼰所在源⽂件内部使⽤。 使⽤建议:⼀个函数只想在所在的源⽂件内部使⽤,不想被其他源⽂件使⽤,就可以使⽤ static 修饰。标签:return,函数,int,代码,C语言,Add,printf,基石 From: https://blog.csdn.net/2201_76027234/article/details/143094225