C Primer Plus读书笔记(四)

文章目录

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

ch12. 存储类别、链接和内存管理

  1. 不同关键字修饰是否对应不同的存储类别
  2. 怎么管理内存,是否有对应的函数
  • 标识符 - 一个名称(变量),用于指向内存中的特定对象。通过作用域和链接来描述标识符的可见性

  • 左值 - 指定内存对象的标识符或者表达式

  • 可修改的左值 - 可以使用左值改变对象中的值

  • const char* pc = "Behold a string literral!"

    • pc - 可以修改的左值,即pc可以指向别的地址
    • *pc - 不可修改的左值,*pc指向'B',不可修改
  • 作用域

    • 块作用域 - 花括号括起来的代码区域。以前块作用域的变量必须声明在块的开头。C99放宽了这一标准,允许在块中的任意位置声明变量
    • 函数作用域 - 仅用于goto语句的标签,即使出现在函数内层块中,作用域也延伸到整个函数
    • 函数原型作用域 - 函数原型中的形参名,只在变长数组中才用到 void use_a_VLA(int n, int m, ar[n][m]),后面的形参可以使用前面定义的形参名
    • 文件作用域 - 从定义处到该定义所在的文件末尾均可见。文件作用域变量也称为全局变量
  • 链接

    • 无链接变量 - 具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。这些变量只属于它们的块作用域、函数作用域或函数原型作用域
    • 外部链接变量(全局作用域/程序作用域) - 首先是有文件作用域,并可在多个文件中使用。
    • 内部链接变量(文件作用域) - 只能在定义该变量的文件中使用。用static关键字定义的全局变量就是内部链接变量。static int dodgers = 3;
  • 存储期 - 表述通过标识符访问的(内存)对象的生存期

    • 静态存储期 - 程序执行期间一直存在
    • 线程存储期 - 从被声明到线程结束一直存在。使用_Thread_local关键字声明
    • 自动存储期 - 块作用域的变量,从进入块执行到离开块,变量的存储期自动分配内存和自动释放内存
    存储类别存储期作用域链接声明方式
    自动自动块内,可以显式使用auto关键字
    寄存器自动块内,使用关键字register
    静态外部链接静态文件外部所有函数外
    静态内部链接静态文件内部所有函数外,使用关键字static
    静态无链接静态块内,使用关键字static
  • 自动变量 - 可以显式使用auto关键字。自动变量不会自动初始化,意味着如果没有没有初始化就读取,可能是残留在对应空间里的值

  • 寄存器变量 - 其实是一种请求,意味着即使声明为寄存器变量,最终变量也不一样会分配在寄存器上。寄存器变量不能使用地址运算符

  • 块作用域的静态变量 - 静态说明在内存中的位置不变,不代表值不变。声明语句在块里,只是限定了作用域在块里,声明语句在程序执行被载入到内存的时候已经执行完毕

  • 外部链接的静态变量 - 源文件中使用的外部变量定义在另一个源文件中,那么在该源文件中必须使用extern关键词再次声明,extern char Coal。外部变量如果没有初始化,会被初始化为对应类型的0值显式初始化不同于自动变量,只能使用常量表达式

  • 函数的存储类别

    • 外部函数 - 可以被所在文件以及其他文件的函数访问(默认)。通过extern声明外部函数。除非显式使用static声明,否则一般函数的声明默认都是externextern这个关键字在函数上可省略
    • 静态函数 - 只能被当前文件的函数访问。static double beta(int, int)
  • 动态分配内存 - malloccalloc

    • malloc - 接收参数:所需的内存的字节数。通常使用n*sizeof(type)
    • calloc - 接收参数:第1个参数为存储单元的数量,第2个参数为存储单元的大小(以字节为单位),例如:calloc(100, sizeof(long))。与malloc不同,这个方法吧数组里的元素的初始值都设置为0
    • 返回值指向void的指针,通常返回值会被强制转为匹配的配型
    • 返回的指针指向所分配空间的其实位置,因此可以像使用数组一样使用
    • 如果分配内存失败,返回NULL
    • 与变长数组相比,变长数组是自动存储类型,程序离开变长数组所在的块时占用的内存将自动释放
  • 释放内存 - free

    • 接收参数 - malloc的返回值
    • 只能释放malloccalloc分配的内存
    • 同一块内存不能被释放两次
    • 内存不及时释放会导致内存泄露
  • 存储类别和内存分配

    • 静态数据 - 包括外部链接、内部链接和无链接的静态数据以及字符串字面量,占用一块内存区域
    • 自动数据 - 这部分内存通常作为栈来处理
    • 动态分配的数据 - 这部分内存通常称为内存或者自由内存
  • const类型限定符

    • 如果是修饰类型,那么说明该类型的值在初始化以后不能修改
    • 如果是修饰指针,取决于const是在*左侧还是右侧
      • const*左侧 - 限定了指向的数据不能改变,比如:const float * pf float const * pf 这两种方式都可以
      • const*右侧 - 限定了指针本身不能改变,即不能再指向别的地址,比如:float * const pf
    • const关键字常用于修饰函数形参
  • volatile类型限定符

    • 直接存取原始内存地址
    • 编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问
  • restrict类型限定符

    • 只能用于指针,表明该指针是访问数据对象的唯一且初始的访问方式
    • 如果使用了restrict关键字,编译器就可以选择捷径优化计算。即告诉编译器可以自由假定一些优化方案
     1int ar[10];
     2int* restrict restar = (int*)malloc(10 * sizeof(int));
     3int* par = ar;
     4
     5for (n = 0; n < 10; n++)
     6{
     7	par[n] += 5;
     8	restar += 5;
     9	ar[n] += 5;
    10	par[n] += 5;
    11	restar[n] += 3; // 这里编译器就可能优化为restar[n] +=8
    12}
    
  • _Atomic类型限定符 - 原子类型,一个线程对原子类型的对象执行原子操作时,其他线程不能访问该对象

