C语言系列:2、数据类型、运算符和表达式


文章目录

  • C语言系列:2、数据类型、运算符和表达式
    • 1. 前言
    • 2. 变量名
    • 3. 数据类型和长度
      • 3.1 基本数据类型
      • 3.2 short和long限定符
      • 3.3 signed 与unsigned限定符
    • 4. 常量
      • 4.1 整型常量和浮点型常量
      • 4.2 八进制和十六进制表示
      • 4.3 字符常量
      • 4.4 转义字符
      • 4.5 常量表达式
      • 4.6 字符串常量
      • 4.7 枚举常量
    • 5. 声明
      • 5.1 变量声明
      • 5.2 初始化
      • 5.3 const限定符
    • 6. 算术运算符
    • 7. 关系运算符和逻辑运算符
      • 7.1 关系运算符
      • 7.2 逻辑运算符
    • 8. 类型转换
      • 8.1 类型转换
      • 8.2 隐式类型转换
      • 8.3 强制类型转换
    • 9. 自增运算符与自减运算符
    • 10. 按位运算符
    • 11. 赋值运算符与表达式
    • 12. 条件表达式与三元运算符
    • 13. 运算符优先级和求值次序

1. 前言

计算机基础、数学、英语算是编程的基础,这些基础越扎实越好,不然这个楼就建不高,我现在就是这么个情况,由于是大学学的是机电所以计算机基础不是很好,一些计算机原理性的东西不清楚导致很多东西知其然不知其所以然,大学数学学的也不够扎实导致现在AI需要的一些数学知识也需要补,英语基础也不是很好,很多英文资料要靠翻译软件才能勉强搞定,经常也看的人头很大,所以现在技术处于一个瓶颈期,不仅基础需要补,新的东西学起来也比较吃力,所幸一腔热血还在,所以即使脚步虚浮前路艰难也还在一直向前,不放弃才有希望不是嘛。

变量和常量是程序处理的两种基本数据对象。声明语句说明变量的名字及类型,也可以指定变量的初值。运算符指定将要进行的操作。表达式则把变量与常量组合起来生成新的值。对象的类型决定该对象可取值的集合以及可以对该对象执行的操作。

2. 变量名

  • 变量名字由字母、数字,下划线一般也被看成字母,但其第一个字符必须为字母;
  • 由于一些例程的名字通常是以下划线开头的,所以变量名不要以下划线开头;
  • 大写字母和小写字母是有区别的;
  • 保留字不能用于变量名;
  • 变量名尽量做到见名知意,局部变量尽量使用较短变量名,外部变量尽量使用较长名字;

3. 数据类型和长度

3.1 基本数据类型

C语言只提供了下列几种基本数据类型:

  • char 字符型,占用一个字节,可以存放本地字符集中的一个字符
  • int 整型,通常反映了所用机器中整数的最自然长度
  • float 单精度浮点型
  • double 双精度浮点型

此外,还可以在这些基本数据类型的前面加上一些限定符。

3.2 short和long限定符

short 与long 两个限定符用于限定整型:

  • short int sh;

  • long int counter;

在上述这种类型的声明中,关键字int可以省略。通常很多人也习惯这么做。

short 与long 两个限定符的引入可以为我们提供满足实际需要的不同长度的整数。
int 通常代表特定机器中整数的自然长度。

  • short 类型通常为16 位,1ong 类型通常为32位,int 类型可以为16 位或32 位;
  • 各编译器可以根据硬件特性自主选择合适的类型长度,但要遵循下列限制:short与int类型至少为16 位,而long类型至少为32 位,并且short类型不得长于int类型,而int类型不得长于long类型。
  • 在64位的机器上long可能占用8个字节,也就是64位,而short一般仍然是2个字节,int为4个字节,这也满足上述限制

long double类型表示高精度的浮点数。同整型一样,浮点型的长度也取决于具体的实现。float、double与long double类型可以表示相同的长度,也可以表示两种或三种不同的长度。

3.3 signed 与unsigned限定符

类型限定符signed 与unsigned 可用于限定char 类型或任何整型。unsigned 类型
的数总是正值或0,并遵守算术模2^n定律,其中n 是该类型占用的位数。例如,如果char对象占用8 位,那么unsigned char类型变量的取值范围为0~255,而signed char类型变量的取值范围则为-128~127(在采用对二的补码的机器上)。不带限定符的char类型对象是否带符号则取决于具体机器,但可打印字符总是正值。

有关这些类型长度定义的符号常量以及其它与机器和编译器有关的属性可以在标准头文件<limits.h>与<float.h>中找到,编写一个程序以确定分别由signed及unsigned 限定的char、short、int与long类型变量的取值范围。采用打印标准头文件中的相应值:

