c语言复习的笔记

c语言作为我大学学习的第一门编程语言,影响深刻。但之前开设课程时正处于排课最多的阶段,我没能深入了解好很多技术细节。想起复习一下c语言的原因,这段时间我想阅读unix源码,unix便是由c语言编写而成的,日后我在学习、工作上会经常接触linux,这是其一;我的专业是信息安全,会碰到很多关于缓冲区的漏洞,学习c语言I/O与缓冲区的关系能够加深我对于漏洞产生原理的理解,并且作为面向程序的编程语言,c语言能够灵活使用指针,我希望我能对底层的知识有更多的理解。
大一学习c语言使用的是谭浩强的红皮书,这次复习我购买了C primer plus一书,这篇文行仅是我对于阅读完全书之后的思考感悟,我挑出感兴趣的,新学会的知识来记录。

一、第八章:字符输入输出和输入验证

1.缓冲区
老的系统没有缓冲区,当我们输入字符后会立即打印,很多时候会重复打印,这是无缓冲(buffer)输入;而目前的系统,我们按下回车之前不会重复打印刚输入的字符,这是缓冲输入。
缓冲分为:完全缓冲I/O、行缓冲I/O
·完全缓冲I/O:当缓冲区满后把其中内容输出,常见大小512字节和4096字节
·行缓冲I/O:出现换行符比如回车的时候,输出内容

阅读书中我看到了这么一句话“ANSI C和后续的C标准规定是缓冲的”,刷B站视频我记得win7以及之前记事本默认编码为ANSI,win10为UTF-8。还有说道缓冲区,我想到了C语言令人担心的一点,如果程序员不对变量初始化赋值,或者修改某个变量大小后没初始化,很有可能给程埋下暗雷–即未初始化会使用内存中的脏数据。

之前学习C语言没有了解好文件、流和键盘输入的关系,现在好好学习。首先,我们了解一些概念:
·直接调用操作系统的函数称为:底层I/O(low-level I/O)
·C通过标准I/O包(standard I/Opackage)来处理文件

流:
C处理的是流(stream)而不是直接处理文件。流,是一个实际输入或输出映射的数据流。让不同属性不同种类的输入由属性更加统一的流来表示。
对于C,处理键盘输入输出是以处理文件的方式进行的

文件结尾:
以前没接触这个字典,计算机操作系统要判断文件的开始和结束,可以放置一些标志,比如内嵌的CTRL+Z字符用来标记文件结尾。在文件中长这样:“^z
如今不一定使用内嵌的ctrl+Z,可能用存储文件大小的方式,但是如果用了ctrl+Z,那操作系统就会认为是一个文件结尾的标记。引起我思考,有没有某种攻击方式能用上这个特性,比如某种截断攻击。任意文件上传,即用截断方式绕过文件检查程序,类似这样的思路展开,也许会有收获。

书中说,不管操作系统以何种方式检查文件结尾,如果使用getchar()函数检测到结尾会返回
EOF(end of file),同样的scanf()检测到文件结尾也会返回EOF。他们的定义会在stdio.h文件中,当我们 #include<stdio.h> 会有对EOF的定义: #define EOF (-1) //因为一般getchar()返回0~255,不会出现-1

因此,我们可以对比getchar()的返回值与EOF的关系,不相同就是没有到达文件结尾,如:
while((ch=getchar())!=EOF) //表示还没有结束时,在Java里是:
import java.util.Scanner;
<T> num=input.next<T>();

重定向和文件
标准输入输出stdin、stdout流。C程序使用标准I/O包查找标准输入作为输入源。程序可以通过两种方式使用文件:
①用函数打开文件、关闭文件、读取文件、写入文件;
②与键盘和屏幕互动的程序,通过不同的渠道重定向输入至文件和从文件输出。
不同系统(使用c开发的UNIX、LINUX\DOS)中重定向,都可以让程序使用文件而不是键盘来输入,重定向输出让程序输出到文件而不是屏幕。
·unix命令行模式
·linux(ditto)
·windows命令提示符
重定向符号:“<” 用法:./echo.exe <words //将words中的字符重定向输入到echo.exe中,直到文件结尾。
标准提示符:“$” 是unix和linux的标准提示符;“A>”或“C>”是windows和DOS中的。
重定向输出:“>” 用法:./echo.exe >mywords //创建了myword用于把echo.exe中的字符重定向输出到mywords,如果原本存在mywords,会覆盖原始数据。
组合重定向:使用“<”、“>”例如,创建mywords的副本temp:./echo.exe <mywords >temp

