程序员的自我修养——读书笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lemonwxj/article/details/51373251

第一部分:温故而知新
第一章:介绍基本的背景知识——操作系统、线程、硬件

1、关于C语言中的hello world这些问题你都清楚吗?
这里写图片描述
2、计算机硬件设备的三个核心部件:
1》中央处理器CPU
2》内存
3》I/O控制芯片

3、
1》开发工具与应用层是属于同一个层次的,因为他们都使用同一个接口,那就是——应用程序编程接口(API——Application Programming Interface)
2》而应用程序接口的提供者是运行库。
3》运行库使用操作系统提供的系统调用接口。
4》系统调用接口在视线中往往以软件中断的方式提供

4、操作系统的功能:
1》提供抽象接口。
2》管理硬件资源

5、多道程序——》分时系统——》多任务系统(CPU采用抢占式分配方式)

6、线程:
1》线程,有时候被称为轻量级进程(LWP),是程序执行流得最小单元。
2》一个标准的线程由线程ID、当前指针指令(PC)、寄存器集合、堆栈组成
3》通常,一个进程由一个到多个线程组成,各线程之间共享程序的内存空间(包括代码段、数据段、堆等)以及一些进程级的资源(如打开文件和信号)

7、二元信号量:
1》二元信号量是一种最简单的锁。它只有两种状态:占用和非占用。
2》它适合只能被唯一一个线程独占访问的资源,当二元信号处于非占用状态时,第一个试图获取该二元信号量的线程会获取该锁,并将二元信号量置为占用状态,以后其他的所有试图获取该二元信号量的线程将会被等待,直到该锁被释放。

8、volatile关键字:
1》阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回。
2》阻止编译器调整volatila变量的指令顺序
3》volatile虽然能够阻止编译器调整顺序,但是无法阻止CPU动态调度换序。

8+、单例模式:Singleton
1》单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。
2》通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约资源。
3》如果希望在系统中的某个类的对象只能存在一个,单例模式是最好的解决方案。

9、一个类只能创建一个对象

class Singleton
{
                 friend Singleton* CreateObj();
                 friend void ReleaseObj();
private:
                Singleton()
                {
                                cout << "Singleton()" << endl;
                }
                ~Singleton()
                {
                                cout << "~Singleton()" << endl;
                }
private:
                 int data;
                 static Singleton* pObj;
                 static std::mutex _mutex;
};
Singleton* Singleton::pObj = NULL;
std::mutex _mutex;

Singleton* CreateObj()
{
                 if (Singleton::pObj == NULL )
                {
                                _mutex.lock();
                                 if (Singleton::pObj == NULL )
                                {
                                                 //线程同步
                                                 //std::_Mutex mutex;//互斥量
                                                Singleton::pObj = new Singleton;
                                }
                                _mutex.unlock();
                }
                 return Singleton::pObj;
}
void ReleaseObj()
{
                 if (Singleton::pObj != NULL )
                {
                                 delete Singleton::pObj;
                                Singleton::pObj = NULL;
                }
}

10、另一个著名的与换序有关的问题:Singleton模式的double-check.
一段经典的代码:

volatile T* pInst=0;
T* GetInstance( )
{
    if(pInst == NULL)
    {
         lock( );
         if(pInst == NULL)
         {
              pInst = new T;
         }
         unlock( );
    }
    return pInst;
}

——当函数返回时,pInst总是指向一个有效地对象。而lock 和 unlock防止了多线程竞争的问题。
——双重if可以让lock的调用开销降低到最小。
但是:实际上这段代码是有问题的,问题来源于CPU的乱序执行。
1》C++里的new包含了两个步骤:分配内存 、 调用构造函数
2》所以pInst = new T也包含了三个步骤:1分配内存 、2在内存的位置上调用构造函数 、3 将内存的地址赋值给pInst。
3》在上面的三步中,2 3 的顺序是可以颠倒的。也就是说完全有可能出现这样的情况:pInst的值已经不是NULL了,但是对象还没有构造完。这时候,如果出现另外一个对GetInstance的并发调用,此时第一个if内的表达式pInst == NULL 为false,所以这个调用会直接返回尚未构造完的对象的地址已提供给用户使用,那么程序就有可能崩溃。

