Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

枚举

使用#define和const创建符号常量,使用enum不仅可以创建符号常量,还能定义新的数据类型。

枚举类型的声明和定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
//声明
enum wT{
Monday,
Tuseday,
Wednesday,
Thursday,
Firday,
Saturday,
Sunday
};

//定义
wT weekday;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
int main()
{
enum wT{Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday}; // 声明wT类型
wT weekday;
weekday = Monday;
weekday = Tuesday;
//weekday = 1; // 错误 不能直接给int值,只能赋值成wT定义好的类型值
//可以写成weekday = wT(1),但是涉及类型转换的写法不推荐
cout << weekday << endl;
//Monday = 0; // 错误 类型值不能做左值,不是有存储空间的变量
int a = Wednesday;
cout << a << endl;

return 0;
}

使用细节:

  • 枚举值不能做左值;
  • 非枚举变量不可以赋值给枚举变量;
  • 枚举变量可以赋值给非枚举变量;

结构体和联合体

1
2
3
4
5
6
7
8
9
10
11
12
struct Student
{
char name[6];
int age;
Score s;
};

union Score
{
double sc;
char level;
};

存储结构与结构体数据对齐

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

int main()
{
union Score
{
double ds; //8字节
char level; //1字节
};
struct Student
{
char name[6]; //6字节
int age; //32位环境 4字节
Score s;
};

cout << sizeof(Score) << endl; // 8

Student s1;
strcpy_s(s1.name, "lili");
s1.age = 16;
s1.s.ds = 95.5;
s1.s.level = 'A';

cout << sizeof(Student) << endl; // 24


return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct s1
{
char x;
int z;
short y;
};

sizeof(s1) = 12;

struct s2
{
char x;
short y;
int z;
};

sizeof(s2) = 8;

数据对齐原则--缺省对齐

  • 32位CPU
    • char:任何地址
    • short:偶数倍地址
    • int:4的整数倍
    • double:8的整数倍

程序是可以修改默认编译选项的,修改结构体内存分配:

1
2
3
4
5
6
7
Visual C++:
#pragma pack(n)


g++:
__attribute__(aligned(n))
__attribute__(__packed__)

三种基本结构

分支结构

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
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. 函数主体:函数主体包含一组定义函数执行任务的语句。

函数重载与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工具,观察其中的一个可以看到:
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
#include <iostream>

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(*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;

return 0;
}

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

内联函数

如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。

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

引入内联函数的目的是为了解决程序中函数调用的效率问题,即空间换时间;
注意:内联函数内部不能有太复杂的逻辑,比如复杂的循环判断或者递归。编译器有时会有自己的优化策略,所以内联不一定起作用。VS中记得把C/C++设置中的内联优化打开。

递归

数学归纳法

数学归纳法是证明当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
斐波那契数列: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);
}

指针

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
int a = 1;
float b = 3.14;
//指针定义
int* c = &a; //c存储a的内存地址
float* d = &b;

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

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

return 0;
}

左值与右值

