0%

从理解静态绑定 & 动态绑定开始,没想到又扯出静态类型 & 动态类型,编译期多态 & 运行期多态。但这些本来就是一体的。

静态绑定 & 动态绑定

刚开始只是看完网上的几篇帖子啰嗦几句心得,落笔时什么都没参考。重新整理笔记时硬是把以前保存在 chrome 书签中帖子给塞进来,也是够难受的。C++中的静态绑定和动态绑定

  • 静态绑定(statically bound),又名前期绑定(early binding);
  • 动态绑定(dynamically bound),又名延期绑定(late binding)。

ps 英文名称摘自《Effective C++》 条款37。此条款中有关于“静态类型、动态类型”的描述。

在 C 语言中并没有“静态绑定”、“动态绑定”的概念(至少我没有查到)。

阅读全文 »

字符串之间的转换分为两类:

  1. 底层字符集之间的转换,表现为 char wchar_t 之间的转换,string wstring 之间的转换;
  2. 在此之上牵扯到 Windows 平台下的各种字符串之间的转换;去伪存真,归根到底还是第一种转换。

为了“知其然,也要知其所以然”,我们在描述相互转换之前,先介绍一些相关的类型。上层所有的字符串类型归根到底都是对底层 char 或者 wchar_t 类型的封装,如果真有刨根问底的兴趣,就需要了解 char wchar_t 的关联和区别。我们比较熟悉 char,接下来看一下 wchar_t

阅读全文 »

我们先从使用的角度,从最直观的数据类型说起。

ps 这一篇笔记并不显式地涉及 wchar_t,也不包含字符(串)之间如何转换。更多内容请跳转 字符(串)之间的转换

类型:char[]\char*\string\CString

转帖来源:C++中的字符串

C++支持两种字符串:C风格字符串和string。之所以抛弃char*的字符串而选用C++标准程序库中的string类,是因为它和前者比较起来,不必担心内存是否足够、字符串长度等等,而且作为一个类出现,它集成的操作函数足以完成我们大多数情况下(甚至是100%)的需要。我们可以用 = 进行赋值操作,== 进行比较,+ 做串联(是不是很简单:-D)。我们尽可以把它看成是C++的基本数据类型。此外,CString类在MFC中广泛应用,简化了字符串的处理。

阅读全文 »

首先,要区分全局变量和局部变量。

全局变量和局部变量

在 C 和 C++ 中,局部变量意义相同;全局变量略有出入。

在《C++ Primer》中很明确的表示:(p44)

名字 main 定义于所有花括号之外,它和其他大多数定义在函数体之外的名字一样拥有全局作用域。一旦声明之后,全局作用域内的名字在整个程序的范围内都可使用。

C语言中文网-C语言局部变量和全局变量 中指出:

在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域是整个源程序

阅读全文 »

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)中,必须在函数的顶部声明局部变量;

更多的区别:1. 在早期的 ANSI C 标准中,main() 函数可以是 void 类型。但是在 C99 中 main 函数的返回类型必须是 int。2. ANSI C标准没有用来表示真和假的值,C语言把0这个值当做假处理,把非0的任何值当做真处理。C99标准则允许在程序中使用true和false关键字。但编译器还是会把他们当做1和0这两个值来处理。

扩展阅读:

gcc

gcc 是 *inux 下的一款编译器,上述三种标准它都支持。

ps 这种说法太笼统,请阅读 GCC对C标准的支持GCC的默认C标准哪个版本的gcc才支持c11有哪些支持C11标准的编译器

  • 在不指定C标准的情况下,GCC默认使用GNU C,所以如果你想让编译器遵循C99标准,需要使用 -std=99 选项。
  • gcc是到了4.7,才真正支持c11的。

windows下使用gcc

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

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

摘两条在书中描述的 Cygwin 项目不完美的地方:

一些Cygwin的gcc版本允许修改字符串字面值,不会报错,但这样做常常是错的。

Cygwin的很多版本中,在多个信号的发送顺序和接收顺序问题上,做了不恰当的假设。

其他

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

基础知识

表达式

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

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

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

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

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

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

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

存储器

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

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

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

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

指针

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

