服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C/C++ - C++详细讲解对象的构造

C++详细讲解对象的构造

2022-11-11 15:18清风自在 流水潺潺 C/C++

当在参数化构造函数中声明对象时,必须将初始值作为参数传递给构造函数。对象声明的常规方法可能不起作用。构造函数可以显式或隐式调用,让我们一起了解对象的构造

一、对象的构造(上)

1.1 对象的初始值

问题:对象中成员变量的初始值是多少?

下面的类定义中成员变量 i 和 j 的初始值为多少?

C++详细讲解对象的构造

下面看一段成员变量初始值的代码:

#include<stdio.h>

class Test
{
  private:
      int i;
      int j;
  public:
      int getI() {return i;}
      int getJ() {return j;}
};

Test gt;

int main()
{
  printf("gt.i = %d\n", gt.getI());
  printf("gt.j = %d\n", gt.getJ());
  
  Test t1;
  
  printf("t1.i = %d\n", t1.getI());
  printf("t1.j = %d\n", t1.getJ());
  
  Test* pt = new Test;
  
  printf("pt->i = %d\n", pt->getI());
  printf("pt->j = %d\n", pt->getJ());   
  
  delete pt;
  
  return 0;
}

下面为输出结果:

C++详细讲解对象的构造

对象t1 所占用的存储空间在栈上面,而且成员变量 i 和 j 也没有明确的初始值,所以初始值就不定。对象 gt 所占用的存储空间在全局数据区,所以初始值统一为 0。

Test* pt = new Test;意味着在堆空间中生成一个 Test 对象,虽然 pt->i 和 pt->j 均为 0,这只是巧合罢了,因为在堆上创建对象时,成员变量初始为随机值。

注:类得到的其实是数据类型,所以说通过这种数据类型在全局数据区、栈和堆上面都能够生成对象。

1.2 对象的初始化

从程序设计的角度,对象只是变量,因此:

  • 在栈上创建对象时,成员变量初始为随机值
  • 在堆上创建对象时,成员变量初始为随机值
  • 在静态存储区创建对象时,成员变量初始为 0 值

生活中的对象都是在初始化后上市的

初始状态(出厂设置)是对象普遍存在的一个状态

—股而言,对象都需要—个确定的初始状态

解决方案

  • 在类中提供一个 public 的 initialize 函数
  • 对象创建后立即调用 initialize 函数进行初始化

如下:

C++详细讲解对象的构造

下面看一段初始化函数的代码:

#include<stdio.h>

class Test
{
  private:
      int i;
      int j;
  public:
      int getI() {return i;}
      int getJ() {return j;}
      void initialize()
      {
          i = 1;
          j = 2;
      }
};

Test gt;

int main()
{
  gt.initialize();
  
  printf("gt.i = %d\n", gt.getI());
  printf("gt.j = %d\n", gt.getJ());
  
  Test t1;
  
  t1.initialize();
  
  printf("t1.i = %d\n", t1.getI());
  printf("t1.j = %d\n", t1.getJ());
  
  Test* pt = new Test;
  
  pt->initialize();
  
  printf("pt->i = %d\n", pt->getI());
  printf("pt->j = %d\n", pt->getJ());   
  
  delete pt;
  
  return 0;
}

下面为输出结果:

C++详细讲解对象的构造

存在的问题

  • initialize 只是一个普通函数,必须显示调用
  • 如果未调用 initialize 函数,运行结果是不确定的

下面为解决办法:

C++中可以定义与类名相同的特殊成员函数

这种特殊的成员函数叫做构造函数

  • 构造没有任何返回类型的声明
  • 构造函数在对象定义时自动被调用

下面来体验一下构造函数:

#include<stdio.h>

class Test
{
  private:
      int i;
      int j;
  public:
      int getI() {return i;}
      int getJ() {return j;}
      Test()
      {
          printf("Test() Begin\n");
      
          i = 1;
          j = 2;
      
          printf("Test() End\n");
      }
};

Test gt;

