【C++初阶】类和对象(上)

【C++初阶】类和对象(上)

📃博客主页: 小镇敲码人
💞热门专栏:C++初阶
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞

1.面向对象与面向过程的初步认识

C++和C语言是不同的,C语言更关注过程,而C++关注更对象。

就好比我想把一头大象装进冰箱里面,C语言想要完成这件事需要先打开冰箱,然后把大象装进冰箱里面,这是具体的过程,C++并不关注具体的过程,C++会把整件事分为两个对象,冰箱和大象,靠对象之间的交互来完成这件事。

2.类的引入

在C语言中,我们学习了struct关键字所自定义的结构体类型,在C++中,我们在struct里面不仅仅可以定义一些变量,而且可以定义函数,struct升级变为了类。

我们在用C语言实现栈时,struct里面只能定义变量,现在我们用C++实现栈,会发现它里面也可以定义函数:

cpp 复制代码
typedef  int DataTypeStack;

struct Stack
{
	void init(size_t capacity)
	{
		_array = (DataTypeStack*)malloc(sizeof(DataTypeStack)*capacity);
		if (nullptr == _array )
		{
			perror("malloc failed");
			exit(-1);
		}
		_size = 0;
		_capacity = capacity;
	}
     
	void push(DataTypeStack x)
	{
		//满了就扩容
		if (_size == _capacity)
		{
			_capacity *= 2;
			DataTypeStack* tmp = (DataTypeStack*)realloc(_array,sizeof(DataTypeStack) * _capacity);
			if (nullptr == tmp)
			{
				perror("realloc failed");
				exit(-1);
			}
			_array = tmp;
		}
		_array[_size] = x;
		_size++;
	}
	DataTypeStack Top()
	{
		return _array[_size - 1];
	}
	void Destory()
	{
		free(_array);
		_array = nullptr;
		_size = 0;
		_capacity = 0;
	}
	DataTypeStack* _array;
	size_t _size;
	size_t _capacity;
};

int main()
{
	Stack St;
	St.init(4);
	
	for (int i = 0; i < 10; i++)
	{
		St.push(i);
		cout << St.Top() << endl;
	}
	St.Destory();
	return 0;
}

运行结果:

这样用起来是不是感觉比C语言里面的方便了很多呢?

3. 类的定义

虽然用struct也可以定义类,但是C++有了一个新的关键字class来定义类,它们有所区别,我们稍后再谈这个问题。

cpp 复制代码
class classname
{
  //类体:由成员变量和成员函数组成。
};

class后面是类的名字,类里面的变量又叫做类的属性,类的成员变量,类里面的函数又叫做成员函数或者成员方法。

我们创建类通常有两种方式:

  1. 类的声明和定义都放在类里面,注意:声明和定义都放在类里面,编译器可能会将类里面的函数当成内联函数处理。
  2. 类的声明放在.h头文件里,类里面成员函数的定义放在.cpp文件里面,需要在成员函数前面加上类名::
    注意:我们在刷题的时候可以用第一种方式,但是以后工作了肯定是用第二中方式最佳。

关于类里面的成员变量的命名的建议:

cpp 复制代码
class Date
{
	void init(int year, int month, int day)
	{
		year = year;
		month = month;
		day = day;
	}
	int year;
	int month;
	int day;
};

这样看着就很抽象,你该如何区分我的成员变量和函数的形参呢?

所以外面应该这样写:

cpp 复制代码
class Date
{
	void init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};

或者这样:

cpp 复制代码
class Date
{
	void init(int year, int month, int day)
	{
		year_ = year;
		month_ = month;
		day_ = day;
	}
	int year_;
	int month_;
	int day_;
};

具体看相应的要求,只要可以区分且易于知道成员变量的含义即可。

4.类的访问限定符及封装

4.1访问限定符

在类里面一般还有三个关键字,public(公用)、private(私有)、protected(保护)。

  • class里面不加访问限定符来修饰。里面的成员变量和成员函数默认是私有的,类外面访问是非法的。
  • struct里面不加访问限定符,默认里面的成员变量和成员函数是公有的,因为C++要兼容C语言。
  • public修饰表示公有在类外面可以访问,被privateprotected修饰表示私有在类外面不能被访问,它们两个没有什么区别。
  • 访问限定符的作用域从一直从访问限定符出现的位置开始,到下一个访问限定符出现的位置结束。如果后面没有访问限定符了,那访问权限到}就结束了。
  • 注意:访问限定符只在编译的时候有,当数据在内存里面时,没有这个概念。

C++structclass的区别:

1.struct里面默认的成员变量和函数是公有的,而class里面默认的是私有的。

  1. struct可以定义结构体但是类不行,但是struct可以用来定义类。

4.2封装

封装是类和对象的三大特性之一,其它的两大特性分别是:继承和多态。我们在类和对象阶段主要把封装搞清楚。

那什么是封装呢?

封装就是一种把数据和数据操作方法进行有机结合,把对象的属性和一些细节通过访问限定符的限制起来,只开放一些成员方法和外界进行交互。

就像我们使用电脑一样,电脑只对用户开放了USB接口,键盘、鼠标、显示器、开关机键,更重要的显卡、cpu则是封装起来了防止用户搞破坏,我们只需要使用它开放的一些工具就可以实现人机交互,C++的封装思想就类似于这个。

5.类的作用域

类定义了一个新的作用域,我们在类外面定义类里面的成员 函数时需要加上类名::

下面一段代码,帮助你知道如何使用:

cpp 复制代码
typedef  int DataTypeStack;
class Stack
{
public:
	void init(size_t capacity);
private:
	DataTypeStack* _array;
	size_t _size;
	size_t _capacity;
};
void Stack::init(size_t capacity)
{
	_array = (DataTypeStack*)malloc(sizeof(DataTypeStack) * capacity);
	if (nullptr == _array)
	{
		perror("malloc failed");
		exit(-1);
	}
	_size = 0;
	_capacity = 0;
}

这个类名::是紧跟在成员函数名前面的,不能加空格,否则编译器就会报错,相当于告诉编译器,这个函数是给叫做stack的类定义的。

6.类的实例化

类是对对象的描述,它就像建造一个工程所需要的图纸,是一个空格,是不占空间的,我们实例化对象的过程,就当于通过这个图纸建造房子了。

像如上代码,直接调用类里面的对象,没有实例化,是会报错的,这就像我们在C语言里面自定义了一个类型,但是没有用这个类型创建出一个变量那么我们就不能使用这个类型是一个道理。

我们想正确使用Stack这个类,应该按如下代码:

cpp 复制代码
typedef  int DataTypeStack;
class Stack
{
public:
	void init(size_t capacity);
private:
	DataTypeStack* _array;
	size_t _size;
	size_t _capacity;
};
void Stack::init(size_t capacity)
{
	_array = (DataTypeStack*)malloc(sizeof(DataTypeStack) * capacity);
	if (nullptr == _array)
	{
		perror("malloc failed");
		exit(-1);
	}
	_size = 0;
	_capacity = 0;
}

int main()
{
	Stack ST;
	ST.init(4);
	return 0;
}

6.类的对象的大小计算

那学会了实例化对象,我们就应该思考类对象的大小是如何计算的呢?我们想计算类对象的大小,就必须搞清楚一个问题,我们可以创建很多相同类的对象,那这些类对象是如何在内存中存储的呢?

我们假设了三种方式:

  1. 每一个对象都有自己的成员变量和成员函数,它们都会保存一份。
  2. 每一个对象只保存自己的成员变量,成员函数放在一个公共的代码段。
    到底结果如何我们可以用下面的代码来验证一下:
cpp 复制代码
#include<iostream>
#include<stdlib.h>

using namespace std;
//既有成员函数,又有成员变量
class c1
{
	void f1();
	int _a;
};

//只有成员函数
class c2
{
	void f2();
};

//空类,什么都没有

class c3
{

};

int main()
{
	c1 A;
	c2 B;
	c3 C;
	cout << sizeof(A) << endl;
	cout << sizeof(B) << endl;
	cout << sizeof(C) << endl;
	return 0;
}

运行结果:

空类比较特殊,编译器给了一个字节来唯一标识这个类的对象,可以看到外面c2不是空类是有一个成员函数的,它也为1,说明类对象的存储方式应该是第二种,只保存成员变量,成员函数放在公共的代码段。

这里计算类对象大小的方法和计算结构体的大小是一样的,注意内存对齐规则即可。

7.类的this指针

7.1this指针的引入

我们先来定义一个Stack的变量:

cpp 复制代码
#include<iostream>
#include<stdlib.h>

using namespace std;
typedef  int DataTypeStack;

class Stack
{
public:
	void init(size_t capacity)
	{
		_array = (DataTypeStack*)malloc(sizeof(DataTypeStack)*capacity);
		if (nullptr == _array )
		{
			perror("malloc failed");
			exit(-1);
		}
		_size = 0;
		_capacity = capacity;
	}
     
	void push(DataTypeStack x)
	{
		//满了就扩容
		if (_size == _capacity)
		{
			_capacity *= 2;
			DataTypeStack* tmp = (DataTypeStack*)realloc(_array,sizeof(DataTypeStack) * _capacity);
			if (nullptr == tmp)
			{
				perror("realloc failed");
				exit(-1);
			}
			_array = tmp;
		}
		_array[_size] = x;
		_size++;
	}
	DataTypeStack Top()
	{
		return _array[_size - 1];
	}
	void Destory()
	{
		free(_array);
		_array = nullptr;
		_size = 0;
		_capacity = 0;
	}
private:
	DataTypeStack* _array;
	size_t _size;
	size_t _capacity;
};

int mian()
{
	Stack S1;
	Stack S2;
	S1.init(4);
	S2.init(8);
	return 0;
}

有这样一个问题,S1、S2都是调用公共代码段的函数init,编译器该如何区分,是应该给S1的成员变量初始化呢,还是给S2的成员变量初始化呢?为了解决这个问题,C++引出了this指针的概念。

即:C++给每一个类的非静态成员函数,都隐式的添加了一个叫this指针的参数,它保存该对象的地址,在函数里面访问对象里的成员变量,都是通过this指针来访问的,只不过程序员不需要显式的写出来,这些都由编译器完成了。

7.2this指针的一些特性

  1. this指针的类型是*const,也就是说this保存的是对象的地址,这个地址是不能被改变的,否则,我们在成员函数里面就无法访问到对象的成员变量了。
  2. 只有在成员变量里面才能使用this指针。
  3. this本质是一个成员函数的一个形式参数,它的地址不保存在对象里面,当对象调用成员函数时将对象的地址传给this指针,它是成员函数的第一个隐式的指针形参,不需要用户自己传,编译器通过ecx寄存器自动传了。

this指针可以为空吗?答案是可以的我们用下面的代码来验证:

cpp 复制代码
#include<iostream>
#include<stdlib.h>

using namespace std;
class c1
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}
private:
	int _a;
};
int main()
{
	c1* ptr1 = nullptr;
	ptr1->Print();
	return 0;
}

这段代码的结果是什么呢?是正常运行还是崩溃呢?

可以看到程序是正常运行了的,有人可能有疑惑,ptr1不是空指针吗,对其进行->操作不是会导致程序崩溃吗,是的,但是这里编译器其实没有真正的对其进行->操作,

因为我们调用这个Print函数和类对象的地址是无关的,函数保存在公共的代码段,这里的ptr1->Print()操作只是起到了一个告诉编译器,Print函数是属于ptr1这个类对象的仅此而已,就相当我们在类外面定义类的成员函数要加类名::是相同的道理,如果你不信我们可以转到反汇编看一下:

再看下面一段代码,它的运行结果又是如何呢?

cpp 复制代码
#include<iostream>
#include<stdlib.h>

using namespace std;
class c1
{
public:
	void Print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	c1* ptr1 = nullptr;
	ptr1->Print();
	return 0;
}

运行结果:

这里程序crash了,我们应该知道,因为此时的this指针是一个空值,我们在Print函数里面访问_a,其实是this->_a,只不过编译器帮助我们完成了这个工作了而已,当然只要形参和函数参数里面不显式的写出来就可以,在访问对象的成员变量时,是可以显式的写出this指针的,这里对空指针进行->操作显然是非法的,所以程序crash是必然的。

  • 结论:this指针为空值,只要在调用的成员函数里面不使用this指针访问成员变量,程序是不会crash的。
相关推荐
gorgor在码农1 分钟前
redis 底层数据结构
java·数据库·redis
沐知全栈开发2 分钟前
基于PyTorch的DDSP设计源码及C/C++实现分析
c++·pytorch·源码分析·ddsp·c/c++实现
int WINGsssss2 分钟前
使用猴子补丁对pytorch的分布式接口进行插桩
人工智能·pytorch·python·猴子补丁
YRr YRr3 分钟前
为什么在PyTorch中需要添加批次维度
人工智能·pytorch·python
爱喝热水的呀哈喽4 分钟前
KAN解possion 方程,方程构造篇代码阅读
人工智能·pytorch·python
YRr YRr4 分钟前
详解 PyTorch 图像预处理:使用 torchvision.transforms 实现有效的数据处理
人工智能·pytorch·python
啊哈哈哈哈哈啊哈哈11 分钟前
P3打卡-pytorch实现天气识别
人工智能·pytorch·python
《源码好优多》15 分钟前
基于Java Springboot华为数码商城交易平台
java·开发语言·spring boot
我想探知宇宙24 分钟前
LangGPT结构化提示词编写实践
python
往日情怀酿做酒 V176392963824 分钟前
Django基础之路由
后端·python·django