C程序设计语言_1(类型,运算符,表达式)

306 阅读15分钟

C程序设计语言_1

类型,运算符与表达式

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

变量名

  1. 变量名是由字母和数字组成的序列,但是其第一个字符必须是字母

  2. 下划线通常被看作字母,用于命名较长的变量名。

  3. 库例程(系统API,服务,库函数)通常以下划线开头,所以变量名不要下划线开头。

  4. 大写字母和小写字母有区别。

  5. 传统的C语言用法中,变量名使用小写字母,符号常量全部使用大写字母。

数据类型以及长度

基本数据类型:

  • char 字符型,占用一个字节,可以存放一个本地字符集中的一个字符
  • int 整数,通常反映了所用机器中整数的最自然长度(16位/32位,也就是2字节/4字节)
  • float 单精度浮点数
  • double 双精度浮点数

限定符

  • shortlong限定符可以用于限定整形:

    short int sh;
    long int counter;
    

    关键字int可以省略。

    short类型通常为16位,long类型通常为32位,int类型可以为16位或32位。

    各编译器可以自主选择合适的类型长度,但是short与int类型至少为16位,而long类型至少位32位,short类型不得长于int类型,int类型不得长于long类型。

  • signedunsigned可以用于限定char类型或任何整数。

    unsigned类型的数总是正值或0,并遵守算数模2n定律,其中n是该类型占用的位数。

    如果char对象占8位,那么unsigned char类型变量的取值范围为0255,而signed char类型变量的取值范围则为-128127(采用二进制补码)。

  • long double类型表示高精度的浮点数,参考long int。

练习:

​ 编写一个程序以确定分别由signed以及unsigned限定的char,short,int与long类型变量的取值范围。采用打印标准头文件中的相应值以及直接计算两种方式实现。

  • 直接打印:

    源代码:

    #include <stdio.h>
    #include <limits.h>
    
    int main(){
        // sigend types
        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);
    
        // unsigned types
        printf("unsigned char max  = %u\n", UCHAR_MAX);
        printf("unsigned short max = %u\n", USHRT_MAX);
        printf("unsigned int max   = %u\n", UINT_MAX);
        printf("unsigned long max  = %lu\n", ULONG_MAX);
    }
    

    运行结果:

    # ubuntu @ VM-0-12-ubuntu in ~/C++/C [11:29:32] 
    $ ./a.out   
    signed char min    = -128
    signed char max    = 127
    signed short min   = -32768
    signed short max   = 32767
    signed int min     = -2147483648
    signed int max     = 2147483647
    signed long min    = -9223372036854775808
    signed long max    = 9223372036854775807
    unsigned char max  = 255
    unsigned short max = 65535
    unsigned int max   = 4294967295
    unsigned long max  = 18446744073709551615
    
  • 利用按位运算符进行计算:

    源代码:

    #include <stdio.h>
    #include <limits.h>
    
    int main(){
        // sigend types
        printf("signed char min    = %d\n", -(char)((unsigned char) ~0 >> 1) - 1);
        printf("signed char max    = %d\n", (char)((unsigned char) ~0 >> 1));
        printf("signed short min   = %d\n", -(short int)((unsigned short int) ~0 >> 1) - 1);
        printf("signed short max   = %d\n", (short int)((unsigned short int) ~0 >> 1));
        printf("signed int min     = %d\n", -(int)((unsigned int) ~0 >> 1) - 1);
        printf("signed int max     = %d\n", (int)((unsigned int) ~0 >> 1));
        printf("signed long min    = %ld\n", -(long int)((unsigned long int) ~0 >> 1) - 1);
        printf("signed long max    = %ld\n", (long int)((unsigned long int) ~0 >> 1));
    
        // unsigned types
        printf("unsigned char max  = %u\n", (unsigned char) ~0);
        printf("unsigned short max = %u\n", (unsigned short int) ~0);
        printf("unsigned int max   = %u\n", (unsigned int) ~0);
        printf("unsigned long max  = %lu\n", (unsigned long) ~0);
    
        return 0;
    }
    

    运行结果:

    # ubuntu @ VM-0-12-ubuntu in ~/C++/C [15:54:19] 
    $ ./a.out
    signed char min    = -128
    signed char max    = 127
    signed short min   = -32768
    signed short max   = 32767
    signed int min     = -2147483648
    signed int max     = 2147483647
    signed long min    = -9223372036854775808
    signed long max    = 9223372036854775807
    unsigned char max  = 255
    unsigned short max = 65535
    unsigned int max   = 4294967295
    unsigned long max  = 18446744073709551615
    

