一、this指针:
this指针特性
this指针的类型:类类型* const this指针并不是对象本身的一部分,不影响sizeof的结果 this的作用域在类”成员函数”的内部 this指针是”类成员函数”的第一个默认隐含参数,编译器自动维护 传递,类编写者不能显式传递 只有在类的非静态成员函数中才可以使用this指针,其它任何函数都 不可以
__thiscall调用约定:
__thiscall只能够用在类的成员函数上 参数从右向左压栈 如果参数个数确定,this指针通过ecx传递给被调用者;如果参数不 确定(_cdecl),this指针在所有参数被压栈后压入堆栈 对参数个数不定的,调用者清理堆栈,否则函数自己清理堆栈
问题: 1. 引用底层也是指针,此处为什么不是引用,而使用this指针呢? 2. this指针是否可以为空?
一个成员函数看起来有n个参素,实际上有n+1个参数,还有一个隐藏的this指针,由编译器传,因为是编译器自动编译的。
this指针传参的方式:
(1)ecx寄存器: 用this_call这种调用约定。
(2)参数压栈:函数接受参数的个数是不确定的,使用_cdecl这种调用约定,而不是this_call
this指针可能为空,只要在当前函数里面没有访问成员变量或者数据,则程序一定不会崩溃
class Test
{
public:
void FunTest()
{
cout << this << endl;
//_data = 10;
}
int _data;
};
int main()
{
Test t;
t.FunTest();
Test *pt = &t;//mov ecx pt
pt->FunTest();//call Test::FunTest()参数为编译器默认this指针的地址
pt = NULL;
pt->FunTest();//把pt的内容当成函数的一个参数,//Test::FunTest(pt(NULL))this指针为空
return 0;
}
二、构造函数:(编译器自己感觉需要时再去合成默认的构造函数)
1、构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时,由编 译器自动调用,在对象的生命周期内只调用一次,保证每个数据成员都有 一个合适的初始值。
2. 构造函数特性
函数名与类名相同
没有返回值
新对象被创建时,由编译器自动调用,且在对象的声明周期内仅调用 一次
构造函数可以重载,实参决定了调用那个构造函数
无参构造函数和带有缺省值的构造函数(全缺省)都认为是缺省的构造函数,并 且缺省的构造函数只能有一个
有初始化列表(可以不用)
如果没有显式定义时,编译器会合成一个默认的构造函数
构造函数不能用const修饰(为什么?)
构造函数不能为虚函数(为什么?)
3. 对象初始化 初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表, 每个”成员变量”后面跟一个放在括号中的初始值或表达式
注意:
每个成员在初始化列表中只能出现一次(为什么?)
初始化列表仅用于初始化类的数据成员,并不指定这些数据成员的初 始化顺序,数据成员在类中定义顺序就是在参数列表中的初始化顺序
尽量避免使用成员初始化成员,成员的初始化顺序好和成员的定义 顺序保持一致
类中包含以下成员,一定要放在初始化列表位置进行初始化:
引用成员变量
const成员变量
类类型成员(该类有非缺省的构造函数)
4. 构造函数作用
构造&初始化对象
类型转换 对于单个参数构造函数,可以将其接受参数转化成类类型对象。用 explicit修饰构造函数,抑制由构造函数定义的隐式转换,explicit 关键字类内部的构建声明上,在类的定义体外部的定义上不再重复
class Data
{
public:
Data(int year)
:_year(year)
{
cout << "Data(int):" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2018);
d1 = 2019;//左操作数为日期对象,右操作数为整型
//先把2019转化为临时对象,以2019作为构造函数的参数,用临时对象给d1赋值。
//转化的前提是类的构造函数是单参的。
return 0;
}
尽量不要使用隐式类型转化,可能产生意向想不到的错误加上关键字explicit
class Data
{
public:
explicit Data(int year)
:_year(year)
{
cout << "Data(int):" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2018);
d1 = 2019;//左操作数为日期对象,右操作数为整型
//先把2019转化为临时对象,以2019作为构造函数的参数,用临时对象给d1赋值。
//转化的前提是类的构造函数是单参的。
return 0;
}
加上exlicit后隐式转换时会报错。
合成构造函数的前提:(1)、如果A类中包含了B类的对象,B类有缺省的构造函数,A没有显式自己的构造函数。
三、拷贝构造函数:是构造函数的重载。
只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这 样的构造函数称为拷贝构造函数。拷贝构造函数是特殊的构造函数,创建 对象时使用已存在的同类对象来进行初始化,由编译器自动调用
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int):" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
int a = 10;
int b(10);
int c(b);
Date d1(1028, 1, 1);
Date d2(d1);//用已经存在的d1创建一个新的实体。只要创建新的对象就要创造实体
return 0;
}
要求:
(1)、函数参数是对类类型的引用并且只能有一个参数,不需要通过参数改变外部实参的话,最好加上const。函数体里面可以打印this的地址。this可以用于函数体中,不可以用于初始化列表。
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int):" << this << endl;
}
Date(const Date &d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date &):" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
int a = 10;
int b(10);
int c(b);
Date d1(1028, 1, 1);
Date d2(d1);//用已经存在的d1创建一个新的实体。只要创建新的对象就要创造实体
return 0;
}
this可否用于初始化列表之中?
this不可以用于初始化列表中,以下会产生错误:
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int):" << this << endl;
}
Date(const Date &d)
:_year(d._year)
, this->_month(d._month)
, _day(d._day)
{
cout << "Date(const Date &):" << this << endl;
}
产生错误的原因是:函数体里面是进行赋值,初始化列表是对类中的各个成员变量进行初始化,上述初始化的位置对象不完整,所以不能使用this。
(2)应用场景:调用构造函数和拷贝构造函数的相同点是都创建新的对象,不同点是:拷贝构造函数是用已经存在的对象创建新的对象,其他位置调用的都是构造函数。
1’对象实例化对象
2' 作为函数参数
3'作为函数返回值
引用和传值两种方式调用构造和拷贝构造函数的过程:
1'、传值
class Date
{
public:
Date(int year=290,int month=11,int day=1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int):" << this << endl;
}
Date(const Date &d)
:_year(d._year)
,_month(d._month)
, _day(d._day)
{
cout << "Date(const Date &):" << this << endl;
}
void SetDay(int day)
{
_day = day;
}
int GetDay()
{
return _day;
}
private:
int _year;
int _month;
int _day;
};
Date TestDate(Date d)
{
Date tmp(d);
tmp.SetDay(tmp.GetDay() + 1);
return tmp;
}
void Test()
{
Date d1(299, 1, 4);//1、调构造函数,构造出d1;
Date d2;//2、创建d2;
d2 = TestDate(d1);
//执行:1、调构造函数,构造出d1;2、创建d2;
//d2 = TestDate(d1):3、因为Date TestDate(Date d)以值的方式传递,生成一个临时对象,用已经存在的
//对象产生新的对象,调用拷贝构造函数
//4、TestDate(Date d)函数体里面 Date tmp(d):调用拷贝构造函数
//5、 tmp.SetDay(tmp.GetDay() + 1); return tmp;tmp以值的方式返回需要创建临时对象
//再调用一次拷贝构造函数
}
2'、引用:
class Date
{
public:
Date(int year=290,int month=11,int day=1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int):" << this << endl;
}
Date(const Date &d)
:_year(d._year)
,_month(d._month)
, _day(d._day)
{
cout << "Date(const Date &):" << this << endl;
}
void SetDay(int day)
{
_day = day;
}
int GetDay()
{
return _day;
}
private:
int _year;
int _month;
int _day;
};
Date &TestDate(Date &d)
{
Date tmp(d);
tmp.SetDay(tmp.GetDay() + 1);
return d;//引用的声明周期一定要比函数的声明周期长,引用变量的声明周期比函数长
//因为引用相当于对实参取别名,实参的声明周期比函数的声明周期长,所以不能直接return tmp
//d的声明周期比tmp长,tmp会销毁。
}
void Test()
{
Date d1(299, 1, 4);//1、调构造函数,构造出d1;
Date d2;//2、创建d2;
d2 = TestDate(d1);
//执行:1、调构造函数,构造出d1;2、调用构造函数创建d2;
//3、调用d2 = TestDate(d1):
//Date tmp(d):因为该函数以引用接收,不需要创建临时对象,调用拷贝构造函数创建tmp
//tmp.SetDay(tmp.GetDay() + 1);临时对象加1,return d;后返回d1,不需要创建临时对象
//d2 = TestDate(d1);赋值给d1
}
用已经存在的对象创建新的对象,叫做拷贝构造函数,其他创建新对象的方式都为构造函数。
(3)特性:
1'、构造函数的重载,构造函数的性质拷贝构造函数均满足 (拷贝构造函数是构造函数的一种特殊形式,所以构造函数的特性拷贝构造函数都满足。除了拷贝构造函数不能重载)
2'、参数必须使用类类型对象引用传递(为什么?)
若是不使用引用函数的调用过程如下:传参的位置一直调用拷贝构造函数,导致栈溢出。所以拷贝构造函数的参数一定是类对象的引用,这样不会出现栈溢出。
3'、如果没有显式定义,系统会自动合成一个默认的拷贝构造函数。默认 的拷贝构造函数会依次拷贝类的数据成员完成初始化
class Time
{
public:
Time(int hour=0, int minute=0, int second=0)
:_hour(hour)
, _minute(minute)
, _second(second)
{}
//拷贝构造函数
Time(const Time& t)
:_hour(t._hour)
, _minute(t._minute)
, _second(t._second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int):" << this << endl;
}
Date(const Date &d)
:_year(d._year)
,_month(d._month)
, _day(d._day)
{
cout << "Date(const Date &):" << this << endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
int a = 10;
int b(10);
int c(b);
Date d1(1028, 1, 1);
Date d2(d1);//d1拷贝构造d2,调用Date类的拷贝构造函数,为了调用Time类中的构造函数,用d1对象中的_t拷贝d2对象中的_t。编译器自己合成一个拷贝构造函数,(用已经存在的d1调用不存在的d2必须调用拷贝构造函数)
//
return 0;
}
//此时日期类虽然没有给拷贝构造函数,编译器自动合成
四、析构函数:
以下的程序,会产生空间无法销毁的情况:
typedef int DataType;
class SeqList
{
public:
SeqList(size_t capacity)
:_pData((int *)malloc(sizeof(DataType)*capacity))
, _capacity(capacity)
, _size(0)
{
}
void PushBack(const DataType &data)
{
_pData[_size++] = data;
}
private:
DataType *_pData;
size_t _capacity;
size_t _size;
};
void TestSeqList()
{
SeqList s(10);
s.PushBack(1);
s.PushBack(2);
s.PushBack(3);
//1、free(s._pData);//不能采用这种方式释放动态内存空间,因为_pData是私有的
//2、采用Get,Set方法来把私有的成员变为公有的成员,在这里不安全,若是让自己的空间
//被不坏好意的人知道,会产生意想不到的错误
}
int main()
{
TestSeqList();
return 0;
}
我们可以引入析构函数,销毁空间:
typedef int DataType;
class SeqList
{
public:
SeqList(size_t capacity)
:_pData((int *)malloc(sizeof(DataType)*capacity))
, _capacity(capacity)
, _size(0)
{
}
void PushBack(const DataType &data)
{
_pData[_size++] = data;
}
~SeqList()
{
cout << "~SeqList"<
if (_pData)
{
free(_pData);
_capacity = 0;
_size = 0;
}
}
private:
DataType *_pData;
size_t _capacity;
size_t _size;
};
void TestSeqList()
{
SeqList s(10);
s.PushBack(1);
s.PushBack(2);
s.PushBack(3);
}
int main()
{
TestSeqList();
return 0;
}
1、原理:析构函数:与构造函数功能相反,在对象被销毁时,由编译器自动调用,完成类 的一些资源清理和汕尾工作。(类中涉及到资源(打开文件、申请资源)一般会显示定义出析构函数,作用是释放资源)
2、特性:
(1)析构函数在类名(即构造函数名)加上字符~
(2)析构函数无参数无返回值 :析构函数没有参数(可以有void类型的参数),不能重载。
(3)一个类有且只有一个析构函数。若未显示定义,系统会自动生成缺省 的析构函数 (类中包含其他类型的对象,该对象包含析构函数,则会合成)
(4)对象生命周期结束时,C++编译系统系统自动调用析构函数
(5)注意析构函数体内并不是删除对象,而是做一些清理工作(不是删除对象的内存空间,仍在在个位置上,只有函数调用结束,才释放内存空间)
(6)编译器在一些情况下会自动合成拷贝构造函数,或者不合成拷贝构造函数,但是实现拷贝构造函数的功能。以下加入SeqList S2(S)会产生的问题:程序会崩溃。s与s2的内容相同,s与s2公用同一块空间,出了函数的时候会销毁,是栈上的变量,创建的晚先销毁,创建的早后销毁,所以销毁的时候,先销毁s2,s2指向的空间已被释放,s成为野指针。
typedef int DataType;
class SeqList
{
public:
SeqList(size_t capacity)
:_pData((int *)malloc(sizeof(DataType)*capacity))
, _capacity(capacity)
, _size(0)
{
}
void PushBack(const DataType &data)
{
_pData[_size++] = data;
}
~SeqList()
{
cout << "~SeqList"< SeqList s2(s);
}
int main()
{
TestSeqList();
return 0;
}
在当前日期的基础上加1000天:
方式一:会改变原先的值,相当于+=,不合适
class Date
{
public:
Date(int year=290,int month=11,int day=1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int):" << this << endl;
}
Date(const Date &d)
:_year(d._year)
,_month(d._month)
, _day(d._day)
{
cout << "Date(const Date &):" << this << endl;
}
Date &Add(size_t days)
{
_day += days;
return *this;//当前对象的声明周期比函数长
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(122, 3, 4);
d1.Add(5);//此方式会改变左边的值,相当于加等,所以不合适
return 0;
}
方式2:不改变左右操作数,用当前对象构造一个临时对象,将天数加到临时对象上,没有改变左右操作数的值,返回加后的结果。临时对象不能通过引用返回,声明周期不同。
class Date
{
public:
Date(int year=290,int month=11,int day=1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int):" << this << endl;
}
Date(const Date &d)
:_year(d._year)
,_month(d._month)
, _day(d._day)
{
cout << "Date(const Date &):" << this << endl;
}
Date Add(size_t days)
{
Date tmp(*this);
tmp._day += days;
return tmp;//当前对象的声明周期比函数长
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(122, 3, 4);
d1.Add(5);
return 0;
}
方式3:d1不能直接加上一个数字,因为有一个自定义的对象,运算符重载的方式,把加的规则告诉编译器:
class Date
{
public:
Date(int year=290,int month=11,int day=1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int):" << this << endl;
}
Date(const Date &d)
:_year(d._year)
,_month(d._month)
, _day(d._day)
{
cout << "Date(const Date &):" << this << endl;
}
Date operator+(int days)//运算符为+,有两个操作数。是成员函数,所以参数位置有一个隐藏的
//this指针,相当于有两个参数.+左边的为左操作数,-为为右操作数
//不能改变左右操作数,所以加到临时空间上
{
Date tmp(*this);
tmp._day += days;
return tmp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(122, 3, 4);
Date d2;
d2 = d1 + 5;
return 0;
}