当前位置: 首页 > news >正文

【C++】类和对象【下】

目录

  • 一、再探构造函数
    • 1、测试题
  • 二、类型转换
  • 三、`static`成员
    • 1. 静态成员变量
    • 2. 静态成员函数
  • 四、友元
  • 五、内部类
  • 六、匿名对象
  • 七、对象拷贝时的编译器优化

在这里插入图片描述
个人主页<—请点击
C++专栏<—请点击

一、再探构造函数

之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有⼀种方式,就是初始化列表,初始化列表的使用方式是以⼀个冒号":"开始,接着是⼀个以逗号","分隔的数据成员列表,每个"成员变量"后面跟⼀个放在括号"()"中的初始值或表达式

每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。

引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。

我们现在来看一下初始化列表怎么写

#include <iostream>
using namespace std;class Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year),_month(2),_day(day){_day = 15;}void Print() const{cout << _year << "/" << _month << "/" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1;d1.Print();return 0;
}

在这里插入图片描述
这就是初始化列表,它在构造函数中是成员变量定义初始化的地方,这里_year定义初始化为1_month定义初始化为2_day定义初始化为1,但函数体内又对_day进行了一次赋值所以变为15,我们打印输出一下:
在这里插入图片描述
这里如果你不写初始化列表的话,也不会报错:
在这里插入图片描述
_year_month会变为随机值,为了不让它们是随机值,我们可以在private声明中给它们加上缺省值,就像函数一样,声明和定义分离时,规定声明是可以给缺省值的。(C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。)例如

private:int _year = 2025;int _month = 2;int _day = 2;

在这里插入图片描述

这样如果没有给值的情况下,会默认使用缺省值,不会导致是随机值的情况。

假设我们的成员变量中存在const成员变量const int i;和引用成员变量int& ret;,我们再来看一下:

Date(int year = 1, int month = 1, int day = 1)	:_year(year),_month(month)
{_day = 15;ret = year;r = day;
}

如果这样写程序会报这样的错误:
在这里插入图片描述
它们都有一个共同的特点就是:必须在定义时就完成初始化。

而初始化列表可以认为是每个成员变量定义初始化的地方,所以它们必须在初始化列表上初始化。

Date(int year = 1, int month = 1, int day = 1)	:_year(year),_month(month),ret(year),r(2)
{_day = 15;
}

这样程序就可以正常运行,语法上是没有错误的,但这里存在一个问题:ret的引用对象是yearyearDate结束的时候就会被销毁,此时ret就相当于野引用,有风险,所以ret最好引用全局变量,或者定义一个变量让它引用。

Date(int& x,int year = 1, int month = 1, int day = 1)	:_year(year),_month(month),ret(x),r(2)
{_day = 15;
}int n = 1;
Date d1(n);
d1.Print();

这样就解决问题了。

还有一种情况就是成员变量中存在没有默认构造的类类型变量,我们给出以下类:

class Phone
{
public:Phone(int price):_price(price){}
private:int _price;
};

我们在我们的成员变量中定义一个Phone类对象pri:

private://声明int _year = 2025;int _month = 2;int _day = 2;int& ret;const int r = 1;Phone pri;

此时运行代码就会发生以下报错:
在这里插入图片描述
没有合适的默认构造函数可用,也就是没有不需要传参的就可以使用的构造函数所以它也必须在初始化列表初始化。
在这里插入图片描述
这样才能完成它的初始化,相当于调用它的构造函数。

只要类中的成员变量中出现了这三类成员变量,只能使用初始化列表初始化,否则会编译报错。

初始化列表还有一个特点:

Date(int& x,int year = 1, int month = 1, int day = 1)	:ret(x),r(2),pri(2100)
{_day = 15;
}

这里我没有写_year、_month、_day的初始化列表,但这三个成员变量也会走初始化列表。
在这里插入图片描述
即使没有写在初始化列表的成员变量,也要走初始化列表,所有的成员变量都要走初始化列表,其中我们上面讲到的三种特殊成员变量必须写在初始化列表上。