常量

数字常量

  1. 类似于1234的整数常量属于int类型。
  2. long类型的常量以字母l或L结尾,如12345678L。
  3. 如果一个整数太大以至于无法用int类型表示,也将被当作long类型处理。
  4. 无符号常量以字母uU结尾。后缀ulUL表示unsigned long类型。
  5. 浮点数常量中包含一个小数点(如123.4)或一个指数(如1e-2),也可以两者都有。
  6. 没有后缀的浮点数常量为double类型。后缀fF表示float类型,而后缀lL则表示long double类型。
  7. 整型数除了用十进制表示外,还可以用八进制或十六进制表示。带前缀0的整型常量表示它为八进制形式,前缀0x0X,则表示它为十六进制形式。
  8. 八进制与十六进制的常量也可以使用后缀L表示long类型,使用后缀U表示unsigned类型。

字符常量(含转义字符常量)

  • 一个字符常量是一个整数,书写时将一个字符括在单括号中,例如'x'。

  • 字符在机器字符集中的数值就是字符常量的值。

    例如,在ASCII字符集中,字符'0'的值为48,它与数值0没有关系。

    如果用字符'0'代替这个与具体字符集有关的值,那么,程序员就无须关心该字符对应的具体值,增加了程序的易读性。

  • 字符常量一般用来与其他字符进行比较,但也可以像其他整数一样参与数值运算。

  • 某些字符可以通过转义字符序列表示为字符和字符串常量。转义字符序列看起来像两个字符,但只表示一个字符。

  • 另外,我们可以用***'\ooo'表示任意的字节大小的位模式。其中ooo代表1~3个八进制数*。

    这种位模式还可以用***‘\xhh'表示,其中hh是一个或多个十六进制数*(09,af,A~F)。

    因此,我们可以按照下列形式书写语句:

    #define VTAB '\013'		// ASCII纵向制符表
    
    #define VTAB '\xb'		// ASCII纵向制附表
    

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

    字符转义含义字符转义含义
    \a响铃符\\反斜杠
    \b回退符?问号
    \f换页符\'单引号
    \n换行符\"双引号
    \r回车符\ooo八进制数
    \t横向制表符\xhh十六进制数
    \v纵向制表符
  • 字符常量'\0'表示值为0的字符,也就是空字符(null)。

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

字符串常量

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

    例如"I am a string",或""(空字符串)。都是字符串。

  • 双引号不是字符串的一部分,它只用于限定字符串。

  • 字符常量中使用的转义字符序列同样可以用在字符串中。

    在字符串中使用\"表示双引号字符。

  • 编译时可以将多个字符串常量连接起来,例如

    ”hello," "world"
    

    等价于

    "hello, world"
    

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

'x'和"x"是不同的,前者是一个整数,其值是字母x在机器字符集中对应的数值;后者是一个包含一个字符以及一个结束符'\0'的字符数组。

枚举常量

  • 枚举常量是另一种类型的常量。枚举是一个常量整型值的列表。例如:enum boolean { NO, YES };

  • 在没有显示说明的情况下,enum类型中第一个枚举名的值为0,第二个为1,以此类推。

  • 如果只指定了部分枚举名的值,那么未指定的枚举名的值将会依着最后一个指定值向后递增,参考下面两个例子中的第二个例子

    enum escapes {BELL = '\a', BACKSPACE = '\b', TAB = '\t', NEWLINE = '\n'};
    
    enum months {JAN = 1, FEB, MAR, APR, JUN, JUL, AUG, SEP, OCT, NOV, DEC};
    // 其中FEB的值为2,MAR的值为3,
    
  • 不同枚举中的名字必须互不相同,同一枚举中不同的名字可以具有相同的值。

