比特币源码分析--C++11和boost库的应用

比特币源码分析--C++11和boost库的应用

 
 

我们先停下探索比特币源码的步伐,来分析一下C++11和boost库在比特币源码中的应用。比特币是一个纯C++编写的项目,用到了C++11和boost的许多特性,本文来总结一下相关特性的用法,或许将来的项目中可以使用到。

1 boost相关
1.1 boost::bind
    bind用于绑定参数到函数、函数指针、函数对象、成员函数上,返回一个函数对象。调用是需要引用<boost/bind.hpp>头文件。

以下是bind的几个例子:

(1) bind普通函数

假设有如下的函数定义:

//normal function
static int fun(int x, int y, double z) {
x += 2;
cout << x << "," << y << "," << z << endl;
}

绑定参数的时候可以:

//绑定普通的函数
boost::bind(&fun, 1, 3, 5)();

(2) 使用占位符绑定

也可以使用占位符来绑定,参数的顺序可以任意:

//使用占位符绑定
boost::bind(&fun, _3, _1, _2)(7, 8, 9);

上面的例子等价于调用fun(9, 7, 8)

(3) bind仿函数

bind也可以绑定仿函数,假设如下一个简单求和的仿函数:

struct Sum {

int operator()(int a, int b) {
return a + b;
}
};

可以按如下方式绑定参数到仿函数上:

//绑定仿函数
Sum sum;
cout << boost::bind<int>(sum, 7, 8)() << endl;

(4) 绑定类的成员函数

假设有如下类的定义:

class Base {

public:
virtual int f(int i);

};

int Base::f(int i) {
return i;
}

class Derived : public Base {

public:
virtual int f(int i);
};

int Derived::f(int i) {
return i + 2;
}

bind同样可以绑定类的成员函数:

//绑定成员函数
Base base;
Derived d;
Base &refBase = d;
cout << boost::bind(&Base::f, base, 4)() << endl; //调用基类函数
cout << boost::bind(&Base::f, boost::ref(refBase), 4)() << endl; //boost::ref传入指向子类的引用,调用子类函数

注意,bind时如果通过boost::ref传入引用,会触发多态。例如上面例子中refBase是一个基类型的引用,但是实际指向一个派生类对象,bind时用boost::ref传引用就会触发多态。

(5) 绑定函数指针

bind也可以绑定函数指针,还是上面的两个类,绑定成员函数指针可以按下面的方式:

//绑定成员函数指针
typedef int (Base::*F)(int);
F fp = &Base::f;
cout << boost::bind(fp, boost::ref(refBase))() << endl;
    C++11标准库中也有对应的实现:std::bind。

1.2 boost::thread_group
    boost::thread_group用于管理一组线程。可以向线程组里添加或者移除线程,向所有线程发送中断信号,等待组内所有线程全部结束等等。

这个类包含以下一些成员函数:

thread_group::create_thread:创建一个新线程添加到thread_group;

thread_group::add_thread:添加一个线程到线程组里;

thread_group::remove_thread:从线程组中移除一个线程;

thread_group::join_all:等待组内所有线程执行完成;

thread_group::interrupt_all:向组内所有线程发送终端信号;

从支持的api可以看出,thread_group只是统一的管理加入其中的线程,不要理解成线程池,因为线程池涉及到池中线程的复用和管理,逻辑更加复杂。

1.3 boost线程中断
    boost线程默认打开中断的,通过boost::thread的interrupt方法可以中断线程。值得注意的是线程只有在指定的中断点才能被中断,否则调用interrupt方法不会起作用。boost线程定义了以下几个中断点:

//线程等待子线程结束

1. thread::join();    
    2. thread::timed_join();
    //线程在条件变量上等待

3. condition_variable::wait();
    4. condition_variable::timed_wait();
    5. condition_variable_any::wait();
    6. condition_variable_any::timed_wait();

//线程休眠

7. thread::sleep();
    8. this_thread::sleep();

//interruption_point()相当于一个标记点,表示当线程执行到这里时可以被中断

9. this_thread::interruption_point()

只有线程允许中断时,thread::interrupt调用才会在上面9个中断点上将线程中断,线程被中断时将会抛出boost::thread_interrupted异常。

boost提供了api控制是否允许线程被中断:

boost::this_thread::disable_interruption:这是一个RAII对象,对象构造时关闭中断,析构时恢复中断;

