分类 C++ 下的文章

基于C API的SQLite3基本数据库操作

  最近闲来无事,就写写博文来打发时间吧。。。

  这里把近期几个项目中总结下来的SQLite数据库使用经验归纳一下,共享出来,一来便于自己查阅,二来也便于和朋友们一起互相学习。

  这里先简单介绍一下SQLite数据库,给未接触过的朋友扫扫盲:
  SQLite是一款开源嵌入式文件型数据库,这个主要是和其他一些C/S架构的关系型数据库比较而来的,比如MySQL等。

  说他是嵌入式,因为SQLite的所有功能全部包装在一个dll中,我们只需要使用其中的导出接口就可以操作SQLite数据库,这样使得数据存储功能能够很方便的集成进用户的程序中,并运行在客户程序的进程空间中

  说他是文件型,因为SQLite的数据库文件就是一个独立文件(SQLite本身不限制数据库文件的扩展名),再没有其他的了,数据,表结构,查询,视图等等都保存在这个数据库文件中,不会依赖任何数据库环境

SQLite的主要特点:

  1. 无需部署,0配置,无服务端
  2. 跨平台
  3. 数据文件管理方便
  4. 较完善的SQL92标准支持,SQLite基本实现了SQL92标准,其他的一些不兼容的地方,可以参看官方相关说明,链接:http://www.sqlite.org/omitted.html
  5. SQL语句执行速度快,具体的对比数据,网上的评测有很多,这里就不多说了
  6. 应用较广。最著名的集成应该数Android了吧,其他的PHP ,Python等都做了集成,所以还是很不错的
  7. 完美的Unicode编码支持。SQLite的接口中,凡是涉及字符串的都是用UTF8或UTF16编码交互,有的同时提供这两种编码的接口函数,所以多语种支持绝对不是问题(这也是我偏爱SQLite的很重要的原因,呵呵)。

  好了,下面说说基于C API的SQLite操作,这里谢绝讨论其他的一些包装库,这不是本文的范围,当然SQLite的接口包装库函数还是很多的,基本覆盖各种主流开发语言。如基于Object Pascal的sqlitesimpledelphi,想了解这个库的朋友可以查看我的其他博文:

Delphi 2010下使用sqlitesimpledelphi连接SQLite数据库及中文乱码问题的解决
sqlitesimpledelphi修正以支持Unicode,Delphi 2010 测试通过

SQLite的数据库操作其实和常规的数据库操作流程是一样的:

  1. 连接数据库。
  2. 构造SQL语句并执行
  3. 对于SELECT语句,可以获取查询结果
  4. 数据库使用完毕之后,关闭数据库

  这里说明一下,下面所介绍的函数不会涵盖所有的API函数,毕竟SQLite针对同一个功能点提供了不同的API函数,主要表现在参数和配置功能上,有需要深入了解的朋友可以参考官方的文档。

打开数据库:

API函数:

int sqlite3_open(
const char *filename,   /* 数据库文件路径(UTF-8编码) */
sqlite3 **ppDb          /* 输出: SQLite 数据库句柄 */
);
int sqlite3_open16(
const void *filename,   /* 数据库文件路径(UTF-16) */
sqlite3 **ppDb          /* 输出: SQLite 数据库句柄 */
);

如果调用成功会返回SQLITE_OK,否则返回错误码。

构造SQL语句

这里就不多说了,这和SQLite本身无关,可以根据需要使用适当的方法构造即可,注意传给SQLite函数的时候,字符串编码要记得转换为UTF8/UTF16

执行SQL语句。

在SQLite中执行SQL语句比较简单的方法是调用函数:

int sqlite3_exec(
sqlite3*,                                  /* 打开的数据库句柄 */
const char *sql,                           /* UTF8编码的SQL语句 */
int (*callback)(void*,int,char**,char**),  /* 回调函数,对于SELECT语句返回的结果处理在回调函数中进行 */
void *,                                    /* 传递给回调函数的参数 */
char **errmsg                              /* 相关错误信息 */
);

