🔥个人主页:Forcible Bug Maker
🔥专栏:C++
目录
前言
取地址及const取地址操作符重载
再谈构造函数
初始化列表
隐式类型转换
explicit关键字
成员变量缺省值
结语
前言
本篇主要内容:类的六个默认成员函数中的取地址及const取地址重载,构造函数初始化列表,隐式类型转换,缺省值。
上篇博客用之前学过的知识实现了一个简单的日期类Date,在日期类中,有介绍到多种类型运算符重载的运用,如前置++后置++等。在运算符重载的过程中,有效的代码复用也非常重要,可以大大简化代码编写过程。最后还提到了const成员和友元。本篇博客将会介绍最后两个类的默认成员函数,不过并不困难。而文中再次谈到的构造函数需要静下心来理解。
取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器默认生成的就够用。
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 取地址重载
Date* operator&()
{
return this;
}
// const取地址重载
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1;
const Date d2;
cout << &d1 << endl;
cout << &d2 << endl;
return 0;
}
取地址重载,其实就是返回地址的两个函数,C++提供这种默认成员函数主要是想兼容操作符重载,给予C++更大的灵活性。在上面的代码案例中,d1取地址时调用的是非const类型的取地址重载函数,而d2取地址时调用的是const类型的取地址重载函数。我们可以改变返回值再去观察一下。
这次我们调整返回值后再打印,是否能感受到关于取地址重载的运用呢?其实,取地址重载很少用,除非你要恶作剧或者想让别人获取到指定的内容,否则默认生成的取地址就是完全够用的。
再谈构造函数
初始化列表
C++的初始化列表(Initializer List)是构造函数的一种特性,用于初始化类的数据成员。在构造函数体执行之前,初始化列表会先执行,确保数据成员在构造函数体开始执行之前就已经被正确地初始化。
初始化列表的使用:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
当使用上述类初始化对象时,三个成员函数都会成功在初始化列表中被传入的参数初始化。你可能会问,为什么要有初始化列表,在构造函数的函数体中初始化不是很香吗?可以来看看下面这个例子:
class stack
{
public:
stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
_size = 0;
_capacity = capacity;
}
void push(int x)
{
_a[_size++] = x;
}
private:
int* _a;
int _size;
int _capacity;
};
class MyQueue
{
public:
MyQueue(int pushN, int popN)
{}
private:
stack _pushst;
stack _popst;
int _size;
};
在上面这份代码中,我们编写了一个MyQueue类,里面定义了两个对象成员和一个整型成员变量,你是否想过,该如何初始化对象成员呢?由于stack中定义了缺省参数,不需要传参就可以完成构造,但如果你需要指定stack的capacity或者没有缺省参数时,该怎么办呢?
仔细思考,进入函数体后,成员变量的空间就已经都开好了,所以在函数体中是无法完成初始化赋值的。而初始化列表就可以完美解决此问题。如下是初始化列表初始化对象的方式:
以下三种类的成员,必须放在初始化列表的位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(且没有默认构造函数)
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
};
建议:能在初始化列表中初始化就在初始化列表中初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
初始化列表的特点:
- 初始化列表,不管写没写,每个成员变量都会走一遍,而且在初始化列表中只能出现一次(初始化只能初始化一次)
- 对于自定义类型,会调用默认构造(没有默认构造则报错)。
- 先走初始化列表,再走函数体。
- 拷贝构造也有初始化列表。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
对于第四点,我们可以使用一份代码来证明:
上面这份代码,根据_a1和_a2的声明顺序,初始化列表先走的_a2,再走的_a1,导致在初始化_a2使用了未初始化_a1,故产生了随机值,佐证了特点四。
隐式类型转换
之前我们讲过,不同类型的内置类型变量在相互赋值时会有隐式类型转换。
double a = 10.5;
int b = a;
就如上面这个简单的赋值,在a赋值给b之前,会产生一个临时变量,最终赋给b值的就是这个临时变量。
当将不同类型的变量取引用时,需要加const的原因,是因为临时变量具有常性。
临时变量具有常性,其本质就跟数字一样如,1,2,3等,可以给变量赋值,正常情况下不能取到地址或者取到引用,除非用const修饰变量。
double a = 10.5;
// int& b = a;// 报错
// int& c = 10;// 报错
const int& b = a;// 正常运行
const int& c = 10;// 正常运行
上述代码中b取的就是a产生的临时变量的引用,临时变量存储在内存的静态区,具有常性,就跟第四行代码的数字10性质是一样的,当你加上const时,这种引用权限就被放开了,因为const确保了你不会对静态区的变量做出改动。对于C++的自定义类型,与内置类型遵循的规则是一样的。
C++支持一种类型转换式的构造:
class A
{
public:
A(int a)
:_a1(a)
{}
A(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
A aa1(1);
A aa2 = 1;
return 0;
}
对于main函数第一行代码是标准的调用了构造函数。而第二行,作为内置类型的1,竟然能给对象的初始化赋值,这是因为在赋值之前,产生了隐式类型转换,1作为一个参数传递给了构造函数,从而产生了一个临时对象,最终临时变量拷贝构造给aa2。
在调用此代码的过程中,我们发现,并没有调用拷贝构造函数,这是因为通过编译器的优化,省去了拷贝构造这一过程,简单来说就是:
构造函数 + 拷贝构造 + 编译器优化 = 构造函数
这时候看这两行能否运行的原因应该就不困难了:
// A& ref = 10;// 报错
const A& ref = 10;//可运行
// 这里ref引用的是类型转换中用10构造的临时对象
在上面代码中,我们使用的构造函数一直是单参数的,可以使用特殊的隐式类型转换构造。但是如果构造函数是多参数的,该怎么使用类似于A aa = 1;的方式创建对象呢?其实C++提供了解决方案,那就是多参数构造。
class A
{
public:
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
A(const A& aa)
:_a1(aa._a1)
,_a2(aa._a2)
{}
void print()const
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1;
int _a2;
};
// 多参数构造
int main()
{
A aa1 = { 2,2 };
aa1.print();
// A& ref = { 2,3 };//报错
const A& ref = { 2,3 };
ref.print();
return 0;
}
不过需要注意的是,只有C++11及其往后的版本才支持多参数构造。老版本,如C++98并不支持这样创建对象。
explicit关键字
这个知识点稍稍提一下,如果不想允许构造时出现类的隐式类型转换,可以在拷贝构造前加个explicit关键字,就可以成功限制类的隐式类型转换了。
关于explicit的更多使用,在后面有机会还会讲。
成员变量缺省值
之前讲过,在C++11的新标准中,支持为类中的成员变量提供缺省值。在类和对象中,提供的缺省值是提供给初始化列表使用的。由于支持隐式类型转换构造等原因,提供的缺省值可以非常灵活,见代码:
class A
{
public:
A(int a1)
:_a1(a1)
{
cout << "A(int a1)" << endl;
}
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{
cout << "A(int a1, int a2)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
, _a2(aa._a2)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a1;
int _a2;
};
class B
{
public:
private:
int _b1 = 1;// 缺省值可以给整型变量
int* ptr = (int*)malloc(40);// 可以开空间给指针
A _aa1 = 1;// 可以给对象类型(A _aa1(1);这样构造是错误的)
A _aa2 = { 1,2 };// 多参数构造
A _aa3 = _aa2;// 拷贝构造,缺省参数甚至可以是一个对象
};
int main()
{
B bb1;
return 0;
}
这些缺省参数,最终都会提供给初始化列表。
如果显示提供了初始化列表,运行时,这些被提供的缺省参数就会被忽略(简单说就是:如果既提供了初始化列表,也有缺省值,编译器默认使用初始化列表提供的值)。
结语
本篇博客将最后两个默认成员函数做了一个收尾,再次谈到了构造函数的一些语法和特性,关于初始化列表的概念和使用;一种很新的创建对象方式,隐式类型转换方式创建对象,而explicit关键字可以限制这种转换的发生;最后还提到了C++11的新特性成员变量的缺省值,列出了对象,指针等类型给缺省值的方式。在类和对象的下一篇,会再介绍几个类和对象的小特性,以及编译器做出的优化。
博主还会继续产出有趣的内容,感谢大家的支持!♥