C++类和对象(this指针6个默认成员函数const成员)
Posted yumoz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++类和对象(this指针6个默认成员函数const成员)相关的知识,希望对你有一定的参考价值。
本文架构:
类和对象(2)
1 this指针
C++编译器给每个“非静态成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中对所有成员变量的访问都是通过该指针访问的。但这个this指针对用户来说是透明的,即用户不需要来传递,编译器自动完成。
简言之,请看下面程序图解:
下面是一个日期类内的成员函数,通过次函数可以发现this指针在成员函数中扮演的角色。下图展示的两段程序表明this指针是成员函数中默认隐藏的,通过该指针去访问类内的成员变量。
关于this指针有几个需要注意的点:
- this指针的类型:类类型*const
- 只能在成员函数的内部使用
- this指针本质其实是一个成员函数的形参,this指针一般存储在栈上,对象调用成员函数时,将对象地址作为实参传递给this形参。对象中不存储this指针。
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
- this是C++的一个关键字,不能用来定义参数变量
- 思考:this指针可以为空吗?
如上图,两段代码,代码一可以调用左边类执行结果为Show(),而代码二会出错,问题在下图:
分析:
代码一:调用类内的show函数,show函数内没有使用到成员变量,所以没有使用到this指针,程序可以正常运行。(说明:p->show()这个函数并没有对p指针解引用,show函数没有使用到this指针,show函数地址也没有存到对象里面,所以不会引发空指针访问的问题,所以就不会发生程序奔溃。)
代码二:调用的类内的PrintA函数,而PrintA函数中使用了类内的成员变量,所以此函数就使用了this指针,void PrintA()转化成 void PrintA(A* this),this指针接收到对象p传来的空指针,引发了访问问题。
2 默认成员函数
下面这个类既没有成员函数,也没有成员变量,我们称之为空类。那么空类里面有什么?很容易想到默认成员函数。下面介绍6个默认成员函数,记住默认成员函数就是我们可以不写,会自动生成的。
class Date{};
2.1 构造函数
一个特殊的成员函数,主要任务是初始化对象。
特性:
- 函数名与类名相同
- 无返回值
- 对象实例化时,编译器自动调用(保证对象一定会被初始化,不会被忘记)对应的构造函数
- 构造函数可以重载(表示可以有多种初始化方式)
代码展示:
class Date
{
public:
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//无参数构造函数
Date(){};
//带参数构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//调用默认参数
Date d2;
d2.Display();
//调用带构造参数
Date d3(2020,2,1);
d3.Display();
return 0;
}
- 如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参数的默认构造函数,一旦用户显示定义,编译器将不再生成。
笔记:
对于内置类型(基本类型,语言原生定义的类型,如int,char,指针等),不初始化。
对于自定义类型(用class,struct等定义的类型),如下图定义的类A的“A _aa;”等,编译器会调用默认构造函数初始化。
6. 无参数的构造函数和全缺省的构造函数都称之为默认构造函数,并且默认构造函数只能有一个。三种构造函数
- 自己不写,编译器默认生成的构造函数
- 自己写的无参数的构造函数
Date()
{
_year = 2020;
_month = 1;
_day = 2;
}
- 自己写的全缺省的构造函数,(推荐)
Date(int year = 0, int month = 1, int day = 2)
{
_year = year;
_month = month;
_day = day;
}
编程个人风格:
成员变量的命名方式:
int year_;
int mYear;
int m_year;//m 代表 member,成员变量
2.2 析构函数
析构函数是特殊的成员函数,其功能与构造函数相反,析构函数不是完成对象的销毁,局部对象销毁工作是有编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
析构函数有如下特征:
- 析构函数名是在类名前加字符 ~
- 无参数、无返回值
- 一个类有且只有一个析构函数(析构函数不能重载)。若未显示定义,系统会调用默认的析构函数。
- 对象声明周期结束时,C++编译系统会自动调用析构函数。
class Date
{
public:
//全缺省构造函数
Date(int year = 2020, int month = 1, int day = 2)
{
_year = year;
_month = month;
_day = day;
}
//析构函数不需要写,没有什么可清理
//默认生成的析构函数,不做什么
private:
int _year;
int _month;
int _day;
};
class Stack
{
public:
//全缺省构造函数
Stack(int capacity = 4)
{
if (capacity <= 0)
{
_a = nullptr;
_size = _capacity = 0;
}
else
{
_a = (int*)malloc(sizeof(int)*capacity);
_capacity = capacity;
_size = 0;
}
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_size = _capacity = 0;
}
private:
int *_a;
int _size;
int _capacity;
};
int main()
{
Date d;
Stack st;
return 0;
}
析构和构造的顺序:
若上述栈类定义了两个栈对象
Stack st1;
Stack st2;
则他们的构造析构顺序为:st1先构造,st2后构造,st2先析构,st1后析构。
解释:对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合后进先出规则。
- 对于编译器自动生成的默认析构函数,对自定义类型成员调用它的析构函数。(简言之:自定义类型,调用对应的析构函数;内置类型,不处理)
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
class Person
{
private:
String _name;//自定义类型,调用对应析构函数
int _age;//内置类型,不处理
};
2.3 拷贝构造函数
只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个,且必须使用引用传参, 用传值方式会引发无穷递归调用。
#include <iostream>
using namespace std;
// 日期类
class Date
{
public:
//构造函数
Date(int year = 0, int month = 1, int day = 2)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//拷贝构造函数
//Date(Date &d)
Date(const Date &d)//使用const修饰Date表示不希望修改对象
{
_year = d._year;
_month = d._month;
_day = d._day;
//d._day = _day;//左值不可改变,因为使用了const
}
/*Date(const Date *d)//这样写可通过,不好,调用方式 Date d2(&d1);
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}*/
/*Date(Date d) //传值拷贝,无穷递归,只要传值,就会调用拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}*/
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 5, 30);
d1.Print();
return 0;
}
分析传值调用:
结论:每次传值都会引发对象的拷贝,最终会形成无穷递归。
- 若拷贝构造函数未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数的对象按内存存储字节序完成拷贝,称这种拷贝为浅拷贝或者值拷贝。
#include <iostream>
using namespace std;
// 日期类
class Date
{
public:
//构造函数
Date(int year = 0, int month = 1, int day = 2)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//默认拷贝构造函数
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 5, 30);
d1.Print();
//d2调用的是默认拷贝构造完成拷贝,d2和d1的值也是一样的
Date d2(d1);
d2.Print();
return 0;
}
总结:
- 有时候浅拷贝不能达到使用效果,所以有了深拷贝。
像如Stack、链表等这样的类,编译器默认生成的拷贝构造完成的是浅拷贝,浅拷贝会导致析构两次,程序会奔溃,不满足我们的需求,需要自己实现深拷贝。
浅拷贝适合日期类这样的类。
- 调用析构函数时,这块空间被free了两次。
- 其中一个对象插入删除数据,都会导致另一个对象也插入删除数据。
拷贝构造函数对内置类型,完成浅拷贝或者值拷贝。对于自定义类型,成员会调用它的拷贝构造完成拷贝。
2.4 赋值运算符重载
2.4.1 运算符重载
C++为了增强代码可读性引入运算符重载,运算符重载是具有特殊函数名的函数,也有返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字ope
注意点;
- 不能通过连接其他符号来创建新的操作符:如operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,如内置类型 - ,不能改变为+
- 作为类成员的重载函数时,其形参看起来比操作数数目少一个成员函数(this指针提供)
- 像 .* 、:: 、sizeof、?: 、 . 这五种运算符不能重载。
一、全局operator==
#include <iostream>
using namespace std;
// 日期类
class Date
{
public:
//构造函数
Date(int year = 0, int month = 1, int day = 2)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
int _year;
int _month;
int _day;
};
bool operator==(const Date &d1, const Date &d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2021, 5, 30);
Date d2(2021, 5, 30);
d1.Print();
d2.Print();
cout << (d1 == d2) << endl;
return 0;
}
二、类内成员函数operator==
#include <iostream>
using namespace std;
// 日期类
class Date
{
public:
//构造函数
Date(int year = 0, int month = 1, int day = 2)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//类内运算符重载
//bool operator(Date* this,const Date& d)
bool operator==(const Date &d)
{
return _year == d._year //this._year == d._year
&& _month == d._month //this._month == d._month
&& _day == d._day; //this._day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 5, 30);
Date d2(2021, 5, 30);
d1.Print();
d2.Print();
//调用方式等价于 d1.operator == (d2);
//即 d1.operator == (&d1, d2); &1传递给this指针 d2传递给形参d
cout << (d1 == d2) << endl;
return 0;
}
2.4.2 赋值运算符重载
赋值运算符重载的几个特点:
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*this
- 一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝。
一、只能 实现 d1 = d2 功能
#include <iostream>
using namespace std;
// 日期类
class Date
{
public:
//构造函数
Date(int year = 0, int month = 1, int day = 2)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//拷贝构造函数
Date(const Date &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值运算符重载
//d1 = d2
//void operator=(Date* this, const Date &d)
void operator=(const Date &d)
{
if (this != &d)//检查不是自己给自己赋值,才需要拷贝。
{
_year = d._year;//this._year = d._year;
_month = d._month;//同上
_day = d._day;//同上
}
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2021, 5, 20);
Date d3;
//调用赋值运算法 d1 = d2
//调用方式 对于 d1 == d2 有 d1.operator=(&d1,d2);
d1 = d2;
d1.Print();
d2.Print();
d3.Print();
return 0;
}
二、连续赋值运算符重载
#include <iostream>
using namespace std;
// 日期类
class Date
{
public:
//构造函数
Date(int year = 0, int month = 1, int day = 2)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//拷贝构造函数
Date(const Date &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//连续赋值运算符重载
//d1 = d2 = d3
//Date& operator=(Date* this, const Date &d)
Date& operator=(const Date &d)
{
if (this != &d)//检查不是自己给自己赋值,才需要拷贝。
{
_year = d._year;//this._year = d._year;
_month = d._month;//同上
_day = d._day;//同上
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2021, 5, 20);
Date d3;
//调用赋值运算法 连续赋值
//调用方式 对于 d1 == d2 有 d1.operator=(&d1,d2);
d3 = d1 = d2;
d1.Print();
d2.Print();
d3.Print();
return 0;
}
对比:拷贝构造函数和赋值运算符重载
调用方式:
- 拷贝构造函数:
已知d1,用同类对象d1初始化一个d2。
Date d1(2021, 5, 30);
Date d2(d1);
- 赋值运算符重载
虽然也是拷贝,但是实现已知d1和d2。相当于把d2的值拷贝给d1。
Date d1;
Date d2(2021, 5, 20);
d1 = d2;
对比:函数重载和运算符重载
都使用了重载这个名词
- 函数重载时,支持定义同名函数
- 运算符重载是为了让自定义类型可以像内置类型一样去使用运算符
有时候编译器生成的默认赋值重载函数可以实现按字节序的值拷贝,但有时候并不能达到我们要求,所以对于下面程序出现的奔溃问题,提出了深拷贝。
class String
{
public:
String(const char* str = "")
{
_str = (char*)malloc(strlen(str) + 1C++ 类和对象