当前位置: 首页 > 工具软件 > reek > 使用案例 >

Pointers On C [Kenneth A.Reek] 读书笔记

皮自明
2023-12-01

代码地址

3.数据

  • c语言的四种基本数据类型——整型、浮点型、指针和聚合类型(如数组和结构等)
  • 字面值(literal)这个术语是字面值常量的缩写——这是一种实体,指定了自身的值,并且不允许发生改变
  • 枚举类型就是指它的值为符号常量而不是字面值的类型。
enum tech {LINUX,OS,DB,K8S,GOLANG=99};
  • c字符串:一串以NUL字节结尾的零个或多个字符。
  • 不要修改字符串,因为在ANSI C中的效果是没有定义的。如果需要修改字符串,请把它存储于数组中。
  • 在程序中,使用字符串常量会生成一个“指向字符的常量指针”。不能把字符串常量赋值给一个字符数组,因为字符串常量的直接值是一个指针,而不是这些字符本身。
  • typedef 类型定义
typedef char *ptr_to_char;
ptr_to_char a, b, c; // 相当于 char *a,*b,*c;
  • 常量指针 int *const a;指针不能修改
  • 指向整型常量的指针:可以修改指针,但不能修改它指向的值。 int const *a;
  • 作用域
    • 代码作用域
    • 文件作用域:任何在代码块之外声明的标识符都具有文件作用域
    • 原型作用域:只适用于在函数原型中声明的参数名
    • 函数作用域:只适用于语句标签,语句标签用于goto语句。
  • 链接属性:
    • external(外部):不论声明多少次,位于多个源文件都表示同一个实体。extern
    • internal(内部):在同一个源文件内的所有声明都指向同一个实体,但位于不同源文件的多个声明则分属不同的实体。static
    • none(无):该标识符的多个声明被当做独立的个体。
  • 存储类型:
    • 普通内存
    • 运行时堆栈
    • 硬件寄存器: register
  • 静态变量:凡是在任何代码块之外声明的变量总是存储在静态内存中,总是初始化为0值。而自动变量不初始化,它的值开始总是垃圾。
  • static关键字的作用:
    • 当它位于函数定义时,或用于代码块之外的变量声明时,static关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明它们的源文件中访问。
    • 当它用于代码块内部的变量时,static关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁。

6.指针

  • 无论程序员还是计算机都无法通过值的位模式来判断它的类型。类型是通过值的使用方法隐式确定的。编译器能够保证值的声明和值的使用之间的关系是适当的。编译器能够保证值的声明和值的使用之间的关系是适当的,从而帮助我们确定值的类型。
  • 声明一个指针变量并不会自动分配任何内存。在对指针执行间接访问前,指针必须进行初始化:或者使它指向现有的内存,或者给它分配动态内存。对未初始化的指针变量执行间接访问操作是非法的,而且这种错误常常难以检测。其结果常常是一个不相关的值被修改。这种错误是很难被调试发现的。
  • NULL指针就是不指向任何东西的指针。它可以赋值给一个指针,用于表示那个指针并不指向任何值。对NULL指针执行间接访问的后果因编译器而异,两个常见的后果分别是返回内存位置零的值以及终止程序。
  • 除了NULL指针之外,再也没有任何内建的记法来表示指针常量,因为程序员无法预测编译器会把变量放在内存中的什么位置。在极少见的情况下,我们偶尔需要使用指针常量,这时我们可以通过把一个整型值强制转换为指针类型来创建它。*(int *)100 = 25,(int *)100表示在指向整型的指针100。
  • 指针运算只有作用在数组中结果才是可以预测的,对任何并非指向数组元素的指针执行算术运算是非法的(但是常常难以检测到)。如果一个指针减去一个整数后,运算结果产生的指针所指向的位置在数组的第一个元素之前,那么它也是非法的,加法运算稍有不同,如果结果指针指向数组最后一个元素后面的那个内存位置仍是合法(但不能对这个指针执行间接访问操作),不过再往后就不合法了。
#define N_VALUES 5
float values[N_VALUES];
float *vp;

for(vp=&values[0];vp<&values[N_VALUES];)
    *vp++ = 0;

for(vp=&values[N_VALUES];vp>&values[0];)
    *--vp = 0;

