一。this指针
1> this指针作为成员函数参数传参
this指针是编译器在调用类非静态成员函数的时候隐式传递给函数的(作为第一个参数),在传递this指针的时候有两种方式:
调用约定一般规定以下几个方面的内容:
1) 函数参数的传递方式,是通过栈传递还是通过寄存器传递
2) 函数参数的传递顺序,是从左到右入栈还是从右到左入栈。
3) 参数弹出方式。函数调用结束后需要将压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由调用方来完成,也可以由被调用方来完成。
4) 函数名修饰方式。函数名在编译时会被修改,调用惯例可以决定如何修改函数名。
下面是几个常见的几个调用约定:
如果类成员函数的参数个数已知,那么采用的调用约定是_thiscall,在visual c++编译器中这里是通过ecx寄存器传递
如果类成员函数采用的是不定参数,那么采用的调用约定是_cdecl
2>this指针的特性
- this指针的类型:类 类型 *const
即this指针的类型是指向不可改变的类类型 - this指针并不是对象本身的一部分,this指针一般存放在寄存器中,即不会影响sizeof的结果
- this指针是“类成员函数”的第一个默认参数,由编译器自动维护传递,类编写者不能显式传递
3>this指针可以用对象的引用替换呢?
答案是可以的,因为引用的底层实现就是指针。那为什么不用引用替换this指针呢?由于c++语言在不断更新,之前C++语言还没有设计出引用的时候,就已经诞生了this指针,后面也一直没有替换。
4>this指针可以为NULL吗?
答案是可以的,看下面这段代码:
class Test
{
public:
void FunTest()
{
cout<<"FunTest():"<<this<<endl;
}
};
int main(void)
{
Test *pt=NULL;
pt->FunTest();//①
return 0;
}
运行结果:FuncTest():NULL
解释:为什么这段代码可以运行成功呢?①行代码不是对pt进行解引用吗?这样难道不会崩溃吗?如果你有这个问题,说明你还没有理解this指针的本质。
①行代码的意思是通过类的指针调用当前类的成员函数,只是当前类的指针指向NULL而已,所以也会打印出NULL。对比下面这段代码相信你就会彻底明白:
class Test
{
public:
void FunTest()
{
this->m_data=0;
cout<<"FunTest():"<<this<<endl;
}
private:
int m_data;
};
int main(void)
{
Test *pt=NULL;
pt->FunTest();
return 0;
}
/*这段代码就会运行失败,原因就在于当前类类型的指针指向空,而FunTest()函数试图访问this指向的空间,当然就会出错了!*/
空类的大小是多少?
0吗?答案是否定的。
空类的大小是1。原因看下面这段代码:
class Test{};
int main()
{
Test t1;
Test t2;
}
//用空类定义了两个对象,这两个对象地址相同吗?当然是不同的
//为了区分它们在内存中地址的不同,所以就让空类的大小为1,而不是0。
二。构造函数
1> 构造函数的特性
- 没有返回值
- 函数名与类名重名
- 创建新对象时由编译器自动调用,且在对象的生命周期内仅调用一次
- 构造函数可以重载
- 没有参数的构造函数和有缺省值的构造函数都可以作为缺省的构造函数,但是一个类中只能存在两者其一(为了避免歧义)
- 如果类的编写者没有显式定义构造函数,由编译器自动合成一个缺省构造函数
- 构造函数不能为const类型
- 构造函数不能为虚函数
2> 构造函数的作用
1. 构造 并 初始化对象,这其中有一些需要注意的地方:
1.初始化列表
class Test
{
private:
int _data1;
int _data2;
public://下面的构造函数就使用了初始化列表
Test(int data1,int data2,int data2):_data1(data1),
_data2(data2)
{}
}
注意:
- 每个成员在初始化列表中只能出现一次
- 初始化列表中成员的初始化顺序和它们在初始化列表中的排列顺序无关,而是由它们在类中的声明顺序决定的。
- 避免用类成员变量来初始化类的成员变量,并且初始化的顺序最好和类中成员变量的声明顺序一致(高质量编程)
- 类中的以下成员一定要在初始化列表中完成初始化
1.引用类型成员变量
因为引用类型的变量具有”从一而终”的特点,所以在初始化对象的时候就要同时初始化该类型的变量,否则以后也无法给其赋值。
2.const类型成员变量(原因同上)
3.当类中有类类型的对象成员(该对象具有非缺省构造函数)时,在类的构造函数和拷贝构造函数的初始化表中要调用对象成员的构造函数初始化该对象成员。
2. 类型转换
类型转换函数和转换构造函数后面专门用一篇博客来讲,这里简单提一下
对于单个参数构造函数,可以将其接收参数转化成类类型的对象,用explicit关键字修饰构造函数,可以抑制有构造函数定义的隐式类型转换(如果类的构造函数放在类的外部来定义,而在类的声明已经用explicit关键字修饰,可以不用在构造函数外部定义上重复修饰)
三。拷贝构造函数
1.概念
只有单个形参,而且该形参是对本类类型对象的引用(用const修饰),这样的构造函数称为拷贝构造函数。如果在类中没有显式定义,则编译器会合成一个缺省的拷贝构造函数。在初始化已经存在对象的时候,由编译器自动调用拷贝构造函数。
2.调用时机
- 当函数返回值为类类型
- 当函数的参数为类类型
- 创建对象的时候用另一个对象初始化新对象
注:下面对这个几个调用时机进行讲解
class test{
//代码块
}
test func(test c){
test a;
return a;
}
int main(void)
{
test b;
b=func();
return 0;
}
1)当函数返回值为类类型
解析:在func函数中,定义了一个类test类型的对象,而我们清楚对象的生命周期在它函数执行结束的时候就会终止,所以我们定义的这个a是不会返回去的,换句话说,b对象不是由a对象赋值的,那b是谁来赋值的呢?事实上,在return a;
这行代码中包含了两次函数调用,一次是拷贝构造函数,由编译器自动创建了一个匿名的对象,然后用a对象初始化这个匿名对象,还有一次是调用析构函数析构掉a对象。
2) 当函数参数是类类型
在传参的时候,编译器创建了一个临时的匿名对象,并且调用拷贝构造函数用实参初始化这个匿名对象,事实上,这也解释了为什么拷贝构造函数为什么参数是对象的引用类型而不是对象类型,因为如果拷贝构造函数的参数是对象,那么就会进入没有出口的递归中去:传参,调用拷贝构造函数,传参,调用拷贝构造函数……
3)创建对象的时候用另一个对象初始化新对象
这里唯一需要区分的就是,它容易和赋值运算符重载函数的调用搞混!不过,你只要记得如果‘=’左边的对象不曾存在(或是之前被析构掉),调用的就一定是拷贝构造函数,否则就是赋值运算符重载函数。
四。析构函数
1.析构函数的定义格式
~类名()
{函数体}
2.析构函数的 作用
完成对对象的收尾工作,一般是对对象内申请内存的释放。注意:析构函数的作用不是删除对象。
代码示例:
#include<iosream>
using namespace std;
class Human
{
private:
char *_address;
char *_name;
public:
Human(char *name,char *address)
{
_address=new char[50]{0};
if(!_address)
{
return;
}
_name=new char[15]{0};
if(!_name)
{
return;
}
strcpy(_address,address);
strcpy(_name,name);
}
~Human()
{
delete []_address;
delete []_name;
_address=NULL;
_name=NULL;
}
}
3.注意
- 对象生命周期结束时,C++编译系统自动调用析构函数
- 一个类只能有一个析构函数,若未显式定义,则系统会缺省生成一个析构函数
- 析构函数无参,无返回值