C/C++ 使用 memoization 优化算法

memoization 是一种缓存计算结果,避免重复计算,用空间换时间的优化方式。

以常见的斐波那契数列计算为例:

#include <stdio.h>

#define COUNT_TIMES 10

int fib(int n)
{
    if (n == 0 || n == 1)
    {
        return 1;
    }
    else
    {
        return fib(n - 2) + fib(n - 1);
    }
}

int main()
{
    int i;
    for (i = 0; i < COUNT_TIMES; i++)
        printf("fib %dn", fib(i));
}

输出:

fib 1
fib 1
fib 2
fib 3
fib 5
fib 8
fib 13
fib 21
fib 34
fib 55

实际上,我们来看看其中的计算次数

#include <stdio.h>

#define COUNT_TIMES 10

int count;

int fib(int n)
{
    if (n == 0 || n == 1)
    {
        return 1;
    }
    else
    {
        count++;
        return fib(n - 2) + fib(n - 1);
    }
}

int main()
{
    int i, *mem;

    for (i = 0; i < COUNT_TIMES; i++)
    {
        printf("n %d 结果 %2d 计算次数 %dn", i, fib(i), count);
        count = 0;
    }
}

结果:

n 0 结果  1 计算次数 0
n 1 结果  1 计算次数 0
n 2 结果  2 计算次数 1
n 3 结果  3 计算次数 2
n 4 结果  5 计算次数 4
n 5 结果  8 计算次数 7
n 6 结果 13 计算次数 12
n 7 结果 21 计算次数 20
n 8 结果 34 计算次数 33
n 9 结果 55 计算次数 54

我们发现实际上计算的次数跟其结果相当,计算 n 的斐波那契数列其计算量就是 fib(n) – 1 次了。想想也是醉了。

那么让我们使用 memoization 来优化一下:

#include <stdio.h>
#include <stdlib.h>

#define COUNT_TIMES 10

int count;

int fib(int n, int *mem)
{
    // 如果没有缓存结果则进行计算,并把结果加入到缓存中
    if (mem[n] == -1)
    {
        mem[n] = fib(n - 1, mem) + fib(n - 2, mem);
        // 统计计算次数
        count++;
    }
    // 返回缓存结果
    return mem[n];
}

int main()
{
    int i, j, *mem;
    for (i = 0; i < COUNT_TIMES; i++)
    {
        // 申请一块内存来缓存结果
        mem = (int *)malloc(sizeof(int) * COUNT_TIMES);
        // 初始化其中的结果
        mem[0] = mem[1] = 1;
        for (j = 2; j < COUNT_TIMES; j++)
            mem[j] = -1;

        // 调用计算
        printf("n %d 结果 %2d 计算次数 %dn", i, fib(i, mem), count);

        count = 0; // 计算次数清零
        free(mem); // 释放用来缓存的内存
    }
}

优化之后,可以发现时间复杂度很轻松的变成 O(n) 了

n 0 结果  1 计算次数 0
n 1 结果  1 计算次数 0
n 2 结果  2 计算次数 1
n 3 结果  3 计算次数 2
n 4 结果  5 计算次数 3
n 5 结果  8 计算次数 4
n 6 结果 13 计算次数 5
n 7 结果 21 计算次数 6
n 8 结果 34 计算次数 7
n 9 结果 55 计算次数 8

优化之后,当 n = 15,速度大概是原版的1000倍,当 n = 27 速度大概是原来的 10000 倍了。应该说重复计算的计算量越大使用 memoization 获得的效果就越明显,不过实际应用中要考虑到其所消耗的内存是否值得,也就是看一个性价比吧。

最后去掉注释简单封装一下。

#include <stdio.h>
#include <stdlib.h>

#define COUNT_TIMES 10

int * init_mem() {
    int i, *mem;
    mem = (int *)malloc(sizeof(int) * COUNT_TIMES);
    mem[0] = mem[1] = 1;
    for (i = 2; i < COUNT_TIMES; i++)
        mem[i] = -1;
    return mem;
}

int fib(int n, int *mem)
{
    if (mem[n] == -1)
        mem[n] = fib(n - 1, mem) + fib(n - 2, mem);
    return mem[n];
}

int main()
{
    int i, *mem;

    for (i = 0; i < COUNT_TIMES; i++)
    {
        mem = init_mem();
        printf("fib %dn", fib(i, mem));
        free(mem);
    }
}

Advertisements

Super fast blur 模糊算法

好久没整理GDI 教程了,这东西本来准备跟教程一起发的,但是要直接讲还接不上目前的教程,所以在硬盘里面堆了好久。
今天突然想起来,为了避免忘记先发上来。废话不多说,有图有真相:

superfast_blur算法

源代码:

#include <windows.h>

// 用于注册的窗口类名
const char g_szClassName[] = "myWindowClass";

#define WINDOW_WIDTH 400
#define WINDOW_HEIGHT 300

#define MAX(x,y) (x>y?x:y)
#define MIN(x,y) (x>y?y:x)

/*
 * Super Fast Blur v1.1
 * Original author: Mario Klingemann (C++ version)
 * Original address: http://incubator.quasimondo.com/processing/superfastblur.pde
 * C version updated by Lellansin (http://www.lellansin.com)
 */
