• C++类和对象 (下)


    作者:@小萌新
    专栏:@C++初阶
    作者简介:大二学生 希望能和大家一起进步!
    本篇博客目标:梳理类和对象剩下几个零碎的知识点
    在这里插入图片描述
    专注是一种能力

    本章目标

    1. 学习类中的输入输出流
    2. 再学构造函数
    3. 学习static成员
    4. 学习友元函数
    5. 学习内部类
    6. 再理解封装
    7. 再理解类和对象

    一. 输入输出流

    1.1 流概念

    相比较于C语言中的输入输出函数来说 C++的输入输出流不用指定类型

    这是为什么呢? 因为做了什么神奇的优化吗?

    其实并不是 这里只是用到了我们之前学过的一种东西而已 它的名字叫 函数重载

    在这里插入图片描述

    当然 这些重载函数能够识别的类型仅限于内置类型 对于我们的自定义类型来说 识别是不可能的

    在这里插入图片描述
    所以说我们这里就需要对于流插入和流提取运算符进行一个重载

    在这里插入图片描述
    而对于流来说 其中有输入流和输出流 也就是我们平时经常说的io流

    而对于流插入和流提取来说 它们对应的分别是 istream 和 ostream

    下面我们来展示下应该怎么重载流插入运算符

    	void operator << (ostream& out)
    	{
    		printf("<<");
    	}
    
    • 1
    • 2
    • 3
    • 4

    我们来试试看

    在这里插入图片描述

    可以完美运行

    但是如果我们这样子操作呢?

    在这里插入图片描述
    我们发现这样子竟然不能运行了

    这是为什么呢?

    还记不记得我们之前讲的this指针

    在这里插入图片描述
    在这里插入图片描述
    我们是不是发现 this指针在前面 流插入在后面啊

    那么我们试试这样子

    在这里插入图片描述

    但是这样子操作是不是有点奇怪啊

    那么 既然我们知道了为什么会出现这种错误 是不是就知道怎么修正了

    void operator << (ostream& out, MyClass& d)
    {
    	printf("<<");
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们将它按照我们需要的参数顺序定义到类的外面是不是就可以了

    我们来看看能不能运行

    在这里插入图片描述

    答案是可以的

    但是假设我们需要打印类内部的成员变量在外面可以打印嘛?

    显然是不行的

    因为成员变量都被外面私有了 外面是无法访问的

    那应该要怎么做呢?

    1.2 解决私有成员变量问题 友元

    我们这里想到的第一个处理方式就是将成员变量全部公有化

    但是这样就失去了我们写类的意义了

    这里试验证明下

    在这里插入图片描述
    那么除了公有化之外我们还有什么处理方式呢? 这里就引出了我们很重要的一个概念 友元

    我们先来看看怎么使用

    friend void operator << (ostream& out, MyClass& d);
    
    • 1

    在类的任意位置使用 friend 关键字 在类中的任意位置声明改函数为类的友元函数(可以访问类里面的私
    有)

    我们将全部类成员变量私有化然后声明友元函数试试看

    在这里插入图片描述
    我们发现可以完成

    当然友元函数最好不要过多声明 这样子会造成函数之间耦合度过高

    我们写程序尽量要符合 高内聚 低耦合

    1.3 总结

    上面我们解决了流插入操作符的重载 并且解决了类私有成员变量的问题

    同样的这里我们对于流提取再写一个重载函数

    代码表示如下

    istream& operator >> (istream& in, MyClass& d)
    {
    	in >> d._a;
    	return in;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述
    我们可以发现 流提取操作也能完美运行

    二. 再谈构造函数

    2.1 构造函数体赋值

    在创建对象的时候 编译器通过调用构造函数 给对象中的各个成员变量赋一个初始值

    class Date
    {
    public:
        Date(int year, int month, int day)
        {
            _year = year;
            _month = month;
            _day = day;
        }
    private:
        int _year;
        int _month;
        int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    比如说这样子

    在这里插入图片描述

    虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。

    简单来说什么意思呢

    就是说只有第一次的赋值叫做初始化 构造函数里面只是赋初值而已

    2.2 初始化列表

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

    我们直接来看看代码是怎么写的

        Date(int year, int month, int day)
            :_year(year)
            ,_month(month)
            ,_day(day)
        {}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    那么既然构造函数里面可以初始化 我们为什么还需要学列表初始化呢?

    事实上我们在上面已经说过了构造函数里面并不是初始化 而是赋初值
    也就是说如果我们在上面就已经初始化了
    具体是什么时候呢? 就是列表初始化的时候
    那么对于一些只能在初始化的时候赋值的变量 我们就只可以使用列表初始化

    有这么几种情况比如要用到列表初始化

    1. 常量初始化
    2. 引用初始化
    3. 自定义类型变量 (无构造函数时)
    class A
    {
    public:
    	A(int a, int b)
    		:_a(a)
    		,_b(b)
    	{}
    private:
    	int _a;
    	int _b;
    };
    
    class B
    {
    public:
    	B(int a, int ref)
    		:_aobj(a,ref)
    		,_ref(ref)
    		,_n(10)
    	{}
    private:
    	A _aobj;        // 没有默认构造函数
    	int& _ref;     // 引用
    	const int _n;  // const
    };
    void Test()
    {
    	B b(10,1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    就比如说我们上面的代码 就是列表初始化使用的场景

    在这里插入图片描述
    假如我们不使用列表初始化 便会发生下面这三个错误

    尽量使用初始化列表初始化

    因为初始化会优先使用列表初始化

    后面的构造函数其实就没必要使用了

    我们来看看上面的话对不对

    在这里插入图片描述

    成员变量在类中声明次序就初始化次序

    这句话怎么理解呢?

    我们直接来看代码

    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();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述
    这里是什么呢?

    因为a2是首先声明的 所以说这里先对a2进行初始化 而这个时候a1的值是随机值 所以说a2就变成了随机值

    而a1是后声明的 a的值传参为1 所以说这个时候a1的值就变成了1

    通过这个题目大家应该可以理解 成员变量在类中声明次序就初始化次序 这句话的含义了

    初始化列表可以与构造函数混用

    学习了初始化列表之后并不是说我们以后就一定要用初始化列表了

    可以将初始化列表和构造函数混着用 灵活变通 因地制宜

    三. explicit关键字

    在了解这个关键字之前我们首先要了解下隐式类型转换

    我们首先来看下面这段代码

    class Date
    {
    public:
    	Date(int year)
    		:_year(year)
    	{}
    	void Print()
    	{
    		cout << _year << endl;
    	}
    private:
    	int _year;
    };
    
    int main()
    {
    	Date d1(10);
    	Date d2 = 10;
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这里的d2是一个自定义类型是吧

    这里的10是一个整形是吧

    但是我们竟然可以将10赋值给d2

    这中间发生了什么呢?

    这里就出现了隐式类型转换
    在这里插入图片描述

    接下来我们再看以下代码

    class Date
    {
    public:
    	Date(int year)
    		:_year(year)
    	{}
    	void Print()
    	{
    		cout << _year << endl;
    	}
    private:
    	int _year;
    };
    
    int main()
    {
    	Date d1(10);
    	Date d2 = 10;
    	Date& d3 = 10;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述
    我们这里可以发现 引用类型为啥就不行了呢?

    这是因为啊 右边是常数 是只读类型的 但是引用是可读可写的所以说这是不是设涉及到权限的放大缩小

    问题了啊 这个时候我们就需要在前面加上一个const

    在这里插入图片描述

    这样子就可以啦

    之后我们来看看 explicit 关键字的语法

    我们将explict关键字放到Date构造函数前面 之后再运行下程序试试

    在这里插入图片描述

    我们可以发现 这里不能进行隐式类型转换了

    关于这个有什么作用 我们后面再讲 这里只要记住到这一点

    被explict修饰构造函数之后便无法进行隐式类型转换了

    四. Static关键字

    4.1 引入问题 统计创建拷贝对象次数

    我们都知道 拷贝函数和构造函数都会创建/拷贝对象

    所以说是不是我们往里面放进去一个计数器就可以了啊

    我们来看这个类

    class Sum
    {
    public:
    	Sum()
    	{
    		_i++;
    	}
    	Sum(const Sum& d)
    	{
    		_i++;
    	}
    	void Print()
    	{
    		cout << _i << endl;
    	}
    private:
    	static int _i;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    假设我们想知道它们拷贝 构造了多少次 是不是直接调用下Print就可以

    我们来看看
    在这里插入图片描述
    这样子我们就知道了 它调用了三次构造或者是拷贝

    这里有一点比较关键的就是static int _i 的初始化

    这里记住 一定要再类的下面 使用

    int Sum :: _a = 0;
    
    • 1

    这样子初始化

    4.2 static关键字

    当我们使用static关键字修饰一个参数的时候 实际上这个参数就被放到静态区去了

    所以说此时它的生命周期应该是整个程序运行时

    但是呢 它们的使用范围却不同

    这里总结下

    全局变量 : 生命周期是全局,可以随意访问调用
    局部变量 :生命周期是全局,在局部变量使用,局部变量外不能使用
    类中定义 :生命周期是全局,类只起到限定域的作用 (相当于类是一个命名域)

    再一句话总结下 什么意思呢?

    虽然它们的生命周期变成了整个程序运行时

    但是它们的使用空间却是没有变化的

    4.3 Static修饰函数

    当static修饰函数的时候 这个函数也会被放到静态区

    当放到静态区之后也就没有this指针

    没有this指针说明什么呢

    我们就不能访问内部成员变量了

    我们来看看代码

    在这里插入图片描述
    这里为什么还能访问_i呢 因为它是一个被static修饰的成员变量 实际上储存在静态区

    实际上我们只要换成_year就不能打印了

    在这里插入图片描述

    类似于这样子

    意区分两个问题:
     1、静态成员函数可以调用非静态成员函数吗?
     2、非静态成员函数可以调用静态成员函数吗?

    问题1:不可以。因为非静态成员函数的第一个形参默认为this指针,而静态成员函数中没有this指针,故静态成员函数不可调用非静态成员函数。
    问题2:可以。因为静态成员函数和非静态成员函数都在类中,在类中不受访问限定符限制

    这里就类似于权限的放大缩小问题

    五. 友元

    5.1 友元函数

    我们再上面的输入输出流中已经介绍过友元

    这里再详细介绍下

    友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

    前面我们解决私有成员变量问题就是用友元函数去解决

    中间的详细解释参考上面的代码

    这里给出友元函数的说明

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

    5.2 友元类

    友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中非公有成员。

    class A
    {
    	// 声明B是A的友元类
    	friend class B;
    public:
    	A(int n = 0)
    		:_n(n)
    	{}
    private:
    	int _n;
    };
    class B
    {
    public:
    	void Test(A& a)
    	{
    		// B类可以直接访问A类中的私有成员变量
    		cout << a._n << endl;
    	}
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    我们再A中声明了B是A的友元类 那么这个时候B就可以访问A的所有私有内容了

    友元类说明:
    1、友元关系是单向的,不具有交换性。
     例如上述代码中,B是A的友元,所以在B类中可以直接访问A类的私有成员变量,但是在A类中不能访问B类中的私有成员变量。
    2、友元关系不能传递。
     如果A是B的友元,B是C的友元,不能推出A是C的友元.

    5.3 内部类

    代码表示如下

    class A
    {
    private:
        static int k;
        int h;
    public:
        class B // B天生就是A的友元
        {
        public:
            void foo(const A& a)
            {
                cout << k << endl;//OK
                cout << a.h << endl;//OK
            }
        };
    };
    int A::k = 1;
    int main()
    {
        A::B b;
        b.foo(A());
        A a;
        cout << sizeof(a) << endl; // 4
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    这里我们形象的解释下

    A将B捧在手心里 所以说A是B的舔狗

    所以说 A对B来说 随叫随到 所以B能够随时访问A的所有私有内容

    但是对于B来说 A只是以一个舔狗 所以说 A不可以访问B的私有内容

    之后我们再来讲一下输出的类A大小 是4

    说明了什么? 说明类的大小与其中的内部类无关

    六. 再次理解封装

    前面的博客中已经说了 封装的本质是一种管理

    就拿疫情来说

    你觉得是放任自由 按照往常一样生活对于疫情防控更加有效

    还是每天核酸 封闭式管理对于疫情防控更加有效呢?

    七. 再理解对象

    大家可以看看这张图

    在这里插入图片描述

    类是抽象的 对象是实际存在的

    就拿我们的祖师爷 冯 诺依曼提出的计算机理论来说

    计算机要有 输入 输出设备 要有 储存器 运算器和控制器

    这就是一个类

    然后大家现在面前的计算机就是由这个类实例化出来的一个对象

    总结

    在这里插入图片描述

    本篇博客主要梳理了类和对象剩下的一些知识点
    好累啊! 类和对象的三篇内容一篇比一篇折磨人 好在终于要结束了 马上就要进入愉快的STL学习时间啦
    由于博主的水平错误再说难免 希望大佬发现后能够及时指正
    如果本篇文章有帮助到你 别忘了一键三连啊
    阿尼亚 哇酷哇酷!

  • 相关阅读:
    “唯品会VIP商品API:一键获取奢侈品详情,尊享品质生活!“
    Jtti:Apache服务的反向代理及负载均衡怎么配置
    齐岳定制:DBCO-PEG-Mesylate|二苯并环辛炔-聚乙二醇-甲磺酸酯
    Linux C应用编程-2-Makefile编写
    SpringBoot实战:轻松实现接口数据脱敏
    网站部署SSL证书是否会影响网站流量?
    概率论基础——拉格朗日乘数法
    Path-Ranking:KBQA中path生成、召回、粗排与精排
    项目实战(SpringJDBC框架事务管理)
    自动化测试:yaml结合ddt实现数据驱动!
  • 原文地址:https://blog.csdn.net/meihaoshy/article/details/127705947