【C++】构造函数、初始化列表,析构函数,拷贝构造函数,运算符重载

 注:本博客图片来源于学习笔记: 学习笔记icon-default.png?t=N7T8https://gitee.com/box-he-he/learning-notes

完整思维导图请前往该博主码云下载。


目录

 注:本博客图片来源于学习笔记: 学习笔记https://gitee.com/box-he-he/learning-notes

完整思维导图请前往该博主码云下载。

一、构造函数

构造函数的形式及其使用:

初始化列表:

explicit关键字

二、析构函数

1 .概念

2. 特性

析构函数的调用顺序

三、拷贝构造函数

特性:

四、运算符重载(部分)

1、概念

2、赋值运算符重载

3、前置++和后置++的实现


 

一、构造函数

构造函数是一种特殊的成员函数,在创建对象时自动调用,用于初始化对象的成员变量和执行其他必要的操作。它的名称与类名相同,没有返回类型,并且可以被重载。

构造函数有以下几个重要的特点:

  1. 构造函数的名称与类名相同,包括大小写和命名空间。
  2. 构造函数没有返回类型。
  3. 构造函数可以被重载,即可以定义多个具有不同参数列表的构造函数。
  4. 构造函数可以有默认参数,从而允许在创建对象时省略某些参数。
  5. 构造函数可以有任意数量的初始化列表,用于按照指定顺序初始化对象的成员变量。
  6. 构造函数可以访问类的所有成员变量和成员函数。
  7. 构造函数在创建对象时自动调用,无需手动调用。
  8. 对象的整个生命周期只调用一次。

构造函数主要用于以下几个方面(功能):

  1. 初始化成员变量:构造函数通过参数或初始化列表来初始化对象的成员变量,确保对象在创建时具有特定的初始值。
  2. 分配内存或资源:如果对象需要动态分配内存或管理其他资源(如文件句柄),构造函数可以在创建对象时进行相应的操作,从而确保对象可以正确地使用这些资源。
  3. 执行必要的操作:有时候,创建对象可能需要执行一些必要的操作,比如打开文件等,构造函数可以在创建对象时执行这些操作。

需要注意以下几点:

  1. 一个类可以有多个构造函数,它们具有不同的参数列表。这被称为构造函数的重载。
  2. 如果没有显式地定义构造函数,编译器会自动生成一个默认构造函数,但它只会对自定义类型变量进行默认初始化,对内置类型(int, double, float等)无法初始化。
  3. 编写构造函数时应该根据类的需求来初始化成员变量,并且可以根据需要执行其他必要的操作。
  4. 如果自定义了构造函数,编译器将不再生成默认构造函数。如果需要同时拥有默认构造函数和自定义构造函数,可以提供默认参数或者显式定义一个无参数的默认构造函数。

构造函数的形式及其使用:

默认构造函数包括:1、无参构造函数;2、全缺省的构造函数;3、编译器默认生成的构造函数 

当用户自己定义构造函数时 ,必须存在一个默认构造函数。

构造函数的使用:

 class Date{public:// 1.无参构造函数(默认构造函数)Date(){}// 2.带参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}// 3.全缺省的构造函数(默认构造函数)Date(int year = 2024, int month = 2, int day = 6){_year = year;_month = month;_day = day;}// 4.编译器默认生成的构造函数(默认构造函数)private:int _year;int _month;int _day;};void TestDate(){Date d1; // 调用无参构造函数Date d2(2015, 1, 1); // 调用带参的构造函数// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)Date d3();}

需要注意的是全缺省的构造函数和无参的构造函数都属于默认构造函数,不能同时存在。

同时 函数2 与 函数3 无法构成函数重载,会造成函数的重定义,也无法同时存在。

 补充:由于编译器默认生成的构造函数无法对内置类型进行初始化,在C++11标准中,对此引入了解决方案:在变量声明时可以为其提供默认值。

 class Date{private:int _year = 2024;int _month = 2;int _day = 6;};void TestDate(){Date d; // 此时d的私有成员初始化为2024/2/6}

 

初始化列表:

1、基本形式

初始化列表:以一个 冒号开始 ,接着是一个以 逗号分隔的数据成员列表 ,每个 " 成员变量 " 后面跟
一个 放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day): _year(year), _month(month), _day(day){}private:
int _year;
int _month;
int _day;
};
【注意】
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
  1. 引用成员变量
  2. const成员变量
  3. 自定义类型成员(且该类没有其默认构造函数时)        
class A
{
public:A(int a):_a(a){}
private:int _a;
};
class B
{
public:B(int a, int ref):_aobj(a), _ref(ref), _n(10){}
private:A _aobj;  // 没有默认构造函数的自定义类型int& _ref; // 引用const int _n; // const 
};

特性:

1、即使不在初始化列表中初始化自定义类型成员,其也会在初始化列表中隐式地调用其构造函数进行初始化。

2、初始化列表的执行顺序是其在类中的声明顺序,与初始化列表中的顺序无关。

explicit关键字

explicit 是一个关键字,用于修饰单参数构造函数,它可以防止编译器进行隐式的类型转换。在 C++ 中,当我们定义一个单参数构造函数时,编译器会自动进行隐式类型转换,将参数类型转换为目标类型,然后调用该构造函数来创建对象。而使用 explicit 关键字修饰的构造函数将被标记为禁止隐式类型转换。

下面是两个具体的示例来说明 explicit 关键字的作用:

1、单参构造函数

class MyClass {
public:explicit MyClass(int x) : value(x) {}int getValue() const {return value;}private:int value;
};int main() {MyClass obj1(5);       // 直接调用带有 int 参数的构造函数int val1 = obj1.getValue();MyClass obj2 = 10;    // 编译错误!禁止隐式类型转换int val2 = obj2.getValue();return 0;
}

在上述示例中,MyClass 类定义了一个带有单参数的构造函数,并使用 explicit 关键字进行修饰。这意味着我们只能通过显式地调用构造函数来创建对象,而不能进行隐式的类型转换。在 main 函数中,我们可以看到以下几点:

  1. 创建 obj1 对象时,我们直接调用了构造函数,不会发生隐式类型转换。这是合法的。

  2. 尝试使用 obj2 = 10; 进行从 int--->MyClass 隐式类型转换时,编译器会抛出错误。因为我们使用了 explicit 关键字,禁止隐式类型转换。

2、半缺省的构造函数

class MyClass {
public://虽然有多个参数,但是创建对象时后两个参数可以不传递,使用explicit修饰,// 不具有类型转换作用,编译不通过explicit MyClass(int x, int y = 1, int z = 1) : value(x), data(y), temp(z){}int getValue() const {return value;}private:int value;int data;int temp;
};int main() {MyClass obj1(5);       // 直接调用带有 int 参数的构造函数int val1 = obj1.getValue();MyClass obj2 = 10;    // 编译错误!禁止隐式类型转换int val2 = obj2.getValue();return 0;
}

二、析构函数

1 .概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?              

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由    

编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

【注意】:析构函数只负责销毁占用的额外资源,即额外申请的内存空间,例如:malloc,new等。而对象本身,如果定义在函数中,则其本身存在于栈区中,随着函数的栈帧一同销毁,与析构函数无关。

2. 特性

析构函数是特殊的成员函数,其特征如下:

1. 析构函数名是在类名前加上字符 ~。

2. 无参数无返回值类型。

3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构

函数不能重载

4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

typedef int DataType;
class Stack
{
public:Stack(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++;}//析构函数~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};
void TestStack()
{Stack s;s.Push(1);s.Push(2);
}

 

析构函数的调用顺序

  


三、拷贝构造函数

拷贝构造函数(Copy Constructor)是一种特殊的构造函数,它接受一个对象作为参数,用于创建一个新的对象,这个新对象与原始对象具有相同的值。

在C++中,如果我们没有显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。默认拷贝构造函数执行的是逐个成员的拷贝,将源对象的每个成员变量的值复制给新创建的对象。

以下是一个简单的示例来说明拷贝构造函数的用法:

class MyClass {
public:int value;// 拷贝构造函数MyClass(const MyClass& other) {value = other.value;}
};int main() {MyClass obj1;obj1.value = 10;MyClass obj2(obj1);  // 使用拷贝构造函数创建新对象// 或者 MyClass obj2 = obj1;return 0;
}

在上述示例中,我们定义了一个名为 MyClass 的类,并在其中定义了一个拥有一个整数成员变量 value 的对象。然后我们在 main 函数中创建了两个对象 obj1 和 obj2

  • obj1 是通过默认构造函数创建的,并将 value 设置为 10。

  • obj2 是通过拷贝构造函数创建的,它的参数是另一个 MyClass 对象 obj1。在拷贝构造函数中,我们将 obj1 的值复制给了 obj2

需要注意的是,拷贝构造函数的参数应该是一个 const 引用(const MyClass& other),以确保不会修改被传递的源对象。

拷贝构造函数在以下情况下会被隐式调用:

  1. 通过赋值运算符(=)进行对象的初始化。
  2. 将一个对象作为函数的参数传递给另一个对象。
  3. 从一个函数返回对象时。

需要注意的是,当类中包含指针成员变量时,需要手动定义拷贝构造函数,以确保指针指向独立的内存,避免浅拷贝引发的指针悬挂问题。

 

特性:

1. 拷贝构造函数是构造函数的一个重载形式

2. 拷贝构造函数的参数只有一个必须是类类型对象的引用,使用传值方式编译器直接报错
因为会引发无穷递归调用。
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// Date(const Date& d)   // 正确写法Date(const Date d)   // 错误写法:编译报错,会引发无穷递归{_year = d._year;_month = d._month;_day = d._day;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;Date d2(d1);return 0;
}

 

 为什么值传递会引起无穷递归呢?为什么引用不会呢?

  答:假如参数不是引用,是值传递,那么传递给函数的值其实就是实参的一个临时拷贝,那在对实参进行临时拷贝时还是需要再次去调用我Date类里的拷贝构造函数,由于一直进行的是值传递,所以这个过程是无限递归的。但是如果拷贝构造函数的参数是对实参的引用,就不需要对实参再进行临时拷贝。引用是实参的一个别名,他们指向的也是同一块内存,所以不需要传递值,而是通过实参的引用来对实参的内存进行读写。

3. 若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按

    字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝

class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};
int main()
{Date d1;// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构//造函数Date d2(d1); // Date d2 = d1; 也是默认调用的拷贝函数return 0;
}

 【注意】:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?

当然像日期类这样的类是没必要的。

但是对于初始化时成员需要额外申请内存的类,此时我们使用默认拷贝函数会发生程序崩溃,这时我们必须手动创建一个拷贝构造函数进行深拷贝。例如下面的栈class Stack;

typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");return;}_size = 0;_capacity = capacity;}// 如果我们在这只是进行简单的值拷贝,程序将会崩溃Stack(const Stack& s){_array = new DataType[_capacity];memcpy(_array, s._array, _size * sizeof(DataType));_size = s._size;_capacity = s._capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _array;size_t _size;size_t _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);return 0;
}

 

【注意】:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
 

5. 拷贝构造函数典型调用场景:     

  • 使用已存在对象创建新对象

  • 函数参数类型为类类型对象

  • 函数返回值类型为类类型对象

class MyClass {
public:int value;// 拷贝构造函数MyClass(const MyClass& other) {value = other.value;}
};MyClass Test(const MyClass obj3)
{MyClass temp;return temp; // 函数返回值类型为类类型对象// 由于temp为局部变量,在函数结束后不存在,所以需对temp进行临时拷贝,// 返回值的实质是temp的一份临时拷贝
}int main() {MyClass obj1;obj1.value = 10;MyClass obj2(obj1);  // 或者 MyClass obj2 = obj1;使用已存在的对象创建新对象Test(obj2);          // 函数的参数类型为类类型对象return 0;
}

四、运算符重载(部分)

1、概念

C++ 为了增强代码的可读性引入了运算符重载 运算符重载是具有特殊函数名的函数 ,也具有其
返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字 operator 后面接需要重载的运算符符号
函数原型: 返回值类型  operator 操作符 ( 参数列表 )
【注意】:
  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型参数
  3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  4. 作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐
    藏的 this
  5. .* :: sizeof ?: . 注意以上5个运算符不能重载。

2、赋值运算符重载


1、重载运算符的格式

  • 参数类型:const T&,传递引用可以提高传参效率

  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

  • 检测是否自己给自己赋值

  • 返回*this :要复合连续赋值的含义

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}Date& operator=(const Date& d){if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}
private:int _year;int _month;int _day;
};

 

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数

【原因】:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数。

【注意】:如果需要在全局中重载其他运算符,需要给两个参数。因为只有类的非静态成员函数具有this指针,全局函数没有。

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

【注意】:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
 