void superFastBlur(unsigned char *pix, int w, int h, int radius)
{
    int div;
    int wm, hm, wh;
    int *vMIN, *vMAX;
    unsigned char *r, *g, *b, *dv;
    int rsum, gsum, bsum, x, y, i, p, p1, p2, yp, yi, yw;

    if (radius < 1) return;

    wm = w - 1;
    hm = h - 1;
    wh = w * h;
    div = radius + radius + 1;
    vMIN = (int *)malloc(sizeof(int) * max(w, h));
    vMAX = (int *)malloc(sizeof(int) * max(w, h));
    r = (unsigned char *)malloc(sizeof(unsigned char) * wh);
    g = (unsigned char *)malloc(sizeof(unsigned char) * wh);
    b = (unsigned char *)malloc(sizeof(unsigned char) * wh);
    dv = (unsigned char *)malloc(sizeof(unsigned char) * 256 * div);

    for (i = 0; i < 256 * div; i++)
        dv[i] = (i / div);

    yw = yi = 0;

    for (y = 0; y < h; y++)
    {
        rsum = gsum = bsum = 0;
        for (i = -radius; i <= radius; i++)
        {
            p = (yi + min(wm, max(i, 0))) * 3;
            bsum += pix[p];
            gsum += pix[p + 1];
            rsum += pix[p + 2];
        }
        for (x = 0; x < w; x++)
        {
            r[yi] = dv[rsum];
            g[yi] = dv[gsum];
            b[yi] = dv[bsum];

            if (y == 0)
            {
                vMIN[x] = min(x + radius + 1, wm);
                vMAX[x] = max(x - radius, 0);
            }
            p1 = (yw + vMIN[x]) * 3;
            p2 = (yw + vMAX[x]) * 3;

            bsum += pix[p1] - pix[p2];
            gsum += pix[p1 + 1] - pix[p2 + 1];
            rsum += pix[p1 + 2] - pix[p2 + 2];

            yi++;
        }
        yw += w;
    }

    for (x = 0; x < w; x++)
    {
        rsum = gsum = bsum = 0;
        yp = -radius * w;
        for (i = -radius; i <= radius; i++)
        {
            yi = max(0, yp) + x;
            rsum += r[yi];
            gsum += g[yi];
            bsum += b[yi];
            yp += w;
        }
        yi = x;
        for (y = 0; y < h; y++)
        {
            pix[yi * 3] = dv[bsum];
            pix[yi * 3 + 1] = dv[gsum];
            pix[yi * 3 + 2] = dv[rsum];

            if (x == 0)
            {
                vMIN[y] = min(y + radius + 1, hm) * w;
                vMAX[y] = max(y - radius, 0) * w;
            }
            p1 = x + vMIN[y];
            p2 = x + vMAX[y];

            rsum += r[p1] - r[p2];
            gsum += g[p1] - g[p2];
            bsum += b[p1] - b[p2];

            yi += w;
        }
    }

    free(r);
    free(g);
    free(b);

    free(vMIN);
    free(vMAX);
    free(dv);
}

VOID BmpBlur(HDC hSrcDC, int x, int y, int iWidth, int iHeight )
{
    int i;
    int iPixel = 24;
    int iBytesPerPixel = iPixel / 8;
    int iBytesScanLine;

    HDC hMemDC;
    HBITMAP hBitmap, hBitmapOld;

    BYTE *pBmpBitsStart;
    BYTE *pBmpBitsCurrent;
    BYTE *newBitmap;
    BYTE *pNewBitsCurrent;

    //图形格式参数
    LPBITMAPINFO lpbmih = (LPBITMAPINFO)malloc(sizeof(BITMAPINFO));

    lpbmih->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    lpbmih->bmiHeader.biWidth = iWidth;
    lpbmih->bmiHeader.biHeight = iHeight;
    lpbmih->bmiHeader.biPlanes = 1;
    lpbmih->bmiHeader.biBitCount = iPixel;
    lpbmih->bmiHeader.biCompression = BI_RGB;
    lpbmih->bmiHeader.biSizeImage = 0;
    lpbmih->bmiHeader.biXPelsPerMeter = 0;
    lpbmih->bmiHeader.biYPelsPerMeter = 0;
    lpbmih->bmiHeader.biClrUsed = 0;
    lpbmih->bmiHeader.biClrImportant = 0;

    // 创建一个 缓存DC
    hMemDC = CreateCompatibleDC(hSrcDC);
    // 创建一个与设备无关的位图对象
    hBitmap = CreateDIBSection(hMemDC, lpbmih, DIB_RGB_COLORS, (void **)&pBmpBitsStart, NULL, 0);
    // hOld
    hBitmapOld = (HBITMAP)SelectObject(hMemDC, hBitmap);

    // 将要模糊的图片拷贝到缓存DC上
    BitBlt( hMemDC, 0, 0, iWidth, iHeight, hSrcDC, x, y, SRCCOPY);

    // 当前位指针指向缓存图片
    pBmpBitsCurrent = pBmpBitsStart;
    // 计算一行有多少字节
    iBytesScanLine = ((iBytesPerPixel * iWidth + 3) >> 2) << 2;

    // 新申请一块内存
    newBitmap = (BYTE *)malloc(sizeof(BYTE) * (iWidth * iHeight * 3));
    // current 指针指向这块内存
    pNewBitsCurrent = newBitmap;

    // 将图片拷贝到这块内存上
    for (i = 0; i < iHeight; ++i)
    {
        // 从图片(pBitsCurrent)拷贝到新内存(current), 多少字节每像素 * 宽度
        memcpy(pNewBitsCurrent , pBmpBitsCurrent , iBytesPerPixel * iWidth);
        pNewBitsCurrent += iBytesPerPixel * iWidth;
        pBmpBitsCurrent += iBytesScanLine;
    }

    // 调用模糊算法来修改这块内存上的数据(图片)
    superFastBlur(newBitmap , iWidth , iHeight , 5);

    // 当前位指针指向缓存图片
    pBmpBitsCurrent = pBmpBitsStart;
    // current 指针指向存有被模糊图片的内存
    pNewBitsCurrent = newBitmap;

    // 将被模糊的数据拷贝回图片缓存
    for (i = 0; i < iHeight; ++i)
    {
        // 每一行只拷贝一半
        memcpy(pBmpBitsCurrent , pNewBitsCurrent , iBytesPerPixel * iWidth / 2);
        pNewBitsCurrent += iBytesPerPixel * iWidth;
        pBmpBitsCurrent += iBytesScanLine;
    }

    SelectObject(hMemDC , hBitmapOld);
    SelectObject(hSrcDC, hBitmap);

    DeleteObject(hBitmap);
    DeleteObject(hBitmapOld);
    DeleteObject(hMemDC);
    
    free(newBitmap);
    free(lpbmih);
}

void Paint(HWND hwnd)
{
    PAINTSTRUCT ps;
    HDC hdc;
    HDC mdc;
    HBITMAP hbmp; // 位图绘制对象句柄

    hdc = BeginPaint(hwnd, &ps);

    // 加载图片到缓存 DC
    mdc = CreateCompatibleDC(hdc);
    hbmp = (HBITMAP)LoadImage(NULL, "D:\bg.bmp", IMAGE_BITMAP, WINDOW_WIDTH, WINDOW_HEIGHT, LR_LOADFROMFILE);
    SelectObject(mdc, hbmp);

    // 将当前缓存DC 中的图片模糊
    BmpBlur(mdc, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);

    // 将缓存DC中的位图复制到窗口DC上
    BitBlt(hdc, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, mdc, 0, 0, SRCCOPY);

    DeleteObject(hbmp);
    DeleteDC(mdc);
    EndPaint(hwnd, &ps);
}

