基础
定义一个struct,有int x,char c两个成员,这个结构体的大小?如果增加一个static int 这个结构体大小怎么变化?
根据内存对齐 int x ,char c,一共 8 。加上 static int 不变还是 8。
static成员分配在静态区,不在类/结构内部分配内存
例子:
typedef struct {
int a;
double b;
short c;
}A;
typedef struct {
int a;
short b;
double c;
}B;
对于 a,24
int
□□□□ _ _ _ _
double
□□□□□□□□
short
□□_ _ _ _ _ _
对于 b ,18
int short
□□□□ | □□ _ _
double
□□□□□□□□
内存对齐原因 平台移植可能出现异常;处理器对于未对齐的内存会两次访问
有哪些数据类型?如何类型强制转换?
四整型(byte 1 ,short 2 ,int 4 ,long 8) 两浮点(double 8 ,float4)一字符 char ,一个bool型
int -2^31-1 ~ 2^31
自动转换:byte–>short–>int–>long–>float–>double , 但是会损失精度超出范围
什么时候用到拷贝函数?
a.一个对象以值传递的方式传入函数体;
b.一个对象以值传递的方式从函数返回;
c.一个对象需要通过另外一个对象进行初始化。
C++浅拷贝和深拷贝的区别
浅拷贝:直接为数据成员赋值的拷贝。只是对指针的拷贝,拷贝后两个指针指向同一个内存空间。比如,类中没有显式地声明一个拷贝构造函数,那么编译器将会自动生成一个默认的复制构造函数,该构造函数完成对象之间的浅复制。
但是浅拷贝是有问题的,如果这个类的成员变量含有指针的话,但是我生成一个对象a1,然后用这个a1给另一个对象a2赋值,这样导致这两个对象指针指向同一块内存,指针被分配一次内存,但是程序结束时该内存却被释放了两次,会导致崩溃!
所以就需要深拷贝,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
那么深拷贝如何实现呢?
主要是拷贝构造函数和赋值函数。
在拷贝构造函数中,我要先 new 一块内存,然后再把要拷贝的内容复制到这里。
在赋值函数中,先把原来的内存 delete 掉,然后开辟跟要赋值的目标对象一样大小的空间,然后再把内容复制到这里。
请说一下C/C++ 中指针和引用的区别?
1.指针有自己的一块空间,而引用只是一个别名;
2.使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;
4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
5.可以有const指针,但是没有const引用;
(为了限制指针更改指向,引入了const指针;引用本身就不能更改,所以不存在const引用)
6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
final和override
原出处更详细
override 就是重载。
如果子类 对父类虚函数进行重写的时候。可以没有这override 关键字。但是如果我不小心把要重写的虚函数的名字写错了。并不会报错,编译器会以为我写了个新函数。如果有override关键字,它指定了子类的这个虚函数是重写的父类的。如果打错了会报错。
final
当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。例子如下:
C++在构造函数和析构函数中的异常
原版更详细
结论 1 :
C++中通知对象构造失败的唯一方法那就是在构造函数中抛出异常。
构造函数中抛出异常,会导致析构函数不能被调用。
结论 2
析构函数不能抛出异常,会出现奇怪的事情(什么程序过早结束)
聊一聊操作符重载
操作符重载就是把一些操作符通过关键字operator,组装成一个函数。基本格式是 函数的返回类型 operator 操作符(参数列表)
。但是也有不能够重载的操作符,比如 .
. *
::
? :
可以方便自定义的运算。
但是也是有限制的,我们为自定义的类重载操作符时,重载操作符的含义应该跟内置类型一样,比如,你不能通过重载+号操作符来实现两个数相乘的运算
前置 ++ 和后置++ 的区别
前置和后置++ 的区别就是一个先用再加,一个先加在用。但是在编译器里他们的操作符重载函数有些区别吧,前置++的话,返回值以引用的形式返回,无参数;后置++ 是以值的形式返回,有一个参数,但这个是为了和前置++区分自动填上去的。
我的理解就是一个是先用再加,一个是先加再用。当然正规说法是,前置++ ,表示取a的地址,增加它的内容,然后把值放在寄存器中;后置++表示取a的地址,把它的值装入寄存器,然后增加内存中的a的值;
(1)前置++的话,返回值以引用的形式返回,无参数;后置++ 是以值的形式返回,有一个参数,但这个是为了和前置++区分自动填上去的。
(2)后置++需要先将原来的值拷贝一份,然后再进行自增,最后返回那个拷贝。这会产生临时对象,所以前置++效率也更高。
关于 赋值=操作符重载
有四个要关注的点
首先,返回是引用返回,一是引用返回少创建一个临时对象,提高效率,二是有返回值就是为了实现连续赋值,(引用返回,返回的变量的生命周期要比函数生命周期长)
二是传入的参数加const 保证形参不变,而且也是引用传入提高效率。
三要先判断是不是在自己给自己赋值,要保证所赋值的对象与本身对象地址是不一样的。
聊聊函数模板(模板函数)
函数模板是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。举个栗子,要写一个交换函数swap,如果交换两个整数就要写一个参数是整型变量的函数,但如果要传入double型又要另外再写一个函数,很麻烦,于是就有了函数模板,相当于有了一个写函数的模子,通过这个模子制造很多功能几乎相同的代码。
函数模板的基本写法是,template
,<类型参数1>
,然后是函数部分,函数返回值类型,模板名称,形参表和函数体。
由模板实例化而得到的函数称为模板函数。
聊聊类模板
类模板与函数模板类似,如果说有多个类,功能相同但只是数据类型不同的话,就可以声明一个类模板,基本的写法是,一个template ,类型形式参数表,类声明。
如果类模板A派生了一个普通类B的话,B就需要让编译器知道父类的数据类型具体是什么(知道父类占多少内存)。
关于模板的全特化和偏特化
函数只有特化版本。
全特化限定死模板实现的具体类型,偏特化的话,模板有多个类型,那么只限定其中的一部分;
比如,一个类模板有一个类型参数,如果我限制这个类型时int型,那么对于任何,当我们用其他类型进行实例化的时候会执行普通版本,当我们用int实例化的时候则会执行int特化版本。
但是一个类模板有两个类型参数,只限定其中一个类型时Int型,这种就是偏特化。
const 在函数不同位置的意义
1.当成员函数后面加const,函数中所有成员属性不能被改变
void get() const
{
…
}
2.当成员函数中形参加const,表示修饰的形参在其函数体不能被修改。
void get (const int a)
{}
3.当成员函数前加const,表示返回值不能被修改
const int get()
{}
const 指针
const 在指针左边 const * ,代表指针指向的内容不能更改,如果const在指针的右边 * const ,指针指向内容可以更改,但是指向不可以更改。
static 关键字
我所知道就是,声明的变量在全局静态区,如果一个类成员变量是static,那么sizeof(这个类) 这个static变量大小不会算进去。
如果static修饰成员变量,类的对象对他共享。牵一发而动全身,只要一个对象改变了这个变量,其他的对象调用这个变量也会变
修饰成员函数,好处不用new一个对象去调用成员函数。可以直接通过 类名.方法名 调用
Static修饰的局部变量只初始化一次,生命期为整个程序的生命期,作用域依旧为局部
Static修饰的全局变量与普通的全局变量相比只是作用域变为了文件作用域,旨在当前代码文件中访问使用。
external关键字
如果全局变量不在开头声明的话,有效的作用范围将只限于其定义处到文件结束。 extern 对该变量作“外部变量声明。表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。
野指针的产生原因,解决方法
1.指针变量定义时没有被初始化—> 定义指针时初始化,可以是具体的地址值也可以是NULL;
2.指针P被free或者delete之后没有置为NULL–>指针指向的内存空间被释放后指针应该指向NULL;(指针变量和它所指的内存空间变量是两个不同的概念)
3.指针超越了所指变量的作用域–>所指变量的作用域结束前释放掉变量的地址空间,并让指针指向NULL;
类的大小
更详细
一般情况下,没有虚函数,直接考虑内存对齐。
例子:
一个 char ,三个 int ,char 考虑对齐,等于四个 int ,大小16布局类似 int arr[4];
存在虚函数 ,则需要多存一个虚指针
多一个虚函数指针,其他的不变。size = 20.
多继承 子类继承父亲母亲类
分析:
父类:两个 int 还有虚函数的虚指针 一共 12;
母类:两个 int ,一个 char ,char 对齐,相当于 三个 Int ,还有虚指针。 一共 16;
子类 : 子类没有数据,只是重写了父类的 fun2。父母有的他也要都有, size = 12 + 16 = 28;
注意,如果用 子类指针直接访问 func1 ,因为父母都有,会出现指定不明。
虚指针与对象绑定,因此,类中多个虚函数,也只有一个虚指针。
面向对象
继承
三种继承方式
https://blog.youkuaiyun.com/sxtdzj/article/details/81906504
三种继承方式不同,区别主要是派生类中成员变量对父类中的成员变量的访问权限,和派生类对象对父类成员变量的访问不一样。
不管是哪种继承方式,派生类中新增成员可以访问基类的公有成员和保护成员,无法访问私有成员。而继承方式影响的是派生类继承成员访问属性,而使用友元(friend)可以访问保护成员和私有成员。
多态
什么是多态?
就是多种状态。一个接口,多种实现。其中多态分为静态多态和动态多态
动态多态和静态多态?
静态绑定是程序编译时确定程序行为。
动态绑定是程序运行时根据具体的对象确定程序行为。 比如虚函数,编译器不会在编译时就确定对象要调用的函数的地址,而是在运行时再去确定要调用的函数的地址。
静态多态一般是函数重载。函数名称相同,但是参数的类型,个数不同。
动态多态是“继承+虚函数”。父类成员函数前加上 virtual 关键字。那么在子类中,我就可以重写这个函数。当我使用父类的指针指向子类对象时,调用的函数是子类中重写的函数,这样就实现了运行时函数地址的动态绑定(动态联编)
作用
为了代码的重用吧,为了接口重用。
C++如何实现多态
(答一下什么是多态)首先,多态就是不同对象对同一行为会有不同的状态。 C++的多态性是用虚函数和动态绑定 来实现的。虚函数的声明是在基类的函数前加上virtual关键字,然后在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
多态的实现有两个条件,1 子类对父类虚函数的重写 2 、是对象调用虚函数时必须是指针或者引用
多态的原理?
多态是用虚函数表实现的。存在虚函数的类都有一个一维的虚函数表叫做虚表。类的对象有一个指向虚表开始的虚指针。
聊聊虚函数的实现
C++中的虚函数的作用主要是实现了多态的机制。
虚函数表
虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。
在这个表中,主是要一个类的虚函数的地址表。如果有子类继承父类覆盖父类的函数这些情况,这个就是一个地址表,表名当前实际调用的函数。
一般子类继承父类,没有重载父类函数。那么在表中每个函数有一个地址。但如果有重载的情况,就会覆盖父类函数。比如 Base *b = new Derive(); b->f();
b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
虚函数表指针和虚函数表创建时机
虚函数表指针随对象走,它发生在对象运行期,当对象创建的时候,虚函数表表指针位于该对象所在内存的最前面。
虚函数表是在编译期间就已经确定,且虚函数表存放虚函数的地址也是在创建时被确定。
纯虚函数
也是使用 virtual 关键字,但是在函数后面加 =0
通常是父类中没有定义,但是在子类中必须要重新实现。带有纯虚函数的类也被称作抽象类。
重载和重写
重载 几个同名函数,但是参数类型不同,个数不同。
重写 他是父类有虚函数,子类继承父类并重新实现了这个虚函数,就是对他进行重写。
实现虚函数时编译器的原理
编译器会给给类的每个对象添加一个隐藏的成员,这个成员保存了一个虚指针,而虚函数表中保存了类对象进行声明的虚函数的地址。也就是说我们可以通过这个隐藏成员访问虚函数表,进而访问被声明的虚函数的的地址,从而调用虚函数。还有一个就是动态绑定,就在上面。
析构函数为什么一般情况下要声明为虚函数?
虚函数是实现多态的基础,当我们通过基类的指针是析构子类对象时候,如果不定义成虚函数,那只调用基类的析构函数,子类的析构函数将不会被调用。如果定义为虚函数,则子类父类的析构函数都会被调用。
构造函数和析构函数哪个能写成虚函数,为什么
构造函数不能是虚函数因为虚函数是基于对象的,构造函数是用来产生对象的,若构造函数是虚函数,则需要对象来调用,但是此时构造函数没有执行,就没有对象存在,产生矛盾,所以构造函数不能是虚函数。
析构函数可以是虚函数,因为若有父类指针指向子类对象存在,需要析构的是子类对象,但父类析构函数不是虚函数,则只析构了父类,造成子类对象没有及时释放,引起内存泄漏。
子类的虚函数中能不能调用父类的虚函数
能。子类虚函数调用父类虚函数,直接用,并不担子类覆盖。这个时候虚函数不是通过虚指针虚表,而是直接跟普通函数一样,直接用。
#include<iostream>
#include<string>
using namespace std;
class Father
{
public:
Father()
{
cout << "this is the Father constructor!" << endl;
}
virtual void watchTv()
{
cout << "Father is watching tv "<< endl;
}
};
class Son :public Father{
public:
Son()
{
cout << "this is the Son constructor!" << endl;
}
virtual void watchTv()
{
Father::watchTv();
cout << "Son is watching tv "<< endl;
}
};
int main(){
Father* fa = new Son();
fa->watchTv();
return 0;
}
多继承
一个子类继承了多个父类。但是会导致 二义性
二义性
1 、C多继承自A和B,A B中都有 Z 函数。然而C的实例在调用 Z 函数的时候就会产生二义性。
解决 要么别重名;要么调用的时候声明具体是哪个父类的函数。
如果 C 中又有 Z函数,那么A B中的Z都被隐藏,直接调用 C 的
2 、菱形继承
A 为父类,B1继承A, B2 继承 A,然鹅 C 又继承B1 ,B2。
这个时候类 A 中的成员变量和成员函数继承到类 C 中变成了两份。那么C 直接访问 A中成员就出现二义性。
解决方法: 让 B1 ,B2 虚继承 A。使得在派生类中只保留一份间接基类的成员。直接在继承的类前面加 Virtual 关键字
菱形继承下的类大小
没有虚继承的话,C 的大小是 B1 + B2 ,相当于A中变量有两份。
但虚继承的话
多继承下析构函数和构造函数的执行顺序
构造:先父类再子类
析构相反
函数执行过程
inline 与普通函数区别(什么是内联?为什么用内联?函数是否都可以是内联?)
因为如果是普通的函数执行的时候,栈要为每个变量和形参分配空间,还要将实参的值传给形参,函数返回地址储存在栈中,执行完了,还要释放栈中局部变量啊形参的内存,再从栈中取出返回地址,继续执行之前的程序。带来了很多额外开销。
于是就有内联函数,把一些内容小的函数声明为内联。
内联与普通函数的主要区别是 ,编译器处理调用内联函数的语句时,不会将该语句编译成函数调用的指令,而是直接将整个函数体的代码插人调用语句处,降低开销。
并不能所有函数都是内联,有循环语句或者switch 这种,换过去了代码碰撞开销反而更大。
inline与宏区别?
首先(1),内联函数在编译时展开,宏在预编译时展开;然后(2)内联函数直接嵌入到目标代码中,宏是简单的做文本替换;所以内联函数可以进行参数是否有效检测,也可以转换返回值类型。内联函数作为类成员函数,可以访问类的保护成员私有成员。
构造/析构/虚函数函数可以内联吗?
类中的函数默认内联。
构造 ,析构可以,但是并不好。因为内联函数一般是不能太复杂的函数,析构和构造函数编译器在编译期间会加入很多代码,所以并不适合。
虚函数的话也可以,但是表现多态性不可以,因为多态是运行时确定,内联是编译时内联。
STL
STL的六大部件和联系
1、容器:各种数据结构,如Vector,List,Deque,Set,Map,用来存放数据。
2、算法:各种常用算法,比如排序算法等等。
3、迭代器:容器与算法之间的胶合剂,即泛型指针。
4、仿函数:行为类似函数,可作为算法的某种策略
5、配接器:用来修饰容器或仿函数或迭代器接口。比如queue,stack,像容器,但是只能算作是一种容器配接器。因为它们的底部完全借助Deque,所有操作有底层的Deque提供。
6、配置器:负责空间配置与管理
相互关系
容器通过配置器取得数据存储空间,算法通过迭代器获取容器内容,仿函数可以协助算法完成不同的策略变化,配接器可以修饰或套接仿函数。
迭代器失效的几种情况
https://blog.youkuaiyun.com/baidu_37964071/article/details/81409272
正确的做法是返回那个迭代器。
it = iVec.erase(it);
vector
size 和 capacity
size是实际有多少对象的个数,capacity是数组的容量。
扩容
首先,扩容,是指扩 capacity。
扩容:扩容是假象。本质是size == capacity,之后如果要放入数据放不下了,capacity扩大到原来的两倍。当然是假象。
实际上,是重新开了一块内存,是原来的两倍,然后把原来的内容拷贝赋值到新内存,释放掉原来的内容。
缩容
vector自己是不能缩容的,但是有个问题,如果不断的插入数据,capacity很大。如果我都不用了,vector.clear(); 内存白白放在那,有什么办法缩容呢?
shrink_to_fit()
直接name.shrink_to_fit();这个函数并不是将vector清空, 而是把capacity降低到与size相同。
所以如果要把这个不用的vector的size和capacity都清空,需要先clear(); 再 shrink;
这个shrink的实现,底层是swap函数,且只需要一句话
vector<int>(ivec).swap(ivec);
生成一个临时数组变量,这个变量拷贝了一份真身数组的内容,但是vector的拷贝构造函数只是给他分配了元素size需要的内存,没有多余的内容。然后把这个数组与我们的真身交换。那么现在,这个临时变量有了原本真身的多余的部分,真身已经缩减了。然后临时变量销毁。
push_back 和 emplace_back 区别
push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
vector和list的区别
聊一聊 set 和 multiset
multiset和set 底层是红黑树,也是默认从小的开始排序。但是不同的是multiset可以有重复键值,set的话相当于可以去重。
聊一聊 set 和 unordered_set
set在上面, unordered_set是 一个哈希表,但是他与map不一样,在底层他也有一个key 对应一个 value 但是key 和 value 相等。
聊一聊map
stl 的 map ,是一个关联式容器。有一个key 和一个对应的value。其底层是红黑树,有排序的能力。map的排序默认按照key从小到大进行排序。不允许有两个相同键值。
聊一聊map 和 Multimap
与map差不多,但是运行有相同的键值。对于重复的元素,查找的时候也是返回中序遍历的第一个元素。是一个二叉搜索树嘛。
聊一聊 map 和 unorderded_map
map 是红黑树,unordered_map是哈希表,前者删改查复杂度是O(logn),后者是常数时间,一般是O(1),最坏情况O(n)。因为哈希冲突,链表法的话,得一个个找。
讲讲哈希表
根据关键码值(Key value)而直接进行访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。
原理是就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value。
优点就是,降低了数据查找的时间,一般是O(1),最坏情况O(n),代价是消耗比较多的内存,相当于是空间换时间。
哈希表解决冲突的方式
三种方法,一个是线性探测,一个是二次探测,链地址法。
线性探测:
插入数据发生冲突后,就往后找空位置放入。查找也是一样,先找散列值所指向的槽,找不到往后找。
(缺点:1、不好删除,因为一旦删除后这个位置就会出现空槽,再次查找到该空槽会认为这个数据不存在
2 、线性递增,容易聚集在一起,存入哈希表的记录在表中都连成一片,后续发生冲突的可能性会越大)
二次探测:
跟线性探测差不多,但是是以平方次探测。
链地址法
将所有具有相同哈希地址的而不同关键字的元素连接到同一个单链表中。并且最新的元素插入到链表的前端。
(优点 : 减少堆积,节点空间是动态申请的,适合长度不确定的情况,删除数据也很方便
缺点:指针需要额外的空间,所以元素规模小可以使用线性探测。)
博客园详解哈希冲突解决方法
数组和链表的区别
(1)数组 是将元素在内存中连续存放,可以快速随机访问数据,可以通过下标迅速访问数组中任何元素。但是插入数据和删除数据效率低。因为需要将数据在内存中都要向后移或者前移。
(2)链表 元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起。上一个元素有个指针指到下一个元素直到末尾元素。如果要访问链表中一个元素,需要从第一个元素开始,一直找到需要的元素位置。但是增删数据很方便,只需要修改指针。
牛客面经详解区别
遍历数组比遍历链表来得快一些,为什么
数组内存空间连续,链表不连续,内存空间连续可以一次读取多个数据放到缓冲区之类的地方,不用多次访问。
如何把链表改造成数组一样的内存空间连续
采用内存池的方式,我的想法是申请一块连续内存空间,遍历一遍全部放入这个空间。
堆
https://www.cnblogs.com/hello-shf/p/11393655.html
建立在完全二叉树上的结构。但是本质的存储是数组。但这不妨碍完成树结构,要找爹直接通过下标。Dad_i = (son_i - 1) / 2;
堆是优先队列的幕后人,优先队列默认是大根堆(less< T > ,根最大,堆顶最大)。堆不保证顺序,但是是相对顺序。每次取出的堆顶,都是最大值。但这不意味着其他节点递减,实际上也不是递减。(之前一直在纠结大根和堆顶的区别,,区别个啥啊这是一个东西啊,树的顶不就是根节点么)
插入和上浮(logn)
保证局部顺序的好东西。这里以大根堆演示。
每次插入一个数据,先放到数组最后,然后跟他的父亲比较,如果比父亲大,交换,上浮。
删除和下沉(logn)
每次取出堆顶元素,也需要保证局部顺序性。
原理是取出堆顶后,把数组的最后一个元素放到堆顶,然后跟左右子比较,比自己大的数据就交换自己下沉。
内存
堆和栈的区别
内存分为5个区:堆、栈、自由存储区、全局/静态存储区和常量存储区。
1)申请方式:
栈由系统自动分配和管理,堆由程序员手动分配和管理。
2)效率:
栈由系统分配,速度快,不会有内存碎片。
堆由程序员分配,速度较慢,可能由于操作不当产生内存碎片。
3)扩展方向
栈从高地址向低地址进行扩展,堆由低地址向高地址进行扩展。
4)程序局部变量是使用的栈空间,new/malloc动态申请的内存是堆空间,函数调用时会进行形参和返回值的压栈出栈,也是用的栈空间。
内存管理
内存泄露和内存溢出区别
内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。并非是内存真的消失了,而是因为设计错误失去了对该段内存的控制,因而造成了内存的浪费。
内存溢出指程序申请内存时,没有足够的内存供申请者使用。
内存泄漏出现的时机和原因,如何避免
1、堆内存泄漏
malloc、new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。而且要删除彻底,new[] 就要对应 delete[]
2、没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。
C++内存布局(几大内存区)
一共五大内存区,堆区,栈区,全局/静态区,常量区,代码区。
栈区:存放函数参数,局部变量。
堆区:可由程序员分配释放。
全局/静态区:全局变量和静态变量的存储。
常量区:存放常量字符串。
程序代码区:存放函数体(类的成员函数、全局函数)的二进制代码。
堆区和栈区的区别
1)申请方式 stack 由系统自动分配。比如声明一个局部变量 int a,系统自动为 a 在heap中开辟空间。堆区, 需要程序员自己申请,并指明大小。
2)申请后系统的响应。只要栈的剩余空间大于所申请空间,系统将为程序提供内存。堆区的话有一个记录空闲内存地址的链表,申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,将该结点的空间分配给程序。(对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。)
3)申请大小的限制及生长方向。栈的话栈是向低地址扩展的数据结构,栈顶的地址和栈的最大容量是系统预先规定好的。因此能从栈获得的空间较小 。
堆:堆是向高地址扩展的数据结构,且不连续。用链表来存储的空闲内存地址的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存,比较大。
4)申请效率的比较: 栈由系统自动分配,速度较快。堆的要慢一些。
new/delete 和 malloc/free的区别
new/delete是C++的关键字,而malloc/free是C语言的库函数,后者使用必须指明申请内存空间的大小,对于类类型的对象,后者不会调用构造函数和析构函数。也就是说,new 是建立了一个对象,而malloc 是分配了一块内存。
C++ 11
聊一聊智能指针
(是一个类)
三个,shared_ptr, weak_ptr, unique_ptr。
unique_ptr : 保证同一时间内只有一个智能指针可以指向该对象,对于避免资源泄露,比如new 了没能及时delete 很有用。(不可以赋值,可以使用move() 转移到其他的unique_ptr 。
shared_ptr : 实现共享概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。它使用计数机制来表明资源被几个指针共享,每析构一次,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
weak_ptr : 为配合 shared_ptr 而引入的一种智能指针。用来解决shared_ptr相互引用时的死锁问题。weak_ptr 和 shared_ptr 相互转换,把其中一个转换成w_p,析构的时候得到释放。
shared_ptr
1、引用计数 对于指针指向的对象,这块地址上每多一个指针指向他,计数加一
2、循环引用 两个Shared_ptr 相互引用 。如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,
3 、简单实现主要是构造、赋值和析构
#include <iostream>
using namespace std;
template <typename T>
class Shared_ptr {
public:
// 空参构造 空指针
Shared_ptr():count(0), _ptr((T*)0) {}
// 构造函数 count必须new出来
Shared_ptr(T* p) : count(new int(1)), _ptr(p) {}
// 拷贝构造函数 使其引用次数加一
Shared_ptr(Shared_ptr<T>& other) : count(&(++ *other.count)), _ptr(other._ptr){}
// 重载 operator*和operator-> 实现指针功能
T* operator->() { return _ptr; }
T& operator*() { return *_ptr; }
// 重载operator=
//将一个智能指针赋值给另一个
// 如果原来的Shared_ptr已经有对象,则让其引用次数减一(因为指向另一个,原来的对象引用 --)并判断引用是否为零(为零则释放,调用delete)。
// 然后将新的对象引用次数加一。
Shared_ptr<T>& operator=(Shared_ptr<T>& other)
{
if (this == &other)
return *this;
++*other.count;
if (this->_ptr && 0 == --*this->count)
{
delete count;
delete _ptr;
cout << "delete ptr =" << endl;
}
this->_ptr = other._ptr;
this->count = other.count;
return *this;
}
// 析构函数 使引用次数减一并判断引用是否为零(是否调用delete)。
~Shared_ptr()
{
if (_ptr && --*count == 0)
{
delete count;
delete _ptr;
cout << "delete ptr ~" << endl;
}
}
int getRef() { return *count; }
private:
int* count; // should be int*, rather than int
T* _ptr;
};
auto 关键字
自动推断变量的类型。不可以全是auto,要有迹可循。
lambda表达式会用吗
分为五个部分。
[函数对象参数] (操作符重载函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}
中括号,小括号 ,mutable 或者 exception声明,-> 返回值类型、{函数体}.
1 、中括号必须写,标识一个 Lambda 表达式的开始。中括号里面是捕获列表,可以用来捕获Lambda表达式以外的变量。如果什么都不写,那就是不能捕获外部变量。如果是一个 = 号,那么就是外部变量都是按值捕获,如果是 &,则是按引用捕获。
2、小括号里是参数列表,与普通函数的参数列表一致,如果不需要传递参数,则可以连通括号一起省略。
3 、mutable是修饰符,默认情况下lambda函数总是一个const函数,Mutable可以取消其常量性。
4、返回值类型。 5、函数体,函数的具体实现。
左值和右值以及区别
有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
右值引用
右值引用和移动构造
消除两个对象交互时不必要的对象拷贝,提高效率。
string a 等于一个什么东西,如果要把 a 的值赋给 string b,如果直接写作string b = a,就会调用拷贝构造函数,但是这样就会造成一个不必要的开销,于是有了右值引用,把 a 的内容 偷出来给b
std::move 是获得右值的方式,通过 move 可以将左值转为右值。
引用折叠
左值引用 int & ,右值引用 int &&
如果排列组合,就有四种情况
左值-左值 T& &
左值-右值 T& &&
右值-左值 T&& &
右值-右值 T&& &&
但是不能这么使用。比如我 int & & a;
为什么会有情况?那就要说万能引用了
万能引用:
template<typename T>
ReturnType Function(T&& parem)
{
// 函数功能实现
}
这里如果T 传入的是 int& ,那么参数就变成了 int & && .
那么这个是啥?这时候就要引用折叠了,直接折叠成 int & ;
传入右值或者右值引用,推到折叠出来的没有引用属性(右值嘛)
如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。
完美转发
继续看到上面的万能模板。如果传入的参数是右值。但是最终结果还是左值。
任何的函数内部,对形参的直接使用,都是按照左值进行的。 传入的哪怕是右值。这时候我就有了完美转发 forward ,使得左值还是左值,右值也不会改变
实现:
如果传入右值,假设是不带引用的 Int
int &&(static_cast<int&&>(param)), 然后按照int && 返回,两个右值引用最终还是右值引用。最终保持了实参的右值属性,转发正确。
如果是左值 int&,,折叠后依然是左值
C++类的默认函数有几个
C++11之前,拷贝构造函数(Copy Constructor)、拷贝赋值运算符(Copy
Assignment operator)和析构函数(Destructor)。
C++11之后,新增加了两个函数:移动构造函数(Move Constructor)和移动赋值运算符(Move Assignment operator)。
口诀:构造函数与赋值运算符的区别是,构造函数在创建或初始化对象的时候调用,而赋值运算符在更新一个对象的值时调用。