其实sqlite3_exec只是封装了sqlite3_prepare、sqite3_step(即SQL中的预编译技术)的,主要目的是为使用者提供方便,笔者个人觉得使用后者会更加好一点,这里先做一下总结:

int sqlite3_prepare(
sqlite3 *db,            /* 打开的数据库句柄 */
const char *zSql,       /* UTF8编码的SQL语句,可以参数化 */
int nByte,              /* SQL语句的字节长度,可以传递-1,即字符串以\0结尾 */
sqlite3_stmt **ppStmt,  /* 输出:预编译之后的SQL语句句柄 */
const char **pzTail     /* 输出: 指向zSql缓冲区中跳过有效SQL字符串的第一个字节 */
);

int sqlite3_prepare_v2(
sqlite3 *db,            /* 打开的数据库句柄 */
const char *zSql,       /* UTF8编码的SQL语句,可以参数化 */
int nByte,              /* SQL语句的字节长度,可以传递-1,即字符串以宽字符\0结尾 */
sqlite3_stmt **ppStmt,  /* 输出: 预编译之后的SQL语句句柄 */
const char **pzTail     /* 输出: 指向zSql缓冲区中跳过有效SQL字符串的第一个字节 */
);

int sqlite3_prepare16(
sqlite3 *db,            /* 打开的数据库句柄 */
const void *zSql,       /* UTF16编码的SQL语句,可以参数化 */
int nByte,              /* SQL语句的字节长度,可以传递-1,即字符串以宽字符\0结尾 */
sqlite3_stmt **ppStmt,  /* 输出: 预编译之后的SQL语句句柄 */
const void **pzTail     /* 输出: 指向zSql缓冲区中跳过有效SQL字符串的第一个字节 */
);

int sqlite3_prepare16_v2(
sqlite3 *db,            /* 打开的数据库句柄 */
const void *zSql,       /* UTF16编码的SQL语句,可以参数化 */
int nByte,              /* SQL语句的字节长度,可以传递-1,即字符串以宽字符\0结尾 */
sqlite3_stmt **ppStmt,  /* 输出: 预编译之后的SQL语句句柄 */
const void **pzTail     /* 输出: 指向zSql缓冲区中跳过有效SQL字符串的第一个字节 */
);

其中带参数的SQL语句可以这样定义参数:
?
?NNN
:VVV
@VVV
$VVV
参数编号从1开始,例如:INSERT into db values(?1, ?2)

v2版本函数时SQLite根据需要添加的增强函数,新的程序推荐使用v2版本函数,只需与原来函数的区别,可以参考官方原文

int sqlite3_step(sqlite3_stmt*);
执行一次预编译SQL语句,在这之前,如果SQL是参数化的,可以调用sqlite3_bind来绑定数据,int、string、blob等等
如果执行成功会返回SQLITE_DONE,如果查询有结果会返回SQLITE_ROW,并可以通过API获取结果中的第一行数据,需要获取下一行数据可以再次调用sqlite3_step直到返回SQLITE_DONE表示后面没有数据了
如果需要重新对预编译的SQL绑定数据并执行,需要先reset一下,然后再调用step,即函数:
int sqlite3_reset(sqlite3_stmt *pStmt);

下面是绑定数据到预编译SQL语句的相关函数:
以下函数的第一个参数指代预编译的SQL句柄,第二个参数指代绑定的参数编号,对应于参数化的SQL语句中的参数编号:

int sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int n, void()(void));
该函数用于绑定二进制数据BLOB,其中最后一个参数是一个回调函数,当成功绑定数据后,会被调用,一般用于自动释放对应的缓冲区

int sqlite3_bind_double(sqlite3_stmt*, int, double);
该函数绑定double浮点数

int sqlite3_bind_int(sqlite3_stmt*, int, int);
该函数绑定int整数

int sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64);
该函数用于绑定具有64位长度的整数,对应于C中的long long结构,由于一个int的范围可能无法满足超大数据量的要求,所以SQLite也支持64位整数,毕竟SQLite官方声称SQLite是支持最大2T的数据的

int sqlite3_bind_null(sqlite3_stmt*, int);
该函数绑定一个空数据到指定列