int main()
{
  printf("gt.i = %d\n", gt.getI());
  printf("gt.j = %d\n", gt.getJ());
  
  Test t1;
  
  printf("t1.i = %d\n", t1.getI());
  printf("t1.j = %d\n", t1.getJ());
  
  Test* pt = new Test;
  
  printf("pt->i = %d\n", pt->getI());
  printf("pt->j = %d\n", pt->getJ());   
  
  delete pt;
  
  return 0;
}

下面为输出结果:

C++详细讲解对象的构造

可以看到,Test() Begin 和 Test() End 出现了三次,也就是说,Test() 这个构造函数被调用了三次,这是因为创建了三个对象。

1.3 小结

  • 每个对象在使用之前都应该初始化
  • 类的构造函数用于对象的初始化
  • 构造函数与类同名并且没有返回值
  • 构造函数在对象定义时自动被调用

 

二、对象的构造(中)

2.1 构造函数

带有参数的构造函数

  • 构造函数可以根据需要定义参数
  • 一个类中可以存在多个重载的构造函数
  • 构造函数的重载遵循 C++ 重载的规则

如下:

C++详细讲解对象的构造

友情提醒

对象定义和对象声明不同

  • 对象定义--申请对象的空间并调用构造函数
  • 对象声明--告诉编译器存在这样一个对象

如下:

C++详细讲解对象的构造

构造函数的自动调用

如下:

C++详细讲解对象的构造

下面看一段带参数的构造函数的代码:

#include <stdio.h>

class Test
{
  public:
      Test() 
      { 
          printf("Test()\n");
      }
      Test(int v) 
      { 
          printf("Test(int v), v = %d\n", v);
      }
};

int main()
{
  Test t;      // 调用 Test()
  Test t1(1);  // 调用 Test(int v)
  Test t2 = 2; // 调用 Test(int v)
  
  return 0;
}

下面为输出结果,和预想中的一致。

C++详细讲解对象的构造

这里需要明确一个问题,int i = 1;与 int i; i = 1;的不同。前者是初始化,后者是先定义,再赋值。后者由于定义 i 时没有初始化,所以 i 的值时随机的。C语言中这两者差别很小,但是在 C++ 中两者差异很大。差别在于在 C++ 中初始化会调用构造函数。下面看一个例子,在上述代码的基础上加一行代码 t = t2;

#include <stdio.h>

class Test
{
  public:
      Test() 
      { 
          printf("Test()\n");
      }
      Test(int v) 
      { 
          printf("Test(int v), v = %d\n", v);
      }
};

int main()
{
  Test t;      // 调用 Test()
  Test t1(1);  // 调用 Test(int v)
  Test t2 = 2; // 调用 Test(int v)
  
  t = t2;
  
  return 0;
}

下面为输出结果,可以看到与上面的代码输出结果一模一样。这就因为 C++ 中初始化和赋值不同,初始化会调用构造函数,赋值的时候则不用。

C++详细讲解对象的构造

下面再看一个例子:

#include <stdio.h>

class Test
{
  public:
      Test() 
      { 
          printf("Test()\n");
      }
      Test(int v) 
      { 
          printf("Test(int v), v = %d\n", v);
      }
};

int main()
{
  Test t;      // 调用 Test()
  Test t1(1);  // 调用 Test(int v)
  Test t2 = 2; // 调用 Test(int v)
  
  int i(100);
  
  printf("i = %d\n", i);
  
  return 0;
}

下面为输出结果:

C++详细讲解对象的构造

构造函数的调用

  • 一般情况下,构造函数在对象定义时被自动调用
  • —些特殊情况下,需要手工调用构造函数

下面看一段构造函数手动调用的代码:

#include <stdio.h>

class Test
{
  private:
      int m_value;
  public:
      Test() 
      { 
          printf("Test()\n");
      
          m_value = 0;
      }
      
      Test(int v) 
      { 
          printf("Test(int v), v = %d\n", v);
      
          m_value = v;
      }
      
      int getValue()
      {
          return m_value;
      }
};

int main()
{
  Test ta[3] = {Test(), Test(1), Test(2)};      
  
  for (int i = 0; i < 3; i++)
  {
      printf("ta[%d].getValue() = %d\n", i, ta[i].getValue());
  }
  
  Test t = Test(100);
  
  printf("t.getValue() = %d\n", t.getValue());
  
  return 0;
}

