加密介绍


加密通讯总共有四大类,签名算法、对称加密算法、非对称加密算法与量子加密技术,下面分别对几大类算法进行介绍

1、签名算法

代表算法:md5、sha1、crc32
有的地方叫签名算法,有的地方叫哈希算法,有的地方叫指纹算法、数字摘要等等,都代表一个意思。在解释这个之前我先说说“哈希”的意思。哈希也叫散列,意思是,将一串任意长度数据经过某种特定的算法转为固定长度的输出。比如10000个字符将其转为20个字符,这样的算法就叫哈希算法。20个字符虽然容易重复,但随着字符的增多,重复概率将会指数级下降,但通过20个字符很容易就能确定10000个字符的范围,比如对N组10000个字符进行对比,这样做的好处是通过比对20个字符的签名就能筛选出绝大部分数据,剩下的数据经过简单的比对就行了,非常省时省力。
签名算法有什么优势呢?
(1)、校验数据准确性。数据的准确性在通信领域有着举足轻重的作用。纠错编码比如海明码等等可以使错误编码在一定范围内时对其进行纠正,但一旦超过阈值,就算使用编码纠错,最终得出的结果也是错误的。这时候就需要哈希算法对数据进行签名,以确保数据准确性。
(2)、对数据进行归纳整理。比如哈希表就基于哈希算法,将一堆数据均匀分布在哈希结构中,使得寻找某个数据非常简单方便。
(3)、做验证之用。比如用户登录界面,在客户端经过哈希算法计算之后,服务器端只需要验证结果是不是符合就可以了,不用传输整个密码。就算黑客在中途截获数据包也无法推算出密码(暂时不考虑彩虹表等玩意儿,哈希算法可以定义为信息不可逆)。
优势挺多,但劣势也比较严重:
(1)、数据不可逆。也就是说,生成的签名几乎不可能再推导回原数据。
(2)、相对来说不那么安全。彩虹表有一定概率可以在短时间破解MD5加密的密码,王小云研究出来的哈希碰撞也能在一定程度上削弱MD5的安全性。

2、对称加密算法

代表算法:des、3des、aes
对称加密算法的原理是,加密和解密使用同一个密钥。最简单的,每个字节分别XOR一个固定字节就实现了简单的对称加密,在解密时还原回来就行了。但这种算法很容易被破解,所以需要一些比较厉害的算法,使加密后的数据不那么容易的被破解。这时候,加密标准应运而生。代表算法是其中最流行的三种。优势是:
(1)、速度快。这个速度相对于非对称加密来说,速度就像飞起来一样。
(2)、可逆。相对于签名算法来说,加密后的信息可以通过密钥还原为加密前的信息。
(3)、用的比较广的算法相对于自己XOR来说,更安全,被破解的概率非常小。
对称加密也有它的劣势:
(1)、一旦获取到加密后的数据与加密的密码,即可推算出原始数据。
(2)、无法实现签名的效果,也就是无法避免中间人攻击。

3、非对称加密算法

代表算法:Diffie-Hellman、rsa
非对称加密的核心是,对同一块数据,加密和解密使用的密码是不同的。DH是非对称加密的开端,真正让非对称加密名扬天下的算法是rsa算法。rsa算法的原理这里不讲,这种算法非常难以破解的原理是,它使用两个非常大的质数的乘积,也就是说,这个大数字只能由这两个数字相乘得到。所以暴力因式分解就非常难了,几乎不可能。目前人类破解的最长的rsa密码是768位的,可以认为1024位比较安全,2048位非常安全。优势:
(1)、通讯双方分别使用了不同的公钥和私钥,使得加密信息非常安全,窃听者必须知道两者的密钥才能完整截获所有数据。
(2)、支持签名的操作,也就是说,可以用来做验证用(签名算法无法实现)。比如软件的数字签名,用于标识软件是否被更改等。
非对称加密的劣势:
(1)、加解密速度非常慢,不过在和对称加密配合使用后,可以弱化这个劣势。
(2)、在量子技术成熟后,单纯的非对称加密也不会太安全。

4、量子加密技术

量子加密技术基于量子力学原理从而诞生的技术,可以确定的是,只要量子力学还统治微观世界,那么这种技术就可以认为几乎是绝对安全的。量子信息技术可以说是和人工智能一样可以颠覆整个行业的技术。这种技术的原理和经典计算机原理很不一样,所以在使用上会比较费劲。另外网上关于量子力学很多言论都不准确,鉴别这个比较费劲。这种技术的优势有:
(1)、通信速度翻N倍。以前是以一堆光子代表一个信号,现在是一个量子代表一个信号。
(2)、可以确定的是,只要量子力学没有被推翻,那么这项技术就是绝对安全的。
量子加密技术劣势:
(1)、一旦有新的物理学推翻量子力学,则这个技术将会在一夜间变得非常不安全。
(2)、基于现有条件,这种技术还很难广泛投入使用。
(3)、只对窃听有效。通讯两者一旦一方被攻陷,那么再安全的加密技术也无济于事。

C#:HTTP客户端与服务器的实现

HTTP协议算是使用最广泛的Web协议了,主要面向基于B/S的实现。对于很多语言比如PHP或者javascript等Web语言,HTTP协议使用上甚至比TCP协议更简单。但这协议对普通的系统语言程序员就不太友好了。C#这种语言嘛,支持上还行,但并没达到特别优秀的效果,比如发起一个POST请求至少需要十行代码以上。这个库是我基于对C#语言封装的协议的了解,在此基础上新增的一个更加方便开发的库。源码在 https://github.com/fawdlstty/hanHttpLib 查看或下载。因为可能更新频繁,所以源码我就不在文章里面贴了。
目前这个库不算特别优秀,目前只支持基础的调用,结构也非常简单,但它能力可不差,用来给小伙伴学习也是极好的。我将不定期更新它,力争将其做到完美。
先说说HTTP的客户端。HTTP客户端主要是浏览器用来请求Web服务器上的资源用的,最广泛的就是浏览器了。每个浏览器都有一套HTTP客户端实现方式,请求完成资源后在界面上显示。但随着时代的发展,HTTP协议有着基本化的趋势。因为使用HTTPS进行通讯可以使用非常小的代价就能非常安全的加密通讯数据。另外HTTP客户端也不止是做浏览器了,除此之外最大的用处是做爬虫。比如很多零散的数据分布在零散的Web服务器上,这时就可以使用爬虫技术将零散的数据归纳起来。更加厉害的就是,再结合人工智能技术,爬取数据之后使用人工智能进行分析,差不多就能成就改变世界的武器了。
虽然对于咱小白来说,都不太懂,但有这个梦啊。梦想还是要有,要不酒后的谈资还有啥。。。回归正题。这儿使用上面的HTTP客户端来实现发起请求。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 创建客户端链接对象,模拟手机访问,GET请求
hanHttpClient client = new hanHttpClient (hanHttp_UserAgent.Android, "GET");
// 设置连接地址
client.set_url ("https://cn.bing.com/search");
// 添加参数
client.add_param ("q", "test");
// 添加Cookie
//client.add_cookie ("test", "test");
// 执行请求
client.do_request ();
// 显示返回的内容
MessageBox.Show (client.m_result_data);

可以说用法是非常简单了,另外请求完成后还可以获取到服务器端设置的Cookie,如果不完全能满足需要,完全可以自己改代码,满足最终需求。
客户端说完了,下面说说服务器端。我对ASP.Net可以说又爱又恨,爱的是用来开发Web服务是相当简单,很方便就能开发出一个不错的网站。并且ASP.Net也是Web后台三大框架最简单的框架了。原生支持WebForm与MVC。可以使用模板页节省大量代码,支持全局初始化释放,POST请求或Form Submit不需Cookie,只要加入__VIEWSTATE与__VIEWSTATEGENERATOR的参数,ASP.Net后台自动识别Session等等。好处一时半会说不完,但因为毕竟是微软的东西,都向巨型化发展,搭建一个简单的Web服务也得配置整个完整的IIS环境,Windows环境用IIS还好,在Linux环境使用Apache或Nginx环境搭建ASP.Net网站就爽歪歪了,各种问题接踵而至。另外ASP.Net的权限与本地文件系统的权限纠缠不清,服务器与本地桌面应用程序交互特别麻烦,……总之一堆问题导致最终被PHP击败。
假如只需搭建一个简单的Web服务器,完全可以不用ASP.Net,使用库实现完全OK。示例代码:

1
2
3
4
5
6
// 创建HTTP服务器对象
hanHttpServer server = new hanHttpServer ();
// 开启服务并指定监听端口
server.start (8080);
// 停止服务
server.stop ();

注意服务器开启默认监听端口所有事件是需要管理员权限的,具体可以看开启服务的代码。服务的开启关闭是非常简单。程序内部使用了异步,所以调用线程会直接返回不阻塞。另外注意开启服务后就不要阻塞了,保持线程畅通^_^
服务的启动是非常简单,那么服务的处理呢?这个也简单,在hanHttpServer.cs中找到以下代码

1
2
3
4
// 此处根据str_url处理输出,假定输入内容写在str里
// 获取请求参数 get_value ("test")
string str = "ok";
res.StatusCode = 200;

此处改为自定义处理过程就ok了。

XML Schema (一种XSL) 简单介绍

简单介绍下,XSL(eXtensible Stylesheet Language,扩展样式语言)是描述XML文档结构规则的语言,XML Schema是其中使用最广泛的一种。也就是说,你可以使用XML Schema限制某一类XML完全按照你的规范来编写代码。
比如,你想让某个XML文件以MainNode作为根节点名称,但可能有main_node作为根节点名称的,符合XML文件规范,但你就是识别不了它。这就比较尴尬了。那么,使用XSL验证机制,就可以保证XML文件100%符合你的规范。
也许做过HTML4的前端知道,DOCTYPE需要指向一个dtd文件,比如

1
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

DTD也是XSL的一种,但因为对扩展的支持性不好,在HTML5中被弃用,之后DTD的使用将会越来越少。对于基于HTML4的网页代码都使用DTD作为规则文件对网页文档进行规范。HTML是从XML基础上演化而来的,当然也属于XML标准规范的一部分。使用标准XML也可以使用dtd进行描述。dtd此处不细说,此处说说XML Schema。

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<f:MainNode xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:f="urn:fawdlstty:2017.01" xsi:schemaLocation="urn:fawdlstty:2017.01 faw.xsd">
    <f:name>fawdlstty</f:name>
    <f:addr xsi:type="URL">www.fawdlstty.com</f:addr>
</f:MainNode>

大概使用XML Schema的XML文件长这样。小伙伴们就发现了,每个标签或者属性都是冒号分开的两个名称。下面我详细解释下这种XML写法。首先,根节点中必须要有一行,xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance",代表着这是基于XML Schema的一个实例,这儿xmlns:f的意思是,定义f这个命名空间,描述指向“urn:fawdlstty:2017.01”,意味着,只要是以f开头的标签或属性,都要遵循这个描述地址所指向的规范。假如这儿写的是xmlns,后面没有接冒号,那含义就是代表默认命名空间规范,意味着所有没有带命名空间的标签和属性都得遵循这个规范。
XML Schema最大优势是支持扩展,也就是说,可以一个XML引用多个Schema验证文件,只需保证命名空间名称和资源名称不重复即可。
下面附一则XML Schema教程链接:XML Schema语法教程
XML Schema除了能验证文档规范性以外还有一个比较有特色的功能,那就是给智能IDE提供自动提示。这个就非常强大了,可以说,有了Schema,写XML就像写代码那样简单了。
基于XSL外,还有一个用的非常广泛的一种语言,叫XSLT(eXtensible Stylesheet Language Transformations,扩展样式转换语言),用于将XML文档转换为其他任意格式的语言。附上教程链接:XSLT语法教程

说了这么多,来点干货。对于现在的XML的访问库啊,暂时还没找到一个好用的,好用的JSON库比如jsoncpp都特别好用,好用的XML库还没找到。首先对于XML吧,不支持带命名空间的就直接刷下去大多数了,只能在为数不多的剩下的中找。我目前使用的是一个基于CXml的库,这玩意封装的挺好,但写起来还是稍微有点麻烦。我就在这库基础上再次封装了一层。地址在这:hanXmlLib

这个库目前比较简陋,但也挺好用了。我也在在慢慢的完善它。
对于不带命名空间的XML来说,可以这么用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 打开文档
hXmlDoc doc = hXmlDoc::create_from_file (_T ("D:/a.xml"));
// 根据字符串创建文档
hXmlDoc doc2 = hXmlDoc::create_from_string (_T ("<Root><a /></Root>"));
// 创建新文档
hXmlDoc doc3 = hXmlDoc::create_new (_T ("RootNode"));
 
// 创建节点
hXmlObject obj = doc.create_child (_T ("ChildNode"));
// 设置属性
obj.set_attr (_T ("name"), _T ("aaa"));
// 获取属性
CString attr = obj.get_attr (_T ("name"));
// 移除属性
obj.remove_attr ();
// 移除当前节点
obj.remove ();
 
// 保存当前文档
doc.save (_T ("D:/a.xml"));

带命名空间的使用上稍微麻烦点,需要初始化命名空间。打开文档和从字符串中创建的代码可以自动捕获命名空间,但对于新建节点对象的命名空间来说,就需要稍微麻烦一些,初始化列表写完所有命名空间,另外还需要调用set_schema_location函数,用于设置Uri地址。

C#:扩展方法集合类

扩展方法集合类在C#中是一种特别的语法糖,可以在开发中极大简化代码的编写。主要功能是在系统类中创建自定义方法。比如字符串转数字,每次都得Convert.ToInt32 (str),从语法上来说就太不简洁了。下面我一步一步讲解扩展方法集合类的编写。首先创建一个静态类,类名必须为ExtensionMethods。示例代码如下

1
2
3
public static class ExtensionMethods {
    //...
}

所有扩展方法都需要放在此类中进行实现。首先创建一个静态函数,第一个参数使用this描述符,表示基于哪个类提供扩展方法

1
2
3
public static Int32 toInt32 (this object o) {
    return Convert.ToInt32 (o);
}

此函数代表在object这个类基础上新增一个扩展方法,无参数,返回值为Int32类型。示例调用代码如下:

1
int t = "123".toInt32();

下面我提供一个我自用的扩展方法集合类供参阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
////////////////////////////////////////////////////////////////////////////////
//
// Class Name:  ExtensionMethods
// Description: C#扩展方法集合类
// Class URI:   https://www.fawdlstty.com/archives/411.html
// Author:      Fawdlstty
// Author URI:  https://www.fawdlstty.com/
// Version:     0.1
// License:     MIT
// Last Update: Jun 25, 2017
//
////////////////////////////////////////////////////////////////////////////////
 
using System;
using System.Text;
 
namespace docx_to_xlsx {
    public static class ExtensionMethods {
        /// <summary>
        /// 返回元素在数组中的索引
        /// </summary>
        /// <param name="a">数组</param>
        /// <param name="o">元素</param>
        /// <returns>索引</returns>
        public static int inArray (this object [] a, object o) {
            for (int i = 0; i < a.Length; i++) {
                if (a [i] == o) return i;
            }
            return -1;
        }
 
        /// <summary>
        /// 返回元素在数组中的索引
        /// </summary>
        /// <param name="o">元素</param>
        /// <param name="a">数组</param>
        /// <returns>索引</returns>
        public static int inArray (this object o, object [] a) {
            return a.inArray (o);
        }
 
        /// <summary>
        /// URL转码
        /// </summary>
        /// <param name="s">原始URL</param>
        /// <returns>新URL</returns>
        public static string encodeUrl (this string s) {
            if (s.isNullOrEmpty ()) return "";
            return s.Replace (":", "%3a").Replace ("/", "%2f").Replace (" ", "+").Replace ("\\", "%2c").Replace ("=", "%3d").Replace ("?", "%3f").Replace ("%", "%25").Replace ("&", "%26");
        }
 
        /// <summary>
        /// 简化string.Format函数的调用
        /// </summary>
        /// <param name="s">格式化字符串</param>
        /// <param name="args">参数列表</param>
        /// <returns>格式化后的字符串</returns>
        public static string format (this string s, params object [] args) {
            return string.Format (s, args);
        }
 
        /// <summary>
        /// 简化Convert.ToInt32函数的调用
        /// </summary>
        /// <param name="o">需要转为数字的对象</param>
        /// <returns>数字</returns>
        public static Int32 toInt32 (this object o) {
            try {
                return Convert.ToInt32 (o);
            } catch (Exception ex) {
                throw ex;
            }
        }
 
        /// <summary>
        /// 简化Convert.ToInt64函数的调用
        /// </summary>
        /// <param name="o">需要转为数字的对象</param>
        /// <returns>数字</returns>
        public static Int64 toInt64 (this object o) {
            try {
                return Convert.ToInt64 (o);
            } catch (Exception ex) {
                throw ex;
            }
        }
 
        /// <summary>
        /// 简化Convert.ToDouble函数的调用
        /// </summary>
        /// <param name="o">需要转为数字的对象</param>
        /// <returns>数字</returns>
        public static double toDouble (this object o) {
            try {
                return Convert.ToDouble (o);
            } catch (Exception ex) {
                throw ex;
            }
        }
 
        public static int hex_to_dec (this String s) {
            try {
                return Convert.ToInt32 (s, 16);
            } catch (Exception ex) {
                throw ex;
            }
        }
 
        ///// <summary>
        ///// MD5加密
        ///// </summary>
        ///// <param name="s">需要加密的数据</param>
        ///// <param name="loop">加密次数</param>
        ///// <returns>加密后的数据</returns>
        //public static string md5Encrypt (this string s, int loop = 3) {
        //    MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider ();
        //    for (; loop > 0; loop--) {
        //        s = BitConverter.ToString (md5.ComputeHash (UTF8Encoding.Default.GetBytes (s))).Replace ("-", "");
        //    }
        //    return "{0}-{1}-{2}-{3}-{4}".format (s.Substring (0, 8), s.Substring (8, 4), s.Substring (12, 4), s.Substring (16, 4), s.Substring (20, 12));
        //}
 
        /// <summary>
        /// 简化string.Join函数调用
        /// </summary>
        /// <param name="s">数组对象</param>
        /// <param name="separator">分隔符</param>
        /// <returns>字符串</returns>
        public static string join (this string [] s, string separator = ";") {
            if (s == null) return "";
            return string.Join (separator, s);
        }
 
        /// <summary>
        /// 二维数组垂直string.Join函数调用
        /// </summary>
        /// <param name="ss">数组对象</param>
        /// <param name="col">垂直第几列</param>
        /// <param name="separator">分隔符</param>
        /// <returns>字符串</returns>
        public static string join (this string [] [] ss, int col, string separator = ";") {
            if (ss == null || ss.Length == 0)
                return "";
            StringBuilder sb = new StringBuilder ();
            for (int i = 0; i < ss.Length; i++) {
                sb.Append (ss [i] [col]).Append (separator);
            }
            return sb.Remove (sb.Length - separator.Length, separator.Length).toString ();
        }
 
        /// <summary>
        /// 简化string.IsNullOrEmpty函数调用
        /// </summary>
        /// <param name="s">字符串</param>
        /// <returns>是否为空</returns>
        public static bool isNullOrEmpty (this string s) {
            return string.IsNullOrEmpty (s);
        }
 
