暂时未有相关云产品技术能力~
生活中有些事情需要我们重复、反复的去做,这时我们就可以说我们循环的去做这件事。要学习循环语句,首先我们介绍一下程序设计语言中的循环是什么意思:循环是程序设计语言中反复执行某些代码的一种计算机处理过程,常见的有按照次数循环和按照条件循环。在不少实际问题中有许多具有规律性的重复操作,因此在程序中就需要重复执行某些语句。一组被重复执行的语句称之为循环体,能否继续重复,取决于循环的终止条件。循环语句是由循环体及循环的终止条件两部分组成的。那么接下来我们就来介绍一下C语言中的3中循环:一.while循环我们已经掌握了,if语句:if(条件)语句;当条件满足的情况下,if语句后的语句执行,否则不执行。但是这个语句只会执行一次。由于我们发现生活中很多的实际的例子是:同一件事情我们需要完成很多次。那我们怎么做呢?C语言中给我们引入了: while 语句,可以实现循环。1.语法介绍和基本使用首先我们来学习while循环,那什么是while循环呢?我们知道,while有当…的时候的意思,所以while循环就是当满足一个特定条件是执行循环体,一旦不满足,就结束循环。while的语法结构://while 语法结构 while (表达式) 循环语句; 举个例子,我们想要在屏幕上打印数字1——10,就可以使用while循环:#include <stdio.h> int main() { int i = 1; while (i <= 10) { printf("%d ", i); i = i + 1; } return 0; } 当i的值加到10 的时候,满足i<=10,再执行一次循环,i再加1,变为11,再进行判断,不满足i<=10,循环结束。上面的代码已经帮我了解了 while 语句的基本语法,那我们再继续向下学习:2. while循环中的break的作用break有终止,中断,逃脱的意思,那么在循环中break的作用是啥呢?我们通过一段代码来学习break的作用:#include <stdio.h> int main() { int i = 1; while (i <= 10) { if (i == 5) break; printf("%d ", i); i = i + 1; } return 0; } 大家思考一下,输出的结果是啥:答案是是的!!!,循环中遇到break循环就直接结束了。break在while循环中的作用:其实在循环中只要遇到break,就停止后期的所有的循环,直接终止循环。所以:while中的break是用于永久终止循环的。3.while循环中continue的作用介绍了break在在while中的作用,那我们再来介绍一下continue再while循环中的作用:还是通过几个实例来解释,上代码:先看第一个://continue 代码实例1 #include <stdio.h> int main() { int i = 1; while(i<=10) { if(i == 5) continue; printf("%d ", i); i = i+1; } return 0; } 思考一下,结果是什么?为什么出现这样的情况呢?别急,我们再来看一个代码://continue 代码实例2 #include <stdio.h> int main() { int i = 1; while (i <= 10) { i = i + 1; if (i == 5) continue; printf("%d ", i); } return 0; } 大家先看一下这个代码与上一个有啥区别,再思考结果是啥:我们发现,这段代码与上一个相比,只是把i=i+1这句代码放在了if语句前面,那结果会有什么不同呢?是这样吗?确实是的。现在我们就可以很好的解释上一个代码的结果了:总结:continue在while循环中的作用就是:continue是用于终止本次循环的,也就是本次循环中continue后边的代码不会再执行,而是直接跳转到while语句的判断部分。进行下一次循环的入口判断二.for循环1.语法介绍和基本使用我们已经知道了while循环,但是我们为什么还要一个for循环呢?首先来看看for循环的语法:for(表达式1; 表达式2; 表达式3) 循环语句; 看一个实际的问题:使用for循环 在屏幕上打印1-10的数字#include <stdio.h> int main() { int i = 0; //for(i=1/*初始化*/; i<=10/*判断部分*/; i++/*调整部分*/) for(i=1; i<=10; i++) { printf("%d ", i); } return 0; } 来看看结果是啥:相信现在大家对于for循环的基本使用已经了解了。2.for循环和while循环的对比我们使用for循环和while循环实现一个相同的功能,进行一下对比:实现相同的功能,使用whileint i = 0; i=1;//初始化部分 while(i<=10)//判断部分 { printf("hehe\n"); i = i+1;//调整部分 } 实现相同的功能,使用forfor(i=1; i<=10; i++) { printf("hehe\n"); } 可以发现在while循环中依然存在循环的三个必须条件,但是由于风格的问题使得三个部分很可能偏离较远,这样查找修改就不够集中和方便。所以,for循环的风格更胜一筹;for循环使用的频率也最高。3. break和continue在for循环中的作用在for循环中也可以出现break和continue,他们的意义和在while循环中是一样的。代码1:#include <stdio.h> int main() { int i = 0; for (i = 1; i <= 10; i++) { if (i == 5) break; printf("%d ", i); } return 0; } 代码2://代码2 #include <stdio.h> int main() { int i = 0; for(i=1; i<=10; i++) { if(i == 5) continue; printf("%d ",i); } return 0; } 好的一点时我们在for循环中这样写不会像while那样出现死循环。因为continue不能跳过调整部分所以在for循环中,break和Continue的作用也是如此:1.遇到break,就停止后期的所有的循环,直接终止循环,执行循环后面的部分。2.遇到continue,直接跳到调整部分,然后进行条件判断。4.for语句的循环控制变量这里给大家提一些建议:1.不可在for 循环体内修改循环变量,防止 for 循环失去控制。#include <stdio.h> int main() { int i = 0; for (i = 1; i <= 10; i++) { i = 3; if (i == 5) continue; printf("%d ", i); } return 0; } 2. 建议for语句的循环控制变量的取值采用“前闭后开区间”写法。(只是建议,这样写不合适的话也不必强求)5. 一些for循环的变种for循环中的初始化部分,判断部分,调整部分是可以省略的,但是不建议初学时省略,容易导致问题1.举个例子:#include <stdio.h> int main() { //代码1 for (;;) { printf("hehe\n"); } } 死循环了,为啥呢?2.在看一个例子:#include <stdio.h> int main() { //代码2 int i = 0; int j = 0; int count = 0; //这里打印多少个hehe? for (i = 0; i < 10; i++) { for (j = 0; j < 10; j++) { count++; printf("hehe\n"); } } printf("count=%d\n", count); } 打印多少次hehe呢?这个比较容易,打印100次。那还是这段代码,如果省略掉初始化部分,这里打印多少个hehe? //代码3 int i = 0; int j = 0; //如果省略掉初始化部分,这里打印多少个hehe? for(; i<10; i++) { for(; j<10; j++) { printf("hehe\n"); } } 去掉初始化部分后,值打印10次:3.for循环中的循环控制变量可以有多个举个例子:#include <stdio.h> int main() { //代码4-使用多个变量控制循环 int x, y; for (x = 0, y = 0; x < 2 && y < 5; ++x, y++) { printf("hehe\n"); } return 0; } 好了,for循环就介绍完了。三.do while循环接下来介绍do while循环1.语法介绍和基本使用do 循环语句; while (表达式); 特点:循环至少执行一次,使用的场景有限,所以不是经常使用。#include <stdio.h> int main() { int i = 1; do { printf("%d ", i); i=i+1; }while(i<=10); return 0; } 2. break和continue在do while循环中的作用break和continue在do while循环中的作用也和在while循环中一样。演示一下:代码1:#include <stdio.h> int main() { int i = 1; do { if(5 == i) break; printf("%d ", i); i=i+1; }while(i<=10); return 0; } 代码2:#include <stdio.h> int main() { int i = 1; do { if(5 == i) continue; printf("%d ", i); i=i+1; }while(i<=10); return 0; } 以上就是对C语言中循环语句的介绍了。欢迎大家指正!!!
这篇文章,我们来探讨一下,我们写的代码,是如何一步步变成可执行程序,最终运行得出结果的,一起来学习吧!!!1. 程序的翻译环境和执行环境在ANSI C(美国国家标准协会(ANSI)及国际标准化组织(ISO)推出的关于C语言的标准)的任何一种实现中,程序都存在两个不同的环境。第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。第2种是执行环境,用于实际代码执行。也就是说:我们写好的任何一个源代码,到最终产生结果,都要经历这两个环境。比如,我们写好了一个test.c的源文件,它需要先经过翻译环境生成可执行程序test.exe,然后再经过执行环境产生最终的结果。2. 翻译环境详解2.1翻译环境介绍对于翻译环境呢,又分为编译和链接1. 有时候我们的一个程序可能不止一个源文件,组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。2. 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。3. 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中,最终生成可执行程序。那下面我们就在vs2022写一个代码,让大家粗略的感受一下编译和链接的这个过程:看这个程序,包含了两个源文件。那我们现在vs上对该程序生成解决方案:然后我们进入到文件所在路径进行观察:我们发现,经过编译过程,test.c和add.c已经生成了对应的目标文件。然后:链接器会把这些目标文件和链接库链接在一起,最终生成可执行程序。那因为在test.c中使用了add.c中的add函数,所以这两个源文件要被链接在一起,这我们能想通。那还有一个链接库,这是个什么鬼?大家有没有注意到我们刚才的程序中还使用到了一个库函数——printf,像这些我们调用到的标准C函数库中的函数,就是放在链接库中的,链接器也会引入标准C函数库中这些被程序所用到的函数。刚刚在上面的过程中我们提到了编译器和链接器这两个东西。而对于我们平时写代码使用的这些工具,就比如我现在使用的这个vs2022,它其实不单单有编译和链接的功能,我们平时用的这些工具,它们都是一个集成开发环境(IDE),像常见的有 Visual Studio、Dev C++、Xcode、Visual C++ 6.0、C-Free、Code::Blocks 等。集成开发环境就是一系列开发工具的组合套装,比如编辑器,编译器,链接器,调式器等。我们可以在上面编辑代码,编译和链接代码,以及调式代码等。这个大家了解一下。2.2 编译详解对于编译本身,又可以划分为3个阶段:预编译(预处理)、编译、汇编。下面我们一起来看一下:就还看上面那段代码,首先,大概的过程是这样的:紧接着我们就来分析一下其中的细节:注:接下来的大部分演示将在Linux环境下利用gcc进行,因为vs上面有些东西我们不好观察,所有有些操作大家不必关心,只要明白我们在干什么就行了。当然这里面用到的一些命令大家可以了解一下:预处理 选项 gcc -E test.c -o test.i预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。编译 选项 gcc -S test.c编译完成之后就停下来,结果保存在test.s中。汇编 gcc -c test.c汇编完成之后就停下来,结果保存在test.o中。然后我们写这样一段代码:我们接下来对我们写的源文件test.c直接编译,然后生成了一个a.out的可执行程序,运行,我们看到成功打印了1到10的数字。但是我们刚刚直接完成了整个编译过程,并没有观察到其中的具体细节。2.2.1 预处理(预编译)下面我们就分别观察一下其中的细节:首先我们利用gcc -E test.c -o test.i让程序在预编译(预处理)之后停下来,并把内容输出到test.i文件中:我们看到里面有很多内容,八百多行,但里面包含了我们写的代码。那为什么多了这么多内容呢?大家有没有注意到我们在代码的第一行就包含了一个头文件stdio.h,那test.i中八百多行的内容中,在我们写的代码之前的那一大部分的内容是不是都是头文件带进来的内容。是的,预编译之后的test.i中前面的那么多内容都是来自头文件stdio.h的内容。我们可以验证一下,我们就打开一下stdio.h看看它里面的内容(具体操作大家不必关心):我们能够看到它们里面的有些内容是完全一样的。那从这里我们就能够得出一个结论:在预编译阶段需要做的事情之一是头文件的包含这件事。那我们继续探讨一下,预处理阶段还会做其它哪些事情呢?我们现在对刚才的代码做一些修改:我们现在不打印数组的元素了,那自然stdio.h我们也不用包含了,然后我们又添加了一行注释,并用#define定义了一个标识符,赋给了变量m。然后我们再把预处理后的内容写到test.i文件中,一起来看一下:这次我们再来看,前面就没有那一大堆#include 带进来的东西了。因为这次我们把头文件的包含注释掉了。而且我们注释掉的代码和自己写的注释也没有出现在test.i中。另外,我们定义的标识符#define MAX 100,也没有,而是直接将MAX替换成了100。所以,我们就知道,在预编译阶段还做了:注释的删除#define定义的符号的替换当然,肯定还不止这些事情,我们现在只是大致了解一下,后面我们会给大家详细介绍预处理。2.2.2 编译那我们接下来就来研究一下编译阶段会发生什么?还是这段代码:我们这次让它在编译之后停下来,然后我们来观察:这时编译之后的内容,如果大家之前在自己的编译器上查看过汇编代码的话,会发现这和汇编代码非常像,其实这就是产生的汇编代码。所以,在编译过程中,会把预处理之后的C语言代码转换成汇编代码。那在转换的过程中,又会做什么呢?1.语法分析2.词法分析3.语义分析4.符号汇总那这几步又是干什么呢?大家如果不知道也没关系,不重要,不过这里我们需要去了解一下符号汇总。那接下来,我们就了解一下符号汇总我们再来写这样一段代码:我们知道这段代码在完成整个编译过程之后,就会产生对应的可执行程序(a.out)。而这个可执行程序是按照一定的文件格式来进行组织的,这个格式叫做elf,a.out文件的内部呢,按照这个格式会划分成一个一个的段,分别存放不同内容的数据,其中有一个叫做符号表的东西。那我们怎么查看a.out这个文件呢?我们去直接打开的话是不行的:不过我们可以使用vim编辑器打开它,但是我们也看不懂:因为a.out其实是个二进制的文件,不过我们可以借助readelf来查看。我们可以利用相关命令只看符号表的内容:我们发现,从中能找到一些我们在代码中定义的符号,我们定义的全局变量g_val,还有main函数和Add函数的函数名。但是我们定义的一些局部变量a,b,c好像并没有在里面找到。所以:符号汇总其实就会把我们程序中的这些全局变量,函数名这种符号给汇总起来。那这其实就是符号汇总的一个作用,为什么要单独解释一下符号汇总呢?因为在链接的部分我们需要用到这些知识。2.2.3 汇编那接下来就是汇编了,编译的最后一步。那经过汇编之后,编译结束,是不是就产生对应的目标文件了呢 ?是的。那我们现在执行相关的命令让它在汇编之后停止:我们发现汇编之后又多了一个文件test.o,这个文件其实就是生成的目标文件。而它,是一个二进制文件。而机器指令就是二进制的。所以:汇编这一步做的其实就是把汇编指令转化为二进制的机器指令。而生成的目标文件test.o其实也是elf格式的,我们打开她也能看到相关的符号:所以,除了把汇编指令转化为二进制的机器指令,这一步还会做什么呢?就是把上一步汇总的符号形成符号表。2.3 链接详解通过上面的学习,我们知道,整个编译过程完成后,会产生目标文件,然后链接器就要对这样目标文件进行链接了。那链接过程又会发生什么呢?1. 合并段表2. 符号表的合并和重定位2.3.1 合并段表那什么是合并段表呢?我们上面提到过生成的目标文件test.o其实也是elf格式的,而按照这个格式呢,会把文件分成一个一个的段,分别用来存放表示不同用途的数据。那就拿我们最开始在vs上写的那个代码来说:两个.c的源文件test.c和add,c,那编译之后就生成两个目标文件test.o和add.o,它们都是elf格式的文件,按同样的方式划分。而最终链接之后生成的可执行文件是不是也是elf格式的啊,那这个时候,它们就会把这些相同段的内容都放在一起,最终生成一个可执行程序:这就是合并段表。2.3.2 符号表的合并和重定位那符号表的合并和重定位又是什么呢?我们已经知道了在汇编阶段会生成符号表,这些符号往往是一些全局变量和函数名。我们还来看这段代码:那这两个文件最后要生成一个可执行文件呀,所以就需要对它们的符号表进行合并。那合并的时候就会有一个问题,两个文件中都有一个add符号,地址应该选哪一个呢?选add.c中的,为什么?因为函数add.c在test.c中只是声明了一下,而真正的函数add的实现是在add.c中的,所以,最终要选择add.c中函数add的地址作为最终add的地址:这就是符号表的合并和重定位。那这些东西有什么用处呢?当链接过程中进行了符号表的合并和重定位之后,test.c中main函数调用add的时候是不是就能通过符号表中重定位之后的有效的函数地址找到add函数并调用它。当然如果add.c中没有定义add函数,或者函数名我们写错的情况下,是不是也会因为符号表中没有有效的信息而报错。我们可以验证一下,相信大家也遇到过这种情况:如果调用时函数名写错呢?这就体现了符号表的用处。3. 运行环境最后,我们来了解一下一个程序执行的过程:程序必须载入内存中。在有操作系统的环境中:这个过程一般由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。程序的执行便开始。接着便调用main函数。开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。终止程序。正常终止main函数;也有可能是意外终止。这篇文章,我们比较笼统的介绍了一个程序从编译到链接,再到最后执行的过程,下一篇文章,我们将详细的介绍一下预处理过程。这篇文章就到这里,希望能帮助到大家,也欢迎大家指正!!!
这篇文章我们一起来学习一下C/C++程序的内存开辟以及柔性数组!!!1. C/C++程序的内存开辟C和C++的内存开辟方式是非常类似的,这篇文章我们就来学习一下C/C++程序的内存开辟。在之前的文章里其实我们简单的介绍过C语言中的内存划分。大致可以分为:栈区,堆区和静态区:那今天,我们来更加细致的细致的讲解一下C/C++程序的内存开辟。首先,我们来看一张图:这张图更细致的划分了一下内存,接下来,我们就一个一个的就看一下:现阶段的学习中我们主要了解一下栈,堆,数据段和代码段就行了。1.内核空间首先第一个我们先来看内核空间,这块空间是用户代码不能读写的,也就是说,我们自己写的代码是不能访问这块空间的。2.栈这里的栈其实就是我们之前提到的栈区,栈区一般用来存放局部变量、函数的形参、调用函数时的返回值等临时变量。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。3.内存映射段内存映射段用来存放文件映射、动态库、匿名映射等内容。4.堆堆就是之前提到的堆区,堆区是用来进行动态内存分配的,像malloc、calloc、realloc这些动态内存函数开辟的空间就是在堆区上的,一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。5.数据段(静态区)数据段其实就是我们之前所说的静态区,静态区主要用来存放一些全局变量以及静态数据(如static修饰的静态变量)等。程序结束后由系统释放。 6.代码段 代码段存放的是可执行代码(函数体、类成员函数和全局函数的二进制代码。)和只读常量。有了这幅图,我们就可以更好的理解之前在《初始C语言——关键字static的作用》中讲的static关键字修饰局部变量的例子了。实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁。所以生命周期变长。2. 柔性数组2.1 柔性数组的定义接下来我们再来学习一个新知识——柔性数组。也许大家可能没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。C99 标准中,结构体中的最后一个元素允许是未知大小的数组,这个成员就叫做『柔性数组』成员什么意思呢?那接下来我们就来举个例子:struct S { int a; double b; int arr[]; }; 我们看struct S这个结构体类型,它就包含了一个柔性数组成员int arr[],它的大小是未知的,我们并没有指定它的大小。如果你这样写了,在你的编译器上报错了无法编译,那可能是你的编译器不支持这种写法,你可以换成这种写法:struct S { int a; double b; int arr[0]; }; 这时两种不同的写法,可能有的编译器支持这种,有的支持那种。当然还要注意这中语法是C99 标准中才引入的。2.2 柔性数组的特点既然它叫柔性数组,呢这个“柔”怎么体现呢?接下来我们就来了解一下柔性数组的特点:结构体中的柔性数组成员前面必须至少有一个其他成员struct S { int arr[0]; }; 也就是说你不能写成像上面这样,柔性数组成员前面至少要有一个其它成员。struct S { int a; //.....;(至少一个其它成员) int arr[0]; }; sizeof 返回的这种结构体的大小不包括柔性数组的内存大小什么意思呢?就是我们用sizeof去计算这种包含柔性数组成员的结构体的大小时,不会加上柔性数组成员的大小。况且柔性数组没有指定数组大小,真要计算好像也没法算啊!我们来计算一个包含柔性数组的结构体的大小看看:#include <stdio.h> struct S { int a;//对齐数4 double b;//对齐数8 int arr[]; }; int main() { printf("%d", sizeof(struct S)); return 0; } 这个结构体大小,如果只看前两个成员,考虑对齐的话,应该是16个字节。我们打印出来看看:确实是16个字节,没有包含柔性数组的内存大小。包含柔性数组成员的结构体应该用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小什么意思呢?包含柔性数组成员的结构体应该用malloc ()函数进行内存的动态分配,这句话意味着我们不能像普通的结构体那样直接拿我们创建好的结构体类型创建结构体变量:比如像这样:#include <stdio.h> struct S { int a; double b; int arr[]; }; int main() { struct S s1; return 0; } 这段代码中struct S是一个包含柔性数组成员的结构体变量,但这里还是像普通的结构体一样创建了一个结构体变量。但这样其实是错误的用法。为什么这样不行呢?我们上面已经讲了,sizeof 返回的这种结构体的大小不包括柔性数组的内存大小,那我们直接像这样创建一个结构体变量,这个柔性数组成员是没有属于自己的空间的,那我们就没法使用它啊。那对于这种包含柔性数组成员的结构体,我们应该怎样正确的为它开辟空间,使得我们可以使用这个柔性数组呢?那就是上面说的,应该用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小比如:struct S { int a; double b; int arr[]; }; 我们就还拿这个包含柔性数组的结构体来说,假如我们想使用这个柔性数组,去存放4个整型数据。那我们就可以这样为它开辟空间:#include <stdio.h> struct S { int a; double b; int arr[]; }; int main() { struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 4); return 0; } 前面sizeof(struct S)就是前面两个成员的大小,后面又加了一个sizeof(int) * 4就是为柔性数组开辟的空间,因为我们想往里放4个整型数据。2.3 柔性数组的使用那开辟好空间,我们就可以使用了:我们现在就给这个结构体的成员赋个值,然后打印一下看看,当然记得malloc的返回值我们还是要判断一下,使用完释放一下,把ps 置空。int main() { struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 4); assert(ps); ps->a = 100; ps->b = 5.5; int i = 0; for (i = 0; i < 4; i++) { scanf("%d", &(ps->arr[i])); } printf("%d %lf\n", ps->a, ps->b); for (i = 0; i < 4; i++) { printf("%d", ps->arr[i]); } free(ps); ps = NULL; return 0; } 给柔性数组输入1,2,3,4,打印一下看看:没问题,这样做就成功为柔性数组开辟了空间,并且可以使用它。那讲到这里,大家是有没有对柔性数组的这个“柔性”有了一点自己的理解呢?大家想一下,我们刚才为柔性数组开辟了4个字节空间,如果我们不使用柔性数组,直接定义一个这样的结构体:struct S { int a; double b; int arr[4]; }; 直接包含一个int arr[4]这样的数组,那它是不是也能放4个整型啊。但是,这样的话,它的大小是不是就固定死了,就能放4个整型,想多放一个都不行。而我们使用柔性数组的话,是使用malloc为它开辟空间的,那我们跟据自己的需求,是不是可以使用realloc再调整柔性数组这块空间的大小啊。struct S* ptr=(struct S*)realloc(ps,sizeof(struct S) + sizeof(int) * 10); assert(ptr); ps=ptr; 这次调整,它就可以放10个整型了。可大可小,是不是有点那种所谓的“柔性”的意思了。2.4 柔性数组的优势那讲完柔性数组的使用,大家可能会想:柔性数组说到底,不就是搞了一个可大可小的数组嘛,那想要实现这样的功能,非得用柔性数组嘛。我们是不是也可以这样搞:struct S { int a; double b; int* arr; }; 我们定义一个int* arr这样一个成员变量,它指向的空间我们可以使用malloc为它开辟啊,如果大小不合适,我们就再使用realloc调整大小,这样是不是也可以达到上面柔性数组的效果啊。那为了和上面的代码保持一致,我们这里创建一个结构体变量是不是也把他所有的成员放到堆区上,那这里我们可以这样搞:struct S { int n; double s; int* arr; }; int main() { struct S*ps = (struct S*)malloc(sizeof(struct S)); if (ps == NULL) return 1; ps->n = 100; ps->s = 5.5; int* ptr = (int*)malloc(4 * sizeof(int)); if (ptr == NULL) { return 1; } else { ps->arr = ptr; } //使用 int i = 0; for (i = 0; i < 4; i++) { scanf("%d", &(ps->arr[i])); } //调整 //realloc(ps->arr, 10*sizeof(int)); //打印 printf("%d\n", ps->n); printf("%lf\n", ps->s); for (i = 0; i < 4; i++) { printf("%d ", ps->arr[i]); } //释放 free(ps->arr); ps->arr = NULL; free(ps); ps = NULL; return 0; } 大家仔细看看这段代码。这样确实也是可以的。上述这两个代码可以达到同样的效果。那既然这样也可以,我们为什么还要搞一个柔性数组呢?因为柔性数组是有一些自己独有的优势的。那接下来,我们就对比一下这两段代码,看看柔性数组存在哪些优势:我们先来看一下第二段代码,仔细观察我们发现第二段代码用了两次malloc:第一次我们是定义了一个struct S*类型的指针ps,将它赋值为(struct S*)malloc(sizeof(struct S)),这样它指向的空间就是在堆区了,它指向的结构体变量就也是在堆区了(和上面代码保持一致)第二次我们malloc是为ps->arr,也就是为柔性数组开辟空间。那这样开辟了两次,有没有什么不好之处呢?那就是这两次开辟的空间有可能不是连续的,不连续的话它们之间就有可能形成内存碎片,而这些残留的空间以后也不太好被有效的利用起来了,这样可能就导致内存的利用率就下降了。而第一种我们使用柔性数组的方法:我们只malloc了一次,使得前两个成员和柔性数组成员放在了一块连续的空间。除此之外:第一种方法我们malloc开辟了两次,那我们就要free释放两次,除了要释放结构体指针指向的那块空间,是不是还要释放结构体指针指向的柔性数组成员所在的那块malloc开辟的空间啊。如果我们忘记释放了某一个,那是不是就造成内存泄漏了。所以通过这一点就体现了方法1(使用了柔性数组)的第一个优势:方便内存释放如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。而方法1使用柔性数组就达到了这样的效果。有利于提高访问速度连续的内存有益于提高访问速度,也有益于减少内存碎片。(不过可能也高不了多少)总的来说,第一段代码使用了柔性数组,在某些方面还是比第二段代码更好一些的。好了,以上就是这篇文章的全部内容了,欢迎大家指正!!!
3. 常见的动态内存错误在进行动态内存管理时,有很多需要注意的,一旦我们使用不当,就有可能会导致错误的发生。接下来我们就来总结一下,哪些操作可能会引发动态内存错误。3.1 对NULL指针的解引用操作通过上面的学习我们已经知道了,malloc,realloc,calloc在开辟空间时,一旦开辟失败,就会返回空指针,如果我们不小心对这些空指针进行了解引用,就会导致错误的发生。举个例子:int main() { int* p = (int*)malloc(4); *p = 20;//如果p的值是NULL,就会有问题 free(p); p = NULL; return 0; } 大家看这段代码有问题吗?因为malloc有可能返回空指针,所以像上面这样不做判断,直接对malloc返回的指针,解引用,就可能会导致问题出现。我们写出这样的代码,有的编译器可能就直接会报警告:不过上面的代码中我们申请的空间比较小,只有4个字节,可能不会申请失败。如果我们要申请一块特别大的空间,很有可能就会开辟失败:我们来试一下:int main() { int* p = (int*)malloc(INT_MAX); *p = 20;//如果p的值是NULL,就会有问题 free(p); p = NULL; return 0; } 开辟INT_MAX字节的空间,INT_MAX是整型的最大值:这次,很有可能就会失败,所以我们最后加一个判断:int main() { int* p = (int*)malloc(INT_MAX); if (p == NULL) { perror("malloc"); return 1; } else { *p = 20; } free(p); p = NULL; return 0; } 这里的perror也是一个打印错误信息的函数(和strerror差不多),不过它可以在前面加上我们自定义的信息。我们看结果是什么:说明,这里malloc就开辟失败了,返回的是空指针。所以,对于malloc,realloc,calloc的返回值,我们一定要进行一个检查,防止对空指针解引用。3.2 对动态开辟空间的越界访问对动态开辟空间的越界访问,也会发生错误。举个例子:int main() { int i = 0; int* p = (int*)malloc(5 * sizeof(int)); assert(p);//断言,防止p为空指针 for (i = 0; i <= 10; i++) { *(p + i) = i;//越界访问 printf("%d ", *p); } free(p); p = NULL; return 0; } 上面的代码中,我们使用malloc开辟了5个整型大小的空间,即20个字节,那p 作为1个整型指针,加1跳过4个字节,那我们循环10次,是不是就越界访问了啊。这时我们运行就出错了:因为我们越界访问了。所以,也注意不能越界访问。3.3 对非动态开辟内存使用free释放这个我们在上面其实也已经提到过了。我们要知道,free是用来释放动态开辟的内存空间的,如果我们用free去释放非动态开辟的内存,此时free的行为是标准未定义的。比如:int main() { int num = 10; int* p = &num; free(p); p = NULL; return 0; } num 是我们定义的局部变量,是保存在栈区的,而堆区才是用来动态内存分配的。这样的代码运行,可能是会出错的。所以我们不要用free去释放非动态开辟的内存。3.4 使用free释放一块动态开辟内存的一部分什么意思呢?我们在使用free释放一块动态开辟的内存空间的时候,传给free那个指针必须是指向这块空间的起始位置。如果在使用过程中,原本指向内存块起始位置的指针发生了改变,不再指向该空间的起始位置,那我们最后用free去释放这块空间的时候,就不能再传这个指针了。举个例子:int main() { int* p = (int*)malloc(10); assert(p); p++; free(p);//p不再指向动态内存的起始位置 p = NULL; return 0; } 这样的操作就是错误的,我们free(p)的时候,p 已经不再指向这块动态内存的起始位置了。运行这样的代码,程序就出错了。如果确实需要改变:我们可以再创建一个指针变量,保存一下最初指向起始地址的指针,这样最后释放的时候,我们依然能找到起始位置的地址。像这样:int main() { int* p = (int*)malloc(10); assert(p); int* ptr = p;//用ptr保存malloc开辟空间的起始位置 p++; free(ptr);//释放的时候传ptr ptr = NULL; p = NULL; return 0; } 这样,程序就不会出错了。3.5 对同一块动态内存多次释放什么意思呢?我们动态开辟一块内存空间,使用完直接释放了,并且没有将指向该内存块起始位置的指针(假设是指针p)置空,过了一会儿可能忘记已经释放过了,然后再后面又把p传给free,又对这块空间进行一次释放。这样的话我们运行代码就也会出错的。像这样:int main() { int* p = (int*)malloc(100); free(p); ///.....; free(p);//重复释放 return 0; } 这样程序是会崩掉的。为了避免这种情况发生:我们在释放掉p指向的空间之后,要及时将p置空。int main() { int* p = (int*)malloc(100); free(p); p = NULL; ///.....; free(p);//重复释放 return 0; } 这样,虽然我们释放了两次,但因为我们第二次传的是空指针,所以不会有问题。因为如果free的参数 ptr 接收的是NULL指针,不执行任何操作。所以:在使用free释放一块动态内存空间后,及时将指向起始位置的指针置空是一个好习惯。3.6 动态开辟内存忘记释放(内存泄漏)就是我们动态开辟的空间在使用完之后,一定要记得释放,不释放的话有可能会造成内存泄漏。切记:动态开辟的空间一定要释放,并且正确释放 。4.经典笔试题讲解下面,我们一起来看几个动态内存管理相关的经典笔试题。4.1 题目1我们来看这段代码:#include <stdio.h> void GetMemory(char* p) { p = (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str); } int main() { Test(); return 0; } 请问运行 Test 函数会有什么样的结果?经过上面的学习,我们相信大家应该很容易能够看出来,上面这段代码存在一些比较严重的问题。我们一起来分析一下:首先,test函数进去,定义了一个字符指针char* str,给它赋了一个空指针NULL,然后,调用GetMemory函数,实参传的是str,GetMemory函数用了一个字符指针p接收,在GetMemory内部,用malloc动态开辟了100个字节的空间,返回值强制类型转换为char*赋给了p。走到这一步我们其实就能发现一个小问题:这里没有对malloc进行一个判断或者断言,因为malloc有可能开辟空间失败返回空指针。当然这还不算这段代码中最严重的问题。那我们接着往下看:GetMemory(str);调用结束,下一句代码是:strcpy(str, "hello world");看到这里我们应该能猜出来这段代码的目的:应该是想把字符串"hello world"拷贝到函数GetMemory(str)中动态开辟的那100个字节空间里,然后打印出来。那到这里第二个问题就出来了。strcpy(str, "hello world")既然是想把"hello world"拷贝到函数GetMemory(str)中动态开辟的那100个字节空间里,那第一个参数str是不是应该指向malloc开辟的那100个字节才对啊。但是,上面代码里面传参传的是指针变量str,形参p实际只是str的一个临时拷贝。我们把malloc的返回值赋给了p,让p指向了这100个字节空间的首地址,但是str是不是并没有改变啊,Test 函数中的 str 一直都是 NULL。而strcpy在拷贝是应该是会对str解引用的,这样会导致程序崩溃的!!!还有一个问题:malloc开辟的空间使用完是需要使用free释放的,但是上述代码并没有释放,这样就可能导致内存泄漏,因此,这也是一个比较严重的错误。那接下来我们就来修改一下这段代码,将它变成正确的:#include <stdlib.h> #include <string.h> void GetMemory(char** p) { *p = (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(&str);//不传地址,将p作为返回值赋给str也可以 strcpy(str, "hello world"); printf(str); free(str); str = NULL; } int main() { Test(); return 0; } 这样代码就正确了:4.2 题目2(返回栈空间地址的问题)char* GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char* str = NULL; str = GetMemory(); printf(str); } int main() { Test(); return 0; } 大家再来看看这段代码有没有什么问题?我们一起来分析一下:首先str还是一个char*的指针,给它赋值为NULL,然后调用GetMemory(),GetMemory()内部创建了一个字符数组p,放了一个字符串"hello world",p是数组名,是首字符’h’的地址,将p作为返回值赋给str,那我们是不是就可以通过str访问数组p了,printf(str)就把"hello world"打印出来了。是这样吗?如果这样想,那就错了。为什么呢?数组p是我们在函数内部创建的一个局部的数组,当函数调用结束就被销毁了,数组所在的这块空间就还给操作系统了,那这时候我们再去打印这块空间里的内容,是不是就非法访问内存了。这样也是不行的。4.3 题目3void GetMemory(char** p, int num) { *p = (char*)malloc(num); } void Test(void) { char* str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); } int mani() { Test(); return 0; } 再来看这段代码,有什么问题:这段代码前面都没有什么大问题,当然这里还是没有对malloc进行是否为空指针的判断。传的是str的地址,GetMemory调用结束后,str指向的就是malloc开辟的那100字节的空间,那strcpy(str, "hello");就能够成功把字符串"hello"拷贝到str指向的空间,然后打印出来,这都没什么问题。但是:是不是没有对malloc开辟的空间进行释放,还是存在一个内存泄漏问题。我们可以来修改一下:void GetMemory(char** p, int num) { *p = (char*)malloc(num); assert(*p); } void Test(void) { char* str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); free(str); str = NULL; } int mani() { Test(); return 0; } 这样就没什么问题了。4.4 题目4void Test(void) { char* str = (char*)malloc(100); strcpy(str, "hello"); free(str); if (str != NULL) { strcpy(str, "world"); printf(str); } } 来看这段代码,有没有问题:首先,第一个问题还是没有对malloc的返回值进行一个判断,有可能是空指针。其次,我们发现,在strcpy(str, "hello");之后,就直接free(str)了,这时str指向的空间已经被释放了,但是str还保留这块空间的地址,因为释放后我们并没有将它置空,那此时的str是不是已经成为野指针了,因为它指向了一块已经被释放的空间。那下面的if (str != NULL)判断结果为真,就会进入if语句,而在if语句里面又有strcpy(str, "world"),把"world"拷贝到已经不属于我们的动态内存区,篡改动态内存区的内容,后果难以预料,非常危险。所以这段代码也是有问题的。以上就是对C语言动态内存管理的讲解及一些笔试题练习,欢迎大家指正!!!
这篇文章,我们一起来学习C语言中的动态内存管理!!!1.为什么存在动态内存分配我们先来想一下,我们现在掌握的开辟内存的方式是什么:是不是就是直接创建一个变量或者数组,然后操作系统给我们分配空间:int main() { int val = 20;//在栈空间上开辟4个字节 int arr[10] = { 0 };//在栈空间上开辟40个字节的连续空间 return 0; } 大家思考一下这样的方式有没有什么弊端:我们这样定义一个数组int arr[10],开辟的空间大小是固定的。int arr[10]就只能存的下10个整型,我们想多存一个都不行。我们想存11个整型,用int arr[10]这个数组就不行了,除非我们再定义一个数组。其次:数组在声明的时候,需要指定数组的长度,它所需要的内存在编译时分配。但是,对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道。那这时候,这样开辟空间的方式就不行了。这个时候就需要动态开辟内存空间了。那怎么实现动开辟内存呢?C语言给提供了一些函数使得我们可以实现对内存的动态开辟。2.动态内存函数的介绍接下来我们就来一起学习一下这些函数:2.1 malloc看一下它的参数:void* malloc (size_t size);那它是用来干嘛的呢?接下来再来给大家详细解释一下:参数size_t size接收我们想要开辟的内存空间的大小,单位是字节,返回指向该内存块开头的指针。int main() { void* p = malloc(40); return 0; } 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。malloc给我们返回的指针类型是void*,但我们知道void*是不能直接解引用的,注意使用时要先转换为我们需要的指针类型。比如我们想再申请的空间里放整数,就应该这样搞:int* p = (int*)malloc(40);然后,我们就可以往里面放整型数据了。当然,你想用来放其他数据,就转换成其它相应的类型。如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。当然用malloc开辟空间也有可能开辟失败,当请求失败的时候,它将会返回空指针(NULL)。我们知道空指针是不能直接解引用的。所以,对于malloc的返回值,使用之前,我们一定要检查一下。如果为空,那就是失败了,就不能使用了。那什么时候又可能失败呢,比如当我们开辟的空间特别大的时候,就有可能失败返回空指针。如果开辟失败我们可以做一个相应处理,打印一下错误信息,然后return一下,让程序结束。 int* p = (int*)malloc(40); if (p == NULL) { printf("%s\n", strerror(errno)); return 1; } 函数strerror我们在之前的文章里介绍过。当然我们也可以断言一下:assert(p);如果不为空,那就是开辟成功了。开辟成功,我们就可以使用了。举个例子,我们现在就在上面开辟好的P指向的40字节的空间里放一些整型数据。 int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } 40个字节,我们可以放10个整型,0到9。我们也可以通过内存观察一下:使用前:这里再给大家提一点:我们发现开辟好的空间里面放的这些其实是一些随机值这也是malloc的一个特性:新分配的内存块的内容不做初始化,仅保留不确定的值。使用后:如果参数size_t size为0,则返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被解引用。此时malloc的行为是标准是未定义的,取决于编译器。所以我们尽量不要这样试,况且这样做也没什么意义,申请一个大小为0的空间?那申请的空间使用完之后,我们是不是什么都不用管了呢?不是的,对于像malloc这些函数动态开辟的内存,使用完之后我们是需要将这些空间释放掉的,不及时释放,有可能会造成内存泄漏。那怎么释放呢?2.2 freeC语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的。接下来我们就来一起学习一下函数free:它的参数是这样的:怎么用呢?参数void* ptr接收一个指针,这个指针指向我们使用malloc这些动态开辟内存函数分配的内存块,无返回值。比如,上面例子中的指针P: int* p = (int*)malloc(20); /*if (p == NULL) { printf("%s\n", strerror(errno)); return 1; }*/ assert(p); int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } 在上述循环的过程中,p 的指向并没有发生改变,还是指向分配的内存块的起始地址,所以我们就可以这样做:free(p); 这样,就把malloc申请的空间释放掉了。那释放掉之后,是不是就万事大吉了呢?不,我们还应该做一件事情:把p置空p = NULL; 为什么要这样做呢?大家想一下,我们现在虽然已经把p指向的那块空间给释放掉了。但是,p是不是还保存着那块空间的地址啊。那么一个指针指向了一块被释放掉的空间,那它是不是一个典型的野指针啊。要知道如果对一个野指针解引用那程序就会出错的。如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。也就是说参数 ptr 指向的空间必须是动态开辟的。如果指向其它的空间,那么free函数会怎么处理是标准未定义的。比如:int main() { int num = 10; int* p = &num; free(p); p = NULL; return 0; } 你写一个这样的代码,肯定是不行的,因为p指向的空间不是动态开辟的。这里的num是一个局部变量,要知道局部变量是保存在栈区的,再来复习一下:而我们这些动态开辟的内存,是堆区分配的。如果参数 ptr 是NULL指针,则函数不执行任何操作。像这样: int* p = NULL; free(p); 函数不执行任何操作。2.3 callocC语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。我们一起来学习一下:函数calloc 有两个参数,无返回值,那它的作用是什么呢?这两个参数分别接收什么呢?函数的功能是为 num 个大小为 size 的元素开辟一块空间,同样返回指向该内存块开头的指针,类型为(void*)参数size_t num接收我们想要分配空间的元素个数;size_t size接收每个元素的大小,单位为字节。那我们就可以这样用:int main() { int* p = (int*)calloc(10,sizeof(int)); /*if (p == NULL) { printf("%s\n", strerror(errno)); return 1; }*/ assert(p); int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } free(p); p = NULL; return 0; } 当然calloc分配的空间使用完也应该使用free释放并将指向空间起始地址的指针置空。与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。对于malloc 来说,它不会对开辟好的空间初始化,里面放的是随机值。但是,calloc 会把申请的空间的每个字节都初始化为0。就拿上面那段代码,我们来调式看一下:和malloc 一样,calloc 函数如果开辟内存块失败,则返回空指针void*。所以对于calloc 的返回值,我们也有必要做一下检查,判断是否为空指针。和malloc一样,如果参数size_t size为0,则返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被解引用。标准未定义的,取决于编译器。总的来说,malloc和calloc 区别不大:1. calloc 会在返回地址之前把申请的空间的每个字节初始化为全0,而malloc不会,里面放的是随机值。2. 它们的参数不同。2.4 reallocrealloc函数的出现让动态内存管理更加灵活。有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们就要对开辟的内存的大小做出灵活的调整。那 realloc 函数就可以做到对动态开辟的内存大小进行灵活的调整。一起来学习一下:两个参数分别接收什么呢?void* ptr接收一个指针,该指针指向我们想要调整大小的内存块,当然这块内存块也应该是我们之前动态开辟的空间。size_t size接收我们想要为内存块调整的新大小,以字节为单位。返回值又是什么呢?返回指向重新分配的内存块的指针举个例子吧,我们再来看一段上面的代码:int main() { int* p = (int*)malloc(40); assert(p); int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } free(p); p = NULL; return 0; } 还是这段代码:我们使用malloc申请了40个字节空间,放了10个整型。那假设我们现在想再放10个整型,那原来的空间就不够用了,那我们现在就可以使用realloc 进行扩容。怎么搞呢?这样写: int* p = (int*)malloc(40); assert(p); int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } int* ptr = (int*)realloc(p, 80); if (ptr != NULL) { p = ptr; ptr = NULL; } //使用 free(p); p = NULL; 变成这样,我们再中间又加了一些代码。我们看到上面代码中我们扩容后返回的指针赋给指针变量ptr ,那为什么不直接给p呢?因为,realloc开辟空间也有可能会失败的,它失败同样返回空指针。所以我们先赋给ptr ,然后判断一下,不为空,再赋给p,让p继续管理扩容后的空间。然后,不使用ptr ,最好将其也置空。然后,没什么问题,我们就可以使用扩容后的空间了。但是,在扩容的时候,又存在存在两种情况:原地扩什么时候是原地扩呢?就还拿刚才的例子来说:int* ptr = (int*)realloc(p, 80);p原来指向的空间是40个字节,现在我们想要使用realloc将p指向的空间扩容为80个字节。那这时realloc就会从原空间向后看,如果后面有足够大的空间能够再增加40个字节,那么realloc就会在原地向后扩容40个字节,使得p指向的空间变为80字节。当然这样realloc返回的地址还是原来p指向的地址。异地扩那什么时候异地扩呢?假设现在还是相把p指向的空间扩容为80个字节。但是,原空间后面没有足够大的空间,那这时候怎么办?这时候:realloc会在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址,不再指向原空间。而且:realloc会将原空间的数据拷贝到新空间,并会将旧空间释放掉。然后返回指向该内存块起始地址的指针。比如:int* p = (int*)realloc(NULL, 40); 那这句代码就相当于:int* p = (int*)malloc(40); 以上就是对这4个动态内存函数的介绍,它们包含的头文件都是#include 。
文章目录一.while循环1.语法介绍和基本使用2. while循环中的break的作用3.while循环中continue的作用二.for循环1.语法介绍和基本使用2.for循环和while循环的对比3. break和continue在for循环中的作用4.for语句的循环控制变量5. 一些for循环的变种三.do while循环1.语法介绍和基本使用2. break和continue在do while循环中的作用生活中有些事情需要我们重复、反复的去做,这时我们就可以说我们循环的去做这件事。要学习循环语句,首先我们介绍一下程序设计语言中的循环是什么意思:循环是程序设计语言中反复执行某些代码的一种计算机处理过程,常见的有按照次数循环和按照条件循环。在不少实际问题中有许多具有规律性的重复操作,因此在程序中就需要重复执行某些语句。一组被重复执行的语句称之为循环体,能否继续重复,取决于循环的终止条件。循环语句是由循环体及循环的终止条件两部分组成的。那么接下来我们就来介绍一下C语言中的3中循环:一.while循环我们已经掌握了,if语句:if(条件)语句;当条件满足的情况下,if语句后的语句执行,否则不执行。但是这个语句只会执行一次。由于我们发现生活中很多的实际的例子是:同一件事情我们需要完成很多次。那我们怎么做呢?C语言中给我们引入了: while 语句,可以实现循环。1.语法介绍和基本使用首先我们来学习while循环,那什么是while循环呢?我们知道,while有当…的时候的意思,所以while循环就是当满足一个特定条件是执行循环体,一旦不满足,就结束循环。while的语法结构://while 语法结构 while (表达式) 循环语句; 举个例子,我们想要在屏幕上打印数字1——10,就可以使用while循环:#include <stdio.h> int main() { int i = 1; while (i <= 10) { printf("%d ", i); i = i + 1; } return 0; } 当i的值加到10 的时候,满足i<=10,再执行一次循环,i再加1,变为11,再进行判断,不满足i<=10,循环结束。上面的代码已经帮我了解了 while 语句的基本语法,那我们再继续向下学习:2. while循环中的break的作用break有终止,中断,逃脱的意思,那么在循环中break的作用是啥呢?我们通过一段代码来学习break的作用:#include <stdio.h> int main() { int i = 1; while (i <= 10) { if (i == 5) break; printf("%d ", i); i = i + 1; } return 0; } 大家思考一下,输出的结果是啥:答案是是的!!!,循环中遇到break循环就直接结束了。break在while循环中的作用:所以:while中的break是用于永久终止循环的。3.while循环中continue的作用介绍了break在在while中的作用,那我们再来介绍一下continue再while循环中的作用:还是通过几个实例来解释,上代码:先看第一个://continue 代码实例1 #include <stdio.h> int main() { int i = 1; while(i<=10) { if(i == 5) continue; printf("%d ", i); i = i+1; } return 0; } 思考一下,结果是什么?为什么出现这样的情况呢?别急,我们再来看一个代码://continue 代码实例2 #include <stdio.h> int main() { int i = 1; while (i <= 10) { i = i + 1; if (i == 5) continue; printf("%d ", i); } return 0; } 大家先看一下这个代码与上一个有啥区别,再思考结果是啥:我们发现,这段代码与上一个相比,只是把i=i+1这句代码放在了if语句前面,那结果会有什么不同呢?是这样吗?确实是的。现在我们就可以很好的解释上一个代码的结果了:总结:continue在while循环中的作用就是:continue是用于终止本次循环的,也就是本次循环中continue后边的代码不会再执行,而是直接跳转到while语句的判断部分。进行下一次循环的入口判断二.for循环1.语法介绍和基本使用我们已经知道了while循环,但是我们为什么还要一个for循环呢?首先来看看for循环的语法:for(表达式1; 表达式2; 表达式3) 循环语句; 看一个实际的问题:使用for循环 在屏幕上打印1-10的数字#include <stdio.h> int main() { int i = 0; //for(i=1/*初始化*/; i<=10/*判断部分*/; i++/*调整部分*/) for(i=1; i<=10; i++) { printf("%d ", i); } return 0; } 来看看结果是啥:相信现在大家对于for循环的基本使用已经了解了。2.for循环和while循环的对比我们使用for循环和while循环实现一个相同的功能,进行一下对比:实现相同的功能,使用whileint i = 0; i=1;//初始化部分 while(i<=10)//判断部分 { printf("hehe\n"); i = i+1;//调整部分 } 实现相同的功能,使用forfor(i=1; i<=10; i++) { printf("hehe\n"); } 可以发现在while循环中依然存在循环的三个必须条件,但是由于风格的问题使得三个部分很可能偏离较远,这样查找修改就不够集中和方便。所以,for循环的风格更胜一筹;for循环使用的频率也最高。3. break和continue在for循环中的作用在for循环中也可以出现break和continue,他们的意义和在while循环中是一样的。代码1:#include <stdio.h> int main() { int i = 0; for (i = 1; i <= 10; i++) { if (i == 5) break; printf("%d ", i); } return 0; } 代码2://代码2 #include <stdio.h> int main() { int i = 0; for(i=1; i<=10; i++) { if(i == 5) continue; printf("%d ",i); } return 0; } 好的一点时我们在for循环中这样写不会像while那样出现死循环。因为continue不能跳过调整部分所以在for循环中,break和Continue的作用也是如此:1.遇到break,就停止后期的所有的循环,直接终止循环,执行循环后面的部分。2.遇到continue,直接跳到调整部分,然后进行条件判断。4.for语句的循环控制变量这里给大家提一些建议:1.不可在for 循环体内修改循环变量,防止 for 循环失去控制。#include <stdio.h> int main() { int i = 0; for (i = 1; i <= 10; i++) { i = 3; if (i == 5) continue; printf("%d ", i); } return 0; } 2. 建议for语句的循环控制变量的取值采用“前闭后开区间”写法。(只是建议,这样写不合适的话也不必强求)5. 一些for循环的变种for循环中的初始化部分,判断部分,调整部分是可以省略的,但是不建议初学时省略,容易导致问题1.举个例子:#include <stdio.h> int main() { //代码1 for (;;) { printf("hehe\n"); } } 死循环了,为啥呢?2.在看一个例子:#include <stdio.h> int main() { //代码2 int i = 0; int j = 0; int count = 0; //这里打印多少个hehe? for (i = 0; i < 10; i++) { for (j = 0; j < 10; j++) { count++; printf("hehe\n"); } } printf("count=%d\n", count); } 打印多少次hehe呢?这个比较容易,打印100次。那还是这段代码,如果省略掉初始化部分,这里打印多少个hehe? //代码3 int i = 0; int j = 0; //如果省略掉初始化部分,这里打印多少个hehe? for(; i<10; i++) { for(; j<10; j++) { printf("hehe\n"); } } 去掉初始化部分后,值打印10次:3.for循环中的循环控制变量可以有多个举个例子:#include <stdio.h> int main() { //代码4-使用多个变量控制循环 int x, y; for (x = 0, y = 0; x < 2 && y < 5; ++x, y++) { printf("hehe\n"); } return 0; } 了,for循环就介绍完了。三.do while循环接下来介绍do while循环1.语法介绍和基本使用do 循环语句; while (表达式); 特点:循环至少执行一次,使用的场景有限,所以不是经常使用。#include <stdio.h> int main() { int i = 1; do { printf("%d ", i); i=i+1; }while(i<=10); return 0; } 2. break和continue在do while循环中的作用break和continue在do while循环中的作用也和在while循环中一样。演示一下:代码1:#include <stdio.h> int main() { int i = 1; do { if(5 == i) break; printf("%d ", i); i=i+1; }while(i<=10); return 0; } 代码2:#include <stdio.h> int main() { int i = 1; do { if(5 == i) continue; printf("%d ", i); i=i+1; }while(i<=10); return 0; } 以上就是对C语言中循环语句的介绍了。欢迎大家指正!!!
这篇文章我们一起学习一下函数的参数,函数的参数分为实参和形参。一.什么是实际参数(实参)首先我们来学习实参,什么是实参呢?实际参数简称“实参”。在调用有参函数时,函数名后面括号中的参数称为“实参”,是我们真实传给函数的参数,实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。下面我们写个代码来帮助大家理解:#include <stdio.h> int add(int x, int y) { return x + y; } int main() { int a = 20; int b = 30; int ret1 = add(20, 30); printf("%d\n", ret1); int ret2 = add(a, b); printf("%d\n", ret2); int ret3 = add(a + b, a - b); printf("%d\n", ret3); int ret4 = add(add(2, 3), 5); printf("%d\n", ret4); return 0; } 无论实参是何种类型的量,它们都必须有确定的值二.什么是形式参数(形参)那什么是形式参数呢?形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。还来看上面的代码:#include <stdio.h> int add(int x, int y) { return x + y; } int main() { int a = 20; int b = 30; //printf("%d %d", x, y); int ret1 = add(20, 30); printf("%d\n", ret1); int ret2 = add(a, b); printf("%d\n", ret2); int ret3 = add(a + b, a - b); printf("%d\n", ret3); int ret4 = add(add(2, 3), 5); printf("%d\n", ret4); return 0; } 1.形参只在函数中有效:我们如果在函数外部使用函数形参,这是不可行的。2.形参在函数调用过程中才实例化(分配内存单元)函数调用之前,形参还未创建函数调用过程中,形参才被实例化函数调用结束,形参生命周期结束,被销毁三.形参与实参的关系了解了什么是函数的形参和实参,那两者之间有什么关系呢?他们的关系是:形参实例化之后其实相当于实参的一份临时拷贝。这里我们对函数的实参和形参进行分析:我们来尝试写一个函数交换两个整形变量的内容。#include <stdio.h> //实现成函数,但是不能完成任务 void Swap1(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); return 0; } 函数swap1用x,y接收了num1,num2,并把x,y进行了交换,但是我们打印出来的num1,num2并没有交换,为啥呢?这是因为在函数调用时,形参x,y是实参num1,num2的一份临时拷贝,形参和实参并没有建立真正意义上的联系,形参x,y是两个独立的变量,和实参num1,num2分别占用不同的内存空间,在这里,形参和实参只是数值相同罢了.所以,交换形参x,y,并不会对实参num1,num2产生影响!!!在这里,我们如果想要达到交换实参的目的,需要进行传址调用,这个后面我们会详细给大家讲解,现在可以先简单了解一下:所谓传址,就是num1,num2的地址作为参数传过去,当然我们就需要两个整形指针去接收,然后,我们在函数内部,就可以通过这两个指针找到num1,num2,对它们进行交换.接下来,我们就用传址调用的方法来实现一下函数:#include <stdio.h> //正确的版本 void Swap2(int* px, int* py) { int tmp = 0; tmp = *px; *px = *py; *py = tmp; } int main() { int num1 = 1; int num2 = 2; Swap2(&num1, &num2); printf("Swap2::num1 = %d num2 = %d\n", num1, num2); return 0; } 这次,我们就真正达到了交换num1,num2的目的总之,我们最后得到的结论就是,函数实参与形参的关系就是:形参实例化之后其实相当于实参的一份临时拷贝。以上就是对函数参数的介绍,欢迎大家指正!!!
一.什么是语句要学习分支语句和循环语句,首先我们要知道什么是语句。在C语言中,由一个分号隔开的就是一条语句。比如:#include <stdio.h> int main() { printf("hehe\n"); 1 + 2; ;//空语句 return 0; }C语句可分为以下五类:1.表达式语句2.函数调用语句3.控制语句4.复合语句5.空语句本章后面介绍的是控制语句。控制语句用于控制程序的执行流程,以实现程序的各种结构方式(C语言支持三种结构:顺序结构、选择结构、循环结构),它们由特定的语句定义符组成,C语言有九种控制语句。可分成以下三类:(1) 条件判断语句也叫分支语句:if语句、switch语句;(2 )循环执行语句:do while语句、while语句、for语句;(3) 转向语句:break语句、goto语句、continue语句、return语句。二.分支语句(选择结构)生活中处处面临选择,不同的选择就会有不同的结果。如果你好好学习,校招时拿一个好offer,走上人生巅峰。如果你不学习,毕业等于失业,回家卖红薯。这就是选择!!!1.if语句(1)语法和基本使用if语句的语法结构是怎么样的呢?语法结构:#include <stdio.h> int main() { //语法结构: if (表达式) 语句; if (表达式) 语句1; else 语句2; //多分支 if (表达式1) 语句1; else if (表达式2) 语句2; else 语句3; return 0; }当满足不同的条件时(那个表达式的结果为真),就会执行不同的语句。下面我们通过代码来理解这三种分支情况:代码1:#include <stdio.h> //代码1 int main() { int age = 0; scanf("%d", &age); if (age < 18) { printf("未成年\n"); } }解释一下:代码2://代码2 #include <stdio.h> int main() { int age = 0; scanf("%d", &age); if (age < 18) { printf("未成年\n"); } else { printf("成年\n"); } }解释一下:代码3://代码3 #include <stdio.h> int main() { int age = 0; scanf("%d", &age); if (age < 18) { printf("少年\n"); } else if (age >= 18 && age < 30) { printf("青年\n"); } else if (age >= 30 && age < 50) { printf("中年\n"); } else if (age >= 50 && age < 80) { printf("老年\n"); } else { printf("老寿星\n"); } }解释一下:我们说在if语句中,那个表达式的结果为真,则那个语句执行。那么在C语言中如何表示真假?0表示假,非0表示真。再提醒一点:大家思考一下这个代码的结果是啥:#include <stdio.h> //代码4 int main() { int age = 0; scanf("%d", &age); if (age < 18) printf("未成年\n"); printf("不能饮酒\n"); } 输入一个小于18的数:输入一个大于18的数:这是因为:if后面如果不跟{ },默认只能控制一条语句。如果条件成立,要执行多条语句,则应该使用代码块。那什么是代码块呢?#include <stdio.h> int main() { if(表达式) { 语句列表1; } else { 语句列表2; } return 0; } 这里的一对 { } 就是一个代码块。#include <stdio.h> //代码5 int main() { int age = 0; scanf("%d", &age); if (age < 18) { printf("未成年\n"); printf("不能饮酒\n"); } } (2)悬空else思考一下,下面这段代码的结果是啥:#include <stdio.h> int main() { int a = 0; int b = 2; if(a == 1) if(b == 2) printf("hehe\n"); else printf("haha\n"); return 0; } 如果我们不细心的话,可能是这样想的:a的值为0,if(a==1)的结果为假,所以执行else语句,打印haha。那结果是这样吗?为什么啥都没打印?因为else的匹配:else是和它离的最近的if匹配的。也就是说,上面代码中的else是和第二个if匹配的,第一个if的条件表达式为假的话,它后面的那条语句,也就是下一个if语句,自然就不执行了,当然与它匹配的else也就不会执行了,所以什么都没打印。如果想达到我们想的那种效果,可以这样改一下:#include <stdio.h> int main() { int a = 0; int b = 2; if (a == 1) { if (b == 2) { printf("hehe\n"); } } else { printf("haha\n"); } return 0; } 这次的结果,就跟我们想的一样了。所以说:适当的使用{}可以使代码的逻辑更加清楚,代码风格很重要。(3)if书写形式的对比看几段代码://代码1 if (condition) { return x; } return y; //代码2 if(condition) { return x; } else { return y; } 分析一下可以发现这两段代码的结果是一样的,但那个写的好一点呢?第二个更好,它的逻辑更加清晰,更容易看懂:满足condition就return x;不满足就return y;再看两段://代码3 int num = 1; if(num == 5) { printf("hehe\n"); } //代码4 int num = 1; if(5 == num) { printf("hehe\n"); } 这两段哪个好,代码4,因为不容易出错:if中的判断条件写成(5==num)可以避免我们把写成(num=5)而导致出错,因为num=5是把5赋值给num,这样表达式的结果永远为真,该判断就没意义了,但是(5=num)的话,编译器就直接报错了,因为不能把变量赋给一个常量值。2.switch语句(1)语法介绍switch语句也是一种分支语句,常常用于 多分支 的情况。比如:输入1,输出星期一输入2,输出星期二输入3,输出星期三输入4,输出星期四输入5,输出星期五输入6,输出星期六输入7,输出星期日那我写成 if…else if …else if 的形式太复杂,那我们就得有不一样的语法形式。这时就可以使用switch 语句。语法:switch(整型表达式){语句项;}而语句项是什么呢?是一些case语句:如下:case 整形常量表达式:语句;switch语句后面的整型表达式的值与哪一个case对应的表达式的值结果一样,就会进入那个case语句(2) switch语句中的 break在switch语句中,我们没办法直接实现分支,搭配break使用才能实现真正的分支。就比如上面那个例子:#include <stdio.h> int main() { int day = 0; scanf("%d", &day); switch (day) { case 1: printf("星期一\n"); case 2: printf("星期二\n"); case 3: printf("星期三\n"); case 4: printf("星期四\n"); case 5: printf("星期五\n"); case 6: printf("星期六\n"); case 7: printf("星期天\n"); } return 0; } 运行这个代码,是不是,输入几就打印星期几呢?为什么是这样,因为语法规定的是:switch后面的整型表达式与哪一个case后面表达式结果一样,就从哪个case语句开始执行,执行完若无break,则继续向下执行,遇到break跳出。所以,搭配break使用才能实现真正的分支。修改代码#include <stdio.h> int main() { int day = 0; scanf("%d", &day); switch (day) { case 1: printf("星期一\n"); break; case 2: printf("星期二\n"); break; case 3: printf("星期三\n"); break; case 4: printf("星期四\n"); break; case 5: printf("星期五\n"); break; case 6: printf("星期六\n"); break; case 7: printf("星期天\n"); break; } return 0; } 这次就达到我们想要的效果了。有时候我们的需求变了:输入1-5,输出的是“weekday”;输入6-7,输出“weekend”所以我们的代码就应该这样实现了:#include <stdio.h> //switch代码演示 int main() { int day = 0; scanf("%d", &day); switch (day) { case 1: case 2: case 3: case 4: case 5: printf("weekday\n"); break; case 6: case 7: printf("weekend\n"); break; } return 0; } 这时。我们运行代码:输入1-5,输出的是“weekday”;输入6-7,输出“weekend”。break语句 的实际效果是把语句列表划分为不同的分支部分。这就是break在switch语句中的作用。编程好习惯在最后一个 case 语句的后面加上一条 break语句。(3)default子句1.如果表达的值与所有的case标签的值都不匹配怎么办?其实也没什么,结果就是所有的语句都被跳过而已。我们试一下,还是上面那段代码:程序并不会终止,也不会报错,因为这种情况在C中并不认为是个错误。2.但是,如果你并不想忽略不匹配所有标签的表达式的值时该怎么办呢?你可以在语句列表中增加一条default子句:default:当 switch 表达式的值并不匹配所有 case 标签的值时,这个 default 子句后面的语句就会执行。我们在上面的代码中添加default子句:#include <stdio.h> //switch代码演示 int main() { int day = 0; scanf("%d", &day); switch (day) { case 1: case 2: case 3: case 4: case 5: printf("weekday\n"); break; case 6: case 7: printf("weekend\n"); break; default: printf("输入错误"); break; } return 0; } 再输入一个与所有的case标签的值都不匹配:所以,每个switch语句中只能出现一条default子句。但是它可以出现在语句列表的任何位置,而且语句流会像执行一个case标签一样执行default子句。编程好习惯在每个 switch 语句中都放一条default子句是个好习惯,甚至可以在后边再加一个 break 。以上就是对C语言中分支语句的介绍!!!
2023年09月
2023年08月
2023年05月