声明(const限定符,初始化值)

​ 所有变量都必须先声明后使用,尽管某些变量可以通过上下文隐式地声明。一个声明指定一种变量类型,后面所带的变量表可以包含一个或多个该类型的变量。还可以在声明的同时对变量进行初始化。在声明中,如果变量名的后面紧跟一个等号以及一个表达式,该表达式就充当对变量进行初始化的初始化表达式。

​ 如果变量不是自动变量,则只能进行一次初始化操作。***每次进入函数或程序块时,显式初始化的自动变量都将被初始化一次,***其初始化表达式可以是任何表达式。默认情况下,外部变量与静态变量都将被初始化为0。未经显式初始化的自动变量的值为未定义值。

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

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

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

int strlen(const char[]);

算数运算符

  • 二元算术运算符包括:+, -, *, /, %。整数除法会截断结果中的小数部分。

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

  • 算术运算符采用从左到右的结合规则。

  • 优先级:一元+, - > *, /, % > 二元+, -

关系运算符与逻辑运算符

  • 关系运算符包括下面几个运算符:>, >=, <, <=

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

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

  • 逻辑运算符**&&||**连接的表达式按从左到右的顺序进行求值,

    并且,在知道结果值的真假后立即停止计算。

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

  • 逻辑非运算符 ! 的作用是将非0操作数转换为0,将操作数0转换为1

练习:

​ 在不使用运算符&&或||的条件下编写一个与下面的for循环语句等价的循环语句。

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

​ 源代码:

i = 0;
while(i < lim - 1){
	if((c = getchar()) == '\n')
		break;
	else if (c == EOF)
		break;
	s[i] = c;
	++i;
}

类型转换

  • 当一个运算符的几个操作数类型不同时,就需要通过一些规则把它们转换为某种共同的类型。

  • 自动转换是指把“比较窄的”操作数转换为“比较宽的”操作数,并且不丢失信息的转换。

  • 针对可能导致信息丢失的表达式,编译器可能会给出警告信息,比如把较长的整型值赋给较短的整型变量,但这些表达式并不非法。


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

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;
}

​ 将字符类型转换为整型时,我们需要注意一点。C语言没有指定char类型的变量时无符号变量还是带符号变量。当把一个char类型的值转换为int类型的值时,其结果也可能为负整数,这取决于与机器的类型。在某些机器中,如果char类型值的最左一边为1,则转换为负整数(进行“符号扩展”)。而在另一些机器中,把char类型值转换为int类型值时,在char类型值的左边添加0,这样转换的结果值总是正值。

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


​ C语言中很多情况下会进行隐式的算术类型转换。一般来说,如果二元运算符的两个操作数具有不同的类型,那么在进行运算之前,要先把较低的类型提升为较高的类型。运算的结果为较高的类型。但是float类型的操作数不会自动转换为double类型,这与最初的定义不同,一般来说数学函数使用双精度类型的变量。使用float类型主要为了在使用较大的数组时节省存储空间,有时也为了节省机器执行时间。


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


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


​ 无论是否进行符号扩展,字符型变量都将被转换位整型变量。当把较长的整数转换为较短的整数或char类型时,超出的高位部分将被丢弃。


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


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

(类型名) 表达式

​ 在上述语句中,表达式首先被赋值给类型指定的类型的某个变量,然后再用该变量替换上述整条语句。

​ 注意,强制类型转换只是生成一个指定类型的n的值,n的值并没有改变。

练习:

​ 编写函数htoi(s),把由十六进制数字组成的字符串(包含可选前缀0x或0X)转换为与之等价的整型值。字符串中允许包含的数字包括:09,af以及A~F。

源代码:

#include <stdio.h>
#include <limits.h>

#define MAXLENGTH 10

int my_getline(char s[]);
long long my_htoi(char s[]);

