C++基础

文章目录

1、容器

顺序容器

1.array

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 操作不交换内存地址决定的。

2.vector

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,引用容器的第一个元素前面的第一个位置

3.deque

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,引用容器的第一个元素前面的一个位置

4.list

Container properties: Sequence | Doubly-linked list | Allocator-aware
容器属性:顺序容器(可顺序访问,但不支持随机访问),双链表,使用内存分配器动态管理内存;//离散内存

和其它的顺序容器(array, vector, deque)相比,list 的最大优势在于支持在任意位置插入、删除和移动元素
底层实现是双链表,双链表允许把各个元素都保存在彼此不相干的内存地址上,但每个元素都会与前后相邻元素关联。
list 的主要缺点是不支持元素的随机访问!如果我们想要访问某个元素,则必须从一个已知元素(如 begin 或 end)开始朝一个方向遍历,直至到达要访问的元素。此外,list 还要消耗更多的内存空间,用于保存各个元素的关联信息。

5.forward_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()操作,

6.map

容器属性:关联容器,有序,元素类型<key, value>,key是唯一的,使用内存分配器动态管理内存 ;

map 是一个关联容器,其元素类型是由 key 和 value 组成的 std::pair,实际上 map 中元素的数据类型正是 typedef pair<const Key, T> value_type;

关联容器,是指对所有元素的检索都是通过元素的 key 进行的(而非元素的内存地址),map 通过底层的「红黑树」数据结构来将所有的元素按照 key 的相对大小进行排序,是严格弱序特性(strict weak ordering)红黑树是一种自平衡二叉搜索树,它衍生自B树

红黑树:
性质1:每个节点要么是⿊⾊,要么是红⾊。
性质2:根节点是⿊⾊。
性质3:每个叶⼦节点(NIL)是⿊⾊。
性质4:每个红⾊结点的两个⼦结点⼀定都是⿊⾊。
性质5:任意⼀结点到每个叶⼦结点的路径都包含数量相同的⿊结点

7.multimap

容器属性:关联容器,有序,元素类型<key, value>,允许不同元素key相同,使用内存分配器管理内存

multimap 与 map 底层原理完全一样,都是使用「红黑树」对元素数据按 key 的比较关系,进行快速的插入、删除和检索操作;
所不同的是 multimap 允许将具有相同 key 的不同元素插入容器(这个不同体现了 multimap 对红黑树的使用方式的差异)。在 multimap 容器中,元素的 key 与元素 value 的映射关系,是一对多的,因此,multimap 是多重映射容器。

8.unordered_map

容器属性:关联容器,无序,元素类型<key, value>,key是唯一的,使用内存分配器动态管理内存 ;

unordered_map 以哈希表(hash table)作为底层数据结构来组织数据。unordered_map 不支持排序,在使用迭代器做范围访问时(迭代器自加自减)效率更低,unordered_map 直接访问元素的速度更快,因为它通过直接计算 key 的哈希值来访问元素,是O(1)复杂度,但内存占用更高,因为底层的哈希表需要预分配足量的空间。

9.unordered_multimap

容器属性:关联容器,无序,元素类型<key, value>,允许不同元素key相同,使用内存分配器管理内存 ;

10.set

容器属性:关联容器,有序,元素自身即key,元素有唯一性,使用内存分配器动态管理内存 ;

set的底层结构是「红黑树」,但和 map 不一样的是,set 是直接保存 value 的,或者说,set 中的 value 就是 key。
set中的元素必须是唯一的,不允许出现重复的元素,且元素不可更改,但可以自由插入或者删除。所以 set 中的元素也是严格弱序(strict weak ordering)排序的,因此支持用迭代器做范围访问(迭代器自加自减)

11.multiset

容器属性:关联容器,有序,元素自身即key,允许不同元素值相同,使用内存分配器动态管理内存 ;

multiset 和 set 底层都是红黑树,multiset 相比于 set 支持保存多个相同的元素

12.unordered_set

容器属性:关联容器,无序,元素自身即key,元素有唯一性,使用内存分配器动态管理内存 ;

所有unordered_XXX类容器的特点都是以哈希表作为底层结构。
和所有的unordered_XXX类容器一样:1. unordered_set 直接用迭代器做范围访问时(迭代器自加自减)效率更低,低于 set;2. 但 unordered_set 直接访问元素的速度更快(尤其在规模很大时),因为它通过直接计算 key 的哈希值来访问元素,是O(1)复杂度!

