- 提醒:不过多赘述「宏」以及本文相关的其它 C++ 基础知识,不明之处暂请自行 Google。
宏定义
即 Macro。
Print Expr & Result
便捷地打印程序输出:变量或表达式,以及计算结果。
重复
cout << variable << endl;这种用于打印程序输出的代码实在麻烦,但不得不写,
所以,为了更便捷地打印程序输出,写了一些辅助的「宏」。1- 关键:用宏定义的展开,减少代码量。
#x的语法可以获取x所代表的变量或表达式的具体内容。(x)中的括号不能去掉!否则可能会得出意想不到的错误结果。- 提示:在程序的预处理阶段,会展开代码中「宏定义」,即对「宏名」进行简单的字符串替换,详情自行 Google。
打印工具的宏定义
print_tools.h 1234567891011121314151617// 说明:"CPP_TEST" 为本 Demo 项目名using std::cout;using std::endl;// 打印字符串// 打印变量或表达式,及其结果// 打印一个空行(分隔不同的程序输出)- 说明:这种声明是为了避免项目重复引入该头文件(即
#include "print_tools.h")时,重复定义其中的内容。作用:防止重复定义 1234// …
- 说明:这种声明是为了避免项目重复引入该头文件(即
演示打印工具
main.cpp 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455#include <iostream>#include "print_tools.h" // 引入上述的宏//using namespace std; // 不将整个 std 引入,因为其中很多东西都用不上。/* 后文的样例中,需要额外引入的「库和对象」放这 */#include <map>using std::map;// …/* 宏定义 */#define MAX(a, b) ((abs((a) - (b)) == (a) - (b)) ? (a) : (b))// …/* 「函数前置声明」放这 */bool isIncr(int *ary, int count);// …int main() {/* 运行的「测试代码」放这 */l("test")int x = 1;int y = -1;v(x)v(x >> 31)v(y)v(y >> 31)ell("MAX")v(MAX(10, 20))v(MAX(-10, -20))ell("用一个递归算法,判断一个数组中的值,是否递增?")int ary = new int[5]{1, 4, 5, 9, 7};v(isIncr(ary, 5))delete []ary;ary = new int[5]{1, 4, 5, 7, 9};v(isIncr(ary, 5))delete []ary;el//…return 0;}/* 「函数定义」放这 */bool isIncr(int *ary, int count); {return count == 1|| (count != 0&& ary[count - 2] <= ary[count - 1]&& isIncr(ary, --count));}// …输出 123456789101112131415testx = 1x >> 31 = 0y = -1y >> 31 = -1MAXMAX(10, 20) = 20MAX(-10, -20) = -10用一个递归算法,判断一个数组中的值,是否递增?isIncr(ary, 5) = 0isIncr(ary, 5) = 1…进阶可参考《宏定义的黑魔法 - 宏菜鸟起飞手册》
数值的大小关系
判断两个数值的大小关系。
|
数值的 sign
判断 signed(有符号)类型的数值的 sign(正负符号)。
|
unsigned 数值类型
判断数值类型是否有符号(signed),即是否区分正负。
|
- 各种语言的数值类型基本都实现了正负值的区分(signed),
所以只使用unsigned来标识少数无符号的数值类型,而很少出现signed这种说法。
int 变量的 bytes
当前运行环境下(与 CPU 和 OS 有关),一个 int(整型)数值类型变量所占用的 bytes(字节数)。
|
|
宏定义语法 ##
## 用于连接前后两段代码。
|
|
|
|
|
- 解释:
因为CONCAT_CODE(2, 3)将代码2、e、3连接成2e3,
等于 int2 * pow(10, 3)=2 * 1000=2000。
位操作
即 Bit Operations,对数值(用二进制表示)的 bit(比特位)进行直接操作。
int 整型数值
整型,表示整数的数值类型。
- 下文中,进行位操作的变量的类型,主要为 int 等表示整数的数值类型。
暂不包括浮点数,float、double、long。 - int 可描述的数值范围:
- 32 位(bits)操作系统(OS)下,占用 4 bytes
-2^32~2^32 - 1(此处2^32表示 2 的 32 次方) - 64 位操作系统下,占用 8 bytes
-2^64~2^64 - 1 - 下文中,默认运行环境为 32 位的操作系统。
- 32 位(bits)操作系统(OS)下,占用 4 bytes
- 数值表示方式:
0x4F中的0x表示该数值在以 16 进制进行展示;'0010'B中的B表示该数值在以 2 进制进行展示。- 注意:C / C++ 中,不支持直接以 2 进制的形式声明数值;
如有需要,通常以 16 进制的形式声明和表示二进制数值(bits)。
- 以 16 进制展示数值:
0x 00 00 00 00= int00x 00 00 00 01= int+10x FF FF FF FF= int-1
- 下文默认你知晓「在底层如何以二进制比特位来储存和表示整型数值,特别是『负数』。」
- 负数:以二进制方式进行储存和表示时,最高位(最左)的 bit 是 1。
- int
0在物理层面使用了0x 00 00 00 00这个数值来表示,
所以,int 等整数数值类型,所能描述的正数的范围被比负数范围小 1。
移位求积
快速求 int 2 的 3 次方
每左移一位,数值增大 2 倍(除非溢出)12 << 3快速求 int x 的 7 倍
1(x << 3) - x
2 的幂
判断 int 数值是否为 2 的若干次幂(即 2 的 n 次方)。
数值在计算机中通常都以 0 和 1 的二进制形式存储着,
其实关键在于,这个问题等价于判断一个整数的二进制形式是否「有且只有一位 bit 为 1」。
|
|
思路:x - 1 等价于「二进制表示的整数 x,从最后(右)一位 1 开始(包括这个 1)的所有 bit 都取反」,
那么 x & (x - 1) 就是去除 x 二进制表示中的最后一个为 1 的 bit;
如果去除最后一位 1 后 x 变成了 0 了,它就是 2 的若干次幂。
|
|
求负数
以位操作的方式,求 int x 的负数。
|
|
- 不详述推导过程,只提供简单的记忆方式:
- 假设现在的运行环境是 8 位的操作系统,
'00 00 00 00'B= int0'00 00 00 01'B= int+1'11 11 11 11'B= int-1
- 对以上三个数值进行通过简单的观察和推算,就能得出上面提供的公式。
- 假设现在的运行环境是 8 位的操作系统,
求平均值
求两个 int 数值的平均值
一般求法
1(x + y) / 2缺点:如果
x + y的数值超出 int 可描述的数值范围(即溢出),会得出错误的结果。位操作的求法
1(x & y) + ((x ^ y) >> 1)提示:从二进制角度来说,
(x & y)等于「所有为1的 bit(比特位)相加的结果除以二」;(x ^ y) >> 1等于「将x、y两数相加等于1的 bit(一个为0另一个为1)除以二」。
求绝对值
以位操作的方式,求 int x 的绝对值。
「求负数」的延伸。以下为思路:
- sign(符号位)的计算:
x < 0 时,x >> 31=-1
x >= 0 时,x >> 31=0>>右移操作,最高位(最左)用原最高位的数值进行补位:
原最高位为 1,右移后,新的最高位补 1,
原最高位为 0,右移后,新的最高位补 0。
- 数值取反:
~x=-1 ^ x=0xFFFFFFFF ^ x- 代入求负数的公式:
-x=~x + 1=(-1 ^ x) + 1- 注意:
^>>等位操作符的优先级低于+-,
所以,书写时不能漏掉括号,以免算式出错。
- 注意:
- 代入求负数的公式:
- 补充条件:
x=0 ^ x(进一步观察得到的,可能需要灵感) - 观察规律:
x < 0 时,abs(x)=-x=(-1 ^ x) + 1=(-1 ^ x) - (-1)
x >= 0 时,abs(x)=x=0 ^ x=(0 ^ x) - (0) - 得到启示:
abs(x)=((x >> 31) ^ x) - (x >> 31) 解法:
函数声明 1234int myAbs(int x) {int mask = x >> 31;return (mask ^ x) - mask;}测试代码 12345v(myAbs(0))v(myAbs(1))v(myAbs(-1))v(myAbs(17))v(myAbs(-29))输出 12345myAbs(0) = 0myAbs(1) = 1myAbs(-1) = 1myAbs(17) = 17myAbs(-29) = 29
多少个 bit 为 1
数值变量的二进制表示中,有多少个 bit(比特位)为 1。
- 说明:暂时想到的方法,还是需要用到循环(复杂度
o(n))。 - 提示:为
1的 bit 分布稀疏时,如何快速求解(减少代码所需循环次数)! 基础知识:将一个 int 变量(二进制表示中,从左到右数)最后一个为
1的 bit 设置为0:1b & (b - 1)解法:
函数声明 12345678int countBit(int x) {int cnt = 0;while (x != 0) {++cnt;x &= x - 1;}return cnt;}测试代码 12v(countBit(46)) // int 46 == '0101110'Bv(countBit(21)) // int 37 == '0010101'B输出 12countBit(46) = 4countBit(21) = 3
相邻 bit 不同时为 1
n 位的二进制数字串,有多少个子串不存在相邻的 bit 同时为 1。
思路:n == 1 时,有 0 和 1 两种符合条件的可能情况;n == 2 时,则有 00、01 和 10 三种可能;n == 3 时,则有 000、001、010、100、101 五种可能…
根据归纳法,我们发现它类似于斐波那契数列。
|
|
|
|
|
|
不用 / 的除法
不用除号完成除法。(利用位操作和加减法,甚至乘法)
|
|
除法的直式计算,就是我们国内小学教育教的那种除法算法,即是平时用草稿手写进行的那种除法计算。
以上方法,并不便于进行详细解释。所以,简单地说,可以这么理解:
我们平时进行的是十进制的直式除法计算,这里只是在二进制下进行这一过程。
|
|
|
|
|
|
不用 + 的加法
不用加号完成加法。
思路:在二进制下,对两个数的相加为 1 的 bit,用「^ 异或」来求和;
对两个数相加为 0 并进位 1 的 bit,先进行「& 按位与」操作,然后「<< 1 进位」,然后再求和。
|
|
|
|
|
|
|
|
不用 * 的乘法
不用乘号完成乘法。
思路:跟上文说的「不用 / 做除法」的思路类似,也是参考小学的时候学的「写草稿计算乘积」的算法。
基础:顺着上文 「2 的幂」 小节的思路,可以知道
得出一个 int 整数 最后一个为 1 的 bit 所代表的数值:
|
|
|
|
|
|
|
|
大小端
判断内存以什么字节序储存数据:Big Endian or Little Endian 大小端?
- 简要说明大小端的区别(字节序):
- 内存
0x4000~0x4003的低地址在左,高地址在右- 内存地址:从低到高(从左到右)。
- 数据
0x 12 34 56 78高字节在左,低字节在右!- 字节高低:从高到低(从左到右)。
- 大端:从左到右 存放数据(操作系统常用顺序)
- 高字节,从 低地址 开始放。
- 小端:从右到左 存放数据(网络传输常用顺序)
- 高字节,从 高地址 开始放。
- 详情参考:详解大端模式和小端模式
- 内存
解法:
函数声明 1234bool isLittleEndian() {unsigned short data = 0x1122;return 0x22 == *((unsigned char*)&data);}测试代码 1v(isLittleEndian()) // 运行在 MBP 的 macOS 上。输出 1isLittleEndian() = 1
数值交换
交换两个变量的数值。
|
|
现在要求:在不使用第三个变量的情况下,实现两个变量的数值交换。
|
|
利用「加减」来交换变量的方法已经很巧妙了,
可是当 x + y 的值超出 int 能表示的数值范围(溢出)时,会出错。
|
|
利用更精妙的「位操作」方法,可以完美解决这个问题!
而且还特别好记,因为每一步都是 x ^ y。
|
|
|
|
但是我们必须要知道:在其它语言里,有更直接的变量交换方法。
|
|
|
|
后记
- 程序源文件附件:
- 发到七牛云存储之后,才想起来用 GitHubGist 更方便:
- 重做这些老题目,需要不到一整天的时间;但是将这些解答输出成本文,却需要超过一天。
- 于是,我面临这样一个纠结的问题:从个人角度来看,到底值不值得将它们写成博文?
- 其实,在写本文之前、思考解题的过程中,我已经写了足以让自己理解、可用于复习的相关笔记和代码。
- 如果要写这样一篇博文,当然要以「别人也能看得懂」为标准来认真地写,写得足够条理清晰。这样一来,就比写个人笔记更费神费时得多了。
- 当然,写作的过程中,我复习了相关的知识点,再一次理清了自己的思路,增进了理解(还给他人分享了经验)。
- 但是,我与其付出整整一天的时间和精力去复习总结这些内容,好像还不如继续深入学习其它知识。
- 我纠结了。根据以往经验,我倾向于认为:不值得这么做。
- 至少不应该急着写。应该隔一段时间,遗忘曲线下行之后再写,可以更好地增进记忆效果了。
- 这次就算了。接下来,我要试着一直学下去,先不写总结归纳的文章,看学习效果是不是更好。
- 吐槽:这种「自学」方面的实验,本来学生时代就该做了。如果当年一早搞清楚怎样的学习方法更适合自己,然后又快又好地进步,或许现在就不会活得那么窘迫了。
(提醒自己:不要再埋怨了,不要埋怨,不要埋怨……)
- 吐槽:这种「自学」方面的实验,本来学生时代就该做了。如果当年一早搞清楚怎样的学习方法更适合自己,然后又快又好地进步,或许现在就不会活得那么窘迫了。
Show Comments