技术开发 频道

C++/CLI 思辨录汇粹集

【IT168 技术文档】

  代理构造函数

  对一个类来说,有多个构造函数是经常的事;并且这多个构造函数有一段共同的代码也很经常。一般地,在这种情况下,我们都是为该共同代码段编写一个独立的函数,然后放在每个构造器中调用。如下例:

class Foo {  private:   int _mem;  public:   Foo() : _mem(0)   {    CommonConstructor();   }   Foo(int mem) : _mem(mem)   {    CommonConstructor();   }   Foo(const Foo& f) : _mem(f._mem)   {    CommonConstructor();   }   // 我们所有的构造器都需要的代码段   void CommonConstructor()   {    printf("Constructing the object");   } };
  但是现在利用C++/CLI引入的新特性-代理构造器,我们可以从一个称为基类构造器的构造器中调用另一个构造器。当你这样做时,执行控制转入到第二个构造器中,在其执行完后再返回到第一个构造器。下面代码片断中的类Foo2演示了这种方法:
class Foo2 {  private:   int _mem;  public:   // 该构造器调用第二个称为基类构造器的构造器   Foo2() : Foo2(0)   {   }   // 下面这个构造器包含由所有构造器使用的公共代码   Foo2(int mem) : _mem(mem)   {    printf("Constructing the object");   }   Foo2(const Foo2& f) : Foo2(f._mem)   {   } };
  然而,每个类都应该包含至少一个非代理构造器,不过该构造器仍然可以有调用一个或者多个基类构造器的初始化操作。
 
  注意,早期情况下,如果你想试用一下代理构造函数,应该会出现一个编译错误。请注意,这是由于资源问题缺乏导致的,在现在版本的C++/CLI中可以避免这一错误。
 
     拷贝构造函数
 
  虽然对象复制看上去很简单,然而如果你没有对其正确理解,可能会出现一些严重问题。默认情况下,复制对象会导致相应的所有成员的复制。如果你只有实例成员,这看上去是相当不错的。但是如果你的类中含有指向在堆中分配的对象时,情况会怎样呢?考虑下面的代码片断:
#include <stdio.h> #include <string.h> class Person {  private:   char* _name;  public:   Person()   {    _name = new char[256];   }   void SetName(const char* name)   {    if(strlen(name) + 1 < 256)     strcpy(_name,name);   }   void PrintName()   {    printf("%s\n",_name);   }  };  int main()  {   // 创建对象的第一个实例并赋于名字为John   Person p1;   p1.SetName("John");   p1.PrintName();   //通过复制p1引用的对象创建另一个对象   Person p2(p1);   p2.SetName("Alice");   p2.PrintName();   //现在再输出p1的名字   p1.PrintName();   scanf("q");   return 0;  }
  这里的类Person有一个指向在堆上分配的字符数组的指针。当构造Person对象时,它创建该字符数组并把它的位置存放到变量_name中。
 
  但是当你创建Person 对象 p2 时,p2的成员用p1的成员初始化。因而,p1的 _name与p2的 _name指向相同的堆对象。如在上例中看到的,调用p2.SetName将改变由这两个类共享的值。所以,当第二次调用p1.PrintName,打印结果是"Alice"。
 
  所以,这不是我们复制对象所期望的结果,而且还会导致堆崩溃的问题。请再考虑某个函数删除了该数组而p1又要调用该函数的情况?下面,当p2调用PrintName时,它将尽量存取实际上不是在堆上的对象。这种情况下产生的结果往往是难以预料的。
 
  C++允许我们通过定义拷贝构造函数来克服这类问题。在我们每次通过复制另一个对象来初始化一个对象时,拷贝构造函数都被执行。你可以在拷贝构造函数中覆盖掉缺省的成员函数的复制行为。
 
  所以,我们的类Person应该修改如下:
class Person {  private:   char* _name;  public:   Person()   {    _name = new char[256];   }   // 这是拷贝构造函数。在此我们初始化一个新的数组,为Person的实例所用   Person(Person&)   {    _name = new char[256];   }   void SetName(const char* name)   {    if(strlen(name) + 1 < 256)     strcpy(_name,name);   }   void PrintName()   {    printf("%s\n",_name);   } };
  这里类Person中的拷贝构造函数保证了它初始化一个新的数组,为在复制时产生的每一个对象实例所用。这就避免了前面我们提到的问题。

