C++进阶
笔者的宿舍被封起来已经有十天了,我用大概四五天的时间总结了C++重要的知识点,分为初阶和进阶,这些知识点总结的不太详细,但是对于想要短时间复习C++的朋友们来说还是很有帮助的。看之前不妨先点个赞!!!
继承
继承实际上是代码复用的一种方式,通过继承使原有的类进行拓展,然后实现新的类
访问限定符
protected实际上就是为了继承而生的。
对于公有继承,派生类会无条件继承基类的访问限定符,派生类是无法访问基类的私有成员的
对如保护继承,因为保护的优先级大于公有,那么公有的变成保护,保护的还是保护。私有的派生类是在派生类中不可见的
对于私有继承,基类的公有成员变成私有,保护成员也是私有,私有成员不可见
这里的不可见是指,私有的虽然也继承下来了,但是无论是类内还是类外,都是无法被访问的
在实际上我们往往会使用公有继承,因为如果是其他继承,继承下来的成员只能是派生类才能访问,实际中并不好用
切片
切片是指派生类对象可以赋值给基类的对象/基类的指针/基类的引用,但是反过来并不可以,虽然基类的指针可以赋值给派生类的指针,但是要通过dynamic_cast强制转换才行
继承的作用域
子类和父类的成员变量如果一样,那么子类成员将屏蔽父类对同名对象的访问,如果成员函数名字一样那么就是成员函数的隐藏,但是我们还是不要在继承体系里边定义同名的成员
派生类的默认成员函数
实际上派生类会继承默认成员函数,但是在调用时一般是先调用基类的成员函数再调用自己的。对于析构函数,派生类类对象先调用派生类析构再调基类的析构,这样才能保证派生类对象先清理,基类再清理
继承的友元
友元无法被继承,比如基类的友元函数可以访问基类的私有成员和函数,但是却无法访问派生类的。
继承与静态成员
我们之前说过static的对象一旦被定义,那么就只有这一个成员,无论怎么继承,那么还是只有一个
菱形继承
class Person
{
public :
string _name ; // 姓名
};
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承
Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地去使用。
class Student : public Person
{
protected :
int _num ; //学号
};
class Teacher : public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a ;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
菱形继承会产生数据冗余以及二义性问题,想要解决这个问题我们可以使用虚拟继承
class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "peter";
}
虚继承的原理
其实就是最终的assistant,中继承了student的成员,而且多加了一个person的偏移量地址,这个地址可以帮助找到,person的成员,继承的teacher的地方也多加了一个person的偏移量地址,帮助找到公有的person成员
多继承实际上是C++的一个缺陷,底层非常复杂,性能也有问题
多态
相同的行为不同的对象去完成会产生不一样的结果
构成条件
多态是不同继承关系的类对象,去调用同一个函数,产生了不同的行为,比如买票行为,普通人买就全票,学生买就半票,我们来用继承哎实现
class person{
public:
virtual void buyticket()
{
cout<<"all price"<<endl;
}
}
class student:public person
{
public:
virtual void buyticket()
{
cout<<"half price"<<endl;
}
//被调用的一定是虚函数,派生类要对基类的函数继续重写
}
void func(person& people)//必须通过基类的指针或者引用调用虚函数
{
people.buyticket();
}
void test
{
perons p;
func(p);
student s;
func(s);
}
- 必须通过基类的指针或者引用调用虚函数
- 被调用的一定是虚函数,派生类要对基类的函数继续重写
虚函数
被virtual修饰的类成员就是虚函数
虚函数的重写
实际上两个虚函数,构成继承,函数的各种属性都一样,那就是虚函数的重写,我们称子类的虚函数重写了基类的虚函数
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后
基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用
析构函数的重写
前面我们说过默认成员函数也会被继承下来,对于析构函数来说,虽然函数名不同,但是仍然构成了重写但是这里编译器做了一个特殊的处理,这里析构函数的名字统一变成了destructor,
override和final
在实际编程中,我们可能由于疏忽写的函数错了字母,从而该重写的函数没有被重写,我们常常用着两个关键字,帮助用户检测是否重写
final:修饰虚函数,表示该虚函数不可以被重写
override:要是不重写那就会报错
重载与重写还要重定义
重载是指在同一个作用域,函数名相同其他不同就可以发生重载
对于重写来说,是两个不同的作用域一个是基类,一个是派生类,而且两个函数完全相同,而且是虚函数
重定义,如果基类和派生类的两个函数一样,但是不是虚函数,那就是重写了
抽象类
虚函数后面写上=0,这个函数就是纯虚函数,包含纯虚函数的类教抽象类,也就是接口类,抽象类因为函数没有定义,无法实例出对象。只有重写虚函数,让这个虚函数被定义才能让派生类实例出对象。抽象类体现处理接口继承
多态的原理
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
问这个base的大小是?
是八个字节,实际上,只要类中含有虚函数那么就隐藏了一个指针,这个指针叫做虚表指针,一般放在对象的前面,这个虚表指针指向虚表,虚表实际上保存了来自基类的函数指针。
总的来说,当派生类继承了基类并且有虚函数,那就把基类虚表拷贝下来,如果派生类重写了基类的虚函数,那就把被重写的基类函数指针给覆盖掉,派生类自己新增加的函数,按次序增加在派生类虚表的最后。
虚函数在哪?虚表又在哪里?
虚函数就是和普通函数一样,存在代码段里边,而虚函数表实际上是函数指针的集合,虚表同样存在代码段
满足多态之后的函数调用,不是在编译时确定的,是允许起来以后到对象中去找的,不满足多态的函数是编译时确认好的。
如何打印出虚表里面的虚函数指针
我们正常用VS进行调试的时候发现只能看见两个指针,这是VS的一个小小的不足,但是我们仍然能用自己的聪明才智把它打印出来,我们直到对象的第一个存的就是虚表指针,所以我们去出对象的地址,把它强转成int*,再进行解引用,这样我们就的得到了虚表指针,我们把这个指针强转成一个函数指针,然后就可以通过这个函数指针来打印虚函数指针了。
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Base b;
Derive d;
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
return 0;
}
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
二叉搜索树
一棵二叉树左子树的节点都小于根节点,右子树的节点都大于根节点
二叉搜索树的查找
很简单按照定义查找即可
二叉树搜索的插入
也很简单,如果是空树那就直接插入,否则按照二叉搜索树的性质查找插入位置插入新节点
二叉树搜索的删除
删除稍微麻烦一些,如果是删除的节点没有孩子节点,或者是只有左节点,或者是只有右节点,那么直接删除这个节点,再让这个节点的父节点指向它的孩子节点
如果被删除的节点左右孩子都有,那么就优点麻烦,因为我们要保持节点顺序不变,让这个节点的右节点左孩子代替这个被删除的节点,然后让代替节点的孩子连在被删除节点的右孩子的左边,听起来有点绕。
实现二叉搜索树
template<class K, class V>
struct BSTreeNode
{
BSTreeNode<K, V>* _left;
BSTreeNode<K, V>* _right;
K _key;
V _value;
//pair<K, V> _kv;
BSTreeNode(const K& key, const V& value)
:_left(nullptr)
, _right(nullptr)
, _key(key)
, _value(value)
{}
};
template<class K, class V>
struct BSTree
{
typedef BSTreeNode<K, V> Node;
public:
BSTree()
:_root(nullptr)
{}
bool Insert(const K& key, const V& value)
{
if (_root == nullptr)
{
_root = new Node(key, value);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key, value);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
// 找到,准备开始删除
if (cur->_left == nullptr)
{
if (parent == nullptr)
{
_root = cur->_right;
}
else
{
if (parent->_left == cur)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (parent == nullptr)
{
_root = cur->_left;
}
else
{
if (parent->_left == cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
}
else
{
Node* minParent = cur;
Node* min = cur->_right;
while (min->_left)
{
minParent = min;
min = min->_left;
}
cur->_key = min->_key;
cur->_value = min->_value;
if (minParent->_left == min)
minParent->_left = min->_right;
else
minParent->_right = min->_right;
delete min;
}
return true;
}
}
return false;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
private:
Node* _root;
};
二叉搜索树的应用
- K模型:只有key作为关键码,结构中只需存储key就可以了,关键码就是搜索到的值
- KV模型,每一个关键码对应一个value,通过寻找关键码来找到value,比如英汉词典
二叉搜索树的性能分析
插入和删除都要查找,所以查找效率就代表了二叉搜索树的性能
对于n个节点的二叉树,我们暂且认为没有元素被查找的效率相等,那么认为二叉搜索树的效率其实就是他的深度O(logn)
但是如果对于特定的插入顺序,会得到效率很差的表现O(n)
为了解决二叉搜索树的缺陷,我们引入AVL树
map和set
图和集合实际上是关联容器,里边存的是键值对,在数据检索的额时候,比序列容器的效率是更高的
set
set是按照一定次序存储元素的容器(互斥集合),在set中key就是value,set的底层是红黑树实现的,里面存在的是<value,value>的键值对,set的元素是不能重复的,因此可以通过set去重,set的元素不能直接去除,原因是因为指向set的迭代器是指向常量的迭代器,所以要进行修改就要先删除再添加
unordered_map
和set的功能一样,但是底层实现是不一样的,它的底层实际上是哈希桶效率相对较高
map
map就是一个关联容器,它按照一定的次序按照key值来比较键值key通常用于排序和唯一地标识元素,存储键值对。map的底层实际上就是红黑树,map的一个重要的用法就是,可以支持下标访问符,也就是在[]里边放入key,就可以找到key对应的value
在元素访问时,有一个与operator[]类似的操作at()(该函数不常用)函数,都是通过key找到与key对应的value,如果这个key不存在,那么下标操作符可以插入key和默认的value,但是at()就直接抛异常
unordered_map
底层是哈希桶实现,效率更高
multiset
和set相比,里面的元素是可以重复的。用迭代器遍历可以得到有序的关于key的序列
multimap
同样的,多个键值对之间的key是可以重复了,用迭代器遍历可以得到有序的关于key的序列
AVL树
二叉搜索树虽然可以缩短查找的效率,但是存在严重缺陷,如果数据接近有序,那么二叉搜索树的深度会很深,查找元素就相当于再顺序表中搜索元素,效率很低。因此AVL树应运而生,它的原理是当在二叉搜索树中插入新节点后,保证每个节点的左右子树的高度差不超过1,就可以降低树的高度。
AVL树的节点
template<class T>
struct AVLTreeNode
{
AVLTreeNode(const T& data)
: _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr)
, _data(data), _bf(0)
{}
AVLTreeNode<T>* _pLeft; // 该节点的左孩子
AVLTreeNode<T>* _pRight; // 该节点的右孩子
AVLTreeNode<T>* _pParent; // 该节点的双亲
T _data;
int _bf; // 该节点的平衡因子
};
左孩子,右孩子,双亲,平衡因子
AVL树的插入
- 按照二叉搜索树的方式插入新节点
- 调节节点的平衡因子
bool Insert(const T& data)
{
// 1. 先按照二叉搜索树的规则将节点插入到AVL树中
// ...
// 2. 新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否破坏了
// 的平衡性
/*
pCur插入后,pParent的平衡因子一定需要调整,在插入之前,pParent
的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:
1. 如果pCur插入到pParent的左侧,只需给pParent的平衡因子-1即可
2. 如果pCur插入到pParent的右侧,只需给pParent的平衡因子+1即可
此时:pParent的平衡因子可能有三种情况:0,正负1, 正负2
1. 如果pParent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功
2. 如果pParent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更新成正负
1,此时以pParent为根的树的高度增加,需要继续向上更新
3. 如果pParent的平衡因子为正负2,则pParent的平衡因子违反平衡树的性质,需要对其进行旋转
处理
*/
while (pParent)
{
// 更新双亲的平衡因子
if (pCur == pParent->_pLeft)
pParent->_bf--;
else
pParent->_bf++;
// 更新后检测双亲的平衡因子
if (0 == pParent->_bf)
break;
else if (1 == pParent->_bf || -1 == pParent->_bf)
{
// 插入前双亲的平衡因子是0,插入后双亲的平衡因为为1 或者 -1 ,说明以双亲为根的二叉树
// 的高度增加了一层,因此需要继续向上调整
pCur = pParent;
pParent = pCur->_pParent;
}
else
{
// 双亲的平衡因子为正负2,违反了AVL树的平衡性,需要对以pParent
// 为根的树进行旋转处理
if(2 == pParent->_bf)
{
// ...
}
else
{
//...
}
}
}
return true;
}
AVL 树的旋转
这里不太想写了,很麻烦,之前写过一次。
红黑树
之前也写过,真的很麻烦
哈希
哈希是校招中十分重要的内容。
unordered_map
底层用哈希桶实现,通过key访问单个元素比map要快,但是如果迭代就没有map快
底层结构
在顺序和平衡树里面,元素的关键码和位置没有对应的关系,因此查找一个元素的时候要经过多次比较
我们还有更好的搜索方法,就是让关键值通过某种函数的映射到元素的储存位置,那么查找的时候通过该函数可以很快找到该元素。
插入的时候根据关键码计算存储位置再进行插入,搜索的时候同样进行计算找到对应存储位置,若关键码相等,则搜索成功。
这种方法不用经过比较,因此效率很高。
哈希函数
关键值和存储位置的映射关系就是哈希函数,构造出来的结构称为哈希表。
最简单的一种哈希函数是hash(key)=key % capacity
但是这种哈希函数存在巨大的问题,也就是多个关键值映射到同一位置,就会存在哈希冲突
解决哈希冲突
解决哈希冲突的有效方法就是改变映射关系
- 直接定制法:hash(key)=A*key+B,使用场景适合查找比较小且连续的情况
- 除留余数法:取一个不大于m的质数p作为除数,hash(key)=key%p,关键码转换为哈希地址
- 还有其他的很多方法
哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法彻底解决
闭散列和开散列
闭散列就是开放定址法,如果哈希冲突,如果哈希表没有被填满,那么就是把key存在下一个没有被占用的位置,所以如何寻找下一个空位置
线性探测
从发生冲突位置开始,依次向后探测,直到寻找下一个空位置为止。具体来说就是,插入的时候计算映射的地址,如果该地址已经冲突,那么就找下一个位置,直到不发生冲突就插入
闭散列处理哈希冲突的时候,不能随便删除哈希表已有的元素,否则会影响其他的元素的搜索,比如我删除元素4,我在查找44的时候找到了一个空位,但是线性探测的原则是空位是要么没有元素,要么要被占用,那么就搜不到44了
所以我们删除元素的时候不是物理删除,而是用标记的方式伪删除
enum State{EMPTY, EXIST, DELETE};
哈希表什么时候扩容
我们前面所述,当一个散列表被填充,填充的越多,发生哈希冲突的概率越大,那么每次插入或者寻找付出的代价越大,所以当填入的元素个数/散列表的长度的值A的值达到一定值的时候,我们要对散列表进行扩容 A通常取0.75
二次探测
线性探测的缺陷是产生哈希冲突的元素堆在一块,冲突越多那么效率越低,那么我们为了解决这个问题,找下一个位置的方法:Hi=(H0+i^2)%m,其中i为1,2,3。。。
当表的长度是质数而且装载因子A不超过0.5的时候,新的表项一定能插入,而且任何一个位置不会被探查第二次,因此只有表中有一半的空位置,就不会存在表满的问题,但是在插入的时候必须要确保表的装载因子A不超过0.5
因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷
开散列
开散列法又叫做开链法,首先对关键码的集合用散列函数计算地址,具有相同地址的归到一个集合里面,每个集合称为一个桶,桶里面的元素连成一个单链表,链表的头节点存在哈希表里面,也就是说所有冲突的元素放在哈希桶里面
开散列的增容
桶的个数是一定的,随着元素的不断插入,每个桶的元素个数不断增多,极端情况下,会影响哈希表的性能,开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,开散列的最好情况是,每个哈希桶刚好挂一个节点,再继续插入元素的时候每一都会产生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。
开散列的思考
只能存储key为整形的元素,其他的类型呢,实际上对于其他的类型可以设计函数将其他类型转换为唯一的对应的整形元素。比如字符串类型
class Str2Int
{
public:
size_t operator()(const string& s)
{
const char* str = s.c_str();
unsigned int seed = 131; // 31 131 1313 13131 131313
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + (*str++);
}
return (hash & 0x7FFFFFFF);
}
};
哈希的应用
位图:
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
这是腾讯的一道面试题,最直接的想法是把这些数字存在set里面,但是一个整数四个字节,4*40亿的话,1G大约是十一亿个字节,这就用到了15六个G的内存了,然而整个地址空间才4个G,所以我们采用其他方法,我们可以建立一个位图,一个字节是是8个比特位,每个比特位代替八个数,用0或1来表示是否存在。这样每个数字占有的空间变成了原来的三十二分之一
位图的应用
- 快速查找某个数据是否在一个集合中
- 排序
- 求两个集合的交集并集
- 操作系统中的磁盘块标记
C++11
C++11是对C++古老版本的一个修正和添加,C++11可以更好的用于系统开发,和库开发,语法更加简单,更加好用。
列表初始化
C++11扩大了可以用花括号进行列表初始化的范围,可用于所有的内置类型和用户自定义的类型,使用列表初始化是可以旋转加上或者不加=号
int main()
{
// 内置类型变量
int x1 = {10};
int x2{10};
int x3 = 1+2;
int x4 = {1+2};
int x5{1+2};
// 数组
int arr1[5] {1,2,3,4,5};
int arr2[]{1,2,3,4,5};
// 动态数组,在C++98中不支持
int* arr3 = new int[5]{1,2,3,4,5};
// 标准容器
vector<int> v{1,2,3,4,5};
map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}};
return 0;
}
自定义类型的列表初始化
1.标准库支持单个对象的列表初始化
2.多个列表的初始化,要给类添加一个带有初始化列表的构造函数即可,initializer_list是系统自定义的类模板,这个类模板主要有三个方法,begin(),end(),以及获取区间元素个数的方法size()
auto
可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便.
decltype
根据表达式的实际类型来推演定义变量时所用的类型,比如我有一个复杂的类型B 我想定义一个相同类型的A
decltype(B) A;
typeid()
获取对象的实际类型
范围for
略
final和override
用于多态,修饰虚函数使虚函数被强制重写,或者不能被重写
智能指针
我们知道除了静态区,和栈空间,每个程序还存在一个内存池,名字叫做堆,堆存放了动态分配对象的空间。
我们还知道我们在堆中开辟一块空间,先new一块空间并返回指向空间的指针,然后delete销毁对象,释放内存,然而实际上,我们常常顽疾释放内存导致内存泄漏,或者这个空间明明有对象在使用,却错误的被释放了。
为了解决这个问题,我们引入智能指针
auto_ptr
这是所有智能指针的始祖,它的优点就是实现了可以自动对于内存进行析构,但是它也有明显的缺点,原因在于它的一个重要特性,std::auto_ptr支持拷贝构造,而且支持operator=
比如我们定义一个自动指针A赋值给另一个自动指针B,那么A的指针就变成空了,无法再对A进行解引用了
td::vector<std::auto_ptr<People>> peoples;
// 这里实例化多个people并保存到数组中
...
std::auto_ptr<People> one = peoples[5];
...
std::cout << peoples[5]->get_name() << std::endl;
上面程序如果将std::auto_tr类型替换为原始指针,就不会有问题。但是这里却会导致程序报段错误崩溃!问题就出在第二行,operator=将指针的所有权转移了给了one
void do_somthing(std::auto_ptr<People> people){
// 该函数内不对people变量执行各种隐式/显示的所有权转移和释放
...
}
std::auto_ptr<People> people(new People("jony"));
do_something(people);
...
std::cout << people->get_name() << std::endl;
这里因为传参进行拷贝赋值,指针的所有权转给了函数里的指针了,那也无法进行下面的解引用了。
unique_ptr
因为自动指针的缺陷,我们往往使用unique_ptr,不仅加入了移动语义的支持,同时也关闭了左值拷贝构造和左值赋值的功能。任何可能被共享的操作都是不允许的,但是可以移动,也就是可以转移控制权,
虽然unique_ptr独享对象,但是也可以移动,即转移控制权。如:
std::unique_ptr<int> up1(new int(42));
std::unique_ptr<int> up2(up1.release());
up2接受up1 release之后的指针,或者:
std::unique_ptr<int> up1(new int(42));
std::unique_ptr<int> up2;
up2.reset(up1.release());
或者使用move:
std::unique_ptr<int> up1(new int(42));
std::unique_ptr<int> up2(std::move(up1));
share_ptr
我们使用share_ptr 定义一个指针指向一块内存的时候,可以有很多share_ptr智能指针指针指向这块内存,它的原理实际上用引用计数来完成对内存的管理,每当一个指针指向这块内存那么引用计数加一,每当一个指针被销毁引用计数-1,直到引用计数的值为0,销毁这块内存。
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
};
namespace wzy {
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pRefCount(new int(1))
,_pmtx(new mutex)
{
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
,_pmtx(sp._pmtx)
{
++(*_pRefCount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (this->_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
~shared_ptr()
{
if (--(*_pRefCount) == 0 && _ptr)
{
cout << "delete" << _ptr << endl;
delete _ptr;
delete _pRefCount;
_ptr = nullptr;
_pRefCount = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
void Release()
{
_pmtx->lock();
bool flag = false;
if (--(*_pRefCount) == 0)
{
delete _ptr;
delete _pRefCount;
flag = true;
}
_pmtx->unlock();
if(flag)
delete _pmtx;
}
void AddRef()
{
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
int useCount()
{
return *_pRefCount;
}
private:
T* _ptr;
//static int _refCount = 0;
int* _pRefCount;
mutex* _pmtx;//只有用指针才能
};
}
weak_ptr
共享指针实际上有缺陷会产生循环引用
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2;
n2->_prev = n1;
这两个指针的析构会产生互相依赖,n2的析构依赖n1,n1的析构依赖n2,这样谁都析构不了。
所以我们引出弱指针
weak_ptr是shared_ptr的小弟,我们知道shared_ptr不仅能指向一块内存,而且可以管理这块内存,也就是是随着共享指针生命周期的结束,内存随之被销毁,但是弱指针的生命周期结束后,所指向的内存完好完好无损,由于在weak_ptr指针生命结束之时,不会对指向内存产生任何影响,因此不会出现“上述shared_ptr引发的环形引用的异常错误”。如果将上述例子中,shared_ptr换做weak_ptr结构将会发生变化
静态数组array
array实际上就是比普通数组多了成员函数,和全局函数,而且比普通数组更加安全。
array 容器中包含了 at() 这样的成员函数,使得操作元素时比普通数组更安全。数组只能通过下标访问,在编写程序的时候很容易出现越界的错误。
forward_list
它和list底层差不多,但是它的底层是用单链表来实现的,这样的化这个容器就不支持双向迭代器了,只能向前迭代。
那它的存在有什么必要呢?
显然双链表的构成更加复杂,单个节点的大小更小,空间利用率更高这就是它的意义
效率高是选用 forward_list 而弃用 list 容器最主要的原因,换句话说,只要是 list 容器和 forward_list 容器都能实现的操作,应优先选择 forward_list 容器。
默认成员函数控制
C++中对于空类编译器会生成一些默认的成员函数,如果类中显式定义了,编译器不会生成默认版本,最常见的是声明了带参数的构造函数,必要的时候要定义无参的构造函数,这样就很混乱,我们让程序员自己来控制要不要编译器生成。
=default
在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数
=delete
如果我们想要限制某些默认函数的生成,给该函数设置成私有,但是这样的话,其他人调用就会报错。在C++11中,将该函数的声明加上=delete,该语法指示编译器不生成对应函数的默认版本,=delete修饰的函数称为删除函数。
右值引用
为了提高程序运行效率,C++11引入了右值引用,右值引用也是别名
const int&& ra=10;
一般认为放在等号右边的,或者不能取地址的称为右值,可以放在=左边的或者能取地址的称为左值。
一般来说普通的类型可以取地址认为左值,对于const修饰的常量虽然可以取地址,但是不能被修改,我们还是按照右值对待。一个临时变量我们也认为是右值,引用我们也认为是左值
普通类型引用只能引用左值,不可以引用右值
右值引用只能引用右值,例外是const引用既可以引用左值又可以引用右值
值的形式返回对象的缺陷
一个类设计到资源管理,用户必须显式提供拷贝构造,赋值运算符的重载,以及析构函数,否则编译器将会自动生成一个默认的,但是默认的遇到拷贝对象或者对象之间的赋值就会出错。
比如我现在有一个值返回的函数,返回值之前要构造一个临时的值,然后销毁函数里面的返回值变量,然后把刚刚的临时值赋值给我们的真正要接收的值,再销毁临时值,这样产生了三个一模一样的变量,可以说非常冗余
所以我们提出了移动语义
移动语义
将一个对象的资源移动给另一个对象中的方式,那么这个对象的资源就没了。
右值引用引用左值
右值引用只能引用右值,但是我们想想引用左值呢,那么就需要使用move()把左值变成右值,它的名字具有迷惑性,它不搬运任何东西,唯一的功能就是将一个左值强制转化为右值引用。然后实现移动语义
完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将函数传递给函数模板中调用的函数
C++通过forward函数实现完美转发
void Fun(int &x){cout << "lvalue ref" << endl;}
void Fun(int &&x){cout << "rvalue ref" << endl;}
void Fun(const int &x){cout << "const lvalue ref" << endl;}
void Fun(const int &&x){cout << "const rvalue ref" << endl;}
template<typename T>
void PerfectForward(T &&t){Fun(std::forward<T>(t));}
int main()
{
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}
右值引用的作用
1.实现移动语义
2.给中间临时变量取别名
lambda
lambda表达式实际上简化匿名函数的实现方式
中括号里面是捕获列表,小括号里面是函数参数,大括号里面是函数体
[=] 以值捕获所有,[&]引用方式捕获 ,[this]表示以值传递方式捕捉当前的this指针。捕捉父作用域的局部变量
函数对象和lambda表达式
class Rate
{
public:
Rate(double rate): _rate(rate)
{}
double operator()(double money, int year)
{ return money * _rate * year;}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lamber
auto r2 = [=](double monty, int year)->double{return monty*rate*year; };
r2(10000, 2);
return 0;
}
使用方式上,函数对象于lambda表达式一样,实际上底层编译器对于lambda表达式的处理上,完全就是按照函数对象的方式处理的如果定义了一个lambda表达式,编译器会自动生成一个类,类中重载了operator();
C++的类型转换
static_cast
static_cast<new_type> expression
相当于C语言的强制类型转换,也用来强迫隐式转换,但是无法用于多态的转换
char a = 'a';
int b = static_cast<char>(a);//正确,将char型数据转换成int型数据
double *c = new double;
void *d = static_cast<void*>(c);//正确,将double指针转换成void指针
int e = 10;
const int f = static_cast<const int>(e);//正确,将int型数据转换成const int型数据
const int g = 20;
int *h = static_cast<int*>(&g);//编译错误,static_cast不能转换掉g的const属性
dynamic_cast
将一个基类对象指针转换为继承类的指针,dynamic_cast会根据基类指针是否真正指向继承类指针做相应处理
dynamic_cast<type*>(expression) //type必须是一个指针
dynamic_cast<type&>(expression) //type必须是一个左值
dynamic_cast<type&&>(expression) //type必须是一个右值
- expression的类型是是目标类型type的公有派生类。
- expression的类型是目标type的共有基类。
- expression的类型就是目标type的的类型
const_cast
用于修改对象的常属性和volatile属性,可以将常量指针变成非常量的指针,把常量引用变成非常量引用,并且指向的对象不变。
#include <iostream>
int main()
{
const int a = 12;
const int *ap = &a;
int* new_ap = const_cast<int*>(ap);
*new_ap = 10;
std::cout<<"const a =" <<a <<"const a adress="<< &a<<std::endl;
std::cout<<"new ap =" <<*new_ap <<" ap address="<< new_ap<<std::endl;
}
输出:
const a =12 const a adress=0x7ffee49388ac
new ap =10 ap address=0x7ffee49388ac
是不是很神奇,a是一个const值,按照C++的规则,a是不可以修改的。但是我们使用了const_cast突破了这种限制,修改了a的地址所存储的值。之后打印出来a 和指针ap 地址是一致的,但是值确不一样。
reinterpret_cast
首先从英文字面的意思理解,interpret是“解释,诠释”的意思,加上前缀"re",就是“重新诠释”的意思,reinterpret_cast整体的意思就是 "重新解释的转型"。
reinterpret_cast运算符是用来处理无关类型之间的转换;它会产生一个新的值,这个值会有与原始参数(expressoin)有完全相同的比特位。所以reinterpret_cast是个很强大的运算符,因为它可以无视种族隔离,随便搞
这种类型转换是一种十分强大的类型转换,有时候的转换是不可逆的,所以慎用。
#include <iostream>
using namespace std;
int main(int argc, char** argv)
{
int num = 0x00636261;//用16进制表示32位int,0x61是字符'a'的ASCII码
int * pnum = #
char * pstr = reinterpret_cast<char *>(pnum);
cout<<"pnum指针的值: "<<pnum<<endl;
cout<<"pstr指针的值: "<<static_cast<void *>(pstr)<<endl;//直接输出pstr会输出其指向的字符串,这里的类型转换是为了保证输出pstr的值
cout<<"pnum指向的内容: "<<hex<<*pnum<<endl;
cout<<"pstr指向的内容: "<<pstr<<endl;
return 0;
}
结语
写到这里就不在写了,重点内容已经写了差不多了,笔者在准备找实习的时候,写下这篇万字长文,祝我能拿到一个offer吧,并能顺利入职!