Container properties: Sequence | Contiguous storage | Fixed-size aggregate
容器属性:顺序容器(支持随机访问),连续内存空间,固定大小;//连续内存
类模板头:template < class T, size_t N > class array;
array 即数组,其大小固定,所有的元素严格按照内存地址线性排列
,array 并不维护元素之外的任何多余数据,甚至也不会维护一个size这样的变量。
Array 和其它标准容器一个很重要的不同是:对两个 array 执行 swap 操作意味着真的会对相应 range 内的元素一一置换,因此其时间花销正比于置换规模;但同时,对两个 array 执行 swap 操作不会改变两个容器各自的迭代器的依附属性,这是由 array 的 swap 操作不交换内存地址决定的。
Container properties: Sequence | Dynamic array | Allocator-aware
容器属性:顺序容器(支持随机访问),动态调整大小,使用内存分配器动态管理内存;//连续内存
vector 就是能够动态调整大小的 array。和 array 一样,vector 使用连续内存空间来保存元素
,这意味着其元素可以用普通指针的++和–操作来访问;不同于 array 的是,其存储空间可以自动调整。
扩容:
在底层上,vector 使用动态分配的 array,当现有空间无法满足增长需求时,会重新分配(reallocate)一个更大的 array 并把所有元素移动过去
,因此,vector 的 reallocate 是一个很耗时的处理。
//vector主要成员函数
vector<int> v1{10,2};
empty()://判断当前向量容器是否为空
size()://返回当前向量容器中的实际元素个数
[]://返回指定下标的元素
reserve(n)://为当前向量容器预分配n个元素的存储空间
capacity()://返回当前向量容器在重新进行内存分配以前所能容纳的元素个数
resize(n)://调整当前向量容器的大小,使其能容纳n个元素
--------------------------------------------------------------
push_back(item)://在当前向量容器尾部添加一个元素
emplace_back();// 该方法采用完美转发,所以插入字面值如myv.emplace("hello")效率比push_back高很多,只要没有遭遇完美转发的限制就可以通过emplace_back传递任意型别的任意数量和任意组合的实参
//push_back 先创建一个对象,再拷贝到容器内,最后析构掉。一次只能添加一个变量。
//emplace_back 直接在容器内构造对象,不需要拷贝,此外还支持一次添加多个变量。
----------------------------------------------------------------
insert(pos,elem)://在pos位置插入元素elem,即将元素elem插入到迭代器pos指定的元素之前
front()://获取当前向量容器的第一个元素
back()://获取当前向量容器的最后一个元素
erase()://删除当前向量容器中某个迭代器或者迭代器区间指定的元素
clear()://删除当前向量容器中的所有元素
begin()://该函数的两个版本分别返回iterator或者const_iterator,引用容器的第一个元素
end()://该函数的两个版本分别返回iterator或者const_iterator,引用容器的最后一个元素后面的一个位置
rbegin()://该函数的两个版本分别返回reverse_iterator或者const_reverse_iterator,引用容器的最后一个元素
rend()://该函数的两个版本分别返回reverse_iterator或者const_reverse_iterator,引用容器的第一个元素前面的第一个位置
Container properties: Sequence | Dynamic array | Allocator-aware
容器属性:顺序容器(支持随机访问),动态调整大小,使用内存分配器动态管理内存;//分段连续内存
deque 不保证存储区域一定是连续的
,或者说是分段连续
的。
从底层机理上能更透彻地理解 deque 的特点:vector 使用的是单一的 array,deque 则会使用很多个离散的 array 来组织数据,这样做的好处是很明显的:deque 在 reallocate 时,只需新增/释放两端的 storage chunk 即可,无需移动已有数据(vector 的弊端),极大提升了效率,尤其在数据规模很大时,优势明显。
// 定义deque双端队列容器的几种方式
deque<int> dp1; // 定义元素为int的双端队列dp1
deque<int> dp2(10); // 指定dp2的初始大小为10个int元素
deque<double> dq3(10,1.23); // 指定dq3的10个初始元素的初始值为1.23
deque<int> dp4(dp2.begin(),dp2.end()) // 用dp2的所有元素初始化dp4
// deque常用函数
empty()://判断双端队列容器是否为空队
size()://返回双端队列容器中的元素个数
front()://取队头元素
back()://取队尾元素
push_front(elem)://在队头插入元素elem
emplace_front(elem);
push_back(elem)://在队尾插入元素elem
emplace_back(elem);
pop_front()://删除队头一个元素
pop_back()://删除队尾一个元素
erase()://从双端队列容器中删除一个或几个元素
clear()://删除双端队列容器中的所有元素
begin()://该函数的两个版本返回iterator或const_iterator,引用容器的第一个元素
end()://该函数的两个版本返回iterator或const_iterator,引用容器的最后一个元素后面的一个位置
rbegin()://该函数的两个版本返回reverse_iterator或const_reverse_iterator,引用容器的最后一个元素
rend()://该函数的两个版本返回reverse_iterator或const_reverse_iterator,引用容器的第一个元素前面的一个位置
Container properties: Sequence | Doubly-linked list | Allocator-aware
容器属性:顺序容器(可顺序访问,但不支持随机访问),双链表,使用内存分配器动态管理内存;//离散内存
和其它的顺序容器(array, vector, deque)相比,list 的最大优势在于支持在任意位置插入、删除和移动元素
,
底层实现是双链表,双链表允许把各个元素都保存在彼此不相干的内存地址上,但每个元素都会与前后相邻元素关联。
list 的主要缺点是不支持元素的随机访问
!如果我们想要访问某个元素,则必须从一个已知元素(如 begin 或 end)开始朝一个方向遍历,直至到达要访问的元素。此外,list 还要消耗更多的内存空间,用于保存各个元素的关联信息。
Container properties: Sequence | Linked list | Allocator-aware
容器属性:顺序容器(可顺序访问,但不支持随机访问),单链表,使用内存分配器动态管理内存 ;
forward_list 相比于 list 的核心区别是它是一个单链表
,因此每个元素只会与相邻的下一个元素关联!由于关联信息少了一半,因此 forward_list 占用的内存空间更小,且插入和删除的效率稍稍高于 list。作为代价,forward_list 只能单向遍历
。
关联容器中的每个元素有一个key(关键字),通过key来存储和读取元素,这些关键字可能与元素在容器中的位置无关,
所以关联容器不提供顺序容器中的front(),push_front(),back(),push_back()以及pop_back()操作,
容器属性:关联容器,有序,元素类型
,key是唯一的,使用内存分配器动态管理内存 ;
map 是一个关联容器,其元素类型是由 key 和 value 组成的 std::pair
,实际上 map 中元素的数据类型正是 typedef pair
关联容器,是指对所有元素的检索都是通过元素的 key 进行的(而非元素的内存地址),map 通过底层的「红黑树」
数据结构来将所有的元素按照 key 的相对大小进行排序,是严格弱序特性(strict weak ordering)
,红黑树是一种自平衡二叉搜索树
,它衍生自B树
红黑树:
性质1:每个节点要么是⿊⾊,要么是红⾊。
性质2:根节点是⿊⾊。
性质3:每个叶⼦节点(NIL)是⿊⾊。
性质4:每个红⾊结点的两个⼦结点⼀定都是⿊⾊。
性质5:任意⼀结点到每个叶⼦结点的路径都包含数量相同的⿊结点
容器属性:关联容器,有序,元素类型
,允许不同元素key相同,使用内存分配器管理内存
multimap 与 map 底层原理完全一样,都是使用「红黑树」
对元素数据按 key 的比较关系,进行快速的插入、删除和检索操作;
所不同的是 multimap 允许将具有相同 key 的不同元素插入容器(这个不同体现了 multimap 对红黑树的使用方式的差异)。在 multimap 容器中,元素的 key 与元素 value 的映射关系,是一对多的
,因此,multimap 是多重映射容器。
容器属性:关联容器,无序,元素类型
,key是唯一的,使用内存分配器动态管理内存 ;
unordered_map 以哈希表(hash table)
作为底层数据结构来组织数据。unordered_map 不支持排序
,在使用迭代器做范围访问时(迭代器自加自减)效率更低,unordered_map 直接访问元素的速度更快
,因为它通过直接计算 key 的哈希值来访问元素,是O(1)复杂度,但内存占用更高,因为底层的哈希表需要预分配足量的空间。
容器属性:关联容器,无序,元素类型
,允许不同元素key相同,使用内存分配器管理内存 ;
容器属性:关联容器,有序,元素自身即key,元素有唯一性,使用内存分配器动态管理内存 ;
set的底层结构是「红黑树」
,但和 map 不一样的是,set 是直接保存 value 的,或者说,set 中的 value 就是 key。
set中的元素必须是唯一的,不允许出现重复的元素,且元素不可更改,但可以自由插入或者删除。所以 set 中的元素也是严格弱序(strict weak ordering)
排序的,因此支持用迭代器做范围访问(迭代器自加自减)
容器属性:关联容器,有序,元素自身即key,允许不同元素值相同,使用内存分配器动态管理内存 ;
multiset 和 set 底层都是红黑树,multiset 相比于 set 支持保存多个相同的元素
容器属性:关联容器,
无序
,元素自身即key,元素有唯一性,使用内存分配器动态管理内存 ;
所有unordered_XXX类容器的特点都是以哈希表
作为底层结构。
和所有的unordered_XXX类容器一样:1. unordered_set 直接用迭代器做范围访问时(迭代器自加自减)效率更低,低于 set;2. 但 unordered_set 直接访问元素的速度更快(尤其在规模很大时),因为它通过直接计算 key 的哈希值来访问元素,是O(1)复杂度!
容器属性:关联容器,无序,元素自身即key,允许不同元素值相同,使用内存分配器动态管理内存 ;
容器适配器是指基于其他容器实现的容器,也就是说适配器容器包含另一个容器作为其底层容器,在底层容器的基础上实现适配器容器的功能,实际上在算法设计中可以将适配器容器作为一般容器来使用
容器属性:容器适配器,先进先出型容器(FIFO);//C++设计模式之适配器模式
templateclass queue;
queue(普通队列)是一个专为 FIFO 设计的容器适配器,也即只能从一端插入、从另一端删除;所谓容器适配器,是指它本身只是一个封装层,必须依赖指定的底层容器(通过模板参数中的class Container指定)才能实现具体功能。默认情况下,queue 使用 deque 作为底层容器
。
不允许顺序遍历
,没有begin()/end()和rbegin()/rend()这样的用于迭代器的成员函数,
empty()://判断队列容器是否为空
size()://返回队列容器中的实际元素个数
front()://返回队头元素
back()://返回队尾元素
push(elem)://元素elem进队
pop()://元素出队
容器属性:容器适配器,后进先出型容器(LIFO);
templateclass stack;
stack 的特点是后进先出(一端进出),不允许遍历
;任何时候外界只能访问 stack 顶部的元素;只有在移除 stack 顶部的元素后,才能访问下方的元素。默认情况下,stack 使用 deque 作为底层容器
。
容器属性:容器适配器,后进先出型容器(LIFO);
是一个容器适配器,需要指定底层容器才能实例化,默认情况下,priority_queue 使用 vector 作为底层容器
。
priority_queue 的核心特点在于其严格弱序特性(strict weak ordering)
:也即 priority_queue 保证容器中的第一个元素始终是所有元素中最大的。
实现原理:大顶堆
,priority_queue 在内部维护一个基于二叉树的大顶堆数据结构,在这个数据结构中,最大的元素始终位于堆顶部,且只有堆顶部的元素(max heap element)才能被访问和获取
https://blog.csdn.net/weixin_44788542/article/details/126187645
https://blog.csdn.net/2201_75772333/article/details/130476037
构造函数:主要作用于创建函数时对对象成员的属性赋值。
析构函数:主要作用于在对象销毁前,执行一些清理工作(如释放new开辟在堆区的空间)。
主要特点:
构造函数语法:类名(){}
1.构造函数,没有返回值也不写void
2.函数名称与类名相同
3.构造函数可以有参数,因此可以发生重载
4.程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法: ~类名(){}
1.析构函数,没有返回值也不写void
2.函数名称与类名相同,在名称前加上符号 ~
3.析构函数不可以有参数,因此不可以发生重载
4.程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
其余特点:
构造函数和析构函数是一种特殊的公有成员函数,每一个类都有一个默认的构造函数和析构函数;
构造函数在类定义时由系统自动调用,析构函数在类被销毁时由系统自动调用;
构造函数的名称和类名相同,一个类可以有多个构造函数,只能有一个析构函数。不同的构造函数之间通过参数个数和参数类型来区分;
我们可以在构造函数中给类分配资源,在类的析构函数中释放对应的资源。
如果程序员没有提供构造和析构,系统会默认提供空实现;
构造函数 和 析构函数,必须定义在public里面,才可以调用
构造函数是一种特殊的成员函数,它会在创建类的新对象时自动被调用
。构造函数的主要目的是初始化类的对象
,类似python中的__init__(self),即在创建类的实例时自动调用。
类型构造函数
(也被称为转换构造函数)是一个特殊类型的构造函数,它只接受一个参数。类型构造函数允许对象在初始化或赋值时进行隐式转换为其类的类型。这种转换构造函数定义了一种从一个特定类型到类类型的转换方式。
explicit关键词要求必须显式地进行转换,变为显示构造函数。
用于创建一个对象的新副本。拷贝构造函数接受一个同类型的对象的引用
作为参数,然后复制这个对象的值。
手动定义的拷贝构造函数实现的是深拷贝
,不仅复制对象的所有非静态成员变量,还会为每个动态内存(堆区)分配的指针成员创建新的内存复制,确保新对象获得的是原始对象数据的全新副本,而不是引用同一内存。
编译器自动生成的拷贝构造函数可能只进行浅拷贝
:只复制对象的所有非静态成员变量到新的对象。如果成员变量中包含指针,则复制的是指针值(也就是地址),而不是指针所指向的内容。这意味着原始对象和复制的对象会共享相同的动态内存。这可能会导致问题,例如,当一个对象被删除时,其析构函数可能会删除共享的内存,从而使另一个对象的指针变为悬挂指针。
一般形式为 ClassName(const ClassName& obj),其中 ClassName 是类的名称,obj 是传递的同类型对象的引用。