        /// <summary>
        /// 字符串或其他类型转为日期时间对象
        /// </summary>
        /// <param name="o">需要转换的对象</param>
        /// <returns>日期时间类型</returns>
        public static DateTime toDateTime (this object o) {
            try {
                return Convert.ToDateTime (o);
            } catch (Exception ex) {
                throw ex;
            }
        }
 
        /// <summary>
        /// 格式化小数
        /// </summary>
        /// <param name="o">需要格式化小数的对象</param>
        /// <param name="afterPoint">保留小数点后几位</param>
        /// <returns>字符串型数据</returns>
        public static string formatDecimal (this object o, int afterPoint = 2) {
            try {
                return string.Format (string.Format ("{{0:F{0}}}", afterPoint), o);
            } catch (Exception ex) {
                throw ex;
            }
        }
 
        /// <summary>
        /// 格式化日期
        /// </summary>
        /// <param name="o">需要格式化的日期</param>
        /// <param name="sDate">日期分隔符</param>
        /// <returns>字符串</returns>
        public static string formatDate (this DateTime o, char sDate = '-') {
            if (o == null) return "";
            return o.ToString (string.Format ("yyyy{0}MM{0}dd", sDate));
        }
 
        /// <summary>
        /// 格式化日期(汉字)
        /// </summary>
        /// <param name="o">需要格式化的日期</param>
        /// <returns>字符串</returns>
        public static string formatDateLong (this DateTime o) {
            if (o == null) return "";
            return o.ToString ("yyyy年MM月dd日");
        }
 
        /// <summary>
        /// 格式化时间
        /// </summary>
        /// <param name="o">需要被格式化的时间</param>
        /// <param name="sTime">分隔符,默认冒号</param>
        /// <returns>格式化后的时间</returns>
        public static string formatTime (this DateTime o, char sTime = ':') {
            if (o == null) return "";
            return o.ToString (string.Format ("HH{0}mm{0}ss", sTime));
        }
 
        /// <summary>
        /// 格式化时间(汉字)
        /// </summary>
        /// <param name="o">需要被格式化的时间</param>
        /// <returns>格式化后的时间</returns>
        public static string formatTimeLong (this DateTime o) {
            if (o == null) return "";
            return o.ToString ("HH时mm分ss秒");
        }
 
        /// <summary>
        /// 格式化日期时间
        /// </summary>
        /// <param name="o">需要格式化的日期时间</param>
        /// <param name="sDate">日期分隔符</param>
        /// <param name="sTime">时间分隔符</param>
        /// <returns>字符串</returns>
        public static string formatDateTime (this DateTime o, char sDate = '-', char sTime = ':') {
            if (o == null) return "";
            return string.Format ("{0} {1}", o.formatDate (sDate), o.formatTime (sTime));
        }
 
        /// <summary>
        /// 格式化日期时间(汉字)
        /// </summary>
        /// <param name="o">需要格式化的日期时间</param>
        /// <returns>字符串</returns>
        public static string formatDateTimeLong (this DateTime o) {
            if (o == null) return "";
            return string.Format ("{0} {1}", o.formatDateLong (), o.formatTimeLong ());
        }
 
        /// <summary>
        /// 将布尔值转为字符串
        /// </summary>
        /// <param name="b">布尔值</param>
        /// <returns>字符串</returns>
        public static string boolToString (this bool b) {
            try {
                return b ? "ok" : "fealure";
            } catch (Exception ex) {
                return ex.Message;
            }
        }
 
        ///// <summary>
        ///// 将一个对象序列化为json字符串
        ///// </summary>
        ///// <param name="o">可序列化对象</param>
        ///// <returns>json字符串</returns>
        //public static string toJson (this object o) {
        //    try {
        //        if (o == null) return "[]";
        //        return new JavaScriptSerializer ().Serialize (o);
        //    } catch (Exception ex) {
        //        return ex.Message;
        //    }
        //}
 
        ///// <summary>
        ///// 将json字符串反序列化为一个对象
        ///// </summary>
        ///// <typeparam name="T">返回值类型</typeparam>
        ///// <param name="s">json字符串</param>
        ///// <returns>反序列化后的对象</returns>
        //public static T fromJson<T> (this string s) {
        //    try {
        //        if (s.isNullOrEmpty ()) return default (T);
        //        return new JavaScriptSerializer ().Deserialize<T> (s);
        //    } catch (Exception ex) {
        //        throw ex;
        //    }
        //}
 
        /// <summary>
        /// 用于可能为空的对象转字符串,避免对象为空引发的错误
        /// </summary>
        /// <param name="o">object对象</param>
        /// <returns>字符串</returns>
        public static string toString (this object o) {
            if (o == null) return "";
            return o.ToString ();
        }
 
        /// <summary>
        /// 向上取整
        /// </summary>
        /// <param name="n">浮点数</param>
        /// <returns>计算结果</returns>
        public static int cell (this double d) {
            int i = (int) d;
            if (d > i)
                return i + 1;
            return i;
        }
 
        /// <summary>
        /// 向下取整
        /// </summary>
        /// <param name="n">浮点数</param>
        /// <returns>计算结果</returns>
        public static int floor (this double d) {
            return (int) d;
        }
 
        //供ID检查用
        private static Func<char, bool> isSign = c => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
 
        /// <summary>
        /// 检查是否为标准32位ID格式
        /// </summary>
        /// <param name="s">ID</param>
        /// <returns>是否标准</returns>
        public static bool isId32 (this string s) {
            if (s.isNullOrEmpty () || s.Length != 32) return false;
            for (int i = 0; i < s.Length; i++) {
                if (!isSign (s [i])) return false;
            }
            return true;
        }
 
        /// <summary>
        /// 检查是否为标准36位ID格式
        /// </summary>
        /// <param name="s">ID</param>
        /// <returns>是否标准</returns>
        public static bool isId36 (this string s) {
            if (s.isNullOrEmpty () || s.Length != 36) return false;
            for (int i = 0; i < s.Length; i++) {
                if (i == 8 || i == 13 || i == 18 || i == 23) {
                    if (s [i] != '-') return false;
                } else {
                    if (!isSign (s [i])) return false;
                }
            }
            return true;
        }
    }
}

C#:使用OpenXML读写Excel文档

对于Office文件的读写操作始终是一个比较麻烦的事情,最近做了一个Office操作的小程序,写一个文章用于备忘。
Office文件读写方法挺多,最好的方法自然是OpenXML无疑,但这库用起来还是比较麻烦。我在此做一个Excel读写小例子,其他Office类型同理。
对于OpenXML这库,首先需要知道OpenXML是什么。新建一个Excel文件,另存为a.xlsx,然后将其重命名为a.rar,解压,然后看到一大堆xml文件。不用怀疑,这就是xml标准。对于Office文件的读的访问来说,如果对OpenXML不熟,并且使用其他语言不方便调用C#的库,那么,手工解压然后找找数据的位置,然后手工解析,是最快捷的方式。当然,在可以使用OpenXML的场合,我个人还是推荐使用OpenXML。
OpenXML2.5下载地址(主体工具摘自微软官网,扩展摘自网络):https://pan.baidu.com/s/1slDa0Jr,提取码98dd
这个工具只有英语版的,但由于操作过于简单,所以对于英语小白也完全无压力。
1、首先依次安装两个库,有次序但不重要,如果你装反了那么系统会不让你继续装,并提示你先装另外一个。不论如何,两个都装上就OK。
2、安装目录下找到OpenXmlSdkTool.exe,因为常用所以可以发送到桌面快捷方式。然后打开这软件,点击Open File或者将Office文件拖到左侧的框中;然后点击Reflect Code。然后就会生成一堆代码。简单解释下,这堆代码复制到工程内,然后调用new GeneratedCode.GeneratedClass().CreatePackage("d:/a.xlsx")。根据文件类型决定生成的文件后缀。然后,就可以生成一个与导入的Office文件完全一样的文件了。
3、代码量可能稍稍有点多,不过多玩几次,看看里面的代码是怎么生成的,然后根据自己的需要,手工修改代码,比如循环生成特定字符串等等,用起来就挺方便了。
OpenXML可以写当然也可以读,但稍微麻烦点。我最近把OpenXML读Excel的功能封装成了一个类,通过这个类可以非常方便的读Excel单元格中的内容。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
////////////////////////////////////////////////////////////////////////////////
//
// Class Name:  excel_access
// Description: 用于读取Excel文档某单元格的字符串内容或者前景色、背景色等
// Class URI:   https://www.fawdlstty.com/archives/405.html
// Author:      Fawdlstty
// Author URI:  https://www.fawdlstty.com/
// Version:     0.2
// License:     MIT
// Last Update: Jul 04, 2017
//
////////////////////////////////////////////////////////////////////////////////
 
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Extensions;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace excel_access {
    public class excel_access {
        private SpreadsheetDocument m_ssDoc = null;
        private WorkbookStylesPart m_styles = null;
        private WorkbookPart m_wbPart = null;
        private List<Sheet> m_sheets = null;
        private Sheet m_sheet = null;
        private WorksheetPart m_wsPart = null;
        private String[] m_colors = null;
 
        /// <summary>
        /// 打开excel文档与工作表
        /// </summary>
        /// <param name="path">文件路径</param>
        /// <param name="sheet">表格名称</param>
        public void open_excel(String path, String sheet = "Sheet1") {
            m_ssDoc = SpreadsheetDocument.Open (path, false);
            m_styles = SpreadsheetReader.GetWorkbookStyles (m_ssDoc);
            m_wbPart = m_ssDoc.WorkbookPart;
            m_sheets = m_wbPart.Workbook.Descendants<Sheet> ().ToList ();
            m_sheet = m_wbPart.Workbook.Descendants<Sheet> ().FirstOrDefault (c => c.Name == sheet);
            m_wsPart = (WorksheetPart) m_wbPart.GetPartById (m_sheet.Id);
 
            Colors colors = m_styles.Stylesheet.Colors;
            if (colors != null) {
                int count = colors.FirstChild.ChildElements.Count;
                m_colors = new String [count];
                for (int i = 0; i < count; ++i)
                    m_colors [i] = ((Color) colors.FirstChild.ChildElements [i]).Rgb.Value;
            }
            else {
                m_colors = new String [] {
                    "00000000", "00FFFFFF", "00FF0000", "0000FF00", "000000FF", "00FFFF00", "00FF00FF", "0000FFFF",
                    "00000000", "00FFFFFF", "00FF0000", "0000FF00", "000000FF", "00FFFF00", "00FF00FF", "0000FFFF",
                    "00800000", "00008000", "00000080", "00808000", "00800080", "00008080", "00C0C0C0", "00808080",
                    "009999FF", "00993366", "00FFFFCC", "00CCFFFF", "00660066", "00FF8080", "000066CC", "00CCCCFF",
                    "00000080", "00FF00FF", "00FFFF00", "0000FFFF", "00800080", "00800000", "00008080", "000000FF",
                    "0000CCFF", "00CCFFFF", "00CCFFCC", "00FFFF99", "0099CCFF", "00FF99CC", "00CC99FF", "00FFCC99",
                    "003366FF", "0033CCCC", "0099CC00", "00FFCC00", "00FF9900", "00FF6600", "00666699", "00969696",
                    "00003366", "00339966", "00003300", "00333300", "00993300", "00993366", "00333399", "00333333"
                };
            }
        }
 
        /// <summary>
        /// 打开工作表
        /// </summary>
        /// <param name="sheet"></param>
        public void open_sheet(String sheet = "Sheet1") {
            m_sheet = m_wbPart.Workbook.Descendants<Sheet> ().FirstOrDefault (c => c.Name == sheet);
            m_wsPart = (WorksheetPart) m_wbPart.GetPartById (m_sheet.Id);
        }
 
        /// <summary>
        /// 关闭文档
        /// </summary>
        public void close() {
            m_ssDoc.Close ();
            m_ssDoc.Dispose ();
 
            m_ssDoc = null;
            m_styles = null;
            m_wbPart = null;
            m_sheets = null;
            m_sheet = null;
            m_wsPart = null;
        }
 
        /// <summary>
        /// 获取单元格
        /// </summary>
        /// <param name="pos">位置</param>
        /// <returns>单元格</returns>
        public Cell get_item(String pos = "A1") {
            return m_wsPart.Worksheet.Descendants<Cell> ().Where (c => c.CellReference.Value == pos).FirstOrDefault ();
        }
 
        /// <summary>
        /// 获取单元格
        /// </summary>
        /// <param name="iCol">第几列,以0起始</param>
        /// <param name="iLine">第几行,以0起始</param>
        /// <returns></returns>
        public Cell get_item(int iCol, int iLine) {
            String strCol = "";
            if (iCol < 26)
                strCol = "{0}{1}".format (((char) ((int) 'a' + iCol)), iLine + 1);
            else if (iCol < 26*26+26) {
                iCol -= 26;
                strCol = "{0}{1}{2}".format(((char) ((int) 'a' + iCol / 26)), ((char) ((int) 'a' + iCol % 26)), iLine + 1);
            }
            return get_item (strCol);
        }
 
        /// <summary>
        /// 获取单元格内容
        /// </summary>
        /// <param name="cell">单元格</param>
        /// <returns>内容</returns>
        public String get_value(Cell cell) {
            string value = cell.InnerText;
            if (cell.DataType != null) {
                switch (cell.DataType.Value) {
                case CellValues.SharedString:
                    var stringTable = m_wbPart.GetPartsOfType<SharedStringTablePart>().FirstOrDefault ();
                    if (stringTable != null) {
                        value = stringTable.SharedStringTable.ElementAt (int.Parse (value)).InnerText;
                    }
                    break;
 
                case CellValues.Boolean:
                    switch (value) {
                    case "0":
                        value = "FALSE";
                        break;
                    default:
                        value = "TRUE";
                        break;
                    }
                    break;
                }
            }
            return value != null ? value : "";
        }
 
        /// <summary>
        /// 计算Tint深浅变化
        /// </summary>
        /// <param name="iColor">颜色值,取值0-255</param>
        /// <param name="tint">深浅变化参数</param>
        /// <returns>计算结果</returns>
        private int calc_single_tinted_value (int iColor, Double tint) {
            if (tint < 0)
                return (iColor * (1.0 + tint)).toInt32 ();
            else if (tint > 0)
                return (iColor * (1.0 - tint) + (255 - 255 * (1.0 - tint))).toInt32 ();
            if (iColor < 0 || iColor > 255)
                throw new Exception ("tint计算结果范围错误!");
            return iColor;
        }
 
        /// <summary>
        /// 获取标准格式颜色字符串
        /// </summary>
        /// <param name="ct">颜色类型</param>
        /// <returns>标准格式颜色字符串</returns>
        private String format_color (ColorType ct) {
            if (ct == null)
                return "";
 
            // 获取颜色数据
            String strColor = "";
            if (ct.Rgb != null) {
                strColor = ct.Rgb.Value;
            } else if (ct.Theme != null) {
                DocumentFormat.OpenXml.Drawing.Color2Type c2t = (DocumentFormat.OpenXml.Drawing.Color2Type) m_ssDoc.WorkbookPart.ThemePart.Theme.ThemeElements.ColorScheme.ChildElements [(int) ct.Theme.Value];
                //Debug.WriteLine (c2t.OuterXml);
                if (c2t.RgbColorModelHex != null)
                    strColor = c2t.RgbColorModelHex.Val;
            } else if (ct.Indexed != null) {
                int iIndexed = (int) (uint) ct.Indexed;
                if (iIndexed < m_colors.Length)
                    strColor = m_colors [iIndexed];
            }
 
            // 获取颜色字符串
            if (strColor.Length == 0) {
                return "";
            } else if (strColor.Length > 6) {
                strColor = strColor.Substring (strColor.Length - 6, 6);
            }
 
            // 计算实际颜色
            if (ct.Tint != null && ct.Tint != 0.0f) {
                int iColor = strColor.hex_to_dec();
                int iRed = (iColor >> 16) & 0xff;
                iRed = calc_single_tinted_value (iRed, ct.Tint);
                int iGreen = (iColor >> 8) & 0xff;
                iGreen = calc_single_tinted_value (iGreen, ct.Tint);
                int iBlue = (iColor) & 0xff;
                iBlue = calc_single_tinted_value (iBlue, ct.Tint);
                iColor = (iRed << 16) + (iGreen << 8) + iBlue;
                strColor = "{0:X}".format(iColor);
                while (strColor.Length < 6)
                    strColor = "0" + strColor;
            }
 
            // 生成结果颜色字符串
            if (strColor.Length > 6)
                strColor = strColor.Substring (strColor.Length - 6, 6);
            while (strColor.Length < 8)
                strColor = "F" + strColor;
            return strColor;
        }
 
        /// <summary>
        /// 获取单元格背景色
        /// </summary>
        /// <param name="cell">单元格</param>
        /// <returns>单元格背景色</returns>
        public String get_background_color(Cell cell) {
            // 获取填充类型
            int cellStyleIndex = 0;
            if (cell.StyleIndex != null)
                cellStyleIndex = (int) cell.StyleIndex.Value;
            CellFormat cf = (CellFormat) m_styles.Stylesheet.CellFormats.ChildElements [cellStyleIndex];
            Fill fill = (Fill) m_styles.Stylesheet.Fills.ChildElements [(int) cf.FillId.Value];
            return format_color (fill.PatternFill.ForegroundColor);
        }
 
        /// <summary>
        /// 获取单元格前景色
        /// </summary>
        /// <param name="cell">单元格</param>
        /// <returns>单元格前景色</returns>
        public String get_foreground_color(Cell cell) {
            // 获取填充类型
            int cellStyleIndex = 0;
            if (cell.StyleIndex != null)
                cellStyleIndex = (int) cell.StyleIndex.Value;
            CellFormat cf = (CellFormat) m_styles.Stylesheet.CellFormats.ChildElements [cellStyleIndex];
            Font font = (Font) m_styles.Stylesheet.Fonts.ChildElements [(int) cf.FontId.Value];
            return format_color (font.Color);
        }
 
        /// <summary>
        /// 获取单元格颜色类型
        /// </summary>
        /// <param name="strForeColor">前景色</param>
        /// <param name="strBackColor">背景色</param>
        /// <returns>单元格颜色类型</returns>
        private int get_cell_format_index(String strForeColor, String strBackColor, int iDefault = 0) {
            // 查找现有列表看是否存在已有类型
            OpenXmlElementList list = m_styles.Stylesheet.CellFormats.ChildElements;
            OpenXmlElementList tlist = m_styles.Stylesheet.Fonts.ChildElements;
            CellFormat cf = null;
            for (int i = 0; i < list.Count; ++i) {
                cf = (CellFormat)list[i];
                String strTmpForeColor = format_color (((Font) tlist [(int) cf.FontId.Value]).Color);
                String strTmpBackColor = format_color (((Fill) tlist [(int) cf.FillId.Value]).PatternFill.ForegroundColor);
                if (strTmpForeColor == strForeColor || strTmpBackColor == strBackColor)
                    return i;
            }
 
            return iDefault;
        }
    }
}

具体用法示例如下:

1
2
3
4
5
6
excel_access ea = new excel_access();
ea.open_excel ("D:/a.xlsx");
Cell cell = ea.get_item ("B3");
System.Diagnostics.Debug.WriteLine ("内容:{0}".format (ea.get_value (cell)));
System.Diagnostics.Debug.WriteLine ("前景色:{0}".format (get_foreground_color (cell)));
System.Diagnostics.Debug.WriteLine ("背景色:{0}".format (get_background_color (cell)));