int main(){
    int len;
    long long value;
    char s[MAXLENGTH];
    printf("请输入要转换的十六进制数:");
    len = my_getline(s);
    if((value = my_htoi(s)) != -1){
        printf("转换为十进制数为:%lld\n", value);
    }
    else
        printf("你输入的十六进制数的格式不对\n");
    return 0;
}

int my_getline(char s[]){
    int c, i;
    for(i = 0; i < MAXLENGTH - 1 && (c = getchar()) != '\n' && c != EOF; ++i)
        s[i] = c;
    if(c == '\n'){
        s[i] = c;
        ++i;
    }
    s[i] = '\0';
    return i;
}

long long my_htoi(char s[]){
    int i, j, p, v;
    long long R;
    R = 0;
    if(s[0] != '0'){
        return -1;
    }
    else if(s[1] != 'X' && s[1] != 'x'){
        return -1;
    }
    else{
        for(i = 2; s[i] != '\n'; ++i)
            ;
        --i;
        for(j = 0; i - j >= 2; ++j){
            p = i - j;
            if(s[p] >= '0' && s[p] <= '9')
                v = s[p] - '0';
            else if(s[p] >= 'a' && s[p] <= 'f')
                v = s[p] - 'a' + 10;
            else if(s[p] >= 'A' && s[p] <= 'F')
                v = s[p] - 'A' + 10;
            else
                return -1;
            v = v << (j * 4);
            R += v;
        }
    }
    return R;
}

运行结果:

# ubuntu @ VM-0-12-ubuntu in ~/C++/C [1:45:28] 
$ ./a.out   
请输入要转换的十六进制数:0x12cf
转换为十进制数为:4815

# ubuntu @ VM-0-12-ubuntu in ~/C++/C [1:45:54] 
$ ./a.out
请输入要转换的十六进制数:0xCF12
转换为十进制数为:53010

# ubuntu @ VM-0-12-ubuntu in ~/C++/C [1:46:03] 
$ ./a.out
请输入要转换的十六进制数:1234
你输入的十六进制数的格式不对

# ubuntu @ VM-0-12-ubuntu in ~/C++/C [1:46:13] 
$ ./a.out
请输入要转换的十六进制数:0x12ag
你输入的十六进制数的格式不对

# ubuntu @ VM-0-12-ubuntu in ~/C++/C [1:46:24] 
$ ./a.out           
请输入要转换的十六进制数:0x1A2B3C4F
转换为十进制数为:439041103

# ubuntu @ VM-0-12-ubuntu in ~/C++/C [1:46:42] 
$ ./a.out
请输入要转换的十六进制数:0x1a2b3c4f
转换为十进制数为:439041103

自增运算符与自减运算符

​ C语言提供了两个用于变量递增与递减的特殊运算符。自增运算符**++使其操作数递增1,自减运算符--**使其操作数递减1。

​ **++--**这两个运算符特殊的地方主要表现在,它们既可以用作前缀运算符(++n),也可以用作后缀运算符(--n)。在这两种情况下,其效果都是将变量n的值加1。但是,它们之间有一点不同。表达式++n先将n的值递增1,然后再使用变量n的值,而表达式n++则是先使用变量n的值,然后再将n的值递增1。

​ 下面是一个例子,函数squeeze(s, c),它删除字符串s中出现的所有字符c:

void squeeze(char s[], int c){
	int i, j;
	
	for(i = j = 0; s[i] != '\0'; i++)
		if(s[i] != c)
			s[j++] = s[i];
	s[j] = '\0';
}

​ 另一个例子,标准函数strcat(s, t),它将字符串t连接到字符串s的尾部。函数strcat假定字符串s中有足够的空间保存这两个字符串连接的结果。

void strcat(char s[], char t[]){
	int i, j;
	
	i = j = 0;
	while(s[i] != '\0')
		i++;
	while((s[i++] = t[j++]) != '\0')
		;
}

