C++构造函数

C++构造函数

构造函数

一、构造函数

1.1、禁止对象产生于 heap 之中

1、对象被直接实例化
2、对象被实例化为 derived class object 内的 base class 成分
3、对象被内嵌于其他对象之中

1.2、构造函数语意学

1、nontrivial : 有用的
2、bitwise : 对每一个 bit 施以

1.3、构造函数为什么不能是虚函数

1、构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。

2、虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化 vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。

3、从存储空间角度,虚函数对应一个指向 vtable 虚函数表的指针,这个指向 vtable 的指针其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable 来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找 vtable 呢?所以构造函数不能是虚函数。

4、从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。

5、构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型。

6、从实现上看,vtable 在构造函数调用后才建立,因而构造函数不可能成为虚函数。从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数),而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有必要成为虚函数。

7、当一个构造函数被调用时,它做的首要的事情之一是初始化它的 vptr。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。所以它使用的 vptr 必须是对于这个类的 vtable。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,vptr 将保持被初始化为指向这个vtable,但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置 vptr 指向它的 vtable,直到最后的构造函数结束。vptr 的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的另一个理由。但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置 vptr 指向它自己的 vtable。如果函数调用使用虚机制,它将只产生通过它自己的 vtable 的调用,而不是最后的 vtable(所有构造函数被调用后才会有最后的 vtable)。

1.4、类成员初始化

对类成员进行初始化有两种方式:

  1. 构造函数后面跟冒号;
  2. 构造函数里面对成员进行赋值。

根据 C++ 的规则,const 类型和引用不可以被赋值,只能被初始化。const 类型和引用必须在声明的时候就初始化,换句话说就是在给 const 和引用类型变量分配内存的时候就初始化。C++ 给类成员初始化的唯一方式就是成员初始化列表,也就是构造函数后面跟冒号的那种形式。

在构造函数里面调用等于号 = 并不是真正意义上的初始化,这个过程相当于:

  1. 系统创建成员变量;
  2. 创建完后再进行赋值操作。

而在构造函数后面跟冒号,就相当于:

  1. 系统创建成员变量并且初始化。也就是系统为成员变量分配了一块内存并且把相应的数据给填了进去。
  2. 而构造函数里面调用等于号的方式是分配好后再进行赋值,多了一个步骤。

构造函数后面跟的冒号代码是在进入构造函数并且在括号里面的第一行代码之前被执行。

通俗的讲,构造函数后面的冒号就是初始化,而括号里面的等于号并不是初始化,而是变量生成以后的赋值而已(永远都是2个步骤)。

引用初始化完成后,就永远指向初始化时候的那个变量,无法再改变了。

1.5、C++ 中可以在构造函数中调用另一个构造函数吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdlib.h>
#include <iostream>
using namespace std;

struct CLS
{
int m_i;
CLS( int i ) : m_i(i){}
CLS()
{
CLS(0);
}
};

int main()
{
CLS obj;
cout << obj.m_i << endl;

system("PAUSE");
return 0;
}

打印结果是不定的,不一定为0。我们知道,当定义一个对象时,会按顺序做2件事情:
1、分配好内存(非静态数据成员是未初始化的)
2、调用构造函数(构造函数的本意就是初始化非静态数据成员)

显然上面代码中,CLS obj; 这里已经为 obj 分配了内存,然后调用默认构造函数,但是默认构造函数还未执行完,却调用了另一个构造函数,这样相当于产生了一个匿名的临时 CLS 对象,它调用 CLS(int) 构造函数,将这个匿名临时对象自己的数据成员 m_i 初始化为0;但是 obj 的数据成员并没有得到初始化。于是 obj 的 m_i 是未初始化的,因此其值也是不确定的。

由上,归纳如下:
1、在 C++ 里,由于构造函数允许有默认参数,使得这种构造函数调用构造函数来重用代码的需求大为减少

2、如果仅仅为了一个构造函数重用另一个构造函数的代码,那么完全可以把构造函数中的公共部分抽取出来定义一个成员函数(推荐为 private),然后在每个需要这个代码的构造函数中调用该函数即可

3、偶尔我们还是希望在类的构造函数里调用另一个构造函数,可以按如下方式做:在构造函数里调用另一个构造函数的关键是让第二个构造函数在第一次分配好的内存上执行,而不是分配新的内存,这个可以用标准库的 placement new 做到

1
2
3
4
inline void *__cdecl operator new(size_t, void *_P)
{
return (_P);
}

正确的方式:

1
2
3
4
5
6
7
8
9
struct CLS
{
int m_i;
CLS(int i) : m_i(i) {}
CLS()
{
new (this)CLS(0);
}
};