/*
 * 第四步,窗口过程
 */
LRESULT CALLBACK MyWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    // 窗口绘制消息
    case WM_PAINT:
        Paint(hwnd); // 调用我们的 GDI 绘制函数
        break;
    // 窗口关闭消息
    case WM_CLOSE:
        DestroyWindow(hwnd);
        break;
    // 窗口销毁消息
    case WM_DESTROY:
        PostQuitMessage(0); // 发送离开消息给系统
        break;
    // 其他消息
    default:
        // pass 给系统,咱不管
        return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

/*
 * 第一步,注册窗口类
 */
void RegisterMyWindow(HINSTANCE hInstance)
{
    WNDCLASSEX wc;

    // 1)配置窗口属性
    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.style         = 0;
    wc.lpfnWndProc   = MyWindowProc; // 设置第四步的窗口过程回调函数
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInstance;
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = g_szClassName;
    wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);

    // 2)注册
    if (!RegisterClassEx(&wc))
    {
        MessageBox(NULL, "窗口注册失败!", "错误", MB_ICONEXCLAMATION | MB_OK);
        exit(0); // 进程退出
    }
}

/*
 * 第二步,创建窗口
 */
HWND CreateMyWindow(HINSTANCE hInstance, int nCmdShow)
{
    HWND hwnd;
    hwnd = CreateWindowEx(
               WS_EX_CLIENTEDGE,
               g_szClassName,
               "Superfast Blur 算法",
               WS_OVERLAPPEDWINDOW,
               CW_USEDEFAULT, CW_USEDEFAULT, 400, 300, // 出现坐标 x,y 默认分配 窗口宽 400 高 300
               NULL, NULL, hInstance, NULL);

    if (hwnd == NULL)
    {
        MessageBox(NULL, "窗口创建失败!", "错误", MB_ICONEXCLAMATION | MB_OK);
        exit(0); // 进程退出
    }

    // 显示窗口
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    return hwnd;
}

