【C++】—— 类与对象(一)

1、类的定义

1.1、类定义

1.1.1、类定义格式

  • c l a s s class class 为定义类的关键字, c l a s s class class 后接类的名字(自己定),{ } 为类的主体,注意类定义结束时,后面的 不能省略

  • 类体中的内容为类的成员:类中的变量称为类的成员变量属性,类中的函数称为类的成员函数方法

  
下面我们简单写一个栈类来感受一下

class Stack
{
public:
	//成员函数
	void Init(int n = 4)
	{
		_a = (int*)malloc(sizeof(int) * n);
		if (nullptr == _a)
		{
			perror("malloc fail");
			exit(1);
		}

		_capccity = n;
		_top = 0;
	}

	void Push(int x)
	{
		// ...扩容
		_a[top++] = x;
	}

	void Destroy()
	{
		fail(_a);
		_a = nullptr;
		_capacity = 0;
		_top = 0;
	}

private:
	//成员变量
	int* _a;
	size_t _capacity;
	size_t _top;
};//分号不能省略

  C++中,并没有规定成员函数和成员变量的位置,只要他们在类中就行,把成员变量混在几个函数的中间也是可以的。但一般来讲都是成员函数在上,成员变量在下。
  

1.1.2、成员变量的标识

  • 为了区分成员变量,一般习惯上成员变量会加上一个特殊的标识,如成员变量前或后加上 _ ;或 m ( m e m b e r ) m(member) m(member) 开头;或 m m m_ 开头。这点C++并没有明确规定,具体看公司的要求

  
  在声明栈类的成员变量时,大家可能发现我在变量名前都加上了 “_”,为什么要这么做呢?
  我们来看一个日期类

class Date
{
public:
	void Init(int year, int month, int day)
	{
		year = year;
		month = month;
		day = day;
	}

private:
	int year;
	int month;
	int day;
};

  可以看到,上述日期类的成员变量与 I n i t Init Init 函数中的形参无法区分,所以为了区分成员变量,要在成员变量前加上特殊的标识
  

1.1.3、C++ 中的 s t r u c t struct struct

  • C++ 中 s t r u c t struct struct 也可以定义类,C++ 中兼容了 C语言 中 s t r u c t struct struct 的用法,同时 s t r u c t struct struct 升级成了
  • 明显的变化是: s t r u c t struct struct 中可以定义函数。 s t r u c t struct struct c l a s s class class 定义类只有一点细微的差别(下面会说),但一般情况下我们还是推荐使用 c l a s s class class 定义类

  
  在C语言,我们定义一个链表的节点,往往是这样定义的:

typedef struct ListNodeC
{
	int val;
	struct ListNode* next;
}ListNodeC;

  
在 C++中, s t r u c t struct struct 升级成了

  1. 类可以定义函数
  2. 名称就可以代表类型,不需要 s t r u c t struct struct + 名称

  

// 不再需要typedef,ListNodeCPP就可以代表类型
struct ListNodeCPP
{
	void Init(int x)
	{
		next = nullptr;
		val = x;
	} 
	
	ListNodeCPP* next;
	int val;
};

  当然,C++ 是兼容 C 的,所以上面的那种方式 C++ 也支持
  

1.1.4、C++ 中的内联函数

  • 在类中定义的成员函数默认是 i n l i n e inline inline,而如果进行声明和定义的分离:声明在类中;定义在类外,则不然。

  

1.1.5、总结

  • c l a s s class class 为定义类的关键字 S t a c k Stack Stack 为类的名字{ } 中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性成员变量;类中的函数称为类的方法或者成员函数
      
  • 为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加 _ 或者 m m m 开头,注意 C++ 中这个并不是强制的,只是一些管理,具体看公司要求
      
  • C++ 中 s t r u c t struct struct可以定义类,C++ 兼容 C 中 s t r u c t struct struct 的用法,同时 s t r u c t struct struct 升级成了类,明显的变化是 s t r u c t struct struct 中可以定义函数,一般情况下我们还是推荐使用 class 定义类
      
  • 定义在类里面的成员函数默认为 i n l i n e inline inline

  

1.2、访问限定符

  可能有小伙伴注意到了,前面定义栈类日期类时,出现了 p u b l i c public public p r i v a t e private private 两个关键字,他们有什么用呢?我们一起来看看

  • C++ 一种实现封装的方式,用类将对象的属性和方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
  • p u b l i c public public 修饰的成员类外可以直接被访问 p r o t e c t e d protected protected p r i v a t e private private 修饰的成员类外不能直接被访问。现阶段我们认为 p r o t e c t e d protected protected p r i v a t e private private 是一样的,以后集成章节才能体现出他们的区别.
  • 访问权限作用域从该访问限定符出现的位置开始,直到下一个访问限定符出现为止,如果后面没有访问限定符,作用域就到 } (收括号)即类结束
  • c l a s s class class 定义成员没有被访问限定符修饰时默认 p r i v a t e private private s t r u c t struct struct 默认 p u b l i c public public 。这也是 c l a s s class class s t r u c t struct struct唯一区别
  • 一般成员变量都会被限制为 p r i v a t e private private/ p r o t e c t e d protected protected,需要给别人使用的成员函数为 p u b l i c public public。当然这只是一般情况,并没有硬性规定

  

在这里插入图片描述

  
举个栗子:

在这里插入图片描述

  当然,一般情况下也没有人会这样写。一般都是公有的放一起,私有的放一起。
  
像这样:

在这里插入图片描述

  
  

1.3、类域

  在C++中,凡是用 { } 括起来的都会形成一个 。类也不例外,类定义了一个新的作用域: 类域,类的所有成员都在类的作用域中。

  不知大家有没有注意到,在前面定义的栈类中,成员函数的命名不再像之前 C语言 写栈时那样要加上栈的标识,如: void STDestroy()。之前 C语言 这样写是因为结构体和函数是分离的,栈叫 D e s t r o y Destroy Destroy ,队列也叫 D e s t r o y Destroy Destroy 就有可能搞混,且同一个域中也不允许出现同名的变量或函数。
  现在他们是栈类的成员函数,在类域之中,即使后面定义的一个队列域有同名的成员函数,也不会冲突。因为名字冲突只发生在同一个域中,不同域可以有相同名字

  那当成员函数的声明和定义分离时,即声明在类内,定义在类的外面,又该如何定义函数呢?

class Date
{
public:
	void Init(int year, int month, int day);


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

void Init(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

  
  我们知道,任何一个变量,编译器都会去找他的出处(声明/定义)。编译器默认只会在全局域或当前函数局部域去找,并不会去类域中找。那怎么办呢?我们告诉他去指定的类域中找就行了。

  • 在类体外定义成员时,需要使用 : : 作用域操作符 指明成员属于哪个类域
class Date
{
public:
	void Init(int year, int month, int day);


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

void Date::Init(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

  这样就行了

  • 类域影响的是编译的查找规则
      
    上述程序 I n i t Init Init 如果不指定类域 D a t e Date Date,那么编译器就把 I n i t Init Init 当成全局函数,那么编译时,找不到 _ y e a r year year 等成员的声明/定义在哪里,就会报错。
    指定类域 D a t e Date Date,就是告诉编译器 I n i t Init Init 是成员函数,在当前域找不到的 _ y e a r year year 等成语,就会到类域中去查找。

  

2、实例化

2.1、实例化的概念

  在讲解类的实例化之前,我们先来思考一个问题

在这里插入图片描述

上述成员变量是声明还是定义呢?

答案是 声明
  对变量来说,到底是声明还是定义是看他是否有开空间开了空间的是定义;没开空间的是声明
  

int main()
{
	Date::_year = 0;
	return 0;
}

  我们直接这样访问是会报错的,因为 _ y e a r year year 只是一个声明,并没有开空间。

  那什么时候开空间呢?
  用该类型实例化出一个对象才是开空间

int main()
{
	Date d1;
	Date d2;
	Date d3;

	return 0;
}

  上述就是类的实例化,类和对象是一对多的关系,一个类可以实例化出多个对象
  
  怎么理解这个实例化呢?我们可以用图纸和实物建筑来类比

在这里插入图片描述

  类就相当于图纸,图纸上有房子上的各种信息,当不能进去住人;而实例化对象就像是依照着图纸将房子盖好,盖好的房子是可以住人的,相当于实例化开了空间。

  

2.2、对象大小

2.2.1、对象的大小包括什么

  在学习 C语言 结构体时,我们知道结构体中就存着成员,这些成员要按照内存对齐的规则去进行计算大小。类的对象中的成员变量也是如此,成员变量是存储在对象中的。
  现在的问题是:成员函数是否是存储在对象中的呢?
答案是:不需要
  
为什么呢?我们用日期类来举例

class Date
{
public :
	void Init(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和d2
	Date d1;
	Date d2;

	d1.Init(2024, 3, 31);
	d1.Print();

	d2.Init(2024, 7, 5);
	d2.Print();

	return 0;
}

  
  上述代码中,示例化出了 d 1 d1 d1 d 2 d2 d2 两个对象
  我们给两个对象的年月日(成员变量)初始化了不同的值,所以他们的成员变量是不一样的,他们要各种存储自己的成员变量,因此成员变量肯定是存储在对象中
  
  那现在的 d 1 d1 d1 d 2 d2 d2 调用 P r i n t Print Print 函数是调用同一个函数还是不同的函数?
  很显然是同一个函数
  既然都是一样的,那在各自的对象中都存一份,是不是太浪费空间了。我要是实例化10000个对象那不是要重复存储10000份?
  我们通过汇编代码可以看到两个对象调用的都是同一个函数
  

在这里插入图片描述
  
两个 c a l l call call 指令, c a l l call call 的地址都是一样的,表明跳转的是同一个函数
  
  在这里插入图片描述
  
c a l l call call 指令跳转到 jmp 指令
  在这里插入图片描述
  
j m p jmp jmp 指令再跳转到函数

  成员函数其实是存在一个公共的区域

  既然函数不存在对象里面,那函数指针有必要存在对象里面吗?
  也是没必要的,原因还是重复存储浪费空间。
  

  • 函数指针是⼀个地址调⽤函数被编译成汇编指令[ c a l l call call 地址],其实编译器在编译链接时,就要找到函数的地址,不是在运⾏时找,只有动态多态是在运⾏时找,就需要存储函数地址,这个我们以后再学习

  

2.2.2、内存对齐规则

类的大小计算和结构体的计算是一样的,这里我们简单回顾一下。详情请看【C语言】——结构体

  • 第一个成员在与结构体偏移量为 0 的地址处。
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
  • 注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值
  • VS 中默认的对齐数是 8
  • 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到之间的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(函嵌套结构体的对齐数)的整数倍

  

2.2.3、空类的大小

class A
{
	public :
	void Print()
	{
		//...
	}
};
int main()
{
	A a;
	cout << sizeof(a) << endl;
	return 0;
}

  现在有一个问题: a a a 的大小是多大呢?
  
运行结果:

在这里插入图片描述

  为什么该对象没有成员变量还有 1 字节的大小呢
  这里的 1 为了占位
  因为如果一个空间对不给,怎么证明这个对象存在过呢,所以这里给了1个空间大小,纯粹是为了占位表示对象存在
  

3、 t h i s this this 指针

class Date
{
public :
	void Init(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;
	Date d2;

	d1.Init(2024, 3, 31);
	d1.Print();

	d2.Init(2024, 7, 5);
	d2.Print();

	return 0;
}

  现在,我们知道 d 1 d1 d1 d 2 d2 d2 调用的都是同一个 I n i t Init Init 函数和 P r i n t Print Print 函数,那既然是同一个函数为什么能打印出各自的成员变量呢?他们是怎么区分 d 1 d1 d1 d 2 d2 d2 的呢?

在这里插入图片描述

  
  C++中,给了一个隐含的 t h i s this this指针 解决这个问题

  • 编译器编译后,类的成员函数默认会在形参的第一个位置,增加一个当前类类型的指针,叫做 t h i s this this指针。比如 D a t e Date Date 类的 I n i t Init Init 的真实原型为: void Init(Date* const this, int year,int month, int day)
  • 类的成员函数中访问成员变量,本质是通过 t h i s this this 指针访问的,如 I n i t Init Init 函数中给 _ y e a r year year 赋值, this->_year = year;
  • C++ 规定不能在实参和形参的位置显示的写 t h i s this this指针(编译时编译器会处理),但是可以在函数体内显示使用 t h i s this this 指针

  
  因此,上述代码底层是这样的:

class Date
{
public:
	void Init(Date* const this, year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}

	void Print(Date* const this)
	{
		cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2;

	d1.Init(&d1, 2024, 3, 31);
	d1.Print(&d1);

	d2.Init(&d2, 2024, 7, 5);
	d2.Print(&d2);

	return 0;
}

:这只是底层,实际代码是不允许这样写的。因为 t h i s this this 指针不能在实参和形参显示写;但在函数体内可以使用 this 指针
  
  
那么 t h i s this this指针是存在哪一个区域的呢?

在这里插入图片描述

  首先,肯定不是对象里面。因为我们前面讲解对象大小时,并没有计算 t h i s this this 指针
  我们来看, t h i s this this 指针是一个形参,那形参是存在哪里的呢?
  答案是:
  但这答案也不完全对。
  因为 t h i s this this 指针需要频繁调用,因此有些编译器(如VS)会对其进行优化,放在寄存器

  

4、练习

4.1、题一

下面程序的运行结果是?
A、编译报错    B、正常运行    C、运行崩溃

class A
{
	public :
	void Print()
	{
		cout << "A::Print()" << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}

  首先把 A 排除了,因为上述程序就算出错那也是因为空指针的问题出错,对空指针的解引用什么时候是编译报错呢?
  
  这题的答案:B

  我们来看p->Print();,这句代码转换成汇编指令是: c a l l call call 指令 -> j m p jmp jmp 指令 -> P r i n t Print Print函数;那 P r r i n t Prrint Prrint 函数在哪呢?在公共代码区,并不在对象中;同时 P r i n t Print Print函数需要传递 t h i s this this 指针,这里传的是 p p p,即 n u l l p t r nullptr nullptr
  
  这一切都没有问题。虽然传递了 n u l l p t r nullptr nullptr t h i s this this 指针,但是函数内并没有对其进行解引用
  我们不要看到对象调用函数:p->Print();就以为是对 n u l l p t r nullptr nullptr 进行了解引用,我们要关注代码的底层
  那为什么需要对象取调用呢?因为 P r i n t Print Print 是其成员函数,在其类域之中,它要过编译那关,语法那关

  

4.2、题二

下面程序的运行结果是?
A、编译报错    B、正常运行    C、运行崩溃

class A
{
	public :
	void Print()
	{
		cout << "A::Print()" << endl;
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}

答案:C

  这题与上题的区别是 P r i n t Print Print 函数中多了cout << _a << endl;
  前面的步骤都是与上题一样,但到了cout << _a << endl;语句时,要对 t h i s this this 指针进行解引用cout << this->_a << endl;,因为 t h i s this this 指针是空指针,对空指针进行解引用自然就运行崩溃啦

  
  
  
  
  


  好啦,本期关于类和对象的知识就介绍到这里啦,希望本期博客能对你有所帮助。同时,如果有错误的地方请多多指正,让我们在C语言的学习路上一起进步!

  • 66
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 111
    评论
评论 111
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值