C++中的类的const成员丶初始化列表丶构造函数进行隐式类型转换以及explicit关键字

一丶类中的const成员

I. const成员函数

在类的成员函数的特征标后加上const,此时该成员函数为const型成员函数

它的功能是这样的:使得该函数不得修改成员变量。

我们拿Date类举例:

#include <iostream>

using namespace std;

class Date
{
public:
	Date(int year = 1999, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	/*void PrintA()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}*/

	//加上const后 实际的类型是这样的:
	// const Date* const this
	//this指向本身就不可修改  
	//又因为添加的const
	//使得this指向的内容不可更改
	//this指向的内容中的成员变量不可被修改
	void PrintB() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

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

int main()
{
	const Date d1(2024, 4, 17);
	//d1.PrintA();		
	//PrintA不为const成员函数 造成权限放大 不允许
	d1.PrintB();

	Date d2(2024, 4, 18);
	d2.PrintB();

	return 0;
}

我们知道在类中的成员函数,第一个默认的不显式的参数为Date* const this(拿Date类举例),在使成员函数为const成员函数后,该参数将变成const Date* const this。这意味着this的指向不可修改外,this指向的内容也不可被修改。


那么我们再来讨论四个问题:

1.const对象可以调用非const成员函数吗?
2.非const对象可以调用const成员函数吗?
3.const成员函数内可以调用其它的非const成员函数吗?
4.非const成员函数内可以调用其它的const成员函数吗?

答:
1.const对象不能调用非const成员函数。
const对象为const属性,传递给非const成员函数时,非const成员函数的第一个参数为“类类型* const this”,此时相当于是将const属性数据传递给非const属性数据,致使权限放大,故不可。

2.非const对象可以调用const成员函数。
从1的答案上,就可以明白,此时相当于将非const属性数据传递给const属性数据,属于权限缩小,这是允许的。

3.const成员函数内不能调用其它的非const成员函数。
仍然从数据的const属性上讲,const成员函数中第一个参数为const型数据,而非const成员函数第一个参数为非const属性,在const成员函数中若调用非const成员函数,相当于将const属性数据传递给非const属性数据,属于权限放大,不允许。

4.非const成员函数内可以调用其它的const成员函数。
从3的答便可以得知,此时相当于非const属性数据传递给const属性数据,属于权限缩小,允许。


我们这里对是否要将成员函数置为const属性做总结:

对于不涉及修改对象内成员变量的成员函数,一般需要添加const使其成为const成员函数;对于涉及到要修改对象的成员函数,不必添加const。

II. 引用运算符重载

拿类A举例:

#include <iostream>

using namespace std;

class A
{
public:
	//平时一般不需要自行编写
	//这两个函数实现的意义是为了实现运算符重载逻辑的完整和闭环
	/*A* operator&()
	{
		cout << "A* operator&()" << endl;
		return this;
	}

	const A* operator&() const
	{
		cout << "const A* operator&() const" << endl;
		return this;
	}*/

private:
	int _a1 = 1;
	int _a2 = 2;
	int _a3 = 3;
};

int main()
{
	A aa1;
	const A aa2;

	cout << &aa1 << endl;
	cout << &aa2 << endl;

	return 0;
}

引用运算符重载函数以及const型引用运算符重载函数都是类默认的特殊的成员函数,跟拷贝构造和赋值运算符重载一样,若自己不进行实现,那么编译器将自行提供


二丶初始化列表

I. 初始化列表的引入

我们知道,在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。比方说:

class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
}

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值


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

以Date类举例:

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)	//初始化列表
	{

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

II.初始化列表的特性

  • 每一个成员变量在初始化列表中只能出现一次(即只能初始化一次)。

初始化列表中进行的操作是初始化,初始化只允许进行一次。


  • 类中包含一下成员时,必须放在初始化列表位置进行初始化:
    1.引用成员变量
    2.const成员变量
    3.自定义类型成员(且该类没有默认构造函数时)
class A
{
public:
	//初始化列表的位置,就相当于
	//数据最开始定义的位置
	//在初始化列表中初始化,就相当于
	//在数据定义时的初始化
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a, int ref)
		:_aobj(a)
		, _ref(ref)
		, _n(10)
	{}
private:
	A _aobj; // 没有默认构造函数
	int& _ref; // 引用
	const int _n; // const
};

  • 尽量使用初始化列表初始化,因为不管是否使用初始化列表,对于自定义类型成员变量,编译器一定会先使用初始化列表初始化。

初始化列表的使用时机是这样的:不管是否自行编写初始化列表,编译器都会先进行初始化列表的操作(用户不编写初始化列表时,编译器会提供初始化列表)。对于内置类型来说,会优先调用其缺省值;对于自定义类型成员来说,会调用其默认构造,倘若该默认构造不存在,那么编译将不会允许通过。

只有当执行完初始化列表后,才会执行函数体内的代码。

因此在实践编写的过程中,尽量使用初始化列表,因为这一步是省略不掉的,只有无法使用初始化列表进行初始化的成员变量,再在函数体中进行编写赋值。

我们先前在类中给予成员变量缺省值,就是给初始化列表使用的。


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

来看下面的代码:

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

int main()
{
	A aa(1);
	aa.Print();
}

它输出的结果是如何呢?
答:输出 1 和 随机值

原因就跟初始化列表中成员变量的初始化顺序有关。

