看前须知:何为面向对象?
面向对象(Object-oriented)是一种计算机编程的方法论和编程范式。面向对象的核心思想是将数据(对象)和操作(方法)封装在一起,形成一个相互关联和相互作用的实体,这些实体被称为对象。每个对象都具有自己的特性(属性)和行为(方法),并且能与其他对象进行交互。
面向对象编程的主要特征包括:
1. 封装:将数据和操作封装在一个对象中,使得对象的内部状态对外部是隐藏的,只能通过对象提供的公共接口进行访问和操作。
2. 继承:通过继承机制,一个对象可以从另一个对象上继承属性和方法。继承能够提供代码的重用性和扩展性,使得新的对象可以基于已有对象进行扩展和修改。
3. 多态:多态性使得对象可以根据上下文的不同拥有多种形态和行为。通过多态,同一种方法可以在不同的对象上呈现出不同的行为。
(上面这些仅作为了解接口,不影响下面的阅读,以后会陆续更新相关内容,敬请关注)
1.类的引入
由于C++是一门面向对象的语言,所以引入了类这个概念,从实现上来说,类就是C语言中结构体的升级,使得结构体里不仅能定义成员变量,还能定义成员函数,举个例子
struct Date {
int _year;
int _month;
int _day;
void Print(){
cout<<_year<<'/'<<_month<<'/'<<_day<<endl;
}
void Init(int year,int month,int day){
_year=year;
_month=month;
_day=day;
}
};
当然,C++里更习惯用class做类如下,上面的代码之所以能运行主要是为了兼容C语言
class Date {
int _year;
int _month;
int _day;
void Print(){
cout<<_year<<'/'<<_month<<'/'<<_day<<endl;
}
void Init(int year,int month,int day){
_year=year;
_month=month;
_day=day;
}
};
好,上面给大家见识过了类的相关实现,那么类的定义是什么?
2.类的定义
class ClassName{
//类体:有成员变量和成员函数构成
};//注意,这里和结构体一样不能忘记;
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分
号不能省略
类的两种定义方式
1.将定义和声明放在类里面,注意:成员函数放在类里面定义,编辑器可能将其当作内联函数
class Date {
int _year;
int _month;
int _day;
void Print(){
cout<<_year<<'/'<<_month<<'/'<<_day<<endl;
}
void Init(int year,int month,int day){
_year=year;
_month=month;
_day=day;
}
};
2.将声明和定义分离,分别放在.h文件和.cpp文件中,注意:成员函数名前需要加类名::
//.h文件
class Date {
int _year;
int _month;
int _day;
void Print();
void Init(int year,int month,int day);
};
//.cpp文件
void Date::Print(){
cout<<_year<<'/'<<_month<<'/'<<_day<<endl;
}
void Date::Init(int year,int month,int day){
_year=year;
_month=month;
_day=day;
}
还有就是要注意一下成员变量命名规则,尽量不要和参数重名
3.类的访问限定符及封装
访问限定符
C++实现封装(不管类的内部实现只管使用类提供的接口)的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
访问限定符一共有pulic(共有),protected(保护),private(私有)三种
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来
和对象进行交互
4.类的实例化(用类类型创建对象的过程)
这里对类的使用可以参照结构体,引用成员函数和成员变量都是一样的用 ' . '
class Date {
private:
int _year;
int _month;
int _day;
public:
void Print(){
cout<<_year<<'/'<<_month<<'/'<<_day<<endl;
}
void Init(int year,int month,int day){
_year=year;
_month=month;
_day=day;
}
};
int main() {
Date x;
//x._year=10;//这样赋值是错误的,要注意变量的访问限定符,private不允许我们在类外引用成员变量
x.Init(2023,7,28);//而public修饰的函数是可以在类外调用的
x.Print();
return 0;
}
那么,问一个问题,类的定义占用空间吗???
我们都知道结构体作为自定义类型不占用空间,只有用结构体创建的对象才会开辟空间,那么同理,作为结构体升级版的类当然也不占用空间,类仅仅是对对象的一种描述(相对结构体更加的具体),是一种模型一样的东西,是不占用空间的,只有用它来定义变量时才会开空间
那么下一个问题,这个空间开多大呢???
由于它和结构体的相似性,我们和容易想到内存对齐,但是还有一点要考虑,成员函数的大小怎么算呢?或者成员函数算作是类定义的变量的大小吗?从节省内存的角度来说,成员函数就不该算作类的大小,因为每一个用类定义的变量,都可以调用成员函数,也就是说成员函数是公用的,但是每个成员变量的值却是不同的,因此,我们就可以大胆的猜测成员函数不算做类变量的大小,下面我们来验证一下猜想
int main() {
Date d;
cout << sizeof(d) << endl;
return 0;
}
Date类的成员变量按照内存对齐来说大小确实是12字节,符合要求,猜想正确,当然有兴趣的老铁可以自己创建个类来算算,当然忘记内存对齐的可以去回顾一下自定义类型,这篇里面详细讲了结构体的内存对齐的原则,这里就不多废话了
总结:类定义的变量的大小由成员变量决定,而成员函数储存在公共代码区,供大家使用
5.this指针
class Date {
private:
int _year;
int _month;
int _day;
public:
void Print(){
cout<<_year<<'/'<<_month<<'/'<<_day<<endl;
}
void Init(int year,int month,int day){
_year=year;
_month=month;
_day=day;
}
};
int main() {
Date x;
x.Init(2023,7,28);
x.Print();
return 0;
}
在上诉的代码中,其实有一个疑问,为什么成员函数能够给x对象里面的成员变量进行初始化和打印?本质其实是因为编辑器自动将x对象的地址当作参数传给了函数,而函数的参数其实也比我们看到的多了一个this指针,这些步骤都是编辑器默认做的,不需要我们进行操作,也不允许我们进行这种显式传参
class Date {
private:
int _year;
int _month;
int _day;
public:
//void Print(Date*const this)---编辑器的处理,我们不能这么写
void Print(){
cout<<_year<<'/'<<_month<<'/'<<_day<<endl;
}
//void Init(Date*const this,int year,int month,int day)---编辑器的处理,我们不能这么写
void Init(int year,int month,int day){
_year=year;//也可以这么写:this->_year=year
_month=month;//this->_month=month
_day=day;//this->_day=day
}
};
int main() {
Date x;
x.Init(2023,7,28);//x.Init(&x,2023,7,28)---编辑器的处理,我们不能这么写
x.Print();//x.Print(&x)---编辑器的处理,我们不能这么写
return 0;
}
总结:
1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值 2. 只能在“成员函数”的内部使用
3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给
this形参。所以对象中不存储this指针
4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传
递,不需要用户传递
这里有个题目考考大家
// 下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
// 下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
相信很多人会被误导,觉得空指针的解引用会导致运行崩溃,所以两题都选B,那么恭喜你蒙对一题,第二题被你蒙对了。
好,那么我们来看看这两题到底怎么思考,第一题,由于成员函数是在公共代码区,所以调用函数没必要对p进行解引用,其实只要知道指针的类型和函数名编辑器就能得到函数的地址进行调用,所以这里是不会发生解引用操作的,所以第一题正常运行,选A 第二题,和第一题的区别在于打印的是成员变量,那么要找到成员变量,我们就需要知道对象的地址,而很显然我们传的地址是nullptr ( p->Print() <=> p->Print(p), 编辑器自动传参),这里才是对空指针的解引用,而不是我们认为的p的解引用出错,选B