C嘎嘎~~[构造函数提升篇]

简介: C嘎嘎~~[构造函数提升篇]

1. 再谈构造函数

1.1. 引入

1.1.1问题引入

class Date
{
public:
private:
  int _year;
  int _month;
  int _day;
};

通过前面所学的知识, 我们知道了_year, _month, _day这三个变量都是一些声明, 并没有开辟空间, 不是定义.


Date d1;

这一个操作就是给 d1这个对象整体定义, 但是对象整体定义,并不代表着里面的三个成员变量定义了.


🗨️那么问题来了: 成员变量是在什么时候定义的??


1.1.2 const引入

class Date
{
public:
private:
  int _year;
  const int _month;
  int _day;
};
int main()
{
  Date d1;
  return 0;
}

当我们用const 来修饰一下其中的一个成员变量, 那么会怎么样了?

9342b12b424e4a2f9d5c10e35e174b9e.png

🗨️这样的报错是什么意思? — 具有未初始化的常量限定数据成员


其实我们可以发现一些端倪: 就是const 修饰的变量有一个特点就是 定义的时候一定要初始化, 那么有些老铁就会说 在里面给 const修饰的变量一个缺省值试一试

eaabd2ecab56491e9d9ade66123461b8.png

🗨️这些不是声明嘛, 给一个缺省值为什么就可以了? 原理是什么啊? 还有就是传一个缺省值是传给谁啊?

其实, 初始化列表可以看成成员变量定义的地方. 而我们给的缺省值也是给初始化列表用的. 缺省, 缺省, 说实话就是一个备胎, 如果我们在初始化列表中给了这个变量的值⇒ 那么就不会用这个缺省值了, 而去用我们在初始化列表中的值.


1.2 正篇

在构造函数中,初始化成员变量有两种方式: 构造函数体赋值 和 初始化列表

其实, 这两种方式是有所不同的, 通过下面的一些比较就能看出他们的不同


1.2.1 构造函数体赋值

class Date
{
public:
  Date(int year, int month, int day)
  {
    _year = year;
    _year = 2022;
    _month = month;
    _month = 5;
    _day = day;
    _day = 20;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2023, 5, 16);
  return 0;
}

🗨️延续上面的问题, 这个是不是初始化?


其实答案很明显, 这个是并不是初始化. 因为每一个变量只有一次初始化, 但是可以有多次赋值. 通过上面的代码, 我们可以发现: 在构造函数内部可以进行多次的赋值, 而初始化只有一次, => 构造函数不是成员变量初始化的地方

1.2.2 初始化列表

1.2.3.1 浅浅认识

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

class Date
{
public:
    // 构造函数
  Date()
       // 此部分是 初始化列表
    :_year(2023)
    // 此部分是 构造函数体赋值
  {
    _month = 5;
    _day = 16;
  }
private:
  const int _year; // const 修饰的成员变量
  int _month;
  int _day;
};
int main()
{
  Date d1;
  return 0;
}

注意:

1.每个成员变量在初始化列表中 最多出现一次(因为只能初始化一次)

2.类中包含以下的成员时, 必须放在初始化列表中进行初始化:

const 修饰的成员变量

引用修饰的成员变量

没有默认构造函数的自定义类型


1.2.3.2 构造函数的 行走顺序

构造函数初始化成员变量有两种形式: 构造函数体赋值 和 初始化列表

一个是 赋值, 一个是 初始化 ⇒ 由此不难看出, 编译器先走的是 初始化列表然后才是构造函数体赋值


🗨️如果没有初始化列表, 编译器会走初始化列表这一步吗?


猛一看, 感觉这个问题是不是有问题; 仔细一想, 其实这个问题问的很有深度~~. 换一句话说, 其实这个问题是想问 ⇒ 初始化列表是构造函数必走的一步吗?

通过下面的代码, 验证一番:

class Date
{
public:
  Date()
  {
    _month = 5;
    _day = 16;
  }
  void Print()
  {
    cout << _year << " " << _month << " " << _day << endl;
  }
private:
  const int _year = 5;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  d1.Print(); // 5 5 16
  return 0;
}
class Date
{
public:
  Date()
    :_year(2023)
  {
    _month = 5;
    _day = 16;
  }
  void Print()
  {
    cout << _year << " " << _month << " " << _day << endl;
  }
private:
  const int _year = 5;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  d1.Print(); //2023 5 16
  return 0;
}

通过上面的两个例子:


1.补丁 — — 成员变量给个缺省值, 这个缺省值其实传给初始化列表的. 如果初始化列表没有对此变量进行操作, 那么就会使用那个缺省值

2.初始化列表是构造函数必走的一步 — — 因为初始化列表是成员变量定义的地方

02d30f4ed13a4149b273a2424b5df6aa.png

1.2.3.3 引用修饰成员变量


class A
{
public:
  A(int a, int& b)
  {
    _a1 = a;
    _a2 = b;
  }
private:
  int _a1;
  int& _a2;
};
int main()
{
  int n = 10;
  A a(10, n);
  return 0;
}
*****
error C2530: “A::_a2”: 必须初始化引用
*****

🗨️有一些老铁, 就会有一些疑问: 为什么这里的传引用不能传递一个常量⇒ 就比如 A a(10, 10);


首先这样写会有报错的:

a83df34974dd471ea466c55b983aae74.png

在这里, 我们只需要看一下红色的报错(蓝色的报错是这一类 的错误 — — 不能用构造函数体赋值来初始化 用引用修饰的成员变量):

因为 _a2 是 b的引用, b 是 10的引用 ⇒ _a2 是 10的引用, 由于 10是一个常量, 所以引用都要用常量引用来接收, 否则就是引用的权限放大造成错误!!!

如果我们这样修改的话, 就必须把成员变量里面也用 const来修饰此变量才可以. 不过这样多不方便⇒ 传一个常量, 一直用, 还不能改变⇒ 这不符合我们的需求啊~

90482791e34e49cda3d1ea3f59fc083c.png

通过上面的例子, 我们发现: 使用构造函数体赋值这种方式是不行的!!


那么, 我们就采用初始化列表:

ab251d3271464b488ec0e31cf600f6f6.png


_a2 = 10 — — _a2 是 b的引用, b 是 n的引用 ⇒ _a2 是 n的引用. 因为初始化列表是成员变量定义的地方, 所以可以在此处进行对 引用修饰的_a2 进行初始化

_a1 是一个随机值 — — 发现传参的 10没用⇒ 想告诉各位读者的是, 我们传参是有自己的目的性 和 选择性; 如果我们不用, 对于内置类型成员变量(当然也没有缺省值)就会被初始化为随机值.

1.2.3.4 没有默认构造的自定义类型

class B
{
public:
  // 无参调用 ==> 默认构造
  B()
  {
  }
private:
  int _b;
  int _tem;
};
class A
{
public:
private:
  int _a;
  B bb;
};
int main()
{
  A a1;
  return 0;
}

上面的代码是正确的, 因为A 类中有一个自定义类型的成语变量 bb. 构造函数是完成成员变量的初始化的, 对于 A类来说, 要完成 对 _a 和 bb( _b _tem) 的初始化. 而要完成对 bb的初始化, 就需要 B类的默认构造. 如果 B类存在默认构造, 那么就会对 bb 进行初始化, 如果 B类不存在默认构造, 那么就不会对 bb进行初始化.👇👇👇


class B
{
public:
  // 有参 ==> 就不存在默认构造
  B(int x, int y)
  {
    _b = x;
    _tem = y;
  }
private:
  int _b;
  int _tem;
};
class A
{
public:
private:
  int _a;
  B bb;
};
int main()
{
  A a1;
  return 0;
}

100cf648388e45fa9a03c4510e291b68.png


上面, 我们故意对 B类的构造函数写成了有参调用⇒ 那么 B类就会失去默认构造=> B(_b _ tem) 就会是随机值, 就不能完成对 B(_b _ tem) 的初始化了=> A类中就不能完成初始化


那我们试一试初始化列表👇👇👇

95f879175073466ca4d21da86191044f.png


对自定义类型成员变量的初始化总结:

bcac0b6fbfa84a14b2659d6e3d2006db.png

1.2.3初始化列表的 '坑'

成员变量在类中声明次序就是在初始化列表中的初始化顺序, 与其在初始化列表中的先后次序无关

class A
{
public:
  A(int a)
    :_a1(a)
    ,_a2(_a1)
  {}
private:
  int _a2;
  int _a1;
};
int main()
{
  A a(1);
}

🗨️上面代码的运行结果是什么?


_a2 是随机值, _a1 = 1

因为声明中的顺序是 _a2 _a1, 那么编译器在初始化列表初始化的时候就会 先初始化 _a2, 再初始化_a1 ⇒

初始化 _a2 的时候: _a2( _a1), 这时候_a1还没有被初始化, 所以是一个随机值; 初始化 _a1的时候: _a1(1), 那么 _a1就是 1喽

建议— — 尽量按照声明的顺序来安排初始化列表中变量的初始化顺序


初始化列表中, 每一个成员变量最多只能出现一次


class A
{
public:
  A()
    :_a1(5)
    ,_a2(10)
    ,_a1(8)
  {}
private:
  int _a2;
  int _a1;
};
int main()
{
  A a;
  return 0;
}

a4272c63566c460dba27cbad649e3433.png


初始化列表是每个成员变量定义的地方, 也是初始化的地方


1.2.4 谈谈初始化列表 和 构造函数

其实, 每一个初学者在这个地方都会停留一段时间.

初始化列表 和 构造函数, 一不留神就会混淆了概念和作用


总的来说, 初始化列表是构造函数的一部分.构造函数的功能就是完成对成员变量的初始化工作, 对自定义类型会调用它的默认构造, 而对于内置类型, 就不会进行处理.

完成这一个初始化工作, 有两个方式: 初始化列表 和 构造函数体赋值. 见名知义: 一个是初始化, 一个是赋值.

如果成员变量中 没有 const修饰, 引用修饰 或者 没有默认构造的自定义类型, 写不写初始化列表都是ok的, 只要有其中的一个存在, 就要写一下初始化列表. 其实const修饰的成员变量也可以不用写初始化列表(上面有例子, 不清楚的上去看看)

比较来说, 初始化列表可以完成的工作 > 构造函数体赋值 ⇒ 我们一般建议使用初始化列表. 当然有些工作时需要构造函数体赋值, 也是要写构造函数体赋值的.

1.3 explicit关键字

1.3.1 引入

前面C语言的学习, 我们知道存在一种 类型转换. 由于C++能够兼容C语言, 我们就大胆实验👇👇👇


class A
{
public:
  A(int a)
    :_a(a)
  {}
private:
  int _a;
};
int main()
{
  A a = 23;
  return 0;
}

上面的代码是可行的.

🗨️为什么可以直接从一个 int类型直接转换成 A类的对象a呢?? 中间出现了什么过程呢??


构造函数不仅可以构造与初始化对象, 对于单个参数或者第一个参数无默认值其余有默认值的构造函数, 还具有类型转换的作用


1.3.2 正篇

1.3.2.1 赋值的含义

🗨️先有一个问题: 在C语言的学习中, 以int a = 10; double b = a; 为例子, 讲述一下 类型转换的过程


类型转换不是直接就 把 a 的值 赋值给 b. 会生成一个 类型为 double的临时变量, 记作 tem , 然后把tem 赋值给 b . 这里多说一句, 后面就出现了引用 &, 就不会有临时对象的生成.

那么延续上面的思路, 我们就会知道上面代码的一个原理:

用 23去构造了一个 类型为A的临时对象, 记作tem, 然后把 tem 拷贝构造给 a

🗨️为啥换到这里就是 构造 和 拷贝构造了??


生成一个类型是 A的临时对象 tem, 用 23去初始化tem — — 构造

用一个已知的对象tem 去初始化另一个对象 a — — 拷贝构造

1.3.2.2 探寻 '隐式类型转换' 的真相

我们已经知道隐式类型转换的一个原理, 那么用代码来验证一番:

class A
{
public:
  A(int a)
    :_a(a)
  {
    cout << "调用了构造函数" << endl;
  }
  A(const A& x)
  {
    cout << "调用了拷贝构造函数" << endl;
  }
private:
  int _a;
};
int main()
{
  A a = 23;
  return 0;
}
*****
调用了构造函数
*****

🗨️嗯??, 跟我们想的不一样, 难道我们想错了??


其实不然, 这里编译器把它给优化了.

我们知道: 构造 和 拷贝构造 的功能都是 初始化, 如果在同一行, 我们同时调用 构造 + 拷贝构造 ⇒ 编译器就只会调用一个构造函数, 直接完成赋值~~

🗨️老陈, 你空口无凭, 给我们看一下证据??


ok, 这就安排

由于,临时对象具有常性, 所以我们想到了用 引用 &来进行验证 <== 因为权限可以缩小 或 平移, 但是不能放大

先看下面的代码👇👇👇

class A
{
public:
  A(int a)
    :_a(a)
  {
    cout << "调用了构造函数" << endl;
  }
  A(const A& x)
  {
    cout << "调用了拷贝构造函数" << endl;
  }
private:
  int _a;
};
int main()
{
  A& a = 23;
  return 0;
}
*****
error C2440: “初始化”: 无法从“int”转换为“A &”
*****
class A
{
public:
  A(int a)
    :_a(a)
  {
    cout << "调用了构造函数" << endl;
  }
  A(const A& x)
  {
    cout << "调用了拷贝构造函数" << endl;
  }
private:
  int _a;
};
int main()
{
  const A& a = 23;
  return 0;
}
*****
调用了构造函数
*****