初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关建议声明顺序和初始化列表顺序保持⼀致

初始化列表总结

  • 无论是否显示写初始化列表,每个构造函数都有初始化列表;
  • 无论是否在初始化列表显示初始化成员变量,每个成员变量都要走初始化列表初始化;
    在这里插入图片描述

1、测试题

下面程序的运行结果是什么?()
A. 输出 1 1
B. 输出 2 2
C. 编译报错
D. 输出 1 随机值
E. 输出 1 2
F. 输出 2 1

#include<iostream>
using namespace std;
class A
{
public:A(int a):_a1(a),_a2(_a1){}void Print(){cout << _a1 << " " << _a2 << endl;}
private:int _a2 = 2;int _a1 = 2;
};
int main()
{A aa(1);aa.Print();return 0;
}

答案:D
原因:首先,初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关,所以初始化列表中会先初始化_a2,因为它是先声明的,而_a2由_a1初始化,此时_a1是随机值,所以_a2就是随机值,之后_a1进行初始化,_a1根据a进行初始化,此时a是1,所以_a1就是1了,综上,答案是D:输出 1 随机值

二、类型转换

  • C++支持内置类型隐式类型转换为类类型对象,需要有相关参数为内置类型的构造函数。
  • 构造函数前面加explicit就不再支持隐式类型转换。
  • 类类型的对象之间也可以隐式转换,需要相应的构造函数支持。

我们来看看类型转换如何实现:

#include <iostream>
using namespace std;class A
{
public:A(int n):_a(n),_b(n){}A(const A& d){_a = d._a;_b = d._b;}void Print(){cout << _a << " " << _b << endl;}
private:int _a = 1;int _b = 2;
};int main()
{return 0;
}

这里我简单定义了一个框架,根据第一个前提条件我们知道内置类型转化为类类型,需要有相关参数为内置类型的构造函数。也就是实现的A(int n),所以现在能够进行用int类型隐式转化为类类型了。

A a1 = 1;
a1.Print();

运行结果:
在这里插入图片描述
代码能够正常运行,也就是发生了隐式类型转换过程:先用1构造一个A的临时对象,再用这个临时对象拷贝构造a1,这也是为什么拷贝构造函数加const的原因,因为临时对象具有常性
在这里插入图片描述
当然新编译器遇到构造+拷贝构造会优化为直接构造。但在老编译器下上面的过程是真实发生的。

注意 A& r = 1;这段代码是错的,因为会先转化为临时对象而临时对象具有常性,A不能引用const A涉及到了权限的放大,所以A之前要加const

这里是构造函数中是一个形参的情况,那要是两个形参该如何更改才能进行隐式类型转换呢?

A(int n,int m):_a(n),_b(m)
{
}

如果我们还按照之前那样写就会报以下错误:
在这里插入图片描述
因为是两个参数,所以报错也正常,应该这样写:

A a1 = { 1,2 };
a1.Print();

在这里插入图片描述
不想支持隐式类型转换,只要在构造函数前面加上explicit就不再支持隐式类型转换了explicit A(int n,int m)

我们再来看一下类类型类类型之间的转换:
A:

class A
{
public://explicit A(int n,int m)A(int n, int m):_a(n), _b(m){}A(const A& d){_a = d._a;_b = d._b;}void Print(){cout << _a << " " << _b << endl;}int Get() const{return _a * _b;}
private:int _a = 1;int _b = 2;
};

增加了Get函数方便B获取值。

B:

class B
{
public:B(const A& d):_c(d.Get()){}void Print(){cout << _c << endl;}
private:int _c = 2;
};
A a1 = { 100,200 };
B b1 = a1;
b1.Print();

运行测试:
在这里插入图片描述
这里也会发生构造临时变量(用a1构造B类型的临时对象),并用临时变量拷贝构造b1的过程。

三、static成员

