当前位置: 首页 > news >正文

python3 ctypes模块使用方法与心得体会--- int* ,char*等指针类型互转

文章目录

        • 前言
        • 正文
          • 1 简介
          • 2. DLL文件解析与简单函数调用
          • 3.基本ctypes数据类型
          • 4.函数的输入、输出数据类型
          • 5. 类方法的调用
          • 6.高级ctypes数据类型——数组
          • 7.高级ctypes数据类型——高维数组
          • 8.高级ctypes数据类型——字符串数组
          • 9. 高级ctypes数据类型——指针
          • 10.高级ctypes数据类型——结构体

欢迎大家关注我的公众号,内有 PyCharm 专业版 安装教程,csdn不允许发布此等博客,
更有大量PDF书籍下载
在这里插入图片描述

前言

需求是python3 和 C写的dll打交道,调用dll中的方法,但是要传递参数,char还好一点,int搞了半天才搞定,所以想着还是记录下,以免后面忘记了

网上找了好多文章,没有什么满意,自己总结了些七七八八的汇总,也算是将网上七七八八的文章取其精华,去其糟粕!!!

看完了觉得可以的点个赞,觉得无用的也不要吐槽,只是单纯的记录,毕竟烂大街的知识了,我只是汇总一下,以免多浪费时间


正文

1 简介

ctypes是一个自Python 2.5开始引入的,Python自带的函数库。其提供了一系列与C、C++语言兼容的数据结构类与方法,可基于由C源代码编译而来的DLL动态链接库文件,进行Python程序与C程序之间的数据交换与相互调用。本文基于Python 3.6.3

注意,在使用ctypes的过程中,Python解释器与C编译器所支持的位数必须一致,即:32位Python解释器必须与32位C编译器配合使用,同理,64位编译器也必须保持一致使用,否则将很有可能导致DLL文件无法被解析、调用等问题。

2. DLL文件解析与简单函数调用

我在这边只强调类型互转,至于dll的调用不做过多介绍,下面会写出调用示例,不再讲解

首先,为方便起见,本文中所有的模块导入均通过“*”语法导入,但在实际代码的编写中,强烈不建议使用这种语法:

from ctypes import *

python3 加载dll 并调用其方法

当编译好一个dll或so动态库文件后,在Python中就可通过ctypes模块去解析与调用了。解析dll文件可调用CDLL函数,其接受dll文件名作为参数,返回一个解析后的实例对象:

dllObj = CDLL('xx.dll')

取得此对象后,即可像调用实例方法那样,以此对象调用dll文件中的各种函数。也就是说,dll文件中的所有函数,在经过CDLL函数解析并返回一个对象后,均可看做是此对象的实例方法,可以以点号的形式进行调用。

假设定义了如下 C源代码,并已编译为 “a.dll”文件:

int addNum(int xNum, int yNum)
{
   return xNum + yNum;
}

而后即可在Python中传入参数并调用此函数:

dllObj = CDLL('a.dll')

print(dllObj.addNum(1, 2))

输出结果为3

由此可见,当调用CDLL函数解析dll文件,并得到解析对象后,即可像调用实例方法那样调用dll文件中的函数,且函数的参数就是实例方法的参数。

3.基本ctypes数据类型

首先贴出ctypes 数据类型和 C数据类型 和 python 类型 对照表

ctypes typeC typePython type
c_bool_Boolbool (1)
c_charchar1-character bytes object
c_wcharwchar_t1-character string
c_bytecharint/long
c_ubyteunsigned charint/long
c_shortshortint/long
c_ushortunsigned shortint/long
c_intintint/long
c_uintunsigned intint/long
c_longlongint/long
c_ulongunsigned longint/long
c_longlong__int64 or long longint/long
c_ulonglongunsigned __int64 or unsigned long longint/long
c_size_tsize_tnt/long
c_ssize_tssize_t or Py_ssize_tint/long
c_floatfloatfloat
c_doubledoublefloat
c_longdoublelong doublefloat
c_char_pchar * (NUL terminated)bytes object or None
c_wchar_pwchar_t * (NUL terminated)string or None
c_void_pvoid *int/long or None

由上表可以看出,ctypes的命名规律是前置的“c_”加上C语言中的数据类型名,构成ctypes中定义的C语言数据类型。上表中较为常用的类型主要包括c_int、c_double、c_char以及c_char_p等。这三种类型将在下一节详细讨论。

4.函数的输入、输出数据类型

首先考虑如下改写的代码:

double addNum(double xNum, double yNum)
{
   return xNum + yNum;
}

此代码唯一的改动之处在于将上文的addNum函数的输入以及输出的数据类型均由int转成了double。此时如果直接编译此文件,并在Python中调用,就会发现程序抛出了ctypes定义的ctypes.ArgumentError异常,提示参数有错误。

出现此问题的原因在于DLL文件无法在调用其中函数时自动设定数据类型,而如果不对类型进行设定,则调用函数时默认的输入、输出类型均为int。故如果函数的参数或返回值包含非int类型时,就需要对函数的参数以及返回值的数据类型进行设定。

设定某个函数的参数和返回值的数据类型分别通过设定每个函数的argtypesrestype属性实现。argtypes需要设定为一个tuple,其中依次给出各个参数的数据类型,这里的数据类型均指ctypes中定义的类型。同理,由于C语言函数只能返回一个值,故restype属性就需要指定为单个ctypes类型。

对于上文的返回值为doubleaddNum,代码修改为如下形式即可运行:

dllObj = CDLL('a.dll')

dllObj.addNum.argtypes = (c_double,c_double)

dllObj.addNum.restype = c_double

print(dllObj.addNum(1.1, 2.2))

上述代码在调用addNum之前,分别设定了此函数的输入参数为两个double,返回值也为double,然后以两个小数作为参数调用这个函数,返回值为3.3,结果正确。

对于一个返回值类型为char指针的C语言函数,则只需要设定restypec_char_p,函数即可直接返回Python的str类型至Python代码中。例:

设有如下返回字符串指针的C语言函数:

char * helloStr()
{
   return "Hello!";
}

则Python的调用代码:

dllObj = CDLL('a.dll')

dllObj.addNum.restype = c_char_p

print(dllObj.helloStr())

上述代码调用了返回字符串指针的helloStr函数,并先行设定返回值类型为c_char_p,则调用后即可直接获得一个Python字符串 “Hello!”

5. 类方法的调用

对于C++中类方法的调用,可以通过多种方法实现。一般情况下,可以编写一个函数,其中动态地声明一个实例指针,然后通过指针调用某个类方法,调用完成后,再释放此时动态申请的内存。例:

extern "C"
{
   void hello();
}

class Test
{
   public:
       void hello();
};

void Test::hello()
{
   printf("Hello!");
}

void hello()
{
   Test *testP = new Test;

   testP -> hello();

   delete testP;
}

上述代码定义了一个Test类,而后定义了一个Test类的hello方法,再定义了一个名为hello的函数作为接口函数。在接口函数中,首先通过new语句创建了一个实例指针,然后通过此指针调用了Test类的hello方法,调用完成后,再释放此指针。故在Python中,只需要调用这个接口函数hello,即可实现调用Teat类中的hello方法。

6.高级ctypes数据类型——数组

首先,由于Python和C的数组在数据结构上有本质的差别,故不可以直接通过赋值等简单操作进行数据结构转换,而需要一种二者兼容的数据结构来进行数据的存储与传递。

ctypes中重载了Python的乘号运算符(本质上是重载了ctypes数据结构基类的__mul__方法,使其所有的子类数据结构均适用),使得乘号变成了定义任意长度数组类的方法。具体的语法非常简单,将一个基本的ctypes数据类型乘上一个整数,即可得到一个新的类,这个类就是基于此数据类型的某个长度的数组类。例:

c_int * 10    #相当于C中的int [10]
c_double * 5  #相当于C中的double [5]

注意,通过将基本类型乘上一个数的方式得到的只是一个新的类,而不是这个类的实例,要得到一个真正的实例对象,就需要实例化这个类。实例化时,类可接受不超过声明长度的,任意数量的参数,作为数组的初始值。没有接受到参数的部分会自动初始化为0、None或其他布尔值为False的量,具体情况视数据类型而定,如果声明的是整形或浮点型数组,那么就会初始化为0,而如果声明的是字符串相关的量,那么就会初始化为None。本节只讨论数字类型的数组,字符串相关的数组将在下文进行讨论。
例:

50的整形数组:
(c_int * 5)()

前三个数为1-3,后续全为010长度浮点型数组:
(c_double * 10)(1, 2, 3)

对于Python而言,数字类型的数组是一个可迭代对象,其可通过for循环、next方法等方式进行迭代,
以获取其中的每一个值。例:
for i in (c_double * 10)(1, 2, 3):

   print(i)

输出结果为1.02.03.0以及后续的70.0

而对于C接口而言,这样的实例对象就等同于在C中创建的数组指针,可直接作为实参传入并修改其中的值。通过这样的兼容数据类型,即可实现Python与C之间的数据传递。

另外,数组对象在数据类型上可看作是指针,且指针变量在ctypes中就等同于int类型,故所有涉及到指针传递的地方,均无需考虑修改argtypes属性的问题。直接以默认的int类型传入即可。下文中也将多次使用到这一性质。例:
C部分:

extern "C"
{
   void intList(int numList[]);
}

void intList(int numList[])
{
   for (int i = 0; i < 10; i++)
       numList[i] = i;
}

这段代码定义了一个intList函数,作用为传入一个整形数组指针(为简单起见,此数组假定为10个整形长度),而后将数组中的值依次赋值为0-9。

Python部分:

dllObj = CDLL('1.dll')

numList = (c_int * 10)()

dllObj.intList(numList)

for i in numList:
   print(i)

这段代码首先定义了一个数组实例numList,长度为10个int,而后将这个实例像传入数组指针一样直接传入dll文件中的intList函数,调用完成后遍历此数组,依次输出每个值。结果即为在C源码中定义的0-9。

由此可见,通过ctypes定义的数组数据类型是一种同时兼容Python与C的数据结构,对于Python而言,这种数据结构是一个可迭代对象,可通过for循环进行遍历求值,而对于C而言,这样的数组结构就等同于在C中声明的数组指针。故可以将Python中定义的数组传入C中进行运算,最后在Python中读取此数组的运算结果,从而实现了数组类型的数据交换。

7.高级ctypes数据类型——高维数组

高维数组与一维数组在语法上大体类似,但在字符串数组上略有不同。本节首先讨论数字类型的高维数组。此外,为简单起见,本节全部内容均对二维数组进行讨论,更高维度的数组在语法上与二维数组是相似的,这里不再赘述。

高维数组类可简单的通过在一维数组类外部再乘上一个数字实现:

(c_int * 4) * 3

这样即得到了一个4 * 3的二维int类型数组的类。

二维数组类的实例化与初始化可看作是多个一维数组初始化的叠加,可通过一个二维tuple实现,且如果不给出任何初始化值(即一对空括号),则其中所有元素将被初始化为0。例:

((c_int * 4) * 3)()

这样就得到了一个所有值均为0的二维数组对象。又例:

((c_int * 4) * 3)((1, 2, 3, 4), (5, 6))

上述代码只实例化了第一个一维数组的全部以及第二个一维数组的前两个值,而其他所有值均为0。

二维数组在使用时与一维数组一致,其可直接作为指针参数传入C的函数接口进行访问,在C语言内部其等同于C语言中声明的二维数组。而对于Python,这样的数组对象可通过双层的for循环去迭代获取每个数值。

8.高级ctypes数据类型——字符串数组

字符串数组在ctypes中的行为更接近于C语言中的字符串数组,其需要采用二维数组的形式来实现,而不是Python中的一维数组。首先,需要通过c_char类型乘上一个数,得到一个字符串类型,而后将此类型再乘上一个数,就能得到可以包含多个字符串的字符串数组。例:

((c_char * 10) * 3)()

上例即实例化了一个3字符串数组,每个字符串最大长度为10。

对于C语言而言,上述的字符串数组实例可直接当做字符串指针传入C函数,其行为等同于在C中声明的char (*)[10] 指针。下详细讨论Python中对此对象的处理。

首先,字符串数组也是可迭代对象,可通过for循环迭代取值,对于上例的对象,其for循环得到的每一个值,都是一个10个长度的字符串对象。这样的字符串对象有两个重要属性:value和raw。value属性得到是普通字符串,即忽略了字符串终止符号(即C中的\0)以后的所有内容的字符串,而raw字符串得到的是当前对象的全部字符集合,包括终止符号。也就是说,对于10个长度的字符串对象,其raw的结果就一定是一个10个长度的字符串。例:

for i in ((c_char * 10) * 3)():
   print(i.value)
   print(i.raw)

上述代码中,i.value 的输出全为空字符串(b’’),而对于i.raw,其输出则为b’\x00\x00…’,总共十个\x00。也就是说,value会忽略字符串终止符号后的所有字符,是最常用的取值方式,而raw得到不忽略终止字符的字符串。