  内部指针的两面性

  在本文中,我将解释为什么使用本地指针来管理对象。原因在于对象是以垃圾收集器移动的。所以,当对象移动时,本地指针就变成无效的。所以,如果你想取得一个托管对象的本地指针,你就会遇到一个编译错。下面的代码显示了这上点。

using namespace System; int _tmain() {  int ^ hnd = gcnew int(100);  int* np = &hnd; // Genarates a compile error }
  但是本地指针还是非常有用的,如在使用指针算术和指针比较时就有许多的优点。所以新的C++ CLI允许你使用一个内部指针实现同样的功能。内部指针是本地指针的一个超集。所以它能够做任何内部指针所能做的一切。 但是当垃圾收集器移动指针所指向的对象时,内部指针也帮助程序实现其指向的地址的更新。
 
  现在让我们看一下如何用内部指针来实现指针算术运算。
#include "stdafx.h" #include <stdio.h> using namespace System; using namespace stdcli::language; int _tmain() {  const int SIZE = 10;  array<int>^ arr = gcnew array<int>(SIZE);  for(int idx = 0; idx < SIZE; idx++)  {   arr[idx] = idx + 1;  }  // 取得指向数组第一个元素的指针  interior_ptr p = &arr[0];  // 通过增加指针值读取并输出数组元素  for(int idx = 0; idx < 10; idx++)  {   printf("Value of the element at %Xh ", p);   Console::WriteLine(" is {0}",*(p++));  }  Console::ReadLine(); }
  不管垃圾收集器执行多少次和数组元素移动多少次,上面的代码仍然能工作良好。于是C++的力量又回到了.net运行时刻库上。但是要小心使用内部指针。这些指针与本地指针非常相似,当试图存取不允许操作的内存区段时能给程序造成危害。例如,如果我在上面的代码中试图存取下一个p++,它将返回恰好在上面数组元素上方4字节的内存段位置。
 
     传递托管堆地址
 
  新的C++特点平衡了把托管堆的地址传递到非托管代码的能力。早期我们遇到的最大问题是,在托管堆中的对象的位置是非静态的。垃圾收集器以变化的时间间隔移动对象。现在新的pin_ptr(别针型指针)的引入禁止垃圾收集器改变在堆上的对象的地址。
 
  下面代码展示了别针型指针的应用。
#pragma unmanaged //本机函数,以整型指针作参数,执行计算 void calc(int* val) {  //执行计算操作 } #pragma managed //托管函数调用本机calc函数 int managedfunc() {  int i=gcnew int(10);  //把i的地址设置为别针型,以阻止对i的移动  pin_ptr<int> ppi=&i;  int* np=ppi;  calc(np);//用别针型int的地址调用本机函数  //把别针型指针的值置为nullptr,脱离了gc的限制  ppi=nullptr;  //完成剩下的工作  return i; }
  基于轮廓的优化(Profile Guided Optimization)
 
  在新版本的托管C++中有一项新增功能称为基于轮廓的优化。这一新的编译特点能够实现在编译时刻把探针注入到代码中。最后的exe文件与一个数据库一起打包发送,由该库记录下注入代码中的探针监测到的结果数据。当用户运行该程序时,这些探针记录下应用程序的使用。当你下一次再编译时,编译器作出智能性决策,如根据探针的记录作为相应的动作。这一特性带来了真实世界的优化。
 
  为什么没有为使用托管类型而定义头文件?
 
  在传统的C++中,一个头文件一般包含对象的接口,或者类与函数的声明,等等。这允许在多个翻译单元上实现某种类型的一致性声明。对于一个用托管代码编写的且其被编译成MSIL的对象,需要在配件集中包含描述对象的元数据。因而,托管C++编译器不是通过头文件,而是通过使用配件集元数据来实现接口的读取。这就是为什么你不必因使用托管类型而包含头文件。事实上,你可以通过使用#using指令来让编译器从配件集中读取元数据。

0
相关文章