《嗨翻C语言》(上)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《嗨翻C语言》(上)相关的知识,希望对你有一定的参考价值。

2016/2/23 14:41:06

《嗨翻C语言》,本书分为三个部分。

本书分为三个部分:第1章到第4章是基础知识,包括基本语法、指针、字符串、小工具和源文件;第5章到第8章为进阶内容,有结构、联合、数据结构、堆、函数指针、动/静态链接;最后四章是高级主题,内容涵盖了系统调用、进程间通信、网络编程和多线程。(加粗部分的笔记在下篇)

以下笔记没有严格按照章节进行整理,因为书中在多个章节可能提到同一类知识点,比如(略)

说明

在此笔记中,不记录不同C标准的差异,具体细节查看原书;不记录不同OS下编写C时的差异,具体细节查看原书或者google。笔记中只对不同标准、不同OS下共性的语言原理、语法细节做出整理。

此上篇整理了1-190项标注和笔记。

一、课外

三种C标准

ANSI C 始于20世纪80年代后期,适用于最古老的代码;1999年开始的C99标准有了很大的改进;在2011年发布的最新标准C11中……想知道编译器支持哪种标准,可以查看编译器的文档。

“指定初始化器”按名设置结构和联合字段,它属于C99标准;

C99标准支持“指定初始化器”,C++不支持。

在代码中间的位置声明新变量,只有C99和C11标准才允许这样做,在ANSI C(在此特指 C89)中,必须在函数的顶部声明局部变量;

扩展阅读:

windows下使用gcc

虽然在本书中作者提供了以下两种方法,但是在后续的介绍中也多有提及其不足之处。而且在接触之后,我还是放弃了 Cygwin,转而在虚拟机上安装了 debian。

如在Windows操作系统上使用gcc(GNU编译器套装),有两种选择:一种是 Cygwin,它可以完全模拟UNIX环境,自然也就包括了gcc;如果你只是想创建能够在Windows下运行的程序,MinGW 可能更符合你的需要。

其他

面向对象是一种对抗软件复杂性的技术。

二、基础知识

表达式

  • 在C语言中,几乎每样东西都有返回值:表达式 x = 4 本身也有一个值,这个值是4,即赋给x的值。

    Q1:这个赋值表达式能否进行再次赋值?例如 (x=3)=4

    A1:在C中编译无法通过,类似3=4;在C++中正常编译,运行,类似x=4

    注意:以下这种再次赋值是成立的。因为无论 y=4(C语言) 还是 y=x(C++) 都是成立的。

    1. int main()
    2. {
    3. int x = 9;
    4. int y = 8;
    5. // 方式一
    6. y = x = 4;
    7. // or 两者意义相同
    8. // y = (x = 4);
    9. printf("x is %d, y is %d\n", x, y);
    10. return 0
    11. }

    关键是理解 C/C++中左值 的概念,而且 C 和 C++ 中是不一样的。

  • 使用switch语句的好处之一是,可以用下落逻辑在不同的分支之间复用代码。

存储器

如果真的想玩转C语言,就需要理解C语言如何操纵存储器。掌握指针和存储器寻址对成为一名地道的C程序员很重要。

函数(例如 main() 函数)中声明变量,计算机会把它保存在一个叫栈(Stack)的存储器区段中;函数以外的地方声明变量,计算机则会把它保存在存储器的全局量段(Globals);堆用于动态存储。

存储器的分布图:参考位置#1022、位置#1322、位置#1448

  • 存储器是进程的
  • sizeof 是运算符,好比+&,它不是库函数。程序是在编译期间计算 sizeof 的。sizeof 运算告知某样东西在存储器中占多少字节,既可以对数据类型使用,也可以对某条数据使用。

指针

  • 指针就是存储器中某条数据的地址
  • 指针做了两件事:避免副本和共享数据。
  • 指针是一种间接形式的地址(怎么理解?)
  • * 运算符可以读取存储器地址中的内容。* 运算符还可以设置存储器地址中的内容。

