好久不见,我是结衣。
时隔多天的类中的默认成员函数,在上篇博客中我们讲了,构造函数和析构函数。简单回顾下,构造函数就是类似于初始化函数,他的函数名与类名相同,且没有返回值。析构函数,函数名就是类名前加上~,也没有返回值。
其实我们还剩下了,四个默认成员函数没有讲,他们分别是:拷贝函数,赋值运算符重载,const成员,取地址重载。
假设我们已经创建了一个对象,如果我们还想再创建一个对象且这个对象和前一个对象的值一模一样,我们要怎么呢?调初始化就太麻烦了,我们可以直接用以创建的对象拷贝给新对象吗?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
1.拷贝构造构造是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,如果使用传值方式的话编译器会直接报错,因为这样的话会引起无穷递归调用。
解释就是,函数传值也是一种拷贝,每次传值都会调用拷贝构造,就会一直调用递归。
正确写法:
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
3.**如果没有显示定义,编译器会生成默认的拷贝构造函数。**默认的拷贝构造函数对象按内存储存按字节序完成拷贝,这种拷叫做浅拷贝,或者值拷贝。
class Time
{
public:
Time()
{
_hour = 1;
}
Time(const Time& t)
{
_hour = t._hour;
cout << "Time::Time(const Time&)" << endl;
}
private:
int _hour;
};
class Date
{
private:
//基本类型
int _year = 2;
int _month = 2;
int _day = 2;
//自定义类型
Time _t;
};
int main()
{
Date d1;
//用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
//但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
Date d2(d1);
return 0;
}
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,二自定义类型是调用其拷贝构造函数完成拷贝的。
4.既然编译器都会帮我们拷贝了,那我们怎么还要自己来写呢?如果是像这种日期类的类当然是没有必要的,但是如果是下面的这种呢?
class Stack
{
public:
Stack(int n = 4)
{
//_a = new int [n];
_a = (int*)malloc(sizeof(int) * n);
_top = 0;
_capacity = n;
}
void push(int x)
{
//判断,扩容......
_a[_top++] = x;
}
~Stack()
{
//delete[] _a;
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack s;
s.push(1);
s.push(2);
s.push(3);
Stack s2(s);
return 0;
}
这里就出问题了,还发出了警报声。为什么会这样呢?
编译器默认生成的拷贝构造函数,默认拷贝构造函数是按照值拷贝的,即将s中的内容原封不动的拷贝到s2.由此
s和s2指向了同一块内存空间。当程序退出的时候,s和s2都要销毁,s2先销毁,调用析构函数,将一块空间释放了,s销毁时又要再释放一次,这里就矛盾了。s释放的空间已经被释放过了,程序就崩溃了。
所以我们就明白了,如果类中没有涉及饿到资源申请的话,写不写拷贝构造都无所谓,但是一但涉及到资源申请,我们就一定要写拷贝构造。
使用已存在对象创建新对象
函数参数类型为类类型对象
函数返回值类型为类类型对象
看懂了这张图的话,拷贝构造的使用场景也就熟悉了。
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊意义函数名的函数,也具有其返回值类型,函数名以及参数列表,其返回值类型与参数列表与普通函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型operator 操作符(参数列表)
注意:
1.不能通过通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变,比如:内置的整形+,不能改变其含义。
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5..* :: sizeof ?: .
注意以上5个运算符不能重载。
bool operator==(const Date&d)//==的运算符重载
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
来看看是否会调取函数吧。
class Date
{
public:
Date(int year = 1,int month = 1,int day = 1)//缺省
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date&d)//==的运算符重载
{
cout << "operator==(const Date&d)" << endl;
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
~Date()
{
cout << "~Date()"<<endl;
}
private:
//基本类型
int _year;
int _month;
int _day;
//自定义类型
};
int main()
{
Date d1(2023,1,10);
Date d2;
d1 == d2;
return 0;
}
1.赋值运算符重载格式
参数类型:const T(类目名)&,传递引用可以提高传参效率。
返回值类型:T&,返回引用可以提高返回的效率,返回值目的是为了支持连续赋值。
检测是否自己给自己赋值
返回*this:要符合连续赋值意义。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)//缺省
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)//=的运算符重载
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
void print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
//基本类型
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 1, 10);
Date d2;
d2 = d1;
d2.print();
return 0;
}
2.赋值运算符只能重载成类的成员函数不能重载成全局函数
如果我们写在类外,就会出现这种情况
全是报错啊!
这是因为,赋值运算符重载如果不显示实现,编译器会生成一个默认的。那么此时用户还在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,所以赋值运算符重载只能是类的成员函数。
3.用户没有显示实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字拷贝,但是要注意的是:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重新完成拷贝。
注意!
就和上面的拷贝构造一样,如果我们只是写日期类不涉及资源管理,写不写都无所谓,但是涉及资源管理的话就必须要求实现。
这里的问题也是和上面拷贝构造函数类似,赋值会指向同一块的地址,最后销毁会引起程序崩溃。
用一张图片来方便大家理解。
Date& operator++()
{
_day += 1;
return *this;//返回加1后的成果,但是没考虑进位
}
前置++和后置++,都是一元操作符,我们在重载的时候要怎么区分呢?
C++规定:后置++在重载时多增加一个int类型的参数,但调用函数时不用传第,编译器会自动传递。
注意:后置++是先使用再加1,由此需要返回+1前的旧值,故要在实现时先将this保存一份,然后给this加1。
Date operator++(int)
{
Date tmp(*this);
_day += 1;
return tmp;
}
tmp 是临时对象,由此只能以值的形式返回,不能返回引用。
将const修饰的“成员函数”称为const成员函数,const修饰成员函数实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)//缺省
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << "void print()" << endl;
cout << _year << '/' << _month << '/' << _day << endl;
}
void print() const
{
cout << "void print() const" << endl;
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
//基本类型
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 1, 10);
d1.print();
const Date d2(2023, 1, 9);
d2.print();
return 0;
}
这里const对象调用const成员函数,非const对象调用非const成员函数
如果我们只写了const成员函数,那么非const对象也会调用const成员函数。但是如果我们只写了非const成员函数,却用const对象去调用那么在编译阶段系统会报错
这两个默认成员函数一般不用重新定义,编译器默认会生成。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
//基本类型
int _year;
int _month;
int _day;
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取指定的内容。