分类目录归档:谜のC语言

SHA-256 摘要算法 实现 及 优化

很久没有发文章了啊,咳咳,最近考试考的人心力交瘁。这两天由于需要(真的是自已的需要么?),做了做摘要算法方面的工作,以前都是调库,但是现在需要将算法移植到stm32上,不仅要保证功能性还要保证效率,于是自己编写了一个SHA-256的程序,现分享一下。

作为哈希算法在我印象中最早听说的是md5算法,不过后来又听说它被发现有一些弱点,其发现者是我国的密码学专家王小云教授,不过虽然其弱点被发现,也并不影响其在一些领域的应用,至今我还经常能看到md5的身影。当然我这里选用了相对安全一些的SHA-256算法,SHA全称为Secure Hash Algorithm,意为安全哈希算法,其算法到目前为止历经了三代的变革,SHA-1,SHA-2和SHA-3,而SHA-256是SHA-2系列中的一个算法,那么先从SHA-256算法本身开始讲起。

哈希做为一个摘要算法,直观上讲,不管给其输入什么内容,其结果最终都会是一个固定长度的码串。对于SHA-256,其输出的长度便是256bit,也就是说所有的数据都可以映射到这个2^256个值的域中。也就是说其与普通上讲的加密算法有所不同的是,哈希算法会将输入的信息损失一部分,因为其值域是有限的,而定义域却是无限的。所以经过这种算法处理,其结果是不能逆向求得值的。(其实哈希算法并不能算作加密算法)

比方在网站存储用户密码的时候,为了防止入侵者入侵数据库后直接拿到用户的密码,往往会使用哈希函数对用户的密码进行处理,这样每当用户登录的时候便可以将用户提交的密码进行同样的运算来对比结果验证身份,同样可以保证在数据库被入侵后用户的密码一定程度上不会完全泄露(简单的密码也就查个表的事,和裸奔没区别)。当然这只是其中一个小应用,其在数字签名,文件防伪等方面也有广泛应用,甚至说是比特币这样的区块链货币的核心算法也不为过。

SHA-256算法的过程

首先我想简单直白的总结一下SHA-256的算法过程。

第一步:先对输入的数据进行补位和分块,SHA-256算法的数据分为512bit一个区块。然后循环的做迭代运算,直到算出结果。由于数据并非一定是512bit的整数倍,所以需要先进行补位,使其成为512bit块的整数倍。补位的操作为向数据后先补1位1然后补0至位长度对512取模为448为止,然后再在末尾添加一个64位长的数据长度字。

这里需要详细说一下,因为一开始我也有点犯晕,比方说你现在有一个信息为“01010”,那么你的补位结果是“0101010000…(省略496个“0”)…000101”,最后一位这个长度字的数值为你输入的数据的bit数。当然在计算机当中一般也不会碰到非整byte的信息,所以这个长度往往是8的倍数,而在进行补1操作时,往往也是在后面跟一个0x80字节。当然一旦你输入的信息超过了448bit那么就必须向数据后添加新的一个512bit区块来补位,这个新的区块可能仅包含0x00和长度字。

我将其总结为以下三种情况:

这样对其分类我们可以采取三种简便的方法来补位。

第二步:对区块进行混合。这里便需要介绍SHA-256的几种混合函数:

其中符号含义为:

SHA-256便是利用这几种混合函数对数据进行混合,值得注意的是,这里前两种函数Ch(x)和Maj(x)对数据进行处理时便已经产生了信息的损失,而后四种函数则是一一对应的,没有损失任何信息。(这里并不是凭空臆测其输入bit与输出bit便草草得出结论,而是经过了程序枚举验证,四个函数的定义域与值域均是一一映射的。)(内存大就是为所欲为)

其混合过程可以由下图总结(图中仅为一轮混合):

图中每一个色块表示32bit的数据,绿色框线中的“+”表示32位加法,剩下的便是混合公式,蓝色框线中为混合时输入的数据。Wt是由用户输入的数据经过变换得来的,而Kt为已知常数。将图中的混合过程迭代重复64次,便可得到最终的SHA-256值。

其中最初的A-H值为:

1
2
3
4
5
6
7
8
A = 0x6a09e667;
B = 0xbb67ae85;
C = 0x3c6ef372;
D = 0xa54ff53a;
E = 0x510e527f;
F = 0x9b05688c;
G = 0x1f83d9ab;
H = 0x5be0cd19;

