【逐步剖C++】-第二章-C++类和对象(下)

news2024/12/25 0:21:13

前言:本文是对类和对象知识点的最后一篇总结,前两篇的链接如下:
【逐步剖C++】-第二章-C++类和对象(上)
【逐步剖C++】-第二章-C++类和对象(中)
这三篇加起来就是笔者学习在类和对象中的所有总结了,希望能对读者有一些帮助
下面是文章导图:
在这里插入图片描述

那么本文也主要以导图为思路进行分享,话不多说,让我们开始吧

一、关于构造函数的补充

前言:在上一章中,我们了解到构造函数的基本功能是做一些初始化工作,对类的成员变量进行赋初值。但严格来说,构造函数内部的赋值不能称作是初始化,因为初始化只能初始化一次,而在构造函数内部可进行多次赋值操作。

且这里会涉及到一个有关类成员变量的声明和定义问题,因为有些类型的对象要求在定义时进行初始化。如const类型的对象。那么我们平常写的成员变量究竟是声明还是定义呢?如果是定义,那么在其之后给缺省值是否就相当于定义并初始化了呢?再具体一点,当一个const类型的对象作为一个类的成员变量时,其定义并初始化的位置在哪呢?这些问题会在下面的介绍中予以解答,请大家继续阅读。

1、初始化列表

(1)定义:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式,如:

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

(2)四点特性

  • 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  • 类中包含以下成员,必须放在初始化列表位置进行初始化
    • 引用类型的成员变量
    • const成员变量
    • 自定义类型成员变量(且该类没默认构造函数时)

说明:可以发现,前两种类型的变量都要求在定义时进行初始化,那么其实C++已经规定了,初始化列表为成员定义的位置。我们平常在private下写的成员仅是声明

还有一点,C++11后,支持在给类的对象声明时给缺省值,但给的缺省值本质也还只是声明,在调用构造函数创建对象时,都会走一遍初始化列表进行定义,此时若在初始化列表处对相应成员变量没有给值,才会真正用上声明时的缺省值。也就是说,声明时的缺省值是服务于初始化列表的

那么最开始的问题也就有了答案:我们平常写的成员变量(无论是否有缺省值)都只是声明,成员变量定义并初始化的位置在构造函数的初始化列表

  • 对于自定义类型的变量,编译器一定会先使用初始化列表进行初始化
  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

可通过一道题来理解上面这个特性:

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。解释如下:
初始化列表执行的顺序和成员变量声明的顺序一致,所以在初始化列表中先执行的是将_a1的值赋给_a2(此时_a1尚未初始化,是个随机值);接着才将参数1传给_a1,故最终输出的结果是1和随机值。

补充一点:初始化列表很重要,加上第三点特性,一般也建议用初始化列表进行初始化,但需要注意仍有初始化列表做不了的工作,如:给成员变量申请空间并检查甚至再对申请的空间写入一些数据等。

总结来说,在调用构造函数时,会优先走一遍初始化列表中的内容,接着仍会逐语句执行构造函数体中的内容

2、explicit关键字

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

class A1
{
public:
	//单参数的构造函数
    A1(int a)
    {
        _a1 = a;
    }

private:
    int _a1;
};

class A2
{
public:
	//除第一个参数无默认值其余均有默认值的构造函数
    A2(int a1,int a2 = 2)
    {
        _a1 = a1;
    }

private:
    int _a1;
    int _a2;
};

int main()
{
    A1 a1 = 1;
    A2 a2 = 2;
    return 0;
}

对于 A1 a1 = 1;本质是隐式类型转换,编译器会先构造一个成员变量_a1 = 1的A1类的匿名对象(关于匿名对象后文会介绍),最后匿名对象在将自己拷贝给对象a1;对于 A2 a2 = 2;同理。
(2)explicit关键字
explicit修饰构造函数,将会禁止构造函数的隐式转换。一定程度上可以增强代码的规范性。
在如上A1,A2两个类的构造函数前加上explicit关键字后,main函数中的两条语句将不能执行,编译报错为:
在这里插入图片描述

二、static静态成员

1、概念

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。

2、特性

