C++ - 构造函数与析构函数

C++ 学习笔记
C++ 构造函数 & 析构函数

构造函数

  • C++规定,每个类必须有默认的构造函数,没有构造函数就不能创建对象。

  • 若没有提供任何构造函数,那么c++提供自动提供一个默认的构造函数,该默认构造函数是一个没有参数的构造函数,它仅仅负责创建对象而不做任何赋值操作。

  • 只要类中提供了任意一个构造函数,那么c++就不在自动提供默认构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
class Student{
public:
Student(){//无参数构造函数
number = 1;
score = 100;
}
void show();
protected:
int number;
int score;
};
void Student::show(){
cout<<number<<endl<<score<<endl;
}
void main()
{
Student a;
a.show();
cin.get();
}
  • 在类中的定义和类名相同,并且没有任何返回类型的Student()就是构造函数,这是一个无参数的构造函数,他再对象创建的时候自动调用,如果去掉 Student()函数体内的代码那么它和 C++默认提供的构造函数是等价的。

  • 构造函数可以带任意多个形式的参数,这一点和普通函数的特性是一样的!下面是一个带参数的构造函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #include<iostream>
    using namespace std;
    class Teacher
    {
    public:
    Teacher(char * input_name)//有参数的构造函数
    {
    name = new char[10];
    //name = input_name;//这样赋值是错误的
    strcpy(name,input_name);
    }
    void show();
    protected:
    char *name;
    }
    void Teacher::show()
    {
    cout<<name<<endl;
    }
    void main()
    {
    //Teacher a;//这里是错误的,因为没有无参数的构造函数
    Teacher a("test");
    a.show();
    cin.get();
    }
  • 两个注意:

    • 我们创建了一个带有字符指针的带有形参的 Teacher(char *input_name) 的构造函数,调用它创建对象的使用类名加对象名称加扩号和扩号内参数的方式调用,这和调用函数有点类似,但意义也有所不同,因为 构造函数是为创建对象而设立的,这里的意义不单纯是调用函数,而是创建一个类对象。一旦类中有了一个带参数的构造函数而又没有无参数构造函数的时候系统无法创建不带参数的 Teacher a;

    • //name = input_name;//这样赋值是错误的。因为 name 指是指向内存堆区的,如果使用 name = input_name; 会造成指针指向改变不是指向堆区而是指向栈区,导致在后面调用析构函数 delete 释放空间出错!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>  
using namespace std;
class Teacher
{
public:
Teacher(char *input_name)
{
name=new char[10];
//name=input_name;//这样赋值是错误的
strcpy(name,input_name);
}
Teacher()//无参数构造函数,进行函数重载
{

}
void show();

protected:
char *name;

};
void Teacher::show()
{
cout<<name<<endl;
}
void main()
{
Teacher test;
Teacher a("test");
a.show();
cin.get();
}
  • 创建一个无参数的同名的 Teacher()无参数函数,一重载方式区分调用,由于构造函数和普通函数一样具有重载特性所以编写程序的人可以给一个类添加任意多个构造函数,来使用不同的参数来进行初始化对象。

  • C++规定如果一个类对象是另外一个类的数据成员,那么在创建对象的时候系统将自动调用哪个类的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>  
using namespace std;
class Teacher
{
public:
Teacher()
{
director = new char[10];
strcpy(director,"王大力");
}
char * show();
protected:
char * director;
};
char *Teacher::show()
{
return director;
}
class Student
{
public:
Student()
{
number = 1;
score = 100;
}
void show();
protected:
int number;
int score;
Teacher teacher;//这个类的成员teacher是用Teacher类进行创建并初始化的
};
void Student::show()
{
cout<<teacher.show()<<endl<<number<<endl<<score<<endl;
}
void main()
{
Student a;
a.show();
Student b[3];
for(int i=0; i<sizeof(b)/sizeof(Student); i++)
{
b[i].show();
}
cin.get();
}
  • 上面代码中的Student类成员中teacher成员是的定义是用类Teacher进行定义创建的,那么系统碰到创建代码的时候就会自动调用Teacher类中的Teacher()构造函数对对象进行初始化工作!

  • 这个例子说明类的分工很明确,只有碰到自己的对象的创建的时候才自己调用自己的构造函数。

