面向对象:
更接近真实世界(关注各对象之间的关系,而非各步骤的进行)
-
将结构体升级成立类
-
类里面可以有:成员函数,成员变量
-
class Stack { public:void Init(int defaultCapacity=4 ) {_a = (int*)malloc(sizeof(int) * defaultCapacity);_capacity = defaultCapacity;_top = 0;}void Push(int x) {_a[_top++] = x;}void Destory() {free(_a);_a = nullptr;} private:int* _a;int _top;int _capacity; }; int main() {Stack st;st.Init();st.Push(1);st.Destory();return 0; }
-
访问限定符(即访问权限):
-
- public:类外可以访问
- protected
- private
- 每一种访问限定符的作用域是从当前限定符到下一个限定符
-
一个类里面的函数,也可以声明与定义分离(如果函数太长了话)(如果声明和定义都直接写在类里面,如果这些函数符合内联规则,则默认都是内联函数)
-
//stack.c class Stack { public:void Init(int defaultCapacity=4);void Push(int x) {a[top++] = x;} }
-
//stack.h //指定类域 void Stack::Init(int defaultCapacity=4 ) {a = (int*)malloc(sizeof(int) * defaultCapacity);capacity = defaultCapacity;top = 0; }
封装
-
本质上是一种更好的管理(便于用户使用类)
-
对于成员变量,只有在实例化某一个类的时候,才会为类里面的成员变量开辟空间(即它的定义)(也就相当于类其实是一种设计,并没有实际的东西,只有实例化之后,才有这种设计对应的实例)
-
class Stack { private:int* _a;int _top;int _capacity;/*这些变量是声明,不是定义(对于变量来说,是否开空间是区分声明和定义的最大区别)所以在实例化对象的时候,才定义这些变量*/ };
-
对于某一个类的实例化对象来说,
sizeof()
计算的是成员变量在经过内存对齐之后的总大小,不加成员函数。(因为对于每一个实例来说,都有一套成员变量都是单独属于它们自己的。但对于成员函数来说,所有实例的该方法都是一样的,没有必要每个实例都存储一份成员函数,只需要在一个公用区域存一份,所有实例都访问这一个就行了,所以sizeof()
只有成员变量,没有成员函数) -
如果一个类只有成员函数,或者什么都没有,
sizeof()
的计算结果是1。相当于这个类的对象默认占一个字节,相当于占一个位置,表示这个对象是存在的,但不存有效数据
This指针
-
其实对于实例化对象来说,虽然成员函数是共用的,但每个实例调用时都是使用自己的成员变量的值,其实本质上是因为编译器默认使用了this指针,将当前实例的地址传给了成员函数,所以不同实例在调用的时候,使用的是自己的成员变量
-
class Date { public:void print() {cout << year << " " << mon << " " << day;}void init() {year = 1;mon = 2;day = 3;} private:int year;int mon;int day; }; int main() {Date d;d.init();d.print();return 0; } /*本质上其实是将当前实例的地址传给了成员函数,所以不同实例在调用的时候,使用的是自己的成员变量void print(Date* this) {cout << this->year << " " << this->mon;}Date d;d.print(&d); */
-
this不能在形参和实参传递,但可以直接在函数内部使用
-
this指针是形参,所以和普通参数一样,是存在栈里面的,作为栈帧的一部分(随压栈一起)
类的六大默认成员函数(不显式写出编译器就会自行生成)
-
一.构造函数(完成实例的初始化):
-
-
函数名与类名相同
-
无返回值(也不需要写void)
-
在对象实例化的时候编译器自动调用对应的构造函数(如果不写构造函数,则编译器会自动生成默认的构造函数)
-
构造函数可以重载(多种初始化的方式)
-
没有自己写的构造函数时,编译器生成默认的构造函数,这个函数针对内置类型不做处理,针对自定义类型成员,会去调用该类型的构造函数(而对于成员变量的声明,可以给默认参数,本质上就是给这个自动生成的构造函数使用的)
-
**默认构造函数(无参构造函数,全缺省构造函数,没有构造函数时编译器默认生成的构造函数)**最多只能有一个,也可以没有(如果没有,那就不能用默认方式初始化)如果当自己写了别的不是默认构造函数形式的构造函数,那就需要自己在写一个默认构造函数
-
有两种情况不需要自己写构造函数:
-
- 内置类型成员都有缺省值,且初始化符合我们的要求
- 全是自定义类型的数据,且这些类型都有自己的默认构造函数
-
class stack { public:stack(int capacity=4) {_a = (int*)malloc(sizeof(int) * capacity);_capacity = 0;_top = 0;}private:/*给的是缺省值,是给编译器的默认构造函数用的,这里并没有开辟空间,在实例化的时候才会开辟空间*/int* _a=nullptr;int _capacity=1;int _top=1; };
-
-
二.析构函数(相当于destroy)(完成对象中资源的清理工作,即free等,为了防止编译器直接销毁对象造成内存泄露):
-
-
在类名前加上
~
-
无返回值无参数
-
不可以重载
-
编译系统自动调用
-
不写析构函数,编译器会自动生成(这个函数针对内置类型不做处理,针对自定义类型成员,会去调用该类型的析构函数)
-
有动态申请的资源,就需要自己写析构函数(因为普通的变量会在函数本身创建的栈上,随函数结束栈的销毁而销毁了,所以只有在堆区或其他区上有自行申请的资源才需要析构)
-
class stack { public:~stack() {free(_a);_a = nullptr;_capacity = 0;_top = 0;} private:int* _a;int _capacity;int _top; };
-
-
三.拷贝构造
-
-
是一种构造函数的特殊重载形式,相当于传一个同类型的变量作为参数
-
在传参时,必须以引用的方式传递参数,不然会发生无限递归
-
因为如果以传值调用来传递参数,本质上来说其实是要调用该类型的拷贝构造函数来创建形参的,也就相当于对于内置数据类型形参是直接拷贝,而自定义数据类型则是调用拷贝构造函数来创建形参的
-
class Date { public:Date(const Date & d) {_year = d._year;_mon = d._mon;_day = d._day ;} private:int _year = 1;int _mon = 1;int _day = 1; };
-
虽然_year这些成员属性是私有的,但私有属性只有在类外不能访问,此时在类内,是可以访问的
-
如果不显式定义,编译器会自己生成默认拷贝构造函数,默认是:
-
- 内置数据类型完成值拷贝/浅拷贝
- 自定义数据类型会调用它的拷贝构造函数
-
在有些场景下,浅拷贝不足以满足要求,就需要自己写拷贝构造函数,以实现深拷贝(比如在stack里,如果进行浅拷贝,那么会使两个栈里面的数组指针指向同一块空间(因为浅拷贝只将数值原封不动的拷贝过去,所以它们指向同一块空间),显然不是我们想要的)
-
-
四.赋值运算符重载:
-
-
用于已经存在的两个对象之间的赋值拷贝
-
本质就是将赋值运算符重载了一下
-
如果不显式定义,编译器会自己生成默认赋值运算符重载,默认是:
-
-
- 内置数据类型完成值拷贝/浅拷贝
-
- 自定义数据类型会调用它的赋值运算符重载
- 赋值运算符只能定义在类内部,不能重载成全局函数。(别的运算符既可以重载在类内,也可以重载成全局,但由于赋值运算符重载是一种默认成员函数,所以只能在类内)
-
-
class Date { public:Date& operator= (Date& x) {_day = x._day;return *this;} //之所以返回值是一个(Date&),是为了支持连续赋值的情景,如果不要求支持连续赋值,则返回值可以写成void private:int _year = 1;int _mon = 1;int _day = 1; };int main() {Date d1(1);Date d2(2);Date d3(3);d3 = d1 = d2;return 0; }
-
-
五.取地址运算符的重载
-
Date* Date::operator& () {return this; } const Date* Date::operator& () const {return this; }//这两个函数因为参数不同构成函数重载
-
const
-
对应一些成员实例,如果是const修饰的,那么在使用一些成员函数的时候可能会造成权限的放大,从而导致错误
-
下面这个代码里,如果
print函数
右侧不加const
,则无法正常打印,因为此时d1的类型是const Date
,而且指向该对象的指针this
是隐式传递的,类型是Date*
由于它的类型是编译器底层传递的,我们不能显式来写出this
,所以只能将const
写在最右边,其实本质就是修饰this
指针的 -
void print() const//修饰this指针的const {cout << _year<<"--"<<_mon<<"--"<<_day << endl; }const Date d1(2023, 1, 26); d1.print();
-
在谈构造函数
-
第一种:构造函数体赋值
-
Date::Date(int year, int mon, int day) {_year = year;_mon = mon;_day = day; }
-
第二种:初始化列表
- 有三类成员变量必须用初始化列表初始化:
-
- 没有默认构造函数的结构体变量
- 引用类型
- const修饰的变量
- 对于初始化列表的理解:在实例化对象时相当于时整个对象的定义,而对于它的成员变量,其实是在初始化列表所在的位置定义的,因为引用和const修饰的变量只有在定义的时候才能赋值,所以它们只能在初始化列表里面初始化。其实在初始化列表里,如果不写出所有成员变量,那么就是自定义类型调用默认构造函数,内置类型不做处理(其实内置类型的成员变量在声明的时候给的默认值,就是传到初始化列表里,用于内置类型成员变量初始化的。)它下面的那个
{}
,是为了某些多步初始化的过程无法在初始化列表里完成,需要在{}
里完成 - 成员变量的初始化顺序是按类中的声明顺序初始化的,和其在初始化列表里的顺序无关
-
Date(int year, int mon, int day):_year(year), _mon(mon), _day(day) {}
explicit 关键字
- 如果构造函数在传参的时候不想让其发生隐式类型转换,就可以在函数名之前加上
explicit
静态成员,函数
- 静态成员变量是属于这个类的,生命周期是全局的。而普通的成员变量是属于该类的实例的。也就是说静态成员变量是所有实例共享的变量。
- 静态成员变量没有初始化列表,所以静态成员变量必须在类外边,全局位置定义。(本质上相当于用类去封装全局变量)
- 静态成员函数:没有this指针,只要指定类域和访问限定符即可访问。静态成员函数只能访问静态成员变量,因为它没有this指针,没法访问某一个实例的普通成员变量
- 非静态函数可以访问静态成员变量,静态函数不可以访问静态成员变量,函数(因为静态函数没有this指针)
class A {
public:static int _a;
private:
};int A::_a = 0;int main() {A aa;cout << ++A::_a << endl;cout << A::_a << endl;return 0;
}
-------------------------------------------------------------
-------------------------------------------------------------
class A {
public:static int Get() {return _a;}
private:static int _a;
};int A::_a = 0;int main() {A aa;cout << aa.Get() << endl;cout << A::Get() << endl;return 0;
}