[C++]构造与毁灭:深入探讨C++中四种构造函数与析构函数

news2024/11/28 3:31:53
  •  个人主页:北·海
  •  🎐CSDN新晋作者
  •  🎉欢迎 👍点赞✍评论⭐收藏
  • ✨收录专栏:C/C++
  • 🤝希望作者的文章能对你有所帮助,有不足的地方请在评论区留言指正,大家一起学习交流!🤗

目录

构造函数有什么作用?

构造函数有什么特点

构造函数的种类

一.默认构造函数

1.什么是默认构造函数

2.默认构造函数的应用

使用情况一:类内初始值

使用情况二:创建对象数组

使用情况三:在派生类中

自定义的默认构造函数

 二.自定义的重载构造函数

1.构造函数的作用

三.拷贝构造函数

1.浅拷贝

2.深拷贝

3.什么时候用到深拷贝/浅拷贝

4.什么时候会调用拷贝构造函数

 四.赋值构造函数

1.赋值构造函数可以怎么样定义

2.赋值构造函数在什么时候会调用?

2.赋值构造函数与拷贝构造函数的区别

五.析构函数

1.析构函数的基本概念

2.容易将调用析构函数与delete释放内存混淆


 概要:

"构造与毁灭,是C++中对象生命周期的两个重要阶段。构造函数用于初始化对象的状态和数据成员,为对象提供合适的初始值;而析构函数则在对象销毁时执行清理工作,释放资源,确保对象的安全结束。通过合理设计构造函数和析构函数,我们能够在对象的创建和销毁过程中维护良好的程序行为和资源管理。


 

构造函数有什么作用?

        在创建一个新的对象时,自动调用的函数,用来进行“初始化”工作:对这个对象内部的数据成员进行初始化

构造函数有什么特点

  1. 自动调用(在创建新对象时,自动调用)
  2. 构造函数的函数名,和类名相同
  3. 构造函数没有返回类型
  4. 可以有多个构造函数(即函数重载形式)

构造函数的种类

     1. 默认构造函数

     2. 自定义的构造函数

     3.拷贝构造函数

     4.赋值构造函数


一.默认构造函数

1.什么是默认构造函数

    1.没有参数的构造函数成为默认构造函数

    2.没有手动定义默认构造函数时,编译器自动为这个类定义一个构造函数。

    3. 如果数据成员使用了“类内初始值”,就使用这个值来初始化数据成员。否则,就使用默认初始化(实际上,不做任何初始化)只有C++11可以使用类内初始值

    

2.默认构造函数的应用

  使用情况一:类内初始值

   以上是当全部成员变量有"类内初始值"时,可以使用默认构造函数

   使用情况二:创建对象数组

      由此报错可以看出,对象数组的创建,必须使用默认的构造函数,在上面87行写了自定义的构造函数之后,程序就不会默认生成默认构造函数了,要想在有自定义构造函数的情况下,创建对象数组,就必须再重载一个不含参数的默认构造函数

使用情况三:在派生类中

派生类没有显式定义构造函数时,它将继承基类的默认构造函数。这使得在创建派生类对象时,基类的成员变量可以正确地初始化。

#include <iostream>
using namespace std;

// 基类
class Base {
public:
    int baseValue;

    // 默认构造函数
    Base() {
        baseValue = 0;
        cout << "Base 默认构造函数被调用" << endl;
    }
};

// 派生类
class Derived : public Base {
public:
    int derivedValue;

    void printValues() {
        cout << "Base 值: " << baseValue << endl;
        cout << "Derived 值: " << derivedValue << endl;
    }
};

int main() {
    Derived derivedObj;
    derivedObj.derivedValue = 10;
    derivedObj.printValues();
    return 0;
}


输出:
Base 默认构造函数被调用
Base 值: 0
Derived 值: 10

可以看到,派生类的对象 derivedObj 在创建时成功继承了基类 Base 的默认构造函数,并正确地初始化了基类的成员变量 baseValue这证实了在派生类中没有显式定义构造函数时,它将继承基类的默认构造函数,并能够正确初始化基类的成员变量。