#include <stdio.h>
#include <limits.h>int main() {printf("signed char max:%d,min:%d\n", CHAR_MAX, CHAR_MIN);printf("unsigned char max:%u\n", UCHAR_MAX);printf("signed short max:%d,min:%d\n", SHRT_MAX, SHRT_MIN);printf("unsigned short max:%u\n", USHRT_MAX);printf("signed int max:%d,min:%d\n", INT_MAX, INT_MIN);printf("unsigned int max:%u\n", UINT_MAX);printf("signed long max:%d,min:%d\n", LONG_MAX, LONG_MIN);printf("unsigned long max:%lu", ULONG_MAX);return 0;
}

4. 常量

4.1 整型常量和浮点型常量

类似于1234 的整数常量属于int 类型。long 类型的常量以字母l 或L 结尾,如
123456789L。如果一个整数太大以至于无法用int 类型表示时,也将被当作long 类型处理。无符号常量以字母u或U结尾。后缀ul或UL表明是unsigned long类型。

浮点数常量中包含一个小数点(如123.4)或一个指数(如1e-2),也可以两者都有。没有后缀的浮点数常量为double 类型。后缀f 或F 表示float 类型,而后缀l 或L 则表示long double类型。

4.2 八进制和十六进制表示

整型数除了用十进制表示外,还可以用八进制或十六进制表示。带前缀0 的整型常量表示它为八进制形式;前缀为0x或0X,则表示它为十六进制形式。例如,十进制数31 可以写成八进制形式037,也可以写成十六进制形式0x1f或0X1F。八进制与十六进制的常量也可以使用后缀L 表示long 类型,使用后缀U 表示unsigned 类型。例如,0XFUL 是一个unsigned long类型(无符号长整型)的常量,其值等于十进制数15。

进制这里不展开讨论,只要知道10进制、16进制、8进制、2进制的转换和表示即可。

4.3 字符常量

一个字符常量是一个整数,书写时将一个字符括在单引号中,如,‘x’。字符在机器字符集中的数值就是字符常量的值。例如,在ASCII 字符集中,字符’0’的值为48,它与数值0没有关系。如果用字符’0’代替这个与具体字符集有关的值(比如48),那么,程序就无需关心该字符对应的具体值,增加了程序的易读性。字符常量一般用来与其它字符进行比较,但也可以像其它整数一样参与数值运算,

4.4 转义字符

ANSI C语言中的全部转义字符序列如下所示:

\a 响铃符\b 回退符\f 换页符\n 换行符\r 回车符\t 横向制表符\v 纵向制表符\\\ 反斜杠\? 问号\\' 单引号\\" 双引号\ooo 八进制数\xhh 十六进制数

字符常量’\0’表示值为0的字符,也就是空字符(null)。我们通常用’\0’的形式代替0,以强调某些表达式的字符属性,但其数字值为0。

4.5 常量表达式

常量表达式是仅仅只包含常量的表达式。这种表达式在编译时求值,而不在运行时求值。
它可以出现在常量可以出现的任何位置,例如:

#define MAXLINE 1000
char line[MAXLINE+1];

#define LEAP 1 /* in leap years */
int days[31+28+LEAP+31+30+31+30+31+31+30+31+30+31];

这通常用来存储一些可能经常更改的常量,便于统一更改,一般常量也都会通过宏的形式表示,防止出现“魔鬼数字”,避免后续维护搞不懂该常量的含义。

4.6 字符串常量

字符串常量也叫字符串字面值,是用双引号括起来的0 个或多个字符组成的字符序列。

例如:

"I am a string"

"" /* 空字符串*/

都是字符串。双引号不是字符串的一部分,它只用于限定字符串。字符常量中使用的转义字符序列同样也可以用在字符串中。在字符串中使用"表示双引号字符。编译时可以将多个字符串常量连接起来,例如,下列形式:

"hello," " world"

等价于

"hello, world"

字符串常量的连接为将较长的字符串分散在若干个源文件行中提供了支持。

从技术角度看,字符串常量就是字符数组。字符串的内部表示使用一个空字符’\0’作为串的结尾,因此。存储字符串的物理存储单元数比括在双引号中的字符数多一个。这种表示方法也说明,C语言对字符串的长度没有限制,但程序必须扫描完整个字符串后才能确定字符串的长度。标准库函数strlen(s)可以返回字符串参数s的长度,但长度不包括末尾的’\0’。

下面是我们设计的strlen函数的一个版本:

/* strlen: return length of s */int strlen(char s[]){    int i;  while (s[i] != '\0')     ++i;  return i;}

标准头文件<string.h>中声明了strlen和其它字符串函数。

我们应该搞清楚字符常量与仅包含一个字符的字符串之间的区别:‘x’与"x"是不同的。前者是一个整数,其值是字母x 在机器字符集中对应的数值(内部表示值);后者是一个包含一个字符(即字母x)以及一个结束符’\0’的字符数组。

4.7 枚举常量

枚举常量是另外一种类型的常量。枚举是一个常量整型值的列表,例如:

enum boolean { NO, YES };

