1. 六大默认成员函数
默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数。
我们需要从从两方面入手:
第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
第⼆:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?
2. 构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使⽤的局部对象在栈帧创建时,空间就开好了),⽽是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack
和Date类
中写的Init
函数的功能,构造函数⾃动调⽤的特点就完美的替代的了Init
。
构造函数有如下特点:
1. 函数名与类型同名
2. 可以重载
3. 没有返回值(不用写void
)
4. 如果用户没有显式写构造函数,编译器会生成一个默认的无参构造函数,⼀旦⽤⼾显
式定义编译器将不再⽣成。
// 构造函数
// 1. 函数名和类名同名 2. 可以重载 3. 没有返回值 4. 用户不写编译器会默认生成无参的构造函数
class Date
{
public:
// 无参构造
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
// 带参数构造
Date(size_t year, size_t month, size_t day)
{
_year = year;
_month = month;
_day = day;
}
//// 全缺省构造
//Date(size_t year = 1, size_t month = 1, size_t day = 1)
//{
// _year = year;
// _month = month;
// _day = day;
//}
//
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
size_t _year;
size_t _month;
size_t _day;
};
int main()
{
// 调用带参数的构造
Date d1(2025,7,5);
d1.Print();
//// 无参构造和全缺省构造会产生调用歧义
//Date d2;
//d2.Print();
// 无参的不能这么写 会和函数声明搞混 eg: void func
// 这是函数声明还是函数定义呢?
/*Date d2();
d2.Print();*/
//// 如果注释掉无参的构造和全缺省构造,会报错
//// C2512 没有合适的默认构造函数可用
//Date d2;
//d2.Print();
// 调用无参的构造函数
Date d3;
d3.Print();
return 0;
}
默认构造函数分为三类:
- 全缺省构造函数
- 无参构造函数
- 编译器默认生成的构造函数
总结一下:不传参的构造函数就是默认构造函数,这三个函数不能同时存在
而全缺省构造函数和无参构造函数虽然构成函数重载,但是调用时会产生调用歧义
我们不显式写构造函数,编译器默认生成的构造函数会如何处理数据?
using namespace std;
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
private:
size_t _hour;
size_t _minute;
size_t _second;
};
class Date
{
public:
// 不写构造函数 编译器会自动生成默认构造函数
// 对于内置类型 编译器是否处理没有明确要求
// 对于自定义类型 调用该类型的默认构造函数
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
size_t _year;
size_t _month;
size_t _day;
Time _t;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
观察调试结果,我们可以得到如下结论:
对于编译器默认生成的构造函数,处理不同类型数据有不同行为:
- 对于内置类型,编译器没有特别要求,对于
VS
环境,给出随机值 - 对于自定义类型,该类型会调用它默认的构造函数
如果把Time
类的无参构造函数注释掉,会有如下现象:
Time
类调用它的默认构造函数,而Time
类的默认构造函数是编译器生成的,又是处理内置类型,所以VS
不做处理,给出随机值
针对这个问题C++11
打了个补丁:内置类型成员变量在声明时给缺省值,用缺省值初始化
using namespace std;
class Time
{
public:
/*Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}*/
private:
// C++11 在声明时给缺省值
size_t _hour = 1;
size_t _minute = 1;
size_t _second = 1;
};
class Date
{
public:
// 不写构造函数 编译器会自动生成默认构造函数
// 对于内置类型 编译器是否处理没有明确要求
// 对于自定义类型 调用该类型的默认构造函数
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// C++11 在声明时给缺省值
size_t _year = 1;
size_t _month = 1;
size_t _day = 1;
Time _t;
};
int main()
{
// 此时 Time类和Date类只有编译器默认生成的构造函数
Date d1;
d1.Print();
return 0;
}
总结:什么时候要显式定义构造函数?
- 一般情况构造函数都要显式实现
- 只有成员全为自定义类型的类不用显式实现
3. 析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++
规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放工作。析构函数的功能类⽐我们之前Stack
实现的Destroy
功能,⽽像Date
没有Destroy
,其实就是没有资源需要释放,所以严格说Date
是不需要析构函数的
析构函数有如下特点:
1. 函数名和类名相同,在函数名前加~
2. 没有返回值
3. 不能重载,意味着一个类只有一个析构函数
4. 如果用户没有显式写,编译器会默认生成析构函数
5. 对象的生命周期结束,编译器自动调用析构函数
class Stack
{
public:
Stack(size_t n = 4)
{
cout << "Stack(size_t n = 4) 析构" << endl;
_arr = (int*)malloc(sizeof(int) * n);
if (_arr == nullptr)
{
perror("malloc err!");
return;
}
_capacity = n;
_top = 0;
}
~Stack()
{
cout << "~Stack() 析构" << endl;
assert(_arr);
free(_arr);
_arr = nullptr;
_capacity = _top = 0;
}
private:
int* _arr;
int _capacity;
int _top;
};
int main()
{
Stack st1;
return 0;
}
和构造函数一样,如果我们不显式实现析构函数,编译器生成的析构函数对于内置类型不做处理,对于定义类型会调用它的析构函数,值得一提的是,是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数
class tmp
{
public:
~tmp()
{
cout << "~tmp() 析构" << endl;
}
private:
int _num;
};
class Stack
{
public:
Stack(size_t n = 4)
{
cout << "Stack(size_t n = 4) 构造" << endl;
_arr = (int*)malloc(sizeof(int) * n);
if (_arr == nullptr)
{
perror("malloc err!");
return;
}
_capacity = n;
_top = 0;
}
/*~Stack()
{
cout << "~Stack() 析构" << endl;
assert(_arr);
free(_arr);
_arr = nullptr;
_capacity = _top = 0;
}*/
private:
int* _arr;
int _capacity;
int _top;
tmp _t;
};
int main()
{
Stack st1;
return 0;
}
我们可以通过调试观察:
总结:什么时候需要显式实现析构函数?
- 有资源需要清理,就必须写析构函数,例如:
Stack
List
… - 无资源要清理,可以不写
- 内置类型成员没有资源要清理,剩下全是自定义类型,可以不写
还有一个重要的点:一个局部域的多个对象,后定义的先析构
设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
- 类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象
- 全局对象先于局部对象进行构造
- 部对象按照出现的顺序进行构造,无论是否为
static
- 所以构造的顺序为
c a b d
- 析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部 对象之后进行析构
- 因此析构顺序为
B A D C
4. 拷贝构造函数
拷贝构造函数的第一个参数是自身类型的引用,且任何额外的参数都有缺省值,这样的函数叫做拷贝构造函数,用于同类对象的拷贝初始化,是构造函数的重载。
本文以最常规情况的拷贝构造函数展开,即有且仅有一个参数:类类型对象的引用
拷贝构造函数有如下特点:
- 拷贝构造函数是构造函数的一个重载
- 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器会报错(会引发无穷递归调用),拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值
// 拷贝构造函数
// 构造函数的重载,第一个参数必须是类类型对象的引用
// 用于同类对象的拷贝初始化
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(Date& d)
{
cout << "call Date(Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
size_t _year;
size_t _month;
size_t _day;
};
int main()
{
Date d1;
// 两种写法都可以
Date d2 = d1;
// d是d1的别名,d3是this指针
Date d3(d1);
d1.Print();
d2.Print();
d3.Print();
return 0;
}
再来看一段代码:
Date(Date& d)
{
cout << "call Date(Date& d)" << endl;
// 如果不小心写反了会发生什么?
d._year = _year;
d._month = _month;
d._day = _day;
}
其余部分不变
初始的d1
也被修改成随机值了,我们进行拷贝构造,提供拷贝值的对象是不能被修改的,所以为了防止这样的情况发生,我们做如下处理:Date(const Date& d)
保证d
的只读性
// 拷贝构造函数
// 构造函数的重载,第一个参数必须是类类型对象的引用
// 用于同类对象的拷贝初始化
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(const Date& d)
{
cout << "call Date(Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
size_t _year;
size_t _month;
size_t _day;
};
int main()
{
Date d1;
// 两种写法都可以
Date d2 = d1;
// d是d1的别名,d3是this指针
Date d3(d1);
d1.Print();
d2.Print();
d3.Print();
return 0;
}
- 自定义类型对象进行拷贝行为必须调用拷贝构造,所以自定义类型传值传参和传值返回都会调用拷贝构造
// 自定义类型对象进行拷贝行为必须调用拷贝构造
// 自定定义类型传值传参和传值返回都会调用拷贝构造完成
class Date
{
public:
Date(size_t year, size_t month, size_t day)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
cout << "调用 Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
size_t _year;
size_t _month;
size_t _day;
};
void func(Date d)
{
d.Print();
}
int main()
{
Date d1(2025, 7, 9);
func(d1);
return 0;
}
调试看一下函数行为:
而传指针和传引用可避免拷贝构造:
C++
推荐使用传引用的方式,因为引用语义更清晰、不能为 null、更安全也更简洁,适合绝大多数函数参数传递场景,除非参数可以为空或需要修改指针本身,否则优先使用引用传参
为什么传值会引发无穷递归?
- 如果不显式写拷贝构造,编译器会默认生成拷贝构造,⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤它的拷⻉构造
// 如果不显式写拷贝构造,编译器会默认生成拷贝构造,
// 自动⽣成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),
// 对自定义类型成员变量会调用它的拷贝构造
class Stack
{
public:
Stack(size_t n = 4)
{
// cout << "Stack(size_t n = 4) 构造" << endl;
_arr = (int*)malloc(sizeof(int) * n);
if (_arr == nullptr)
{
perror("malloc err!");
return;
}
_capacity = n;
_top = 0;
}
void Push(int data)
{
_arr[_top++] = data;
}
~Stack()
{
// cout << "~Stack() 析构" << endl;
assert(_arr);
free(_arr);
_arr = nullptr;
_capacity = _top = 0;
}
private:
int* _arr;
int _capacity;
int _top;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(1);
st1.Push(1);
Stack st2 = st1;
return 0;
}
崩溃了
完成了拷贝,程序却崩溃了,为什么?我们没有显式写拷贝构造,默认生成的拷贝构造调用栈对象的拷贝构造,进行了浅拷贝。
而深拷贝,会复制一个相同的空间,这样就不会冲突
class Stack
{
public:
Stack(size_t n = 4)
{
// cout << "Stack(size_t n = 4) 构造" << endl;
_arr = (int*)malloc(sizeof(int) * n);
if (_arr == nullptr)
{
perror("malloc err!");
return;
}
_capacity = n;
_top = 0;
}
// 深拷贝
// Stack st2 = st1;
Stack(const Stack& st)
{
_arr = (int*)malloc(sizeof(int) * st._capacity);
if (_arr == nullptr)
{
perror("malloc err!");
return;
}
memcpy(_arr, st._arr, sizeof(int)* st._capacity);
_capacity = st._capacity;
_top = st._top;
}
void Push(int data)
{
_arr[_top++] = data;
}
~Stack()
{
// cout << "~Stack() 析构" << endl;
assert(_arr);
free(_arr);
_arr = nullptr;
_capacity = _top = 0;
}
private:
int* _arr;
int _capacity;
int _top;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(1);
st1.Push(1);
Stack st2 = st1;
return 0;
}
完成深拷贝,二者有自己的独立空间
总结:什么时候需要显式写拷贝构造?
- 若无资源管理,不用显式写拷贝构造,eg:
Date
- 若类的成员变量都是自定义类型,并且内置类型的的成员没有指向的资源,不用显示写拷贝构造
tips: 不显式写析构,就不用写拷贝 - 若类内部有指针或者一些值指向资源,要写析构释放,就需要显式写深拷贝,eg:
Stack
5. 赋值重载
未完待续~