注意:

只要手动定义了任何一个构造函数,编译器就不会生成“默认构造函数”

一般情况下,都应该定义自己的构造函数,不要使用“默认构造函数”

仅当数据成员全部使用了“类内初始值”,才宜使用“合成的默认构造函数”

自定义的默认构造函数

如果既有类内初始值,在默认构造函数里面又有初始化,则以默认构造函数里的为准

说明:如果某数据成员使用类内初始值,同时又在构造函数中进行了初始化,那么以构造函数中的初始化为准。相当于构造函数中的初始化,会覆盖对应的类内初始值。


 二.自定义的重载构造函数

1.构造函数的作用

  1. 初始化对象:自定义构造函数允许你在创建对象时对其进行初始化。您可以通过构造函数的参数传递初始值,或在构造函数中执行特定的初始化操作,确保对象在创建时处于正确的状态。

  2. 参数化构造:自定义构造函数可以接受参数,从而使对象的创建更加灵活和可定制化。通过不同的构造函数形式,可以为不同的使用场景提供不同的对象初始化方式。

  3. 代码可读性和维护性:通过显式定义构造函数,可以提高代码的可读性和维护性。构造函数明确地指示了对象的创建方式和初始化过程,使代码更加清晰易懂,并且便于后续修改维护。

// 定义一个“人类”
class Human {
public:  
	Human();
	Human(int age, int salary);//自定义构造函数

	string getName();
	int getAge();
	int getSalary();

private:
	string name = "Unknown";
	int age = 28;
	int salary;
};

Human::Human() {
	name = "无名氏";
	age = 18;
	salary = 30000;
}

Human::Human(int age, int salary) {
	cout << "调用自定义的构造函数" << endl; 
	this->age = age;      //this是一个特殊的指针,指向这个对象本身
	this->salary = salary;
	name = "无名";
}



string Human::getName() {
	return name;
}

int Human::getAge() {
	return age;
}

int Human::getSalary() {
	return salary;
}


int main(void) {
	Human  h1(25, 35000);  // 使用自定义的默认构造函数

	cout << "姓名:" << h1.getName() << endl;
	cout << "年龄: " << h1.getAge() << endl;    
	cout << "薪资:" << h1.getSalary() << endl; 

	system("pause");
	return 0;
}

由此可以看出,自定义构造函数,只是比默认构造函数多了形参,由于创建的对象都形形色色,所以大多数情况下都会用自定义构造函数,在定义对象时,将参数传递进去给对象赋值


三.拷贝构造函数

1.浅拷贝

浅拷贝是指在对象拷贝过程中,仅简单地复制对象的成员变量值,而不复制成员变量所指向的动态分配的内存。这意味着原对象和拷贝对象将共享同一块内存区域,对其中一个对象的修改会影响到另一个对象。

在进行浅拷贝时,通常会使用默认的拷贝构造函数或赋值运算符重载来完成。这些默认的复制操作只会简单地逐个拷贝对象的成员变量的值。

class Human {
private:
	int Age=30;
	string Name ="LiHua";
public:
	Human(int age,string name);
	int getAge();
	string getName();

	void print();
};


int Human::getAge() {
	return this->Age;
}

string Human::getName() {
	return this->Name;
}

void Human::print() {
	cout << "姓名 : " << this->getName() << endl;
	cout << "年龄 : " << this->getAge() << endl;
}
Human::Human(int age,string name) {
	this->Age = age;
	this->Name = name;
}
int main() {

	Human h1(19,"Lihua");
	Human h2 = h1;//给对象赋值方法一
	Human h3(h2);//给对象赋值方法二

	h1.print();
	h2.print();
	h3.print();
}

由此可以得出:给对象的赋值方法有两种,这两种方法都会调用拷贝构造函数,由于三个变量用的一块内存,所以说,改其中一个对象的值,其他三个的值都会被改变

上面两张图片中输出的地址是一个地址,改变一个对象的值,另一个对象的值也会改变,这就验证了,浅拷贝的结果是两个对象共用了一个地址

说明 : 默认的拷贝构造函数的缺点: 使用“浅拷贝

2.深拷贝

根据上面浅拷贝中的例子,其中有个地址,如果h1想要改addr,但是h2却不想改他的addr,这种情况就需要用到深拷贝了,深拷贝就必须得写自定义的拷贝构造函数,例子如下:

class Human {
private:
	int Age=30;
	string Name ="LiHua";
    char* addr;
public:
	Human(int age,string name);
	Human(const Human& the);
    ~Human();//析构函数

	int getAge();
	string getName();

	void setAge(int age);
	void setName(string name);
	void setAddr(const char* addr);
	
	void print();

};
int Human::getAge() {
	return this->Age;
}

string Human::getName() {
	return this->Name;
}

Human::~Human(){

delete addr;
}

void Human::print() {
	cout << "姓名 : " << this->getName() << endl;
	cout << "年龄 : " << this->getAge() << endl;
	cout << "地址 : " << this->addr << endl;
	printf("该对象的addr地址 : %p\n", this->addr);
}
Human::Human(int age,string name) {
	this->Age = age;
	this->Name = name;
	addr = new char[64];
	strcpy(addr, "China");
}
void Human::setName(string name) {
	this->Name = name;
}
void Human::setAge(int age) {
	this->Age = age;
}
void Human::setAddr(const char* addr) {
	if (!addr) {
		return;
	}
	strcpy(this->addr, addr);
}
Human::Human(const Human& the) {
	this->Age = the.Age;
	this->Name = the.Name;
	//给拷贝对象的addr重新分配内存,存储该地址
	this->addr = new char[64];
	strcpy(this->addr, the.addr);
}
int main() {

	Human h1(19,"Lihua");
	Human h2(h1);//给对象赋值方法一
	//Human h3=h1;//给对象赋值方法二

	h1.print();
	h2.print();
	//addr是char*类型
	h2.setAddr("美国");
	cout << "------------------------" << endl;
	h1.print();
	h2.print();
}

可以看到,此时通过深拷贝,两个对象的addr地址已经改变了

3.什么时候用到深拷贝/浅拷贝

  1. 对象拥有动态分配的资源:如果对象包含了动态分配的内存(如使用 newmalloc 创建的内存),在进行拷贝时需要进行深拷贝。这是因为默认的浅拷贝(shallow copy)只会复制指针,而不会为新对象分配独立的内存空间,这可能会导致多个对象指向同一内存,造成资源释放问题或不可预测的行为。

  2. 修改的独立性要求:如果你需要在拷贝对象后对其进行修改,而不希望修改影响到原始对象,那么深拷贝是必需的。深拷贝会创建一个完全独立的副本,修改副本不会影响原始对象。

  3. 对象包含指向其他对象的引用:当一个对象包含指向其他对象的引用或指针时,进行拷贝时可能需要进行深拷贝。这样可以确保每个对象都有自己的引用,而不是共享同一个引用。

需要注意的是,并非所有情况都需要进行深拷贝。有时候浅拷贝已经足够满足需求,尤其是当拷贝的对象是只读的或者没有包含指向动态分配资源的指针时。

在 C++ 中,可以通过自定义拷贝构造函数和赋值运算符重载来实现深拷贝

4.什么时候会调用拷贝构造函数

1.调用函数时,实参是对象,形参不是引用类型

2.函数的返回类型是类.而且不是引用类型

3.对象数组的初始化列表中,使用对象 


 四.赋值构造函数

1.赋值构造函数可以怎么样定义

 使用重载运算符 " = "进行实现

class Human {
private:
	int age;
	string name;
	char* addr;

public:
	//通过重载 " = "实现
	Human& operator=(const Human& other);
	
