C++对象模型(5) -- 对象构造语义学

一、继承体系下的对象构造步骤及虚函数调用

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
class A
{
public:
A()
{
printf("A this = %p\n", this);
cout << "A::A()" << endl;
}
virtual ~A() {}
virtual void myvirfunc() {}
virtual void myvirfunc2() {}
};
class B:public A
{
public:
B()
{
printf("B this = %p\n", this);
cout << "B::B()" << endl;
}
virtual ~B() {}
virtual void myvirfunc() {}
virtual void myvirfunc2() {}
};
class C:public B
{
public:
C():m_c(11)
{
myvirfunc(); //构造函数中,这里没有走虚函数表,而是直接通过虚函数地址,直接调用这个虚函数(静态方式调用)
myvirfunc1(); //虚函数中再调虚函数,走虚函数表
printf("C this = %p\n", this);
cout << "C::C()" << endl;
}
virtual ~C() {}
virtual void myvirfunc() {}
virtual void myvirfunc1() { myvirfunc2(); }
virtual void myvirfunc2() {}
int m_c;
};
int main()
{
C cobj;

C *mycobj = new C();
mycobj->myvirfunc(); //代码实现上的多态
return 1;
}

  (1)继承关系为:C继承于B,B继承于A。当定义一个对象的时候,构造函数的调用顺序为:类A的构造函数、类B的构造函数、类C的构造函数。
  (2)当类中有虚函数,并且在构造函数中调用时,如果被调用虚函数里面没有虚函数,则不是走虚函数表调用,而是直接通过虚函数地址静态调用。如果被调用虚函数里面再调用其他虚函数,则就会走虚函数表调用。

二、对象拷贝、析构函数

对象的拷贝行为

  (1)如果我们不写自己的拷贝构造函数和拷贝赋值运算符,编译器也会有默认的对象拷贝和对象赋值行为。
  (2)当我们提供自己的拷贝赋值运算符和拷贝构造函数时,我们就接管了系统默认的拷贝行为,此时,我们有责任在拷贝赋值运算符和拷贝构造函数中写适当的代码,来完成对象的拷贝或者赋值的任务。
  (3)要想禁止对象的默认拷贝构造和赋值,只要把拷贝构造函数和拷贝赋值运算符私有起来,只声明,不需要写函数体。

析构函数

  编译器会给我们生成一个析构函数的情况:
  (1)如果继承一个基类,基类中带析构函数,那么编译器就会给我们合成出一个析构函数来调用基类中的析构函数。
  (2)如果类成员是一个类类型成员,并且这个成员带析构函数,编译器也会合成出一个析构函数,这个析构函数存在的意义是要调用这个类类型成员所在类的析构函数。
  如果我们有自己的析构函数,那么编译器就会在适当的情况下扩展我们的析构函数代码:
  (1)如果类成员是一个类类型成员,并且这个成员带析构函数,编译器就扩展这个类的析构函数代码,即先执行了本类的析构函数代码,再执行类类型的析构函数代码。
  (2)如果继承一个基类,基类中带析构函数,那么编译器就会扩展我们类的析构函数来调用基类中的析构函数。

三、局部类对象、全局类对象的构造和析构函数

  (1)对于局部类对象,只要出了对象的作用域,编译器总会在适当的地方插入调用对象析构函数的代码。因此局部对象应该现用现定义,这样可以减少某些情况下的开销(如果在开头处定义对象,而程序还没有运行到使用对象的时候就已经退出,此时就算没有用到对象,编译器也会调用局部类对象的析构函数,这会增加不必要的开销)。
  (2)全局变量是放在数据段里的,在编译阶段就会把空间分配出来(全局变量的地址在编译期间就确定好的)。全局对象,在不给初值的情况下,编译器默认会把全局对象所在内存全部清0。
  (3)全局对象构造和析构的步骤:
   a)全局对象获得地址(编译时确定好的,内存也是编译时分配好的,内存时运行期间一直存在)
   b)把全局对象的内存内容清0(也叫静态初始化)
   c)调用全局对象所对应的类的构造函数
   d)main(){……}
   e)调用全局对象所对应类的析构函数
  (4)全局对象在main函数执行之前就被构造完毕,可以在main函数中直接使用,在main函数执行完毕后才被析构掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A
{
public:
A()
{
cout << "A::A()" << endl;
}
~A()
{
cout << "A::~A()" << endl;
}
int m_i;
};
A g_aobj; //全局对象
int main()
{
A aobj; //局部对象
return 1;
}