13.unordered_multiset

容器属性:关联容器,无序,元素自身即key,允许不同元素值相同,使用内存分配器动态管理内存 ;

容器适配器

容器适配器是指基于其他容器实现的容器,也就是说适配器容器包含另一个容器作为其底层容器,在底层容器的基础上实现适配器容器的功能,实际上在算法设计中可以将适配器容器作为一般容器来使用

14.queue

容器属性:容器适配器,先进先出型容器(FIFO);//C++设计模式之适配器模式
template <class T, class Container = deque > class queue;

queue(普通队列)是一个专为 FIFO 设计的容器适配器,也即只能从一端插入、从另一端删除;所谓容器适配器,是指它本身只是一个封装层,必须依赖指定的底层容器(通过模板参数中的class Container指定)才能实现具体功能。默认情况下,queue 使用 deque 作为底层容器
不允许顺序遍历,没有begin()/end()和rbegin()/rend()这样的用于迭代器的成员函数,

empty()://判断队列容器是否为空
size()://返回队列容器中的实际元素个数
front()://返回队头元素
back()://返回队尾元素
push(elem)://元素elem进队
pop()://元素出队

15.stack

容器属性:容器适配器,后进先出型容器(LIFO);
template <class T, class Container = deque > class stack;

stack 的特点是后进先出(一端进出),不允许遍历;任何时候外界只能访问 stack 顶部的元素;只有在移除 stack 顶部的元素后,才能访问下方的元素。默认情况下,stack 使用 deque 作为底层容器

16.priority_queue

容器属性:容器适配器,后进先出型容器(LIFO);

是一个容器适配器,需要指定底层容器才能实例化,默认情况下,priority_queue 使用 vector 作为底层容器
priority_queue 的核心特点在于其严格弱序特性(strict weak ordering):也即 priority_queue 保证容器中的第一个元素始终是所有元素中最大的。
实现原理:大顶堆,priority_queue 在内部维护一个基于二叉树的大顶堆数据结构,在这个数据结构中,最大的元素始终位于堆顶部,且只有堆顶部的元素(max heap element)才能被访问和获取