	Human(int age, string name,const char*addr);
	Human(){}
	~Human() {
		cout << "调用析构函数" << endl;
		delete addr;
	}

	void print() {
		cout << "age :" << age << endl;
		cout << "name : " << name << endl;
		cout << "addr : " << addr << endl;
	}
};

Human& Human::operator=(const Human& other) {
	if (this == &other) {
		return *this;
	}

	addr = new char[64];
	strcpy(addr, other.addr);

	age = other.age;
	name = other.name;

	return *this;
}
Human::Human(int age, string name, const char* addr) :age(age), name(name) {
	this->addr = new char[64];
	strcpy(this->addr, addr);
}

int main() {
	Human h1(23, "LiHua", "Chinese"),h2;
	h2 = h1;
	h1.print();
	cout << "************" << endl;
	h2.print();

}

2.赋值构造函数在什么时候会调用?

当用一个对象给另一个对象进行赋值时候会被调用,定义加赋值的话调用拷贝构造函数,例如:

int main(){

Test p1(20);
Test p2 = p1;
此时会调用拷贝构造函数
}
class Test{

.....
public:

Test(int n){
this.age  =20;
}
Test test(Test & man){
//内联函数
return man;//返回对象
}

};

int main(){

Test p1(20),p2;
p2 = p1;//此时会调用赋值构造函数

Test p3 = h1;//此时创建对象p3同时初始化,会调用的是拷贝构造函数 

p2 = test(p1);//此时会调用赋值构造函数

Test p4  =test(p1);//此时会调用拷贝构造函数

}

2.赋值构造函数与拷贝构造函数的区别

  1. 触发时机:拷贝构造函数在创建一个新对象并初始化时被调用,而赋值构造函数在已存在的对象进行赋值操作时被调用。

  2. 参数类型:拷贝构造函数使用被拷贝对象的引用作为参数,通常是常量引用,用于初始化新对象;而赋值构造函数使用所需赋值的对象的引用作为参数,用于将已存在的对象赋值给另一个已存在的对象。

  3. 功能:拷贝构造函数的主要目的是创建一个新对象,并将其初始化为与被拷贝对象相同的值。它通常用于深拷贝,确保新对象与原对象是独立的。赋值构造函数的主要目的是将一个已经存在的对象的值赋给另一个已经存在的对象

  4. 默认实现:如果没有显式定义拷贝构造函数和赋值构造函数,C++ 编译器会为类生成默认的拷贝构造函数和默认的赋值构造函数。默认的拷贝构造函数会执行浅拷贝,简单地将成员变量的值复制给新对象。默认的赋值构造函数也执行浅拷贝,将每个成员变量的值从一个对象复制到另一个对象。

需要明确的是,拷贝构造函数和赋值构造函数在语法上是不同的,它们具有不同的函数形参和使用方式。在设计类时,根据对象的需求,需要选择正确的构造函数来满足对象的初始化和赋值操作。


五.析构函数

1.析构函数的基本概念

作用:

对象销毁前,做清理工作。

具体的清理工作,一般和构造函数对应

比如:如果在构造函数中,使用new分配了内存,就需在析构函数中用delete释放

如果构造函数中没有申请资源(主要是内存资源),

那么很少使用析构函数。

函数名:

~类型

没有返回值,没有参数,最多只能有一个析构函数

访问权限:

一般都使用public

使用方法:

不能主动调用。

对象销毁时,自动调用。

如果不定义,编译器会自动生成一个析构函数(什么也不做)

2.容易将调用析构函数与delete释放内存混淆

在我接触析构函数的时候,一直懂得就是在对象被销毁的时候才会调用析构函数,但是每次在类的成员函数中遇到new出来的空间时,就会以为将delete写在该成员函数中,然后delete执行了就会去调用析构函数,这个从逻辑上都是说不过去的

现在懂得概念很明确,只有在对象销毁的时候调用,在类的成员函数或者构造函数中遇到new出来的空间时候,就应该给析构函数里面写delete释放该空间,等到对象被销毁的时候,就会去调用析构函数,就会执行delete语句将内存释放