接下来讨论ctypes中对字符串对象的赋值方法。由于ctypes的字符串对象通过某个固定长度的字符串类实例化得到,故在赋值时,这样的字符串对象只可以接受等同于其声明长度的字符串对象作为替代值,这是普通Python字符串做不到的。要得到这样的定长字符串,需要用到ctypes的create_string_buffer函数。

create_string_buffer函数用于创建固定长度的带缓冲字符串。其接受两个参数,第一参数为字符串,第二参数为目标长度,返回值即为被创建的定长度字符串对象,可以赋值给字符串数组中的某个对象。注意,create_string_buffer函数必须接受字节字符串作为其第一参数,在Python2中,普通的字符串就是字节字符串,而在Python3中,所有的字符串默认为Unicode字符串,故可以通过字符串的encode、decode方法进行编码方式的转化。encode方法可将Python3的str转为bytes,其中的encoding参数默认就是UTF-8,故无需给出任何参数即可调用。同理,bytes可通过decode方法,以默认参数将bytes转化为Python3的str,对于Python2而言,无需考虑此问题。例:

charList = ((c_char * 10) * 3)()
strList = ['aaa', 'bbb', 'ccc']

for i in range(3):
   charList[i] = create_string_buffer(strList[i].encode(), 10)

for i in charList:
   print(i.value)

上述代码的核心在于,通过create_string_buffer函数创建了一个10长度的带缓冲字符串,其第二参数10用作指定长度,而其第一参数为一个通过encode方法转化成的bytes字符串,这样得到的对象即可赋值给一个10长度的字符串对象。注意,通过create_string_buffer函数创建的字符串对象,其长度必须严格等同于被赋值的字符串对象的声明长度,即如果声明的是10长度字符串,那么create_string_buffer的第二参数就必须也是10,否则代码将抛出TypeError异常,提示出现了类型不一致。

在字符串数组的初始化过程中,这样的字符串对象也可作为初始化的参数。例:

strList = ['aaa', 'bbb', 'ccc']
charList = ((c_char * 10) *3)(*[create_string_buffer(i.encode(), 10) for i in strList])

for i in charList:
   print(i.value.decode())

上述代码将实例化与初始化合并,通过列表推导式得到了3个10长度的缓冲字符串,并使用星号展开,作为实例化的参数。则这样得到的charList效果等同于上例中通过依次赋值得到的字符串数组对象。最后通过for循环输出字符串对象的value属性(一个bytes字符串),且通过decode方法将bytes转化为str。

9. 高级ctypes数据类型——指针

上文已经讨论了Python中的数组指针,而根据指针在Python中的定义,其本质上就是一个int类型的值,所以在传参时无需考虑修改argtypes属性的问题。本节主要讨论单个数字类型的指针。

首先,对于单个字符串,其不需要通过指针指针转换即可当做指针传递。例:

void printStr(char *str)
{
   printf("%s", str);
}

则Python中:

dllObj = CDLL('a.dll')

dllObj.printStr('Hello!')

由此可见,对于单个字符串传进dll,则直接通过字符串传递即可,传递过程中字符串会自动被转化为指针。而对于返回单个字符串的C函数,上文已经讨论过,通过修改restype属性为c_char_p后,即可在Python中直接接收字符串返回值。

还有传递char*指针参数,然后回去改变值,需要定义变量传递,可参考如下:

C部分

void modifyStr(char *str)
{
  //使用strcpy将1233赋值给str
  strcpy(str,1233);
}

Python部分

将python变量转换成char* 变量
strdata = ""
cdata = c_char_p(str(strdata).encode());
dllObj = CDLL('a.dll')

dllObj.modifyStr(cdata)char*数据转换成python变量
strData = str(cdata.value, encoding="utf-8")
print("strdata",strdata)

对于单个数值的指针,则需要通过byref或者pointer函数取得。首先考虑如下函数:

void swapNum(int *a, int *b)
{
   int temp = *a;

   *a = *b;

   *b = temp;
}

此函数接收两个int类型指针,并在函数内部交换指针所在位置的整形值。此时如果通过Python传入这两个指针参数,就需要使用到byref或者pointer函数。byref函数类似于C语言中的取地址符号&,其直接返回当前参数的地址,而pointer函数更为高级,其返回一个POINTER指针类型,一般来说,如果不需要对指针进行其他额外处理,推荐直接调用byref函数获取指针,这是较pointer更加快速的方法。此外,这两个函数的参数都必须是ctypes类型值,可通过ctypes类型实例化得到,不可直接使用Python内部的数值类型。例:

dllObj = CDLL('a.dll')

a, b = c_int(1), c_int(2)

dllObj.swapNum(byref(a), byref(b))

以上代码首先通过c_int类型实例化得到了两个c_int实例,其值分别为1和2。然后调用上文中的swapNum函数,传入的实际参数为byref函数的返回指针。这样就相当于在C语言中进行形如swapNum(&a, &b)的调用。

要将c_int类型再转回Python类型,可以访问实例对象的value属性:

print(a.value, b.value)

value属性得到的就是Python的数字类型,而经过上述的传递指针的函数调用,此时的输出应为2 1。

对于pointer函数,其同样接受一个ctypes实例作为参数,并返回一个POINTER指针类型,而不是简单的一个指针。POINTER指针类型在传参时也可直接作为指针传入C语言函数,但在Python中,其需要先访问contents属性,得到指针指向的数据,其一般为ctypes类型的实例,然后再访问value属性,得到实例所对应的Python类型的数据。例:

a, b = pointer(c_int(1)), pointer(c_int(2))

print(a.contents.value, b.contents.value)

dllObj.swapNum(a, b)

print(a.contents.value, b.contents.value)

以上代码通过pointer函数创建了两个指针类型变量,并将这两个指针作为参数传入函数进行调用。由此可见,通过pointer函数创建的指针类型可直接当做指针使用。但在将指针转换为Python数据类型时,需要先访问contents属性,得到指针指向的值,由于此值是ctypes类型,故还需要继续访问value属性,得到Python的数值类型。

10.高级ctypes数据类型——结构体

这里提一下,如果想用其他python其他结构体库和C打交道,可以看我 python struct 结构体, 继续ctypes结构体的讲解

结构体在ctypes中通过类进行定义。用于定义结构体的类需要继承自ctypes的Structure基类,而后通过定义类的_fields_属性来定义结构体的构成。_fields_属性一般定义为一个二维的tuple,而对于其中的每一个一维tuple,其需要定义两个值,第一个值为一个字符串,用作结构体内部的变量名,第二个值为一个ctypes类型,用于定义当前结构体变量所定义的数据类型。注意,在Python中定义的结构体,其变量名,类名等均可以不同于C语言中的变量名,但结构体变量的数量、数据类型与顺序必须严格对应于C源码中的定义,否则可能将导致内存访问出错。例:

class TestStruct(Structure):
   _fields_ = (
       ('x', c_int),
       ('y', c_double),
    )

以上代码即定义了一个结构体类型,其等同于C中的struct声明。此结构体定义了两个结构体变量:x对应于一个int类型,y对应于一个double类型。

结构体类型可以通过实例化得到一个结构对象,在实例化的同时也可传入初始化参数,作为结构变量的值。在得到结构对象后,也可通过点号访问结构体成员变量,从而对其赋值。例:

testStruct = TestStruct(1, 2)

print(testStruct.x, testStruct.y)

testStruct.x, testStruct.y = 10, 20

print(testStruct.x, testStruct.y)

上述代码通过实例化TestStruct类,并为其提供初始化参数,得到了一个结构体实例,第一次输出结果即为1 2.0。而后,再通过属性访问的方式修改了结构体中的两个变量,则第二次输出结果为10 20.0

上面定义的结构体可直接传入C代码中,且上文已经提到,两边定义的结构体变量的各种名称均可不同,但数据类型、数量与顺序必须一致。例:

struct TestStruct
{
   int a;
   double b;
}

extern "C"
{
   void printStruct(TestStruct testStruct);
}

void printStruct(TestStruct testStruct)
{
   printf("%d %f\n", testStruct.a, testStruct.b);
}

Python部分:

dllObj = CDLL('1.dll')

class TestStruct(Structure):
   _fields_ = (
       ('x', c_int),
       ('y', c_double),
    )
    
testStruct = TestStruct(1, 2)

dllObj.printStruct(testStruct)

由此可见,在Python中实例化得到的结构体实例,可以直接当做C中的结构体实参传入。

结构体也可以指针的方式传入,通过上节介绍的byref或者pointer函数即可实现转化。同样的,这两个函数都可直接接受结构体实例作为参数进行转化,byref返回简单指针,而pointer返回指针对象,可访问其contents属性得到指针所指向的值。例:

C部分,上述printStruct函数修改为接受结构体指针的版本:

void printStruct(TestStruct *testStruct)
{
   printf("%d %f\n", testStruct -> a, testStruct -> b);
}

Python部分:

testStruct = TestStruct(1, 2)

dllObj.printStruct(byref(testStruct))

上述代码将结构体对象testStruct作为byref的参数,从而将其转换为指针传入printStruct函数中。又例:

testStruct = pointer(TestStruct(1, 2))

dllObj.printStruct(testStruct)

print(testStruct.contents.x,testStruct.contents.y)

上述代码通过结构体对象生成了一个指针类型,并将此指针传入函数,可达到同样的效果。且在Python内部,结构体指针类型可以访问其contents属性,得到指针所指向的结构体,然后可继续访问结构体的x与y属性,得到结构体中保存的值。

如果觉得博主写的还不错,就请博主喝杯咖啡☕吧!!!
在这里插入图片描述

相关文章:

  • android 蓝牙程序扫描出现异常解决办法:qt.bluetooth.android: ACCESS_COARSE|FINE_LOCATION permission availab
  • git clone时RPC failed; curl 18 transfer closed with outstanding read data remaining
  • 最新 QtAV播放器 Window or Android 环境编译以及运行
  • 编译FFmpeg4.3.1 并移植到Android app中使用(最详细的FFmpeg-Android编译教程)
  • Qt for Android 动态全屏显示
  • QSS 选择器
  • Qt for android Java传递List集合对象到Qt(C/C++)
  • Qt for android 监听 android 系统 输入法的弹出消失,顶出UI等问题
  • linux centos7 升级 make 4.3
  • JS中的Map对象
  • python3 程序定时器执行(可循环),最佳有效方案
  • android studio 编译出的apk安装报错 “应用是非正式发布版本,请使用官方版本进行安装“ 解决方案
  • Android 将后台应用切换到前台
  • 如何从GitHub上下载一个项目中的单个文件或者子文件夹
  • Qt on Android 之设置应用名为中文
  • IE9 : DOM Exception: INVALID_CHARACTER_ERR (5)
  • [译] 怎样写一个基础的编译器
  • CentOS从零开始部署Nodejs项目
  • const let
  • JavaScript设计模式之工厂模式
  • JDK9: 集成 Jshell 和 Maven 项目.
  • Netty源码解析1-Buffer
  • PAT A1017 优先队列
  • php中curl和soap方式请求服务超时问题
  • weex踩坑之旅第一弹 ~ 搭建具有入口文件的weex脚手架
  • windows下mongoDB的环境配置
  • 解决jsp引用其他项目时出现的 cannot be resolved to a type错误
  • 如何进阶一名有竞争力的程序员?
  • 小程序上传图片到七牛云(支持多张上传,预览,删除)
  • 用mpvue开发微信小程序
  • HanLP分词命名实体提取详解
  • ​2020 年大前端技术趋势解读
  • ​如何在iOS手机上查看应用日志
  • # 20155222 2016-2017-2 《Java程序设计》第5周学习总结
  • #Z2294. 打印树的直径
  • #我与Java虚拟机的故事#连载13:有这本书就够了
  • (delphi11最新学习资料) Object Pascal 学习笔记---第8章第2节(共同的基类)
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_READ_ONLY)讲解
  • (二)Linux——Linux常用指令
  • (十六)Flask之蓝图
  • (学习日记)2024.03.25:UCOSIII第二十二节:系统启动流程详解
  • (一) springboot详细介绍
  • (转)c++ std::pair 与 std::make
  • (转载)VS2010/MFC编程入门之三十四(菜单:VS2010菜单资源详解)
  • *p++,*(p++),*++p,(*p)++区别?
  • .MyFile@waifu.club.wis.mkp勒索病毒数据怎么处理|数据解密恢复
  • .net core MVC 通过 Filters 过滤器拦截请求及响应内容
  • .Net Core webapi RestFul 统一接口数据返回格式
  • .NET 使用 ILRepack 合并多个程序集(替代 ILMerge),避免引入额外的依赖
  • .NET开发者必备的11款免费工具
  • .NET设计模式(2):单件模式(Singleton Pattern)
  • @transaction 提交事务_【读源码】剖析TCCTransaction事务提交实现细节
  • [ C++ ] STL_vector -- 迭代器失效问题
  • [ Linux 长征路第五篇 ] make/Makefile Linux项目自动化创建工具