note

C++基础入门

一、C++初识

C++中也可以用 exit(0); 提前退出程序,中间的0只是一个标志

变量命名规范:

变量命名有许多约定俗成的规范,下面的这些规范能有效提高程序的可读性:

数据类型选择建议:

1.1 安装、注释

​ 安装visual studio 2017,选择默认的就好了

第一个C++项目: 新建一个空的c++项目,然后再源文件中填一个一个cpp文件:这都是固定写法

#include<iostream>
using namespace std;

int main() {
	cout << "helllo world" << endl;
	system("pause");
	return 0;
}

注释:

1.2 常量定义

变量:语法:数据类型 变量名 = 初始值;

常量:用于不可更改的数据

C++定义常量两种方式

#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 宏的编写技巧,简单说明一下:

1.3 关键字

关键字:关键字是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++得关键字,否则会产生歧义。

1.4 作用域

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++规定在创建一个变量或者常量时,必须要指定出相应的数据类型,否则无法给变量分配内存

2.1 sizeof关键字

可用sizeof求出数据类型占用内存大小,语法:sizeof(数据类型) 或者 sizeof(定义的变量名) cout << sizeof(a) << endl; cout << sizeof(long long) << endl;

2.2 整型

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也是要根据所在环境位数来决定的)。

2.3 实型(浮点型)

浮点型变量分类为两种:单精度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,也有讲到不同的初始化方法,以及使用列表初始化。

2.4 字符型

类型 含义 最小尺寸
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;
}

2.5 字符串型

新增用法,类似于python的r”“,:R"(这里面放字符串)"

字符串型用于显示==一串字符==,两种风格

数字转成字符串:==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值 字符 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 码大致由以下两部分组成:

2.6 布尔类型 bool

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)

2.7 转义字符

作用:用于表示一些==不能显示出来的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;
}

2.8 auto类型

​ 变量类型还有: 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 = &num;   // b是一个指向整数常量的指针(对常量对象取地址是一种底层const)

2.9 键盘数据的输入

作用:用于从键盘获取数据

关键字:==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.1 算数运算符

算术运算符包括以下符号:

运算符 术语 示例 结果
+ 正号 +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;
}

前置递增先对变量进行++,再计算表达式;后置递增则是先计算表达式,再对变量进行++

故:最终变量自己一定进行了++操作,只是有赋值的话,结果不一样

3.2 逻辑运算符

​ 在写if条件判断的时候就用这,不再是and、or

运算符 术语 示例 结果
! !a 如果a为假,则!a为真; 如果a为真,则!a为假。可以有!!a
&& a && b 如果a和b都为真,则结果为真,否则为假。
|| a || b 如果a和b有一个为真,则结果为真,二者都为假时,结果为假。

cout « (a && b) « endl; // 注意用这运算符时一定要括号,

3.3 位运算符

注意:以下都是按二进制的形式来。

看这个菜鸟教程&按位与、 ** 按位或 、 **^异或运算符、 ~取反运算符

在上面链接的最后的笔记里有讲»«,这里简单写下:(也可参考和这个笔记

四、流程结构

4.1 选择结构

​ 注意这种花括号结尾都是没分号的

(1) if语句

(2) 三目运算符

语法:表达式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

(3) switch语句

​ 执行多条件分支语句:每个case里都还是给上break,不然会一直执行下去,比如score给的9,那么就会直接执行case 9的代码,然后8、default;

int score = 0;
std::cin >> score;
switch (score) {
	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行别的就要括起来。

对比:与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()
{ 
    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;
}

4.2 循环结构

(1) while结构

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
}

(2)do…while结构

​ 语法:do {循环执行的语句} while (循环条件);

int num = 0;
do {
	std::cout << num << std::endl;
	num++;
} while (num <= 10);

总结:这与while最大的区别就是,这无论怎样都要先执行一次循环语句,再判断循环条件,而while循环可能直接进不去的。

最常用的做法还是do{…} while(0); :

​ 在c++中计算几次方:

#include <cmath>    // 要在上面导入这个头文件
int main() {
	int value = 0;
	int a = 4;
	value = pow(a, 3);    // 这就是a的3次方
}

(3)for循环语句

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

4.3 跳转语句

goto语句

语法:goto 自己定义的标记; //别忘了这个分号 如果标记的名称存在。执行到goto语句时,会跳转到标记的位置。

std::cout << "这是第1行代码" << std::endl;
cout << "这是第2行代码" << std::endl;
goto MYFLAG;     // 标记得起名尽量就全大写吧(跟变量名一样)
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);