字符串

  • 指向字符串字面值(string literal)的指针变量不能用来修改字符串的内容

    • 在存储器的非只读区域创建了字符串的副本,就可以修改它的字母
    • 如果你想把指针设成字符串字面值,必须确保使用了 const 关键字:

      1. // bus error 运行时崩溃
      2. char *card = "JQK";
      3. card[1] = ‘A‘;

      编译错误,比在运行时崩溃好太多了!

      1. // 编译不通过
      2. const char *s = "some string";
      3. s[0] = ‘S‘;
    • 加不加 const,字符串字面值都是只读的,const 修饰符表示,一旦你试图用 const 修饰过的变量去修改数组,编译器就会报错

      1. // 通过jimmy 修改内容就会报编译错误,
      2. // 但是通过masked_raider 修改就可以成功。
      3. char masked_raider[] = "Alive";
      4. const char *jimmy = masked_raider;

数组

  • 数组的索引值是一个偏移量
  • 数组变量好比指针……

    1. char quote[] = "Cookies make you fat";

    计算机为字符串的每一个字符以及结束字符 \0 在栈上分配空间,并把首字符的地址和 quote 变量关联起来。函数传参时传给函数的是指针。

  • 数组变量与指针又不完全相同(区别:重点理解2、3点)

    1. sizeof(数组) 是……数组的大小;sizeof(指针) 返回4或8。
    2. 数组的地址……是数组的地址;指针的地址是另一个地址。

      1. &s == s; // 数组变量
      2. &t != t; // 指针
    3. 数组变量不能指向其他地方。
      计算机会为数组分配存储空间,但不会为数组变量分配任何空间。

  • 指针退化

    把数组赋给指针变量,指针变量只会包含数组的地址信息,而对数组的长度一无所知,相当于指针丢失了一些信息。我们把这种信息的丢失称为退化。

结构(结构体)

  • 结构可以像数组那样在结构中保存字段,但读取时只能按名访问。

    Q1:数组变量就是一个指向数组的指针,那么结构变量是一个指向结构的指针吗?

    A1:不是,结构变量是结构本身的名字。 数组变量的地址是数组变量自身;结构变量的地址是指针,不是自身。

  • 为结构变量赋值相当于叫计算机复制数据。

    重点在函数传参时很可能浪费较多存储资源;而数组作为函数参数传的是指针。

  • typedef 为结构创建别名。用 typedef 定义结构时可以省略结构名,只写类型名。

    1. // 定义结构体
    2. struct call_phone {
    3. int cell_no;
    4. const char *wallpaper;
    5. float minutes_of_charge;
    6. };
    7. // 创建别名
    8. typedef struct call_phone {
    9. int cell_no;
    10. const char *wallpaper;
    11. float minutes_of_charge;
    12. } phone;
    13. // 省略结构名
    14. typedef struct {
    15. int cell_no;
    16. const char *wallpaper;
    17. float minutes_of_charge;
    18. } phone;
  • 关于“对齐”,可以查看原文或者进一步从网上学习

  • 在C语言中,参数按值传递给函数。

    作者要表达的就是字面意思[email protected]但这句话在国内出版物中经常作为前半句出现—“值传参和“指针传参”,以至于之前理解都有偏差。其实“指针传参”还是按值传递给函数的。

