冰冰学习笔记:类与对象(上)

发布于:2023-02-15 ⋅ 阅读:(634) ⋅ 点赞:(0)

欢迎各位大佬光临本文章!!!

还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。

本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。

我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog

我的gitee:冰冰棒 (bingbingsupercool) - Gitee.comhttps://gitee.com/bingbingsurercool


系列文章推荐

冰冰学习笔记:《结构体存储方式》

冰冰学习笔记:《C++基础语法》


目录

系列文章推荐

前言

1.类的引入

2.类的定义

3.类的两种定义方式

4.类的成员命名风格

5.类的访问限定符以及封装

6.类的作用域与实例化

7.类对象的大小计算

8.this指针

总结


前言

        C++与C语言不同,C语言是面向过程,关注的是解决问题的过程,分析出解决问题的步骤,通过调用各种功能函数逐步解决问题。而C++是基于面向对象的,关注的是对象本身,将一件事情拆解成不同的对象,通过对象之间的交互进行完成。以外卖平台为例,C语言在实现时,可能需要下单函数,呼叫外卖员函数,取餐函数,送餐函数以及点评函数进行实现。反观C++,实现是可能创建用户,骑手,商家三个对象,通过三个对象之间的交互进行点餐送餐等服务的实现。

1.类的引入

        在学习C语言时,我们通过结构体来实现自定义类型,用来创建复杂的类型,C++中结构体不仅可以定义变量,也可以定义函数。例如用C++实现的数据结构栈中,函数可以定义在结构体内部。

struct Stack
{
	void Init(int newcapacity=4)
	{
		_a = (DataType*)malloc(sizeof(DataType) * newcapacity);
		_size = 0;
		_capacity = newcapacity;
	}
	void Push(int data)
	{
		if ( _size == _capacity )
		{
			BuyNode();
		}
		_a[_size++] = data;
	}
	DataType* _a;
	int _size;
	int _capacity;
};

        原因在于C++中将结构体struct进行了升级,使其不仅可以作为普通的结构体进行自定义类型创建,还将其升级成了类,只不过在C++中我们更喜欢用class来代替。 

2.类的定义

        类的定义与结构体极其相似,class为定义类的关键字,Class Name为类的名字,{ }中为类的主体,最后用分号结尾。类体中的内容称为类的成员:类中的变量称为类的属性或者成员变量;类中的函数称为类的方法或者成员函数

class Stack
{
public:
	void Push(DataType x);//成员函数
	void Print();
private:
	void BuyNode();
private:
	DataType* a = 0;//成员变量
	int size = 0;
	int capacity = 0;
};

        与C语言中的结构体不同,我们在定义类时即便不使用typedef也可直接使用类名进行操作,不需要添加class的前缀。

3.类的两种定义方式

(1)声明和定义全部放在类体中

        成员函数在类中进行定义,编译器会自动在前面增加inline关键字,有可能将其作为内联函数处理。

class Stack
{
public:
	void Init(int newcapacity=4)//自动处理成内联
	{
		_a = (DataType*)malloc(sizeof(DataType) * newcapacity);
		_size = 0;
		_capacity = newcapacity;
	}
	void Push(int data)
	{
		if ( _size == _capacity )
		{
			BuyNode();
		}
		_a[_size++] = data;
	}
private:
	DataType* _a;
	int _size;
	int _capacity;
};

(2)类声明与定义分开处理,注意:成员函数名前需要增加类名::

        那我们应该怎样选择呢?一般频繁调用,并且代码量不是很多的小函数我们都会实现在类里面,方便调用,当函数体过大时,我们就会分开实现,减少类体的代码量,方便阅读。 

4.类的成员命名风格

        当我们实现一个复杂的类后,里面不仅有成员变量,还有成员函数,函数中还有各种参数变量,函数体中又要实现各种算法运算,一个类中往往具有大量的变量,如果命名不规范,就会形成变量名混乱,容易犯错。如下面的例子中我们就很难区分成员变量和函数参数:

        因此我们需要具备一个良好的命名习惯,来预防上述情况的发生,增加代码的可读性。

        对于函数名来说,C++中大多数采用驼峰法进行命名,即单词和单词之间首字母大写间隔:StackPush。还可以单词全部小写,用_进行分割:stack_push

        对于成员变量,通常前面增加下划线进行区分:_day,_month,_year。

5.类的访问限定符以及封装

        在用C++定义类时,我们通常会用到访问限定符,这些限定符就是C++用来封装的方式。通过封装,C++将类与对象的属性和方法结合在一起,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用,用户只需要调用接口进行功能调用,不必关心内部实现的逻辑。使其代码更加安全,维护性高。

        C++中类的访问限定符有public、protected、private。

(1)public修饰的成员在类外可以直接访问,换言之,public修饰的成员是对外开放的。

(2)protected和private修饰的成员在类外不能直接被访问,只能在类中进行访问。

(3)访问限定符的作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。

(4)如果后面没有访问限定符,作用域到类的}结束。

(5)class的默认访问权限是为private,struct的默认访问权限为public,因为C++中struct需要兼容C语言的struct。

        我们知道:面向对象具有三大特性:封装、继承、多态。类和对象主要研究的就是封装。

那么什么是封装呢?

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

        封装本质就是一种管理,让用户更加方便的使用类,不必在乎太多的细节。

6.类的作用域与实例化

        类定义了一个新的作用域,类的成员都在类的作用域中。在类体外定义成员时,需要使用 ::作用域操作符指明成员属于哪个类域。否则无法访问类中定义的对象。

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

(1)类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。类中定义的成员变量仅仅是一个声明,,我们只有对类进行实例化,里面的成员才真正被创建出来。

(2)一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量

简单来说,类实例化出对象就像按照设计图设计出来的房子,而类就是图纸。

7.类对象的大小计算

        既然类并不是实例化,那么类是否具有大小呢?况且类中不仅具备成员,还有成员函数,大小是怎么分配的呢?

在对象存储方案的设计中,一共提出了三种方案:

(1)类成员变量,类成员函数存储在一起

         此种方式缺陷太过明显,我们在实例化类时,为每一个成员,函数都开辟不同的空间,但是类成员的函数我们只需要开辟一次即可,每次调用同一个函数就可以。此种方式造成了大量的空间浪费。

(2)函数代码只保存一次,在对象中保存存放代码的地址

(3)只保存成员变量,成员函数存放在公共的代码段

         C++在实现类的存储时采用了第三种方式,当我们使用类中的成员函数时,编译器并不会去类中寻找,而是去公共代码区寻找对应的函数,如果函数中不包含类中的其他变量,那么本次调用是不会进入到类中。因此C++中一个类的大小实际就是成员变量大小和,并且与结构体一样,符合成员对其规则,忘记的小伙伴可以移步到博主的其他文章《结构体内存对其规则》进行复习。

(1) 第一个成员在与结构体偏移量为0的地址处。

(2)其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。 VS中默认的对齐数为8

(3)结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

(4) 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

空类的大小并不是0而是1,用来标记这个类的对象。

8.this指针

        我们在使用类进行实例化后,对不同的变量调用同一个函数,我们并没有对函数进行显示传参将对象本身传递过去,那么函数是如何对该对象本身进行操作,而不是对其他对象进行操作呢?

        例如在定义下列日期类时,我们使用d1,d2分别调用函数print和init,得到的是对两个对象的初始化和打印,但是我们并没有将d1,d2传参,函数是如何进行区分的呢?

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 a;
};
int main()
{
    Date d1,d2;
    d1.Init(2022,7,23);
    d2.Init(2022,7,24);
    d1.Print();
    d2.Print();
    return 0;
}

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

this指针的特性:

(1)this指针的类型:类类型* const,即成员函数中,不能给this指针赋值

(2)只能在“成员函数”的内部使用

(3)this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。 所以对象中不存储this指针。

(4)this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

        因此我们需要注意:this指针可以在成员函数内部使用,但是形参和实参位置不能显示传递和接收this指针。

        this指针存在栈上,因为他是一个形参。

        this指针有时会存在寄存器中,取决于编译器,因为会提高效率。

两道小题:

//(1).下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A 
{
public:
     void Print()
     {
         cout << "Print()" << endl;
     }
private:
     int _a;
};
int main()
{
     A* p = nullptr;
     p->Print();
     return 0;
 }
// (2).下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{ 
public:
     void PrintA() 
     {
         cout<<_a<<endl;
     }
private:
     int _a;
};
int main()
{
     A* p = nullptr;
     p->PrintA();
     return 0; 
}

解答:

(1)第一个题会正常运行,虽然p是一个空指针,但是在执行p->Print();时并不会对p进行解引用,而是去公共代码区进行寻找对应的函数,函数体内部不包含类中的成员变量,因此不会进入到A中,所以不会报错,正常运行。

(2)第二个题会崩溃,在调用Print时虽然不会对空指针进行解引用,但是函数内部具有类的成员,访问成员需要对this指针进行解引用进入类中寻找,所以会对空指针进行解引用,导致程序崩溃。

总结

        C++中的结构体兼容C语言中的结构体,并将其升级成了类。class与struct实现的类其默认成员访问权限不同,class默认private,struct默认public。类中对象存储时不会将成员函数存储在类中,函数存储在公共代码区,访问函数去公共代码区寻找。C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,即this指针,this指针不要显示传递,在成员内部可以使用,访问时都是通过this指针前去指定对象的。


网站公告

今日签到

点亮在社区的每一天
去签到