以上代码演示了如何打开一个文档,以及获取单元格内容、前景色、背景色信息。在此重点解释下Tint这个Double类型变量。这个类型代表明暗程度深浅的变化,取值区间为[-1.0,1.0],如果为负代表颜色变暗,如果为正代表颜色变亮。如果存在这个值,那么代表变量中的颜色并非实际显示颜色,实际显示颜色还得再经过一轮深浅变换。公式摘自MSDN

说完了读,下面说说写。写Excel主要分为写单元格内容与设置单元格颜色等样式。首先,对于需要生成某个Excel文件,那么先用OpenXmlTool转成代码,然后找到GenerateWorksheetPart1Content函数,里面的Row、Cell这儿。大概代码如下:

1
2
3
4
5
6
7
8
9
Row row1 = new Row(){ RowIndex = (UInt32Value)1U, Spans = new ListValue<StringValue>() { InnerText = "1:1" }, DyDescent = 0.2D };
 
Cell cell1 = new Cell(){ CellReference = "A1", StyleIndex = (UInt32Value)1U, DataType = CellValues.SharedString, CellValue = new CellValue("0") };
row1.Append (cell1);
 
Cell cell2 = new Cell(){ CellReference = "B1", StyleIndex = (UInt32Value)2U, DataType = CellValues.SharedString, CellValue = new CellValue("1") };
row1.Append (cell2);
 
sheetData1.Append (row1);

Cell的属性CellReference代表单元格的位置。比如"A1"就是左上角第一个单元格。然后,关键位置注意(敲黑板),如果DataType类型为CellValues.SharedString,那么CellValue的内容为一个引用,否则为一个值。假如,需要在单元格存储一个数字或者一个小数,那么可以直接在CellValue里面写上相应的内容(一定不要是字符串),就行了。假如是一个字符串呢?那么稍微麻烦点,CellValue里面定义字符串类型的数字,写上索引,从0开始,然后,再找到GenerateSharedStringTablePart1Content函数,大概有以下这种形状的代码:

1
2
3
4
SharedStringItem sharedStringItem1 = new SharedStringItem();
sharedStringItem1.Append (new Text ("aaa"));
sharedStringItem1.Append (new PhoneticProperties () { FontId = (UInt32Value) 1U, Type = PhoneticValues.NoConversion });
sharedStringTable1.Append (sharedStringItem1);

注意下,写到这儿的值都是字符串。sharedStringTable1变量中第一个append的值,就是单元格定义那儿,CellValue为"0"的索引的那个值,相应的,第二个append的值,就是CellValue为"1"的索引的那个值。以此类推。相应的。如果A1和B1都是CellValues.SharedString类型,并且CellValue均为"0",那么它们的值都是sharedStringTable1变量中第一个append的值。

说完了单元格内容,下面说说写单元格样式。这个样式稍微麻烦点,需要知道三个东西:Font、Fill与CellFormat。简而言之,Font代表前景色,Fill代表背景色,CellFormat用于将其关联。
假如需要修改某个单元格的颜色,那么需要这样做:
1、找到GenerateWorkbookStylesPart1Content函数,Fonts这儿Count值+1,然后新建一个Font,Color这儿写成new Color () { Rgb = "FFDD9933" },前两个为F,后六个分别为RGB的十六进制值。
2、Fills这儿Count值+1,然后新建一个Fill,写如下代码:

1
2
3
4
5
6
7
Fill fill = new Fill();
fill.Append (new PatternFill () {
    PatternType = PatternValues.Solid,
    ForegroundColor = new ForegroundColor () { Rgb = "FF3399DD" },
    BackgroundColor = new BackgroundColor () { Indexed = (UInt32Value) 64U }
});
fills1.Append (fill);

其中Rgb这儿代表颜色,格式同上。另外别被名称忽悠了,这儿的ForegroundColor就代表背景色,BackgroundColor的Indexed值只能为64U。
3、CellsFormats这儿新建一个CellsFormat,写大概以下格式代码

1
2
3
4
5
6
7
8
9
10
cellFormats1.Append (new CellFormat () {
    NumberFormatId = (UInt32Value) 0U,
    FontId = (UInt32Value) 3U,
    FillId = (UInt32Value) 3U,
    BorderId = (UInt32Value) 0U,
    FormatId = (UInt32Value) 0U,
    ApplyFont = true,
    ApplyFill = true,
    Alignment = new Alignment () { Vertical = VerticalAlignmentValues.Center }
});

其中FontId代表之前新建的Font在Fonts的Append的位置,比如第一个append,那么就为0;FillId代表之前新建的Fill在Fills的Append的位置,比如第一个append那么就为0。这两个值稍微改一下,另外也不需要完全按照这种格式来,最好在自己的添加代码那儿复制一份,然后再改。注意下,边框也在CellsFormat这儿,自己新建一个有边框色的xlsx文档,然后生成代码,然后照着看一下,就明白了。
4、找到之前创建Cell的位置,比如A1单元格需要改为自己新建的格式那么StyleIndex的值改为CellFormat的Index(也就是CellFormats第几个append的位置,第一个append那么就为0)。如果是字符串,那么还得找到创建SharedStringItem的位置,找到创建PhoneticProperties对象时传的参数,FontId为字体的Id,必须与CellFormat里的FontId一致,否则生成的xlsx文件就可能有问题。

Excel操作就写完了。最后说下注意事项。CellStyleFormats与CellFormats非常相似,添加的都是CellFormat类型的对象,很容易搞混,这儿我们不关心CellStyleFormats类型对象,只添加CellFormats类型对象

GDI/GDI+用法总结

GDI是Graphics Device Interface的缩写,含义是图形设备接口,它的主要任务是负责系统与绘图程序之间的信息交换,处理所有Windows程序的图形输出。GDI+是在GDI基础上提供的一层更高级的图像绘制抽象接口,语义更明确调用更方便。它们都支持向图片对象或者窗口上输出图形。在窗口上绘图时它们都使用窗口提供的HDC句柄实现绘制;在图片对象绘制图像时,GDI+支持直接传入图片对象实现对图片的绘制,GDI需要先创建一个与图片兼容的HDC,再将HDC与被绘制图片进行绑定,然后才能在图片上进行绘制。
它们在用法上相似,区别主要有以下几个方面:

  • GDI不支持透明图片处理(AlphaBlend只能混合颜色,透明得由第三方库支持)
  • GDI不支持反锯齿(对于图片绘制线条、图像或拉伸等处理时,可能出现白色锯齿形状图像,影响美观)
  • GDI对于图片颜色处理具有很大优势。GDI+慢的一比
  • GDI是以C的接口形式提供接口,GDI+是以C艹和托管类的方式提供接口
  • 使用GDI+的程序在初始化后、程序关闭前需调用GDI+初始化、释放的代码
  • 从层次结构上来说,GDI+更好用

没有绝对的好与坏,可以根据需求来决定使用GDI或GDI+。下面开始详细教程:
需要使用GDI+首先需要以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <Gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
// 下面这句看个人爱好是否使用
using namespace Gdiplus;
 
 
// 在第一次使用GDI+对象前,调用以下代码:
ULONG_PTR gdiplusToken; // 这个变量需要保存下来
GdiplusStartupInput gdiplusStartupInput;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
 
// 在最后一次使用GDI+对象之后,调用以下代码:
GdiplusShutdown(gdiplusToken);

首先是在窗口上绘制一个图像,有两种实现思路。一种是拿到窗口绘图句柄就直接绘制,这种方式绘制的图像在窗口移动到屏幕边缘再移回来,那么移出屏幕部分就会消失,或者把窗口最小化再还原,那么绘制的部分也会被消失。实现代码类似如下:

1
2
3
4
5
// 根据是否需要裁剪非客户区或者如何裁剪,选择合适的函数获取HDC
HDC hdc = GetDC (hWnd); // GetDCEx、GetWindowDC
// 在此处编写绘图代码
// 对于获取的窗口DC,使用ReleaseDC释放;对于自己生成的DC,使用DeleteDC释放
ReleaseDC (hWnd, hdc);

另一种是在WM_PAINT事件里面绘制,这种绘制方式不会有以上那样的问题,也是个人比较推荐的方式。实现代码类似如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LRESULT CALLBACK WndProc (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
    case WM_PAINT:
        PAINTSTRUCT ps = {0};
        HDC hdc = BeginPaint (hWnd, &ps);
        // 在此处编写绘图代码
        EndPaint (hWnd, &ps);
        break;
    default:
        return DefWindowProc (hWnd, uMsg, wParam, lParam);
        break;
    }
    return 0;
}
 
// 需要刷新窗口时调用以下代码
RECT rect;
GetWindowRect (hWnd, &rect);
InvalidateRect (hWnd, &rect, TRUE);

对于直接绘制到界面上的代码,很可能出现闪屏的问题,另外多次的直接向界面绘制图像,效率也不高。推荐做法是使用双缓冲。具体实现思路是创建一个窗口大小的图片,首先向图片进行绘制,绘制完成后再将图片绘制到屏幕HDC上,可以提高效率,且避免闪屏的问题。GDI与GDI+窗口绘制的示例代码分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//
// GDI
//
 
// 创建内存兼容 DC
HDC hTmpDc = CreateCompatibleDC (hdc);
// 创建内存兼容位图
HBITMAP hTmpBmp = CreateCompatibleBitmap (hdc, width, height);
// 选定绘图对象
SelectObject (hTmpDc, hTmpBmp);
 
// 在此处编写绘图代码,绘制到 hTmpDc 设备
 
// 将图片绘制到屏幕上
BitBlt (hdc, 0, 0, width, height, hTmpDc, 0, 0, SRCCOPY);
// 释放内存兼容位图及内存兼容DC
DeleteObject (hTmpBmp);
DeleteDC (hTmpDc);
 
 
 
//
// GDI+
//
 
// 创建临时位图
Gdiplus::Bitmap tmpBmp (width, height, PixelFormat32bppARGB);
// 创建绘制临时位图所需 Graphics 对象
Gdiplus::Graphics tmpG (&tmpBmp);
 
// 在此处编写绘图代码,绘制到 tmpG 对象
 
// 创建窗口DC所需 Graphics 对象
Gdiplus::Graphics g (hdc);
// 将图片绘制到屏幕上
g.DrawImage (&tmpBmp, Gdiplus::Rect (0, 0, width, height),
    0, 0, width, height, Gdiplus::UnitPixel);
// C艹对象在函数结束时自动释放,此处就不需要编写释放代码咯

从代码形式上看,GDI+简洁太多了。

说说GDI+里面最坑的东西。对于像素点需要一个一个去计算的实现代码,用GetPixel、SetPixel简直慢的一比。网上对此也有很多实现思路,比如用GDI来代替实现什么的。但我还是推荐使用物理内存访问来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 图片对象
Gdiplus::Bitmap bmp (width, height, PixelFormat32bppARGB);
// 图片数据对象
Gdiplus::BitmapData bmpData;
// 锁定内存区域
bmp.LockBits (&Gdiplus::Rect (0, 0, width, height),
    Gdiplus::ImageLockModeRead, PixelFormat32bppARGB, &bmpData);
 
// 此时可以使用指针来读取图片内存区域。bmpData.Scan0 指向的就是图片数据区
// 图片像素大小受像素格式影响,此处一个像素就占4个字节
// 在这个图片里面读取数据,然后在另一个图片里面写入数据
 
// 解锁内存区域
bmp.UnlockBits (&bmpData);

接下来附一则常用代码:从资源加载 Image 对象的代码的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Gdiplus::Image *load_image_from_resource (LPCTSTR lpResName, LPCTSTR lpResType) {
    HMODULE hModule = GetModuleHandle (NULL);
 
    //搜索资源
    HRSRC hRsrc = FindResource (hModule, lpResName, lpResType);
    if (NULL == hRsrc)
        return nullptr;
 
    //获取资源大小
    DWORD dwSize = SizeofResource (hModule, hRsrc);
 
    //加载资源
    HGLOBAL hGlobal = LoadResource (hModule, hRsrc);
    if (NULL == hGlobal) {
        FreeResource (hGlobal);
        return nullptr;
    }
 
    //锁定资源
    LockResource (hGlobal);
 
    // 创建资源流
    LPSTREAM pStream;
    if (S_OK != CreateStreamOnHGlobal (hGlobal, true, &pStream)) {
        GlobalUnlock (hGlobal);
        FreeResource (hGlobal);
        return nullptr;
    }
 
    // 从资源流创建 Image 对象
    Gdiplus::Image *img = Gdiplus::Image::FromStream (pStream);
 
    GlobalUnlock (hGlobal);
    FreeResource (hGlobal);
 
    return img;
}

由于GDI函数不好查,下面就附一则不太全的 GDI 函数索引表,摘自百度百科:
设备上下文函数(如GetDC、CreateDC、DeleteDC)
画线函数(如LineTo、Polyline、Arc)
填充画图函数(如Ellipse、FillRect、Pie)
画图属性函数(如SetBkColor、SetBkMode、SetTextColor)
文本、字体函数(如TextOut、GetFontData)
位图函数(如SetPixel、BitBlt、StretchBlt)
坐标函数(如DPtoLP、LPtoDP、ScreenToClient、ClientToScreen)
映射函数(如SetMapMode、SetWindowExtEx、SetViewportExtEx)
元文件函数(如PlayMetaFile、SetWinMetaFileBits)
区域函数(如FillRgn、FrameRgn、InvertRgn)
路径函数(如BeginPath、EndPath、StrokeAndFillPath)
裁剪函数(如SelectClipRgn、SelectClipPath)

Transact-SQL基本用法整理

Sql Server在服务器开发中用的不是最多,但也属于主流数据库服务器之一。对于各种不同的数据库服务器,数据库查询语句有细微差别。这边文章用于做为T-SQL语句的整理,一方面列出各种用法作为参考,另一方面也为其他不同类型数据库使用提供另一种实现思路。

常用数据类型:

1
2
3
4
5
6
7
8
9
10
INT        --整数,不带宽度
FLOAT      --浮点数,不带宽度
DATE       --日期,不带宽度
datetime   --日期时间,不带宽度
CHAR       --字符,带宽度
NCHAR      --Unicode字符,带宽度
VARCHAR    --变长字符,带宽度
nvarchar   --变长Unicode字符,带宽度
DECIMAL    --定点数,带宽度
NUMERIC    --定点数,带宽度

除了常用类型外还有二进制流类型、Image类型等,因为用的非常少所以就不全部列举了。

5种约束:

1
2
3
4
5
PRIMARY KEY    --主键,指定列或多个列不允许重复,查询数据库默认以主键排序
UNIQUE         --唯一键,指定列或多个列不允许重复
CHECK          --检查键,插入的数据必须符合此条件
DEFAULT        --默认键。当插入数据时写入的值为default,那么值将被设置为默认键指定的值
FOREIGN KEY    --外键。指定键必须与其他表某列相关联。

估计玩数据库的朋友们最讨厌的就是外键了,清空表时各种删不掉,改外键值也导致各种关联错误。Sql Server在外键方面提供了两个限制语句极大方便了外键的操作。只需在创建表时写上这个语句即可实现功能:

1
2
ON DELETE cascade  --同步删除。删除其他表数据时,同步删除这张表的关联数据
ON UPDATE cascade  --同步更新。修改其他表关联的这个列的数据时,同步修改这个外键的数据

数据库作为数据存储的基本载体,使用文件的形式进行存放数据。Sql Server通常使用*.mdf、*.ldf两个文件来存放数据库的东西。mdf用来存储所有数据,ldf用来存储操作日志
数据库的增删改查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
--创建数据库
CREATE datebase 数据库名 ON
(
    name='数据库名_data',                              --数据库名定义
    filename='D:/MyDataBase/数据库名_data.dbf',        --数据文件存储位置
    SIZE=5mb,                                         --初始大小
    maxsize=20mb,                                     --最大大小
    filegrowth=10%                                    --文件大小递增方式
)
log ON
(
    name='数据库名_log',                               --数据库日志名定义
    filename='D:/MyDataBase/数据库名_data.dbf',        --数据日志文件存储位置
    SIZE=2mb,                                         --初始大小
    maxsize=5mb,                                      --最大大小
    filegrowth=1mb                                    --文件大小递增方式
)
 
--删除数据库
DROP DATABASE 数据库名
 
--重命名数据库:
EXECUTE sp_renamedb 数据库旧名, 数据库新名
 
--引用数据库:
USE 数据库名

表的增删改查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--创建表
CREATE TABLE [数据库名.]表名 (
    --列名      类型         其他1         INT          CONSTRAINT PK_表名 PRIMARY KEY    IDENTITY(1, 1),2         CHAR(10)     NOT NULL,
    CONSTRAINT2 DF_列2 DEFAULT ('abcd')
)
 
--删除
DROP TABLE [数据库名.]表名
 
--改名
EXECUTE sp_rename 旧表名,新表名
 
--查询所有表名
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE'

列的增删改查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
--增加
ALTER TABLE [数据库名.]表名 ADD 列名 INT
 
--删除
ALTER TABLE [数据库名.]表名 DROP COLUMN 列名[, 列名2]
 
--修改
EXECUTE sp_rename '[数据库名.]表名.旧列名', '新列名', 'COLUMN'          --修改列名
ALTER TABLE [数据库名.]表名 ALTER COLUMN 列名 NOT NULL                 --修改列属性
ALTER TABLE [数据库名.]表名 ADD CONSTRAINT 约束名 约束类型 约束表达式  --添加约束
ALTER TABLE [数据库名.]表名 DROP CONSTRAINT 约束名                     --删除约束
 
--查询
SELECT * FROM [数据库名.]表名 WHERE 1=2         --只使用列名就行

行作为最基本的数据存储单元。普通对于数据库的操作用的最多的都是行操作。
行的增删改查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
--增加
INSERT INTO [数据库名.]表名 VALUES ('列1值', '列2值')
 
--删除
DELETE FROM [数据库名.]表名 WHERE 条件
DELETE FROM [数据库名.]表名      --清空
TRUNCATE TABLE [数据库名.]表名   --清空
 
--修改
UPDATE [数据库名.]表名 SET2='列2新值' WHERE1='列1旧值'
 
--查询
SELECT1 AS1显示名,2显示名 =2 FROM [数据库名.]表名     --别名
SELECT1指标 =                                                  --临时改变
    CASE
        WHEN1 < 100 THEN '少'
        WHEN1 < 200 THEN '一般'
        WHEN1 >= 200 THEN '多'
    END
    FROM [数据库名.]表名
SELECT1,2, 临时列1 = (1*2) FROM [数据库名.]表名  --临时列
SELECT DISTINCT1 FROM [数据库名.]表名                   --不重复
SELECT top 50 * FROM [数据库名.]表名                       --前50行
SELECT top 15 percent * FROM [数据库名.]表名               --前15%行
 
--分页
SELECT top 10 * FROM 表名 WHERE 表名.某列 NOT IN (SELECT top 90 表名.某列 FROM 表名)

视图在简单的数据库使用中几乎不被用到,但它也有非常方便的地方比如修改视图可以直接同步进数据库。用来抽象数据表就非常方便了。
视图的增删改查:

1
2
3
4
5
6
7
8
9
10
11
--增加
CREATE VIEW 视图名 AS <SELECT语句>
 
--删除
DROP VIEW 视图名
 
--修改
ALTER VIEW 视图名 AS <SELECT语句>
 