/*
 * 主函数
 */
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{
    HWND hwnd;
    MSG Msg;

    // 第一步:注册窗口类
    RegisterMyWindow(hInstance);
    // 第二步:创建窗口
    hwnd =  CreateMyWindow(hInstance, nCmdShow);
    // 第三步:消息循环
    while (GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;
}

C语言 零长度数组的应用

注:本文大部分源引: What is the advantage of using zero length arrays in C 。但是并非逐句翻译,而是加上了很多自己的理解,除了其中的内容之外,还有加上关于国内一些说法的澄清,以及 windows 方面的例子,并补充了一些运用。转载请标明出处 @lellansin

零长度(zero length)的数组这听起来有点让人疑惑。笔者也百度过不少目前国内关于这个概念的博文,不过发现大都只是知道这个概念,却不知道它的应用。那么这里就让笔者当个搬运工给大家科普一下。

首先请大家看看下面的例子。你想要一个结构体来描述 email 数据:

struct email 
{
    int send_time;
    int flags;
    int length;
    char body[EMAIL_BODY_MAX];
};

为了方便讨论,上述的例子中已经省略了很多字段。请大家不要在意,我们需要在意的是,这个结构体中有一个很常见的问题等待我们解决:

即使你的邮件只有一两个字,依旧不得不消耗 EMAIL_BODY_MAX 个字节的内存。不幸的是,email 的长度总是有着很大的不确定性:有的很大,而有的可能只有几个字。我们不仅常常浪费 EMAIL_BODY_MAX 个字节中的大部分,而且为了保险总是会把 EMAIL_BODY_MAX 设置的很大。更糟糕的是,目前并没有明确的标准来限定 email 内容的长度,也就是说如果你的应用发送不了这么长的邮件,但是别人可以的话,将是一个很难堪的情况。所以,我们很希望停止这个恶性循环。

而零长度的数组则正好可以解决这个问题:

struct email 
{
    int send_time;
    int flags;
    int length;
    char body[];
};

如果你在普通情况下使用这个结构体,你将会发现 body 这个成员并不占用任何内存:

struct email *email = malloc( sizeof(struct email) );

这里的 body 就是零长度的。 你不能合法的访问 body 所在的内存。

实际上在 32位系统上这个结构体只占用了 12个字节的内存,这些都是由前三个 int 成员占用的。就这个现象而言,也看到不少国人讨论,那么让笔者在这里用一个例子总结一下他们的讨论结果:

#include <stdio.h>

struct email
{
    int send_time;
    int flags;
    int length;
    char body[];
};

int main()
{
    struct email e;
    printf("%d n", sizeof(e));
    printf("%p n", &e);
    printf("%p n", &(e.body) );
    printf("%p n", ((int)&e + sizeof(e)) );
    printf("%d n", ((int)&e + sizeof(e)) == ((int)&e.body) );
}

结果是发现,零长度数组 body 不占用结构体的大小,且其值指向结构体的末尾。目前一些相关文章中,有说这种写法只在 GNU C 中才有,实际上只是 char body[0] 这种直接写 0 的写法不兼容,省略不写的零长度数组定义方式是兼容的。

不过要利用零长度数组,不应该用普通的方式来获得内存,而应该这样做:

struct email *email = malloc (sizeof (struct email) + email_length);
email->length = email_length;

由于零长度数组刚好指向结构体的末尾,也就正好对应多申请的内存。所以当你的邮件内容为 16 字节长度的时候,body 的长度也只有 16字节,而整个结构体的长度则是 28 字节。

通过这个方式,零长度数组与普通数组同样拥有足够有效的内存,区别只是普通数组因为类型可以通过 sizeof 获取其长度,而零长度数组则不能。

精明的读者现在会问,为什么不用直接在结构体中使用指针存储这个长度不固定的 body?如果你觉得你的情况中使用指针就可以解决问题,那么也非常推荐你直接用指针。

实际上,零长度数组只在特定清下非常有用:当你有一个很大的结构体,并且该结构体中包含如上述 body 的动态长度数组,同时你需要把这个结构体的数据共享给其他的进程或者发送给其他的计算机。例如,在 linux 内核提供的监控文件 inotify 接口中就使用了零长度数组:

struct inotify_event {  
    int      wd;       /* Watch descriptor */  
    uint32_t mask;     /* Mask of events */  
    uint32_t cookie;   /* Unique cookie associating related 
                          events (for rename(2)) */  
    uint32_t len;      /* Size of ’name’ field */  
    char     name[];   /* Optional null-terminated name */  
};  

该结构体代表一个 inotify 事件,事件类型例如 写入 目标文件名比如 /home/rlove/wolf.txt,你会发现这样一个问题————我们需要多大的字符数组来存储这个文件名?要知道,文件名可能是任意长度的,不同文件系统的最大文件名长度各异(PATH_MAX不是标准长度,只是个偷懒的常量)。现在,如果只是返回文件名,我们可以只是简单的返回一个动态申请的地址 char * 。但是我们必须返回的是一个巨大的结构体。而且,这里是在做系统调用,所以结构体中如果存储的是 char * 地址,传过去的时候其所指向的值不会再结构体中展开,而且传过去之后这个地址就会失效(系统调用的内存空间与普通进程的用户空间不同,属于内核空间)。所以在这些限制条件下,使用零长度数组是最好的解决方案。

windows 程序员可能会问, 这是否是个只在 linux 平台是个常见的技巧。当然不是,windows 上也是可以运用的。比如当你在使用 socket 或者管道的时候,零长度数组也能起到一个不错的作用:

// 使用零长度数组定义一个消息结构体
typedef struct
{
    int length;
    char content[];
} MyMsg;

// socket 发送结构体数据
msg = (MyMsg *)malloc(sizeof(MyMsg) + 12);
msg->length = 12;
strcpy(msg->content, "hello world");
send(sockClient, (char *)msg, (sizeof(MyMsg) + 12), 0);

// socket 接收数据
recv(sockClient, recvBuf, 100, 0);
recvMsg = (MyMsg *)recvBuf;
printf("length: %d, content:%s n", recvMsg->length, recvMsg->content);

与在结构体中使用指针相比较,这种方式可以让你的结构体在传输的时候更加方便。有些人可能会说,在传输的过程中直接发送结构体可以说是明文发送数据,这是很不安全的。但是笔者想说的是,如果你使用的是对一块数据进行加密的通用方法(而不是针对某一类结构体写的加密方法),那么使用零长度数组也是很方便的。

而且,值得注意的是,由于这种运用方式中,其内存是通过 malloc 申请的一段连续内存,所以 free 函数可以一次性对其进行释放,而不必像指针那样分别释放。

所以,笔者这里想说,零长度数组在普通的情况下也不是一种鸡肋的写法,这里附上例子各位可以比较一下:

非常基础的存储学生信息:

// 学生 结构体
typedef struct student
{
    int id;
    char name[100];
} student;

使用零长度数组来写一个班级结构体的话可以这样:

// 班级 结构体
typedef struct class 
{
    char *teacher;
    int class_id;
    student students[];
} class;

// 使用班级
class *classA;
classA = (class *) malloc (sizeof (class) + sizeof (student) * number_of_students);

// 遍历学生
for(int i=0; i < number_of_students; i++)
    class->students[i].id = i;

// 释放资源
free(classA);

而, 这个例子如果使用指针来写的话则会变成:

typedef struct class 
{
    char *teacher;
    int class_id;
    student *students;
} class;

class *classA;

classA = (class *) malloc (sizeof (class));
classA->students = (student *) malloc(sizeof (student) * number_of_students );

// 遍历 ...

// 释放
free(classA->students);
free(classA);

不知道各位有没有感同身受, 至少笔者是能感觉到, 使用指针没有用零长度数组那么优雅, 申请和释放都多一步。

最后提一点,请合理利用零长度数组,出于可读性和可维护性考虑,在波动数值较小的范围内还是建议使用指定长度的数组。另外,使用包含零长度数组的结构体作为另一个结构体的成员是很不推荐的。

c/c++ 按照自定义脚本递归处理所有文件

今天突然说项目要申请软助, 然后要把准备几千行代码出来. 想着一行一行拷贝很麻烦, 不过专门写一个把文件合并的程序又很鸡肋, 所以就写了一个用来递归处理文件的程序.

recur

效果如上图: 参数1 是递归遍历的目录, 参数2 是执行的命令(%s 每一个单独的文件名), 参数3 是过滤的文件类型
然后整理几千行代码就可以简单的用:

recur.exe E:workspace "type %s" "c,cpp" > code.txt

如果还有其他特别的需求要处理多个文件, 就可以专心写处理一个文件的脚本或者程序然后调用. (程序 > code.txt 就是把程序的输出导入到 code.txt 文件中)

recur.exe E:workspace "你的程序.exe %s" "类型1,类型2"

来一次处理多个.

实现代码

#include <stdio.h>
#include <windows.h>

void help();

int checkSuffix(char *name, char *suffix)
{
    char *p = suffix;
    char *ext = strrchr(name, '.');
    int len, end;

    while (*p)
    {
        end = *(p + 1) == '\0';
        if (*p == ',' || end)
        {
            len = p - suffix;

            if (end)
                len++;

            if (strncmp(ext + 1, suffix, len) == 0)
                return 1;

            suffix = p + 1;
        }
        p++;
    }

    return 0;
}

int isDirectory(WIN32_FIND_DATA file)
{
    return file.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY;
}

char *getFilepath(char *dest, char *dir, char *name)
{
    strcpy(dest, dir);
    strcat(dest, "\");
    strcat(dest, name);
    return dest;
}

void recur(char *src, char *command, char *suffix)
{
    char *name;
    char path[MAX_PATH];
    char dir[MAX_PATH];
    char buf[MAX_PATH];
    WIN32_FIND_DATA file;
    HANDLE hDir;

    getFilepath(dir, src, "*.*");
    hDir = FindFirstFile(dir, &file);

    if (hDir == INVALID_HANDLE_VALUE)
        return;

    while (FindNextFile(hDir, &file))
    {
        name = file.cFileName;
        if (isDirectory(file))
        {
            if (name[0] != '.')
            {
                getFilepath(path, src, name);
                recur(path, command, suffix);
            }
        }
        else if (checkSuffix(name, suffix))
        {
            getFilepath(path, src, name);
            wsprintf(buf, command, path);
            system(buf);
        }
    }
}

int main(int argc, char *argv[])
{
    if (argc != 4)
    {
        help();
        return 0;
    }

    recur(argv[1], argv[2], argv[3]);

    printf("nRecur overn");
    return 0;
}

void help()
{
    printf("Usage:  recur <dir> <cmd> <suffix>n" 
           "        recur E:\test "echo %%s" c,cpp n"
           "   dir  Directorys that you deal with.n" 
           "        It can be like E:\test E:\test2 n" 
           "   cmd  command you want run for example:n" 
           "        1) type %%s n"
           "        2) echo %%s n"
           "        3) your.bat %%s n"
           " suffix filter suffix, for example:n"
           "        c,cpp n"
           );
}

计算机中的进制转换

计算机中的进制转换

  1. 计算机数据存储单位
  2. 进制转换
    1. 常见的进制转换
    2. 二进制简介
    3. 进制计算的基本概念
      1. 七进制计算
      2. 六十进制计算
      3. 十进制计算
      4. 进制计算的规律与公式推导
    4. 二进制与十进制的转换
      1. 二进制转十进制
      2. 十进制转二进制
        1. 六十进制中
        2. 十进制中
        3. 除二取余法原理
    5. 二进制与十六进制的转换

计算机数据存储单位

1TB (Trillionbyte 万亿字节 太字节 )   = 1024 GB
1GB (  Gigabyte   吉字节 又称“千兆” ) = 1024 MB
1MB (  Megabyte   兆字节 简称“兆” )   = 1024 KB
1KB (  Kilobyte   千字节 )            = 1024 B
1B  (    Byte     字节 )              =   8  Bit

各位需要注意,内存上最小的单位是字节(byte)没有比这更小的单元了。而我们讨论的位(bit)是比字节还要小的单位,这个单位在计算机上是无法直接查看的(因为字节已经是最小的了),但是实际上而言计算机真正的所有数据都是有一个位一个位的1010之类的二进制数据组成的。而我们要进行C/C++编程的话,很多时候都需要去处理这个比内存最小单位还要小的位,如果是专做底层的话使用位操作就更加频繁了,所以我们必须要了【位级】也就是【二进制层面】的数据形式。

进制转换

常见的进制转换

生活中常见的进制:

进制 用途 进位
七 进制 星期 满 七 进一
十 进制 普通数字 满 十 进一
十二 进制 月份 满 十二 进一
二十四 进制 小时 满 二十四 进一
六十 进制 时分秒 满 六十 进一

我们的生活中充斥着各种各样的进制转换,例如每天工作生活的时候,可能很多人都不知道今天几号,但是却一定知道今天星期几,这个星期的概念中就存在着进制转换。满了七天就可以进一位变成一个星期,这是一个很常见的进制转换。

上述还列举了好几种其他的进制转换,我们可以很清晰的发现,其实不同的进制在【进位】的时候很明显的体现出一个规律便是:几进制就满几进一。

二进制简介

那么实际上二进制跟上述常见的进制都是类似的,也就很简单的是满 2 进一位,我们可以看如下的式子:

1 + 1 = 2

这在我们十进制中是一个幼儿园的小孩子都懂的加法,但是到了二进制中这个结果会变得不一样了:

1 + 1 = 10

乍一看会觉得很难接受,但是仔细一想这也是可以理解的,因为在二进制中 满2 就会进一位,所以 1+1=2 已经满2了就进了一位结果就是 10 了。不过各位需要注意的是这个 10 是二进制中的 10 :

10(二进制) = 2(十进制)

那么我们的二进制,再加几个数字看看:

10 + 1 =  11 (等于十进制中的3)
11 + 1 = 100 (等于十进制中的4)

情况就是这样,我们可以用这样的方式依次类推下来:

二进制   十进制
    1       1
   10       2
   11       3
  100       4
  101       5
  110       6
  111       7
 1000       8
 1001       9
 1010      10
 1011      11
 1100      12
 1101      13
 1110      14
 1111      15
10000      16

有些人可能会觉得奇怪,为了为什么这个看起来没有开始七进制那个星期的那样好理解,这里博主解释一下。

真正数学意义上的七进制里面是没有7的,因为到了6之后再加一 满 7 就要进一位变成 10 ,这个形式我们不是很熟悉但是换个熟悉的方式就理解了,因为我们平常生活着中把这个七进制的【个位】叫做【天】,【十位】叫做【星期】所以我们的七进制的:

10 天

也可以看做

1星期 0天

进制计算的基本概念

首先我们要了解的是进制本身的一个概念:

七进制计算

以星期的七进制举例:

21

这个值代表的天数到底是多少?我们将其换成我们所熟悉的单位一看:

2 星期 1 天

这样很直观的就能算出来 这个七进制的 21 所代表的的是十进制的 15 天,为什么呢?

很简单,因为一个星期是7天,所以上述的七进制数字转成十进制就是:

7*2 + 1 = 15

六十进制计算

以时分秒的六十进制举例:

356

那么这个六十进制的 356 秒到底是十进制的多少秒?乍一看又有点晕了,不过没关系,换成我们所熟悉的单位来看看:

3小时 5分钟 6秒

这样看一下一下子就觉得简单了,我们也可以开始算了,答案就是:

3*3600 + 5*60 + 6 = 11106(秒)

十进制计算

接下来我们再用十进制来举例:

十进制的:

1234

我想这个大家应该一眼就能看出来这个十进制的值,那么我们要看的是这个十进制的每一位拆开来看的式子:

1*1000 + 2*100 + 3*10 + 4

进制计算的规律与公式推导

相信经过上面三种进制的举例之后,大家的心里已经模糊的有点想法,接下来我们就要把这个模糊的概念变的清晰。

首先我们定义一个未知数x(x>=2),那么假设我们所使用的这个进制是 x进制,那么这个 x进制有什么特点?

不用多说:

① 满x进一

相信大家可以理解

那么对于这个 x进制 的个位,单位的话一定 1,因为满了 x 就会进一位,所以由 ① 式可得:

② 个位的最大值是 x-1

那么各位同学想想,由 ①、② 式可以知道什么?那么博主也不卖关子:

③ 第二位的 1 是第一位的 x 倍

那么接下来的问题就是:

x进制的 abcd (abdc 分别是0到x-1的数字) 等于 10进制的多少?

大家可以停在这里先仔细考虑这个问题,当然草稿纸也是必要的,因为经过思考之后再来看博主的推理绝对会效果更佳!

wwwwwwwwwwwwwwwwwwwwwwwwwwwwwww 博主的时间分割线 233333333333333333333333333333~

那么,如果你已经思考过了我们就继续往下看。

由 第①、③式 就可以知道我们想要的结论了:

关于x进制的 abcd:

个位数就是  d
十位数是   x*c
百位数是  x*x*b
千位数是 x*x*x*a

所以最后答案是:

④ 1111(x进制)= a*x^3 + b*x^2 + c*x^1 + d*x^0 (10进制)

备注:

1. x^3 的意思就是 x的3次方 即 x*x*x

2. x^0 的意思是 x的0次方 即 1(除0以外,任意数的0次方都是1)

那么这个式子 ④ 是一个通用形的公式,可以计算任意进制转十进制。各位可以摊开草稿纸自行推导,以及套用求,博主这里就不废话了。

 

二进制与十进制的转换

二进制转十进制

既然有了公式,那么二进制转十进制自然是可以套用通用公式来计算的:

例如 0100 1011 的十进制计算:

0100 1011(二进制) = 1*2^6 + 1*2^3 + 1*2^1 + 1*2^0 = 75 (十进制)

或者使用 1248 法,那么什么是 1248 法,额,博主在写的时候才发现其实这个就是公式一种运用方式吧。就是记住2
的各个次方,依次从0次方开始分别是:1、2、4、8、16、32、64…… 等等

然后计算的时候碰到如下情况,直接套用:

如 0001 0101 的十进制值计算:

0001 0101(二进制) = 16 + 4 + 1 = 21

二进制的话,0就是没有值不用乘,1的话乘的时候可以省略,所以只要记住一些常见的 2 的次方就可以快速的将二进制转成十进制了。博主在视频当中二进制转十进制的时候就是这么做的 =v=

十进制转二进制

使用【除二取余法】,例如:

789(十进制) = 1100010101(二进制)

 789 / 2 = 商 394 余 1  个位
 394 / 2 = 商 197 余 0  十位
 197 / 2 = 商 98  余 1  百位
  98 / 2 = 商 49  余 0  第..位
  49 / 2 = 商 24  余 1  第..位
  24 / 2 = 商 12  余 0  第..位
  12 / 2 = 商 6   余 0  第..位
   6 / 2 = 商 3   余 0  倒数第3位
   3 / 2 = 商 1   余 1  倒数第2位
   1 / 2 = 商 0   余 1  倒数第1位

那么我们的余数倒过来就是我们的二进制的值了。

六十进制中

11106 秒转 六十进制

11106 / 60 = 商 185  余数 6
  185 / 60 = 商 3    余数 5
    3 / 60 = 商 0    余数 3 

所以 11106 = 3小时 5分 6秒

十进制中
1234 / 10 = 商 123 余 4
 123 / 10 = 商 12  余 3
  12 / 10 = 商 1   余 2
   1 / 10 = 商 0   余 1

除二取余法原理

设两个未知数 x, y (x>=2) ,假设我们面对的是 x进制,要把十进制数 y 转成 x 进制

那么我们一定要清楚的是这个 y 去除以 x 能得到什么

假设这个 x进制的数是 abcd (abcd为0~x之间常数) 当然这个x进制的数可能没有4位也可能超过4位,这没有关系我们只是看个示意,那么有了这个示例,根据我们最开始算出来公式可以得出下列等式:

abcd(x进制) = a*x^3 + b*x^2 + c*x^1 + d*x^0 (十进制) = y 

用这个数去除以 x 你会发现:

y / x = ( a*x^3 + b*x^2 + c*x^1 + d*x^0 ) / x (十进制)
      = 商 a*x^2 + b*x^1 + c*x 余数 d

那么获取的这个余数 d 就是我们 abcd 这个 x进制数的【个位】也就是第一位

( a*x^2 + b*x^1 + c*x ) / x = 商 a*x^1 + b*x 余数 c

再用这个商去除以 x 就能获得 x进制数的 第二位。依次类推,只要商不为0,我们就可以通过这个办法一直求下去。

各位可以用摊开草稿纸残念一下,也可以用这个结论反过来看看前几个 二进制、十进制、六十进制 用【除二取余法】的数据,博主就不赘述了,顺便感慨一下,百度百科上面那些进制转换的原理好残念,完全看不懂,不知道写着有什么意思。

 

二进制与十六进制的转换

如果上面的公式你都有参与推倒,并且有使用公式进行过一定的转换联系,那么这个部分的内容对你而言肯定是小case

 二进制   十六进制 十进制
0000 0000    00      0
0000 0001    01      1
0000 0010    02      2
0000 0011    03      3
0000 0100    04      4
0000 0101    05      5
0000 0110    06      6
0000 0111    07      7
0000 1000    08      8
0000 1001    09      9
0000 1010    0A      10
0000 1011    0B      11
0000 1100    0C      12
0000 1101    0D      13
0000 1110    0E      14
0000 1111    0F      15
0001 0000    10      16
0001 0001    11      17
0001 0010    12      18
0001 0011    13      19
0001 0100    14      20
0001 0101    15      21
0001 0110    16      22
0001 0111    17      23
0001 1000    18      24
0001 1001    19      25
0001 1010    1A      26
0001 1011    1B      27
0001 1100    1C      28
0001 1101    1D      29
0001 1110    1E      30
0001 1111    1F      31
0010 0000    20      32

简单的观察一下就会发现上述二进制的低四位对应十六进制的第一位,上述二进制的高四位对应十六进制的第二位。

这其实是一个很简单的事情,因为【一】个【十六进制】的位刚好包含了【四】个【二进制】的位,这是因为十六进制的 x 是 16也就是 2的四次方,而二进制的 x 是 2 即 2的1次方。

我们通过观察上述数据也可以发现二进制的 0~1111 对应的刚好就是 十六进制的 0 ~ F 也就是二进制的四个位刚好对应一个十六进制的位。所以二进制与十六进制的转化十分简单:

二进制   :1111 0001 1010 0101 0111 1000
十六进制 :   F    1    C    5    7    8

查看进程 socket 连接的简单办法

起因是想知道一款游戏连接的服务器地址。虽然可以考虑用抓包工具之类的做到,但是感觉有点麻烦,要说监控的话还是挺难实现的。不过思考之后,发现可以用系统自带的 netstat 命令来做。比如想知道一个游戏连接的服务器地址是多少,可以先找到这个游戏的进程 pid,然后通过:

netstat -ano | findstr [pid]

来过滤查找。

简单写了个 C语言的程序调用这个 cmd 命令:

#include <windows.h>
#include <stdio.h>

void help( );

int main(int argc, char const *argv[])
{
	INT i;
	HWND hWnd;
	CHAR command[256];
	DWORD dwProcessId;

	if( argc == 1)
	{
		help();
		return;
	}

	if (argc == 3)
	{
		if ( strcmp(argv[1], "/p" ) == 0 )
		{
			wsprintf(command, "netstat -ano | findstr %s", argv[2]);
			system(command);
		}

		if ( strcmp(argv[1], "/w" ) == 0 )
		{
			hWnd = FindWindow(NULL, argv[2]);
			if (hWnd <= 0)
			{
				printf("Can't find process by window name %s", argv[2]);
				return 0;
			}
			GetWindowThreadProcessId(hWnd, &dwProcessId);
			if (dwProcessId > (DWORD)0)
			{
				printf("Pid: %d n", dwProcessId);
				wsprintf(command, "netstat -ano | findstr %d", dwProcessId);
				printf("  协议   本地地址              外部地址        状态           PIDn");
				system(command);
			} else {
				printf("Can't find process by window name %s", argv[2]);
			}
		}
	}
	return 0;
}

void help()
{
	printf("getport [options] [param] n");
	printf("/p 通过 pid 查找 n");
	printf("/w 通过窗口名称查找 n");
}

获取程序的 socket 连接

就这样简单的拿到了一个程序连接的 socket 信息(包括 ip 和 端口)。

当然并不是所有程序都能通过这个拿到,有的程序可能是通过子进程来发的 socket。有的程序(比如QQ)会通过服务器中转,所以发送文件的时候只会拿到腾讯中转服务器的 ip(不过早期的QQ确实是可以通过拿到聊天对象的 ip)。

Windows GDI 教程(六) DC与加载位图

为了能够理清楚一些常见的 DC ,这里请让博主在介绍一下几种 DC 的获得方式:

  • BeginPaint 通过窗口 (hwnd) 获得
  • GetDC 通过窗口 (hwnd) 获得
  • CreatCompatibleDC 通过 hdc 获得

关于这三种情况获得的 DC,需要注意的是:从窗口上获得到的DC在释放资源的时候需要调用 ReleaseDC (释放引用,这样其他地方还可以接着用),而通过 CreatCompatibleDC 创建的 DC 我们通常称之为 Memory DC (博主管这叫“缓存DC”),当你不需要它的时候,需要通过 DeleteDC 来释放资源(因为这个DC是你单独申请的,如果你不用了,其他也不会有地方用它,只能直接 Delete)。

BeginPaint 与 GetDC

同样是通过窗口获得其客户端区域(Client area)的可绘制DC,BeginPaint 函数是先使窗口有效再重画,而 GetDC 没有这样,使用 GetDC 时由于窗口始终处于无效状态,不断会产生WM_PAINT消息。

BeginPaint 的表现是,收到 WM_PAINT 时绘制。所以正常情况下,如果没有手动触发 WM_PAINT 那么打开窗口则只绘制一次。

GetDC 的表现是,不断收到 WM_PAINT 消息,绘图区域(DC)不停地刷新。如果绘制过程比较慢的

CreatCompatibleDC

CreatCompatibleDC 函数的书面描述是,用来创建一个与指定设备兼容的内存设备上下文环境(DC)。

这个书面描述感觉有点不好理解,这里博主说一下自己的看法:

关于这个 CreatCompatibleDC 函数首先要了解这其中的单词:Creat、Compatible、DC。其中 Create 是“创建”、Compatible 是兼容的、DC 是博主说的“可画图的内存对象”。那么创建和 DC 都很好理解。这里我们重点要理解这个“兼容的”这个单词。

3个意思:

  1. 该DC的类型与兼容目标相同。虽然博主一再强调关于DC只需要把它当做“可以用来画图的内存就好了”,但是“橘生淮南则为橘,生于淮北则为枳”,从不同的设备中创建出来的DC,互相之间也是有差别的,由此产生的DC的类型包括:显示器、打印机、存储器和数据的索引。
  2. 该DC不需要指定区域、大小。感觉这一条有点凑数的感觉(博主:别打我!)
  3. 该DC与兼容的对象可以方便交流数据。该功能在GDI内部实现。比如使用 BitBlt 函数可以将兼容 DC 上的位图写入到目标 DC中。

加载BMP位图实例

BMP 是 Bitmap 的简写,中文常常称之为 “位图”。

#define WINDOW_WIDTH 400
#define WINDOW_HEIGHT 300

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HDC	mdc;
	HBITMAP hbmp; // 位图绘制对象句柄

	hdc = BeginPaint(hwnd, &ps); 

	// 创建缓存DC (当前窗口DC的兼容DC)
	mdc = CreateCompatibleDC(hdc);

	// 加载 "E:\bg.bmp" 到位图绘制对象 hbmp 中
	hbmp = (HBITMAP)LoadImage(
		NULL,			// 模块实例句柄(要加载的图片在其他DLL中时)
		"E:\bg.bmp",	// 位图路径
		IMAGE_BITMAP,	// 位图类型
		WINDOW_WIDTH,	// 指定图片宽
		WINDOW_HEIGHT,	// 指定图片高
		LR_LOADFROMFILE	// 从路径处加载图片
		);

	// 缓存DC选择位图绘制对象(可以理解为将图片存到mdc中)
	SelectObject(mdc, hbmp);

	// 将缓存DC中的位图复制到窗口DC上
	BitBlt(
		hdc,			// 目的DC
		0,0,			// 目的DC的 x,y 坐标
		WINDOW_WIDTH,	// 要粘贴的图片宽
		WINDOW_HEIGHT,	// 要粘贴的图片高
		mdc,			// 缓存DC
		0,0,			// 缓存DC的 x,y 坐标
		SRCCOPY 		// 粘贴方式
		);

	DeleteObject(hbmp);
	DeleteDC(mdc);
	EndPaint(hwnd, &ps);
}

效果图:
加载BMP图片

如何理解上述代码

对于初学者很容易对上述代码迷惑。常见的几个问题列举:

为什么要用 HBitMap 去 LoadImage,而不是直接用 DC 去 Load?

按照博主的思路来描述一下这个问题吧,画图可以简单的分成三个部分,分别是:
载体(如白纸、油画画板)、工具(如2B铅笔、马克笔)、颜料

对应到GDI程序中,使用画笔可以看成:载体(DC)、工具(画笔 HPEN)、颜料CreatePen 函数中传入)。