四、局部静态对象的构造和析构

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
class A
{
public:
A()
{
cout << "A::A()" << endl;
}
~A()
{
cout << "A::~A()" << endl;
}
int m_i;
};
const A &myfunc()
{
//局部静态对象
static A s_aobj1;
printf("s_aobj1的地址是%p\n", &s_aobj1);
return s_aobj1;
}
int main()
{
myfunc();
myfunc();

return 1;
}

  (1)如果我们不调用myfunc()函数,那么根本不会触发A的构造函数。
  (2)局部静态对象,内存地址是在编译期间就确定好的。
  (3)静态局部变量刚开始也被初始化为0。
  (4)局部静态对象的析构,是在main函数执行结束后才被调用的。(前提是这个静态局部对象被构造过)
  (5)不管myfunc()函数被调用几次,s_aobj1这种静态局部对象只会被构造1次。(只调用一次构造函数)

五、new和delete

  (1)new类对象时加不加括号的差别:如果是个空类,那么加不加括号没有区别(现实中,不可能光写一个空类)。如果类中有成员变量,则带括号的初始化会把一些和成员变量有关的内存清0,但不是整个对象的内存全部清0。当类中有构造函数,两种写法都会调用类的构造函数。
  (2)new干了两个事:一个是调用operator new(malloc),一个是调用了类A的构造函数。delete也干了两个事:一个是调用了类A的析构函数,一个是调用operator delete(free)。

六、临时对象

拷贝构造函数相关的临时对象

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
class A
{
public:
A()
{
cout << "A::A()构造函数被执行" << endl;
}
A(const A& tmpobj)
{
cout << "A::A()拷贝构造函数被执行" << endl;
}
~A()
{
cout << "A::~A()析构函数被执行" << endl;
}
};

A operator+(const A& obj1, const A& obj2)
{
A tmpobj;
return tmpobj;
//此时编译器产生临时对象,把tmpobj对象的内容通过调用拷贝构造函数
把tmpobj的内容拷贝构造给这个临时对象,然后返回的是这个临时对象。
}
int main()
{
A myobj1;
A myobj2;
A resultobj = myobj1 + myobj2;
//这个从operator+里返回的临时对象直接构造到了resultobj里;
return 1;
}

拷贝赋值运算符相关的临时对象

class A
{
public:
    A()
    {
        cout << "A::A()构造函数被执行" << endl;
    }
    A(const A& tmpobj)
    {
        cout << "A::A()拷贝构造函数被执行" << endl;
    }
    A & operator=(const A& tmpaobj)
    {
        cout << "A::operator()拷贝赋值运算符被执行" << endl;
        return *this; 
    }
    ~A()
    {
        cout << "A::~A()析构函数被执行" << endl;
    }
};

A operator+(const A& obj1, const A& obj2)
{
    A tmpobj;
    return tmpobj; 
//编译器产生临时对象,把tmpobj对象的内容通过调用拷贝构造函数
把tmpobj的内容拷贝构造给这个临时对象,然后返回的是这个临时对象。
}

int main()
{
    A myobj1;
    A myobj2;
    A resultobj;
    resultobj = myobj1 + myobj2;    //调用拷贝赋值运算符
    //A resultobj = myobj1 + myobj2; //调用拷贝构造函数
    return 1;
}

直接运算产生的临时对象

class A
{
public:
    A()
    {
        cout << "A::A()构造函数被执行" << endl;
    }
    A(const A& tmpobj)
    {    
        cout << "A::A()拷贝构造函数被执行" << endl;
        m_i = tmpobj.m_i;
    }
    A & operator=(const A& tmpaobj)
    {
        cout << "A::operator()拷贝赋值运算符被执行" << endl;            
        return *this;
    }
    ~A()
    {
        cout << "A::~A()析构函数被执行" << endl;
    }
    int m_i;
};
A operator+(const A& obj1, const A& obj2)
{
    A tmpobj;
    tmpobj.m_i = obj1.m_i + obj2.m_i;
    return tmpobj; 
//编译器产生临时对象,把tmpobj对象的内容通过调用拷贝构造函数
把tmpobj的内容拷贝构造给这个临时对象,然后返回的是这个临时对象。
}
int main()
{
    A myobj1;
    myobj1.m_i = 1;
    A myobj2;        
    myobj2.m_i = 2;
    A resultobj = myobj1 +myobj2; //临时对象被接住,不会被立即析构
    myobj1 + myobj2; //产生了临时对象,然后该临时对象立即被析构;
    printf("(myobj1 + myobj2).m_i = %d\n", (myobj1 + myobj2).m_i);
//临时对象的析构是整行语句的最后一步,这样就能保证printf打印出来一个有效值。 
编译器要往必要的地方,帮助我们插入 A tmpobja1 = (myobj1 + 
myobj1);这样的代码,来产生临时对象供编译器完成我们程序开发者代码要实现的意图。
    return 1;
}