11、内存屏障——barrier指令
1》内存屏障,也称内存栅栏、屏障指令,是一类同步屏障指令,使得CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。
2》大多数现代计算机为了提高性能而采取的乱序执行,这使得内存屏障成为必须。
3》语义上:内存屏障前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。
4》因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。

12、许多体系结构的CPU都提供barrier指令,不过它们的名称各不相同,例如POWERPC提供的其中一条指令名叫lwaync。
我们可以这样保证线程安全:

#define barrier( ) _asm_ volatile ( "lwaync")
volatile T* pInst=0;
T* GetInstance( )
{
    if(pInst == NULL)
    {
         lock( );
         if(pInst == NULL)
         {
              T* temp = new T;
              barrier( );
              pInst = temp;
         }
         unlock( );
    }
    return pInst;
}

——由于barrier的存在,对象的构造一定在barrier执行之前完成,因此当pInst被赋值时对象总是完整的。

第二部分:静态链接
第二章:编译和链接

1、一个 Hello World 程序 需要完成四个步骤:分别是 预处理(Prepressing) 、编译(Compilation) 、汇编 (Assembly)、和链接(Linking)

2、
这里写图片描述
3、预编译:
1》首先是源代码文件helloc.c和相关的头文件,如stdio.h等被预编译器cpp预编译成一个 .i 文件。对于C++程序来说,它的源代码文件的扩展名可能是 .cpp 或 .cxx ,头文件的扩展名可能是 .hpp ,而预编译后的文件按扩展名是 .ii 。
2》第一步预编译的过程相当于如下的命令(-E表示只进行预编译):
gccEhello.cohello.igcc hello.c > hello.i
3》预编译过程主要处理那些源文件代码中的以“#”开始的预编译指令,比如“#include”、“#define”等,主要处理规则如下:
1>将所有“#define”删除,并展开所有的宏定义。
2>处理所有条件预编译指令,比如:“if”、“ifdef”、“elif”、“else”、“endif”
3>处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
4>删除所有的注释“//”和“/* */”.
5>添加行号和文件标识,比如#2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息以及用于编译时产生的编译错误或者警告时能够显示行号。
6>保留所有#pragma 编译器指令,因为编译器需要使用它们。
4》经过预编译之后的 .i 文件不包含任何宏定义,因为所有的宏都已经被展开了,并且包含的文件也已经被插入到 .i 文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

4、编译:
1》编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后产生相应的汇编代码文件,这个过程往往是我们所说的整个程序构建的核心部分,也是最复杂的部分之一。
2》上面的编译过程相当于如下命令:gccshello.iohello.sgcc -s hello.o -o hello.s
3》实际上gcc这个命令只是这些后台程序的包装,它会根据不同的参数要求去调用预编译编译程序cc1、汇编器as、链接器ld

5、汇编:
1》汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于汇编器来讲比较简单,它没有复杂的语法,也没有语义,也不需要指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。
2》上面的汇编过程我们可以调用汇编器as来完成: ashello.sohello.ogcc -c hello.s -o hello.o 或者使用gcc命令从C源代码文件开始,经过预编译、编译、汇编直接输出目标文件 $gcc -c hello.c -o hello.o

6、链接:
这里写图片描述

7、编译器:
编译器的作用:从最直观的角度来讲,编译器就是将高级语言翻译成机器语言的一个工具。比如我们用C/C++语言写的一个程序可以使用编译器将其翻译成机器可以执行的指令及数据。

8、编译过程:
1》编译过程一般分为6步:扫描、词法分析、语义分析、源代码优化、代码生成、和目标代码优化
2》整个过程如下图:
这里写图片描述

9、词法分析:首先源代码程序被输入到扫面器(Scanner),扫描器的任务很简单,它只是简单的进行词法分析,运用一种类似于有限状态机(Finite State Machine)的算法可以很轻松的将源代码的字符序列分割成一系列的记号(Token)。

10、语法分析:接下来语法分析器(Frammar Parser)将对扫描器产生的记号进行语法分析,从而产生了语法树。整个分析过程采用了上下文物管语法的分析手段。

11、语义分析:语义分析是由语义分析器来完成的。

展开阅读全文

没有更多推荐了,返回首页