C++中的初始化列表丶单参数构造函数以及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