以良好的方式编写class :
class without pointer members :例如complex类
class with pointer members : 例如string类
学习类之间的关系:继承,复合,委托;
基于对象:面对的是单一class的设计;、
面向对象:面对的是多重class的设计;
C++的运算符重载:使得对象的运算表现得和编译器内置类型一样;
重载的运算符是具有特殊名字的函数;
如果一个运算符函数是成员函数,那么它的第一个(左侧)运算对象绑定到隐藏的成员函数形参this指针上;
选择作为非成员还是成员函数呢?
1.必须是成员函数:赋值=,下标[ ],调用()和成员访问箭头->
2.复合赋值一般是成员,但并非必须;
3.改变对象状态的运算符 或者 与给定类型密切相关,如递增、递减、解引用通常是成员;
4.具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因该是非成员函数;
重载输出运算符 输出运算符的第一个形参是非常量ostream对象的引用。之所以ostream是非常量是因为向输出流写入内容会改变其状态,而使用引用是因为我们无法直接复制一个ostream对象。返回ostream的引用保证可以连续输出,必须是非成员函数;
算术和关系运算符定义为非成员函数以允许左侧或右侧的运算对象进行转换;如果定义了算术运算符,一般也会定义复合赋值运算符(成员函数),此时,最有效的方式是使用复合赋值来定义算术运算符;
类定义了operator ==运算符,则也应该定义operator != ,这样类可以更容易的使用标准库容器和算法;注意都是非成员函数;!=和==不能相互委托;
下标[ ]运算符必须是成员函数,下标运算符以所访问元素作为返回值,这样的好处是下标可以出现在赋值运算符的任意一端;且最好同时定义常量版本和非常亮版本;这样当作用于一个常量对象时候,下标运算符返回常量引用以确保我们不会给返回的对象赋值;如下面代码:
public:
std::string& operator [ ] (std::size_t n) {return elements[n];}
const std::string& operator [ ](std::size_t n) const {return elements[n]}
private:
std::string* elements;
递增和递减运算符应该定义为成员函数:且应该同时提供前置和后置版本;后置版本接受一个额外的(不被使用)int类型的形参,当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参;这个形参唯一的作用就是区分前置和后置;注意前置版本返回对象引用,而后置版本返回值;
成员访问运算符: 箭头访问运算符->必须是成员函数,解引用运算符* 通常也是成员函数;
std::string& operator *() const {
//check
return (*p)[curr];
}
std::string* operator->() const {
return &this->operator*();
//->箭头运算符一般委托*解引用运算符
}
函数调用运算符:如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象;因为这样的类同时也能存储状态,所以与普通函数相比更加灵活;
如果类定义了函数调用运算符,则该类的对象称为函数对象;因为可以调用这种对象,所以我们说这种对象的行为像函数一样;
下面实现自己的复数类complex:因为complex的成员没有指向外部资源,所以编译器默认的拷贝构造函数,拷贝赋值运算符和析构函数这三个BIg Treee函数够用了;但是下一节的字符串类String,因为内部有指针,指向外部的字符串,所以不能使用编译器默认的拷贝构造函数,拷贝赋值运算符和析构函数,必须自己定义Big Three这三个函数;
comlex类:
标准的头文件防卫式声明(微软的是#pragma once)
第一次#include这个头文件的时候进来,然后定义__MYCOMPLEX__,当后续重复包含头文件的时候,因为已经定义了__COMPLEX__就不会进入下面代码了,有效防止了重复包含。
#ifndef __MYCOMPLEX__
#define __MYCOMPLEX__
class complex; //类的前置声明
complex&
__doapl (complex* ths, const complex& r);
complex&
__doami (complex* ths, const complex& r);
complex&
__doaml (complex* ths, const complex& r);
class complex
{
public:
构造函数有默认实参;通过成员初始化列表完成类中成员变量的初始化;成员初始化效率要高,且有些成员必须要在成员初始化完成初始化,详看C++对象模型。
区别:1.在初始化列表里是成员变量的初始化2.在{}大括号里面是在默认初始化后,再进行赋值;
complex (double r = 0, double i = 0): re (r), im (i) { }
complex& operator += (const complex&);//返回引用可以实现连续赋值,且引用比传值效率高;
complex& operator -= (const complex&);
complex& operator *= (const complex&);
complex& operator /= (const complex&); //复合赋值运算符一般声明为成员方法;
//编译器做对象运算的时候,会调用对象的运算符重载函数;优先调用成员运算符重载函数,如果没有就去全局作用域找合适的运算符重载函数
函数如果在类中完成定义,变自动成为inline候选人,意思就是编译器看函数如果不复杂,就生成内联函数。
double real () const { return re; }
double imag () const { return im; }
不会改成对象的成员函数最好加上const,这样const对象和非const对象都能调用;不加const,那么const对象无法调用这个成员函数;
函数后加const实际上是修饰成员函数的隐藏形参this指针,调用成员函数的对象的地址会传递给这个this指针。
++--是单目运算符,怎么区分前置++后置++呢?C++编译器规则是后置++通过一个不用的int形参标识,如operator ++(int) 后置++标识
一般是前置++返回引用,后置++返回值
complex& operator ++() //前置++返回引用,因为this不是函数体内的局部对象
{
re+=1;
im+=1;
return *this;
}
complex operator ++(int) //加一个形参int标识这是后置++;返回的是函数体内的局部对象,因此返回值(绝不能返回引用)
{
return complex(++re,++im);
}
private:
double re, im;
friend complex& __doapl (complex *, const complex&);
friend complex& __doami (complex *, const complex&);
friend complex& __doaml (complex *, const complex&);
//类中声明了友元函数,那么友元函数可以自由取得类中的私有成员;相同类的各个object互为友元;
友元函数是在定义在类外部的普通函数或者类,但是需要在类中声明为友元,为了和普通成员函数区别,在前面加上关键字friend;
友元函数不是成员函数,但他可以访问类中的私有成员;友元不是授权类的成员,所以它不受类声明区域private,protected,public的影响;
};
inline complex&
__doapl (complex* ths, const complex& r)
{
ths->re += r.re;
ths->im += r.im;
return *ths;
}
inline complex&
complex::operator += (const complex& r)
{
return __doapl (this, r);
}
inline complex&
__doami (complex* ths, const complex& r)
{
ths->re -= r.re;
ths->im -= r.im;
return *ths;
}
inline complex&
complex::operator -= (const complex& r)
{
return __doami (this, r);
}
inline complex&
__doaml (complex* ths, const complex& r)
{
double f = ths->re * r.re - ths->im * r.im;
ths->im = ths->re * r.im + ths->im * r.re;
ths->re = f;
return *ths;
}
inline complex&
complex::operator *= (const complex& r)
{
return __doaml (this, r);
}
inline double
imag (const complex& x)
{
return x.imag ();
}
inline double
real (const complex& x)
{
return x.real ();
}
inline complex
operator + (const complex& x, const complex& y)
{
return complex (real (x) + real (y), imag (x) + imag (y));
}
inline complex
operator + (const complex& x, double y)
{
return complex (real (x) + y, imag (x));
}
inline complex
operator + (double x, const complex& y)
{
return complex (x + real (y), imag (y));
}
inline complex
operator - (const complex& x, const complex& y)
{
return complex (real (x) - real (y), imag (x) - imag (y));
}
inline complex
operator - (const complex& x, double y)
{
return complex (real (x) - y, imag (x));
}
inline complex
operator - (double x, const complex& y)
{
return complex (x - real (y), - imag (y));
}
inline complex
operator * (const complex& x, const complex& y)
{
return complex (real (x) * real (y) - imag (x) * imag (y),
real (x) * imag (y) + imag (x) * real (y));
}
inline complex
operator * (const complex& x, double y)
{
return complex (real (x) * y, imag (x) * y);
}
inline complex
operator * (double x, const complex& y)
{
return complex (x * real (y), x * imag (y));
}
complex
operator / (const complex& x, double y)
{
return complex (real (x) / y, imag (x) / y);
}
inline complex
operator + (const complex& x)
{
return x;
}
inline complex
operator - (const complex& x)
{
return complex (-real (x), -imag (x));
}
inline bool
operator == (const complex& x, const complex& y)
{
return real (x) == real (y) && imag (x) == imag (y);
}
inline bool
operator == (const complex& x, double y)
{
return real (x) == y && imag (x) == 0;
}
inline bool
operator == (double x, const complex& y)
{
return x == real (y) && imag (y) == 0;
}
inline bool
operator != (const complex& x, const complex& y)
{
return real (x) != real (y) || imag (x) != imag (y);
}
inline bool
operator != (const complex& x, double y)
{
return real (x) != y || imag (x) != 0;
}
inline bool
operator != (double x, const complex& y)
{
return x != real (y) || imag (y) != 0;
}
#include <cmath>
inline complex
polar (double r, double t)
{
return complex (r * cos (t), r * sin (t));
}
inline complex
conj (const complex& x)
{
return complex (real (x), -imag (x));
}
inline double
norm (const complex& x)
{
return real (x) * real (x) + imag (x) * imag (x);
}
#endif //__MYCOMPLEX__
//测试代码
#include <iostream>
#include "complex.h"
using namespace std;
istream& operator >>(istream& in,complex& src)
{
//注意要把这个函数在类中进行友元声明
is>>src.re>>src.im;
return in;
}
ostream&
operator << (ostream& os, const complex& x)
{
//这里因为用的成员函数,所以不需要在类中进行友元声明
return os << '(' << real (x) << ',' << imag (x) << ')';
}
operator <<是非成员函数,那么当cout<<c2<<c1实际上是先cout<<c2,然后是cout<<c1
背后调用operator <<是非成员函数,所有cout和c2作为二个函数实参传递,operator <<(cout,a)
因输出运算符的调用对象是在右边,所以不能定义为成员函数,定义为全局函数;第一个参数是是输入流或者输出流的const对象;
为了保证连续输入,所以返回输入输出流的引用返回;
int main()
{
complex c1(2, 1);
complex c2(4, 0);
cout << c1 << endl;
cout << c2 << endl;
cout << c1+c2 << endl;
cout << c1-c2 << endl;
cout << c1*c2 << endl;
cout << c1 / 2 << endl;
cout << conj(c1) << endl;
cout << norm(c1) << endl;
cout << polar(10,4) << endl;
cout << (c1 += c2) << endl;
cout << (c1 == c2) << endl;
cout << (c1 != c2) << endl;
cout << +c2 << endl;
cout << -c2 << endl;
cout << (c2 - 2) << endl;
cout << (5 + c2) << endl;
return 0;
}
string类的设计:
#ifndef __STRING__
#define __STRING__ //防卫式声明,防止重复包含
#include <cstring>
#define _CRT_SECURE_NO_WARNINGS
class String
{
private:
char* m_data; //指向实际字符串存储位置的指针
public:
String(const char* cstr = 0); //形参有默认参数的构造函数
String(const String&); //拷贝构造函数
String& operator =(const String&);//拷贝赋值运算符重载
~String(); //析构函数
char* get_c_str()const { return m_data; }
bool operator >(const String& str) const ; //< == =!省略
int length () const {return strlen(m_data);} //返回有效字符个数(不包括'\0')
char& operator [] (int index) { return m_data[index]; }
const char& operator [](int index) const {return m_data[index];}
//常量版本和非常量版本的下标访问运算符重载下标[ ]运算符必须是成员函数,
下标运算符以所访问元素作为返回值,这样的好处是下标可以出现在赋值运算符的任意一端; 且最好同时定义常量版本和非常亮版本;
这样当作用于一个常量对象时候,下标运算符返回常量引用以确保我们不会给返回的对象赋值;
};
inline
String::String(const char* cstr)//构造函数
{ //想想这个构造函数处理什么?怎么写?
std::cout << "构造函数" << std::endl;
if (cstr) //cstr不是空指针
{
m_data = new char[strlen(cstr) + 1];//申请内存空间
//strlen(char*)返回实际字符串长度,遇到'\0'结束但不包含'\0’这个字符
strcpy(m_data, cstr); //strcpy(char*dest,const char*src)把末尾含有'\0'的字符串复制到另一个地址空间
}
else//cstr是空指针
{
m_data = new char[1];//申请一个空间存放字符串结束标记'\0'
m_data = '\0'; //就算是空字符串要存'\0'字符说明他是空的
}
}
inline
String::~String() //析构函数
{
std::cout << "析构函数" << std::endl;
delete[] m_data;
}
inline
String::String(const String& str)//拷贝构造函数
{
std::cout << "拷贝构造函数" << std::endl;
m_data = new char[strlen(str.m_data) + 1];//申请空间
strcpy(m_data, str.m_data); //字符串拷贝
}
inline
String& String::operator =(const String& str)
{
std::cout << "拷贝赋值=运算符" << std::endl;
//拷贝赋值运算符重载最重要的是: 检测自我赋值 & 异常安全(new)
if(this != &str)
{
String temp(str); //临时实例
char* p = temp.m_data;
temp.m_data = m_data;
m_data = p;
}
return *this; //返回引用可以连续赋值
}
//全局函数: 重载<<运算符输出字符串
//为什么不写成成员函数呢? 因为这样使用起来用的方向就会相反,不符合人的使用习惯
#include<ostream>
std::ostream& operator <<(std::ostream& os, const String& str)
{
os << "operator <<";
os << str.get_c_str();
return os; //为了重复输出
}
#endif // !__STRING__
string中的iterator迭代器实现:
从迭代器的使用方式上来说,迭代器属于容器类型的一个嵌套类(std::string::iterator )容器类型是外层类,外层类加了作用域嵌套类;那么这段代码到底什么意思呢?
容器有个begin方法,返回的是它底层首元素的迭代器表示;底层的数据结构怎么从一个元素变到下一个元素我们不关心,我们作为使用者只需要对迭代器进行++操作就可以了。而其容器底层真真正正从一个元素跑到下一个元素,他们不同数据结构的差异都封装在了这个迭代器的++运算符里面;
后面学习泛型算法的时候,泛型算法参数接受的都是迭代器;泛型算法是全局的函数,是给所有容器使用的;所有泛型算法得有一套方式能够统一的遍历所有容器的元素-迭代器;
迭代器的功能:提供一种统一的方式,来透明的遍历容器;不需要知道容器底层的数据结构,所有不同数据结构的遍历都被封装到了迭代器的++运算符重载函数上面;
class String{
private:
char * m_data; //.....String的成员和函数
//定义iterator为类String的嵌套类;每个容器都有自己的迭代器;
class iterator{
private:
//迭代一个char数组用什么?用下标和指针都可以
char* _p;
public:
iterator(char* p = nullptr ):_p(p) {}
//迭代器要指向一个位置,那么参数也得接受一个指针;用传进来的指针给迭代器的成员变量指针进行初始化;
bool operator!=(const iterator& it){
return _p != it._p;
//当前迭代器的指针和it引用的迭代器的指针不相等,那就说明二个迭代器不相等
}
//注:前置++返回的是对象的引用,而后置++返回的是临时量;所以前置++效率高一点
void operator++(){
++_p;
}
char& operator* (){ //给迭代器解引用相当于给底层的指针解引用
return *_p;
}
}; //注意begin()和end()是容器的方法,不是迭代器的方法
iterator begin(){return iterator(m_data);}; //begin返回容器首元素的迭代器表示;
//_p指向的就是底层数组的首元素,用它来定义一个迭代器,就相当于让迭代器指向了容器底层的首元素了;
iterator end(){return iterator(m_data+ length());};//end返回容器末尾元素的后继位置的迭代器表示
}
vector迭代器失效问题:
迭代器失效场景1: 在vector容器中通过迭代器删除元素;
vector中删除一个迭代器it指向的元素,那么从之前的it及其后序迭代器都会失效,再使用失效的迭代器都会报错;
迭代器失效场景2:给vector容器中所有偶数前面添加一个小于偶数值1的数字;
vector中在迭代器it指向的元素之前添加一个元素,1.如果vector没有扩容:那么it及其后序的迭代器都会失效,再使用失效的迭代器都会报错;2.如果vector扩容了,那么之前vector的迭代器全部失效了;
总结一下:1.迭代器为什么会失效?
a:当容器调用erase方法后,当前元素到容器末尾元素的所有迭代器全部失效;
b:当容器调用insert方法后,当前位置到容器末尾元素的所有迭代器全部失效;
从首元素->插入点或者删除点:这些迭代器还是有效的;
从插入点或者删除点-》末尾元素,这些迭代器全部失效;
c:例外情况:对于insert来说,如果发生了扩容,那么所有迭代器全部失效了,因为整块的内存空间已经改变了,原来的迭代器指向的是旧内存,已经不用了,元素都被拷贝都新内存空间了;
d:不同容器的迭代器是不能进行比较运算的;只能是同一个容器的迭代器进行比较才是有意义的。
迭代器失效之后,问题怎么解决?
对插入或者删除点的迭代器要进行更新操作;
迭代器失效场景1: vector容器中通过迭代器删除元素;
std::vector<int> v ;
for(int i =0;i<20;++i){
v.push_back(i) ;
}
//把vec中的偶数全部删除
for(std::vector<int>::iterator it = v.begin();it!=v.end();++it){
if (*it % 2 == 0) {
// v.erase(it); //错误
//迭代器失效问题,第一次调用erase后,it就失效了,然后对失效迭代器++出现不可预期的错误了;
}
}//程序出错了,报异常;
迭代器失效场景2:给vector容器中所有偶数前面添加一个小于偶数值1的数字;
for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
if (*it % 2 == 0) {//给vector容器中所有偶数前面添加一个小于偶数值1的数字;
// v.insert(it, *it + 1); 错误
//这里报异常:这里迭代器在第一次insert后,迭代器就失效了
}
}
正确的vector中通过迭代器增加和删除元素的操作:
1.vector容器中通过迭代器删除元素的正确方法:
std::vector<int>::iterator it = v.begin();
while (it!=v.end())
{
if (*it % 2 == 0) {
it = v.erase(it);
//erase(it)后,it指向的迭代器失效了,不能再使用it了;
//而erase的返回一个指向被删除元素后继元素的迭代器,再赋给it,it就可以继续遍历了
//erase返回指向删除元素之后元素的迭代器,这个迭代器是正确的;
//当删除元素后,后面元素都会往前挪动;后面那个元素移动到当前位置了,所以不需要对it进行加1;
}
else {
++it;
}
}
2.vector中插入元素的正确方法:
std::vector<int> vi {0,1,2,3,4,5,6,7,8,};
std::vector<int>::iterator iter = vi.begin(); //调用begin而不是cbegin,因为我们要改变iter
while (iter!=vi.end() ){
if(* iter %2) { //奇数
iter=vi.insert(iter,*iter);
//在迭代器iter位置插入元素后,iter及其后序的迭代器都会失效;如果扩容了,那么全部迭代器都会失效;
//insert是将当前位置的元素复制一份放到it位置之前,返回指向插入的新元素的迭代器
iter +=2; //移动迭代器,跳过当前元素以及插入到它之前的元素;需要移动2下
}else {
iter = vi.erase(iter);
//删除iter后,之前的iter及其后序迭代器全部失效了,所以这里通过erase返回的迭代器更新iter,保证迭代器有效;
//erase返回删除的迭代器iter指向元素下一个元素的迭代器
}
}
内存管理:
malloc和free都是C语言的库函数; new和delete,new[]和delete[]是运算符;
//--------------C语言的malloc和free----------------------------
int * p =(int*)malloc(sizeof(int));//1.malloc是按照字节开辟内存的,返回值值类型需要强制转换;
if(p==nullptr){ //2.malloc内存开辟失败是通过返回值空指针判断;
return -1;
}
* p = 20; //3.malloc只负责开辟内存,不进行初始化,我们需要单独进行初始化。
free(p);//free释放内存只需要传进去起始地址就行了。
//----malloc开辟数组内存
int* 1 = (int*)malloc(sizeof(int) * 100);
if(q==nullptr){
return -1;
}
free(q);
//------------C++的new和delete
int *p1 = new int (20);
1.new不仅可以开辟内存,还能完成初始化操作;
2.new开辟内存失败是抛出异常bad_alloc来判断;
所以为了安全我们可以这样:
tyr {
int * p1 = new int (20);
}catch (const bad_alloc& e){
处理内存申请失败的情况
}
delete p1; //释放内存
//-----new开辟数组内存
int* q1 = new int [20](); //20个int的内存;加()保证元素内存初始化为0
..捕获异常跟上面一样
delete [] p1;
注意:new配delete使用,new[]配合delete[]使用
new有多少种?
int * p1 = new int (20); //申请内存失败会抛出bad_alloc异常
int* p2 = new (nothrow)int ; //申请内存失败不会抛出异常
const int* p3 = new const int(40); //相当于在堆上开辟了一个常量
//定位new,placement new
int data = 0;
int* p4 = new (&data) int(50); //在指定的内存上划分4字节内存,然后給初值50
还要什么类内部的operator new ,全局的operator new 可以重载更改。
回忆一下之前的笔记:
1.malloc和new的区别?
a:malloc是按照字节开辟内存的;new是开辟内存时指定类型的,开辟多少个这种类型的内存;
b:malloc是C标准库的函数;new是C++的运算符;
c:malloc开辟的内存返回的指针是void* ,而new返回的指针是执行类型的指针;
d:malloc只负责开辟内存;而new运算符底层是调用了operator new开辟内存,然后又调用所指定那个类型的构造函数去初始化对象;
e:malloc开辟内存失败返回nullptr指针;而new开辟内存失败抛出bad_alloc类型的异常;
2.free和delete的区别?
a:free通过传入的指针去释放内存;而delete是先对指针所指对象调用析构函数,然后释放指针的内存;
b:free是c标准库的函数;delete是运算符;
可以重载全局的operator new 和 operator delete 或者 类中的operator new和operator delete
new,new[] 和delete,delete[]可以混用吗?为什么C++要区分单个元素和数组的内存分配和释放呢?
对于基本类型int等,混用可能没有影响,因为基本类型只有内存开辟,底层都是调用malloc,没有构造函数和析构函数调用;而对于自定义的类类型来说,
在new[ ]的时候在所开辟内存空间的上方会多开辟4个字节的空间来存放数组中对象的个数,这样调用delete[ ]的时候从要释放指针的上4个字节获取对象个数,就知道要调用多少次析构函数;
之前我们把operator new 和operator delete提供在全局空间了,那么后序程序只要使用new和delete都会调用全局的operator new和operator delete进行内存的申请和释放;有时候我们不想把所有的new和delete都改变,而只是想改变我们自定义类对象的内存管理方式,那么就可以在类中重载operator new 和operator delete ;