在没有显式说明的情况下,enum 类型中第一个枚举名的值为0,第二个为1,依此类推。如果只指定了部分枚举名的值,那么未指定值的枚举名的值将依着最后一个指定值向后递增,参看下面两个例子中的第二个例子:

enum escapes {    BELL = '\a',    BACKSPACE = '\b',    TAB = '\t',    NEWLINE = '\n',    VTAB = '\v',    RETURN = '\r'};enum months { JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC };/* FEB的值为2,MAR的值为3,依此类推*/

不同枚举中的名字必须互不相同。同一枚举中不同的名字可以具有相同的值。

枚举为建立常量值与名字之间的关联提供了一种便利的方式。相对于#define语句来说,它的优势在于常量值可以自动生成。尽管可以声明enum类型的变量,但编译器不检查这种类型的变量中存储的值是否为该枚举的有效值。不过,枚举变量提供这种检查,因此枚举比#define更具优势。此外,调试程序可以以符号形式打印出枚举变量的值。

5. 声明

5.1 变量声明

所有变量都必须先声明后使用,尽管某些变量可以通过上下文隐式地声明。一个声明指定一种变量类型,后面所带的变量表可以包含一个或多个该类型的变量。例如:

int lower, upper, step;char c, 1ine[1000];

一个声明语句中的多个变量可以拆开在多个声明语句中声明。上面的两个声明语句也可以等价地写成下列形式:

int lower;int upper;int step;char c;char line[1000];

按照这种形式书写代码需要占用较多的空间,但便于向各声明语句中添加注释,也便于以后修改。

5.2 初始化

还可以在声明的同时对变量进行初始化。在声明中,如果变量名的后面紧跟一个等号以及一个表达式,该表达式就充当对变量进行初始化的初始化表达式。例如:

char esc = '\\';int i = 0;int limit = MAXLINE + 1;float eps = 1.0e-5;

如果变量不是自动变量,则只能进行一次初始化操作,从概念上讲,应该是在程序开始执行之前进行,并且初始化表达式必须为常量表达式。每次进入函数或程序块时,显式初始化的自动变量都将被初始化一次,其初始化表达式可以是任何表达式。默认情况下,外部变量与静态变量将被初始化为0。未经显式初始化的自动变量的值为未定义值(即无效值)。

5.3 const限定符

任何变量的声明都可以使用const限定符限定。该限定符指定变量的值不能被修改。对数组而言,const限定符指定数组所有元素的值都不能被修改:

const double e = 2.71828182845905;const char msg[] = "warning: ";

const限定符也可配合数组参数使用,它表明函数不能修改数组元素的值:

int strlen(const char[]);

如果试图修改const限定符限定的值,其结果取决于具体的实现。

6. 算术运算符

二元算术运算符包括:+、-、、/、%(取模运算符)。整数除法会截断结果中的小数部分。表达式:

x % y

的结果是x 除以y 的余数,当x 能被y 整除时,其值为0。例如,如果某一年的年份能被4整除但不能被100整除,那么这一年就是闰年,此外,能被400整除的年份也是闰年。因此,可以用下列语句判断闰年:

if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)  printf("%d is a leap year\n", year);else  printf("%d is not a leap year\n", year);

取模运算符%不能应用于float 或double 类型。在有负操作数的情况下,整数除法截取的方向以及取模运算结果的符号取决于具体机器的实现,这和处理上溢或下溢的情况是一样的。

二元运算符+和-具有相同的优先级,它们的优先级比运算符*、/和%的优先级低,而运算符*、/和%的优先级又比一元运算符+和-的优先级低。算术运算符采用从左到右的结合规则。

7. 关系运算符和逻辑运算符

7.1 关系运算符

关系运算符包括下列几个运算符:

> >= < <=

它们具有相同的优先级。优先级仅次于它们的是相等性运算符:

== !=

关系运算符的优先级比算术运算符低。因此,表达式i < lim - 1 等价于i < (lim-1)。

7.2 逻辑运算符

逻辑运算符&&与||有一些较为特殊的属性,由&&与||连接的表达式按从左到右的顺序进行求值,并且,在知道结果值为真或假后立即停止计算。绝大多数C 语言程序运用了这些属性。例如:

for (i=0; i<lim-1 && (c=getchar()) != '\n' && c != EOF; ++i)  s[i] = c;

在读入一个新字符之前必须先检查数组s 中足否还有空间存放这个字符,因此必须首先测试条件i<lim-1。如果这一测试失败,就没有必要继续读入下一字符。

类似地,如果在调用getchar函数之前就测试c是否为EOF,结果也是不正确的,因此,函数的调用与赋值都必须在对c中的字符进行测试之前进行。

运算符&&的优先级比||的优先级高,但两者都比关系运算符和相等性运算符的优先级低。

因此,表达式i<lim-1 && (c = getchar()) != ‘\n’ && c!= EOF就不需要另外加圆括号。但是,由于运算符!=的优先级高于赋值运算符的优先级,因此,在表达式(c = getchar()) != ‘’\n’中,就需要使用圆括号,这样才能达到预期的目的:先把函数返回值赋值给c,然后再将c与’\n’进行比较。

