结论 在默认情况下,Windows 加载程序在用户磁盘上搜索 DLL 的搜索顺序: 包含可执行文件的目录。 Windows 的系统目录,该目录可以通过 GetSystemDirectory 得到,一般为 System32 目录,若为 32 位程序跑在 64 位系统下,则为 SysWOW64 目录。 16 位的系统目录,即 Windows 目录中的 System 目录。 Windows 目录,该目录可以通过 GetWindowsDirectory 得到。 进程的当前目录。 PATH 环境变量中所列出的目录。 如果调用 LoadLibrary 时传入的是绝对路径,那么加载程序将只尝试从该绝对路径搜索 DLL。 附注 以上结论在「Windows 核心编程」中列出,书中指出: 注意,对应用程序当前目录的搜索位于 Windows 目录之后,这个改变始于 Windows XP SP2,其目的是防止加载程序在应用程序的当前目录中找到伪造的系统 DLL 并将它们载入,从而保证系统 DLL 始终都是从它们在 Windows 目录的正式位置载入的。 我对这个说法持保留意见,因为在我的验证中,在一个 Windows XP SP1 的环境中已经应用了此搜索顺序。 另外,有一些其它方法可以改变加载程序的搜索顺序,已知的有: SetDllDirectory 函数。如果传入一个有效路径,那么它将被插入在默认顺序的 1 与 2 之间。 HKEY_LOCAL_MACHINES\SYSTEM\CurrentControlSet\Control\Session Manager 下的 SafeDllSearchMode 键值。 调用 LoadLibraryEx 函数时使用 LOAD_WITH_ALTERED_SEARCH_PATH 等标志。 验证 证明代码片段: #include <Windows.h> #include <stdio.h> BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: { char szPath[MAX_PATH] = {0}; GetModuleFileName(hinstDLL, szPath, MAX_PATH); printf("lib.dll is in %s\n", szPath); } break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: break; } return (TRUE); } 用如下命令行生成 lib.dll 文件: gcc lib.c -shared -o lib.dll 加载 lib.dll 的程序: #include <Windows.h> int main() { SetCurrentDirectory("D:\\test"); HMODULE hDll = (HMODULE)LoadLibrary("lib.dll"); if (hDll) { FreeLibrary(hDll); } return 0; } 用如下命令行生成 test.exe 程序: gcc test.c -o test.exe 测试方法: 在结论中提及的所有路径中各放置一份 lib.dll 文件。 运行 test.exe,可以看到控制台输出加载的 lib.dll 文件的路径。 把本次 test.exe 加载到的 lib.dll 文件删掉。 重复 2-3 步骤。
本文是对《Effective C++》的”Item 4: Make sure that objects are initialized before they’re used”的笔记和验证。 结论 在进入构造函数体之前,数据成员的初始化就已完成。 数据成员的初始化顺序取决于声明顺序。 结论 1 证明 证明代码片段: #include <stdio.h> class CA { public: CA(const char* pName = "default") { printf("CA::CA(const char*) pName = %s\n", pName); } CA(const CA& a) { printf("CA::CA(const CA&)\n"); } CA& operator = (const CA& a) { printf("CA::operator =\n"); return *this; } }; class CTest { public: CTest(CA& a) { printf("CTest::CTest(CA)\n"); m_a = a; } private: CA m_a; }; int main() { CA a("special"); CTest test(a); return 0; } 输出: CA::CA(const char*) pName = special CA::CA(const char*) pName = default CTest::CTest(CA) CA::operator = 这已经能很好地证明结论 1。而进入函数体之前的数据成员的初始化如何控制呢?答案就是——成员初始化列表。 让我们来看看将CTest的构造函数改成使用成员初始化列表以后的情况: CTest(CA& a): m_a(a) { printf("CTest::CTest(CA)\n"); } 输出: CA::CA(const char*) pName = special CA::CA(const CA&) CTest::CTest(CA) 在成员初始化列表的指定下调用了CA的复制构造函数。这两种方式的差别相当于CA a; a = b;与CA a(b);的差别,很显然使用成员初始化列表效率要更高一点。 PS: 顺便吐槽一下很多建议使用成员初始化列表而不讲为什么的老师和书,你们多讲一句能费多大劲 T.T。 结论 2 证明 证明代码片段: #include <stdio.h> class CA { public: CA(const char* pName = "default") { printf("CA::CA(const char*) pName = %s\n", pName); } CA(const CA& a) { printf("CA::CA(const CA&)\n"); } CA& operator = (const CA& a) { printf("CA::operator =\n"); return *this; } }; class CB { public: CB(const char* pName = "default") { printf("CB::CB(const char*) pName = %s\n", pName); } CB(const CB& b) { printf("CB::CB(const CB&)\n"); } CB& operator = (const CB& b) { printf("CB::operator =\n"); return *this; } }; class CTest { public: CTest(CA& a, CB& b): m_b(b), m_a(a) { printf("CTest::CTest(CA)\n"); } private: CA m_a; CB m_b; }; int main() { CA a("special A"); CB b("special B"); CTest test(a, b); return 0; } 输出: CA::CA(const char*) pName = special A CB::CB(const char*) pName = special B CA::CA(const CA&) CB::CB(const CB&) CTest::CTest(CA) 可以看出在成员初始化列表中的顺序并无作用,对成员的初始化还是以声明顺序为依据。
本文是个人对第二章:欢迎来到 Lisp——ANSI Common Lisp 中文版 一文中习题的解答。 1. 描述下列表达式求值之后的结果: (a) (+ (- 5 1) (+ 3 7)) 答案:14 (b) (list 1 (+ 2 3)) 答案:(1 5) (c) (if (listp 1) (+ 1 2) (+ 3 4)) 答案:7 (d) (list (and (listp 3) t) (+ 1 2)) 答案:(NIL 3) 2. 给出 3 种不同表示 (a b c) 的 cons 表达式 。 答案: (cons 'a '(b c)) (cons 'a (cons 'b '(c))) (cons 'a (cons 'b (cons 'c nil))) 3. 使用 car 与 cdr 来定义一个函数,返回一个列表的第四个元素。 答案: (defun get-forth(lst) (car (cdr (cdr (cdr lst))))) 4. 定义一个函数,接受两个实参,返回两者当中较大的那个。 答案: (defun get-max(x y) (if (< x y) y x)) 5. 这些函数做了什么? (a) (defun enigma (x) (and (not (null x)) (or (null (car x)) (enigma (cdr x))))) 答案:判断 x 列表中是否有 nil 元素 (b) (defun mystery (x y) (if (null y) nil (if (eql (car y) x) 0 (let ((z (mystery x (cdr y)))) (and z (+ z 1)))))) 答案:查找 x 在列表 y 中的下标,如果没有则为 nil 6. 下列表达式, x 该是什么,才会得到相同的结果? (a) > (car (x (cdr ‘(a (b c) d)))) B 答案:car (b) > (x 13 (/ 1 0)) 13 答案:or (c) > (x #’list 1 nil) (1) 答案:or '(1) 或 apply 7. 只使用本章所介绍的操作符,定义一个函数,它接受一个列表作为实参,如果有一个元素是列表时,就返回真。 答案: 非递归版本 (defun has-child-list (lst) (let ((x nil)) (dolist (obj lst) (setf x (or x (listp obj)))) x)) 递归版本 (defun has-child-list-re (lst) (if (null lst) nil (if (listp (car lst)) t (has-child-list-re (cdr lst))))) 8. 给出函数的迭代与递归版本: a. 接受一个正整数,并打印出数字数量的点。 答案: 非递归版本 (defun print-dots (n) (do ((i 0 (+ i 1))) ((= i n ) 'done) (format t "."))) 递归版本 (defun print-dots-re (n) (if (= n 0) 'done (progn (format t ".") (print-dots-re (- n 1))))) b. 接受一个列表,并返回 a 在列表里所出现的次数。 答案: 非递归版本: (defun print-a-times (lst) (let ((flag 'a)(x 0)) (dolist (obj lst) (setf x (+ x (if (eql obj flag) 1 0)))) x)) 递归版本: (defun print-a-times-re (lst) (if (null lst) 0 (let ((flag 'a)) (+ (if (eql flag (car lst)) 1 0) (print-a-times-re (cdr lst)))))) 9. 一位朋友想写一个函数,返回列表里所有非 nil 元素的和。他写了此函数的两个版本,但两个都不能工作。请解释每一个的错误在哪里,并给出正确的版本。 (a) (defun summit (lst) (remove nil lst) (apply #'+ lst)) 答案:因为 remove 并不会改变 lst 本身。正确的程序: (defun summit (lst) (let ((newlst (remove nil lst))) (apply #'+ newlst))) (b) (defun summit (lst) (let ((x (car lst))) (if (null x) (summit (cdr lst)) (+ x (summit (cdr lst)))))) 答案:因为递归没有边界退出分支。正确的程序: (defun summit (lst) (if (null lst) 0 (let ((x (car lst))) (if (null x) (summit (cdr lst)) (+ x (summit (cdr lst)))))))
略读完了潘爱民老师的《COM 原理与应用》一书,将书中个人认为一些比较重要的概念摘引如下,附少量自己的标注。 COM 对象的接口原则 书中总共列举了五条原则,其它几条我觉得理所当然,符合我「想当然」中的理解,故略去不记,这一条认为比较实用。 对于同一个对象的不同的接口指针,查询得到的 IUnknown 接口必须完全相同。也就是说,每个对象的 IUnknown 接口指针是唯一的,因此,对两个接口指针,我们可以通过判断其查询到的 IUnknown 接口是否相等来判断它们是否指向同一个对象。 反之,如果查询的不是 IUnknown 接口,而是其它接口,则通过不同的途径得到的接口指针允许不一样。这就允许有的对象可以在必要的时候才动态生成接口指针,当不用的时候可以把接口指针释放掉。 客户程序创建组件对象 包容和聚合 包容 对象 B 包容对象 A,对象 B 使用包容方式提供接口 InterfaceA,则对象 B 需要实现 InterfaceA,只不过在实现里只是将调用全部转发给了对象 A 的对应函数。 对象包容示意图: 聚合 对象 B 聚合对象 A,对象 B 使用聚合方式提供接口 InterfaceA,则对象 B 不需要实现 InterfaceA,直接暴露对象 A 的 InterfaceA。 对象聚合示意图: 对象 A 也需要为支持聚合做一些事情——实现一个委托 IUnknown 接口和一个非委托 IUnknown 接口。 按照通常使用方式实现的 IUnknown 为非委托 IUnknown,而委托 IUnknown 在不同的情况下有不同的行为:当对象被正常使用时,委托 IUnknown 把调用传递给对象的非委托 IUnknown;当对象被聚合使用时,委托 IUnknown 把调用传递到外部对象的 IUnknown 接口,即对象被创建时传递进来的 pUnknownOuter 参数,并且,这时外部对象通过非委托 IUnknown 对内部对象进行控制。 支持聚合的对象在非聚合方式下的接口示意图: 支持聚合的对象在聚合方式下的接口示意图: 进程外组件与客户程序的协作方式 代理对象用列集手段处理成员函数的参数,通过列集处理后得到一个数据包(数据流),然后通过一种跨进程的数据传输方法,比如共享内存方法,甚至是网络协议等,当数据包传输到对象进程后,存根代码用散集(列集的反过程)的方法把数据包参数解译出来,再用这些参数去调用组件对象;当组件对象成员函数返回后,存根代码又把返回值和输出参数列集成新的数据包,并把数据包传到客户进程中,代理对象接收到数据包后,把数据包解译出来再返回给客户函数,从而完成一次调用。
如下内容是学习《Head First 设计模式》第五部分《单件模式》所得,主要就是一些原文摘抄和少量自己的总结。 单件模式定义 单件模式确保类只有一个实例,并提供一个全局访问点。 书中示例的 C++ 实现版源码 这个 Solution 的 SingletonPattern 工程里。 书中示例的类图 Visio 原图见这里: 关键代码片断 class CSingleton { public: static CSingleton* GetInstance(); /** 释放单例,应仅限于所有对单例对象的使用完成后调用 */ static void ReleaseInstance(); void DoSomething(); private: CSingleton(); CSingleton(const CSingleton&); // private and not implemented copy constructor CSingleton& operator=(const CSingleton&); // private and not implemented = operator volatile static CSingleton* m_sUniqueInstance; static HANDLE m_hSync; }; volatile CSingleton* CSingleton::m_sUniqueInstance = NULL; HANDLE CSingleton::m_hSync = ::CreateEvent(NULL, FALSE, TRUE, NULL); CSingleton* CSingleton::GetInstance() { if (NULL == m_sUniqueInstance) { WaitForSingleObject(m_hSync, INFINITE); if (NULL == m_sUniqueInstance) { m_sUniqueInstance = new CSingleton(); } SetEvent(m_hSync); } return (CSingleton*)m_sUniqueInstance; } void CSingleton::ReleaseInstance() { if (0 != m_sUniqueInstance) { delete m_sUniqueInstance; m_sUniqueInstance = NULL; } if (NULL != m_hSync) { CloseHandle(m_hSync); } } 小结 单件模式的精华是通过设计来限制一个类产生多个实例的可能性,从而满足有且仅能有一个实例的类的设计需求。 为了实现上述目标,采用了私有化构造函数和提供一个公开的获取唯一实例的接口。 要记得将复制构造函数和重载赋值运算符也声明为私有,不然类的用户仍然可以使用CSingleton ObjA(*CSingleton::GetInstance())和CSingleton ObjB = *CSingleton::GetInstance())的写法来得到第二、第三甚至更多的实例对象。 为了实现多线程安全,在创建唯一实例的时候会采用多线程同步设施如临界区、事件等来保证,声明实例指针使用 volatile 关键字。 在 Java 等语言中有「同步方法」和「急切」创建实例等方式来解决多线程的问题,C++ 中貌似都不太适用,使用 volatile 加双重检查加锁相对靠谱。 记得在合适的地方释放单例以及时和正确地回收资源。
最近抽空浏览了一遍《COM 原理与应用》,一本老书了,COM 技术在我工作中运用得不多,但是接口设计规范和标准这一套东西还是能带给我一些有用的实践经验和启发的。在读到第二章《COM 对象和接口》的时候,看到虚函数表的一些相关知识,这些之前倒是也都知道,但是从来没有试着自己描述过,所以老觉得理解得不够彻底,那么……就试着结合一些小的代码段描述一下看,权当笔记加深记忆。 以下代码及运行结果基于 Win7_64 + GCC 4.7.2 环境,其它环境下可能程序运行结果等有出入,但是原理相通。 目录 无继承类对象的内存结构 单继承的类对象的内存结构 普通多继承的类对象的内存结构 单虚继承的类对象的内存结构 钻石结构的类对象的内存结构 无继承类对象的内存结构 先来看看有与没有虚函数的类的对象的内存结构的不同之处: 无虚函数的对象 内存结构: 验证如下: #include <stdio.h> class CObj { public: CObj(int nData1, int nData2) : m_nData1(nData1), m_nData2(nData2) {} ~CObj() {} void Func() { printf("CObj::Func()\n"); } private: int m_nData1; int m_nData2; }; int main() { CObj obj(11 ,22); return 0; } 来看看 obj 的实际内存分布: 小结: 数据成员按声明顺序排列 有虚函数的对象 内存结构: 验证如下: #include <stdio.h> class CObj { public: CObj(int nData1, int nData2) : m_nData1(nData1), m_nData2(nData2) {} ~CObj() {} virtual void FuncA() { printf("CObj::FuncA()\n"); } virtual void FuncB() { printf("CObj::FuncB()\n"); } private: int m_nData1; int m_nData2; }; int main() { CObj obj1(10,20); CObj obj2(11,22); return 0; } 来看看 obj1 和 obj2 的实际内存结构: 小结: 虚函数指针在虚表内按声明顺序排列 单继承的类对象的内存结构 子类覆盖父类虚函数之后虚函数表的变化可以通过对比明显的得出,这即是多态的实现机制。 子类无覆盖父类的虚函数 内存结构: 验证如下: #include <stdio.h> class CBase { public: CBase() { m_nData1 = 10; } virtual void FuncA() { printf("CBase::FuncA\n"); } private: int m_nData1; }; class CDerive : public CBase { public: CDerive() { m_nData2 = 20; } virtual void FuncB() { printf("CDerive::FuncB\n"); } private: int m_nData2; }; int main() { CDerive *pDerive = new CDerive; return 0; } 来看看 pDerive 的实际内存结构: 小结: 父类成员在子类成员之前 父类虚函数在子类虚函数之前 子类有覆盖父类的虚函数 内存结构: 验证如下: #include <stdio.h> class CBase { public: CBase() { m_nData1 = 10; } virtual void FuncA() { printf("CBase::FuncA\n"); } private: int m_nData1; }; class CDerive : public CBase { public: CDerive() { m_nData2 = 20; } virtual void FuncA() { printf("CDerive::FuncA\n"); } // add this line virtual void FuncB() { printf("CDerive::FuncB\n"); } private: int m_nData2; }; int main() { CDerive *pDerive = new CDerive; return 0; } 来看看 pDerive 的实际内存结构: 普通多继承的类对象的内存结构 内存结构: 验证如下: #include <stdio.h> class CBase1 { public: CBase1() { m_nData1 = 10; } virtual void FuncA() { printf("CBase1::FuncA\n"); } private: int m_nData1; }; class CBase2 { public: CBase2() { m_nData2 = 20; } virtual void FuncB() { printf("CBase2::FuncB\n"); } private: int m_nData2; }; class CDerive : public CBase1, public CBase2 { public: CDerive() { m_nData3 = 30; } virtual void FuncA() { printf("CDerive::FuncA\n"); } virtual void FuncB() { printf("CDerive::FuncB\n"); } virtual void FuncC() { printf("CDerive::FuncC\n"); } private: int m_nData3; }; int main() { CDerive *pDerive = new CDerive; CBase1 *pBase1 = pDerive; CBase2 *pBase2 = pDerive; return 0; } 来看看 pDerive、pBase1 和 pBase2 在实际内存中的情况: 小结: 多个父类的成员在内存中按继承时声明的顺序排列 子类数据成员放在最后一个父类的数据成员之后 子类虚函数列表在第一个虚表中 第一张虚表中存放了所有的虚函数指针,其它虚表存放了某个父类的(可能是被覆盖后的)虚函数指针 单虚继承的类对象的内存结构 内存结构: 验证如下: #include <stdio.h> class CBase { public: CBase() { m_nData1 = 10; } virtual void FuncA() { printf("CBase::FuncA\n"); } private: int m_nData1; }; class CDerive : public virtual CBase { public: CDerive() { m_nData2 = 20; } virtual void FuncA() { printf("CDerive::FuncA\n"); } virtual void FuncB() { printf("CDerive::FuncB\n"); } private: int m_nData2; }; int main() { CDerive *pDerive = new CDerive; CBase *pBase = pDerive; return 0; } 来看看 pDerive、pBase 在实际内存中的情况: 小结: 父类数据成员会放在第二张虚表指针之后 第一张虚表里存放了所有的虚函数指针 钻石结构的类对象的内存结构 内存结构: 验证如下: #include <stdio.h> class CBase { public: CBase() { m_nData = 1; } virtual void Func() { printf("CBase::Func\n"); } private: int m_nData; }; class CBase1 : virtual public CBase { public: CBase1() { m_nData1 = 10; } virtual void FuncA() { printf("CBase1::FuncA\n"); } private: int m_nData1; }; class CBase2 : virtual public CBase { public: CBase2() { m_nData2 = 20; } virtual void FuncB() { printf("CBase2::FuncB\n"); } private: int m_nData2; }; class CDerive : public CBase1, public CBase2 { public: CDerive() { m_nData3 = 30; } virtual void FuncA() { printf("CDerive::FuncA\n"); } virtual void FuncB() { printf("CDerive::FuncB\n"); } virtual void FuncC() { printf("CDerive::FuncC\n"); } private: int m_nData3; }; int main() { CDerive *pDerive = new CDerive; CBase *pBase = pDerive; CBase1 *pBase1 = pDerive; CBase2 *pBase2 = pDerive; return 0; } 来看看 pDerive、pBase、pBase1 和 pBase2 在实际内存中的情况: 小结: 第一张虚表里没有存放根父类的虚函数指针
想要灵活应用宏,离不开#和##。 ” 在学习#和##之前,先来看一个关于"的例子: #include <stdio.h> #include <string.h> int main() { const char* p1 = "Hello," "World!"; // 一个空格 const char* p2 = "Hello," "World!"; // 多个空格 const char* p3 = "Hello,""World!"; // 没有空格 const char* p4 = "Hello,World!"; // 一个整串 const char* p5 = "Nihao,""Shijie!"; // 一个不同的串 printf("p1 = %s, strlen(p1) = %d\n", p1, strlen(p1)); printf("p2 = %s, strlen(p2) = %d\n", p2, strlen(p2)); printf("p3 = %s, strlen(p3) = %d\n", p3, strlen(p3)); printf("p4 = %s, strlen(p4) = %d\n", p4, strlen(p4)); printf("p5 = %s, strlen(p5) = %d\n", p5, strlen(p5)); return 0; } 输出为: p1 = Hello,World!, strlen(p1) = 12 p2 = Hello,World!, strlen(p2) = 12 p3 = Hello,World!, strlen(p3) = 12 p4 = Hello,World!, strlen(p4) = 12 p5 = Nihao,Shijie!, strlen(p5) = 13 查看 PE 文件的常量字符串段,发现经过编译器优化后只存在一个Hello,World!串。 即 p1,p2,p3,p4 这四种写法是等价的,这一点作为之后解释#用法的前提。 字符串化操作 (#) 当用作字符串化操作时,#的主要作用是将宏参数不经扩展地转换成字符串常量。 要点: 宏定义参数的左右两边的空格会被忽略,参数的各个 Token 之间的多个空格会被转换成一个空格。 宏定义参数中含有需要特殊含义字符如"或\时,它们前面会自动被加上转义字符\。 还是通过 MSDN 上的例子来看看容易懂: #define F abc #define B def #define FB(arg) #arg #define FB1(arg) FB(arg) FB(F B) FB1(F B) 初看到时推测这两行预编译出来后效果是一样的,但是看了使用 gcc -E 编译出来代码,这才理解了 MSDN 上对「不经扩展」有了更深刻的理解,实际的预编译后代码为: "F B"; "abc def"; 推测变换过程应该如下: FB(F B) --> #F B -->"F B" FB1(F B) --> FB1(abc def) --> FB(abc def) --> #abc def --> "abc def" 标记连接操作 (##) 将多个 Token 连接成一个 Token。 要点: 它不能是宏定义中的第一个或最后一个 Token。 前后的空格可有可无。 来理解一下 MSDN 上举的例子: #include <stdio.h> #define paster( n ) printf_s( "token" #n " = %d", token##n ) int token9 = 9; int main() { paster(9); } paster(9);的预处理步骤应该如下: paster(9); printf_s( "token" #9 " = %d", token##9 ); printf_s( "token" "9" " = %d", token9 ); printf_s( "token9 = %d", token9 ); 这样应该就很好理解了。 参考 MSDN Stringizing Operator(#) : http://msdn.microsoft.com/EN-US/library/7e3a913x(v=VS.110,d=hv.2).aspx Token-Pasting Operator(##) : http://msdn.microsoft.com/EN-US/library/09dwwt6y(v=VS.110,d=hv.2).aspx
ExpandEnvironmentStrings 风险: 进程会继承其父进程的环境变量,在展开如 %APPDATA% 等目录时,有可能父进程对此环境变量进行过修改,那么可能你获取的就不是你想要的当前 SESSION 的 %APPDATA% 了。 建议: 使用 SHGetFolderPath 系列函数来做这件事。 GetModuleFileName 风险: 在 DLL 中调用时,若传入的 instance 参数为 NULL,那获取的将是加载 DLL 的进程的 EXE 的路径,若需要获取 DLL 的路径,传入的 instance 参数需为 DLL 的 hModule。 RegQueryValueEx 风险: 最后一个参数为 inout 参数,需要带入 Buffer 的 Bytes 数。不然可能出现 API 调用失败,返回 ERROR_MORE_DATA 等错误码。 建议: 调用 Windows API 时对参数的 in、out、inout 及要求的取值弄清楚。 PS:这个严格来讲不算是坑,是在 Windows API 中存在的一种现象,但是如果不小心也可能出现很难解释和调试的 BUG,记在此以备忘。 ShellExecuteEx 风险: 调用 API 之后,若初始 MASK 设置不正确,SHELLEXECUTEINFO 结构体里的 hProcess 可能为空。 建议: 若确定要使用 hProcess,则在调用 ShellExecuteEx 前确认 SHELLEXECUTEINFO 结构体的 fMask 成员设置为 SEE_MASK_NOCLOSEPROCESS。而且 MSDN 上对 hProcess 成员的注释如下: A handle to the newly started application. This member is set on return and is always NULL unless fMask is set to SEE_MASK_NOCLOSEPROCESS. Even if fMask is set to SEE_MASK_NOCLOSEPROCESS, hProcess will be NULL if no process was launched. For example, if a document to be launched is a URL and an instance of Internet Explorer is already running, it will display the document. No new process is launched, and hProcess will be NULL. Note ShellExecuteEx does not always return an hProcess, even if a process is launched as the result of the call. For example, an hProcess does not return when you use SEE_MASK_INVOKEIDLIST to invoke IContextMenu. UrlDownloadToFile 风险 1: 使用 UrlDownloadToFile 下载文件,若文件内容经过 gzip 压缩,即返回 header 包括 Content-Encoding: gzip,若调用线程没有初始化 COM,那 UrlDownloadToFile 会失败,因为 urlmon 不能正确处理压缩后的数据包。 建议: 调用此函数前需确保该线程已经调用 CoInitialize。 风险 2: 使用 UrlDownloadToFile 下载文件前它会自动先在本地缓存中查找此文件,所以可能最终得到的不是 Server 上的最新内容。 建议: 可以为 URL 添加随机参数以防止缓存,也可以使用 DeleteUrlCacheEntry 清理缓存后再使用 UrlDownloadToFile 下载文件。
查找 FIND 函数 作用: 用于在第二个文本串中定位第一个文本串,并返回第一个文本串的起始位置的值,该值从第二个文本串的第一个字符算起。 语法: FIND(find_text, within_text, [start_num]) 示例: =FIND("ha", B2) 替换 REPLACE 函数 作用: 使用其它文本字符串并根据所指定的字符数替换某文本字符串中的部分文本。 语法: REPLACE(old_text, start_num, num_chars, new_text) 示例: =REPLACE(B2, FIND("(", B2), FIND(")", B2) - FIND("(", B2) + 1, "") SUBSTITUTE 函数 作用: 在文本字符串中用 new_text 替换 old_text。如果需要在某一文本字符串中替换指定的文本,请使用函数 SUBSTITUTE;如果需要在某一文本字符串中替换指定位置的任意文本,请使用函数 REPLACE。 语法: SUBSTITUTE(text, old_text, new_text, [instance_num]) 示例: =SUBSTITUTE(B2, "helo", "hello") 去掉单元格里的空格 TRIM 函数 作用: 除了单词之间的单个空格外,清除文本中所有的空格。 语法: TRIM(text) 示例: =TRIM(B2)
如下内容是学习《Head First 设计模式》第三部分《装饰者模式》所得,主要就是一些原文摘抄和少量自己的总结。 装饰者模式定义 装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。 OO 原则 对扩展开放,对修改关闭。 书中示例的 C++ 实现版源码 这个 Solution 的 DecoratorPattern 工程里。 书中示例的类图 Visio 原图见这里: