this指针
引入:
我们先定义一个描述人体态特征的类,包含身高体重。
#include<iostream>
using namespace std;
class Person
{
public:
void Init(int stature, int weight)
{
_stature = stature;
_weight = weight;
}
void Print()
{
cout << _stature << ' ' << _weight << endl;
}
private:
int _stature;
int _weight;
};
int main()
{
Person p1;
Person p2;
p1.Init(180, 60);
p1.Print();
p2.Init(185, 65);
p2.Print();
return 0;
}
在这里我们定义了同一个类的两个不同对象p1和p2,但函数体中并没有对不同对象的区分,那么在调用同一个类的函数时,又是如何区别调用的是p1还是p2呢?
在C++中引入了this指针来解决这个问题。C++编译器给每个非静态成员函数增加了一个隐藏的this指针,让该指针指向该对象,在函数体中所有对成员变量的操作都是通过该指针去访问。不过不用用户自己去传,编译器自动完成。
this指针的特性
this指针的类型:类型* const,即成员函数中,不能给this指针赋值,this指针的指向是不能修改的。
只能在成员函数的内部使用。
this指针本质是成员函数的形参,当对象调用成员函数时,会将对象的地址作为实参传给this指针形参,所以对象中不存储this指针。(this指针一般是存在栈区的)
this指针是成员函数隐藏的第一个指针形参,不需要用户自己传递。
请看以下的代码:
class Basic
{
public:
void PrintBasic()
{
cout << "Print()" << endl;
cout << this << endl;
}
private:
int _a;
};
int main()
{
Basic* p = nullptr;
p->PrintBasic();
return 0;
}
创建了一个Basic类的对象的指针p,当调用类的函数时,因为类的成员函数的地址是存放在公共代码区的,所以在编译链接阶段,成员函数的地址就已经被确定了,所以,不回去对这个指针解引用。又因为this指针是该对象的地址,而p就是一个指针,所以会传递p给this指针,因为p为nullptr,所以this指针打印结果如上图。
类的6个默认成员函数
所谓默认成员函数,就是一个类被创建时,编译器会默认生成的六个成员函数。(C++11里新增了移动构造和移动赋值)。
构造函数
请看一个类
class Person
{
public:
void Init(int stature, int weight)
{
_stature = stature;
_weight = weight;
}
void Print()
{
cout << _stature << ' ' << _weight << endl;
}
private:
int _stature;
int _weight;
};
当我们通过Person类创建一个该类的对象时,可以调用Init函数来对该对象进行初始化,但每次创建新的对象时都要调用该函数,显得有些麻烦,而且有可能出现忘记初始化的情况。构造函数就可以很好的解决这个问题。
构造函数是一个特殊的成员函数,名字与类名相同,无返回值(就是什么都不写)。当创建一个该类对象时,这个对象都会去调用构造函数(编译器自动调用),以保证每个成员变量都有一个被设置的初始值。每个对象的生命周期内只调用一次。需要注意的是,虽然构造函数的名字叫做“构造”,但构造函数的主要任务并不是开辟空间,而是对成员变量进行初始化。
举个例子:
class Person
{
public:
Person(int stature, int weight)
{
_stature = stature;
_weight = weight;
}
Person()
{}//构造函数也可以无参,结合函数重载,如果不传参,那么就会调用这个构造函数。
void Print()
{
cout << _stature << ' ' << _weight << endl;
}
private:
int _stature;
int _weight;
};
如果类中,我们自己没有写构造函数,那么编译器会自动为我们生成一个无参的默认构造函数,而默认的构造函数的功能就是,对成员变量中的内置类型(int, double, char, int*等)不进行处理,自定义类型去调用该自定义类型的构造函数。如果我们写了构造函数,那么编译器就不会自动生成无参的默认构造函数了。
举个例子,比如我们在上面的Person类中写了构造函数,如果我们没有写第二个无参的构造函数,那么在创建一个对象时,如果不给它传递参数,那么编译器就会报错,显示没有匹配的构造函数可用,所以这就印证了这个结论。
注意:C++11中针对内置类型成员不初始化的缺陷打了补丁,即:内置类型成员在类中声明时可以给默认值。
还是上面的例子:
class Person
{
public:
Person(int stature, int weight)
{
_stature = stature;
_weight = weight;
}
Person()
{}//构造函数也可以无参,结合函数重载,如果不传参,那么就会调用这个构造函数。
void Print()
{
cout << _stature << ' ' << _weight << endl;
}
private:
int _stature = 180;
int _weight = 60;
};
如果不给这两个成员变量传参,那么就会使用我们在类中声明时的默认值,否则就会使用我们传入的参数。
另外,无参的构造函数和全缺省的构造函数都被视为默认构造函数。所以当创建一个无参的对象时,如果我们在类中同时写了一个无参的构造函数和一个全缺省的构造函数,编译器就会报错,因为此时编译器不知道该调用哪一个。需要注意的是:无参的构造函数,全缺省的构造函数,我们没写编译器自动生成的构造函数都被称为默认构造函数。
析构函数
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而编译器在对象销毁时会自动调用析构函数,完成对象中的资源清理工作。
比如我们在C语言中malloc申请了一块内存,那么在使用完之后要free掉,但是会出现忘记free而导致的内存泄漏问题。而析构函数的出现,可以解决这个问题(前提是你写了),当一个对象出了自己的作用域后,就会自动销毁,而此时编译器就会自动调用析构函数来释放这个对象申请的资源。降低了出现内存泄漏问题的概率(只要你记得写)。
析构函数的特征:
析构函数名是在类名前加 ' ~ '
无参数无返回值类型
一个类只能有一个析构函数,若我们没有自己写析构函数,那么编译器会自动生成默认的析构函数,对内置类型不做处理,对自定义类型则会去调用该自定义类型自己的构造函数。
对象生命周期结束时,编译器会自动调用对象的析构函数
举个例子:
#include<iostream>
using namespace std;
class Basic
{
public:
Basic(int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == nullptr)
{
perror("malloc");
}
_array = tmp;
cout << "Basic()" << endl;
}
~Basic()
{
if (_array)
{
free(_array);
_array = nullptr;
}
cout << "~Basic" << endl;
}
private:
int* _array;
};
int main()
{
Basic b(10);
return 0;
}
拷贝构造
在创建对象时,可能我们需要一个和某个已经创建的对象数据一摸一样的另一个对象(视情况而定)。
此时该类就需要知道通过怎样的方式进行拷贝,拷贝构造函数就应运而生了。
该函数只有单个形参,该形参是这个类对象的引用,一般在前面加上const修饰,接收度更高,防止不可更改的形参传过来时出现无法接收的情况。
拷贝构造的特征:
拷贝构造函数是构造函数的一种重载形式
拷贝构造函数的参数有且只有一个,并且必须是类对象的引用,使用传值方式的话,编译器会直接报错,因为会引发无限递归
若我们自己没有写,那么编译器会自动生成一个拷贝构造函数,是按照字节拷贝的方式来进行的,就是值拷贝,称为“浅拷贝”
浅拷贝会出现的问题:
因为浅拷贝是按字节序直接拷贝的,就是值拷贝,如果被拷贝的类中不涉及指针,那么是没有问题的,比如刚开始的Person类,只有weight和stature这两个变量,那么就可以采用”浅拷贝“,而对于上面定义的Basic类就不行,因为存在指针array,这个指针申请的空间的地址存在这个指针变量中,如果采用值拷贝,那么新的对象的指针也会指向同一块空间,由于每个对象在生命周期结束时,编译器都会自动去调用该类的析构函数,那么该指针的空间就会被free两次。
//此处写一个Basic类的拷贝构造函数
Basic(const Basic& d)
{
//深拷贝就是给拷贝的指针另外开辟一块和原对象指针指向的空间一样大的空间,然后把数据拷贝过来
}
赋值重载
运算符重载:
C++为了增强代码的可读性引入了运算符重载,我们知道,像一些内置类型,可以直接比较大小或者赋值,比如int a = 10, b = 20;那么a和b就可以进行比较操作和赋值操作,而自定义类型之间直接比大小是没有定义的,也就是编译器不知道该如何去进行比大小或者赋值的操作,那么此时运算符重载就相当于给自定义类型一种比较或者赋值的方法,告诉编译器比较或者赋值的方法,那么这些操作符就可以用于这些自定义类型了。
运算符重载其实是有特殊函数名的函数,也具有其返回值类 型,函数名字以及参数列表,其返回值类型和参数列表和普通函数类似。函数名称为关键字operator和后面要重载的运算符,返回值类型写在operator前,参数列表在函数名称之后。
注意:
' .* ',' :: ',' sizeof ',' ? :',' . '这五个运算符是不能重载的
作为类成员函数重载时,其参数看起来比操作对象少1,但其实第一个参数是本对象的this指针
不能通过连接其他符号来创建新的操作符,例如operator)
举个例子:
class Person
{
public:
Person(int stature, int weight)
{
_stature = stature;
_weight = weight;
}
Person()
{}//构造函数也可以无参,结合函数重载,如果不传参,那么就会调用这个构造函数。
void Print()
{
cout << _stature << ' ' << _weight << endl;
}
bool operator>(const Person& p)
{
return _weight > p._weight;
}//此处重载了大于符号,使得两个Person类的对象可以通过体重的属性进行比大小了
private:
int _stature = 180;
int _weight = 60;
};
现在可以说运算符重载了,其实就是对' = '符号进行重载,返回值为该类的引用(可以提高返回的效率,也可以支持连续赋值) 。参数类型为const类的引用(提高传参效率)。
Person& operator=(const Person& p)
{
if (this != &p)
{
_weight = p._weight;
_stature = p._stature;
}
}//Person类的复制重载,如果传入的对象地址和this不同,就赋值(排除了自己给自己赋值的情况)
需要注意的是:如果是给已存在的对象赋值,那么调用的就是赋值重载,如果想用' = '创建一个新的对象,那么调用的其实是拷贝构造。当我们自己没有写赋值重载的时候,编译器会自动生成一个默认的赋值重载,按字节的方式拷贝。
另外有比较特殊的重载:前置++和后置++,前置--和后置--
//此处假设返回类型为int
int operator++(){}//前置++的写法
int operator++(int)//后置++的写法
int operator--(){}//前置--的写法
int operator--(int)//后置--的写法
另外,想以不能改变参数内容为目的的const应该加到参数列表的外面,如:
Person(int stature, int weight) const
{
_stature = stature;
_weight = weight;
}//此时相当于this指针的类型从Person* const this变成了 const Person* const this
此时该类内的成员变量均不可被修改。
取地址以及const取地址操作符重载
class Person
{
public:
Person* operator&()
{
return this;
}
const Person* operator&()const
{
return this;
}
private:
int _stature = 180;
int _weight = 60;
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比 如想让别人获取到指定的内容。
水平有限,欢迎指正。