c++构造函数的显式和隐式调用

  • 如果在编写类时没有显示的写出其构造函数,析构函数,以及重载赋值操作符,编译器会在编译代码时,会为该类加上这些。其形式大致如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    A(){
    }

    A& operator =(const A& a){
    // 这个地方可能有点不当,只是为了表明这是一个浅拷贝
    memcpy(this, &a, sizeof(A));
    return *this;
    }

    A(const A& a){
    // 这儿调用了赋值重载函数
    *this = a;
    }

    // 注意在析构函数前不会加上virtual关键字
    ~A(){
    }
  • 下面给出一些示例,注释部分说明了函数调用的情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void f()
    {
    // A()构造函数被调用
    A a;
    // A(const A& a)构造函数被调用
    A b(a);
    // A(const A& a)构造函数被调用
    A c = a;
    // A& operator = (const A& a)赋值操作符重载函数被调用
    b = c;
    }

    // 离开f()函数之前,a,b,c的析构函数被调用,做一些清理工作
  • “A c = a;”

  • 这句代码实际调用的是拷贝构造函数,而非赋值函数。

  • 因此,我们可以构造出这样的代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    class A
    {
    private:
    int *m_data;
    std::string ss;
    public:
    A()
    {
    m_data = NULL;
    }
    A(int n)
    {
    m_data = NULL;
    if (n>0)
    m_data = new int[n];
    }
    A& operator =(const A& a)
    {
    memcpy(this, &a, sizeof(A));
    return *this;
    }
    virtual ~A()
    {
    if (NULL!=m_data)
    {
    delete [] m_data;
    m_data = NULL;
    }
    }
    };


    int main(int argc, char* argv[])
    {
    // 将整数3赋值给一个对象
    A a = 3;
    return 0;
    }
  • 将整数3赋值给一个A类型对象a,然而以上代码可以编译通过。 – 有点不合常理

  • 这是由于“单参数构造函数”被自动型别转换(这是一个隐式转换)。

  • 可以通过explicit关键字,阻止“以赋值语法进行带有转型操作的初始化”。如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    class A
    {
    private:
    int *m_data;
    std::string ss;
    public:
    A()
    {
    m_data = NULL;
    }
    explicit A(int n)
    {
    m_data = NULL;
    if (n>0)
    m_data = new int[n];
    }
    A& operator =(const A& a)
    {
    memcpy(this, &a, sizeof(A));
    return *this;
    }
    virtual ~A()
    {
    if (NULL!=m_data)
    {
    delete [] m_data;
    m_data = NULL;
    }
    }
    };


    int main(int argc, char* argv[])
    {
    // A a = 3; // 编译无法通过
    A b(3); // 可以编译通过

    return 0;
    }

一种特殊的构造函数的写法

正常的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <string>

class Entity
{
private:
std::string name;
public:
Entity()
{
name = "Unknown";
}

Entity(const std::string& n)
{
name = n;
}

const std::string& GetName() const { return name; }
};

// 程序的主函数
int main( )
{
const Entity e;
std::cout << e.GetName() << std::endl;

const Entity e1("qinhan");
std::cout << e1.GetName() << std::endl;

std::cin.get();
return 0;
}

特殊写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <string>

class Entity
{
private:
std::string name;
int score;
public:
Entity()
:name("Unknown"), score(0)
{
}

Entity(const std::string& n)
:name(n), score(12) // 需要将所有的参数都带上,而且需要按照顺序来
{
}

const std::string& GetName() const { return name; }
};

// 程序的主函数
int main( )
{
const Entity e;
std::cout << e.GetName() << std::endl;

const Entity e1("qinhan");
std::cout << e1.GetName() << std::endl;

std::cin.get();
return 0;
}

这样有啥好处呢?

  • 如果按照一般的写法,成员变量会被赋值两次,第一次是初始值,然后再进行一次赋值

析构函数

  • 一个类可能需要在构造函数内动态分配资源,那么这些动态开辟的资源就需要在对象不复存在之前被销毁掉,那么c++类的析构函数就提供了这个方便。

  • 析构函数 的定义:析构函数也是特殊的类成员函数,它没有返回类型,没有参数,不能随意调用,也没有重载,只有在类对象的生命期结束的时候,由系统自动调用。

  • 析构函数与构造函数的不同:析构函数与构造函数最主要大不同就是在于调用期不同,构造函数可以有参数可以重载!

  • 如何编写析构函数:

    • 析构函数可以的特性是在程序结束的时候逐一调用,那么正好与构造函数的情况是相反,属于互逆特性,所以定义析构函数因使用“~”符号(逻辑非运算符),标示它为逆构造函数,加上类名称来定义。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      #include <iostream>  
      #include <string>
      using namespace std;
      class Teacher
      {
      public:
      Teacher()
      {
      director = new char[10];
      strcpy(director,"王大力");
      //director = new string;
      // *director="王大力";//string情况赋值
      }
      ~Teacher()
      {
      cout<<"释放堆区director内存空间1次";
      delete[] director;
      cin.get();
      }
      char *show();
      protected:
      char *director;
      //string *director;
      };
      char *Teacher::show()
      {
      return director;
      }
      class Student
      {
      public:
      Student()
      {
      number = 1;
      score = 100;
      }
      void show();
      protected:
      int number;
      int score;
      Teacher teacher;
      };
      void Student::show()
      {
      cout<<teacher.show()<<endl<<number<<endl<<score<<endl;
      }
      void main()
      {
      Student a;
      a.show();
      Student b[3];
      for(int i=0; i<sizeof(b)/sizeof(Student); i++)
      {
      b[i].show();
      }
      cin.get();
      }
  • 上面的代码中我们为Teacher类添加了一个名为~Teacher()的析构函数用于清空堆内存。

  • 程序将在结束前也就是对象声明周期结束的时候自动调用~Teacher()。

  • 有直接的关系,以后能为 delete 操作符只能清空堆空间而不能够清除栈空间,如果强行清除栈控件内存的画将导致程序的崩溃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>    
#include <string>
using namespace std;
class Teacher
{
public:
Teacher(char *temp)
{
director = new char[10];
strcpy(director,temp);
}
~Teacher()
{
cout<<"释放堆区director内存空间1次";
delete[] director;
cin.get();
}
char *show();
protected:
char *director;
};
char *Teacher::show()
{
return director;
}
class Student
{
public:
Student()
{
number = 1;
score = 100;
}
void show();
protected:
int number;
int score;
Teacher teacher("王大力");//错误,一个类的成员如果是另外一个类的对象的话,不能在类中使用带参数的构造函数进行初始化
};
void Student::show()
{
cout<<teacher.show()<<endl<<number<<endl<<score<<endl;
}
void main()
{
Student a;
a.show();
Student b[3];
for(int i=0; i<sizeof(b)/sizeof(Student); i++)
{
b[i].show();
}
cin.get();
}
  • 这样不行,程序不能够编译成功,

  • 因为:类是一个抽象的概念,并不是一个实体,并不能包含属性值,只有对象才占有一定的内存空间,含有明确的属性值。

  • C++的解决方案是叫构造类成员。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    #include <iostream>    
    using namespace std;
    class Teacher
    {
    public:
    Teacher(char *temp)
    {
    director = new char[10];
    strcpy(director,temp);
    }
    ~Teacher()
    {
    cout<<"释放堆区director内存空间1次";
    delete[] director;
    cin.get();
    }
    char *show();
    protected:
    char *director;
    };
    char *Teacher::show()
    {
    return director;
    }
    class Student
    {
    public:
    Student(char *temp):teacher(temp)
    {
    number = 1;
    score = 100;
    }
    void show();
    protected:
    int number;
    int score;
    Teacher teacher;
    };
    void Student::show()
    {
    cout<<teacher.show()<<endl<<number<<endl<<score<<endl;
    }
    void main()
    {
    Student a("王大力");
    a.show();
    //Student b[3]("王大力"); //这里这么用是不对的,数组不能够使用带参数的构造函数,以后我们将详细介绍vector类型
    // for(int i=0; i<sizeof(b)/sizeof(Student); i++)
    //{
    // b[i].show();
    //}
    cin.get();
    }
  • 最明显的改变在这里

    1
    Student(char *temp):teacher(temp)
  • 冒号后的 teacher 就是要告诉调用 Student 类的构造函数的时候把参数传递给成员 teacher 的 Teacher 类的构造函数,这样一来我们就成功的在类体外对 teacher 成员进行了初始化,既方便也搞笑,这种冒号后制定调用某成员构造函数的方式,可以同时制定多个成员,这一特性使用都好方式,例如:

1
Student(char *temp):teacher(temp),abc(temp),def(temp)
  • 修改一下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    #include <iostream>    
    #include <string>
    using namespace std;
    class Teacher
    {
    public:
    Teacher(char *temp)
    {
    director = new char[10];
    strcpy(director,temp);
    }
    ~Teacher()
    {
    cout<<"释放堆区director内存空间1次";
    delete[] director;
    cin.get();
    }
    char *show();
    protected:
    char *director;
    };
    char *Teacher::show()
    {
    return director;
    }
    class Student
    {
    public:
    Student(char *temp,int &pk):teacher(temp),pk(pk),ps(10)
    {
    number = 1;
    score = 100;
    }
    void show();
    protected:
    int number;
    int score;
    Teacher teacher;
    int &pk;
    const int ps;
    };
    void Student::show()
    {
    cout<<teacher.show()<<endl<<number<<endl<<score<<endl<<pk<<endl<<ps<<endl;
    }
    void main()
    {
    char *t_name="王大力";
    int b=99;
    Student a(t_name,b);
    a.show();
    cin.get();
    }
  • 调用的时候我们使用 :

    1
    Student a(t_name,b);
  • 我们将b的地址传递给了int &pk这个引用,使得Student类的引用成员pk和常量成员ps进行了成功的初始化。
    但是细心的人会发现,我们在这里使用的初始化方式并不是在构造函数内进行的,而是在外部进行初始化的,的确,在冒号后和在构造函数括号内的效果是一样的, 但和teacher(temp)所不同的是,pk(pk)的括号不是调用函数的意思,而是赋值的意思,我想有些读者可能不清楚新标准的 C++对变量的初始 化是允许使用括号方式的,int a=10和int a(10)的等价的,但冒号后是不允许使用=方式只允许()括号方式,所以这里只能使用pk(pk)而不能是pk=pk了。

  • *c++规定,所有的全局对象和全局变量一样都在主函数main()之前被构造,函数体内的静态对象则只构造一次,也就是说只在首次进入这个函数的时候进行构造! *

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>    
#include <string>
using namespace std;
class Test
{
public:
Test(int a)
{
kk=a;
cout<<"构造参数a:"<<a<<endl;
}
public:
int kk;
};
void fun_t(int n)
{
static Test a(n);
//static Test a=n;//这么写也是对的
cout<<"函数传入参数n:"<<n<<endl;
cout<<"对象a的属性kk的值:"<<a.kk<<endl;
}
Test m(100);
void main()
{
fun_t(20);
fun_t(30);
cin.get();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>    
#include <string>
using namespace std;
class Test
{
public:
Test(int a)
{
kk=a;
cout<<"构造参数a:"<<a<<endl;
}
public:
int kk;
};
void fun_t(int n)
{
static Test a(n);
//static Test a=n;//这么写也是对的
cout<<"函数传入参数n:"<<n<<endl;
cout<<"对象a的属性kk的值:"<<a.kk<<endl;
}
Test m(100);
void main()
{
fun_t(20);
fun_t(30);
cin.get();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>    
using namespace std;

class Test
{
public:
Test(int j):pb(j),pa(pb+5)
{

}
public:
int pa;
int pb;
};
void main()
{
Test a(10);
cout<<a.pa<<endl;
cout<<a.pb<<endl;
cin.get();
}
  • 类成员的构造是按照在类中定义的顺序进行的,而不是按照构造函数说明后的冒号顺序进行构造的。

Q&A

delete 某个类的指针会调用该类的析构函数么?

  • 会的.如果不调用的话怎么析构一个类.

  • 不过指针所指向的对象必须是在堆中用new关键词开创的

  • 如果指针指向的是一个栈中的对象,会引起调用两种析构函数而导致程序错误

    1
    2
    3
    4
    class ob
    ob a;
    ob *p=&a;
    delete p;//这样会导致调用两次析构函数.是会引起程序错误的
  • 只有

    1
    2
    3
    class ob
    ob * p= new ob;
    delete p; //这样是正确的

对于 new 创建的对象,只有调用 delete 才能析构。

构造函数

  • 什么是构造函数?通俗的讲,在类中,函数名和类名相同的函数称为构造函数。它的作用是在建立一个对象时,作某些初始化的工作(例如对数据赋予初值)。C++允许同名函数,也就允许在一个类中有多个构造函数。如果一个都没有,编译器将为该类产生一个默认的构造函数。

  • 构造函数上惟一的语法限制是它不能指定返回类型,甚至void 也不行。

  • 不带参数的构造函数:一般形式为 类名 对象名(){函数体}

  • 带参数的构造函数:不带参数的构造函数,只能以固定不变的值初始化对象。带参数构造函数的初始化要灵活的多,通过传递给构造函数的参数,可以赋予对象不同的初始值。一般形式为:构造函数名(形参表);

  • 创建对象使用时:类名 对象名(实参表);

  • 构造函数参数的初始值:构造函数的参数可以有缺省值。当定义对象时,如果不给出参数,就自动把相应的缺省参数值赋给对象。一般形式为:

    • 构造函数名(参数=缺省值,参数=缺省值,……);

析构函数

  • 当一个类的对象离开作用域时,析构函数将被调用(系统自动调用)。析构函数的名字和类名一样,不过要在前面加上 ~ 。对一个类来说,只能允许一个析构函数,析构函数不能有参数,并且也没有返回值。析构函数的作用是完成一个清理工作,如释放从堆中分配的内存。

  • 一个类中可以有多个构造函数,但析构函数只能有一个。对象被析构的顺序,与其建立时的顺序相反,即后构造的对象先析构。


欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 qinhan_shu@163.com

文章标题:C++ - 构造函数与析构函数

本文作者:QinHan

发布时间:2019-10-29, 15:34:45

最后更新:2020-02-20, 05:42:12

原始链接:https://qinhan.site/2019/10/29/cpp-constructor/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