三种基本结构

分支结构

if分支语句

  • 单一语句:在任何一个表达式后面加上分号;
  • 符合语句:用一对花括号{}括起来的语句块,在语法上等效一个单一的语句;
  • if语句:if语句是最常用的一种分支语句,也称为条件语句。

if语句练习:实现一个函数:输入一个年号,判断是否是闰年。闰年判断:1、能够被400整除;2、能被4整除但不能被100整除。

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;

bool isLeapYear(unsigned int year)
{
// 双分支if
//if ( (year % 400 == 0) || (year%4==0 && year%100 != 0) )
//这里有个if的优化,因为||操作符,第一个条件满足不看后面的,&&操作符第一个条件不满足不看后面的
//所以按下面的方式效率更高,因为涉及命中率问题,被4整除的概率更高
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return true;
}
else
{
return false;
}
}

int main()
{
// 是否是闰年
cout << isLeapYear(2000) << endl;
cout << isLeapYear(2020) << endl;
cout << isLeapYear(2021) << endl;
return 0;
}

if语句练习:判断一个整数是否是另一个整数的倍数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;

int main()
{
// b是否是a的倍数
int a = 0;
int b = 4;
//短路运算,除数不能为0。千万别写反了
if ((a != 0) && b % a == 0)
{
cout << "b是a的倍数" << endl;
}
else
{
cout << "b不是a的倍数" << endl;
}

return 0;
}

switch分支语句

1
2
3
4
5
6
7
switch(表达式)
{
case 常数1:语句1; break;
case 常数2:语句2; break;
case 常数3:语句3; break;
default : 语句;
}
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
#include <iostream>
using namespace std;

typedef enum __COLOR
{
RED,
GREEN,
BLUE,
UNKOWN
}COLOR;

int main()
{
// 多分支条件的if
// if, else if, else
COLOR color0;
color0 = BLUE;
int c0 = 0;
if (color0 == RED) {
//cout << "red" << endl;
c0 += 1;
}
else if (color0 == GREEN) {
//cout << "green" << endl;
c0 += 2;
}
else if (color0 == BLUE) {
//cout << "blue" << endl;
c0 += 3;
}
else {
//cout << "unknown color" << endl;
c0 += 0;
}

// 多分支条件的switch
// switch,case,default
COLOR color1;
color1 = GREEN;
int c1 = 0;
switch (color1)
{
case RED:
{
//cout << "red" << endl;
c1 += 1;
break;
}
case GREEN:
{
//cout << "green" << endl;
c1 += 2;
break;
}
case BLUE:
{
//cout << "blue" << endl;
c1 += 3;
break;
}
default:
{
//cout << "unknown color" << endl;
c1 += 0;
break;
}
}


return 0;
}

为了比较两种分支语句的区别,我们可以看更底层的汇编代码,拿上面例子举例:

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
// 多分支条件的if
// if, else if, else
COLOR color0;
color0 = BLUE;
001A5D85 mov dword ptr [color0],2
int c0 = 0;
001A5D8C mov dword ptr [c0],0
if (color0 == RED) {
001A5D93 cmp dword ptr [color0],0
001A5D97 jne main+164h (01A5DA4h)
//cout << "red" << endl;
c0 += 1;
001A5D99 mov eax,dword ptr [c0]
001A5D9C add eax,1
001A5D9F mov dword ptr [c0],eax
001A5DA2 jmp main+18Ch (01A5DCCh)
}
else if (color0 == GREEN) {
001A5DA4 cmp dword ptr [color0],1
001A5DA8 jne main+175h (01A5DB5h)
//cout << "green" << endl;
c0 += 2;
001A5DAA mov eax,dword ptr [c0]
001A5DAD add eax,2
001A5DB0 mov dword ptr [c0],eax
001A5DB3 jmp main+18Ch (01A5DCCh)
}
else if (color0 == BLUE) {
001A5DB5 cmp dword ptr [color0],2
001A5DB9 jne main+186h (01A5DC6h)
//cout << "blue" << endl;
c0 += 3;
001A5DBB mov eax,dword ptr [c0]
001A5DBE add eax,3
//cout << "blue" << endl;
c0 += 3;
001A5DC1 mov dword ptr [c0],eax
}
else {
001A5DC4 jmp main+18Ch (01A5DCCh)
//cout << "unknown color" << endl;
c0 += 0;
001A5DC6 mov eax,dword ptr [c0]
001A5DC9 mov dword ptr [c0],eax
}

// 多分支条件的switch
// switch,case,default
COLOR color1;
color1 = GREEN;
001A5DCC mov dword ptr [color1],1
int c1 = 0;
001A5DD3 mov dword ptr [c1],0
switch (color1)
001A5DDA mov eax,dword ptr [color1]
001A5DDD mov dword ptr [ebp-10Ch],eax
001A5DE3 cmp dword ptr [ebp-10Ch],0
001A5DEA je main+1C0h (01A5E00h)
001A5DEC cmp dword ptr [ebp-10Ch],1
001A5DF3 je main+1CBh (01A5E0Bh)
001A5DF5 cmp dword ptr [ebp-10Ch],2
001A5DFC je main+1D6h (01A5E16h)
001A5DFE jmp main+1E1h (01A5E21h)
{
case RED:
{
//cout << "red" << endl;
c1 += 1;
001A5E00 mov eax,dword ptr [c1]
001A5E03 add eax,1
001A5E06 mov dword ptr [c1],eax
break;
001A5E09 jmp main+1E7h (01A5E27h)
}
case GREEN:
{
//cout << "green" << endl;
c1 += 2;
001A5E0B mov eax,dword ptr [c1]
001A5E0E add eax,2
001A5E11 mov dword ptr [c1],eax
break;
001A5E14 jmp main+1E7h (01A5E27h)
}
case BLUE:
{
//cout << "blue" << endl;
c1 += 3;
001A5E16 mov eax,dword ptr [c1]
001A5E19 add eax,3
001A5E1C mov dword ptr [c1],eax
break;
001A5E1F jmp main+1E7h (01A5E27h)
}
default:
{
//cout << "unknown color" << endl;
c1 += 0;
001A5E21 mov eax,dword ptr [c1]
001A5E24 mov dword ptr [c1],eax
break;
}
}
return 0;
001A5E27 xor eax,eax
}

在分支不是很多的情况下,两者差异不大,但如果分支很多的话,switch的效率更高。从汇编看switch更像一个表结构,if是个树结构。

使用场景的区别:

  1. switch只支持常量值固定相等的分支判断;
  2. if可以做区间范围判断;
  3. switch能做的if都可以,但反之不行。

性能比较:

  1. 分支少的时候,差别不是很大,分支多时,switch性能较高;
  2. if开始处的几个分支效率高,之后效率递减;
  3. switch的所有case速度几乎一样。

循环结构

C++中一共提供了三种循环语句:while、do while、for

三种循环结构

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
//计算1 + 2 + …… + 99 + 100的和

#include <iostream>
using namespace std;
int main()
{ // TODO: 1+2+3+4...+100
// while语句
int sum = 0;
int index = 1;
while (index <= 100)
{
sum += index;
index += 1;
}
cout << sum << endl;

// for语句
index = 1;
sum = 0;
for (index = 1; index <= 100; ++index)
{
sum += index;
}
cout << sum << endl;

// do-while语句
sum = 0;
index = 1;
do
{
sum += index;
index += 1;
} while (index <= 100);
cout << sum << endl;

return 0;
}

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
三种汇编代码的底层逻辑:
// while语句
int sum = 0;
002A17BE mov dword ptr [sum],0
int index = 1;
002A17C5 mov dword ptr [index],1
while (index <= 100)
002A17CC cmp dword ptr [index],64h
002A17D0 jg main+46h (02A17E6h)
{
sum += index;
002A17D2 mov eax,dword ptr [sum]
002A17D5 add eax,dword ptr [index]
002A17D8 mov dword ptr [sum],eax
index += 1;
002A17DB mov eax,dword ptr [index]
002A17DE add eax,1
002A17E1 mov dword ptr [index],eax
}
002A17E4 jmp main+2Ch (02A17CCh)
//cout << sum << endl;

