• 类和对象(下)


           关于类和对象依旧有许多难点,这篇博客将会讲解关于类的构造函数的初始化列表,静态成员,友元,内部类,以及匿名对象等一些比较复杂的东西。

    初始化列表

    我们之前就已经学过类和对象的构造函数,但是实际上那并不算是对象的初始化,其实算是给成员变量赋值。

    而我们用c语言和c++时都知道,变量初始化只能初始化一次,而赋值可以在函数内赋值无数次

    既然说构造函数只能算是给成员变量赋值,那么怎样才能证明?

    根据之前学的知识,我们都知道用const修饰的对象都只能在初始化的时候确定值

    那么我们用默认构造函数初始化const成员变量不就可以证明了吗?

    我们直接看看:

    发现用默认构造函数实际上不能初始化用const修饰的变量

    这就变相的证明了默认构造函数内部实际上不是初始化,而是赋值

    那么类的对象在哪里才算是初始化呢?

    这就轮到初始化列表出场了。

    不过在了解初始化列表之前需要先了解下初始化列表的使用场景。

    初始化列表使用场景:

    1.用来初始化const成员变量

    2.用来初始化无默认构造函数的自定义类型成员变量

    3.用来初始化引用成员变量

    所谓初始化列表,实际上就是在构造函数下,以一个冒号为开始成员之间以逗号隔开的列表每一个成员变量后面用括号跟上赋的值

    初始化列表的使用方式:

    我们可以看到,确实是成功的初始化了成员变量。

    而上面也说了,初始化列表不仅能够初始化const成员变量;

    也可以修饰其它两种变量,其中,自定义类型的变量需要好好了解一下。

    1. class B {
    2. public :
    3. B(int _b)
    4. {
    5. b = _b;
    6. }
    7. private:
    8. int b;
    9. };
    10. class A {
    11. public:
    12. A()
    13. :a(10)
    14. ,_b(5)
    15. {
    16. }
    17. private:
    18. const int a;
    19. B _b;
    20. };
    21. int main()
    22. {
    23. A _a;
    24. return 0;
    25. }

     通过这里我们可以发现,实际上初始化列表在初始化自定义类型的变量的时候;

    会调用对应的构造函数,并且将括号里的数据用来初始化成员变量,而若是初始化列表没有显示初始化自定义类型的成员变量时,就会调用默认构造函数,无则报错;

    此外,还有引用的成员变量需要使用初始化列表才能用。

     这里我们可以看到,成功的初始化了rc这个引用类型的成员变量。

    初始化列表的规则

    对象的所有成员都会走一套初始化列表,面对自定义类型的变量,若是初始化列表没有显示初始化就会调用对应的默认构造函数,无则报错,而面对内置类型,有显示初始化就用显示的值,无则用随机值或者构造函数内部的数值。

    此外,还有一个规则

    初始化列表的初始化顺序是根据成员变量的声明顺序决定的。

     这样我们发现,成员变量的声明顺序是 先a2后a1,而初始化列表则是先a1后a2,这就说明,初始化列表的初始化顺序由变量的声明顺序决定。

    explicit 关键字

    之前我们写过Date类,而Date类中的构造函数其实还有其他用处。

    比如有一个构造函数只有一个参数,或者说只有第一个参数是没有缺省值的时候,会出现隐式类型转换

    我们先来看看代码。

     我们发现,Date类型的对象d1居然能够直接用int类型的常量来初始化。

    实际上这里涉及到类的隐式类型转换。

    首先编译器会将用构造函数创建一个Date类型的中间变量,再用拷贝构造将中间变量的值给d1

    当然,实际上这只是便于理解的说法,现在的编译器都对这个过程进行了优化。

    过程变成了直接用构造函数来将2022作为参数来构造d1。

    下图可证:

    这样实际上用处并不大,因为大部分的类的成员变量都不只有一个,比如Date类就有三个成员变量。

    当然,我们也可以这样初始化一个变量,不过格式需要注意,如下:

     就好像是初始化数组一样,这样也是可以的,但是这样会影响可读性,因此c++针对这个出现了一个关键字——explicit。

    将explicit关键字放于构造函数之前,就可以禁止这样的隐式类型转换。

    我们可以看到,对象实例化的地方出现了报错。

    因此这样就可以避免这种错误出现。

    静态成员变量以及函数(static)

    在类中,有一种特殊的成员变量——static成员变量,这种被称为静态成员变量,那么这种变量有何妙用呢?接下来我们就来深入了解一下吧。

     在之前我们学习过,static修饰的变量和普通变量不一样,普通变量都是在栈区,除非你是动态开辟的那么就在堆区,而static修饰的则在静态区。

    而类中的成员若是用static修饰会怎样呢?

    static修饰的成员特性

    1.静态成员为所有对象共享,存放在静态区。

    2.静态成员变量必须在类外定义,定义时不用加static关键字。

    3.类的静态成员可以直接用类名::静态成员或者对象.静态成员来访问。

    4.静态成员函数没有隐藏的this指针

    5.静态成员也受限定符限制

    了解了静态成员的特性后,我先来直接看看实现。

    1. #include
    2. using namespace std;
    3. class Date {
    4. private:
    5. int _year;
    6. int _month;
    7. int _day;
    8. static int time;
    9. public:
    10. Date(int year = 0, int month = 0, int day = 0)
    11. {
    12. _year = year;
    13. _month = month;
    14. _day = day;
    15. }
    16. static void Print1()
    17. {
    18. cout << "静态成员函数" << endl;
    19. // Print2();静态成员函数不能调用非静态成员函数,因为非静态成员函数必须在对象初始化后才能使用
    20. }
    21. void Print2()
    22. {
    23. cout << "非静态成员函数" << endl;
    24. Print1();//而非静态成员函数可以调用静态成员函数
    25. }
    26. };
    27. int Date::time = 1;
    28. int main()
    29. {
    30. Date::Print1();
    31. Date d1;
    32. cout << endl << endl;
    33. d1.Print2();
    34. return 0;
    35. }

     

    我们发现,静态成员函数不能调用非静态成员函数,而非静态成员函数则 能调用静态成员函数

    此外,定义的time的static类型的变量只能在外面才能初始化,并且构造成员函数无法初始化静态成员变量,但是限定符依旧能够限制外部直接访问time这个静态成员变量。

    友元

    友元函数

    之前我们实现了Date类,但是我们还有几个方法没有实现。

    比如用istream直接输入Date变量,而不是通过构造函数来创建。

    当然,我们可以在类里面实现,但是这样我们就会有隐藏的this指针,我们的输入就会变成这样:

    1. Date d1;
    2. d1>>cin;

    这样就和cin不同了,因此 为了可读性,我们只能在类外面实现这种函数。

    但是我们Date类的成员变量又有访问限定符private来防止外部直接访问,那么我们该怎么办呢?

    这里就轮到友元出场了。

    1. class Date {
    2. friend istream& operator>>(istream& in, Date& d);
    3. friend ostream& operator<<(ostream& out, const Date& d);
    4. private:
    5. int _year;
    6. int _month;
    7. int _day;
    8. public:
    9. static int time;
    10. Date(int year = 0, int month = 0, int day = 0)
    11. {
    12. _year = year;
    13. _month = month;
    14. _day = day;
    15. }
    16. static void Print1()
    17. {
    18. cout << "静态成员函数" << endl;
    19. // Print2();静态成员函数不能调用非静态成员函数,因为非静态成员函数必须在对象初始化后才能使用
    20. }
    21. void Print2()
    22. {
    23. cout << "非静态成员函数" << endl;
    24. Print1();//而非静态成员函数可以调用静态成员函数
    25. }
    26. };
    27. int Date::time = 1;
    28. istream& operator>>(istream& in, Date& d)
    29. {
    30. in >> d._year >> d._month >> d._day;
    31. return in;
    32. }
    33. ostream& operator<<(ostream& out, const Date& d)
    34. {
    35. out << d._year << '/' << d._month << '/' << d._day << endl;
    36. return out;
    37. }
    38. int main()
    39. {
    40. Date d;
    41. cin >> d;
    42. cout << d;
    43. return 0;
    44. }

    看过实现后,再来看看友元函数的特性:

    友元类的特性

    1.友元函数可以直接访问类的私有和保护成员,但不是类的成员函数

    2.友元函数不能用const修饰

    3.友元函数可以在类的任意位置定义不受类的访问限定符限制

    4.一个函数可以是多个类的友元

    5.友元函数的调用和普通函数的调用原理相同

    友元类

    友元可不止只有友元函数可以使用,实际上,也有友元类存在。

    而友元类实际上差不多。

    1. class Time {
    2. friend class Date;
    3. private:
    4. int _hour;
    5. int _minte;
    6. public:
    7. Time()
    8. {
    9. }
    10. void Print()
    11. {
    12. cout << _hour << _minte << endl;
    13. }
    14. };
    15. class Date {
    16. private:
    17. int _year;
    18. int _month;
    19. int _day;
    20. Time t;
    21. public:
    22. Date()
    23. {
    24. }
    25. void Print()
    26. {
    27. cout << _year << _month << _day << t._hour << t._minte << endl;
    28. }
    29. };

    我们在Date类创建了一个Time类型的成员变量t,我们就能够直接访问 t 的成员变量。

    友元类的特性

    1.友元关系是单向的,不具有交换性。
    2.友元关系不能传递
    3.友元类不能继承

    内部类

    当我们在一个类的内部定义了另一个类,那么这个类就是内部类。

    而这个内部类实际上就是相当于外部类的友元函数,因此内部类可以直接使用外部类的变量。

    但是这个友元只是单向的,外部类并不能用内部类的成员。

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

    此外,内部类受外部类的访问限定符和类域限制。

    内部类的特性:

    1. 内部类可以定义在外部类的 publicprotectedprivate 都是可以的。
    2. 注意内部类可以直接访问外部类中的 static成员,不需要外部类的对象/ 类名
    3. sizeof( 外部类)= 外部类,和内部类没有任何关系。

    匿名对象

     

    在c++中有这样一种奇怪的对象,它没有名字,被称为匿名对象,我们直接看看如何实现。

    1. class A {
    2. private:
    3. int _a;
    4. public:
    5. A(int a)
    6. :_a(a)
    7. {
    8. }
    9. ~A()
    10. {
    11. cout << "这是一个析构函数" << endl;
    12. }
    13. };
    14. int main()
    15. {
    16. A a1(1);
    17. A(1);
    18. return 0;
    19. }

    这个匿名对象十分神奇,它的生命周期只有这一行。

     

    那么匿名对象有什么用呢?

    比如,我们需要用类的方法返回一个值。

    比如这样的类:

    1. class Solution {
    2. private:
    3. int n;
    4. public:
    5. Solution(int _n)
    6. :n(_n)
    7. {
    8. }
    9. int addFromTo(int from,int to)
    10. {
    11. int ret = 0;
    12. for (int i = from; i <= to; i++)
    13. {
    14. ret += i;
    15. }
    16. return ret;
    17. }
    18. };
    19. int main()
    20. {
    21. Solution d(5);
    22. cout << d.addFromTo(0, 100) << endl;
    23. return 0;
    24. }

     

     

    我们为了计算从0到100的和而创造了一个对象。

    但是这个对象可能之后都用不着了;

    这时候就可以用匿名对象了。

    编译器的优化

    在一些新一点的编译器中,对象的创建会被编译器优化。

    我们先创建一个这样的类。

    1. class A {
    2. private:
    3. int _a;
    4. public:
    5. A(int a = 0)
    6. :_a(a)
    7. {
    8. cout << "这是一个构造函数" << endl;
    9. }
    10. ~A()
    11. {
    12. cout << "这是一个析构函数" << endl;
    13. }
    14. A(const A& a)
    15. {
    16. cout << "这是拷贝构造" << endl;
    17. }
    18. };

    1.优化场景1

     

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

    在以前的编译器中,这里的对象应该是先构造一个中间变量再用中间变量拷贝构造出a。

    顺序本来是这样的:

     但是现在是这样了:

     我们可以看到这里只有一次构造函数。

    2.优化场景2

    1. int f(A a)
    2. {
    3. }
    4. int main()
    5. {
    6. f(A());
    7. return 0;
    8. }

     像这里,如果是用匿名对象来传参,就会直接优化,

    变成一次构造函数:

     3.优化场景3

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

     像这样的场景,本来应该是构造加拷贝构造再拷贝构造的。

     而这里编译器会做一个优化,免去中间的拷贝构造,化为一个拷贝构造。

     这就是编译器的优化。

    以上就是类的剩余知识了,谢谢大家。

  • 相关阅读:
    JVM概述及类加载器
    安全驱动怎么设计(一)
    北京程序员的真实一天!!!!!
    JS算法练习 11.20
    外网nat+nat server,内网做路由过滤,以及ppp CHAR认证 企业网搭建
    C++vector模拟实现
    算法---优美的排列(Kotlin)
    算法11.从暴力递归到动态规划4
    《小狗钱钱》阅读笔记(四)
    JavaScript的DOM操作(二)
  • 原文地址:https://blog.csdn.net/m0_64028711/article/details/127778309