// 下面的不合法,比较vp>=&values[0]的值是未定义的,因为vp移动到了数组的边界之外。但是大多数编译器都能让它工作
for(vp=&values[N_VALUES-1];vp>=&values[0];vp--)
    *vp = 0;
  • 如果两个指针都指向同一个数组中的元素,那么它们之间可以相减。指针减法的结果经过调整(除以数组元素类型的长度),表示两个指针在数组中相隔多少个元素。如果两个指针并不是指向同一个数组的元素,那么它们之间进行相减就是错误的。
  • 任何指针之间都可以进行比较,测试它们相等或不相等。如果两个指针都指向同一个数组中的元素,那么它们之间还可以执行<、<= 、>、>=等关系运算,用于判断数组的相对位置。对于两个不相关的指针执行关系运算,其结果未定义。

7.函数

  • 抽象数据类型,用static控制可见性
  • 可变参数函数,使用 stdarg.h,可变参数只能东第一个到最后一个一次访问。
#include <stdarg.h>
#include <stdio.h>
float average(int n_values,...);

// 可变参数
int main(void){
    printf("%f\n",average(5,1,2,3,4,5));
    printf("%f\n",average(7,1,2,3,4,5,6,7));
    return 0;
}

float average(int n_values,...){
    va_list args; // 用于访问参数列表未确定部分
    int count;
    float sum = 0;

    va_start(args,n_values); // 准备访问可变参数

    for(count=0;count<n_values;count+=1){
        sum += va_arg(args,int); // 依次取可变参数的值,n_values表示可变参数的个数
    }

    va_end(args); // 完成可变参数处理
    return sum/n_values;
}

10.结构与联合

  • 在结构中,不同类型的值可以存储在一起。结构中的值称为成员,它们是通过名字访问的。结构变量是一个标量,可以出现在普通标量变量可以出现的任何场合。
  • 结构的声明列出了结构包含的成员列表。不同的结构声明即使它们的成员列表相同也被认为是不同的类型。结构标签是一个名字,它与一个成员列表相关联。你可以使用结构标签在不同的声明中创建相同类型的结构变量,这样就不用每次在声明中重复成员列表。typedef也可以用于实现这个目标。
  • 结构的成员可以是标量、数组或指针。结构也可以包含本身也是结构的成员。在不同的结构中出现同样的成员名是不会引起冲突的。使用点操作符访问结构变量的成员。如果你拥有一个指向结构的指针,可以使用箭头操作符访问这个结构的成员。
  • 结构不能包含类型也是这个结构的成员,但它的成员可以是一个指向这个结构的指针。这个技巧常常用于链式数据结构中。结构变量可以用一个由花括号包围的值列表进行初始化。这些值的类型必须是和它所初始化的那些成员。
  • 编译器为一个结构变量的成员分配内存时要满足它们的边界对其要求。在实现结构存储的边界对其时,可能会浪费一部分内存空间。根据边界对其要求降序排列结构成员可以最大限度地减少结构存储中浪费的内存空间。 sizeof返回的值包含了结构中浪费的内存空间。
  • 结构可以作为参数传递给函数,也可以作为返回值从函数返回。但是,向函数传递一个指向结构的指针往往效率更高。在结构指针参数的声明中可以加上const关键字防止函数修改指针所指向的结构。
  • 位段是结构的一种,但它的成员长度以位为单位指定。位段声明在本质上是不可移植的,因为它涉及许多与实现有关的因素。但是,位段允许你把长度为奇数的值包装在一起以节省存储空间。源代码如果需要访问一个值内部任意的一些位,使用位段比较简便。
  • 一个联合的所有成员都存储于同一个内存位置。通过访问不同类型的联合成员,内存中相同的位组合可以被解释为不同的东西。联合在实现变体记录时很有用,但程序员必须负责确认实际存储的是哪个变体并选择正确的联合成员以便访问数据。联合变量也可以进行初始化,但初始值必须与联合第一个成员的类型匹配。

11.动态内存分配

  • malloc从内存池中提取一块合适的内存,并向该程序返回一个指向这块内存的指针。这块内存没有初始化。 分配的是一块连续的内存,参数为需要分配的字节数。如果内存池无法满足分配请求,malloc就返回一个NULL指针。因此,对于每个从malloc返回的指针都需要进行检查,确保它并非NULL是非常重要的。malloc返回一个类型为 void * 的指针,这个指针可以转换为其他任何类型的指针。
  • free的参数要么是NULL,要么是从malloc、calloc或realloc返回的值。向free传递一个NULL参数不会产生任何效果。
  • calloc也用于分配内存。和malloc的主要区别是calloc返回指向内存的指针之前把它初始化为0
  • realloc函数用于修改一个原先已经分配的内存块的大小。使用这个函数,可以使一块内存扩大或者缩小。如果扩大,原先内容保留,新添加的内存添加到原先内存块的后面,新内存并未初始化。如果缩小,该内存块的尾部的部分被拿掉,剩余部分内存的原先内容依然保留。如果原先的内存块无法改变大小,realloc将分配另一块正确大小的内存,并把原来那块内存的内容复制到新的块上。因此,在使用realloc之后,你就不能再使用旧内存的指针,而是应该改用realloc所返回的新指针。。如果realloc的第一个参数是NULL,那么它的行为就和malloc一样