这8个值分别为前8个质数(2,3,5,7,11,13,17,19)的平方根取小数部分前32bit。混合时使用的常数Kt为:

1
2
3
4
5
6
7
8
9
uint32_t K[] = { 
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};

这64个值则是前64个质数的立方根取小数部分前32bit而来。(事实上我觉得这些取值没什么理论依据,就跟我一拍大腿想出来的一样,看起来很混乱就是了)

Wt是用户需要做SHA-256变换输入的数据,其中前16轮变换输入的数据为一个512bit区块分割为16个32bit区块顺序填入的值。而第17至64轮变换时,Wt的生成公式为:

当64轮混合做完了之后,将变换结果以每32位为一块与变换前的值做加法,便得到了最终的SHA-256摘要值。如果区块不止一块的话则将前一块的摘要值作为下一块的初始值,继续进行64轮变换,直到迭代到最后一块区块变换完成为止。

第三步:将最终的结果返回便是该输入信息的SHA-256哈希值。

SHA-256算法实现

搜索GitHub便可得到很多SHA-256的实现代码,比如Openssl中就包含SHA-256的实现,还有Crypto++BitCoin等,其中的代码均可借鉴。

其中Openssl的SHA-256算法实现可以说非常的清晰易懂:

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
static void sha256_block_data_order(SHA256_CTX *ctx, const void *in,
                                    size_t num)
{
    unsigned MD32_REG_T a, b, c, d, e, f, g, h, s0, s1, T1, T2;
    SHA_LONG X[16], l;
    int i;
    const unsigned char *data = in;
 
    while (num--) {
 
        a = ctx->h[0];
        b = ctx->h[1];
        c = ctx->h[2];
        d = ctx->h[3];
        e = ctx->h[4];
        f = ctx->h[5];
        g = ctx->h[6];
        h = ctx->h[7];
 
        for (i = 0; i < 16; i++) {
            (void)HOST_c2l(data, l);
            T1 = X[i] = l;
            T1 += h + Sigma1(e) + Ch(e, f, g) + K256[i];
            T2 = Sigma0(a) + Maj(a, b, c);
            h = g;
            g = f;
            f = e;
            e = d + T1;
            d = c;
            c = b;
            b = a;
            a = T1 + T2;
        }
 
        for (; i < 64; i++) {
            s0 = X[(i + 1) & 0x0f];
            s0 = sigma0(s0);
            s1 = X[(i + 14) & 0x0f];
            s1 = sigma1(s1);
 
            T1 = X[i & 0xf] += s0 + s1 + X[(i + 9) & 0xf];
            T1 += h + Sigma1(e) + Ch(e, f, g) + K256[i];
            T2 = Sigma0(a) + Maj(a, b, c);
            h = g;
            g = f;
            f = e;
            e = d + T1;
            d = c;
            c = b;
            b = a;
            a = T1 + T2;
        }
 
        ctx->h[0] += a;
        ctx->h[1] += b;
        ctx->h[2] += c;
        ctx->h[3] += d;
        ctx->h[4] += e;
        ctx->h[5] += f;
        ctx->h[6] += g;
        ctx->h[7] += h;
 
    }
}

