如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下 6 个默认成员函数。默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数
一、构造函数
1.1概念class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Init(2023, 7, 20); d1.Print(); Date d2; d2.Init(2023, 7, 21); d2.Print(); return 0; }
对于Date 类,可以通过成员函数 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?构造函数 是一个 特殊的成员函数,名字与类名相同, 创建类类型对象时由编译器自动调用 ,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次
1.2构造函数的特性
构造函数 是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象 。其特征如下:1. 函数名与类名相同。2. 无返回值。3. 对象实例化时编译器 自动调用 对应的构造函数。4. 构造函数可以重载。日期类class Date { public: Date()//1、函数名与类名相同 2、无返回值 { cout << "调用了Date()" << endl;//3、对象实例化时编译器自动调用 _year = 1970; _month = 1; _day = 1; } Date(int year, int month, int day)//4、构造函数的重载(参数个数不同) { cout << "调用了Date(int year, int month, int day)" << endl; _year = year; _month = month; _day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print(); Date d2(2023, 7, 21);//给有参数的构造函数传参形成类对象的代码格式 d2.Print(); //Date d3()//这种写法是错误的,编译器无法区分这种写法是定义一个无参的对象还是一个函数的声明 //d3.Print(); return 0; }
Date(int year = 1970, int month = 1, int day = 1) { cout << "调用了Date()" << endl; _year = year; _month = month; _day = day; }//将上面两个构造函数合并 int main() { Date d1(2023, 7, 21); d1.Print(); Date d2(2023, 7); d2.Print(); Date d3(2023); d3.Print(); Date d4; d4.Print(); return 0; }
栈类
class stack { public: // 成员函数 //stack()//栈的构造函数 //{ // a = nullptr; // top = capacity = 0; //}//这种写法的问题是,如果要开辟一个比较大的栈,需要多次relloc扩容 Stack(size_t n = 4)//优化,不传参默认开辟4个,也可以传参,指定开辟的栈的大小 { if (n == 0) { a = nullptr; top = capacity = 0; } else { a = (int*)malloc(sizeof(int) * n); if (a == nullptr) { perror("realloc fail"); exit(-1); } top = 0; capacity = n; } } void Push(int x) { if (top == capacity) { size_t newcapacity = capacity == 0 ? 4 : capacity * 2; int* tmp = (int*)realloc(a, sizeof(int) * newcapacity); if (tmp == nullptr) { perror("relloc fail"); exit(-1); } a = tmp; capacity = newcapacity; } a[top++] = x; } void Pop() { assert(top > 0); top--; } int Top() { return a[top - 1]; } void Destory() { free(a); a = nullptr; top = capacity = 0; } bool Empty() { return top == 0; } private: // 成员变量 int* a; int top; int capacity; }; int main() { stack st1; st1.Push(0); st1.Push(1); st1.Push(2); st1.Push(3); while (!st1.Empty()) { cout << st1.Top() << " "; st1.Pop(); } cout << endl; st1.Destory(); stack st2(100); for (size_t i = 0; i < 100; i++) { st2.Push(i); } while (!st2.Empty()) { cout << st2.Top() << " "; st2.Pop(); } st2.Destory(); return 0; return 0; }
定义栈对象时调用了构造函数
构造函数,也是默认成员函数,我们不写,编译器会自动生成
编译生成的默认构造的特点:
1、我们不写才会生成,我们写了任意一个构造函数就不会生成了
2、内置类型的成员不会处理(C++11,声明支持给缺省值)
3、自定义类型的成员才会处理,会去调用这个成员的默认构造函数
4、无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
总结:一般情况都需要我们自己写构造函数,决定初始化方式
成员变量全是自定义类型,可以考虑不写构造函数
二、析构函数
2.1概念
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
2.2析构函数的特性
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
5.与构造函数类似,默认的析构函数对内置类型成员不会处理,对自定义类型成员会调用这个成员的析构函数
6.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如stack类
class stack { public: // 成员函数 stack(size_t n = 4)//优化,不传参默认开辟4个,也可以传参,指定开辟的栈的大小 { if (n == 0) { a = nullptr; top = capacity = 0; } else { a = (int*)malloc(sizeof(int) * n); if (a == nullptr) { perror("realloc fail"); exit(-1); } top = 0; capacity = n; } } ~stack() { cout << "调用了析构函数~Stack()" << endl; free(a); a = nullptr; top = capacity = 0; } void Push(int x) { if (top == capacity) { size_t newcapacity = capacity == 0 ? 4 : capacity * 2; int* tmp = (int*)realloc(a, sizeof(int) * newcapacity); if (tmp == nullptr) { perror("relloc fail"); exit(-1); } a = tmp; capacity = newcapacity; } a[top++] = x; } void Pop() { assert(top > 0); top--; } int Top() { return a[top - 1]; } void Destory() { free(a); a = nullptr; top = capacity = 0; } bool Empty() { return top == 0; } private: // 成员变量 int* a; int top; int capacity; }; int main() { stack st1; stack st2; return 0; }
st2先析构
st1再析构
析构按照后定义先析构
三、拷贝构造函数
引例:
class stack { public: // 成员函数 stack(size_t n = 4)//优化,不传参默认开辟4个,也可以传参,指定开辟的栈的大小 { if (n == 0) { a = nullptr; top = capacity = 0; } else { a = (int*)malloc(sizeof(int) * n); if (a == nullptr) { perror("realloc fail"); exit(-1); } top = 0; capacity = n; } } ~stack() { cout << "调用了析构函数~Stack()" << endl; free(a); a = nullptr; top = capacity = 0; } void Push(int x) { if (top == capacity) { size_t newcapacity = capacity == 0 ? 4 : capacity * 2; int* tmp = (int*)realloc(a, sizeof(int) * newcapacity); if (tmp == nullptr) { perror("relloc fail"); exit(-1); } a = tmp; capacity = newcapacity; } a[top++] = x; } void Pop() { assert(top > 0); top--; } int Top() { return a[top - 1]; } void Destory() { free(a); a = nullptr; top = capacity = 0; } bool Empty() { return top == 0; } private: // 成员变量 int* a; int top; int capacity; }; void func2(stack s)× { ; } void func2(stack& s)√//改为形参改为引用型 { ; } int main() { stack s1; func2(s1); return 0; }
类对象的拷贝参数是必须是同类类对象的引用,使用值传递的方式在函数结束后会引发两次析构从而发生错误。
若想要改变s的同时不改变s1,那么就引出第三个默认成员函数,拷贝构造函数
3.1概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建对象时由编译器自动调用
3.2特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须是同类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
class Date { public: Date(int year = 1970, int month = 1, int day = 1) { cout << "调用了Date()" << endl; _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: int _year; int _month; int _day; }; class stack { public: // 成员函数 stack(size_t n = 4)//优化,不传参默认开辟4个,也可以传参,指定开辟的栈的大小 { if (n == 0) { a = nullptr; top = capacity = 0; } else { a = (int*)malloc(sizeof(int) * n); if (a == nullptr) { perror("realloc fail"); exit(-1); } top = 0; capacity = n; } } stack(const stack& s) { cout << "调用了拷贝构造Stack(const stack& s)" << endl; a = (int*)malloc(sizeof(int) * s.capacity); if (a == nullptr) { perror("realloc fail"); exit(-1); } memcpy(a, s.a, sizeof(int) * s.capacity); top = s.top; capacity = s.capacity; } ~stack() { cout << "调用了析构函数~Stack()" << endl; free(a); a = nullptr; top = capacity = 0; } void Push(int x) { if (top == capacity) { size_t newcapacity = capacity == 0 ? 4 : capacity * 2; int* tmp = (int*)realloc(a, sizeof(int) * newcapacity); if (tmp == nullptr) { perror("relloc fail"); exit(-1); } a = tmp; capacity = newcapacity; } a[top++] = x; } void Pop() { assert(top > 0); top--; } int Top() { return a[top - 1]; } void Destory() { free(a); a = nullptr; top = capacity = 0; } bool Empty() { return top == 0; } private: // 成员变量 int* a; int top; int capacity; }; int main() { Date d1(2023, 9, 12); Date d2(d1); //Date d2 = d1;等价写法 stack s1; stack s2(s1); return 0; }
3.若未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
由此可知,日期类不需要写拷贝构造函数,默认生成的浅拷贝的拷贝构造函数就够用,而栈类型需要我们实现深拷贝的拷贝构造函数,默认生成的会出现问题
四、赋值运算符重载
4.1运算符重载C++ 为了增强代码的可读性引入了运算符重载 , 运算符重载是具有特殊函数名的函数 ,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字 operator 后面接需要重载的运算符符号 。函数原型: 返回值类型 operator 操作符 ( 参数列表 )注意:1.不能通过连接其他符号来创建新的操作符:比如 operator@2.重载操作符必须有一个类类型参数3.用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义4.作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐藏的 this.* 、:: 、 sizeof 、 ? :(三目运算符) 、 . 注意以上 5个运算符不能重载。class Date { public: Date(int year = 1970, int month = 1, int day = 1) { cout << "调用了Date()" << endl; _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; } //d1<d2 //d1.operator<(const Date& d2) bool operator<(const Date& d)//隐含了this指针这个参数 { if (_year < d._year) { return true; } else if (_year == d._year && _month < d._month) { return true; } else if (_year == d._year && _month == d._month && _day < d._day) { return true; } else { return false; } } int _year; int _month; int _day; }; int main() { Date d1(2023, 9, 12); Date d2(2023, 9, 13); cout << (d1 < d2) << endl; cout << (d1.operator<(d2)) << endl; return 0; }
4.1赋值运算符的重载
1. 赋值运算符重载格式参数类型 : const T& ,传递引用可以提高传参效率返回值类型 : T& ,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值返回 *this :要复合连续赋值的含义Date& Date::operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; }
int main() { Date d1(2023, 8, 6); // 拷贝构造,一个已经存在的对象去初始化另一个要创建的对象 Date d2(d1); Date d3(2023, 8, 13); // 赋值,两个已经存在的对象进行拷贝 //d1 = d3; // d1.operator=(d3) d1 = d2 = d3; return 0; }
五、取地址及const取地址操作符重载
class Date { public: Date(int year = 1970, int month = 1, int day = 1) { cout << "调用了Date()" << endl; _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; } Date* operator&() { return this; } const Date* operator&() const { return this; } private: int _year; int _month; int _day; }; int main() { const Date d1(2023, 9, 14); cout << &d1 << endl; Date d2(2023, 9, 15); cout << &d2 << endl; return 0; }