#1 ========================================================================================================
空容器:没有任何元素,只表明当前容器内存放什么样的数据
vector<A> v1;
deque<int> d1;指定容器容量:指定容量,每个元素都使用默认值
vector<A> v1(10); //10个元素,每个都使用A的默认构造指定容量和初值:指定容量,给所有元素相同的初值
vector<A> v1(10,A(1)); //使用临时变量A(1),给容器内的10个元素赋初值
(需要A有响应的转换/构造函数,比如这里A就要有 A(int) 构造函数)
使用两个迭代器之间的值:所有容器都有一个构造函数,那就是接收两个迭代器,使用这两个迭代器中间的值来构造自己
vector<A> v2(v1.begin(),v1.end()); //v2使用v1的所有值来构造自己使用其他容器构造:所有容器都具备拷贝构造函数
vector<A> v2(v1); //使用v1来构造v2使用其他容器赋值:所有容器都具备赋值拷贝函数
vector<A> v2 = v1; //使用v1给v2赋值#2 ========================================================================================================
容器的移除元素动作不会调用析构函数,因此如果把对象从容器中移除,请确保使用了 共享指针 或者 手动释放对象。
#3 ========================================================================================================常用算法:
https://docs.microsoft.com/zh-cn/previous-versions/yah1y2x8(v=vs.120)?redirectedfrom=MSDN
#4 ========================================================================================================常用的函数对象:
#include <functional>常用函数对象的adaptor : 一些对adaptor再包装的函数对象
(!!!)#5 ==============================================================================================
Iterator inserter 迭代器插入器
上面提到了,可以使用 拷贝构造 和 赋值拷贝 来给新的容器赋值/初始化。这个时候被赋值的容器会自动增长到
实参的大小。比如:v1有10个元素,v2是空容器(或小于10),这个时候 拷贝构造 和 拷贝赋值 都会让v2变成10个元素。
可见,STL在实现容器模板类的时候考虑到了自动扩容的问题。但是,如果拷贝和移动相关的泛型算法缺没有考虑到这个问题。还是上面的例子,如果使用copy将 v1 的 2~8号元素
copy给v2 ,则会 coredump,因为v2 是空的(或小于 7的容量)。
(!)这说明,泛型算法的不具备扩容能力。我们在使用copy相关的拷贝/移动算法时,必须保证目的端有足够的容量来接纳
即将到来的数据源。
这个陷阱很危险!!!根本原因;之所以会有上面的问题,在于copy这些算法的本质是使用了模板参数类的赋值运算符作为底部支撑,因此
在解引用赋值的时候会出现解空地址的问题。
那么如果来修补这个问题???STL提供了三种 adaptor 修饰 copy类函数的第三个参数(目的容器)。
1) back_inserter(v1) //在目的容器的尾端增加将要被赋值的元素,比如v1有10个元素,这10个全
//都拷贝,那么按照之前的说法,v2的容量至少要是10,但是使用这个adapter
//以后就不需要考虑这个问题了,adapter会自己解决扩容的问题。
// copy(v1.begin(),v1.end(),back_inserter(v2)); 把v1的所有内容追加到v2尾部
//,会保留v2的原有内容
2) inserter(v1,v1.begin()++) //在指定位置插入,copy(++v1.begin().--v1.end(),inserter(v2,++v2.begin()))
//将v1 的 第2 ~ 倒数第2 的元素拷贝到 v2的第2个元素位置,v2的其他元素被排
//到 v1 拷贝过来的元素后面。
3) front_inserter(v1) //同上,在首部插入,注:考虑到容器的数据结构,此适配器只能作用于可头部增加的
//容器————>list 和 deque
#6 ========================================================================================================mutable的潜台词:
mutable用来解除const的限制,比如我们有一个const实例 const A c_a ; 那么我们无法通过A的成员函数
来修改其成员变量,即便这个成员变量是public的也不行。那么我们如何应对这种场景呢,毕竟实例一旦
被定义成const就表示其中所有成员都是const的,这个时候就可以用mutable修饰想要特殊处理的成员变量。
那么这个时候就可以修改了,即便是cosnt 实例也不影响。#7 ========================================================================================================
拷贝构造 、拷贝赋值、移动构造、移动赋值 ,这四种 复制类 的动作,都需要在实施 复制 之前对现有对象和
目标对象进行重复检查,即 “不能把自己复制给自己” --->if(this!=&target) //如果不相同
{
//复制值的动作
}
return *this;#8 ========================================================================================================
递增/递减 的前置 和 后置
前置:operator++() operator--()
后置:operator++(int) operator--(int)从众多开源项目的源码中也可窥见,前置用的比较多,因此在编译器的规则中,也将前置动作视为惯用的,这
也便于助记,即前置的重载括号内没有东西,后置重载括号内有int
#9 ========================================================================================================指向 类成员函数的指针:
指向普通函数的指针很简单,指明返回值和入参列表的类型即可,然后通过通过取值 * 或 括号() 调用即可:
void func(int){
....
}
typedef void(*pf)(int);
pfunc pf_1 = func();
类比,指向类成员函数的指针需要注意如下几点:
1)需要遵循类的 访问权限控制,即外部指针 只能指向类的 public成员函数,不能指向private,但是如果指针就是
类的成员,则可以指向private成员函数
2)定义时,需要指明类名,typedef void (A::*pf)(int);
3)使用时,需要 使用 .* 或者 ->* ,或者如果是 静态成员函数被指向了,那么可以直接 A::* 。 即需要绑定到指定
的类实例才能调用(静态成员函数除外)
4)成员函数地址给指针时要用取地址运算符例子:
typedef bool(A::*p_A_fun)(void); //能够 指向 A中所有 “bool返回值,无入参” 成员函数的指针类型
p_A_fun p_pub_func;
p_pub_func = &A::pub_func; //让p_pub_func指向pub_func成员函数
A a;
A* pa = new A();
(a.*p_pub_func)(); //局部实例访问
(pa->*p_pub_func)(); //堆实例访问
(A().*p_pub_func)(); //临时实例访问
具体参考 code-cpp 中 “指向成员函数的指针”
#10 ========================================================================================================虚函数有两种形态:
1.有实现 (普通虚函数)
2.无实现(也可以有),同时需要指定为纯虚 =0 (纯虚函数)(!!!)
父类如果有普通虚函数(非=0,且有自己的实现),那么它的潜台词是:所有继承我的子类,都可以选择重写这些普通虚函数。
父类如果有纯虚函数(=0),那么它的潜台词是:所有继承我的子类,都必须重写这些虚函数,注意,这里强调的是子类必须
重写,而没有强调自己是否实现了。
注:纯虚函数 和 普通虚函数 的区别仅仅是 告知子类 “是否必须重写” , 而没有告知子类 “父类是否有自己的实现”。
因此。可以在父类中给纯虚函数以实现(但是,父类还是不能实例化,但凡包含纯虚函数的父类都不能实例化),子类
可以通过 B::func() 来调用这些有实现的纯虚函数。子类继承基类的虚函数也是虚函数,不需要手动指定virtual,默认是虚函数,当然,也可以加
PS:虚函数与纯虚函数的区别就在于,是否要求子类必须实现自己,其他并无差异,附加影响就是有纯虚
函数的类不能由程序员创建(可以在子类的初始化列表中创建)。#11 ========================================================================================================
子类的成员函数中可以自由访问 基类的 public 和 protected 成员,仿佛这些成员就是自己的,如果是虚函数或者是
函数函数重载,那么如果期望调用不同的实现,则需要加上类名:: , 如果不存在重载问题,则直接使用即可。ps :成员函数都有一个隐藏参数,即第一个参数 this,用来指向当前函数的调用者,那么如果在
成员函数里调用了另外一个虚成员函数,这其实相当于 this->vfunc() , 即调用调用者的虚函数
,那么这就存在 动态绑定(多态)问题了,因此这个行为是运行时才能决定的。如果我们想在
编译时就决定,那么只需要使用class scope运算符 :: 即可。
#12 ========================================================================================================用 reference 而不是 pointer 作为函数入参的一个原因:
reference永远无法指向空对象,而pointer可能是空指针,因此,我们使用reference就不需要判断是否为空的问题
用 pointer 而不是 reference 作为函数入参的一个原因:
reference一旦指定就无法变更指向,当然,但部分情况下函数入参都不会变来变去,毕竟传入的参数都变来变去还
怎么运算
#12 ========================================================================================================纯虚类(抽象类) 不能被手动实例化,但是在其子类被实例化的时候,编译器会生成其对象。因此,如果抽象类有
成员变量,那么可以给抽象类定义一个有初始化列表的构造函数,然后在其子类的构造时调用即可。
#13 ========================================================================================================针对抽象类能否收动创建的一些思考:
抽象类,是指包含了纯虚函数的类(不管此函数是否有实现,只要被=0标注了就是),那么编译器就认为这种类是不完整的
类,因此不允许程序员自行开辟空间来创建实例。但是如果抽象类作为基类被继承了,根据子类构造的执行流程,基类
的构造函数会被调用,那么这就相当于编译器可以调用抽象类的构造函数(程序员不能创建实例),即可以创建抽象类
实例,只不过这个是存在于子类的空间中。#14 ========================================================================================================
RTTI 执行期的类型鉴定机制
使用 typeid 作用与 对象(注意,不是指针)上,可以获得一个 type_info 对象,这个对象中存放了当前实例的
所有信息,其中就包裹一个 存放了 类名的字符串,可以使用name()函数来读取这个字符串。比如 typeid(*this); 可以在成员函数中获取当前成员函数调用者的相关信息
#15 ========================================================================================================
非虚函数 跟着指针走,因此如果想通过基类指针访问子类实例的非虚函数,就需要先把指针转型:
Animal* pAnimal = new Dog();
Dog* pdog = static_cast<Dog*>(panimal);注:上面的转型是,把基类指针转换成子类指针,在继承图上来看是自上而下的(基类在图中处于上层),因此叫做向下转型。
向下转型是危险的,编码者必须知道指针确实是指向一个子类实例,如果指向的是一个基类实例,那么这种转型将出错。
向上转型,把一个子类指针转换成基类指针,这种转型始终是安全的,因为基类实例在内存层面始终处于子类实例的内部。
通常在进行向下转型之前,需要先通过typeid来判断下指针指向的实例到底是子类实例还是基类实例,如果是基类实例,那么
就表示其不能被基类指针指向,也就不能进行向下转型。ps:转型涉及两个要点,1)指针类型,2)指针指向的实例类型。而转型是指指针类型的转换,但是指针指向的实例类型
直接决定了指针类型的转换是否安全合法。
#16 ========================================================================================================
dynamic_cast上面说了static_cast转型的问题。static_cast属于“强制”转换,不会进行任何判断,所以不安全。
dynamic_cast内部实现会先用typeid来判断是否可转换,如果可以转换则转换,否则返回0,同时转化动作也不会发生。
可以认为 dynamic_cast 是对 static_cast 的一种封装。
(!)注:如果想使用dynamic_cast函数,则操作的实例必须有虚函数,因为dynamic_cast需要去虚函数表中
搜集数据来完成自己的功能。
dynamic_cast不能用作普通类型转换。
(!!!)
#17 ========================================================================================================
四种 caststatic_cast主要用于
1.基本类型的转换,比如int转char
2.类的上行转换,子类的指针或者引用转换为基类(安全)
3.类的下行转换,基类的指针或引用转换为子类(不安全,没有类型检查)dynamic_cast用于类的指针或引用的转换
1.类的上行转换,子类的指针或引用转化为基类(安全)
2.类的下行转换,基类的指针或引用转化为子类
(因为有类型检查所以是安全的,但类型检查需要运行时类型信息,这个信息位于虚函数表中,所以必须要有虚函数)const_cast 主要是用于改变类型的常量属性:常量指针转化为非常量指针;常量引用转化为非常量引用;常量对象转化为非常量对象
reinterpret_cast用于非关联类型的转换,操作结果是一个指针到其他指针的二进制拷贝,没有类型检查。
#18 ========================================================================================================
局部资源管理器在使用一些包含资源分配和释放的代码时,可能在分配了内存后,释放内存前这中间的某个位置出现了异常,这就
会导致后面释放资源的代码不会执行。解决上述问题有3种方法:1)通过try将原有语句包裹起来,然后在catch中再写一遍释放资源的代码:
try{
//分配资源
//...dothing
//释放资源
}catch(...){
//释放资源
}这个写法的好处在于 “通俗明了” ,缺点在于代码量明显增加了不少,而且释放资源的代码还被写了两次
2)将流程封装成一个类,然后把分配资源 和 dothing 的动作放在构造函数里,把释放资源的动作放在析构函数里
然后在使用时只创建临时变量,那么临时变量在离开作用域时会自动调用析构,这样即便是出现异常,亦可以保证
资源的释放
class A{
A(){
//分配资源
//dothing
}
~A(){
//释放资源
}
}
void func(){
A a; //局部变量,离开作用域会自动调用析构,而出现异常时,异常会一层层沿着调用栈传递,因此可以确保离开作用域
}
3)使用智能指针,智能指针是模板,其接受任意地址作为构造入参,当智能指针离开作用域时,其指向的内存单元
也会被释放,智能指针的实现和 2)一样,即在析构函数中释放自己构造时持有的内存地址
void func(){
auto_ptr<A> pa(new A());
}
#19 ========================================================================================================锁的类实现
Windows:
class Lock
{
public:
Lock(const CRITICAL_SECTION& lock):m_lock(lock){
EnterCriticalSection(&m_lock);
}
~Lock(){
LeaveCriticalSection(&m_lock);
}priavte:
CRITICAL_SECTION m_lock;
}Linux:
class Lock
{
public:
Lock(const Mutex& lock):m_lock(lock){
//mutex - m_lock acquire
}
~Lock(){
//mutex - m_lock release
}priavte:
Mutex m_lock;
}#20 ========================================================================================================
标准异常
下面说的异常都是被封装成了类,因此在catch的时候应该这样写:
try{
new A();
}catch(bad_alloc exc){ //这里的bac_alloc是异常类名,exc是临时异常变量名
//...
}catch(bad_alloc){ //也可以只捕捉某个类型的异常,而不创建临时变量
//...
}new 无法获得足够的内存 ----> bad_alloc
我们可以指定nothrow来表示告知函数不应当抛出异常,具体使用时再行查阅
PS:我们可以自定义一个普通类,然后把它作为异常抛出,当然为了做区分,类名最好叫做xxx_exception。
[essential c++] 读书笔记
最新推荐文章于 2023-04-19 17:39:46 发布