(1)静态成员为所有类的对象所共享,不属于某个具体的对象,存放在静态区
(2)静态成员变量必须在类外定义,也就是说其没有初始化列表(类的普通成员定义的地方),故不能在声明时直接给缺省值;定义时不添加static关键字,类中只是声明
(3)类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
(4)静态成员函数没有隐藏的this指针不能访问任何非静态成员;静态成员函数和静态成员变量一般配套出现
(5)静态成员也是类的成员,受publicprotectedprivate 访问限定符的限制
(6)静态成员就相当于同一个类的所有对象的全局变量(或者可理解为将全局变量封装到类中,但只有通过该类或类的对象才能访问
代码例:

class A
{
public:
	static int GetA() { return _a; }	//静态成员函数
	
private:
	static int _a;		//静态成员变量
};

int A::_a = 0;		//静态成员变量需在类外定义(注意是定义,所以要加上类型)

int main()
{
    A a;
    //两种访问静态成员函数的方法
    cout << a.GetA() << endl;
    cout << A::GetA() << endl;
}

常见问题:

  • 静态成员函数可以调用非静态成员函数吗
    不能,因为静态成员函数没有this指针,无法进行传参;
  • 非静态成员函数可以调用类的静态成员函数吗
    可以,都在类域中,类的静态成员函数就相当于对于整个类而言的全局函数

3、使用场景

(1)实现一个类来统计程序中共创建了多少这个类的对象
实现思路:类的对象在创建时会自动调用类的构造函数或拷贝构造函数,故在调用构造函数和拷贝构造时时让相应地静态成员变量++即可。
具体代码如下:

class A
{
public:
	//对象创建,计数变量++
	A() { ++_scount; }
	A(const A& t) { ++_scount; }
	
	//对象销毁,计数变量--
	~A() { --_scount; }
	static int GetACount() { return _scount; }
	
private:
	static int _scount;
};

int A::_scount = 0;

(2)还有一道 “趣味” 题
原题链接:JZ64 求1+2+3+…+n
求解思路:创建一个类,类中有两个静态成员变量,一个_count用于统计当前对象的数量,一个_sum用于进行总和的计算。把它们放入构造函数中,每创建一个对象时
_count++; sum += _count;这样在创建完n个对象时_sum的值即为1+2+3+…+n的值。
具体代码如下:

struct Sum
{
    Sum()
    {
        _count++;
        _sum += _count;
    }
    static int _sum;
    static int _count;
};

int Sum::_sum = 0;
int Sum::_count = 0;

class Solution {
public:
    int Sum_Solution(int n) 
    {
        for(int i = 0; i < n; i++)
        {
            Sum s;
        }
        return Sum::_sum;
    }
};

(3)有时会有这样的要求:设计一个类,在类外只能在栈上创建对象或在类外只能在堆上创建对象
为了实现这个要求,除了要利用静态成员函数外,更重要的一步是对构造函数的 “不一般” 处理——将构造函数作为private成员。如此一来就只有在类中的成员函数才能调用构造函数了;此时将相应调用构造函数的成员函数作为public并定义为static的即可(向外界提供接口)
具体代码如下:

class A
{
public:
    //返回一个栈区上的A类对象()
    static A GetStackObj()
    {
        A a(1);
        return a;
    }

    //返回一个堆区上的A类对象
    static A* GetHeapObj()
    {
        return new A(1);
    }

private:
    A(int a =1)
        :_a(a)
    {}
    int _a;
};

int main()
{
    
    //创建一个栈区上的A类对象
    A sa = A::GetStackObj();

    //创建一个堆区上的A类对象
    A* ha = A::GetHeapObj();

    return 0;
}

三、友元

前言:友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用

1、友元函数

(1)定义:友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
在上一篇日期类的简单实现中,流插入和流提取运算符的重载就是友元函数
(2)特性

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同

2、友元类

(1)定义
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
(2)特性

  • 友元关系是单向的,不具有交换性。
    若声明A为B的友元类,则在A中可以直接访问所有B类中的非公有成员;但在B类中无法访问A类中的非公有成员
  • 友元关系不能传递
    如果C是B的友元, B是A的友元,则不能说明C时A的友元
  • 友元关系不能继承(关于继承会在后续章节进行说明)

四、内部类

1、定义

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限

2、特性

(1)内部类天生就是外部类的友元类
(2)注意内部类可以直接访问外部类中的static成员,不需要借助外部类的对象/类名(因为静态成员变量属于整个类,故对内部类也不例外)
(3)sizeof(有内部类的外部类对象) = sizeof(没有该内部类的相同外部类对象)。即计算外部类对象的大小时,与内部类无关,因为内部类仅是定义在外部类中,并没有实例化出对象
(4)内部类也受访问限定符的限制,限定的效果体现在通过外部类使用内部类时

相关代码例:

class A
{
public:
    A(int a1 = 1, int a2 = 2)
        :_a1(a1)
        ,_a2(a2)
     {}

    static int _acount;

    //内部类
    class InnerA
    {
    public:
        InnerA(int ia1 = 1, int ia2 = 2)
            :i_a1(ia1)
            , i_a2(ia2)
        {}

        void Func(const A& a)
        {
            cout << a._a1 << " " << a._a2 << endl;	//内部类天生就是外部类的友元类
            cout << ++_acount << endl;		//直接访问外部类中的static成员
        }

    private:
        int i_a1;
        int i_a2;
    };
private:
    int _a1;
    int _a2;
};

int A::_acount = 0;

int main()
{
    A::InnerA ia;	
    A a(2, 3);
    ia.Func(a);
}

运行结果:
在这里插入图片描述
其中A::InnerA ia;是使用内部类创建对象的方法。InnerA类本质定义在了A类中,故若想使用则需通过域作用访问符,“ 穿过 ” A的类域去使用(和命名空间类似,告诉编译器到哪里去找);注意若InnerA类在A类中为private成员,那么穿过A的类域也是无法使用InnerA类创建对象的。

五、匿名对象

1、定义

匿名对象就是和我们平常定义的对象相比,就是没有名字对象,定义方式为类型(),如:

//定义一个A类对象
A a;

//定义一个A类的匿名对象
A();

2、特性

(1)匿名对象不用取名,定义后能和正常对象一样使用;
(2)匿名对象的生命周期只有一行,即用即销;
(3)匿名对象具有常性;则const引用可延长匿名对象的生命周期,如:const A& ra = A();
(4)匿名对象通常用于仅需要相关类的对象发挥一次作用的场景,如:

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

void Func(A& a)
{
	...//
}

//若仅需将成员变量为1的A类对象传给Func函数完成一次操作即可写为:
Func(A(1));
//A(1)表示创建了一个成员变量值为1的A类匿名对象

六、编译器拷贝构造的优化

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,提高程序运行的效率,下面结合代码进行简单的介绍,大家对这部分的知识可以作为了解即可。

class A
{
public:

    A(int a = 0)
        :_a(a)
    {
        cout << "A(int a)" << endl;
    }

    A(const A& aa)
        :_a(aa._a)
    {
        cout << "A(const A& aa)" << endl;
    }

    A& operator=(const A& aa)
    {
        cout << "A& operator=(const A& aa)" << endl;
        if (this != &aa)
        {
            _a = aa._a;
        }
        return *this;
    }

private:
    int _a;

};

void f1(A aa)
{}

A f2()
{
    A aa;
    return aa;
}

int main()
{

    cout << "(1):" << endl;
    f1(1);		// (1)
    cout << endl;

    cout << "(2):" << endl;
    f1(A(2));	//(2)
    cout << endl;

    cout << "(3):" << endl;
    A aa1 = f2();	//(3)
    cout << endl;

    cout << "(4):" << endl;
    A aa2;	
    aa2 = f2();	// (4)
    cout << endl;

    return 0;
}

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

说明

  • (1)
    其中包含了隐式类型转换:因为函数f1需要接收一个A类的对象,所以先用整数1构造出了一个A类的匿名对象;又因为函数f1传值调用,所以在调用时还会进行一次拷贝构造;由此一来,此条语句的执行就包含了连在一起的构造+拷贝构造,此时编译器会优化为直接构造
  • (2)
    本质和1类似,先是显示创建了一个成员变量为2的匿名对象,再将其作为参数传给f1;所以本条语句的执行也包含了连在一起的构造+拷贝构造,编译器会优化为一个构造
  • (3)
    调用函数f2回一个A类对象会调用一次拷贝构造(PS:函数f2中还有一次构造,但不涉及到这里的优化,这里的拷贝构造是将f2中构造出来的对象aa拷贝给一个A类的临时对象);返回的A类对象再用于初始化另一个A类对象会再调用一次拷贝构造(PS:将A类的临时对象拷贝给将要创建的对象aa1);如此一来,此条语句的执行就包含了连在一起的拷贝构造+拷贝构造,此时编译器会优化成一个拷贝构造
  • (4)
    先调用构造创建了一个对象aa2,之后通过调用f2返回一个对象重新赋值给该对象;那么此条语句包含的就是连在一起的 拷贝构造+赋值重载,但可以发现编译器对此不会进行优化

强调一点:
可以发现,上面所发生的优化都有一个共同的要求:连在一起
若构造和拷贝构造不在一条语句时,就不会发生优化,如:

A aa1;	//构造
f1(aa1);	//拷贝构造

故构造和拷贝构造能写到一个步骤就写到一个步骤。

本章完

看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或有误地方的地方还请过路的朋友们留个评论,多多指点,谢谢朋友们!🌹🌹🌹

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

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

相关文章

计算机网络网络层、应用层、数据链路层协议详解

目录 一、计算机网络 二、网络层 三、应用层 四、数据链路层 一、计算机网络 计算机网络是将多台计算机和其他网络设备通过通信链路连接起来&#xff0c;以实现数据交换和资源共享的系统。它是现代信息社会的基础设施之一&#xff0c;为人们提供了快速、可靠、安全的数据传…

学校项目培训之Carla仿真平台之Carla学习内容

一、Blender Blender入门&#xff1a;https://www.bilibili.com/video/BV1fb4y1e7PD/ Blender导入骨骼&#xff1a;https://www.bilibili.com/video/BV1hc41157nL 做一个车&#xff1a;https://www.bilibili.com/video/BV1hY411q7w2 二、Roadrunner RoadRunner Scenario…

Flink状态管理与检查点机制

1.状态分类 相对于其他流计算框架,Flink 一个比较重要的特性就是其支持有状态计算。即你可以将中间的计算结果进行保存,并提供给后续的计算使用: 具体而言,Flink 又将状态 (State) 分为 Keyed State 与 Operator State: 1.1 算子状态 算子状态 (Operator State):顾名思义…

计组—— I/O系统

&#x1f4d5;&#xff1a;参考王道课件 目录 一、I/O系统的基本概念 1.什么是“I/O”&#xff1f; ​编辑2.主机如何和I/O设备进行交互&#xff1f; 3.I/O控制方式 &#xff08;1&#xff09;程序查询方式 &#xff08;2&#xff09;程序中断方式 &#xff08;3&#x…

【MATLAB源码-第41期】基于压缩感知算法的OFDM系统信道估计和LS算法对比仿真。

操作环境&#xff1a; MATLAB 2013b 1、算法描述 压缩感知&#xff08;Compressed Sensing, CS&#xff09;是一种从稀疏或可压缩信号中重构完整信号的数学理论和技术。下面详细介绍压缩感知和它在OFDM信道估计中的应用。 1. 压缩感知基本概念 在传统采样理论中&#xff0…

数字电路逻辑与设计 之循环码和 移存码

有发现错误的能力&#xff0c;不能纠正 只能检查单次的错误&#xff0c;不能完全抗干扰 可以按照上面的方法来循环构造 移存码可以通过前推后推来实现

pytorch_神经网络构建1

文章目录 pytorch简介神经网络基础分类问题分析:逻辑回归模型逻辑回归实现多层神经网络多层网络搭建保存模型 pytorch简介 为什么神经网络要自定义数据类型torch.tensor? tensor可以放在gpu上训练,支持自动求导,方便快速训练,同时支持numpy的运算,是加强版,numpy不支持这些 为…

C++项目:【高并发内存池】

文章目录 一、项目介绍 二、什么是内存池 1.池化技术 2.内存池 3.内存池主要解决的问题 4.malloc 三、定长的内存池 四、高并发内存池整体框架设计 1.高并发内存池--thread cache 1.1申请内存&#xff1a; 1.2释放内存&#xff1a; 1.3用TLS实现thread cache无锁访…

GD32F10 串口通信

1. 什么是通信 通信&#xff0c;指人与人或人与自然之间通过某种行为或媒介进行的信息交流与传递&#xff0c;从广义上指需要信息的双方或多方在不违背各自意愿的情况下采用任意方法&#xff0c;任意媒质&#xff0c;将信息从某方准确安全地传送到另方。通信双方如果想正确传输…

SystemUI导航栏

SystemUI导航栏 1、系统中参数项1.1 相关开关属性2.2 属性设置代码 2、设置中设置“三按钮”导航更新流程2.1 属性资源覆盖叠加2.2 SystemUI导航栏接收改变广播2.3 SystemUI导航栏布局更新2.4 时序图 android13-release 1、系统中参数项 1.1 相关开关属性 设置->系统->…

C++算法 —— 动态规划(9)完全背包问题

文章目录 1、动规思路简介2、完全背包【模板】3、零钱兑换4、零钱兑换Ⅱ5、完全平方数 背包问题需要读者先明白动态规划是什么&#xff0c;理解动规的思路&#xff0c;并不能给刚接触动规的人学习。所以最好是看了之前的动规博客&#xff0c;以及01背包博客&#xff0c;才能看完…

学习C++语言可以适用于哪些方面

学习C可以让你具备开发各种类型软件和系统的能力&#xff0c;它是一种通用的、高性能的编程语言。以下是学习C的一些用途和应用领域&#xff1a; 系统开发&#xff1a;C被广泛用于操作系统、驱动程序和嵌入式系统的开发。通过学习C&#xff0c;你可以编写底层的系统代码&#x…

java大富翁

一、 概述 Java Swing大富翁游戏是一个经典的大富翁桌面游戏的简单实现&#xff0c;使用Java Swing库创建。该游戏允许玩家在一个虚拟棋盘上掷骰子&#xff0c;购买和升级属性&#xff0c;赚取租金和尽量丰富自己。这个文档说明将介绍如何安装和运行游戏&#xff0c;以及游戏规…

【C++】C++11——右值引用和移动语义、左值引用和右值引用、右值引用使用场景和意义、完美转发、新的类功能

文章目录 C115.右值引用和移动语义5.1左值引用和右值引用5.2左值引用与右值引用比较5.3右值引用使用场景和意义5.4右值引用引用左值及其一些更深入的使用场景分析5.5完美转发 6.新的类功能 C11 5.右值引用和移动语义 右值引用是C11引入的一个新特性&#xff0c;用于支持移动语义…

冯诺依曼体系结构与进程的初步理解

目录 一&#xff0c;冯诺依曼体系结构 1.是什么&#xff1f;特点 2.为什么&#xff1f; 二&#xff0c;操作系统 三&#xff0c;进程 1.什么是进程&#xff1f; 2.查看进程 3.进程的管理 4.fork()创建子进程 1.fork()简介 2.fork()干了啥 3.fork()为什么会有两个返回…

【Java】微服务——Ribbon负载均衡(跟进源码分析原理)

添加LoadBalanced注解&#xff0c;即可实现负载均衡功能&#xff0c;这是什么原理 1.负载均衡原理 SpringCloud底层其实是利用了一个名为Ribbon的组件&#xff0c;来实现负载均衡功能的。 2.源码跟踪 为什么我们只输入了service名称就可以访问了呢&#xff1f;之前还要获取…

mstsc无法保存RDP凭据, 100%生效

问题 即使如下两项都打勾&#xff0c;其还是无法保存凭据&#xff0c;特别是连接Ubuntu (freerdp server)&#xff1a; 解决方法 网上多种复杂方法&#xff0c;不生效&#xff0c;其思路是修改后台配置&#xff0c;以使mstsc跟平常一样自动记住凭据。最后&#xff0c;如下的…

Python无废话-办公自动化Excel写入操作

Python 办公自动化-Excel写入 创建并保存Excel文件 import openpyxl workbookopenpyxl.Workbook() #创建空Excel文件 sheetworkbook.active #获取活动的工作表 sheet.title“测试“ #修改sheet工作表名称为测试 workbook.save(“data\input\Test.xlsx”) #保存Excel文件 …

R中的min()函数 和max()函数

通过min()函数和max()函数产生Inf 数值空集的最小值和最大值是Inf和–Inf(按此顺序&#xff01;)这确保了传递性&#xff0c;例如min(x1&#xff0c;min(x2)) min(x1&#xff0c;x2)。对于数值x&#xff0c;每当length (x) 0时&#xff0c;max(x) - Inf和min(x) Inf(如果需…

C#餐饮收银系统

一、引言 餐饮收银系统是一种用于管理餐馆、咖啡厅、快餐店等餐饮业务的计算机化工具。它旨在简化点餐、结账、库存管理等任务&#xff0c;提高运营效率&#xff0c;增强客户体验&#xff0c;同时提供准确的财务记录。C# 餐饮收银系统是一种使用C#编程语言开发的餐饮业务管理软…