.NET程序员的C\C++情结

摘要

这个系列是本人在工作或工作之余开发和学习C\C++的一些笔记。本文涉及C++/CLI的一些内容。

本文为原创,首发于我的个人博客:.NET程序员的C\C++情结(3)。欢迎交流指正。转载请注明出处。

虽然现在主要从事.NET平台的开发,但是一直以来对C\C++有着那份难以割舍的情结。本文会涉及到托管C++的一些随笔记录。

当然,如果写纯.NET应用的话,C#无疑是最合适的语言的。但是托管C++在同时处理Native调用和托管调用上无疑是十分吸引人的,往往用来作为托管世界和Native世界的桥梁。当然。你可以说用.NET的“平台调用”特性同样能够胜任,萝卜青菜各有所爱吧。

托管C++基础语言特性

在托管C++中需要像下面这样定义一个托管类型

public ref class ARSession
{
public:
 property UInt32 FieldId;
}

默认情况下这样的类是默认实现IDisposable的,原因很简单,既然用到C++来封装托管类型,那么八成类型需要涉及到非托管对象,实现IDisposable减少了出错的可能。可以同时实现两种“析构函数”:

!ARSession(void)
~ARSession(void)

前者是好比Dispose(),后者是C++原生的析构函数。

可以同时引用托管的命名空间和C++命名空间

using namespace System;
using namespace System::Collections::Generic;
using namespace std;

也可以向普通C++一样#include头文件,编译的过程可以理解成跟本地C++的编译过程一样,只是在编译的时候会有/clr开关,并至少引用相应的托管dll:mscorlib.dll

对于托管类型,在类型的标识右使用”^”标注,比如:

String^
array<String^>^
List<AREntry^>^

但注意,对于Nullable的值类型,使用

Nullable<UInt32>

而不是

Nullable<UInt32>^

前者在C#中会看到是uint?,而后者在C#中会看到是ValueType

托管C++支持类似C#中的ref

Int32% totalMatch

out的话需要加一个Attribute

using namespace System::Runtime::InteropServices;    
void foo([Out] Bar^% x);

在本地堆中申请内存是使用new关键字,而在托管堆中申请内存,使用gcnew关键字:

ARException^ exception = gcnew ARException();

托管C++的内存管理

上面简单介绍的一些语言特性是我实际碰到的,可能不全。与语言特性相比,更为重要的是内存管理带来的复杂性。原生的C++只有一个由C运行库管理的“本地堆”,而C++/CLI允许同时操作本地堆和托管堆。众所周知,托管堆由CLR管理,在托管堆中的内存会随时被CLR回收和压缩,这意味着,如果使用C#的引用或者C++/CLI中的“Handle”(即由String^等“戴帽子的类型“声明的变量)来操作托管堆的内存,不会有任何问题,因为CLR会自动更改引用或Handle指向的地址。然而,如果在本地堆或者栈上的本地指针来指向托管堆上的内存的话,CLR不会对压缩内存带来的地址修改负任何责任。如果发生这种情况的话,再次使用该指针将导致内存违规。下面这张图可以解释这个现象(图片来源http://www.codeproject.com/Articles/17817/C-CLI-in-Action-Using-interior-and-pinning-pointer):

net-cpp-hobby-img0

在上图中,本地指针指向的地址本来是Data,但是当CLR的GC工作后,Data可能被压缩至托管堆的其他地方,而取而代之的是另外一块内存。很典型的情况就是,我们要在托管的byte[]和非托管的usigned char*对象之间传递内存,下面这段代码将String对象转化成以UTF8编码的字节数组:

char* MarshalStringCopyToChar(String^ Source)
{
   if(String::IsNullOrEmpty(Source))
       return NULL;
   array<Byte>^ vText = System::Text::Encoding::UTF8->GetBytes(Source);
   pin_ptr<unsigned char> pText = &vText[0];
   char* Des = (char*)calloc(vText->Length+1,sizeof(char));
   memcpy(Des, pText, vText->Length);
   Des[vText->Length] = '\0';
   return Des;
}

上述代码实际上是将托管堆中的一部分内存数据copy到非托管堆,使其奏效的关键就是pin_ptr<unsigned char>这个指针了。

在托管C++中也可以使用如下方法代替上面的实现:

std::string tmp = marshal_as<std::string>(Source);

但是,似乎在转换过程中是以ANSI编码来转换的,具体没有详细研究。不过marshal_as是可以扩展的,详见:http://msdn.microsoft.com/zh-cn/library/bb384865.aspx

C++运行库的问题

在开发过程中碰到一个很怪异的_CrtIsValidHeapPointer错误,关于这个问题,需要了解Microsoft C运行库以及其管理堆内存的一些原则:

首先,到目前为止,Microsoft C运行库实际上已经有很多版本了,在应用程序执行期间,很可能在内存中存在多个版本的C运行库,而且每个C运行库版本维护自己的堆,这样,如果在不同的运行库之间引用堆内存,那么在Debug模式下会有一个_CrtIsValidHeapPointer宏来防止这个操作(Release模式没有验证过是不是就没有这个限制了)。那么典型的场景就是,当我们在引用某个第三方动态链接库时,如果这个第三方的动态链接库所引用的C运行库跟我们的主程序不一致,那么将会在内存中同时存在两个版本的运行库,所以,如果主程序申请的堆内存,由其他dll来释放,那么就会报错。所以,所谓的“谁申请谁释放”的原则在这里实际上也是适用的。上面这个错误就是在Debug模式下,帮助开发人员发现这种跨运行库的heap的指针引用的问题,尚不知道这种引用是否完全不合法,还是仅仅只有风险。

另外,如果以静态链接的方式链接到C运行库的话,即使是同一个版本的运行库,在内存中也存在两份copy,并有两块由不同运行库维护的堆内存。

从上述这点看来,如果要自己开发一个dll的话,记得要提供堆内存释放的函数,以避免出现不同运行库的冲突。

C++模板

老是说C++的模板真心比C#的泛型在语言层面要复杂的多,使用模板并不难,但是要自己设计模板类,就出问题了。这里简单总结一些模板的基础。

模板类的声明如下:

template <typename T>
public class IntelligentARStructAR
{
private:
  T _Struct;
public:
  ~IntelligentARStructAR();
}

模板类的实现(定义):

template<typename T> IntelligentARStructAR<T>::~IntelligentARStructAR(){}

模板类的具化:

编译器在编译过程中,需要等模板在源代码中使用的时候,才会生成一个对应的类型,这个过程叫模板类的具化。

编译器要生成一个模板的定义,必须同时能看到模板的声明、模板的定义以及模板的具化要素,如果编译器在编译阶段不能具化,那么只能寄希望于链接器

来看个典型的错误:

  • template.h:里面有模板的声明
  • template.cpp:include template.h,里面有模板的实现(定义)
  • main.cpp:include template.h,里面有使用模板(即模板具化的要素)

编译器在编译template.cpp时,同时看到了模板声明和模板定义,但是因为没有模板的具化要素,编译器无法生成模板类型(因为,在没有要素的情况下,不可能知道T这个类型的结构大小,也就无法生成二进制代码);在编译main.cpp,能够看到的是模板的声明和模板的具化要素,但没有模板的定义,于是无法编译通过。

这个典型的使用就是:C++编译器不能支持对模板的分离式编译的原因。

解决这个问题的方法有如下几种:

  1. 在具化要素时,让编译器看到模板定义。典型的方式是将模板的声明和定义同时写在头文件中。
  2. 用另外的编译单元中显示的具化。在另一个cpp文件中显示的使用模板,这样链接器能够在链接阶段找到模板类型。
  3. export关键字。据说还没有编译器实现。
 欢迎访问我的github主页:http://pchou.info
 
 
标签: C++C++/CLI

C\C++

 
.NET程序员的C\C++情结(3)
摘要: 摘要这个系列是本人在工作或工作之余开发和学习C\C++的一些笔记。本文涉及C++/CLI的一些内容。本文为原创,首发于我的个人博客:.NET程序员的C\C++情结(3)。欢迎交流指正。转载请注明出处。虽然现在主要从事.NET平台的开发,但是一直以来对C\C++有着那份难以割舍的情结。本文会涉及到托管C++的一些随笔记录。当然,如果写纯.NET应用的话,C#无疑是最合适的语言的。但是托管C++在同时处理Native调用和托管调用上无疑是十分吸引人的,往往用来作为托管世界和Native世界的桥梁。当然。你可以说用.NET的“平台调用”特性同样能够胜任,萝卜青菜各有所爱吧。托管C++基础语言特性在托阅读全文
POSTED @ 2013-01-12 09:48 P_CHOU 阅读(445) | 评论 (0) 编辑
【Windows】线程漫谈——.NET线程同步之Event和Mutex
摘要: 本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等本篇介绍与内核对象同步相关的Event对象和Mutex对象AutoResetEvent和ManualResetEvent同步事件有两种:AutoResetEvent和 ManualResetEvent。主要用户线程之间的通知,实现同步。无论是哪种,实际上是每个Event对象关联了一个事件内核对象。在【Windows】线程漫谈——线程同步之等待函数和事件内核对象中,详细介绍了事件内核对象和等待函数。.NET这里的两个版本是对它们的封装。下面简单回顾一下事件内核对象:事件内核对象有两种状态:触发状态、非阅读全文
POSTED @ 2012-08-19 09:37 P_CHOU 阅读(1257) | 评论 (1) 编辑
【Windows】线程漫谈——.NET线程同步之Interlocked和ReadWrite锁
摘要: 摘要: 本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。这篇来说说静态的Interlocked类和ReadWrite锁.NET中的InterlockedInterlocked的系列方法提供了对简单类型的原子操作(不会被打断的操作),因此这也是一种多线程共享变量,防止冲突争用的方法。比如下面的方法作用是以原子的方式递增整数i:int i = 0 ;Interlocked.Increment( ref i);除此之外还包括Add、Exchange、CompareExchange、Decrement、Read和其中的某些泛型版本。如果看官使用过win阅读全文
POSTED @ 2012-07-24 08:08 P_CHOU 阅读(1194) | 评论 (0) 编辑
【Windows】线程漫谈——.NET线程同步之Monitor和lock
摘要: 摘要: 本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。从这篇开始,在线程同步的方法上,开始在.NET平台上做个总结,同时对比Windows原生的API方法。你可以发现其中的联系。.NET中的Monitor和lock相信很多看官早已对此十分熟悉了。本文作为总结性的文章,有一些篇幅将对比Monitor和关键段的关系。由于lock就是Monitor,所以先从Monitor说起,通常Monitor是像下面这样使用的:Monitor.Entry(lockObj);try{ // lockObj的同步区}catch(Exception e){ ...阅读全文
POSTED @ 2012-07-18 13:55 P_CHOU 阅读(1294) | 评论 (2) 编辑
【Windows】线程漫谈——线程同步之信号量和互斥量
摘要: 本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等信号量内核对象信号量内核对象用来进行资源计数,它包含一个使用计数、最大资源数、当前资源计数。最大资源数表示信号量可以控制的最大资源数量,当前资源数表示信号当前可用的资源数量。设想一个场景:需要开发一个服务器进程,最多同时运行5个线程来响应客户端请求,应该设计一个“线程池”。最开始的时候,5个线程都应该在等待状态,如果有一个客户端请求到来,那么唤醒其中的一个线程以处理客户端请求,如果同时的请求数量为5,那么5个线程将全部投入使用,再多的请求应该被放弃。也就是说,随着客户端请求的增加,当前资源计数随之递阅读全文
POSTED @ 2012-07-13 20:27 P_CHOU 阅读(1215) | 评论 (0) 编辑
【Windows】线程漫谈——线程同步之计时器内核对象
摘要: 本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。可等待的计时器内核对象下面的函数CreateWaitableTimer用以创建一个计时器内核对象:HANDLE WINAPI CreateWaitableTimer( __in_opt LPSECURITY_ATTRIBUTES lpTimerAttributes, __in BOOL bManualReset, __in_opt LPCTSTR lpTimerName);第一和第三个参数与内核对象的共有特性有关,与线程同步无关,这里不再阐述,通常传入NULL即可。参数bMan...阅读全文
POSTED @ 2012-07-07 20:39 P_CHOU 阅读(1341) | 评论 (0) 编辑
【Windows】线程漫谈——线程同步之等待函数和事件内核对象
摘要: 本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。用内核对象进行线程同步内核对象:Windows操作系统使用内核对象来管理进程、线程、文件等诸多种类的大量资源。内核对象的创建通常是通过Windows API,比如CreateThread将创建一个线程内核对象,并返回一个内核对象句柄。内核对象实际上是一小块内存,其中包括了引用计数、安全性描述等信息,操作系统通过这一小段内存来管理对应的内核资源。内核对象的实际内存地址并非句柄所展示的,它们在进程内的内核对象句柄表中有映射。在前几篇中,介绍了在用户模式下的线程同步机制:InterLocked系列、关键阅读全文
POSTED @ 2012-07-03 08:44 P_CHOU 阅读(1131) | 评论 (4) 编辑
【Windows】线程漫谈——线程同步之Slim读/写锁
摘要: 本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。Slim读/写锁SRWLock的目的和关键段相同,对一个资源进行保护,构造了一段“原子访问”的代码,不让其他线程访问它。但与关键段不同的是SRWLock允许区分想要读取资源值的线程和想要写入资源值的线程,因为仅仅读取资源是不会破坏数据的,下面是Slim读/写锁的简单用法:SRWLOCK g_srwLock...//init SRWLockInitializeSRWLock(&g_srwLock);...//当需要写入资源的时候申请"排他锁"AcquireSRWLOckE阅读全文
POSTED @ 2012-06-24 09:03 P_CHOU 阅读(1038) | 评论 (0) 编辑
【Windows】线程漫谈——线程同步之关键段
摘要: 本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。关键段关键段(Critical Section)是一小段代码,它在执行之前需要独占对一些共享资源的访问权。这种方式可以让多行代码以“原子方式”对资源进行操控。这里的原子方式,指的是代码知道除了当前线程之外,没有其他任何线程会同时访问该资源。当然,系统仍然可以暂停当前线程去调度其他线程。但是,在当前线程离开关键段之前,系统是不会去调度任何想要访问同一资源的其他线程的。下面的代码展示了Critical Section的使用方法:const int COUNT = 10;int g_nSum = 0;C阅读全文
POSTED @ 2012-06-20 13:00 P_CHOU 阅读(1354) | 评论 (0) 编辑
【Windows】线程漫谈——线程同步之原子访问
摘要: 本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。多线程同步的难题我们知道单核处理器同一时刻只能处理一条指令,操作系统通过时间片调度实现了多任务和多线程。在这个过程中,操作系统随时会中断一个线程(这种中断是以指令为单位的),也就是说完全有可能在一个不确定的时候,线程用完了时间片,控制权交给了另一个线程,另一个线程用完时间片,控制权转回,但是这一进一出有可能一个被共享的全局变量的值已经变了!这也许会带来灾难性的后果,也许不会。因此,站在系统层面考虑,每当属于线程的时间片用完之后,系统要把当前CPU寄存器的值(比如,指令寄存器,栈指针寄存器)写入线程阅读全文
POSTED @ 2012-06-17 21:21 P_CHOU 阅读(1183) | 评论 (0) 编辑
【Windows】线程漫谈——线程栈
摘要: 本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。预备知识众所周知,线程在初始化时,系统会为其分配线程栈,用于局部变量、函数调用时的参数等。在开始讨论前,先交代一些背景知识。栈:一种先入后出的数据结构,push和pop是它典型的操作,对应“入栈”和“出栈”的术语。系统内存的分配机制:简单的说包括“预订”和“调拨”两个过程。预订并不真正分配物理存储器,只是对进程虚拟地址空间中的内存进行“预分配”,以使得这块内存不至于被当前进程的其他指令分配;调拨就是为预订的内存空间分配物理存储器(windows中物理存储器可能是物理内存,也可能是内存页交换文件)。阅读全文
POSTED @ 2012-06-14 19:20 P_CHOU 阅读(1325) | 评论 (1) 编辑
【Windows】线程漫谈——线程基础
摘要: 本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。进程与线程理解线程是至关重要的,每个进程至少有一个线程,进程是线程的容器,线程才是真正的执行体,线程必然在某个进程的上下文中运行。进程拥有惰性,如果进程中所有的线程都已结束,那么进程也就没有存在的必要了。一个进程由如下两部分组成:1、一个进程地址空间;2、一个进程内核对象一个线程由如下两部分组成:1、一个线程栈;2、一个线程内核对象线程的开销要比进程少很多,所以在解决编程问题的时候尽量考虑在当前进程中创建线程而不是创建新的进程。然而,线程的切换需要消耗一定数量的CPU资源,因此,也不是说可以毫无顾阅读全文
POSTED @ 2012-06-10 15:45 P_CHOU 阅读(1417) | 评论 (2) 编辑
.NET程序员的C情结(二)
摘要: C多文件编译、作用域和存储周期 所谓的编译,分为两个步骤:编译和链接编译有两个过程:a)预编译:处理#...的语句。#define的宏替换、#if条件编译、#include只是简单的把对应的文件内容复制到#include语句的位置b)单元源代码编译:随后编译器对每个cpp文件(在预编译阶段已经将#include的文件复制完成)单独编译成模块(.obj/.o等),在这个过程中除了语法检查外,还要在本cpp文件中检查调用函数或引用变量是否声明过。最后生成的模块开头会有一个符号表,其中包括了本模块定义的函数或变量在本模块中的偏移量;以及本模块引用的外部变量或函数(称为unsolved symbol.阅读全文
POSTED @ 2012-05-25 15:31 P_CHOU 阅读(347) | 评论 (0) 编辑
.NET程序员的C情结
摘要: 即将两年的.NET经验,一年的BMC经验,作为一个电子专业的人来说,心中仍然保留着对C和C++的情结。最近项目空闲之余在看Windows Programming和Windows via C/C++,并且用C++为我们开发的类库制作安装程序,虽然只是简单的Windows C程序,但是那份成就感油然而生。本文记录开发这个小程序过程中的心得:1、基于对话框的windows程序虽然说标准的Windows程序总是由用户注册的窗口类开始的,并且程序员需要自己用while来接收应用程序消息队列里的消息,并处理。但是如果想要快速开发小程序的话,可以像下面这样基于对话框的来开始程序。对话框自动建立消息循环,接收阅读全文
POSTED @ 2012-05-24 23:05 P_CHOU 阅读(378) | 评论 (0) 编辑
原文地址:https://www.cnblogs.com/Leo_wl/p/2857576.html