C++入门四式——类和对象(中)

发布于:2025-03-17 ⋅ 阅读:(17) ⋅ 点赞:(0)

this指针

我们的date类中有int跟print()两个成员,函数体中没有关于不同对象的区分,那当d1调用int和print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?

这里C++给出了一个隐含的this指针解决这里的问题。

//.h
#include<iostream>
using namespace std;
class date
{
public:
   //void init(date* const this,int year......
	void init(int year,int month,int day)
	{
     //this->_year=year;
		_year = year;
     //......
		_month = month;
    //this->_day=day;
		_day = day;

	}
    //void print(date* const this)
	void print()
	{
		cout << _year << "/" << _month << "/" << _day<<endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
//.cpp
#include"test.h"
int main()
{
	date d1,d2;
	d1.init(2025, 3, 13);
	d1.print();
	d2.init(2025, 6, 1);
	d2.print();
	return 0;

}
  • 编译器编译后,类的成员函数默认都会在形参第一个位置增加一个当前类类型的指针,叫做this指针。上图注释的就是。
  • 类的成员函数中访问成员变量,本质也是通过this指针访问的,如init中给_year赋值。 
  • C++中规定不能在形参和实参的位置显示的写this指针,编译器会自动处理。但是可以在函数体内显示使用this指针,在后面我们实现date类的加减会用到。 

小测试 

1.this指针应该存在内存里的哪个区域?

a.栈      b.堆       c.静态区     d.常量区   e.对象里面 

this指针是函数形参,形参放在函数栈帧中,所以应该在栈区。

但是,在vs编译器中,this指针需要频繁调用,vs把它放在ecx这个寄存器中了。 

2.下⾯程序编译运⾏结果是()
A、编译报错 B、运⾏崩溃 C、正常运⾏
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}

答案是C。 

可能有的朋友注意到了void*指针的问题,我们知道void*指针不能进行指针的+-整数和解引用操作 。会感觉这题应该是运行崩溃。其实不然,我们使用了指针就一定解引用了吗?

可以看出,我们调用Print函数是没有解引用的,所以运行正确。

2.下⾯程序编译运⾏结果是()
A、编译报错 B、运⾏崩溃 C、正常运⾏
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}

 这题的答案则是B。

原因是调用print函数时,打印_a,this->_a需要解引用。 空指针解引用不会报错,而是运行崩溃,大家可以试试。

 类的默认成员函数

默认成员函数是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。一个类我们不写的情况下默认会生成以下6个默认成员函数,我们着重讲的是前4个。

我们学习时,从两个方面去学习:

  1. 我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
  2. 编译器默认生成的函数不满足我们的要求时,我们要自己实现,如何实现?

构造函数

构造函数是特殊的函数成员 ,需要注意的是,构造函数虽然叫构造,但跟开空间创造对象没关系,而是对象实例化时初始化对象。

构造函数的本质是要替代我们之前所写的类中的Init函数的功能,构造函数自动调用的特点就完美的代替了Init.

特点: 

  • 函数名与类名相同
  • 无返回值(不是void,不用写返回值)
  • 对象实例化时系统会自动调用对应的构造函数
  • 构造函数可以重载
  • 如果类中没有显式定义,则编译器会生成一个无参的默认构造函数,一旦显示定义,编译器不再生成。
  • 编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是否初始化不确定;对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数就会报错。

        我们要初始化这个成员变量就要用初始化列表才能解决(类和对象(下)会讲 )。

#include<iostream>
using namespace std;
class date 
{
public:
	//无参
	/*date()
	{
		_year = 2024;
		_month = 2;
		_day = 1;
	}*/
	//全缺省
	date(int year = 2025, int month = 3, int day = 14)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()
	{
		cout << _year << "/" << _month << "/" << _day<<endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	/*date d1;
	d1.print();*/
	//无参不用带括号
	date d1;
	d1.print();
	//全缺省若不填值也不用带括号
	return 0;
}
#include<iostream>
using namespace std;
class date 
{
public:
	//含参
	/*date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/
	void print()
	{
		cout << _year << "/" << _month << "/" << _day<<endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	//date d1(2025, 3, 14);//调用含参的构造函数
	date d1;//编译器构造的默认函数
    d1.print();
	return 0;
}

可以看出编译器默认构造的函数并没有初始化内置类型成员变量。所以对于内置类型,我们一般默认不初始化,需要我们自己实现。

析构函数 

 析构函数跟构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管。C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。

析构函数的特点:

  • 析构函数名是在类名前加上~
  • 无参数无返回值
  •  一个类只能有一个析构函数。若未显示定义,系统会自动生成默认的析构函数。
  • 对象生命周期结束时,系统会自动调用析构函数 
  • 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用它的析构函数。
  • 我们显示写析构函数,对于自定义成员也会调用它的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
  • 如果类中没有申请资源,析构函数可以不写,直接使用编译器生成的默认析构函数,如:Date;如果默认生成的析构函数就可以用,也就不需要显示写析构,如:myqueue;但是有资源申请时,一定要自己写,否则就会造成资源泄露,如:stack。
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
class MyQueue
{
public:
//编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源
// 显⽰写析构,也会⾃动调⽤Stack的析构
/*~MyQueue()
{}*/
private:
Stack pushst;
Stack popst;
};

而如果我们对stack使用默认析构函数会发生内存泄漏的原因: 

  1.  默认析构函数只会调用类成员的析构函数(如果成员是类类型),但不会释放动态分配的内存。
  2.   stack类内部使用了动态内存分配,默认析构函数无法释放这些内存,从而导致内存泄漏。

同时,通过显示写myquene的析构 ,我们可以看出他也会自动调用stack的析构。

  • 一个局部域的多个成员,后定义的先析构

拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,从中我们可以看出拷贝构造函数是一个特殊的构造函数。

拷贝构造函数的特点:

  1. 拷贝构造函数是构造函数的一个重载。
  2. 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器会直接报错。因为语法逻辑上会引发无穷递归调用。
#include<iostream>
using namespace std;
class date 
{
public:
	date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//正确形式
//使用引用,同时d不期望被改变,加const
   date(const date& d)
   {
	    _year =d. _year;
	    _month =d. _month;
	    _day =d. _day;
   }
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	date d1(2025, 3, 14);
    d1.print();
	return 0;
}

  

  • c++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用构造拷贝完成。 
  • 若未显示定义构造拷贝,编译器会自动生成拷贝构造函数,自动生成的构造拷贝对内置类型会完成值拷贝/浅拷贝(即一个字节一个字节的拷贝),对自定义类型变量会调用他的构造拷贝。
  • 一个类如果显式实现了析构并释放资源,那么他就需要显示写拷贝构造,否则不需要。例如stack,必须显示写析构且释放资源,不然就报错。stack是必须显示写拷贝构造的,因为:

 

  • 传值返回会产生一个临时对象调用拷贝构造,传值引用返回 ,返回的是返回对象的别名,没有产生拷贝。但如果返回的是当前函数域的局部对象,函数结束就销毁了,是不能传引用返回的,这时候的引用是一个野引用。
#include<iostream>
using namespace std;
class date
{
public:
	date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//不是拷贝构造,虽然也可以拷贝,但是只是普通的构造函数
	date(const date* d)
	{
		_year = d->_year;
		_month = d->_month;
		_day = d->_day;
	}
	//拷贝构造
	date(const date& d)
	{
		_year =d. _year;
		_month =d. _month;
		_day =d. _day;
	}
	
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	date d1(2025, 3, 14);
	//普通构造
	date d2(&d1);
	//拷贝构造
	date d3(d1);
	date d4 = d1;//这样也是拷贝构造哦
	return 0;
}

赋值运算符重载

运算符重载

  • 当运算符被用于类类型的对象时,我们可以通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转化成对应运算符重载 ,若无,则报错。
  • 运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同组成。它也有返回类型和参数列表,函数体
eg:
int operator+=(const date& d)
  •  重载运算符的参数个数和该运算符作用的运算对象数量一样多。                                                一元运算符一个参数,二元两个。左侧运算对象传给第一个参数,右侧传给第二个。
  • 如果一个重载运算符成员是函数成员,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
  • 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
  • 不能通过连接语法中没有的符号来创建新的操作符,eg:operator@.
  • .*      ::     sizeof    ?:(三目操作符)     .   这五个运算符不能重载。
  • 重载操作符至少有一个类类型参数
  • 一个类需要重载哪些运算符,是看那些运算符重载后有意义,比如date类重载-就有意义(看两个日期相差多少天),但是重载operator+就没有意义。
  • 重载++运算符时,后置++增加一个int形参,跟前置++构成区分。
operator++();//为前置++
operator++(int x);//为后置++
  •  重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象<<cout,不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。
//.h
#include<iostream>
using namespace std;
class date
{
public:
	date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	date(const date& d)
	{
		_year = d._year;
		_month = d._month;
		_day =d. _day;
	}
	bool operator==(const date& d)
	{
		if ((_year == d._year) && (_month == d._month) && (_day == d._day))
			return true;
		else
			return false;

	}
	date operator++()
	{
		cout << "前置++" << endl;
		++_day;
		return *this;
	}
	date operator++(int a)
	{
		cout << "后置++" << endl;
		date tmp(*this);
		++_day;
		return tmp;
	}
private:
	int _year;
	int _month;
	int _day;
};
//.cpp
#include"test.h"
int main()
{
	date d1(2025, 3, 16);
	date d2(2025, 3, 16);
	d1.operator==(d2);//运算符重载可以显式调用
	d1 == d2;
	++d1;
	d2++;
	return 0;
}

 赋值运算符重载

赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。

赋值运算符重载的特点:

  1. 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const当前类类型引用,否则会传值传参有拷贝。
  2. 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值。
  3. 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认运算符重载行为跟默认拷贝构造函数类似,对内置类型成员会完成浅拷贝,对自定义类型成员变量会调用它的赋值重载函数。
  4. 跟拷贝重载一样,一个类如果显式实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则不需要。例如stack,必须显示写析构且释放资源,不然就报错。 

 

取地址运算符重载

const成员函数 

  • 将const修饰的成员函数称之为const成员函数,const修饰成员放到成员函数参数列表的后面。

eg:
void print()const;
==
void print(const date*const this);
  • const实际修饰该成员函数隐含的this指针,表明该成员函数中不能对类的任何成员进行修改。const修饰date类的print成员函数,print隐含的this指针由date*const this变为const date* const this.  

 取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就够了,不需要去显示实现。除非一些特殊的场景,比如:我们不想让别人取到我们当前的地址,就可以自己实现一份,胡乱返回一个地址。

	date* operator&()
	{
		return this;
	}
private:
	int _year;
	int _month;
	int _day;