VC++学习——1

目录

1、概念

2、消息和消息队列

3、WinMain函数

4、主函数 main WinMain _tmain _tWinMain 的区别

5、C++基础补充

1、概念

Windows程序设计:事件驱动的程序设计

用户在窗口进行一些操作时,操作系统感知到这一事件,然后将这一事件打包成消息,投递到应用程序的消息队列中,然后应用程序从消息队列中取出消息并进行响应。

窗口过程:当“发送消息”时,操作系统调用应用程序中一个专门负责处理消息的函数,这个函数叫做窗口过程。
窗口句柄:窗口是通过窗口句柄(HWND,Handle to a window)来标识的

2、消息和消息队列

消息:由MSG结构体来表示
typedef struct tagMSG {
	HWND hwnd;//窗口句柄,表示消息所属窗口
	UINT message;/*消息的标识符,是一个unsigned int的数值,不同消息表示不同数值,Windows通过宏定义
				 将这些数值定义为WM_XXX,其中为某种消息的英文拼写的大写形式,例如WM_LBUTTONDOWN表示
				 按下*/
	WPARAM wParam;
	LPARAM lParam;//wParam和lParam指定消息的附加信息
	DWORD time;//消息投递到消息队列的时间
	POINT pt;//鼠标的当前位置
}MSG;
消息队列:Windows将产生的消息放到消息队列中,应用程序通过一个消息循环不断地从消息队列中取消息,并进行响应。
进队消息和不进队消息

进队消息:系统把进队消息放到消息队列中,等应用程序取。
不进队消息:系统调用窗口过程时,直接发送给窗口。

进队消息基本上是用户的输入:击键的消息(WM_KEYDOWN、WM_KEYUP)键盘输入产生字符(WM_CHAR)、鼠标移动(WM_MOUSEMOVE)、
鼠标键(WM_LBUTTONDOWN)、计时消息(WM_TIMER)、刷新消息(WM_PAINT)和退出消息(WM_QUIT)

不进队消息是指由Windows直接调用消息处理函数,把消息直接交给其处理。而进队消息是指Windows将消息放入到程序中的
消息队列中取,并通过程序中的消息循环,循环把消息取出,经过一定处理(如例子中经过translate),然后由函数
DispathMessage函数将消息分发给消息处理函数处理

一般情况下,不进队消息的产生是由于调用了其他Windows函数。
如,当调用CreateWindow时,Windows将创建WM_CREATE消息、
当调用ShowWindow时,将产生WM_SIZE和WM_SHOWWINDOW消息、
当调用UpdateWindow时创建的WM_PAINT消息

(注意,并不是某个类型是进队消息就永远是进队消息,如WM_PAINT有进队的,也有不进队的)、还有其他进队消息也有可能
在不进队消息中出现,整个处理过程是复杂的,但由于Windows已经解决大部分的问题,因此我们可以认为我们获得的消息是
有序的、同步的。
发送消息:SendMessage 和 PostMessage,SendMessage为发送“不进队消息”,直接调用处理函数处理,返回处理函数处理结果。
ostMessage为发送“进队消息”。PostThreadMessage为向线程发消息

消息分为进队消息消息和非进队消息。所谓进队消息就是windows将消息发送到每个线程所专有的队列中,然后由程序自主处理,
这种消息基本上是由用户输 入产生(wm_keydown,wm_keyup,wm_char,wm_mouse**,以及wm_paint,wm_timer,wm_quit)或 者是调用
ostmessage,postthreadmessage产生的消息;所谓的非进队消息就是直接发送给窗口过程的消息,就是直接调用窗口过 程,上述
消息以外的一般都是这种类型!
各种消息是用户和系统之间的交互,系统内核收集。转发给各个应用程序的消息队列。每个线程一个消息队列
回到目录

3、WinMain函数

程序实现的步骤:
  1. WinMain函数的定义
  2. 创建一个窗口
  3. 进行消息循环
  4. 编写窗口过程函数
定义:
int APIENTRY _tWinMain(
                     HINSTANCE hInstance,//程序当前运行的实例句柄,唯一标识当前运行的实例,是一个数值
                     HINSTANCE hPrevInstance,//当前实例的上一个实例的句柄,Win32环境下,这个参数总是NULL
                     LPTSTR    lpCmdLine,//以空终止的字符串,传递给应用程序的命令行参数
                     int       nCmdShow//指定窗口应该如何显示,不需要理会
                     );
1)创建窗口
  1. 设计一个窗口类(对WNDCLASS对象赋值,设计窗口的特征)
  2. 注册窗口类(调用RegisterClass(CONST WNDCLASS *lpWndClass)函数注册)
  3. 创建窗口(调用CreateWindow函数)
  4. 显示和刷新窗口(调用ShowWindow和UpdateWindow函数)