联合

  • 定义一种叫“量”的数据类型,然后根据特定的数据决定要保存个数、重量还是容积。
  • 每次创建结构实例,计算机都会在存储器中相继摆放字段,联合则不同。当定义联合时,计算机以其中最大的字段分配空间,然后由你决定里面保存什么值。

    1. typedef union {
    2. short count;
    3. float weight;
    4. float volume;
    5. } quantity;
  • 指定初始化器(designated initializer)按名设置结构和联合字段,它属于C99标准

    1. quantity q = {.weight=1.5};
    2. phone p= {.cell_no=15210, .minutes_of_charge=1.2};

    联合提供了一种让你创建支持不同数据类型的变量的方法。

  • 联合经常和结构一起用。创建联合相当于创建新的数据类型。

  • 编译器不会记录你在联合中设置或读取过哪些字段。

    Q1:可以在 union 中保存任何字段(countweight 或者 volume)的值,这些不同类型的值保存在存储器中相同的位置……既然如此,你怎么知道我保存的是 float 还是 short?要是我保存了 float 字段,却读取了 short 字段呢?

    A1:解决方法:只要用枚举或其他东西记录一下就行了。

枚举

(略)

位字段

  • 位字段(bitfield)的初衷是节省存储器空间:真/假的值只需要一位就能表示;月份等小范围的数字……
  • 只有当多个位字段出现在同一个结构中,才能节省空间。

    如果编译器发现结构中只有一个位字段,还是会把它填充成一个字,这就是为什么位字段总是组合在一起。

  • 可以用位字段指定一个字段有多少位。位字段应当声明为 unsigned int

    1. typedef struct {
    2. // 是否第一次参观?
    3. unsigned int first_visit:1;
    4. // 还会再来吗?
    5. unsigned int come_agin:1;
    6. // 被咬掉了几根手指?
    7. unsigned int fingers_lost:4;
    8. // 被鲨鱼袭击过吗?
    9. unsigned int shark_attack:1;
    10. // 一周来几天?
    11. unsigned int days_a_week:3;
    12. } survey;

Q1:为什么C语言不支持二进制字面值?

A1:因为二进制字面值占了很大空间(质疑?),而且十六进制通常写起来更快。

递归

  • 为了保存可变数量的数据,需要一样比数组更灵活的东西,即链表。

    链表是一种抽象数据结构。链表是通用的,可以用来保存很多不同类型的数据,所以被称之为抽象数据结构。

  • 与数组相比,链表还有一个优点:插入数据非常快。

    Q1:如何在C语言中创建链表?

    A1:通过创建递归结构实现。

  • 如果一个结构包含一个链向同种结构的链接,那么这个结构就被称为递归结构。

    在递归结构中,需要包含一个相同类型的指针,C语言的语法不允许用typedef别名来声明它。个人猜测,应该和声明顺序有关。毕竟如果 typedef 新类型 newtype,然后 newtype * pointer 创建指针,是可行的。

    1. // 在递归结构中,必须为结构体命名。别名可选
    2. typedef struct island {
    3. char *name;
    4. char *opens;
    5. char *closes;
    6. struct island *next; // 此处不允许使用别名来声明
    7. } island;

    >

    C语言需要知道结构在存储器中占的具体大小,如果在结构中递归地复制它自己,那么两条数据就会不一样大。指针的大小是确定的。

  • 在C语言中,NULL 的值实际上为0

三、分而治之

小工具

问:什么是小工具?——其实就是其字面意思,重点是要有这个概念。

操作系统都有小工具。类Unix的操作系统为完成工作会大量使用工具,Windows用的少一些。

C语言小工具执行特定的小任务,例如读写文件、过滤数据。如果要完成更复杂的任务,可以把多个工具链接在一起。

小工具是一个C程序,它做一件事情并把它做好。

当你想解决一个大问题时,可以把它分解成一连串的小问题,然后针对每个小问题写一个小工具。

问:为什么小工具要使用标准输入和标准输出?

答:有了它们,就可以轻易用管道将小工具们串连起来。

问:如果两个程序用管道相连,第二个程序要不要等第一个程序执行完后才能开始运行?

答:不需要,两个程序可以同时运行,第一个程序一发出数据,第二个程序马上就可以处理。

