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

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;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • •1 C语言程序的结构认识 用一个简单的c程序例子,介绍c语言的基本构成、格式、以及良好的书写风格,使读者对c语...
    CONLYOUC阅读 8,696评论 9 66
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,504评论 1 51
  • 谨记: 上帝在缔造每个人的时候,给予每个人的一切都是相同的,然而,人与人却是存在如此大的差距,记着上帝为你关上了一...
    长风留言阅读 4,856评论 0 5
  • 标题:用情商思维来解读步步高招生话术 昨天对我团队的成员进行了话术抽查,无非就是以家长的角色来咨询他们一些师资和费...
    爱折腾的小铭同学阅读 728评论 0 1
  • 王晓瑜 咖啡一杯细啜饮,遥望银河烟雨飞,郎君鼾声甜醉梦,疑是吴刚话月魂!
    王晓瑜阅读 403评论 3 2