• C++——类和对象(初始化列表、匿名对象、static成员、类的隐式类型转换和explicit关键字、内部类)


    初始化列表、匿名对象、static成员、类的隐式类型转换和explicit关键字、内部类

    本章思维导图

    在这里插入图片描述

    注:本章思维导图对应的xmind文件和.png文件都已同步导入至资源

    1. 初始化列表

    在这里插入图片描述

    1.1 再谈构造函数

    众所周知,每个变量只能被初始化一次,我们之前一直认为成员变量的初始化是在构造函数的函数体中,但是,成员变量是可以在构造函数的函数体出现多次的

    class Date
    {
    public:
    	Date(int year = 1, int month = 1, int day = 1)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    		//出现多次,且可以编译通过
    		_year = 100;
    		_month = 200;
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    因此,我们只能认为在构造函数函数体内执行的是赋值操作,而不是初始化

    这就说明,构造函数的函数体并不是类的成员变量真正初始化的地方,那么成员变量到底是在哪里初始化的呢?

    1.2 初始化列表

    初始化列表是成员变量真正初始化的地方

    1.2.1 初始化列表的语法

    初始化列表以分号:开始,以逗号,分割,每个成员变量后面带上放在括号()里的初始值或者表达式

    例如,对于上面的构造函数:

    Date(int year = 1, int month = 1, int day = 1)
    		//初始化列表
        : _year(year)
        , _month(month)
        , _day(day)
    {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    1.2.2 初始化列表的意义

    初始化列表解决了三类不能在构造函数的函数体内初始化的问题

    • &修饰的引用成员变量——引用成员在定义时就必须初始化
    • const修饰的const成员变量——const变量在定义时就必须初始化
    • 没有默认构造的自定义类型——在函数体内不能初始化自定义类型

    也就是说,上面所说三类成员变量必须在初始列表里面进行初始化

    例如;

    class Stack
    {
    public:
        //这不是默认构造,因为要传参数
    	Stack(int capacity)
    	{
    
    	}
    
    private:
    	int* _a;
    	int _capacity;
    	int _top;
    };
    
    class Date
    {
    public:
    	Date(int year = 1, int month = 1, int day = 1)
    		: num1(2)
    		, num2(_year)
    		, st(3)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    private:
    	
    	int _year;
    	int _month;
    	int _day;
    	
    	const int num1;
    	int& num2;
    	Stack st;
    
    };
    
    • 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
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    1.3 注意事项

    • 因为初始化列表是成员变量初始化的地方,而每个变量又只能初始化一次,因此成员变量只能在初始化列表出现一次

    • 因为初始化列表是真正初始化成员变量的地方,因此无论有没有显示的写出初始化列表,成员变量都会经过初始化列表的初始化

    • 如果没有显示的写出初始化列表,那么:

      • 对于内置类型,那就赋予其初始值
      • 对于自定义类型,就调用它的默认构造
    • 能使用初始化列表就使用初始化列表。但也不是说初始化列表就能完全替代函数体。因为有时候函数体需要进行检查等操作。

    • 初始化列表的初始化顺序是成员变量声明的顺序,而不是在初始化列表里出现的顺序。

      class A
      {
      public:
      	A()
      		: a1(1)
      		, a2(a1)
      	{
      	}
      
      	void Print()
      	{
      		cout << a1 << endl << a2 << endl;
      	}
      private:
      	int a2;
      	int a1;
      };
      
      int main()
      {
      	A a;
      	a.Print();
      
      	return 0;
      }
      
      /*output:
      	1
      	-858993460
      */
      //a2声明在a1之前,因此,在初始化时,先执行a2(a1),此时a1为随机值
      //因此建议成员变量的初始化顺序和声明顺序一致
      
      • 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
      • 30
      • 31
      • 32

    2. 匿名对象

    在这里插入图片描述

    class Date
    {
    public:
    	Date(int year = 1, int month = 1, int day = 1)
    		: _year(year)
    		, _month(month)
    		, _day(day)
    	{
    		myCount++;
    	}
    
    	void Print()
    	{
    		cout << "Date" << endl;
    	}
    private:
    	
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    如果我们想不实例化对象,但想调用Date类里的Print()函数来知道这是个什么类,该如何做到呢?

    这里就可以用到我们的匿名对象来解决:

    int main()
    {
    	Date().Print();	//Date()创建出一个匿名对象,再用这个匿名对象来调用成员函数Print()
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    创建匿名对象的方式:

    className()

    匿名对象的特点:

    • 匿名对象是一种临时对象,它没有分配给任何命名变量,而是在需要时被创建并使用
    • 其生命周期仅存在于当前行,执行完后立即销毁
    • 匿名对象一般是常量对象,不可被修改

    3. static成员

    在这里插入图片描述

    如果我们想记录一个类究竟被构造了多少次

    我们不难写出这样的代码:

    //定义一个全局变量来记录类A构造的次数
    int myCount = 0;
    
    class A
    {
    public:
    	//构造函数
    	A()
    	{
    		myCount++;
    	}
    	//拷贝构造
    	A(A& a)
    	{
    		myCount++;
    	}
    };
    
    int main()
    {
    	A a[10];
    
    	cout << myCount << endl;
    
    	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

    但是这就出现了一个问题:我们可以在全局随意修改变量myCount的值:

    int main()
    {
    	A a[10];
    
    	myCount++;
    
    	cout << myCount << endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这样也就不能保证类被构造次数的正确性了。

    3.1 static成员变量

    为了解决这个问题,我们可以在类里面声明一个static成员,并用这个成员来记录类被构造的次数

    class A
    {
    public:
    	//构造函数
    	A()
    	{
    		myCount++;
    	}
    	//拷贝构造
    	A(A& a)
    	{
    		myCount++;
    	}
    
    private:
    	static int myCount;
    };
    
    int A::myCount = 0;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这个static修饰的静态成员变量有如下特点

    • 实际上也是一个全局变量,只是受类域和访问限定符所限制
    • 静态成员变量只能在类里面声明在类外面定义。
    • 静态成员变量在声明时不能和非静态成员变量一样给缺省值,因为这个缺省值是给初始化列表里用的,而静态成员变量不用初始化列表初始化。
    • static修饰的静态成员变量是这个类所属的,而不是由这个类实例化的某个对象所独有

    3.2 static成员函数

    知道如何利用static成员变量之后,针对最开始的问题,我们不难写出下面的代码:

    class A
    {
    public:
    	//构造函数
    	A()
    	{
    		myCount++;
    	}
    	//拷贝构造
    	A(A& a)
    	{
    		myCount++;
    	}
    
    	//因为myCount被private修饰,在类外面无法访问
    	//因此要用成员函数访问myCount
    	int GetCount()
    	{
    		return myCount;
    	}
    
    private:
    	static int myCount;
    };
    
    int A::myCount = 0;
    
    int main()
    {
    	A a[10];
    
    	//为了调用GetCount成员函数,必须要实例化一个对象,而这个对象是没有意义的,因此最终结果要减一
    	cout << A().GetCount() - 1 << endl;
    	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
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    但是又有一个问题出现了:

    我们只是想知道A类到底被调用了多少次,但是要知道这个结果又必须新实例化一个对象,有没有什么方法不实例化对象就可以直接得到myCount的值呢?

    为了解决上述问题,就需要用到static成员函数

    class A
    {
    public:
    	//构造函数
    	A()
    	{
    		myCount++;
    	}
    	//拷贝构造
    	A(A& a)
    	{
    		myCount++;
    	}
    
    	//static静态成员函数
    	static int GetCount()
    	{
    		return myCount;
    	}
    
    private:
    	static int myCount;
    };
    
    int A::myCount = 0;
    
    int main()
    {
    	A a[10];
    
    	cout << A::GetCount() << endl;
    
    	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
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    static修饰的静态成员函数有如下特点

    • 静态成员变量一样,静态成员函数实际上也是一个全局函数,只是受类域和访问限定符限制

    • 静态成员函数在类里面声明,但既可以在类外面定义也可以在类里面定义

    • 和非静态成员函数不同,静态成员函数没有this指针,因此静态成员函数无法访问非静态成员变量和非静态成员函数,但也因如此,它可以直接通过类名和域作用限定符::调用

    在这里插入图片描述

    4. 类的隐式类型转换和explicit关键字

    4.1 类的隐式类型转换

    以前我们一般是这么实例化一个对象的:

    class Date
    {
    public:
    
    private:
    };
    
    int main()
    {
    	Date d1;	//利用构造函数实例化对象
    	Date d2(d1);	//利用拷贝构造实例化对象
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    现在又有一个新的实例化对象的方法——类的隐式类型转换

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

    在这里插入图片描述

    可以看出,整形10确实被转换为了A类型。

    根据当隐式类型转换发生时会产生临时变量的知识点,我们可以推导出A A1 = 10这行代码的具体实现逻辑:

    在这里插入图片描述

    应该清楚,要支持这种隐式类型转换,该类的构造函数应该支持只传一个内置类型就可以实现构造

    例如对于下面几种情况,就不支持内置类型隐式转换为类类型:

    //Error_1
    class A
    {
    public:
    	A()
    		: _a(a)
    
    	{
    
    	}
    
    private:
    	int _a;
    };
    int main()
    {
    	A A1 = 10;
    
    	return 0;
    }
    /*
    报错:
     	error C2065: “a”: 未声明的标识符
     	error C2440: “初始化”: 无法从“int”转换为“A”
    	message : 无构造函数可以接受源类型,或构造函数重载决策不明确
    */
    
    //Error_2
    class A
    {
    public:
    	A(int a, int b)
    		: _a(a)
    
    	{
    
    	}
    
    private:
    	int _a;
    	int _b;
    };
    int main()
    {
    	A A1 = 10;
    
    	return 0;
    }
    /*
    报错:
    	error C2440: “初始化”: 无法从“int”转换为“A”
     	message : 无构造函数可以接受源类型,或构造函数重载决策不明确
    */
    
    • 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
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    类似的,对于有多个形参的构造函数,我们也可以传入多个内置类型进行构造:

    class Date
    {
    public:
    	Date(int year = 1, int month = 1, int day = 1)
    		: _year(year)
    		, _month(month)
    		, _day(day)
    	{
    
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	Date d3 = {2023, 11, 7};	//传入三个内置类型进行构造
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述

    4.1 explicit关键字

    有些时候,如果我们不想让上面所说的隐式类型转换发生,我们可以在构造函数的声明前加上explicit关键字:

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

    加上explicit关键字后,如果继续进行隐式类型转换,就会报错:

     error C3445: "Date" 的复制列表初始化不能使用显式构造函数
    
    • 1

    5. 内部类

    C++支持在类的内部继续创建类,例如:

    class A
    {
    public:
    	class B
    	{
    
    	};
    
    private:
    	int _a;
    	int _b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    内部类有如下的特点:

    • 内部类是一个独立的类,它不属于外部类,不能通过外部类的对象来访问内部类的成员

    • 内部类天生就是外部类的友元类,可以直接访问外部类的成员变量和成员函数

      class A
      {
      public:
      	class B
      	{
      		void Print(A& a)
      		{
      			a._a = 1;
      		}
      
      	public:
      		int _b;
      	};
      
      private:
      	int _a;
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
    • sizeof(外部类)的结果和内部类无关

      class A
      {
      public:
      	class B
      	{
      	public:
      		int _b;
      	};
      
      private:
      	int _a;
      };
      
      int main()
      {
      	cout << sizeof(A) << endl;
      
      	return 0;
      }
      
      //output:4
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21

    • C++类和对象的知识到这里就学习完毕了,之后博主会发布C++类和对象的总结篇

    • 下一篇,博主将介绍C++的内存管理,感兴趣的小伙伴可以来看看哦~

    请添加图片描述

  • 相关阅读:
    Linux使用之xshell、xftp保姆教学(含安装包,详细使用方法,连接失败解决方法)
    ucontext的简单介绍
    【云原生 中间件】Netty是干什么的?
    【人工智能 & 机器学习 & 深度学习】基础选择题 61~90题 练习(题目+答案)
    弹簧(压簧)力度计算与设计
    成熟NFT数字藏品交易app软件平台开发介绍
    白嫖一个月的ES,完成了与MySQL的联动
    Spring Boot——yml和properties详解
    攻防世界WEB练习-favorite_number
    vue中动态路由详解
  • 原文地址:https://blog.csdn.net/l315225/article/details/134274897