输入输出

  • 使用 scanf() 时要小心:限制 scanf() 能读入的字符数

    1. // 注意字符数组的长度,和scanf 读取的限制
    2. int main()
    3. {
    4. char card_name[3];
    5. puts("输入牌名:");
    6. scanf("%2s", card_name);
    7. // other code
    8. return 0;
    9. }

    如果忘了限制 scanf() 读取字符串的长度,用户就可以输入远远超出程序空间的数据,多余的数据会写到计算机还没有分配好的存储器中

  • 如果你要向 fgets() 函数传递数组变量,就用 sizeof;如果只是传指针,就应该输入你想要的长度。

  • 操作系统控制数据如何进出标准输入、标准输出。
  • scanf()printf() 函数,只管从标准输入读数据,向标准输出写数据
  • 进程有一只耳朵(标准输入)和两张嘴(标准输出和标准错误)
  • printf() 其实只是 fprintf() 函数的特例,
  • 可以用 > 重定向标准输出,2> 重定向标准错误。
  • 用管道连接输入与输出

创建自己的数据流

  • 程序运行时,操作系统会为它创建三条数据流:标准输入、标准输出和标准错误。但有时你需要创建自己的数据流。

    每条数据流用一个指向文件的指针来表示,可以用 fopen() 函数创建新数据流。

    创建数据流后,可以用 fprintf() 函数往数据流中打印数据;可以用 fscanf() 函数从数据流中读取数据。

    当用完数据流,别忘了使用 fclose() 函数关闭它。

  • 虽然所有的数据流在程序结束后都会自动关闭,但你仍应该自己关闭它们:

命令行参数

  • 十个程序有九个需要选项。聊天程序有“系统设置”,游戏有调整难度的选项,而命令行工具需要有命令行选项。

    很多程序都会用到命令行选项,因此有一个专门的库函数,可以使用它来简化处理流程。这个库函数叫做 getopt(),每一次调用都会返回命令行中下一个参数。

    有关函数的具体使用方法,翻阅原书或者 google 都行。

  • unistd.h头文件不属于C标准库,而是POSIX库中的一员。POSIX的目标是创建一套能够在所有主流操作系统上使用的函数。

使用多个源文件

  • 当编译器看到尖括号,就会到标准库代码所在目录查找头文件;用引号把文件名括起来,编译器就会在本地查找文件。
  • C语言是一种很小的语言,共有32+n个保留字。以下列出部分:

    auto register static extern typedef union volatile

  • 使用头文件

    • (函数)声明与定义分离

      将声明和定义分离之后也不用再严格调整函数定义之间的顺序。

      把声明放到一个独立的头文件中有两大优点,第一是主代码变短了,第二可以共享代码(被两个以上源文件 include 时)。

    • 如此,就可以在不同的文件之间共享函数了,但如果你想共享变量呢?

      为了防止两个源文件中的同名变量相互干扰,变量的作用域仅限于某个文件内。如果你想共享变量,就应该在头文件中声明,并在变量名前加上 extern 关键字(需要进一步的学习):

      ps:原文表述不太恰当。改为:

      1. 为了防止两个源文件中的同名变量相互干扰,变量的作用域仅限于某个文件内——(这句话存在异议,使用 gcc 实际编译时不同文件使用同名变量会报错“重复定义”)。
      2. 如果你想共享变量,就应该在头文件中声明。——声明变量,即在变量名前使用 extern 关键字,不应该用“并”。
      3. 关于 extern,见《进一步学习extern》笔记。

四、动态存储

  • ——另起篇幅




以上是关于《嗨翻C语言》(上)的主要内容,如果未能解决你的问题,请参考以下文章

我的C语言学习进阶之旅解决 Visual Studio 2019 报错:错误 C4996 ‘fscanf‘: This function or variable may be unsafe.(代码片段

我的C语言学习进阶之旅解决 Visual Studio 2019 报错:错误 C4996 ‘fscanf‘: This function or variable may be unsafe.(代码片段

SQL Select 语句的用法

Head First C 笔记

C语言100个经典算法源码片段

需要示例代码片段帮助