Openssl这里代码写的非常规整,当然这只是作为原理示范代码,其真正使用的代码是用汇编语言编写的。再列举一个BitCoin的代码:

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
void Transform(uint32_t* s, const unsigned char* chunk, size_t blocks)
{
    while (blocks--) {
        uint32_t a = s[0], b = s[1], c = s[2], d = s[3], e = s[4], f = s[5], g = s[6], h = s[7];
        uint32_t w0, w1, w2, w3, w4, w5, w6, w7, w8, w9, w10, w11, w12, w13, w14, w15;
 
        Round(a, b, c, d, e, f, g, h, 0x428a2f98 + (w0 = ReadBE32(chunk + 0)));
        Round(h, a, b, c, d, e, f, g, 0x71374491 + (w1 = ReadBE32(chunk + 4)));
        Round(g, h, a, b, c, d, e, f, 0xb5c0fbcf + (w2 = ReadBE32(chunk + 8)));
        Round(f, g, h, a, b, c, d, e, 0xe9b5dba5 + (w3 = ReadBE32(chunk + 12)));
        Round(e, f, g, h, a, b, c, d, 0x3956c25b + (w4 = ReadBE32(chunk + 16)));
        Round(d, e, f, g, h, a, b, c, 0x59f111f1 + (w5 = ReadBE32(chunk + 20)));
        Round(c, d, e, f, g, h, a, b, 0x923f82a4 + (w6 = ReadBE32(chunk + 24)));
        Round(b, c, d, e, f, g, h, a, 0xab1c5ed5 + (w7 = ReadBE32(chunk + 28)));
        Round(a, b, c, d, e, f, g, h, 0xd807aa98 + (w8 = ReadBE32(chunk + 32)));
        Round(h, a, b, c, d, e, f, g, 0x12835b01 + (w9 = ReadBE32(chunk + 36)));
        Round(g, h, a, b, c, d, e, f, 0x243185be + (w10 = ReadBE32(chunk + 40)));
        Round(f, g, h, a, b, c, d, e, 0x550c7dc3 + (w11 = ReadBE32(chunk + 44)));
        Round(e, f, g, h, a, b, c, d, 0x72be5d74 + (w12 = ReadBE32(chunk + 48)));
        Round(d, e, f, g, h, a, b, c, 0x80deb1fe + (w13 = ReadBE32(chunk + 52)));
        Round(c, d, e, f, g, h, a, b, 0x9bdc06a7 + (w14 = ReadBE32(chunk + 56)));
        Round(b, c, d, e, f, g, h, a, 0xc19bf174 + (w15 = ReadBE32(chunk + 60)));
 
        Round(a, b, c, d, e, f, g, h, 0xe49b69c1 + (w0 += sigma1(w14) + w9 + sigma0(w1)));
        Round(h, a, b, c, d, e, f, g, 0xefbe4786 + (w1 += sigma1(w15) + w10 + sigma0(w2)));
        Round(g, h, a, b, c, d, e, f, 0x0fc19dc6 + (w2 += sigma1(w0) + w11 + sigma0(w3)));
        Round(f, g, h, a, b, c, d, e, 0x240ca1cc + (w3 += sigma1(w1) + w12 + sigma0(w4)));
        Round(e, f, g, h, a, b, c, d, 0x2de92c6f + (w4 += sigma1(w2) + w13 + sigma0(w5)));
        Round(d, e, f, g, h, a, b, c, 0x4a7484aa + (w5 += sigma1(w3) + w14 + sigma0(w6)));
        Round(c, d, e, f, g, h, a, b, 0x5cb0a9dc + (w6 += sigma1(w4) + w15 + sigma0(w7)));
        Round(b, c, d, e, f, g, h, a, 0x76f988da + (w7 += sigma1(w5) + w0 + sigma0(w8)));
        Round(a, b, c, d, e, f, g, h, 0x983e5152 + (w8 += sigma1(w6) + w1 + sigma0(w9)));
        Round(h, a, b, c, d, e, f, g, 0xa831c66d + (w9 += sigma1(w7) + w2 + sigma0(w10)));
        Round(g, h, a, b, c, d, e, f, 0xb00327c8 + (w10 += sigma1(w8) + w3 + sigma0(w11)));
        Round(f, g, h, a, b, c, d, e, 0xbf597fc7 + (w11 += sigma1(w9) + w4 + sigma0(w12)));
        Round(e, f, g, h, a, b, c, d, 0xc6e00bf3 + (w12 += sigma1(w10) + w5 + sigma0(w13)));
        Round(d, e, f, g, h, a, b, c, 0xd5a79147 + (w13 += sigma1(w11) + w6 + sigma0(w14)));
        Round(c, d, e, f, g, h, a, b, 0x06ca6351 + (w14 += sigma1(w12) + w7 + sigma0(w15)));
        Round(b, c, d, e, f, g, h, a, 0x14292967 + (w15 += sigma1(w13) + w8 + sigma0(w0)));
 
        Round(a, b, c, d, e, f, g, h, 0x27b70a85 + (w0 += sigma1(w14) + w9 + sigma0(w1)));
        Round(h, a, b, c, d, e, f, g, 0x2e1b2138 + (w1 += sigma1(w15) + w10 + sigma0(w2)));
        Round(g, h, a, b, c, d, e, f, 0x4d2c6dfc + (w2 += sigma1(w0) + w11 + sigma0(w3)));
        Round(f, g, h, a, b, c, d, e, 0x53380d13 + (w3 += sigma1(w1) + w12 + sigma0(w4)));
        Round(e, f, g, h, a, b, c, d, 0x650a7354 + (w4 += sigma1(w2) + w13 + sigma0(w5)));
        Round(d, e, f, g, h, a, b, c, 0x766a0abb + (w5 += sigma1(w3) + w14 + sigma0(w6)));
        Round(c, d, e, f, g, h, a, b, 0x81c2c92e + (w6 += sigma1(w4) + w15 + sigma0(w7)));
        Round(b, c, d, e, f, g, h, a, 0x92722c85 + (w7 += sigma1(w5) + w0 + sigma0(w8)));
        Round(a, b, c, d, e, f, g, h, 0xa2bfe8a1 + (w8 += sigma1(w6) + w1 + sigma0(w9)));
        Round(h, a, b, c, d, e, f, g, 0xa81a664b + (w9 += sigma1(w7) + w2 + sigma0(w10)));
        Round(g, h, a, b, c, d, e, f, 0xc24b8b70 + (w10 += sigma1(w8) + w3 + sigma0(w11)));
        Round(f, g, h, a, b, c, d, e, 0xc76c51a3 + (w11 += sigma1(w9) + w4 + sigma0(w12)));
        Round(e, f, g, h, a, b, c, d, 0xd192e819 + (w12 += sigma1(w10) + w5 + sigma0(w13)));
        Round(d, e, f, g, h, a, b, c, 0xd6990624 + (w13 += sigma1(w11) + w6 + sigma0(w14)));
        Round(c, d, e, f, g, h, a, b, 0xf40e3585 + (w14 += sigma1(w12) + w7 + sigma0(w15)));
        Round(b, c, d, e, f, g, h, a, 0x106aa070 + (w15 += sigma1(w13) + w8 + sigma0(w0)));
 
        Round(a, b, c, d, e, f, g, h, 0x19a4c116 + (w0 += sigma1(w14) + w9 + sigma0(w1)));
        Round(h, a, b, c, d, e, f, g, 0x1e376c08 + (w1 += sigma1(w15) + w10 + sigma0(w2)));
        Round(g, h, a, b, c, d, e, f, 0x2748774c + (w2 += sigma1(w0) + w11 + sigma0(w3)));
        Round(f, g, h, a, b, c, d, e, 0x34b0bcb5 + (w3 += sigma1(w1) + w12 + sigma0(w4)));
        Round(e, f, g, h, a, b, c, d, 0x391c0cb3 + (w4 += sigma1(w2) + w13 + sigma0(w5)));
        Round(d, e, f, g, h, a, b, c, 0x4ed8aa4a + (w5 += sigma1(w3) + w14 + sigma0(w6)));
        Round(c, d, e, f, g, h, a, b, 0x5b9cca4f + (w6 += sigma1(w4) + w15 + sigma0(w7)));
        Round(b, c, d, e, f, g, h, a, 0x682e6ff3 + (w7 += sigma1(w5) + w0 + sigma0(w8)));
        Round(a, b, c, d, e, f, g, h, 0x748f82ee + (w8 += sigma1(w6) + w1 + sigma0(w9)));
        Round(h, a, b, c, d, e, f, g, 0x78a5636f + (w9 += sigma1(w7) + w2 + sigma0(w10)));
        Round(g, h, a, b, c, d, e, f, 0x84c87814 + (w10 += sigma1(w8) + w3 + sigma0(w11)));
        Round(f, g, h, a, b, c, d, e, 0x8cc70208 + (w11 += sigma1(w9) + w4 + sigma0(w12)));
        Round(e, f, g, h, a, b, c, d, 0x90befffa + (w12 += sigma1(w10) + w5 + sigma0(w13)));
        Round(d, e, f, g, h, a, b, c, 0xa4506ceb + (w13 += sigma1(w11) + w6 + sigma0(w14)));
        Round(c, d, e, f, g, h, a, b, 0xbef9a3f7 + (w14 + sigma1(w12) + w7 + sigma0(w15)));
        Round(b, c, d, e, f, g, h, a, 0xc67178f2 + (w15 + sigma1(w13) + w8 + sigma0(w0)));
 
        s[0] += a;
        s[1] += b;
        s[2] += c;
        s[3] += d;
        s[4] += e;
        s[5] += f;
        s[6] += g;
        s[7] += h;
        chunk += 64;
    }
}

