目录
this指针
this指针是C++类和对象中的一个关键,后面的运算符重载,以及很多知识都涉及到this指针。
引出:
bool Date::Change(int year, int month, int day) {
if(CheckDate(year,month,day)) {
_year = year;
_month = month;
_day = day;
return true;
}else{
return false;
}
}
如上是一个日期类的修改日期的函数,如果date1 和 date2分别调用此函数,那么这个函数是如何正确修改对应对象的数据成员_year _month _ day的呢? 就是因为this指针。
概念:
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
示例:
所以上述示例代码编译器会处理为:
bool Date::Change(Date* const this, int year, int month, int day) {
if(this->CheckDate(year,month,day)) {
this->_year = year;
this->_month = month;
this->_day = day;
return true;
}else{
return false;
}
}
而我们的调用语句,编译器会处理为
Date d1(2022,8,10);
Date d2(2021,3,1);
d1.Print(&d1);
d1.Change(&d1,2022,2,22);
d2.Change(&d2,2021,1,1);
由上可知,任何一个非静态成员函数,调用时必须由某个类实例化对象调用,而调用后,就会隐式地在第一个参数位置传递这个对象的地址。而形参列表的第一个也是一个隐式的Date* const this。由此才能对某个对象的数据成员进行准确的读写操作!
注意点:
1. 实参和形参部分,不能显式地传递和接收this指针,这是编译器隐式进行的操作。
2. 在函数内可以显式地使用this指针,去调用其数据成员或者成员函数,如果不显式写,在每个数据成员和成员函数前,编译器会隐式处理为this->
3. this指针只能在成员函数内部使用
4. this指针默认为常量指针,也就是this的指向不可以改变。
5. const对象的this指针为const X* const this,代表此this指向的对象的数据不可以被改变,即指向常量对象的常量指针。 这就会引出const成员函数等一系列问题。后面再详细说
6. 如果成员函数被定义为const成员函数,则表示此函数不会改变this指向的对象的数据,则this指针变为const X* const this ,而对应第5点,可知,const对象只能调用const成员函数,因为指向常量的this指针如果传递给一个非const成员函数,就属于权限的方法,是非法的。(后面再详细说const成员函数,其实内容并不多)
7. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
this指针存储在哪里?
this指针并不存储在对象内部,因为this指针是作为参数传递的,所以在函数栈帧内,也就是栈区。但是这个也不绝对,因为this指针是一个使用频繁的参数,有些编译器会将使用频繁的变量进行优化,将其存储在寄存器中,不过这是属于编译器的优化行为。
一个经典题
class A
{
public:
void Print()
{
printf("%p\n",this);
// cout<<this<<endl;
cout<<"Print()"<<endl;
}
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
p->PrintA();
return 0;
}
问,如果单独运行p->Print(); 和 p->PrintA(); 分别是什么结果?
答: Print()函数会正常运行,因为这里的p虽然是空指针,但是我们调用Print函数时,并没有解引用它,因为函数代码根本不存在对象内,是存储在公共代码区里的,通过指针得知其指向A类型对象,然后到对应的区域调用Print()函数,打印出00000000 和 Print() (这里也会把this指针作为第一个参数隐式传递过去)
PrintA();函数会运行崩溃,因为执行函数内代码cout<<_a<<endl;时,_a为this->_a ,会解引用this指针,因为_a是存储在对象内的。后发觉这是一个空指针,就会崩溃。(这并不是编译时报错,而是运行时)
六个默认成员函数👇
默认成员函数:用户没有显式实现时,编译器自动生成的成员函数称为默认成员函数。
下面介绍六个默认成员函数,默认成员函数即每个类默认都会有的成员函数,它们各自有不同的作用,如果你不显式实现,那么编译器会自动实现一个默认的。分别是:构造函数,析构函数,拷贝构造函数,拷贝赋值运算符重载函数,&运算符重载函数,const版&运算符重载函数。其中前四个为重点,最后两个了解即可。
学习这些函数时,需要针对几个点进行学习
这些函数各自的作用,什么时候调用,编译器默认生成的能够完成什么工作,什么时候需要我们自己实现。 暂时想不到其他的了,剩下的就是一些细节的点了
一、构造函数
简介与作用
构造函数的存在的意义是什么呢? 比如我们用C语言写一个Stack,那么这个Stack创建出变量/对象之后,要调用Init进行初始化。其实构造函数的作用就是,帮助我们进行更方便地初始化对象。因为每个类实例化对象时,都会由编译器自动调用配对的构造函数,(无论是编译器自己生成的还是我们写的,无论是有参还是无参)这样就保证了对象的初始化工作,并且更加的方便。(一个类如果没有构造函数,则无法创建对象)
所以说,构造函数并不是用来构造/创建对象的,而是初始化对象的!
基本语法与一些规则
1. 构造函数无返回值,函数名与类名相同(不想说了)
2. 构造函数在创建对象时由编译器自动调用,且必须调用一个,无论哪个版本
3. 构造函数可以重载,以适应不同的初始化情形。
默认构造函数
默认构造函数只能有一个,(默认构造函数是不传参时调用的那个构造函数),为了避免歧义,所以只能有一个。
默认构造函数包括:
a、我们不实现任意构造函数时,编译器自动创建的那个
b、我们写的无参构造函数 Date() {}
c、我们实现的,所有参数都有默认值/缺省值的构造函数 Date(int year = 1, int month = 1, int day = 1) {}
这三个,一个类中只能存在一个。 (一旦用户显式定义任何一个构造函数,编译器都不会再生成那个默认构造函数)建议每个类都实现一个默认构造函数,原因暂时略了。
构造函数还有几个需要注意的点,如初始化列表,成员变量声明处的默认值。编译器自动生成的默认构造函数完成了什么工作。
Date类的初步实现,主要关注其中的两个构造函数
class Date
{
public:
// 我们主动实现的全缺省默认构造函数
Date(int year = 1, int month = 1, int day =1)
: _year(year),_month(month),_day(day){
assert(CheckDate(_year,_month,_day)); // 断言此检查为真,如果为假则报错。
}
// 构造函数重载
Date(int month,int day)
:_month(month),_day(day) {
_year = 2022;
}
~Date() = default;
Date(const Date& d) = default;
Date& operator=(const Date& d) = default;
void Print()const;
static bool CheckDate(int year,int month,int day);
bool Change(int year,int month, int day);
static int GetDayOfMonth(int year,int month); // 这里设置为静态的,主要是因为函数内部没有使用this
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
初始化列表
初始化列表对于理解构造函数非常重要。 理解初始化列表的作用,它和构造函数函数体的功能的区别!!
1. 每个构造函数都存在初始化列表,如果我们不显式写,就会有一个默认的。且每次执行构造函数都会执行初始化列表
2. 初始化列表的作用是初始化数据成员,构造函数函数体的作用是给数据成员赋值。他们是有本质区别的
3. 如果我们不写初始化列表,它会执行默认的。默认的对于基本类型会初始化为随机值,自定义类型会调用其默认构造函数进行初始化。 这里和编译器自动形成的默认构造函数的作用也有关系,因为那个构造函数的作用就是靠它的初始化列表实现的!!
4. 由上我们可以知道,能使用初始化列表就使用,除非一些特定的操作必须函数体内执行。
(有点烦)