void *malloc(size_t size);
void free(void *pointer);
void *calloc(size_t num_elements,size_t element_size);
void *realloc(void *ptr,size_t new_size);
  • 常见动态内存错误
    • 对NULL指针进行解引用操作
    • 对分配的内存进行操作时越过边界
    • 释放并非动态分配的内存
    • 视图释放一块动态分配的内存的一部分
    • 一块动态内存被释放之后被继续使用
  • memory leak:内存泄漏,分配内存但在使用完毕后不释放将引起内存泄漏。

13.高级指针

  • 函数指针常用用途:作为参数传递给函数以及用于转换表
  • 回调函数:用户把一个函数指针作为参数传递给其他函数,后者将“回调”用户的函数。
  • 转移表:声明并初始化一个函数指针数组。确保这些函数的原型出现在这个数组之前。
#include <stdio.h>

double add(double, double);
double sub(double, double);
double mul(double, double);
double div(double, double);
double (*oper_func[])(double, double) = {add, sub, mul, div};

int main(void)
{
    printf("%lf\n", oper_func[0](1, 2));
    printf("%lf\n", oper_func[1](1, 2));
    printf("%lf\n", oper_func[2](1, 2));
    printf("%lf\n", oper_func[3](1, 2));
}

double add(double a, double b) { return a + b; }
double sub(double a, double b) { return a - b; }
double mul(double a, double b) { return a * b; }
double div(double a, double b) { return a / b; }

14.预处理器

  • 预处理器:它的主要任务包括删除注释、插入被#include指令包含的文件的内容、定义和替换由#define指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令进行编译。
  • 不要在一个宏定义的末尾加上分号,使其成为一条完整的语句
  • 在宏定义中使用参数,需要在参数两侧加上括号
  • 在整个宏定义周围加上括号
  • 避免用#define指令定义可以用函数实现的很长序列的代码
  • 避免使用#define宏定义一种新的语言
  • 使用命名约定,让程序员很容易看出某个标识符是否是宏
  • 头文件应该只包含一组函数和数据的声明
  • 把不同集合的声明分离到不同的头文件中可以改善信息隐藏

15.输入/输出函数

  • perror函数以一种简单、统一的方式报告错误。
  • 文本流的换行符处理在不同系统中可能有区别,但是二进制流的字节将完全根据程序编写它们的形式写入到文件或设备中或读出到程序中。
  • 调用fclose关闭一个流,可以防止与它相关联的文件被再次访问,保证任何存储于缓冲区的数据被正确地写到文件中,并且释放FILE结构使它可以用于另外的文件。
  • EOF被定义为一个整数,它的值在任何可能出现的字符范围之外。这种解决方法允许我们使用这些函数来读取二进制文件。 让读取字符的函数返回int。
  • fflush(FILE *):它迫使一个输出流的缓冲区内的数据进行物理写入,不管它是不是已经写满。例如,调用fflush函数保证调试信息实际打印出来,而不是保存在缓冲区中直到以后才打印。

18.运行时环境

  • 为了从汇编程序调用C程序:
    • 如果寄存器d0,d1,a0,a1保存了重要的值,它们必须在调用C程序之前进行保存,因为C函数不会保存它们的值。
    • 任何函数的参数必须以参数列表相反的顺序压入到堆栈中。
    • 函数必须由一条“跳转子程序”类型的指令调用,它会把返回地址压入堆栈中。
    • 当C函数返回时,汇编程序必须清除堆栈中的任何参数
    • 如果汇编程序期望接受一个返回值,它将保存在d0(如果返回的值类型为double,它的另一半存储在d1)
    • 任何在调用之前进行保存的寄存器此时可以恢复
  • 为了编写一个C调用的汇编程序:
    • 保存任何你希望修改的寄存器(除d0,d1,a0,a1外)
    • 参数值从堆栈中获得,因为调用它的C函数把参数压入在堆栈中
    • 如果函数应该返回一个值,它的值应保存在d0中(在这种情况下d0不能进行保存和恢复)
    • 在返回之前,函数必须清理任何它压入到堆栈中的内容。
 类似资料: