初识C++ · 类和对象(下)

目录

1 再谈构造函数

2 类中的隐式类型转换

3 Static成员

4 友元和内部类

5 匿名对象

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;
};
int main()
{Date d1;return 0;
}

当我们生成解决方案的时候,系统会报错:没有默认的构造函数,因为我们显式调用了构造函数,也没有默认构造,我们没有给缺省值,参数也没有缺省值,调用的时候就会报错。

C++引入了一个概念叫做初始化列表,以冒号开始,逗号分割,括号给值:

Date():_year(2024),_month(4),_day(30)
{}	

也就是我们将构造函数写成这样,这样我们不传参也是可以成功的。

可能看起来没什么用?

class Stack
{
public:
Stack(int n):_size(10), _capacity(n), arr(nullptr)
{//……
}
private:int _size;int _capacity;int* arr;
};
class MyQueue
{
public:MyQueue(int n):s1(n),s2(n){_size = n;}
private:Stack s1;Stack s2;int _size;
};
int main()
{MyQueue q1(10);return 0;
}

对于类中有自定义类型的,我们原本的想法是给MyQueue一个值,然后初始化,并且stack调用自己的默认构造,如果没有初始化列表,Stack就完不成自己的初始化,那么MyQueue也就完不成自己的默认构造。

初始化列表赋值的时候都是用括号赋值,如果不想用括号,那么进入花括号里面进行赋值也是可以的,一般来说的话能直接括号就直接括号了。

赋值的括号里面可以是变量也可以是加减法一类的,也可以是常量。

有意思的是括号里面还可以进行运算。

初始化的本质可以理解为声明和定义,private那里是声明,初始化列表就是定义,定义的时候我们给缺省值也是没有问题的。

那么,初始化列表有那么几个需要注意的地方。

有三种成员必须要在初始化列表初始化:
第一种是const成员:

int main()
{const int a;a = 10;return 0;
}

这种代码就是错误的,因为const定义的变量只有一次初始化的机会,就是定义的时候,定义好了之后就不能改值的,所以const成员变量必须要在初始化列表初始化。

第二种是引用类型:

int main()
{int x = 10;int& xx;xx = x;return 0;
}

引用类型和const类型是一样的,不可能说先给一个外号,看谁像就给谁,所以引用类型也是要在初始化列表的时候给值。

第三种类型是没有默认构造的自定义类型的成员:

class Stack
{
public:Stack(int n):_size(n), _capacity(n), arr(nullptr){//……}private:int _size;int _capacity;int* arr;
};
class MyQueue
{
public:MyQueue(int n = 10):s1(n),s2(n){_size = n;}
private:Stack s1;Stack s2;int _size;
};

像这种,stack类必须要传参才是初始化的,没有默认构造函数,那么为了让他能顺利初始化,就在初始化列表里面初始化了。

对于初始化列表来说,三类成员必须在初始化列表初始化,其他类型的可以在初始化列表进行初始化,也可以进入函数体内初始化。

看个有意思的:
 

class Stack
{
public:Stack(int n = 4):_size(n), _capacity(n), arr(nullptr){}
private:int _size;int _capacity;int* arr;
};
class MyQueue
{
public:MyQueue(int n = 10){_size = n;}
private:Stack s1;Stack s2;int _size;
};
int main()
{MyQueue q1;return 0;
}

我们给stack默认构造函数,使用MyQueue的初始化列表的时候没有Stack的初始化,那么stack会不会初始化呢?

stack类也是初始化了的,那么这就意味着,初始化列表不管你写不写编译器都是要走一遍的,所以C++打的补丁缺省值,实际上给的是初始化列表。即便我初始化列表什么都不写,仍然会走一遍初始化列表。无非就是调用它自己的默认构造函数而已。

一般的顺序都是先走一遍初始化列表,再走函数体,比如初始化一个指针,我们可以这样初始化:

	Stack(int n = 4):_size(n), _capacity(n), arr((int*)malloc(sizeof(int) * 10)){memset(arr, 1, 40);}

函数体更多的是用来进行其他参数,初始化一般在初始化列表就可以了。

接下来看一个有意思的:

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

问最后结果是什么?

答案可能出乎你的意料:

打印出来了一个随机值,这是因为初始化列表的一个特点:
成员变量的声明次序就是初始化列表中的初始化顺序

我们先声明的_a2,所以_a2先给值,是_a1给的,_a1还没开始初始化,所以给的是随机值,然后初始化_a1,这时候_a1初始化为了1,所以打印出来有一个是1,有一个是随机值。

如果我们声明次序换一下,就是Ok的:


2 类中的隐式类型转换

先来看一个很有用的小代码:

class A
{
public:A(int n):_a(n){}
private:int _a;
};
int main()
{A a1();A a2 = 2;return 0;
}

我们创建对象的时候,可以用构造函数创建,也可以利用隐式类型转换创建,内置类型被转换为自定义类型,这里是2构建了一个A的临时对象,然后临时对象拷贝复制给a2。

当然了如果我们要引用一个的话,就得加一个const了,因为const具有常性。

	const A& aa = 1;

按道理来说,2构造了一个临时对象,发生了一次构造,然后临时对象拷贝构造给a2,所以一共是两次函数调用,但是在编译认为连续的构造 + 拷贝构造不如优化为构造,测试一下:

class A
{
public:A(int n):_a(n){cout << "int n" << endl;}A(const A& aa):_a(aa._a){cout << "const A& aa" << endl;}
private:int _a;
};
int main()
{A a1(1);//构造A a2 = 2;//构造+拷贝构造 = 直接构造return 0;
}

这个隐式类型转换应用的场景比如:


class A
{
public:A(int n = 1):_a(n){cout << "int n" << endl;}A(const A& aa):_a(aa._a){cout << "const A& aa" << endl;}
private:int _a;
};class Stack
{
public:void push(const A& aa){//...}
private:int _size;
};int main()
{A a1;Stack s1;s1.push(a1);s1.push(2);return 0;
}

我往栈里面插入一个自定义类型,如果没有隐式类型转换,我就需要先创建一个,再插进去,这多麻烦,有了隐式类型转换直接就插入进去了。

但是有没有发现一个问题就是,隐式类型转换是内置类型给给自定义类型,如果是多个参数,又怎么办呢?

先不急,还有一个关键字explicit,它的用法很简单,就是防止隐式类型转换的发生的:

当多参数的时候,万能的花括号就派上用场了:

class A
{
public:A(int n,int m):_a(n),_b(m + 1),_c(n + 2){cout << "int n" << endl;}A(const A& aa):_a(aa._a){cout << "const A& aa" << endl;}
private:int _a;int _b;int _c;
};
int main()
{A a1 = { 1,2};A a2{ 1,3 };const A& aa{ 2,2 };return 0;
}

对于多参数的初始化,用花括号即可,并且在新的标准中可以不用等好,直接就花括号就可以了,


3 Static成员

class A
{
public:A(){_count++;}A(const A& aa){_count++;}~A(){_count--;}
private:int _a;int _b;static int _count;
};

都知道static是用来修饰静态成员变量,那么在类里面如上,请问该类的大小是多大呢?

sizeof计算出来是8,也就是说_count是不在类里面的,因为它在静态区里面,那么结合初始化列表的知识,我们能给缺省值吗?

当然是不行的,因为缺省值最后都是要走初始化列表的,static的成员变量都不在类里面,怎么能走呢?

因为static的成员是静态的,我们只能在定义的时候给初始值,我们就只能在全局给一个初始值:

int A::_count = 1;

既然它是静态的,所以我们可以用来计数,比如实时观察有几个对象:

class A
{
public:A(int n = 1):_a(n),_b(n){_count++;}A(const A& aa){_count--;}~A(){_count++;}
//private:int _a;int _b;static int _count;
};int A::_count = 0;A Func()
{A a1;return a1;
}int main()
{A a1;//1A a2 = a1;//2A a3 = 3;//3Func();//4//拷贝构造一个5cout << a1._count << endl;return 0;
}

函数里面有一次初始化,一次拷贝,加上主函数的三次,一共就是5个。

但是!

以上的所有操作都是基于count是公有的解决的,但是成员变量一般都是私有的,所以解决方法是用static修饰的函数:

static int Getcount()
{return _count;
}

因为函数也是静态的,所以没有this指针,那么访问的只能是静态成员,比如_count,其他成员变量都是不能访问的。


4 友元和内部类

友元前面已经简单提过,这里也介绍一下:

class A
{friend class B;//A是B的友元
public://...
private:int _a1;int _a2;
};
class B
{
public://...
private:int _b1;int _b2;
};

A是B的友元,友元的位置声明放在任意位置都是可以的,既然A是B的友元,也就是说A是B的朋友,那么B就可以访问A中的成员,如:

class A
{friend class B;//A是B的友元
public://...
private:int _a1 = 1;int _a2 = 2;
};
class B
{
public://...void BPrint(){cout << a1._a1 << endl;}
private:int _b1;int _b2;A a1;
};
int main()
{B bb;bb.BPrint();return 0;
}

但是反过来就不行了,A是B的朋友没错,但是B不是A的朋友,所以A不能使用B的成员,这个世界的情感很多都是单向的~

但是呢友元关系不能继承,之后介绍。

内部类,和友元关系挺大的:

class A
{
public:class B{public:private:int _b1 = 1;int _b2 = 2;};
private:int _a1 = 1;int _a2 = 2;
};

B是A的内部类,那么他们天生就有B是A的友元的关系,所以A可以直接访问B的成员变量,但是sizeof(外部类)的结果就是外部类:

内部类还可以直接访问外部类的static变量,不需要类名等:

class A
{
public:class B{public:void PirntK(){cout << _k << endl;}private:};
private:static int _k;
};
int A::_k = 1;
int main()
{A::B b1;b1.PirntK();return 0;
}

5 匿名对象

不少人看到匿名对象可能会联想到匿名结构体,不同的是匿名对象是对象实例化的时候不给名字,如:

class A
{
public:A(int num = 1):_a(num){cout << "int A" << endl;}~A(){_a = -1;cout << "~A" << endl;}
private:int _a;
};
int main()
{A a1;//有名对象A(1);//匿名对象return 0;
}

与匿名结构体不同的是,匿名i对象的声明周期只在这一行,没错,就是只有一行,我们可以通过析构函数调用实验一下:

int main()
{A(1);cout << "666" << endl;return 0;
}

如果是有名对象,那么析构函数的调用会在主函数结束的时候调用,那么666的打印就会在~A之前打印,但是这是匿名对象,创建即是销毁。

那么有用没呢?

存在即合理,比如我们调用函数:

class S
{
public:void P(){cout << " aaa " << endl;}
private:};
int main()
{S s1;s1.P();S().P();return 0;
}

这是两种调用方法,两行代码的是有名对象的调用,一行代码的是匿名对象的调用,所以嘛,存在即合理。


6 编译器的一些优化

编译器的一些优化在2022是不太好观察的,因为2022的优化是比较大的,这里推荐的是Vs2019或者使用Linux机器观察,这里使用Vs2019观察:

先来看一下传值传参热热身:

class A
{
public:A(int num = 1):_a(num){cout << "int A" << endl;}A(const A& aa){cout << "const A& aa" << endl;}~A(){cout << "~A" << endl;}
private:int _a;
};

//测试代码
void Func(A aa)
{}
int main()
{A a;Func(a);cout << endl;return 0;
}

顺序是a的构造->aa的拷贝构造->aa的析构(因为出了函数的作用域)->a的析构:

打印出来的换行也可以说明。

这里可能有人要问了,为什么拷贝构造函数要用个const修饰,因为有了匿名对象,呼应上了这就:
匿名对象发生的是临时变量的拷贝,具有常性,所以我们应该用const进行修饰

	Func(A(1));

1 连续的构造 + 拷贝构造 = 直接构造(不绝对)

如下三个场景:

int main()
{Func(2);Func(A(2));A aa = 3;return 0;
}

比如最后一个,给一个3,那么3会构造一个临时对象,临时变量拷贝给aa,整个过程就是连续的构造 + 拷贝构造,编译器会直接优化为构造。

但是为什么说不绝对呢?这和内联函数都是一样的,取决于编译器的实现,优化,内联函数对编译器来说都只是个建议,具体看的是编译器。

2 连续的拷贝构造 + 拷贝构造 = 一个拷贝构造

A Func()
{A aa;return aa;
}
int main()
{A ret = Func();return 0;
}

代码执行的顺序是aa的构造 -> aa返回临时变量进行拷贝 -> ret拷贝构造一个临时对象

这里是连续的拷贝构造即被编译器优化为一个拷贝构造:

但是……

int main()
{A ret;ret= Func();return 0;
}

这里是连续的拷贝构造吗?

并不是,ret  = Fun()这里是一个赋值重载,所以就不会有编译器的优化。

即拷贝 + 赋值重载 = 无法优化。

这是debug版本下的优化,release版本下的优化简直可以吓死人:


void operator=(const A& aa)
{cout << "operator=" << endl;
}
A Func()
{A aa;return aa;
}
int main()
{A ret;ret= Func();return 0;
}

原来是构造 + 构造 + 拷贝 +  赋值重载,这直接:​​​​​

拷贝直接优化掉了,直接赋值重载,这还不是最吓人的。

A Func()
{A aa;return aa;
}
int main()
{A ret= Func();return 0;
}

按道理来说,有构造 + 拷贝 + 拷贝,编译器直接三合一:

厉害吧?所以有时候观察麻烦就是因为编译器给优化掉了。

以上就是类和对象下的内容。


感谢阅读!

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

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

相关文章

C#修改默认参数settings文件

右击项目在设置中进行修改&#xff1a; 千万不要在这里改。 如果要在自己的项目里添加这个文件&#xff0c;首先新建个文件夹&#xff0c;然后添加.setting文件&#xff0c;然后再像上面说的那样添加属性。

Vue3专栏项目 -- 一、第一个页面(上)

一、ColumnList 组件&#xff08;专栏列表组件&#xff09;编码&#xff1a; 该组件要接收一个数组&#xff0c;数组中是一个个专栏数据&#xff0c;数据中包括id、title、avator、description。所以我们定义一个泛型&#xff0c;泛型为id为number类型title为string类型如下这…

明火检测实时识别报警:视觉算法助力安全生产管理

背景与现状 在各种工作、生产环境下&#xff0c;明火的存在往往是潜在的安全隐患。无论是加油站、化工园区、仓储场所还是校园&#xff0c;明火一旦失控就会引发火灾&#xff0c;造成严重的人员伤亡和财产损失。传统的明火检查手段主要依赖于人工巡查和定期的消防检查&#xf…

shell脚本脚本变量

shell脚本的概念&#xff1a; 1.讲要执行的命令按顺序保存到一个文本文件 2.给文件可执行权限 3.可以结合各种shell控制语句以完成更复杂的操作 linux中包含shell的文件有 [rootlocalhost ~]# cat /etc/shells /bin/sh #UNIX最初使用的 shell&#xff0c;已经被…

Type-C转音频(USB2.0数据传输)+PD充电芯片乐得瑞LDR6500/LDR6023

LDR6500 USB-C DRP 接口 USB PD 通信芯片概述 Type-C转音频(USB2.0数据传输)PD充电芯片乐得瑞LDR6500LDR6500是乐得瑞科技针对USB Type-C标准中的Bridge设备而开发的USB-C DRP&#xff08;Dual Role Port&#xff0c;双角色端口&#xff09;接口USB PD&#xff08;Power Deliv…

选择适用的无尘棉签:保障洁净生产环境下的高效擦拭

随着洁净生产条件的日益普及和无尘级别要求的提高&#xff0c;无尘擦拭用品成为广大用户追捧的必备工具。在这个领域&#xff0c;无尘棉签作为一种高效的擦拭工具&#xff0c;扮演着重要的角色。然而&#xff0c;面对市场上种类繁多的无尘棉签&#xff0c;如何选择最合适的产品…

【FL常用插件#1】Ozone11臭氧的安装和使用

本文内容收集自互联网&#xff0c;仅供个人学习参考使用&#xff0c;不允许用于商业用途&#xff0c;造成的侵权行为与本文作者无关 安装 VST2、VST3、AAX和NKS是音频技术界常见的几种插件格式&#xff0c;它们在功能和兼容性上有所不同&#xff1a; VST2 (Virtual Studio Tec…

46. UE5 RPG 实现角色死亡效果

在上一篇文章中&#xff0c;我们实现了敌人受到攻击后会播放受击动画&#xff0c;并且还给角色设置了受击标签。并在角色受击时&#xff0c;在角色身上挂上受击标签&#xff0c;在c里&#xff0c;如果挂载了此标签&#xff0c;速度将降为0 。 受击有了&#xff0c;接下来我们将…

element-plus el-time-picker 时间段选择(可多选)

实现一个如图的时间段选择器 处理好时间回显逻辑&#xff0c;组件内[‘’,‘’],后端数据[{startTime:‘’,endTime:‘’}]处理好加和减的显示逻辑 <template><div><div v-for"(item, index) in currentChoose" :key"index" class"fl…

STM32G0存储器和总线架构

文章目录 前言一、系统架构二、存储器构成三、存储器地址映射四、存储器边界地址五、外设寄存器边界地址 前言 此文章是STM32G0 MCU的学习记录&#xff0c;并非权威&#xff0c;请谨慎参考。 STM32G0主流微控制器基于工作频率可达64 MHz的高性能Arm Cortex-M0 32位RISC内核。该…

Day_2

1. 菜品管理 新增菜品 接口设计 1. 根据类型查询分类&#xff08;分类管理已完成&#xff09; 查看接口文档即可 2. 文件上传 创建Bucket 采用的是阿里云的OSS对象存储服务 新增AccessKey 3. 菜品的新增逻辑 代码开发 1. 文件上传接口开发 为了提高代码的解耦性&#…

【强训笔记】day11

NO.1 思路&#xff1a;枚举&#xff0c;设一号大礼包的数量为x&#xff0c;二号大礼包的数量为y&#xff0c;用循环枚举一号大礼包的个数得到二号大礼包的数量&#xff0c;使得某一时刻axby的值最大。 代码实现&#xff1a; #include<iostream>using namespace std;lo…

昂科烧录器支持Infineon英飞凌的三相电机驱动器TLE9877QXA40

芯片烧录行业领导者-昂科技术近日发布最新的烧录软件更新及新增支持的芯片型号列表&#xff0c;其中Infineon英飞凌的三相电机驱动器TLE9877QXA40已经被昂科的通用烧录平台AP8000所支持。 TLE9877QXA40是一款单芯片三相电机驱动器&#xff0c;集成了行业标准的ARMCortex™M3 内…

如何去官网下载windows10操作系统iso镜像

文章目录 一、先从微软中国官网https://www.microsoft.com/zh-cn/进去二、然后按图示一步步点进去三、点击下载工具这个工具会帮你生成windows操作系统iso文件四、下载好后一步步按图示要求成功操作一、先从微软中国官网https://www.microsoft.com/zh-cn/进去 二、然后按图示一…

HackMyVM-Animetronic

目录 信息收集 arp nmap nikto whatweb WEB web信息收集 feroxbuster steghide exiftool hydra ssh连接 提权 系统信息收集 socat提权 信息收集 arp ┌──(root㉿0x00)-[~/HackMyVM] └─# arp-scan -l Interface: eth0, type: EN10MB, MAC: 08:00:27:9d:6d:7…

代码随想录第52天|300.最长递增子序列 718. 最长重复子数组

300.最长递增子序列 300. 最长递增子序列 - 力扣&#xff08;LeetCode&#xff09; 代码随想录 (programmercarl.com) 动态规划之子序列问题&#xff0c;元素不连续&#xff01;| LeetCode&#xff1a;300.最长递增子序列_哔哩哔哩_bilibili 给你一个整数数组 nums &#xff0…

学习软考----数据库系统工程师25

关系规范化 1NF&#xff08;第一范式&#xff09; 2NF&#xff08;第二范式&#xff09; 3NF&#xff08;第三范式&#xff09; BCNF&#xff08;巴克斯范式&#xff09; 4NF&#xff08;第四范式&#xff09; 总结

KUKA机器人故障报警信息处理(一)

1、KSS00276 机器人参数不等于机器人类型 ①登录专家模式 ②示教器操作&#xff1a;【菜单】—【显示】—【变量】—【单个】 ③名称输入&#xff1a;$ROBTRAFO[] 新值&#xff1a;TRAFONAME[] ④点击【设定值】。 2、电池报警&#xff1a; ①“充电电池警告-发现老化的蓄电池…

TriCore TC162 Archievture Volume 笔记

说明 本文是 英飞凌 架构文档 TriCore TC162P core archiecture Volume 1 of 2 (infineon.com) 的文笔&#xff0c;稍作整理方便查阅&#xff0c;错误之处&#xff0c;还请指正&#xff0c;谢谢 :) 1. Architecture 2. General Purpose & System Register 名词列表&#…

Redis学习(十)|使用消息队列的重试机制实现 MySQL 和 Redis 的数据一致性

文章目录 介绍原理整体方案实现步骤示例代码总结其他&#xff1a;Kafka 重试策略配置1. 生产者重试策略配置2. 消费者重试策略配置 介绍 在分布式系统中&#xff0c;保持 MySQL 和 Redis 之间的数据一致性是至关重要的。为了确保数据的一致性&#xff0c;我们通常采取先更新数…