// for语句
//index = 1;
sum = 0;
002A17E6 mov dword ptr [sum],0
for (index = 1; index <= 100; ++index)
002A17ED mov dword ptr [index],1
002A17F4 jmp main+5Fh (02A17FFh)
002A17F6 mov eax,dword ptr [index]
002A17F9 add eax,1
002A17FC mov dword ptr [index],eax
002A17FF cmp dword ptr [index],64h
002A1803 jg main+70h (02A1810h)
{
sum += index;
002A1805 mov eax,dword ptr [sum]
{
sum += index;
002A1808 add eax,dword ptr [index]
002A180B mov dword ptr [sum],eax
}
002A180E jmp main+56h (02A17F6h)
//cout << sum << endl;

// do-while语句
sum = 0;
002A1810 mov dword ptr [sum],0
index = 1;
002A1817 mov dword ptr [index],1
do
{
sum += index;
002A181E mov eax,dword ptr [sum]
002A1821 add eax,dword ptr [index]
002A1824 mov dword ptr [sum],eax
index += 1;
002A1827 mov eax,dword ptr [index]
002A182A add eax,1
002A182D mov dword ptr [index],eax
} while (index <= 100);
002A1830 cmp dword ptr [index],64h
002A1834 jle main+7Eh (02A181Eh)
//cout << sum << endl;

从底层逻辑上看,do while循环的效率最高,for循环的效率最低。

多层循环与循环优化

请输出所有形如aabb的四位完全平方数:

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
#include <iostream>
using namespace std;
#include <math.h>

int main()
{
// aabb的完全平方数

//方法一:一个显而易见的方法
int n = 0;
double m = 0;
for (size_t a = 1; a < 10; a++)
{// for1
for (size_t b = 0; b < 10; b++)
{// for 2
n = a * 1100 + b * 11; //aabb
//求平方根,看是否是整数
m = sqrt(n);
if (m - int(m) < 0.00000001) //当差值小于一个极小值即可,这个值和浮点数的精度有关
{
cout << n << endl;
}
}// for 2
}// for1


//方法二:逆向思维
//先保证是平方根,再看是否是aabb类型
//这样做第一不需要双层循环,第二避免了开平方根造成的精度丢失和低速运算
int high, low;

//不需要从1开始,31*31=961,32*32=1024
for (size_t index = 31; ; index++)
{
n = index * index; //n就是我们要构造的完全平方数
if (n < 1000)
continue; // 继续下一次循环
if (n > 9999)
break; // 退出循环
high = n / 100; // 4567/100 = 45
low = n % 100; // 4567%100 = 67
if ((high / 10 == high % 10) && (low / 10 == low % 10)) // 判断aa, bb
{
cout << n << endl;
}
}

return 0;
}

函数

  一个C++程序是由若干个源程序文件构成,一个源程序是由若干函数构成,函数将一段逻辑封装起来,便于复用;

  从用户角度看,函数分成:

  • 库函数:标准函数,由C++系统提供;
  • 用户自定义函数:需要用户自定义后使用

  函数的组成部分:

  1. 返回类型:一个函数可以返回一个值;
  2. 函数名称:函数的实际名称,函数名和参数列表一起构成了函数签名,函数签名才是被调用的真正名字;
  3. 参数:参数列表包括函数参数的类型、顺序、数量。参数是可选的,可以不包含参数;
  4. 函数主体:函数主体包含一组定义函数执行任务的语句。

参数传递

  函数的参数传递分为形参和实参。形参出现在函数定义中,在整个函数体内都可以使用,离开函数体则不能使用。实参出现在主调函数中,进入被调函数中,实参变量也不可以使用。形参和实参的功能是做数据传送。发生函数调用时,主调函数把实参的值传送给被调函数的形参,从而实现主调函数向被调函数的数据传送。

  C语言的传递方式有两种:值传递和指针传递;C++新增了引用传递。

  给函数传递实参遵循变量初始化的规则。非引用类型的形参以相应实参的副本初始化,需要给形参分配存储单元,对形参的任何修改仅作用于副本,并不影响实参本身。如果传递的是对象,还将调用拷贝构造函数。

  为了避免传递副本的开销,可以将形参指定为引用类型。传递引用不会再内存中产生实参的副本,是直接对实参操作。因此当传递的参数数据较大时,用引用比普通参数传递的效率和空间占用都要好。对引用形参的任何修改会直接影响实参本身,所以如果不需要修改相应实参,可以将引用形参定义为const。

  使用指针作为函数的实参虽然也能达到使用引用的效果,但在被调函数中同样要给形参分配存储单元,且需要重复使用“*指针变量名”进行运算。这很容易产生错误且程序阅读性较差。且在主调函数的调用点必须传递实参的地址,相比较引用更复杂不清晰。

  不过我们可以传递指针的引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//两个数交换:
int swap(int& p, int& q)
{
int temp = p;
p = q;
q = temp;
}


//两个指针交换:
int ptrswap(int* &v1. int* &v2) //v1是个引用,于指向int型对象的指针关联
{
int* tmp = v2;
v2 = v1;
v1 = tmp;
}

默认参数

  在函数声明或定义时,直接对参数赋值就是默认参数。在函数调用时没有指定与形参相对应的实参时就自动使用默认参数。

1
2
3
int mal(int a, int b = 3, int c = 6, int d = 8)

int mal(1); //b、c、d使用默认参数

  默认参数只可以在函数声明中设定一次。只有在无函数声明时,才可以在函数定义中设定。

  默认参数定义的顺序为自右到左。如果一个参数设定了默认值,那么其右边的参数都要有默认值。

  默认值可以是全局变量、全局常量,甚至是一个函数,但不可以是局部变量。因为默认参数的调用是在编译器确定的,局部变量的位置与默认值在编译时无法确定。

可变参数

  编程时可能会遇到一些参数可变的函数,比如printf()函数,原型如下,除了有一个参数format固定外,后面跟的参数的个数和类型是可变的,用三个点“...”做参数占位符

1
int printf(const char* format, ...);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//接受可变参数的函数实现多个数相加
int add(int num, ...)
{
int sum = 0;
int index = 0;
int* p = (int*)&num + 1;
for(; index < num; ++index){
sum += *p++;
}
}

int main()
{
int i = 1, j = 2, k = 3;
cout << add(3, i, j, k);
return 0;
}

函数重载与Name Mangling

函数重载Overload,函数的名称一样,但参数列表不同:

1
2
3
int test(int a);
int test(double a);
int test(int a,double b);

程序是怎么选择调用哪个函数的呢?这就引入了Name Mangling,给重载的函数不同的签名,以避免调用时的二义性调用。

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;
#include <math.h>

int test(int a)
{
return a;
}

int test(double a)
{
return int(a);
}

int test(int a, double b)
{
return a + b;
}

int main()
{
int result = test(1);
result = test(2.0);
result = test(1, 2.0);

return 0;
}

观察编译生成的.obj中间代码,可以看到有三个test函数:

1
2
3
//?test@@YAHH@Z 
//?test@@YAHN@Z
//?test@@YAHHN@Z

我们用VS自带的undname.exe工具,观察其中的一个可以看到:

即在程序中存储的是程序的签名。需要注意函数重载和函数的返回值无关,只和参数列表有关,返回值类型不影响函数的重载。如果参数类型相同,返回值不同,编译器会报错,这不是重载。

  函数重载减少了函数命名的数量,避免了名字空间的污染。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
#include <iostream>

int test(int a)
{
return a;
}

int test(double a)
{
return int(a);
}

int test(int a, double b = 2.0)
{
return a + b;
}

int main()
{
//int test(int a)和int test(int a, double b = 2.0)都满足下面这个调用方法
int result = test(1);

//为了解决重载问题,可以使用函数指针调用
int(*p)(int); //定义一个指针p,表示参数是int,返回值是int的一个函数
p = test; //指针变量赋值,此时虽然有三个同名函数,但明确指向的是参数为int的函数
int result = (*p)(1); //函数指针的使用,此时*p是间接引用


return 0;
}

要注意区别指向函数的指针和返回指针的函数:

  • 每一个函数都占用一段内存单元,我们可以通过内存访问到函数,它们有个起始地址,我们可以用一个指针指向起始地址。指向函数入口地址的指针称为函数指针;

函数指针一般形式:数据类型(*指针变量名)(参数表)

  • 区分与返回指针的函数的区别
    • int(*p)(int); //是指针,指向一个函数的入口地址
    • int* p(int); //是函数,返回的值是int指针
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
#include <iostream>
using namespace std;

int MaxValue(int x, int y)
{
return (x > y) ? x : y;
}

int MinValue(int x, int y)
{
return (x < y) ? x : y;
}

int Add(int x, int y)
{
return x + y;
}

bool ProcessNum(int x, int y, int(*p)(int a, int b)) //参数中有函数指针
{
cout << p(x, y) << endl;
return true;
}

int main()
{
int x = 10, y = 20;

//直接用函数名做参数,只要保证函数的参数列表是两个int,返回值是int就可以
ProcessNum(x, y, MaxValue);
ProcessNum(x, y, MinValue);
ProcessNum(x, y, Add);

return 0;
}

  上文的例子中,bool ProcessNum(int x, int y, int(*p)(int a, int b))参数有函数指针,我们把函数名传递进去,真正的调用是在这个函数体内部的。我们把这样的调用方式称为回调函数

命名空间

  开发过程中,可能会出现相同的函数签名,但内部实现不一样的情况。为了解决这个问题,可以使用命名空间。

  命名空间这个概念,可作为附加信息来区分不同库中相同名称的函数、类、变量等,命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。

  关键词:using和namespace的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;

int test(int a)
{
return a;
}

namespace AA
{
int test(int a)
{
return a + 1;
}
}

int main()
{
cout << test(1) << endl; //全局的

cout << AA::test(1) << endl; //AA中的

return 0;
}

  在开发中,命名空间的使用非常广泛,尤其是使用第三方库的时候。程序中常用的cout就属于std命名空间。

内联函数

  内联函数用inline修饰,分为两种:

  1. 成员函数为内联

  类中定义的成员函数默认全是内联的,可以显式的加上inline标识符,也可以不加。如果类中声明的函数没加inline,但类外定义时加了inline,该成员函数也是内联函数。

  1. 普通函数为内联

  通常在编译时,调用内联函数的地方将不使用函数调用,而是使用函数体直接替换调用处的函数名,形式类似宏替换,这种替换称为内联扩展。内联扩展可以消除函数调用时的时间开销。inline对编译器来说只是一个建议,编译器可以选择忽略。一般来说内联机制适用于优化小的、只有几行的但被经常调用的函数。大多数的编译器都不支持递归函数的内联。

1
2
3
4
inline int MaxValue(int x,int y)
{
return (x > y) ? x : y;
}

递归函数

  递归是重要的程序设计方法。如果在一个函数、过程或数据结构的定义中又调用了它自身,那么这个函数、过程或数据结构称为是递归定义的。

数学归纳法

数学归纳法是证明当n等于任意一个自然数时某命题成立。证明步骤分两步:

  1. 证明当n=1时命题成立;
  2. 假设n=m时成立,那么可以推导出在n=m+1时命题也成立(m为任意自然数)

递归背后的数学逻辑就是数学归纳法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
斐波那契数列:112358132134,……

1 n=1,2
Fib(n) = Fib(n-1)+Fib(n-2) n>2

程序实现:
int Fib(int n)
{
if(n == 0)
return 0;
else if(n == 1)
return 1;
else
return Fib(n-1) + Fib(n-2);
}

递归的基本法则

  1. 基准情形:必须存在无需递归就能解决的场景;
  2. 不断推进:每一次递归调用都必须使求解状况朝着接近基准情形的方向推进;
  3. 设计法则:假设所有的递归调用都能运行;
  4. 合成效益法则:求解一个问题的同一个实例时,切勿在不同的递归调用中做重复性的工作;

递归的优化

  递归是一种重要的编程思想,很多重要的算法都包含递归的思想;但递归在时间和空间上都有很大的缺陷:空间上需要开辟大量的栈空间;时间上可能需要大量的重复运算。

递归优化思路:

  1. 尾递归:所有递归形式的调用都出现在函数的末尾;
  2. 使用循环替代;
  3. 使用动态规划,空间换时间;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用循环来代替递归调用
int Fib3(int n)
{
if (n < 2)
{
return n;
}
int n0 = 0, n1 = 1;
int temp;
for (int i = 2; i <= n; i++)
{
temp = n0;
n0 = n1;
n1 = temp + n1;
}
return n1;
}

//循环调用避免了每次递归创建的函数入栈操作,减小空间使用
//也减少了重复运算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//尾递归
int Fib2(int n, int ret0, int ret1) //ret0和ret1入参是斐波那契的第一个值和第二个值
{
if (n == 0)
{
return ret0;
}
else if (n == 1)
{
return ret1;
}
return Fib2(n - 1, ret1, ret0 + ret1);
}

//使用普通递归,return Fib(n-1) + Fib(n-2)是一个表达式,函数堆栈很复杂
// 每次递归调用都需要保存多个函数调用堆栈


//尾递归直接Fib2(n - 1, ret1, ret0 + ret1),只是简单的函数调用,编译器会帮我们做优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//动态规划
int g_a[1000]; // 全局的数组,记录斐波那契数列的前1000个值

int Fib4(int n)
{
//assert(n >= 0);
g_a[0] = 0;
g_a[1] = 1;
for (int i = 2; i <= n; i++)
{
if (g_a[i] == 0) //全局变量初始化值为0,g_a[i]==0代表没计算过
{
g_a[i] = g_a[i - 1] + g_a[i - 2];
}
}
return g_a[n];
}


//动态规划的思想就是空间换时间
//把曾经计算过的信息保存下来,不需要重复计算

extern "C"

  在C++程序中调用被C编译器编译后的函数,需要加extern "C"。

  C++语言是一种面向对象编程语言,为了支持重载机制,在编译器生成的汇编代码中,要对函数的名字进行一些处理,加入比如函数的返回类型等信息。在C语言中,值是简单的函数名字而已,不会加入其他的信息,也就是说:C++和C语言对产生的函数名字的处理是不一样的。这样在链接阶段若是按照C++的函数命名规则去查找C编译器编译的函数,就会出现链接错误,

  extern "C"告诉编译器该函数是用C编译器编译的,请用C的方式来链接它们,从而解决了此问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//用法1:
extern "C" void fun();


//用法2:
extern "C"
{
void fun();
}


//用法3,将这些函数放在头文件中:
extern "C"{
#include "head.h"
}

指针

  内存由很多内存单元组成。这些内存单元用于存放各种类型的数据。为了标识内存单元,计算机对内存的每个单元都进行了编号,这个编号就称为内存地址,内存地址决定了内存单元在内存中的位置。程序员并不需要记住这些内存地址,C++的编译器让我们可以通过名字来访问这些内存位置。

  指针本身就是一个变量,其符合变量定义的基本形式,它存储的值是内存地址。对于一个基本类型T,T* 是“到T的指针”类型,一个类型为T*的变量能够保存一个类型为T的对象的地址。

  通过一个指针访问它所指向的地址的过程称为间接访问(indirection)或者引用指针(dereferencing the point)。这个用于执行间接访问的操作符是单目操作符*。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main()
{
int a = 1;
float b = 3.14f;

//指针变量定义
int* c = &a; //c存储a的内存地址
float* d = &b; //d存储b的地址

cout << c << endl; //输出a变量的地址

//间接访问
cout << (*c) << endl; //等同于cout<<a<<endl;
cout << (*d) << endl;//等同于cout<<b<<endl;

return 0;
}

变量、地址和指针变量总结:

一个变量具有三个重要的信息:

  1. 变量的类型;
  2. 变量所存储的信息;
  3. 变量的地址位置

指针变量是一个专门用来记录变量地址的变量,通过指针变量可以间接访问另一个变量的值,这里的另一个变量也可以是个指针,这就是多级指针的问题;

左值与右值

字符串可以用字符数组表示,也可以用指针来表示:

1
2
3
4
5
6
7
8
9
10
int main()
{
//两种字符串表示
char strHello[] = { "hello" };
const char* pStrHello = "hello";

pStrHello = strHello; //正确,指针变量的值可以改变
strHello = pStrHello; //编译器报错,数组变量的值不允许改变
return 0;
}

strHello不可改变,strHello[index]的值可以改变;pStrHello可以改变,pStrHello[index]的值能否改变取决于所指的存储区域是否可变。这里就涉及到了左值与右值的概念,左值与右值是相对赋值运算符"="来说的,左边需要一个存储单元,右边需要一个值:

  • 左值:编译器为其单独分配了一块存储空间,可以取其地址的,左值可以放在赋值运算符左边(也可以放右边);
  • 右值:指的是数据本身;编译器没有分配存储空间,不能取到自身地址,右值只能放在赋值运算符右边

左值最常见的情况就是函数和数据成员变量的名字;右值是没有标识符、不可取地址的表达式,一般也称为“临时对象”。

比如:a = b + c;&a是允许的操作而&(b+c)不能通过编译,因此a是一个左值,(b+c)是一个右值;

C++原始指针

一般类型指针T*

T是一个泛型,泛指任何一种类型

1
2
3
4
5
6
7
int i = 4;
int* iP = &i;
cout<<(*iP)<<endl; //这俩的*含义不一样,一个是指针,一个是间接引用

double d = 3.14;
double* dP =&d;
cout << (*dP) << endl;

不论T是什么类型,T*这个指针的内存空间都是一样的,为4个字节

指针的数组 与 数组的指针

指针的数组 T* t[]:指针的数组仍然是数组,里面每个值是个指针(array of pointers)

数组的指针 T(*t)[] :一个指针,指向一个数组(a pointer to an array)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int* a[4];  //一个数组,每个元素都是int指针

int c[4] = {1,2,3,4};
int(*b)[4]; //声明一个数组指针,可以指向包含4个int元素的数组
b = &c; //数组的个数一定要匹配!这里是&c,c的类型是int*,&c的类型是int(*)[4]

//将数组c中的元素赋值给数组a:
for(unsigned int i = 0; i < 4; i++)
{
a[i] = &(c[i]);
}


int *p = a; //p是指向a数组首元素的指针

//输出数组内容:
cout<<*(a[0])<<endl; //先取数组下标得到地址,然后做间接访问
cout<<(*b)[3]<<endl; //b是指针,先间接访问取值得到数组,然后取数组下标

const与指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
char strHello[] = { "helloworld" };

char const* pStr1 = "helloworld"; //修饰char,指针指向的地址可变,但是存储的区域中的内容不可变
char* const pStr2 = "helloworld"; //修饰char*,指针指向的地址不可变。在最新的VS中编译不过,因为"helloworld"是常量,必须要用const指针
const char* const pStr3 = "helloworld"; //地址和空间中的内容都不允许改变

pStr1 = strHello;
//pStr2 = strHello; //错误,pStr2地址不可改
//pStr3 = strHello; //错误,pStr3地址不可改

//pStr1[1] = 'a'; //错误,存储区内的char不可改变
pStr2[1] = 'a';
//pStr3[1] = 'a'; //错误,存储区内的char不可变

return 0;
}

