C++ 类和对象(二)构造函数、析构函数、拷贝构造函数

这篇具有很好参考价值的文章主要介绍了C++ 类和对象(二)构造函数、析构函数、拷贝构造函数。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

C++ 类和对象(二)构造函数、析构函数、拷贝构造函数,C++,c++,开发语言

前言

        本文将介绍类的6个默认成员函数中的构造函数、析构函数和拷贝构造函数,赋值重载和取地址重载涉及运算符重载的知识,将在下篇讲解。所谓默认成员函数,也就是每个类都有的成员函数,我们可以显式定义这些函数,否则,编译器会自动生成它们。

C++ 类和对象(二)构造函数、析构函数、拷贝构造函数,C++,c++,开发语言

目录

前言

1 构造函数

概念

特性

1 函数名与类名相同

2 无返回类型

3 可以重载

4 实例化对象时自动调用

5 默认构造函数

6 合成的默认构造函数

2 析构函数

概念

特性

1 名字为~加类名

2 无参数和返回类型

3 对象生命周期结束时自动调用

4 默认生成的析构函数

3 拷贝构造函数

概念

特性

1 构造函数的一个重载

2 参数只有一个且为引用类型

3 默认生成的拷贝构造函数

4 拷贝构造调用场景


首先定义一个日期类(Date)和一个栈类(Stack),之后将基于它们演示实例,如下:

// 日期类
class Date
{
public:

	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

// 栈类
typedef int DataType;
class Stack
{
public:

	void Init(size_t capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}

	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

    DataType Top()
	{
		if(_size)
			return _array[_size];
	}
	// 其他方法...
	void Destory()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}

private:
	DataType* _array; 
	int _capacity;
	int _size;
};

1 构造函数

概念

        构造函数是一个或几个(函数重载)特殊的函数,它的名字与类名相同,用来初始化类的成员变量。只要类的对象被创建,就会执行(编译器自动调用)构造函数来初始化它,在对象的生命周期内,构造函数只会被执行一次

注意:构造函数的名称虽然叫构造,但它的任务并不是创建对象,而是初始化对象。

特性

1 函数名与类名相同

        日期类已有一个成员函数Init,它的功能就是初始化成员变量。但是每次创建对象后需要显式调用Init函数来初始化,这样太过麻烦。另外,一个对象可以多次调用Init函数为成员变量重新赋值,这并不符合初始化的含义。现在,我们将Init函数改写成构造函数:

class Date
{
public:
    /*void Init(int year, int month, int day)*/
    // 构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

栈类的Init函数改成构造函数:

Stack(size_t capacity = 3)
{
	_array = (DataType*)malloc(sizeof(DataType) * capacity);
	if (NULL == _array)
	{
		perror("malloc申请空间失败!!!");
		return;
	}
	_capacity = capacity;
	_size = 0;
}

2 无返回类型

        我们显式定义了日期类的构造函数,没有写返回类型,因为构造函数是特殊的成员函数,它没有返回值,也不需要像其他无返回值函数那样定义成void类型。

注:C语言中没有显示定义类型的函数默认是void类型,C++中则是int类型。

3 可以重载

        构造函数和其它函数一样可以重载,不同的构造函数的参数类型或数量有所区别。例如,我们可以再定义一个无参的构造函数,将对象初始化成1970.1.1:

// 无参构造函数
Date()
{
	_year = 1970;
	_month = 1;
	_day = 1;
}

对于日期类来说,没有必要定义两个不同的构造函数,它们可以通过设置缺省参数进行合并,如下:

Date(int year = 1970, int month = 1, int day = 1)
{
	_year = year;
	_month = month;
	_day = day;
}

4 实例化对象时自动调用

        关于自动调用,有两方面理解:一是实例化一个对象时,编译器就会自动调用它的构造函数,不需要用户显式调用;二是编译器会自动调用合适的构造函数,对于有多个重载的构造函数,会根据实参数量和类型调用最匹配的一个。例如:

class Date
{
public:

    Date()
	{
		_year = 1970;
		_month = 1;
		_day = 1;
	}

	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1;            // 调用无参构造函数
	Date d2(2023, 1, 1);// 调用带参构造函数

	d1.Print();
	d2.Print();

	return 0;
}

执行结果:

C++ 类和对象(二)构造函数、析构函数、拷贝构造函数,C++,c++,开发语言

注意:通过无参构造函数创建对象时,对象名后面不应该带括号,否则成了函数声明:

Date d1(); // 声明函数d1返回类型是Date

5 默认构造函数

        无参的构造函数和全缺省的构造函数都称为默认构造函数,默认构造函数只能有一个,因为不带参数实例化对象,会造成调用无参构造函数和调用全缺省构造函数的歧义。另外,下面讲的合成的默认构造函数也可以称为默认构造函数,因为只有当类中没有定义构造函数时,编译器才会隐式定义构造函数,也就是说它和其它构造函数不可能同时存在。

6 合成的默认构造函数

        如果类中没有显式定义构造函数,那么编译器会隐式定义一个无参的默认构造函数,它会默认初始化成员变量。

        对于内置类型(C++提供的数据类型:int,char等)的成员变量,将使用编译器提供的默认值初始化,例如:

class Date
{
public:
	// 编译器隐式定义的无参构造函数
	/*Date()
	{

	}*/
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int a;

int main()
{
	Date d;
	d.Print();

	cout << a;
	return 0;
}

执行结果:

C++ 类和对象(二)构造函数、析构函数、拷贝构造函数,C++,c++,开发语言

可以看到,对于类中的int成员变量,编译器合成的默认构造函数将它们初始化成了随机值,这种方式就如同全局变量a被默认初始化成0一样。当然,不同编译器的初始化值可能不同。

        对于自定义类型成员变量(使用struct/class/union等自定义的类型),编译器合成的默认构造函数会调用该类型的默认构造函数,例如:

class A
{
public:
	A()
	{
		_a = 1;
	}

	int _a;
};

class B
{
public:
	A _aa;
};

B b1; // 调用合成的默认构造函数

B类的成员变量_aa是一个A类的对象,实例化一个B类的对象b1,此时编译器会调用A类的默认构造函数来初始化b1的成员变量_aa,结果就是b1._aa._a的值为1。

如果一个类中的自定义类型成员变量没有默认构造函数,那么编译器将无法初始化该成员,大部分类需要我们自己定义默认构造函数

        内置类型成员变量使用默认的初始化值,自定义类型成员变量也只有编译器合成的默认构造函数时,该变量的成员同样使用默认的初始化值,类合成的默认构造函数似乎并没有工作?如何很好地解决此问题:

C++11为合成的默认构造函数新增了初始化规则,即:内置类型成员变量在类中声明时可以给默认值,合成的默认构造函数会使用这些默认值初始化对象的成员变量例如:

class Date
{
public:

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	// 声明时给默认值
	int _year = 1970;
	int _month = 1;
	int _day = 1;
};


int main()
{
    Date d;
    d.Print();

    return 0;
}

执行结果:

C++ 类和对象(二)构造函数、析构函数、拷贝构造函数,C++,c++,开发语言

        以上讲解的都是构造函数通过函数体对成员变量进行初始化,本质上是对它们赋初值。构造函数还有一种初始值列表的用法,实现了真正意义上的初始化工作,该语法将在后续博客讲解。

2 析构函数

概念

         析构函数与构造函数的功能相反,析构函数释放对象申请的空间,并销毁对象的成员变量。对于日期类,它的对象在生命周期结束时,对象的成员变量也就跟着销毁了,它的析构函数没有什么作用;对于栈这样的类,当它的对象生命周期结束时,其动态开辟的内存空间仍然存在,这时析构函数就非常重要。

特性

1 名字为~加类名

       析构函数的名字由字符~接类名构成。在前面的栈类中,已经定义了Destory函数实现清理资源的功能,与Init函数类似,我们需要显式调用它,这个工作通常容易忘记,从而造成资源浪费。现在将Destory函数改写成析构函数:

typedef int DataType;
class Stack
{
public:
    // 构造函数
	Stack(size_t capacity = 3)
	{
        cout << "Stack()" << endl;

		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}

	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