根据定义,在关系表达式或逻辑表达式中,如果关系为真,则表达式的结果值为数值1;如果为假,则结果值为数值0。

逻辑非运算符!的作用是将非0 操作数转换为0,将操作数0 转换为1。该运算符通常用于下列类似的结构中:

if (!valid)

一般不采用下列形式:

if (valid == 0)

当然,很难评判上述两种形式哪种更好。类似于!valid 的用法读起来更直观一些(“如果不是有效的”),但对于一些更复杂的结构可能会难于理解。

8. 类型转换

8.1 类型转换

当一个运算符的几个操作数类型不同时,就需要通过一些规则把它们转换为某种共同的类型。一般来说,自动转换是指把“比较窄的”操作数转换为“比较宽的”操作数,并且不丢失信息的转换,例如,在计算表达式f+i 时,将整型变量i 的值自动转换为浮点型(这里的变量f 为浮点型)。不允许使用无意义的表达式,例如,不允许把float 类型的表达式作为下标。针对可能导致信息丢失的表达式,编译器可能会给出警告信息,比如把较长的整型值赋给较短的整型变量,把浮点型值赋值给整型变量,等等,但这些表达式并不非法。

由于char类型就是较小的整型,因此在算术表达式中可以自由使用char类型的变量,
这就为实现某些字符转换提供了很大的灵活性,比如,下面的函数atoi就是一例,它将一串
数字转换为相应的数值:

/* atoi: convert s to integer */int atoi(char s[]){    int i, n;    n = 0;  for (i = 0; s[i] >= '0' && s[i] <= '9'; ++i)     n = 10 * n + (s[i] - '0');    return n;}

s[i] - ‘0’能够计算出s[i]中存储的字符所对应的数字值,这是因为’0’、'l’等在字符集中对应的数值是一个连续的递增序列。

函数 lower是将char类型转换为int类型的另一个例子,它将ASCII字符集中的字符
映射到对应的小写字母。如果待转换的字符不是大写字母,lower函数将返回字符本身。

/* lower: convert c to lower case; ASCII only */int lower(int c){    if (c >= 'A' && c <= 'Z')       return c + 'a' - 'A';  else        return c;}

上述这个函数是为ASCII 字符集设计的。在ASCII 字符集中,大写字母与对应的小写字母作为数字值来说具有固定的间隔,并且每个字母表都是连续的——也就是说,在A~Z之间只有字母。但是,后面一点对EBCDIC 字符集是不成立的,因此这一函数作用在EBCDIC 字符集中就不仅限于转换字母的大小写。

**注意:**将字符类型转换为整型时,我们需要注意一点。C语言没有指定char类型的变量是无符号变量(signed)还是带符号变量(unsigned)。当把一个char 类型的值转换为int 类型的值时,其结果有没有可能为负整数?对于不同的机器,其结果也不同,这反映了不同机器结构之间的区别。在某些机器中,如果char类型值的最左一位为1,则转换为负整数(进行“符号扩展”)。而在另一些机器中,把char 类型值转换为int 类型时,在char 类型值的左边添加0,这样导致的转换结果值总是正值。

C语言的定义保证了机器的标准打印字符集中的字符不会是负值,因此,在表达式中这些字符总是正值。但是,存储在字符变量中的位模式在某些机器中可能是负的,而在另一些机器上可能是正的。为了保证程序的可移植性,如果要在char类型的变量中存储非字符数据,最好指定signed或unsigned限定符。

8.2 隐式类型转换

C语言中,很多情况下会进行隐式的算术类型转换。一般来说,如果二元运算符(具有两个操作数的运算符称为二元运算符,比如+或*)的两个操作数具有不同的类型,那么在进行运算之前先要把“较低”的类型提升为“较高”的类型,运算的结果为较高的类型。但是,如果没有unsigned 类型的操作数,则只要使用下面
这些非正式的规则就可以了:

  • 如果其中一个操作数的类型为long double,则将另一个操作数转换为1ong double类型;

  • 如果其中一个操作数的类型为double,则将另一个操作数转换为double类型;

  • 如果其中一个操作数的类型为float,则将另一个操作数转换为float类型;

  • 将 char与short类型的操作数转换为int类型;

  • 如果其中一个操作数的类型为long,则将另一个操作数也转换为long类型。

注意,表达式中float类型的操作数不会自动转换为double类型,这一点与最初的定
义有所不同。一般来说,数学函数(如标准头文件<math.h>中定义的函数)使用双精度类型的变量。使用float类型主要是为了在使用较大的数组时节省存储空间,有时也为了节省机器执行时间(双精度算术运算特别费时)。

