什么是匈牙利法则?我在ASP上看到的
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了什么是匈牙利法则?我在ASP上看到的相关的知识,希望对你有一定的参考价值。
匈牙利命名法匈牙利命名法是一种编程时的命名规范。基本原则是:变量名=属性+类型+对象描述,其中每一对象的名称都要求有明确含义,可以取对象名字全称或名字的一部分。命名要基于容易记忆容易理解的原则。保证名字的连贯性是非常重要的。
举例来说,表单的名称为form,那么在匈牙利命名法中可以简写为frm,则当表单变量名称为Switchboard时,变量全称应该为 frmSwitchboard。这样可以很容易从变量名看出Switchboard是一个表单,同样,如果此变量类型为标签,那么就应命名成 lblSwitchboard。可以看出,匈牙利命名法非常便于记忆,而且使变量名非常清晰易懂,这样,增强了代码的可读性,方便各程序员之间相互交流代码。
据说这种命名法是一位叫 Charles Simonyi 的匈牙利程序员发明的,后来他在微软呆了几年,于是这种命名法就通过微软的各种产品和文档资料向世界传播开了。现在,大部分程序员不管自己使用什么软件进行开发,或多或少都使用了这种命名法。这种命名法的出发点是把量名变按:属性+类型+对象描述的顺序组合起来,以使程序员作变量时对变量的类型和其它属性有直观的了解,下面是HN变量命名规范,其中也有一些是我个人的偏向:
属性部分
全局变量
g_
常量
c_
c++类成员变量
m_
静态变量
s_
类型部分
指针
p
函数
fn
无效
v
句柄
h
长整型
l
布尔
b
浮点型(有时也指文件)
f
双字
dw
字符串
sz
短整型
n
双精度浮点
d
计数
c(通常用cnt)
字符
ch(通常用c)
整型
i(通常用n)
字节
by
字
w
实型
r
无符号
u
描述部分
最大
Max
最小
Min
初始化
Init
临时变量
T(或Temp)
源对象
Src
目的对象
Dest
这里顺便写几个例子:
hwnd : h 是类型描述,表示句柄, wnd 是变量对象描述,表示窗口,所以 hwnd 表示窗口句柄;
pfnEatApple : pfn 是类型描述,表示指向函数的指针, EatApple 是变量对象描述,所以它表示
指向 EatApple 函数的函数指针变量。
g_cch : g_ 是属性描述,表示全局变量,c 和 ch 分别是计数类型和字符类型,一起表示变量类
型,这里忽略了对象描述,所以它表示一个对字符进行计数的全局变量。
上面就是HN命名法的一般规则。
小结:匈牙利命名法
匈牙利命名法
MFC、句柄、控件及结构的命名规范 Windows类型 样本变量 MFC类 样本变量
HWND hWnd; CWnd* pWnd;
HDLG hDlg; CDialog* pDlg;
HDC hDC; CDC* pDC;
HGDIOBJ hGdiObj; CGdiObject* pGdiObj;
HPEN hPen; CPen* pPen;
HBRUSH hBrush; CBrush* pBrush;
HFONT hFont; CFont* pFont;
HBITMAP hBitmap; CBitmap* pBitmap;
HPALETTE hPaltte; CPalette* pPalette;
HRGN hRgn; CRgn* pRgn;
HMENU hMenu; CMenu* pMenu;
HWND hCtl; CState* pState;
HWND hCtl; CButton* pButton;
HWND hCtl; CEdit* pEdit;
HWND hCtl; CListBox* pListBox;
HWND hCtl; CComboBox* pComboBox;
HWND hCtl; CScrollBar* pScrollBar;
HSZ hszStr; CString pStr;
POINT pt; CPoint pt;
SIZE size; CSize size;
RECT rect; CRect rect;
一般前缀命名规范 前缀 类型 实例
C 类或结构 CDocument,CPrintInfo
m_ 成员变量 m_pDoc,m_nCustomers
变量命名规范 前缀 类型 描述 实例
ch char 8位字符 chGrade
ch TCHAR 如果_UNICODE定义,则为16位字符 chName
b BOOL 布尔值 bEnable
n int 整型(其大小依赖于操作系统) nLength
n UINT 无符号值(其大小依赖于操作系统) nHeight
w WORD 16位无符号值 wPos
l LONG 32位有符号整型 lOffset
dw DWORD 32位无符号整型 dwRange
p * 指针 pDoc
lp FAR* 远指针 lpszName
lpsz LPSTR 32位字符串指针 lpszName
lpsz LPCSTR 32位常量字符串指针 lpszName
lpsz LPCTSTR 如果_UNICODE定义,则为32位常量字符串指针 lpszName
h handle Windows对象句柄 hWnd
lpfn callback 指向CALLBACK函数的远指针
前缀 符号类型 实例 范围
IDR_ 不同类型的多个资源共享标识 IDR_MAIINFRAME 1~0x6FFF
IDD_ 对话框资源 IDD_SPELL_CHECK 1~0x6FFF
HIDD_ 对话框资源的Help上下文 HIDD_SPELL_CHECK 0x20001~0x26FF
IDB_ 位图资源 IDB_COMPANY_LOGO 1~0x6FFF
IDC_ 光标资源 IDC_PENCIL 1~0x6FFF
IDI_ 图标资源 IDI_NOTEPAD 1~0x6FFF
ID_ 来自菜单项或工具栏的命令 ID_TOOLS_SPELLING 0x8000~0xDFFF
HID_ 命令Help上下文 HID_TOOLS_SPELLING 0x18000~0x1DFFF
IDP_ 消息框提示 IDP_INVALID_PARTNO 8~0xDEEF
HIDP_ 消息框Help上下文 HIDP_INVALID_PARTNO 0x30008~0x3DEFF
IDS_ 串资源 IDS_COPYRIGHT 1~0x7EEF
IDC_ 对话框内的控件 IDC_RECALC 8~0xDEEF
Microsoft MFC宏命名规范 名称 类型
_AFXDLL 唯一的动态连接库(Dynamic Link Library,DLL)版本
_ALPHA 仅编译DEC Alpha处理器
_DEBUG 包括诊断的调试版本
_MBCS 编译多字节字符集
_UNICODE 在一个应用程序中打开Unicode
AFXAPI MFC提供的函数
CALLBACK 通过指针回调的函数
库标识符命名法 标识符 值和含义
u ANSI(N)或Unicode(U)
d 调试或发行:D = 调试;忽略标识符为发行。
静态库版本命名规范 库 描述
NAFXCWD.LIB 调试版本:MFC静态连接库
NAFXCW.LIB 发行版本:MFC静态连接库
UAFXCWD.LIB 调试版本:具有Unicode支持的MFC静态连接库
UAFXCW.LIB 发行版本:具有Unicode支持的MFC静态连接库
动态连接库命名规范 名称 类型
_AFXDLL 唯一的动态连接库(DLL)版本
WINAPI Windows所提供的函数
Windows.h中新的命名规范 类型 定义描述
WINAPI 使用在API声明中的FAR PASCAL位置,如果正在编写一个具有导出API人口点的DLL,则可以在自己的API中使用该类型
CALLBACK 使用在应用程序回叫例程,如窗口和对话框过程中的FAR PASCAL的位置
LPCSTR 与LPSTR相同,只是LPCSTR用于只读串指针,其定义类似(const char FAR*)
UINT 可移植的无符号整型类型,其大小由主机环境决定(对于Windows NT和Windows 9x为32位);它是unsigned int的同义词
LRESULT 窗口程序返回值的类型
LPARAM 声明lParam所使用的类型,lParam是窗口程序的第四个参数
WPARAM 声明wParam所使用的类型,wParam是窗口程序的第三个参数
LPVOID 一般指针类型,与(void *)相同,可以用来代替LPSTR
--------------------------------------------------------------------------------
抨击匈牙利命名法
匈牙利命名法是一种编程时的命名规范。命名规范是程序书写规范中最重要也是最富争议的地方,自古乃兵家必争之地。命名规范有何用?四个字:名正言顺。用二分法,命名规范分为好的命名规范和坏的命名规范,也就是说名正言顺的命名规范和名不正言不顺的命名规范。好的舞鞋是让舞者感觉不到其存在的舞鞋,坏的舞鞋是让舞者带着镣铐起舞。一个坏的命名规范具有的破坏力比一个好的命名规范具有的创造力要大得多。
本文要证明的是:匈牙利命名法是一个坏的命名规范。本文的作用范围为静态强类型编程语言。本文的分析范本为C语言和C++语言。下文中的匈法为匈牙利命名法的简称。
一 匈牙利命名法的成本
匈法的表现形式为给变量名附加上类型名前缀,例如:nFoo,szFoo,pFoo,cpFoo分别表示整型变量,字符串型变量,指针型变量和常指针型变量。可以看出,匈法将变量的类型信息从单一地点(声明变量处)复制到了多个地点(使用变量处),这是冗余法。冗余法的成本之一是要维护副本的一致性。这个成本在编写和维护代码的过程中需要改变变量的类型时付出。冗余法的成本之二是占用了额外的空间。一个优秀的书写者会自觉地遵从一个法则:代码最小组织单位的长度以30个自然行以下为宜,如果超过50行就应该重新组织。一个变量的书写空间会给这一法则添加不必要的难度。
二 匈牙利命名法的收益
这里要证明匈牙利命名法的收益是含糊的,无法预期的。
范本1:strcpy(pstrFoo,pcstrFoo2) Vs strcpy(foo,foo2)
匈法在这里有什么收益呢?我看不到。没有一个程序员会承认自己不知道strcpy函数的参数类型吧。
范本2:unknown_function(nFoo) Vs unknown_function(foo)
匈法在这里有什么收益呢?我看不到。对于一个不知道确定类型的函数,程序员应该去查看该函数的文档,这是一种成本。使用匈法的唯一好处是看代码的人知道这个函数要求一个整型参数,这又有什么用处呢?函数是一种接口,参数的类型仅仅是接口中的一小部分。诸如函数的功能、出口信息、线程安全性、异常安全性、参数合法性等重要信息还是必须查阅文档。
范本3:nFoo=nBar Vs foo=bar
匈法在这里有什么收益呢?我看不到。使用匈法的唯一好处是看代码的人知道这里发生了一个整型变量的复制动作,听起来没什么问题,可以安心睡大觉了。如果他看到的是nFoo=szBar,可能会从美梦中惊醒。且慢,事情真的会是这样吗?我想首先被惊醒的应该是编译器。另一方面,nFoo=nBar只是在语法上合法而已,看代码的人真正关心的是语义的合法性,匈法对此毫无帮助。另一方面,一个优秀的书写者会自觉地遵从一个法则:代码最小组织单位中的临时变量以一两个为宜,如果超过三个就应该重新组织。结合前述第一个法则,可以得出这样的结论:易于理解的代码本身就应该是易于理解的,这是代码的内建高质量。好的命名规范对内建高质量的助益相当有限,而坏的命名规范对内建高质量的损害比人们想象的要大。
三 匈牙利命名法的实施
这里要证明匈牙利命名法在C语言是难以实施的,在C++语言中是无法实施的。从逻辑上讲,对匈法的收益做出否定的结论以后,再来论证匈法的可行性,是画蛇添足。不过有鉴于小马哥曾让已射杀之敌死灰复燃,我还是再踏上一支脚为妙。
前面讲过,匈法是类型系统的冗余,所以实施匈法的关键是我们是否能够精确地对类型系统进行复制。这取决于类型系统的复杂性。
先来看看C语言:
1.内置类型:int,char,float,double 复制为 n,ch,f,d?好像没有什么问题。不过谁来告诉我void应该怎么表示?
2.组合类型:array,union,enum,struct 复制为 a,u,e,s?好像比较别扭。
这里的难点不是为主类型取名,而是为副类型取名。an表示整型数组?sfoo,sbar表示结构foo,结构bar?ausfoo表示联合结构foo数组?累不累啊。
3.特殊类型:pointer。pointer在理论上应该是组合类型,但是在C语言中可以认为是内置类型,因为C语言并没有非常严格地区分不同的指针类型。下面开始表演:pausfoo表示联合结构foo数组指针?ppp表示指针的指针的指针?
噩梦还没有结束,再来看看类型系统更阿为丰富的C++语言:
1.class:如果说C语言中的struct还可以用stru搪塞过去的话,不要梦想用cls来搪塞C++中的class。严格地讲,class根本就并不是一个类型,而是创造类型的工具,在C++中,语言内置类型的数量和class创造的用户自定义类型的数量相比完全可以忽略不计。stdvectorFoo表示标准库向量类型变量Foo?疯狂的念头。
2.命名空间:boostfilesystemiteratorFoo,表示boost空间filesystem子空间遍历目录类型变量Foo?程序员要崩溃了。
3.模板:你记得std::map<std::string,std::string>类型的确切名字吗?我是记不得了,好像超过255个字符,还是饶了我吧。
4.模板参数:template <class T, class BinaryPredicate>const T& max(const T& a, const T& b, BinaryPredicate comp) 聪明的你,请用匈法为T命名。上帝在发笑。
5.类型修饰:static,extern,mutable,register,volatile,const,short,long,unsigned 噩梦加上修饰是什么?还是噩梦。百度地图
本数据来源于百度地图,最终结果以百度地图最新数据为准。
参考资料:http://www.baidu.com/
参考技术A 匈牙利法则是程序的一个命名约定由生于匈牙利布达佩斯的Charles Simonyi 开发的,它通过在数据和函数名中加入额外的
信息以增进程序员对程序的理解。例如:
char ch; /* 所有的字符变量均以ch 开始 */
byte b; /* 所有的字节均冠以b */
long l; /* 所有的长字均冠以l */
对于指向某个数据类型的指针,可以先象上面那样建立一个有类型的名字,然后给该
名字加上前缀字母P:
char* pch; /* 指向ch 的指针以p开始 */
byte* pb; /* 同理 */
long* pl;
void* pv; /* 特意显用的空指针 */
char** ppch; /* 指向字符指针的指针 */
byte** ppb; /* 指向字节指针的指针 */
匈牙利式名字通常不那么好念,但在代码中读到它们时,确实可以从中得到许多的信息。例如,当你眼看到某个函数里有一个名为pch 的变量时,不用查看声明就立即知道它
是一个指向字符的指针。
为了使匈牙利式名字的描述性更强.或者要区分两个变量名,可以在相应类型派生出
的基本名字之后加上一个以大写字母开头的“标签”。例如,strcpy 函数有两个字符指针
参数:一个是源指针,另一个是目的指针。使用匈牙利式命名约定,其相应的原型是:
char* strcpy(char* pchTo, char* pchFrom); /* 原型 */
在上面的例子中,两个字符指针有一个共同的特点 —— 都指向以0 为结尾的C 的字
符串。因此在本书中,每当用字符指针指向字符串时,我们就用一个更有意义的名子str
来表示。因此,上述strcpy 的原型则为:
char* strcpy(char* strTo, char* strFrom) /* 原型 */
本书用到另一个类型是ANSI 标准中的类型size_t。下面给出该类型的一些典型用
法:
size_t sizeNew, sizeOld; /*
原型 */
void* malloc(size_t size); /*
原型 */
void* realloc(void* pv, size_t sizeNew); /* 原型 */
函数和数组的命名遵循同样的约定,名字由相应的返回类型名开始,后跟一个描述的
标签。例如:
编写优化、高效、无错地代码 7
ch = chLastKeyPressed; /* 由变量得一字符 */
ch = chInputBuffer[]; /* 由数组得一字符 */
ch = chReadKeyboard; /* 由函数得一字符 */
如果利用匈牙利式命名方法,mall~和reali~可以写成如下形式:
void* pvNewBlock(size_t size); /* 原型 */
void* pvResizeBlock(void* pv, size_t sizeNew); /* 原型 */
由于匈牙利式命名方法旨在增进程序员对程序的理解,所以大多数匈牙利式名字的长
度都要超过ANSI 严格规定6 个字母的限制。这就不妙,除非所用的系统是几十年前设计的
系统,否则这6 个字母的限制只当是历史的遗迹。
以上内容基本上没有涉及到匈牙利式命名约定的细节,所介绍的都是读者理解本书中所用
变量和函数名称意义的必需内容。如果读者对匈牙利式命名约定的详细内容感兴趣,可以
什么是三法则?
【中文标题】什么是三法则?【英文标题】:What is The Rule of Three? 【发布时间】:2010-11-13 13:27:09 【问题描述】: 复制对象是什么意思? 什么是拷贝构造函数和拷贝赋值运算符? 我什么时候需要自己申报? 如何防止我的对象被复制?【问题讨论】:
请在投票结束前阅读this whole thread和thec++-faq
tag wiki。
@Binary:在您投票之前至少花点时间阅读评论讨论。文本过去要简单得多,但 Fred 被要求对其进行扩展。此外,虽然这是 语法上的四个问题,但它实际上只是一个包含多个方面的问题。 (如果您不同意这一点,请通过单独回答每个问题来证明您的 POV,然后让我们对结果进行投票。)
相关:The Law of The Big Two
请记住,从 C++11 开始,我认为这已升级为五规则,或类似的规则。
@paxdiablo The Rule of Zero 准确地说。
【参考方案1】:
简介
C++ 使用值语义处理用户定义类型的变量。 这意味着对象在各种上下文中被隐式复制, 我们应该明白“复制对象”的真正含义。
让我们考虑一个简单的例子:
class person
std::string name;
int age;
public:
person(const std::string& name, int age) : name(name), age(age)
;
int main()
person a("Bjarne Stroustrup", 60);
person b(a); // What happens here?
b = a; // And here?
(如果您对name(name), age(age)
部分感到困惑,
这称为member initializer list。)
特殊成员函数
复制person
对象是什么意思?
main
函数显示了两种不同的复制场景。
初始化person b(a);
由复制构造函数 执行。
它的工作是根据现有对象的状态构造一个新对象。
赋值b = a
由复制赋值运算符 执行。
它的工作通常稍微复杂一点,
因为目标对象已经处于某种需要处理的有效状态。
由于我们自己既没有声明复制构造函数也没有声明赋值运算符(也没有析构函数), 这些是为我们隐含定义的。引用标准:
[...] 复制构造函数和复制赋值运算符,[...] 和析构函数是特殊的成员函数。 [ 注意:实现会隐式声明这些成员函数 对于某些类类型,当程序没有显式声明它们时。 如果使用它们,实现将隐式定义它们。 [...] 结束说明 ] [n3126.pdf 第 12 节 §1]
默认情况下,复制一个对象意味着复制它的成员:
非联合类 X 的隐式定义的复制构造函数执行其子对象的成员复制。 [n3126.pdf 第 12.8 节第 16 节]
非联合类 X 的隐式定义的复制赋值运算符执行成员复制赋值 的子对象。 [n3126.pdf 第 12.8 节第 30 节]
隐式定义
person
的隐式定义的特殊成员函数如下所示:
// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
// 2. copy assignment operator
person& operator=(const person& that)
name = that.name;
age = that.age;
return *this;
// 3. destructor
~person()
Memberwise 复制正是我们在这种情况下想要的:
name
和 age
被复制,所以我们得到一个自包含、独立的 person
对象。
隐式定义的析构函数始终为空。
在这种情况下这也很好,因为我们没有在构造函数中获取任何资源。
person
析构函数完成后,成员的析构函数被隐式调用:
在执行析构函数的主体并销毁主体内分配的所有自动对象后, X 类的析构函数调用 X 的直接 [...] 成员的析构函数 [n3126.pdf 12.4 §6]
管理资源
那么我们什么时候应该显式地声明这些特殊的成员函数呢? 当我们的类管理一个资源,也就是 当类的对象对那个资源负责时。 这通常意味着资源在构造函数中被获取 (或传入构造函数)并在析构函数中释放。
让我们回到标准前的 C++。
没有 std::string
这样的东西,程序员都爱上了指针。
person
类可能看起来像这样:
class person
char* name;
int age;
public:
// the constructor acquires a resource:
// in this case, dynamic memory obtained via new[]
person(const char* the_name, int the_age)
name = new char[strlen(the_name) + 1];
strcpy(name, the_name);
age = the_age;
// the destructor must release this resource via delete[]
~person()
delete[] name;
;
即使在今天,人们仍然以这种风格编写课程并遇到麻烦:
“我将一个人推入向量中,现在我出现了疯狂的记忆错误!”
请记住,默认情况下,复制对象意味着复制其成员,
但是复制name
成员只是复制一个指针,不是它指向的字符数组!
这有几个不愉快的影响:
-
可以通过
b
观察通过a
进行的更改。
一旦b
被销毁,a.name
就是一个悬空指针。
如果a
被销毁,则删除悬空指针会产生undefined behavior。
由于赋值没有考虑到赋值前name
指向的内容,
迟早你会到处出现内存泄漏。
显式定义
由于按成员复制没有达到预期的效果,我们必须明确定义复制构造函数和复制赋值运算符来对字符数组进行深度复制:
// 1. copy constructor
person(const person& that)
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
// 2. copy assignment operator
person& operator=(const person& that)
if (this != &that)
delete[] name;
// This is a dangerous point in the flow of execution!
// We have temporarily invalidated the class invariants,
// and the next statement might throw an exception,
// leaving the object in an invalid state :(
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
return *this;
注意初始化和赋值的区别:
我们必须在分配给name
之前拆除旧状态,以防止内存泄漏。
此外,我们必须防止x = x
形式的自我分配。
如果没有该检查,delete[] name
将删除包含 source 字符串的数组,
因为当你写x = x
时,this->name
和that.name
都包含相同的指针。
异常安全
很遗憾,如果new char[...]
由于内存耗尽而引发异常,此解决方案将失败。
一种可能的解决方案是引入一个局部变量并重新排序语句:
// 2. copy assignment operator
person& operator=(const person& that)
char* local_name = new char[strlen(that.name) + 1];
// If the above statement throws,
// the object is still in the same state as before.
// None of the following statements will throw an exception :)
strcpy(local_name, that.name);
delete[] name;
name = local_name;
age = that.age;
return *this;
这也可以在没有明确检查的情况下处理自分配。 这个问题的一个更强大的解决方案是copy-and-swap idiom, 但我不会在这里详细介绍异常安全。 我只提到例外是为了说明以下几点:编写管理资源的类很难。
不可复制的资源
某些资源不能或不应该被复制,例如文件句柄或互斥体。
在这种情况下,只需将复制构造函数和复制赋值运算符声明为 private
而不给出定义:
private:
person(const person& that);
person& operator=(const person& that);
或者,您可以从 boost::noncopyable
继承或将它们声明为已删除(在 C++11 及更高版本中):
person(const person& that) = delete;
person& operator=(const person& that) = delete;
三法则
有时您需要实现一个管理资源的类。 (永远不要在一个类中管理多个资源, 这只会导致痛苦。) 在这种情况下,请记住三法则:
如果您需要显式声明析构函数, 自己复制构造函数或复制赋值运算符, 您可能需要明确声明所有三个。
(不幸的是,C++ 标准或我知道的任何编译器都没有强制执行此“规则”。)
五法则
从 C++11 开始,对象有 2 个额外的特殊成员函数:移动构造函数和移动赋值。实现这些功能的五态规则也是如此。
签名示例:
class person
std::string name;
int age;
public:
person(const std::string& name, int age); // Ctor
person(const person &) = default; // 1/5: Copy Ctor
person(person &&) noexcept = default; // 4/5: Move Ctor
person& operator=(const person &) = default; // 2/5: Copy Assignment
person& operator=(person &&) noexcept = default; // 5/5: Move Assignment
~person() noexcept = default; // 3/5: Dtor
;
零法则
3/5 规则也称为 0/3/5 规则。规则的零部分规定,在创建类时,您可以不编写任何特殊成员函数。
建议
大多数时候,您不需要自己管理资源,
因为诸如std::string
之类的现有课程已经为您完成了。
只需使用std::string
成员比较简单代码
对于使用char*
的复杂且容易出错的替代方案,您应该被说服。
只要您远离原始指针成员,三法则就不太可能涉及您自己的代码。
【讨论】:
Fred,如果 (A) 你不会在可复制代码中拼出错误实现的赋值并添加一个说明它错误的注释并在细则中查看其他地方,我会对我的投票感觉更好;要么在代码中使用 c&s 要么跳过实现所有这些成员 (B) 你会缩短前半部分,这与 RoT 无关; (C) 你会讨论引入移动语义以及这对 RoT 意味着什么。 但是我认为帖子应该是 C/W。我喜欢你保持这些术语大部分准确(即你说“copy assignment operator”,并且你没有进入一个常见的陷阱,即赋值不能暗示一个副本)。跨度> @Prasoon:我不认为删掉一半的答案会被视为对非 CW 答案的“公平编辑”。 如果你为 C++11 更新你的帖子会很棒(即移动构造函数/赋值) @solalito 使用后必须释放的任何东西:并发锁、文件句柄、数据库连接、网络套接字、堆内存...【参考方案2】:Rule of Three 是 C++ 的经验法则,基本上是说
如果你的班级需要任何一个
一个复制构造函数, 一个赋值运算符, 或析构函数,明确定义,那么很可能需要全部三个。
原因是它们三个通常都用于管理资源,如果您的类管理资源,则通常需要管理复制和释放。
如果复制您的类管理的资源没有良好的语义,则考虑通过将复制构造函数和赋值运算符声明为(不是defining)private
来禁止复制。
(请注意,即将发布的新版 C++ 标准(即 C++11)为 C++ 添加了移动语义,这可能会改变三法则。但是,我对此知之甚少,无法编写 C+ +11 关于三法则的部分。)
【讨论】:
防止复制的另一种解决方案是(私下)从无法复制的类(如boost::noncopyable
)继承。它也可以更清晰。我认为 C++0x 和“删除”函数的可能性在这里会有所帮助,但忘记了语法:/
@Matthieu:是的,这也有效。但是除非noncopyable
是标准库的一部分,否则我不认为这是一个很大的改进。 (哦,如果你忘记了删除语法,那你就忘记了我所知道的更多。:)
)
@Daan:见this answer。但是,我建议坚持使用Martinho 的Rule of Zero。对我来说,这是过去十年创造的 C++ 最重要的经验法则之一。
Martinho 的零规则现在更好(没有明显的广告软件接管)位于 archive.org【参考方案3】:
三大定律如上。
一个简单的例子,用简单的英语,它解决了什么样的问题:
非默认析构函数
您在构造函数中分配了内存,因此您需要编写一个析构函数来删除它。否则会导致内存泄漏。
您可能认为这已经完成了。
问题是,如果对您的对象进行了复制,那么该副本将指向与原始对象相同的内存。
一旦其中一个删除了其析构函数中的内存,另一个将有一个指向无效内存的指针(这称为悬空指针),当它尝试使用它时,事情会变得很糟糕。
因此,您编写了一个复制构造函数,以便为新对象分配它们自己的内存块来销毁。
赋值运算符和复制构造函数
您在构造函数中为类的成员指针分配了内存。当你复制这个类的一个对象时,默认的赋值运算符和复制构造函数会将这个成员指针的值复制到新的对象中。
这意味着新对象和旧对象将指向同一块内存,因此当您在一个对象中更改它时,另一个对象也会更改。如果一个对象删除了这个内存,另一个对象将继续尝试使用它 - eek。
要解决这个问题,您需要编写自己版本的复制构造函数和赋值运算符。您的版本为新对象分配单独的内存并复制第一个指针指向的值而不是其地址。
【讨论】:
因此,如果我们使用复制构造函数,则复制是在完全不同的内存位置进行的,如果我们不使用复制构造函数,则复制是指向同一个内存位置的。这就是你想说的吗?因此,没有复制构造函数的副本意味着会有一个新指针存在但指向相同的内存位置,但是如果我们有用户明确定义的复制构造函数,那么我们将有一个单独的指针指向不同的内存位置但具有数据。 对不起,我很久以前就回复了这个,但我的回复似乎还没有在这里 :-( 基本上,是的 - 你明白了 :-)【参考方案4】:基本上,如果你有一个析构函数(不是默认析构函数),这意味着你定义的类有一些内存分配。假设该类被某些客户端代码或您在外部使用。
MyClass x(a, b);
MyClass y(c, d);
x = y; // This is a shallow copy if assignment operator is not provided
如果 MyClass 只有一些基本类型的成员,则默认赋值运算符会起作用,但如果它有一些指针成员和没有赋值运算符的对象,则结果将是不可预测的。因此我们可以说,如果在类的析构函数中有要删除的东西,我们可能需要一个深拷贝操作符,这意味着我们应该提供一个拷贝构造函数和赋值操作符。
【讨论】:
【参考方案5】:复制一个对象是什么意思? 有几种方法可以复制对象——让我们谈谈你最有可能指的 2 种——深拷贝和浅拷贝。
由于我们使用的是面向对象的语言(或者至少假设是这样),假设您分配了一块内存。由于它是一种面向对象语言,我们可以轻松地引用我们分配的内存块,因为它们通常是原始变量(整数、字符、字节)或我们定义的由我们自己的类型和原语组成的类。所以假设我们有一个 Car 类如下:
class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
private String sPrintColor;
private String sModel;
private String sMake;
public changePaint(String newColor)
this.sPrintColor = newColor;
public Car(String model, String make, String color) //Constructor
this.sPrintColor = color;
this.sModel = model;
this.sMake = make;
public ~Car() //Destructor
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
public Car(const Car &other) // Copy Constructor
this.sPrintColor = other.sPrintColor;
this.sModel = other.sModel;
this.sMake = other.sMake;
public Car &operator =(const Car &other) // Assignment Operator
if(this != &other)
this.sPrintColor = other.sPrintColor;
this.sModel = other.sModel;
this.sMake = other.sMake;
return *this;
深拷贝是如果我们声明一个对象,然后创建一个完全独立的对象副本......我们最终会在 2 组完整的内存中得到 2 个对象。
Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.
现在让我们做一些奇怪的事情。假设 car2 要么编程错误,要么故意共享 car1 的实际内存。 (这样做通常是一个错误,并且在课堂上通常是它在下面讨论的毯子。)假装每当你询问 car2 时,你真的在解析指向 car1 内存空间的指针......这或多或少是一个浅拷贝是。
//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.
Car car1 = new Car("ford", "mustang", "red");
Car car2 = car1;
car2.changePaint("green");//car1 is also now green
delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve
the address of where car2 exists and delete the memory...which is also
the memory associated with your car.*/
car1.changePaint("red");/*program will likely crash because this area is
no longer allocated to the program.*/
因此,无论您使用哪种语言编写,在复制对象时都要非常小心您的意思,因为大多数时候您想要一个深层副本。
什么是复制构造函数和复制赋值运算符?
我已经在上面使用过它们。当您键入诸如Car car2 = car1;
之类的代码时,就会调用复制构造函数。本质上,如果您声明一个变量并在一行中对其进行赋值,那就是调用复制构造函数的时候。赋值运算符是使用等号时发生的情况 --car2 = car1;
。注意car2
没有在同一个语句中声明。您为这些操作编写的两段代码可能非常相似。事实上,典型的设计模式有另一个函数,一旦你满意初始复制/分配是合法的,你就可以调用它来设置一切——如果你看一下我写的速记代码,这些函数几乎是相同的。
我什么时候需要自己申报? 如果您不以某种方式编写要共享或用于生产的代码,那么您实际上只需要在需要它们时声明它们。如果您“偶然”选择使用它并且没有使用它,您确实需要知道您的程序语言会做什么 - 即你得到编译器的默认值。例如,我很少使用复制构造函数,但赋值运算符覆盖很常见。您知道您也可以覆盖加法、减法等的含义吗?
如何防止我的对象被复制? 使用私有函数覆盖允许为对象分配内存的所有方式是一个合理的开始。如果您真的不希望人们复制它们,您可以将其公开并通过引发异常而不复制对象来提醒程序员。
【讨论】:
问题被标记为 C++。这个伪代码说明充其量只能说明明确定义的“三法则”,而最坏的情况只是散布混乱。【参考方案6】:我什么时候需要自己声明?
三法则规定,如果您声明任何一个
-
复制构造函数
复制赋值运算符
析构函数
那么你应该声明所有三个。它源于以下观察:接管复制操作含义的需要几乎总是源于执行某种资源管理的类,这几乎总是暗示
在一个复制操作中进行的任何资源管理都可能需要在另一复制操作中完成,并且
类析构函数也将参与资源的管理(通常是释放它)。要管理的经典资源是内存,这就是为什么所有标准库类 管理内存(例如,执行动态内存管理的 STL 容器)都声明了“三巨头”:复制操作和析构函数。
三规则的结果是用户声明的析构函数的存在表明简单的成员明智复制不太可能适合类中的复制操作。反过来,这表明如果一个类声明了一个析构函数,则可能不应该自动生成复制操作,因为它们不会做正确的事情。在采用 C++98 的时候,这种推理的重要性还没有被完全理解,所以在 C++98 中,用户声明的析构函数的存在对编译器生成复制操作的意愿没有影响。在 C++11 中仍然如此,但这只是因为限制生成复制操作的条件会破坏太多遗留代码。
如何防止我的对象被复制?
将复制构造函数和复制赋值运算符声明为私有访问说明符。
class MemoryBlock
public:
//code here
private:
MemoryBlock(const MemoryBlock& other)
cout<<"copy constructor"<<endl;
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
return *this;
;
int main()
MemoryBlock a;
MemoryBlock b(a);
在 C++11 之后,您还可以声明复制构造函数和赋值运算符已删除
class MemoryBlock
public:
MemoryBlock(const MemoryBlock& other) = delete
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
;
int main()
MemoryBlock a;
MemoryBlock b(a);
【讨论】:
【参考方案7】:许多现有答案已经涉及复制构造函数、赋值运算符和析构函数。 然而,在 C++11 之后,引入移动语义可能会将其扩展到 3 之外。
最近 Michael Claisse 做了一个涉及这个话题的演讲: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class
【讨论】:
【参考方案8】:C++中的三法则是设计和开发三个要求的基本原则,即如果以下成员函数之一有明确定义,则程序员应将其他两个成员函数一起定义。即以下三个成员函数是必不可少的:析构函数、复制构造函数、复制赋值运算符。
C++ 中的复制构造函数是一个特殊的构造函数。用于构建新对象,新对象相当于现有对象的副本。
复制赋值运算符是一种特殊的赋值运算符,通常用于将现有对象指定给其他同类型对象。
有一些简单的例子:
// default constructor
My_Class a;
// copy constructor
My_Class b(a);
// copy constructor
My_Class c = a;
// copy assignment operator
b = a;
【讨论】:
您好,您的回答没有添加任何新内容。其他人更深入,更准确地涵盖了该主题-您的答案是近似的,实际上在某些地方是错误的(即这里没有“必须”;它是“很可能应该”)。对于已经彻底回答的问题,发布这种答案真的不值得。除非你有新的东西要添加。 另外,还有四个快速示例,它们在某种程度上与三个两个相关/i> 三法则所说的。太混乱了。以上是关于什么是匈牙利法则?我在ASP上看到的的主要内容,如果未能解决你的问题,请参考以下文章