C++奇迹之旅:深入学习类和对象的初始化列表

请添加图片描述

文章目录

  • 📝再谈构造函数
  • 🌠 构造函数体赋值
    • 🌉初始化列表
    • 🌉初始化列表效率
      • 🌠隐式类型转换
      • 🌉复制初始化
  • 🌠单多参数构造函数
    • 🌉explicit关键字
  • 🚩总结


📝再谈构造函数

🌠 构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个变量一个合适的初始值

class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};

虽然上面构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,这和我们之间常常说的给缺省值其实就是赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。

🌉初始化列表

初始化列表:以一个冒号开始,接着是一个逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号的初始化或表达式

class Date
{
public:Date(int year,int month,int day):_year(year),_month(month),_day(day){//}private:int _year;int _month;int _day;
};

为什么要有初始化列表来赋初值,不能直接给缺省值,或者传参吗?

class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;_x = 1;}private:int _year;int _month;int _day;//必须在定义初始化const int _x;
};

由于const必须在定义时就要进行初始化,而这个在构造函数中_x=1的行为是赋值行为,不是初始化,因此const 修饰_x无法再赋值。引用&也是如此,需要在定义的时候并且进行初始化,不能分开。
在这里插入图片描述
因此对于普通的内置类型,普通成员变量都可以在函数体或者在初始化列表进行初始化,

int _year;
int _month;
int _day;

因为在这里只是声明,没有定义,定义时实例化的时候完成的,而有些特殊的成员变量需要再定义的时候就初始化,而不是再通过赋值。

class Date
{
public:Date(int year, int month, int day, int& refDay): ref(refDay), _x(1){_year = year;_month = month;_day = day;}void printDate(){std::cout << "Date: " << _year << "-" << _month << "-" << _day << std::endl;std::cout << "Reference day: " << ref << std::endl;std::cout << "Constant value: " << _x << std::endl;}private:int _year;int _month;int _day;int& ref;const int _x;
};int main()
{int someDay = 22;Date date1(2024, 4, 22, someDay);date1.printDate();return 0;
}

在这里插入图片描述

小知识:初始化和赋值之间的本质区别
初始化对象就是在对象创建的同时使用初值直接填充对象的内存单元,因此不会有数据类型转换等中间过程,也就不会产生临时对象;而赋值则是在对象创建好后任何时候都可以调用的而且可以多次调用的函数,由于它调用的是“=”运算符,因此可能需要进行类型转换,即会产生临时对象

但是类中包含以下成员,必须放在初始化列表位置进行初始化:

  • 引用&成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时)
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
};int main()
{int x = 10;B bb(20, x);return 0;
}

自定义类型成员(且该类没有默认构造函数时)会发生错误
在这里插入图片描述

这是按F11一步一步运行的顺序:
请添加图片描述
这里我们知道,对于 int、double、float 等内置类型的成员变量,如果没有在初始化列表中显式初始化,它们将被默认初始化,这个初始化编译器可能会初始化为0,但是默认初始化他其实是未定义的,有可能为0,也有可能为随机值。

对于自定义类类型的成员变量,如果没有在初始化列表中显式初始化,它们将使用该类的默认构造函数进行初始化。如果该类没有提供默认构造函数,则会出现编译错误。

我们知道_n和引用ref是通过初始化列表进行赋值的,因为是const和引用,只能在初始化列表初始化,但是而这些内置类型_year可以不使用初始化列表显示赋值,他们先进行默认初始化,然后再在构造函数体内进行_year = year; _month = month; _day = day;等赋值操作,那在赋值之前,他们的值是未定义的–》

class Date
{
public:Date(int year, int month, int day, int& refDay): ref(refDay), _x(1){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;int& ref;const int _x;
};

而我们之前也学过一个在声明时使用的一个操作:给缺省值:

请添加图片描述
这里是我们熟悉的给缺省值,我们可以看到当进入对象里面时,我们先去找内置类型,然后给缺省值,当走完缺省值,他还会走一遍初始化列表,因为这上面没有写出初始化列表,那么我们调试看不出来,自定义先去找他的构造函数,如果没有就会报错,因此自定义类型的尽头还是内置类型,所以,这个缺省值是给初始化列表准备的,有缺省值,没有初始化化列表,就用缺省值来初始化列表,那两者都有呢,先走缺省值,然后再去按初始化列表,最终还是按照初始化列表来初始化。

总结一下就是:

  1. 初始化列表,不管你写不写,每个成员变量都会先走一遍
  2. 自定义类型的成员会调用默认构造(没有默认构造就编译错误)
  3. 内置类型有缺省值用缺省值,没有的话,不确定,要看编译器,有的编译器会报错
  4. 先走初始化列表 + 再走函数体
    实践中:尽可能使用初始化列表初始化,不方便在使用函数体初始化

以下是调试代码,可以动手试试哦:

typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 4){_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;
};class MyQueue
{
public:// 初始化列表,不管你写不写,每个成员变量都会先走一遍// 自定义类型的成员会调用默认构造(没有默认构造就编译报错)// 内置类型有缺省值用缺省值,没有的话,不确定,要看编译器,有的编译器会处理,有的不会处理// 先走初始化列表 + 再走函数体// 实践中:尽可能使用初始化列表初始化,不方便再使用函数体初始化MyQueue():_size(1), _ptr((int*)malloc(40)){memset(_ptr, 0, 40);}private:// 声明Stack _pushst;Stack _popst;// 缺省值  给初始化列表用的int _size = 0;const int _x = 10;int* _ptr;
};int main()
{MyQueue q;return 0;
}

尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
请添加图片描述

成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
看看这个代码会出现什么情况:

class A
{
public:A(int a):_a1(a),_a2(_a1){}void Print(){cout << _a1 << " " << _a2 << endl;}
private:int _a2;int _a1;
};int main()
{A aa(1);aa.Print();
}
A. 输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值

正确答案是 D. 输出1 随机值。

在这个例子中, _a2_a1 之后声明, 所以 _a2 会先被初始化。但在初始化 _a2 时, _a1 还没有被初始化, 所以 _a2 会被初始化为一个随机值。

Print() 函数被调用时, _a1 被正确初始化为 1, 但 _a2 被初始化为一个随机值, 因此输出结果会是 “1 随机值”。

所以, 这个程序不会崩溃也不会编译失败, 只是输出结果不是我们期望的。要解决这个问题, 可以调换 _a1_a2 在初始化列表中的顺序, 或者在构造函数中手动初始化 _a2

修改后的代码:

class A
{
public:A(int a):_a1(a),_a2(_a1){}void Print(){cout << _a1 << " " << _a2 << endl;}
private:int _a1;int _a2;
};int main()
{A aa(1);aa.Print();
}

运行截图:
在这里插入图片描述

当使用成员初始化列表来初始化数据成员时,这些成员函数真正的初始化顺序并不一定与你在初始化列表中为他们安排的顺序一致,编译器总是按照他们在类中声明的次序来初始化的,因此,最好是按照他们的声明顺序来书写成员初始化列表:

  1. 调用基类的构造函数,向他们传递参数
  2. 初始化本类的数据成员(包括成员对象的初始化)
  3. 在函数体内完成其他的初始化工作

🌉初始化列表效率

class A
{//...A(); //默认构造函数A(const A& d); //拷贝构造函数A& operator=const A& d); //赋值函数
};class B
{
public:B(const A& a); //B的成员对象
private:A m_a;     //成员对象
};

(1)采用初始化列表的方式初始化

B::B(const A& a) :m_a(a)
{...
}

(2)采用函数体内赋值的方式初始化

B::B(const A& a)
{m_a = a;...
}

本例第一种方式,类B的构造函数在其初始化列表里调用了类A的拷贝构造函数,从而将成员对象 m_a初始化。
本例第二种方式,类B的构造函数在函数体内用赋值的方式将成最对象a初始化。我们看到的只是一条赋值语句,但实际上 B 的构造函数干了两件事、先暗地里创建m_a对象(调用了 A 的默认构造函数),再调用类A的赋值函数,才将参囊。赋给 m_a
显然第一种方式的效率比第二种高。
对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别,

🌠隐式类型转换

看看小类A:

class A
{
public:A(int a):_a(a){cout << "A(int a)" << endl;}private:int _a;
};
int main()
{A aa1(1);//拷贝构造A aa2 = aa1;A aa3 = 3;return 0;
}

在这里插入图片描述

  • 这个代码把1作为参数调用构造函数,创建aa1对象。
A aa1(1);
  • 在代码中,这个代码为啥能直接赋值?
A aa3 = 3;

在这个代码中,A aa3 = 3;能够直接赋值是因为发生了隐式类型转换