2、构造函数和析构函数

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 是传递的同类型对象的引用。
![在这里插入图片描述](https://img-blog.csdnimg.cn/ed9305d9514b45438f75eb2ea2fb6d12.png #pic_center =500x)

什么情况下会调⽤拷⻉构造函数(三种情况)

类的对象需要拷⻉时,拷⻉构造函数将会被调⽤:

  • ⼀个对象以值传递的⽅式传⼊函数体,需要拷⻉构造函数创建⼀个临时对象压⼊到栈空间中
  • ⼀个对象以值传递的⽅式从函数返回,需要执⾏拷⻉构造函数创建⼀个临时对象作为返回值。
  • ⼀个对象需要通过另外⼀个对象进⾏初始化
为什么拷⻉构造函数必需是引⽤传递,不能是值传递?

为了防⽌递归调⽤。当⼀个对象需要以值⽅式进⾏传递时,编译器会⽣成代码调⽤它的拷⻉构造函数⽣成⼀个副本,如果类 A 的拷⻉构造函数的参数不是引⽤传递,⽽是采⽤值传递,那么就⼜需要为了创建传递给拷⻉构造函数的参数的临时对象,⽽⼜⼀次调⽤类 A 的拷⻉构造函数,这就是⼀个⽆限递归。

如果把拷贝构造函数的参数设置为值传递,那么参数肯定就是本类的一个object,采用值传递,在形参和实参相结合的时候,是要调用本类的拷贝构造函数,就是一个死循环了

析构函数

在对象生命周期结束时进行清理工作,防止发生内存泄漏。
不需要显式调用析构函数,它会在对象被销毁时自动被调用。没有返回类型,没有参数:析构函数没有返回类型,也没有参数。这意味着你不能重载析构函数

在这里插入图片描述
基类函数的析构函数必须为虚函数。如果基类的析构函数不是虚函数,那么派生类对象调用析构函数时调用的时基类函数的析构函数,导致子类对象没有被正确清理,发生内存泄漏。

调用顺序

1、构造函数的调用顺序:

  • 首先,基类的构造函数被调用。
  • 然后,按照它们在类定义中出现的顺序,依次调用派生类的成员变量的构造函数。
  • 最后,调用派生类的构造函数

2、析构函数的调用顺序:
析构函数的调用顺序与构造函数的调用顺序完全相反。

  • 首先,派生类的析构函数被调用。
  • 然后,按照它们在类定义中出现的顺序的相反顺序,依次调用派生类的成员变量的析构函数。
  • 最后,调用基类的析构函数。

构造函数的执行算法

  • 在派生类构造函数中,所有的虚函数以及上一层基类的构造函数被调用
  • 对象的vptr被初始化
  • 如果有成员初始化列表,将在构造函数体内扩展开来,这必须在 vptr 被设定之后才做;
  • 执⾏程序所提供的代码;

构造函数的扩展过程

  • 记录在成员初始化列表中的数据成员初始化操作会被放在构造函数的函数体内,并以成员的声明顺序为顺序;
  • 如果⼀个成员并没有出现在成员初始化列表中,但它有⼀个默认构造函数,那么默认构造函数必须被调⽤;
  • 如果 class 有虚表,那么它必须被设定初值;
  • 所有上⼀层的基类构造函数必须被调⽤;
  • 所有虚基类的构造函数必须被调⽤

析构函数被扩展的过程

  • 析构函数函数体被执⾏
  • 如果 class 拥有成员类对象,⽽后者拥有析构函数,那么它们会以其声明顺序的相反顺序被调⽤
  • 如果对象有⼀个 vptr,现在被重新定义
  • 如果有任何直接的上⼀层⾮虚基类拥有析构函数,则它们会以声明顺序被调⽤

3、C++面向对象的三大特性

封装

把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰。

作用:

  • 保护或防止代码(数据)在无意中被破坏。保护类中的成员,不让类以外的程序直接访问或修改,只能通过提供的公共接口访问(数据封装)
  • 隐藏方法(实现)细节,只要接口不变,内容的修改不会影响到外部的调用者(方法封装)
  • 使对象拥有完整的属性和方法(类中的函数)
  • 外部不能直接访问对象的属性,只能通过该属性对应的公有方法访问

继承

让某种类型对象获得另一个类型对象的属性和方法,方便资源重用。可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

常见的继承有三种方式:

  • 实现继承:指使用基类的属性和方法而无需额外编码的能力
  • 接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力
  • 可视继承:指子窗体(类)使用基窗体(类)的外观和实现代码的能力

多态

同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为。即⼀个接⼝,可以实现多种⽅法。

重载实现编译时多态(静态多态),虚函数实现运行时多态(动态多态)

实现方式

  • 静态类型:对象在声明时采用的类型,在编译期即确定;
  • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期
  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期

静态/动态绑定

  • 静态多态/静态绑定:编译期间确定;函数重载、函数模板,缺省参数值也是静态绑定的
  • 动态多态/动态绑定:运行期间确定;虚函数+继承实现,基类指针/引用运⾏期间再决定使⽤哪个函数。

动态绑定如何实现:

  • 当编译器发现类中有虚函数时,会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在对象中增加一个指针vptr,用于指向类的虚函数表
  • 当派生类覆盖基类的虚函数时,会将虚函数表中对应的指针进行替换,从而调用派生类中覆盖后的虚函数,从而实现动态绑定

动态多态有什么作用?有哪些必要条件?

  • 隐藏实现细节,使得代码模块化,提高代码的可复用性
  • 接口重用,使得派生类的功能可以被基类的指针/引用所调用,即向后兼容,提高代码的可扩展性和可维护性

必要条件:

  • 必须有继承
  • 必须有虚函数覆盖
  • 必须有基类指针/引用指向子类对象

4、虚函数

小结:

  1. 每一个基类都会有自己的虚函数表,派生类的虚函数表的数量根据继承的基类的数量来定
  2. 派生类的虚函数表的顺序,和继承时的顺序相同。
  3. 派生类自己的虚函数放在第一个虚函数表的后面,顺序也是和定义时顺序相同。
  4. 对于派生类如果要覆盖父类中的虚函数,那么会在虚函数表中代替其位置

虚函数实现原理

  • C++中多态的表象,在基类的函数前加上 virtual 关键字,在派⽣类中写该函数,运⾏时将会根据对象的实际类型来调⽤相应的函数。如果对象类型是派⽣类,就调⽤派⽣类的函数,如果是基类,就调⽤基类的函数。
  • 实际上,当一个类中包含虚函数时,编译器会为该类生成一个虚函数表,保存该类中的虚函数的地址。同样,派生类继承基类,派生类中自然一定有虚函数,所以编译器也会为派生类生成自己的虚函数表。当我们定义一个派生类对象时,编译器检测该类型有虚函数,所以为这个派生类对象生成一个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的
  • 后续如果有⼀个基类类型的指针,指向派⽣类,那么当调⽤虚函数时,就会根据所指真正对象的虚函数表指针去寻找虚函数的地址,也就可以调⽤派⽣类的虚函数表中的虚函数以此实现多态。

编译器处理虚函数表应该如何处理

对于派生类来说,编译器建立虚函数表的过程其实一共是三个步骤:

  1. 拷贝基类的虚函数表,如果是多继承,就拷贝每个有虚函数基类的虚函数表
  2. 当然还有一个基类的虚函数表和派生类自身的虚函数表共⽤了⼀个虚函数表,也称为某个基类为派生类的主记录
  3. 查看派生类中是否有重写基类中的虚函数,如果有,就替换成已经重写的虚函数地址;查看派生类是否有自身的虚函数,如果有,就追加自身的虚函数到自身的虚函数表中
    在这里插入图片描述

哪些函数不能是虚函数

  • 构造函数:构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化。虚函数指针的初始化是在构造函数中完成的
  • 内联函数inline:内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数,因为没有函数入口了。
  • 静态函数static:静态函数不属于对象,属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。
  • 友元函数:友元函数不属于类的成员函数,只能被重载,不能被继承。对于没有继承特性的函数没有虚函数的说法
  • 普通函数:普通函数不属于类的成员函数,不具有继承特性,因此普通函数不能是虚函数

为什么基类的构造函数不能定义为虚函数

虚函数的调用依赖于虚函数表,而指向虚函数的指针vptr需要在构造函数中进行初始化,所以无法调用定义为虚函数的构造指针

  • 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定
  • ⽽且从⽬前编译器实现虚函数进⾏多态的⽅式来看,虚函数的调⽤是通过实例化之后对象的虚函数表指针来找到虚函数的地址进⾏调⽤的,如果构造函数是虚的,那么虚表指针则是不存在的,⽆法找到对应的虚函数表来调⽤虚函数,那么这个调⽤实际上也是违反了先实例化后调⽤的准则。

为什么基类的析构函数需要定义为虚函数?

  • 为了实现动态绑定,基类指针指向派生类对象
  • 一个基类的指针指向一个派生类的对象,如果基类的析构函数没有定义成虚函数,那么在对象销毁时,编译器根据指针类型就会认为当前对象的类型是基类,就会调用基类的析构函数,只能销毁派生类对象中的部分数据
  • 所以必须将析构函数定义为虚函数,从而在对象销毁时,调用派生类的析构函数,从而避免内存泄露

构造函数或析构函数中调用虚函数会怎样

实际上当创建⼀个派⽣类对象时,⾸先会创建派⽣类的基类部分,执⾏基类的构造函数,此时,派⽣类的⾃身部分还没有被初始化,对于这种还没有初始化的东⻄,C++选择当它们还不存在作为⼀种安全的⽅法。即对象在派生类构造函数执行前并不会成为一个派生类对象。

纯虚函数

https://blog.csdn.net/sugarbliss/article/details/106179220
是一种特殊的虚函数,当基类函数不能对虚函数给出明确或者有意义的实现时,采用这种方式,相当于只提供了一个方法的接口,但是具体的实现要在子类对象中实现。
纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能够实例化,但可以声明指向实现该抽象类的具体类的指针或引用。

实现方式: virtual <函数返回值> <函数名>()=0
具体例子:

#include <iostream>
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;
}

