W...Y的主页
代码仓库分享
前言:
关于C++的博客中,我们已经了解了六个默认函数中的四个,分别是构造函数、析构函数、拷贝构造函数以及函数的重载。但是这些函数都是有返回值与参数的。提到参数与返回值我们就会想到可以修饰它们的一个关键字const。而且关于构造函数,我们并没有将内容全部讲完,所以我们今天这篇博客就是对const关键字的讲解以及构造函数的补充!话不多说,我们直接开始。
目录
const成员
取地址及const取地址操作符重载
再谈构造函数
初始化列表
const对于我们有语言基础的人并不陌生,就是关于修饰变量使其成为一个不可修改的内容。在C++中也是如此,但是C++中类的出现,伴随的出现的就是一系列的成员函数,而被const修饰的成员函数就是const成员函数。
我们来看一下这段代码:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022,1,13);
d1.Print();
const Date d2(2022,1,13);
d2.Print();
}
这是为什么呢?当我们使用const修饰d2时,d2的类型就是const Date类型,而我们去调用print函数去打印时,print隐藏的函数参数其实是Date* const this,所以参数不匹配导致程序报错。
那this指针的参数应该怎么是隐藏的,所以C++规定在函数后加上const的实际意义就是在this指针前加const。
所以正确的print函数应该在函数后加上const进行修饰。
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
这样无论在自定义类型的前面是否加上const进行修饰,都可以对上述函数进行调用。所以在调用时,我们可以将一个变量的权限放小,但是绝不能进行放大。
随之又会引出一个问题:成员函数有const进行修饰,无论实参有无const都能进行调用,那我们需不需要将所以的成员函数都加上const呢?
其实是不用的,我们加上const进行修饰的this指针指向的内容不被修改,如果我们的成员函数需要修改this指针所指向的内容,我们就不用去加const。
Date operator++(int);
Date& operator+=(int day);
Date& operator-=(int day);
Date& operator++();
Date& operator--();
Date operator--(int);
比如上述的运算符重载就不用加const,因为这些都是改变this指向的内容的。
bool Date::operator>(const Date& y)
{
if (_year > y._year)
{
return true;
}
else if (_year == y._year && _month > y._month)
{
return true;
}
else if (_year == y._year && _month == y._month && _day > y._day)
{
return true;
}
return false;
}
int main()
{
Date s1();
const Date s2();
s1 < s2;//正确
s2 < s1;//报错
return 0;
}
上述代码是<的运算符重载,在之前的博客中我们已经进行了复现,但是当我们的参数类型一个被const修饰,另一个没有const修饰当我们调用此函数s2 < s1时就会出现报错,因为不能将实参的权限进行放大,也就是参数类型不匹配,所以这种类似内容的函数就必须加上const进行修饰。
void Print() const;
bool operator==(const Date& y) const;
bool operator!=(const Date& y) const;
bool operator>(const Date& y) const;
bool operator<(const Date& y) const;
bool operator>=(const Date& y) const;
bool operator<=(const Date& y) const;
int operator-(const Date& d) const;
Date operator+(int day) const;
Date operator-(int day) const;
总结:
1.能定义const的成员函数都应该定义成const,这样const成员与非const成员都可以进行调用。调用条件(权限平移)(权限缩小)。
2.要修改成员变量的函数不能定义const。
取地址操作运算符重载也是六大默认函数之一,通过重定义对对象进行取地址操作就是取地址操作符的重载。这两个默认成员函数一般不用重新定义 ,编译器默认会生成。为什么会是两个呢?因为有无const是有区别的,他们会形成函数重载。
Date* operator&()
{
cout << "Date* operator&()" << endl;
return this;
}
const Date* operator&()const
{
cout << "const Date* operator&()const" << endl;
return this;
}
int main()
{
// const对象和非const对象都可以调用const成员函数
const Date d1(2023, 10, 31);
d1.Print();
Date d2(2023, 1, 1);
d2.Print();
cout << &d1 << endl;
cout << &d2 << endl;
return 0;
}
这里许多人就会有疑问,这里不会产生二义性吗?针对cout << &d2 << endl;因为d2没有被const修饰,所以既可以调用理论上来说两个函数都可以进行调用。但是C++会优先匹配最合适的类型,因为d2没有被const进行修饰,所以优先会调用没有被const修饰的函数。
如果将没有const修饰的函数进行屏蔽,两种实参照样可以进行调用。
之前我们就讲过构造函数已经将了有80%了,现在我们将构造函数中剩下的20%进行收尾。我们先来复习一下之前的构造体系:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
这是函数体内初始化,我们进行对象的初始化时就会调用此函数,当我们没有构造函数时,我们就会调用C++提供的默认构造函数进行匹配。
构造函数的特征:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量
的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始
化一次,而构造函数体内可以多次赋值。
现在我们还有一种可以初始化的办法:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
,_ref(year)
,_n(1)
{
// 初始化列表
}
这样的初始化我们称之为初始化列表。
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟
一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
这样写我们照样可以进行初始化。这两种方法都可以进行初始化,他们的区别在哪呢?
上述的例子使用两种初始化都可以,但是有些成员变量就只能使用初始化列表进行初始化。因为在类中私有成员都只是声明,没有开辟空间,而特殊的成员变量只能在定义的时候进行赋值,比如:引用、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
};
所以引用成员变量、const成员变量、自定义类型成员(且该类没有默认构造函数时)都要进行初始化列表赋值。
当我们去定义一种自定义类型时,如果没有对应的构造函数,程序就会报错。所以当我们定义一个类嵌套在另一个类时,在创建类的构造函数时创建成全缺省参数的构造函数。
下面给大家看一个题:
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout<<_a1<<" "<<_a2<
这道题应该选D,这是为什么呢?成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关 ,所以_a2是在私有成员中先声明的,所以在初始化中先定义_a2,因为_a1在后面所以先为随机值,所以_a2为随机值,_a1为1.
最后我们来总结一下初始化列表解决的问题:
1、必须在定义的地方显示初始化 引用 const 没有默认构造自定义成员
2、有些自定义成员想要显示初始化,自己控制
3. 尽量使用初始化列表初始化
4. 构造函数能不能只要初始化列表,不要函数体初始化
答:不能,因为有些初始化或者检查的工作,初始化列表也不能全部搞定
class Stack
{
public:
Stack(int n = 2)
:_a((int*)malloc(sizeof(int)* n))
, _top(0)
, _capacity(n)
{
//...
//cout << "Stack(int n = 2)" << endl;
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
memset(_a, 0, sizeof(int) * n);
}
当我们进行动态内存开辟时,我们就需要进行函数内外的配合,因为在初始化列表中不能进行其他操作,而在函数体内可以,为了避免开辟失败,我们需要进行指针的检查,以及其他操作。所以80-100%初始化列表搞定,还有需要用函数体,他们可以混着用
以上就是本次全部内容,感谢大家观看!!!