【 C++ 】类和对象的学习(三)

2024-09-07 16:52
文章标签 c++ 学习 对象

本文主要是介绍【 C++ 】类和对象的学习(三),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言:

😘我的主页:OMGmyhair-CSDN博客

目录

一、初始化列表

二、类型转换

三、static成员

四、友元

五、内部类

六、匿名对象


一、初始化列表

当我们之前在写构造函数时,我们通常在构造函数内对成员变量进行赋值。但其实还有一种方法是对成员变量进行初始化,那就是初始化列表:

初始化列表的使用: 以⼀个冒号开始,接着是⼀个以逗号分隔的数据成
员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。
但是函数体内也能对成员变量进行赋值,那么为什么还要初始化列表呢?
其实初始化列表可以认为是每个成员变量定义初始化的地方。
通过这句话我们来看看以下初始化需要注意的地方:
1.每个成员变量在初始化列表只能出现一次
其实很好理解,既然它是成员变量定义的地方,而成员变量不可以重复定义,因此也不能重复出现。
可以看到编译器对重复出现的成员变量进行了报错:已初始化。

2.引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化。

这个也很好理解。像这三种变量,它们本身必须在定义时就进行初始化。而初始化列表就是它们定义的地方,所以它们在初始化列表中必须进行初始化:

(1)引用变量

对于未定义时未初始化的引用变量,编译器要求对齐进行初始化。

(2)const成员变量

至于被const修饰的的变量,定义初始化后就不能再改变,也就不能再赋值,所以必须在定义时就进行初始化。

(3)没有默认构造的类类型变量

如果一个类没有默认构造,那么它的对象在定义时就必须进行初始化,否则没有合适的构造函数供它使用。

对这三类变量正确的初始化可以如下:

class A
{
public:A(int x){_a = x;}private:int _a;
};class Date
{
public:Date(int& y,int date=20140101): year(y),_date(date),a(2014){}
private:int& year;const int _date;A a;
};

3.在C++11中支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。

首先来看下面的一段代码:

class Date
{
public:Date(int year = 2014, int month = 12, int day = 3): _year(),_day(13){}void Print(){cout << _year << "/" << _month << "/" << _day << "/" <<_date<<endl;}private:int _year = 1000;int _month = 10;int _day = 10;int _date;
};int main()
{Date d1(2024, 9, 6);Date d2;cout << "d1日期为:";d1.Print();cout << "d2日期为:";d2.Print();return 0;
}

来看看结果和你想的是否一样:

(1).首先,我们要区分构造函数参数列表的缺省值和成员变量声明处的缺省值

参数列表的缺省值是给你在初始化对象时调用构造函数是否需要传参用的,而成员变量声明处的缺省值,是用来给没有初始化的成员变量用的。

如果你参数列表的值没有赋值给成员变量,那么成员变量的值不会受到改变,这就是为什么d1初始化时是(2024,9,6),但是d1和d2的实际结果相同。

(2).为什么_year的值是0而不是1000?为什么_date是随机值?

在C++其实int也有构造函数,在初始化列表中,我们显式写了_year(),这一过程编译器会调用int的默认构造函数将_year赋值为0。

而_date我们没有在初始化列表中显式写出,则不会调用构造函数,那么就是随机值。

(3)._month为什么是10?_day为什么是13而不是10。

在初始化列表中,我们没有显式写出_month,这时如果在声明中有缺省值,就会用缺省值对_month进行赋值。

而_day在初始化列表中被我们显式地写出,进行了初始化,就不会再去使用在声明中的缺省值。

4.对于没有显式在初始化列表初始化的自定义成员变量,编译器会调用它们自己的默认构造函数。如果没有默认构造函数,编译器会报错。

无论我们是否显式地写出初始化列表,每个构造函数都有初始化。无论我们是否在初始化中显式写出某个成员变量,每个成员变量都会走一遍初始化列表。