5、C++内存分配情况

  • :由程序员管理,需要⼿动 new/malloc、delete/free 进⾏分配和回收,空间较⼤,但可能会
    出现内存泄漏和空闲碎⽚的情况。
  • :由编译器管理分配和回收,存放局部变量函数参数
  • 全局/静态存储区:分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量
    静态变量
  • 常量存储区:存储常量,比如字符串,⼀般不允许修改。
  • 代码区:存放程序的⼆进制代码。
    在这里插入图片描述

6、static/const关键字

static关键字

控制变量的存储方式和可见性
https://blog.csdn.net/weixin_45910068/article/details/123621193

  1. 修饰局部变量。局部变量存在栈区,并且生命周期在包含语句块执行结束时便结束了。用static修饰后,变量被存放在静态数据区,并且生命周期会一直延续到整个程序执行结束。但是作用域没有变化,仍然是其语句块内。(如果static变量定义时未赋初值,编译时会自动将其赋值为0)
  2. 修饰全局变量。全局变量可以在本文件和同一个工程中的其他源文件被访问。用static修饰后,会改变其作用域范围,由整个工程可见变成仅本文件可见。
  3. 修饰函数。作用同修饰全局变量,改变函数的作用域范围,由整个工程可见变成仅本文件可见。
  4. 修饰类。对类中的某个函数用static修饰,表示该函数属于这个类而非某个特定对象;对类中的某个变量修饰,表示该变量属于这个类(或者说属于该类的所有对象所有,存储空间只存在一个副本)。静态⾮常量数据成员,其只能在类外定义和初始化,在类内仅是声明⽽已。调用时通过<class>::<静态变量>或者<对象>.<静态变量>调用。