成员变量在类中声明次序就是其在初始化列表中的初始化顺序。此时_a2声明在前,_a1声明在后,那么无论在初始化列表中_a2和_a1的相对位置如何,都会先初始化_a2,再初始化_a1。

此时对于上面的代码来说,那就出问题了。因为由于声明顺序,将导致_a2先初始化,但此时_a1还未初始化,导致将_a1的随机值初始化给了_a2,然后紧接着_a1被初始化为1。

在使用初始化列表时,一定要注意成员们的声明顺序,防止出现上面这样的问题。


三丶构造函数进行隐式类型转换

构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。接收单个参数的构造函数具体表现:

  • 1.构造函数只有一个参数
  • 2.构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
  • 3.全缺省构造函数

单参构造函数和多参构造函数都不为默认的构造函数。

对于单参构造函数也好,多参构造函数也好,可以直接通过符合参数特征的变量或常量来直接创建对象。

I.单参构造函数进行隐式类型转换

首先先看单参构造函数:

#include <iostream>

using namespace std;

class A
{
public:
	//单参数构造函数
	A(int a)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	
	A(const A& aa)
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}

private:
	int _a;
};

int main()
{
	//构造
	A aa1(1);

	//拷贝构造
	A aa2(aa1);

	//隐式类型转换
	//内置类型最终转换为自定义类型
	//内置类型数据->对应的包装类->该类
	A aa3 = 2;			//编译器将优化为直接的构造
	const A& raa = 3;	//编译器将优化为直接的构造

	//通过内置类型数据转换为自定义类型
	//这个操作是通过单参数构造函数实现的的

	return 0;
}

我们来看一下上面四个对象是如何被创建的。

首先是aa1和aa2,这两个好说,一个是构造函数,另一个是拷贝构造函数。我们直接来看aa3和raa对象的构造过程。

对于aa3对象,它是A aa3 = 2; 这里直接赋值了一个常量,它仍能通过编译,这要归公于单参构造函数的实现。
aa3对象的生成过着是这样的:首先通过内置类型数据,也就是常量2,生成2对应的包装类Integer,然后Integer类类型转换成A类,此时会调用该单参构造函数,生成一个A类的临时对象;再通过传递该临时对象到拷贝构造中,最终生成aa3对象。整个过程,是有参构造+拷贝构造的过程。

一般的编译器遇到这种连续调用不同构造的情况,会将其直接优化成一次有参构造。

再来说rra,它是aa3同理,只不过属于const类型的引用变量,仅此而已。其内部构造的逻辑也是两次,被编译器优化为一次。

综上,我们得知,这个使用常量利用构造函数创建对象时,会存在隐式类型转换。


那么这种写法的优势如下:

#include <iostream>

using namespace std;

class A
{
public:
	//单参数构造函数
	A(int a)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	
	A(const A& aa)
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}

private:
	int _a;
};

class Stack
{
public:
	void Push(const A& aa)
	{

	}
};

int main()
{
	Stack st;
	A a1(1);
	st.Push(a1);

	//更便利的方法
	//2可通过单参数构造变成A类型对象
	//因此可以直接进行传递
	st.Push(2);

	return 0;
}

II. 多参构造函数进行隐式类型转换

多参构造函数和单参构造函数只是在实例化对象时不同,其内部构造逻辑是一致的。

多参数利用花括号来直接构造对象。

#include <iostream>

using namespace std;

class A
{
public:
	//多参数构造函数
	A(int a, int b, int c)
		:_a(a)
		,_b(b)
		,_c(c)
	{
		cout << "A(int a, int b, int c)" << endl;
	}
	
	A(const A& aa)
		:_a(aa._a)
		, _b(aa._b)
		, _c(aa._c)
	{
		cout << "A(const A& aa)" << endl;
	}

private:
	int _a;
	int _b;
	int _c;
};

class Stack
{
public:
	void Push(const A& aa)
	{

	}
};


//内部跟单参数一致  编译器会优化为直接构造
int main()
{
	A aa1(1,2,3);
	A aa2(aa1);
	//利用多参构造函数创建对象
	A aa3 = { 1,2,3 }; 
	//支持将=符去掉
	A aa4{ 1,2,3 };
	const A& raa = { 1,2,3 };

	Stack st;
	st.Push(aa1);
	st.Push({ 1,2,3 });

	return 0;
}

四丶explicit关键字

explicit关键字用于禁止构造函数中的类型转换。

我们在上面的单参和多参构造函数中得知,通过符合特征的常量和变量可以直接构造对象。其中包含的步骤之一是隐式类型转换。而explicit就是禁止这种隐式类型转换的行为。

#include <iostream>

using namespace std;

class Date
{
public:
	// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
	// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
	explicit Date(int year)
		:_year(year)
	{}
	/*
	// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转
	换作用
	// explicit修饰构造函数,禁止类型转换
	explicit Date(int year, int month = 1, int day = 1)
	: _year(year)
	, _month(month)
	, _day(day)
	{}
	*/
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};
void Test()
{
	Date d1 = 2023; //此时由于explicit关键字
					//导致该编译不能通过
}

用explicit修饰构造函数,将会禁止构造函数的隐式转换。


本博客仅供个人参考,如有错误请多多包含。
Aruinsches-C++日志-4/19/2024
  • 39
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值