在本章节的博客中,引入在C++中知名的类与对象的概念,同时将其和C语言中的结构体做对比,介绍类域对象的基本定义、
一.类的定义
类的定义关键字为class,使用方法和C语言中的结构体struct类似。
class 类名 {类的主体};如下面这段示例代码所示:
cpp
class Date
{
public:
//成员函数
//成员函数默认为inline内联函数
void DateInit(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
//成员变量
//为了区分成员变量,一般加上一个特殊标识
int _year;
int _month;
int _day;
};
在这里定义了一个类名为Date的类,类中包含了一个初始化函数DateInit和三个整形成员变量_year _month _day。
从示例中可以明显看出C++中的类与C语言中结构体的不同之处:
1.类中可以定义函数,在类中所定义的函数称为成员函数或类的属性。类中定义的变量称为成员变量。
而类就是成员变量和对于该成员变量操作的成员函数整合到一起,形成一个集成化的操作接口,更为简洁的提供给用户进行IO操作。
2.类中存在访问限定符,就是上例代码中的public和private,以及没有出现过的protected。
在这里简述其区别,public限定符,表示该对象可以被外部(该类之外的地方,例如主函数中)访问;private和protected则表示该对象不可被外部访问。
如果在定义类时,不添加任何访问限定符,默认为private。
cpp
include<iostream>
using namespace std;
class Date
{
public:
//成员函数
//成员函数默认为inline内联函数
void DateInit(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
//成员变量
//为了区分成员变量,一般加上一个特殊标识
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.DateInit(2026, 2, 20);
d1.print();
//d1._day //成员变量为private,不可访问
return 0;
}
例如在这段代码中,在Date类的基础之上,在其内部多增加了一个打印函数。
主程序中先创建了一个Date类型的实例d1,然后调用类内成员函数DateInit对其进行初始化,之后调用成员函数Print对三个成员变量进行打印。可观察到输出结果。
但这里不能在main函数中,利用cout对类内成员_year _month _day进行打印,因为这三个成员变量是被private所限制。
错误类型为:error C2248: "Date::_day": 无法访问 private 成员(在"Date"类中声明)
仔细观察上述的错误类型,可以发现,出现了针对某个特定域的双引号操作符。这里继续引出概念,不同的类属于不同的域,其类似于全局域,主要目的是为了区分定义不同类型之间成员函数重名的情况。
如果成员函数的声明与定义分离,则其定义应该如下面的示例代码所写:
cpp
class Date
{
public:
void DateInit(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print();
int _year;
int _month;
int _day;
};
void Date::Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
在类内对函数进行声明,然后类外进行函数定义的时候,要对其指定是哪个类域中的函数对象。
类内的成员函数默认为inline内联函数类型。
3.在C++中,同样支持C语言中的struct结构体操作,并且在C语言结构体的基础之上,和class类同样支持内部定义函数和访问限定符。
cpp
struct Date1
{
public:
//成员函数
void DateInit(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
//成员变量
int _year;
int _month;
int _day;
};
如上述代码所示,其创建方法和类几乎一样,不同点是,struct定义的结构体如果不写访问限制符,则默认为public。
C++中定义的结构体不再需要typedef重命名。
二.实例化
class类只是对抽象变量的一个集合约束,其并没有在内存中开辟或占据实际空间,只有真正用这个类型创建了一块空间,才可以实际利用我们所创建的类型。
而这个过程,称为类的实例化 或 类实例化出对象。
一个类可以实例化出多个对象。
cpp
class Date
{
public:
void Init(int year , int month ,int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
class A
{
};
class B
{
void print()
{
cout << endl;
}
};
int main()
{
Date d1;
d1.Init(2026, 2, 20);
d1.Print();
//该类的大小为12字节,为三个整型变量的空间,从此处可以看出,类中只存有变量,而不存储类内函数
//为了节省空间,多次创建该类和调用相同函数时节省了这个相同函数的地址空间。
//符合C语言结构体内存对齐的规则
//内存对齐本质以空间换时间,为了可以一次读取到一类数据
cout << sizeof(Date) << endl;
//类A和B都没有任何变量,其理论大小应该为0,但实际输出却为1,这个1是占位标识符,为了表示这个类型的存在
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
return 0;
}
在这段代码中,创建了Date 、A和B这三种类,利用sizeof输出这三种类可以发现,Date的大小为12字节,恰好为三个整形变量遵循C语言中结构体内存对齐规则所计算出的大小。
所以,在这里引出C++中类的大小只计算类内成员变量的大小,而不计算类内成员函数的大小。其计算规则符合C语言中结构体内存对齐的规则。
那么为什么不在类中包含成员函数的大小呢?本质是为了节省空间,如果创建了100个利用该类实例化的对象,那么调用该函数的过程中就需要多存储100个完全一致的call指令(函数的地址为第一条指令的地址),函数地址完全相同,所以就不需要在类内对其进行存储。
(上述代码中VS默认对齐数为8)。
根据C语言中结构体的内存对齐规则,A和B的类内,没有任何成员变量,其理论大小应该是0,但是实际输出A和B的大小,发现为1。这个1,是类A B的占位标识符,为了证明该类存在。
三. this 指针
cpp
class Date
{
public:
//在这里
//void Init(Date* const this ,int year, int month, int day)//不能进行参数传递
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//void Print(Date* this)//编译器处理后会形成这样,但在写函数时不应该进行调用
void Print()
{
cout << this << endl;//但是在类型内部可以进行调用
cout << _year << "/" << _month << "/" << _day << endl;
//cout << this->_year << "/" << this->_month << "/" <<this-> _day << endl;//本质是这样进行输出
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(2026, 2, 20);
d2.Init(2025, 2, 20);
d1.Print();
d2.Print();
//编译器对其进行了一定程度的处理,本质实际传参是:
//d1.Print(&d1);
//d2.Print(&d2);
//但要注意,其不能在调用时进行传递
//this指针本质是函数传递的形参,存储在栈或寄存器
return 0;
}
利用类实例化了两个对象d1 d2,分别对其进行初始化和打印数值的操作。但是并没有传递一些标识符类型的地址或引用,那么类是如何找到或区分这两个不同的对象呢?
这里引出this指针的概念,编译器会在编译过程中,默认给类内函数传递一个Date* const this类型的指针。
所以Init函数的参数在语法层面只需要写三个,而在编译器处理后本质就变成了:void Init(Date* const this ,int year, int month, int day)。
Print函数的参数就变成了:void Print(Date* this),实际调用的时候就变成了:
cout << this->_year << "/" << this->_month << "/" <<this-> _day << endl.
所以根据传递的this指针的不同来具体区分是哪一个实例化的对象。
但要注意的是,this指针不可以显示的进行参数的传递和函数形参的占位,但其可以在类内直接使用。
下面通过两个类的代码来加深对this指针的理解:
cpp
class A
{
public:
void Print()
{
cout << "A" << endl;
}
private:
int _a;
};
class B
{
public:
void Print()
{
cout << "B" << endl;
cout << _b << endl;
}
private:
int _b;
};
int main()
{
A* p = nullptr;
//此处,p为空指针,但是并不会报空指针错误。
//本质原因是没有对p解引用,因为类的内置函数类型并不存储在类中,所以本质并不是对p解引用找到的类内函数
//所以可以正常运行
p->Print();
//底下这个是相同的道理。
(*p).Print();
B* pb = nullptr;
pb->Print();//这里就会产生空指针解引用的问题,因为在函数内部,进行了pb指针的this传参
//打印_b本质是this->_b,空指针解引用为问题
return 0;
}
首先创建了两个类型A和B,唯一不同点在于A中的print函数仅输出一个字符,而B的print函数还输出了类中定义的成员变量_b。
首先直接看主函数,创建了A类型的指针变量p赋值为空指针,然后利用p调用Print函数,这里第一次见到肯定会认为其是空指针调用从而使得程序报错。但实际运行可以发现,print函数可以正常打印出A。本质原因是p虽然是空指针,但并没有对其解引用,因为函数指针并没有存储在类内,所以就算p是空指针,也可以正常找到该函数的地址,从而打印出A。同理(*p).Print也是相同的道理。
但是如果B这样,虽然能正常找到Print函数,但是其内部需要具体输出_b,而输出_b的过程本质是this->_b ,this是空指针,这里就会形成空引用。从而导致程序崩溃。
最后针对this指针再说一个点,this指针本质是函数的形参,其存储在内存的栈区或者寄存器中。
最后总结一下C++的类:其具有面向对象的三大特征:封装、继承、多态。
C++可以将变量和对变量进行操作的函数都封装到类中,同时可以通过访问限定符限制某些变量的访问,从而规范化输入输出格式,避免随意访问修改。以及C++有着一些比较方便的语法,例如函数参数缺省以及this指针隐式传递。