当表达式中包含unsigned 类型的操作数时,转换规则要复杂一些。主要原因在于,带符号值与无符号值之间的比较运算是与机器相关的,因为它们取决于机器中不同整数类型的大小。例如,假定int类型占16 位,long类型占32位,那么,-1L < 1U,这是因为unsighed int 类型的1U 将被提升为signed long 类型;但-1L > 1UL,这是因为1L 将被提升为unslgned long类型,因而成为一个比较大的正数。

赋值时也要进行类型转换。赋值运算符右边的值需要转换为左边变量的类型,左边变量的类型即赋值表达式结果的类型。

前面提到过,无论是否进行符号扩展,字符型变量都将被转换为整型变量。当把较长的整数转换为较短的整数或char类型时,超出的高位部分将被丢弃。因此,下列程序段:

int i;char c;i = c;c = i;

执行后,c的值将保持不变。无论是否进行符号扩展,该结论都成立。但是,如果把两个赋值语句的次序颠倒一下,则执行后可能会丢失信息。

如果x是float类型,i是int类型,那么语句x = i 与i = x 在执行时都要进行类型转换。当把float类型转换为int类型时,小数部分将被截取掉;当把double类型转换
为float类型时,是进行四舍五入还是截取取决于具体的实现。

由于函数调用的参数是表达式,所以在把参数传递给函数时也可能进行类型转换。在没有函数原型的情况下,char与short类型都将被转换为int类型,float类型将被转换为double 类型。因此,即使调用函数的参数为char 或float 类型,我们也把函数参数声明为int或double类型。

8.3 强制类型转换

最后,在任何表达式中都可以使用一个称为强制类型转换的一元运算符强制进行显式类型转换。在下列语句中,表达式将按照上述转换规则被转换为类型名指定的类型:

(类型名) 表达式

我们可以这样来理解强制类型转换的准确含义:在上述语句中,表达式首先被赋值给类型名指定的类型的某个变量,然后再用该变量替换上述整条语句。例如,库函数sqrt 的参数为double类型,如果处理不当,结果可能会无意义(sqrt在<math.h>中声明)。因此,如果n是整数,可以使用sqrt((double) n)在把n 传递给函数sqrt 之前先将其转换为double 类型。注意,强制类型转换只是生成一个指定类型的n的值,n本身的值并没有改变。强制类型转换运算符与其它一元运算符具有相同的优先级。

在通常情况下,参数是通过函数原型声明的。这样,当函数被调用时,声明将对参数进行自动强制转换。例如,对于sqrt的函数原型

double sqrt(double);

下列函数调用:

root2 = sqrt(2);

不需要使用强制类型转换运算符就可以自动将整数2强制转换为double类型的值2.0。

标准库中包含一个可移植的实现伪随机数发生器的函数rand 以及一个初始化种子数的函数srand。前一个函数rand使用了强制类型转换。

unsigned long int next = 1;/* rand: return pseudo-random integer on 0..32767 */int rand(void){  next = next * 1103515245 + 12345; return (unsigned int)(next/65536) % 32768;}/* srand: set seed for rand() */void srand(unsigned int seed){   next = seed;}

为了考虑程序的可读性,一般都要求强制的显式类型转换,尽量避免隐式类型转换。

9. 自增运算符与自减运算符

C语言提供了两个用于变量递增与递减的特殊运算符。自增运算符++使其操作数递增1,自减运算符使其操作数递减1。我们经常使用++运算符递增变量的值,如下所示:

if (c == '\n')   ++nl;

++与–这两个运算符特殊的地方主要表现在:它们既可以用作前缀运算符(用在变量前面,如++n)。也可以用作后缀运算符(用在变量后面,如n++)。在这两种情况下,其效果都是将变量n的值加1。但是,它们之间有一点不同。**表达式++n先将n的值递增1,然后再使用变量n 的值,而表达式n++则是先使用变量n 的值,然后再将n 的值递增1。**也就是说,对于使用变量n的值的上下文来说,++n和n++的效果是不同的。如果n的值为5,那么

x = n++;

执行后的结果是将x的值置为5,而

x = ++n;

将x的值置为6。这两条语句执行完成后,变量n的值都是6。自增与自减运算符只能作用于变量,类似于表达式(i+j)++是非法的。

注意上面的特殊之处即可,有的奇怪的题目喜欢这么考来避免考察是否清楚这点,实际编码过程中我们应尽量避免这样特殊的用法,这不是在炫技,这是在找骂。

10. 按位运算符

C语言提供了6个位操作运算符。这些运算符只能作用于整型操作数,即只能作用于带符号或无符号char、short、int、long类型:
& 按位与(AND)
| 按位或(OR)
^ 按位异或(XOR)
<< 左移
>> 右移
~ 按位求反(一元运算符)

按位与运算符&经常用于屏蔽某些二进制位,例如:

n = n & 0177;

该语句将n中除7个低二进制位外的其它各位均置为0。

按位或运算符|常用于将某些二进制位置为1,例如:

x = x | SET_ON;

该语句将x中对应于SET_ON中为1 的那些二进制位置为1。

