Inside C++ object Model--构造函数

简介:

默认构造函数

构造函数是干啥的, 是在构造类对象的时候, 给程序员进行对象初始化操作的机会. 不仅如此, 同时也是给编译器进行对象初始化的机会. 当然程序员和编译器的扮演的角色是不一样的, 考虑的问题也是不一样的.

当程序员觉得这个类对象没有任何初始化的必要时, 他就不会特意去声明构造函数.
那么对于一个类, 当程序员没有声明任何构造函数的时候, 编译器有可能 会为该类声明一个default 构造函数. 之所以是'有可能', 是因为编译器也是很懒的, 如果他也觉得这个类没有任何初始化的必要时, 他其实也是不会真正构造default 构造函数的. 只有当他认为这个构造函数为nontrivial, 即他确实需要对类对象进行某些初始化操作时, 才会创建default 构造函数.

下面先举个例子, 从程序员的角度 , 啥时候需要构造函数,
class Foo { public: int val; Foo *pnext; };

void foo_bar()
{
    // Oops: program needs bar's members zeroed out
    Foo bar;
    if ( bar.val || bar.pnext )
        // ... do something
    // ...
}
如上面的代码, 程序员期望创建对象的时候, 会生成default 构造函数对成员变量进行初始化清零, 而实际上在编译器看来, 这种活是程序员应该做的. 所以编译器不会特意构造default 构造函数来初始化类成员.

那么这段逻辑就是有bug的, 这也是新手很容易范的一个错,
Global objects are guaranteed to have their associated memory "zeroed out" at program start-up. Local objects allocated on the program stack and heap objects allocated on the free-store do not have their associated memory zeroed out; rather, the memory retains the arbitrary bit pattern of its previous use.
对于全局变量, 程序开始时会清零, 但对于在stack和heap上分配的内存, 会保持上次使用时的遗留数据, 所以如果不手工清零, 你有可能得到任意值. 而这步清零操作就是程序员应该负责在构造函数中调用的, 而不是编译器的责任.

那从编译器的角度 , 啥时候是一定需要在构造函数里面加上对象初始化操作的
有以下4种情况,
Member Class Object with Default Constructor 
对于对象成员也是一个类对象, 并且该类定义了Default Constructor, 编译器必须保证该成员对象被正确的初始化, 即Default Constructor被调用.
Base Class with Default Constructor 
同样编译器必须保证该base class被正确初始化

Class with a Virtual Function 
这种情况有两种case,
1. The class either declares (or inherits) a virtual function
2. The class is derived from an inheritance chain in which one or more base classes are virtual
对于有虚函数的类, 编译器必须额外做下面两件事, 以保证多态能够正常工作,
1. A virtual function table (referred to as the class vtbl in the original cfront implementation) is generated and populated with the addresses of the active virtual functions for that class.
2. Within each class object, an additional pointer member (the vptr ) is synthesized to hold the address of the associated class vtbl.
说白了, 编译器必须创建虚表, 并在每个对象中添加指向虚表的指针.
Class with a Virtual Base Class 
对于Virtual Base Class的实现, 各个编译器有很大的不同
what is common to each implementation is the need to make the virtual base class location within each derived class object available at runtime .
所以编译器必须保证在构造函数中确定虚基类在执行期的地址

对于上面的4种情况, 编译器必须创建Default Constructor来做特殊处理, 如果程序员已经定义了构造函数, 编译器也要在该构造函数中插入特殊处理. 除了上面4种情况, 编译器不会额外产生Default Constructor, 也不会对对象做任何额外的初始化操作, 如果想做什么, 程序员请自己搞定.

拷贝构造函数

有三种情况会使用到copy构造函数, 即以一个对象的内容作为另一个对象的初值.

对一个对象做明确的初始化操作,

class X { ... }; 
X x; 
// explicit initialization of one class object with another 
X xx = x;

另外两种情况是当object被当作参数交给某个函数, 或者当函数返回一个对象时.