字符串本身就是一个字符数组,但是字符串还可以用指针来表示:

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是允许的操作,a是一个左值
&(b+c)不能通过编译,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[]:指针的数组仍然是数组,里面每个值是个指针(arrao 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
int* a[4];

int c[4] = {1,2,3,4};
int(*b)[4];
b = &c; //数组的个数一定要匹配
//注意,[]的优先级比较高


for(unsigned int i = 0;i<4;i++)
{
a[i] = &(c[i]);
}



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

const pointer && pointer to 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指针
char const* 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修饰:

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

pointer to pointer

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 = 12;//错误!!

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

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

NULL指针

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

1
int* a = NULL;

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

对于一个指针,如果已经知道将被初始化为什么地址,那么请给他赋值,否则请把它设置为NULL,这样可以有效避免不可确定性访问的问题。

在对一个指针间接引用前,先判断这个指针的值是否为NULL。

指针使用完成后也请重新赋值为NULL。

野指针

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

一般情况下有三种情况被称为野指针

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

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

指针的基本运算

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

&操作符不能做左值,&操作编译器做是事情是把变量的地址位置取出来,然后放在内存空间中。但是他本身并不是变量自身,仅仅是一块空间存储着变量地址,这块空间我们的程序是没办法获取到的。

间接引用操作当用作左值的时候,实际的操作是把变量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
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
#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栈区

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结束
编译期间大小确定 变量大小范围确定 需要运行期间才能确定
大小范围 Win默认1M,Linux默认8M或10M,注意空间很小,不要分配大内存变量 所有系统的堆空间上限接近内存(虚拟内存)总大小(有一部分被OS占用)
内存分配方式 地址由高到底减少 地址由低到高增加
内容是否可变 可变 可变
全局静态存储区 常量存储区
存储内容 全局变量,静态变量 常量
编译期间大小是否确定 确定 确定
内容是否可变 可变 不可变

内存泄漏(Memory Leak)问题

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

内存泄漏发生的原因和排查方式:

  1. 内存泄漏主要发生在堆内存分配方式中,即“配置了内存后,所有指向该内存的指针都遗失了”。如果缺乏垃圾回收机制,这样的内存片就无法归还系统;
  2. 因为内存泄漏属于程序运行中的问题,无法通过编译识别,所以只能在程序运行过程中来判别和诊断。

比指针更安全的解决方案

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

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

C++的智能指针

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

auto_ptr

  auto_ptr是一种简单直接的智能指针,可以指向一个泛型对象。我们由new获得的对象在堆区中,如果auto_ptr指向这个对象,那么在auto_ptr对象销毁的时候,它所管理的对象也会一并delete掉,这不是一个特别合理的行为,因为指针指向对象不是一种强关联的关系。
  auto_ptr还要注意所有权转移的问题:有一个auto_ptr指向一个对象,如果我们不小心把对象传递给另外的智能指针,原来的指针就不再拥有这个对象了。这个操作是通过C++中的拷贝构造和赋值完成的,会直接剥夺指针对源对象内存的控制权。被剥夺后,对象内存的所有权转移给新指针,然后将原对象指针置为nullptr。因为这个问题,导致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
#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;
//}
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//memory文件

//观察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
#include <memory>
#include <iostream>
using namespace std;
int main()
{
// 在这个范围之外,unique_ptr被释放
{
auto i = unique_ptr<int>(new int(10));
cout << *i << endl;
}

// 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,那么循环引用会导致堆里面的内存无法正常回收,造成内存泄漏。

  为了避免这种循环引用,标准库提供了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; //weak_ptr不会对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. 一个引用永远指向它初始化的那个对象,不允许被修改。

引用可以认为是指定变量的别名,使用时可以认为是变量本身:

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;
}

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

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

字符串变量与常量

字符串变量

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

字符串常量

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

字符串的指针表示

1
const char* pStrHelloWorld = "helloworld";

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

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

  • 区分地址本身和地址存储的信息;
  • 区分可变与不可变;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main()
{
char strHello[11] = { "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;
}

字符串操作

头文件:string.h

字符串长度

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
strcmp(s1,s2); 
wcscmp(s1,s2);
//s1 == s2,返回0
//s1 < s2 ,返回值小于0
//s1 > s2 ,返回值大于0

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

字符串拷贝

1
2
3
strcpy(s1,s2);   //赋值字符串s2到字符串s1

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

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

字符串拼接

1
strcat(s1,s2);   //将字符串s2拼接到s1后面

这个更加要注意,s1的大小要足够存放拼接后的s1+s2字符串的大小
两个字符串指针不能直接相加,拼接必须要用这个函数。(指针相加是什么操作啊(#`O′))

字符串查找

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

安全函数

以上程序都需要我们进行边界检查以防止内存问题。现在我们往往在程序中使用更加安全的API函数

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

字符串操作重点

  1. C原始字符串的操作在安全性和效率中存在一定问题:
     1. 一定要注意不要缓冲区溢出。
     2. strlen的效率可以提升,当字符串很长的时候strlen的效率不高,我们可以用空间换时间,在定义字符串的时候用变量记住它的长度,不使用strlen函数来计算
    
  2. 目前有一个很好的开源库Redis字符串:Redis库

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]表示第三个元素

    数组优点:

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

数组使用常见错误: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]<<" ";
}

增删改查

  • 在尾部添加和删除操作,时间复杂度为O(1)。只需要操作尾部元素即可,对与其他元素没有干扰。
  • 在数组中间进行添加和删除操作,时间复杂度为O(n)。在中间操作,会对其他元素造成影响,操作元素后面的每一个元素都需要进一步操作

  • 数组的访问用遍历很高效,时间复杂度为O(1);

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
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跨切循环的次数。

std::vector

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

我们经常使用的简单的数组,因为定义的时候就有固定容量,无法实现动态扩容插入元素。

使用vector容器,可以轻松实现动态扩容添加元素。

添加

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(), 4); //{1,2,3,4,4,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

自增/自减运算符放在变量的前面和后面是不一样的。放在变量前面就是先做运算再出结果;放在后面就是先出结果再运算。

关系运算符

A=10 ;B=20

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

关系运算符返回的都是bool类型的值。

逻辑运算符

A = 1; B = 0;

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

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

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

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

其他运算符

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

运算符优先级

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

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

只需要记住两点:

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

编译型语言

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

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

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

数据类型

C++中的每个变量都有其数据类型,数据类型决定这个变量所占用内存空间的大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算。
在计算机中,1byte=8bit。我们在说数据类型的大小的时候一般以byte作为单位来计算。

名称 字节数 描述 范围
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
//常见的数据类型的定义
char a[10] = "a";
short int s = 97;
int m = 97;
long int n = 97;
float f = 97.0f;
double d = 97.0;
long double k = 97.0;
bool b = true;
wchar_t w[10] = L"a";

标识符和关键字

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不会出现在编译时期,在编译出错时不容易排错。

整数常量

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

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

布尔常量

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

字符常量

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

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++是最合适的语言。