推荐使用 boost.property_tree,简洁,高效,支持多种格式: ini , xml ...
并且使用统一的操作接口。
re: C++的一个疑惑 luckycat 2010-12-20 22:43
B(A& a){} 不但是一个构造函数,而且是一个自定义的类型转换操作( A -> B),你的问题有出在这里,如果要去掉这种非有意的自定义类型转换,使用 explicit B(A& a){}。
一个 非const引用,只能引用与其类型完全相同的对象,或者是其派生类的对象 ,所以 B &refB = objectB ; B &refB = objectB1 都是合法的,但是 B &refB = objectA 就不是合法的
,因为 A 与 B的类型不相同,且不是B的派生类,所以编译时会报错,于是 " B1( B& b ); A a; B1 b1(a)" 就不能通过,简化一下就相当于 " B &b = a".
一个 const引用 满足 非const引用 的特性的同时,还有很重要的一点,const 引用可以引用一个与其类型完全不相同的类型(因为编译器会生成一个转换后可引用的临时对象),
前提是被引用的类型可以转换为引用的类型(编译器自定义的类型提升,或者是用户自定义的类型转换,如上面的 B(A& a)。 ),
举个例子:
const int &iValue = 3.14; 就是OK的,这里使用编译器内部的类型转换 double -> int.
const B &b = a; 也是OK的,因为使用 B( A &a) 可以将 A -> B ,于是 const B &b = a; 的背后,编译器所做的就是:
const B tempB( a ); //调用 B( A &a)
const B &b = tempB;
BTW: 为什么在const引用情况下,编译器会生成一个可被引用的临时对象,原因很简单,你是用一个 const引用 来操作这个临时对象,所以,这个临时对象的状态是不会变的,
也就是安全的(当然,如果你把const引用 const_cast 成一个非 const引用来操作这个编译器生成的临时对象,那么结果是未定义的).
re: Google Test测试框架 luckycat 2010-05-26 23:00
@ouyang
GoogleTest同样可以用于测试Win32 GUI Application。
你的想法可能是MFC写出来的应用是没有对应的控制台,所以GoogleTest无法将输出结果显示出来(实际上Win GUI应用程序也可以同时具备Console Output,只不过这需要手工编码实现,默认情况下是没有的),当然GoogleTest已经考虑到这个问题了,GoogleTest支持将测试结果以XML文件格式输出.参考下面的链接:
http://code.google.com/p/googletest/wiki/GoogleTestAdvancedGuide#Controlling_Test_Output
@小苏
VC6我这里没有,我上面的输出是在VC2008下的测试结果,你换用VC2005/2008再试试.
我的建议是学习C++就不要用VC6了,可以用VC2005/2008.
如果你想用一个轻量级的环境学习C++,MinGW Studio,
wxDev-Cpp , CodeBlocks , CodeLite 都是不错的选择.
@小苏
为了更好的理解我上面的分析,你可以打开VC2005/VC2008(我这里是VC2008)的单步调试,
在调试模式下的"自动窗口"(位于IDE下方).
观察memset 前后"man -> strName -> _Bx -> _Ptr"的值的变化
_Ptr 实际上就是std::string内部用于存储字符串的堆内存缓冲区的地址,也相当于我上面提到的 m_pCharBuffer
@小苏
在阅读下面的分析之前,希望你对"C++对象的内部布局"有一定的了解.
既然你也发现了内存泄漏的情况,那么你再用下面的测试代码运行一下:
#include "Windows.h"
#include <string>
#include <cstring>
#include <cstdlib>
using namespace std;
typedef unsigned int UINT32;
typedef unsigned short UINT16;
typedef struct structMan
{
UINT32 sexType; //ENUM_SEXY_TYPE
UINT16 usAge;
string strName;
string strAddress;
bool operator < (const structMan &man) const
{
return usAge < man.usAge;
}
bool operator > (const structMan &man) const
{
return usAge > man.usAge;
}
}MAN;
int main( int argc , char *argv[] )
{
while( true )
{
MAN man;
fprintf( stdout , "before memset: char buffer address( heap address ) to store string = %p , size = %lu , capacity = %lu\n" , \
*reinterpret_cast< const int* >( man.strName.c_str() ) , \
man.strName.size() , man.strName.capacity() );
memset( &man , 0 , sizeof( MAN ) );
fprintf( stdout , "after memset: char buffer address( heap address ) to store string = %p , size = %lu , capacity = %lu\n\n\n" , \
*reinterpret_cast< const int* >( man.strName.c_str() ) , \
man.strName.size() , man.strName.capacity() );
man.strAddress = "abcdef";
man.strName = "abc";
Sleep( 1000 ); //这里sleep是为了让大家有时间在任务管理器中看到内存增长的过程,不至于一下子耗尽内存.
}
return 0;
}
我选取我这里的一个循环中的输出,如下:
before memset: char buffer address( heap address ) to store string = 00636200 ,
size = 0 , capacity = 15
after memset: char buffer address( heap address ) to store string = 00000000 ,
size = 0 , capacity = 0
下面把上述代码进行简化便于分析:
while( true )
{
MAN man; //这里会使用man由编译器自动生成缺省构造函数来调用strName的缺省构造函数对strName进行构造.
memset( &man , 0 , sizeof( MAN ) );
上面的memset操作会把 &man 这个地址开始的 sizeof( MAN )字节的内存空间全部清零.
这也就意味着 man 对象内部的每个成员子对象所占据的内存都被清零.
man 对象内部一个 std::string , 而std::string 内部包含一个std::string用于实际存储字符串的指向动态分配的堆内存的指针,
我们假设这个指针的名称为 m_pCharBuffer;
在std::string的析构函数中释放这个动态分配的堆内存的时候需要使用这个m_pCharBuffer,也即是调用 delete[] m_pCharBuffer;
如果我写出下在的代码:
char *m_pCharBuffer = new char[ BUFFER_SIZE ]; 这一个操作即是strName的缺少构造函数的操作,只不过 BUFFER_SIZE = 15 + 1(最后有一个'\0');
m_pCharBuffer = NULL; //这一个操作与上述的 memset 对 man.strName中用于指向动态内存的指针所产生的作用相同:将指针所指向的堆内存地址清零.
delete[] m_pCharBuffer; //此时 m_pCharBuffer 为NULL , 不过在C++中, delete NULL指针是安全的.不过因为 m_pCharBuffer 已经不指向上述new出来的内存
//所以这里进行 delete[] m_pCharBuffer 时已经不能进行资源的释放了,也即是发生了内存泄漏.
man.strName = "abc";
上面的赋值操作中,实际上要调用: std::string::operator=( const char* );
首先,operator=会判断当前的strName的 capacity能否容纳下"abc",由上面的memset之后我们可以看出此时存储 capacity 值的变量因为memset为0,所以
man.strName.capacity() 输出为0,这也就意味着这个"容积"不能容纳下3个字节的"abc".
所以这时 operator= 要扩大内部用于存储字符串的缓冲区,扩充的基本原理如下:(代码进行简化处理)
std::string& operator=( const char *szString )
{
// check parameter
if( m_pCharBuffer != szString ) // 防止 self-assign
{
delete[] m_pCharBuffer;
m_pCharBuffer = new char[ NEW_SIZE ];
memcpy( m_pCharBuffer , szString , strlen( szString ) + 1 );
}
return *this;
}
上面的操作: *reinterpret_cast< const int* >( man.strName.c_str() ) 即是相当于获取这个 m_pCharBuffer 的地址.
这一点你一定要明白.
由上面的代码以及运行输出可以知道,
注意: 在调用 strName = "abc"时,已经进行了memset操作,此时的 m_pCharBuffer 已经因为上面的 memset操作而被清零,即是 m_pCharBuffer = NULL,
因为memset操作不会调用析构函数 ,所以实际上在清零之前它所指向的动态内存块并没有被释放,
在 operator=中,delete[] m_pCharBuffer; 相当于 delete[] NULL;
这就不能释放 m_pCharBuffer 之前在缺省构造时所指向的动态分配的 15 + 1 字节的内存了,所以出现了内存泄漏.
}
@小苏
你运行后仔细观察这个编译后运行的exe在"windows任务管理器"中对应的
"内存使用"数值.
我用VC2005和VC2008都测试过,结果是"内存不停增长".
还用哪位同学运行过我上面的测试代码,出来公布一下测试结果,谢谢!
re: 设计的两难:选择异常还是两段构造 luckycat 2010-03-06 21:27
@陈梓瀚(vczh)
你列举的判断标准都值得借鉴,不过你后续补充的对"例外情况"的处理方式不敢苟同:
因为构造可能没有成功,那么我们需要调用IsAvailable之类的函数,甚至于后续需要因为判断之前构造函数的状态来
对调用的每个成员函数进行"try catch"或者还要从"每个成员函数的返回值中来判断之前的构造操作是否成功".
这种设计是可行的,但对类的使用者来说太复杂.
这种情况下我觉得使用"两段构造"可能更好一些,我们只需要判断"两段构造"是否成功即可,如果构造成功,在后续的成员函数调用过程中,
就再也不用为了确认构造函数的状态来对每个被调用的成员函数进行"try catch"或检查返回值的操作,这样的设计应该更简洁一些.
@小苏
你后续修改的代码,在我看来,即使在多个编译器下都是OK的,但是就代码风格来说,还有改进的地方.
在编码过程中,我很少会对struct进行memset操作,只是偶尔会对sockaddr进行memset操作;更不会对class进行memset操作.
在你上述的代码中,你对MAN进行memset操作,无非也就是想将各个成员的初值清零,如果基于这个出发点,那设计一个构造函数多好:
structMan::structMan( UINT32 enumSexType = SEXY_TYPE_MAN , \
UINT16 uiAge = 0 , \
const std::string &refStrName = "" , \
const std::string &refStrAddress = "" )
:sexType( enumSexType ) , usAge( uiAge ) , \
strName( refStrName ) , strAddress( refStrAddress )
{
// check parameters here。
}
只需要少量的代码就会带来大量的方便,而且你也就再也不用memset.
你也就不需要对struct的各个成员依次赋值了,直接传参构造就可以了,这样代码应该会更优雅一些.
另一方面,对 std::list 进行sort操作从逻辑上是没有问题的,但是设计风格上是有问题的:
因为std::list中的每一个成员是基于链的形式连接在一起的,所以我们不能对其进行随机访问,
如果我们要访问std::list中的第N个成员,那么我们需要从链表头开始向链表尾部依次迭代N次,
在这种情况下,如果一个链表过大,那么这里就有效率问题.
一般情况下,我们只对"类似于数组的可以随机访问"的std容器进行排序.
呵呵,我就喜欢大家这种踊跃讨论的氛围,互相学习:)
上面的代码你在VC6下面测试通过了,因为从我的第一感觉来看,必定:coredump.
当时我还真不太相信,所以我自己也测试了一下,结果如下:
Win32: VC2005 debug/release下均可运行正常,不过因为memset非POD,出现内存泄漏.
Win32: MinGW Studio 直接abort.(这是我预期的结果).
Linux: Slackware32/GCC 直接abort.(这也是我预期的结果).
为了证明上在win32/VC2005下上面的代码出现内存泄漏,大家可以用下面的代码做测试:
(这里把小苏同学的代码取了一部分用于配合测试)
运行下面的代码,大家在任务管理器中观察内存增长情况:)
#include "Windows.h"
#include <string>
#include <cstring>
#include <cstdlib>
using namespace std;
typedef unsigned int UINT32;
typedef unsigned short UINT16;
typedef struct structMan
{
UINT32 sexType; //ENUM_SEXY_TYPE
UINT16 usAge;
string strName;
string strAddress;
bool operator < (const structMan &man) const
{
return usAge < man.usAge;
}
bool operator > (const structMan &man) const
{
return usAge > man.usAge;
}
}MAN;
int main( int argc , char *argv[] )
{
while( true )
{
MAN man;
memset( &man , 0 , sizeof( MAN ) );
man.strAddress = "abcdef";
man.strName = "abc";
Sleep( 10 ); //这里sleep是为了让大家有时间在任务管理器中看到内存增长的过程,不至于一下子耗尽内存.
}
return 0;
}
@小苏
sorry,没有注意到最后一句话"注意: 以上代码在VC6环境下编译、测试通过".
我所指出的bug依然存在,不同的编译器对"memset 非POD处理方式可能不一样".
即使VC6测试通过,你可以换个编译器试试.
看完代码,给我的第一感觉:代码存在严重的bug(不知道你自己测试过没有).
简单的说就是"不要对非POD类型进行memset操作".
在C++中不要对class进行memset操作;尽量不要对struct进行memset操作.
re: 设计的两难:选择异常还是两段构造 luckycat 2010-03-05 22:40
@qiaojie
谢谢赐教!
"像std::invalid_arguement基本没人会去用"这句话说得有点绝对了,用的人应该还是有一些的,可能我们没有接触到.
另外,我们总是被"天朝"代表,想不到这次被 qiaojie 代表了:)
你说"保证参数的正确性是调用者的责任,而不是被调用的函数的责任",
这一点我也同意,不过我觉得作为函数的设计者,我们不应当对用户所传递的参数有太多理想化的假设,所以我们应当在函数中进行参数合法性的检查,
一方面,可以在函数的入口处尽早发现非法参数的问题,这样就不至于后续会使用错误的参数在函数中进行一些无意义的操作.
另一方面,在函数入口处检查参数的合法性,可以增强函数的健壮性,进一步增强系统的健壮性.
举个例子,如果传递给函数的实参不应该是NULL指针,用户却以NULL作为实参调用函数,假设我们没有进行对应参数合法性检查,
那么后续基于这个NULL实参的操作可能会导致系统"coredump".
对于参数的合法性检查,在debug版本和release版本下应该都需要进行,类似于"assert(0 < age && age < 200);"这种检测参数的合法性的代码只在debug版本下可以起作用,
在release版本下就不起用了,也就不能在release版本下作为参数合法性检查的工具.
在debug版本下,如果assert断言失败,那么我们可以看到对应的abort信息,然后程序异常退出.
实际上这样做可能有的时候并不合适,因为在一些情况下,仅仅是参数非法,我们可以进行相应的处理而不需要系统因此而退出运行.
"强调不要用异常或者错误返回值,是因为盲目的大量使用这类错误处理机制会导致整个项目变得混乱"
这句话如果仅仅是理论上来探讨"如何让系统设计的更优雅",那么这无疑可以作为一个"系统设计准则",
但是在实际的开发过程中,有的时候一个函数内部出现"非正常情况"的可能性实在是太多了,我们必须要进行相应的处理.
如果我们既不使用"异常"也不使用"返回错误码"的形式来告知调用者,
那么在反馈给调用者"函数内部出现非正常情况"这一点上我们将"无能为力",但我们又必须在这一点有所作为.
在大多数情况下,"异常"和"错误码"可能是我们仅有的两个选择方案,如何选择其一作为最终的处理方案,
甚至如何在不使用"异常"和"错误码"的前提下也达到相同的效果,这是一件很"纠结"的事情.
追求系统在架构和代码设计上的完美是开发者的一个方向,但是有时我们需要考虑"追求完美的代价",
在时间,人力以及成本的多重影响下,很多时候我们必须放弃对最优方案的探索,而选择一种"不那么完美但是可行,可以很好解决问题"的方案.
也许这个时候作为函数调用状态反馈的"异常"和"错误码"机制会在我们的思考和运用范围之内.
re: 设计的两难:选择异常还是两段构造 luckycat 2010-03-05 19:33
@qiaojie
也许你的异常哲学是正确的并值得大家学习,还请你发文一篇让大家有一个学习的机会,
如果从你的文章中我确实发现了自己的错误,也会从中有所改正,当然,你也不需要"对牛弹琴"这个词语.
我这篇文章中的"People"只是一个用于作为讨论基础的例子,根本的问题是对于"构造函数的参数非法或是构造失败"时,我们应当如果告知调用者.
我并没有说一定要把参数的合法性全部放在构造函数中完成,但是在构造函数中检查参数的合法性是应该的,
就像上面的同学说的"为了程序的健壮性,多余的操作也是必须的"。
在这个例子中,你可以往自己熟悉的GUI方向进行特化,所以你可以使用"对话框"之类的工具来进行传入构造函数之前的参数合法性检验以及进行相关的错误处理,
但是在那些"非GUI"的领域,在那些"我们不能确保传入构造函数的参数一定是合法的,不能保证构造函数一定会构造成功"的情况下,我们到底该如何处理,
我考虑到可以使用"基于异常"或"基于两段构造的形式".
C++提供的异常机制是一种工具,可以作为"函数内部向函数的调用者传递函数内部非正常运行状态"的一种方法.
就如同你说的"内存耗尽,网络错误,文件错误"这种情况下是异常,也许这种情况下我们应当使用"异常机制"(希望没有理解错).
但是如果一个函数内部可能出现"内存耗尽"也会出现"参数非法的问题"(再重申一遍,我们不能永远都保证传入每一个函数的参数都是合法的).
"内存耗尽"这种情况我们使用异常,但是"参数非法问题"我们使用什么呢,
按照你的看法,"参数非法"不属于异常的范围之内,我们不应该使用"异常的形式",但我们还是要告知用户"参数非法"的信息,
假定这里我们"无法使用类似于弹出对话框的形式来告知用户参数非法",那么我可以想到的告知调用者这一信息的方式是"使用错误码",
当然,我们还可以选择"errno"的形式.
这样一来,我们就面临一个问题"一个函数会以异常和错误码两种方式来告知调用者相关的非正常运行信息",
接下来,调用者就要同时使用"try catch"和检查函数的错误码两种方式来检查函数的运行状态,
我觉得如果真的这样设计函数的话,这就是一种很糟糕的设计,不知道你怎么认为.
在告知调用者一个函数内部的"非正常状态"时,我只会择优使用"错误码"或"异常这两种形式"之一,不会同时使用.
基于这一点,如果我选择"以错误码的形式"来反馈给调用者,那么在函数内部"网络错误"时我也会使用错误码来告知调用者(按你的看法,这种情况应该使用异常),
如果我选择"基于异常"的形式,那对"参数非法"的信息我也会抛出"std::invalid_arguement".这是设计上的取舍产生的必然选择.
说到这里,不知道你对于作为std异常类型之一的"std::invalid_arguement"这个词语有什么感想,
我觉得你应该向标准委员会指明"std::invalid_arguement"这个词语,
"从使用异常的哲学上的角度上来看这个概念是错误的,因为参数非法根本就不是异常,我们又怎么能因为参数的非法而throw std::invalid_arguement,
这是在误导广大的std用户,所以必须去掉".
re: 设计的两难:选择异常还是两段构造 luckycat 2010-03-05 12:02
@饭中淹
将"两段构造"与"Fatcory Pattern"结合起来确实是一种巧妙的设计!
内部实现上还是"两段构造",但是对于 class 的用户而言,class CPeople 展现的却是一个单一的"构造接口",
用户一旦调用这个接口"构造对象",那么"两段构造"自动完成,极大地减少了"两段构造"中因为忘记调用"Initialize"所带来的问题.
class CPeople 中的 Create 和 Release 所扮演的角色类似于"构造函数和析构函数",都是进行资源的分配与回收操作.
单纯从"资源管理"的角度来说,肯定是"构造函数和析构函数"相比如"Create 和 Release"更优一些,
因为"构造函数和析构函数"对于"非动态分配的对象以及非placement new方式生成的对象",
构造和析构都会由编译器保证正确自动地调用,大大简化了对资源的管理,或许这也是C++设计构造和析构的出发点之一.
在"两段构造" & "Fatcory Pattern"这种模式下,所有的CPeople对象将都由 Create 接口创建,这势必需要我们管理大量的动态分配的对象,
在这种情况下,如果稍有不慎,我们将面临"resource leak"的问题.这个时候如果我们能将动态分配的CPeople对象用一种更方便安全的方式来管理就更好了,
于是我想到了boost::shared_ptr,不知道大家想到了什么?
类似于下面这样:
void FreeResource( CPeople *pPeople )
{
if( NULL != pPeople )
{
pPeople -> Release();
}
}
CPeople *pHebe = CPeople::Create( 2 );
if( NULL == pHebe )
{
// handle error
}
boost::shared_ptr< CPeople > pPeople( pHebe , FreeResource );
下面我们就可以使用 pPeople 这个智能指针"do whatever you want" :) ,而且使用起来直观方便:
pPeople -> Sing();
也减少了对动态分配资源进行管理的复杂度.
re: 设计的两难:选择异常还是两段构造 luckycat 2010-03-04 23:25
@qiaojie
呵呵,可不能一棒子打死啊.
至于说,类似于"用户输入非法数据"之类的问题到底是算作错误还是异常情况,这一点依赖每个人对同一个事物的认知,
有的人认为这是异常情况,有的人认为这是错误,这个认知层面上的问题我们先不讨论,尊重每个人的看法.
实际上,即使存在上面的认知差异也没有关系,因为问题的本质是对"用户输入非法数据"这种异常也好,错误也好,
我们在代码逻辑中应该如何处理,你觉得应该用类似于"对话框+ValidateUserInput"之类的方法来处理,
我觉得可以通过返回错误码或抛出异常的形式来做处理. 本质上都是在处理一种"非正常情况",只是我们的处理方式不同,
你说应该用你的方法比较好,我觉得用我的方法处理也是可行的,到底用哪一种呢,
即使在这种情况下,我们还是很难选择一个所有人都接受的处理方式. 这里就涉及到设计的权衡和取舍了,有很多种方法都可行,我们尊重每个人在特定的环境中所做出的选择.
/*
"而是在用户输入完成,对话框结束,构造People之前,加入一个ValidateUserInput()函数来校验用户输入,
如果age属于非法值,弹出一个错误对话框向用户说明错误的原因"
*/
你这里只是对一种特例的处理,实际上我们很难在所有的情况都保证传入构造函数的参数是合法的,要是我们真的找到了这样一种方法,
那么"there is a silver bullet !" , 接下来,对于所有奋斗在开发一线的同学们而言,生活就要美好很多,应该再也不会发生类似于"小贝"的悲剧了:)
在你处理的特例中,既然我们能够保证传入构造函数的参数一定是合法的,那确实太好了,"使用异常"和"两段构造"都是多余的.
对于那种我们不能确保传入构造函数的参数是一定是合法的情况,我们该选择哪种处理方式呢,这是这篇文章讨论的根本问题.
如果因为构造函数的参数不合法,或者因为其它的原因构造失败,最基本的一点,我们应当让调用者知道这一情况,
至于调用者如何处理就不在我们关心的范围之内了,是"弹出对话框告知用户重试","忽略这个错误",还是直接"abort",不同的场景下也有不用的选择.
我们要做的就是在"构造一个对象发生异常时"告知调用者"发生了非正常情况".
这篇文章的主题也就是讨论"在构造发生非正常情况时采取何种方式来告知调用者这一情况".
对于这个问题很难有一个"放之四海而皆准"的处理方案,因为这涉及到不同的编程风格,应用场景和设计时的取舍.
不过我们还是可以踊跃地发表自己的看法,在讨论和交流的过程中我们总能发现思维的闪光点.互相学习:)
re: 设计的两难:选择异常还是两段构造 luckycat 2010-03-04 22:32
@Corner Zhang
关于这一点,我们的观点还是很相近的,不同的编程风格决定了不同的设计取舍以及代码风格,
没有哪一种一直都是最优的,选择一种适合自己的并一直坚持下去(当然,适当的时候还是要变通一下).
实际上我并不愿意在代码中大量使用"throw try catch",所以,基本上我不愿意去看java代码,
就如你所说的,传统的代码风格易于跟踪调试;在我看来,传统的"基于错误码"的代码比基于"使用异常"的代码
要紧凑得多,因为我们可以在错误发生的地方立即处理错误,而不像"基于异常"的代码中我们要向下跨越N行代码
来进行错误处理(这一点使得代码的可读性很差).
而且,如果try代码块中太大,那么在对应的catch块中尽管我们可以进行相应的异常处理,但是此时我们却失去了
对发生错误的代码上下文的必要了解.这一点使得我们降低了对代码整体运行流程的可预知性,
更重要的是也降低了错误处理的针对性,因为同一种类型的异常可能由try代码块中的多个地方throw.具体是哪一个throw的无从了解.
so,我的观点是:让构造函数尽量简单,减少误用的可能性,并增加构造函数的安全性(尽量减少构造函数构造失败的可能性).
这样我们也就能在一定程度上减少对异常机制的依赖.至于其它的可带有返回值的成员函数都使用"返回错误码"来取代"抛出异常".
我觉共享内存的这种"非显示删除自保留性"是很有用的特性而不是问题,也许当时设计共享内存机制时这种特性是有意提供的.
设想一个简单的例子:
我们使用一块有访问控制保护的内存缓冲区来存储进程或线程之间共用的的数据.
并且我们需要保证数据的安全性,即使server倒掉,这块缓冲区里面的数据也不能丢失.
1. 对于多线程的情况,当一个线程core掉之后(比如因为segment fault),线程所对应的进程将不能幸免.
在这种情况下,如果我们使用的是一般的"内存缓冲区"而不是共享内存,那么当进程退出后,
这块缓冲区对应的内存空间将会被系统回收重新分配给其它进程使用,缓冲区中对应的数据也就丢失了.
但是如果我们换用共享内存来作为线程间交换数据的缓冲区,我们就能很好的解决这个问题.
2. 在多进程的情况下,不但能满足上述特性,而且共享内存也是进程间数据交换的一种高效方式.
至于共享内存的手动删除问题,我的做法是,在生成IPC对象时,将对应的ftok生成的key值写入到一个文件中,
生成类似于下面的shell脚本,这样当我们需要手动删除IPC时也很方便:
ipcrm -M xxx
ipcrm -Q xxx
ipcrm -S xxx
相比于通过socket来作为进程间通信的方式,共享内存的最大不足在于"不能跨机器共用".
after all, there is no silver bullet :)
你的代码中是通过判断信号量的值为0来作为对共享内存读取操作的依据,即是如下代码:
wait_v(semid);
printf("Message geted is: %s \n",shm + 1);
但实际上这里有一个潜在的问题:
即如果 wait_v(semid); 成功后,在执行接下来的printf("Message geted is: %s \n",shm + 1)之前,进程被挂起.
那么此时 server 进程可能会重新获取这个信号量并对共享内存中的数据进行写操作(当然,你这里用server sleep的时间远大于client sleep的时间来解决这个问题)
当挂起的进程重新被调度投入运行后,此时printf("Message geted is: %s \n",shm + 1)的数据实际上就不是wait_v(semid)成功后共享内存中对应的数据.
我觉得对于这种典型的 "provider/consumer" 模型,一种更好的做法是
// for provider // for consumer
P( write_sem ); P( read_sem );
// write operation // read operation
V( read_sem ); V( write_sem );
当然,这里我们需要使用 write_sem 和 read_sem 两个信号量.
re: Linux下快速扩展文件大小 luckycat 2010-03-02 21:28
@阿福:
一直都把truncate用作截断文件,没有发现truncate还可以用于扩展文件大小,刚才看了一下 man 文档:
int truncate(const char *path, off_t length);
If the file previously was larger than length, the extra data is discarded.
If the file was previously shorter than length, its size is increased, and the extended area appears as if it were zero-filled.
这样一来,截断和扩展文件都可以用"truncate"来完成,这样相对于上面的EnlargeFile就更简洁了而且基于"truncate"的形式只需要一次系统调用即可实现相同的效果,效率上也更有优势.
看来这次真的是"reinvent the wheel"了:(
thank you for reminding me.
最后分享一个 linux 2.6 的 man pages 打包成的CHM文件,在上面的下载文件中.
re: Linux下快速扩展文件大小 luckycat 2010-03-02 12:53
谢谢指教!
以前在Win32下做过一段时间,深感Microsoft的巨大努力给我们带来的便利:) 海量的MSDN和丰富的Win32 API让我们遇到问题有据可查,
同时也减少了大量"reinvent the wheel"的时间.
但是到了*nix下面,很多东西都不一样了,*nix的哲学是"提供解决问题的机制而不是具体的实现",相反,Win32的哲学是"提供具体的实现但是不告诉你具体的机制"。
所以这篇文章的出发点就是"利用Linux提供的机制来解决一个实际的问题",形式上与Win32的"SetFilePointer & SetEndOfFile"组合不一样,但是仔细分析一下,
它们是如此的相似:按照你的建议"先调用 SetFilePointer(设置文件逻辑指针位置) 然后调用 SetEndOfFile(设置文件物理末端位置)"即可快速扩展文件大小。
在上面的代码中将参数合法性判断以及对应的函数调用状态判断去掉,简化一下就是下面这样了:
bool EnlargeFile( int iFileHandle , off_t iNewSize )
{
1. lseek( iFileHandle , 0 , SEEK_CUR ); //保存文件指针的当前位置以便于在扩展文件大小后恢复到当前位置
2. lseek( iFileHandle , iMoveOffset , SEEK_SET );
3. write( iFileHandle , " " , WRITE_BYTE_COUNT ); //写入一个字节的数据,完成对文件大小的更改
4. lseek( iFileHandle , iCurPos , SEEK_SET ); //恢复文件指针到之前保存的文件位置
return true;
}
其中的第1行和第4行是为了在扩展文件的过程中保存和恢复文件指针位置,如果我们将这一点也简化掉(实际上必须保留),如下:
bool EnlargeFile( int iFileHandle , off_t iNewSize )
{
// 设置文件指针(这里是逻辑指针)位置,相当于Win32下调用 SetFilePointer
2. lseek( iFileHandle , iMoveOffset , SEEK_SET );
// 写入一个字节的数据,完成对文件大小的更改,即是设置了文件的物理末端指针位置,相当于调用了 SetEndOfFile
3. write( iFileHandle , " " , WRITE_BYTE_COUNT );
return true;
}
这一次很清晰了,形式上不一样,但是本质上很相近了。