chapter1
struct
struct声明,class定义只会警告,实际由定义的关键字决定
对象
对象之中的变量在内存中的顺序为:
同access section中按照声明顺序,不同的access section按照section写的顺序
c++对象模型
对象内:
non-static data members(非静态数据成员) 放在类内
static data member(静态数据成员) 放在类外独立一块(独立在任何对象外)
non-static func 类外独立一块
static func 类外独立一块
vptr -> vptbl -> virtual func vptr在类内
若数据复杂,则可以把数据成员分离出来作为一个struct
在旧的c中,由class继承数据struct
现在的做法是把struct的指针/引用/对象放在class的private中
三种模型
程序模型:数据函数分离
抽象模型ADT
单一对象加上接口
面向对象模型
基类分配统一接口给所有的派生类
class大小计算
non-static data member的大小
virtual函数的vptr
virtual base class 的 virtual base class pointer
齐位造成的补足空间(可能补在成员间可能补在对象边界)
void*
可以指向任何对象,但是无法执行任何操作
多态
引用与指针时实现多态机制的根本
在派生类赋给(初始化给)基类的“对象”时,编译器需要进行裁定这个object的真正类型,并且保证不同object的vptr不会因为这个对象的创建/赋值而改变。故,派生类会被裁切出基类的部分,但是基类无法扩充成派生类对象(因为无法给出相应的vptr)。
程序内存
global object的内存保证会在每一次程序启动的时候清零;local object的内存保存在栈,heap object保存在自由空间(堆),它们的状态位上一次使用的状态。
chapter2
默认构造函数default constructor
如果object内含其他object,则default constructor会自动按照“类内声明这些object的顺序“进行default constructor的调用(执行这些功能的代码会插入在任何用户写在构造函数体内的代码前)。即使声明了含有参数的constructor或显示调用了一些构造函数(或使用部分初始化列表),编译器仍然会把没有调用的默认函数插入。这一条对继承的基类也同样奏效,调用基类的默认构造函数的代码也会插入到每个构造函数(按照继承的顺序,而不是初始化列表的顺序)
会合成default constructor构造函数的情况
1、拥有object成员,需要调用object的default constructor
2、拥有基类,而基类有合成/声明的default constructor
3、具有virtual function,需要创建vptr,所以基类拥有virtual function在非private时也是(在private的话是不需要去创建的,因为调用不了)
4、具有virtual base class,需要为每个virtual base class创建一个指向基类的指针,放在派生类的每个虚拟继承的基类之中,也就是每有一个虚拟基类就增加一个虚指针的空间。
其他时候,或许可以使用无参构造,但实际没有合成构造函数。也就是说并不是没有default构造,编译器就会合成一个。另一方面,默认构造并不会初始化全部的成员,只会初始化对象成员,内置类型不会拥有初始化值。
class A {
public:
//A() :num(1) { cout << "A" << endl; }
int num;
};
class B {
public:
//B() :num(2) { cout << "B" << endl; }
int num;
};
class C {
public:
C(int n) :num(n) { cout << "C" << endl; }
int num;
B b;
A a;
};
int main()
{
C c(3);//c中的b,a里的num都没有初始化,但它们的默认构造函数是调用了的
cout << c.a.num << endl;
cout << c.b.num << endl;
cout << c.num << endl;
//A a;
//cout << a.num << endl;//无法通过编译,因为没有初始化num,但是不用就不会有问题
}
2.2
拷贝构造函数copy constructor
通常object可以进行copy,但是并不代表其有一个拷贝构造函数,而是其使用了编译器的memberwise initialization -> 对每个变量进行bitwise copy(逐位复制),如果有对象成员,那么就对这个对象进行递归的memberwise initialization。这个时候其实没有一个实际合成的拷贝构造函数。
会合成拷贝构造函数的情况
1、对象成员有显示声明/合成的拷贝构造
2、基类具有显示声明/合成的拷贝构造
3、拥有vptr
4、拥有virtual base class
1与2需要合成拷贝构造来调用相应的构造,3与4需要拷贝构造来创建正确的vptr,这一点特别发生在把派生类对象赋值给基类对象进行初始化,编译器需要决定vptr的类别。
2.3
初始化参数的方法
1、先创建一个临时对象,调用拷贝构造复制传入的参数,然后把function改为接受一个引用,引用的就是这个临时对象的地址。(需要一组拷贝/析构)//通常做法
2、直接在参数的位置上构造这个对象(构造/析构)
返回值的初始化
1、返回对象的时候,先为函数增加一个引用参数作为返回结果,然后在return前调用copy constructor赋值一个对象,把这个对象地址给第二个参数。、
2、这个返回的对象如果没有用来初始化另一个对象,那么就会被析构(即使使用=赋值给另一个对象也不行,使用=只是进行了拷贝),用来初始化另一个对象的时候并不是调用拷贝构造函数,而是把返回的对象保留,变成需要创建的对象。
int num;
class A {
public:
A() { cout << "default" << endl; num++; }
A(const A &a) { cout << "copy " << a.name << endl; num++; }
~A() { cout << name << "'s desstructor" << endl; }
string name;
};
A test(A a) {
cout << "-------" << endl;
A b = a;
b.name = "b";
cout << "-------" << endl;
return b;
}
int main()
{
A a;
a.name = "a";
A b(test(a));
cout << num << endl;
cout << b.name << endl;
}
A a;
a = func();//销毁
A a(func()); 或 A a = func();//保留,变成a
在使用者层面进行返回值以及参数传递的优化
引用代替对象作为参数,return 语句调用构造函数返回临时创建对象
X fun(const X&lhs,const X&rhs)
{ return X(lhs,rhs); }
//如此就没有任何由编译器创建的临时对象产生了,不过这样的return是可能出问题的
引用参数不会进行对象复制,显示调用构造也避免了对返回的对象进行复制。但是要慎用,比如smart_ptr就不可以这么返回,这样会导致智能指针在语句结束后被析构,也会导致其指向的对象被释放(可能被释放,如果你返回的这个对象没有被用来初始化,那么这个对象就要被销毁,销毁智能指针可能会销毁其管理的内置指针指向的值)。
在编译器层面进行优化
把
X fun()
{
X xx;
//进行一次对xx的拷贝,然后析构xx,再把拷贝所得的结果返回
return xx;
}
优化为
void fun(X &result)
{
result.X::X();
return;
}
也就是把接受结果的对象的地址作为引用参数,然后直接调用构造函数,并且不返回值,避免了拷贝的进行。
也可以说是使用引用参数代替了return返回结果,成为name return value(NRV优化)
这个优化只有在copy constructor发生了inline的时候发生(可能你手动加上了inline都还没发生,实际上我就没试到发生过)
copy constructor的取舍(剔除)
这条并不是说什么时候不定义copy constructor,而是系统什么时候不选择copy constructor
初始化形式
A a(1024)//单一参数构造
A a = A(1024)//拷贝运算符
A a = (A)(1024)//强制类型转换
以上三种形式,后两者似乎应该是先调用默认构造创建a,然后再进行拷贝构造,但实际上,三者的效果完全一致,剔除了copy constructor的调用,使用的一致是单一参数构造函数。
copy costructor定义的取舍
对于一个内部只有非静态类型成员的对象,也许不要定义copy costructor会更好,即使自定义的copy constructor只是简单复制数值,它的效率也不会比编译器的bitwise copy更好,它的inline copy constructor出现的时候应该是能够确认拷贝操作大量出现的时候才定义,用来启动NRV优化,否则不定义就好了。
memset与memcopy
memcopy(pointer result,pointer locate,size)//复制pointer locate开始size大小的内存块
memset(pointer result,0/-1,size)//为指定内存块填入0/-1不能是其他值
这两个函数均无法在有虚指针的时候使用
因为他们会把vptr也一起复制/清零
初始化list
初始化的顺序是成员声明的顺序,而不是初始化列表中使用的顺序。
初始化代码会插入在任何用户写在constructor中的代码之前。
允许使用类内函数在初始化列表中初始化某个成员,也可以用来当做基类构造函数的参数,但是最好不要这么做,因为可能无法确定这个函数对自身成员的依赖性。
如果要用一个成员初始化另一个成员,那么考虑把这个操作放到函数体里,保证初始化代码已经发生
必须要使用初始化list的情况
1、const/reference成员初始化
2、需要调用基类除了默认构造函数以外的构造函数
3、需要调用成员除了默认构造函数以外的构造函数