现在来看这么一个简单的例子,输出的顺序,就明白,只有对象被销毁的时候才会调用析构函数,遇见了new,就应该在析构函数里面写delete

class Human {
private:
	int age;
	string name;
	char* addr;

public:
	Human() {
		cout << "调用构造函数" << endl;
		addr = new char[64];
	}

	~Human() {
		cout << "调用析构函数" << endl;

		delete[]this->addr;
	}

	void print() {
		cout << "调用print函数" << endl;
		strcpy(addr, "China");
		cout << addr << endl;
	}
};

int main() {
	Human p1;
	p1.print();
}


总结:

"在C++中,构造函数和析构函数是类的特殊成员函数,它们扮演着至关重要的角色。通过构造函数,我们可以初始化对象的状态,确保对象在被创建时处于合适的状态。而析构函数则负责在对象生命周期结束时进行善后工作,释放动态分配的资源,保证对象的安全销毁。深入理解和合理设计构造函数和析构函数,可以帮助我们编写更可靠、高效的C++程序,有效地管理资源,避免内存泄漏和访问冲突。构造与毁灭是C++编程中的关键概念,它们共同构成了面向对象编程中的基石,为我们提供了强大而灵活的工具,让我们能够更好地利用和管理对象的生命周期。" 

 


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/952291.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ModaHub魔搭社区:星环科技致力于打造更优越的向量数据库

在数字化时代,数据成为了最重要的资源之一。随着人工智能、大数据等技术的不断发展,向量数据库成为了处理这类数据的关键工具。星环科技作为一家专注于数据存储和管理技术的公司,其重要目标就是将向量数据库打造得更为优越。 在星环科技,有一个专注于向量数据库的团队。这个…

当面试遇到难题:解决棘手问题的三大策略

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

【C++】关于fixed和setprecision的学习和介绍

前言 在学习swap函数的时候&#xff0c;偶然了解到了fixed和setprecision&#xff0c;这两条控制语句&#xff0c;在了解了之后&#xff0c;觉得很有用&#xff0c;于是写一篇文章来介绍fixed和setprecision这两条控制语句 fixed控制输出形式 使用fixed语句需要包含<ioma…

Python2021年06月Python二级 -- 编程题解析

题目一 没有重复数字的两位数统计 编写一段程序&#xff0c;实现下面的功能: (1)检查所有的两位数; (2)程序自动分析两位数上的个位与十位上的数字是否相同&#xff0c;相同则剔除&#xff0c; 不同则保留(例:12符合本要求&#xff0c;个位是2&#xff0c;十位是1&#xff0c;两…

第7节——渲染列表+Key作用

一、列表渲染 我们再react中如果渲染列表&#xff0c;一般使用map方法进行渲染 import React from "react";export default class LearnJSX2 extends React.Component {state {infos: [{name: "张三",age: 18,},{name: "李四",age: 20,},{nam…

C#,数值计算——Midexp的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { public class Midexp : Midpnt { public new double func(double x) { return funk.funk(-Math.Log(x)) / x; } public Midexp(UniVarRealValueFun funcc, double aa, d…

跳槽面试:如何转换工作场所而不失去优势

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

16 个前端安全知识

16 个前端安全知识 去年 security course 上的是 React&#xff0c;然后学了一些 一些 React 项目中可能存在的安全隐患&#xff0c;今年看了一下列表&#xff0c;正好看到了前端也有更新&#xff0c;所以就把这个补上了。 一个非常好学习各种安全隐患的机构是 https://owasp…

《Python入门到精通》webbrowser模块详解,Python webbrowser标准库,Python浏览器控制工具

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;小白零基础《Python入门到精通》 webbrowser模块详解 1、常用操作2、函数大全webbrowser.open() 打开浏览器webbro…

Django请求的生命周期