同样copy构造函数用于当使用一个类对象初始化另一个类对象时, 给程序员或编译器一个做某些特殊处理的机会.
同样程序员和编译器需要考虑的问题是不同的,
对于程序员举个典型的例子来说明copy构造函数的意义,
当对象成员为指针时, 如果仅仅赋值指针本身, 会导致多个指针指向同一对象, 当一个指针把对象释放后, 其他地方就会报错.
所以程序员在copy构造函数中, 需要新创建指针成员指向对象的copy, 并将新对象的成员指针指向该copy, 这样才是完全拷贝.

当程序员不特意创建copy构造函数时, 编译器在大部分情况下默认为Bitwise Copy Semantics, 即把一个对象中的数据原封不动的按bit拷到需初始化的对象中. 所以对上面说的成员是指针的情况, 会导致两个对象的成员指针指向同一个对象实体的不合理的情况.
但是对于下面4种情况, 编译器必须要特意生成copy构造函数来保证copy的正确性,
1. When the class contains a member object of a class for which a copy constructor exists

2. When the class is derived from a base class for which a copy constructor exists

3. When the class declares one or more virtual functions 

4. When the class is derived from an inheritance chain in which one or more base classes are virtual 
1和2很容易理解, 编译器必须保证member object或base class中程序员所写的copy constructor被调到
3和4相对复杂一些, 其实对于这两种情况, 如果是相同类之间的对象互相初始化, 也是不用做特殊处理的, 看下面的例子
class ZooAnimal {......}
class Bear : public ZooAnimal {......}

Bear yogi;
Bear winnie = yogi;  //对于这种情况, 直接拷贝就可以了
ZooAnimal franny = yogi;  //但是这种情况, 编译器必须要调整franny的虚表指针, 因为yogi的vptr是指向Bear的, 要改成ZooAnimal
对于有虚函数的类, 基类与派生类之间的初始化, 编译器必须产生copy构造函数去调整vptr的值
对于有虚基类的情况, 同样也是当基类与派生类之间的初始化时, 编译器需要产生copy构造函数去仲裁虚基类对象的位置.

总结, 其实对于default构造函数和copy构造函数而言, 只有当在类成员对象或基类中有特殊的构造函数需要调用, 或类含有虚函数和虚基类需要做特殊处理外, 编译器不会对类对象做任何初始化操作, 大家不用怀疑编译背地里做了太多操作, 他也很懒的...

拷贝构造函数引起的程序转换

上面说了调用copy构造函数的3种情况, 可是他们都不是显式的调用, 所以编译器需要做程序转换, 下面就分情况说明

1. 显式的初始化

X x0; 
the following three definitions each explicitly initialize its class object with x0:
void foo_bar() { 
   X x1( x0 ); 
   X x2 = x0; 
   X x3 = x( x0 ); 
   // ... 
}

对于严谨的C++编译器, 定义就是指占用内存, 所以对于它而言, 定义和初始化式分开的

// Possible program transformation 
// Pseudo C++ Code 
void foo_bar() { 
   X x1; 
   X x2; 
   X x3; 

   // compiler inserted invocations 
   // of copy constructor for X 
   x1.X::X( x0 ); 
   x2.X::X( x0 ); 
   x3.X::X( x0 ); 
   // ... 
}

这个转换很好理解.

2. 参数的初始化

void foo( X x0 ); 
an invocation of the form

X xx; 
// ... 
foo( xx );

对于这样的参数传递, 其实编译器会做如下转换

// Pseudo C++ code 
// compiler generated temporary 
X __temp0; 

// compiler invocation of copy constructor 
__temp0.X::X ( xx ); 

// rewrite function call to take temporary 
foo( __temp0 );

这儿会首先把xx的值通过copy构造函数初始化临时变量_temp0, 之所以需要临时变量, 是因为xx在函数外, 当进入foo函数后xx是不可见的, 所以先要保留一份xx的备份.

好, 这样进入foo函数后的第一件事,就是再把_temp0通过拷贝构造函数去初始化x0

这样传递一个参数, 调用了两遍拷贝构造函数, 挺低效的

所以, 这儿编译器可以把函数声明改写成

void foo( X& x0 );

这样的话,就可以直接把x0指向_temp0就可以了, 提高了效率