这里就变相地证明了: 上面的隐式类型转换 是经历过了构造 + 拷贝构造, 不过是编译器有优化而已

注意: 不同的编译器有不同的优化, 所以看到不同的结果不必大惊小怪的~~


1.3.2.3 explicit关键字

在一些场景下, 隐式类型转换会很方便(后面会学到的 string, STL… …),

但在另一些场景下, 我们又不希望隐式类型转换的出现(后面会学到的 智能指针… …)

那我们如何不让 隐式类型转换 发生呢?? — — 答案就是explicit关键字

把explicit 放在 构造函数的前面就会不让 隐式类型转换发生

class A
{
public:
  explicit A(int a)
    :_a(a)
  {
    cout << "调用了构造函数" << endl;
  }
  A(const A& x)
  {
    cout << "调用了拷贝构造函数" << endl;
  }
private:
  int _a;
};
int main()
{
  A a = 23;
  return 0;
}
*****
error C2440: “初始化”: 无法从“int”转换为“A”
message : class“A”的构造函数声明为“explicit”
*****


大鹏一日同风起,扶摇直上九万里


相关文章
|
网络安全
[网络安全]xss-labs level-15 解题详析
[网络安全]xss-labs level-15 解题详析
386 0
|
存储 运维 负载均衡
Redis Cluster集群原理+三主三从交叉复制实战+故障切换
Redis Cluster集群原理+三主三从交叉复制实战+故障切换
2618 0
Redis Cluster集群原理+三主三从交叉复制实战+故障切换
|
存储 数据库 Android开发
|
开发工具 开发者
鸿蒙开发:如何无线调试应用
以上呢,就是鸿蒙开发中,如何使用无线来调试应用,相对来说还是比较的简单,需要注意的事项只有一个,那就是,两台设备,一个是PC电脑,一个是真机设备,都需要在同一网络下,才能运行起来应用。
495 1
|
11月前
|
敏捷开发 前端开发 JavaScript
多维视角分工工具全解析:任务颗粒度、责任维度与协作逻辑一站梳理
在项目协作中,任务失控往往源于“角色失明”——责任不清、分工模糊、推诿频发。本文提出“多维视角分工体系”,从角色、职责、阶段、技能等多维度结构化拆解任务,建立责任明确、过程可视、可追溯的协同机制,解决跨职能合作中的混乱与低效问题。通过工具支持与文化落地,打造清晰、有序、高协同的项目协作网络。
|
运维 关系型数据库 MySQL
MySQL8之mysql-community-debuginfo的作用
【7月更文挑战第10天】
364 6
|
弹性计算 数据可视化 安全
高效部署企业门户网站【阿里云云效平台详细指南】
使用阿里云云效部署企业网站涉及备案域名、ECS、VPC、云效代码仓库和流水线。一键部署通过ROS快速配置,手动部署则需详细配置流水线,包括代码源、构建、部署到ECS。整个流程约10分钟,但需注意网络问题可能导致的异常。一键部署适合快速启动,手动部署适合定制化。文档详细,但可增加常见问题解答和自动化脚本支持。
高效部署企业门户网站【阿里云云效平台详细指南】
|
存储 算法 Linux
VDO卷的配置
在CentOS 7环境下,VDO通过重删和压缩技术节省磁盘空间,使1T磁盘可存储1.5T数据,降低成本。创建VDO步骤包括:添加64GB新硬盘,安装vdo和kmod-kvdo软件,使用`vdo create`命令创建VDO卷,如`vdo create --name=myvdo --device=/dev/sdb --vdoLogicalSize=200G`。之后,可通过`vdo list`、`vdo status`和`vdostats`检查状态和空间使用。最后,格式化VDO卷,创建挂载点并挂载,完成设置。
472 0
|
云栖大会 云计算 开发者
开发者眼中的云栖
这个时代中的每一位开发者都是构筑新生活的设计师,而云栖大会则是优秀设计师的年度答卷。今天,我们想要从开发者的视角去了解,他们眼中的云栖大会。
开发者眼中的云栖
|
Linux 网络安全 数据安全/隐私保护
用法小技巧:SecureCRT的跳板机功能
用法小技巧:SecureCRT的跳板机功能
1485 0
用法小技巧:SecureCRT的跳板机功能

热门文章

最新文章