按位异或运算符^当两个操作数的对应位不相同时将该位设置为1,否则,将该位设置为0。

我们必须将位运算符&、|同逻辑运算符&&、||区分开来,后者用于从左至右求表达式的真值。例如,如果x的值为1,Y的值为2,那么,x & y的结果为0,而x && y 的值为1。

移位运算符<<与>>分别用于将运算的左操作数左移与右移,移动的位数则由右操作数指定(右操作数的值必须是非负值)。因此,表达式x << 2 将把x 的值左移2 位,右边空出的2 位用0 填补,该表达式等价于对左操作数乘以4。在对unsigned类型的无符号值进行右移位时,左边空出的部分将用0 填补;当对signed 类型的带符号值进行右移时,某些机器将对左边空出的部分用符号位填补(即“算术移位”),而另一些机器则对左边空出的部分用0填补(即“逻辑移位”)。

一元运算符~用于求整数的二进制反码,即分别将操作数各二进制位上的1 变为0,0 变为1。例如:

x = x & ~077

将把x的最后6位设置为0。注意,表达式x & ~077与机器字长无关,它比形式为x & 0177700的表达式要好,同为后者假定x是16位的数值。这种可移植的形式并没有增加额外开销,因为,~077是常量表达式,可以在编译时求值。

为了进一步说明某些位运算符,我们来看函数getbits(x, p, n),它返回x中从右边
数第p位开始向右数n位的字段。这里假定最右边的一位是第0位,n与p都是合理的正值。

例如,getbits(x, 4, 3)返回x中第4、3、2三位的值。

/* getbits: get n bits from position p */unsigned getbits(unsigned x, int p, int n){ return (x >> (p+1-n)) & ~(~0 << n);}

其中,表达式m << (p+1-n)将期望获得的字段移位到字的最右端。0的所有位都为1,这里使用语句0 << n 将~0左移n位,并将最右边的n位用0 填补。再使用~运算对它按位取反,这样就建立了最右边n位全为1 的屏蔽码。

11. 赋值运算符与表达式

在赋值表达式中,如果表达式左边的变量重复出现在表达式的右边,如:

i = i+2

则可以将这种表达式缩写为下列形式:

i += 2

其中的运算符+=称为赋值运算符。
大多数二元运算符(即有左、右两个操作数的运算符,比如+)都有一个相应的赋值运算符op=,其中,op可以是下面这些运算符之一:

+ - * / % << >> & ^ |

如果expr1和expr2是表达式,那么

​ expr1 op= expr2

等价于:
expr1 = (expr1) op (expr2)

它们的区别在于,前一种形式expr1只计算一次。注意,在第二种形式中,expr2两边的圆括号是必不可少的,例如,

​ x *= y + 1

的含义是:

​ x = x * (y + 1)

而不是

​ x = x * y + 1

我们这里举例说明。下面的函数bitcount统计其整型参数的值为1的二进制位的个数。

/* bitcount: count 1 bits in x */
int bitcount(unsigned x)
{int b;for (b = 0; x != 0; x >>= 1)if (x & 01)b++;return b;
}

这里将x 声明为无符号类型是为了保证将x 右移时,无论该程序在什么机器上运行,左边空出的位都用0(而不是符号位)填补。

除了简洁外,赋值运算符还有一个优点:表示方式与人们的思维习惯比较接近。我们通常会说“把2 加到i 上”或“把i 增加2”,而不会说“取i 的值,加上2,再把结果放回到i中”,因此,表达式i += 2比i = i + 2更自然,另外,对于复杂的表达式,例如:

yyval[yypv[p3+p4] + yypv[p1+p2]] += 2

赋值运算符使程序代码更易于理解,代码的阅读者不必煞费苦心地去检查两个长表达式是否完全一样,也无须为两者为什么不一样而疑惑不解,并且,赋值运算符还有助于编译器产生高效代码。

从上述例子中我们可以看出,赋值语句具有值,且可以用在表达式中。下面是最常见的一个例子:

while ((c = getchar()) !=EOF)

其它赋值运算符(如+=、-=等)也可以用在表达式中,尽管这种用法比较少见。

在所有的这类表达式中,赋值表达式的类型是它的左操作数的类型,其值是赋值操作完成后的值,

12. 条件表达式与三元运算符

下面这组语句:

if (a > b)z = a;
elsez = b;

用于求a和b中的最大值,并将结果保存到z中。条件表达式(使用三元运算符“? :”)提供了另外一种方法编写这段程序及类似的代码段,在表达式

expr1 ? expr2 : expr3

中,首先计算expr1,如果其值不等于0(为真),则计算expr2 的值,并以该值作为条件表达式的值,否则计算expr3 的值,并以该值作为条件表达式的值。expr2 与expr3 中只能有一个表达式被计算。因此,以上语句可以改写为:

z = (a > b) ? a : b; /* z = max(a, b) */