类的对象需要拷⻉时,拷⻉构造函数将会被调⽤:
为了防⽌递归调⽤。当⼀个对象需要以值⽅式进⾏传递时,编译器会⽣成代码调⽤它的拷⻉构造函数⽣成⼀个副本,如果类 A 的拷⻉构造函数的参数不是引⽤传递,⽽是采⽤值传递,那么就⼜需要为了创建传递给拷⻉构造函数的参数的临时对象,⽽⼜⼀次调⽤类 A 的拷⻉构造函数,这就是⼀个⽆限递归。
如果把拷贝构造函数的参数设置为值传递,那么参数肯定就是本类的一个object,采用值传递,在形参和实参相结合的时候,是要调用本类的拷贝构造函数,就是一个死循环了
在对象生命周期结束时进行清理工作,防止发生内存泄漏。
不需要显式调用析构函数,它会在对象被销毁时自动被调用
。没有返回类型,没有参数:析构函数没有返回类型,也没有参数。这意味着你不能重载析构函数
。
基类函数的析构函数必须为虚函数
。如果基类的析构函数不是虚函数,那么派生类对象调用析构函数时调用的时基类函数的析构函数,导致子类对象没有被正确清理,发生内存泄漏。
1、构造函数的调用顺序:
基类的构造函数
被调用。派生类的成员变量
的构造函数。派生类的构造函数
。2、析构函数的调用顺序:
析构函数的调用顺序与构造函数的调用顺序完全相反。
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏
,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰。
作用:
让某种类型对象获得另一个类型对象的属性和方法,方便资源重用。可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
常见的继承有三种方式:
同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为。
即⼀个接⼝,可以实现多种⽅法。
重载实现编译时多态(静态多态),虚函数实现运行时多态(动态多态)
发生在编译期
;发生在运行期
;必要条件:
小结:
- 每一个基类都会有自己的虚函数表,
派生类的虚函数表的数量根据继承的基类的数量来定
。- 派生类的虚函数表的顺序,和继承时的顺序相同。
派生类自己的虚函数放在第一个虚函数表的后面
,顺序也是和定义时顺序相同。- 对于派生类如果要覆盖父类中的虚函数,那么会
在虚函数表中代替其位置
。
virtual
关键字,在派⽣类中写该函数,运⾏时将会根据对象的实际类型来调⽤相应的函数。如果对象类型是派⽣类,就调⽤派⽣类的函数,如果是基类,就调⽤基类的函数。虚函数表
,保存该类中的虚函数的地址。同样,派生类继承基类,派生类中自然一定有虚函数,所以编译器也会为派生类生成自己的虚函数表。当我们定义一个派生类对象时,编译器检测该类型有虚函数,所以为这个派生类对象生成一个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的。对于派生类来说,编译器建立虚函数表的过程其实一共是三个步骤:
派生类的主记录
派生类中是否有重写基类中的虚函数
,如果有,就替换成已经重写的虚函数地址;查看派生类是否有自身的虚函数
,如果有,就追加自身的虚函数到自身的虚函数表中构造函数
:构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化。虚函数指针的初始化是在构造函数中完成的内联函数inline
:内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数,因为没有函数入口了。静态函数static
:静态函数不属于对象,属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。友元函数
:友元函数不属于类的成员函数,只能被重载,不能被继承。对于没有继承特性的函数没有虚函数的说法普通函数
:普通函数不属于类的成员函数,不具有继承特性,因此普通函数不能是虚函数虚函数的调用依赖于虚函数表,而指向虚函数的指针vptr需要在构造函数中进行初始化,所以无法调用定义为虚函数的构造指针
构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的
。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定虚函数的调⽤是通过实例化之后对象的虚函数表指针来找到虚函数的地址进⾏调⽤的
,如果构造函数是虚的,那么虚表指针则是不存在的,⽆法找到对应的虚函数表来调⽤虚函数,那么这个调⽤实际上也是违反了先实例化后调⽤的准则。实际上当创建⼀个派⽣类对象时,⾸先会创建派⽣类的基类部分,执⾏基类的构造函数,此时,派⽣类的⾃身部分还没有被初始化,对于这种还没有初始化的东⻄,C++选择当它们还不存在作为⼀种安全的⽅法。即对象在派生类构造函数执行前并不会成为一个派生类对象。
https://blog.csdn.net/sugarbliss/article/details/106179220
是一种特殊的虚函数,当基类函数不能对虚函数给出明确或者有意义的实现时,采用这种方式,相当于只提供了一个方法的接口,但是具体的实现要在子类对象中实现。
纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能够实例化
,但可以声明指向实现该抽象类的具体类的指针或引用。
实现方式: virtual <函数返回值> <函数名>()=0
具体例子:
#include
using namespace std;
// 抽象类
class Shape
{
public:
// 提供接口框架的纯虚函数
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
class Triangle: public Shape
{
public:
int getArea()
{
return (width * height)/2;
}
};
int main(void)
{
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
// 输出对象的面积
cout << "Total Rectangle area: " << Rect.getArea() << endl;
Tri.setWidth(5);
Tri.setHeight(7);
// 输出对象的面积
cout << "Total Triangle area: " << Tri.getArea() << endl;
return 0;
}
局部变量
和函数参数
。全局变量
静态变量
控制变量的存储方式和可见性
https://blog.csdn.net/weixin_45910068/article/details/123621193
修饰局部变量
。局部变量存在栈区
,并且生命周期在包含语句块执行结束时便结束了。用static修饰后,变量被存放在静态数据区
,并且生命周期会一直延续到整个程序执行结束。但是作用域没有变化,仍然是其语句块内。(如果static变量定义时未赋初值,编译时会自动将其赋值为0)修饰全局变量
。全局变量可以在本文件和同一个工程中的其他源文件被访问。用static修饰后,会改变其作用域范围
,由整个工程可见变成仅本文件可见。修饰函数
。作用同修饰全局变量,改变函数的作用域范围,由整个工程可见变成仅本文件可见。修饰类
。对类中的某个函数用static修饰,表示该函数属于这个类而非某个特定对象;对类中的某个变量修饰,表示该变量属于这个类(或者说属于该类的所有对象所有,存储空间只存在一个副本)。静态⾮常量数据成员,其只能在类外定义和初始化,在类内仅是声明⽽已。调用时通过::<静态变量>
或者<对象>.<静态变量>
调用。cout << A::_count << endl;
cout << a1._count << endl;
函数体内 static 变量的作⽤范围为该函数体,不同于 auto 变量,该变量的内存只被分配⼀次,因此其值在下次调⽤时仍维持上次的值;
在模块内的 static 全局变量可以被模块内所有函数访问,但不能被模块外其它函数访问;
在模块内的 static 函数只可被这⼀模块内的其它函数调⽤,这个函数的使⽤范围被限制在声明它的模块内;
在类中的 static 成员变量属于整个类所拥有,对类的所有对象只有⼀份拷⻉;
在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因⽽只能访问类的 static 成员变量。
static 类对象必须要在类外进⾏初始化,static 修饰的变量先于对象存在,所以 static 修饰的变量要在类外初始化;
由于 static 修饰的类成员属于类,不属于对象,因此 static 类成员函数是没有 this 指针,this 指针是指向本对象的指针,正因为没有 this 指针,所以 static 类成员函数不能访问⾮static 的类成员,只能访问 static修饰的类成员;
static 成员函数不能被 virtual 修饰,static 成员不属于任何对象或实例,所以加上 virtual没有任何实际意义;static成员函数没有 this 指针,虚函数的实现是为每⼀个对象分配⼀个vptr 指针,⽽ vptr 是通过 this 指针调⽤的,所以不能为 virtual;虚函数的调⽤关系,this->vptr->ctable->virtual function。
//空类在实例化时得到⼀个独⼀⽆⼆的地址,所以为1.
class A{}; sizeof(A) = 1;
//当 C++ 类中有虚函数的时候,会有⼀个指向虚函数表的指针(vptr)
class A{virtual Fun(){} }; sizeof(A) = 4(32bit)/8(64bit)
//静态成员变量不占用类的实例的内存空间,存储在一个共享的位置,并且在所有类的实例之间共享
class A{static int a; }; sizeof(A) = 1;
class A{int a; }; sizeof(A) = 4;
class A{static int a; int b; }; sizeof(A) = 4;
const int a = 0 ;
int const a = 0 ;
// const修饰int,即指针c指向的地址存在的数据不能变,但是指针c指向的地址可以变。
const int *c
int var1 = 10;
int var2 = 11;
c = &var1; //指针指向的地址可以变
*c = 13; //错误,指针指向的地址存放的内容不能变
// const修饰int,同上
int const *d;
// const修饰指针,指针e指向的地址不能改变,但是地址存放的内容可以变。
int *const e = var1;
*e = var2; // 指针指向地址的内容可以变
e = &var2; // 错误,指针指向的地址不能变
// const修饰指针,也修饰int,即地址和内容都不能变
const int * const f = NULL;
// 修饰返回值
/* 返回const int的值,返回值的内容不可修改, 无意义,因为函数的返回值本身要给其他变量 */
const int MyFun();
/* 返回指向int类型的指针,指针指向的内容不可修改,也即返回的内容不可修改*/
const int* MyFun();
/* 返回指向int类型的指针,该指针本身不可修改 */
int *const MyFun();
//修饰参数
/* 传递一个内容不可变的int型参数,无意义,值传递,函数内部会赋值一个临时变量*/
void MyFun(const int a);
/* 传递一个指向int类型的指针参数,指针本身不可变,无意义,值传递,函数内部会产生一个临时变量,承接该变量,本来就不会改变 */
void MyFun(int *const a);
/* 传递一个指向int类型的指针参数, 传递的内容不可变,虽然为值传递,产生一个临时的指针,但是指针所指向的内存空间不能变*/
void MyFun(const int * a);
/* 传递一个int类型的引用,该引用的内容不可变, 意义不大 */
void MyFun(const int &a);
/* 引用传递,传递一个对象的引用,函数内不能改变该对象的值, 避免修改,不产生临时变量,避免了临时变量构造和析构过程 */
void MyFun(const A_st &a);
https://blog.csdn.net/weixin_48560325/article/details/122643221
指针是一个保存变量地址的变量,即
int x=1;
int *p;
p = &x; //用&表示取地址
指针变量名-指针变量的地址
”添加到符号表中,所以说,指针包含的内容是可以改变的,允许拷⻉和赋值,有 const 和⾮ const 区别,甚⾄可以为空。*
操作符可以解引用指针,获取指针指向的变量的值。使用->
操作符可以通过指针访问目标对象的成员(如果目标是一个对象或结构体))。引⽤变量名-引⽤对象的地址
添加到符号表,符号表⼀经完成不能改变,所以引⽤必须⽽且只能在定义时被绑定到⼀块内存上,后续不能更改(底层是一个常量指针,即指向的地址不能改了),也不能为空,也没有 const 和⾮ const 区别。指针传递:形参是指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进⾏操作;
引用传递:把引⽤对象的地址放在了开辟的栈空间中,函数内部对形参的任何操作可以直接映射到外部的实参上⾯。
是指向函数的指针变量。
每个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。
char * fun(char * p) {…} // 函数fun
char * (*pf)(char * p); // 函数指针pf
pf = fun; // 函数指针pf指向函数fun
pf(p); // 通过函数指针pf调⽤函数fun
智能指针的作用及原理:智能指针是一个类,当超出类的作用域时,类会自动调用析构函数,从而自动释放资源。所以智能指针的原理时在函数结束时自动释放内存空间,不需要手动释放内存空间。
1、auto_ptr(C++11已弃用)独占所有权模式
auto_ptr<std::string> p1 (new string ("hello"));
auto_ptr<std::string> p2;
p2 = p1; //auto_ptr 不会报错.
但是,p2剥夺了p1的所有权,当程序运行时访问p1会报错。所以auto_ptr存在潜在的内存崩溃的问题
2、unique_ptr(auto_ptr的替代)独占所有权模式
实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针指向该对象。避免资源泄露,比auto_ptr更安全。
unique_ptr<int> uptr1(new int);
unique_ptr<int> uptr2 = uptr1; // 非法初始化
unique_ptr<int> uptr3; // 正确
uptr3 = uptr1; // 非法赋值
当 unique_ptr 将要离开作用域时,它管理的对象也将被删除。如果要删除智能指针管理的对象,但同时又保留智能指针在作用域中,则可以将其值设置为 nullptr,或者调用其 reset() 成员函数
uptr = nullptr;
//或者使用reset()方法
uptr.reset();
要想转移所有权可以使用unique_ptr提供的move()
方法, 用于将对象的所有权从一个独占指针转移到另外一个独占指针
3、 shared_ptr共享所有权模式,强引用
多个智能指针可以指向相同对象,该对象和其相关资源会在最后⼀个引用被销毁时释放。
可以通过成员函数 use_count()
来查看资源的所有者个数,除了可以通过 new 来构造,还可以通过传⼊auto_ptr, unique_ptr,weak_ptr 来构造。当调⽤ release() 时,当前指针会释放资源所有权,计数减⼀。当计数为0时,资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性 (auto_ptr 是独占的),在使⽤引⽤计数的机制上提供了可以共享所有权的智能指针。
适用场景:
(1)有多个使用者共同使用同一个对象,而这个对象没有一个明确的拥有者;
(2)某一个对象的复制操作很费时,用指针传递代替对象的复制操作,此时用shared_ptr;
(3)把指针存入STL时,用智能指针比裸指针更方便,省去了普通指针释放内存的过程。
4、 weak_ptr 弱引用
提供了对管理对象的⼀个访问⼿段,配合 shared_ptr ⽽引⼊的⼀种智能指针来协助 shared_ptr ⼯作,它的构造和析构不会引起引⽤记数的增加或减少
。
它只可以从⼀个 shared_ptr 或另⼀个 weak_ptr 对象构造,和 shared_ptr 之间可以相互转化,shared_ptr 可以直接赋值给它,它可以通过调⽤ lock 函数来获得shared_ptr。
weak_ptr 是⽤来解决 shared_ptr 相互引用时的死锁问题,如果说两个 shared_ptr 相互引⽤,那么这两个指针的引⽤计数永远不可能下降为0,也就是资源永远不会释放,此时把其中⼀个改为weak_ptr就可以。
class B;
class A
{
public:
A(){cout<<"A()"<<endl;}
~A(){cout<<"~A()"<<endl;}
weak_ptr<B> m_ptrb; //其他地方持有对象的弱智能指针
};
class B
{
public:
B(){cout<<"B()"<<endl;}
~B(){cout<<"~B()"<<endl;}
weak_ptr<A> m_ptra; //其他地方持有对象的弱智能指针
};
int main(int argc, char* argv[])
{
shared_ptr<A> ptra(new A()); //创建对象时持有强智能指针
shared_ptr<B> ptrb(new B()); //创建对象时持有强智能指针
ptra->m_ptrb = ptrb;//把指向另一个类的强引用指针换成弱引用指针
ptrb->m_ptra = ptra;
return 0;
}
创建对象的时候用shared_ptr强智能指针,别的地方一律持有weak_ptr弱智能指针,否则析构顺序有可能出现错误。
当通过弱智能指针访问对象时,需要先进行lock提升操作,提升成功,证明对象还存在,再通过强智能指针访问对象
lock()
:如果当前 weak_ptr 已经过期(指针为空,或者指向的堆内存已经被释放),则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。
https://zhuanlan.zhihu.com/p/603910874?utm_id=0
shared_ptr<class_A> ptra(new class_A); //创建对象时持有强智能指针
shared_ptr<class_A> ptra = make_shared<class_A>();
优点:
(1)提高性能。使用make_shared的方法相比于方法一,只需要分配一次内存。因为std::make_shared
申请一个单独的内存块来同时存放class_A对象和控制块(包含被指向对象的引用计数以及其他东西),而方法一需要先new对象,即分配一块内存给class_A,然后再分配一块内存给控制块。
(2)异常安全
缺点:
(1)构造函数是保护或私有时,无法使用make_shared。创建的对象没有公有的构造函数时, make_shared 就无法使用了
(2)对象的内存可能无法及时回收。由于make_shared对象和控制块保存在一起,weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 若引用都减为 0 时才能释放, 意外的延迟了内存释放的时间。
https://blog.csdn.net/weixin_43899008/article/details/123261412
都可以⽤来在堆
上分配和回收空间。new/delete 是操作符,malloc/free 是库函数。
int* p=new int; //分配大小为sizeof(int)的空间
int* p=new int(6); //分配大小为sizeof(int)的空间,并且初始化为6
int* p=(int)malloc(sizeof(int)100);//分配可以放下100个int的内存空间
int* ptr=new int[100];//分配100个int的内存空间
int* p=(int)malloc(sizeof(int)100);//分配可以放下100个int的内存空间
//这些版本可能抛出异常
void * operator new(size_t);
void * operator new[](size_t);
void * operator delete (void * )noexcept;
void * operator delete[](void *0)noexcept;
//这些版本承诺不抛出异常
void * operator new(size_t ,nothrow_t&) noexcept;
void * operator new[](size_t, nothrow_t& );
void * operator delete (void *,nothrow_t& )noexcept;
void * operator delete[](void *0,nothrow_t& )noexcept;
10.效率方面
new的效率高于malloc。能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。
https://blog.csdn.net/u_hcy2000/article/details/122470469
static_cast, dynamic_cast, const_cast, reinterpret_cast
语法格式为: xxx_cast(<变量>)
static_cast
:明确指出类型转换,⼀般建议将隐式转换都替换成显示转换,因为没有动态类型检查,上⾏转换(派⽣类->基类)安全,下⾏转换(基类->派⽣类) 不安全,所以主要执⾏非多态的转换操作。当类型不⼀致时,转换过来的是错误意义的指针,可能造成⾮法访问等问题。static_cast在编译期间完成类型转换,能够更加及时地发现错误。#include
#include
using namespace std;
class Complex{
public:
Complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ }
public:
operator double() const { return m_real; } //类型转换函数
private:
double m_real;
double m_imag;
};
int main(){
//下面是正确的用法
int m = 100;
Complex c(12.5, 23.8);
long n = static_cast<long>(m); //宽转换,没有信息丢失
char ch = static_cast<char>(m); //窄转换,可能会丢失信息
int *p1 = static_cast<int*>( malloc(10 * sizeof(int)) ); //将void指针转换为具体类型指针
void *p2 = static_cast<void*>(p1); //将具体类型指针,转换为void指针
double real= static_cast<double>(c); //调用类型转换函数
//下面的用法是错误的
float *p3 = static_cast<float*>(p1); //不能在两个具体类型的指针之间进行转换
p3 = static_cast<float*>(0X2DF9); //不能将整数转换为指针类型
return 0;
}
dynamic_cast
:专⻔⽤于派⽣类之间的转换,type-id 必须是类指针,类引⽤或 void*,对于下⾏转换是安全的,当类型不⼀致时,转换过来的是空指针。dynamic_cast在程序运行期间借助 RTTI 进行类型转换,这就要求基类必须包含虚函数。#include
using namespace std;
class A{
public:
virtual void func() const { cout<<"Class A"<<endl; }
private:
int m_a;
};
class B: public A{
public:
virtual void func() const { cout<<"Class B"<<endl; }
private:
int m_b;
};
class C: public B{
public:
virtual void func() const { cout<<"Class C"<<endl; }
private:
int m_c;
};
class D: public C{
public:
virtual void func() const { cout<<"Class D"<<endl; }
private:
int m_d;
};
int main(){
A *pa = new A();
B *pb;
C *pc;
//情况①
pb = dynamic_cast<B*>(pa); //向下转型失败
if(pb == NULL){
cout<<"Downcasting failed: A* to B*"<<endl;
}else{
cout<<"Downcasting successfully: A* to B*"<<endl;
pb -> func();
}
pc = dynamic_cast<C*>(pa); //向下转型失败
if(pc == NULL){
cout<<"Downcasting failed: A* to C*"<<endl;
}else{
cout<<"Downcasting successfully: A* to C*"<<endl;
pc -> func();
}
cout<<"-------------------------"<<endl;
//情况②
pa = new D(); //向上转型都是允许的
pb = dynamic_cast<B*>(pa); //向下转型成功
if(pb == NULL){
cout<<"Downcasting failed: A* to B*"<<endl;
}else{
cout<<"Downcasting successfully: A* to B*"<<endl;
pb -> func();
}
pc = dynamic_cast<C*>(pa); //向下转型成功
if(pc == NULL){
cout<<"Downcasting failed: A* to C*"<<endl;
}else{
cout<<"Downcasting successfully: A* to C*"<<endl;
pc -> func();
}
return 0;
}
派生类对象可以用任何一个基类的指针指向它,这样做始终是安全的。本例中的情况②,pa 指向的对象是 D 类型的,pa、pb、pc 都是 D 的基类的指针,所以它们都可以指向 D 类型的对象,dynamic_cast 只是让不同的基类指针指向同一个派生类对象
const_cast
:专门用于 const 属性的转换,去除 const 性质,或增加 const 性质, 是四个转换符中唯⼀⼀个可以操作常量的转换符。const string s = "Inception";
string& p = const_cast (s);
string* ps = const_cast (&s); // &s 的类型是 const string*
reinterpret_cast
:不到万不得已,不要使⽤这个转换符,⾼危操作。使⽤特点: 从底层对数据进⾏重新解释,依赖具体的平台,可移植性差; 可以将整形转换为指针,也可以把指针转换为数组;可以在指针和引⽤之间进⾏肆⽆忌惮的转换。#include
using namespace std;
class A
{
public:
int i;
int j;
A(int n):i(n),j(n) { }
};
int main()
{
A a(100);
int &r = reinterpret_cast<int&>(a); //强行让 r 引用 a
r = 200; //把 a.i 变成了 200
cout << a.i << "," << a.j << endl; // 输出 200,100
int n = 300;
A *pa = reinterpret_cast<A*> ( & n); //强行让 pa 指向 n
pa->i = 400; // n 变成 400
pa->j = 500; //此条语句不安全,很可能导致程序崩溃
cout << n << endl; // 输出 400
long long la = 0x12345678abcdLL;
pa = reinterpret_cast<A*>(la); //la太长,只取低32位0x5678abcd拷贝给pa
unsigned int u = reinterpret_cast<unsigned int>(pa);//pa逐个比特拷贝到u
cout << hex << u << endl; //输出 5678abcd
typedef void (* PF1) (int);
typedef int (* PF2) (int,char *);
PF1 pf1; PF2 pf2;
pf2 = reinterpret_cast<PF2>(pf1); //两个不同类型的函数指针之间可以互相转换
}
预处理→编译→汇编→链接
根据编译及运行时是否依赖动态链接库进行区分。
在C++中,联编是指一个计算机程序的不同部分彼此关联的过程。按照联编所进行的阶段不同,可以分为静态联编和动态联编:
联编⼯作在编译阶段完成的,这种联编过程是在程序运⾏之前完成的,⼜称为早期联编
。要实现静态联编,在编译阶段就必须确定程序中的操作调⽤(如函数调⽤)与执⾏该操作代码间的关系,确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引⽤的类型。其优点是效率⾼,但灵活性差
。联编在程序运⾏时动态地进⾏,根据当时的情况来确定调⽤哪个同名函数
,实际上是在运⾏时虚函数的实现。这种联编⼜称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果。活性强,但效率低。
C++中⼀般情况下的联编是静态联编,但是当涉及到多态性和虚函数时应该使⽤动态联编。动态联编规定,只能通过指向基类的指针或基类对象的引⽤来调⽤虚函数,其格式为:指向基类的指针变量名->虚函数名(实参表)
或基类对象的引⽤名.虚函数名(实参表)
实现动态联编三个条件:
⽗进程产⽣⼦进程使⽤ fork 拷⻉出来⼀个⽗进程的副本,此时只拷⻉了⽗进程的⻚表,两个进程都读同⼀块内存。
当有进程写的时候使⽤写实拷⻉机制分配内存,exec 函数可以加载⼀个 elf ⽂件去替换⽗进程,从此⽗进程和⼦进程就可以运⾏不同的程序了。
fork 从⽗进程返回⼦进程的 pid,从⼦进程返回 0,调⽤了 wait 的⽗进程将会发⽣阻塞,直到有⼦进程状态改变,执⾏成功返回 0,错误返回 -1。
exec 执⾏成功则⼦进程从新的程序开始运⾏,⽆返回值,执⾏失败返回 -1。