Skip to content

Latest commit

 

History

History
229 lines (156 loc) · 9.76 KB

More.md

File metadata and controls

229 lines (156 loc) · 9.76 KB

main函数执行前后

一个典型程序的大致运行步骤:

  1. 操作系统创建进程后,把控制权交到了程序入口,这个入口往往是程序运行库中的某个入口函数。
  2. 入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量的构造等等。
  3. 入口函数在完成初始化之后,调用main函数,正式开始执行函数主体部分。
  4. main函数执行完毕之后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程。

一些全局变量、对象和静态变量、对象的空间分配和赋初值就是在执行main函数之前。而main函数执行完后,还要去执行一些诸如释放空间、释放资源使用权等操作。

此外,用atexit注册的函数也会在main之后执行。atexit 函数是标准 C 新增的。它“注册”一个函数,使这个函数将在 exit 函数被调用时或者当 mian 函数返回时被调用。当程序异常终止时(例如调用 abort 或 raise),通过它注册的函数并不会被调用。

编译器必须至少允许程序员注册32个函数。如果注册成功,atexit 返回0,否则返回非零值。没有办法取消一个函数的注册。在 exit 所执行的任何标准清理操作之前,被注册的函数按照与注册顺序相反的顺序被依次调用。每个被调用的函数不接受任何参数,并且返回类型是 void。被注册的函数不应该试图引用任何存储类别为 auto 或 register 的对象(例如通过指针),除非是它自己所定义的。多次注册同一个函数将导致这个函数被多次调用。

如下程序:

#include <iostream>
void fn1(void)
{
    printf("next.\n");
}

void fn2(void)
{
    printf("executed ");
}

void fn3(void)
{
    printf("is ");
}

void fn4(void)
{
    printf("This ");
}

int main(void)
{
    //
    // 注册需要在 main 函数结束后执行的函数.
    // 请注意它们的注册顺序和执行顺序
    // 在 main 函数结束后被调用,调用顺序与注册顺序相反。先注册后执行。
    //

    atexit(fn1);
    atexit(fn2);
    atexit(fn3);
    atexit(fn4);

    // 这条输出语句具有参照性,它可不是最后一句输出.
    puts("This is executed first.");

    // EXIT_SUCCESS 代表 0,它定义在 stdlib.h 中.
    // 我只是顺便提一下,也许你知道,但我担心你不知道,呵呵.
    //
    return EXIT_SUCCESS;
}

浮点数存储

整数可以很方便地用二进制来存储,对于小数来说,直观上来看,转为二进制也很简单,分整数部分和小数部分分别转换,比如十进制数 9.3125 表示为二进制为 1001.0101。(整数部分用2除,取余数;小数部分用2乘,取整数位。)

0.3125 * 2 = 0.625 整数位是 0   --> .0
0.625 * 2 = 1.25   整数位是 1   --> .01
0.25 * 2 = 0.5    整数位是 0   --> .010
0.5 * 2 = 1.0      整数位是 1   --> .0101

通常,浮点数可以被表示成 N = S * r^j ,其中 S 为尾数,j为阶码,r是基数。阶码是整数,阶符和阶码的位数合起来反映浮点数的表示范围及小数点的实际位置。尾数是小数,其位数反映了浮点数的精度。

在计算机中,基数一般取 2,所以数 N=11.0101,可以写为:

0.110101 * 2^2
1.10101 * 2^1
1101.01 * 2^-2

为了提高精度以及便于浮点数的比较,在计算机中规定浮点数的尾数用纯小数表示,此外将尾数最高位为1的浮点数称为规格化数,即 N=0.110101 为N的规格化形式。

C++ 编译器中浮点数存储采用的是 IEEE 754标准,它定义的单精度浮点数的长度为 32 位,按位域可划分为:符号位、阶码位与尾数位。

单精度浮点数位域划分如下:

 31----------------------22----------------------------------------------------------0
  |                       |                                                          |
  X X X X    X X X X    X X X X    X X X X    X X X X    X X X X    X X X X    X X X X
  | |-------------------| |----------------------------------------------------------|
符号        阶码                                     尾数

其中:

  • 符号位:取 0 表示正数,取 1 表示负数。
  • 阶码:阶码用移码表示,阶码的真值都被加上一个常数(偏移量),单精度偏移量为 127 (0x7f) 。
  • 尾数:尾数长度在图示中是 23 位,但实际上却是 24 位,还有一个位是“不可见”的,其值固定为 1,也就是说 IEEE 754 标准所定义的浮点数,其有效数字是介于 1 与 2 之间的小数。规格化尾数时注意小数范围为 1 到 2.