应该注意,条件表达式实际上就是一种表达式,它可以用在其它表达式可以使用的任何地方;如果expr2 与expr3 的类型不同,结果的类型将由本章前面讨论的转换规则决定。

例如,如果f为float类型,n为int类型,那么表达式

(n > 0) ? f : n

是float类型,与n是否为正值无关。

条件表达式中第一个表达式两边的圆括号并不是必须的,这是因为条件运算符?:的优先级非常低,仅高于赋值运算符。但我们还是建议使用圆括号,因为这可以使表达式的条件部分更易于阅读。

采用条件表达式可以编写出很简洁的代码。例如,下面的这个循环语句打印一个数组的n个元素,每行打印10个元素,每列之间用一个空格隔开,每行用一个换行符结束(包括最后一行):

for (i = 0; i < n; i++)printf("%6d%c", a[i], (i%10==9 !! i==n-1) ? '\n' : ' ');

在每10个元素之后以及在第n个元素之后都要打印一个换行符,所有其它元素后都要打印一个空格。编写这样的代码可能需要一些技巧,但比用等价的if-else结构编写的代码要紧凑一些。下面是另一个比较好的例子:

printf("You have %d item%s.\n", n, n==1 ? "" : "s");

13. 运算符优先级和求值次序

表 2-1总结了所有运算符的优先级与结合性,其中的一些规则我们还没有讲述。同一行中的各运算符具有相同的优先级,各行间从上往下优先级逐行降低。例如,、*、/与%三者具有相同的优先级,它们的优先级都比二元运算符+、-高。运算符( )表示函数调用。运算符->和.用于访问结构成员。

表 2-1 运算符的优先级与结合性

运算符 结合性
() [] -> . 从左至右
! ~ ++ – + - * (type) sizeof 从右至左
* / % 从左至右
+ - 从左至右
<< >> 从左至右
< <= > >= 从左至右
== != 从左至右
& 从左至右
^ 从左至右
| 从左至右
&& 从左至右
|| 从左至右
?: 从左至右
= += -= *= /= %= &= ^= |= <<= >>= 从右至左
, 从右至左

注:一元运算符+、-、&与比相应的二元运算符+、-、&与的优先级高。

注意,位运算符&、^与|的优先级比运算符==与!=的低。这意味着,位测试表达式,如

if ((x & MASK) == 0) ...

必须用圆括号括起来才能得到正确结果。

同大多数语言一样,C语言没有指定同一运算符中多个操作数的计算顺序(&&、||、?:和,运算符除外)。例如,在形如

x = f() + g();

的语句中,f()可以在g()之前计算,也可以在g()之后计算。因此,如果函数f 或g 改变了另一个函数所使用的变量,那么x 的结果可能会依赖于这两个函数的计算顺序。为了保证特定的计算顺序,可以把中间结果保存在临时变量中。

类似地,C语言也没有指定函数各参数的求值顺序。因此,下列语句

printf("%d %d\n", ++n, power(2, n)); /* 错*/

在不同的编译器中可能会产生不同的结果,这取决于n的自增运算在power调用之前还是之后执行。解决的办法是把该语句改写成下列形式:

++n;
printf("%d %d\n", n, power(2, n));

函数调用、嵌套赋值语句、自增与自减运算符都有可能产生“副作用”——在对表达式求值的同时,修改了某些变量的值。在有副作用影响的表达式中,其执行结果同表达式中的变量被修改的顺序之间存在着微妙的依赖关系,下列语句就是一个典型的令人不愉快的情况:

a[i] = i++;

问题是:数组下标i 是引用旧值还是引用新值?对这种情况编译器的解释可能不同,并因此产生不同的结果。C语言标准对大多数这类问题有意未作具体规定。表达式何时会产生这种副作用(对变量赋值),将由编译器决定,因为最佳的求值顺序同机器结构有很大关系。(ANSI C标准明确规定了所有对参数的副作用都必须在函数调用之前生效,但这对前面介绍的printf函数调用没有什么帮助。)

在任何一种编程语言中,如果代码的执行结果与求值顺序相关,则都是不好的程序设计风格。很自然,有必要了解哪些问题需要避免,但是,如果不知道这些问题在各种机器上是如何解决的,就最好不要尝试运用某种特殊的实现方式。

运算符的优先级没有特殊需求的话不必死记硬背,实际编码时都会要求添加括号来清晰展示执行顺序。

