LIST特性
- List 是个双向链表
- 双向循环
- G2.9中STL实现的LIST,其NODE的next 和 prev是 void型指针
- 所需要内存是一个数据 + 两个指针
LIST的iterator
- 后置++的实现一定是先实现了前置++
- = * 会先调用=的重载
- ++ *this 同样会调用 前置++重载函数
- 前置++返回引用,后置++返回普通
这是因为看齐整数,++++i 可以,i++++禁用。
Vector 扩充
- 扩充只能在其他地方找空间,不能在连续的地方上直接扩充。
- Vector 三个指针,start,finish,end_of_storage
- 扩充是两倍扩充,扩充时,代价十分大。
Array不可扩充
- 没有构造器,和析构
Deque
- stack和queue都是通过Deque实现,反向封装Deque。默认使用Deque
- stack和queue也可以选择List封装
- stack可以用vector做封装,queue不能用vector做封装
- stack和queue不提供iterator
Set和MultiSet
- set不允许key重复,MultiSet允许重复key
- 底层数据结构是红黑树
Map和MultiMap
- 底层数据结构是红黑树
HashTable
- 底层的buckts是用vector实现的
- hash函数没有string类型的
迭代器
- 迭代器分类是对象,其继承关系如下
- 这样的继承关系被用于减少代码,不适用enum的巧妙用法
C++的内存布局
- 封装并不会使得c++比起c来增加任何成本。
- 主要额外负担是由virtual function造成。
- 在c语言中
- 为什么在结构体的最后加一个c[0]数组?
struct fuck{
int a;
int b;
char c[0];
}
-这样每个结构体都可以拥有一个可变数组 by using code like this
struct p = (struct fuck*)malloc(sizeof(struct fuck) + sizelen);
这样p就有了长度为sizelen的可变数组大小,然后就可以用以下方式使用这个可变数组
p->c[i]//i depends on the sizelen
- 但这样的trick不能在c++中使用,因为c++的内存模型和c不一样
- 若要使用这样的技巧,可以用下面的方式
struct c_point{
char c[0];
};
class Point : public c_point{
};
- more advance method of doing this
struct c_point{
char p[0];
};
class point{
public:
operator c_point(){return _c_point;}
private:
c_point _c_point;
}
OO与ADT的差异
- OO(object-oriented)所要处理的无限可能的对象(多态),而ADT所要处理的是某个具体对象。
- 换句话说,OO的哲理是,类型虽然有界定(animal),但其背后却有无穷可能(dog,cat,mouse)
- 由此,想要熟练使用OO,那么就需要pointer和reference。而ADT则只需要具体的对象。(pointer是class的pointer,int * void *不会触发)
- 为什么int *,void 无法触发多态?这是因为一个指向地址的int,其涵盖的地址空间是1000-1003。同理,我想你看到这里应该就能反映过来why,现在的我懒得写了。
- 至于说父类对象指针,和子类对象指针的区别,那也是涵盖地址的差异
operator int*()与operator int()
- 两个都是类型转换运算符
- 即是将某个class的object转换为 int*,int类型。
- 用处:
- 撰写nullptr实现时,nullptr是一个class的object
- 通过如下代码所示,可将一个nullptr对象,转换为T类型的空指针
- int *p = mynullptr,隐式触发 operator T * ()函数
- (int *) mynullptr, 显示触发 operator T * ()函数
- operator T * ()函数将返回0,也就是将nullptr转为0
const class mynullptr_t
{
public:
template<typename T>
operator T*() const
{
cout << "T* is called" << endl;
return 0;
}
} mynullptr = {};
- if(cin) ,希望将标准输入流cin对象转换为int型,用于判断。因此可以实现operator int()函数。
- Therefore,可以使用cin << intval。<< 将被解释为左移运算符
explicit与构造函数
- explicit用处,避免单一参数的constructor被用用作是一个conversion运算符。
- 什么是conversion运算符?参见上文。
- Say for instance
CxString(int size)
{
_size = size;
_pstr = malloc(size + 1);
memset(_pstr, 0, size + 1);
}
In main function:
CxString string2 = 10;
- 因为没有加上explicit,所以这句话实际上被翻译成,CxString string2(10)
- Therefore, explicit的关键点字是:单一参数构造函数,避免conversion运算。
编译器与构造函数(41-43)
- 需要注意的是,所有代码讨论的都是内含,而不是继承关系。
- 编译器需要和程序员需要
- 如果一个object A内含了n个object,那么编译器会为A的构造器中,插入并调用其余n个object的构造函数。
- 上述的是编译器行为,由编译器负责
- 至于说,object A中数据成员的初始化,那就是程序员需要负责的。
- 编译器的default constructor,不会初始化数据成员
- 若一个class内含了另外一个object,则需要这个object提供一个默认构造函数。如果没有,那么则无法生成default constructor。
- 如果class内,内含了一个复杂对象,那么就需要这个复杂对象提供其自身的构造函数,否则这个class无法生成default constructor
- 默认构造函数,不会初始化数据对象
Copy constructor被唤起的时机
- X xx = x;//其中x为左值
- foo(X x)
- X foo_bar(){}
-
这就是为什么大部分函数的返回都是引用而不是值,这样能避免调用 Copy constructor
-
如果没有自己编写Copy constructor那么将会默认使用bitwise copy semantics(浅拷贝)
-
默认构造函数生成法则:如果一个class内含了一个object,如果这个object显示的申明了自己的构造函数,拷贝构造函数,那么就会生成默认的constructor和Copy constructor。
-
如果不写,c++并不一定就会生成默认的constructor和Copy constructor。
-
特殊地,对于默认的Copy constructor。如果一个class声明了一个或多个virtual function时,会自动生成默认Copy constructor。这个Copy constructor会显式地设定object vptr的virtual table,而不是直接对vptr赋值。
-
这样做是为了保证父类的vptr不改变。
-
say for instance
//franny 是父类,yogi是子类
zooAnimal franny = yogi;
draw(yogi);
draw(franny);
如果vptr直接被bitwise copy,那么draw(franny)将会调用yogi的draw,事实上我们所期望的行为是调用franny自身的zooAnimal::draw,而不是Bear::draw
- 如下代码片段所示,传参数调用函数时。
//foo(X x0)
X xx;
foo(xx);
使用编译器可能生成的代码
X _temp0;
_temp0.X::(xx)//调用copy constructor
foo(_temp0)
问题出现在foo的声明上,虽然_temp0使用copy constructor正确的构造了自己,但是在将_temp0传入foo时,却会使用bitwise拷贝至x0中。也就说,上面所有提到的bitwise的缺点都会存在。
-
像是这个class X存在虚函数,那么x0的vptr会直接指向_temp0。
-
Therefore,必须需要 修改foo的声明foo(X &x0)。而class X的deconstructor会销毁这个x0.
-
NRV优化提升效率,即是编译器将函数的参数统一命名,如下代码所示。but,这种优化现在却反倒会引起效率的下降。
//未开启NRV
void bar(X &_result){
X xx;
xx.X::X();
//对xx操做
_result.X::XX(xx);
return;
}
//开启NRV
void bar(X &_result){
_result.X::XX();
//对_result操做,而不是xx。
return;
}
- 一个class如果不包含任何object,其所有数据成员都是基础数据类型,那么不显示提供copy constructor是正确的。调用bitwise拷贝既高速又安全。
Member initialization list
- 思考以下代码
class word{
String _name;
int _cnt
public:
word(){
_name = 0;
_cnt = 0;
}
};//效率十分低下 why?
因为该class拥有一个string object对象,因此编译器的所生成的代码可能如下
class word{
String _name;
int _cnt
public:
word(){
//_name = 0;
_name.String::String();
String temp = String(0);
_name.String::operator=(temp);
temp.String::~String();
_cnt = 0;
}
};
- 为什么会生成这样的代码?这是因为_name = 0,这句代码,会触发赋值函数。因为_name已经提前被定义,所以会提前调用构造,已经构造的对象,再使用=就会调用赋值函数。
- 使用Member initialization list可以减少代码
class word{
String _name;
int _cnt
public:
word():_name{0}{
_cnt = 0;
}
};//效率十分低下 why?
编译器所产生的代码
class word{
String _name;
int _cnt
public:
word():_name{0}{
_name.String::String(0);
_cnt = 0;
}
};//效率更高
- Member initialization list中初始化成员的顺序,是按照在class中声明的顺序执行的。list中的顺序,就是个摆设
- a dangerous example
class X{
int i;
int j;
public:
X(int val)
: j{val},i{j}
{}
}//至于说为什么危险,现在的我就不写了。留给未来正在读的你思考2020-08-07 14:59:00
//我思考nmd,能不写清楚点,现在的我记得住个屁,以后写博客能不能不要这么装。2021-2-24 23:05
member
- static member是不存在于object中的,而是存在于class中。虽然语法上表现出来,似乎就是在origin上一样,但是其实际上会被转述为对class中的chunksize的调用。如下述代码所示
//chunksize is a static member
Point3d origin;
origin.chunksize; -> Point3d ::chunksize;
origin.chunksize; -> Point3d ::chunksize;
- member存取效率的差异,参考以下代码
Point3d origin,*pt=&origin;
origin.x=0.0;
pt->x=0.0;
如果x是一个struct member,class member,单一继承,多重继承的情况下都相同。
问题就出在如果x是一个virtual base class的member。那么因为不能确定pt是具体指向哪一种class type,因此需要在运行时才能够知道。所以pt->x的存取效率会远低于origin.x。其offset在编译时就已经确定。
-
当class A继承class B时,需要将class的alignment padding一起继承。换句话说,计算一个class的大小时,是其父类元素+padding+加自身
-
至于说为什么要这样设计语言?那是为了保护subobject在复制发生时,不被破坏。具体参看下图
Virtual function
- 当一个class被添加上Virtual 后,其所需要增加的负担如下
- 为class导入virtual table,记录虚函数
- 为class导入vptr指向virtual table
- 增强constructor,使得其能够为vptr赋值
- 增强deconstructor,使其能够消除virtual table 和 vptr
Function 语义
- member function 和 nonmember function 不应该有执行效率上的差异,这是c++的设计准则之一。所以所有的member function都会被转换为nonmember function。//这应该是go语言的面向对象之所以要用 func (p *Class)memberfunction这样的方式去实现一个class的方法的原因把 2021/2/24 23:09
- 这就是为什么,所有对象对member function的调用,都需要传入this指针的原因
ptr -> normalize();//normalize 为虚函数
- 上述对虚函数调用的代码,将会被转换为如下代码
(* ptr->vptr[1]) (ptr)
- 即使从vptr[1]中取出一个函数,最后并将ptr作为this指针传入,member function。
- 一个class只会存在一个virtual table,所有的object使用vptr共享。(单继承下)
- 重写虚函数,会覆盖base class的虚函数实列。
- 继承base class时,如果新增了虚函数,那么将会扩大virtual table的size,新增一个slot,而不是再新增一个virtual table
- 必须要定义pure virtual destructor。因为每一个derived class的destructor会被编译器扩张,以静态的方式调用其每一个virtual base class,以及上一层base class的destructor。
情况在多重继承之下,发生了变化
- 一个derived class不只是仅有一个virtual tables,而是含有n-1个额外的virtual tables。n表示其上一层的base classes个数
- Therefore,derived class中的每一个subobject中都会存在一个vptr、以指向其自身的virtual tables。
Base2 *phase2 = new Derived;
因为Derived中存在多个virtual tables,所以编译器会根据phase的指针类型,相应的修改其offset,使得phase指向Derived中Base2的subobject,即是如下代码
Derived *temp = new Derived;
Base2 *phase2 = temp? temp + sizeof(Base1) : 0;
- 劝告:不要在virtual base class中声明nonstatic data members。(即最好不要在c++中使用抽象类,全做接口用)
- inline函数被调用多次,会产生大量扩展码(即为了避免变量冲突,产生临时变量),使代码量增加
- 劝告:不要再使用c语法那样将所有的变量声明都放在代码起始处,因为c++ constructor和destructor的缘故,会使得代码被编译器加入许多调用。所以c++语法中,要做到随用随生成。
Template代码的常见谬误
template <typename T>
class Fuck{
public:
Fuck(T t = 1024)
:_t(t)
{
if(tt!=t)
throw ex ex;
}
private:
T tt;
}
- !=没有定义,除了typename是 int、float、double这些基础数据类型时!=可以使用之外,其他情况下,!=均无法正常使用。
- t=1024,同上除了int、float、double这些基础类型之外,t=1024都不一定能够正常赋值。
栈展开与其危害
- 什么是栈展开?参考以下代码
#include <iostream>
using namespace std;
void f1() throw (int) {
cout<<"\n f1() Start ";
throw 100;
}
void f2() throw (int) {
cout<<"\n f2() Start ";
f1();
cout<<"\n f2() End ";
}
void f3() {
cout<<"\n f3() Start ";
try {
f2();
}
catch(int i) {
cout<<"\n Caught Exception: "<<i;
}
cout<<"\n f3() End";
}
int main() {
f3();
return 0;
}
程序首先从f3()开始,从f3()中调用f2(),然后f2()再调用f1(),最后f1()抛出异常。因为f1()中没有catch异常处理,因此删除f1()栈中所有临时object,向上进入到f2()中。又因为f2()中没有cathc处理异常,因此删除f2()中所有临时object,向上进入到f3()中。在f3()中发现到了catch异常处理,最后使用f3()中的catch异常处理。
f2()中的cout<<"\n f2() End ";并不会被执行,而是直接从f1()的调用中返回,进入到f3();
- 栈展开的危害是什么?
在上面的叙述中提到了会删除栈中所有的临时object,对于普通object来说,栈展开的过程会调用它们的destructor但是于指针、数组变量来说,栈展开的过程并不会调用delete [] 。因此如果在栈展开过程中使用了new运算符申请了heap内存,那么就会发生内存泄露。 - 参考如下代码
#include <iostream>
#include <memory>
using namespace std;
void autoptrtest1(){
A* pa=new A(1);
throw 1;
delete pa;
}
int main(){
try{
autoptrtest1();
}
catch(int){
cout<<"there is no destructor invoked"<<endl;
}
}
在函数autoptrtest1()中使用了new运算符,申请了heap内存,随后throw出异常。因此在栈展开的过程,只会删除该指针,而不会调用delete删除该指针。所以指向的heap内存会发生内存泄露。
- 如何解决这个问题?
#include <iostream>
#include <memory>
using namespace std;
void autoptrtest2(){
auto_ptr<A> pa(new A(2));
pa->show();
throw 2;
}
int main(){
try{
autoptrtest2();
}
catch(int){
cout<<"A's destructor does be invoked"<<endl;
}
}
不直接使用*指针,而是使用类模板auto_ptr生成一个具体的object。auto_ptr pa(new A(2)); 使用 new A(2)生成一个heap内存对象,将其作为参数传入auto_ptr pa的构造函数中,生成一个pauto_ptr pa对象。
随后在栈展开的过程中,就会有调用其destructor自动析构
downcast与其危害
- 什么是downcast?
如果存在以下继承结构
class type{}
class fct:public type{}
class gen:public type{}
那么将type*的一个变量转换至fct或是gen,这就是downcast,如下代码所示。
type* pt;
fct *pf = (fct*) pt
-
什么是downcast的危害?
因为type class的基类指针,可以指向任意派生类。那么在转型时,type* pt可能是指向gen类型的object。因为多态的原因,使得我们无法得知type* pt到底背后是个什么东东。因此在转型时,很有可能时一个gen的object被转换为fct object。 -
Therefore,后续所有对pf的使用都是不正确的,这样就会发生意想不到的危害。
-
如何避免这样的危害?
使用dynamic_cast动态检查pt指针其背后真正的object,如下代码所示
fct* pf = dynaminc_cast<fct *> pt
上述这段代码,会被编译器翻译为如下代码
((type_info*)(pt->vptr[0]))->_type_descriptor
即是通过vptr,去将virtual table中存储的_type_descriptor取出来,然后再将这个东东,传入到一个runtime library函数中进行比较。
-
由上述代码可知,要想使用dynamic_cast首先肯定是需要vptr滴,那么vptr是怎么来滴?当然是虚函数咯,如果没有虚函数还想用dynamic_cast?那就是在想peach。
-
dynamic_cast并不能够用于reference之上。因为dynamic_cast返回0,则表示没有指针没有指向任何object。
-
但是问题出在reference和pointer的区别上。pointer可以用0表示NULL,但是reference用0会创建一个临时对象,这个对象的初值为0。总结就是reference不能像pointer一样用0来表示NULL。
-
Therefore,想要对reference使用dynamic_cast那么就需要进行如下封装。
myconvert(const type &t){
try{
fct* pf = dynaminc_cast<fct *> pt
}catch(bad_cast){
}
}