如何确定const修饰的内容:

  • 看左侧最近的部分
  • 如果左侧没有,则看右侧

指向指针的指针

1
2
3
int a = 123;
int* b = &a;
int** c = &b; //二级指针

*操作符具有从右向左的结合性,**c 这个表达式相当于 *(*c),必须从里向外逐层求值 *c得到的是c指向的位置,即b的地址;**c相当于 *b,间接引用得到变量a的值。

下表是上面例子的一些变量表示:

表达式
a 123
b &a
*b a,123
c &b
*c b,&a
**c *b,a,123

未初始化指针和非法指针

1
2
int* a;  //只声明不赋值,a指向哪里完全不知道
*a = 12;//错误!!

  上述操作并没有对指针a进行初始化,也就是说我们并不知道a最终会指向哪里。运气好的话定位到一个非法地址(程序不能访问的地址),程序会出错从而崩溃终止。最坏的情况下,a定位到了一个可以访问的地址,这样我们就无意间修改了它,这样的错误难以捕捉,引发的错误与原先用来操作的代码毫不相干,我们根本无法定位。

用指针进行间接访问之前,一定要确保它已经初始化,并且被恰当的赋值。

NULL、nullptr和void*

NULL指针是一个特殊的指针变量,表示不指向任何东西。

1
int* a = NULL;

NULL指针的概念非常有用,它给了一种方法,来表示特定的指针目前未指向任何东西。

  • 对于一个指针,如果已经知道将被初始化为什么地址,那么请给他赋值,否则请把它设置为NULL,这样可以有效避免不可确定性访问的问题;
  • 在对一个指针间接引用前,先判断这个指针的值是否为NULL;
  • 指针使用完成后也请重新赋值为NULL;

  

  在早期的C语言中,编译器定义:#define NULL ((void*)0)。void*是一个很万能的指针,可以转换任意的类型。参数传递时经常用到,函数编写时无法预测将来要传递什么信息,就用void*代替。

  C++语言诞生时没有把NULL定义为void*,而是定义为了0:

1
2
3
4
5
6
7
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#endif

  C++中NULL表示为int而不是指针,和C语言的差异会导致问题,所以C++11中用nullptr来代替(void*)0,NULL只表示0。在新的C++标准中,空指针尽量使用nullptr来表示。

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
void func(void* i)
{
cout << "func(void* i)" << endl;
}
void func(int i)
{
cout << "func(int i)" << endl;
}

int main()
{
int* pi = NULL;
int* pi2 = nullptr;
char* pc = NULL;
char* pc2 = nullptr;
func(NULL); // func(int i)
func(nullptr); // func(void* i)
func(pi); // func(void* i)
func(pi2); // func(void* i)
func(pc); // func(void* i)
func(pc2); // func(void* i)



return 0;
}

函数指针

  函数指针是指指向函数而非指向对象的指针。像其他指针一样,函数指针也指向某个特定的函数类型,函数类型由其返回类型以及形参列表确定,与函数名无关:

1
2
3
4
5
bool (*pf)(const string&, const string&)

//pf声明为指向函数的指针
//它所指向的函数带有两个const string&类型形参和bool类型的返回值
//*pf两侧的圆括号是必须的

  函数指针类型相当冗长。使用typedef为指针类型定义同义词,可以将函数指针的使用大大简化。对函数指针初始化时,可以直接使用函数名。直接引用函数名等效于在函数名上应用取地址操作符。指向函数的指针可用于调用它所指向的函数。

1
2
3
4
5
6
7
8
9
10
11
typedef bool (*cmpFcn)(const string&, const string&);

//cmpFun是一种指向函数的指针类型的名字。
//该指针类型为“指向返回bool类型并且带有两个const string&引用形参的函数的指针”
//在使用这种函数指针类型时,只需直接使用cmpFcn即可,不必每次都把整个类型声明全部写出来

bool lengthCompare(const string&, const string&);
cmpFun pf = lengthCompare;
lengthCompare("h1", "bye"); //直接调用
pf("h1", "bye"); //利用函数指针调用,未使用*
(*pf)("h1", "bye"); //利用函数指针调用,使用*

  函数的形参也可以是指向函数的指针,可以用以下两种方式编写:

1
2
void useBigger(const string&, const string&, bool(const string&,const string&));
void useBigger(const string&, const string&, bool(*)(const string&,const string&));

  函数的返回值也可以返回指向函数的指针,但是写出这种返回类型相当不容易:

1
2
3
4
5
6
7
8
9
int (*ff(int))(int*, int);

//由里向外理解
//先看ff(int),ff声明为一个函数,带有一个int形参
//ff数返回int(*)(int*, int)

//为了方便理解,可以这么写:
typedef int (*PF)(int*, int);
PF ff(int);

野指针

野指针是指向“垃圾”内存的指针。if等判断对它们不起作用,因为没有置为NULL,它存有值,但是我们用不了;

一般情况下有三种情况被称为野指针: 1. 指针变量没有初始化; 2. 已经释放不用的指针没有置为NULL,如delete和free之后的指针; 3. 指针操作超越了变量的作用域范围(指针指向具有一定生命周期的空间);

没有初始化的,不用的或者超出范围的指针,请一定置为NULL

指针的基本运算

  C/C++常常把地址当作整数处理,但不意味着程序员可以对地址进行各种算术操作。指针可以做的运算有限,像指针与其他变量的乘除、两个指针之间的乘除、两个指针相加都是没有意义、不被编译器接受的。

算数运算

  指针加上一个整数的结果是另一个指针。当一个指针和一个整数量进行算术运算时,整数在执行加法运算前始终会根据合适的大小进行调整,这个“合适的大小”指的是指针所指向的类型的大小。假设某台机器float占据4字节,则float类型指针+3时,这个3将根据float的大小变为12,即指针增加3个floag的大小。

  也可以进行指针之间的算数运算,但只有两个指针指向同一个数组中的元素时才允许这种运算,如果不是同一个数组中的元素,相减的结果是未定义的。减法的运算表示的是两个指针在内存中的距离,以数组元素的长度为单位,而不是以字节为单位

&与*操作符

1
2
char ch = 'a';
char* cP = &ch;

  &操作符不能做左值,&操作编译器做是事情是把变量的地址位置取出来,然后放在内存空间中。但是他本身并不是变量自身,仅仅是一块空间存储着变量地址,这块空间的地址我们的程序是没办法获取到的。就像上图,&ch操作拿到的是ch变量的地址,但取出的信息不会像cp这个变量一样有一块能获取地址的内存空间来存储。一定要注意,虽然cp是ch的地址,&ch也是ch的地址,但这俩不是一个概念,只是恰好存的东西一样。

  

1
2
3
*cp = 'b';  //左值,取的是空间

char tmp = *cp; //右值,取的是内容

  间接引用操作当用作左值的时候,实际的操作是把变量ch当前的位置取出来(取空间),这种操作我们可以对这块空间进行操作,比如赋值操作;当我们把他当作右值时,实际的操作取的就不是存储空间,而是存储空间中的值。

  

  *cp + 1首先得到cp中的值,得到a,做+1操作就是对ASCII码进行操作,得到b。但是这个操作还是由编译器创造一块空间取值,我们得不到这个变量的地址,不能做左值。这个+1的操作是按照cp的类型来做加法的,移动的是cp这个类型的大小。

  *(cp+1)操作我们先做了+1,而cp本身是个指针,我们做的是指针的加法,得到的是ch这个变量的地址的后面那个地址(做这个操作前要确定cp指向的地址后面的内容是可以访问的)。这个操作也是可以用作左值和右值,左值就是取地址,右值就是取空间中存储的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main()
{
char ch = 'a';

//&操作符
//&ch = 97; //错误,&ch左值不合法
char* cp = &ch; //&ch做右值
//&cp = 97; //错误,&左值不合法
char** cpp = &cp; //&cp右值


//*操作符
*cp = 'a'; //*cp左值取变量ch的位置
char ch2 = *cp; //*cp右值取变量ch存储的值
//*cp + 1 = 'a'; //错误,*cp+1左值不合法的位置
ch2 = *cp + 1; //*cp+1右值取到的字符做ASCII码+1操作
*(cp + 1) = 'a'; //左值,语法上合法,访问到cp后面的位置,赋值为a.一定要保证这个位置是可以访问的,这种操作有风险
ch2 = *(cp + 1); //右值操作,取ch后面的位置的值
}

指针的++ 与 --

1
2
3
4
5
6
7
char* cp2 = ++cp;
//汇编代码:
mov eax,dword ptr [cp] //eax是寄存器,dwptr存储cp指针。把指针内容放置寄存器内
add eax,1 //寄存器数据+1
mov dword ptr [cp],eax //把寄存器内容存回cp中
mov ecx,dword ptr [cp] //把cp的内容放置在ecx寄存器
mov dword ptr [cp2],ecx //把寄存器ecx内容放置在cp2中
1
2
3
4
5
6
7
char* cp3 = cp++;
//汇编代码:
mov eax,dword ptr[cp] //把cp指针内容放置在eax寄存器中
mov dword ptr[cp3],eax //把eax内容直接放在cp3指针
mov ecx,dword ptr[cp] //把cp信息放置在exc寄存器中
add ecx,1 //ecx+1操作
mov dword ptr[cp],ecx //ecx内容写入cp指针

前置操作先做加法再赋值,后置操作先赋值后做加法操作。自减操作符和自增操作符相同,前置操作先做减法再赋值,后置操作先赋值再做减法。

  

自增/自减操作获得的地址不能当作左值,它得到的只是个地址的副本,没有明确的变量来存储它的位置。

  

++操作符优先级高于*,先计算地址偏移;

++++和----等运算符连续

  编译器程序分解符号的方法是:一个字符一个字符的读入,如果该字符可能组成一个符号,那么读入下一个字符,一直到读入的字符不能组成一个有意义的符号。这个处理过程称为“贪心法”。

1
2
3
4
5
int a = 1,b=2
int c;
int d;
c = a+++b; //相当于a++ +b。连续读取,当读取到两个+号时不能再组成新符号了
d = a++++b; //相当于a++ ++b。这个是错误的 ,两个表达式之间不构成任何运算

关系运算符

  指针可以进行<、<=、>、>=运算,不过前提是它们都指向同一个数组中的元素。根据所使用的操作符,比较表达式将告诉我们哪个指针位于数组中更前或更后的元素。

C++程序的存储区域划分

栈和队列

  数据结构中有两种常见的结构,一种是栈结构,先进入的数据会被压在栈底,后进入的数据会被放在栈顶,是一种先进后出的结构;还有一种是队列结构,和栈相反,类似于生活中的队列,先进入的数据会先出队列。

  在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
