2.5 取地址及const取地址操作符重载
2.5.1 const成员
在介绍取地址和const取地址操作符重载前,需要先介绍一下const成员作为基础。在C++中,我们将使用const修饰的成员函数称为const成员函数。const看似修饰成员函数,实际则是对成员函数隐含的this指针进行修饰,被const修饰的this指针就不可以解引用改变值了,即该成员函数不能对类的成员做修改。
由于this指针不能显式写出,所以const就加在函数之后来表示修饰。
void Print2() const
{
cout << _year << endl;
}
//void Print2(const Date* this)
//{
// cout << _year << endl;
//}
const成员的说明
①因为函数内修改成员变量都是通过this指针完成的,所以当this被加上const限制,就说明该函数无法对成员变量做任何修改。
②注意调用传参情况下权限问题。权限的缩小和平移都是允许的,而权限的放大(由const变为非const)是不被允许的。所以const修饰的对象不可以调用非const修饰的成员函数。同理,const修饰的成员函数内也不允许调用非const修饰的成员函数。
③成员函数是否需要const修饰取决于其是否需要在函数内进行写操作。如果成员函数涉及到成员变量的写操作,则不可以加const修饰;如果只涉及到成员变量的读访问,则建议加const修饰。
class Date
{
private:
int _year;
public:
Date(int year)
{
_year = year;
}
//如果成员函数涉及到成员变量的写操作,则不可以加const修饰
//如果成员函数只涉及到成员变量的读访问,则建议加const修饰
void Print1()
{
cout << _year << endl;
this->Print2(); //非const修饰的成员函数调用const修饰成员函数,this指针由非const变为const,权限缩小
}
void Print2() const //const修饰this指针
{
cout << _year << endl;
//this->Print1(); //error:const修饰的成员函数this指针是const,调用非const修饰成员函数会使this指针权限放大
}
};
int main()
{
const Date d1(2022);
Date d2(2023);
//d1.Print1(); //error:(&d1)const Date* -> (this)Date*,权限的放大
d1.Print2(); // (&d1)const Date* -> (this)const Date*,权限的平移
d2.Print1(); // (&d2)Date* -> (this)Date*,权限的平移
d2.Print2(); // (&d2)Date* -> (this)Date*,权限的缩小
}
2.5.2 取地址及const取地址操作符重载
取地址操作符重载和const取地址操作符重载是类的6个默认成员函数的最后两个,因为重载了取地址操作符,所以其功能就是取出对象的地址。
因为是默认成员函数,所以当我们不实现时,编译器会自动生成。编译器自动生成的取地址操作符重载也是返回对象的地址。所以一般情况下,没有对取地址操作符有特殊要求,那么可以不用写,使用编译器默认生成的即可。
class Date
{
public:
//取地址及const取地址操作符重载
//一般不用自己写,采取编译器默认生成的,编译器生成的默认成员函数也是返回对象变量的地址
//特殊情况下才需要自己生成
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
};
int main()
{
Date d1;
Date d2;
cout << &d1 << endl;
cout << &d2 << endl;
return 0;
}
到此为止,我们便对六个默认成员函数进行了细致的解析,当然还有许多我们没有涉及到的细节点。在对类和成员函数有了大致认识后,接下来我们再来深入了解一下类中所涉及到的细节。
3. 构造函数——初始化列表
在最初介绍构造函数的时候,我们曾说对象在定义时会自动调用构造函数,并在构造函数内完成定义与初始化。这句话不能算是很精确,因为成员变量的定义实际并不是在构造函数体内完成的,而是在初始化列表中完成定义与初始化的。
初始化列表的说明
①初始化列表位于构造函数名之后,以一个冒号开始,接着是以逗号分隔的数据成员列表,每个成员变量后使用放在括号中的初始值或表达式来初始化。
class Date
{
private:
int _year;
int _month;
int _day;
public:
//初始化列表
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
void Print()
{
cout << _year << '-' << _month << '-' << _day << endl;
}
};
int main()
{
Date d1(2003,12,4);
d1.Print();
}
②首先理清定义发生的逻辑:列在类中的成员变量属于成员变量的声明,还没有定义。成员变量的定义发生在对象实例化后,调用了对象的构造函数,构造函数会先执行初始化列表,再执行函数体。而成员变量的定义正是发生在初始化列表中。
③初始化列表是成员变量完成定义的地方,所有成员变量无一例外都在也只能在初始化列表定义,尽管有的成员变量没有在初始化列表显式写出,定义和初始化依旧会发生。对于定义后初始化的值,(1)首先取决于初始化列表中成员变量之后的括号中的值,如果没有给出,那么(2)考虑缺省值,初始化列表初始化的缺省值在成员变量的声明处给出。如果以上两步均没有成功初始化,那么便会(3)初始化为随机值。
④所有变量都在初始化列表完成了定义与初始化,且在初始化列表每个成员变量只能出现一次(只能定义并初始化一次)。在构造函数体内部,则是修改赋值的部分,成员变量在此处可以多次修改赋值。
⑤需要区分清楚,在构造函数中涉及到两组缺省值。第一组是构造函数的参数缺省值,其作用就是参数缺省值的作用,当实参数量不够时才采用该缺省值赋值给形参。第二组是声明处给定的缺省值,其作用是为初始化列表提供初始化的缺省值,如果初始化列表没有给定初始化值,便会使用该缺省值初始化。
class Date
{
private:
//此部分属于成员变量的声明,还没有定义
//此处所写的缺省值是提供给初始化列表的,是成员变量定义的初始化值
int _year = 1999;
int _month = 10;
int _day;
const int _n;
public:
//在构造函数中,初始化列表部分才是每个成员变量定义初始化的位置
Date(int year,int month,int day) //构造函数参数缺省值和声明给定初始化缺省值不冲突,前者在调用构造函数传参缺省时才会启用,后者在初始化列表缺省时启用
//此处虽然看似只定义了_n和_month并初始化,实际上所以成员变量都在此定义了
:_n(1)
,_month(1) //没有给出_year成员的初始化值,采取缺省值,初始化为1999
//给出了_month成员的初始化值,直接初始化为1
//没有给出_day成员的初始化值,且_day成员没有缺省值,则初始化为随机值
//给出了_n成员的初始化值,直接初始化为1
//在构造函数的函数体内就不再是定义初始化了,而是赋值修改
{
_year = year;
_month = month;
_day = day;
//_n = 1; //error:定义不发生在构造函数函数体,所以此处认为是赋值,给const赋值所以报错
}
void Print()
{
cout << _year << '-' << _month << '-' << _day << endl;
}
};
int main()
{
//在对象实例化的时候定义了对象整体,而对象的成员定义则要通过调用构造函数来定义
Date d1(2003,12,4);
d1.Print();
}
⑥在定义成员变量时,建议能用初始化列表,就用初始化列表,因为初始化列表是不可避免会执行的,所以建议尽量多使用初始化列表。
⑦在定义时必须初始化的变量必须使用初始化列表(与其说必须使用,不如说必须显示写出,因为要被人为初始化就必须显式写出):const修饰的成员变量;引用成员变量;自定义类型成员(且不存在默认构造函数时)。这三种都是需要在定义时就完成初始化的,所以需要必须要在初始化列表显式写出。
重点解释一下不存在默认构造函数的自定义成员。
首先明确默认构造函数的定义:不需要传参的构造函数就是默认构造函数,编译器自动生成的构造函数的类型就是默认构造函数。
如果类的成员变量中有自定义类型成员,那么在初始化列表定义时,当定义到该自定义类型成员时,会调用它的构造函数。如果该自定义类型存在默认构造函数,那么就说明无需传参也可以调用构造函数,因此不需要手动完成,可以不在初始化列表显式定义。但是如果自定义类型中没有默认构造函数,即构造函数需要手动传参,那么就必须在初始化列表显式定义了。
总结一下,自定义对象是一定会在初始化列表定义的,初始化时调用其自身的构造函数。而是否存在默认构造函数,只决定是否允许不在初始化列表显式写出定义。
class A
{
public:
A(int a)
{}
};
class C
{
public:
C(int a = 1)
{}
};
class B
{
private:
int _b;
const int _n;
int& _ref;
A _aa;
C _cc;
public:
B(int b, int n, int& ref)
:_b(b)
,_n(n)
,_ref(ref)
,_aa(3) //A类因为实现了需要传参的构造函数,所以对A类对象初始化需要手动传参,所以需要在初始化列表显式定义
//C类因为存在默认构造函数(不需要传参的构造函数),所以无需手动操作,可以不在初始化列表显式定义
//类对象是一定会在初始化列表定义的,初始化时调用其自身的构造函数
//而是否存在默认构造函数,只决定是否允许不在初始化列表显式写出定义
{}
};
int main()
{
int num;
B b1(1, 2, num);
}
⑧初始化列表的初始化值只限制是可以赋值的右值即可。
class A
{
private:
int* p = nullptr;
public:
A()
:p((int*)malloc(sizeof(int) * 10))
//x(y) 相当于是 x=y,同理此处是p=(int*)malloc(sizeof(int) * 10),括号内应该是一个值,缺省值同理
{
if (p == NULL)
{
perror("malloc fail");
}
}
};
⑨以下介绍另一个构造函数的特性:单参数(或只有第一个参数无缺省值)构造函数支持隐式类型转换。
如果类对象的构造函数仅有一个参数,那么就支持隐式类型转换。例如如下代码,当写下A a2 = 2; 的代码后,由于A类的构造函数是单参数,所以赋值号右侧的2就会发生隐式类型转换,将类型变为A类的对象类型,这和使用强制类型转换 (A)2; 的效果一致。此时2变成了A类的临时对象(因为发生了隐式类型转换所以是临时的),在赋值给a2,就是调用了拷贝构造将该临时对象拷贝给了a2。
class A
{
private:
int _a;
public:
A(int a)
:_a(a)
{}
};
class B
{
private:
A _aa = 3;//由于单参数构造函数支持隐式类型转换,便可以顺利给出类对象缺省值
};
int main()
{
A a1(1);
//单参数(或只有第一个参数无缺省值)构造函数支持隐式类型转换
A a2 = 2; //A a2 = (A)2;
//构造函数参数需要int,所以实际是2先构造了一个A类的临时对象,再将这个临时对象通过调用拷贝构造给a2
//实际执行时,同一个表达式连续步骤的构造编译器会进行优化,此处将最后的拷贝构造优化掉了
const A& ra1 = 3; //3隐式转换为A类型的临时对象,具有常性从而可以被常引用引用
//A& ra2 = 4; //error:发生隐式转换后的临时对象具有常性,权限缩小
}
在C++11中规定,支持了多参数隐式类型转换。
class A
{
public:
A(int a,int b)
{}
};
int main()
{
A a1 = { 1,2 };//多参数隐式类型转换 C++11标准规定
}
⑩explicit关键字修饰构造函数,可以禁止上一点中介绍的类型转换。
class A
{
public:
A(int a,int b)
{}
};
int main()
{
A a1 = { 1,2 };//多参数隐式类型转换 C++11标准规定
}
⑪构造函数初始化列表中定义成员变量的顺序是按照成员变量声明的顺序进行的,而不是初始化列表的顺序。
class A
{
private:
int _a2;
int _a1;
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
};
int main() {
A aa(1);
aa.Print(); //输出:1 随机值
//规定初始化顺序按照声明顺序初始化,并不是按照初始化列表顺序
}