C语言系列:2、数据类型、运算符和表达式相关推荐

  1. c语言程序计算p q真值表,C语言程序设计第2章数据类型﹒运算符和表达式.ppt

    C语言程序设计第2章数据类型﹒运算符和表达式 教学目标 掌握C语言标识符的组成 理解C语言的基本数据类型 掌握变量定义的方法 掌握常用的运算符的使用 掌握混合运算的数据转换方法 2.1 C语言的数据类 ...

  2. 二进制补码求值用c语言,C语言程序设计第2章数据类型.运算符与表达式.ppt

    C语言程序设计第2章数据类型.运算符与表达式 教学目标 掌握C语言标识符的组成 理解C语言的基本数据类型 掌握变量定义的方法 掌握常用的运算符的使用 掌握混合运算的数据转换方法 2.1 C语言的数据类 ...

  3. 数据类型,运算符和表达式02 - 零基础入门学习C语言03

    第二章:数据类型,运算符和表达式02 让编程改变世界 Change the world by program 整型变量 整型变量的分类(注意:这里占多少个字节跟系统和编译器规定有关!可以在编译器上自己 ...

  4. c语言程序设计课件第二章,c语言程序设计课件张元国 ISBN9787566300386 PPT第二章数据类型 运算符与表达式...

    1.第2章 数据类型.运算符与表达式,语言的数据类型 常量与变量 运算符与表达式 不同类型数据间的转换,2.1语言的数据类型,数据是计算机程序处理的所有信息的总称,数值.字符.文本等都是数据,在各种程 ...

  5. c语言常数-ox6a是什么意思,C语言第2讲-数据类型运算符和表达式.pdf

    C语言第2讲-数据类型运算符和表达式 高级语言程序设计 (C Programming) 丁嵘 第二讲:数据类型.运算符和表达式 本章目录  2.1 基本符号  2.2 C语言的数据类型  2.3 ...

  6. 数据类型,运算符和表达式03 - 零基础入门学习C语言04

    第二章:数据类型,运算符和表达式03 让编程改变世界 Change the world by program 字符型数据 字符型数据包括字符常量和字符变量 字符常量: 是用单引号括起来的一个字符. 例 ...

  7. c语言表达式的类型,C语言_类型、运算符与表达式

    类型.运算符与表达式 变量和常量是C语言中的基本数据对象. 1.变量与变量名 局部变量:较短的变量名 外部变量:较长的变量名 变量名使用小写字母,符号常量名使用大写字母. 2.数据类型与长度 字符型 ...

  8. 51单片机c语言除法符号,单片机c语言教程:C51运算符和表达式

    上两课说了常量和变量,先来补充一个用以重新定义数据类型的的语句吧.这个语句就是 typedef,这是个很好用的语句,但我却不常用它,通常我定义变量的数据类型时都是使 用标准的关键字,这样别人能很方便的 ...

  9. 鸡啄米:C++编程入门系列之五(运算符和表达式)

    转载自: http://www.jizhuomi.com/software/32.html 上一讲鸡啄米给大家讲了一些数据类型,这一讲主要讲解编程入门知识-运算符和表达式.运算符,顾名思义,就是用于计 ...

  10. c语言运算符 %3c%3c4,C语言学习笔记二---数据类型运算符与表达式

    一.C的基本语法单位 1.标识符:有效长度:31(DOS环境下) 2.关键字:main不是 3.分隔符:空格符,制表符,换行符,换页符 4.注释符:a./*.....*/   b.// 二.C的常用输 ...

最新文章

  1. Mysql Binlog三种格式详细介绍
  2. 最大期望算法与混合高斯模型的推导
  3. java 字符数组与字符串_用于字符串和数组的5种简单有效的Java技术
  4. Java中加载properties文件的6种方法
  5. Thread源码-----传实现了Runnable接口的类的实例给Thread的作用
  6. 中国海洋大学c语言程序设计,中国海洋大学C语言级期末卷.pptx
  7. [转] linux 下查看一个进程运行路径的方法
  8. C#初学者教程系列19:Winform应用程序
  9. 五大常用算法之贪心算法
  10. 多媒体技术是指运用计算机,多媒体技术的概念与应用
  11. 电机振动噪声(NVH)——整数槽分数槽谐波分析补充
  12. nas文件服务器权限安全,大势至局域网共享文件监控NAS文件权限设置的方法
  13. 用Python制作核酸检测日历
  14. 企业竞争竞争情报系统的流程整合
  15. linux centos杀毒软件,在CentOS系统上安装开源杀毒软件ClamAV
  16. 计算机桌面保护时间的调整显示,电脑屏幕显示时间设置_电脑屏幕的显示时间...
  17. js下载文件格式为Excel后提示与文件扩展名不一致,打开文件前请验证文件没有损坏且来源可信.
  18. python支付宝二维码支付源代码
  19. 用jsp实现文件的下载(download.jsp)
  20. 安卓开发常用的adb命令

热门文章

  1. 物流智慧化主要技术及其应用
  2. TMS320F2812产生PWM波程序
  3. RxAndroid基础
  4. 体验完老罗的直播,我再也不敢了...
  5. 计算机声卡原理,关于声卡的性能的理解
  6. Tomcat应用部署是否要一个萝卜一个坑?
  7. um是代表什么意思_开平板规格代表什么意思?纯干货
  8. Charles的安装和配置
  9. android 11 机型,安卓11支持哪些机型,Android11适配机型一览
  10. 面经|小米-未来星-数据科学家|一面|70min