练习:

  • 重新编写squeeze(s1, s2),将字符串s1中任何与字符串s2中的字符匹配的字符都删除。

    源代码:

    #include <stdio.h>
    #include <limits.h>
    
    #define MAXLENGTH 20
    
    int my_getline(char s[]);
    void squeeze(char s1[], char s2[]);
    
    int main(){
        int len;
        long long value;
        char s1[MAXLENGTH];
        char s2[MAXLENGTH];
        printf("请输入一个字符串:");
        my_getline(s1);
        printf("请输入你希望去除的字符:");
        my_getline(s2);
        squeeze(s1, s2);
        printf("处理后的字符串为:%s\n", s1);
        return 0;
    }
    
    int my_getline(char s[]){
        int c, i;
        for(i = 0; i < MAXLENGTH - 1 && (c = getchar()) != '\n' && c != EOF; ++i)
            s[i] = c;
        if(c == '\n'){
            s[i] = c;
            ++i;
        }
        s[i] = '\0';
        return i;
    }
    
    /*
    void squeeze(char s[], char c){
        int i, j;
        for(i = 0; s[i] != '\0'; ++i){
            // 如果是重复的话,j就不会增加,那么重复的字符就会被覆盖。
            if(s[i] != c)
                s[j++] = s[i];
        }
    }
    */
    
    void squeeze(char s1[], char s2[]){
        int i, j, n;
    
        for(n = 0; s2[n] != '\0'; ++n){
            for(i = j = 0; s1[j] != '\0'; ++i)
                if(s1[i] != s2[n] )
                    s1[j++] = s1[i];
        }
    }
    

    运行结果:

    # ubuntu @ VM-0-12-ubuntu in ~/C++/C [19:17:20] 
    $ ./a.out   
    请输入一个字符串:abcdefg1234
    请输入你希望去除的字符:ceg3
    处理后的字符串为:abdf124
    
    # ubuntu @ VM-0-12-ubuntu in ~/C++/C [19:17:37] 
    $ ./a.out
    请输入一个字符串:1234
    请输入你希望去除的字符:2134
    处理后的字符串为:
    
    # ubuntu @ VM-0-12-ubuntu in ~/C++/C [19:17:50] 
    $ ./a.out
    请输入一个字符串:aabbcc
    请输入你希望去除的字符:abc
    处理后的字符串为:
    
  • 编写函数any(s1, s2),将字符串s2中的任一字符在s1中第一次出现的位置作为结果返回。如果s1中不包含s2中的字符,则返回-1。

    源代码:

    #include <stdio.h>
    #include <limits.h>
    
    #define MAXLENGTH 1000
    
    int my_getline(char s[]);
    int* any(char s1[], char s2[]);
    
    int main(){
        int len, i;
        int* index;
        long long value;
        char s1[MAXLENGTH];
        char s2[MAXLENGTH];
    
        printf("请输入一个字符串:");
        my_getline(s1);
        printf("请输入你希望定位的字符串:");
        len = my_getline(s2);
        printf("它们的位置为:");
        index = any(s1, s2);
        for(i = 0; i <= len - 2; i++){
            printf("%2d ", index[i]);
        }
    
        return 0;
    }
    
    int my_getline(char s[]){
        int c, i;
    
        for(i = 0; i < MAXLENGTH - 1 && (c = getchar()) != '\n' && c != EOF; ++i)
            s[i] = c;
        if(c == '\n'){
            s[i] = c;
            ++i;
        }
        s[i] = '\0';
    
        return i;
    }
    
    int* any(char s1[], char s2[]){
        static int place[MAXLENGTH];
        int i, j;
    
        for(i = 0; s2[i] != '\n'; ++i){
            place[i] = -1;
            for(j = 0; s1[j] != '\n'; ++j){
                if(s2[i] == s1[j]){
                    place[i] = j + 1;
                    break;
                }
            }
        }
    
        return place;
    }
    

    运行结果:

    PS C:\Users\14703\Documents\Program Source Code\C> .\test.exe
    请输入一个字符串:1234cdaf
    请输入你希望定位的字符串:a2fh
    它们的位置为: 7  2  8 -1
    