typedef struct _WNDCLASS{
	UINT style;//窗口的样式,CS_XXX,详细到WIN32 API中查看,常用样式CS_HREDRAW,CS_VREDRAW,CS_NOCLOSE,CS_DBCLKS
	WNDPROC lpfnWndProc;//指向窗口过程函数,WNDPROC类型为LRESULT CALLBACK *,所以窗口过程函数的返回类型应为LRESULT CALLBACK,DispatchMessage函数将消息回传到系统,系统调用窗口过程函数对消息进行处理
	int cbCLSExtra;//类附加内存,所有调用该窗口类的窗口共享,单位为字节,一般初始化为0
	int cbWndExtra;//窗口附加内存,应用程序可以使用这部分内存存储窗口特有的数据,一般初始化为0
	HANDLE hInstance;//指定包含窗口过程的程序的实例句柄,一般为WinMain函数的第一个参数
	HICON hIcon;//指定窗口类的图标句柄,调用HICON LoadIcon(HINSTANCE hInstance,LPCTSTR lpIconName),调用系统的图标时,将第一个参数设置为NULL
	HCURSOR hCursor;//指定窗口类的图标句柄,调用HCURSOR LoadCursor(HINSTANCE hInstance,LPCTSTR lpCursorName),调用系统的图标时,将第一个参数设置为NULL
	HBRUSH hbrBackground;//指定窗口类的背景画刷句柄,调用HGDIOBJ GetStockObject(int fnObject),fnObject详见WIN32API,返回类型要进行强制类型转换(HBRUSH),GetStockObject函数可以获取画刷、画笔、字体、调色板的句柄,所以返回值类型需要进行强制类型转换、
	LPCTSTR lpszMenuName;//以空为终止的字符串,指定菜单资源的名字,将该值设为空,设置默认菜单,由该窗口创建的窗口奖没有默认菜单。
	LPCTSTR lpszClassName;//以空为终止的字符串,指定窗口类的名字
};
ATOM RegisterWindow(CONST WNDCLASS *lpWndClass);//只有一个参数,为上一步设计的窗口类对象的指针
//该函数一般需要先创建一个窗口句柄HWND hwnd,接受该函数的返回值
HWND CreateWindow(
				  LPCTSTR lpClassName,//指定窗口类的名称,应与注册的窗口类对象中窗口类的名字对应lpszClassName
				  LPCTSTR lpWindowName,//指定窗口的名字,如果窗口样式指定了标题栏,那么将显示在标题栏上
				  DWORD dwStyle,//指定创建的窗口样式,与WNDCLASS的style不同,style是该窗口类创建窗口都具有的样式,dwStyle是指定某个具体的窗口样式,常用WS_OVERLAPPEDWINDOW样式
				  int x,
				  int y,//窗口左上角的坐标,若将x设置为CW_USEDEFAULT,那么使用默认坐标,并忽略y
				  int nWidth,
				  int nHeight,//窗口的宽度和高度,若将nWidth设置为CW_USEDEFAULT,那么使用默认坐标,并忽略nHeight
				  HWND hWndParent,//父窗口的句柄,子窗口必须有WS_CHILD样式
				  HMENU hMend,//指定菜单句柄
				  HANDLE hInstance,//窗口所属的应用程序的实例句柄
				  LPVOID lpParam,//作为WM_CREATE消息的附加参数lParam传入的数据指针,一般窗口为NULL,多文档界面窗口,必须指向CLIENTCRATESTRUCT结构体
				  );
//创建成功返回窗口句柄,失败返回NULL
BOOL ShowWindow(
				HWND hwnd,//上一步创建成功后返回的窗口句柄
				int nCmdShow//窗口显示状态,SW_XXXX,第一次显示窗口时应该用SW_SHOWNORMAL,详见WIN32API
				);
BOOL UpdateWindow(
				  HWND hwnd,
				  );
//该函数通过发送一个WM_PAINT消息给窗口过程函数来处理,而没有放到消息队列中去,属于未进队消息
MSG msg;
whlle(GetMessage(&msg,NULL,0,0))
{
	TranslateMessage(&msg);
	DispatchMessage(&msg);
}
//GetMessage从消息队列里取出消息,只有接收到WM_QUIT消息,才返回0
//TranslateMessage用于将虚拟键消息转换为字符消息,然后将字符消息投递到消息队列中,当下一次调用GetMessage函数式被取出,该函数不修改原有消息
//DispatchMessage将消息发送给操作系统,操作系统调用窗口过程函数对消息进行处理
BOOL GetMessage(
				LPMSG lpMsg,//指向一个消息结构体(MSG),将从消息队列取出的消息保存到MSG结构体中
				HWND hwnd,//指定接收属于哪个窗口的消息,通常设置为NULL,勇于接收属于调用现成的所有窗口的窗口消息
				UINT wMsgFilterMin,//接收消息的最小值,通常设置为0
				UINT wMsgFilterMax//接收消息的最大值,通常也设置为0,当这两个都设置为0时,接收所有消息
};
//从消息队列里获取消息还可以使用PeekMessage函数
BOOL PeekMessage(
				LPMSG lpMsg,
				HWND hwnd,
				UNIT wMsgFilterMin,
				UNIT wMsgFilterMax,
				UINT wRemoveMsg//PM_NOREMOVE不从消息队列中移除消息,PM_REMOVE反之作用于GetMessage相同,更多见API
};
//发送消息可以使用两个函数
SendMessage(hwnd,wMsg,wParam,lParam)//直接发送给窗口,窗口过程函数处理完返回
PostMessage(hwnd,wMsg,wParam,lParam)//放到消息队列中
LRESULT CALLBACK WindowProc(
			HWND hwnd,//接收消息的特定窗口
			UINT uMsg,
			WPARAM wParam,
			LPARAM lParam
};
//窗口过程函数内部使用switch/case语句进行消息处理

PLUS:sz(string zero,以零结束的字符串)