字符串

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

    • 在存储器的非只读区域创建了字符串的副本,就可以修改它的字母
    • 如果你想把指针设成字符串字面值,必须确保使用了 const 关键字:
      1
      2
      3
      // bus error 运行时崩溃
      char *card = "JQK";
      card[1] = 'A';
      编译错误,比在运行时崩溃好太多了!
      1
      2
      3
      // 编译不通过
      const char *s = "some string";
      s[0] = 'S';
    • 加不加 const,字符串字面值都是只读的,const 修饰符表示,一旦你试图用 const 修饰过的变量去修改数组,编译器就会报错
      1
      2
      3
      4
      // 通过jimmy 修改内容就会报编译错误,
      // 但是通过masked_raider 修改就可以成功。
      char masked_raider[] = "Alive";
      const char *jimmy = masked_raider;

      数组

  • 数组的索引值是一个偏移量

  • 数组变量好比指针……

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

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

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

    1. sizeof(数组) 是……数组的大小;sizeof(指针) 返回4或8。
    2. 数组的地址……是数组的地址;指针的地址是另一个地址。
      1
      2
      &s == s;  // 数组变量
      &t != t; // 指针
    3. 数组变量不能指向其他地方。
      计算机会为数组分配存储空间,但不会为数组变量分配任何空间。
  • 指针退化

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

结构(结构体)

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

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

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

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

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

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

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

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

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

联合

  • 定义一种叫“量”的数据类型,然后根据特定的数据决定要保存个数、重量还是容积。

  • 每次创建结构实例,计算机都会在存储器中相继摆放字段,联合则不同。当定义联合时,计算机以其中最大的字段分配空间,然后由你决定里面保存什么值。

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

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

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

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

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

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

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

枚举

(略)

位字段

  • 位字段(bitfield)的初衷是节省存储器空间:真/假的值只需要一位就能表示;月份等小范围的数字……

  • 只有当多个位字段出现在同一个结构中,才能节省空间。

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

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

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

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

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

递归

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

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

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

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

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

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

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

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

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

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

分而治之

小工具

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

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

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

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

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

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

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

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

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

输入输出

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

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

    如果忘了限制 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++类中定义线程函数

参考:在C++类中定义线程函数的方法

在多线程的开发中,一般都是把线程函数写成全局函数来使用。但是如果要把线程操作写成类,线程函数放在类里面呢?

下意识地会把线程函数写作普通的类函数,但这样子是有问题的。因为在创建线程的 api 中传入的线程函数需要在编译时确定地址,如果是普通的类函数,编译时不能确定地址,需要创建类的对象才能获取。我们可以把线程的执行函数写成 static 函数,或者是全局函数,因为这两者的函数地址在编译时是确定的。

两者的区别:static 在形式上能够体现“包装”性,能够保全类的封装性;全局函数貌似通过命名空间也能提供“包装性”呢,破坏类的封装性。实际业务中,我个人更倾向于前者——使用类的静态函数作为线程入口函数。

阅读全文 »

每次用到时,都要到网上搜索一下的感觉很不好。尤其是经常用到,每次搜索时你完全能认识到你已经查过很多遍了。另外,和不自信(拿不准的心理)以及养成了这种坏习惯都有关系,查得多了自然知道调用 C 标准库 atoi(itoa 不是 C 标准库函数)以及使用 stringstream 流来解决问题,但每每觉得差那么一点意思,不够简洁。每次用到时都要搜索一下,可能是希望找到一种让内心舒坦的转换“手法”吧。

参考 C++11 中的 string - atoi/itoa,岂止是参考,根本就是抄袭。可是好不喜欢原文的排版。

阅读全文 »

2018/9/19 17:46:55 这篇罗列概念很没水平,就别看了。

后记:这篇笔记限于罗列书中的概念,自己的理解并不多,组织章节也不好。后来又整理过一篇: 《再谈初始化》 ,两者不分先后,没有依赖关系。

参考《C++ Primer》和网上资源誊抄完正文后,自己总结如下:

  1. 定义一个变量/对象时,如果不进行显式地初始化,就是默认初始化(除了使用 extern 进行纯粹的声明的)。需要关注的是,默认初始化时编译器究竟赋予了变量/对象什么内容。

  2. 默认初始化变数太多,比如造成内置类型未定义行为,对于使用编译器合成的默认构造函数的类也可能产生不良影响,所以在定义变量时最好进行初始化!初始化的语法格式有好多种呢:

    • 根据是否使用等号分为拷贝初始化和直接初始化;
    • 根据是否使用了花括号,区分出列表初始化;
  3. 想在初始化时偷懒,不想写太多参数(vector 值初始化);但又操心默认初始不好使(new 动态分配时值初始化)。所以,又出现了值初始化。