1. 静态成员变量

static修饰的成员变量,称之为静态成员变量静态成员也是类的成员,受public、protected、private访问限定符的限制。静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。所以静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表,因此静态成员变量⼀定要在类外进行初始化

我们一起来看一下静态成员变量是如何定义使用的:

#include <iostream>
using namespace std;class A
{
public:private://不属于某个对象,属于整个类//不能给缺省值//声明static int _count;int _a;
};
//定义并初始化
int A::_count = 0;int main()
{return 0;
}

这就是静态成员变量定义的代码,那接下来假设我要计算程序一共创建了多少个类,给如何计算呢?

A(int a):_a(a)
{++_count;
}
A(const A& d)
{_a = d._a;++_count;
}

创建类对象肯定要经过这两个成员函数,所以调用这两个成员函数时我让_count加加即可。

为了获取count的值我们可以再写一个成员函数:

int GetCount()
{return _count;
}

我们执行以下代码:

A a1(1);
A a2 = a1;
cout << a1.GetCount() << endl;

运行结果:
在这里插入图片描述
用全局变量也可以统计,但是用静态成员变量更好,它有一个优点,就是别人不能修改,能减少出错。

注意:这里打印不能写成cout << A::GetCount() << endl;,因为GetCount函数是某一个类对象的,不是整个类的。

2. 静态成员函数

  • static修饰的成员函数,称之为静态成员函数静态成员函数没有this指针
  • 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针
  • 非静态的成员函数,可以访问任意的静态成员变量静态成员函数

依旧以统计类对象个数为例,我们可以把GetCount函数换成静态的。

//没有this指针
static int GetCount()
{//_a++;不能访问非静态的,没有this指针return _count;
}
A a1(1);
A a2 = a1;
cout << A::GetCount() << endl;
cout << a1.GetCount() << endl;

运行结果
在这里插入图片描述

四、友元

  • 友元提供了⼀种突破类访问限定符封装的方式,友元分为:友元函数友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到⼀个类的里面。
  • 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,它不是类的成员函数。
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制

在上期博客讲解流插入、流提取运算符重载时,我们已经用过友元函数了:
在这里插入图片描述
补充

  • ⼀个函数可以是多个类的友元函数。比如一个函数void func(const A& a, const B& b);,它想要访问A类对象B类对象中的私有成员就要分别在A类B类中声明友元函数。
#include <iostream>
using namespace std;
// 前置声明,否则A的友元函数声明编译器不认识B
class B;
class A
{friend void func(const A& a, const B& b);
public:A(int a):_a(a){ }
private:int _a = 1;
};
class B
{friend void func(const A& a, const B& b);
public:B(int b):_b(b){ }
private:int _b = 2;
};
void func(const A& a, const B& b)
{cout << a._a << endl;cout << b._b << endl;
}
int main()
{A a(2);B b(3);func(a, b);return 0;
}

运行结果
在这里插入图片描述

  • 友元类的关系是单向的不具有交换性,比如B类A类的友元,但是A类不是B类的友元,即B类可以访问A类,但A类不能访问B类。

A

class A
{friend class B;
public:A(int a):_a(a){}
private:int _a = 1;
};

B:

class B
{
public:B(int b):_b(b){}void Print(const A& d){cout << "A._a: " << d._a << endl;cout << "B._b: " << _b << endl;}
private:int _b = 2;
};

main:

int main()
{A a(2);B b(3);b.Print(a);return 0;
}

运行结果
在这里插入图片描述

  • 友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。
  • 友元有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用

五、内部类

  • 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟定义在全局相比,它只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。简单来讲,它们是平行关系不是包含关系
#include <iostream>
using namespace std;
class A
{
public:class B{private:int _b = 2;};
private:int _a = 3;
};
int main()
{A a;return 0;
}

在这里插入图片描述
定义一个A类对象可以看到内部只有成员变量,所以A类B类之间不是包含关系