LRESULT CALLBACK WinSunProc(
							HWND hwnd,      // handle to window
							UINT uMsg,      // message identifier
							WPARAM wParam,  // first message parameter
							LPARAM lParam   // second message parameter
							)
{
	switch(uMsg)
	{
	case WM_CHAR:
		char szChar[20];
		sprintf(szChar,"char code is %d",wParam);
		MessageBox(hwnd,szChar,"char",0);//hwnd将被创建的消息框的拥有窗口
		//szChar将被显示的以NULL结尾的消息字符串,"char"对话框标题,同样是以NULL结尾
		//第四个参数是决定对话框的内容和行为,MB_OK为缺省值,等于0
		break;//不要忘加break
	case WM_LBUTTONDOWN://鼠标左键按下产生的消息
		MessageBox(hwnd,"mouse clicked","message",0);
		HDC hdc;
		//DC(Device Context)设备上下文、设备描述表,是管理设备和驱动程序的
		//WINDOWS下所有的图形操作都是用DC来完成的
		//HDC:DC的句柄,用HDC GetDC(HWND)获得,DC相当于画师,HWND窗口句柄相
		//当于画布,他们之间通过GetDC绑定,使用完必须释放
		hdc=GetDC(hwnd);
		TextOut(hdc,0,50,"程序员之家",strlen("程序员之家"));
		//在指定位置输出字符串
		ReleaseDC(hwnd,hdc);//释放DC所占用的资源,否则会引起内存泄漏
		break;
	case WM_PAINT:
	//如果想要某个图像始终在窗口中显示,那么就应该将图形绘制放在响应WM_PAINT的
	//代码中
		HDC hDC;
		PAINTSTRUCT ps;//用来接收绘制的信息
		hDC=BeginPaint(hwnd,&ps);
		//hwnd绑定窗口句柄,第二参数是指向PAITSTRUCT对象的指针
		//除了WM_PAINT消息内可以使用BeginPaint获取HDC,其他消息内必须使用GetDC来获取
		TextOut(hDC,0,0,"http://www.sunxin.org",strlen("http://www.sunxin.org"));
		EndPaint(hwnd,&ps);//必须使用EndPaint函数来释放DC
		break;
	case WM_CLOSE:
	//点击关闭按钮时产生的消息,如果没有对该消息进行相应,则会调用DefWindowProc函数,该函数调用DestroyWindow函数来响应这条WM_CLOSE消息。
	//MessageBox函数的返回值都是IDXXX,例如第四个参数设置为MB_YESNO,则返回值
	//有IDYES和IDNO两种,未点击是会阻塞
		if(IDYES==MessageBox(hwnd,"是否真的结束?","message",MB_YESNO))
		{
			DestroyWindow(hwnd);
			//DestroyWindow函数会发送WM_DESTROY消息
		}
		break;
	case WM_DESTROY:
		PostQuitMessage(0);
		//发送WM_QUIT消息,其参数会作为WM_QUIT消息的wParam参数,这个参数通常作为WinMain函数的返回值
		break;
	default:
		return DefWindowProc(hwnd,uMsg,wParam,lParam);
		//对程序没有处理的其他消息进行默认处理,必须要有这一句,否则窗口将无法正常显示,并且将该函数的返回值作为窗口过程函数的返回值进行返回。
	}
	return 0;//别落下这一句
}

回到目录

4、主函数 main WinMain _tmain _tWinMain 的区别

主函数 main WinMain _tmain _tWinMain 的区别

main是C/C++的标准入口函数名

WinMain是windows API窗体程序的入口函数。(int WINAPI WinMain()) 中 WINAPI是__stdcall宏,在windef.h中定义的。

_tmain _tWinMain 是Unicode版本函数别名,对应与wmain和wWinMain。


中有如下几行:

#ifdef _UNICODE

#define _tmain wmain
#define _tWinMain wWinMain

#else

#define _tmain main
#define _tWinMain WinMain

#endif

这样定义是为了自动适应是否定义了UNICODE,其中wmain和wWinMain是支持UNICODE字符的。

前缀为"_t"的应用与UNICODE的函数,工程中最好用这类函数。

来自另一篇文章:

  1. main是c/c++的标准入口函数名
  2. winmain是windows api窗体程序的入口函数(int winapi winmain()中winapi是__stdcall的宏 在windows.h中定义)
  3. _tmain _twinmain是unicode版本函数别名 为了编译时能自动转换字符串编码


1.main是C程序的函数,_tmain是main为了支持unicode所使用的main的別名

2._tmain的定义在可以找到,如#define _tmain main,所以要加#include 才能用。 _tmain()是个宏,如果是UNICODE则他是wmain()否则他是main()

3.因此_tmain compile后仍为main,所以都可以执行



  1. main()是WINDOWS的控制台程序(32BIT)或DOS程序(16BIT),
  2. WinMain()是WINDOWS的GUI程序,
    wmain()是UNICODE版本的main(),
    3)_tmain()是个宏,如果是UNICODE则他是wmain()否则他是main()
    外,wmain也是main的另一個别名,是为了支持二个字节的语言环境

回到目录

5、C++基础补充

1. 结构体成员默认是共有的(public),类成员默认是私有的(private)
2. 结构体的默认集成方式是public,类默认的继承方式是private,最好显示的写出来
3. C++中 delete 和 delete[] 的区别

建议使用delete[],因为不管是否是数组类型的,都会释放,而delete却不行
当调用delete的时候,系统会自动调用已分配的对象的析构函数。当我们用new [] 分配的对象是基本数据类型时,用delete和delete [] 没有区别。但是,当分配的对象是自定义对象时,二者不能通用。一般来说使用new分配的对象,用delete来释放。用new[] 分配的内存用delete [] 来逐个释放。

1.我们通常从教科书上看到这样的说明:

delete 释放new分配的单个对象指针指向的内存

delete[] 释放new分配的对象数组指针指向的内存

那么,按照教科书的理解,我们看下下面的代码:

int *a = new int[10];
delete a;        //方式1
delete [] a;     //方式2

肯定会有很多人说方式1肯定存在内存泄漏,是这样吗?

(1). 针对简单类型 使用new分配后的不管是数组还是非数组形式内存空间用两种方式均可 如:

int *a = new int[10];
delete a;
delete [] a;

此种情况中的释放效果相同,原因在于:分配简单类型内存时,内存大小已经确定,系统可以记忆并且进行管理,在析构时,系统并不会调用析构函数,

它直接通过指针可以获取实际分配的内存空间,哪怕是一个数组内存空间(在分配过程中 系统会记录分配内存的大小等信息,此信息保存在结构体_CrtMemBlockHeader中,

具体情况可参看VC安装目录下CRTSRCDBGDEL.cpp)

(2). 针对类Class,两种方式体现出具体差异

当你通过下列方式分配一个类对象数组:

class A
   {
   private:
      char *m_cBuffer;
      int m_nLen;
   public:
      A(){ m_cBuffer = new char[m_nLen]; }
      ~A() { delete [] m_cBuffer; }
   };
   A *a = new A[10];
   delete a;         //仅释放了a指针指向的全部内存空间 但是只调用了a[0]对象的析构函数 剩下的从a[1]到a[9]这9个用户自行分配的m_cBuffer对应内存空间将不能释放 从而造成内存泄漏
   delete [] a;      //调用使用类对象的析构函数释放用户自己分配内存空间并且   释放了a指针指向的全部内存空间

所以总结下就是,如果ptr代表一个用new申请的内存返回的内存空间地址,即所谓的指针,那么:

delete ptr 代表用来释放内存,且只用来释放ptr指向的内存。

delete[] rg 用来释放rg指向的内存,!!还逐一调用数组中每个对象的destructor!!

对于像int/char/long/int*/struct等等简单数据类型,由于对象没有destructor,所以用delete 和delete [] 是一样的!但是如果是C++对象数组就不同了!

关于 new[] 和 delete[],其中又分为两种情况:(1) 为基本数据类型分配和回收空间;(2) 为自定义类型分配和回收空间。

对于 (1),上面提供的程序已经证明了 delete[] 和 delete 是等同的。但是对于 (2),情况就发生了变化。

我们来看下面的例子,通过例子的学习了解C++中的delete和delete[]的使用方法

#include <iostream>
using namespace std;
/class Babe
class Babe
{
public:
    Babe()
    {
        cout << \"Create a Babe to talk with me\" << endl;
    }
    ~Babe()
    {
        cout << \"Babe don\'t Go away,listen to me\" << endl;
    }
};
//main function
int main()
{
    Babe* pbabe = new Babe[3];
    delete pbabe;
    pbabe = new Babe[3];
    delete pbabe[];
    return 0;
}

结果是:

Create a babe to talk with me
 
Create a babe to talk with me
 
Create a babe to talk with me
 
Babe don\'t go away,listen to me
 
Create a babe to talk with me
 
Create a babe to talk with me
 
Create a babe to talk with me
 
Babe don\'t go away,listen to me
 
Babe don\'t go away,listen to me
 
Babe don\'t go away,listen to me

大家都看到了,只使用delete的时候只出现一个 Babe don’t go away,listen to me,而使用delete[]的时候出现3个 Babe don’t go away,listen to me。不过不管使用delete还是delete[]那三个对象的在内存中都被删除,既存储位置都标记为可写,但是使用delete的时候只调用了pbabe[0]的析构函数,而使用了delete[]则调用了3个Babe对象的析构函数。你一定会问,反正不管怎样都是把存储空间释放了,有什么区别。答:关键在于调用析构函数上。此程序的类没有使用操作系统的系统资源(比如:Socket、File、Thread等),所以不会造成明显恶果。如果你的类使用了操作系统资源,单纯把类的对象从内存中删除是不妥当的,因为没有调用对象的析构函数会导致系统资源不被释放,如果是Socket则会造成Socket资源不被释放,最明显的就是端口号不被释放,系统最大的端口号是65535(216 _ 1,因为还有0),如果端口号被占用了,你就不能上网了,呵呵。如果File资源不被释放,你就永远不能修改这个文件,甚至不能读这个文件(除非注销或重器系统)。如果线程不被释放,这它总在后台运行,浪费内存和CPU资源。这些资源的释放必须依靠这些类的析构函数。所以,在用这些类生成对象数组的时候,用delete[]来释放它们才是王道。而用delete来释放也许不会出问题,也许后果很严重,具体要看类的代码了.

5.记一个编程的小问题,是关于构造函数和创建对象的问题

类中有成员变量,那么就要在构造函数中初始化,若不初始化,编译器会产生警告,运行时会出现运行时错误。

6.类的对象和指针的区别

类的对象:用的是内存栈,是个局部的临时变量.
类的指针:用的是内存堆,是个永久变量,除非你释放它.

7.尽量别在成员变量声明时初始化,C++11后可以

因为类只是定义的类型, 还没有实例化,也就是没有定义类的对象(变量), 没法存储
你可以在初始化列表里进行初始化 , 而构造函数的函数体之内赋值的话, 是在初始化后,

至于定义成static的, 这是静态的, 所有对象共享一个副本, 程序开始执行就初始化了, 就算没有定义对象, 也有它的实例, 能直接使用 A::static_a = xxx;

8.什么时候使用拷贝构造函数

(1)一个对象以值传递的方式传入函数体
(2)一个对象以值传递的方式从函数返回
(3)一个对象需要通过另外一个对象进行初始化。

9.拷贝构造函数

复制构造函数
几个原则:

C++ primer p406 :复制构造函数是一种特殊的构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显示使用复制构造函数。当该类型的对象传递给函数或从函数返回该类型的对象时,将隐式调用复制构造函数。

C++支持两种初始化形式:复制初始化(int a = 5;)和直接初始化(int a(5);)对于其他类型没有什么区别,对于类类型直接初始化直接调用实参匹配的构造函数,复制初始化总是调用复制构造函数,也就是说:

A x(2);  //直接初始化,调用构造函数
A y = x;  //复制初始化,调用复制构造函数

必须定义复制构造函数的情况:

只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义复制构造函数也可以复制;有的类有一个数据成员是指针,或者是有成员表示在构造函数中分配的其他资源,这两种情况下都必须定义复制构造函数。

