C++入门之类和对象
文章目录
- C++入门之类和对象
- 1. 类的6个默认对象
- 2. 构造函数
- 2.1 概念
- 2.2 特性
- 2.3 补丁
- 3. 析构函数
- 3.1 概念
- 3.2 特性
- 3.3 总结
- 4. 拷贝构造函数
- 4.1 概念
- 4.2 特性
- 4.3 总结
1. 类的6个默认对象
如果一个类中什么都没有,那么这个类就是一个空类。但是,任何类如果什么都不写的话,编译器会自动生成6个默认成员函数
默认成员函数:用户没有显式实现(用户没有写),编译器自动生成的成员函数被称为默认成员函数
class Data{};
2. 构造函数
2.1 概念
假设有以下类:
#include <iostream>
using namespace std;
class Date
{
public:void Init(int year = 2024, int month = 4, int day = 15){_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();d1.Print();Date d2;d2.Init(2024, 5, 1);d2.Print();return 0;
}
上述类中是使用Init函数对类进行初始化,Init使用全缺省参数,如果没有传值的话,使用缺省值初始化,但是对于这这种类,就算不初始化也不会有什么问题,但是对于顺序表,链表等,如果不初始化就会报错,往往我们会容易忘记调用初始化,这时候,构造函数就派上用场了
构造函数是一种特殊的函数,名字与类名相同,没有返回值(在默认成员函数中,没有返回值指的都是不写),在创建类对象时由编译器自动调用,保证每个成员都有一个初始值,并且在类对象整个生命周期只会调用一次
2.2 特性
构造函数是一种特殊的函数,构造函数并不是用于开辟空间创建对象,而是为对象进行初始化
特征
- 函数名与类名相同
- 函数没有返回值(不写返回值)
- 对象实例化时编译器会自动调用
- 构造函数可以重载(可以根据需求写多个初始化方式)
- 如果类中没有显式定义构造函数(没有写),编译器就会自动生成一个无参的构造函数,反之,编译器则不会生成
- 由编译器生成的无参构造函数,不会对类中的内置类型(int char等等)进行处理,但是对调用类中自定类型(class struct union等等)的构造函数,如果类中自定类型还是没有则也不处理
C++中没有规定对自定类型(class struct union等等)初始化成0或者其他,取决于编译器的实现- 无参构造函数和全缺省的构造函数,由编译器自动生成构造函数都可以被称为默认参构造函数,但是默认参构造函数只能存在一个
示例1:
#include <iostream>
using namespace std;
class Date
{
public://Date(int year, int month, int day) (这种写法会报错,这种不是默认的构造函数)Date(int year = 2024, int month = 4, int day = 15){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1;//调用全缺省的构造函数(2024-4-15)d1.Print();return 0;
}
示例2:
#include <iostream>
using namespace std;
class Time
{
private:int _hour;int _minute;int _second;
};
class Date
{
public:void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;Time a;
};int main()
{Date d1;d1.Print();return 0;
}
示例3:
#include <iostream>
using namespace std;
class Date
{
public:Date(){}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;d1.Print();return 0;
}
存在多个构造函数,报错
2.3 补丁
由于不对内置类型进行初始化,所以在C++ 11中,打了一个补丁,允许内置成员在声明时可以给一个默认值
#include <iostream>
using namespace std;
class Date
{
public:void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year = 2024;int _month = 4;int _day = 15;
};int main()
{Date d1;d1.Print();return 0;
}
用声明时的默认值初始化(2024-4-15)
总结:
一般情况下,构造函数都要由我们自己实现,少部分情况下可以不用实现(如果类中只有自定类型,而这个自定类型内部存在构造函数),例如:MyQueue
3. 析构函数
3.1 概念
析构函数是与构造函数相反的一种特殊函数,析构函数不是对对象进行销毁,局部变量的销毁是由编译器处理的,而是析构函数是对对象中资源的清理,且会在对象销毁时自动调用
3.2 特性
- 在类名前面加上~
- 无参数无返回值(不写返回值)
- 一个类只有一个析构函数,如果没有显式定义(没有写),则编译器会自动生成默认析构函数(由于没有参数,析构函数不能重载)
- 在对象生命周期结束时,编译器会自动调用析构函数
- 与构造函数相似的是,析构函数不会对内置类型进行处理,对自定类型则是调用其析构函数
示例:
#include <iostream>
using namespace std;
class Stack
{
public:Stack(int n = 4){cout << "Stack()" << endl;int* tmp = (int*)malloc(sizeof(int) * n);if (nullptr == tmp){perror("malloc fail");return;}_arr = tmp;_capacity = n;_size = 0;}void Push(int x){//扩容_arr[_size] = x;_size++;}~Stack(){cout << "~Stack()" << endl; //方便查看if (_arr) //防止被多次销毁,加个判断{free(_arr);_arr = nullptr;_capacity = 0;_size = 0;}}
private:int* _arr;int _size;int _capacity;
};
int main()
{Stack s;s.Push(1);s.Push(2);s.Push(3);s.~Stack();return 0;
}
析构函数也是可以显式调用的
3.3 总结
- 在没有需要资源清理的时候可以不写析构函数,
a. 如Date类,没有需要清理的内置类型
b.没有需要清理的内置类型,剩下的其他自定类型中存在析构函数,如MyQueue,也不需要写析构函数
2.有资源清理就要写析构函数,如Stack,List
4. 拷贝构造函数
4.1 概念
拷贝构造函数:只有一个形参,该形参为本类型对象的引用(一般会使用const修饰),在用已经存在的类类型对象时创建新对象时会由编译器自动调用
4.2 特性
- 是构造函数的一种重载形式(函数名与类型一致)
- 拷贝构造函数的参数只能有一个,且得是类类型对象的引用,否则在使用拷贝构造函数会直接报错(引发无穷递归调用)
- 如果没有显式定义,编译器会自动生成默认的拷贝构造函数,默认的拷贝构造函数会对内置类型进行处理,按内存存储按字节序完成拷贝,也被称为浅拷贝或者值拷贝
示例1:
#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 Init(int year = 2024, int month = 4, int day = 15){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2024,4,15);Date d2 = d1; //与下面创建对象d3是等价的,两种写法d2.Print();Date d3(d1);d3.Print();return 0;
}
示例2:
#include <iostream>
using namespace std;
class Stack
{
public:Stack(int n = 4){cout << "Stack()" << endl;int* tmp = (int*)malloc(sizeof(int) * n);if (nullptr == tmp){perror("malloc fail");return;}_arr = tmp;_capacity = n;_size = 0;}void Push(int x){//扩容_arr[_size] = x;_size++;}~Stack(){cout << "~Stack()" << endl;if (_arr){free(_arr);_arr = nullptr;_capacity = 0;_size = 0;}}
private:int* _arr;int _size;int _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);Stack s2 = s1;return 0;
}
代码运行结果:
报错
编译器生成的默认拷贝构造是不够用,在上述代码中,s2对象使用s1对象的拷贝,由于是浅拷贝,会将s1中的内容原封不动的拷贝给s2,因此s1和s2调用的是同一块空间,在调用析构函数时,s1将空间释放了,但是s2中存放的还是s1的空间,因为还会再释放一次,一块内存空间的多次释放,会造成程序奔溃。同时在对任意一个栈中push数据的时候,另一个栈中的size是不会加的,但是共用的是同一块空间,数据丢失等等问题
示例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;}Date(Date d) //错误写法 会引发无穷递归{_year = d._year;_month = d._month;_day = d._day;}
private:int _year;int _month;int _day;
};Date Func(Date d)
{Date tmp(d);return tmp;
}int main()
{Date d1(2024, 4, 15);Func(d1);return 0;
}
在返回一个局部变量时,由于局部变量出作用域就销毁了,所以会将局部变量拷贝给一个临时变量,在给拷贝给临时变量时,又会调用拷贝构造函数,在调用拷贝构造函数时,又会将返回值拷贝给一个临时变量,造成无穷递归
4.3 总结
- 在类中,如果没有涉及需要资源管理的内置类型,是可以不写拷贝构造函数的,编译器自动生成的浅拷贝就够用,但是一旦涉及,就需要自己实现拷贝构造函数了
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用