阅读全文 »

动态存储(189~)

  • 堆上的数据不会自动清除,因此堆是保存数据结构的绝佳场所。

    可以把在堆上保存数据想象成在储物柜中寄存物品。

  • 当程序不断地申请存储器,又不释放那些不再需要的存储器,就会发生存储器泄漏

    创建数据不会发生泄漏,只有当程序失去了所有对数据的引用才会导致泄漏。

  • malloc()获取空间,memory allocation;调用free()释放存储器

    • malloc()接收一个参数:所需要的字节数。因此malloc()经常与sizeof运算符一起使用。
    • malloc()返回的是通用指针,即void*类型的指针。
  • strdup()函数可以把字符串复制到堆上。

    strdup()函数总是在堆上创建空间,而不是在栈上。所以千万记得要用free()函数释放空间。

  • valgrind工具,它用于Linux操作系统中。

    当程序想分配堆存储器时,valgrind将会拦截你对malloc()free()的调用,然后调用自己的malloc()free()

  • 垃圾收集(garbage collection),一些语言会跟踪你在堆上分配的数据,当你不再使用这些数据时,就会释放它们。C语言没有“垃圾收集”

虽然不必在程序结束前释放所有数据,操作系统会在程序结束时清除所有存储器。不过,你还是应该显式释放你创建的每样东西,这是一种好的习惯。

——很不懂作者为什么在章节快结束的时候,放上一句这么没营养的话。有机会去查一查原文,确认是不是译者的问题。个人认为以下表述更恰当:
虽然操作系统会在程序结束时清除所有存储器。不过,你还是应该显式释放你创建的每样东西,这是一种好的习惯。

高级函数(201~)

函数指针

此章节介绍“学习如何把函数作为参数传递,从而提高代码的智商”。

有些时候,我们希望“把代码打包传给函数”。“把代码打包”就是封装成函数,所以,问题就是:怎么把函数传给函数?

  • 在C语言中,函数名也是指针变量。

    当你创建了一个叫go_to_warp_speed(int speed)函数的同时也会创建了一个叫go_to_warp_speed的指针变量,变量中保存了函数的地址。只要把函数指针类型的参数传给find(),就能调用它指向的函数了。

    上述中go_to_warp_speed指针变量是一个常量。类似 int* a=4中的4, char* str="abc"中的"abc"

    扩展

    1
    2
    3
    4
    5
    6
    /* 此处声明时的*和 赋值操作时的*应不是同一个意思?!
    那么为什么初始化不写成int* a=&4?? —就是声明了一个指针,指向了常量存储区的一块地址 */
    int* a; //声明
    int b=4;
    *a=4; //改变指针指向存储器中的值,但单纯这么写是错的
    a=&b; //给指针赋值,改变指针指向的存储器
  • 函数名是指向函数的指针……两者并不完全相同,函数名是L-value,而指针变量是R-value,因此函数名不能像指针变量那样自加或自减。

  • 函数指针是C语言最强大的特性之一。

函数指针的语法

  • 没有函数类型,即:不能用function * pointer_func 声明函数指针。

  • 因为需要把函数的返回类型和接收参数类型告诉C编译器,所以创建函数指针如下:

    返回类型(* 指针变量)(参数类型),例如

    1
    2
    3
    int (*warp_fn)(int);  //创建函数指针
    wrap_fn = go_to_warp_speed; //函数指针赋值
    wrap_fn(4); //使用函数指针

    Q1:如果函数指针是指针,为什么调用时不需要再它们前面加*

    A1:可加可不加。wrap_fn(4)(*wrap_fn)(4)都可以。如果fp是函数指针,那么可以用fp(参数,……)调用函数。 也可以用(*fp)(参数,……),两种情况都能工作。

    Q2:可以用&取得函数的地址吗?

    A2:可以。find(sports_or_workout)find(&sports_or_workout)都可以。如果你有函数shoot(),那么shoot&shoot都指向了shoot()函数。

    Q3:那为什么不这么写?

    A3:即使省略*&,C编译器也能识别他们,(而且)这样代码更好读。

    延伸:由A3推测,是不是 常量4或者"abc"既表示本身的值,又代表其在常量存储区的地址?此处亦类似?

