见下文
学习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
根据以上叙述,所以在C++中,类是这样定义的
class className
{
//成员函数
//成员变量
};//注意分号
所以简单来说,类就是属性和方法的集合,属性就是类中的数据,方法就是调用这些数据进行操作的函数
要注意,成员函数如果在类中定义,编译器会将其当作内联函数处理。
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;
};
在前面类的定义中,有一个public
,它属于访问限定符,除了public(公有)
还有protected(保护)
,private(私有)
,他们用于修饰成员,比如public修饰的成员可以在类的外面直接被访问,而是用protected
和private
修饰的在类的外面不可以被直接访问
注意以下几点
我们知道面向对象三大特性:封装,继承,多态
封装本质是一种管理手段,将属性(数据)和方法(接口)有机结合起来,再隐藏他们,只对外公开接口(函数)来和对象进行交互。不想给别人看的,使用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;
_capactiy=capacity;
}
类可以理解为一个房屋的图纸,这个图纸规定了一些基本信息,比如说这个房子朝向是什么,有几扇窗户,房子材料是什么等等。但是图纸终归就是图纸,纸上谈兵永远不会成为现实,要把这个图纸现实化,就要根据这个图纸的规定,修出相应的房子。当然,不按照图纸也能修,只不过修出来的房子可能成为四不像,或者不安全。这也就像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;
_capactiy=capacity;
}
//test.cpp
int main()
{
Stack stack;
stack._arr=4;//操作非法,成员是private的
stack.Init();//初始化这个栈
}
面向对象编程:其实很多学习编程的人,对于面向过程编程和面向对象编程这两个概念总是搞不清,具体的专业的定义在这里也不去说了,根据上面的叙述,我们可以这样去通俗的解释面向对象。
举个例子:我去洗澡,如果按照面向过程的角度考虑,那么我先进入浴室,然后打开水龙,然后洗漱,然后把身体擦干,也就是说面向过程关注的是解决问题的步骤;如果用面向对象考虑,只需记住一句话,万物皆对象,你是对象,水龙头也是对象,所以我先传递力的参数给浴室门,然后门就开了,然后我在传递消息给水龙头,水龙头得到消息,放水,最后传递消息给毛巾,毛巾利用它吸水的特性,调用吸水方法擦干身体,也就是说面向对象关注的是对象,将一个问题拆分为不同对象,依靠参数完成对象之间的交互。
**为什么要进行面向对象编程?**这也是一个很值得思考的问题。举个例子,活字印刷术发明之前使用的是雕版印刷,这种方式弊端太大,如果需要改稿,那么雕版就必须重新雕刻,而活字印刷术则解决了这样的问题,需要改动,有可能只需改动几处。面向过程正如雕版印刷一样,也正如做数学题一样,中间某个环节一旦出现需求变更,那么整个工程几乎需要大改,要耗费大量时间精力。面向对象正如活字印刷术一样,如果需求变化了,可能只是修改其中一部分,也就是一个对象,而且最关键的一点是这些对象可以服用,进行活字印刷术一样,不是说这个对象在这个工程中发挥完之后,它就没有价值了,它还可能被其他工程所用。
下面这个类实例化后所占空间大小几何
class A
{
public:
void printA
{
std::cout<<a<std::endl;
}
private:
char_a;
};
猜测:结合之前C语言学习结构体内存对齐,这个类中成员变量和成员函数,其中char
占用1个字节,成员函数实际是一个函数地址,所以在32位平台下,一个指针占用4个字节,所以总共是5个,然后根据内存,所以占用8个。究竟是不是这样,可以执行
结果显示只有一个字节,不是预想的八个字节,再次观察这个类,也能想到类在计算大小时不管成员函数,只管成员变量和内存对齐。
那么成员函数究竟在哪?
根据以上叙述,不禁会提出一个疑问:为什么成员函数不在类内?
其实:一个类可以实例化很多对象,就拿栈来说,这些栈它的存储的数据可能会不一样,但是它们的方法,如进栈出栈都是一样的。所以说实例化时,如果为他们再开辟空间,就会造成浪费
所以解决方法是:只保存成员变量,成员函数存放在公共的代码段(常量区)
定义一个日期类
class Date
{
public:
void SetDate(int year,int month,int day)
{
_year=year;
_month=month;
_day=day;
}
private:
int _year;
int _month;
ibt _day;
}
int main()
{
Date d1;
Date d2;
d1.SetDate(2021,2,20);
d2.SetDate(2022,8,19);
}
上述代码中看起来没有问题。但是容易忽略一个细节,类中成员函数是公用的,它并不存在于类中,那么d1.SetDate(2021,2,20)
,编译器怎么会知道要给d1这个类去设置,而不会给d2去设置。这里很多有疑问,不是函数前面已经写了一个d1
吗,这里大家一定要跳出这个误区,函数只是放在了公用的代码段,如果函数是放在类里的,那无可厚非。但是当放在公用的代码段后,不要说用实例化的类去调用它,就是我先如果知道函数的地址的话,用汇编都可以去调用,那么当无数个类去调用这个函数时,编译器怎么会分的这么清楚。
这个问题在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;
ibt _day;
}
int main()
{
Date d1;
Date d2;
d1.SetDate(&d1,2021,2,20);
d2.SetDate(&d2,2022,8,19);所以实参里,其实就是类的地址
}
所以这样就是为什么编译器可以分清楚的原因
这是一个极高频率的面试题
谈及C/C++的内存开辟,就不得不上这一张图了
this指针是作为一个函数隐藏的形参,而栈是存放局部变量,形参的。所以,this指针存在于栈中
还有一个经典的题,如下请问这段程序是否会崩溃
class A
{
public:
void printA()
{
std::cout << _a << std::endl;
}
void show()
{
std::cout << "Show()" << std::endl;
}
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()
来说,他们不存在与类内,而存在于公共的代码段内,所以使用指针p
进行这样的操作,并不是在取成员,所以问题不在于空指针。
问题在于:调用printA()
时,将这个p指针作为this
指针传了过去,那么在函数中就会进行std::cout<
this
指针
C++:类和面向对象很懵?其实很简单