位运算符

  • C语言提供了6个位操作运算符。这些运算符只能作用于整型操作数,即只能作用于带符号或无符号的char,short,int与long类型:&, |, ^, <<, >>, ~

  • 按位与运算符&经常用于屏蔽某些二进制位,例如:n = n & 0177; 该语句将n中除了7个低位二进制位外的其他各位均置为0。

  • 按位或运算符|经常用于将某些二进制位置为1,例如:x = x | SET_ON该语句将x中对应于SET_ON中为1的那些二进制位置为1。

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

  • 一个例子:函数getbits(x, p, n),它返回x中从右边第p位开始向右数n位的字段。这里假定最右边的一位是第0位,n与p都是合理的正值。例如,getbits(x, 4, 3)返回x中第4,3,2三位的值。

    unsigned getbits(unsigned x, int p, int n){
    	return x >> (p+1-n) & ~(~0 << n);
    }
    

练习:

  • 编写一个函数setbits(x, p, n, y),该函数返回对x执行下列操作后的结果值:将x中从第p位开始的n个(二进制位)位设置为y中最右边n位的值,x的其余各位保持不变。

    分析:

    0 1 1 0 | 1 0 1 0 | 1 0 0 0 | 0 1 1 0 x

    1 1 1 0 | 0 1 1 1 | 1 1 0 1 | 0 0 1 1 y

    将x中从第p = 7位开始的n = 5个位设置为y中的最右边的n = 5位

    也就是最后要得到:

    0 1 1 0 | 1 0 1 0 | 1 0 0 1 | 1 1 1 0


    处理x中要改变的位为0: 首先得到屏蔽码: ~0 << n = 1 1 1 1 | 1 1 1 1 | 1 1 1 0 | 0 0 0 0

    ~(0 << n) = 0 0 0 0 | 0 0 0 0 | 0 0 0 1 | 1 1 1 1

    ~(0 << n) << (p + 1 - n) = 0 0 0 0 | 0 0 0 0 | 1 1 1 1 | 1 0 0 0

    ((0 << n) << (p + 1 - n)) = 1 1 1 1 | 1 1 1 1 | 0 0 0 0 | 0 1 1 1

    然后使用&屏蔽: x & (((0 << n) << (p + 1 - n))) = 0 1 1 0 | 1 0 1 0 | 0 0 0 0 | 0 1 1 0


    得到y中要改变为的目标值: y & (~(0 << n)) = 0 0 0 0 | 0 0 0 0 | 0 0 0 1 | 0 0 1 1

    将要要改变的目标值移动到对应的位置: (y & (~(0 << n))) << (p + 1 - n) = 0 0 0 0 | 0 0 0 0 | 1 0 0 1 | 1 0 0 0


    然后将屏蔽对应位后的x和要到改变的值进行或处理:0 | 1 = 1,0 | 0 = 0,这样就可以变为对应的值 (x & (((0 << n) << (p + 1 - n)))) | ((y & (~(0 << n))) << (p + 1 - n)) = 0 1 1 0 | 1 0 1 0 | 1 0 0 1 | 1 1 1 0

    这样就完成了。

    源代码:

    unsigned setbits(unsigned x, int p, int n, unsigned y){
    	return (x & (~(~(0 << n) << (p + 1 - n)))) | ((y & (~(0 << n))) << (p + 1 - n));
    }
    
  • 编写一个函数invert(x, p, n),该函数返回对x执行下列操作后的结果值:将x中从第p位开始的n个(二进制)位求反(即1变为0,0变为1),x的其余位保持不变。

    分析:

    1 ^ 1 = 1, 0 ^ 1 = 0; 保持不变

    1 ^ 0 = 0, 0 ^ 0 = 1; 取反

    所以我们设置屏蔽位为0,其余位为1的屏蔽码,

    ((0 << n) << (p + 1 - n))

    然后将其和x异或就可以得到屏蔽位取反的值

    x ^ (((0 << n) << (p + 1 - n)))

    源代码:

    unsigned invert(unsigned x, int p, int n){
    	return x ^ (~(~(0 << n) << (p + 1 - n)));
    }
    
  • 编写一个函数rightrot(x, n),该函数返回将x循环右移(即从最右端移出的位将从最左端移入)n(二进制)位后所得到的值。

    分析:

    这里n的大小是有可能大于x所占的字长的,所以,不能简单的将x直接右移然后将被移动的位补充到左边。

    源代码:

    方案一

    unsigned rightrot(unsigned x, int n){
    	int wordlength(void);
    	unsigned rbit;
    	
    	while(n-- > 0){
    		rbit = x << (wordlength() - 1);
    		x = x >> 1;
    		x = x | rabit;
    	}
    	
    	return x;
    }
    
    int wordlength(void){
    	int i;
    	unsigned v = (unsigned) ~0;
    	
    	for(i = 1; ( v = v >> 1) > 0; i++)
    		;
    	
    	return i;
    }
    

    方案二

    unsigned rightrot(unsigned x, int n){
    	int wordlength(void);
    	unsigned rbits;
    	
    	if((n = n % wordlength()) > 0){
    		rbits = ~(~0 << n) & x;
    		
    		rbits = rbits << (wordlength() - n);
    		x = x >> n;
    		x = x | rbits;
    	}
    	
    	return x;
    }
    
    int wordlength(void){
    	int i;
    	unsigned v = (unsigned) ~0;
    	
    	for(i = 1; ( v = v >> 1) > 0; i++)
    		;
    	
    	return i;
    }
    