cout << A::_count << endl;
cout << a1._count << endl;

类成员/类函数声明 static

  • 函数体内 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关键字

  • 修饰基本类型数据:放在类型说明符前后都可以,表示该数据不可被修改,所以必须要在定义时赋初值。
const int a = 0 ; 
int const a = 0 ;
  • 修饰指针变量和引用变量:const优先修饰其左边的量,如果左侧没有则修饰右侧量。
// 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);
  • 类中使用const
    • const成员函数,在函数访问方面,只能访问其他const函数,不能访问其他非const函数
    • const成员函数,在变量访问方面,可以访问const和非const变量;
    • 非const成员函数,可以访问const和非const的成员函数和变量;
    • const 对象只能操作const成员函数,不能操作非成员函数;
    • const对象可以访问const和非const变量

7、指针

指针和引用的区别

https://blog.csdn.net/weixin_48560325/article/details/122643221
指针是一个保存变量地址的变量,即

int x=1;
int *p;
p = &x;  //用&表示取地址
  • 在编译的时候,则是将“指针变量名-指针变量的地址”添加到符号表中,所以说,指针包含的内容是可以改变的,允许拷⻉和赋值,有 const 和⾮ const 区别,甚⾄可以为空。
  • sizeof 指针得到的是指针本身的⼤⼩
  • 在参数传递中,指针需要被解引⽤后才可以对对象进⾏操作(使用*操作符可以解引用指针,获取指针指向的变量的值。使用->操作符可以通过指针访问目标对象的成员(如果目标是一个对象或结构体))。
  • 作为参数时,传指针的实质是传值,传递的值是指针指向的地址。
    在这里插入图片描述
    引用是一个变量的别名,它没有自己的内存地址,而是和目标变量共享一块内存地址,即
    在这里插入图片描述
  • 编译时将引⽤变量名-引⽤对象的地址添加到符号表,符号表⼀经完成不能改变,所以引⽤必须⽽且只能在定义时被绑定到⼀块内存上,后续不能更改(底层是一个常量指针,即指向的地址不能改了),也不能为空,也没有 const 和⾮ const 区别。
  • sizeof 引⽤得到代表对象的⼤⼩
  • 在参数传递中,直接对引⽤进⾏的修改会直接作⽤到引⽤对象上
  • 作为参数时,传引⽤的实质是传地址,传递的是变量的地址
    在这里插入图片描述

指针传递:形参是指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进⾏操作;
引用传递:把引⽤对象的地址放在了开辟的栈空间中,函数内部对形参的任何操作可以直接映射到外部的实参上⾯。

空指针/引用和野指针/引用

  • 空指针/引用:空指针是指向空内存(nullptr)的指针;空引用不存在的,引用必须引用一个已存在的变量。
  • 野指针/引用:野指针是指针指向未知或无效内存地址的指针;野引用是引用一个未初始化变量的引用。
  • 悬空指针:指针最初指向的内存已经被释放了的指针。

函数指针

是指向函数的指针变量。
每个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。

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 时才能释放, 意外的延迟了内存释放的时间。

8、new / delete ,malloc / free 区别

https://blog.csdn.net/weixin_43899008/article/details/123261412
都可以⽤来在上分配和回收空间。new/delete 是操作符,malloc/free 是库函数

  • new的过程:(1)分配未初始化的内存空间(malloc)(2)使⽤对象的构造函数对空间进⾏初始化;返回空间的⾸地址。
  • delete 的过程:(1)使⽤析构函数对对象进⾏析构;(2)回收内存空间(free)