boost::this_thread::restore_interruption:也是一个RAII对象,构造时恢复线程中断,析构时关闭中断。restore_interruption对象构造时需要一个disable_interruption对象作为参数,也就意味着一个线程只有用disable_interruption关闭中断以后才能在之后再调用restore_interruption临时恢复中断。

平时在开发过程中我们经常性会遇到需要一个线程在无限循环中处理,然后当某个条件触发以后终端这个线程的诉求,一般常见的处理方式是定义一个bool型的变量,然后需要中断的时候设置该bool变量。如果用boost,我们可以通过interrupt结合中断点更加优雅的中断线程。

来看看比特币中的使用示例。

比特币系统在初始化的时候会创建一个线程组,然后向线程组中添加若干个脚本校验线程、任务调度线程以及从磁盘中加载区块的线程:

//向线程组添加若干个脚本校验线程
if (nScriptCheckThreads) {
for (int i=0; i<nScriptCheckThreads-1; i++)
threadGroup.create_thread(&ThreadScriptCheck);
}

// Start the lightweight task scheduler thread
//向线程组添加任务调度线程
CScheduler::Function serviceLoop = boost::bind(&CScheduler::serviceQueue, &scheduler);
threadGroup.create_thread(boost::bind(&TraceThread<CScheduler::Function>, "scheduler", serviceLoop));

这些线程一加入线程组后就像脱缰的野马一样狂奔起来,直到用户发出stop控制指令以后,线程组的interrupt_all被调用,组内所有的线程就会收到中断指令,在相关的中断点上被中断:

// After everything has been shut down, but before things get flushed, stop the
// CScheduler/checkqueue threadGroup
threadGroup.interrupt_all();
threadGroup.join_all();
    线程组中的线程在收到终端指令后会在各自的中断点上被中断,比如任务调度线程,在一个大的无限循环中包含了下面的代码片段,确保当收到中断指令以后,线程可以在特定的中断点上被中断:

while (!shouldStop() && taskQueue.empty()) {
// Wait until there is something to do.
// 中断点,队列为空时收到中断信号,直接中段线程
newTaskScheduled.wait(lock);
}

// Wait until either there is a new task, or until
// the time of the first item on the queue:

// wait_until needs boost 1.50 or later; older versions have timed_wait:
#if BOOST_VERSION < 105000
//中断点,队列不空,但是在等待调度通知时收到中断信号,线程中断,队列中已有的任务不在执行
while (!shouldStop() && !taskQueue.empty() &&
newTaskScheduled.timed_wait(lock, toPosixTime(taskQueue.begin()->first))) {
// Keep waiting until timeout
}
#else
// Some boost versions have a conflicting overload of wait_until that returns void.
// Explicitly use a template here to avoid hitting that overload.
while (!shouldStop() && !taskQueue.empty()) {
boost::chrono::system_clock::time_point timeToWaitFor = taskQueue.begin()->first;
if (newTaskScheduled.wait_until<>(lock, timeToWaitFor) == boost::cv_status::timeout)
break; // Exit loop after timeout, it means we reached the time of the event
}
#endif
1.4 boost::signal2::signal
    也叫signal/slot(信号/插槽)机制,这是boost提供的一种用于模块间解耦的机制。和观察者类似。定义一些信号(事件),给信号注册对应的处理器(任意可以调用的实体,例如仿函数,函数指针等等),当信号触发以后就会调用注册的处理器处理之。boost提供的signal可以让我们免于去自己动手撸个观察者模式,写各种不同场景的观察者接口,开发者只关注两件事:定义信号和给信号绑定不同的处理器。

来看个比特币中的例子:bitcoind在运行过程中需要监视一些事件:比如有区块从区块链的主链上断开了,有新的区块连到了主链上,区块链的定点有更新等等,利用boost::signal,可以定义一组信号:

struct MainSignalsInstance {
boost::signals2::signal<void (const CBlockIndex *, const CBlockIndex *, bool fInitialDownload)> UpdatedBlockTip;
boost::signals2::signal<void (const CTransactionRef &)> TransactionAddedToMempool;
boost::signals2::signal<void (const std::shared_ptr<const CBlock> &, const CBlockIndex *pindex, const std::vector<CTransactionRef>&)> BlockConnected;
boost::signals2::signal<void (const std::shared_ptr<const CBlock> &)> BlockDisconnected;
boost::signals2::signal<void (const CTransactionRef &)> TransactionRemovedFromMempool;
boost::signals2::signal<void (const CBlockLocator &)> ChainStateFlushed;
boost::signals2::signal<void (int64_t nBestBlockTime, CConnman* connman)> Broadcast;
boost::signals2::signal<void (const CBlock&, const CValidationState&)> BlockChecked;
boost::signals2::signal<void (const CBlockIndex *, const std::shared_ptr<const CBlock>&)> NewPoWValidBlock;

// We are not allowed to assume the scheduler only runs in one thread,
// but must ensure all callbacks happen in-order, so we end up creating
// our own queue here :(
SingleThreadedSchedulerClient m_schedulerClient;

explicit MainSignalsInstance(CScheduler *pscheduler) : m_schedulerClient(pscheduler) {}
};

然后给这些信号绑定处理器:

void RegisterValidationInterface(CValidationInterface* pwalletIn) {
g_signals.m_internals->UpdatedBlockTip.connect(boost::bind(&CValidationInterface::UpdatedBlockTip, pwalletIn, _1, _2, _3));
g_signals.m_internals->TransactionAddedToMempool.connect(boost::bind(&CValidationInterface::TransactionAddedToMempool, pwalletIn, _1));
g_signals.m_internals->BlockConnected.connect(boost::bind(&CValidationInterface::BlockConnected, pwalletIn, _1, _2, _3));
g_signals.m_internals->BlockDisconnected.connect(boost::bind(&CValidationInterface::BlockDisconnected, pwalletIn, _1));
g_signals.m_internals->TransactionRemovedFromMempool.connect(boost::bind(&CValidationInterface::TransactionRemovedFromMempool, pwalletIn, _1));
g_signals.m_internals->ChainStateFlushed.connect(boost::bind(&CValidationInterface::ChainStateFlushed, pwalletIn, _1));
g_signals.m_internals->Broadcast.connect(boost::bind(&CValidationInterface::ResendWalletTransactions, pwalletIn, _1, _2));
g_signals.m_internals->BlockChecked.connect(boost::bind(&CValidationInterface::BlockChecked, pwalletIn, _1, _2));
g_signals.m_internals->NewPoWValidBlock.connect(boost::bind(&CValidationInterface::NewPoWValidBlock, pwalletIn, _1, _2));
}

绑定信号处理器非常简单,只需要connect一下就可以了,绑定以后的signal就是一个可调用的实体了,当有信号触发时,信号绑定的处理器就会被调用:

void CMainSignals::BlockChecked(const CBlock& block, const CValidationState& state) {
m_internals->BlockChecked(block, state);
}

来总结一下boost的信号/插槽机制:

(1) 定义一组信号,信号是boost::signals2::signal,这是一个模板类,模板类的参数是该信号的处理器的签名;

(2) 通过signal::connect给信号绑定处理器,处理器的签名必须和信号模板参数指定的一致;

(3) signal也是一个可调用的实体,当有事件发生时,调用signal,则与之绑定的处理器就会被调用。

1.5 boost visitor模式
    visitor模式主要的适用场景是集合中存在不同的元素,而对于不同的元素,有不同的操作,这种情况下用visitor模式就比较合适。我们举个比特币中的例子,比特币中在对交易进行签名的时候,需要根据提供的不同的类型来生成脚本地址,比如目前比特币支持的就有CKeyID,CScriptID,CWithnessV0ScriptHash,CWithnessV0KeyHash,很多时候我们可能会习惯性的撸出下面这样的代码:

if (destination typeof CKeyID) {
// CKeyID
} else if (destination typeof CScriptID) {
// CScriptID
} else if (destination typeof CWithnessV0KeyHash) {
// CWithnessV0KeyHash
} else if (destination typeof CWithnessV0ScriptHash) {
// CWithnessV0ScriptHash
}
    这种做法的最大问题在于如果项目代码中分散着很多这种if-else构成的分支,如果将来新增一种类型,就需要逐个找出这些地方然后新增一个判断分支,这样很不利于后续的维护。这里变化的是不同类型的对象接收到访问请求时的处理方式,可以用visitor将这一部分代码收归起来,看看比特币的源码中是如何处理的:

首先提供一个visitor:

class CScriptVisitor : public boost::static_visitor<bool>
{
private:
CScript *script;
public:
explicit CScriptVisitor(CScript *scriptin) { script = scriptin; }

bool operator()(const CNoDestination &dest) const {
script->clear();
return false;
}

bool operator()(const CKeyID &keyID) const {
script->clear();
*script << OP_DUP << OP_HASH160 << ToByteVector(keyID) << OP_EQUALVERIFY << OP_CHECKSIG;
return true;
}

bool operator()(const CScriptID &scriptID) const {
script->clear();
*script << OP_HASH160 << ToByteVector(scriptID) << OP_EQUAL;
return true;
}

bool operator()(const WitnessV0KeyHash& id) const
{
script->clear();
*script << OP_0 << ToByteVector(id);
return true;
}

bool operator()(const WitnessV0ScriptHash& id) const
{
script->clear();
*script << OP_0 << ToByteVector(id);
return true;
}

bool operator()(const WitnessUnknown& id) const
{
script->clear();
*script << CScript::EncodeOP_N(id.version) << std::vector<unsigned char>(id.program, id.program + id.length);
return true;
}
};
} // namespace

然后将不同的类型用boost::variant来表示:

typedef boost::variant<CNoDestination, CKeyID, CScriptID, WitnessV0ScriptHash, WitnessV0KeyHash, WitnessUnknown> CTxDestination;

boost::variant可以理解为联合体,与联合体不同的是它里面可以塞任何类型的数据。

然后只要对这个variant运用visitor就可以了:

boost::apply_visitor(CScriptVisitor(&script), dest);
    这样不管variant当前的值是什么类型,visitor都能处理。如果将来想有不同的处理方式,换个visitor就可以了。代码中分散在四处难以维护的if-else分支现在被收归到visitor中,即便有新的类型加进来,也只需要在visitor中改一处就好。

上面的例子中其实已经看到了boost提供的visitor模式的用法,这里总结一下:

(1) 首先boost提供的visitor模式是需要和boost::variant来配合使用的。boost::variant里是一些不同的类型,这些类型可以通过给variant应用visitor来访问。

(2) 编写visitor的实现,visitor需要继承boost::static_visitor,这是一个模板类,模板参数代表操作的返回值;

(3) boost::apply_visitor将visitor应用到variant上;

与一般的设计模式的书中visitor模式的实现有所不同,boost的visitor提供的是基于模板的实现,用起来更简便,效率也更高。

2 C++11
    比特币源码用到了许多C++11的特性,本节简单来看看。

2.1 std::bind
    这个和前面介绍的boost::bind用法基本上一致,请参考boost::bind用法,这里不在重复。

2.2 std::function
    std::function是对一切可以调用的实体的封装,比如函数指针,仿函数,lambda表达式等等,都可以转化成function对象。std::function的最大的一个用处是实现延迟调用:可以将回调先保存到function中,等到需要的时候在调用。比特币中有大量的std::function的使用,基本上都是用于回调。以下的示例程序展示了std::function的使用:

#include <iostream>
#include <functional>

using namespace std;

int func(int i, int j) {
return i + j;
}

class Test {

public:

int f(int i, int j) {
return i + j;
}

static int static_func(int i, int j) {
return i + j;
}
};

struct FuncObj {
int operator()(int i, int j) {
return i + j;
}
};

int main(int argc, char **argv) {

std::function<int(int,int)> f;

//normal function
f = func;
cout << f(1, 2) << endl;

//function object
FuncObj fobj;
f = fobj;
cout << f(3, 4) << endl;

//member function
Test test;
f = std::bind(&Test::f, &test, std::placeholders::_1, std::placeholders::_2);
cout << f(5, 6) << endl;

//static member function
f = &Test::static_func;
cout << f(7, 8) << endl;
}
    示例程序中展示了用std::function来存储普通函数,仿函数和成员函数(静态与非静态)的例子。另外C++11中还支持lamda表达式,稍后说明lamda表达式时来看看std::function和lamda表达式结合在一起的例子。

2.3 std::thread
    在linux系统上,我们很多时候可能会直接调用系统的pthread_create接口来创建线程,这种方式的限制在于不能直接绑定类的非静态成员函数作为线程体,如果想让线程和类的非静态成员函数绑定起来还需要额外进行封装。而C++11引入了std::thread以后,事情就容易多了。以下是std::thread的使用示例:

#include <iostream>
#include <functional>
#include <thread>
using namespace std;

int func(int i, int j) {
cout << "thread normal" << endl;
return i + j;
}

class Test {

public:

int f(int i, int j) {
cout << "thread member" << endl;
return i + j;
}
};

struct FuncObj {
int operator()(int i, int j) {
cout << "thread function object" << endl;
return i + j;
}
};

int main(int argc, char **argv) {

std::function<int(int,int)> f;

//普通函数
std::thread t1; //null thread
cout << "thread joinable?" << t1.joinable() << endl; //joinable=false
t1 = std::thread(func, 1, 2);
cout << "thread joinable?" << t1.joinable() << endl; //joinable=true

//仿函数
FuncObj fobj;
f = fobj;
std::thread t2(f, 3, 4);

//成员函数
Test test;
f = std::bind(&Test::f, &test, std::placeholders::_1, std::placeholders::_2);
{
std::thread t3(std::bind(&Test::f, &test, 20, 30));
t3.join(); //必须调用join或者detach,否则会抛出异常
}
//nromal
t1.join();
t2.join();
//t3.join();
}
    可以看到,std::thread和std::function一起,任何可以调用的实体都可以被线程执行。关于std::thread有以下几点需要注意:

(1) std::thread对象和线程关联以后,在对象销毁前要么通过join等待线程结束,要么调用detach将线程和std::thread对象脱离,否则会抛出异常。

(2) thread对象detach以后,线程依然会被OS调度运行;

如果你还在用pthread_create等系统接口创建线程,请远离他们,尽快拥抱std::thread吧。

2.4 lamda表达式
    c++11支持lamda表达式,用lamda表达式可以实现匿名函数,尤其是如果你的代码中存在许多只调用一次的小函数,用lamda表达式来重构掉他们是个不错的主意。

以下是C++11支持的四种lamda表达式的形式,死记硬背记住就可以了:

(1) [capture] (params) mutable exception attribute -> ret {body}

这是最完整的lamda表达式,各个部分的解释如下:

(a) capture:表示在lamda表达式中所有可见的外部变量列表,至于这些变量是传值还是传引用,请继续看:

[a, &b]:表示变量a以值的方式传递,变量b以引用的方式传递;

[]:空的表示不捕获任何外部变量;

[&]:表示以引用的方式捕获可见域内所有的外部变量;

[=]:表示以值的方式捕获可见域内所有的外部变量;

(b) mutable:表示在lamba表达式内能够改动被捕获的变量,也能够访问被捕获对象的非const成员;

(c) exception:lamba表达式内可能抛出的异常

(2) [capture] (params) ->ret {body}

const类型的lamda表达式,在表达式内部不能修改capture列表中捕获的变量;

(3) [capture] (params) {body}

省略了返回类型的lamba表达式,返回类型可以根据表达式内部的return语句推断出来,如果没有return语句则认为返回void;

(4) [capture] {body}

省略了参数列表,相当于一个无参数的匿名函数;

以下代码片段示例了c++11中lamda表达式的使用:

#include <iostream>
#include <functional>
#include <thread>
using namespace std;

int main(int argc, char **argv) {

int a = 1;

//a以值的方式传递,lamda表达式为const,无法修改传入变量
auto f = [a](){
cout << "a = " << a << endl;
//a = 2; //编译错误
};

f();

//a以值的方式被捕获,lamda表达式可以改变传入值
auto f2 = [a]() mutable {
a = 2;
cout << "a = " << a << endl;
};
f2();
cout << "after lamda:a = " << a << endl; //a = 1 因为以值的方式被捕获,所以lamda执行以后a的值并不会改变

//a以引用的方式被捕获
auto f3 = [&a]() mutable {
a = 2;
cout << "a = " << a << endl;
};
f3();
cout << "after lamda:a = " << a << endl; //a = 2 以引用的方式被捕获,lamda执行以后a的值夜被改变了

//带返回值的lamda表达式
int i = 2, j = 3;
auto f4 = [](int i, int j) ->int {
return i + j;
};
cout << f4(i, j) << endl;

//自动推断返回值类型
auto f5 = [](int i, double d) {
return i + d;
};
cout << f5(1, 2.2f) << endl;
}
    比特币的代码中也有使用到lamda表达式,看看下面这段代码:

std::unique_ptr<Handler> handleNotifyHeaderTip(NotifyHeaderTipFn fn) override
{
return MakeHandler(
::uiInterface.NotifyHeaderTip.connect([fn](bool initial_download, const CBlockIndex* block) {
fn(initial_download, block->nHeight, block->GetBlockTime(),
GuessVerificationProgress(Params().TxData(), block));
}));
}
    这段代码为信号NotifyHeaderTip绑定处理器,其中处理器就用了lamda表达式。

如果您此前还没有在C++中用过lamda表达式,初看到这种代码的时候可能会不太理解,现在了解了lamda表达式,以后在遇见应该就会很亲切了。

2.5 std::array
     C++11中引入的数组类型,与vector相比,std::array的内存位于堆栈中,所以性能更高。

std::array的用法也很简单,只要指定类型和大小就可以了,以下是比特币源码中的一个例子:

static const std::array<unsigned char, 32> buff = {
{
17, 79, 8, 99, 150, 189, 208, 162, 22, 23, 203, 163, 36, 58, 147,
227, 139, 2, 215, 100, 91, 38, 11, 141, 253, 40, 117, 21, 16, 90,
200, 24
}
};
    需要注意的是std::array的对象不能由编译器隐式转换为指针类型,因此对于下面的接口:

std::string EncodeBase58(const unsigned char* pbegin, const unsigned char* pend)
    不能直接传入std::array对象,而是需要通过其接口std::array::data()来获取数组首元素的指针:

EncodeBase58(buff.data(), buff.data() + buff.size());
2.6 using的新用法
    C++11中using有了新用法,其中之一就是代替typedef:

代替typedef:在C++11之前,我们定义新类型的别名都是通过typedef来,在C++11中,typedef可以通过using来代替,以下是来自比特币中的一段代码:

using NodesStats = std::vector<std::tuple<CNodeStats, bool, CNodeStateStats>>;
virtual bool getNodesStats(NodesStats& stats) = 0;
    通过using定义了新类型NodesStats,实质上一个一个vector<std::tuple<>>类型。用using比typedef更简单一些。

2.7 auto和decltype
    auto可以让编译器自动为我们推断变量的类型,这个特性能极大的简化我们的编程,提高效率。比如说如果不用auto,我们只能写出下面这样的代码:

std::map<int, vector<int> > amap;
std::map<int, vector<int> >::iterator iter = amap.begin();
    我们得在键盘上敲入长长的一串来指定iter的类型,有了auto以后就方便多啦:

std::map<int, vector<int> > amap;
auto iter = amap.begin();
    有了auto,你无须知道函数返回了一个什么,无须知道某个类的成员是什么类型,一切由编译器自动推断,还在等什么,用起来再说吧。

当然auto也不是万能的,比如如果想定义函数的返回值为auto就会编译出错:

static auto func(int i, int j) {
cout << "I am a function" << endl;
return i + j;
}
    此时需要用C++11中的trailing return type来进行:

static auto func(int i, int j) ->int{
cout << "I am a function" << endl;
return i + j;
}
    当然上面这个例子可能并不是很恰当,因为我们完全知道函数会返回什么,但是当涉及到模板的时候,问题可能会开始变的复杂,来看看比特币中一个现实的案例:

template <typename T>
class reverse_range
{
T &m_x;

public:
explicit reverse_range(T &x) : m_x(x) {}

auto begin() const -> decltype(this->m_x.rbegin())
{
return m_x.rbegin();
}

auto end() const -> decltype(this->m_x.rend())
{
return m_x.rend();
}
};
    看看这个模板类的begin函数,因为无法知道m_x的具体类型,也不知道m_x的rbegin的返回类型,因此begin()函数的返回类型是不能确定的,此时trailing return type就发挥了作用。C++11新引入了decltype来计算表达式的返回类型(不会计算表达式,因此不用担心效率问题),用decltype让编译器去推算出返回类型。

3 小结
    比特币是一个纯C++项目,其中用到了boost和C++11中的许多特性,本文简单列举一些,并不是全部,读者在阅读源码的过程中可以留意并学习,然后在项目中去尝试使用这些新特性。
---------------------
作者:jupiterwangq
来源:CSDN
原文:https://blog.csdn.net/ztemt_sw2/article/details/81187098
版权声明:本文为博主原创文章,转载请附上博文链接!

上一篇:C语言-数据类型


下一篇:VC6.0支持UNICODE的步骤