目录
一、类
1.1 类的引入
c语言结构体中只能定义变量,在c++中,结构体内不仅可以定义变量,也可以定义函数(把数据和方法放在了一起)。c++兼容c语言,在c++中struct以前的用法都可以继续用,同时c++升级成了类。
下面的Stack就是一个类:
#include<iostream>
using namespace std;
struct Stack {
//成员函数
void Init(int defaultcapacity = 4)
{
a = (int*)malloc(sizeof(int) * defaultcapacity);
if (a == nullptr)
{
perror("malloc fail");
return;
}
capacity = defaultcapacity;
top = 0;
}
void push(int x)
{
//...
a[top++] = x;
}
void Destroy()
{
free(a);
a = nullptr;
}
//成员变量
//这里的成员变量可以在任意位置,可以写到函数之前,也可以写到函数之后,写到函数之后也能找到,他认为类域是一个整体
int* a;
int capacity;
int top;
};
int main()
{
struct Stack st1;
st1.Init();
st1.push(3);
return 0;
}
1.2 类的定义
在C++中,我们更喜欢使用class定义类。
class classname
{
//类体:由成员函数和成员变量组成
}; //注意分号
class是定义类的关键字,classname是类的名字,{}中是类的主体,注意类定义结束时后面的分号不能省略。
类体中内容是类的成员,类中的变量称为类的属性或成员变量,类中的函数称为类的方法或者成员函数。
1.3 类的访问限定符及封装
1.3.1 类的访问限定符
如果我们把上述代码中的struct改为class,我们会发现会报错。
这是为什么?是由于类的访问限定符。C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
![](https://img-blog.csdnimg.cn/1935bc1540354dd1972146263d677a80.jpeg)
访问限定符的说明:
- public修饰的成员在类外可以直接被访问。
-
protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的
-
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
class Stack { public: //一般情况下,成员函数是公有的 //成员函数 void Init(int defaultcapacity = 4) { a = (int*)malloc(sizeof(int) * defaultcapacity); if (a == nullptr) { perror("malloc fail"); return; } capacity = defaultcapacity; top = 0; } void push(int x) { //... a[top++] = x; } void Destroy() { free(a); a = nullptr; } //private之前的是public访问权限的作用域 private: //一般情况下,成员变量是私有的 int* a; int capacity; int top; };
-
如果后面没有访问限定符,作用域就到 } 即类结束。
-
class的默认访问权限为private,struct为public(因为struct要兼容C)
1.3.2 封装
面向对象的三大特性:封装、继承、多态。
在类和对象阶段,主要是研究类的封装特性,那什么是封装呢? 封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。 封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来 隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
1.4 类定义方式
1.4.1 声明和定义都放在类体里
声明和定义都放在类体里,需要注意的是成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
class Date
{
public:
//类里面定义的函数默认是inline的
void Init(int year)
{
_year = year;
}
private:
int _year;
int _month;
int _day;
};
1.4.2 类可以声明和定义分离
1.5 类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
class Date
{
public:
void Init(int year);
private:
int _year;
int _month;
int _day;
};
void Date::Init(int year) //指定类域,类中某个成员函数的定义
{
_year = year;
}
1.6 类的实例化
1.6.1 定义
用类类型创建对象的过程,称为类的实例化。
1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。
2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄。
类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
//只是声明,还没有对象
class Stack {
public:
//成员函数
void Init(int defaultcapacity = 4);
void push(int x)
{
//...
a[top++] = x;
}
void Destroy()
{
free(a);
a = nullptr;
}
private:
//成员变量
//这里的成员变量可以在任意位置,可以写到函数之前,也可以写到函数之后,写到函数之后也能找到,他认为类域是一个整体
int* a;
int capacity;
int top;
};
int main()
{
//类实例化对象
Stack st1;
Stack st2;
return 0;
}
1.6.2 计算类对象的大小
首先,我们需要知道,类中可以有成员变量,又可以有成员函数,那么一个类的对象包含了什么?类的对象的大小只算成员变量,不算成员函数。
其次,在我们计算类的大小时,只算成员函数,但是我们依然要按照内存对齐的方式去计算。
结构体的对齐规则:
1.第一个成员在与结构体变量偏移量为0的地址处。
2.其他的成员变量要对齐到某个【对齐数】的整数倍的偏移处。【对齐数】:结构体成员自身大小和编译器默认对齐数的较小值,在vs中默认对齐数是8.
3.结构体的总大小,必须是最大对齐数的整数倍,每个结构体变量都有一个对齐数,其中最大的对齐数就是最大对齐数。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
具体关于结构体内存对齐的相关知识可以参考:结构体内存对齐_李有鱼的博客-CSDN博客
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _b;
char _a;
double _c;
};
int main()
{
A a1;
A a2;
cout << sizeof(a1) << endl;
cout << sizeof(a2) << endl;
cout << sizeof(A) << endl;
return 0;
}
二、this指针
2.1 this指针的使用
class Data
{
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;
};
int main()
{
Data d1;
Data d2;
d1.Init(2023,5,3);
d2.Init(2023,2,5);
d1.Print();
d2.Print(); //d1,d2调用的函数相同,为什么打印的结果不同?
return 0;
}
在上述的代码中,Data类中有Init和Print函数,函数体中没有关于不同对象的区分,当d1调用函数时,这个函数怎么知道是设置d1对象,而不是设置d2对象呢?通过this指针,在编译阶段,编译器会对成员函数进行处理,类似于下方,在调用类中的函数时,传一个指针,在函数定义处,使用一个指针来接收,这些都是编译器进行的操作。
class Data
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
//在编译时,编译器会对成员函数继续处理,处理后变成:
/*void Print(Data* this)
{
cout << this->_year << " " << this->_month << " " << this->_day << endl;
}*/
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1;
Data d2;
d1.Print();
d2.Print();
//编译时,编译器处理后:
/*d1.Print(&d1);
d2.Print(&d2);*/
return 0;
}
2.2 this指针的特性
- this指针不能在形参和实参显式传递,但是可以在函数内部显示使用,且不能改变this。
-
this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
-
this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
2.3 this指针存在哪里?
this指针是形参,所以this指针是和普通参数一样存在函数调用的栈帧里面。
2.4 this指针可以为空吗?
this指针可以为空,但是当this指针为空时,不能对this指针进行解引用。
我们来看下面的两个例子:
2.4.1 例一
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
上述代码会正常运行,p调用Print,不会对p解引用,Print的地址不在p指向的对象中,他在公共代码段,调用Print实际上传递的是对象的地址,即空指针p,实参就是p,形参是this指针,在Print函数内部并没有对this指针(空指针)发生解引用操作,所以程序正常运行。
2.4.2 例二
class A
{
public:
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
上述代码会导致运行奔溃,p调用Print,Print的地址不在对象中,而是在公共代码段中,在此处未发生对空指针的解引用,调用时,p是实参,this指针是形参,p传给this指针,this指针是空指针,在Print函数中访问了_a,本质上是this->_a,发生了对空指针的访问,所以程序会奔溃。
三、C语言和C++实现栈
3.1 C语言实现栈
3.1.1 代码
//C语言实现栈
#include<stdlib.h>
#include<assert.h>
typedef int DataType;
typedef struct Stack
{
DataType* a;
int top;
int capacity;
}Stack;
void StackInit(Stack* ps)
{
assert(ps);
ps->a = (DataType*)malloc(sizeof(DataType) * 4);
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
ps->top = 0;
ps->capacity = 4;
}
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
void CheckCapacity(Stack* ps)
{
assert(ps);
DataType* tmp = (DataType*)realloc(ps->a, sizeof(DataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
void StackPush(Stack* ps, DataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
CheckCapacity(ps);
}
ps->a[ps->top] = x;
ps->top++;
}
int StackEmty(Stack* ps)
{
assert(ps);
return 0 == ps->top;
}
void StackPop(Stack* ps)
{
assert(!StackEmty(ps));
ps->top--;
}
DataType StackTop(Stack* ps)
{
assert(!StackEmty(ps));
return ps->a[ps->top - 1];
}
int StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
int main()
{
Stack s;
StackInit(&s);
StackPush(&s, 1);
StackPush(&s, 2);
StackPush(&s, 3);
StackPush(&s, 4);
StackPush(&s, 5);
printf("%d\n", StackTop(&s));
StackPush(&s, 7);
printf("%d\n", StackTop(&s));
StackPop(&s);
StackPop(&s);
StackPop(&s);
printf("%d\n", StackTop(&s));
return 0;
}
3.1.2 特点
- 每个函数的第一个参数都是Stack*
- 函数中必须要对第一个参数检测,因为该参数可能会为NULL
- 函数中都是通过Stack*参数操作栈的
- 调用时必须传递Stack结构体变量的地址
我们可以发现显著的特点,C语言实现栈,它的数据和操作数据的方法是分离的。
3.2 C++实现栈
3.2.1 代码
//C++实现栈
typedef int DataType;
#include<stdlib.h>
class Stack
{
public:
Stack(int decapacity = 4)
{
_a = (DataType*)malloc(sizeof(DataType) * decapacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = decapacity;
}
void Push(DataType x)
{
CheckCapacity();
_a[_top] = x;
_top++;
}
void Pop()
{
if (Empty())
return;
_top--;
}
DataType Top()
{
return _a[_top-1];
}
int Empty()
{
return 0 == _top;
}
int Size()
{
return _top;
}
void Destroy()
{
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
private:
void CheckCapacity()
{
if (_top == _capacity)
{
DataType* tmp = (DataType*)realloc(_a, sizeof(DataType) * _capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_a = tmp;
_capacity *= 2;
}
}
DataType* _a;
int _top;
int _capacity;
};
int main()
{
Stack s(5);
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
s.Push(5);
printf("%d\n", s.Top());
s.Pop();
s.Pop();
s.Pop();
printf("%d\n", s.Top());
}