new和malloc的区别

  1. 属性方面
  • new是关键字,需要编译器支持;
  • malloc是库函数,需要头文件支持。
  1. 参数方面
  • new申请内存无需指定内存大小,编译器会根据类型信息自行计算。除此之外,new会调用构造函数。
int* p=new int; //分配大小为sizeof(int)的空间
int* p=new int(6); //分配大小为sizeof(int)的空间,并且初始化为6
  • malloc必须由我们计算需要申请的字节数,需要显式指出所需内存的尺寸,并且返回后强行转换为实际类型的指针。而且malloc只管分配内存,并不能初始化数值,所以得到的一片新内存中,其值是随机的。
int* p=(int)malloc(sizeof(int)100);//分配可以放下100个int的内存空间
  1. 处理数组
  • new有处理数组的new[],使用new[]分配的内存必须使用delete[]进行释放。
int* ptr=new int[100];//分配100个int的内存空间
  • malloc要想动态分配一个数组的内存,需要我们手动定义数组的大小。使用malloc分配内存必须使用free来释放内存。
int* p=(int)malloc(sizeof(int)100);//分配可以放下100个int的内存空间
  1. 返回类型
  • new分配成功返回的是对象类型指针,与对象严格匹配,无类型转换,所以new是符合类型安全性操作符;
  • malloc返回值类型是void*,一般需要接强制类型转换成我们需要的类型。
  1. 分配失败方面
  • new内存分配失败的时候,抛出bad_ alloc异常;
  • malloc分配内存失败时返回NULL。
  1. 自定义类型方面
  • new会先调用operator new函数,申请足够的内存,然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存。
  • malloc是库函数,只能动态地申请和释放内存,无法强制要求其做自定义类型对象构造和析构函数。
  1. 重载方面
    new可以重载,malloc不可以重载。
    opeartor new /operator delete可以被重载。标准库是定义了operator new函数和operator delete函数的8个重载版本:
//这些版本可能抛出异常 
void * operator new(size_t); 
void * operator new[](size_t); 
void * operator delete (void * )noexcept; 
void * operator delete[](void *0noexcept; 
//这些版本承诺不抛出异常 
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;
  1. 内存区域方面
  • new在自由储存区(C++中通过new和delete动态分配和释放对象的抽象概念)分配内存。
  • malloc在(操作系统层面上的定义,提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,调用free()归还内存)上分配内存。
  1. 内存泄漏方面
    内存泄漏对于new和malloc都能检测出来
  • new可以明确指出是哪个文件的哪一行,
  • malloc不可以明确指出是哪个文件的哪一行

10.效率方面
new的效率高于malloc。能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。
在这里插入图片描述

9、C++ 的四种强制转换

https://blog.csdn.net/u_hcy2000/article/details/122470469
static_cast, dynamic_cast, const_cast, reinterpret_cast
语法格式为: xxx_cast(<变量>)

  • static_cast:明确指出类型转换,⼀般建议将隐式转换都替换成显示转换,因为没有动态类型检查,上⾏转换(派⽣类->基类)安全,下⾏转换(基类->派⽣类) 不安全,所以主要执⾏非多态的转换操作。当类型不⼀致时,转换过来的是错误意义的指针,可能造成⾮法访问等问题。static_cast在编译期间完成类型转换,能够更加及时地发现错误。
#include <iostream>
#include <cstdlib>
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 <iostream>
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 <string&> (s);
string* ps = const_cast <string*> (&s);  // &s 的类型是 const string*
  • reinterpret_cast:不到万不得已,不要使⽤这个转换符,⾼危操作。使⽤特点: 从底层对数据进⾏重新解释,依赖具体的平台,可移植性差; 可以将整形转换为指针,也可以把指针转换为数组;可以在指针和引⽤之间进⾏肆⽆忌惮的转换。
#include <iostream>
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); //两个不同类型的函数指针之间可以互相转换
}

10、编译相关问题

C++高级语言代码->二进制代码四个阶段