5.1 一维数组定义方式

​ 我自己推荐第三种吧。

动态给数组长度

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;  //这是错的,千万不要索引越界了,会有结果,但是错的离谱
// 这要在上面加入<string>的头文件
string arr3[] = { "dasd", "asdas", "asda" };  // 后面给几个,前面会知道有几个的

​ Ps:上面这个是字符串的数组,只能使用string,不能使用C的风格,因为C风格定义字符串就是 char a[] = "hello"; 虽然可以像第三种定义数组的方式 char a[] = {"hello"} ,但是里面只能放一个值,多一个都要报错

(1) 数组名用途

练习:将数组反转:

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)才做

(2) 冒泡排序

/*
	冒泡排序:下面第一个是我自己写的(我的好像不大像冒泡,但实现了效果):
	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;
}

5.2 二维数组定义方式

​ 我自己推荐就使用第二种

(1) 数组名用途

5.3 数组打印

==遍历: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 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 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 int(&row)[4] : arr) { 
	for (int col : row) {
		std::cout << col << ' ';
	}
	std::cout << std::endl;
}

5.4 数组补充(书)

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表示函数没形参:

返回类型:函数返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。

6.1 函数的定义

​ 语法:

返回值类型 函数名 (参数列表) {
	函数体内执行的语句;
	
	return 表达式;    // 注意返回值的类型必须和函数名前一样
}

​ Ps:如果不需要返回值,定义函数时可以写 void 函数名() {} 然后可以省略掉return语句

​ 例:定义一个加法函数,实现两个数的相加

int add(int num1, int num2) {
	int sum = num1 + num2;
	return sum;
}

6.2 函数声明

作用:告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。

// 函数声明
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;
}

6.3 函数的分文件编写(.h、.cpp)

实例:

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

6.4 函数指针

函数指针是指向的函而非对象。

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::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开始记录的,一般用十六进制数字表示
- 可以利用指针变量保存地址

7.1 指针定义和使用

​ 定义语法:数据类型 *变量名;

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 存放的是数据;而指针变量存放的是==地址==。

7.2 指针所占内存空间

int *p;
cout << sizeof(int *) << endl;   // 放指针类型;4
cout << sizeof(p) << endl;       // 放指针;4
cout << sizeof(double *) << endl;  

7.3 空指针和野指针

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; // 指针变量的申明;这是野指针,它还没有指向哪里

总结:空指针和野指针都不是我们申请的空间,因此不要访问。

7.4 const修饰指针

const修饰指针有三种情况

7.5 用指针遍历数组

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个字节向后移,就能到了所有的地址
}

7.6 指针与函数(地址传递、值传递)

// 值传递
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;
}

7.7 数组通过指针到函数做处理

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

7.8 返回数组指针(书)

​ 因为数组不能被拷贝,所以函数不能返回数组;不过,函数可以返回数组的指针或引用。但是从语法上来说,要想定义一个返回数组的指针或引用比较繁琐,比较简单的处理办法是使用==类型别名==(这在c++关键字.md中类型别名写到过的)

那么 arrT* func(int i); // 函数func返回的就是一个指向含有10个整数的数组的指针

经典例子:

所以当一个函数要返回数组指针时,如果不使用类型别名,那就会是这么定义: int (*func(int i)) [10]; // 跟上面例子第三个加括号是一个意思

但在c++11新标准中还有一种==尾置返回类型==简化这声明方法,上面的就可以写成: auto func(int i) -> int(*)[10]; // auto、-> 这都是固定写法,不管引用还是指针,都是在->后面的括号里体现。

还有另外一中方法,使用decltype

int s[10];
decltype(s) *func();

练习:编写一个函数声明,使其返回数组的引用并且该数组包含10个string对象

  1. 最原始的声明:std::string (&func0())[10];
  2. 使用类型别名:
  3. using str10_1 = std::string[10]; str10_1 &func1();
  4. typedef std::string str10_2[10]; str10_2 &func1();
  5. 使用尾置返回类型:auto func2() -> std::string(&)[10];
  6. 使用decltype:std::string s[10]; decltype(s) &func3();

7.9 指针、引用后续的补充知识点(==书==)


生成空指针的办法(一般用来初始化指针):

空指针也可以用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 };

8.1 结构体定义和使用

语法: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;
}

8.2 结构体数组

​ 作用:将自定义的结构体放到数组中方便维护(比如定义了一个名为学生的结构体,弄了很多学生,放到一个结构体数组)

​ 语法: 可以不给元素个数(这你的示例跟本地的有出入,这里转换不通过) ​ 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.3 结构体指针

​ 作用:就是通过指针访问结构体中的成员

用的是8.2中定义的 结构体Student
// 生成一个结构体变量
struct Student stu1 = {"张三", 18, 100};

// 关键,定义的指针一定也是结构体的(同样,struct可省)
struct Student *p = &stu1;
cout << p->name << p->age << p->score << endl;

Ps:可以看下这个demo

8.4 结构体嵌套结构体

​ 作用:结构体中的成员可以是另外一个结构体(需要注意的是,这是被嵌套的结构体需要先被定义)。

​ 例如:老师辅导学生,在一个老师的结构中,记录一个所带学生的结构体。

#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,但各是各的,互不影响!

8.5 结构体做函数参数

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

8.6 结构体中const使用

​ 作用:使用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>

文件类型分为两种:

操作文件的三大类(导入上面的头文件后,这三个类都可以用了):

9.0 输入的检查控制(新增)

一种输入的检查控制:

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

​ 注意:没有逐个检查每个读取操作,而是等到读取了所有数据后赶在使用这些数据前做一次性检查(注意第四行的写法)。

9.1 文本文件

文件打开方式:

打开方式 解释
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里面数据清不掉)

9.1.1 写文件

写文件步骤如下:

  1. 导入头文件:#include <fstream> //
  2. 创建流对象:std::ofstream ofs; // 写还可以用这个类std::fstream ofs;
  3. 打开文件:ofs.open("要存文件路径", 打开方式);
  4. 写数据:ofs << "写入的数据" << endl; // 用左移运算符,换行号也可以这样写
  5. 关闭文件: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("要存的路径", 打开方式)

9.1.2 读文件

读文件与写文件步骤相似,但是读取方式相对于较多

  1. 导入头文件:#include <fstream>
  2. 创建流对象:std::istream ifs; // 同样也可以用 std::fstream ifs;
  3. 打开文件,并要判断是否打开成功:ifs.open("文件路径", 打开方式)
  4. 读数据:四种读取方式,就用C++的第三种(在OpenGL的学子中出现了更好的做法)
  5. 关闭文件: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;
	}
	//// 第一种
	//// 初始化一个字符串(视频里说这是数组)
	//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;
	}
}

9.1.3 判断文件是否为空

  1. 文件不存在:ifs.is_open() 来判断

    还有一种,直接使用 if (ifs) 来判断也行,只是上面会比较直观

  2. 文件存在但为空:

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

9.1.4 文件按行读取

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

9.2 二进制文件

​ 以二进制的方式对文件进行读写操作,打开方式要指定为ios::binary

例如:用二进制方式写文件:ios::out | ios::binary

9.2.1 写文件

二进制方式写文件主要利用==流对象==调用成员函数==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();
}

9.2.2 读文件

二进制方式读文件主要利用==流对象==调用成员函数==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;
}

9.3 标准库读书后补充

9.3.1 IO类

标准库(这样前面都要加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; // 这是绝对错误的,不能对流对象赋值。

9.3.2 条件状态

std::iostream::iostate; (iostream可以是上表的其它流类型)

若有一个流s:

注:前面几个多用于结合 if 判断,为真就是返回true。

9.3.3 刷新输出缓冲区

endl、ends、flush

std::cout << "hi!" << std::endl;   // 输出内容和换行,再刷新缓冲区
std::cout << "hi!" << std::ends;  // 输出内容和一个空字符,然后刷新缓冲区
std::cout << "hi!" << std::flush;  // 输出内容然后刷新缓冲区,不附加任何额外字符
unitbuf操纵符

如果想每次输出操作后都刷新缓冲区:

std::cout << std::unitbuf;   // 所有输出操作后都会立即刷新缓冲区,即任何输出都立即刷新,无缓冲
std::cout << std::nounitbuf;  // 回到正常的缓冲方式

9.4 书后补充:IO库再探

​ 标准库定义了一组==操纵符==来修改流的格式状态,一个操纵符是一个函数或是一个对象。已经使用过的一个操纵符——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 « 这样的后面,不会单独成一行拿出来。

9.4.1 格式化输出

==控制布尔值的输出格式==: 一但改变输出格式,后续的格式都会像这样改变,一定要谨记这个;有改变格式的,一般就会有对应的恢复到默认格式的成对操作:好比==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;

==控制浮点数输出格式==:(指定打印精度)

​ 默认:==浮点值按六位数字精度打印==;如果浮点值没有小数部分,则不打印小数点;标准库会选择一种可读性更好的格式:非常大和非常小的值打印为科学记数法形式,其它值打印为定点十进制形式。

可以控制浮点数输出三种格式:

  1. 以多高精度(多少个数字)打印浮点值;有2中控制方式
    1. 调用IO对象的precision成员:precision成员是重载的,一个版本接收一个int值,将精度设置此值,并返回旧精度值。另一个版本不接受参数,返回当前精度值。
    2. 使用==setprecision操纵符==来改变精度:setprecision操纵符接收一个参数,用来设置精度。 Note:操纵符 setprecision 和其它一些控制输出的操纵符都定义在头文件#include <iomanip>中。
  2. 数值是打印为十六进制、定点十进制还是科学计数法形式;
  3. 对于没有小数部分的浮点值是否打印小数点。

方式一:(核心是==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;

注意:


==科学计数==:

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(' ');          // 恢复正常的补白字符(千万别忘了这)

9.4.2 控制输入格式

默认情况下,输入运算符会忽略空白符(空格符、制表符、换行符、换纸符和回车符)。

当输入是==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次,所有的空白也会输出,输入是什么样,输出就是什么样子的。

9.4.3 未格式化的输入/输出操作

​ 前面的两节都是用的==格式化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,’ ‘)),我不加最后一个参数,正常使用,加了后就一直在运行,有问题。

​ 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变量时会发生什么完全取决于编译器。

9.4.4 流随机访问

​ 各种流通常都支持对流中数据的随机访问,好比可以先读取最后一行,再读取第一行。标准库提供了一对函数,来定位(seek)到流中给定的位置,以及告诉(tell)我们当前位置。

注意: istream和ostream类型通常不支持随机访问(因为cout直接输出时,类似向回跳十个位置这种操作是没有意义的),所以下面讲的流随机访问只适用于fstream和sstream类型。

seek和tell函数

​ 标准库定义了两对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;
}

解读: