1. 面向过程和面向对象的认识
- c语言是面向过程的,关注的是过程,分析出求解的步骤,通过函数调用逐步 解决问题。
- c++是 基于面向对象 的,关注的是对象,将一件事拆分成不同的对象,靠对象之间的交互完成。
如果写一个外卖系统
面向过程:
- 点餐下单
- 接单
- 分配骑手
- 骑手送餐
面向过程只会注意过程。
面向对象:
- 用户
- 商家
- 骑手
面向对象需要注意不同完成,对不同对象的需求和对象间的关系。
2. 类的引入
c语言中,结构体只能定义变量
c++中,结构体也就是类不仅可以定义变量,也可以定义函数。
c++中的类,也就可以认识是c语言结构体的升级
实现栈
c语言:
struct Stack
{
int* a;
int top;
int capacity;
}
void StackInit(struct Stack* pst)//初始化
{
//...
}
void StackPush(struct Stack* pst,int x)//入栈
{
//...
}
int main()
{
struct Stack st1;
StackInit(&st1);
StackPush(&st1,1);
return 0;
}
c++
struct Stack
{
void Init()//初始化
{
a=nullptr;
top=capacity=0;
}
void Push(int x)//入栈
{
//...
}
int* a;
int top;
int capacity;
}
int main()
{
//struct Stack st1;
Stack st1;
st1.Init();//初始化
st1.Push(1);//入栈
return 0;
}
相比于c语言中,c++中实现栈和调用更加方便
- c++中可以结构体中定义函数
- 入栈和初始化,我们不需要自己传入栈,直接在函数内使用对应的成员变量即可。
- 结构体中的函数称为成员函数,使用和c语言中成员变量相似,需要指明是哪个栈的成员函数。
3. 访问限定符
3.1. c++中为什么会出现类的概念
首先我们先看 以前写的结构体 有什么缺点
struct Stack
{
//成员函数
void Init()
{
a = nullptr;
top = capacity = 0;
}
void Push(int x)
{
//..扩容
a[top++]=x;
}
void Top()
{
return a[top-1]
}
//成员变量
int* a;
int top;
int capacity;
};
int main()
{
struct Stack st1;
Stack st2;
st2.Init();
st2.Push(1);
st2.Push(2);
st2.Push(3);
st2.Push(4);
return 0;
}
我们的成员变量 top 是从0开始的,也就是说 栈顶元素是 top-1的为位置的值
但是 Top 函数里面只有一句return,有人觉得调用麻烦,但是又不清楚我们写的栈 top 从哪里开始,所以他访问栈顶元素可能会这样写
cout<<st2.a[st2.top]<<endl;
这样会直接造成越界访问
3.2. 访问限定符:
- 如果我们以后还是和以前一样,让别人随意访问我们定义的内容,很可能会像上面的过程一样出错。
c++中,为了避免随意访问,就引入了访问限定符
而这也是c++实现封装的方式:用类创造出的对象,通过访问权限的选择将接口提供给外部用户使用。
(简单来说就是:用上面定义的结构体(类),定义了一个栈 st1(对象),只允许别人通过我们提供的公共区域使用我们这个对象)
上面的 类 创建的 st1 对象,别人对我们的成员变量进行访问导致出现bug,所以我们可以通过访问限定符限制权限,让他们不能使用我们的成员变量,只允许使用成员函数
struct Stack2
{
//成员函数
void Init()
{
a = nullptr;
top = capacity = 0;
}
void Print()
{
cout << a[top - 1] << endl;
}
//成员变量
private://私有
int* a;
int top;
int capacity;
};
在成员变量前加上 “private:” 即可,这样的话,在访问成员变量时就会提示不可访问
ps:我们只对类中的变量进行限定,类中的函数属于公共区域,可以随意访问,这样就能限制别人只能通过我们写的函数间接使用变量,而不是直接对成员变量进行操作。
3.3. 访问限定符的特点
- public修饰的成员可以在类外直接被访问。
- protected和private修饰的成员不能在类外直接被访问(protected和private类似)。
- 访问权限作用域从该访问 限定符出现的位置开始,一直到下一个访问限定符的出现时为止。
- 如果后面没有访问限定符,作用域到 "}"结束
- class默认权限为 private ,struct 默认权限为 public
- 注意:访问限定符只在编译上有用,当数据映射到内存后,没有任何访问限定符的区别(知道私有成员的地址,也可以对其进行操作)。
4. 类的定义
4.1. class关键字
class classname
{
//类体:由成员函数和成员变量组成
};//和结构体一样,必须有分号
class 为定义类的关键字
class为类的名字
类体中的内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法和成员函数。
4.2. 类的两种定义方式:
- 声明和定义全放在类体中,就像我们上面写的方式。需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
- 声明和定义分别放在两个文件中
Stack.h
#include<iostream>
#include<stdlib.h>
class Stack
{
public:
//成员函数声明
void Init();
void Push(int x);
void Destory();
int Top();
//成员变量
private: //类里面不受访问限定符的限制,限制的是类外面的使用
int* a;
int top;
int capacity;
};
Stack.cpp
注意在每个函数定义前,都需要在函数名前加上 Stack::
类定义了一个新的作用域
类的所有成员都在类的作用域中,在类外定义成员变量时,序要使用 "::"作用域操作符指定成员属于哪个类域。
#include"Stack.h"
void Stack::Init() //Stack 需要放在函数前面
{
a = nullptr;
top = capacity = 0;
}
void Stack::Push(int x)
{
if (top == capacity)
{
int newcapacity = capacity == 0 ? 4 : 2 * capacity;
a = (int*)realloc(a, newcapacity * sizeof(int));
capacity = newcapacity;
}
a[top++] = x;
}
void Stack::Destory()
{
free(a);
a = nullptr;
top = 0;
capacity = 0;
}
int Stack::Top()
{
return a[top - 1];
}
class特点:
struct :默认限定符公有(不加访问限定符,所有的内容都可以随意访问)
class :默认限定符私有(不加访问限定符,所有的内容都不可访问)
class:
class Stack
{
void Init()
{
a = nullptr;
top = capacity = 0;
}
void Push(int x)
{
//...
}
//成员变量
int* a;
int top;
int capacity;
};
如果想要让class公有,必须给public限定符
class Stack
{
public:
//...
};
平时建议使用class ,必须使用限定符
ps:类里面的内容不收访问限定符的限制
4.3. 类成员变量命名
定义一个 Date 的类,里面定义一个初始化的函数
#include<iostream>
using namespace std;
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 a;
a.Init(2023, 10, 15);
return 0;
}
这里的函数也是采用局部变量优先的原则
也就是说在 Init函数 内,局部变量 year 会给自己赋值,而不是给 a 中的 year 赋值,所以函数结束后,a 中值并未改变。
所以一般情况下,我们习惯在成员变量前加上 “_”,这样就能避免和传入参数同名 “_year”,“_month”,“_day”。
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;//声明
int _month;
int _day;
};
这并不是规定,是一种好习惯,也就是不一定非要前面加"_",也可以后面加,也可以用 "myear"来表示,怎么方便怎么来。
5.类的实例化
用类创建对象的过程----类的实例化
(简单来说,类相当于蓝图,蓝图无法住人,依据蓝图建造的房子可以住人)
#include<iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
cout << _year <<" "<< _month << " " << _day << endl;
private:
int _year;
int _month;
int _day;
};
int main()
{
Date _year=2013;
Date _month=10;
Date _day=18;
return 0;
}
我们不能直接对类的成员变量在外直接赋值,因为这些数想当于只是声明这个类里面有这个东西,但是并没有为其开辟空间。
只有用类实例化后的对象,才有定义的占有空间的成员变量,才能存储数据。
还是使用上面的类,我们计算类的大小
int main()
{
Date a;
Date b;
cout << sizeof(a) << endl;
cout << sizeof(Date) << endl;
return 0;
}
我们发现,最后的结果只有成员变量的大小,但是包含没有类中函数的大小。
这是因为这里计算的只会计算成员变量的大小,不会计算类内函数的大小。
在上面我们定义了两个 Date 类型,a 和 b,a 和 b 的成员变量时不相同的,a._year 和 b._year 是两个数,但是他们所调用的 Print 函数是相同的。
- 类在定义的时候,如果每个对象里都有定义函数的话,会导致空间的浪费,对所有对象来说, 内部的函数发挥的作用是相同的,所以没必要定义那么多函数,只需要把类里的函数放在类的公共区域,所有同类的对象都可以调用即可。
我们可以做个测试:
int main()
{
Date a;
a.Init(2023, 10, 18);
a.Print();
Date b;
b.Init(2023, 12, 12);
b.Print();
return 0;
}
我们分别对 a,b调用 Init 和 Print 函数,在反汇编内,可以看见他们调用的地址相同,也就是说他们调用的是同一个函数。
注意:a和b 内部定义的变量都不相同
a._year;
b._year
上面两个值不同,地址也不同
6. 类的大小
//1. 类中含函数和成员变量
class A1
{
public:
void f1()
{}
private:
int _a;
};
//2.类中仅有成员函数
class A2
{
public:
void f2()
{}
};
//3类中什么都没有
class A3
{};
int main()
{
cout<<sizeof(A1)<<endl;
cout<<sizeof(A2)<<endl;
cout<<sizeof(A3)<<endl;
return 0;
}
- 第一个很简单,就是上面所说的,只会计算成员变量的值,不会计算函数的值,int 为 4字节,所以为4。
- 下面两个结构体开的 1 字节的空间只是为了表示存在这两个结构体,这 1 字节不存储数据,只是占位,表示存在过,但是至于 1 字节存储什么不重要,只是为了区别不同的类,平时我们使用的时候是访问不到这个 1 的。
7.this指针
在说完类中函数时存储在类的公共区域,
那肯定有聪明的小伙伴会问:为什么同一类型下的不同对象,调用类中的函数,可以识别出是使用哪个对象呢
这就全靠我们将要说的 this 指针
首先还是我们的老朋友 Date类
using namespace std;
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;
};
int main()
{
Date a;
a.Init(2023, 10, 18);
a.Print();
return 0;
}
在我们调用的时候,我们看到的
void Init(int year, int month, int day);
void Print();
a.Init(2023,10,18);
a.Print();
实际上函数的定义和传入
void Init(Date* this,int year, int month, int day)
void Print(Date* this)
a.Init(&a,2023,10,28);
a.Print(&a);
我们初始化传入3个参数,输出没有传入参数
实际上初始化传入了4个参数,输出传入了1个参数
这个隐藏的参数----this 指针。
这个 this 是编译器自动加入的
函数的定义也就会变成下面的样子
void Init(Date*this,int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print(Date*this)
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
this-> 也是编译器自己加的
- 编译器自己加的,这里如果我们在传入参数位置就加不了了,自己加上去会报错
- this 在实参和形参位置不能明显的写。
- 但是在类里面可以明显的用
类中的函数可以调用自己类中的函数
class Date
{
public:
void Init(int year, int month, int day)
{
_year=year;
_month=month;
_day=day;
Print();
}
void Print()
{
cout<<_year<<"/"<<_month<<"/"<endl;
}
private:
int _year;
int _month;
int _day;
}
这里要注意的是, Init 和 Print 两个函数中分别使用的 this 指针,是指向同一块空间的两个不同指针,也就是说把其他一个置为 nullptr,另一个指针还存在,指向的空间不受影响。
8. this指针使用注意
- 问题1
class A
{
public:
void Print()
{
cout<<"Print()"<<endl;
}
private:
int _a;
};
//_________________________________________________
int main()
{
A* p=nullptr;
p->Print();
return 0;
}
程序会正常运行,
Printf 函数不在对象内保存,所以编译器会直接调用函数
2.问题2
class A
{
public:
void Print()
{
cout<<"Print()"<<endl;
}
private:
int _a;
};
int main()
{
A* p=nullptr;
(*p).Print();
return 0;
}
程序会正常运行;
在 vs 中,空指针解引用不会报错,但只是解引用不会报错,如果想要赋值或者进行其他操作,很可能会出错
3.问题3
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p=nullptr;
p->Print();
return 0;
}
这个代码就会崩溃
_a 是成员变量,但是传进来的是空指针,对空指针解引用查找成员变量就会崩溃