数组和函数、程序的编译和链接

张开发
2026/4/10 6:01:12 15 分钟阅读

分享文章

数组和函数、程序的编译和链接
数组什么是数组呢c语言中我们可以把数组理解为一组相同类型元素的集合我们把数组分为一维数组和二维数组。一维数组可以想象成一条直线直线中的线段里储存了数据二维数组可以想象成一个面面里有很多小格子小格子里储存了数据一维数组数组的创建type arr_name[常量值];上图中type为数组的类型你可以设置为int、char、float等等arr_name为数组的名字可以任意定义[]中的常量值为数组的大小可以通过设定常量值决定数组元素的个数比如我们想要储存20个人的英语成绩就可以创建一个数组int English[20]这样我们就完成了数组的创建数组的初始化有时候在创建数组的时候需要给定一个初始值这就叫数组的初始化初始化需要在[]后面加上{}int English[5]{96,85,62,88,99};//完全初始化 char arr[3]{a};//不完全初始化 double math[2]{99.99,88.88,66.66};//错误的初始化初始化项太多数组下标c语言中数组是有下标的下标从0开始到n-1n是数组元素的个数数组下标是数组元素的编号如下图数组元素的打印我们现在知道了数组怎么创建、怎么初始化也知道了数组下标的概念那么当我们想打印数组时该怎么操作呢#include stdio.h int main() { int arr[6] { 1,2,3,4,5,6 }; printf(%d, arr[6]); return 0; }你会不会想要这样打印来让我们执行以下试一试我们发现打印出来了一串乱码这是怎么回事呢当我们打印arr[6]的时候并不是打印整个数组而是打印了数组下标为6的元素我们知道数组下标是从0开始计算的“6”元素的下标是5也就是说这个数组最大的下标是5我们打印arr[6]就溢出了如果想要打印数组中“6”这个元素可以这样操作把下标号放在[]里这样就可以打印单个元素了那么如果我们想打印所有元素要怎么操作呢这时候我们就需要使用for循环如果你对循环不太了解可以点此跳转for循环讲解我们用for循环逐个打印数组元素当i0时打印arr[0]也就是打印0下标对应的元素“1”当i1时打印arr[1]也就是打印1下标对应的元素“2”以此类推到这里恭喜你学会了一维数组元素的打印给自己个掌声数组的输入数组怎么输入呢其实和数组打印原理是一样的需要使用for循环挨个读入数组元素#define _CRT_SECURE_NO_WARNINGS #include stdio.h int main() { int arr[6] { 1,2,3,4,5,6 }; for (int i 0; i 5; i) { scanf(%d, arr[i]); } for (int i 0; i 5; i) { printf(%d , arr[i]); } return 0; }输入9 8 7 6 5 4打印9 8 7 6 5 4这样我们就可以通过使用数组下标来输入数组元素如果在开头给数组元素初始化了也可以通过scanf修改数组元素二维数组当我们把一维数组作为数组的元素这个数组就叫做二维数组二维数组的创建创建二维数组的语法如下type arr_name[常量1][常量2] int arr[2][3]二维数组比一维数组多了一个[]上面代码的arr数组中第一个[]中的2表示数组有两行第二个[]中的3表示每一行有3个元素二维数组的初始化上图中有三种初始化方式第一种完全初始化第二种不完全初始化用{}限定两个元素为一行第三种第一个[]中的常量值省略系统会根据数组元素的个数自动分配数组行数第二个[]绝对不能省略二维数组的打印当我们想打印单个二维数组可以通过两个[]中的常量值定位比如注意数组行的下标是从0开始的列也是从0开始的这里的意思是打印行下标为1第二行列下标为2第三列的元素当我们想打印二维数组的所有元素可以使用for循环的嵌套#define _CRT_SECURE_NO_WARNINGS #include stdio.h int main() { int arr[2][3] {1,2,3,4,5,6}; for (int i 0; i 1; i) { for (int j 0; j 2; j) { printf(%d , arr[i][j]); } } return 0; }恭喜你到这里你已经基本了解了数组的相关知识休息一下然后开始函数的学习函数函数的形式在c语言中什么是函数呢我们可以这么理解c语言中的函数就是完成某一项任务的一小段代码。函数可以分为库函数和自定义函数库函数就是我们所熟知的“printf”“scanf”等等他们需要调用对应的头文件比如 #include stdio.h这里我们主要了解自定义函数自定义函数的形式如下return_type fuction_name形式参数 { 函数体 }return_type是函数返回值的类型有时候也可以使用void表示什么都不返回fuction_name是函数的名称方便后续调用函数函数的参数就相当于工厂中送进去的原材料函数的参数也可以是 void 明确表示函数没有参 数。如果有参数要交代清楚参数的类型和名字以及参数个数函数体用来完成计算过程比如这里是一个加法函数函数名叫Add函数返回值类型为整型Add需要接收两个整型类型的参数函数内部计算出结果z后将z作为返回值返回给调用Add的语句最终赋值给变量c#include stdio.h int Add(int x, int y)//加法函数 { int z 0; z x y; return z;//z返回值给Add } int main() { int a, b; scanf(%d %d, a, b); int c Add(a, b);//调用Add函数 printf(%d %d %d, a, b, c); return 0; }再比如下图 1我们定义了一个返回值类型为void、无参数的函数print这个函数仅执行函数体中的操作无需返回数值当调用print()时会在控制台打印出 “haha”。#include stdio.h void print(void) { printf(haha); } int main() { print(); return 0; }这里有一个小细节需要注意图1中print函数的参数是void表示没有参数但图2中print函数没有明确规定有没有参数这就会出现一个问题因为图1中的print明确规定了没有参数所以当我们想调用print1时把参数1传给print系统会报错但图2中的print没有明确规定有没有参数所以当我们想调用print1时也会照常打印haha我们可以借助这个例子更深入的理解一下函数的参数实参和形参回到我们刚才写的加法函数这里我们把真实传递给函数的参数叫做实际参数也就是a和b把Add后面的x和y叫做形式参数为什么它叫形式参数呢如果我们只定义Add函数而不调用那么x和y就只是在形式上存在的不会向内存申请空间不会真实存在所以叫做形式参数。形式参数只有在函数被调用的过程中为了存放实参传递过来的值才会向内存申请空间这个过程叫做形参的实例化怎么样是不是一下就理解通透了#include stdio.h int Add(int x, int y)//形参 { int z 0; z x y; return z; } int main() { int a, b; scanf(%d %d, a, b); int c Add(a, b);//实参 printf(%d %d %d, a, b, c); return 0; }注意实参和形参是两个完全不同的内存空间形参是实参的一份临时拷贝形参的修改不会影响实参return语句1、return后边可以是⼀个数值也可以是⼀个表达式如果是表达式则先执行表达式再返回表达式的结果2、return后边也可以什么都没有直接写return;这种写法适合函数返回类型是void的情况3、return返回的值和函数返回类型不⼀致系统会自动将返回的值隐式转换为函数的返回类型4、return语句执行后函数就彻底返回后边的代码不再执行5、如果函数中存在if等分支的语句则要保证每种情况下都有return返回否则会出现编译错误比如这个代码没有考虑n%20的情况编译器会报警告#include stdio.h int test2(int n) { if (n % 2 1) return 1; } int main() { int x; scanf(%d, x); int i test2(x); return 0; }数组做函数参数例写一个函数将arr数组的内容全部设置为-1再写一个函数将arr数组的内容全部打印出来//写一个函数将arr数组的内容全部设置为-1 //再写一个函数将arr数组的内容全部打印出来 #include stdio.h void set_arr(int arr1[10], int size1) { int i 0; for (i; i size1; i) { arr1[i] -1; } return; } void print_arr(int arr[10],int size) { for (int i 0; i size; i) { printf(%d , arr[i]); } return; } int main() { int arr[10] { 0 }; int size sizeof(arr) / sizeof(arr[0]); set_arr(arr, size); print_arr(arr, size); return 0; }嵌套调用嵌套调用就是函数之间的相互调用举个例子假设我们现在要计算某年某月有多少天如果用函数实现我们可以设计两个函数is_leap_year()根据年份确定是否是闰年get_days_of_month()调用is_leap_year确定是否是闰年后再根据月计算这个月的天数#include stdio.h #include stdBool.h//_Bool类型头文件 _Bool is_leap_year(int year)//判断是否闰年 { if ((year % 4 0 year % 100 ! 0) || year % 400 0) return true; else return false; } int get_day_of_month(int year, int month)//判断月份的天数 { int days[] { 0,30,28,31,30,31,30,31,31,30,31,30,31 }; //月份对应下标 0 1 2 3 4 5 6 7 8 9 10 11 12 int day days[month]; if (is_leap_year(year) month 2)//如果是闰年二月day1 day 1; return day; } int main()//主函数 { int year; int month; printf(请输入年份和月份(用空格隔开)系统反馈所属天数); scanf(%d %d, year, month); int day get_day_of_month(year, month); printf(%d, day); return 0; }这段代码中main函数调⽤get_days_of_monthget_days_of_month 函数调用is_leap_year链式访问链式访问就是将⼀个函数的返回值作为另外⼀个函数的参数像链条⼀样将函数串起来就是函数的链式访问。比如下面这条代码就是先得出strlen的返回值“6”再由printf打印“6”#include stdio.h #include string.h int main() { printf(%d, strlen(abcdef)); return 0; }函数的声明和定义单个文件一般我们要使用函数的时候直接把函数写出来就是用了比如写一个判断是否闰年的函数如果我们把函数的调用放在函数的定义前面如下#include stdio.h int main() { int i; scanf(%d, i); int j is_leap_year(i); if (j 1) printf(YES); else printf(NO); return 0; } int is_leap_year(int n) { if (n % 4 0 n % 100 ! 0 || n % 400 0) return 1; else return 0; }这时编译器会跳出警告信息这是因为编译器在编译的时候是从上往下编译的当遇到is_leap_year的调用时没有发现is_leap_year的定义就会发出警告要解决这个问题我们可以先声明一下这个函数声明函数的时候需要交代清楚函数名、函数返回类型、函数参数下面就是一个函数的声明int is_leap_year(int n);我们把它加入到代码的开头就可以正常编译了#include stdio.h int is_leap_year(int n); int main() { int i; scanf(%d, i); int j is_leap_year(i); if (j 1) printf(YES); else printf(NO); return 0; } int is_leap_year(int n) { if (n % 4 0 n % 100 ! 0 || n % 400 0) return 1; else return 0; }多个文件使用多个文件可以帮助我们更好的实现多人协作在多个文件中我们把函数的声明放在.h头文件中把函数的实现放在.c源文件中例右键头文件 — 添加 — 新建项;我们把函数声明放在.h文件下把函数定义放在.c文件下这样我们在使用函数前调用一下头文件就可以运行这个加法函数了#include stdio.h #include addd.h int main() { int a, b, c; scanf(%d %d, a, b); c addd(a,b); printf(%d, c); return 0; }static 和 extern我们先了解两个名词作用域和生命周期作用域限定一个名字的可用性的代码范围就是这个名字的作用域1、局部变量的作用域是变量所在的局部范围2、全局变量的作用域是整个工程生命周期变量的创建申请内存到变量的销毁收回内存之间的⼀个时间段1、局部变量的生命周期是进⼊作用域变量创建生命周期开始出作用域生命周期结束。2、全局变量的生命周期是整个程序的生命周期。static 修饰局部变量我们对比一下代码1和代码2代码1test函数中的局部变量 i 是每次进⼊test函数先创建变量生命周期开始并赋值为0然后再打印出函数的时候变量⽣命周期将要结束释放内存。代码2i 的值有累加的效果test函数中的 i 创建好后出函数的时候是不会销毁的重新进⼊函数也就不会重新创建变量直接上次累积的数值继续计算。于是我们可以得出static改变了变量的生命周期改变生命周期的本质是改变了变量的存储类型由原本的栈区改变到了静态区储存在静态区的变量相当于全局变量只有程序结束后才会销毁使用建议未来⼀个变量出了函数后我们还想保留值等下次进入函数继续使用就可以使用static 修饰。static修饰全局变量代码1 代码2test_g_val.c test_g_val.c源.c 源.cextern 是用来声明外部符号的如果⼀个全局的符号在A文件中定义的在B文件中想使用就可以使用 extern进行声明然后使用。代码1正常代码2在编译的时候会出现链接性错误原因是一个全局变量被static修饰会使它只能在本源文件使用不能在其他源文件使用全局变量默认是有外部链接属性的在外部想使用只要适当声明就就可以但是被static修饰后外部链接属性就变成了内部连接属性只能在自己所在的源文件使用了使用建议如果⼀个全局变量只想在所在的源文件内部使用不想被其他文件发现就可以使用static修饰。static修饰函数代码1 代码2test.c test.c源.c 源.c代码1是能够正常运行的但是代码2就出现了错误。其实static修饰函数和static修饰全局变量是⼀模⼀样的⼀个函数在整个工程都可以使用被static修饰后只能在本文件内部使用其他文件无法正常的链接使用了。本质是因为函数默认是具有外部链接属性具有外部链接属性使得函数在整个工程中只要适当的声明就可以被使用。但是被 static修饰后变成了内部链接属性使得函数只能在自己所在源文件内部使用。使用建议⼀个函数只想在所在的源文件内部使用不想被其他源文件使用就可以使用static修饰。程序的编译和链接我们写好的程序会经过 预编译编译汇编链接四个阶段⼀个C语言的项目中可能有多个 .c 文件⼀起构建那多个 .c 文件如何生成可执行程序呢多个.c文件单独经过编译器编译处理生成对应的目标文件。多个目标文件和链接库⼀起经过链接器处理生成最终的可执行程序。链接库是指运行时库(它是支持程序运行的基本函数集合)或者第三方库。预编译预处理阶段主要处理那些源文件中#开始的预编译指令。比如#include,#define处理的规则如下将所有的 #define 删除并展开所有的宏定义。处理所有的条件编译指令如 #if、#ifdef、#elif、#else、#endif 。处理#include 预编译指令将包含的头文件的内容插入到该预编译指令的位置。这个过程是递归进行的也就是说被包含的头文件也可能包含其他文件。删除所有的注释添加行号和文件名标识方便后续编译器生成调试信息等。或保留所有的#pragma的编译器指令编译器后续会使用。#define定义常量define定义常量没有参数 语法为#define name stuff。它就是最基础的“查找并替换”。例子#define MAX 1000。代码里所有的MAX都会被无脑替换成1000。#define定义宏define定义宏有参数 语法为#define name() stuff。它允许把参数替换到文本中 ()功能上有点像一个简易版的函数。例子#define SQUARE(x) (x)*(x)。如果你写SQUARE(5)它会被替换成(5)*(5)。头文件的包含当我们使用#include头文件包含的时候预处理器会先删除这条指令并⽤#include的内容替换本地文件的包含#include filename查找策略先在源文件所在目录下查找如果该头文件未找到编译器就像查找库函数头文件⼀样在标准位置查找头文件。 如果找不到就提示编译错误。库文件包含#include filename.h查找策略查找头文件直接去标准路径下去查找如果找不到就提示编译错误。这样是不是可以说对于库文件也可以使用 “” 的形式包含答案是肯定的可以但是这样做查找的效率就低些当然这样也不容易区分是库文件还是本地文件了。编译编译过程就是将预处理后的文件进行⼀系列的词法分析、语法分析、语义分析及优化生成相应的汇编代码文件。汇编汇编器是将汇编代码转转变成机器可执行的指令每⼀个汇编语句几乎都对应⼀条机器指令。就是根据汇编指令和机器指令的对照表⼀⼀的进行翻译也不做指令优化。链接链接是⼀个复杂的过程链接的时候需要把⼀堆文件链接在⼀起才生成可执行程序。链接过程主要包括地址和空间分配符号决议和重定位等这些步骤。链接解决的是⼀个项目中多文件、多模块之间互相调用的问题运行环境程序必须载入内存中。在有操作系统的环境中⼀般这个由操作系统完成。在独立的环境中程序的载入必须由手工安排也可能是通过可执行代码置入只读内存来完成。程序的执行便开始。接着便调用main函数。开始执行程序代码。这个时候程序将使用⼀个运行时堆栈stack存储函数的局部变量和返回地址。程序同时也可以使用静态static内存存储于静态内存中的变量在程序的整个执行过程⼀直保留他们的值。终止程序。正常终止main函数也有可能是意外终止。

更多文章