C Primer Plus读书笔记(五)
文章目录
本文是《C Primer Plus》第十五章至第十七章读书笔记,持续更新中。
ch15. 位操作
- 位操作运算符
- 不同进制的数据表示法
一般系统是使用8bit作为一个字节。C语言中定义的是char类型的位长作为一个字节的长度。C语言中可以通过宏
CHAR_BIT
获取char类型的位长八进制 - 一个八进制位对应3个二进制位。例如八进制0377对应二进制000 011 111 111
十六进制 - 一个十六进制位对应4个二进制位。因此两个十六进制位恰好对应一个8位字节。例如:0xC2相当于二进制1100 0010
按位逻辑运算符号
二进制反码或按位取反
~
:按二进制位,把1变成0,把0变成1按位与
&
:按二进制位,当运算位都是1时,结果才为1。支持&=
运算符按位或
|
:按二进制位,当运算位有一个是1或者两个都是1时,结果就是1。支持|=
运算按位异或
^
:按二进制位,当运算位有且只有一个是1时,结果是1。支持^=
运算用法 说明 示例 掩码 原始二进制通过 &
和掩码运算。保留掩码(mask)中二进制位为1的位,其他位置都置为0,0即为掩码10010110 & 00000010 = 00000010
打开位 打开特定位,其他位置保持不变。通过设置特定位为1,其他位置为0进行` `运算 关闭位(清空位) 关闭特定位,其他位置保持不变。通过设定mask(需要关闭的位置为1,其他位置置为0),然后对mask取反后再进行 &
运算00001111 & ~(10110110) = 00001001
切换位 打开已关闭的,或者关闭已打开的位。假设某一位为b,那么0^b为b本身,1^b会对b进行切换,即b原来为1则切换为0,原来为0则切换成1 00001111 ^ 10110110 = 10111001
检查位的值 通过设置掩码而后进行 &
运算来判断某个位是否为1或者0if(flags & mask) == mask
移位运算符
左移
<<
:运算对象的每一位向左移动指定的位数。运算对象移出左末端的值丢失,用0填充空出的位置右移
>>
:运算对象的每一位向右移动指定的位数。运算对象移出右末端的值丢失,对于无符号数而言用0填充空出的位置;对于有符号数,取决于机器用法 说明 number << n
number乘以2的n次幂函 number >> n
如果number为非负,则用number除以2的n次幂
注意:不管是按位逻辑运算还是移位运算,都是产生新值,而不是改变原始值
应用代码解读
1// 输出数字的二进制字符串 2char* itobs(int n, char* ps) 3{ 4 int i; 5 // 计算int类型的二进制位。sizeof计算出int类型的字节数,而CHAR_BIT为一个字节的二进制位数 6 const static int size = CHAR_BIT * sizeof(int); 7 8 // ps是char数组,数组大小为size+1,最后一个存放空字符'\0'表示字符串 9 // ps的高索引处,存放数字的二进制地位 10 for (i = size - 1; i >= 0; i--, n >>= 1) 11 // 01是八进制位,实际用十进制1也可以。01 & n是的n的二进制第0位解析出二进制形式,而后n往右移1位,进而解析出二进制第1位 12 // 数值 + '0'将数值转化为字符。这里的数值要么是0要么是1,'0' + 数值就是'0'本身或者后面一个字符即'1' 13 ps[i] = (01 & n) + '0'; // 这里的01&n必须要用括号,因为&运算的优先级低于+ 14 ps[size] = '\0'; 15 16 return ps; 17}
位字段
位字段是一个
signed int
或者unsigned int
类型变量中相邻的位通过结构声明来建立,该结构为每个字段提供标签并确定该字段的宽度
1struct 2 { 3 unsigned int autfd: 1; 4 unsigned int bldfc: 1; 5 unsigned int undln: 1; 6 unsigned int itals: 1; 7 } prnt; // prnt定义了4个1位的字段,prnt被存储在int大小的内存单元中,prnt占用了一个int的内存空间
位字段不限制1位大小,可以按需设置字段的位长。但是总位数不能超过一个
unsigned int
类型的位长。如果超过一个unsigned int
的位长,则会用到下一个unsigned int
的的存储位置定义位字段时,可以设置“空洞”或者强制字段与下一个
unsigned int
对齐1struct 2 { 3 unsigned int field1: 1; 4 unsigned int : 2; 5 unsigned int field2: 1; // field2和filed1之间隔了2个位的空隙 6 unsigned int : 0; 7 unsigned int field3: 1; // field3将存储在下一个unsigned int中 8 } stuff; // stuff占用了两个int的内存空间
对齐特性 - 类型本身有对齐要求,可以使用
_Alignof(type)
来计算某个类型的对齐要求。对齐要求本身是一个数字,代表几个字节。例如,float
的对齐要求是4,那么float类型数据的地址可以被4整除。除了类型本身的对齐要求,可以设置某个数据的对齐要求,但是不能低于类型本身的对齐要求
ch16. C预处理器和C库
- 预处理器的作用和用法
- C提供了哪些预处理指令,对应的含义是什么
预处理器 - 简单理解的话,基本上是把一些文本转换成另外一些文本。转换的话可能通过包含别的文件,或者把符号缩写替换成其表示的内容
#define 指令
格式:
#define 缩写(宏) 替换体
宏的作用域从它在文件中的声明处开始,直到用#undef指令取消为止,或者延伸至文件末尾。如果宏是通过头文件引入,那么宏的位置取决于#include指令的位置
宏:可以是类对象宏,可以是类函数宏
1#define TWO 2 2#define FOUR TWO*TWO // 这里并不会发生乘积运算,乘积发生在编译阶段 3#define PX printf("X is %d.\n", x)
宏的名称不允许有空格,要遵循C变量的命名规则
宏展开:宏变成最终替换文本的过程称为宏展开。宏展开是在编译器编译之前发生,只是做替换不做计算
宏常量可以定义标准数组的大小和const变量的初始值
1#define LIMIT 20 2const int LIM = 50; 3static int data1[LIMIT]; // 有效 4static int data2[LIM]; // 无效 5const int LIM2 = 2 * LIMIT; // 有效 6const int LIM3 = 2 * LIM; // 无效
类函数宏 - 要注意的是,宏参数和函数参数有差别。宏参数是没有经过计算的,只是替代。见下面的例子。使用类函数宏的时候,要使用足够多的括号来确保运算和结合的正确顺序,并且避免使用++x作为宏参数
1#define SQUARE(X) X*X 2 3int x = 5; 4SQUARE(x + 2); // 这个结果不是25,宏展开:x+2*x+2 = 17, 5100 / SQUARE(2); // 这个结果不是25,宏展开:100/2*2 = 100 6SQUARE(++x); // 这个结果不是36,宏展开:++x*++x = 6*7 = 42
#运算符 - 替换体中通过#运算符引用宏参数
1#define PSQR(x) printf("The square of " #x " is %d.\n",((x)*(x))) // 通过空格串联字符串 2PSQR(y); // The square of y is 25. 3PSQR(2 + 4); // The square of 2 + 4 is 36.
##运算符(预处理器粘合剂) - 类函数宏替换部分
1#define XNAME(n) x ## n 2 3int XNAME(1) = 14; // 宏展开 int x1 = 14; 4int XNAME(2) = 20; // 宏展开 int x2 = 20; 5 6PRINT_XN(1); // 宏展开 printf("x1 = %d\n", x1);
变参宏:
...
和__VA_ARGS__
1#define PR(X, ...) printf("Message " #X ": " __VA_ARGS__) 2double x = 48; 3double y; 4 5y = sqrt(x); 6PR(1, "x = %g\n", x); // 宏展开:printf("Message " 1 ":" "x = %g\n", x) Message 1: x = 48 7PR(2, "x = %.2f, y = %.4f\n", x, y); // Message 2: x = 48.00, y = 6.9282
宏和函数的主要区别
- 宏:替换生成内联代码,即在程序中生成语句
- 函数:函数的调用会产生上下文的切换,即会在被调函数和主调函数之间切换
#include 指令
- 预处理器将
#include
后面的文件的内容包含到当前文件中,替换原来的#include
的位置 - 头文件通过#include执行包含到源文件中。通常头文件中包含:明示常量、宏函数、函数声明、结构模板定义和类型定义(typedef),也可以使用头文件声明外部变量供其他文件贡献
#include <stdio.h>
- 查找系统目录#include "hot.h"
- 优先查找本地目录(根据编译器设定不同,可能是当前目录,也可能是工作目录),而后再查找系统目录- 通常,头文件(
*.h
)里的内容是编译器在创建可执行代码时所需的信息,而不是执行代码。执行代码通常在源文件中(*.c
) - 通常应该用
#ifndef
和#define
防止多重包含头文件
- 预处理器将
#undef 指令 - 取消已定义的#define指令。即使没有定义过,也可以使用#undef指令来取消
#ifdef / #else / #endif 条件编译指令
- 如果通过#define定义了某一个标识符,那么执行#ifdef块里内容
- 除了可以用于预处理指令,也可以用于标记C语句块
1#ifdef MAVIS 2 #include "horse.h" 3 #define STABLES 5 4#else 5 #include "cow.h" 6 #define STABLES 15 7#endif
#ifndef 指令 - 与#ifdef类似,与 #else / #endif指令配套使用
主要用于头文件被多次包含的时候,避免重复定义头文件中的声明
1#ifndef CLEARN_CH16_NAMES_ST_H_ 2#define CLEARN_CH16_NAMES_ST_H_ 3 4// 此处省略头文件定义 5 6#endif //CLEARN_CH16_NAMES_ST_H_
宏(标识符)的命名:文件名作为标识符,使用大写字母,用下划线代替文件名中的点字符
系统定义的宏或者标识符多以
_
开头
#if / #elif / #endif指令
通过这个判断标识符是否定义或者是否等于某个值
1#if SYS == 1 2... 3#elif SYS ==2 4... 5#endif 6 7#if defined (IBMPC) // 和#ifdef类似 8... 9#elif defined (VAX) 10... 11#endif
预定义宏(标识符)
宏(标识符) 含义 __DATE__
Mmm DD YYYY格式的当前日期 __FILE__
当前源代码文件的文件名 __LINE__
当前源代码文件中的行号, __LINE__
在哪一行就是那一行的号__STDC__
设置为1时,表明实现遵循C标准 __STDC_HOSTED__
本机环境设置为1,否则为0 __STDC__VERSION__
支持C99标准,设置为199901L;支持C11标准,设置为201112L __TIME__
hh:mm:ss格式的当前时间 __func__
标识符,当前函数名 #line和#error
1#line 1000 // 把当前行号置为1000,后面如果出现__LINE__则是相对于1000来增加的 2#line 10 "cool.c" // 把当前行号置为10,把当前文件名置为”cool.c“ 3#error xxxx // 让预处理器发出一条错误信息,中断编译
#pragma 指令 - 修改编译器的一些配置
内联函数
把函数变成内联函数,编译器可能会用内联代码替换函数调用,并(或)执行一些其他的优化,但也可能不起作用
具有内部链接的函数(static修饰)可以成为内联函数,且内联函数的定义与调用该函数的代码必须在同一文件中
使用
inline
和static
声明(定义)内联函数 (通常内联函数的原型和定义在一起)1#include <stdio.h> 2inline static void eatline() 3{ 4 while(getchar()!='\n') 5 continue; 6} 7int main() 8{ 9 ... 10 eatline(); 11 ... 12}
如果多个文件中都需要使用同一个内联函数,可以把内联函数放在头文件中,然后源文件包含那个头文件。一般头文件中不包含可执行代码,内联函数是个特例