C++类和对象3:关于类内部的更多细节

目录

初始化列表:

explicit关键字

​编辑

 static成员

友元

内部类

 匿名对象

 拷贝对象时的一些编译器优化


我们已经接触过了构造函数,其功能可以很方便的帮助我们为变量赋值,但是在这里并不是初始化,因为一个构造函数可以为几个变量进行多次赋值,这与初始化的概念相悖。

简而言之,我们前文所提到的初始化方法是比较……冒牌的,如下:

class Date
{
public:
 
	Date(int year=2022, int month=12, int day=29)
	{
		_year = year ;
		_month= month ;
		_day = day   ;
	}
 
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day  << endl;
	}
 
private:
	int _year;
	int _month;
	int _day;
};

 所以,真正的通过构造函数去初始化的流程是使用初始化列表。

初始化列表:

 初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟
一个放在括号中的初始值或表达式,如下记录两种写法,两种写法的效果相同,按着喜好写就好

class Date
{
public:
    //写法一
	Date()
        :_year(2022)
        ,_month(1)
        ,_day(2)
	{}

    //写法二
    Date()
        :_year(2022),_month(1),_day(2)
	{}

private:
	int _year;
	int _month;
	int _day;
};

每个成员变量在初始化列表中只能出现一次

初始化列表也可以跟函数体内部的表达式混用,比如栈的初始化

混着来也是有必要的,初始化列表没法干所有的事情。

 这玩意看着也不厉害啊?到底有啥特攻的呢?

类中包含以下成员,必须放在初始化列表位置进行初始化:

1.const成员变量

2.没有默认构造函数的自定义类型

3.引用成员变量


第一个使用场景:const成员变量

  • 由于const只有一次初始化的机会,所以用构造函数的函数体内部去初始化一个const类型的成员变量是不被允许的。因为此时已经走过了定义初始化阶段,而定义初始化阶段就是初始化列表。

  • 而对于内置类型,假如我们不给列表里的值进行初始化编译器会给一个随机值,而在之前我们应对这种情况的解决办法就是缺省值,联系现在的知识其实这个缺省值给予的阶段就是初始化列表的时候,因为不论如何所有的成员变量都会走初始化列表

 第二个使用场景:引用成员变量

class B
{
public :

	B(int use,int a)
		:_i(0)
		,_use(use)
	{
	}

private:
	const int _i;
	int& _use;
	A _A;

};

 第三个使用场景:没有默认构造函数的自定义类型

其实记述到这里,你或许会有一点疑惑,为什么一定要初始化列表才能初始化这三兄弟?主要还是取决于它们的特殊性,例如引用,它在被创建时必须初始化,const对象也是同理。

那么为什么没有默认构造函数的自定义类型也要走初始化列表呢?

我们先回顾以下前文的知识,类对待成员变量时,对内置类型不处理,而对自定义类型会调用它的默认构造函数。

那么,没有没有默认构造函数的自定义类型,如下:


class A
{
public:
	A(int a )
		:_a(a)
	{}

private:
	int _a;
};

我们可以看到,我们写了构造函数,但不是一个默认构造函数,它需要传参才能正常的初始化,那么在这种情况下,若类B想要成功的初始化这个自定义类型,就必须找到其初始化的方法,可是完全找不着,就会报错

 那么,这种情况下,为什么初始化列表就能解决这个问题呢?

其实在C++的类中,我们就算不写初始化列表,那也是会走的,而且每个成员都一定会走初始化列表。我们相当于为其在这个阶段直接传参,成功的使其初始化成功,这才能解决这个问题。

总结如下:

需要注意的是,初始化列表中的顺序是按照声明的次序来的,比如日期类就是按照年月日的声明顺序来的,初始化列表的顺序也相同。

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

class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}
	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};


explicit关键字

当类的构造函数为单参数的构造函数的时候,我们可以只用一个参数赋值进行构造。

也就是直接Date d2 = 2022;

之所以可以用int类型直接创建一个对象,其原理则是隐式类型转换。

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。

也就是在单参数构造的这个过程之中,2022构造一个无名对象,最后用无名对象给d2对象进行赋值

原理如上图所示,但是当我们想使用引用来创建一个对象的时候,直接使用引用会报错,因为发生任何转换的时候都会产生一个临时变量,而这个临时变量具有常性,在前面加上一个const才可以成立。

当然,这个转换发生的条件也不是那么苛刻的,只需要转递参数的都会发生隐式类型转换。

 static成员

有时候我们不得不使用全局变量来实现某一些操作,但是全局变量本身并不安全,因为它可以被随意更改,所以,为了更加安全的使用全局变量或者说具有全局性质的变量,我们可以使用static去修饰一个类里的成员。

在此之前,我们需要注意的是局部的静态和全局的静态的区别究竟在哪?

局部静态变量与全局静态变量之间的区别在于其作用域的不同,局部的只能在它的作用域里面用,而全局的在哪都能用,他们都存放于静态区,生命周期相同。但是而类里面的静态就属于类的作用域。

 声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化

因为作为一个类的成员变量,每一次创建一个类就去初始化一个成员变量天经地义,但是每一次创建一个就初始化一次N个这个变量的静态属性就没有任何意义了,所以静态的成员变量一定要在类外进行初始化,同理,缺省值也是不能初始化静态成员变量的。

 定义初始化要给类型。

当然,作为一个属于这个类的成员变量在里面直接访问也是没有问题的。

总而言之:


旧题再回顾,这个会不会报错?

 不会, 这段依旧可以运行的原因是同成员函数一样存放在对象外头,static放在静态区还是可以被找到的。

由于静态变量的特殊性质,我们一般不会把它放到共有里面,而是放在私有里,为了获取它的值的时候,我们就写一个函数来获取。

 但是这样子的话每一次想要使用这个函数的时候都需要先创建一个对象才行,用起来还是比较蛋疼的,有没有什么办法可以直接拿到它的值呢?这个时候就可以使用静态成员函数来解决这个问题。

 作为静态的成员函数,它没有this指针,结合前面的知识来讲,我们不能越过类直接调用成员函数的原因主要是因为This指针的存在,而它没有,就可以不用创建对象直接调用了。

 但是这样也带来了一些副作用,这个函数将无法访问非静态的成员变量。

 总结下来

静态类型在类和对象中主要是起到打破类的墙的作用,而且其特性很像同类认证,只有都是static的同类才被互相认可。

1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区

2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明

3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问

4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员

5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

友元

友元,我们前面已经接触过,简单来说就是开个后门可以偷家访问到私有的变量,突破私有的封装。

·不过我们之前接触的是友元函数而非友元类,友元类其实也大差不差,主要有以下几种性质。

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

友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。

友元关系不能传递如果C是B的友元, B是A的友元,则不能说明C时A的友元。友元关系不能继承,在继承位置再给大家详细介绍。

内部类

内部类的概念其实就是类中类,在一个类里创建另一个类,这个类就是内部类,内部类有如下几种特征。

1. 内部类可以定义在外部类的public、protected、private都是可以的。

2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。

3. sizeof(外部类)=外部类,和内部类没有任何关系。

在这里内部类的特点还是很奇怪的,B可以随意地访问A的成员,而A却不能访问B的。

 

 匿名对象

 我们正常创建对象的方法有如下两种

 匿名对象就是这种

 匿名对象的特点是:它的生命周期仅仅只有这一行,在某些情况下这个特点将会显得很有用,比方说一个被封装在类里的很简单的某个成员函数

 要创建一个对象,但是匿名对象就可以直接用

 还有一种应用场景:

优化成:

 拷贝对象时的一些编译器优化

 优化场景1:

我们回到单个传参即可创建对象的那一行代码中,也就时explicit关键字的那块,我们可以仅仅使用一个变量触发隐式类型转换然后创建一个对象,生成过程是先拿1创建一个空版临时对象,然后拿着这个临时对象去构造aa1

我们在创建这个对象的时候,在老版本的编译器下,执行的步骤就是:

1.先用1构造一个相同类型的对象

2.拷贝构造到aa1里。

在现代的编译器中,编译器变得更加的“负责”它会优化这一过程变成直接构造


 优化场景2:

 

在这个场景下,我们用A类创建了一个aa1对象,将aa1传参进入f1,在这个过程中,进入函数体内部时发生了拷贝构造

这是一个构造加拷贝构造,但是这并不是一个连续的步骤,为了保证不改变优化后的正确性,编译器不会优化这段过程。换用如下的创建方法即可触发优化。


 优化场景3:

 

这里这个函数的原生执行步骤为构造+拷贝构造

如果是有返回值接收的话,编译器会优化成如下步骤。

 以上的是还没有新建一个对象才能这样优化,如下的就不行了

 


优化场景4:

 

 代码注释部分为非优化写法,在这个非优化写法中,我们先构造了一个aa,然后再拷贝构造出一个临时对象,接着,这个临时对象再次触发拷贝构造给到返回值。

 优化写法如下:

 总而言之:利用好编译器的优化机制,我们能直接返回啊,或者创建变量什么的,不要有中间过程,直接拿来用最佳。


到此,类和对象就概述完毕了!

感谢阅读,希望对你有点帮助!

猜你喜欢

转载自blog.csdn.net/m0_53607711/article/details/128518779