本文将对C++类和对象中的一些细节包括C++11引进的新特性,如:1. 拷贝、赋值和销毁、2. 隐式类型转换结合代码样例做深入分析
一、拷贝、赋值和销毁
当定义一个类时,我们显示地或隐式地指定此类型的对象拷贝、赋值和销毁做什么。一个类中通过定义五种特殊的成员函数来控制这些操作,包括: 拷贝构造函数、赋值重载、析构函数。这里以我自己实现的string来举例:
1. 拷贝
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数为拷贝构造函数。
//string类的拷贝构造函数
string(const string& s)
:_str(nullptr)
{
cout <<"string(const string& s) -- 拷⻉构造" << endl;
reserve(s._capacity);
for (auto ch: s)
{
push_back(ch);
}
}
拷贝构造函数的特点:
① 拷贝构造函数是构造函数的重载
② 自定义类型对象传值传参和传值返回时会调用拷贝构造
//该函数传值传参和传值返回
Cw::string test1(Cw::string s)
{
return s;
}
int main()
{
Cw::string s1("1111111111");
Cw::string s2 = test1(s1);
//test();
return 0;
}
上述代码采用传值传参和传值返回,在vs 2022编译器优化下调用了两次拷贝构造,但对于传值返回严格来说会构造一个临时对象,然后再调用拷贝构造函数。
因此,拷贝构造函数的形参若是自身类类型的非引用则会引发无穷递归。
③ 若未显式定义拷⻉构造,编译器会⾃动⽣成拷⻉构造函数(称为合成拷贝构造函数)。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造函数。
2. 赋值
赋值运算符重载是一个默认成员函数,用于完成两个已存在的对象直接拷贝赋值,区别与拷贝构造:拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
string& operator=(const string& s)
{
cout <<"string& operator=(const string& s) -- 拷贝赋值" <<endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch: s)
{
push_back(ch);
}
}
return* this;
}
如上代码所示,还是以string类为例子,赋值运算符的特点:
① 赋值运算重载的参数建议写成const 当前类类型引⽤,否则会传值传参会有拷⻉
② 赋值运算符通常返回一个指向其左侧运算对象的引用,引用返回可以提高效率,返回值是为了支持连续赋值场景
③ 与拷贝构造函数一样,如果类为显示定义,编译器会⾃动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认构造函数类似,对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造
3. 销毁
析构函数执行与构造函数相反的操作: 构造函数初始化对象的非static成员变量,还可能做一些其他工作;析构函数释放对象使用的资源,并销毁对象的非static成员变量。
~string()
{
cout << "~string() -- 析构" << endl;
delete[] _str;
_str = nullptr;
}
如上代码所示,析构函数特点;
① 析构函数是类的成员函数,名字由波浪号 + 类名构成,没有返回值,也不接受参数,因此它不能重载。对一个类,只有唯一的析构函数。
② 对象生命周期结束时,编译器会自动调用析构函数
③ 跟构造函数类似,我们不写编译器⾃动⽣成的合成析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数
④ 当自己显示写析构函数,对于自定义类型成员也会调用它的析构函数,因此自定义类型成员变量无论什么情况都会自动调用析构函数
⑤ 一个局部域的多个对象,C++标准规定后定义的先析构
二、隐式类型转换
1. 内置类型
int& a = 1;//报错,字面常量1隐式类型转换成int的临时对象,且具有常性
const int& a = 1;//正确写法,加上const
内置类型走隐式类型转换会产生一个临时对象,该临时对象具有常性,因此对其引用需要加const关键字
2. 自定义类型
class A
{
public:
// 构造函数explicit就不再⽀持隐式类型转换
// explicit A(int a1)
A(int a1)
:_a1(a1)
{}
//explicit A(int a1, int a2)
A(int a1, int a2)
:_a1(a1)
,_a2(a2)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
// 构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa1
// 编译器遇到连续构造+拷⻉构造->优化为直接构造
A aa1 = 1;
aa1.Print();
const A& aa2 = 1;
// C++11之后才⽀持多参数转化
A aa3 = { 2,2 };
return 0;
}
如上代码所示:单参数构造函数会走隐式类型转换,因此字面量1构造了A类型的临时对象,再用这个临时对象拷贝构造aa1。但现代版本的编译器会对这个过程优化为用字面量1直接构造aa2对象。
C++11中 : 新增了列表初始化的规则,采用列表初始化的自定义类型对象同样也会走隐式类型转换构造临时对象,再用这个临时对象拷贝构造,编译器优化后会直接构造。具体代码细节如下:
struct Point
{
int _x;
int _y;
};
class Date
{
public :
Date(int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d)
: _year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 这⾥本质是⽤{ 2025, 1, 1}构造⼀个Date临时对象
// 临时对象再去拷⻉构造d1,编译器优化后合⼆为⼀变成{ 2025, 1, 1}直接构造初始化
Date d1 = { 2025, 1, 1 };
const Date& d2 = { 2024, 7, 25 };//这⾥d2引⽤的是{ 2024, 7, 25 }构造的临时对象
return 0;
}
总结
本文是对C++类和对象中最常用的细节:拷贝、赋值、销毁和隐式类型转换,结合代码样例做系统性的分析,欢迎大家批评指正,谢谢。