VC++视频勘误和说明
一、在视频Lesson2中,在介绍构造函数时,我说:“构造函数最重要的作用是创建对象本身,对象内存的分配由构造函数来完成的”,这句话是错的,对象内存的分配和构造函数没有关系,对象内存的分配是由编译器来完成的,构造函数的作用是对对象本身做初始化工作,也就是给用户提供初始化类中成员变量的一种方式,在类对象有虚表的情况下,构造函数还对虚表进行初始化。
另外,我说:“C++又规定,如果一个类没有提供任何的构造函数,则C++提供一个默认的构造函数(由C++编译器提供)”,这句话也是错误的,正确的是:
如果一个类中没有定义任何的构造函数,那么编译器只有在以下三种情况,才会提供默认的构造函数:
1、如果类有虚拟成员函数或者虚拟继承父类(即有虚拟基类)时;
2、如果类的基类有构造函数(可以是用户定义的构造函数,或编译器提供的默认构造函数);
3、在类中的所有非静态的对象数据成员,它们对应的类中有构造函数(可以是用户定义的构造函数,或编译器提供的默认构造函数)。
二、在视频 Lesson4 的Code 中,画扇形用如下代码即可:
if(m_bDraw == TRUE) { dc.MoveTo(m_ptOrigin); dc.LineTo(point); }
带边线的扇形用如下代码即可:if(m_bDraw == TRUE) { dc.MoveTo(m_ptOrigin); dc.LineTo(point); dc.LineTo(m_ptOld); m_ptOld = point; }
三、在视频Lesson8中,关于在对话框上放置组合框的问题,我说“如果拖动的矩形较小,组合框的列表框部分将无法显示,此时也无法调整组合框的上下位置的大小了”。实际上,组合框的上下位置还是可以调整的,调整的办法如下:
在对话框资源处于编辑状态时,将鼠标移动到组合框控件右边向下的箭头上,当鼠标变成上下箭头形状时,单击鼠标左键,此时可以看到举行框围绕着组合框。将鼠标移动到该矩形框下端的蓝色小方块上,当鼠标变成上下箭头形状时,按住鼠标左键向下拖动,直到把组合框的下拉列表框范围拖动到合适的大小时松开鼠标左键。
四、在视频Lesson16的事件代码中,有一个问题,修改如下:
void main() { HANDLE hThread1; HANDLE hThread2;
g_hEvent=CreateEvent(NULL,FALSE,FALSE,NULL); //将CreateEvent()函数放置在这个位置
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL); hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL); CloseHandle(hThread1); CloseHandle(hThread2);
//g_hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);//取消这个位置的CreateEvent()函数。
/*g_hEvent=CreateEvent(NULL,FALSE,FALSE,"tickets"); if(g_hEvent) { if(ERROR_ALREADY_EXISTS==GetLastError()) { cout<<"only instance can run!"<<endl; return; } }*/ SetEvent(g_hEvent);
Sleep(4000); CloseHandle(g_hEvent); }
原因:如果在线程产生之后调用CreateEvent(),假如线程提前被操作系统调度,那么线程里的 WaitForSingleObject 等待的将是一个空的g_hEvent, 在这种情况下,WaitForSingleObject 将返回WAIT_FAILED 。这个问题可以通过在CreateEvent()函数前添加Sleep(10)的调用来查看。
五、在视频Lesson16的采用事件的多线程同步代码中,有一个问题:在线程1和线程2的代码中,有下面一段:
while(TRUE) { WaitForSingleObject(g_hEvent,INFINITE); if(tickets>0) { Sleep(1); cout<<"thread1 sell ticket : "<<tickets--<<endl; } else break; SetEvent(g_hEvent); }
这个代码有一个问题,当线程1或线程2卖完最后一张票时,调用SetEvent(g_hEvent);将事件对象设置为有信号状态,另一个线程等待到事件对象,开始执行代码,判断tickets不大于0,于是执行else语句下的break,退出循环,此时SetEvent(g_hEvent);就没有被执行,导致线程1或线程2一直等待,直到主线程终止运行,整个程序才退出。应将SetEvent(g_hEvent);在 if 和 else 中分别调用,修改如下:
DWORD WINAPI Fun1Proc( LPVOID lpParameter // thread data ) { while(TRUE) { WaitForSingleObject(g_hEvent,INFINITE); // ResetEvent(g_hEvent); if(tickets>0) { Sleep(1); cout<<"thread1 sell ticket : "<<tickets--<<endl; SetEvent(g_hEvent); } else { SetEvent(g_hEvent); break; } } return 0; }
DWORD WINAPI Fun2Proc( LPVOID lpParameter // thread data ) { while(TRUE) { WaitForSingleObject(g_hEvent,INFINITE); // ResetEvent(g_hEvent); if(tickets>0) { Sleep(1); cout<<"thread2 sell ticket : "<<tickets--<<endl; SetEvent(g_hEvent); } else { SetEvent(g_hEvent); break; } } return 0; }
六、在视频Lesson16的采用关键代码段的多线程同步代码中,有一个问题:在视频讲解过程中,在线程1和线程2的代码中,有下面一段:
while(TRUE) { EnterCriticalSection(&g_cs); Sleep(1); if(tickets>0) { Sleep(1); cout<<"thread1 sell ticket : "<<tickets--<<endl; } else break; LeaveCriticalSection(&g_cs); }
这个代码有一个问题,当线程1或线程2卖完最后一张票时,释放对临界区对象的所有权后,另外一个线程进入关键代码段,判断tickets不大于0,执行else语句下的break,退出循环,于是LeaveCriticalSection(&g_cs);就没有执行,导致线程1或线程2一直等待,直到主线程终止运行,整个程序才退出。应将LeaveCriticalSection(&g_cs);在 if 和 else 中分别调用,修改如下:
DWORD WINAPI Fun1Proc( LPVOID lpParameter // thread data ) { while(TRUE) { EnterCriticalSection(&g_cs); Sleep(1); if(tickets>0) { Sleep(1); cout<<"thread1 sell ticket : "<<tickets--<<endl; LeaveCriticalSection(&g_cs); } else { LeaveCriticalSection(&g_cs); break; } }
return 0;
}
DWORD WINAPI Fun2Proc( LPVOID lpParameter // thread data ) {
while(TRUE) { EnterCriticalSection(&g_cs); Sleep(1); if(tickets>0) { Sleep(1); cout<<"thread2 sell ticket : "<<tickets--<<endl; LeaveCriticalSection(&g_cs); } else { LeaveCriticalSection(&g_cs); break; } } return 0;
}
七、在视频Lesson16 的 Code 中,Chat 的函数代码, OnSock 函数忘记释放内存了,可以在出错判断的地方以及将要返回的地方加释放内存的语句 ( delete[] wsabuf.buf )。void CChatDlg::OnSock(WPARAM wParam,LPARAM lParam) { switch(LOWORD(lParam)) { case FD_READ: WSABUF wsabuf; wsabuf.buf=new char[200]; wsabuf.len=200; DWORD dwRead; DWORD dwFlag=0; SOCKADDR_IN addrFrom; int len=sizeof(SOCKADDR); CString str; CString strTemp; HOSTENT *pHost; if(SOCKET_ERROR==WSARecvFrom(m_socket,&wsabuf,1,&dwRead,&dwFlag, (SOCKADDR*)&addrFrom,&len,NULL,NULL)) { MessageBox("接收数据失败!"); delete[] wsabuf.buf; // 这里加一句释放内存的语句 return; } pHost=gethostbyaddr((char*)&addrFrom.sin_addr.S_un.S_addr,4,AF_INET); //str.Format("%s说 :%s",inet_ntoa(addrFrom.sin_addr),wsabuf.buf); str.Format("%s说 :%s",pHost->h_name,wsabuf.buf); str+="/r/n"; GetDlgItemText(IDC_EDIT_RECV,strTemp); str+=strTemp; SetDlgItemText(IDC_EDIT_RECV,str); delete[] wsabuf.buf; // 这里加一句释放内存的语句 break; } }
八、在视频Lesson17的剪贴板编程的代码中,有一个问题,修改如下:
if(OpenClipboard()) { if(IsClipboardFormatAvailable(CF_TEXT)) { HANDLE hClip; char *pBuf; hClip = GetClipboardData(CF_TEXT); pBuf = (char *)GlobalLock(hClip); GlobalUnlock(hClip); SetDlgItemText(IDC_EDIT_RECV, pBuf); //CloseClipboard();//去掉这一句。错误原因:如果程序没有进入第二个if语句, 那么剪贴板不会关闭。 } CloseClipboard();//在这里添加关闭剪贴板的操作。}
九、在视频Lesson18中,在OnIntervalChanged()函数中的代码逻辑上有一些问题,原先的代码如下:
void CClockCtrl::OnIntervalChanged() { // TOD Add notification handler code if(m_interval<0 || m_interval>6000) { m_interval=1000; } else { m_interval=m_interval/1000*1000; KillTimer(1); SetTimer(1,m_interval,NULL); BoundPropertyChanged(0x1); } SetModifiedFlag(); }
应该改为:
void CClockCtrl::OnIntervalChanged() { // TOD Add notification handler code if(m_interval<1000 || m_interval>6000) { m_interval=1000; } else { m_interval=m_interval/1000*1000; } KillTimer(1); SetTimer(1,m_interval,NULL); BoundPropertyChanged(0x1); SetModifiedFlag(); }
十、第18课ActiveX控件的问题
一个不知名的网友问我这样一个问题:
你的18课,用到的activex控件用debug版本生成的控件,在vc中插入这个控件,调整interval属性的时候,会出错。只要是debug版本在VC下就会出错。release版本不会?为什么?
出错操作步骤如下:
在ClockTest程序中的对话框资源上单击鼠标右键,选择【Insert ActiveX Control…】,在弹出的“Insert ActiveX Control”对话框中,找到“Clock Control”,点击“OK”按钮,在ClockTest对话框上插入“Clock”Activex控件。在控件上单击鼠标右键,选择【Properties】,然后切换到Control标签页,修改“Interval”的值,例如改为“2000”,关闭“Clock Control Properties”属性对话框,将看到如下图所示的错误信息:
我对这个问题的回答如下:
原因是因为当将 Clock控件放到VB 的Form上时,该控件的窗口已经创建,也就是说, CClockCtrl类的 OnCreate()方法被执行了,这样就设置了定时器。而在VC 的对话框上插入Clock 控件时,却没有调用CClockCtrl类的 OnCreate()方法, 当修改 Interval 属性时,会调用 CClockCtrl 类的 OnIntervalChanged() 方法,在这个方法中,调用了 KillTimer(1) ,因为定时器根本就没有创建,因此就出现了非法操作。这可能是因为对话框并不是真正的容器组件。解决办法,用一个变量保存定时器的返回值,然后在 OnIntervalChanged() 方法中对返回值进行判断。
十一、在视频Lesson2中,关于函数覆盖的讲解是不正确的,感谢网友guyanhun指出这个问题。我查过了C++标准,先更正如下:
1、函数的覆盖
在介绍多态性的时候,我们给出了下面的代码片段:
例1
class animal
{
public:
…
virtual void breathe()
{
cout<<"animal breathe"<<endl;
}
};
class fish:public animal
{
public:
void breathe()
{
cout<<"fish bubble"<<endl;
}
在基类animal的breathe函数前添加了virtual关键字,声明该函数为虚函数。在派生类fish中重写了breathe函数,我们注意到,fish类的breathe函数和animal类的breathe函数完全一样,无论是函数名,还是参数列表都是一样的,这称为函数的覆盖(override)。构成函数覆盖的条件为:
基类函数必须是虚函数(使用virtual关键字进行声明)。
发生覆盖的两个函数要分别位于派生类和基类中。
函数名称与参数列表必须完全相同
由于C++的多态性是通过虚函数来实现的,所以函数的覆盖总是和多态关联在一起。在函数覆盖的情况下,编译器会在运行时根据对象的实际类型来确定要调用的函数。
2、函数的隐藏
class animal
{
public:
…
void breathe()
{
cout<<"animal breathe"<<endl;
}
};
class fish:public animal
{
public:
void breathe()
{
cout<<"fish bubble"<<endl;
}
你看出来这段代码和例1所示代码的区别了吗?在这段代码中,派生类fish中的breathe函数和基类animal中的breathe函数也是完全一样的,不同的是breathe函数不是虚函数,这种情况称为函数的隐藏。所谓隐藏,是指派生类中具有与基类同名的函数(不考虑参数列表是否相同),从而在派生类中隐藏了基类的同名函数。
函数的隐藏与函数的覆盖、重载很容易混淆,我们看下面两种函数隐藏的情况:
(1)派生类的函数与基类的函数完全相同(函数名和参数列表都相同),只是基类的函数没有使用virtual关键字,此时基类的函数将被隐藏,而不是覆盖(请参照上文讲述的函数覆盖进行比较)。
(2)派生类的函数与基类的函数同名,但参数列表不同,在这种情况下,不管基类的函数声明是否有virtual关键字,基类的函数都将被隐藏。注意这种情况与函数重载的区别,重载是发生在同一个类中。
下面我们给出一个例子,以帮助大家更好地理解函数的覆盖和隐藏。代码如例3所示。
例3
class Base
{
public:
virtual void fn();
};
class Derived : public Base
{
public:
void fn(int);
};
class Derived2 : public Derived
{
public:
void fn();
在这个例子中,Derived类的fn(int)函数隐藏了Base类的fn()函数,Derived类fn(int)函数不是虚函数(注意和覆盖相区别)。 Derived2类的fn()函数隐藏了Derived类的fn(int)函数,由于Derived2类的fn()函数与Base类的fn()函数具有同样的函数名和参数列表,因此Derived2类的fn()函数是一个虚函数,覆盖了Base类的fn()函数。注意,在Derived2类中,Base类的fn()函数是不可见的,但这并影响fn函数的覆盖。
当隐藏发生时,如果在派生类的同名函数中想要调用基类的被隐藏函数,可以使用“基类名::函数名(参数)”的语法形式。例如,要在Derived类的fn(int)方法中调用Base类的fn()方法,可以使用Base::fn()语句。
|