下面为输出结果,可以看到,Test(1)、Test(2) 和 Test(100) 均为手动调用构造函数。

C++详细讲解对象的构造

2.2小实例

需求:开发一个数组类解决原生数组的安全性问题

  • 提供函数获取数组长度
  • 提供函数获取数组元素
  • 提供函数设置数组元素

IntArray.h:

#ifndef _INTARRAY_H_

#define _INTARRAY_H_

class IntArray

{
  private:
      int m_length;

      int* m_pointer;
  public:
      IntArray(int len);

      int length();

      bool get(int index, int& value);

      bool set(int index ,int value);

      void free();
};
#endif

IntArray.cpp:

#include "IntArray.h"



IntArray::IntArray(int len)

{

  m_pointer = new int[len];

  

  for (int i = 0; i < len; i++)

  {

      m_pointer[i] = 0;

  }

  

  m_length = len;

}



int IntArray::length()

{

  return m_length;

}



bool IntArray::get(int index, int& value)

{

  bool ret = (0 <= index) && (index < length());

  

  if( ret )

  {

      value = m_pointer[index];

  }

  

  return ret;

}



bool IntArray::set(int index, int value)

{

  bool ret = (0 <= index) && (index < length());

  

  if( ret )

  {

      m_pointer[index] = value;

  }

  

  return ret;

}



void IntArray::free()

{

  delete[]m_pointer;

}

main.cpp:

#include <stdio.h>

#include "IntArray.h"



int main()

{

  IntArray a(5);    

  

  for (int i = 0; i < a.length(); i++)

  {

      a.set(i, i + 1);

  }

  

  for (int i = 0; i < a .length(); i++)

  {

      int value = 0;

      

      if( a.get(i, value) )

      {

          printf("a[%d] = %d\n", i, value);

      }

  }

  

  a.free();

  

  return 0;

}

下面为输出结果:

C++详细讲解对象的构造

这样写出来的数组很安全,没有数组越界问题。

2.3 小结

  • 构造函数可以根据需要定义参数
  • 构造函数之间可以存在重载关系
  • 构造函数遵循 C++ 中重载函数的规则
  • 对象定义时会触发构造函数的调用
  • 在一些情况下可以手动调用构造函数

 

三、对象的构造(下)

3.1 特殊的构造函数

两个特殊的构造函数

无参构造函数

  • 没有参数的构造函数
  • 当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空

拷贝构造函数

  • 参数为 const class_name& 的构造函数
  • 当类中没有定义拷贝构造函数时,编译器默认提供一个拷贝构造函数,简单的进行成员变量的值复制

下面看一段无参数构造函数的代码(代码3-1):

#include <stdio.h>

class Test
{
  private:
      int i;
      int j;
  public:
      int getI()
      {
          return i;
      }
      int getJ()
      {
          return j;
      }
};

int main()
{
  Test t;
  
  return 0;
}

可以看到,编译通过:

C++详细讲解对象的构造

创建一个类的对象必须要调用构造函数,为什么能够编译通过呢?这是因为编译器在发现我们没有定义构造函数时,会默认提供一个无参构造函数,等效如(代码3-2):

#include <stdio.h>

class Test
{
  private:
      int i;
      int j;
  public:
      int getI()
      {
          return i;
      }
      int getJ()
      {
          return j;
      }
      Test()
      {
      }
};

int main()
{
  Test t;
  
  return 0;
}

小贴士:所以说,class T { }; 里面不是什么都没有,里面至少有一个无参构造函数。

下面再来看一段代码(代码3-3):

#include <stdio.h>

class Test
{
  private:
      int i;
      int j;
  public:
      int getI()
      {
          return i;
      }
      int getJ()
      {
          return j;
      }
};

int main()
{
  Test t1;
  Test t2 = t1;
  
  printf("t1.i = %d, t1.j = %d\n", t1.getI(), t1.getJ());
  printf("t2.i = %d, t2.j = %d\n", t2.getI(), t2.getJ());
  
  return 0;
}

下面为输出结果:

C++详细讲解对象的构造

