运算符重载

1.复数类

运算符重载目的:使对象运算表现得和编译器内置类型一样;

复数类例子

#include<iostream>

using namespace std;

class CComplex{
public:
CComplex(int r = 0, int l = 0): mreal(r), mimage(l) {}
void show() {
cout << "实部:" << mreal << "虚部:" << mimage << endl;
}
CComplex operator+(const CComplex &temp) { // 一个参数 相当于xxx.operator+(temp);
return CComplex(mreal + temp.mreal, mimage + temp.mimage);//对象优化,减少生成一个对象的构造和析构过程;
}
CComplex& operator++() {//先加,前置的
mreal++;
mimage++;
return *this;
}
CComplex operator++(int) {//后加,后置的
return CComplex(mreal++, mimage++);
}
void operator+=(const CComplex &temp) {
mreal += temp.mreal;
mimage += temp.mimage;
}
private:
int mreal;
int mimage;
friend CComplex operator+(const CComplex &other, const CComplex &temp);
friend ostream& operator<<(ostream &out, const CComplex &temp);
friend istream& operator>>(istream &in, CComplex &temp); };
CComplex operator+(const CComplex &other, const CComplex &temp) { //两个参数, ::operator+(other, temp);
return CComplex(other.mreal + temp.mreal, other.mimage + temp.mimage);
}
ostream& operator<<(ostream &out, const CComplex &temp) { //全局函数好
out << temp.mreal << " " << temp.mimage << endl;
return out;
}
istream& operator>>(istream &in, CComplex &temp) { //设置成全局函数好,不能设置为const,因为需要改变
in >> temp.mreal >> temp.mimage;
return in;
}
int main() {
CComplex comp1(10, 20);
CComplex comp2(20, 30);
CComplex comp3 = comp1 + comp2; //如果调用类方法:comp1.operator+(comp2); 如果调用全局方法:::operator+(comp1, comp2);优先调用全局方法
comp3.show();
CComplex comp4 = comp3 + 20;//调用全局方法和类方法都可以,因为知道comp3是什么类,然后会将20转换成对应的类;
comp4 = 20 + comp3;//只能通过调用全局方法,调用类方法是不成功的,因为20不知道是什么类;
comp4.show();
++comp4;
comp4.show();
CComplex comp5 = comp4++;;
comp5.show();
cout << comp5 << comp4 << endl;
return 0;
}

2.实现string类

#include<iostream>
#include<string> using namespace std; class String {
public:
String(const char* str = nullptr) {
if (str != nullptr) {
len = strlen(str);
_data = new char[len + 1];
strcpy(_data, str); }
else {
len = 0;
_data = new char[1];
_data[0] = '\0';
}
}
~String() {
delete[]_data;
_data = nullptr;
}
String(const String& other) { //拷贝构造
_data = new char[other.len + 1];
strcpy(_data, other._data);
len = other.len;
}
String operator=(const String& other) { //赋值重载
if (*this == other) return *this; delete[]_data;
_data = new char[other.len + 1];
strcpy(_data, other._data);
return *this;
}
char operator[](const int index) { //可以改值
if (index >= len) throw "已经越界啦";
return _data[index];
}
const char operator[](const int index) const { //不可以改值
if (index >= len) throw "已经越界啦";
return _data[index];
}
size_t size() { return len; }
String operator+=(const String& str) {
len += str.len;
char* newstring = new char[len + 1];
strcpy(newstring, _data);
strcat(newstring, str._data);
delete[]_data;
_data = newstring;
return *this;
}
bool operator==(const String& str) {
return strcmp(str._data, _data) == 0;
}
private:
char* _data;
size_t len;
friend String operator+(const String& temp, const String& other);
friend ostream& operator<<(ostream& out, const String& other);
friend istream& operator>>(istream& in, String& other);
};
String operator+(const String& temp, const String& other) {
char* p = new char[temp.len + other.len + 1];
strcpy(p, temp._data);
strcat(p, other._data);//追加
String res(p); //res 也要new和delete
delete[]p;
return res;
}
ostream& operator<<(ostream& out, const String& other) { //输出
out << other._data;//不用加*,按地址输出全部
return out;
}
istream& operator>>(istream& in, String& other) { //输入,每次输出都标记为重新输入
char temp[1000];
in >> temp;
delete[]other._data;
other._data = new char[strlen(temp) + 1];
strcpy(other._data, temp);
return in;
}
int main() {
String test = "abc";
test += "d";
cout << test;
const char* p = "abc";
cout << *p << " " << p << endl;//cout 从地址不断输入数据,而*p读到的就是地址的值;
cin >> test;
cout << test << endl;
cout << test[2] << endl;
return 0;
}

存在问题:

在operator+的重载函数中,存在两次new和两次delete,浪费内存和时间,可以优化一下:

String operator+(const String& temp, const String& other) {
//char* p = new char[temp.len + other.len + 1];
String res;
res._data = new char[temp.len + other.len + 1];
strcpy(res._data, temp._data);
strcat(res._data, other._data);
return res; //只有一次的new和delete
}

3.实现string字符串对象的迭代器iterator

为什么需要迭代器:因为数据往往存在类的成员属性中,比如上述的String类的char *_data,如果我想一个个访问数据,我需要也用一个char* 的指针指向—_data,而事实是我们不可以访问对象的private属性!所以需要迭代器来实现;

迭代器:要访问顺序容器和关联容器中的元素,需要通过"迭代器(iterator)"进行,提供一种统一的方式,来透明地遍历容器

迭代器是一个变量,相当于容器和操纵容器的算法之间的中介。迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素。从这一点上看,迭代器和指针类似。

迭代器有正向迭代器(容器类名::iterator 迭代器名;)、常量正向迭代器(容器类名::const_iterator 迭代器名;)、反向迭代器(容器类名::reverse_iterator 迭代器名;)、常量反向迭代器(容器类名::const_reverse_iterator 迭代器名;)

遍历例子:

string str1 = "hello world!";//str1叫容器吗?,可以,因为str1其底层放了一组char
//iterator即容器的迭代器
string::iterator it = str1.begin();
for (; it!=str1.end(); ++it)
{
cout << *it << " ";
}
cout << endl;

说明:

泛型算法:比如sort等;泛型算法:给所有容器都可以使用,参数接受的都是容器的迭代器。

我们要知道:

  • 如上面所说,str1对象中存入了各种各样的字符,字符串类型底层的成员变量为私有的,我们无法看见。
  • 容器有一个begin()方法,begin()返回它底层的迭代器的表示,it迭代器指向容器的首元素位置。
  • 容器中还有一个end()方法,end()表示容器中最后一个元素的后继位置,循环中it!=end(),++it,将其遍历一遍。
  • 底层无论是数组还是链表什么的,如何从当前元素遍历到下一个元素,我们不需要操心;容器底层元素真真正正从一个元素跑到下一个元素,不同数据结构,不同差异都封装在迭代器++运算符重载函数中,我们一般采用前置++(后置++比前置++多一个临时对象的构造和析构过程)
  • 迭代器还需要提供 * 运算符重载,访问迭代器所迭代元素的值,迭代器解引用访问的就是容器底层数据。

String 迭代器的实现:

class String {
public:
..... //上面的代码
class iterator {
public:
iterator(char *p = nullptr):_p(p) {}
bool operator!=(const iterator& it) {
return _p != it._p;
}
void operator++() { //前置++,减少临时对象的构造;
++_p;
}
char& operator*() {
return *_p;
}
private:
char* _p;
};
iterator begin() { //不能采用引用,因为是临时对象
return iterator(_data);//构造临时迭代器,指向首元素;
}
iterator end() { //不能采用引用,因为是临时对象
return iterator(_data + len);//构造临时迭代器,指向末元素的后继位置;
}
private:
.....
};
int main() {
String test = "hello word";
//String::iterator it = test.begin();
auto it = test.begin(); //用自动推导遍历auto 代替 String::iterator
for (; it != test.end(); ++it) { //
cout << *it << " ";
}
cout << endl;
for (char ch : test) { // foreach内部也是用iterator实现,如果没有iterator会发生错误;
cout << ch << " ";
}
return 0;
}

4.实现vector容器的迭代器iterator

直接上手代码,注意多了解选择const,&,这些加和不加的问题;

template<typename T>
class vector {//和之前的模板vector一样
public:
....
class iterator {
public:
iterator( T *p): _p(p) {} //不可以加const,因为加了const的话会有_p(T*) = p(const T*),这样会类型转换错误; //相信一句话:如果不改变值,就加const,参数为const,可接受const和非const,函数为const,可以常对象和普通对象调用;
bool operator!=(const iterator &it)const {
return _p != it._p;
}
void operator++() {
++_p;
}
T& operator*() {
return *_p;
}
const T& operator*()const { //常对象也能调用,并且不可以改变值;
return *_p;
}
private:
T* _p;
};
iterator begin() {
return iterator(_first);
}
iterator end() {
return iterator(_end);
}
private:
.....
}
int main() {
vector<int> arr;
for (int i = 0; i < 20; i++) {
arr.push_back(i);
}
auto i = arr.begin();
*i = 100;//调用的是普通方法,非常方法!
for (; i != arr.end(); ++i) {
cout << *i << " ";
}
return 0;
}

5.容器的迭代器失效问题

①迭代器的失效问题:

对容器的操作影响了元素的存放位置,称为迭代器失效

②失效情况:

  • 当容器调用erase()方法后,当前位置到容器末尾元素的所有迭代器全部失效。
  • 当容器调用insert()方法后,当前位置到容器末尾元素的所有迭代器全部失效。
  • 如果容器扩容,在其他地方重新又开辟了一块内存。原来容器底层的内存上所保存的迭代器全都失效了。
  • 不同容器的迭代器,是不能进行比较运算的。

例子:程序:把vec容器中所有的偶数全部删掉;把vec容器中的偶数前驱位置插入 偶数值-1;

③迭代器失效的解决办法:对插入/删除点的迭代器进行更新操作。(新的迭代器才是有效的)

//对于删除操作
auto it = vec.begin();
while (it!=vec.end())
{
if (*it % 2 == 0)
{
it = vec.erase(it);//返回更新当前删除位置迭代器,并且不进行++操作,因为元素已经往前移动; }
else
{
++it;
}
} //给vec容器中所有的偶数前面添加一个小于偶数值1的数字
auto it = vec.begin();
for (; it!=vec.end(); ++it)
{
if (*it % 2 == 0)
{
it = vec.insert(it, *it-1);//更新当前增加位置迭代器
++it; // 这样一共移动两个位置
}
}

④迭代器失效原理:

我们尝试写一下底层迭代器的代码:

在vector类中加一个迭代器链表(保存每一个迭代器的信息);在迭代器中加多一个当前的vector指针(看看该迭代器属于谁);erase、insert操作都移动当前vector里数据的位置,扩容操作搬移vector中的数据到另一块新内存,将迭代器链表中信息对应不上的迭代器 失效掉;步骤如下:

  • 在iterator私有成员下添加一个指向当前对象的指针,让迭代器知道当前迭代的容器对象,不同容器之间不能相互比较

    vector<T, Alloc> *_pVec;
  • 增加一个结构体维护了一个链表。cur是指向某一个迭代器,迭代器中存有①该迭代器是哪个类对象②该迭代器指向哪个元素;又定义了一个指向下一个Iterator_Base节点的指针;外面定义了一个头节点_head,记录了用户申请的迭代器,记录在Iterator_Base链表中

  • verify检查有效性。在我们增加或删除后,把我们当前节点的地址到末尾的地址,全部进行检查,在存储的迭代器链表上进行遍历,哪一个迭代器指针指向的迭代器迭代元素的指针在检查范围内,就将相应迭代器指向容器的指针置为空,即为失效的迭代器
void verify(T *first, T *last)
{
Iterator_Base *pre = &this->_head;
Iterator_Base *it = this->_head._next;
while (it != nullptr)
{
if (it->_cur->_ptr > first && it->_cur->_ptr <= last)
{
//迭代器失效,把iterator持有的容器指针置nullptr
it->_cur->_pVec = nullptr;
//删除当前迭代器节点,继续判断后面的迭代器节点是否失效
pre->_next = it->_next;
delete it;
it = pre->_next;
}
else
{
pre = it;
it = it->_next;
}
}
}
  • !=运算符号重载前要检查迭代器的有效性,即两个迭代器比较之前检查是否失效,或者是否是同一类型容器的迭代器。 ++、*运算符也需要检验其有效性

    bool operator!=(const iterator &it)const
    {
    //检查迭代器的有效性
    if (_pVec == nullptr || _pVec != it._pVec)//迭代器为空或迭代两个不同容器
    {
    throw "iterator incompatable!";
    }
    return _ptr != it._ptr;
    }
    //++、*也一样;
  • pop_back中加入了verfiy, 自定义insert实现(未考虑扩容与ptr合法性,加入verify操作,还有往后移动数据),自定义erase实现(加入verify操作,还有往前移动数据);

void pop_back()//尾删
{
if (empty()) return;
verify(_last - 1, _last);
//不仅要把_last指针--,还需要析构删除的元素
--_last;
_allocator.destroy(_last);
}
//自定义vector容器insert方法实现
iterator insert(iterator it, const T &val)
{
//1.这里我们未考虑扩容
//2.还未考虑it._ptr指针合法性,假设它合法
verify(it._ptr - 1, _last);
T *p = _last;
while (p > it._ptr)
{
_allocator.construct(p, *(p-1));
_allocator.destroy(p - 1);
p--;
}
_allocator.construct(p, val);
_last++;
return iterator(this, p);
}
//自定义vector容器erase方法实现
iterator erase(iterator it)
{
verify(it._ptr - 1, _last);
T *p = it._ptr;
while (p < _last-1)
{
_allocator.destroy(p);
_allocator.construct(p, *(p+1));
p++;
}
_allocator.destroy(p);
_last--;
return iterator(this, it._ptr);
}

总结:vector会有维护迭代器的链表,在vector迭代器中,如果有插入和删除操作,那么与这些位置有关联的迭代器(比如在插入删除后面)会全部失效,因为会用verify函数将迭代器失效,失效后什么操作都会失败;

总代码:

#include<iostream>
#include<algorithm> using namespace std; //容器的空间配置器
template <typename T>
struct Allocator
{
T* allocate(size_t size)//只负责内存开辟
{
return (T*)malloc(sizeof(T) * size);
}
void deallocate(void* p)//只负责内存释放
{
free(p);
}
void construct(T* p, const T& val)//已经开辟好的内存上,负责对象构造
{
new (p) T(val);//定位new,指定内存上构造val,T(val)拷贝构造
}
void destroy(T* p)//只负责对象析构
{
p->~T();//~T()代表了T类型的析构函数
}
}; //####1
template <typename T, typename Alloc = Allocator<T>>
class vector//向量容器
{
public:
vector(int size = 10)//构造
{
_first = _allocator.allocate(size);
_last = _first;
_end = _first + size;
}
~vector()//析构
{
for (T* p = _first; p != _last; ++p)
{
_allocator.destroy(p);//把_first指针指向的数组的有效元素析构
}
_allocator.deallocate(_first);//释放堆上的数组内存
_first = _last = _end = nullptr;
}
vector(const vector<T>& rhs)//拷贝构造
{
int size = rhs._end - rhs._first;//空间大小
_first = _allocator.allocate(size);
int len = rhs._last - rhs._first;//有效元素
for (int i = 0; i < len; ++i)
{
_allocator.construct(_first + i, rhs._first[i]);
}
_last = _first + len;
_end = _first + size;
}
vector<T>& operator=(const vector<T>& rhs)//赋值运算符重载
{
if (this == &rhs)
{
return *this;
} for (T* p = _first; p != _last; ++p)
{
_allocator.destory(p);//把_first指针指向的数组的有效元素析构
}
_allocator.deallocate(_first);//释放堆上的数组内存 int size = rhs._end - rhs._first;//空间大小
_first = _allocator.allocate(size);
int len = rhs._last - rhs._first;//有效元素
for (int i = 0; i < len; ++i)
{
_allocator.construct(_first + i, rhs._first[i]);
}
_last = _first + len;
_end = _first + size;
return *this;
}
void push_back(const T& val)//尾插
{
if (full())
{
expand();
}
_allocator.construct(_last, val);//_last指针指向的内存构造一个值为val的对象
_last++;
}
void pop_back()//尾删
{
if (empty()) return;
verify(_last - 1, _last); //调用verify
//不仅要把_last指针--,还需要析构删除的元素
--_last;
_allocator.destroy(_last);
}
T back()const//返回容器末尾元素值
{
return *(_last - 1);
}
bool full()const
{
return _last == _end;
}
bool empty()const
{
return _first == _last;
}
int size()const//返回容器中元素个数
{
return _last - _first;
}
T& operator[](int index)
{
if (index < 0 || index >= size())
{
throw "OutOfRangeException";
}
return _first[index];
} //#####2
//迭代器一般实现成容器的嵌套类型
class iterator
{
public:
friend class vector <T, Alloc>;
//新生成当前容器某一个位置元素的迭代器
iterator(vector<T, Alloc>* pvec = nullptr
, T* ptr = nullptr)
:_ptr(ptr), _pVec(pvec)
{
Iterator_Base* itb = new Iterator_Base(this, _pVec->_head._next); //this指向当前迭代器(在上面类里面表示什么);_pVec->_head表示当前对象的迭代器链表头
_pVec->_head._next = itb;
}
bool operator!=(const iterator& it)const
{
//检查迭代器的有效性
if (_pVec == nullptr || _pVec != it._pVec)//迭代器为空或迭代两个不同容器
{
throw "iterator incompatable!";
}
return _ptr != it._ptr;
}
void operator++()
{
//检查迭代器有效性
if (_pVec == nullptr)
{
throw "iterator incalid!";
}
_ptr++; //只改变了当前迭代器指向的元素
}
T& operator*()
{
//检查迭代器有效性
if (_pVec == nullptr)
{
throw "iterator invalid!";
}
return *_ptr;
}
const T& operator*()const
{
if (_pVec == nullptr)
{
throw "iterator invalid!";
}
return *_ptr;
}
private:
T* _ptr;//当前迭代器是哪个容器对象,找到当前迭代器的目标值
vector<T, Alloc>* _pVec;//指向当前对象容器的指针
}; iterator begin()
{
return iterator(this, _first);
}
iterator end()
{
return iterator(this, _last);
}
//检查迭代器失效
void verify(T* first, T* last)
{
Iterator_Base* pre = &this->_head;
Iterator_Base* it = this->_head._next;
while (it != nullptr)
{
if (it->_cur->_ptr > first && it->_cur->_ptr <= last)
{
//迭代器失效,把iterator持有的容器指针置nullptr
it->_cur->_pVec = nullptr;
//删除当前迭代器节点,继续判断后面的迭代器节点是否失效
pre->_next = it->_next;
delete it;
it = pre->_next;
}
else
{
pre = it;
it = it->_next;
}
}
} //自定义vector容器insert方法实现
iterator insert(iterator it, const T& val)
{
//1.这里我们未考虑扩容
//2.还未考虑it._ptr指针合法性,假设它合法
verify(it._ptr - 1, _last); //调用verify
T* p = _last;
while (p > it._ptr)
{
_allocator.construct(p, *(p - 1));
_allocator.destroy(p - 1);
p--;
}
_allocator.construct(p, val);
_last++;
return iterator(this, p);
} //自定义vector容器erase方法实现
iterator erase(iterator it)
{
verify(it._ptr - 1, _last); //调用verify
T* p = it._ptr;
while (p < _last - 1)
{
_allocator.destroy(p);
_allocator.construct(p, *(p + 1));
p++;
}
_allocator.destroy(p);
_last--;
return iterator(this, it._ptr);
}
private:
T* _first;//起始数组位置
T* _last;//指向最后一个有效元素后继位置
T* _end;//指向数组空间的后继位置
Alloc _allocator;//定义容器的空间配置器对象 //容器迭代器失效增加代码
struct Iterator_Base
{
Iterator_Base(iterator* c = nullptr, Iterator_Base* n = nullptr)
:_cur(c), _next(n) {}
iterator* _cur;
Iterator_Base* _next;
};
Iterator_Base _head; //存迭代器的链表 void expand()//扩容
{
int size = _end - _first;
//T *ptmp = new T[2*size];
T* ptmp = _allocator.allocate(2 * size);
for (int i = 0; i < size; ++i)
{
_allocator.construct(ptmp + i, _first[i]);
//ptmp[i] = _first[i];
}
//delete[]_first;
for (T* p = _first; p != _last; ++p)
{
_allocator.destroy(p);
}
_allocator.deallocate(_first);
_first = ptmp;
_last = _first + size;
_end = _first + 2 * size;
}
}; int main() {
vector<int> vec;
for (int i = 0; i < 5; ++i)
{
vec.push_back(i);
} auto it3 = vec.end();
vec.pop_back(); // it3 失效
auto it2 = vec.end();
cout << (it3 != it2); auto it1 = vec.end();
while (it1 != vec.end()) { //因为这条语句有end(),会构造迭代器,加多了vec迭代器链表中的元素;但是每次生成的都是最后一个元素的迭代器;
++it1;
}
vec.pop_back();//verify(_last-1, _last)
auto it2 = vec.end();
cout << (it1 != it2) << endl;
return 0;
}

6.深入理解new和delete的原理

new与delete实现原理进行剖析

malloc和new:

  • malloc按字节开辟内存的;new开辟内存时需要指定类型;
  • malloc开辟内存返回的都是void * ,new相当于运算符重载函数,返回值自动转为指定的类型的指针。
  • malloc只负责开辟内存空间,new不仅仅也有malloc功能,还可以进行数据的初始化(使用构造函数)。
  • malloc开辟内存失败返回nullptr指针;new抛出的是bad_alloc类型的异常。
  • malloc开辟单个元素内存与数组内存 的内存计算是一样的,都是给所有元素所需要的字节数(比如一个int 4个字节,int arr[2] 8个字节);new开辟时,如果元素是编译器内置类型,则和malloc一样(malloc需要自己算,new不用);但是对于类对象,对单个元素内存后面不需要[],内存大小也和元素所需要的字节数一样(一个 int 4个字节);数组需要加上[]并给上元素个数,字节数则是是 所有元素所需字节数 + 4个字节(记录有多少个元素); 可以看最后的解析

delete和free

  • free不管释放单个元素内存还是数组内存,只需要传入内存的起始地址即可。
  • delete释放单个元素内存,不需要加中括号,但释放数据内存时需要加中括号。中括号:[]
  • free只有一步,就是释放内存;delete执行其实有两步,先调用析构,再释放内存

new与delete实现原理剖析

int* p = new int; delete p;

反汇编为:

我们发现,其实new和delete的本质是有对new和delete重载函数的调用new -> operator new delete -> operator delete

实现一下operator new和 operator delete:

//new:先调用operator开辟内存空间,然后调用对象的构造函数
void* operator new(size_t size)
{
void *p = malloc(size);
if (p == nullptr)
{
throw bad_alloc();
}
cout << "operator new addr:" << p <<endl;
return p;
} //operator new[]实现
void* operator new[](size_t size)
{
void *p = malloc(size);
if (p == nullptr)
{
throw bad_alloc();
}
cout << "operator new[] addr:" << p <<endl;
return p;
} //delete p:调用p指向对象的析构函数,再调用operator delete释放内存空间
void operator delete(void *ptr)
{
cout << "operator delete addr:" << ptr <<endl;
free(ptr);
}
//operator delete[]实现
void operator delete[](void *ptr)
{
cout << "operator delete[] addr:" << ptr <<endl;
free(ptr);
}

其中,bad_alloc()函数实际代码:

测试:以Test类为例子:

class Test {
public:
Test(int data = 1): ptr(new int(data)) { cout << "Test()" << endl; }
~Test() { cout << "~Test()" << endl; }
private:
int *ptr;
};
int main() {
Test* ptr = new Test;
delete ptr;
cout << endl;
Test *test = new Test[2];
delete[]test;
return 0;
}

③重要问题:new 和delete能够混用吗?

我们看看c++编译器内置类型:

以int为例子:

int *p = new int;
delete[]p; int *q = new int[10];
delete q;

是没有问题的!!!因为对于整型来说,没有构造函数与析构函数,针对于int类型,new与delete功能只剩下malloc与free功能,可以将其混用。

我们看看类 类型:

以上述的Test为例子:

Test *p1 = new Test();
delete[]p1; Test *p2 = new Test[5];
delete p2;

第一个会一直不断死循环,第二个会触发异常;

具体原因:

正常情况下,每一个test对象是只有一个整型指针成员变量,只占用四个字节的;而Test[5]这里分配了5个test对象。delete时先调用析构函数,this指针需要将正确的对象的地址传入析构函数中,加了[]表示有好几个对象,对数组中的每一个对象都要进行析构。但delete真正执行指令时,底层是malloc按字节开辟,并不知道是否开辟了5个test对象的数组,因此还要再多开辟一个4字节来存储对象的个数,假设它的地址是0x100;但是new完之后p2返回的地址是0x104地址,是第一个元素的地址。当我们执行delete[]时,会到前四个4字节来取一下对象的个数,将知道了是5个并将这块内存平均分为5份,将其每一份对象起始地址传给相应的析构函数,正常析构,最后将0x100开始的4字节也释放。

而上述中p2出错是给用户返回的第一个对象的起始地址,delete p2认为p2只是指向了一个对象,只将Test[0]对象析构,直接从0x104 free(p2),但底层实际是从0x100开辟的,因此崩溃存在异常;而p1出错则很明显:p1只是单个元素,从0x104开始开辟内存,但是delete[]p1,里面并没有那么多元素,最后还释放了4个字节的存储对象个数的内存(即从0x100释放)因此崩溃。

总结:1.对于普通的编译器内置类型new/delete[]混用是可以的;new[]/delete混用也是可以的。 2.自定义的类类型,有析构函数,为了调用正确的析构函数,那么开辟对象数组时会多开辟4个字节记录对象的个数,不能混用。

④扩展问题:C++中如何设计一个程序检查内存泄露问题?

核心是用new与delete运算符重载接管整个应用的内存管理,对内存开辟释放都要记录。 检查内存泄露,在全局中重写new与delete,new操作中用映射表记录一下都有哪些内存被开辟过了;delete时候,将相应的内存资源用内存地址标识,再删除掉。如果整个系统运行完发现映射表中有一些内存还没有被释放则存在内存泄露;

7.new和delete重载实现的对象池引用

**对象池: ** 在一部分内存空间(池子)中事先实例化好固定数量的对象,当需要使用池中的对象时,首先判断该池中是否有闲置(暂未使用)的对象,如果有,则取出使用,如果没有,则在池中创建该对象。当一个对象不再被使用时,其应该将其放回对象池,以便后来的程序使用,最后程序结束的时候,再统一释放;

为什么需要对象池:因为大量的push和pop操作,会不断申请和释放空间,影响性能;拿Queue为例子:

#include<iostream>

using namespace std;

template<typename T>
class Queue {
public:
Queue(){
_front = _rear = new QueueItem(); //提供一个空节点
}
~Queue() {
QueueItem* cur = _front;
while (cur != nullptr) {
_front = _front->next;
delete cur;
cur = _front;
}
}
void push(const T &val) { //队尾入队
QueueItem* item = new QueueItem(val);
_rear->next = item;
_rear = item;
}
void pop() { //队头出队, 不用返回值,记住队头为空
if (empty()) throw "队列为空";
QueueItem* item = _front->next;
_front->next = item->next;
if (_front->next == nullptr) { //说明只有一个元素;
_rear = _front;
}
delete item;
}
T front()const { //只读操作所以加const
if (empty()) throw "队列为空";
return _front->next->_data; //对象才有点操作,指针是箭头操作,有多个next因为有空的头节点
}
bool empty()const { return _rear == _front; } //只读操作 private:
struct QueueItem //想做一个对象池(10000个QueueItem的节点),不用大量new和delete,用到就里面拿,不用就归还,程序员来内存管理
{
QueueItem(T data = T()): _data(data), next(nullptr) {} //留意一下,这个T data = T()操作
T _data;
QueueItem* next;
};
QueueItem* _front; //队头
QueueItem* _rear; //队尾
}; int main() {
Queue<int> que;
for (int i = 0; i < 1000000; ++i)
{
que.push(i); //这里存在大量的new和delete
que.pop(); //
}
cout << que.empty() << endl;
return 0;
}

这里我们可以看出:每次push都会执行一次new QueueItem,每次pop都会执行一次QueueItem的delete;而节点不同的是只是结点中数据不同,短时间内大量对其进行调用,会影响我们程序的性能。所以采用对象池更佳,对象池模型如下:

可知:第一次对象池生成的对象全部用完,会重新开辟新对象在对象池中,如果归还第一次开辟的对象,也会链接到对象池空闲对象中

对象池实现:

#include<iostream>

using namespace std;

template<typename T>
class Queue {
public:
Queue(){
_front = _rear = new QueueItem(); //提供一个空节点
}
~Queue() {
QueueItem* cur = _front;
while (cur != nullptr) {
_front = _front->next;
delete cur;
cur = _front;
}
}
void push(const T &val) { //队尾入队
QueueItem* item = new QueueItem(val); //先调用new在对象池中拿出对象,然后采用构造函数初始化对象;可以和之前new汇编做了什么行为进行对比
_rear->next = item;
_rear = item;
}
void pop() { //队头出队, 不用返回值,记住队头为空
if (empty()) throw "队列为空";
QueueItem* item = _front->next;
_front->next = item->next;
if (_front->next == nullptr) { //说明只有一个元素;
_rear = _front;
}
delete item;
}
T front()const { //只读操作所以加const
if (empty()) throw "队列为空";
return _front->next->_data; //对象才有点操作,指针是箭头操作,有多个next因为有空的头节点
}
bool empty()const { return _rear == _front; } //只读操作 private:
//想做一个对象池(100000个QueueItem的节点),不用大量new和delete,用到就里面拿,不用就归还,程序员来内存管理
struct QueueItem
{
QueueItem(T data = T()): _data(data), next(nullptr) {} //留意一下,这个T data = T()操作!!!!!!!
//QueueItem提供自定义内存管理
void* operator new(size_t size){ //会自动变为static方法,这里的size没有用到
if (_itemPool == nullptr) {
_itemPool = (QueueItem*)new char[POOL_ITEM_SIZE * sizeof(QueueItem)]; //为什么内存池用char?
QueueItem* p = _itemPool;//指向首地址
for (; p < _itemPool + POOL_ITEM_SIZE - 1; ++p) { //最后一个元素指向空,不需要遍历到最后一个元素
p->next = p + 1;
}
p->next = nullptr;
}
QueueItem* p = _itemPool;
_itemPool = _itemPool->next;
return p;
} void operator delete(void* ptr) { //使用void*,自动变为静态方法
QueueItem* p = (QueueItem*)ptr;
p->next = _itemPool;
_itemPool = p;
}
T _data;
QueueItem* next;
static QueueItem* _itemPool; //内存池指针
static const int POOL_ITEM_SIZE = 100000;
};
QueueItem* _front; //队头
QueueItem* _rear; //队尾
};
template <typename T>
typename Queue<T>::QueueItem *Queue<T>::QueueItem::_itemPool = nullptr; //记住样例 int main() {
Queue<int> que;
for (int i = 0; i < 1000000; ++i)
{
que.push(i); //用对象池处理
que.pop(); //
}
cout << que.empty() << endl;
return 0;
}

什么是0构造

深入C++05:运算符重载的更多相关文章

  1. C/C++对Lu系统内置动态对象进行运算符重载

    欢迎访问Lu程序设计 C/C++对Lu系统内置动态对象进行运算符重载 1 说明 要演示本文的例子,你必须下载Lu32脚本系统.本文的例子需要lu32.dll.lu32.lib.C格式的头文件lu32. ...

  2. C++ 运算符重载时,将运算符两边对象交换问题.

    在C++进行运算符重载时, 一般来讲,运算符两边的对象的顺序是不能交换的. 比如下面的例子: #include <iostream> using namespace std; class ...

  3. C#高级编程笔记2016年10月12日 运算符重载

    1.运算符重载:运算符重重载的关键是在对象上不能总是只调用方法或属性,有时还需要做一些其他工作,例如,对数值进行相加.相乘或逻辑操作等.例如,语句if(a==b).对于类,这个语句在默认状态下会比较引 ...

  4. C++运算符重载

    C++运算符重载 基本知识 重载的运算符是具有特殊名字的函数,他们的名字由关键字operator和其后要定义的运算符号共同组成. 运算符可以重载为成员函数和非成员函数.当一个重载的运算符是成员函数时, ...

  5. 标准C++之运算符重载和虚表指针

    1 -> *运算符重载 //autoptr.cpp     #include<iostream> #include<string> using namespace std ...

  6. python运算符重载

    python运算符重载就是在解释器使用对象内置操作前,拦截该操作,使用自己写的重载方法. 重载方法:__init__为构造函数,__sub__为减法表达式 class Number: def __in ...

  7. PoEduo - C++阶段班【Po学校】-Lesson03-5_运算符重载- 第7天

    PoEduo - Lesson03-5_运算符重载- 第7天 复习前面的知识点 空类会自动生成哪些默认函数 6个默认函数    1  构造  2  析构   3  赋值  4 拷贝构造  5 oper ...

  8. 不可或缺 Windows Native (24) - C++: 运算符重载, 自定义类型转换

    [源码下载] 不可或缺 Windows Native (24) - C++: 运算符重载, 自定义类型转换 作者:webabcd 介绍不可或缺 Windows Native 之 C++ 运算符重载 自 ...

  9. 我的c++学习(8)运算符重载和友元

    运算符的重载,实际是一种特殊的函数重载,必须定义一个函数,并告诉C++编译器,当遇到该运算符时就调用此函数来行使运算符功能.这个函数叫做运算符重载函数(常为类的成员函数). 方法与解释 ◆ 1.定义运 ...

随机推荐

  1. Java中使用最频繁及最通用的Java工具类

    在Java中,工具类定义了一组公共方法,Java中使用最频繁及最通用的Java工具类. 一. org.apache.commons.io.IOUtils closeQuietly:关闭一个IO流.so ...

  2. pycharm——import已存在的库居然失败!

    问题 明明在cmd中可以import的库,放到pycharm中却找不到. 问题根源 找了一圈,最后得到这个结论. 因为pycharm默认就是这样的... 解决 打开设置,找到解释器 点击右边齿轮图标, ...

  3. FSB—QPI—DMI总线的发展

    intel CPU有的是前端总线(FSB),有的是QPI总线,有的又是DMI总线 FSB总线(由于cpu的发展,fsb总线制约了cpu的发展,所以该总线已经渐渐淡出历史舞台) FSB即Front Si ...

  4. 虚拟机安装linux

    https://blog.csdn.net/wujiele/article/details/92803655https://www.cnblogs.com/yunwangjun-python-520/ ...

  5. [资源] 桃宝上十几块买的C#教程网盘

    我粗略的看了下觉得挺好的,分享给大家 链接:https://pan.baidu.com/s/1iHuLUXde4_L7NB-Zw9JWNg 提取码:1314

  6. Java学习day12

    Set集合特点:不能包含重复的元素:没有所有的方法,不能通过普通的for循环遍历集合 哈希值是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值,同一个对象多次调用hashCode()方法 ...

  7. OpenHarmony标准设备应用开发(一)——HelloWorld

    (以下内容来自开发者分享,不代表 OpenHarmony 项目群工作委员会观点) 邢碌 本文是 OpenAtom OpenHarmony(以下简称"OpenHarmony")标准设 ...

  8. HbuilderX失焦时自动保存编辑器内容

    hbuilderX 有一个非常好用的功能:就是自动保存. 而且不需要安装什么插件,只需要在编辑器设置就可以了.接下来我们一起来设置吧: 1.打开我们的hbuilderX编辑器.在最上排选项栏里打开 & ...

  9. ThreadLocal的原理及产生的问题

    点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人. 文章不定期同步公众号,还有各种一线大厂面试原题.我的学习系列笔记. ThreadLocal的原理 特点 ThreadLocal和Sychro ...

  10. 记一次sql注入的解决方案

    点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人. 本文在公众号文章已同步,还有各种一线大厂面试原题.我的学习系列笔记. 今天业务提了个模糊查询,一听就知道这种问题有坑,肯定涉及到sql注入, ...