ch13. 文件输入/输出

  1. 了解读写文件的方式已经对应的函数
  2. 文件模式和二进制模式的差别,如何选择
  3. 顺序访问和随机访问
  • exit()函数关闭所有打开的文件并结束程序。通常正常结束的程序传递0,异常结束的程序传递非0。可以用系统定义的宏来传值

    • EXIT_SUCCESS - 程序正常结束退出
    • EXIT_FAILURE - 程序失败退出
  • fopen(path, mode) - 打开一个文件,返回文件指针FILE*。打开模式详见P358

  • getc(fp) - 从fp指定的文件中获取一个字符

  • putc(ch, fpout) - 把字符ch放入FILE指针fput指定的文件中。stdout是作为与标准输出相关联的文件指针

  • 指向标准文件的指针(FILE*)

    标准文件文件指针通常使用的设备
    标准输入stdin键盘
    标准输出stdout显示器
    标准错误stderr显示器
  • fprint()/fscanf()printf()/scanf()类似,只是前者的第一个参数需要指定一个文件指针,可以是标准输入输出,也可以是常规的文件指针,例如fopen()的返回结果

  • fseek(fp, offset, start) - 根据参数移动文件指针fp到特定的位置

    • fp - fopen返回的文件指针

    • offset - 文件指针起始位置的偏移量,数据类型是long,意义是偏移多少字节

    • start - 偏移量起始点

      模式偏移量的起始点
      SEEK_SET文件的开始处
      SEEK_CUR当前位置
      SEEK_END文件末尾
    • 如果出现错误,该函数返回-1,否则返回0

  • ftell(fp) - 返回类型是long,返回的是从文件开始处到fp指向的当前位置的字节数。如果fp当前位置指向文件末尾,那么ftell返回的就是当前文件的所有字节数

  • fseekfseek接收的文件指针以二进制模式打开和以文本模式打开的时候的区别

  • 文件的二进制形式和文本形式

    • 所有数据都是以二进制形式存储
    • 二进制形式存储:以程序所用的表示法把数据存储在文件中,称以二进制形式存储数据
    • 文本形式存储:文件中所有的数据都被解释成字符码,称以文本形式存储数据
    • 例如:对于int num = 12345,如果以二进制形式存储则存储的是0011000 00111001;如果以文本形式存储,那么存放是'1' ,'2','3','4','5'这几个字符对应的字符码(取决于编码方式)
    • 二进制I/O -fread()fwrite()

