前言
类和对象没有技巧,只有多加练习,多多尝试自己完成代码,例如各种运算符的重载,或是实现一个自己的日期类
目录
一、类的六个默认成员函数
二、构造函数
2.1 概念
2.2 特点
2.3 默认无参的构造函数
三、析构函数
3.1 概念
3.2 特性
3.3 默认析构函数
四、拷贝构造函数
4.1 概念
4.2 特性
五、对象复制
六、const成员
一、类的六个默认成员函数
二、构造函数
先看一下我们之前如何初始化一个类的成员变量:
#include<iostream> using namespace std; class student { private: string _id; string _name; int _age; public: void Inset(string id, string name, int age) { _id = id, _name = name, _age = age; } void print() { cout << _id << endl << _name << endl << _age << endl; } }; int main() { student a; student b; a.Inset("001", "大黄", 19); b.Inset("002", "小黄", 18); return 0; }
我们调用了一个成员函数来实现初始化,那么有没有一种方式使我们可以直接在定义的时候初始化呢? C++ 提供了一种构造函数来实现。
2.1 概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.2 特点
1. 构造函数不为对象创建空间,而是对对象进行初始化;
2. 构造函数的函数名与类名相同;
3. 构造函数无返回值;
4. 对象实例化时,编译器会自动调用对应的构造函数;
5. 构造函数可以重载。
例如:
#include<iostream> using namespace std; class student { private: string _id; string _name; int _age; int k = 10; public: student(string id = "", string name = "", int age = 18) { _id = id, _name = name, _age = age; } student(student& x) { _id = x._id; _name = x._name; _age = x._age; } void print() { cout << _id << endl << _name << endl << _age << endl << k << endl; } }; int main() { student a("001", "小黄", 19); student b; student c(a); // student print(); a.print(); b.print(); c.print(); return 0; }
我们可以看到,我们可以在定义时直接初始化,也可以通过类似于函数传参的方式,直接对类进行初始化,还可以定义其函数重载实现不同的传参用来初始化,同时我们也可以通过缺省参数的方式对每一个类都初始化,但是需要注意的是,当全缺省时,初始化类的后面不能加括号,否则万一恰好存在一个和新创建的类的名字相同的另一个函数呢?那不就可以理解为另一个函数的调用了吗?正如上图中被注释的一行,这在大多数编译器下是不可以运行的,会给出一个报错。
2.3 默认无参的构造函数
如果类中没有显式定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,而如果用户显式定义,则编译器将不再生成。但是如果我们不写构造函数的话就会发现,似乎编译器生成的默认构造函数并没有什么作用,成员变量还是随机值,那么这个默认无参的构造函数似乎就完全没有作用欸?
实际上,C++ 把成员变量分为内置类型(基本类型)和自定义类型。内置类型就是语言自带的变量类型,如 int / char / double …,自定义类型就是我们使用 class / struct / union … 自己定义的类型。编译器生成的默认构造函数只会对自定义类型初始化,而不管内置类型。
例如:
#include<iostream> using namespace std; class A { private: int a; public: A() { a = 10; } void print() { cout << a << endl; } }; class B { private: int age; A x; public: void print() { x.print(); cout << age << endl; } }; int main() { B x; x.print(); return 0; }
也许还有人会觉得奇怪,类 A 中还是自己写的构造函数呀?那不是还是需要自己写构造函数吗?但是,思考一下,如果没有类 B 的默认构造函数,那么也就不会调用类 A 的构造函数,x 中的变量 a 也就不会初始化。因此可见,默认构造函数函数还是很有必要的。
PS:无参的构造函数,全缺省构造函数,编译器自动生成的默认构造函数,都可以被称为默认构造函数哦~
三、析构函数
3.1 概念
有构造函数初始化,那么对应的也会有析构函数来进行资源清理。注意!是资源清理,不是销毁对象本身!对象的销毁是由编译器完成的,析构函数是在对象销毁的时候自动调用的函数,用于对象中资源的清理工作。例如对堆区空间的释放等。
3.2 特性
1. 析构函数的函数名是在类名前加上字符 ~;
2. 析构函数无参数无返回值类型;
3. 一个类只能有一个析构函数,若用户未显式定义,编译器会自动生成默认的析构函数,即析构函数不可重载;
4. 在对象生命周期结束时,由编译器自动调用析构函数。
例如:
#include<iostream> using namespace std; class A { private: int* arr; char m; int n; public: A() { arr = (int*)malloc(4); m = 'x'; n = 99; } ~A() { if (arr != nullptr) free(arr); arr = nullptr; m = n = 0; cout << "析构完成" << endl; } }; int main() { A x; return 0; }
如图便是一个析构函数,主要作用时将 A.arr 申请的堆空间释放掉了。
3.3 默认析构函数
和构造函数一样,当用户未显式地定义类的析构函数的时候,编译器会自动生成一个默认的析构函数,同样,它也是仅对自定义类型起作用。
例如:
#include<iostream> using namespace std; class A { private: int* arr; public: A() { arr = (int*)malloc(4); } ~A() { if (arr != nullptr) free(arr); arr = nullptr; cout << "析构完成" << endl; } }; class B { private: int nums = 99; A p; }; int main() { B x; return 0; }
可以看到,虽然我们没有写类 B 的析构函数,但是它的成员变量中有类 A ,还是会调用类 A 的析构函数,因此可以推断出系统自动生成了一个析构函数。由此可见当我们定义的类中没有申请资源的时候,可以不写析构函数,而出现了申请资源的情况,则一定要写,否则会造成内存泄漏!
四、拷贝构造函数
假设我们需要初始化一个类,但是已经存在一个类,我们希望两者是一样的,那么能不能把这个类直接传参过去,实现拷贝呢?
答案是肯定的,这就是拷贝构造函数。
4.1 概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类的类型对象创建新对象时由编译器自动调用。本质上是构造函数的一种重载。
4.2 特性
1. 拷贝构造函数的参数有且仅能有一个,通常用该类的引用进行传参,不能使用传值调用,否则系统将报错;
2. 若未显示定义拷贝构造函数,则编译器会生成默认的拷贝构造函数,默认拷贝构造函数将函数成员变量按字节序完成拷贝,这种方式为浅拷贝(值拷贝);
3. 在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
例如:
#include<iostream> using namespace std; class A { private: int* arr; public: A() { arr = (int*)malloc(4); } A(A& a) { arr = (int*)malloc(4); cout << "拷贝构造" << endl; } ~A() { if (arr != nullptr) free(arr); arr = nullptr; cout << "析构完成" << endl; } }; int main() { A x; A y(x); return 0; }
当我们在主函数里面新创建一个类A时,用已经有的 x 对新的 y 进行构造,需要特别注意的是,在类 A 的拷贝构造函数中只能用引用传参。
如果是普通的形参,那么在之前的 C 语言的学习中我们已经知道在调用拷贝构造函数的时候,形参会新创建一个,也就是说此处会新创建一个类 A,而这个类 A 的构造方式又是拷贝构造,然后再次调用拷贝构造,那么又需要新的形参,从而导致了死循环,因此只能采用引用传参的方式。
五、对象复制
对象复制从某种程度上来说和拷贝构造函数有一定的相似,不过对象复制是两个已有的对象之间进行赋值,这个时候就会用到运算符重载,什么叫运算符重载呢?简单来说,即对于自定义的一个类,系统不知道怎么调用一些运算法,例如两个日期相减,我们都知道可以得到一个数字,但是系统没有日期类的定义,因此也就会导致两个自定义的日期类不能想减。
1.通常情况下,我们也是采用引用传参的方式进行构造,这样做的目的是为了节省形参需要的空间。
2.函数的返回值采用类的引用,这样做的目的是实现连续赋值,例如 x = y = z
3.赋值运算符只能重载为成员函数,这是系统规定的(原因在于,系统会自动生成一个默认的赋值运算符重载)
4.在赋值运算符的重载中也需要注意“浅拷贝”带给的危害
#include<iostream> using namespace std; class A { private: int* arr; public: A() { arr = (int*)malloc(4); } A(A& a) { arr = (int*)malloc(4); cout << "拷贝构造" << endl; } ~A() { if (arr != nullptr) free(arr); arr = nullptr; cout << "析构完成" << endl; } A& operator = (const A& p) { if (this == &p) return *this; *arr = *(p.arr); cout << "对象复制" << endl; } }; int main() { A x; A y = x; y = x; return 0; }
六、const成员
我们常常会看到在一个类的成员函数的参数列表之后,多出一个 const,这个 const 修饰的是当前调用函数的对象,也就是 this 指针。简单来说,就是表示这次调用函数时,调用函数的对象的内容不能被修改。通常用于代码的安全性,防止误操作修改了对象。
#include<iostream> using namespace std; class A { private: int* arr; public: A() { arr = (int*)malloc(4); } A(A& a) { arr = (int*)malloc(4); cout << "拷贝构造" << endl; } ~A() { if (arr != nullptr) free(arr); arr = nullptr; cout << "析构完成" << endl; } A& operator == (const A& p) const { // *arr = *(p.arr); 不能对this.arr修改 cout << "对象比较" << endl; } }; int main() { A x; A y = x; y = x; return 0; }