--查询(当做一张表进行查询,可修改,可使用where等等几乎所有限定条件)
SELECT * FROM 视图名

存储过程是Sql Server中非常强大的一个功能,对于类似高并发的数据库服务访问,存储过程可以确保所得结果的正确性,对于出错过程自动回滚。
存储过程的增删改查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
--增加
CREATE PROCEDURE 存储过程名(          --创建存储过程,procedure可简写为proc
    @参数1 INT =0 output,              --变量可用于输出
    @参数2 nvarchar(10) ='默认数据'
)
AS
    DECLARE @变量1          --定义变量
    SET @变量1 = @参数1     --赋值
    print @参数1            --存储过程中输出参数1的值
 
CREATE PROCEDURE 存储过程名
WITH recompile              --不缓存
AS SQL语句
 
CREATE PROCEDURE 存储过程名
WITH encryption             --加密
AS SQL语句
 
--删除
DROP PROCEDURE 存储过程名
 
--修改
ALTER PROCEDURE 存储过程名 AS SQL语句
 
--执行
EXECUTE 存储过程名 参数1, 参数2      --调用存储过程,execute可简写为exec

自定义函数与存储过程类似,但它作用于另外一块,比如使用一个数据库语句几乎不可能完成的操作,使用自定义函数一步一步实现,就会非常方便。
自定义函数的增删改查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--增加
CREATE FUNCTION 函数名 (@参数名 参数类型, ...)
    RETURNS 返回值类型
    [WITH {Encryption | Schemabinding }]
AS BEGIN
    DECLARE @变量 变量类型
    SET @变量 = ...
    RETURN @变量
END
 
--删除
DROP FUNCTION 函数名
 
--执行
SELECT 函数名 (@参数名, ...)

触发器是一个比较神奇的东西。通常在某个表添加数据、修改数据时触发。对于一些已经完成的项目需要新加一些什么功能但又不想把代码改乱,那么使用触发器来实现就比较适合。
触发器的增删改查:

1
2
3
4
5
6
7
8
9
10
11
12
13
--增加
CREATE TRIGGER 触发器名
    ON 表名或视图名
    { FOR | after | instead OF } [INSERT, UPDATE, DELETE]
AS
    <SQL语句>
 
--删除
DROP TRIGGER 触发器名
 
--修改
ALTER TRIGGER 触发器名
    ON ...

以上整理只列出了基本的各种对象的操作,不包括任何高级用法。这篇文章用来作为对数据库使用方法的整理做速查用。对于学习此文的朋友们不要觉得此文看懂就是高手了。你们的数据库学习之路才刚刚开始(~ ̄▽ ̄)~

C++:时间

时间在计算机中,通常以三种形式存储,三种方式各有各的特点。
1、Unix时间戳
这个东西算是时间表示的元老级结构了,因为出现的早,所以用的也最广泛。原理是,一个32位的数字,代表从1970年1月1日到现在的秒数。由于最高位代表符号位未被使用,所以可以表示的最长的时间点为2038年左右。目前所有Unix及类Unix操作系统都使用这种方式表示时间。著名的水果1970变砖BUG就是因为这种时间表示方式的固有特性所导致。这种时间表示方式还有一个非常大的问题就是,不能计算闰秒。闰秒的含义是,因为地球绕太阳自转的速度越来越慢,导致地球公转一圈的时间更久。现代的人们感受不出来,但如果时间线放长,并且不闰秒,那么可以预见,未来某一天的正午12点是晚上,凌晨12点是白天。所以,在某个时间点的闰秒尤为重要。闰秒通常在某个分钟的变化时,秒数记为57、58、59、60、0、1、2……,也就是说,多出来一个第60秒。闰秒虽好,统一地球公转,为太阳系历法做贡献,但每次闰秒通常会造成几千万美元的经济损失,原因为,使用了Unix时间戳这种计时方法的操作系统,并不能区分闰秒,导致操作系统时间与世界时间不同,然后在金融等领域,一秒钟多计算了什么什么,少计算了什么什么,导致结果不是实际想要的。这时候计算误差所导致的经济损失累积起来,就有这么严重了。另外,这种计时方式最多只能计算到2038年,之后就无能为力了。如果这些操作系统迟迟不更新原子计时方式,那么,等待它们的,只有,系统罢工了。因为以上两个问题太严重了,导致人们常常忽略了它的第三个问题:精度太差。最高精度就是秒了,但对于计算机来说,计算很多东西精度常常需要达到毫秒才够用,有的甚至需要微秒级精度,所以这些地方也用不了这种时间表示方式。
基于这种计时方式,出现了一个分支,使用64位进行计时,这就不存在年份限制这BUG了,不过这分支用的比较少。
2、DATE时间类型
网上也有将其称作VB时间类型或者浮点时间类型,使用一个浮点数,代表从1899年12月30日凌晨到目标时间的天数。比如,1900-01-01 00:00:00表示为:2.0;1900-01-01 06:00:00表示为:2.25。这种事件类型有一个好处就是精度高,另外它也能代表1899年前的时间,使用负数表示。这种时间类型还有一个分支,代表着从1900年1月1日起至今的天数,也就是说,上一个事件类型的值-2就成了这种时间类型的值。这种事件类型通常不能用累加来实现时间增量,所以不会出现闰秒的问题,另外精度也够高。有一个小缺憾是,这种时间类型由于是浮点数,计算稍显麻烦,另外也不能代表时间增量,总的来说,比Unix时间戳好多了。所以用的也比较多。Win32平台内部很多地方都使用的这种事件类型。
3、结构化时间类型
这种计时方式是最笨的,同时也是最有效的。只要一个时间结构,年月日时分秒分别用不同的整型数字存放,那么都属于这种时间类型。这种时间类型的分支也特别多,有的全部用int存储,有的月份用4位、日用5位来存储;有的月份范围为1~12,有的月份范围为0~11;有的精度只有秒,有的甚至可以存放微秒,由于分支特别多,所以在不同的分支上进行转换时,需要特别注意按照规定来。这种时间类型好处都有啥,谁说对了金坷垃送给他。

好咯。基本的时间存储方式说明白了,那么开始讲解代码。
1、计算时间差
有时候,需要计算一段代码的执行时间,比如多少多少毫秒,这时候如果用完整的时间类型通常大材小用了。代表时间差有两种便捷的方式,一种是使用Win32的GetTickCount函数,返回一个DWORD的时间。只需要在需要统计时间的代码前和后面分别调用它,相减,就是时间差的毫秒数。关于这个函数的实际含义,msdn上面说返回系统启动之后所经过的时间的毫秒数。我开始还以为是开机时间,然而经我测试后,结果换算了一下大约为10天左右,刚好这电脑有很长时间没用了,计算BIOS时间的电源也断了,大概在10天前开了一下,所以,我猜测,如果没有我那种完全放电的话,这个应该代表BIOS第一次加电时至今的毫秒数,如果经过断电,那就为上一次加电至今的毫秒数。示例代码如下:

1
2
3
4
5
6
7
8
DWORD d = ::GetTickCount (); // 计时开始
 
// 一大串需要计时的代码,或者直接。。。
::Sleep (1000); //暂停一秒钟
 
// 计时结束
d = ::GetTickCount () - d;
// 这时候的d里面所存储的就是时间差的毫秒数

2、暂停一段时间
大型任务为避免持续占CPU时间,通常需要休息一段时间,让出CPU时间片给其他线程。暂停的方式除了上面Win32提供的Sleep函数外,还有一种是C++11所提供的,示例代码如下:

1
2
3
4
5
6
7
#include <thread>
#include <chrono>
 
// ...
 
std::this_thread::sleep_for (std::chrono::seconds (2));       // 暂停2秒
std::this_thread::sleep_for (std::chrono::milliseconds (10)); // 暂停10毫秒

3、C语言时间结构
C语言时间结构通常有两种类型,一种是time_t,一种是tm。time_t在32位或64位开发环境下,所代表的也分为两种不同大小的时间结构。定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef _TIME32_T_DEFINED
typedef _W64 long __time32_t;   /* 32-bit time value */
#define _TIME32_T_DEFINED
#endif  /* _TIME32_T_DEFINED */
 
#ifndef _TIME64_T_DEFINED
typedef __int64 __time64_t;     /* 64-bit time value */
#define _TIME64_T_DEFINED
#endif  /* _TIME64_T_DEFINED */
 
#ifndef _TIME_T_DEFINED
#ifdef _USE_32BIT_TIME_T
typedef __time32_t time_t;      /* time value */
#else  /* _USE_32BIT_TIME_T */
typedef __time64_t time_t;      /* time value */
#endif  /* _USE_32BIT_TIME_T */
#define _TIME_T_DEFINED         /* avoid multiple def's of time_t */
#endif  /* _TIME_T_DEFINED */

貌似。与目标环境配置无关,只要是在64位环境编译,就算目标生成类型为32位应用程序,那么time_t也是64位。
然后是tm这种结构,定义如下:

1
2
3
4
5
6
7
8
9
10
11
struct tm {
    int tm_sec;     /* seconds after the minute - [0,59] */
    int tm_min;     /* minutes after the hour - [0,59] */
    int tm_hour;    /* hours since midnight - [0,23] */
    int tm_mday;    /* day of the month - [1,31] */
    int tm_mon;     /* months since January - [0,11] */
    int tm_year;    /* years since 1900 */
    int tm_wday;    /* days since Sunday - [0,6] */
    int tm_yday;    /* days since January 1 - [0,365] */
    int tm_isdst;   /* daylight savings time flag */
};

不太节省内存空间哪,4位足够代表月份了,但足足用了一个int。也罢,现在计算机内存这么大,浪费这点也没什么。
以下代码示例基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 获取当前时间,实际代表的值为本地时间
time_t t = time (NULL);
 
// 将当前时间转为格林威治时间
//tm *t2 = gmtime (&t);
 
// 将当前时间转为本地时间
tm *t2 = localtime (&t);
 
// 输出当前时间
printf ("%4d-%02d-%02d %02d:%02d:%02d", t2->tm_year + 1900, t2->tm_mon + 1, t2->tm_mday, t2->tm_hour, t2->tm_min, t2->tm_sec);
 
//// 下面这段代码效果同上一行代码完全一样,写法稍有不同
//char cBuf [64];
//strftime (cBuf, 64, "%Y-%m-%d %H:%M:%S", t2);
//printf (cBuf);
 
// 将tm时间结构转回time_t
t = mktime (t2);

然后是计算时间差。由于time_t精度为秒,所以无法计算秒以下的单位。示例代码如下:

1
2
3
4
5
6
time_t t = time (NULL);
std::this_thread::sleep_for (std::chrono::milliseconds (1234));
time_t t2 = time (NULL);
 
// 计算时间差
printf ("%lf", difftime (t2, t));

结果为1.0000000,说明单位就是秒。但返回类型是双精度浮点型。估计这个小数没任何卵用。
4、C++11时间结构
使用前需包含头文件chrono。为了同C语言兼容,所以C++11时间也可以直接与time_t互转

1
2
3
4
5
6
7
8
// 获取当前时间
std::chrono::system_clock::time_point tp = std::chrono::system_clock::now ();
 
// C++11时间转C语言时间
time_t t = std::chrono::system_clock::to_time_t (tp);
 
// C语言时间转C++11时间
tp = std::chrono::system_clock::from_time_t (t);

除了必要的转换外,C++11一般格式化时间也是首先转为C语言时间结构体然后进行打印。然后同样是计算时差,这个时间结构稍微高级一些,精度可以达到纳秒(1e-9),示例代码如下:

1
2
3
4
5
6
7
std::chrono::system_clock::time_point t = std::chrono::system_clock::now ();
std::this_thread::sleep_for (std::chrono::seconds (2));
std::chrono::system_clock::time_point t2 = std::chrono::system_clock::now ();
 
// 纳秒级精度
n = std::chrono::duration_cast <std::chrono::nanoseconds> (t2 - t).count ();
// 将以上代码的 nanoseconds 替换成 seconds、milliseconds、microseconds 就分别代表着秒级精度、毫秒级精度、微秒级精度

另外c++11时间的输出如果需要带毫秒那稍微需要转一个弯,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::string format_time ()
{
    char buf_time [32], buf_time2 [32];
    buf_time [0] = buf_time2 [0] = '\0';
    auto time_now = std::chrono::system_clock::now ();
    auto duration_in_ms = std::chrono::duration_cast<std::chrono::milliseconds>(time_now.time_since_epoch ());
    auto ms_part = duration_in_ms - std::chrono::duration_cast<std::chrono::seconds>(duration_in_ms);
    time_t raw_time = std::chrono::system_clock::to_time_t (time_now);
    tm local_time_now;
    _localtime64_s (&local_time_now, &raw_time);
    strftime (buf_time2, sizeof (buf_time2), "%Y-%m-%d %H:%M:%S", &local_time_now);
    //char *xx = std::put_time (&local_time_now, "%Y-%m-%d %H:%M:%S");
    _snprintf (buf_time, sizeof(buf_time), "%s.%03d", buf_time2, ms_part.count ());
    return buf_time;
}

5、MFC/ATL时间结构
主要就是CTime和COleDateTime,后者使用前需包含ATLComTime.h头文件。这两者非常相似所以放在一起说;另外这个放在C++11之后不代表比C++11的更高级,我个人喜欢用标准的东西,但很多大型MFC项目用的基本都是这个,所以有必要说说。基本用法挺相似:

1
2
3
4
5
CTime t = CTime::GetCurrentTime ();
printf (t.Format ("%Y-%m-%d %H:%M:%S"));
 
COleDateTime t2 = COleDateTime::GetCurrentTime ();
printf (t2.Format ("%Y-%m-%d %H:%M:%S"));

它俩构造函数比较相似,但COleDateTime功能稍强

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CTime () throw ();
CTime (__time64_t time) throw ();
CTime (int nYear, int nMonth, int nDay, int nHour, int nMin, int nSec, int nDST = -1);
CTime (WORD wDosDate, WORD wDosTime, int nDST = -1);
CTime (const SYSTEMTIME& st, int nDST = -1);
CTime (const FILETIME& ft, int nDST = -1);
CTime (const DBTIMESTAMP& dbts, int nDST = -1) throw ();
 
COleDateTime () throw ();
COleDateTime (const VARIANT& varSrc) throw ();
COleDateTime (DATE dtSrc) throw ();
COleDateTime (__time32_t timeSrc) throw ();
COleDateTime (__time64_t timeSrc) throw ();
COleDateTime (const SYSTEMTIME& systimeSrc) throw ();
COleDateTime (const FILETIME& filetimeSrc) throw ();
COleDateTime (int nYear, int nMonth, int nDay, int nHour, int nMin, int nSec) throw ();
COleDateTime (WORD wDosDate, WORD wDosTime) throw ();
COleDateTime (const DBTIMESTAMP& dbts) throw ();

可见CTime构造是相当受限制的,COleDateTime构造除了有以上内容外,还可以与VARIANT类型、DATE类型(浮点时间类型)互转,甚至time_t也区分了32位与64位。
虽然这俩功能还行但毕竟是M$大大的东西,推荐还是使用C/C++标准写法,兼容性更强,并且如果以后项目需要迁移至其他平台,这种写法也更方便,几乎不用修改代码。

C++:字符串编码与字符串

1、编码
在讲字符串之前首先说说编码方式。字符串在程序用用数据类型进行存储,同时数据类型存储的也可以是不同编码方式的字符串。总的来说,常用编码方式有以下几种:
ASCII:最古老的编码方式,只使用后7位,可以存储英语大小写、数字及几乎所有常用半角符号。
ISO-8859-1:西欧地区使用的编码方式,兼容ASCII码,在最高位为1时用于描述西文符号。
GB2312/GBK/GB18030:这个是天朝用户专用编码方式,兼容ASCII码,对于英文字符使用1字节进行存储,对于中文使用2字节进行存储,同时两个字节的最高位均为1。值得注意的是,GB2312在Win32开发中常常被称作Ansi编码;其次,GBK为GB2312的扩充,GB18030为GBK的扩充。以前它们是不同的编码方式,但现在也没有严格的划分,通常三者代表同一种编码方式。
BIG-5:也是天朝用户专用编码方式,兼容ASCII码,与GB2312不同的是,它只能编码繁体字,不能编码简体字。
UTF-16/UCS-2:这两个名称所代表的是同一种编码方式,使用两个字节来存储一个中文字符或者一个字母,不兼容ASCII码。这种编码方式也划分为两种不同的子编码方式,分别为UCS-2 Big Endian与UCS2 Little Endian。常说的UTF-16或者UCS-2通常指的是UCS-2 Big Endian。这两种子编码方式的区别为,Big Endian高字节在前,低字节在后;Little Endian低字节在前,高字节在后。这种编码方式在Win32开发中常常被称作Unicode编码,但它属于一种误称;另外,这种编码方式有点浪费存储空间,并且也不能描述世界上所有的符号,相比其他编码,唯一优势是,字符串长度就等于字符个数。
UTF-8:使用最广泛的编码方式,没有之一!几乎所有的网页、XML描述文件、Json数据文件、大多数数据库以及Linux系统均使用的编码方式,相比而言GB2312、UTF-16只有在Windows平台用用而已,仗着Windows平台用的人多,所以也作为常用的编码方式,对于英文字符使用1字节进行存储,因此兼容ASCII编码;它同时也能编码世界上所有的文字,对于汉字而言这种编码方式使用3个字节进行存储,但理论上可以使用2、3、4、5或6字节来编码一个特定字符。
UTF-32/UCS-4:由于UTF-16不能编码所有的编码方式,但发明这编码的人不服,爱搞事,所以发明了4个字节来编码一个字符的编码方式,理论上可以描述世界上所有的字符,但由于一个字母都需要4个字节,过于浪费存储空间,所以这种编码方式几乎没有人使用。
以上是需要了解的编码方式,除了上面几个之外,不同地方也有他们自己的编码方式,以下为不完全统计:
西欧语系:ISO-8859-1
东欧语系:ISO-8859-2
土耳其语:ISO-8859-3
波罗的语:ISO-8859-4
斯拉夫语:ISO-8859-5
阿拉伯文:ISO-8859-6
希腊文:ISO-8859-7
希伯来文:ISO-8859-8
日文:Shift-JIS
韩文:EUC-KR
……

