C语言学习笔记 这篇笔记不是C语言的学习教程,不是书上能看到的,也不是网上的教学视频会讲的,全是对C语言的个人学习的深入思考与总结,全是脑洞与干货,每一个问题的解决和结论的得出都基于代码实现的验证…… 很多人说C语言中一切都是指针,我说:C语言中一切都是地址。我们看不见数据的内存地址,但是通过给特定的地址起个名字,我们就具备了操作这个地址的能力。
C语言数据类型 变量作用域与内存区间
内存空间分配图 一般情况下内存的动态存储区地址分配是从大到小的,静态存储区地址分配是从小到大的。 常量区间中只有”值”(数据),没有”名”,值唯一而不重复,每一个特定的值占据一个常量空间。 所有变量共有3个作用域(简称域),全局域,主函数域,自定义函数域。 3个不同的域之间允许出现同名变量;(但因为同名变量的内存地址不同,其实是不同的变量,类似于同姓名的人,但不是一个人,对吧? 全局域因为不存在子域,所以域内部不存在同名变量;主函数域与自定义函数域因为可能存在子域,所以域内部可能出现同名变量(同理,也不是同一个变量!) 全局域中,定义的不同变量,依次在全局内存区间占据一个单位的内存空间; 主函数域的子域的变量占据主函数区间首地址后的1个单位内存空间(不管是并列子域还是嵌套子域,依照声明依次分配地址); 自定义函数域的子域的变量占据自定义函数区间首地址后的1个单位内存空间(不管是并列子域还是嵌套子域,依照声明依次分配地址); 自定义函数中的嵌套自定义函数(子函数)地址分配的条件: 与父函数的首地址相差48字节,但当这个地址与父函数的尾地址相差不大于32字节时,向后移动16字节; 全局域是主函数域和自定义函数域的父域,也可以说是整个程序中所有域的根域;主函数域是所有主函数域中子域(包含嵌套的子域)的父域;自定义函数域是所有本自定义函数域中子域(包含嵌套的子域)的父域; 变量取值时,如果域内没有声明,则取父域中的同名变量,如果父域没有声明,则继续往上取,直至根域; 所有自定义函数域的变量是共用的一个自定义函数区间,自定义函数的变量不会被销毁(值保留),子函数(包含嵌套子函数)的变量会被销毁(值清空)。 ……上面的信息量有点大,能理解多少是多少吧…… Const 修饰符 Const通常有2种用法:1.用来修饰一个声明的变量,= read only, 变量数据在任何情况下不能更改; 2.用来修饰一个自定义函数的形式参数,通常与指针型参数连用(指针型参数相当危险,可以直接修改MAIN函数变量),表示这个自定义函数不能对传递的实参做任何的更改。 STATIC(静态)变量 用static修饰申明变量时作用域不变 static并不会影响变量的作用域;存储区间改变 static使变量保存在内存的静态存储区间;全局变量本身就在静态存储区间,所以static不会改变全局变量地址;静态存储区间的变量在整个程序执行过程中一直存在,不会被销毁;因为不会被销毁,所以将再次申明这个静态变量的时候,程序会直接指向原有变量,再次使用这个变量时能继承这个变量原有的值。 用法:因为上述的特性,一般来说,static常用于修饰自定义函数变量(自定义函数不断调用的应用场景),而用来修饰全局变量或者主函数变量时,你并不能感觉到static的作用。 函数的参数 参数的作用:让特定自定义函数外部的变量能够被这个自定义函数所使用。所谓的特定自定义函数外部的变量包括:全局变量、主函数变量、其他自定义函数的变量。 个人的想法:参数的真正存在理由其实是为了打破变量作用域的限制,把全局变量作为参数传递给自定义函数的做法其实是有点自找麻烦的感觉,因为全局变量作用域本身就涵盖了整个C文件,既然可以直接拿来用,干嘛还要传递? 参数如何传递变量值的2种方式:复制 以外部变量名作为参数,将外部变量的值赋给自定义函数变量;自定义函数对自身变量的运算不会改变外部变量的值;引用 以外部变量地址作为参数,自定义函数通过引用外部变量的地址直接操作外部变量进行运算。如果不想改变外部变量本身的值,可以用const修饰参数。 数组参数只有引用一种传递方式.为什么?可以这么简单的来理解这个事儿: 当调用自定义函数时,实参是输入的数组名,但数组名其实就是数组首地址,等于引用的方式。 数组之间为什么不能直接赋值? 数组之间的赋值是不能直接用=号的,因为数组名是这个数据所占内存空间的首地址,是一个常量,是不能被改变的。以字符串数组为例: 比如:char names1[5]=”jack”; Char names2[5]=”son”; 那么:names1=names2 是错误的,从程序语法上来讲,names1和names2都是const char* 类型,是不能被再赋值的;从内存操作上来理解:这句话相当于是让2个不同的数组占据同一个内存地址开始的内存空间,这是绝对不可能的。 同理,任意类型的数组都是不能直接用=号相互赋值的,特殊的:C语言提供了字符串数组的复制函数,所以能直接调用实现。 计算机内存数据输入的步骤 计算机内存输入数据的过程: 第一步:扫描并缓存 扫描输入设备(通常是键盘、文本),得到输入数据并放入内存输入缓冲区,标识为可用状态(当然,可用状态的输入数据可能不止1个,而是有N个);
输入缓冲区示意图 第二步:赋值并擦除 将输入缓冲区数据赋予变量,然后数据将被标识为不可用(当然,你可以理解为输入数据被擦除,也是没有问题的)。关键的:计算机并不能选择特定的某一个输入缓冲区可用数据进行赋值,计算机总是选择时间戳最小的(最先进入缓冲区)的这个数据进行赋值。 SCANF函数是如何操作内存的—–用(nue)它(wo)千万次,一朝见真容 搞懂了缓冲区原理,现在来看看scanf(“%d”,num)这句话,这是C语言中最常见最普通的一个赋值语句,那么这句话是怎么操作内存的?语句开始运行的时候分2种情况:当输入缓冲区中存在可用状态的输入数据时,直接执行第二步,越过了从输入设备扫描输入数据的过程;当输入缓冲区不存在可用状态的输入数据时,从第一步开始执行。当进行到第二步的时候又分为2种情况:当输入数据的类型是语句中想要得到的数据类型(案例中为整型)时,执行第二步;当输入数据的类型是语句中不是想要得到的数据类型(比如案例中输入一个字符型)时,第二步并不会被执行,缓冲区数据被保留,然后开始执行scanf语句的下一条语句。 通过下面的几行代码和运行结果可以验证这个过程:
提示:因为缓冲区存在字符a,第二个scanf语句的扫描步骤被越过了 Scanf(“%*s”);和 fflush(stdin)的区别 这2句话都是用来清除输入缓冲区的,这么说对吗? 对的! 这2句话的实现原理是不一样的,这么说对吗?也是对的! 先打个比方:格式化一个盘符和删除这个盘符里面所有的数据的作用是一样的吗?当然是一样的对吧。但是格式化和删除这2个动作的区别在哪?无论这个盘符里面有没有东西,你都可以直接执行格式化对吧;而如果要执行删除动作,当这个盘符里面没有东西给你删的时候怎么办?但是你又必须完成删除这个动作,好吧,那我就先丢个东西进来再删吧。 Scanf(“%*s”):删除缓冲区数据,缓冲区没有数据?对不起,请你先输入一个数据! fflush(stdin):格式化缓冲区,管你缓冲区有没有数据,格式化! 可以用下面的代码测试这2句话的区别,交换使用者2句话,观察程序运行结果,打开你的脑洞,结合上面我讲的SCANF语句的内存执行过程,多思考。
提示:当输入不是整型的时,比如输入一个字符a的时候,使用这2句话程序的运行结果是没有区别的;但是当输入非1-3之间的数字时,就能看出来区别了。 补充的:当输入缓冲区中存在不同类型的数据时,会造成程序BUG,so: 连续两次不同类型的输入之间,一定要记得清空输入缓冲区. If和else if用法 if…if… ,2个if之间是独立的关系,互不干涉,同理适用于N个if连用的情况,计算机会执行所有的条件判断语句; if…else if… ,if和else if之间是互斥的关系,怎么理解?如果if条件成立,语句执行,那么else if就不会执行,同理适用于if后面N个else if 的情况,能提高程序运行效率;如果程序执行到else if,那么说明之前的if条件不成立,同理适用于前面还有N个else if的情况,说明前面的条件都不成立。暗含隐藏条件,能简化条件判断语句,特别适用于区间值的判断。例如下面的2段代码是等价的: If(num>100)… If(num>10&&num<100)… If(num>100)… Else if(num>10)… 递归函数 递归函数几乎都可以用嵌套循环来实现,但是为什么还要用递归?递归可以避开循环变量边界的问题;递归可以忽略具体的运算细节。 所以在特定的应用场景下,递归是非常简洁的实现方法。 字符串数组 字符串数组是特殊的数组,以\0为结尾标志,\0不会输出,但是占据一个内存字节. 有用的:只有当字符串独立存在(不赋予给数组)的时候才会作为字符串常量占据常量内存空间; 比如:printf(“%s\n”,“abc”); printf(“%p\n”,“abc”); char * ptr=”abc”;当字符串出现的时候处于赋值语句中(不管是=号赋值,还是strcpy函数赋值),即非独立存在的,那么字符串直接存入了对应数组所处的变量空间,不会占据常量内存空间。以下为验证代码:
补充的:为什么在字符串数据申明之后,不能再用=号直接赋值? 因为申明完成之后,数组名是一个地址,字符是不能直接赋给一个地址的。为什么2个字符串数组即使长度相同,也不能用=号直接赋值? 太简单了,地址1=地址2 是什么意思? 是企图让内存里面的2个不同的内存单具有相同地址的意思,这可能吗? 引申的:结构体.字符串数组名不能直接=字符串 字符串和字符指针的赋值方式
字符串5种赋值方式 1.字符串数组有5种赋值方式 (1)strcpy常用于从另外一个字符串数组中复制字符串; (2)Gets是最常用的动态赋值方式,Fgets的好处是能截取字符串,副作用是使输入缓冲区残留可用数据,影响后续赋值语句,需要与fflush(stdio)连用; (3)5种方式的字符串都保存在栈区中。 2.字符指针有6种赋值方式
字符指针6种赋值方式 STRUCT结构体 为什么需要结构体 C语言中的默认数据类型是固定的,但是用这些数据类型描述所有的事物是不够的,或者说是不够方便的。这个时候我们就需要更多的数据类型。 结构体的本质 结构体实际上就是自定义数据类型,当它被定义之后,它就是一种数据类型,可以用来声明变量和指针,与默认数据类型的使用方式并没有区别。 结构体指针 当存在嵌套结构体的时候,子结构体变量的赋值和输出通常比较繁琐,通过声明直接指向子结构体的指针可以很好的简化程序语句。当结构体作为自定义函数参数的时候,最好使用结构体指针作为参数传递,因为大多数对结构体的变量操作是要返回变更的,如果不返回,可以使用const修饰。 补充的:结构体变量之间可以用=号直接赋值。 附录
常用内置函数
补充内置函数 Rename(char*,char*) stdlib.h 重命名文件并改变存放路径 Mkdir(char*) direct.h 创建文件夹,只能创建1级
Fopen(char*, r\w\a) stdio.h 带权限打开一个文件夹,与文件指针连用 常用WINDOWS库函数 设置控制台标题 void setTitle(char* title) { SetConsoleTitle(title); } 设置前景与背景色 void setColor(int fore,int back) { HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleTextAttribute(handle,fore + back * 0x10); } 设置光标位置 void setPosition(int x,int y) { HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); COORD coord={x,y}; SetConsoleCursorPosition(handle,coord); } System函数调用DOS命令
实例
system(“dir H:\\PDF /B > H:\\name0.txt”); //识别PDF文件夹中的文件名并复制到新建的name0.txt.
2024最新激活全家桶教程,稳定运行到2099年,请移步至置顶文章:https://sigusoft.com/99576.html
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。 文章由激活谷谷主-小谷整理,转载请注明出处:https://sigusoft.com/82694.html