int sqlite3_bind_text(sqlite3_stmt*, int, const char*, int n, void()(void));
该函数绑定一段字符串,源字符串是UTF8编码的

int sqlite3_bind_text16(sqlite3_stmt*, int, const void*, int, void()(void));
该函数绑定一段字符串,源字符串是UTF16编码的

int sqlite3_bind_value(sqlite3_stmt*, int, const sqlite3_value*);
该函数绑定以SQLite结构sqlite3_value存储的通用数据,其中sqlite3_value可以是上述的所有类型,此函数不太常用

int sqlite3_bind_zeroblob(sqlite3_stmt*, int, int n);
该函数绑定指定大小的全零BLOB数据

获取SQL查询结果

对于SELECT语句,还需要能够获取结果。上面也提到调用sqlite3_step之后,对于有结果的查询会返回第一行结果,这时可以通过API函数获取当前行的指定字段结果:

const void sqlite3_column_blob(sqlite3_stmt, int iCol);
该函数以BLOB数据格式获取对应列的数据,BOLB长度使用sqlite3_column_bytes获取

int sqlite3_column_bytes(sqlite3_stmt*, int iCol);
int sqlite3_column_bytes16(sqlite3_stmt*, int iCol);
该函数可以用于返回BLOB和字符串的字节长度。对于BLOB,两个函数效果是一样的,但是对于字符串sqlite3_column_bytes返回的是UTF8编码的字符串长度,而sqlite3_column_bytes16返回的是UTF16编码的字符串长度,其间会做必要的字符串格式转换

double sqlite3_column_double(sqlite3_stmt*, int iCol);
该函数返回double数据列

int sqlite3_column_int(sqlite3_stmt*, int iCol);
该函数返回int数据列

sqlite3_int64 sqlite3_column_int64(sqlite3_stmt*, int iCol);
该函数返回64位整数,即long long数据

const unsigned char sqlite3_column_text(sqlite3_stmt, int iCol);
const void sqlite3_column_text16(sqlite3_stmt, int iCol);
该函数返回字符串,其中sqlite3_column_text输出的字符串使用UTF8编码

sqlite3_column_text16使用UTF16编码
int sqlite3_column_type(sqlite3_stmt*, int iCol);
该函数返回对应列的数据类型

sqlite3_value sqlite3_column_value(sqlite3_stmt, int iCol);
该函数以sqlite3_value结构体返回数据

上面是根据列ID来获取对应的列数据的,如果想通过列名称获取列数据,则需要将列名称转换为对应的列ID,可以使用下面的函数:
const char sqlite3_column_name(sqlite3_stmt, int N);
const void sqlite3_column_name16(sqlite3_stmt, int N);
该函数返回对应列的名称

关闭数据库

int sqlite3_close(sqlite3 * db);
使用该函数可以关闭数据库

好了,基本的调用流程就这么些了,看完这篇文章,应该能够完成基本的数据读写操作了,至于其他的高级操作,有兴趣的朋友可以登录SQLite官方网站查看,这里就不在本文多说了。

OK,打完收功,欢迎大家多多讨论指正^^^_^

SQLite多线程下的并发操作

这两天一直在捣鼓SQLite数据库,基本的操作就不说了,比较简单,打算有空的话另起一篇博文简单总结一下。

这里主要想探讨一下多路并发下的数据库操作

SQLite作为一款小型的嵌入式数据库,本身没有提供复杂的锁定机制,无法内部管理多路并发下的数据操作同步问题,更谈不上优化,所以涉及到多路并发的情况,需要外部进行读写锁控制,否则SQLite会返回SQLITE_BUSY错误,以驳回相关请求。

如果有朋友想了解SQLite相关的锁定机制,可以看看我转载的博文sqlite的事务和锁,讲解的比较透彻,也容易理解,这里就不再重复讲解了。

返回SQLITE_BUSY主要有以下几种情况:

  1. 当有写操作时,其他读操作会被驳回
  2. 当有写操作时,其他写操作会被驳回
  3. 当开启事务时,在提交事务之前,其他写操作会被驳回
  4. 当开启事务时,在提交事务之前,其他事务请求会被驳回
  5. 当有读操作时,其他写操作会被驳回
  6. 读操作之间能够并发执行

