十七、对象的构造

1、成员变量的初始值

#include <stdio.h>

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

Test gt;		// 全局对象 全局区,统一初始值为0

int main()
{
    printf("gt.i = %d
", gt.getI()); // 0
    printf("gt.j = %d
", gt.getJ()); // 0
    
    Test t1;	// 局部对象 栈区
    
    printf("t1.i = %d
", t1.getI()); // 随机值
    printf("t1.j = %d
", t1.getJ()); // 随机值
    
    Test* pt = new Test;	// 类也是一个数据类型,堆区
    
    printf("pt->i = %d
", pt->getI()); // 堆区应该也是随机值
    printf("pt->j = %d
", pt->getJ());
    
    delete pt;
    
    return 0;
}

2、对象的初始化

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

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

静态存储区包括了全局变量和static修饰的局部变量

需要解决的问题:使类的成员变量不管在哪个存储区进行定义,它的初始值都是固定的。

对象的初始化:

  • 一般而言,对象都需要一个确定的初始状态
  • 解决方案:
    • 在类中提供一个publicinitialize函数
    • 在对象创建后立即调用initialize函数进行初始化
#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
", gt.getI());
    printf("gt.j = %d
", gt.getJ());
    
    Test t1; 
    t1.initialize();  
    printf("t1.i = %d
", t1.getI());
    printf("t1.j = %d
", t1.getJ());
    
    Test* pt = new Test; 
    pt->initialize();
    printf("pt->i = %d
", pt->getI());
    printf("pt->j = %d
", pt->getJ());
    delete pt;

    return 0;
}

这种方式存在的问题:

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

这个初始化函数在对象创建之手就必须马上调用,新建对象之手,需要人工手动添加initialize()函数,如果可以有一个函数在创建对象后自动调用,初始化成员变量就是极好的。

于是C++出现了构造函数来解决这个问题

3、构造函数

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() {
		i = 1;
		j = 2;
	}
};

Test gt;

int main()
{
	//gt.initialize();
	printf("gt.i = %d, gt.j = %d
", gt.getI(), gt.getJ());

	Test t1;
	//t1.initialize();
	printf("t1.i = %d, t1.j = %d
", t1.getI(), t1.getJ());

	Test * pt = new Test;
	//pt->initialize();
	printf("pt->i = %d, pt->j = %d
", pt->getI(), pt->getJ());

	return 0;
}

4、带参数的构造函数

构造函数和普通函数的差别:构造函数没有返回值,名字和类型一样

此时就只剩下参数可以讨论:构造函数也可以带参数

带有参数的构造函数:

  • 构造函数可以根据需要定义参数
  • 一个类中可以存在多个重载的构造函数
  • 构造函数的重载遵循C++重载的规则
class Test
{
public:
	Test(int v)
	{
        // use v to initialize member
	}
};

注意:

对象定义和对象声明不同:

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

int main()
{
    // 告诉编译器存在名为t的Test对象
    extern Test t;
    
    return 0;
}

构造函数的自动调用

class Test {
public:
    Test(){}
	Test(int v) { }
    Test(const int& cv){}	// 拷贝构造函数
};	

Test t;			// 调用构造函数Test()
Test t1(1);		// 定义了一个对象t1,并调用带有参数的构造函数,传入参数为1,根据重载规则,构造函数为Test(int v)
Test t2 = 1;	// 用 1 来初始化对象t2,初始化需要借助构造函数,根据重载规则,选择Test(int v)
/*这里的过程其实是:
首先调用构造函数Test(int v)创建一个临时对象,参数为1;
然后就变成了用一个对象初始化另一个对象,此时应该是要调用拷贝构造函数进行成员变量值的复制,将这个临时对象作为参数用来构造对象t2。
但是编译器发现,可以通过重载的构造函数Test(int v)来直接初始化对象,而达到相同效果,所以将这条语句优化为Test t1(1)
	
*/

初始化和赋值:

#include <stdio.h>

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

int main()
{
    Test t;      // 调用 Test()
    Test t1(1);  // 调用 Test(int v)
    Test t2 = 2; // 调用 Test(int v)
    
    
    int i = 1;	// 用1来初始化变量i
    i = 2;	    // 用2对变量i进行赋值
    
    t = t2;		// 用对象t2对对象t进行赋值
    
    int i(100);	// 用100来初始化i
    
    printf("i = %d
", i);
    
    return 0;
}

初始化和赋值是不一样的,C语言中差别不大,C++中差别很大,因为对象的初始化要调用构造函数

构造函数的调用:

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

5、创建一个数组

#include <stdio.h>

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

int main()
{
    Test ta[3];	// 调用3次Test() ,每个数组元素中的m_value都按Test()来处理,不一定需要这样的结果
    Test ta2[3] = {Test(), Test(1), Test(2)};	// 手工调用构造函数,3个数组元素调用不同的构造函数
    
    for (int i = 0; i < 3; i++)
    {
        printf("ta[%d].getValue() = %d
", i, ta[i].getValue());	
        // 手工调用构造函数后,m_value初始化成不同值
    }
    
    Test t = Test(100);	// 创建对象之后,调用构造函数来初始化对象
    
    return 0;
}

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

  • 提供函数获取数组长度
  • 提供函数获取函数元素
  • 提供函数设置数组元素
// 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.c
#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.c
#include <stdio.h>
#include "IntArray.h"
int main()
{
	IntArray a(5);	// 定义了一个对象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
", i, value);
		}
	}

	a.free();
    
    return 0;
}

6、特殊的构造函数

两个特殊的构造函数

  • 无参构造函数:无参数的构造函数

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

  • 拷贝构造函数:参数为const class_name&的构造函数

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

如果类中已经有构造函数,编译器就不会提供默认的构造函数

#include <stdio.h>

class Test
{
private:
    int i;
    int j;
public:
    int getI()
    {
        return i;
    }
    int getJ()
    {
        return j;
    }
    
    /*
    	Test(){};	// 编译器会提供一个默认的无参构造函数
    */
    
     // 拷贝构造函数,这也是一个构造函数,写了这个之后,编译器就不会提供默认的无参构造函数,创建对象就会失败,需要手工再创建一个无参构造函数
    Test(const Test& t)
    {
        i = t.i;
        j = t.j;
    }
    
    Test(){};
    
};

class T
{
/*
    空类:里面至少有一个无参构造函数
*/
}


int main()
{
    Test t;	// 未定义构造函数时,依然能创建对象,因为编译器提供了一个无参的构造函数

    int i = 2;
    int j = i;	// 用一个变量初始化另一个变量
    
    // 同理到 类 对象中
    Test t1;
    Test t2 = t1;	// 用对象初始化另一个对象时,编译器提供默认的拷贝构造函数
        
    printf("t1.i = %d, t1.j = %d
", t1.getI(), t1.getJ());	// 随机数
    printf("t2.i = %d, t2.j = %d
", t2.getI(), t2.getJ());	// 随机数
    
    return 0;
}

拷贝构造函数的意义:

  • 兼容C语言的初始化方式
  • 初始化行为能够符合预期的处理

初始化和赋值不一样的地方在于,初始化涉及到拷贝构造函数的调用,通过拷贝构造函数可以利用一个已知的对象去创建初始化一个新的对象

拷贝构造函数分为:

  • 浅拷贝

    拷贝后对象的物理状态相同(只进行值的拷贝,成员的复制)

  • 深拷贝

    拷贝后对象的逻辑状态相同(逻辑状态的拷贝)

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

#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指针的值不直接复制
        p = new int;	// 指向一个新的堆空间的地址
        
        *p = *t.p;		// 将新的堆空间的值进行重新指定
        			   // 将t.p指向的堆空间的值,复制给新的p指向内存
    }
    
    // 定义一个带参数的构造函数
    Test(int v)
    {
        i = 1;
        j = 2;
        p = new int;	
        *p = v;
    }
    
    void free()
    {
        delete p;
    }
};

int main()
{
    Test t;		// 没参数,没有默认的无参构造函数,所以会创建失败
    Test t1(3);	// 提供一个参数,创建对象
    /* 
    t1在创建的时候,p指针指向堆空间里面的某个内存地址 
    用t1去初始化t2的时候,t2.p也应该指向堆空间的某个内存地址,并且和t1.p不是指向同一个内存地址
    这个程序只是将参数3传给v,然后在堆空间里存放这个值
    拷贝构造的时候也是做这件事,而不是直接将指针地址复制
    */
    Test t2 = t1;
    // 或者
    Test t2(t1);	// 将t1作为参数传入拷贝构造函数
	Test t3 = 2;	// 以2为参数调用构造函数Test(2), 生成临时对象去初始化对象t3,但是被编译器优化为Test t3(2),不调用拷贝构造函数

    
    printf("t1.i = %d, t1.j = %d, *t1.p = %p
", t1.getI(), t1.getJ(), t1.getP());
    printf("t2.i = %d, t2.j = %d, *t2.p = %p
", t2.getI(), t2.getJ(), t2.getP());
    /* 
    采用默认的拷贝构造函数:
    	t1.p = 0x8e6a008 	指向堆空间的一个地址
   		t2.p = 0x8e6a008	
    	t1和t2的p指向了堆空间的同一个地址
    采用手工构造的拷贝构造函数:
    	t1.p = 0x8528008
    	t2.p = 0x8528018
    	两个对象的指针成员指向的堆空间的地址不一致了,状态还是一致吗?但是这个地址里面存储的int类型的数据确是相同的
    */
   	printf("t1.i = %d, t1.j = %d, *t1.p = %d
", t1.getI(), t1.getJ(), *t1.getP());
    printf("t2.i = %d, t2.j = %d, *t2.p = %d
", t2.getI(), t2.getJ(), *t2.getP());
    // 打印值:*t1.p = 3, *t2.p = 3
    
    
    t1.free();
    t2.free();
    /* 
    这里会报内存错误 
    0x8e6a008这个内存地址会被释放两次,t1.free()之后,t2.free()就不能再释放了
    */
    
    return 0;
}

/*
    t1.i = 1, t1.j = 2, t1.p = 0x8528008
    t2.i = 1, t2.j = 2, t2.p = 0x8528018
   	这就是物理状态,就是对象占据的内存中,他们的每个字节是否相等
   	t1和t2这两个对象在内存中占据的空间中的值是不一样的
   	
   	从另一个角度看
   	t1.i = 1, t1.j = 2, *t1.p = 3
    t2.i = 1, t2.j = 2, *t2.p = 3
    这就是逻辑状态,t1和t2是一样的,我们需要的仅仅是t1和t2的p指针,所指向的空间中的值是一样的
*/


什么时候使用深拷贝:对象中有成员指向了系统资源

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

一般性原则:自定义拷贝构造函数,必然需要实现深拷贝!!!

7、数组类的改进

// IntArray.C
// 构造函数
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];
	}
}

8、小结

  • 每个对象在使用之前都应该初始化

    类的构造函数用于对象的初始化

    构造函数与类同名并且没有返回值

    构造函数在定义时自动被调用

  • 构造函数可以根据需要定义参数

    构造函数之间可以存在重载关系

    构造函数遵循C++中重载函数的规则

    对象定义时会触发构造函数的调用

    在一些情况下可以手动调用构造函数

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

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

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

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

    • 浅拷贝使得对象的物理状态相同
    • 深拷贝使得对象的逻辑状态相同
原文地址:https://www.cnblogs.com/chenke1731/p/9643695.html