0%

cpp_funcmember_memory

c++ function语意学

something:
恩,这部分记录自己对c++中的函数特殊性的理解,本想深入函数本质,之前接触过,记得在linux内核剖析那本书讲的很好。
可以参考;

  • 实现类的成员函数和类之间的关联,即通过对象能调用成员函数。这种实现,可以是以下几种方式:
    1)struct+函数指针:结构体中,一个成员函数对应一个指针,增加空间 如何操作成员?
    2)函数+this指针,普通函数,this指针一个,不添加额外多的空间,this 操作成员
    。。。

  • c++的类主要是将成员和函数封装起来。那么成员的封装就像struct一样,而函数呢?成员函数如何和类,对象联系起来?–this指针

  • c++中的成员函数和普通函数一样,只是多了个this指针,这个指针指向当前调用这个函数的对象,并以形参传入,这样,成员函数就可以使用对象成员了,通过
    this指针;

  • 这里也是根据深入探索c++模型中第四章,function语义学总结的:

    引入这个问题:

    通过对象和对象指针来调用成员函数的不同:
    Point3d obj;
    Point3d *p=&obj;
    两者效率有何不同?

    通过上面分析:当这个成员函数为普通的成员函数时,则直接调用函数,传入this指针,所以基本没有大的区别

以下分为几种函数讨论:

非静态成员函数:

为了支持this指针等构成成员函数,c++做了如下步骤:

  • a;改写函数原型:安插了一个this参数

  • b;对对象成员的操作,通过this

  • c;对成员函数写成外部函数,并将其名称进行mangling处理,成独一无二~类似:函数名___类名参数名 等等

  • 如何命名:成员函数和成员都会被名称特殊化处理:一般加上base class &derived class名
    对重载函数而言如何区分:加上参数链表;
    当extern C时,会压抑这种特殊命名化
    具体编译器实现不同,可以通过汇编等。gdb等看

  • 鉴于此:看一个例子:
    当你需要构造一个(拷贝构造函数)或者改变一个类的成员: 伪代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void normalize__Point(register const Point3d *const this,Point3d &_result)
    {
    _reuslt.Point3d::Point3d() //默认构造函数:
    _result._x=this->__x/2;
    ...
    return ;
    }
    那么,以下这种方式:更好:
    Point3d Point3d::normmalize() const{
    return Point3d(_x/2 ...)直接构建会更快)
    =》转换为return Point3d(this->_x/2,...)
    virtual func

    若normilaze是虚拟函数,则
    ptr->normilaze()=》 (*ptr->vptr[1])(ptr);
    可以看到效率较低,所以当确认不用多态机制的时候,最好抑制它,直接使用:Point3d::xxx 这种方式调用函数
    而若被写成内连函数会更优–原因待探索:

静态成员函数:

显然这种能被类直接调用的函数,没有this指针,从而也就无法用成员
这种函数的出现,是因为一种需求:即不用每个成员函数都会使用成员,不一定需要this
不用声明一个对象就可以调用函数,当非要对象不可,又不用成员时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
 ((Point3d*)0)->object_count() ;```
有了static后,就不用上述方式了
所以static的特性完全来源它的原理:
+ 它不能直接存取non static 成员
+ 它不能被声明为const volatile virtual
+ 能直接被类调用
静态成员函数和普通函数更像,因为它没有this指针,也就不是这种类型:~ unsigned int(*) ();
所以更可以和类之外的元素沟通,比如回调函数

##### 虚拟成员函数
+ a 为了支持多态,需要在执行期间能判断到底调用哪个函数?即pz->z()这个函数,pz为基类指针,而能调用子类函数
+ b 基类指针或引用参考到的(指向的)子类,,子类中的重写父类的函数,在调用时并不知道传入哪个this,即表现为调用哪个函数
+ c 那么为了支持通过基类能调用它,通过子类也能调用该函数,则需要基类和子类共享某个东西,他能正确决议出要调用的函数

+ 带来:额外的空间,和c的兼容性
##### 积极多态的概念:
(1)被指出的对象真正被使用;(2)dynamic_cast
那么哪些函数需要支持这样的特性---》由virtual标志来指出
如何在执行期间调用真正的函数?---》ptr所指的对象真实类型+函数的位置 真实类型放在vtable[0]

+ 编译期间做的:
在每个对象中加入:一个字符串或数字来表示class 类型+一个指针,指向表格vtable,它带有程序的virtual func执行期地址
确定表格中的函数指针,执行期无法改变表格一个类对应一个表格,每个virtual func被指定一个固定的索引值
+ 执行期间做的:
为vptr分配内存地址。它的值在编译期间确定,类似于x=3; 指向vtable
调用函数时激活 。编译器已经为其转换语义为xxx->vptr[n](this)..
+ 注意,当一个子类继承基类时,vptr继承过来,当子类改写virtual函数时,则改变表中的指针指向子类的;当子类添加一个新的virtual func时,则在表中加一个slot
+ 唯一在执行期间才知道的:slot(n)到底指向哪个函数实体
细想一下:
derived de; //编译期间,符号表确认要分配的对象类型,执行期间分配内存和调用构造函数等,构造函数中会初始化vptr,
base *p=&de;//编译期间,类似于int x=3;,执行期间分
配内存给x,并将3给x,确定p所指的真正对象,从而才能取得vptr,到调用到最终的函数
p->xx();//xx为virtual (*p->vptr[1])(p)

##### some question
//关于执行期和编译期确定的东西,有些困惑,为啥编译器不能在p->xx()的时候指定调用子类的xx()?
//在编译器扫描代码的时候,知道定义了一个derived类型的变量,知道,定义了一个base指针,并指向的变量类型为derived,后面调用时也能
确定函数名,为啥不能根据已存的这些信息。来得到哦。这时p指向的是derive类型的。所以调用derived的函数?
不过也是,编译器从做词汇扫描,到语义扫描,出符号表,到语法检查,语义规则等


##### 多重继承下面的virtual func
考虑以下例子:
```cpp
class base1{
public :
int a;
   virtual int a(){return a;}
virtual base1* clone() const;
}
class base2{
public:
int b;
virtual int b(){return b;
virtual base2* clone() const;
}
class derive:public base1,public base2
int c;
virtual int c(){return c;}
virtual derive* clone() const;
}

对于和derived有相同起点的base1不用考虑太多,关键是base2,;有三个问题要解决:

   (1)virtual destructor (记得之前是逐层调用)
  (2)被继承下来的b()
    (3)一组clone函数 
  (a)   做base2 *pbase2=new derive;
      &&&编译期间确定:&&&
   =>  derived *tmp=new derive;
      base2 *pbase2=tmp?tmp+sizeof(base1):0;```
      
 为了使pbase2能访问到 b 即pbase2->b, 多重继承下使用 base *p=de;时,注意此时需要调整,让p指向的是de中的base对应部分
+ 当通过delete pbase2时,此时需要删除完整的对象,所以pbase2指针需要调整到指出完整对象的起始点  
  他也要通过上述a类似的加法,以及调用virtual destructor函数  
```cpp 
   如base2 *pbase2=new derive;  
  delete pbase2;//invoke derive class's destructor (virtual )```
  
  首先这个调用要通过vptr,其次,传入的this指针需要调整
  &&&执行期间确定&&&
+ 注意:然而,这种offset加法无法在编译期间直接设定。pbase2所指的真正对象只有在执行期间才能确定  
//如何调整;早期的想法是:将vtable中的slot改为一个slot放置函数指针faddr和this的offset  
 //则:(*pbase2->vptr[1])(pbase2);  
 //改为 (*pbase2->vptr[1].faddr)(pbase2+pbase2-  >vptr[1].offset);但是连带处罚了其他形式virtual func调用,
  
 + 那如何处理?  
 + [1]方法1:thunk
     ~:this+=sizeof(base1)
          Derived::~Derived(this);//只有汇编才有效率
   如何使能?在virtual table slot继续内含一个简单的指针,slot中的地址可以指向一个virtual func或者一个thunk(若需要调整this)
  其实多重继承来讲,每个继承的基类,都需要一个virtual table;,这样派生类就会维护多个基类的vtable
  + 1)经由derived或第一个base class)调用,不需要调整this
  + 2)经由>=2的base class 调用,同一种函数在virtual table中可能需要多笔对应的slot
        base1 *pbase1 =new derived;
        base2 *pbase2=new derived;
        delete pbase1//不需要调整this,virtual table slot放置正真的destructor地址
        
        delete pbase2//需要调整this ,放置thunk
        vptr和vtable命名也会被特殊化
        参考图在书中,这里不放