此时我们的B类public也就是对外开放的我们可以这样定义一个B类对象A::B b;,这也从侧面说明它们之间并不是包含关系,而是平行关系。如果B类隐私或者保护下,就不能再访问B类了。

  • 内部类默认是外部类的友元类
class A
{
public:class B{public:B(const A& d):_b(d._a){ }private:int _b = 2;};
private:int _a = 3;
};

B类中可以访问A类的私有成员变量。
在这里插入图片描述

  • 内部类本质也是⼀种封装,当B类A类紧密关联,B类实现出来主要就是给A类使用,那么可以考虑把B类设计为A内部类,如果放到private/protected位置,那么B类就是A类专属内部类,其他地方都用不了。

六、匿名对象

  • 类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象。只要给它命名了,它就是有名对象。
  • 匿名对象生命周期只在当前⼀行,⼀般临时定义⼀个对象当前用⼀下即可,就可以定义匿名对象。

下面我们写一个带有构造和析构函数的类来学习一下匿名对象。

class A
{
public:A(int a=1):_a(a){ cout << "A(int a=1)" << endl;}~A(){cout << "~A" << endl;}
private:int _a = 1;
};

如此一来函数有没有执行构造和析构我们看一下打印窗口就知道了。

// 不能这么定义对象,因为编译器⽆法
// 识别下⾯是⼀个函数声明,还是对象定义
//A a();//匿名对象
A();//声明周期只在当前一行
A(1);

调试代码到最后一行:
在这里插入图片描述
我们可以看到刚刚调试运行到A(1);时,A();完成了构造和析构,说明它的生命周期已经结束了。
匿名对象的使用场景在于当你想要调用或者使用类中的东西又不想过多定义类对象时,就可以使用匿名对象,用完之后下一行它就销毁了

例如:

class sum
{
public:int sum_count(int n){//...return n;}
};

在这里插入图片描述

七、对象拷贝时的编译器优化

现代编译器会为了尽可能提高程序的效率在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝

如何优化,C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。例如:Visual Studio2022在优化方面就非常先进。

linux下可以将下⾯代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elide-constructors 的方式关闭构造相关的优化。

总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~

相关文章:

  • Linux 驱动开发步骤及 SPI 设备驱动移植示例
  • chili调试笔记13 工程图模块 mesh渲染 mesh共享边显示实现
  • 藏文智能输入入门实践-简单拼写纠错
  • 【Agent】使用 Python 结合 OpenAI 的 API 实现一个支持 Function Call 的程序,修改本机的 txt 文件
  • 光伏“531”政策倒逼下,光储充一体化系统如何破解分布式光伏收益困局?
  • VMware更改语言设置
  • 使用Go语言对接全球股票数据源API实践指南
  • 【C++进阶】第1课—继承
  • 【软件设计师:数据结构】1.数据结构基础(一)
  • 【Bootstrap V4系列】学习入门教程之 组件-轮播(Carousel)高级用法
  • linux基础学习--linux磁盘与文件管理系统
  • OC的实例对象,类对象,元类对象
  • 外包团队协作效率低,如何优化
  • python打卡day18
  • 【一篇详解】深入浅出RabbtiMQ消息队列
  • openstack的网络
  • 第十六次博客打卡
  • Qt开发经验 --- 避坑指南(6)
  • Java中字符转数字的原理解析 - 为什么char x - ‘0‘能得到对应数字
  • C++回顾 Day4
  • 顾家家居:拟定增募资近20亿元,用于家居产品生产线的改造和扩建等
  • 上海将发布新一版不予行政处罚清单、首份减轻行政处罚清单
  • 夜读丨母亲的手擀面
  • 金融监管总局:正在修订并购贷款管理办法,将进一步释放并购贷款的潜力
  • 欧盟官员:欧盟酝酿对美关税政策反制措施,包含所有选项
  • 蓝佛安:中方将采取更加积极有为的宏观政策,有信心实现今年5%左右增长目标