参考:https://blog.csdn.net/haoel/article/details/1948051/
https://blog.csdn.net/linyt/article/details/51811314
类和结构
inline内联函数
定义: 当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.
在类里面写函数,这个函数会自动被声明成inline函数
函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数.
优点: 当函数体比较小的时候, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联.
缺点: 滥用内联将导致程序变慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。
结论: 一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则: 内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行).
this指针
this指针是指向实例的指针。
P->Hello(…)实质上应该是Hello(p, …)
重载、重写、隐藏
overload:传入参数不同,编译器根据传入参数决定调用哪个函数
override:重写虚函数
hiding:重写非虚函数,父类的这个函数在子类中就相当于隐藏了,但是如果用父类的指针访问子类对象,又会访问到父类的那个函数。
构造函数
构造顺序:先调用基类的构造函数
单个类的成员按照被声明的顺序执行构造。
多继承情况下被继承类的构造函数按照继承的顺序执行 。
构造函数会初始化vptr(虚函数表指针)
创建对象时一定会指明对象的类型,不存在多态调用,构造函数不能是虚函数。(工厂模式可以缓解这个问题)
调用虚函数需要vptr,而vptr是由构造函数初始化的。
静态成员?
析构函数
析构顺序:从派生类开始析构,最后调用基类的系构函数
抽象基类的析构函数一定要声明成析构函数。
// 不管析构函数是否是虚函数(即是否加virtual关键词),delete时基类和子类都会被释放;
SubClass* pObj = new SubClass();
delete pObj;
// 若析构函数是虚函数(即加上virtual关键词),delete时基类和子类都会被释放;
若析构函数不是虚函数(即不加virtual关键词),delete时只释放基类,不释放子类;
BaseClass* pObj = new SubClass();
delete pObj;
验证代码
#include <iostream>
using namespace std;
class Base
{
public:
Base() { cout << "contructor Base!" << endl; };
// ~Base() { cout << "destructor Base!" << endl; };
virtual ~Base() { cout << "destructor Base!" << endl; };
};
class Derived : public Base
{
public:
Derived() { cout << "contructor Derived!" << endl; };
~Derived() { cout << "destructor Derived!" << endl; };
};
int main()
{
Base *pBase = new Derived;
delete pBase;
pBase = NULL;
return 0;
}
/*输出结果为:
contructor Base!
contructor Derived!
destructor Base!
*/
抽象类
如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 “= 0” 来指定的
多继承
参考:https://www.kancloud.cn/digest/effectivecplusplus/177313
基类函数同名问题
最好的办法:修改原始类或者增加中间类,不要含有同名函数
如果含有同名函数存在问题:
如果同名函数是虚函数,所有同名函数都会被派生类重写。
如果同名函数不是虚函数,所有函数都不会被丢弃,这时候是一个命名冲突的问题。
参考:http://c.biancheng.net/cpp/biancheng/view/2210.html
多个基类中有同名函数时,即使参数不一样,也不会像同一个域中的同名函数那样自动推演调用哪个版本。如果继承时不做任何声明,调用时必须指出是哪个基类的函数。这种方法会失去多态的特性,尽量避免。
class A {
public:
int func ( int );
void func ( char );
// ...
};
class B {
public:
double func ( double );
// ...
};
class AB : public A, public B {
public:
// ...
};
void f1 ( AB* ab )
{
ab->func(1); // 出现歧义
ab->A::func(1); // 明确地指出是用哪个基类的同名函数
ab->B::func(1);
}
第二种方法,继承时用using关键字处理
在派生类中通过using声明,把基类中的同名函数都引入到一个公共域中,这样重载解析规则就可以正常使用。
class AB: public A, public B {
public:
using A::f; //注意语法
using B::f;
char func ( char ); // 把A::func ( char ) 覆盖了
AB func ( AB );
// ...
};
void f2 ( AB* ab )
{
ab->func ( 1 ); // A::func ( int )
ab->func (0.5 ); // B::func ( double )
ab->func ( ‘c’ ); // AB::func ( char )
ab->func ( *ab ); // AB::func ( AB )
}
虚拟继承
使用virtual关键字,保证菱形继承情况下只有一份基类。
virtual关键字加在中间层的类上。
class B {……};
class B1 : virtual public B{……};
class B2: virtual public B{……};
class D : public B1, public B2{ …… };
多态,虚函数
在基类中用virtual关键字声明的函数可以在子类中被重写。在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。如果不使用virtual关键字,即使重写调用的也还是基类中的函数(早绑定)。
虚函数的权限
C++中, 虚函数可以为private, 并且可以被子类覆盖(因为虚函数表的传递),但子类不能调用父类的private虚函数。虚函数的重载性和它声明的权限无关。
纯虚函数可以设计成私有的,不过这样不允许在本类之外的非友元函数中直接调用它,子类中只有覆盖这种纯虚函数的义务,却没有调用它的权利。
一个成员函数被定义为private属性,标志着其只能被当前类的其他成员函数(或友元函数)所访问。而virtual修饰符则强调父类的成员函数可以在子类中被重写,因为重写之时并没有与父类发生任何的调用关系,故而重写是被允许的。
编译器不检查虚函数的各类属性。被virtual修饰的成员函数,不论他们是private、protect或是public的,都会被统一的放置到虚函数表中。对父类进行派生时,子类会继承到拥有相同偏移地址的虚函数表(相同偏移地址指,各虚函数相对于VPTR指针的偏移),则子类就会被允许对这些虚函数进行重载。且重载时可以给重载函数定义新的属性,例如public,其只标志着该重载函数在该子类中的访问属性为public,和父类的private属性没有任何关系!
class Base
{
public:
Base();
virtual ~Base(); //每个实例都有虚函数表
void set_num(int num) //普通成员函数,为各实例公有,不归入sizeof统计
{
a=num;
}
private:
int a; //占4字节
char *p; //4字节指针
};
class Derive:public Base
{
public:
Derive():Base(){};
~Derive(){};
private:
static int st; //非实例独占
int d; //占4字节
char *p; //4字节指针
};
int main()
{
cout<<sizeof(Base)<<endl;
cout<<sizeof(Derive)<<endl;
return 0;
}
虚函数指针vptr:定义子类对象时,vptr先指向父类的虚函数表,在父类构造完成之后,子类的vptr才指向自己的虚函数表。(这也就是在父类或者子类的构造函数中调用虚成员函数不会实现多态的原因,这是一道面试题)
友元类和友元函数
class BigBox {};
//在类中使用friend声明该类的友元函数或友元类
class Box
{
double width;
public:
friend void printWidth(Box box);
friend class BigBox;
void setWidth(double wid);
};
//BogBox类中的任何成员函数都可以访问Box类的私有变量
//友元函数printWidth不是任何一个类的成员函数
template <typename T>
class my_class
{
friend T; // 将模板参数声明为 friend
//...
};
使用其他类的成员函数作为友元函数
// classes_as_friends1.cpp
// compile with: /c
class B;
class A {
public:
int Func1( B& b );
private:
int Func2( B& b );
};
class B {
private:
int _b;
// A::Func1 is a friend function to class B
// so A::Func1 has access to all members of B
friend int A::Func1( B& );
};
int A::Func1( B& b ) { return b._b; } // OK
int A::Func2( B& b ) { return b._b; } // C2248
数据抽象,数据封装
纯虚函数,接口,抽象类
参考:https://www.runoob.com/cplusplus/cpp-interfaces.html
设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。
因此,如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。
可用于实例化对象的类被称为具体类。
如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 “= 0” 来指定的,如下所示:
class Box
{
public:
// 纯虚函数
virtual double getVolume() = 0;
virtual ~getVolume(){}; // 基类的析构函数一定要是虚函数
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
注意:基类的析构函数一定要是虚函数。否则用del 基类指针时只调用基类析构函数,不调用派生类析构函数。
当父类的构造函数/析构函数即使被声明virtual,子类的构造/析构方法仍无法覆盖父类的构造方法和析构方法。释放对象时先调用基类析构函数,再调用派生类析构函数。
类的实现,this指针
类在struct的基础上编写。
成员函数最终被编译成与对象无关的普通函数,除了成员变量,会丢失所有信息,所以编译时编译器要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以此来关联成员函数和成员变量。这个额外的参数,实际上就是 this,它是成员函数和成员变量关联的桥梁。
对象的内存布局
参考:https://blog.csdn.net/haoel/article/details/1948051
https://blog.csdn.net/haoel/article/details/3081328
https://blog.csdn.net/haoel/article/details/3081385
虚函数指针(vptr)和虚函数表(vtable)的内存布局
虚函数表存在data段(windows)或rodata段(linux)中。
虚函数指针存储在类的实例的首部,指向虚函数表的地址。
类的实例存在栈中,虚函数指针就存在栈中。类的实例存在堆中,虚函数指针就存在堆中。
同一个子类的不同实例共用同一个虚函数表,它们的虚函数指针都指向这个虚函数表。
如果子类不重写父类的虚函数,子类会完全继承父类的虚函数,子类有自己的虚函数表,但是子类的虚函数表内容和父类一样。
如果子类重写父类的虚函数,子类的虚函数表内容就会改变。
代码验证
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
class Derived: public Base{
public:
void f() { cout << "Derived::f" << endl; }
};
int main()
{
typedef void(*Fun)(void);
Base b1;
Derived d1;
Derived d2;
cout << "b1虚函表指针的地址:" << (int*)(&b1) << endl;
cout << "d1虚函表指针的地址:" << (int*)(&d1) << endl;
cout << "d2虚函表指针的地址:" << (int*)(&d2) << endl;
cout << "b1虚函数表地址:" << (int*)*(int*)(&b1) << endl;
cout << "d1虚函数表地址:" << (int*)*(int*)(&d1) << endl;
cout << "d2虚函数表地址:" << (int*)*(int*)(&d2) << endl;
cout << "b1虚函数表中的第一个函数地址:" << (int*)*((int*)*(int*)(&b1)) << endl;
cout << "d1虚函数表中的第一个函数地址: " << (int*)*((int*)*(int*)(&d1)) << endl;
cout << "d2虚函数表中的第一个函数地址: " << (int*)*((int*)*(int*)(&d2)) << endl;
return 0;
}
gdb查看对象的内存布局和虚函数表
print xxx 查看object xxx的内存布局
info vtbl xxx 查看object xxx的虚函数表
单一继承
单一继承时,只存在一个虚函数表,类实例能用的所有虚函数(自己定义的和继承下来的)地址都存在这个虚函数表中。
代码验证
#include <iostream>
using namespace std;
class Parent
{
public:
int iparent;
Parent() : iparent(10) {}
virtual void f() { cout << " Parent::f()" << endl; }
virtual void g() { cout << " Parent::g()" << endl; }
virtual void h() { cout << " Parent::h()" << endl; }
};
class Child : public Parent
{
public:
int ichild;
Child() : ichild(100) {}
virtual void f() { cout << "Child::f()" << endl; }
virtual void g_child() { cout << "Child::g_child()" << endl; }
virtual void h_child() { cout << "Child::h_child()" << endl; }
};
class GrandChild : public Child
{
public:
int igrandchild;
GrandChild() : igrandchild(1000) {}
virtual void f() { cout << "GrandChild::f()" << endl; }
virtual void g_child() { cout << "GrandChild::g_child()" << endl; }
virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; }
};
// 我们使用以下程序作为测试程序:(下面程序中,我使用了一个int** pVtab 来作为遍历对象内存布局的指针,这样,我就可以方便地像使用数组一样来遍历所有的成员包括其虚函数表了,在后面的程序中,我也是用这样的方法的,请不必感到奇怪,)
int main()
{
typedef void (*Fun)(void);
Fun pFun;
GrandChild gc;
long long **pVtab = (long long **)&gc;
cout << "[0] GrandChild::_vptr->" << endl;
for (int i = 0; (Fun)pVtab[0][i] != NULL; i++)
{
pFun = (Fun)pVtab[0][i];
cout << " [" << i << "] ";
pFun();
}
cout << "[1] Parent.iparent = " << (long long)(pVtab[1]) << endl;
cout << "[2] Child.ichild = " << (long long)pVtab[2] << endl;
cout << "[3] GrandChild.igrandchild = " << (long long)pVtab[3] << endl;
return 0;
}
/* Output:
[0] GrandChild::_vptr->
[0] GrandChild::f()
[1] Parent::g()
[2] Parent::h()
[3] GrandChild::g_child()
[4] Child::h_child()
[5] GrandChild::h_grandchild()
[1] Parent.iparent = 429496729610
[2] Child.ichild = 140733193389032
[3] GrandChild.igrandchild = -3717484718879368448
*/
这个例子中,Child类重写了Parent类的f函数,GrandChild类重写了Child类的g_child函数。
GrandChild类一共有6个函数:
从Parent继承下来的f,自己重写;
从Parent继承下来的g
从Parent继承下来的h
从Child继承下来的g_child,自己重写
从Child继承下来的h_child(下面右图里名字写错了)
自己定义的h_grandchild
多重继承
代码验证1
class base1 {
int b1;
public:
virtual void f1() {}
virtual void g1() {}
};
class base2 {
int b2;
public:
virtual void f2() {}
virtual void g2() {}
};
class base3 {
int b3;
public:
virtual void f3() {}
virtual void g3() {}
};
class derive: public base1, public base2, pbulic base3 {
int d;
public:
virtual void f1() override {}
virtual void f2() override {}
virtual void f3() override {}
};
derive对象内存结构有以下几个特点:
- base1, base2, base3这3个基类依次排列,后面才是derive类新增的d成员
- 每个基类都有一个虚函数表指针,每个虚函数表指针存在对应基类的变量前面
- 第一个vtable指针指向的虚函数表,既包含从base1继承的函数,也包含derive类新增的函数和重写的函数。
- derive另外两个虚函数表,以base2坑的虚函数表为例,它有两项。第一项是non-virtual thunk to derive::f2(),第二项是base2::g2()。因为derive类没有重写g2函数,所以第二项填base2::g2()是乎合理解的。而non-virtul thunk to derive::f2()这项我们后面会解释。
- 其它的-8和-16数字,估计是其它语法场景下有用,目前没有看到,可以先跳过它们。
- 另外在整个derive的虚函数表中,出现两次derive类的type_info指针,先忽略它们吧。
代码验证2
#include <iostream>
using namespace std;
class Base1
{
public:
int ibase1;
Base1() : ibase1(10) {}
virtual void f() { cout << "Base1::f()" << endl; }
virtual void g() { cout << "Base1::g()" << endl; }
virtual void h() { cout << "Base1::h()" << endl; }
};
class Base2
{
public:
int ibase2;
Base2() : ibase2(20) {}
virtual void f() { cout << "Base2::f()" << endl; }
virtual void g() { cout << "Base2::g()" << endl; }
virtual void h() { cout << "Base2::h()" << endl; }
};
class Base3
{
public:
int ibase3;
Base3() : ibase3(30) {}
virtual void f() { cout << "Base3::f()" << endl; }
virtual void g() { cout << "Base3::g()" << endl; }
virtual void h() { cout << "Base3::h()" << endl; }
};
class Derive : public Base1, public Base2, public Base3
{
public:
int iderive;
Derive() : iderive(100) {}
virtual void f() { cout << "Derive::f()" << endl; }
virtual void g1() { cout << "Derive::g1()" << endl; }
};
// 我们通过下面的程序来查看子类实例的内存布局:下面程序中,注意我使用了一个s变量,其中用到了sizof(Base)来找下一个类的偏移量。
//(因为我声明的是int成员,所以是4个字节,所以没有对齐问题。关于内存的对齐问题,大家可以自行试验,我在这里就不多说了)
int main()
{
typedef void (*Fun)(void);
Derive d;
long long **pVtab = (long long **)&d;
cout << "[0] Base1::_vptr->" << endl;
Fun pFun = (Fun)pVtab[0][0];
cout << " [0] ";
pFun();
pFun = (Fun)pVtab[0][1];
cout << " [1] ";
pFun();
pFun = (Fun)pVtab[0][2];
cout << " [2] ";
pFun();
pFun = (Fun)pVtab[0][3];
cout << " [3] ";
pFun();
pFun = (Fun)pVtab[0][4];
cout << " [4] ";
cout << pFun << endl;
cout << "[1] Base1.ibase1 = " << (long long)pVtab[1] << endl;
long long s = sizeof(Base1) / 8;
cout << "[" << s << "] Base2::_vptr->" << endl;
pFun = (Fun)pVtab[s][0];
cout << " [0] ";
pFun();
pFun = (Fun)pVtab[s][1];
cout << " [1] ";
pFun();
pFun = (Fun)pVtab[s][2];
cout << " [2] ";
pFun();
pFun = (Fun)pVtab[s][3];
cout << " [3] ";
cout << pFun << endl;
cout << "[" << s + 1 << "] Base2.ibase2 = " << (long long)pVtab[s + 1] << endl;
s = s + sizeof(Base2) / 8;
cout << "[" << s << "] Base3::_vptr->" << endl;
pFun = (Fun)pVtab[s][0];
cout << " [0] ";
pFun();
pFun = (Fun)pVtab[s][1];
cout << " [1] ";
pFun();
pFun = (Fun)pVtab[s][2];
cout << " [2] ";
pFun();
pFun = (Fun)pVtab[s][3];
cout << " [3] ";
cout << pFun << endl;
s++;
cout << "[" << s << "] Base3.ibase3 = " << (long long)pVtab[s] << endl;
s++;
cout << "[" << s << "] Derive.iderive = " << (long long)pVtab[s] << endl;
return 0;
}
/* Output:
*/
菱形继承
class B {……};
class B1 : virtual public B{……};
class B2: virtual public B{……};
class D : public B1, public B2{ …… };
如果不使用virtual关键字,就和普通的多继承一样。
如果使用virtual关键字进行菱形继承,内存中的顺序是B1,B2,D,B
注意B被放在了最后面