这里的 i 和 j 打印出来的都是随机值,这是因为类里面没有手工编写的构造函数,所以 t1 和 t2 所采用的就是编译器提供的默认无参构造函数构造的,编译器提供的无参构造函数为空,所以 i 和 j 的值就是随机的。

上述代码就相当于(代码3-4):

#include <stdio.h>

class Test
{
  private:
      int i;
      int j;
  public:
      int getI()
      {
          return i;
      }
      int getJ()
      {
          return j;
      }
      Test(const Test& t)
      {
          i = t.i;
          j = t.j;        
      }
};

int main()
{
  Test t1;
  Test t2 = t1;
  
  printf("t1.i = %d, t1.j = %d\n", t1.getI(), t1.getJ());
  printf("t2.i = %d, t2.j = %d\n", t2.getI(), t2.getJ());
  
  return 0;
}

但是编译的时候会报错:

C++详细讲解对象的构造

这是因为在类里面没有编写任何构造函数时,编译器才提供默认的无参构造函数。这里手工编写了一个拷贝构造函数,编译器就不会提供默认的无参构造函数,需要自己把无参构造函数加上。

如下,自己加上无参构造函数(代码3-5):

#include <stdio.h>
class Test
{
  private:
      int i;
      int j;
  public:
      int getI()
      {
          return i;
      }
      int getJ()
      {
          return j;
      }
      Test(const Test& t)
      {
          i = t.i;
          j = t.j;        
      }
      Test()
      {
      }
};

int main()
{
  Test t1;
  Test t2 = t1;
  
  printf("t1.i = %d, t1.j = %d\n", t1.getI(), t1.getJ());
  printf("t2.i = %d, t2.j = %d\n", t2.getI(), t2.getJ());
  
  return 0;
}

这样就能编译通过了,而且效果跟代码3-3的相同:

C++详细讲解对象的构造

3.2 拷贝构造函数

拷贝构造函数的意义

兼容C语言的初始化方式

初始化行为能够符合预期的逻辑

浅拷贝

  • 拷贝后对象的物理状态相同(物理状态指的是对象占据的内存当中每个字节是否相等,如代码3-6)

深拷贝

  • 拷贝后对象的逻辑状态相同(逻辑状态指的是指针所指向的内存空间的值是否相同,如代码3-9)

注:编译器提供的拷贝构造函数只进行浅拷贝!

下面看一段代码(代码3-6):

#include <stdio.h>

class Test
{
  private:
      int i;
      int j;
      int* p;
  public:
      int getI()
      {
          return i;
      }
      int getJ()
      {
          return j;
      }
      int* getP()
      {
          return p;
      }
      Test(int v)
      {
          i = 1;
          j = 2;
          p = new int;
      
          *p = v;
      }

};

int main()
{
  Test t1(3);
  Test t2 = t1;
  
  printf("t1.i = %d, t1.j = %d, t1.p = %p\n", t1.getI(), t1.getJ(), t1.getP());
  printf("t2.i = %d, t2.j = %d, t2.p = %p\n", t2.getI(), t2.getJ(), t2.getP());
  
  return 0;
}

下面为输出结果:

C++详细讲解对象的构造

这段程序的第一个问题就是 t1 和 t2 的 p 指针都指向同一个堆空间中的地址,第二个问题就是申请了内存并没有释放,会造成内存泄漏。

下面加上释放内存的代码(代码3-7):

#include <stdio.h>

class Test
{
  private:
      int i;
      int j;
      int* p;
  public:
      int getI()
      {
          return i;
      }
      int getJ()
      {
          return j;
      }
      int* getP()
      {
          return p;
      }
      Test(int v)
      {
          i = 1;
          j = 2;
          p = new int;
      
          *p = v;
      }
      void free()
      {
          delete p;
      }

};

int main()
{
  Test t1(3);
  Test t2 = t1;
  
  printf("t1.i = %d, t1.j = %d, t1.p = %p\n", t1.getI(), t1.getJ(), t1.getP());
  printf("t2.i = %d, t2.j = %d, t2.p = %p\n", t2.getI(), t2.getJ(), t2.getP());
  
  t1.free();
  t2.free();
  
  return 0;
}

