引言:
本篇博客将深入探究C++中的类和对象。我们将从普通高校教学点开始,逐步介绍类的定义、对象的创建和使用,以及类与对象之间的关系。通过详细讲解访问控制和成员函数,我们将揭示封装的重要性以及如何实现数据的隐藏和安全性。
目录
1.引用传值引入与拷贝构造
1.1引用传参防止对象重复析构
1.2 引用缺陷与拷贝构造
1.2.1拷贝构造函数的特征及分析
2.运算符重载
3.const成员
3.1权限的放缩与const成员
3.1.1读写分离函数的重载
3.1.2 const成员与非const成员的调用
3.1.3 拓展:取地址及const取地址操作符重载
4.未完待续......
1.引用传值引入与拷贝构造
1.1引用传参防止对象重复析构
我们先来看一段传值函数的代码:
class A
{
public:
A(int a = 0)
{
cout << "A(int a)" << endl;
_a = a;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_array);
_array = nullptr;
_size = _capacity = 0;
}
private:
// 内置类型
DataType* _array;
int _capacity;
int _size;
// 自定义类型
A _aa;
};
void func(Stack s)
{
}
int main()
{
Stack s;
func(s);
//类对象二次析构
return 0;
}
我们都知道函数的调用是需要创建函数栈帧的,而函数的传值传参传的只是实参的一份拷贝,但是我们的实参中含有在堆中开辟空间的成员,这种成员要进行拷贝,就是我们所谓的“浅拷贝”,相当于将拷贝出来的指针也指向了与实参指向的同一块空间,这样在C中本来没太大的问题,但是在C++的类中,有了析构函数,这种的传值传参就会带来析构两次的错误。
为此,我们可以采用引用传参的方法,传入一个实参对象的拷贝,这样一来,参数的生存周期就变成了main函数结束后再析构,这样也就解决了问题。
1.2 引用缺陷与拷贝构造
我们常常会遇到某个函数只是为了当时获得对象的属性,但是我并不想因为调用函数而改变实参从而失去数据或者是某些信息,上面的func函数是引用传参,虽然解决了重复析构的错误问题,但是如果我们要在函数里操作该对象,也可能会导致原实参的改变(含有指针等数据成员),这是我们不愿意看到的,这就要引出我们的C++的又一个默认成员函数-拷贝构造函数,俗称“深拷贝”。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
1.2.1拷贝构造函数的特征及分析
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
为何不能使用传值传参?
当拷贝构造函数的参数是对象本身并采用了传值传参的方式时,会导致无穷递归调用的问题。这是因为传值传参会触发拷贝构造函数的调用,而该调用又会触发新的拷贝构造函数的调用,形成了无限循环。
具体来说,当我们使用对象的值作为参数传递给拷贝构造函数时,编译器会尝试创建一个新的对象,以便在函数内部使用。但是在创建新对象的过程中,会再次调用拷贝构造函数,因为创建新对象需要将原始对象的值拷贝给新对象,这又会触发新一轮的拷贝构造函数调用。这就形成了一个无限循环,导致无穷递归调用,直到内存耗尽或栈溢出。
为了避免这种无限递归调用,我们通常要么使用对象的引用作为拷贝构造函数的参数,或者避免在拷贝构造函数中使用对象的值作为参数。通过使用引用参数,我们可以避免对象的拷贝,并且只传递对象的引用,从而避免了无限递归调用的问题。
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
3.若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
2.运算符重载
这里简要的给出日期类的代码,附有基础的运算符重载的代码,这里不做过多的解释:
#include<iostream>
using namespace std;
class Date
{
public:
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
static int montharr[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };//加static的作用是防止函数重复调用导致数组一直重新开辟
if (month==2 &&((year % 100 != 0 && year % 4 == 0) || year % 400 == 0))//闰年而且二月
{
return 29;
}
return montharr[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)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
// 日期+天数
Date operator+(int day)//加法运算符this对象不能改变
{
Date temp(*this);
temp += day;
return temp;
}
// 日期-天数
Date operator-(int day)
{
Date temp(*this);
temp._day -= day;
while (temp._day <= 0)
{
--temp._month;
if (temp._month == 0)
{
temp._year--;
temp._month = 12;
}
temp._day += GetMonthDay(temp._year, temp._month);
}
return temp;
}
// 日期-=天数
Date& operator-=(int day)
{
*this = *this-day;
return *this;
}
// 前置++
Date& operator++()//前置++结束后原对象直接改变
{
*this += 1;
return *this;
}
// 后置++
Date operator++(int)
{
Date temp(*this);
(*this) += 1;;
return temp;
}
// 后置--
Date operator--(int)
{
Date temp(*this);
(*this)-=1;
return temp;
}
// 前置--
Date& operator--()
{
(* this) -= 1;
return *this;
}
// >运算符重载
bool operator>(const Date& d)
{
if (_year != d._year)
return _year > d._year;
if (_month != d._month)
return _month > d._month;
if (_day != d._day)
return _day > d._day;
return false;
}
// ==运算符重载
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
// >=运算符重载
bool operator >= (const Date& d)
{
return (*this) > d || (*this) == d;
}
// <运算符重载
bool operator < (const Date& d)
{
if (_year != d._year)
return _year < d._year;
if (_month != d._month)
return _month < d._month;
if (_day != d._day)
return _day < d._day;
return false;
}
// <=运算符重载
bool operator <= (const Date& d)
{
return (*this) < d || (*this) == d;
}
// !=运算符重载
bool operator != (const Date& d)
{
return _year != d._year || _month != d._month || _day != d._day;
}
// 日期-日期 返回天数
//法1:
/*int operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
min++;
++n;
}
return n * flag;
}
*/
int operator-(const Date& d)
{
Date temp(*this);
int ans = 0;
if (temp < d)
{
while (temp != d)
{
++temp;
ans--;
}
}
else
{
while (temp != d)
{
temp--;
ans++;
}
}
return ans;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
Date d1(2023, 8, 15);
d.print();
d1.print();
Date d2(d1);
d2.print();
Date d3 = d1 + 200;
Date d4(2002, 2, 13);
d1.print();
d3.print();
d4.print();
cout << d1 - d4 << endl;
d1 -= 300;
d1.print();
return 0;
}
3.const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
3.1权限的放缩与const成员
我们根据上面的日期类,给出一份这样的测试代码,
解决办法就是将成员函数print以const修饰,但是这种const实际上是修饰的this指针,而this指针在成员函数中优势隐含的,所以,我们规定将const关键字放在函数的参数列表的括号之后,表示修饰this指针为const,这样,像我们上面的调用,只会产生非const对象调用所产生的“权限缩小”和const对象调用所带来的“权限平移”,两者都是被允许的。
3.1.1读写分离函数的重载
我们以重载数组访问运算符[ ]为例来看,我们知道,[ ]运算符可以重载为输出功能,当然本省也具有修改数组内容的功能,但是,如果我们将其重载为输出函数,那么,这个运算符就不再具备修改数组的值的能力,我们的目的是在重载这个运算符的基础上,不失去原来运算符所具备的功能,为此,我们可以将const修饰的运算符重载函数写入,与原运算符重载函数形成函数重载,
//只读操作,后面的const修饰,代表将传入的this指针权限缩小为const对象,而const引用返回也防止了被返回对象在堆上的数据被修改
const int& operator[](size_t i)const
{
assert(i < arr.size());
return arr[i];
}
//读、写操作,能够返回引用,也就是数组在堆上的空间的引用,所以可以对返回的对象进行修改操作
int& operator[](size_t i)
{
assert(i < arr.size())
return arr[i];
}
3.1.2 const成员与非const成员的调用
1. const成员函数内可以调用其它的非const成员函数吗?
当一个成员函数被声明为const时,它的语义表示该函数不会修改对象的状态。由于非const成员函数可以修改对象的状态,所以在const成员函数中调用非const成员函数可能会破坏const成员函数的语义,属于“权限放大”,是不被允许的。
2. 非const成员函数内可以调用其它的const成员函数吗?
非const成员函数内可以通过强制类型转换或使用const_cast来绕过const限制,并间接调用const成员函数。也就是“权限缩小”,是被允许的。在设计类的成员函数时,应该根据函数的目的和预期的行为来合理地使用const关键字。
一个函数,在允许的情况下,写成const好还是非const好呢?
从上面我们可以看出,const修饰的成员函数,非const对象和const对象都可以调用,而非const函数,const对象却不能调用,所以,const成员函数更加有优势,并且可以避免一些修改值的错误产生,但是,const成员函数要求不能对涉及的内部对象进行修改,所以,一般的,只要不涉及修改对象的属性,我们就可以将函数设置为const修饰。
3.1.3 拓展:取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public :
Date* operator&()
{
return this;
//return nullptr;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这样的目的是想让别人取不到有效的地址在函数返回值中,我们可以让别人获取指定的任一地址,属于恶搞行为,实际中一般不使用,了解即可。
4.未完待续......
敬请期待,C++--深入类和对象(下)。