C++中也可以用 exit(0);
提前退出程序,中间的0
只是一个标志
变量命名规范:
变量命名有许多约定俗成的规范,下面的这些规范能有效提高程序的可读性:
数据类型选择建议:
安装visual studio 2017,选择默认的就好了
第一个C++项目: 新建一个空的c++项目,然后再源文件中填一个一个cpp文件:这都是固定写法
#include<iostream>
using namespace std;
int main() {
cout << "helllo world" << endl;
system("pause");
return 0;
}
注释:
// 描述信息
/*这里是描述信息*/
ctrl+k+c 解除注释ctrl+k+u
# 这样的注释都是反斜杠的变量:语法:数据类型 变量名 = 初始值;
常量:用于不可更改的数据
C++定义常量两种方式
#define 常量名 常量值
// 记得没有分号
const 数据类型 常量名 = 常量值;
#include<iostream>
using namespace std;
#define day 7 // 注意没有分号
#define PI 3.141592653
#define FILENAME "workers.txt" // 定义一个宏常量来做文件名
int main() {
const int month = 12; // 这没有const的话,下面就可以重新赋值
cout << day<< month << endl;
//month = 13; //这行会直接个报错,因为上面把变量变成常量了
cout << day << month << endl;
cout << PI << endl; //打印出来会看到,后面没有,因为精度的问题
const int c = 0x10; // 16 这种定义进制数字时一定要用int,用flaot虽然也有结果,但是错的,且系统不会报错(0x 十六进制)
const int d = 0b10; // 2 (0b 二进制)
system("pause");
return 0;
}
注意这种宏的写法(c里面的要求,不知道c++是不是,看这):
#define EXPECT_EQ_BASE(equality, expect, actual, format) \
do {\
test_count++;\
if (equality)\
test_pass++;\
else {\
fprintf(stderr, "%s:%d: expect: " format " actual: " format "\n", __FILE__, __LINE__, expect, actual);\
main_ret = 1;\
}\
} while(0)
#define EXPECT_EQ_INT(expect, actual) EXPECT_EQ_BASE((expect) == (actual), expect, actual, "%d")
EXPECT_EQ_BASE
宏的编写技巧,简单说明一下:
do { /*...*/ } while(0)
包裹成单个语句,否则会有问题。do…while的用处之一就是在这,之二就是用在函数中,用来代替go to语法,海康的SDK就常用这。关键字:关键字是C++中预先保留的单词(又叫标识符),如下:
asm | do | if | return | typedef |
auto | double | inline | short | typeid |
bool | dynamic_cast | int | signed | typename |
break | else | long | sizeof | union |
case | enum | mutable | static | unsigned |
catch | explicit | namespace | static_cast | using |
char | export | new | struct | virtual |
class | extern | operator | switch | void |
const | false | private | template | volatile |
const_cast | float | protected | this | wchar_t |
continue | for | public | throw | while |
default | friend | register | true | |
delete | goto | reinterpret_cast | try |
提示:在给变量或者常量起名称时候,不要用C++得关键字,否则会产生歧义。
int a = 7;
// c++是int a 空间就开辟了,哪怕还没值,值来了再放进这空间;其他如python则是 var = 5, 有一个空间放5,这空间在叫var
int b; // 未初始化的全局变量
int globle() { // 全局函数
return 5;
}
int main() {
// 打印的全局变量
cout << a << " " << b << endl; // 7, 0 ,未初始化的b也不会报错,且为0
int a = 2;
int c; // 若是局部变量未初始化,就去使用就会报错
cout << a << " " << ::a << endl; // 2, 7 (前面局部,后面全局)
// 调用全局变量,再前面加两个冒号就好了
{
float b = 5.2f; // 块中再定义一个变量
}
//cout << d << endl; // 这里就会直接报错。
cout << ::globle() << endl; // 调用全局函数
//调用全局函数,一样加个:: 但是这里不加也行,因为块里没有这个名字的
system("pause");
return 0;
}
C++规定在创建一个变量或者常量时,必须要指定出相应的数据类型,否则无法给变量分配内存
可用sizeof求出数据类型占用内存大小,语法:sizeof(数据类型)
或者 sizeof(定义的变量名)
cout << sizeof(a) << endl;
cout << sizeof(long long) << endl;
常用来求一个数组的长度,比如在ffmpeg中看到的,定义成宏的方式:
#define FF_ARRAY_ELEMS(a) (sizeof(a) / sizeof((a)[0]))
// 定义并实例化一个结构体,自这种自定义结构的数组一样能获取长度
struct SFE {
enum AVSampleFormat sample_fmt;
const char* fmt_be;
const char* fmt_le;
};
struct SFE sample_fmt_entries[] = {
{ AV_SAMPLE_FMT_U8, "u8", "u8" },
{ AV_SAMPLE_FMT_S16, "s16be", "s16le" },
{ AV_SAMPLE_FMT_S32, "s32be", "s32le" },
};
int lens = FF_ARRAY_ELEMS(sample_fmt_entries);
C++中能够表示整型的有以下几种方式,区别在于==所占内存空间不同==:
short a = 32767;
int b = 11; long c = 12; long long d = 13;
数据类型 | 占用空间 | 取值范围 |
---|---|---|
short(短整型) | 2字节 | (-2^15 ~ 2^15-1) |
int(整型) | 4字节 | (-2^31 ~ 2^31-1) |
long(长整形) | Windows为4字节,Linux为4字节(32位),8字节(64位) | (-2^31 ~ 2^31-1) |
long long(长长整形) | 8字节 | (-2^63 ~ 2^63-1) |
Ps:注意别越界了,上面你定义的 a 值是可以的,再大一点就超出范围了,程序不会报错,但是打印出来的值,也就是a的值是错误的。
整型大小比较:short < int <= long <= long long
Ps:unsigned代表无符号,有符号的话(默认是有的),最高位的0(正号),1(负号)用来表示正负号了,所以表示的范围就比无符号的少(注意这种unsigned int k = -2;
,定义了无符号,还赋值符号,编译不会出错,但k的值错的离谱,一定注意)。如下:
int main() {
unsigned u = 10, u2 = 42;
std::cout << u2 - u << std::endl; // 32
std::cout << u - u2 << std::endl; // 4294967264
// 32位的,结果是这样来的2^32
int i = 10, i2 = 42;
std::cout << i - u << std::endl; // 0
std::cout << u - i << std::endl; // 0
std::cout << i - u2 << std::endl; // 4294967264
int j1 = -20;
unsigned j2 = 10;
std::cout << j1 + j2 << std::endl; // 4294967286
system("pause");
return 0;
}
注:当有符号的与无符号的混用时,结果一定是无符号的,是先会先把两个结果做计算,如果结果为正,那就是整数,如果为负数,就会把结果转成2^32+这个负数结果(32也是要根据所在环境位数来决定的)。
浮点型变量分类为两种:单精度float
双精度double
数据类型 | 占用空间 | 有效数字范围 |
---|---|---|
float | 4字节 | 7位有效数字 |
double | 8字节 | 15~16位有效数字 |
int main() {
float a = 3.14f; //单精度加个f,不然会默认改成双精度
double b = 3.1415926;
cout << a << endl;
cout << b << endl; // 打印出来的显示只会是3.14159,默认是6位有效数字,后面的就没有(要显示完还要做额外的配置)
/*科学计算法*/
float x = 3e2f; // e后是正数就是10的2次方
float y = 3e-2f; // e后是负数就是10的负2次方
system("pause");
return 0;
}
一种关于数据的初始化及类型强制转换时的数据丢失:
数据的初始化可以是int a = 123;
或int b(a);
或int c{a};
double a = 3.14159;
//int b{ a }, c = { a }; // 编译会出错,因为存在丢失信息的危险,
int d(a), e = d; // 正确,就会丢失小数部分
也就是说,使用{ }
来初始化,那定义的数据类型必须和传进来的数据类型一致,不然就会报错(这是因为列表初始化时,初始值存在丢失的风险时,编译器就会报错);反之,系统会智能去掉小数部分保留整数。
使用{ }
的初始化的形式叫==列表初始化==,现在无论是初始化对象还是某些时候为对象赋值,都可以使用这样一组由花括号括起来的初始值了。
在c++小知识.md中的21点memset,也有讲到不同的初始化方法,以及使用列表初始化。
类型 | 含义 | 最小尺寸 |
---|---|---|
char | 字符 | 8位(1个字节) |
wchar_t | 宽字符型 | 2或4个字节 |
char16_t | Unicode字符 | 16位(2个字节) |
char32_t | Unicode字符 | 32位(4个字节) |
字符型变量用于显示==单个字符==,语法:char name = 'a'
int main() {
char name = 'a';
cout << (int)name << endl; // 转成了ASCII码
cout << int(name) << endl; // 这两行效果一样
name = 99;
cout << name << endl; // 结果是c,前面定义了字符型,就可以通过ASCII码赋值
system("pause");
return 0;
}
新增用法,类似于python的r”“,:R"(这里面放字符串)"
字符串型用于显示==一串字符==,两种风格
char 变量名[20] = "字符串值;"
–> char str1[] = "hello world;"
->
const char *str2 = "hello"
// 定义时就赋值,可以不给20;只定义还是给上20;还可以写成指针形式,直接打印str2就是对应的结果,一定要加const
或者:char str1[] = { 'h', 'e', 'l', 'l', 'o', '\0' };
//也可以这样的形式给,注意结尾一定要有 \0
(这是零),才认定为字符串,其实上面的字符串也有,就是省略了。
char name[5] = "lisi";
后面字符串的长度最多只能是5-1=4
(可以不给5,后面长度就任意)const char* str3 = "hello";
string 变量名= "字符串值;"
–> string str2 = "hello world;"
#include<string>
== // 这个很重要数字转成字符串:==to_string==
#include <iostream>
#include <string> // 别忘了这
int main() {
int a = 456;
std::string b = '0' + std::to_string(a) // 导入头文件,函数 to_string()
return 0;
}
由单引号括起来的一个字符称为char型字面值,双引号括起来的零个或多个字符则构成==字符串型字面值==。
通过添加前缀或或后缀,可以改变整形、浮点型和字符型字面值得默认类型:
前缀 | 含义 | 类型 |
---|---|---|
u | Unicode16字符 | char16_t |
U | Unicode32字符 | char32_t |
L | 宽字符 | wchar_t |
u8 | UTF-8(仅用于字符串字面常量) | char |
后缀 | 类型 |
---|---|
u or U | unsigned (无符号整形) ,如 0U 跟 3.14f 一个意思 |
l or L | long (整形) |
ll or LL | long long (整形) |
f or F | float (浮点型) |
l or L | long double (浮点型) |
Tips:为了避免混淆,尽量使用大写的L,不用小写l。
ASCII值 | 控制字符 | ASCII值 | 字符 | ASCII值 | 字符 | ASCII值 | 字符 |
---|---|---|---|---|---|---|---|
0 | NUT | 32 | (space) | 64 | @ | 96 | 、 |
1 | SOH | 33 | ! | 65 | A | 97 | a |
2 | STX | 34 | ” | 66 | B | 98 | b |
3 | ETX | 35 | # | 67 | C | 99 | c |
4 | EOT | 36 | $ | 68 | D | 100 | d |
5 | ENQ | 37 | % | 69 | E | 101 | e |
6 | ACK | 38 | & | 70 | F | 102 | f |
7 | BEL | 39 | , | 71 | G | 103 | g |
8 | BS | 40 | ( | 72 | H | 104 | h |
9 | HT | 41 | ) | 73 | I | 105 | i |
10 | LF | 42 | * | 74 | J | 106 | j |
11 | VT | 43 | + | 75 | K | 107 | k |
12 | FF | 44 | , | 76 | L | 108 | l |
13 | CR | 45 | - | 77 | M | 109 | m |
14 | SO | 46 | . | 78 | N | 110 | n |
15 | SI | 47 | / | 79 | O | 111 | o |
16 | DLE | 48 | 0 | 80 | P | 112 | p |
17 | DCI | 49 | 1 | 81 | Q | 113 | q |
18 | DC2 | 50 | 2 | 82 | R | 114 | r |
19 | DC3 | 51 | 3 | 83 | S | 115 | s |
20 | DC4 | 52 | 4 | 84 | T | 116 | t |
21 | NAK | 53 | 5 | 85 | U | 117 | u |
22 | SYN | 54 | 6 | 86 | V | 118 | v |
23 | TB | 55 | 7 | 87 | W | 119 | w |
24 | CAN | 56 | 8 | 88 | X | 120 | x |
25 | EM | 57 | 9 | 89 | Y | 121 | y |
26 | SUB | 58 | : | 90 | Z | 122 | z |
27 | ESC | 59 | ; | 91 | [ | 123 | { |
28 | FS | 60 | < | 92 | / | 124 | | |
29 | GS | 61 | = | 93 | ] | 125 | } |
30 | RS | 62 | > | 94 | ^ | 126 | ` |
31 | US | 63 | ? | 95 | _ | 127 | DEL |
ASCII 码大致由以下两部分组成:
bool类型只有两个值,只占用==一个字节==:
bool a = true
or bool a = -2.1
or bool = 457
打印a出来的结果都是 1
Ps:c++中是没有True和False这样的布尔值的,,可以是int a = true int b = false;打印出来结果直接是1和0。(可通过bool的操纵符打印出来true和false)
作用:用于表示一些==不能显示出来的ASCII字符==
现阶段我们常用的转义字符有: \n \\ \t
转义字符 | 含义 | ASCII码值(十进制) |
---|---|---|
\a | 警报 | 007 |
\b | 退格(BS) ,将当前位置移到前一列 | 008 |
\f | 换页(FF),将当前位置移到下页开头 | 012 |
==\n== | ==换行(LF) ,将当前位置移到下一行开头== | ==010== |
\r | 回车(CR) ,将当前位置移到本行开头 | 013 |
==\t== | ==水平制表(HT) (跳到下一个TAB位置)== | ==009== |
\v | 垂直制表(VT) | 011 |
==\\== | ==代表一个反斜线字符”"== | ==092== |
' | 代表一个单引号(撇号)字符 | 039 |
" | 代表一个双引号字符 | 034 |
\? | 代表一个问号 | 063 |
\0 | 数字0 | 000 |
\ddd | 8进制转义字符,d范围0~7 | 3位8进制 |
\xhh | 16进制转义字符,h范围0~9,a~f,A~F | 3位16进制 |
示例:
int main() {
/*制表符加上前面的一共占8个位置a多空格就少,这样三行的hello都是在同个地方开头的*/
cout << "aa\thelloworld" << endl;
cout << "aaaa\thelloworld\n"; // 可以这样直接换行
cout << "a\thelloworld" << endl;
system("pause");
return 0;
}
变量类型还有: NULL变量代表没有;
==auto类型==,简单来说就是auto a = 3.1
,它会自己去推断这个类型是什么.
c++11新标准引入了auto类型说明符
auto让编译器通过初始值来推算变量的类型,故auto定义的变量必须有初始值
const一般会忽略掉顶层const,同时底层const则会保留下来
const int num = 123;
auto a = num; // a是一个整形(num的顶层const被忽略了)
auto b = # // b是一个指向整数常量的指针(对常量对象取地址是一种底层const)
作用:用于从键盘获取数据
关键字:==cin==, 语法:cin >> 变量
int main() {
//char a[] = "hello";
char a = 'R'; // 注意数据定义的类型,定义字符,给字符串就只会保留第一个字符
std::cout << "现在的数据是"<< a << std::endl;
std::cin >> a; // 核心就是这里,跟python的input是一样的
std::cout << "输入的数据是" << a << std::endl;
return 0;
}
算术运算符包括以下符号:
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
+ | 正号 | +3 | 3 |
- | 负号 | -3 | -3 |
+ | 加 | 10 + 5 | 15 |
- | 减 | 10 - 5 | 5 |
* | 乘 | 10 * 5 | 50 |
/ | 除 | 10 / 5 | 2 |
% | 取模(取余) | 10 % 3 | 1 |
++ | 前置递增 | a=2; b=++a; | a=3; b=3; |
++ | 后置递增 | a=2; b=a++; | a=3; b=2; |
– | 前置递减 | a=2; b=–a; | a=1; b=1; |
– | 后置递减 | a=2; b=a–; | a=1; b=2; |
int main() {
int a = 10;
int b = 3;
std::cout << a / 3 << std::endl; // 结果是 3, 整数之间的除法只能得到整数的
return 0;
}
Ps:然后两个小数也是不能取模运算的
前置/后置 递增
int main() {
/*下面这两个的结果是一样*/
int a = 10;
a++; // 后置递增
std::cout << a << std::endl; // 11
int b = 10;
++b; // 前置递增
std::cout << b << std::endl; // 11
int a2 = 10;
int b2 = ++a2 * 10;
std::cout << "a2:" << a2 << "; " << "b2:" << b2 << std::endl; // 11和110
int a3 = 10;
int b3 = a3++ * 10;
std::cout << "a3:" << a3 << "; " << "b3:" << b3 << std::endl; // 11和100
return 0;
}
前置递增先对变量进行++,再计算表达式;后置递增则是先计算表达式,再对变量进行++
故:==最终变量自己一定进行了++操作,没有赋值操作的话,两个一模一样==;但有赋值的时候,两个有区别结果不一样。
在写if条件判断的时候就用这,不再是and、or
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
! | 非 | !a | 如果a为假,则!a为真; 如果a为真,则!a为假。可以有!!a |
&& | 与 | a && b | 如果a和b都为真,则结果为真,否则为假。 |
|| | 或 | a || b | 如果a和b有一个为真,则结果为真,二者都为假时,结果为假。 |
cout « (a && b) « endl; // 注意用这运算符时一定要括号,
注意:以下都是按二进制的形式来。
看这个菜鸟教程。&按位与、 ** | 按位或 、 **^按位异或、 ~按位取反 |
在上面链接的最后的笔记里有讲»和«,这里简单写下:(也可参考和这个笔记)
>>
:向右位移,就是把尾数去掉位数,例如:153 » 2,153的二进制是:10011001,屁股后面去掉 2 位 100110,100110 转化成十进制就是 38,153 = 10011001,38 =100110,”01” 去掉了。<<
:向左位移,就是把开头两位数去掉,尾数加位数00。
注意这种花括号结尾都是没分号的
单行格式if语句:if (条件) {条件满足执行的语句}
多行格式if语句:if (条件) {条件满足执行的语句} else {条件不满足执行的语句}
多条件的if语句:if (条件1) {条件1满足执行的语句} else if (条件2) {条件2满足执行的语句} else {都不满足执行的语句}
int score = 0;
std::cout << "请输入一个分数" << std::endl;
cin >> score;
std::cout << "输入的分数为:" << score << std::endl;
if (score > 512) {
std::cout << "恭喜考上一本大学" << std::endl;
}
else {
std::cout << "很遗憾" << std::endl;
}
语法:表达式1 ? 表达式2 : 表达式3;
int a = 10;
int b = 20;
int c = 0;
c = a < b ? a : b; // 复杂一点你的三目表达式还是用括号括起来,c = (a < b ? a : b);
std::cout << c << std::endl; // 10
特别注意:三目运算符返回的是变量,可以继续赋值
int a = 10;
int b = 20;
(a > b ? a : b) = 130; // 这里等式1不成立,所以返回的是b,再把130赋值给b,故此时b=130
std::cout << a << std::endl; // 10
std::cout << b << std::endl; // 130
执行多条件分支语句:每个case里都还是给上break,不然会一直执行下去,比如score给的9,那么就会直接执行case 9的代码,然后8、default;
int score = 0;
std::cin >> score;
switch (score) { // 一定要去看下面的注意 3
case 10:
std::cout << "完美" << std::endl;
break;
case 9:
std::cout << "非常好" << std::endl;
break;
case 8:
std::cout << "好" << std::endl;
break;
default:
std::cout << "不好" << std::endl;
}
就是一个注意点,case后面跟的必须是整形常量表达式(单个字符也是可以的)
int main(int argc, char **argv) {
//unsigned ival=1, jval=2, kval=3; // 错的,编译都通过不了,分析一下,这样ival的值是可以改变的,并不固定
const int ival = 1, jval = 2, kval = 3; // 这加了const,就成常量了
unsigned out;
unsigned judge = 2;
switch (judge) {
case ival: // 这里跟的值也必须是不变的常量
out = ival * sizeof(int);
break;
case jval:
out = jval * sizeof(int);
break;
case kval:
out = kval * sizeof(int);
break;
}
std::cout << out << std::endl;
return 0;
}
注意1:switch语句表达式;类型只能是==整型==或==字符型==;
注意2:case里如果没有break,程序会从进入的case语句一直向下执行完;
注意3:==case跟的语句很短,就一两行的话没事,要是比较长,就要把这些代码(不包括break)用一个{}括起来,表明这是一个代码块==;
注意4:对3的扩充,就一行函数可以,但是用了函数+ 1行别的就要括起来。
注意5:default语句不是必须得。
对比:与if语句比,对于多条件判断时,switch的结构清晰,执行效率高,缺点是switch不可以判断区间
练习5.14:编写一段程序,从标准输入中读取若干string对象并查找连续重复出现的单词。所谓连续重复出现的意思是:一个单词后面紧跟着这个单词本身。要求记录连续重复出现的最大次数以及对应的单词。如果这样的单词存在,输出重复出现的最大次数;如果不存在,输出一条信息说明任何单词都没有连续出现过。例如,如果输入是 how now now now brown cow cow 那么输出应该表明单词now连续出现了3次。
代码(自己写的):
int main(int argc, char **argv) {
std::string str, str0, str1, temp;
unsigned int out = 1, count = 1;
std::cin >> str0;
while (std::cin >> str1) {
temp = str0; // 把变化之前的值记录下来
if (str0 != str1) {
str0 = str1;
count = 1;
}
else {
++count;
}
if (out < count) {
out = count;
str = temp;
}
}
if (out == 1) {
std::cout << "没有" << std::endl;
}
else {
std::cout << "单词" << str << "连续会出现了" << out << "次。" << std::endl;
}
system("pause");
return 0;
}
示例代码:
#include <iostream>
#include <string>
using std::cout; using std::cin; using std::endl; using std::string; using std::pair;
int main() {
std::pair<string, int> max_duplicated;
int count = 0;
for (string str, prestr; cin >> str; prestr = str) {
if (str == prestr) ++count;
else count = 0;
if (count > max_duplicated.second) max_duplicated = { prestr, count };
}
if (max_duplicated.first.empty()) cout << "There's no duplicated string." << endl;
else cout << "the word " << max_duplicated.first << " occurred " << max_duplicated.second + 1 << " times. " << endl;
return 0;
}
while(std::cin » val) unix系统中是ctrl+d来标志着输入结束
系统生成[0, 100]随机数:int num = rand() % 100 +1;
// 前面的表达式固定这么写生成0-99的数,后面再+1就达到,应该也可以直接用int num = rand() % 101;
C++这样子每次运行的随机数都是一样的,得生成数字前加随机种子:srand((unsigned int)time(NULL));
// 这是利用当前系统时间生成随机数,固定写法(之间没空格),且还得添加一个头文件:#include <ctime>
// 这是time系统时间头文件
#include <iostream>
#include <string>
#include <ctime> // 搭配根据时间的随机种子
int main() {
// srand、time、rand不用加std都是可以的
srand((unsigned int)time(NULL)); // 固定随机种子写法
int num = rand() % 100 + 1; // 随机数(不+1,这就是生成一个随机数,范围是0-99)
int val = 0;
while (1) {
std::cout << "请猜数字:" << std::endl;
std::cin >> val;
if (val > num) {
std::cout << "数字大了" << std::endl;
}
else if (val < num) {
std::cout << "数字小了:" << std::endl;
}
else {
std::cout << "猜中了:" << std::endl;
break;
}
}
return 0;
}
语法:do {循环执行的语句} while (循环条件);
int num = 0;
do {
std::cout << num << std::endl;
num++;
} while (num <= 10);
总结:这与while最大的区别就是,这无论怎样都要先执行一次循环语句,再判断循环条件,而while循环可能直接进不去的。
最常用的做法还是do{…} while(0); :
- 用于宏定义代码块。
- 替代掉goto用法。
在c++中计算几次方:
#include <cmath> // 要在上面导入这个头文件
int main() {
int value = 0;
int a = 4;
value = pow(a, 3); // 这就是a的3次方
}
for循环一句的特殊写法:
int i = 100, sum = 0;
for (int i = 0; i != 10; ++i)
sum += i; // 循环体只有一句的话,不可以不要花括号,但只能有一句,多的都不算进循环体的
std::cout << i << " " << sum << std::endl; // 100 45
// 还有更常见的写法:
if (i == 5) continue;
特别来说明:for中第二种是 满足条件,而不是像python那种退出条件
for (int i = 5; i >= 0; i--) {
std::cout << array[i] << std::endl;
}
// 这样倒着输出数值,那就是 i >= 0 才去执行,不是想着小于0就不执行退出而写成 i < 0 ,那样永远满足不了条件,就永远进不去循环
注意这种大括号、花括号后面都是 没有分号的
语法:for (起始表达式; 满足条件的表达式; 末尾循环体) {循环语句}
要注意一点,for里面这三个表达式可以任意两个或一个甚至0个,但是里面的两个 分号 一个也不能少,这三个式子都可以在其它地方写的:
int i = 0;
for (; ; i+=2) { // 这里的i+=2也可以写进循环里的
if (i % 2 == 0) {
continue;
}
std::cout << i << std::endl;
if (i > 50) {
break;
}
}
练习1:从1开始数到数字100, 如果数字个位含有7,或者数字十位含有7,或者该数字是7的倍数,我们打印敲桌子,其余数字直接打印输出。
for (int i = 1; i <= 100; i++) {
if ((i % 7 == 0) || (i % 10 == 7) || (i / 10 == 7)) {
std::cout << "敲桌子" << std::endl;
}
else {
std::cout << i << std::endl;
}
}
练习2:打印乘法口诀表
for (int i = 1; i < 10; i++) {
for (int j = 1; j < i + 1; j++) {
std::cout << j << "*" << i << "=" << (i * j) << "\t";
}
std::cout << std::endl;
}
语法:goto 自己定义的标记;
//别忘了这个分号
如果标记的名称存在。执行到goto语句时,会跳转到标记的位置。
std::cout << "这是第1行代码" << std::endl;
cout << "这是第2行代码" << std::endl;
goto MYFLAG; // 标记得起名尽量就全大写吧(跟变量名一样)
// int a = 0; // 错误,所有的变量的声明一定要在第一次出现的goto语句之前
std::cout << "这是第3行代码" << std::endl;
std::cout << "这是第4行代码" << std::endl;
MYFLAG: // 自定义的标记名后记得跟个冒号
std::cout << "这是第5行代码" << std::endl;
注意:
在c++11新标准引进的连个名为begin和end的函数,用于获取数组的首地址和末尾地址的后一个:
int arr[] = { 1, 2, 3, 4, 5 };
std::vector<int> v(std::begin(arr), std::end(arr));
// 当然也能值拷贝一段值
std::vector<int> v1(arr +1, arr+3);
我自己推荐第三种吧。
数据类型 数组名[数组长度];
// 都要给数组长度
数据类型 数组名[数组长度] = {值1, 值2, 值3};
// 值的个数小于等于数组长度(那剩余位置会自动填0)数据类型 数组名[] = {值1, 值2, 值3.....};
// 这种值可以给任意个动态给
数组长度
int n; cin » n; int *array1 = new int[n]; delete[] array1; // 记得释放内存,数组释放记得加
[]
==注意==:尽量别用 char类型的数组吧,跟上面的C风格的字符串定义很相似,然后像下面这个例子,按道理打印数组名 arr1 ,得到的应该是数组的首地址,但结果却含有乱码。
char arr1[2]; // 尽量不使用这种类型的数组
arr1[0] = 'x';
arr1[1] = 'y'; // 这样去赋值
// arr1[2] = 'z'; // 这是错的,千万别超出了,可能会有结果,但一定是错的
int arr2[5] = {10, 20, 30}; // 定义了5个长度,只给了3个,那后面2个就默认填0了
std::cout << arr2[4] << std::endl;
// cout << arr2[5] << endl; //这是错的,千万不要索引越界了,会有结果,但是错的离谱
由于上面这个特性,很多时候这样来初始化一个数组
float maskInputValues[300] = { 0 }; // 这样数组里的值就全是0了
// 这要在上面加入<string>的头文件
string arr3[] = { "dasd", "asdas", "asda" }; // 后面给几个,前面会知道有几个的
Ps:上面这个是字符串的数组,只能使用string,不能使用C的风格,因为C风格定义字符串就是 char a[] = "hello";
虽然可以像第三种定义数组的方式 char a[] = {"hello"}
,但是里面只能放一个值,多一个都要报错
获取整个数组占用的内存空间大小
通过数组名取到数组的首地址
int array[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
cout << "数组所占空间:" << sizeof(array) << endl;
cout << "每个所占空间:" << sizeof(array[0]) << endl; // 一个数组中数据类型都一样
cout << "数组的元素个数:" << sizeof(array) / sizeof(array[0]) << endl;
cout << "数组首地址:" << (int)array << endl;
cout << "数组第1个元素地址:" << (int)&array[0] << endl; // 这和上面的首地址是一样的
cout << "数组第2个元素地址:" << (int)&array[1] << endl; // 这跟上面的地址就差4,因为int是4个字节
// array[0]是把值打出来,加了个取址符 & ,结果好像是16进制的,再 (int) 强转成10进制的
Ps: array = 100; 这也是绝对错误的,==数组名是常量,因此不可以再赋值了==
练习:将数组反转:
int array[] = {1, 2, 3, 4, 5, 6};
int len = sizeof(array) / sizeof(array[0]);
int temp = 0;
int times = len / 2; // 这是交换次数
int i = 1;
for (times; times > 0; times--) {
temp = array[len - i];
array[len - i] = array[i - 1];
array[i - 1] = temp;
i++;
} // 可以定义start=0,end=数组长度-1的下标,然后start++,end--,直到while (start < end)才做
/*
冒泡排序:下面第一个是我自己写的(我的好像不大像冒泡,但实现了效果):
array[0]和所有所有数比大小,把最小的放到array[0];然后再用array[1]和后面所有数比大小,把最小的再放array[1],然后这样弄完;
*/
int array1[] = { 4, 2, 8, 0, 5, 7, 1, 9, 6 };
int len = sizeof(array1) / sizeof(array1[0]);
for (int i = 0; i < len - 1; i++) {
for (int j = i + 1; j < len; j++) {
if (array1[i] > array1[j]) {
int temp = array1[i];
array1[i] = array1[j];
array1[j] = temp;
}
}
}
/*
教学视频的方法;第一轮也是所有数两两相比,array1[0]?array1[1]、array1[1]?array1[2]...array1[len-2]?array1[len-1],然后最大的就到最后去了;
接着第二轮又是array1[0]?array1[1]、array1[1]?array1[2]...array1[len-3]?array1[len-2],知道倒数第2个数(它就是这轮最大的);
多轮这样下去后就完成了冒泡排序
*/
int array1[] = { 4, 2, 8, 0, 5, 7, 1, 9, 6 };
int len = sizeof(array1) / sizeof(array1[0]);
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < len - i - 1; j++) { // 注意下面会用到j+1,所以j < len - i - 1这里一定要有-1
if (array1[j] > array1[j + 1]) {
int temp = array1[j];
array1[j] = array1[j + 1];
array1[j + 1] = temp;
}
}
}
for (int i = 0; i < len; i++) {
cout << array1[i] << endl;
}
我自己推荐就使用第二种
数组类型 数组名[行数][列数];
// 跟以为数组一样,定义这这,后面去赋值数组类型 数组名[行数][列数] = { {数据1, 数据2}, {数据3, 数据4}};
// 推荐就使用这,直观数据类型 数组名[行数][列数] = {数据1, 数据2, 数据3, 数据4};
// 与上不同点是可以只用一个花括号数据类型 数组名[][列数] = {数据1, 数据2, 数据3, 数据4};
// 同样可以只给列数,会自动计算行数==遍历:c++11新标准引进的连个名为 begin 和 end 的函数==
假设有一个数组arr1,可以通过 std::end(arr1) - std::begin(arr1)
来获取数组的个数
std::string str[] = { "hello", "world", "this", "is" };
// 数组还可以这样遍历
std::string *beg = std::begin(str); // 获得首指针
auto *last = std::end(str); // 获得str数组尾元素的下一位置的指针
for (; beg != last; ++beg) {
std::cout << *beg << std::endl;
}
// 若是有两个指针,p1, p2 都指向同一个数组中的元素,那么
p1 += p2 - p1;
// 那这种操作就是把p1移动到p2位置,在任何场景下都是合法的,p1、p2无论哪个大都行
注意:这个指针跟上面的vector的iteration迭代器用法一致,也是可以指针+一个整数来变换位置的这些操作的。
先把下标为2的元素地址赋值给一个指针,然后这个指针是可以以自己为中心,进行下标的+-运算的,p[1],那就是代表str[3]的值,p[-2]那就是代表str[0]的值
std::string str[] = { "hello", "world", "this", "is" };
std::string *p = &str[2];
std::string j = p[1];
std::string k = p[-2]; // 这俩都不是指针了
std::cout << *p << std::endl; // "this"
std::cout << j << std::endl; // "is"
std::cout << k << std::endl; // "hello"
std::cout << str[1] << std::endl; // "world",这个数组+下标的结果直接就是值
double b[5] = { 100.2, 2.3, 3.4, 7.1, 50 }; // 最后一个元素我放整数好像也行吼
cout << *b << endl; // 取出第一个元素
cout << *(b + 1) << endl; // 取出第二个值
cout << *(b + 9) << endl; // 越界取值,危险操作(一定不要)
void test01() {
int a[] = {0, 1, 2, 3, 4, 5};
// 前面是引用,a说是指针,这样就获取了数组里的元素
for (int& k : a) {
cout << k << endl;
} // 当然还有传统的循环去取值a[i]
}
Ps:这种 for(auto k : v) v是std::vector也是可用的。
// 创建一个二维数组
int c[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
cout << c[1][2] << endl; // 取出第一行第二列的值
cout << c[1] << endl; // 取出第一行第一个元素的指针
cout << *c[1] << endl; // 结果为4
cout << *(c[1] + 1) << endl; // 结果为5
多维数组的打印的其它方式,如把下面数组arr打印出来::
int arr[3][4] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
int main() {
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 4; ++j) {
std::cout << arr[i][j] << ' ';
}
std::cout << std::endl;
}
}
直接的指针来:
注意下面的声明,圆括号必不可少:
- int *row[4]; // 整形指针的数组
- int (*row)[4]; // 指向含有4个整数的数组(数组名都是指针那种)
int main() {
// 这里必须是int(*row)[4],代表4个值的数组,只是int *row只是一个整形指针,跟数组无关。
for (int(*row)[4] = arr; row != arr + 3; ++row) {
// 下面这两种写的方式都是可以的
//for (int *col = *row; col != *row + 4; ++col) {
for (int *col = std::begin(*row); col != std::end(*row); ++col) {
std::cout << *col << ' ';
}
std::cout << std::endl;
}
}
把int (*row)[4]重新取一个名字,方便写(4是第二个维度的个数是4,可以是其它的):
有两种方式(这俩效果一样,使用都是当数据类型 arr_4 ):
- using arr_4 = int[4];
- typedef int arr_4[4];
- // 注意写法,不能是typedef int[4] arr_4;这是错的
int main() {
//using arr_4 = int[4];
typedef int arr_4[4]; // 这俩是一样的
// 注意下面这种写法
for (arr_4 *row = arr; row != arr + 3; ++row) {
// 这两种写的方式都是可以的
for (int *col = *row; col != *row + 4; ++col) {
//for (int *col = std::begin(*row); col != std::end(*row); ++col) {
std::cout << *col << ' ';
}
std::cout << std::endl;
}
}
for循环
const不是必须的,但只是读,就加上吧;
这里必须是&row,必须要有引用,不单单是下面操作是只读,引用无所谓,更深层次的为为了避免被自动转成指针,假如不用引用&,则成了一下形式
for (auto row : arr) // 当然这里用auto &row是可以的 for (auto col : row)
这样程序时编译不通过的,因为第一遍遍历arr,得到的是大小为4的数组,row没用引用,那么==编译器初始化row时就会自动将这些数组形式的元素转换成指向该数组内收元素的指针==,这样得到的row的类型就是==int*==,那显然内层的循环就不再合法。
故:总结:要使用for语句处理多维数组,除了最内存的循环外,其它所有循环的控制变量都应该是引用类型。
for (const int(&row)[4] : arr) {
for (int col : row) {
std::cout << col << ' ';
}
std::cout << std::endl;
}
int len1 = 42; // 不是常量表达式
constexpr int len2 = 45; // 常量表达式
int array1[len1]; // 这是错的(但这在clion里可以,尽量不用)
int array2[len2]; // 用这,这是OK的
// 假定 get_size() 是一个返回整形的函数
int array3[get_size()]; // 若 get_size()是常量表达式则正确,否则就是错误的
int *parr[11]; // 含有11个整形指针的数组
字符数组的特殊性: 空字符:”\0”,空字符往往作为字符串的结束标志
char a1[] = {‘c’, ‘+’, ‘+’}; // 列表初始化,没有空字符, // 3
char a2[] = {‘c’, ‘+’, ‘+’, ‘\0’}; // 含有显式空字符, // 4
char a3[] = “c++”; // 这会自动添加表示字符串结束的空字符, // 4
char a4[5] = “hello”; // 这是错的,没有空间放空字符
一般来说,不能将数组的内容拷贝给其它数组作为初始值,也不能用数组为其它数组赋值(有些编译器支持数组的赋值,这是==编译器扩展==,但尽量还是避免使用者非标准特性)
int a[] = {0, 1, 2};
int a1[] = a; // 错的
a2 = a; // 错的
std::string s1[10];
int ia1[10];
int main() {
std::string s2[10];
int ia2[10];
return 0;
}
ps: s1、s2全都默认为空;ia1会被全部自动初始化为0,ia2的元素全部未定义
函数的形参列表可以为空,在为了C语言兼容,可以用关键字void表示函数没形参:
返回类型:函数返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
语法:
返回值类型 函数名 (参数列表) {
函数体内执行的语句;
return 表达式; // 注意返回值的类型必须和函数名前一样
}
Ps:如果不需要返回值,定义函数时可以写 void 函数名() {} 然后可以省略掉return语句
例:定义一个加法函数,实现两个数的相加
int add(int num1, int num2) {
int sum = num1 + num2;
return sum;
}
作用:告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。
// 函数声明
int max(int num1, int num2);
// 这里因为定义的函数max在main后面,所以需要在其签名声明这个函数的存在,只要前面那段就好了。
int a = 333; // 全局变量
int main() {
int a = 10;
int b = 20;
int result = max(a, b);
cout << result << endl;
// 可以在前面加冒号代表调用全局函数(这里没重名的,不要也行)
int res = ::max(a, b);
cout << ::a << a <<endl; // 333 10
// 注意这种有全局变量和局部变量重名的
system("pause");
return 0;
}
int max(int num1, int num2) {
return num1 > num2 ? num1 : num2;
}
实例:
1、max.h
自己定义的头文件
#pragma once // 这行是定义头文件时自动生成的
#include <iostream>
using namespace std;
int max_func(int num1, int num2); // 这是函数声明
2、max.cpp
同名源文件
#include "max.h" // 导入定义的头文件
// 这函数名必须跟头文件函数声明一样
int max_func(int num1, int num2) {
return num1 > num2 ? num1 : num2;
}
Ps:系统带的头文件时#include <iostream>
自定义头文件导入#include "max.h"
3、main函数入口的mytest.cpp
文件
#include "max.h" // 导入头文件就是
int main() {
int a = 21;
int b = 20;
int result = max_func(a, b);
cout << result << endl;
system("pause");
return 0;
}
函数指针是指向的函而非对象。
bool (*pf) (const std::string &, const std::string &); // 未初始化
解读:pf是一个指针,它指向一个函数,该函数的参数是两个const std::string的引用,返回值是bool类型。(千万注意:==*pf两端的括号必不可少==,如果没有这对括号,则pf是一个返回值为bool指针的函数)
使用函数指针:==当我们把函数名作为一个值使用时,改函数自动转换成指针==,
假设有个函数是:bool my_print (const std::string &, const std::string &); 那么:
pf = my_print; // pf指向名为my_print的函数
pf = &my_print; // 等价的赋值语句:即取地址符是可选的
此外,还可以直接使用指向函数的指针调用该函数,无须提前解引用指针(感觉就像是起了个别名啊);
bool b1 = pf(“hello”, “nihao”); bool b2 = (*pf) (“hello”, “nihao”); bool b3 = my_print(“hello”, “nihao”); // 三个都是等价的调用
感觉比较复杂了:还可以使用==尾置返回类型==的方式声明一个返回函数指针的函数:
auto f1(int) -> int (*) (int *, int); // 了解吧,理不顺了(书223页)
练习:编写函数的声明,令其接受两个int形参并返回类型也是int,然后声明一个vector对象,令其元素是指向该元素的指针。
解答:
int abc(int, int);
std::vector<int(*)> v; // 这是我写的(就是错的,不能这么来)
// 标准答案
std::vector<decltype(abc)*> v1;
特别注意:==将decltype==作用于某个函数时,它返回函数类型而非指针类型,因此我们需要显式的加上*以表明我们需要返回指针,而非函数本身。
接着练习:编写三个函数,并用vector对象保存这些函数的指针,然后再输出出来:
#include <iostream>
#include <vector>
int func(int, int);
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int mul(int a, int b) {
return a * b;
}
int main() {
// 这里应该是能用 std::function<int(int, int)>来代替 decltype(func)*
// 这就需要头文件 <functional>,然后替代后,下面的 “效果一样” 那两行就是错误的了
std::vector<decltype(func)*> vec{add, &sub, mul};
for (auto v : vec) {
std::cout << v(3, 2) << std::endl;
std::cout << (*v)(3, 2) << std::endl; // 效果一样
}
// 这里就是两种方式,都是一样的
for (auto iter = vec.begin(); iter != vec.end(); ++iter) {
std::cout << (*iter)(3, 2) << std::endl;
std::cout << (**iter)(3, 2) << std::endl; // 效果一样
}
system("pause");
return 0;
}
Tips:
==指针的作用:可以通过指针间接访问内存(我的理解是指针用来存放变量的内存地址)==(指针就是一个地址)
- 内存编号是从0开始记录的,一般用十六进制数字表示
- 可以利用指针变量保存地址
定义语法:数据类型 *变量名;
int main() {
int a = 10;
int *p; // 必须定义同类型的指针
int *p2 = &a; // 也可以在定义时就建立关系
p = &a; // (建立关系),用取址符得到地址,再赋值给 p
cout << &a << endl;
cout << p << endl; // 这俩的结果都是一样的,都是地址
cout << *p << endl; // 10;通过 * 解引用 获得指针变量指向的内存
cout << *&a << endl; // 10;先&取址,再*解引用
system("pause");
return 0;
}
Ps:普通变量 a 存放的是数据;而指针变量存放的是==地址==。
int *p;
cout << sizeof(int *) << endl; // 放指针类型;4
cout << sizeof(p) << endl; // 放指针;4
cout << sizeof(double *) << endl;
总结:空指针和野指针都不是我们申请的空间,因此不要访问。 用指针时要么用new创建对象,要么用智能指针,以避免悬空指针。
用途:初始化指针变量 —— int *p = NULL;
注意:空指针指向的内存是不可以访问的(也就是不能去解引用,语法上没错,但是是非法的,运行会报错)
int *p = NULL;
// 访问空指针报错
//内存编号0 ~255为系统占用内存,不允许用户访问
cout << *p << endl;
野指针:指针变量指向非法的内存空间
//指针变量p指向内存地址编号为0x1100的空间
int * p = (int *)0x1100;
// 我的理解是 (int *) 定义一个整型指针,后面跟的是地址,跟 int a=10; int *p = &a; 有点像
//访问野指针报错
cout << *p << endl;
int *ip; // 指针变量的申明;这是野指针,它还没有指向哪里
悬空指针:指针是有效的,但其指向的对象已经释放。 // 所以用指针时要么用new创建对象,要么用智能指针
class MyClass {
private:
int *ptr;
int *ptr_new;
int a;
int b;
public:
MyClass() : ptr(nullptr) {
a = 10;
}
void initialize() {
b = 20;
// 注意:temp的生命周期在initialize结束后就结束了
int temp = 42; // 在栈上创建一个整数
ptr = &temp; // 将指针指向栈上对象
ptr_new = new int(42);
}
void display() {
this->initialize(); // 在display中调用initialize
// 这里结束后,temp生命周期就到了,ptr已经变成了悬空指针
if (ptr != nullptr) {
// 会执行这里,但打印出来的结果绝不是 42,而是一个随机未知的数字
// 因为这里访问的是无效内存,temp在initialize()结束时已经销毁
std::cout << "Inside display(), ptr points to: " << *ptr << std::endl;
}
else {
std::cout << "Pointer is null or invalid!" << std::endl;
}
if (ptr_new != nullptr) {
// 这就是OK的,因为new的对象在栈上
std::cout << "Inside display(), ptr_new points to: " << *ptr_new << std::endl;
}
std::cout << "a:" << a << std::endl; // ok的
std::cout << "b: " << b << std::endl; // 这是OK的
}
~MyClass() {
// 析构函数中不需要释放ptr指向的内存,因为它指向栈上的变量
// 但要释放new的堆上的数据
delete ptr_new;
}
};
int main(int aegc, char* argv[]) {
MyClass obj;
obj.display();
return 0;
}
const修饰指针有三种情况
const修饰指针 – 常量指针
int a = 10;
int b = 20;
const int * p1 = &a;
cout << *p1 << endl; // 10
p1 = &b; // 常量指针是可以改指针的指向的;这里就没有用到 *
// const在 * 前 ,那 *p1 指向的值肯定不让改,那它 p1代表的指针指向就可以改
cout << *p1 << endl; // 20
// *p1 = 30; // 直接报错,是不能去改值的
// const修饰了指针,那这种带 * 的解引用再赋值肯定不让了
const修饰常量 – 指针常量
int a = 10;
int b = 20;
int * const p2 = &a;
*p2 = b; // 或 *p2 = 20
cout << *p2 << endl; // 20
cout << a << eendl; // 此时再打印a结果也是20了,因为两是同样的地址
// p2 = &b; // 也是直接报错,const就p2前面,那p2代表的指针指向就不能改了;它 *p 指向的值就可以改
const即修饰指针,又修饰常量
int a = 10;
int b = 20;
const int * const p3 = &a; // 相当于只可读,都不能改
int array[] = { 888, 2, 3, 4, 5, 6 };
int *p = NULL;
p = array; // 数组名就代表数组的首地址
cout << *p << endl; // 解引用后就是数组第一个值888
for (int i = 0; i < 6; i++) {
//cout << array[i] << endl;
cout << *p << endl;
p++; // 指针++,它会根据自己的类型,比如这里就是4个字节向后移,就能到了所有的地址
}
// 值传递
void swap01(int x, int y) {
int temp = x;
y = x;
x = temp;
}
// 地址传递
void swap02(int *x, int *y) {
int temp = *x; // 找到指针x地址,然后*解引用,
*x = *y; // 这里都一样,相当于直接操作的传进来的a、b
*y = temp;
}
int main() {
int a = 10;
int b = 20;
swap01(a, b); // 值传递,是改变不了实参a、b的值
cout << a << "\t" << b << endl;
swap02(&a, &b); // 地址传递,下面啊a、b的值已经交换
cout << a << "\t" << b << endl;
system("pause");
return 0;
}
#include <iostream>
using namespace std;
void bubbleSort(int *p, int len) {
cout << p << endl; // 是传进来数组的首地址
cout << *p << endl; // 解引用后得到的就是数组的第一个值
cout << sizeof(p) << endl; // 4;p是指针,无论什指针都只占4个字节
for (int i = 0; i < len; i++) {
for (int j = 0; j < len - 1 -i; j++) {
if (p[j] > p[j+1]) { // 注意这里的p就跟主函数里的arr鲜果一样了,除了sizeof的值可能不同
int temp = p[j];
p[j] = p[j+1];
p[j+1] = temp;
}
}
}
for (int i = 0; i < len; i++) { // 和main函数里的实现效果一样
cout << *p++ << endl;
}
}
int main() {
int arr[] = {7, 3, 4, 6, 1, 8, 2, 9, 0, 5};
int len = sizeof(arr) / 4;
int *p = arr; // 或者int *p; p = arr;
// 这也是循环打印数组;通过指针
for (int i = 0; i < len; i++) {
//cout << *p << ' ';
//p++; // p++使指针向后移动;因为是整形,所以4个字节(4个字节是我的理解,不知道其它数据类型是不是相应的变化)
cout << *p++ << ' '; // 这一行跟上面两行实现的效果一样
}
cout << endl;
// 上面写的函数,实现冒泡排序,且是地址传递,所以下面打印的时候,顺序已经改变
bubbleSort(arr, len);
for (int i = 0; i < len; i++) {
cout << arr[i] << ' ';
}
cout << endl;
system("pause");
return 0;
}
因为数组不能被拷贝,所以函数不能返回数组;不过,函数可以返回数组的指针或引用。但是从语法上来说,要想定义一个返回数组的指针或引用比较繁琐,比较简单的处理办法是使用==类型别名==(这在c++关键字.md中类型别名写到过的)
- typedef int arrT[10]; // 固定写法,arrT是一个类型别名,表示的类型是含有10个整数的数组;
- using arrT = int[10]; // arrT的等价声明,上面写到过的
那么 arrT* func(int i); // 函数func返回的就是一个指向含有10个整数的数组的指针
经典例子:
- int arr[10]; // arr是一个含有10个整数的数组;
- int *p1[10]; // p1是一个含有10个整形指针的数组;
- int (*p2)[10] = &arr; // p2是一个指针,他指向含有10个整数的数组。
所以当一个函数要返回数组指针时,如果不使用类型别名,那就会是这么定义:
int (*func(int i)) [10];
// 跟上面例子第三个加括号是一个意思
但在c++11新标准中还有一种==尾置返回类型==简化这声明方法,上面的就可以写成:
auto func(int i) -> int(*)[10];
// auto、-> 这都是固定写法,不管引用还是指针,都是在->后面的括号里体现。
还有另外一中方法,使用decltype
,
int s[10];
decltype(s) *func();
练习:编写一个函数声明,使其返回数组的引用并且该数组包含10个string对象
- 最原始的声明:std::string (&func0())[10];
- 使用类型别名:
- using str10_1 = std::string[10]; str10_1 &func1();
- typedef std::string str10_2[10]; str10_2 &func1();
- 使用尾置返回类型:auto func2() -> std::string(&)[10];
- 使用decltype:std::string s[10]; decltype(s) &func3();
生成空指针的办法(一般用来初始化指针):
空指针也可以用if去判断:
int *p=nullptr; if(p);
// 就是false
加了const限定后:
const int *p; // 正确,可以不初始化,因为后续可以p=&a; int *const p1; // 错误,必须要初始化,后续不能p1=&a;了
指针也是一个对象,所以可以有指向指针的指针;但是引用本身不是一个对象,因此不能定义指向引用的指针,但是指针是对象,所以存在对指针的引用(即对指针取别名):int *p; int *&r = p;(这要从右往左读):
==留个void* 指针的坑==
void* 指针。后续要来说 (void *
是从C语言那里继承过来的,可以指向任何类型的对象。 而其他指针类型必须要与所指对象严格匹配。)
概念:结构体属于用户 ==自定义的输数据类型==,允许存储不同的数据类型
==聚合类==:聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式,当一个类满足一下条件时,它就是聚合的:
以上说的特殊初始化,就是下面的struct结构体的第二种初始方法: struct Student stu2 = { “李四”, 23, 148 };
在deskflow中,有看到一种匿名定义,没定义结构体的名字,直接在定义时实例化+初始化:
static const struct
{
int x;
int y;
const char *name;
} neighbourDirs[] = {
{1, 0, "right"},
{-1, 0, "left"},
{0, -1, "up"},
{0, 1, "down"},
};
语法:struct 结构体名 {结构体成员列表};
通过结构体创建变量的三种方式(先在上面创建出结构提了):
Ps:在创建实体变量时,可以省略掉关键词 struct;我也推荐第二种吧。
#include <iostream>
#include <string>
using namespace std;
// (1)定义结构体
struct Student {
// 成员列表,有点属性的味道
string name;
int age;
float score;
}; // 一定注意分号结尾
// (2)定义的同时搞几个变量:
struct Student {
int age;
float score;
} stu1, stu2;
struct Student stu3; // stu1、stu2、stu3这就一样,这就是声明的变量名
// (3)省去结构体的名字
struct {
int age;
float score;
} stu1, stu2;
// 这种方式就不能再向上面那种创建出stu3了
int main() {
// 第一种方式:
struct Student stu1; // 创建结构体变量时可以省略struct关键字
stu1.name = "张三";
stu1.age = 18;
stu1.score = 95.5;
// 第二种方式(推荐):
struct Student stu2 = { "李四", 23, 148 };
// 第三种方式是在定义结构体时就跟上结构体变量名,然后赋值 struct Student {成员列表} stu3;
// 然后就对stu3像第一种方式赋值
// 实体对象以 . 的形式去取值
cout << stu2.name << stu2.age << stu2.score << endl;
system("pause");
return 0;
}
作用:将自定义的结构体放到数组中方便维护(比如定义了一个名为学生的结构体,弄了很多学生,放到一个结构体数组)
struct 结构体名 数组名[元素个数];
// 可以这样初始化(一般放max,代表保存的上限)
步骤:
#include <iostream>
#include <string>
using namespace std;
// 1、定义结构体
struct Student {
string name;
short age;
float score;
};
int main() {
// 2、创建结构体数组
struct Student stuArray[] = {
{"张三", 29, 120.5},
{"李四", 20, 98},
{"王五", 19, 140}
};
// 可以赋值,也可以用这去改变原有的值
stuArray[1].name = "赵六";
// 遍历数组打印出来
int len = sizeof(stuArray) / sizeof(stuArray[0]);
for (int i = 0; i < len; i++) {
cout << "姓名:" << stuArray[i].name << "\t年龄:"
<< stuArray[i].age << "\t分数:" << stuArray[i].score << endl;
}
system("pause");
return 0;
}
作用:就是通过指针访问结构体中的成员
->
去获得结构体属性,而不再是 .
用的是8.2中定义的 结构体Student
// 生成一个结构体变量
struct Student stu1 = {"张三", 18, 100};
// 关键,定义的指针一定也是结构体的(同样,struct可省)
struct Student *p = &stu1;
cout << p->name << p->age << p->score << endl;
Ps:可以看下这个demo
作用:结构体中的成员可以是另外一个结构体(需要注意的是,这是被嵌套的结构体需要先被定义)。
例如:老师辅导学生,在一个老师的结构中,记录一个所带学生的结构体。
#include <iostream>
#include <string>
using namespace std;
struct Student { // 必须定义在Teacher之前
string name;
short age;
float score;
};
struct Teacher {
int id;
string name;
char genger;
struct Student stu; // 嵌套了学生的结构体变量
};
int main() {
// 第一种赋值方式 ***注意这里直接把被嵌套的结构一起放进去
struct Teacher t1 = { 5, "张三", 'm', {"李四", 18, 100} };
// 第二种赋值方式
struct Teacher t2;
t2.id = 3;
t2.stu.name = "王五";
cout << t1.stu.name << endl; // 李四
cout << t2.stu.name << endl; // 王五
// cout << t2.stu.age << endl; // 错误的,t2的学生没给age赋值
system("pause");
return 0;
}
t1、t2虽然都 .stu,但各是各的,互不影响!
#include <iostream>
#include <string>
using namespace std;
struct Student {
string name;
short age;
float score;
};
// 函数传参的定义也要跟进来的数据保持一致
void print1(struct Student a_stu) {
a_stu.age = 23;
cout << "值传递函数中:" << a_stu.age << a_stu.name << a_stu.score << endl;
}
void print2(struct Student *a_stu) {
a_stu->age = 33;
cout << "地址传递函数中:" << a_stu->age << a_stu->name << a_stu->score << endl;
}
int main() {
struct Student stu1 = { "张三", 18, 99.9 };
cout << "原始数据:" << stu1.age << stu1.name << stu1.score << endl;
// 值传递,在函数内改变age
print1(stu1);
cout << "值传递后:" << stu1.age << stu1.name << stu1.score << endl; // age不会变
// 地址传递,在函数里改变age
print2(&stu1);
cout << "地址传递后:" << stu1.age << stu1.name << stu1.score << endl; // age改变了
system("pause");
return 0;
}
作用:使用const来防止误操作数据;接着8.5看
函数参数传递有:值传递和地址传递
若不想改变本来的数据就用值传递,值传递相当于会拷贝一份数据,在拷贝的数据上做操作;而地址传递就是在原数据上做修改,由于不会拷贝,很节省很多的空间和运行速度(后面这个速度是我自己觉得的)。
所以在有很多数据,且一般只是读的时候,防止有误修改的操作,就用const修饰函数的参数,这样就只可读吧,不可以修改。
// 这个也可以加const修饰,但毫无意义
void print1(struct Student a_stu) {
a_stu.age = 23;
cout << "值传递函数中:" << a_stu.age << a_stu.name << a_stu.score << endl;
}
// const加在前面就好了(struct可省略)
void print2(const struct Student *a_stu) {
// a_stu->age = 33; // 有了const修饰,这行修改操作就是错的
cout << "地址传递函数中:" << a_stu->age << a_stu->name << a_stu->score << endl;
}
C++中对文件操作需要包含头文件#include <fstream>
文件类型分为两种:
操作文件的三大类(导入上面的头文件后,这三个类都可以用了):
一种输入的检查控制:
std::istream &operator>>(std::istream &is, Sales_data &item) {
double price;
is >> item.bookNo >> item.units_sold >> price;
if (is) // 检查输入是否成功(还是很有必要,做一个容错检查)
item.revenue = item.units_sold * price;
else
item = Sales_data(); // 输入失败时:对象被赋予默认的状态
return is;
}
注意:没有逐个检查每个读取操作,而是等到读取了所有数据后赶在使用这些数据前做一次性检查(注意第四行的写法)。
文件打开方式:
打开方式 | 解释 |
---|---|
std::ios::in | 为读文件 |
std::ios::out | 为写文件 |
std::ios::ate | 打开文件时,初始位置:文件尾 |
std::ios::app | 追加方式写文件 |
std::ios::trunc | 如果文件存在先删除,再创建 |
std::ios::binary | 二进制方式 |
Ps:文件打开方式可以配合使用,利用
|
操作符例如:用二进制方式写文件:
ios::out | ios::binary
追加方式写文件:
ios::app
,但尽量还是用ios::out | ios::app
(二者都可以)#include <fstream> // 这里的 std::fstream::ate 和 std::ios::ate 是一模一样的 std::fstream inOut(path, std::fstream::ate | std::fstream::in | std::fstream::out)
ios::trunc
:就可以用来做将文件内容全部清空的操作,直接ofstream ofs(“123.txt”, ios::trunc); ofs.close(); // 这里只能用ofstream;不能用fstream(这不会报错,但是txt里面数据清不掉)
写文件步骤如下:
- 导入头文件:
#include <fstream>
//- 创建流对象:
std::ofstream ofs;
// 写还可以用这个类std::fstream ofs;
- 打开文件:
ofs.open("要存文件路径", 打开方式);
- 写数据:
ofs << "写入的数据" << endl;
// 用左移运算符,换行号也可以这样写- 关闭文件:
ofs.close();
#include <fstream>
void test01() {
std::ofstream ofs;
ofs.open("test.txt", std::ios::out);
/*一般来说,是这两种组合方式
std::fstream ofs("test.txt", std::ios::out); // 这要指明打开方式为写
std::ofstream ofs("test.txt"); // 因为是 ofstream ,默认就是写
*/
if (!ofs) {
std::cerr << "Could not open plan output file" << std::endl;
assert(false);
}
ofs << "姓名:张三" << std::endl; // cout是向屏幕输出
ofs << "年龄:18" << std::endl;
ofs.close();
}
Ps:2、3步骤是可以组合成一步的,直接相当于在类实例化对象时用构造函数
std::ofstream ofs("要存的路径", 打开方式)
读文件与写文件步骤相似,但是读取方式相对于较多
- 导入头文件:
#include <fstream>
- 创建流对象:
std::istream ifs;
// 同样也可以用std::fstream ifs;
- 打开文件,并要判断是否打开成功:
ifs.open("文件路径", 打开方式)
- 读数据:四种读取方式,就用C++的第三种(在OpenGL的学子中出现了更好的做法)
- 关闭文件:
ifs.close();
#include <iostream>
#include <fstream>
#include <string>
void test01() {
std::ifstream ifs;
ifs.open("test.txt", std::ios::in);
/*一般来说,是这两种组合方式
std::fstream ifs("test.txt", std::ios::in); // 这要指明打开方式为读取
std::ifstream ifs("test.txt"); // 因为是 ifstream ,默认就是读取
*/
// 判断文件是否打开成功:创建的对象.is_open()
// if(!ifs) // 这一行与下一行是一个意思,要不要.is_open()都一样
if (!ifs.is_open()) { // 前面一个 `!` 是取反的操作
// 还看到这样的判断 (!ifs.good()),一个效果
std::cout << "文件打开失败" << std::endl;
return;
}
//// 第一种(中间空格会被当成分割符,看下面的9.1.4)
//// 初始化一个字符串(视频里说这是数组)
//char buff[1024] = { 0 }; // 1024是自己定的,好像不一定要初始化
//while (ifs >> buff) { // 这里读到尾了,就会返回假而退出
// std::cout << buff << std::endl;
//}
//ifs.close();
//// 第二种 (这就是9.4.3里面的`多字节操作`的例子代码)
//char buff[1024] = { 0 };
//// .getline()函数第一个参数要的是一指针,第二个要的是大小,虽然可以直接填1024,还是用函数获取吧
//while (ifs.getline(buff, sizeof(buff))) {
// std::cout << buff << std::endl;
//}
//ifs.close();
// 第三种 c++的string,前面都是c的风格
std::string buff;
while (std::getline(ifs, buff)) { // 这里的ifs对象,和cin就有点相似的味道了
std::cout << buff << std::endl;
}
ifs.close();
//// 第四种
//char c; // 这是一个个读的就慢很多
//while ((c = ifs.get()) != EOF) { // EOF:文件尾部的标志
// std::cout << c; // 这就不能加换行符了
//}
//ifs.close();
}
OpenGL原样读取数据,包括空格空行这样,得到的字符串和原样一模一样,强烈建议使用:
#include <iostream>
#include <sstream>
#include <fstream>
int main() {
std::string path = "E:\\VS_project\\Study\\LearnOpenGL\\3.3.shader.vs";
std::string text;
std::ifstream ifs;
ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try {
// 1、open file
ifs.open(path);
// 2、read file's buffer contents into streams
std::stringstream fileStream;
fileStream << ifs.rdbuf();
// 3、close file handlers (一定要关闭)
ifs.close();
// 4、convert stream into string
text = fileStream.str();
// 也可以转成c的字符串
const char* c_text = text.c_str();
std::cout << text << std::endl; // 和文本文件格式一模一样,空格都一样
std::cout << c_text << std::endl;
}
catch (std::ifstream::failure& e) {
std::cout << "ERROE:" << e.what() << std::endl;
}
}
文件不存在:ifs.is_open() 来判断
还有一种,直接使用 if (ifs) 来判断也行,只是上面会比较直观
文件存在但为空:
char buff;
ifs >> buff; // 读一个字符,使用.eof()函数,空的话就是true
if (ifs.eof()) {
std::cout << "文件是空的" << std::endl;
}
建议的直接写法:
void test01() {
std::ifstream ifs("record.csv", ios::in);
// 判断若是文件不存在
if (!ifs.is_open()) { // 注意取反
std::cout << "文件不存在!" << std::endl;
ifs.close();
return;
}
// 文件存在但为空
char ch;
ifs >> ch;
if (ifs.eof()) { // 为空就是直接读到末尾了
std::cout << "文件存在但为空!" << std::endl;
ifs.close();
return;
}
// 但要不为空,记得要把这个读取的字符放回去
ifs.putback(ch); // 一定要放火去,不然会缺第一个字符
std::string line;
// 注意这里直接的在按行读取
while (ifs >> line) {
//cout << line << endl;
// 这里假设是这样用逗号隔开的数据 10002,7.74375,10011,7.52375,10003,6.85 (注意这最后是没有逗号的)
// 由于最后没有逗号,下面的方法势必就会把最后一个数据遗漏,那就在后面加一个`,`
line += ",";
int start = 0;
int index = -1;
std::vector<string> v; // 用来放分割的string
std::string temp_str;
while (true) {
index = line.find(",", start);
if (index == -1) {
break;
}
// 这种就不会改变原来line对应的最原始的字符串
temp_str = line.substr(start, index - start);
v.push_back(temp_str);
start = index + 1;
}
for (int i = 0; i < v.size(); i++) {
std::cout << v[i] << ' ';
}
std::cout << std::endl;
}
}
}
int nums = 0; // 记录有多少行
int id;
string name;
int dept_Id;
std::fstream ifs;
ifs.open("123.txt", std::ios::in);
//文件每行就是这样的内容,按空格分开的
//while (ifs >> id >> name >> dept_Id) { // 可以的,或者
while (ifs >> id && ifs >> name && ifs >> dept_Id) {
nums += 1; // 读取一行就+1;读完了就会退出
}
ifs.close();
// ifs >> 也可看自己在OpenVMS的PR中的使用:https://github.com/cdcseacave/openMVS/pull/1026/commits/7dce247dc6357f35014bd52d9403f641ff60813f
以二进制的方式对文件进行读写操作,打开方式要指定为ios::binary
例如:用二进制方式写文件:ios::out | ios::binary
二进制方式写文件主要利用==流对象==调用成员函数==write==
函数原型:ostream& write(const char *buffer, int len);
// 注意是标准iostream中的ostream
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数
#include <iostream>
#include <fstream>
class Person {
public:
char m_Nmae[64]; // 视频说这尽量用C的字符串,不要用C++的string
int m_Age;
};
void test01() {
// 创建对象时就直接打开,调用构造函数(std::ios 和 std::ios_base 是一样的)
// 其实 ofstream 已经表明是输出了,就不需要std::ios::out,除非是std::fstream,就需要这样写
std::ofstream ofs("person.txt", std::ios::out | std::ios::binary);
Person person = {"张三", 18}; // 记得回去看这种初始化
// &person是可以给Person类型的指针,但是这个write函数要的类型是const char *,所以就要这样强转过去
ofs.write((const char *)&person, sizeof(person));
// c++还是用 static_cast<const char *>(&person) 来转换指针类型吧
// 这里这样居然就直接写进去了自定义数据类型
ofs.close();
}
二进制方式读文件主要利用==流对象==调用成员函数==read==
函数原型:std::istream& read(char *buff, int len);
// 注意是标准iostream中的istream
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节
#include <iostream>
#include <fstream>
class Person {
public:
char m_Nmae[64];
int m_Age;
};
void test01() {
std::ifstream ifs;
// 这是接着上面写那个二进制文件得到的"person.txt"
ifs.open("person.txt", std::ios::in | std::ios::binary);
if (!ifs.is_open()) {
std::cout << "文件打开失败" << std::endl;
return;
}
// 存的这个数据类型,就先搞一个对象出来,用于接收
Person person;
// 这里也是强转成char *指针类型,是read函数的强行要求;len长度就按照数据类型给
ifs.read((char *)&person, sizeof(Person)); // 不知道怎么写的时候,就先乱填一个,就会弹出提示
ifs.close();
std::cout << "姓名:" << person.m_Nmae << std::endl;
std::cout << "年龄:" << person.m_Age << std::endl;
}
标准库(这样前面都要加std),定义了一些IO类型:
头文件名称 | 类型(就是类名) |
---|---|
#include <iostream> | istream, wistream 从流读取数据 |
ostream, wostream 向流写入数据 | |
iostream, wiostream 读写流 | |
#include <fstream> | ifstream, wifstream 从文件读取数据 |
ofstream, wofstream 向文件写入数据 | |
fstream, wfstream 读写文件 | |
#include <sstream> | istringstream, wistringstream 从string读取数据 |
ostringstream, wostringstream | |
stringstream, wstringstream 读写string |
Tips:特别注意,因为是标准库定义的,==在使用这些类名的时候一定要加上std::
==。
类型 ifstream 和 istreingstream 都继承自 istream,因此可以像使用istream对象一样来使用ifstream和istringstream对象。例如可以对一个 ifstream 或 istringstream对象调用 getline, 也可以使用 » 从一个ifstream或istringstream对象中读取数据。同理 ofstream 和 ostringstream 类似。
IO对象无拷贝或赋值
std::ofstream out1, out2; out1 = out2; // 这是绝对错误的,不能对流对象赋值。
std::iostream::iostate; (iostream可以是上表的其它流类型)
若有一个流s:
注:前面几个多用于结合 if 判断,为真就是返回true。
endl、ends、flush
std::cout << "hi!" << std::endl; // 输出内容和换行,再刷新缓冲区
std::cout << "hi!" << std::ends; // 输出内容和一个空字符,然后刷新缓冲区
std::cout << "hi!" << std::flush; // 输出内容然后刷新缓冲区,不附加任何额外字符
如果想每次输出操作后都刷新缓冲区:
std::cout << std::unitbuf; // 所有输出操作后都会立即刷新缓冲区,即任何输出都立即刷新,无缓冲
std::cout << std::nounitbuf; // 回到正常的缓冲方式
标准库定义了一组==操纵符==来修改流的格式状态,一个操纵符是一个函数或是一个对象。已经使用过的一个操纵符——endl
,它输出一个换行符并刷新缓冲区。
下表是定义在iostream中的操纵符
*表示默认流状态 | (使用时记得加std::在前面) |
---|---|
std::boolalpha | 将true和false输出为字符串 |
std::noboolalpha * | 将true和false输出为 1 和 0 |
std::showbase | 对整形输出带有表示进制的前缀 |
std::noshowbase * | 不生成表示进制的前缀 |
std::showpoint | 对浮点值总是显式小数点 |
std::noshowpoint * | 只有当浮点值包含小数部分时才显式小数点 |
std::showpos | 对非负数显式+ |
std::noshowpos * | 对非负数不显示+ |
std::uppercase | 在十六进制中打印0X,科学计数法中打印E |
std::nouppercase * | 在十六进制中打印0x,科学计数法中打印e (就是大小写) |
std::dec * | 整型值显示为十进制 |
std::hex | 整型值显式为十六进制 |
std::oct | 整型值显式为八进制 |
std::left | 在值的右侧添加填充字符 |
std::right | 在值的左侧添加填充字符 |
std::internal | 在符号和值之间添加填充字符 |
std::fixed | 浮点值显示为定点十进制(即不使用科学计数法) |
std::scientific | 浮点值显示为科学计数法(可以推荐使用) |
std::hexfloat | 浮点值显示为十六进制(C++11新特性) |
std::defaultfloat | 重置浮点数格式为十进制(C++11新特性) |
std::unitbuf | 每次输出操作后都刷新缓冲区 |
std::nounitbuf * | 恢复正常的缓冲区刷新方式 |
std::skipws * | 输入运算符跳过空白符 |
std::noskipws | 输入运算符不跳过空白符 |
std::flush | 刷新ostream缓冲区 |
std::ends | 插入空字符,然后刷新ostream缓冲区 |
std::endl | 插入换行,然后刷新ostream缓冲区 |
下表是定义在iomanip中的操纵符
#include <iomanip> | 注意加std:: |
---|---|
std::setfill(a_char) | 用a_char填充空白 |
std::setprecision(n) | 将浮点精度设置为n |
std::setw(w) | 将读或写值的宽度设为w个字符 |
std::setbase(b) | 将蒸熟输出为b进制 |
注意:这些操纵符使用一般都是要跟在std::cout « 这样的后面,不会单独成一行拿出来。
==控制布尔值的输出格式==: 一但改变输出格式,后续的格式都会像这样改变,一定要谨记这个;有改变格式的,一般就会有对应的恢复到默认格式的成对操作:好比==std::boolalpha==和==std::noboolalpha==
std::cout << true << " " << false << std::endl; // 1 0 这是默认的
std::cout << std::boolalpha << true << " " << false << std::endl; // true false
std::cout << true << " " << false << std::endl; // 还是打印 true false
std::cout << std::noboolalpha; // 将输出格式恢复回去
std::cout << true << " " << false << std::endl; // 1 0 又都恢复回去
所以最好的使用建议是:
std::cout « std::boolalpha « true « std::noboolalpha; // 用完就改回来,仅对此条有用,不影响后续的cout格式
==指定整形值的不同进制==:
std::cout << "default,10进制: " << 20 << " " << 1024 << std::endl;
std::cout << "8进制,octal: " << std::oct << 20 << " " << 1024 << std::endl;
std::cout << 9 << std::endl; // 11 这里还是会用上面的8进制格式
std::cout << "16进制,hex: " << std::hex << 20 << " " << 1024 << std::endl;
std::cout << "10进制,decimal: " << std::dec << 20 << " " << 1024 << std::endl;
Tips:
以上代码打印时,却并没有指明哪里各种进制的前缀,并不能一眼看出来:
std::cout << std::showbase << std::uppercase << std::hex
<< "16进制:" << 20 << " " << 1024
<< std::nouppercase << std::noshowbase << std::dec << std::endl;
==控制浮点数输出格式==:(指定打印精度)
默认:==浮点值按六位数字精度打印==;如果浮点值没有小数部分,则不打印小数点;标准库会选择一种可读性更好的格式:非常大和非常小的值打印为科学记数法形式,其它值打印为定点十进制形式。
可以控制浮点数输出三种格式:
#include <iomanip>
中。方式一:(核心是==std::cout.precision(12);==)
#include <cmath>
std::cout << "当前精度:" << std::cout.precision() // 6 (默认的)
<< ", Value: " << std::sqrt(2.0) << std::endl; // 1.41421 (一共6个数字)
std::cout.precision(12); // 将精度设为12了
std::cout << std::sqrt(2.0) << std::endl; // 1.41421356237
int a = std::cout.precision(12); // 将精度设为12了 (可以有返回值,一般不用)
std::cout << a << std::endl; // 会返回旧精度 6
// 以及float转str时带精度
#include <sstream>
std::ostringstream out;
out.precision(12);
out << std::fixed << a_value; // std::fixed 代表用十进制
std::cout << out.str() << std::endl;
方式二:(核心是==std::cout « std::setprecision(3);==)(此操纵符在上面表中有)
#include <iomanip>
#include <cmath>
std::cout << std::setprecision(3); // 这里一定要这么写,操作符那种,不能只写std::setprecision(3);
std::cout << "当前精度:" << std::cout.precision() // 3
<< ", Value:" << std::sqrt(2.0) << std::endl;
注意:
#include <iomanip>
的;#include <cmath>
,不然在vs中可以,在linux下一定报错,所以以后凡是用到数学函数一定要加这个参数。==科学计数==:
std::cout << "科学计数法:" << std::scientific
<< 100 * std::sqrt(2.0) << std::defaultfloat << std::endl;
==打印小数点==:
std::cout << 10.0 << std::endl; // 只会打印10,不会打印小数点
std::cout << std::showpoint << 10.0 << std::noshowpoint << std::endl;
==输出补白==:(挺重要,就是把输出的格式对齐)(下面这些操纵符在上面表中有)
#include <iomanip> // 别忘了这个头文件
int i = -16;
double d = 3.14159;
// 补白第一列,使用输出中最小12个位置
std::cout << "i: " << std::setw(12) << i << "next col" << '\n'
<< "d: " << std::setw(12) << d << "next col" << '\n';
// 补白第一列,左对齐所有列
std::cout << std::left << "i: " << std::setw(12) << i << "next col" << '\n'
<< "d: " << std::setw(12) << d << "next col" << '\n'
<< std::right; // 别忘了恢复正常对齐
// 补白第一列,右对齐所有列 (默认也都是右对齐的)
std::cout << std::right << "i: " << std::setw(12) << i << "next col" << '\n'
<< "d: " << std::setw(12) << d << "next col" << '\n';
// 补白第一列,但补在域的内部
std::cout << std::internal << "i: " << std::setw(12) << i << "next col" << '\n'
<< "d: " << std::setw(12) << d << "next col" << '\n';
// 补白第一列,用 # 作为补白字符
std::cout << std::setfill('#') << "i: " << std::setw(12) << i << "next col" << '\n'
<< "d: " << std::setw(12) << d << "next col" << '\n'
<< std::setfill(' '); // 恢复正常的补白字符(千万别忘了这)
默认情况下,输入运算符会忽略空白符(空格符、制表符、换行符、换纸符和回车符)。
当输入是==a b c d==时,一般:
char ch;
while (std::cin >> ch)
std::cout << ch;
这样循环只会执行4次,会跳过中间的空格以及可能的制表符和换行符。输入就是==abcd==,是连在一起的。
然后这些空白符都是可以读取的:
std::cin >> std::noskipws; // 设置cin读取空白符(不但是cin,打开文件,读取的文件流也行)
while (std::cin >> ch)
std::cout << ch;
std::cin >> std::skipws; // 用完记得将cin恢复带默认状态,从而丢弃空白符
这样循环就就不止执行4次,所有的空白也会输出,输入是什么样,输出就是什么样子的。
前面的两节都是用的==格式化IO==操作,输入(»)运算符忽略空白符,输出(«)运算符应用补白、精度等规则。
标准库还提供了一组低层操作,支持==未格式化IO==,这些操作允许将一个流当做一个无解释的字节序列来处理。
==单字节操作==:
单字节低层IO操作 | 下面的is、os(std::istream、std::ostream)都是一个流 |
---|---|
is.get(ch) | 从istream is读取下一个字节存入字符ch中,返回is |
os.put(ch) | 将字符ch输出到ostream os,返回os |
is.get() | 将is的下一个字节作为int返回 |
is.putback(ch) | 将字符ch放回is,返回is |
is.unget() | 将is向后移动一个字节,返回is |
is.peek() | 将下一个字节作为int返回,但不从流中删除它 |
这些都是每次一个字节地处理流,他们会读取而不是忽略空白符,例如可以使用未格式化IO操作get和put来读取和写入一个字符:
char ch;
while (std::cin.get(ch))
std::cout.put(ch);
此程序保留输入中的空白符,其输入与输出完全相同,它的执行过程与前面使用std::noskipws的程序完全相同。
==将字符放回输入流==:
有时我们需要读取一个字符才能知道还未准备好处理它,这时,就希望将字符放回流中,标准库提供了三种方法:
一般情况下,在读取下一个值之前,标准库保证我们可以退回最多一个值。即,标准库不保证在中间不进行读取操作的情况下能连续调用putback或unget。
==从输入操作返回的int值==:
函数peek和无参的get版本都以int类型从输入流返回一个字符,这些函数返回int的原因:可以返回文件尾标记。
使用char范围中的每个值来表示一个真实字符,因此,取值范围中没有额外的值可以用来表示文件尾。
返回int的函数将他们要返回的字符先转换为unsigned char,然后再将结果提升到int。因此,即使字符集中有字符映射到负值,这些操作返回的int也是正值(前面类型转换讲过)。而标准库使用负值表示文件尾,这样就可以保证与任何合法字符的值都不同。==头文件cstdio定义了一个名为EOF的const,可以用它检测从get返回的值是否是文件尾:
int ch; // 使用一个int,而不是一个char来保存get()的返回值
// 循环读取并输出输入中的所有数据
while ((ch = std::cin.get()) != EOF)
std::cout.put(ch);
这与上面一个程序完成相同的工作,唯一不同的是用来读取输入的get版本不同。
==多字节操作==:例子可以看这里的第二种示例
一些未格式化IO操作一次处理大块数据,要考虑速度的话,下面这些操作就很重要,也容易出错,这些操作要求我们自己分配并管理用来保存和提取数据的字符数组。
多字节低层IO操作
注意:一般定义是 char sink[250]; 这样的方式,,然后delim一般可以不给,示例里有看到这while(ifs.getline(sink,250,’ ‘)),我不加最后一个参数,正常使用,加了后就一直在运行,有问题。
is.get(sink, size, delim) // 从is流中读取最对size个字节,并保存在字符数组中,字符数组的其实地址由sink给出。读取过程直至遇到了字符delim或读取了size个字节或文件尾时停止。如果遇到了delim,则将其留在输入流中,不读取出来存入sink
is.getline(sink, size, delim) // 与接收三个参数的get版本类似,但会读取并丢弃delim
is.read(sink, size) // 读取最多size个字节,存入字符数组sink中,返回is
is.gcount() // 返回上一个未格式化读取操作从is读取的字节数
os.write(source, size) // 将字符数组source中的size个字节写入os,返回os
is.ignore(size, delim) // 读取并忽略最多size个字符,包括delim。与其它未格式化函数不同,igbore有默认参数:size的默认值为1,delim的默认值为文件尾
#include <iostream> #include <string> int main() { std::ostream &os = std::cout; std::string name = "zhangsan"; os.put('g').put('\n'); os.write("hel", 3).put('\n').write(name.c_str(), name.size()); return 0; } // put、write这就是会直接在控制台打印,跟 << 作用一模一样
这例子就是:put输出一个字符g,再输出一个换行符;write写的是时候,string必须是c类型字符串,后面的长度尽量就给其字符串长度(可以少,代表输出前几个,大于字符串长度,可能会输出一些其它地址上存的东西)。。
get和getline函数接收相同的参数,他们的行为类似但不相同,在两个函数中,sink都是一个char数组,用来保存数据。两个函数都是 一直读取数据,直至下面条件之一发生:
两个函数的差别是处理分隔符的方式:get将分隔符留作istream中的下一个字符,而getline则读取并丢弃分隔符。然后无论哪个函数都不会将分隔符保存在sink中。
确定读取了多少个字符: 某些操作从输入读取未知个数的字节,可以调用gcount来确定最后一个未格式化输入操作读取了多少个字符。应该在任何后续未格式化输入操作之前调用gcount,特别是将字符退回流的单字符操作也属于是未格式化输入操作。如果在调用gcount之前调用了peek、unget或putback,则gcount的返回值为0。
书上的一个警告:一个常见的错误是本想从流中删除分隔符,但却忘了做。
书上的一个警告:一个常见的编程错误是将get或peek的返回值赋予了一个char而不是一个int。例如,在一台char被实现为unsigned char的机器上,下面的循环永远不会停止(这个不是那么理解,还是感觉怪怪的):
char ch;
while ((ch = std::cin.get()) != EOF)
std::cout.put(ch);
错误的:当get返回EOF时,此值会被转换为一个unsigned char,转换得到的值与EOF的int值不再相等(EOF上面讲到过,是系统定义的一个int值),因此循环永远也不会停止了。
在一台char被实现为signed char的机器上,就不能确定上面循环的行为,当一个越界的值被赋予一个signed变量时会发生什么完全取决于编译器。
各种流通常都支持对流中数据的随机访问,好比可以先读取最后一行,再读取第一行。标准库提供了一对函数,来定位(seek)到流中给定的位置,以及告诉(tell)我们当前位置。
注意: istream和ostream类型通常不支持随机访问(因为cout直接输出时,类似向回跳十个位置这种操作是没有意义的),所以下面讲的流随机访问只适用于fstream和sstream类型。
标准库定义了两对seek和tell函数,g版本用于输入流表示“获得”(读取)数据,而p版本用于输出流表示“放置”(写入)数据。
tellg() | 返回一个输入流中标记的当前位置 |
tellp() | 返回一个输出流中标记的当前位置 |
seekg(pos) | 在一个输入流中将标记重定位到给定的绝对地址 |
seekp(pos) | 输出流,其它同上。pos通常是前一个tellg或tellp返回的位置 |
seekg(offset, from) | 在一个==输入==流中将标记定位到from之前或之后offset个字符 - std::ifstream::beg - std::ifstream::cur - std::ifstream::end // 应该也可以std::ios::end或std::fstream::end |
seekp(offset, from) | ==输出==:from可以是下列值之一 - std::ofstream::beg,偏移量相对于流开始位置(看下面代码里的使用) - std::ofstream::cur,偏移量相对于流当前位置 - std::ofstream::end,偏移量相对于流结尾位置 |
注意:即使标准库对两种标记进行了区分,但它在一个流中值维护单一的标记,即并不存在独立的读标记和写标记。比如只读类ifstream流调用tellp,编译错会直接报错;若是fstream类型,它可以读写同一个流,有单一的缓冲区用于保存读写的数据,同样标记也只有一个,表示缓冲区的当前位置。标准库将g和p版本的读写位置都映射带这个单一的标记。由于只有单一的标记,因此只要我们在读写操作间切换,就必须进行seek操作来重定位标记。
==重定位标记==: 接着上表:==参数pos和offset的类型分别是pos_type和off_type==,这两个类型都是机器相关的,他们定义在头文件istream和ostream中。pos_type表示一个文件位置,而off_type表示距当前位置的一个偏移量。一个off_type类型的值可以是正的,也可以是负的,代表在文件中向前移动或向后移动。
==访问标记==: 函数tellg和tellp返回一个pos_type值,表示流的当前位置,tell函数通常用来记住一个位置,以便稍后再定位回来:
#include <sstream> // 下面这些类,一定要这个头文件
std::ostringstream writeStr; // 输出stringstream
std::ostringstream::pos_type mark = writeStr.tellp(); // 或者 std::streampos mark,,很多时候你可能会看到 int mark,
// ...,经过一系列操作
if (cancelEntry) // 这里是随便给的一个标志
writeStr.seekp(mark); // 回到刚才记住的位置
==Demo示例==:读写同一个文件(一个挺不错的例子) 假定已经给定了一个要读取的文件,我们要在此文件的尾行写入新的一行,这一行包含文件中每行的相对起始位置。如给定下面的文件(一定要有最后的空行):
abcd
efg
hi
g
程序修改后就是这样的:
abcd
efg
hi
g
5 9 12 14
注意,我们的程序不必输出第一行的偏移,因为它总是从位置0开始。统计偏移量时必须播包含每行末尾不可见的换行符。 下面程序时逐行读取文件,对每一行,将递增计数器,将刚刚读取的一行的长度加到计数器上,则此计数器即为下一行的其实地址:
#include <iostream>
#include <fstream>
int main(int argc, char*argv[]) {
static std::string path = "C:\\Users\\Administrator\\Desktop\\3月.txt";
std::fstream inOut(path, std::fstream::ate | std::fstream::in | std::fstream::out);
if (!inOut) {
std::cerr << "unable to open file!" << std::endl;
return EXIT_FAILURE;
}
std::fstream::pos_type end_mark = inOut.tellg(); // 记住原文件尾位置(因为是ate打开,就是在尾) (也经常这样打开,这样就直接获得了这个文件的大小len) 或者 std::streampos end_mark 或者 int end_mark,这三个类型是一个意思,都代表了这个文件的size,特别是这样读取文件时,在文件末尾打开,用tellg()获取到size,再seekg()到开始位置,,比如tensortrt的.engine文件反序列化时,要先知道整个.engine文件的大小,就是这样做的。
inOut.seekg(0, std::fstream::beg); // 重定位到文件开始,这里偏移量offset就设置的0
size_t cnt = 0; // 字节数累加器
std::string line; // 保存输入的每行
// 继续读取的条件:还未遇到错误&&还在读取原数据&&还可以获取一行输入
while (inOut && inOut.tellg() != end_mark && std::getline(inOut, line)) {
cnt += line.size() + 1; // +1表示换行符
auto mark = inOut.tellg(); // 记住读取位置
inOut.seekp(0, std::fstream::end); // 将写标记移动到文件尾
inOut << cnt; // 输出累计的长度
// 如果不是最后一行,打印一个分隔符
if (mark != end_mark) inOut << " ";
inOut.seekg(mark); // 恢复读位置
}
inOut.seekp(0, std::fstream::end); // 定位到文件尾
inOut << "\n"; // 在文件尾输出一个换行符
return 0;
}
解读:
unsigned const int hread_count = std::thread::hardware_concurrency();
// 获取当前机器的逻辑核心数。
这些都是在C++11后才有的,之前的多线程得借助操作系统的API。基本的东西:
#include <iostream>
#include <thread>
void thread_func() {
std::cout << "123456 hello fram threading" << std::endl;
std::cout << "123456 hello fram threading" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
void thread_args(int a, int b) {
std::cout << "hello fram thread_args " << a << " " << b << std::endl;
}
class MyCalss {
public:
void memberFunctionTask() {
std::cout << "hello fram Member function" << std::endl;
}
};
int main() {
// 1.1这里传入可调对象,函数指针、lambda表达式、匿名函数(仿函数)、bind创建的对象等
std::thread my_thread(thread_func); // 函数指针 (创建的这一刻就会执行了)
std::cout << "hello fram main thread" << std::endl;
// 1.2、成员函数指针(注意这种传入类中成员函数的写法)
// 更多是在写比如自定义的线程安全的队列这种class,clas内部,那就是 void t2(&Myclass:func, this) 类的内部,this指针代表着这个类的对象。
MyCalss obj; // 后面可以直接跟成员函数的参数,指针、值传递直接传,引用要 std::ref(argv1) 包一下
std::thread t1(&MyCalss::memberFunctionTask, &obj, argv1, argv2, ...); // 注意写法,传入的是对象指针
t1.join();
// 如果是类自己的方法,第二个参数传入的就是 this ,是ClientToolsTable类自己的实例对象
std::thread img_thread(&ClientToolsTable::get_image, this, std::ref(rtsp_path), 2);
// 2.1、传参数(值传递)(参数传递时直接把参数写后面就好了)
std::thread t2(thread_args, 5, 10);
t2.join();
// 2.2、 传参数(引用传递)(指针传递肯定也是OK的)
int x = 5;
int y = 10;
// 引用传递得用 std::ref() 包起来
std::thread t3(thread_args, std::ref(x), std::ref(y));
t3.join();
my_thread.join(); // 阻塞线程,直到线程执行完毕(它在创建那一刻就会执行了)(千万别忘了一定要join())
return 0;
}
C++中提供了多种线程同步机制,常用的方法包括:
互斥锁通常具有以下几个特性:
基本的互斥锁有四种,它们都是class类:在头文件 #include <mutex>
中:
注意事项:
要注意避免死锁:使用更高级的同步原语,如std::lock_guard或std::unique_lock ,他们可以自动管理锁的获取和释放;
异常时的安全:如果在锁定互斥量后抛出异常,那么必须确保互斥量被正确解锁。使用std::lock_guard或std::unique_lock可以自动处理这种情况,因为它们在析构时会释放锁。
考虑使用更高级的同步原语:除了std::mutex之外,C++标准库还提供了其他更高级的同步原语,如条件变量(std::condition_variable)、读写锁(std::shared_mutex)等,它们可以在特定场景下提供更高效的同步机制。
==std::mutex==
#include <iostream>
#include <thread>
#include <mutex> // 互斥锁需要的
//共享变量
int counter = 0;
std::mutex mymutex; // 实例化一个互斥锁对象
void increament_counter(int times) {
for (int i = 0; i < times; ++i) {
mymutex.lock(); // 访问临界变量前先加锁
counter++;
mymutex.unlock(); // 结束后一定要解锁
}
}
// 这里写std::lock_guard是想说它是异常安全的,能在发生异常时自动析构释放锁
std::mutex mtx; // 全局互斥量
void safe_function() {
std::lock_guard<std::mutex> lock(mtx); // 锁定互斥量
// 在这里执行需要互斥访问的代码
// 如果抛出异常,std::lock_guard 会在析构时自动解锁 mtx
try {
if () {
throw std::runtime_error("An error occurred!");
}
}
catch (const std::exception &e) {
// 处理异常,但不需要担心解锁,因为 std::lock_guard 会自动处理
std::cerr << "error: " << e.what() << std::endl;
}
}
int main() {
std::thread t1(increament_counter, 1000000);
std::thread t2(increament_counter, 1000000);
// 等待完成
t1.join();
t2.join();
std::cout << counter << std::endl;
// 假设这里有一线程调用了 saft_function()
// 由于使用了 std::lock_guard ,所以无论是否抛出异常, mtx都会被正确解锁
return 0;
}
==std::recursive_mutex== # 加锁与解锁的次数一定要对应起来,否则也会报错
#include <iostream>
#include <thread>
#include <mutex>
std::recursive_mutex mtx;
void recursive_fuc() {
mtx.lock(); // 第一次锁定
std::cout << "do something!" << std::endl;
// 递归锁定
mtx.lock(); // 同一个线程可以多次锁定,(如果是 std::mutx ,还没.unlock() 就又在.lock() 会直接报错)
std::cout << "do something!" << std::endl;
mtx.unlock(); // 解锁一次
std::cout << "do something!" << std::endl;
mtx.unlock(); // 再次解锁
std::cout << "do something!" << std::endl;
}
==std::timed_mutex==
#include <iostream>
#include <thread>
#include <mutex> // 互斥锁需要的
std::timed_mutex mtx;
void timed_lock_func() {
auto start = std::chrono::high_resolution_clock::now();
// 尝试在指定的时间内获取锁(成功的话就会自动加锁)
if (mtx.try_lock_for(std::chrono::seconds(2))) {
std::cout << std::this_thread::get_id() << " do something!" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
mtx.unlock();
std::cout << std::this_thread::get_id() << " do something!" << std::endl;
}
else{
std::cout << std::this_thread::get_id() << "do something other!" << std::endl;
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << "Thread " << std::this_thread::get_id() << "took time " << diff.count() << std::endl;
}
int main() {
std::thread t1(timed_lock_func);
std::thread t2(timed_lock_func);
t1.join(); // 这个一开始就拿到锁,整体就花了5秒;
t2.join(); // 尝试了2秒,拿不到锁,就走的else,整体就花了2秒;
return 0;
}
==std::recursive_timed_mutex==
它是一个==模板类==,用来接管mutx的对象,它是不可复制和不可移动的,确保了mutex的独占性,所以它的模板里,是禁用了拷贝构造函数和拷贝赋值运算符(就是=号的重载),语法层面,这俩成员函数都被声明成了 = delete;
使用RAII管理锁: 使用RAII(资源获取即初始化)原则来管理锁的生命周期,通过std::lock_guard或std::unique_lock来确保锁在不用时自动释放。(有点像智能指针的味道)
为什么需要std::lock_guard?
#include <iostream>
#include <thread>
#include <mutex> // 互斥锁需要的
#include <vector>
std::mutex mtx;
int mycout = 0;
/*
sum()函数中把std::lock_guard放在第一行,默认结束周期是函数执行完,那它会管函数里的所有代码,
如果把一些代码放在std::lock_guard之前,那之前的代码是不受锁的管理;
如果想提前结束std::lock_guard的生命周期,可以用一对花括号括起来
{
std::lock_guard<std::mutex> lock(mtx);
// do something!
} // 那这个锁的生命周期就只局限于这花括号中
int a = 123; // 这代码虽然在它锁的下面,但因为花括号,锁已经释放,这里就管不到了
*/
void sum() {
// 保护的是 std::mutex 类型的锁,实例对象是mtx;一般就是与这个类型的锁组合使用
std::lock_guard<std::mutex> lock(mtx);
for (size_t i = 0; i < 1000000; ++i) {
mycout++;
}
// 到这里超出生命周期,自动解锁,不需要去.unlock()
}
int main() {
// std::thread mythreads[10]; // 用数组的方式去声明也是OK的
std::vector<std::thread> mybox;
for (size_t i = 0; i < 9; ++i) {
// 如果sum函数有参数,那就直接跟后面 .emplace_back(sum, args1, args2)
mybox.emplace_back(sum); // 注意它这样直接把函数传进去,而且应该是传进去就在运算了
// mybox.emplace_back(std::thread(sum)); // 我肯定会这样写,视频直接像上面这样写乐,见识到了
}
for (std::thread &t : mybox) {
t.join();
}
std::cout << mycout << std::endl; // 代码很简单,家里电脑不加锁运算都是正确的
return 0;
}
std::lock_guard的缺点:
在简单场景下使用问题不大,复杂场景就差点意思,就是这些缺点,所以就引出了std::unique_lock。
它也是一个==模板类==,用来接管mutx的对象。
std::unqiue_lock的特定:
它常用的成员函数:
有4种构造传传参:
#include <mutex> // 互斥锁需要的
std::mutex mtx;
// 除了std::mutex是还可以接受 std::timed_mutex 这些类型的,这样第1种都还要接收第二个时间参数,例子看下面
std::unique_lock<std::mutex> lock1(mtx); // 1、就自动上锁(用起来跟std::lock_guard差不多)
std::unique_lock<std::mutex> lock2(mtx, std::defer_lock); // 2、延迟上锁,即不自动上锁,后续由程序员手动.lock()上锁
std::unique_lock<std::mutex> lock3(mtx, std::try_to_lock); // 3、尝试去上锁,锁上就返回true,没有就false
std::unique_lock<std::mutex> lock4(mtx, std::adopt_lock); // 4、接受一个已锁定的std::mutex (比如这里的 mtx之前已经执行过了.lock(),这里也能接收,相当于是来接管)
第一种:自动上锁
std::mutex mtx;
void example_defer_lock() {
// 就自动上锁(用起来跟std::lock_guard差不多)
std::unique_lock<std::mutex> lock(mtx);
std::cout << "Locked with defer_lock" << std::endl;
lock.unlock(); // 显示的解锁,这步不是必须的,有没有这行也是OK的,会自动解锁
}
第二种:延迟上锁,自己去手动锁定
std::mutex mtx;
void example_defer_lock() {
// 不会立即锁定,如果没有显示的调用 .lock(),等于没加锁
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
lock.lock();
std::cout << "Locked with defer_lock" << std::endl;
lock.unlock(); // 显示的解锁,这步不是必须的,没有这行也是OK的,会自动解锁
}
第三种:尝试去上锁
std::mutex mtx;
void example_try_to_lock() {
// 如果我这里来一个(写了这行的话,最后要记得加一个mtx.unlock() )
mtx.lock(); // 那下面的std::unique_lock永远是执行的else,因为这里拿到锁了,下面尝试拿锁一定拿不到
// 尝试锁定
std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
if (lock.owns_lock())
std::cout << "Locked with try_to_lock, 锁上了" << std::endl;
else {
std::cout << "Failed to lock with try_to_lock,没锁上" << std::endl;
}
mtx.unlock();
}
第四种:接受一个已锁定的std::mutex
std::mutex mtx;
void example_adopt_lock() {
mtx.lock(); // 先锁定互斥锁 (这句一定要的,因为下面的类型就是要接管已经锁定的互斥锁)
std::unique_lock<std::mutex> lock(mtx, std::adopt_lock); // 用这个类型来接管已锁定的互斥锁
std::cout << "Locked with adopt_lock" << std::endl;
// 因为std::unique_lock会智能管理解锁,所以下面这句要不要都无所谓
// mtx.unlock();
}
还有一些接受其它类型锁的:
基于(时间点)/(时间段)的超时锁定: (其实时间点、时间段都差不多)
std::timed_mutex mtx;
void example_time_lock() {
// 现在往后5s时间内去尝试获取锁,然后这段时间都会尝试,后面成功了,下面就能if判断
// 方式一:时间点
auto now = std::chrono::system_clock::now();
std::unique_lock<std::timed_mutex> lock(mtx, now + std::chrono::seconds(5));
// 方式二:时间段 (都是一个意思,可以说没差别)
std::unique_lock<std::timed_mutex> lock(mtx, std::chrono::seconds(5));
if (lock.owns_lock()) {
std::cout << "Locked with time lock" << std::endl; // 成功拿到锁就会走这
}
else {
std::cout << "没锁上" << std::endl;
}
}
std::condition_variable cdv; # 注意别叫cv,如果用了opencv,会与opencv的命令空间cv冲突
注意:条件变量必须与互斥锁一起使用,以确保在检查条件(while(!flag))和修改共享数据(flag=true;)时的线程安全。如果省略了互斥锁,可能导致数据竞争和其它并发问题。
==工作原理==:(非常重要,非常好用,opencv读取摄像头,我就用了,实现了类似python的.get()没数据时阻塞等待)
条件一:另一个线程调用了同一个条件变量的notify_one或notify_all方法,并且该线程是等待队列中的第一个线程(对于notify_one)或等待队列中的所有线程(对于notify_all)。
条件二:谓词函数 pred 返回true。或是共享变量满足(即while检查)
由“OpenCV_C++版.md”中多线程读相机并显示的实践总结出来的:
再由下面线程池中的终结出来:(非所有情况,主要还是看共享变量时什么样的)
注意事项:
#include <iostream>
#include <vector>
#include <thread>
#include <mutex> // 互斥锁需要的
#include <condition_variable> // 条件变量需要
std::mutex mtx;
std::condition_variable cdv; // 条件变量
bool flag = false; // 共享的变量,用于监测条件是不是成立
void myprint(int i) {
std::unique_lock<std::mutex> lock(mtx); // 这会自动拿到锁
// 方式一:
/*
while (!flag) { // 一些特殊场景下falg不要也是OK的,这非必须
cdv.wait(lock); // 一开始flag不成立,就进入阻塞状态,就会释放掉锁(所以lock一定要先拿到锁),等待别的线程唤醒
// 当别的线程把它唤醒了,它会立即重新去获得这个锁,然后再去判断条件
}
*/
// 方式二:(这跟方式一是一模一样的效果)(一些特殊场景下falg不要也是OK的,这非必须,相当于只等待通知,且只等一次)
cdv.wait(lock, []() { return flag; }); // 尽量就是用这第二种
// 下面接着做其它的逻辑
std::cout << std::this_thread::get_id() << " - " << i << std::endl;
}
// 用于去更新flag的线程,(当然也可以是主线程中while循环来改变共享变量flag的值,调用通知等)
void updateFlag() {
std::unique_lock<std::mutex> lock(mtx); // 只要涉及到读取共享变量,都要加锁
flag = true;
// 更新后要通知各个线程,相当于去唤醒等待的阻塞线程
// cdv.notify_one(); // 这是通知所有用了这同一个cv变量进行阻塞等待的线程
cdv.notify_one(); // 这调一次,通知一个线程,
cdv.notify_one(); // 调用两次,就通知两个线程(顺序大概率还是按创建顺序来的,我试简单的是按顺序,复杂的不太确定)
}
int main() {
std::vector<std::thread> mybox;
for (int i = 0; i < 10; ++i) {
// 注意这里是可以直接传函数指针,后面紧跟参数,可以不用去构建std::thread的匿名函数
mybox.emplace_back(myprint, i);
}
// 主线程中来更新一下flag的值
updateFlag();
// 可以发现下面打印出来的 i 都是乱序的
for (std::thread &t: mybox) {
t.join();
}
return 0;
}
个人理解:
条件变量除了上面的wait方法外,还有==wait_for==、==cv.wait_until==方法:
wait_for理论:(wait_until比较类似,自己去类推吧)
wait_for与wait方法类似,它允许指定一个超时时间,如果在这段时间内条件没有满足,并且没有收到唤醒信号,那么wait_for方法就会返回,并且此线程会重新获取互斥锁;
wait_for有两个重载版本:
版本一:(它其实是完全不管共享变量flag的,当然也可以用 while(!flag) 包起来)
接收两个参数,一个std::unique_lock类型的锁,一个std::chrono库中定义的任何时间单位的间隔时间
返回值是一个==std::cv_status==枚举值,表示等待操作的结果,一般这俩:
std::cv_status::no_timeout:表示等待没超时,即在指定时间收到了其它线程的唤醒通知(这是不管共享变量flag的)
std::cv_status::timeout:表示等待超时,即指定时间内没收到notify_all这样的通知,是不管共享变量flag的,哪怕它为true了
std::mutex mtx;
std::condition_variable cdv;
bool flag = false;
void my_watit_for() {
// 注意一定是用的 std::unique_lock 不能用std::lock_guard
std::unique_lock<std::mutex> lock(mtx);
if (cdv.wait_for(lock, std::chrono::seconds(3)) == std::cv_status::no_timeout) {
std::cout << "没超时" << std::endl;
}
else {
std::cout << "超时" << std::endl;
}
std::cout << "hello world!" << std::endl;
}
```
```
版本二:
接受3个参数,比版本一多一个谓词
返回值就是bool值:==必须要指定时间内同时收到其它线程的notify_all通知和谓词返回值为true,这的返回结果才会是true==,任意一个不成立都是返回false。
std::mutex mtx;
std::condition_variable cdv;
bool flag = false;
void my_watit_for() {
std::unique_lock<std::mutex> lock(mtx);
if (cdv.wait_for(lock, std::chrono::seconds(5), []() { return flag; })) {
std::cout << "没超时" << std::endl;
}
else {
std::cout << "超时" << std::endl;
}
std::cout << "hello world!" << std::endl;
}
void updateFlag() {
std::unique_lock<std::mutex> lock(mtx); // 只要涉及到读取共享变量,都要加锁
std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待1秒就通知
cdv.notify_all();
std::this_thread::sleep_for(std::chrono::seconds(2)); // 再等待2秒才改flag的值
flag = true;
}
int main() {
std::thread t1(my_watit_for);
std::thread t2(updateFlag);
t1.join();
t2.join();
return 0;
}
说明:
例子:让两个线程交替递增一个共享变量mycounter,并确保他们不会同时修改mycounter
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cdv;
bool flag = false;
int mycounter = 0;
void increase(int thread_id, int n) {
std::unique_lock<std::mutex> lock(mtx);
for (int i = 0; i < n; ++i) {
cdv.wait(lock, [=]() {return (thread_id == 1) ? flag : !flag; });
++mycounter;
flag = !flag;
std::cout << "thread " << thread_id << " " << i << std::endl;
cdv.notify_one(); // 用cdv.notify_all() 也是一样的
}
}
int main() {
int n = 100;
std::thread t1(increase, 0, n);
std::thread t2(increase, 1, n);
t1.join();
t2.join();
std::cout << mycounter << std::endl;
return 0;
}
说明:
std::unique_lock<std::mutex> lock(mtx);
放在了for循环内,我将其放在for循环之上也是OK的。std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
,然后在cdv.wait之前手动加锁lock.lock();再在flag = !flag;之后lock.unlock();也是可以的,从而看来这个锁比我想象的智能多了。 C++中的读写锁(也称为共享锁(std::shared_mutex、std::shared_lock)和独占锁(std::unique_lock))是一种同步机制,用于控制对共享资源的访问,允许多个线程同时读取资源,但在写入资源时只允许一个线程独占访问。在读多写少的情况下能显著提升性能。
==std::shared_mutex==是C++17引入的一种互斥锁,用于支持多读单写的并发访问模式,它允许多个线程同时拥有共享锁(读锁),但在持有独占锁(写锁)时,其它线程不能再持有任何类型的锁。 ==std::shared_lock==是C++17引入的一种锁的管理器,用于管理std::shared_mutex的共享锁,可以自动获取和释放共享锁,类似于std::mutex与std::unique_lock之间的关系
注意:
std::shared_mutex根据成员函数,是有两种功能的:
但为了方便区分,==一般不用std::sshared_mutex的排他性成员函数,而是用std::unique_lock或是std::lock_guard来管理这种需要排他性的操作。然后只用它的共享锁定==
它还有一些其它主要的成员函数:
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
std::shared_mutex rw_mutex; // 读写锁
int shared_data = 0; // 共享数据
// 读线程
void reader(int thread_n) {
std::shared_lock<std::shared_mutex> lock(rw_mutex); // 申请共享锁
std::cout << "Reader thread: " << thread_n << " reads value: " << shared_data << std::endl;
}
// 写线程
void writer(int thread_n, int value) {
// 注意这里适用std::unique_lock来管理读写锁的 独占锁
std::unique_lock<std::shared_mutex> lock(rw_mutex); // 申请独占锁
shared_data = value; // 修改共享数据
std::cout << "Writer thread: " << thread_n << " write value: " << shared_data << std::endl;
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
//threads.emplace_back(reader, i);
//threads.emplace_back(std::thread(reader, i));
threads.push_back(std::thread(reader, i)); // 三个都是一个意思
}
for (int i = 0; i < 2; ++i) {
threads.push_back(std::thread(writer, i, i+5));
}
for (auto &t : threads) {
// joinable方法主要判断是否可以使用join方法或者detach方法,可以返回true,不可以返回false
// 一个线程最多只能调用一次join或者detach,可以判一下,简单的问题不大,太复杂的代码判定一下可靠些
if (t.joinable()) {
t.join();
}
}
return 0;
}
原子变量:是指使用==std::atomic模板类==定义的变量,这些变量提供了对其所表示的值进行原子操作的能力。原子变量确保在多线程环境中,对变量的读写操作是线程安全的,即操作不会被其它线程中断或干扰。
还有一大特性就是原子变量不需要使用锁的机制,因此不会引起线程上下文切换,具有更高的性能。
g++ 01.cpp -lpthread
用了多线程记得加 -lpthread#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
std::atomic<int> atomicInt(0);
int myconuter = 0;
void add(int n) {
for (int i = 0; i < n; ++i) {
myconuter++; // 没加锁,因为寄存器这种会中断操作,最后的值肯定不对
atomicInt++; // 这是原子变量,中间的操作不能被打断,即便没加锁,所以最后值是对的
// 注意:atomicInt = atomicInt + 1 这不是原子操作,是不行的,原子操作都是通过成员函数或重载运算符实现的
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 9; ++i) {
threads.emplace_back(add, 10000);
}
for (auto &t : threads) {
t.join();
}
std::cout << myconuter << std::endl;
std::cout << atomicInt << std::endl;
return 0;
}
原子变量可以有的原子操作有:=、前置++(–)、后置++(–)、-=、+=、&=、 | =、^=这些操作符,主要是对这些操作符做了重载。 |
原子变量的原子成员函数:(可以用上面的原子变量atomicInt.出来)(下面的order参数都是内存顺序,可选)
bool is_lock_free() # 检查改原子操作是否是无锁的;
T load(memory_order order=memory_order_seq_cst) const # 原子的读取并返回当前值; (这样来读取原子变量的值)
void store(T desired, order=memory_order_seq_cst) # 原子的将值desired存储到对象中;
T exchange(T desired, order=memory_order_seq_cst) # 原子的将值desired存储到对象中,并返回之前的值; (这样来赋值改变原子变量的值,注意尽量都使用成员函数来实现原子操作)
bool compare_exchange_weak(T &expected, T desired, order=memory_order_seq_cst) # 如果当前值等于参数expected,则将其替换为参数desired,并返回true,否则返回false并将expected更新为当前值。 # 注意,这个函数还有别的重载版本,后面可以跟两个内存序参数(成功、失败的内存序)
bool compare_exchange_strong(T &expected, T desired, order=memory_order_seq_cst) # 比上个函数,有更强的保证,即她不会由于伪失败而返回false;
T fetch_add(T val, order=memory_order_seq_cst) # fetch_sub 就是对应加减
T fetch_and(T val, order=memory_order_seq_cst) # fetch_or fetch_xor 对应就是按位亦、按位或、按位亦或
std::atomic<int> a(5);
std::cout << a.load() << std::endl; // 5
a.exchange(6); // 这样用原子变量的成员函数
std::cout << a.load() << std::endl; // 6
上面的原子成员函数都有一个内存序的参数可以设置,那么:
为什么有内存序问题?原子操作的内存序问题,视频地址。
所以原子成员函数有一个参数可以来指定原子操作内存序: 内存顺序定义了多线程环境中操作的可见性和顺序规划,确保线程间的同步。原子操作可以指定不同的内存顺序,以控制操作的可见性和排序。 这个参数是一个枚举类型(std::memory_order),如:std::memory_order::memory_order_seq_cst
memory_order_relaxed:没有同步或顺序约束,仅保证原子性
memory_order_acquire:确保此操作之前的所有读操作不会被重排序到此操作之后
memory_order_release:确保此操作之后的所有写操作不会被重排序到此操作之前
memory_order_acq_rel:同时具备acquire和release的特性
memory_order_seq_cst:顺序一致性,保证所有线程的操作按顺序发生
一般默认都是用的最后一种,但记住,内存序不会影响原子操作的原子性。即使在最弱的std::memory_order_relaxed内存序下,操作仍然是原子的,不可中断。然而,内存序会影响操作的可见性和顺序。
std::counting_semaphore是C++20标注库的一个类模板,它实现了一个计数信号量。
信号量(Semaphore):是一种用于管理和协调多线程或是多进程访问共享资源的同步机制,它==通过计数器来控制对资源的访问数量==。
信号量的类型:
信号量的操作:(P操作、V操作都是原子操作)
因为要C++20,我这里没去整了,这代码放这里做个参考,不一定跑的起来。
简答的计数使用:
#include <iostream>
#include <thread>
#include <vector>
#include <semaphore>
/*
第一个1:类模板的非类型模板参数,指定了信号量的最大计数值,也就是可以同时允许多少个资源被访问;
那这个例子的<1>就是意思是最多只有一个线程可以访问受保护的资源;同理<5>就是可以有5个线程同时访问受保护的资源
第二个1:这是信号量的初始计数值,表示当前有多少资源可供访问,
那这个例子中,(1)表示信号量初始化是为1,意味着最开始有1个资源可供访问。
*/
std::counting_semaphore<1> sem(1); // 初始值为1的计数信号量
void worker(int id) {
sem.acquire(); // P操作,等待信号量大于0并将其减1(不满足会阻塞在这里)
std::cout << "max: " << sem.max() << std::endl;
std::cout << "Worker " << id << " is working" << std::endl;
std::this_thread::seleep_for(std::chrono::seconds(1));
sem.release(); // V操作,将信号量+1
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(worker, i);
}
for (auto &t : threads) {
t.join();
}
return 0;
}
std::counting_semaphore实现线程同步
#include <iostream>
#include <thread>
#include <vector>
#include <semaphore>
std::counting_semaphore<1> ready(0); // 初始计数值为0,用于控制work线程的执行
std::counting_semaphore<1> done(1); // 初始计数值为1,用于控制prepare线程的执行
void prepare() {
done.acquire(); // 减少信号量done的计数值,确保prepare在work完成(不满足会阻塞在这里)
std::cout << "Prepareing...\n";
std::this_thread::seleep_for(std::chrono::seconds(2));
std::cout << "Preparation done" << std::endl;
ready.release(); // 增加信号量ready的计数值,通知work线程可以开始工作
}
void work() {
ready.acquire();
std::cout << "Working...\n"; // 等待ready信号量,确保准备工作完成后再执行
std::this_thread::seleep_for(std::chrono::seconds(2));
std::cout << "Work done" << std::endl;
done.realse(); // 增加信号量done的计数值,通知prepare西城可以中心进入准备阶段
}
int main() {
std::thread t1(prepare);
std::thread t2(work);
t1.join();
t2.join();
std::cout << "All tasks completed." << std::endl;
return 0;
}
C++20才有的。
栅栏(barrier)对象是一种同步原语,用于协调多个线程的执行,使它们能够在某个特定的点(即栅栏)等待,直到所有线程都到达这一点,然后它们才能继续执行。栅栏可以确保并发任务在某些关键时刻同步,如果等待所有线程完成某个阶段的工作,然后再进入到下一阶段。
特点:
作用:
以下代码先放这,可能有问题,暂没有C++20的环境,没测试过:
#include <iostream>
#include <thread>
#include <vector>
#include <barrier> // 需要这个头文件
void worker(int id, std::barrier<> &sync_point) {
std::cout << "Worker " << id << " is doing phase 1 work" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100 * id)); // 模拟不同用时
// 到达栅栏,等待其它线程
sync_point.arrive_and_wait();
std::cout << "Worker " << id << " has completed phase 1 and is doing phase 2 work" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100 * id)); // 模拟不同用时
}
int main() {
const int mum_threads = 5;
std::barrier sync_point(num_threads); // 创建一个栅栏,等待所有线程到达
std::vector<std::thread> threads;
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(worker, i + 1, std::ref(sync_point));
}
for (auto &t : threads) {
t.join();
}
std::cout << "All workers have completed theire tasks" << std::endl;
return 0;
}
注:这下面的std::future对象在.get()拿不到值的时候阻塞,
然后基本都有成员函数:
注意:同一个future对象的.get()方法只能被调用一次,如果多次调用,将抛出 std::future_error 的异常。
一般多线程是拿不到函数的执行结果的,前面提到过,可以用传递引用的方式来拿到结果。或是这里最新的std::promise、std::future来获取结果。
注意:异常处理,当使用std::promise时,如果其 set_exception 方法没有被调用,但异步子线程中确实发生了异常,那么这些异常将不会被捕获,并可能导致程序崩溃。(好像调用 .set_exception 方法也会解开.get的阻塞,好像说是这样能把异常传递出来。)
简单的传递引用: 注:这是比较简单,直接把线程的.join()放在了主线程调用变量ret之前,这就相当于成了单线程了,更好的应该是用锁+条件变量来通知,同时确保线程安全(下面代码.join()放在16行打印后的话,task函数执行很久的话,那打印的结果肯定是初始值0)
#include <iostream>
#include <thread>
// 把结果存到 ret中,记得用引用
void task(int a, int b, int &ret) {
int ret_a = a * a;
int ret_b = b * 2;
ret = ret_a + ret_b;
}
int main() {
int ret = 0;
std::thread t(task, 2, 2, std::ref(ret)); // 前面提到了,传引用进去,要用 std::ref进行包装
t.join();
std::cout << "ret: " << ret << std::endl;
return 0;
}
更好的方式是std::promise、std::future,他们也是线程安全的(==可能日常使用就用这个吧,就应该够了==)
#include <iostream>
#include <thread>
#include <future> // 要这个头文件
void task(int a, int b, std::promise<int> &ret) {
int ret_a = a * a;
int ret_b = b * 2;
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时
ret.set_value(ret_a + ret_b); // 还有成员函数.set_value_at_thread_exit(T val)
// do other things
std::this_thread::sleep_for(std::chrono::seconds(2));
}
int main() {
std::promise<int> p;
std::future<int> f = p.get_future(); // 两个这样关联起来
std::thread t(task, 2, 2, std::ref(p));
// 在前面 .set_value() 调用后下面的 .get() 才能拿到值,且只能调用一次
std::cout << "ret: " << f.get() << std::endl;
t.join(); // .join()就放到后面去了
return 0;
}
解读:
再进阶:上面是子线程赋值,主线程获取,还可以主线程赋值,子线程获取 用途:创建子线程时,主线程还没算出要给子线程传递的值,就可以用这
#include <iostream>
#include <thread>
#include <future> // 要这个头文件
// b在创建线程时还不知道,就用std::future,刚好跟结果的std::promise反过来
void task(int a, std::future<int> &b, std::promise<int> &ret) {
int ret_a = a * a;
int ret_b = b.get() * 2;
std::this_thread::sleep_for(std::chrono::seconds(2));
ret.set_value(ret_a + ret_b); // 还有成员函数.set_value_at_thread_exit(T val)
// do other things
std::this_thread::sleep_for(std::chrono::seconds(2));
}
int main() {
// 结果
std::promise<int> p_ret;
std::future<int> f_ret = p_ret.get_future(); // 两个这样关联起来
// 传入的参数
std::promise<int> p_in;
std::future<int> f_in = p_in.get_future();
// 先创建线程,但此刻并不知道 f_in 传入的值
std::thread t(task, 2, std::ref(f_in), std::ref(p_ret));
std::this_thread::sleep_for(std::chrono::seconds(1)); // 假装一些操作
// 这里才知道传入的值
p_in.set_value(2);
std::cout << "ret: " << f_ret.get() << std::endl;
t.join();
return 0;
}
注:
std::future<int> &b
,它在里面调用了b.get(),一定记住一个future对象,只能调用一次.get()
再进阶:因为上一个程序存在的问题,假设我要用taks创建很多线程,那不是要创建很多个future对象,就很夸张 就可以用std::future对象f_in的.share()成员函数来创建一个可以复制的对象
// 但整体的代码没跑起来,所以只放了这个在这里做个了解吧
std::promise<int> p_in;
std::future<int> f_in = p_in.get_future();
std::shared_future<int> f_in_shared = f_in.share(); // 主要是这句(用f_in_shared来替代 f_in)
再说明一下不可复制
std::promise<int> p_ret;
std::promise<int> p_ret2 = p_ret; // 这是错的,不允许复制
std::promise<int> p_ret2 = std::move(p_ret); // 但可以用std::move来完成
10.3.1中的写法也是比较清楚明了的,但还有别的方式,如std::async、std::packaged_task。
==std::async== 这里面都没出现std::thread了,其实是做了一层封装 # 如果是比较简单的任务,就用std::async的方式二,直接完成异步子线程操作。
#include <iostream>
#include <vector>
#include <thread>
#include <future> // 还是要这个头文件
int task(int a, int b) {
int ret_a = a * a;
int ret_b = b * 2;
std::cout << "这个线程ID: " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
return ret_a + ret_b;
}
int main() {
std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
// 方式一:这里不会真正开启一个线程,而是由操作系统看具体情况(一般都会);
// 没第一个参数其实是等价于 std::async(std::launch::async | std::launch::deferred, task, 1, 2);
std::future<int> fu01 = std::async(task, 1, 2); // 基本跟方式二是等价的
std::cout << "return value is: " << fu01.get() << std::endl;
// 方式二(推荐):那要必须开启一个子线程,传递一个参数 std::launch::async
// 这会立即开启一个子线程,打印的ID都不一样,子线程的结果没出来的话,下面的 .get() 依然会阻塞
std::future<int> fu02 = std::async(std::launch::async, task, 1, 2);
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "return value is: " << fu02.get() << std::endl;
// 方式三:当传递 std::launch::deferred 时,则是延迟调用,当下面调用 fu02.get() 时才去执行task函数
// 这里两个ID打印出来都一样,说明这种就相当于是单线程运行了
std::future<int> fu03 = std::async(std::launch::deferred, task, 1, 2);
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "return value is: " << fu03.get() << std::endl;
return 0;
}
注:
==std::packaged_task==
这不会开启一个新的子线程,它只是起到一个包装的作用,要异步子线程,就用使用std::thread包一下。(下面线程池用到了这个,用来取线程执行结果)
#include <iostream>
#include <thread>
#include <future> // 还是要这个头文件
int task(int a, int b) {
int ret_a = a * a;
int ret_b = b * 2;
std::cout << "这个线程ID: " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
return ret_a + ret_b;
}
int main() {
std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
// 方式一:<int(int, int)> 代表的是 返回类型是int,传入了两个参数,类型也是int
// 这种都还是一个线程,且创建对象时,不能传递参数,仅仅是包装,包装后就是一个函数,只不过可以通过.get()得到函数执行结果
std::packaged_task<int(int, int)> t1(task);
t1(1, 2); // 这还是同步执行,要异步子线程执行,则是 std::thread(std::ref(t1), 1, 2)
std::cout << t1.get_future().get() << std::endl;
// 方式二:创建是就传递参数,用std::bind打包一下
// 虽然没有参数了,但一定要写成 int() 代表传递的是返回int值的函数,而不是一个Int值
std::packaged_task<int()> t2(std::bind(task, 1, 2));
t2();
// 结合上面的future看,返回值t2.get_future()得到的是std::future<int>对象,返回函数时也尽量返回future对象,不要直接去.get_future().get(),如果没计算完,.get()操作是会阻塞的
std::cout << t2.get_future().get() << std::endl;
return 0;
}
先写一个自定义类,把队列容器包装成一个线程安全的自定义类。
这里把生产者、消费者各写一个函数,写进了一个自定义类中,可以去做参考吧,我平时可能比较难用到,可能都是线程安全的队列比较多,所以这里不花时间了。
下面代码参考的这个教程。
手动通过线程安全的队列,实现线程池代码,注意一下几点:
代码:
SimpleThreadPool.hpp
#ifndef _SIMPLETHREADPOOL_
#define _SIMPLETHREADPOOL_ // 一定要加这,避免多次导入后,重复定义
#include <queue>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <future>
template<typename T>
class SafeQueue {
public:
SafeQueue() {}
~SafeQueue() {}
// 保证线程安全,所以拷贝构造函数、拷贝复制运算符重载、移动构造函数、移动赋值运算符重载都禁用了
SafeQueue(const SafeQueue &other) = delete;
SafeQueue(SafeQueue &&other) = delete;
SafeQueue(const SafeQueue &&other) = delete;
SafeQueue& operator=(const SafeQueue &other) = delete;
SafeQueue& operator=(const SafeQueue &&other) = delete;
bool empty() {
std::unique_lock<std::mutex> locker(this->m_Mutex);
return this->m_Queue.empty();
}
int size() {
std::unique_lock<std::mutex> locker(this->m_Mutex);
return this->m_Queue.size();
}
void push(const T &value) {
std::unique_lock<std::mutex> locker(this->m_Mutex);
this->m_Queue.emplace(value);
}
void push(T&& value) {
std::unique_lock<std::mutex> locker(this->m_Mutex);
this->m_Queue.emplace(std::move(value));
}
bool pop() {
std::unique_lock<std::mutex> locker(this->m_Mutex);
if (this->m_Queue.empty()) {
return false;
}
this->m_Queue.pop();
return true;
}
bool pop(T &value) {
std::unique_lock<std::mutex> locker(this->m_Mutex);
if (this->m_Queue.empty()) {
return false;
}
value = std::move(this->m_Queue.front());
this->m_Queue.pop();
return true;
}
private:
std::queue<T> m_Queue;
std::mutex m_Mutex;
};
// 简答任务队列,线程池
// 提交任务:普通函数,匿名函数(lambda函数)、仿函数(重载()运算符的类或结构体)、类成员函数、std::function/std::packaged_task、std::bind等
// 返回值不同,参数列表不同
class SimpleThreadPool {
public:
// 保证线程安全,所以拷贝构造函数、拷贝复制运算符重载、移动构造函数、移动赋值运算符重载都禁用了
SimpleThreadPool(const SimpleThreadPool&) = delete;
SimpleThreadPool(const SimpleThreadPool&&) = delete;
SimpleThreadPool& operator=(const SimpleThreadPool&) = delete;
SimpleThreadPool& operator=(const SimpleThreadPool&&) = delete;
SimpleThreadPool() : m_Threads(std::thread::hardware_concurrency()), m_RuningStatus(true) {
this->initialize();
}
SimpleThreadPool(int threadNum) : m_Threads(threadNum), m_RuningStatus(true) {
this->initialize();
}
// 包装成不带参数的调用对用,同时外部能得到它的返回值
// 可变模板参数
// 这里可以写后置返回类型(可以不要),几种写法(第一种应该是不会有C++版本限制的,其它的要注意版本)
// 1、:auto submitTask(Func &&func, Args... args) -> std::future<decltype(func(args...))> {}
// 2、:auto submitTask(Func &&func, Args... args) -> std::future<typename std::invoke_result<Func, Args...>::type> {}
// 3、auto submitTask(Func &&func, Args... args) -> std::future<std::invoke_result_t<Func, Args...>> {}
template<typename Func, typename... Args>
auto submitTask(Func &&func, Args... args) -> std::future<decltype(func(args...))> {
// 1、传进来的函数有返回值,要得到它的返回值的类型(std::invoke_result是在c++17才有的)
using returnType = typename std::invoke_result<Func, Args...>::type;
// using returnType = std::invoke_result_t<Func, Args...>; // 这两行是等价的,源码里也是用的using invoke_result_t 去得到的
/*
// C++11或者C++14可以用这行代码替代(但说是std::invoke_result是更为通用和强大的工具,因为它基于std::invoke机制,支持更广泛的调用类型。如果处理复杂的调用表达式,特别是涉及到成员函数或成员变量的调用,那么std::invoke_result是更好的选择。)
using returnType = typename std::result_of<Func(Args...)>::type;
// 除了上面的方法,还在github看到很多人这么写的,也是完全可运行的
using returnType = decltype(func(args...));
*/
// 2、将传进来的函数连通参数,包装成一个无参的函数(使用std::forward保证传递类型跟原来一模一样,不会发生变化)
// returnType是类型,后面的()代表是无参,像是void()、int(),这一起是std::function的模板参数,不是函数调用
std::function<returnType()> taskWrapper01 = std::bind(std::forward<Func>(func), std::forward<Args>(args)...);
// 3、这是为了拿到返回值,再打包了一次,用了std::packaged_task,需要头文件 <future>
// 这里用智能指针是为了下一步打包的传递
auto taskWrapper02 = std::make_shared<std::packaged_task<returnType()>>(taskWrapper01);
// 4、再打包一次,抹除返回值,因为任务队列需要的是没有返回值的函数,这里用lambda函数取去执行,又没写return,如果直接push taskWrapper02,那它执行是有返回值的,wrapperFunction执行是没返回值的
// 这里不能是值传递,用引用好像也不太行,下面的&i就不对,所以上面包装成指针,这里直接传递指针
// 这里写 TaskType wrapperFunction 也是样的,下面定义了 TaskType
// 不知这里为啥能用 TaskType ,其它地方lambda整的函数,只能用auto+后置返回类型这种(可能是这个是包装成函数,是没返回值的)
std::function<void()> wrapperFunction = [taskWrapper02]() {
(*taskWrapper02)(); // 这个函数执行虽然后返回值,但不处理,lambda又没写return,这里就包装成了无返回值了
};
this->m_TaskQueue.push(wrapperFunction);
this->m_CV.notify_one();
// 返回的类型是std::future<returnType> 也可以写作:std::fucture<decltype(func(args...))>
return taskWrapper02->get_future(); // 返回的是std::future的对象
}
/*
// 上面的 auto submitTask(Func &&func, Args... args) {} 函数可以指定后置返回类型,不是必须,放这里当学习
auto submitTask(Func &&func, Args... args) -> std::future<typename std::invoke_result<Func, Args...>::type> {}
*/
// 析构清理工作
~SimpleThreadPool() {
// 命令行运行才看得到这里,不然system("pause")就卡住了,它也要最后才自动析构,如果它是在main函数中运行的话。
std::cout << "析构开始运行" << std::endl;
this->m_RuningStatus = false;
this->m_CV.notify_all();
// 这种去拿去线程对象,必须是 & 引用
for (auto &t : m_Threads) {
// joinable方法主要判断是否可以使用join方法或者detach方法,可以返回true,不可以返回false
// 一个线程最多只能调用一次join或者detach,可以可以判一下
if (t.joinable()) {
t.join();
}
}
std::cout << "析构结束" << std::endl;
}
private:
// 初始化时,通过传参创建了12个线程,每个线程都在while (this->m_RuningStatus) {}中执行,当从队列中拿的一个任务完成后,它又循环去取,拿下一个任务,就保证线程在池中持续运行;实现线程池在整个程序生命周期中不断地处理任务队列中的任务
// 当任务队列为空后,这个线程就会卡在条件变量的.wait函数等待
// 所以最后析构的时候,把m_RuningStatus设为false,同时唤醒所有等待线程,往下运行后,也会退出while循环,就达到了退出
void initialize() {
for (size_t i = 0; i < m_Threads.size(); ++i) {
// 这是构建了一个lambda函数,没有return那种,然后放进vector中(必须是i,不能是&i,不然值不对)
auto worker = [this, i]() {
while (this->m_RuningStatus) {
TaskType task;
bool isSUccess = false;
// 下面用{},相当以可以圈定unique_lock能在这段代码运行完后,生命周期就结束了,就释放锁
{
std::unique_lock<std::mutex> locker(this->m_Mutex);
if (this->m_TaskQueue.empty()) {
this->m_CV.wait(locker);
}
isSUccess = this->m_TaskQueue.pop(task);
/*
// 注意:这里不能用.wait()接收两个参数来替代while循环。
// 如果替代后,所有代码都能如期正常运行,但是到最后析构的时候,m_CV.notify_all()会唤醒所有线程,
// 但后面的任务队列却永远为空了,这样就永远满足不了,就会阻塞在这,无法退出了。
this->m_CV.wait(locker, [this]() { return !this->m_TaskQueue.empty(); });
*/
}
if (isSUccess) {
std::cout << "Start running task in worker:[ID] " << i << std::endl;
task();
std::cout << "End running task in worker:[ID] " << i << std::endl;
}
}
};
// 这里创建时,子线程就在运行了,运行的就是上面的lambda函数,可一开始m_TaskQueue是空的,就会阻塞
this->m_Threads[i] = std::thread(worker);
}
}
private:
using TaskType = std::function<void()>; // <void()> 代表接受的函数返回值是void,()代表没有参数
// 这里我写成的单文件,以后用还是分文件写,并把这用智能指针类型替代
SafeQueue<TaskType> m_TaskQueue; // 这就是在实例化对象了,所以用指针,只是声明
std::vector<std::thread> m_Threads;
std::condition_variable m_CV;
std::mutex m_Mutex;
std::atomic<bool> m_RuningStatus;
};
#endif // _SIMPLETHREADPOOL_
main.cpp
#include <iostream>
#include "SimpleThreadPool.hpp"
int very_time_consuming_task(int a, int b, int id) {
//std::this_thread::sleep_for(std::chrono::seconds());
int temp = id % 2 ? id*100 : id*200; // 偶数,结果为0,不满足条件,就会暂停得久些
std::this_thread::sleep_for(std::chrono::milliseconds(temp));
return a + b;
}
int main(int argc, char** argv) {
SimpleThreadPool simpleThreadPool(12);
int taskNum = 30;
std::vector<std::future<int> > results(taskNum); // 这时是知道得到的future对象是int,就直接写
std::cout << "Start to submit tasks..." << std::endl;
for (rsize_t i = 0; i < taskNum; ++i) {
results[i] = simpleThreadPool.submitTask(very_time_consuming_task, i, i + 1, i);
}
std::cout << "End submit tasks...\n" << std::endl;
std::cout << "Main thread do something else..." << std::endl;
/*
// 模拟主线程做其它的事 (如果这里给的时间短,下面打印结果就很乱,因为一些子线程还没执行完)
std::this_thread::sleep_for(std::chrono::seconds(10));
// 主线程很快来到这,子线程没拿到值,.get()是会阻塞的
for (size_t i = 0; i < taskNum; ++i) {
std::cout << "result[" << i << "]: " << results[i].get() << std::endl;
}
*/
// 因为.get会阻塞,像上面代码那么写,如果第一个任务执行最久,那直接就在第一个.get卡住了,哪怕后面全部运行完了
// 那可以用.wait_for()去判定一下,先循环一次,把超时的线程的索引拿来出
std::vector<int> time_out; // 存超时的索引
std::future_status status;
for (size_t i = 0; i < taskNum; ++i) {
status = results[i].wait_for(std::chrono::milliseconds(100));
switch (status) {
case std::future_status::ready: // 没超时就拿出来做别的
std::cout << "result[" << i << "]: " << results[i].get() << std::endl;
break;
case std::future_status::timeout: // 超时就把索引记录下来
time_out.push_back(i);
break;
}
}
// 后面一直去轮训,直到记录超时的线程的索引的vector为空,就代表所有的子线程都完成了
std::vector<int> temp;
while (1) {
if (time_out.empty()) break;
temp.clear();
for (const auto &idx : time_out) {
status = results[idx].wait_for(std::chrono::milliseconds(100));
switch (status) {
// 如果得到的是std::future_status_defrred,则代表使用的std::launch::deferred策略,异步操作尚未启动
case std::future_status::ready:
std::cout << "result[" << idx << "]: " << results[idx].get() << std::endl;
break;
case std::future_status::timeout:
temp.push_back(idx);
break;
}
}
if (temp.empty()) break;
time_out.swap(temp);
}
std::cout << "End of getting results..." << std::endl;
system("pause");
return 0;
}