• 类和对象(末)


    目录

    ·初始化列表:

     explicit

    static修饰的静态成员变量

     友元:

     友元类:

    内部类

    匿名对象

     拷贝对象时的一些优化:


    ·初始化列表

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

    如代码所示:

    代码中的这部分就是初始化列表,初始化列表也可以完成构造函数的任务。

     并且构造函数函数体内的初始化列表可以混合着一起使用。

    我们写一个栈的构造函数:

    1. Stack(int capacity = 4)
    2. : _a((int*)malloc(sizeof(int)*capacity))
    3. , _top(0)
    4. , _capacity(capacity)
    5. {
    6. if(_a == nullptr)
    7. {
    8. perror("malloc fail");
    9. exit(-1);
    10. }
    11. }

    同样,我们也可以这样写:

    我们可以把_a的初始化放到函数体内:

    1. Stack(int capacity = 4)
    2. : _top(0)
    3. , _capacity(capacity)
    4. {
    5. _a = (int*)malloc(sizeof(int)*capacity);
    6. if(_a == nullptr)
    7. {
    8. perror("malloc fail");
    9. exit(-1);
    10. }
    11. }

    当然,初始化列表并不能够解决全部的初始化问题。

    假如我们想要把我们动态申请的_a的这部分空间全部置为空,我们的初始化列表就完成不了。

    例如:

    1. Stack(int capacity = 4)
    2. : _top(0)
    3. , _capacity(capacity)
    4. {
    5. _a = (int*)malloc(sizeof(int)*capacity);
    6. if(_a == nullptr)
    7. {
    8. perror("malloc fail");
    9. exit(-1);
    10. }
    11. memset(_a, 0, sizeof(int)*capacity);
    12. }

    总结:初始化列表是在构造函数体之外写的一种初始化方法,但是初始化列表并不能够解决全部的初始化问题,所以我们需要和构造函数的函数体内容一起配合使用。

     问题:初始化列表的一个变量可以被重复初始化吗?

    答:不能,例如:

     _top被初始化了两次。

    这里就会报错。

    问题:既然构造函数体能够解决全部的问题,而初始化列表只能解决一部分问题,那初始化还起到什么作用呢?什么场景下需要使用初始化列表呢?

    答:例如const修饰的成员变量

    1. class B
    2. {
    3. public:
    4. B()
    5. {
    6. _n = 10;
    7. }
    8. private:
    9. const int _n;
    10. };
    11. int main()
    12. {
    13. B b;
    14. return 0;
    15. }

    对于这样的一串代码,我们进行编译:

     报错的原因是这样:

    1. int main()
    2. {
    3. const int a;
    4. return 0;
    5. }

    例如这样的一串代码,我们进行编译:

     报错的原因是const修饰的变量是静态变量是不可以修改的,所以我们必须在其定义时进行赋值:

    同理:

     

     我们的_n定义发生在我们创建对象的时候

    _n的定义再细节一些:定义是在构造函数体内还是初始化列表呢? 

    答:定义是在初始化列表中进行的,我们的初始化列表并没有写这个静态变量的初始化,所以就会报错。

    如何解决这个问题呢?

    答:我们可以在初始化列表中进行初始化:

     总结:所有的成员变量进行初始化时都要经过初始化列表,并且成员变量的定义发生在初始化列表中。

    我们再写一个内置类型的成员变量,不显示写初始化列表,看初始化列表对内置类型如何处理:

    例如:

    _m内置类型被初始化成了随机值。

    得出结论:对于内置类型,假如我们不显示写初始化列表时,初始化列表会把内置类型初始化成为随机值。 

    假如我们在声明位置给_m一个缺省值,_m的值会变成什么?

    答:

     所以,缺省值对于初始化列表也是同样适用的。

    但是,假如我们对内置类型进行显示初始化列表呢?

    答:

     假如我们显示初始化列表了,那我们给的缺省值就不起任何作用了。

    总结:如果没有在初始化列表中显示初始化,对于内置类型,假如有缺省值就用缺省值,没有缺省值就用随机值。

     那对于自定义类型呢?

    例:

     我们在类B中的成员变量中添加一个内置类型的A,A是这样的一个类:

    1. class A
    2. {
    3. public:
    4. A(int a)
    5. :_a(a)
    6. {}
    7. private:
    8. int _a;
    9. };

    我们可以发现,A没有默认构造函数。

    默认构造函数的定义:我们不写参数也可以调用的函数叫做默认构造函数,例如无参,全缺省,或者不写编译器默认给的。

    我们进行编译:

     会报错。

    但是假如我们在类A中写一个默认构造函数呢?

     我们进行调试:

     成功调用了默认构造函数。

    总结:假如我们没有在初始化列表中显示初始化时,对于自定义类型,假如我们的自定义类型有默认构造函数,调用默认构造函数,假如没有默认构造函数,就报错。

     那我们该如何显示的在初始化列表中初始化自定义类型呢?

    例如:

     和内置类型的初始化是相同的

     这里就相当于显示调用类A的构造函数。

    我们写一个之前熟知的用两个栈来实现队列:

    1. class MyQueue {
    2. public:
    3. MyQueue()
    4. {}
    5. void push(int x)
    6. {
    7. _pushST.Push(x);
    8. }
    9. private:
    10. Stack _pushST;
    11. Stack _popST;
    12. size_t _size = 0;
    13. };

    我们的构造函数和初始化列表就这样写:

     因为我们没有在初始化列表中显示初始化,所以对于自定义类型,默认调用其构造函数,假如没有构造函数的话就报错。对于内置类型,有缺省值的用缺省值,没有缺省值的用随机值。

    假如我们删除栈的默认构造函数:

    例如:

    我们进行调用:

     

     这个时候就会显示没有默认构造函数可用。

    尽管我们没有创建对象,但是只要我们写了初始化列表,我们就会检查对应的自定义类型是否会有构造函数,没有的话,就会报错。

    所以什么情况下需要使用初始化列表:当我们是自定义类型,并且我们没有写默认构造函数时,这时候,我们需要自己写初始化列表。

    我们先恢复栈的默认构造:

     定义一个队列的对象:

    我们看是否可以实现初始化:

     

     可以完成初始化。

    假如我们要求MyQueue需要输入一个参数:

     我们输入一个参数看是否能完成初始化:

     我们进行调试:

     我们发现,并没有完成我们期望的初始化。原因是什么?

    答:无论我们显示写不写初始化列表,我们的全部成员变量都会走初始化列表,对于自定义类型的化,调用其默认构造,这里的默认构造就是栈的默认构造,我们的栈的默认构造的capacity,对于内置类型我们有缺省值就用缺省值,没有缺省值的话,就用默认构造。

    那我们该如何显示初始化列表呢?

    答: 

     总结:有哪些成员必须要显示初始化?

    答:1:const修饰的静态成员变量

    2:引用成员变量

    3:没有写默认构造函数的自定义类型

    注意:尽量去使用初始化列表去初始化,因为无论你是否使用初始化列表,对于自定义类型,一定会使用初始化列表去初始化。

    我们来写一道题目:

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

    程序输出的结果是多少?

    答:1   随机值

     原因如下:我们的初始化列表的初始化顺序是根据成员变量的书写顺序进行的,所以我们先初始化的是_a2,_a2的结果是_a1,但是_a1这时候还没有被初始化,_a1这时候的结果是随机值。

    所以_a2的结果就是随机值。

    我们再初始化_a1,_a1是_a的拷贝,所以_a1的结果为1.

    我们打印的顺序是先打印_a1,再打印_a2,所以我们的结果就是1和随机值。

    判断题

    一个类如果不提供默认构造的话就会报错?

    答:错误,例如:

    1. class A
    2. {
    3. public:
    4. A(int a)
    5. /*:_a1(a)
    6. , _a2(_a1)*/
    7. {}
    8. void Print(){
    9. cout << _a1 << " " << _a2 << endl;
    10. }
    11. private:
    12. int _a2;
    13. int _a1;
    14. };

    我们进行编译:

     原因:我们并通过该类来创建对象

    当我们使用该类来创建对象,并且不输入参数时,就会报错:

     这个时候进行运行就会报错。

    当我们注释掉自己写的构造函数时,就不会报错:

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

    因为默认构造函数分为三种:

    1:全缺省

    2:无参

    3:我们不写,编译器默认的。

    这里我们不写,就会执行编译器默认的构造函数。

    我们再写一个例子来加深印象:

    1. class A
    2. {
    3. public:
    4. A(int a)
    5. :_a1(a)
    6. , _a2(_a1)
    7. {}
    8. void Print(){
    9. cout << _a1 << " " << _a2 << endl;
    10. }
    11. private:
    12. int _a2;
    13. int _a1;
    14. };
    15. class B
    16. {
    17. private:
    18. A _aa;
    19. };
    20. int main()
    21. {
    22. B bb;
    23. return 0;
    24. }

    我们这里没有创建对象,就不会报错

    但是我们假如创建了对象就会报错:

     

     

     原因是什么,我们进行分析:

    答:首先,我们先创建一个对象bb,对象bb中有一个成员变量_aa,我们没有显示写初始化,对于自定义类型会调用其默认构造:

     而这里又没有默认构造,就会报错。

    1. class A
    2. {
    3. public:
    4. A(int a)
    5. :_a1(a)
    6. , _a2(_a1)
    7. {}
    8. void Print(){
    9. cout << _a1 << " " << _a2 << endl;
    10. }
    11. private:
    12. int _a2;
    13. int _a1;
    14. };
    15. class B
    16. {
    17. public:
    18. B()
    19. {}
    20. private:
    21. A _aa;
    22. };
    23. int main()
    24. {
    25. /*B bb;*/
    26. return 0;
    27. }

    但是我们这里没有创建对象也会报错。

    原因是什么?

    答:原因是只要我们这里写了

    只要我们显示写了这部分,我们就会首先进行初始化列表(无论我们是否创建对象),对于自定义类型会调用其默认构造。

     但是我们并没有默认构造,所以就会报错。

    总结:情况1:没有显示写初始化列表,对于自定义类型,会调用其默认构造,假如没有默认构造,就会报错(创建过对象)

    情况2:只显示了初始化列表的框架,对于自定义类型,会调用其默认构造,假如没有默认构造,就会报错(无论是否创建对象)

    注意:能用初始化列表就用初始化列表,因为初始化列表一定不会错。

    2:尽量给一个函数提供默认构造。

     explicit

    例:

     对于这个日期类,我们一般是这样创建对象的:

     但是现在我们c++98中又添加了一种写法:

     我们可以这样写。

    这种写法如何理解呢?

    答:我们可以类比整型和浮点型之间的赋值:

    例如:

     我们可以对这里这样理解:首先把浮点型强制类型转换为整型,赋给一个临时变量tmp,这个临时变量的类型是整型,然后再把该临时变量赋给d。

    所以,对于

    我们可以这样理解成这样的代码:

     

     这里的意思是我们首先把2022当作参数创建一个临时日期类对象tmp,然后再进行拷贝构造,把tmp拷贝给d2.

    所以

    这里相当于把构造和拷贝构造隐式类型转换成为了一个构造

    我们再举一个例子:

     

     为什么加上了const就没有错误提示符,而没有加const就有错误提示符呢?

    答:因为我们首先通过参数2022创建一个临时日期类对象,该日期类对象不可修改,具有常性,我们的引用的类型不对等,所以我们要加上const把类型也转换成具有常性的日期类对象来接收。

    我们用内置类型也可以解释:

     

     这类相对简便的写法单参数的对于单参数的构造函数是支持的。

     

     假如我们不想让这类隐式类型转换发生,我们可以加一个explicit

    这时候,我们的这种写法就会报错了。

    我们就不能再使用这种隐式类型转换了。

    是不是仅仅只有一个参数的构造函数才能使用这种隐式类型转换呢?

    答:并不是,例如,我们可以这样写:

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

    虽然我们有3个参数,但是因为我们的构造函数的三个参数有两个参数是缺省值,所以我们依然可以只输入一个参数完成构造。

    我们进行实验:

     

     没有报错,我们的假设成功。

    当然全缺省的也是可以的:

     

     

     也是可以运行成功的。

    如果是多参数呢?

    例如:

     我们这样写明显不可以。

    那这样呢?

     这样写也是不可以的。

    这样写可以吗?

     这样写是可以的。

    这两种写法对应的结果其实是一样的,只是过程不同。

     总结:c++98支持单参数的隐式类型转换,而c++11才支持了多参数的隐式类型转换

    隐式类型转换途中生成的临时变量是具有常性的。

    explicit修饰的构造函数不能使用隐式类型转换

    static修饰的静态成员变量

    面试题:实现一个类,计算类中一共创建了多少个对象。

    1. class A
    2. {
    3. public:
    4. A(int a = 0)
    5. :_a(a)
    6. {}
    7. private:
    8. int _a;
    9. };
    10. int main()
    11. {
    12. A aa1(1);
    13. A aa2 = 2;
    14. return 0;
    15. }

    统计以下A类型的对象创建了多少个。

    我们可以分析出一个结论:以A类型创建的对象不是构造出来的就是拷贝构造出来的。

    所以我们可以这样写:

    1. int N = 0;
    2. class A
    3. {
    4. public:
    5. A(int a = 0)
    6. :_a(a)
    7. {
    8. ++N;
    9. }
    10. A(const A&aa)
    11. :_a(aa._a)
    12. {
    13. ++N;
    14. }
    15. private:
    16. int _a;
    17. };
    18. int main()
    19. {
    20. A aa1(1);
    21. A aa2 = 2;
    22. A aa3 = aa1;
    23. cout << N << endl;
    24. return 0;
    25. }

    我们的思路是这样的:我们先创建一个全局变量N,当调用构造函数或者调用拷贝构造函数时,我们的N++,我们就可以检查出一个创建了多少个对象了。

    我们构造了2个,拷贝构造了1个,所以打印的结果应该为3

     说明我们的编译器经过了优化,假如我们的编译器不进行优化,

    这里就是一个构造和一个拷贝构造,那我们编译的结果就是4了。

     但是这里有一些问题需要注意以下:

    例如:我们多写一个函数:

     这个函数的参数是一个类对象。

     我们在main函数中进行调用:

    进行运行:

    我们进行运行:

     

     我们可以发现 ,当我们调用了F1函数,N的值就加了1,原因是什么呢?

    答:

     因为我们调用函数需要传递参数,我们需要传递的是一个类对象的参数,传递参数的过程本身就是一种拷贝,所以就会调用拷贝构造,调用拷贝构造的话,对应的N就加1.

    传值传参会调用拷贝构造,那传值返回会吗?

    答:会,例如:

    我们先写一个传值返回的函数:

    1. A F2()
    2. {
    3. A aa;
    4. return aa;
    5. }

    我们在main函数的函数体内对F2进行调用:

    我们进行编译:

     

     3,4我们已经解释过了,那这里的6的原因是什么呢?

    答:

     因为我们在函数体内创建了一个对象,会调用构造函数,然后再对对象进行返回,返回给返回值本身,这里本质上也发生了拷贝,所以会调用拷贝构造,所以这个函数既调用了构造函数又调用了拷贝构造,所以N+2.

    我们使用引用传参:

     这个时候,我们继续运行:

     

     我们可以发现,传引用传参相对于传值传参的作用在于减少了拷贝构造。

    假设我们传引用返回呢?

     但是本质上,这里传引用是错误的,因为函数调用完毕时,aa就会被释放,对于释放的空间我们再引用返回,返回的值就是被释放的空间。

    不过,我们这里强制的传引用返回:

     传引用返回也可以减少拷贝构造。

    总结:传引用传参和传引用返回都会减少拷贝构造函数的产生。

     但是,这里的全局变量N其实存在有一些问题的。

    首先,这里的全局变量N什么时候在什么地方都可以修改:

    例如:

     我们进行编译:

     我们总结以下影响变量的生命周期的都有哪些?

    答:1:局部变量在栈里面

    2:静态变量在静态区,既会影响生命周期又会影响链接属性

    链接属性指的是链接过程中是否会进入符号表。

    3:malloc申请的空间在堆上

    4:常量:存储在常量区,代码段。

    局部的静态变量和全局的静态变量的区别是什么?

    答:生命周期都是全局,局部的静态变量的作用域是局部,全局的静态变量的作用域是全局,局部的静态变量的生命周期是局部。

    我们可以在类里面设置一个静态变量:

     类里面的静态变量有什么特点:

    1:收到类域的限制

    2:生命周期是全局的。

    我们提出一个问题:类里面的静态变量存在于对象里吗?

     aa1和aa2都有_a,那么aa1和aa2有没有N?

    答:没有,因为对象aa1和aa2都是局部变量,局部变量在栈上面,而静态变量在静态区,两者肯定不能相互包含

    两者的关系是类对象共享一个N,我们对一个类对象中的N进行修改,所有对象中的N都发生了改变。

    这里的N可以给缺省值吗?

    答:不可以,因为这里的N只是一个声明,静态变量是在定义时进行初始化的,所以我们这里不可以给缺省值。

    我们在哪里对静态变量进行初始化呢?

    答:我们可以在类外面对类里面的静态变量进行初始化

     类里面的静态变量什么特点

    答:生命周期是全局的,因为类是不会销毁的。

    作用域受类域限制。

     我们先把类里面的静态变量设置为public的:

    对于公开的类的静态变量,我们有几种访问方法?

    答:两种,1:我们可以用域操作限定符来进行访问:

     2:我们也可以用对象来进行访问:

     这里的意思并不是说对象aa1中含有静态变量N,而是说aa1属于类A,类A中包含了静态变量N,然后我们可以对N进行访问。

    我们也可以用指针来访问N

    例如:

     当然对于空指针也是可以访问的:

     原因是只要表示我们这里的ptr的类型是A*,就可以对N进行访问。

    那假设我们的静态变量是私有的,我们怎么进行处理?

     答:我们可以用java的写法,在类里面写一个成员函数来取出类里面的成员变量。

     假设我们要访问N时,我们需要这样写:

     但是有对象的时候,我们可以顺便调用,但是没对象的时候,假如我们要调用函数的话,还需要额外创建一个对象,并且只要我们创建了一个对象,我们的N值就会++,怎么处理这种情况?

    答:这个时候,我们可以写一个静态成员函数。

     写一个静态成员函数就能解决我们的问题,因为当我们是静态成员函数,静态成员函数没有this指针,所以我们不需要创建对象也可以调用函数

    我们可以这样写:

     当然,即使我们有对象时,我们也可以用普通的方法:

     这种写法也是正确的。

    这种方法叫做突破类域的方法。

    静态成员函数能够访问其他的成员变量吗?

    答:不可以,例如:

     因为没有this指针,所以我们无法访问其他的成员变量。

    总结:在类里面我们设置静态变量,这个静态变量有以下特点:

    1:生命周期是全局的。

    2:受类域限制。

    3:不在对象里,而被所有对象所共享

    我们也可以设置静态成员函数来取出受private限制的静态变量

    特点

    1:静态成员函数没有this指针,所以不需要对象也可以调用

    2:有对象情况下依然可以调用。

    3:因为没有this指针,只能访问静态成员变量,不能访问其他的变量。

    我们做一道算法题目:

     正常,我们用循环就可以求出n的阶乘,但是这里并没有求出,我们该怎么处理呢?

    答:我们可以采用刚刚的思路,写一个类,写一个静态成员变量,调用n次构造函数即可。

    1. class Sum {
    2. public:
    3. Sum() {
    4. _ret += _i;
    5. ++_i;
    6. }
    7. static int GetRet() {
    8. return _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的阶乘,我们的思想是创建一个类,在类里面写一个构造函数,在构造函数中写两个静态变量,用这两个静态变量来实现阶乘,既然用了类的静态变量,我们就需要写一个静态成员函数来取出静态变量,然后我们进行输出,我们可以用变长数组的方法,这里的变长数组的意思就是创建n个对象,每创建一个对象,我们就求出对应的阶乘,然后我们求出的就是n的阶乘。

    问题:

    为什么这里需要使用类的静态变量,为什么不能用类的普通变量

    答:因为类的静态变量具有共享的性质,也就是任何一个对象使类的静态变量改变时,这个改变对于所有其他对象的静态变量都适用

     类的静态变量怎么使用?

    答:静态变量的声明放到类里面,静态变量的定义放到类外面,注意静态变量的定义不需要再加static,但是需要我们的域操作限定符。然后我们在类里面写一个静态成员函数,作用是取出我们要求的静态变量。

    题目:

    要求我们创建的对象只能在栈上面:

    答:我们先写一个简单的类。

    1. class A
    2. {
    3. public:
    4. A(int a = 0)
    5. :_a(a)
    6. {}
    7. private:
    8. int _a;
    9. };

    要求我们用这个类创建的对象只能在栈上面。

    我们思考:我们创建的对象都能在什么地方?

     大概分为这三部分,假如我们要求只能在栈上创建空间,我们就需要限制静态区和堆创建的对象。

    我们可以从他们三个的共同点做起:他们三个都需要调用构造函数。

    我们可以把构造函数设为私有的:

     那假设我们要创建对象,我们只有一种方法了,那就是在类中创建对象,也就是设置一个成员函数,在该成员函数中创建对象,然后然后该对象即可。

     现在,假如我们要创建对象,我们需要这样写:

     但是我们发现了一处错误:那就是假如我们需要调用GetObj函数时,我们需要创建一个对象才可以调用,但是我们又只有调用函数才能创建对象,所以就产生了矛盾。

    这时候,我们想起了static,静态成员函数没有this指针,调用的时候不需要对象。

     这个时候,我们就不需要创建对象既可以调用函数了:

     这个时候,我们就实现了只能在栈上创建对象:

    因为假如我们要创建对象,只能调用静态成员函数,通过接收静态成员函数的返回值来创建对象,但是我们的静态成员函数内部是创建的对象是在栈上面的,所以我们保证了创建的对象都在栈上面。

    总结:我们创建的对象可以在栈,堆,静态区。

    我们了解了一种题型:限制创建的对象的位置。

    我们的大体思路分为以下即可步骤

    1:限制构造函数的使用

    2:创建成员函数来调用构造函数

    3:使用静态成员函数,不需要创建对象

    4:实现限制创建对象的位置。

     友元:

     友元类:

    1. class Date
    2. {
    3. public:
    4. Date(int year, int month ,int day)
    5. :_year(year)
    6. , _month(month)
    7. , _day(day)
    8. {}
    9. void SetTimeOfDate(int hour, int minute, int second)
    10. {
    11. _t._hour = hour;
    12. _t._minute = minute;
    13. _t._second = second;
    14. }
    15. private:
    16. int _year;
    17. int _month;
    18. int _day;
    19. Time _t;
    20. };
    21. class Time
    22. {
    23. friend class Date;
    24. public:
    25. Time(int hour = 0, int minute = 0, int second = 0)
    26. :_hour(hour)
    27. , _minute(minute)
    28. , _second(second)
    29. {}
    30. private:
    31. int _hour;
    32. int _minute;
    33. int _second;
    34. };

    如代码所示:Time类就是Date类的友元类。

    假如一个函数是一个类的友元函数,那么这个函数就可以访问这个类里面的所有成员变量,友元类的所有成员函数都可以是另一个类的友元函数,也就是说Date类的所有成员函数都可以访问Time类的所有成员变量。

    例如这里:

     友元函数是单向的,不具有交换性:

    我们的Date是Time的友元类

    所以

     

     所以,Date的成员函数可以访问Time的成员变量,而Time的成员函数不能访问Date的成员变量。

    内部类

     我们举一个例子:

    1. class A
    2. {
    3. private:
    4. int _a;
    5. public:
    6. class B
    7. {
    8. int _b;
    9. };
    10. };
    11. int main()
    12. {
    13. cout << sizeof(A) << endl;
    14. return 0;
    15. }

     打印的结果是4还是8?

     原因如下:我们的类A中只有一个成员变量,是一个整型,占4个字节。

    而我们的类B虽然是类A的内部类,但是类B和类A本质上没有所属关系。

    那么我们创建一个类A对象中有没有类B的成员?

    答:没有,例如:

     对象aa中并没有类B的成员变量。

    这两个类就相当于两个独立的类。

    B类受A的类域和访问限定符的限制。

    例如:

     会报错

     我们要创建B类对象需要这样创建:

     内部类是外部类的友元函数:

    例如: 

    内部类可以访问外部类的成员变量。

     

     而外部类不能访问内部类的成员函数。

    内部类可以直接访问外部类的静态成员变量并且不需要创建对象。

    例如:

     

     原因是静态成员变量可以是类所共享的,可以突破类域。因为类B是类A的友元类,所以B可以直接访问类A的静态成员变量。

    我们之前写的那道面试题,我们可以用内部类的方式解决:

    我们可以把Sum类写成Solution的内部类

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

    但是我们的外部类是无法访问内部类的静态成员变量的。

    所以我们可以把静态成员变量写道外面的类里面。

    因为内部类是外部类的友元函数,所以内部类可以访问这些静态成员变量。

     

     然后我们把类的初始化的域作用限定符改以下即可。

    总结:内部类

    1:内部类是外部类的友元函数

    2:外部类无法访问内部类的成员变量

    3:外部类所占空间的大小不包含内部类。

    4:内部类可以直接访问外部类的静态成员变量,无需船舰对象。 

    匿名对象

    我们先写一个类

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

    对于这个类,创建对象的方法有哪些?

    第一个就是普通的构造,第二个是隐式类型转换,本质上是构造和拷贝构造的集合。

     我们可以写两个匿名对象:

     匿名对象只存在于创建它的那一行。

    而有名对象的生命周期在所在的局部域,这里就在main函数中。

    例如,我们进行调试:

    接下俩,我们来创建第一个匿名对象。

     我们可以发现,刚过了创建匿名对象这一行,匿名对象的构造和析构函数就都进行完毕了,所以匿名对象的生命周期在创建它的那一行。

    匿名对象在一些情况下是有意义的:

    例如:

    1. class Solution{
    2. public:
    3. int Sum_Solution(int n){
    4. return n;
    5. }
    6. };
    7. int main()
    8. {
    9. Solution so;
    10. so.Sum_Solution(10);
    11. return 0;
    12. }

    对于这个类,我们本来的目的只是为了调用函数,返回n值,但是假如我们需要调用函数的话,还需要创建一个又名对象。

    这个时候,匿名对象就派上了用场:

     我们可以这样写:我们不需要对对象起名字了,并且1行就解决了我们的问题。

    我们再举一个例子:

    1. class A
    2. {
    3. public:
    4. A(int a=1)
    5. {}
    6. private:
    7. int _a = 0;
    8. };
    9. A F()
    10. {
    11. A ret(10);
    12. return ret;
    13. }

    假如我们要传对象返回时,我们需要创建一个对象,然后返回。

    有了匿名对象,我们可以这样写:

    这样就方便多了。

    总结:匿名对象的特性

    1:写法:类名(+参数)

    2:生命周期:创建对象的那一行

    3:匿名对象用于调用类中函数和传值返回类中。

     拷贝对象时的一些优化:

    例如,我们写一个类

    1. class A
    2. {
    3. public:
    4. A(int a = 0)
    5. :_a(a)
    6. {
    7. cout << "A(int a)" << endl;
    8. }
    9. A(const A&aa)
    10. :_a(aa._a)
    11. {
    12. cout << "A(const A& aa" << endl;
    13. }
    14. A&operator=(const A& aa)
    15. {
    16. cout << "A& operator=(const A& aa)" << endl;
    17. if (this != &aa)
    18. {
    19. _a = aa._a;
    20. }
    21. return *this;
    22. }
    23. ~A()
    24. {
    25. cout << "~A()" << endl;
    26. }
    27. private:
    28. int _a;
    29. };

    我们这样创建参数就是进行优化:

    1. int main()
    2. {
    3. A aa1 = 1;
    4. return 0;
    5. }

    我们知道,这样写的本质是构造加上拷贝构造,假如编译器进行了优化,那我们就只进行构造:

     我们没有进行拷贝构造,编译器执行了优化。

    传参也可以优化

    正常情况下的传参:

    1. void f1(A aa)
    2. {}
    3. int main()
    4. {
    5. /*A aa1 = 1;
    6. return 0;*/
    7. A aa1;
    8. f1(aa1);
    9. return 0;
    10. }

    我们进行运行的结果应该是一个构造加上一个拷贝构造。

     如图所示。

    我们可以这样写来进行优化:

     这时候,我们进行编译:

    我们只调用了一个构造函数。

    假如我们在这里传一个引用呢?

     然后我们进行调用:

     我们进行编译:

     和上次的结果相同,但是

    注意:这个写法在有些编译器下是不支持的,因为有些编译器会把匿名对象看作常性,我们不能用引用来接收常数,所以会报错。

     最后一个优化:

    1. A f2()
    2. {
    3. A aa;
    4. return aa;
    5. }
    6. int main()
    7. {
    8. f2();
    9. }

    我们进行编译:正常的情况下应该是构造加上拷贝构造。

     这样不可以优化,但是这种写法呢?

    1. A f2()
    2. {
    3. A aa;
    4. return aa;
    5. }
    6. int main()
    7. {
    8. A ret=f2();
    9. }

    这种写法实际上经历了三次构造

    1:创建对象aa的构造

    2:返回aa时会调用拷贝构造

    3:A ret=f2()这里也会调用拷贝构造。

    所以这里应该是构造 拷贝构造 拷贝构造。

    我们进行编译:

    我们发现,编译器优化掉了一个拷贝构造函数。

    那我们看这样写可以优化吗?

    1. A f2()
    2. {
    3. A aa;
    4. return aa;
    5. }
    6. int main()
    7. {
    8. A ret;
    9. ret = f2();
    10. }

    我们进行分析:

    首先,我们先创建一个对象ret,调用构造函数。

    然后调用f2函数,先创建一个对象aa,调用构造函数

    然后返回aa,调用拷贝构造,然后释放掉aa。

    然后调用赋值重载:

     可以发现,这种写法不能优化。

    假如我们不用对对象的数据进行修改时,我们可以这样写:

    1. A f3()
    2. {
    3. return A(10);
    4. }
    5. int main()
    6. {
    7. A ret = f3();
    8. return 0;
    9. }

    这里实际上发生了构造,拷贝构造,拷贝构造。

    但是我们这里进行了优化:

    编译器只执行了一个构造。

    总结:创建对象时,如何进行优化

    1:能不创建“中间商”就不创建“中间商”

    2:能直接返回匿名对象就直接返回匿名对象。 

  • 相关阅读:
    互联网Java工程师面试题·Java 总结篇·第十一弹
    K8s和Docker
    Ubuntu18.04更改镜像源(网易,阿里,清华,中科大,浙大)
    【SpringBoot】YAML 配置文件
    稚晖君项目复刻:L-ink门禁卡(1)——环境搭建与第一个项目创建
    外骨骼机器人混战:程天科技做“深”,傅利叶智能做“广”
    Tomcat的安装和配置
    蓝城兄弟完成私有化交割:从纳斯达克退市 作价6000万美元
    【CV】第 8 章:语义分割和神经风格迁移
    Web前端HTML页面input属性总结
  • 原文地址:https://blog.csdn.net/qq_66581313/article/details/127403401