    DataType Top()
	{
		if(_size)
			return _array[_size];
	}
	// 其他方法...

	/*void Destory()*/
    // 析构函数
	~Stack()
	{
        cout << "~Stack()" << endl;

		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}

private:
	DataType* _array; 
	int _capacity;
	int _size;
};

2 无参数和返回类型

        析构函数和构造函数一样没有返回类型,同时析构函数没有参数,这意味着析构函数不能重载,一个类只能有一个析构函数。

3 对象生命周期结束时自动调用

        无论何时一个对象被销毁,就会自动调用它的析构函数。栈类的构造函数和析构函数都增加了一行输出语句,目的是验证它们是否被调用。例如:

int main()
{
	Stack st;
	st.Push(1);

	int top = st.Top();
	cout << top << endl;

	return 0;
}

执行结果:

C++ 类和对象(二)构造函数、析构函数、拷贝构造函数,C++,c++,开发语言

4 默认生成的析构函数

        和构造函数一样,如果类中没有显式定义的析构函数,那么编译器会隐式定义一个析构函数。类对象销毁时,它的成员变量也跟着销毁,对于内置类型的成员变量,不需要额外的处理;对于自定义类型的成员变量,默认生成的析构函数则会调用该类型的析构函数,以清理它申请的资源。例如:

class A
{
public:
	A()
	{
		cout << "A()" << endl;

		_a = (int*)malloc(sizeof(int) * 10);
		memset(_a, 0, 10);
	}

	~A()
	{
		cout << "~A()" << endl;

		if (_a)
		{
			free(_a);
			_a = nullptr;
		}
	}
private:
	int* _a;
};

class B
{
public:
	// 默认生成的无参构造函数
	/*B()
	{

	}*/
	// 默认生成的析构函数
	/*~B()
	{

	}*/
private:
	A _aa;
};

int main()
{
	B b;
	return 0;
}

执行结果:

C++ 类和对象(二)构造函数、析构函数、拷贝构造函数,C++,c++,开发语言

可以看到,B类有一个A类型的成员_aa,实例化一个B类的对象b时,编译器生成的构造函数调用了A类的默认构造函数为_aa初始化,当main函数栈帧结束,b被销毁,编译器默认生成的析构函数调用了A类的析构函数,清理_aa的资源。

3 拷贝构造函数

概念

        拷贝和赋值是一个意思,当以拷贝的方式初始化一个对象时,就会调用拷贝构造函数。拷贝构造函数的参数是自身类类型的引用,一般用const修饰

特性

1 构造函数的一个重载