ch14. 结构和其他数据形式

  1. 结构体的声明和定义
  2. 如何访问结构体成员
  3. 结构体作为函数参数如何传递
  • 结构声明描述了一个结构的组织布局,并未创建实际的数据对象。(结构声明也称为模板)

  • 结构声明的几种方式

     1#define MAXTITL 41
     2#define MAXAUTL 31
     3
     4struct book1 /* 结构声明,或者叫结构模板 */
     5{
     6	char title[MAXTITL];
     7	char author[MAXAUTL];
     8	float value;
     9};
    10
    11struct book2
    12{
    13	char title[MAXTITL];
    14	char author[MAXAUTL];
    15	float value;
    16} library2;
    17
    18struct /* 简化声明 */
    19{
    20	char title[MAXTITL];
    21	char author[MAXAUTL];
    22	float value;
    23} library3;
    
  • 初始化结构的两种方式

     1struct book1 library1_1 = {
     2		"The Pious Pirate and the Devious Damsel",
     3		"Renee Vivotte",
     4		1.95
     5	};
     6
     7	struct book1 library1_2 = {
     8		.title = "The Pious Pirate and the Devious Damsel",
     9		.author = "Renee Vivotte",
    10		.value = 1.95
    11	};
    
  • 指向结构的指针

    • 声明结构指针:struct book1* book
    • 使用->操作符访问成员:book->title
    • 使用指针解引用的方式访问成员:(*book).title
  • 向函数传递结构的三种方式

    • 传递结构成员 - 这种和传递某个变量没有差别,传递的是结构成员的值的副本,对于被调函数本身也不会感知传递的是结构的成员
    • 传递结构的地址(指针)- 这时候可以通过->运算符获取结构成员。另外和数组不一样,需要使用&原算法获取结构的地址
    • 传递结构 - 实参被初始化成被传递参数的相应成员的值的副本
  • 结构的赋值 - n_datao_data是两个结构,当运行o_data = n_data的时候,o_data的成员的值相应的被赋值成n_data的成员的值。注意,这种赋值,是成员的值发生的赋值,类似于传参时的值传递

  • 结构中的字符串

    • 用字符数组 - 这种方式比较简单,默认会分配空间,因此赋值的时候比较安全。这种方式,字符串是存储在结构里面
    • 用字符指针 - 这种方式比较危险,未初始化的情况,指向的可能是未知的区域。这种方式,字符串不存储在结构里面,结构里只是存放了字符串的地址
  • 结构复合字面量

    • 创建一个临时结构值,(struct book) {"The Idiot", "Fyodor Dostoyevsky", 6.99}
    • 创建在函数外部则具有静态存储期
    • 创建在块中,则具有自动存储期
  • 伸缩型数组成员

    • 伸缩的意思是数组成员的数量在初始化的时候确定

    • 伸缩型数组成员必须是结构的最后一个成员,且结构中必须至少有一个其他成员

    • 伸缩数组的声明的方括号中是空的

    • 应该声明一个指向结构的指针,而不是声明变量。且需要使用malloc来初始化,不能进行拷贝或者赋值

    • 不应该将带有伸缩数组成员的结构作为数组成员或者另一个结构的成员

      1struct flex
      2{
      3  int count;
      4  double average;
      5  double scores[];
      6};
      7
      8struct flex* pf = malloc(sizeof(struct flex) + 5*sizeof(double)); // 该伸缩数组成员包含5个元素
      
  • 匿名结构

    • 使用嵌套的匿名结构定义成员

      1struct person
      2{
      3  int id;
      4  struct {char first[20]; char last[20];};
      5}
      
    • 访问嵌套结构成员 - 像是直接访问外层结构成员一样访问

      1struct person ted = {8483, {"Ted", "Grass"}};
      2puts(ted.first);
      
  • 联合union

    • 同一个内存空间内存放联合声明中的某个数据(联合中的某一个字段)。实际上会根据占用空间最大的类型相应的分配内存

    • 可以创建联合类型的变量、数组和指针

       1union hold{
       2  int digit; // int - 2个字节
       3  double bigfl; // double - 8个字节,因此该联合为8个字节
       4  char letter; // char - 1个字节
       5};
       6
       7// 不同联合类型的变量
       8union hold fit;
       9union hold save[10]; // 联合类型数组
      10union hold* pu; // 联合类型指针
      11
      12// 联合的几种初始化方式
      13union hold valA;
      14valA.letter = 'R';
      15union hold valB = valA; // 用另一个联合来初始化
      16union hold valC = {88}; // 初始化digit
      17union hold valD = {.bigfl = 118.3}; // 指定初始化器
      
    • 联合的一些用法

      1fit.digit = 23; // 把23存储在联合中,占用2个字节
      2fit.bigfl = 2.0; // 清除23,存储2.0,占用8个字节
      3
      4pu = &fit; // 获取联合指针
      5x = pu->digit; // 使用->操作符访问联合
      
  • 枚举enum

    • 声明符号名称表示整型常量,实际上enum常量就是int类型

      1enum spectrum {red, orange, yellow, green, blue, violet}; // spectrum称为标记名,red/yellow这些称为枚举符
      2enum spectrum color; // enum spectrum作为类型来使用
      
    • 默认情况下,列表中的枚举常量都被赋值0,1,2以此类推,也可以显式赋值

      1enum levels {low = 100, medium = 500, high = 2000};
      2enum feline {cat, lynx = 10, puma, tiger}; // cat的值是0, lynx、puma和tiger的值分别是10、11、12
      
  • 共享名称空间 - 在同一个作用域中,结构标记、联合标记和枚举标记都共享名称空间,意味着这三者之中的名称标记不能一样;但是可以和变量名一样

  • typedef - 为某一类型自定义名称

    • typedef 实际类型 名称
    • typedef 实际上并没有创建任何新的类型,只是为某个已经存在的类型增加了一个方便使用的标签
    • 只能用于类型,不能用于值定义(这个和#define不同)
    • 如果定义在函数中,就具有局部作用域;如果定义在函数外面,就具有文件作用域
    • 通常typedef定义中用大写字母表示被定义的名称
  • * () []的优先级规则 - 通常这几个符号的不同组合可以声明出复杂的数据类型

    • []()具有相同的优先级,它们的优先级都比*

    • []()具有相同的优先级,从左往右结合

    • 一些典型的例子

      声明解释
      int* risks[10][]优先级高于*,因此[]先和risk结合,声明一个含有10个元素的数组,元素的类型是int*指针
      int(*risks)[10]()[]优先级相同,因此从左往右结合。这里()先和*结合,声明了一个指针,这个指针指向的数据类型是int[10]即包含10个int元素的数组
      int goods[12][50]从左往右结合,[12]先声明了一个含有12个元素的数组,数组元素的类型是int[50],即12*50的二维数组
      int* oof[3][4]*优先级比[]低,因此先是声明了一个3*4的数组,数组元素是int*需要注意,这时候需要分配的存放12个指针的空间
      int(* oof)[3][4]*先和()结合声明了一个指针,指针指向的数据是int[3][4]3*4的二维数组。需要注意,这时候只需要分配存放1个指针的空间
      char* fump(int)定义一个函数,返回值是指向char的指针
      char (* frump)(int)指向函数的指针,该函数的返回类型为char
      char (* frump[3])(int)指向函数的指针数组,数组元素指向函数,函数的返回类型为char
  • 函数指针 - 顾名思义,指向函数的指针

    • 通常用作函数参数,用于在函数内部,根据传入的函数指针相应的调用函数指针指向的函数。例如:void show(void (fp*)(char*), char* str)

    • 函数指针存储着函数代码起始处的地址。函数名存放的也是函数地址

    • 函数指针定声明的方式 - 先声明一个该类型的函数,然后把函数名替换成(*pf)形式的表达式,然后pf就称为了指向该类型的函数指针

    • 函数指针的几种用法

       1// 函数指针的声明和赋值
       2void ToUpper(char*);
       3void ToLower(char*);
       4int round(double);
       5void (*pf)(char*); // 声明了一个函数指针,指向的函数的输入为char*类型,返回值为void
       6pf = ToUpper; // 有效
       7pf = ToLower; // 有效
       8pf = round; // 无效,round与指针类型不匹配
       9
      10// 通过函数指针调用函数
      11char mis[] = "Nina Metier";
      12pf = ToUpper;
      13(*pf)(mis); // 调用方式1
      14pf = ToLower;
      15pf(mis); // 调用方式2
      16
      17// 通过typedef简化函数指针的声明
      18typedef void (*V_FP_CHARP)(char *);
      19void show(V_FP_CHARP fp, char*)