#include <iostream>
using namespace std;

int a = 0; //GVAR全局初始化区
int* p1; //BSS全局未初始化区

int main() //text 代码区
{
int b = 1; //stack栈区
char s[] = "abc"; //stack栈区
int* p2 = NULL; //stack栈区

const char* p3 = "123456"; //"123456\0"在常量区,p3在stack栈区

/*
char* p3 = "123456";这条语句“123456”在内存中只有一份拷贝,p3只是个指针变量
char s[] = "abc";这条语句,“abc”在内存中有两份拷贝,一份在字符常量区,一份在s所在的栈区
*/

static int c = 0; //GVAR全局(静态)初始化区

p1 = new int(10); //heap堆区变量
p2 = new int(20); //heap堆区变量

char* p4 = new char[7]; //heap堆区变量
strcpy_s(p4, 7, "123456"); //text代码区

if (p1 != NULL)
{
delete p1;
p1 = NULL;
}

if (p2 != NULL)
{
delete p2;
p2 = NULL;
}

//p3指向常量区,由内存接管

if (p4 != NULL)
{
delete[] p4;
p4 = NULL;
}

return 0;
}

通过调试上面的代码,可以观察到一些程序中的地址分布:

  上图是栈区变量b,s,p2的地址空间,可以看到虽然我们定义变量的顺序是b,s,p2,但是内存空间的地址位置是从高地址到低地址变化的,越早分配的变量,拿到的地址位置越高。

  再观察p3变量。看p3本身的地址可以观察到它的地址分配再p2的上面,因为都是栈区变量,但是内部存储的一个地址并不是在栈区,是在常量区。在常量区中的内容,我们是无法修改的。这就是指针变量的特点,可以指向不同的位置。如果p2指向的是一个字符数组,那么指向的就是栈区空间,是可以改变的。

  继续观察p1,p1是在函数之外声明的,看地址也可以观察到,它的地址和b,s相差很大,可知它并不在栈区。这种定义在函数外的变量属于全局的区域。

  当p1和p2执行完new操作后,观察p1和p2指向的地址空间,发现两个区域相邻。而且p1先new,p2后new,地址空间p2指向的地址也比p1要高。new操作会产生新的区域,我们称为堆区,和栈区相反,内存分配方式由低地址向高地址分配。

  再看p4,p4本身是在main函数中定义的,是栈区变量。它new的是一个char型的数组,new出的地址和p1和p2指向的地址也很接近,可知也是堆区内。

对存储区域做一个总结,如下图:

动态分配资源--堆区

  1. 从现代编程语言的观点来看,使用堆,或者说使用动态内存分配,是一件很自然的事情;
  2. 动态内存带来了不确定性:内存分配耗时需要多久(分配大空间不好控制)?分配失败了怎么办?在实时性要求很高的场合,如嵌入式控制器和电信设备,这些不确定性是很严重的;
  3. 一般而言,当我们在堆上分配内存时,很多语言会使用new这样的关键字,也有些语言是隐式分配,不使用new的语义,但使用的是new的方式。在C++中new对应词是delete,因为C++是允许程序员完全接管内存的分配释放的。

分配和回收动态内存的原则

程序通常需要牵扯到三个内存管理器的操作:

  1. 分配一个某大小的内存块;
  2. 释放一个之前分配的内存块;
  3. 垃圾收集操作,寻找不再使用的内存块并给予释放;

这个回收策略需要实现性能、实时性、额外开销等各方面的平衡,很难有统一和高效的做法。C++语言使用了1和2;Java使用了1和3。

资源管理方案--RAII(Resource Acquisition Is Initization)

  • 这是C++特有的资源管理方式,主流的编程语言中,C++是唯一一个依赖RAII来做资源管理的,核心思想是分配资源的时候就可以管理资源;
  • RAII依托析构函数,来对所有的资源--包括堆内存在内进行管理。比如一个对象在构造和析构中就把资源管理起来,当对象生存空间超出后进入析构状态,我们就可以进行资源的释放。RAII的使用,使得C++不需要类似于Java哪样的垃圾收集方法也能有效管理内存。
  • RAII有些比较成熟的智能指针代表,如std::auto_ptr和boost::stared_ptr。

C++的几种变量的对比

stack heap
作用域 函数体内,语句块{}作用域,超出后被系统回收 整个程序范围内,由new、malloc开始,delete、free结束
编译期间大小确定 变量大小范围确定 需要运行期间才能确定
大小范围 Windows默认1M,Linux默认8M或10M,注意空间很小,不要分配大内存变量 所有系统的堆空间上限接近内存(虚拟内存)总大小(有一部分被OS占用)
内存分配方式 地址由高到底减少 地址由低到高增加
内容是否可变 可变 可变
全局静态存储区 常量存储区
存储内容 全局变量,静态变量 常量
编译期间大小是否确定 确定 确定
内容是否可变 可变 不可变

内存泄漏(Memory Leak)问题

  内存泄漏指的是程序中已经动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果;

  内存泄漏主要发生在堆内存分配方式中,即“配置了内存后,所有指向该内存的指针都遗失了”。如果缺乏垃圾回收机制,这样的内存片就无法归还系统;

  因为内存泄漏属于程序运行中的问题,无法通过编译识别,所以只能在程序运行过程中来判别和诊断。

比指针更安全的解决方案

  使用指针是非常危险的行为,可能存在空指针,野指针的问题,并可能造成内存泄漏问题。可是指针又非常高效,所以我们希望以更安全的方式来使用指针。一般有两种典型方案:

  • 使用更安全的指针:智能指针;
  • 不使用指针,使用更安全的方式:引用;

C++的智能指针

  C++推出了四种常见的智能指针:unique_ptr、shared_ptr、weak_ptr和C++11中已经废弃(deprecated)的auto+ptr,C++17中auto_ptr已经被正式删除。

