[转]静态成员数据和静态成员函数
在没有讲述本章内容之前如果我们想要在一个范围内共享某一个数据,那么我们会设立全局对象,但面向对象的程序是由对象构成的,我们如何才能在类范围内共享数据呢?
这个问题便是本章的重点:
声明为static的类成员或者成员函数便能在类的范围内共同享,我们把这样的成员称做静态成员和静态成员函数。
下面我们用几个实例来说明这个问题,类的成员需要保护,通常情况下为了不违背类的封装特性,我们是把类成员设置为protected(保护状态)的,但是我们为了简化代码,使要说明的问题更为直观,更容易理解,我们在此处都设置为public。
以下程序我们来做一个模拟访问的例子,在程序中,每建立一个对象我们设置的类静态成员变自动加一,代码如下:
#include <iostream>
using namespace std;
class Internet
{
public:
Internet(char *name,char *address)
{
strcpy(Internet::name,name);
strcpy(Internet::address,address);
count++;
}
static void Internet::Sc()//静态成员函数
{
cout<<count<<endl;
}
Internet &Rq();
public:
char name[20];
char address[20];
static int count;//这里如果写成static int count=0;就是错误的
};
Internet& Internet::Rq()//返回引用的成员函数
{
return *this;
}
int Internet::count = 0;//静态成员的初始化
void vist()
{
Internet a1("中国软件开发实验室","
www.cndev-lab.com
");
Internet a2("中国软件开发实验室","
www.cndev-lab.com
");
}
void fn(Internet &s)
{
cout<<s.Rq().count;
}
void main()
{
cout<<Internet::count<<endl;//静态成员值的输出
vist();
Internet::Sc();//静态成员函数的调用
Internet b("中国软件","
www.cnsoft.com
");
Internet::Sc();
fn(b);
cin.get();
}
上面代码我们用了几种常用的方式建立对象,当建立新对象并调用其构造函数的时候,静态成员cout便运行加1操作,静态成员的初始化应该在主函数调用之前,并且不能在类的声明中出现,通过运行过程的观察我们发现,静态成员count的状态并不会随着一个新的对象的新建而重新定义,尽而我们了解到类的静态成员是属于类的而不是属于哪一个对象的,所以静态成员的使用应该是类名称加域区分符加成员名称的,在上面的代码中就是Internet::count,虽然我们仍然可以使用对象名加点操作符号加成员名称的方式使用,但是不推荐的,静态态类成员的特性就是属于类而不专属于某一个对象。
静态成员函数的特性类似于静态成员的使用,同样与对象无关,调用方法为类名称加域区分符加成员函数名称,在上面的代码中就是Internet::Sc();,静态成员函数由于与对象无关系,所以在其中是不能对类的普通成员进行直接操作的。
如果上面的 static void Internet::Sc()修改成为:
static void Internet::Sc()//静态成员函数
{
cout<<name<<endl;//错误
cout<<count<<endl;
}
静态成员函数与普通成员函数的差别就在于缺少this指针,没有这个this指针自然也就无从知道name是哪一个对象的成员了。
根据类静态成员的特性我们可以简单归纳出几点,静态成员的使用范围:
1.用来保存对象的个数。
2.作为一个标记,标记一些动作是否发生,比如:文件的打开状态,打印机的使用状态,等等。
3.存储链表的第一个或者最后一个成员的内存地址。
为了做一些必要的练习,深入的掌握静态对象的存在的意义,我们以前面的结构体的教程为基础,用类的方式描述一个线性链表,用于存储若干学生的姓名,代码如下:
#include <iostream>
using namespace std;
class Student
{
public:
Student (char *name);
~Student();
public:
char name[30];
Student *next;
static Student *point;
};
Student::Student (char *name)
{
strcpy(Student::name,name);
this->next=point;
point=this;
}
Student::~Student ()//析构过程就是节点的脱离过程
{
cout<<"析构:"<<name<<endl;
if(point==this)
{
point=this->next;
cin.get();
return;
}
for(Student *ps=point;ps;ps=ps->next)
{
if(ps->next==this)
{
cout<<ps->next<<"|"<<this->next<<endl;
ps->next=next;//=next也可以写成this->next;
cin.get();
return;
}
}
cin.get();
}
Student* Student::point=NULL;
void main()
{
Student *c = new Student("marry");
Student a("colin");
Student b("jamesji");
delete c;
Student *fp=Student::point;
while(fp!=NULL)
{
cout<<fp->name<<endl;
fp=fp->next;
}
cin.get();
}
从上面的代码来看,原来单纯结构化编程需要的一个链表进入全局指针在这里被类的静态成员指针所替代(类的静态成员完全可以替代全局变量),这个例子的理解重点主要是要注意观察类成员的析构顺序,通过对析构顺序的理解,使用析构函数来进行节点的脱链操作。
静态成员的提出是为了解决数据共享的问题。实现共享有许多方法,如:设置全局性的变量或对象是一种方法。但是,全局变量或对象是有局限性的。这一章里,我们主要讲述类的静态成员来实现数据的共享。
静态数据成员
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。
使用静态数据成员可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。
静态数据成员的使用方法和注意事项如下:
1、静态数据成员在定义或说明时前面加关键字static。
2、静态成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式如下:
<数据类型><类名>::<静态数据成员名>=<值>
这表明:
(1) 初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆。
(2) 初始化时不加该成员的访问权限控制符private,public等。
(3) 初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员。
3、静态数据成员是静态存储的,它是静态生存期,必须对它进行初始化。
4、引用静态数据成员时,采用如下格式:
<类名>::<静态成员名>
如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员。
下面举一例子,说明静态数据成员的应用:
#include
class Myclass
{
public:
Myclass(int a, int b, int c);
void GetNumber();
void GetSum();
private:
int A, B, C;
static int Sum;
};
int Myclass::Sum = 0;
Myclass::Myclass(int a, int b, int c)
{
A = a;
B = b;
C = c;
Sum += A+B+C;
}
void Myclass::GetNumber()
{
cout<<"Number="< }
void Myclass::GetSum()
{
cout<<"Sum="< }
void main()
{
Myclass M(3, 7, 10),N(14, 9, 11);
M.GetNumber();
N.GetNumber();
M.GetSum();
N.GetSum();
}
从输出结果可以看到Sum的值对M对象和对N对象都是相等的。这是因为在初始化M对象时,将M对象的三个int型数据成员的值求和后赋给了Sum,于是Sum保存了该值。在初始化N对象时,对将N对象的三个int型数据成员的值求和后又加到Sum已有的值上,于是Sum将保存另后的值。所以,不论是通过对象M还是通过对象N来引用的值都是一样的,即为54。
静态成员函数
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。如果静态成员函数中要引用非静态成员时,可通过对象来引用。下面通过例子来说明这一点。
#include
class M
{
public:
M(int a) { A=a; B+=a;}
static void f1(M m);
private:
int A;
static int B;
};
void M::f1(M m)
{
cout<<"A="< cout<<"B="< }
int M::B=0;
void main()
{
M P(5),Q(10);
M::f1(P); file://调用时不用对象名
M::f1(Q);
}
读者可以自行分析其结果。从中可看出,调用静态成员函数使用如下格式:
<类名>::<静态成员函数名>(<参数表>);
posted @
2006-07-31 02:54 Jerry Cat 阅读(278) |
评论 (0) |
编辑 收藏
为MFC应用程序添全屏幕显示功能
在CMainFrame类中添加下列成员变量和成员函数(使用ClassWizard),下面是这些变量和函数的功能
说明:
成员变量:
BOOL m_bFullScreen; //全屏幕显示标志
CRect m_FullScreenWindowRect; //全屏幕显示窗口Rect
WINDOWPLACEMENT m_wpPrev; //用于保存正常视图时的窗口位置信息
CToolBar * m_wndFullScreenBar; //全屏幕显示时的浮动工具条
成员函数:
void OnMenuFullscreen(); //全屏幕显示的处理函数
void OnGetMinMaxInfo(); //捕获WM_GETMINMAXINFO消息以便允许你增加窗口大小
void OnUpdateViewFullScreen(); //更新“全屏幕显示”菜单的状态
源码
void CMainFrame::OnMenuFullscreen()
{//全屏幕显示的处理函数
RECT rectDesktop;
WINDOWPLACEMENT wpNew;
if (m_bFullScreen)
{//全屏幕显示模式
//隐藏工具条和状态条
m_wndStatusBar.ShowWindow(SW_HIDE);
m_wndToolBar.ShowWindow(SW_HIDE);
//保存正常视图时的窗口位置信息以便恢复原来状态
GetWindowPlacement (&m_wpPrev);
m_wpPrev.length = sizeof m_wpPrev;
//调整RECT为新的窗口尺寸
::GetWindowRect ( ::GetDesktopWindow(), &rectDesktop );
::AdjustWindowRectEx(&rectDesktop, GetStyle(), TRUE, GetExStyle());
//保存RECT以便OnGetMinMaxInfo()使用
m_FullScreenWindowRect = rectDesktop;
wpNew = m_wpPrev;
wpNew.showCmd = SW_SHOWNORMAL;
wpNew.rcNormalPosition = rectDesktop;
//生成新的工具条
m_wndFullScreenBar=new CToolBar;
if(!m_wndFullScreenBar->Create(this, CBRS_SIZE_DYNAMIC|CBRS_FLOATING)
|| !m_wndFullScreenBar->LoadToolBar(IDR_FULLSCREEN))
{
TRACE0("Failed to create toolbar\n");
return; // fail to create
}
}
posted @
2006-07-31 02:51 Jerry Cat 阅读(313) |
评论 (0) |
编辑 收藏
MFC socket程序开发
Socket编程在大多数的编程语言中都是一件比较有趣的事情。它是比较常用的编写通过网络通信的服务器和客户端方法。在windows平台Socket通信大多是基于MS Winsock设计的。Windows支持基于TCP和UDP的socket通信。Windows APIs在socket编程中是非常有用的,但是有些人发现在用它们工作的时候有困难。
所以在这里我介绍一种最简单用MFC socket类进行socket编程的方法。这不仅可以使你的工作变得简单而且能减少你在网络程序上的开发时间。你可以定制一个socket类,然后你可以在你的其他的网络应用程序中重用。
在socket编程中,MFC提供了两个基本的类,分别是CAsyncSocket和Csocket。Csocket是从CAsyncSocket继承来的。我们可以建立定制的socket类,也是从CasyncSocket继承而来的,当然也是为了我们程序特殊的需要。
初始化socket
首先需要调用AfxSocketInit()函数来初始化我们的socket环境。
为了初始化sockets,我们需要调用AfxSocketInit()函数。它通常是在MFC中的InitInstance()函数中被调用的。如果我们用程序向导来创建socket程序的话,查看“use Windows Sockets”这个选项,然后选中它。它将会自动的为我们创建这个步骤了。(如果我们没有选中这个选项的话,我们也可以手动添加这些代码的。)这个函数的返回值显示这个函数的调用成功或失败。
BOOL CServerApp::InitInstance()
{....
if( AfxSocketInit() == FALSE)
{
AfxMessageBox("Sockets Could Not Be Initialized");
return FALSE;
}
...
}
创建Server Sockets
为了创建一个Server Socket,我们需要声明一个CAyncSocket的变量或者我们自己定制的一个从AyncSocket或是Cscket继承来的类的类型的变量。然后调用Create()函数,同时指定监听的端口。这个函数的返回值显示这个函数的调用成功或失败。
UpdateData(TRUE);
m_sListener.Create(m_port);
if(m_sListener.Listen()==FALSE)
{
AfxMessageBox("Unable to Listen on that port,please try another port");
m_sListener.Close();
return;
}
创建Client Sockets
为了创建Client socket类,我们需要声明一个CAyncSocket的变量或者我们自己定制的一个从AyncSocket或是Cscket继承来的类的类型的变量。然后调用Create()函数,同时指定监听的端口。这个函数的返回值显示这个函数的调用成功或失败。
m_sConnected.Create();
m_sConnected.Connect("server ip",port);
监听客户端的连接
创建了server socket以后,我们要进行监听。调用Listen()函数。这个函数的返回值显示这个函数的调用成功或失败。
if( m_sListener.Listen()== FALSE)
{
AfxMessageBox("Unable to Listen on that port,please try another port");
m_sListener.Close();
return;
}
接受连接
连接请求要被接受accept,是用另外的socket,不是正在监听的socket。请参看代码。
void CXXXDlg::OnAccept()
{
CString strIP;
UINT port;
if(m_sListener.Accept(m_sConnected))
{
m_sConnected.GetSockName(strIP,port); //应该是GetPeerName,获取对方的IP和port
m_status="Client Connected,IP :"+ strIP;
m_sConnected.Send("Connected To Server",strlen("Connected To Server"));
UpdateData(FALSE);
}
else
{
AfxMessageBox("Cannoot Accept Connection");
}
}
发送数据
数据放在一个buffer中或是结构体中,调用send()函数发送。
m_sConnected.Send(pBuf,iLen);
接受数据
调用receive()接受数据。
void CXXXrDlg::OnReceive()
{
char *pBuf =new char [1025];
CString strData;
int iLen;
iLen=m_sConnected.Receive(pBuf,1024);
if(iLen == SOCKET_ERROR)
{
AfxMessageBox("Could not Recieve");
}
else
{
pBuf[iLen]=NULL;
strData=pBuf;
m_recieveddata.Insert(m_recieveddata.GetLength(),strData);
//display in server
UpdateData(FALSE);
m_sConnected.Send(pBuf,iLen); //send the data back to the Client
delete pBuf;
}
}
关闭连接
m_sConnected.ShutDown(0); 停止发送数据
m_sConnected.ShutDown(1); 停止接受数据
m_sConnected.ShutDown(2); 停止发送接受数据
m_sConnected.Close();
编写自己的socket类
在class view中选择添加一个新类,设置它的基类为CAsyncSocket,在类向导的帮助下添加如下的一些函数。
class MySocket : public CAsyncSocket
{ // Attributes
public:
// Operations
public:
MySocket();
virtual ~MySocket();
// Overrides
public:
void SetParentDlg(CDialog *pDlg);// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(MySocket)
public:
virtual void OnAccept(int nErrorCode);
virtual void OnClose(int nErrorCode);
virtual void OnConnect(int nErrorCode);
virtual void OnOutOfBandData(int nErrorCode);
virtual void OnReceive(int nErrorCode);
virtual void OnSend(int nErrorCode);
//}}AFX_VIRTUAL // Generated message map functions
//{{AFX_MSG(MySocket)
// NOTE - the ClassWizard will add and remove member functions here. //}}AFX_MSG
protected:
private:
CDialog * m_pDlg;
};
设置“Parent Dialog”
调用这个socket类的SetParentDlg函数,保证当socket事件发生的时候这个窗体能接收到。
m_sListener.SetParentDlg(this);
m_sConnected.SetParentDlg(this);
建立Socket 事件和窗体成员函数之间的联系
在这个窗体类中添加一些函数,比如void OnReceive(); void OnClose(); void OnAccept(); void OnConnect()等,它们会在我们编写的的socket类中调用到。
void MySocket::OnAccept(int nErrorCode)
{
// TODO: Add your specialized code here and/or call the base class
if(nErrorCode==0)
{
((CServerDlg*)m_pDlg)->OnAccept();
}
CAsyncSocket::OnAccept(nErrorCode);
}
这里只写了一个OnAccept()函数,其他的几个中也有类似的调用。详细的请参考代码。
posted @
2006-07-28 01:07 Jerry Cat 阅读(728) |
评论 (0) |
编辑 收藏
构造函数、析构函数与赋值函数是每个类最基本的函数。每个类只有一个析构函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)和多个赋值函数(除了同类的赋值以外,还有其他的赋值方法)。对于任意一个类A,如果不想编写上述函数,C++编译器将自动为A产生四个缺省的函数,如
A(void); // 缺省的无参数构造函数
A(const A &a); // 缺省的拷贝构造函数
~A(void); // 缺省的析构函数
A & operate =(const A &a); // 缺省的赋值函数
有几个需要注意的内容:
@ 构造函数与析构函数的另一个特别之处是没有返回值类型
@ 构造从类层次的最顶层的基类开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,在析构的时候,最低层的派生类的析构函数最开始被调用,然后调用每个基类的析构函数。
@ “缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错
下面通过例子进一步说明,
1.构造函数的初始化表
设存在两个类:
class
A
{
…
A(
void
);
//
无参数构造函数
A(
const
A
&
other);
//
拷贝构造函数
A
&
operate
=
(
const
A
&
other);
//
赋值函数
virtual
~
A(
void
);
//
析构函数
};
class
B
{
public
:
B(
const
A
&
a);
//
B的构造函数
private
:
A m_a;
//
成员对象
};
下面面是B的构造函数的2个实现,其中第一个的类B的构造函数在其初始化表里调用了类A的拷贝构造函数,从而将成员对象m_a初始化;而第二个的B的构造函数在函数体内用赋值的方式将成员对象m_a初始化。我们看到的只是一条赋值语句,但实际上B的构造函数干了两件事:先暗地里创建m_a对象(调用了A的无参数构造函数),再调用类A的赋值函数,将参数a赋给m_a。
B::B(
const
A
&
a)
: m_a(a)
{
…
}
B::B(
const
A
&
a)
{
m_a
=
a;
…
}
2.拷贝函数和构造函数的区别
拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。
String a(“hello”);
String b(“world”);
String c = a; // 调用了拷贝构造函数,最好写成 c(a);
c = b; // 调用了赋值函数
本例中第三个语句的风格较差,宜改写成String c(a) 以区别于第四个语句。
如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,可以将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。
3.析构函数与虚析构函数
基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:
@ 派生类的构造函数应在其初始化表里调用基类的构造函数
@ 基类与派生类的析构函数应该为虚(即加virtual关键字)
#include
<
iostream
>
class
Base
{
public
:
virtual
~
Base() { cout
<<
"
~Base
"
<<
endl ; }
};
class
Derived :
public
Base
{
public
:
virtual
~
Derived() { cout
<<
"
~Derived
"
<<
endl ; }
};
void
main(
void
)
{
Base
*
pB
=
new
Derived;
//
upcast
delete pB;
}
输出结果为:
~Derived
~Base
如果析构函数不为虚,那么输出结果为
~Base
posted @
2006-07-28 00:50 Jerry Cat 阅读(201) |
评论 (0) |
编辑 收藏
什么是对象的句柄?它是指针吗?它是引用吗?它是指向指针的指针?它是什么?
句柄术语一般用来指获取另一个对象的方法——一个广义的假指针。这个术语是(故意的)含糊不清的。
含糊不清在实际中的某些情况下是有用的。例如,在早期设计时,你可能不准备用句柄来表示。你可能不确定是否将一个简单的指针或者引用或者指向指针的指针或者指向引用的指针或者整型标识符放在一个数组或者字符串(或其它键)以便能够以哈希表(hash-table)(或其他数据结构)或数据库键或者一些其它的技巧来查询。如果你只知道你会需要一些唯一标识的东西来获取对象,那么这些东西就被称为句柄。
因此,如果你的最终目标是要让代码唯一的标识/查询一个Fred类的指定的对象的话,你需要传递一个Fred句柄这些代码。句柄可以是一个能被作为众所周知的查询表中的键(key)来使用的字符串(比如,在std::map<std::string,Fred> 或 std::map<std::string,Fred*>中的键),或者它可以是一个作为数组中的索引的整数(比如,Fred* array = new Fred[maxNumFreds]),或者它可以是一个简单的 Fred*,或者它可以是其它的一些东西。
初学者常常考虑指针,但实际上使用未初始化的指针有底层的风险。例如,如果Fred对象需要移动怎么办?当Fred对象可以被安全删除时我们如何获知?如果Fred对象需要(临时的)连续的从磁盘获得怎么办?等等。这些时候的大多数,我们增加一个间接层来管理位置。例如,句柄可以是Fred**,指向Fred*的指针可以保证不会被移动。当Fred对象需要移动时,你只要更新指向Fred*的指针就可以了。或者让用一个整数作为句柄,然后在表或数组或其他地方查询Fred的对象(或者指向Fred对象的指针)。
重点是当我们不知道要做的事情的细节时,使用句柄。
使用句柄的另一个时机是想要将已经完成的东西含糊化的时候(有时用术语magic cookie也一样,就像这样,“软件传递一个magic cookie来唯一标识并定位适当的Fred对象”)。将已经完成的东西含糊化的原因是使得句柄的特殊细节或表示物改变时所产生的连锁反应最小化。举例来说,当将一个句柄从用来在表中查询的字符串变为在数组中查询的整数时,我们可不想更新大量的代码。
当句柄的细节或表示物改变时,维护工作更为简单(或者说阅读和书写代码更容易),因此常常将句柄封装到类中。这样的类常重载operator-> 和 operator*算符(既然句柄的效果象指针,那么它可能看起来也象指针)。
posted @
2006-07-28 00:48 Jerry Cat 阅读(474) |
评论 (0) |
编辑 收藏
[转]重载函数和重载运算符
重载函数有如下约束
@ 该组重载函数中任何两个都必须有不同的参量表。
@ 具有相同类型参量表、仅在返回值类型上不同的重载函数会引起错误。
@ 成员函数的重载不能仅基于一个说明为静态的,另一个说明为非静态的。
@ typedef说明并未定义新的类型,它们仅为已存在的类型引入了一个同义词。它们不能影响重载机制。
@ 枚举类型是一些可区分的类型,故可以区分重载函数。
@ 从区分重载函数的意义上说,类型“数组”和“指针”是相同的。对于一维数组来说是正确的。
运算符重载有如下的约束
@ 运算符要遵守它们同内部类型一起使用所指定的优先原则、分组及操作数的个数。
@ 单目运算符说明为成员函数不带参量;如果说明为全局函数,要带一个参量。双目运算符说明为成员函数只带一个参量;如果说明为全局函数,要带两个参量。
@ 所有的重载运算符除了赋值(operator=)外均可被派生类继承。
@ 重载运算符的成员函数的第一个参量总是激活该运算符的对象的类类型参量(运算符被定义的类,或者定义了运算符的类的派生类)。对于第一个参量也不支持转换。
具体内容:
单目运算符函数
ret-type operator op() //成员,使用类型的内部成员
ret-type operator op(arg) //全局,参数为对其操作的类型的变量
双目运算符函数
ret-type oprator op(arg) //arg可以为任意类型的变量
ret-type operator op(arg1, arg2)
//全局,arg1和arg2是参量。至少其中之一必须是操作类类型。
注意:对于双目运算符的返回类型没有限制;然而大多数用户自定义型双目运算符返回类类型或类类型的引用。
参考:
C++运算符重载转换运算符
C++运算符重载赋值运算符
posted @
2006-07-28 00:43 Jerry Cat 阅读(372) |
评论 (0) |
编辑 收藏
[转]智能指针与微妙的隐式转换
C++
虽然是强类型语言,但是却还不如
Java
、
C#
那么足够的强类型,原因是允许的隐式转换太多
-
从
C
语言继承下来的基本类型之间的隐式转换
-
T*
指针到
void*
的隐式转换
-
non-explicit constructor
接受一个参数的隐式转换
-
从子类到基类的隐式转换
(
安全)
-
从
const
到
non-const
的同类型的隐式转换
(
安全
)
除开上面的五种隐式转换外,
C++
的编译器还非常聪明,当没法直接隐式转换的时候,它会尝试间接的方式隐式转换,这使得有时候的隐式转换非常的微妙,一个误用会被编译器接受而会出现意想不到的结果。例如假设类
A
有一个
non-explicit constructor
,唯一的参数是类
B
,而类
B
也有一个
non-explicit constructor
接受类型
C
,那么当试图用类型
C
的实例初始化类
A
的时候,编译器发现没有直接从类型
C
构造的过程,但是呢,由于类
B
可以被接受,而类型
C
又可以向类型
B
隐式转换,因此从
C->B->A
的路就通畅了。这样的隐式转换多数时候没什么大碍,但是不是我们想要的,因为它可能造成一些微妙的
bug
而难以捕捉。
为了在培训的时候展示栈上析构函数的特点和自动资源管理,准备下面的一个例子,结果测试的时候由于误用而发现一些问题。
(
测试的
IDE
是
Visual Studio 2005)
class A
{
public:
A(){ a = 100; }
int a;
void f();
};
A * pa = new A();
std::auto_ptr<A> p = pa; //
无意这样使用的,本意是
std::auto_ptr<A> p(pa)
p->f();
这个写法是拷贝构造函数的形式,显然从
T*
是不能直接拷贝构造的
auto_ptr
的,但是编译器会尝试其他的路径来转换成
auto_ptr
来拷贝构造,因此如果存在一个中间的
,这个类能接受从
T*
的构造,而
同时auto_ptr也能接受从类X
的构造,那编译器就会很高兴的生成这样的代码。
这段代码在
VC6
上是通不过的,因为
VC6
的
auto_ptr
实现就只有一个接受
T*
指针的
explicit constructor
.
但是
C++ Standard
的修正规范中,要求
auto_ptr
还应该有个接受
auto_ptr_ref
的
constructor
。那么这个
auto_ptr_ref
是什么呢?按照
C++ Standard
的解释
:
Template auto_ptr_ref holds a reference to an auto_ptr. It is used by the auto_ptr
conversions to allow auto_ptr
objects to be passed to and returned from functions.
有兴趣可以参考
Scott Meyers
的
" auto_ptr update page " (
http://www.awprofessional.com/content/images/020163371X/autoptrupdate%5Cauto_ptr_update.html
)讲诉auto_ptr的历史.
再回到前面的代码,本来应该是通不过的编译,但是
VC2005
的编译器却没有任何怨言的通过
(
即使把警告等级设置到
4)
。结果运行的时候却崩溃了,出错在
auto_ptr
的析构函数
,delete
的指针所指向地址是
100
,而如果在
p->f()
后面加上一句
cout << pa->a << endl;
发现输出结果为
0
。
为什么会这样,原因就是前面所诉的间接的隐式转换,这与
VC 2006
的
auto_ptr
和
auto_ptr_ref
实现有关,看看
P.J.Plauger
是怎么实现的
:
// auto_ptr_ref
template<class _Ty>
struct auto_ptr_ref
{
// proxy reference for auto_ptr copying
auto_ptr_ref(void *_Right)
: _Ref(_Right)
{ // construct from generic pointer to auto_ptr ptr
}
void *_Ref;// generic pointer to auto_ptr ptr
};
// construct auto_ptr from an auto_ptr_ref object
auto_ptr(auto_ptr_ref<_Ty> _Right) _THROW0()
{
// construct by assuming pointer from _Right auto_ptr_ref
_Ty **_Pptr = (_Ty **)_Right._Ref;
_Ty *_Ptr = *_Pptr;
*_Pptr = 0;
// release old
_Myptr = _Ptr;
// reset this
}
这样代码通过编译的原因也就清楚了,
A* -> void * -> auto_ptr_ref -> auto_ptr -> copy constructor -> accept.
好长的隐式转换链
, -_-, C++
编译器太聪明了。
那么为什么最后会出现指针被破坏的结果呢,原因在
auto_ptr
的实现,因为按照
C++ Standard
要求,
auto_ptr_ref
应该是包含一个
auto_ptr
的引用,因此
auto_ptr
的构造函数也就假设了
auto_ptr_ref
的成员
_Ref
是一个指向
auto_ptr
的指针。
而
auto_ptr
中只有一个成员就是
A*
的指针,因此指向
auto_ptr
对象的指针相当于就是个
A**
指针,因此上面
auto_ptr
从
auto_ptr_ref
构造的代码是合理的。
但是由于罪恶的
void*
造成了一条非常宽敞的隐式转换的道路,
A*
指针也能够被接受,因此把
A*
当作
A**
来使用,结果可想而知,
A*
指向地址的前
4
个字节
(
因为
32
位
OS)
被拷贝出来,而这四个字节被赋值为
0( *_Pptr=0 )
。
所以出现了最后的结果是
_Myptr
值为
100
,而
pa->a
为
0
。
如果要正确执行结果,只要保证是个
A**
指针就行了,有两个方法
第一,
auto_ptr_ref
所包含的引用是指向的
auto_ptr
对象
A * p = new A();
std::auto_ptr<A> pt( new A() );
std::auto_ptr_ref<A> ra( pt );
std::auto_ptr<A> pb
=
ra
;
pb->f();
第二,直接用二级指针
A * p = new A();
std::auto_ptr<A> pb = &p; //
这句话后
, p
将等于
0
pb->f();
当然第二种是利用了
VC2005
的实现而造出来的,看着很别扭
,:)
。
我不明白
P.J.Plauger
为什么用
void *
,而不是用
auto_ptr<T>&
,因为任何指针都能隐式转换为
void *
,这样的危险性大多了。并且如果用了
auto_ptr<T>&
,从
auto_ptr_ref
构造也容易写得更简单清楚,看看以前的实现方式吧,仍然是
P.J.Plauger
的,但是版本低了点:
template<class _Ty>
struct auto_ptr_ref
{
// proxy reference for auto_ptr copying
auto_ptr_ref(auto_ptr<_Ty>& _Right)
: _Ref(_Right)
{
// construct from compatible auto_ptr
}
auto_ptr<_Ty>& _Ref;
// reference to constructor argument
};
auto_ptr(auto_ptr_ref<_Ty> _Right) _THROW0()
: _Myptr(_Right._Ref.release())
{
// construct by assuming pointer from _Right auto_ptr_ref
}
这样的实现方法,显然不能接受任何指针的隐式转换,也就防止一开始的那种错误写法,并且也是符合
C++ Standard
的要求的。
而
SGI STL
的
auto_ptr_ref
的实现则是包含了一个
T*
的指针,构造
auto_ptr
时候直接从
auto_ptr_ref
中拷贝这个指针,因此这样的实现可以上代码编译通过,运行也正确,不过不符合
C++ Standard
。
总结一下,危险的潜伏bug的隐式转换应该被杜绝的,特别是
void *
的隐式转换和构造函数的隐式转换,因此建议是
:
-
慎用
void *
,因为
void *
必须要求你知道转换前的实现,因此更适合用在底层的、性能相关的内部实现。
-
单一参数的构造函数应该注意是否允许隐式转换,如果不需要,加上
explicit
。例如
STL
容器中
vector
接受从
int
的构造函数,用于预先申请空间,这样的构造函数显然不需要隐式转换,因此加上了
explicit
。
-
重载函数中,如果可能,就用更有明确意义的名字替代重载,因为隐式转换也许会带来一些意想不到的麻烦。
-
避免隐式转换不等于是多用显示转换。
Meyers
在
Effective C++
中提到,即使
C++
风格的显示转换也应该尽量少用,最好是改进设计。
posted @
2006-07-25 00:37 Jerry Cat 阅读(756) |
评论 (0) |
编辑 收藏
[转]Linux下使用和生成库
基本概念
库有动态与静态两种,动态通常用.so为后缀,静态用.a为后缀。例如:libhello.so libhello.a
为了在同一系统中使用不同版本的库,可以在库文件名后加上版本号为后缀,例如: libhello.so.1.0,由于程序连接默认以.so为文件后缀名。所以为了使用这些库,通常使用建立符号连接的方式。
ln -s libhello.so.1.0 libhello.so.1
ln -s libhello.so.1 libhello.so
使用库
当要使用静态的程序库时,连接器会找出程序所需的函数,然后将它们拷贝到执行文件,由于这种拷贝是完整的,所以一旦连接成功,静态程序库也就不再需要了。然而,对动态库而言,就不是这样。动态库会在执行程序内留下一个标记‘指明当程序执行时,首先必须载入这个库。由于动态库节省空间,linux下进行连接的缺省操作是首先连接动态库,也就是说,如果同时存在静态和动态库,不特别指定的话,将与动态库相连接。
现在假设有一个叫hello的程序开发包,它提供一个静态库libhello.a 一个动态库libhello.so,一个头文件hello.h,头文件中提供sayhello()这个函数
/* hello.h */
void sayhello();
另外还有一些说明文档。这一个典型的程序开发包结构
1.与动态库连接
linux默认的就是与动态库连接,下面这段程序testlib.c使用hello库中的sayhello()函数
/*testlib.c*/
#include <hello.h>
#include <stdio.h>
int main()
{
sayhello();
return 0;
}
使用如下命令进行编译
$gcc -c testlib.c -o testlib.o
用如下命令连接:
$gcc testlib.o -lhello -o testlib
在连接时要注意,假设libhello.o 和libhello.a都在缺省的库搜索路径下/usr/lib下,如果在其它位置要加上-L参数
与与静态库连接麻烦一些,主要是参数问题。还是上面的例子:
$gcc testlib.o -o testlib -WI,-Bstatic -lhello
注:这个特别的"-WI,-Bstatic"参数,实际上是传给了连接器ld.
指示它与静态库连接,如果系统中只有静态库当然就不需要这个参数了。
如果要和多个库相连接,而每个库的连接方式不一样,比如上面的程序既要和libhello进行静态连接,又要和libbye进行动态连接,其命令应为:
$gcc testlib.o -o testlib -WI,-Bstatic -lhello -WI,-Bdynamic -lbye
3.动态库的路径问题
为了让执行程序顺利找到动态库,有三种方法:
(1)把库拷贝到/usr/lib和/lib目录下。
(2)在LD_LIBRARY_PATH环境变量中加上库所在路径。例如动态库libhello.so在/home/ting/lib目录下,以bash为例,使用命令:
$export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/ting/lib
(3) 修改/etc/ld.so.conf文件,把库所在的路径加到文件末尾,并执行ldconfig刷新。这样,加入的目录下的所有库文件都可见、
4.查看库中的符号
有时候可能需要查看一个库中到底有哪些函数,nm命令可以打印出库中的涉及到的所有符号。库既可以是静态的也可以是动态的。nm列出的符号有很多,常见的有三种,一种是在库中被调用,但并没有在库中定义(表明需要其他库支持),用U表示;一种是库中定义的函数,用T表示,这是最常见的;另外一种是所谓的“弱态”符号,它们虽然在库中被定义,但是可能被其他库中的同名符号覆盖,用W表示。例如,假设开发者希望知道上央提到的hello库中是否定义了printf():
$nm libhello.so |grep printf
U printf
U表示符号printf被引用,但是并没有在函数内定义,由此可以推断,要正常使用hello库,必须有其它库支持,再使用ldd命令查看hello依赖于哪些库:
$ldd hello
libc.so.6=>/lib/libc.so.6(0x400la000)
/lib/ld-linux.so.2=>/lib/ld-linux.so.2 (0x40000000)
从上面的结果可以继续查看printf最终在哪里被定义,有兴趣可以go on
生成库
第一步要把源代码编绎成目标代码。以下面的代码为例,生成上面用到的hello库:
/* hello.c */
#include <stdio.h>
void sayhello()
{
printf("hello,world\n");
}
用gcc编绎该文件,在编绎时可以使用任何全法的编绎参数,例如-g加入调试代码等:
gcc -c hello.c -o hello.o
1.连接成静态库
连接成静态库使用ar命令,其实ar是archive的意思
$ar cqs libhello.a hello.o
2.连接成动态库
生成动态库用gcc来完成,由于可能存在多个版本,因此通常指定版本号:
$gcc -shared -Wl,-soname,libhello.so.1 -o libhello.so.1.0 hello.o
另外再建立两个符号连接:
$ln -s libhello.so.1.0 libhello.so.1
$ln -s libhello.so.1 libhello.so
这样一个libhello的动态连接库就生成了。最重要的是传gcc -shared 参数使其生成是动态库而不是普通执行程序。
-Wl 表示后面的参数也就是-soname,libhello.so.1直接传给连接器ld进行处理。实际上,每一个库都有一个soname,当连接器发现它正在查找的程序库中有这样一个名称,连接器便会将soname嵌入连结中的二进制文件内,而不是它正在运行的实际文件名,在程序执行期间,程序会查找拥有soname名字的文件,而不是库的文件名,换句话说,soname是库的区分标志。
这样做的目的主要是允许系统中多个版本的库文件共存,习惯上在命名库文件的时候通常与soname相同
libxxxx.so.major.minor
其中,xxxx是库的名字,major是主版本号,minor 是次版本号
posted @
2006-07-25 00:33 Jerry Cat 阅读(511) |
评论 (0) |
编辑 收藏
[转]如何让你的程序安全通过windows防火墙
http://www.cppblog.com/davyy/archive/2006/07/24/10410.html
大家开发网络程序,经常要连接其他主机,如果在xp上运行,一定会提示你,只有选择解除阻止才能
实现正常的网络连接.那么有没有办法在防火墙的例外列表里面通过编程的方式加入自己的程序呢?
当然有了,不然就不要介绍了:)
xp的系统目录下面有个hnetcfg.dll就是这个编程接口,头文件是netfw.h,初始化代码如下:
INetFwProfile* m_pFireWallProfile=NULL;
HRESULT hr = S_FALSE;
INetFwMgr * fwMgr = NULL;
INetFwPolicy * fwPolicy = NULL;
FW_ERROR_CODE ret = FW_NOERROR;
try
{
if ( m_pFireWallProfile )
throw FW_ERR_INITIALIZED;
// Create an instance of the firewall settings manager.
hr = CoCreateInstance( __uuidof(NetFwMgr), NULL, CLSCTX_INPROC_SERVER, __uuidof( INetFwMgr), ( void ** ) & fwMgr );
if ( FAILED( hr ))
throw FW_ERR_CREATE_SETTING_MANAGER;
// Retrieve the local firewall policy.
hr = fwMgr -> get_LocalPolicy( & fwPolicy );
if ( FAILED( hr ))
throw FW_ERR_LOCAL_POLICY;
// Retrieve the firewall profile currently in effect
hr = fwPolicy -> get_CurrentProfile( & m_pFireWallProfile );
if ( FAILED( hr ))
throw FW_ERR_PROFILE;
}
catch ( FW_ERROR_CODE nError)
{
ret = nError;
}
if ( fwPolicy )
fwPolicy -> Release();
if ( fwMgr )
fwMgr -> Release();
return ret; 将程序名称加入例外列表:
WinXPSP2FireWall::AddApplication( const wchar_t* lpszProcessImageFileName, const wchar_t* lpszRegisterName )
{
FW_ERROR_CODE ret = FW_NOERROR;
HRESULT hr;
BOOL bAppEnable;
BSTR bstrProcessImageFileName = NULL;
BSTR bstrRegisterName = NULL;
INetFwAuthorizedApplication* pFWApp = NULL;
INetFwAuthorizedApplications* pFWApps = NULL;
try
{
if( m_pFireWallProfile == NULL )
throw FW_ERR_INITIALIZED;
if( lpszProcessImageFileName == NULL || lpszRegisterName == NULL )
throw FW_ERR_INVALID_ARG;
// First of all, check the application is already authorized;
FW_ERROR_CODE nError = this->IsAppEnabled( lpszProcessImageFileName, bAppEnable );
if( nError != FW_NOERROR )
throw nError;
// Only add the application if it isn't authorized
if( bAppEnable == FALSE )
{
// Retrieve the authorized application collection
hr = m_pFireWallProfile->get_AuthorizedApplications( &pFWApps );
if( FAILED( hr ))
throw FW_ERR_AUTH_APPLICATIONS;
// Create an instance of an authorized application
hr = CoCreateInstance( __uuidof(NetFwAuthorizedApplication), NULL, CLSCTX_INPROC_SERVER, __uuidof(INetFwAuthorizedApplication), (void**)&pFWApp);
if( FAILED( hr ))
throw FW_ERR_CREATE_APP_INSTANCE;
// Allocate a BSTR for the Process Image FileName
bstrProcessImageFileName = SysAllocString( lpszProcessImageFileName );
if( SysStringLen( bstrProcessImageFileName ) == 0)
throw FW_ERR_SYS_ALLOC_STRING;
// Set the process image file name
hr = pFWApp->put_ProcessImageFileName( bstrProcessImageFileName );
if( FAILED( hr ) )
throw FW_ERR_PUT_PROCESS_IMAGE_NAME;
// Allocate a BSTR for register name
bstrRegisterName = SysAllocString( lpszRegisterName );
if( SysStringLen( bstrRegisterName ) == 0)
throw FW_ERR_SYS_ALLOC_STRING;
// Set a registered name of the process
hr = pFWApp->put_Name( bstrRegisterName );
if( FAILED( hr ))
throw FW_ERR_PUT_REGISTER_NAME;
// Add the application to the collection
hr = pFWApps->Add( pFWApp );
if( FAILED( hr ))
throw FW_ERR_ADD_TO_COLLECTION;
}
}
catch( FW_ERROR_CODE nError )
{
ret = nError;
}
SysFreeString( bstrProcessImageFileName );
SysFreeString( bstrRegisterName );
if( pFWApp )
pFWApp->Release();
if( pFWApps )
pFWApps->Release();
return ret;
}
posted @
2006-07-25 00:22 Jerry Cat 阅读(478) |
评论 (0) |
编辑 收藏
/********************************************\
| 欢迎转载, 但请保留作者姓名和原文链接, 祝您进步并共勉! |
\********************************************/
读VC++内幕之体悟 - 09
作者: Jerry Cat
时间: 2006/07/24
链接:
http://www.cppblog.com/jerysun0818/archive/2006/07/24/10428.html
9. 所有的ActiveX控件属性,包括设计时的属性,在运行时都是可以访问的。
posted @
2006-07-24 23:58 Jerry Cat 阅读(532) |
评论 (0) |
编辑 收藏