• C++:类与对象(2)


    在完成类和对象(一)以及日期类Date的学习后,我们对类和对象已经有了初步了解,现在我们针对部分没有补充完整的知识点,进一步深入学习。

    目录

    1.初始化列表

    手动实现初始化列表: 

    不手动写初始化列表:

    易错:

     2.自定义类型和内置类型的隐式类型转化

    3.关键字explict

    4.多参数的传参、隐式转换

    5.static修饰的静态成员

    6.static修饰的成员函数

    7.友元

    7.1友元函数

    7.2友元类

    8.内部类(较少使用)

    9.匿名对象

    10.对象拷贝的优化

    11.对象的析构顺序


     

    1.初始化列表

    在类与对象1C++入门:类与对象(1)-CSDN博客中我们谈到,默认构造函数必须是不传参就能使用的函数:如果没有自己显式实现,会生成默认的构造函数;如果自己显式实现不传参的构造函数,就直接调用;唯独不能写需要显式传参的构造函数,如下:

    1. public:
    2. Stack(size_t capacity )
    3. {
    4. _array = (DataType*)malloc(sizeof(DataType) * capacity);
    5. if (NULL == _array)
    6. {
    7. perror("malloc申请空间失败!!!");
    8. return;
    9. }
    10. _capacity = capacity;
    11. _size = 0;
    12. }

    我们之前讲到,可以通过初始化列表的方式完成对这种需要传参的显式实现的构造函数的构造。

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

    手动实现初始化列表: 

    冒号开始,逗号分割:

    初始化列表位于函数体{}和函数头之间,用后面的括号表示所传的参数。

    内置类型后面括号中的参数给多少就是多少,自定义类型利用给的参数调动自己对应构造函数

    易错:

    初始化列表作用是用来传参,传完之后后面必须接上函数名的中括号,否则语法错误

     

         在有了初始化列表的概念之后,我们现在又可以将一个类中的变量的创建分为声明定义两部分了,就像之前的函数一样:在private下的那一部分我们可以理解为“声明”,在构造函数或初始化列表中就被当作“定义”


    初始化列表和函数中的内容也可以混用:

                                              

    哪些成员应该在初始化列表中初始化?

         先给结论,引用、const修饰、没有默认构造的自定义类型成员这三类必须要在初始化列表中定义。

    const修饰的成员只有一次初始化的机会(因为一旦被确认就不能再被更改),所以说定义的时候必须初始化,因此我们必须在初始化列表中定义这个const int _x

    引用&  同理(引用在第一次赋值之后就不能改变其代表的变量)。

       

    引用也是一样: 

                                       

    对象初始化才是定义的地方,也就是初始化列表里

    而在class的private下写的那个算是变量的声明


    不手动写初始化列表:

    不写初始化列表,编译器也会自动生成,并且所有成员都会走一遍该默认生成的初始化列表。

    自定义类型成员会走默认构造(编译器悄悄帮你做了,这也是为什么说cpp难学,编译器悄悄做了太多事)

    初始化列表和默认构造一起,形成一个逻辑闭环。不写的时候自动生成默认构造,默认构造在函数名和函数体之间又自动放了初始化列表,每一个成员都会先走一遍,对于自定义类型,又会调用他的构造函数,我们显式写的是Stack _popst(10),他默认生成的初始化列表调用的是Stack _popst(),因为找不到匹配的,所以就会报错

           对于内置类型,如果在初始化列表中写了,就不会再去管private下声明处写的缺省参数,如果没在初始化列表中写, 就会使用这个缺省参数。可通过调试观察。

    如下图:既有缺省参数,又显式写了初始化列表,则以初始化列表为准(只有初始化列表中找不到时,才去缺省参数处寻找,缺省参数处的数值是给初始化列表用的)

                                

    因此,我们可以认为声明处的缺省值其实是给初始化列表用的

    此处的“=”只是一个形式,其实际表示意义就是将“=”后面的值作为参数传入初始化列表。

    例如  Stack _pushst=10;的意思就是将10作为_pushst中的需要参数的构造函数的参数。

    所有的构造函数都满足以下规则: 

                            

    在实践中,我们多希望使用初始化列表:

    初始化列表中的参数可以是常数,也可以是如malloc等这类表达式

                            

    观察以下代码,思考_x会在哪里被初始化?

                                         

    _x依然会在初始化列表中被初始化,如果没有显式写

    1. //
    2. : _x(20),
    3. //

    就会使用缺省值10。

                          

    1. class A
    2. {
    3. public:
    4.    A(int a)
    5.       :_a1(a)
    6.       ,_a2(_a1)
    7.   {}
    8.    
    9.    void Print() {
    10.        cout<<_a1<<" "<<_a2<
    11.   }
    12. private:
    13.    int _a2;
    14.    int _a1;
    15. };
    16. int main() {
    17.    A aa(1);
    18.    aa.Print();
    19. }

    (如果初始化列表中不写_x,又没有缺省值,就会报错)

    易错:

    成员变量 在类中 声明次序 就是其在初始化列表中的 初始化顺序 ,与其在初始化列表中的先后
    次序无关

     例题:

    1. class A
    2. {
    3. public:
    4.    A(int a)
    5.       :_a1(a)
    6.       ,_a2(_a1)
    7.   {}
    8.    
    9.    void Print() {
    10.        cout<<_a1<<" "<<_a2<
    11.   }
    12. private:
    13.    int _a2;
    14.    int _a1;
    15. };
    16. int main() {
    17.    A aa(1);
    18.    aa.Print();
    19. }

    实例化aa后,aa中的_a2 和_a1分别是什么值?

    由于初始化列表中的真实初始化顺序是其在类中的声明顺序,所以先声明_a2,但是此时的_a1中是随机值,所以_a2是随机值

    我们再通过一个初始化列表来理解:

                     

    对于类成员_a,先在private修饰下作出声明。若是构造函数(必须有参,否则报错),则先走初始化列表,由于_a是内置类型int,所以: _a(a)语句将a赋值给_a(内置类型直接赋值);若是拷贝构造,则我们使用 实参的别名aa来找到其对应的_a,赋值给this对应的_a.


     2.自定义类型和内置类型的隐式类型转化

    之前在谈论权限问题时,我们提到了如下隐式转化:

    1. int a = 10;
    2. double b = a;

    a在赋值给b的过程中,先将a拷贝,对a的拷贝进行构造,构造成double类型后赋值给b。

    那么类和类、类和常量之间能不能有如此的转换呢?

                                

    先将3作为参数,(将3作为参数传参)构造 A类,然后将这个类通过拷贝构造赋值给aa3

    隐式转换遇到引用时依然有权限问题:

    1. class A {
    2. private:
    3. int _a;
    4. public:
    5. A(int a)
    6. :_a(a)
    7. {
    8. cout << "A(int a)" << endl;
    9. }
    10. A(const A& a)
    11. :_a(a._a)
    12. {
    13. cout << "A(const A& a)" << endl;
    14. }
    15. };

                                

    xx引用的是类型转换中用4构造的临时对象。但是临时对象有常性,所以必须用const修饰

                                              

    下图的raa同理:

    注意:临时对象建立在当前函数栈帧,不要被名字中的“临时”二字迷惑,此处xx引用的和raa引用的临时变量的生命周期都和main函数一致。

    3.关键字explict

    不希望隐式转化发生,就加关键字:explicit

    记住一个点即可:explicit函数是用于修饰构造函数的 

    4.多参数的传参、隐式转换

    若我们将上文中的类A的构造函数修改为需要两个参数:

    1. class A {
    2. private:
    3. int _a;
    4. int _b;
    5. public:
    6. A(int a,int b)
    7. :_a(a),
    8. _b(b)
    9. {
    10. cout << "A(int a)" << endl;
    11. }
    12. A(const A& a)
    13. :_a(a._a),
    14. _b(a._b)
    15. {
    16. cout << "A(const A& a)" << endl;
    17. }
    18. };

    此时,如何在声明处的缺省参数处传参或者使用隐式转换中的传参呢?

                                       

    用花括号括起来即可。

    问为什么要加const的可以打死了。

    缺省值传参时也一样,使用中括号: 

                                            

    我们也可以通过类似的方法进行传参: 

      将push的对象改为一个双参的A类:

                                    

                                              

    如果我们实现了一个单参的构造和一个双参的构造,也可以这样使用:

                                                 


    5.static修饰的静态成员

    声明为 static 的类成员 称为 类的静态成员 ,用 static 修饰的 成员变量 ,称之为 静态成员变量 ;用 static 修饰 成员函数 ,称之为 静态成员函数 静态成员变量一定要在类外进行初始化
    类中不会存储静态成员变量
    静态成员不能给缺省值,因为缺省值是给初始化列表的,但是static修饰过的成员在静态区,不在对象中,因此他不会走初始化列表

                       

           
    必须在外部定义,并且必须既声明又定义,否则报错。
    每当一个静态成员变量被定义后,他就属于该类型  类 的所有对象
    当静态变量的属性是public时,也有两种方法来使用该静态变量
                               
                     
    静态成员变量可以用于统计该类型的对象一共创建了多少个,比如在构造和拷贝构造中++,在析构中--
    但是如果我们将_scount关闭public属性,改变其为private属性,
    上图中的两种访问方法就失效了。需要用一个内置的函数去执行。

    6.static修饰的成员函数

    该成员函数没有this指针,因此static修饰的函数只能访问静态成员

                         
    静态的函数不能访问非静态、类里的非静态成员( 因为没有this指针!);
    但是非静态的函数可以访问静态的成员;

    所以我们希望访问static和private一起修饰的成员变量时,自己实现一个函数即可。


    利用一个题目来展示static修饰成员函数的意义:

    求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)

    1. class Sum{
    2. public:
    3. Sum(){
    4. _ret+=_i;
    5. ++_i;
    6. }
    7. static int getret(){
    8. return Sum::_ret;
    9. }
    10. private:
    11. static int _i;
    12. static int _ret;
    13. };
    14. int Sum :: _i=1;
    15. int Sum :: _ret=0;
    16. class Solution {
    17. public:
    18. int Sum_Solution(int n) {
    19. Sum arr[n];
    20. return Sum::getret();
    21. }
    22. };

    思路如下:

            传统方法不能使用,我们利用构造函数中++,析构函数中--的方法,来操作n个数相加。

    但是作为结果的_ret和作为每一步加数的_i  ,   我们应当使用static修饰他们。为了调用该类共有的函数getret,我们也需要使用static修饰getret,否则需要实例化一个特定的对象来调用getret。

    使用static更符合当下情景。     

    由此我们可以看出,不论static修饰的成员函数还是成员变量,他都让该变量或函数成为一个公用的模块


    7.友元

    7.1友元函数

    友元函数 可访问类的私有和保护成员,但 不是类的成员函数
    友元函数 不能用 const 修饰
    友元函数 可以在类定义的任何地方声明, 不受类访问限定符限制
    一个函数可以是多个类的友元函数
    友元函数的调用与普通函数的调用原理相同
    友元函数不能const修饰。由于 友元函数不属于任何类的成员函数,它们无法被 const 修饰。
    const修饰函数时修饰的是形参中的隐藏参数this,友元函数作为外部函数,参数中不具备隐藏的this指针,自然也不可能被后置const修饰
    语法很简单,在类内部任意位置声明该函数并加一个friend即可。
    1. friend ostream& operator<<(ostream& _cout, const Date& d);
    2. //之前实现的流提取运算符的重载

    7.2友元类

    声明友元可以声明在任意位置
    一般建议在我们正在实现的类的最上面声明 友元类
                     
    意思就是,time已经声明了,Data是Time的友元, 所以Data中可以访问Time中的元素(包括私有)但是Time不可以访问Data中的元素,因为没有说明Data是否将Time当作友元类

     少用友元,其在一定程度上打破了类的封装性。


    8.内部类(较少使用)

         

    概念: 如果一个类定义在另一个类的内部,这个内部类就叫做内部类 。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越 的访问权限。
    注意: 内部类就是外部类的友元类 ,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

    内部类仅仅受到类域的限制

    1.假如A类中有一个B类,如果我们测试A类的大小,则B类的大小不会被计算在内。

    2.若B类被分为A类的private成员变量,则不能通过A使用B,如下图:

                                                            

    3.B天生就是A的友元

    4.Java中的内部类使用频率高于C++,C++并不经常使用内部类。


    内部类可以优化刚刚牛客网的题目。 

                                

    我们将_i和_ret放在了solution中,又因为内部类是外部类的私有,所以Sum可以自由访问_i以及 _ret


    9.匿名对象

    顾名思义,定义时不给名字的对象。

    有名对象的生命周期在当前作用域,

    匿名对象的生命周期只在当前这一行,即用即销毁,调完构造马上调析构。

    匿名对象的特点不用取名字

    10.对象拷贝的优化
     

        编译器会根据语法,自动对我们写出的代码进行优化,老旧版本之间、release和debug之间都不相同。

         我们通过在构造函数和拷贝构造函数中加上打印函数名来观察。

    1. void f1(A aa)
    2. {}

    如以上函数,当f1是一个传值传参函数时,形参是实参的拷贝,所以又会调动对aa1的拷贝,来变成aa,最后销毁aa、销毁aa1 

    此时似乎还观察不出优化。

    我们将传值传参改为引用传参。

    如果使用 匿名对象传参 或者 拷贝构造隐式转换 就会有报错:

                                                        

     匿名对象具有常性,拷贝构造出现的隐式转化也具有常性,所以需要在形参处加一个const修饰

                                       

    我们观察结果:

    因此,引用传参确实可以有效减少拷贝次数。不过此时依然没有发生有效的优化。


    我们再将f1的引用传参改回传值传参。

           在上一篇中我们提到,构造加拷贝构造会被编译器优化成直接构造。

    如下三种:

                                               

    按理来说,以上三种都应该是先 普通构造、再拷贝构造,但是这样会浪费中间那一层的拷贝。因此,编译器此时只会执行一次直接构造。(vs2022可能观察不出来,其优化开的很高)

    再如下: 

                                

    按理来说应该是构造(aa)+拷贝构造(aa传值返回需要)+拷贝构造(返回值给ret初始化)

    但是编译器直接将两次拷贝构造优化为一次拷贝构造。

                                   

    会在f2结束前,销毁f2对应的函数栈帧之前,直接对ret拷贝构造,省略中间的临时变量。

    同理,aa销毁之前直接调用了拷贝构造,在f2结束之前构造ret2

    release版本下或者vs2022版本中的优化更大,会存在跨行构造,甚至有的变量直接不在新的栈帧中开辟,直接对调用该函数的部分进行赋值。


    11.对象的析构顺序

    函数栈帧中,每一个空间都是类似栈的做法,先开的空间后销毁,后开的空间先销毁。

    有了这一点能够更好的理解对象的拷贝优化部分。

                                          

  • 相关阅读:
    速度轴模拟量控制FB(博途SCL+三菱ST代码)
    Linux: IO中断驱动开发教程
    IBM Spectrum Symphony 获享高度可扩展、高吞吐量、低延迟的工作负载管理
    ResponseBodyAdvice接口使用导致的报错及解决
    前端食堂技术周刊第 50 期:TypeScript 4.8、Deno 1.25、Terminal Gif Maker、CSS :has() 伪类
    分布式基础
    【毕业设计】58-基于51单片机的智能语音密码锁设计(原理工程+PCB工程+仿真工程+源代码+答辩论文+实物图)
    HDLbits 刷题 -- Kmap3
    你不知道的JS 之 this& this指向
    联想G50笔记本直接使用F键功能(F1~F12)需要在BIOS设置关闭热键功能可以这样操作!
  • 原文地址:https://blog.csdn.net/2301_79501467/article/details/137917184