基于以上讨论,可以看出这是一个典型的读者写者问题,读操作要能够共享,写操作要互斥,读写之间也要互斥

可以设计如下的方案解决并发操作数据库被锁定的问题,同时保证读操作能够保持最大并发

  1. 采用互斥锁控制数据库写操作
  2. 只有拥有互斥锁的线程才能够操作数据库
  3. 写操作必须独立拥有互斥锁
  4. 读操作必须能够共享互斥锁,即在第一次读取的时候获取互斥锁,最后一次读取的时候释放互斥锁

具体的代码实现就不贴了,有了思路,实现就很简单了,欢迎大家一起讨论!

下面是我简单编写的一个共享锁,smutex是一个跨平台的锁实现,简单,不多说了:

// 共享锁,第一个进入时锁定,最后一个离开时释放
class shared_mutex
{
private:
static int taked_man_; // 当前持有该锁的线程数
static sp::smutex man_lock_; // taked_man_的修改锁

private:
// 自动模式
bool is_auto_;
sp::smutex *mutex;
public:
void aquire()
{
   sp::sguard<sp::smutex> auto_lock(shared_mutex::man_lock_);
   if(taked_man_ == 0)
   {
    mutex->acquire();
   }
   taked_man_++;
}
void release()
{
   sp::sguard<sp::smutex> auto_lock(shared_mutex::man_lock_);
   if(this->taked_man_ > 0)
   {
    taked_man_--;
    if(taked_man_ == 0)
    {
     mutex->release();
    }
   }
}
public:
shared_mutex(sp::smutex &mt, bool auto_ = true) : mutex(&mt)
{
   sp::sguard<sp::smutex> auto_lock(shared_mutex::man_lock_);
   this->is_auto_ = auto_;
   if(this->is_auto_)
   {
    this->aquire();
   }
}

~shared_mutex()
{
   sp::sguard<sp::smutex> auto_lock(shared_mutex::man_lock_);
   if(this->is_auto_)
   {
    this->release();
   }
}
};

(转)SQLite的事务和锁

事务

事务定义了一组SQL命令的边界,这组命令或者作为一个整体被全部执行,或者都不执行。事务的典型实例是转帐。

事务的范围

事务由3个命令控制:BEGIN、COMMIT和ROLLBACK。
BEGIN开始一个事务,之后的所有操作都可以取消。
COMMIT使BEGIN后的所有命令得到确认;
而ROLLBACK还原BEGIN之后的所有操作。
如:
sqlite> BEGIN;
sqlite> DELETE FROM foods;
sqlite> ROLLBACK;
sqlite> SELECT COUNT() FROM foods;
COUNT(
)
412

上面开始了一个事务,先删除了foods表的所有行,但是又用ROLLBACK进行了回卷。再执行SELECT时发现表中没发生任何改变。

SQLite默认情况下,每条SQL语句自成事务(自动提交模式)。

冲突解决

如前所述,违反约束会导致事务的非法结束。大多数数据库(管理系统)都是简单地将前面所做的修改全部取消。

SQLite有其独特的方法来处理约束违反(或说从约束违反中恢复),被称为冲突解决。
如:
sqlite> UPDATE foods SET id=800-id;
SQL error: PRIMARY KEY must be unique

SQLite提供5种冲突解决方案:REPLACE、IGNORE、FAIL、ABORT和ROLLBACK。
REPLACE: 当发违反了唯一完整性,SQLite将造成这种违反的记录删除,替代以新插入或修改的新记录,SQL继续执行,不报错。
IGNORE
FAIL
ABORT
ROLLBACK

数据库锁

在SQLite中,锁和事务是紧密联系的。为了有效地使用事务,需要了解一些关于如何加锁的知识。

SQLite采用粗放型的锁。当一个连接要写数据库,所有其它的连接被锁住,直到写连接结束了它的事务。SQLite有一个加锁表,来帮助不同的写数据库都能够在最后一刻再加锁,以保证最大的并发性。