重定向运算符连接一个可执行程序和一个数据文件,不能用于连接两个数据文件;不能读取多个文件输入也不能输出至多个文件
追加之末尾的连接运算符“>>”:表示把数据追加到现有文件的末尾;
管道“|”:表示前者的结果为后者的输入;

在8.5.1使用缓冲输入这个内容里,我注意到:
while(getchar() != ‘\n’)
continue; //因为当输入y或者n 之后我们会输入回车来换行,但是这也意味着会输入一个‘\n’,程序会进行多的判断,利用这条代码会再识别y之后终止本次循环,也就是说跳过本次输入字符串中y后面的部分,包括那个’\n‘

混合数值和字符输入:
注意到getchar()会读取每一个字符,包括空格,制表符等,但是scanf()读取数字时会跳过制表符、空格和换行!
在书中例子提醒我们,使用getchar()和scanf()混用,在输入数字后我们按回车换行,尽管scanf()没有处理它,但是这个换行符仍然在输入队列,当下一次循环whlie检查到这个换行符,就会跳出循环。在本条例子中我注意到使用了break语句,用来检测scanf()的执行情况,查询scanf()的返回值,如下图:

如果是int ch=scanf(“%d %d %d”,&a,&b,&c); 输入 1 1 s ,那么会ch会是2,因为只有前两个被正确接收。

二、第九章:函数

尾递归:相当于循环,是最简单的递归。那么当循环和递归都能解决问题的时候,应该优先选择循环,因为每次递归都会创建一组变量,因此递归使用的内存会很多,执行速度会慢。

查找地址:&运算符
假设一个变量名为sigma,那么&sigma是这个变量的地址。

指针:pointer,是c语言最重要的概念,用来存储变量的地址

间接运算符:*
假设一个指针名或者地址名为sigma,那么*sigma表示存储在指针指向地址上的值。
通常声明指针时用空格,如:int * pi; //这是一个指向int变量的指针叫做pi

三、第十章:数组

·sizeof运算符给出它的运算对象的大小(以字节为单位)

指定初始化器(c99):
这是以前没听过的用法。现在复习我最大的感悟就是初始化的重要性,如果不初始化用的是脏数据,如果初始化了,其他会被设置成0.可以使用“int array[6]={[5]=233}”来对下标为5的元素初始化其他元素会被初始化为0.注意到,如果出现“{1,2,3,[1]=1}”,那么会把下表为1的元素的值改成1覆盖掉原来的2.

数组边界问题:
以前学习数据结构、操作系统的时候,有时粗心没有考虑好边界。跑数据的时候就出现越界的问题,在生产中这是十分严重的。c语言相信程序员,我们如果担心自己写错导致越界,应该把size写死然后再在循环中引用,比如:#define size 4;……;for(int i=0;i<size;i++){…}

指针和数组的关系:
数组表示法其实是变相使用指针,举个例子,假设一个数组叫做sigma,包含的元素为{1,2,3,4}那么数组名sigma是数组中第一个变量sigma[0]也就是1的地址。相当于:
sigma==&sigma[0];
指针+1:表示指针的值递增它所指向类型的大小(以字节为单位),换成人话就是:*sigma指向1,则
*(sigma+1)指向2,*(sigma+2)指向3
.注意*运算符优先级比+要高,sigma[2]可以表示为*(sigma+2)

函数、数组、指针的关系:
学习c语言一定要理清楚指针的概念。如果我们想用函数处理数组比如total=sum(sigma)注意到数组sigma是首元素的地址,我们要传给一个指针形式的参数!比如int sum(* pointer); //定义函数时的形参为指针
书中提示我们要注意:只有在函数原型或者函数定义头中,才能使用int sigma[]代替*sigma

书中使用两个指针指明数组开始与结束的时候,#define size 10;引用函数的时候使用如下代码:
answer=sum(sigma,sigma+size) //按道理说sigma+size将会是指向sigma[10]而sigma最后一个元素是sigma[9],指针越界了。书本给出的解释是:C保证给数组分配空间时,指向数组最后面元素后的下一个位置的指针仍然是有效的指针。也就是说*(sigma+10)也是有效的。。。。