2、C语言中的字符串
编码方式就不细究了,这个讲不完。接下来进入正题:字符串的处理
C++中的字符串基于C语言的字符串,所以先说说C语言中的字符串。
以上几种常用编码方式,,有点多了,我重点只说说三种:
2.1、GB2312编码
使用char*或者char[]来存放一个字符串,Win32开发中以大写字母A结尾的API或者C语言自带函数均为这种编码方式的字符串。比如strlen(C语言获取字符串长度用的API)、MessageBoxA(Win32用于显示提示框的API)。通常使用双引号 "" 来表示一个字符串。
2.2、UTF-16编码
使用wchar_t*或者wchar_t[]来存放一个字符串,Win32开发中以大写字母W结尾的API使用这种编码方式的字符串。比如MessageBoxW(Win32用于显示提示框的API)。通常使用L+双引号的形式 L"" 来表示一个字符串。
2.3、在讲第三种编码方式前,重点说说以上两种编码方式
首先,Win32中API定义了很多字符串数据类型:LPSTR、LPWSTR、LPCSTR、LPCWSTR、LPTSTR、LPCTSTR……等等等等,看起来好复杂的样子……其实原理非常简单,下面我说说这个的原理:
字符串以P或LP开头,以STR结尾。STR的意思大家都懂吧,字符串的英文string的简写;P的意思是“指针”,Pointer,学过C语言的应该都知道指针的含义。LP的写法源于历史原因:在计算机只有16位的时候,惜KB如金的年代,指针占两个字节有些浪费,所以,16位的系统通常有两种指针,一种为短指针short pointer,占1字节,代表偏移量-128~127,另一种为长指针long pointer,占2字节,用于指示内存中一个固定区域。然后计算机到达32位,不再那么需要节约一两字节内存,所以指针都以4个字节表示,但同时也继承了程序猿的习惯,有P与LP两种写法,都代表同一个含义。
对于最基本的字符串类型LPSTR,表示的数据类型为char*;然后,P/LP与STR之间,一般有三个字母,T、W与C。C代表const,比如LPCSTR的实际类型为const char*;W代表UCS-2编码方式字符串,比如LPWSTR代表wchar_t*;T代表这个类型根据宏来动态解释。比如LPTSTR如果定义了UNICODE宏,那么代表wchar_t*类型;反之代表char*类型。这种写法有一个好处就是,同一套代码可以生成两种不同编码方式的程序,也是推荐的写法。
对于字符串来讲,GB2312与UCS-2使用不同的写法,这时候可以引用一个头文件tchar.h,然后,统一用_T ("")的写法,那么就可以在Unicode或Ansi被分别解析了。
如果不想使用这头文件也可以自己定义一个:

1
2
3
4
5
#ifdef UNICODE
#define _T(str) L##str
#else
#define _T(str) str
#endif

两个#号代表连接前后两个符号,因此Unicode环境下_T ("")写法与L""写法没有区别。
然后是常用字符串函数,比如字符串复制,推荐使用lstrcpy写法,它在有无UNICODE宏的情况下也是分别解析为lstrcpyA与lstrcpyW,非常方便。常用字符串函数中有一个坑就是_s结尾的函数。Windows那帮人认为C语言的很多东西不安全,于是自己改写了一堆库,使用_s结尾,具体含义为,如果以_s结尾的API,那么对于字符串参数来说,都需要传入一个整数代表字符串的长度。如果有这方面的“安全”需要,那么函数都加上_s,否则,定义一个如下的宏:

1
#define _CRT_SECURE_NO_WARNINGS

定义了这个宏之后,就不会报那种编译错误了。
2.4、UTF-8编码
这个就麻烦了,Linux平台还好,常用字符串就是UTF-8格式,Win32平台就麻烦了,既不能直接定义,也不能直接使用。比如需要访问一个网页,下载下来的内容直接显示的话绝壁是乱码。这个就涉及到编码转换了。在 Windows下编码转换 这篇文章中有提到编码的转换,转为Win32通用编码方式后才能显示。
如果需要直接定义的话,那么通常方式是定义以下宏:

1
#pragma execution_character_set("utf-8")

然后,所有定义""这种风格的字符串的编码方式全部为UTF-8了。值得注意的是,这种编码使用char*存储,适合用C风格字符串函数比如lstrcpyA、lstrcatA等等,但不适用于Windows函数,比如MessageBoxA,如果传入UTF-8编码字符串并且带中文那么一定会乱码。

3、说完了C语言的字符串,接下来说说C++中的字符串
Windows上C++字符串通常分三种,标准C++字符串,MFC字符串与COM+字符串。
3.1、标准C++字符串,用std::string或std::wstring表示。由于没有通用的宏,所以通常使用前,用户会自定义一个宏来代表字符串。比如:

1
2
3
4
5
#ifdef UNICODE
typedef std::wstring string_t;
#else
typedef std::string string_t;
#endif

当然,如果涉及到其他比如文件操作或者字符串流操作,还得分别定义,不过胜在“标准”。
这种字符串还有一个问题就是出现时间很早,所以很多通用的简写都没有,比如格式化一个字符串,还得调用C的库sprintf等,所以,在下收集了一些网上的轮子,做成了一个简单的字符串处理库 C++中std::string实现trim、format等函数,可以实现format等操作。
3.2、MFC字符串,也就是大众所熟知的CString,这个类分两个版本,CStringA与CStringW,分别对应Ansi与Unicode环境。封装了format等简单的操作,但有个问题就是,MFC由于近期更新太少,属于接近被淘汰的库,但以前很多比较老的大型项目都在用,并且跨版本不兼容。所以这就有点尴尬了,同样是M$大大的东西,VC6上的代码还不能直接放在VS上编译。这东西,能不沾就尽量别沾吧,以免升级环境时麻烦。
3.3、COM+字符串。众所周知M$大大习惯一个系统分很多开发组然后让他们互相竞争,这就给开发者造成了一个麻烦比如COM+上面的所有字符串都不是上面常见的字符串。比如_bstr_t格式字符串。不过还好这种字符串支持直接转换char与wchar_t字符串类型。它内部使用BSTR格式来存储字符串,所谓BSTR实际上就是wchar_t*。然后是COM+操作的接口,很多是变体类型:VARIANT或者_variant_t类型,后者好说,是一个类可以自由转换,前者就稍微有点麻烦了,需要手工判断类型然后分别处理。

4、没想到吧这儿还有一小节,用来讲传说中的System::String
C++/CLR是一个奇特的存在,诞生于微软的.NET战略,用于将C#、VB.NET等语言编译为IL中间语言,但通用框架怎么能少了C++?另外C++也无法直接进入托管环境,所以微软强推了一种新的C++托管框架,名为C++/CLR,或者C++/CLI,或者C++.Net,反正都代表一个框架。这个框架还是挺好用的,从C++开发者角度来说,可以随时进入托管环境调用托管库,也可以随时调用非托管代码,相当方便的框架,由于语法比较奇特,所以被一些不懂C++的人喷。所以,下次如果遇到有人喷C++/CLR,那么说明他不懂C++,准确度超过90%,哈哈~
托管字符串基本语法为:

1
System::String ^str = gcnew System::String(L"aaa");

由于托管字符串使用了托管的指针,与C++标准指针不兼容,所以使用^符号来声明托管指针;其次托管里面只有指针不能直接定义字符串;另外托管的new和delete与C++中的不兼容,所以M$大大创造了两个关键字gcnew与gcdelete,用于托管指针的分配与释放;最后,托管字符串内部是使用UCS-2编码进行存放,所以传递C风格字符串前最好传入UCS-2编码字符串。
托管字符串的使用算是比较简单的,除了奇特的托管语法外,其他都和C#用法一样,不明白的童鞋看看C#教程也就懂了托管的用法了。
还有比较重要的一点是,托管字符串与非托管字符串之间的互相转换:

1
2
3
4
5
6
7
8
9
10
11
#include <vcclr.h>
 
//……
 
// 托管转非托管
System::String ^clrstr = gcnew System::String (L"aaa");
pin_ptr<const wchar_t> wch = PtrToStringChars (clrstr);
wchar_t *wstr = wch;
 
// 非托管转托管
clrstr = gcnew System::String (wstr);

原因讲过,由于CLR内部使用wchar_t字符串结构存储,所以只能与UCS-2编码互转,如果需要与GB2312或者UTF-8编码互转,那么依旧是参考 Windows下编码转换 这个~

C++11:&&右值引用、std::ref与std::unique_ptr

非常容易混淆的三种语法,在新手看来含义似乎相同,都是在函数调用上参数的传递,但实际上有非常大的区别。下面我们来具体分析分析。
首先是&&右值引用,在说这个之前先得弄清什么是右值。右值也叫将亡值(这翻译简直(¬_¬)),顾名思义也就是说将会死亡的值。比如类似这样的代码:

1
std::string s = std::string("abc");

从原理上来说,这句代码的含义是,首先在等号右侧构建一个临时字符串对象,然后将右侧对象执行拷贝构造,拷贝至变量s中,然后释放等号右侧对象(实际的实现中,编译器通常会对这样的代码进行优化,直接在变量s中构建字符串对象)。
如果上面的例子不太明显那么再来一个例子,Gdi+中经常会出现这样的代码:

1
2
Gdiplus::Graphics g(&img);
g.DrawImage(&img2, &Gdiplus::Rect(0, 0, 10, 10), ...);

似曾相识的代码对吧?这儿的DrawImage函数在执行前首先构造匿名Gdiplus::Rect对象,然后作为参数传递,函数调用返回时,也就是这行代码执行完时,匿名对象将被释放。这也是它被叫做将亡值的原因。
既然这种对象在使用时生命周期短,拷贝至左值又可能会影响效率,那有没更好的引用方式呢?当然有,这叫右值引用。

1
2
3
4
5
6
7
8
9
#include <iostream>
 
int main (int argc, char* argv []) {
    int &&i = std::move (1);
    std::cout << i << std::endl;
    i = 2;
    std::cout << i << std::endl;
    return 0;
}

代码运行结果为1、2。原理就是,通过std::move移动语义,将右值“1”的寿命延长,在当前行执行完之后还可以继续使用,寿命在引用变量释放时结束。右值引用也可以传递,不过需要注意的是,一旦传递之后,原始变量就不能使用这个对象了。
另外需要注意的是,移动语义的引用不能放在当前函数块之外继续使用。比如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <string>
using namespace std;
 
class A {
public:
    A () { cout << "new object" << endl; }
    ~A () { cout << "delete object" << endl; }
};
 
A&& func () {
    return move (A ());
}
 
int main (int argc, char* argv []) {
    A&& a = func ();
    cout << "function end" << endl;
    return 0;
}

代码执行结果为:
20161030125609
由此可见,对象虽然返回了右值引用,但在进入main作用域前就已经被释放掉了。所以这东西在栈内存上酌情使用。
下面我说说std::ref这个东西,顾名思义,引用,不过这个东西和普通的参数引用有什么区别呢?

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <functional>
 
int main (int argc, char* argv []) {
    auto f = [] (int &i) { i += 1; };
    int x = 1;
    auto f2 = std::bind (f, x);
    f2 ();
    std::cout << x << std::endl;
    return 0;
}

如果遇到了这样的代码,可能有些人就完全找不着北了。lambda使用引用参数,然后将其与变量x绑定,讲道理结果应该为2啊,但很神奇的,结果为1。产生这个神奇现象的原因是,std::bind内部构造的问题。std::bind通常作用域多线程领域,假如需要一个std::string参数,传递的如果不是左值,那么很容易导致程序崩溃。这时候就将其拷贝一份,虽然牺牲点效率但总比出现让人摸不着头脑的bug要好。
那对于引用参数绑定如何传递呢?解决方法是使用std::ref关键字,对其引用传递,将std::bind那行代码改为如下形式:

1
auto f2 = std::bind (f, std::ref (x));

这样之后,变量就能引用传递了。不出所料程序运行结果为2。
最后剩下的这个,智能指针,它主要用于自动回收内存,具体实现参考 C++11:智能指针

C++:调用协定

调用协定这个东西涉及到C语言函数实现的原理,大家都知道,调用函数时,保存当前执行环境,跳转到目标函数指令地址执行函数代码,执行完成后恢复调用前的执行环境。但实现的方式这么多,甚至可能不同的语言提供的接口有不同的方式,如何实现与目标函数相同的方式来调用呢?调用协定因此诞生。
可能有人觉得,这东西我从没听说过,但我写的C艹代码一样没问题,有必要研究吗?说实话这东西还是有必要的,主要在不同框架、语言间使用。比如BCB主要用__fastcall实现函数调用,C语言自身默认使用__cdecl实现函数调用,Windows平台又以__stdcall作为主要函数调用方式,而.NET Framework托管平台又以__clrcall实现函数调用,它们之间如何互相调用就是个问题。
Win32平台上可用函数调用协定有以下几种,后面的为别名:
__cdecl : _cdecl 、 cdecl 、 CDECL 、 WINAPIV
__pascal : pascal 、 PASCAL
__stdcall : WINAPI 、 APIENTRY 、 CALLBACK 、 APIPRIVATE
__fastcall
__clrcall
可能因为操作系统版本不同,导致函数定义有些许差异,在Win8.1上,__pascal已经被定义成了__stdcall,但并不妨碍我们对调用协定的学习。上面的关键字看不懂?没关系,我们一一来验证它们的作用。

1、__cdecl调用协定

首先__cdecl,C语言默认调用协定。首先我们先定义一个这样的函数,然后对其进行调用。

1
2
3
4
5
int __cdecl func_cdecl (int a, int b, int c) {
    return a + b + c;
}
//...
func_cdecl (1, 2, 3);

调用协定的定义方式为,将名称写在返回类型与函数名之间。写完之后,通过对齐进行Disassembly,显示如下结果:
20160910230845-cdecl
20160910230845-cdecl-call
我们在调用函数时,push了三个参数,push一次esp寄存器会减0x4,那么我们push了三次应该是减0xC,可以注意函数反汇编的最后一行,ret直接返回了,并没配平栈,反而是调用者执行了add esp, 0Ch,意味着调用者配平栈。为啥会这样呢?这个涉及到C语言的变长参数。比如printf函数,参数数量并不固定,甚至写上"%d"的格式化字符串之后,后面跟上十几个int类型数据也会成功运行。这儿我声明一点:函数代码并不能确定调用者传入了几个参数。因此,只有调用者知道自己传入了多少参数,所以这儿只能交由调用者自己来配平栈。
可能有人会问:为啥需要配平栈?不配平貌似也没啥问题啊。。。你可能忽略了一点,如果只push,不配平,那么调用了N次代码之后,栈顶指针只减不增,将会很快使用完所有内存,并且正在使用中的栈内存与没被使用的栈内存完全无法区分开,没有任何方法安全释放栈内存,因此需要配平。
首先我先说说栈的原理:栈在内存中,基地址(ebp)为高位,每push一次,栈顶指针(esp)减0x4;每pop一次,栈顶指针加0x4,如果栈顶指针等于基地址,那么代表栈中没有元素,同理,基地址与栈顶指针的差值代表栈中所存放的数据的总大小,其次栈顶指针始终小于等于基地址。从这儿可以看出,栈的生长方向为,由高地址向低地址生长。网上的一些不科学的砖家说什么栈的生长方向为向上向下啥啥啥生长,完全是误导。
另外我在说明汇编代码执行的原理: call func_cdecl 这句代码执行方式大致为,首先保存当前指令指针(IP)地址,有时候会保存当前环境比如正在使用的其他寄存器的值等等,然后跳转到函数所在地的地址,函数里面首先执行 push ebp 和 mov ebp, esp 两句代码,含义为保存栈基址指针,然后将当前栈顶指针的值移动至栈基址寄存器。然后一个 sub esp, xxx 代表将栈顶指针减多少多少,意味着当前函数分配了多大的栈空间,通常定义了一些局部变量时,就是在这儿减去相应大小就可以了。我这儿并没声明局部变量,另外这是Debug模式,减了这么多我不知道怎么解释,你们就理解为,Debug模式在检查栈是否配平时的需要,就行了(估计并不是这样)。
然后是 push ebx 、 push esi 、push edi ,这三句代码的含义为,保存当前执行环境。比如我们正在使用 ebx 寄存器,然后调用了一个函数,并且希望函数调用完成之后寄存器的值依旧为原来的值;但如果每次调用函数就保存所有寄存器的值那太影响执行速度,因此一个不太标准的调用方式是保存 ebx 、 esi 、 edi 三个寄存器的值。写过纯汇编代码的人估计也通过定义函数的伪代码知道一般保存这几个变量的值,同时也知道需要保存几个值完全可以自定义,一个都不保存,或者一次性保存所有寄存器的值,都没问题。
下面一小段是Debug专用代码,效果为,打印未初始化的字符串,显示为“烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫……”而不是随机字符,方便调试时找出问题。
然后是函数的返回值。对于一个基本类型数据,返回值用eax变量存放,因此上面代码中在eax里面计算完这条代码后,就不管了。
然后是三个pop,恢复函数执行前的环境。
再然后是 mov esp,ebp 和 pop ebp 两句代码,用于恢复栈环境。
最后是ret,有时候后面会跟上一个数字,代表先 pop eip ,然后加上跟的这个数字。
还是不太懂?没关系,下面我再贴一个寄存器调试窗口。首先,我再调用函数之前查看寄存器的值,可以看到esp值为00DBF7BC(这个地址可能每次执行时都不一样,不要介意为啥和我的不一样):
20160911010200-reg-pre
然后,执行了三句 push 指令之后的寄存器的值:
20160911010200-reg-incall
可以看到eip与esp的值改变了。eip代表当前代码执行位置,32位系统每句push指令占两个字节,因此当前执行地址跟随了三句push之后,加上了0x6;esp嘛,执行push改的就是esp,32位系统上每个int类型占4字节,三次之后,esp减去0xC,因此值也变为了上面这个结果。然后我们执行call指令:
20160911010200-reg-after
由于跳转到另外一个地址因此eip变动的有点大;其次esp也减去了4,这4字节内存存放的即为调用函数前的eip值,用于在函数返回时,能找到调用者的位置。

2、__pascal调用协定

然后,由于在本机上__pascal调用协定被定义为__stdcall形式,与我所理解的__pascal不同,因此这儿不再解释,关于实现可以参考下面__stdcall调用协定。

3、__stdcall调用协定

1
2
3
4
5
int __stdcall func_stdcall (int a, int b, int c) {
    return a + b + c;
}
//...
func_stdcall (1, 2, 3);

函数与调用者的反汇编代码如图:
20160910231043-stdcall
20160910231043-stdcall-call
与__cdecl大同小异,相同的部分不再重新说明,我只说说不同的部分。首先是调用者在call之后并没配平栈,其次是函数在ret指令后面跟的有0Ch。含义为,调用者只管push一堆参数,并不管栈是否配平,配平栈的任务交给函数来完成。因此,你们是否发现了,Win32API里面没有边长参数API,就是这个原因。

4、__fastcall调用协定

这是一个神奇的调用协定,它将第一个参数与第二个参数分别放在 ecx 、 edx 两个寄存器中传递给函数,优点是执行速度更快,缺点是实现这种调用协定稍微要复杂一些。反汇编实现如下:
20160910230930-fastcall
20160910230930-fastcall-call
可以看到,函数内部将两个参数存放至栈空间,然后在函数结束时 ret 指令后面只跟了4。

5、__clrcall调用协定

这种调用协定我就不写代码了。这个主要用于在CLR托管平台,几个别名为 VC++.NET 、 C++/CLR 、 C++/CLI ,经测试,这种调用协定实现方式几乎与__fastcall相同。主要用于以下情况:比如非托管平台通过托管函数指针调用托管平台代码,那么需要通过定义 __clrcall 调用协定的函数指针。有一点比较意外的情况,如果将项目设置为托管项目,那么之前定义的几个函数全部变为了托管调用协定,这点让我很意外啊,。。

C++11:tuple元组类型

这种类型可以简单理解为:不需要typedef的自定义结构体类型。有时候需要传递、返回一些结构体类型,但其他地方根本用不上这种结构体,那么就不必用typedef了,可以避免公开一些内部结构体名称,更加符合软件工程思想,另外还能少些一大堆代码。
同时,STL中的结构体map中,需要用到的类型std::pair,就可以将其理解为std::tuple
废话不多说,上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <tuple>
using namespace std;
 