SQLite使用锁逐步上升机制,为了写数据库,连接需要逐级地获得排它锁。

SQLite有5个不同的锁状态:
未加锁(UNLOCKED)
共享 (SHARED)
保留(RESERVED)
未决(PENDING)
排它(EXCLUSIVE)。

每个数据库连接在同一时刻只能处于其中一个状态。每 种状态(未加锁状态除外)都有一种锁与之对应。

最初的状态是未加锁状态,在此状态下,连接还没有存取数据库。当连接到了一个数据库,甚至已经用BEGIN开始了一个事务时,连接都还处于未加锁状态。

未加锁状态的下一个状态是共享状态。为了能够从数据库中读(不写)数据,连接必须首先进入共享状态,也就是说首先要获得一个共享锁。多个连接可以 同时获得并保持共享锁,也就是说多个连接可以同时从同一个数据库中读数据。但哪怕只有一个共享锁还没有释放,也不允许任何连接写数据库。

如果一个连接想要写数据库,它必须首先获得一个保留锁。一个数据库上同时只能有一个保留锁。保留锁可以与共享锁共存,保留锁是写数据库的第1阶段。保留锁即不阻止其它拥有共享锁的连接继续读数据库,也不阻止其它连接获得新的共享锁。

一旦一个连接获得了保留锁,它就可以开始处理数据库修改操作了,尽管这些修改只能在缓冲区中进行,而不是实际地写到磁盘。对读出内容所做的修改保存在内存缓冲区中。

当连接想要提交修改(或事务)时,需要将保留锁提升为排它锁。为了得到排它锁,还必须首先将保留锁提升为未决锁。获得未决锁之后,其它连接就不能 再获得新的共享锁了,但已经拥有共享锁的连接仍然可以继续正常读数据库。此时,拥有未决锁的连接等待其它拥有共享锁的连接完成工作并释放其共享锁。

一旦所有其它共享锁都被释放,拥有未决锁的连接就可以将其锁提升至排它锁,此时就可以自由地对数据库进行修改了。所有以前对缓冲区所做的修改都会被写到数据库文件。

死锁

为什么需要了解锁的机制呢?为了避免死锁。

考虑下面表4-7所假设的情况。两个连接——A和B——同时但完全独立地工作于同一个数据库。A执行第1条命令,B执行第2、3条,等等。
表4-7 一个死锁的假设情况

A连接 B连接
sqlite> BEGIN;
sqlite> BEGIN;
sqlite> INSERT INTO foo VALUES('x');
sqlite> SELECT * FROM foo;
sqlite> COMMIT;
SQL error: database is locked
sqlite> INSERT INTO foo VALUES ('x');
SQL error: database is locked

两个连接都在死锁中结束。B首先尝试写数据库,也就拥有了一个未决锁。A再试图写,但当其INSERT语句试图将共享锁提升为保留锁时失败。

为了讨论的方便,假设连接A和B都一直等待数据库可写。那么此时,其它的连接甚至都不能够再读数据库了,因为B拥有未决锁(它能阻止其它连接获得共享锁)。那么时此,不仅A和B不能工作,其它所有进程都不能再操作此数据库了。

如果避免此情况呢?当然不能让A和B通过谈判解决,因为它们甚至不知道彼此的存在。答案是采用正确的事务类型来完成工作。

事务的种类

SQLite有三种不同的事务,使用不同的锁状态。

事务可以开始于:DEFERRED、MMEDIATE或EXCLUSIVE。事务类型在BEGIN命令中指定:
BEGIN [ DEFERRED | IMMEDIATE | EXCLUSIVE ] TRANSACTION;

一个DEFERRED事务不获取任何锁(直到它需要锁的时候),BEGIN语句本身也不会做什么事情——它开始于UNLOCK状态。默认情况下就是这样的,如果仅仅用BEGIN开始一个事务,那么事务就是DEFERRED的,同时它不会获取任何锁;当对数据库进行第一次读操作时,它会获取 SHARED锁;同样,当进行第一次写操作时,它会获取RESERVED锁。

