【C++】类和对象(一)

【C++】类和对象(一)

对面向过程和面向对象的初步认识


C语言是面向过程的。所谓面向过程,关注的是过程,将一个事件逐步分析,分析出解决问题的步骤,再通过调用对应的函数逐步解决问题

例如洗衣服,面向过程就是下面这样的
请添加图片描述

而C++是基于面向对象的。关注的是对象,将整个事件拆分为不同对象,靠着对象之间的交互解决问题

这次以洗衣机洗衣服为例。可将事件分为不同对象:人、衣服、洗衣机、洗衣粉

人将洗衣粉和衣服放入洗衣机,启动洗衣机,随后洗衣机自己完成洗衣及甩干。

完成整个事件主要是人、衣服、洗衣机、洗衣粉这几个对象之间的交互,至于洗衣机是如何完成工作的,人不需要关心
请添加图片描述

这一块儿看的模模糊糊也没关系,在以后的学习中我们会逐渐领会面向对象的思想

类的引入


C语言的结构体中仅可以定义变量,而C++的结构体不仅可以定义变量,还可以定义函数

以下是用C语言实现栈,结构体中仅可以定义变量,而栈的各种函数只能定义在结构体之外

// 栈的结构体
struct Stack
{
	int* _data;
	int _capacity;
	int _size;
};

// 初始化栈
void StackInit(struct Stack* ps, int capacity)
{
	ps->_data = (int*)malloc(sizeof(int) * capacity);
	ps->_capacity = capacity;
	ps->_size = 0;
}

用C++就可以将变量与函数定义在同一结构体中

struct Stack
{
	void Init(int capacity)
	{
		_data = (int*)malloc(sizeof(int) * capacity);
		_capacity = capacity;
		_size = 0;
	}

	int* _data;
	int _capacity;
	int _size;
};

类的定义


定义类与定义结构体类似,不过定义类使用的关键字是class

class ClassName
{
	// 类体
};	// 注意分号不要丢了

在类中,类体部分的内容称为类的成员, 类中的变量称为类的属性成员变量;类中的函数称为类的方法成员函数

简单定义一个日期:

class Date
{
	// 初始化
	void Init(int year)
	{
		year = year;
	}

    // 成员变量
	int year;
	int month;
	int day;
};

在上面的类中,成员函数Init()的代码是不是不太好看?无法区分形参与成员变量

为了避免以上情况,一般是给成员变量加个前缀或后缀,如下

class Date
{
	// 初始化
	void Init(int year)
	{
		_year = year;
	}

    // 成员变量
	int _year;
	int _month;
	int _day;
};

类的定义方式

类的定义方式有两种

1.声明和定义都放在类体中

class Date
{
	// 初始化
	void Init(int year)
	{
		_year = year;
	}

    // 成员变量
	int _year;
	int _month;
	int _day;
};

当成员函数写在类体中,可能会被编译器当成内联函数展开

2.声明和定义分离

声明放在头文件.h中,成员函数的实现放在.cpp文件中

// Date.h
class Date
{
	// 初始化
	void Init(int year);

    // 成员变量
	int _year;
	int _month;
	int _day;
};
// Date.cpp
#include "Date.h"
void Date::Init(int year)	// 成员函数名前需加上类名+域访问限定符::
{
	_year = year;
}

一般建议用第二种方法

类的访问限定符与封装


访问限定符

类有三个访问限定符:public(公有)、private(私有)、protected(保护),用来修饰类成员

1.被privateprotected修饰的成员只能在类中被访问,不能在类外被访问

class Date
{
private:
    // 被 private 修饰的类成员
	// 初始化
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};

这时如果访问类的私有成员

int main()
{
	Date d1;
	d1.Init(2024, 3, 26);
	return 0;
}

将会发生报错
请添加图片描述

2.被public修饰的成员可以在类外被访问

class Date
{
public:
	// 成员函数可以在类外被访问
	// 初始化
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	// 成员变量不可以在类外被访问
	int _year;
	int _month;
	int _day;
};

一般我们不期望成员变量可以在类外被访问,如果要修改成员变量,可以通过被public修饰的成员函数来改变成员变量

3.访问限定符的作用域

从访问限定符出现开始,直到遇到下一个访问限定符,这就是该访问限定符的作用域;如果直到结束都没有出现下一个限定符,那就作用到类域结束

4.如果类中没有写访问限定符,那么默认权限就是private;而struct为了兼容C语言,默认权限是public

封装

所谓封装,就是将成员与方法放在类中,再用访问限定符来规定谁能访问,谁不能访问。本质上是管控

封装的好处:

1.类中的数据不会被外部轻易修改,使数据更加安全;

2.将数据和方法放到类中,易于管理,使用也更加便利

类的作用域


与命名空间类似,类也开辟了一个作用域,类的成员都在类的作用域中

在类外定义成员时,需使用域访问限定符来说明成员属于哪个类

class Date
{
public:
	void Init(int year, int month, int day);
private:
	int _year;
	int _month;
	int _day;
};
// Date::指明类域
void Date::Init(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

类的实例化


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

// 定义一个类
class Date
{
	// 类体
};

Date d1; // 类的实例化

类是用来描述对象的,本身就类似一种概念,定义类不占用空间;而通过类类型实例化出来的对象,则占用物理空间,用来存储成员变量

可以理解为:定义出来的类相当于建房子的图纸,而实例化的类对象就是依据图纸建出来的房子
图纸和房子是随便找的,对不上很正常(

类对象的存储


上面我们说过,类对象会占用物理空间用来存储成员变量,而我并没有说存储成员函数。至于为什么,我们稍后就清楚了

类对象中可能会有很多不同类型的变量,那又是如何存储的呢?——类对象采用的存储方式与结构体的存储方式相同:内存对齐

内存对齐这里就不再赘述了,如果不清楚可以到网上去了解下,或者看我之前写过的文章:http://t.csdnimg.cn/qtVYb

下面我们来验证一下类对象的存储

class A
{
private:
	char _a;
	int _b;
	int _c;
};

int main()
{
	A a1;
	cout << sizeof(a1) << endl;
	return 0;
}

按照内存对齐的规则,a1的大小应该是 12

结果如我们所料
在这里插入图片描述

这时我们再来看成员函数的存储问题,我们在类中写一个函数,再来看看类对象的大小会如何

class A
{
 public:
    void Print()
    {
        cout << "Print" << endl;
    }
private:
	char _a;
	int _b;
	int _c;
};

int main()
{
	A a1;
	cout << sizeof(a1) << endl;
	return 0;
}

直接看结果
在这里插入图片描述

还是12,这就说明类对象会存储成员变量,而不会存储成员函数。这是为什么?

我们假设由一个类实例化了多个类对象,每个类对象不仅会存储自己的成员变量,还会存储成员函数
在这里插入图片描述

根据需要,每个类对象中的成员变量的具体数据可能会不同,但是成员函数一定是相同的。当不同的类对象都会调用成员函数a,每个类对象都存着成员函数a,每个成员函数a都是相同的,这就有点浪费空间了

所以成员函数与成员变量的存储是分离的,成员函数独立存储于类对象之外。成员函数只需存储一份,不同的类对象需要时就调用同一函数,节约了空间
在这里插入图片描述

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(2023, 3, 28);
	d2.Init(2023, 3, 29);
	d1.Print();
	d2.Print();
	return 0;
}

运行结果:
在这里插入图片描述

如上代码中,使用日期类实例化了两个对象,d1d2,随后两个对象分别调用初始化函数和打印函数

那么问题来了,成员函数中并没有对不同对象进行区分,那当d1调用函数时,函数是如何知道调用者是d1,而不是d2呢?——C++中通过引入this指针解决该问题

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

意思就是,每个对象调用成员函数时,都会将指向自己的指针传给函数,成员函数也会根据这个指针来对对象进行处理。这个指针对外隐藏,指针的传递和解引用工作不需要用户去做,编译器自己就做了

编译器处理后的代码可以这样理解(部分代码):

class Date
{
public:
	void Print(Date* this)
	{
		cout << this->_year << '/' << this->_month << '/' << this->_day << endl;
	}
// ...
};	

// ...
	d1.Print(&d1);
	d2.Print(&d2);
// ...

注意:在实际编程中,无论是形参还是实参,都不可以显示写this指针,但是在成员函数中可以使用,如下

class Date
{
public:
	void Print() // 不可以显示写this指针
	{
        // 函数中可以使用
		cout << this->_year << '/' << this->_month << '/' << this->_day << endl;
	}
// ...
};	

// ...
	d1.Print(); // 不可以显示写this指针
	d2.Print();
// ...

this指针的特性

1.this指针不可修改,this指针的类型是这样的:类类型* const this,如:Date* const this

2.this指针是成员函数的第一个参数,只不过是隐藏的

3.this指针本质就是一个形参,类对象不会储存this指针

4.不可以在形参和实参中显示写,在函数中可以使用

小练习

以下代码运行结果: A.编译出错 B.运行崩溃 C.正常运行

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

int main()
{
	A* a = nullptr;
	a->Print();
	return 0;
}

来看结果:
在这里插入图片描述

运行成功,下面来作解释

A首先排除,即便有空指针问题,那也不是语法问题,编译肯定是过得去的

B和C,有人可能会有疑问,a->Print()这不是对空指针解引用了吗,为什么不崩溃?——我们知道,成员函数并没有存储在对象中,即使这样写了解引用,编译器也不会傻傻的去解引用,而是直接去找成员函数了

下面对代码稍作修改,以下代码运行结果: A.编译出错 B.运行崩溃 C.正常运行

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

int main()
{
	A* a = nullptr;
	a->Print();
	return 0;
}

ok,直接看结果:
请添加图片描述

程序崩溃了

编译器在这一步a->Print()不是不会解引用吗?怎么崩溃了?

其实导致崩溃的是这里,打印成员变量时,对this指针进行了解引用

void Print()
	{
		cout << _a << endl;
	}

this指针是什么?是形参;是谁的形参?是指向对象的指针的形参

而指向对象的指针A* a = nullptr是空指针,对空指针解引用自然就导致崩溃

结束,再见 😄

  • 11
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿洵Rain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值