[C++基础](3)类和对象(上)|初步理解类和对象及其定义|this指针

本文介绍了面向过程与面向对象的概念,强调面向对象关注对象的属性和功能,通过类和对象实现封装。类的定义包括class和struct,访问限定符用于控制成员的访问权限。类的作用域、实例化、内存分配以及对象大小的计算等概念也被详细解释。此外,还讨论了this指针在成员函数中的作用。面向对象编程通过封装实现数据保护,提高了代码的可维护性和复用性。
摘要由CSDN通过智能技术生成

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

面向过程:关注的是过程,分析出求解问题的步骤,通过函数 用逐步解决问题。如C语言

面向对象:关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互来完成。如C++

比如外卖系统这件事:

  • 面向过程的角度:1.商家上架商品 2.用户点外卖 3.商家接单 4.分配骑手 5.商家完成订单 6.骑手派送 7.用户评价

  • 面向对象的角度:三个对象:商家、用户、骑手。商家拥有商品,可以上架商品、接单、完成订单;用户拥有钱,可以点外卖和评价;骑手可以接单并派送。

从上面的例子可以看出,面向过程是想到一步写一步,是以事件为中心,一步一步完成。简单的问题可以通过面向过程的思路来解决,比较直截了当。但是面对大规模问题,面向过程就显得不足了。面向对象则是把问题拆分成各个对象,每个对象有自己的属性和功能,建立对象的目的不是为了完成某一个步骤,而是描述某个对象在解决问题时的属性和行为。

具体到编程中,面向过程就是要我们关注过程中要实现哪些方法和哪些函数。面向对象则要关注需要实现哪几个类,这几个类之间有什么关系,如何交互。

更具体的区别需要我们在后面的学习中慢慢体会~

类和对象的定义

类有两种定义方式,主要使用class定义类(class)。方法类似于C语言结构体,但是其中不仅可以声明变量,也可以定义函数。

class定义类

class classname
{
	//类体:包含成员变量和成员函数
};

class是C++定义类的关键字。

类中的元素称为类的成员,类中的数据称为类的属性成员变量,类中的函数称为类的方法成员函数

class student
{
	void Init(const char* name, const char* gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}

	void Print()
	{
		cout << _name << " " << _gender << " " << _age << endl;
	}

	char _name[20];
	char _gender[3];
	int _age;
};

👆:我们通常习惯在成员变量名前加一个_作为标识。如果不加,Init函数内的_age = age就会变成age = age,导致形参和成员变量无法区分。

struct定义类

C++将struct升级成了类,其内部也可以定义函数,所以我们也可以用struct定义类

struct student
{
    //类体同上
};

C++兼容C语言以struct+结构体名作为类型名定义变量的方式,但是C++还可以直接把类名当作类型名。

struct student s1; //C语言
student s2;        //C++

所以C++中定义类的时候一般就不需要typedef了。定义出的变量在C++中就叫做对象(object)

struct定义的类创建好对象后,我们可以直接调用其中的函数:

void test1()
{
	student s1;
	s1.Init("李白", "男", 20);
	s1.Print();
}
//结果:李白 男 20

回到上述外卖系统的例子,商家、骑手、用户就是三个类,每个类都可以定义出多个具体的对象,比如商家类可以定义出肯德基对象、麦当劳对象… … 骑手类可以定义出骑手1对象,骑手2对象… … 用户类可以定义出用户A对象,用户B对象… … 他们拥有各自的属性和功能,之间进行各种互动,来组成这个外卖系统。

访问限定符

面向对象的三大基本特征之一就是封装

C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用

用法

访问限定符有3个:

  • public:修饰的成员在类外可以被直接访问。
  • private:修饰的成员不能在类外被直接访问。
  • protected:现阶段,我们认为它和private类似。学到继承时才可以知道它们的区别。

修饰方法:访问限定符+:

修饰范围:从一个访问限定符向下直到下一个访问限定符或者类的结尾为止都是这一个访问限定符的修饰范围。

说明:class定义的类默认访问权限为private,struct定义的类默认访问权限为public(为了兼容C)

上述student类加上访问限定符后如下:

class student
{
public:
	void Init(const char* name, const char* gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}

	void Print()
	{
		cout << _name << " " << _gender << " " << _age << endl;
	}
private:
	char _name[20];
	char _gender[3];
	int _age;
};
void test2()
{
	student s;
    cout << s._name << endl;//此处报错:成员 "student::_name" (已声明 所在行数:20) 不可访问
	s._age = 20;//此处报错:成员 "student::_age" (已声明 所在行数:22) 不可访问
}

👆:只可以使用public修饰的(Init和Print函数),如果没加public,那么这两个函数也不能在类外被直接访问,因为class定义的类默认访问权限为私有。

意义

我们知道,C语言的数据和方法是分离的,而分离不便于很好的管理。

举个例子,C语言定义一个的结构体,函数是写在结构体外的,然后我们进行创建变量,初始化,入栈的基本操作:

struct Stack
{
	int* _a;
	int _top;
	int _capacity;
};
void StackInit(struct Stack* ps)
{
	//略
}
void StackPush(struct Stack* ps, int x)
{
	//略
}
int StackTop(struct Stack* ps)
{
	//略
}
void test3()
{
	struct Stack st;
	StackInit(&st);
	StackPush(&st, 3);
	StackPush(&st, 4);
	StackPush(&st, 5);
	printf("%d\n", StackTop(&st));//规范
	printf("%d\n", st._a[st._top]);//不规范
}

要取栈顶元素,有两种方法,一是调用给出的StackTop函数,二是直接访问数组中的元素。很明显后者是不规范的。

写栈的人定义的_top有可能表示的是栈顶元素的下一个位置,也有可能就是栈顶元素的位置,使用者应该用它给好的函数接口,而不是自己去随意访问。

所以这里就存在误用,设定访问权限可以有效避免这种误用。

C++的做法:

把数据和方法封装在一起,想给你自由访问的设计成公有,不想给你自由访问的设计成私有。

class Stack
{
public:
	void Init()
	{
		//略
	}
	void Push(int x)
	{
		//略
	}
	int Top()
	{
		//略
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

在研究类和对象阶段,我们只研究类的封装特性,根据以上研究,可以给出如下定义:

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

类的作用域

定义一个类的同时也定义了一个新的作用域,类的所用成员都在类的作用域中。在类外定义成员,需要使用::来指定该成员属于哪个类域。

当我们的声明和定义分离时需要用到这一点。

如.h文件这样写:

class Stack
{
public:
	void Init();
	void Push(int x);
	void Pop();
private:
	int* _a;
	int _top;
	int _capacity;
};

.cpp文件就应该这样写:

void Stack::Init()//在函数名前面加了Stack::,表示这是类的成员函数
{
	_a = nullptr; //虽然代码写在类外面,但它是成员函数,这里依然属于类内访问
	_top = 0;
	_capacity = 0;
}
void Stack::Push(int x)
{
	//略
}
void Stack::Pop()
{
	//略
}

小概念:成员函数声明和定义不分离,全部放在类体中,编译器默认可能会将其当作内联函数处理。

也就是说,一般情况下,短小的函数可以直接在类里面定义,长的函数建议声明和定义分离

类的实例化

理解类和对象的内存分配

首先应该明确一点,类内的函数可以是函数声明,也可以是函数定义,而类内的成员变量一定是声明(类似于C语言的结构体)。我们知道,函数的声明和定义以及变量的声明都不会在栈区开辟空间,它们都存放在代码区中。

也就是说,定义出的类并没有分配实际的内存来存储它,一个类可以实例化出多个对象,实例化的对象才占用实际的物理空间

打个比方,类内限定了有哪些函数和变量,它相当于房屋建造的设计图,而实例化的对象根据类的变量声明和函数定义才有了具体的参数以及功能,相当于按照设计图建造出来的房屋,当然就需要实际的空间去存储。

对象大小的计算

那么问题来了,对于栈类,下面代码的结果是多少?

class Stack
{
public:
	void Init();
	void Push(int x);
	void Pop();
private:
	int* _a;
	int _top;
	int _capacity;
};
cout << sizeof(Stack) << endl;

答:32位平台下的结果为12,64位平台下结果为16

解:首先了解以下两点

  1. sizeof(类型名)表示该类型实例化的对象所占内存的大小。
  2. 如果一个类实例化出多个对象,那么它们所保存的值是各不相同的,但是成员函数对它们来说是公共的

所以实例化的对象只需要定义具体的变量函数并不存在对象之中

也就是说,计算对象的大小,只要考虑成员变量的大小,不考虑成员函数。同C语言结构体类似,需要考虑内存对齐

此处有坑🕳

以下代码的结果是多少?

class A
{
public:
	void f()
	{}
};
void test4()
{
	cout << sizeof(A) << endl;
}

答案:1

解:虽然A类没有成员变量,但是实例化的对象依然要占一个字节,表示该对象存在。

this指针

上面提到多个对象共用同一份成员函数,对象里面并不会单独存一份函数。那么成员函数是怎么知道自己被哪个对象所调用了呢?

如下代码:

class ThisTest
{
public:
	void Init(int value)
	{
		_value = value;
	}
	void Print()
	{
		cout << _value << endl;
	}
private:
	int _value;
};

void test5()
{
	ThisTest t1;
	t1.Init(10);
	ThisTest t2;
	t2.Init(20);
    
	t1.Print();//结果:10
	t2.Print();//结果:20
}

结果不一样,但是查看汇编指令发现:

	t1.Print();
00374713  lea         ecx,[t1]  
00374716  call        ThisTest::Print (03713E3h)  //①
	t2.Print();
0037471B  lea         ecx,[t2]  
0037471E  call        ThisTest::Print (03713E3h)  //②

①②两处调用的函数地址相同,确实是同一个函数。


以前我们在用C语言写栈和队列的时候,每个函数都要有一个结构体指针,调用时还要取地址传参。

类似于这个原理,在C++中,这个指针叫做this指针this指针是隐含的,由编译器进行处理,this指针指向调用该成员函数的对象。

上述类经过编译器处理后会变成这样:

class ThisTest
{
public:
	void Init(ThisTest* const this, int value)//形参部分的处理
	{
		this->_value = value;//函数内部的处理
	}
	void Print(ThisTest* const this)//形参部分的处理
	{
		cout << this->_value << endl;//函数内部的处理
	}
private:
	int _value;
};

void test5()
{
	ThisTest t1;
	t1.Init(&t1, 10);
	ThisTest t2;
	t2.Init(&t2, 20);

	t1.Print(&t1);//调用部分的处理
	t2.Print(&t2);
}

注意

  1. C++中this指针是隐含的,在参数部分,我们不能显示的去写
  2. this指针是被const修饰的,它的指向不能被修改
  3. this指针我们是可以使用的
class ThisTest
{
public:
	void Init(int value)
	{
		this->value = value;
	}
	void Print()
	{
		cout << this->value << endl;
	}
private:
	int value;
};

👆:在这个场景下,由于我们习惯不好,成员变量的命名没有加_标识。那么我们还可以使用this->value = value来区分成员变量和形参,使程序正常运行。


面试题

1.以下程序的运行结果 A.编译报错 B.运行崩溃 C.正常运行

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

答案:C

首先可以排除A,因为我们最怀疑的是存在空指针的解引用,而这种问题一定是在运行时才能发现,不会出现编译报错。

其次,这里并不存在空指针的解引用,因为调用函数并不会去访问对象内部,show函数的this指针虽然是nullptr,但内部没有解引用,所以程序正常运行。

2.以下程序的运行结果 A.编译报错 B.运行崩溃 C.正常运行

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

答案:B

这里就是运行崩溃了,在PrintA函数内部出现了空指针解引用。

3.this指针存在哪里?

注意,this指针不可能在类里,因为类根本没分配空间,也不可能在对象里,我们在计算对象大小的时候并没有包括this指针。

this指针其实也是形参,在函数栈帧里,也就是在栈区

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

世真

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

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

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

打赏作者

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

抵扣说明:

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

余额充值