由BEGIN开始的IMMEDIATE事务会尝试获取RESERVED锁。如果成功,BEGIN IMMEDIATE保证没有别的连接可以写数据库。但是,别的连接可以对数据库进行读操作;但是,RESERVED锁会阻止其它连接的BEGIN IMMEDIATE或者BEGIN EXCLUSIVE命令,当其它连接执行上述命令时,会返回SQLITE_BUSY错误。这时你就可以对数据库进行修改操作了,但是你还不能提交,当你 COMMIT时,会返回SQLITE_BUSY错误,这意味着还有其它的读事务没有完成,得等它们执行完后才能提交事务。

EXCLUSIVE事务会试着获取对数据库的EXCLUSIVE锁。这与IMMEDIATE类似,但是一旦成功,EXCLUSIVE事务保证没有其它的连接,所以就可对数据库进行读写操作了。

上节那个例子的问题在于两个连接最终都想写数据库,但是它们都没有放弃各自原来的锁,最终,SHARED锁导致了问题的出现。如果两个连接都以 BEGIN IMMEDIATE开始事务,那么死锁就不会发生。在这种情况下,在同一时刻只能有一个连接进入BEGIN IMMEDIATE,其它的连接就得等待。BEGIN IMMEDIATE和BEGIN EXCLUSIVE通常被写事务使用。就像同步机制一样,它防止了死锁的产生。

基本的准则是:如果你正在使用的数据库没有其它的连接,用BEGIN就足够了。但是,如果你使用的数据库有其它的连接也会对数据库进行写操作,就得使用BEGIN IMMEDIATE或BEGIN EXCLUSIVE开始你的事务。

Delphi和VC混合编程总结

  项目开发到了最后阶段,内核基本成型,Demo开发最终提上日程。

  为了尽快完成Demo开发,毕竟Demo只是为了演示,所以决定使用Delphi来完成,因为做界面Delphi的效率是相当高的。而我们的内核引擎是使用C++开发,VC编译,这就涉及到了Delphi和VC的混合编程,这里将这几天来的经验总结共享出来,需要的时候可以随时查看。

  目前的程序结构是这样的,内核使用dll封装为SDK,交付兄弟部门或者其他公司做二次开发,Demo采用Delphi主调内核,并实现结果呈现。

首先,在接口一层,要保证Delphi能够和VC进行数据交互,所以需要对应相关的数据类型。

同时这里不推荐在接口函数层使用复杂数据类型,因为如果参数和返回值过于复杂,是不能简单通过基本类型来进行传递的,这样编译器内部就会进行编码,以完成复杂数据类型的传递。但是不同的语言,甚至不同的编译器的内部实现不同,很容易导致复杂数据类型数据传递失败,而产生无效传递的情况,切记切记。

基本类型对照表如下:

VC Delphi
int Integer
unsigned int Cardinal
char Byte
wchar_t WideChar
int* PInteger
unsigned int* PCardinal
char* PByte
wchar_t* PWideChar

在数据类型对应的情况下,才能保证类型能够正确传递

其次,接口函数调用规范的统一

由于函数调用过程中函数参数要通过堆栈进行传递,而参数在堆栈中所占空间的释放问题由谁进行,直接导致函数调用规范的提出,这就是我们知道的stdcal,cdcel,pascal,fastcal等调用规范,为了保证参数的正确传递和结果的正确获取,需要在两种语言中统一调用规范。

这里使用stdcal,没有什么特殊的理由来选择这个,目的只有一个,那就是统一,以保证参数传递正确

最后,Delphi中声明VC动态链接库的导出函数

格式举例:

原型:

const wchar_t* __stdcall testW(const wchar_t* waveFile, const wchar_t* waveFmt, const wchar_t* grammarList, const wchar_t* params, int *recogStatus, int *result);
function test(waveFile:PWideChar;
                          const wav_Fmt: PWideChar;
                          const grammarList:PWideChar;
                          const params:PWideChar;
                          recogStatus:PInteger;
                          func_result:PInteger): PWideChar;stdcall;
                                                 external “qisr.dll” name 'testW';