若构造函数调用自身,则会出现无限递归调用,是不允许的。

所以,在实际使用的时候,单纯的在构造函数中调用其它的构造函数,只是会产生一个临时的匿名变量。如果仅仅是为了重用代码,可以把重用的代码封装成一个新的函数。

二、拷贝构造

2.1、C++ 中拷贝构造函数的定义传参问题

在 C++ 中,我们使用拷贝构造函数来实现对象的复制。我们需要注意的是,在定义拷贝构造函数的时候,传入参数不能是传值参数,例如 A(A other)。因为如果是传值函数,就会在拷贝构造函数内将形参复制为实参,而复制的时候又会调用拷贝构造函数,这样就会造成无休止的递归调用,导致栈溢出,因此 C++ 不允许拷贝构造函数传递值参数,最好将拷贝构造函数修改为传递常量引用。

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
#include<iostream>

using namespace std;

class A
{
private:
int value;
public:
A(int n)
{
value = n;
}
A(A other)
{
value = other.value;
}
void Paint()
{
cout << value << endl;
}
};
// 无法通过编译
int main()
{
char c;
A a = 10;
A b = a;
b.Paint();
cin >> c;
return 0;
}

正确的写法,如下所示:

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
#include<iostream>

using namespace std;

class A
{
private:
int value;
public:
A(int n)
{
value = n;
}
A(const A& other)
{
value = other.value;
}
void Paint()
{
cout << value << endl;
}
};

int main()
{
char c;
A a = 10;
A b = a;
b.Paint();
cin >> c;
return 0;
}

2.2、C++ 拷贝构造函数(深拷贝,浅拷贝)

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
#include <iostream>
using namespace std;

class CExample
{
private:
 int a;

public:
CExample(int b)
{
a=b;
}
void Show()
{
cout<<a<<endl;
}
};

int main()
{
 CExample A(100);
 CExample B=A;
 B.Show();
 return 0;
}

运行程序,屏幕输出 100。从以上代码的运行结果可以看出,系统为对象 B 分配了内存并完成了与对象 A 的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。下面举例说明拷贝构造函数的工作过程。

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
#include <iostream>
using namespace std;

class CExample {
private:
int a;
public:
CExample(int b)
{ a=b;}

CExample(const CExample& C)
{
a=C.a;
}
void Show ()
{
cout<<a<<endl;
}
};

int main()
{
CExample A(100);
CExample B=A;
B.Show ();
return 0;
}

CExample(const CExample& C) 就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是 const 类型,是不可变的。例如:类 X 的拷贝构造函数的形式为 X(X& x)。

当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:

  • 一个对象以值传递的方式传入函数体
  • 一个对象以值传递的方式从函数返回
  • 一个对象需要通过另外一个对象进行初始化

如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,后面将进行说明。

自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。

浅拷贝和深拷贝

在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如 A=B。这时,如果 B 中有一个成员变量指针已经申请了内存,那 A 中的那个成员变量也指向同一块内存。这就出现了问题:当 B 把内存释放了(如析构),这时 A 内的指针就是野指针了,出现运行错误。

深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。下面举个深拷贝的例子。

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
#include <iostream>
using namespace std;

class CA
{
 public:
  CA(int b, char* cstr)
  {
   a=b;
   str=new char[b];
   strcpy(str, cstr);
  }
  CA(const CA& C)
  {
   a=C.a;
   str=new char[a]; //深拷贝
   if(str!=0)
    strcpy(str, C.str);
  }
  void Show()
  {
   cout<<str<<endl;
  }
  ~CA()
  {
   delete str;
  }
 private:
  int a;
  char *str;
};

int main()
{
 CA A(10, "Hello!");
 CA B=A;
 B.Show();
 return 0;
}

深拷贝和浅拷贝的定义可以简单理解成:如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候,这个过程就可以叫做深拷贝,反之对象存在资源,但复制过程并未复制资源的情况视为浅拷贝。

浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程序运行出错。

Test(Test &c_t) 是自定义的拷贝构造函数,拷贝构造函数的名称必须与类名称一致,函数的形式参数是本类型的一个引用变量,且必须是引用。

当用一个已经初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用,如果你没有自定义拷贝构造函数的时候,系统将会提供给一个默认的拷贝构造函数来完成这个过程,上面代码的复制核心语句就是通过 Test(Test &c_t) 拷贝构造函数内的 p1=c_t.p1; 语句完成的。

参考文章
C++拷贝构造函数(深拷贝,浅拷贝)

评论

:D 一言句子获取中...

加载中,最新评论有1分钟缓存...