有不少朋友会遇到这种情况,在学完C++入门的一些知识后,就开始接触类和面向对象的概念,大家去看书、文章的时候都会先抛出一大堆的概念,导致我们听得很懵。那么这篇文章将言简意赅,理解类和面向对象其实很简单!
1. 面向对象与面向过程
其实很多学习编程的人,对于面向过程编程和面向对象编程这两个概念总是搞不清,具体的专业的定义在这里也不去说了,根据上面的叙述,我们可以这样去通俗的解释面向对象。
举个例子:我去洗澡,如果按照面向过程的角度考虑,那么我先进入浴室,然后打开水龙,然后洗漱。然后把身体擦干,也就是说面向过程关注的是解决问题的步骤;如果用面向对象考虑,只需记住一句话,万物皆对象,你是对象,水龙头也是对象,所以我先传递力的参数给浴室门,然后门就开了,然后我在传递消息给水龙头,水龙头得到消息,放水,最后传递消息给毛巾,毛巾利用它吸水的特性,调用吸水方法擦干身体,也就是说面向对象关注的是对象,将一个问题拆分为不同对象,依靠参数完成对象之间的交互。
为什么要进行面向对象编程?这也是一个很值得思考的问题。举个例子,活字印刷术发明之前使用的是雕版印刷,这种方式弊端太大,如果需要改稿,那么雕版就必须重新雕刻,而活字印刷术则解决了这样的问题。需要改动,有可能只需改动几处。面向过程正如雕版印刷一样,也正如做数学题一样,中间某个环节—旦出现需求变更,那么整个工程几乎需要大改,要耗费大量时间精力。面向对像正如活字印刷术一样,如果需求变化了。可能只是修改其中一部分,也就是一个对象,而且最关键的一点是这些对象可以复用,进行活字印刷术一样。不是说这个对象在这个工程中发挥完之后,它就没有价值了,它还可能被其它工程所用。
2. 类的引入
学习C语言时,我们知道,结构体的作用是把一些具有不同类型的变量组合一起。
struct Student
{char _name[20];char _gender[3];int _age;
};
而在C++中,其结构体内不止可以定义变量,而且还可以定义函数。
struct Student
{char _name[20];char _gender[3];int _age;void SetStudentInfo(const char *name,const char *gender,int age){strcpy(_name,name);strcpy(_gender,gender);_age=age;}
};int main()
{Student s;s.SetStudentInfo("Bob","男",18);return 0;
}
这样定义后,结构体就可以被称为类,只不过在C++中,我们更喜欢用class代替struct 。
其实我们在定义类的时候,和定义结构体差不多,只不过要记住名称换了,而且类里面还可以定义函数。
3. 类的定义
3.1 C++类的定义
根据以上叙述,所以在C++中,是这样定义的
class className
{//成员函数//成员变量};//注意分号
所以简单来讲,类就是属性和方法的集合,属性就是类中的数据,方法就是调用这些数据进行操作的函数。
3.2 类的两种定义方式
3.2.1 声明和定义全部都放在类体当中
要注意,成员函数如果在类中定义,编译器会将其当做内联函数处理。
class Person //人就是一个类
{public://成员函数void showInfo()//显示这个人的信息{std::cout<<_name<<std::endl;std::cout<<_sex<<std::endl;std::cout<<_age<<std::endl;}public://成员变量char _name;char _sex;int _age;
};
3.2.2 定义和声明分开放
这种方式最常用。
// Person.hclass Person
{public:void showInfo();public:char* _name;char* _sex;int _age;
};
//Person.cpp#include"Person.h"void Person ::ShowInfo()
{std::cout<<_name<<std::endl;std::cout<<_sex<<std::endl;std::cout<<_age<<std::endl;
}
4. 类的访问限定符及封装
4.1 访问限定符
在前面类的定义中,有一个 public ,它属于访问限定符,除了 public(公开)还有 private(私密),protected(保护),它们用于修饰成员,比如 public 修饰的成员可以在类的外面直接被访问,而是用 protected 和 private 修饰的在类的外面不可以直接被访问。
注意以下几点:
- 访问限定符的作用域是从该访问限定符出现的位置开始直到下一个访问限定符为止
- class的默认访问权限(就是没有给出)为 private,struct 则为public
- 访问限定符限制的是外面,就像锁子防的是外人
4.2 封装
我们知道面向对象三大特性:封装,继承,多态
封装本质是一种管理手段。将属性(数据)和方法(接口)有机结合起来,再隐藏他们,只对外公开接口(函数)来和对象进行交互。不想给别人看的,使用 protected/private 进行修饰,而开放一些公有函数对成员进行合理访问,合理访问可以理解为阅读,不合理访问可以理解为改写(不准确)。
这里也就能说明,为什么C语言不支持大型项目的编写,因为它不支持面向对象,管理能力较弱,不是函数就是数据,就拿 struct 来说,其默认就是 public 的,安全性也堪忧,总的来说,用C语言编写项目,会感觉很乱,模块与模块独立性很差。
之前使用C语言实现过很多数据结构,比如说栈,我们都是新建一个工程然后分别建立它的声明。实现等文件,也就是说数据和方法是分离的,是自由的,如果换到C++中,按照面向对象考虑,一个栈就可以作为一个对象,这个对象有它的数据(动态增长的数组,长度,容量),还有它的方法(栈的初试线,压栈。出栈等等)。像这些数据,应该就是私有的,因为要防止外部操作改变数据,一旦数据出错,那么方法也就会受到相应的波及,然后这些操作按照实际需求让外部使用。
如:
//stack.h
class Stack //类
{
public:void Init(int capacity=4); //缺省参数,声明private:int *_arr;int _length;int _capacity;};//stack.cpp
void Stack::Init(int capacity)//该方法的实现
{_arr=(int *)malloc(sizeof(int )*capacity);_length=0;_capacity=capacity;
};
5. 类的实例化
5.1 类的实例化
类可以理解为一个房屋的图纸,这个图纸规定了一些基本信息,比如说这个房子朝向是什么。有几扇庶户。房子材料是什么等等。但是图纸终归就是图纸,纸上谈兵永远不会成为现实,要把这个图纸现实化,就要根据这个图纸的规定,修出相应的房子。当然,不按照图纸也能修,只不过修出来的房子可能成为四不像,或者不安全。这也就像C语言,没有图纸,接口随意写,数据任你改,是很自由,但是稍有不慎,房子就可能用不久了。
所以说根据图纸建造房子的过程,称为类的实例化,就像前面的栈一样,实例化出一个真正的栈,什么叫做真正的栈--这个栈它占用实际空间。一个图纸不可能只能造出一个房子,而是可以造出千千万万个房子,这个诸多房子本原都是一个,要想让他们之间有所区别,这取决于使用者,比如你和我都按照这个图纸了建造了相同的房子,但是我们对房子的装饰不同,所以看起来也就像两个不同的房子。回到栈,我们可以实例化出许许多多的栈,但是有些栈用于进行非递归操作,有些栈用于排序操作,他们就不是一样的了。
比如前面的栈可以实例化为:
//stack.h
class Stack //类
{
public:void Init(int capacity=4); //缺省参数,声明private:int *_arr;int _length;int _capacity;};//stack.cpp
void Stack::Init(int capacity)//该方法的实现
{_arr=(int *)malloc(sizeof(int )*capacity);_length=0;_capacity=capacity;
};//test.cpp
int main()
{Stack stack;stack._arr=4;//操作非法,成员是 privatae 的stack.Init();//初始化这个栈
}
6. 类对象模型
6.1 如何计算类对象的大小
下面这个类实例化后所占的空间大小:
class A
{
public:void print A{std::cout<<a<<std::endl;}private:char _a;
};
猜测:结合之前学习C语言学习结构体内存对齐,这个类中成员变量和成员函数,其中 char 占用一个字节,成员函数实际是一个函数地址,所以在32为平台下,一个指针占用4个字节,所以总共是5个,然后根据内存,然后占用8个,究竟是不是这样,可以执行。
结构显示只有一个字节,不是预想的8个字节,再次观察这个类,也能想到类在计算大小时不管成员函数只管成员变量和内存对齐。
那么成员函数究竟在哪?
6.2 类对象的储存方式
根据以上叙述,不禁会提出一个问题:为什么成员函数不在类内?
其实:一个类可以实例化很多对象,就拿栈来说,这些栈它储存的数据可能不一样,但他们的方法进栈出栈都是一样的,所以在实例化时,如果为他们再开辟空间,就会造成浪费。
所以解决方法是:只保存成员变量,成员函数存放到公共的代码段内(常量区)
- 注意:一般情况下,一个类的大小 ,实际就是该类中成员变量和成员对齐,但是要注意空类,空类大小不是0,编译器提供了一个字节表示它存在。
7. this指针
7.1 一个问题
定义一个日期类
class Date
{public:void SetDate(int year,int month,int day){_year=year;_month=month;_day=day;}private:int _year;int _month;int _day;
}int main()
{Date d1;Date d2;d1.SetDate(2021,2,20);d2.SetDate(2024,7,10);
}
上述代码中看起来没有问题。但是容易忽略一个细节,类中成员函数是公用的,它并不存在于类中,那么 d1.SetDate(2021,2,20),编译器怎么会知道要给d1这个类去设置,而不会给d2去设置。这里很多有疑问,不是函数前面已经写了一个 d1 吗,这里大家一定要跳出这个误区,函数只是放在了公用的代码段,如果函数是放在类里的,那无可厚非。但是当放在公用的代码段后,不要说用实例化的类去调用它,就是我先如果知道函数的地址的话,用汇编都可以去调用,那么当无数个类去调用这个函数时,编译器怎么会分的这么清楚。
7.2 this指针
这个问题在C++中,是通过引入this指针来解决的。官方定义:C++编译器给每个非静态的成员函数增加了一个隐藏的指针参数。让该指针指向当前函数(运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针操作。
所以如果上述代码,写完整应该是下面这样的
class Date
{public:void SetDate(Date* this,int year,int month,int day){//生成一个类指针this->_year=year;this->_month=month;this->_day=day;//通过指针的指向来辨别对象}private:int _year;int _month;int _day;
}int main()
{Date d1;Date d2;d1.SetDate(&d1,2021,2,20);d2.SetDate(&d2,2024,7,10);//所以参数里实际就是函数的指针
}
7.3 this指针存在那里?
这是一个极高频率的面试题
谈及C、C++的内存开辟,就不得不上这样一张图了
this指针是作为一个函数隐藏的形参,而栈是存放局部变量,形参的。所以,this指针存在于栈中。还有一个经典的题,如下这段程序是否会崩溃
class A
{public:void printA(){ std::cout<<_a<<std::endl;}void show(){std::cout<<"show"<<std::show;}private:int _a;};int main()
{A*p=NULL;p->printA();p->show();
}
这段程序的结果是,如果只执行 p->printA() 就会崩溃,如果只执行 p->show() 就不会崩溃。
如下:
仔细分析,未免让人觉着有点匪夷所思, A* p=NULL ,表明p是一个空的类指针,那么既然是空的就不能对空指针进行操作,但是观察上述两种情况, p→printA()报错了, p→>show()并没有报错。其实原因就是在于这this指针,首先对于 show()和 printA()来说,他们不存在与类内,而存在于公共的代码段内,所以使用指针 ρ 进行这样的操作,并不是在取成员,所以问题不在于空指针。
问题在于:调用 printA() 时,将这个 p 指针作为 this 指针传了过去,那么在函数中就会进行std::cout<<this→_a<<std::endl;的操作,自然而然会出现问题,而另一个函数没有崩溃是因为它就没有使用到这个 this 指针。
这篇文章对类和面向对象的相关知识点进行了梳理。欢迎大家评论交流!