【C++深度解析】12、构造函数与拷贝构造函数

1 问题

下面的类定义中成员变量 i 和 j 的初始值是多少?
在这里插入图片描述

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

全局变量位于静态存储区,局部变量在栈中,malloc 申请的在堆中,new 从自由存储区申请空间,自由存储区很多用堆来实现。

2 构造函数

构造函数的作用是在对象被创建时使用特定的值构造对象,或者说将对象初始化为一个特定的状态。

  • 构函函数与类名相同,没有返回类型
  • 构造函数在对象定义时自动被调用

编程实验:构造函数初探

// 12-1.cpp
#include<stdio.h>
class Test
{
private:
    int i;
    int j;
public:
    Test(int newi, int newj)
    {
        printf("Test() Begin\n");
        i = newi;
        j = newj;
    }
    int getI() {return i;}
    int getJ() {return j;}
};
int main()
{
    Test t1(1, 2);
    printf("t1.i = %d\n", t1.getI());
    printf("t1.j = %d\n", t1.getJ());
    return 0;
}

2.1 构造函数重载

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

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

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

在这里插入图片描述
编程实验:构造函数重载

// 12-2.cpp
#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;
}

2.2 手动调用构造函数

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

编程实验:手动调用构造函数

// 12-3.cpp
#include<stdio.h>
class Test
{
private:
    int m_value;
public:
    Test()
    {
        m_value = 0;
        printf("Test()\n");
    }
    Test(int v)
    {
        m_value = v;
        printf("Test(v), v = %d\n", 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;
}

类有两个构造函数,一个有参数,一个没有参数。main() 函数中定义了一个数组 ta[3],我们指定数组的第一个元素调用没有参数的构造函数,后两个调用有参数的构造函数。

Test t = Test(100); 也是指定构造函数的一种方法。

编译运行:

$ g++ 12-3.cpp -o 12-3
$ ./12-3
Test()
Test(v), v = 1
Test(v), v = 2
ta[0].getvalue() = 0
ta[1].getvalue() = 1
ta[2].getvalue() = 2
Test(v), v = 100
t.getvalue() = 100

2.3 开发数组类解决原生数组安全性问题

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

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

直接看代码,IntArray.h 用于定义类,IntArray.cpp 用于类成员函数的实现。

// IntArray.h
#ifndef _INTARRAY_H_
#define _INRARRAY_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
#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 = (index >= 0 && index < m_length);
    if (ret)
    {
        value = m_pointer[index];
    }
    return ret;
}
bool IntArray::set(int index, int value)
{
    bool ret = (index >= 0 && index < m_length);
    if (ret)
    {
        m_pointer[index] = value;
    }
    return ret;
}
void IntArray::free()
{
    delete[]m_pointer;
}
// 12-4.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;
}
$ g++ 12-4.cpp IntArray.cpp -o 12-4
$ ./12-4
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4
a[4] = 5

3 拷贝构造函数

两个特殊的构造函数

  • 无参构造函数
    • 当类中没有定义构造函数时,编译器默认提供一个无参构造函数,函数体为空
  • 拷贝构造函数
    • 参数为 const class_name&,当类中没有定义拷贝构造函数时,编译器默认提供一个拷贝构造函数,简单的进行成员变量的值复制
// 12-5.cpp
#include<stdio.h>
class Point
{
public:
	Point(int xx = 0, int yy = 0)
	{
		x = xx;
		y = yy;
	}
	Point(const Point& p)
	{
		x = p.x;
		y = p.y;
		printf("Calling the copy constructor\n");
	}
	int getX(){ return x; }
	int getY(){ return y; }
private:
	int x;
	int y;
};
//形参为Point类对象的函数
void fun1(Point p) {
	printf("p.getX() = %d\n", p.getX());
}
//返回值为Point类对象的函数
Point fun2() {
	Point a(1, 2);
	return a;
}
int main() {
	Point a(4, 5);
	Point b = a; 			//情况一,用A初始化B。第一次调用拷贝构造函数
	printf("b.getX() = %d\n", b.getX());
	fun1(b); 				//情况二,对象B作为fun1的实参。第二次调用拷贝构造函数
	b = fun2(); 			//情况三,函数的返回值是类对象,函数返回时调用拷贝构造函数
	printf("b.getX() = %d\n", b.getX());
	return 0;
}
Calling the copy constructor
b.getX() = 4
Calling the copy constructor
p.getX() = 4
Calling the copy constructor
b.getX() = 1

3.1 深拷贝与浅拷贝

对于简单的类,默认的拷贝构造函数一般就够用了。但是当类持有其它资源时,例如动态分配的内存、指向其他数据的指针等,默认的拷贝构造函数就不能拷贝这些资源了,我们必须显式地定义拷贝构造函数,以完整地拷贝对象的所有数据。

例如,类 Point 中有一个指针变量在构造函数中申请空间,在析构函数中释放空间,如果用浅拷贝,p1 和 p2 中的指针指向的是同一块地址空间,析构时 delete 这块内存两次会出错。

采用深拷贝时,重新分配一块同样大小的内存,并将数据拷贝下来,这样 p1 和 p2 各自指向自己的数据块,析构时释放各自的内存。

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

在这里插入图片描述

// 12-5.cpp
#include<stdio.h>
class Point
{
public:
    Point(int v, int xx = 0, int yy = 0)
    {
        p = new int;
        *p = v;
        x = xx;
        y = yy;
    }
    Point(const Point& pp)
    {
    	p = new int;
    	*p = *pp.p;
        x = pp.x;
        y = pp.y;
    }
    int getX(){ return x; }
    int getY(){ return y; }
    int* getP(){ return p; }
    ~Point(){ delete p; }
private:
    int* p;
    int x;
    int y;    
};
int main() {
    Point p1(3);
    Point p2(p1);
    printf("p1.x = %d, p1.y = %d, p1 = %p\n", p1.getX(), p1.getY(), p1.getP());
    printf("p2.x = %d, p2.y = %d, p2 = %p\n", p2.getX(), p2.getY(), p2.getP());
	return 0;
}
$ g++ 12-6.cpp -o 12-6
$ ./12-6
p1.x = 0, p1.y = 0, p1 = 0x5653bc52fe70
p2.x = 0, p2.y = 0, p2 = 0x5653bc52fe90

从运行结果看到,两个指针指向的内存是不一样的。

3.2 什么时候需要深拷贝?

当对象中有成员使用了系统中的资源时

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

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

3.3 数组类改进

由于前面实现的数组类中动态的申请了空间,这里增加拷贝构造函数实现深拷贝。

// IntArray.h
#ifndef _INTARRAY_H_
#define _INRARRAY_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);
    ~IntArray();
};
#endif
#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 = (index >= 0 && index < m_length);
    if (ret)
    {
        value = m_pointer[index];
    }
    return ret;
}
bool IntArray::set(int index, int value)
{
    bool ret = (index >= 0 && index < m_length);
    if (ret)
    {
        m_pointer[index] = value;
    }
    return ret;
}
IntArray::~IntArray()
{
    delete[]m_pointer;
}
// 12-6.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);
        }
    }
    return 0;
}

4 小结

1、构造函数可以重载
2、对象定义时会触发构造函数的调用
3、可以手动调用构造函数
4、拷贝构造函数的浅拷贝与深拷贝

发布了248 篇原创文章 · 获赞 115 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/happyjacob/article/details/104103078