那么对应加载位图可以这样看:载体(DC)、工具(?)、颜料LoadImage 中指定图片)

这样很明显可以看到缺少一个工具加载位图的工具。也就是我们的 HBITMAP 对象。

这里拓展一下GDI中可以在DC用来绘制的工具:

对象 通过什么函数创建
Bitmap CreateBitmap, CreateBitmapIndirect, CreateCompatibleBitmap, CreateDIBitmap, CreateDIBSection
Bitmap对象只能被缓存DC(通过 CreatCompatibleDC创建)选中,并且单个bitmap不能同时被多个DC选中。
Brush CreateBrushIndirect, CreateDIBPatternBrush, CreateDIBPatternBrushPt, CreateHatchBrush, CreatePatternBrush, CreateSolidBrush
Font CreateFont, CreateFontIndirect
Pen CreatePen, CreatePenIndirect
Region CombineRgn, CreateEllipticRgn, CreateEllipticRgnIndirect, CreatePolygonRgn, CreateRectRgn, CreateRectRgnIndirect

此列表出处见 SelectObject function on MSDN

为什么要用 LoadImage 到 HBitMap,然后让 mdc 去 select,然后再让 mdc 复制到 hdc?

如果C语言基础学的好的话,那么这个问题也可以用一个很简单的角度去看待:
1.申请缓存空间
2.加载数据到缓存
3.输出数据(将缓存中的数据写入到目标内存)

