类型,运算符与表达式
类型,运算符与表达式
1.变量名
- 变量名以下划线“_”和字母开头,是由字母和数数字组成的序列,下划线被当成字母。
- 变量不能使用保留的关键字“if,else,int等。
- 选择的变量名要能尽量表达变量的用途。
2.数据类型及长度
C语言只提供4种基本数据类型
- char 字符型,占用__一个字节__。
- int 整型,通常反映了__所用机器中整数的最自然长度__。
- float 单精度浮点型。
- double 双精度浮点型。
__short__与__long__两个限定符用于限定整型(int)。
(声明中关键字_int_可以省略)
- long 类型通常为32位,short类型通常为16位,int类型可以为16位或32位。(根据各个硬件特性不同,长度也不同,但至少int类型长度不得大于long,不得小于short。
类型限定符__signed__和__unsigned__可用于限定char类型或任何整型。
- unsigned类型的数总是正值或零,并遵守[^算术模二]定律,不带限定符的char类型对象是否带符号则取决于具体机器,但可打印字符总是正值。
long duoble 类型表示高精度的浮点数。同整型一样,浮点数的长度也取决于具体的机器,float和double与long double 类型可以表示相同的长度,也可以表示两种或三种不同的长度。
_有关这些类型长度的符号常量以及其他与机器和编译器有关的属性可以在标准头文件<limits.h>与<float.h>_中找到__。
练习题2-1:编写程序确定变量的取值范围(采用两种计算方式,一种直接打印标准库的相关头文件<limits.h>和<float.h>。一种是直接计算方式,第二种方式相对来说较困难。)
- 方法一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main()
{
printf("signed char min = %d\n",SCHAR_MIN);
printf("signed char max = %d\n",SCHAR_MAX);
printf("signed short min = %d\n",SHRT_MIN);
printf("signed short max = %d\n",SHRT_MAX);
printf("signed int min = %d\n",INT_MIN);
printf("signed int max = %d\n",INT_MAX);
printf("signed long min = %ld\n",LONG_MIN);
printf("signed long max = %ld\n",LONG_MAX);
//printf("unsigned char min = %u\n",UCHAR_MIN);
printf("unsigned char max = %u\n",UCHAR_MAX);
// printf("unsigned short min = %u\n",USHRT_MIN);
printf("unsigned short max = %u\n",USHRT_MAX);
// printf("unsigned int min = %u\n",UINT_MIN);
printf("unsigned int max = %u\n",UINT_MAX);
// printf("unsigned long min =%lu\n",ULONG_MIN);
printf("unsigned long max = %lu\n",ULONG_MAX);
return 0;
}
方法二
数据类型范围的确定注意几点,
(1)所占用的字节数
(2)参考计算机组成原理方面的书籍,有专门讲数据类型范围的。浮点数,在内存中的存储结构是比较特殊的,只有了解存储结构才可以根据字节数算出来
(3)对于有符号数注意是补码的范围,组成原理书里也会有讲的。上一段是摘抄csdn的一个师傅的话,本身学艺不精,对该问题还没解决,以后看了组成原理再来。
3.常量
整型常量后面的符号‘L,l,u,U,ul,UL’。
- long类型的常量以字母l或者L结尾。
- 无符号常量u或U结尾。
- unsigned long 常量后缀为ul或UL。
浮点数常量中包含一个小数点或者一个指数。
* 没有后缀的浮点数常量为double类型。 * 后缀为f或者F表示float类型。 * 后缀为l或L则表示long double类型。
整型数除了用十进制表示也可以用十六进制和八进制表示
- 八进制带前缀0。
- 十六进制带前缀0x或0X。
- 十六进制和八进制也可以用后缀表示long和unsigned类型。
一个字符常量就是一个整数,书写时将一个字符括在单引号中,如’x’。字符常量一般用来与其他字符进行比较,但也可以像其他整数一样参与数值运算。
某些字符可以通过转义字符序列表示为字符和字符串常量。
\ooo表示1~3个八进制数字 \xhh 表示一个或者多个十六进制数字。
\a 响铃符 \\ 反斜杠 \b 回退符 ? 问号 \f 换页符 \‘单引号
\n 换行符 \“双引号 \r 回车符 \ooo八进制数 \t横向制表符
\xhh 十六进制数 \v纵向制表符
字符常量’\0’表示值为0的字符,也就是空字符(null)。我们通常用’\0’的形式来代替0,以强调某些表达式的字符属性,但其数字值为0。(这里的数字值为0指的是该字符的ASCII码为0)
常量表达式是仅仅包含常量的表达式。这种表达式在编译时求值,而不在运行时求值。
gcc编译过程:预处理(Preprocess),编译(Comolie),汇编(Assemble),链接(Link)。
字符串常量也叫字符串字面值,是用双引号括起来的0个或者多个字符组成的字符序列。
- 从技术角度来看,字符常量就是字符数组。字符串的内部表示使用一个空字符’\0’作为串的结尾,因此,__储存字符串的物理存储单元数比括在双引号的字符数多一个__。
- 字符常量与仅包含一个字符的字符串之间的区别:__”x”和’x’是不同的,后者是一个整数,其值是字母x在机器字符集中对应的数值(内部表示值);后者表示一个包含一个字符x以及一个结束符’\0’的字符数组。__
枚举常量数是另一种类型的常量,枚举是一个常量整型值的列表。
- 不同枚举中的名字必须互不相同,同一枚举中不同的名字可以具有相同的值。
- 枚举为建立常量值和名字之间的关联提供了一种便利的方式。
- 相对于#define语句来说,枚举类型的优势是提供检查这种类型变量中储存的值是否是该枚举的有效值。而且,调试程序可以以符号的形式打印出枚举变量的值。
- 枚举分为__枚举值和枚举子__
1
2
3 enum Example {FIRST,SECOND,THIRD}; // 三个枚举子对应枚举值默认是0,1,2。
printf("%d\n", FIRST); //输出结果为:0
//FIRST为枚举子,0为对应的枚举值
4.声明
__所有变量都必须先声明后使用__。一个声明指定一种变量类型,后面所带的变量表可以包含一个或多个该类型的变量。
如果变量不是[^自动变量] ,则只能进行一次初始化操作。自动变量未初始化是的值是该内存空间以前储存的值。默认情况下,外部变量与静态变量将被初始化为0,未经显式初始化的自动变量的值为未定义值(即无效值)。
任何变量的声明都可以用__const__限定符限定。该限定符指定的变量的值不能被修改。对数组而言,const限定符指定数组所有元素的值都不能被修改。
const限定符也可以配合数组参数使用,他表明函数不能修改数组元素的值。
1 int strlen (const char[]);如果试图修改const限定符限定的值,其结果取决于具体的实现。
5.算术运算符
- 二元运算符包括:+ ,- ,*,/,%(取模运算符)。整数除法会截断结果中的小数部分。
- 取模运算符%不能应用于float或double类型。__在有负操作数的情况下,整数除法截取的方向以及取模运算结果的符号取决于具体机器的实现,__这和处理[^上溢和下溢] 是一样的。
6.关系运算符与逻辑运算符
关系运算符包括以下几个运算符:
1 > >= < <= 运算符优先级比相等性运算符优先级高 == !=逻辑运算符&&与||有一些较为特殊的属性。由&&和||连接的表达式从左到右进行求值,并且,__在知道结果值为真或假后立即停止运算。绝大多数C语言运用了这些属性。__且&&比||运算符优先级高。
在关系表达式或逻辑表达式中,如果关系为真,则表达式的结果值为数值1;如果为假,则结果值为数值0。逻辑非运算符!的作用是将非0的操作数转换为0,将操作数0转换为1。
7.类型转换
当一个运算符的几个操作数类型不同时,需要通过一些规则把他们转换为某种共同的类型。__一般来说,自动转换是指把“比较窄的”操作数转换为“比较宽的”操作数__并且不丢失信息的转换。
计算字符对应的数字值(在标准库里对应的函数为(isdigit(c))
1 s[i]-'0'将大写字符转换为对应的小写字符(在标准库里对应的函数为tolower(c))
1
2
3
4
5
6
7 int lower(int c)
{
if(c>='A'&&c <= 'Z')
return c+'a'-'A';
else
return c;
}在字符类型转换为整型时,我们需要注意一点。
- C语言没有指定char类型的变量是无符号变量还是带符号变量,当把一个char类型的值转换为int类型的值时,其结果有可能转换为负整数。对于不同的机器,其结果也不同,这反映了不同结构的区别。
- 在某些机器中,如果char类型的值最左边一位为1,则转换为负整数(进行符号扩展)。
- 而在另一些机器中,把char类型值转换为int类型值的时候,在char类型值的左边添加0,这样导致的转换结果总是正值。
C语言的定义保证了机器的标准打印字符集的字符不会为负值,因此,在表达式中,这些字符总是正值。但是,存储在变量字符中的[^位模式] 在某些机器中可能为负,而在另一些可能是正的。
- 为了保证程序的可移植性,如果要在char类型的变量中存储非字符数据,最好指定signed或unsigned限定符。
C语言中,很多情况下会进行隐式的算术类型转换。一般来说,__两个操作数类型不同,会在运算前将“较低”类型提升为“较高”类型__。
- 一般来说,表达式中的float类型的操作数不会自动转换为double类型,数学函数(如标准头文件<math.h>中定义的函数)使用双精度类型的变量。使用float类型主要是为了在使用较大的数组时节省存储空间,有时也为了节省机器执行时间(双精度算术运算特别费时间)。
在表达式中包含unsigned类型的操作数时,转换规则要复杂一些。主要原因在于,带符号值与无符号值之间的比较运算是与机器相关的,他们取决于机器中不同整数类型的大小。
- 例如,假定int类型占16位,long类型占32位,那么,-1L < 1U,这是因为unsigned int类型将被提升为signed long类型;但-1L < 1UL,这是因为-1L将被提升为unsigned long 类型,因而成为一个比较大的正数。
赋值也要进行类型转换。赋值运算符右边的值需要转换为左边变量的类型,左边变量的类型即赋值表达式结果的类型。
- 前面提到过,无论进不进行符号扩展,字符型变量都将被转换为整型变量。
- 当把较长的整数转换为较短的整数或char类型时,超出高位的部分将被丢弃。
- 当float类型和int相互赋值时,都要进行类型转换。
- 当把double类型转换为float时,是进行四舍五入还是截取取决于具体的实现。
强制类型转换类型的__一元运算符__可以对表达式进行强制转换。
1 sqrt((double)n)//对n进行强制转换为double
在通常情况下,参数是通过函数原型声明的。这样,当函数被调用时,声明将对参数进行自动强制转换。
1
2 double sqrt(double);//下面是对函数的调用
root2 = sqrt(2);
补充说明
1. 自动变量
自动变量在C与C++中的实现即为“自动变量”(Automatic Variable)。默认情况下,在代码块内声明的变量都是自动变量,但亦可用自动变量的关键字auto明确标识存储类; 而如若使用register(而非auto)存储类标识代码块内的变量,编译器就会将变量缓存于处理器内的寄存器中,此种情况下不能对该变量或其成员变量使用引用操作符&以获取其地址,因为&只能获取内存空间中的地址;除此以外,由于寄存器的数量及其所能存储的数据类型受硬件限制而可能无法存储指定变量,编译器可以忽略声明内的register关键字。对于一个未初始化的自动变量来说,在为其赋值之前其值都为undefined(未定义)
属于自动存储类别的变量具有自动存储期,块作用域且无链接。
1.默认情况下,声明在块或函数头的任何变量都属于自动存储类别。为了表明有意覆盖一个外部变量定义,或者强调不要把该变量改为其他存储类别,__可以显式使用关键字auto
1 | int main(void) |
特别说明:关键字auto在c++中的用法完全不同,如果编写c/c++兼容的程序,最不要使用auto作为存储类别说明符。
__块作用域和无链接意味着__只有在定义变量声明所在的块时变量存在,程序在退出该块时变量消失。原来该变量占用的内存位置现在做他用。
1 | int loop(int n) |
如果内层块中声与外层变量同名时,内层块会隐藏外层块的定义。但是离开内层块后,外层块变量的作用域又回到了原来的作用域。
1 | // hiding.c -- 块中的变量 |
c99的一个特性:作为循环或if语句的一部分,即使不使用花括 号({}),也是一个块。更完整地说,整个循环是它所在块的子块(subblock),循环体是整个循环块的子块。与此类似,if 语句是一个块,与其 相关联的子语句是if语句的子块。这些规则会影响到声明的变量和这些变量 的作用域。
1 | // forc99.c -- 新的 C99 块规则 |
自动变量的初始化,自动变量不会初始化,除非显式初始化它。
1 | int main(void) |
tents变量被初始化为5,但是repid变量的值是之前占用分配给repid的空 间中的任意值(如果有的话),别指望这个值是0。可以用非常量表达式 (non-constant expression)初始化自动变量,前提是所用的变量已在前面定义过
1 | int main(void) |
2.函数的隐式声明
在C语言中,函数调用前不一定非要声明。如果没有声明,那么编译器会自动按照一种隐式声明的规则,为调用函数的C代码产生汇编代码。
1
2
3
4
5int main(int argc, char** argv)
{
double x = any_name_function();
return 0;
}单纯编译源代码,不会报错,只是在链接阶段找不到名为any_name_function的函数体而报错。
之所以编译不会报错,是因为c语言规定,对于没有声明的函数,自动使用隐式声明。相当于变成了如下代码
1
2
3
4
5
6int any_name_function();
int main(int argc, char** argv)
{
double x = any_name_function();
return 0;
}
3.上溢下溢
overflow:溢出
overflow:上溢
underflow:下溢
stack underflow:堆栈下溢;
上溢下溢
是当一个超长的数据进入到缓冲区时,超出部分被写入上级缓冲区,上级缓冲区存放的可能是数据、上一条指令的指针,或者是其他程序的输出内容,这些内容都被覆盖或者破坏掉。可见一小部分数据或者一套指令的溢出就可能导致一个程序或者操作系统崩溃。
与之对应的就是下溢,下溢是当一个超长的数据进入到缓冲区时,超出部分被写入下级缓冲区,下级缓冲区存放的是下一条指令的指针,或者是其他程序的输出内容。
其他解释
上溢:超出所能表示的最大正数
下溢:超出所能表示的最小负数
两数相加溢出的判断
同号时才可能溢出,同为正,有可能上溢,同为负,有可能下溢;
1 | int add(int a, int b) {undefined |
4.上溢下溢和缓冲区溢出
上溢是当一个超长的数据进入到缓冲区时,超出部分被写入上级缓冲区,下溢是当一个超长的数据进入到缓冲区时,超出部分被写入下级缓冲区。
随便往缓冲区中填东西造成它溢出一般只会出现“分段错误”(Segmentation fault),而不能达到攻击的目的。
最常见的手段是通过制造缓冲区溢出使程序运行一个用户shell,再通过shell执行其它命令。如果该程序有root或者suid执行权限的话,攻击者就获得了一个有root权限的shell,可以对系统进行任意操作了。
攻击原理:
通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,造成程序崩溃或使程序转而执行其它指令,以达到攻击的目的。造成缓冲区溢出的原因是程序中没有仔细检查用户输入的参数。
当程序需要接收用户数据,程序预先为之分配了4个格子(下图1中黄色的0~3号格子)。按照程序设计,就是要求用户输入的数据不超过4个。而用户在输入数据时,假设输入了16个数据,而且程序也没有对用户输入数据的多少进行检查。
就往预先分配的格子中存放,这样不仅4个分配的格子被使用了,其后相邻的12个格子中的内容都被新数据覆盖了。这样原来12个格子中的内容就丢失了。
缓冲区溢出:
缓冲区溢出是指当计算机程序向缓冲区内填充的数据位数超过了缓冲区本身的容量。溢出的数据覆盖在合法数据上。理想情况是,程序检查数据长度并且不允许输入超过缓冲区长度的字符串。但是绝大多数程序都会假设数据长度总是与所分配的存储空间相匹配,这就为缓冲区溢出埋下隐患。操作系统所使用的缓冲区又被称为堆栈,在各个操作进程之间,指令被临时存储在堆栈当中,堆栈也会出现缓冲区溢出。
上溢是当一个超长的数据进入到缓冲区时,超出部分被写入上级缓冲区,上级缓冲区存放的可能是数据、上一条指令的指针,或者是其他程序的输出内容,这些内容都被覆盖或者破坏掉。可见一小部分数据或者一套指令的溢出就可能导致一个程序或者操作系统崩溃。
下溢是当一个超长的数据进入到缓冲区时,超出部分被写入下级缓冲区,下级缓冲区存放的是下一条指令的指针,或者是其他程序的输出内容。
缓存:
缓存(Cache memory)是硬盘控制器上的一块内存芯片,具有极快的存取速度,它是硬盘内部存储和外界接口之间的缓冲器。由于硬盘的内部数据传输速度和外界介面传输速度不同,缓存在其中起到一个缓冲的作用。缓存的大小与速度是直接关系到硬盘的传输速度的重要因素,能够大幅度地提高硬盘整体性能。当硬盘存取零碎数据时需要不断地在硬盘与内存之间交换数据,如果有大缓存,则可以将那些零碎数据暂存在缓存中,减小外系统的负荷,也提高了数据的传输速度。
硬盘的缓存主要起三种作用:一是预读取。当硬盘受到CPU指令控制开始读取数据时,硬盘上的控制芯片会控制磁头把正在读取的簇的下一个或者几个簇中的数据读到缓存中(由于硬盘上数据存储时是比较连续的,所以读取命中率较高),当需要读取下一个或者几个簇中的数据的时候,硬盘则不需要再次读取数据,直接把缓存中的数据传输到内存中就可以了,由于缓存的速度远远高于磁头读写的速度,所以能够达到明显改善性能的目的;二是对写入动作进行缓存。当硬盘接到写入数据的指令之后,并不会马上将数据写入到盘片上,而是先暂时存储在缓存里,然后发送一个“数据已写入”的信号给系统,这时系统就会认为数据已经写入,并继续执行下面的工作,而硬盘则在空闲(不进行读取或写入的时候)时再将缓存中的数据写入到盘片上。虽然对于写入数据的性能有一定提升,但也不可避免地带来了安全隐患——如果数据还在缓存里的时候突然掉电,那么这些数据就会丢失。对于这个问题,硬盘厂商们自然也有解决办法:掉电时,磁头会借助惯性将缓存中的数据写入零磁道以外的暂存区域,等到下次启动时再将这些数据写入目的地;第三个作用就是临时存储最近访问过的数据。有时候,某些数据是会经常需要访问的,硬盘内部的缓存会将读取比较频繁的一些数据存储在缓存中,再次读取时就可以直接从缓存中直接传输。
缓存容量的大小不同品牌、不同型号的产品各不相同,早期的硬盘缓存基本都很小,只有几百KB,已无法满足用户的需求。2MB和8MB缓存是现今主流硬盘所采用,而在服务器或特殊应用领域中还有缓存容量更大的产品,甚至达到了16MB、64MB等。
大容量的缓存虽然可以在硬盘进行读写工作状态下,让更多的数据存储在缓存中,以提高硬盘的访问速度,但并不意味着缓存越大就越出众。缓存的应用存在一个算法的问题,即便缓存容量很大,而没有一个高效率的算法,那将导致应用中缓存数据的命中率偏低,无法有效发挥出大容量缓存的优势。算法是和缓存容量相辅相成,大容量的缓存需要更为有效率的算法,否则性能会大大折扣,从技术角度上说,高容量缓存的算法是直接影响到硬盘性能发挥的重要因素。更大容量缓存是未来硬盘发展的必然趋势。
[^算术模二]: 移位寄存器的每一级只可能有两种不同的存数(或状态),分别用0和1来表示。这里, 0和1不再具有一般数量的含义,而只具有逻辑含义。对于这样一种只包含0和1两个元素(符号)的集合(叫做二元集)来说,普通的四则运算不再适用,因而必须重新规定一种新的运算规则。所谓模2运算就是这样一种新的运算规则定律。模2运算是一种二进制算法,CRC校验技术中的核心部分。与四则运算相同,模2运算也包括模2加、模2减、模2乘、模2除四种二进制运算。而且,模2运算也使用与四则运算相同的运算符,即“+”表示模2加,“-”表示模2减,“×”或“·”表示模2乘,“÷”或“/”表示模2除。与四则运算不同的是模2运算不考虑进位和借位,即模2加法是不带进位的二进制加法运算,模2减法是不带借位的二进制减法运算。这样,两个二进制位相运算时,这两个位的值就能确定运算结果,不受前一次运算的影响,也不对下一次造成影响。 ↩
[^自动变量]: 在计算机编程领域,自动变量(Automatic Variable)指的是局部作用域变量,具体来说即是在控制流进入变量作用域时系统自动为其分配存储空间,并在离开作用域时释放空间的一类变量。在许多程序语言中,自动变量与术语“局部变量”(Local Variable)所指的变量实际上是同一种变量,所以通常情况下“自动变量”与“局部变量”是同义的。
[^上溢和下溢]: 上溢:超出所能表示的最大正数 下溢:超出所能表示的最小负数