直接将循环展开,虽然提高了性能但却加大了存储开销(然而PC机不在乎)。(果然人类的本质是复读机)

SHA-256实现优化

由于我需要在stm32f767zit这块芯片上运行SHA-256,单片机不仅需要更高的效率,还需要较小的开销,包括RAM和Flash两方面的开销(而且不想上汇编不仅不好维护而且万一后面就不用了岂不是白费好大功夫)。所以上面两种代码均不能直接使用。(实际上上面两个程序也并不是直接使用C语言编写的代码,而是都有各自的用汇编编写的针对性优化程序,甚至使用了SIMD)这让我深刻的理解了跨平台的两种方法,一种是使用高级语言,另一种是将所有常用平台的汇编都写一遍(简称大力出奇迹)。

仔细观察上边的混合算法框图,不难发现,其实每轮便换更新的值只有两块,第一块是H的值,新的值是原来值加上所有的部分组成的,第二块是D的值,其新的值仅仅只是加上H块运算中的一个中间值。所以我们完全可以节省中间变量,先算一部分H的值,然后将H加在D上,再继续运算到结束。

对于Wt前16块与17至64块不相同需要迭代的问题,完全不需要开辟一个64块的内存或者单独来进行计算,通过观察发现,每一轮变换完全用不到16块之前的Wt值,也就是说超过当前变换轮数16块的值便可以丢弃,即当前计算结束后的Wt值在未来的第16次变换便不再需要了。所以可以在当前变换完了之后直接对之后的第16次变换所需要的值进行计算,并填在当前值的位置上,因为当前值已经不再需要了。这样不仅节省了Flash的开销,同时也节省了内存的开销,同时也不牺牲性能,可谓一举两得。