auto_ptr

  auto_ptr是一种简单直接的智能指针,可以指向一个泛型对象。我们由new获得的对象在堆区中,这不是一个特别合理的行为,因为指针指向对象不是一种强关联的关系。

  需要注意,auto_ptr的析构函数中删除指针用的是delete而不是delete[],所以不应该用auto_ptr来管理指针数组。同时因为STL标准容器需要用到大量的拷贝赋值操作,auto_ptr也不能用于STL中。

  所有权转移:有一个auto_ptr指向一个对象,如果我们不小心把对象传递给另外的智能指针(即有另一个auto_ptr指向了原来的对象),原来的指针就不再拥有这个对象了。这个操作是通过C++中的拷贝构造和赋值完成的,会直接剥夺指针对源对象内存的控制权。被剥夺后,对象内存的所有权转移给新指针,然后将原对象指针置为nullptr。这个问题影响很大,如果将auto_ptr作为函数参数按值传递,因为函数在调用过程中会产生一个局部对象接收传入的auto_ptr(拷贝构造),会导致传入的实参失去对原有对象的控制权,原对象在函数退出后被局部auto_ptr删除。因为这个问题,导致auto_ptr存在很大的安全隐患,这是被废弃的重要原因。

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
#include <string>
#include <iostream>
#include <memory> //智能指针头文件
using namespace std;
int main()
{
{// 确定auto_ptr失效的范围
// 对int使用
auto_ptr<int> pI(new int(10)); //关注pI的生命周期范围和指向的堆区空间的声明周期范围
cout << *pI << endl; // 10

// auto_ptr C++ 17中移除 拥有严格对象所有权语义的智能指针
// auto_ptr原理:在拷贝 / 赋值过程中,直接剥夺原对象对内存的控制权,转交给新对象,
// 然后再将原对象指针置为nullptr(早期:NULL)。这种做法也叫管理权转移。
// 他的缺点不言而喻,当我们再次去访问原对象时,程序就会报错,所以auto_ptr可以说实现的不好,
// 很多企业在其库内也是要求不准使用auto_ptr。

//对字符串数组使用
auto_ptr<string> languages[5] = {
auto_ptr<string>(new string("C")),
auto_ptr<string>(new string("Java")),
auto_ptr<string>(new string("C++")),
auto_ptr<string>(new string("Python")),
auto_ptr<string>(new string("Rust"))
};
cout << "There are some computer languages here first time: \n";
for (int i = 0; i < 5; ++i)
{
cout << *languages[i] << endl;
}
auto_ptr<string> pC;
pC = languages[2]; // languges[2] loses ownership. 将所有权从languges[2]转让给pC!!!
//此时languges[2]不再引用该字符串从而变成空指针
cout << "There are some computer languages here second time: \n";
for (int i = 0; i < 2; ++i)
{
cout << *languages[i] << endl;
}
cout << "The winner is " << *pC << endl;
//下面会报错
//cout << "There are some computer languages here third time: \n";
//for (int i = 0; i < 5; ++i)
//{
// cout << *languages[i] << endl;
//}
}

//出了大括号,auto_ptr的内存被回收
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//xmemory文件

//观察auto_ptr的模板可以看到它是怎么实现资源释放和所有权转移的

//资源释放:是通过析构来完成的
~auto_ptr() noexcept {
delete _Myptr;
}


//所有权转移,观察赋值的重载:
auto_ptr& operator=(auto_ptr& _Right) noexcept {
reset(_Right.release()); //保存原有的地址,把原有保存这个地址的指针置成nullptr
return *this;
}

unique_ptr

  auto_ptr提供了自动管理内存的一个方法,但是它和对象的耦合性太紧了,如果多方操作对象很容易出问题,所以推出了unique_ptr。unique_ptr是专属所有权,所以被unique_ptr管理的内存,只能被一个对象持有,不支持复制(参数传递)和赋值(=)操作。

  移动语义:虽然unique_ptr禁止了拷贝语义,但有时候我们也需要能够转移所有权,于是提供了移动语义,即可以使用std::move()进行所有权的转移。

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
#include <memory>
#include <iostream>
using namespace std;
int main()
{
// 在这个范围之外,unique_ptr被释放
{
auto i = unique_ptr<int>(new int(10));
//i在栈区,指向一个堆区分配的区域
cout << *i << endl;
}//大括号结束后,i自动释放,指向的堆区内部也自动delete

// unique_ptr
auto w = std::make_unique<int>(10);
cout << *(w.get()) << endl; // 10。get()方法返回的就是指针

//auto w2 = w; // 编译错误!!如果想要把 w 复制给 w2, 是不可以的。
// 因为复制从语义上来说,两个对象将共享同一块内存。

// unique_ptr 只支持移动语义, 即如下
auto w2 = std::move(w); // w2 获得内存所有权,w 此时等于 nullptr
cout << ((w.get() != nullptr) ? (*w.get()) : -1) << endl; // -1
cout << ((w2.get() != nullptr) ? (*w2.get()) : -1) << endl; // 10
return 0;
}

shared_ptr和weak_ptr

  unique_ptr在同一时间只能由一个指针持有对象,使用上具有局限性。所以推出了shared_ptr。

  shared_ptr通过一个引用计数共享一个对象,在这个机制上提供了可以共享所有权的智能指针,当然引用计数需要额外的开销。当引用计数为0时,说明该对象没有被使用,可以进行析构。

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
#include <iostream>
#include <memory>
using namespace std;
int main()
{
// shared_ptr
{
//shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。
auto wA = shared_ptr<int>(new int(20));
{
auto wA2 = wA; //shared_ptr可以赋值,同时指向对象
cout << ((wA2.get() != nullptr) ? (*wA2.get()) : -1) << endl; // 20
cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl; // 20
cout << wA2.use_count() << endl; // 打印引用计数 2
cout << wA.use_count() << endl; // 打印引用计数 2
}
//cout << wA2.use_count() << endl;
cout << wA.use_count() << endl; // wA2消亡,引用计数为1
cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl; // 20
//shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,
// 引用计数会 + 1。当一个 shared_ptr 离开作用域时,引用计数会 - 1。
// 当引用计数为 0 的时候,则 delete 内存。
}
//跳出作用域,wA也消亡,同时内存会被释放

// shared_ptr也支持move()语法
auto wAA = std::make_shared<int>(30);
auto wAA2 = std::move(wAA); // 此时 wAA 等于 nullptr,wAA2.use_count() 等于 1
cout << ((wAA.get() != nullptr) ? (*wAA.get()) : -1) << endl; // -1
cout << ((wAA2.get() != nullptr) ? (*wAA2.get()) : -1) << endl; // 30
cout << wAA.use_count() << endl; // 0
cout << wAA2.use_count() << endl; // 1
//将 wAA 对象 move 给 wAA2,意味着 wAA 放弃了对内存的所有权和管理,此时 wAA对象等于 nullptr。
//而 wAA2 获得了对象所有权,但因为此时 wAA 已不再持有对象,因此 wAA2 的引用计数为 1。

return 0;
}

  引用计数也会带来一个严重问题:循环引用。即存在一种情况,有两个对象,对象A内部有shared_ptr指针指向B,B中也有shared_ptr指向A,当A使用完毕打算回收内存空间时,会检查内部变量pA,此时会去尝试清理B,但B中也有pB指向A,此时循环引用会导致堆里面的内存无法正常回收,造成内存泄漏。

  为了避免这种循环引用,标准库提供了weak_ptr,被用来和shared_ptr共同工作,用一种观察者模式工作,获得资源的观测权,像旁观者那样观测资源的使用情况。比如两个对象A和B互为关联,但B只是想获取A的一些属性,并不需要A的所有权,那么可以用weak_ptr,指向A但是并不拿A的引用计数。因为B没有A的引用计数,那么A销毁的时候,B也可以同时销毁。这就是观察者模式,观察者意味着weak_ptr只对shared_ptr进行引用,而不改变其引用计数,当被观察的shared_ptr失效后,相应的weak_ptr也失效。

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
#include <string>
#include <iostream>
#include <memory>
using namespace std;

struct B;
struct A {
shared_ptr<B> pb;
~A()
{
cout << "~A()" << endl;
}
};
struct B {
shared_ptr<A> pa;
~B()
{
cout << "~B()" << endl;
}
};

// pa 和 pb 存在着循环引用,根据 shared_ptr 引用计数的原理,pa 和 pb 都无法被正常的释放。
// weak_ptr 是为了解决 shared_ptr 双向引用的问题。
struct BW;
struct AW
{
shared_ptr<BW> pb;
~AW()
{
cout << "~AW()" << endl;
}
};
struct BW
{
weak_ptr<AW> pa;
~BW()
{
cout << "~BW()" << endl;
}
};

void Test()
{
cout << "Test shared_ptr and shared_ptr: " << endl;
shared_ptr<A> tA(new A());
shared_ptr<B> tB(new B());
cout << tA.use_count() << endl; //1
cout << tB.use_count() << endl; //1
tA->pb = tB;
tB->pa = tA;
cout << tA.use_count() << endl; // 2
cout << tB.use_count() << endl; // 2
}
void Test2()
{
cout << "Test weak_ptr and shared_ptr: " << endl;
shared_ptr<AW> tA(new AW());
shared_ptr<BW> tB(new BW());
cout << tA.use_count() << endl; // 1
cout << tB.use_count() << endl; // 1
tA->pb = tB;
tB->pa = tA; //tB->pa是weak_ptr,指向tA但不会对tA的引用计数产生影响
cout << tA.use_count() << endl; // 1
cout << tB.use_count() << endl; // 2
}

int main()
{
Test();
Test2();


return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上面代码输出:
Test shared_ptr and shared_ptr:
1
1
2
2
Test weak_ptr and shared_ptr:
1
1
1
2
~AW()
~BW()


可以看到weak_ptr不会对引用计数产生影响,而产生循环引用的地方不会发生析构

引用

  引用就是对象的另一个名字。引用在本质上仍然是是指针,只不过自身比较特殊,是不允许修改的指针。(我们常说java中没有指针,其实java中的指针就是引用)

在指针使用上,我们会遇到一些问题:

  1. 空指针
  2. 野指针(没有初始化)
  3. 不知不觉改变了指针的值,我们却仍然在使用

使用引用,我们可以避免这些问题:

  1. 不存在空引用;
  2. 引用在创建时就必须被初始化;
  3. 一个引用永远指向它初始化的那个对象,不允许被修改。

  sizeof(引用)得到的是所指向的变量或者对象的大小,sizeof(指针)得到的是指针本身的大小。如果返回动态分配的对象或内存,千万要使用指针,使用引用很可能引起内存泄漏。

  • 引用的基本使用:可以认为是指定变量的别名,使用时可以认为是变量本身:
1
2
3
4
5
6
int x1 = 1,x2 = 3;
int& rx = x1; //定义引用,可以认为rx是x1的别名
rx = 2;
cout << x1 << rx << endl; //x1和rx都是2
rx = x2; //引用一旦被初始化就不能更改,所以这里不是赋值rx为x2,而是x1=x2(别名直接替换)
cout << x1 << x2 << endl; //都是3
  • 当我们在函数中需要操作形参并且返回时一并返回,这时候我们就可以传递引用。
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
#include <iostream>
#include <assert.h>
using namespace std;

// 编写一个函数,输入两个int型变量a,b
// 实现在函数内部将a,b的值进行交换。
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
void swap2(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}

int main()
{
// 交换变量的测试
int a = 3, b = 4;
swap(a, b);
assert(a == 4 && b == 3);

a = 3, b = 4;
swap2(&a, &b);
assert(a == 4 && b == 3);

return 0;
}
  • 引用也可以做类的数据成员:引用类型成员的初始化不能在构造函数中执行,必须用到初始化列表;凡是有引用类型的数据成员,必须定义构造函数。
1
2
3
4
5
6
7
8
9
class ConstRef
{
public:
ConstRef(int ii) : i(ii), ci(i), ri(ii) { }
private:
int i;
const int ci;
int& ri;
};

  C++为什么要同时存在指针和引用?在java语言中我们直接使用引用,传统C语言我们都使用指针。C++可以认为是夹在C和java之间的一种。之所以要使用引用是为了支持函数的运算符重载。而C++为了兼容C语言不能摒弃指针。

  在函数传递参数的时候,对于内置基础类型(int、double等)而言,在函数中传递值更高效(pass by value);在面向对象中自定义类型而言,在函数中传递const引用更高效(pass by reference to const)。

  字符串是由零个或多个字符组成的有限串行。

  子串的定义:串中任意个连续的字符组成的子序列,并规定空串是任意串的子串,任意串是其自身的子串

字符串变量与常量

C风格的字符串包括两种:字符串常量和末尾添加了'\0'的字符数组。无论怎么表示,都以'\0'结尾。

字符串变量

  • 字符串是一个特殊的字符数组,以空字符'\0'结尾;
  • 空字符'\0'自动添加到字符串的内部表示中;
  • 在声明字符串变量的时候,要时刻记得为这个空结束符预留一个额外元素的空间:char str[11] = {"helloworld"},十个字符要留11个位置

字符串常量

  • 字符串常量就是一对双引号括起来的字符序列:"helloworld";
  • 字符串常量中的每个元素可以作为一个数组元素访问;
  • 字符串常量也是以'\0'结尾的;

字符串的指针表示

1
const char* pStrHelloWorld = "helloworld";

表示一个char型的指针变量,指向内存中一个存储字符串“helloworld”的地址。

定义字符串有两种方式,char[]和char*,要区分以下两个概念:

  1. 区分地址本身和地址存储的信息;
  2. 区分可变与不可变;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main()
{
//char strHello[10] = { "helloworld" }; //错误定义,没有给'\0'留空间

char strHello[11] = { "helloworld" };
char strHello[] = { "helloworld" }; //这样定义也可以
//strHello是数组变量,它本身的值是不允许改变的,但是它指向的区域的值strHello[index]是可以改变的
strHello[2] = '6'; //strHello = he6loworld
//strHello = { "helloworld" }; //本身不允许改变,编译不通过


//ptrHello是指针变量,它本身是可以改变的,但是ptrHello[index]指向的值可变与否要取绝与所指向的区间是否可以改变
const char* ptrHello_1 = "helloworld"; //指针指向常量区(必须要加const),ptrHello[index]不可改变,但本身指向的地址可以变。
ptrHello_1 = "change";

char tmp[7] = "change";
char* ptrHello_2 = strHello; //指针指向数组变量,ptrHello[index]可以改变,本身也可以变
ptrHello_2[2] = 'l';
ptrHello_2 = tmp;

return 0;
}

char* pstr是指针,pstr本身可变,pstr[index]是否可变取决于指向的存储区间是否可变;

char str[]是字符串数组,str本身的值不允许改变,但可以改变str[index];

字符串操作

头文件:string.h

  C/C++字符串处理函数,传递给这些标准库函数例程的指针必须具有非零值,并且指向以null结束的字符数组中的第一个元素。其中一些标准库函数会修改传递给它的字符串,这些函数将假定它们所修改的字符串具有足够大的空间接收新字符串,程序员需要保证目标字符串足够大。

字符串遍历

  一般来说,我们使用指针的算数操作来编译C风格的字符串,每次对指针进行测试并递增1,直到到达结束符null为止:

1
2
3
4
5
6
const char *cp = "helloworld";
while(*cp)
{
//do something
++cp;
}

字符串长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
strlen(s);    //返回字符串s的长度,要注意字符串的长度不包含'\0'。

sizeof(s); //返回字符串所占用的空间,包含'\0'


int main()
{
char strA[11] = "helloworld";
auto n1 = sizeof(strA); //11
auto n2 = strlen(strA); //10


wchar_t strB[11] = L"helloworld";
auto n3 = sizeof(strB); //22
auto n4 = wcslen(strB); //10

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//自定义实现strlen
int strlen(const char* str)
{
assert(str != NULL);
int len = 0;
while((*str++) != '\0'){
len++;
}
return len;
}

//或者不使用临时变量,用递归:
int strlen(const char* str)
{
assert(str != NULL);
return *str == '\0' ? 0 : (1 + strlen(++str));
}

字符串比较

1
2
3
4
5
strcmp(s1,s2); 
wcscmp(s1,s2);
//s1 == s2,返回0
//s1 < s2 ,返回值小于0
//s1 > s2 ,返回值大于0

两个字符串自左向右逐个字符相比,按照ASCII值大小来比较,直到出现不同字符或遇到'\0'为止;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//自定义实现strcmp:
int strcmp(const char* str1, const char* str2)
{
assert(str1 != NULL && str2 != NULL);
int ret = 0;
while(!(ret = *(unsigned char*)str1 - *(unsigned char*)str2) && str1)
{
str1++;
str2++;
}
if(ret < 0) ret = -1;
else if(ret > 0) ret = 1;

return ret;
}

字符串拷贝

1
2
3
strcpy(s1,s2);   //赋值字符串s2到字符串s1的地址空间

strncpy(s1,s2,n); //将字符串s2中的前n个字符拷贝到s1的地址空间

要注意一个陷阱,一定要保证字符串s1的内存大小能存的下要拷贝的s2的大小

1
2
3
4
5
6
7
8
//自定义实现strcpy
char* strcpy(char* strDestination, const char* strSource)
{
assert(strDestination != NULL && strSource != NULL);
char* strD = strDestination;
while((*strDestination++ = *strSource++) != '\0');
return strD;
}

拷贝还可以使用内存拷贝函数:

1
2
//从src所指向的内存地址起始位置开始拷贝n个字节到dest所指的内存地址的起始位置中。返回指向dest的指针
void* memcpy(void* dest, const* src, size_t n);

memcpy和strcpy的区别:

  1. 复制内容不同。strcpy只能复制字符串,而且不仅复制字符串内容,也复制结束符'\0';memcpy可以复制任意内容,用途更广;
  2. strcpy不需要指定长度,memcpy需要指定长度

字符串拼接

1
2
3
strcat(s1,s2);   //将字符串s2拼接到s1后面(覆盖s1的'\0'),返回s1

strncat(s1, s2, n); //将s2的前n个字符拼接到s1,并返回s1
1
2
3
4
5
6
7
8
9
10
11
12
//自定义实现strcat:
char *strcat(char* strDest, const char* strSrc)
{
char* address = strDest;
assert((strDest != NULL) && strSrc != NULL);
while(*strDest) //如果是strncat,这里用n作为结束标志
{
strDest++;
}
while(*strDest++ = *strSrc++);
return address;
}

这个更加要注意,s1的大小要足够存放拼接后的s1+s2字符串的大小

两个char*不能直接相加,字符串拼接必须要用这个函数。(指针相加是什么操作啊(#`O′))

字符串查找

1
2
strchr(s1,ch);   //指向字符串s1中字符ch第一次出现的位置
strstr(s1,s2); //指向字符串s1中字符串s2的第一次出现的位置

内容替换

1
2
//将s中的前n个字节用ch替换并返回s
void* memset(void* s, int ch, size_t n);

这个函数的作用是在一块内存中填充某个给定的值,是对较大的结构体或者数组进行清零的最快方法。

安全函数

C语言的字符串处理有其漏洞所在。上面的所有方法都无法做边界检查,如果不注意就会发生缓冲区溢出的问题,这个问题在编码时是很严重的事故。。现在我们往往在程序中使用更加安全的API函数:

上述所有API函数都有其_s版本,如strcpy_s(),在执行操作的同时要告诉系统当前可使用的缓冲区的大小。 strlen()的安全版本是strnlen_s(),传入字符串的同时要传入一个最大的边界,以防止某些字符串没有'\0'结束标志。

C++新型字符串--string类

C++标准库中提供了string类型专门表示字符串

1
2
#include<string>
using namespace std;

使用string可以更加方便和安全的管理字符串,对性能要求不是特别高的场景可以使用。

定义

1
2
3
4
string s;   //定义空字符串
string s = "helloworld"; //定义并初始化
string s("helloworld");
string s = string("helloworld");

字符串长度

1
2
3
cout<<s1.length()<<endl;    //输出字符串长度
cout<<s1.size()<<endl; //本质和上面一样
cout<<s1.capacity()<<endl; // 字符串的容量

字符串比较

直接使用运算符== != < > >= < = 即可,通过ASCII码来比较。

转换为C风格字符串

1
2
string s1 = "hello";
const char* c_str1 = s1.c_str();

随机访问

字符串本身就是数组,可以使用下标的方式访问

1
2
string s = "hello";
cout<<s[0]<<endl;

字符串拷贝

不需要使用函数,直接使用=赋值

1
2
string s1 = "hello";
string s2 = s1;

字符串拼接

string重载了+和+=,不需要使用函数

1
2
3
string s1 = "hell0",s2 = "world";
string s3 = s1+s2;
s1 += s2;

数组

序列型容器

数组代表内存里一组连续的同类型的存储区,可以用来把多个存储区合并成一个整体。比如:int arr[10] = {1,2,3,4,5,6,7,8};

数组声明:

  • int arr[10];
  • 类型名int表示数组里所有元素的类型
  • 整数10表示数组里包含的元素个数
  • 数组元素个数不可以改变,是常量

使用注意:

  • 每个元素都有下标,标识一个元素在当前数组容器中的位置。通过下标可以直接访问数组内任意元素
  • 下标从零开始
  • 超过范围的下标不可以使用,会内存错误。一定要注意,内存溢出是程序中经常出现的BUG
  • 数组名和下标可以表示数组里的元素,如a[2]表示第三个元素
  • 数组定义中的类型可以是除引用之外的任意类型,引用是不能赋值的,所以没有引用数组:int& a[10]错误

数组优点:

  • 可以编写循环程序,依次处理数组里的所有元素;
  • 循环变量依次代表所有有效下标;

数组使用常见错误:off-by-one error(差一错误)

考虑一个问题,假定整数x满足边界条件x>=16且x<=37,那么此范围内的整数x可能的取值有多少?这个问题思考有两个基本原则:

  1. 首先考虑最简单的情况(特例),然后将结果外推;
  2. 仔细计算边界问题;

特例:x的上界与下界重合,即x>=16与x<=16,显然其中的x个数为1;那么假定下界位low,上界位high;当low与high重合,个数为1,即共有high-low+1个元素。那么外推,这里共有37-16+1=22个元素。 最容易出错的地方就是+1,很容易就仅仅计算high-low。

  我们可以抽象出一个编程技巧来规避这个错误:用数学上的左闭右开[,)区间表示,上述问题可以看作[16,38),即38-16=22。C++中,我们编程也遵循这个原则,从0开始,使用非对称区间,让下界可以取到值,上界取不到值。这样设计程序,可以直接用上界-下界来取得范围大小;当取值范围为空的时候,上界值=下界值;即使取值范围为空,上界值也永远不可能小于下界值。

1
2
3
4
5
6
7
8
9
10
11
//推荐的方式:
for(int index=0;index < 10;++index)
{
cout<<a[index]<<" ";
}

//不推荐的方式
for(int index=0;index <= 9;++index)
{
cout<<a[index]<<" ";
}

  

结论--C语言设计数组下标的原则:从0开始,使用非对称区间[,);让下界可以取到值,上界取不到值;这样设计的好处:

  1. 取值的范围大小可以直接上界-下界;
  2. 如果这个取值范围为空,上界值=下界值;
  3. 即使取值范围为空,上界值永远不可能小于下界值;

初始化

  定义数组时,可以为其元素提供一组用逗号分隔的初值,用{}括起来,称为初始化列表。数组元素初始化时若没有显式提供元素初值,则元素会被向普通变量一样初始化:

  1. 全局的数组变量,元素初始化为0;
  2. 函数内部的局部内置类型数组,元素无初始化;如果只初始化部分元素,剩余元素也会被初始化为0;
  3. 数组类型不是内置类型,则无论在哪里定义,自动调用其默认的构造函数为其初始化,若该类无默认构造函数则会报错;

增删改查

  • 在尾部添加和删除操作,时间复杂度为O(1)。只需要操作尾部元素即可,对与其他元素没有干扰。
  • 在数组中间进行添加和删除操作,时间复杂度为O(n)。在中间操作,会对其他元素造成影响,操作元素后面的每一个元素都需要进一步操作
  • 数组的访问用遍历很高效,时间复杂度为O(1);
  • 数组的查找复杂度一般为O(n),取决于数组的容量(数组容量很大的时候并不适合遍历查找,时间复杂度太高,可以用二分查找);
1
2
3
4
5
6
//下标访问
a[2] = 5;

//指针访问
int* p = a;
*(p+2) = 5;
1
2
3
4
5
6
7
8
9
//寻找a[]中第一个值为3的目标:
int len = sizeof(a)/sizeof(a[0]);
for(int index = 0; index < len; ++index)
{
if(a[index] == 3)
{
return index;
}
}

  

二维数组

二维数组是包含行列两个维度的数组。二维数组的初始化既可以按行初始化,也可以顺序初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int ia[3][4] = {
{0,1,2,3},
{4,5,6,7},
{8,9,10,11}
};

int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};


//只初始化每行第一个元素,其余元素为0:
int ia[3][4] = {{0}, {4}, {8}};

//只初始化第一行元素,其余元素默认为0:
int ia[3][4] = {0, 3, 6, 9};

//如果对二维数组所有元素赋值,则第一维(行数)可以省略
//只有第一维可以省略,哪怕是100维的数组也只能忽略第一维
int ia[][3] = {123456}; //相当于int ia[2][3] = {1,2,3,4,5,6}

二维数组访问:

1
2
3
4
5
6
7
8
9
int a[2][4] = {{1,2,3,4},{5,6,7,8}};
for(int row = 0; row < 2; ++row) //行
{
for(int col = 0; col < 4; ++col) //列
{
cout<<a[row][col]<<" ";
}
cout<<endl;
}

  二维数组我们一般按照一行一行的遍历,虽然可以进行一列一列的遍历的,但是实际操作中不建议这么做:因为循环需要满足“空间局部性”:在一个小的时间窗口内,访问的地址越接近越好,这样执行的速度快;也就是说我们一般把最长的循环放在内层,最短的循环放在最外层,以减少CPU跨切循环的次数。

  本质上所有数组在内存中都是一维线性的。不同的语言采用的存储方式不同,有的采用行优先存储,有的采用列优先存储。C/C++中采用行优先顺序存储。

二维数组动态声明:

1
2
3
4
5
6
7
8
9
10
11
//a[m][n]
int **a = new int*[m];
for(int i = 0; i < m; i++){
a[i] = new int[n];
}

//动态声明的数组需要释放内存:
for(int i = 0; i < m; ++i){
delete []a[i];
}
delete []a;

  

C++新型数组--vector

Vector是面向对象方式的动态数组。

动态是指容量不固定:我们经常使用的简单的数组,因为定义的时候就有固定容量,所以无法实现动态扩容插入元素。使用vector容器,可以轻松实现动态扩容添加元素。

面向对象是C++的特性,vector封装了一系列的方法,我们创建vecor的对象可以方便的使用。

添加

  使用vector容器,可以轻松实现动态扩容插入元素,传统的C数组容量有限,vector可以动态管理扩容。

  • push_back,尾添加
  • insert,任意位置添加(效率不高)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<vector>
using namespace std;

int main()
{
vector<int> vec = { 1,2,3,4 };

//尾添加
vec.push_back(5); //尾添加,不会出现下标越界 {1,2,3,4,5}


//指定位置添加
//insert方法,传入位置和值
vec.insert(--vec.end(), 6); //{1,2,3,4,6,5}
return 0;
}

遍历

1
2
3
4
for(int index = 0;index < vec.size(); ++index)
{
cout<<vec[index]<<endl;
}

  我们可以使用vector中的capacity()方法来查看vector当前的容量,用size()的方法来查看已经存储的元素的个数。这两者有区别,容量是指容器可以容纳元素的个数,数量是已经存储的数据个数;

删除

  • vec.pop_back(); //尾删除
  • vec.eraser(pos); //指定位置删除

  要注意如果打算使用eraser的方法进行尾删除,要注意end()的位置。数组的区间往往是左闭右开,end()指向的是尾元素的后面的地址,并不是尾元素自身。

1
2
vec.pop_back();
vec.eraser(vec.end()-1); //这两者等价

  编写代码时,我们总会做出一些假设,断言就是用在代码中捕获这些假设。断言表示为一些布尔表达式,我们相信在程序中的某个特定点该表达式的值为真,可以在任意时刻启用和禁用断言来验证。

  举个例子,在离散数学中有个德摩根定律,我们在C++语言中可以验证:

1
2
3
4
5
6
7
8
9
10
#include<iostream>
using namespace std;
int main()
{
bool A = false;
bool B= true;
//德摩根率:
cout<<(!(A || B) == (!A && !B))<<endl; //输出1
cout<<(!(A && B) == (!A || !B))<<endl; //输出1
}

这种代码方式在开发中并不常见,而是使用以下断言的方式:

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
#include<assert.h>
using namespace std;
int main()
{
bool A = false;
bool B= true;
//德摩根率:
assert( !(A || B) == (!A && !B) );
assert( !(A && B) == (!A || !B) );
}

在这个程序中,并不会有任何输出结果,但是程序正常运行。

假如assert()函数中表达式出错(非真),整个程序在运行的时候就会报错,并指出哪里出了问题。

所以今后在我们开发的时候可以运用这一特性,完成开发测试用例的编写:

1
assert(函数返回值 == 预期结果);

只要函数运行符合预期结果,程序正常运行,一旦不符合就会出错。

注意assert()不是函数,而是一个宏定义。

  运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C++内置了丰富的运算符,并提供了以下类型的运算符:

  • 算数运算符
  • 关系运算符
  • 逻辑运算符
  • 位运算符
  • 赋值运算符
  • 杂项运算符

  在程序中,运算符是用来操作数据的,因此,这些数据也被称作操作数。使用运算符将操作数连接而成的式子称为:表达式。 表达式的特点:

  • 变量和常量都可以认为是表达式;
  • 运算符的类型对应了表达式的类型,如算术运算符对应算数表达式;
  • 每一个表达式都有自己的值,即表达式都有运算结果;

算数运算符

A=10,B=20

运算符 描述 实例
+ 两个操作数相加 A+B=30
- 第一个操作数减去第二个操作数 A-B=-10
* 两个操作数相乘 A*B=200
/ 分子除以分母 B/A=2
% 取模运算符,整除后的余数 B%A=0
++ 自增运算符,整数值增加1 ++A = 11
-- 自减运算符,整数值减少1 - -A=9

  除法运算中整数和浮点数的运算结果不太一样,整数除的话结果就是整数,要想输出完整整型数:需要用浮点数去除:

1
2
3
int A = 10;
cout << 15 / A << endl; //输出1
cout << 15.0 / A << endl; //输出1.5

  自增/自减运算符放在变量的前面和后面是不一样的。放在变量前面称为前缀运算,先取变量的地址,做运算后再把值放入寄存器(先变后用);放在变量后面称为后缀运算,先取变量的地址中的内容放入寄存器,然后对内存中的信息做运算(先用后变)。因为前缀运算返回的是操作数本身,所以前缀运算可以作为左值表达式;后缀运算返回的是临时变量,只能用于后缀表达式。

  自增和自减运算符只能用于变量,不能用于表达式:6++、(i+j)++、(&p)++这些都是不合法的。

  C/C++编译器对程序编译时,从左到右尽可能多的将字符组成一个运算符或者标识符,因此"i+++j++"是合法的,因为编译器会理解为"(i++)+(j++)",这两个自增运算符作用的对象都是变量;但是"++i+++j"是不合法的,编译器会理解为"++(i++)+j",其中第一个自增运算符作用的对象是"i++",这是一个表达式,不合法。

1
2
3
4
int A = 10;
int B = 10;
cout << ++A << endl; //输出11
cout << B++ << endl; //输出10(但是B变为11)

关系运算符

A=10 ;B=20

运算符 描述 实例
== 检查两个操作数是否相等,如果相等则条件为真 (A == B)不为真
!= 检查两个操作数是否相等,如果不相等则条件为真 (A!=B)为真
> 检查左操作数是否大于右操作数,如果是则条件为真 (A>B)不为真
< 检查左操作数是否小于右操作数,如果是则条件为真 (A<B)为真
>= 检擦左操作数是否大于等于右操作数,如果是则为真 (A>=B)不为真
<= 检查左操作数是否小于等于右操作数,如果是则条件为真 (A<=b)为真

关系运算符返回的都是bool类型的值。千万不要连起来用:"i< j < k"这种写法有问题,"i < j"运算完成后直接返回true或false,是拿这个true或false来和k做比较的。

逻辑运算符

A = true; B = false;

运算符 描述 实例
&& 逻辑与运算符。如果两个操作数不为零,则条件为真 (A && B)为假
|| 逻辑或运算。两个操作数任意一个非零,则条件为真 (A || B)为真
! 逻辑非运算,用来逆转操作数的逻辑状态,是单目运算符 !(A && B)为真

可以用逻辑运算符来表示德摩根律:\(\neg(A \land B)\) 等价于 \(\neg A \lor \neg B\)\(\neg(A \lor B)\) 等价于 \(\neg A \land \neg B\)

1
2
cout << (!(A || B) == (!A && !B)) << endl;
cout << (!(A && B) == (!A || !B)) <<endl;

逻辑运算符本身也是有优先级的

1
2
auto a = true || true && false;     //true
auto b = (true || true) && false; //false

逻辑与和逻辑或求值策略都是“短路求值”:先计算其左边的操作数,只有再仅靠左操作数无法确定逻辑表达式的值的时候,才会求解右操作数。

赋值运算符

把右侧的值赋给左侧。注意左侧的值一定要是一个变量。

运算符 描述
= 赋值运算
+= 加且赋值运算符
-= 减且赋值运算符
*= 乘且赋值运算符
/= 除且等于运算符
%= 求模且等于运算符
<<= 左移且赋值运算符
>>= 右移且赋值运算符
&= 按位与且赋值运算符
^= 按位异或且赋值运算符
|= 按位或且赋值运算符

位运算符

位运算符作用于位(bit),并逐位进行操作。

p q p&q p | q p^q
0 0 0 0 0
0 1 0 1 1
1 1 1 1 0
1 0 0 1 1

位运算符还包括取反操作"~"和移位运算"<<"和">>",都是以bit为单位运算。

与、或、异或都是双目运算符,结合性从左到右,优先级高于逻辑运算符,低于关系运算符,且从高到低为&、^、|

使用移位运算符需要注意,左移运算还比较简单,移走的位自动填充0,但右移运算有两种情况,逻辑右移时移走的位填充0,但算数右移时移走的位与符号位有关。底层到底是逻辑右移还是算数右移取决于编译器,而不是程序员,所以对于有符号数,尽可能不要使用右移运算!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 位运算
int A = 10,B = 20;

cout << (A&B) << endl; // 01010 & 10100 = 00000 ==> 0

cout << (A | B) << endl; // 01010 | 10100 = 11110 ==> 30

cout << (A^B) << endl; // 01010 ^ 10100 = 11110 ==> 30

//取反运算需要补码的知识,计算机中都是用补码表示的
cout << (~A) << endl; // ~0000000000001010 = 11111111111110101 ==> 0000000000001011==-11

cout << (A << 2) << endl; // 00001010 << 2 ==> 00101000 ==> 40

cout << (A >> 2) << endl;// 00001010 >> 2 ==> 00000010 ==> 2

  异或操作有特性:两个相同的数异或后结果为0,且满足交换律,即"A ^ B ^ C ^ D ^ F ^ B"等价于"A ^ C ^ D ^ E ^ F"。这个特性常用来寻找成对出现的数据时缺失的哪一个数:如对于一组数【A、B、C、D、A、B、C】,直接做异或运算"A ^ B ^ C ^ D ^ A ^ B ^ C = D"找出缺失的D。

  异或操作还可以交换两个变量的值,利用了异或操作的自反性(a ^ a = 0)、恒等性(a ^ 0 = a)以及交换律(a ^ b = b ^ a、(a ^ b) ^ c = a ^ (b ^ c))。不过要注意两个变量不能相等,相等后结果为0:

1
2
3
4
5
6
7
8
9
10
11
a = a ^ b; //步骤1
b = a ^ b; //步骤2
a = a ^ b; //步骤3

/*
设a = A ,b = B
步骤1执行完后:a = A^B , b = B
步骤2执行完后:a = A^B , b = A^B^B=A^0=A
步骤3执行完后:a = A^B^A=B , b = A
*/

杂项运算符

运算符 描述
sizeof sizeof运算符,返回变量的大小,即这个变量的类型所占用的byte的长度
Condition ?X:Y 条件运算符,唯一的三目运算符。如果Condition为真,则值为X,否则值为Y
逗号运算符,会执行一系列运算,整个逗号运算符表达式的值是以逗号分隔的列表中的最后一个表达式的值
.(点)和->(箭头) 成员运算符,用于引用类、结构体和共用体的成员
Cast 强制类型转换运算符,把一种数据类型转换成另一种数据类型,如int(2.20)将返回2。C++中不建议使用强制转换
& 指针运算符&,返回变量的地址
* 指针运算符*,指向一个变量

  一定要注意,sizeof是运算符,不要理解为函数!!!sizeof的操作数可以是一个表达式或者是类型名。需要牢记sizeof的计算发生在编译时期,所以它可以被当作常量表达式使用,且会忽略括号内的运算。比如sizeof(a++)中的++不会执行。

  如果sizeof中传递的是个函数调用,则返回函数返回类型的大小,函数不会调用。这里只能传递函数调用,不能传递函数名(Func()这个是函数调用,Func是函数名)。且如果函数返回的是void,不能确定类型,也不可以使用sizeof运算符。

运算符优先级

下表从高到低列出各个运算符,较高的运算符会被优先计算:

类别 运算符 结合性
后缀 () [] ++ -- 从左到右
一元 + - ! ~ ++ -- (type)* & sizeof() 从右到左
乘除 * / % 从左到右
加减 + - 从左到右
移位 << >> 从左到右
关系 < <= > >= 从左到右
相等 == != 从左到右
位与AND & 从左到右
位异或XOR ^ 从左到右
位或OR | 从左到右
逻辑与 && 从左到右
逻辑或 || 从左到右
条件 ?: 从右到左
赋值 = += -= *= = %= >>= <<= ^= |= 从右到左
逗号 从左到右

只需要记住两点:

  1. 一般来说,一元运算符优先级高于对应的二元运算符;
  2. 弄不清就加括号

编译型语言

编程语言可以分为四个层次,从上到下,语言会更接近人类使用语言的方式,但相对应的程序运行效率逐渐降低

  1. 机器语言、汇编语言:这些语言执行效率很高,但移植性很差;
  2. 编译型语言:C/C++。这种类型的语言更接近人类的使用方式,在不同的机器上也可以跨平台使用。但跨平台有一些难度;
  3. 解释型语言:Basic、Python。这种类型的语言通过解释器作为中间层,跨平台性很强;
  4. 脚本语言:bash、csh。不同的平台拥有不同的脚本,本身的功能并不强大,但是很容易把各种语言融合在一起。

C++属于编译型语言,需要经历编译和链接的过程,才能变成真正的可执行程序:

数据类型

  C++中的每个变量都有其数据类型,数据类型决定这个变量所占用内存空间的大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算。

  在计算机中,1byte=8bit。我们在说数据类型的大小的时候一般以byte作为单位来计算。

  计算机中都是用0、1这种二进制存储的。日常中我们习惯用10进制来看,有时候为了方便,调试过程中更习惯用16进制的方式来表示。

  内置的C++类型分为基本类型和复合类型。基本类型包括整数、浮点数及两者的多种变体;复合类型包括数组、字符串、指针、引用、结构体和共用体等。一般32位编译环境中的类型大小如下,需要注意不同的编译环境结果可能不同:

名称 字节数 描述 范围
char 1 存储字符或者整数,8位(bit) 有符号:-128~127;
无符号:0 ~ 255;
short 2 短整数,16位 有符号:-32768~32767;
无符号:0 ~65535;
long 4 长整数,32位 有符号:-2147483648~2147483647;
无符号:0 ~4294967295 ;
int 4 整数 同上
float 4 浮点数
double 8 双精度浮点数
long double 8 长双精度浮点数
bool 1 布尔值,只能是真值或假值 true或false
wchar_t 2 宽字符,这是为存储两字节长的国际字符而设计的类型
1
2
3
4
5
6
7
8
9
10
11
//常见的数据类型的定义
char a[10] = "a";
short int s = 97;
int m = 97;
long int n = 97;
//给float类型的变量赋值时,要在数值后面加上一个f或F,表示这是一个float类型的常量,否则默认为double类型的常量
float f = 97.0f;
double d = 97.0;
long double k = 97.0;
bool b = true;
wchar_t w[10] = L"a";

内存分区

  1. 堆:由程序员手动分配和释放,完全不同于数据结构中的堆,分配方式类似链表。由malloc(C)或new(C++)来分配,free(C)和delete(C++)释放。若程序员不释放,程序结束时由系统释放;
  2. 栈:由编译器自动分配和释放的,存放函数的参数值、局部变量等。操作方式类似数据结构中的栈;
  3. 全局(静态)存储区:存放全局变量和静态变量。包括DATA段(全局初始化区)和BSS段(全局未初始化区)。其中初始化的全局变量和静态变量存放在DATA段,未初始化的全局变量和未初始化的静态变量存放在BSS段。程序结束后由系统释放;在程序执行之前BSS段会自动清0,所以未初始化的全局变量和静态变量在程序执行之前就已经是0了;
  4. 文字常量区:常量字符串存储在这里,程序结束后由系统释放;
  5. 程序代码区:存放函数体的二进制代码

标识符和关键字

C++中的标识符是用来标识变量、函数、类、模块,或者任何其他用户自定义项目的名字;

  • 一个标识符通常以字母A~Z或a ~z或下划线_开始,后面跟零个或者多个字母、下划线和数字;
  • 一个标识符不允许使用数字开头;
  • 一个标识符内不允许出现标点字符,如@、&等等;
  • 不能混淆大小写,C++是区分大小写的编程语言;
  • 不能使用C++关键字作为标识符,原则上不允许长度超过32位;

标识符命名规则:

  • 不要试图发明最好的命名规则,要制定一个让大部分人都满意的命名并在项目组中贯彻执行;
  • 标识符应该直观,可以望文知意,尽量使用英文单词组合,不要使用拼音;
  • 标识符长度要符合“min-length&max-information”原则,长度尽可能短,信息尽可能多
  • 变量名字尽可能使用“名词”或“形容词+名词”形式,尽量避免出现数字;函数名尽量使用“动词+名词”
  • 可借鉴的命名规则:
    • 匈牙利命名法:开头字母用变量类型缩写,其余部分用变量的英文或者英文缩写,要求第一个字母大写,如int iMyAge;
    • Camel(驼峰)命名法:第一个单词首字母小写,后面单词首字母大写,如int myAge;
    • Pascal命名法:每个单词的第一个字母都大写,如int MyAge;

C++的关键字

变量与常量

变量

  • 变量就是在程序运行过程中,值可以改变的量;
  • 变量在程序的执行中可以进行赋值造作,发生变化;
  • 变量有一个名字,并在使用之前要说明其类型,一经说明,就在内存中占据与其类型相应的存储单元;

变量定义:

  • 首先是类型说明符,随后紧跟一个或者多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束:int m = 1,n = 1;
  • 当变量在创建时获得了一个特定的值,我们说这个变量被初始化了。用于初始化变量的值可以是任意复杂的表达式;
  • 当一次定义了一个或多个变量时,变量的名字随着定义就可以使用了,如:int m=1,n=1; int sum = m + n;

常量

  • 在程序运行过程中,其值一直保持不变的量为常量;
  • 常量也区分不同的数据类型;

常量在C++中有两种定义的方法:

  1. 使用#define,如:#define PI 3.14
  2. 使用const,如:const double PI = 3.14

尽量使用const形式定义变量,因为#define不会出现在编译时期,在编译出错时不容易排错。define是C语言时期的习惯。

宏定义 const
宏定义相当于字符替换 常量声明
预处理器处理 编译器处理
无安全类型检查 有安全类型检查
不分配内存 分配内存
存储在代码段 存储在数据段
可以通过#undef取消 不可取消

整数常量

整数常量可以是十进制、八进制或者十六进制的常量:

  • 前缀指定基数:0x或者0X表示十六进制,0表示八进制,不带前缀则默认十进制;
  • 整数常量也可以带一个后缀,后缀是U和L的组合,U表示无符号整数,L表示长整数。后缀可以是大写也可以小写,不区分顺序。后缀不能重复。

布尔常量

布尔常量共有两个,他们都是C++关键字,true代表真,false代表假。

字符常量

  • 字符常量是括在单引号中的。如果常量以L(仅当大写时)开头,则表示宽字符常量(如L'x'),此时必须保存在wchar_t类型的变量中;否则就是窄字符常量(如'x'),此处可以存储在char类型的简单变量中;
  • 字符常量可以是一个普通字符('x')、一个转义序列('\t')、或者一个通用字符('\u02C0')。
  • 需要表达一些符号的时候需要用到转义字符;

常见转义字符如下:

转义字符 含义
\\\ \ 字符
\' '字符
\" "字符
\? ?字符
\a 警报铃
\b 退格键
\f 换页符
\n 换行符
\r 换行符
\t 水平制表符
\v 垂直制表符
\ooo 一到三位的八进制数
\xhh... 一个或多个数字的十六进制数

C++概况

C++语言应用广泛,凡是要求大型的、对性能要求很高的程序,以及物联网应用上,都会用到C++语言。

C++历史

  • C++编程语言可追溯到1979年,Bjarne Stroustrup创立了带类的C,这就是C语言的超集。1983年,这个语言被命名为C++;
  • C++最开始仅仅是C语言的增强版,但是后来又引入了很多功能。1998年伴随着STL标准库的诞生,一堆新功能和特性的产生,C++标准委员会发布了第一个国际标准C++ + ISO/IEC 1488:称为C++ 98;
  • 2011年中期,C++标准(被称作C++ 11)完成了新的标准。Boost库项目对新标准产生了相当大的影响,一些新模块直接从响应的Boost库中派生出来;
  • C++ 11之后,每三年C++都会发布新的语言标准,每一版都在基本保留向后兼容性的同时,提供着改进和新功能;

C++特点

优点:

  1. 强大的抽象封装能力:这让C++语言具备了强大的开发工程能力;
  2. 高性能:运行快,占用资源少;
  3. 低功耗:特别适合在各种微型嵌入式设备中高效运行程序;

缺点:

  1. 语法相对复杂,细节比较多;
  2. 需要一些好的规范和范式,否则代码很难维护;

C++的应用

  • 大型桌面应用程序(如Google Chrome、Miscrosoft Office)
  • 大型网站的后台(Google的搜索引擎)
  • 游戏和游戏引擎(Unity)
  • 视觉库和AI引擎(Opencv和Tensorflow)
  • 数据库(Mircrosoft SQL Server)
  • 嵌入式领域

总的来说,在开发成本比较高,对于内存和CPU要求很高,低功耗并且程序被调用非常频繁等场景中,目前C++是最合适的语言。

Hexo博客要求必须使用Github风格的Markdown语言编辑。所以需要了解具体的编写格式。

一、编辑器

市面上有很多的Markdown编辑器,之前很多人喜欢用Typora,但目前这个已经没有免费版可以用了。干脆直接用VSCode。

使用VSCode需要下载两个插件,第一个是【Markdown All in One】,是一个组合包,把最常用的markdown优化都安装好;第二个是【Markdown Preview Github Styling】,是Github使用的Markdown渲染样式。使用这个样式,在本地就能预览Markdown文件最终在Github Pages中显示的效果。

二、常用语法

标题

利用#设置标题,一个表示一级,两个表示二级,依次类推

1
2
3
#      一级标题
## 二级标题
……………… 以此类推

引用

利用>可以创建引用块

文本高亮

利用一组反引号`来把文本包起来。

1
`这里面是需要高亮的字符`

代码块

利用三个反引号`把内容扩起来即可。在上面的那个反引号后面还可以加上语言名称来进行自适应。

斜体

可以用一组*号来把文字包起来,也可以直接用快捷键Ctrl+I

加粗

k可以用一组**来把文字包起来,也可以用快捷键Ctrl+B

删除线

用一组~~把文字包起来

下划线

下划线可以用<u> </u>把文字包起来

按键效果

可以用一组<kbd> </kbd>把文字包起来,实现按键效果。

列表

列表分为有序列表和无序列表

1
2
3
4
5
6
7
无序列表:用-加空格
- 第一
- 第二

有序列表:用数字加.加空格
1. 第一
2. 第二

三、链接与图片

图片其实是一种超链接,所以把图片归类为链接里面。

1、跳转链接

普通的网站跳转都属于跳转链接,语法很简单:

1
[网站说明](跳转链接)

2、图片链接

图片链接的markdown语法和跳转链接类似:

1
![](图片链接)

3、图片存储

网上很多教程都是利用相对路径来存储图片,操作如下: 1. 安装图片插件

1
npm install hexo-asset-image --save

  1. _config.yml配置文件中,修改为 post_asset_folder: true

其中第二步可以忽略,第二步的意义在于我们新建文章的时候,会在soruce目录下生成文章同名文件夹供我们存储图片,我们也可以手动创建文件夹。

但是对于GitPages或者云服务器来说,其提供的免费存储空间都是有上限的,这样操作只适用于图片较少的情况。我们可以利用云服务器对象存储功能实现图床。

我的域名使用的是腾讯的,那就用腾讯的COS来操作,并且腾讯提供可视化工具COSBrowser,很方便。 参考博客: Hexo 博客图片添加至图床---腾讯云COS图床使用

一个图片测试:

四、公式

公式使用的是Latex语法。但需要注意,Hexo并不支持Laxte语法,所以我们需要开启相关的开关。有些主题就自带这个功能。 Markdown使用$包裹实现行内公式,使用$$包裹实现行间公式。 Latex语法可以不掌握,借助工具即可: HostMath 不过Hexo显示数学公式还需要一些其他设置,Hexo默认使用 hexo-renderer-marked 引擎渲染网页,该引擎会把一些特殊的 markdown 符号转换为相应的 html 标签,导致公式不显示。

五、表格

1
2
3
4
5
6
|     1   |      T2       |  T3   |
|----------|:-------------:|------:|
| 1 | 1 | 1 |
| 22 | 22 | 22 |
| 333 | 333 | 333 |

1 T2 T3
1 1 1
22 22 22
333 333 333

------表示左对齐,:------:表示居中,-----:表示右对齐 Hexo的表格必须和正文之间有空行,否则不能正常显示

六、流程图

Hexo里流程图限制多多,直接在其他软件画完截图吧。我也懒得学了。【流汗黄豆】

七、特殊字符

常见转义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
! &#33; — 惊叹号 Exclamation mark
” &#34; &quot; 双引号 Quotation mark
# &#35; — 数字标志 Number sign
$ &#36; — 美元标志 Dollar sign
% &#37; — 百分号 Percent sign
& &#38; &amp; Ampersand
‘ &#39; — 单引号 Apostrophe
( &#40; — 小括号左边部分 Left parenthesis
) &#41; — 小括号右边部分 Right parenthesis
* &#42; — 星号 Asterisk
+ &#43; — 加号 Plus sign
< &#60; &lt; 小于号 Less than
= &#61; — 等于符号 Equals sign
- &#45; &minus; — 减号
> &#62; &gt; 大于号 Greater than
? &#63; — 问号 Question mark
@ &#64; — Commercial at
[ &#91; --- 中括号左边部分 Left square bracket
\ &#92; --- 反斜杠 Reverse solidus (backslash)
] &#93; — 中括号右边部分 Right square bracket
{ &#123; — 大括号左边部分 Left curly brace
| &#124; — 竖线Vertical bar
} &#125; — 大括号右边部分 Right curly brace

八、hexo模板

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
# !!!!!!!!!!
# 每一项的 : 后面均有一个空格
# 且 : 为英文符号
# !!!!!!!!!!

title:
# 文章标题,可以为中文

date:
# 建立日期,如果自己手动添加,请按固定格式
# 就算不写,页面每篇文章顶部的发表于……也能显示
# 只要在主题配置文件中,配置了 created_at 就行
# 那为什么还要自己加上?
# 自定义文章发布的时间

updated:
# 更新日期,其它与上面的建立日期类似
# 不过在页面每篇文章顶部,是更新于……
# 在主题配置文件中,是 updated_at

permalink:
# 若站点配置文件下的 permalink 配置了 title
# 则可以替换文章 URL 里面的 title(文章标题)

categories:
# 分类,支持多级,比如:
# - technology
# - computer
# - computer-aided-art
# 则为 technology/computer/computer-aided-art
# (不适用于 layout: page)

tags:
# 标签
# 多个可以这样写 [标签1,标签2,标签3]
# (不适用于 layout: page)

description:
# 文章的描述,在每篇文章标题下方显示
# 并且作为网页的 description 元数据
# 如果不写,则自动取 <!-- more -->
# 之前的文字作为网页的 description 元数据

keywords:
# 关键字,并且作为网页的 keywords 元数据
# 如果不写,则自动取 tags 里的项
# 作为网页的 keywords 元数据

comments:
# 是否开启评论
# 默认值是 true
# 要关闭写 false

layout:
# 页面布局,默认值是 post,默认值可以在
# 站点配置文件中修改 default_layout
# 另:404 页面可能用到,将其值改为 false

type:
# categories,目录页面
# tags,标签页面
# picture,用来生成 group-pictures
# quote?
# https://io-oi.me/tech/test/

photos:
# Gallery support,用来支持画廊╱相册,用法如下:
# - photo_url_1
# - photo_url_2
# - photo_url_3
# https://io-oi.me/tech/test/

link:
# 文章的外部链接
# https://io-oi.me/tech/test/

image:
# 自定义的文章摘要图片,只在页面展示,文章内消失
# 此项只有参考本文 5.14 节配置好,否则请勿添加!

sticky:
# 文章置顶
# 此项只有参考本文 5.15 节配置好,否则请勿添加!

password:
# 文章密码,此项只有参考教程:
# http://shenzekun.cn/hexo的next主题个性化配置教程.html
# 第 24 节,配置好,否则请勿添加!
# 发现还是有 bug 的,就是右键在新标签中打开
# 然后无论是否输入密码,都能看到内容

由于Hexo属于静态博客,文章内容都是本地存储,只有静态页面(public文件夹)会push到git上面。如果我们更换电脑的话,配置相同的环境、依赖和博客文件,如果用U盘拷贝就太low了。所以我们可以利用Github远程仓库来实现多台终端的博客更新部署。

主要思路是利用git的分支,把静态博客页面文件放在master主分支,把hexo的部署环境文件和博客原文放在新建的Hexo分支上。

# 一、Hexo的原理和依赖

1、环境

Hexo需要有Node.js环境和Git环境,这在哪个电脑都必须有的。

2、资源配置依赖

首先要安装Hexo

1
npm install -g hexo-cli 

hexo所需要的包信息都已经存储在package.json文件中了,我们可以直接利用这个文件,执行命令行安装所需资源:

1
npm install

3、博客内容

博客的内容主要就是主题文件和博客文件

  • node_modules: hexo所需要的文件,这个文件不需要上传git
  • themes:主题文件
  • source:博文的md文件
  • public:生成的静态页面,这个一般都clean掉
  • _config.yml:全局配置文件
  • .gitignore:hexo自动生成的,里面一般会记录node_modules,我们不许要改,这个文件就会被忽略掉

二、Git仓库修改

对我们之前创建的username.github.io仓库,创建一个新的hexo分支,并在仓库->Settings->Branches->Default branch中把这个分支设置为默认分支。clone到本地,此时本地应该是Hexo分支。

之后我们把我们之前写的博客目录下的文件,全部拷贝到这个本地仓库中【注意要把之前themes目录中的.git文件删掉。因为一个git目录下不能有另一个git仓库。如果我们需要更新主题文件,只能把主题clone到本地后进行拷贝覆盖】。然后提交git到Hexo分支:

1
2
3
git add .
git commit -m 'update'
git push

此时我们观察github,可以看到,master分支里面保存着静态博客资源(在_config.yml中的Deployment配置的),Hexo分支备份博客部署文件,供我们维护更新。两者在一个仓库中互不干扰。

三、新的提交操作

目前我们在写博客时,就要在提交前做git更新了

1
2
3
4
5
6
7
8
9
10
11
git pull     //提交前最好更新最新


git add .
git commit -m 'update'
git push


hexo clean
hexo g
hexo d