总结一下成员变量走初始化列表的各种情况:

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

我们用一道题目来加深理解:

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();
}

我们来看看上面代码的输出结果:

首先,初始化列表按照成员变量的声明顺序进行初始化。所以第一个进行初始化的成员变量是_a2,此时将_a1的值赋给_a2,但这个时候_a1还未进行初始化,因此_a1的值是随机值,所以_a2也是随机值。

第二个进行初始化的值是_a1,将a的值赋给_a1,而a的值为1,所以_a1的值也为1。

所以这道题的答案为1和随机值,选D。


二、类型转换

1.C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。

在上面的代码中就发生了隐式类型转换。在A a=2这句代码中产生了一个A类型的临时变量,2赋值给这个临时变量,这个临时变量再赋值给a。换句话说,2构造了一个A类型的临时对象,这个临时对象拷贝构造给a,而编译器遇到连续构造加上拷贝构造会直接优化成直接构造,意思是直接对a进行构造不用临时对象。

我们还能对这个临时对象进行引用:

因为临时对象具有常性,而我们引用的就是临时对象,所以需要加上const。

在C++11后我们还能进行多个参数的类型转换:

2.我们还能进行不同类类型对象之间的转换,需要相应的构造函数支持

class B
{
};class A
{
public:A(int x){_a = x;}A(int x, int y){_a = x;_aa = y;}A(B b)//隐式转换需要相应的构造函数支持{_b = b;}
private:int _a;int _aa;B _b;
};int main()
{A a = { 1,1 };//不同类类型对象之间的隐式转换B ab;A aa = ab;return 0;
}

3.在构造函数前面加explicit就不再支持隐式转换:

可以看到加入了explicit关键字后,编译器对隐式转换进行了报错。


三、static成员

静态成员就是静态成员变量和静态成员函数,首先来看看它们共同的特性:

1.静态成员也是类的成员,受public、protected、private 访问限定符的限制。

2.突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。

class A
{
public:static int GetX(){return _ax;}static int _az;private:static int _ax;static int _ay;
};int A::_ax=1;
int A::_ay=2;
int A::_az=3;int main()
{A a;cout << A::_az << endl;cout << A::GetX() << endl;cout << a._az << endl;cout << a.GetX << endl;
}

来看在类中静态成员变量的几个特性:

1.用static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化。

class Date
{
private:static int _year;static int _month;static int _day;
};int Date::_year;
int Date::_month;
int Date::_day;
2.静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
可以看到在ra的大小比rb的大小小4个字节,因为A类型的对象没有将静态成员变量a的大小算进去。
3. 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,静态成员 变量不属于某个对象,不走构造函数初始化列表。
可以看到编译器对拥有缺省值的静态成员变量进行了报错:
再来看 静态成员函数的特性:
1.⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。因为它没有this指针,所以静态成员函数可以访问其它的静态成员,但不能访问非静态的。
2.非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。

使用静态成员的特性,我们来看三道题:

1.实现一个类,计算程序中创建出了多少的类对象

class Create
{
public:Create()//默认构造函数{_count++;}Create(const Create& c)//拷贝构造函数{_count++;}~Create(){_count--;}static int GetCount(){return _count;}private:static int _count;
};int Create::_count=0;

2.求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