Django请求的生命周期是指: 当用户在浏览器上输入URL到用户看到网页的这个时间段内&#xff0c;Django后台所发生的事情。 直白的来说就是当请求来的时候和请求走的阶段中&#xff0c;Django的执行轨迹。 一个完整的Django生命周期: 用户从客户端发出一条请求以后&#xff…

Kubernetes技术--Kubernetes架构组件以及核心概念

1.Kubernetes集群架构组件 搭建一个Kubernetes环境集群,其架构如下所示: 内容详解: Master:控制节点,指派任务、决策 Node:工作节点,实际干活的。 Master组件内容:

Python小知识 - 如何使用Python的Flask框架快速开发Web应用

如何使用Python的Flask框架快速开发Web应用 现在越来越多的人把Python作为自己的第一语言来学习&#xff0c;Python的简洁易学的语法以及丰富的第三方库让人们越来越喜欢上了这门语言。本文将介绍如何使用Python的Flask框架快速开发Web应用。 Flask是一个使用Python编写的轻量级…

迈向无限可能, ATEN宏正领跑设备切换行业革命!

随着互联网在各个领域的广泛应用,线上办公这一不受时间和地点制约、不受发展空间限制的办公模式开始广受追捧,预示着经济的发展正朝着新潮与活跃的方向不断跃进。当然,在互联网时代的背景下,多线程、多设备的线上办公模式也催生了许多问题:多设备间无法进行高速传输、切换;为保…

Mybatis 日志(JDK Log)

上一篇我们介绍了Mybatis中的参数&#xff0c;本篇我们使用JDK Log打印一下Mybatis运行时的日志&#xff0c;看一下Mybatis执行的过程。 这里我选取上一篇的示例进行JDK Log的集成&#xff0c;这里如果您想对上一篇进行详细了解&#xff0c;可以参考&#xff1a; Mybatis参数…

【python爬虫】4.爬虫实操(菜品爬取)

文章目录 前言项目&#xff1a;解密吴氏私厨分析过程代码实现&#xff08;一&#xff09;获取与解析提取最小父级标签一组菜名、URL、食材写循环&#xff0c;存列表 代码实现&#xff08;二&#xff09;复习总结 前言 上一关&#xff0c;我们学习了用BeautifulSoup库解析数据和…

说说构建流批一体准实时数仓

分析&回答 基于 Hive 的离线数仓往往是企业大数据生产系统中不可缺少的一环。Hive 数仓有很高的成熟度和稳定性&#xff0c;但由于它是离线的&#xff0c;延时很大。在一些对延时要求比较高的场景&#xff0c;需要另外搭建基于 Flink 的实时数仓&#xff0c;将链路延时降低…

国标视频云服务EasyGBS国标视频平台迁移服务器后无法启动的问题解决方法

国标视频云服务EasyGBS支持设备/平台通过国标GB28181协议注册接入&#xff0c;并能实现视频的实时监控直播、录像、检索与回看、语音对讲、云存储、告警、平台级联等功能。平台部署简单、可拓展性强&#xff0c;支持将接入的视频流进行全终端、全平台分发&#xff0c;分发的视频…

即插即生产与基于技能的设计

智能制造领域的主要研究工作就是为制造领域所有事物和行为构建数字化模型。最终实现制造工厂中设备&#xff0c;软件&#xff0c;物流所有事物的互联互通。而且实现这种互联互通是便捷&#xff0c;灵活的。通俗地将就是“即插即生产”。不过&#xff0c;要实现这一目标并非易事…

【C#每日一记】常用泛型数据结构类及题单实践回顾

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

【算法题】1761. 一个图中连通三元组的最小度数

题目&#xff1a; 给你一个无向图&#xff0c;整数 n 表示图中节点的数目&#xff0c;edges 数组表示图中的边&#xff0c;其中 edges[i] [ui, vi] &#xff0c;表示 ui 和 vi 之间有一条无向边。 一个 连通三元组 指的是 三个 节点组成的集合且这三个点之间 两两 有边。 连…