🎈个人主页:🎈 :✨✨✨初阶牛✨✨✨
🐻推荐专栏1: 🍔🍟🌯C语言初阶
🐻推荐专栏2: 🍔🍟🌯C语言进阶
🔑个人信条: 🌵知行合一
🍉本篇简介:>:讲解C++中有关类和对象的介绍,本篇是中篇的第结尾篇文章,讲解拷贝构造,运算符重载以及取地址重载符.
金句分享:
✨别在最好的年纪,辜负了最好的自己.✨
一、“拷贝构造函数”
拷贝构造函数:
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
2.1 自动生成的"拷贝构造函数"
假设哦我们需要创建两个一模一样的对象A
和B
.
那我们可以先创建一个对象A
,再通过将A
作为参数,传给B
进行初始化,
即一个自定义类型实例化出的对象(B
)用另一个该类型实例化出的对象(A
)进行初始化.
class Date
{
public:
Date(int year = 2020, int month = 1, int day = 1)//全缺省构造函数
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date A(2023, 7, 20);
A.Print();
printf("\n");
Date B(A);//会调用系统生成的拷贝构造
B.Print();
return 0;
}
运行结果:
2023-7-20
2023-7-20
其实拷贝构造函数就是构造函数的一种重载形式,他也是六大天选之子之一,没有显式定义时,编译器也会自动生成,但是只会完成"浅拷贝
"(下面讲)…
2.2 自定义"拷贝构造函数"
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
class Date
{
public:
Date(int year = 2020, int month = 1, int day = 1)//全缺省构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)//拷贝构造函数
{
cout << "拷贝构造" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 7, 20);
d1.Print();
printf("\n");
Date d2(d1);
d2.Print();
return 0;
}
我们发现Date(const Date& d)
这里使用了引用传参,如果直接传参会怎样呢?
为什么会报错呢?
void test(int a)
{
}
void test(Date d1)
{
}
int main()
{
Date d1(2023, 7, 20);
test(2);
test(d1);
return 0;
}
这段代码会调用Date
类的拷贝构造.
对于自定义类型作为参数时,必须调用该类型的拷贝构造函数.
所以可以回答上面的问题了.
所以拷贝构造函数传参时采用引用传参,这样就避免了传参时调用拷贝构造.
2.3 深浅拷贝?
前面在介绍编译器自动生成的"拷贝构造函数"时,提到了浅拷贝,那什么是浅拷贝呢?
浅拷贝:按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
深拷贝:
示例:
栈类中没有显式定义拷贝构造函数,编译器自动生成的拷贝构造是浅拷贝带来的问题.
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
typedef int DataType;
class Stack
{
public:
Stack(int capacity=5)//全缺省构造函数
{
cout << "Stack" << endl;
_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;
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
void CheckCapacity()
{
if (_size == _capacity)
{
int newcapacity = _capacity * 2;
DataType* temp = (DataType*)realloc(_array, newcapacity *
sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
_array = temp;
_capacity = newcapacity;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);//这条语句会报错.
return 0;
}
运行结果:
原因:
因为编译器默认生成的拷贝构造是浅拷贝,这里两个对象的_array
也就指向了同一块内存空间,但是两个对象的声生命周期结束时,会调用各自的析构函数,这也就导致对同一块空间进行了释放操作.
解决方法:
显示定义一个拷贝构造函数.
Stack(const Stack& S)//深拷贝
{
_array = (int*)malloc(sizeof(int) * S._capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
memcpy(S._array,_array,sizeof(int)*S._size);
_capacity = S._capacity;
_size = S._size;
}
总结:
拷贝构造使用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
- 当一个对象作为参数传递给函数时,拷贝构造函数会被调用来创建一个新的对象,该新对象与传递的对象具有相同的属性和属性值,但是它们在内存中是独立的。
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝.
二、赋值运算符重载(“=”)
2.1 运算符重载的介绍
class Date//日期类
{
public:
Date(int year = 2023, int month = 10, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void test1()
{
Date d1(2023, 7, 28);
Date d2;
if (d2 == d1)
{
cout << "d1=d2";
}
if (d1 < d2)
{
cout << "d1<d2";
}
}
自定义类型是无法像内置类型一样比较大小和使用一些常规运算符的.
为什么呢?
因为自定义类型是用户自己定义的,编译器不知道该如何进行比较.那编译器太笨了吧,日期按 年-月-日依次比较不就行了?
个人理解:
- 格局打开,如果是别的类呢?比如:
person
是按名字还是按职位,还是按什么?你不告诉编译器如何比较,编译器也很无奈,不敢瞎搞的. - 编译器咋知道你
year
是年,要是牛牛用nian
来命名,他也能识别出来是年吗?
综上,自定义类型如何进行运算比较,只有用户自己知道,所以用户需要自己来设计规则.
C++
为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型.
函数名:关键字operator
+需要重载的运算符符号。
operator+ 需要重载的运算符
注意事项:
-
不能通过连接其他符号来创建新的操作符:
示例:operator@
-
重载操作符必须有一个类类型参数
运算符重载是通过类的成员函数或全局函数来实现的,而这些函数必须具有特定的参数列表。
对于成员函数的重载操作符,至少需要一个类类型参数,它表示操作符的左操作数。例如,对于二元操作符(如 +、-、* 等),成员函数的参数列表通常还包括一个非常量引用或常量引用,表示操作符的右操作数。 -
用于内置类型的运算符,其含义不能改变:
例如:内置的整型*
不要实现为了/
,害人是不对的. -
作为类成员函数重载时,其形参看起来比操作数数目少1一个,因为成员函数的第一个参数为隐藏的this .
-
注意以下5个运算符不能重载。“
.*
” (点星) 、"::
"sizeof
? :
.
在C++中,有一些操作符是不能被重载的,包括以下几种情况:
-
::
(作用域解析操作符):作用域解析操作符用于指定命名空间、类或结构的作用域,并访问其成员。它不能被重载,因为它的含义在语言中已经固定不可更改。 -
.*
(指针到成员操作符)和->*
(指向成员指针的操作符):这些操作符用于访问类的成员指针。它们存储了一个指向类成员的指针,并用于在运行时访问该成员。它们也不能被重载。 -
sizeof
(大小操作符):sizeof
操作符用于获取一个对象或类型的大小(以字节为单位)。它是一个编译时的操作符,不能在运行时被重载。因为在编译时就已经确定了对象或类型的大小。 -
?
:(条件操作符,即三目运算符):条件操作符是一个三元操作符,用于根据条件选择不同的表达式。它不能被重载,因为它的语法和含义已经在语言中定义好了。 -
.
在C++
中,点操作符(“.
”)是用来访问对象的成员的,而它本身是不能被重载的。点操作符的行为在语言中是固定的,无法通过重载来改变。
2.2 赋值运算符重载:
(1)编译器自动生成的 “赋值运算符重载”
class Date//日期类
{
public:
Date(int year = 2023, int month = 10, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void test1()
{
Date d1(2023, 7, 28);
Date d2;
d1.print();
d2.print();
cout << endl;
d2 = d1;
d1.print();
d2.print();
}
int main()
{
test1();
return 0;
}
赋值运算符只能重载成类的成员函数不能重载成全局函数:
原因:
赋值运算符如果不显式实现(自己定义),编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
那编译器会生成一个默认赋值运算符重载会做什么事情呢?
以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
当然对于日期类这种只需要浅拷贝的类来说,编译器默认生成就已经足够了,但是像stack
类,同样引发深浅拷贝的问题.
三、最后的两个天选之子
哈哈哈,期待到最后的两个默认成员函数其实没什么要讲解的.
- 取地址操作符重载
operator&()
- const取地址操作符重载
operator&()const
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可除非你想搞点特殊的,返回一个特定的特殊地址.
本篇内容到此讲解完了,后续介绍日期类
的具体实现,方便大家更好的理解类和对象的知识,实战才能锻炼水平哦.