C++ 与 C语言区别:
C++ | C | |
特性 | 封装 继承 多态 + 新特性(面向对象、安全性) | - |
面向 | 对象 | 过程 |
安全性 | 智能指针(shared - point)、cast转换、const常量、try-catch | - |
可复用性 | 模版、STL | - |
封装操作 | class:(对象数据封装,默认private(含三种权限),私有继承) | struct:(数据结构 集合,默认public(仅含一种权限),无继承) |
struct声明对比展开 | ||
成员函数 | 可有(允许是虚函数) | 无 |
静态成员 | 可有 | 无 |
访问控制 | public、private、protected | 默认public |
继承关系 | 可继承 | 不可继承 |
初始化 | 可有 | 无法直接 |
------------------ | ---------------------------------------------------------- | ---------------------------------------------------------- |
编译区别 | 支持函数重载(考虑参数类型+函数名) | 函数名 |
new 与 malloc区别:
new | malloc | |
本质 | 操作符 (对象) | 函数 |
调用步骤 | 分配内存 -> 构造函数 -> 使用 -> 析构函数 | 无构造析构函数 |
内存分配 | 无需指定内存大小,调用构造函数会自动分配 | 申请指定内存大小 |
重载性 | 可以被重载 | 无法重载 |
安全性 | 分配内存 直接安全 | - |
发生错误 | 抛出异常 | 返回null |
底层实现步骤 | 创建对象 -> 构造函数作用域给新对象(this指向新对象)-> 执行构造函数代码(为这个对象添加属性) -> 返回新对象 | 开辟内存空间(> 128k -> 调用mmap()),后调用brk()函数。 采用内存池管理方式:申请大块内存作为堆区,并分为多个内存块。用户申请时分配空闲块,采用隐式链表记录空闲连续的内存地址 |
左值与右值:
特点 | 左值 (Lvalue) | 右值 (Rvalue) |
---|---|---|
定义 | 代表可以标识的内存位置 | 代表临时值或常量 |
赋值 | 可以出现在赋值语句的左边 | 只能出现在赋值语句的右边或参数列表中 |
地址访问 | 可以获取其地址(可以被地址访问) | 不能获取其地址(无法访问地址) |
例子 | 变量、数组元素、指针引用等 | 常量、字面量、临时表达式的结果等 |
C++11及以后的处理 | 支持左值引用 | 支持右值引用和移动语(move) |
数组和指针:
数组 | 指针 | |
概念 | 连续存放多相同数据类型数据的集合,数组名是首元素的地址 | 变量,指针名指向内存的首地址 |
赋值 | 单元素一个一个的赋值 | 同类型指针可相互赋值 |
存储方式 | 连续存放 | 任意指向 |
存储空间 | 静态区或者栈 | 无法确定 |
sizeof | sizeof(数组名)/ sizeof(数据类型) | 32bit -> 4 ; 64bit -> 8 |
函数指针与函数指针:
函数指针 | 指针函数 | |
定义 | 指向函数入口地址的指针。 | 一个返回值为指针的函数 |
写法 | | |
使用场景 | 别人库调用我们的函数,称回调函数(callback)。 |
指针函数与函数指针的区别_chiying9800的博客-CSDN博客
悬挂指针 内存泄漏 野指针:
野指针:指针没有做初始化操作,从而不明确指针所指向的位置,成为野指针。
指针悬挂:指针所指向的内存空间已经被删除,从而该指针所指向的空间就不确定了,容易导致错误或者崩溃。
内存泄漏(Memory Leak):是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
避免方法: 1)初始换置NULL ;2)申请内存判空 ;3)指针释放后置NULL ;4)智能指针(自动释放内存)
内联函数、宏与函数的区别:
函数调用:函数调用是有时间和空间开销的。程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码;函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码。
内联函数 | 宏函数 | 普通函数 | |
表示 | 含关键字inline | 含关键字define(本质非函数) | |
调用开销 | 无需函数调用开销 ----> 无需寻址 | 预处理器用复制宏代码的方式代替函数调用 | 需函数调用----> 需寻址函数入口地址 |
特性 | 编译时插入函数体,代码简单。 若代码较长,占用内存代价大。 若控制语句多,函数执行时间比调用开销大 | 预编译时处理,将宏名用宏体表示 | |
类型检查 | 有,检查返回值,参数列表是否满足条件 | 无 |
静态局部变量、全局变量、局部变量特点及使用场景:
静态全部变量 | 全局变量 | 静态局部变量 | 局部变量 | |
初始化 | 首次用到时 | 首次用到时 | 首次用到时 | 开始函数 |
作用域 | 全局作用域 + 文件作用域 | 全局作用域 | 局部作用域 | 局部作用域 |
空间 | 静态存储区 | 静态存储区 | 静态存储区 | 栈 |
生命周期 | 程序结束 | 程序结束 | 程序结束 | 作用域回收内存时 |
全局变量、静态变量、局部变量的生存周期与作用域_全局变量的生命周期_Nine_CC的博客-CSDN博客
const 相关:
const与define:
const | define | |
生成阶段 | 编译阶段 | 预编译阶段 |
内存 | 需要占用内存 | 直接操作数,无需占用内存 |
类型 | 带类型 | 无类型 |
const int a | const int *a | int const *a | int *const a | const int *const a |
a常量,不允许修改 | a指向内存值不变 | a指向内存值不变 | a指向内存地址不变 | 都不变 |
C++代码到可执行文件过程:
C++程序从源码到执行文件过程:预编译、编译、汇编、链接。
预编译:(宏 include 注释操作等)
- 将所有的#define删除,并且展开所有的宏定义
- 处理所有的条件预编译指令,如#if、#ifdef
- 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
- 过滤所有的注释
- 添加行号和文件名标识。
编译:(源代码生成汇编代码)
- 词法分析:将源代码的字符序列分割成一系列的记号。
- 语法分析:对记号进行语法分析,产生语法树。
- 语义分析:判断表达式是否有意义。
- 代码优化。
- 目标代码生成:生成汇编代码。
- 目标代码优化。
汇编:这个过程主要是将汇编代码转变成机器可以执行的指令。
链接:将不同的源文件产生的目标文件进行链接,从而形成一个可执行的程序。
链接:静态链接、动态链接。
动态链接库 | 静态链接库 | |
形式 | 执行时找链接函数,生成exe文件只包含重定位信息。 无动态库无法运行程序 | exe文件含调用函数与过程链接。 无静态库依旧可执行程序 |
过程 | 调用 - > 重定位信息 -> dll库 -> 调用函数 | 调用 -> 链接与exe(内含lib)-> 调用函数 |
exe速度 | 略慢 | 略快 |
装载代码速度 | 慢 | 快 |
内存 | 体积小,节省内存 | |
加载时间 | 运行时 | 编译时 |
传递方式:
值传递:传入的参数为实参的拷贝临时变量,本质为形参,作用与局部变量中,由编译器在栈内自动分配内存与释放内存。
引用传递:绑定传入参数的对象,即保证通过函数操作后,会实现引用对象的内值改变,更安全。
指针传递:传入参数为指向特定地址的内存,故没有拷贝流程,返回结果也会对指向该地址的内值出现改变。
C++内存:
堆与栈:
栈 | 堆 | |
空间分配 | 操作系统自动分配释放(局部变量) | 程序员自定分配释放(new) |
缓存方式 | 一级缓存(调用释放速度快 | 二级缓存 |
数据结构 | 栈结构(先进后出 | 数组 |
内存分配方式 | 介绍 |
堆 | 函数/局部变量执行完自动被释放 |
栈 | new分配内存块,new-delete |
自由存储区 | malloc分配内存快,malloc -free |
全局/静态存储区 | 全局变量与静态变量分配内存区域 |
常量存储区 | 只能存放常量不能被修改 |
内存泄漏:
申请内存后,使用完毕没有及时释放掉 ----> 1)new 后没有delete 2)子继承父类,父析构非虚。
内存错误与对策:
问题 | 对策 |
未分配成功,就用 | new申请内存后,检查指针值是否为NULL |
分配成功,未初始化,就用 | 数组与动态内存赋初值,防止右值使用问题。 |
初始化成功,操作超边界 | 避免数组指针越界判断,尤其是+1,-1 |
未释放内存,内存泄漏 | new - delete 配对使用,释放后记得指针置NULL,防止野指针 |
释放内存却使用 | 智能指针 |
内存对齐:
- 作用:使CPU对变量进行快速访问,减少访问offset对齐时间。
- 操作:起始地址应满足“最宽基本类型成员"的整数倍
程序section:
程序内存段 | 纪要 |
代码段 | 存放程序执行代码的内存区域,只读 |
BSS段 | 存放未初始化或初始化为0的全局或静态变量,读写 |
数据段 | 存放已初始化的全局静态变量 |
堆区 | 动态申请内存,地址从低向高增长 |
栈区 | 存储局部变量/函数参数的连续空间,地址由高向低增长 |
文件映射区 | 处于堆栈之间的区域 |
常量存储区 | 存放常量不许修改。 |
面向对象:
面向对象和面向过程:
面向对象 | 面向过程 |
对数据和函数进行绑定封装,加快开发速度 | 业务逻辑由上到下的写 |
C++三大特性:
特性名称 | 描述 |
封装 | 封装隐藏对象属性及细节(pp 安全性),通过公开接口与对象交互(包含三大对象属性(ppp) |
继承 | 父子类对象的属性继承(private > protected > public) |
多态 | 用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数; 重载函数(参数数据类型不同),重写函数(父子类函数重构) |
封装方面:
构造函数类别:
类别 | 描述 |
默认构造函数 | 无参数传入情况,直接调用默认值 |
初始化构造函数 | 有参数传入时,初始化对象属性 |
拷贝构造函数 | 赋值构造函数属于浅拷贝(指向地址相同) 复制构造函数为深拷贝(指向地址不同,防止释放同一块内存) |
移动构造函数 | 用于将其他类型变量,隐式转换为本类对象;源对象内容丢失被目标对象所占有, 目标对象接管源对象内存,无需拷贝节省时间 |
移动赋值与拷贝赋值:
- 1)拷贝构造函数的形参是一个左值引用,而移动构造函数的形参是一个右值引用;
- 2)拷贝构造函数完成的是整个对象或变量的拷贝,而移动构造函数是生成一个指针指向源对象或变量的地址,接管源对象的内存,相对于大量数据的拷贝节省时间和内存空间。
形参类型 | 赋值形式 | |
拷贝赋值 | 左值引用 | 整个对象或变量的拷贝 |
移动赋值 | 右值引用 | 生成一指针指向源对象或变量的地址,接管源对象的内存 避免不必要的资源复制,提高代码的效率,并减少内存使用 (所有权形式,节省拷贝时间,节省内存空间) |
默认及初始化构造函数:
class Student
{
public:
//默认构造函数
Student()
{
num=1001;
age=18;
}
//初始化构造函数
Student(int n,int a):num(n),age(a){}
private:
int num;
int age;
};
int main()
{
//用默认构造函数初始化对象S1
Student s1;
//用初始化构造函数初始化对象S2
Student s2(1002,18);
return 0;
}
拷贝构造函数:(浅拷贝)
#include "stdafx.h"
#include "iostream.h"
class Test
{
int i;
int *p;
public:
Test(int ai,int value)
{
i = ai;
p = new int(value);
}
~Test()
{
delete p;
}
Test(const Test& t)
{
this->i = t.i;
this->p = new int(*t.p);
}
};
//复制构造函数用于复制本类的对象
int main(int argc, char* argv[])
{
Test t1(1,2);
Test t2(t1);//将对象t1复制给t2。注意复制和赋值的概念不同。
return 0;
}
深拷贝与浅拷贝:
描述 | |
浅拷贝 | 引用变量名不同,但是指向同一个区域共用一份实体,地址相同 |
深拷贝 | 开辟与源对象一样的空间,指向地址不同,拷贝内容相同,无重复释放统一内存的错误 |
注意知识:
- 只定义析构函数,自动生成 默认构造函数 与 拷贝构造函数
- 默认空类自动生成: 1)默认无参构造 2)拷贝构造 3)析构函数 4)赋值运算符
继承方面:
当派生类对象初始化顺序(析构顺序相反):
- 父类构造函数
- 派生类成员构造函数 (按照派生类中成员声明顺序)
- 派生类构造函数
class Base {
public:
Base() {
cout << "Base constructor" << endl;
}
};
class Derived : public Base {
public:
int derivedVar;
AnotherClass anotherObject;
Derived() : derivedVar(0), anotherObject() {
cout << "Derived constructor" << endl;
}
};
int main() {
Derived derivedObj;
return 0;
}
分析:Derived
是一个派生类,它公开继承自 Base
。Derived
类中有一个 int
类型的成员变量 derivedVar
和一个名为 AnotherClass
的对象 anotherObject
。在 Derived
类的构造函数中,可以看到它首先调用 Base
类的构造函数,然后按照声明的顺序初始化 derivedVar
和 anotherObject
,最后才调用自身的构造函数。
转型类型:
描述 | |
向上转型 | 子类转父类,利用dynamic_cast |
向下转型 | 父类转子类,利用强转会导致数据丢失 ; |
多态之向下转型(强制转型) - deng-hui - 博客园 (cnblogs.com)
不同类继承访问权限:
继承方式 | Private继承 | Protected继承 | Public继承 |
基类private成员 | 不可见 | 不可见 | 不可见 |
基类protected成员 | Private | protected | protected |
基类public成员 | Private | protected | Public |
抽象类:
抽象类表示至少包含一个纯虚函数的类;抽象类只能用作其他类的基类。
菱形继承问题:
问题 | 解决方案 |
菱形继承:数据重复(浪费存储空间),产生二义性 | 虚继承/虚基类: |
虚继承:存在于爷辈和父辈之间,生成两个不同的虚基类表和两虚表指针,但是指向的区域内容相同,解决了二义性的问题(继承来自父类还是叔类)和数据重复问题。
shellmad-c++_50 菱形继承与虚继承_哔哩哔哩_bilibili
多态方面:
重写与重载:
描述 | |
重写 | 在派生类中重定义父类的虚函数(必须要有V修饰);基类函数为虚函数,派生型重写; 函数名,参数列表,返回值类型必须同基类 |
重载 | 同一函数通过 输入参数不同 区别,得到重载函数;在编译时对其进行区分 |
多态类型:
调用时机 | 形式 | |
动态多态(晚绑定) | 运行过程中,通过基类指针或虚函数调用时 | 重写虚函数 调用子类方法 |
静态多态(早绑定) | 编译期间完成,编译器会根据实参类型推断该调用何函数 | 重载函数 调用父类方法 |
虚析构与虚构造 虚函数表:
名称 | 描述 |
虚析构 | 可用,保证基类指针指向子类空间释放是内存的释放,防止内存泄漏;调用虚析构会先排查派生类构造函数是否存在,调用派生类析构,在调用基类虚构 |
虚构造 | 虚函数对应一个虚表需要分配内存空间,只有对象实例化时才能分配内存空间,逻辑相悖 解释:要构造虚函数,需要调用虚函数表,要建立虚函数表,需实例化对象,对象没有实例化,内存空间未分配,虚函数表建立不了,无法虚构造。 |
虚函数表: | 存放虚函数地址的数组,在编译阶段生成,构造函数之前写入。 |
注: C++ 中默认析构函数不为虚函数,虚函数本身需要虚函数指针与虚表占用额外内存。
class TimeKeeper
{
public:
TimeKeeper() {}
virtual ~TimeKeeper() {}
};
虚表与虚表指针:
名称 | 描述 |
虚表 | 编译阶段生成,存放虚函数的一维虚函数表(本质数组)以NULL结尾 ----> 虚表对应类 对象实例化后,开辟内存空间,写入虚指针,在调用构造函数 。即:虚表在构造函数前写入 |
虚表指针 | 类对象通过指向虚表的虚指针找到虚函数------> 虚表指针对应类对象 |
虚函数与纯虚函数:
虚函数 | 纯虚函数 | |
描述 | 存在V函数的类对应虚表,类对象有指向V表的V指针 | 纯虚函数:virtual void fun() = 0; 即 先有名称,没有内容 |
调用 | 父类定义,子类重写都可,动态绑定 | 基类内无法实例化,需派生类里重写函数, 动态绑定 |
抽象 | 必须含有纯虚函数 | |
形式 | virtual func{ } | virtual func{ } = 0 |
注:纯虚函数的目的: 使派生类仅仅只是继承函数的接口。让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。|| 告诉子类设计者,你需要提供纯虚函数的实现,但是功能怎么实现不关我事。。。
#include<iostream>
using namespace std;
class Base
{
public:
virtual void func() = 0;
};
class Derived :public Base
{
public:
void func() override
{
cout << "哈哈" << endl;
}
};
int main()
{
Base *b = new Derived();
b->func();
return 0;
}
不支持虚函数的函数:
名称 | 原因 |
普通函数(非成员函数 | 只能被重载,不能被重写,编译过程自动绑定函数 |
静态成员函数 | 属于一类,非对象,无法绑定类 |
内联成员函数 | 在编译时展开,虚函数运行时动态绑定;调用时机不同 |
构造函数 | 悖论逻辑 |
友元函数 | 继承特性无虚函数说法。 |
STL部分:
STL分类:
容迭仿算适配空间
广义 | 算法 algorithm、容器container、迭代器iterator |
详细 | 算法 algorithm、容器container、迭代器iterator、仿函数function object、适配器、空间配置器 |
名称 | 描述 |
Container | 数据结构,以模板类方法提供List、vector、deque |
algorithm | 操作容器的数据的模板函数 |
iterator | 提供访问容器对象的方法 |
仿函数 | 函数对象,重载操作符的struct |
适配器 | 接口类,作用提供新接口,或调用现用函数实现功能 |
空间配制器 | 为STL提供空间配置的系统,作用:1)对象创建、销毁 ; 2)内存申请或释放 |
常见容器类型:
容器类型名称 | 实现原理 |
顺序容器 | Vector :动态数组,内存连续存放,头尾删减较好,支持随机访问,中间插入删减不佳 Deque :双向队列,内存连续存放,头尾删减较好,支持随机访问 List :双向链表,内存不连续存,任何位置删减均方便,不支持随机访问,查找速度慢 |
关联式容器 | 实现原理:平衡二叉树 Set/multiset:不能修改set值,set迭代器时const Map/multimap:map会根据key值对元素进行由小到大排列,可以根据key检索元素 |
容器适配器 | 封装一些基本容器,增加新的函数功能; stack:后进先出,检查更改只能是头部修改项 queue:先进先出,插入可以在尾部,删改查在头部 priority_queue: 内部保持有序,优先级最高者在头部。 |
名称 | 实现原理 |
map | 红黑树:自动排序功能,增删查控制在 O(logn) 数量级,内部元素有序,key不可修改,value可改 存储方式:中序 |
哈希表 | 函数映射思想,记录存储位置与关键字关联 通过映射查找,比vector deque等比较查找效率更高 |
STL容器增删查时间复杂度:
容器 | 插入 | 查看 | 删除 |
Vector | O(N) | O(1) | O(N) |
Deque | O(N) | O(1) | O(N) |
List | O(1) | O(N) | O(1) |
Map、set、mmap… | O(logN) | O(logN) | O(logN) |
Unordered… | O(1)~O(N) | O(1)~O(N) | O(1)~O(N) |
vector与list:
vector | list |
动态数组,在堆内存放,连续存放,减小大小不会内存释放 增大大小后扩容内存:1)申请三倍内存空间,2)拷贝数据值新内存,3)释放旧内存,4)指针指向新内存 | 内存空间不连续,访问效率低, 增删效率高,无[ ]操作符重载 |
resize与reserve:
名称 | 概念 |
capacity | 容器初始化时赋值,代表容器能容纳的最大元素个数 |
size | 表示此时容器实际元素个数 |
区别:
resize | reserve | |
空间分配 | 即分配空间,也创建了对象 | 只预留了空间,需要insert或push_back()创建对象 |
修改 | 即修改capacity也修改了size | 只修改capacity |
形参 | 包含两个形参,容器大小+初始值 | 容器预留大小 |
STL空间配置器:
1. C++ 对象实例化方式
直接构造类对象 | Test test(); | 局部变量存储于栈空间 函数代码运行结束,自动释放内存 |
通过new来实例化对象 | Test *p = new Test | 堆空间存储及释放 堆分配-构造-析构-堆释放 |
2. 内存分配形式
分配区域 | 分配时机 | 形式 |
静态存储区 | 编译时 | 整个运行空间均存在(全局变量,静态变量) |
栈空间 | 程序运行时 | 函数执行完毕,栈空间被回收(局部变量) |
堆空间 | 程序运行时 | New/malloc创建,free/delete释放 |
3. 空间配置器实现 (两级配置器)
配置器级别 | 内容 |
一级配置器 | 考虑大块内存空间的配置,malloc和free |
二级配置器 | 考虑小块碎片内存空间的配置,链表free_list维护内存池,通过union将空闲内存挂接一起。 使用小块内存空间是,从free_list中剔除,便于维护。 |
迭代器失效/删除(erase)情况:
容器类型 | Erase后效果 |
Vector deque | 后面元素的迭代器失效,前移一位返回到下一个有效迭代器 |
Map set | 不会影响下一个元素的迭代器 |
List | 不连续分配内存,返回到下一个有效迭代器 |
remove与erase的区别:
功能 | erase 方法 | remove 算法 |
---|---|---|
作用 | 删除指定位置或区间的元素,减少 vector 的大小 | 将指定值从范围内移到后面,但不改变 vector 的大小 |
参数 | 可接受单个位置参数或一个区间 [begin, end) | 接受一个值参数 val 和一个范围 [begin, end) |
返回值 | 返回被删除元素后的迭代器位置 | 返回新的 end 迭代器位置,但不改变原 end |
对 size 的影响 | 减少 vector 的大小 | 不改变 vector 的大小 |
原始元素顺序 | 被删除的元素后面的元素会向前移动 | 保持不移动 val 之外的元素顺序 |
复杂度 | 平均时间复杂度 O(N) | 平均时间复杂度 O(N) |
示例 | vec.erase(position); | vec.erase(std::remove(vec.begin(), vec.end(), val), vec.end()); |
迭代器作用:
-
用于指向顺序容器和关联容器中的元素 -----> 指向元素
-
通过迭代器可以读取它指向的元素 ------------> 读取元素
-
通过非const迭代器还可以修改其指向的元素 > 修改元素
-
不暴露集合内部结构达到遍历集合的效果 ----> 安全性
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> v; //一个存放int元素的数组,一开始里面没有元素
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::const_iterator i; //常量迭代器
for (i = v.begin(); i != v.end(); ++i) //v.begin()表示v第一个元素迭代器指针,++i
指向下一个元素
cout << *i << ","; //*i表示迭代器指向的元素
cout << endl;
vector<int>::reverse_iterator r; //反向迭代器
for (r = v.rbegin(); r != v.rend(); r++)
cout << *r << ",";
cout << endl;
vector<int>::iterator j; //非常量迭代器
for (j = v.begin();j != v.end();j++)
*j = 100;
for (i = v.begin();i != v.end();i++)
cout << *i << ",";
return 0;
}
/* 运行结果:
1,2,3,4,
4,3,2,1,
100,100,100,100,
*/
容器与迭代器类别:
容器 | 容器上迭代器类别 |
vector | 随机访问 |
deque | 随机访问 |
list | 双向 |
set、multi | 双向 |
map、multi | 双向 |
stack | 不支持 |
queue | 不支持 |
p_queue | 不支持 |
pushback与emplace_back区别:
- push_back需要构建临时对象,对这个对象拷贝到容器末端
- emplace_back直接在容器尾端构造对象,省去拷贝环节
int main()
{
vector<A> vec;
vec.reserve(10);
for(int i=0;i<10;i++){
vec.push_back(A(i)); //调用了10次构造函数和10次拷贝构造函数,
// vec.emplace_back(i); //调用了10次构造函数一次拷贝构造函数都没有调用过
}
}
C++ 11 新特性:
成员变量默认初始化
//程序实例
#include<iostream>
using namespace std;
class B
{
public:
int m = 1234; //成员变量有一个初始值
int n;
};
int main()
{
B b;
cout << b.m << endl;
return 0;
}
auto:定义变量编译器自己自动判断(需要对其进行初始化,根据(右值推断))
//程序实例
#include <vector>
using namespace std;
int main(){
vector< vector<int> > v;
vector< vector<int> >::iterator i = v.begin();
auto it = v.begin();
return 0;
}
decltype:适用于某些auto无法自动推导的情况(根据exp表达式推断 (左值推断))
auto varname = value;
decltype(exp) varname = value;
智能指针:多个shared_ptr 可以共同使用同一块堆内存,实现采用引用计数机制,
sp释放内存后,该内存块引用计数 -1,其他sp仍可使用;
只有当引用计数为0,堆内存才会被自动释放。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
//构建 2 个智能指针
std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2(p1);
//输出 p2 指向的数据
cout << *p2 << endl;
p1.reset();//引用计数减 1,p1为空指针
if (p1) {
cout << "p1 不为空" << endl;
}
else {
cout << "p1 为空" << endl;
}
//以上操作,并不会影响 p2
cout << *p2 << endl;
//判断当前和 p2 同指向的智能指针有多少个
cout << p2.use_count() << endl;
return 0;
}
/* 程序运行结果:
10
p1 为空
10
1
*/
空指针:nullptr 是 nullptr_t 类型的右值常量,专用于初始化空类型指针,解决内存泄漏
nullptr 是实例化对象,NULL是数据。
#include <iostream>
using namespace std;
void isnull(void *c){
cout << "void*c" << endl;
}
void isnull(int n){
cout << "int n" << endl;
}
int main() {
isnull(NULL);
isnull(nullptr);
return 0;
}
/* 程序运行结果:
int n
void*c
*/
可以通过隐式转换,转换nullptr数据类型
int * a1 = nullptr;
char * a2 = nullptr;
double * a3 = nullptr;
迭代器for循环:使用迭代器或其他容器进行遍历
unordered_map:用法和功能同map一模一样,区别在于哈希表的效率更高
lambda表达式
移动语句:
本质:所有权模式,将左值转换为右值,资源转移,资源窃取。
bstd::move & 左值右值 &左值引用右值引用_move 左值变右值_s11show_163的博客-CSDN博客
智能指针:
智能指针与普通指针的区别:
智能指针 | 普通指针 | |
定义 | 指针变量是专门用来存放地址的,并将它定义为“指针类型”。 | |
内存释放特性 | 对普通指针加一层封装,可以自动释放所指对象,更方便管理一个对象的生命周期。 | 使用new生成对象在堆内分配内存,不需要时,需要释放内存delete并指针置NULL,如果没有正确操作, 出现 1) 内存泄漏 2) 野指针 问题 |
智能指针解决的问题:
针对普通指针出现内存泄露或野指针的问题,智能指针能避免这类问题。
将智能指针抽象成一个类,超出类作用域时,主动调用析构函数释放资源,避免手动释放内存。
智能指针的类别:
智能指针类型 | 特点 |
---|---|
shared_ptr | 允许多个指针共享资源的所有权,引用计数管理 |
unique_ptr | 同一时间独占资源的所有权,不能复制,只能移动 |
weak_ptr | 配合shared_ptr使用,不增加引用计数 |
auto_ptr (废弃) | 在C++11中被废弃,不推荐使用,废弃原因:所有权模式,存在内存崩溃的风险 |
auto_ptr (废弃)内存崩溃问题:
auto_ptr<string> p1(new string("I reigned loney as a cloud."));
auto_ptr<string> p2;
p2=p1; //auto_ptr不会报错
如上述代码所示,P2将P1的所有权剥夺,即P1指针置NULL,再次访问P1将会报错。
unique_ptr 针对 auto_ptr问题优化:
unique_ptr:同一时间只能有一个智能指针指向该对象,避免内存崩溃的问题。
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
分析:#情况1 pu2不能把pu1的所有权占据,B:给我用下 A:如果想要你自己去搞
#情况2 pu3可占据临时右值,同时临时对象自行毁灭,C:欸地上有个没人要的,那我捡起来
shared_ptr(强引用):
对于auto独占性的问题,提出了引用计数机制,即可在同一时刻多个智能指针指向相同对象,
每释放一个指针所有权时,引用计数减一;当最后一个指针被reset ( ) 后,资源会被释放。
成员函数 | 描述 |
---|---|
use_count() | 返回引用计数的个数,即指向同一资源的shared_ptr对象的个数。 |
unique() | 检查是否是独占所有权,即use_count()是否为1。如果为1,则表示当前shared_ptr是该资源的唯一所有者。 |
swap() | 交换两个shared_ptr对象,即交换所拥有的对象。 |
reset() | 放弃内部对象的所有权或拥有对象的变更,会引起原有对象的引用计数的减少。 |
get() | 返回内部对象的指针,由于已经重载了()方法,因此和直接使用对象是一样的。 |
weak_ptr:
解决问题:shared_ptr相互引用发生死锁问题 - > 引用计数永远不会降为0,资源永远不会被释放。
weak_ptr本质是一种弱引用,是一种不控制对象生命周期(构造析构不会引起计数改变)的智能指针,仅是一种对管理对象的访问手段。--------------> 无法知晓指向对象的引用计数
可由shared_ptr或weak_ptr对象构造;1)shared_ptr直接赋值给它 2)通过lock函数获得shared_ptr
class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout<<"A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout<<"B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main()
{
fun();
return 0;
}
1过程 全是shared:
创建B类对象pb,count B= 1
创建A类对象pa,count A= 1
pb强引用pa_ , count B = 2
pa强引用pb_ , count A = 2
跳出函数同时析构,引用计数分别 - 1,无法释放内存资源
2过程 shared + weak:
创建B类对象pb,count B= 1
创建A类对象pa,count A= 1
pb弱引用pa_ , count B = 1
pa强引用pb_ , count A = 2
跳出函数同时析构,引用计数分别 - 1,B对象引用计数为 0 ,A对象引用计数减为1同时由于B析构释放内存,A对象引用计数减为0,A对象B对象均释放。
注意:weak_ptr 无法直接访问,需要转换成shared_ptr,再进行访问!!
智能指针发生内存泄漏:
会,循环引用导致计数无法置零,导致内存泄露。
引入weak_PTR
十大排序算法:
https://blog.csdn.net/m0_62789921/article/details/131815782?spm=1001.2014.3001.5501