默认成员函数就是用户没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个空类编译器会默认⽣成8个默认成员函数。本文只介绍其中6个,C++11增加两个函数见后续博客。
目录
一、构造函数
1.1 概念
构造函数是一个特殊的成员函数,名字与类名相同,不写返回值。创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
1.2 特性
- 函数名与类名相同。⽆返回值。 (返回值啥都不需要给,也不需要写void)
- 构造函数可以重载。分为无参构造函数,带参构造函数,全缺省构造函数
class Date { public: // 1. ⽆参构造函数 Date() { _year = 1; _month = 1; _day = 1; } // 2. 带参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } //3. 全缺省构造函数 Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
- 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦用户显式定义编译器将不再⽣成。
- 默认构造函数:⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数。这三个函数有且只有⼀个存在,不能同时存在。不传实参就可以调⽤的构造就叫默认构造。
- 用户不写,编译器默认⽣成的构造,对内置类型成员变量的初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错。
1.3 使用举例
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, 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 d1; // 调⽤默认构造函数
Date d2(2005, 6, 8); // 调⽤带参的构造函数
d1.Print();
d2.Print();
return 0;
}
1.4 初始化列表
1.4.1 概念
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(1)
{}
private:
int _year;
int _month;
int _day;
};
如何理解初始化列表?
借助C语言的特性:在private中可以看作是变量的声明,如果用之前在函数体中的构造,我们可以发现缺少了变量的定义。事实上如果我们不写初始化列表,编译器会自动生成初始化列表。
由此可知,初始化列表其实是每个成员变量定义初始化的地⽅
为什么会有初始化列表?
有些变量比如:const变量,还有引用----要求在定义时初始化,仅仅依靠之前在函数体内初始化的方式是无法对这些变量进行初始化的,所以初始化列表至关重要。
1.4.2 特性
- 引⽤成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始化,否则会编译报错。
- C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的。
- 在初始化列表的成员-显式写;不在初始化列表的成员:a.声明的地方有缺省值用缺省值 b.没有缺省值:内置类型看编译器处理,自定义类型调用默认构造。如果没有默认构造会报错。
- 初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致。
/*试分析下列程序*/ #include<iostream> using namespace std; class test { public: test(int a) :_t1(a) , _t2(_t1) {} void Print() { cout << _t1 << " " << _t2 << endl; } private: int _t2 = 2; int _t1 = 2; }; int main() { test t(1); t.Print(); }
由于初始化列表是按照声明顺序初始化的,先初始化_t2,此时_t1没有初始化为随机值,所以结果如下
#include<iostream>
using namespace std;
class Test
{
public:
Test(int a)
:_t(a)
{}
private:
int _t;
};
class Date
{
public:
Date(int& x, int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
/*, _t(12)
, _ref(x)
, _n(1)*/
{}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
Test _t; // 没有默认构造
int& _ref; // 引⽤
const int _n; // const
};
int main()
{
int i = 0;
Date d1(i);
d1.Print();
return 0;
}
1.4.3 使用
由于每个构造函数都有初始化列表,建议以后都采用初始化列表的方式进行构造函数的初始化,使用中建议建议声明顺序和初始化列表顺序保持⼀致。
二、析构函数
2.1 概念
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
2.2 特性
- 析构函数名是在类名前加上字符~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 析构函数不能重载。编译器自动调用
- 不写编译器会自动生成默认析构函数
2.3 使用
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
Date类; - 有资源申请时,必须要写,否则会造成资源泄漏,比如Stack类。
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_arr = (int*)malloc(sizeof(int) * capacity);
if (_arr == NULL)
{
perror("malloc");
exit(1);
}
_capacity = _capacity;
_size = 0;
}
~Stack()
{
if (_arr)
{
free(_arr);
_arr = NULL;
_capacity = _size = 0;
}
}
private:
int* _arr;
int _capacity;
int _size;
};
三、拷贝构造
3.1 概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存
在的类类型对象创建新对象时由编译器自动调用。
为什么必须是引用?
由C语言相关知识我们可以知道:传值调用实际上是传了当前变量的拷贝。所以如果拷贝构造函数的参数为传值调用,会引发无限的调用递归。所以要用传引用调用
3.2 特性
- 拷⻉构造函数是构造函数的⼀个重载。
- C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。
- 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。
浅拷贝:默认的浅拷贝仅复制指针的地址,两个对象共享同一块动态内存。如果一个对象释放了内存,另一个对象会出现悬空指针的问题。适用于对象不包含动态内存或对动态内存的共享是可接受的情况。
深拷贝:通过手动实现深拷贝构造函数和赋值运算符,确保每个对象都有自己独立的动态内存,避免悬空指针和共享内存的问题。适用于对象包含动态内存且需要独立内存空间的情况。
3.3 使用
有以下三种情况
- 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。
- 像Stack这样的类,虽然也都是内置类型,但是指向了资源,编译器⾃动⽣成的拷⻉构造完成的浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉
- 像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器自动生成的拷贝构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现MyQueue的拷⻉构造。
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_arr = (int*)malloc(sizeof(int) * capacity);
if (_arr == NULL)
{
perror("malloc");
exit(1);
}
_capacity = _capacity;
_size = 0;
}
~Stack()
{
if (_arr)
{
free(_arr);
_arr = NULL;
_capacity = _size = 0;
}
}
Stack(const Stack& s)
{
_arr = (int*)malloc(sizeof(int) * s._capacity);
if (_arr == NULL)
{
perror("malloc");
exit(1);
}
_capacity = s._capacity;
_size = s._size;
memcpy(_arr, s._arr, sizeof(int) * s._size);
}
private:
int* _arr;
int _capacity;
int _size;
};
注意事项:
传值返回会产⽣⼀个临时对象调⽤拷⻉构造。
传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。
但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。
四、赋值运算符重载
4.1 特性
- 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引⽤,否则会传值传参会有拷⻉。
- 有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了支持连续赋值场景。
- 没有显式实现时,编译器会⾃动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认构造函数类似,对内置类型成员变量会完成浅拷⻉,对⾃定义类型成员变量会调⽤他的拷⻉构造。
4.2 使用
与拷贝构造函数使用相同,分为三类
- 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的赋值运算符重载就可以完成需要的拷⻉,所以不需要我们显⽰实现赋值运算符重载。
- 像Stack这样的类,虽然也都是内置类型,但是指向了资源,编译器⾃动⽣成的赋值运算符重载完成的浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉。
- 像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的赋值运算符重载会调⽤Stack的赋值运算符重载,也不需要我们显⽰实现MyQueue的赋值运算符重载。
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_arr = (int*)malloc(sizeof(int) * capacity);
if (_arr == NULL)
{
perror("malloc");
exit(1);
}
_capacity = _capacity;
_size = 0;
}
~Stack()
{
if (_arr)
{
free(_arr);
_arr = NULL;
_capacity = _size = 0;
}
}
Stack(const Stack& s)
{
_arr = (int*)malloc(sizeof(int) * s._capacity);
if (_arr == NULL)
{
perror("malloc");
exit(1);
}
_capacity = s._capacity;
_size = s._size;
memcpy(_arr, s._arr, sizeof(int) * s._size);
}
Stack& operator=(const Stack& s)
{
//检查是否自己给自己赋值
if (this != &s)
{
_arr = (int*)malloc(sizeof(int) * s._capacity);
if (_arr == NULL)
{
perror("malloc");
exit(1);
}
_capacity = s._capacity;
_size = s._size;
memcpy(_arr, s._arr, sizeof(int) * s._size);
}
}
private:
int* _arr;
int _capacity;
int _size;
};
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// d1 = d2 表达式的返回对象应该为d1 ,也就是*this
return *this;
}
private:
int _year;
int _month;
int _day;
};
4.3 与拷贝构造的区别
- 赋值重载完成两个已经存在的对象直接的拷⻉赋值
- 拷⻉构适用于⼀个对象拷⻉初始化给另⼀个要创建的对象(还没创建)
int main()
{
Date d1(2024, 7, 5);
Date d2(d1);//拷贝构造
Date d3(2024, 7, 6);
d1 = d3;//赋值运算符重载
Date d4 = d1;//拷贝构造
return 0;
}
五、取地址操作符重载
5.1 cosnt
- 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后⾯。
- const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。
//以Date类为例 //this指针由 Date* const this 变为 const Date* const this class Date { public: void Print() const { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; };
5.2 重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动⽣成的就足够使用了,不需要去显⽰实现。
六、操作符重载补充
具体应用实现详见:
6.1 前置++与后置++
重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。所以C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。
6.2 流操作符重载
重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象<<cout,不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。