c++概念——类和对象的初步认识

类和对象的初步认识

在上一个部分我们已经初步认识了c++中的一些概念。这个章节将开始c++的第一个重要内容——类和对象进行介绍。由于这个部分内容非常多,我将用多篇文章进行讲解。本文是对类和对象的初步认识部分

类的定义

对于类的理解,其实我们可以把他理解为一个变量。即c++中引入了一个新的变量类,而我们现在就是需要对这个特殊的变量进行了解学习并应用。

类定义格式

定义一个类,需要用到关键字class。就好比我们以往实现过的栈这个数据结构类型,定义方式为:

typedef int STDataType;
class Stack{
public:
   void StackInit(int n = 4){
       //栈的初始化实现
   }
   //...一系列栈的方法
   void StackDestroy(){
       //栈的销毁实现
   }
private:
   STDataType* _a;
   int _top;
   int _capacity;
};//分号不能省略

上述就是类的定义方式,里面有很多新的东西。但是不用着急,我们慢慢来讲。

这个方式和结构体类型的变量的定义方式有一些类似,只不过在c语言中,结构体变量内部是不能定义函数的。而类变量是可以。我们称这些函数为成员函数。内部的变量就是成员变量
但是:在这里先说一个结论,c++中规定了struct也是可以内部定义函数的。
但是一般情况下还是会更多的使用类class。

为了区分成员变量,一般习惯上成员变量会加⼀个特殊标识(如上面写的 _a等 ),如成员变量前面或者后面加 _ 或者 m开头,注意C++中这个并不是强制的,只是⼀些惯例。避免和成员函数的参数相冲。

还有要注意的是,对于成员函数,c++默认这些为内联函数(inline)。这是需要注意的。也就是说,这些成员函数在调用的时候是不会开辟函数栈帧的,而是会直接将语句展开调用。

访问限定符

我们又看到,类的定义中又有两个关键字:publicprivate。这是干什么用的呢?

这两个是访问限定符。从它们的中文意思就可以看的出来了。
public即公共的,是可以访问的。
private即私有的,即不能访问。
当然还有一个访问限定符是protected,而当前的知识无法对这个进行更深入的讲解,当前就暂且认为它和private一样是私有的即可。

这个是很好理解的,对于有些类,其成员变量肯定是有一些是不想被外界直接进行访问的。这样是十分不安全的,就必须要通过类提供的成员函数进行访问。这样子不仅安全,且更加的规范。所以c++对于类的使用进行了访问限定

类的默认访问限定

那么如果一个类中没有写任何的访问限定符时会如何处理呢?

结论:对于没有访问限定符的类,c++默认这些成员为私有的(private)。
这样就不太合适了。大部分情况下还是需要对类中的变量进行操作的。全部为私有成员还是不太合适。所以还是需要对public区域和private进行区分的。

访问限定符的操作范围

当然,一个类中可以写多个访问限定符。我们得直到访问限定符的操作范围。

c++规定,访问限定符的限定范围是:当前访问限定符与下一个访问限定符之间的位置。如果后续没有访问限定符,那就一直到类定义结束的位置(即末尾的}位置)。

这样子我们就可以对多个成员进行有效的分区了。但是,这样写是非常麻烦且没必要的,所以一般来说都是将公共区的成员放在一起写在类的上半部分,私有的写在下半部分直到结束。

类与结构体的区别

在前面提到过,c++其实是对结构体的使用进行改进,甚至可以说是升级了。在c语言中,结构体内部只能存储变量,如int、double、数组、甚至是结构体的嵌套。无法存定义函数。

而c++中则对这个用法进行了改进,结构体中也可以定义函数。

二者是有区别的:class定义成员没有被访问限定符修饰时默认为private,struct默认为public。

类域

以往提到过,c++中有四个域:全局域、函数局部域、命名空间、类域

其实就是{ }包含的范围内就是一个域。c++是支持不同域中的命名相同的,而同一个域中不行。只不过访问的时候需要使用域作用限定符::进行访问使用。

当然,类中是可以只定义函数而不进行实现的。可以将函数的实现放在其他地方实现,即让成员函数的定义和实现分离。但是需要注意的是,在外部实现,需要使用域作用限定符访问。

typedef int STDataType;
class Stack{
public:
   void StackInit(int n = 4);
   //...一系列栈的方法
   void StackDestroy();
private:
   STDataType* _a;
   int _top;
   int _capacity;
};//分号不能省略

void Stack::StackInit(int n = 4){
    STDataType* tmp = (STDataType*)malloc(sizeof(STDataType) * n);
    if (!tmp) {
	    cout << "内存开辟失败" << endl;
	    exit(-1);
    }
   _a = tmp;
   _top = 0;
   _capacity = n;
}
//...一系列方法的实现
void Stack::StackDestroy(){
   free(_a);
   _a = nullptr;
   _top = _capacity = 0;
}

实例化应用

我们先来理解一下什么是实例化。

我们已经基本讲述完一个类的创建。但是这毕竟是一个变量的类型,我们不能直接使用。就好比我们在c语言学习结构体的时候,直接把struct进行使用。这是不对的。

那我们就应该声名变量。这就是实例化。

我们可以举一个浅显易懂的例子:
创建这个类对象就好比建房子的设计图纸,上面只是规定了哪里应该怎么做,房间多大,排线如何等。而真正要住的时候就是要根据这个图纸建造出房子才能使用。不可能直接住在房子里面吧。而类的实例化就是这样,我们虽然创建了类对象,但是没办法用。需要以此为基础来创建一系列的类,为其内部的变量开辟空间存储。

在这里插入图片描述

计算类的内存大小

既然实例化是为类中的成员开辟内存空间进行存储,那该如何计算类的大小呢?
因为类中有声名的成员函数,那成员函数在类中是以什么方式存在呢?

带着这些问题,我们来研究一下。

类在内存中的大小计算方式其实和c语言中结构体的内存大小计算方式是一模一样的。即需要通过内存对其的方式来进行计算大小。然后需要注意的是,类中虽然定义了函数,但是函数其实并不是存储在类当中的。即函数是类进行调用的。

对于内存对齐早已讲过,这里就不再赘述,感兴趣的读者可以去这篇文章看看:c语言自定义类型——结构体、联合、枚举——结构体在内存中的存储

所以我们只需要计算一下内部的成员变量,如整形数据、浮点型数据、数组等的总对齐内存大小。而对于函数我们不做计算。

其实函数不存储在类当中就是为了效率考虑的。这是c++的特点,记住就行。

对成员的调用

那我们现在创建了一个实例化对象,又应该如何调用呢?

这里我们创建一个简单的日期类进行举例:

class Date {
public:
	void InitDate(int year = 0, int month = 0, int day = 0); 
	void Print();
private:
	int _year;
	int _month;
	int _day;
};

void Date::Print() {
	cout << _year << "/" << _month << "/" << _day << endl;
}

void Date::InitDate(int year, int month , int day ) {
//带有缺省值的函数定义 实现的时候不能写缺省值
	_year = year;
	_month = month;
	_day = day;
}

现在我们就创建了一个日期类 ,比较简单,但仅仅只是举例子。

int main() {
	//实例化
	Date d1;
	d1.InitDate(2025, 4, 3);
	d1.Print();
	Date* pd1 = &d1;
	d1->InitDate();
	d1->Print();
	return 0;
}

其实调用和结构体还是很像的。如果当前实例化的是一个类变量,使用操作符.就可以了。如果是指针,就是用->调用。

来看看效果:
在这里插入图片描述
很明显符合预期的效果。

this指针

但现在有一个问题,为什么实例化一个对象后,调用成员函数就能直接访问其内部成员变量呢?这是如何识别到的呢?

这个时候就得提到一个特殊的指针:this指针。

编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this指针。比如Date类的Init的真实原型为:
void Init(Date* const this, int year,int month, int day)
第一个参数是隐含的,是编译器在编译的时候就自行处理了的。不需要我们写。写了会报错!

这个this指针就是存放着调用成员函数的那个类的地址,然后这样子就直到是如何处理的了。虽然this不能自行写入参数,但是我们可以在成员函数的内部进行使用:

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

void Date::InitDate(int year, int month , int day ) {
//带有缺省值的函数定义 实现的时候不能写缺省值
	this->_year = year;
	this->_month = month;
	this->_day = day;
}

我们可以这样子写函数。但是这个this指针编译器自己会处理,所以写不写都可以。但是this指针也是有用处的,有时候会作为返回值。这个我们后面部分再讲。

也就是说,对于上面的调用,都是讲地址传入给this指针然后进行成员函数的调用,这样就很简单地能知道调用的是哪个对象的的成员了。

注意事项

这里我们来讲一下注意事项,举两个例子:

例子1:

#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;
}

来判断一下这个程序是以下哪种情况:
A.编译错误 B.程序崩溃 C.正常运行

首先,对空指针的引用是不会编译错误的,只是会导致程序崩溃。
很多人这么想之后一看,欸,这不就是对空指针解引用了嘛。引用内部的成员函数。那这么想就大错特错了。虽然确实是将nullptr传给了this指针。
但是:要注意的是,我们在前面提到过,类中是不存放函数的,所以虽然看上去像是解引用,但其实是去调用函数了。而且print函数中并没有调用任何的内部成员,只是进行打印操作,所以肯定是正常执行的:
在这里插入图片描述

有了这个基础,我们来看看下一个例子就简单多了:
<font color=“pink">例子2:

#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;
}

有了上面的例子做铺垫就简单的多了,问题就在于:cout << _a << endl;
虽然调用函数没有对空指针解引用,但是我们知道,在成员函数内部,调用是通过this指针,所以也可以写成cout << this->_a << endl;

而此时的this指针又是空指针,那这个时候就对空指针解引用了。这就会导致程序崩溃。

还需要注意的是,由于this指针只存在于成员函数内部,一旦出了局部域就不存在了,根据这个特点很容易知道,this指针是存放在栈区的,仅在函数栈帧内存在。

类的封装性——用栈来引例

这个部分主要还是展示一下c++和c语言的区别。主要在于c++的多态性、继承性、封装性。
先来展示以下其封装性:

我们以栈的实现为例子:

c语言版本:

//Stack.h
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<stdbool.h>

typedef char STDataType;

typedef struct Stack
{
	STDataType* a;
	int top;		// 栈顶
	int capacity;  // 容量 
}Stack;

void StackInit(Stack* ps);//栈的初始化

void StackPush(Stack* ps, STDataType data); //入栈 

void StackPop(Stack* ps);//出栈

STDataType StackTop(Stack* ps);//获取栈顶元素 

int StackSize(Stack* ps);//获取栈中有效元素个数 

int StackEmpty(Stack* ps);//检测栈是否为空,如果为空返回非零结果,如果不为空返回0 

void StackDestroy(Stack* ps);//销毁栈 

//Stack.c
#include"stack.h"

void StackInit(Stack* ps) {
	assert(ps);
	ps->a = NULL;
	ps->capacity = ps->top = 0;
	//top指向栈顶 如果一开始定义了top为0 此时没有元素 所以top指向的是栈顶元素的后一个位置
}

void StackPush(Stack* ps, STDataType data) {
	assert(ps);
	if (ps->capacity == ps->top) {
		//需要扩容
		int tmpsize = (ps->capacity == 0) ? 4 : ps->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(ps->a, tmpsize*sizeof(STDataType));
		if (!tmp) {
			perror("realloc");
			exit(-1);
		}
		ps->a = tmp;
		ps->capacity = tmpsize;
	}
	ps->a[ps->top] = data;
	ps->top++;
}

void StackPop(Stack* ps) {
	assert(ps);
	ps->top--;
}

STDataType StackTop(Stack* ps) {
	assert(ps);
	return ps->a[ps->top - 1];
}

int StackSize(Stack* ps) {
	assert(ps);
	return ps->top;
}

int StackEmpty(Stack* ps) {
	assert(ps);
	if (ps->top==0) return 1;
	else return 0;
}

void StackDestroy(Stack* ps) {
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->capacity = ps->top = 0;
}

这是标准的c语言实现过程。

我们来看看c++版本(注:由于这里没有设计类的构造函数等,所以只是简单展示)

#include<iostream>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
using namespace std;

typedef char STDataType;
class Stack {
public:
	void StackInit(int n = 4) {
		STDataType* tmp = (STDataType*)malloc(sizeof(STDataType) * n);
		if (!tmp) {
			cout << "内存开辟失败" << endl;
			exit(-1);
		}
		_a = tmp;
		_top = 0;
		_capacity = n;
	}

	void Push(STDataType x) {
		if (_top == _capacity) {  
			int tmpnum = _capacity * 2;  
			STDataType* tmp = (STDataType*)realloc(_a,tmpnum);
			if (!tmp) {
				cout << "内存开辟失败" << endl;
				exit(-1);
			}
			_a = tmp;
			_capacity = tmpnum;
		}
		_a[_top++] = x;
	}

	void pop() {
		assert(_top > 0);
		_top--;
	}

	bool StackEmpty() {
		return _top == 0;
	}

	int StackSize() {
		assert(_top > 0);
		return _top - 1;
	}

	STDataType StackTop() {
		assert(_top > 0);
		return _a[_top - 1];
	}

	void StackDestroy() {
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	STDataType* _a;
	int _top;
	int _capacity;
};

我们发现最大的特点就是对于传参部分是极大简化了。且初始化阶段可以通过缺省参数对需要的空间进行一次性的开辟。避免多次的扩容。

我们以之前Leetcode上的有效的括号为例子看看c++的实现:

int Is_Match(char s1, char s2) {
	if (s1 == '(' && s2 == ')')return 1;
	else if (s1 == '[' && s2 == ']')return 1;
	else if (s1 == '{' && s2 == '}')return 1;
	else return 0;
}

bool isValid(char* arr) {
	Stack st;
	st.StackInit();
	while (*arr != '\0') {
		if (*arr == '(' || *arr == '[' || *arr == '{') {
			st.Push(*arr);
		}
		else {
			if (st.StackEmpty()) { 
				st.StackDestroy(); 
				return  false;
			}
			STDataType top = st.StackTop();
			st.pop();
			if (!Is_Match(top, *arr)) {
				st.StackDestroy();
				return false;
			}
		}
		arr++;
	}
	bool flag = st.StackEmpty();
	st.StackDestroy();
	return flag;  
}

我们发现最大的有点就是代码更好理解了,且对应的函数不需要传参了。

当然在这还没有学习构造函数和析构函数,否则对代码的简化将会更优,这一点放在下一篇文章来讲。

到此本篇文章就结束了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值