下面为输出结果,编译能通过,但是运行时发生了错误,释放了两次堆空间的内存:

C++详细讲解对象的构造

下面为解决方法(代码3-8):

#include <stdio.h>

class Test
{
  private:
      int i;
      int j;
      int* p;
  public:
      int getI()
      {
          return i;
      }
      
      int getJ()
      {
          return j;
      }
      
      int* getP()
      {
          return p;
      }
      
      Test(const Test& t)
      {
          i = t.i;
          j = t.j;
          p = new int;
          
          *p = *t.p;
      }
      
      Test(int v)
      {
          i = 1;
          j = 2;
          p = new int;
      
          *p = v;
      }
      
      void free()
      {
          delete p;
      }

};

int main()
{
  Test t1(3);
  Test t2(t1);
  
  printf("t1.i = %d, t1.j = %d, t1.p = %p\n", t1.getI(), t1.getJ(), t1.getP());
  printf("t2.i = %d, t2.j = %d, t2.p = %p\n", t2.getI(), t2.getJ(), t2.getP());
  
  t1.free();
  t2.free();
  
  return 0;
}

下面为输出结果,可以到 t1 和 t2 的 p 指针分别指向不同的堆空间地址:

C++详细讲解对象的构造

如果我们看一下逻辑状态,也就是 *t1.p 和 *t2.p 的值,代码如下(代码3-9):

#include <stdio.h>

class Test
{
  private:
      int i;
      int j;
      int* p;
  public:
      int getI()
      {
          return i;
      }
      
      int getJ()
      {
          return j;
      }
      
      int* getP()
      {
          return p;
      }
      
      Test(const Test& t)
      {
          i = t.i;
          j = t.j;
          p = new int;
          
          *p = *t.p;
      }
      
      Test(int v)
      {
          i = 1;
          j = 2;
          p = new int;
      
          *p = v;
      }
      
      void free()
      {
          delete p;
      }

};

int main()
{
  Test t1(3);
  Test t2(t1);
  
  printf("t1.i = %d, t1.j = %d, t1.p = %p\n", t1.getI(), t1.getJ(), t1.getP());
  printf("t2.i = %d, t2.j = %d, t2.p = %p\n", t2.getI(), t2.getJ(), t2.getP());
  
  t1.free();
  t2.free();
  
  return 0;
}

下面为输出结果,可以看到 *t1.p 和 *t2.p 的值相同,也就是说逻辑状态相同,这就叫做深拷贝。

C++详细讲解对象的构造

什么时候需要进行深拷贝?

对象中有成员指代了系统中的资源

  • 成员指向了动态内存空间
  • 成员打开了外存中的文件
  • 成员使用了系统中的网络端口
  • ......

问题分析

下面就是浅拷贝:

C++详细讲解对象的构造

一般性原则

自定义拷贝构造函数,必然需要实现深拷贝!!!

下面看一个使用深拷贝,对前面数组的代码进行改造。

IntArray.h:

#ifndef _INTARRAY_H_

#define _INTARRAY_H_
class IntArray

{

  private:

      int m_length;

      int* m_pointer;

  public:

      IntArray(int len);

      IntArray(const IntArray& obj);

      int length();

      bool get(int index, int& value);

      bool set(int index ,int value);

      void free();

};
#endif

IntArray.cpp:

#include "IntArray.h"



IntArray::IntArray(int len)

{

  m_pointer = new int[len];

  

  for (int i = 0; i < len; i++)

  {

      m_pointer[i] = 0;

  }

  

  m_length = len;

}



IntArray::IntArray(const IntArray& obj)

{

  m_length = obj.m_length;

  

  m_pointer = new int[obj.m_length];

  

  for (int i = 0; i < obj.m_length; i++)

  {

      m_pointer[i] = obj.m_pointer[i];

  }

}



int IntArray::length()

{

  return m_length;

}



bool IntArray::get(int index, int& value)

{

  bool ret = (0 <= index) && (index < length());

  

  if( ret )

  {

      value = m_pointer[index];

  }

  

  return ret;

}



bool IntArray::set(int index, int value)

