我爱编程C++

C++第四弹---构造函数&析构函数

2018-06-29  本文已影响23人  黄巴巴

类与对象

  1. 类与对象的区别
  1. 定义对象
    属于不同类的对象在不同的时刻、不同的地方分别被建立。全局对象在主函数开始执行前首先被建立,局部对象在程序执行遇到它们的对象定义时才被建立。与定义变量类似,定义对象时,c会为分配空间。

    例如,下面的代码定义了两个类,创建了类的全局对象、局部对象、静态对象和堆对象:

class Desk  //Desk类
{
public:
    int weight;
    int height;
    int width;
    int length;
};

class Stool  //另一个类: Stool类
{
public:
    int weight;
    int height;
    int width;
    int length;
};

Desk da;  //全局对象
Stool sa;
void fu()
{
    static Stool ss;  //静态局部对象
    Desk da;  //局部对象
    //...
}
void main()
{
    Stool bs;    //局部对象
    Desk *pd=new Desk;  //堆对象
    Desk nd[50];  //局部对象数组
    //...
    delete pd;  //释放对象
}
  1. 对象的初始化
    当对象在创建时获得了一个特定的值,我们说这个对象被初始化。初始化不是赋值,初始化的含义是创建变量赋予其一个初始值,而赋值的含义是把当前值擦除,而以一个新值来替代。对象初始化可以分为默认初始化、直接初始化、拷贝初始化以及值初始化。

    • 默认初始化:如果定义变量时没有指定初值,则变量被默认初始化。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。如果是内置类型的变量未被显示初始化,它的值由定义的位置决定,定义在任何函数体之外的变量被初始化为0。但是有一种例外,定义在函数体内部的内置类型变量将不被初始化。一个未被初始化的内置类型变量时未定义的,如果试图拷贝或以其他形式访问此变量将引发错误。
  int i1;//默认初始化,在函数体之外(初始化为0)
  int f(void)
  {
    int i2;//不被初始化,如果使用此对象则报错
  }

每个类各自决定了其初始化对象的方式。绝大数类支持无须显示的初始化而定义对象。默认调用该类的默认构造方法。

string empty;//empty非显示的初始化为一个空串,调用的是默认构造函数
string str1(10,'9');//直接初始化
string str2(str1);//直接初始化
string str3 = str1;//拷贝初始化
vector<int> v1(10);//10个元素,每个元素的初始化为0
vector<string> v2(10);//10个元素,每个元素都为空

使用new动态分配和初始化对象
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针:

int *pi = new int;//pi指向一个动态分配的,未初始化的无名对象

默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将使用默认构造函数进行初始化:

string *ps = new string;//初始化为空string
int *pi = new int;//pi指向一个未初始化的int

我们可以使用直接初始化方式来初始化一个动态分配的对象:

int *pi = new int(1024);//pi指向的对象的值为1024
string *ps = new string(10,'9');//*ps为"9999999999"

也可以对动态分配的对象进行值初始化,只需要在类型名后跟一对空括号即可:

string *ps1 = new string;//默认初始化为空string
string *ps2 = new string();//值初始化为空string
int *pi1 = new int;//默认初始化
int *pi2 = new int();//值初始化为0 

构造函数&析构函数

  1. 构造函数

类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。

构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。

下面的实例有助于更好地理解构造函数的概念:

#include <iostream>
 
using namespace std;
 
class Line
{
   public:
      void setLength( double len );
      double getLength( void );
      Line();  // 这是构造函数
 
   private:
      double length;
};
 
// 成员函数定义,包括构造函数
Line::Line(void)
{
    cout << "Object is being created" << endl;
}
 
void Line::setLength( double len )
{
    length = len;
}
 
