类的定义
cpp
class className{
//成员字段
//成员函数
};
class
定义类的关键字,className是自己决定的类名,{ }
为类的主体,花括号里是类的内容。类的内容大致分为类的成员属性(变量)
和类的成员函数
。- 注意定义类后面需要跟
;
,与C语言中定义结构体一样。Java中类就不用;
下面以一个栈类举例:
cpp
#include<cstdlib>
class Stack {
#define DEFAULT_CAPACITY 4
private:
int* _arr;
int _top;
int _capacity;
public:
Stack(int capacity = DEFAULT_CAPACITY) {
_arr = (int*)malloc(DEFAULT_CAPACITY * sizeof(int));
_top = 0;
_capacity = capacity;
}
void push(int val) {
if (_top == _capacity) {
int* tmp = (int*)realloc(_arr,2 * _capacity * sizeof(int));
if (tmp == nullptr) {
exit(-1);
}
_arr = tmp;
_capacity = 2 * _capacity;
}
_arr[_top++] = val;
}
bool isEmpty() {
return _top == 0;
}
int pop() {
if (isEmpty()) {
return 0xFFFF;
}
return _arr[--_top];
}
int peek() {
if (isEmpty()) {
return 0xFFFF;
}
return _arr[_top-1];
}
~Stack() {
free(_arr);
}
};
cpp
//#include<iostream>
int main(void) {
Stack stack;//创建一个类变量(对象)
stack.push(1);//类似中的结构体引用字段,这里同样的方式调用类中函数。
stack.push(2);
int ret = stack.pop();
std::cout<< ret << std::endl;//输出: 2
return 0;
}
C++中,并没有规定成员函数和成员变量的位置。推荐函数和字段分开写。
cpp
//这是三个成员字段。
private:
int* _arr;
int _top;
int _capacity;
为什么变量命名前面加_?
考虑若是我们取消了下划线,采用如下命名:
cpp
Stack(int capacity = DEFAULT_CAPACITY) {
arr = (int*)malloc(DEFAULT_CAPACITY * sizeof(int));
top = 0;
capacity = capacity;
}
局部作用域中的capacity覆盖了类里面的capacity,那么capacity = capacity
等价于局部变量自己和自己赋值,对类中的capacity字段
没有任何影响,故命名风格改为下划线打头(一种风格)。
C++中的struct
C++是C语言的超集, C++将C中的struct升级成了类,那么现在可以粗略认为class等价于struct
主要区别在于默认的访问权限和继承权限
:访问权限下文叙述。
struct 升级成了类,那么意味着:
- 类可以定义函数
- 类表示类型。
cpp
//定义一个链表类---普通的单链表
struct list {
//定义链表节点
struct Node {
int val;
Node* next;
};
Node *head;//头指针
//......
};
int main() {
// 不需要sizeof(struct list),可以省略struct,类名就是类型
std::cout<<"sizeof(list):"<<sizeof(list) << std::endl;
return 0;
}
类成员函数的内联性
类内定义的成员函数:
- 当成员函数在类内部定义时,编译器默认将其视为
inline
。这种定义方式允许编译器在每个调用点直接插入函数代码,从而可能提高性能。
cpp
class MyClass {
public:
void inlineFunction() { // 默认 inline
std::cout << "Inline function" << std::endl;
}
};
C++通常将类的声明和定义分开放置,有助于实现接口和实现的分离。
类中放函数定义(提供接口),成员函数在外部实现。
访问修饰符
C++实现封装的方式:类将对象的属性与方法结合在,让对象更加完善,通过访问权限允许用户使用。
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员类外不能直接被访问,类中可以使用(protected具体在继承中说明)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到} 即类结束。
- class的默认访问权限为private,struct为public(struct要兼容C)
类域
C++中,用{ }
括起来的都会形成一个域。类定义了一个新的作用域: 类域,类的所有成员都在类的作用域中。
命名冲突在C语言中很明显, C++中类域有效解决了问题。比如C中自定义的Stack和Queue若都定义了size函数,那么如果你想同时引用两个头文件,不可豁免的出现了命名冲突。这时候,就不得不做出折中,选择size方法加上标识以区分。
任何一个变量,编译器都会去找他的出处。编译器默认优先在局部域寻找,然后是全局域,不会去类域中找。类外定义成员时,要使用 : :
作用域操作符指明成员属于哪个类域.
cpp
class Date
{
public:
void Init(int year, int month, int day);
private:
int _year;
int _month;
int _day;
};
void Date::Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
类域影响的编译器查找规则,不指定类域,Init
函数会被全局函数。
域名限定修饰符中的Init
是Date类中的成员函数, 那么既然是类中的函数,那么可以引用类的成员字段。
实例化对象
用类类型创建对象的过程,称为类的实例化
- 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没
有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个
类,来描述具体学生信息。
类就像谜语一样,对谜底来进行描述,谜底就是谜语的一个实例。
谜语:"年纪不大,胡子一把,主人来了,就喊妈妈" 谜底:山羊 - 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄。 - 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设
计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象
才能实际存储数据,占用物理空间。
cpp
class Date
{
public:
void Init(int year, int month, int day);
private:
//只是声明类中有这个字段,并没有申请空间。
int _year;
int _month;
int _day;
};
cpp
int main(void) {
//用类这个类型创建一个变量的过程叫做实例化对象。
Date date;//只要实例化对象,才会分配空间
date.Init(2024, 9, 18);
return 0;
}
计算类对象的大小
cpp
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
char _a;
}
问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算
一个类的大小?
类实例化出的每个对象,都有独⽴的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?⾸先函数被编译后是⼀段指令,对象中没办法存储,这些指令存储在⼀个单独的区域(代码段),那么对象中⾮要存储的话,只能是成员函数的指针。再分析⼀下,对象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各⾃独⽴的成员变量 _year/_month/_day存储各⾃的数据,但是d1和d2的成员函数Init/Print指针却是⼀样的,存储在对象中就浪费了。如果⽤Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。
其实函数指针是不需要存储的,函数指针是⼀个地址,调⽤函数被编译成汇编指 令[call 地址],其实编译器在编译链接时,就要找到函数的地址,不是在运⾏时找.
只有动态多态是在运行时找,就需要存储函数地址。
cpp
// 类中既有成员变量,又有成员函数
class A1 {
public:
void f1() {}
private:
int _a;
};
class A2 {
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
int main(void) {
std::cout << "A1:" << sizeof(A1) << " A2:" << sizeof(A2) << " A3:" << sizeof(A3) << std::endl;
return 0;
}
输出结果:A1:4 A2:1 A3:1
结论:一个类的大小,实际就是该类中"成员变量"之和,但需注意内存对齐 空类比较特殊,编译器给了空类"一个字节"来唯一标识这个类的对象。
结构体内存对齐
这里只是列出C语言处学的结构体内层对齐规则。
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8 - 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
this指针
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;
}
对于上述类,有这样的一个问题:
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函
数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
在 C++ 中,调用一个对象的成员函数时,编译器自动将调用该函数的对象地址通过 this 指针传递给该函数。这个 this 指针指向当前调用函数的对象实例,从而让成员函数知道它应该操作哪个对象的成员变量。
具体来说, Date 类中,Init 和 Print 函数其实都隐含了对 this 指针的使用。编译器在编译时,会自动将 this 指针传递给这两个成员函数,使得它们能够操作调用它们的对象的成员变量。下面通过对代码的解释来说明:
cpp
class Date
{
public:
void Init(int year, int month, int day)
{
// 隐含 this 指针:this->_year, this->_month, this->_day
_year = year;
_month = month;
_day = day;
}
void Print() //隐含于void Print(Date *this)
{
// 隐含 this 指针:this->_year, this->_month, this->_day
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
this指针的特性
- this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
- 只在"成员函数"的内部使用
- this指针本质上是"成员函数"的形参,当对象调用成员函数时,将对象地址作为实参传递给
this形参。所以对象中不存储this指针。- this指针是"成员函数"第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传
递,不需要用户传递.
- this指针存在哪里?
函数栈帧的局部变量中,或者在函数内。 - this可以为nullptr?
在C++中this指针不能为nullptr,它始终指向当前对象(而当前对象是已分配有效地址的变量)。调用非静态成员函数时,this指针必须指向有效的对象实例。
如果内部没有出现类似this->?
操作,那么程序不会崩溃,但这种本身就是未定义行为。