下面以数字 0.75 为例:

十进制 0.75 转换为二进制 -- 0.11
二进制 0.11 规格化-------- 1.1*2^-1
计算阶码----------------- -1+127=126
符号位 指数部分  尾数部分
0 ----01111110 -10000000000000000000000
以单精度浮点格式存储该数 0011 1111 0100 0000 0000 0000 0000 0000

一个十进制数能否用二进制浮点数精确表示,关键在于小数部分。我们来看一个最简单的小数 0.1,它会得到一个无限循环的二进制小数 0.000110011...,用有限位无法表示无限小数,因此无法用IEEE 754 浮点数精确表示。

IEEE 754 标准所定义的单精度浮点数所表示的数的范围大约为 -2^128 ~ 2^128 (-10^38 ~ 10^38 ),因为尾数的最大值是接近 2,而指数的范围是 [-127, 127],那么这个范围就可以表示为2*2^127 。单精度浮点数的精度为小数点后面 5~6(2^23 =8388608)个十进制位。

float b = 54.00001;
float c = 54.000001;
cout << setprecision(10) << b << endl; // 54.00001144
cout << setprecision(10) << c << endl; // 54

double 精度丢失

placement new

placement new 是operator new的一个重载版本,只是我们很少用到它。用它可以在已经分配好的内存中(栈或堆中)构建新的对象,这个构建过程不需要额外分配内存,只需要调用对象的构造函数即可。

它的原型如下:

void *operator new( size_t, void *p ) throw(){
    return p;
}

void *p 实际上就是指向一个已经分配好的内存缓冲区的的首地址。

举例来说:

class foo{};
foo* pfoo = new foo;

pfoo指向的对象的地址你是不能决定的,因为new已经为你做了这些工作。第一步分配内存,第二步调用类的构造函数。而placement new是怎么做的呢,说白了就是把原本new做的两步工作分开来。第一步你自己分配内存,第二步你调用类的构造函数在自己分配的内存上构建新的对象。placement new的好处:

  1. 在已分配好的内存上进行对象的构建,构建速度快。
  2. 已分配好的内存可以反复利用,有效的避免内存碎片问题。

在很多情况下,placement new的使用方法和其他普通的new有所不同。

1. 缓存提前分配

为了保证通过placement new使用的缓存区的memory alignmen(内存队列)正确准备,使用普通的new来分配它:

class Task ;
char * buff = new [sizeof(Task)]; //分配内存

2. 对象的分配

在刚才已分配的缓存区调用 placement new 来构造一个对象。

Task *ptask = new(buff) Task;

3. 使用

按照普通方式使用分配的对象:

ptask->suspend();
ptask->resume();

4. 对象的销毁

一旦你使用完这个对象,你必须调用它的析构函数来销毁它。按照下面的方式调用析构函数:

ptask->~Task(); //调用外在的析构函数

5. 释放

可以反复利用缓存并给它分配一个新的对象(重复步骤2,3,4)。如果不打算再次使用这个缓存,你可以象这样释放它:

delete [] buff;

跳过任何步骤就可能导致运行时间的崩溃,内存泄露,以及其它的意想不到的情况。

6. 建立对象数组

placement new 还可以用来建立带参数的构造函数对象数组。

#include <iostream>
#include <new>
using namespace std;

class CPong
{
public:
    CPong(int m) : v(m) {
        cout << "CPong constructor" << v << endl;
        cout << this << endl;
    }

private:
    int v;
};

int main(){
    const int arr_size = 3;
    char* pong = new char[sizeof(CPong) * arr_size];
    CPong* pp = (CPong*)pong;

    for (int i=0; i<arr_size; ++i )
    {
        new (pp+i)CPong(i); // 对象构造函数
    }

    for (int i=0; i<arr_size; ++i )
    {
        new (pp+i)CPong(i+arr_size);
    }

    for (int j=0; j<arr_size; ++j )
    {
        pp[j].~CPong();
    }

    delete [] pong;
    return 0;
}

更多阅读

main函数执行前、后再执行的代码
IEEE 754 浮点数的表示精度探讨
IEEE 754浮点数在机器中的格式
一个浮点数跨平台产生的问题
float and double
Placement new的用法及用途
遵循placement new的用法规范