        拷贝构造函数也是一个构造函数,除了参数的特殊性,其余形式与构造函数相同。接下来定义日期类的拷贝构造函数:

// 日期类的拷贝构造函数
Date(Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

调用拷贝构造初始化对象:

int main()
{
	Date d1(2023, 9, 1);
	Date d2(d1); // 调用拷贝构造
	Date d3 = d2;

	d1.Print();
	d2.Print();
	d3.Print();

	return 0;
}

执行结果:

C++ 类和对象(二)构造函数、析构函数、拷贝构造函数,C++,c++,开发语言

实例化对象d2时,参数并不是3个具体的int类型,而是已经存在的Date类的对象d1,那么就会调用拷贝构造函数,函数体内的功能就是用d1的成员变量赋值给d2的成员变量,从而初始化d2。

注意:Date d3 = d2;也会调用拷贝构造函数,因为其本质也是用已存在的同类型的对象初始化新的对象。

2 参数只有一个且为引用类型

        我们知道类的成员函数都有一个隐含的this指针形参,拷贝构造只有一个参数指的是它只有一个显式的参数,并且该参数必须是该类的同类型的引用。

        对于自定义类型的拷贝,也就是将一个对象的所有成员变量拷贝给另一个对象,那么拷贝构造函数的参数可以是类类型而不是引用吗?先将对象拷贝给函数参数,再用形参这个局部变量拷贝给this对象是否可行:

答案是不可行的,因为传参会调用拷贝构造,调用拷贝构造又要传参...结果就会无穷递归,如下图所示:

C++ 类和对象(二)构造函数、析构函数、拷贝构造函数,C++,c++,开发语言

3 默认生成的拷贝构造函数

        拷贝构造也是默认成员函数,如果类中没有显式定义,则编译器会默认定义一个拷贝构造函数。虽然拷贝构造函数也是构造函数,但是只要没有显式定义拷贝构造函数,即使已经定义了构造函数,也会生成默认拷贝构造函数。可以认为拷贝构造函数是特殊的构造函数,它们之间互不影响。

        默认的拷贝构造函数的作用是按内存储存将一个对象拷贝给新的对象,称之为浅拷贝,对于日期类,我们可以不定义拷贝构造函数,默认的拷贝构造函数执行的功能是相同的。像栈这样的类我们就需要自己定义拷贝构造函数。

        首先分析栈类使用默认的拷贝构造函数的缺陷:

typedef int DataType;
class Stack
{
public:
	// 默认生成的拷贝构造函数功能如下:
	/*Stack(Stack& st)
	{
		_array = st._array;
		_capacity = st._capacity;
		_size = st._size;
	}*/

	Stack(size_t capacity = 3)
	{
		cout << "Stack()" << endl;

		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}

	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	DataType Top()
	{
		if (_size)
			return _array[_size - 1];
	}
	// 其他方法...

	~Stack()
	{
		cout << "~Stack()" << endl;

		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}

private:
	DataType* _array;
	int _capacity;
	int _size;
};

先定义一个对象st1,再用st1拷贝构造对象st2:

int main()
{
	Stack st1;
	Stack st2(st1);

	return 0;
}

执行结果:

C++ 类和对象(二)构造函数、析构函数、拷贝构造函数,C++,c++,开发语言

可以看到析构函数被调用了两次,然后程序就崩溃了,原因是st2是st1通过浅拷贝构造的,它们的成员_array指向同一块内存空间,其中一个对象调用析构函数free了_array它的空间归还给了系统,另一个对象的_array就成了野指针,再去调用析构函数时free野指针引发程序崩溃。

所以对于栈类,我们必须自己定义拷贝构造函数,为对象的_array申请一块与为其拷贝的对象的_array大小相同的内存空间,这样称之为深拷贝

定义栈类的拷贝构造函数:

// 拷贝构造函数
Stack(Stack& st)
{
	_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
	if (NULL == _array)
	{
		perror("malloc申请空间失败!!!");
		return;
	}

	_capacity = st._capacity;
	_size = st._size;
}

总结:如果类的成员没有涉及资源申请,拷贝构造函数是否显式定义都可以;否则,需要我们自己定义拷贝构造函数,避免浅拷贝。

4 拷贝构造调用场景

1 使用已存在对象创建新对象时需要调用拷贝构造函数。

2 函数参数为类类型时,需要调用拷贝构造函数创建临时对象。

3 函数返回类型为类类型时,调用拷贝构造为接收的对象赋值。

为了提高程序效率,减少拷贝构造函数的调用,函数参数应尽量使用引用类型,函数返回值根据实际情况,也尽量选择引用类型。

C++ 类和对象(二)构造函数、析构函数、拷贝构造函数,C++,c++,开发语言

如果本文内容对你有帮助,可以点赞收藏,感谢支持,期待你的关注。

下篇预告:C++ 类和对象(三)运算符重载、const成员函数、实现日期类文章来源地址https://www.toymoban.com/news/detail-706626.html

到了这里,关于C++ 类和对象(二)构造函数、析构函数、拷贝构造函数的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【c++】类和对象(三)构造函数和析构函数

    🔥 个人主页 :Quitecoder 🔥 专栏 :c++笔记仓 朋友们大家好,本篇文章我们带来类和对象重要的部分, 构造函数和析构函数 如果一个类中什么成员都没有,简称为空类 任何类在什么都不写时,编译器会自动生成以下6个默认成员函数 (用户没有显式实现,编译器会生成的成

    2024年04月12日
    浏览(48)
  • 【C++】类和对象(中)---构造函数和析构函数

    个人主页:平行线也会相交💪 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 平行线也会相交 原创 收录于专栏【C++之路】💌 本专栏旨在记录C++的学习路线,望对大家有所帮助🙇‍ 希望我们一起努力、成长,共同进步。🍓 我们知道类包含成员变量和成员函数,当一个类中

    2024年02月05日
    浏览(103)
  • 【C++】类和对象(中)之构造函数与析构函数

    👀 樊梓慕: 个人主页  🎥 个人专栏: 《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》 🌝 每一个不曾起舞的日子,都是对生命的辜负 目录 前言 1.构造函数 1.1概念 1.2特性 2.析构函数 2.1概念 2.2特性 我们继续学习类和对象,今天主要讲解构造

    2024年02月07日
    浏览(49)
  • 【C++入门到精通】C++入门 —— 类和对象(构造函数、析构函数)

    目录 一、类的6个默认成员函数  二、构造函数 ⭕构造函数概念 ⭕构造函数的特点 ⭕常见构造函数的几种类型 三、析构函数      ⭕析构函数概念     ⭕析构函数的特点 ⭕常见析构函数的几种类型 四、温馨提示          这一篇文章是上一篇的续集(这里有上篇链接)

    2024年02月15日
    浏览(53)
  • 【C++】类和对象②(类的默认成员函数:构造函数 | 析构函数)

    🔥 个人主页: Forcible Bug Maker 🔥 专栏: C++ 目录 前言 类的6个默认成员函数 构造函数 概念 构造函数的特性及用法 析构函数 概念 析构函数的特性及用法 结语 本篇主要内容:类的6个默认成员函数中的 构造函数 和 析构函数 进入到类和对象内容的第二节,上篇博客中介绍了

    2024年04月16日
    浏览(57)
  • C++之构造函数、析构函数、拷贝构造函数终极指南:玩转对象的诞生、生命周期与复制

    W...Y的主页 代码片段分享  前言: 在上篇内容里,我们初识了C++中的类与对象,了解了类的定义、类的实例化、 类的作用域等等,今天我们将继续深入了解类与对象的相关内容,学习构造函数、析构函数与拷贝构造函数,话不多说我们发车!!! 目录 类的6个默认成员函数

    2024年02月06日
    浏览(47)
  • 【C++】:类和对象(中)之类的默认成员函数——构造函数and析构函数

    如果一个类中什么成员都没有,简称为空类 空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数 默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数 由于编译器的优化 我们未给_a赋值 这里是不会报

    2024年02月08日
    浏览(51)
  • 【c++】类和对象(四)深入了解拷贝构造函数

    🔥 个人主页 :Quitecoder 🔥 专栏 :c++笔记仓 朋友们大家好啊,本篇内容带大家深入了解 拷贝构造函数 拷贝构造函数是一种特殊的构造函数,在对象需要以同一类的另一个对象为模板进行初始化时被调用。它的主要用途是初始化一个对象,使其成为另一个对象的副本 我们先

    2024年04月16日
    浏览(68)
  • 【C++】类和对象(中)---拷贝构造函数、赋值运算符重载

    个人主页:平行线也会相交💪 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 平行线也会相交 原创 收录于专栏【C++之路】💌 本专栏旨在记录C++的学习路线,望对大家有所帮助🙇‍ 希望我们一起努力、成长,共同进步。🍓 拷贝构造函数,又称复制构造函数,是一种特殊的

    2024年02月05日
    浏览(49)
  • C++ -3- 类和对象 (中) | 拷贝构造函数 & 赋值运算符重载

    示例: 拷贝初始化构造函数的作用是将一个已知对象的数据成员值拷贝给正在创建的另一个同类的对象 无穷递归 ? Date(Date d){……} 首先,分析 传值传参的过程 传引用传参 : 没有拷贝 的过程,直接传 传值传参: 内置类型 编译器可以直接拷贝(浅拷贝/值拷贝——一个字节

    2023年04月19日
    浏览(80)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包