A 类中,有一个接受 int 类型参数的构造函数 A(int a), 在 main() 函数中,A aa3 = 3; 是一个复制初始化的过程,编译器在执行复制初始化时,会尝试将右侧的 3 隐式转换为 A 类型,由于 A 类有一个接受 int 类型参数的构造函数,编译器会自动调用这个构造函数,将 3 转换为 A 类型的对象 aa3
在这里插入图片描述

🌉复制初始化

复制初始化(copy initialization)是 C++ 中一种常见的初始化方式,它指的是使用等号(=)来初始化一个变量。

复制初始化的过程如下:

  1. 首先,编译器会尝试将等号右侧的表达式转换为左侧变量的类型。
  2. 如果转换成功,则使用转换后的值来初始化左侧变量。
  3. 如果转换失败,则编译器会尝试调用类的拷贝构造函数来初始化左侧变量。

例如:

A aa1(1); // 直接初始化
A aa2 = aa1; // 复制初始化,调用拷贝构造函数
A aa3 = 3; // 复制初始化,调用 A(int) 构造函数进行隐式转换
  • A aa1(1) 是直接初始化,调用的是 A(int) 构造函数。
  • A aa2 = aa1 是复制初始化,调用的是拷贝构造函数。
  • A aa3 = 3 也是复制初始化,但是由于 A 类有一个接受 int 类型参数的构造函数,所以编译器会自动将 3 转换为 A 类型,然后调用该构造函数来初始化 aa3

编译器遇到连续构造+拷贝构造->优化为直接构造,C++ 编译器的一种常见优化技巧,称为"构造+拷贝构造优化"。
在某些情况下,编译器可以识别出连续的构造和拷贝构造操作,并将其优化为单次直接构造。这种优化可以提高程序的性能,减少不必要的拷贝操作。

class A
{
public:A(int a):_a(a){cout << "A(int a)" << endl;}A(const A& aa):_a(aa._a){cout << "A(const A& aa)" << endl;}private:int _a;
};
int main()
{A aa1(1);A aa3 = 3;return 0;
}

在这里插入图片描述
在语句 A aa3 = 3; 中,编译器会进行优化,将连续的构造和拷贝构造操作优化为单次直接构造。

编译器首先会调用 A(int a) 构造函数,使用字面量 3 创建一个临时 A 对象,通常情况下,这个临时对象应该被拷贝到 aa3 变量中。但是,聪明的编译器可以识别出这种模式,并将其优化为直接在 aa3 变量的位置上构造一个 A 对象。因此,编译器会直接调用 A(int a) 构造函数,在 aa3 变量的位置上构造一个 A 对象,省略了中间的拷贝步骤。
所以,在这个例子中,输出结果应该是:

A(int a)

只会输出一次 A(int a),而不会输出 A(const A& aa) 表示拷贝构造函数的调用。

这种优化技巧可以提高程序的性能,因为它减少了不必要的拷贝操作。编译器会自动进行这种优化,开发者无需手动进行。这是 C++ 编译器常见的一种性能优化手段。

因此编译器遇到连续构造+拷贝构造->优化为直接构造

🌠单多参数构造函数

class A
{
public:A(int a):_a(a){cout << "A(int a)" << endl;}private:int _a;
};
int main()
{A& raa = 3;return 0;
}

在这里插入图片描述
在代码 A& raa = 3; 中,编译器无法进行隐式转换,因为不能从 int 类型直接转换为 A& 类型的引用。

这里发生的问题是:

  1. 编译器试图将字面量 3 绑定到 A& 类型的引用 raa 上,因为3是常量,具有常性,相当于有了const,而我们知道从常性到正常无常性的转换,不就等于权限的放大,权限的放大将不会发生转换。

正确的做法应该是:

A aa(3);
A& raa = aa;

或者:

A aa = 3;
A& raa = aa;

在这两种情况下,编译器都能找到合适的构造函数来创建 A 对象,然后再将引用绑定到该对象上。

这样写的是对的,但是不方便,我们可以直接加const

const A& raa = 3;
//或者
const A& raa = aa(3);

在这里插入图片描述
此时此刻,两行可以写成一行,这下就方便了

class Stack
{
public:void Push(const A& aa){//...}//...
};
int main()
{Stack st;A a1(1);st.Push(a1);A a2(2);st.Push(a2);//可以直接写st.Push(2);st.Push(4);return 0;
}

或者声明了一个名为lt的list,向列表中添加元素:

#include<vector>
#include<list>int main()
{list<string> lt;// 第一种写法:string s1("111");lt.push_back(s1);//第二种写法lt.push_back("1111");return 0;
}

这是单参数构造函数,以下是多参数构造函数

 //多参数构造函数
A(int a1, int a2):_a(0),_a1(a1),_a2(a2)
{}A aaa1(1, 2);
A aaa2 = { 1, 2 };
const A& aaa3 = { 1, 2 };

🌉explicit关键字

构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。接收单个参
数的构造函数具体表现:

  1. 构造函数只有一个参数
  2. 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
  3. 全缺省构造函数
class Date
{
public:// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译explicit Date(int year):_year(year){}/*// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用// explicit修饰构造函数,禁止类型转换explicit Date(int year, int month = 1, int day = 1): _year(year), _month(month), _day(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;
};
void Test()
{Date d1(2022);// 用一个整形变量给日期类型对象赋值// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值d1 = 2023;// 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转换的作}

上述代码可读性不是很好,用explicit修饰构造函数,将会禁止构造函数的隐式转换。


🚩总结

请添加图片描述

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

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

相关文章

厉害了,Numpy

几乎所有使用Python处理分析数据的人都用过Pandas&#xff0c;因为实在太方便了&#xff0c;就像Excel一样&#xff0c;但你知道Pandas是基于numpy开发出来的吗? Pandas和Numpy的关系类似于国产安卓系统和原生安卓&#xff0c;Numpy提供底层数据结构和算法&#xff0c;搭配数…

C++心决之类和对象详解(中篇)(封装入门二阶)

目录 1.类的6个默认成员函数 2. 构造函数 2.1 概念 2.2 特性 3.析构函数 3.1 概念 3.2 特性 4. 拷贝构造函数 4.1 概念 4.2 特征 5.赋值运算符重载 5.1 运算符重载 5.2 赋值运算符重载 5.3 前置和后置重载 7.const成员 8.取地址及const取地址操作符重载 1.类的…

掌握item_get_app,提升电商推广转化率

一、引言 在数字化时代&#xff0c;电商行业蓬勃发展&#xff0c;竞争也日趋激烈。为了提高销售额和用户满意度&#xff0c;电商企业需要不断探索新的推广策略和技术手段。其中&#xff0c;掌握item_get_app技术&#xff0c;对于提升电商推广转化率具有重要意义。本文将深入探…

软考139-上午题-【软件工程】-软件容错技术

一、软件容错技术 提高软件质量和可靠性的技术大致可分为两类&#xff0c;一类是避开错误&#xff0c;即在开发的过程中不让差错潜入软件的技术&#xff1b;另一类是容错技术&#xff0c;即对某些无法避开的差错&#xff0c;使其影响减至最小的技术。 1-1、容错软件的定义 归…

更换本地yum源的步骤

更换本地yum源的流程与命令&#xff1a;

【Canvas与艺术】绘制圆形biozhazrad蒙版

【关键点】 制作一个半圆形不透明蒙版来造成左右两边相异的明暗效果。 【成果图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><tit…

Vue3、 Vue2 Diff算法比较

Vue2 Diff算法 源码位置:src/core/vdom/patch.ts 源码所在函数:updateChildren() 源码讲解: 有新旧两个节点数组:oldCh和newCh; 有下面几个变量: oldStartIdx 初始值=0 oldStartVnode 初始值=oldCh[0] oldEndIdx 初始值=oldCh.length - 1 oldEndVnode 初始值=oldCh[ol…

Arthas介绍及使用技巧

文章目录 简介能做什么&#xff1f; 使用下载并启动arthas选择应用 java 进程退出 arthas 常用查看命令帮助查看 dashboard通过 thread 命令来获取到线程的栈通过 jad 来反编译 Classwatch 查看方法出入参、sc 搜索类: 查看已加载类所在的包monitor 方法执行监控trace 方法内调…

fastgpt、dify功能分析比较

目录 前言 一、dify、fastgpt是什么&#xff1f; 二、同场pk 1.大模型接入 2.chat&#xff08;最简应用&#xff09; 3.发布应用 4.知识库 5.workflow 6.其他 三、一些point记录 总结 前言 现在都开始AI应用开发&#xff0c;何谓AI应用&#xff0c;起码要和AI大模型…

应对电网挑战!lonQ与橡树岭国家实验室利用量子技术改善关键基础设施

内容来源&#xff1a;量子前哨&#xff08;ID&#xff1a;Qforepost&#xff09; 文丨浪味仙 排版丨沛贤 深度好文&#xff1a;1800字丨6分钟阅读 摘要&#xff1a;美国电网正在面临需求增加和能源扩散的挑战&#xff0c;对能够应对优化和安全挑战的创新解决方案有着迫切需求…

利用结构类型实现简单通讯录

复合数据类型之结构类型 在C语言中&#xff0c;结构类型是一种复合数据类型&#xff0c;它允许用户将不同类型的数据组合成一个单一的数据结构。结构类型为用户提供了自定义数据类型的机制&#xff0c;使得可以将多个相关的数据项组合成一个整体&#xff0c;常用来处理像记录、…

验证二叉搜索树(98)

解题思路&#xff1a;可以直接中序遍历放进一个数组里根据特性判断是否是递增就可以&#xff0c;如果采用递归的思路话用中序遍历和创建一个指针指向前一个节点&#xff0c;根据前一个节点是否比上一个节点小来判断是否是二叉树 具体代码如下&#xff1a; class Solution { …

华为P系列“砍了”,三角美学系列全新登场

2021 年 10 月&#xff0c;Intel 正式带来了颠覆以往的第 12 代酷睿「混合架构」 CPU。 不知道是良心发现还是为了弥补 11 代酷睿过于拉胯表现&#xff0c;Intel 终于把狠活儿都用在了这代。 全新 Intel 7 工艺、全新架构、单核与多核性能大幅提升&#xff0c;让大家十分默契…

【Linux高性能服务器编程】——高性能服务器框架

hello &#xff01;大家好呀&#xff01; 欢迎大家来到我的Linux高性能服务器编程系列之高性能服务器框架介绍&#xff0c;在这篇文章中&#xff0c;你将会学习到高效的创建自己的高性能服务器&#xff0c;并且我会给出源码进行剖析&#xff0c;以及手绘UML图来帮助大家来理解&…

Win 进入桌面黑屏,只有鼠标

大家好&#xff0c;我叫秋意零。 今天&#xff0c;遇到一个同事电脑进入桌面黑屏&#xff0c;只有鼠标。经过询问沟通&#xff0c;说是 Windows 突然进行了自动更新&#xff0c;更新之后桌面就黑了屏。经过查询是一个桌面进程没启动才会导致桌面黑屏。首先分两种情况&#xff0…

代码随想录算法训练营DAY30|C++回溯算法Part.6|332.重新安排行程、51.N皇后、31.解数独

文章目录 332.重新安排行程思路死循环的问题记录映射关系解决死循环并解决字母序问题 伪代码实现CPP代码 51.N皇后思路伪代码实现CPP代码 31.解数独伪代码实现CPP代码 332.重新安排行程 力扣题目链接 文章讲解&#xff1a;332.重新安排行程 状态&#xff1a;题目要求所有机票都…

工业级3D可视化工具HOOPS Visualize, 快速构建移动端和PC端工程应用程序!

HOOPS Visualize是一款强大的工业级3D渲染引擎&#xff0c;帮助您打造出众的工程应用程序。HOOPS Visualize的基石是图形内核&#xff0c;这是一种全功能的&#xff0c;以工程为重点的场景图技术&#xff0c;我们称为Core Graphics。Core Graphics集成到一个框架中&#xff0c;…

大模型改变了NLP的游戏规则了吗

NLP已经死了吗&#xff1f; 自从 ChatGPT 横空出世以来&#xff0c;自然语言处理&#xff08;Natural Language Processing&#xff0c;NLP&#xff09; 研究领域就出现了一种消极的声音&#xff0c;认为大模型技术导致 NLP “死了”。在某乎上就有一条热门问答&#xff0c;大…

Docker 部署网页版 vscode (code-server)

什么是 code-server code-server 是一个基于 Visual Studio Code 的开源项目&#xff0c;它允许你通过 Web 浏览器来使用 Visual Studio Code 的编辑功能。这意味着你可以在任何设备上&#xff0c;只要有浏览器和网络连接&#xff0c;就可以访问和使用 Visual Studio Code&…

漫谈HAMR硬盘的可靠性-2

很显然&#xff0c;HAMR已经成为业内用于提升HDD硬盘容量硬盘的技术手段。三家机械硬盘HDD厂商&#xff0c;希捷、西数、东芝都已对HAMR硬盘进行了十多年的研究&#xff0c;但只有希捷大胆押注HAMR。相反&#xff0c;东芝和西部数据在采用HAMR之前选择了能量辅助垂直磁记录&…