+ [2]方法2 :  
   因为动态链接器的原因,使得符号链接变得缓慢
   为了调整执行期链接器的效率,sun编译器将多个vtable 连锁为一个,指向次要表格的指针可以由主要表格加offset
   其他类似。
例子:
```cpp
       base2 *ptr =new derived;
       delete ptr;//此时调用derived::~deirved,故ptr需要被调整,若为virtual,需要更多
       
       derived *pder=new derived;
       pder->b();//注意b没有被改写,所以需要调整pder指向base2 subobj
       
       base2 *pb1=new derived;
       base2 *pb2=pb1->clone()//注意是调用derived:clone,所以调用后返回值要改变器指向base2 subobj
       
       当函数被认为足够小 ,sun编译器会采用某种算法优化,split。。,否则不会,具体待了解。
       所以virtual func的通常大小为8行```
+ [3]IBM:  
函数若支持多重进入点,将不必有许多thunk;将thunk操作放在virtual func中,里面判断是否需要调整this指针
     
     
##### 虚拟继承下的virtual func
这种情况相当复杂了。需要探索上述的这些问题:point2d.point3d的对象不再相符,即使只继承一个,即单一虚拟继承,也需要调整this指针
```cpp
class point2d{
  public:
  point2d()
  virtual ~point2d()
  virtual void mumble()
  virutal  float z()
  protected:
  float _x,_y;
  } 
  class point3d:public virtual point2d{
    public:
    point3d()
    ~point3d
    protected:
    float _z;
    }```
    可以尝试下写出例子比较point2d和point3d指针看指向是否相同
    当virtual base class中包含virtual func和nonstatic member时,各个厂商实现不同,复杂,作者不建议这么做
    
    -------------------
#### 函数的效能:
  这部分可以通过函数的调用方式感性的了解,或者根据不同的编译器。测试对比几种调用的时间,以及是否已经开启优化等
  (如编译器将被视为不变的表达式提到循环之外)
  (通过消除局部对象的使用可以消除对constructor的调用)
  
#### 指向memeber func的指针:
  取一个nonstatic member func的地址,若为nonvirtual 则为其在内存中的真正地址。
  但需要this参数。 
  指向member func的指针:double (Point::*pmf)();类似
  定义:double (point::*coord)() =&point::x;
       赋值:coord=&point::y
       调用:(origin.*coord)()/(ptr->*coord)()
       转换为:(coord)(&origin)/(coord)(ptr)
       
##### 指向virtual memeber func指针
  在g++中
  ```cpp
  #include<iostream>
#include<stdio.h>
using namespace std;
class Base1 {
        public:
                virtual float getp() { return mp;}
                void setp(float mmp){mp=mmp;}
                virtual int test(){return 3;}
                virtual int test1(){return 4;}
        private:
                float mp;
}; 
int main()
{  
        float (Base1::*pmf)()=&Base1::getp;
        Base1 *ptr=new Base1;
        ptr->setp(3.2);
        cout<<ptr->getp()<<endl;//3.2
        cout<<(ptr->*pmf)()<<endl;//3.2  会被内部转换:(*ptr->vptr[(int)pmf])(ptr)
        printf("%p\n",&Base1::getp);//1 索引值
        printf("%p\n",&Base1::setp);//40xxx真实地址
        printf("%p\n",&Base1::test);// 9,为什么是9不清楚
        printf("%p\n",&Base1::test1);//11
        
        return  0;
}```
  所以,对于虚拟函数也是可以通过函数指针来访问的,只不过得到的地址是索引值,但不影响访问