以基础的C语言代码为例,假设是“输出用户输入的一个整数”,那么C语言代码如下:

// 1.(申请缓存空间) 声明一个4字节的int型变量,用于缓存用户输入
int num;
// 2.(加载数据到缓存) 将用户输入的数字加载到num中
scanf("%d", &num);
// 3.(将缓存中的数据写入到目标内存) 输出 num 到屏幕(将num的值写入到显存)
printf("你输入的是:%d n", num);

对应到加载图片的程序就可以这样理解了。

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HDC	mdc;
	HBITMAP hbmp;

	hdc = BeginPaint(hwnd, &ps); 

	// 1.(申请缓存空间) 创建缓存DC
	mdc = CreateCompatibleDC(hdc);

	// 2.(加载数据到缓存) 通过 Hbitmap 加载数据到 缓存DC
	hbmp = (HBITMAP)LoadImage( ... );
	SelectObject(mdc, hbmp);

	// 3.(将缓存中的数据写入到目标内存) 将缓存DC中的位图复制到窗口DC上
	BitBlt( ... );

	DeleteObject(hbmp);
	DeleteDC(mdc);
	EndPaint(hwnd, &ps);
}

这代码也就是看起来长了一点,没什么大不了的,本质上还是基础的程序逻辑。区别仅仅在于:
1.缓存的空间从 int 换成了 HDC
2.加载数据的方法从使用 scanf 换成了 LoadImageSelectObject
3.输出数据的对象从 int类型的num 写入到显存,换成了 HDC类型的mdc 写入到 hdc;输出数据的方法从 printf 换成了 BitBlt

