赞
踩
首先,通过一张最新(2021.11)的编程语言排名图来了解常见的编程语言:
从图中可以看出,C++的排名相对于Python、Java、C来说并不突出,很大的原因是因为C++难度过大,也可以说是知识点太多,我们很难说能精通C++这门语言,只能说对它的部分了解,并能在工作中使用;
1、cppreference.com:这是一个C++最具权威的百科全书,但只有英文模式,相信大家可以看懂的;
2、Compiler Explorer (godbolt.org):在线代码编辑,可以直接生成汇编代码,可选择不同的编译器和版本,功能强大;
3、C++ Insights (cppinsights.io):一个可以将C++代码及逆行翻译的网页,具体化代码高级功能;
简单来说,C++是C语言的扩展,主要有以下两方面;
首先关注性能方面,这是继承于C语言的特性:
接着是扩展部分,引入大量特性,便于工程实践
最后,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++的基础语法以及常用特性,并且从细节上去理解;
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在最终程序编译使用,优化较多;
本次讲解一个小知识点,也是最常见的一个知识点:iostream;不管编写什么程序,必然会使用到IO流交互,从细节上理解简单的IO流;
定义:标准库所提供的IO接口,用于与用户交互;
输入流:cin
输出流:cout、cerr、clog
输出流三者的一个区别:
1、首先就是输出目标的不同,cerr主要用来输出错误信息,clog主要用来输出日志信息;
2、cerr有个最大的不同,会立即刷新缓冲区,输出比较快,另外两种输出信息可能在程序崩溃时会丢失;
缓冲区刷新还有别的方法:std::flush、std::endl;
主要是为了避免名称冲突,可见如下代码:
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就是标准库的命名空间,也是最常用的一个命名空间;
本次就讲这么一点知识点,这也是入门C++的很多人疑惑的点,相信大部分人都是从输出"Hello World"开始的,那么C++中的IO细节估计没有弄懂,看完本篇就会有一个清晰的认知;
类型一直是C++中最重要的部分,相比于其他高级语言,C++的类型会复杂许多,往往一个类型匹配错误就会导致程序报错,本篇主要讲解一些常用类型的概念以及细节,如果对于C++有一定基础的,可以跳转到思考部分,从中了解自己的掌握程度;
定义:初始化与赋值语句是程序中最基本的语句,功能是将某个值与一个对象关联起来;
初始化的基本操作:
1、在内存中开辟空间、保存相应的数值;
2、在编译器中构造符号表、将标识符与相关内存空间关联起来;
下面通过几点概要说明:
1、类型是编译期概念,可执行程序中不存在类型的概念;
2、C++是强类型语言;
强类型语言定义:一旦一个变量被定义类型,如果不经过强制转换,那么它永远就是该数据类型;
弱类型语言定义:某一变量被定义类型,该变量可根据环境变化自动进行转换,不需要强转;
3、引入类型是为了更好描述程序,防止误用;
4、类型描述的信息:
#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,在不同的硬件下可能不同;
类型可以划分为基本类型和复杂类型;
基本(内建)类型:C++语言中支持的类型,包含以下几种:
1、数值类型
注意:在C++11中引入了固定尺寸的整数类型,如int32_t等,之前在针对开发板的程序中有见过该类型,主要是便于硬件的可移植性:
2、void类型
复杂类型:由基本类型组合、变种所产生的类型,可能是标准库引入,或自定义类型;
字面值:在程序中直接表示为一个具体数值或字符串的值;
每个字面值都有其类型,例子如下:
像如果想要定义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个字节;
特点:
注意两个符号:*(解引用符)、&(取地址符);
解引用符在不同环境下含义不同,看如下代码:
int x = 10;
int* p = &x;// 表示p为一个int指针类型
*p; // 表示解引用,获取指针指向地址的值
关于nullptr:
void 指针*:没有记录对象的尺寸,可以表示任意类型指针,一般作为形参或返回值;
指针对比对象:指针复制成本低,引用成本高;
总结:指针在程序中的作用,最重要的就是作为参数传入,由于数据类型可能很大,传入指针大小固定为8个字节,并且指针值为地址可复制,复制成本低,并且可在函数中改变变量的值;
2、引用:
取地址符&也有两个含义:
int x = 10;
&x;// 取地址符
int& ret = x; // 定义ret为一个引用类型
特点:
常量指针(顶层常量):
int* const p = &x;
常量指针表示指针为常量,指针不能更改指向;
底层常量:
const int* p = &x;
底层常量表示指针指向的地址的内容不能发生改变,指针指向可改变;
常量引用:
常量表达式:
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模式来查看类型;
本篇知识点较多,可以选择自己想了解的部分进行查看,后续会继续推出更深层次的内容;
上一篇讲解了类型,通过类型来开始本篇的学习;
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;
}
C字符串本质也是数组;
声明一个字符数组并打印长度
#include <cstring>
char a[] = "Hello";
std::cout << strlen(a) <<std::endl;
使用函数strlen需要引入头文件cstring;
定义:是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();
std::vector<int> x = {1, 2, 3};
x[2];// 跟数组一样,越界不报错
x.at(2);// 不可以越界
std::vector<int> x = {1, 2, 3};
std::vector<int>* p = &x;
std::cout << p->size() << std::endl;
定义:是C++标准库中定义的一个类模板特化别名,用于内建字符串的替代品;
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++标准库提供的一些关于数组的容器,大家也可以从思考部分来了解数组的一些细节;
定义:表达式由一到多个操作数组成,可以求值并通常会返回求值结果;
最基本表达式:变量、字面值,通常包含操作符;
操作符特性:
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++中,左值也不一定能放在等号左边,右值也可能放在等号左边;
值类型关系图:
C++是支持左值和右值的转换的;
decltype可以接收表达式,并且根据类型不同产生不同的值:
可参考文档:https://zh.cppreference.com/w/cpp/language/decltype
定义:一些操作符要求其操作数具有特定的类型,或者具有相同的类型,此时可能产生类型转换;
隐式类型转换:编译器自动发生的;
参考文档: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++对于语句的特性;
1、语句常见类型
2、顺序语句
3、非顺序语句(加入分支)
在执行过程中引入跳转,从而产生复杂变化;
分支预测错误可能导致执行性能下降;(分支预测是用来优化非顺序语句执行效率)
最基本的非顺序语句goto:通过标签指定跳转到的位置,本质上对应了汇编语言中的跳转指令,但容易造成逻辑混乱,应避免使用;
1、在switch语句中,如果没有用break跳出当前的switch就会执行下面的case;这样的一个情况在C++17中引入了一个属性**[fallthrough]**,感兴趣的可以了解下;
2、switch与if的优劣:
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,基本涵盖了每个知识点的原型以及使用案例,可以抽时间静下心看看;
函数:封装了一段代码,可以在一次执行过程中被反复调用,包含函数头和函数体;
函数头:
函数体:一个语句块(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 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++的内存分布:
栈区:由编译器自动分配与释放,存放为程序运行时函数分配的局部变量、函数参数;栈内存分配运算内置于处理器的指令集中,效率很高,但是分配内存的容量有限;
堆区:由new、malloc分配的内存块,释放由应用程序控制,不需要编译器释放;如果程序员没有对该内存进行释放,程序结束后系统自动回收,堆的地方比栈大很多;
静态区:存放的是static的静态变量和一些全局变量,特点是只读、大小固定;静态变量和全局变量的存储期是一起的,一旦静态区的内存被分配,要一直等到程序全部结束后才释放;
二、栈区与堆区的区别
1、分配方式不同:栈区系统分配系统回收;堆区由程序员手动申请,需要求程序员自行回收,如果没有回收,系统在程序结束后会进行回收,这种情况会造成内存泄漏;
2、生命周期不同:栈区生命周期是系统分配到系统回收,也就是在大括号内;堆区是从申请到释放;
3、效率不同:主要原因是地址空间是否连续,栈区地址空间是连续的,效率会高一些;堆区地址空间不连续,需要遍历链表才能找到最近的地址,效率会低一些;
4、内存碎片:堆区容易产生内存碎片,栈区不会;
5、生长方向不同:栈区申请空间的地址(表示地址的八个十六进制数)是从大到小的,堆区申请空间地址是从小到大的。栈区是先进后出的原则,类比栈结构的特点;
栈区特点:更好的局部性,对象自动销毁;
堆区特点:运行期动态扩展,需要显示释放;
注意点:申请的空间是在堆区,变量本身是在栈区!
一、内存分配方式
可操作的内存分配:
不可操作的内存分配:
内核区、代码区、局部变量的分配也属于系统分配;
二、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;
四、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修改;
还有一点需要注意,不管任何机器上运行程序,操作的都是虚拟内存,内部通过页表定位到对应的物理内存。关于硬件方面的本质,如果做嵌入式端的话需要深入研究。
上一篇介绍了软件层面上的内存,并没有涉及很多底层的原理;但在实际工程中,部署一个项目往往需要考虑内存的占用,这里的内存也就是嵌入式板子上的内存;本篇文章就简单介绍一下嵌入式端的一个内存管理;
主要分为五大模块:
本次主要讲解内存管理模块,其他模块不做介绍;
在Linux环境下,可通过free -m查看内存使用情况;
下图是一台rk3326机器的内存情况:
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可以避免在快速读写时候的问题;
cache和buffer的一个区别:
在很多嵌入式板子上都有内存对齐的处理;
思考下以下结构占用的内存:
struct A{
char a;// 1
char b;// 1
int c;// 4
}
根据CPU的分配机制,在64位机器上占用8个字节,这也是做了一些对齐处理;
不仅仅是内存,一些板子(例如昇腾310)会对图像数据进行对齐,图像的分辨率要满足硬件支持的倍数,这样才能做到高效处理;
本篇只是对上一篇内存的一个补充,主要讲解Linux中的内存;这部分对于一些端侧部署的伙伴来说比较重要,推荐针对不同的板子,还是需要先阅读API文档,了解关于内存的API后再进行代码的开发;
多线程是开发中必不可少的,往往我们需要多个任务并行,就需要多线程开发;就好比图像检测和图像结果的处理,这就是一个可闭环的任务,用多线程是可以加速这个任务的;
有三种方式可以构建多线程,前提是都需要引入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();
使用案例:
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语言: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.对象生成能够更简单
在类中定义成员方法,不占用内存;
不传参数,自己会识别那个对象调用。
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关键字
(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)设置类或者函数友元(不建议,会暴露私有成员)
类与类之间的关系
组合类:一个类是另一个类的一个组成部分
代理类:一个类的成员是另一个类成员的子集
类的编译程序:先编译类名,再编译成员名,再编译成员方法体
对象的生成顺序:先生成成员对象,再生成自身对象
对象析构顺序:先析构自身,再析构成员
成员对象如果没有默认的构造函数,则该成员对象的初始化必须手动放在初始化列表;如果成员对象有默认的构造函数,系统会自动再初始化列表加上该对象的默认构造。
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; }
太复杂需优化。
建议:
优化结果:
(7)堆区申请对象
int main()
{
Tmp* tp = new Tmp();
return 0;
}
没有析构,进程结束才会释放空间,要手动析构delete tp
作用:建立一个通用函数,其函数返回值类型和形参类型可以不具体定制,用虚拟的类型来表示
关键字:template
语法:
template <typename T> // template <class 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;
}
语法:
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; }
作用:建立一个通用的类,但是类中的成员变量类型未知,用一个虚拟的类型来表示
语法:
与函数模板的区别:
示例:
//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、整个类进行模板化 }
当类模板碰到继承时:
语法:
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:写主函数和应用功能
原因:类模板的成员函数在主函数调用的时候才创建,所以会导致预处理时链接不到
解决办法(建议用第二种):
两种方法,目的都是在跑主函数之前,将声明和实现都先跑了一遍。
类模板和友元配合成的全局函数的类内实现和类外实现:
示例:
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); //类外实现 }
六大组件就是上线说到的:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器
作用:存放数据,将广泛使用的数据结构给实现出来
常用的数据结构:数组、链表、树、栈、队列、集合、映射表等
容器的分类:
作用:问题的解法,即用有限的步骤,解决逻辑上的难题
算法的分类:
作用:容器和算法之间的链接器,即就是提供一种方法,既能依序寻找容器内某个元素,又不暴露容器内部的表示方法
迭代器的种类:
种类 | 功能 | 支持运算 |
---|---|---|
输入迭代器 | 对数据的只读访问 | 只读,支持++、==、!= |
输出迭代器 | 对数据的只写访问 | 只写,支持++ |
前向迭代器 | 对数据的向前操作 | 读写,支持++、==、!= |
双向迭代器 | 对数据的向前和向后操作 | 读写,支持++、– |
随机访问迭代器 | 访问任何数据,功能最强 | 读写,支持++、–、[n]、-n、<、<=、>、>= |
示例:
#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字符串赋值
函数原型:
示例:
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; }
作用:就是在字符串末尾再拼接一段字符串
函数原型:
示例:
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 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”
}
作用:字符串之间的比较
特点:
函数原型:
示例:
int main()
{
string s1 = "dafwfw" ;
string s2 = "dasff" ;
if ( s1.compare( s2 ) == 0)
{
//主要是用来比较是否相等
}
else if (s1.compare(s2) > 0)
{
}
else
{
}
}
对单个字符进行存取有两种方法:
函数原型:
示例:
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 ;
}
作用:对字符串中进行插入和删除字符的操作
函数原型:
示例:
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;
}
作用:从字符串中截取想要的子串
函数原型:
示例:
int main()
{
string s1 = "safagrgwg";
string substr = s1.substr( 0 , 3 ); //从s1字符串中,从位置0开始,截取3个字符作为子串
cout << substr << end;
system("pause");
rerurn ;
}
vector容器结构以及成员函数
作用:创建vector容器
函数原型:
示例:
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容器进行赋值操作
函数原型:
示例:
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容器的容量和大小参数
函数原型:
示例:
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); }
作用:在容器中插入元素和删除元素
函数原型:
示例:
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; }
作用:获取容器中的数据
函数原型:
示例:
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:用于收缩容器容量(原理就是创建一个小容量的容器与本身的大容量容器互换)
函数原型:
示例:
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次。
函数原型:
示例:
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; }
函数原型:
示例:
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与vector的区别:
deque内部结构和成员函数
deque内部工作原理
作用:创建deque容器
函数原型:
示例:
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容器进行赋值
函数原型:
示例:
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; }
函数原型:
示例:
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; }
作用:在容器两端进行插入和删除新数据
函数原型:
示例:
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容器中的数据进行存取
函数原型:
示例:
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; }
函数原型:
示例:
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容器内部结构以及成员函数
函数原型:
函数原型:
函数原型:
函数原型:
示例:
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内部结构和成员函数
函数原型:
函数原型:
函数原型:
函数原型:
示例;
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容器
函数原型:
作用:给list容器赋值,以及交换list容器
函数原型:
示例:
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的长度
函数原型:
示例:
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容器进行元素插入和删除
函数原型:
示例;
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; }
函数原型:
示例:
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; }
作用:将容器中的元素反转,以及对元素进行排序
函数原型:
示例:
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容器
函数原型:
函数原型:
函数原型:
函数原型:
作用:对set容器进行数据查找以及数据统计
函数原型:
示例:
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.insert()的函数原型:
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); }
作用:成对出现的数据,可以返回两个数据
创建方法的函数原型:
示例:
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;
}
示例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的区别:
函数原型;
函数原型:
函数原型:
函数原型:
函数原型:
示例:
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; }
示例:
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:函数对象作为参数进行传递 }
示例:
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; }
示例:
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; }
分类:
作用:实现四则运算
函数原型:
示例:
#include<functional>
int main()
{
negate<int> n; //创建一个内建的取反对象
cout << n(57) << endl; //这就完成取反操作了
plus<int> p; //创建一个内建的加法对象,这里只需要声明一个int的数据类型就好了,因为只有同一类型的才能算术运算
cout << p(10,20) << endl; //这样就完成加法操作了
system("pause");
return 0;
}
作用:实现关系对比
函数原型:
示例:不用自己写的仿函数,用内建函数对象来改变排序规则
#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; }
作用:实现逻辑运算
函数原型:
示例:用逻辑非操作将容器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; }
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。