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 总结
- 在类中,如果没有涉及需要资源管理的内置类型,是可以不写拷贝构造函数的,编译器自动生成的浅拷贝就够用,但是一旦涉及,就需要自己实现拷贝构造函数了
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用