以下便是根据这几点构建的代码,可以看到整个函数只有133字节的内存开销(64字节存储Wt,32字节存储初始Hash值,32字节存储变换Hash值,4字节作为轮换临时值,1字节作为循环控制变量),并且短小精悍(疯狂自吹):

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
CRYPTO_STATUS sha_256_update(uint32_t* data)
{
	uint8_t i;
	uint32_t tmp;
 
	memcpy(SHA256_Data2, SHA256_Data1, 32);
	for (i = 0; i < 64; ++i)
	{
		SHA256_Data2[7] += sha256_ch(SHA256_Data2[4], SHA256_Data2[5], SHA256_Data2[6])
			+ sigma_h1(SHA256_Data2[4]) + SHA256_CalcTab[i] + data[i & 0x0F];
		SHA256_Data2[3] += SHA256_Data2[7];
		SHA256_Data2[7] += sha256_maj(SHA256_Data2[0], SHA256_Data2[1], SHA256_Data2[2])
			+ sigma_h0(SHA256_Data2[0]);
 
		tmp = SHA256_Data2[7];
		SHA256_Data2[7] = SHA256_Data2[6];
		SHA256_Data2[6] = SHA256_Data2[5];
		SHA256_Data2[5] = SHA256_Data2[4];
		SHA256_Data2[4] = SHA256_Data2[3];
		SHA256_Data2[3] = SHA256_Data2[2];
		SHA256_Data2[2] = SHA256_Data2[1];
		SHA256_Data2[1] = SHA256_Data2[0];
		SHA256_Data2[0] = tmp;
 
		data[i & 0x0F] += sigma_l1(data[(i + 14) & 0x0F]) + data[(i + 9) & 0x0F] + sigma_l0(data[(i + 1) & 0x0F]);
	}
	for (i = 0; i < 8; ++i)
	{
		SHA256_Data1[i] += SHA256_Data2[i];
	}
	return CRYPTO_SUCCESS;
}

对代码进行性能测试结果为:

===================

平台:Intel Xeon E5-2699 v4 @ 2.2GHz

编译器:MS VC++ v14.0 (优化等级:O2)

测试结果:129.6MByte/s

===================

平台:STM32F767ZIT @ 216MHz (开启指令和数据缓存,指令预取和Flash预取)

编译器:Armcc v5.06 (优化等级:O3)

测试结果:3.21MByte/s

===================

