一、字符串函数
我们经常要处理字符和字符串,为了⽅便操作字符和字符串,C语⾔标准库中提供了⼀系列库函数,接下来我们就学习⼀下这些函数。
1、字符分类函数
字符分类函数就是用来判断字符类型的函数。
2、字符转换函数
2.1 toupper
int toupper(int ch);
将传进函数的小写字符转换成大写并返回。
2.2 tolower
int tolower(int ch);
将传进函数的大写字符转换成小写并返回。
2.3 atoi
int atoi(const char*);
将传进去的字符串解读成整数。开头的空格会被跳过,直到遇到+、- 和数字开始解读。开始解读后,遇到非数字字符就会返回当前得到的数字。
3、strlen
• 字符串以 '\0' 作为结束标志,strlen函数返回的是在字符串中 '\0' 前⾯出现的字符个数(不包 含 '\0' )。 • 参数指向的字符串必须要以 '\0' 结束。 • 注意函数的返回值为size_t,是⽆符号的( 易错 ) • strlen的使⽤需要包含头⽂件<string.h>
4、strcpy 与 strncpy
4.1 strcpy
• 将指向的源C字符串复制到指向的目标数组中,包括终止的空字符(并在那一点停止)。 • 源字符串必须以 '\0' 结束。 • 会将源字符串中的 '\0' 拷⻉到⽬标空间。 • ⽬标空间必须⾜够⼤,以确保能存放源字符串。 • ⽬标空间必须可修改。4.2 strncpy
•将源C字符串的前num个字符复制到目标数组中。如果在复制num个字符之前找到源C字符串的结尾(由空字符标志),则将目标数组填充为零,直到总共写入了num个字符为止。 • 拷⻉num个字符从源字符串到⽬标空间。 • 如果源字符串的⻓度⼩于num,则拷⻉完源字符串之后,在⽬标的后边追加0,直到num个。5、strcat 与 strncat
5.1 strcat
• 将源字符串的副本附加到目标字符串。目标字符串中的终止空字符被源字符串的第一个字符覆盖,并在目标字符串中包含一个空字符,以表示由两者连接而成的新字符串的结尾。 • 源字符串必须以 '\0' 结束。 • ⽬标字符串中也得有 \0 ,否则没办法知道追加从哪⾥开始。 • ⽬标空间必须有⾜够的⼤,能容纳下源字符串的内容。 • ⽬标空间必须可修改。5.2 strncat
• 将source指向字符串的前num个字符追加到destination指向的字符串末尾,再追加⼀个 \0 字符。 • 如果source 指向的字符串的⻓度⼩于num的时候,只会将字符串中到 \0 的内容追加到destination指向的字符串末尾。6、strcmp 与 strncmp
6.1 strcmp
•此函数开始比较每个字符串的第一个字符。如果它们相等,它将继续比较后续字符对,直到字符不同或直到达到终止空字符。 • 标准规定: ◦ 第⼀个字符串⼤于第⼆个字符串,则返回⼤于0的数字 ◦ 第⼀个字符串等于第⼆个字符串,则返回0 ◦ 第⼀个字符串⼩于第⼆个字符串,则返回⼩于0的数字 ◦ 那么如何判断两个字符串? ⽐较两个字符串中对应位置上字符ASCII码值的⼤⼩。6.2 strncmp
• ⽐较str1和str2的前num个字符,如果相等就继续往后⽐较,最多⽐较num个字⺟,如果提前发现不⼀ 样,就提前结束,⼤的字符所在的字符串⼤于另外⼀个。如果num个字符都相等,就是相等返回0.7、strchr 与 strstr
7.1 strchr
strchr函数用于在一个字符串中查找特定字符的第一次出现,并返回该字符的指针。如果没有找到,则返回NULL。
7.2 strstr
strstr函数用于在一个字符串中查找特定子串的第一次出现,并返回该子串的指针。其中,str1为要查找的字符串,str2为要查找的子串。如果找到了该子串,则返回该子串的指针。如果没有找到,则返回NULL。
8、strtok
• sep参数指向⼀个字符串,定义了⽤作分隔符的字符集合 • 第⼀个参数指定⼀个字符串,它包含了0个或者多个由sep字符串中⼀个或者多个分隔符分割的标 记。 • strtok函数找到str中的下⼀个标记,并将其⽤ \0 结尾,返回⼀个指向这个标记的指针。(注: strtok函数会改变被操作的字符串,所以在使⽤strtok函数切分的字符串⼀般都是临时拷⻉的内容 并且可修改。) • strtok函数的第⼀个参数不为NULL ,函数将找到str中第⼀个标记,strtok函数将保存它在字符串 中的位置。 • strtok函数的第⼀个参数为 NULL ,函数将在同⼀个字符串中被保存的位置开始,查找下⼀个标 记。 • 如果字符串中不存在更多的标记,则返回 NULL 指针。二、数据的存储
1、整数的存储
整数的2进制表⽰⽅法有三种,即 原码、反码和补码。三种表⽰⽅法均有符号位和数值位两部分,符号位都是⽤0表⽰“正”,⽤1表⽰“负”,⽽位最⾼位的⼀位是被当做符号位,剩余的都是数值位。 正整数的原、反、补码都相同;负整数的三种表⽰⽅法各不相同。 原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。 反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。 补码:反码+1就得到补码。 对于整形来说:数据存放内存中其实存放的是补码。 为什么呢? 在计算机系统中,数值⼀律⽤补码来表⽰和存储。原因在于,使⽤补码,可以将符号位和数值域统⼀处理;同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。2、大小端字节序
在调试的时候,我们看到内存中a的数据从低到高似乎和我们赋值时的顺序是相反的。这是为什么呢? 这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着⼀个字节,⼀个字节为8 bit 位,但是在C语⾔中除了8 bit 的 char 之外,还有16 bit 的 short 型,32 bit 的 long 型(要看具体的编译器),另外,对于位数⼤于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度⼤于⼀个字节,那么必然存在着⼀个如何将多个字节安排的问题。因此就导致了⼤端存储模式和⼩端存储模式。
⼤端(存储)模式:是指数据的低位字节内容保存在内存的⾼地址处,⽽数 据的⾼位字节内容,保存在内存的低地址处。
⼩端(存储)模式:是指数据的低位字节内容保存在内存的低地址处,⽽数据的⾼位字节内容,保存 在内存的⾼地址处。
3、浮点数的存储
3.1 浮点数的表示
上图中当n为整数9时以%d形式打印出来9,以%f的形式打印出来0。当n为小数9.0时,以%d形式打印出来一个极大数,以%f的形式打印出来9.000。 上⾯的代码中,num 和 *pFloat 在内存中明明是同⼀个数,为什么浮点数和整数的解读结果会差别 这么⼤?这似乎说明整数和浮点数在内存中的存储方式不一样。
我们都知道一个十进制的数可以用一个大于等于1小于10的数乘以10的n次幂来表示。那么二进制其实也一样。
根据国际标准IEEE(电⽓和电⼦⼯程协会) 754,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式: • (−1)^S 表⽰符号位,当S=0,V为正数;当S=1,V为负数 • M 表⽰有效数字,M是⼤于等于1,⼩于2的 • 2 ^E 表⽰指数位举例来说: ⼗进制的5.0,写成⼆进制是 101.0 ,相当于 1.01×2^2 。 那么,按照上⾯V的格式,可以得出S=0,M=1.01,E=2。 ⼗进制的-5.0,写成⼆进制是 -101.0 ,相当于 -1.01×2^2 。那么,S=1,M=1.01,E=2。 IEEE 754规定:
对于32位的浮点数,最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位的浮点数,最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M3.2 浮点数存的过程
IEEE 754 对有效数字M和指数E,还有⼀些特别规定。 前⾯说过, 1 ≤ M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中 xxxxxx 表⽰⼩数部分。 IEEE 754 规定,在计算机内部保存M时,默认这个数的第⼀位总是1,因此可以被舍去,只保存后⾯的 xxxxxx部分。⽐如保存1.01的时候,只保存01,等到读取的时候,再把第⼀位的1加上去。这样做的⽬的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第⼀位的1舍去以后,等于可以保存24位有效数字。 ⾄于指数E,情况就⽐较复杂 ⾸先,E为⼀个⽆符号整数(unsigned int)这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存⼊内存时E的真实值必须再加上⼀个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。⽐如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。3.3 浮点数取的过程
指数E从内存中取出还可以再分成三种情况: E不全为0或不全为1: 这时,浮点数就采⽤下⾯的规则表⽰,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第⼀位的1。 ⽐如:0.5 的⼆进制形式为0.1,由于规定正数部分必须为1,即将⼩数点右移1位,1.0*2^(-1),其阶码为-1+127(中间值)=126,表⽰为01111110,⽽尾数1.0去掉整数部分为0,补⻬0到23位 00000000000000000000000,则其⼆进制表⽰形式为: 1 0 01111110 00000000000000000000000 E全为0: 这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第⼀位的1,⽽是还原为0.xxxxxx的⼩数。这样做是为了表⽰±0,以及接近于0的很⼩的数字。 1 0 00000000 00100000000000000000000 E全为1: 这时,如果有效数字M全为0,表⽰±⽆穷⼤(正负取决于符号位s); 1 0 11111111 00010000000000000000000三、自定义类型
1、结构体类型
有些时候很难用一个变量将一个事物比如一个人描述清楚,这个时候就要多个变量。但如果只是用多个变量但不用结构体定义的类型将这些变量串联在一起的话,那么描述这一个人各方面的变量很难统一管理。
1.1 变量的创建与声明
可以用typedef重命名,创建变量时方便一些。
在创建变量时进行初始化可以按顺序输入数据,跟声明时的顺序一 一对应。也可以用a2的初始化来对指定成员初始化。
<—也可在声明时创建并初始化变量(不可与typedef同时出现)。
1.2 匿名结构体
结构体在声明时可以选择不给名字,只用这个结构创建几个变量,之后不会再是使用这个类型创建变量了。就可以选择匿名。
1.3 结构体自引用
有没有可能结构体的成员包含自己呢?
上图中成员中包含自己,那么结构体大小到底是多少呢?根本没法求,就不可能创建出变量了正确的方式是在成员中使用指向自己的指针。指针的大小是确定的。
注意:在自引用时只能用全名,也就是 struct 类型名 。匿名和typedef出的名字都没法自引用。
1.4 结构体的内存对齐
结构体存在内存对齐大约有两个原因。
1. 平台原因 (移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 2. 性能原因: 数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。 总体来说:结构体的内存对⻬是拿空间来换取时间的做法对齐规则: 1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处 2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。 对⻬数= 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。
- VS 中默认的值为 8 - Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩ 3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍。 4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。 内存分配————> 右图为结构体的内存分布,总大小为16个字节。右侧数字表示当前字节相对于变量起始地址的偏移量。(红色->a,黄色->b,蓝色->c,绿色->d,白色->浪费的字节)。 第一个成员a没有限制,放在起始地址。成员b的大小为1,比默认对齐数8小,所以b的对齐数是1,放在1的整数倍也就是偏移量为4的字节上。同理变量c的对齐数为4,所以要对齐到4的整数倍的字节上,所以5、6、7对应的字节都被浪费了,并不存储任何数据。变量d对齐数为1,对齐到12上。最后,由于结构体大小必须为成员中最大对齐数也就是4的整数倍,所以从本来的13个字节变成了16个字节,而其中的6个字节都被浪费了。注意:因为内存对齐,同样的成员可能会因为声明时的排序方式不同,导致结构体大小不同。如果想要大小尽可能的小,应该将大小较小的成员放在一起。
1.5 结构体实现位段
1.5.1 什么是位段
位段的声明和结构是类似的,有两个不同: 1. 位段的成员必须是 int, unsigned int 或 signed int , 在C99中位段成员的类型也可以选择其他类型。 2. 位段的成员名后边有⼀个冒号和⼀个数字。 比如:1.5.2 位段的内存分配
1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型 2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。 3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。1.5.3 位段的使用
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。 所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员1.5.4 位段的跨平台
1. int 位段被当成有符号数还是 ⽆符号数是不确定的。 2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会 出问题。 3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。 4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃 剩余的位还是利⽤,这是不确定的。 总结:跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。1.5.5 位段的应用
下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥ 使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络 的畅通是有帮助的。2、联合体类型