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

类与对象

  1. 类与对象的区别
  • 类是对某一类事物的描述,是抽象的;
    而对象是一个实实在在的个体,是类的一个实例。
    比如:“人”是一个类,而“教师”则是“人”的一个实例。

  • 对象是函数、变量的集合体;
    而类是一组函数和变量的集合体,
    即类是一组具有相同属性的对象集合体。

  • UML的类图和对象图之间的区别是:
    类图中类名首字母大写,对象图中的对象名首字母小写。
    对象名下有一条下划线,而类名没有。

  • 类的数据值是共享的,一个实例能访问它所属类的类数据值;
    而实例数据属于单个对象,除共享了所在类中的数据外,
    不同对象还会有不同的数据值。

  • 先有类,才有类的实例——对象。
    应用在:当你在创建某个类的实例(对象)之前,这个类必须被定义。

  • 实例方法和类方法的区别在于:
    实例方法属于单个对象,类方法属于类。

  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非显示的初始化为一个空串,调用的是默认构造函数
  • 拷贝初始化:使用等号(=)初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去,拷贝初始化通常使用拷贝构造函数来完成。拷贝初始化不仅在我们使用=定义变量时会发生,在下列情况也会发生
    (1)将一个对象作为实参传递给一个非引用类型的形参。
    (2)从一个返回类型为非引用类型的函数返回一个对象。

  • 直接初始化:当使用直接初始化时,我们实际上是要求编译器使用普通的函数来选择与我们提供的参数最匹配的构造函数。

string str1(10,'9');//直接初始化
string str2(str1);//直接初始化
string str3 = str1;//拷贝初始化
  • 值初始化:我们只提供vector对象容纳的元素数量而去忽略元素的初始值,此时库会创建一个值初始化的元素初值,并把它赋予容器中的所有元素。这个初值由vector对象中元素的类型决定。如果vector对象的元素是内置类型,比如int,则元素初始值自动设置为0.如果元素是某种类类型,比如string,则元素由类默认初始化。
vector v1(10);//10个元素,每个元素的初始化为0
vector 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 
 
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() <

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

Object is being created
Length of line : 6

注意:

  • 构造函数定义既可以放在类的内部定义,也可放在类的外部定义;放在外部定义的构造函数,其函数名前要加上"类名::"。因为在类定义外部,有各种函数的定义,采用此方法是为了区别成员和非成员函数。
  • 构造函数有一特殊之处就是没有返回类型,函数体中也不允许返回值,但可以有无值返回语句"return"。因为构造函数专门用于创建对象和其初始化,所以它不能被随意调用。
  • 一个类定义中,类的数据成员可能为另一个类的对象。如果一个类的对象是另一个类的数据成员,则在那个类的对象创建所调用的构造函数中,则该成员对象自动调用其构造函数。
  1. 默认构造函数
    当用户没有显式的去定义构造函数时, 编译器会为类生成一个默认的构造函数, 称为 "默认构造函数", 一旦你为你的类定义了构造函数,哪怕只是一个,那么编译器将不再生成默认的构造函数,默认构造函数不能完成对象数据成员的初始化, 只能给对象创建一标识符, 并为对象中的数据成员开辟一定的内存空间。
  • 当你使用静态分配的数组,而数组元素类型是某个类的对象时,就要调用默认的构造函数,比如下面的代码。
Object buffer[10]; // call default constructor
  • 当你使用动态分配的数组,而数组元素类型是某个类的对象时,就要调用默认的构造函数,比如下面的代码,如果Object没有默认的构造函数,是无法通过编译的,因为new操作符要调用Object类的无参构造函数类初始化每个数组元素。
Object* buffer = new Object[10];
  • 当你使用标准库的容器时,如果容器内的元素类型是某个类的对象时,那么这个类就需要默认的构造函数,原因同上。
vector buffer;
 
 
  • 一个类A以另外某个类B的对象为成员时,如果A提供了无参构造函数,而B未提供,那么A则无法使用自己的无参构造函数。下面的代码将导致编译错误。
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 

    using namespace std;

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

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

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

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

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

#include 
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 <

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

析构函数

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

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

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

#include 
 
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() <

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

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

总结:

  • 局部和静态对象,以声明顺序构造
    局部和静态对象是指块作用域和文件作用域的对象。它们声明的顺序与它们在程序中出现的顺序是一致的。

  • 静态对象只能被构造一次
    静态对象和静态变量一样,文件作用域的静态对象在主函数开始运行前全部构造完毕。块作用域的静态对象,则在首次进入到定义该静态对象的函数时,进行构造。

  • 所以全局对象都在主函数main()之前被构造
    和全局变量一样,所以的全局对象在主函数开始运行之前,全部已被构造。

  • 全局对象构造时无特殊顺序

  • 成员以其在类的声明顺序构造

你可能感兴趣的:(C++第四弹---构造函数&析构函数)