什么情况使用复制构造函数:

类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:
(1)一个对象以值传递的方式传入函数体
(2)一个对象以值传递的方式从函数返回
(3)一个对象需要通过另外一个对象进行初始化。

深拷贝和浅拷贝:

所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间

如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝

上面提到,如果没有自定义复制构造函数,则系统会创建默认的复制构造函数,但系统创建的默认复制构造函数只会执行“浅拷贝”,即将被拷贝对象的数据成员的值一一赋值给新创建的对象,若该类的数据成员中有指针成员,则会使得新的对象的指针所指向的地址与被拷贝对象的指针所指向的地址相同,delete该指针时则会导致两次重复delete而出错。下面是示例:

复制代码

 1 #include <iostream.h>
 2 #include <string.h>
 3 class Person 
 4 {
 5 public :
 6          
 7     // 构造函数
 8     Person(char * pN)
 9     {
10         cout << "一般构造函数被调用 !\n";
11         m_pName = new char[strlen(pN) + 1];
12         //在堆中开辟一个内存块存放pN所指的字符串
13         if(m_pName != NULL) 
14         {
15            //如果m_pName不是空指针,则把形参指针pN所指的字符串复制给它
16              strcpy(m_pName ,pN);
17         }
18     }        
19        
20     // 系统创建的默认复制构造函数,只做位模式拷贝
21     Person(Person & p)    
22     { 
23         //使两个字符串指针指向同一地址位置         
24         m_pName = p.m_pName;         
25     }
26  
27     ~Person( )
28     {
29         delete m_pName;
30     }
31          
32 private :
33     char * m_pName;
34 };
35  
36 void main( )
37 { 
38     Person man("lujun");
39     Person woman(man); 
40      
41     // 结果导致   man 和    woman 的指针都指向了同一个地址
42      
43     // 函数结束析构时
44     // 同一个地址被delete两次
45 }
46  
47  
48 // 下面自己设计复制构造函数,实现“深拷贝”,即不让指针指向同一地址,而是重新申请一块内存给新的对象的指针数据成员
49 Person(Person & chs);
50 {
51      // 用运算符new为新对象的指针数据成员分配空间
52      m_pName=new char[strlen(p.m_pName)+ 1];
53  
54      if(m_pName)         
55      {
56              // 复制内容
57             strcpy(m_pName ,chs.m_pName);
58      }
59    
60     // 则新创建的对象的m_pName与原对象chs的m_pName不再指向同一地址了
61 }

复制代码

重载赋值操作符:

通过定义operate=的函数,可以对赋值进行定义。像其他任何函数一样,操作符函数有一个返回值和形参表。形参表必须具有与该操作符操作数书目相同的形参(如果操作符是一个成员,则包括隐式this形参)。赋值是二元运算,所以该操作符函数有两个形参:第一个形参(隐含的this指针)对应着左操作数,第二个形参对应右操作数。

一个应用了对赋值号重载的拷贝构造函数的例子:

复制代码

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 class A
 6 {
 7 public:
 8     A(int);//构造函数
 9     A(const A &);//拷贝构造函数
10     ~A();
11     void print();
12     int *point;
13     A &operator=(const A &);
14 };
15 
16 A::A(int p)
17 {
18     point = new int;
19     *point = p;
20 }
21 
22 A::A(const A &b)
23 {
24     *this = b;
25     cout<<"调用拷贝构造函数"<<endl;
26 }
27 
28 A::~A()
29 {
30     delete point;
31 }
32 
33 void A::print()
34 {
35     cout<<"Address:"<<point<<" value:"<<*point<<endl;
36 }
37 
38 A &A::operator=(const A &b)
39 {
40     if( this != &b)
41     {
42         delete point;
43         point = new int;
44         *point = b.point;
45     }
46 }
47 
48 
49 int main()
50 {
51     A x(2);
52     A y = x;
53     x.print();
55     y.print();
56 
57     return 0;
58 }
10.虚继承

1.为什么要引入虚拟继承

虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继 承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如 下:

class A

class B1:public virtual A;

class B2:public virtual A;

class D:public B1,public B2;

虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。

为什么需要虚继承?

由于C++支持多重继承,那么在这种情况下会出现重复的基类这种情况,也就是说可能出现将一个类两次作为基类的可能性。比如像下面的情况

1 #include<iostream>
 2 using std::cout;
 3 using std::endl;
 4 class Base
 5 {
 6 protected:
 7 int value;
 8 public:
 9 Base()
10 {
11 cout<<"in Base"<<endl;
12 }
13 };
14 class DerivedA:protected Base
15 {
16 public:
17 DerivedA()
18 {
19 cout<<"in DerivedA"<<endl;
20 }
21 };
22 class DerivedB: protected Base
23 {
24 public:
25 DerivedB()
26 {
27 cout<<"in DerivedB"<<endl;
28 }
29 };
30 class MyClass:DerivedA,DerivedB
31 {
32 public:
33 MyClass()
34 {
35 cout<<"in MyClass"<<value<<endl;
36 }
37 };

编译时出现如下警告:
在这里插入图片描述
虚继承的特点是,在任何派生类中的virtual基类总用同一个(共享)对象表示

11.利用VS的命令行工具查看对象在内存中的布局

打开vs的命令行工具

开始 -> 程序 -> Microsoft Visual Studio 2008 -> Visual Studio Tools -> Visual Studio 2008 Command Prompt

微软的VS2010提供了一个新的选项,给用户显示C++对象在内存中的布局。这个选项就是:

/d1reportSingleClassLayout
具体使用方法如下,在写好相应的cpp文件之后,需要启动VS2010的命令行工具“Visual Studio 2010Command Prompt”,切换到cpp文件所在目录之后,输入如下的命令:

cl [filename].cpp /d1reportSingleClassLayout[className]