或者对以下
```cpp
#include<iostream>
#include<stdio.h>
using namespace std;
class Base1 {
        public:
                virtual float getp() { return mp;}
                void setp(float mmp){mp=mmp;}
                    int test(){return 3;}
                virtual int test1(){return 4;}
        private:
                float mp;
}; 
int main()
{  
        float (Base1::*pmf)()=&Base1::getp;
        Base1 *ptr=new Base1;
        ptr->setp(3.2);
        cout<<ptr->getp()<<endl;
        cout<<(ptr->*pmf)()<<endl;
        printf("%p\n",&Base1::getp);
        printf("%p\n",&Base1::setp);
        printf("%p\n",&Base1::test);
        printf("%p\n",&Base1::test1);
                  
                int (Base1::*pmi)()=&Base1::test1;//or test 可以指向两种,编译器如何区分呢?cfront2 通过判断是索引(may <127)还是函数地址来区分
                Base1 *ptr2=new Base1;
                cout<<(ptr2->*pmi)()<<endl;
                delete ptr;
                delete ptr2;
        
        return  0;
}```
##### 多重继承下指向member func指针
为了让mem func point能支持多重继承和虚拟继承:
噢这种情况很复杂了。你得知道它指向的是virtual 还是non virtual
书中并没有探讨太多,野比较复杂,这里举一个例子,探索细节可以看编译器,gdb等结果:
```cpp
#include<iostream>
#include<stdio.h>
using namespace std;
class Base1 {
        public:
                virtual float getp() { return mp;}
                void setp(float mmp){mp=mmp;}
                    int test(){return 3;}
                virtual int test1(){return 4;}
        private:
                float mp;
};
class Base2 {
        public:
                virtual float get2p(){return m2p;}
                virtual void set2p(float m2pp){m2p=m2pp;}
                int test22() {return 5;}
                virtual int test21(){return 6;}
        private:
                float m2p;
};
class Der:public Base1,public Base2{
        public:
                virtual float get3p(){return m3p;}
        private:
                float m3p;

};
int main()
{  
        float (Base1::*pmf)()=&Base1::getp;//这后面的调用就发挥想象把,想怎么尝试都行
        Base1 *ptr=new Der;
        ptr->setp(3.2);
        cout<<ptr->getp()<<endl;
        cout<<(ptr->*pmf)()<<endl;
        printf("%p\n",&Der::getp);
        printf("%p\n",&Base1::setp);
        printf("%p\n",&Base1::test);
        printf("%p\n",&Base1::test1);
                  
                int (Base1::*pmi)()=&Base1::test1;//or test
                Base1 *ptr2=new Base1;
                cout<<(ptr2->*pmi)()<<endl;
                delete ptr;
                delete ptr2;
        
        return  0;
}```
##### 效能
##### inline func:
首先得直到,内联函数实际上表现为什么?函数本质上被经过栈调用,而内联函数是直接 在主函数中铺开为表达式,所以调用内联函数 能提高效率,但是响应的
源代码会变大,而且有参数的限制
应用:在操作符函数中,指定set,get类函数,使得操作符函数能被写成普通的函数。 而set get 写成inline函数,会减少效率降低
inline关键词只是请求,并不会所有都inline展开,编译器会做判断权衡
具体看书,不是很细
内联函数两个注意点:
形式参数:inline扩展时,每一个形式参数会被对应的实际参数代替:
```cpp
        inline int min (int i,int j){
                   return i<j?i:j;
                   }
         三个调用:
         inline int bar() {
            int minval;
            int val1=1024;
            int val2=2048;
            minval=min(val1,val2); 参数直接替换val1<val2?val1:val2;
            minval=min(1024,2048);替换后直接使用常量:1024
            minval=min(fool(),bar()+1) 引发参数副作用,需要导入一个临时对象,以避免重复求值:
                     int t1,t2; minval=(t1=foo()),(t2=bar()+1),t1<t2?...)
           
           
            return minval}```
 局部变量:在内联函数中加入局部变量,会导致因为维护局部变量带来的成本;  
 内联函数在替代c宏,是比较安全的;但是若宏中参数有副作用的话,如上,会导致大的扩展码以维护局部变量或者生成临时的变量