class Sum
{
public:Sum(){_ret += _n;_n++;}static int GetRet(){return _ret;}private:static int _n;static int _ret;
};int Sum::_n = 1;
int Sum::_ret = 0;class Solution
{
public:int Sum_Solution(int n) {//调用n个构造函数Sum* p = new Sum[n];delete[] p;//释放申请空间return Sum::GetRet();}
};int main()
{cout << Solution().Sum_Solution(5) << endl;//使用匿名对象进行访问return 0;
}

3.设已经有A,B,C,D 4个类的定义,程序中A,B,C,D构造函数调用顺序为?()

设已经有A,B,C,D 4个类的定义,程序中A,B,C,D析构函数调用顺序为?()

C c;
int main()
{
A a;
B b;
static D d;
return 0;
}

选项:

A D B A C
B B A D C
C C D B A
D A B D C
E C A B D
F C D A B
首先看构造函数调用顺序,全局变量是在main函数之前就创建好的,至于局部静态变量是第一次运行到static D d才会初始化,那么调用顺序就非常清楚了,首先是全局变量C再是main函数中的A、B,直到运行到局部静态变量处才是D,故选择E选项。
再来看析构函数调用顺序,我们已知对象是后定义的先析构。局部静态变量的生命周期是全局的,而局部变量生命周期是在函数体内,肯定比全局和局部静态先一步销毁,所以是B、A这俩个先销毁调用析构。再来看全局和静态,它们的生命周期都是全局,所以谁后定义先析构,因此这俩个析构顺序是D、C,故选择B选项。

四、友元

友元提供了⼀种突破类访问限定符封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到⼀个类的里面。
首先来看友元函数的特性:
1.外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
2.友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
3.⼀个函数可以是多个类的友元函数。
4. 友元函数不能用const修饰,因为友元没有this指针
class Time;//Time的前置声明,否则Date不认识Time
class Date
{friend void Print(const Date& date, const Time& time);public:Date(int year = 2014, int month = 12, int day = 13){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};class Time
{friend void Print(const Date& date, const Time& time);
public:Time(int hour = 21, int minute = 18, int second = 13){_hour = hour;_minute = minute;_second = second;}private:int _hour;int _minute;int _second;
};void Print(const Date& date, const Time& time)
{cout << date._year << endl;cout << time._hour << endl;
}int main()
{Date a;Time b;Print(a,b);return 0;
}

再来看看友元类的特性:

1.友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。

2.友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。

3.友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。

class Time
{friend class Date;
public:Time(int time=2129){_time = time;}private:int _time;
};class Date
{friend class Age;
public:Date(int date=20240906){_date = date;cout << _t._time << endl;//Date是Time的朋友,可以访问Time的私有}private:int _date;Time _t;
};class Age
{
public:Age(int age = 20){_age = age;cout << _d._date << endl;//Age是Date的朋友,可以访问Date的私有}private:int _age;Date _d;
};

但是这里的朋友是单向的,例如Date是Time的朋友,可以访问Time的私有。但是Time不是Date的朋友,不能访问Date的私有。

朋友关系不能连续。Date是Time的朋友,Age是Date的朋友,但是Age不是Time的朋友。


五、内部类

1.如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。所以当我们计算外部类的大小时,不需要算上内部类
在下面示例中A类对象大小和C类对象相同,所以计算外部类大小时不需要算上内部类。
2.内部类默认是外部类的友元类。同样这种关系只是单向,内部类可以访问外部类的私有,但是外部类不能访问内部类的成员变量。
3.内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考 虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
像之前题目中的用类计算一个数的累和,可以将其改造成外部类和内部类。
class Solution
{
public:class Sum{public:Sum(){_ret += _n;_n++;}};int Sum_Solution(int n) {//调用n个构造函数Sum* p = new Sum[n];delete[]p;return _ret;}private:static int _n;static int _ret;
};int Solution::_n=1;
int Solution::_ret=0;

六、匿名对象

1.用类型(实参)定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名(实参)定义出来的叫有名对象 。
2.匿名对象生命周期只在当前一行,⼀般临时定义⼀个对象当前用一下即可,就可以定义匿名对象。
当我们需要用到类里面的成员函数又不想初始化对象,就可以使用匿名对象来调用类里面的成员函数:
注意不要将匿名对象写成下面这样:
因为编译器无法识别是对象定义还是函数声明。

七、编译器优化

现代编译器为了追求效率会在不影响正确性的前提下对传值或传参过程中产生的拷贝进行省略。大部分编译器会对一个表达式中的连续拷贝进行优化,还有一些编译器更加“激进”,会对一些跨行跨表达式进行优化。

在下面这条语句中就产生了连续拷贝:

实际过程:

编译器优化后:


我现在使用的是vs2022,所以在一些场景下,优化比较厉害(以下都是在debug版本下进行):

场景1:

真实情况:

其实在这一过程中相当于省掉了a这一对象,因为a的析构是在打印函数之前,这里相当于直接构造临时对象。


场景2:

即使我们对a进行了前置++操作,编译器还是省掉了构造a的这一过程,可以看出编译器还是很厉害的。


场景3:

从这个场景来看,优化得就很明显了。不仅省掉了传值返回过程中临时对象的构造,还省掉了对a的构造,相当于直接用1去构造对象ret。



如果这篇文章有帮助到你,请留下您珍贵的点赞、收藏+评论,这对于我将是莫大的鼓励!学海无涯,共勉!😘😊😗💕💕😗😊😘




这篇关于【 C++ 】类和对象的学习(三)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1145616

相关文章

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

使用Java读取本地文件并转换为MultipartFile对象的方法

《使用Java读取本地文件并转换为MultipartFile对象的方法》在许多JavaWeb应用中,我们经常会遇到将本地文件上传至服务器或其他系统的需求,在这种场景下,MultipartFile对象非... 目录1. 基本需求2. 自定义 MultipartFile 类3. 实现代码4. 代码解析5. 自定

Unity新手入门学习殿堂级知识详细讲解(图文)

《Unity新手入门学习殿堂级知识详细讲解(图文)》Unity是一款跨平台游戏引擎,支持2D/3D及VR/AR开发,核心功能模块包括图形、音频、物理等,通过可视化编辑器与脚本扩展实现开发,项目结构含A... 目录入门概述什么是 UnityUnity引擎基础认知编辑器核心操作Unity 编辑器项目模式分类工程

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数

C++ vector越界问题的完整解决方案

《C++vector越界问题的完整解决方案》在C++开发中,std::vector作为最常用的动态数组容器,其便捷性与性能优势使其成为处理可变长度数据的首选,然而,数组越界访问始终是威胁程序稳定性的... 目录引言一、vector越界的底层原理与危害1.1 越界访问的本质原因1.2 越界访问的实际危害二、基

Python学习笔记之getattr和hasattr用法示例详解

《Python学习笔记之getattr和hasattr用法示例详解》在Python中,hasattr()、getattr()和setattr()是一组内置函数,用于对对象的属性进行操作和查询,这篇文章... 目录1.getattr用法详解1.1 基本作用1.2 示例1.3 原理2.hasattr用法详解2.

c++日志库log4cplus快速入门小结

《c++日志库log4cplus快速入门小结》文章浏览阅读1.1w次,点赞9次,收藏44次。本文介绍Log4cplus,一种适用于C++的线程安全日志记录API,提供灵活的日志管理和配置控制。文章涵盖... 目录简介日志等级配置文件使用关于初始化使用示例总结参考资料简介log4j 用于Java,log4c

C++归并排序代码实现示例代码

《C++归并排序代码实现示例代码》归并排序将待排序数组分成两个子数组,分别对这两个子数组进行排序,然后将排序好的子数组合并,得到排序后的数组,:本文主要介绍C++归并排序代码实现的相关资料,需要的... 目录1 算法核心思想2 代码实现3 算法时间复杂度1 算法核心思想归并排序是一种高效的排序方式,需要用

javaSE类和对象进阶用法举例详解

《javaSE类和对象进阶用法举例详解》JavaSE的面向对象编程是软件开发中的基石,它通过类和对象的概念,实现了代码的模块化、可复用性和灵活性,:本文主要介绍javaSE类和对象进阶用法的相关资... 目录前言一、封装1.访问限定符2.包2.1包的概念2.2导入包2.3自定义包2.4常见的包二、stati