12.引入虚继承和直接继承会有什么区别呢

由于有了间接性和共享性两个特征,所以决定了虚继承体系下的对象在访问时必然会在时间和空间上与一般情况有较大不同。

2.1时间:在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样),其实就是调整this指针以指向虚基类对象,只不过这个调整是运行时间接完成的。

2.2空间:由于共享所以不必要在对象内存中保存多份虚基类子对象的拷贝,这样较之 多继承节省空间。虚拟继承与普通继承不同的是,虚拟继承可以防止出现diamond继承时,一个派生类中同时出现了两个基类的子对象。也就是说,为了保证 这一点,在虚拟继承情况下,基类子对象的布局是不同于普通继承的。因此,它需要多出一个指向基类子对象的指针

13.虚基类的对象在内存中分布的问题

http://blog.csdn.net/wangqiulin123456/article/details/8059536

14.c++重载、覆盖、隐藏的区别和执行方式

既然说到了继承的问题,那么不妨讨论一下经常提到的重载,覆盖和隐藏
4.1成员函数被重载的特征
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
4.2“覆盖”是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
4.3“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,特征是:

(1)如果派生类的函数与基类的函数同名,但是参数不同,此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,但是参数相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

小结:说白了就是如果派生类和基类的函数名和参数都相同,属于覆盖,这是可以理解的吧,完全一样当然要覆盖了;如果只是函数名相同,参数并不相同,则属于隐藏。

4.4 三种情况怎么执行:

4.4.1 重载:看参数。

4.4.2 隐藏:用什么就调用什么。

4.4.3 覆盖:调用派生类。

15.C++子类继承父类后子类的大小
1 #include <iostream>
 2 using namespace std;
 3 class  A 
 4 {
 5 private:
 6  int a;
 7 };
 8 
 9 class B:public  A
10 {
11 private:
12  int b;
13 };
14 
15 int main()
16 {
17  cout<<sizeof(A)<<endl;
18  cout<<sizeof(B)<<endl;
19  return 0;
20 }

刚开始我一想子类继承父类不会继承父类的私有变量,如此我认为结果为4,4(错误)。而事实上结果是4,8。也就是说子类把父类的私有变量也继承下来了,但是却无法访问,对于我这种菜鸟来说一下子没法转个弯来,后来看看资料焕然大悟,子类虽然无法直接访问父类的私有变量,但是子类继承的父类的函数却可以访问,不然的话如果只继承函数而不继承变量,哪么父类的函数岂不成了无米之炊了。所以必须把父类的所有变量都继承下来,这样既能保护父类的变量也能使用父类的函数。

16.C++虚拟继承的实际大小

输出下面class的大小:

class X{};  
class Y : public virtual X{};  
class Z : public virtual X{};  
class A : public Y, public Z{}; 

继承关系如下图:

这是可能大家就会觉得他们的大小都应该是0,因为他们中没有任何一个有明显的数据,只表示了继承关系。但是至少也认为class x应该是0吧,他什么都没有。结果却让你想不到,我在vs2010环境下测试的大小是:(不同编译器可能这个大小是不一样)

cout<<"sizeof X: " <<sizeof X<<endl  
    <<"sizeof Y: " <<sizeof Y<<endl  
    <<"sizeof Z: " <<sizeof Z<<endl  
    <<"sizeof A: " <<sizeof A<<endl;  

在这里插入图片描述

很奇怪吧,为什么是这个结果呢。一个空的class事实上并不是空,它有一个隐藏的1 byte,这个是编译器安插进去的char,这样就可以保证定义的对象在内存中的大小是独一无二的,这个地方你可以自己测试下,比如:

X xa,xb;  
if (&xa == &xb)  
    cout<<"is equal"<<endl;  
else  
    cout<<"not equal"<<endl;

但是让人搞不懂的是Y、Z的大小。主要大小受三个因素的影响:

语言本身所造成的额外负担,当语言支持虚基类的时候,就导致一个额外的负担,这个一般都是一个虚表指针。里面存储的就是虚基类子对象的地址,就是偏移量。
编 译器对于特殊情况所提供的优化处理,因为class X有1 byte的大小,这样就出现在了class Y和class Z身上。这个主要视编译器而定,比如某些存在这个1byte但是有些编译器就将他忽略了(因为已经用虚指针了所以这个1byte就可以不用作为内存中的一 个定位)。
Alignment的限制,就是所谓的对齐操作,比如你现在占用5bytes编译器为了更有效率地在内存中存取就将其对齐为8byte。
下面说明在vs2010中的模型,因为有了虚指针后所以1byte就不用了,所以class Y和class Z的大小就是4bytes,如下图:

现在你觉得class A的大小应该是多少呢?一个虚基类子对象只会在派生类中存在一份实体,不管他在继承体系中出现多少次,所以公用一个1byte的classX实体,再加上 class Y和class Z这样就有9bytes,如果有对齐的话就是12bytes但是vs2010中省略了那1byte所以就不存在对齐就直接是8bytes。谜底终于揭开 了!!!

17、C++赋值运算符重载函数(operator=)

写在前面:

  关于C++的赋值运算符重载函数(operator=),网络以及各种教材上都有很多介绍,但可惜的是,内容大多雷同且不全面。面对这一局面,在下在整合各种资源及融入个人理解的基础上,整理出一篇较为全面/详尽的文章,以飨读者。

正文:

Ⅰ.举例

例1

#include<iostream>
#include<string>
using namespace std;