预处理→编译→汇编→链接

  • 预处理:将 #开头的头文件插入到程序文本中,生成一个.i文件。
  • 编译:将 hello.i ⽂件翻译成汇编语言⽂件 hello.s。
  • 汇编:将 hello.s 翻译成机器语⾔指令。把这些指令打包成可᯿定位⽬标程序,即.o⽂件。hello.o是⼀个⼆进制⽂件,它的字节码是机器语⾔指令,不再是字符。
  • 链接:将编译器提供的标准库函数对应的编译好的目标文件合并到.o文件中,比如将printf.o合并到hello.o中。最终得到可执行目标文件。

动态编译与静态编译

根据编译及运行时是否依赖动态链接库进行区分。

  • 静态编译,编译器在编译可执⾏⽂件时,把需要⽤到的对应动态链接库中的部分提取出来,链接到可执⾏⽂件中去,使可执⾏⽂件在运⾏时不需要依赖于动态链接库
  • 动态编译,可执⾏⽂件需要附带⼀个动态链接库,在执⾏时,需要调⽤其对应动态链接库的命
    令。所以其优点⼀⽅⾯是缩⼩了执⾏⽂件本身的体积,另⼀⽅⾯是加快了编译速度,节省了系
    统资源。缺点是哪怕是很简单的程序,只⽤到了链接库的⼀两条命令,也需要附带⼀个相对庞
    ⼤的链接库;⼆是如果其他计算机上没有安装对应的运⾏库,则⽤动态编译的可执⾏⽂件就不
    能运⾏。

动态链接和静态链接区别

  1. 库文件调用
  • 静态连接库就是把 (lib) ⽂件中⽤到的函数代码直接链接进⽬标程序,程序运⾏的时候不再需
    要其它的库⽂件;
  • 动态链接就是把调⽤的函数所在⽂件模块(DLL)和调⽤函数在⽂件中的位置等信息链接进⽬标程序,程序运⾏的时候再从 DLL 中寻找相应函数代码,因此需要相应DLL ⽂件的⽀持
  1. 指令可见性
  • 静态链接库,lib中的指令都全部被直接包含在最终⽣成的 EXE ⽂件中;
  • 若使⽤ DLL,该 DLL 不必被包含在最终 EXE ⽂件中,EXE ⽂件执⾏时可以“动态”地引⽤和卸载这个与 EXE 独⽴的 DLL ⽂件
  1. 扩展性
  • 静态链接库中不能再包含其他的动态链接库或者
    静态库;
  • 动态链接库中还可以再包含其他的动态或静态链接库。

静态联编和动态联编

在C++中,联编是指一个计算机程序的不同部分彼此关联的过程。按照联编所进行的阶段不同,可以分为静态联编和动态联编:

  • 静态联编是指联编⼯作在编译阶段完成的,这种联编过程是在程序运⾏之前完成的,⼜称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调⽤(如函数调⽤)与执⾏该操作代码间的关系,确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引⽤的类型。其优点是效率⾼,但灵活性差
  • 动态联编是指联编在程序运⾏时动态地进⾏,根据当时的情况来确定调⽤哪个同名函数,实际上是在运⾏时虚函数的实现。这种联编⼜称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果。活性强,但效率低。

C++中⼀般情况下的联编是静态联编,但是当涉及到多态性和虚函数时应该使⽤动态联编。动态联编规定,只能通过指向基类的指针或基类对象的引⽤来调⽤虚函数,其格式为:指向基类的指针变量名->虚函数名(实参表)基类对象的引⽤名.虚函数名(实参表)

实现动态联编三个条件:

  • 必须把动态联编的⾏为定义为类的虚函数;
  • 类之间应满⾜⼦类型关系,通常表现为⼀个类从另⼀个类公有派⽣⽽来;
  • 必须先使⽤基类指针指向⼦类型的对象,然后直接或间接使⽤基类指针调⽤虚函数;

11、多线程相关

fork, wait, exec

⽗进程产⽣⼦进程使⽤ fork 拷⻉出来⼀个⽗进程的副本,此时只拷⻉了⽗进程的⻚表,两个进程都读同⼀块内存。
当有进程写的时候使⽤写实拷⻉机制分配内存,exec 函数可以加载⼀个 elf ⽂件去替换⽗进程,从此⽗进程和⼦进程就可以运⾏不同的程序了。
fork 从⽗进程返回⼦进程的 pid,从⼦进程返回 0,调⽤了 wait 的⽗进程将会发⽣阻塞,直到有⼦进程状态改变,执⾏成功返回 0,错误返回 -1。
exec 执⾏成功则⼦进程从新的程序开始运⾏,⽆返回值,执⾏失败返回 -1。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值