c++是一门基于对象编程的语言
所以在c++中引入了类和对象的概念
让我来介绍类和对象是什么,以及他们的用法和作用
以我目前的水平很难讲述清楚对象是什么,我会在下面插入我目前对对象的思想。
文章目录
类
1.类的概述
a.什么是类?
在c语言中,有着struct结构体,在c++中结构体被升级成了类,他有着结构体的全部功能,也有着新增功能,并且现在下面的A也是这个结构体的名字,不需要再typedef。
它现在可以在里面定义函数。
struct A {
int Add(int x, int y)
{
return x + y;
}
//...
int _x;
int _y;
int xxx;
//...
};
而且它里面的成员和函数都是公有的,至于公有私有稍后再详述。
在c++中我们使用类一般是用另一个关键字class,来定义一个类
class A {
int Add(int x, int y)
{
return x + y;
}
//...
int _x;
int _y;
int xxx;
//...
};
虽然他和上面的代码很相似,但是大有不同。他里面的成员是默认私有。
b.私有成员和公有成员
在这里简单说一下私有成员和公有公有成员
私有成员:私有成员(变量和函数)在类的域外无法访问,作用域运算符也无法访问
公有成员:外界可以访问。
class A {
public:
int Add(int x, int y)
{
return x + y;
}
int x;
int y;
private:
void Print()
{
cout << "void Print()" << endl;
}
int _x;
int _y;
};
在这里面,public以后到下一个访问限定符或者域的结尾之间的东西都可以被外界访问,private之后的到下一个访问限定符或者类的花括号结尾之间的东西不能被外界访问。
int main()
{
A a1;
cout << a1.Add(3, 5) << endl;//可以正常执行
a1.x = 1;
a1.y = 2;
a1.Print();//私有成员会报错
a1._x = 0;
a1._y = 1;
}
到这里类体现出两个好处:
1.编写代码简洁。
在C语言中要写一个数据结构,我们的函数命名可能是StackPush等等,而在类中我们可
以直接命名函 数为Push,我们可以很明了的知道,Push就是Stack的函数。
class Stack
{
public:
// 成员函数
void Init()
{
_a = nullptr;
_top = _capacity = 0;
}
void Push(int x)
{
if (_top == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
_a = (int*)realloc(_a, sizeof(int) * newcapacity);
_capacity = newcapacity;
}
_a[_top++] = x;
}
int Top()
{
return _a[_top - 1];
}
private:
// 成员变量
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st;
st.Init();
st.Push(1);
st.Push(2);
st.Push(3);
st.Push(4);
cout << st.Top() << endl;
return 0;
}
2.私有成员的关系,对于维护代码的人员来说有了更好的保障,也提高了代码的规范性。
a):私有成员不允许被访问,也就不能随意直接修改
b):在顺序表结构中有人要获取数组中的数据时,可能会直接对数组用下标访问,
而不使用规范性的函数来获取。
c.定义和声明分离
成员函数是可以声明定义分离的,但是函数如果有缺省值,缺省值只写在声明里,并且定义时要明确定义的是哪个类的函数。
.h文件中
#include <stdlib.h>
class Stack
{
public:
// 成员函数
void Init();
void Push(int x = 0);
int Top();
private:
// 成员变量
int* _a;
int _top;
int _capacity;
};
.cpp文件中
#include "Satck.h"
void Stack::Init()
{
_a = nullptr;
_top = _capacity = 0;
}
void Stack::Push(int x)
{
if (_top == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
_a = (int*)realloc(_a, sizeof(int) * newcapacity);
_capacity = newcapacity;
}
_a[_top++] = x;
}
int Stack::Top()
{
return _a[_top - 1];
}
d.类的存储
同样类的存储遵循内存对齐原则,并且他只计算成员变量的大小,例如上面的栈类的大小就是十二个字节,成员函数不存储在类中。
注意:空类或者类中只有成员函数所占的字节数是1
e.this指针
在上面我们使用Push函数时可能有人会想到为什么函数在执行的时候,他就能够把数据放在对象st中呢?,他咋知道这个就是st里的成员呢?
其实在每个成员函数里隐藏都存在一个this指针,这个指针指向调用函数的对象,类型是(类型*)。
例如上面的Push代码:
void Stack::Push(int x)
{
if (this->_top == _capacity)
{
size_t newcapacity = this->_capacity == 0 ? 4 : this->_capacity * 2;
this->_a = (int*)realloc(_a, sizeof(int) * newcapacity);
this->_capacity = newcapacity;
}
_a[_top++] = x;
}
成员变量都可以用this访问,且this指针必定是成员函数第一个形参。
2.类中的默认成员函数
在c++中当我们,写好一个类后就会生成六个默认的成员函数
a.构造函数
构造函数的函数名与类型相同,无返回值,
Stack(DataType capacity = 4)
{
if (capacity == 0)
{
_a = nullptr;
_top = _capacity = 0;
}
else
{
_a = (DataType*)malloc(sizeof(DataType) * capacity);
_capacity = capacity;
_top = 0;
}
}
// 成员函数
//void Init()
//{
// _a = nullptr;
// _top = _capacity = 0;
//}
这样就不需要再写Init函数了。
既然那么编译器自动生成的构造函数会做什么事情呢?
我们不写默认生成的构造函数,对对象的成员变量的内置类型(int, double等)不做处理,对自定义类型会调用那个自定义类型的默认构造函数。这样也兼容了C语言开辟好一个对象后里面的值是随机值。
而其中默认构造函数有三种:编译器自动生成的,无参的构造函数,全缺省的构造函数
默认构造函数:不传参就可以调用。
我们在使用中:
int main()
{
Stack st1(4);
Stack st2();
Stack st3;
return 0;
}
一般情况下,我们都需要写构造函数,来初始化对象,但如果类中的成员变量都是自定义类型可以不用写,前提是该自定义类型的构造函数是默认构造函数,否则无法完成,解决方法需要初始化列表的知识,会在后续介绍。
比如用两个栈实现队列,由于我们上面的栈的构造函数是全缺省,所以可以不用实现队列的构造函数
class Queue
{
public:
//....
private:
Stack _push;
Stack _pop;
};
int main()
{
Queue q1;
return 0;
它会自动初始化。
b.析构函数
析构函数是销毁对象的函数,它会在函数返回之后函数结束之前被调用(如果该函数中有局部类对象)函数名是~(波浪号,也有跟构造函数取反的意思)+类名 他也是对内置类型不做处理,对自定义类型会调用它的析构函数,且他的调用顺序,跟栈的原理相似,先创建的后销毁也与对象生命周期有关。
有了析构函数后,就会面临一个问题,在C语言中,我们传一个结构体参数的时候往往是传地址,传本身也不会有问题,只不过会占用更大的内存来建立栈帧。但是在c++里假如我们传一个栈对象的形参。加入执行如下代码:
typedef int DataType;
class Stack
{
public:
//构造函数
Stack(DataType capacity = 4)
{
if (capacity == 0)
{
_a = nullptr;
_top = _capacity = 0;
}
else
{
_a = (DataType*)malloc(sizeof(DataType) * capacity);
_capacity = capacity;
_top = 0;
}
}
//析构函数
~Stack()
{
free(_a);
_capacity = _top = 0;
}
// 成员函数
//void Init()
//{
// _a = nullptr;
// _top = _capacity = 0;
//}
void Push(DataType x = 0)
{
if (_top == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
_a = (DataType*)realloc(_a, sizeof(DataType) * newcapacity);
_capacity = newcapacity;
}
_a[_top++] = x;
}
int Top()
{
return _a[_top - 1];
}
private:
// 成员变量
int* _a;
int _top;
int _capacity;
};
void func(Stack st)
{
cout << "void func(Stack st)" << endl;
}
int main()
{
Stack st1(4);
func(st1);
return 0;
}
他就会崩溃,原因是对同一块内存的连续释放。在创建st1时用了构造函数,数组_a开辟了内存,进入函数func中创建形参,形参是实参的一份临时拷贝,所以st是值拷贝st1过后的对象两者_a存储的地址相同,_top,_capacity的值也相同,而在结束函数的时候st调用析构函数将_a指向的内存释放,回到主函数后,主函数结束时st1也需要析构,从而对开辟的空间进行了连续的释放,导致程序崩溃。所以我们应该传他的指针或者引用,但是语法结构上引用不占用内存所以优先引用:
void func(Stack& st)
{
cout << "void func(Stack st)" << endl;
}
int main()
{
Stack st1(4);
func(st1);
return 0;
}
由于析构函数的存在,我们要格外注意传参格式。如果自定义类型创建对象没有开辟内存的
话,就不用自己写了,否则就需要自己写将对象内存释放。
c.拷贝构造
在创建对象的时候我们可能会把某个对象的内容复制到当前对象上:
int main()
{
int i = 0;
for (i = 0; i < 10; i++)
{
//j的初始化
for (int j = i; j < 10 - i + 1; j++)
{
//...
}
}
return 0;
}
又或者在面临上述的func函数中,两个对象中的_a指向同一块内存的问题,体现出自定义类型简单的值拷贝(浅拷贝)是无法完成的,这时候就需要这个函数–拷贝构造函数:他也是一个构造函数,也是一个默认成员函数,函数名是类名参数是const的类的引用参数
默认拷贝构造:
int main()
{
Stack st1(4);
//这两个都是拷贝构造,对象的初始化
Stack st2(st1);
Stack st3 = st1;
return 0;
}
可以看到这里三个对象的_a指向同一块地址,在后续main函数结束时析构三次程序肯定会崩溃,这不合理,于是需要我们自己写拷贝构造函数,来解决这一情况。
typedef int DataType;
class Stack
{
public:
//构造函数
Stack(DataType capacity = 4)
{
if (capacity == 0)
{
_a = nullptr;
_top = _capacity = 0;
}
else
{
_a = (DataType*)malloc(sizeof(DataType) * capacity);
_capacity = capacity;
_top = 0;
}
}
//拷贝构造
Stack(const Stack& st)
{
_a = (DataType*)malloc(sizeof(DataType) * st._capacity);
memcpy(_a, st._a, sizeof(DataType) * st._capacity);
_top = st._top;
_capacity = st._capacity;
}
//析构函数
~Stack()
{
free(_a);
_capacity = _top = 0;
}
void Push(DataType x)
{
if (_top == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
_a = (DataType*)realloc(_a, sizeof(DataType) * newcapacity);
_capacity = newcapacity;
}
_a[_top++] = x;
}
int Top()
{
return _a[_top - 1];
}
private:
// 成员变量
int* _a;
int _top;
int _capacity;
};
再运行那段代码:
可以发现现在完全实现了真正的拷贝—深拷贝。程序正常运行。
默认拷贝构造:对内置类型值拷贝,自定义类型调用它的拷贝构造。
d.赋值重载
在说赋值重载前,先说运算符重载,在c++中面对普通类型指针维护的一块整型数组中我们可以直接使用下标访问符用下标访问。
但是顺序表呢?(具体细节不实现)
class Seqlist
{
public:
Seqlist(DataType capacity = 4)
{
if (capacity == 0)
{
_a = nullptr;
_top = _capacity = 0;
}
else
{
_a = (DataType*)malloc(sizeof(DataType) * capacity);
_capacity = capacity;
_top = 0;
}
}
~Seqlist()
{
free(_a);
_capacity = _top = 0;
}
void Push(DataType x)
{
if (_top == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
_a = (DataType*)realloc(_a, sizeof(DataType) * newcapacity);
_capacity = newcapacity;
}
_a[_top++] = x;
}
//返回后返回对象存在可以使用引用返回
int& SLIndexDate(int pos)
{
assert(pos > 0 && pos < _top);
return _a[pos];
}
private:
DataType* _a;
int _capacity;
int _top;
};
我们只能通过这样的方式来获取顺序表中的数据:
int main()
{
Seqlist sl(4);
sl.Push(1);
sl.Push(2);
sl.Push(3);
sl.Push(4);
cout << sl.SLIndexDate(2) << endl;
return 0;
}
运算符重载需要一个关键字operator:
class Seqlist
{
public:
Seqlist(DataType capacity = 4)
{
if (capacity == 0)
{
_a = nullptr;
_top = _capacity = 0;
}
else
{
_a = (DataType*)malloc(sizeof(DataType) * capacity);
_capacity = capacity;
_top = 0;
}
}
~Seqlist()
{
free(_a);
_capacity = _top = 0;
}
void Push(DataType x)
{
if (_top == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
_a = (DataType*)realloc(_a, sizeof(DataType) * newcapacity);
_capacity = newcapacity;
}
_a[_top++] = x;
}
int& SLIndexDate(int pos)
{
assert(pos > 0 && pos < _top);
return _a[pos];
}
//运算符重载
int& operator[](int pos)
{
assert(pos > 0 && pos < _top);
return _a[pos];
}
private:
DataType* _a;
int _capacity;
int _top;
};
这时候可以这样:
int main()
{
Seqlist sl(4);
sl.Push(1);
sl.Push(2);
sl.Push(3);
sl.Push(4);
cout << sl[2] << endl;
return 0;
}
就可以实现数组一样的效果。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其
返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator + 操作符(参数列表)
注意:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this
.* :: sizeof ?: . 注意以上5个运算符不能重载。
后置加加中要有一个不接收的int参数与前置加加构成重载
这时候我们在来探讨赋值重载
赋值重载是在我们赋值的时候需要的,编译器默认的赋值重载只会浅拷贝,当有内存开辟时,需要我们自己写:
typedef int DataType;
class Stack
{
public:
//构造函数
Stack(DataType capacity = 4)
{
if (capacity == 0)
{
_a = nullptr;
_top = _capacity = 0;
}
else
{
_a = (DataType*)malloc(sizeof(DataType) * capacity);
_capacity = capacity;
_top = 0;
}
}
//拷贝构造
Stack(const Stack& st)
{
_a = (DataType*)malloc(sizeof(DataType) * st._capacity);
memcpy(_a, st._a, sizeof(DataType) * st._capacity);
_top = st._top;
_capacity = st._capacity;
}
//析构函数
~Stack()
{
free(_a);
_capacity = _top = 0;
}
Stack& operator=(const Stack& st)
{
_a = (DataType*)malloc(sizeof(DataType) * st._capacity);
memcpy(_a, st._a, sizeof(DataType) * st._capacity);
_top = st._top;
_capacity = st._capacity;
return *this;
}
void Push(DataType x)
{
if (_top == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
_a = (DataType*)realloc(_a, sizeof(DataType) * newcapacity);
_capacity = newcapacity;
}
_a[_top++] = x;
}
int Top()
{
return _a[_top - 1];
}
private:
// 成员变量
int* _a;
int _top;
int _capacity;
};
int main()
{
//构造
Stack st1(4);
st1.Push(1);
st1.Push(2);
st1.Push(3);
st1.Push(4);
//拷贝构造
Stack st2(st1);
Stack st3 = st1;
Stack st4;
//已经初始化,所以现在是赋值
st4 = st1;
Stack st5;
//赋值重载返回对应类的引用面对以下环境,连等
st5 = st4 = st3;
return 0;
}
编译器默认的赋值重载只会浅拷贝。
e.取地址重载
这个函数很简单,用于返回对象的指针。默认生成的大概逻辑如下:
typedef int DataType;
class Stack
{
public:
//构造函数
Stack(DataType capacity = 4)
{
if (capacity == 0)
{
_a = nullptr;
_top = _capacity = 0;
}
else
{
_a = (DataType*)malloc(sizeof(DataType) * capacity);
_capacity = capacity;
_top = 0;
}
}
//拷贝构造
Stack(const Stack& st)
{
_a = (DataType*)malloc(sizeof(DataType) * st._capacity);
memcpy(_a, st._a, sizeof(DataType) * st._capacity);
_top = st._top;
_capacity = st._capacity;
}
//析构函数
~Stack()
{
free(_a);
_capacity = _top = 0;
}
//赋值重载
Stack& operator=(const Stack& st)
{
_a = (DataType*)malloc(sizeof(DataType) * st._capacity);
memcpy(_a, st._a, sizeof(DataType) * st._capacity);
_top = st._top;
_capacity = st._capacity;
return *this;
}
//取地址重载
Stack* operator&()//处理普通对象
{
return this;
}
const Stack* operator&()const//处理const对象,这个const构成重载
{
return this;
}
void Push(DataType x)
{
if (_top == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
_a = (DataType*)realloc(_a, sizeof(DataType) * newcapacity);
_capacity = newcapacity;
}
_a[_top++] = x;
}
int Top()
{
return _a[_top - 1];
}
private:
// 成员变量
int* _a;
int _top;
int _capacity;
};
两个的原因是处理普通对象和const对象,当调用const对象时如果使用第一个取地址重载函数,权限的放大,会报错。所以要有const修饰函数,
const修饰函数
其实本质是修饰this指针-----const Stack* this,不能修改this指向的内容,const修饰的函数跟在函数后面就可以。
这就是c++类和对象目前的认识,要是有不对或者不足的地方,请指正。