C++初阶:类和对象(上)

✨✨小新课堂开课了,欢迎欢迎~✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C++:由浅入深篇

小新的主页:编程版小新-CSDN博客

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

面向过程:我们之前学习的C语言就是面向过程,以过程为中心,强调函数的实现,将问题分解为一系列的过程,然后调用函数解决问题。

面向对象:C++是基于面向对象的,强调的是对象与对象的联系将问题分解为一系列相互协作的对象,每个对象都有自己的状态和行为。

举例说明面向过程和面向对象的区别。

我们以借书为例。

借书分为三个过程:用户找借书机构,根据书名搜索图书,利用图书ID和用户ID借书。我们关注的是这三个过程。

借书也可以抽离出三个对象:用户图书馆。我们关注的是这三个对象之间的联系。

更具体的去解释就是,在面向过程的编程中,我们可能需要创建多个函数来处理不同的任务,比如searchBook(title)根据图书搜索书名,borrowBook(bookID,userID)利用图书ID和用户ID借书,listAvailiableBooks()图书馆提供的可借阅书籍。调用这三个函数就能解决借书问题。

在面向对象的编程中,我们会创建几个类来封装数据和行为。

图书类:包含书名,以及借阅的方法。

用户类:用户的信息,以及借书的方法。 

图书馆类:管理图书集合和用户集合,提供搜索,借阅图书的方法。

对比

2.类的引入

在C语言中,结构体中只能定义变量,但在C++中,结构体内不仅可以定义变量,还可以定义函数。C++兼容C语言中struct的用法,struct升级成了类。

2.1类的两种定义

第一种:用struct定义类

#include<iostream>
using namespace std;

// C++将struct升级成了类
// 1、类里面可以定义函数
// 2、struct名称就可以代表类型
// C++兼容C中struct的用法
typedef struct ListNodeC
{
	//变量
	struct ListNodeC* next;
	int val;

}LTNode;

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

	//变量
	ListNodeCPP* next;
	int val;

};

但是一般情况下,我们更推荐使用第二种。

第二种:用class定义类

其语法的基本结构如下

class className
{
    //类体:由成员函数和成员变量组成
};

#include<iostream>

using namespace std;

class Data
{
	int a;
	int b;
};

2.2单文件和多文件书写

类的声明和定义可以放在一个文件书写,也可以放在不同的文件中书写。

1.将声明和定义全放在类体内,编译器会将定义在类体内的成员函数默认为内联函数处理

#include<iostream>
using namespace std;

class Data
{
	//声明和定义放在一起
	int ADD(int x, int y)
	{
		return x + y;
	}

	int a;
	int b;
};

2.将声明放在.h文件中,定义放在.cpp文件中。要注意的是成员函数名前需要加类名: :

#include<iostream>
using namespace std;
#include"test.h"

//test.h
class Data
{
    //声明
	int ADD(int x, int y);

	int a;
	int b;

};

//test.cpp
//定义
int Data::ADD(int x, int y)
{
	return x + y;
}

注意:在一般练习时,我们更习惯于将声明和定义放在类内,但是在实际工程中,更倾向于采用第二种。

2.3成员变量的命名

为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加_ 或者 m
开头,注意C++中这个并不是强制的,只是一些惯例,具体看公司的要求。
之所以大家会约定俗成给成员变量加一个特殊标识,是为了解决可能存在的命名冲突的问题。
#include<iostream>
using namespace std;


class Date
{
	void Init(int year, int month, int day)
	{
		//命名冲突
		year = year;
		month = month;
		day = day;
	}
	int year;
	int month;
	int day;
};

如果对成员变量加以修饰的话,在视觉上就会很好的区分。

#include<iostream>
using namespace std;


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

3.类的成员访问限定符及封装

3.1访问限定符

在C++中类有三种访问限定符:publicprivateprotected。他们的作用各不相同。

public修饰的成员在类外可以直接被访问;
protectedprivate修饰的成员在类外不能直接被访问;
protected和private是⼀样的,以后继承章节才能体现出他们的区别。

3.2访问限定符的访问权限作用域

访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面有访问限定符,作用域就到 }即类结束。
这里要补充两个点: class定义成员没有被访问限定符修饰时默认为privatestruct默认为public(因为struct要兼容C)。
⼀般成员变量都会被限制为private/protected,需要给别人使用的成员函数会放为public。

3.3类的封装

封装: C++一种实现封装的方式,用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
我们可以将封装理解为是一种管理,举个例子,我们去爬长城,C语言没有封装的概念,就好比谁都可以去爬长城,那我们要想维护长城,就需要靠每个人的素质,这是不稳定,我们也可以理解为是难以维护。现在C++有了封装的概念,我们去爬长城就要购买门票(突破封装),刷身份证进入,那每天进出长城的人都有记录,如果其中有人在长城上乱涂乱画,我们就可以找到这个人,给予他惩罚,我们可以理解为易于维护。
仔细想想,我们使用类将数据和方法都封装起来。不想对外开放的就用 protected/private 封装起来,而用 public 封装的成员允许外界对其进行合理的访问。所以封装本质上是一种管理。

3.4类域

类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
类域影响的是编译的查找规则,下面程序中Init如果不指定类域Stack,那么编译器就把Init当成全
局函数,那么编译时,找不到array等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知
道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找。
#include<iostream>
using namespace std;

 class Stack
 {
 public:
	 // 成员函数
		 void Init(int n = 4);
 private:
	 // 成员变量
	 int* array;
	 size_t capacity;
	 size_t top;
 };


 // 声明和定义分离,需要指定类域
 void Stack::Init(int n)
 {
	 array = (int*)malloc(sizeof(int) * n);
	 if (nullptr == array)
	 {
		 perror("malloc申请空间失败");
		 return;
	 }
	 capacity = n;
	 top = 0;
 }


 int main()
 {
	 Stack st;
	 st.Init();
	 return 0;
 }

4.类的实例化

用类类型在物理内存中创建对象的过程,称为类实例化出对象
类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间
Data::_year = 2024;//error,并没有分配空间,不能初始化
一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。(后面我们会解释为什么只存储成员变量)
打个比方:类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,设计图规划了有多少个房间,房间大小功能等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。同样类就像设计图一样,不能存储数据,实例化出的对象分配物理内存存储数据

5.类对象模型

我们首先来思考一些问题,一个类中既有成员变量,又有成员函数,那一个类对象中有什么呢?我们该如何计算类对象的大小呢?

5.1类对象的存储方式猜测

分析一下类对象中哪些成员呢?类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?我们有以下几种猜测。
存储方式一
每次实例化出对象时,都开辟一个空间存储成员函数和成员变量。
首先函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。因此存储一的方式不可行
存储方式二
每次实例化出对象时,都开辟一个空间存储成员变量和成员函数的指针。
我们再分析一下,对象中是否有存储指针的必要呢?Date实例化d1和d2两个对象,d1和d2都有各自独立的成员变量_year/_month/_day存储各自的数据,但是d1和d2的成员函数Init/Print指针却是一样的,存储在对象中就浪费了。
如果用Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这里需要再额外哆嗦一下,其实函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在
运行时找,就需要存储函数地址。因此存储二的方式也不可行
我们也由此受到启发,成员函数是不存在类对象中的。
存储方式三
每次实例化出对象时,都开辟一个空间存储成员变量,成员函数单独存储在一个区域(公共代码段)。
已经破解了存储方式之谜,我们接下来看如何计算类对象的大小。

5.2类对象的大小

C++规定,实例化的对象要采用内存对齐的方式存储。

内存对齐规则
第一个成员在与结构体偏移量为0的地址处。
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的⼀个对齐数与该成员大小的较小值。
VS中默认的对对齐为8
结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
#include<iostream>
 using namespace std;
 class Date
 {
 public:
	 void Init(int year, int month, int day)
	 {
		 _year = year;
		 _month = month;
		 _day = day;
	 }
	 
 private:
	 int _year;
	 int _month;
	 int _day;
 };


 int main()
 {
	 Date d;
	 cout << sizeof(d) << endl;
	 return 0;
 }

运行结果:

 接下来我们看一个比较特殊的例子,如果类里面没有成员变量,只有成员函数,或者什么都没有呢?对象的大小是多少?

#include<iostream>
 using namespace std;

 class B
 {
 public:
	 void Print()
	 {
		 //...
	 }
 };

 class C
 {};

 int main()
 {
	 B b;
	 C c;
	
	 cout << sizeof(b) << endl;
	 cout << sizeof(c) << endl;
	 return 0;
 }

运行结果:

上面的程序运行后,我们看到没有成员变量的B和C类对象的大小是1,为什么没有成员变量还要给1个字节呢?因为如果⼀个字节都不给,怎么表示对象存在过呢!所以这里给1字节,纯粹是为了占位标识对象存在

 

6.this指针

6.1this指针的引入

我们来看下面一段代码

#include<iostream>
using namespace std;

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;
}
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用Init和
Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?(换而言之,d1和d2的成员变量是不同,但类里面的成员变量只是声明,没有开空间,不存在值,那么用同一个函数,怎么做到打印不同的值的呢?)那么这就要看到C++给了一个隐含的this指针解决这里的问题。

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

#include<iostream>
using namespace std;

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

		//this->_day = day;
		_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(&d1, 2024, 3, 31);
	d1.Init(2024, 3, 31);

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

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

	//d2.Print(&d2)
	d2.Print();
	return 0;
}

上述代码调用成员函数传参时,看似只传入了一些基本数据,实际上还传入了指向该对象的指针。

6.2this指针的特性

1.this指针不能修改:this指针的类型为**const**
2.this指针只能在成员函数内部使用
3.对象中并不存储this指针:this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参
4.this指针不需要用户传递:this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,
下面通过两道选择题巩固一下前面学习的知识。
例题一
1.下面程序编译运行结果是()
A、编译报错  B、运行崩溃 C、正常运行
#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;
}

这里我们可能会理解成对空指针解引用导致运行崩溃,其实不然,程序是正常运行的。这里并没有对空指针p进行解引用,因为Print等成员函数地址并没有存到对象里面,成员函数的地址是存在公共代码段的。虽然表面看上去是解引用,但实际上编译器不需要通过解引用去找对应函数,只需要去公共代码区执行对应函数即可。

例题二
1.下面程序编译运行结果是()
A、编译报错  B、运行崩溃 C、正常运行
#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;
}
这里为什么会运行崩溃呢,Print()函数中打印了成员变量_a,成员变量_a只有通过对this指针进行解引用才能访问到,而 this指针此时接收的是nullptr,对空进行解引用必然会导致程序的崩溃。

7.C++和C语言实现Stack的简单对比

通过下面两份代码对比,我们发现C++实现Stack形态上还是发生了挺多的变化,底层和逻辑上没啥变化。

C++中数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的⼀种体现,这个是最重要的变化。
C++中有一些相对方便的语法,比如Init给的缺省参数会方便很多,成员函数每次不需要传对象地
址,因为this指针隐含的传递了,方便了很多,使用类型不再需要typedef用类名就很方便
C语言实现:
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{
	STDataType* a;
	int top;
	int capacity;
}ST;
void STInit(ST* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = 0;
	ps->capacity = 0;
}
void STDestroy(ST* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}
void STPush(ST* ps, STDataType x)
{
	assert(ps);
	
		// 满了, 扩容
	if (ps->top == ps->capacity)
	{
		int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
	    STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity *
		sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		ps->a = tmp;
		ps->capacity = newcapacity;
	}

	ps->a[ps->top] = x;
	ps->top++;
}
bool STEmpty(ST* ps)
{
	assert(ps);
	return ps->top == 0;
}
void STPop(ST* ps)
{
	assert(ps);
	assert(!STEmpty(ps));
	ps->top--;
}
STDataType STTop(ST* ps)
{
	assert(ps);
	assert(!STEmpty(ps));
	return ps->a[ps->top - 1];
}
int STSize(ST* ps)
{
	assert(ps);
	
		return ps->top;
}
int main()
{
	ST s;
	STInit(&s);
	STPush(&s, 1);
	STPush(&s, 2);
	STPush(&s, 3);
	STPush(&s, 4);
	while (!STEmpty(&s))
	{
		printf("%d\n", STTop(&s));
		STPop(&s);
	}
	STDestroy(&s);
	return 0;
}

C++实现:

#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
	// 成员函数
	void Init(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	
	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
				sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}
	void Pop()
	{
		assert(_top > 0);
		--_top;
	}
	bool Empty()
	{
		return _top == 0;
	}
	int Top()
	{
		assert(_top > 0);
		return _a[_top - 1];
	}
	void Destroy()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	
	// 成员变量
	STDataType * _a;
	size_t _capacity;
	size_t _top;
};
int main()
{
	Stack s;
	s.Init();
	s.Push(1);
	s.Push(2);
	s.Push(3);
	s.Push(4);
	while (!s.Empty())
	{
		printf("%d\n", s.Top());
		s.Pop();
	}
	s.Destroy();
	return 0;
}
  • 24
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值