目录
💕1.类的默认成员函数
💕2.构造函数
💕3.析构函数
💕4.缺省值
💕5.拷贝构造函数
(最新更新时间——2025.1.14)
这世间没有绝境
只有对处境绝望的人
💕1.类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。⼀个类,我们什么都不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可
💕2.构造函数
在C++中,构造函数是特殊的成员函数,这种函数的意义是在对象实例化时初始化对象,替代我们之前所写的Init函数,并且构造函数可以自动调用,构造函数⾃动调⽤的特点就完美的替代的了Init
构造函数该如何书写?
构造函数的特点:(可以不看,后面逐一讲解)
1. 函数名与类名相同。
2. 无返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
3. 对象实例化时系统会⾃动调⽤对应的构造函数。
4. 构造函数可以重载,但不推荐,推荐构造函数全写成全缺省。
5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦用户显式定义编译器将不再⽣成。
6. ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调用的构造就叫默认构造。
7. 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。
8.对于类中的自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。
我们接下来会逐一讲解->:
A.首先注意构造函数的写法,函数名与类名相同,无返回值。这也是1,2点所讲的
B.对象实例化时系统会⾃动调⽤对应的构造函数,如图,我们在写student s1时,没有调用无参构造函数,但它也运行了无参构造函数,说明对象实例化时会自动调用对应的构造函数
同时,构造函数分为无参构造函数和有参构造函数,
C:构造函数可以重载,但不推荐,推荐构造函数全写成全缺省
我们如图所见,可以看到,s1报错了,为什么?其实这就是因为函数的重载,我们可以看到,在写构造函数时,我们创建了一个全缺省函数,这就会导致调用歧义,在对象实例化时,s1不知道该调用哪个构造函数,因为两个构造函数都可以不传参
D:如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦用户显式(也叫自己写)定义编译器将不再生成。
但是,说是会自动生成一个无参的构造函数,但是其实这个无参的构造函数对内置类型是不做处理的,什么意思?
编译器生成的构造函数不会对类中的内置类型/基本类型(int,char,…指针)处理
它只会对类中的自定义类型处理(class/struct)类型,什么意思,我们来分析下面的代码->:
#include<iostream> #include<stdlib.h> using namespace std; //A类 class A { public: A() { _a = 0; cout << "A()" << ' ' << endl; } private: int _a; }; //Time类 class Time { private: int _hour; int _minute; int _second; A _aa; }; //日期类 class Date { private: int _year; int _month; int _day; // 自定义类型调用默认构造函数 Time _t; }; int main() { Date d2; return 0; }
我们首先自定义一个Date类对象d2,对象d2没有构造函数,那么系统就会自定义生成一个构造函数,系统自动生成的函数不会对类中的基本类型进行处理,但是会对自定义类型Time _t进行处理,这里的对自定义类型的处理就意味着会调用Time _t 的构造函数,那么对Time _t进行处理时,发现Time类也没有构造函数,那么Time类就会生成一个编译器自动生成的默认构造函数,Time类的默认构造函数并不会处理Time类的基本类型,而是去处理Time类中的自定义类型,也就是A_aa,处理A_aa时,就会调用A类的构造函数,发现A类中有构造函数,那么就不会生成默认构造函数,也就是调用我们所写的A类中的构造函数,也就会打印出字符串 "A()"
总结->:
编译器自动生成的构造函数:
对于内置类型成员变量,没有规定要不要处理(有些编译器会处理)
对于自定义类型的成员变量才会调用它的不传参就可以调用的函数(包括全缺省函数)
E : 无参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。如图->:
全缺省构造函数与无参构造函数不能同时存在,因为会存在调用歧义,除非你传一个值,而如果两者都不写编译器就会自动生成一个默认构造函数,所以三者不能同时存在
F:对于类中的自定义类型成员变量,编译器会优先调用这个成员变量的默认构造函数初始化
如图所见,先调用的是Time类的构造函数
💕3.析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放⼯作
析构函数的特点:
1. 析构函数名是在类名前加上字符 ~
2. ⽆参数⽆返回值 (这⾥跟构造类似,也不需要加void)
3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数
4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数
5. 跟构造函数类似,我们不写,编译器会自动生成析构函数,自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数
6. 还需要注意的是我们显示写的析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调⽤析构函数(与构造函数相同)
我们举一个代码的例子
#include<iostream> #include<stdlib.h> using namespace std; class student { public: ~student() { if (pa != nullptr) { free(pa); } } int _age; int _number; int* pa; }; int main() { student s1; s1.pa = (int*)malloc(sizeof(100)); }
我们实例化对象s1,向s1中的pa开辟100个字节,然后再在类中写一个析构函数,这样在对象的生命周期结束时,编译器就会紧接着自动运行类中的析构函数,各位可以自己复制代码调试一下,即使我们不去主动调用它,编译器也会自动调用的
💕4.缺省值
在C++中,引入了缺省值的存在,缺省值的存在是为了弥补编译器自动生成的默认构造函数不会对内置类型初始化的补丁,代码如下->:
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: // 给缺省值 int _year = 1; int _month = 1; int _day = 1; }; int main() { Date d2(2024, 4, 9); d2.Print(); return 0; }
我们可以看到,在类的基本类型中我们给它进行了缺省值的存在,为什么叫缺省值不叫初始化呢?
因为类中的成员变量属于声明,并没有定义出实例化,所以在C++中规定为给缺省值而不是进行初始化
💕5.拷贝构造函数
拷贝构造函数的定义->:
1.拷贝构造函数是构造函数的一种重载
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,如果使用传值方式编译器会直接报错,因为会引发无穷递归调用
3.若未显示定义,编译器会生成默认的拷贝构造函数,默认的拷贝构造函数按内存存储,按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
浅谈一下浅拷贝->:
浅拷贝是指在复制对象时,对于对象中的数据成员,基本数据类型会直接复制其值,而对于指针类型的数据成员,只会复制指针的值(即内存地址),而不会复制指针所指向的实际数据。
我们接下来进行逐一讲解->:
我们先举一个简单的构造函数例子->:
#include<iostream> #include<stdlib.h> using namespace std; class Date { public: Date(int year = 2000,int month = 1,int day = 1) { _year = year; _month = month; _day = day; cout << "Date(int year = 2000,int month = 1,int day = 1)" << endl << _year << ' ' << _month << ' ' << _day << endl; } //拷贝构造函数 Date(Date& s) { _year = s._year; _month = s._month; _day = s._day; cout <<"Date(Date& s)" <<endl<< _year << ' ' << _month << ' ' << _day << endl; } public: int _year; int _month; int _day; }; int main() { Date s1(2025, 1, 14); Date s2(s1); Date s2 = s1;//这也是拷贝构造 }
为什么说拷贝构造函数是构造函数的一种重载,如代码所见,拷贝构造函数与构造函数的写法只有传入的值不同,拷贝构造函数需要传入对象的别名
在创造对象s2时,如果要调用拷贝构造函数,则编译器不会再进行s2的构造函数,也就是说直接调用拷贝构造函数而不去调用构造函数
提出一个疑问?编译器自动生成的拷贝构造函数是什么样的?
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; cout << "Date(int year = 2000,int month = 1,int day = 1)" << endl << _year << ' ' << _month << ' ' << _day << endl; } //拷贝构造函数 /*Date(Date& s) { _year = s._year; _month = s._month; _day = s._day; cout << "Date(Date& s)" << endl << _year << ' ' << _month << ' ' << _day << endl; }*/ public: int _year; int _month; int _day; }; int main() { Date s1(2025, 1, 14); Date s2(s1); cout << endl; s2._year = 2000; cout << s1._year << ' ' << s1._month << ' ' << s1._day << endl;//输出2025 1 14 cout << s2._year << ' ' << s2._month << ' ' << s2._day << endl;//输出2000 1 14 //s2的值的改变并不会改变s1的值,间接说明所以说明s2与s1的空间是不同的 }
我们这里把显现的拷贝构造函数注释掉了,那么编译器就会生成默认的拷贝构造函数,默认的拷贝函数依旧会对基本类型进行拷贝,也就是说明s2依旧是s1拷贝出来的,我们进行s2数据的改变,s1的数据不会变,这也说明了s2与s1的内存空间是不同的
第2个问题-:>:
为什么说拷贝构造函数的参数只有一个且必须是类类型对象的引用,如果使用传值方式编译器会直接报错,因为会引发无穷递归调用
我们可以作图来思考一下直接传对象的情况(代码如下->:)
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; cout << "Date(int year = 2000,int month = 1,int day = 1)" << endl << _year << ' ' << _month << ' ' << _day << endl; } //拷贝构造函数直接传值(这里编译器会直接报错) Date(Date s) { _year = s._year; _month = s._month; _day = s._day; cout << "Date(Date& s)" << endl << _year << ' ' << _month << ' ' << _day << endl; } public: int _year; int _month; int _day; }; int main() { Date s1(2025, 1, 14); Date s2(s1); cout << endl; }
在C++中规定了,类类型的传值传参要优先调用该对象的拷贝构造函数,再去传给参,那么当我们不利用引用作为参数时,就会形成如下图的样子
每一次传值传参都要调用它的拷贝构造函数,那么这里就会变成一个无穷递归,也就是死循环,所以拷贝构造函数要将对象的引用作为形参
第3个问题->:
浅拷贝更清晰的区分->;
我们首先用类来简单模拟一个错误的栈->:
#include<iostream> #include<stdlib.h> using namespace std; typedef int DataType; class Stack { public: Stack(size_t capacity = 3) { cout << "Stack(size_t capacity = 3)" << endl; _array = (DataType*)malloc(sizeof(DataType) * capacity); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; } _capacity = capacity; _size = 0; } // Stack st2 = st1; Stack(const Stack& st) { _array = st._array; _size = st._size; _capacity = st._capacity; } void Push(DataType data) { // CheckCapacity(); _array[_size] = data; _size++; } private: DataType* _array; int _capacity; int _size; }; int main() { Stack st1; st1.Push(1); st1.Push(2); // 拷贝构造 Stack st2(st1); return 0; }
我们提到过浅拷贝是指在复制对象时,对于对象中的数据成员,基本数据类型会直接复制其值,而对于指针类型的数据成员,只会复制指针的值(即内存地址),而不会复制指针所指向的实际数据。
那么执行Stack st2(st1)时,st2与st1中的array所指向的地址,其实是一样的,这就会导致内存重复,如图->:
还需要注意的是,如果st1与st2的_array所指向的内存地址相同,如果在类中加入一个析构函数,就会导致同一块动态内存析构两次,就会报错
正确的写法如下->:
#include<iostream> #include<stdlib.h> using namespace std; typedef int DataType; class Stack { public: Stack(size_t capacity = 3) { cout << "Stack(size_t capacity = 3)" << endl; _array = (DataType*)malloc(sizeof(DataType) * capacity); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; } _capacity = capacity; _size = 0; } // Stack st2 = st1; Stack(const Stack& st) { _array = (DataType*)malloc(sizeof(DataType) * st._capacity); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; } memcpy(_array, st._array, sizeof(DataType) * st._size); _size = st._size; _capacity = st._capacity; } void Push(DataType data) { // CheckCapacity(); _array[_size] = data; _size++; } bool Empty() { return _size == 0; } DataType Top() { return _array[_size - 1]; } void Pop() { --_size; } // 其他方法... ~Stack() { cout << "~Stack()" << endl; if (_array) { free(_array); _array = NULL; _capacity = 0; _size = 0; } } private: DataType* _array; int _capacity; int _size; }; int main() { Stack st1; st1.Push(1); st1.Push(2); // 拷贝构造 Stack st2(st1); return 0; }