为了记住*与++的优先级关系,举出下面例子:
total +=*start++ //*和++的优先级相同,但是结合律是从右往左看,也就是*(start++)

指针着实是一个十分敏感的东西,用的好速度极快,用不好极容易造成大问题。书本提醒:千万不要解引用未初始化的指针!
int * sigma;
*sigma=5; //出大问题,尽管是将5赋值给sigma,但是会造成擦除原来存储的数据!

因为创建一个指针的时候,系统只分配了存储指针本身的内存,并没有分配存储数据的内存!我们可以用已分配的地址初始化它,比如说用现有的某个变量的地址初始化它!或者使用malloc()函数先分配内存
看到这里,解开了我这些年关于为什么要使用malloc()的疑问!

在b站看到一个up主的视频,我一下子又更理解了一点。当我们int a=3;计算机会给变量开辟一个空间,假设为0xa0,也就是在这个地址里存着一个数值3,当我们int * pi;也会给指针开辟空间&pi为0xb0,;那么当我们 *pi=&a;相当于是0xb0这个地址上存者0xa0这个地址。

对于数据的保护:
对形式参数使用const。如果函数的意图不是修改数组中的数据内容,就在函数原型和函数定义中声明形式参数时使用const,比如:int sum(const int ar[],int n);

之前上课的时候,讲到指向指针的指针很多多同学表示不理解,我开始也不太明白。假设有一个二维数组:sigma[3][2]={{4,2},{2,3},{6,5}} //sigma[2][1]=5 相当于*(*(sigma+2)+1)=5

四、第十一章:字符串以及字符串函数

书中强调对数据初始化的重要性。
关于字符串数组和初始化:
·如果一个字符串的末尾没有空字符”\0“那么他就不是字符串而是字符数组。所以我们要确保在指定数组大小的时候,数组元素的个数至少比字符串长度多1(为了容纳空字符),没被使用的元素会被初始化为”\0“

在数组和指针区别时,注意到:
char * word=”sigma”;
word[o]=”h”;
printf(“sigma”); 会把所有simga实例都修改,这会造成严重后果。于是在把指针初始化为字符串的时候,应该使用const限定符:
const char * word=“sigma”;
如果打算修改字符串,就不要用指针指向字符串字面量

讲到字符串数组时,对比char * sigma[lim]=和char higms[lim][size],higma数组的数据是被存储了两次的,并且higma每行长度固定,sigma则是不规则的不固定的,使用起来sigma比higma高效得多。如果要用数组表示一些列待显示的字符串,应该使用指针数组

在字符串的输出:我们注意要把一个字符串读入程序,就要给该字符串留出空间,再输入该字符串。
char * name;
scanf(“%s,name”); //这是不对的,name没有初始化很容易擦除别的数据,可以写成name[81]

读取时,scanf()和转换说明%s只允许读取一个单词,gets()可以读取整行输入直到遇到换行符。但是注意到,gets()函数只能接受一个参数,他不能检查还能否装得下输入行也就是说函数只知道数组的开始,不知道数组的长度(有多少个数据)。很容易造成缓冲区溢出
既然gets()有些危险,人们使用fgets()来替代它。下面是fgets()和fputs():
·fgets():有三个参数,第二个参数限制读入字符最大数量,如果该参数为n,那么函数会读入n-1个字符,或者遇到换行符,而与gets()不同,fgets()读到换行符的时候会保存在字符串中
·fgets()的第三个参数,要指明要读入的文件。如果是要从键盘输入,入参就要写:stdin
·一般fgets()与fputs()组合使用,fputs()有两个参数,第二个参数指明要写入的文件,如果在屏幕输出,第二个参数要写:stdout
·fputs()在输出字符串之后不会添加换行符

fgets()存储换行符:
·坏处:你并不想存储这些换行符
·好处:对于存储的字符串而言,检查末尾是否有换行符可以判断是否读取了一整行。

空指针和空字符:
空字符时\0是一个字符,占一个字节
空指针是地址,占4个字节

gets_s():这个函数以前没接触过,书上说该函数只接受标准输入,所以比起fgets()函数不需要第三个参数,但是gets_s会丢弃换行符。

字符串函数:
字符串函数的原型在string.h头文件中,常见有:strlen(),strcat(),strcmp(),strncmp(),strcpy(),strncopy()
而在stdio.h中有sprintf()函数

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注