C标准库的排序函数

  • C标准库的排序函数会接收一个比较器函数(comparator function)指针,用来判断两条数据的大小。
  • qsort()函数看起来像这样:
    1
    2
    3
    4
    qsort(void* array,       //数组指针
    size_t length, //数组长度
    size_t item_size, //数组中每个元素的长度
    int (*comparator)(const void*, const void*));
  • qsort()函数在原数组上进行改动。

创建函数指针数组

  • 在数组中保存函数,就必须告诉编译器函数的具体特征:函数返回什么类型以及接收什么参数。
    1
    void (* replies[])(response) = {dump, second_chance, marriage};
  • 函数指针数组让代码易于管理,它们让代码变得更短、更易于扩展,从而可以伸缩。
    ——至于说降低程序可读性,那是因为你笨。

可变参数函数(variadic function)(228~)

C标准库中有一组宏(macro)可以帮助你建立自己的可变参数函数。
——TODO:kindle中描述这一组宏的图片看不清,抽空在网上找一下。

静态库和动态库

(省略。在接触Linux下C++开发之后,这方面知识不算陌生。)

系统调用(249~)

在大部分计算机上,系统调用就是操作系统内核中的函数,是程序用来与内核对话的函数。

system()

  • 简单易用

  • 安全问题

  • 无法应付复杂的应用场景

    操作系统必须解释你传给 system()的字符串,这可能引发错误,尤其当你动态创建命令字符串时。

exec()

  • exec() 函数通过运行其他程序来替换当前进程。

    • exec()函数的版本众多,但可以分为两组:列表函数和数组函数。
    • 如果exec()调用成功,当前程序就会停止运行。一旦程序运行了exec()以后的代码,就说明出了问题。
  • 每个进程都有一组环境变量。

失败黄金法则

  • 尽可能收拾残局

  • errno变量设为错误码

    • errno变量是定义在errno.h中的全局变量,和它定义在一起的还有很多标准错误码
    • 可以使用string.hstrerror()函数查询标准错误信息
  • 返回-1

    • 系统调用出错时通常会返回-1,但不是绝对的。
    • fileno():根据文件指针获取文件描述符。失败时不返回-1
    • exit()是唯一没有返回值而且不会失败的函数。
    • 记住:一定要检查系统调用的返回值

fork()

  • fork()会克隆当前进程

    • 新建副本将从同一行开始运行相同程序。
    • 原进程叫父进程,而新建副本叫子进程。
  • 进程需要以某种方式区分自己是父进程还是子进程

    fork()会返回一个整型值:为子进程返回0,为父进程返回一个正数。父进程将接收到子进程的进程标识符。

  • 与Unix和Mac不同,Windows天生不支持fork()

进程间通信(274~)

重定向

  • 三大默认数据流:标准输入、标准输出和标准错误;文件连接和网络连接也属于数据流。

  • 进程需要记录数据流的连向,比如标准输出连到了哪里。进程用文件描述符(就是个数字)表示数据流

  • 描述符表:描述符表的前三项万年不变:0号标准输入,1号标准输出,2号标准错误

    • 标准输入/输出/错误在描述符表中的位置是固定的,但它们指向的数据流可以改变。
    • 每打开一个文件,操作系统都会在描述符表中新注册一项
  • dup2()复制数据流

  • exit()是系统调用!

    exit()系统调用是结束程序的最快方式

  • waitpid()函数会等子进程结束以后才返回

    • 为了得到子进程的退出状态,可以把pid_status的值传给WEXITSTATUS()

管道

  • 在命令行用管道连接两条命令时,实际把它们当成了父子进程来连接。child | parent,详见shell中管道的原理

  • pipe()函数建立管道

    pipe()函数创建了管道,并返回了两个描述符:fd[1]用来向管道写数据,fd[0]用来从管道读数据,

信号

  • 当信号到来时,进程必须停止手中一切工作去处理信号。进程会查看信号映射表,表中每个信号都对应一个信号处理器函数。

捕捉信号,然后运行自己的代码

  • sigaction是一个函数包装器,是一个结构体。
  • 创建sigaction以后,需要用sigaction()函数来让操作系统知道它的存在
  • 有一个例外,代码捕捉不到SIGKILL信号,也没法忽略它
  • 在自定义的信号处理函数中使用raise(),这样程序就能在接收到低级别的信号时引发更高级别的信号