double Line::getLength( void )
{
    return length;
}
// 程序的主函数
int main( )
{
   Line line;
 
   // 设置长度
   line.setLength(6.0); 
   cout << "Length of line : " << line.getLength() <<endl;
 
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Object is being created
Length of line : 6

注意:

  1. 默认构造函数
    当用户没有显式的去定义构造函数时, 编译器会为类生成一个默认的构造函数, 称为 "默认构造函数", 一旦你为你的类定义了构造函数,哪怕只是一个,那么编译器将不再生成默认的构造函数,默认构造函数不能完成对象数据成员的初始化, 只能给对象创建一标识符, 并为对象中的数据成员开辟一定的内存空间。
Object buffer[10]; // call default constructor
Object* buffer = new Object[10];
vector<Object> buffer;
class B
{
    B(int i){}
}; class A
{
    A(){}
    B b;
}; int main(void) 
{ 
    A a(); // error C2512: 'B' : no appropriate default constructor
available 
   getchar() ; 
   return 0 ; 
} 

再比如下面的代码,类A定义了拷贝构造函数,而没有提供默认的构造函数,B继承自A,所以B在初始化时要调用A的构造函数来初始化A,而A没有默认的构造函数,故产生编译错误。

class A
{
    A(const A&){}
}; class B : public A
{

}; int main(void) 
{ 
    B b; //error C2512:'B': no appropriate default constructor available
    getchar() ;
    return 0 ; 
} 

带参构造函数

默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值,如下面的例子所示:

#include <iostream>

    using namespace std;

    class Point
    {
        public:
            Point(int x = 0, int y = 0)     //带有默认参数的构造函数
            {
                cout<<"自定义的构造函数被调用...\n";
                xPos = x;         //利用传入的参数值对成员属性进行初始化
                yPos = y;
            }
            void printPoint()
            {
                cout<<"xPos = " << xPos <<endl;
                cout<<"yPos = " << yPos <<endl;
            }

        private:
            int xPos;
            int yPos;
    };

    int main()
    {
        Point M(10, 20);    //创建对象M并初始化xPos,yPos为10和20
        M.printPoint();

        Point N(200);       //创建对象N并初始化xPos为200, yPos使用参数y的默认值0
        N.printPoint();

        Point P;            //创建对象P使用构造函数的默认参数
        P.printPoint();

        return 0;
    }

编译运行的结果:

自定义的构造函数被调用...
        xPos = 10 
        yPos = 20 
自定义的构造函数被调用...
        xPos = 200  
        yPos = 0 
自定义的构造函数被调用...
        xPos = 0 
        yPos = 0 

代码说明:
在这个示例中的构造函数 Point(int x = 0, int y = 0) 使用了参数列表并且对参数进行了默认参数设置为0。在 main 函数中共创建了三个对象 M, N, P。
M对象不使用默认参数将M的坐标属性初始化10和20;
N对象使用一个默认参数y, xPos属性初始化为200;
P对象完全使用默认参数将xPos和yPos初始化为0。

        Point(int x = 0, int y = 0):xPos(x), yPos(y)  //使用初始化表
        {
            cout<<"调用初始化表对数据成员进行初始化!\n";
        }

在 Point 构造函数头的后面, 通过单个冒号 : 引出的就是初始化表, 初始化的内容为 Point 类中int型的 xPos 成员和 yPos成员, 其效果和 xPos = x; yPos = y; 是相同的。

初始化列表的成员初始化顺序:
C++初始化类成员时,是按照声明的顺序初始化的,
而不是按照出现在初始化列表中的顺序。

与在构造函数体内进行初始化不同的是, 使用初始化表进行初始化是在构造函数被调用以前就完成的。每个成员在初始化表中只能出现一次, 并且初始化的顺序不是取决于数据成员在初始化表中出现的顺序, 而是取决于在类中声明的顺序。

此外, 一些通过构造函数无法进行初始化的数据类型可以使用初始化表进行初始化, 如: 常量成员和引用成员, 这部分内容将在后面进行详细说明。使用初始化表对对象成员进行初始化的完整示例:

#include <iostream>
using namespace std; 4 

class Point      
{
  public:
    Point(int x = 0, int y = 0):xPos(x), yPos(y)
    {
      cout<<"调用初始化表对数据成员进行初始化!\n"; 
     }  
    void printPoint() 
    { 
      cout<<"xPos = " << xPos <<endl;
      cout<<"yPos = " << yPos <<endl; 
    } 
  private: 
         int xPos; 
         int yPos; 
}; 

int main()
{ 
  Point M(10, 20);    //创建对象M并初始化xPos,yPos为10和20
  M.printPoint(); 
  return 0; 
}

重载构造函数

在一个类中可以定义多个构造函数,以便提供不同的初始化的方法,供用户选用。这些构造函数具有相同的名字,而参数的个数或参数的类型不相同。这称为构造函数的重载。

例如:定义两个构造函数,其中一个无参数,一个有参数。

#include <iostream>
using namespace std;
class Box
{
   public : Box( ); //声明一个无参的构造函数
   //声明一个有参的构造函数,用参数的初始化表对数据成员初始化
   Box(int h,int w,int len):height(h),width(w),length(len){ }
   int volume( );
   private :
   int height;
   int width;
   int length;
};
Box::Box( ) //定义一个无参的构造函数
{
   height=10; width=10; length=10;
}
int Box::volume( ){
   return (height*width*length);
}
int main( )
{
   Box box1; //建立对象box1,不指定实参
   cout<<"The volume of box1 is "<<box1.volume( )<<endl;
   Box box2(15,30,25); //建立对象box2,指定3个实参
   cout<<"The volume of box2 is "<<box2.volume( )<<endl;
   return 0;
}

在本程序中定义了两个重载的构造函数,其实还可以定义其他重载构造函数,其原型声明可以为:
Box::Box(int h); //有1个参数的构造函数
Box::Box(int h,int w); //有两个参数的构造函数
在建立对象时分别给定1个参数和2个参数。

析构函数

与构造函数相反, 析构函数是在对象被撤销时被自动调用, 用于对成员撤销时的一些清理工作, 例如在前面提到的手动释放使用 new 或 malloc 进行申请的内存空间。析构函数具有以下特点:
■ 析构函数函数名与类名相同, 紧贴在名称前面用波浪号 ~ 与构造函数进行区分, 例如: ~Point();
■ 构造函数没有返回类型, 也不能指定参数, 因此析构函数只能有一个, 不能被重载;
■ 当对象被撤销时析构函数被自动调用, 与构造函数不同的是, 析构函数可以被显式的调用, 以释放对象中动态申请的内存。

当用户没有显式定义析构函数时, 编译器同样会为对象生成一个默认的析构函数, 但默认生成的析构函数只能释放类的普通数据成员所占用的空间, 无法释放通过 new 或 malloc 进行申请的空间, 因此有时我们需要自己显式的定义析构函数对这些申请的空间进行释放, 避免造成内存泄露。

下面的实例有助于更好地理解析构函数的概念:

#include <iostream>
 
using namespace std;
 
class Line
{
   public:
      void setLength( double len );
      double getLength( void );
      Line();   // 这是构造函数声明
      ~Line();  // 这是析构函数声明
 
   private:
      double length;
};
 
// 成员函数定义,包括构造函数
Line::Line(void)
{
    cout << "Object is being created" << endl;
}
Line::~Line(void)
{
    cout << "Object is being deleted" << endl;
}
 
void Line::setLength( double len )
{
    length = len;
}
 
double Line::getLength( void )
{
    return length;
}
// 程序的主函数
int main( )
{
   Line line;
 
   // 设置长度
   line.setLength(6.0); 
   cout << "Length of line : " << line.getLength() <<endl;
 
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Object is being created
Length of line : 6
Object is being deleted

总结:

上一篇下一篇

猜你喜欢

热点阅读