赋值运算符与表达式

​ 在赋值表达式中,如果表达式左边的变量重复出现在表达式的右边,比如:i = i + 2; 则可以将这种表达式缩写为下列形式:i += 2。其中的运算符+=称为赋值运算符。

​ 大多数二元运算符都有一个相应的赋值运算符op=,其中,op可以是下面这些运算符之一:+ - * / % << >> & ^ |

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

expr1 op= expr2

等价于:

expr1 = (expr1) op (expr2)

比如:x *= y + 1的含义是x = x * (y + 1)。而不是x = x * y + 1。

​ 再举一个例子,函数bitcount统计其整型参数的值为1的二进制位的个数。

int bitcount(unsigned x){
	int b;
	
	for(b = 0; x != 0; x >> 1)
		if(x & 01)
			b++;
	return b;
}

​ 在所有的单独赋值表达式中,赋值表达式的类型是它的左操作数的类型,值是赋值操作完成后的值。

练习:

​ 在求二进制补码时,表达式x&=(x-1)可以删除x中最右值为1的一个二进制位,请解释这样所的道理。用这一方法重写bitcount函数,以加快其执行速度。

​ 答:x - 1之后会导致最右边为1的位会变为0,然后右边其余位为0的变为1,x & (x - 1)之后,x最右边为1的位会和0进行&操作,变为0,而右边其余为0的位进行&操作之后仍然为0,所以x &= (x - 1)可以删除x中最右值为1的一个二进制位。

源代码:

int bitcount(unsigned x){
	int b;
	
	for(b = 0; x != 0; x &=(x-1))
		++b;
		
	return b;
}

条件表达式

​ 条件表达式使用三元运算符 ?:,形式如下:

expr1 ? expr2 : expr3

​ 表达式首先计算expr1,如果其值不等于0,则计算expr2的值,并以该值作为条件表达式的值,否则计算expr3的值,并以该值作为条件表达式的值。expr2与expr3中只能有一个表达式被计算。

​ 应该注意,条件表达式实际上就是一种表达式,它可以用在其他表达式可以使用的任何地方。

练习:

重新编写将大写字母转换为小写字母的函数lower,并用条件表达式代替if-else结构。

源代码:

int lower(int c){
	return (c >= 'A' && c <= 'Z') ? (c + 'a' - 'A') : c;
}

运算符优先级与求值次序

​ 下表总结了所有运算符的优先性和结合性,其中的一些规则我们还没有讲述。同一行的各个运算符具有相同的优先级,各行间从上往下优先级逐渐降低。

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

​ 同大多数编程语言一样,C语言没有指定同一运算符中多个操作数的计算顺序(&&, ||, ?,和“,"运算符除外。