int main (int argc, char* argv []) {
    //打包一个tuple
    tuple<int, int, char> c = make_tuple (1, 5, 'c');
    //结构体大小
    cout << sizeof (c) << endl;//结构体大小,结果为12
    //获取tuple中的某个元素
    cout << get<1> (c) << endl;//获取第1个元素(从0开始),值为5
 
    //tie解包,ignore为忽略
    int a, b;
    tie (a, b, ignore) = c;//此时a、b的值分别为1、5
    cout << a << '\t' << b << endl;
 
    return 0;
}

同时,这种随意的数据类型也有自定义结构体的特性,比如传参或返回时,直接传递即为拷贝,通过指针或右值可以减小拷贝开销;另外,tuple也能嵌套使用,使用方法大同小异。

C++:偏移类型

地址偏移类型在C艹中算是偏冷门的技术,但在特定场合下可以节省大量的代码从而实现需求。这种类型的定义为:类或结构体中的数据成员相对于基址的偏移量。比如,一个类里面成员a地址相对于类基地址偏移量为4;成员b地址相对于类基地址偏移量为8等等。
示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;
 
class A {
public:
    int a, b, c;
    A () : a (3), b (5), c (7) {}
};
 
int main (int argc, char* argv[]) {
    int (A::* x) = &A::a;
    int A::* y = &A::b;
    using TZ = int (A::*);
    TZ z = &A::c;    
 
    A *p = (A*)nullptr;
 
    cout << "地址偏移与偏移量值:" << endl;
    cout << &(p->a) << '\t' << &(p->*x) << endl;
    cout << &(p->b) << '\t' << &(p->*y) << endl;
    cout << &(p->c) << '\t' << &(p->*z) << endl;
 
    cout << "直接访问的结果:" << endl;
    cout << x << '\t' << y << '\t' << z << endl;
 
    return 0;
}

代码含义为:首先创建一个类,类里面有三个int类型成员属性,然后在main函数里面写了三种生成地址偏移类型的方法,其中第三种为首先构造地址偏移类型,然后用类型来定义。从经验上看,很容易看出偏移类型为0、4、8,实际运行结果也确实如此。
本机运行结果如下:
20160910151508
直接访问的结果有点出人意料,按道理说也应该是0、4、8才对,难道只代表一个符号么?
于是,在Disassembly上调试,为便于查看,我将代码改为cout << x;,以上代码反汇编结果为: 20160910152858
通过断点调试,x、y、z的值确实是0、4、8,另外反汇编代码可以明显看出,直接打印的含义为,如果值为0xFFFFFFFF,那么显示0,否则为1。因为通常不需要直接打印偏移长度,所以就把这个功能给省略了。真正需要用到的是,这个偏移是不是有效的。定义为,假如偏移长度为0xFFFFFFFF,那么代表这个偏移是无效的。于是我简单加了个hack:

1
2
__asm { mov x, 0ffffffffh }
cout << x;

代码不出所料,结果为0。这儿的0和1分别代表此处偏移类型是否有效。

C++:如何制作高逼格二维码

在网上看到一个制作高逼格二维码的文章,来源http://www.chenxublog.com/2016/05/22/pic-qrcode-colorful.html
步骤那可是相当的复杂,一堆软件各种ps啊什么的,不过根据实现的效果一看,原理貌似很简单嘛,何必弄那么复杂,于是我用代码实现了一个,本文着重介绍原理。编译后的程序在最下面,不过建议使用前先看过去,否则可能你不会用2333
先来几张图你们感受下,支付宝:
zfb

微信:
wx

首先是二维码的原理,通过一个图片里面的4个定位块进行定位,有的定位块是9个。如下图示例:
zfb

我这儿只说说4个定位块的二维码,因为9个定位块的代码我还没实现2333其中左上角、左下角、右上角是三个大的定位块,分别有8*8的大小,右下角有个小的定位块有5*5。除了定位块外还有用于计算二维码宽高的标志位,分别位于第6行与第6列,这个标志位颜色为黑白相间。

由于识别二维码的代码网上没找到(没找到VS2013能编译的、有效的代码,如果谁有的话告诉我一声2333),所以嘛,就有点麻烦了。比如这个二维码:
zfb

这个二维码有三个问题,颜色不是黑色、中间一小块读取的数据是错的、上下左右都有很大的边距。虽然通过较正位可以较正中间错误的色块但这不是没源码嘛。稍微有点麻烦,需要先转一下。

首先来草料二维码扫描器,将二维码解析出来,然后来草料二维码生成器,生成新二维码。通过转换,可以得到准确色块的二维码。

然而这二维码并不怎么准确2333,新生成的二维码里面的黑色或白色的方块,有的宽高为7,有的宽高为6,只能说大体上准确。这就稍微麻烦了,我之前是用直接填数字的方法不过这方法很不好用,于是用代码加了个简单识别宽度的代码,方便朋友们使用。

然后就是一堆比较中庸的代码,总的来说就是提取二维码像素,然后转为类似下面这样的图片(标识色块全部保留,其他色块只保留色块正中间的一个像素,其余的像素全部透明):
zfb2

这种图片弄出来之后,然后就需要找一个动态gif图片,每帧给绘制一遍上面这图片,然后保存,大概如下图这样:
bg
zfb

在此之前,还需要把GIF图片保存下来。GDI+好像没有直接保存的功能,于是使用了一个网上的保存GIF实现:C++ GDI+ 多张图片合并生成GIF动画格式图片

源代码比较杂乱,所以不贴了,如果有需要请到github上面下载。

源代码下载地址:https://github.com/fawdlstty/hm_QRcode

已编译程序下载地址(Win32平台):https://github.com/fawdlstty/hm_QRcode/releases

C++:socket服务端模型

首先解释下socket含义。网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。 Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。
下面我列举几个常用的服务端TCP socket实现代码

1、首先是最原始的阻塞模型。这种模型简单易懂,[以下代码基于Windows]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
 
int mian (int argc, char* argv []) {
    //Winsock环境初始化
    WSAData wd;
    WSAStartup (MAKEWORD (2, 2), &wd);
 
    //创建套接字
    SOCKET sock = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);//对于UDP协议,第二个参数填 SOCK_DGRAM
 
    //绑定前的操作,UDP可以不用绑定
    sockaddr_in addr;
    addr.sin_family = AF_INET;//需要绑定的本地地址
    addr.sin_addr.s_addr = 0;
    addr.sin_port = htons (51423);//需要绑定的本地端口
 
    //执行绑定
    bind (sock, (struct sockaddr*)&addr, sizeof (addr));
 
    //监听
    listen (sock, SOMAXCONN);
 
    while (true) {
        //如果有链接请求,则接受链接
        SOCKET session = accept (sock, NULL, NULL);
        if (INVALID_SOCKET == session) break;
 
        //接收数据
        char buf [1024];
        int len = recv (session, buf, sizeof (buf), 0);
 
        //发送数据
        send (session, buf, len, 0);
 
        //关闭链接
        closesocket (session);
    }
 
    //关闭套接字
    closesocket (sock);
 
    //Winsock环境释放
    WSACleanup ();
    return 0;
}

这种模型原理是,通过阻塞程序来一直读取socket数据,直到读取数据时才进行处理,否则一直处于阻塞状态。当然以上逻辑完全不可能作为服务器,估摸web体系刚建立时的程序猿都是使用这种架构+多线程实现并发模式。以上代码仅做了解即可,不用深究。

2、然后是比较原始的select模型,特殊场合使用,[以下代码基于Windows]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
 
#include <vector>
using namespace std;
 
std::vector<int> connect_sockets;
 
int mian (int argc, char* argv []) {
    //Winsock环境初始化
    WSAData wd;
    WSAStartup (MAKEWORD (2, 2), &wd);
 
    //创建套接字
    SOCKET sock = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);//对于UDP协议,第二个参数填 SOCK_DGRAM
 
    //绑定前的操作,UDP可以不用绑定
    sockaddr_in addr;
    addr.sin_family = AF_INET;//需要绑定的本地地址
    addr.sin_addr.s_addr = 0;
    addr.sin_port = htons (51423);//需要绑定的本地端口
 
    //执行绑定
    bind (sock, (struct sockaddr*)&addr, sizeof (addr));
 
    //监听
    listen (sock, SOMAXCONN);
 
    while (true) {
        int maxfd;
        fd_set readfds;
        //fd_set writefds;
        //fd_set exceptfds; 
        struct timeval tv;
 
        FD_ZERO (&readfds);
 
        FD_SET (sock, &readfds);
        maxfd = sock;
        //要将所有的client也加入到集合
        std::vector<int>::iterator it;
        for (it = connect_sockets.begin (); it != connect_sockets.end (); ++it) {
            int fd = *it;
            FD_SET (fd, &readfds);
            if (fd > maxfd) maxfd = fd;
        }
        maxfd++;
 
        // 等待1秒钟
        tv.tv_sec = 1;
        tv.tv_usec = 0;
 
        int count = select (maxfd, &readfds, NULL, NULL, &tv);
        if (count == 0) continue;
        if (SOCKET_ERROR == count) break;
 
        // count > 0
        if (FD_ISSET (sock, &readfds)) {
            SOCKET session = accept (sock, NULL, NULL);
            // 将fd保存到全局的一个变量中,方便下一次去出来,放入fd_set
            connect_sockets.push_back (session);
        }
 
        for (it = connect_sockets.begin (); it != connect_sockets.end ();) {
            SOCKET session = *it;
            if (FD_ISSET (session, &readfds))  // 如果session是在readfds中,说明fd有人发消息过来了,应该调用recv
            {
                // 接收数据
                char buf [1024];
                int len = recv (session, buf, sizeof (buf), 0);
 
                // 回应
                send (session, buf, len, 0);
                closesocket (session);
 
                it = connect_sockets.erase (it);
            } else {
                ++it;
            }
        }
    }
 
    //关闭套接字
    closesocket (sock);
 
    //Winsock环境释放
    WSACleanup ();
    return 0;
}

Linux的select代码和Windows的差不多,就不重写一遍了。这种模型需要定义一个fd表,然后一直不停的迭代扫描这个表;当有事件产生时,系统会给fd表置一个标识,程序扫描到标识时根据定义执行不同的操作。这种模型优点是效率高,但缺点也很明显,效率随着链接数增加而直线下降。这种模型用的非常少了,但还有一些链接少但要求实时数据更新的软件还在用。效率在特定场合比WSAAsyncSelect或epoll高,但高的这一点完全可以忽略。

3、接下来是Windows环境稍微先进些的IOCP模型,[以下代码基于Windows]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
#include <iostream>
using namespace std;
 
#define RECV_POSTED 1001
#define SEND_POSTED 1002
 
HANDLE hCompletionPort;
typedef struct _PER_HANDLE_DATA {
    SOCKET sock;
} PER_HANDLE_DATA, *LPPER_HANDLE_DATA;
 
typedef struct _PER_IO_OPERATION_DATA {
    OVERLAPPED Overlapped;
    WSABUF DataBuff [1];
    char Buff [24];
    BOOL OperationType;
} PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;
 
#define _PROGRAM_EXIT system ("pause");WSACleanup ();return 0;
 
DWORD WINAPI ServerWorkerThread (LPVOID CompletionPort) {
    DWORD dw;
    LPPER_HANDLE_DATA PerHandleData;
    LPPER_IO_OPERATION_DATA PerIoData;
    BOOL bT;
 
    while (TRUE) {
        //等待完成端口上SOCKET的完成
        cout << "等待完成端口上SOCKET的完成" << endl;
        bT = GetQueuedCompletionStatus ((HANDLE) CompletionPort, &dw, (LPDWORD) &PerHandleData, (LPOVERLAPPED *) &PerIoData, INFINITE);
 
        //检查是否有错误产生
        if (dw == 0 && (PerIoData->OperationType == RECV_POSTED || PerIoData->OperationType == SEND_POSTED)) {
            //关闭SOCKET
            cout << PerHandleData->sock << "SOCKET关闭" << endl;
            closesocket (PerHandleData->sock);
            free (PerHandleData);
            free (PerIoData);
            continue;
        }
 
        //为请求服务
        if (PerIoData->OperationType == RECV_POSTED) {
            //处理
            cout << "接收处理" << endl;
            cout << PerHandleData->sock << "SOCKET :" << PerIoData->Buff << endl;
            //回应客户端
            ZeroMemory (PerIoData->Buff, 24);
            strcpy (PerIoData->Buff, "OK");
            ZeroMemory ((LPVOID) &(PerIoData->Overlapped), sizeof (OVERLAPPED));
            PerIoData->DataBuff [0].len = 2;
            PerIoData->DataBuff [0].buf = PerIoData->Buff;
            PerIoData->OperationType = SEND_POSTED;
            WSASend (PerHandleData->sock, PerIoData->DataBuff, 1, &dw, 0, &(PerIoData->Overlapped), NULL);
        } else /*if(PerIoData->OperationType == SEND_POSTED)*/ {
            //发送时的处理
            cout << "发送处理" << endl;
            ZeroMemory ((LPVOID) &(PerIoData->Overlapped), sizeof (OVERLAPPED));
            ZeroMemory (PerIoData->Buff, 24);
            PerIoData->DataBuff [0].len = 24;
            PerIoData->DataBuff [0].buf = PerIoData->Buff;
            PerIoData->OperationType = RECV_POSTED;
            DWORD flags = 0;
            WSARecv (PerHandleData->sock, PerIoData->DataBuff, 1, &dw, &flags, &(PerIoData->Overlapped), NULL);
        }
    }
}
 
int main (int argc, char* argv []) {
    cout << "初始环境..." << endl;
    WSAData wsaData;
    if (WSAStartup (MAKEWORD (2, 2), &wsaData) != 0) {
        cout << "WSAStartup失败" << endl;
        return 0;
    }
 
    //创建一个IO完成端口
    cout << "创建一个IO完成端口" << endl;
    hCompletionPort = CreateIoCompletionPort (INVALID_HANDLE_VALUE, NULL, 0, 0);
    if (hCompletionPort == INVALID_HANDLE_VALUE) {
        cout << "创建IO完成端口失败" << endl;
        _PROGRAM_EXIT
    }
    //获取CPU数目
    SYSTEM_INFO si;
    GetSystemInfo (&si);
    //创建一定数目的工作者线程,本例中以一个处理器一个线程搭配
    for (int i = 0; i<(int) si.dwNumberOfProcessors * 2; i++) {//NumberOfProcessors
        DWORD thread_id;
        HANDLE hThread = CreateThread (NULL, 0, ServerWorkerThread, (LPVOID) hCompletionPort, 0, &thread_id);
        cout << "创建工作者线程" << i << endl;
        CloseHandle (hThread);
    }
    //创建监听SOCKET
    cout << "创建监听SOCKET" << endl;
    SOCKET sockListen = WSASocket (AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
    if (sockListen == SOCKET_ERROR) {
        cout << "WSASocket错误" << endl;
        _PROGRAM_EXIT
    }
 
    int reuse_addr = 1;
    if (setsockopt (sockListen, SOL_SOCKET, SO_REUSEADDR, (const char *) &reuse_addr, sizeof (int)) != 0) {
        cout << "setsockopt错误" << endl;
        _PROGRAM_EXIT
    }
 
    struct sockaddr_in addrLocal;
    addrLocal.sin_family = AF_INET;
    addrLocal.sin_addr.s_addr = htonl (INADDR_ANY);
    addrLocal.sin_port = htons (9090);
    if (bind (sockListen, (struct sockaddr *)&addrLocal, sizeof (sockaddr_in)) != 0) {
        cout << "bind错误" << endl;
        _PROGRAM_EXIT
    }
    //准备监听
    cout << "准备监听" << endl;
    if (listen (sockListen, 5) != 0) {
        cout << "listen错误" << endl;
        _PROGRAM_EXIT
    }
    while (true) {
        //接收用户连接,被和完成端口关联
        SOCKET session = WSAAccept (sockListen, NULL, NULL, NULL, 0);
        LPPER_HANDLE_DATA perHandleData = (LPPER_HANDLE_DATA) malloc (sizeof (PER_HANDLE_DATA));
        if (perHandleData == NULL) continue;
        cout << "socket number " << session << "接入" << endl;
        perHandleData->sock = session;
 
        LPPER_IO_OPERATION_DATA ioperdata = (LPPER_IO_OPERATION_DATA) malloc (sizeof (PER_IO_OPERATION_DATA));
        memset (&(ioperdata->Overlapped), 0, sizeof (OVERLAPPED));
        (ioperdata->DataBuff [0]).len = 24;
        (ioperdata->DataBuff [0]).buf = ioperdata->Buff;
        ioperdata->OperationType = RECV_POSTED;
        if (ioperdata == NULL) { free (perHandleData); continue; }
        //关联
        cout << "关联SOCKET和完成端口" << endl;
        if (CreateIoCompletionPort ((HANDLE) session, hCompletionPort, (DWORD) perHandleData, 1) == NULL) {
            cout << session << "createiocompletionport错误" << endl;
            free (perHandleData);
            free (ioperdata);
            continue;
        }
        //投递接收操作
        cout << "投递接收操作" << endl;
        DWORD flags, n_recv;
        WSARecv (perHandleData->sock, ioperdata->DataBuff, 1, &n_recv, &flags, &(ioperdata->Overlapped), NULL);
    }
    _PROGRAM_EXIT
}

这种模型在比较大的链接的情况下比select效率高很多,使用多线程来并发处理访问数据。但它也有几个坑,首先是模型结构比较复杂,基本框架就是上面一堆代码;其次是这种模型通过多线程来并发处理数据,多线程的坑你们都懂得。所以这种模型我也不推荐使用。慢慢来不要急,好的模型都在后面呢。

4、接下来是先进的WSAAsyncSelect模型,[以下代码基于Windows]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#pragma comment(linker, "/subsystem:console /entry:WinMain")
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#include <tchar.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
 
#define PORT      10086
#define MSGSIZE   1024
#define WM_SOCKET WM_USER+1
 
//单线程异步模型通过窗口消息循环进行通讯,所以这儿需要创建一个窗口
 
//窗口消息循环
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
    static SOCKET sock;
    switch (message) {
    case WM_CREATE:
        //初始化WinSock
        WSAData wd;
        WSAStartup (MAKEWORD(2, 2), &wd);
 
        //创建socket
        sock = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);
 
        //绑定
        SOCKADDR_IN local;
        local.sin_addr.S_un.S_addr = htonl (INADDR_ANY);
        local.sin_family = AF_INET;
        local.sin_port = htons (PORT);
        bind (sock, (struct sockaddr *)&local, sizeof (local));
 
        //监听
        listen (sock, 100);
        //设置网络模型
        WSAAsyncSelect (sock, hwnd, WM_SOCKET, FD_ACCEPT);
        return 0;
    case WM_DESTROY:
        closesocket (sock);
        WSACleanup ();
        PostQuitMessage (0);
        return 0;
 
    case WM_SOCKET:
        //遇到什么错误,,这时候其实可以WSAGetLastError查看错误详情
        if (WSAGETSELECTERROR (lParam)) {
            closesocket (wParam);
            break;
        }
 
        switch (WSAGETSELECTEVENT (lParam)) {
        case FD_ACCEPT:
            //接受链接请求
            SOCKADDR_IN client;
            int iAddrSize = sizeof (client);
            SOCKET sClient = accept (wParam, (struct sockaddr *)&client, &iAddrSize);
            WSAAsyncSelect (sClient, hwnd, WM_SOCKET, FD_READ | FD_CLOSE);
            break;
        case FD_READ:
            //读取数据
            char szMessage [MSGSIZE];
            int ret = recv ((SOCKET) wParam, szMessage, MSGSIZE, 0);
            if (ret == 0 || ret == SOCKET_ERROR && WSAGetLastError () == WSAECONNRESET) {
                closesocket ((SOCKET) wParam);
            } else {
                szMessage [ret] = '\0';
                send ((SOCKET) wParam, szMessage, strlen (szMessage), 0);
                printf ("%s\n", szMessage);
            }
            break;
 
        case FD_CLOSE:
            //关闭链接
            closesocket ((SOCKET) wParam);
            break;
        }
        return 0;
    }
    //默认消息循环
    return DefWindowProc (hwnd, message, wParam, lParam);
}
 