class MyStr
{
private:
    char *name;
    int id;
public:
    MyStr() {}
    MyStr(int _id, char *_name)   //constructor
    {
        cout << "constructor" << endl;
        id = _id;
        name = new char[strlen(_name) + 1];
        strcpy_s(name, strlen(_name) + 1, _name);
    }
    MyStr(const MyStr& str)
    {
        cout << "copy constructor" << endl;
        id = str.id;
        if (name != NULL)
            delete[] name;
        name = new char[strlen(str.name) + 1];
        strcpy_s(name, strlen(str.name) + 1, str.name);
    }
    MyStr& operator =(const MyStr& str)//赋值运算符
    {
        cout << "operator =" << endl;
        if (this != &str)
        {
            if (name != NULL)
                delete[] name;
            this->id = str.id;
            int len = strlen(str.name);
            name = new char[len + 1];
            strcpy_s(name, strlen(str.name) + 1, str.name);
        }
        return *this;
    }
    ~MyStr()
    {
        delete[] name;
    }
};

int main()
{
    MyStr str1(1, "hhxx");
    cout << "====================" << endl;
    MyStr str2;
    str2 = str1;
    cout << "====================" << endl;
    MyStr str3 = str2;
    return 0;
}

结果:
在这里插入图片描述

Ⅱ.参数

一般地,赋值运算符重载函数的参数是函数所在类的const类型的引用(如上面例1),加const是因为:

①我们不希望在这个函数中对用来进行赋值的“原版”做任何修改。

②加上const,对于const的和非const的实参,函数就能接受;如果不加,就只能接受非const的实参。

用引用是因为:

这样可以避免在函数调用时对实参的一次拷贝,提高了效率。

注意:

上面的规定都不是强制的,可以不加const,也可以没有引用,甚至参数可以不是函数所在的对象,正如后面例2中的那样。

Ⅲ.返回值

一般地,返回值是被赋值者的引用,即*this(如上面例1),原因是

①这样在函数返回时避免一次拷贝,提高了效率。

②更重要的,这样可以实现连续赋值,即类似a=b=c这样。如果不是返回引用而是返回值类型,那么,执行a=b时,调用赋值运算符重载函数,在函数返回时,由于返回的是值类型,所以要对return后边的“东西”进行一次拷贝,得到一个未命名的副本(有些资料上称之为“匿名对象”),然后将这个副本返回,而这个副本是右值,所以,执行a=b后,得到的是一个右值,再执行=c就会出错。

注意:

这也不是强制的,我们可以将函数返回值声明为void,然后什么也不返回,只不过这样就不能够连续赋值了。

Ⅳ.调用时机

当为一个类对象赋值(注意:可以用本类对象为其赋值(如上面例1),也可以用其它类型(如内置类型)的值为其赋值,关于这一点,见后面的例2)时,会由该对象调用该类的赋值运算符重载函数。

如上边代码中

str2 = str1;

一句,用str1为str2赋值,会由str2调用MyStr类的赋值运算符重载函数。

需要注意的是,

MyStr str2;

str2 = str1;

MyStr str3 = str2;

在调用函数上是有区别的。正如我们在上面结果中看到的那样。

前者MyStr str2;一句是str2的声明加定义,调用无参构造函数,所以str2 = str1;一句是在str2已经存在的情况下,用str1来为str2赋值,调用的是拷贝赋值运算符重载函数;而后者,是用str2来初始化str3,调用的是拷贝构造函数。

Ⅴ.提供默认赋值运算符重载函数的时机

当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动生成这样一个赋值运算符重载函数。注意我们的限定条件,不是说只要程序中有了显式的赋值运算符重载函数,编译器就一定不再提供默认的版本,而是说只有程序显式提供了以本类或本类的引用为参数的赋值运算符重载函数时,编译器才不会提供默认的版本。可见,所谓默认,就是“以本类或本类的引用为参数”的意思。

见下面的例2

#include<iostream>
#include<string>
using namespace std;

class Data
{
private:
    int data;
public:
    Data() {};
    Data(int _data)
        :data(_data)
    {
        cout << "constructor" << endl;
    }
    Data& operator=(const int _data)
    {
        cout << "operator=(int _data)" << endl;
        data = _data;
        return *this;
    }
};

int main()
{
    Data data1(1);
    Data data2,data3;
    cout << "=====================" << endl;
    data2 = 1;
    cout << "=====================" << endl;
    data3 = data2;
    return 0;
}

结果:

在这里插入图片描述

上面的例子中,我们提供了一个带int型参数的赋值运算符重载函数,data2 = 1;一句调用了该函数,如果编译器不再提供默认的赋值运算符重载函数,那么,data3 = data2;一句将不会编译通过,但我们看到事实并非如此。所以,这个例子有力地证明了我们的结论。

Ⅵ.构造函数还是赋值运算符重载函数

如果我们将上面例子中的赋值运算符重载函数注释掉,main函数中的代码依然可以编译通过。只不过结论变成了
在这里插入图片描述

可见,当用一个非类A的值(如上面的int型值)为类A的对象赋值时

如果匹配的构造函数和赋值运算符重载函数同时存在(如例2),会调用赋值运算符重载函数。

如果只有匹配的构造函数存在,就会调用这个构造函数。

Ⅶ.显式提供赋值运算符重载函数的时机

用非类A类型的值为类A的对象赋值时(当然,从Ⅵ中可以看出,这种情况下我们可以不提供相应的赋值运算符重载函数而只提供相应的构造函数来完成任务)。

当用类A类型的值为类A的对象赋值且类A的成员变量中含有指针时,为避免浅拷贝(关于浅拷贝和深拷贝,下面会讲到),必须显式提供赋值运算符重载函数(如例1)。

Ⅷ.浅拷贝和深拷贝

拷贝构造函数和赋值运算符重载函数都会涉及到这个问题。

所谓浅拷贝,就是说编译器提供的默认的拷贝构造函数和赋值运算符重载函数,仅仅是将对象a中各个数据成员的值拷贝给对象b中对应的数据成员(这里假设a、b为同一个类的两个对象,且用a拷贝出b或用a来给b赋值),而不做其它任何事。

