目录
类
类的定义(class)
- C++中用class关键字来定义类,和C语言中结构体一样其中类的主体用{}括起来,并在后面加上分号
- 类中的变量称为类的属性或成员变量; 类中的函数称为类的⽅法或
者成员函数 - 为了区分成员变量,在命名的时候开头用_或者m(不强制)
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
【 注】:class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略
还记得我们在数据结构中讲的栈吗?那时候我们是用结构体来完成栈的定义的,然后把他的各种功能函数集中声明在一个头文件中。
- 但是我们现在在C++中学了类的定义,知道这些功能函数可以直接在类中声明。栈的结构也可以直接在类中定义
#pragma once
#include<iostream>
typedef int STDataType; //方便以后修改存储类型
using namespace std;
class Stack
{
public:
void Init(int n = 4); //缺省参数
private:
STDataType* _arr;
int _Capacity;
int _top;
};
类的定义(struct)
- C++中struct也可以定义类,C++兼容C中struct的⽤法,同时struct升级成了类,明显的变化是struct中可以定义函数,⼀般情况下我们还是推荐⽤class定义类
typedef int DataType;
struct Stack
{
void Init(size_t capacity)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(const DataType& data)
{
// 扩容...
_array[_size] = data;
++_size;
}
DataType Top()
{
return _array[_size - 1];
}
void Destroy()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
DataType* _array;
size_t _capacity;
size_t _size;
};
- 这里在定义类的时候也可以把成员函数一起定义实现了
- 但是值得注意的是定义在类⾯的成员函数默认为inline(内联函数)。内联函数只适合实现短小函数(几行代码),所以在类里的成员函数最后只声明,定义在另一个定义文件中实现
类的三个访问限定符
public(共有)
- 从public访问限定符到下一个限定符为止的成员函数(一般)可以在类的作用域以外的作用域直接访问。如果只有public访问限定符,那么直到收括号};为止
- 注意:public限定的一般都是成员函数(方法),我们通常只希望用户使用这个方法而不修改数据,所以数据这些(如栈中的top(栈顶元素))一般都是用private (私有)限定符限定
#pragma once
#include<iostream>
#include<assert.h>
typedef int STDataType; //方便以后修改存储类型
using namespace std;
class Stack
{
public:
void Init(int n = 4); //缺省参数
void Push(STDataType x);
STDataType Top();
private:
STDataType* _arr;
int _Capacity;
int _top;
};
- 可以知道inti函数是被public限定
#define _CRT_SECURE_NO_WARNINGS
#include"Stack.h"
void Stack::Init(int n) //要使用类中的成员函数和成员变量,要指定类域
{
_arr = (STDataType*)malloc(sizeof(STDataType) * n);
if (_arr == nullptr)
{
perror("malloc failed");
exit(1);
}
_Capacity = _top = 0;
}
void Stack::Push(STDataType x)
{
//扩容...
_arr[_top++] = x;
}
STDataType Stack::Top()
{
assert(_top > 0);
return _arr[_top - 1];
}
- 因为Init方法被public(共有)限定,这里就可以在Test()函数这个局部域中直接访问Stack类中的Init方法
注意:
用struct关键字定义的类中,如果是没有写限定符限定成员变量和函数,他们默认被public限定
private(私人)
- private指的就是私有,这很直观,对于共有来说就是所有东西都是公开的,无论是谁都可以访问;那对于私有来说便是无法访问,谁无法访问呢?这里指的是外界无法访问,但类内还是可以访问的,例如就是类内的成员函数访问这些成员变量是不受限制的
如:
typedef int STDataType; //方便以后修改存储类型
using namespace std;
class Stack
{
public:
void Init(int n = 4); //缺省参数
void Push(STDataType x);
STDataType Top();
private:
STDataType* _arr;
int _Capacity;
int _top;
};
cout << st._top << endl;
cout << st._Capacity << endl;
- 因为_top和_capacity成员变量在类中是被private限定符修饰,所以只能在类中访问这些成员变量
那为什么可以对他们去进行一个初始化呢?那不是访问到了这些成员变量了
如:
void Stack::Init(int n) //要使用类中的成员函数和成员变量,要指定类域
{
_arr = (STDataType*)malloc(sizeof(STDataType) * n);
if (_arr == nullptr)
{
perror("malloc failed");
exit(1);
}
_Capacity = _top = 0;
}
- 这是因为我们访问类里面的成员变量的时候,并没有直接向上面一样直接访问,而是调用类里面的成员函数来访问了成员变量,因为这个成员函数也是在类里面和成员变量一个作用域,当然可以直接访问了
注意:
- 用class关键字定义的类如果没有使用限定符,默认被private限定符修饰
初探类的封装
对于上面的操作来说,就已经涉及到类的三大特性之一——封装
- 封装思想:就是把数据和方法封装在一起,让对象更加完善,将其接口提供给外面的用户使用
- 封装思想就如同用户使用电脑,用户通过键盘,鼠标,显示屏等外部设备和电脑互动,但是电脑的各种功能通常是用CPU,显卡,内存条等硬件实现,那么我们每次使用一个功能的时候都要调整这些硬件来实现吗?不妨,直接把他们集中一起封装,我们通过其提供的外部接口来享受这些功能。(很好地将内部的细节给屏蔽起来了,方便管理】)
protected(保护)
这个限定符在初始类和对象这个阶段我们就把他认为和private限定符作用一样,以后在继承章节才能体现出他们的区别
- ⼀般成员变量都会被限制为private/protected,需要给别⼈使⽤的成员函数会放为public
类的域
- 类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤ :: 作⽤域操作符指明成员属于哪个类域。
如:
#pragma once
#include<iostream>
#include<assert.h>
typedef int STDataType; //方便以后修改存储类型
using namespace std;
class Stack
{
public:
void Init(int n = 4); //缺省参数
void Push(STDataType x);
STDataType Top();
private:
STDataType* _arr;
int _Capacity;
int _top;
};
#define _CRT_SECURE_NO_WARNINGS
#include"Stack.h"
void Stack::Init(int n) //要使用类中的成员函数和成员变量,要指定类域
{
_arr = (STDataType*)malloc(sizeof(STDataType) * n);
if (_arr == nullptr)
{
perror("malloc failed");
exit(1);
}
_Capacity = _top = 0;
}
- 在上初始C++中,我们讲域的时候知道,通常我们编译器会对变量和函数找出处,通常会先找局部域,然后找全局域。不会主动在命名空间域和类域中查找,这时候如果不指定域就会找不到这个变量或函数的出处
类的实例化
当我们写好一个类或者一个结构体后,都要定义个变量来使用。这时候就要牵扯到一个问题,什么是定义,什么又是声明
- 变量的声明和定义
- 这个是在类中被private限定符修饰的成员变量,但是类此时变没有定义变量,所以这些变量就只是声明
- 总结一下:只有被分配了内存空间才叫定义,没有被分配内存空间就是声明。
所以说,类就相当于一张修别墅的一张图纸,他限定了别墅应该有那些家具,布局这些。但是并没有真正的开始建造别墅,所以其中的家具之类的不存在。
- 而类的实例化就是用类来定义出对象,这时候就是拿着类这张图纸建造别墅,建造完成后,其图纸上的家具这些就有了
这样就是定义了,真正的分配空间
int main(void)
{
Date d; //类对象的实例化
return 0;
}
计算类的大小
要想知道这个类中存在多少东西,其实我们去计算一个它的大小即可
- 还记得结构体内存对齐吗?忘记了就再去看看,下面是对应的规则👇
- 在C语言中,我们有去计算过一个结构体的大小,那上面我们在对于结构体和类做对比的时候说到对于struct和class都可以去定义一个类,那么结构体内存对齐的规则也一样适用。不过我们只会计算成员变量的大小,那就来先计算一下这个【year】、【month】、【day】的大小
- 那要是类中有成员函数呢?他们的空间是多少
- 这里我们之间给出答案,我们计算类的大小时,不用计算成员函数的大小。
为啥?难道成员函数在类中不分配存储空间吗?对!成员函数的空间是在公共代码区里开辟的。
因为如果我们把成员函数空间开辟在类中:
可以看到如果有多个对象,创建一个对象就会对同一个功能的函数重复的开辟空间调用,这不是浪费空间吗?
- 可以看到如果把同样功能的成员函数放在公共代码区,那么我们每次创建一个对象的时候就只需要访问公共代码区的成员函数即可,不用再创建的同时开辟空间给成员函数了
空类大小计算
- 学习了如何去计算一个类之后,接下去请你来判别一下下面三个类的大小分别为多少
// 类中既有成员变量,又有成员函数
class A1 {
void f1() {}
private:
int a;
- List item
};
// 类中仅有成员函数
class A2 {
void f1(){}
};
// 类中什么都没有---空类
class A3 {};
- 通过观察可以得知,似乎只算对了第一个类A1的大小,但是前两个类的大小为什么都是1呢?这相信读者也是非常疑惑吧?立马为你来解答👇
- 一个类的大小,如果说有成员变量的话实际上就是成员变量之和(内存对齐后),如果没有成员变量,那么就是一个空类,而空类的大学就是1,表示这个类存在过,用于占位
this指针
先来看一个我们的日期类:
class Date {
public:
//定义
void Init(int year, int month, int day)
{
_year = year;
_year = month;
_year = day;
}
void Print()
{
cout << "year:" << _year << endl;
cout << "month:" << _year << endl;
cout << "day:" << _year << endl;
}
private:
int _year; //仅仅是声明,并没有开出实际的空间
int _month;
int _day;
};
- 此时我们在main函数中进行对类的成员函数调用
int main()
{
d1.Init(2024, 3, 31);
d1.Print();
d2.Init(2024, 7, 5);
d2.Print();
}
- 前面我们讲,成员函数是放在公共代码区的,那么我们创建的d1和d2都会调用公共代码区的同一个函数,那外部传入实参的时候是可以分清的,但是传入到内部时_year = year中的[_year]要怎么区分这是d1还是d2的成员变量呢?若有又定义了一个d3呢?如何做到精准赋值无误?
- 这里的问题就是,我们公共代码区的函数工作时,不知道传递的参数到底是给那个对象,那么我们如果想要知道给那个对象,是不是应该传一下那个对象的地址。
- 上面讲了这么多不知读者是否关注到我说的一点:外界无法传入当前对象的地址给到被调用的成员函数
- 其实这个地址是编译器自动会传递,编译器隐式用this指针传递了每个对象的地址。
//void Init(int year, int month, int day)
void Init(Date* this, int year, int month, int day)
- 那么形参部分改变了,实参也需要修改,那要传递什么呢?没错,就是当前对象的地址
//d1.Init(2023, 3, 18);
d1.Init(&d1, 2023, 3, 18);
- 类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值, this-
_year = year;
- 不过通过观察可以发现,似乎我们自己去加上这一个参数好像是行不通,编译器报出了很多错误
- C++规定不能在实参和形参的位置显⽰的写this指针(编译时编译器会处理),但是可以在函数体内显
⽰使⽤this指针。 - 但是在成员函数内
- List item
部是可以用this指针的
夺命面试题
this指针存放在哪里?
- 这时候就有人说,this指针是在对象里进行操作的,肯定就是存放在对象里。
如果你真的那么想,那就错了。
- this指针是作为成员函数的隐式形参,属于局部变量,那么局部变量就需要压栈,所以this指针是存放在栈里的。
this指针的解引用
class Date {
public:
//定义
//void Init(Date* this, int year, int month, int day)
void Init(int year, int month, int day)
{
cout << "this:" << this << endl;
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print()
{
cout << "Print()" << endl;
}
private:
int _year; //仅仅是声明,并没有开出实际的空间
int _month;
int _day;
};
int main()
{
Date* p = nullptr;
p->Print();`在这里插入代码片`
}
- 有人看到这个就说,肯定会运行出错,这里是对空指针解引用。
但是我们看:
- 代码并没有任何问题
这里我们再来看看我们的操作:
Date* p = nullptr;
p->Print();
是不是对类里面的成员函数解引用?
- 但是我们前面说了,成员函数是存在类里面吗?不是吧,是存在公共代码区里面。所以说这里并没有对Print()函数的地址进行解引用。就正常运行了。
那我们再来看看:
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
- 这道题跟上面不同的地方就是多了cout<<_a<<endl
运行结果:
- 注意看上面类的代码,我们_a成员变量可是实实在在存在类里面的他可不是什么成员函数,那么既然在类里,就可以通过类的指针解引用,但是这里在main函数对类的指针初始化空指针,所以这里就会对空指针解引用
注意这里成员函数内部不用this->_year也会对空指针解引用
因为我们前面说过,this指针也可以不写,编译器会隐式自动加上。