不知你在学习C语言和C++的适合,曾否听过老师这么讲述这两个语言之间的区别:
- C语言是面向过程的
- C++是面向对象的
我一直不太理解这二者之间的区别,在查阅了一些博客后,发现了一个比较好的解释👉【传送门】
用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭。所谓盖浇饭,北京叫盖饭,东北叫烩饭,广东叫碟头饭,就是在一碗白米饭上面浇上一份盖菜,你喜欢什么菜,你就浇上什么菜。
本篇博客,就让我们从类和对象开始,渐渐了解什么是“面向对象编程”
感谢你关注慕雪,欢迎来我的寒舍坐坐❄慕雪的寒舍
文章目录
1.类的引入
1.1结构体
在C语言中,我们可以定义自定义类型:结构体。在C++中对结构体的语法进行了扩充,结构体内部不仅能定义变量,还能定义函数
struct Student{
void Print()
{
cout<<_name<<" "<<_sex<<" "<<_age<<" "<<_phone<<endl;
}
char _name[20];
char _sex[8];
int _age;
char _phone[20];
};
1.2class
为了和C语言里面的结构体作为区分,我们不再用struct
来指代这种包含函数的自定义类型,而是使用class
作为它的名字,称之为类
class className{
//类体:由成员函数和成员变量组成
};
//一定要注意后面的分号
class为定义类的关键字,ClassName
为类的名字,{ }
中为类的主体,注意类定义结束时后面分号(这一点和结构体相同)
类中的元素称为类的成员:
- 类中的数据称为类的属性或者成员变量
- 类中的函数称为类的方法或者成员函数
2.类成员的定义
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 ::
作用域解析符指明成员属于哪个类域。
2.1函数声明和定义分离
和普通的函数一样,类同样支持声明和定义分离。下面的代码中,我并没有分离函数的声明和定义
- 成员函数在类里面定义,编译器会默认处理为内联函数
class Student{
void Print()
{
cout<<_name<<" "<<_sex<<" "<<_age<<" "<<_phone<<endl;
}
char _name[20];
char _sex[8];
int _age;
char _phone[20];
};
如果需要在.h
中写入类的定义,类外面的.cpp
中编写类里面的函数,就需要用到类似于命名空间的使用方法::
class Student{
void Print();//声明函数
char _name[20];
char _sex[8];
int _age;
char _phone[20];
};
在另外的源文件中定义函数
#include "Student.h"
void Student::Print()
{
cout<<_name<<" "<<_sex<<" "<<_age<<" "<<_phone<<endl;
}
当我们编写大型项目的时候,一般都会采用声明和定义分离的方式来编写源码,这样可以更方便他人快速查阅我们的头文件,理解代码的“大纲”
这样看起来好像和以前的方式没啥区别。但当我们引入访问权限的概念后,一切都变啦!
2.2访问限定符
class的默认访问权限是私有;struct默认为公有
你不知道什么是访问权限?那就继续往下看👇
我们可以用public
和private
这两个访问限定符来表明某一个具体类型的共有还是私有。它们的最大区别就是在这个类的外部能不能访问
- public成员可以在类外面直接使用
- protected和private修饰的成员只能在类里面访问
- 访问权限的作用域是从该访问限定符出现,到下一个访问限定符出现为止
- 访问限定符是在编译过程中处理的,并不影响数据在内存上的存放
在前期学习的时候,我们可以认为protected
和private
是相同作用的
class Student{
public:
//函数在类里面定义,编译器默认为内联函数
void Print()
{
cout<<_name<<" "<<_sex<<" "<<_age<<" "<<_phone<<endl;
}
void Init(const char * name,const char*sex, int age,const char* phone)
{
strcpy(_name,name);
strcpy(_sex,sex);
_age=age;
strcpy(_phone,phone);
}
//函数在类里面声明
void Delet();
private:
//这个是对变量的声明
//变量的声明:没有开辟空间
//变量的定义:开辟了空间来存放内容
char _name[20];
char _sex[8];
int _age;
char _phone[20];
//在创建对象的时候定义
};
- 数据和方法分装到一个类里面
- 想给你自用访问的(如函数接口)设置为共有
- 不想给你访问的(如通讯录每个用户的信息类型)设置为私有
这样可以做到,当函数定义修改之后,只要不修改函数声明,就不需要修改main函数中函数的调用!
这叫做:高内聚,低耦合
2.3封装
为什么会有权限的出现呢?我们可以细想一下下面这个场景:
假设我们编写了一个数组栈的代码,里面有一个top用来标明栈顶的元素位置。一般可以把top定义为0或者1,但是这两种方法的函数使用是不同的!如果有那个铁憨憨跑过来,非要把那个top的定义给改掉,那整个代码就废掉不能用辣!
设立私有和公有的初衷,就是为了避免这种情况。开放一些共有函数供类外面访问,这样对整个代码的访问会变得易于管理。当我们自己修改类里面的代码时,只需要做好处理,就不会影响类外面的函数调用(否则就是直接推翻重写了)
这便是我们常常提到的封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
3.类的实例化
用类类型创建对象的过程,称为类的实例化
- 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
- 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
类就好比一个毛坯房,现在毛坯房建好了,要想它变得精致,我们还需要在main中调用这个类,不然毛坯房就要变成烂尾楼了
Student p1;
p1.Init("牛爷爷","男",58,"13251341680");
p1.Print();
4.计算类的大小
一般有4种类:包含函数和变量的、只包含函数的、只包含成员变量的、空类
让我们来康康如何计算这些类的大小,解析见注释哦!
其实只需要记住,空类和只有函数的类会有1个字节的空间。计算类的空间的时候不会计算函数大小,成员变量的大小计算遵循结构体内存对齐的计算方法就行了!
#include<iostream>
using namespace std;
class A1{
public:
void func1()
{
int ret=3;
return ;
}
};
class A2{
};
class A3{
public:
char _a;
};
5.this指针
5.1特点
当你用同样的图纸建了很多个屋子后,有没有想过应该如何区分它们呢?
C++在设计这部分的时候,添加了一个this指针来解决这个问题:
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参 数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
- this指针的类型:
类名* const
- 只能在“成员函数”的内部使用
- this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this 形参。所以对象中不存储this指针。
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
5.2显式使用this
就用下面这个函数举例
void Print()
{
cout<<_name<<endl;
cout<<_sex <<endl;
}
实际上,在调用它的时候,编译器会做如下处理
void Print(Student*const this)
{
cout<<this->_name<<endl;
cout<<this->_sex <<endl;
}
因为只有这样,才能完整的区分两个不同的类
进一步看看下面这个代码,可以帮助你理解this指针
bool operator==(const Date& d){
return _year == d._year
&& _month== d._month
&& _day == d._day;
}
这是一个日期的比较函数,是操作符重载(后面会讲到)
你可以看到,这个函数我们传入了一个Date类型的引用,这是区别于this的另外一个类的对象
如果没有this,那就很难区分两个变量的_year
,于是编译器会把它优化成下面这样,就不会存在无法区分的问题了
bool operator==(Date*const this,const Date& d){
return this->_year == d._year
&& this->_month== d._month
&& this->_day == d._day;
}
5.3空指针问题
int main()
{
int x=10;
//在程序中,访问NULL不会报错,但是解引用Null会报错
int*a=NULL;
int*b=&x;
//a=x;//err
*b=20;
return 0;
}
#include<iostream>
using namespace std;
class ta{
public:
void Print()
{
cout<<"print ta"<<endl;
//cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
ta* p=nullptr;
p->Print();
//可以去访问空指针的函数
//因为函数只是去调用了类里面的Print函数
//同时传了一个p的this指针(空指针传参是不会报错的)
//但是如果你去访问p里面_a变量,就会报运行错误
return 0;
}
6.类中成员函数名的处理
除了this指针之外,编译器在链接函数名的时候,也做了相应的处理。在Linux里面查看下面这串代码的汇编代码
#include<iostream>
class Test{
public:
void func1(const int* a1,int* k,const int*a2,char arr)
{
*a1+*a2;
func2(a1,k,a2,arr);
}
private:
void func2(const int* a1,int* k,const int*a2,char arr)
{
*a1+*a2;
}
};
int main()
{
Test t;
int arr1[10];
int arr2[20];
int a=10;
t.func1(arr1,&a,arr2,'a');
return 0;
}
可以找到这两个类的成员函数的地址和函数名
方法参考我的这篇博客👉【末影门】
00000000004006da <_ZN4Test5func1EPKiPiS1_c>:
000000000040071a <_ZN4Test5func2EPKiPiS1_c>:
可以发现,这两个函数的地址不同,但函数名中并没有包含它的公有、私有信息。这也能证明2.2
中写道的“访问限定符是在编译过程中处理的,并不影响数据在内存上的存放”
下面是一个普通函数(不在类里面)的函数名
观察类里面的函数名,可以看到比起普通函数,它还包含了类名,来标明自己是被封装在某个类里面的。同时前缀也从_Z
变为了_ZH
这里
S1_
的含义是我多次传相同类型参数,查看汇编代码测试出来的
当我把相同传参的函数放在类外面,重新查看汇编代码
000000000040064d <_Z5func3PKiPiS0_c>:
可以看到它发生了一些变化,比如前缀变为了_Z
,函数名后面的E
不见了,S1_
变成了S0_
虽然我现在还不知道前缀_Z
和_ZH
、函数名后面那个E
分别代表什么含义,但是我们可以看的出,这是编译器在编译链接过程中为了区分类中函数和类外函数做的优化
结语
本篇博客是类和对象的第一篇笔记,不知能否帮到你呢