在我们学习过的C语言中,C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题;而C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
C语言结构体中只能定义成员变量,在C++中,结构体内不仅可以定义成员变量,也可以定义成员函数。比如:之前在数据结构中,用C语言方式实现的栈,结构体中只能定义成员变量;现在以C++方式实现,会发现 struct 中也可以定义成员函数,例如以下代码:
struct Stack
{
// 初始化
void STInit()
{}
// 入栈
void STPush(int x)
{}
int* a;
int top;
int capacity;
};
注意,以上代码中函数的实现并没有实现,实际上我们需要在内部实现;
但是实际上在 C++ 中更喜欢用 class 来代替 struct,class为定义类的关键字,类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
类的两种定义方式:
.h 文件中放声明:
#pragma once
#include
using namespace std;
class Stack
{
public:
// 初始化
void STInit();
// 入栈
void STPush(int x);
private:
int* a;
int top;
int capacity;
};
.cpp 文件放定义:
#include "Test.h"
void Stack::STInit()
{}
void Stack::STPush(int x)
{}
如上面定义的函数前,需要加 Stack::
到指定域去找。
在声明成员变量上,我们要注意命名规则,例如以下有一个日期类,我们要将它初始化:
// 日期类
class Date
{
public:
// 初始化
void Init(int year, int month, int day)
{
year = year;
month = month;
day = day;
}
private:
int year;
int month;
int day;
};
在初始化函数中,能区分出来哪个 year 是哪个吗?能被初始化吗?答案都是未知的,所以我们需要注意命名规则,例如在声明前加个 _ ,如以下代码:
// 日期类
class Date
{
public:
// 初始化
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
这样看起来就舒服很多了。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
类的访问限定符包括:public(公有),private(私有),protected(保护)。其中,访问限定符有以下特点:
例如上面的日期类,成员变量一般设为私有,因为这个类是我们自己实现的,只有实现的人才会用到成员变量,所以设为私有;而成员函数设为共有,因为成员函数是给别人用的,共有的在类外是可以访问的;如日期类:
// 日期类
class Date
{
public:
// 初始化
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
用类类型创建对象的过程,称为类的实例化。
在类的成员变量中,只是对它们进行声明,并没有开空间,所以即使将成员变量设为共有,直接使用也会报错的,例如以以上日期类为例:
int main()
{
Date._year = 2023; // 编译失败:error C2059: 语法错误:“.”
return 0;
}
此时还没有对类进行实例化,所以正确的用法应该是:
int main()
{
// 定义开空间,实例化
Date d;
d.Init(2023, 7, 20);
return 0;
}
假设就以上面的日期类为例,我们计算一下这个类究竟有多大:
int main()
{
// 定义开空间,实例化
Date d;
d.Init(2023, 7, 20);
cout << sizeof(d) << endl;
return 0;
}
代码的结果如下:
这个时候我们就要想起以前学过的结构体内存对齐规则,结构体内存对齐 去计算这个类的大小,很明显,成员变量中三个 int 类型就已经是 12 个字节了,那么我们猜想成员函数没有算进类的大小中。注意,这里无论是 sizeof(Date)
还是 sizeof(d)
都是一样的结果。
结果确实是如此的,对于一个类来说,每个类都有自己对应的公共代码区,这个类的所有成员函数都存放在公共代码区,而不是存放在实例化出来的对象中,如果存放在每个对象中,会导致对象变得很大,并且会有很多重复的函数;所以,每个实例化出来的对象,都是在公共代码区中调用相应的函数,这样就相应的节省了类和对象的空间。
那么又有另外一个问题了,如果类里面只有一个成员函数或者空类是不是就没有大小了呢?这两个其实是一个问题,因为成员函数并不存在类中,所以相当于空类,例如以下代码:
// 类中仅有成员函数
class A2
{
public:
void f2()
{}
};
// 类中什么都没有---空类
class A3
{};
执行的结果如下:
所以结论是,即使是空类,编译器也会给空类一个字节来唯一标识这个类的对象,不存储数据,只是占位,表示对象存在过。
我们先简单定义一个日期类 Date :
// 日期类
class Date
{
public:
// 初始化
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
//声明
int _year;
int _month;
int _day;
};
再实例化出来两个对象 d1 和 d2,并对它们进行初始化:
int main()
{
Date d1;
d1.Init(2023, 7, 20);
Date d2;
d2.Init(2023, 6, 20);
return 0;
}
那么问题来了,函数体中没有关于不同对象的区分,当我们对 d1 和 d2 对象进行初始化时,d1 调用 Init 函数时,该函数是如何知道应该设置 d1 对象,而不是设置 d2 对象呢?
C++中通过引入 this 指针解决该问题,即:C++ 编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
例如上面两段代码,等价于以下代码:
这个过程不需要我们显示地去传,编译器会帮我们完成;我们再看成员函数:
我们可以看到,实际上成员函数是通过 this 指针分别对不同的对象进行相应的操作的。
那么我们一起看看以下代码的执行结果是什么:
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
大家第一时间反应一定是 nullptr 解引用了,代码会崩溃,这里 p->Print();
等价于 (*p)->Print();
,然而实际上并不是这样的,我们看结果:
代码可以正常运行,在这里,我们首先需要知道一个点,这个成员函数是否存在对象中,根据我们上面类对象模型所学,成员函数并不是存在对象中,它是存在公共代码区中的,而编译器在这里会进行处理,它不会解引用,而是会在公共代码区中查找这个函数的地址,所以这里并不会报错。
而下面这段代码,它仅仅跟上面代码的不同之处是 Print() 成员函数,这段代码它打印的是成员变量 _a ,这个时候 cout << _a << endl;
其实就是 cout << this->_a << endl;
,而 this 指向的对象是空指针,所以这里代码会崩溃。
class A
{
public:
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
到此,类和对象的上篇就结束啦,到类和对象中篇我会和大家分享类的默认成员函数相关的知识点噢~ 都看到这里啦,点个赞再走呗 ~ 感谢支持 ~