类和对象
C语言: 各种各样的函数的定义 或者是 struct
C++:类 => 实体的抽象类型
实体(属性、行为) -> ADT(abstract data type)//抽象的数据类型
| |
对象(占内存空间)<-进行实例化 类(属性->写成成员变量 行为->写成成员方法)
对象代表的是现实中的实体。
OOP语言的四大特征是什么?
抽象 封装/隐藏 继承 多态
我们现在用类来描述一个商品实体:
封装是通过:
访问限定符:public公有的 private私有的 protected保护的
在C++中用class定义类。
在C中用struct定义结构体。
但是struct也可以定义类。
属性一般都是私有的,在类的外部是不能访问的,私有的成员变量对外是隐藏,封装的,我们通过给外部提供公有的方法来访问私有的属性。
成员方法一定要依赖于对象,才可以调用。
我们要添加名字空间作用域:
对象的内存大小 =》只和成员变量有关,和成员方法没有关系的。
先找占用内存最长的成员变量,以它为内存字节对齐的方式,然后计算总的内存的大小。按double的8字节对齐。
cd 02C++面向对象.cpp
/d1reportSingleClassLayoutCGoods
CGoods可以定义无数的对象,每一个对象都有自己的成员变量
但是它们共享一套成员方法
show() => 怎么知道处理哪个对象的信息?
init(name, price, amount) => 怎么知道把信息初始化给哪一个对象的呢?
类的成员方法一经编译,所有的方法参数,都会加一个this指针,接收
调用该方法的对象的地址
const int NAME_LEN = 20;//长度初始化成常量
class CGoods// => 商品的抽象数据类型 类型是不占用内存空间的,类型定义的对象才占内存,在栈上
{
public://给外部提供公有的成员方法,来访问私有的属性
//做商品数据初始化用的
void init(const char *name, double price, int amount);
//打印商品信息
void show();
//给成员变量提供一个getXXX或setXXX的方法
//类体内实现的方法,自动处理成inline内联函数
//set 设置
void setName(char *name) { strcpy(_name, name); }
void setPrice(double price) { _price = price; }
void setAmount(int amount) { _amount = amount; }
//get 获取
const char* getName() { return _name; }//加上const,让外部调用不能修改其值
double getPrice() { return _price; }
int getAmount() { return _amount; }
private://属性一般都是私有的成员变量
char _name[NAME_LEN];//名称
double _price;//单价
int _amount;//数量
};
void CGoods::init(const char *name, double price, int amount)//类外定义,在方法名字的前面要加上类的作用域
//如果在类外定义,要定义成内联函数,得自己加上inline关键字 inline void CGoods::init
{
strcpy(this->_name, name);
this->_price = price;
this->_amount = amount;
}
void CGoods::show()//类外定义,在方法名字的前面要加上类的作用域
{
cout << "name:" << this->_name << endl;
cout << "price:" << this->_price << endl;
cout << "amount:" << this->_amount << endl;
}
int main()
{
/*
CGoods可以定义无数的对象,每一个对象都有自己的成员变量
但是它们共享一套成员方法
show() => 怎么知道处理哪个对象的信息?
init(name, price, amount) => 怎么知道把信息初始化给哪一个对象的呢?
类的成员方法一经编译,所有的方法参数,都会加一个this指针,接收
调用该方法的对象的地址
*/
//cl 02C++面向对象.cpp /d1reportSingleClassLayoutCGoods
CGoods good1;//类实例化了一个对象,代表实体
//init(&good1, "面包", 10.0, 200)
good1.init("面包", 10.0, 200);
//show(&good1)
good1.show();
good1.setPrice(20.5);
good1.setAmount(100);
good1.show();
CGoods good2;//类实例化了一个对象
good2.init("空调", 10000.0, 50);
good2.show();
return 0;
}
编译器会自动给成员方法加上this指针,用户自己不加this,编译器也会自动加上去。
构造函数和析构函数
OOP实现一个顺序栈
构造函数和析构函数:
函数的名字和类名一样
没有返回值
定义了对象,自动调用构造函数,出作用域自动调用析构函数
代替了对象成员变量的初始化操作和对象出作用域之前把资源释放掉。
栈上的对象 先构造的后析构,后构造的先析构(相当于是入栈出栈的过程)
class SeqStack
{
public:
//构造函数 SeqStack s1; SeqStack s2(20); 开辟资源
SeqStack(int size = 10)//是可以带参数的,因此可以提供多个构造函数,叫做构造函数的重载
{
cout << this << " SeqStack()" << endl;
_pstack = new int[size];
_top = -1;
_size = size;
}//构造函数调用完之后,成员变量都进行合法的初始化了,对象就产生了(逻辑概念)
//析构函数 释放资源
~SeqStack()//析构函数是不带参数的,所有每个类析构函数只能有一个!!!
{
cout << this << " ~SeqStack()" << endl;
delete[]_pstack;
_pstack = nullptr;
}//调用完析构函数,我们说对象不存在了
void push(int val)//入栈
{
if (full())
resize();
_pstack[++_top] = val;
}
void pop()//出栈
{
if (empty())
return;
--_top;
}
int top()//获取栈顶元素值
{
return _pstack[_top];
}
bool empty() { return _top == -1; }//判空
bool full() { return _top == _size - 1; }//判满
private:
int *_pstack;//动态开辟数组,存储顺序栈的元素
int _top;//指向栈顶元素的位置
int _size;//数组扩容的总大小
void resize()//扩容操作,不想用户调用它,写出私有的
{
int *ptmp = new int[_size * 2];
for (int i = 0; i < _size; ++i)
{
ptmp[i] = _pstack[i];
}//为什么不用memcpy(ptmp, _pstack, sizeof(int)*_size); 或者realloc?
//因为这些涉及的是内存拷贝 ,在对象里面不适合
delete[]_pstack;
_pstack = ptmp;
_size *= 2;
}
};
SeqStack gs;//全局的对象,定义的时候构造,等程序结束的时候才析构,在数据段上
int main()
{
SeqStack *ps = new SeqStack(60);
//new的操作:malloc在堆上内存开辟+SeqStack(60)对象构造
ps->push(70);
ps->push(80);
ps->pop();
cout << ps->top() << endl;
delete ps;
//堆上的对象要我们手动释放,先调用ps->~SeqStack()+然后free(ps) 这是delete和free的区别
//定义一个对象有两件事情:1.开辟内存 2.调用构造函数
SeqStack s;//调用默认的无参的构造函数
//s.init(5); //对象成员变量的初始化操作
for (int i = 0; i < 15; ++i)
{
s.push(rand() % 100);
}
while (!s.empty())
{
cout << s.top() << " ";
s.pop();
}
//s.release();//释放对象成员变量占用的外部堆内存(外部资源)
SeqStack s1(50);//也可以这样定义栈,初始化为50,因为有整型参数的构造函数,出main函数自动析构
s1.~SeqStack();//析构函数可以自己调用,因为对象还在,但是调用以后,我们就说对象不存在了,但是内存还在
s1.push(30);//析构函数调用完之后,编译没有问题,但是这样做,就是堆内存的非法访问了!!!
//不建议自己去调用析构函数
return 0;
}
构造函数:
定义对象时,自动调用的;可以重载的;构造完成,对象产生了
析构函数:
不带参数,不能重载,只有一个析构函数;析构完成,对象就不存在了
.data上的对象 定义的时候构造,程序结束时析构
heap上的对象 new的时候构造 delete的时候析构
stack上的对象 进入函数到它定义的地方构造,出函数作用域析构
对象的浅拷贝和深拷贝
this指针 =》 一个类-》很多对象 独自的成员变量 共享一套成员方法
成员方法,一经编译,方法的参数都会添加一个this指针,接收调用该方法的对象
浅拷贝
class SeqStack
{
public:
//构造函数
SeqStack(int size = 10)
{
cout << this << " SeqStack()" << endl;
_pstack = new int[size];
_top = -1;
_size = size;
}
//析构函数
~SeqStack()
{
cout << this << " ~SeqStack()" << endl;
delete[]_pstack;
_pstack = nullptr;
}
void push(int val)
{
if (full())
resize();
_pstack[++_top] = val;
}
void pop()
{
if (empty())
return;
--_top;
}
int top()
{
return _pstack[_top];
}
bool empty() { return _top == -1; }
bool full() { return _top == _size - 1; }
private:
int *_pstack;//动态开辟数组,存储顺序栈的元素
int _top;//指向栈顶元素的位置
int _size;//数组扩容的总大小
void resize()
{
int *ptmp = new int[_size * 2];
for (int i = 0; i < _size; ++i)
{
ptmp[i] = _pstack[i];
} //memcpy(ptmp, _pstack, sizeof(int)*_size); realloc
delete[]_pstack;
_pstack = ptmp;
_size *= 2;
}
};
int main()
{
SeqStack s;//没有提供任何构造函数的时候,会为你生成默认构造和默认析构,是空函数
//当你提供一个构造函数的时候,就不会提供默认的构造了
SeqStack s1(10);
SeqStack s2 = s1;// #1 如果我们没有提供拷贝构造,调用的是默认的拷贝构造函数-》是做直接内存的数据拷贝
//SeqStack s3(s1);// #2 #1和#2这两种写法是一样的
return 0;
}
在调用第二个析构函数的时候崩溃了
系统提供的默认的=函数做的是内存的拷贝—浅拷贝
s1先生成,s2后生成。s2析构时候,把中间那块资源释放掉了,并且把自己的指针置为nullptr。
s1的指针还是指向中间那块内存,但是中间那块内存已经被释放了,所以s1的这个指针已经成为了野指针。因此,s2析构完到s1析构的时候,成了释放野指针的操作了,程序崩溃
对象有成员变量指针,指向了对象内存之外的外部的资源,发生浅拷贝,两个对象的指针就指向了同一个资源,析构的时候造成程序崩溃,同一个资源释放了2次。
默认的拷贝构造函数如下:(浅拷贝)
深拷贝
我们要自定义拷贝构造函数(深拷贝),因为对象的浅拷贝有问题。
//自定以拷贝构造函数 =》深拷贝
SeqStack(const SeqStack& src)
{
cout << "SeqStack(const SeqStack &src)" << endl;
_pstack = new int[src._size];
for (int i = 0; i <= src._top; ++i)
{
_pstack[i] = src._pstack[i];
}
_top = src._top;
_size = src._size;
}
这样,对象就是各自析构自己的外部资源了
扩容的时候为什么要用for循环?
因为在进行数据的拷贝的时候,我们把内存上的数据拷贝到另一块内存上,如果内存上放的是整型数据,每一个整型不占用整型之外的资源,我们使用memcpy拷贝字节过来,是没有问题的。但是,我们假设这个数组里面放的不是整型,而是对象,每一个对象都有指针,指向外部的资源,则这个数组里的对象的浅拷贝是有问题的,用memcpy只是把对象本身的内存拷贝一份,调用realloc也是如此,造成指针指向的都是同一块外部资源,在析构的时候,这个外部资源要析构2次,造成程序崩溃。
除非明确拷贝的数据,没有占用外部资源,则可以使用memcpy方法或者realloc,否则我们一定要用for循环去解决问题。
自定义赋值函数
int main()
{
SeqStack s1(10);
SeqStack s2 = s1;
//s2.operator=(s1)
//void operator=(const SeqStack &src)
s2 = s1;//默认的赋值函数 =》 做直接的内存拷贝
return 0;
}
赋值操作,s1,s2都是存在的对象。
我们现在没有给类提供赋值操作,系统产生默认的赋值函数,也是做直接的内存拷贝!会在第二次析构的时候出现问题!
又是浅拷贝,而且还把s2原本的资源给丢了
我们得把s2原本指向的内存释放掉,然后根据s1的尺寸重新开辟一个堆内存,自己的指针指向自己的外部资源。
//自定义赋值重载函数 s1 = s1;
void operator=(const SeqStack& src)
{
cout << "operator=" << endl;
//防止自赋值
if (this == &src)
return;
//需要先释放当前对象占用的外部资源
delete[]_pstack;
_pstack = new int[src._size];
for (int i = 0; i <= src._top; ++i)
{
_pstack[i] = src._pstack[i];
}
_top = src._top;
_size = src._size;
}
String类型代码应用
这个类的对象的成员变量有占用外部资源,所以我们要重写它的拷贝构造函数和赋值函数
处理连续赋值
如果是这样写,则达不到连续赋值,因为返回的void不能给str3赋值。
//使用String&是为了支持连续的operator=赋值操作
void operator=(const String& other)//赋值重载函数,深拷贝
{
if (this == &other)//防止自赋值
{
//return *this;//str1
}
delete[]m_data;//释放当前的外部资源
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
//return *this;//str1
}
str2给str1赋值后,要返回str1对象本身,才能给str3赋值。
//使用String&是为了支持连续的operator=赋值操作
string& operator=(const String& other)//赋值重载函数,深拷贝
{
if (this == &other)//防止自赋值
{
return *this;//str1
}
delete[]m_data;//释放当前的外部资源
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
return *this;//str1
}
完整代码如下
class String
{
public:
String(const char *str = nullptr)//普通构造函数
{
if (str != nullptr)
{
m_data = new char[strlen(str) + 1];//'\0'要算上
strcpy(this->m_data, str);
}
else//用户传进来的字符串是空的话 ,为了让其他方法不用判空
{
m_data = new char[1];//new char;开辟1个字节大小的空间
*m_data = '\0';//0
}
}
String(const String &other)//拷贝构造函数,深拷贝
{
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
}
~String(void)//析构函数
{
delete[]m_data;
m_data = nullptr;//防止野指针的出现
}
//使用String&是为了支持连续的operator=赋值操作
string& operator=(const String &other)//赋值重载函数,深拷贝
{
if (this == &other)//防止自赋值
{
return *this;//str1
}
delete[]m_data;//释放当前的外部资源
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
return *this;//str1
}
private:
char *m_data;//用于保存字符串
};
int main()
{
//用带const char*参数的构造函数
String str1;//调用默认的构造,形参是nullptr
String str2("hello");
String str3 = "world";//str2
//调用拷贝构造函数
String str4 = str3;
String str5(str3);
//调用赋值重载函数
/*
str1 = str2
str1.operator=(str2) => str1
str3 = str1
*/
str3 = str1 = str2;
return 0;
}
循环队列
初始化的时候,两个指针都指向队头
随着元素的添加,rear指针后移
如果到了下面这种情况,添加元素满了
然后现在有元素出队,从队头出,front指针也要后移
如果是用顺序表实现队列,那么现在,不能入队了,因为已经满了,前面有元素出队,但是没办法放。
rear++的时候,我们不要让他加到末尾,让他加到前面去
包括front也一样,把78出队后,回到队前面。
所以,我们这么做。
//循环队列 memcpy realloc
class Queue
{
public:
Queue(int size = 5)//构造函数
{
_pQue = new int[size];
_front = _rear = 0;
_size = size;
}
//Queue(const Queue&) = delete;
//Queue& operator=(const Queue&) = delete;
Queue(const Queue &src)//拷贝构造函数
{
_size = src._size;
_front = src._front;
_rear = src._rear;
_pQue = new int[_size];
for (int i = _front;
i != _rear;
i = (i + 1) % _size)
{
_pQue[i] = src._pQue[i];
}
}
Queue& operator=(const Queue &src)//赋值函数
{
if (this == &src)
return *this;
delete[]_pQue;
_size = src._size;
_front = src._front;
_rear = src._rear;
_pQue = new int[_size];
for (int i = _front;
i != _rear;
i = (i + 1) % _size)
{
_pQue[i] = src._pQue[i];
}
return *this;
}
~Queue()//析构函数
{
delete[]_pQue;
_pQue = nullptr;
}
void push(int val)//入队操作,队尾入
{
if (full())
resize();
_pQue[_rear] = val;//队尾赋值
_rear = (_rear + 1) % _size;//因为是循环队列
}
void pop()//出队操作
{
if (empty())
return;
_front = (_front + 1) % _size;
}
int front()//获取队头元素
{
return _pQue[_front];
}
bool full() { return (_rear + 1) % _size == _front; }//判满
bool empty() { return _front == _rear; }//判空
private:
int *_pQue;//申请队列的数组空间,方便扩容
int _front;//指示队头的位置
int _rear;//指示队尾的位置
int _size;//队列扩容的总大小
void resize()//扩容操作
{
int *ptmp = new int[2 * _size];
int index = 0;
for (int i = _front;
i != _rear;
i = (i + 1) % _size)
{
ptmp[index++] = _pQue[i];
}
delete[]_pQue;
_pQue = ptmp;
_front = 0;
_rear = index;
_size *= 2;
}
};
int main()
{
Queue queue;
for (int i = 0; i < 20; ++i)
{
queue.push(rand() % 100);
}
while (!queue.empty())
{
cout << queue.front() << " ";
queue.pop();
}
cout << endl;
Queue queue1 = queue;
queue1 = queue;
return 0;
}