diff --git a/C++/0 Primer c++.md b/C++/0 Primer c++.md new file mode 100644 index 0000000..ef288a5 --- /dev/null +++ b/C++/0 Primer c++.md @@ -0,0 +1,2319 @@ +# Primer c++第5版补充内容 + + + +c++的一个api查询[网址](https://cplusplus.com/)。(好像不是那么好用) + +Effective Modern C++:[地址](https://github.com/CnTransGroup/EffectiveModernCppChinese)。 + +传参实例化类对象时,用()、{}都是可以的,[这里](https://zhuanlan.zhihu.com/p/268894227)看区别。(Person是一个自定义类,接受一个string的传参)即 + +Person p_a(“张三”); +Person p_a{“张三”}; 这俩都是可以的,最近看到不少类实例化用的这个{} # 更新,看到一些使用{}这个方式实例化(这是列表初始化)的,很大概率这个类是一个stl的容器类别,或者这个自定义的类继承了一个类(这个类可能是标准容器类,也可能是一个自定义类继承了标准容器类) + + - 出现了,在tensorrt的教程中,`nvinfer1::Weights conWeights {nvinfer1::DataType::kFLOAT, nullptr, size};`只能用花括号初始化,改用大括号(),ide提示报错,没有匹配的构造函数。难道用()是用的构造函数,而{}不是用的构造函数? + + - OpenAI给出了解答: + Q:c++中,实例化类对象时,使用{}和()之间的区别? + A:{}是初始化列表,用来初始化类的成员变量;()是构造函数,用来初始化类的其他部分,比如调用其他函数,分配内存等。 + Q:那么{}初始化列表时怎么知道那个值对应哪个呢? + A:{}中的值会按照类中成员变量的声明顺序进行初始化,因此可以根据类中成员变量的声明顺序来确定{}中的值对应哪个成员变量。 + + 总结:我自己试了下,都还是要有参构造函数的实现,()、{}才不会报错,所以我实例化还是用(),然后还是知道有{}这么个初始化的方式(花括号就是列表初始化)。 + +## 一、lambda表达式 + +### 1.1 lambda定义 + +​ 目前为止:使用过的仅有的良好总可调用对象是函数和函数指针,另外还有其他两种可调用对象:重载了函数调用运算符的类(应该就是仿函数),以及==lambda表达式==。 + +​ 一个lambda表达式表示一个可调用的代码单元,可以将其理解为一个未命名的内联函数,一个lambda具有一个返回类型、一个参数列表和一个函数体,但与函数不同,lambda可能定义在函数内部。一个lambda表达式具有如下形式: + +> [capture list]\(parameter list) -> return type {function body;} + +- capture list:捕获列表,是一个lambda所在函数中定义的局部变量的列表(即当lambda定义在函数体内,要用函数体内的局部变量,就要把要用的局部变量放进这里,通常为空);当然==lambda可以直接使用局部static变量和它所在函数之外声明的名字==,而无需使用捕获列表; + +- parameter list:参数列表(是不能有默认参数的); + +- return type:返回类型; + +- function body:函数体; + +- 注意:lambda具体有两种写法,==一种==是单成一行赋值给一个对象,那么这种必须使用尾置返回来指定返回类型, + + - ```c++ + auto func = [](int a, int b) {return a + b; }; // 1、2行是一样的,可以不要返回类型,然后通过return推断 + auto func = [](int a, int b) -> int {return a + b; }; // 尾置返回来指定返回类型(可能以后会搜索后置返回类型) + // int func = [](int a, int b) -> int {return a + b; }; // 这就就是错的 + ``` + + - 注意看上面代码,这前面只能写auto,不能写具体的数据类型,如第3行直接就是错的。 + + - 这种也是定量定义在函数体内(至少在main函数中),==capture list中的参数不要使用定义在main函数外的全局变量==,会直接报错的。 + + - ==另外一种==是下面的示例,lambda直接写进std::for_each算法的参数位置当参数,for_each的是一个vector,vector中的元素是pair,那么(参数列表)里,给参数类型就要给pair,然后后面的操作就是对vector中的每一个元素,即pair进行操作。 + + ```c++ + std::vector> vec = { + {"zhangsan", 13}, {"lis", 14} }; + + std::for_each(vec.begin(), vec.end(), + [](std::pair p) {std::cout << p.first << " : " << p.second << std::endl; } + ); + ``` + +- 若是单成一行,最后结尾肯定要分号(就相当于日常每行完了有分号一样),若是直接写在参数位置就不需要分号了。 + +`下面这个特别重要:` + 我们==可以忽略参数列表和返回类型==,但==必须永远包含捕获列表和函数体==: + auto f = [] {return 42;} +以上我们定义了一个可调用对象f,它不接受参数,返回42。 +调用:std::cout << f() << std::endl; +在lambda中忽略括号和参数列表等价于指定一个空参数列表,上面的例子f的参数列表是空的,如果忽略返回类型,lambda根据函数体中的代码推断出返回类型。 + +​ 这是书上的Note:如果lambda的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void。 + +下面是一个实例使用(有带参数): + +```c++ +#include +#include +#include +#include // std::find_if需要这个头文件 +int main() { + std::vector vec{"over", "fox", "the", "quick", "red", "fox", "the", "turtle"}; + // 给定一个长度阈值 + std::vector::size_type sz = 4; + // 获取一个迭代器,指向第一个满足size() > sz 的元素 + auto wc = std::find_if(vec.begin(), vec.end(), + // 捕获列表[]里一定要有sz,lambda函数里才能用的 + //[sz](const std::string &a) -> bool {return a.size() > sz; } // 这两行一个效果 + [sz](const std::string &a) {return a.size() > sz; } // 不要返回类型也是可以的 + ); + + // 结合上面的wc,打印后面的单词,且每个后面接一个空格 + std::for_each(wc, vec.end(), + // 核心是这行,它是lambda参数,也是for_each算法的一个参数 + [](const std::string &s) {std::cout << s << " "; } + ); + system("pause"); + return 0; +} +``` + +- 核心是13、19行,是直接把lambda函数写在算法传参的位置。 + +### 1.2 lambda捕获和返回 + +- ==值捕获==(就就函数的值传递,会进行拷贝,你改变原值也无所谓) + +- ==引用捕获==(就是函数的引用传递,改变原值,会影响最终结果) + + ```c++ + int main() { + // 1.值捕获 + int a = 2; + auto func1 = [a](int b) {return a + b; }; + a = 10; + std::cout << func1(3) << std::endl; // 5 + + // 2.引用捕获 + int aa = 2; + // 主要就是这里,传内部变量时要用引用 + auto func2 = [&aa](int b) {return aa + b; }; + aa = 10; + std::cout << func2(3) << std::endl; // 13 + return 0; + } + ``` + + - ==[capture list]中的参数不要使用定义在main函数外的全局变量==,会直接报错的。 + - 当使用 ostream这种对象时,因为其是不能拷贝的,就要使用引用捕获(或指向其的指针)。 + - 书上建议:应该尽量减少捕获(值捕获)的数据量(即捕获列表尽量为空),来避免潜在的捕获导致的问题,而且,如果可能的话,应该避免捕获指针或引用。 + +- ==隐式捕获== + + 即捕获列表[capture list]除了直接给定外,还可以让编译器根据lambda中的代码来推断使用了哪些变量,为了指示编译器推断捕获列表,应在捕获列表中写一个=====或==&==,其中=代表采用值捕获,&则代表采用引用捕获,那么上面的代码就可以写成: + + ```c++ + int main() { + // 1.值捕获 + int a = 2; + // 这里指定=(值捕获),具体使用的参数a让编译器推断, + auto func1 = [=](int b) {return a + b; }; + a = 10; + std::cout << func1(3) << std::endl; // 5 + + // 2.引用捕获 + int aa = 2; + // 这里指定&(引用捕获),具体使用的参数aa让编译器推断, + auto func2 = [&](int b) {return aa + b; }; + aa = 10; + std::cout << func2(3) << std::endl; // 13 + return 0; + } + ``` + +- 对一部分值采用值捕获,对一部分值采用引用捕获,可以混合使用隐式捕获和显示捕获: + + ```c++ + auto wc = std::find_if(vec.begin(), vec.end(), + // & 必须在前 + [&, aa](const std::string &a) {return a + aa; } + ); + auto wc = std::find_if(vec.begin(), vec.end(), + // = 必须在前 + [=, &aa](const std::string &a) {return a + aa; } + ); + ``` + + - 核心代码是第2行和第5行,代码含义不重要,现在是假定以上代码是在一个函数体内,然后aa也是函数体内的一个变量; + - 当我们混合使用显式捕获和隐式捕获捕获时,==捕获列表的第一个元素必须是=或&==,此符号就指定了默认捕获方式为值或是引用: + - 当前面默认指定&时,部分想用值捕获的参数就直接写在后面,逗号分隔,前面不用加引号; + - 当前面默认指定=时,部分想用引用捕获的参数写在后面,且前面必须要有&符号指定。 + +### 1.3 可变lambda + +​ 默认情况下,对于一个值被拷贝的变量(值捕获),lambda不会改变其值,如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字==mutable==,如下: + +```c++ +int main() { + // 1.值捕获 + int a = 2; + // 这里用了关键字 mutable,里面让a进行了+1的操作 + auto func1 = [a](int b) mutable {return ++a + b; }; + a = 10; + std::cout << func1(3) << std::endl; // 6 + return 0; +} +``` + +- 默认lambda是不改变捕获列表里的变量的值,如果不加mutable,编译器会直接在++a报错说表达式必须是可修改的左值,加上mutable就可以修改了。 + +### 1.4 指定lambda返回类型 + +​ lambda只包含单一的return语句时,可以不指定其返回类型,编译器会自动推断,但如果一个lambda体包含return之外的任何语句,则编译器都会假定此lambda返回void,与其它返回void的函数类型,被推断返回void的lambda不能返回值。 +例子:使用标准库 transform 算法和一个lambda来讲一个序列中的每个负数替换为其绝对值: + +```c++ +std::transform(vec1.begin(), vec1.end(), vec2.begin(), [](int num) {return num < 0 ? -num : num;}); +``` + +- 以上这个例子我们就无须指定lambda的返回类型,因为可以根据条件运算符的类型推断出来。 + +但是如果将程序改写成看起来等价的if语句,就会产生编译错误(下面这是错误的): + +```c++ +std::transform(vec1.begin(), vec1.end(), vec2.begin(), [](int num) { if (num < 0) return -num; else return num; }); +``` + +- 分析:编译器推断这个版本的lambda返回类型为void(上面写过原因了),但它返回了一个int值。 + +修改:需要为lambda定义返回类型,且==必须使用尾置返回类型==(可能以后会搜索后置返回类型): + +```c++ +std::transform(vec1.begin(), vec1.end(), vec2.begin(), [](int num) -> int { if (num < 0) return -num; else return num; }); +``` + +--- + +一个有意思的例子(主要是看它的写法): + +​ 编写一个 `lambda`,捕获一个局部 `int` 变量,并递减变量值,直至它变为0。一旦变量变为0,再调用`lambda`应该不再递减变量。`lambda`应该返回一个`bool`值,指出捕获的变量是否为0。 + +```c++ +int main() { + int a = 5; + std::cout << !a << std::endl; // 0 + + auto func = [&a]() -> bool {return a == 0 ? true : !(a--); }; + while (!func()) { + std::cout << a << std::endl; + } + return 0; +} +``` + +### 1.5 参数绑定 + +​ 在一些如 std::find_if 算法时,第三个参数一般是一元谓词,但是每个数据作为一个参数默认进到这个一元谓词中,那么一元谓词中的长度(比如长度3)是写死了的,这里就没办法修改大于的长度;然而在用lambda表达式时,因为捕获列表的存在,是可以传进不止一个参数的,可以动态的指定长度sz,但如果函数体比较复杂,会多次复用时,使用lambda表达式就会比较复杂。 + +​ 参数绑定的意义:不使用lambda表达式也能传递给 std::find_if 这样算法的第三个参数(一元谓词)几个参数。 + +#### 1.5.1 标准库bind函数 + +​ 这就是用来解决上述问题的,它是定义在头文件#include 中,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。 + +调用bind的一般形式如下: + auto newCallable = std::bind(callable, arg_list); + +- 其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应传给定的callable的参数 + 即,当调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数; +- arg_list中的参数可能包含形如 \_n 的名字,其中 n 是一个整数,这些参数是“占位符”,表示newCallable的参数;他们占据了传递给newCallable的参数的“位置”,数值n表示生成的可调用对象中参数的位置:_ + - \_1 为newCallable的第一个参数,\_2为第二个参数,依次类推; + - _n都是定义在一个名为 placeholders 的命名空间,而这个命名空间本身定义在std命名空间;直接一句 ==using namespace std::placeholders;==,那么这些\_n就可以直接使用了,与std::bind函数一样,placeholders命名空间也定义在#include \ 头文件中。 + +```c++ +#include +#include +#include +#include +#include // std::bind 需要这个头文件 +// 这才是一元谓词 +bool compar(const std::string &s) { + return s.size() > 3; +}; +// 这里传递2个参数,不能直接用到 std::find_if中了 +bool check_size(const std::string &s, int sz) { + return s.size() > sz; +} +std::vector vec{ "over", "fox", "the", "quick", "red", "fox", "the", "turtle" }; + +int main() { + // 第一种:lambda表达式中的捕获列表,使得std::find_if的第三个参数位置(接收一元谓词)的地方可以传进两个参数 + int sz = 4; + auto iter = std::find_if(vec.begin(), vec.end(), [sz](const std::string &a) { return a.size() > sz; }); + std::cout << *iter << std::endl; // quick + + // 这里使用纯一元谓词,就会把长度sz写死成3 + auto iter_1 = std::find_if(vec.begin(), vec.end(), compar); + std::cout << *iter_1 << std::endl; //over + + + // 第二种:不使用lambda表达式,那么就要使用参数绑定 + auto check_size_6 = std::bind(check_size, std::placeholders::_1, 5); + /* + 此bind调用只有一个占位符,表示 check_size_6 只接受单一参数,占位符出现在arg_list的第1个位置, + 表示check_size_6的此参数对应 check_size 的第一个参数,此参数是一个const std::string&, + 因此调用 check_size_6会将此参数传递给 check_size + */ + auto iter_2 = std::find_if(vec.begin(), vec.end(), check_size_6); + std::cout << *iter_2 << std::endl; // turtle + + auto iter_3= std::find_if(vec.begin(), vec.end(), std::bind(check_size, std::placeholders::_1, 5)); + std::cout << *iter_3 << std::endl; // turtle + + return 0; +} +``` + +#### 1.5.2 bind的参数 + +​ 可以用std::bind绑定给定可调用对象中的参数或重新安排其顺序,例如,假定func是一个可调用对象,它有5个参数,则下面对bind的调用: + +​ // 假定 g 是一个有两个参数的可调用对象: +​ #include \ // 别忘了这个头文件 +​ using namespace std::placeholders +​ auto g = std::bind(func, a, b, _2, c, _1); + +那么我们在对g调用时 g(X, Y),那么它实际调用就会是 func(a, b, Y, c, X); + +#### 1.5.3 绑定引用参数(ref函数) + +​ 默认情况下,std::bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中,但是,与lambda类型,有时对有些绑定的参数我们希望以引用方式传递,或是要绑定的参数的类型是无法拷贝的(如输入输出流); + +例如,为了替换一个引用方式捕获ostream的lambda: + +// os是一个局部变量,引用一个输出流; +// c是一个局部变量,类型为char + +std::for_each(vec.begin(), vec.end(), [&os, c]\(const std::string &s) { os << s << c; }); // 第三个参数是lambda表达式 + +然后可以很容易的编写一个函数,完成相同的工作: + +std::ostream &print(std::ostream &os, const std::string &s, char c) { + return os << s << c; +} + +但是不能直接用std::bind来代替对os的捕获: + +// 下面这行是错的,原因:os是不能拷贝的 +std::for_each(vec.begin(), vec.end(), std::bind(print, os, std::placeholders::_1, 'a')); + +- 详解:bind拷贝其参数,然而ostream是不能拷贝的,如果我们希望传递给bind一个对象,而又不拷贝它,就必须使用`标准库ref函数`: + +std::for_each(vec.begin(), vec.end(), std::bind(print, std::ref(os), std::placeholders::_1, 'a')); // 这是对的,核心是 ref(os) + +- 函数ref返回一个对象,包含给定的引用,此对象是可以拷贝的,标准库中还有一个`cref函数`,生成一个保存const引用的类, + +与bind一样,函数ref和cref也定义在头文件 #include \中。 + +--- + +一个向后兼容的:参数绑定 + +​ 旧版本C++的绑定函数参数的语言特性显示更多,更复杂。标准库定义了两个分别名为bindlst和bind2nd的函数,类似bind,但是这些函数分别只能绑定第一个或第二个参数。 +​ 所以它们在新标准中已被弃用(deprecated),弃用的特性就是在新版本中不再被支持。 + +--- + +最终练习:统计长度小于4的单词的数量 + +```c++ +#include +#include +#include +#include +#include // std::bind std::placeholders都需要这个头文件 + +bool little(const std::string &s) { + return s.size() < 4; // 4直接写死 +} +bool my_bind(int n, const std::string &s) { + return s.size() < n; +} + +std::vector vec{ "over", "fox", "the", "quick", "red", "fox", "the", "turtle" }; + +int main() { + // 第一种:一元谓词,4直接写死 + auto num1 = std::count_if(vec.begin(), vec.end(), little); + std::cout << num1 << std::endl; + + // 第二种:lambda表达式 + int sz = 4; + auto num2 = std::count_if(vec.begin(), vec.end(), [sz](const std::string &s) -> bool { return s.size() < sz; }); + std::cout << num2 << std::endl; // 这里lambda表达式可以不要返回类型(->bool),编译器会自己推断, + + // 第三种:参数绑定 + // 注意这里参数写的顺序,要跟my_bind()函数顺序结合起来。且只用了std::placeholders::_1,当然也可以不另起一个对象名func,把表达式直接写进count_if的参数中 + auto func = std::bind(my_bind, 4, std::placeholders::_1); + auto num3 = std::count_if(vec.begin(), vec.end(), func); + std::cout << num3 << std::endl; + + return 0; +} +``` + +## 二、动态内存与智能指针 + +### 2.1 概念(重要) + +- 静态内存:用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量; +- 栈内存:用来保存定义在函数内的非static对象; +- 自由空间/堆:每个程序还有一个内存池,这部分内存被称作自由空间或堆,程序用堆 来存储==动态分配==的对象————即那些在程序运行时分配的对象。动态对象的生存期由自己程序来控制,也就是说,当动态对象不再使用时,我们的diamante必须显示地销魂它们。 + +注意:分配在静态或栈内存中的对象由编译器自动创建和销毁,对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。 + +--- + +有三种==智能指针==,都定义在`#include `头文件中: + +- shared_ptr:允许对个指针指向同一个对象; +- unique_ptr:“独占”所指向的对象; +- weak_ptr:伴随类,它是一种弱引用,指向shared_ptr所管理的对象。 + +--- + +注意智能指针陷阱,所以坚持一些基本规范: + +- 不使用相同的内置指针值初始化(或reset)多个智能指针; +- 不 delete get()返回的指针; +- 不使用get()初始化或reset另一个智能指针; +- 如果使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了; +- 如果使用指针指针管理的资源不是new分配的内存,记住传递给它一个删除器(不多写,如果有需要,在417页看看)。 + +--- + +尽量使用智能指针,使用new和delete管理动态内存常见的三个问题: + +1. 忘记delete内存,忘记释放动态内存会导致人们常说的==内存泄露==问题,因为这种内存永远不可能被归还给自由空间了,一般等到真正耗尽内存时,才能检测到这种错误。 +2. 使用已经释放掉的对象。 +3. 同一块内存释放两次。 + +故:==坚持只使用智能指针==,就可避免这些问题。对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它。 + +--- + +==引用计数==:一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。 + +auto r = std::make_shared\(42); // r指向的int只有一个引用者 +r = q; // 给r赋值,令它指向另一个地址;会递增q指向的对象的引用计数;递减r原来指向的对象的引用计数;这里r原来指向的对象已没有引用者,会自动释放。 + +--- + +​ 一个抛出错误的直接可用的代码:==throw std::out_of_range("this is a error");== + +​ 传统的对象构造方式是使用圆括号,新标准下,也可以使用列表初始化(即使用花括号),sting那里就有写到。 + +### 2.2 shared_ptr智能指针 + +shared_ptr和unique_ptr都支持的操作: + +- p->成员函数/属性 解引用*p +- p.get() // 返回p中所保存的指针,要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了。 + - 书上也有一句,不要使用get来初始化另一个智能指针或是为智能指针赋值 +- swap(p,q) // 交换p和q中的指针,也可以写作p.swap(q) + +#### 2.2.1 定义及基本使用 + +​ 定义:`std::shared_ptr a_ptr;` (记得去[其它](#2.2.6 其它(也挺重要))里面看,有相关的也挺重要) // 与vector一样,指定数据类型;默认初始指针中保存着一个空指针,常用: + +```c++ +// p1不为空,检查它是否指向一个空string, +std::shared_ptr p1; // 这是空指针,一定要去初始化 +if (p1 && p1->empty()) { // 如果p1的类型是int这种,就是没有empty()成员函数的。 + *p1 = "hi"; // 如果解引用指向一个空string,解引用p1,将一个新值赋予string +} +// 学习PCL时学到的,这件去简单初始化一个对象,还是要new关键字,类型后面带了括号当做匿名对象,当然不带()也是可以的,不带括号也是用的默认无参构造函数。 +pcl::PointCloud::Ptr cloud_in(new pcl::PointCloud()); +// 上面的Ptr是using Ptr = shared_ptr >; +``` + +shared_ptr独有的操作: + +- std::make_shared\ (args); // 返回一个shared_ptr,指向一个动态分配的类型为T的对象,并使用args初始化此对象。 + +- std::shared_ptr\ p(q); // p为shared_ptr指针q的拷贝,此操作会递增q中的计数器,q中的指针必须能转换为T* (这个T*在书4.11.2里有提及) + +- p = q; // p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数,若p的引用计数变为0,则将其管理的原内存释放。 + +- p.use_count() // 返回与p共享对象的智能指针数量,可能很慢,主要用于调试。 + +- p.unique() // p.use_count()为1,返回true,否则返回false。 + + - 还有一个reset来将一个新的指针赋予一个shared_ptr: + + ```c++ + std::shared_ptr p(new int(42)); // 引用计数为1 + // 重新赋值 + // p = new int(1024); // 这是错误的,不能隐式转换,下面讲到了的 + p.reset(new int(1024)); // 正确:p指向一个新对象 + ``` + + reset会更新引用计数,reset成员经常与unique一起使用,来控制多了shared_ptr共享的对象。在改变底层对象之前,我们检查自己是否是与当前对象仅有的用户,如果不是,在改变之前要制作一份新的拷贝(这块不是很理解): + + ```c++ + if (!p.unique()) + p.reset(new std::string(*p)); // 我们不是唯一用户;分配新的拷贝 + *p += newVal; // 现在我们知道自己是唯一的用户,可以改变对象的值。 + ``` + +注意:shared_ptr在引用计数变为0时,会销毁所管理的对象,还会自动释放相关联的内存;但如果将shared_ptr存放于一个容器中,而后不在需要全部元素,应确保使用erase删除那些不再需要的shared_ptr元素,不删没啥影响,就是会浪费内存。 + +#### 2.2.2 make_shared函数 + +​ 最安全的分配和使用动态内存的方法是调用一个名为`make_shared`的标准库函数,也是定义在头文件`#include `中: + +> - std::shared_ptr\ p1 = std::make_shared\(42); +> 特别注意:这里==p1就是一个指针==了 +> std::shared_ptr\> que = std::make_shared\>(); // 很重要 +> \# 一定要这样去初始化这个指针,不然que只是一个空指针,一定要实例化。 +> - std::shared_ptr\> que; // 这样是一个空指针,都没实例化,不能直接拿去 que->push()的 +> - std::shared_ptr\> que{}; // 这样虽然实例化了,但还是个空指针,也不能直接拿去用的 +> - std::shared_ptr\ p2 = std::make_shared\(10, 'a'); +> - std::shared_ptr\ p3 = std::make_shared\(); +> // p3指向一个值初始化的int,即,值为0 +> - auto p4 = std::make_shared\>(); +> // p4指向一个动态分配的空vector\ + +注意:new的动态分配一定要delete去释放,当然还是直接用智能指针方便 + +```c++ +#include +// 1.普通new一个vector的指针 +std::vector* alloc_vector() +{ + return new std::vector(); +} +// 2.使用智能指针(注意这个函数的返回类型写法与return写法,两者好好看) +std::shared_ptr> delivery() { + //return std::shared_ptr> (); // 错的 + return std::make_shared>(); // 一定要用make_shared来创建 +} +int main(int argc, char*argv[]) { + auto p1 = alloc_vector(); + delete p1; // 用完后一定手动回收 + /******************************************/ + std::shared_ptr> p2 = delivery(); + // 这个用完就不管,当引用计数为0时,会自动销毁对象并回收,就不怕忘记delete. +} +``` + +#### 2.2.3 内存泄露|定位new + +经典内存泄露,如下: + +```c++ +int *q = new int(42), *r = new int(100); +r = q; +auto q2 = std::make_shared(42), r2 = std::make_shared(100); +r2 = q2; +``` + +​ 解度:`r` 和 `q` 指向 42,==而之前 `r` 指向的 100 的内存空间并没有被释放==,因此会发生内存泄漏。`r2` 和 `q2` 都是智能指针,当对象空间不被引用的时候会自动释放。所以要用智能指针啊。 + +内存耗尽时的异常: + +- int *p1 = new int; // 如果内存耗尽,则会抛出std::bad_alloc的错误。 +- int *p2 = new (std::nothrow) int; // 如果分配失败,new返回一个空指针。 + 称这种形式的new为==`定位new`==,new表达式允许我们向new传递额外的参数,这里是由标准库定义的名为nothrow的对象,将其传递给new,告诉其不能抛出异常 + +==bad_alloc==、==nothrow==都定义在头文件`#include `中(vs中又是没导入也能用) + +#### 2.2.4 delete后重置指针值 + +​ delete后,指针虽已无效,但在很多机器上指针任然保存着(已经释放了的)动态内存的地址,在delete后,指针就变成了==空悬指针==,即指向一块曾经保存数据对象但现在已经无效的内存的指针 + +​ 未初始化指针的所有缺点(书2.3.2,49页)空悬指针也都有。有一种方法可以避免空悬指针的问题:在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果我们需要保留指针,可以==在delete之后将nullptr赋予指针==,这样就清楚地指出指针不指向任何对象。 + +​ 但这保护也只是有限的(如下,重置p对q没任何作用): + +``` +int *p(new int(42)); // p指向动态内存 +auto q = p; // p和q指向相同的内存 +delete p; // p和q均变为无效 +p = nullptr; // 重置,指出p不再绑定到任何对象 +``` + +#### 2.2.5 shared_ptr和new结合使用 + +如果不初始化一个智能指针,它就会被初始化为一个空指针,还可以用new返回的指针来初始化智能指针: + +```c++ +std::shared_ptr p1; // ok的 +std::shared_ptr p2(new int(1024)); // 正确:使用了直接初始化形式 +std::shared_ptr p3 = new int(1024); // 错误的(一定注意这是错的):必须使用直接初始化的形式 +``` + +解读:这是因为接收指针参数的智能指针构造函数是==explicit==的,因此,==不能将一个内置指针隐式转换成一个智能指针==,所以必须使用直接初始化形式。故: + +```c++ +std::shared_ptr clone1(int p) { return new int(p); } +// 错误,这也会隐式转换为std::shared_ptr,然而就像上面讲的这是不被允许的 +// 下面才是这个函数的正确方式 +std::shared_ptr clone2(int p) { return std::shared_ptr(new int(p)); } +// 虽然这可以,但是还是要尽量用 std::make_shared(p) 去初始化智能指针 +``` + +但是书上讲了,==不要混合使用普通指针和智能指针==,操作会很危险(pdf中直接输入413页查看); + +​ 简单来说,==智能指针和内置指针一起使用(应该是指如上内置指针赋予给智能指针)可能出现的问题,在表达式结束后,智能指针会被销毁,它所指向的对象也会释放,而此时内置指针依旧指向该内存空间(应该是因为内置指针始终是需要delete释放的),那么之后对内置指针的操作可能会引发错误==。 + +​ 下面就是错误的示例: + +```c++ +auto sp = std::make_shared(); +auto p = sp.get(); +delete p; +``` + +​ 智能指针 sp 所指向空间已经被释放,再对 sp 进行操作会出现错误。 + +#### 2.2.6 其它(也挺重要) + +定义和改变std::shared_ptr的其它方法: + +- std::shared_ptr\p(q); // p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为T*类型; + +- std::shared_ptr\p(u); // p从unique_str u那里接管了对象的所有权,并将u置为空 + +- `std::shared_ptrp(q, d);` // p接管了==内置指针q==所指向的对象的所有权,q必须能转换为T*类型。p将使用可调用对象d(lambda对象或是函数对象这种吧)来代替delete,例如: + + ```c++ + // v是一个int的vector + std::shared_ptr p(new int[v.size()], [](int *p) {delete[] p; }); + ``` + + 很多时候使用new创建指针时,若出现异常,那么p指向的内容就不会被释放,就会造成内存泄露,一般常用的两种处理方式就是:一、使用智能指针;二、不使用指针,使用struct对象,将new的构建放到构造函数中,将delete操作放到析构函数中。 + +- std::shared_ptr\p(p2, d); // p是std::shared_ptr p2的拷贝(这就是和上一个的差异),但和传拷贝的差异是这个p将用可调对象d来代替delete + +- 若p是唯一指向其对象的 std::shared_ptr,, + - p.reset(); // reset会释放此对象 + - p.reset(q); // 若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空 + - p.reset(q, d); // 若还传递了参数d,将会调用d而不是delete来释放q。(d一般是函数、lambda等可调用对象,且默认会把q这个指针作为参数传递给d这可调用对象) + +下面是一个示例,针对std::shared_ptr\p(q, d);这种手动去写的一个可调用对象d,第32行: + +```c++ +#include +#include +#include +// 1.一个连接类 +struct connection { + std::string ip; + int port; + connection(std::string a_ip, int a_port) : ip(a_ip), port(a_port) {} +}; +// 2.一个当做目标服务器的类 +struct destination { + std::string ip; + int port; + destination(std::string a_ip, int a_port) { + ip = a_ip; + port = a_port; + } +}; +// 3.连接函数 +connection func_connect(destination *pDest) { + std::shared_ptr pConn(new connection(pDest->ip, pDest->port)); + std::cout << "creating connection(" << pConn.use_count() << ")" << std::endl; + return *pConn; +} +// 4.结束释放函数 +void end_connection(connection *pConn) { + std::cout << "connection close(" << pConn->ip << ":" << pConn->port << ")" << std::endl; +} +// 5.main函数中执行的函数 +void f(destination &d) { + connection conn = func_connect(&d); + //std::shared_ptr p(&conn, end_connection); // 注意这行;或者使用下面的lambda表达式,两行效果一样 + std::shared_ptr p(&conn, [](connection *a_con) {std::cout << "connection close(" << a_con->ip << ":" << a_con->port << ")" << std::endl; }); + std::cout << "connecting now(" << p.use_count() << ")" << std::endl; + // 注意这,当这行执行完了,p要智能释放了,才会去调用end_connection();打印出来就知道顺序了 +} + +int main(int argc, char*argv[]) { + destination dest("192.168.108.147", 10086); + f(dest); + return 0; +} +``` + +解释:std::shared_ptr为什么没有==release==成员? + +​ 答:release成员的作用是放弃控制权并返回指针,因为在某一时刻只能有一个std::unique_ptr指向某个对象,unique_ptr不能被赋值,所以要用release成员将一个unique_ptr的指针的所有权传递给另外一个unique_ptr。而shared_ptr允许有多个shared_ptr指向同一个对象,因此不需要release成员。 + +### 2.3 unique_ptr智能指针 + +​ 当定义一个unique_ptr时,需要将其绑定到一个new返回的指针上,类似于shared_ptr,初始化unique_ptr必须采用直接初始化形式: + +- std::unique_ptr\ p1; // 可以指向一个double的unique_ptr +- std::unique_ptr\ p2(new int(42)); // p2指向一个值为42的int +- 它也有前面类似于std::make_shared的创建指针的使用: + auto sph2 = std::make_unique\<类型>(Vector3f(0.5, -0.5, -8), 1.5); + + +​ 由于一个unqiue_ptr拥有它指向的对象,因此unique_ptr不支持普通拷贝或赋值操作(都是针对同为uniqie_ptr的): + +> std::unique_ptr\ p1(new std::string("hello")); +> std::unique_ptr\ p2(p1); // 错误的,unique_ptr不支持拷贝 +> std::unique_ptr\ p3; +> p3 = p1; // 错误的,unique_ptr不支持赋值 +> +> 像是这种就是ok的: +> int *pi2 = new int(2048); +> std::unique_ptr\ p(pi2); // 但是可能会使得==pi2==成为空悬指针(应该是p释放了,pi2就空悬了) +> +> --- +> +> 虽然不能拷贝或赋值unique_ptr,但可通过调用release或reset将指针的所有权从一个(非const)ubique_ptr转移给另一个unique_ptr: +> +> 如将所有权从p1转移给p2: +> std::unique_ptr\ p1(new std::string("hello")); +> std::unique_ptr\ p2(p1.release()); // release将p1置为空 +> +> 将所有权从p3转移给p2: +> std::unique_ptr\ p3(new std::string("Trex")); +> p2.reset(p3.release()); // reset释放了p2原来指向的内存。 +> +> 说明: +> +> - release成员返回unique_ptr当前保存的指针并将其置为空,因此,p2被初始化为p1原来保存的指针,而p1被置为空; +> - reset成员接收一个可选的指针参数,令unique_ptr重新指向给定的指针,如果unique_ptr不为空,它原来指向的对象被释放,因此:对p2调用reset释放了用“hello”初始化的string所使用的的内存,将p3对指针的所有权转移给p2,并将p3置为空。 +> - 重要:调用release()会切断unique_ptr和它原来管理的对象间的练习,release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值,所以如果不用另一个智能指针来保存release返回的指针,就要手动负责资源的释放: +> p2.release(); // 错误的,p2不会释放内存,而且会丢失了指针 +> autp p = p2.release(); // 正确,但必须记得delete(p); + +下面是unqiue_ptr特有的操作: + +- std::unique_ptr\ u1; // 空unique_ptr,u1会使用delete来释放指针 + std::unique_ptr\ u2; // 空unique_ptr,u2会使用一个类型为D的可调用对象来释放指针 +- std::unique_ptr\ u(d); // 空unique_ptr,用类型为D的对象d代替delete +- u = nullptr; // 释放u指向的对象,将u置为空 +- u.release(); // u放弃对指针的控制权,==返回指针==,并将u置为空(重要) +- u.reset(); // 释放u指向的对象 + u.reset(q); // 如果提供了内置指针q,令u指向这个对象;否则将u置为空 + u.reset(nullptr); + +注意:不能拷贝的unique_ptr的规则有一个例外,可以拷贝或赋值一个将要被销魂的unique_ptr,最常见的是从函数返回一个unique_ptr: + +```c++ +std::unique_ptr my_clone(int p) { + return std::unique_ptr(new int(p)); +} +``` + +还可以返回一个局部对象的拷贝: + +```c++ +std::unique_ptr my_clone(int p) { + std::unique_ptr ret(new int(p)); + return ret; +} +``` + +这两段代码,编译器都知道要返回的对象将要被销毁,在此情况下,编译器执行一种特殊的“拷贝”,后续补充 + +​ 还有一个向后兼容: 标准库较早版本包含了一个名为==std::auto_ptr==的类,它具有unique_ptr的部分特性,但不是全部,特别是,既不能在容器中保存auto_ptr,也不能从函数中返回auto_ptr,虽然auto_ptr仍是标准库的一部分,但是编写程序时应该使用unique_ptr。 + +### 2.4 weak_ptr智能指针 + +​ std::weak_ptr是一种不控制所指向对象生存周期的智能指针,它指向由一个 shared_ptr 管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会释放,即便有weak_ptr指向该对象。特性: + +> - std::weak_ptr\ w; // 空weak_ptr可以指向类型为T的对象 +> - std::weak_ptr\ w(sp); // 与 shared_ptr sp指向相同对象的weak_ptr,T必须能转换为sp指向的类型 +> - w = p; // p可以是一个shared_ptr或一个weak_ptr,赋值后w与p共享对象 +> - w.reset(); // 将w置为空 +> - w.use_count(); // 与w共享对象的shared_ptr的数量 +> - w.expired(); // 若w.use_count()为0,返回true,否则返回false +> - w.lock(); //如果expire为true,返回一个空shared_ptr,否则返回一个指向w的对象的shared_ptr + +当创建一个weak_ptr时,要用一个shared_ptr来初始化它: + +```c++ +auto p = std::make_shared(42); +std::weak_ptr wp(p); // wp弱共享p,p的引用计数未改变 +``` + +由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock,此函数检查weak_ptr指向的对象是否仍存在,如果存在,lock返回一个指向共享对象的sahred_ptr,与任何其它shared_ptr类似,只要此shared_ptr存在,它所指向的底层对象也就会一直存在,例如: + +```c++ +if (std::shared_ptr np = wp.lock()) { // np不为空条件才成立 + // 在if中,np与p共享对象 +} +``` + +### 2.5 动态数组 + +​ 了解一下,大多数应用应该使用标准库容器而不是动态分配的数组,使用容器更为简单,更不容易出现内存管理错误并且可能有更好的性能。 + +初始化动态分配对象的数组: + +> - int *pia1 = new int[10]; // 10个未初始化的int +> - int *pia2 = new int[10]\(); // 10个初始值为0的int +> - std::string *psa = new std::string[10]; // 10个空string +> - std::string *psa2 = new std::string[10]\(); // 10个空string +> +> 新标准中还可以提供一个元素初始化器的花括号列表: +> +> - int *pia3 = new int[6] {0, 1, 2, 3, 4, 5}; +> - std::string *p3 = new std::string[10] {"a", "an", "the", std::string(3, 'x')}; + +​ 注意:如果初始化器数目小于元素数目,剩余元素将进行值初始化,如果初始化器数目大雨元素数目,则new表达式失败,不会分配任何内存,应该也会抛出一个类型为==bad_array_new_length==的异常,类似于==bad_alloc==,此类型定义在==头文件new中==。 + +--- + +动态数组删除:delete[] p3; + +std::unique_ptr\ p(new int[10]); // p指向一个包含10个未初始化int的数组 +p.release(); // 自动调用delete[]销毁其指针 + +注意一点:==指向数组的unique_ptr不支持成员访问运算符(点和箭头运算符),接着上面,访问p中的成员,只能是用p[i]这样的方式==。 + +与unique_ptr不同,shared_ptr不直接支持管理动态数组,如果要用,就==必须提供自定义的删除器==: +std::shared_ptr\ sp(new int[10], \[](int *p) {delete[] p;}); +sp.reset(); // 使用自己提供的lambda释放数组,它使用delete[] + +且shared_ptr不直接支持动态数组管理这一特性会影响数组中元素的访问: +// shared_ptr未定义下标运算符,并且不支持指针的算术运算 +for (size_t i = 0; i != 10; ++i) { + *(sp.get() + i) = i; // 使用get获取一个内置指针 + +} + +练习:连接两个字符串字面敞亮,将结果保存到一个动态分配的char数组中,以及重写此程序,连接两个标准库string对象: + +```c++ +const char *a = "hello ", *b = "world!"; +std::cout << a << std::emdl; // 会直接打印 hello +unsigned len = strlen(a) + strlen(b) + 1; +char *r = new char[len](); +strcat_s(r, len, a); +strcat_s(r, len, b); // 注意这些个用法吧 +std::cout << r << std::endl; + +std::string str1 = "hello ", str2 = "nihao!"; +strcpy_s(r, len, (str1 + str2).c_str()); // 注意这些个用法吧 +std::cout << r << std::endl; + +delete[] r; +``` + +#### allocator类 + +​ 标准库==allocator==类定义在==头文件#include \==中,它主要将内存分配和对象构造分离开来,类似于vector,allocator是一个模板,为了定义一个allocator对象,必须指明这个allocator可以分配的对象类型,当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对其位置: + +std::allocator\ my_alloc; // 可分配string的allocator对象 +auto const p = my_alloc.allocate(5); // 分配5个未初始化的string + +标准库allocator类及其算法: + +> - std::allocator\ a; // 定义一个allocator对象 +> - a.allocate(n); // 分配一段原始的、未构造的内存,保存n个类型为T的对象 +> - a.deallocate(p, n); // 释放从T*指针p中地址开始的内存,p必须是一个先前由allocator返回的指针,且n必须是p创建时所要求的大小。在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destory +> - a.construct(p, args); // p必须是一个类型为T*的指针,指向一块原始内存;arg被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象 +> - a.destory(p); // p为T*类型的指针,此算法对p指向的对象执行析构函数 + +这个有些搞不明白了(接上面): + +```c++ +auto q = p; +my_alloc.construct(q++); +my_alloc.construct(q++, 10, 'c'); +my_alloc.construct(q++, "hi"); +``` + +当用完对象后,必须对每个元素调用destory销毁, + +```c++ +while (q != p) { + my_alloc.destory(--q); // 释放真正构造的string +} +``` + +一旦元素被销毁后,可以重新使用这部分内存来保存其它string,也可将其归还给系统,释放内存通过调用deallocate来完成:my_alloc.deallocate(p, n); (传递给deallocate的指针不能为空,它必须指向由allocate分配的内存,n也必须与分配内存时提供的大小参数保持一样)。 + +--- + +拷贝和填充未初始化内存的算法: + 标准库还为allocator类定义了两个伴随算法,可以在未初始化内存中创建对象,他们也都==定义在头文件memory中==。 + +> - std::uninitialized_copy(v.begin(), v.end(), b2); // 把迭代器输入范围内元素拷贝到迭代器b2孩子的那个的未构造的原始内存中 +> - std::uninitialized_copy_n(v.begin(), n, b2); // 从迭代器v开始拷贝n个元素到b2开始的内存中 +> - std::uninitialized_fill(v.begin(), v.end(), t); // 在迭代器v指定原始内存范围中创建对象,对象的值均为t的拷贝 +> - std::uninitialized_fill_n(v.begin(), n, t); + +​ 假定有一个int的vector,希望将其内容拷贝到动态内存中,,那么先分配一块比vector中元素所占用空间大一倍的动态内存,然后将原vector中的元素拷贝到前一半空间,对后一半空间用一个定值进行填充: +auto p = my_alloc.allocate(v.size() * 2); +auto q = std::uninitialized_copy(v.begin(), v.end(), p); // 通过拷贝v中的元素来构造从p开始的元素 +std::uninitialized_fill_n(q, v.size(), 42); // 将剩余元素初始化为42 + +## 三、拷贝控制 + +### 3.1 更新三/五法则 + +​ 更新三五法则:所有的五个拷贝控制成员应该看作一个整体:一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作(我的理解是拷贝构造函数、拷贝赋值运算符函数(就是重载,operator=)、析构函数、移动构造函数、移动赋值运算符函数),某些类则是必须定义前三个才能正确工作。 + +### 3.2 拷贝、赋值与销毁 + +#### 3.2.1 拷贝构造函数 + +​ 如果一个构造函数的第一个参数是自身类类型的引用,且==任何额外参数都有默认值==,则此构造函数是==拷贝构造函数==。当使用拷贝初始化时,我们会用到拷贝构造函数。 + +​ ==拷贝构造函数的第一个参数必须是一个引用类型==(为什么必须是引用类型,理解不明白了,书442页),Person(const int&); //拷贝构造函数,可以定义非const,但几乎总是一个const的引用,拷贝构造函数在几种情况下都会被隐式的使用,因此拷贝构造函数通常不应该是explicit的. + +注意一下拷贝构造函数的写法: + +```c++ +class HasPtr { + //HasPtr(const HasPtr &hp) : ps(hp.ps), age(hp.age) {} + // 下面这个相比上面是动态分配一个新的string,注释是`*hp.ps`,要有*号,属性中ps是一个指针类型(拷贝构造函数) + HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), age(hp.age) {} + HasPtr& operator=(const HasPtr &hp) { + ps = hp.ps; age = hp.age; // 拷贝赋值运算符 + return *this; + } +private: + std::string *ps; + int age; +}; +``` + +#### 3.2.2 =default + +==std::swap可以直接交换两个数据==。 + +```c++ +struct My_print { + My_print() = default; // 使用合成的默认构造函数,直接用 My_print(); 好像区别不大 + My_print(const My_print&) = default; // 拷贝构造函数 + My_print& operator=(const My_print&); // 拷贝赋值运算符 + ~My_print() = default; +}; +``` + +​ 在C++11新标准中,如果我们需要默认行为,那么可以通过在参数列表后面写上`= default`来要求编译器生成构造函数,其中 = default 既可以和声明一起出现在类内部,也可以作为定义出现在类的外部。和其它函数一样,如果 = default 在类的内部,则默认构造函数是内联的,如果它在类外部,则该成员默认下不是内联。 + +​ class 和 struct定义类的唯一区别就是默认的访问权限,struct默认是public,而class默认是private。 + +#### 3.2.3 =delete + +​ 阻止拷贝:例如iostream类阻止了拷贝,在新标准下,可以通过将拷贝构造函数和拷贝赋值运算符定义为==删除的函数==来阻止拷贝。删除的函数是这样一种函数:虽然声明了它们,但不能以任何方式使用它们,在函数的参数列表后加上=delete来指出希望这个函数定义为删除的。就是禁用该构造函数 + +```c++ +struct NoCopy { + NoCopy() = default; // 使用合成的默认构造函数 + NoCopy(const NoCopy&) = delete; // 阻止拷贝 + NoCopy& operator=(const NoCopy&) = delete; // 阻止赋值 + ~NoCopy() = default; // 使用合成的析构函数 +}; +``` + +​ 与=default不同的是,=delete必须出现在函数第一次声明的时候,,=default直到编译器生成代码时才需要;另一点,可以对任何函数指定=delete,但是只能对编译器可以合成的默认构造函数或拷贝控制成员使用=default。 + +​ 本质上,当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就会被定义为删除的。在新标准发布前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private的来组织拷贝,但现在如果想阻止拷贝,则还是应该使用=delete + +--- + +还有一个注意点:==类内静态变量一定要类外实现==: + +```c++ +class Employee { +public: + static int unique_id; + //static int unique_id = 5; // 这是错的 +}; +int Employee::unique_id = 5; // 类内static变量必须类外初始化 +``` + +### 3.3 对象移动 + +​ 比如自己写数组的扩容,就可以不是把已有元素拷贝到新地址,而是直接移动,就会大幅度提升性能(这种拷贝也是拷贝后会直接销毁原对象);;还有一个原因:源于IO类或unique_ptr这样的类,都包含了不能被共享的资源(如指针或IO缓冲),因此这些类型的对象不能拷贝但可以移动。 + +​ 小知识:旧c++标准中,没有直接的方法移动对象,容器保存的类必须是可拷贝的。。但是在新标准中,可以用容器保存不可拷贝的类型,只要他们能被移动即可,标准容器、string和shared_ptr即支持移动也支持拷贝,IO类和unique_ptr类可以移动但不能拷贝。 + +#### 3.3.1 移动构造函数和std::move + +​ 如果自己实现类似于vector的扩容,都是会开辟一个新空间,再把数据拷贝进去。新标准库引入了2种机制,可以避免元素的拷贝。 + +- 一种:有一些标注库类,包括string,都定义了所谓的“移动构造函数”,可以假定string的一定构造函数进行了指针的拷贝,而不是为字符分配内存空间然后拷贝字符。 +- 两种:名为std::move的标准库函数,它定义在`#include `头文件中,需要用std::move来表示希望使用string的移动构造函数,如果漏掉了move的调用,将会使用string的拷贝构造函数 +- 在2c++核心编程.md 中的 5.0.3 有关于对std::move的我的理解 + +#### 3.3.2 右值引用 + +​ 新标准引入的,==通过`&&`来获得`右值引用`==,右值引用的一个重要的性质:只能绑定到一个将要销毁的对象;因此介意自由的将一个右值引用的资源“移动”到另一个对象中。 + +​ 性质(很重要):常规引用我们可以将其称之为`左值引用`:不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式;==右值引用==有着完全相反的绑定特性:可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上。如下: + +> int i = 42; +> +> - int &r = i; // 正确:r引用i +> - int &&rr = i; // 错误:不能将一个右值引用绑定到一个左值上 +> - int &r2 = i * 42; // 错误:i*42是一个右值 +> - const int &r3 = i * 42; // 正确:可以将一个const的引用绑定到一个右值上(注意这) +> - int &&rr2 = i * 42; // 正确:将rr2绑定到乘法结果上(右值引用) +> - int &&r3 = i; // 错误:表达式i是左值,不能右值引用 + +所以,可以将一个const的左值引用或一个右值引用绑定到得到右值的这类表达式上。 + +故左值持久,右值短暂,右值要么是字面常量,要么是在表达式求职过程中创建的临时对象 + +> - int &i = 42; // 错误 +> - const &r1 = 42; // 正确 +> - int &&r2 = 42; // 正确 + +由于右值引用智能绑定带临时对象,所以: + +- 所引用的对象将要被销毁 +- 该对象没有其它用户 + +这俩特性也意味着:使用右值引用的代码,可以自由的接管所引用的对象的资源。 + +int &&r3 = i; // 错误:表达式i是左值,不能右值引用 + +​ 虽然不能将一个右值引用直接绑定到一个左值上,但可以现实地将一个左值转换为对应的右值引用类型,通过调用一个名为`move`的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件`#include `中 +那么:int &&r3 = std::move(i); // ok + +​ 调用move后就意味着:除对 i 赋值或销毁它外,将不再使用它,且为了避免潜在的名字冲突,尽量使用std::move,而不是move + +练习: + +```c++ +int f(); +vector vi(100); +int &&r1 = f(); +int &r2 = vi[0]; // 注意这个是左值引用 +int &r3 = r1; +int &&r4 = vi[0] * f(); +``` + +--- + +Tips: + +- 在移动操作后,源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设,例如对一个标准库string或容器移动数据时,我们知道移后源对象任然有效,因此可以对它执行诸如empty或size这些操作,但是不知道将会得到什么结果,我们可能期望一个移后源对象是空的,但是这是没有保证的,所以尽量不去操作移后源对象。 + +- 合成的移动操作(如果自己没写,又用了,那么就叫是==合成==,如合成拷贝/移动构造函数、合成赋值运算符、合成析构函数。): + 自己的类,即便不声明自己的拷贝构造函数或拷贝赋值运算符,编译器总会为我们合成这些操作(可直接使用),拷贝操作要么被定义为逐成员拷贝,要么被定义为对象赋值,要么被定义为删除的函数。 + 与拷贝操作不同,编译器根本不会为某些类合成移动操作,特别是,如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。如果一个类没有移动操作,通常正常的函数匹配,类会使用对象的拷贝操作来代替移动操作。 + +- ==只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符==: + + ```c++ + #include + struct X { + int i; + std::string s; + }; + struct hasX { + X men; + }; + // 编译器会为X和hasX合成移动操作 + X x1, x2 = std::move(x1); + hasX hx1, hx2 = std::move(hx1); + ``` + + ​ 如果显式地要求编译器生成=default的移动操作,且编译器不能移动所有成员(好像有const的就不能移动吧),则编译器就会将移动操作定义为删除的函数: + + ```c++ + // 假定 Y 是一个类,它定义了自己的拷贝构造函数但未定义自己的移动构造函数: + struct hasY { + hasY() = default; + hasY(hasY &&) = default; + Y men; // hasY将有一个删除的移动构造函数 + }; + hasY hy1, hy2 = std::move(hy1); // 错误:移动构造函数是删除的 + ``` + +#### 3.3.3 noexcept + +移动操作、标准库和异常: + +​ 由于移动操作“窃取”资源,它通常不分配任何资源,因此移动操作通常不会抛出任何异常,当编写一个不抛出异常的移动操作时,应该将此事通知标准库。除非标准库知道我们的移动构造函数不会抛出异常,否则它会移动我们的类对象时可能会抛出异常,并为了处理这种可能性而做一些额外的工作。 + +​ 一种通知标准库的方法是在构造函数中指明`noexcept`,这是新标准引入的,noexcept使我们承诺一个函数不抛出异常的一种方法,通常在一个函数的参数列表后指定noexcept;在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之间。 + +```c++ +class strVec { +public: + strVec(strVec&&) noexcept; // 移动构造函数(这是定义) +}; +如果是类内列表初始化,那就是: ` &&p) noexcept : age(p.age), name(p.name) {..}` +strVec::strVec(strVec&&) noexcept { // (这是实现) + /*实现的内容*/ +} +strVec &strVec::operator=(strVec &&rhs) noexcept { // 拷贝赋值运算符 + /*实现的内容*/ +} +``` + +​ tips:不抛出异常的移动构造函数和移动赋值运算符都必须标记为noexcept,且头文件和实现文件中,都要指定noexcept + +​ 深入理解:首先标准库容器能对异常发生时自身行为提供保障,像vector,如果调用push_back时发生异常,vector自身不会改改变。==vector它就是除非知道元素类型的移动构造函数函数不会抛出异常,否则在重新分配内存的过程中,它就必须使用拷贝构造函数而不是移动构造函数==,使用拷贝即便出现异常,也会把原来的保留。如果希望vector重新分配内存时对我们自定义类型的对象进行移动而不是拷贝,就必须显示地告诉标准库我们的移动构造函数可以安全使用,这就是通过将移动构造函数(及移动赋值运算符)标记为noexcept来做到这一点。 + +#### 3.3.4 移动构造函数 + +- 好像只有一个 & 的是拷贝构造函数 + +- 然后有两个 && 的是移动构造函数 + +```c++ +class strVec { +public: + strVec(strVec&); // 拷贝构造函数 + strVec(strVec&&) ; // 移动构造函数 +}; +``` + +==移动右值,拷贝左值==: + 如果一个类strVec既有移动构造函数,也有拷贝构造函数,编译器就会使用普通的函数匹配规则来确定使用那个构造函数,自己的类 strVec: + +``` +strVec v1, v2; +v1 = v2; // v2是左值,使用拷贝赋值 +strVec getVec(std::istream &); // 返回的一个右值 +v2 = getVec(std::cin); // getVec(std::cin)是一个右值,使用移动赋值。 +``` + +​ Tips:如果一个类有一个可用的拷贝构造函数而没有移动构造函数,又强行使移动时,其实是通过拷贝函数来实现“移动”的。拷贝赋值运算符和移动赋值运算符的情况类似。 + +重要: + +​ 一个成员函数同时提供拷贝和移动版本,它也能从中受益,例如定义了push_back的标准容器提供两个版本:一个版本是一个const左值引用,另一个版本是右值引用参数: + +``` +void push_back(const T&); // 拷贝, +void push_back(T&&); // 移动 +``` + +​ 对于第二个版本,只可以传递给它非const的右值,这样就是精确匹配(也是更好的匹配)的,所以当传递的是一个可修改的右值,编译器就会选择运行这个版本。 + +--- + +==移动迭代器==: + +​ 新标准库中定义了一种移动迭代器,,与其它迭代器不同,移动迭代器的解引用运算符生成一个右值引用,通过调用标准库的==std::make_move_iterator==,可能就是std::uninitialized_copy(std::make_move_iterator(vec.begin(), std::make_move_iterator(vec.end()), another_vec.begin()); // 可是这样 + +这样就是不是拷贝操作了,而是移动。 + +​ 总之:由于一个移后源对象具有不确定的状态,对其调用std::move是危险的,通过在类代码中小心地使用move可以大幅度提升性能,但也容器出难以查找的错误,只有当进行的移动操作十分安全时,才可以使用std::move,就是要慎用。 + +#### 3.2.5 引用限定符 + +​ 有时,右值的使用方式使人惊讶: s1 + s2 = "wow!"; // 我们对两个string的连接结果,一个右值进行了赋值,在旧标准中无法阻止这种使用方式,为了维持向后的兼容性,新标准库仍然允许向右值赋值,但我们可能希望在自己的类中阻止这种用法,就可以强制左侧运算对象(即this指向的对象)是一个左值。 + +​ 方法是:在参数列表后放置一个`引用限定符`,==引用限定符可以是&(只能将它用于左值)和&&(只能将它用于右值)==,分别指出this可以指向一个左值或右值,==类似const限定符,引用限定符只能用于(非static)成员函数,且必须同时出现在函数的声明和定义中==。 + +``` +class Foo { +public: + Foo &operator=(const Foo &rhs) &; // 只能向可修改的左值赋值 + Foo anotherFunc() const &; // 若有const,引用限定符&必须在const之后 +}; +``` + +示例: + +``` +Foo &retFoo(); // 返回一个引用;retFoo调用是一个左值 +Foo retVal(); // 返回一个值,retVal调用是一个右值 +Foo i, j; // i和j是左值 +i = j; // 正确:i是左值 +retFoo() = j; // 正确:retFoo返回是一个左值 +retVal() = j; // 错误:retVal返回是一个右值 +i = retVal(); // 正确:可以将一个右值作为赋值操作的右侧运算对象。 +``` + +## 四、其它 + +### 4.1 c++中的可调用对象(函数、函数指针、lambda表达式、bind创建的对象、匿名函数) + +这还是比较重要有意义的。 + +​ c++中有几种可调用的对象:函数、函数指针、lambda表达式、==bind创建的对象==以及重载了函数调用运算符的类(匿名函数吧)(bind创建的对象使用可以看2c++核心编程.md中的4.6.7 标准库定义的函数对象中的最后一个练习)。 + +c++语言中,==函数表==用于存储指向这些可调用对象的“指针”,当程序需要执行某个特定的操作时,从表中查找该调用的函数。函数表很容易通过map来实现,运算符符号的string对象作为关键字,使用实现运算符的函数作为值: + +```c++ +int my_add(int i , int j) {return i + j;}; // 普通函数 +// 注意下面的value的类型是`函数指针`,接收两个参数 +std::map a_map; // 定义map +// 添加元素 +a_map.insert({"+", my_add}); // {"+", add}是一个pair,定要注意 +``` + +然后可以使用一个名为`function`的新的标准库类型,它也是定义在在`#include `头文件中 + +| function的操作 | | +| --------------------------------------- | ------------------------------------------------------------ | +| std::function\ f; | f是一个对象,函数类型T(T是retType(args)) | +| std::function\ f(nullptr); | 显示地构造一个空function | +| std::function\ f(obj); | 在f中存储可调用对象obj的副本 | +| f | 将f作为条件:当f中有一个可调用对象时为真,否则为假 | +| f(args); | 调用f中的对象,参数是args | +| ==定义为std::function\的成员的类型== | | +| result_type | 该function类型的可调用对象返回的类型 | +| argument_type | 当T有一个或两个实参时定义的类型。如果T只有一个实参,
则argument_type是该类型的同义词; | +| first_argument_type | 如果T有两个实参,则first_argument_type和 | +| seconde_argument_type | second_argument_type分别代表两个实参的类型 | + +简单使用: + +​ 简单来说,这种场景是不是比如“my_add”函数不在这个cpp文件里,而是把my_add函数的参数类型、返回类型通过std::function写进这个cpp的参数里,后面调用时,直接传入这个函数,比如[这里](https://github.com/godotengine/godot/blob/11d3768132582d192b8464769f26b493ae822321/core/extension/gdextension.cpp#L49)。 + +```c++ +#include +int my_add(int i, int j) { return i + j; }; // 普通函数 + +struct my_divide { + int operator() (int deno, int divi) { + return deno / divi; + } +}; +int main(int argc, char** argv) { + + std::function f1 = my_add; // 函数指针 + std::function f2 = my_divide(); // 函数对象类的对象(匿名函数) + std::function f3 = [](int i, int j) {return i * j; }; // lambda + std::cout << f1(4, 2) << std::endl; // 6 + std::cout << f2(6, 2) << std::endl; // 3 + std::cout << f3(4, 2) << std::endl; // 8 +} +``` + +那么使用这个function类型就可以重新定义map: + +```c++ +#include +#include + +int my_add(int i, int j) { return i + j; }; // 普通函数 +struct my_divide { + int operator() (int deno, int divi) { + return deno / divi; + } +}; +auto my_mod = [](int deno, int div) {return deno % div; }; +int main(int argc, char*argv[]) { + // 这里面插入的都是pair对组数据类型,还可以有别的写法 + std::map> a_map = { + {"+", my_add}, // 函数指针 + {"-", std::minus()}, // 标准库函数对象 + {"/", my_divide()}, // 我定义的函数对象(匿名对象) + {"*", [](int i, int j) {return i * j; }}, // 未命名的lambda + {"%", my_mod} // 命名了的lambda对象 + }; + std::cout << a_map["+"](10, 5) << std::endl; // 15 + std::cout << a_map["%"](10, 5) << std::endl; // 0 + return 0; +} +``` + +Tips:新版标准库中的 function 类与旧版本中的unary_function和binary_function没有关联,后两个类已经被更通用的bind函数替代了。 + +### 4.2 正则表达式 + +​ C++正则表达式库(RE库),它是新标准库的一部分,RE库定义在头文件`#include `中,它包好多个组件库,如下表所示: + +| 正则表达式库组件 | (有的是类,有的是function,写到的时候注意看提示) | +| -------------------- | ------------------------------------------------------------ | +| std::regex | 表示有一个正则表达式的==类== | +| std::regex_match | 将一个字符序列与一个正则表达式匹配 | +| std::regex_search | 寻找第一个与正则表达式匹配的子序列 | +| std::regex_replace | 使用给定格式替换一个正则表达式 | +| std::sregex_iterator | 迭代器适配器,调用regex_search来遍历一个string中所有匹配的子串 | +| std::smatch | 容器==类==,保存在string中搜索的结果 | +| std::ssub_match | string中匹配的子表达式的结果,==类== | + +​ 例如函数regex_match和regex_search确定一个给定字符序列与一个给定regex是否匹配,如果整个输入序列与表达式匹配,则regex_match函数返回true,如果输入序列中一个子串与表达式匹配,则regex_search函数返回true。 + +​ 下表列出了regex的函数的参数,都是返回bool值,且都被重载了:其中一个版本接收一个类型为==smatch==的附加参数,如果匹配成功,这些函数将成功匹配相关信息保存在给定的smatch对象中。 + +> regex_search和regex_match的参数:(这些操作会返回bool值,指出是否找到匹配) +> +> (seq, m, r, mft) +> +> (seq, r, mft) // 相当于两个版本 + +​ 解读:在字符序列seq中查找regex对象r中的正则表达式。seq可以是一个string,表示范围的一对迭代器以及一个指向空字符结尾的字符数组的指针;m是一个==match对象==,用来保存匹配结果的相关细节,m和seq必须具有兼容的类型;mft是一个可选的regex_constants::match_flag_type值,下表描述了这些值,它们会影响匹配过程。 + +#### 4.2.1 使用正则表达式库 + +==Demo_1==:查找违反众所周知的拼写规则:“i除非在c之后,否则必须在e之前”的单词:(相当重要) + +```c++ +#include // 注意这个头文件 +int main(int argc, char*argv[]) { + std::string pattern("[^c]ei"); + pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*"; // alpha就一个冒号 + std::regex r(pattern); + + std::smatch results; + std::string test_str = "receipt freind theif receive"; + + if (std::regex_search(test_str, results, r)) { + std::cout << results.str() << std::endl; + } + return 0; +} +``` + +解读: + +- 第3行:\[^c]表示匹配任意不是c的字符,这里就是找任意不是c的字符后跟着ei的字符串(共3个字符); +- 第4行:要匹配整个单词,regex使用的正则表达式语言是==ECMAScript==,在这里,模式==[[::alpha:]]==匹配任意字母,符号==+==和==*==分别表示希望“一个或多个”或“零个或多个”匹配,因此==[[::alpha:]]*==将匹配零个或多个字母; +- 正则表达式存入字符串pattern后,用它来初始化一个名为r的regex对象; +- 再定义了一个名为results的==smatch==对象,它将传递给regex_search,如果找到匹配子串,results将会保存匹配位置的细节信息; +- 定义的test_str中,与模式匹配的单词(“freind”和“ theif”)和不匹配的单词(“receipt”和“receive”); +- 最后调用==std::regex_search==,找到匹配子串,就返回true,用results的==str()成员==来打印test_str中与模式匹配的部分;且==此函数在输入序列中只要找到一个匹配子串就会停止查找==,故输出的会是freind,后面的就不管了。若要全部打印,就要看下面的[4.2.4小节](#4.2.4 匹配与Regex迭代器类型)。 + +补充:下面是指定regex对象的选项(参考上面看): + +- std::regex r(re); + +- std::regex r(re, f); + +- r1 = re; + +- r1.assign(re, f); // 就是各种方式创建regex对象 + +- r.mark_count() r中子表达式的数目 + +- r.flags() 返回r的标志集 + + ​ 下表是定义regex时指定的标志: + +| 定义在regex和regex_constants::syntax_option_type中 | | +| -------------------------------------------------- | ----------------------------- | +| icase | 在匹配过程中忽略大小写 | +| nosubs | 不保存匹配的子表达式 | +| optimize | 执行速度优于构造速速 | +| ECMAScript | 使用ECMA-262指定的语法 | +| basic | 使用POSIX基本的正则表达式语法 | +| extended | 使用POSIX扩展的正则表达式语法 | +| awk | 使用POSIX版本的awk语言的语法 | +| grep | 使用POSIX版本的grep的语法 | +| egrep | 使用POSIX版本的egrep的语法 | + +​ 这最后6个标志指出编写正则表达式所用的语言,必须且只能设置其中一个,默认ECMAScript标志被设置,从而regex会使用ECMA-262规范,这也是很多Web浏览器所使用的正则表达式语言。 + +==Demo_2==:编写一个正则表达式来识别“==一个或多个字母或数字字符后接一个‘.’再接‘cpp’或‘cxx’或‘cc==’,且==不区分大小写==”:(重要) + +```c++ +#include +int main(int argc, char*argv[]) { + // 下面一定要注意,[[]]不要少了任意半边,不然编译不会出错,运行出错很难找到 + std::regex r("[[:alnum:]]+\\.(cpp|cxx|cc)$", std::regex::icase); + std::smatch results; + std::string filename; + while (std::cin >> filename) { + if (std::regex_search(filename, results, r)) { + std::cout << results.str() << std::endl; + } + } + return 0; +} +``` + +解读: + +- 正则表达式中有特殊字符,字符点(.)通常匹配任意字符,与c++一样,在字符前放置一个反斜线来转义,由于反斜线在c++中也是一个特殊字符,所以还需要加一个反斜线,所以想要一个普通的点==.==,如上的写法就是`\\.` + +#### 4.2.2 指定或使用正则表达式时的错误 + +​ 正则表达式是在运行时,当一个regex对象被初始化或赋予一个新模式时,才被“编译”的(这不是由c++编译器解释的),所以一个正则表达式的语法是否正确是在运行时解析的。 + +​ 如果编写的正则表达式存在错误,则在运行时标准库会抛出一个类型为regex_error的异常。类似于标准异常类型,regex_error有一个==what()成员==操作来描述发生了什么错误。regex_error还有一个名为==code()的成员==,用来返回某个错误类型对应的数值编码,code返回的值是由其具体实现定义的。RE库能抛出的标准错误如下表: + +| 正则表达式错误类型 | 定义在regex和regex_constants::error_type中 | +| ------------------ | -------------------------------------------- | +| error_collate | 无效的元素校对请求 | +| error_ctype | 无效的字符类 | +| error_escape | 无效的转义字符或无效的尾置转义 | +| error_backref | 无效的向后引用 | +| error_brack | 不匹配的方括号 [或] | +| error_paren | 不匹配的小括号 (或) | +| error_brace | 不匹配的花括号 {或} | +| error_badbrace | {}中无效的范围 | +| error_range | 无效的字符范围 (如[z-a]) | +| error_space | 内存不足,无法处理此正则表达式 | +| error_badrepeat | 重复字符(*、?、+或{)之前没有有效的正则表达式 | +| error_complexity | 要求的匹配过于复杂 | +| error_stack | 栈空间不足,无法处理匹配 | + +​ 总之为了减小开销,应避免创建很多不必要的regex,特别是在一个循环中使用正则表达式时,应在循环外创建它,而不是每步迭代时都编译它。 + +==特别重要==:以后写正则表达式都这样来捕获错误,不然很难发觉哪里写错了: + +```c++ +#include +try { // 下面少了一个],正确应该是 [[:alnum:]] + std::regex r("[[:alnum:]+\\.(cpp|cxx|cc)$", std::regex::icase); +} +catch (std::regex_error e) { + std::cout << e.what() << "\ncode:" << e.code() << std::endl; +} +``` + +把这段代码放进main函数中运行,就得得到这样的输出: + +> regex_error(error_brack): The expression contained mismatched [ and ]. +> code:4 + +这样就比较明确是哪里出错了,就比较方便查找。 + +#### 4.2.3 正则表达式类和输入序列类型 + +​ 输入可以是普通char数据或wchar_t数据,字符可以保存在标准库string中或是char数组中(或是宽字符版本,wstring或wchar_t数组中)。RE库为这些不同的输入序列都定义了对应的类型。 + +​ 例如:regex类保存类型char的正则表达式。标准库还定义了一个wregex类保存类型wchar_t,其操作与regex完全相同,唯一差别是wregex的初始值必须使用wchar_t而不是char。 + +​ 匹配和迭代器类型(下小节就会写到)更为特殊,这些类型的差异不仅在于字符类型,还在于序列是在标准库string中还是数组中:smatch表示string类型的输入序列;cmatch表示字符数组序列;wsmatch表示宽字符串(wstring)输入;而wcmatch表示宽字符数组。 + +demo: + +```c++ +std::regex r("[[:alnum:]+\\.(cpp|cxx|cc)$", std::regex::icase); +std::smatch results; // 将匹配string输入序列,而不是char* +if (std::regex_search("myfile.cc", results, r)) {/**/} // 错误的:输入为char* +``` + +​ 所以以上代码会编译失败,因为match参数的类型与输入序列的类型不匹配,如果我们希望搜索一个字符数组,就必须使用cmatch对象: + +```c++ +std::cmatch results; // 注意这里的区别 +if (std::regex_search("myfile.cc", results, r)) {/**/} // 正确的 +``` + +下表为==正则表达式库类==: + +| 如果输入序列类型 | 则使用正则表达式类 | +| ---------------- | ---------------------------------------------- | +| string | regex、smatch、ssub_match和sregex_iterator | +| const char* | regex、cmatch、csub_match和cregex_iterator | +| wstring | wregex、wsmatch、wssub_match和wsregex_iterator | +| const wchar_t* | wregex、wcmatch、wcsub_match和wcregex_iterator | + +#### 4.2.4 匹配与Regex迭代器类型 + +​ 上面4.2.1最开始那个例子只能打印出来匹配到的第一个,没办法打印后续,那就需要用到sregex_iterator来获得所有匹配,这些操作也适用于cregex_iterator、wsregex_iterator和wcregex_iterator: + +​ std::sregex_iterator it(b, e, r); // 一个sregex_iterator,编译迭代器b和e表示的string,它调用sregex_search(b, e, r)将it定位到输入中第一个匹配的位置 + +```c++ +#include // 注意这个头文件 +int main(int argc, char*argv[]) { + std::string pattern("[^c]ei"); + pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*"; + // 以上两行可以换成下面这个通用的那种正则表达式,效果一样的 + // std::string pattern("(\\w*)[^c]ei(\\w*)"); // 里面加小括号只是方便看 + + std::regex r(pattern); + + std::smatch results; + std::string test_str = "receipt freind theif receive"; + // 注意核心是下面这行: + for (std::sregex_iterator it(test_str.begin(), test_str.end(), r), end_it; it != end_it; ++it) { + std::cout << it->str() << std::endl; + } + return 0; +} +``` + +解读: + +- 这里就会打印满足条件的 freind 和 theif; +- for语句中初始值定义了it和end_it,当定义it时,sregex_iterator的构造函数调用regex_search将it定位到test_str中第一个与r匹配的位置;而end_it是一个空sregex_iterator,起到尾后迭代器的作用(就理解为最后一个元素后一个位置,就像一个序列的.end()); +- for语句中的递增运算通过regex_search来“推进”迭代器,当解引用迭代器时,会得到一个表示当前匹配结果的smatch对象,调用它的str()成员来打印匹配的单词。 + +==使用匹配数据==: + 匹配类型有两个名为==prefix()==和==suffix()==的成员,分别返回表示输入序列中当前匹配之前和之后部分的ssub_match对象,一个ssub_match对象有两个名为str和length的成员,分别返回匹配的string和该string的大小。接着上面的代码,就是把里面循环丰富了: + +```c++ +for (std::sregex_iterator it(test_str.begin(), test_str.end(), r), end_it; it != end_it; ++it) { + auto pos = it->prefix().length(); // 前缀的大小 + pos = pos > 40 ? pos - 40 : 0; // 想要最多40个字符 + std::cout << it->prefix().str().substr(pos) // 前缀的最后一部分 + << "\n\t\t>>>" << it->str() << " <<<\n" // 匹配的单词 + << it->suffix().str().substr(0, 40) << std::endl; // 后缀的第一部分 +} +``` + +​ 下表就是smatch对象操作:(这些操作也适用于cmatch、wsmatch、wcmatch和对应的csub_match、wssub_match、wcsub_match) + +| smatch操作 | (下面m就是它的一个对象,自己理解的) | +| -------------------- | ------------------------------------------------------------ | +| m.ready() | 如果已经通过调用regex_search或regex_match设置了m,则返回true。
如果ready()返回false,则对m进行操作是未定义的 | +| m.size() | 如果匹配失败,则返回0;否则返回最近一次匹配的正则表达式中子表达式的数目 | +| m.empty() | 若m.size()为0,则返回true | +| m.prefix() | 一个ssub_match对象,表示当前匹配之前的序列 | +| m.suffix() | 一个ssub_match对象,表示当前匹配之后的部分 | +| m.format(...) | 下面会讲,看书这部分吧 | +| m.length(n) | 第n个匹配的子表达式的大小 | +| m.position(n) | 第n和子表达式距序列开始的距离 | +| m.str(n) | 第n个子表达式的string(如果不匹配,m.str()是会返空的) | +| m[n] | 对应第n个子表达式的ssub_match对象 | +| m.begin(), m.end() | 表示m中sub_match元素范围的迭代器 | +| m.cbegin(), m.cend() | | + +​ 根据上面的经验,n似乎不是必须的,一般都不要,除非特别指定第n个时才给这个参数吧(n一定要看子表达式里的demo)。 + +#### 4.2.5 使用子表达式 + +正则表达式中国的模式通常包含一个或多个==子表达式==,正则表达式语法通常==用括号表示子表达式==: + +// 下面r有两个子表达式:第一个是点之前表示文件名的部分,第二个表示文件扩展名 +std::regex r("([[:alnum:]]+)\\\\.(cpp|cxx|cc)$", std::regex::icase); + +那么先的模式包含两个括号括起来的子表达式: + +- ([[:alnum:]]+) // 匹配一个或多个字符的序列 +- (cpp|cxx|cc) // 匹配文件扩展名 + +```c++ +#include +int main(int argc, char*argv[]) { + // 注意这与上面的区别,这里的 [[:alnum:]]+ 用了一个括号括起来,作为子表达式,+号也一定要被括进去 + std::regex r("([[:alnum:]]+)\\.(cpp|cxx|cc)$", std::regex::icase); + std::smatch results; + std::string filename; + while (std::cin >> filename) { + // 假定输入一个 foo.cpp + if (std::regex_search(filename, results, r)) { + std::cout << results.str() << std::endl; // foo.cpp + std::cout << results.str(0) << std::endl; // foo.cpp + std::cout << results.str(1) << std::endl; // foo + std::cout << results.str(2) << std::endl; // cpp + } + } + return 0; +} +``` + +Tips: + +- 子表达式一定要用()括起来,没有的话结果就不是所想的;还要注意那个+号的位置,它也特别重要,它位置错了,结果也会变的。 +- 位置[0]的元素表示整个匹配;元素[1]...[n]表示每个对应的子表达式。 + +然后有一个子表达式验证电话号码的demo就不写了,挺复杂的,用到时看书左上角标的654页。 + +#### 4.2.6 使用regex_replace + +直接上例子: + +```c++ +std::string fmt = "$2.$5.$7"; +std::string phone = "(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})"; +std::regex r(phone); + +std::string number = "(908) 555-1800"; +std::cout << std::regex_replace(number, r, fmt) << std::endl; +``` + +解读: + +- 第1行:用一个符号$后跟子表达式的索引号来表示一个特定的子表达式(这里就是希望在替换字符串中使用第2个、第5个、第7个子表达式,而忽略第一个、第三个、第四个、第六个子表达式);这里就是想将号码改成ddd.ddd.dddd的样式; + 因为结果是想要==.==来连接,所以这里用的点,如果是想要ddd-ddd-dddd,那就是`"$2-$5-$7"` +- 第2行,phone子表达式的解读:(ECMAScript正则表达式语言特性看书654页) + - (\\\\()? // 表示区号部分可选的左括号 + - (\\\\d{3}) // 表示区号 + - (\\\\))? // 表示区号部分可选的右括号 + - ([-. ])? // 表示区号部分可选的分隔符 + - (\\\\d{3}) // 表示号码的下三位数字 + - ([-. ])? // 表示可选的分隔符 + - (\\\\d{4}) // 表示号码的最后四位数字 +- 最后会得到输出结果:908.555.1800 + +​ 用来控制匹配和格式的标志,其类型为match_flag_type,这些值都定义在名为regex_constants的命名空间中,一般的例子:std::regex_constants::format_no_copy 还有一些其它的标志在书上,不写了,用到时去看吧。 + +### 4.3 随机数 + +#### 生成随机数技巧 + +> 小技巧:c++中生成随机小数的技巧 +> +> - float score = std::rand() % 10 + 1; // 这只会得到 7.0这样的数据,它相当于只是把一个随机整数强转成了float +> - float score = (float)(std::rand() % 41 + 60) / 10.0f; // 8.6 +> - // 前面整型生成的是0\~40的整数,+60就是60~100的整数,记得先转成float,再除以浮点型的10.0f得到的就是 6.5、7.6、9.1这样的小数,记得分子分母都得是浮点型,不然精度要丢失 +> - float score = (float)(std::rand() % 401 + 600) / 100.0f; // 8.65 +> - 这就是要两位小数的话,都先放大100倍,再除以100倍,得到的就是6.53、7.62、9.19这样的小数了 + +```c++ +// 设了随时间的随机种子,每次才不一样;(使用随机数时都加上这个) +#include //记得头文件 +std::srand((unsigned int)time(NULL)); // 这对下面c++的方式并不起作用 +``` + +--- + +​ 新标准之前,C和C++都依赖于一个简单的C库函数==rand==来生成随机数,此函数生成均匀分布的伪随机整数,范围在0和一个系统相关的最大值(至少为32767(定义的宏“RAND_MAX”,十六进制为0x7fff))之间。 + +​ 定义在头文件`#include `中的随机数库通过一组协作的类来解决生成随机浮点数、非均匀分布的数的问题:下面是随机数库的组成 + +- ==随机数引擎类== 类型(==default_random_engine==) ,生成随机unsigned整数序列 +- ==随机数分布类== 类型,使用引擎返回服从特定概率分布的随机数 + +​ Note:C++程序不应该使用库函数rand,而应使用==default_random_engine==类(生成的是==无符号随机整数==,调用这个对象的输出就是类似C库函数rand的输出)和恰当的分布类对象。 + +```c++ +#include // 注意一定要这个头文件 +int main(int argc, char*argv[]) { + // 然后头文件这种,一定要std::开头 + std::default_random_engine e; // 默认构造函数,使用该引擎的默认种子 + // std::default_random_engine e(5); // 也可以给个种子 + e.seed(6); // 也可以这样重置引擎的状态 + for (size_t i = 0; i < 10; ++i) { + std::cout << e() << std::endl; // “调用”对象来生成一个随机数 + } + std::cout << e.min() << std::endl; // 0 + std::cout << e.max() << std::endl; // 此引擎可生成的最大最小值 + system("pause"); + return 0; +} +``` + +补充: + +- 上面的例子非常重要,直接看上面就知道它的使用方式; +- std::default_random_engine::result_type a = 5; // 此引擎生成的unsigned整数类型 +- e.discard(u) // 将引擎推进u步,u的类型为unsigned long long + +==分布类型和引擎==:(重要,有==生成指定范围类的数字==) + +​ 就上面例子而言,一般随机数引擎的输出是不能直接使用的(上面出来的数字都非常大,通常与我们想要的不符),所以称之为原始随机数。 + +为了得到在一个指定范围内的数,使用一个分布类型的对象:下面代码就是==生成0~9(包含)之间均匀分布随机数== + +```c++ +#include +#include +int main(int argc, char*argv[]) { + // 下面是针对整型的随机数 + static std::uniform_int_distribution u(0, 9); // 要random头文件的(0、9都能取到,是闭区间) + // static std::default_random_engine e; // 每次运行得到的结果都是一致的(可能是时间太短) + // 使用当前时间(秒数)作为随机数引擎的种子,每次结果都不一致 + static std::default_random_engine e(static_cast(std::time(nullptr))); + for (size_t i = 0; i < 10; ++i) { + std::cout << u(e) << std::endl; + } + return 0; +} +``` + +解读: + +- 第3行:是一个类模板,要显式地指定类型,构造时要指定范围0,9; +- 第4行:生成无符号随机整数; +- 第6行:将u作为随机数源,每个调用返回在指定范围内并服从均匀分布的值;且注意这里是u(e)意思是传递给分布对象u的是引擎对象本身e,不要写成了u(e()),这就是把e生成的值传递给u,这会导致编译错误。 +- ==一定看这==:在linux下,使用这种方式,好像第一次随机的数永远是u.min(),这里也就是0,要注意下这个情况。 + +这里把==C生成随机数==的方式也写这里,感觉在linux下,这种方式更易用: + +```c++ +#include +#include +#include // 搭配根据时间的随机种子 +int main() { + // srand、time、rand不用加std都是可以的 + srand((unsigned int)time(NULL)); // 固定随机种子写法 + // 方式一:这是生成1-100的随机数 + int num = rand() % 100 + 1; // (如果不+1,rand() % 100就是成成0-99) + + // 方式二:在PCL中,还看到 生成0-1023之间的数,(里面是有小数的,比如:34.22 20.5313 931.5) + float nun01= 1024.0f * rand() / (RAND_MAX + 1.0f); // RAND_MAX是宏,上面写过 + return 0; +} +``` + +所以要生成范围(min, max)内的随机数:min + rand()%(max-min + 1); # 加1是为了包含取到max + +最后:当说==随机数发生器==时,是指==分布对象和引擎对象的组合==。 + +--- + +==让每次生成的随机数不同==: + +​ 书上叫“引擎生成一个数值序列”。像上面写的例子的引擎,在一个程序中多次调用,或一个程序多次运行,得到的结果都是一样的,即: + +- 一个给定的随机数发生器一直会生成相同的随机数序列; +- 重要:==一个函数如果定义了局部随机数发生器,应将其(包括引擎和分布对象)定义为static的,否则每次调用函数都会生成相同的序列==。(定义为static就只是让其生命周期延长,类似于全局变量了,特别是函数里,应该这样写) + +```c++ +#include +void my_random() { + static std::default_random_engine e; // 注意这两行的static + static std::uniform_int_distribution u(0, 9); + for (size_t i = 0; i < 10; ++i) + std::cout << u(e) << std::endl; + std::cout << u.max() << std::endl; // 9 这也有max()、main() +} +int main(int argc, char*argv[]) { + my_random(); + std::cout << "------------------" << std::endl; + my_random(); + return 0; +} +``` + +解读: + +- 如果第3、4行没有`static`,程序无论运行多少次,第9、11行的结果永远是一样的; + + - `一定要加static,一定要啊,养成习惯!` +- 现在加了static,所以它们在函数调用之间会保持住状态,第一次调用(9行)会使用u(e)生成的序列的前10个随机数,第二次调用(11行)会获得接下来的10个,以此类推。那么一次程序运行第9行和11行的结果就不一样; +- 但是多次程序运行,每一次的第9行结果永远是一样,每一次的第11行结果也都是一样的。所以就要设置时间的随机种子, +- ==一定看这==:在linux下,使用这种方式,好像第一次随机的数永远是u.min(),这里也就是0,要注意下这个情况。 + +#### 时间的随机种子 + +​ 想要每次程序运行时给的随机数不一样,那就要设定不一样的随机种子,一般就是调用系统函数time,这个函数定义在头文件`#include `中,它返回从一个特定时刻到当前经过了多少秒。函数time接收单个指针参数,它指向用于写入时间的数据结构,如果此指针为空,则函数简单地返回时间: + +```c++ +#include +#include // 别忘了头文件 +std::default_random_engine e(std::time(0)); // 直接构造时指定 +e.seed(std::time(0)); // 或是这样来改变 +``` + +Tips: + +- 由于time返回以秒计的时间,因此这种方式只适用于生成种子的间隔为秒级或更长的应用。更精细的话就要用std::chrono库来做种子的 +- 这里的time()无论在linux还是windows下都是可以直接使用的,不用加std::也是可以的。 +- time(0)和time(NULL)应该是一样的,这里的参数好像是一个指针,所以都行。 + +#### 小数随机数分布 + +==生成随机实数(主要是浮点数)==: + +​ 最常用但不正确的从rand函数获得一个随机浮点数的方法是rand()的结果除以RAND_MAX,其随机整数的精度通常低于随机浮点数,这样有一些浮点值就永远不会被生成了。 + +使用新标准库设施,可轻松获得随机浮点数: + 首先定义一个==uniform_real_distribution==类型的对象(这个的构造函数是explicit的),并让标准库来处理从随机整数到随机浮点数的映射,其使用与uniform_int_distribution基本类似。 + +```c++ +static std::default_random_engine e; +static std::uniform_real_distribution u(0, 5); // 类型必须是浮点型 +for (size_t i = 0; i < 10; ++i) { + std::cout << u(e) << std::endl; + // 这个也有 u.min() u.max() + // 还有 u.reset() 重建u的状态,使得随后对d的使用不依赖于d已经生成的值 +} +``` + +Tips: + +- 第2行,模板的默认类型就是double,但因为是类模板不可以隐式指定类型,可以像这样写代表使用默认结果类型:std::uniform_real_distribution\<> u(0, 5); // 给个尖括号放那里代表使用默认类型 + +--- + +==std::normal_distribution==类型: + +```c++ +#include +#include +int main(int argc, char*argv[]) { + static std::default_random_engine e; + e.seed(5); // 可以重设随机种子,也可以不要 + static std::normal_distribution<> n(4, 1.5); // 生成的值以均值4位中心,标准差为1.5 + std::cout << n(e) << std::endl; + + std::cout << std::lround(n(e)) << std::endl; + return 0; +} +``` + +解读: + +- 别忘了随机数引擎类可以在构建时设定随机种子,也可以用seed来重新指定; +- 第6行,只给了\<>,代表使用默认类型,然后一次就是生成一个数; +- 第9行,==std::lround()就是一个四舍五入的函数==,书上说这是在头文件``中,vs中不要也行,但是还是一定要写这个头文件,不然linux上就会直接报错。 + +--- + +==std::bernoulli_distribution==类型: + +​ 这是一个普通类,而非模板,所以不接受模板参数,此分布总是返回一个bool值,它返回true的概率是一个常数,此概率的默认值是0.5,也可以人为的去改变。 + +```c++ +std::default_random_engine e; // 随机数引擎类 +std::bernoulli_distribution b; // 随机数分布类 +bool result = b(e); // 返回true的默认概率就是0.5,即默认50/50的概率 +``` + +Tips: + +- 这叫伯努利分布,返回值只有true和false +- std::bernoulli_distribution b(0.9); // 让返回true的概率更大 + std::bernoulli_distribution b(0.2); // 让返回true的概率更小 + + + +最后:随机数引擎类一般就是用这一个==std::default_random_engine==,但是随机数分布类有很多种,除了上面写到的常用的几种,还有一些在书上右上角标的第781页。 + +### 4.4 异常处理 + +​ 头文件定义的异常类: + +| 异常类型 | | +| ---------------- | ---------------------------------------------- | +| exception | 最常见的问题 | +| runtime_error | 只有在运行时才能检测出的问题(如除数为0) | +| range_error | 运行时错误:生成的结果超出了有意义的值域范围 | +| overflow_error | 运行时错误:计算上溢 | +| underflow_error | 运行时错误:计算下溢 | +| logic_error | 程序逻辑错误 | +| domain_error | 逻辑错误:参数对应的结果值不存在 | +| invalid_argument | 逻辑错误:无效参数 | +| length_error | 逻辑错误:试图创建一个超出该类型最大长度的对象 | +| out_of_range | 逻辑错误:使用一个超出有效范围的值 | + +以下这张图是==异常类层次==说明,catch字句捕获要从最细的类(下)到上: + +![](./c++遇到的坑/illustration/异常类层次.png) + +​ C++标准库定义了一组类,用于报告标准库函数遇到的问题。这些异常类也可以在用户编写的程序中使用,它们分别定义在4个头文件中: + +- exception头文件定义了最通用的异常类exception。它只报告异常的发生,不提供任何额外信息。 +- stdexcept头文件定义了几种常用的异常类,详细信息如上表。 +- new头文件定义了bad_alloc异常类型,这种类型将在后面说 +- type_info头文件定义了bad_cast 异常类型,这种类型将在后面说。 + +只能以默认初始化的方式初始化exception、bad_alloc和bad_cast对象,不允许为这些对象提供初始值; + +​ 其它异常类的要求刚好相反:应该使用string对象或者C风格字符串初始化这些类型的对象,但是不允许使用默认初始化的方式,当创建此类对象时,必须提供初始值(就是字符串的自定义错误提示信息),(这也就应该解释了上面示例代码的必须要throw表达式,就是用这来初始化吧) + +​ ==异常类型只定义了一个名为what()的成员函数,返回值是一个指向C风格字符串的const char*==;对于没有初始值的异常类型来说,what返回的内容由编译器决定。 + +#### 4.4.1 throw | try + +(注意:点到这里时,也稍微往上划一下,上面也还有些不错的内容) + +==throw==跟int这些一样直接使用,是关键字;throw表达式引发一个异常(直接程序运行不下去,报错的),throw紧跟的表达式类型就是抛出的异常类型,简单的例子: + +```c++ +#include +if (a == b) { + throw std::runtime_error("这是一个错误抛出"); +} +``` + +书上说类型runtime_error是标准异常类型的一种,定义在stdexcept头文件中,但是我在win下和linux下不导入这个头文件都是可用的。 + +==try==语法: + +```c++ +try { + // 这里代码,好像一般都要有一个throw语句把错误抛出 +} +catch (一个错误类型) { + // 错误的处理 +} +catch (又一个其它的错误类型) { + +} +``` + +示例: + +```c++ +#include +#include + +int main() { + int i = 2, j = 0; + try { + if (j == 0) { + throw std::runtime_error("除数为0了"); + throw std::invalid_argument("field is not a bool"); // 这行参考 + } + std::cout << i / j << std::endl; + } + catch (const std::runtime_error& err) { + std::cout << err.what() << ";;这是这一行的提示信息" << std::endl; + } + system("pause"); + return 0; +} +``` + +Tips: + +- 上面代码try里的代码,一定要在执行i/j的除法前,做判断把错误通过throw抛出来(但是这不会像上面单独使用throw一样直接报错);好像一定要有这个不能像python那样,把除法写这里,然后让系统去直接捕获异常;换言之,throw表达式语句,存在于代码块中,将控制权限转移到相关的catch子句。 +- 第13行的`err.what()`的返回结果就是字符串,内容就是上throw表达式里写的内容。 + +一般这么用: + +```c++ +std::range_error r("error"); +throw r; + +std::exception *p = &r; +throw *p; +``` + +vs中给的示例代码片段: + +```c++ +try { + // 需要执行的代码 +} +catch (const std::exception&) { // 也可以是 const std::exception& err + // do something // 就可以输出 err.what() 查看错误信息 +} +``` + +#### 4.4.2 catch(...) + +捕获所有异常:`catch (...){ }` 是固定写法, + +```c++ +try {/* 内容 */ +} +catch (...) { // ... 是固定写法 + // 处理异常的某些特殊操作 + throw; // 执行完当前局部能完成的工作,随后重新抛出异常 + // throw若在其它异常处理代码之外,编译器将调用terminate +} +``` + +Tips: + +- catch(...)能单独出现,也能与其它几个catch语句一起出现,若一起出现,它必须被放在最后。 +- catch(const std::exception&) 也算是捕获所有异常。 + +#### 4.4.3 noexcept + +​ 在c++新标准中,可以通过提供==noexcept说明==指定某个函数不会抛出异常,其形式是关键字noexcept紧跟在函数的参数列表后面: +void recoupt(int) noexcept; // 不会抛出异常(这就是做了==不抛出说明==) +void recoupt(int) throw(); // 与上面等价,在早期写法,==c++新版本中已经被取消了== + +void alloc(int); // 可能抛出异常 + +- 对一个函数来说,noexcept 要么出现在该函数的所有声明语句和定义语句中,要么一次也不出现; +- noexcept 应该出现在函数的尾置返回类型之前; +- 在typedef或类型别名中不能不出现noexcept ; +- 在成员函数中,noexcept 说明符需要跟在const及引用限定符之后,而在final、override或虚函数的=0之前。 + +--- + +noexcept还可以添加一个异常说明的是实参(该实参必须能转换成bool类型): +void recoupt(int) noexcept(true); // recoupt不会抛出异常 +void alloc(int) noexcept(false); // alloc可能抛出异常,相当于不加noexcept说明 + +​ 以上都是为了一个==noexcept运算符==,它常与上面说的实参一起使用,比较普通的形式是 noexcept(e),当e调用的所有函数都做了不抛出说明且e本身不含有throw语句时,这表达式为true,否则返回false。以下一个异常来说明: +​ void f() noexcept(noexcept(g())); // f和g的异常说明一致 + +​ 如果g承诺了不会抛出异常,则函数f也不会抛出异常;如果g没有异常说明符,或者g虽然有异常说明符但允许抛出异常,则f也可能排除异常。 + +说明: + 单单就一个noexcept,那它就是一个==noexcept说明==;如果是像上面noexcept(g())用其计算返回bool值,那它就是一个==noexcept运算符==。 + +#### 4.4.4 自定义异常类型 + +使用自己的异常类型:通过继承来写自己的异常类,然后拿来使用:(直接是代码demo) + +```c++ +class isbn_mismatch : public std::logic_error { +public: + explicit isbn_mismatch(const std::string &s) : std::logic_errors(s) {} + isbn_mismatch(const std::string &s, const std::string &lhs, const std::string &rhs) : std::logic_error(s), left(lhs), right(rhs) {} + + const std::string left, right; +}; +``` + +// 如果参与加法的两个对象并非同一书籍,则抛出一个异常: + +```c++ +Sales_data& Sales_data::operator+=(const Sales_data& rhs) { + if (isbn() != rhs.isbin()) + // 下面这就是用的上面我们自己写的异常类来抛出,用法跟标准库都是一模一样的 + throw isbn_mismatch("wrong isbns", isbn(), rhs.isbn()); +} +``` + +类似上面自定义的异常类,也还有简单一点的(是一样的): + +```c++ +class out_of_stock : public std::runtime_error { +public: + explicit out_of_stock(const std::string &s) : std::runtime_error(s) {} +} +``` + +#### 4.4.5 其它概念 + +==栈展开==: + 一个try触发时,会检查与该try块关联的catch子句,若没找到且该try语句嵌套在其它try块中,则继续检查与外层try匹配的catch子句,若这样仍没找到,则退出当前这个主调函数,继续在调用了刚刚推出的这个函数的其它函数中寻找,以此类推,这个过程就是==栈展开==。最后都找不到匹配的catch时,程序将调用标准函数库terminate来终止程序的执行过程。 + +--- + +==函数try语句块==与构造函数: + +​ 就是把异常捕获放进构造函数,下面是伪代码,格式大致是: + +``` +template +Blod::Blod(std::initializer_list li) try : data(std::make_shared (li)) { + /* 空函数体 */ +} catch(const std::bad_alloc &e) {handle_out_of_memory(e);} +``` + +​ 注意:关键字try出现在表示构造函数初始值列表的冒号以及表示函数体的花括号之前。与这个try关联的catch既能处理构造函数体抛出的异常,也能处理成员初始化列表抛出的异常。 + +### 4.5 命名空间 + +==定义==:以下就定义了一个名为 cplusplus_primer 的命名空间,包含三个成员:两个类和一个重载的+运算符。 + +```c++ +namespace cplusplus_primer { + class Sales_data { /*...*/}; + Sales_data operator+(const int&, const int&); + class Query { /*....*/}; +} // 注意结尾是没有分号的 +``` + +Tips: + +- 命令空间可以定义在全局作用域内,也可以定义在其他命令空间中,但是不能定义在函数或类的内部; +- ==命名空间可以是不连续的==:比如上面的代码,可能是定义了一个名为 cplusplus_primer 的新命令空间,也可能是为已经存在的命名空间添加一些新成员(那就是打开已经存在的命名空间定义并为其添加一些新成员的声明); + 所以注意:那么在多个(或单个)文件中的一个同名的命名空间里的数据是共同的,相当于增加,而不是覆盖。如imgui这个库的,在imgui.h头文件中namespace ImGui就出现了几次,且注意这个命令空间中的函数实现,并不都是在imgui.cpp中实现的,它是把头文件中的具体实现,分到了多个.cpp文件中(这些的特点就是都导入了imgui.h) +- 在全局下定义的一个名字,num,可以直接使用,如可能有冲突时,也可写作::num(这其实就是全局命名空间)。 + +==命名空间的别名==: + +比如上面的命名空间 cplusplus_primer 起个别名就是: +namespace primer = cplusplus_primer; // 以namespace起头 + +别名还可以指向一个嵌套的命名空间: +namespace n1 = cplusplus_primer::name1; // name1是cplusplus_primer中的一个嵌套命名空间 + +--- + +==嵌套的命名空间 | 内联命名空间==:(内联命名空间能比较方便获取内部其它命名空间的成员) + +​ 嵌套的命名空间:是指定义在其它命名空间的命名空间,那么使用的时候就要是 cplusplus::QueryLib::Query,嵌套了多少次就要用::这样的方式去指定。 + +​ C++11新标准引入了一种新的嵌套命名空间,称为==内联命名空间(inline namespace)==,和普通的嵌套命名空间不同,==内联命名空间中的名字可以被外层命名空间直接使用==,也就是说无须在内联命名空间的名字前添加表示该命名空间的前缀,通过外层命名空间的名字就可以直接访问它。如下:定义就是在关键字namespace前添加关键字inline,如下: + +```c++ +inline namespace name1 {} +// 注意这两行,关键字inline必须出现在命名空间第一次定义的地方,后续打开命名空间时,inline可写也可以不写 +namespace name1 { + class Query_base { /*...*/ }; +} +``` + +再来一个命名空间是非内联的: + +```c++ +namespace name2 { + class Item_base { /*...*/ }; +} +``` + +使用内联的好处:假定上面的两个命名空间都定义在同名头文件中,那么可以把命名空间 cplusplus_primer 定义成如下形式: + +```c++ +namespace cplusplus_primer { + #include "name1.h" + #include "name2.h" +} +``` + +​ 那么:因为name1是内联的,那就可以使用 cplusplus_primer::的代码获取name1的成员,而name2是非内联的,那就需要加上完整的外层命名空间名字,比如 cplusplus_primer::name2::Item_base + +--- + +==未命名的命名空间==:(这也叫“匿名命名空间”) + 是指关键字namespace后紧跟花括号起来的一系列声明语句。未命名的命名空间中定义的变量拥有==静态生命周期:他们在第一次使用前创建,直接程序结束才销毁==。 + +- 一个未命名的命名空间可以在某个给定的文件内不连续,但是不能跨越多个文件。 + +定义在未命名的命名空间中的名字可以直接使用,但一定要与全局作用域中的名字有所区别: + +```c++ +int i; +namespace { + int i; +} +i = 10; // 不对,有二义性了,i不知道是哪一个 +``` + +同样未命名的命名空间可以嵌套在其他命名空间中,然后就可以通过外层命名空间的名字来访问: + +```c++ +int i; +namespace local { + namespace { + int i; + } +} +local::i = 42; // 正确,这就有所区分 +``` + +==未命名的命名空间的意义==: + 取代文件中的静态声明。在标准c++引入命名空间的概念之前,需要将名字声明成static的以使得其对于整个文件有效。在文件中进行静态声明的做法是从C语言继承而来的,在C语言中,声明为static的全局实体在其所在的文件外不可见。 + 在文件中进行静态声明的做法已经被C++标准取消了,现在的做法就是使用未命名的命名空间。 + 也就是说==需要定义一系列静态的变量的时候,==应该使用未命名的命名空间。更多的解释看[这里](https://stackoverflow.com/questions/154469/unnamed-anonymous-namespaces-vs-static-functions)。 + +--- + +==using声明==:只对其所在的作用域有用,一次只引进命名空间的一个成员,如`using std::cout;` + +==using指示==:可以在全局作用域、局部作用域和命名空间作用域,但是它是不能出现在类的作用域中的,如`using namespace std;`,这个也是可以放到函数里的,特别是自己写的命名空间,就可以少写很多: + +```c++ +void func() { + using namespace cplusplus_primer; +} +``` + +总之少用using指示吧,但在命名空间本身的实现文件中可以使用using指示,这样会比较方便。 + +### 4.6 union类 + +​ ==联合(union)==是一种特殊的类:一个union可以有多个数据成员,但是在==任意时刻只有一个数据成员可以有值==,当给union的某个成员赋值后,该union的其它成员就变成了未定义的状态了。(是一种节省空间的类) +​ union可以为其成员指定public、protected和private等保护标记。默认情况下,union的成员都是公有的,这一点与struct相同。 + +==定义==: + 先关键字union,随后是该union的(可选的)名字以及花括号内的一组成员声明。 + +```c++ +union Token { + char cval; + int ival; + double dval; +}; +``` + +注意:Token类型的对象只有一个成员,该成员的类型可能是以上三种的任意一种。 + +==使用==: + 默认情况下union是未初始化的,可以像显示地初始化聚合类一样使用一对花括号内的初始值显式地初始化一个union:= + +- Token first_token = {'a'}; // 聚合类可以去看1C++基础.md中关于结构体那里 + +- Token *pt = new Token; // 指向一个未初始化的Token对象的指针 + pt->ival = 42; // 成员访问运算符来赋值 + +要注意:union在任意时都只有一个数据成员可以有值。 + +==匿名union==: + union后不要跟名字,就是一个匿名union。 + +```c++ +union { + char cval; + int ival; + double dval; +}; +cval = 'c'; // 为刚刚定义的未命名的匿名union对象赋一个新值 +ival = 42; // 该对象当前保存的值是42 +``` + +在匿名union的定义所在的作用域内该union的成员都是可以直接访问的(跟不限定作用域的枚举成员访问有些像) + +### 4.7 固有的不可移植的特性 + +​ 介绍C++从C语言继承而来的另外两种==不可移植的特性==:==位域==和==volatile限定符==。另外还介绍==链接指示==,它是c++新增的一种不可移植的特性。 + +​ 所谓不可移植特性是指因机器而已的特性,当我们将含有不可移植特性的程序从一台机器转移到另一台机器上时,通常需要重新编写该程序。算术类型的大小在不同机器上不一样,这是使用过得不可移植特性的一个典型示例。 + +#### 4.7.1 位域 + +​ 类可以将其(非静态)数据成员定义成==位域==,在一个位域中含有一定数量的二进制,当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。 + +​ 位域的类型必须是整型或枚举类型。因为带符号位域的行为是由具体实现确定的,所以在通常情况下,使用无符号类型保存一个位域。==位域的声明形式在在成员名字之后紧跟一个常量表达式==,该表达式用于指定成员所占的二进制位数: + +```c++ +typedef unsigned int my_Bit; +class my_File { + my_Bit mode : 2; // 占2位 + my_Bit modified : 1; // 占1位 + my_Bit prot_owner : 3; // 占3位 + my_Bit prot_group : 3; + my_Bit prot_world : 3; + // my_File的操作核数据成员 +public: + // 文件类型以八进制的形式表示(以0开头的整数代表八进制) + enum modes { READ = 01, WRITE = 02, EXECUTE = 03 }; + my_File &open(modes); + void close(); + void write(); + bool isRead() const; + void setWrite(); +}; +``` + +使用位域(接着上面的代码): + +```c++ +void my_File::write() { + modified = 1; + // .... +} +void my_File::close() { + if (modified) + // .... 保存内容 +} + +// 通常使用内置的位运算符操作超过1位的位域 +my_File &my_File::open(my_File::modes m) { + mode |= READ; // 按默认凡是设置READ + if (m & WRITE) + // ... 按照读/写方式打开文件 + return *this; +} +``` + +如果一个类定义了位域成员,则它通常也会定义一组内联的成员函数以检验或设置位域的值: + +```c++ +inline bool my_File::isRead() const { return mode & READ; } +inline void my_File::setWrite() { mode |= WRITE; } // 这里用了一个位运算符 | +``` + +#### 4.7.2 volatile限定符 + +​ 例如,程序可能包含一个由系统时钟定时更新的变量。当对象的值可能再程序的控制或检测之外被改变时,应该将对象声明为volatile,此关键字volatile告诉编译器不应对这样的对象进行优化。 + +使用:volatile限定符的用法和const很相似,它也是对类型的一个额外修饰: + +volatile int i; // 该int值可能发生改变 +volatile int iax[max_size]; // iax的每个元素都是volatile + +- 某种类型既可能是const的也能是volatile的; +- 也可以将成员函数定义为volatile的,也只有volatile的成员才能被volatile的对象调用; +- 可以声明volatile指针、指向volatile对象的指针以及指定volatile对象的volatile指针(跟const限定符和指针的相互作用类似); + +合成的拷贝对volatile对象无效:const和volatile的一个重要区别就是不能使用合成的拷贝/移动构造函数及赋值运算符初始化volatile对象或从volatile对象赋值。 + +#### 4.7.3 链接指示:extern "C" + +​ C++使用==链接指示==指出任意非C++函数所用的语言。(想要把C++代码和其它语言(包括C语言)编写的代码放在一起使用,要求我们必须有权访问该语言的编译器,并且这个编译器与当前的C++编译器是兼容的) + +- 链接指示可以有两种形式:单个的或复合的; +- 链接指示不能出现在类定义或函数定义的内部; +- 同样的链接指示必须在函数的每个声明中都出现。 + +```c++ +// 可能出现在C++头文件中的链接指示 +// 单语句链接指示 +extern "C" size_t strlen(const char *); + +// 复合语句链接指示 +extern "C" { + int strcmp(const char*, const char*); + char *strcat(char*, const char*); +} +``` + +​ 链接指示的第一种形式包含一个关键字extern,后面是一个字符串字面常量值以及一个“普通的”函数声明。其中的字符串字面值常量指出了编写函数所用的语言。编译器应该支持对C语言的链接指示。此外,编译器也可能会支持其它语言的链接指示,如extern "Ada"、extern "FORTRAN"等。 + +--- + +==链接指示与头文件==: + 多重声明的形式可以应用于整个头文件,例如,C++的cstring头文件可能形如: + +```c++ +// 符合语句链接指示 +extern "C" { + #include // 操作C风格字符串的C函数 +} +``` + +​ 当个#include指示被放置在复合链接指示的花括号中,头文件中的所有普通函数声明都被认为是由链接指示的语言编写的。链接指示可以嵌套,因此如果头文件包含带自带链接指示的函数,则该函数的链接不受影响。 + +--- + +指向C函数的指针与指向C++函数的指针是不一样的类型: + +```c++ +void (*pf1) (int); // 指向一个c++的函数 +extern "C" void (*pf2) (int); // 指向一个C函数 +pf1 = pf2; // 错误:pf1和pf2的类型不同 +``` + +​ extern "C" typedef void FC(int); // FC是一个指向C函数的指针 + +​ void f2(FC *); // f2是一个c++函数,该函数的形参是指向C函数的指针 + +--- + +导出C++函数到其它语言: + 通过使用链接指示对函数进行定义,可以令一个C++函数在其它语言编写的程序中可用: + +extern "C" double calc(double dparm) {/*...\*/} // calc函数可以被c程序调用 + +​ 编译器将为该函数生成适合于指定语言的代码。但注意,可被多种语言共享的函数的返回类型或形参类型受到很多限制。例如,不太可能把一个C++类的对象传给C程序,因为C程序根本无法理解构造函数、析构函数以及其它类特有的操作。 + +--- + +小操作:对链接带C的预处理器的支持 + +​ 有时需要在C和C++中编译同一个源文件,为了实现这一目的,在编译C++版本的程序时预处理器定义==__cplusplus==(两个下划线),利用这个变量,可以在编译C++程序的时候有条件的包含进来一些代码: + +```c++ +#ifdef __cplusplus +extern "C" +#endif +int strcmp(const char*, const char*); +``` + +--- + +重载函数与链接指示: + C语言不支持函数重载,所以C链接指示智能用于寿命一组重载函数中的某一个: +// 以下错误:两个extern "C"函数的名字相同 +extern "C" void print(const char*); +extern "C" void print(int); + +所以,在一组重载函数中有一个是C函数,则其余的必定都是C++函数 + +### 4.8 位运算符 + +| 运算符 | 功能 | 用法 | +| --------- | ------------- | --------------------------------- | +| ~ | 位求反 | ~expr | +| <<
>> | 左移
右移 | expr1 << expr2
expr1 >> expr2 | +| & | 位与 | expr & expr | +| ^ | 位异或 | expr ^ expr | +| \| | 位或 | expr \| expr | + +用的比较少,到时候直接看书吧,直接输入136页! + +还经常会看到这样的表达式:expr |= expr 这就像是+=,先位或再赋值。 \ No newline at end of file diff --git "a/C++/1 c++\345\237\272\347\241\200.md" "b/C++/1 c++\345\237\272\347\241\200.md" new file mode 100644 index 0000000..e8b1218 --- /dev/null +++ "b/C++/1 c++\345\237\272\347\241\200.md" @@ -0,0 +1,2843 @@ +# C++基础入门 + +## 一、C++初识 + +C++中也可以用 `exit(0);` 提前退出程序,中间的`0`只是一个标志 + +变量命名规范: + +变量命名有许多约定俗成的规范,下面的这些规范能有效提高程序的可读性: + +- 标识符要能体现实际含义; +- 变量名一般用小写字母,如index,不要使用Index或INDEX; +- 用户自定义的类名一般以大写字母开头,如Sales_item; +- 如果标识符由多个单词组成,则单词间应有明显区分,如 student_loan或studentLoan,不要使用studentloan。 + +数据类型选择建议: + +- 当明确知晓数值不可能为负时,选用无符号类型; +- 使用int执行整数运算,在实际应用中,short常常显得太小而long一般和int有一样的尺寸。如果你的数值超过了int的表示范围,选用1ong long,在算术表达式中不要使用char或bool,只有在存放字符或布尔值时才使用它们。因为类型char在一些机器上是有符号的,而在另一些机器上又是无符号的,所以如果使用char进行运算特别容易出问题。如果你需要使用一个不大的整数,那么明确指定它的类型是signed char或者unsigned char +- 执行浮点数运算选用 double,这是因为float通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。事实上,对于某些机器来说,双精度运算甚至比单精度还快。long double提供的精度在一般情况下是没有必要的,况且它带来的运行时消耗也不容忽视。 + +### 1.1 安装、注释 + +​ 安装visual studio 2017,选择默认的就好了 + +第一个C++项目: +新建一个空的c++项目,然后再源文件中填一个一个cpp文件:这都是固定写法 + +```c++ +#include +using namespace std; + +int main() { + cout << "helllo world" << endl; + system("pause"); + return 0; +} +``` + +注释: + +- 单行注释:`// 描述信息` +- 多行注释:`/*这里是描述信息*/` +- 注释快捷键:`ctrl+k+c 解除注释ctrl+k+u` # 这样的注释都是反斜杠的 + +### 1.2 常量定义 + +变量:语法:`数据类型 变量名 = 初始值;` + +常量:用于不可更改的数据 + +C++定义常量两种方式 + +- #define 宏常量:`#define 常量名 常量值` // 记得没有分号 + - 通常在文件上方定义,表示一个常量 +- const修饰的变量:`const 数据类型 常量名 = 常量值;` + - 通常在变量定义前加关键字==const==,修饰该变量为常量,不可修改 + +```c++ +#include +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++是不是,看[这](https://zhuanlan.zhihu.com/p/22460835)): + +```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` 宏的编写技巧,简单说明一下: + +- 反斜线代表该行未结束,会串接下一行; +- 而如果宏里有多过一个语句(statement),就需要用 `do { /*...*/ } while(0)` 包裹成单个语句,否则会有问题。do...while的用处之一就是在这,之二就是用在函数中,用来代替go to语法,海康的SDK就常用这。 + +### 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 作用域 + +```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++规定在创建一个变量或者常量时,必须要指定出相应的数据类型,否则无法给变量分配内存 + +### 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的值错的离谱,一定注意)。如下: + +```c++ +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位有效数字 | + +```c++ +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};` + +```c++ +double a = 3.14159; +//int b{ a }, c = { a }; // 编译会出错,因为存在丢失信息的危险, +int d(a), e = d; // 正确,就会丢失小数部分 +``` + +​ 也就是说,使用`{ }`来初始化,那定义的数据类型必须和传进来的数据类型一致,不然就会报错(这是因为列表初始化时,初始值存在丢失的风险时,编译器就会报错);反之,系统会智能去掉小数部分保留整数。 + +​ 使用`{ }`的初始化的形式叫==列表初始化==,现在无论是初始化对象还是某些时候为对象赋值,都可以使用这样一组由花括号括起来的初始值了。 + +在[c++小知识.md](./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'` + +- 单引号内只能有一个字符,不可以是字符串(否则会直接报错) +- 在显示字符型变量时,==只能用单引号==将字符括起来,不能使用双引号(否则会字节报错)。 +- C和C++中字符型变量只占用==1个字节== +- 字符型变量并不是把字符本身放到内存中存储,而是将对应的ASCII编码(再以二进制)放入到存储单元 + +```c++ +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"(这里面放字符串)"` + +字符串型用于显示==一串字符==,两种风格 + +- C风格字符串:`char 变量名[20] = "字符串值;"` --> `char str1[] = "hello world;"` -> + `const char *str2 = "hello"` // 定义时就赋值,可以不给20;只定义还是给上20;还可以写成指针形式,直接打印str2就是对应的结果,一定要加const + 或者:`char str1[] = { 'h', 'e', 'l', 'l', 'o', '\0' };` //也可以这样的形式给,注意结尾一定要有 `\0`(这是零),才认定为字符串,其实上面的字符串也有,就是省略了。 + - C风格的字符串==必须要用双引号==括起来 + - 很重要啊:`char name[5] = "lisi";` 后面字符串的长度最多只能是`5-1=4`(可以不给5,后面长度就任意) + - C风格的字符串好像就是一个char型的数组;真正的C风格的字符串是`const char* str3 = "hello";` +- C++风格字符串:`string 变量名= "字符串值;"` --> `string str2 = "hello world;"` + - ==C++风格字符串,需要加入头文件`#include`== // 这个很重要 + +数字转成字符串:==to_string== + +```c++ +#include +#include // 别忘了这 +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。 + +- L'a' // 宽字符型字面值,类型是wchar_t +- u8"hi" // utf-8字符串字面值(utf-8用8位编码一个Unicode字符) +- 42ULL // 后缀ULL,无符号整形字面值,类型时unsigned long long +- 1E-3F // 后缀F,单精度浮点型字面值,类型时float +- 3.14159L // 后缀L,扩展精度浮点型字面值,类型是 long double + +#### 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 码大致由以下两部分组成: + +* ASCII 非打印控制字符: ASCII 表上的数字 0-31 分配给了控制字符,用于控制像打印机等一些外围设备; +* ASCII 打印字符:数字 32-126 分配给了能在键盘上找到的字符,当查看或打印文档时就会出现。 + +### 2.6 布尔类型 bool + +bool类型只有两个值,只占用==一个字节==: + +- true --- 真(本质是1,其它任意非0的数也是一样) +- false --- 假(本质是0) + +`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进制 | + +示例: + +```c++ +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则会保留下来 + +```c++ +const int num = 123; +auto a = num; // a是一个整形(num的顶层const被忽略了) +auto b = # // b是一个指向整数常量的指针(对常量对象取地址是一种底层const) +``` + + + +### 2.9 键盘数据的输入 + +作用:用于从键盘获取数据 + +关键字:==cin==, 语法:`cin >> 变量` + +```c++ +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; | + +```c++ +int main() { + int a = 10; + int b = 3; + std::cout << a / 3 << std::endl; // 结果是 3, 整数之间的除法只能得到整数的 + return 0; +} +``` + +Ps:然后两个小数也是不能取模运算的 + +前置/后置 递增 + +```c++ +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 位运算符 + +注意:以下都是按二进制的形式来。 + +看这个[菜鸟教程](https://www.runoob.com/cplusplus/cpp-operators.html)。**&**按位与、 **|**按位或 、 **^**异或运算符、 **~**取反运算符 + +在上面链接的最后的笔记里有讲**>>**和**<<**,这里简单写下:(也可参考和这个[笔记](https://blog.csdn.net/jackuylove/article/details/105088312?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-105088312-blog-111367069.pc_relevant_3mothn_strategy_and_data_recovery&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-105088312-blog-111367069.pc_relevant_3mothn_strategy_and_data_recovery&utm_relevant_index=1)) + +- `>>`:向右位移,就是把尾数去掉位数,例如:153 >> 2,153的二进制是:10011001,屁股后面去掉 2 位 100110,100110 转化成十进制就是 38,153 = 10011001,38 =100110,"01" 去掉了。 +- `<<`:向左位移,就是把开头两位数去掉,尾数加位数00。 + - 1<<20 就代表1M的大小 + - 1<<30 就代表1G的大小 + +## 四、流程结构 + +### 4.1 选择结构 + +​ 注意这种花括号结尾都是没分号的 + +#### (1) if语句 + +- 单行格式if语句:`if (条件) {条件满足执行的语句}` + +- 多行格式if语句:`if (条件) {条件满足执行的语句} else {条件不满足执行的语句}` + +- 多条件的if语句:`if (条件1) {条件1满足执行的语句} else if (条件2) {条件2满足执行的语句} else {都不满足执行的语句}` + + ```c++ + 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; + } + ``` + +#### (2) 三目运算符 + +语法:`表达式1 ? 表达式2 : 表达式3;` + +- 如果表达式1的值为真,就执行表达式2,并返回表达式2的结果 +- 如果表达式1的值为假,就执行表达式3,并返回表达式3的结果 + +```c++ +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 +``` + +>特别注意:三目运算符返回的是变量,可以继续赋值 + +```c++ +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; + +```c++ +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后面跟的必须是整形常量表达式(单个字符也是可以的) + +```c++ +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次。 + +代码(自己写的): + +```c++ +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; +} +``` + +示例代码: + +```c++ +#include +#include + +using std::cout; using std::cin; using std::endl; using std::string; using std::pair; + +int main() +{ + pair 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 ` // 这是time系统时间头文件 + +```c++ +#include +#include +#include // 搭配根据时间的随机种子 + +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 (循环条件);` + +```c++ +int num = 0; +do { + std::cout << num << std::endl; + num++; +} while (num <= 10); +``` + +>总结:这与while最大的区别就是,这无论怎样都要先执行一次循环语句,再判断循环条件,而while循环可能直接进不去的。 +> +>最常用的做法还是do{...} while(0); : +> +>- 用于宏定义代码块。 +>- 替代掉goto用法。 + +​ 在c++中计算几次方: + +```c++ +#include // 要在上面导入这个头文件 +int main() { + int value = 0; + int a = 4; + value = pow(a, 3); // 这就是a的3次方 +} +``` + +#### (3)for循环语句 + +for循环一句的特殊写法: + +```c++ +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那种退出条件` + +```c++ +for (int i = 5; i >= 0; i--) { + std::cout << array[i] << std::endl; +} +// 这样倒着输出数值,那就是 i >= 0 才去执行,不是想着小于0就不执行退出而写成 i < 0 ,那样永远满足不了条件,就永远进不去循环 +``` + +​ 注意这种大括号、花括号后面都是 没有分号的 + +​ 语法:`for (起始表达式; 满足条件的表达式; 末尾循环体) {循环语句}` 要注意一点,for里面这三个表达式可以任意两个或一个甚至0个,但是里面的两个 分号 一个也不能少,这三个式子都可以在其它地方写的: + +```c++ +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的倍数,我们打印敲桌子,其余数字直接打印输出。 + +```c++ +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:打印乘法口诀表 + +```c++ +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 跳转语句 + +- break; // 就不多说了 +- continue; // 这也不多说了 + +#### goto语句 + + 语法:`goto 自己定义的标记;` //别忘了这个分号 +如果标记的名称存在。执行到goto语句时,会跳转到标记的位置。 + +```c++ +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的函数,用于获取数组的首地址和末尾地址的后一个: + +```C++ +int arr[] = { 1, 2, 3, 4, 5 }; +std::vector v(std::begin(arr), std::end(arr)); +// 当然也能值拷贝一段值 +std::vector v1(arr +1, arr+3); +``` + +### 5.1 一维数组定义方式 + +​ 我自己推荐第三种吧。 + +- `数据类型 数组名[数组长度];` // 都要给`数组长度` +- `数据类型 数组名[数组长度] = {值1, 值2, 值3};` // 值的个数小于等于数组长度(那剩余位置会自动填0) +- `数据类型 数组名[] = {值1, 值2, 值3.....};` // 这种值可以给任意个 + +>动态给`数组长度` +> +>int n; +>cin >> n; +>int *array1 = new int[n]; +>delete[] array1; // 记得释放内存,数组释放记得加`[]` + +==注意==:尽量别用 char类型的数组吧,跟上面的C风格的字符串定义很相似,然后像下面这个例子,按道理打印数组名 arr1 ,得到的应该是数组的首地址,但结果却含有乱码。 + +```c++ +char arr1[2]; // 尽量不使用这种类型的数组 +arr1[0] = 'x'; +arr1[1] = 'y'; // 这样去赋值 +// arr1[2] = 'z'; // 这是错的,千万别超出了,可能会有结果,但一定是错的 +``` + +```c++ +int arr2[5] = {10, 20, 30}; // 定义了5个长度,只给了3个,那后面2个就默认填0了 +std::cout << arr2[4] << std::endl; +// cout << arr2[5] << endl; //这是错的,千万不要索引越界了,会有结果,但是错的离谱 +``` + +```c++ +// 这要在上面加入的头文件 +string arr3[] = { "dasd", "asdas", "asda" }; // 后面给几个,前面会知道有几个的 +``` + +​ Ps:上面这个是字符串的数组,只能使用string,不能使用C的风格,因为C风格定义字符串就是 `char a[] = "hello";` 虽然可以像第三种定义数组的方式 `char a[] = {"hello"}` ,但是里面只能放一个值,多一个都要报错 + +#### (1) 数组名用途 + +- 获取整个数组占用的内存空间大小 + +- 通过数组名取到数组的首地址 + + ```c++ + 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; + ``` + + ```c++ + 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; 这也是绝对错误的,==数组名是常量,因此不可以再赋值了== + +练习:将数组反转: + +```c++ +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) 冒泡排序 + +```c++ +/* + 冒泡排序:下面第一个是我自己写的(我的好像不大像冒泡,但实现了效果): + 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; + } + } +} +``` + +```c++ +/* + 教学视频的方法;第一轮也是所有数两两相比,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, 数据2}, {数据3, 数据4}};` // 推荐就使用这,直观 +- `数据类型 数组名[行数][列数] = {数据1, 数据2, 数据3, 数据4};` // 与上不同点是可以只用一个花括号 +- `数据类型 数组名[][列数] = {数据1, 数据2, 数据3, 数据4};` // 同样可以只给列数,会自动计算行数 + +#### (1) 数组名用途 + +- 查看二维数组所占的内存空间 +- 获取二维数组首地址 + +### 5.3 数组打印 + +==遍历:c++11新标准引进的连个名为 begin 和 end 的函数== + +假设有一个数组arr1,可以通过 `std::end(arr1) - std::begin(arr1)`来获取数组的个数 + +```c++ +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]的值 + +```c++ +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",这个数组+下标的结果直接就是值 +``` + +- 一维: + +```c++ +double b[5] = { 100.2, 2.3, 3.4, 7.1, 50 }; // 最后一个元素我放整数好像也行吼 +cout << *b << endl; // 取出第一个元素 +cout << *(b + 1) << endl; // 取出第二个值 +cout << *(b + 9) << endl; // 越界取值,危险操作(一定不要) +``` + +```c++ +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也是可用的。 + +- 二维: + +```c++ +// 创建一个二维数组 +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 }; +``` + +- 最直接的通过下标打印出来 + +```c++ +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个整数的数组(数组名都是指针那种) + +```c++ +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;这是错的 + +```c++ +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语句处理多维数组,除了最内存的循环外,其它所有循环的控制变量都应该是引用类型。 + +```c++ +for (const int(&row)[4] : arr) { + for (int col : row) { + std::cout << col << ' '; + } + std::cout << std::endl; +} +``` + +### 5.4 数组补充(书) + +```c++ +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"; // 这是错的,没有空间放空字符 + +*** + +​ 一般来说,不能将数组的内容拷贝给其它数组作为初始值,也不能用数组为其它数组赋值(有些编译器支持数组的赋值,这是==编译器扩展==,但尽量还是避免使用者非标准特性) + +```c++ +int a[] = {0, 1, 2}; +int a1[] = a; // 错的 +a2 = a; // 错的 +``` + + + +```c++ +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表示函数没形参: + +- 隐式地定义空形参列表:int func1() {} +- 显示地定义空形参列表:int func1(void) {} + +返回类型:函数返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。 + +### 6.1 函数的定义 + +​ 语法: + +``` +返回值类型 函数名 (参数列表) { + 函数体内执行的语句; + + return 表达式; // 注意返回值的类型必须和函数名前一样 +} +``` + +​ Ps:如果不需要返回值,定义函数时可以写 void 函数名() {} 然后可以省略掉return语句 + +​ 例:定义一个加法函数,实现两个数的相加 + +```c++ +int add(int num1, int num2) { + int sum = num1 + num2; + return sum; +} +``` + +### 6.2 函数声明 + +作用:告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。 + +- 函数的声明可以是多次(但一般就是一次);但函数的定义只能有一次。 + +```c++ +// 函数声明 +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 < num2 ? num1 : num2; +} +``` + +### 6.3 函数的分文件编写(.h、.cpp) + +- 创建后缀名为.h的头文件;并在头文件里写函数的声明 +- 创建后缀名为.cpp的同名源文件;并在源文件中写函数的定义 +- 最后再搞一个其它名字的.cpp文件,再里面写main()函数,调用其它头文件 + +实例: + +1、`max.h` 自己定义的头文件 + +```c++ +#pragma once // 这行是定义头文件时自动生成的 +#include +using namespace std; + +int max_func(int num1, int num2); // 这是函数声明 +``` + +2、`max.cpp` 同名源文件 + +```c++ +#include "max.h" // 导入定义的头文件 + +// 这函数名必须跟头文件函数声明一样 +int max_func(int num1, int num2) { + return num1 > num2 ? num1 : num2; +} +``` + +Ps:系统带的头文件时`#include ` 自定义头文件导入`#include "max.h"` + +3、main函数入口的`mytest.cpp`文件 + +```c++ +#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对象,令其元素是指向该元素的指针。 + +解答: + +```c++ +int abc(int, int); +std::vector v; // 这是我写的(就是错的,不能这么来) +// 标准答案 +std::vector v1; +``` + +​ 特别注意:==将decltype==作用于某个函数时,它返回函数类型而非指针类型,因此我们需要显式的加上*以表明我们需要返回指针,而非函数本身。 + +接着练习:编写三个函数,并用vector对象保存这些函数的指针,然后再输出出来: + +```c++ +#include +#include +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 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: + +- 注意第15行:vector里类型的声明就是用decltype去的,且它返回的是函数类型,我们要指针,所以再加一个*;然后把函数指针放进vector时,印证了上面写的,取址符&要与不要都是一样的。 +- 第17、18行,v的auto类型应该就是函数指针,也再一下印证了上面写的调用时,可解引用也可不解引用。 +- 第22、23行,同理,第一个解引用只是把vector的指针解出来,第二个解引用可要可不要。 + +## 七、指针 + +==指针的作用:可以通过指针间接访问内存(我的理解是指针用来存放变量的内存地址)==(指针就是一个地址) + + - 内存编号是从0开始记录的,一般用十六进制数字表示 + - 可以利用指针变量保存地址 + +### 7.1 指针定义和使用 + +​ 定义语法:`数据类型 *变量名;` + +```c++ +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 指针所占内存空间 + +- 所有指针类型在32位下占的都是 ==4个字节== (我们一般用的开发都是4个字节) +- 所有指针类型在64位下占的都是 ==8个字节== + +```c++ +int *p; +cout << sizeof(int *) << endl; // 放指针类型;4 +cout << sizeof(p) << endl; // 放指针;4 +cout << sizeof(double *) << endl; +``` + +### 7.3 空指针和野指针 + +- 空指针:指针变量指向内存中编号为0的空间; + - 用途:初始化指针变量 —— `int *p = NULL;` + - 注意:空指针指向的内存是不可以访问的(也就是不能去解引用,语法上没错,但是是非法的,运行会报错) + +```c++ +int *p = NULL; + +// 访问空指针报错 +//内存编号0 ~255为系统占用内存,不允许用户访问 +cout << *p << endl; +``` + +- 野指针:指针变量指向非法的内存空间 + +```c++ +//指针变量p指向内存地址编号为0x1100的空间 +int * p = (int *)0x1100; +// 我的理解是 (int *) 定义一个整型指针,后面跟的是地址,跟 int a=10; int *p = &a; 有点像 +//访问野指针报错 +cout << *p << endl; + +int *ip; // 指针变量的申明;这是野指针,它还没有指向哪里 +``` + +>总结:空指针和野指针都不是我们申请的空间,因此不要访问。 + +### 7.4 const修饰指针 + +const修饰指针有三种情况 + +- const修饰指针 -- 常量指针 + + - ```c++ + 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修饰常量 -- 指针常量 + + - ```c++ + 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即修饰指针,又修饰常量 + + - ```c++ + int a = 10; + int b = 20; + const int * const p3 = &a; // 相当于只可读,都不能改 + ``` + +### 7.5 用指针遍历数组 + +```c++ +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 指针与函数(地址传递、值传递) + +```c++ +// 值传递 +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 数组通过指针到函数做处理 + +```c++ +#include +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中类型别名写到过的) + +> - 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对象 + +>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 指针、引用后续的补充知识点(==书==) + +- int &ref = 10; // 错误,引用类型的初始值必须是一个对象;且引用必须被初始化。 +- const int &ref = 10; //这就是OK的 + +*** + +生成空指针的办法(一般用来初始化指针): + +- int *p = nullptr; // nullptr是一种特殊的字面值 +- int *pi = 0; // 也是初始化,是一个空指针,但是没有指向任何对象 + +空指针也可以用if去判断: + `int *p=nullptr; if(p);` // 就是false + + + +加了const限定后: + +const int *p; // 正确,可以不初始化,因为后续可以p=&a; +int *const p1; // 错误,必须要初始化,后续不能p1=&a;了 + +*** + +指针也是一个对象,所以可以有指向指针的指针;但是引用本身不是一个对象,因此不能定义指向引用的指针,但是指针是对象,所以存在对指针的引用(即对指针取别名):int *p; int *&r = p;(这要从右往左读): + +- 离变量名r最近的符号(此例中为&)对变量的类型有最直接的影响,因此r是一个引用; +- 声明符的其余部分用以确定引用的类型是什么,此例中的符号*说明r引用是一个指针; +- 最后的int,说明r引用的是一个int指针。 + + + +==留个void* 指针的坑== + +void* 指针。后续要来说 (`void *`是从C语言那里继承过来的,可以指向任何类型的对象。 而其他指针类型必须要与所指对象严格匹配。) + + + +## 八、结构体 | 聚合类 + +概念:结构体属于用户 ==自定义的输数据类型==,允许存储不同的数据类型 + +==聚合类==:聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式,当一个类满足一下条件时,它就是聚合的: + +- 所有成员都市public的; +- 没有定义任何构造函数; +- 没有类内初始值; +- 没有基类,也没有virtual函数。 + +以上说的特殊初始化,就是下面的struct结构体的第二种初始方法: + struct Student stu2 = { "李四", 23, 148 }; + +### 8.1 结构体定义和使用 + +语法:`struct 结构体名 {结构体成员列表};` + +通过结构体创建变量的三种方式(先在上面创建出结构提了): + +- struct 结构体名 变量名; +- struct 结构体名 变量名 = {成员1值, 成员2值...}; +- 定义结构体时顺便创建变量 + +Ps:在创建实体变量时,可以省略掉关键词 struct;我也推荐第二种吧。 + +```c++ +#include +#include + +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,代表保存的上限) + + 步骤: + +- 定义一个结构体 +- 创建结构体数组 +- 给结构体数组中的元素赋值 + +```c++ +#include +#include + +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 结构体指针 + +​ 作用:就是通过指针访问结构体中的成员 + +- 核心就是通过指针使用操作符 `->` 去获得结构体属性,而不再是 `.` + +```c++ +用的是8.2中定义的 结构体Student +// 生成一个结构体变量 +struct Student stu1 = {"张三", 18, 100}; + +// 关键,定义的指针一定也是结构体的(同样,struct可省) +struct Student *p = &stu1; +cout << p->name << p->age << p->score << endl; +``` + +Ps:可以看下这个[demo](https://pintia.cn/problem-sets/14/problems/814) + +### 8.4 结构体嵌套结构体 + +​ 作用:结构体中的成员可以是另外一个结构体(需要注意的是,这是被嵌套的结构体需要先被定义)。 + +​ 例如:老师辅导学生,在一个老师的结构中,记录一个所带学生的结构体。 + +```c++ +#include +#include + +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 结构体做函数参数 + +```c++ +#include +#include + +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修饰函数的参数,这样就只可读吧,不可以修改。 + +```c++ +// 这个也可以加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 ` + +文件类型分为两种: + +- 文本文件:文件以文本的==ASCII码==形式存储在计算机中 +- 二进制文件:文件以文本的==二进制==形式存储在计算机中,用户一般不能直接读懂 + +操作文件的三大类(导入上面的头文件后,这三个类都可以用了): + +- ofstream:写操作(把output的内容记录下就是写) +- ifstream:读操作 +- fstream:读写操作 + +### 9.0 输入的检查控制(新增) + +一种输入的检查控制: + +```c++ +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 \ // 这里的 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 ` // +>2. 创建流对象:`std::ofstream ofs;` // 写还可以用这个类`std::fstream ofs;` +>3. 打开文件:`ofs.open("要存文件路径", 打开方式);` +>4. 写数据:`ofs << "写入的数据" << endl;` // 用左移运算符,换行号也可以这样写 +>5. 关闭文件:`ofs.close();` + +```c++ +#include + +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 ` +>2. 创建流对象:`std::istream ifs;` // 同样也可以用 `std::fstream ifs;` +>3. 打开文件,并要判断是否打开成功:`ifs.open("文件路径", 打开方式)` +>4. 读数据:四种读取方式,就用C++的第三种(在OpenGL的学子中出现了更好的做法) +>5. 关闭文件:`ifs.close();` + +```c++ +#include +#include +#include + +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原样读取数据,包括空格空行这样,得到的字符串和原样一模一样,强烈建议使用: + +```c++ +#include +#include +#include +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. 文件存在但为空: + + ```c++ + char buff; + ifs >> buff; // 读一个字符,使用.eof()函数,空的话就是true + if (ifs.eof()) { + std::cout << "文件是空的" << std::endl; + } + ``` + +建议的直接写法: + +```c++ +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 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 文件按行读取 + +```c++ +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是读写的字节数 + +```c++ +#include +#include + +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(&person) 来转换指针类型吧 + // 这里这样居然就直接写进去了自定义数据类型 + ofs.close(); +} +``` + +#### 9.2.2 读文件 + +二进制方式读文件主要利用==流对象==调用成员函数==read== + +函数原型:`std::istream& read(char *buff, int len);` // 注意是标准iostream中的`istream` + +​ 参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节 + +```c++ +#include +#include + +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 | istream, wistream 从流读取数据 | +| | ostream, wostream 向流写入数据 | +| | iostream, wiostream 读写流 | +| #include | ifstream, wifstream 从文件读取数据 | +| | ofstream, wofstream 向文件写入数据 | +| | fstream, wfstream 读写文件 | +| #include | istringstream, wistringstream
从string读取数据 | +| | ostringstream, wostringstream | +| | stringstream, wstringstream 读写string | + +- iostream 定义了读写流的基本类型; +- fstream 定义了读写命令文件的类型; +- sstream 定义了读写内存string对象的类型。 +- 为了支持使用宽字符的语言,宽字符版的类型和函数的名字以一个w开始,例如wcin、wcout和wcerr是分别对应cin、cout和cerr的宽字符版对象。上面的类名也是一样的,然后宽字符版和类型与函数与对应的普通char版本的类型是定义在同一个头文件中。 + +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可以是上表的其它流类型) + +- iostate 是一种机器相关的类型,提供了表达条件状态的完整功能; +- badbit 用来值流已崩溃; +- failbit 用来指出一个IO操作失败了; +- eofbit 用来值流已经到达了文件结束; +- goodbit 用来指出流未处于错误状态,此值保证为零。 (查看定义:好像这些状态都是int类型) + +若有一个流s: + +- s.eof() s的eofbit置位 +- s.fail() s的eofbit或babit置位 +- s.bad() s的badbit置位 +- s.good() s处于有效状态 +- s.clear() 将流s中所有条件状态复位,将流状态设置为有效,返回void +- s.clear(flags) 根据给定的flags标志位,将流s中对应条件状态复位,flags的类型为std::iostream::iostate这样的,返回void +- s.setstate(flags) 同上 +- s.rdstate() 返回流s的当前条件状态,返回值类型为 std::iostream::iostate 这样的。 + +注:前面几个多用于结合 if 判断,为真就是返回true。 + +#### 9.3.3 刷新输出缓冲区 + +endl、ends、flush + +```c++ +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 \ | 注意加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== + +```c++ +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格式 + +--- + +==指定整形值的不同进制==: + +```c++ +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: + +- 类似boolalpha,这些操纵符也会改变格式状态,他们会影响下一个和==随后所有的整形输出==,直至另一个操纵符又改变了格式为止; +- 操纵符oct、hex、dec只影响整形运算对象,浮点值的表示形式不受影响。 + +以上代码打印时,却并没有指明哪里各种进制的前缀,并不能一眼看出来: + +```c++ +std::cout << std::showbase << std::uppercase << std::hex + << "16进制:" << 20 << " " << 1024 + << std::nouppercase << std::noshowbase << std::dec << std::endl; +``` + +- std::showbase 让打印的值前面带有对应的进制(跟python的有点不一样) + - 前导0x表示十六进制 + - 前导0表示八进制 + - 无前导字符串表示十进制 +- std::uppercase 是让前导以大写的形式,默认是小写的; +- 注意上面的形式,特别是第3行,用完后立马就改回了。 + +--- + +==控制浮点数输出格式==:(指定打印精度) + +​ 默认:==浮点值按六位数字精度打印==;如果浮点值没有小数部分,则不打印小数点;标准库会选择一种可读性更好的格式:非常大和非常小的值打印为科学记数法形式,其它值打印为定点十进制形式。 + +可以控制浮点数输出三种格式: + +1. 以多高精度(多少个数字)打印浮点值;有2中控制方式 + 1. 调用IO对象的precision成员:precision成员是重载的,一个版本接收一个int值,将精度设置此值,并返回旧精度值。另一个版本不接受参数,返回当前精度值。 + 2. 使用==setprecision操纵符==来改变精度:setprecision操纵符接收一个参数,用来设置精度。 + Note:操纵符 setprecision 和其它一些控制输出的操纵符都定义在头文件`#include `中。 +2. 数值是打印为十六进制、定点十进制还是科学计数法形式; +3. 对于没有小数部分的浮点值是否打印小数点。 + +方式一:(核心是==std::cout.precision(12);==) + +```c++ +#include +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 +std::ostringstream out; +out.precision(12); +out << std::fixed << a_value; // std::fixed 代表用十进制 +std::cout << out.str() << std::endl; +``` + +方式二:(核心是==std::cout << std::setprecision(3);==)(此操纵符在[上面表](#9.4 书后补充:IO库再探)中有) + +```c++ +#include +#include +std::cout << std::setprecision(3); // 这里一定要这么写,操作符那种,不能只写std::setprecision(3); +std::cout << "当前精度:" << std::cout.precision() // 3 + << ", Value:" << std::sqrt(2.0) << std::endl; +``` + +注意: + +- 上面第3行的写法,然后std::setprecision(3);一定是要头文件`#include `的; +- 使用数学函数std::sqrt()一定要头文件`#include `,不然在vs中可以,在linux下一定报错,所以以后凡是用到数学函数一定要加这个参数。 + +--- + +==科学计数==: + +``` +std::cout << "科学计数法:" << std::scientific + << 100 * std::sqrt(2.0) << std::defaultfloat << std::endl; +``` + +==打印小数点==: + +```c++ +std::cout << 10.0 << std::endl; // 只会打印10,不会打印小数点 +std::cout << std::showpoint << 10.0 << std::noshowpoint << std::endl; +``` + +--- + +==输出补白==:(挺重要,就是把输出的格式对齐)(下面这些操纵符在[上面表](#9.4 书后补充:IO库再探)中有) + +- setw 指定下一个数字或字符串值得最小空间;(类似endl,不改变输出流的内部状态,只决定下一个输出的大小) +- left 用来左对齐输出,right用来右对齐输出(默认格式); +- internal 控制负数的符号位置,它左对齐符号,右对齐值,用空格填满所有中间空间; +- setfill 允许指定一个字符代替默认的空白开补白输出 + +```c++ +#include // 别忘了这个头文件 +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==时,一般: + +```c++ +char ch; +while (std::cin >> ch) + std::cout << ch; +``` + +这样循环只会执行4次,会跳过中间的空格以及可能的制表符和换行符。输入就是==abcd==,是连在一起的。 + +然后这些空白符都是可以读取的: + +```c++ +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来读取和写入一个字符: + +```c++ +char ch; +while (std::cin.get(ch)) + std::cout.put(ch); +``` + +此程序保留输入中的空白符,其输入与输出完全相同,它的执行过程与前面使用std::noskipws的程序完全相同。 + +--- + +==将字符放回输入流==: + +有时我们需要读取一个字符才能知道还未准备好处理它,这时,就希望将字符放回流中,标准库提供了三种方法: + +- peek:(使用 std::cin.peek() )返回输入流中下一个字符的副本,但不会将它从流中删除,peek返回的值仍然留在流中; +- unget:使得输入流向后移动,从而最后读取的值又回到流中,即使不知道最后从流中读取什么值,仍然可以调用unget; +- putback:是更特殊版本的unget,它退回从流中读取的最后一个值,但它接收一个参数,此参数必须与最后读取的值相同。 + +一般情况下,在读取下一个值之前,标准库保证我们可以退回最多一个值。即,标准库不保证在中间不进行读取操作的情况下能连续调用putback或unget。 + +--- + +==从输入操作返回的int值==: + +函数peek和无参的get版本都以int类型从输入流返回一个字符,这些函数返回int的原因:可以返回文件尾标记。 + +使用char范围中的每个值来表示一个真实字符,因此,取值范围中没有额外的值可以用来表示文件尾。 + +​ 返回int的函数将他们要返回的字符先转换为unsigned char,然后再将结果提升到int。因此,即使字符集中有字符映射到负值,这些操作返回的int也是正值(前面类型转换讲过)。而标准库使用负值表示文件尾,这样就可以保证与任何合法字符的值都不同。==头文件cstdio定义了一个名为EOF的const,可以用它检测从get返回的值是否是文件尾: + +```c++ +int ch; // 使用一个int,而不是一个char来保存get()的返回值 +// 循环读取并输出输入中的所有数据 +while ((ch = std::cin.get()) != EOF) + std::cout.put(ch); +``` + +这与上面一个程序完成相同的工作,唯一不同的是用来读取输入的get版本不同。 + +--- + +==多字节操作==:例子可以看[这里](#9.1.2 读文件)的第二种示例 + +​ 一些未格式化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的默认值为文件尾 +> +> - ```c++ +> #include +> #include +> 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数组,用来保存数据。两个函数都是 一直读取数据,直至下面条件之一发生: + +- 已读取了size-1个字符; +- 遇到了文件尾; +- 遇到了分隔符。 + +​ 两个函数的差别是处理分隔符的方式:get将分隔符留作istream中的下一个字符,而getline则读取并丢弃分隔符。然后无论哪个函数都不会将分隔符保存在sink中。 + +确定读取了多少个字符: + 某些操作从输入读取未知个数的字节,可以调用gcount来确定最后一个未格式化输入操作读取了多少个字符。应该在任何后续未格式化输入操作之前调用gcount,特别是将字符退回流的单字符操作也属于是未格式化输入操作。如果在调用gcount之前调用了peek、unget或putback,则gcount的返回值为0。 + + + +书上的一个警告:一个常见的错误是本想从流中删除分隔符,但却忘了做。 + +书上的一个警告:一个常见的编程错误是将get或peek的返回值赋予了一个char而不是一个int。例如,在一台char被实现为unsigned char的机器上,下面的循环永远不会停止(这个不是那么理解,还是感觉怪怪的): + +```c++ +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函数通常用来记住一个位置,以便稍后再定位回来: + +```c++ +#include // 下面这些类,一定要这个头文件 +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开始。统计偏移量时必须播包含每行末尾不可见的换行符。 +​ 下面程序时逐行读取文件,对每一行,将递增计数器,将刚刚读取的一行的长度加到计数器上,则此计数器即为下一行的其实地址: + +```c++ +#include +#include +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; +} +``` + +解读: + +- 由于程序还在想输入文件写入数据,不能通过文件尾来判断是否停止读取,而是在达到元数据的末尾时停止(所以要记住原文件未位置); +- 12行,seek用法:seek到距文件起始位置偏移量为0的地方,即将读标记重定位到文件起始位置; +- 循环体先将当前位置记录在mark中,是为了在输出下一个偏移量化后再退回来,接着调用seekp将写标记重定位到文件尾,输出计数器的值,然后再调用seekg回到记录在mark中的位置,回退到原位置后,就准备好检查下一次循环条件了; +- 每步循环都会输出下一行的偏移量,因此最后一步循环负责输出最后一行的偏移量。我们还需要在文件尾输出一个换行符,与其它写操作一样,在输出换行符之前还要调用seekp来定位到文件尾。 \ No newline at end of file diff --git "a/C++/2 c++\346\240\270\345\277\203\347\274\226\347\250\213.md" "b/C++/2 c++\346\240\270\345\277\203\347\274\226\347\250\213.md" new file mode 100644 index 0000000..a88a6c6 --- /dev/null +++ "b/C++/2 c++\346\240\270\345\277\203\347\274\226\347\250\213.md" @@ -0,0 +1,4024 @@ +# C++核心编程 + +## 一、内存分区模型 + +C++程序在执行时,将内存大方向划分为==4个区域== + +- 代码区:存放函数体的二进制代码,由操作系统进行管理 (程序运行前) +- 全局区:存放==全局变量==和==静态变量==以及==常量== (程序运行前) +- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等 (程序运行后才有) +- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收 (程序运行后才有) + +内存四区意义:不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程 + +### 1.1 程序运行前 + + 在程序编译后,生成了exe可执行程序,==未执行该程序前==分为两个区域 + +- 代码区: + - 存放CPU执行的机器指令 + - 代码区是==共享==的,目的是对于频繁执行的程序,只需要在内存中有一份代码即可 + - 代码区是==只读==的,为了防止程序意外地修改了它的指令 +- 全局区: + - 全局变量和静态变量存放于此 + - 全局区还包含了常量区-——字符串常量和其它常量(也就是const修饰的全局变量) + - 关于static,可以去看看与之相关的==未命名的命名空间==(也叫匿名命名空间)的作用。 + - 该区域的数据在程序结束后由操作系统进行释放 + +```c++ +#include +#include + +using namespace std; + +int a = 10; +int b = 20; // 全局变量 + +const int c_g_a = 10; +const int c_g_b = 20; + +int main() { + cout << (int)&a << endl; + cout << (int)&b << endl; // 在全局区 + + static int s_a = 10; + static int s_b = 20; // 用static关键词修饰的就是静态变量 + cout << (int)&s_a << endl; + cout << (int)&s_b << endl; // 在全局区,跟上面地址相似 + + cout << (int)&c_g_a << endl; + cout << (int)&c_g_b << endl; // 在全局区,const修饰的全局变量,跟上面地址相似 + + cout << (int)&"hello" << endl; // 这是字符串常量,这在全局区,跟上面地址相似 + + // 下面的就不在全局区了 + string ss = "world"; + cout << (int)&ss << endl; // 注意这应该是字符串变量,就不在全局区,地址也不相似 + + const int c_l_a = 10; + const int c_l_b = 20; + cout << (int)&c_l_a << endl; + cout << (int)&c_l_b << endl; // 这也不在全局区,这是const修饰的局部变量,局部的应该都不在全局区 + + system("pause"); + return 0; +} +``` + +### 1.2 程序运行前 + +- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等。 + + - ```c++ + int *func(int b) { // 返回地址的,就这样带*定义指针 + b = 20; // 函数形参也是在栈空间 + int a = 10; + return &a; // 尽量别这样用,这是栈数据,会被系统自动回收的 + } + + int main() { + int *p = func(3); // 返回地址的结果,自然也要用指针去接收 + cout << *p << endl; // 解引用,10,是因为系统做了一个保留, + cout << *p << endl; // 再来就是乱码了,已经回收了 + + system("pause"); + return 0; + } + ``` + + ​ 注意:==不要返回局部变量的地址==,栈区开辟的数据由编译器自动释放。 + +- 堆区:由程序员分配释放,若程序员不释放,程序结束时由操作系统回收;在C++中主要利用==new==在堆区开辟内存。 + + - ```c++ + int *func() { + // 这个temp指针还是在栈空间,指向的数据20是在堆空间 + int *temp = new int(20); // 创建一个整型的20,返回的就是堆数据地址,所以要用指针来接收 + // 注意是 new int(20) 有个小括号把具体数值括起来 + return temp; // 返回的就是指针,就是地址 + } + + int main() { + int *p = func(); // 指针去接收地址 + cout << *p << endl; // 20 + cout << *p << endl; // 20 + + // 用完后使用delete释放堆区数据 + delete p; // 后面就不能再操作指针p了,不然就是非法操作了 + + system("pause"); + return 0; + } + ``` + +### 1.3 new操作符 + +​ C++中利用==new==操作符在堆区间开辟数据,手动释放利用操作符==delete==, + +- int *p = new int; // 默认初始化,\*p的值未定义 + int *p = new int(); // 值初始化为0,\*p为0 + int \*p = new int(5); // \*p为5 + + - 数组: + int *arr = new int[6]; // 只是开辟长度为6的数组,值都没定义 + int *arr = new int\[6](); // 开辟长度为6的数组,值都初始化为0,这不能初始化其它值。 + +- std::vector vec = new std::vector{0, 1, 2, 3}; //后面跟的就这些些的初始化 + +- 基本语法:具体见上的例子 + +- 开辟数组 + + - ```c++ + int *p = new int[10]; // 创建容量为10的数组;返回的地址就是数组的首地址 // 注意这int[10]是创建长度为10的数组;int(10)才是整数10 + for (int i = 0; i < 10; i++) { + p[i] = i + 100; // 跟数组用法一样,用索引去赋值、取值 + cout << p[i] << endl;; + } + // 释放数组,delete 后面要加一个 `[]` + delete[] p; + ``` + +Ps:针对数组是`new int[个数]`; 针对单个数字是`new int(5)` (一个中括号,一个小括号) + +--- + +针对图像内存地址申请,看到海康相机的SDK,可以参考一下: + +```c++ +{ + unsigned char *pData = nullptr; + unsigned char *pDataForBGR = nullptr; + unsigned int nDstBufferSize = nWidth * nHeight * 4 + 2048; // 随便给的值,大小要根据具体情况而定 + // 原来的申请方式。一:(数据包大小的数据类型一般为 unsigned int) + pData = (unsigned char *)malloc(sizeof(unsigned char) * 数据包大小); + pDataForBGR = (unsigned char *)malloc(nDstBufferSize); + free(pData); + free(pDataForBGR); + // 用c++的方式。二: + pData = new unsigned char[数据包大小]; + pDataForBGR = new unsigned char[nDstBufferSize]; + delete[] pData; + delete[] pDataForBGR; +} +``` + +说明: + +- 这就是申请数组内存的方式,c++里自己还是用c++的new吧,相机里的sdk还是用的malloc。 +- malloc是函数,需要头文件“”支持,new则是关键字,需要c++编译器。 + +### 1.4 内存泄漏 + +申请的堆空间,记得手动去释放,不然容易造成内存泄漏 + +```c++ +#include // 下面Sleep(1)需要用到的包 +// 泄露(要看泄露效果的话就把delete屏蔽掉) +void leak() { + for (int i = 1; i < 1000000; i++) { + int*a = new int[100000]; // 这中括号是是数组;申请100000个整形空间 + delete[] a; // 这是中括号,释放内存就要有[] + + //int a[100000] // c++是先申请空间(可以先不给值);这一行是看栈空间不手动释放也不会泄露 + Sleep(1); // 休眠一秒 + } +} +``` + +## 二、引用 + +### 2.1 引用的基本使用 + +作用:给变量起别名 + +语法:`数据类型 &别名 = 原名;` // 注意这个数据类型要和原名的类型一样 + +```c++ + int a = 10; + int &b = a; // 引用必须初始化 + cout << "a:" << a << endl; // 10 + cout << "b:" << b << endl; // 10 + + int c = 20; + // 起别名后,就可以把b看做a了,所有对b的操作,也会改变a的 + b = c; // 这就是赋值 + cout << "a:" << a << endl; // 20 + cout << "b:" << b << endl; // 20 +``` + +### 2.2 引用做函数参数 + +​ 这也是可以直接修改传进来的参数的 + +```c++ +// 这就是定义的引用传递;;相当于给传进来的参数起别名 +void swap(int &a, int &b) { + int temp = a; // 有了别名后,直接对别名的操作就是对原数据的操作 + a = b; + b = temp; +} +int main() { + int x = 10; + int y = 20; + swap(x, y); // 引用传递的时候直接把x、y传进去就行了 + cout << "a:" << x << endl; // 20 + cout << "b:" << y << endl; // 10 + + system("pause"); + return 0; +} +``` + +### 2.3引用做函数的返回值 + +```c++ +// 必须要加这个引用,不然返回10,下面就是 int &ref = 10; 这是错的,引用不能是对一个数字常量,就会报错 +int& test01() { + int a = 10; // 栈区 + return a; // 本来返回的a这个值,但是函数返回那里加了引用,返回的就是a的引用 +} +int& test02() { + static int a = 20; // 加了static关键字后,成了静态变量,存放在全局区,全局区的数据在程序结束后系统释放 + return a; +} +int main() { + int &ref = test01(); // 函数返回的是引用,所以要用一个引用去接收 + // 千万不要引用一个局部变量,它在栈区,用了一次后就会被系统释放 + cout << "ref:" << ref << endl; // 10 ,系统做了一次保留 + // cout << "ref:" << ref << endl; // 随机乱码了,错的 + + int &a_ref = test02(); + cout << "a_ref:" << a_ref << endl; // 20 + cout << "a_ref:" << a_ref << endl; // 20 这就没问题 + + + // 函数返回的引用可以做左值 + test02() = 1000; + // 这里就是返回的a,然后重赋值了1000;;而上面int &a_ref = test02(); 又给这个起了别名叫a_ref + // 前面重新赋值了,那对应的别名a_ref也等于1000,因为这俩指向的是同一个地址 + cout << "a_ref:" << a_ref << endl; // 1000 + + system("pause"); + return 0; +} +``` + +Ps:函数不要返回局部变量的引用;函数返回的引用可以做左值,进行赋值操作 + +### 2.4引用的本质 + +本质:引用的本质在C++内部实现就是一个==指针常量== + +```c++ +//发现是引用,转换为 int* const ref = &a; +void func(int& ref){ + ref = 100; // ref是引用,转换为*ref = 100 +} +int main(){ + int a = 10; + + //自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改 + int& ref = a; + ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20; + + cout << "a:" << a << endl; + cout << "ref:" << ref << endl; + + func(a); + return 0; +} +``` + +### 2.5 常量引用 + +​ 作用:常量引用主要用来修饰形参(函数传递),防止误操作 + +```c++ +void showValue(const int &value) { + //value = 30; // 加了const就不准再赋值了 + cout << value << endl; +} +int main() { + //int &ref = 10; // 引用本身需要引用一个合法的内存空间,这是错误的。 + const int &ref = 10; // 这是可以的,但一般更多还是用在定义函数时 + // 这里面也是编译器优化代码,过程是 int temp=10; const int &ref=temp; temp就是系统任意起的一个名字 + + //ref = 20; // 加入const后不可修改变量 + cout << ref << endl; + + // 函数中利用常量引用防止误操作修改实参(就相当于是只读) + int a = 20; + showValue(a); + + system("pause"); + return 0; +} +``` + +## 三、函数提高 + +### 3.1 默认参数 + +在C++中,函数的形参列表中的形参是可以有默认值的 + +```c++ +// 函数声明和函数实现,如果需要默认值,只要其中任意一个写就好了 +int add(int a=10, int b=20); + +int add(int a, int b) { + return a + b; +} +int main() { + int out = add(); + cout << out << endl; + system("pause"); + return 0; +} +``` + +注意:当在头文件写了默认参数 int add(int a, int b=20); 那么在.cpp文件实现时,默认参数就不写了,只能写成int add(int a, int b) {/*...*/} 绝对不能写成int add(int a, int b=20){},这样会引发错误“错误 C2572 , 重定义默认参数 : 参数 1” + +### 3.2 函数占位参数 + +C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置 + +```c++ +//函数占位参数 ,占位参数也可以有默认参数 +void func(int a, int=20) { + cout << "this is func" << endl; +} +int main() { + func(10); //占位参数必须填补(在没有默认参数时) + system("pause"); + + return 0; +} +``` + +>PS:在运算符重载,区分前置++与后置++,用作后置++的占位参数 + +### 3.3 函数重载 + +作用:函数名可以相同,提高复用性 + +函数重载需满足以下条件: + +- 同一个作用域下; +- 函数名称相同; +- 函数参数 ==类型不同== 或者 ==个数不同== 或者 ==顺序不同== // 简言之在调用函数时能通过传参分辨出来。 + +Ps:若函数名、传参都一模一样,仅仅是定义函数时返回类型不同是不行的(在调用时也无法区分到底是选择的哪个。) + +```c++ +//函数重载需要函数都在同一个作用域下(现目前都是在一个作用域) +void func() +{ + cout << "func 的调用!" << endl; +} +void func(int a) +{ + cout << "func (int a) 的调用!" << endl; +} +void func(double a) +{ + cout << "func (double a)的调用!" << endl; +} +void func(int a ,double b) +{ + cout << "func (int a ,double b) 的调用!" << endl; +} +void func(double a ,int b) +{ + cout << "func (double a ,int b)的调用!" << endl; +} +``` + +>上面很明显可以在调用函数时通过传参的不同来区别。 + +#### 注意点 + +- 当引用作为重载条件 + + ```c++ + void func(int &a) { // 传变量a进来就是 int &a = a;是可以的,但是 int &a = 10;是非法的 + cout << "调用的是:func1(int &a)" << endl; + } + void func(const int &a) { // 这个对于上面写的两种都是可以的:const int &a = 10; 系统帮忙创建了一个中间变量名 + cout << "调用的是:func2(const int &a)" << endl; + } + + int main() { + int a = 10; + func(a); // a是一个变量,上面两种都是可以的,都在默认是第一种 + + func(20); // 只有 常量引用 才可以 + system("pause"); + return 0; + } + ``` + +- 函数重载碰到函数默认参数 + + ```c++ + // 下面这俩在语法上是通过的,但是由于传参的问题,容易产生二义性 + void func(int a, int b=10) { + cout << "这是:func(int a, int b=10)" << endl; + } + void func(int a) { + cout << "这是:func(int a)" << endl; + } + + int main() { + func(10, 20); // 这是可以的,明显可以区分 + //func(10); // 这就不行,因为两个都可以,产生了二义性 + + system("pause"); + return 0; + } + ``` + + + +## 四、类和对象 + +一种类定义的方式: + +```c++ +typedef struct { + int age; + const char* name; +}a_type; // 可以像这样定义一个数据类型 +// 同样这里的struct还可以换成一个enum枚举,形式是一样 +// 这里的struct后面没跟一个类型名,也可以跟一个,意义不大,enum也是一样的 + +int main(int argc, char** argv) { + a_type person; + person.age = 42; + person.name = "zhangsan"; + return 0; +} +``` + +### 4.0 =default + +```c++ +struct My_print { + My_print() = default; // 使用合成的默认构造函数,直接用 My_print(); 好像区别不大 + My_print(const My_print&) = default; // 拷贝构造函数 + My_print& operator=(const My_print&); // 拷贝赋值运算符 + ~My_print() = default; +}; +``` + +​ 在C++11新标准中,如果我们需要默认行为,那么可以通过在参数列表后面写上`= default`来要求编译器生成构造函数(我的理解是自己写了拷贝构造函数,就不会生成默认构造函数,加上就是代表即使有拷贝构造函数,也要编译器生成合成的默认构造函数),其中 = default 既可以和声明一起出现在类内部,也可以作为定义出现在类的外部。和其它函数一样,如果 = default 在类的内部,则默认构造函数是内联的,如果它在类外部,则该成员默认下不是内联。 + +​ class 和 struct定义类的唯一区别就是默认的访问权限,struct默认是public,而class默认是private。 + +### 4.1 封装、权限 + +封装的意义: + +- 将属性和行为作为一个整体,表现生活中的事物 +- 将属性和行为加以权限控制(public、protected、private) + +```c++ +// 这3个权限里的东西,类内都可以访问 +class Student { +// 公共权限 +public: // public类外也可访问 + string name; + +// 保护权限 +protected: // 类外不能访问,继承这个类的子类可以访问 + string gender = "male"; + +// 私有权限 +private: // 类外不可以访问,继承这个类的子类也不可以访问 + int id=15; + void showGender() { + cout << "这个人的性别是:" << gender << endl; + } + +public: + void showStudent() { // 类内的函数都可以访问 + cout << "姓名:" << name << endl; + cout << "性别:" << gender << endl; + cout << "学号:" << id << endl; + } + // 可以传指针、引用、值传递 + void demo(Student *s1) { // 可以放其他类作为参数,甚至自己这个类 + + } +}; + +int main() { + // 实例化 + Student s1; + s1.name = "张三"; + //s1.id = 123; // 这就是错的,访问不了 + //s1.showGender(); // 函数一样没有权限 + s1.showStudent(); + + system("pause"); + return 0; +``` + +Ps:类中的函数,定义的参数,可以是一个类的实例化那种,看上面的24行代码 + +* `protected` : 基类和和其派生类还有友元可以访问。 +* `private` : 只有基类本身和友元可以访问。 + +### 4.2 struct和class区别 + +​ 在C++中,struct和class==唯一区别==就在于在没有权限声明时 ==默认的访问权限不同== + +- struct 默认权限为 public +- class 默认权限为 private + +#### 4.2.1 枚举:enum + +​ 枚举数据类型跟struct和class有点相似,一般两种定义方式,跟struct类似。 + +- ```c++ + // 1、定义 + enum direction {east, west, north, south }; + // 像是实例化,下面这三种都是可以的 + direction dir = west; + direction dir(test); + direction dir; dir = west; + ``` + +- ```c++ + // 2、定义 + enum direction {east, west, north, south } dir, dir1, dir2; + // 这种甚至可以省略掉类型名`direction` + enum {east, west, north, south } dir, dir1, dir2; + dir = west; + // 还可以将枚举变量赋值给整形 + int i = north; // 那么 i 就等于 2 + ``` + + >这时候 cout << dir ,打印的结果都是`1`;枚举类型默认以此从 0、1、2、3 往下排;这种就常搭配switch-case(只能是字符型和整形)使用,就很贴合。 + +还可以修改枚举数据类型的默认值: + +```c++ +enum { east=10, west=20, north=30, south=40 } dir; + dir = south; + switch (dir) { + case north: + cout << "这是北半球" << endl; + break; + case south: + cout << "这是南半球:" << south << endl; + break; + } +``` + +甚至可以只是 `enum {east=10, west=20};` 然后直接使用里面的值 + +​ 使用枚举值的一个demo: + +```c++ +// 1、枚举值放全局变量 +enum Department { scheme = 10, arts = 20, develop = 30 }; + +class Person { +public: + // 2、枚举值放函数类 + //enum Department { scheme = 10, arts = 20, develop = 30 }; + + string m_Name; + int m_Salary; // 薪酬 + Department m_Depart; // 部门 + Person(string name, int salary, Department depart) { + this->m_Name = name; + this->m_Salary = salary; + this->m_Depart = depart; + } +}; +void test01() { + // 1、枚举值放全局变量 + Person p1("张三", 200, Department::arts); + Person p2("李四", 400, arts); + + // 2、枚举值放函数类 + //cout << Person::arts << endl; // 20 + //cout << Person::Department::develop << endl; // 30 + // 这俩都是可以的,,类名+枚举值也行 + //Person p1("张三", 200, Person::arts); + //Person p2("李四", 400, Person::Department::develop); +} +``` + +> ​ Ps:1是一种情况,一起放开或注释;2是另一种情况,也是一起放开或注释; +> +> ​ 枚举值可以直接使用,或前面加上其所在作用域,枚举值的"类名"可加可不加。 + +--- + +以下是书上的内容补充:effective中建议使用enum class而非enmu + +​ C++11新标准引入了==限定作用域范围的枚举类型==,定义的一般形式是:首先是关键字 enum class(或者等价地使用enum struct),随后枚举类型名字以及枚举成员: +​ `enum class open_model {input, output, append};` +​ 这就是定义了一个名为open_models的枚举类型,有三个枚举成员。 + +还有一种==不限定作用域的枚举类型==,定义时省略掉关键字class(或struct),枚举类型的名字是可选的。 + +- enum color {red, yellow, green}; // 不限定作用域的枚举类型 + enum {east=10, west=20, north=30}; // 未命名的、不限定作用域的枚举类型 + +限定作用域范围的枚举类型与不限定作用域的枚举类型比较直接的==区别==: + +- open_model om = append; // 错误,append这一枚举成员不在有效的作用域中 + open_model om = open_model::append; // 正确(这只能这么访问) + +- color eyes = yellow; // 正确,不限定作用域范围的枚举值成员位于有效的作用域中 + color eyes = color::yellow; // 依然正确 +- 正是因为这不限定作用域的区别,那么: + - enum class new_model {input, output, append}; // 正确:枚举成员被隐藏了 + - enum other_color {red, yellow, green}; // 错误:重复定义了枚举成员(上面定义了color) + - 说明:前者限定作用域范围是需要加上类型名才能访问,所以不同类型名,可以重复定义相同的枚举成员;后者不限定作用范围的是可直接使用枚举成员,所以相同的枚举成员就会重复定义,就错了。、 + - 还有一个==区别==: + - int i = color::red; // 正确:不限定作用域的枚举成员可以隐式地转换成int + - int j = open_model::input; // 错误:限定作用域法人枚举类型不会进行隐式转换,所以需要写成static_cast(open_model::input) + + + +C++11新标准中,可以在enum的名字后加上冒号以及想在enum中使用的类型:(注意后缀 UL、ULL 这些后缀) + `enum intValues : unsigned long long {a=100, b=42946UL, c=15484561651578ULL};` + +​ 在不指定enum的潜在类型,默认下:限定作用域的enum成员类型是int;对于不限定作用域的枚举类型来说,其不存在默认类型。 + +还有一个注意点,若是要提前声明enum,像上面intValues这种不限定作用域的就一定要指明类型: + enum intValues : unsigned long long; // 提前声明,一定要指明类型(因为没有默认类型) + enum class open_model; // 提前声明,可以不指定类型(因为限定作用域的有默认类型int) + +### 4.3 对象的初始化和清理 + +#### 4.3.1 构造函数、析构函数 + +​ c++利用了==构造函数和析构函数==解决对象的==初始化和清理==,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。 + +​ 对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和解析,编译器会提供,但提供的都是空实现(即有这两个函数,但是里面没有实体代码)。 + +- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。 +- 析构函数:主要作用在于==对象销毁前==系统自动调用,执行一些清理工作。 + +==构造函数语法==:`类名() {}` + +1. 构造函数,没有返回值,也不写void; +2. 函数名称与类名相同; +3. 构造函数可以有参数,因此==可以发生重载==; +4. 程序在调用对象时,会自动调用构造函数,且只会调用一次。 + +==析构函数语法==:`~类名() {}` // 就是多了一个 ~ + +1. 析构函数,没有返回值也不写void; +2. 函数名称与类名相同,在名称前加上符号 ~ +3. 析构函数==不可以有参数==,因此==不可以发生重载==; +4. 程序在对象销毁前会自动调用析构,且只会调用一次。 + +```c++ +class Person { +public: + Person() { + cout << "这是会自动调用的构造函数!" << endl; + } + ~Person() { + cout << "这是析构函数" << endl; + } +}; +void test01() { + Person p1; // 这创建的空间就是在栈上 +} +int main() { + test01(); // 这个函数执行完,就会调用 析构函数 + + Person p2; // 这种先调用 构造函数, 再执行下一句就卡住了,任意键后,就会看到这个调用的 析构函数,就是在return之前进行释放 + cout << "看这个在哪里" << endl; // 在这对象调用析构函数之前 + system("pause"); + return 0; +} +``` + +#### 4.3.2 构造函数的分类及调用 + +两种分类方式: + +​ 按参数分为:有参构造和无参构造 + +​ 按类型分为:普通构造和拷贝构造 + +三种调用方式: + +​ 括号法、显式法、隐式转换法 + +实例: + +```c++ +// 分类 +class Person { +public: + // 构造函数是可以重载的 + // 这就是无参构造函数,又称为默认构造函数 + Person() { + cout << "这是无参构造函数" << endl; + } + Person(int a) { + age = a; // 构造函数就是来初始化 + cout << "这就是有参构造函数" << endl; + } + + // 拷贝构造函数(一般是就是把属性进行拷贝);可以把Person看做定义时的数据类型 + // const是为了防止数据被修改,然后对象传进来一定要是`引用`以及const; + Person(const Person &p1) { + cout << "这就是拷贝构造函数" << endl; + age = p1.age; // 把对象p1的age赋值过来 + } + // 与拷贝构造函数相似用的还有`拷贝赋值运算符` + Person& operator=(const Person &p1) { + cout << "这就是拷贝赋值运算符" << endl; + age = p1.age; // 把对象p1的age赋值过来 + } + + // 析构函数是不能重载的 + ~Person() { + cout << "这是析构函数" << endl; + } +public: + int age; +}; + +// 调用 +void test01() { + // 1、括号法 + Person p1; // 默认构造函数调用 + // 这里一定不要给括号,即不要写成了Person p1(); 因为编译器认为是函数的声明,不是在创建对象,就不会执行对象的初始化(数据类型Person,函数名p1()) + + Person p2(10); // 有参构造函数(给值初始化) + Person p3(p2); // 拷贝构造函数(把上面的一个对象p2直接传进来就好了) + cout << p2.age << endl; // 10 + cout << p3.age << endl; // 10 +} + +void test02() { + // 2、显式法 + Person p1; + Person p2 = Person(10); // 其实差不多,知道这么个存在就好 + Person p3 = Person(p2); + + Person(20); // 这是`匿名对象`,即初始化对象时没有命名, + // 匿名对象 就`函数名()`,构造函数无参数时,有参数就是上面那行 + // 这在后面函数调用运算符`()`重载时用得到 + // 特点:当前行执行结束后,系统会立即回收掉匿名对象(没名后续本也就无法调用) + cout << "***再看看这行的顺序***" << endl; // 在这个对象的调用析构函数之后 + + // 错误:不用利用拷贝函数来 初始化匿名对象, + // Person(p3); // 这种是等于 Person (p3) == Person p3;前面又定义了p3,就会报错重定义(知道一下就行) +} + +void test03() { + // 3、隐式转换法 + Person p1 = 10; // 编译器自动转成了 Person p1 = Person(10); 自动转成显式法了 + Person p2 = p1; // 这是拷贝构造,上面一行就是有参构造 +} +``` + +#### 4.3.3 拷贝构造函数调用时机 + +C++中拷贝构造函数调用时机通常有三种情况 + +- 使用一个已经创建完毕的对象来初始化一个新对象 +- 值传递的方式给函数参数传值 +- 以值方式返回局部对象 + +Person类使用4.3.2里面的。 + +```c++ +// 1.使用一个已经创建完毕的对象来初始化一个新对象 +void test01() { + Person p1(30); + Person p2(p1); // 调用拷贝构造(显示法) + Person p3 = p1; // 调用拷贝构造(隐式转换法) +} + +// 2.值传递的方式给函数参数传值 +void doWork2(Person p) { +} +void test02() { + Person p1; // 无参构造函数 + doWork2(p1); // 这就是前面讲的值传递,是会复制一个副本出来,所以会调用拷贝构造函数 +} + +// 3.以值方式返回局部变量 +Person doWork3() { // 定义的函数,注意返回值类型 + Person p1; // 也是拷贝构造函数 + cout << (int)&p1 << endl; + return p1; +} +void test03() { + Person p2 = doWork3(); + cout << (int)&p2 << endl; // 因为拷贝,所以两个的地址是不一样的 +} +``` + +#### 4.3.4 构造函数调用规则 + +默认情况下,C++编译器至少给一个类添加3个函数 + +- 默认构造函数(无参,函数体为空) +- 默认析构函数(无参,函数体为空) +- 默认拷贝构造函数,会默认对所有属性进行值拷贝 + +构造函数调用规则如下: + +- 如果用户定义有参构造函数,c++不再提供默认无参构造,但还是会提供默认拷贝构造 +- 如果用户定义拷贝构造函数,c++不会再提供其它的构造函数了 + +```c++ +class Person { +public: + // 自定义了有参构造,那就不会再有默认构造了, + // 虽然没自定义拷贝构造,但是编译器会自动提供,并会把属性都进行复制 + Person(int a) { + age = a; // 构造函数就是来初始化 + cout << "这就是有参构造函数" << endl; + } + + // 析构函数是不能重载的 + ~Person() { + cout << "这是析构函数" << endl; + } +public: + int age; +}; + +void test01() { + // 这就是错的,系统不再提供默认构造函数 + //Person p; + Person p1(18); + // 虽然没再自定义拷贝构造函数,编译器会自动提供,并把属性复制了 + Person p2(p1); + cout << "p1的年纪:" << p1.age << endl; // 18 + cout << "p2的年纪:" << p1.age << endl; // 18 +} +``` + +#### 4.3.5 深拷贝和浅拷贝 + +浅拷贝:简单的赋值拷贝操作 + +深拷贝:在堆区间重新申请空间,进行拷贝操作 + +```c++ +class Person { +public: + Person(int a, int b) { + age = a; + height = new int(b); + cout << "这就是有参构造函数" << endl; + } + // 可以先试试不要自定义拷贝构造函数,这代码回再最后一行报错的 + Person(const Person &p) { + //height = p.height; // 这会是系统提供的默认拷贝构造函数的写法,简单的浅拷贝 + height = new int(*p.height); // 自己把数据解引用出来,再开辟一个新的堆空间:深拷贝 + } + + ~Person() { + // 析构函数就是在创建的对象回收前调用;那就刚好用来回收手动创建的new开辟的堆空间 + if (height != NULL) { + delete height; + height = NULL; // 防止野指针出现再做一个置空的操作 + } + cout << "这是析构函数" << endl; + } +public: + int age; + int *height; +}; +``` + +>Ps:如果有属性在堆区间开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。 + +#### 4.3.6 ==构造函数初始化列表== + +c++提供了初始化列表语法,用来初始化属性 + +语法:`构造函数(): 属性1(值1), 属性2(值2) {} ` 或者 `构造函数(int a, int b): 属性1(a), 属性2(b) {} ` + +```c++ +class Person { +public: + // 初始化列表的方式初始化 + //Person() :m_A(10), m_B(20), m_C(30) { // 这样初始化时就写死了 + + //} + Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) { + + } // 这样里面也不用写赋值操作了 + void show() { + std::cout << "m_A = " << m_A << std::endl; + std::cout << "m_B = " << m_B << std::endl; + std::cout << "m_C = " << m_C << std::endl; + } +public: + int m_A, m_B, m_C; +}; +int main() { + Person p(10, 20, 30); + p.show(); + return 0; +} +``` + +--- + +子类的构造函数在初始化列表时可以同时初始化父类的构造函数,如: + +```c++ +class GameObject { +public: + bool IsSolid; + bool Destroyed; + // constructor(s) + GameObject(); + GameObject(glm::vec2 pos, glm::vec2 size); +}; +// 子类 +class BallObject : public GameObject { +public: + // 球的状态 + GLfloat Radius; + GLboolean Stuck; + + BallObject(); + BallObject(glm::vec2 pos, GLfloat radius); +}; + +// 子类实现 // 注意这里还初始化了其父类的构造函数 +BallObject::BallObject() : GameObject(), Radius(12.5f), Stuck(true) { } +BallObject::BallObject(glm::vec2 pos, GLfloat radius) : GameObject(pos, glm::vec2(radius * 2.0f, radius * 2.0f), Radius(radius), Stuck(true) { } +``` + +#### 4.3.7 类对象作为类成员 + +c++类中的成员可以是另一个类的对象,称该成员为==对象成员== + +```c++ +class Phone { +public: + Phone(string phone_name) :p_name(phone_name) { + cout << "Phone的构造函数" << endl; + } + ~Phone() { + cout << "Phone的析构函数" << endl; + } + +public: + string p_name; +}; + +class Person { +public: + // a_phone(pName)可以看作是 Phone a_phone = pName; 隐式转换法 + Person(string name, string pName) :a_name(name), a_phone(pName) { + cout << "Person的构造函数" << endl; + } + ~Person() { + cout << "Person的析构函数" << endl; + } +public: + string a_name; + // 一个类的实例对象作为一个类的成员 + Phone a_phone; // 这是默认构造的写法,但是有了有参构造,讲道理这写法是不对的,但是(看上面) // 再看时的理解:这只是声明一个数据类型,下面实例化的时候传参数`小米手机`进来会将这自动构造 +}; + +void test01() { + Person p("张三", "小米手机"); + // p.a_phone还只是一个类的实例对象,还要再去点一下p_name属性 + cout << p.a_name << " 拿着 " << p.a_phone.p_name << endl; +} +``` + +>Ps:Phone类的实例,作为Person的属性,那么构造函数的先后顺序:先有的Phone的构造函数,再有的Person的构造函数(就像先有小的部件,再有完整的玩具); +> +>​ 但是析构函数时先Person,再Phone,和构造函数刚好相反。 + +#### 4.3.8 静态成员 + +静态成员就是在成员变量和成员函数前加上关键字`static`,成为静态成员,具体: + +- 静态成员变量 + - 所有对象共享一份数据 + - 在编译阶段分配内存 + - 类内声明,类外初始化 +- 静态成员函数 + - 所有对象共享同一个静态成员函数 + - 静态成员函数只能访问静态成员变量 + +```c++ +class Person { +public: + static void func() { + // 修改值 + a = 100; // 静态成员函数只能访问静态成员变量 + + // 直接报错,因为所有对象共用一个函数,对象都会有自己的属性b,就不知道改哪个,而静态成员变量就一份 + //b = 200; + cout << "静态成员函数的调用,a = " << a << endl; + } +public: + static int a; // 静态成员变量 + int b; // 非静态成员变量 +}; +int Person::a = 50; // 类外初始化 + +void test01() { + // 1、通过对象访问 + Person p; + p.func(); + // 2、通过类名访问 + Person::func(); +} +``` + +### 4.4 C++对象模型和this指针 + +#### 4.4.1 成员变量和成员函数分开存储 + +​ 在c++中,类内的成员变量和成员函数分开存储,==只有非静态成员变量才属于类的对象上,才占类实例化对象的空间== + +```c++ +class Person { +public: + // 非静态成员变量,实例化一个对象,就有这么一份 + int m_A; // 非静态成员变量,属于类的对象上,所以下面打印对象的占的空间比空类会变大 + static int m_B; // 静态成员变量,不占对象空间,下面打印对象的大小也不会变 + + void func() {} + static void func1() {} // 这俩都不占对象空间,所有函数都是共享一个函数实例 + // 这种就相当于代码就一份,大家共用,下面会讲到用this区分是谁在调用 +}; + +void test01() { + Person p1; + // 空类都会占一个字节,用于标记区分空对象占内存的位置,即空类占一个字节 + cout << "空类占的字节数:" << sizeof(p1) << endl; // 1(空类时) + // 在加了int类型的非静态成员变量后,大小就成了4 +} +``` + +#### 4.4.2 this指针概念 + +​ 在上一小节知道,在c++中成员变量和成员函数是分开存储的,每一个非静态成员函数(我学习时的理解觉得静态成员函数也是啊)都只会诞生一份函数实例,也就说是一个类的多个实例化对象会共用一份代码,那么问题:这一块代码时如何区分哪个对象调用自己的呢? + +​ 答:c++通过提供特殊提供的对象指针----this指针,解决上述问题;==this指针指向被调用的成员函数所属的对象== + +- this指针是隐含在每一个非静态成员函数内的一种指针 +- this指针不需要定义,直接使用即可 + +this指针的用途: + +- 1.当形参和成员变量名字相同时,可用this指针来区分(但还是尽量规范,别相同) +- 2.在类的非静态成员函数中返回对象本身,可用`return *this;` + +```c++ +class Person { +public: + // 1、thi来区分同名是的情况 + Person(int age) { // 构造函数 + // 这样的话就根本没再给成员变量赋值,因为名字相同了,是错的 + //age = age; // 下面打印出来根本不是20岁, + this->age = age; // +一个this,表明这是成员变量的age(但尽量命名要规范) + } + + void personOneAddAge(Person p) { // 本来的age加上传进来的人的age + //age += p.age; // 这样也是可以的,但是下面写法更清楚 + this->age += p.age; // 只可加一次 + } + // 2、返回对象本身 + //Person personManyAddAge(Person p) { // 值返回 + Person& personManyAddAge(Person p) { // 引用返回 + this->age += p.age; + return *this; // this指向实例化对象本身,解引用后返回 + } +public: + int age; +}; + +void test01() { + Person p1(10); + cout << "p1的年纪:" << p1.age << endl; + + Person p2(15); + p2.personOneAddAge(p1); + cout << "p2的年纪:" << p2.age << endl; + + Person p3(10); + /*要是上面函数返回的类型是 Person,就是值传递,第一次操作时就拷贝构造是复制一份去做加的操作; + 第二次执行.personManyAddAge(p1),还是原来的p3.age=10复制一份去操作,所以无论多少次最后结果都是20;*/ + /*要是是返回的引用,即 Person& ,就返回的是引用,就一直是在p3.age=10上进行加法, + 所以最后的结果会是40 */ + p3.personManyAddAge(p1).personManyAddAge(p1).personManyAddAge(p1); + // p3.personManyAddAge(p1)只有返回了对象本身才能再继续调用.personManyAddAge()函数 + cout << "p3的年纪:" << p3.age << endl; +} +``` + +#### 4.4.3 空指针访问成员函数 + +​ C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针(有用到成员属性的,默认前面就会有this指针,就不行); + +​ 如果有空指针和this指针(我理解为用到成员属性),需加以判断保证代码健壮性。 + +```c++ +class Person { +public: + Person(int age) { + m_Age = age; + //this->m_Age = age; // 这也是可以的 + } + void showSentence() { + cout << "只为打印一句话,没用到成员属性(就没this指针)" << endl; + } + void showAge() { + // 必须加这个空指针判断,更健壮,不然下面就会报错,错的原因: + // 传进来一个空指针,去访问成员属性肯定是没有的,是错的 + if (this == NULL) { + return; + } + cout << "年龄:" << m_Age << endl; // 这俩是一样的,前面不写也会默认有 + cout << "年龄:" << this->m_Age << endl; + } +public: + int m_Age; +}; + +void test01() { + Person *p = NULL; // 创建一个空指针 + p->showSentence(); // 指针要这样去访问,空指针这也没问题 + p->showAge(); +} +``` + +#### 4.4.4 const修饰成员函数 + +==常函数==:成员函数后加const后我们称这个函数为常函数。 + +- 常函数内不可以修改成员属性 +- 成员属性声明时加关键字`mutable`后(`mutable int a;`),在常函数中依然可以修改 + +==常对象==: + +- 声明(实例化)对象前加const,称该对象为常对象 +- 常对象只能调用常函数 + +```c++ +class Person { +public: + // 构造函数初始化 + Person() { + m_A = 10; + m_B = 20; + } + /*this指针的本质是一个指针常量,指向不可以改了,但是指向的值是可以改的,这就是普通函数通过赋值操作来改指向的值 + 要想使指向的值也不可改,需要声明常函数,指针前面也要再加个const,这里加到了函数名称后,就是常函数,就是 const Type * const pointer + */ + void showPerson() const { + // 普通成员函数时可以修改的,加了const后这就是错的,就不能修改 + // this->m_A = 100; // this可省略 + cout << "a:" << m_A << endl; + this->m_B = 50; // this可省略 + cout << "b:" << m_B << endl; // 常函数中只能对加了mutable的成员属性可以修改 + } + void func() { + cout << "这就是一个普通的函数" << endl; + } +public: + int m_A; + mutable int m_B; +}; + +void test01() { + Person p1; // 普通对象 + p1.showPerson(); // OK + p1.func(); // OK + cout << "*****" << endl; + // 常对象 + const Person p2; + p2.showPerson(); // OK + // p2.func(); // 错的,常对象只能调用常函数 + // 常对象要是能调用别的函数,别的函数是可以修改成员属性的,就违背了常对象这一理念, +} +``` + +### 4.5 友员 + +​ 对于一个类,在类外,public的属性是都能访问的,但对于私有属性(private)是不能访问的,但偶尔想让一些例外的函数进行访问,就需要用到==友员==的技术; + +​ 友员的目的就是让==一个函数或者类能够访问另一个类中的私有成员; + +友员的三种实现: 关键字为 ==friend== + +- 全局函数做友员 +- 类做友员 +- 成员函数做友员 + +#### 4.5.1 全局函数做友员 + +​ 那这全局函数就可以访问这个类中的private属性。 + +```c++ +class Building { + // 友员函数核心是这 + friend void goodGay(Building *buil); +public: + Building() { + SittingRoom = "客厅"; + BeddingRoom = "卧室"; + } +public: + string SittingRoom; +private: + string BeddingRoom; +}; +// 全局函数(练习下指针) +void goodGay(Building *buil) { + cout << "好友正在访问:" << buil->SittingRoom << endl; + // 直接这样是访问不了私有属性的, 类内最上面像写函数声明一样,然后再加上`friend` + cout << "好友正在访问:" << buil->BeddingRoom << endl; +} +int main() { + Building building; + goodGay(&building); + system("pause"); + return 0; +} +``` + +#### 4.5.2 类做友员 + +​ 那这个类中的成员函数都可以访问另外一个类的私有属性。 + +```c++ +// 下面类在用 Building这个类了,先在这里声明下,后面在写,不然会报错,类似于函数声明 +class Building; +class GoodGay { +public: + GoodGay(); // 构造函数,也可以在类外实现 +public: + // 在类内可以只写函数声明,然后再类外实现 + void visit(); + // 把另外一个类作为函数成员,且创建的是指针 + Building *building; // 看作一个属性,然后要去赋值的 +}; + +class Building { + // 告诉解释器,GoodGay类时Building类的好朋友,它的成员函数都可以访问Building中的私有属性 + friend class GoodGay; +public: + Building() { + m_SittingRoom = "客厅"; + m_BeddingRoom = "卧室"; + } +public: + string m_SittingRoom; +private: + string m_BeddingRoom; +}; + +// 类外构造函数(也要加作用域) +GoodGay::GoodGay() { + // 上面类属性building就是定义的指针,new出来的值就必须指针去接收 + building = new Building; // new的语法:new 类型; +} +// 类中函数在类外实现时一定要加作用域,不然就成了全局函数 +void GoodGay::visit() { + // 也可以不要构造函数,直接就在这里初始化building + //building = new Building; // new的语法:new 类型; + cout << "好友正在访问:" << building->m_SittingRoom << endl; + // GoodGay这个类时不能去访问Building类中的private属性的,除非它是它的友员 + cout << "好友正在访问:" << building->m_BeddingRoom << endl; +} + +void test01() { + GoodGay person; + person.visit(); +} +``` + +#### 4.5.3 类成员函数做友员 + +​ 这个类指定的函数,可以访问另一个类的private属性,其他不行。 + +```c++ +class Building; +class GoodGay { +public: + GoodGay(); + +public: + void visit01(); + void visit02(); + Building *build; +}; + +class Building { + // 只把GoodGay中的visit01函数设成了友员函数 + friend void GoodGay::visit01(); +public: + Building() { + this->m_SittingRoom = "客厅"; + m_BedingRoom = "卧室"; // 这俩是一样的,一定要看得懂 + } + string m_SittingRoom; +private: + string m_BedingRoom; +}; + +GoodGay::GoodGay() { + // Building类实现一定要写在这前面,不然这会报错 + build = new Building; +} +void GoodGay::visit01() { + cout << "visit01正在访问:" << build->m_SittingRoom << endl; + cout << "visit01正在访问:" << build->m_BedingRoom << endl; +} +void GoodGay::visit02() { + cout << "visit02正在访问:" << build->m_SittingRoom << endl; + // 这不是友员,就不能访问 + //cout << "visit02正在访问:" << build->m_BedingRoom << endl; +} + +void test01() { + GoodGay guy; + guy.visit01(); + guy.visit02(); +} +``` + +### 4.6 运算符重载 + +​ 概念:对已有的运算符(+、-等等)重新进行定义,赋予其另一种功能,以适应不同的数据类型;简单来说已有运算符一般是用来运算内置数据类型,重载后就可以更好的计算更多的是自定义数据类型。 + +​ 关键字:`operator` 加上要重载的符号作为函数名,如:`operator+`、`operatpr<<` + +​ `[]`这种也可以重载,用于自定义数据数组的[2]这样去取数据 + +#### 4.6.0 下标重载的注意点 + +一般会定义下标运算符 operator[] + +​ ==下标运算符返回的是元素的引用==,所以当StrVec是非常量时,可以给元素赋值,而当我们对常量对象取下标时,不能为其赋值: + +```c++ +const StrVec cvec = svec; // 假设 svec 是一个StrVec对象,这是把svec的元素拷贝到cvec中 +if (svec.size() && sevc[0].empty()) { + svec[0] = "zero"; // 正确:svec是非常量,下标运算符返回string的引用,可以赋值 + cvec[0] = "zip"; // 错误:对cvec取下标返回的是常量引用 +} +``` + +#### 4.6.1 `+`运算符重载 + +​ ==加号运算符==重载:实现有自定义数据类型相加的运算 + +```c++ +// 类内成员函数实现重载 + +class Person { +public: + Person(int a, int b) :m_A(a), m_B(b) {} + // 两种方式实现成员函数 + 号运算符重载 + // 1.可以取任意的名字 + //Person personAddPerson(Person *p1) { // 这也可以是引用 + // 2.使用operator关键字可以可以简化写法 + Person operator+(Person &p1) { + // 因为上面构造函数的写法,我这必须要初始化 + Person temp(0, 0); + temp.m_A = this->m_A + p1.m_A; + temp.m_B = m_B + p1.m_B; // 类内,自身的+上传进来的 + return temp; // 值返回,相当与是创建一个新的副本 + } +public: + int m_A; + int m_B; +}; +void test01() { + Person p1(10, 20); + Person p2(5, 10); + //Person p3 = p1.personAddPerson(&p2); + //Person p3 = p1.operator+(&p2); // 本质调用,下面是简写 + Person p3 = p1 + p2; // 注意使用operator关键字时,尽量就用引用,用指针前面还要加个&,不简洁,不好看 + // 这应该用的就是隐式转换法,使用的拷贝构造 + cout << "p3.m_A = " << p3.m_A << endl; + cout << "p3.m_B = " << p3.m_B << endl; +} +``` + +​ 全局函数实现运算符重载,也可函数重载。 + +```c++ +// 全局函数运算符的重载 +// 若是用p + 10 就是Perons + int 就不对,就可以使用下面函数重载 +Person operator+(Person &p1, Person &p2) { + Person temp(0, 0); + temp.m_A = p1.m_A + p2.m_A; + temp.m_B = p1.m_B + p2.m_B; + return temp; +} +// 运算符重载,也可以发生函数重载 +Person operator+(Person &p, int a) { + Person temp(0, 0); + temp.m_A = p.m_A + a; + temp.m_B = p.m_B + a; + return temp; +} + +void test01() { + Person p1(10, 20); + Person p2(5, 10); + + //Person p3 = operator+(p1, p2); // 本质调用,下面是简写 + Person p3 = p1 + p2; + cout << "p3.m_A = " << p3.m_A << endl; + cout << "p3.m_B = " << p3.m_B << endl; + + Person p4 = p1 + 100; + cout << "p4.m_A = " << p4.m_A << endl; + cout << "p4.m_B = " << p4.m_B << endl; +} +``` + +>- 对于内置的数据类型的表达式的运算符时不可能改变的 +>- 不要滥用运算符重载(+的重载却故意把内容写成-) + +#### 4.6.2 `<<`运算符重载 + +​ ==左移运算符==重载:可以输出自定义数据类型 + +```c++ +class Person { + // 私有属性,外部访问,需加成友员函数 + friend std::ostream& operator<<(std::ostream &cout, Person p); +public: + Person(int a, int b): m_A(a), m_B(b) {} +private: + int m_A; + int m_B; +}; +/* +ostream &cout 是标准输出流,必须唯一,所以传进去必须用引用; +若无返回值,是void,那就只能执行一次,就没办法做到链式编程了, +那下面就无法跟"hello world"了,,所以要返回标准的cout的引用 +*/ +// 注意这是函数,()里是定义的参数,那么就是`参数类型 参数名`,不要把`ostream cout` (这只是举例)看成了类的实例化,更不会有::这种作用域的符号 +std::ostream& operator<<(std::ostream &cout, Person p) { + cout << "m_A = " << p.m_A << "m_B = " << p.m_B; + return cout; +} +/* 这由于是引用,就是起别名,所以可以就写成任意名,比如c,都是指向标准的cout */ +//ostream& operator<<(ostream &c, Person p) { +// c << "m_A = " << p.m_A << "m_B = " << p.m_B; +// return c; +//} + +void test01() { + Person p(10, 20); + // 本质:operator<<(cout, p) 简写就是 cout << p + cout << p << " hello woeld" << endl; +} +/* +这个无法使用成员函数重载,成员函数的本质形式一定是这样 p.operator<<(cout) , +简写就是 p << cout ,就不对 +成员函数,必然是自身的this去.operator +*/ +``` + +Ps:若有需要,可以看看自己做题的这个[demo](https://pintia.cn/problem-sets/14/problems/814) + +#### 4.6.3 `++`运算符重载 + +​ ==递增运算符==重载:通过这,实现自己的整型数据的++ + +```c++ +class MyInteger { +public: + MyInteger(int a) { + m_A = a; + } + // 这是写的前置++,因为自身+1后再返回的 + // 必须返回自身引用才能一直对一个数据对象+,不然每次都是在复制一份再+,++(++myint) 的结果是12了,但是myint始终是11 + MyInteger& operator++() { + this->m_A += 1; + return *this; // 这this是指针,*解引用后就是对象,返回引用 + } + + // 这是后置++,先用一个临时对象来记录当前值,m_A再+1,再返回临时对象 + // 这里千万不能返回引用,不能返回局部变量(栈区对象)引用,一次后就释放了,后续的操作就是非法的 + // int 就是前面写到的函数占位参数,这里必须int,区分前置++和后置++的重载 + MyInteger operator++(int) { + // 这应该是 隐式转换法,this是这个对象的指针,再解引用就是这个对象 + // 类似 Person p2 = p1;会调用拷贝构造,把所有的属性都拷过去 + MyInteger temp = *this; // + this->m_A += 1; + return temp; + } + int m_A; +}; + +// 全局重载 左移运算符 << ;如果m_A是private,就是这写进MyInteger类做友员 +ostream& operator<<(ostream &cout, MyInteger m_int) { + cout << m_int.m_A; + return cout; +} +void test01() { + MyInteger myint(10); + // 为了输出自定义数据类型,先要重载一下 << + cout << ++(++myint) << endl; //12 + cout << myint << endl; // 12,,如果上面返回的不是引用,这就是11 +} +void test02() { + MyInteger myint(10); + cout << (myint++)++ << endl; + cout << myint << endl; +} +``` + +> Ps:前置递增要==返回引用==;后置递增要==返回值==。 +> +> 如果是显示的调用后置运算符(用的上面的对象myint): +> myint.operator++(0); // 调用后置版本的operator++;我理解0就是那个占位的int +> myint.operator++(); // 调用前置版本的operator++ + +#### 4.6.4 `=`运算符重载 + +c++编译器至少给一个类添加4个函数 + +1. 默认构造函数(无参,函数体为空) +2. 默认构造函数(无参, 函数体为空) +3. 默认拷贝构造函数,对属性进行值拷贝 +4. 赋值运算符 `operator=` ,对属性进行值拷贝 + +​ ==赋值运算符==重载意义:如果类中有属性指向堆区,做赋值操作时就会出现深浅拷贝的问题,编译器自带的赋值是浅拷贝,假如有属性是指针,指向堆区,默认的就是把一个对象的指针赋值给另外一个对象,那么这两个对象的这个指针属性都是指向同一个堆区,当class有析构函数对堆区数据进行回收时,就会因为反复释放引发错误,就需要重载赋值运算法进行深拷贝。 + +```c++ +class Person { +public: + Person(int age) { + m_Age = new int(age); + } + ~Person() { + if (m_Age != NULL) { + delete m_Age; + m_Age == NULL; + } + } + // 重载赋值运算符 + // 参数一定要用引用或是指针传递,绝对不能用值传递 + Person& operator=(Person &p) { + // 编译器的操作就是(浅拷贝): + // m_Age = p.m_Age; 把自己指向的地址复制过去,所以第二次释放就会错 + + // 深拷贝之前还要判定一下这个区域为不为空,不空就要删除堆区,再置为NULL,然后重新开辟 + if (m_Age != NULL) { + delete m_Age; + m_Age = NULL; + } + this->m_Age = new int(*p.m_Age); + return *this; // void只能用一次(p1 = p2),为了连等(p1=p2=p3) + } + int *m_Age; +}; +void test01() { + Person p1(10); + Person p2(20); + p1 = p2; // 把p2都赋值给p1; + // 自带的赋值是浅拷贝,然后析构函数对堆区数据的释放,第二次就会报错,故要把自带的赋值运算符重载成深拷贝 + + cout << *p1.m_Age << endl; // p1.m_Age是指针,要*解引用 + cout << *p2.m_Age << endl; + Person p3(30); + p1 = p2 = p3; + // p2 = p3 先执行(p3赋值给p2),又返回p2本身得到 p1=p2;所以最后都是30 +} +``` + +#### 4.6.5 `== !=`运算符重载 + +一般重载了 == 操作,那尽量也要重载 != ;重载了 < ,也要重载其它关系运算符,以保持较好的兼容性。且一种比较便捷的写法就是: + +``` +bool operator==(const A_class &lhs, const A_class &rhs) { + return lhs.isbn() == rhs.isbn() && + lhs.units_sold == rhs.units_sold; +} +// 接下来重载不等就很简单了 +bool operator!=(const A_class &lhs, const A_class &rhs) { + return !(lhs == rhs); // 直接用上面==的结果取反就好了 +} +``` + +​ ==关系运算符==重载:可以让两个自定义类型对象进行对比操作。 + +```c++ +class Person { +public: + Person(string name, int age) { + m_Name = name; + this->m_Age = age; + } + // 记得用引用 + bool operator==(Person &p) { + bool out = false; + if (m_Age == p.m_Age && m_Name == p.m_Name) { + out = true; + } + return out; + } + bool operator!=(Person &p) { + bool out = false; + if (m_Age != p.m_Age || m_Name != p.m_Name) { + out = true; + } + return out; + } + string m_Name; + int m_Age; +}; +void test01() { + Person p1("张三", 10); + Person p2("张三", 10); + cout << (p1 != p2) << endl; +} +``` + +#### 4.6.6 `()`运算符重载 + +​ ==函数调用运算符==重载,由于重载后的使用方式非常像函数的调用,因此也称为==仿函数==,仿函数没有固定写法,非常灵活。 + +```c++ +class MyAdd { +public: + // 重载了函数调用的() + int operator()(int a, int b) { + return a + b; + } +}; +// 全局函数实现一个加法 +int addFunc(int a, int b) { + return a + b; +} +void test01() { + MyAdd ya; + // 这又叫仿函数,重载了(),跟函数调用一模一样 + int ret1 = ya(10, 20); + // 这是函数调用,长得很像 + int ret2 = addFunc(10, 20); + cout << "ret1 = " << ret1 << endl; + cout << "ret2 = " << ret2 << endl; + // 看到 函数名()第一反应就应该是匿名对象,再跟着的 () 就是重载的 () 的调用 + cout << MyAdd()(10, 20) << endl; // 30 + // 针对单纯调用一下,并不想创建对象,用完就回收 +} +``` + +#### 4.6.7 标准库定义的函数对象 + +标准库定义了函数对象(可以理解为函数名来代替各种运算符):这写都是定义在`#include `头文件中。 + +| 算术 | 关系 | 逻辑 | +| -------------------------- | -------------------------- | -------------------------- | +| std::plus\ + | std::equal_to\ == | std::logical_and\ && | +| std::minus\ - | std::not_equal_to\ != | std::logical_or\ \|\| | +| std::multiplies\ * | std::greater\ > | std::logical_not\ ! | +| std::divides\ / | std::greater_equal\ >= | | +| std::modulus\ % | std::less\ < | | +| std::negate\ 取相反数 | std::less_equal\ <= | | + +使用:把这看作一个类,先实例化一个对象,然后用这个对象去做对应的操作: + +```c++ +#include +std::minus a; +std::negate b; +std::cout << a(10, 5) << std::endl; // 5 +std::cout << b(10) << std::endl; // -10 +``` + +这个就是常用于一些算法的第三个参数,类似于这`std::sort(v.begin(), v.end(), std::greater());`它这里就理解为一个匿名对象,等着被`()`调用,看这行就很好理解了(第一个括号时是创建匿名对象,第二个才是调用): + +```c++ +std::cout << std::minus()(10, 5) << std::endl; // 5 +``` + + 一个练习: + +(a) 统计大于1024的值有多少个。 +(b) 找到第一个不等于pooh的字符串。 +(c) 将所有的值乘以42。 + +```c++ +using std::placeholders; +std::count_if(vec.begin(), vec.end(), std::bind(std::greater(), _1, 1024)); +std::find_if(v.begin(), v.end(), std::bind(std::not_equal_to)(), _1, "pooh"); +std::transform(v.begin(), v.end(), des_begin(), std::bild(std::multiplies(), _1, 42)); +``` + + + +再一个练习:题目是:使用标准库函数对象判断一个给定的`int`值是否能被 `int` 容器中的所有元素整除。 + +```c++ +#include +#include +std::vector vec{2, 4, 5, 6, 8, 10}; +int num = 2; // 先把这个任意num写死 +// 我的写法是: +auto res = std::find_if(vec.begin(), vec.end(), std::bind(std::modulus(), std::placeholders::_1, num)); // 注意modulus(),有这个()才是匿名对象 +if (res != vec.end()) { /* 不等就代表有除不尽的 */} + +// 书上的写法是 +std::modulus mod; +auto pre = [&](int i) {return 0 == mod(num , i); }; +auto is_divid = std::any_of(vec.begin(), vec.end(), pre); // 注意这个用法 +std::cout << (is_divid ? "Yes!" : "no!" ) << std::endl; +``` + +Tips: + +- 练习里面又学到一个算法`std::any_of`; +- 用到了bind创建的对象,它是像函数、lambda表达式一样可调用的,更多可以去看0Primerc++.md中,关于==c++中的可调用对象==。 + +#### 4.6.8 类型转换运算符 + +==类型转换运算符==是类的一种特殊成员函数,它负责将一个类型的值转换成其他类型。类型转换函数的一般形式如下:`operator type() const;` (这在书上也是重载的那一章) + +- type表示某种类型(一定注意这,type只是类型,具体使用时一般为 int double之类的具体数据类型),类型转换运算符可以面向任意类型(除了void之外)进行定义,只要该类型能作为函数的返回类型; +- 因此,不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针即函数指针)或者引用类型; +- 类型转换运算符既没有显式的返回类型,也没有形参,而且==必须定义成类的成员函数,它不能声明返回类型,参数列表也必须为空==;类型转换运算符通常不应该改变待转换对象的内容,所以它一般被定义为const成员。 + +比如: + +```c++ +class SmallInt { +public: + SmallInt(int i = 0):val(i) { + if (i < 0 || i > 255) { // 定义一个类,表示0~255之间的整数 + throw std::out_of_range("Bad SmallInt value"); + } + } + // 这里就定义了从类类型向其他类型的转换。 + operator int() const { + return val; + } +private: + std::size_t val; +}; + +int main(int argc, char*argv[]) { + + SmallInt si; + // 首先将4隐式地转换成SmallInt,然后调用SmallInt::operator= (这里没定义,应该就是调用合成的) + si = 4; + std::cout << si << std::endl; // 如果没定义`类型转换运算符`,这里是会直接报错的 + si + 3; // 首先将si隐式地转换成int,然后执行整数的加法 + + SmallInt si1 = 3.14; // 调用SmallInt(int)构造函数 + si + 3.14; // 内置类型转换将所得的int继续转换成double + system("pause"); + return 0; +} +``` + +注意容易错误的:地方 + +```c++ +class SmallInt; +operator int(SmallInt&); // 错误:不是成员函数 +class SmallInt { + int operator int() const; // 错误:指定了返回类型 + operator int(int i = 0) const; // 错误:参数类表不为空 + operator int*() const {return 42;} // 错误:42不是一个指针 +}; +``` + +##### 显式的类型转换运算符 + +​ 类型转换运算符可能产生意外结果:简单说如果类型转换自动发生(就像上面那样隐式转换),可能会引发意想不到的结果,所以在c++11新标准中引入了`显式的类型转换运算符` + +```c++ +class SmallInt { +public: + // 这样编译器就不会自动执行这一类型转换 + explict operator int() const {return val;} + // explict operator const int() {return val;} // 这种是没有意义的 + // 其它成员与上面的版本一致 +}; +``` + +这样和显式的构造函数一样,编译器(通常)也不会将一个显式的类型转换运算符用于隐式类型转换: + +``` +SmallInt si = 3; +si + 2; // 错误:此处需要隐式的类型转换,但类的运算符指定必须是显式的 +static_cast(si) + 2; // 正确:显式地请求类型转换 +``` + +​ 即:当类型转换运算符是显式的试,必须通过显式的强制类型转换才可以,但是该规定错在一个例外:如果表达式被用作条件,则编译器会将显式的类型转换自动应用于它;换句话说,当表达式出现在下列外置时,显式的类型转换将被隐式地执行: + +- if、while及do语句的条件部分; +- for语句头的条件表达式; +- 逻辑非运算符(!)、或运算符(||)、与运算符(&&)的运算对象 +- 条件运算符(? :)的条件表达式。 + +##### 转换为bool + +​ 早期标准中,IO类定义了向void*的转换规则,以求避免上面提到的问题,在c++11新标准中,IO标准库通过定义一个向bool的显式类型转换实现同样的目的。 + +​ 无论什么时候在条件中使用流对象,都会使用为IO类型定义的operator bool。例如:while(std::cin >> value),while语句的条件执行输入运算符,它负责将数据读入到value并返回cin,为了对条件求值,cin被istream operator bool类型转换函数隐式地执行了转换,如果cin的条件状态是good(文件流那里应该记录过这),则该函数返回为真,否则返回为假。Tips:向bool的类型转换通常在条件部分,因此operator bool一般定义为explicit的。 + +##### 转换的优先级 + +1. 精确匹配 +2. `const` 转换。 +3. 类型提升 +4. 算术转换 +5. 类类型转换 + +### 4.7 继承 + +基本语法:`class 子类 : 继承方式 父类 {};` + +`class A : public B {};` // 还可以有private、protected + +A 类称为子类 或 派生类 + +B 类称为父类 或 基类 //基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。 + +#### 4.7.0 override | final + +​ 派生类必须在内部对所有重新定义的虚函数进行声明,派生类可以在这样的函数之前加上virtual关键字,但是并不是非得这么做,C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个`override`关键字(override关键字来说明派生类中的虚函数,相当于一种标记)。 + +```c++ +struct A { + virtual void f1(int) const; + virtual void f2(); + void f3(); +}; +struct B : A { + virtual void f1(int) const override; // 正确:和下面一行一个意思,virtual可要可不要 + void f1(int) const override; // 正确:f1重载,且与基类中的f1匹配 + void f2(int) override; // 错误:类A中没有形如f2(int)的函数 + void f3() override; // 错误:f3并不是虚函数(只有虚函数才能被覆盖) + void f4() override; // 错误:A中并没有名为f4的函数 +}; // + +// 关键字final还能阻止函数被覆盖 +struct B2 : A { + // 已经从A中继承f2()和f3(),然后下面覆盖f1(int) + void f1(int) const final; // 加了final就不允许后续的其它类覆盖f1(int)了 +}; +struct C3 : B2 { + void f2(); // 正确:覆盖从间接基类B2继承而来的f2 + void f1(int) const; // 错误:B3已经将f2声明成了 final +}; +``` + +final和override说明符出现在形参列表(包括任何const或引用修饰符)以及位置返回类型之后。 + +有防止继承的发生:c++11新标准提供了一种防止继承发生的办法,即在类名后跟一个关键字`final`: + +```c++ +// 1、 +class NoDerived final { /**/ }; // 类NoDerived就不能作为基类了 + +// 2、 +class Base { /**/ }; +class Last final : Base { /**/ }; // 这时类Last就不能作为基类了 +class Bad : Last { /**/ }; // 错误:Last是final的 +``` + + + +​ 在某些情况下,希望对虚函数不要进行动态绑定,而是强迫其执行虚函数的某个特定版本,则可以使用作用域运算符实现这一目的,如下面的代码: +​ double undis_123 = base->Quote::net_price(42); +​ 该代码强行调用Quote的net_price函数,而不管baseP实际指向的对象类型到底是什么,该调用将在编译时完成解析。这就是==回避虚函数的机制==。就是如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本的自身的调用,从而导致无限递归。 + +--- + +可以将一个成员函数同时声明成 `override` 和 `final`:`override` 的含义是重写基类中相同名称的虚函数,`final` 是阻止它的派生类重写当前虚函数。 + +``` +class Base { /**/ }; +struct D1 : Base { /**/ }; // 默认public继承 +class D2 : Base { /**/ }; // 默认private继承 +``` + +#### 4.7.1 继承方式 + +- 公共继承 +- 保护继承 +- 私有继承 + +```c++ +class Base { +public: + int m_A; +protected: + int m_B; +private: + int m_C; +}; +class Son1 : public Base { +public: + // 需要用函数类内去访问,不能直接在外面做m_A = 11;这样的操作 + void func() { + m_A = 11; // OK,父类public,到子类也是public + m_B = 12; // OK,父类protected,到子类也是protected + //m_C = 13; // 错的,父类的privated不能访问 + } +}; +class Son2 : protected Base { +public: + void func() { + m_A = 11; // OK,父类public,到子类是protected + m_B = 12; // OK,父类protected,到子类还是protected + //m_C = 13; // 错的,父类的private不能访问 + } +}; + +// 这种就全部变成了私有属性,Son3再作为父类继承,那它的子类是一个属性都访问不到的 +class Son3 : private Base { +public: + void func() { + m_A = 11; // OK,父类public,到子类是private + m_B = 12; // OK,父类protected,到子类还是private + //m_C = 13; // 错的,父类的privated不能访问 + } +}; +// 类外都只能访问public,类内可以访问public和protected +void test01() { + Son1 s1; + Son2 s2; + Son3 s3; + s1.m_A = 10; + //s2.m_A = 10; // 这俩都不能访问 + //s3.m_A = 10; +} +``` + + + +​ 父类中所有非静态成员竖向都会被子类继承下去,私有成员只是被编译器隐藏了访问不到,还是会继承下去。 + +```c++ +class Base { +public: + int m_A; +protected: + int m_B; +private: + int m_C; +}; +class Son : public Base { +public: + int m_D; +}; +void test01() { + Son s; + cout << sizeof(s) << endl; // 16个字节 +} +``` + + + +==继承中构造和析构顺序== + +>子类继承父类后,当创建子类对象,也会调用父类的构造函数 +> +>​ 且 +> +>先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反(先子类,再父类) + +#### 4.7.2 继承同名函数调用 + +问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢? + +* 访问子类同名成员 直接访问即可 +* 访问父类同名成员 需要加作用域 + +```c++ +class Base { +public: + Base() { + m_A = 100; + } + void func() { + cout << "Base - func()调用" << endl;; + } + // 同名重载 + void func(int) { + cout << "Base - func(**int**)调用" << endl; + } + int m_A; +}; +class Son : public Base { +public: + Son() { + m_A = 200; + } + void func() { + cout << "Son - func()调用" << endl; + } + int m_A; +}; +void test01() { + Son s; + cout << s.m_A << endl; // 200, 直接访问的是子类的 + cout << s.Base::m_A << endl; // 10, 加作用域 + + s.func(); + // s.func(10); 不行,讲道理,这个传参数重载了,应该不用加作用域了,但是子类中也有同名的存在 + s.Base::func(); + s.Base::func(10); +} +``` + +>当子类与父类拥有同名的成员函数(静态成员函数也一样),子类会隐藏父类中所有版本的同名成员函数 +> +>如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域 + +#### 4.7.3 继承同名静态成员 + +```c++ +class Base { +public: + static void func() { + cout << "---父类func()调用---" << endl; + } + static int m_A; +}; +// 静态变量一定要类内定义,类外实现,且一定要加作用域 +int Base::m_A = 100; + +class Son : public Base { +public: + static void func() { + cout << "---子类func()调用---" << endl; + } + static int m_A; +}; +int Son::m_A = 200; + +// 静态成员属性 +void test01() { + // 1、通过创建对象来访问 + Son s; + cout << s.m_A << endl; // 直接访问肯定是子类的 + cout << s.Base::m_A << endl; // 访问父类是这样加作用域 + s.func(); + s.Base::func(); // 注意调用方式,先点再俩冒号 + cout << "************************************" << endl; + + // 2、通过类名来访问 + cout << Son::m_A << endl; // 类名::属性名称访问 + cout << Son::Base::m_A << endl; // 通过资子类中的父类作用域去访问 + Son::func(); + Son::Base::func(); // 这是俩冒号 +} +``` + +>Ps:同时静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(==通过对象== 和 ==通过类名==) + +#### 4.7.4 多重继承 + +C++允许一个类继承多个类,但在实际开发中,不建议使用多继承。 + +语法:`class 子类 : 继承方式 父类1, 继承方式 父类2... {};` + +​ `class Son : public Base2, public Base1 {};` + +>​ 多继承可能会引发父类中有同名成员出现(即比如父类1、父类2中都有一个m_A属性),那就产生了==二义性==,那子类访问的时候就要像上面同名函数处理一样,加作用域进行区分。 + +​ 用于查看类的结构,用打开vs的命令提示符,cd到这个类所在cpp的路径,然后输入下面这行命令就行了:`cl /d1 reportSingleClassLayout类名 类所在cpp文件名称` + +#### 4.7.5 菱形继承|虚继承 + +​ 概念:两个派生类都继承了同一个基类;而又有某个类同时继承了这两个派生类,这样的继承被称为==菱形继承==,或者==钻石继承==。 + +​ 那么这某个类在使用时就会自最开始的基类的数据继承了两份,然而我们其实就要一份就好,这就加大了开销,导致了资源浪费及毫无意义。 + +​ ==虚继承==的目的是令某个类做出声明,承诺愿意共享它的基类,共享的基类子对象称为成为==虚基类==,这样无论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。 + +```c++ +// 菱形继承 +// Son1 Son2两个类都继承了Base,都有m_A属性,此时GrandChdir又同时继承了Son1 Son2两个类,那就有两份m_A(就会有资源浪费) +class Base { +public: + int m_A; +}; +class Son1 : public Base {}; +class Son2 : public Base {}; +class GrandChdir : public Son1, public Son2 {}; + +void test01() { + GrandChdir gs; + gs.Son1::m_A = 18; + gs.Son2::m_A = 20; + cout << gs.Son1::m_A << endl; // 18 + cout << gs.Son2::m_A << endl; // 20,必须加作用域去访问 + // couy << gs.m_A << endl; //因为有二义性,不能这样去访问 +} +``` + +​ 解决办法:继承前加==virtual==关键字后,变成==虚继承==。 + +```c++ +// 此时公共的父类(最开始的基类)称为虚基类 +class Base { +public: + int m_A; +}; +class Son1 : virtual public Base {}; +class Son2 : virtual public Base {}; // 就这俩加了 +class GrandChdir : public Son1, public Son2 {}; + +void test01() { + GrandChdir gs; + gs.Son1::m_A = 18; + gs.Son2::m_A = 20; + cout << gs.Son1::m_A << endl; // 20 + cout << gs.Son2::m_A << endl; // 20 + cout << gs.m_A << endl; // 20 // 这是就唯一了,就可以这样访问 +} +``` + +>Ps:虚继承实现的原理,靠的是一个叫`vbptr`(叫做`虚基类指针`),具体怎么实现的看[视频](https://www.bilibili.com/video/BV1et411b73Z?p=134&spm_id_from=pageDriver)吧。 + +### 4.8 多态 + +#### 4.8.1 多态的基本概念 + +多态是C++面向对象的三大特性之一 + +多态分为两类: + +- 静态多态:==函数重载== 和 ==运算符重载== 属于静态多态,复用函数名 +- 动态多态:派生类和虚函数实现运行时多态 + + + +静态多态和动态多态的区别: + +- 静态多态的函数地址早绑定 -即- 编译阶段确定函数地址 +- 动态多态的函数地址晚绑定 -即- 运行阶段确定函数地址 + +```c++ +class Animal { +public: + virtual void speak() { + cout << "动物在说话" << endl; + } +}; +class Cat : public Animal { +public: + void speak() { + cout << "猫在说话" << endl; + } +}; +class Dog : public Animal { +public: + void speak() { // 子类在重写时也是可以加个virtual关键字的(一般不要吧) + cout << "狗在说话" << endl; + } +}; +// 如果函数地址在编译阶段就能确定,那么就是静态联编 +// 如果函数地址在运行阶段才能确定,那么就是动态联编 + +// 这里相当于是nimal &animal = cat; +/* 要是没有虚函数,那就是`地址早绑定`,在编译阶段已经确定函数地址, +所以无论后面传什么派生类动物,都是`动物在说话`*/ +void test01(Animal &animal) { // 注意这是父类指针或是引用 + animal.speak(); +} +/*可是我们想传猫,猫说话;传狗,狗说话(即希望传入什么对象,就调用什么对象的函数),那么这个函数地址就不能早绑定, +需要在运行阶段进行绑定,即地址晚绑定,那就需要在基类这函数加virtual关键字, + 这应该就是`虚函数` */ + +int main() { + Cat cat; + test01(cat); + Dog dog; + dog.speak(); + + // 若是只有一个简单的speak函数,就占一个字节 + // 若是加了virtual,就占4个字节:存的是一个叫`vfptr`的`虚拟函数(表)指针` + // 多态的实现原理看下方视频链接 + cout << sizeof(Animal) << endl; + system("pause"); + return 0; +} +``` + +>总结: +> +>多态满足的条件: +> +>- 有继承关系 +>- 子类重写父类中的==虚函数== +> - 重写:函数返回值类型 函数名 参数列表 完全一致称为重写,注意与重载的区别 +> +>多态使用的条件: +> +>- 父类指针或引用指向子类对象 +> +>[多态实现原理](https://www.bilibili.com/video/BV1et411b73Z?p=136&spm_id_from=pageDriver) + +#### 4.8.2 多态案例·计算器类 + +分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类 + +多态的优点: + +- 代码结构组织结构清晰 +- 可读性强 +- 利于前期和后期的扩展以及维护(提倡的==开闭原则==:对扩展进行开放,对修改进行关闭) + +```c++ +// 1、普通写法,要是加一个加法就不好扩展,就要在原来的代码上进行改动 +class Cal { +public: + int m_A; + int m_B; + + int getResult(char oper) { + switch (oper) { + case '+': + return m_A + m_B; + case '-': + return m_A - m_B; + case '*': + return m_A * m_B; + } + } +}; +void test01() { + Cal cal = { 5, 3 }; // 这里居然可以这样初始化赋值 + int out = cal.getResult('*'); + cout << out << endl; +} +/*===============================================================*/ + +// 2、写一个计算器的抽象类 +class Calculator { +public: + virtual int getResult() = 0; // 纯虚函数 + + int m_A; + int m_B; +}; +// 每一种计算写一个类 +class AddCal : public Calculator { +public: + int getResult() { + return m_A + m_B; + } +}; +class SubCal : public Calculator { +public: + int getResult() { + return m_A - m_B; + } +}; +class MulCal : public Calculator { +public: + int getResult() { + return m_A * m_B; + } +}; +class DivCal : public Calculator { +public: + int getResult() { + return m_A / m_B; + } +}; + +void test02(Calculator *cal) { + int out = cal->getResult(); + cout << out << endl; +} + +int main() { + // 1、以函数传参的方式来实现多态 + DivCal divcal; + divcal.m_A = 15; + divcal.m_B = 3; + test02(&divcal); + + MulCal mulcal; + mulcal.m_A = 5; + mulcal.m_B = 6; + test02(&mulcal); + + // 2、直接左边是抽象类(一定是父类),右边是派生类的数据类型 + Calculator *abc = new AddCal; + abc->m_A = 3; + abc->m_B = 2; + cout << abc->getResult() << endl; + delete abc; // 用完了把数据销毁释放 + + abc = new SubCal; // 数据释放了,指针还在 + abc->m_A = 20; + abc->m_B = 10; + cout << abc->getResult() << endl; + + system("pause"); + return 0; +} +``` + + + +#### 4.8.3 纯虚函数和抽象类(= 0;) + +​ 在多态中,通常父类中虚函数的实现是毫无意义的主要都是调用子类重写的内容,因此可以将==虚函数==改为==纯虚函数==,纯虚函数语法:`virtual 返回值类型 函数名(参数列表) = 0;` // 这里好像不要virtual也行,核心标志是`=0` + +​ 当类中有了纯虚函数,这个类也称为==抽象类==。 + +抽象类特点: + +- 抽象类无法实例化对象; +- ==子类必须重写抽象类中的纯虚函数,否则也属于抽象类==。 + +```c++ +class Base { +public: + virtual void func() = 0; // 纯虚函数可以不要实现代码 +}; +class Son : public Base { +public: + void func() { + cout << "hello world" << endl; + } +}; +void test01() { + Son s; + s.func(); +} +``` + +#### 4.8.4 虚析构和纯虚析构 + +​ 引出:多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。 + +​ 解决方式:将父类中的析构函数改为==虚析构==或者==纯虚析构==。 + +虚析构和纯虚析构共性: + +- 都可以解决父类指针释放子类对象 +- ==都需要有具体的函数内部实现== // 我好像就是空的,也没问题啊(感觉不一定要内部实现代码) + +虚析构和纯虚析构的区别: + +- 如果是纯虚析构,该类属于抽象类,无法实例化对象 + + + +虚析构语法:析构函数前加virtual `virtual ~类名() {}` + +纯虚析构语法:`virtual ~类名() = 0;` 、 `类名::~类名() {}` // 相当于是类内定义,类外实现(类外一定要有实现,且要加作用域) + +Ps:一定注意,这 virtual 都是加在基类里面的 + +```c++ +class Animal { +public: + Animal() { + cout << "Animal构造函数调用" << endl; + } + // 纯虚函数 + virtual void speak() = 0; + + //// 方式1、 + //// 虚析构函数(没这的话,多态调用时父类指针是不会释放子类中的堆数据的) + //virtual ~Animal() { // + // cout << "Animal析构函数调用" << endl; + //} + + // 方式2、纯虚析构:必须类内定义,类外实现 + virtual ~Animal() = 0; +}; +Animal::~Animal() { + cout << "Animal析构函数调用" << endl; +} // 好像要不要这内部实现都可以的啊(这里跟上面点写定义时有点出入) + + +class Cat : public Animal { +public: + Cat(string name) { + // 指针的赋值尽量就用new的方式创建,而不是直接定义传进来来为string *name + m_A = new string(name); + cout << "Cat构造函数调用" << endl; + } + // 重写从父类继承的虚函数 + void speak() { + cout << "这是小猫在说话,多态的调用" << endl; + } + ~Cat() { + cout << "Cat析构函数调用" << endl; + if (this->m_A != NULL) { + delete m_A; + m_A == NULL; + } + } + string *m_A; // 定义一个指针类型,后续好在堆空间开辟内存 +}; + +void test01() { + Animal *animal = new Cat("Tom"); + animal->speak(); + delete animal; + // 要是没有虚析构函数或是纯虚析构函数结束了都不会调用子类Cat的析构函数; + // 那么子类属性在堆上的数据就可能泄露, + // +} +``` + +>总结: +> +>1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象(我的理解就是多态调用时,也能够调用子类的析构函数,没有虚析构或纯虚析构是不能的) +>2. 如果子类在都定义时没有定义指针类型的成员属性,也就是没有堆区数据,可以不写虚析构或纯虚析构 +>3. 拥有纯虚析构函数的类也属于抽象类 +>4. 虚析构、纯虚析构一定是在基类中,关键字virtual也是在基类里 + +#### 4.8.5 多态案例·制作饮品 + +基本描述:制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料 + +利用多态技术实现,提供抽象制作饮品基类,提供子类制作咖啡和茶叶 + +```c++ +class AbstractDranking { +public: // 可以有几个纯虚函数 + virtual void boil() = 0; // 煮水 + virtual void brew() = 0; // 冲泡 + virtual void pourCup() = 0; // 倒入杯中 + virtual void addsomething() = 0; // 加东西 + // 制定流程顺序 + void dowork() { + boil(); + brew(); + pourCup(); + addsomething(); + } +}; + +class Tea : public AbstractDranking { + void boil() { + cout << "用农夫山泉煮水" << endl; + } + void brew() { + cout << "用紫砂壶冲泡" << endl; + } + void pourCup() { + cout << "倒入茶杯中" << endl; + } + void addsomething() { + cout << "加入一些枸杞" << endl; + } +}; +class Coffee : public AbstractDranking { + void boil() { + cout << "用自来水煮水" << endl; + } + void brew() { + cout << "用简单器皿冲泡" << endl; + } + void pourCup() { + cout << "倒入咖啡杯中" << endl; + } + void addsomething() { + cout << "加入一些suger" << endl; + } +}; + +void test01(AbstractDranking *ad) { + ad->dowork(); + delete ad; // 记得释放,防止内存泄露 +} +int main() { + test01(new Tea); // 直接就把new写进去了 + cout << "===================" << endl; + test01(new Coffee); + system("pause"); + return 0; +} +``` + +#### 4.8.6 多态案例·电脑组装 + +电脑主要组成部件为 CPU(用于计算),显卡(用于显示),内存条(用于存储) + +将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和AMD厂商 + +创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口;测试时组装三台不同的电脑进行工作 + +```C++ +// 几个基本硬件抽象类 +class CPU { +public: + virtual void calculate() = 0; +}; +class VideoCard { +public: + virtual void display() = 0; +}; +class Memory { +public: + virtual void storage() = 0; +}; + +// 具体化这几个硬件(重写) +class InterCPU : public CPU { +public: + void calculate() { + cout << "inter的CPU开始计算" << endl; + } +}; +class AmdCPU : public CPU { +public: + void calculate() { + cout << "amd的CPU开始计算" << endl; + } +}; + +class NvidiaVideoCard : public VideoCard { +public: + void display() { + cout << "英伟达的显卡正在使用" << endl; + } +}; +class AmdVideoCard : public VideoCard { +public: + void display() { + cout << "AMD的显卡正在使用" << endl; + } +}; + +class PirateMemory :public Memory { +public: + void storage() { + cout << "海盗船的内存条正在使用" << endl; + } +}; +class JinMemory : public Memory { +public: + void storage() { + cout << "金士顿的内存条正在使用" << endl; + } +}; + +// 电脑类把这几个组装起来 +class Computer { +public: + Computer(CPU *cpu, VideoCard *vc, Memory *m) { + this->cpu = cpu; + this->vc = vc; + this->m = m; + } + void work() { + cpu->calculate(); + vc->display(); + m->storage(); + } + ~Computer() { + if (cpu != NULL) { + delete cpu; + cpu = NULL; + } + if (vc != NULL) { + delete vc; + vc = NULL; + } + if (m != NULL) { + delete m; + m = NULL; + } + } +private: + CPU *cpu; + VideoCard *vc; + Memory *m; +}; + +int main() { + // 每台电脑都是要新的一个零件 + Computer c1(new InterCPU, new AmdVideoCard, new PirateMemory); + c1.work(); + cout << "=================" << endl; + Computer c2(new AmdCPU, new NvidiaVideoCard, new JinMemory); + c2.work(); + cout << "=================" << endl; + Computer *c3 = new Computer(new InterCPU, new NvidiaVideoCard, new PirateMemory); + c3->work(); // 千万记得new要用 *c3去接收啊 + delete c3; + cout << "=================" << endl; + system("pause"); + return 0; +} +``` + +## 五、模板 + +- 模板就是建立通用的模具,大大==提高复用性== +- 模板不可以直接使用,它只是一个框架 +- typename关键字是在模板广泛使用之后才引用C++语言的,很多程序员任然只用class,效果一样。 + +### 5.0 书上补充(非常重要) + +#### 5.0.1 typename跟class的区别 + +​ 假定T是一个模板类类型参数,当编译器遇到类似T::men这样代码时,它不会知道men是一个类型成员还是一个static数据成员,直到实例化时才会知道。但是,为了处理模板,编译器必须知道名字是否表示一个类型。 +​ 例如:假定T是一个类型参数的名字,当编译器遇到这语句时:T::size_type * p; 它需要知道这是正在定义一个名为p的变量还是将一个名为size_type的static数据成员与名为p的变量相乘;默认情况下,C++语言假定通过作用域运算符(::)访问的名字不是类型,因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型,我们通过==关键字typename==来实现这一点(不能使用class),所以除了通知编译器一个名字表示类型时,必须使用typename,其他时候typename跟class没任何差别。 + +简单总结:==使用using这种来命名别称时,或者其它,如果类型中出现了“::”的情况,前面就一定要加typename==。 + +```c++ +template +typename T::value_type func(const T &c) { + if (!c.empty()) + return c.back(); + else + return typename T::value_type(); +} + +// 同理,在pcl库还看到很多这样的(后面两个就必须加typename,表示是类型) +using value_type = PointT; +using reference = PointT&; +using const_reference = const PointT&; +using difference_type = typename VectorType::difference_type; +using size_type = typename VectorType::size_type; +``` + +​ 解读:func函数期待一个容器类型的实参,它使用typename指明其返回类型并在c中没有元素时生成一个值初始化的元素返回给调用者。 + +如下面的一个练习:编写函数,接收一个容器的引用,打印容器中的元素,(1)使用容器的size_type和size成员来控制打印元素的循环;(2)改用begin和end返回的迭代器来控制循环 + +```c++ +#include // std::for_each算法需要这个头文件 +template +void func(const T &t) { + // 第一小题: + // 下面一定要typename(没有一定报错),表示类型,,而不是作用域符号::默认的代表的数据成员 + for (typename T::size_type i = 0; i < t.size(); ++i) { + std::cout << t[i] << std::endl; + } + + // 第二小题,第1种:这也一定要typename + std::for_each(t.begin(), t.end(), [](typename T::value_type val) {std::cout << val << std::endl; }); + + // 第二小题,第2种:自己写类型也一定要typename,当然也可以用auto自己推导 + //for (auto iter = t.begin(); iter != t.end(); ++iter) { + for (typename T::const_iterator iter = t.begin(); iter != t.end(); ++iter) { + std::cout << *iter << std::endl; + } +} + +int main(int argc, char*argv[]) { + std::vector vec = { "hello", "wotld", "this", "is" }; + std::deque d(3, 5); + func(vec); + func(d); + system("pause"); + return 0; +} +``` + +​ 特别注意:==当使用类型参数::类型时,一定要在前面加typename,即好比`typename T::size_type index= 0;`==,因为这样才能告诉编译器,使用的是size_type这种类型,而不是作用域符号(::)默认代表后面的一个成员名称。如果不加,就可能会得到这个错误提示==语法错误: 意外的令牌“标识符”,预期的令牌为“;”==,或是更加直白的==“value_type”: 类型 从属名称的使用必须以“typename”为前缀==(第11行没加typename就是和这个错误) + +--- + +#### 5.0.2 默认模板实参、尾置返回类型与类型转换 + +==默认模板实参==: + +在新标准中可以为函数和类模板提供模板实参,而更早的C++标准只允许为类模板提供默认实参。 + +```c++ +template > +int compare(const T &v1, const T &v2, F f = F()) { + if (f(v1, v2)) return -1; + if (f(v2, v1)) return 1; + return 0; +} +int main(int argc, char*argv[]) { + // 这一行就是默认用的less小于比较 + int res1 = compare(42, 0); // 1 + // 下面这样就是用的greater大于比较(注意传进去可调用对象的形式) + int res2 = compare(0, 42, std::greater()); // 1 + return 0; +} +``` + +Tips: + +- F也是一种数据类型,这里表示可调用对象的类型; +- 第二行后面的 F f = F() 这是==定义了一个新的函数参数f,绑定到一个可调用对象上==,所以第3、4行写的是f(v1, v2) 这也很重要。这也是指出f是将类型F的一个默认初始化的对象。 +- 注意第11行代码,我们自己传进去了一个可调用对象 + +--- + +==显式实例化==: + +​ 当模板被使用时才会进行实例化,那么相同的实例可能出现在多个对象文件中,当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。 +​ 在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重,在新标准中,可以通过==显式实例化==来避免这种开销,一个显式实例化有如下形式(declaration是一个类或函数声明): +​ extern template declaration; // 声明 +​ template declaration; // 定义(这就是实例化定义了) + +举个例子就是: + extern template class My_class; // 声明 + template int compare(const int&, const int&); // 定义 + +当编译器遇到 extern 模板声明时,它不会再本文件中生成实例化代码,那么这也意味着承诺再程序其它位置有该实例化的一个非extern声明(定义)。 + +--- + +==参数类型进行自己推断的时候,要统一==,好比下面: + +```c++ +template +int compare(const T&, const T&); +(a) compare("hi", "world"); // 这是错的:const char[3] 和 const char[6],推断时两个实参类型不一致 +(b) compare("bye", "dad"); // 这就是ok的,两个参数类型都是const char[4] + +(c) compare("hi", "world"); // 这就是ok的,显式指定模板类型 +``` + +--- + +==显式指定类型和位置返回类型(挺重要的)==: + +显式似乎就好像==c++都是模板编程==,不显示的指定参数类型,它就会推断,能推断出来就可以,若是不能推断出来就会编译不通过(这一点挺重要的,对后续无论是函数还是类的使用): + +```c++ +int a = 1, c = 2; +double b = 2; +std::max(a, b); // 错误;无法推断类型应该是int还是doubule +std::max(a, b); // 正确:指定显式模板参数,所以也是可以std::max(a, b); +std::max(a, c); // 正确,隐式推断出来类型都是int +``` + + + +==尾置返回类型与类型转换==:例如希望编写一个函数,接受表示序列的一对迭代器和返回序列中一个元素的引用: + +```c++ +template +auto func(It beg, It end) -> decltype(*beg) { + return *beg; // 返回序列中一个元素的引用 +} +int main(int argc, char*argv[]) { + std::vector vec = { "hello", "wotld", "this", "is" }; + std::deque d(3, 5); + + auto &i = func(vec.begin(), vec.end()); // 返回的是string& + auto &s = func(d.begin(), d.end()); // 返回的是int& + return 0; +} +``` + +解读: + +- 我们并不知道返回的数据类型具体是什么,但知道所需数据类型是所处理的序列的元素类型,所以第2行函数返回数据类型没办法直接指定,所以就用auto; +- 只用auto也是不够的(没有位置返回类型调用会直接报错的),类型推断不出来; +- 因为知道函数应该返回`*beg`(这里的星是解引用),然后可以用`decltype(*beg)`来获取此表达式的类型,但是在编译器遇到函数的参数列表之前,beg都是不存在的,所以为了定义此函数,必须使用尾置返回类型,由于尾置返回出现在参数列表后,它可以使用函数的参数。 +- 此例中,通知编译器函数func的返回类型与解引用beg参数的结果类型相同。==解引用运算符返回一个左值==,因此通过decltype推断的类型为beg表示的元素的类型的引用。所以int序列,则返回类型就是 int&。 + +以上这都只能获取到引用,无法直接获得所需要的类型,好比上面func函数,如果我们想要返回一个元素的值而非引用,那就要用到==标准库的类型转换模板==,这些模板定义在头文件`#include `,上面的func的写法就成了: + +```c++ +#include +template +auto func(It beg, It end) -> + typename std::remove_reference::type // 核心是这行 +{ + return *beg; +} +auto i = func(vec.begin(), vec.end()); // 是auto i,不是auto &i了 +``` + +解读: + +- std::remove_reference模板有一个模板类型参数和一个名为type的(public)类型成员,如果用一个引用类型实例化remove_reference,则type将表示被引用的类型。好比std::remove_reference\,则type成员将是int; +- 所以std::remove_reference::type中:decltype(*beg)得到的是元素引用类型,remove_reference::type脱去引用,剩下元素类型本身; +- 上面也讲到过,为了使用模板参数的成员,必须用typename来告诉编译器。 + +下表为==标准类型转换模板==: + +| 对Mod,中Mod为 | 若T 为 | 则Mod\::type为 | +| -------------------- | -------------------------- | ----------------- | +| remove_reference | X&或X&&
否则 | X
T | +| add_const | X&、const X或函数
否则 | T
const T | +| add_lvalue_reference | X&
X&&
否则 | T
X&
T& | +| add_rvalue_reference | X&或X&&
否则 | T
T&& | +| remove_pointer | x*
否则 | X
T | +| add_pointer | X&或X&&
否则 | X*
T\* | +| make_signed | unsigned X
否则 | X
T | +| make_unsigned | 带符号类型
否则 | unsigned X
T | +| remove_extent | X[n]
否则 | X
T | +| remove_all_extents | X[n1]\[n2]...
否则 | X
T | + +根据上表:每个类型模板的工作方式都与std::remove_reference类似,每个模板都一个名为type的public成员,表示一个类型。简单总结来说,如果T是一个之类类型,则std::remove_pointer\::type是T指向的类型,如果T不是一个指针,则不会进行任何转换,从而type具有与T相同的类型。 + +--- + +==`特别特别重要`==,对于函数传递时,类似func(123)是错的,num=123;func(num);又是可以的一个说明: + +```c++ +// 1:绑定非const右值 +void f1(int &&index) { + std::cout << index << std::endl; +} +// 2:左值和const右值 +void f2(const int &index) { + std::cout << index << std::endl; +} +// 以下的说明非常重要 +void test1() { + f1(123); // 可以的,函数参数是非const右值 + f2(456); // 可以的,函数参数是左值和const右值 + + int i = 789; + //f1(i); // 错误,无法将右值引用绑定到左值 + f1(std::move(i)); // 这样就可以了 + + // 如果f2函数中没有const,直接f2(456)也是错误的。 +} +``` + +--- + +#### 5.0.3 std::move的理解 + +==std::move的理解==: + +​ 来理解一下std::move是如何定义的(就是下面my_move函数):move的函数参数T&&是一个指向模板类型参数的右值引用,通过==引用折叠==(我的理解是参看上面的表的内容),此参数可以与任何类型的实参匹配,特别是,既可以传递给move一个左值,也可以传递给它一个右值。 + +```c++ +#include // 书上说remove_reference需要这个头文件,vs中没加好像也行 +template +typename std::remove_reference::type&& my_move(T&& t) { + return static_cast::type&&>(t); +} // 上面的 std::remove_reference::type 可以用 std::remove_reference_t 代替,注意这种 _t的方式是c++14后才有的特性 +void test1() { + std::string s1("hi"), s2; + s2 = my_move(std::string("ok!")); // 正确,从一个右值移动数据 + s2 = my_move(s1); // 正确:但在赋值后,s1的值是不确定的 +} +``` + +再次说明,这里的my_move,就是std::move的实现,我为了作区分,文字里写的move就是上面的my_move,也代表std::move + +那么开始std::move是如何工作的: + +- 针对第8行(可以认为就是传的一个右值): + - std::remove_referebce\的type成员推断出是string,(前面一定要加typename,说明是类型) + - my_move函数的返回类型就是string&&,函数参数t的类型也是string&& + - 那么这个函数调用的实例化:string&& my_move(string &&t) + - 函数体返回 static_cast\(t),t的类型就已经是string&&,于是类型转换什么都不用做,调用的结果就是他所接受的右值引用. +- 第9行,传进来的是一个左值,那么: + - 推断出T的类型为string& (string的引用,而非普通string(这里不是很清楚为啥)) + - 因此,std::remove_reference用的是string&进行实例化 + - std::remove_reference\的type成员是string,my_move的返回类型仍是string&& + - my_move的函数参数t实例化为string& &&,会折叠为string& (这里也不是很懂了) + - 故:这个实例化调用是 string&& my_move(string &t) + - 这就是希望将一个右值引用绑定到一个左值。这个函数的函数体返回static_cast\(t),在此情况下,t的类型为string&,cast将其转换为string&&。 + +说明:从一个左值 static_cast 到一个右值是允许的。通常情况下,static_cast只能用于其它合法的类型转换,但有一条针对右值引用的特许规则:==虽然不能隐式地将一个左值转换为右值引用,但可以用static_cast显示地讲一个左值转换为一个右值引用==。 + +--- + +==std::forward==: + +这一小节再写一句:很多时候参数传递,特别是函数代码中有其他函数的引用,参数传递进去后一些细节会被改变,这时候就要用`std::forward`,它会保持实参类型的所有细节: + +```c++ +template +void flip(F, f, T1 &&t1, T2 &&t2) { + f(std::forward(t2), std::forward(t1)); +} +``` + +如果我们用flip(g, i, 42),i将以int&类型传递给g,42将以int&&类型传递给g(如果不用std::forward,引用这些会有改变,就不细写了) + +--- + +#### 5.0.4 可变参数模板(==...==)以及==sizeof...== 运算符 + +==可变参数模板(使用...)==: + +补充:( sizeof... 运算符 + +当需要知道包中有多少元素时,可以使用 sizeof...运算符,类似sizeof,然后sizeof...也返回一个常量表达式,且不会对其实参求值:) + +​ 一个==可变参数模板==就是一个接受不可变数目参数的模板函数或模板类。其中可变数目的参数被称为==参数包==,存在两种参数包:==模板参数包==,表示零个或多个模板参数;==函数参数包==表示零个或多个函数参数。 + +​ 用一个省略号来指出一个模板参数或函数参数表示一个包;在一个模板参数列表中==class...==或==typename...==指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。 + +```c++ +template +void foo(const T &t, const Args& ... rest) { + std::cout << sizeof...(Args) << std::endl; // 类型参数的数目 + std::cout << sizeof...(rest) << std::endl; // 函数参数的数目 +} +``` + +- Args是一个模板参数包(表示零个或多个模板类型参数);rest是一个函数参数包(表示零个或多个函数参数) + +``` +int i = 0; double d = 3.14; std::string s = "hello!"; +foo(i, s, 42, d); // 包中有3个参数 +foo(s, 42, "hi"); // 包中有2个参数 +foo(d, s); // 包中有1个参数 +foo("hi"); // 空包 +``` + +编译器会为foo实例化出四个不同的版本。 + +​ 编写可变参数函数模板: + +​ 可变参数函数通常是递归的,第一步调用处理包中的第一个实参,然后剩余实参调用自身。可以定义print函数为这样的模式,每次递归调用将第二个实参打印到第一个实参表示的流中,为了终止递归,一定还要定义一个非可变参数的print函数,它接收一个流和一个对象: + +```c++ +// print版本一: +template +std::ostream& print(std::ostream &os, const T &t) { + return os << t; // 包中最后一个元素之后不打印分隔符 +} +// print版本二: +template +std::ostream& print(std::ostream &os, const T &t, const Args&... rest) { + os << t << ", "; + return print(os, rest...); +} +void test01() { + int i = 123; double d = 3.14; std::string s = "hello!"; + print(std::cout, i, s, 42); +} +``` + +调用顺序: t rest + +- print(cout, i, s, 42) i s, 42 +- print(cout, s, 42) s 42 +- print(cout, 42) // 最后一次就是调用非可变参数版本的print + +注意:一定要有版本一,当定义可变版本的print时,非可变参数版本的声明必须在作用域中,否则可变参数版本会无线递归,会报错的。 + +#### 5.0.5 实现shared_ptr和unique_ptr + +下面是练习的解答(我没自己写):编写你自己版本的shared_ptr和unique_ptr + +shared_ptr + +```cpp +#pragma once +#include +#include "delete.h" + +namespace cp5 +{ + template + class SharedPointer; + + template + auto swap(SharedPointer& lhs, SharedPointer& rhs) + { + using std::swap; + swap(lhs.ptr, rhs.ptr); + swap(lhs.ref_count, rhs.ref_count); + swap(lhs.deleter, rhs.deleter); + } + + template + class SharedPointer + { + public: + // + // Default Ctor + // + SharedPointer() + : ptr{ nullptr }, ref_count{ new std::size_t(1) }, deleter{ cp5::Delete{} } + {} + // + // Ctor that takes raw pointer + // + explicit SharedPointer(T* raw_ptr) + : ptr{ raw_ptr }, ref_count{ new std::size_t(1) }, deleter{ cp5::Delete{} } + {} + // + // Copy Ctor + // + SharedPointer(SharedPointer const& other) + : ptr{ other.ptr }, ref_count{ other.ref_count }, deleter{ other.deleter } + { + ++*ref_count; + } + // + // Move Ctor + // + SharedPointer(SharedPointer && other) noexcept + : ptr{ other.ptr }, ref_count{ other.ref_count }, deleter{ std::move(other.deleter) } + { + other.ptr = nullptr; + other.ref_count = nullptr; + } + // + // Copy assignment + // + SharedPointer& operator=(SharedPointer const& rhs) + { + //increment first to ensure safty for self-assignment + ++*rhs.ref_count; + decrement_and_destroy(); + ptr = rhs.ptr, ref_count = rhs.ref_count, deleter = rhs.deleter; + return *this; + } + // + // Move assignment + // + SharedPointer& operator=(SharedPointer && rhs) noexcept + { + cp5::swap(*this, rhs); + rhs.decrement_and_destroy(); + return *this; + } + // + // Conversion operator + // + operator bool() const + { + return ptr ? true : false; + } + // + // Dereference + // + T& operator* () const + { + return *ptr; + } + // + // Arrow + // + T* operator->() const + { + return &*ptr; + } + // + // Use count + // + auto use_count() const + { + return *ref_count; + } + // + // Get underlying pointer + // + auto get() const + { + return ptr; + } + // + // Check if the unique user + // + auto unique() const + { + return 1 == *refCount; + } + // + // Swap + // + auto swap(SharedPointer& rhs) + { + ::swap(*this, rhs); + } + // + // Free the object pointed to, if unique + // + auto reset() + { + decrement_and_destroy(); + } + // + // Reset with the new raw pointer + // + auto reset(T* pointer) + { + if (ptr != pointer) + { + decrement_n_destroy(); + ptr = pointer; + ref_count = new std::size_t(1); + } + } + // + // Reset with raw pointer and deleter + // + auto reset(T *pointer, const std::function& d) + { + reset(pointer); + deleter = d; + } + // + // Dtor + // + ~SharedPointer() + { + decrement_and_destroy(); + } + private: + T* ptr; + std::size_t* ref_count; + std::function deleter; + + auto decrement_and_destroy() + { + if (ptr && 0 == --*ref_count) + delete ref_count, + deleter(ptr); + else if (!ptr) + delete ref_count; + ref_count = nullptr; + ptr = nullptr; + } + }; +}//namespace +``` + +unique_ptr: + +```cpp +#include "debugDelete.h" + +// forward declarations for friendship + +template class unique_pointer; +template void +swap(unique_pointer& lhs, unique_pointer& rhs); + +/** +* @brief std::unique_ptr like class template. +*/ +template +class unique_pointer +{ + friend void swap(unique_pointer& lhs, unique_pointer& rhs); + +public: + // preventing copy and assignment + unique_pointer(const unique_pointer&) = delete; + unique_pointer& operator = (const unique_pointer&) = delete; + + // default constructor and one taking T* + unique_pointer() = default; + explicit unique_pointer(T* up) : ptr(up) {} + + // move constructor + unique_pointer(unique_pointer&& up) noexcept + : ptr(up.ptr) { up.ptr = nullptr; } + + // move assignment + unique_pointer& operator =(unique_pointer&& rhs) noexcept; + + // std::nullptr_t assignment + unique_pointer& operator =(std::nullptr_t n) noexcept; + + + + // operator overloaded : * -> bool + T& operator *() const { return *ptr; } + T* operator ->() const { return &this->operator *(); } + operator bool() const { return ptr ? true : false; } + + // return the underlying pointer + T* get() const noexcept{ return ptr; } + + // swap member using swap friend + void swap(unique_pointer &rhs) { ::swap(*this, rhs); } + + // free and make it point to nullptr or to p's pointee. + void reset() noexcept{ deleter(ptr); ptr = nullptr; } + void reset(T* p) noexcept{ deleter(ptr); ptr = p; } + + // return ptr and make ptr point to nullptr. + T* release(); + + + ~unique_pointer() + { + deleter(ptr); + } +private: + T* ptr = nullptr; + D deleter = D(); +}; + + +// swap +template +inline void +swap(unique_pointer& lhs, unique_pointer& rhs) +{ + using std::swap; + swap(lhs.ptr, rhs.ptr); + swap(lhs.deleter, rhs.deleter); +} + +// move assignment +template +inline unique_pointer& +unique_pointer::operator =(unique_pointer&& rhs) noexcept +{ + // prevent self-assignment + if (this->ptr != rhs.ptr) + { + deleter(ptr); + ptr = nullptr; + ::swap(*this, rhs); + } + return *this; +} + + +// std::nullptr_t assignment +template +inline unique_pointer& +unique_pointer::operator =(std::nullptr_t n) noexcept +{ + if (n == nullptr) + { + deleter(ptr); ptr = nullptr; + } + return *this; +} + +// relinquish contrul by returnning ptr and making ptr point to nullptr. +template +inline T* +unique_pointer::release() +{ + T* ret = ptr; + ptr = nullptr; + return ret; +} +``` + +### 5.1 函数模板 + +#### 5.1.1 函数模板语法 + +​ 函数模板作用: + +​ 建立一个通用函数,其函数返回值类型和形参类型可以不具体指定,用一个==虚拟的类型==来代表。 + +语法: + +```c++ +template +函数声明或定义 // 注意两个是紧挨着写的 +``` + +- template --- 申明创建模板 +- typename --- 表明其后面的符号是一种数据类型,可用class代替 +- T --- 通用的数据类型,名称可以替换,通常为大写字母 + +demo: + +```c++ +// 模板就理解为把数据类型的地方换成了`T`, +// 函数要是有返回值,void也可以换成`T` +template +void myswap(T &a, T &b) { + T temp = a; + a = b; + b = temp; +} +// 两种模板使用方式 +void test01() { + int x = 10, y = 20; + // 1、自动推到类型 + //myswap(x, y); // 我的理解是都是int,可以直接推导出来 + + // 2、显示指定类型(就中间加个类型) + myswap(x, y); // 要是x、y是别的类型,就类似加 + std::cout << "x:" << x << std::endl; + std::cout << "y:" << y << std::endl; + + float e = 3.14f, f = 5.23f; + myswap(e, f); + std::cout << "e:" << e << std::endl; + std::cout << "f:" << f << std::endl; +} +``` + +> 总结: +> +> - 函数模板利用关键字`template` +> - 使用函数模板有两种方式:==自动类型推导==、==显示指定类型== +> - 模板的目的是为了提高复用性,将类型参数化 + +#### 5.1.2 函数模板注意事项 + +- 自动类型推导,必须推导出一直的数据类型T,才可以使用 + +demo: + +```c++ +template +void myswap(T &a, T &b) { + T temp = a; + a = b; + b = temp; +} +void test01() { + int x = 10, y = 20; + char z = 'c'; + // 针对自动类型推导 + // 错的,x,z的数据类型推断出来不一致,不行 + //myswap(x, z); + myswap(x, y); // 可以,能推导出一致的T +} +``` + +- 调用模板时,必须要确定出T的数据类型,才可以使用 + +```c++ +// 虽然是模板函数,也可以是没有参数的 +template +void func() { + cout << "这是模板函数" << endl; +} +void test02() { + // 若不是模板,只是普通函数就没问题 + //func(); // 调用失败,没有参数无法自动推导,又没有手动指定 + func(); // 随便指定都行,float也可以,显示指定类型的方式,一定要给T一个类型 +} +``` + +#### 5.1.3 函数模板demo + +​ 利用函数模板封装一个可以对不同数据类型数组从小到大排序的函数,再用char数组和int数组进行测试。 + +```c++ +// 排序模板 +template +void mysort(T array[], int len) { + // 特别注意:传数组名作为参数,不能有引用符号,直接传进来的就是数组的地址了;;然后数组作为参数传进来要带`[]`,千万别忘了 + for (int i = 0; i < len; i++) { + for (int j = i + 1; j < len - 1; j++) { + if (array[i] > array[j]) { + T temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + } + } +} +// 打印模板 +template +void myprint(T array[], int len) { + for (int i = 0; i < len; i++) { + cout << array[i] << ' '; + } + cout << endl; +} +void test01() { + int arr1[] = { 5, 6, 1, 3, 7, 9, 4, 2, 8 }; + int len1 = sizeof(arr1) / sizeof(arr1[0]); + + char arr2[] = "kjhgfds"; + int len2 = sizeof(arr2) / sizeof(char); // 注意这要减去一个或是不减去,下面打印输出都一样 + + mysort(arr1, len1); + mysort(arr2, len2); // 排序模板 + + myprint(arr1, len1); + myprint(arr2, len2); // 打印模板 +} +``` + +#### 5.1.4 普通函数与函数模板的区别 + +主要区别: + +- 普通函数调用时可以发生自动类型转换(即:隐式类型转换) +- 函数模板调用时,如果是利用==自动类型推导==,不会发生隐式类型转换 +- 如果利用==显示指定类型==的方式,可以发生隐式类型转换 + +demo: + +```c++ +template +//T myadd(T a, T &b) { // 这在第3个方法处是要报错的,不能引用 +T myadd(T a, T b) { // 测试时,这不能传引用,特别是要传字符的参数 + return a + b; +} +int fun_add(int a, int b) { + return a + b; +} +void test01() { + int x = 10, y = 20; + char z = 'a'; + // 1、普通函数(这里`a`转成了97) + cout << fun_add(x, z) << endl; // 107 + + // 2、自动类型推导(这是错的,不知道该怎么转) + //cout << myadd(x, z) << endl; + + // 3、显示指定类型(指定了类型,就可以隐式转换) + cout << myadd(x, z) << endl; +} +``` + +> ​ 总结:建议就直接使用`显示指定类型`的方式,调用函数模板,直接就自己确定了通用类型T,不去推断。 + +#### 5.1.5 普通函数与函数模板的调用规则 + +调用规则如下: + +1. 如果函数模板和普通函数都可以实现,优先调用普通函数 +2. 可以通过空模板参数列表来强制调用函数模板 +3. 函数模板也可以发生重载 +4. 如果函数模板可以产生更好的匹配,优先调用函数模板 + +demo: + +```c++ +void func(int a, int b) { + cout << "这是普通函数" << endl; +} +template +void func(T a, T b) { + cout << "这是函数模板的调用" << endl; +} +template +void func(T a, T b, T c) { // 函数模板重载 + cout << "这是重载的函数模板的调用" << endl; +} +void test01() { + int x = 10, y = 20; + // 1、都可以时,就会调用普通函数 + func(x, y); + // 2、通过空参数列表强制调用函数模板 + func<>(x, y); + // 3、函数模板重载 + func(x, y, 100); + + // 4、普通函数、函数模板都是可以的,但是普通函数还要做一次隐式转换,所以直接使用函数模板更优 + char e = 'a', f = 'b'; + func(e, f); +} +``` + +>总结:既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性。 + +#### 5.1.6 具体化的模板 + +​ 模板更多的是对内置数据类型的通用,可是要是是自定义的数据类型,一般就很难处理了,因此c++提供模板的重载,为这些==特定类型==提供==具体化的模板==。 + +​ 具体化的模板大概是这样,以 template <> 开头: +`template<> bool compare(Person &p1, Person &p2) {}` + +demo:在[inicpp](https://github.com/Rookfighter/inifile-cpp/blob/master/include/inicpp.h)这个项目里就用到了很多具体化的模板。 + +```c++ +class Person { +public: + Person(std::string name, int age):m_Name(name), m_Age(age) {} + std::string m_Name; + int m_Age; +}; +template +bool compare(T &a, T &b) { + if (a == b) { + return true; + } + else { + return false; + } +} +// 具体化,显示具体化的原型,定义以`template<>`开头,并通过名称来指出类型 +// 具体化优先于常规模板 +template<> +bool compare(Person &p1, Person &p2) { +// 这行写作 bool compare(Person &p1, Person &p2) { 也是可以的,上面的inicpp炫目就是大量用了这种具体化模板的写法 + if (p1.m_Age == p2.m_Age && p1.m_Name == p2.m_Name) { + return true; + } + else { + return false; + } +} +void test01() { + int x = 10, y = 10; + int ret1 = compare(x, y); // true + std::cout << ret1 << std::endl; + + // 这个类型不行,就要用具体化的原型 + Person p1("zhaoliu", 30), p2("zhaoliu", 30); + int ret2 = compare(p1, p2); + std::cout << ret2 << std::endl; +} +``` + +> 总结: +> +> - 利用具体化的模板,可以解决自定义类型的通用化 +> - 学习模板更多的是在STL能够运用系统提供的模板 + +### 5.2 类模板 + +使用类模板,必须显示指定传入的数据类型 + +#### 5.2.1 类模板语法 + +​ 类模板作用:建立一个通用类,类中的成员 数据类型不具体制定,用一个==虚拟的类型==来代表。 + +语法: + +```c++ +template // class也可以是typename +类 +``` + +demo: + +​ 这个例子不知道为什么,要么是第3行跟第7行这个组合;要么是第2行跟第6行这个组合,==顺序要一致==,不然第16行总是报错。 + +```c++ +// 需要两个类型,就整两个,后面的名字自己起就好 +//template //这行 +template +class Person { +public: + //Person(NameType name, AgeType age) { // 这行 + Person(AgeType age, NameType name) { + this->m_Name = name; + this->m_Age = age; + } + AgeType m_Age; + NameType m_Name; +}; +void test01() { + // 使用类模板,必须显示指定传入的数据类型 + Personp1("孙悟空", 9999); +} +``` + +#### 5.2.2 类模板与函数模板的区别 + +主要区别: + +- 类模板是没有自动类型推导的使用方式 +- 类模板在模板参数列表中可以有默认参数 + +```c++ +template // 可以制定一个默认类型 +class Person { +public: + Person(T1 name, T2 age) { + this->m_Name = name; + this->m_Age = age; + } + T1 m_Name; + T2 m_Age; +}; +void test01() { + //Person p("张三", 18); // 这是错的,是没有自动推导的 + + Personp1("张三", 18); // 有了默认类型,就可以少给一个 + Personp2("王五", 18.5); // 当然也可以给其它的类型 +} +``` + +#### 5.2.3 类模板中成员函数创建时机 + +类模板中成员函数和普通类中成员函数创建时机是有区别的: + +- 普通类中的成员函数一开始(应该是编译时)就可以创建 +- 类模板中的成员函数在调用时才创建 + +理解:写类模板时,你可以写很多成员函数,编译是不会出错的,因为传进来的数据类型不确定(可以是自己写的类),是都可能的,就不会在编译时创建(我理解的),所以是在调用时(即 `.函数名()`)才创建。 + +#### 5.2.4 类模板对象做函数参数 + +类模板实例化的对象,向函数传参有三种方式: + +1. 指定传入的类型 --- 直接显示对象的数据类型 +2. 参数模板化 --- 将对象中的参数变为模板进行传递 +3. 整个类模板化 --- 将这个对象类型 模板化进行传递 + +```c++ +// 1、第一种,指定传入的类型 +void myprint1(Person &person) { + cout << person.m_Name << " " << person.m_Age << endl; +} +void test01() { + Personp1("孙悟空", 1000); + myprint1(p1); +} + +// 2、第二种,参数模板化 +template // 比起第一种就是加了这行,把参数模板化了 +void myprint2(Person &person) { + cout << person.m_Name << " " << person.m_Age << endl; +} +void test02() { + Personp2("猪八戒", 800); + myprint2(p2); +} + +// 3、第三种,整个类模板化 +template +void myprint3(T &person) { // 直接整个都是全部自己推导 + cout << person.m_Name << " " << person.m_Age << endl; + cout << typeid(person).name() << endl; +} +void test03() { + Personp3("猪八戒", 800); + myprint3(p3); +} +``` + +> 总结:比较广泛使用的是==第一种==,即指定传入的类型,比较直接,后面两种都是类模板和函数模板的联合使用了,不够简洁。 + +#### 5.2.5 类模板与继承 + +当类模板碰到继承时,需要注意以下几点: + +- 当子类继承的父类是一个类模板时,子类在继承声明时,要指定出父类中T的类型;如果不指定,不知道类型,编译器就无法给子类分配内存,编译就会出错。 +- 如果想灵活指定父类中T的类型,子类也需要变为类模板 + +```c++ +template +class Base { + T name; +}; +//class Son : Base { // 这就是错的,没指定类型 +class Son : Base { // 必须要指定一个类型 + int age; +}; +``` + +或者: + +```C++ +template +class Base { + T name; +}; +// 子类也写成模板,这样就能灵活指定父类的数据类型了 +template +class Son : Base { + T1 age; +}; +``` + +#### 5.2.6 类模板成员函数类外实现 + +- 类模板中成员函数类外实现时,除了作用域,还要加上模板参数列表 + +```c++ +template +class Person { +public: + Person(T1 name, T2 age); + void showPerson(); + T1 m_Name; + T2 m_Age; +}; +// 开始类外实现 +// 必须要这行template,不然编译器就不认识这T1、T2 +template +Person::Person(T1 name, T2 age) { + this->m_Name = name; + this->m_Age = age; +} +// 类外实现不但要有作用域,还一定要在作用域后跟上模板参数列表,不然就会报错(不加的话就跟普通的类外实现一样了) +template +void Person::showPerson() { + cout << "这就是类外实现" << endl; +} +``` + +#### 5.2.7 类模板分文件编写 + +问题: + +- 类模板中成员函数创建时机是在调用阶段,导致份文件编写时链接不到(就会报外部链接无法解析) + +解决: + +- 解决方式1:导入时不是.h头文件,而是对应的.cpp源文件 +- 解决方式2:直接将声明和实现写到同一个文件中,并把后缀名改为.hpp(.hpp是约定的名称,并不是强制) + +==person.h== + +```c++ +#pragma once +#include +#include +template +class Person { +public: + Person(T1 name, T2 age); + void showPerson(); + T1 m_Name; + T2 m_Age; +}; +``` + +==person.cpp== + +```c++ +#include "person.h" +using namespace std; +// 开始类外实现 +// 必须要这行template,不然编译器就不认识这T1、T2 +template +Person::Person(T1 name, T2 age) { + this->m_Name = name; + this->m_Age = age; +} +template +void Person::showPerson() { + cout << "这就是类外实现" << this->m_Name << endl; +} +``` + +主程序入口: + +```c++ +#include +#include +using namespace std; + +#include "person.h" +// #include "person.cpp" // 第一种解决办法:直接导入源码文件 +void test01() { + Personp("孙悟空", 1000); + p.showPerson(); +}// 这里就会报连接错误,这有两行就有两个外部链接无法解析; +``` + +> ​ 错误解析:因为编译器在看Peron.h时,Person是一个类模板就不会创建,声明都没看,就更不会看到person.cpp源码中的实现了,等到这主程序中第7、8行开始调用时,根本就没有,所以无法解析。 +> +> ​ 解决:最后还是把类模板的声明实现写到一个文件里,并将后缀名改为.hpp + +#### 5.2.8 类模板与友员 + +目标:掌握类模板配合友元函数的类内和类外的实现 + +- 全局函数类内实现 - 直接在类内声明友员,同时完成定义即可 +- 全局函数类外实现 - 需要提前让编译器知道全局函数的存在 + +全局函数类内实现: + +```c++ +template +class Person { + // 注意这是全局函数,并不是成员函数(去回顾友员);这是声明实现一起写了,这里面不能用this指针这种直接去访问成员属性的 + friend void showPerson(Person &person) { // 传进来的Person类型也是推导的 + cout << "这是全局函数类内实现:" << person.m_Name << endl; + } +public: + Person(T1 name, T2 age) { + this->m_Name = name; + this->m_Age = age; + } +private: + T1 m_Name; + T2 m_Age; +}; +void test01() { + Personp1("孙悟空", 1000); + showPerson(p1); // 这是全局函数,不是成员函数,不能 p1.showPerson()的 +} +``` + +全局函数类外实现: + +```c++ +template +class Person; // 要先声明,上面这行也一定不能少 + +template // 提前让知道这个函数的存在(也可以把实现直接写到这里) +void showPerson(Person &person); // 这里用到了Person类模板,所以上面第2行也要先声明 + +template +class Person { + // 下面这行是错的,因为这是普通函数的声明,而下面的实现又是模板的 + //friend void showPerson(Person &person); + friend void showPerson<>(Person &person); // 所以一定要加空的模板参数列表 + // 这里还要注意一点,全局函数类外实现,需要让编译器提前知道这个函数的存在 +public: + Person(T1 name, T2 age) { + this->m_Name = name; + this->m_Age = age; + } +private: + T1 m_Name; + T2 m_Age; +}; +// 下面是类外实现 +template +void showPerson(Person &person) { + cout << "这是全局函数--内外实现:" << person.m_Age << endl; +} + +void test01() { + Personp1("孙悟空", 1000); + showPerson(p1); +} +``` + +#### 5.2.9 类模板案例 + +描述:实现一个通用的数组类,要求如下: + +- 可以对内置数据类型以及自定义数据类型的数据进行存储 +- 将数组中的数据存储到堆区 +- 构造函数中可以传入数组的容量 +- 提供对应的拷贝构造函数以及operator=的重载,防止浅拷贝 +- 提供尾插法和尾删法对数组中的数据进行增加和删除 +- 可以通过`[i]`下标的形式访问数组中的元素(重载`[]`) +- 可以获取数组中当前元素个数和数组的容量 + +`array.hpp`文件:一开始直接写模板不出来,于是先写的int类型,再把对应位置的int改成模板T + +```c++ +#pragma once +#include +#include +using namespace std; + +template +class Array { +public: + int m_len; // 数组容量 + int m_record; // 记录现在数组中有的元素个数 + T ** m_Array; // 定义数组,数组中存放的是指针比较好,这样也能放自定义数据 + // 普通有参构造函数,来确定整个数组长度 + Array(int len) { + this->m_record = 0; // 初始化为0 + this->m_len = len; + m_Array = new T*[len]; + } + // 自定义拷贝构造函数(数组中的数据都是new的,必须得深拷贝) + Array(const Array &arr) { + this->m_len = arr.m_len; + this->m_record = arr.m_record; + // 深拷贝主要是这,自己开辟新的空间,然后把传进来的值赋值 + this->m_Array = new T*[m_len]; + for (int i = 0; i < arr.m_record; i++) { + m_Array[i] = new T(*arr.m_Array[i]); + } + } + // 析构函数 + ~Array() { + // 先释放数组中的数据 + for (int i = 0; i < this->m_record; i++) { + if (this->m_Array[i] != NULL) { + delete m_Array[i]; + m_Array[i] = NULL; + } + } + // 再释放整个数组 + if (this->m_Array != NULL) { + delete[] m_Array; + m_Array = NULL; + } + } + // 重载`=`这一赋值符号,使其为深拷贝 + Array& operator=(const Array &arr) { // 加const防止被修改 + // 因为有初始化,所有要判一下(这里的逻辑,应该都是不会为空) + if (this->m_Array != NULL) { + delete[] this->m_Array; + m_Array = NULL; // 先所有属性清掉再全部赋值 + this->m_len = 0; + this->m_record = 0; + } + // 开辟一个数组放指针,类型就是T* + this->m_Array = new T*[arr.m_len]; + for (int i = 0; i < arr.m_record; i++) { + this->m_Array[i] = new T(*arr.m_Array[i]); + } + this->m_record = arr.m_record; + this->m_len = arr.m_len; + return *this; // 返回本身,好后面可以使用链式编程,a=b=c这种 + } + // 用[]去取值,重载了,尽量都是返回引用 + T& operator[](int index) { + return *this->m_Array[index]; // 数据是指针,解引用先 + } + + // 在末尾添加数据 + void add_Num(T a_num) { + if (this->m_record == this->m_len) { + cout << "数组已满,无法添加" << endl; + return; + } + m_Array[this->m_record] = new T(a_num); + this->m_record++; + } + // 删除末尾的数据 + void sub_Num() { + if (this->m_record == 0) { + cout << "数组已经为空了,不能再删除" << endl; + return; + } + delete this->m_Array[this->m_record - 1]; + this->m_Array[this->m_record - 1] = NULL; + this->m_record--; + } + void showInfo() { + cout << "当前数组元素个数:" << this->m_record << endl; + cout << "当前数组的容量:" << this->m_len << endl; + } +}; +``` + +`随便一个.cpp` + +```c++ +#include +#include +#include "array.hpp" //导入模板类 +using namespace std; + +struct Person { // 自定义数据类型 + string m_Name; + int m_Age; +}; +// 【1】 +void test01() { + // (1)使用类模板时都要给定模板参数列表 + Array arr1(5); + arr1.add_Num(3.14); + arr1.add_Num(4.15); + arr1.add_Num(5.16); + // (2)测试重写的构造函数 + Array arr2(arr1); //Array arr2 = arr1; // 一样的 + + // (3)测试重载的operator=(重载成深拷贝,系统是浅拷贝) + Array arr5(100); // 初始化了一个100的(后面赋值会把这清掉重来,随便给的值) + arr5 = arr2; // 不重载就是浅拷贝,就会出错 + for (int i = 0; i < arr5.m_record; i++) { + cout << *arr5.m_Array[i] << endl; + } + arr2.showInfo(); + // (4)测试重载 `[]` + cout << arr5[1] << endl; // 4.15 +} + +// 注意这里打印时要指定这个类模板的数据类型+参数列表(要打印那个,就把那个实例化前面的数据类型弄过来) +void MyPrint(Array & arr) { + for (int i = 0; i < arr.m_record; i++) { // 注意下面两种不同的打印,后面这解引用要括起来 + cout << "姓名:" << arr.m_Array[i]->m_Name << " 年龄:" << (*arr.m_Array[i]).m_Age << endl; + } +} +// 【2】这里就是测试类模板添加自定义数据类型 +void test02() { + Array arr3(6); + // 可以这样创建结构体指针的初始化 + //Person *p1 = new Person({ "孙悟空", 1000 }); + Person p1 = { "孙悟空1", 1000 }; + Person p2 = { "猪八戒2", 800 }; + struct Person p3 = { "沙和尚3", 500 }; + arr3.add_Num(p1); + arr3.add_Num(p2); + arr3.add_Num(p3); + MyPrint(arr3); // 函数的定义一定要在这个前面,没有实现都要有声明的 + + // [重要]测试重载的`[]` + cout << arr3[1].m_Name << endl; + // 要是没重载,这里必须得是 arr3.m_m_Array[1].m_Name +} + +// 【3】下面仅仅是为了复习数组的两种不同的打印 +void test03() { + struct Person persons[3] = { {"孙悟空", 1000}, {"猪八戒", 800}, {"沙和尚", 500} }; + // 方法1: 指针在结构体中的使用来遍历 + Person *p = persons; + for (int i = 0; i < 3; i++) { + cout << p++->m_Name << endl; + } + // 方法2: 这种方式来遍历数组 + for (Person &k : persons) { + cout << k.m_Name << endl; + } +} +``` diff --git "a/C++/3 c++\346\217\220\351\253\230\347\274\226\347\250\213.md" "b/C++/3 c++\346\217\220\351\253\230\347\274\226\347\250\213.md" new file mode 100644 index 0000000..cad4c84 --- /dev/null +++ "b/C++/3 c++\346\217\220\351\253\230\347\274\226\347\250\213.md" @@ -0,0 +1,4637 @@ +# C++提高编程 + +本阶段主要针对c++==泛型编程==和==STL==技术做详细讲解 + +## 一、STL初识 + +- C++的==面型对象==和==泛型编程==思想,目的就是==复用性的提升== +- 为了建立数据结构和算法的一套标准,诞生了==STL== + +SIL基本概念: + +- STL(Standard Template Library),就是==标准模板库== +- STL从广义上分为: + - 容器(container) + - 算法(algorithm) + - 迭代器(iterator) +- ==容器==和==算法==之间通过==迭代器==进行无缝连接 +- STL 几乎所有的代码都采用了类模板或者函数模板 + +STL大体分为六大组件,分别是: + +`容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器` + +1. 容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据; +2. 算法:各种常用的算法,如sort、find、copy、for_each(遍历)等; +3. 迭代器:扮演了容器和算法之间的胶合剂; +4. 仿函数:行为类似函数(小括号重载那个),可作为算法的某种策略; +5. 适配器:一种用来修饰容器或者仿函数或迭代器接口的东西 +6. 空间适配器:负责空间的配置与管理。 + +--- + +**似乎只要导入了头文件 algorithm ,然后map,vector这些容器就可以直接用了,不用在导入相关头文件了。** + +### 1.1 STL中容器|算法|迭代器 + +- 容器:STL容器就是将运用最广泛的一些数据结构表现出来;常用的数据结构:`数组、链表、树、栈、队列、集合、映射表`等,这些容器分为==序列式容器==和==关联式容器==两种: + + - 序列式容器:强调值的排序,序列式容器中的每个元素均有固定的位置; + - 关联式容器:二叉树结构,个元素之间没有严格的物理上的顺序关系。 + +- 算法:有限的步骤,解决逻辑或数学上的问题(Algorithms);算法分为==质变算法==和==非质变算法==: + + - 质变算法:是指运算过程中会改变区间内元素的内容,如拷贝、替换、删除等; + - 非质变算法:是指运算过程中不会更改区间内的元素内容,如查找、计数、遍历、寻找极值等。 + +- 迭代器:提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。每个容器都有自己专属的迭代器,==迭代器的使用非常类似于指针==,初学阶段可以先理解迭代器为指针。 + + 迭代器种类: + + | 种类 | 支持运算 | 功能 | + | -------------- | --------------------------------------- | -------------------------------------------------------- | + | 输入迭代器 | 只读,支持++、==、!= | 对数据的只读访问 | + | 输出迭代器 | 只写,支持++ | 对数据的只写访问 | + | 前向迭代器 | 读写,支持++、==、!= | 读写操作,并能向前推进迭代器 | + | 双向迭代器 | 读写,支持++、--, | 读写操作,并能向前和向后操作 | + | 随机访问迭代器 | 读写,支持++、--、[n]、-n、<、<=、>、>= | 读写操作,可以以跳跃的方式访问任意数据,功能最强的迭代器 | + + 常用的容器中迭代器种类为==双向迭代器==和==随机访问迭代器==。 + +## 二、STL-常用顺序容器 + +注意: + +- 这里写的方法里,很多还是不全的,还有一些重载版本没有写出来,记得在实际中按需使用;==一般这些容器的成员函数都是能对迭代器进行操作的==,如string容器的 .remove() +- string也可以用std::string::itertor iter = a_string.begin(); iter++ 这种方式来遍历字符串的每一个字符。 + +### 容器适配器 + +​ 除了顺序容器外,标准库还定义了三个顺序容器适配器:==stack==、==queue==、==priority_queue==。 + +​ 默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的。 + +​ queue和priority_queue适配器定义在queue头文件中:标准库queue使用的是先进先出(first-in,first-out,FIFO),==但是priority_queue允许为队列中的元素建立优先级,新加入的元素回排在所有优先级比它低的已有元素之前==。就好比饭店按照客人预定时间而不是来的时间早晚来为他们安排座位。 + + + +​ 还有一个 forward_list 顺序容器,表示一个单项链表,只能顺序访问,迭代器不支持--操作 + +a_vector.data() // 返回一个指向数组中第一个元素的指针,该指针在向量内部使用,是 int *pos; 也可以pos++ + +打印出来是一个地址,结果和 &(*a_vector.begin()) 一样 + + + +### 2.1 string容器 + +特别注意:string的一个字符的格式就是char, +std::string str("hello!"); ==typeid(str[0]).name() 的结果是 char== + + + +本质:stringshiC++风格的字符串,但它本质上是一个类。 + +==string和char * 区别==: + +- char * 是一个指针 +- string是一个类,类内部封装了 char *,管理这个字符串,是一个 char * 型的容器。 + +特点:string内部封装了很多成员方法 + +​ 例如:查找find,拷贝copy,删除delete,替换replace,插入insert等; +​ string管理char * 所分配的内存,不用担心复制越界和取值越界等,由类内部进行负责。 + +创建字符串的时候还可以这样初始化: + +```c++ +std::string str1("this is (pee)."); +std::string str2{"this is (pee)."}; +``` + +这里第一次看到用花括号来初始化的,记录一下吧,感觉用起来是一样的,。 + +#### 2.1.1 构造函数 + +构造函数原型: + +- `std::string();` // 创建一个空的字符串,string str; +- `std::string(const char* s);` // 用字符串s初始化; +- `std::string(const string& str);` // 使用一个string对象初始化; +- `std::string(int n, char c);` // 使用n个字符c来初始化; + +这是string这个类的构造函数的各个重载版本,就很明了了。 + +```c++ +void test01() { + std::string str1; // 默认构造,空的 + + const char* s = "hello"; // C风格字符串 + std::string str2(s); // C风格的字符串转成了string + + std::string str3(str2); // 拷贝构造 + std::string str4 = str3; + + std::string str5(5, 'a'); // 后面只能是字符 +} +``` + +补充: + +```c++ +int main(int argc, char **argv) { + // 必须要有const + const char *cp = "hello world!"; // 以空字符串结束的数组 + char cp1[] = { 'h', 'i'}; // 不是以空字符结束 + + // 拷贝操作是遇到空字符时停止,所以: + std::string str1(cp); + std::string str2(cp1, 2); // 给定个数是ok的 + // 因为没空字符,或给的数字大过数组的个数,则构造函数的行为(比如下面这)就是未定义的, + std::string str3(cp1); + return 0; +} +``` + +#### 2.1.2 赋值操作 + +赋值的函数原型: + +- `std::string& operator=(const char* s);` // C风格字符串来赋值 +- `std::string& operator=(const std::string &str);` // 字符串来赋值 +- `std::string& operator=(char c);` // 字符赋值给字符串 + 所以这个可以:std::string name("zhangsan");const char a_p='d'; name = a_P; + +除了重载的`=`号赋值,还有重载的成员函数`assign` + +- `std::string& assign(const char* s);` // C风格字符串来赋值 +- `std::string& assign(const std::string &s);` // 字符串来赋值 +- `std::string& assign(const char* s, int n);` // 把C风格字符串的前n个赋值给字符串==记住这个== +- `std::string& assign(int n, char c);` // n个字符c来赋值 + +```c++ +void test01() { + std::string str1, str2, str3, str4, str5, str8; + + const char *s = "hello"; + str1 = s; // 就普通赋值 + str2 = str1; + str3 = 'd'; // 相当于把字符转成字符串了 + + str4.assign(s); // asign几乎类似 + str5.assign(str2); + str8.assign(3, 'a'); + + // 需要注意: + const char* a = "hello world"; + std::string b; // C风格得到的是前面的 + b.assign(a, 3); // `hel` + std::string e = "hello world"; + std::string f; // C++风格得到的是后面的 + f.assign(e, 3); // `lo world` + std::cout << "这是b:" << f << std::endl; +} +``` + +#### 2.1.3 字符串拼接 + +作用:实现在字符串末尾拼接字符串 + +函数原型: + +​ 1、成员函数重载运算符`+=`: + +- `std::string& operator+=(const char* str);` + +- `std::string& operator+=(const char c);` + +- `std::string& operator+=(const std::string& str);` + + 2、重载成员函数`append`: + +- `std::string& append(const char* s);` + +- `std::string& append(const char* s, int n);` + + - // 连接上字符串s前n个,注意是C风格的字符串 + +- `std::string& append(const std::string &s);` + +- `std::string& append(const std::string &s, int pos, int n);` + + - // 字符串s中从pos开始的n个字符连接到结尾,C++风格 + +```c++ +void test01() { + std::string str = "hello"; + const char* s = " world"; + str += s; + char c = '!'; + str += c; + + std::string str1 = " how "; + str += str1; + // 这两行是c风格 + str.append(s); + str.append(s, 2); // 直接是前2个 + // 这两行是c++风格 + str.append(str1); + str.append(str1, 1, 2); // 这里要指定开始位置 + std::cout << str << std::endl; + // 注意,要是c++风格的只给了一个int,那就是此处索引到结尾的所有字符(跟上面截取赋值一样) + str.append(str1, 1); + std::cout << str << std::endl; +} +``` + +#### 2.1.4 查找和替换 + +作用:查找字符串索引或是替换指定位置的字符串 + +函数原型: + +- `int find(const std::string& str, int pos=0) const;` + - // 查找str第一次出现的位置,从pos开始查找 + +- `int find(const char* s, int pos=0) const;` +- `int find(const char* s,int pos, int n) const;` + - // 针对C风格字符串s,从pos位置查找s的前n个字符第一次出现的位置 +- `int find(const char c, int pos=0) const;` +- `int rfind(const std::string& str, int pos=npos) const;` // 注意这 + - 剩下的那几个跟find是一样的 +- `std::string& replace(int pos, int n, const std::string& str);` + - 从pos开始n个字符替换为字符串str +- `std::string& replace(int pos, int n, const char* s);` + +```c++ +void test01() { + std::string str = "abcdefgde"; + int pos = str.find("de"); // 3 + pos = str.rfind("de"); // 7 + // 替换(从索引1往后的3个字符替换成`hello`) + str.replace(1, 3, "hello"); // 这就是在原来的字符串上修改 + std::cout << str << std::endl; + + int new_pos = str.find("de", 5); // 7,就是从str的第5个位置开始查找 de +} +``` + +> 总结: +> +> - find或是rfind返回的索引,都是从左往右数的那种 +> - replace在替换时,要指定从哪个位置开始,多少个字符,替换成什么样的字符串 +> - 无论只是`str.replace(1, 3, "hello");`,还是同时去赋值`std::string new_str = str.replace(1, 3, "hello");`,原来的字符串`str`都会被修改,若是有赋值,那么`str.compare(new_str) == 0` ,即是相同的。 + +#### 2.1.5 字符串比较 + +作用:比较两个字符串是否相同 + +- 字符串比较是按字符的ASCII码进行对比 + - 相等 返回 0 # 或者直接就使用 str1 == str2 作比较,相等结果是1,注意。 + - 大于 返回 1 + - 小于 返回 -1 + +函数原型:(这是常函数) + +- `int compare(const std::string &s) const;` // C++风格 + +- `int compare(const char* s) const;` // C风格 + +```c++ +void test01() { + std::string str1 = "hello"; + std::string str2 = "aello"; + int ret = str1.compare(str2); + if (ret == 0) { + std::cout << "str1 等于 str2" << std::endl; + } + else if (ret == 1) { + std::cout << "str1 大于 str2" << std::endl; + } + else if (ret == -1) { + std::cout << "str1 小于 str2" << std::endl; + } +} +``` + +> ​ 总结:字符串对比主要还是用来比较两个字符串是否相等,判断谁小谁大的意义并不是很大。 +> +> ​ 这个还有一些重载版本,可以通过指定 pos1,n1,s2,pos2,n2的方式来对字符串s1和s2的部分进行对比。 + +#### 2.1.6 字符修改|获取|判空|size() + +string中单个字符存取方式有两种 + +- `char& operator[](int n);` // 重载`[]`,通过`[i]`方式取字符 +- `char& at(int n);` // 通过`.at(i)`方式获取字符 + +```c++ +void test01() { + std::string str = "hello"; + // 可用`.size()`获取字符串长度 + for (int i = 0; i < str.size(); i++) { + cout << str[i] << " "; + } + std::cout << std::endl; + for (int i = 0; i < str.size(); i++) { + std::cout << str.at(i) << ' '; + } + std::cout << std::endl; + // 字符串修改 + str[0] = 'x'; + str.at(1) = 'x'; +} +``` + +> 总结: +> +> - 可通过`str.size()`来获取字符串的长度; +> - auto len = str.size(); // 返回的一定是unsigned int,那后续使用时要注意与它运算的要是无符号的,假设n是一个具有负值的int,则表达式str.size() < n 的判断结果几乎肯定是true,这是因为<左边的结果一定是无符号整形,负值n会自动转换成一个比较大的无符号值。 +> - c++的字符串是可以通过下标来修改单个字符的(但是如果字符串定义时加了const,那就是不能修改)。 + +判断一个字符串是否为空,有如下三种方法: + +> std::string name = ""; +> +> 1. if (name.empty()) {} +> 2. if (name.size() == 0) {} +> 3. if (name == "") {} + +​ 几种方法中,`empty()`函数是效率最高,也是最常用的一种。注意:不能使用name==NULL来判断,NULL一般只拿和指针做比较或者赋给指针,string是类,传参进函数时str调用默认的构造函数已经初始化了,并且str都已经是对象了,它不可能为NULL,也不能和NULL比较。 + +#### 2.1.7 插入和删除 + +函数原型: + +- `std::string& insert(int pos, const char* s);` // 插入字符串 +- `std::string& insert(int pos, const std::string& str);` // 两种风格都一样 +- `std::string& insert(int pos, int n, char c);` // 指定处插入n个字符c +- `std::string& erase(int pos, int n = npos);` // 删除从pos开始的n个 +- `std::string& erase(iter1, iter2);` // 是还有其它重载版本的,写代码的时候去看,这里写的都不全 + +```c++ +void test01() { + std::string str = "hello"; + str.insert(2, " zhangsan "); // 索引2处加字符串 + str.insert(1, 5, 'a'); // 在1处加5个a + + str.erase(2, 3); // 从2处开始删除3个字符 + str.erase(0, str.find_first_not_of(" \t\n\r")); // 把字符串最前面的空白删了 + + str.erase(3); // 从3处往后删掉所有 +} +``` + +> ​ 总结:针对删除,若是没有给第2个参数(即删除几个字符),那么看函数原型,是有默认参数的,是字符串最后的位置,所有就会从给的pos往后删完。 + +#### 2.1.8 截取子串substr + +作用:从字符串中截取想要的子字符串 + +函数原型: + +- `std::string substr(int pos=0, int n=npos) const;` // 返回由pos开始的n个字符组成的字符串(注意都是有默认值的,特别不给n的话,也是默认切到结尾) + +```c++ +void test01() { + std::string str = "hello world"; + string substr = str.substr(3); + std::cout << substr << std::endl; // lo world + + std::string email = "songh@foxmail.com"; + int pos = email.find('@'); // 5,截取5个 + std::string name = email.substr(0, pos); + std::cout << name << std::endl; // songh +} +``` + +​ Tips:使用substr是不会改变原来的string的,所以一定要用一个string去接收结果,不然就没有意义。 + +#### 2.1.9 string注意事项 + +​ 直接的"nihao" "abcd"这些是字符串字面值(好像是C风格的字符串),他们是不能直接相加的,如下: + +```c++ +std::string s1 = "hello"; + +std::string b = "niho" + "abcd"; // 非法,字符串字面值不能直接相加 + +std::string a = s1 + "niho" + "abcd"; // 正确,s1是string,加上nihao,会自动转换成string对象,然后再加abcd + +std::string c = "niho" + "abcd" + s1; // 非法,这里就是先nihao + abcd,都是字符串字面值,不能直接相加,就错了,里面至少要有一个string对象。 +``` + +--- + +string类的输入运算符和getline函数分别是如何处理空白字符的: + +- 类似cin >> s string对象会忽略开头的额空白从第一个真正的字符开始,直到遇到下一个==空白==为止; +- 类似std::getline(cin, s) string对象会从输入流中读取字符,直到遇见==换行符==为止。 + +别忘了这种输入: + +```c++ +std::string str1, str2; +while (std::cin >> str1 >> str2) { /*...*/ } +// 控制台输入 hello world str1、str2就得到了这两个词 +``` + +#### 2.1.10 字符串常用处理函数 + +cctype头文件中的函数(可见c++小知识.md中关于头文件的说明): + +```c++ +#include + +std::string s1 = "hello 123 world!!!"; + +decltype(s1.size()) count = 0; +for (auto &c : s1) { // 注意这里是引用就会修改 + std::cout << typeid(c).name() << std::endl; // 类型是char,所以如果要修改,一定是: + c = 'X'; // 注意必须是单引号, + + if (std::ispunct(c)) { // 上面一定有导入了 ,才能有std::ispunct()这些 + ++count; + } + c = std::toupper(c); // 好像不要,也行,前面不加std::就行 +} +std::cout << count << std::endl; // 3 个标点符号 +std::cout << s1 << std::endl; // 把字母都变成大写了 +``` + +>​ 表3.3:cctype头文件中的函数(==c只能是字符==) +> +>#inlcude // 记得导入这个头文件,然后用 +>std::isdigit()这样的写法 +> +>- isalnum(c) // 当c是字母或数字时为真 +> +>- isalpha (c) // 当c是字母时为真 +> +>- isdigit (c) // 当c是数字时为真 +> +> +> +>- islower (c) // 当c是小写字母时为真 +> +>- isupper(c) // 当c是大写字母时为真 +> +>- tolower (c) // 如果c是大写字母,输出对应的小写字母:否则原样输出c +> +>- toupper(c) // 如果c是小写字母,输出对应的大写字母;否则原样输出c +> +> +> +>- ispunct (c) // 当c是标点符号时为真(即c不是控制字符、数字、字母、可打印空白中的一种) +> +>- isgraph (c) // 当c不是空格但可打印时为真 +>- isprint (c) // 当c是可打印字符时为真(即c是空格或c具有可视形式) +>- isspace(c) // 当c是空白时为真(即c是空格、横向制表符、纵向制表符、回车符、换行符、进纸符中的一种) +>- isblank(c) // 字符c是空格(好像跟上面isspace一样的) +>- iscntrl(c) // 当c是控制字符时为真 +>- isxdigit(c) // 当c是十六进制数字时为真 + +### 2.2 vector容器 + +​ 功能:vector数据结构和数组非常相似,也称为==单端数组==,——数组左端是封闭的,一般都是通过右端添加删除。 + +​ vector与普通数组区别:数组是静态空间(定义好了多大就是多大),而vector可以==动态扩展==。 + +- 动态扩展:并不是在原空间之后续接新空间,而是找更大的内存空间,然后将原数据拷贝到新空间,再释放掉原空间。 + +Ps: + +- vector容器的迭代器是支持随机访问的迭代器 + +- vector的迭代器有两对`v.begin() v.end()`和 `v.rbegin() v.rend()`;这个v.rbegin()就是指向最后一个数,v.rbegin()是指向第一个的左边,和前面那对刚好相反。 + +- c++11新引进了两个函数,`cbegin()`、`cend()`也是代表容器的第一个值和最后一个,只是他们返回的数据类型一定是 const_iterator,不能用iterator去接受,数据也不可能修改(只读的操作建议用这个来) + vector\::const_iterator it = v.cbegin(); // 数据还是第一个 + auto it = v.cend(); // 最后一个数据 + +- 若是不知道容器内的数据的类型,想要定义这种类型的话,可以:`value_type` + + ```c++ + std::vector vec(10, 1); + std::vector::value_type val = vec[0]; // 注意这里 + std::cout << val << std::endl; + std::cout << typeid(val).name() << std::endl; + ``` + + 如果需要元素类型的一个引用,使用使用reference或const_reference + + ```c++ + std::vector v{ "a", "ab" }; + // 注意这里一定是要const的,C风格的字符串一定要有const + ``` + +- 从ffmpeg中的代码来的一个说明:给一个vector装数据时,除了传递它的std::vector<类型>::iterator iter = buffer.begin();这个头iter,还可以用传递第一个元素的指针,即==auto my_begin = &buffer[0];==,my_begin就是一个指针,指向vector的第一个元素的地址,然后也可以用my_begin++这种,就是等同于在操作iter: + + - ```c++ + struct Color_RGB { + int r; + int g; + int b; + }; + + int main(int argc, char* argv[]) { + std::vector buffer(3); // 初始化全是0 + + // 特别有C时,这种写法更多 + auto my_begin = &buffer[0]; + for (int i = 1; i <= 3; i++) { + *my_begin = {i*1, i*2, i*3}; + my_begin++; + } + + std::vector::iterator iter = buffer.begin(); + /* // 上面几行赋值代码,等同于这 + for (int i = 1; i <= 3; i++) { + *iter = { i * 1, i * 2, i * 3 }; + iter++; + } + */ + // 这里打印出来就是上面10-14行赋值的结果 + for (; iter != buffer.end(); iter++) { + std::cout << iter->r << " " << iter->g << " " << iter->b << std::endl; + } + system("pause"); + return 0; + } + ``` + + +#### 2.2.0 vector与指针的联系使用(书) + +​ 再注意: std::begin(a_arr)、std::end(a_arr)这俩可以获取数组的首地址和尾地址后一位。 + +​ 注意:在早期c++标准中,如果vector元素还是vector或是其它模板类型,就需要右尖括号和元素类型之间添加一个空格,必须写成:vector > // 这里面必须有空格(可能一些老的编译器还需要) +​ 现在直接写成:vector> 就行。 + +==vector 和 string 的迭代器支持的运算==(n为常数): + +- iter + n iter - n +- iter1 - iter2 // 还支持的运算符有 > >= < <= +- std::cout << (end - begin) / 2 << std::endl; + +但是特别注意:这个是不支持 iter1 + iter2 这种两个迭代器相加的, +所以迭代器的中间值的写法只能是: + +> auto begin = v.begin(), end = v.end(); +> auto mid = begin + (end - begin) / 2; +> +> 绝对不能是: auto mid = (begin + end) / 2; // 两个迭代器不能相加 + +```c++ +std::vector v1{ 1, 2, 3 }; +std::vector v2{ 11, 2, 3 }; +if (v1 == v2) { + std::cout << "一样" << std::endl; +} // 两个vector之间是可以直接比较是否相等的,但还是两个数组是不能这样的,数组名代表的是各自的首地址,可能是不一样的,不能这样比 +``` + +​ ==为什么两个迭代器之间不能相加==:将两个指针相减可以表示两个指针(在同一数组中)相距的距离,将指针加上一个整数也可以表示移动这个指针到某一位置。但是两个指针相加并没有逻辑上的意义,因此两个指针不能相加。 + +#### 2.2.1 vector构造函数 + +后续遇到的补充: + +- std::vector\ vec{"张三", "nihao", "这样子初始化也是可以的"}; // 注意事项花括号 + + ​ 这等价于 std::vector\ vec = {"张三", "nihao", "这样子初始化也是可以的"}; // 这是c++11新标准提供的列表初始化。 + +- std::vector\ v1(10, 1); // 10个元素,每个值就都是1; + 可以直接 `std::vector v(5);`这就是初始化了一个容量,size都是5的容器,里面的5个值都是0; + +- std::vector\ v2{10, 1}; // 2个元素,10和1。 注意这在函数返回时很常见,别用错了 + +- ==把一个数组转变成容器==: + + ```c++ + int ia[] = { 0, 1, 2, 3, 4, 5 }; + std::vector vec(ia, std::end(ia)); + ``` + +函数原型: + +- `std::vector v;` // 模板类实现,默认构造函数 + +- `std::vector(v.begin(), v.end());` // 将v[begin(), end())区间元素拷贝给这个 + + - ```c++ + int arr[] = { 1, 2, 3, 4, 5 }; + std::vector v(std::begin(arr), std::end(arr)); + // 这里的begin、end是获取数组的首地址和末尾地址后一个,是要往这两个函数里面传数组对象的。 + // 当然也能只截取一段值 + std::vector v(arr +1, arr+3); + ``` + +- `std::vector(n, elem);` // 将n个elem拷贝给本身 + +- `std::vector(const vector &vec);` // 拷贝构造函数 + +```c++ +#include +#include // 要导入这个 + +//template // 不知道这里为啥不让函数模板 +void printVector(std::vector &vec) { + //for (vector::iterator iter = vec.begin(); iter != vec.end(); iter++) { + //// vector::iterator 主要是这里不让用,一定得失具体的类型 + // cout << *iter << ' '; + //} + //cout << endl; + for (std::vector::iterator iter = vec.begin(); iter != vec.end(); iter++) { + std::cout << *iter << ' '; + } + std::cout << std::endl; +} +void test01() { + std::vector v; // 无参构造 + for (float i = 0; i < 10; i++) { + v.push_back(i); + } + printVector(v); + + // 这个`++`不能放在begin()后面 + std::vector v1((++v.begin()), v.end()); + printVector(v1); + + std::vector v2(10, 3.14); // 10个3.14 + printVector(v2); + + std::vector v3(v2); // 拷贝构造 + printVector(v3); +} +``` + +一些初始化的注意: + +```c++ +std::vector v1; // size:0, no values. +std::vector v2(10); // size:10, value:0 +std::vector v3(10, 42); // size:10, value:42 +std::vector v4{ 10 }; // size:1, value:10 +std::vector v5{ 10, 42 }; // size:2, value:10, 42 +std::vector v6{ 10 }; // size:10, value:"" // 10并不是string,所以就当做std::vsctor v6(10)来处理了。 +std::vector v7{ 10, "hi" }; // size:10, value:"hi" // 10并不是string,就当做v7(10, "hi")来处理了。 +``` + +#### 2.2.2 vector赋值操作 + +函数原型: + +- `std::vector& operator=(const vector &vec);` // 重载`=` +- `assign(begin, end);` // 将[begin, end)区间中的数据拷贝 +- `assign(n, elem);` // 将n个elem拷贝赋值 + +```c++ +#include +#include +void test01() { + std::vector v; // 无参构造 + for (float i = 0; i < 10; i++) { + v.push_back(i); + } + std::vector v1; // 先构造 + v1 = v; // 在赋值,下面也一样 + + std::vector v2; + v2.assign(v.begin(), v.end()); + + std::vector v3; + v3.assign(10, 3.14); +} +``` + +#### 2.2.3 vector容量和大小 + +函数原型: + +- `empty();` // 判断容器是否为空,返回布尔值 +- `capacity();` // 获取容器的 容量 +- `size();` // 获取容器现在的元素个数 +- `resize(int num);` // 重新指定容器的长度为num + - 若容器变长,则以默认值0填充所有没有使用的位置(个数=capacity() - size()) + - 若容器变短,则末尾超出容器长度的元素被删除 +- `resize(int num, elem);` // 变长的部分用指定的`elem`填充,变短的话也是删除超出容器长度的元素。 + +```c++ +void printVector(vector &vec) { + for (std::vector::iterator iter = vec.begin(); iter != vec.end(); iter++) { + std::cout << *iter << ' '; + } + std::cout << std::endl; +} +void test01() { + std::vector v; // 无参构造 + for (float i = 0; i < 10; i++) { + v.push_back(i); + } + std::cout << v.empty() << std::endl; // 0 + std::cout << v.capacity() << std::endl; // 13 != 10 + std::cout << v.size() << std::endl; // 10 + v.resize(15); + printVector(v); + v.resize(5); + printVector(v); + v.resize(20, 5); + printVector(v); +} +``` + +#### 2.2.4 vector插入和删除 + +函数原型: + +- `push_back(elem);` // 尾部插入元素`elem` + +- `pop_back();` // 删除最后一个元素 + + 下面的必须是迭代器指向位置 + +- `insert(const_iterator pos, elem);` + +- `insert(const_iterator pos, int count, elem);` + + - 前者是指向位置插入元素elem,后者是插入n个elem + +- `erase(const_iterator pos);` + +- `erase(const_iterator start, const_iterator end);` + + - 前者删除一个迭代器指向的元素,后者是删除迭代器从start到end之间的元素 + +- `clear();` // 删除容器内的所有元素 + +```c++ +void printVector(vector &vec) { + for (std::vector::iterator iter = vec.begin(); iter != vec.end(); iter++) { + std::cout << *iter << ' '; + } + std::cout << std::endl; +} +void test01() { + std::vector v; + for (int i = 1; i < 6; i++) { + v.push_back(i * 10); // 尾插 + } + printVector(v); + v.pop_back(); // 尾删 + printVector(v); + + std::vector::iterator iter1 = v.begin(); + v.insert(iter1, 101); // 尽量直接使用`v.begin()` + v.insert(v.begin(), 3, 102); // 插入3个102 + //v.insert(iter1, 3, 102); // 这是错的 + //这里就一定不能再用`iter1`,动态库扩展的,再用就变了,就会 + printVector(v); + + v.erase(v.begin()); // 删掉一个数 + // 下面俩都是清空操作了 + v.erase(v.begin(), v.end()); + v.clear(); + printVector(v); +} +``` + +#### 2.2.5 vector数据存取 + +函数原型: + +- `at(int idx);` // 函数at返回索引位置元素 +- `operator[](int idx);` // 重载`[]` +- `front();` // 返回容器第一个元素 +- `back();` // 返回元素最后一个元素 + +```c++ +void test01() { + std::vector v; + for (int i = 1; i < 6; i++) { + v.push_back(i * 10); // 尾插 + } + for (int i = 0; i < v.size(); i++) { + // 两种访问方式 + cout << v[i] << '/' << v.at(i) << endl; + } + std::cout << "第一个元素:" << v.front() << std::endl; + std::cout << "最后一个元素:" << v.back() << std::endl; +} +``` + +#### 2.2.6 vector互换容器 + +功能:实现两个容器内元素的交换 + +函数原型: + +- `swap(vec);` // 将传进来的vec元素与本身的元素互换 + +```c++ +// 简单的交互 +void printVector(vector &vec) { + for (std::vector::iterator iter = vec.begin(); iter != vec.end(); iter++) { + std::cout << *iter << ' '; + } + std::cout << std::endl; +} +void test01() { + std::vector v, v1; + for (int i = 1; i < 6; i++) { + v.push_back(i * 10); // 尾插 + } + for (int i = 5; i > 0; i--) { + v1.push_back(i * 10); + } + std::cout << "交换前:" << std::endl; + printVector(v); + printVector(v1); + + v.swap(v1); + std::cout << "交换后:" << std::endl; + printVector(v); + printVector(v1); +} +``` + + + +// swap可以使两个容器互换,以达到使用的收缩内存效果 + +```c++ +void test02() { + std::vector v; + for (int i = 0; i < 100000; i++) { + v.push_back(i); + } + std::cout << "v的容量:" << v.capacity() << std::endl; // 13万多 + std::cout << "v的个数:" << v.size() << std::endl; // 10万 + // 会发现容量比实际使用的个数多很多 + + v.resize(5); + + std::cout << "v的容量:" << v.capacity() << std::endl; // 13万多 + std::cout << "v的个数:" << v.size() << std::endl; // 5 + // resize()后,容量却没减小,就浪费了很多空间 + + // 收缩内存 + std::vector(v).swap(v); // 匿名对象,这行执行完就释放 + /*拆解:1、构建了一个匿名对象,是用v去构造的vector(v),v的个数现在是5个,所以这个匿名对象容量、个数都是5,; + 2、这个匿名对象和v交换了指针指向,然后现在的v容量个数都是5了, + 匿名对象是容量有13万多了,但是这行执行完就释放了。*/ + std::cout << "v的容量:" << v.capacity() << std::endl; // 5 + std::cout << "v的个数:" << v.size() << std::endl; // 5 +} +``` + +#### 2.2.7 vector预留空间 + +作用:减少vector在动态扩展容量是的扩展次数 + +函数原型: + +- `reserve(int len);` // 容器预留len个元素长度,预留位置不初始化,元素不可访问 + +```c++ +void test01() { + std::vector v; + // 预留空间 + v.reserve(100000); // 要这就nums=1;不要nums=30 + + int nums = 0; // 记录扩展次数 + int *p = NULL; + for (int i = 0; i < 100000; i++) { + v.push_back(i); // 这个放上面,一开始才有数 + //if (p != v.begin()) // 这是错的,不能这么写 + // 如果p不等于数组首地址了,就把首地址赋值给p + if (p != &v[0]) { + p = &v[0]; // 一定是v[0],数组首地址来赋值 + nums++; + } + } + std::cout << "nums:" << nums << std::endl; // 30 + // 即数组首地址变了30次,需要30次动态扩展 +} +``` + +> 总结:如果数据量较大,可以一开始利用reserve预留空间 + +#### 2.2.8 书上内容补充(书) + +拷贝构造的补充: + +```c++ +std::vector vec(other_vec); // 拷贝 other_vec 的元素 +std::vector vec(other_vec.begin(), other_vec.end()); // 拷贝 other_vec 的元素 +``` + +- 对于第一种,接受一个容器创建其拷贝的构造函数,必须容器类型和元素类型都要相同; + +- 对于第二种,接受两个迭代器创建拷贝的构造函数,只需要元素的类型能够相互转换,==容器类型和元素类型可以不同==。比如从一个list初始化一个vector + + ```c++ + // 不同容器类型,不同元素类型的一个拷贝初始化 + std::list ilist(5, 3); + std::vector dev(ilist.begin(), ilist.end()); + + // 同类型容器,不同元素类型的一个拷贝初始化(但其类型要可以相互转换的) + std::vector ivc(5, 5); + std::vector dev2(ivc.begin(), ivc.end()); + ``` + + 还有比如:将一个`list`中的`char *`指针元素赋值给一个`vector`中的`string`。 + + ```c++ + std::list li{"aa", "bb", "cc"}; + std::vector vec(li.begin(), li.end()); + // 或者用assign + vec.assign(li.begin(), li.end()); + ``` + +==特别注意==:向一个 vector、string 或deque 中insert插入元素会使所有指向容器的迭代器、引用和指针失效。而且插入任何位置都是合法的,但可能会很耗时。 + +--- + +​ 以下是一种写法操作,挺好的,可细看:从一个list中拷贝元素到deque中,值为偶数的放一个,为奇数的放另外一个: + +```c++ +int main() { + std::deque deq1, deq2; + std::list li{ 1, 2, 3, 4, 5 }; + for (auto k : li) { + (k % 2 == 0 ? deq1 : deq2).push_back(k); + // 上面是我的写法,下面的写法等价,书上的 + // (k & 0X1 ? deq1 : deq2).push_back(k); + } + return 0; +} +``` + +--- + +还有一个操作后返回值的问题,也挺重要: + +- insert后,都是有返回值的(只是可以不去接收),返回的就是插入值的迭代器位置。可以在循环中使用 iter = vec.insert(); 这样的的代码,然后iter就一直是这个容器的首地址(注意上面说的失效问题)。 +- 同理 erase也是,erase都返回指向删除的(最后一个)元素之后位置的迭代器,即若j是i之后的元素,那么erase(i)将返回指向j的迭代器(当然也可以不去接收)。 + +这俩最常用的就是这种,比如下面循环删除一个list中所有的奇数元素 + +```c++ +int main() { + std::list li{ 1, 2, 3, 4, 5 }; + auto iter = li.begin(); + while (iter != li.end()) { + if (*iter % 2) + // 核心是下面这步(当然其它地方擦除可以不要返回值的) + iter = li.erase(iter); + else + ++iter; + } + return 0; +} +``` + +#### 2.2.9 vector存放不同数据类型及打印 + +1、内置数据类型: + +- 容器:`vector` +- 算法:`for_each` +- 迭代器:`vector::iterator` + +注意:v.begin()是数组的第一个位置,然而v.end()是数组最后一个位置还要往后偏移一个。 + +```c++ +#include +#include // 记得这头文件 +#include // 使用算法的头文件 + +void MyPrint(int val) { + std::cout << val << std::endl; +} +void test01() { + std::vector v; // 像类模板那样用 + v.push_back(10); // 这是插入函数 + v.push_back(20); + v.push_back(30); + // 第1种遍历: + // v.begin()是一个函数,返回值要定义一个数据类型去接收 + std::vector::iterator itBegin = v.begin(); + // std::vector::iterator 就是拿到vector这种容器的迭代器类型 + std::vector::iterator itEnd = v.end(); + while (itBegin != itEnd) { + std::cout << *itBegin++ << std::endl; // 当做指针去用(解引用加++的偏移) + } + // 第2种遍历:for循环 + for (std::vector::iterator it = v.begin(); it != v.end(); it++) { + // 注意下面这行写法是错的,这里不能再用 v.begin() != ,不能再v.begin(),必须用初始化的it + //for (vector::iterator it = v.begin(); v.begin() != v.end(); it++) { + std::cout << *it << std::endl; + } + // 第3种遍历:算法`for_each`,记得头文件 + std::for_each(v.begin(), v.end(), MyPrint); + // 第三个参数是一个自定义函数,那个参数还不是很明确 +} +``` + +2、自定义数据类型: + +​ 目标:vector中存放自定义数据类型,并打印输出 + +```c++ +#include +#include + +class Person { +public: + Person(std::string name, int age) { + this->m_Name = name; + this->m_Age = age; + } + std::string m_Name; + int m_Age; +}; +void test01() { + Person p1("aaa", 10); + Person p2("bbb", 20); + Person p3("ccc", 30); + vector v; // 是什么类型就给什么 + v.push_back(p1); + v.push_back(p2); + v.push_back(p3); + // 遍历 + for (std::vector::iterator it = v.begin(); it != v.end(); it++) { + // (*it)这解引用出来的数据类型就是`<>`里面的类型,即Person + std::cout << (*it).m_Name << it->m_Age << std::endl; // 也可当指针使用 + } +} +void test02() { + Person p1("eee", 10); + Person p2("fff", 20); + Person p3("ggg", 30); + std::vector v; // 存放的数据是指针 + v.push_back(&p1); // 指针不非得是new,可以是取址符 + v.push_back(&p2); + v.push_back(&p3); + std::vector::iterator it = v.begin(); + while (it != v.end()) { + // 解引用一次拿到的是一个数据的地址指针 + std::cout << (*it)->m_Name << (**it).m_Age << std::endl; + it++; + } +} +``` + +3、容器嵌套容器: + +​ 目标:容器中嵌套容器,将所有数据进行遍历输出。 + +```c++ +void test01() { + std::vector v1, v2, v3; + for (int i = 0; i < 5; i++) { + v1.push_back(i * 10); + v2.push_back(i * 100); + v3.push_back(i * 1000); + } + // 大容器:容器里面的数据类型是容器 + std::vector> v; + v.push_back(v1); + v.push_back(v2); + v.push_back(v3); + // 遍历;再次注意,这种`*iter`解引用得到的数据类型就是`<>`括号里的 + for (std::vector>::iterator iter = v.begin(); iter != v.end(); iter++) { + // 太多的时候,这种解引用一定要加`括号`哦 + for (std::vector::iterator it = (*iter).begin(); it != (*iter).end(); it++) { + std::cout << *it << "\t"; + } + // 或者用个中间变量 + //vector temp = *iter; + //for (vector::iterator it = temp.begin(); it != temp.end(); it++) { + // cout << *it << "\t"; + //} + std::cout << std::endl; + } +} +``` + +### 2.3 deque容器 + +功能:==双端数组==,可以对头端进行插入删除操作 + +deque与vector区别: + +- vector对于头部的插入删除效率低,数据量越大,效率越低(因为头部动了,整体都要移动) +- deque相对而言,对头部的插入删除速度会比vector快 +- vector访问元素时的速度会比deque速度快,这与两者的内部实现有关(vector是一段连续的内存空间,而deque不是) + +deque内部工作原理: + +​ deque内部有个==中控器==,维护每段缓冲区的内容,缓冲区存放真实数据 + +​ 换言之:中控器维护的是每个缓冲区的地址,使得使用deque时像一片连续的内存空间,不上图了,记不起来时点击[这里](https://www.bilibili.com/video/BV1et411b73Z?p=204&spm_id_from=pageDriver) + +#### 2.3.1 deque构造函数 + +和vector基本就是一模一样了 + +- `std::deque deqT;` // 默认构造形式 +- `std::deque(begin, end);` +- `std::deuqe(n. elem);` +- `std::deque(const deque &deq);` + +```c++ +#include +#include + +void printDeque(const std::deque d) { + // 加个const保证只读,防止值被修改,对应的下面也要改 + //for (deque::iterator iter = d.begin(); iter != d.end(); iter++) { + for (std::deque::const_iterator iter = d.begin(); iter != d.end(); iter++) { + // 这里要对应的改成 `const_iterator` + std::cout << *iter << ' '; + } + std::cout << std::endl; +} +void test01() { + std::deque d1; + for (int i = 0; i < 10; i++) { + d1.push_back(i); + } + printDeque(d1); + // 其他的就不演示了,跟vector一模一样 +} +``` + +> 注意:在打印时,要是加了const,让只读,那么对应的iterator也要改成`const_iterator` + +#### 2.3.2 deque赋值操作 + +跟vector一模一样,就不写了,看vector + +#### 2.3.3 deque大小操作 + +​ 和vector类似,但是是没有容量的概念的,在硬件支持的情况下理论上是可以无限扩容的。 + +- deque没有容量的概念 +- empty() --- 判断对否为空 +- size() --- 返回元素的个数 +- resize() --- 重新指定个数 + +#### 2.3.4 deque插入和删除 + +函数原型: +两端插入操作: + +- `push_back(elem);` // 在容器尾部添加一个数据 +- `push_front(elem);` // 在容器头部添加一个数据 +- `pop_back();` // 删除最后一个数据 +- `pop_front();` // 删除第一个数据 + +指定位置操作: + +- `insert(pos, elem);` +- `insert(pos, n, elem);` +- `insert(pos, begin, end);` +- `clear();` +- `erase(begin, end);` +- `erase(pos);` + +### 2.4 案例-评委打分 + +​ 描述:有5名选手:选手ABCDE,10个评委分别对每一名选手打分,去除最高分,去除评委中最低分,取平均分。 + +实现步骤: + +1. 创建五名选手,放到vector中; +2. 创建了一个用于存放评分的vector,里面的每一个元素都是一个deque,里面是10个评委的打分; +3. sort算法对deque容器中分数排序,去除最高和最低分; +4. deque容器遍历一遍,累加总分; +5. 获取平均分。 + +```c++ +#include +#include +#include +#include +#include // sort算法要的 +#include // 设置随机种子要的 + + +class Person { +public: + std::string m_Name; + float m_Score; + Person(std::string name, float score): m_Name(name), m_Score(score) {} +}; +// 初始化选手 +void createPerson(std::vector &v) { + std::string name_id = "ABCDE"; + + float score = 0; // 初始化分数0 + for (int i = 0; i < 5; i++) { + // 统一都是`选手`开头(必须循环里,每次重置) + std::string name = "选手"; + name += name_id[i]; // string可以直接加等,用索引去取 + Person p(name, score); + v.push_back(p); + } +} +// 给每个选手打分(假设打分随机在60~100) +void score(std::vector &v, std::vector> &v_score) { + for (std::vector::iterator it = v.begin(); it != v.end(); it++) { + std::deque d; // 记录每个人的打分 + for (int i = 0; i < 10; i++) { + // c++中随机数的写法`rand() % 41`得到的就是0~40的随机数 + int score = rand() % 41 + 60; // 0~40 + 60 = 60~100 + d.push_back(score); + } + v_score.push_back(d); // 那5个人的乘积也放到队列 + } +} +void mySort(std::vector> &v_score) { + /*千万别像这样临时起个变量,这样修改的就不是v_score了*/ + //deque temp; + //for (int i = 0; i < v_score.size(); i++) { + // temp = v_score[i]; + // sort(temp.begin(), temp.end()); // 排序 + // temp.pop_front(); + // temp.pop_back(); // 去掉最高分和最低分 + //} // 错误的写法(语法没问题,但是没有对传进来的排序) + + for (int i = 0; i < v_score.size(); i++) { + // sort排序里面给的也是迭代器 + sort(v_score[i].begin(), v_score[i].end()); // 排序 + v_score[i].pop_front(); + v_score[i].pop_back(); // 去掉最高分和最低分 + } +} +// 传入存选手信息的数组及存选手得分的数组 +void cal(std::vector &v, std::vector> &v_score) { + if (v.size() != v_score.size()) { + cout << "错误,选手信息与选手得分不符!" << endl; + return; + } + float avg = 0; + for (int i = 0; i < v.size(); i++) { + int sum = 0; + for (std::deque::iterator it = v_score[i].begin(); it != v_score[i].end(); it++) { + sum += *it; + } + avg = float(sum) / v_score[i].size(); // 平均分 + v[i].m_Score = avg; + } +} + +int main() { + // 随机数种子(根据系统时间算随机种子) + std::srand((unsigned int)time(NULL)); + // 这跟Python不同,有随机种子,每次才不一样,没有的话,每次都不一样 + + // 存放存放人的数组 + std::vector v; + createPerson(v); // 去初始化赋值 + // 存放打分的数组 + std::vector> v_score; + // 去随机打分 + score(v, v_score); + // sort对deque中排序,去掉最高分和最低分; + mySort(v_score); + // 这就看下效果 + for (std::vector>::iterator it = v_score.begin(); it != v_score.end(); it++) { + for (std::deque::iterator iter = (*it).begin(); iter != (*it).end(); iter++) { + std::cout << *iter << '\t'; + } + std::cout << std::endl; + } + std::cout << "************************" << std::endl; + // 开始累加,计算平均分,并赋值给成员 + cal(v, v_score); + for (std::vector::iterator it = v.begin(); it != v.end(); it++) { + std::cout << "姓名:" << it->m_Name << "\t平均分:" << it->m_Score << std::endl; + } + system("pause"); + return 0; +} +``` + +#### 生成随机数技巧 + +> 小技巧:c++中生成随机小数的技巧 +> +> - float score = std::rand() % 10 + 1; // 这只会得到 7.0这样的数据,它相当于只是把一个随机整数强转成了float +> - float score = (float)(std::rand() % 41 + 60) / 10.0f; // 8.6 +> - // 前面整型生成的是0\~40的整数,+60就是60~100的整数,记得先转成float,再除以浮点型的10.0f得到的就是 6.5、7.6、9.1这样的小数,记得分子分母都得是浮点型,不然精度要丢失 +> - float score = (float)(std::rand() % 401 + 600) / 100.0f; // 8.65 +> - 这就是要两位小数的话,都先放大100倍,再除以100倍,得到的就是6.53、7.62、9.19这样的小数了 + +~~双波浪线中间就是加一条删除线~~ + +~单对波浪线,就是中间的字体变小,做下标时可以考虑~ + +### 2.5 stack容器 + +​ 概念:栈,stack是一种==先进后出==(FILO, First In Last Out)的数据结构,它只有一个出口 + +栈中只有顶端(栈顶)的元素才可以被外界使用,因此==栈不允许有遍历行为== + +栈中进入数据称为 --- ==入栈== `push` + +栈中弹出数据称为 --- ==出栈== `pop` + +#### 2.5.1 stack常用接口 + +构造函数: + +- `std::stack stk;` // stack采用模板类实现,stack的默认构造 +- `std::stack(const std::stack &stk);` // 拷贝构造函数 + +赋值操作: + +- `std::stack& operator=(const std::stack &stk); ` // 重载`=` + +数据存取: + +- `push(elem);` // 向栈顶添加元素 +- `pop();` // 从栈顶移除第一个元素 +- `top();` // 返回栈顶元素 + +大小操作: + +- `empty();` // 判断堆栈是否为空,返回布尔值 +- `size();` // 返回栈的大小 + +```c++ +#include +#include + +void test01() { + std::stack stk; + for (int i = 1; i < 6; i++) { + stk.push(i * 10); + } + // 不可以遍历,不能这样去打印 + //for (int i = 0; i < stk.size(); i++) + + std::cout << stk.size() << std::endl; // 5 + int elem = 0; + // 这不是遍历,一直在删除栈顶元素 + while (!stk.empty()) { // `!`取反 + elem = stk.top(); // 栈顶元素 + // 先进先出,第一个是50 + std::cout << elem << std::endl; + stk.pop(); // 删除栈顶元素 + } + std::cout << stk.size() << std::endl; // 0 +} +``` + +### 2.6 queue容器 + + 概念:队列,Queue是一种==先进先出==FIFO,First In First Out)的数据结构,它有两个口。 + +队列容器允许从一端新增元素,从另一段移除元素 + +队列中只有队头和队尾才可以被外界使用,因此==队列不允许有遍历行为== + +队列中进数据称为 --- ==入队== `push` + +队列中出数据称为 --- ==出队== `pop` + +队列是单向的,只能从队尾进(push),队头出(pop) + +#### 2.6.1 queue常用接口 + +构造函数: + +- `std::queue que;` // 采用模板类实现,默认构造 +- `std::queue(const queue &que);` // 拷贝构造函数 + +赋值操作: + +- `std::queque& operator=(const queue &que);` // 重载`=` + +数据存取: + +- `push(elem);` // 往队尾添加元素 +- `pop();` // 从队头移除第一个元素 +- `back();` // 返回最后一个元素 +- `front();` // 返回第一个元素 + +大小操作: + +- `empty();` // 判断堆栈是否为空 + +- `size();` // 返回栈的大小 + +```c++ +#include +#include +#include + +void test01() { + Person p1("唐僧", 500); + Person p2("孙悟空", 1000); + Person p3("猪八戒", 900); + Person p4("沙僧", 800); + // 创建队列 + std::queue que; + que.push(p1); + que.push(p2); + que.push(p3); + que.push(p4); + + while (!que.empty()) { + std::cout << "队头元素:" << que.front().m_Name << std::endl; + std::cout << "队尾元素:" << que.back().m_Name << "\n" << std::endl; + que.pop(); // 移除队头的第一个元素 + } +} +``` + +### 2.7 list容器 + +功能:将数据进行链式存储 + +​ ==链表==(list)是一种物理存储单元上非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的。 + + + +链表的组成:链表有一系列==节点==组成,一个列表10个元素就是10个节点; + +节点的组成:一个节点由两部分组成: + + - 一个是直接存储数据元素的==数据域== + - 另一个是存储下一个节点地址的==指针域== 不上图了,看[这个视频](https://www.bilibili.com/video/BV1et411b73Z?p=215&spm_id_from=pageDriver) + +​ + +​ STL中的链表是一个==双向循环链表==(简单来说就是最好一个节点的指针域并不指向NULL,而是指向第一个元素,且一个节点即指向后一个节点,也指向了前一个节点) + + + +list的优点: + +- 采用动态内存分配,不会造成内存浪费和溢出 + - 我的理解是内存并不是连续的,靠指针维护,随用随加,反观数组vector,放10万元素,容量却是13万多,多的就浪费了 +- 链表执行插入和删除操作十分方便,修改指针即可,就很快,不需要移动大量元素 + - 添加、删除只需要操作那个节点前后的指针指向即可,反观数组,连续存储,中间改动一个数据,后面的都必须要跟着动 + +list的缺点: + +- 链表灵活,但是空间和时间额外消耗较大 + - 空间开销大是指,一个节点,数据(int)要占空间,指针域(记录位置的指针)也要占空间 + - 时间是指,如果遍历,链表是不连续的,反观数组是连续空间,直接指针偏移就遍历了,就快 + +​ 总结:链表(list)的优点就是数组(vector)的缺点;它的缺点就是数组的优点。 + +>特别注意: +> +>​ List有一个重要的性质,插入和删除操作都不会造成原有list迭代器(可以理解为那个指针)都不会失效,但是这在vector种是绝对不行的(迭代器也像是一个指针,记录着位置,数组的插入删除会另外开辟一个新的空间,原先的迭代器也就失效了,前面有例子) + +#### 2.7.1 list构造函数 + +函数原型: + +- `std::list lst;` // 模板类实现,类对象的默认构造 +- `std::list(begin, end);` // 构造函数将[begin, end)拷贝,里面是迭代器 +- `std::list(n, elem);` // 将n个elem拷贝 +- `std::list(const std::list &lst);` // 拷贝构造函数 + +```c++ +#include +#include + +void printList(const std::list &lst) { + // 上面用了const,那么下面一定要是const_iterator + for (std::list::const_iterator it = lst.begin(); it != lst.end(); it++) { + cout << *it << ' '; + } + std::cout << std::endl; +} +void test01() { + std::list lst; + lst.push_back(10); + lst.push_back(20); + lst.push_back(30); + printList(lst); + + std::list lst1(lst.begin(), lst.end()); + printList(lst1); + + std::list lst2(10, 5); + printList(lst2); + + std::list lst3(lst2); + printList(lst3); +} +``` + +#### 2.7.2 list赋值和交换 + +功能描述:给list容器进行赋值,以及交换list容器 + +函数原型: + +- `assign(begin, end); ` // 是迭代器区间[begin, end) + +- `assign(n. elem);` // 将n个elem赋值 + +- `std::list& operator=(const list &lst);` // 重载`=` + +- `swap(lst);` // 将传进来的lst元素与本身互换 + +```c++ +void printList(const std::list &lst) { + // 上面用了const,那么下面一定要是const_iterator + for (std::list::const_iterator it = lst.begin(); it != lst.end(); it++) { + std::cout << *it << ' '; + } + std::cout << std::endl; +} +void test01() { + std::list lst; + lst.push_back(10); + lst.push_back(20); + lst.push_back(30); + + std::list lst1, lst2, lst3; + lst1.assign(lst.begin(), lst.end()); + + lst2.assign(10, 5); + + lst3 = lst1; + + std::cout << "交换前:" << std::endl; + printList(lst1); + printList(lst2); + std::cout << "交换后:" << std::endl; + lst1.swap(lst2); // 交换 + printList(lst1); + printList(lst2); +} +``` + +#### 2.7.3 list大小操作 + +函数原型:算是成员函数吧,要用实例化对象去调用 + +- `size();` // 返回容器中元素的个数 +- `empty();` // 判断容器是否为空 +- `resize(num);` // 变长,0去填充,变短,尾部超出元素删除 +- `resize(num, default_elem);` // 不用0,而用default_elem填充 + +```c++ +void test01() { + std::list lst; + lst.push_back(10); + lst.push_back(20); + lst.push_back(30); + + std::cout << lst.size() << std::endl; // 3 + std::cout << lst.empty() << std::endl; // 0 + lst.resize(2); // 10 20 + lst.resize(5, 3); // 10 20 3 3 3 +} +``` + +#### 2.7.4 list插入删除 + +函数原型: + +- `push_back(elem);` // 尾部插入一个elem +- `push_front(elem);` // 头部插入一个 +- `pop_back();` // 尾部删除一个元素 +- `pop_front();` // 头部删除一个 +- `insert(pos, elem);` + - // 在pos位置插入elem元素的拷贝,==并返回新数据的位置== +- `insert(pos, n, elem);` // 在pos位置插入n个elem数据,无返回值 +- `insert(pos, begin, end);` // 在pos位置插入[begin,end)区间的数据,无返回值 +- `clear();` // 移除容器的所有数据 +- `erase(begin, end);` + - // 删除[begin, end)区间的数据,==返回下一个数据的位置== +- `erase(pos);` + - // 删除pos位置的数据,==返回下一个数据的位置== +- `remove(elem);` // 这是很不同的一个,==删除容器中所有与elem匹配的元素== + +> ​ 注意:这上面的`pos`、`begin`、`end`均是指的迭代器,也要主要巧用有返回值的函数。`remove`也是list特有的。 + +```c++ +void printList(const std::list &lst) { + // 上面用了const,那么下面一定要是const_iterator + for (std::list::const_iterator it = lst.begin(); it != lst.end(); it++) { + std::cout << *it << ' '; + } + std::cout << std::endl; +} +void test01() { + std::list lst; + lst.push_back(10); + lst.push_back(20); + lst.push_front(30); + lst.push_front(40); // 40 30 10 20 + + // 这个会返回新插入数据的位置,记得要用迭代器去接收 + std::list::iterator it = lst.insert(lst.begin(), 5); // 5 40 30 10 20 + std::cout << *it << std::endl; // 5 + + lst.insert(lst.end(), 3, 100); // 尾部插了3个100 + + // erase后都会返回下一个元素的地址,可以不去接收的 + //lst.erase(lst.begin()); // 这也是可以的 + std::list::iterator it1 = lst.erase(lst.begin()); + std::cout << *it1 << std::endl; // 40 可以不要这个 + + // 会删除所有的 100 + lst.remove(100); + + printList(lst); +} +``` + +#### 2.7.5 list数据存取 + +​ list容器不可以通过`[]`或者`at()`方式访问数据,是不支持随机访问的,因为list本质是链表,不是用连续线性空间存储数据,迭代器也是不支持随机访问的。 + +函数原型: + +- `front();` // 返回第一个元素 +- `back();` // 返回最后一个元素 + +```c++ +void test01() { + std::list lst; + lst.push_back(10); + lst.push_back(20); + lst.push_front(30); + lst.push_front(40); // 40 30 10 20 + + std::cout << lst.front() << std::endl; // 40 + std::cout << lst.back() << std::endl; // 20 + + // 验证迭代器是不支持随机访问的 + std::list::iterator it = lst.begin(); + it++; + it--; // 这俩是可以的,就说明是支持双向的 + + // 下面可验证是不是支持随机访问 + //it = it + 1; // 这里是错误的 + //it = it + 5; // 或者其他int + // 这就说明list是不支持随机访问的,其它的如vector支持随机访问的,这就没问题 +} +``` + +#### 2.7.6 list反转和排序 + +功能:将容器中的元素反转,以及将容器中的数据进行排序 + +函数原型:注意这里的`sort是成员函数`,不是全局的那个sort + +- `reverse();` // 反转链表 +- `sort();` // 链表排序 + +```c++ +void printList(const std::list &lst) { + // 上面用了const,那么下面一定要是const_iterator + for (std::list::const_iterator it = lst.begin(); it != lst.end(); it++) { + std::cout << *it << ' '; + } + std::cout << std::endl; +} +bool myCompare(int num1, int num2) { + return num1 > num2; // 这就是升序 +} +void test01() { + std::list lst; + lst.push_back(10); + lst.push_back(20); + lst.push_front(30); + lst.push_front(40); // 40 30 10 20 + // 反转 + lst.reverse(); + printList(lst); // 20 10 30 40 + // 排序(默认升序排列) + lst.sort(); + printList(lst); // 10 20 30 40 + // 若想要升序,就要仿函数 + lst.sort(myCompare); + printList(lst); // 40 30 20 10 +} +``` + +> ​ Ps:所有不支持随机访问迭代器的容器,不可以使用标准算法(即中的算法);所以不支持随机访问迭代器的容器,内部会提供对应一些算法。 + +#### 2.7.7 排序案例 + +​ 案例描述:将Person自定义数据类型进行排序,Person中属性有姓名、年龄、身高 + +​ 排序规则:按照年龄进行升序,如果年龄相同按照身高进行降序 + +```c++ +#include +#include +#include +class Person { +public: + std::string m_Name; + int m_Age; + int m_Height; + Person(std::string name, int age, int height) { + this->m_Name = name; + this->m_Age = age; + this->m_Height = height; + } +}; + +void printList(const std::list &L) { + for (std::list::const_iterator it = L.begin(); it != L.end(); it++) { + std::cout << "姓名:" << it->m_Name << "\t年龄:" << it->m_Age + << "\t身高:" << it->m_Height << std::endl; + } +} +// 自己写的排序函数 +bool desc(Person p1, Person p2) { + if (p1.m_Age == p2.m_Age) { + return p1.m_Height > p2.m_Height; + } + return p1.m_Age < p2.m_Age; +} +void test01() { + Person p7("钱五", 12, 150); + Person p1("张三", 18, 175); + Person p2("李四", 23, 165); + Person p6("钱六", 12, 199); + Person p3("王五", 15, 190); + Person p4("赵六", 30, 155); + Person p5("钱七", 12, 168); + + std::list L; + L.push_back(p7); + L.push_back(p1); + L.push_back(p2); + L.push_back(p6); + L.push_back(p3); + L.push_back(p4); + L.push_back(p5); + + std::cout << "排序前:" << std::endl; + printList(L); + std::cout << "***********\n" << std::endl; + + std::cout << "排序后:" << std::endl; + L.sort(desc); + printList(L); + std::cout << "***********\n" << std::endl; +} +``` + +> 总结:对于自定数据类型,必须指定排序规则,否则编译器不知道如何进行排序。 + +### 2.8 array容器 + +​ 还有这个容器,了解即可,定义后的空间是固定的,明确知道多少个可以试着用array,元素个数经常变,vector比较好。 + +```c++ +#include // 同名头文件 +int main() { + std::array arr1; // 10个默认初始化的int + std::array arr2 = { 1, 2, 3 }; // 列表初始化 + std::array arr3 = { 12 }; // arr3[0]=12,剩余为0 +} +``` + +## 三、STL-常用关联容器 + +​ set和list的区别:set是有序不重复集合,底层实现是红黑树;而list是无序可重复集合,底层实现是链表。 + +标准库一共定义了8个关联容器:(无序容器则定义在头文件==unordered_map==和==unordered_set==中。) + +| | | +| :------------------- | --------------------------------------------- | +| 按关键字有序保存元素 | | +| map | 关联数组:保存关键字-值对 | +| set | 关键字即值,即只保存关键字的容器 | +| multimap | 关键字可重复出现的map | +| multiset | 关键字可重复出现的set | +| 无序集合 | (不保持关键字顺序的容器都以==unordered==开头) | +| unordered_map | 用哈希函数组织的map | +| unordered_set | 用哈希函数组织的set | +| unordered_multimap | 哈希组织的map:关键字可重复出现 | +| unordered_multiset | 哈希组织的set:关键字可重复出现 | + +==无序容器==的一点补充: + +unordered_set unordered_map 这些 + +新标准定义了4个无序关联容器,这些容器不是使用比较运算符来组织元素,而是使用一个==哈希函数==和关键字类型的==运算符。。map、set这些是有序的(会自动排序的),在某些关键字元素类型无明显的序关系下,无序容器也很有用,,因为维护元素的序代价非常高昂。可以不使用默认的hash,可以自己重载定义,也不想说了,了解。 + +无序容器,有一个==管理桶==的概念,管理操作————==桶接口==、==桶迭代==、==哈希策略==放这里吧,就不详说了,以后用到再讲吧。 + +总结:==无序容器拥有更好的性能,有序容器使得元素始终有序==。 + +### 3.1 set/multiset容器 + +set和multiset定义在头文件set中; + +- 所有的元素在插入时自动被排序 + +本质:set/multiset属于==关联式容器==,底层结构是用==二叉树==实现 + + + +set和multiset区别: + +- set不允许容器中有重复的元素 +- multiset允许容器中有重复的元素 + +==初始化==: + +```c++ +std::set excl= {"the", "and", "The", "And", "and"}; +``` + +- 注意初始化的方式,set中有个重复的,但是会被自动去掉,只会有4个值; +- 重要一点:set、map都是有序的,这会将他们自动排序的,这里就不会是初始化的顺序,可以打印出来看。 + +#### 3.1.1 set构造和赋值 + +构造: + +- `std::set st;` // 默认构造 +- `std::set(const set &st);` // 拷贝构造函数 + +赋值: + +- `std::set& operator=(const std::set &st);` // 重载`=` + +```c++ +#include +#include + +void printSet(const std::set &s) { + // 没想到在set里这iterator也是可以的,但还是统一使用 + //for (set::iterator it = s.begin(); it != s.end(); it++) { + for (set::const_iterator it = s.begin(); it != s.end(); it++) { + std::cout << *it << ' '; + } + std::cout << std::endl; +} +void test01() { + std::set s1; + // 数据插入的方式 只有insert方式 + s1.insert(20); + s1.insert(10); + s1.insert(40); + s1.insert(30); + s1.insert(10); + // 会自动插入,且重复插入的值无效 + printSet(s1); // 10 20 30 40 + + // 拷贝构造 + std::set s2(s1); + // 赋值 + std::set s3; + s3 = s2; +} +``` + +#### 3.1.2 set大小和交换 + +​ 在set中是没有resize的,因为resize变长是要填充默认值的,都是重复的,而重复值在set中是不被允许的。 + +函数原型: + +- `size();` // 返回容器中元素的个数 +- `empty();` // 哦安短容器是否为空 +- `swap();` // 交换两个集合容器 + +```c++ +void test01() { + std::set s1; + // 数据插入的方式 只有insert方式 + s1.insert(20); + s1.insert(10); + s1.insert(40); + s1.insert(30); + s1.insert(10); // s1 // 10 20 30 40 + // 会自动插入,且重复插入的值无效 + + std::cout << s1.size() << std::endl; // 4 + std::cout << s1.empty() << std::endl; // 0 + + std::set s2; + s2.insert(50); + s2.swap(s1); + // s1和s2中元素的值就互相交换了 +} +``` + +#### 3.1.3 set插入和删除 + +注意:set在插入时会自动按照从小到大的顺序排序。 + +函数原型: + +- `insert(elem);` // 容器中插入元素 + a_set.insert(vec.begin(), vec.end()); // 可以把一个vec转成set的 + a_set.insert({1, 1, 2, 3, 3, 5}); // 会自动去重 +- `clear();` // 清除所有元素 +- `erase(pos);` + - // 删除pos迭代器所指的元素,并==返回下一个元素的迭代器== +- `erase(begin, end);` + - //删除区间[begin, end)的所有元素,并==返回下一个元素迭代器== +- `erase(elem);` // 删除容器中,值为elem的元素 + +```c++ +void printSet(const std::set &s) { + for (std::set::const_iterator it = s.begin(); it != s.end(); it++) { + std::cout << *it << std::endl; + } +} +void test01() { + std::set s; + s.insert(20); + s.insert(40); + s.insert(10); + s.insert(50); + s.insert(30); + printSet(s); // 10 20 30 40 50 + + s.erase(s.begin()); // 删除的是10 + // 这个删除会返回下一个迭代所指的元素 + std::set::iterator it = s.erase(s.begin()); // 删的20 + std::cout << *it << std::endl; // 30 + + // 直接删除指定元素,像list中的remove + s.erase(40); + printSet(s); // 30 50 + + // 清空,这都是清空 + s.erase(s.begin(), s.end()); + s.clear(); +} +``` + +#### 3.1.4 set查找和统计 + +函数原型: + +- `find(elem);` + - // 存在返回该元素的迭代器,若不存在,则返回set.end() +- `count(elem);` // 统计elem元素的个数,对set而言,不是0就是1 + +```c++ +void test01() { + std::set s; + s.insert(20); + s.insert(10); + s.insert(30); + s.insert(10); + + std::set::iterator it = s.find(10); + //it = s.find(100); // 找不到就返回s.end() + if (it != s.end()) { + std::cout << "找到该元素了:" << *it << std::endl; + } + else { + std::cout << "该元素不存在" << std::endl; + } + std::cout << s.count(10) << std::endl; // 存在就是1 + std::cout << s.count(100) << std::endl; // 不存在就是0 +} +``` + +#### 3.1.5 set和multiset区别 + +两个都是用的头文件`#include ` + +区别: + +- set不可以插入重复的数据,而multiset可以 +- set插入数据的同时会返回插入结果,来表示插入是否成功 +- multiset不会检测数据,因此可以插入重复数据 + +```c++ +// set +void test01() { + std::set s; + // 可以在定义里看到isnert返回的是一个对组 + std::pair::iterator, bool> ret = s.insert(10); + // 对组里面两个数,用first和second去取 + std::cout << *(ret.first) << std::endl; // 10 + std::cout << ret.second << std::endl; // 1 + // 再插入同一个值,返回的bool就是0了 + ret = s.insert(10); + std::cout << *(ret.first) << std::endl; // 10 + std::cout << ret.second << std::endl; // 0 +} +// multiset +void test02() { + std::multiset ms; + ms.insert(10); // 这是不会去检测的,也是没有返回值的 + ms.insert(10); + ms.insert(10); + for (std::multiset::iterator it = ms.begin(); it != ms.end(); it++) { + std::cout << *it << std::endl; + } +} +``` + +#### 3.1.7 set容器自定义排序 + +目标: + +- set容器默认排序规则为从小到大,掌握如何改变排序规则 + +主要技术点: + +- 利用仿函数,可以改变排序规则 + +示例一:set=放置内置数据类型 + +```c++ +// 这个一定要写到函数printCompare前面 +class MyCompare { +public: + bool operator()(int val1, int val2) { + return val1 > val2; + } +}; +void printSet(const std::set &s) { + for (std::set::const_iterator it = s.begin(); it != s.end(); it++) { + std::cout << *it << ' '; + } + std::cout << std::endl; +} +void test01() { + //set s; // 会默认从小到大排序 + // 这里改了模板类型,后面打印的时候全部类型都要改 + std::set s; + s.insert(20); + s.insert(50); + s.insert(10); + s.insert(40); + s.insert(30); + printSet(s); +} +``` + +> ​ 理解:`set s;` MyCompare说是用的仿函数,我的理解,这是一个类,重载了`()`,正常使用是先实例化:`MyCompare mc;`再`mc()`就是仿函数,这里的参数本来是要一个函数名的,放个类名,可以理解为是匿名对象,再被后台调用`()`时就是仿函数。 + +实例二: + +```c++ +class Person { +public: + std::string m_Name; + int m_Age; + Person(std::string name, int age) { + this->m_Name = name; + this->m_Agvoid test01() { + std::set s; + Person p1("张三", 13); + Person p2("李四", 14); + Person p3("王五", 19); + Person p4("赵六", 15); + s.insert(p1); + s.insert(p2); + s.insert(p3); + s.insert(p4); + // 这么插入就不行,不能实例化;必须用匿名对象 + //s.insert(Person p7("钱七", 23)); + s.insert(Person("钱七", 23)); // 匿名对象是OK的 + + std::set::iterator it = s.begin(); + for (; it != s.end(); it++) { + std::cout << "姓名:" << it->m_Name + << " 年龄:" << (*it).m_Age << std::endl; + } +} 自己写的,一定要有const,不然编译不过 + //bool operator()(Person &p1, Person &p2) { + bool operator()(const Person &p1, const Person &p2) { + return p1.m_Age > p2.m_Age; + } +}; +void test01() { + std::set s; + Person p1("张三", 13); + Person p2("李四", 14); + Person p3("王五", 19); + Person p4("赵六", 15); + s.insert(p1); + s.insert(p2); + s.insert(p3); + s.insert(p4); + //set::iterator it = s.begin(); + for (std::set::iterator it = s.begin(); it != s.end(); it++) { + std::cout << "姓名:" << it->m_Name + << " 年龄:" << (*it).m_Age << std::endl; + } +} +``` + +> 注意: +> +> - 对于自定义数据类型放set时,编译器是不知道怎么排序的,就一定要利用仿函数来指定排序规则 +> - 自定义数据类型,写仿函数,重载`()`传参时,自定义数据类型前一定要加const,不然编译不会通过 +> - 很对时候报错,出现了const,都是在定义传参类型时没有在前面加const + +### 3.2 map/multimap + +类型map和multimap定义在头文件map中;有点像是Python中的字典 // 或许应该尽量使用 对应的 std::unordered_map 就不会去排序,可能会节省性能。 + +简介: + +- map中所有的元素都是pair(对组) + +- pair中所有第一个元素为key(键),起到索引作用,第二个元素为value(值) + +- ==所有元素都会根据元素的键自动排序(默认std::less)==,于是可以给map第三个参数来改变它的排序,例: + + - ```c++ + std::map mapWord = { { "father", 1 },{ "mother", 4 },{ "daughter", 5 } }; // 等价于下面,默认是less: + std::map> mapWord2 = { { "father", 1 },{ "mother", 4 },{ "daughter", 5 } }; + + // key值改为从大到小:用的std::greater + std::map> mapWord2 ; + ``` + + 注:用比较函数、lambda、自定义类的重载来实现第三个参数,具体可看[这](https://blog.csdn.net/usstmiracle/article/details/79877330)。 + + - 学习时看到别人写的代码:==一个容器就是一个类,一个类继承了map,那这个类的对象也能有map的属性==。想要跟进一步,去看[inifile-cpp](https://github.com/nianjiuhuiyi/inifile-cpp/blob/master/include/inicpp.h)项目(我fork的,有我的注解),不要被迷惑了,看它的class IniFileBase这个类,里面的void decode(std::istream &is)函数才是核心,它继承于一个map,而这个map的.second的类型又是一个继承于map的类;map的第三个参数不重要,这里的第三个参数也没产生多大的意义。 + + ```c++ + struct StringInsensitiveLess { + bool operator()(std::string lhs, std::string rhs) const { + std::transform(lhs.begin(), lhs.end(), lhs.begin(), + [](const char c) {return static_cast(std::tolower(c)); }); + std::transform(rhs.begin(), rhs.end(), rhs.begin(), + [](const char c) {return static_cast(std::tolower(c)); }); + return lhs < rhs; + } + }; + + template // IniField是自定义的一个类,这里不重要 + class IniSectionBase : public std::map { + public: + IniSectionBase() {} + ~IniSectionBase() {} + }; + // 注意看下面这两行的例子 + using IniSection = IniSectionBase>; + // 这个类里重载的是`()`,不理解时去看一元谓词那里的解释。 + using IniSectionCaseInsensitive = IniSectionBase; + ``` + +本质: + +- map/multimap属于==关联式容器==,底层结构是用二叉树实现 + +优点: + +- 可以根据key值快速找到value值,比较高效 + + + +map和multimap区别: + +- map不允许容器中有重复key值元素 +- multimap允许容器中有重复key值元素 + +==初始化==: + +```c++ +std::map info = { + {"zhangsan", 13}, + {"lisi", 14} +}; +``` + +- map的键值对都是以`{key, value}`的形式给的; +- 重要一点:set、map都是有序的,这会将他们自动排序的,这里就不会是初始化的顺序,可以打印出来看。 + +#### 3.2.1 map构造和赋值 + +函数原型: + +构造: + +- `std::map m;` // map默认构造函数 +- `std::map(const std::map &m);` // 拷贝构造函数 + +赋值: + +- `std::map& operator=(const std::map &m);` // 重载`=` + +```c++ +#include +#include +#include + +void printMap(const std::map &m) { + for (std::map::const_iterator it = m.begin(); it != m.end(); it++) { + std::cout << "key:" << (*it).first << " value:" << it->second << endl; + } +} +void test01() { + std::map m; + // 放的元素都要是对组 + std::pair p1(2, 20); + std::pair p2 = make_pair(3, 30); + m.insert(p1); + m.insert(p2); + // 只能是这样匿名对象 + m.insert(pair(5, 50)); + m.insert(pair(1, 10)); + printMap(m); // 会自动排序的 + + std::map m1(m); // 拷贝构造 + + std::map m2; + m2 = m1; // 赋值 +} +``` + +> 总结:map中所有的元素都是成对出现的,插入数据时要使用对组。 + +#### 3.2.2 map大小和交换 + +函数原型: + +- `size();` // 返回容器中元素的个数 +- `empty();` // 判断是否为空 +- `swap(m1);` // 交换两个map容器 + +```c++ +void printMap(const std::map &m) { + for (std::map::const_iterator it = m.begin(); it != m.end(); it++) { + std::cout << "key:" << (*it).first << " value:" << it->second << endl; + } + cout << endl; +} +void test01() { + std::map m; + std::pair p1(2, 20); + std::pair p2 = make_pair(3, 30); + m.insert(p1); + m.insert(p2); + m.insert(pair(1, 10)); + std::map m1; + m1.insert(pair(6, 60)); + m1.insert(pair(5, 50)); + std::cout << "交换前:" << std::endl; + printMap(m); // 会自动排序的 + printMap(m1); + std::cout << "\n交换后:" << std::endl; + m.swap(m1); + printMap(m); + printMap(m1); +} +``` + +#### 3.2.3 map插入和删除 + +函数原型: + +- `insert(elem);` + a_map.insert({"age", 13}); + a_map.insert(std::make_pair("age", 13)); + a_map.insert(std::pair\("age", 13)); + a_map.insert(std::map\::value_type("age", 13)); +- `clear();` +- `erase(pos);` // 删除pos迭代器所致元素,返回下一个元素的迭代器 +- `erase(begin, end);` // 删除区间元素,返回下一个元素的迭代器 +- `erase(key);` // 删除容器中值为key的元素 + +```c++ +void printMap(const std::map &m) { + for (std::map::const_iterator it = m.begin(); it != m.end(); it++) { + std::cout << "key:" << (*it).first << " value:" << it->second << endl; + } + std::cout << std::endl; +} +void test01() { + std::map m; + // 第一种 + m.insert(std::pair(3, 30)); + // 第二种 + m.insert(std::make_pair(2, 20)); + // 第三种 + m.insert(std::map::value_type(5, 50)); + // 第四种 + m[1] = 10; + + // 注意这里,key=9是不存在的, + int a = m[9]; + std::cout << a << std::endl; // 0 + // 怕那是这样,这个对组(11, 0)也会被加到m中去 + std::cout << m[11] << std::endl; + printMap(m); // 会发现这里会被创建key=10,value=0的对组 + + m.erase(m.begin()); // 单纯删除 + // 这会返回下一个元素的迭代器 + std::map::iterator it = m.erase(m.begin()); + std::cout << it->first << it->second << std::endl; // 330 + + // 删除容器中元素key=5的对组; + m.erase(5); + m.erase(510); // 删除一个不存在的key没有任何影响 + printMap(m); +} +``` + +> ​ 注意:在map中使用`m[key]`的时候,无论这出现在哪里,如果key不存在,就一定会向map容器中添加一个key=key,value=0的对组。 + +注意这种嵌套式写法: + +```c++ + std::map> m; + m["1"]["1.1"] = "张三"; + m["1"]["1.2"] = "张四"; + m["2"]["2.5"] = "王五"; + m["2"]["2.6"] = "王六"; + for (const auto &fir : m) { + std::cout << fir.first << std::endl; + for (const auto &sec : fir.second) { + std::cout << sec.first << " = " << sec.second << std::endl; + } + } +``` + +#### 3.2.4 map查找和统计 + +函数原型: + +- `find(key);` // 若key存在,返回该键对应的元素的迭代器,若不存在,返回map.end(); +- `count(key);` // 统计这个key的元素个数(因为map中是不允许有重复值的,结果只会是0或1;multimap就不一定了) + +```c++ +void test01() { + std::map m; + m.insert(std::pair(1, 10)); + m.insert(std::pair(3, 30)); + // 注意find返回的是迭代器 + std::map::iterator it = m.find(1); + if (it != m.end()) { + std::cout << "找到该元素了" << std::endl; + std::cout << it->first << ' ' << it->second << std::endl; + } + else { + std::cout << "没找到到就会执行这里" << std::endl; + // 没找到,返回的就是 m.end() + } + std::cout << m.count(2) << std::endl; // 0 + std::cout << m.count(3) << std::endl; // 1 +} +``` + +#### 3.2.5 map容器排序 + +- map容器默认规则为:按照key值进行升序排序 +- 对于自定义数据类型一定要指定排序的方式,同set容器 + +- 利用仿函数,来改变排序规则 + +```c++ +class MyCompare { +public: + // 如果是自定义数据类型,定义传参时都还是加上`const` + bool operator()(int v1, int v2) { + return v1 > v2; + } +}; +void test01() { + // 不指定排序方式会自动排序 + // 降序 + std::map m; + m.insert(std::pair(1, 10)); + m.insert(std::make_pair(3, 30)); + m.insert(std::map::value_type(5, 50)); + m.insert(std::pair(2, 20)); + // 下面这种方式在加了排序方式后就编译不通过,就尽量少用 + //m[2] = 20; + + for (std::map::iterator it = m.begin(); it != m.end(); it++) { + std::cout << "key:" << (*it).first << " value:" << it->second << endl; + } + std::cout << std::endl; +} +``` + +> ​ 注意:在有了自定义排序规则时,数据的插入一定不要用`m[2] = 20`这样的方式,编译通过不了,这种插入方式其它地方也慎用、少用。 + +#### 3.2.7 下标操作(重要) + +以下 c 都是一个map + +map: +std::map word_count; // 空的map +word_count["hello"] = 2; +这里其实执行了如下操作: + +- 在word_count中搜索关键字为“hello”的元素,未找到; +- 将一个新的关键字-值对插入带word_count中,关键字是一个const string,为hello,值进行初始化,为0 +- 提取处新插入的元素,并将值 2 赋予它。 + +所以说下标运算可能会插入一个新元素,只能对非const的map使用下标操作,即便是只写 word_count["a_word"]; a_word这个key不存在,也会把这添加进去的,所以要谨慎 + +map和unordered_map的下标操作: + +> c[k] 返回关键字为k的元素,如果k不在c中,添加一个关键字为k的元素,并对其进行值初始化 +> +> c.at(k) 访问关键字为k的元素,带参数检查,若k不在c中,抛出一个out_of_range异常 + +所以不确定key是否在map中,且不想修改map时,一定不要去用下标操作。而是用find先查找一下,判断一下再做决定,if (word_count.find("a_key") == wor_count.end()) + + + +还有一些成员函数操作:lower_bound和upper_bound 这俩不适用于无序容器 + +- c.lower_bound(k) // 返回一个迭代器,指向第一个关键字(键)不小于k的元素 +- c.upper_bound(k) // 同上,不大于 (这俩如果没结果,应该都是得c.end()吧) +- c.equal_range(k) // 返回一个迭代器pair,假设叫pos,表示关键字等于k的元素的范围,若k不存在,pair的两个成员均等于c.end(),即pos.first == pos.second + +于是:如果lowe_bound和upper_bound返回相同的迭代器,则说明给定的关键字不在容器中。一般如果要同时用到lower_bound和upper_bound的话,更好的选择就是用equal_range,就有头有尾,不会需要中间变量。 + +#### 3.2.6 书上补充 + +==添加元素==: + +a_map.insert({"age", 13}); +a_map.insert(std::make_pair("age", 13)); +a_map.insert(std::pair\("age", 13)); +a_map.insert(std::map\::value_type("age", 13)); + +a_map.emplace("name", 13); // 这里a_map的类型是\ 这也是插入数据,C++11开始有的,这个成员可以直接传递用于生成对象的参数,对象的创建过程交给容器去执行(其它容器应该也有)。除了emplace_front以外,C++11还提供了emplace和emplace_back方法,分别对应insert和push_back方法 + +​ 关联性容器,除了有insert插入数据,还有一个a_map.emplace() 好像也是一样的,而且这个函数会返回一个pair数据类型,包含一个迭代器,指向具有指定关键字的元素,以及一个指示插入是否成功的bool值;对于map、set这种不能有重复key的,insert就可能会失败,得到的bool值就是0 + +```c++ +std::map p; +auto ret1 = p.insert({15, 16}); // ret是pair类型,里面一个迭代器,一个bool +std::pair::iterator, bool> ret = p.insert({5, 10}); +// 下面一定要注意括号,少了会因为解析顺序问题而失败 +std::cout << (*(ret.first)).first << std::endl; // 5 +std::cout << ret.first->first << std::endl; // 5 +std::cout << (*(ret.first)).second << std::endl; // 10 +std::cout << ret.second << std::endl; // 1 成功 +``` + +像是解引用*,++一般都是最后执行的,所以想要让其先运行,就要加括号,好比第4行的,或者++result.first->second,在没有括号时这种就是最后执行++ + +比如下面这个(单词计数嘛): + +```c++ +while (std::cin >> word) + ++word_count.insert({word, 0}).first->second; +// 上面的就等价于这: +while (std::cin >> word) { + auto result = word_count.insert({word, 0}); + ++(result.first->second); +} +``` + +说明: +若`insert`成功:先添加一个元素,然后返回一个 `pair`,`pair` 的 `first`元素是一个迭代器。这个迭代器指向刚刚添加的元素,这个元素是`pair`,然后递增`pair`的`second`成员。 +若`insert`失败:递增已有指定关键字的元素的 `second`成员。 + + + +使用(注意返回类型怎么写): + +```c++ +std::map> m; + std::pair>::iterator, bool> ret = m.insert(std::pair>("age", {1, 2, 3})); // 后面的{1, 2, 3}是vector的一个初始化列表,也可以写成 std::vector{1, 2, 3} +``` + +像这种如果类型很长,且这种类型还会经常用到的话,就使用:using retrunType = std::pair>::iterator, bool> + +#### 3.2.7 单词计数(以及remove_if使用) + +​ 练习:编写一个单词计数程序(忽略大小写和标点符号,如Example、exam!p.le、eXa%mPle*都算作一个) + +```c++ +#include +#include +#include +#include +#include + +int main(int argc, char*argv[]) { + // 但是这里没导入 cstddef 头文件也能直接使用啊 + std::map word_count; + std::string word; + while (std::cin >> word) { + + //[&word](char c) {std::tolower(c); }; // 不行 + for (auto &s : word) // 这能这样变小写,用lambda表达式不行 + s = std::tolower(s); + + word.erase(std::remove_if(word.begin(), word.end(), [](char c) { return !std::isalnum(c); }), word.end()); + // remove_if中除了使用lambda外,还可直接使用std::ispunct, + //word.erase(std::remove_if(word.begin(), word.end(), std::ispunct), word.end()); // 千万注意不能是std::ispunct(),绝对不能有这个括号 + + ++word_count[word]; // 最关键的统计 + + } + for (std::map::const_iterator iter = word_count.begin(); iter != word_count.end(); ++iter) { + std::cout << iter->first << " occurs " << iter->second << + (iter->second > 1 ? " tiems" : " time") << std::endl; + } + return 0; +} +``` + +说明: + +- 核心是map:如果 word 还未在map中,下标运算符就会创建一个新元素,其关键字为word,值为0,不管元素是否是新创建的,都将其值+1; + +- 在使用数组下标时,通常将其定义为==size_t==类型,size_t是一种机器相关的无符号类型,在==cstddef==头文件中定义了 size_t 类型,这个文件是C标准库 stddef.h 头文件的C++版本; +- 算法remove_if以及erase成员函数: + - remove_if并不会实际移除容器中的元素(这很重要);所有的元素都还在容器中; + - remove_if将所有应该移除的元素都移动到了容器尾部,并返回一个分界的迭代器; + - 为了实现真正的移除,就让容器自行调用成员函数erase(),从分界迭代器删到最后; + - remove_if中谓词参数部分,也不要只想着只是用lambda函数,也有简单的方法,看注释。 + - remove_if类似于partition,有两点不同:1)它们谓词条件刚好相反,2)remove_if只强调前面部分,后面部分就不需要了 + - remove_if不能用于关联容器,如map、set + +#### 3.2.8 map姓氏练习 + +一个练习:定义一个`map`,关键字是家庭的姓,值是一个`vector`,保存家中孩子(们)的名。编写代码,实现添加新的家庭以及向已有家庭中添加新的孩子。 + +我的实现: + +```c++ +std::map> ped; // 值是一个vector +ped.insert(std::pair>("song", std::vector{"hui"})); + +std::pair> an{ "zhang", std::vector{"3", "4"} }; +ped.insert(an); // 或者以这种方式添加 + +ped["song"].push_back("huang"); +ped["chen"].push_back("chao"); // 重点是这样 + +for (auto &k : ped) { + std::cout << "姓氏:" << k.first << std::endl; + for (int i = 0; i < k.second.size(); ++i) { + std::cout << k.first + k.second.at(i) << " "; + } + std::cout << std::endl; +} +``` + +注意:map里一开始是没有'chen'这个key的,也不是通过insert插入进去,这样就会自动创建这个键值对;关联容器一般用insert插入数据,vector这些放数据的时候都是用push_back。 + + + +​ 可以定义 `vector::iterator` 到 `int` 的`map`,但是不能定义 `list::iterator` 到 `int` 的`map`。因为`map`的键必须实现 `<` 操作,`list` 的迭代器不支持比较运算。当然还可以自定义类的行为正常的<运算符。 + +### 3.3 pair类型 + +pair是标准库类型(把它看作一种数据类型),它定义在`头文件utility中`(在vs中没导入也在直接使用) + +功能:成对出现的数据,利用对组可以返回两个数据 + +两种创建方式: + +- `std::pair p(value1, value2);` +- `std::pair p = std::make_pair(value1, value2);` + +```c++ +void test01() { + std::pair p1("张三", 13); + std::pair p2 = make_pair("李四", 14); + std::cout << "姓名:" << p1.first << " 年龄:" << p1.second; +} +``` + +> 用first获取对组的第一个元素,second获取对组的第二个元素。 + +```c++ +std::pair P; +std::pair P(V1, V2); // 这里花括号也是可以的,但是早期c++是不让用花括号的 +std::pair P = {V1, V2}; // 等价于P(V1, V2),注意这种写法 +P = std::make_pair(V1, V2); +p1 == p2 当first和second成员分别相等时才等 +``` + +假设vec是一个存放pair数据类型的vector,那么就可以有如下三种形式: + +```c++ +vec.push_back(std::pair(str, num)); +vec.push_back({str, num}); +vec.push_back(std::make_pair(str, num)); +``` + +注意:这个pair和map的数据类型是不一样的,,map使用insert(pair数据) + +### 3.4 tuple类型(==放任意数据类型==) + +类似于实现python的检测结果,[name, conf, position]:[["YBKC", 0.89, [11, 22, 33, 44]], ["XDSC", 0.59, [77, 88, 99, 10]]] + +```c++ +#include +#include +#include +#include + +// 定义出来类型 +using detectType = std::tuple>; + +// 放入数据 +std::vector vec; +vec.push_back({ "YBKC", 0.5, {1, 2, 3, 4} }); +vec.push_back({ "XDSC", 0.78, {4, 5, 6, 7} }); + +// 拿数据,方式一:(这里要不了那么多,还可以用std::ignore占位) +std::string name; +float conf; +std::vector position; +for (detectType& item : vec) { + std::tie(name, conf, position) = item; + std::cout << name << ": " << conf << std::endl; +} +// 拿数据,方式二: +for (detectType& item : vec) { + std::string name = std::get<0>(item); + float conf = std::get<1>(item); + std::vector position = std::get<2>(item); + std::cout << name << ": " << conf << std::endl; +} +``` + +下面有详细的解释。 + +--- + +​ tuple是类似pair的模板,它里面可以任意数量的不同数据类型,可以将其看作一个“快速而随意”的数据结构,定义在头文件`#include `, + +==定义==:当定义一个tuple时,需要指出每个成员的类型: + +```c++ +std::tuple t1; // 三个成员都设置为0 +std::tuple, int, std::list> + someVal{"constants", {3.14, 2.718}, 42, {0, 1, 2, 3, 4, 5}}; +``` + +注意: + +- 可以像上面t1定义时使用tuple的默认构造构造函数,它会对每个成员进行值初始化 +- `std::tuple t2 = {1, 2, 3};` // 这是错误的,只能这样写t2(1, 2, 3);因为tuple的这个构造函数是explicit的,必须使用直接初始语法 + +当然除了以上定义,类似piar的make_pair函数,标准库定义了std::make_tuple函数,可用它来生成tuple对象:`auto item = std::make_tuple("nihao", 42, 20.00);`这个函数使用初始值类型来推断tuple的类型,这里的类型为tuple\。 + +==访问tuple的成员==: + 使用一个名为==get==的标准库函数模板(业绩的tuple头文件),必须指定一个显式模板参数,指出我们想要访问第几个成员,再传递给get一个tuple对象,它==返回指定成员的引用==:使用上面定义的someVal + +```c++ +auto name = std::get<0>(someVal); // 返回第一个成员 +auto li = std::get<1>(someVal); +auto res1 = std::get<2>(someVal) / 2; // 返回第二成员的值除以2的结果 +std::get<2>(someVal) *= 2; +std::cout << std::get<2>(someVal) << std::endl; // 84了 +``` + +特别注意: + +- 第4行,因为返回的是引用,可以这样直接修改其内部的值; +- get尖括号中的值必须是一个整形常量表达式。 + +==获取成员类型即数量==: + 如果不知道一个tuple准确的类型信息,可以用两个辅助类模板std::tuple_size、std::tuple_element来查询tuple成员的数量和类型: + +```c++ +std::tuple, int, std::list, int> + someVal{ "constants", {3.14, 2.718}, 42, {0, 1, 2, 3, 4, 5}, 12 }; + +typedef decltype(someVal) trans; // trans就是someVal的类型 +size_t number = std::tuple_size::value; // 5(注意结果是5,而不是4) + +std::tuple_element<1, trans>::type cnt = std::get<1>(someVal); // cnt是一个vector +``` + +解读: + +- 为了使用tuple_size和tuple_element,需要知道一个tuple对象的类型,确定一个对象的类型最简单的办法就是使用decltype,第4行相当于获取其类型,并将其重命名为trans; +- tuple_size有一个名为value的public static数据成员,它表示给定tuple中成员的数量(注意两个int也是2个成员哦); +- tuple_element接收一个索引值,一个tuple类型,它有一个名为type的public类型成员,表示给定tuple类型中指定成员的类型,然后索引值与get类似,也是从0开始计数的。 + +​ 再来一点:tuple定义了<和==这样的比较运算符,可以将tuple序列传递给算法,并且可以在无序容器中将tuple作为关键字类型。两个tuple想要比较大小或等不等这些,那成员数量一定要先一致,成员的数据类型也要一样才能比较。 + +​ 以往有多个数据类型时,就是用struct结构体来进行封装,其实没那么灵活,现在就可以用tuple来装,然后再使用==typedef 一个tuple类型 另外一个别名==,或者==using my_name = std::tuple==,这两种方式来简单使用。 + +--- + +在图形学中学会的补充: + 现在有这样一个函数:std::tuple compute(){return {c1,c2,c3};} + +- 注意上面的返回值的又一种简单的写法; +- 在c++17,可以这样去接收函数返回值:auto[alpha, beta, gamma] = compute(); +- 但c++11不支持,可以这样实现:(提前定义,然后用std::tie()去解开) + float alpha, beta, gamma; + std::tie(alpha, beta, gamma) = compute(); +- 如果只想拿其中某一个或两个,除了上面的get指定,还可以使用std::ignore来占位,即 + std::tie(alpha, std::ignore, gamma) = compute(); + +#### bitset类型 + +​ ==bitset==类,使得位运算的使用更为容易,并且能够处理超过最长整数类型大小的位集合。bitset类定义在`#include `中。 + +这里就不详写了,如果以后有用到,来看书自己右上标的==641页==。 + +### 3.5 关联容器删除点 + +假定c是关联容器: + +- c.erase(a_key); // 从c中删除键为a_key的元素,返回一个size_type的值,值为被删除元素的数量 +- c.erase(p.begin()); // 给个迭代器的位置,迭代器必须指向c中一个真实元素,返回一个指向这个给的迭代器之后元素的迭代器,若给的这个迭代指向c中的最后一个元素,则返回c.end() +- c.earse(x, y); // 删除迭代器x和y所表示范围中的元素,返回e这个迭代器。 + +### 3.6 获取关联容器数据类型 + +key_type value_type mapped_type (后面这个是map类型(unorder_map等等)特有的,是相当于set的value_type) + +```c++ +std::set::key_type v1; // int +std::set::value_type v2; //int + +std::map::key_type m1; // string +std::map::value_type m2; // 注意:这个结果类型是 pair +std::map::mapped_type m3; // 值的类型,int +``` + +注意:map的value_type的类型是一个pair,第一个值是key的类型(是有const不可修改的),第二个值是value的类型,是不同于set的。 + +set同时定义了iterator和const_iterator两种迭代器类型,但都是只读的,像map的key不能修改一样,set数据都是const的 + +### 3.7 案例-员工分组 + +案例描述: + +- 10个员工(ABCDEFGHIJ); +- 员工信息有: 姓名 工资组成;部门分为:策划、美术、研发 +- 随机给10名员工分配部门和工资 +- 通过multimap进行信息的插入 key(部门编号) value(员工) +- 分部门显示员工信息 + +实现步骤: + +1. 创建10名员工,放到vector中 +2. 遍历vector容器,取出每个员工,进行随机分组 +3. 分组后,将员工部门编号作为key,具体员工作为value,放入到multimap容器中 +4. 分部门显示员工信息 + +```c++ +#include +#include +#include +#include + +class Person { +public: + // 部门的枚举值 + enum Department { scheme = 10, arts = 11, develop = 12 }; + + std::string m_Name; + int m_Salary; // 薪酬 + Department m_Depart; // 部门 +}; + +void insertPerson(std::vector &v) { + std::string name_ids = "ABCDEFGHIJ"; + Person p; + for (int i = 0; i < 10; i++) { + p.m_Name = name_ids[i]; + p.m_Salary = std::rand() % 1001 + 1000; + // 员工随机部门 + int sec = std::rand() % 3 + 10; + switch (sec) { + case 10: + p.m_Depart = Person::scheme; + break; + case 11: + p.m_Depart = Person::arts; + break; + case 12: + p.m_Depart = Person::develop; + break; + default: + break; + } + v.push_back(p); + } +} + +// 重载版本(1) +std::ostream& operator<<(std::ostream &cout, std::vector::const_iterator it) { + std::cout << "员工编号:" << it->m_Name << "\t员工工资:" << it->m_Salary << "\t员工部门编号:" << (*it).m_Depart; + return cout; +} +// 重载版本(2) +std::ostream& operator<<(std::ostream &cout, const Person &p) { + std::cout << "员工编号:" << p.m_Name << "\t员工工资:" << p.m_Salary << "\t员工部门编号:" << p.m_Depart; + return cout; +} +void printPerson(const std::vector &v) { + // 打印方式1 + for (std::vector::const_iterator it = v.begin(); it != v.end(); it++) { + // 这也是普通打印 + std::cout << "员工编号:" << it->m_Name << "\t员工工资:" << it->m_Salary << "\t员工部门编号:" << (*it).m_Depart << std::endl; + // 重载版本(1) + std::cout << it << std::endl; + } + std::cout << "*********************" << std::endl; + // 打印方式2 + for (int i = 0; i < v.size(); i++) { + // 这是普通打印 + std::cout << "员工编号:" << v[i].m_Name << "\t员工工资:" << v[i].m_Salary << "\t员工部门编号:" << v[i].m_Depart << std::endl; + // 重载版本(2) + std::cout << v[i]; + // 注意这个重载,函数传进来的是const vector &v,是有const的;那么重载时一定要写const Person &p,不能少了const,不然类型不对 + } +} + +// 1、简单排序 +void test01() { + std::vector v; + // 插入10个员工 + insertPerson(v); // 可以看看这 + // 打印信息(写的比较全,很多方法,重载版本) + //printPerson(v); + + std::multimap mm; + for (std::vector::iterator it = v.begin(); it != v.end(); it++) { + mm.insert(pair(it->m_Depart, *it)); + } + // count计数,multimap的key是可以重复的, + std::cout << mm.count(Person::Department(Person::Department::develop)) << std::endl; // 5 + + // 再试用一下查找find + std::multimap::iterator it = mm.find(Person::Department::arts); + if (it != mm.end()) { + // 这个find得到的是第一个key的位置,相同key肯定是连续的;这里不能用迭代器到mm.end(),会把后面所有的都遍历出来 + for (int i = 0; i < mm.count(Person::Department::arts); i++) { + std::cout << "部门编号:" << it->first + << " 员工编号:" << it->second.m_Name + << " 薪酬:" << it->second.m_Salary << std::endl; + it++; + } + } +} + +class MyCompare { +public: + bool operator()(const Person::Department &d1, const Person::Department &d2) { + return d1 > d2; + } +}; +// 2、自定义排序 +void test02() { + std::vector v; + // 插入10个员工 + insertPerson(v); + // 自定义部门升序排列 + std::multimap mm; + for (std::vector::iterator it = v.begin(); it != v.end(); it++) { + mm.insert(pair(it->m_Depart, *it)); + } + // 打印的时候也要跟上排序的仿函数(部门为key,排了序,算是分部门打印了吧) + for (std::multimap::iterator it = mm.begin(); it != mm.end(); it++) { + std::cout << it->first << ' ' << it->second.m_Name << ' ' << it->second.m_Salary << std::endl; + } + // 注意:用了自定义排序后,这count、find就编译不通过了,不知道咋解决 + //mm.count(Person::Department(Person::Department::develop)); + //multimap::iterator it = mm.find(Person::Department::develop); +} + +int main() { + test01(); + std::cout << "*********" << std::endl; + test02(); + system("pause"); + return 0; +} +``` + +## 四、STL-函数对象 + +### 4.1 函数对象 + +概念: + +- 重载==函数调用操作符==的类,其对象称为==函数对象== +- 函数对象使用重载的`()`时,行为类似函数调用,也叫==仿函数== + +本质:函数对象(仿函数)是一个类,不是一个函数 + +函数对象使用,特点: + +- 函数对象在使用时,可以像普通函数那样调用,可以有参数、返回值 +- 函数对象超出普通函数的概念,函数对象可以有自己的状态 +- 函数对象可以作为参数传递 + +```c++ +class MyAdd { +public: + int operator()(int v1, int v2) { + return v1 + v2; + } +}; +class MyPrint { +public: + void operator()(std::string &s) { + cout << s << endl; + } +}; + +// 注意这里如果是引用string &s,那下面传参数时不能直接给"hello" +void doPrint(MyPrint &mp, string s) { + mp(s); +} +void test01() { + // 1、可以有参数、返回值 + MyAdd myadd; + cout << myadd(10, 20) << endl; + // 注意匿名对象是这样调用的 + cout << MyAdd()(10, 20) << endl; + + // 2、 可以自己的状态(解读就是因为这是类,可以有其它的属性值,记录自己的内部状态) + + // 3、 函数对象作为参数传递 + MyPrint myPrint; + doPrint(myPrint, "hello"); +} +``` + +### 4.2 谓词 + +谓词概念:返回bool类型的仿函数称为==谓词== + +#### 4.2.1 一元谓词 + +- 如果operator()接收一个参数,那么叫做一元谓词 + +例:找数组中有没有大于5的数 + +```c++ +// 一元谓词(5写死了不太好) +struct GtraterFive { + bool operator()(int val) { + return val > 5; + } +}; + +// 这也是对函数对象,有自己内在属性的一个解释 +class MyGreater { +public: + // 构造函数 + explicit MyGreater(int value):point_value(value) {} + // explicit关键字,启用显示转换,禁止隐式转换(更多的是针对一个参数) + + bool operator()(int val) { + return val > this->point_value; + } + + int point_value; +}; + +void test01() { + std::vector v; + for (int i = 0; i < 10; i++) { + v.push_back(i); + } + // GtraterFive()这是匿名对象,也可以先另起一行声明一个对象 + // find_if是算法中的,接收三个参数:开始、结束、谓词(参数名称是有Pred的,就是代表谓词);返回值也是迭代器;这个就是找有没有满足条件的 + //vector::iterator it = find_if(v.begin(), v.end(), GtraterFive()); // 以前简单谓词的写法,把大于的值——5是写死了的 + std::vector::iterator it = find_if(v.begin(), v.end(), MyGreater(6)); // 匿名对象,再`()`初始化,就等于普通函数名了, + // 找到了返回找到的位置(看源码定义,一满足条件就会break退出不再找了),没有找到就是返回v.end() + if (it == v.end()) { + std::cout << "未找到大于5的数" << std::endl; + } + else { + std::cout << "找到了大于5的数:" << *it << std::endl; // 6 + } +} +``` + +##### explicit关键字 + +​ 一般约定所有单参数的构造函数都必须是显示的,就用关键字`explicit`声明,只有极少数情况下拷贝构造函数可以不声明称explicit。[参考](https://www.cnblogs.com/rednodel/p/9299251.html)。 + +前提,类Person的构造函数只有一个参数string的name,简单来说,显示就必须是Person p1("张三");而不能是隐式的Person p2 = "张三"; 而且传递的参数类型就必须直接是定义的数据类型,不能发生隐式转换,如:定义一个函数 void f(std::vector\); 那么 + +f(10); // 错误,不能用一个explicit的拷贝一个实参(我的理解就是不能隐式转换) +f(std::vector\(10)); // 正确,从一个int直接构造一个临时的vector + +#### 4.2.2 二元谓词 + +- 如果operator()接受两个参数,那么叫做二元谓词 + +给数组降序排列: + +```c++ +void printVector(const vector &v) { + for (std::vector::const_iterator it = v.begin(); it != v.end(); it++) { + std::cout << *it << std::endl; + } +} +// 二元谓词 +class MyCompare { +public: + bool operator()(int v1, int v2) { + return v1 > v2; + } +}; +void test01() { + std::vector v; + v.push_back(20); + v.push_back(50); + v.push_back(10); + v.push_back(30); + v.push_back(40); + printVector(v); + std::cout << "升序排列:" << std::endl; + sort(v.begin(), v.end()); // 注意都是要给区间的迭代器 + printVector(v); + std::cout << "降序排列:" << std::endl; + // 匿名对象 + std::sort(v.begin(), v.end(), MyCompare()); // 要头文件 + printVector(v); + // 这升序就是默认的版本,默认就是v1` + +#### 4.3.1 算数仿函数 + +功能描述: + +- 实现四则运算 +- 其中`negate`是一元运算,其它都是二元运算 + +仿函数原型: + +- `template T plus` // 加法仿函数 +- `template T minus` // 减法仿函数 +- `template T multiplies` // 乘法仿函数 +- `template T divides` // 除法仿函数 +- `template T modulus` // 取模仿函数 +- `template T negate` // 取反仿函数(相当于乘以-1) + +```c++ +#include +void test01() { + // 要导入头文件 + negate n; + // 直接当仿函数那样使用 + std::cout << n(15) << std::endl; // -15 + + // 两个参数,参数列表应该是两个,但是同为int才能加,所以就一个int就行了 + plus p; + std::cout << p(10, 20) << std::endl; // 30 +} +``` + +#### 4.3.2 关系仿函数 + +功能描述:实现各种关系对比 + +仿函数原型: + +- `template bool equal_to` // 等于 +- `template bool not_equal_to` // 不等于 +- `template bool greater` // 大于 +- `template bool greater_equal` // 大于等于 +- `template bool less` // 小于 +- `template bool less_equal` // 小于等于 + +实例greater,等同于我们自己用类实现的仿函数(重载`()`)的降序排序 + +```c++ +void printVector(const vector &v) { + for (std::vector::const_iterator it = v.begin(); it != v.end(); it++) { + std::cout << *it << std::endl; + } +} +class MyCompare { +public: + bool operator()(int v1, int v2) { + return v1 > v2; + } +}; +void test01() { + std::vector v; + v.push_back(30); + v.push_back(10); + v.push_back(50); + v.push_back(20); + v.push_back(40); + + // 1、自己实现的仿函数降序排序 + //std::sort(v.begin(), v.end(), MyCompare()); + + // 2、模板,头文件带的 + std::sort(v.begin(), v.end(), std::greater()); + // 这里看定义,如果只传前两个参数,默认用了less<>() + // 传三个参数,就是又一个重载版本 + + printVector(v); +} +``` + +#### 4.3.3 逻辑仿函数 + +功能描述:实现逻辑运算 + +函数原型: + +- `template bool logical_and` // 逻辑与 +- `template bool logical_or` // 逻辑或 +- `template bool logical_not` // 逻辑非 + +简单示例一个`非` + +```c++ +#include +#include +#include +#include + +void test01() { + std::vector v; + v.push_back(true); + v.push_back(false); + v.push_back(true); + v.push_back(false); + + std::vector v2; + v2.resize(v.size()); + // 逻辑非 将v容器搬运到v2中,并执行逻辑非运算 + std::transform(v.begin(), v.end(), v2.begin(), std::logical_not()); + // std::logical_not 需要头文件 +} +``` + +## 五、STL-常用算法 + +概述: + +- 算法主要由头文件`` `` ``组成 + +- ``是所有STL头文件中最大的一个,范围涉及到比较、交换、查找、遍历操作、复制、修改等等。 + +- ``定义了一些模板类,用以声明函数对象。 + +- ``体积很小,只包括几个在序列上面进行简单数学运算的模板函数 + + + +特定容器算法: + +​ 链表类型list和forward_list,定义了独有的sort、merger、remove、reverse、unique,通用版本的sort要求随机访问迭代器,就不能用于list和forward_list。所以对于list和forward_list,应该优先使用成员函数版本的算法而不是通用算法(一些能用的通用算法代价会高很多)。 + +链表类型还定义了==splice==算法,是链表数据结构所特有的,因此不需要通用版。 + +### 5.0 一些命名规则 + +- 一些使用重载形式传递一个谓词: + std::unique(beg, end); // 使用==运算符比较元素 + std::unique(beg, end, comp); // comp谓词参数,用comp比较元素 + +- _if版本的算法:同名+\_if版本,接受一个谓词,带有\_if前缀的算法一般都接收一个谓词 + + std::find(beg, end, val); // 查找val第一次出现的位置 + std::find_if(beg, end, pred); // 查找第一个令pred为真的元素 + +- 区分拷贝元素版本和不拷贝元素版本:写到额外空间的,都加一个\_copy + std::reverse(beg, end); // 反转顺序 + std::reverse_copy(beg, end, dest); // 将元素逆序拷贝到dest + +- 一些同时提供\_copy和\_if版本,这些版本接收一个目的位置迭代器和一个谓词: + std::remove_if(v1.begin(), v1.end(), []\(int i) {return i % 2;}); // 删除奇数 + std::remove_copy_if(v1.begin(), v1.end(), std::back_inserter(v2), []\(int i) {return i % 2;}); // 将偶元素从v1拷贝到v2,且==v1不变== + +### 5.1 常用遍历算法 + +#### 5.1.1 for_each + +功能:实现遍历容器,需要头文件\ + +函数原型: + +- `std::for_each(iterator begin, iterator end, _func);` + - 前面是两个迭代器的位置 + - _func 函数或函数对象 + +```c++ +// 1、函数:因为要去使用的地方就int的vector +void printVector(int val) { + std::cout << val << ' '; +} +// 2、仿函数 +class Print02 { +public: + void operator()(int val) { + std::cout << val << ' '; + } +}; +void test01() { + std::vector v; + for (int i = 0; i < 10; i++) { + v.push_back(i); + } + // 1、两个迭代器,自己写的对vector的操作的函数名 + std::for_each(v.begin(), v.end(), printVector); + std::cout << std::endl; + + // 2、仿函数(放的是匿名对象) + std::for_each(v.begin(), v.end(), Print02()); + cout << endl; + // ,也可以先初始化一个对象再放进去 + Print02 p02; + std::for_each(v.begin(), v.end(), p02); +} +``` + +#### 5.1.2 transform + +功能描述:搬运一个容器的数据到另外一个容器中,需要头文件\ + +函数原型: + +- `std::transform(iterator beg1, interator end1, iterator beg2, _func);` + - // beg1 源容器的开始迭代器 + - // end1 源容器结束迭代器 + - // beg2 目标容器开始迭代器 + - // _func 函数或函数对象(可以对数据进行一些操作再搬运)(当然还能是lambda表达式) + +```c++ +class Print02 { +public: + void operator()(int val) { + std::cout << val << ' '; + } +}; +class MyTransform { +public: + int operator()(int val) { + // 这就是把原数据+100后再搬运,也可以原样搬运 + return val + 100; + } +}; +void test01() { + std::vector v; + for (int i = 0; i < 10; i++) { + v.push_back(i); + } + std::for_each(v.begin(), v.end(), Print02()); + std::cout << std::endl; // 这是源容器 + + std::vector targetV; + // 必须要先开辟一个空间才能搬运,不然直接报错 // 更新,或者直接使用std::back_inserter + targetV.resize(v.size()); + // 使用自动填充后,记得自己加`()`啊,总不是忘记 + std::transform(v.begin(), v.end(), targetV.begin(), MyTransform()); + std::transform(v.begin(), v.end(), std::back_inserter(targetV), MyTransform()); + // 以上两行是一个效果,第二个就不用先去resize开辟空间了,所以推荐这个 + std::for_each(targetV.begin(), targetV.end(), Print02()); + + // 更新。直接用它把string小写改大写(主要是第三个参数,目标容器就是它本身) + std::string name = "zhangsan"; // 强推这个,直接变更本身,就不去创建新的容器了。 + std::transform(name.begin(), name.end(), name.begin(), + [](const char c){return std::toupper(c);}); +} +``` + +> 注意: +> +> - 这些都是要头文件的`` +> - 目标容器必须先开辟出空间才能使用transform +> - 搬运的函数里面,可以对数据进行一些操作再返回 + +### 5.2 常用查找算法 + +#### 5.2.1 find + +​ 功能描述:查找指定元素,找到就返回指定元素的迭代器(就会退出,后面的就不找了);找不到就返回迭代器end().,头文件 + +函数原型: + +- `std::find(iterator beg, iterator end. value);` // value是要查找的元素 + +```c++ +#include +// 1、内置数据类型 +void test01() { + std::vector v; + for (int i = 0; i < 10; i++) { + v.push_back(i); + } + std::vector::iterator it = std::find(v.begin(), v.end(), 50); + if (it == v.end()) { + std::cout << "这就是没找到该元素,或说该元素不存在于此容器" << endl; + } + else { + std::cout << "找到该元素,为:" << *it << std::endl; + } +} + +// 2、 自定义数据类型 +class Person { +public: + std::string m_Name; + int m_Age; + Person(string name, int age) { + this->m_Name = name; + this->m_Age = age; + } + // 特别注意这里,因为是底层的`==`对比,不能修改值,一定要加const,不然就是错的 + bool operator==(const Person &p) { + if (this->m_Age == p.m_Age && m_Name == p.m_Name) { + return true; + } + return false; + } +}; +void test02() { + std::vector v; + Person p1("aa", 10); + Person p2("bb", 20); + Person p3("cc", 30); + v.push_back(p1); + v.push_back(p2); + v.push_back(p3); + Person p("cc", 30); + // 底层也是用`==`去作对比的,自定义数据类型就需要重载`==` + std::vector::iterator it = std::find(v.begin(), v.end(), p); + if (it == v.end()) { + std::cout << "没有找到该人" << std::endl; + } + else { + std::cout << "找到该人,姓名:" << it->m_Name << " 年纪:" << (*it).m_Age << std::endl; + } +} +``` + +> 注意: +> +> - 自定义数据类型一定要类内重载`==`,让底层知道怎么做对比 +> - 重载`==`时,参数一定要加`const`,这种只读的尽量养成习惯吧 +> - find前记得加std:: ,然后这个头文件在vs中不要行,但在linux环境下一定要(故一定都要加上) + +--- + +除了作用于容器,还可以作用于数组,那它的返回值就是对应数组类型的指针: + +```c++ +#include +#include +int main() { + + int arr[] = { 11, 22, 33, 44, 55 }; + // 不太确定类型是怎么写的时候,可用auto + int *out = std::find(std::begin(arr), std::end(arr), 222); + if (out == std::end(arr)) { + std::cout << "无" << std::endl; + } + return 0; +} +``` + +甚至可以通过运算指定范围查找: + +```c++ +int arr[] = { 11, 22, 33, 44, 55 }; +auto out = std::find(arr+1, arr+4, 222); // 这里是右开的,是不包括arr[4]这个数据的 +``` + +#### 5.2.2 find_if + +功能描述:按条件查找元素 + +函数原型: + +- `std::find_if(iterator beg, iterator end, _Pred);` + - // _Pred就是谓词或返回bool类型的仿函数 + - // 找到就会返回该元素的迭代器,并直接退出,后面不找了,没找到就返回最后的迭代器end() + +```c++ +// 1、内置数据类型 +class Greater5 { +public: + bool operator()(int val) { + return val > 5; + } +}; +void test01() { + std::vector v; + for (int i = 0; i < 10; i++) { + v.push_back(i); + } + std::vector::iterator it = std::find_if(v.begin(), v.end(), Greater5()); + // 注意这里找到第一个就会退出了,得到的也是找到的第一个的位置 + if (it == v.end()) { + std::cout << "这就是没找到该元素,或说该元素不存在于此容器" << std::endl; + } + else { + std::cout << "找到该元素,为:" << *it << std::endl; + } +} + +// 2、 自定义数据类型 +class Person { +public: + std::string m_Name; + int m_Age; + Person(std::string name, int age) { + this->m_Name = name; + this->m_Age = age; + } +}; +// 查找年龄>10的 +// (1)、一元谓词 +class Greater10 { +public: + bool operator()(const Person &p) { + return p.m_Age > 10; + } +}; +// (2)、仿函数(上面才是仿函数啊,这就是一个普通函数啊) +bool myGreater10(const Person &p) { + return p.m_Age > 10; +} +void test02() { + std::vector v; + Person p1("aa", 10); + Person p2("bb", 20); + Person p3("cc", 30); + v.push_back(p1); + v.push_back(p2); + v.push_back(p3); + // 看这个参数,第三个参数是要`谓词` + //vector::iterator it = find_if(v.begin(), v.end(), Greater10()); + // 或者使用仿函数 + std::vector::iterator it = std::find_if(v.begin(), v.end(), myGreater10); + if (it == v.end()) { + std::cout << "没有找到该人" << std::endl; + } + else { + std::cout << "找到该人,姓名:" << it->m_Name << " 年纪:" << (*it).m_Age << std::endl; + } +} +``` + +> ​ 总结:find_if按条件查找使查找更加灵活,提供的仿函数可以改变不同的策略。 +> +> - 在书上看到find_if接受一个一元谓词,因此传递给find_if的可调用对象必须接受单一参数(但是自己没去验证过,然后其它的类似接受一元谓词的算法应该也是类似) + +#### find_first_of + +字符串中,其它的查找方法: + +- find_first_of :查找与给定字符串中任何一个字符匹配的位置, + +- find_first_not_of :搜索第一个不在参数中的字符,如下: + + ```c++ + std::string numbers("012345678"), name("r2d2"); + int pos = name.find_first_of(numbers); // 1(name中2的下标) + std::string dept("03714p3"); + int index = dept.find_first_not_of(numbers); // 5(p的下标) + ``` + +- 还有: + +- find_last_of + +- find_last_not_of + + ```c++ + // 去掉字符串的开头、结尾的空白 + std::string trim_leading_whitespace(const std::string &str) { + size_t first = str.find_first_not_of(' '); + if (std::string::npos == first) { + return str; + } + size_t last = str.find_last_not_of(' '); + return str.substr(first, (last - first + 1)); + } + ``` + + + +一个练习(挺重要,写法思想挺有意思,用来==查找字符串中是否包含或是不包含另一组字符串中任意一个字符==): + 首先查找字符串"ab2c3d7R4E6a"中的每个数字字符并打印,然后查找其中给的每个字母字符,两个版本,第一个用find_first_of,第二个用find_first_not_of: + +```c++ +#include +#include +int main(int argc, char **argv) { + std::string number("0123456789"); + std::string str("ab2c3d7R4E6a"); + // 找数字 + for (int pos = 0; (pos = str.find_first_of(number, pos)) != std::string::npos; ++pos) { + std::cout << str[pos] << " "; + } + // 找字母 + for (int pos = 0; (pos = str.find_first_not_of(number, pos)) != std::string::npos; ++pos) { + std::cout << str[pos] << " "; + } + return 0; +} +``` + +特别重要的Tips: + +- 重要:注意这个创建number这个用意,假设以后要查找某个字符串中有没有包含哪些符号,可以把这些符号也创建一个字符串,然后这样使用查找; + +- 然后就是自循环中的写法,可以求一个表达式的结果(记得括号括起来)再去做逻辑判断;然后同时把找到的新pos赋值给pos,也别忘了++pos; + +- 特别注意:字符串,每个搜索操作都会返回一个std::string::size_type值(有时候我们直接用int作为接收类型也是可以用的),表示匹配发生位置的下标,==如果搜索失败(即没有找到),就会返回一个名为std::string::npos的static成员==,且标准库将npos定义为一个const std::string::size_type类型,并初始化为-1。 + + ```c++ + std::string str123("aaaaaabbbb"); + // 这里定义为int pos也是一样的效果 + auto pos = str123.find_first_of(number); + if (pos == -1) { + std::cout << "进来了" << std::endl; + } + if (pos == std::string::npos) { + std::cout << "这俩效果是一样的" << std::endl; + } + ``` + + 会发现用 -1 或 std::string::npos 的效果是一样的,但直接打印std::string::npos会是一个很大的数字。 + +##### std::string::npos + +直接看上面,有说明,find也会经常用,可见leetcode的[796题](https://leetcode-cn.com/problems/rotate-string/)。 + +#### 5.2.3 adjacent_find + +功能描述:查找相邻重复元素 + +函数原型: + +- `std::adjacent_find(iterator beg, iterator end);` + - // 查找相邻重复元素,返回相邻元素的第一个位置的迭代器 + +```c++ +void test01() { + std::vector v; + v.push_back(10); + v.push_back(30); // 输出的就是这个30 + v.push_back(30); + v.push_back(10); + v.push_back(30); + std::vector::iterator it = adjacent_find(v.begin(), v.end()); + if (it == v.end()) { + std::cout << "没有" << std::endl; + } + else { + std::cout << *it << std::endl; + } +} +``` + +#### 5.2.4 binary_search + +功能描述:查找指定元素是否存在 + +函数原型: + +- `bool std::binary_search(iterator beg, iterator end, value);` + - // value 要查找的元素 + - // 这个不同的是查找到返回true,没有找到返回的是false + +```c++ +void test01() { + std::vector v; + for (int i = 0; i < 10; i++) { + v.push_back(i); + } + // 得到的是bool值 + bool ret = std::binary_search(v.begin(), v.end(), 5); + if (ret) { + std::cout << "找到了,容器中有该元素" << std::endl; + } + else { + std::cout << "没找到" << std::endl; + } +} +``` + +> 总结: +> +> - 跟其它算法不同的是,这个的返回值不是迭代器,而是`布尔值` +> - 二分查找法的查找效率很高,但注意查找的容器中元素必须得是`有序的` + +#### 5.2.5 count + +功能描述:统计元素个数(也可以说是出现次数) + +函数原型: + +- `std::count(iterator beg, iterator end, value);` + +对自定义数据类型,找指定年龄的有多少个 + +```c++ +#include +// 1、内置数据类型 +void test01() { + std::vector v; + for (int i = 0; i < 10; i++) { + v.push_back(5); + } + int nums = std::count(v.begin(), v.end(), 5); // 10个 + std::cout << nums << std::endl; +} + +// 2、 自定义数据类型 +class Person { +public: + std::string m_Name; + int m_Age; + Person(string name, int age) { + this->m_Name = name; + this->m_Age = age; + } + // 底层也是用的`==`来对比,一定要加const + // 自定义数据类型一定要重载`==`,返回布尔值,说明怎么才是相等 + bool operator==(const Person &p) { + if (this->m_Age == p.m_Age) { + return true; + } + return false; + } +}; +void test02() { + std::vector v; + Person p1("aa", 10); + Person p2("cc", 20); + Person p3("aa", 10); + Person p4("aa", 15); + v.push_back(p1); + v.push_back(p2); + v.push_back(p3); + v.push_back(p4); + Person p("dd", 10); + // 这就是查找跟p年龄相同的人的个数 + int nums = std::count(v.begin(), v.end(), p); + std::cout << nums << std::endl; // 2 +} +``` + +> ​ 注意:自定义数据类型,内部一定要重载`bool operator==() {}`,内部说明怎么才算是相等。 + +还有一个点注意:(std::find对string来说也是这样) + +```c++ +// a_str 是 std::string类型 +int mun = std::count(a_Str.begin(), a_tarStr.end(), 'l'); // 这是ok的,因为a_Str的个体是char,所以查找char没问题,但是后面查找的内容不能是std::string,即以下是错的的 +int mun = std::count(a_Str.begin(), a_tarStr.end(), "lol"); +``` + + + +#### 5.2.6 count_if + +功能描述:获取满足条件的元素的个数 + +函数原型: + +- `std::count_if(iterator beg, iterator end, _Pred);` + - // _Pred 谓词 + +```c++ +// 1、内置数据类型 +bool greater20(int val) { + return val > 5; // 仿函数(这是普通函数) +} +void test01() { + std::vector v; + for (int i = 0; i < 10; i++) { + v.push_back(i); + } + // 用的仿函数(不是是不是当时理解错了,这就是普通函数啊),也可以放谓词,获取大于5个元素个数 + int nums = std::count_if(v.begin(), v.end(), greater20); + std::cout << nums << std::endl; +} + +// 2、 自定义数据类型 +class Person { +public: + std::string m_Name; + int m_Age; + Person(string name, int age) { + this->m_Name = name; + this->m_Age = age; + } +}; +// 用的谓词(谓词是这样写的,要重载`bool operator()() {}`) +class Greater10 { +public: + bool operator()(const Person &p) { + return p.m_Age > 10; + } +}; +void test02() { + std::vector v; + Person p1("aa", 10); + Person p2("cc", 20); + Person p3("aa", 10); + Person p4("aa", 15); + v.push_back(p1); + v.push_back(p2); + v.push_back(p3); + v.push_back(p4); + // 查找年龄大于10的 + int nums = std::count_if(v.begin(), v.end(), Greater10()); + std::cout << nums << std::endl; // 2 +} +``` + +> ​ 总结:这种带if,按条件查询的,都要给一个谓词(写的类,重载的`()`,返回的是布尔值),或是给仿函数。 + +### 5.3 常用排序算法 + +#### 5.3.1 sort + +函数原型:需要头文件\ + +- `std::sort(iterator beg, iterator end, _Pred);` + - // _pred:谓词,可以不给,默认从小到大 + +```c++ +void myPrint(int val) { + std::cout << val << ' '; +} +// 1、方法1,重载`()`来做谓词,仿函数 +class Sort01 { +public: + bool operator()(int v1, int v2) { + return v1 > v2; + } +}; +// 2、方法2,直接写一个返回bool值的函数 +bool Sort02(int v1, int v2) { + return v1 > v2; +} +void test01() { + std::vector v; + v.push_back(30); + v.push_back(10); + v.push_back(50); + v.push_back(20); + v.push_back(40); + std::sort(v.begin(), v.end()); // 默认降序 + std::for_each(v.begin(), v.end(), myPrint); + std::cout << std::endl; + // 需要一个谓词,三种方法 + // 方法1 + //sort(v.begin(), v.end(), Sort01()); + + // 方法2(注意函数千万不要给括号,上面是匿名对象) + //sort(v.begin(), v.end(), Sort02); + + // 方法3(内建函数对象)(还是导入头文件,兼容低版本) + std::sort(v.begin(), v.end(), std::greater()); + std::for_each(v.begin(), v.end(), myPrint); +} +``` + +#### 5.3.2 random_shuffle + +功能描述:指定范围内的元素随机调整次序 + +函数原型: + +- `std::random_shuffle(iterator beg, iterator end);` // 只要开始、结束位置 + +```c++ +void test01() { + std::vector v; + for (int i = 0; i < 10; i++) { + v.push_back(i); + } + // 设了随时间的随机种子,每次才不一样;记得头文件 + std::srand((unsigned int)time(NULL)); + std::random_shuffle(v.begin(), v.end()); +} +``` + +#### 5.3.3 merge + +功能描述:将两个容器元素合并,并存储到另一容器中 + +函数原型: + +- `std::merge(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator vTarget.begin);` // 前面四个参数就是两个容器的开始和结尾,最后一个参数就会目标容器的开始迭代器 + +```c++ +void test01() { + std::vector v1, v2; + for (int i = 0; i < 10; i++) { + v1.push_back(i); + v2.push_back(i + 1); + } + std::vector vTarget; + // 目标容器必须要先开辟空间 + vTarget.resize(v1.size() + v2.size()); + std::merge(v1.begin(), v1.end(), v2.begin(), v2.end(), vTarget.begin()); +} +``` + +> ​ 注意:v1、v2必须是两个有序的序列,且要么都是升序,要么都是降序;得到的目标容器也是有序的。 + +#### 5.3.4 reverse + +函数原型: + +- `std::reverse(iterator beg, iterator end);` // 直接开始、结束给进去就行 + +```c++ +std::reverse(v.begin(), v.end()); +``` + +### 5.4 常用拷贝和替换算法 + +#### 5.4.1 copy + +功能描述:容器内指定范围内的元素拷贝到另一容器中 + +函数原型: + +- `std::copy(iterator beg, iterator end, iterator vTarget.beg);` + +```c++ +void myPrint(int val) { + std::cout << val << ' '; +} +class Print02 { +public: + void operator()(int val) { + std::cout << val << ' '; + } +}; +void test01() { + std::vector v1; + for (int i = 0; i < 10; i++) { + v1.push_back(i); + } + std::vector v2; + v2.resize(v1.size()); // 记得先给空间 + std::copy(v1.begin(), v1.end(), v2.begin()); + std::for_each(v2.begin(), v2.end(), myPrint); + std::cout << std::endl; + std::for_each(v2.begin(), v2.end(), Print02()); +} +``` + +> 总结: +> +> - 知道就行,更多直接用v2 = v1的赋值操作就好了 +> - 核心:像for_each这种,里面需要传个自己写的函数的,好像都是可以用对应的仿函数(内类重载`()`来实现),反之好像也成立 + +--- + +std::copy 这个是不需要什么头文件的 + +```c++ +#include +int main() { + int a1[] = { 0, 1, 2, 3, 4, 5 }; + int a2[sizeof(a1) / sizeof(*a1)]; + // 核心是这行代码 + auto ret = std::copy(std::begin(a1), std::end(a1), std::begin(a2)); + return 0; +} +``` + +​ std::copy接受3个参数,前两个表示一个输入范围,第三个表示目的序列的起始位置(所以上面第6行代码的第3个参数可以直接写成 a2 也行),copy返回的是其目的位置迭代器(递增后)的值,即ret恰好指向拷贝到a2的微博元素之后的位置。 + +#### 5.4.2 replace + +功能描述:将容器内指定范围内的旧元素改为新元素 + +函数原型: + +- `std::replace(iterator beg, iterator end, oldvalue, newvalue);` + - // 将区间内所有值为oldvalue的元素全部全部替换为newvalue + +```c++ +void test01() { + std::vector v; + v.push_back(10); + v.push_back(20); + v.push_back(20); + v.push_back(30); + std::for_each(v.begin(), v.end(), myPrint); + std::cout << std::endl; + // 核心是这行;将容器中所有20换成50 + std::replace(v.begin(), v.end(), 20, 50); + + for (std::vector::iterator it = v.begin(); it != v.end(); it++) { + std::cout << *it << ' '; + } +} +``` + +> 注:std::replace除了只能给iterator后,还可以给位置,特别是对string,非常友好: +> +> 例:a_str.replace(1, 2, std::string(5, 'a')); // 从索引为1开始往后2个字符被替换成5个a +> +> std::replace(str.begin(), str.end(), 'c', 'a'); // 传统的给迭代器,是把所有的c换成a,而且这里的值只能给char,不能是string + +##### replace_copy + +​ 使用replace时都是会改变原来的容器中的值,如果不想改变源容器中的值,想把提替代后的值重新放一个容器,那就是 std::replace_copy + +```c++ +#include +#include +#include +#include // 需要这个头文件 +int main() { + std::list li{0, 1, 2, 0, 1, 2}; + std::vector vec; + // 核心是下面这行代码 + std::replace_copy(li.cbegin(), li.cend(), std::back_inserter(vec), 0, 42); + return 0; +} +``` + +​ 解释:这个就比replace多了一个参数,std::back_inserter(vec),然后vec中的值就是li中的0改成42后的数据。li中的数据是保持不变的。 + +​ 这里一个注意点:书上提到过,标准库算法不会改变他们所操作容器的大小,为什么使用`std::back_inserter`不会使这一断言失效? +​ 答:back_inserter是插入迭代器,在`iterator.h`头文件中(但试验下来好像vs、linux都不需要导入头文件就能用),不是标准库的算法。 + +#### 5.4.3 replace_if + +功能描述:将区间内满足条件的元素,替换成指定元素 + +函数原型: + +- `std::replace_if(iterator beg, iterator end, _Pred, newvalue);` + - // _Pred,长这样的都是谓词 + +```c++ +class Print02 { +public: + void operator()(int val) { + std::cout << val << ' '; + } +}; +class GreaterEqual20 { +public: + bool operator()(int val) { + return val >= 20; + } +}; +bool gre_equ20(int val) { + return val >= 20; +} +void test01() { + std::vector v; + v.push_back(10); + v.push_back(20); + v.push_back(20); + v.push_back(30); + std::for_each(v.begin(), v.end(), Print02()); + std::cout << std::endl; + + // 核心是这行,将大于等于20的元素置为50 + //replace_if(v.begin(), v.end(), GreaterEqual20(), 50); // 1、(谓词)(仿函数)的实现 + std::replace_if(v.begin(), v.end(), gre_equ20, 50); // 两行的效果是一模一样的,2、这是是全局函数的实现 + std::replace_if(v.begin(), v.end(), [](int i) {return i >= 20; }, 50); // 3、使用lambda函数的实现;;这里lambda表达式之所以是 int i ,是因为vector中的元素是 int + + for (int i = 0; i < v.size(); i++) { + std::cout << v[i] << ' '; + } +} +``` + +> ​ 总结:谓词也是仿函数,现在就下定论了,凡是参数是_Pred(就是谓词(谓词又是类内重载`()`的)),其实也就是仿函数,都是可以用一个返回布尔值的全局函数来替代,反之亦然。 +> +> ​ 这种带有if的都是可以自定义满足的条件的。 + +练习:结合上面replace_if中的代码查看,这里就只放了一点核心代码: + +```c++ +// v中元素 10 20 20 30 +std::vector v2; +// 方式一:dest直接使用v2.begin() +//v2.resize(v.size()); // 那一定需要这行,必须初始化,不然下面一定报错(这时v2中元素0 0 0 0) +//std::replace_copy_if(v.begin(), v.end(), v2.begin(), [](int i) {return i >= 20; }, 50); + +// 方式二:直接使用std::back_inserter(v2),就不需要上面初始化resize +std::replace_copy_if(v.begin(), v.end(), std::back_inserter(v2), [](int i) {return i >= 20; }, 50); +``` + +>replace(beg, end, old_val, new_val); // 直接把old_val替换掉 +>replace_if(beg, end, pred, new_val); // 使用谓词判断,慢足条件的替换掉 +>replace_copy(beg, end, dest, old_val, new_val); // 复制所有值到新容器,再在新容器内把old_val替换成new_val +>replace_copy_if(beg, end, dest, pred, new_val); // 上面只能是替换掉指定旧值,带if的就是自己写谓词条件 + +#### 5.4.4 swap + +功能描述:互换两个==相同类型==容器的元素 + +函数原型: + +- `std::swap(container c1, container c2);` + +```c++ +void test01() { + std::vector v1; + v1.push_back(10); + v1.push_back(20); + std::vector v2; + v2.push_back(333); + v2.push_back(555); + + std::swap(v1, v2); +} +``` + +### 5.5 常用算术生成算法 + +这属于小型算法,使用的时候记得包含头文件`#include ` + +#### 5.5.1 accumulate + +功能描述:计算区间内,容器元素累计总和 + +函数原型: + +- `std::accumulate(iterator beg, iterator end, value);` + - // value是设定的累加初始值,一般都是设置为0 + +```c++ +#include +#include +#include // 这个算法的头文件是这个 + +void test01() { + std::vector v; + for (int i = 0; i <= 100; i++) { + v.push_back(i); + } + // 这个算法一定要头文件`` + // 100是一个初始值,可根据需要设置成别的 + int total = std::accumulate(v.begin(), v.end(), 100); + std::cout << total << std::endl; // 5050 + 100 = 5150 +} +``` + +> 注意:若容器v中数据是浮点型,那给的起始值一定也要是这样的浮点型 +> +> `float total = std::accumulate(v.begin(), v.end(), 0.0f);` +> +> ​ 起始累加值必须得是浮点型,若是给的`0`,那么容器内的浮点型数据的精度是要被丢失掉的。 + +--- + +它可以把一些字符串连接起来: + +```c++ +#include +#include +#include +#include + +int main() { + std::list li{"aa", "bb", "aa", "cc"}; + std::string s = std::accumulate(li.begin(), li.end(), std::string("")); + return 0; +} +``` + +​ 特别注意:默认加的空字符串能是==std::string("")==,绝对不能是==""==(编译会直接出错),因为==""==的类型是const char*,而不是string,它是没定义+运算符,是不能字符串连接。 + +实现python中的 "_".join(s_list) 效果。 + +```c++ +#include +#include +#include +#include + +int main() { + std::vector vec{"aa", "bb", "aa", "cc"}; + std::string result = ""; + if (!vec.empty()) { + result = vec.at(0); // 第一个 + // 如果直接用下划线拼接起来(那直接拼接最前面也会有一个下划线,所以下面是从第2个开始拼接) + result += std::accumulate(vec.cbegin() + 1, vec.cend(), std::string(""), [](const std::string& str1, const std::string& str2) {return str1 + "_" + str2;}); + // 最后基本都是用lambda表达式,也可以用谓词去实现更复杂的写法 + } + std::cout << result << std::endl; + return 0; +} +``` + +#### 5.5.2 fill + +功能描述:向容器中所有元素变成指定的元素(相比resize,这个更多的是后期来重新填充元素) + +函数原型: + +- `std::fill(iterator beg, iterator end, value);` // value是填充的值 + +```c++ +#include +#include +#include // 注意这个头文件 + +void test01() { + std::vector v; + for (int i = 0; i <= 10; i++) { + v.push_back(i); + } + // 把这十个元素全部变成5了 + // 一样记得头文件`` + std::fill(v.begin(), v.end(), 5); + for (std::vector::iterator it = v.begin(); it != v.end(); it++) { + std::cout << *it << std::endl; + } +} +``` + +#### 5.5.3 fill_n + +如下,它将给定值(100)赋予迭代器指向的元素开始(vec.begin() + 2)的指定个(3)元素。 + +```c++ +#include +#include +#include // 注意这个头文件 +int main() { + std::vector vec{ 11, 22, 33, 44, 55 , 22, 33, 22 }; + // 核心在下面这行,将33、44、55这几个数全改为100 + std::fill_n(vec.begin() + 2, 3, 100); + return 0; +} +``` + +### 5.6 常用集合算法 + +集合算法都要注意一下三个点 + +>注意: +> +>1. 求交集的两个集合必须是==有序序列== +> 1. 且要么都为升序,要么都为降序 +>2. 目标容器需要提前开辟空间,空间的大小设置为两个原容器元素个数的较小值 +>3. set_intersection返回的是交集中最后一个元素的位置 + +#### 5.6.1 set_intersection + +功能描述:求两个容器的交集 + +函数原型: + +- `std::set_intersection(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator vTarget.begin());` + - // 前面四个参数是两个容器的开始、结束迭代器 + - // 最后一个参数是目标容器(即结果的容器的开始) + - // 函数还会返回目标容器(即交际结果)中最后一个元素的位置 + + + +```c++ +#include +#include +#include + +void myPrint(int val) { + std::cout << val << ' '; +} +class Print02 { +public: + void operator()(int val) { + std::cout << val << ' '; + } +}; +void test01() { + std::vector v1, v2; + for (int i = 0; i < 10; i++) { + v1.push_back(i); // 0~9 + v2.push_back(i + 5); // 5~14 + } + // 要先创建一个接受合集的容器 + std::vector vTarget; + // 然后一定要记得指定目标容器大小 + vTarget.resize(min(v1.size(), v2.size())); + // min(value1, value2) 这也需要头文件`` + + // 核心是这个 + std::vector::iterator itEnd = std::set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), vTarget.begin()); + std::for_each(vTarget.begin(), vTarget.end(), myPrint); // 不要用这各容器的.end(),会有很多的0元素,因为空间没用完,有默认值 + std::cout << std::endl; + // 记得使用函数返回值得到的最后一个元素的迭代器的位置 + std::for_each(vTarget.begin(), itEnd, Print02()); // 5 6 7 8 9 +} +``` + +#### 5.6.2 set_union + +功能描述:求两个集合的并集 + +函数原型: + +- `std::set_union(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator vTarget.begin());` + +```c++ +void test01() { + std::vector v1, v2; + for (int i = 0; i < 10; i++) { + v1.push_back(i); // 0~9 + v2.push_back(i + 5); // 5~14 + } + // 要先创建一个接受合集的容器 + std::vector vTarget; + // 然后一定要记得指定目标容器大小 + vTarget.resize(v1.size() + v2.size()); + // 核心是这个 + std::vector::iterator itEnd = std::set_union(v1.begin(), v1.end(), v2.begin(), v2.end(), vTarget.begin()); + std::for_each(vTarget.begin(), itEnd, Print02()); // 结果是它们的合集 +} +``` + +#### 5.6.3 set_difference + +功能描述:求两个集合的差集 + +函数原型: + +- `std::set_difference(iterator beg1, iterator end1, iterator beg2, iterator end2, iteratorvTarget.begin());` + - // 这是求前面这个容器对于后面这个容器的差集 + +```c++ +#include +#include +#include + +void myPrint(int val) { + std::cout << val << ' '; +} +class Print02 { +public: + void operator()(int val) { + std::cout << val << ' '; + } +}; +void test01() { + std::vector v1, v2; + for (int i = 0; i < 10; i++) { + v1.push_back(i); // 0~9 + v2.push_back(i + 5); // 5~14 + } + // 要先创建一个接受合集的容器 + std::vector vTarget; + vTarget.resize(max(v1.size(), v2.size())); + // v1相对v2的差集 + vector::iterator itEnd = set_difference(v1.begin(), v1.end(), v2.begin(), v2.end(), vTarget.begin()); + for_each(vTarget.begin(), itEnd, myPrint); // 0 1 2 3 4 + cout << endl; + vector vTarget1; + vTarget1.resize(max(v1.size(), v2.size())); + // v2相对于v1的差集 + std::vector::iterator itEnd1 = std::set_difference(v2.begin(), v2.end(), v1.begin(), v1.end(), vTarget1.begin()); + std::for_each(vTarget1.begin(), itEnd1, Print02()); // 10 11 12 13 14 +} +``` + +### 5.7 通用算法 + +#### 5.7.1 equal + +​ 用来确定两个序列是够保存相同的值,是只读的,所有元素都相等就返回true,否则返回false。 + +==std::equal(vec1.cbegin(), vec1.cend(), vec2.cbegin());== + +而且vec1可以是vector\,而vec2是list\ + +​ 它有一个很重要的假设:假定第二个序列(vec2)长度大于等于第一个序列(vec1),且算法要处理的第一个序列中的每个元素,它假定在第二个序列汇总都有一个与之对应的元素。 + + + +​ 注意:若两个容器保存的都是C风格字符串而不是string,调用equal算法,会发生什么? 答:C风格字符串是指向字符的指针表示的,因此会比较两个指针的值(地址),而不会比较两个字符串的内容。 + +#### 5.7.2 back_inserter + +​ 插入迭代器,书上写它是定义在头文件 #include \ 中的一个函数,但是我在vs或是linux下没有导入这个头文件都能使用。看这里,这是属于[插入迭代器](#5.8 插入迭代器)。 + +```c++ +#include +#include +int main() { + std::vector vec; // 1.空的vector + auto it = std::back_inserter(vec); // 2.通过它赋值会将元素添加到vec中 + *it = 42; // 3.vec中现在有一个元素,值为42 + // 4.像vec中往后添加3个值,值均为5 + std::fill_n(std::back_inserter(vec), 3, 5); + return 0; +} +``` + +​ 注:第4.点中,由于我们传递的参数时back_inserter放回的迭代器,因此每次赋值都会在vec上调用push_back,最终这条std::fill_n语句想vec的末尾添加了3个元素,每个元素的值都是5。 + +#### 5.7.3 unique + +​ 消除重复单词,但前提是一定要先sort排序,std::sort和std::unique的使用都需要头文件 #include\ + +```c++ +#include +#include +#include +#include // 都要这头文件 +int main() { + std::vector vec{"over", "fox", "the", "quick", "red", "fox", "the", "turtle"}; + + // 必须要先排序才能使用 std::unique + std::sort(vec.begin(), vec.end()); // 1. + auto end_unique = std::unique(vec.begin(), vec.end());//2 + + vec.erase(end_unique, vec.end()); //3. + return 0; +} +``` + +- 1.去重前都要先排序,以便查找重复单词(必须要先排序); +- 2.排完序后,单个单词只想保存一次,std::unique算法重排输入序列,将相邻的重复项“消除”,并返回一个还在想不重复值范围末尾的迭代器(可以理解为把有重复的值仅保留一个,其它的重复项直接都移动到末尾去,这个返回的迭代器就是指向所有保留下面不重复值得末尾后一个,也是那些多出来到末尾的值的开端); + 注意:这个时候vec的大小仍是8个,unqiue并不真的删除任何元素,只是让看不见了,这些重复元素仍是存在的,只是都到末尾去了(我这么理解的)(因为标准库算法对迭代器而不是容器进行操作,因此,算法不能直接添加或删除元素) +- 3.使用容器删除操作erase删除元素:删除从 end_unique开始直至vec末尾的所有元素,然后就剩6个值了。如果vec中没有重复值,那么返回的end_unique就等于vec.end(),那erase()传递进去的元素范围为空,那也是没问题的操作。 + +问:算法不改变容器大小的原因是什么?答: + +- 将算法和容器的成员函数区分开; +- 算法的参数时迭代器,不是容器本身。 + +#### 5.7.4 partition + +​ 这是一个标准库定义的算法std::partition,需要导入头文件 #include\, 它接受一个谓词,对容器内容进行划分(==有点像python的lambda表达式对列表进行筛选==),使得谓词结果为true的值排在容器的前半部分,而使谓词为false的值排在后半部分,==结果是返回的一个迭代器,指向最后一个使谓词为true的元素的后一个位置==,(这也再次印证了标准算法库不会改变容器的) + +```c++ +#include +#include +#include +#include // 这个算法需要这个头文件 +bool predicate(const std::string &s) { + return s.size() > 3; +} +int main() { + std::vector vec{"over", "fox", "the", "quick", "red", "fox", "the", "turtle"}; + auto pivot = std::partition(vec.begin(), vec.end(), predicate); + // 只会打印 over turtle quick + for (auto iter = vec.cbegin(); iter != pivot; ++iter) { + std::cout << *iter << std::endl; + } + return 0; +} +``` + +#### 5.7.5 distance + +计算两个迭带器之间的距离,好像不需要什么头文件 + +```c++ +std::vector vec{ 1, 2, 0, 4, 5, 6, 7, 0, 9 }; +auto it = std::find(vec.crbegin(), vec.crend(), 0); +std::cout << *it << std::endl; // 0 +std::cout << std::distance(it, vec.crend()) << std::endl; // 8 +``` + +注:for (auto iter = vec.crbegin(); iter != vec.crend(); iter++) {/*...\*/} 这样就可以倒序遍历容器。 + +### 5.8 插入迭代器 + +三种迭代器的不同之处: + +- `back_inserter` 使用 `push_back` 。容器要支持后面的push_back,前面的才能用 +- `front_inserter` 使用 `push_front` 。 +- `inserter` 使用 `insert`,此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。 + +除了unique之外,标准库还定义了名为unique_copy的函数,它接收第三个迭代器,表示拷贝不重复元素的目的位置,即将一个vector中不重复的元素拷贝到一个初始化为空的list中。 + +```c++ +#include // std::unique_copy需要这个头文件 +//#include // 看前面解释,这个头文件好像不是必须的 +std::vector vec{1, 1, 3, 5, 6, 6, 7}; +std::list li; +// 前面unique讲过了,vec一定是要排好序的,没排序就要先用sort +std::unique_copy(vec.begin(), vec.end(), std::back_inserter(li)); +``` + +### 5.9 iostream迭代器 + +==std::istream_iterator== + +```c++ +#include +#include +#include // std::accumulate要 +#include // std::istream_iterator要 +#include // 读文件要这个 +#include + +int main(int argc, char **argv) { + std::string path = "C:\\Users\\Administrator\\Music\\123.txt"; + std::ifstream file_stream(path); // 从123.txt中读取数字字符串 + // istream_iterator 必须要头文件 + std::istream_iterator str_in(file_stream); // 此时str_in和std::cin一样是流 + std::istream_iterator a_eof; // 表示尾后位置 + std::vector vec1; + + // 构造vec方式一:(两种方式只能任选其一) + while (str_in != a_eof) { //当有数据可供读取时 + vec1.push_back(*str_in++); // 解引用一个值后,再后置递增指针读取流 + } + // 构造vec方式二:// 从迭代器范围构造vec + std::vector vec2(str_in, a_eof); + // 或者 + std::copy(str_in, a_eof, std::back_inserter(vec2)); + // 下面只是单纯打印出来看看 + for (auto e : vec2) { + std::cout << e << std::endl; + } + return 0; +} + +// 下面是一个打印的别的办法 +void a_test() { + // 还可以直接使用算法操作流迭代器(输入一些数字,直接求得和) + std::istream_iterator in(std::cin), eof; + std::cout << std::accumulate(in, eof, 0) << std::endl; +} + +``` + +==std::ostream_iterator== + +```c++ +int main(int argc, char **argv) { + // ostream_iterator 必要要头文件 + std::vector vec{1, 1, 3, 5, 6, 6, 7}; + + // 可以用 ostream_iterator 来输出值的序列 + std::ostream_iterator out_iter(std::cout, " "); // 每个元素后面加一个空格 + // 方式一:下面就会将vec打印出来 + for (auto e : vec) { + *out_iter++ = e; // 赋值语句实际上将元素写到cout + //out_iter = e; // 两行的效果一模一样 + } + /*详解:这会将vec中的每个元素写到cout,每个元素后加一个空格,每次向out_iter赋值时,写操作就会被提交 + 但是,向out_iter赋值时,可以忽略解引用和递增运算,所以两行是一个意思*/ + std::cout << std::endl; + + // 方式二:通过调用copy来打印vec中的元素(copy在vs中什么头文件都不需要) + std::copy(vec.begin(), vec.end(), out_iter); + std::cout << std::endl; + return 0; +} +``` + +Tips: + +- std::ostream_iterator\ out_iter(std::cout, "\n"); // 还可以样加其它的 + +- 或者: + + ```c++ + #include + + std:: results{"hello", "world", "this", "is"}; + std::stringstream ss; + std::copy(results.begin(), results.end(), std::ostream_iterator(ss, "")); // 相当于用空格连接了容器里的所有 + + return ss.str(); + ``` + +--- + +​ 示例:使用流迭代器、sort和copy从标准输入读取一个整数序列,将其排序,并将结果写到标准输出(每个元素只打印一次,即要去掉重复元素) + +```c++ +#include +#include // std::istream_iterator、std::ostream_iterator需要 +#include +#include +#include // std::greater兼容低版本需要这个头文件 + +int main(int argc, char **argv) { + std::istream_iterator in(std::cin), eof; + + //std::vector vec; + //std::copy(in, eof, std::back_inserter(vec)); + std::vector vec(in, eof); // 上面两行与这效果一样 + + std::sort(vec.begin(), vec.end(), std::greater()); // 排序 + + std::vector new_vec; // 排序后才能去掉重复元素 + std::unique_copy(vec.begin(), vec.end(), std::back_inserter(new_vec)); + + // 打印输出 + std::copy(new_vec.begin(), new_vec.end(), std::ostream_iterator(std::cout, "\n")); + return 0; +} +``` + +注意:参考答案里,是直接用的std::unique去改变最原来的vec容器,我这是创建了一个新的,我用std::unique,答案很奇怪,不是我想要的。 + +--- + +​ 习题:接受三个参数:一个输入文件和两个输出文件的文件名。输入文件保存的应该是整数。使用 `istream_iterator` 读取输入文件。使用 `ostream_iterator` 将奇数写入第一个输入文件,每个值后面都跟一个空格。将偶数写入第二个输出文件,每个值都独占一行。(这个需要准备一个txt文件,里面都是数字,用空格或是换行分开) + +解答,我写的: + +```c++ +#include +#include +#include +#include +#include + +// 因为没办法把string转成char的相关,所以这里不能是 char *input_file (这语法行,但是在这列传递不过来) +void my_iterator(std::string input_file, std::string out_file1, std::string out_file2) { + // 1.先把要写的两个文件流创建好 + std::fstream ofs1(out_file1, std::ios::out); // 写文件一定要写模式 + std::fstream ofs2(out_file2, std::ios::out); // 两个输出文件 + // 2.把创建的文件流用ostream的迭代器写好 + std::ostream_iterator my_cout1(ofs1, " "); + std::ostream_iterator my_cout2(ofs2, "\n"); + // 3.读取文件的istream的迭代器 + std::fstream ifs(input_file); + std::istream_iterator num_in(ifs), my_eof; + while (num_in != my_eof) { + int num = std::stoi(*num_in); + if (num % 2 != 0) { // 这种以后就用三目运算符吧 + // 这就是把文件写进去,如果不是文件流,是cout流,就会打印出来 + my_cout1 = *num_in; + } + else { + my_cout2 = *num_in; + } + ++num_in; // 把这个放到while里就是不对,很奇怪(别忘了,否则会死循环) + } + ofs1.close(); + ofs2.close(); + ifs.close(); +} + +int main(int argc, char **argv) { + if (argc != 4) { + std::cout << "参数输入有错误,请重新输入!" << std::endl; + return -1; + } + my_iterator(argv[1], argv[2], argv[3]); + std::cout << "处理完毕" << std::endl; + system("pause"); + return 0; +} +``` + +参考答案的(确实太简练了,它用的lambda表达式,帅,但是17行那句,执行顺序,加定义的变量const int i ,还去做右值,确实很费解): + +但是这个案例给我们的启发式,如果读的文本全是数字。。那么可以直接 + +std::ostream_iterator\ my_cout1(ofs1, " "); // 直接用int接收,是可以自动转换的,而非读取文本就一定要用std::string去接收。 + +```c++ +#include +#include +#include + +int main(int argc, char **argv) +{ + if (argc != 4) return -1; + + std::ifstream ifs(argv[1]); + std::ofstream ofs_odd(argv[2]), ofs_even(argv[3]); + + std::istream_iterator in(ifs), in_eof; + std::ostream_iterator out_odd(ofs_odd, " "), out_even(ofs_even, "\n"); + + std::for_each(in, in_eof, [&out_odd, &out_even](const int i) + { + *(i & 0x1 ? out_odd : out_even)++ = i; + }); + return 0; +} +``` + diff --git "a/C++/GCC\347\274\226\350\257\221\345\231\250.md" "b/C++/GCC\347\274\226\350\257\221\345\231\250.md" new file mode 100644 index 0000000..70c68f6 --- /dev/null +++ "b/C++/GCC\347\274\226\350\257\221\345\231\250.md" @@ -0,0 +1,1824 @@ +## 一、g++基础使用 + +1. ==GCC 编译器==支持编译 Go、Objective-C,Objective-C ++,Fortran,Ada,D 和 BRIG(HSAIL)等程序; + +实际使用中: + +- 使用 gcc 指令编译 C 代码 +- 使用 g++指令编译 C++ 代码 + +当执行完程序`./main`,在控制台再输入`echo $?`可获取返回状态 + +- 得到0就是标志成功 +- 将代码main函数中改成return -1;(返回-1通常被当做程序错误的标识),编译执行这个程序,并不会有异常,但再`echo $?`得到的状态值255 + +### 1.1 编译过程 + +1. ### 预处理-Pre-Processing // .i文件 + + >g++ -E test.cpp -o test.i //.i文件 + > + >\# -E 选项指示编译器仅对输入文件进行预处理;更多的是对程序中的宏定义等相关的内容先进行前期的处理 + +2. ### 编译-Compling // .s文件 + + >g++ -S test.i -o test.s + > + >\# -S 编译选项告诉 g++ 在为 C++ 代码产生了汇编语言文件后停止编译 + >\# g++ 产生的汇编语言文件的缺省扩展名是 .s + +3. ### 汇编-Assembling // .o文件 + + >g++ -c test.s -o test.o + > + >\# -c 选项告诉 g++ 仅把源代码编译为机器语言的目标代码 + >\# 缺省时 g++ 建立的目标代码文件有一个 .o 的扩展名。 + +4. ### 链接-Lingking // bin文件 + + >g++ test.o -o test + > + >\# -o 编译选项来为将产生的可执行文件用指定的文件名g++ + +### 1.2 g++重要编译参数 + +#### 1.2.1 -g + +- -g 编译带调试信息的可执行文件 + + > g++ -g main.cpp + > + > // -g 选项告诉 GCC 产生能被 GNU 调试器GDB使用的调试信息,以调试程序。 + +#### 1.2.2 -O + +- -O~n~ 优化源代码 // n常为0~3 + + > g++ -O2 main.cpp // 这是大写的字母O + > + > - -O 同时减小代码的长度和执行时间,其效果等价于-O1; + > + > - -O0 表示不做优化; + > - -O1 为默认优化; + > - -O2 除了完成-O1的优化之外,还进行一些额外的调整工作,如指令调整等 ; + > - -O3 则包括循环展开和其他一些与处理特性相关的优化工作。 + + ​ -O 选项告诉 g++ 对源代码进行基本优化。这些优化在大多数情况下都会使程序执行的更快;所谓优化,例如省略掉代码中从未使用过的变量、直接将常量表达式用结果值代替等等,这些操作会缩减目标文件所包含的代码量,提高最终生成的可执行文件的运行效率。 + + ​ -O选项的使用将使整个编译过程花费的时间更多,但通常产生的代码执行速度会更快。 + + 简单的例子: + + ```c++ + #include + int main() { + unsigned long int counter; + unsigned long int result; + unsigned long int temp; + unsigned int five; + for (counter = 0; counter < 2009 * 2009 *100 / 4 + 2010; counter += (10 -6) / 4) { + temp = counter / 1979; + for (int i = 0; i < 20; ++i) { + five = 200 * 200 / 8000; + result = counter; + } + } + std::cout << result << std::endl; + return 0; + } + ``` + + 以上代码是非常低效的,每次循环都要去计算那固定的值,可以以此测试, + + `g++ main.cpp -o out1` `time ./out1` + + `g++ main.cpp -o out2 -O2 ` `time ./out2` + + `time` 后面跟命令可以测试命令花费的时间,可以发现加了-O2参数的out2执行时间短很多。 + +#### 1.2.3 -l 和 -L (重要必看) + +- -l 和 -L 指定`库文件` | 指定`库文件路径` + + > \# 链接一个名为`glog`的库: + > + > g++ -lglog main.cpp // -l后直接跟上库名,多个库的话就写多个 -l + > + > \# 链接名为`mytest`的库, libmytest.so在/opt/software/test/目录下 + > + > g++ -L/opt/software/tes1/ -L/opt/software/test2/ -lmytest main.cpp + + 注意: + + - -l和-L参数紧跟就是库名和地址,中间是`没有空格`的;(动态库有两个路径就给两个-L,空格隔开) + - 在`/lib`、`/usr/lib`和`/usr/local/lib`里的库直接用-l参数就能链接,如果库文件没放在这三个目录里就需要用-L参数指定库文件所在目录。 + +- 重要:以写那个俄罗斯方块来说,需要一个库==ncurses==,一开始直接 g++ main.cpp -o main, + + - 直接执行,一般会得到这个错误:“fatal error: ncurses.h: No such file or directory”,这是缺少库ncurses,centos是需要yum安装ncurses-devel.x86_64; + - 安装完后,直接再执行上面的编译命令g++ main.cpp -o main,会得到一堆“`undefined reference to`”的错误,这是因为并没有指定库。 + - 最终解决:g++ main.cpp -l ncurses -o main // 一定要指定-l ncurses,或写一起-lncurses + - 故:很多时候我们库安装好了,使用命令行编译,不会报找不到头文件的错了,但是会得到第二个错误 ,就是没有指定库,急着这个原因,出现好多次了。 + +#### 1.2.4 -I + +- -I 指定`头文件`搜索目录 + + > g++ -I/opt/home/myinclude main.cpp // 也是-I后紧跟路径 + + ​ 说明:`/usr/include`目录一般是不用指定的,gcc知道去那里找,但是如果头文件不在此目录里我们就要用-I参数指定了,比如头文件放在/opt/home/myinclude目录里,那编译命令行就要加上-I/opt/home/myinclude参数了; + + ​ 如果不加,应该就会得到一个"xxxx.h: No such file or directory"的错误。`-I参数可以用相对路径`,比如头文件在当前目录,可以用-I.来指定。上面我们提到的-cflags参数就是用来生成-I参数的。 + +#### 1.2.5 -D + +- -D 定义宏 // 这很重要 + + 实例demo: + + ```c++ + #include + + int main() { + #ifdef MYDEBUG // 名字自己随便起 + printf("debug log is on\n"); // 主要看这行是否执行 + #endif + printf("it is off\n"); // 这行怎样都会执行的 + return 0; + } + ``` + + `g++ main.cpp`: 执行程序时,第5行代码并不会执行 + + `g++ -DMYDEBUG main.cpp`: 第5行会执行,`-DMYDEBUG`就是在定义宏`MYDEBUG`,不给值,默认定义内容为字符串“1”;所以应该是可以定义为其它的,比如`-DMYDEBUG=aabbcc`,这肯定也是为真,所以第5行也是会执行。 + +#### 1.2.6 其它 + +- -Wall 打印警告信息 +- -w 关闭警告信息 +- -std=c++11 设置编译标准 +- -o 指定输出文件名,不给就默认是a.out + +### 1.3 实战命令 + +#### cpp文件、头文件不在一个文件夹 + +​ 最初的目录结构:2个dierctories、共3个files,具体如下: + +>. +>├── main.cpp +>├── myinclude +>│ └── swap.h +>└── src +> └── swap.cpp + +​ 直接编译`g++ main.cpp src/swap.cpp`,会报错,说是找不到`swap.h`头文件,那就需要用-I参数来指定了,且可以是相对路径,于是`g++ main.cpp src/swap.cpp -Imyinclude` (注意是大写的字母I紧跟着的头文件的相对路径) + +### 1.4 生成库文件并编译 + +>. +>├── main.cpp +>├── myinclude +>│ └── swap.h +>└── src +> └── swap.cpp # 依然以这个路径说 + +说明: + +- linux(它的库都是以`lib`作为开头的): + - 静态库以`.a`结尾; + - 动态库以`.so`结尾, + - ldd a.exe # 这样就可以查看可执行文件依赖的共享库.so(即动态链接库) + ldd b.so # 也可以查看动态库依赖了哪些 +- windows: + - 静态库以`.lib`结尾; + - 动态库以`.dll`结尾, + - Windows不适用.so共享库文件,要查看可执行文件的依赖库,需要用微软自家的==Dependency Walker==工具,更多介绍看[这里](http://c.biancheng.net/view/3868.html)(这里还有MinGW、MSYS、MSYS2之间的区别使用,及安装下载)。 + +#### 1.4.1 静态库 + +链接`静态库`生成可执行文件: + +```shell +cd src # 进到src下,去把swap.h编译成静态库 + +# 汇编,生成swap.o +g++ swap.cpp -c -I../myinclude # 头文件的位置,相对路径,会得到swap.o +# 生成静态库libswap.a # 约定库文件以`lib`开头吧 +ar rs libswap.a swap.o # 就会得到 libswap.a 静态库文件 + +# 回到上级目录 +cd .. +# 链接,生成可执行文件:static_main +g++ main.cpp -lswap -Lsrc -Imyinclude -o static_main +``` + +#### 1.4.2 动态库 + +链接`动态库`生成可执行文件: + +```shell +cd src # 进到src下,去把swap.h编译成动态库 + +# 生成动态库:libswap.so # 约定库文件都是lib开头 +g++ swap.cpp -I../myinclude -fPIC -shared -o libswap.so +# 以上命令可以拆分成以下两条命令 +gcc swap.cpp -I../myinclude -c -fPIC +gcc -shared swap.o -o libswap.so + +# 回到上级目录,链接生成可执行文件:dynamic_main +cd .. +g++ main.cpp -lswap -Lsrc -Imyinclude -o dynamic_main +``` + +- -fPIC:代表说与路径无关(不是很懂) +- -shared:说明是要生成动态库文件 + +#### 1.4.3.总结 + +1. 最后一步链接参数解读:需要链接库文件,所以直接`-lswap`,库文件的路径`-Lsrc`,它的头文件在`-Imyinclude`,一定要结合上面的g++编译参数来看; + +2. 静态库、动态库最后的链接命令是一样的,当同一目录下,有静态库也有动态库时,`默认优先选用的动态库`; + +3. 静态库是在链接时会会把汇编的.o文件打包进去,而动态库则是在使用时再去链接,所以文件`static_main`会比`dynamic_main`大一些; + +4. 静态库生成对的`static_main`是可以直接就执行的,然而执行`dynamic_main`时,就会报找不到`libswap.so`动态库的错误,这是因为这个动态库不在那系统三个路径下,找不到,就需要我们手动添加一下,那运行就是: + + - 静态库:`./static_main` + + - 动态库:`LD_LIBRARY_PATH=src ./dynamic_main` + - 注意这是一条命令 + +## 二、GDB调试 + +### 2.1 简单介绍 + +​ ==GDB(GNU Debugger)==是一个用来==调试C/C++程序==的功能强大的==调试器==,是Linux系统开发C/C++最常用的调试器 + +GDB主要功能: + +- 设置==断点==(断点可以是条件表达式); + +- 使程序在指定的代码行上暂停执行,便于观察; +- ==单步==执行程序,便于调试; +- 查看程序中变量值的变化; +- 动态改变程序的执行环境; +- 分析崩溃程序产生的core文件。 + +### 2.2 常用调试命令参数 + +​ 调试开始:执行`gdb [exefilename]`,进入gdb调试程序,其中exefilename为要调试的可执行文件名。 + +​ 下面是进到gdb后调试命令,以下命令括号内为命令的简化使用,比如run(r),直接输入r就代表命令run。 + +> - help(h) run # 代表查看run命令的帮助 +> - run(r) # 重新开始运行文件,没有断点就会直接把程序执行完(run-text:加载文本文件;run-bin:加载二进制文件)(建议进来先打断点,然后直接run) +> - start # 单步执行,运行程序,停在第一行执行语句(没有断点的话,系统会自己加一个断点,就是main函数下有效的第一句,执行这个会让调试从开始) +> - list(s) # 在此界面查看源码,默认断点处的上下5行(list 9:从第9行开始查看上下各5行代码;list 函数名:查看具体函数) +> - set # 设置变量的值 +> - next(n) # 逐过程,函数直接执行得结果 +> - step(s) # 逐语句,会跳入自定义函数内部执行 +> - info(i) # 查看函数内部局部变量的数值 +> - finish # 结束当前函数,返回函数调用点(就是IDE里面的跳出过程) +> - until 行号 # 一般用于跳出循环,把行号设置在循环体外(好像也可用于直接条跳转到其他行位置,有点c跳转到下一个断点的感觉) +> - continue(c) # 作用跟visual studio中的F5一样,会主程序中一行行执行下去,要是有多个断点,就会切到下一个断点 +> - print(p) # 打印值及地址 (只会打印一次,后面跟变量名就好了) +> - quit(q) # 退出gdb +> - break(b) num # 在第num行设置断点 +> - 这样是在main函数中打断点 +> - 给其它源文件打断点则是:`b yolo.cpp:123` +> - info breakpoints # 查看当前设置的所有断点 +> - delete(d) breakpoints-num # 删除第num个断点(首先要info breakpoints,就会得到所有断点的编号,然后直接delete 2) +> - display # 追踪查看具体变量值(后续任意命令的执行,都会展示)(也是后面直接跟变量名) +> - undisplay # 取消追踪观察变量(后面跟的是要取消的观察变量的编号,先info display获取到编号,在display 3这样的方式删掉) +> - watch # 被设置观察点的变量发生变量修改时,打印显示 +> - i watch # 显示观察点(这个i就是上面的info) +> - enable breakpoints # 启用断点 +> - disable breakpoints # 禁用断点 +> - x # 查看内存x/20xw 显示20个单元,16进制,4字节每单元 +> - run argv[1] argv[2] # 调试时命令行传参 +> - set follow-fork-mode child # Makefile项目管理:选择跟踪父子进程(fork()) + +> - (gdb) shell # 这会进到shell环境中去,然后输入 exit 回到gdb界面 +> - wi # 可视化调试,不知是不是就是打开tui模式 + +Tips: + +- 编译程序时需要加上-g,之后才能用gdb调试(不带-g参数,gdb [exefilename一般就会得到`no debugging symbols found`这样的提示信息); +- **回车键:重复上一命令**; +- gdb打开后,使用`ctrl+x+a`,可以打开tui模式,就可以比较直观的看调试过程,也可再次按这个退出。 +- break test.cpp:6 if num>0 //这就是设置带条件的断点;或者 b 函数名 就可以在函数开始行设置断点;一般跳转到断点所在行,这行都是还没执行。 +- 若是执行程序时需要传递参数,就在 gdb 二进制文件 进入到gdb调试界面,紧接着就输入`set args 参数1 参数2 ...`,然后再start就可以了。 + 或者最开始run的时候直接加上参数:run argv[1] argv[2] # 调试时命令行传参 + +### 2.3 快速使用 + +​ 一般针对就地一个新项目,调试界面进去后,建议直接先执行`start`,它会从main函数的第一行开始逐步debug,等找到main函数中报错的那一行(假如是55行),就打个断点`b 50`,再按`s`,就进到里面去debug,想要重新开始就直接输入`run`,就会自动执行到50行断点位置停住,就再进去调试就好; + +​ 等找到报错来自其它的源文件,就可以把上面50行处的断点删除,再在更深一步报错的.cpp源文件打断点,打断点的方式也不太一样,为:`b yolo.cpp:123`,然后直接`run`就会来到这个断点,就可以继续后续调试了。 + +Tips: + +- 按照上面快速给其它.cpp源文件打断点就好了,但如果这个源文件和主函数源文件不在一个目录里,就会提示 "No source file",让在加载的依赖库文件里打断点,按个y同意就好(源文件在不同文件夹也行,库文件生成时指定了)。 +- 不同文件夹中的文件,可以在gdb调试界面使用`directory`命令,好比`(gdb) directory ../test1/`,但是这个我没成功过就用上面的方法就好了。 +- 当然也别忘了快速去到下一个断点的命令`c`。 + +### 2.4 python同理调试 + +​ python在linux下的快速调试:在python文件中不引用pdb库,可以在执行python文件的时候,加上参数: + +方式一、 + +`python -m pdb demo.py`,来到pdb调试交互界面,debug模式将会停止在的第一行程序代码行,接下里的操作跟gdb就是一样的(暂时还不知道怎么调出源代码) + +- 设置断点:b(or break):设置断点;设置函数:b demo.func;设置行数:b demo:14(行数) # 还没试过 +- r(return):就是把当前函数执行完 + +方式二、 + +直接在代码中加入,执行程序时,就会直接到这行停住。 + +```python +import pdb +pdb.set_trace() # 直接把这行放在想要调试的代码前 +``` + +[这](https://www.cnblogs.com/xiaohai2003ly/p/8529472.html)是参考链接。 + +方式三、 + +这是终端输入参数的调试方法:[VSCode的c++开发.md](./VSCode的c++开发.md)中的最底部位置。 + +## 三、make + +### 3.1 make简介 + +利用make工具可以自动完成编译工作,这些工作包括: + +- 如果修改了某几个源文件,则只重新编译这几个源文件 +- 如果某个头文件被修改了,则重新编译所有包含该头文件的源文件 + +Ps:判断的依据是最近修改时间和上次是否一致,只要时间不一致,哪怕只是加了一个空行也会重新编译, + +​ 利用这种自动编译可以大大简化开发工作,避免不必要的重新编译(只编译修改了的)。make工具通过一个称为Makefile的文件来完成并自动维护编译工作,Makefile文件描述了整个工程的编译、连接规则。 + +### 3.2 Makefile文件 + +- 参考这个[教程](https://www.cnblogs.com/wang_yb/p/3990952.html)。还有这个[英文教程](https://makefiletutorial.com/#getting-started),也挺好。 + +Makefile的基本规则是: + +> TARGET... : DEPENDENCIES... +> COMMAND1 +> COMMAND2 +> ... + +- TARGER:目标程序产生的文件,如可执行文件和目标文件,目标也可以是要执行的动作,如clean,也称为伪目标。 +- DEPENDENCIES:依赖是用来产生目标的输入文件列表,一个目标通常依赖与多个文件。 +- COMMAND:命令是make执行的动作(命令是shell命令或是可在shell下执行的程序),注意每个命令行的起始字符必须为TAB字符。 +- 如果DEPENDENCIES中有一个或多个文件更新的话,COMMAND就要执行,这就是Makefile最核心的内容。 + +#### 3.2.1 简单示例 + +​ 假如有`calute.h`、`calute.cpp`、`input.h`、`input.cpp`、`main.cpp`,其中main.cpp是主程序入口,导入了其它两个头文件。有3中处理方式: + +1. 最直接的方式:`g++ -o my_out calute.cpp input.cpp main.cpp` 或是直接 g++ -o my_out *.cpp ,这样凡是有一点修改就把整个编译所有文件,多了就很不方便 + +2. 先编译再链接: + + - 编译得到.o文件:`g++ -c calute.cpp` g++ -c calute.cpp g++ -c main.cpp 就会得到这三个的.o文件, + + - 链接:`g++ input.o calute.o main.o -o my_out` 就会得到执行文件`my_out`了(这里面可以一部分是.o文件,一部分是.cpp文件) + +3. 写成Makefile的形式: + + ​ 每一行顶格写的都是目标,冒号后面紧跟着的就是依赖;然后每个命令行是需要用tab去空一段出来;make命令会为Makefile中的每一个以TAB开始的命令创建一个shell进程去执行。 + + ```makefile + # 第一行就是要实现的主目标,(main也可以是其它任意词),后面的*.o就是需要的依赖 + + main: main.o input.o calute.o input.o + g++ -o my_out main.o input.o calute.o # 主目标实现的命令 + main.o: main.cpp # 若是上面没有main.o依赖就在这里生成,它又依赖于main.cpp + g++ -c main.cpp + input.o: input.cpp + g++ -c input.cpp # 这个命令一定要tab空出来 + calute.o: calute.cpp + g++ -c calute.cpp # 上面就是缺哪个来下面找哪个 + + clean: # 这个目标就没有依赖,那输入这,就一定会执行下面的命令 + rm -f *.o + rm my_out + ``` + + Tips: + + - 这个就是哪个文件改了,就只会重新编译改了的或是引用了它的文件; + - 着重注意:`每个命令必须是以tab出来的空格,不能是4个空格`,那就是错的。 + +### 3.3 Makefile中的变量 + +​ Makefile中的变量都是字符串,它的使用方法是先定义一个变量,使用的时候就是`$(变量名)`,接着上面的Makefile写到: + +```makefile +name = zhangsan +curname1 = $(name) + +curname2 := $(name) + +curname3 ?= $(name) + +name = lisi +print: + @echo "curname1:$(curname1)" # 此时curname1的结果是liis + +​ @echo "curname2:$(curname2)" # zhangsan # 不知道这点怎么来的 + +​ @echo "curname3:$(curname3)" # lisi +``` + +- `=`赋值变量 + + ​ 然后执行`make print`就会得到`curname1:lisi`,也就是说,`=`获取的变量是它最后一次的有效值; + +- `:=`赋值变量 + + ​ 这就是看上面的例子,它在做`curname2:=$(name)`时,就是用前面`name`已经定义好的,无论这后面怎么变,它始终都不会变,上面的例子结果也会是`curname2:zhangsan` + +- `?=`赋值变量 + + ​ 这是一个很有用的赋值符,比如`curname3 ?= zhangsan`意思就是==如果变量curname前面没有赋值,那么curname就等于zhangsan,如果前面已经赋值了,那curname就用前面赋的值==。 + +- `+=`变量追加 + + 就是字符串的拼接 + +Tips: + +- 上面`@`的作用就是不让执行的命令打印出来,如果没有@,执行`make print`就会得到两行结果。一行是`echo "now_name:lisi"`,另一行是`now_name:lisi` + +### 3.4 Makefile模式规则 + +​ 接着上面的例子,main目标后,接下来的三条规则命令都是把.cpp文件编译成.o文件,基本算是重复的,那就可以使用一条规则来讲所有的.cpp文件编译为对应的.o文件。 + +​ 模式规则中,至少在规则的目标中(TARGET)中要包含`%`,否则就是一般规则,目标中的`%`表示对文件名的匹配,它表示`任意长度的非空字符串`,比如`%.cpp`就是所有以`.cpp`结尾的文件,类似于通配符,`a%.cpp`就表示以`a`开头,且以`.cpp`结束的所有文件; + +​ 当`%`出现再目标中(TARGET)的时候,目标中`%`所代表的值也决定了依赖中(DEPENDENCIES)的`%值`,上面的例子就成了: + +```makefile +object = main.o calute.o input.o +main: $(object) + g++ -o main $(object) # 这两行就是用变量名取代了 +%.o: %.cpp # 这里就是`%`的模式匹配 + g++ -c $< # 后面这个写法就是所有依赖,不能写成`g++ -c %.cpp` +``` + +#### 3.4.1 自动化变量 + +​ 第5行这个又叫==Makefile自动化变量==:上面说的模式规则中,目标和依赖都是一系列的文件,每一次对模式规则进行解析的时候都会是不同的目标和依赖文件,而命令只有一行,如何通过一行命令来从不同的依赖文件中生成对应的目标?自动化变量就是完成这个功能的! + +​ 所谓自动化变量就是这种变量会把模式中所定义的一系列的文件自动的挨个取出,直至所有的符合模式的文件都取完,==自动化变量只应该出现在规则的命令中==,常用的自动化变量如下表: + +| 自动化变量 | 描述 | +| :--------: | :----------------------------------------------------------: | +| $@ | 规则中的目标集合,在模式规则中,如果有多个目标的话,“S@”表示匹配模式中定义的目标集合。 | +| $% | 当目标是函数库的时候表示规则中的目标成员名,如果目标不是函数库文件,那么其值为空。 | +| $< | 依赖文件集合中的第一个文件,如果依赖文件是以模式(即“%”)定义的,那么“S<”就是符合模式的一系列的文件集合。 | +| $? | 所有比目标新的依赖目标集合,以空格分开。 | +| $^ | 所有依赖文件的集合,使用空格分开,如果在依赖文件中有多个重复的文件,“$^”会去除重复的依赖文件,值保留一份。 | +| $+ | 和“$个”类似,但是当依赖文件存在重复的话不会去除重复的依赖文件。 | +| $* | 这个变量表示目标模式中"%"及其之前的部分,如果目标是test/a.test.c,目标模式为a.%.c,那么“$*”就是test/a.test。 | + +常用的三种:`$@`、`$<`、`$^` + +### 3.5 Makefile伪目标 + +​ Makefile有一种特殊的目标——伪目标,主要是为了避免Makefile中定义的只执行命令的目标和工作目录下的时机文件出现名字冲突,有时候我们需要编写一个规则用来执行一些命令,但是这个规则不是用来创建文件的,比如前面用到的`clean` + +```makefile +clean: # 我们并不想得到clean这样的一个文件目标 + rm *.o + rm main +``` + +​ 上述规则中并没有创建文件clean的命令,因此工作目录下永远都不会存在文件clean,当输入`make clean`以后,后面的两条命令总是会执行;可当工作目录下有一个名为`clean`的文件,当执行`make clean`的时候,规则因为没有依赖文件,所以目标被认为是最新的,因此后面的rm命令也就不会执行,那设想的清理工作就无法完成,故为了避免这个问题,可以将clean声明为为目标,声明方式如下: + +```makefile +.PHONY: clean +``` + +这一行只要在clean之前定义了就行,不一定非得靠在一起。 + +### 3.6 修改Makefile(重要) + +​ 一般来说,使用cmake ..这样的命令在build中创建了Makefile,还是已经执行完了make命令后的Makefile,都是还可以修改Makefile的,经常用于修改CMAKE_INSTALL_PREFIX的值来改变安装的地址,操作比较简单(有些): + +- make help # 查看下有哪些可以执行的命令 +- make edit_cache # 这就会进到一个界面,看着操作就好了 +- 改完后,按c进行保存一下,退出,然后再执行make && make install + +Tips: + +- 这里面改东西是没有回退操作的,写错了或是要改原本的内容,Enter之后直接输入覆盖; +- make install的地址可以不存在的,它会自己创建 +- 不同的Makefile是有操作命令的,具体以make help为主; +- 很多自己写的都是没有写install的规则的(下面有说明,还是尽量写上完善吧),所以也就不会有make install的; +- 这还可以对某个单独的文件进行编译。 + +### 3.7 通用模板 + +这里有一些通用模板,[地址](https://mp.weixin.qq.com/s/1nXoEcdURd5EUWo4fb_Umg)。 + +## 四、cmake + +[这里](./modern-cmake.pdf)是cmake的一个PDF。 + +- EXECUTE_PROCESS : 这个命令可以在里面运行shell脚本,举例(更多网上看): + + ```cmake + execute_process(COMMAND <一句shell命令> WORKING_DIRECTORY <这句shell命令执行的工作目录>) + execute_process(COMMAND sh test.sh WORKING_DIRECTORY ) + ``` + +### 4.1 常用变量 + +这种变量是cmake自己就定义好了的,说明: + +- 引用变量内容需要使用`${变量名}`格式; + +- 可以使用message命令查看变量具体内容: + + - message用法:message([STATUS|WARNING|FATAL_ERROR] "some messafe") // 还有一些其它可选参数,但不重要了 + + ```cmake + message("The CMAKE_SOURCE_DIR is ${CMAKE_SOURCE_DIR}") + message(STATUS "The CMAKE_VERSION is ${CMAKE_VERSION}") + message(WARNING "this is a warning!") + message(FATAL_ERROR "this is a fatal warning!") + ``` + + > 1. 不加任何参数,会把内容直接打印出来(算重要消息); + > 2. STATUS,会在打印的信息前加上`-- ` (算非重要提示信息); + > 3. WARNING,一般会有红色的警告提示信息(算警告,会继续执行); + > 4. FATAL_ERROR,直接就会报错,在这里停住,生成也会失败。 + +- 可以在CMakeLists.txt中使用set命令改变某些变量值:`set(CMAKE_BUILD_TYPE Debug)` + + - 可以通过cmake命令行参数设置变量值:如`cmake -DCMAKE_BUILD_TYPE=Release`,但是注意这种方式会被CMakeLists.txt中的set命令设置的变量值覆盖 + +常用变量: + +| 变量名 | 含义 | +| :-----------------------: | :----------------------------------------------------------: | +| PROJECT_NAME | `project`命令中写的项目名 | +| CMAKE_VERSION | 当前使用cmake的版本 | +| CMAKE_SOURCE_DIR | 工程顶层目录,即入口CMakeLists.txt文件所在路径 | +| PROJECT_SOURCE_DIR | 同==CMAKE_SOURCE_DIR== | +| \_SOURCE_DIR | 同==CMAKE_SOURCE_DIR== | +| CMAKE_BINARY_DIR | 工程编译发生的目录,即执行cmake命令所在的目录,一般是在新建build目录下,使用`外部构建`,==建议使用这个== | +| PROJECT_BINARY_DIR | 同==CMAKE_BINARY_DIR==,也建议使用 | +| \_BINARY_DIR | 同==CMAKE_BINARY_DIR== | +| CMAKE_CURRENT_SOURCE_DIR | 当前处理的CMakeLists.txt所在的路径 | +| CMAKE_CURRENT_BINARY_DIR | 当前处理的CMakeLists.txt中生成目标文件所在编译目录 | +| CMAKE_CURRENT_LIST_FILE | 输出调用这个变量的CMakeList.txt文件的完整路径 | +| CMAKE_CURRENT_LIST_DIR | 当前处理的CMakeList.txt文件所在目录的路径 | +| CMAKE_INSTALL_PREFIX | 指定`make install`命令执行时包安装路径 | +| CMAKE_MODULE_PATH | `find_package`命令搜索包路径之一,默认为空 | + +编译配置相关变量,一般都需要set去显示指定: + +| 变量名 | 含义 | +| :--------------------: | :----------------------------------------------------------: | +| CMAKE_BUILD_TYPE | 编译类型,`Release`或`Debug`,如`set(CMAKE_BUILD_TYPE Release)` | +| CMAKE_CXX_FLAGS | `set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")`编译标志,设置C++11编译,有点原来的变量名基础上追加了内容,再覆盖这个变量名。 | +| CMAKE_CXX_STANDARD | 也可以设置C++11编译,`set(CMAKE_CXX_STANDARD 11)` (用上面的方式) | +| CMAKE_CXX_FLAGS | `set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")`,g++编译选项,在后面追加-std=c++1再覆盖 | +| CMAKE_C_FLAGS | gcc编译选项,也是一样在后面追加编译选项 | +| CMAKE_C_COMPILER | 指定C编译器 | +| CMAKE_CXX_COMPILER | 指定C++编译器 | +| EXECUTABLE_OUTPUT_PATH | 可执行文件输出的存放路径 | +| LIBRARY_OUTPUT_PATH | 库文件输出的存放路径 | + +判断操作系统 + +```cmake +IF (CMAKE_SYSTEM_NAME MATCHES "Linux") + +ELSEIF (CMAKE_SYSTEM_NAME MATCHES "Windows") + +ELSEIF (CMAKE_SYSTEM_NAME MATCHES "FreeBSD") + +ELSE () + MESSAGE(STATUS "other platform: ${CMAKE_SYSTEM_NAME}") +ENDIF (CMAKE_SYSTEM_NAME MATCHES "Linux") +``` + +判断编译器 + +```cmake +if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") +# using Clang +elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") +# using GCC +elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Intel") +# using Intel C++ +elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") +# using Visual Studio C++ +endif() +``` + +### 4.2 重要指令 + +简单说明: + +- 基本语法格式:指令(参数1 参数2...) + + - 参数使用括弧括起来 + - 参数之间使用==空格==或==分号==分开(一般选用空格) + +- 指令是大小写无关的,参数和变量是大小写相关的 + + ```cmake + set(HELLO hello.cpp) # 指定变量名 HELLO 它代替hello.cpp + add_executable(hello main.cpp hello.cpp) # 下面这两行就是一样的 + ADD_EXECUTABLE(hello main.cpp ${HELLO}) # 注意变量名是区分大小写的 + ``` + +- 变量使用`${}`方式取值,但是在`IF`控制语句中是直接使用变量名 + + - 比如上面的e`HELLO`是变量名,那么就应该是`if(HELLO)`,而不是if(${HELLO}),这就是错的。 + +==全局说明==: + +​ ==下面笔记中`[]`中的指令代表是可选的,可以不要==。 + +​ ==如果CMakeLists.txt中设定了一个参数,CMake命令执行时再给这个参数来修改其值是没用的==。 + +#### 4.2.1 cmake_minimun_required() + +含义:指定CMake的最小版本要求 + +- 语法:cmake_minimum_required(VERSION versionNumber [FATAL_ERROR]) + + ```cmake + # CMake最小版本要求为2.8.3 # 版本不对,后面系统自己就会报这个错误 + cmake_minimum_required(VERSION 2.8.2) + ``` + +#### 4.2.2 project() + +含义:定义工程名称,并可指定工程支持的语言 + +- 语法:project(projectName [CXX]\[C]\[Java]) + + ```cmake + project(hello_cmake) # 指定工程名hello_cmake + ``` + + project()函数将创建一个值为hello_cmake的变量${PROJECT_NAME} + +#### 4.2.3 set() | file() + +含义:显示地定义变量,创建的变量名可以方便后续直接使用 + +- 语法:set(VAR [VALUE]\[CACHE TYPE DOCSTRING [FORCE]]) + + +```cmake +# 定义一个名为`SOURCE`的变量 +# 方式1: +set(SOURCE src/Hello.cpp src/main.cpp) # 顺序是没有关系的 + +# 方式2,用GLOB+通配符 +file(GLOB SOURCE "src/*.cpp") + +# 语法:add_executable(exename source1 source2...) ---> add_executable(my_main 1.cpp 2.cpp...)这是是要所有的cpp源文件 +add_executable(${PROJECT_NAME} ${SOURCE}) + +# 因为有头文件,要让编译器知道,就就是为目标执行文件,链接头文件 +target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/include) +# 不知道中间为什么要这个`PRIVATE`,不要就会报错 +``` + +Tips: + +- 对于现代CMake,不建议为源代码使用变量;通常是在add_xxx函数中直接声明源文件。这对于glob命令尤其重要,因为如果添加新的源文件,glob命令可能并不总是显示正确的结果; + +- 前面执行make的时候,输出只显示构建的状态,要查看用于调试的完成输出,可以在运行make的时候添加`VERBOSE=1`的标志,即:`make VERBOSE=1` ,这个输入可以详细去看看,可以在输出里明显看到include目录添加到c++编译器命令中的。 + +##### aux_source_directory() + +​ 发现一个目录下所有的源代码文件并将列表存储在一个变量中,这个指令临时被用来自动构建源文件列表 + +- 语法:aux_source_directory(dir VARIABLE) + + ```cmake + # 定义一个SRC变量,其值为当前目录下所有的源代码文件 + aux_source_directory(. SRC) + # 编译SRC变量所代表的源代码文件,生成main可执行文件 + add_executable(main ${SRC}) + ``` + +#### 4.2.4 include_directories() + +含义:向工程添加多个特定的头文件搜索路径 --->相当于指定g++的`-I`参数(这是大写的I) + +- 语法:include_directories([AFTER|BEFORE] [SYSTEM] dir1 dir2...) # 前面有一些可选参数,暂时不知道干嘛的 + + ```cmake + #将绝对路径和相对CMakeLists.txt的相对路径./myinclude添加到头文件搜索路径 + include_directories(/opt/opencv/include/ myinclude) + ``` + +#### 4.2.5 add_libaray() | target_include_directories() | target_link_libraries + +- ==add_libaray()== -生成库文件(包括==动态和静态==) + + - 语法:add_libaray(libname [SHARE|STATIC|MODULE] [EXCLUDE_FROM_ALL] source1 source2... sourceN) # 这里的可选参数一般都要跟,动态或静态 + + - >target_link_libraries(demo LINK_PRIVATE ${OpenCV_LIBS} avcodec.so avformat avutil swscale.so) # avformat avformat.so是一个意思,要不要后缀都行 +- ==target_include_directories()== -为生成库文件指定使用到的头文件路径,一般是自己写的头文件 +- ==target_link_libaries()==-为target(执行程序)添加需要链接的共享库(动态库) --->相当于g++的`-l`参数(小写的l) + + - 语法:target_link_libaraies(target library1\ library2...) + - 比如opencv,要加了这个后,include的时候头文件才不会报错 + +##### (1)静态库 + +目录结构如下: + +> . +> ├── CMakeLists.txt +> ├── include +> │ └── static +> │ └── Hello.h +> └── src +> ├── Hello.cpp +> └── main.cpp + +```cmake +project(hello_library) +# 通过源文件生成静态库,我们这里指定的是`hello_library`,最后目录里的是`libhello_library.a` +add_library(hello_library STATIC src/Hello.cpp) +# 生成静态库时也要告诉它头文件在哪里啊(这说是把范围设成public,然后会使得这个头文件的会在以下地方使用:- 当编译这个库文件时; - 当编译任何链接了这个库的目标时)(Populating Including Directories) + +target_include_directories(hello_library PUBLIC ${PROJECT_SOURCE_DIR}/include) +# 注意去看这里的源码,它main.cpp、Hello.cpp中导包都是用的 #include "static/Hello.h" 都省去了上一级include目录,(这里的static也是文件夹的名字) + +# 生成可执行文件,需要链接这个静态库(Linking a Library) +add_executable(hello_binary src/main.cpp) +target_link_libraries(hello_binary PRIVATE hello_library) # 为target添加需要链接的共享库(动态库) --->相当于g++的`-l`参数(小写的l) +``` + +这个命令就像是: + +```shell +/usr/bin/c++ CMakeFiles/hello_binary.dir/src/main.cpp.o -o hello_binary -rdynamic libhello_library.a +``` + +##### 参数范围(scopes)的意义: + +- PRIVATE:the directory is added to this target’s include directories +- INTERFACE:the directory is added to the include directories for any targets that link this library. +- PUBLIC:As above, it is included in this library and also any targets that link this library. + +##### (2)动态库 + +目录结构: + +> . +> ├── CMakeLists.txt +> ├── include +> │ └── shared +> │ └── Hello.h +> └── src +> ├── Hello.cpp +> └── main.cpp + +```cmake +project(share_project) +# 生成一个动态库 +add_library(hello_library SHARED src/Hello.cpp) +# 一样也要告诉它头文件的位置 +target_include_directories(hello_library PUBLIC ${PROJECT_SOURCE_DIR}/include) +# 这是一个`Alias Target`,在项目中给这库起个别名,但实际名还是在的 +add_library(hello::library ALIAS hello_library) + +add_executable(hello_main src/main.cpp) +target_link_libraries(hello_main PRIVATE hello::library) +# 一样注意下main.cpp中的导入写法 +``` + +这个命令就像是: + +```shell +/usr/bin/c++ CMakeFiles/hello_binary.dir/src/main.cpp.o -o hello_binary -rdynamic libhello_library.so -Wl,-rpath,/home/matrim/workspace/cmake-examples/01-basic/D-shared-library/build +``` + +#### 4.2.6 install() | -DCMAKE_INSTALL_PREFIX + +​ CMake是可以控制这些生成的头文件、二进制文件、库文件被安装在哪里,命令是`make install`,是由`install()`函数控制的: + +这是基于上一个例子,目录结构: + +> . +> ├── cmake-examples.conf +> ├── CMakeLists.txt +> ├── include +> │ └── installing +> │ └── Hello.h +> ├── README.adoc +> └── src +> ├── Hello.cpp +> └── main.cpp + +```cmake +project(cmake_examples_install) + +# 生成一个动态库 +add_library(install_library SHARED src/Hello.cpp) +target_include_directories(install_library PUBLIC ${PROJECT_SOURCE_DIR}/include) # 动态库要链接头文件 + +# 生成可执行文件 +add_executable(install_main src/main.cpp) +# 把前面生成的动态库链接到执行文件 +target_link_libraries(install_main PRIVATE install_library) +#**********************************************# +# Install (这其实就是复制文件,也可以是复制其它的源文件) +# (1)Binaries(二进制可执行文件) +install(TARGETS install_main DESTINATION bin) +# TARGETS是固定写法;install_main是可执行文件名;DESTINATION也是固定写法;bin就是代表放在指定路径的bin目录下 + +# (2)Library(库文件) (有需要才写) +install(TARGETS ${install_library} DESTINATION lib) # 库可能很多个,起了别明后。要${}这样引用了才行 + +# (3)Header files(头文件) (有需要才写) +install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/ DESTINATION include) +# DIRECTORY也是固定写法;结合上面tree结果看,这回把include目录下的installing目录都放进去 + +# (4)Config (要有这文件才写) +install(FILES cmake-examples.conf DESTINATION etc) +# FILES也是固定写法;会生成名为cmake-examples.conf的文件在目标路径的etc下 +``` + +在window上可能有点不一样,是要用: + +```cmake +install (TARGETS install_library + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin) +``` + +再注意:install安装可是当复制文件用的,可是是复制可执行文件、复制文件、文件夹、库这些是有不同的区别的,具体看[这里](https://blog.csdn.net/qq_38410730/article/details/102837401),写的很详细。 + +​ 就按照自己写learnOpenGL的实战demo来看,有install(TARGETS|PROGRAMS|FILE|DIRECTORY) + +```cmake +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + message(STATUS "Setting default CMAKE_INSTALL_PREFIX path to ${CMAKE_BINARY_DIR}/install") + set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/install CACHE STRING "The path to use for make install" FORCE) +endif() + +install(TARGETS ${NAME} DESTINATION .) # 1.1 执行文件(用的TARGETS) + + # 1.2 也可以写为 "${CMAKE_SOURCE_DIR}/dlls/*.dll" +file(GLOB DLLS "dlls/*.dll") +install(PROGRAMS ${DLLS} DESTINATION .) # PROGRAMS指的是非目标文件的可执行程序 +# 1.3 文件夹 就要用 DIRECTORY ,前面 resources 目录加不加引号都一样 +install(DIRECTORY "resources" DESTINATION .) + +# 不能下面这样写,是错的,不同的文件用不一样的 TARGETS、PROGRAMS、FILE、DIRECTORY +# install(TARGETS "${CMAKE_SOURCE_DIR}/dlls/*.dll" .) +# install(TARGETS "resources" DESTINATION .) +``` + +Tips: + +==-DCMAKE_INSTALL_PREFIX== + +- `DESTINATION`就是代表`${CMAKE_INSTALL_PREFIX}`,这个路径是可以被ccmake指定,或者是使用`cmake -DCMAKE_INSTALL_PREFIX=/home/mypath ..`指定,==可以使用相较于执行cmake命令时的相对路径==; + + - 如果没有指定路径,那install的默认地址就是`/usr/local`; + + - 这是在在没有设置`-DCMAKE_INSTALL_PREFIX`参数时,设定把安装路径设置在构建build路径下的install目录(==一定要提前创建这个install目录==),放在顶级的CMakeLists.txt,且要在生成二进制文件或是库文件之前,最好就放在一开始: + + ```cmake + if( CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT ) + message(STATUS "Setting default CMAKE_INSTALL_PREFIX path to ${CMAKE_BINARY_DIR}/install") + set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/install" CACHE STRING "The path to use for make install" FORCE) + endif() + ``` + + - 当然还可以再设置路径的前缀-`DESTDIR`,即`${DESTDIR}/${CMAKE_INSTALL_PREFIX}` + + - make install DESTDIR=/home/temp,如果没设置CMAKE_INSTALL_PREFIX,那结果就是`/home/temp/usr/local`,设置了就按设置的来。 + +- 一般的执行就是mkdir build; cd build; cmake ..; make; make install # 当然最后两步是可以直接就一句make install完成; + + - make install运行完后,就会得到一个名为`install_manifest.txt`的文件,里面记录了安装的文件所在路径的详细内容; + - make [-j4/-j8] # make后面这个参数是选填的,代表使用几个cpu去编译,提高效率。 + +- 执行生成的二进制文件:它在/usr/local/bin下,这是在PATH路径中,名为install_main,直接执行install_main,可能就会报找不到我们程序要链接的`libinstall_library.so`动态库文件,这是因为存放这的动态库文件的路径`/usr/local/lib`没有添加到环境变量中,故: + + - `LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib install_main` + - \# 这是一个命令(如果是指定了安装路径,后面的lib路径可以跟相对地址的); + - `export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib`;`install_main` + - \# 第二个方法就是添加了临时变量,然后再执行。 + +- CMake没提供卸载的类似`make uninstall`的方法,但是可以使用`xargs rm < install_manifest.txt`。 + +#### 4.2.7 -DCMAKE_BUILD_TYPE + +BUILD_TYPE,构建选项: + +- Release:会添加`-O3`、`-DNDEBUG`的标识给编译器 (包括优化但没有调试信息) +- Debug:会添加`-g`的标识给编译器 (禁用优化并包含调试信息) +- MinSizeRel:会添加`-Os`、`-DNDEBUG`给编译器 (优化大小,没有调试信息) +- RelWithDebInfo:会添加`-O2`、`-g`、`-DNDEBUG`的标识给编译器 (优化速度并包含调试信息) + +通常使用的话可以:`cmake -DCMAKE_BUILD_TYPE=Release` # cmake命令行编译时用 + +一般来说,很简单的一个使用的话就是在顶级CMakeLists.txt中设置这个变量: + ==set(CMAKE_BUILD_TYPE Debug)== # 这是写到CMakeLists.txt中 + +或者官方按照下面设置默认的编译类型(也是放在顶级CMakeLists.txt): + +```cmake +# 如果没有指定编译类型,就默认设置为`RelWithDebInfo` +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message("Setting build type to 'RelWithDebInfo' as none was specified.") + set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE) + # 这是为了cmake-gui界面可以选择编译类型 + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" + "MinSizeRel" "RelWithDebInfo") +endif() +``` + +#### 4.2.8 添加宏 + +​ 说明:3.12版本之前是用的add_options(-Wall -std==c++11 -O2 -g)、add_options(-D一个宏),放这里作为一个了解,及3.12版本及以后用下面的版本。 + +==add_compile_options==-添加编译参数,感觉是图像化界面的一个选择参数,用的不多,放这里作为一个了解吧 + +- 语法:add_compile_options(\