总的来说,速度还是能达到使用要求的。

PS:整个工程和源文件在后续整理注释后会上传到这里。(先偷个懒)

(完)

第三章:C++中的C

在使用C++编写程序的时候,我们会发现其中很多地方所使用的语句与C语言十分相似,其实从其命名可以看出来,C++与C语言有着不一般的关系。C++可以认为是为了使C语言更容易被开发,具有更强大的工程能力而被制造出来的。其中的基础语句以及函数的调用等等,都与标准C非常的相似,但是也有很多不同的地方,这些细节也都是C++为了解决一些问题而改进的地方,正因为”++”在C++里边有着自增的含义,所以C++也可以理解为在C的基础上更上一层楼。其实在使用C++编译器时,几乎可以将C语言代码原封不动的放置进来编译,不过C++也加入了很多独有的特性,同时也改进了很多C语言当中的问题,所以这里仅着重介绍一些两者之间的不同以及编写程序的时候值得注意的地方。

首先关于函数的创建,在C和C++当中,有一个特征叫函数原型,或者叫函数接口,它的意思是对函数返回值类型和参数列表的一个声明。而编译器则可以利用这个原型去检查函数的调用及返回并及时发现错误。在声明函数原型的时候,可以只依次写出函数参数列表的参数类型,而不必使用标识符,当然使用标识符可以使程序员获得更多信息有助于编写程序,但是编译器并不会检查函数声明和定义时的标识符是否匹配。而在函数定义时,标准C认为所有参数都必须具有标识符才可以被调用,所以在标准C当中定义函数时必须给所有变量指定标识符,而C++认为在函数开发过程中可能需要具有一些保留位来提高兼容性,所以其参数列表允许不使用标识符,仅做占位用途,当然该位置输入的参数也不能被直接调用,不过,删去不使用的标识符可以消除编译器的警告。在不清楚具体需要输入几个参数的时候,C语言提供了一种解决方法,即在参量表中输入省略号(…)或者直接将参量表留空,这两者在C中表示的意思都是可变参数参量表,即编译器不会对输入的参数类型进行检查。但是在C++当中,由于有函数重载这样的方法,可以尽量避免使用可变参量表,因为可变的参量表总是会导致很多bug。在C和C++当中,声明一个函数都需要声明其返回值类型,如果不进行声明编译器将会按照int型进行检查。

与C语言非常相似的是,在C++当中的几乎所有执行控制语句都与C当中的一模一样,比如if-else语句、switch-case语句、while语句、do-while语句、for语句甚至goto语句,说到goto语句,这是一个十分不推荐使用的语句,因为goto语句在C++里边可以引起严重的混乱。由于C++具有实时定义的特点,所以在C++当中处处都有着严格的作用域控制,即一个变量在声明之后,只会在离它最近的包裹着它的花括号范围内起作用,脱出这个范围之后,这个变量即会失去作用,也就是说它被释放掉了。但是如果使用goto跨越了作用域的边界,其并不会触发变量的释放,因此就会引发一系列错误。

再来说说实时定义,这是较C语言方便太多了的一个特性,它允许程序员在代码中任何地方声明定义变量,不必像C语言那样必须将变量在作用域的开始位置定义。一方面在编写代码的过程中不停的来回插入所需变量十分的麻烦,另一方面在读者阅读代码的时候由于变量的定义与使用地方距离太远,不容易对应观察其含义。所以实时定义是一个非常重要且好用的特性。

在变量方面C++与C语言并没有什么显著的区别,不过在常量方面,C++与C具有很大差距,在C语言当中,建立一个常量有时只能使用C语言的预处理器特性,使用#define来定义一个宏。而在C++当中则还可以选择使用const关键字。在这个方面C++要显得人性化很多,因为它的编译器会对const类型的数据进行很多的优化,比如如果程序所有的地方都只是对const的值进行使用,则编译器会将const进行变量折叠,即不给它进行内存分配,而是直接写入到程序的符号表里边去。而如果有对其地址的请求的话,编译器则会将其放在内存中,并且保证其不被改变,任何改变行为都会引发编译错误。还有一点就是编译器会对const类型数据进行类型检查,保证const在作为参数被传递的时候的安全性。而#define则只是简单的文字替换,容易引发很严重的问题。后来在C语言当中也引入了const的用法,不过不同的是,在C语言当中,const只能表示一段不能被改变的内存,由编译器来保证其不被改变。

C语言与C++的运算符在基础类型操作中表现的一模一样,其也是由右至左进行等式的赋值等等,其中几乎所有的逻辑运算符、位运算符、关系运算符以及特殊的运算符在基础应用情况下均是相同的。不过不同的是,在C++当中运算符似乎不仅仅再是运算符那么简单。在C++当中,运算符是可以被重载的,比如在使用C++的标准输出cout的时候,用法是cout << “str”;其中”<<“即是移位操作符,不过在此处它并不是移位符的意思,而是一种函数调用,对于上面那个表达式来说,cout和”str”分别为函数的两个参数,而表达式的值则是函数的返回值。而重载的意思就是在C++当中,可以用同一个名字来作为多个函数的名字,只要他们的参量表不完全相同,编译器就可以对函数的调用进行区分。具体的细节在之后的内容会专门讲到。而运算符也可以作为一类特殊的函数。

当然在进行运算编写的过程中,我们经常由于各种原因需要对不同类型的变量进行转换,在C语言当中,一些相对比较常见而且一般不会导致数据丢失等现象的转换会由编译器隐式完成,不过在一些较为容易出现问题或者转换目的不明确的时候,编译器会提示错误,并且需要程序员专门输入转换方式进行显式转换即用括号在变量名前冠上需要转换到的类型。而在C++当中,虽然它支持相同的转换方式,不过它也给出了一种显式转换的方法来替替代旧的风格,即static_cast, const_cast, reinterpret_cast, dynamic_cast这几种转换方式分别控制了几种不同的转换过程,static_cast是静态转换,就是进行一些定义明确的但是可能具有危险的,或者甚至编译器都会进行自动转换的类型转换。而const_cast则是将const类型的常量转换为非const类型的变量,或者将volatile类型的变量转换成非volatile类型的变量。(这里volatile变量指的是“不知何时会改变的变量”,它在内存当中可能会被除了本段代码以外的其它东西所改变,所以编译器会对这个变量不进行任何优化,比如将其加载到寄存器中进行连续赋值操作等。)reinterpret_cast指的是按照位拷贝的方式进行转换,一般情况下这种转换非常少用,除非在进行一些特殊的操作或算法时,因为按位拷贝转换成其他变量之后,大多数情况下需要再转换回来才能正常使用该变量,当然如果进行一些位操作的话倒没有什么太大的影响。最后dynamic_cast则是用于对类的派生与其基类之间的转换,在多态方面有着重要的应用,这些概念在书中的15章有详细的阐述。值得注意的一点是,在编写程序的时候尽量还是应该少使用类型转换,它们极易引发程序的漏洞,而C++将类型转换的方法设计的尤为复杂,也是在时时刻刻提醒着程序员,尽量少使用类型转换,而且一旦出现了问题,只需要再程序中检索cast关键字便可以迅速排查程序中出现的类型转换问题。

最后,C++的结构体联合体以及枚举等特殊用法也和C语言几乎一模一样,不过有一点不同的是,在C++当中结构体中的内容不一定必须是数据的集合,也可以将函数添加进去,而类(class)的概念也是从这个基础之上诞生的,将数据和函数封装成为一个集合,并且由内部的函数完成一些数据的操作,最后达到一种黑箱的效果,这也是所谓面向对象的一个最直观形象的一个解释。

总而言之,C++其中几乎包含了C语言的所有用法,并且对其中很多不甚妥当的地方进行了改进,使它在代码性能丝毫不逊色C语言的情况之下,更加的贴近程序员的思路,并且加速了开发的流程,而且使得可以由更多的人进行同一个项目的开发不会变得混乱不堪。这些都是C++的优点,也是它流行起来的原因之一。其相较C语言更上一层楼可谓当之无愧。

第二章:对象的创建与使用 (2) (利用C++标准库创建对象)

在我们进行编程的时候,并不需要每次都从零开始构建一个程序,往往我们可以使用一些由其他人或者先前的工程师进行精心测试过得代码来快速构建我们自己的程序,这样我们可以节省很多很多的时间和精力,同样在大多数情况下其性能也会出色很多。而在C和C++当中通过调用库函数来实现这一点。所谓库即是由别的公司或者个人将它们编写测试好的代码进行编译之后得到的一个或多个文件,这些文件当中含有的函数可以由连接器连接到用户所编写的程序当中去。而由于库中往往含有多个函数和变量,用户去一一声明自己所使用到的函数将会十分麻烦,然而C和C++当中的“头文件”很好的解决了这个问题,即函数库的提供者同时提供一份头问件,其中包含用户可以使用的所有的变量及函数的声明,如此一来用户只需要使用#include预处理器命令将对应文件插入到用户所需的地方即可,从而免去了用户繁杂的声明工作。

对于#include预处理器命令,有些值得注意的地方:首先双引号和尖括号包含文件名有着不同的意义。尖括号表示预处理器以特定的方式来寻找文件,一般是环境目录中或者编译器命令行中指定的某种路径,具体寻找路径跟随机器、系统、编译器不同而不同,而双引号则表示从当前目录开始寻找,如果没有找到则再按照尖括号的形式来寻找。

#include <header.h>

#include “header.h”

关于标准库中头文件的引用,在早期不同的编译器厂商选用了不同的扩展名,而后为了增加源代码的可移植性,则使用了一种标准引用,去除了扩展名,比如

#include <iostream>

而从C语言继承下来的库则可以去除文件后缀后在开头加入字母”c”。即

#include <cstdio>

#include <cstdlib>

当然传统的后缀包含法依然可用。只不过两种引用方式稍有不同,对于标准C++库来说

#include <iostream.h>

相当于

#include <iostream>

using namespace std;

如果使用新式的引用方式,则必须显式声明使用名字空间std(“standard”之意)名字空间为C++为了解决C语言当中程序规模庞大后的函数以及变量命名困难问题,即将函数包含进名字空间,使用某个名字空间中的函数时需要在函数或变量名前边加上作用域解析符(“::”双冒号),比如:

std::cout << “hello” << std::endl;

当然也可以使用using namespace std;使得以下的语句等同于上边这条语句。

cout << “hello” << endl;

当然如果只是想单独不加作用域解析符使用其中一个函数或变量,则可以:

using std::cout;

cout << “hello” << std::endl;

当然在使用名字空间的时候尽量在源代码文件当中使用,不要将其包含在头文件当中,因为在头文件当中包含using的话会导致所有引用这个头文件的源代码都出现混乱。很可能造成编译失败或者难以发现的函数及变量调用错误。

对于字符串输出这里,C的预处理器对长字符串做出了一些优化,即它可以将相邻的几个字符串拼接成为一个长字符串,于是在输出长文本时我们不必将其完全写在一行当中,比如:

std::cout << “Hello World “

“I feel good \n”;

以下为C++标准库当中一些头文件的简单介绍:

Reference-C

图片来源:http://www.cplusplus.com/reference/

其中最常用的莫过于标准容器库,所谓容器,其介绍如下:

A container is a holder object that stores a collection of other objects (its elements). They are implemented as class templates, which allows a great flexibility in the types supported as elements.

The container manages the storage space for its elements and provides member functions to access them, either directly or through iterators (reference objects with similar properties to pointers).

参考来源:http://www.cplusplus.com/reference/stl/

这段话的含义是:容器是储存其他对象(它的元素)的东西,他们使用具有对类型兼容性很高的模板类来实现。容器管理元素存储所需空间并且提供操作访问它们的函数,当然也可以通过直接访问和迭代器访问(一个和指针类似的指向性类)

根据我的理解,容器即是一个可以容纳几乎任何类型数据的管理器,它将我们平时使用的存储模型(向量存储,链表存储,图存储,堆栈,队列等)进行了模板化处理,并且提供了标准的操作函数,还有标准的访问工具(迭代器)这样让我们可以在开发程序的时候不用太在意如何去实现这样一个模型,去考虑更多更高层面的问题,而且标准库中提供的实现会更加严谨全面和高性能。而且所有的容器都可以自动的管理分配存储空间,比如向量容器,与我们使用的数组十分相似,但是其可以在数组大小将尽的时候自动扩展数组上限以容纳更多元素。

总之,学会基本语法以及如何使用标准函数库之后,就可以做出属于自己的第一份C++代码了,书中有详细的模板使用方法,并给出了很多样例代码,(主要以vector即向量模板为例)可以参照它做出自己的第一个C++程序,万事开头难,至此便启程C++之旅。