//主函数
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) {
    //注册窗口类
    static TCHAR szAppName [] = _T ("AsyncSelect Model");
    HWND         hwnd;
    WNDCLASS     wndclass;
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WndProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor (NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szAppName;
    if (!RegisterClass (&wndclass)) {
        MessageBox (NULL, _T ("This program requires Windows NT!"), szAppName, MB_ICONERROR);
        return 0;
    }
 
    //创建窗口
    hwnd = CreateWindow (szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
    ShowWindow (hwnd, iCmdShow);
    UpdateWindow (hwnd);
 
    //消息处理
    MSG msg;
    while (GetMessage (&msg, NULL, 0, 0)) {
        TranslateMessage (&msg);
        DispatchMessage (&msg);
    }
 
    UnregisterClass (szAppName, hInstance);
    return msg.wParam;
}

看起来好像这种模型也不简单,实际上这种模型设计架构非常先进,使用单线程异步模式,通过创建一个窗口,然后使用窗口消息循环来处理网络事件,在单线程处理大量链接的同时还能不用自己管理数据结构。这种模型也有缺点(有人说这是缺点,仁者见仁智者见智)就是,一旦使用这种模型就必须得创建一个窗口。不过对于现在这么大的内存来说,创建一个窗口也不是个什么事。网络上一些不太靠谱的砖家说什么什么TCP数据包可以直接发给窗口之类的,指的就是这种模型。连外行都能说个大概(虽然完全没说到点上2333)由此可见这种模型的知名度。
除此之外,还有一种类似的模型:WSPAsyncSelect,用法和上面差不多,对程序猿来说差异仅仅为函数名不同而已,这儿就不重写一遍了。

5、接下来是epoll模型,[以下代码基于Linux]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
// inet相关的头文件
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/epoll.h>
#include <stdlib.h>
 
void set_nonblock (int sock) {
    int flags = fcntl (sock, F_GETFL, 0);
    flags |= O_NONBLOCK;
    fcntl (sock, F_SETFL, flags);
}
 
int main (int argc, char* argv []) {
    // 1. 创建socket对象
    // 第一个参数是协议族 AF_INET表示以太网
    // 第二个参数是协议类型 SOCK_STREAM TCP
    // 第三个是协议 对于以太网来说,永远都是0
    int sock = socket (AF_INET, SOCK_STREAM, 0);
    set_nonblock (sock);
 
    // Linux下使用int表示socket对象,它的地位和open出来的文件是一样的
 
    // 2.给socket指定端口
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;     // 绑定本地地址
    addr.sin_addr.s_addr = 0;
    addr.sin_port = htons (51423); // 绑定本地端口
    bind (sock, (struct sockaddr*)&addr, sizeof (addr));
 
    // 3. 启动监听
    listen (sock, 200);
 
    // 创建epoll对象
    int epollfd = epoll_create (1024);
 
    // 把sock加入到epollfd
    struct epoll_event ev;
    ev.data.fd = sock;
    ev.events = EPOLLIN;
    epoll_ctl (epollfd, EPOLL_CTL_ADD, sock, &ev);
 
    struct epoll_event* ev_out = malloc (sizeof (*ev_out) * 8);
    int count = 8;
 
    while (1) {
        //struct epoll_event ev_out[8];
        int ret = epoll_wait (epollfd, ev_out, count, 1000);
 
        // 没有socket有消息
        if (ret == 0) continue;
        if (ret == -1) {
            if (errno == EINTR) continue;
            break;
        }
 
        int i;
        for (i = 0; i<ret; ++i) {
            struct epoll_event* p = ev_out + i;
            if (p->data.fd == sock) {
                int session = accept (p->data.fd, NULL, NULL);
 
                struct epoll_event ev1;
                ev1.data.fd = session;
                ev1.events = EPOLLIN;
                epoll_ctl (epollfd, EPOLL_CTL_ADD, session, &ev1);
            } else {
                char buf [1024];
                int len = recv (p->data.fd, buf, sizeof (buf), 0);
                send (p->data.fd, buf, len, 0);
 
                close (p->data.fd);
 
            }
        }
    }
 
    // 关闭socket
    close (sock);
 
    return 0;
}

实际上在讨论epoll模型前应该先说说poll模型,但poll模型只是在select模型架构之上,极大提高select模型(网上说基于Linux的select模型最大并发链接数是1024)的并发链接数,然而这并没什么卵用因为这种模型也是迭代处理结构体消息,意味着poll模型也和select模型一样,链接数越大效率越低。所以,链接数少还能用用select,链接数一多那就epoll,这种模型已经被淘汰所以这儿不贴示例代码了。
然后正式说说epoll模型。这种模型与Windows环境下的WSAAsyncSelect模型相似,基于单线程异步模型,但它有个好处就是连窗口都给省了,消息全部通过while循环进行处理,没有事件就等待,有事件就处理,并且几乎无最大链接数限制,可以说是Linux上面最完美的模型。

6、Boost.asio阻塞模型,[以下代码跨平台]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <boost/asio.hpp>
 
using boost::asio::ip::tcp;
 
int main (int argc, char* argv []) {
    try {
        boost::asio::io_service serv;
        tcp::acceptor acpt (serv, tcp::endpoint (tcp::v4 (), 51423));
 
        while (true) {
            tcp::socket session (serv);
            acpt.accept (session);
            session.write_some ("hello world");
            session.close ();
        }
    } catch (std::exception& e) {
        std::cerr << "Exception: " << e.what () << "\n";
    }
    return 0;
}

可能有人问,已经讲了各平台最完美的网络模型了,为啥还在继续?道理很简单嘛,之前的模型都不跨平台。通过编写跨平台代码可以极大减少代码对平台的相关依赖,可以很方便的将代码移植到不同的操作系统。这个可以说是优势也可以说是劣势,因为有人觉得它们开发的代码不需要跨平台,跨平台代码效率也不能达到最佳(跨平台代码本质也是通过调用平台框架模型而实现),里面大部分代码为了解决平台依赖而涉及很多中间转换过程,相比之下写跨平台代码效率比写epoll这类模型效率要低一部分。不过嘛有必要这么纠结嘛?现代计算机CPU频率这么高内存这么大,提升那几毫秒的时间也几乎看不出任何效果。还是那句话,除了在特定场合外,尽量不要写平台相关代码。
下面开始介绍介绍这个模型,哦不,框架。模型也就那样,阻塞模型,不过asio这框架用来开发网络相当方便。上面的阻塞模型代码看到了吧?一大堆不懂啥玩意的代码还都是定式,为啥不能省略之?在此环境下asio诞生了, 它的诞生不仅仅是一场革命,更是方便了广大程序猿2333
另外,asio基于boost,这个库被人们称之为“C++准标准库”,里面很多特性、类,都很可能成为C艹的标准。

7、Boost.asio异步模型,[以下代码跨平台]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <iostream>
#include <boost/asio.hpp>
 
using boost::asio::ip::tcp;
 
//会话类,每个链接都是一个会话
class session : public std::enable_shared_from_this<session> {
public:
    session (tcp::socket socket) : socket_ (std::move (socket)) {}
 
    void start () {
        do_read ();
    }
 
private:
    //读数据,异步事件代表完成
    void do_read () {
        auto self (shared_from_this ());
        socket_.async_read_some (boost::asio::buffer (data_, max_length), [this, self] (boost::system::error_code ec, std::size_t length) {
            if (!ec) do_write (length);
        });
    }
 
    //写数据,异步事件代表完成
    void do_write (std::size_t length) {
        auto self (shared_from_this ());
        boost::asio::async_write (socket_, boost::asio::buffer (data_, length), [this, self] (boost::system::error_code ec, std::size_t length) {
            if (!ec) do_read ();
        });
    }
 
    //会话句柄
    tcp::socket socket_;
    enum {
        max_length = 1024
    };
 
    //会话数据缓冲区
    char data_ [max_length];
};
 
//服务器类,用于控制会话
class server {
public:
    //初始化异步模型
    server (boost::asio::io_service& io_service, short port) : acceptor_ (io_service, tcp::endpoint (tcp::v4 (), port)), socket_ (io_service) {
        do_accept ();
    }
 
private:
    //相应链接事件
    void do_accept () {
        acceptor_.async_accept (socket_, [this] (boost::system::error_code ec) {
            if (!ec) std::make_shared<session> (std::move (socket_))->start ();
            do_accept ();
        });
    }
 
    tcp::acceptor acceptor_;
    tcp::socket socket_;
};
 
int main (int argc, char* argv []) {
    try {
        boost::asio::io_service io_service;
        server s (io_service, 8877);
        io_service.run ();
    } catch (std::exception& e) {
        std::cerr << "Exception: " << e.what () << "\n";
    }
    return 0;
}

终于进入正题了,其实我想说的也就这个模型,之前的模型看看就得了O(∩_∩)O2333~。asio的异步网络模型使用了先进的C++11规范,通过lambda表达式实现异步方法调用,感觉用着挺爽的,不用手工管理session,另外对于每个session封装成了一个类,可以自己手工管理结构体属性等,用起来比WSAAsyncSelect、epoll更爽,且代码更加清晰直观,乃服务端模型上上之选。代码逻辑太过简单我都不知道怎么写注释了。
以上服务端模型基于TCP。由于麻烦我这儿就不写UDP了反正这东西也没链接写起来也简单

C++:文件操作

文件操作是个啥大家都懂了,下面我给出几个文件操作示例
1、C语言文件操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//在windows平台下避免函数不安全报错而定义的一个宏
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main () {
    //fopen第二个参数有以下几种取值模式
    //首先是打开方式,从以下打开方式中任选一种或组合
    //    a(append)    以写方式打开文件并移动文件指针到文件尾部
    //    r(read)      以读方式打开文件,文件指针位置未知建议打开后重置文件指针
    //    w(write)     以写方式打开文件并清空文件内容
    //然后是数据类型,默认为文本方式,如果加上b则为二进制方式,区别在于,在windows环境下,不加b(文本模式)会造成:
    //    以文本方式读文件时,换行符\r\n被读成\n
    //    以文本方式写文件时,换行符\n被写成\r\n
    //最后是扩展方式,如果带+号代表如果文件不存在则新建。但我实际测试结果为,带不带+号并没什么卵用
    //示例:第二个参数为 rwb+ 代表以二进制读写方式打开文件,如果文件不存在则新建
    FILE *f = fopen ("a.txt", "rw+");
    if (f) {
        char *data = "hello";
        //写文件,不多做解释
        fwrite (data, strlen(data), 1, f);
 
        //移动文件指针到结束位置,第三个参数三种取值分别为 SEEK_SET(开始位置)、SEEK_CUR(当前位置)、SEEK_END(结束位置)
        fseek (f, 0, SEEK_END);
 
        //获取文件指针的偏移,这时候指针在末尾,含义就代表文件长度。
        //值得注意的是如果以文本方式并且有回车并且读取文件时,文件长度与读取的字节数不一样,原因上面有说明
        int len = ftell (f);
 
        //这行代码含义为移动文件指针到文件起始位置,等价于 fseek (f, 0, SEEK_SET);
        rewind (f);
 
        //其他文件操作函数不做说明,这儿给出常用列表
        //fwrite     输出一块数据到文件
        //fprintf    格式化输出到文件
        //fputc      输出一个字节到文件
        //fputs      输出一串字符串到文件
        //fread      读取一块数据到文件
        //fscanf     从文件格式化输入
        //fgetc      从文件中读取一个字符,因为文件可能结束,所以返回类型为int。如果值为 EOF 代表文件已读到最后
        //fgets      从文件中读取一串字符串
        //feof       判断文件指针是否在文件末尾
 
        //如果为多个程序同时访问文件的情况时建议加上这句。含义代表将文件缓冲区内容刷新到文件中
        //不加可能出现文件更新不及时的现象,不管是否调用这句,关闭文件时始终会刷新的
        fflush (f);
 
        //关闭文件
        fclose (f);
    } else {
        printf ("打开文件失败!");
    }
 
    system ("pause");
    return 0;
}

2、C++文件操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <fstream>
#include <string>
 
using namespace std;
 
int main () {
    //以读方式打开文件,第二个参数可以不填,默认 ios::in ,有以下几种取值方式
    //ios::in        代表读文件
    //ios::binary    代表二进制模式
    ifstream ifs ("a.txt");
    string s;
 
    //按分隔方式读文件并打印。分隔符可以为空格符、制表符、回车符三种,这种读取方式并不好
    //while (ifs >> s) cout << s << endl;
 
    //按行方式读取文件,这种方式值得推荐
    while (getline (ifs, s)) cout << s << endl;
 
    //读取整个文件至 std::string 中
    std::string str ((std::istreambuf_iterator<char> (ifs)), std::istreambuf_iterator<char> ());
    cout << str << endl;
 
    //按流方式读取文件还有个好处,那就是可以直接将文件内容输入/输出到结构体中,比较方便
    //其他读文件方式严重不推荐比如什么 ifs.get (); 什么什么的
 
    //获取当前文件流指针位置,对于 std::ofstream 使用 tellp 函数
    streampos p = ifs.tellg ();
 
    //移动文件指针到起始位置,对于 std::ofstream 使用 seekp 函数
    //有三个参数 含义你们都造含义 ios_base::beg、ios_base::cur、ios_base::end
    ifs.seekg (0, ios_base::beg);
 
    //关闭文件
    ifs.close ();
 
    //以写方式打开文件,第二个参数可以不填,默认 ios::out ,有以下几种取值方式
    //ios::out       代表写文件
    //ios::app       代表增加(append),含义见C里面的a模式
    //ios::binary    代表二进制模式,可以和以上任意一种方式组合使用,比如 ios::app | ios::binary 
    //               如果需要组合 out 那么可以直接简写为 ios::binary
    ofstream ofs ("a.txt");
 
    //愉快地写文件咯。注意回车最好写成 std::endl
    ofs << "asdasdasdasdasd";
 
    //关闭文件
    ofs.close ();
 
    return 0;
}

3、Win32 API 文件操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <Windows.h>
#include <tchar.h>
 
int main () {
    //打开文件,oh my god 好多参数吓死本宝宝了
    HANDLE hFile = CreateFile (
        _T ("a.txt"),
        GENERIC_READ | GENERIC_WRITE,//文件读写方式,图省事可以 GENERIC_ALL
        FILE_SHARE_READ | FILE_SHARE_WRITE,//共享方式,如果不希望共享访问那么这儿传0
        NULL,//安全描述符,一般情况下不要设置
        OPEN_ALWAYS,//文件创建方式,有以下几种取值:
            //CREATE_NEW           如果文件存在则调用失败
            //CREATE_ALWAYS        不管文件是否存在始终创建新文件,如果文件原有数据将被覆盖
            //OPEN_EXISTING        打开文件,如果文件不存在则打开失败
            //OPEN_ALWAYS          如果文件存在则打开,如果文件不存在则新建
            //TRUNCATE_EXISTING    与 GENERIC_WRITE 同时存在,一旦打开则文件被截短为0字节
        FILE_ATTRIBUTE_NORMAL,//文件属性,如果想玩玩隐藏文件之类的那么输入 FILE_ATTRIBUTE_ 然后看自动提示
        NULL//文件模板句柄,传NULL就行了
    );
 
    //Windows平台文件调用传递路径,仅做参考,看不懂可以不用管:
    //CreateFile -> ZwCreateFile -> NtCreateFile -> 汇编sysenter指令进入驱动层 -> ZwCreateFile -> NtCreateFile -> IoCreateFile -> Irp消息 -> 捕获Irp消息的钩子,如果未处理则继续传递 -> 文件系统驱动 -> 硬件抽象层驱动 -> 硬件
 
    //小技巧:常数类型在判断时写在前面,避免等号写成一个时出错并且还找不到bug
    if (INVALID_HANDLE_VALUE != hFile) {
        char* data = "aaaaaaaaaaaaaaaaaaaaa";
 
        //写文件,参数太少不解释
        DWORD dw;//已写字节数
        WriteFile (hFile, data, lstrlenA(data), &dw, NULL);
 
        //设置文件指针,第四个参数的三种取值方式:FILE_BEGIN、FILE_CURRENT、FILE_END
        SetFilePointer (hFile, 0, NULL, FILE_BEGIN);
 
        //读文件
        char buf [MAX_PATH];
        ReadFile (hFile, buf, 10, &dw, NULL);
 
        //关闭文件
        CloseHandle (hFile);
    }
 
    return 0;
}

以上三个为阻塞模型文件操作示例,相对来说写法简单,除了阻塞文件操作之外还有种单线程异步操作模型,这种模型相对较复杂,但可以使IO效率最大化,对于高手来说值得尝试。
具体写法为:Windows平台上,IO函数调用全部加上Ex,比如 WriteFileEx ,这类函数将会立即返回,并且在IO操作完毕后通过异步回调通知主线程;其次是跨平台Boost.afio库,github地址为 https://github.com/ned14/boost.afio,写法类似异步boost.asio。

Windows服务访问控制

服务控制在win32用户层系统编程中比较重要,一般用于创建自启动项或加载驱动,这样的话启动服务就是载入驱动,停止服务就是卸载驱动,极大方便了驱动控制。下面贴一个服务控制代码,可用于方便的控制服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#pragma once
#ifndef __HSERVER_HPP__
#define __HSERVER_HPP__
 
#include <Windows.h>
 
class hService {
    SC_HANDLE h_scm = NULL, h_service = NULL;
    QUERY_SERVICE_CONFIG *h_qsc = NULL;
    SERVICE_STATUS h_status;
 
public:
    hService (LPCTSTR serv_name) {
        h_qsc = (LPQUERY_SERVICE_CONFIG)new BYTE [8 * 1024];
        if ((h_scm = ::OpenSCManager (NULL, NULL, SC_MANAGER_ALL_ACCESS)) && serv_name && serv_name[0]) {
            h_service = ::OpenService (h_scm, serv_name, SERVICE_ALL_ACCESS);
        }
    }
    ~hService () {
        delete h_qsc;
        if (h_service) ::CloseServiceHandle (h_service);
        if (h_scm) ::CloseServiceHandle (h_scm);
    }
 
    //服务是否正在运行
    BOOL is_running () {
        if (!h_service) return FALSE;
        if (::QueryServiceStatus (h_service, &h_status)) {
            if (h_status.dwCurrentState == SERVICE_RUNNING) return TRUE;
        }
        return FALSE;
    }
 
    //服务是否已停止
    BOOL is_stopped () {
        if (!h_service) return FALSE;
        if (::QueryServiceStatus (h_service, &h_status)) {
            if (h_status.dwCurrentState == SERVICE_STOPPED) return TRUE;
        }
        return FALSE;
    }
 
    //服务是否已暂停
    BOOL is_pause () {
        if (!h_service) return FALSE;
        if (::QueryServiceStatus (h_service, &h_status)) {
            if (h_status.dwCurrentState == SERVICE_PAUSED) return TRUE;
        }
        return FALSE;
    }
 
    //服务是否自动启动
    BOOL is_auto_run () {
        DWORD d;
        if (!h_service) return FALSE;
        if (::QueryServiceConfig (h_service, h_qsc, 8 * 1024, &d)) {
            return h_qsc->dwStartType <= 2;
        }
        return FALSE;
    }
 
    //启动服务
    BOOL start () {
        if (!h_service) return FALSE;
        if (is_running ()) return TRUE;
        return ::StartService (h_service, NULL, NULL);
    }
 
    //停止服务
    BOOL stop () {
        if (!h_service) return FALSE;
        if (is_stopped ()) return TRUE;
        return ::ControlService (h_service, SERVICE_CONTROL_STOP, &h_status);
    }
 
    //暂停服务
    BOOL pause () {
        if (!h_service) return FALSE;
        if (is_running ()) return ::ControlService (h_service, SERVICE_CONTROL_PAUSE, &h_status);
        return TRUE;
    }
 
    //恢复服务
    BOOL resume () {
        if (!h_service) return FALSE;
        if (is_pause ()) return ::ControlService (h_service, SERVICE_CONTROL_CONTINUE, &h_status);
        return is_running ();
    }
 
    //设置自动启动服务
    BOOL auto_start () {
        if (!h_service) return FALSE;
        if (is_auto_run ()) return TRUE;
        SC_LOCK sclLock = ::LockServiceDatabase (h_scm);
        BOOL bRet = ::ChangeServiceConfig (h_service, SERVICE_NO_CHANGE, SERVICE_AUTO_START, SERVICE_NO_CHANGE, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
        if (sclLock) ::UnlockServiceDatabase (sclLock);
        return bRet;
    }
 
    //设置手动启动服务
    BOOL demand_start () {
        if (!h_service) return FALSE;
        if (!is_auto_run ()) return TRUE;
        SC_LOCK sclLock = ::LockServiceDatabase (h_scm);
        BOOL bRet = ::ChangeServiceConfig (h_service, SERVICE_NO_CHANGE, SERVICE_DEMAND_START, SERVICE_NO_CHANGE, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
        if (sclLock) ::UnlockServiceDatabase (sclLock);
        return bRet;
    }
 
    //创建服务
    static hService *create_service (LPCTSTR serv_name, LPCTSTR display_name, DWORD service_type, LPCTSTR path) {
        hService *service = new hService (NULL);
        service->h_service = ::CreateService (service->h_scm, serv_name, display_name, SC_MANAGER_ALL_ACCESS, service_type, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, path, NULL, NULL, NULL, NULL, NULL);
        return service;
    }
 
    //删除服务
    BOOL delete_service () {
        if (::DeleteService (h_service)) {
            h_service = NULL;
            return TRUE;
        }
        return FALSE;
    }
};
#endif //__HSERVER_HPP__

创建头文件,引入以上代码之后,就可以方便进行服务控制了。调用方式在代码中已有注释。

Windows下编码转换

对于网络程序来说经常需要用到编码转换,比如访问utf8编码网页下载之后,转为unicode进行显示。windows自带的编码API比较难用,所以对其进行简单的封装。分SDK和MFC两个版本代码,SDK版本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#pragma once
#ifndef __HCODEC_HPP__
#define __HCODEC_HPP__
 
#include <Windows.h>
#include <string>
 
class hCodec {
    //不可实例化
    hCodec () = delete;
    ~hCodec () = delete;
 
    static bool hCodec::_conv_Down (std::wstring& _old, std::string& _new, UINT ToType) {
        int lenOld = lstrlenW (_old.c_str ());
        int lenNew = ::WideCharToMultiByte (ToType, 0, _old.c_str (), lenOld, NULL, 0, NULL, NULL);
        std::string s;
        s.resize (lenNew);
        bool bRet = ::WideCharToMultiByte (ToType, 0, _old.c_str (), lenOld, const_cast<char*>(s.c_str ()), lenNew, NULL, NULL);
        _new.clear ();
        _new = s.c_str ();
        return bRet;
    }
    static bool hCodec::_conv_Up (std::string& _old, std::wstring& _new, UINT ToType) {
        int lenOld = lstrlenA (_old.c_str ());
        int lenNew = ::MultiByteToWideChar (ToType, 0, _old.c_str (), lenOld, NULL, 0);
        std::wstring s;
        s.resize (lenNew);
        bool bRet = ::MultiByteToWideChar (ToType, 0, _old.c_str (), lenOld, const_cast<wchar_t*>(s.c_str ()), lenNew);
        _new.clear ();
        _new = s.c_str ();
        return bRet;
    }
 
public:
    static bool hCodec::AnsiToUnicode (std::string& _old, std::wstring& _new) {
        return hCodec::_conv_Up (_old, _new, CP_ACP);
    }
    static bool hCodec::UnicodeToAnsi (std::wstring& _old, std::string& _new) {
        return hCodec::_conv_Down (_old, _new, CP_ACP);
    }
    static bool hCodec::Utf8ToUnicode (std::string& _old, std::wstring& _new) {
        return hCodec::_conv_Up (_old, _new, CP_UTF8);
    }
    static bool hCodec::UnicodeToUtf8 (std::wstring& _old, std::string& _new) {
        return hCodec::_conv_Down (_old, _new, CP_UTF8);
    }
    static bool hCodec::AnsiToUtf8 (std::string& _old, std::string& _new) {
        std::wstring t;
        if (!hCodec::AnsiToUnicode (_old, t)) return false;
        return hCodec::UnicodeToUtf8 (t, _new);
    }
    static bool hCodec::Utf8ToAnsi (std::string& _old, std::string& _new) {
        std::wstring t;
        if (!hCodec::Utf8ToUnicode (_old, t)) return false;
        return hCodec::UnicodeToAnsi (t, _new);
    }
};
 
#endif //__HCODEC_HPP__

MFC版本与其类似,不过专用于MFC环境,调用方式相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#pragma once
#ifndef __HCODEC_HPP__
#define __HCODEC_HPP__
 
#include <afxstr.h>
 
class hCodec {
    //不可实例化
    hCodec () = delete;
    ~hCodec () = delete;
 
    static bool hCodec::_conv_Down (CStringW& _old, CStringA& _new, UINT ToType) {
        int lenOld = lstrlenW (_old.GetBuffer ());
        int lenNew = ::WideCharToMultiByte (ToType, 0, _old.GetBuffer (), lenOld, NULL, 0, NULL, NULL);
        CStringA s;
        s.GetBufferSetLength (lenNew);
        bool bRet = ::WideCharToMultiByte (ToType, 0, _old.GetBuffer (), lenOld, const_cast<char*>(s.GetBuffer ()), lenNew, NULL, NULL);
        _new = s.GetBuffer ();
        return bRet;
    }
    static bool hCodec::_conv_Up (CStringA& _old, CStringW& _new, UINT ToType) {
        int lenOld = lstrlenA (_old.GetBuffer ());
        int lenNew = ::MultiByteToWideChar (ToType, 0, _old.GetBuffer (), lenOld, NULL, 0);
        CStringW s;
        s.GetBufferSetLength (lenNew);
        bool bRet = ::MultiByteToWideChar (ToType, 0, _old.GetBuffer (), lenOld, const_cast<wchar_t*>(s.GetBuffer ()), lenNew);
        _new = s.GetBuffer ();
        return bRet;
    }
 
public:
    static bool hCodec::AnsiToUnicode (CStringA& _old, CStringW& _new) {
        return hCodec::_conv_Up (_old, _new, CP_ACP);
    }
    static bool hCodec::UnicodeToAnsi (CStringW& _old, CStringA& _new) {
        return hCodec::_conv_Down (_old, _new, CP_ACP);
    }
    static bool hCodec::Utf8ToUnicode (CStringA& _old, CStringW& _new) {
        return hCodec::_conv_Up (_old, _new, CP_UTF8);
    }
    static bool hCodec::UnicodeToUtf8 (CStringW& _old, CStringA& _new) {
        return hCodec::_conv_Down (_old, _new, CP_UTF8);
    }
    static bool hCodec::AnsiToUtf8 (CStringA& _old, CStringA& _new) {
        CStringW t;
        if (!hCodec::AnsiToUnicode (_old, t)) return false;
        return hCodec::UnicodeToUtf8 (t, _new);
    }
    static bool hCodec::Utf8ToAnsi (CStringA& _old, CStringA& _new) {
        CStringW t;
        if (!hCodec::Utf8ToUnicode (_old, t)) return false;
        return hCodec::UnicodeToAnsi (t, _new);
    }
};
 
#endif //__HCODEC_HPP__

调用方式非常简单,比如SDK版如下

1
2
3
4
5
//gb2312编码到unicode编码的转换
std::string ansi = "hello world!";
std::wstring unicode;
hCodec::AnsiToUnicode (ansi, unicode);
//然后unicode这儿就是转换后的文本了

除了这个之外,另外推荐一种ATL的编码转换方式,感觉这种方式还是挺好用的,首先引入头文件

1
#include <atlconv.h>

然后,在函数内部,如果这个函数里面的代码涉及到编码转换,那么在函数第一行加上

1
USES_CONVERSION;

然后就可以自由转换了。转换API比较多,通常使用下面几个:A2T、A2W、T2A、T2W、W2A、W2T等等,使用方式如下:

1
MessageBox (NULL, A2T ("test"), A2T ("test"), MB_OK);

直接使用类似C语言那样的访问就可以了。

另外说个小窍门,对于Visual Studio环境,有时候代码中大量的字符串需要初始化为utf-8编码,那么可以在首行加入如下代码

1
#pragma execution_character_set("utf-8")

加上之后,全局定义的"xxx"风格的字符串全部默认以utf-8方式进行编码。

Windows注册表访问的封装

注册表这东西用的越来越少了,但在特定场合还是不可替代的东西。比如开机启动项、环境变量和文件扩展名等各种系统信息都是放在注册表中。
由于注册表API访问稍微麻烦了点,于是我对其进行简单的封装。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#pragma once
#ifndef __HREG_HPP__
#define __HREG_HPP__
 
#include <Windows.h>
 
// main_key 注册表根键可取值
// HKEY_CLASSES_ROOT
// HKEY_CURRENT_CONFIG
// HKEY_CURRENT_USER
// HKEY_LOCAL_MACHINE
// HKEY_USERS
 
// type 键值类型可取值
// REG_BINARY               二进制数据
// REG_DWORD                双字
// REG_DWORD_LITTLE_ENDIAN  双字小头模式
// REG_DWORD_BIG_ENDIAN     双字大头模式
// REG_EXPAND_SZ            包含未展开的环境变量的引用的字串
// REG_LINK                 一个用 RegCreateKeyEx 传 REG_OPTION_CREATE_LINK 创建的符号链接的字串
// REG_MULTI_SZ             多个字符串,用\0分隔,末尾为\0\0
// REG_NONE                 没定义值类型
// REG_QWORD                四字
// REG_QWORD_LITTLE_ENDIAN  四字小头模式
// REG_SZ                   普通字符串
 
 
class hReg {
    //不可实例化
    hReg () = delete;
    ~hReg () = delete;
    hReg (hReg&) = delete;
 
public:
    //创建注册表路径
    //main_key:   注册表根键
    //sub_key:    注册表路径
    //type:       键值类型
    //value:      值
    //value_size: 值长度(字节)
    static BOOL set_path (HKEY main_key, LPCTSTR sub_key, DWORD type, LPBYTE value, DWORD value_size) {
        return ERROR_SUCCESS == ::RegSetValueEx (main_key, sub_key, 0, type, value, value_size);
    }
 
    //创建注册表键值
    //main_key:   注册表根键
    //sub_key:    注册表路径
    //sub_key2:   键值名称
    //type:       键值类型
    //value:      值
    //value_size: 值长度(字节)
    static BOOL set_key (HKEY main_key, LPCTSTR sub_key, LPCTSTR sub_key2, DWORD type, LPBYTE value, DWORD value_size) {
        HKEY hKey;
        if (ERROR_SUCCESS != ::RegOpenKeyEx (main_key, sub_key, 0, KEY_WRITE, &hKey)) return FALSE;
        BOOL bRet = ERROR_SUCCESS == ::RegSetValueEx (hKey, sub_key2, 0, type, value, value_size);
        ::RegCloseKey (hKey);
        return bRet;
    }
 
    //获取注册表路径键值
    //main_key:   注册表根键
    //sub_key:    注册表路径
    //value:      值
    //value_size: 值长度(字节)
    static BOOL get_path_value (HKEY main_key, LPCTSTR sub_key, LPBYTE value, DWORD &value_size) {
        return ERROR_SUCCESS == ::RegQueryValueEx (main_key, sub_key, NULL, NULL, value, &value_size);
    }
 
    //获取注册表路径键值
    //main_key:   注册表根键
    //sub_key:    注册表路径
    //sub_key2:   键值名称
    //value:      值
    //value_size: 值长度(字节)
    static BOOL get_key_value (HKEY main_key, LPCTSTR sub_key, LPCTSTR sub_key2, LPBYTE value, DWORD &value_size) {
        HKEY hKey;
        if (ERROR_SUCCESS != ::RegOpenKeyEx (main_key, sub_key, 0, KEY_READ, &hKey)) return FALSE;
        BOOL bRet = ERROR_SUCCESS == ::RegQueryValueEx (hKey, sub_key2, NULL, NULL, value, &value_size);
        ::RegCloseKey (hKey);
        return bRet;
    }
 
    //删除注册表路径
    //main_key:   注册表根键
    //sub_key:    注册表路径
    static BOOL delete_path (HKEY main_key, LPCTSTR sub_key) {
        return ERROR_SUCCESS == ::RegDeleteKey (main_key, sub_key);
    }
 
    //删除注册表键值
    //main_key:   注册表根键
    //sub_key:    注册表路径
    //sub_key2:   注册表路径
    static BOOL delete_key (HKEY main_key, LPCTSTR sub_key, LPCTSTR sub_key2) {
        HKEY hKey;
        if (ERROR_SUCCESS != ::RegOpenKeyEx (main_key, sub_key, 0, KEY_ALL_ACCESS, &hKey)) return FALSE;
        BOOL bRet = ERROR_SUCCESS == ::RegDeleteKey (hKey, sub_key2);
        ::RegCloseKey (hKey);
        return bRet;
    }
};
 
#endif //__HREG_HPP__

全部为静态函数,注上了比较完整的注释,另外函数名也较之前清晰,统一返回BOOL表示执行成功或失败。

Win32打开文件/另存为/文件夹对话框实现

一个比较有年代的东西,适合收藏。效果如下:
20160526161352
20160526161420
调用稍微麻烦了点,已将其封装为库,首先引用hSelect.hpp,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#pragma once
#ifndef __HSELECT_HPP__
#define __HSELECT_HPP__
 
#ifndef _WINDOWS_
#include <windows.h>
#endif
#include <tchar.h>
#include <ShlObj.h>
 
class selFile {
public:
    selFile (HWND hWnd) {
        ofn.lStructSize = sizeof (OPENFILENAME);
        ofn.hwndOwner = hWnd;
        ofn.hInstance = NULL;
        ofn.lpstrFilter = _T("Portable Network Graphics(*.png)\0*.png\0")
            _T ("Joint Photographic experts Group(*.jpg)\0*.jpg\0")
            _T ("Graphics Interchange Format(*.gif)\0*.gif\0")
            _T ("Bitmap Files(*.bmp)\0*.bmp\0")
            _T ("All Files\0*.*\0\0");
        ofn.lpstrCustomFilter = NULL;
        ofn.nMaxCustFilter = 0;
        ofn.nFilterIndex = 0;
        ofn.lpstrFile = NULL;
        ofn.nMaxFile = MAX_PATH;
        ofn.lpstrFileTitle = NULL;
        ofn.nMaxFileTitle = MAX_PATH;
        ofn.lpstrInitialDir = NULL;
        ofn.lpstrTitle = NULL;
        ofn.Flags = 0;
        ofn.nFileOffset = 0;
        ofn.nFileExtension = 0;
        ofn.lpstrDefExt = TEXT ("jpg");
        ofn.lCustData = 0;
        ofn.lpfnHook = NULL;
        ofn.lpTemplateName = NULL;
    }
    LPCTSTR open () {
        buf [0] = _T ('\0');
        TCHAR tTitle[MAX_PATH] = _T ("打开文件");
        ofn.lpstrFile = buf;
        ofn.lpstrFileTitle = tTitle;
        ofn.Flags = 0;
        if (GetOpenFileName (&ofn)) return buf;
        return NULL;
    }
    LPCTSTR save () {
        buf [0] = _T ('\0');
        TCHAR tTitle [MAX_PATH] = _T ("另存为");
        ofn.lpstrFile = buf;
        ofn.lpstrFileTitle = tTitle;
        ofn.Flags = OFN_OVERWRITEPROMPT;
        if (GetSaveFileName (&ofn)) return buf;
        return NULL;
    }
private:
    OPENFILENAME ofn;
    TCHAR buf [MAX_PATH];
};
 
class selFolder {
public:
    selFolder (HWND hWnd) {
        bi.hwndOwner = hWnd;
        bi.pidlRoot = NULL;
        bi.pszDisplayName = buf;//此参数如为NULL则不能显示对话框
        bi.lpszTitle = _T("指定目录");
        bi.ulFlags = 0;
        bi.lpfn = NULL;
        bi.iImage = NULL;
        bi.lParam = 0;
    }
    LPCTSTR get () {
        BOOL bRet = FALSE;
        LPITEMIDLIST pIDList = SHBrowseForFolder (&bi);//调用显示选择对话框
        if (pIDList) {
            SHGetPathFromIDList (pIDList, buf);
            bRet = TRUE;
        }
 
        LPMALLOC lpMalloc;
        if (FAILED (SHGetMalloc (&lpMalloc))) return FALSE;
 
        //释放内存
        lpMalloc->Free (pIDList);
        lpMalloc->Release ();
 
        if (bRet) return buf;
        return NULL;
    }
private:
    BROWSEINFO bi;
    TCHAR buf [MAX_PATH];
};
 
#endif //__HSELECT_HPP__

由于设置比较麻烦,所以打开文件、保存文件的类型就直接固定了,各位看情况修改。调用示例:

1
2
3
4
5
6
7
8
#include "hSelect.hpp"
 
int main(int argc, char* argv[]) {
    selFile f (NULL);
    LPCTSTR p = f.open ();
    if (p) MessageBox (NULL, p, _T("test"), MB_ICONINFORMATION);
    return 0;
}

此头文件包含两个类,selFile与selFolder,分别对应文件和文件夹。创建时统一传递一个参数,当前窗口句柄,如果不是很懂那直接传NULL就可以了。然后,selFile提供了两个公开方法,open与save,分别对应打开文件和保存文件;selFolder提供了一个公开方法,get,表示获取用户选择的目录,这仨返回的都是LPCTSTR类型,如果成功即为选择的路径,如果失败那么统一返回NULL。
注:selFile或selFolder对象释放时,返回的指针同时失效,使用时注意下。
这种文件夹选择对话框不好用,还有一种更好用的文件夹选择框,在MFC下特别好用,代码如下:

1
2
3
CFolderPickerDialog dlg (path, 0, this);
if (IDOK == dlg.DoModal ())
    path = dlg.GetFolderPath ();