天天看点

C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针2 默认成员函数3 const 成员

本文架构:

C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针2 默认成员函数3 const 成员

类和对象(2)

  • 1 this指针
  • 2 默认成员函数
    • 2.1 构造函数
    • 2.2 析构函数
    • 2.3 拷贝构造函数
    • 2.4 赋值运算符重载
      • 2.4.1 运算符重载
      • 2.4.2 赋值运算符重载
    • 2.5-2.6 取地址及const取地址操作符重载
  • 3 const 成员
    • 3.1 const修饰类的成员函数
    • 3.2关于函数调用的分析

1 this指针

C++编译器给每个“非静态成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中对所有成员变量的访问都是通过该指针访问的。但这个this指针对用户来说是透明的,即用户不需要来传递,编译器自动完成。

简言之,请看下面程序图解:

下面是一个日期类内的成员函数,通过次函数可以发现this指针在成员函数中扮演的角色。下图展示的两段程序表明this指针是成员函数中默认隐藏的,通过该指针去访问类内的成员变量。

C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针2 默认成员函数3 const 成员

关于this指针有几个需要注意的点:

  1. this指针的类型:类类型*const
  2. 只能在成员函数的内部使用
  3. this指针本质其实是一个成员函数的形参,this指针一般存储在栈上,对象调用成员函数时,将对象地址作为实参传递给this形参。对象中不存储this指针。
  4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
  5. this是C++的一个关键字,不能用来定义参数变量
  • 思考:this指针可以为空吗?
    C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针2 默认成员函数3 const 成员
    如上图,两段代码,代码一可以调用左边类执行结果为Show(),而代码二会出错,问题在下图:
    C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针2 默认成员函数3 const 成员

    分析:

    代码一:调用类内的show函数,show函数内没有使用到成员变量,所以没有使用到this指针,程序可以正常运行。(说明:p->show()这个函数并没有对p指针解引用,show函数没有使用到this指针,show函数地址也没有存到对象里面,所以不会引发空指针访问的问题,所以就不会发生程序奔溃。)

    代码二:调用的类内的PrintA函数,而PrintA函数中使用了类内的成员变量,所以此函数就使用了this指针,void PrintA()转化成 void PrintA(A* this),this指针接收到对象p传来的空指针,引发了访问问题。

2 默认成员函数

下面这个类既没有成员函数,也没有成员变量,我们称之为空类。那么空类里面有什么?很容易想到默认成员函数。下面介绍6个默认成员函数,记住默认成员函数就是我们可以不写,会自动生成的。

2.1 构造函数

一个特殊的成员函数,主要任务是初始化对象。

特性:

  1. 函数名与类名相同
  2. 无返回值
  3. 对象实例化时,编译器自动调用(保证对象一定会被初始化,不会被忘记)对应的构造函数
  4. 构造函数可以重载(表示可以有多种初始化方式)

代码展示:

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++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针2 默认成员函数3 const 成员
  1. 如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参数的默认构造函数,一旦用户显示定义,编译器将不再生成。
C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针2 默认成员函数3 const 成员

笔记:

对于内置类型(基本类型,语言原生定义的类型,如int,char,指针等),不初始化。

对于自定义类型(用class,struct等定义的类型),如下图定义的类A的“A _aa;”等,编译器会调用默认构造函数初始化。

C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针2 默认成员函数3 const 成员

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 析构函数

析构函数是特殊的成员函数,其功能与构造函数相反,析构函数不是完成对象的销毁,局部对象销毁工作是有编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

析构函数有如下特征:

  1. 析构函数名是在类名前加字符 ~
  2. 无参数、无返回值
  3. 一个类有且只有一个析构函数(析构函数不能重载)。若未显示定义,系统会调用默认的析构函数。
  4. 对象声明周期结束时,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后析构。

解释:对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合后进先出规则。

  1. 对于编译器自动生成的默认析构函数,对自定义类型成员调用它的析构函数。(简言之:自定义类型,调用对应的析构函数;内置类型,不处理)
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;
}
           

分析传值调用:

C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针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;
	}
	//默认拷贝构造函数
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、链表等这样的类,编译器默认生成的拷贝构造完成的是浅拷贝,浅拷贝会导致析构两次,程序会奔溃,不满足我们的需求,需要自己实现深拷贝。

浅拷贝适合日期类这样的类。

  1. 调用析构函数时,这块空间被free了两次。
  2. 其中一个对象插入删除数据,都会导致另一个对象也插入删除数据。

    拷贝构造函数对内置类型,完成浅拷贝或者值拷贝。对于自定义类型,成员会调用它的拷贝构造完成拷贝。

2.4 赋值运算符重载

2.4.1 运算符重载

C++为了增强代码可读性引入运算符重载,运算符重载是具有特殊函数名的函数,也有返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字ope

注意点;

  • 不能通过连接其他符号来创建新的操作符:如[email protected]
  • 重载操作符必须有一个类类型或者枚举类型的操作数
  • 用于内置类型的操作符,其含义不能改变,如内置类型 - ,不能改变为+
  • 作为类成员的重载函数时,其形参看起来比操作数数目少一个成员函数(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==

C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针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;
	}
	//类内运算符重载
	//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 功能

    C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针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(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;
}
           

二、连续赋值运算符重载

C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针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(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) + 1);
		strcpy(_str, str);
	}
	//析构函数
	~String()
	{
		cout << "~String()" << endl;
		free(_str);
	}

private:
	char* _str;
};
int main()
{
	String s1("blogs");
	String s2("yumoz");
	s1 = s2;
}
           

2.5-2.6 取地址及const取地址操作符重载

C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针2 默认成员函数3 const 成员

附上代码:

#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date* operator&()
	{
		cout << "Date* operator&()" << endl;
	    return this;
	}

	const Date* operator&() const
	{
		cout << "const Date* operator&()const" << endl;
		return this;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2021, 5, 27);
	const Date d2(2021, 4, 27);

	Date* ptr1 = &d1;
	const Date* ptr2 = &d2;

	cout << &d1 << endl;
	cout << &d2 << endl;

	return 0;
}

           

3 const 成员

3.1 const修饰类的成员函数

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针2 默认成员函数3 const 成员

3.2关于函数调用的分析

C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针2 默认成员函数3 const 成员

对于:

1)可以调用

2)可以调用

3)可以调用 const Date* 到const Date*(权限缩小)

4)不可调用,出错,错误为下面展示。const Date* 到 Date* (权限放大)

错误 1 error C2662: “void Date::Print(void)”: 不能将“this”指针从“const Date”转换为“Date &”

几个思考题:

  1. const对象可以调用非const成员函数吗?

    答:不可以,仔细阅读上图,这种情况输入权限放大,将只读类型调用可读可写类型,故会出错。

  2. 非const对象可以调用const成员函数吗?

    答:可以,可读可写调用只读,权限缩小,所以可以。

  3. const成员函数内可以调用其他的非const成员函数吗?

    答:不可以,const Date* 调用Date* ,权限放大,不可以。

  4. 非const成员函数可以调用其他的const成员函数吗?

    答:可以,Date* 调用const Date* 权限缩小,故可以调用。

继续阅读