简单解释一些函数声明的含义:
function 用于定义函数
external 关键字用于定义导出该函数的动态链接库名
name 用于定义动态链接库中导出函数的导出名

如果有VC链接库开发经验的朋友可能会发现,为什么Delphi中没有类似VC中对应于dll的lib导入库呢?这里简单解释一下,其实VC中与dll配套的lib文件起到的作用也就是告诉连接器静态链接导出函数的时候,从哪里获取导出函数入口地址用的,实际没有实体代码,这里Delphi中的声明代码其实起到的也就是这个作用。由于lib没有实体代码,所以很容易通过dll获取对应的lib导入库。

(转)static_cast、dynamic_cast、reinterpret_cast和const_cast的区别

关于强制类型转换的问题,很多书都讨论过,写的最详细的是C++ 之父的《C++ 的设计和演化》。最好的解决方法就是不要使用C风格的强制类型转换,而是使用标准C++的类型转换符:static_cast, dynamic_cast。标准C++中有四个类型转换符:static_cast、dynamic_cast、reinterpret_cast、和const_cast。下面对它们一一进行介绍。

static_cast

用法:static_cast < type-id > ( expression_r_r )

该运算符把expression_r_r转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:
用于类层次结构中基类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的。
用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
把空指针转换成目标类型的空指针。
把任何类型的表达式转换成void类型。
注意:static_cast不能转换掉expression_r_r的const、volitale、或者__unaligned属性。

dynamic_cast

用法:dynamic_cast < type-id > ( expression_r_r )

该运算符把expression_r_r转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;如果type-id是类指针类型,那么expression_r_r也必须是一个指针,如果type-id是一个引用,那么expression_r_r也必须是一个引用。

dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。

在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。

class B {
public:
    int m_iNum;

    virtual void foo();
};

class D : public B {
public:
    char *m_szName[100];
};

void func(B *pb) {
    D *pd1 = static_cast<D *>(pb);
    D *pd2 = dynamic_cast<D *>(pb);
}

在上面的代码段中,如果pb指向一个D类型的对象,pd1和pd2是一样的,并且对这两个指针执行D类型的任何操作都是安全的;但是,如果pb指向的是一个 B类型的对象,那么pd1将是一个指向该对象的指针,对它进行D类型的操作将是不安全的(如访问m_szName),而pd2将是一个空指针。另外要注意:B要有虚函数,否则会编译出错;static_cast则没有这个限制。这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见)中,只有定义了虚函数的类才有虚函数表,没有定义虚函数的类是没有虚函数表的。

另外,dynamic_cast还支持交叉转换(cross cast)。如下代码所示。

class A {
public:
    int m_iNum;
    virtual void f(){}
};

class B : public A {
};

class D : public A {
};

void foo() {
    B *pb = new B;
    pb->m_iNum = 100;
    D *pd1 = static_cast<D *>(pb); //copile error
    D *pd2 = dynamic_cast<D *>(pb); //pd2 is NULL
    delete pb;
}

在函数foo中,使用static_cast进行转换是不被允许的,将在编译时出错;而使用 dynamic_cast的转换则是允许的,结果是空指针。

reinpreter_cast

用法:reinpreter_cast (expression_r_r)

type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。

该运算符的用法比较多。

const_cast

用法:const_cast (expression_r_r)

该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression_r_r的类型是一样的。

常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。

Voiatile和const类试。举如下一例:

class B {
public:
    int m_iNum;
}

void foo() {
    const B b1;
    b1.m_iNum = 100; //comile error
    B b2 = const_cast<B>(b1);
    b2. m_iNum = 200; //fine
}

上面的代码编译时会报错,因为b1是一个常量对象,不能对它进行改变;使用const_cast把它转换成一个常量对象,就可以对它的数据成员任意改变。注意:b1和b2是两个不同的对象。

最容易理解的解释:
dynamic_cast: 通常在基类和派生类之间转换时使用;
const_cast: 主要针对const和volatile的转换.
static_cast: 一般的转换,如果你不知道该用哪个,就用这个。
reinterpret_cast: 用于进行没有任何关联之间的转换,比如一个字符指针转换为一个整形数。