`

【C/C++和指针】类默认生成的四个函数

 
阅读更多

序:对于一个空类,编译器默认生成四个成员函数:默认构造函数、析构函数、拷贝构造函数、赋值函数
一,默认构造函数
默认构造函数(default constructor)就是在没有显式提供初始化式时调用的构造函数。它由不带参数的构造函数,或者为所有的形参提供默认实参的构造函数定义。如果定义某个类的变量时没有提供初始化式就会使用默认构造函数。
  如果用户定义的类中没有显式的定义任何构造函数,编译器就会自动为该类型生成默认构造函数,称为合成的构造函数(synthesized default constructor)。

C++语言为类提供的构造函数可自动完成对象的初始化任务
全局对象和静态对象的构造函数在main()函数执行之前就被调用,局部静态对象的构造函数是当程序第一次执行到相应语句时才被调用。然而给出一个外部对象的引用性声明时,并不调用相应的构造函数,因为这个外部对象只是引用在其他地方声明的对象,并没有真正地创建一个对象。   
C++的构造函数定义格式为:   
class <类名>   
{   
public:   <类名>(参数表)   //...(还可以声明其它成员函数)   
};   
<类名>::<函数名>(参数表)  
 {   //函数体   }   
如以下定义是合法的:   
class T   
{   
public:   T(int a=0){i=a;}//构造函数允许直接写在类定义内,也允许有参数表。   
private:int i;   
};   

二,析构函数
当程序员没有给类创建析构函数,那么系统会在类中自动创建一个析构函数,形式为:~A(){},为类A创建的析构函数。当程序执行完后,系统自动调用自动创建的析构函数,将对象释放。
默认的析构函数不能删除new运算符在自由存储器中分配的对象或对象成员。如果类成员占用的空间是在构造函数中动态分配的,我们就必须自定义析构函数,然后显式使用delete运算符来释放构造函数使用new运算符分配的内存,就像销毁普通变量一样
#include <iostream>
using namespace std;
class Pig
{
public:
Pig()
{
cout < < "Pig constructed " < <endl;
}
~Pig()
{
cout < < "Pig destructed " < <endl;
}
};
class Japanese:Pig
{


};
int main()
{
Japanese dog;
return 0;
}
输出:
Pig constructed
Pig destructed
如果改成一下new 生成的对象则不调用默认析构函数
int main()
{
Japanese *dog=new Japanese;
return 0;
}
输出就只有:
Pig constructed


三,拷贝构造函数
CExample(const CExample&); //参数是const 对象的引用&
【注意】如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。
以类String的两个对象a,b为例,假设a.m_data的内容为“hello”,b.m_data的内容为“world”。
现将a赋给b,缺省赋值函数的“位拷贝”意味着执行b.m_data = a.m_data。这将造成三个错误:一是b.m_data原有的内存没被释放,造成内存泄露;二是b.m_data和a.m_data指向同一块内存,a或b任何一方变动都会影响另一方;三是在对象被析构时,m_data被释放了两次。
1)默认拷贝构造函数
对于普通类型的对象来说,它们之间的复制是很简单的,例如:
int a=88;
int b=a; //复制
而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。
下面看一个类对象拷贝的简单例子。
#include <iostream>
using namespace std;


class CExample {
private:
 int a;
public:
 CExample(int b)
 { a=b;}
 void Show ()
 {
cout<<a<<endl;
}
};


int main()
{
 CExample A(100);
 CExample B=A;
 B.Show ();
 return 0;
}

运行程序,屏幕输出100。
系统为对象B分配了内存并完成了与对象A的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。下面举例说明拷贝构造函数的工作过程。
2)显式拷贝构造函数
#include <iostream>
using namespace std;


class CExample {
private:
int a;
public:
CExample(int b)
{ a=b;}

CExample(const CExample& C)//拷贝构造函数
{
a=C.a;
}
void Show ()
{
cout<<a<<endl;
}
};


int main()
{
CExample A(100);
CExample B=A;
B.Show ();
return 0;
}

CExample(constCExample& C)就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。例如:类X的拷贝构造函数的形式为X(X& x)。
当用一个已初始化过了的对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:
一个对象以值传递的方式传入函数体
一个对象以值传递的方式从函数返回
一个对象需要通过另外一个对象进行初始化。
如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,后面将进行说明。
浅拷贝和深拷贝
  在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
  深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
3)深拷贝 (主要应对类中有指针变量的情况)
#include <iostream>
using namespace std;
class CA
{
 public:
  CA(int b,char* cstr)
  {
   a=b;
   str=new char[b];
   strcpy(str,cstr);
  }
  CA(const CA& C)
  {
   a=C.a;
   str=new char[a]; //深拷贝
   if(str!=0)
     strcpy(str,C.str);
  }
  void Show()
  {
   cout<<str<<endl;
  }
  ~CA()
  {
   delete str;
  }
 private:
  int a;
  char *str;
};


int main()
{
 CA A(10,"Hello!");
 CA B=A;
 B.Show();
 return 0;
}

深拷贝:类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候
浅拷贝:对象存在资源,但复制过程并未复制资源的情况视为浅拷贝。
浅拷贝缺点:浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程序运行出错。
Test(Test &c_t)是自定义的拷贝构造函数,拷贝构造函数的名称必须与类名称一致,函数的形式参数是本类型的一个引用变量,且必须是引用。
当用一个已经初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用,如果你没有自定义拷贝构造函数的时候,系统将会提供给一个默认的拷贝构造函数来完成这个过程,上面代码的复制核心语句就是通过Test(Test &c_t)拷贝构造函数内的p1=c_t.p1;语句完成的。

四,赋值函数
每个类只有一个赋值函数  
由于并非所有的对象都会使用拷贝构造函数和赋值函数,程序员可能对这两个函数有些轻视。
   1,如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。
以类String的两个对象a,b为例,假设a.m_data的内容为“hello”,b.m_data的内容为“world”。   
现将a赋给b,缺省赋值函数的“位拷贝”意味着执行b.m_data = a.m_data。
这将造成三个错误:
一是b.m_data原有的内存没被释放,造成内存泄露;
二是b.m_data和a.m_data指向同一块内存,a或b任何一方变动都会影响另一方;
三是在对象被析构时,m_data被释放了两次。   
2,拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。以下程序中,第三个语句和第四个语句很相似,你分得清楚哪个调用了拷贝构造函数,哪个调用了赋值函数吗?  
  String a(“hello”);   
String b(“world”);
String c = a; // 调用了拷贝构造函数,最好写成 c(a);
  c = b; // 调用了赋值函数   
本例中第三个语句的风格较差,宜改写成String c(a) 以区别于第四个语句。   
类String的拷贝构造函数与赋值函数   
// 拷贝构造函数   
String::String(const String &other)   
{   
// 允许操作other的私有成员m_data   
int length = strlen(other.m_data);   
m_data = new char[length+1];  
  strcpy(m_data, other.m_data);  
  }   
// 赋值函数   
String & String::operator =(const String &other)   
{   // (1) 检查自赋值  
if(this == &other)  
return *this;   
// (2) 释放原有的内存资源
   delete [] m_data;   
// (3)分配新的内存资源,并复制内容  
  int length = strlen(other.m_data);  
  m_data = new char[length+1];   
strcpy(m_data, other.m_data);   
// (4)返回本对象的引用   
return *this;  
 }   
类String拷贝构造函数与普通构造函数的区别是:在函数入口处无需与NULL进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL。
 类String的赋值函数比构造函数复杂得多,分四步实现:   
(1)第一步,检查自赋值。你可能会认为多此一举,难道有人会愚蠢到写出 a = a 这样的自赋值语句!的确不会。但是间接的自赋值仍有可能出现,例如   // 内容自赋值   b = a;   …   c = b;   …   a = c;   // 地址自赋值   b = &a;   …   a = *b;   也许有人会说:“即使出现自赋值,我也可以不理睬,大不了化点时间让对象复制自己而已,反正不会出错!”   他真的说错了。看看第二步的delete,自杀后还能复制自己吗?所以,如果发现自赋值,应该马上终止函数。注意不要将检查自赋值的if语句   if(this == &other)   错写成为   if( *this == other)   
(2)第二步,用delete释放原有的内存资源。如果现在不释放,以后就没机会了,将造成内存泄露。   
(3)第三步,分配新的内存资源,并复制字符串。注意函数strlen返回的是有效字符串长度,不包含结束符‘\0’。函数strcpy则连‘\0’一起复制。   
(4)第四步,返回本对象的引用,目的是为了实现象 a = b = c 这样的链式表达。注意不要将 return *this 错写成 return this 。那么能否写成return other 呢?效果不是一样吗?    不可以!因为我们不知道参数other的生命期。有可能other是个临时对象,在赋值结束后它马上消失,那么return other返回的将是垃圾。   偷懒的办法处理拷贝构造函数与赋值函数   如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,怎么办?   


偷懒的办法是:只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。  
  例如:   
class A   { …  
  private:   A(const A &a); // 私有的拷贝构造函数
   A & operate =(const A &a); // 私有的赋值函数   
};   
如果有人试图编写如下程序:   
A b(a); // 调用了私有的拷贝构造函数  
 b = a; // 调用了私有的赋值函数   
编译器将指出错误,因为外界不可以操作A的私有函数。   注意:以上例子在vc中可能编译不过,因关键字不是operate ,而是operator   


3.在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值.



分享到:
评论

相关推荐

    gsoap 2.8 (SOAP/XML 关于C/C++ 语言的自动化实现工具内附 CSharp webservice例子,及GSOAP client和server例子)

    这些标志分布再四个类中:传输(IO),内容编码(ENC  ),XML编组(XML)及C/C++数据映射。不再提倡使用旧标志soap_disable_X及soap_enable_X(其中,X表示选项名)。具体内容请参见9.12节  。  3. gSoap2.x版与...

    -C++参考大全(第四版) (2010 年度畅销榜

    13.3 C++指针的类型检查 13.4 this指针 13.5 指向派生类型的指针 13.6 指向类成员的指针 13.7 引用 13.8 格式问题 13.9 C++的动态分配运算符 第14章 函数重载、拷贝构造函数和默认变元 14.1 函数重载 14.2 重载构造...

    新手学习C++入门资料

    这种情况多出现在用一个通用的函数指针调用多个函数的场合,其中有些函数不需要函数指针声明中的所有参数。看下面的例子: int fun(int x,int y) { return x*2; } 尽管这样的用法是正确的,但大多数C和C++的...

    谭浩强C语言程序设计,C++程序设计,严蔚敏数据结构,高一凡数据结构算法分析与实现.rar

    1.6 C和C++ 1.7 简单的C程序介绍 1.8 输入和输出函数 1.9 C源程序的结构特点 1.10 书写程序时应遵循的规则 1.11 C语言的字符集 1.12 C语言词汇 1.13 Turbo C 2.0 集成开发环境的使用 1.13.1 Turbo C 2.0 ...

    谭浩强C语言程序设计,C++程序设计,严蔚敏数据结构,高一凡数据结构算法分析与实现.rar )

    1.6 C和C++ 1.7 简单的C程序介绍 1.8 输入和输出函数 1.9 C源程序的结构特点 1.10 书写程序时应遵循的规则 1.11 C语言的字符集 1.12 C语言词汇 1.13 Turbo C 2.0 集成开发环境的使用 1.13.1 Turbo C 2.0 ...

    C++MFC教程

    CRect:用来表示矩形的类,拥有四个成员变量:top left bottom right。分别表是左上角和右下角的坐标。可以通过以下的方法构造: CRect( int l, int t, int r, int b ); 指明四个坐标 CRect( const RECT& srcRect )...

    static 用法

    C++重用了这个关键字,并赋予它与前面不同的第三种含义:表示属于一个类而不是属于此类的任何特定对象的变量和函数. 这是与普通成员函数的最大区别, 也是其应用所在, 比如在对某一个类的对象进行计数时, 计数生成...

    基于c++数字逻辑电子仿真器

    //全局变量pNodeNow是在IsPointInObject()这个函数里面记录的 //circlepoint和put是在IsInInput1() IsInInput2() IsInOutput1() //这三个函数中记录的 //判断此时触点时否己连接非常重要 if...

    传智播客扫地僧视频讲义源码

    本教程共分为5个部分,第一部分是C语言提高部分,第二部分为C++基础部分,第三部分为C++进阶部分,第四部分为C、C++及数据结构基础部分,第五部分为C_C++与设计模式基础,内容非常详细. 第一部分 C语言提高部分目录...

    数据结构(C++)有关练习题

    请编写一个函数将这个线性表原地逆置,即将数组的前n个原地址内容置换为(en-1,en-2,…,e3,e2,e1,e0)。 2、 针对带附加头结点的单链表,试编写下列函数: A. 定位函数Locate:在单链表中寻找第i个结点。若找到...

    基于C++开发的射击游戏

    VC++开发平台提供了两个函数:GetPrivateProfileSectionNames()和GetPrivateProfileString(),用来读取硬盘上的配置文件(.cfg),这样,每一架飞机的初始化信息可以写在.cfg文件中,通过一个循环算法来读取。...

    VC学习大纲 VC学习讲义

    AppWizard是一个原代码生成工具,是计算机辅助程序设计软件,Winmain在MFC程序中是如何从源程序中被隐藏的,theApp全局变量是如何被分配的,MFC框架中的几个类的作用与相互关系,MFC框架窗口是如何产生和销毁的,对...

    代码语法错误分析工具pclint8.0

    Windows平台下也有好多人都喜欢用SourceInsight编辑C/C++程序,如果将pclint集成到SourceInsight中,那就相当于给SourceInsight增加了一个C/C++编译器,而且它的检查更严格,能发现一些编译器发现不了的问题,可以...

    vld(Visual Leak Detector 内存泄露检测工具 源码)

    这个函数需要处理的事情是记录下此时的调用堆栈和此次堆内存分配的唯一标识——requestNumber。  得到当前的堆栈的二进制表示并不是一件很复杂的事情,但是因为不同体系结构、不同编译器、不同的函数调用约定所产生...

    Qt Creator 的安装和hello world 程序+其他程序的编写--不是一般的好

    加入的这个函数的作用就是移除字符串开头和结尾的空白字符。 12.最后,如果输入错误了,重新回到登录对话框时,我们希望可以使用户名和 密码框清空并且光标自动跳转到用户名输入框,最终的登录按钮的单击事件的槽 ...

    深入浅出MFC 2e

    四个重要的工具 内务府总管:Visual C++集成开发环境 关于project 关于工具设定 Source Browser Online Help 调试工具 VC++调试器 Exception Handling 程序代码产生器:AppWizard 东圈西点完成MFC程序骨干 威力强大...

    深入浅出MFC【侯捷】

    多线程程序设计实例 第2章 C++的重要性质 类及其成员——谈封装(encapsulation) 基类与派生类:谈继承(Inheritance) this指针 虚拟函数与多态(Polymorphism) 类与对象大解剖 Object slicing与虚拟函数 静态...

    侯捷- -深入浅出MFC

    四个重要的工具 内务府总管:Visual C++集成开发环境 关于project 关于工具设定 Source Browser Online Help 调试工具 VC++调试器 Exception Handling 程序代码产生器:AppWizard 东圈西点完成MFC程序骨干 威力强大...

    VC++6.0核心编程源码.rar

    建立这样一个列表时存在的问题是,你可以调用一个Windows函数,但是该函数能够在内部调用另一个函数,而这另一个函数又可以调用另一个函数,如此类推。由于各种不同的原因,这些函数中的任何一个函数都可能运行失败...

Global site tag (gtag.js) - Google Analytics