{

  bool ret = (0 <= index) && (index < length());

  

  if( ret )

  {

      m_pointer[index] = value;

  }

  

  return ret;

}



void IntArray::free()

{

  delete[]m_pointer;

}

main.cpp:

#include <stdio.h>

#include "IntArray.h"



int main()

{

  IntArray a(5);    

  

  for (int i = 0; i < a.length(); i++)

  {

      a.set(i, i + 1);

  }

  

  for (int i = 0; i < a.length(); i++)

  {

      int value = 0;

      

      if( a.get(i, value) )

      {

          printf("a[%d] = %d\n", i, value);

      }

  }

  

  IntArray b = a;

  

  for (int i = 0; i < b.length(); i++)

  {

      int value = 0;

      

      if( b.get(i, value) )

      {

          printf("b[%d] = %d\n", i, value);

      }

  }

  

  a.free();

  b.free();

  

  return 0;

}

下面为输出结果:

C++详细讲解对象的构造

可以看到 b 数组里面的元素与 a 数组里面的元素相同,这就是深拷贝构造函数的结果。

3.3 小结

C++ 编译器会默认提供构造函数

无参构造函数用于定义对象的默认初始状态

拷贝构造函数在创建对象时拷贝对象的状态

对象的拷贝有浅拷贝和深拷贝两种方式

  • 浅拷贝使得对象的物理状态相同
  • 深拷贝使得对象的逻辑状态相同

到此这篇关于C++详细讲解对象的构造的文章就介绍到这了,更多相关C++ 对象的构造内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://blog.csdn.net/weixin_43129713/article/details/123262524

延伸 · 阅读

精彩推荐
  • C/C++c++实现单纯形法现行规划问题的求解(推荐)

    c++实现单纯形法现行规划问题的求解(推荐)

    这篇文章主要介绍了c++实现单纯形法现行规划问题的求解,本文针对问题通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值...

    含光Aries3932021-08-30
  • C/C++c++ *运算符重载

    c++ *运算符重载

    运算符重载重载运算符是C++ 的一个重要特性,使用运算符重载, 的一个重要特性,使用运算符重载, 重载运算符是程序员可以把C++ 运算符的定义扩展到运算分...

    C++教程网4352021-02-03
  • C/C++浅析C++中cout的运行机制

    浅析C++中cout的运行机制

    关于C++中cout的使用,相信大家再熟悉不过了,然而对于cout是如何输出的?输出的机制是啥,需要进一步的了解。本章娓娓道来。前几天在网上看到这么一...

    C++教程网11332021-01-04
  • C/C++约瑟夫经典问题扩展成双向约瑟夫问题

    约瑟夫经典问题扩展成双向约瑟夫问题

    今天小编就为大家分享一篇关于约瑟夫经典问题扩展成双向约瑟夫问题,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起...

    baiduoWang8482021-07-24
  • C/C++用C# 实现鼠标框选效果的实现代码

    用C# 实现鼠标框选效果的实现代码

    本篇文章是对用C#实现鼠标框选效果的实现代码进行了详细的分析介绍,需要的朋友参考下 ...

    C#教程网5392020-11-30
  • C/C++浅谈Linux环境下并发编程中C语言fork()函数的使用

    浅谈Linux环境下并发编程中C语言fork()函数的使用

    fork函数在Linux中可以创建子进程即一个新的进程,这里我们根据实例来浅谈Linux环境下并发编程中C语言fork()函数的使用,需要的朋友可以参考下...

    C语言教程网9672021-04-07
  • C/C++详解C++编程中的单目运算符重载与双目运算符重载

    详解C++编程中的单目运算符重载与双目运算符重载

    这篇文章主要介绍了详解C++编程中的单目运算符重载与双目运算符重载,是C++入门学习中的基础知识,需要的朋友可以参考下...

    C++教程网4192021-03-14
  • C/C++C语言菜鸟基础教程之for循环

    C语言菜鸟基础教程之for循环

    c语言中的for循环语句使用最为灵活,不仅可以用于循环次数已经确定的情况,而且可以用于循环次数不确定而只给出循环结束条件的情况,它完全可以代替wh...

    翡翠森林Z8012021-06-04