C Primer Plus读书笔记(五)

文章目录

本文是《C Primer Plus》第十五章至第十七章读书笔记,持续更新中。

ch15. 位操作

  1. 位操作运算符
  2. 不同进制的数据表示法
  • 一般系统是使用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则切换成100001111 ^ 10110110 = 10111001
      检查位的值通过设置掩码而后进行&运算来判断某个位是否为1或者0if(flags & mask) == mask
  • 移位运算符

    • 左移<<:运算对象的每一位向左移动指定的位数。运算对象移出左末端的值丢失,用0填充空出的位置

    • 右移>>:运算对象的每一位向右移动指定的位数。运算对象移出右末端的值丢失,对于无符号数而言用0填充空出的位置;对于有符号数,取决于机器

      用法说明
      number << nnumber乘以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库

  1. 预处理器的作用和用法
  2. 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修饰)可以成为内联函数,且内联函数的定义与调用该函数的代码必须在同一文件中

    • 使用inlinestatic声明(定义)内联函数 (通常内联函数的原型和定义在一起)

       1#include <stdio.h>
       2inline static void eatline()
       3{
       4  while(getchar()!='\n')
       5    continue;
       6}
       7int main()
       8{
       9  ...
      10  eatline();
      11  ...
      12}
      
    • 如果多个文件中都需要使用同一个内联函数,可以把内联函数放在头文件中,然后源文件包含那个头文件一般头文件中不包含可执行代码,内联函数是个特例