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;
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) {
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;
}
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; // 标记得起名尽量就全大写吧(跟变量名一样)
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; //这是错的,千万不要索引越界了,会有结果,但是错的离谱
// 这要在上面加入<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::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;
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; // 指针变量的申明;这是野指针,它还没有指向哪里
总结:空指针和野指针都不是我们申请的空间,因此不要访问。
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 };
语法: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;
}
//// 第一种
//// 初始化一个字符串(视频里说这是数组)
//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();
以二进制的方式对文件进行读写操作,打开方式要指定为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;
}
解读: