文章目录
前言
介绍了类的一些概念,this指针,默认生成函数和符号重载
面向对象和过程的初步介绍
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
类
类其实类似于我们C语言中的结构体,不过有对类进行了较多的修改使其更加优秀.
在C语言中我们只能向里面放变量,但是在类里我们可以向里面放函数
如下就是一个类
class date
{
public:
void Init(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
C++习惯使用class来创建类,不过你是用struct来创建类也是可以的=.=,最好使用class
而且C++中我们可以不用typora来省事了直接可以用类名来创建变量.
如我们就可以直接使用
int main()
{
date a;
a.Print();
return 0;
}
类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
一般向类中放函数可以有两种情况
- 声明定义都放在类中
- 值将声明放在类中,把定义放在.cpp的文件里
其中声明定义都放在类中的就如我们下面举的例子一样.
class date
{
public:
void Init(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
这样放入类中的函数,编译器默认其为内联函数.不过内联函数是建议具体要看编译器.
第二种情况
类似下面代码
我们一般推荐第二种方式.
(本文为了方便讲解先使用第一种方法)
类的访问限定符与封装
访问限定符
如果我们在C语言中使用结构体来写栈,如果有人不通过我们的函数接口来改变变量也是可以的.
如下图
我们好好的写好的代码就这么被人随便改了,我们后续再写代码的时候直接就一堆错误,非常痛苦.
于是我们的C++语言就加入了public
private
protected
这三个来保护权力
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
class date
{
public:
void Init(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
我们的public下到private之间的内容是都是类之外可以访问的.
而我们private到类定义结束的区间都是我们类之外不能访问的.
类的实例化(对象)
和我们的C语言结构体一样我们的类也只是一个蓝图而已,他并没有在建造类的时候占据空间,只有在形成变量的时候才会占据空间.
也就是说我们创建的类其实就类似于一个类型,一个自己定义的类型.
我们把用类创建的变量叫做对象.
如何计算类的大小
我们的类中既有函数又有变量那么实例化之后的类的大小如何计算呢?
其实和C语言的结构体相同都是用对齐来计算大小的,我们的类虽然内部可以定义和声明函数但是函数并不占据类实例化后的大小.
而我们在类中的函数其实是存储在公共内存中的,无论对象是否相同
就像我们不在类里的函数一样也都是开辟一次之后就在那一片内存中使用.
所以在类中的函数也是和普通函数一样放在一块公共区域中,然后我们将传入的参数压进去进行操作.
也就不放在类中计算大小了.
如果我们不放变量那么类的大小应该是多大呢?
答案是:1—这个1的意思并不是保存了什么,而是为了占位表示对象存在.
this指针
- this指针的类型:类类型* const
- 只能在“成员函数”的内部使用
- this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
这个指针被我们的编译器隐藏起来了.
在上面我们定义函数的时候你可以看见
class date
{
public:
void Init(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
我在定义Print函数的时候直接写了_year
,_month
,_day
而我们打印的时候也雀氏会打印这些变量保存的内容,其实原因也很简单我们的C++通过隐藏this指针来得到的元素内容.
其实Print这个函数里面的内容应该是这样的.
void date::Print(date* const this)
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
因为是在类里所以private的元素也是可以访问的.
this是C++自己创立自己传参自己修饰,我们只需要写_year
这种变量名即可.
this指针也跟普通形参一样存储在栈中
this指针其实是可以为空的.只要不在函数内解引用this指针就不会报错.
如下:
但是如果函数内对this指针进行了解引用就不可以了.
类<2>
类的6个默认生成函数
类如果是个空类我们的编译器,也会生成6个默认的函数并在符合条件的情况下自己调用.这些函数包括
构造函数,析构函数,拷贝构造,赋值重载,两个取地址重载
这6个函数都是我们可以进行改造的并且在使用的时候编译器会自己调用,非常舒服.
构造函数
比如我们现在创建了一个日期型类,我们想对其进行初始化,但是如果我们只是写了个初始化的函数我们还需要每次使用都调用,非常不方便.
而我们的构造函数作为一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
值得注意的是,虽然名字叫构造函数但是他跟对象实例化构造没啥关系,构造函数就只管给成员附上值.
特性
函数名与类名相同。
无返回值。 (且不是void而是没有返回的类型)
对象实例化时编译器自动调用对应的构造函数。
构造函数可以重载。
因为可以重载所以可以创建多个来方便我们使用
- 在类被其他类引用是构建函数是会被调用.
class date
{
public:
//构造函数
date(int year = 2002, int month = 8, int day = 26)
{
_year = year;
_month = month;
_day = day;
}
//打印函数
void Print()
{
cout << _year << "_" << _month << "_" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date a;
a.Print();
return 0;
}
上面代码实现的结果如下图:
我们这样创建的构造函数是可以传参的
传参形势如下:
上面的构造函数我们使用了全缺省,但是如果我们不使用全缺省会发生什么呢?
我们将构造函数改成下面形势
改成上面形势后就会报下面的错误说我们没有默认构造函数使用.
那么什么可以成为默认构造函数呢?
[第五点介绍](# 特性)第五点的意思直接看图吧=.=
首先建立一个测试类并搭建好他的构建函数
在另一个类中使用测试类
来看看我们测试类的构造函数有没有被调用
调用了.
默认构造函数
直接告诉大家: 只有全缺省,无参,编译器自动生成的可以做默认构造函数,一个类没有默认构造函数并且没有传参的话是实例化出对象的.
但是如果没有默认构造函数,只要传参得当也是可以实例化出对象的.
比如下面的半缺省.
不过我们在搭建默认构造函数的时候还是使用全缺省较好.
而且全缺省的函数和无参不能同时出现,不然我们在使用的时候编译器无法识别.
注意: 我们类成员变量在取名的时候最后前面加上_
(不同公司规定不同,反正最好不要直接使用对应名称如year
,不然可能会出现以下情况)
因为我们的编译器的this指针是编译器自己调用的,并不是十分智能,所以我们最好还是在前面加上_
或者在其他地方加上标识.
也可以用this指针来弄,不过给人感觉怪怪的=.=
析构函数
概念
析构函数也不是将类内成员都销毁那是编译器干的事情,析构函数是在对象的生命结束要被销毁的时候自动调用的函数,比如我们的栈类要向堆区要空间,我们就可以在此处进行归还
特征
- 析构函数名是在类名前加上字符 ~.
- 无参数无返回值.
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数.
- 对象生命周期结束时,C++编译系统系统自动调用析构函数.
- 在当类一中有其他类的时候,类一的对象在被销毁前会调用其他类的析构函数.
来个例子看看吧
class Stack
{
public:
//构造函数
Stack()
{
_data = (int*)malloc(sizeof(int) * 4);
_top = 0;
_capacity = 4;
}
//析构函数
~Stack()
{
free(_data);
_top = 0;
_capacity = 0;
}
private:
int* _data;
int _top;
int _capacity;
};
拷贝构造函数
概念
用于将一个相同类型的对象内容拷贝到另一个对象中,只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象 创建新对象时由编译器自动调用。
举例如下
class date
{
public:
//构造函数
date(int year = 2002, int month = 8, int day = 26)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
date(const date& c)//也是构造函数的重载
{
_year = c._year;
_month = c._month;
_day = c._day;
}
//打印函数
void Print()
{
cout << _year << "_" << _month << "_" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
这样我们对另一个对象进行初始化时就可以直接传来对象了.
在初始化时使用=
赋值也可以调用到拷贝构造函数
其实我们不写拷贝构造函数我们的编译器也会给我们生成一个,自己生成的拷贝构造函数在浅拷贝的时候完全够用了,当我们需要深拷贝的时候就可以自己写.
深拷贝
既然我们不写拷贝构造函数编译器会自动生成一个用于浅拷贝的,为啥还要有呢?
主要是因为我们要写深拷贝.
深拷贝就是我们在实现的时候注意一下,不能简单通过赋值操作来拷贝的需要深拷贝一下.
比如指针等.
我们比如我们使用Stack类的时候需要从堆区拿空间,就需要指针来保存变量,如果我们使用浅拷贝就会造成free两次的警告,而且在使用的时候也十分诡异.
所以我们要专门写个拷贝构造函数来达到深拷贝的目的.
比如我们的Stack类的.
Stack(const Stack& s)
{
_data = (int*)malloc(sizeof(int) * (s._capacity));
memcpy(s._data, _data, sizeof(int) * s._capacity);
_capacity = s._capacity;
_top = s._top;
}
我们的这些默认生成的函数都可以自己实现,只要自己记住他们的格式就好.
其中取地址重载等函数都可以实现但是,没必要编译器的实现以及足够我们使用了.
操作符重载
我们用类的时候总要使用-,+,=,*,/,++,--,==,!=
等操作符,还是以日期类为例.
当然日期类就一般不会使用*,/
了=.=
早操作符重载的格式如下
//返回值类型+operator+要重载的符号+(形参)
Date& operator=(Date& d1 ,const Date& d2);
//上方就是我对=号赋值符的一次实现.
而具体的我们要看实现.
比如我们可以选择在类里实现或是在类外实现.
先来一个日期类吧
class date
{
public:
void Init(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
我们先用类外的方式实现一个==
看看吧
bool operator==(const Date& d1, const Date& d2)
{
if (d1._year == d2._year && d1._month == d2._month && d1._day == d2._day)
{
return true;
}
else
{
return false;
}
}
这个其实并不能使用因为我们_year
是私有形势存储的,要想使用就必须将_day,_month,_year
公有化或者使用内部共有函数来得到他们的值,因为我们只是演示,所以我暂时先把他公有化出来.
我们实现好了这个==
运算符后
上图框起的两种方式都可以使用到我们的
==
重载功能,但是我们偏向使用下面的a==b
.在我们使用
a == b
的时候其实编译器会帮我们换成第一种的形式—operator==(a,b).
不过我们还是把这些运算符放在类里较好,及保证了类的封装性,又保证了我们元素不被外界访问.
我们将操作符重载放在类里需要对格式进行稍微改变.
// ==运算符重载
bool operator==(const Date& d)
{
if (_year == d._year && _month == d._month && _day == d._day)
{
return true;
}
else
{
return false;
}
}
在类内定义的格式其实和外面定义的几乎没啥区别,除了少了一个变量,在使用_year
的时候我们不用把它变成共有化的了.
其实少的那个变量使用了this指针来代替.而
_year
其实也是通过this指针得到的.
但是我们的this是两个变量的那个呢?
来看看我们如何使用类里定义的重构函数就知道了.
我们在使用的时候的形势其实注定了,上面的①中的this其实就是a的地址.
而下面的②其实this也是a的地址.
两者在编译看来没有区别.编译器也会2变成1后,然后编译.
如果有两个变量一般是左边的左this指针,三个变量就是最左的是this指针
类比思考一下,我们的前置后置++
--
等其实在类内实现的时候就没必要专门设置形参只需要一个this指针就够了.但是这样就没办法分别了,所以我们规定后置类型要创建一个int
类型的形参用于函数分辨.以便形成重构.
下面是对日期类所有操作符重载的实现,我们在实现的时候,有些部分可以操作符可以复用最好复用,复用带来的好处有很多,不仅方便,而且后期找bug也可以减少低级错误.
class Date
{
public:
void Print()
{
cout << _year << "_" << _month << "_" << _day << endl;
}
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
if (month > 12)
{
cout << "月输入错误" << endl;
return -1;
}
int const arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//保存每个月的日期
if (month == 2 && JudgeLeapYear(year))//闰年
{
return 29;
}
return arr[month];
}
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
// 析构函数
~Date()
{
;//不写用默认的也可
}
// 日期+=天数
Date& operator+=(int day)
{
*this = *this + day;//复用+
return *this;
}
// 日期+天数
Date operator+(int day)
{
Date tmp(*this);
(tmp._day) += day;
while (tmp._day >= GetMonthDay(tmp._year, tmp._month))//判断我们的day是不能超过当月的最大数值的.
{
tmp._day -= GetMonthDay(tmp._year, tmp._month);
tmp._month += 1;
if (tmp._month == 13)
{
tmp._month -= 12;
tmp._year += 1;
}
}
return tmp;
}
// 日期-天数
Date operator-(int day)
{
Date tmp(*this);
(tmp._day) -= day;
while (tmp._day <= 0)
{
tmp._day += GetMonthDay(tmp._year, tmp._month);
tmp._month -= 1;
if (tmp._month == 0)
{
tmp._month += 12;
tmp._year -= 1;
}
}
return tmp;
}
// 日期-=天数
Date& operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
_day += GetMonthDay(_year, _month);
_month -= 1;
if (_month == 0)
{
_month = 12;
_year -= 1;
}
}
return *this;
}
// 前置++
Date& operator++()
{
*this += 1;
return *this;
}
// 后置++
Date operator++(int)//传的int是语法规定
{
Date tmp(*this);
*this += 1;
return tmp;
}
// 后置--
Date operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
// 前置--
Date& operator--()
{
*this -= 1;
return *this;
}
// >运算符重载
bool operator>(const Date& d)
{
if (_year > d._year || (_year == d._year && _month > d._month) || (_year == d._year && _month == d._month && _day > d._day))
{
return true;
}
else
{
return false;
}
}
// ==运算符重载
bool operator==(const Date& d)
{
if (_year == d._year && _month == d._month && _day == d._day)
{
return true;
}
else
{
return false;
}
}
//>=运算符重载
inline bool operator >= (const Date& d)
{
if (*this > d || *this == d)
{
return true;
}
else
{
return false;
}
}
// <运算符重载
bool operator < (const Date& d)
{
return !(*this >= d);
}
// <=运算符重载
bool operator <= (const Date& d)
{
if (*this < d || *this == d)
{
return true;
}
else
{
return false;
}
}
// !=运算符重载
bool operator != (const Date& d)
{
return !(*this == d);
}
// 日期-日期 返回天数
int operator-(const Date& d)
{
Date min = (*this > d ? d : *this);
Date max = (*this > d ? *this : d);
int ret = 0;
while (min != max)
{
min++;
ret++;
}
return ret;
}
private:
bool JudgeLeapYear(int year)
{
if ((year % 4 == 0 && year % 100 != 0) || year % 100 == 0)
{
return true;
}
else
{
return false;
}
}
private:
int _year;
int _month;
int _day;
};
const成员
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改。
其实很简单,就是在类内函数()后加上const就可以把原来类型为Date*
的this指针变成const Date*
类型.