【C++学习】—— (一)概念
首先,通过一张最新(2021.11)的编程语言排名图来了解常见的编程语言:
从图中可以看出,C++的排名相对于Python、Java、C来说并不突出,很大的原因是因为C++难度过大,也可以说是知识点太多,我们很难说能精通C++这门语言,只能说对它的部分了解,并能在工作中使用;
C++相关网站推荐
1、cppreference.com:这是一个C++最具权威的百科全书,但只有英文模式,相信大家可以看懂的;
2、Compiler Explorer (godbolt.org):在线代码编辑,可以直接生成汇编代码,可选择不同的编译器和版本,功能强大;
3、C++ Insights (cppinsights.io):一个可以将C++代码及逆行翻译的网页,具体化代码高级功能;
C++和C的关系
简单来说,C++是C语言的扩展,主要有以下两方面;
首先关注性能方面,这是继承于C语言的特性:
- 与底层硬件紧密结合
- 对象生命周期的精确控制
- Zero-overhead Abstraction
接着是扩展部分,引入大量特性,便于工程实践
- 三种编程范式:面向过程、面向对象、泛型
- 函数重载、异常处理、引用
最后,C++也是一系列不断演进的标准集合
- C++98、C++11、C++14、C++17、C++20、C++23?
- 语言本身的改进(例如Memory Model、Lambda Expressiomn)
- 标准库的改进(auto_ptr、type_train/ranges)
- C++标准的工业实现(MSVC、GCC、Clang)等编译器还存在差异;
C++特性说明
下面通过具体案例来说明上面列出的点
1、与底层硬件紧密结合
打开网站: https://godbolt.org/z/xPq6e9
运行其中的案例可以发现,输出的数从大到小;
其实在硬件底层,存放内存的地址分为大端法和小端法,而C++可以很好的适应不同硬件的存储方式,这样有助于提升性能,相比来说,JAVA就不考虑这些,导致数据存放到内存还需要再做一些处理,会增加一定的时间;
2、对象生命周期的精确控制
这里我们使用C++与C#做比较,对于C#而言,使用完一个对象并不用关注销毁,底层会自动进行销毁(垃圾回收机制);而C++从对象的创建、使用到销毁都需要使用者自己执行,不需要系统额外引用机制;这里有两个需要平衡的点,也就是易用和性能,这两者是有一定取舍的;
下面是C++和C#对于异常的处理方式:
C++:
C#:
造成这处理异常不同的原因就是生命周期精确控制引发的,Finally主要是用来显式销毁对象;
对生命周期的精准控制的好处:能够及时释放资源,并且不需要额外资源(线程)进行垃圾回收,提高了性能;
3、Zero-Overhead Abstraction
主要包含以下两点:
- 不需要为没有使用的语言特性付出代价
例如虚函数,或者说在堆或栈构造对象,可以通过这个例子来理解:Compiler Explorer (godbolt.org)
- 使用一些语言特性不等于付出运行期成本
可以通过这个例子来理解:Compiler Explorer (godbolt.org)
我们调用函数并没有付出函数中的运行成本,这也是由于函数在编译期执行;
本次为C++的一个开篇,重点是更好的理解C++相对于其他编程语言的一个特性,之后会持续更新,本次专栏计划是掌握C++的基础语法以及常用特性,并且从细节上去理解;
【C++学习】——(二)编译与链接模型
编译与链接模型
1、一开始的C++是一个简单的加工模型,如下图所示:
这样会存在一些问题:
- 无法处理大型程序
- 加工耗时较长
- 即使少量修改,也需要全部重新加工
2、为了解决以上问题,引入了分块处理的方式:
编译链接模型的好处:
- 编译耗资源但一次处理输入较少
- 链接输入较多但处理速度快
- 便于程序修改(只需修改一部分)
在引入分块处理后,出现了一些常见概念:
①定义与声明:一个变量在只需在一个文件中定义,其他文件可通过声明该变量;
②头文件与源文件:由于声明的变量、函数过多,可将声明放在头文件中,在源文件中引用头文件加载这些声明;
③翻译单元:源文件 + 相关头文件(直接/间接)- 应忽略的预处理语句(宏定义不符合条件的);
编译与链接流程
下面通过一个实际例子,讲解程序如何从cpp一步步到可执行文件的;
下图为一个整体流程图:
1、预处理阶段:将cpp或c的源程序进行处理(头文件展开等),转换成以i结尾的翻译单元文件
g++ -E ./main.cpp -o ./main.i
2、编译阶段:生成编译后以s为后缀的汇编代码文件
g++ main.i -S -o main.s
3、汇编阶段:将汇编代码进行汇编生成以o为后缀目标文件
4、链接阶段:合并多个目标文件,关联声明与定义,生成可执行程序
以上为系统内部具体的实现操作,我们在实际运行中可以通过一行命令实现编译链接:
g++ ./main.cpp -o ./main
注意点:在用IDE编译程序时,往往会有两种模式:Debug和Release,Debug在开发中使用,优化较少,Release在最终程序编译使用,优化较多;
总结
- C++的编译与链接过程是复杂的,预处理、编译与链接都可能出错,要细心排除;
- 编译可能产生警告、错误,都要重视;
- 我个人的感受,会遇到一些很奇怪的坑,不管从环境还是依赖库,这就需要有耐心不断尝试,并且总结经验;
【C++学习】——(三)系统IO
开篇
本次讲解一个小知识点,也是最常见的一个知识点:iostream;不管编写什么程序,必然会使用到IO流交互,从细节上理解简单的IO流;
iostream标准库
定义:标准库所提供的IO接口,用于与用户交互;
输入流:cin
输出流:cout、cerr、clog
输出流三者的一个区别:
1、首先就是输出目标的不同,cerr主要用来输出错误信息,clog主要用来输出日志信息;
2、cerr有个最大的不同,会立即刷新缓冲区,输出比较快,另外两种输出信息可能在程序崩溃时会丢失;
缓冲区刷新还有别的方法:std::flush、std::endl;
namespace
主要是为了避免名称冲突,可见如下代码:
namespace People1
{
void fun(){}
}
namespace People2
{
void fun(){}
}
上面代码中有两个同名函数,编译是可通过的,这就是命名空间的作用;
在使用fun()这个函数时,需要指定命名空间;
命名空间的使用有以下三种方式:
// 1、域解析符::
People1::fun();
// 2、using语句
using namespace People1;
fun();
// 3、命名空间别名
namespace ns1 = People1;
ns1::fun();
!!!注意:std就是标准库的命名空间,也是最常用的一个命名空间;
- printf:使用直观,但容易出错;
- cout:不容易出错,但书写冗长;
- C++20格式化库:新的方案(目前还不常用);
总结
本次就讲这么一点知识点,这也是入门C++的很多人疑惑的点,相信大部分人都是从输出"Hello World"开始的,那么C++中的IO细节估计没有弄懂,看完本篇就会有一个清晰的认知;
【C++学习】——(四)类型
开篇
类型一直是C++中最重要的部分,相比于其他高级语言,C++的类型会复杂许多,往往一个类型匹配错误就会导致程序报错,本篇主要讲解一些常用类型的概念以及细节,如果对于C++有一定基础的,可以跳转到思考部分,从中了解自己的掌握程度;
初始化与赋值
定义:初始化与赋值语句是程序中最基本的语句,功能是将某个值与一个对象关联起来;
- 值:字面量、对象(变量或常量)所表示的值等
- 标识符(对象):变量、常量、引用
初始化的基本操作:
1、在内存中开辟空间、保存相应的数值;
2、在编译器中构造符号表、将标识符与相关内存空间关联起来;
类型概述
下面通过几点概要说明:
1、类型是编译期概念,可执行程序中不存在类型的概念;
2、C++是强类型语言;
-
强类型语言定义:一旦一个变量被定义类型,如果不经过强制转换,那么它永远就是该数据类型;
-
弱类型语言定义:某一变量被定义类型,该变量可根据环境变化自动进行转换,不需要强转;
3、引入类型是为了更好描述程序,防止误用;
4、类型描述的信息:
- 存储所需要的大小(sizeof,标准没有严格限制,根据硬件不同字节数也不同)
- 取值空间(可用std::numeric_limits来判断,超过范围可能产生溢出)
#include<iostream>
#include<limits>
int main() {
int x = 10;
std::cout << std::numeric_limits<int>::min() << std::endl;//-2147483648
std::cout << std::numeric_limits<int>::max() << std::endl;//2147483647
std::cout << std::numeric_limits<unsigned int>::min() << std::endl;//0
std::cout << std::numeric_limits<unsigned int>::max() << std::endl;//4294967295
}
由上面程序运行结果可知,无符号int类型占4个字节,也就是32个比特位,所以最大范围为232,在不同的硬件下可能不同;
- 对齐信息(一般存放在内存中按类型的对齐信息的整数倍存储,比如int的对齐信息为4个字节,那存储的空间首地址为4的倍数,在结构体中,因为存在对齐信息,char也会按4个字节保存)
- 类型可执行的操作
类型分类
类型可以划分为基本类型和复杂类型;
基本(内建)类型:C++语言中支持的类型,包含以下几种:
1、数值类型
- 字符类型:char、wchar_t、char16_t、char32_t,通常为1个字节,表示256个值,也就是ASCII编码的字符;
- 整数类型:带符号整数类型(short、int、long、long long),无符号整数类型(unsigned+带符号整数类型)
- 浮点类型:float、double、long double
注意:在C++11中引入了固定尺寸的整数类型,如int32_t等,之前在针对开发板的程序中有见过该类型,主要是便于硬件的可移植性:
2、void类型
复杂类型:由基本类型组合、变种所产生的类型,可能是标准库引入,或自定义类型;
字面值及其类型
字面值:在程序中直接表示为一个具体数值或字符串的值;
每个字面值都有其类型,例子如下:
- 整数字面值(int):20(十进制)、024(八进制)、0x14(十六进制)
- 浮点数(double):1.3、1e8
- 字符字面值(char):‘c’、’\n’
- 字符串字面值(char[4]):“cpp”,注意这里字符串后会默认加/0,所以是四个字符长度
- 布尔字面值(bool):True、False
像如果想要定义float类型的数,可以加入后缀如1.3f;
C++提供了用户创建自定义后缀的函数:
#include<iostream>
// 后缀可自行定义,我这里用_bang
int operator "" _bang(long double x)
{
return (int)x * 2;
}
int main() {
int x = 7.14_bang;
std::cout << x << std::endl;
}
上面代码将7.14的浮点类型转换成整型并增大一倍,可自行定义后缀试一下;
变量及其类型
变量:对应一段存储空间,可以改变其中内容;
声明与定义的区别:不能重定义已经初始化的变量,需要加入extern用来声明;
初始化:全局变量会默认初始化为0,局部变量会缺省初始化(随机数值);
复合类型
1、指针:一种间接类型;
如上图为一个指针p指向一段内存,p保存的为val的地址,我们通过打印尺寸可知,指针p为8个字节;
特点:
- 可以"指向"不同的对象;
- 具有相同的尺寸;
- 指针与bool的隐式转换:非空指针可以转换为true、空指针可以转换为false;
注意两个符号:*(解引用符)、&(取地址符);
解引用符在不同环境下含义不同,看如下代码:
int x = 10;
int* p = &x;// 表示p为一个int指针类型
*p; // 表示解引用,获取指针指向地址的值
关于nullptr:
- 一个特殊的对象(类型为nullptr_t),表示空指针;
- 类似于C中的NULL,但更加安全;
void 指针*:没有记录对象的尺寸,可以表示任意类型指针,一般作为形参或返回值;
指针对比对象:指针复制成本低,引用成本高;
总结:指针在程序中的作用,最重要的就是作为参数传入,由于数据类型可能很大,传入指针大小固定为8个字节,并且指针值为地址可复制,复制成本低,并且可在函数中改变变量的值;
2、引用:
取地址符&也有两个含义:
int x = 10;
&x;// 取地址符
int& ret = x; // 定义ret为一个引用类型
特点:
- 是对象的别名,不能绑定字面值(指针也不能指向字面值);
- 构造时绑定对象,在其生命周期内不能绑定其他对象(赋值操作会改变对象内容);
- 不存在空引用,但可能存在非法引用,总体比指针安全;
- 属于编译期概念,在底层还是通过指针实现;
常量类型
- 使用关键字const声明常量对象;
- 是编译期概念,由编译器保证,作用为防止非法操作、优化程序逻辑;
常量指针(顶层常量):
int* const p = &x;
常量指针表示指针为常量,指针不能更改指向;
底层常量:
const int* p = &x;
底层常量表示指针指向的地址的内容不能发生改变,指针指向可改变;
常量引用:
- 用const int&定义一个常量引用;
- 主要用于函数形参(对于较复杂的数据类型);
- 可以绑定字面值;
常量表达式:
constexpr int x = 1;// x的类型仍为const int
- 声明的是编译期的常量,编译器可以对其进行优化;
类型别名
类型别名:引入特殊的含义或便于使用,例如size_t;
-
引入类型别名的两种方式:
1、typedef int Mytype;
2、using Mytype = int;(C++11后)
第二种方式更好;
-
应将指针类型别名视为一个整体,引入常量const表示指针为常量的类型;
-
不能通过类型别名构造引用的引用;
类型自动推导
定义:通过初始化表达式定义对象类型,编译器会自动推导得到;(C++11开始)
-
推导得到的类型还是强类型,并不是弱类型;
-
自动推导的几种形式:
1、auto:最常用的形式,会产生类型退化(由于左值右值的类型区别);
2、const auto、constexpr auto:推导出的是常量、常量表达式类型;
3、auto&:推导出引用类型,避免类型退化;
4、decltype(exp):返回exp表达式的类型(左值加引用);
5、decltype(val):返回val的类型;
6、decltype(auto):简化decltype的使用,C++14开始支持;
补充:类型退化表示一个变量作为左值和右值时类型不同,例如数组作为右值为指针;
域与对象声明周期
域(scope):表示程序中的一部分,其中的名称有唯一含义,有全局域、块域等;
- 域可以嵌套,嵌套域中定义的名称可以隐藏外部域中定义的名称;
- 对象的生命周期起始于被初始的时刻,终止于被销毁的时刻;
- 全局对象的生命周期是整个程序运行期间,局部对象终止在所在域执行完成;
思考
1、思考下下面关于指针的两行代码的含义:
int x = 1;
int* p = &x;
int y = 0;
*p = y;// 第一行
p = &y;// 第二行
这两行表明了指针的一个特定,可改变性,每一行的含义如下:
第一行:将指针p指向的内存地址的值改变为y;
第二行:不改变x的值,而是将指针p的指向改成y;
2、经过指针的思考后,我们看看关于引用的思考:
int x = 1;
int& f = x;
int y = 0;
f = y;// 思考一下这一行的作用,是改变了引用f的绑定吗?
上面这行代码并不改变f的绑定,而是改变了f的值,同时引用对象x的值也发生改变;
3、经过了指针和引用的思考,下面思考下两者在底层有什么关联:
int x;
int* p = &x; *p = 1;
int& f = x; f = 1;
分析下上面两行代码,他们底层实现会相同吗?
这是两者的汇编代码实现,可以发现是完全相同的,引用底层也是通过指针实现的;
4、思考以下代码中&x是什么数据类型?
int x = 1;
const int* p = &x;
如果我们只考虑&x的话,这是一个int*的类型,但由于第二行代码执行拷贝构造,隐式地将&x转换为左值所需要的 const int *类型;
5、思考下面函数传参的区别?
void fun(int x){}
void fun(const int& x){}
从本质上来说,上面两种传参实现的作用是一致的,第一个进行拷贝构造传递,所以在函数内部无法改变外部x变量的值,而下面的传参传入引用可以在函数内部改变外部x的值,加入const强制成变量;第二种其实是画蛇添足地做法,但常量引用对于复杂的数据类型来说,是能够节省很多空间的,比如自定义的结构体;
6、下面常量表示底层常量还是顶层常量?
using mytype = int*;
int x = 1;
const mytype p = &x;
这里我们容易误导,还会认为这是一个底层常量,但由于别名的定义,这里其实是一个顶层常量,我们可以将mytype看作一个整体,那么指针的指向不可发生改变;
7、下面auto&自动推导出的y是什么类型?
const int x = 1;
auto& y = x;
相信大部分人会认为x会类型退化,从而y为int&类型,实际上这里类型不会退化,所以y为const int&类型;
8、下面来看看decltype自动推导的类型是什么?
int x = 1;
decltype(x);// 1
decltype((x));// 2
decltype在传入参数为左值时加入引用,那么第一行为一个变量,所以为int类型,第二行为表达式,所以加入引用为int&类型;
总结
本篇讲解的类型知识点很杂,并且涵盖很多小的知识点,很多细节部分在实际工程中不一定会接触到,当然在工程中也会遇到很多自己不理解的类型转换,需要多通过debug模式来查看类型;
本篇知识点较多,可以选择自己想了解的部分进行查看,后续会继续推出更深层次的内容;
【C++学习】——(五)数组
开篇
上一篇讲解了类型,通过类型来开始本篇的学习;
int a[10];
上述代码中的a是什么类型呢?
相信很多人都知道是一个数组类型,具体来说是一个int[10]的类型;
数组概念
定义:将一到多个相同对象串连到一起,所组成的类型;
初始化方式:
1、缺省初始化:int x[5];
2、聚合初始化:int x[] = {1,2,3};
注意:
1、不能用auto来声明数组类型;
2、数组不能复制,也不能赋值;
数组的复杂声明
指针数组的声明:
int *i[5];
大家思考下i的类型是什么?
指针数组表示数组内的每个元素都是int*类型,所以i的类型为int *[5];
数组指针的声明:
int (*x)[5];
大家思考下x的类型是什么?
这里a是一个指针,类型为int(*)[5];
数组到指针
- 使用数组对象时,通常会产生数组到指针的隐式转换;
- 可通过引用声明来避免隐式转换;
int a[3] = {1, 2, 3};
auto b = a;// b的类型为int*
auto &b = a;// b的类型为int(&) [3]
- 数组和指针的转换关系图
指向数值开头的指针很好获得,比如a、&(a[0])、std::begin(a);
获取指向数组结尾的指针(上图指向80):a+3、&(a[3])、std::end(a);
使用标准库获取开头和结尾指针的方法在别的数据类型也适用;
数组操作
1、获取数组元素个数;
int x[3];
// 方法一
std::cout << sizeof(x) / sizeof(int) << std::endl;
// 方法二
std::cout << std::size(a) << std::endl;
// 方法三
std::cout << std::end(a) - std::begin(a) << std::endl;
方法三实际上是在运行期才执行的,增加程序运行耗时,不推荐;
方法一类型需要自己传入,适用性差,不推荐;
推荐用方法二;
2、使用for循环遍历数组(C++11开始支持)
int a[3] = {1, 2, 3};
for (int x: a)
{
std::cout << x << std::endl;
}
拓展
1、C字符串
-
C字符串本质也是数组;
-
声明一个字符数组并打印长度
#include <cstring>
char a[] = "Hello";
std::cout << strlen(a) <<std::endl;
使用函数strlen需要引入头文件cstring;
2、vector
定义:是C++标准库中定义的类模板;
- 与内建数组相比,更侧重于易用性(相对而言性能比内建数组差),可复制,可在运行期动态改变元素个数;
- 初始化与构建
// 1、聚合初始化
std::vector<int> x = {1, 2, 3};
// 2、其他初始化方式
std::vector<int> x(3, 1);// 个数为3,并且每个元素都为1
vector的初始化方式还有很多,可参考:https://en.cppreference.com/w/cpp/container/vector/vector
- 获取元素个数
std::cout << x.size() << std::endl;
- 判断为空
std::cout << x.empty() << std::endl;
- 尾部添加元素
x.push_back(2);// 向容器中添加一个整数2
- 删除最后一个元素
x.pop_back();
- 打印vector中的元素
std::vector<int> x = {1, 2, 3};
x[2];// 跟数组一样,越界不报错
x.at(2);// 不可以越界
- 在标准库中的begin和end函数,在vector中也有同名的方法并且作用相同,返回一个迭代器;
- 可以使用指针引用一个vector对象的方法:
std::vector<int> x = {1, 2, 3};
std::vector<int>* p = &x;
std::cout << p->size() << std::endl;
3、string
定义:是C++标准库中定义的一个类模板特化别名,用于内建字符串的替代品;
- 与内建字符串相比,更侧重易用性,可复制,可在运行期动态改变字符串个数;
- 构造和初始化,可参考:https://en.cppreference.com/w/cpp/string/basic_string
- 支持比较、赋值、拼接、索引、转换为C字符串(c_str());
思考
1、思考以下代码输出什么?
int i[3] = {1, 2, 3};
std::cout << *(a) << std::endl;// 第一行
std::cout << *(a + 1) << std::endl;// 第二行
第一行的输出是1,第二行输出的是2,这就相当于a[0]和a[1]的值,说明数组底层也是指针实现,第二行中加一表示首地址地址移动类型大小的字节;
2、以下代码能够编译通过吗?
int a[2] = {1, 2};
std::cout << a[100] << std::endl;
这个数组越界在C++中是可以编译通过的,会输出一个毫无关系的值,编译器不会有边界检查,需要特别注意!
3、在另一个文件中定义了数组,如何在该文件中定义?
test.cpp:
int arr[3] = {1, 2, 3};
main.cpp:
extern int arr[];
上述声明称为不完整类型的声明,可以在main.cpp中找到test.cpp定义的数组;
总结
本篇简要介绍了数组的常用方法以及C++标准库提供的一些关于数组的容器,大家也可以从思考部分来了解数组的一些细节;
【C++学习】——(六)表达式
基础
定义:表达式由一到多个操作数组成,可以求值并通常会返回求值结果;
-
最基本表达式:变量、字面值,通常包含操作符;
-
操作符特性:
1、接收几个操作数:一元、二元、三元;
2、对类型有要求(可能涉及类型转换);
3、操作数是左值还是右值;
4、结果的类型;
5、结果是左值还是右值;
6、优先级与结合性,可用小括号来改变运算顺序;
7、 操作符重载:不改变接收操作数的个数、优先级与结合性;
注意:关于操作符优先级问题可以参考以下文档:https://en.cppreference.com/w/cpp/language/operator_precedence
左值与右值
参考网站:https://zh.cppreference.com/w/cpp/language/value_category
在C语言中:左值可能放在等号左边,右值只能放在等号右边;
在C++中,左值也不一定能放在等号左边,右值也可能放在等号左边;
值类型关系图:
- glvalue:泛左值,标识一个对象、位或函数;
- pvalue:纯右值,用于初始化对象或作为操作数;
- xvalue:亡值,表示其资源可以被重新使用;
C++是支持左值和右值的转换的;
decltype可以接收表达式,并且根据类型不同产生不同的值:
可参考文档:https://zh.cppreference.com/w/cpp/language/decltype
- 表达式的值类别为纯右值,则为type;
- 表达式的值类别为纯左值,则为type&;
- 表达式的值类别为亡值,则为type&&;
类型转换
定义:一些操作符要求其操作数具有特定的类型,或者具有相同的类型,此时可能产生类型转换;
-
隐式类型转换:编译器自动发生的;
参考文档:https://zh.cppreference.com/w/cpp/language/implicit_conversion
-
显式类型转换
1、static_cast<新类型>(表达式);
2、const_cast<新类型>(表达式):去除常量性或增加常量性;
3、显示引入的转换;
操作符
-
算数运算符中,除逻辑非外,其它操作符都是左结合的;
-
逻辑与、逻辑或具有短路特性;
-
逻辑与&&优先级高于逻辑或||;
-
按位取反符:~ 按位与:& 按位或:| 按位异或:^ 移位操作符:<<、>>
-
移位操作在一定程度上是乘以或除以2的幂,但速度更快;
-
赋值操作符是右结合的;
-
还有一些其他操作符,比如成员访问操作符.和->,条件操作符?:等,在这就不做介绍了;
思考
1、思考下面x变量是左值还是右值,有什么特性?
const int x = 3;
x是一个纯左值,由于系统内部为常量,所以不能放在等号左边;
2、思考一下以下赋值操作符的原始代码是怎样的?
int x;
int y;
x = y = 5;
首先赋值操作符是右结合的,先计算y=3,并且不是把y的值赋予给x,而是这个表达式的返回值赋予x,原始代码如下:
x = (y = 3);
可以通过https://cppinsights.io/这个网站,看出C++内部对一些代码的转换处理;
3、思考以下代码做了什么事情?
int x = 2;
int y = 3;
x^=y^=x^=y;
答案就是通过复合赋值操作,交换了x和y的值,详细也和异或这个操作符有关;
总结
表达式这个概念在C++中属于比较细节的知识了,很多时候我们只用知道怎么用,对于编译器内部怎么处理我们并不关心;并且关于左值和右值这个概念,也是C++比较深的一个小知识点,了解后对于程序的优化是有很大帮助的,本篇重点需要关注左值和右值,多参考官方cppreferenc的文档,这是最权威的说明文档;
【C++学习】——(七)语句
开篇
语句这个概念应该是学习编程语言最熟悉的了,还记得第一次写冒泡排序吗?那个循环和判断也难住了很多人;本篇不会具体介绍每一个语句的作用,而是讲述一些细节以及关于C++对于语句的特性;
基础
1、语句常见类型
- 表达式语句:表达式后面加分号,对表达式求值后丢弃,会产生副作用(也就是赋值等操作);
- 空语句:仅包含一个分号的语句,可能与循环一起工作;
- 复合语句:由大括号组成,无需在结尾加分号,形成独立的域(语句域);
2、顺序语句
- 从语义上按照先后顺序执行;
- 实际的执行顺序可能产生变化(编译器优化、硬件乱序)
- 与硬件流水线紧密结合,执行效率高;
3、非顺序语句(加入分支)
-
在执行过程中引入跳转,从而产生复杂变化;
-
分支预测错误可能导致执行性能下降;(分支预测是用来优化非顺序语句执行效率)
-
最基本的非顺序语句goto:通过标签指定跳转到的位置,本质上对应了汇编语言中的跳转指令,但容易造成逻辑混乱,应避免使用;
一些语句的细节
1、在switch语句中,如果没有用break跳出当前的switch就会执行下面的case;这样的一个情况在C++17中引入了一个属性**[fallthrough]**,感兴趣的可以了解下;
2、switch与if的优劣:
- switch的分支能力较弱,switch能实现的if都能实现,反过来则不能;
- switch在一些情况下能引入更好的优化;
3、关于for循环的使用案例,可以参考:https://zh.cppreference.com/w/cpp/language/for
4、基于范围的for循环
本质:语法糖,编译器会转换为for循环的调用方式;
注意:在C++11、17、20中不断演化改进;
参考文档:https://zh.cppreference.com/w/cpp/language/range-for
5、break和continue
break:导致外围的for、范围for、while、do-while循环或switch终止;
continue:用于跳过整个for、while或do-while循环体的剩余部分;
总结
本篇对于语句的基础并没有过多的讲解,不管是C++、JAVA还是Python,在循环部分都是相似的,只有语言特性上的不同;而对于循环逻辑,在实际工程中是最重要的,往往一个边界判断错误就会导致越界或者报错的情况,这也需要大家不断实践,推荐刷一些Leetcode真题,可看我总结的Leetcode刷题专栏;
对于想深入了解C++的工程师来说,强烈推荐多看cppreference,基本涵盖了每个知识点的原型以及使用案例,可以抽时间静下心看看;
【C++学习】——(八)函数
基础
函数:封装了一段代码,可以在一次执行过程中被反复调用,包含函数头和函数体;
函数头:
- 函数名称(标识符),用于后续的调用;
- 形式参数,代表函数的输入参数;
- 返回类型,函数执行完成后返回结果的类型;
函数体:一个语句块(block),包含具体的计算逻辑;
函数的声明与定义:
- 函数声明只包含函数头,不包含函数体,通常在头文件中;
- 函数声明可以出现多次,定义通常出现一次(也有例外);
函数调用:
- 需要提供函数名与实际参数;
- 实际参数拷贝初始化给形式参数;
- 返回值会拷贝给函数的调用者;
- 栈帧结构(可自行了解下);
参数
-
对于非模板函数来说,其每个形参都有确定的类型,但形参可以没有名称;
-
实参到形参的拷贝顺序是不确定的;
-
函数的形参的传递一般分为:传值、传址、传引用;
-
变长参数的定义:
1、使用initializer_list传递:
#include <initializer_list> void fun(std::initializer_list<int> a){} int main { fun({1, 2, 3, 4}) }
注意:该方法只能传递类型相同的变长参数;
2、可变长度模板参数
3、使用省略号表示形式参数(一般不使用)
-
函数的缺省实参注意点:
1、如果某个形参具有缺省参数,那么它右侧的形参都必须具有缺省实参;
void fun(int x=1, int y=2){}// 这里y必须给定缺省值
2、具有缺省实参的函数调用时,传入实参按照从左到右的顺序进行匹配;
3、在一个翻译单元中,每个形参的缺省实参只能定义一次;
4、缺省实参为对象时,实参的缺省值会随对象值的变化而变化;
-
main函数的版本
1、无形参版本(一般使用)
2、带形参版本
int main(int argc, char *argv[]) {}
argc是非负数,表述传入参数个数,argv是一个指针指向传输参数的数组头。
返回类型
-
返回类型的几种书写方式:
1、经典方法:位于函数头的前部,也是最常规的写法;
2、C++11引入的方式:位于函数头的尾部;
auto fun(int x) -> int { return x*2; }
3、C++14引入的方式:返回类型的自动推导;
auto fun(int a) { return a;// 会根据return语句进行推导 }
函数重载与解析
函数重载:使用相同的函数名定义多个函数,每个函数具有不同的参数列表;
注意:不能基于不同的返回类型进行重载;
名称查找:
- 分为限定查找和非限定查找:有无限定在某个作用域中;
- 非限定查找会进行域的逐级查找——名称隐藏;
- 查找通常只会在已声明的名称集合中进行;
重载解析:在名称查找的基础上进一步选择合适的调用函数;
- 过滤不能被调用的版本:参数个数不对、无法将实参转为形参、实参不满足形参的限制条件;
内联函数
定义:将比较简单的函数逻辑展开到调用函数的部分,避免栈帧销毁,提升性能;
关键字:inline,如果一个函数在多个翻译单元展开,加入这个关键字可以避免重复定义;
constexpr函数
定义:之前有介绍常量表达式时用到了该关键字,现在对于函数也可以用该关键字;
作用:使得函数在编译器被执行,当然在有变量情况下也可在运行期执行;
constexpr int fun(int x){
// int y; std::cin >> y;会报错,该语句需要用户传入参数,只能在运行期执行
return x * 2;
}
int main
{
constexpr int x = fun(2);// 编译器会翻译成 move eax 4, 去掉constexpr也可以
return x;
}
注意:constexpr函数中的语句必须是可以在编译器执行的语句;
拓展:关键字consteval(C++20引入),函数只能在编译器执行;
函数指针
作用:可以用于高阶函数中,将函数指针作为参数;
代码案例:
int add(x) { return x + 1};
using T = int(int);
int fun(K* F, int x)
{
int tmp = (*F)(x);
return tmp * 2;
}
int main
{
std::cout << fun(&add, 50) << std::endl;
}
说明:这就是用函数指针定义的一个高阶函数,在之后的很多高阶函数、泛型算法中也是这样的用法;
注意:当函数对象进行赋值或者返回值时,返回的是一个函数指针类型的对象;
思考
1、我们常常会见到如下代码,是由什么作用?
extern "C"
int fun(int x, int y)
{
return x + y;
}
C语言对于函数是不能重载的,当用C调用C++程序时,往往找不到C++编译后的函数名,可通过如上代码定义一个函数为C类型函数;
2、可以用别名定义一个函数类型吗?
using X = int[3];
X a;// 这是定义了一个数组,同int a[3]
using X = int(int);
X fun;// 这是定义了一个int返回类型的函数
函数也是有类型的,可以用别名定义,并且函数类型不包含形参名称,并且只能声明,不能定义;
总结
本篇主要介绍了函数的基础概念以及一些特殊的函数方法和类型。重点需要注意的就是函数重载以及函数指针,这个在后续的模板以及泛型编程都会用到。
【C++学习】——(九)内存
内存基础
一、内存分布
通过下面一张图看看C++的内存分布:
-
栈区:由编译器自动分配与释放,存放为程序运行时函数分配的局部变量、函数参数;栈内存分配运算内置于处理器的指令集中,效率很高,但是分配内存的容量有限;
-
堆区:由new、malloc分配的内存块,释放由应用程序控制,不需要编译器释放;如果程序员没有对该内存进行释放,程序结束后系统自动回收,堆的地方比栈大很多;
-
静态区:存放的是static的静态变量和一些全局变量,特点是只读、大小固定;静态变量和全局变量的存储期是一起的,一旦静态区的内存被分配,要一直等到程序全部结束后才释放;
二、栈区与堆区的区别
1、分配方式不同:栈区系统分配系统回收;堆区由程序员手动申请,需要求程序员自行回收,如果没有回收,系统在程序结束后会进行回收,这种情况会造成内存泄漏;
2、生命周期不同:栈区生命周期是系统分配到系统回收,也就是在大括号内;堆区是从申请到释放;
3、效率不同:主要原因是地址空间是否连续,栈区地址空间是连续的,效率会高一些;堆区地址空间不连续,需要遍历链表才能找到最近的地址,效率会低一些;
4、内存碎片:堆区容易产生内存碎片,栈区不会;
5、生长方向不同:栈区申请空间的地址(表示地址的八个十六进制数)是从大到小的,堆区申请空间地址是从小到大的。栈区是先进后出的原则,类比栈结构的特点;
栈区特点:更好的局部性,对象自动销毁;
堆区特点:运行期动态扩展,需要显示释放;
注意点:申请的空间是在堆区,变量本身是在栈区!
内存分配
一、内存分配方式
可操作的内存分配:
- 静态存储区分配:static静态变量、全局变量;
- 栈上分配:局部变量;
- 堆上分配:new、malloc进行内存分配;
不可操作的内存分配:
内核区、代码区、局部变量的分配也属于系统分配;
二、new的用法
-
C++中通常使用new、delete来构造和销毁对象;
-
使用new创建对象,返回的是对象的首地址,需要用指针接收:
int *y = new int(2);
std::cout << *y << std::endl;
-
对象的构建和销毁分为两步:分配内存、所分配内存上构造对象(销毁与之类似);
-
new的几种常见形式:
1、new int(2):构造单一对象、new int[5]:构造数组;
2、nothrow new:标准库定义,解决内存分配失败异常的问题;
3、placement new:使用已经创建的内存,跳过分配内存;
4、new auto;
三、delete用法
- 根据分配的是单一对象还是数组,采用相应的方式销毁;
int *y = new int[3];
delete[] y;
- 不能delete一个非new返回的内存(也就是栈内存);
- delete nullptr是可被允许的;
- 同一块内存不能delete多次;
四、new与malloc的区别
new不需要指定分配多大,malloc使用的时候必须指定大小;new的底层实现就是malloc,两者都必须释放内存,不否则容易造成野指针或内存泄漏。需要注意一点,释放内存后需设置相关指针为空指针;
总结:
1、属性:new为关键字(编译器),malloc是库函数(需引入头文件);
2、参数:new无需指定大小,malloc需指定大小;
3、返回类型:new返回对象指针,malloc返回void*;
4、对于自定义的类:new会调用构造和析构函数,malloc不会调用构造和析构函数;
5、分配失败:new会抛出异常,malloc会返回空;
五、内存泄漏
是指由于疏忽或错误造成程序未能释放掉不再使用的内存的情况,内存泄漏并非指内存在物理上的小时,而是应用程序分配某段内存后,由于设计错误,失去该段内存的控制从而造成内存浪费;
可能的原因:
1、申请后未释放(最常见)
2、void* 指针的释放
3、new[] 回收时没有用delete[] ,数组的回收要注意
内存拓展
内存概念:
计算机重要部件之一,是外存与CPU进行沟通的桥梁。计算机所有程序都是在内存运行的,因此内存的性能对计算机的影响非常大。内存也称为内存储器和主存储器,作用是暂时存放CPU的运算数据,以及与硬盘等外部存储器交换的数据;
寻址空间:保存内存地址的多少,通常我们说的4G内存,就表示计算机能保存2的32次方个地址,也就是能找到这些地址上的二进制信息;
寻址能力:每个地址里能存多少个bit,现在的计算机大多数是16位机器了;
虚拟内存:
使得系统运行实际的内存空间比想象的大得多,虚拟内存是可以远大于物理内存的,同时主要为了使程序运行的时候可以不限制于只访问内存大小,可以通过虚拟内存地址去访问磁盘空间;
每一个进程虚拟内存都是独立的,独立的享有计算机的内存。虚拟内存地址的大小是与地址总线位数相关,物理内存地址的大小是与物理内存条的容量与磁盘容量相关。
思考
1、代码中的b属于栈区还是堆区?
void fun()
{
int *b = new int[14];
}
b是在栈区的变量,由于b是一个局部变量,随着函数域的结束被释放,不需要程序员自行释放,尽管b使用new进行初始化,还是可以认为分配在栈区;
总结
本次系统的从内存的基础概念到内存分配进行了讲解,内存是我们开发中最重要的一部分,往往逻辑上的错误就会造成内存泄漏,导致程序无法运行。或者一些分配内存的方式不够细心,也会造成冗余内存的使用。在目前的很多嵌入式板子上,针对内存的接口是必备的,往往也都是基于malloc修改;
还有一点需要注意,不管任何机器上运行程序,操作的都是虚拟内存,内部通过页表定位到对应的物理内存。关于硬件方面的本质,如果做嵌入式端的话需要深入研究。
【C++学习】——(十)嵌入式内存管理
开篇
上一篇介绍了软件层面上的内存,并没有涉及很多底层的原理;但在实际工程中,部署一个项目往往需要考虑内存的占用,这里的内存也就是嵌入式板子上的内存;本篇文章就简单介绍一下嵌入式端的一个内存管理;
Linux内核系统结构
主要分为五大模块:
本次主要讲解内存管理模块,其他模块不做介绍;
查看Linux内存
在Linux环境下,可通过free -m查看内存使用情况;
下图是一台rk3326机器的内存情况:
- Mem:表示物理内存统计;
- total:表示物理内存总量(used + free);
- used:表示总计分配给缓存(包含buffers与cache)使用的内存数量,但其中部分缓存并未实际使用;
- free:未被分配的内存;
- shared:内存共享;
- buffers:系统分配但未被使用的buffers数量;
- cached:系统分配但未被使用的cache数量;
- -/+ buffers/cache:表示物理内存的缓存统计;
- Swap:表示硬盘上交换分区的使用情况;
cache
cache的作用不同于buffer,它的速度极快,当进行底层优化的时,可能要编写基于cache的内存管理程序;它是直接与CPU交互的,不用走DDR;
思考以下哪种循环效率高:
// 第一种循环
int arr[10][100];
for (i = 0;, i < 10; i++)
for (j = 0; j < 100; j++)
arr[i][j] = 8;
// 第二种循环
for (i = 0; i <100; i++)
for (j = 0; j < 10; j++)
arr[j][i] = 8;
从硬件层面来看,第二种的效率最高,因为内存的跳转相对少了很多,所以我们需要注意在嵌套循环中,尽量把大的循环写在内层;
buffer
buffer是缓冲区,作用是开辟一块地址空间,可以将程序需要用到的内存空间先开辟好,有了buffer可以避免在快速读写时候的问题;
cache和buffer的一个区别:
- cache:把读取过来的数据保存起来,重新读取时若命中,则不需要再去硬盘读取;其中的数据会根据读取频率进行筛选,把频繁读取的数据放在最容易找到的位置,把不在读取的数据往后排,直到删除,这也是LRU缓存算法的原理;
- buffer:是根据磁盘的读写设计的,把分散的写操作集中进行,减少磁盘碎片和硬盘的反复寻道,从而提高系统性能;
内存补齐
在很多嵌入式板子上都有内存对齐的处理;
思考下以下结构占用的内存:
struct A{
char a;// 1
char b;// 1
int c;// 4
}
根据CPU的分配机制,在64位机器上占用8个字节,这也是做了一些对齐处理;
不仅仅是内存,一些板子(例如昇腾310)会对图像数据进行对齐,图像的分辨率要满足硬件支持的倍数,这样才能做到高效处理;
总结
本篇只是对上一篇内存的一个补充,主要讲解Linux中的内存;这部分对于一些端侧部署的伙伴来说比较重要,推荐针对不同的板子,还是需要先阅读API文档,了解关于内存的API后再进行代码的开发;
【C++学习】——(十一)线程
开篇
多线程是开发中必不可少的,往往我们需要多个任务并行,就需要多线程开发;就好比图像检测和图像结果的处理,这就是一个可闭环的任务,用多线程是可以加速这个任务的;
线程的状态
- 就绪态:线程能够运行,正在等待处理机资源;
- 运行态:正在运行,可能有多个线程处于运行态;
- 阻塞态:线程由于等待某些条件而无法运行,例如IO、锁、互斥量等;
- 终止态:线程从起始函数返回或被取消;
多线程的构建
有三种方式可以构建多线程,前提是都需要引入pthread.h这个头文件;
1、函数;
2、仿函数;
3、Lambda表达式;
三者的本质都是在调用函数;
// 函数方式
void fun(string s){
cout<< &s<<endl;
cout<< "first thread programm"<<s<<endl;
}
int main(){
string s = "Hell world";
thread th = thread(fun, s);
th.join();
}
-
上面代码为最简单线程的一个构造;
-
join函数是一个等待线程完成函数,主线程需要等待子线程运行结束才可以结束;还有一个detach的函数,会让线程在后台运行,需要等到程序退出才结束;
计算时间
计算时间在这里介绍两种方式:
一、程序运行时间
long n =0;
clock_t start,finish;
start=clock();
while(n<1000000000)
n++;
finish=clock();
printf("spend time %f s \n", (double)(finish-start)/CLOCKS_PER_SEC);
printf("spend time %f ms \n", (double)(finish-start)/1000);
- 这种方式和系统时间无关,一般用来调试时打印时间;
二、chrono
#include <chrono>
//方式三 chrono
std::chrono::system_clock::time_point Cstart = std::chrono::system_clock::now(); //系统时间
// std::chrono::steady_clock::time_point Cstart = std::chrono::steady_clock::now(); //稳定时间
long n =0 ;
while(n<1000000000)n++;
std::chrono::system_clock::time_point Cend = std::chrono::system_clock::now(); //系统时间
std::chrono::duration<float> spend_time = Cend-Cstart;
cout<<spend_time.count()<<endl;
- 这个方式用系统时间进行计算,在实际程序中用这个方式;
共享资源和互斥锁
-
关于互斥锁的概念,引用这篇博主的讲解:文章
-
引入互斥锁原因:当有两个线程共享一块资源时,容易造成冲突,也就是上个线程还没结束就进行下个线程,举个例子就是读写操作,添加互斥锁可以很好的解决这个冲突问题;
-
互斥锁是个简单的加锁方法,互斥锁只有两种状态:上锁(lock)和解锁(unlock);
-
互斥锁特点:
1、原子性:把一个互斥量锁定为一个原子操作,这意味着如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量;
2、唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;
3、非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。
互斥锁的使用:
mutex mtx; //创建互斥锁对象
mtx.lock();
g_pcm_elapseds.push_back(std::make_pair(pcm_data, elapsed));// 执行语句
mtx.unlock();
condition_variable
- condition_variable条件变量可以阻塞(wait)调用的线程直到使用(notify_one或notify_all)通知恢复为止
使用案例:
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_thread_id(int id){
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck,[]{return ready;});
std::cout<< "thread"<<id <<endl;
}
void go(){
std::unique_lock<std::mutex> lck(mtx);
ready = true;
cv.notify_all(); // 唤醒所有线程
};
int main(){
std::thread threads[10];
for(int i=0;i<10;i++){
threads[i] = std::thread(print_thread_id,i);
}
std::cout<< " thread read all done"<<endl;
go();
for(auto &th:threads) th.join();
return 0;
}
线程池
作用:每一个任务都起一个线程,这样的效率是不高的,起一个线程池,哪个线程空闲就来处理任务,这样的结构高效;
实现思想:管理一个任务队列,一个线程队列,然后每次取一个任务队列分配给一个线程去做,循环反复;
这里参考一个Github:地址
其中的ThreadPool.h头文件写的很好,可以直接使用;
关于多线程与线程池的概念,这里推荐:https://www.cnblogs.com/haippy/p/3284540.html
写的特别具体,实战代码也很多,大家可以参考参考;
总结
线程这部分涉及的知识点比较多,实现起来细节也多。本篇先对其中的概念部分进行总结,实战代码部分可参考我提供的文章进行学习。后续有精力会更新在线程的实战,想要掌握线程还是需要从实战中学习。
- C++中除了面向对象的编程思想外,还有另一种就是泛型编程
- 主要用到的技术就是模板
模板机制的分类:
- 函数模板
- 类模板
C++面向对象的编程
一、引入
实现程序的目的------模拟现实
C语言:struct只有属性没有行为
struct Person
{
const char* _name;
int _sex;
int _age;
void (*eat)(struct Person* per);
void (*run)(struct Person* per);
void (*sleep)(struct Person* per);
void (*show)(struct Person* per);
};//28个字节
void eat(struct Person* per)
{
cout << per->_name << " eat eat eat ······" << endl;
}
void run(struct Person* per)
{
cout << per->_name << " run run run ······" << endl;
}
void sleep(struct Person* per)
{
cout << per->_name << " sleep sleep sleep ······" << endl;
}
void show(struct Person* per)
{
cout << "name:" << per->_name << endl;
cout << "sex:" << per->_sex << endl;
cout << "age:" << per->_age << endl;
}
int main()
{
struct Person per;
per.eat = eat;
per.run = run;
per.sleep = sleep;
per.show = show;
per._name = (char*)malloc(strlen("zhangsan"+1));
// per._name = new char[strlen("zhangsan" + 1)];
strcpy_s(per._name, strlen("zhangsan") + 1, "shangsan");
per._sex = 1;
per._age = 23;
per.show(&per);
per.eat(&per);
per.sleep(&per);
per.run(&per);
struct Person per1;
per1._name = "lisi";
return 0;
}
设想:
1.能否成员方法不占用空间
2.成员方法不用传递参数具体是哪个成员
3.对象死亡时候能否自动清理内存
4.对象生成能够更简单
二、C++面向对象
在类中定义成员方法,不占用内存;
不传参数,自己会识别那个对象调用。
1.构造函数,拷贝函数,运算符重载,析构函数
(1)构造函数
- 构造函数没有返回值;
- 构造函数函数名和类名相同;
- 构造函数在对象生成的时候会自动调用;
- 如果没有实现构造函数,系统会自己生成一个没有参数的构造函数,这个构造函数什么都不做;
- 构造函数可以有多个,多个之间存在重载关系。
(2)拷贝函数
- 拷贝构造函数是在用已存在的对象构造新生成的对象的时候自动调用;
- 如果没有自己实现拷贝构造函数,系统会自动生成一个拷贝构造函数,只进行浅拷贝,什么都不做。
注意:防止浅拷贝;一定要传引用。
浅拷贝:_name=src._name
深拷贝:_name=new char[strlen(src.name)+1]
strcpy_s(name,strlen(src.name)+1,src._name)
传引用:Person(Person& src){}
(3)等号运算符重载
- 等号运算符是用已存在的对象给已存在的对象进行赋值的过程自动调用的函数;
- 如果没有自己实现的等号运算符重载函数系统会默认生成一个浅拷贝的等号运算符重载函数动态调用。
注意:防止自赋值;防止内存泄漏;防止浅拷贝
(4)析构函数
- 在对象死亡的时候,自动调用的函数;
- 如果没有自己实现的析构函数,系统会默认生成一个空的析构函数;
- 先构造的后析构。
注意: 防止内存泄漏
class Person
{
public:
char* _name;
int _sex;
int _age;
/*
系统默认C++在类中普通成员方法第一个参数是类的指针,不可以手动写
类的普通成员方法的形参列表第一个参数是this指针,默认不感知,不可以手动写
类的普通成员方法使用的成员前面默认加上this->,不感知,也可以手动写
*/
//构造函数
Person(/*Person *const this*/const char* name, int sex, int age)
{
_name = new char[strlen(name) + 1];
strcpy_s(_name, strlen(name) + 1, name);
_sex = sex;
_age = age;
}
//没有参数的构造函数称为默认构造函数
Person()
{
cout << "Person()" << endl;
}
//拷贝构造函数
//要传&不能直接传对象,如果没有&,相当于Person src=per直接拷贝过程,从而进入死递归;
Person(Person& src)
{
_name = new char[strlen(src._name) + 1];
strcpy_s(_name, strlen(src._name) + 1, src._name);
_age = src._age;
cout << "Person(Person& src)" << endl;
}
//等号运算符重载
Person& operator=(const Person& src)
{
cout << "Person& operator=(const Person& src)" << endl;
//防止自赋值
if (&src == this)
{
return *this;
}
//防止内存泄漏
delete[]_name;
//防止浅拷贝
_name = new char[strlen(src._name) + 1];
strcpy_s(_name, strlen(src._name) + 1, src._name);
_sex = src._sex;
_age = src._age;
return *this;
}
//析构函数
~Person()
{
if (NULL != _name)
{
delete[]_name;
_name = NULL;
cout << "~Person()" << endl;
}
}
void eat()
{
cout << _name << " eat eat eat ······" << endl;
}
void run()
{
cout << _name << " run run run ······" << endl;
}
void sleep()
{
cout << _name << " sleep sleep sleep ······" << endl;
}
void show()
{
cout << "name:" << _name << endl;
cout << "sex:" << _sex << endl;
cout << "age:" << _age << endl;
}
};
int main()
{
//生成对象的过程叫做构造,自动调用构造函数
Person per(/*&per*/"zhangsan",32,1);
//Person per1;//ok
//Person per1();//error,编译器不知道是函数声明还是构造对象
//用一个已经存在的对象构造一个正在生成的对象
//叫做拷贝构造
Person per1(per);//等价于Person per1=per
Person per2 = per;
//per.show(/*&per*/);
//per.eat(/*&per*/);
//per.run(/*&per*/);
//per.sleep(/*&per*/);
//cout << "==========================" << endl;
//per1.show();
//per._name[4] = '8';
//per.show();
//per1.show();
//用已存在的对象给已存在的对象赋值
per1 = per = per2;//赋值
//per1.show();
per = per;
}
小结:
c++中面向对象,系统自动做的事情:
1.自动调用构造函数
2.自动调用拷贝构造
3.自动调用等号运算符重载
4.自动调用析构函数
5.如果自动调用的函数不存在,就会默认生成该函数
6.普通成员方法的形参列表在第一个参数的位置默认加上this指针
7.在普通成员方法调用的地方,实参列表第一个默认加上this指针
8.在普通成员方法中使用到普通成员的地方默认加上this->
2.面向对象
抽象 封装
面向对象的三大特征:封装、继承、多态
面向对象的四大特征:抽象、封装、继承、多态
(1)访问权限
public:公有的
所有人都可以访问
一般对外提供的接口使用public
private:私有的
只有类内部的成员方法可读可写
类外不可见不可写
除了必须对外提供的接口,其他的都写成私有
在class类中所有的成员默认的属性是私有-private;
在c++中,struct也是类,在struct类中所有成员的默认属性是公有的。
如何选择用class和struct:
如果简单的类,成员方法不多的一般用struct;class一般用于较大一点,功能较多的类。
除了必须对外提供的接口,其他的都写成私有;
对于外界需要访问成员属性也写成私有,在公有的权限中提供访问接口
(2)类中的static关键字
- 成员属性加上static代表整个类只有这一份;
- 静态成员属性必须在类外初始化(因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的);
- 私有的static成员属性依然可以在外界进行初始化,但是私有的成员除了初始化,在外界其他地方不可以访问;
- 私有成员的访问不必依赖对象,可以直接使用 类名::
- 静态成员方法:使用不依赖于对象;访问可以直接使用 类名::;静态成员方法没有传入this指针,所以,静态成员方法内不可以访问普通成员
(3)const
const 对象
常对象只能调用普通常方法和静态方法
常方法指的是在方法参数列表后括号后面加上const,这个const修饰的是this指针
建议:所有的普通成员方法如果方法内没有修改成员就将该方法写成常方法
const 成员属性
const成员属性必须放在初始化列表进行初始化
(4)初始化列表
只有构造函数有初始化列表;
const成员属性必须放在初始化列表;
引用成员属性必须放在初始化列表;
必须初始化成员必须放在初始化列表;
位置:在构造函数的参数列表下面,函数体的前{上面
为什么引用必须初始化?
因为编译时会在使用引用的地方全部替换成指针的解引用,所以引用在初始化后没有办法访问到引用本身,所以引用一经指向没有办法改变,必须初始化.
(5)单例模式
将构造函数写在私有的里面;
在共有的里面写一个静态的获取对象指针的方法;
将唯一的对象指针存储在私有的静态成员属性中,每次被获取。
class Stu
{
private:
char* _name;
const int _sex;
int& _school;
int _age;
double _grade;
static int _num;
static Stu* p_stu;
//单例模式
Stu(const char* name, int sex, int school, int age, double grade)
:_sex(sex), _school(school)
{
_name = new char[strlen(name) + 1];
strcpy_s(_name, strlen(name) + 1, name);
//_sex = sex;
_age = age;
_grade = grade;
cout << "stu(const char* name, int sex, int age, double grade)" << endl;
}
public:
//单例模式接口
static Stu* get_stu(const char* name, int sex, int school, int age, double grade)
{
if (NULL == p_stu)
{
p_stu = new Stu(name, sex, school, age, grade);
}
return p_stu;
}
static int get_num()
{
get_age();
return _num;
}
//静态成员方法内无法访问普通成员
static int get_age()
{
cout << "hahah" << endl;
//show();//error
//return this->_age;//error
}
Stu(int school)
:_sex(0),_age(int()),_school(school)
{
_name = NULL;
// _sex = 0;
//_age = 0;//普通的也可以写到初始化列表,但const必须写
_grade = 0;
cout << "stu()" << endl;
}
/*
Stu(const char* name, int sex, int school, int age, double grade)
:_sex(sex),_school(school)
{
_name = new char[strlen(name) + 1];
strcpy_s(_name, strlen(name) + 1, name);
//_sex = sex;
_age = age;
_grade = grade;
cout << "stu(const char* name, int sex, int age, double grade)" << endl;
}
*/
Stu(const Stu& src)
:_sex(src._sex),_school(src._school)
{
_name = new char[strlen(src._name) + 1];
strcpy_s(_name, strlen(src._name) + 1, src._name);
//_sex = src._sex;
_age = src._age;
_grade = src._grade;
cout << "stu(const stu& src)" << endl;
}
Stu& operator=(const Stu& src)
{
cout << "stu& operator=(const stu& src)" << endl;
if (&src == this)
{
return *this;
}
if(NULL!=_name)
{
delete[]_name;
}
_name = new char[strlen(src._name) + 1];
strcpy_s(_name, strlen(src._name) + 1, src._name);
_sex = src._sex;
_age = src._age;
_grade = src._grade;
return *this;
}
~Stu()
{
if (NULL != _name)
{
delete[]_name;
_name = NULL;
}
cout << "~stu()" << endl;
}
void show(/*Stu* const this*/)const
{
//this = NULL;//error,this不让变
cout << "name:" << _name << endl;
cout << "sex:" << _sex << endl;
cout << "age:" << _age << endl;
cout << "grade:" << _grade << endl;
}
//如果外界需要访问,提供接口。通过成员方法修改属性,确保安全性
void change_grade(int flage,double grade)
{
if (flage == 1)
{
_grade = grade;
}
}
double get_grade()
{
return _grade;
}
};
#if 0
int main()
{
Stu stu;
Stu stu1("aaa", 12, 1, 45);
stu= stu1;
Stu stu2 = stu;
stu.show();
stu1.show();
stu2.show();
cout << stu._age << endl;
stu._age = 10;
return 0;
}
#endif
#if
int Stu::_num = 0;
Stu* Stu::p_stu = NULL;
int main()
{
/*
//cout << Stu::_num << endl;
int n = Stu::get_num();
Stu stu;
stu.show();
*/
/*
const Stu stu("zhangsan", 1, 23, 5);
//const Stu* p
stu.show();//Stu* const this//把常量的地址给了一个非常量的指针,将方法改成:void show()const
*/
Stu* stu = Stu::get_stu("zhangsan", 2, 1, 23, 5);
Stu* stu2 = Stu::get_stu("zhangsan", 2, 1, 23, 5);
return 0;
}
#endif // 1
3.通过链表实现单例模式的链栈
mlist.h
#ifndef LIST_H
#define LIST_H
#include<iostream>
using namespace std;
struct Node
{
int _val;
Node* _next;
Node(int val=int())
{
_val = val;
_next = NULL;
}
};
class List
{
public:
List()
{
_head = new Node();
_tail = _head;
}
//传参必须传引用,防止死递归
List(const List& src)
{
_head = new Node();
_tail = _head;
Node* tmp = src._head->_next;
while (NULL != tmp)
{
insert_tail(tmp->_val);
tmp = tmp->_next;
}
}
List& operator=(const List& src)
{
if (&src == this)
{
return *this;
}
while(!is_empty())
{
delete_head();
}
Node* tmp = src._head->_next;
while (NULL != tmp)
{
insert_tail(tmp->_val);
tmp = tmp->_next;
}
return *this;
}
~List()
{
while (!is_empty())
{
delete_head();
}
delete _head;
_head = NULL;
_tail = NULL;
}
bool is_empty()
{
return _head == _tail;
}
void insert_head(int val)
{
Node* p = new Node(val);
p->_next = _head->_next;
_head->_next = p;
if (p->_next == NULL)
{
_tail = p;
}
}
void insert_tail(int val)
{
Node* p = new Node(val);
p->_next = NULL;
_tail->_next = p;
_tail = p;
}
int delete_head()
{
if (is_empty())
{
return 0;
}
Node* p = _head->_next;
_head->_next = p->_next;
if (_tail == p)
{
_tail = _head;
}
delete p;
return 1;
}
int delete_tail()
{
if (is_empty())
{
return 0;
}
Node* tmp = _head;
while (tmp->_next != _tail)
{
tmp = tmp->_next;
}
delete tmp->_next;
tmp->_next = NULL;
_tail = tmp;
return 1;
}
int search(const int val)
{
Node* p = _head->_next;
while (NULL != p && val != p->_val)
{
p = p->_next;
}
if (NULL == p)
{
return 0;
}
else
{
return 1;
}
}
void show()
{
Node* p = _head->_next;
while (NULL != p)
{
cout << p->_val << " ";
p = p->_next;
}
cout << endl;
}
int get_first()
{
if (is_empty())
{
return -1;
}
return _head->_next->_val;
}
int get_last()
{
if (is_empty())
{
return -1;
}
return _tail->_val;
}
private:
Node* _head;
Node* _tail;
};
#endif
mstack.h
#ifndef MASTACK_H
#define MSTACK_H
#include"mlist.h"
#include<iostream>
using namespace std;
class Stack
{
public:
static Stack* get_stack()
{
if (NULL == _p_stack)
{
_p_stack = new Stack();
}
return _p_stack;
}
void pop();
void push(int val);
int top();
bool is_empty();
void show();
private:
//构造函数写到私有的里面,外界没有办法构造
Stack();
static Stack* _p_stack;
List _list;
};
#endif
mstack.cpp
#include<iostream>
#include"mlist.h"
#include"mstack.h"
//类外进行类内静态成员变量初始化
Stack* Stack::_p_stack = NULL;
Stack::Stack()
{
}
void Stack::pop()
{
if (is_empty())
{
return;
}
_list.delete_head();
}
void Stack::push(int val)
{
_list.insert_head(val);
}
int Stack::top()
{
return _list.get_first();
}
bool Stack::is_empty()
{
return _list.is_empty();
}
void Stack::show()
{
_list.show();
}
main.cpp
#include<iostream>
#include "mlist.h"
#include"mstack.h"
using namespace std;
#if 0
int main()
{
List list;
for (int i = 0; i < 10; i++)
{
list.insert_head(i);
}
list.show();
for (int i = 0; i < 10; i++)
{
list.insert_tail(i);
}
list.show();
int a = list.get_first();
cout << a << endl;
list.delete_head();
list.show();
int b = list.get_last();
cout << b << endl;
list.delete_tail();
list.show();
return 0;
}
#endif // 0
#if 1
int main()
{
//Stack sta;
Stack* sta = Stack::get_stack();
for (int i = 0; i < 5; i++)
{
sta->push(i);
}
sta->show();
cout << sta->top()<<endl;
sta->pop();
sta->show();
return 0;
}
#endif // 1
4.访问私有成员的两种方式
(1)在public提供私有成员的接口;
(2)设置类或者函数友元(不建议,会暴露私有成员)
类与类之间的关系
组合类:一个类是另一个类的一个组成部分
代理类:一个类的成员是另一个类成员的子集
类的编译程序:先编译类名,再编译成员名,再编译成员方法体
对象的生成顺序:先生成成员对象,再生成自身对象
对象析构顺序:先析构自身,再析构成员
成员对象如果没有默认的构造函数,则该成员对象的初始化必须手动放在初始化列表;如果成员对象有默认的构造函数,系统会自动再初始化列表加上该对象的默认构造。
5.
class Tmp
{
public:
Tmp()
{
cout <<"Tmp() "<< endl;
}
Tmp(int a)
{
cout << "Tmp(int a) " << endl;
}
Tmp(const Tmp& src)
{
cout << "Tmp(const Tmp & src)" << endl;
}
Tmp& operator= (const Tmp & src)
{
cout << "Tmp& operator= (const Tmp & src)" << endl;
return *this;
}
~Tmp()
{
cout<< "~Tmp()" << endl;
}
};
(1)
int main()
{
Tmp t3;
t3 = 20;
return 0;
}
(2)
int main()
{
Tmp t7 = 30;
return 0;
}
先构造临时对象,使用临时对象拷贝构造,析构临时对象
-------->会直接优化为构造目标对象
(3)
int main()
{
const Tmp& tt = 40;//必须加const,否则会泄露常量的地址给非常量的指针
cout << "===============" << endl;
return 0;
}
使用40构造一个临时对象;把临时对象的引用给到tt
如果对临时对象进行引用,该临时对象的生命周期会扩大到和引用一致
(4)
class Tmp
{
public:
Tmp()
{
cout <<"Tmp() "<< endl;
}
Tmp(int a)
{
_a = a;
cout << "Tmp(int a) " << endl;
}
Tmp(const Tmp& src)
{
cout << "Tmp(const Tmp & src)" << endl;
}
Tmp& operator= (const Tmp & src)
{
cout << "Tmp& operator= (const Tmp & src)" << endl;
return *this;
}
~Tmp()
{
cout<< "~Tmp()" << endl;
}
int _a;
};
const Tmp& fun()
{
return 10;
}
int main()
{
const Tmp& tt = fun();
cout << endl;
cout << tt._a << endl;
return 0;
}
先根据10构造临时对象,该临时对象分配在fun()栈帧上,fun()结束后,临时对象的空间也被销毁(销毁指的是标记为未用空间),引用t成为了野指针。所以最后tt._a是随机值。
禁止返回临时对象||局部对象的引用或者指针
(5)
class Tmp
{
public:
Tmp()
{
cout <<"Tmp() "<< endl;
}
Tmp(int a)
{
_a = a;
cout << "Tmp(int a) " << endl;
}
Tmp(const Tmp& src)
{
cout << "Tmp(const Tmp & src)" << endl;
}
Tmp& operator= (const Tmp & src)
{
cout << "Tmp& operator= (const Tmp & src)" << endl;
return *this;
}
~Tmp()
{
cout<< "~Tmp()" << endl;
}
int _a;
};
Tmp fun()
{
Tmp t(10);//构造t
return t;//用t拷贝构造临时对象;析构t
}
int main()
{
Tmp t1;
//构造t1
t1 = fun();
/*
使用 临时对象等号运算符 赋值给t1
析构临时对象
*/
return 0;
}
调用的方法:
(6)加上传参
class Tmp
{
public:
Tmp()
{
cout <<"Tmp() "<< endl;
}
Tmp(int a)
{
_a = a;
cout << "Tmp(int a) " << endl;
}
Tmp(const Tmp& src)
{
cout << "Tmp(const Tmp & src)" << endl;
}
Tmp& operator= (const Tmp & src)
{
cout << "Tmp& operator= (const Tmp & src)" << endl;
return *this;
}
~Tmp()
{
cout<< "~Tmp()" << endl;
}
int _a;
};
Tmp fun(const Tmp tt)
{
Tmp t(10);
/*
用t拷贝构造临时对象
*/
return t;
/*
析构t
析构tt
*/
}
int main()
{
Tmp t1;
Tmp t2;
//构造t1
t2 = fun(t1);//使用t1拷贝构造tt
return 0;
}
太复杂需优化。
建议:
- 函数传递参数,能传引用就传引用,节省空间时间。如果确定函数中不会修改参数的值,就是用const引用
- 如果要返回的数据是一个新产生的对象,可以返回临时量就返回临时量,并使用返回的临时量直接构造新的对象
优化结果:
(7)堆区申请对象
int main()
{
Tmp* tp = new Tmp();
return 0;
}
没有析构,进程结束才会释放空间,要手动析构delete tp
函数模板
作用:建立一个通用函数,其函数返回值类型和形参类型可以不具体定制,用虚拟的类型来表示
关键字:template
语法:
template <typename T> // template <class T>
函数声明或者定义
- template:声明创建模板
- typename:表明后面的符号是一种数据类型,也可以用class代替
- T:通用的数据类型,名称可以替换,通常为大写字母
意义:将类型参数化,提高代码通用性。
使用方法:
- 自动类型推导
- 显示指定类型
注意事项:
- 自动类型推导,无论有几个 T,其必须要推导出一致的数据类型,才可以使用
- 函数模板必须要确定 T 的数据类型,才可以使用
与普通函数的区别:
- 普通函数调用时可以发生自动类型转换,也就是隐式转换
- 函数模板只有在调用显示指定类型才会发生隐式转换,用自动类型推导就不会
示例:
void SwapDouble(double &a, double &b) //这是一个普通的两个double类型数值交换的函数,缺点是数据类型被固定死了
{
double temp = a;
a = b;
b = temp;
}
template<typename T> //声明一个函数模板,T是通用的数据类型
void mySwap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
template<class T>
void func()
{
cout << "注意事项" << endl;
}
int main()
{
int a = 20;
int b = 10;
char c = '5';
//1、自动推导类型,又编译器自动推导出数据类型
mySwap(a , b);
//mySwap(a , c); //×,因为 T 的数据类型不一致,一个是int型,一个是char型
// 同时自动推导类型不会发生隐式转换,无法将int变char型,也无法将char变int型
//2、显示指定类型,直接告诉编译器这两个传参的数据类型
mySwap<int>(a , b)
//func(); //×,因为 T 没有一个确定的数据类型
func<int>(); //√,因为我们给 T 指定了一个确定的数据类型
// 同时显示指定类型可以发生隐式转换,强制转换为int型
}
调用规则
- 如果普通函数和函数模板都实现了,优先使用普通函数
- 可以通过空模板参数列表强制调用函数模板
- 函数模板可以以发生函数重载
- 如果函数模板可以更好的匹配,那肯定是优先使用函数模板啦
示例:
void Print(int a , int b)
{
cout << "普通函数" << endl;
}
template<class T>
void Print(T a , T b)
{
cout << "函数模板" << endl;
}
template<class T>
void Print(T a , T b , T c)
{
cout << "函数模板" << endl;
}
int main()
{
int a = 10;
int b = 5;
char c1 = 'a';
char c2 = 'b';
Print(a , b); //1、普通函数和函数模板你都一样实现的情况下,优先调用普通函数
Print<>(a , b); //2、利用空模板参数列表来强制调用函数模板
Print<>(a , b , c); //3、函数模板可以发生函数重载
Print(c1 , c2); //4、函数模板可以更好的匹配,优先调用函数模板
//这是编译器所做的决定,它觉得直接将类型变成通用类型 比 将char转换成int更加方便
}
函数模板的局限性
一些特定的数据类型,函数模板就处理不了
示例:
template<class T>
void func(T a , T b) //如果a 和 b 都是数组类型,那赋值操作就运行不了
{ //如果 T 是某一个类自定义数据类型,也处理不了
a = b;
}
- 为了解决这些问题,C++提供了模板的重载,为特定的数据类型提供具体化的模板
- 利用具体化的模板,可以解决类型自定义的通用性
- 但是模板其实不用去写,可以在STL中调用系统提供的模板
语法:
template<> 返回值类型 函数名(参数1,参数2,···)
示例:
class Person
{
public:
Person(string name ,int age)
{
this ->m_name = name;
this ->m_age = age;
}
string m_name;
int m_age;
};
template<class T>
bool Compare(T &a , T &b) //只能对比一些普通的变量,对于自定义的数据类型,无法处理
{
if(a == b)
return ture;
else
return flase;
}
//template<> 表示这是个具体化Person实现的函数模板
template<> bool Compare(Person &a , Person &b)
{
if(a.m_name == b.m_name && a.m_age == b.m_age)
return ture;
else
return flase;
}
int main()
{
Person p1("张三",20);
Person P2("李四",30);
if(Conpare(p1 , p2)) //只要参数是Person类型,编译器会自动调用具体化Person类型的函数模板
cout << "完全一致" << endl;
else
cout << "对比失败" << endl;
}
类模板
作用:建立一个通用的类,但是类中的成员变量类型未知,用一个虚拟的类型来表示
语法:
- template:声明创建一个模板
- typename:表明后面的 T 是一种数据类型,typename 也可以用class替代
- T:通用的数据类型
与函数模板的区别:
- 类模板在使用的时候,没有自动类型推导的方式
- 类模板在模板参数列表中可以有默认参数类型
示例:
//temolate<class Nametype, class AgeType = int> //类模板的参数列表可以说有默认参数类型
template<class NameType, class AgeType> //因为会有涉及到两个不同类型的变量,所以创建了两个虚拟的类型
class Person
{
public:
Person(NameType name, AgeType age)
{
this->m_name = name;
this->m_age = age;
}
NameType m_name;
AgeType m_age;
}
int main()
{
Person<string ,int> p1("张三",99); //既然指定了string类型,记得添加string头文件
// Person("李四",56); //×,类模板没有自动类型推导的方式
// Person<string> p2("王五",66); //√,只要在前面有模板参数列表的声明中默认参数就可以用
}
类模板成员函数创建时机
- 普通类中的成员函数,在一可以就可以创建
- 类模板中的成员函数,在函数调用时才创建
示例:
class Person1
{
public:
void showPerson1(){ cout << "show Person1" << endl; }
}
class Person2
{
public:
void showPerson2() { cout << "show Person2" << endl; }
}
template<class T>
class myClass
{
public:
T obj;
void func1(){ obj.showPerson1(); } //因为是类模板,函数此时其实还没有创建,所以随便用别人家的函数也没事
void func2(){ obj.showPerson2(); }
}
int main()
{
myClass<Person1> p1; // 将通用类型指定为 Person1 类型,创建了类 p1
p1.func1(); //此时才是创建成员函数 func1() 的时候,调用的时候才进行声明
//p1.func2(); //因为指定的是 Person1 类,所以只有 Person1类中的 showPerson1()函数被声明了
//Person2 类中的 showPerson2() 并没有被声明,说明不能调用
myClass<Person2> p2;
//p2.func1();
p2.func2();
}
类模板对象做函数参数
作用:用模板实例出的对象来当做函数参数传入
传入方式:
- 指定传入的类型:直接显示对象的数据类型
- 参数模板化:将对象中的参数变为模板进行传递
- 整个类模板化:将整个对象进行模板化,传递下去
一般而言第一种传递方式比较常见;
第二种和第三种,其实有点像函数模板和类模板结合使用;
使用函数 typeid(temp) 可获知一个参数 temp 的类型;
示例:
template<class T1, class T2>
class Person
{
Person(T1 name, T2 age)
{
this->m_name = name;
this->m_age = age;
}
showPerson()
{
cout << "名字:" << this->m_name <<"年龄: " << this->m_age << endl;
}
T1 m_name;
T2 m_age;
}
void func1( Person<string, int>&p ) //1、指定传入类型
{
p.showPerson();
}
template<class T1, class T2> //要加上这个,不然编译器不知道 T1 和 T2 是什么
void func2( Person<T1, T2>&p ); //2、参数模块化,T1 和 T2 由编译器根据传参去推导
{
p.showPerson();
cout << T1 的数据类型 << typeid(T1).name << endl; //使用typeid()可获知 T1 / T2 的数据类型
cout << T2 的数据类型 << typeid(T2).name << endl;
}
template<classs T>
void func3(T &p) //3、整个类进行模板化,T 由编译器根据传参去推导
{
p.showPerson();
cout << "T 的数据类型 " << typeid(T).name << endl; //获取 T 的数据类型
}
int main()
{
Person<string , int> p("张三", 50);
func1(p); //1、指定传入类型
func2(p); //2、参数模板化
func3(p); //3、整个类进行模板化
}
类模板与继承
当类模板碰到继承时:
- 当子类继承的父类是一个类模板时,子类在声明时要指定父类 T 的类型
- 如果不指定,编译器就无法给子类分配内存
- 要想灵活指定父类中 T 的类型,子类也必须为类模板
语法:
template<class T1 , class T2>
class 子类:继承方式 父类<T2>
{
//T1 通用类型留给子类自己用
}
示例:
template<class T>
class Base
{
T m;
}
// class Son : public Base //×,父类是类模板,继承中需要指定父类中 T 的类型
class Son1 : public Base <int> //将父类中的数据类型指定为 int 型,这样继承才可以成功
{
}
template<class T1, class T2>
class Son2:public Base<T2> //这样 类模板的 Son2 的参数 T2 就变成了父类的 T 的类型,达到灵活指定父类 T类型的目的
{
T1 m;
}
int main ()
{
Son1 s1; //创建子类对象s1的时候,父类中的 T 类型就被强制指定成 int 型
Son2 <char , int>s2; //创建子类对象s2的时候,父类中的 T 类型就随着子类的定义而改变,此时 父类中 T 便是 int 型
}
类模板成员函数的类外实现
类外实现的方法:
- 类内声明,类外写函数定义
- 类外写函数时,作用域需要加上模板参数列表
语法:
template<class T>
返回值类型 作用域<T>::成员函数() //作用域要加上模板参数列表<T>,不管传参和函数内部有没有用到 T ,<T> 都要写出来
{
}
示例:
template <class T1, class T2>
class Person
{
public:
//Person(T1 name , T2 age) //这是成员函数之构造函数的类内实现
// {
// this->m_name = name;
// this->m_age = age;
// }
// void PrintPerson() //这是成员函数之普通函数的类内实现
// {
// cout << "姓名:" << this->m_name << "年龄" << this->m_age << endl;
// }
Person(T1 name, T2 age); //类外实现之类内声明
void PrintPerson();
T1 m_name;
T2 m_age;
}
template<class T1, class T2>
Person<T1, T2>::Person(T1 name ,T2 age)
{
this->m_name = name;
this->m_age = age;
}
template<class T1, class T2>
void Person<T1 , T2>::PrintPerson()
{
cout << "姓名:" << this->m_name << "年龄" << this->m_age << endl;
}
int main()
{
Person<string ,int> p("张三" , 50);
p.PrintPerson();
}
分文件编写
在使用类模板过程中,进行分文件编写会出现一些问题:
一般的分文件编写:
.h:写类声明和成员函数定义
.cpp:写类中成员函数的实现
x.cpp:写主函数和应用功能
原因:类模板的成员函数在主函数调用的时候才创建,所以会导致预处理时链接不到
解决办法(建议用第二种):
- 直接包含.cpp源文件(将x.cpp中的#include".h">改成#include".cpp",而.cpp中也有包含头文件,这样改,就是在跑主函数之前,将声明和实现都跑了一遍)
- 将声明和实现写在同一个文件中,后缀名改成.hpp(.hpp是约定俗成的名字,不是强制的,但大家都这么写)
两种方法,目的都是在跑主函数之前,将声明和实现都先跑了一遍。
类模板与友元
类模板和友元配合成的全局函数的类内实现和类外实现:
- 类内实现:直接在类内声明友元即可(建议这个,比较简单)
- 类外实现:需要提前让编译器知道全局函数的存在(有点复杂,不建议)
示例:
template<class T1, class T2> //类外实现——这部分最好写在最前面,提前让编译器知道这个Person类的存在
class Person;
template<class T1, class T2> //类外实现——这部分实现最好写在前面,提前让编译器知道这个函数的存在
void PrintPerson2( Person<T1, T2>p)
{
cout << "姓名:" << p.m_name << "年龄" << p.m_age << endl;
}
template<class T1, class T2>
class person
{
friend void PrintPerson1( Person<T1,T2> p) //全局函数,友元函数的类内实现,这个比较简单
{
cout << "姓名:" << p.m_name << "年龄" << p.m_age << endl;
}
friend void PrintPerson2<>( Person<T1, T2> p); //全局函数,友元函数的类外实现,需要先类内声明,类外实现
//加一个空模板参数列表,将这个普通函数变成类模板函数
public:
Person(T1 name, T2 age)
{
this->m_name = name;
this->m_age = age;
}
private:
T1 m_name;
T2 m_age;
}
int main()
{
Person<string, int> p("张三", 50);
PrintPerson1(p); //类内实现
PrintPerson2(p); //类外实现
}
STL
- 之所以有C++的面向对象和泛型编程,目的就是提升可重复性
- STL的诞生,就是为了建立一套数据结构和算法的标准,提高可重复利用性
STL基本概念
- STL,即标准模板库
- 从广义上分为:容器、算法、迭代器
- 从细分上分为:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器
- 容器和算法通过迭代器进行无缝连接
- STL所有的代码都运用到了函数模板和类模板
STL六大组件
六大组件就是上线说到的:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器
- 容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据
- 算法:各种常用算法,如sort、find、copy、for_each等
- 迭代器:容器和算法之间的链接器
- 仿函数:类似函数,可作为算法的某种策略
- 适配器:一种用来修饰容器或者仿函数或者迭代器的东西
- 空间配置器:负责空间的分配和管理
容器
作用:存放数据,将广泛使用的数据结构给实现出来
常用的数据结构:数组、链表、树、栈、队列、集合、映射表等
容器的分类:
- 序列式容器:强调值的位置,每个元素都有固定的位置
- 关联式容器:二叉树结构,元素之间没有严格的物理上的顺序关系
算法
作用:问题的解法,即用有限的步骤,解决逻辑上的难题
算法的分类:
- 质变算法:运算中更改区间内元素的内容,如拷贝、替换、删除等
- 非质变算法:运算中不会更改区间内元素的内容,如查找、计数、遍历、寻值等
迭代器
作用:容器和算法之间的链接器,即就是提供一种方法,既能依序寻找容器内某个元素,又不暴露容器内部的表示方法
- 每个容器都有自己专属的迭代器
- 迭代器的使用方法类似于指针
- 最常用的是双向迭代器和随机访问迭代器
迭代器的种类:
种类 | 功能 | 支持运算 |
---|---|---|
输入迭代器 | 对数据的只读访问 | 只读,支持++、==、!= |
输出迭代器 | 对数据的只写访问 | 只写,支持++ |
前向迭代器 | 对数据的向前操作 | 读写,支持++、==、!= |
双向迭代器 | 对数据的向前和向后操作 | 读写,支持++、– |
随机访问迭代器 | 访问任何数据,功能最强 | 读写,支持++、–、[n]、-n、<、<=、>、>= |
String容器
基本概念
- string是C++风格的字符串,但是本质上是一个类
- String是内部封装了char_类型,管理string这个类,_用的是char*的容器
- 包含头文件#include
特点
- string内部封装了很多成员函数方法,比如find查找,copy复制,delete删除,replace替换,insert插入等等
- string管理char*所分配出来的内存,不用担心复制越界内存和取值越界,也不用担心溢出或者碎片,这些由容器内部统一管理
string构造函数
- string();无参构造,主要用来创建一个空字符串
- string(const char* s); 用来初始化一个字符串
- string(const string& str); 用一个string对象来初始化另一个string对象
- string(int n,char c); 初始化n个字符c
- 以上构造函数没有什么可比性,根据实际情况灵活运用即可
示例:
#include<string> //只要涉及到string,就要包含其头文件
int main()
{
string s1; //1、用于创建一个空的字符串
const char* str = "中秋节";
string s2(str); //2、创建一个字符串s2,将字符串s1初始化赋值给字符串s2
cout << "str2 = " << s2 << endl;
string s3(s2); // 调用拷贝构函数
cout << "str3 = " << s3 << endl;
string s4(10,'A'); //3、创建一个字符串4,初始化赋值为10个‘A’
cout << "str4 = " << s4 << endl;
}
赋值操作
作用:给string字符串赋值
函数原型:
- string& operator= ( const char *s ) ; 将char *类型的字符串赋值给当前字符串
- string& operator= ( const string &s ) ; 将string类型字符串赋值给当前字符串
- string& operator= ( char c ) ; 将字符c赋值给当前字符串
- string& assign ( const char*s ) ; 将char *类型字符串赋值给当前字符串
- string& assign ( const char *s, int n ) ; 将char *类型的字符串的前n个字符赋值给当前字符串
- string& assign ( const string &s ) ; 将const类型的字符串赋值给当前字符串
- string& assign ( int n,char s ) ; 将n个字符s赋值给当前字符串
示例:
int main()
{
string s1;
s1 = "hello C++"; //相当于operator=("hello C++"),由编译器进行内部转化
cout << "s1 = " << s1 << endl;
string s2;
s2 = s1; //相当于operator=(s1),由编译器进行内部转化
cout << "s2 = " << s2 << endl;
string s3;
s3 = 'A'; //相当于operator=('A'),由编译器进行内部转化
cout << "s3 = " << s3 << endl;
string s4;
s4.assign("Hello C++"); //也就是 string& assign ( const char *s)
cout << "s4 = " << s4 << endl;
string s5;
s5.assign(s4); //也就是 string& assign ( const string &s )
cout << "s5 = " << s5 << endl;
string s6;
s6.assign("hello C++",5); //也就是 string& assign ( const char *s,int n)
cout << "s6 = " << s6 << endl;
string s7;
s7.assign(10,'A'); //也就是 string& assign ( int n, char s)
cout << "s7 = " << s7 << endl;
}
字符串拼接
作用:就是在字符串末尾再拼接一段字符串
函数原型:
- string& operator+ ( const char *c ) ; //将char*类型的字符串拼接到当前字符串末尾
- string& operator+ ( const char c ) ; //将字符c拼接到当前字符串末尾
- string& operator+ ( const string& str ) ; //将string类型的字符串拼接到当前字符串末尾
- string& append ( const char *s ) ; //将char*类型的字符串拼接到当前字符串末尾
- string& append ( const char *s , int n ); //将char*类型字符串的前n个字符拼接到当前字符串末尾
- string& append ( const string &s ) ; // 将string类型额字符串拼接到当前字符串末尾
- string& append ( const string &s, int pos , int n ); //将string类型的字符串从pos个开始的n个字符拼接到当前字符串末尾
示例:
int main()
{
string s1 = "I";
s1 += "love games"; //相当于 string& operator+ ( const char *c ),由编译器内部自动转化
cout << "s1 = " << s1 <<endl;
s1 += ';'; //相当于 string& operator+ ( const char c ),由编译器内部自动转化
cout << "s1 = " << s1 <<endl;
string s2 = "LOL";
s1 += s2; //相当于 string& operator+ ( const string& str ),由编译器内部自动转化
cout << "s1 = " << s1 <<endl;
string s3 = "I";
s3.append(" love "); //相当于 string& append ( const char *s ) ;
s3.append("game abcsd ", 4); //相当于 string& append ( const char *s ,int n) ;
cout << "s3 = " << s3 <<endl;
s3.append(s2); //相当于 string& append ( const string& s ) ;
cout << "s3 = " << s3 <<endl;
string s4 = "sjdia DNF";
s3.append(s4, 5, 3); //相当于 string& append ( const string &s, int pos , int n );
cout << "s3 = " << s3 <<endl;
}
字符串的查找和替换
作用:
- 查找:查找指定的字符串是否存在
- 替换:在指定的位置替换字符串
函数原型:
- int find ( const string& str , int pos = 0 ) const ; //查找str第一次出现的位置,从pos开始查找
- int find ( const char* s , int pos = 0 ) const ; //查找 s 第一次出现的位置,从pos开始查找
- int find ( const char* s ,int pos , int n ) const ; //查找s的前n个字符第一次出现的位置,从pos开始查找
- int find ( const char c , int pos = 0 ) const ; //查找字符串 c 第一次出现的位置,从pos开始查找
- int rfind ( const string& str , int pos = npos ) const; //查找str最后一次的位置,从pos开始查找
- int rfind ( const char* s , int pos = npos ) const ; //查找 s 最后一次出现的位置,从pos开始查找
- int rfind ( const char* s , int pos , int n) const; //查找 s 的前n个字符最后一次的位置,从pos开始查找
- int rfind ( const char s , int pos = 0 ) const ; //查找字符 s 最后一次出现的位置,从pos开始查找
- string& replace ( int pos , int n , const string& str) ; //从pos开始的n个字符,替换成字符串str
- string& replace ( int pos , int n , const char* s); //从pos开始的n个字符,替换成字符串 s
特点:
- find是从左往右找,rfind是从右往左找,找到了返回下标位置,找不到返回 -1
- replace会把替换字符串全部替换进去,不管你指定多少个字符
示例:
int main()
{
string s1 = "sdafaffgg";
int ret = s1.find("fa"); //查找"fa"第一次出现的位置
if( ret == -1 )
return 0;
ret = s1.rfind ("fa"); //查找"fa"最后一次出现的的位置
if( ret == 1)
return 0;
s1.replace(2, 3, "AAAAA"); //将字符串s1的第2个开始的3个字符开始,全部替换成“AAAAA”
}
字符串比较
作用:字符串之间的比较
特点:
- 以其中的字符的ASCII值得方式进行比较
- > 返回 1
- < 返回 -1
- = 返回 0
- 主要是用来比较两个字符串是否一样,比较大小没有什么意义
函数原型:
- int compare ( const string& s) const ; //与string类型的字符串进行比较
- int compare ( const char s ) const ; //与char*类型的字符串进行比较
示例:
int main()
{
string s1 = "dafwfw" ;
string s2 = "dasff" ;
if ( s1.compare( s2 ) == 0)
{
//主要是用来比较是否相等
}
else if (s1.compare(s2) > 0)
{
}
else
{
}
}
字符存取
对单个字符进行存取有两种方法:
- 通过 [ ] 方式进行存取
- 通过at函数方式进行存取
函数原型:
- char& operator[](int n) ; //通过[]方式存取
- char& at(int n); //通过at方式存取
示例:
int main()
{
string s1 = "hello C++";
for(int i = 0 ; i < s1.size(); i++)
{
cout << s1[i] << endl; //通过 [] 方式读取
cout << s1.at(i) << endl; //通过 at 方式读取
}
str[0] = 'H'; //通过[]方式修改
str.at(1) = 'E'; //通过at方式修改
cout << s1 << endl ;
}
字符的插入和删除
作用:对字符串中进行插入和删除字符的操作
函数原型:
- string& insert ( int pos , const char* _s ) ; //_从pos位置开始插入字符
- string& insert ( int pos , const string& s); //从pos位置开始插入字符
- string& insert ( int pos , int n, char s); //从pos位置开始,插入n个字符s
- string& erase( int pos , int n = npos ) ; //从pos位置开始,删除n个字符
- 参数pos的位置,都是从下标0开始的
示例:
int main()
{
string s1 = "hello" ;
s1.insert(1 , "aaaa"); //从下标1位置开始,插入字符串“aaaa”
cout << s1 << endl;
s1.erase( 1, 4 ); //从下标1位置开始,删除4个字符
cout << s1 << endl;
system("pause");
return 0;
}
字符串子串
作用:从字符串中截取想要的子串
函数原型:
- string substr( int pos , int n = npos) const ; //从pos位置开始,截取n个字符
示例:
int main()
{
string s1 = "safagrgwg";
string substr = s1.substr( 0 , 3 ); //从s1字符串中,从位置0开始,截取3个字符作为子串
cout << substr << end;
system("pause");
rerurn ;
}
Vector容器
基本概念
- vector数据结构与数组非常相似,但是只能进行尾部的插入和删除,所以也被称为单端数组
- vector容器的迭代器是支持随机访问的迭代器,功能最强大的迭代器之一
- 包含头文件 #include
与普通数组的区别
- 数组是静态空间
- vector可以动态扩展
- 动态扩展并不是在原空间上续接新空间,而是寻找更大的空间,将原数据拷贝过去,在新空间后续接空间,释放源空间
vector容器结构以及成员函数
构造函数
作用:创建vector容器
函数原型:
- vector v ; //默认构造函数,T 是数据类型 , v是容器名字
- vector ( v.begin() , v.end() ) ; //将v.[ begin() , end() ) 区间中的元素拷贝给本身
- vector ( n , a ) ; //构造函数将 n 个 a 数值拷贝给本身
- vector ( const vector &ver ) ; //拷贝构造函数
示例:
int main()
{
vector <int> v1; //创建方法一:默认构造,无参构造
for ( int i = 0; i< 10; i++)
{
v1.push_back(i); //将0~9这些参数填入容器中
}
vector<int> v2 ( v1,begin(), v1.end()); //创建方法二:将v1容器中的数据拷贝给容器v2
vector<int> v3( 10, 100 ); //创建方法三:将10个100填入容器v3
vector<int> v4( v3 ); //创建方法四:将v3容器数据拷贝给容器v4
system("pause");
return 0;
}
vector赋值
作用:给vector容器进行赋值操作
函数原型:
- vector& operator= ( const vector &vst ) ; //重载方式进行赋值
- assign ( begin , end ) ; //将 [ begin , end )区间内的数值赋值给本身
- assign (n , A) ; //将n个字符A赋值给本身
示例:
int main()
{
vector<int> v1;
for(int i = 0 ; i < 10; i++)
{
v1.push_back(i);
}
vector<int> v2; //赋值方法一:重载赋值
v2 = v1;
vector<int> v3;
v3.assign(v1,begin() , v1.end() ); //赋值方式二:将v1的区间[begin ,end) 数据赋值给v3
vector<int> v4;
v4.assign(10 , 200); //赋值方式三,将10个200赋值给v4
}
容量和大小
作用:获取vector容器的容量和大小参数
函数原型:
- empty ( ) ; //判断是否为空
- capacity ( ) ; //获取容器的容量
- size ( ) ; //获取元素个数
- resize ( int num ) ; //重新指定容器长度,容器变长,超出的用0表示;容器变短,超出的被删除
- resize ( int num,int elem ) ; //重新指定容器元素,容器变长,超出的用elem表示,变短则超出的被删除
示例:
void PrintVector(vector<int>& v) //利用迭代器写出的打印容器函数
{
for(vector<int>::iterator i = v.begin();i < v.end(); i++)
{
cout << *i << " " ;
}
cout << endl;
}
int main()
{
vector<int> v;
for( int i = 0;i < 10; i++)
v.push_back(i);
PrintVector(v);
if( v.empty() ) //判断容器是否为空
{
cout << "容器v为空" << endl;
}
else
{
cout << " 容器v不为空" << endl;
cout << "容器v的容量为:" << v.capacity() << endl; //获取容器的容量
cout << "容器v的大小为:" << v.size() << endl; //获取容器的元素个数
}
v.resize( 15 , 100); //重新指定容器长度
//如果超出容器本身长度,超出的用100表示
//如果比容器本身小,多余的会被删除
PrintVector(v);
v.resize ( 5 ); //重新指定容器长度
//如果超出容器本身长度,超出的默认0表示
//如果比容器本身小,多余的会被删除
PrintVector(v);
}
插入和删除
作用:在容器中插入元素和删除元素
函数原型:
- push_back ( ) ; //尾部插入
- pop_back ( ) ; //删除尾部最后一个元素
- insert ( const_iterator pos , elem ) ; //插入元素elem,第一个参数是迭代器指向位置
- insert (const_iterator pos , int num , elem ) ; //插入num个elem数值,第一个参数是迭代器指向位置
- erase ( const_iterator pos ); //删除元素,第一个参数是迭代器指向位置
- erase ( const_iterator begin , const_iterator end ); //删除从头到尾的元素
- clear ( ); //删除容器内全部元素
示例:
void PrintVector( vector<int>& v) //一个打印函数,用于打印容器内的数据
{
for( vector<int>::iterator i = v.begin(); i<v.end(); i++)
{
cout << "*i" << " " ;
}
cout << endl;
}
int main()
{
vector<int> v;
for(int i=0;i<10;i++)
v.push_back( i ); //尾部插入0~9元素
PrintVector(v);
v.pop_back(); //删除最后一个元素
PrintVector(v);
v.insert(v.begin() , 100); //在迭代器begin位置插入元素100
PrintVector(v);
v.insert( v.begin() , 3, 100 ); //在迭代器begin位置插入3个元素100
PrintVector(v);
v.erase( v.begin() ); //产出迭代器begin位置的元素
PrintVector(v);
v.erase( v.begin() , v.end() ); //产出迭代器从begin到end之间的元素
PrintVector(v);
v.clear(); //删除容器内全部元素
PrintVector(v);
system("pause");
return 0;
}
数据存取
作用:获取容器中的数据
函数原型:
- at ( int idx ) ; //返回索引下的元素
- operator[ ] ; //重载方式返回索引下的元素
- front ( ) ; //获取容器第一个元素
- back ( ) ; //获取容器中最后一个元素
示例:
int main()
{
vector<int> v;
for(int i = 0; i< 10; i++)
{
v.push_back ( i ) ; //开始填充容器元素
}
for(i = 0; i< 10; i++)
cout << v[i] << " "; //以重载方式operator[]获取容器元素
cout << endl;
for(i = 0;i < 10; i++)
cout << v.at(i) << " "; //以at()方式获取容器元素
cout << endl;
cout << "容器v的的第一个元素:" << v.front() << endl; //获取第一个元素v.front()
cout << "容器v的最后一个元素:" << v.back() << endl; //获取最后一个元素v.back()
}
容器互换
作用1:实现两个容器之间的元素互换
作用2:用于收缩容器容量(原理就是创建一个小容量的容器与本身的大容量容器互换)
函数原型:
- swap( vec ) ; //将容器vec中的元素与本身互换
示例:
void PrintVector( vector<int>& v )
{
for(vectot<int>::iterator it = v.begin(); it < v.end(); it++)
{
cout << *it << " ";
}
cout << endl;
}
int main()
{
//==========================作用1:容器元素互换========================================//
vector<int> v1;
for(int i = 0 ; i< 10 ; i++)
v1.push_back(i);
vector<int> v2;
for(int i = 10; i > 0; i--)
v2.push_back(i);
PrintVector(v1); //元素互换之前
PrintVector(v2);
v1.swap ( v2 ) ;
PrintVector(v1); //元素互换之后
PrintVector(v2);
//==============================作用2:收缩容器内存============================================//
vector<int> v;
for(int i = 0; i < 10000; i++) //创建一个容器,里面有10000个元素
v.push_back(i);
cout << "此时容器大小为:" << v.size() << endl; //此时大小 = 10000
cout << "此时容器的容量为:" << v.capacity() << endl; //此时容量 > 10000
v.resize(3); //重新指定容器大小 = 3
cout << "此时容器大小为:" << v.size() << endl; //此时大小 = 3
cout << "此时容器的容量为:" << v.capacity() << endl; //但是此时容量依旧是 > 10000,这就很浪费内存
vector<int> v.swap ( v ) ; //以v为拷贝,创建一个匿名对象x,此时x大小和容量 = 3
//将容器x 与 容器v进行互换
//容器x变成了容器v,此时x的大小 = 3,,容量 > 10000
//容器v就换成了容器x,此时v就变成大小和容量 = 3
//而容器x由于是匿名对象,执行完其内存就后就被系统释放掉了
cout << "此时容器的容量为:" << v.capacity() << endl; //此时容器v的容量 = 3
cout << "此时容器大小为:" << v.size() << endl; //此时容器的大小 = 3
}
预留内存
作用:在一开始就开辟出足够的空间,减少动态扩展的次数
原理:因为一旦容器输入数据量较大的时候,编译器会根据内存实际情况时不时进行动态扩展,动态扩展次数一旦多了,操作时间就长了,所以最好是在一开始就预留足够多的空间,将动态扩展次数维持为1次。
函数原型:
- reserve ( int len ) ; //预留len个元素长度,预留位置不初始化,也不可以访问
示例:
int main()
{
vector<int> v;
v.reserve(10000); //从一开始就预留10000个元素大小的内存
int num = 0;
int *p = NULL:
for(int i = 0; i< 10000; i++)
{
v.push_back( i );
if( p != &v[0]) //以 指针p实时跟踪容器首元素位置 来确定动态扩展次数
{
p = &v[0];
num ++;
}
}
cout << "动态扩展的次数:" << num << endl;
}
vector排序
- 作用:利用算法对容器内元素进行排序
- 需要包含算法头文件 #include
- 对于随时访问的迭代器都可以使用该排序函数
函数原型:
- sort( iterator begin , iterator end ) ; // 对begin和end之间的元素进行从小到大排序
示例:
void PrintVector( const vector<int>& dv)
{
for( vector<int>::const_iterator it = d.begin(); it != d.end(); it++ )
{
cout << *it << " ";
}
cout << endl;
}
int main()
{
vector<int> d;
d.push_back(10);
d.push_back(30);
d.push_back(20);
d.push_back(100);
d.push_back(50);
d.push_back(70);
Printvector(d); //排序前
sort( d.begin(), d.end() ); //进行排序
Printvector(d); //排序后
system("pause");
return 0;
}
deque容器
- 与数组类似,可以进行头部和尾部的插入和删除,因为也被称为双端容器
- deque容器的迭代器也是支持随机访问的
- 头文件#include
deque与vector的区别:
- vector容器只能尾部插入和删除,因为对于头部操作效率很低,数据量越大越低
- deque容器头部的插入和删除速度比vector快多了
- vector和deque内部结构不同,导致元素的速度vector会更快,导致deque没有容量限制,可以无限扩张
- 二者的迭代器都支持随机访问,是最强大的迭代器之一
deque内部结构和成员函数
内部工作原理
- deque内部有一个中控器,用于记录每段缓冲区的地址,缓冲区中放置真实数据
- 中控器+缓冲区,使得deque看起来像是一块连续的内存
deque内部工作原理
构造函数
作用:创建deque容器
函数原型:
- deque d ; //默认构造函数,T表示数据类型
- deque( begin , end ) ; //将容器[ begin , end )区间内的数据拷贝给本身
- deque( count , elem ) ; //将count个elem数值拷贝给本身
- deque ( const deque& d ) ; //拷贝构造函数,将容器数据整个拷贝给本身
示例:
void PrintDeque( const deque<int>& d ) //为了放置d容器被中间意外修改,最后增加const修饰
{
for(deque<int>::const_iterator i = d.begin(); i != d.end(); i++) //迭代器就得使用const修饰的迭代器
{
cout << *i << " ";
}
cout << endl;
}
int main()
{
deque<int> d1; //构造方法一:无参构造
for( int i = 0; i < 10 ; i++)
{
d1.push_back(i);
}
PrintDeque(d1);
deque<int> d2( d1.begin() , d1.end() ); //构造方法二:将d1的[begin() , end()) 区间之内的数据拷贝给容器d2
PrintDeque(d2);
deque<int> d3(10 , 200); //构造方式三:将10 个 200 拷贝给容器d3
PrintDeque(d3);
deque<int> d4 (d1); //构造方式四:将d1容器整个拷贝给d4
PrintDeque(d4);
system("pause");
return 0;
}
赋值操作
作用:给deque容器进行赋值
函数原型:
- deque& operator=( const deque& d) ; //重载方式进行赋值
- assign ( begin , end ); //将[ begin , end )区间内的数据赋值给容器
- assign ( count , elem ); //将count个elem数值赋值给容器
示例:
void PrintDeque ( const deque<int>& d )
{
for( deque<int>::const_iterator it = d.begin(); it != d.end(); it++ )
{
cout << *it << " " ;
}
cout << endl;
}
int main()
{
deque<int> d1;
for(int i = 0; i< 10; i++)
d1.push_back( i );
PrintDeque(d1);
deque<int> d2;
d2 = d1; //赋值方式一:重载方式
PrintDeque(d2);
deque<int> d3;
d3.assign( d1.begin() , d1.end() ); //赋值方式二:将d1的[ begin , end )之间数据赋值给d3
PrintDeque(d3);
deque<int> d4;
d4.assign( 10 , 200); //赋值方式三:将10个200赋值给容器d4
PrintDeque(d4);
system("pause");
return 0;
}
获取大小,没有容量限制
- 作用:获取deque内部的元素长度
- 由于中控器+缓冲区的内部结构,导致deque是没有容量限制的,理论上内存可以无限扩张,所以没有获取容量的函数
函数原型:
- empty( ) ; // 判断容器是否为空
- size( ) ; //获取元素个数
- resize ( num ) ; //重新指定容器长度,若容器变长,以默认值0填充新位置;若长度变短,超出部分删除
- resize ( num , elem ) ; //重新指定容器长度,若容器变长,以elem填充新位置,若变短,超出部分删除
示例:
void PrintfDeque(const deque<int>& d)
{
for( deque<int>::const_iterator i = d.begin(); i != d.end(); i++ )
{
cout << *i << " " ;
}
cout << endl;
}
int main()
{
deque<int> d;
for(int i = 0; i < 10 ; i++ )
d.push_back(i);
if ( d.empty() )
{
cout << "容器d为空:" << endl;
}
else
{
cout << "容器d不为空:" << endl;
PrintfDeque(d);
cout << "容器d的长度为:" << d.size() << endl;
}
d.resize( 5 );
PrintDeque(d);
d.resize(10 ,100);
PrintfDeque(d);
system("pause");
return 0;
}
插入和删除
作用:在容器两端进行插入和删除新数据
函数原型:
- push_back( elem); // 尾部插入数据elem
- push_front( elem); //头部插入数据elem
- pop_back( ); //删除最后一个元素
- pop_front( ); //删除第一个元素
- insert( pos ,elem); //在迭代器位置pos插入数据elem
- insert( pos , n, elem ); //在迭代器位置pos插入n个elem数据
- insert( pos, begin, end ); //在迭代器位置pos插入[begin , end)区间内的数据
- erase( pos); //删除迭代器位置pos的数据
- erase( begin , end ); //删除从begin到end之间的数据
- clear(); //删除容器内所有元素
示例:
void PrintDeque( const deque<int>& d )
{
for( deque<int>::const_iterator it = d.begin(); it != d.end(); it++)
cout << *it << " " ;
cout << endl;
}
int main()
{
deque<int> d1;
for(int i = 0; i< 10 ; i++)
d1.push_back(i); //容器尾部添加数据
PrintDeque(d1);
d.push_front(100); //容器头部插入100
d.pop_back(); //删除最后一个元素
d.pop_front(); //删除第一个元素
PrintDeque(d1);
deque<int> d2;
deqeu<int>::iterator i = d2.begin(); //用迭代器指定位置
d2.insert(i , 100 ); //在指定位置 i 插入100
d2.insert(i+1 , 2, 50 ); //在指定位置 i 插入2个50
PrintDeque(d2);
d2.insert(i , d1.begin(), d1.end() ); //在指定位置 i 插入d1的[begin , end )所有数据
PrintDeque(d2);
d2.earse(i); //删除指定位置的元素
d2.erase(d2.begin(), d2.end() ); //删除从begin到end之间的元素
d2.clear(); //删除容器内所有元素
PrintDeque(d2);
}
数据存取
作用:对deque容器中的数据进行存取
函数原型:
- operator[ ] ; //重载方式返回索引idx的数据
- at( int dix ) ; //at()返回索引idx的数据
- front( ) ; //返回第一个元素
- at( ) ; //at()返回索引idx的数据
示例:
void PrintDeque( const deque<int>& d )
{
for( deque<int>::const_iterator it = d.begin(); it != d.end(); it++)
cout << *it << " ";
cout << endl;
}
int main()
{
deque<int> d;
for(int i = 0;i< 10;i++ )
{
d.push_back(i);
}
for(i=0;i<10;i++)
cout << d[i] << " "; //重载方式获取容器元素
cout << endl;
for(i = 0;i<10;i++)
cout << d.at(i) << " " ; //at()方式获取容器元素
cout <<endl;
cout << "第一元素为:" << d.front() << endl; //获取第一个元素
cout << "最后一个元素为:" << d.back() << endl; //获取最后一个元素
system("pause");
return 0;
}
deque排序
- 作用:利用算法对容器内元素进行排序
- 需要包含算法头文件 #include
- 对于随时访问的迭代器都可以使用该排序函数,包括vector容器
函数原型:
- sort( iterator begin , iterator end ) ; // 对begin和end之间的元素进行从小到大排序
示例:
void PrintDeque( const deque<int>& d )
{
for( deque<int>::const_iterator it = d.begin(); it != d.end(); it++ )
{
cout << *it << " ";
}
cout << endl;
}
int main()
{
deque<int> d;
d.push_back(10);
d.push_back(30);
d.push_back(20);
d.push_back(100);
d.push_back(50);
d.push_back(70);
PrintDeque(d); //排序前
sort( d.begin(), d.end() ); //进行排序
PrintDeque(d); //排序后
system("pause");
return 0;
}
stack容器
- stack容器是一种前进后出的数据结构,又称为栈容器
- 该容器只有顶端的元素才可以给外界获取
- 该容器不允许有遍历行为
- 包含头文件 #include
stack容器内部结构以及成员函数
构造函数
函数原型:
- stack stk ; //默认构造函数
- stack( const stack &stk ) ; // 拷贝构造函数
赋值操作
函数原型:
- stack& operator=( const stack& stk ) ; //重载方式进行赋值
数据存取
函数原型:
- push( elem ) ; //向栈顶添加元素
- pop( ) ; //删除栈顶第一个元素
- top( ) ; //返回栈顶元素
大小获取操作
函数原型:
- empty( ) ; //判断容器是否为空
- size( ) ; // 返回容器的大小
示例:
int main()
{
stack<int> stk; //创建一个栈容器
for(int i = 0; i< 10; i++ )
{
stk.push(i); //往栈顶添加元素
}
while( !stk.empty() ) //判断栈是否为空
{
cout << "栈顶的元素为:" << stk.top() << endl; //返回栈顶元素
cour << "栈的长度为:" << stk.size() << endl; //返回栈的大小
stk.pop(); //删除栈顶元素
}
system("pause");
return 0;
}
queue容器
- queue容器是一种先进先出的数据结构,又称为队列容器
- 允许从一段添加元素,另一端删除元素
- 只有队尾和队头可以被外界使用,因此不允许有遍历行为
- 包含头文件 #include
queue内部结构和成员函数
构造函数
函数原型:
- queue que ; //默认构造函数
- queue( const queue& que ) ; //拷贝构造函数
赋值操作
函数原型:
- queue& operator=( const queue& que ) ; //重载方式赋值
数据存取
函数原型:
- push( elem ) ; //往队尾添加元素
- pop( ) ; //删除队头的元素
- back( ) ; //返回最后一个元素
- front( ) ; //返回第一个元素
大小获取
函数原型:
- empty( ) ; //判断队列是否为空
- size( ) ; // 判断队列的大小
示例;
class Person()
{
public;
Person(string name , int age)
{
this->m_name = name;
this->m_age = age;
}
string m_name;
int m_age;
}
int main()
{
queue<Person> q;
Person p1("张三" ,10 );
Person p2("李四" , 20 );
Person p1("王五" ,30 );
Person p1("赵六" ,40 );
q.push(p1);
q.push(p2);
q.push(p3);
q.push(p4);
while( !empty() )
{
cout << "第一元素:" << q.front() << endl;
cout << "最后一个元素:" << q.back() << endl;
cout << "容器的长度:" << q.size() << endl;
q.pop();
}
}
list容器
- list容器是一种双向循环的数据结构,又称为链式容器
- 内部结构并不是连续的内存,迭代器不能随访问,只能前移和后移,属于双向迭代器
- 包含头文件 #include
- 动态存储分配,不会存在内存浪费和溢出的问题
- 随机访问,插入和删除非常方便
- 占用空间比较大,遍历的消耗非常大
list容器内部结构以及成员函数
构造函数
作用:创建一个list容器
函数原型:
- list lst ; //默认构造函数
- list( begin , end ) ; //将区间 [ begin ,end)之间的元素拷贝给本身
- list( n , elem ) ; // 将 n个elem拷贝给本身
- list( const list &lst) ; // 拷贝构造函数
赋值与交换
作用:给list容器赋值,以及交换list容器
函数原型:
- list& operator=( const list& lst ) ; // 重载方式进行赋值
- assign( begin() , end() ) ; //将区间[ begin , end)之间的元素赋值给本身
- assign( n , elem ) ; //将 n 个 elem赋值给本身
- swap( lst ) ; // 将容器 与 本身的元素互换
示例:
void PrintList( const list<int>& L)
{
for( list<int>::const_iterator it = L.begin(); it!=L.end(); it++ )
{
cout << *it << " " ;
}
cout << endl;
}
int main()
{
list<int> L1; //创建方法一:默认构造函数L1
for( int i = 0 ; i<10; i++ )
L1.push_back( i );
PrintList(L1);
list<int> L2( L1.begin() , L1.end() ); //创建方法二:将L1区间[ begin , end )之间的数据拷贝给L2
PrintList(L2);
list<int> L3( 10 ,100 ); //创建方法三:将 10 个 100 拷贝给L3
PrintList(L3);
List<int> L4(L1); //创建方法四:将L1整个容器本身拷贝给L4
PrintList(L4);
List<int> L5;
L5 = L1; //赋值方式一:重载方式赋值
PrintList(L5);
List<int> L6;
L6.assign( L1.begin() , L1.end() ); //赋值方式二:将L1[begin ,end)之间的元素赋值给L6
PrintList(L6);
List<int> L7;
L7.assign( 10 , 200 ); //赋值方式三:将10 个 200 赋值给L7
PrintList(L7);
L7.swap( L6 ); //将容器L6 与容器 L7 进行元素交换
PrintList(L6);
PrintList(L7);
}
大小操作
作用:获取容器list的长度
函数原型:
- size( ) ; //返回元素个数
- empty( ) ; // 判断是否为空
- resize( num ) ; //重新指定容器长度,容器变长,以默认值0填充,容器变短,超出的元素被删除
- resize( num , elem ) ; //重新指定容器长度,容器变长,以数值elem填充,容器变短,超出的元素被删除
示例:
void PrintList( const list<int>& L)
{
for( list<int>::const_iterator it = L.begin(); it!=L.end(); it++ )
{
cout << *it << " " ;
}
cout << endl;
}
int main()
{
list<int> L1;
for( int i = 0 ; i<10; i++ )
L1.push_back( i );
PrintList(L1);
if( L1.empty()) // 判断容器是否为空
{
cout << "L1 为空:" << endl;
}
else
{
cout << "L1不为空:" << endl;
cout << "L1的元素个数为:" << L1.size() << endl; //判断容器的长度
}
L1.resize( 5 ); //重新指定容器大小为5,超出长度5的元素被删除
PrintList(L1);
L1.resize(10 , 10000); //重新指定容器大小为10,超出原有长度的位置用10000填充
PrintList(L1);
}
插入和删除
作用:对list容器进行元素插入和删除
函数原型:
- push_back( 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之间的元素
- erase( pos ); //删除迭代器pos位置的元素
- erase( begin , end ); //删除迭代器[ begin , end ) 之间的元素
- remove( elem ); //删除容器中与elem相同的所有元素
示例;
void PrintList( const list<int>& L)
{
for( list<int>::const_iterator it = L.begin(); it!=L.end(); it++ )
{
cout << *it << " " ;
}
cout << endl;
}
int main()
{
list<int> L;
for(int i = 0;i<10; i++)
{
L.push_back(i); //尾部插入元素
L.push_front(i); //头部插入元素
}
PrintList(L);
L.pop_front(); //删除第一个元素
L.pop_back(); //删除最后一个元素
PrintList(L);
list<int>::iterator it = L.begin();
L.insert( it , 1000); //在迭代器it位置插入1000
L.insert( it++ ,5, 1000); //在迭代器it++位置插入5个1000
PrintList(L);
L.remove( 10000 ); //移除容器中与1000相同的所有元素
PrintList(L);
list<int> L1;
L1.insert( L1.begin() , L.begin() , L.end() ); //将L[begin,end)之间的元素赋值给L1容器
PrintList( L1 );
L1.erase(L1.begin); //擦除迭代器位置的元素
L1.earse( L1.begin() , L1.end() ); //擦除容器从begin到end之间的元素
PrintList( L1 );
system("pause");
return 0;
}
数据存取
- 存取方式不是[]获取,也不是at()获取
- 因为list容器本质是链表,既不是连续的内存,迭代器也不支持随机访问
函数原型:
- front(); // 返回第一个元素
- back(); // 返回最后一个元素
示例:
void PrintList( const list<int>& L)
{
for( list<int>::const_iterator it = L.begin(); it!=L.end(); it++ )
{
cout << *it << " " ;
}
cout << endl;
}
int main()
{
list<int> L;
for(int i = 0;i<10; i++)
{
L.push_back(i); //尾部插入元素
}
PrintList(L);
cout << "第一个元素是:" << L.front() << endl;
cout << "最后一个元素是:" << L.end() << endl;
list<int>::iterator it = L.begin();
it++; //√,没报错,说明迭代器支持前移
it--; //√,没报错,说明迭代器支持后移
//it = it + 1 ; //×,编译器报错,说明迭代器不支持地址随机跳跃,it = it-1也一样不行
//it = it + 2 ; //×,编译器报错,说明迭代器不支持地址随机跳跃,it = it-2也一样不行
//it = it + x ; //×,编译器报错,说明迭代器不支持地址随机跳跃,it = it-x也一样不行
system("pause");
return 0;
}
反转和排序规则
作用:将容器中的元素反转,以及对元素进行排序
函数原型:
- reverse(); //容器元素进行反转
- sort(); //容器元素进行从小到大排序,没有迭代器参数,所以这个是内部成员函数,不是标准算法
- //对于定义数据类型,必须要指定排序规则(高级排序),否则编译器是不知道怎么排的
示例:
void PrintList( const list<int>& L)
{
for( list<int>::const_iterator::it = L.begin(); it!= L.end(); it++ )
{
cout << *it << " " ;
}
cout << endl;
}
bool mycompare(int v1 , int v2) //指定排序规则,提供降序方法
{
return v1 > v2 ; //指定规则:第一个数 > 第二个数,代表降序
}
int main()
{
list<int> L;
L.push_back(10);
L.push_back(30);
L.push_back(50);
L.push_back(80);
L.push_back(60);
PrintList(L); //反转前
L.reverse();
PrintList(L); //反转后
//sotr( L.begin(), L.end() ); //×,因为list迭代器不支持随机访问,所以不能使用标准算法库的函数
L.sort(); //√,list容器内部提供一个自己的排序算法,排序是从小到大
L.sort(myCompare); //√,提供一个降序排序,这是底层算法决定的,会用就行
system("pause");
return 0;
}
set/multiset容器
- 在set/multiset容器中,所有的元素都会在插入时自动排序,默认从小到大
- 两者属于关联式容器,内部结构是用二叉树实现的
- 两者的头文件都是 #include
set和multiset的区别:
- set容器不允许有重复的元素出现,即使强制插入重复元素,插入也会失败
- multiset容器可以有重复的元素出现
set的构造函数
作用:创建set容器
函数原型:
- set st ; // 默认构造函数
- set(const set& st) ; //拷贝构造函数
set的赋值函数
函数原型:
- set& operator( const set& st); // 函数重载实现赋值操作
set的插入和删除
函数原型:
- insert( elem ) ; // 插入只有insert()方式,不能指定位置,因为会自动排序
- erase(pos) ; // 删除迭代器pos位置的元素
- erase ( elem ); //删除数值为elem的元素
- erase( begin , end ); //删除[ begin,end)之间的元素
- clear(); //清除所有元素
set的大小和交换
- 作用:统计容器的大小以及交换set容器
- 没有重新指定容器大小的操作,因为会有默认值填充位置,而set容器中不能出现重复的元素
函数原型:
- size( ); //返回容器中元素个数
- empty( ); //判断容器是否为空
- swap( ); // 交换两个集合容器中的数据
set的查找和统计
作用:对set容器进行数据查找以及数据统计
函数原型:
- find( key ); //查找一个元素是否存在,存在则返回元素迭代器位置,不存在返回set.end()
- cout( key ); //统计key的元素个数,对于set而言不是0就是1,因为不允许有重复元素
示例:
void PrintSet( const set<int>& s)
{
for(set<int>::const_iterator it = s.begin();it!= s.end();it++)
{
cout << *it << " " ;
}
cout << endl;
}
int main()
{
set<int> s1; //默认构造函数创建set容器s1
s1.insert(10); //插入一些数据
s1.insert(50);
s1.insert(30);
s1.insert(60);
s1.insert(20);
PrintSet(s1);
set<int> s2(s1); //拷贝构造函数,创建容器s2
PrintSet(s2);
set<int> s3;
s3 = s1; //重载方式进行赋值
PrintSet(s3);
if( s1.empty())
{
cout << "容器s1为空" << endl;
}
else
{
cout << "容器s1不为空" << endl;
cout << "容器s1的个数:" << s1.size() << endl;
}
set<int> s4;
for(int i = 0; i<10;i++)
{
s4.insert(i); //插入一些元素
}
PrintSet(s4);
s4.swap(s1); //s4与s1容器进行交换
PrintfSet(s4);
s4.erase( s4.begin() ); //删除迭代器位置begin的元素
PrintSet(s4);
s4.erase( 5 ); // 删除数值为 5 的元素
PrintSet(s4);
erase(s4.begin() , s4.end()); // 删除在[begin,end)区间之间的元素
PrintSet(s4);
s4.clear(); //清除所有元素
PrintSet(s4);
set<int>::iterator pos = s1.find(30); //查找元素是否存在,存在则返回元素的迭代器位置
if( pos != s1.end() )
{
cout << "找到元素:" << *pos << endl;
}
else
{
cout << "没有找到元素" << endl;
}
cout << "统计元素个数" << s1.count(30) << endl; //统计元素30的个数
//对于set容器而言,不是0就是1,因为其不会出现重复的元素
}
set和multiset的区别
- set不可以插入重复数据,而multiset可以
- set插入数据会返回插入结果,同时也是这样检测是不是重复元素
- multiset插入数据不会检测,因此可以插入重复数据
set.insert()的函数原型:
- _Pairib insert( value_type&& _Val) ;
- using _Pairib = pair<inerator , bool>; //set.insert()的返回值类型是对组,返回迭代器位置和插入结果
multiset.insert()的函数原型:
- iterator insert(value_type&& _Val); //multiset.insert()返回值类型只有迭代器位置,没有检测结果
示例:
void PrintfSet( set<int>& s)
{
for( set<int>::iterator it = s.begin(); it != s.end(); it++)
{
cout << *it << " " ;
}
cout << endl;
}
int main()
{
set<int> s;
pair<set<int>::iterator, bool> ret = s.insert(10); //获取第一次插入数据的迭代器位置和插入结果
if(ret.second) //ret.second存放的就是插入结果
cout << "第一次插入成功" << endl;
else
cout << "第一次插入失败" << endl;
ret = s.insert(10); //获取第二次插入的迭代器位置和插入结果
if(ret.second)
cout << "第二次插入成功" << endl;
else
cout << "第二次插入失败" << endl; //因为是插入了相同的元素,所以这一次的插入失败
multiset<int> ms; //multiset容器是可以插入相同元素的
ms.insert(30);
ms.insert(30);
PrintSet(ms);
}
pair对组创建
作用:成对出现的数据,可以返回两个数据
创建方法的函数原型:
- pair<type , type> p (value1, value2 );
- pair<type , type> p = make_pair( value1, value2 );
示例:
int main()
{
pair<string , int> p1("Tom" , 20); // 第一种创建方式
cout << "姓名:" << p1.first << "年龄:" << p1.second << endl;
pair<string , int> p2 = make_pair("jerry" , 30); // 第二种创建方式
cout << "姓名:" << p2.first << "年龄:" << p2.second << endl;
system("pause");
return 0;
}
set容器排序
- 作用:set容器排序是默从小到大,但是可以可以通过仿函数来改变排序规则
- 要在插入数据之前就指定排序规则
- 如果是自定义数据类型,一定要指定排序规则
示例1:指定排序规则——内置数据类型
class Mycompare
{
public:
bool operator()( int val1,int val2)
{
return val1 > val2 ;
}
};
int main()
{
//set<int> s1; //这么写,那插入的数据肯定就是默认从下到大了
set<int , MyCompare> s1; // 既然要指定排序规则,就要在插入之前做好指定,由MyCompare类型来指定
//而set<>中的参数只能是数据类型,所以MyCompare得用数据类型来表示,不能用函数名
s1.insert(30); //现在插入的数据,就会根据Mycompare来降序排或者升序排
s1.insert(10);
s1.insert(50);
s1.insert(40);
s1.insert(20);
for( set<int,MyCompare>::iterator it = s1.begin(); it!=s1.end(); it++ )
{
cout << *it << " ";
}
cout << endl;
}
示例2:指定排序规则——自定义数据类型
class Person
{
public:
Person(string name , int age)
{
this->m_name = name;
this->m_age = age;
}
string m_name;
int m_age;
}
class ComparePerson
{
public:
bool operator()(set<Person>& p1 , set<Person>& p2)
{
return p1.m_age > p1.m_age ; //指定规则为按照年龄进行降序排序
}
};
int main()
{
set<person , ComparePerson> s;
Person p1("刘备" , 40); //如果没有指定排序规则的话,set容器都不知道按照什么进行排序,容器自己都蒙了
Person p2("关羽" , 30);
Person p3("张飞" , 28);
Person p4("赵云" , 20);
s.insert(p1); //此时插入数据,容器就会按照年龄来进行排序
s.insert(p2);
s.insert(p3);
s.insert(p4);
for(set<Person , ComparePerson>::iterator it = s.begin(); it != s.s.end(); it++)
{
//cout << "姓名:" << *it.m_name << "年龄:" << *it.m_age << endl;
cout << "姓名:" << it->m_name << "年龄:" << it->m_age << endl;
}
system("pause");
return 0;
}
map/multimap容器
- map/multimap都是关联式容器,内部结构是二叉树
- map容器中的元素全是pair对组
- pair中第一个元素为key(键值,起到索引作用),第二个元素为value(实值)
- 所有的元素都会根据元素的key键值自动排序,默认从小到大
- 可以根据键值快速找到value值,高效率高性能
- map/multimap两者的头文件都是#include
map和multimap的区别:
- map容器中不允许有重复的key值元素
- multimap容器中允许有重复的key值元素
map的构造函数
函数原型;
- map<T1 , T2> mp ; // 默认构造函数
- map<const map& mp> ; // 拷贝构造函数
map的赋值函数
函数原型:
- map& operator=( const map& mp) ; // 重载等号操作符进行赋值操作
map的大小和交换
函数原型:
- size(); // 返回元素个数
- empty(); // 判断容器是否为空
- swap(); // 交换两个map容器
map的插入和删除
函数原型:
- insert(elem); // 插入元素,地址没法自定义,因为会自动排序
- erase(key); // 删除key值的元素
- erase(pos); // 删除迭代器pos位置的元素
- erase(begin , end); // 删除区间为[begin,end)之间的元素
- clear(); // 清除容器中所有元素
map的查找和统计
函数原型:
- find( key ); //查找元素是否存在,存在则返回元素迭代器,不存在则返回map.end() ;
- count( key ); //返回键值为key的元素的个数,对于map,不是0就是1,因为不允许出现重复元素
示例:
void PrintMap( map<int , int>& m )
{
for( map<int,int>::iterator::it = m.begin(); it != m.end(); it++)
{
cout << "key = " << *it.first << "value = " << it->second << endl;
}
cout << endl;
}
int main()
{
map<int,int> m1; // 创建方式一:默认构造函数
m1.insert( pair<int,int>(1,10) ); //插入方式一
m1.insert( pair<int,int>(3,30) );
m1.insert( make_pair(2,20) ); //插入方式二
m1.insert( make_pair(4,40) );
m1.insert( map<int,int>::value_type(6,60)) //插入方式三
m1.insert( map<int,int>::value_type(7,70))
m1[6] = 90; //插入方式四,不建议用,如果插错了,value会被赋成0
m1[7] = 80; //不建议用这方式赋值,只适合用来访问
PrintMap(m1);
map<int , int> m2(m1); //创建方式二:拷贝构造函数
PrintMap(m2);
map<int,int> m3; //重载运算符进行赋值操作
m3 = m1;
if( m3.empty()) // 判断容器是否为空
cout << "map容器为空" << endl;
else
{
cout << "map容器不为空" << endl;
cout << "容器元素个数:" << m3.size() << endl; // 获取容器元素个数
}
map<int , int> m4;
for(int i = 0;i<10 ;i++)
{
m4.insert( pair<int,int>(i,i*10) );
}
PrintMap(m4); // 容器交换前
m4.swap(m1); //容器m4与m1进行交换
PrintMap(m4); //跟容器m1交换后
m4.erase(m4.begin()); //删除迭代器begin位置的元素
PrintMap(m4);
m4.erase(20); //删除key值为20的元素
PrintMap(m4);
m4.erase(m4.begin , m4.end); //删除区间为 [m4.begin , m4.end())之间的元素
PrintMap(m4);
m4.clear(); //清除容器m4中的所有元素
PrintMap(m4);
map<int,int>::iterator pos = m1.find(3); //找到元素3,如果存在,则返回元素迭代器元素
if( pos != m1.end()) //如果不存在,则返回m1.end()
{
cout << "找到了元素" << endl;
cout << "key = " << *pos.first << "value = " << "*pos.second" << endl;
}
else
{
cout << "未找到元素" << endl;
}
int num = m1.count(3); //获取key = 3 的元素个数
cout << "num = " << num << endl; //对于map容器,不是1就是0
system("pause");
return 0;
}
map容器排序
- 作用:利用仿函数,改变排序规则
- 排序规则一定要在插入数据前指定
示例:
class MyCompare
{
public:
bool operator()(int v1,int v2)
{
return v1 > v2 ; //指定降序的排序规则
}
};
int main()
{
map<int,int,MyCompare> m;
m.insert( pair<int,int,MyCompare>(1,10) );
m.insert( pair<int,int,MyCompare>(3,30) );
m.insert( make_pair(2,20) );
m.insert( make_pair(4,40) );
m.insert( make_pair(6,60) );
m.insert( make_pair(5,50) );
for( map<int,int,MyCompare>::iterator::it = m.begin(); it != m.end(); it++)
{
cout << "key = " << *it.first << "value = " << it->second << endl;
}
system("pause");
return 0;
}
函数对象/仿函数
- 本质上是一个类,重载函数调用运算符的类,其创建的对象也就被称为函数对象
- 因为函数对象在发生重载时,就像函数调用一样,所以也叫做仿函数
- 说白了就是一个对象,一个可以当函数用的对象
特点:
- 函数对象在使用时跟函数差不多,可以有参数,也可以有返回值
- 函数对象可以有自己的状态,函数没有
- 函数对象可以作为参数传递
示例:
calss MyAdd
{
public:
int oprator()(int v1, int v2) //特点1:本质是一个类,函数对象既可以有参数,也可以有返回值
{
return v1+v2;
}
};
clss MyPrint
{
public:
MyPrint()
{
this->count++; //特点2:函数对象可以拥有自己的状态
}
void operator()(string str)
{
cout << str << endl;
}
int count;
};
void test(MyPrint& mp , string str) //特点3:可以作为函数参数传递
{
mp(str);
}
int main()
{
MyAdd add;
cout << "add = " << add(10 , 30) << endl; //特点1:像普通函数一样调用
MyPrint print;
print("Hello C++"); //特点2:可以有自己的状态,比如获取自己被调用了几次
print("Hello C++");
print("Hello C++");
print("Hello C++");
cout << "print被调用的次数为:" << print.count << endl;
test(print , "Hello C++"); //特点3:函数对象作为参数进行传递
}
谓词
- 返回bool类型的仿函数,就称为谓词
- 如果operator()接受一个参数,就是一元谓词
- 如果operator()接受两个参数,就是二元谓词
一元谓词
- 如果operator()接受一个参数,就是一元谓词
示例:
class GreatFive
{
public:
bool operator()(int val) // 返回值是bool类型的仿函数,并且只有一个参数,就称为一元谓词
{
return val > 5;
}
};
int main()
{
vector<int> v;
for(int i=0;i<10;i++)
{
v,push_back(i);
}
//功能,找到容器中,元素大于5的元素
// GreatFive()是一元谓词,是一个匿名的函数对象,功能是说明要找到大于5的元素
vectot<int>::iterator it = find_if( v.begin(), v.end().Greatfive() );
if( it == v.end() )
{
cout << "没找到" << endl;
}
else
{
cout << "找到了" << *it << endl;
}
return 0;
}
二元谓词
- 如果operator()接受两个参数,就是二元谓词
示例:
class MyCompare
{
public:
bool operator()(int val1,int val2) // 返回类型为bool类型,并且有两个参数,所以是二元谓词
{
return val1 > val2 ; // 功能是从大到小
}
};
int main()
{
vector<int> v;
v.push_back(40);
v.push_back(30);
v.push_back(50);
v.push_back(20);
v.push_back(70);
v.push_back(10);
sort( v.begin() , v.end() ); // 默认是从小到大
//利用仿函数改变排序规则,从大到小
sort( v.begin() , v.end(), MyCompare() );
return 0;
}
内建函数对象
- STL内建了一些已经封装好了的函数对象
- 这些对象是由仿函数所产生的,用法和一般对象一样
- 需要加入头文件#include
- 内建函数对象是已经写好的标准算法入口,愿意你就用,不愿意就自己写一个
分类:
- 算术仿函数
- 关系仿函数
- 逻辑仿函数
算术仿函数
作用:实现四则运算
函数原型:
- template T plus // 加法仿函数,二元运算
- template T minus //减法仿函数,二元运算
- template T multiplies //乘法仿函数,二元运算
- template T divides //除法仿函数,二元运算
- template T modulus //取模仿函数,二元运算
- template T negate //取反仿函数 ,一元运算
示例:
#include<functional>
int main()
{
negate<int> n; //创建一个内建的取反对象
cout << n(57) << endl; //这就完成取反操作了
plus<int> p; //创建一个内建的加法对象,这里只需要声明一个int的数据类型就好了,因为只有同一类型的才能算术运算
cout << p(10,20) << endl; //这样就完成加法操作了
system("pause");
return 0;
}
关系仿函数
作用:实现关系对比
函数原型:
- template bool equal_to // 等于
- template bool not_equal_to //不等于
- template bool greater //大于
- template bool greater_equal //大于等于
- template bool less //小于
- template bool less_equal //小于等于
示例:不用自己写的仿函数,用内建函数对象来改变排序规则
#include<vector>
#include<algorithm>
#include<functional>
class MyCompare
{
public:
bool operator()(int val1, int val2)
{
return val1 > val2;
}
};
int main()
{
vector<int> v;
v.push_back(50);
v.push_back(30);
v.push_back(70);
v.push_back(40);
v.push_back(20);
v.push_back(10);
// sort(v.begin() , v.end(), MyCompare()); // 自己写的反函数开改变排序规则,实现大于等于的降序
sort(v.begin(), v.end(), greater<int>()); // 用内建函数对象来改变排序规则,实现大于等于的降序
system("pause");
return 0;
}
逻辑仿函数
作用:实现逻辑运算
函数原型:
- template bool logical_and // 逻辑与
- template bool logical_or //逻辑或
- template bool logical_not //逻辑非
示例:用逻辑非操作将容器v1的元素取反并搬运到容器v2中
#include<vector>
#include<algorithm>
#include<functional>
int main()
{
vector<bool> v1;
v1.push_back(true);
v1.push_back(flase);
v1.push_back(true);
v1.push_back(flase);
vector<bool> v2;
v2.resize( v1.size() ); //重新指定容器v2的大小
//transform这是标准算法,用于搬运元素
//用逻辑非的规则将容器v1搬运到容器v2中
transform(v1.begin(), v1.end(), v2.begin(), logical_not<bool>());
system("pause");
return 0;
}