假设我们将例1中显式提供的拷贝构造函数注释掉,然后同样执行MyStr str3 = str2;语句,此时调用默认的拷贝构造函数,它只是将str2的id值和nane值拷贝到str3,这样,str2和str3中的name值是相同的,即它们指向内存中的同一区域(在例1中,是字符串”hhxx”)。如下图

在这里插入图片描述
这样,会有两个致命的错误

①当我们通过str2修改它的name时,str3的name也会被修改!

②当执行str2和str3的析构函数时,会导致同一内存区域释放两次,程序崩溃!

这是万万不可行的,所以我们必须通过显式提供拷贝构造函数以避免这样的问题。就像我们在例1中做的那样,先判断被拷贝者的name是否为空,若否,delete[] name(后面会解释为什么要这么做),然后,为name重新申请空间,再将拷贝者name中的数据拷贝到被拷贝者的name中。执行后,如图
在这里插入图片描述
这样,str2.name和str3.name各自独立,避免了上面两个致命错误。

我们是以拷贝构造函数为例说明的,赋值运算符重载函数也是同样的道理。

Ⅸ.赋值运算符重载函数只能是类的非静态的成员函数

C++规定,赋值运算符重载函数只能是类的非静态的成员函数,不能是静态成员函数,也不能是友元函数。关于原因,有人说,赋值运算符重载函数往往要返回*this,而无论是静态成员函数还是友元函数都没有this指针。这乍看起来很有道理,但仔细一想,我们完全可以写出这样的代码

static friend MyStr& operator=(const MyStr str1,const MyStr str2)
{
    ……
    return str1;
}

可见,这种说法并不能揭露C++这么规定的原因。

其实,之所以不是静态成员函数,是因为静态成员函数只能操作类的静态成员,不能操作非静态成员。如果我们将赋值运算符重载函数定义为静态成员函数,那么,该函数将无法操作类的非静态成员,这显然是不可行的。

在前面的讲述中我们说过,当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动提供一个。现在,假设C++允许将赋值运算符重载函数定义为友元函数并且我们也确实这么做了,而且以类的引用为参数。与此同时,我们在类内却没有显式提供一个以本类或本类的引用为参数的赋值运算符重载函数。由于友元函数并不属于这个类,所以,此时编译器一看,类内并没有一个以本类或本类的引用为参数的赋值运算符重载函数,所以会自动提供一个。此时,我们再执行类似于str2=str1这样的代码,那么,编译器是该执行它提供的默认版本呢,还是执行我们定义的友元函数版本呢?
为了避免这样的二义性,C++强制规定,赋值运算符重载函数只能定义为类的成员函数,这样,编译器就能够判定是否要提供默认版本了,也不会再出现二义性。

Ⅹ. 赋值运算符重载函数不能被继承

见下面的例3

#include<iostream>
#include<string>
using namespace std;

class A
{
public:
    int X;
    A() {}
    A& operator =(const int x)
    {
        X = x;
        return *this;
    }    
};
class B :public A
{
public:
    B(void) :A() {}
};

int main()
{
    A a;
    B b;
    a = 45;
    //b = 67;
    (A)b = 67;
    return 0;
}

复制代码
注释掉的一句无法编译通过。报错提示:没有与这些操作数匹配的”=”运算符。对于b = 67;一句,首先,没有可供调用的构造函数(前面说过,在没有匹配的赋值运算符重载函数时,类似于该句的代码可以调用匹配的构造函数),此时,代码不能编译通过,说明父类的operator =函数并没有被子类继承。

为什么赋值运算符重载函数不能被继承呢?

因为相较于基类,派生类往往要添加一些自己的数据成员和成员函数,如果允许派生类继承基类的赋值运算符重载函数,那么,在派生类不提供自己的赋值运算符重载函数时,就只能调用基类的,但基类版本只能处理基类的数据成员,在这种情况下,派生类自己的数据成员怎么办?

所以,C++规定,赋值运算符重载函数不能被继承。
上面代码中, (A)b = 67; 一句可以编译通过,原因是我们将B类对象b强制转换成了A类对象。

Ⅺ.赋值运算符重载函数要避免自赋值

对于赋值运算符重载函数,我们要避免自赋值情况(即自己给自己赋值)的发生,一般地,我们通过比较赋值者与被赋值者的地址是否相同来判断两者是否是同一对象(正如例1中的if (this != &str)一句)。

为什么要避免自赋值呢?

为了效率。显然,自己给自己赋值完全是毫无意义的无用功,特别地,对于基类数据成员间的赋值,还会调用基类的赋值运算符重载函数,开销是很大的。如果我们一旦判定是自赋值,就立即return *this,会避免对其它函数的调用。

如果类的数据成员中含有指针,自赋值有时会导致灾难性的后果。对于指针间的赋值(注意这里指的是指针所指内容间的赋值,这里假设用_p给p赋值),先要将p所指向的空间delete掉(为什么要这么做呢?因为指针p所指的空间通常是new来的,如果在为p重新分配空间前没有将p原来的空间delete掉,会造成内存泄露),然后再为p重新分配空间,将_p所指的内容拷贝到p所指的空间。如果是自赋值,那么p和_p是同一指针,在赋值操作前对p的delete操作,将导致p所指的数据同时被销毁。那么重新赋值时,拿什么来赋?

所以,对于赋值运算符重载函数,一定要先检查是否是自赋值,如果是,直接return *this。

结束语:

至此,本文的所有内容都介绍完了。由于在下才疏学浅,错误纰漏之处在所难免,如果您在阅读的过程中发现了在下的错误和不足,请您务必指出。您的批评指正就是在下前进的不竭动力!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值