PS:引用博主一句 装逼 的话来总结一下上面的对比:“程序做并且只在做两件事,一、读数据,二、写数据”

为什么不直接让 hdc 直接去 select 图片的 HBitMap?

1.首先上文有提到过 Bitmap 对象只能被缓存DC (Memory DC,通过 CreateCompatibleDC 创建的)选中。

2.其次,就算情况1不存在,bitmap 可以被 hdc 选中,这在程序的逻辑上也是一件很别扭的事情。依旧是套用 C语言基础的程序来看这个问题:

// 1.不申请缓存空间

// 2.3.直接获取数据并输出
printf("你输入的是:%d n", scanf( ? ) );

或者这样:

// 1.不申请缓存空间

// 2.3.直接获取数据并输出
printf("你输入的是:%s n", gets( ? ) );

基础好的可以马上发现上面两个示意的代码都是没法实现的。当然,如果你依旧觉得这个合理也是可以的,毕竟在其他的语言(比如python)里面这种情况是可以实现的。但是这样写的话,会有如下两种情况:

  1. 不自由。无法修改该数据,只能死板的输出。
  2. 效率低。没有缓存,要输出的时候必须要当时加载,下一次再次用到的时候又要重新加载。

综上。缓存DC是有存在的必要。

修改图片之后再显现的实例

为了照应上文,这里附上一个修改缓存DC中图片然后再显示到窗口的例子。

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HDC	mdc;
	HBITMAP hbmp; // 位图绘制对象句柄

	hdc = BeginPaint(hwnd, &ps); 

	// 加载图片到缓存 DC
	mdc = CreateCompatibleDC(hdc);
	hbmp = (HBITMAP)LoadImage(NULL, "E:\bg.bmp", IMAGE_BITMAP, WINDOW_WIDTH, WINDOW_HEIGHT, LR_LOADFROMFILE);
	SelectObject(mdc, hbmp);

	// 修改缓存DC (在缓存的图片上写字)
	TextOut(mdc, 0,0, TEXT("Hi, 缓存DC你好。"), strlen(TEXT("Hi, 缓存DC你好。")));

	// 将缓存DC中的位图复制到窗口DC上
	BitBlt(hdc, 0,0, WINDOW_WIDTH, WINDOW_HEIGHT, mdc, 0,0, SRCCOPY);

	DeleteObject(hbmp);
	DeleteDC(mdc);
	EndPaint(hwnd, &ps); 
}

效果图:
修改缓存DC

教程索引