美文网首页
C语言08- 位运算,宏定义,递归

C语言08- 位运算,宏定义,递归

作者: sanpintian | 来源:发表于2018-01-07 23:25 被阅读0次

    16:位运算

    16.1:位运算概述

    二进制与位运算

    程序中的数据在内存中,都是以二进制的形式存在的。内存中的数据都是0和1组成的序列。
    
    位运算:是直接对整数在内存中的二进制位进行操作;直接对二进制位进行操作,使得位运算比普通的运算操作效率要高
    
    学习位运算,首先要学习什么是二进制位?
    1. byte字节
    2. bit位
    
    位运算操作符:
    1. 与(and):&
    2. 或(or):|
    3. 取反(not):~
    4. 异或(xor):^
    5. 左移(shl):<<
    6. 右移(shr/sar):>>
    
    

    16.2:与(and):&

    与运算:只有当2个数对应的位都为1,该位运算结果为1,否则运算结果为0。

    int main(void) {
        int a = 15;//1111
        int b = 10;//1010
        int c = 15&10;//1010
        printf(“a&b=%d\n”, c);//a&b=10
        
        return 0;
    }
    

    &应用:

    //1. 获取一个整数的后n位:(如现实中子网掩码)
    获取一个整数的后3位:
    int a=100;
    a&7(7的二进制码为0111)
    
    2. 使用与运算与取反和移位运算相结合,将整数的某位置零:
    a&(~(1<<n))//即可将x第n位置为零
    #define CLEARFLAG(a, n) ((a) &= ~(1<<(n)))//把整数a中第n位置为0
    
    3. 判断a中第n位是否为1:
    #define FLAGON(a,n) ((a)&(1<<(n)))//判断整数a中第n位是否为1
    
    4. 请出整数a二进制中最右边的1:
    a&(a-1)
    //计算整数有几个二进制1
    int count_ones(int x) {
        int count = 0;
        
        while(x) {
            count++;
            x &= (x-1);
        }
        
        return count
    }
    
    

    16.3:或(or):|

    |:只有当2个数对应的位都为0,该位运算结果为0,否则运算结果为1。

    int main(void) {
        int a = 15;//1111
        int b = 10;//1010
        int c = 15|10;//1111
        printf(“a|b=%d\n”, c);//a|b=15
        
        return 0;
    }
    
    |应用:
    //把整数a中的第n位置为1
    #define SETFLAG(a,n)  ((a) |= (1<<(n)))
    

    16.4:取反(not):~

    ~:单目运算符;是将一个整数中位为1的变成0,位为0的变成1。

    int main(void) {
        int a = 10;//1010
        int b = ~10;//0101
        printf(“~10=%d\n”, b);//5
        
        return 0;
    }
    
    ~应用:
    
    1.
    //取反运算同与运算结合,可以将一个整数的某位置0,比如:
    #define CLEARFLAG(a,n) ((a) &= ~(1<<(n)))//把整数a中第n位置为0
    

    16.5:异或(xor):^

    ^:相同为0,不同为1。

    int main(void) {
        int a = 15;//1111
        int b = 10;//1010
        int c = 15^10;//0101
        printf(“a^b=%d\n”, c);//a^b=5
        
        return 0;
    }
    
    //异或运算:
    1. 置零
    a^0=a
    a^a=0//xor eax,eax
    
    2. 两数交换
    #define SWAP(a,b) \
    do{\
    a=a^b;\
    b=a^b;\
    a=a^b;\
    }while(0)
    /*
    证明:
    假设:a和b的初始值:a=a0,b=b0;
    第一句:a=a^b即a为a0^b0
    第二句:b=a^b即(a0^b0)^b0=》a0^(b0^b0)=》a0^0=》a0
    第三句:a=a^b即a0^b0^a0=》a0^a0^b0=》0^b0=》b0
    因此,通过上面的推导,实现了a与b值的交换。
    */
    
    3. 单指针实现双链表
    /*
    可以用单个指针域来表示双向链表:
    有任意3个相邻结点:PL, P, PR
    那么P->next = PL^PR
    对于头结点来说:P没有左边的结点,所以左结点为NULL
    所以Head->next = NULL^PR
    
    对于尾结点来说:
    尾结点没有右边的结点,所以PR为NULL
    Tail->next = PL^NULL
    
    按照上述规则生成链表之后,遍历方法如下:
    从左往右遍历链表:
    pl=NULL;
    p=Head;
    
    while(p!=Tail) {
        pr=pl^(p->next);
        pl=p;
        p=pr;
    }
    
    从右往左遍历链表:
    pr=NULL;
    p=Tail;
    
    while(p!=Head) {
        pl=pr^(p->next);
        pr=p;
        p=pl;
    }
    */
    

    单指针双链表

    image.png

    16.6:移位运算左移(shl):<<

    <<:左移运算符;将一个数a向左移动n位记为:a<<n

    将一个数左移N位相当于将一个数乘以2^N

    int main(void) {
           int a = 10;
           int b = a<<2;
           printf(“b=%d\n”, b);//40
    
           return 0;
    
    }
    

    16.7:移位运算右移(shr/sar):>>

    >>:右移运算符;将一个数a向右移动n位记为:a>>n。

    右移动运算分为两种右移:

    1. 逻辑右移,在移动过程中,左边位用0填充。(有符号数:10000011,对于逻辑右移,向右移动3位,那么左边用0填充,变成了:00010000。)
    2. 算术右移,在移动过程中,左边用符号位来填充。(算术右移,向右移动3位,那么左边用1(1为符号位)填充,变成了11110000。而对于01000011,算术右移3位,那么左边用0(0为符号位)填充,变成了00001000。)

    在C语言中,右移运算符为算术右移运算符,即左边用符号位来填充。对于无符号数,而将一个数右移N位相当于将这个数除以2^N

    int main(void) {
        int a = -3;
        int b = 10;
        int c = a>>2;//
        int d = b>>1;//10/2^1=5
        printf(“a>>2=%d, b>>1=%d\n”, c, d);
        
        return 0; 
    }
    //证明:
    /*
        int a = -3;
        //-3原码10000000 00000000 00000000 00000011
        //-3反码11111111 11111111 11111111 11111100
        //-3补码11111111 11111111 11111111 11111101(加一)
        printf("a的16进制:%x\n", a);//0Xfffffffd
        int b = 10;
        int c = a>>2;//
        //-3补码11111111 11111111 11111111 11111101>>2
        //c补码11111111 11111111 11111111 11111111
        //c反码11111111 11111111 11111111 11111110(减一)
        //c原码10000000 00000000 00000000 00000001(即-1)
        printf("c的16进制:%x\n", c);//0Xffffffff
        int d = b>>1;//10/2^1=5
        printf("a>>2=%d, b>>1=%d\n", c, d);
    */
    

    16.8:位运算综合应用:

    
    1. 将第n位置1或者清零(n是从零开始计算的):
    //把整数a中的第n位置为1
    #define SETFLAGE(a, n) ((a)|(1<<(n)))
    //把整数a中的第n位置为0
    #define CLEARFLAG(a, n) ((a)&(~(1<<(n))))
    //判断整数a中第n位是否为1
    #define FLAGON(a, n) ((a)&(1<<(n)))
    
    #define BIT(n) (1<<n)
    //置1:
    a |= BIT(n);
    //置0:
    a &= ~BIT
    //判断:
    a & BIT(n)
    
    2. 对称加密:XOR
    异或性质:A^0==A, A^A==0
    A为明文,B为密文
    加密:B=A^key
    解密:A=B^key
    
    void func(char *data, int datalen, char *key, int keylen) {
        int i = 0;
        
        for (i=0; i<datalen; i++) {
            data[i] = (char)(data[i] ^ key[i%keylen])
        }
    }
    

    常见的位运算:


    image.png

    位运算在实际项目中的运用

    //先将该整数右移24位,与0xFF做与运算,即获得该8位的值。高8位记录了对文件的修改标记:
    ulDisposition = (lpIrpStack->Parameters.Create.Options >> 24) & 0xFF;
    
    //清除或者判断某些标志:
    #define FlagOn(_F, _SF) ((_F)&(_SF))//查看_F是否包含_SF
    
    mov eax, CRO
    add eax, 0FFFEFFFFh//相当于CR0 &= 0xFFFEFFFF;
    mov CRO, eax
    
    mov eax, CRO
    or  eax, NOT OFFFEFFFFh//CR0 |= ~0xFFFEFFFF
    mov CRO, eax 
    

    17:宏定义

    预处理->编译->链接->可执行文件(预处理将由预处理程序负责完成)。

    程序在编译预处理的阶段,不仅提供了宏定义的功能,还提供了文件包含以及条件编译的功能。

    宏在编译前预处理阶段,就已经完成了替换

    17.1:宏的定义

    1. 宏定义:指用一个标志符代表一个字符串,该标志符就称为宏名。
    2. 宏展开:在程序编译预处理阶段,所有宏名将会被替换为宏定义中的字符串的操作。

    其定义格式如下(/其标识符,称为宏名):

    //1. 不带参数格式:#define 标识符 字符串
    //2. 带参数的格式:#define 标识符(参数表) 字符串
    
    //例子:
    #define PI 3.14
    float calccirclearea(float r) {
           return PI*r*r;
    }
    
    #define MAX(X, Y) (X) > (Y) ? (X) : (Y)
    void main(void) {
           int a = 10;
           int b = 11;
           int c = MAX(a, b);
    
           printf(“max = %d\n”, c);
    }
    

    宏定义的优缺点:

    #define PI 3.14
    1. 有意义
    2. 减少修改
    3. 无类型检查
    4. 无法调试
    (宏不是变量,也没有类型,不占内存)
    
    const float pi=3.14f;
    1. 有意义
    2. 减少修改
    3. 有类型
    4. 可调试
    

    带参数的宏与函数的优缺点比较:

    1. 宏的效率要高(inline),没有了函数调用过程中的进栈出栈传参拷贝和出栈栈平衡;
    2. 宏无法调试
    3. 宏无法做到类型检查
    4. 传参的计算也不同,宏是简单的替换;函数先计算,再传参。
    
    如:
    #define MAX(a, b) ((a)>(b)?(a):(b))
    
    int getMax(int x, int y) {
        return x>y?x:y;
    }
    
    MAX(1+2, value)-->((1+2)>(value)?(1+2):(value));
    getMax(1+2, value);
    

    17.2:宏的应用与注意事项

    1. 宏名一般用大写
    2. 使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。例如:数组大小常用宏定义
    3. 预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查。
    4. 定义末尾不加分号;
    5. 宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头。
    6. 可以用#undef命令终止宏定义的作用域
    7. 宏定义允许嵌套
    8. 字符串" "中永远不包含宏
    9. 宏定义不分配内存,变量定义分配内存。
    10. 宏定义不存在类型问题,它的参数也是无类型的。
    1. #,宏把#的内容当做一个字符串替换
    #define CAT(c) "123"#c ---> CAT(abc) "123""abc" ===> "123abc"
    #define FDR(c) #c ---> FDR(a) ===> "a"
    
    2. ##,用于把两侧的参数合并为一个符号
    #define combine(a, b, c) a##b##c ---> combine(1, 2, 3) ===> "123"
    #define WIDE(str) L##str ---> WIDE("adb") ===> L"abc"
    

    17.3:条件编译

    条件编译:只有在符合条件下,代码才能编译进程序。

    预编译格式:

    1.
    #ifdef  标识符
      程序段1
    #else
      程序段2
    #endif
    
    2.
    #ifndef  标识符
      程序段1
    #else
      程序段2
    #endif
    
    3.
    #if 常量表达式
        程序段1
    #else 
        程序段2
    #endif
    
    4.
    #if 表达式1
        语句段1
    #elif 表达式2
        语句段2
    #else
        语句段3
    #endif
    

    18:递归

    递归:指某个函数直接或间接的调用自身;

    18.1:递归定义

    如果确定递归:

    1. 递归首先需要有一个或者多个递归出口(即递归终止的条件) ,也就是最小子问题的求解
    2. 递归需要一个递归式,这个递归式规定如何将原问题划分成子问题(子问题解决的对象和原问题解决的对象性质一样)
    //阶乘
    int fact(unsigned int  n) {
        if(n==0)
            return 1;//递归出口
        return n*fact(n-1);//n*Fact(n-1)就是递归式,将求n的阶乘,转化为子问题求n-1的阶乘
    }
    
    int main(void) {
         printf("10!=%d\n", fact(10));
         return 0;
    }
    
    //斐波那契数列
    //斐波那契数列指的是这样一个数列  1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...这个数列从第三项开始,每一项都等于前两项之和。
    因此可以写出它的递归函数与递归出口:
    /*
    f(1) = 1;
    f(2) = 1;
    f(n) = f(n-1) + f(n-2) n > 2
    最简子问题(递归出口):f(1),f(2)
    子问题转化:f(n)=f(n-1)+f(n-2) n>2
    */
    
    unsigned long feibo(unsigned int n) {
        if (n==1 || n==2) {
            return 1;
        } else {
            return feibo(n-1) + feibo(n-2);
        }
    }
    

    递归的优缺点:

    1. 递归的时间复杂度和对应的非递归差不多,但递归的效率是低不少(主要原因:反复函数调用和进栈出栈,各种终端等机制)。递归嵌套过甚过多消耗栈内存,回造成栈溢出。
    2. 内核开发不能使用递归,因为栈只有几K

    18.2:递归的应用

    //1. 不允许使用任何全局或局部变量编写 int strlen(char *s),计算字符串的长度
    size_t strlen(const char *s) {
        if(s==NULL || *s==’\0’) {
            return 0;
        }
    
        return 1+strlen(s+1);
    }
    
    size_t strlen(const char* s) {
        return (s==NULL||*s==’\0’)?0:1+strlen(s+1);
    }
    
    //2. 请反向的输出一个字符串:比如”hello, world!”,打印出来是:”!dlrow, olleh”。
    void inverse_print(char *s) {
        if(*s=='\0'||s==NULL )
             return;
    
        inverse_print(s+1);//先递归反向打印s的子串s+1
    
        printf( "%c", *s);
    }
    
    //3. 字符串逆置
    void reverse_str(char *str,size_t len) {
        if(str==NULL || len==1||len==0)
            return;
    
        reverse_str(str+1,len-2);//先逆置子串
        char tmp=*str;//再交换主串最左边与最右边的字符
        *str=*(str+len-1)
        *(st+len-1)=tmp;
    }
    

    相关文章

      网友评论

          本文标题:C语言08- 位运算,宏定义,递归

          本文链接:https://www.haomeiwen.com/subject/zpcvnxtx.html