C++中的类与对象丶this指针和构造函数与析构函数 (一)

一丶类与对象

C语言是面向过程的,而C++是基于面向对象的(其中也有一些面向过程的)。比方说我们经常能看到C++中涉及C的代码,C++并不是完全的面向对象。

1. 类的引入

C中有结构体,C++中保留了C中结构体的语法,同时还产生了新的语法:类。

可以简单地理解为类是结构体的升级版。

  • 1.类中和结构体中都可以声明变量
  • 2.类中可以声明和定义函数,作为类的成员函数;而结构体中不允许声明或定义函数

C++中类和结构体都可以直接用类和结构体名称来代表类型。

#include <iostream>

using namespace std;

struct MyStruct
{
	//只允许定义和声明变量
	int _A = 1;
	int _B = 2;
};

class MyClass
{	
	//允许定义和声明变量
	//也允许声明和定义方法
public:
	void Init(int A, int B)
	{
		_A = A;
		_B = B;
	}

	void Print()
	{
		cout << _A << " and " << _B << endl;
	}

private:
	int _A = 1;
	int _B = 2;
};


int main()
{
	//直接用结构体名称定义变量
	MyStruct ms;	//无需在最前加上struct				
	cout << ms._A << " and " << ms._B << endl;	//1 and 2

	//直接用类的名称定义变量
	MyClass mc;									
	mc.Print();				// 1 and 2
	
	return 0;
}

上面在类中出现的publicprivate,都属于访问权限,在下面会有讲解。


2. 类的实例化

用类的类型创建对象的过程,称为类的实例化。

  • 1.是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类时并没有分配实际的内存空间
  • 2.一个类可以实例化对多个对象,实例化出的对象,占用实际的物理空间,存储类成员变量
#include <iostream>

using namespace std;

class MyClass
{
public:
	void Init(int A = 1, int B = 2)
	{
		_A = A;
		_B = B;
	}
	
	void Print()
	{
		cout << _A << " and " << _B << endl;
	}
	
private:
	int _A;
	int _B;
};

int main()
{
	//a1和a2都属于MyClass类
	//都是由MyClass类创建的对象
	MyClass a1;
	a1.Init();
	a1.Print();		//1 and 2

	MyClass a2;
	a2.Init();
	a2.Print();		// 1 and 2

	return 0;
}

形象化地讲:类就好比是设计图,而对象是依据设计图而建造的房屋

3. 类的类型的大小

类的数据类型占的字节大小是根据成员变量决定的,其中的成员函数不会被算入类型所占空间中。

I. 计算类或对象的大小

#include <iostream>

using namespace std;

class MyClassA
{
public:
	void Init(int A = 1, int B = 2)
	{
		_A = A;
		_B = B;
	}
	
	void Print()
	{
		cout << _A << " and " << _B << endl;
	}
	
private:
	int _A;
	int _B;
};


class MyClassB
{
private:
	int _A;
	int _B;
};

int main()
{
	MyClassA A1;
	cout << "MyClassA所占字节数:" << sizeof(MyClassA) << endl;
	cout << "A1所占字节数:" << sizeof(A1) << endl;

	MyClassB B1;
	cout << "MyClassA所占字节数:" << sizeof(MyClassB) << endl;	
	cout << "A1所占字节数:" << sizeof(B1) << endl;

	return 0;
}

在这里插入图片描述


可以 sizeof(类名)计算类的大小
也可以sizeof(对象名)计算对象的大小
类的类型所占大小等于对象所占大小


II.规定空类占一个字节大小

C++规定空类占一个字节大小

空类指的是没有成员变量的类,有成员函数不作为评判是否为空类的条件。

#include <iostream>

using namespace std;

class Empty
{

};

class EmptyWithFuncNumber
{
public:
	void func()
	{
		//do nothing
	}
};

int main()
{
	//不带成员函数的空类
	Empty ept;
	cout << "Empty所占字节数大小:" << sizeof(Empty) << endl;	//1个字节
	cout << "ept所占字节数大小:" << sizeof(ept) << endl;		//个字节
	//带成员函数的空类
	EmptyWithFuncNumber ewfn;
	cout << "Empty所占字节数大小:" << sizeof(EmptyWithFuncNumber) << endl;	//1个字节
	cout << "ept所占字节数大小:" << sizeof(ewfn) << endl;		//个字节
	
	//空类指的是没有成员变量的类,有成员函数不作为评判是否为空类的条件
	return 0;
}

这样规定是有一定的道理的,比方说我们定义了一个空类,假设它任何内存都不占,也就是0个字节,那么我们知道声明了类,就可以创建类的对象,那么等同于创建的类的对象所占空间大小也就是0了,这很明显是不合理的。
于是规定空类的大小为一个字节,即使它创建了对象,也是一个字节。

4. 类中的访问权限

这里只简单介绍一下类中的三类权限(访问限定符):

  • 1.公有:public
  • 2.保护:protected
  • 3.私有:private

说明:

  • 1.public修饰的成员在类外可以直接被访问
  • 2.protectedprivate修饰的成员在类外不能直接被访问(此情景下的protectedprivate是类似的)
  • 3.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  • 4.如果后面没有访问限定符,作用域就到 } 即类结束
  • 5.class的默认访问权限为private,struct为public(因为struct要兼容C)

5. 类中的构造函数和析构函数

I. 构造函数

构造函数时特殊的成员函数,需要注意的时,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。其特征:

  • 1.函数名与类名相同
  • 2.无返回值
  • 3.对象实例化时编译器自动调用对应的构造函数
  • 4.构造函数可以重载

构造函数可以自行创建,也可以由编译器自行创建,但由编译器创建的构造函数是空的构造函数,这个函数被称为默认构造函数。比如一个Date类,编译器默认定义的构造是这样的:

Date()
{

}

一般情况下,我们在编写类的时候都会自行编写构造函数,这个构造函数是全缺省构造函数,也属于默认构造函数。如下:

class Date
{
public:

	//自行定义的默认构造函数
	Date(int day = 1, int month = 12, int year = 2024)
	{
		_day = day;
		_month = month;
		_year = year;
	}

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

默认构造函数指的是无参调用的构造函数,它分两种:

  • 1.没有参数,比方说由编译器自行创建的,或者自己定义的
  • 2.每个参数都有初始值的全缺省型

注意,如果未定义任何构造函数,那么编译器会自行提供无参的默认构造函数;如果定义了构造函数,无论自行定义是何种构造函数,那么编译器都不会再提供构造函数。

此外,一个类可以有多个构造函数,而且构造函数允许重载,有参的或是无参的等等,但要注意在调用的时候是否会发生歧义,比如自己定义了两个这样的构造函数:

#include <iostream>

using namespace std;

class Date
{
public:
	//自行定义的无参默认构造函数
	Date() 
	{

	}
	//自行定义的全缺省型默认构造函数
	Date(int day = 1, int month = 12, int year = 2024)
	{
		_day = day;
		_month = month;
		_year = year;
	}

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

int main()
{
	Date date;	//编译前就会报错 
	//调用歧义 两个构造函数的优先级一致
	// 编译器无法抉择使用哪个进行初始化

	return 0;
}

另外,默认构造函数的调用是这样的(拿上面的Date类举例):

	Date date;

而不是这样的:

	Date date();

对普通的有参构造函数才需要加括号:

//这里补充一个Date类的有参构造做示范:
Date(int day, int month, int year)
{
	_day = day;
	_month = month;
	_year = year;
}

int main()
{
	Date date(1, 12, 2024);
	return 0
}

要注意的一点:构造函数是在创建对象时就调用的,要注意构造函数的调用时机。

此外延申一下构造函数的知识。对于编译器自行生成的默认构造函数,它对于内置类型的成员变量,比方说char/int/指针等等内置数据类型,没有规定对其进行处理(比如进行初始化的处理)(有些编译器可能会处理);对于自定义类型的成员变量,比方说类,那么才会调用它的无参构造进行处理。

II. 析构函数

与构造函数的功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。其特征:

  • 1.析构函数名在类名前加上字符~
  • 2.无参数而且无返回值类型
  • 3.一个类只能由一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  • 4.对象的生命周期结束时,C++编译系统自动调用析构函数。

一般析构在对象销毁前进行自行调用,不需要手动调用,而且只调用一次。

~Date()
{
	//若在创建对象时动态开辟空间,在此处需要释放空间
}

二丶 this指针

1. this指针的引出

我们在定义类的时候,拿下面的MyClass类举例,在Init或是Print两个成员函数中并没有关于不同对象的区分,那么当mcA调用Init函数时,该函数是如何知道应该设置mcA对象,而不是设置d2对象呢?

#include <iostream>

using namespace std;

class MyClass
{
public:
	void Init(int A = 1, int B = 2)
	{
		_A = A;
		_B = B;
	}
	
	void Print()
	{
		cout << _A << " and " << _B << endl;
	}
	
private:
	int _A;
	int _B;
};

int main()
{
	MyClass mcA;
	mcA.Init();
	mcA.Print();		//1 and 2

	MyClass mcB;
	mcB.Init(3, 4);
	mcB.Print();		//3 and 4

	return 0;
}

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

2. this指针的特性

  • 1.this指针的类型:类的类型 const,即成员函数中,不能给this指针赋值,它的指向是不可更改的*
  • 2.只能在“成员函数”的内部使用
  • 3.this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
  • 4.this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器(汇编中的知识)自动传递,不需要用户传递

this指针是“成员函数”第一个隐含的指针形参:

#include <iostream>

using namespace std;

class CA
{
public:

	CA()
	{

	}
	//实际上,编译器是这样处理的
	//void Init(CA* const this, int numA, int numB)
	void Init(int numA, int numB)
	{
		_numA = numA;
		_numB = numB;
	}
	//实际上,编译器是这样处理的
	// void MyPrint(CA* const this)
	void MyPirnt()
	{
		//内部是这样访问的
		//cout << this->_numA << " and " << this->_numB << endl;
		cout << _numA << " and " << _numB << endl;
	}

private:
	int _numA;
	int _numB;
};

int main()
{
	CA ca1;
	CA ca2;

	ca1.Init(1, 1);
	ca2.Init(2, 2);
	//隐式使用了this指针  this指向的是对象
	
	ca1.MyPirnt();
	//实际上 编译器是这样处理的:cal.MyPrintf(&ca1);

	ca2.MyPirnt();
	//实际上 编译器是这样处理的:ca2.MyPrintf(&ca2);

	return 0;
}

3. this指针的使用以及可能出现的问题

I.this指针的使用

this指针指向调用对象,通过this指针可以获取到该对象的成员函数或是成员变量,但this只能在成员函数内使用this指针。

当this指针出现后,类中成员函数的形参可以和类内成员变量同名,赋值时在成员变量前加上this->,表示指代的是类内成员变量,否则按局部优先原则会指代为形参。比如:

#include <iostream>

using namespace std;

class CA
{
public:
	CA()
	{

	}

	void InitVice(int numA, int numB)
	{
		numA = numA;
		numB = numB;
	}

	void Init(int numA, int numB)
	{
		this->numA = numA;
		this->numB = numB;
	}
	
	void MyPirnt()
	{
		//也可以写成cout << this->numA << " and " << this->numB << endl;
		//两者在此处是一样的
		cout << numA << " and " << numB << endl;
	}

private:
	int numA = 0;
	int numB = 0;
};

int main()
{
	CA ca1;
	CA ca2;
	//使用了this 指代明确 Init初始化成功
	ca1.Init(1, 1);
	ca1.MyPirnt();
	
	//同名却未使用this 因局部优先致使Init初始化失败
	ca2.InitVice(2, 2);
	ca2.MyPirnt();

	return 0;
}

II.使用this指针可能出现的问题

这里抛出一个问题,下面的代码会不会报错?

	classA aa;
	classA* p = nullptr;
	p->Printf();

答案是:不会。
把完整的代码放到这里:

class classA
{
public:
	//等价于void Printf(classA* const this)
	void Printf()
	{
		cout << this << endl;
		cout << "Printf()" << endl;
		//cout << _a << endl;
	}

private:
	int _a;
};

int main()
{
	classA aa;
	classA* p = nullptr;
	p->Printf();
	
	return 0;
}

这里乍一看是空指针解引用了,实际上并不是。我们进入反汇编看一下:在这里插入图片描述
实际上,这里只是进行了空指针的传递和函数的调用。我们注意看Printf()中的实现,并没有通过需要通过解引用p实体后访问的数据:

void Printf()
{
	cout << this << endl;
	cout << "Printf()" << endl;
}

没有通过p的实体调用函数或使用数据,也就是说没有对p进行空指针的解引用(访问)。

那么也就是说,以这种方式进行操作,调用成员函数时,并没有涉及对象的实体,也就是说此时没有进行对p的解引用操作来获取到p的实体对象。注意:我们这里指的是再调用成员函数时,此时还未进入成员函数的内部执行代码,这个时机很关键。

上面这样不算解引用,那么我们修改一下Printf函数,让它涉及p实体的数据,然后再以相同的方式调用,来试试看是怎样的:

void Printf()
{
	cout << this << endl;
	cout << "Printf()" << endl;
	cout << _a << endl;
}

这时却报错了:
在这里插入图片描述
这里我们使用了p实体对象中的成员变量_a,但是p是nullptr,它是没有实体对象的啊,也就自然会出现访问空指针的冲突。也就是说,这里,在cout<<_a<<endl处才对p进行解引用获取它的实体。

这样我们就十分清楚了。在类指针进行->(语法上称之为解引用)操作时,真正是否解引用了,要看具体的代码实现是如何的,是否通过->操作来访问p指向实体对象的成员变量。如果它访问了,那必然是解引用了;否则,即使表面上使用了->操作符,也未必是真的解引用。

解引用的实质是通过指针访问到实体。

再来两个代码实例,来判断一下:
代码1:

#include <iostream>

using namespace std;

class A
{
public:
	void Printf()
	{
		cout << "Printf()" << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->Printf();

	return 0;
}

上述代码是否能正确运行:
A. 编译报错
B. 运行崩溃
C. 正常运行

正确答案:C

代码2:

#include <iostream>

using namespace std;

class A
{
public:
	void Printf()
	{
		cout << _a << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->Printf();

	return 0;
}

上述代码是否能正确运行:
A. 编译报错
B. 运行崩溃
C. 正常运行

正确答案:B

4. this指针的存放位置

this指针一般存放在栈里,部分编译器会存放在ecx寄存器中


本博客仅供个人参考,如有错误请多多包含。
Aruinsches-C++日志-4/7/2024
  • 23
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值