当然这边的改写策略是编译器相关的, 这儿只是一种方案, 其他的编译器方案就不介绍了.

3. 返回值初始化

X bar()
{
   X xx;
   // process xx ...
   return xx;
}

对于返回值, 编译器的会做如下转换
// function transformation to reflect
// application of copy constructor
// Pseudo C++ Code
void
bar( X& __result )
{
   X xx;

   // compiler generated invocation
   // of default constructor
   xx.X::X();

   // ... process xx
   // compiler generated invocation
   // of copy constructor
   __result.X::X( xx );

   return;
}
可见, 所谓的返回值, 其实编译器只是增加了一个result参数, 最后将xx通过拷贝构造函数初始化result
所以对于底下的调用
X xx = bar();
编译器会改写成
// note: no default constructor applied
X xx;
bar( xx );
为了提高效率, 对于返回值, 我们可以想些办法来避免拷贝构造函数的调用

a. 使用者优化 
X bar( const T &y, const T &z )
{
   X xx;
   // ... process xx using y and z
   return xx;
}
优化成,
X bar( const T &y, const T &z )
{
   return X( y, z );
}

这段代码, 编译器转换为
// Pseudo C++ Code
void
bar( X &__result )
{
   __result.X::X( y, z );
   return;
}
这样就避免了拷贝构造函数的调用, 可以直接调用构造函数来产生返回值, 但这样最大的问题, 你必须定义新的构造函数来以y,z构造X, 会导致这种特殊用途的构造函数大量扩散. 这个方法不太靠谱

b.编译器优化

X bar()
{
   X xx;
   // ... process xx
   return xx;
}

编译器如果比较聪明的话, 会自动对它进行优化, 直接用__result替换xx, 这样就不用后面费事调用拷贝构造函数了
void
bar( X &__result )
{
   // default constructor invocation
   // Pseudo C++ Code
   __result.X::X();

   // ... process in __result directly

   return;
}
This compiler optimization, sometimes referred to as the Named Return Value (NRV) optimization.
这样的NRV优化对于需要大量调用该函数的case, 效率提高还是很明显的
但是这种优化对于逻辑比较复杂的函数, 也是无能为力的.


本文章摘自博客园,原文发布日期:2011-07-05

目录
相关文章
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
98 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
85 4
|
3月前
|
编译器 C++
C++ 类构造函数初始化列表
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。
81 30
|
2月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
30 1
|
2月前
|
C++
C++构造函数初始化类对象
C++构造函数初始化类对象
26 0
|
2月前
|
C++
C++入门4——类与对象3-2(构造函数的类型转换和友元详解)
C++入门4——类与对象3-2(构造函数的类型转换和友元详解)
29 0
|
4月前
|
编译器 C++
C++的基类和派生类构造函数
基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。 在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。 这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。 下面的例子展示了如何在派生类的构造函数中调用基类的构造函数:
|
6月前
|
安全 编译器 C++
C++一分钟之-构造函数与析构函数
【6月更文挑战第20天】C++中的构造函数初始化对象,析构函数负责资源清理。构造函数有默认、参数化和拷贝形式,需注意异常安全和成员初始化。析构确保资源释放,避免内存泄漏,要防止重复析构。示例代码展示了不同构造函数和析构函数的调用情况。掌握构造和析构是有效管理对象生命周期和资源的关键。
54 2
|
5月前
|
编译器 C++
【C++】详解构造函数
【C++】详解构造函数
|
7月前
|
编译器 C++
C++的基类和派生类构造函数
在 C++ 中,类的构造函数不能被继承,但基类的普通成员函数可以在派生类中访问。派生类必须通过其构造函数初始化继承的成员变量,由于私有成员变量无法直接初始化,因此需要在派生类构造函数中调用基类的构造函数来完成。示例代码显示了如何在派生类构造函数中调用基类构造函数,确保正确初始化。构造函数的调用顺序遵循自顶向下、从基类到派生类的规则,且只能调用直接基类的构造函数。如果基类没有默认构造函数,而派生类未指定构造函数调用,会导致编译错误。