3、前置++和后置++的实现

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 前置++:返回+1之后的结果// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率Date& operator++(){_day += 1;return *this;}// 后置++:// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器//自动传递// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存//一份,然后给this + 1// 而temp是临时对象,因此只能以值的方式返回,不能返回引用Date operator++(int){Date temp(*this);_day += 1;return temp;}
private:int _year;int _month;int _day;
};int main()
{Date d;Date d1(2022, 1, 13);d = d1++; // d: 2022,1,13 d1:2022,1,14d = ++d1; // d: 2022,1,15 d1:2022,1,15return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Maven - 编译报错:程序包 XXX 不存在(多模块项目)

问题描述 编译报错&#xff1a;程序包 XXX 不存在&#xff08;多模块项目&#xff09; 原因分析 检查依赖模块 pom 文件&#xff0c;看是不是引入了如下插件 <plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-pl…

[NOIP2017 提高组] 宝藏

[NOIP2017 提高组] 宝藏 题目背景 NOIP2017 D2T2 题目描述 参与考古挖掘的小明得到了一份藏宝图&#xff0c;藏宝图上标出了 n n n 个深埋在地下的宝藏屋&#xff0c; 也给出了这 n n n 个宝藏屋之间可供开发的 m m m 条道路和它们的长度。 小明决心亲自前往挖掘所有宝…

dolphinscheduler海豚调度(一)简介快速体验

1、简介 Apache DolphinScheduler 是一个分布式易扩展的可视化DAG工作流任务调度开源系统。适用于企业级场景&#xff0c;提供了一个可视化操作任务、工作流和全生命周期数据处理过程的解决方案。 Apache DolphinScheduler 旨在解决复杂的大数据任务依赖关系&#xff0c;并为应…

“小手艺”有“大情怀”, 《青春手艺人》赋能乡村振兴,传承新时代文化

文化传承发展要坚持“守正创新”&#xff0c;以守正创新的正气和锐气&#xff0c;赓续历史文脉、谱写当代华章。中央广播电视总台农业农村节目中心推出的聚焦年轻手艺人故事的微纪录片《青春手艺人》&#xff0c;为守正创新的文化传承增添了新的鲜活的青春故事。节目积极响应二…

shell脚本基础语法(.sh ./ sh bash source shell)

Linux 之 Shell 脚本基础语法 0. 学习一门语言的顺序 1. Shell 编程概述 1.1 Shell 名词解释 在 Linux 操作系统中&#xff0c;Shell 是一个命令行解释器&#xff0c;它为用户提供了一个与操作系统内核交互的界面。用户可以通过 Shell 输入命令&#xff0c;然后 Shell 将这些…

vue项目开发vscode配置

配置代码片段 步骤如下&#xff1a; 文件->首选项->配置用户代码片段新增全局代码片段起全局代码片段文件名“xxx.code-snippets” 这里以配置vue2初始代码片段为例&#xff0c;配置具体代码片段 {"name": "vue-sph","version": "…

零基础学编程从哪里入手,在学习中可以线上会议答疑解惑

一、前言 零基础学编程可以先从容易学的语言入手&#xff0c;比如中文编程&#xff0c;然后再学其他编程语言则会比较轻松&#xff0c;初步掌握编程思路。很多IT人士一般学2到3种编程语言。 今天给大家分享的中文编程开发语言工具资料如下&#xff1a; 编程入门视频教程链接…

DAY39: 动态规划不同路径问题62

Leetcode: 62 不同路径 机器人从(0 , 0) 位置出发&#xff0c;到(m - 1, n - 1)终点。 基本思路 1、确定dp数组&#xff08;dp table&#xff09;以及下标的含义 dp[i][j] &#xff1a;表示从&#xff08;0 &#xff0c;0&#xff09;出发&#xff0c;到(i, j) 有dp[i][j]条…

人工智能|深度学习——使用多层级注意力机制和keras实现问题分类

代码下载 使用多层级注意力机制和keras实现问题分类资源-CSDN文库 1 准备工作 1.1 什么是词向量? ”词向量”&#xff08;词嵌入&#xff09;是将一类将词的语义映射到向量空间中去的自然语言处理技术。即将一个词用特定的向量来表示&#xff0c;向量之间的距离&#xff08;例…

【极数系列】Flink集成KafkaSink 实时输出数据(11)

文章目录 01 引言02 连接器依赖2.1 kafka连接器依赖2.2 base基础依赖 03 使用方法04 序列化器05 指标监控06 项目源码实战6.1 包结构6.2 pom.xml依赖6.3 配置文件6.4 创建sink作业 01 引言 KafkaSink 可将数据流写入一个或多个 Kafka topic 实战源码地址,一键下载可用&#xf…

【语音合成】中文-多情感领域-16k-多发音人

模型介绍 语音合成-中文-多情感领域-16k-多发音人 框架描述 拼接法和参数法是两种Text-To-Speech(TTS)技术路线。近年来参数TTS系统获得了广泛的应用&#xff0c;故此处仅涉及参数法。 参数TTS系统可分为两大模块&#xff1a;前端和后端。 前端包含文本正则、分词、多音字预…

9.0 Zookeeper 节点特性

本章节介绍一下 zookeeper 的节点特性和简单使用场景&#xff0c;正是由于这些节点特性的存在使 zookeeper 开发出不同的场景应用。 1、同一级节点 key 名称是唯一的 实例&#xff1a; $ ls / $ create /runoob 2 已存在 /runoob 节点&#xff0c;再次创建会提示已经存在。 …