定时器

  • 不要同时使用alarm()sleep()。两者都使用了间隔计时器,因此同时使用这两个函数会发生冲突。

  • 一个进程只有一个定时器。因此每次调用alarm()函数都会重置定时器

    定时器由操作系统的内核管理,如果一个进程有很多定时器,内核就会变得很慢,因此操作系统需要限制进程能使用的定时器个数。

  • 信号可以让程序从容结束,而间隔定时器可以帮助处理一些超时任务。

网络编程(323~)

C程序用数据流读写字节。如果想要写一个与网络通信的程序,就需要一种新数据流——套接字。

  • 在使用套接字与客户端程序通信前,服务器需要经历四个阶段:

    1. 绑定(Bind)

      • 为了绑定它(端口),你需要两样东西:套接字描述符(还记得“文件描述符”吗?)和套接字名。
    2. 监听(Listen)

    3. 接受(Accept)

    4. 开始(Begin)

  • 之前见过的数据流都可以用fprintf()fscanf()与它们通信。但套接字是双向的:

    • 如果想向套接字输出数据,就要用send()函数;
    • recv()函数;
  • 绑定端口有延时:当你在某个端口绑定了套接字,在接下来的30秒内,操作系统不允许任何程序再绑定它,包括上一次绑定这个端口的程序。

  • recv()读取数据

    • 字符串不以\0结尾。
    • 当用户在telnet输入文本时,字符串以\r\n结尾。
    • recv()将返回字符个数,如果发生错误就返回-1,如果客户端关闭了连接,就返回0
    • recv()调用不一定能一次接收到所有字符。
    • revc()用起来十分繁琐,最好把它封装在某个函数中
  • getaddrinfo()获取域名的地址

    • getaddrinfo()会在堆上创建一种叫名字资源的新数据结构
    • 因为名字资源在堆上创建,所以要用一个叫freeaddrinfo()的函数清除它

多线程(342~)

你可以使用很多线程库,这里我们将使用最流行的一种:POSIX线程库,也叫pthread。

  • 线程函数的返回类型必须是void*

  • 可以用pthread_create()创建并运行线程。

  • 如果程序运行完这段代码就结束了,线程也会随之灭亡,因此必须等待线程结束:

    • pthread_join()会接收线程函数的返回值,并把它保存在一个void指针变量中
  • 通常当两个线程读写相同变量时,代码就是非线程安全的。

  • PTHREAD_MUTEX_INITIALIZER实际上是一个宏,当编译器看到它,就会插入创建互斥锁的代码。

  • 如果你希望把某个整型值传给线程,并让它返回某个整型值,一种方法是:

    • long,因为它的大小和void指针相同,可以把它保存在void指针变量中

问:怎样设计高效的多线程程序? 答:减少线程需要访问的共享数据的数量。

写在最后

另一种理解思路:

  • C语言允许你创建只能在函数局部作用域访问的全局变量:

    • 函数内:static关键字会把变量保存在存储器中的全局量区。
  • static关键字用来控制变量或函数的作用域

    • 函数外使用static关键字,表示“只有这个.c文件中的代码能使用这个变量(或函数)”

关于“自动化测试”,你又了解多少?

之前读书时做的笔记全部誊抄、整理完毕,上篇中的知识点基本都已熟悉掌握,下篇的内容因为学校时练习不多,实际做项目经验也很少,所以只处于了解、知道的层次,勤复习,多练习。笔记内容包含编程过程中的“为什么这样”,也包括一些具体的语法细节、函数使用(实际上后者是不应该杂糅到一起的,但是基础太差)。在以后的复习、练习过程中,争取将语法细节、函数使用这些具体化的内容一点点去掉。

C++ 获取系统时间

有的帖子真心写的很好,你都想帮作者打广告。有的烂到即使你从里面学到了知识,你也不想再见到它,你都不想在自己的笔记里放它的链接,不然说不准哪天又点进去浪费自己的时间。

说一下思路:

  1. 使用开发语言自身的标准库。对于 C++ 来说,使用 C 的或者 C++ 的
  2. 使用第三方库,如果有的话
  3. 使用系统相关 API。缺点在于不能跨平台
阅读全文 »