目录
类的6个默认成员函数
构造函数
概念
特性
析构函数
概念
特性
拷贝构造
概念
特征
运算符重载
概念
未完持续……
注意本篇文章代码默认并没有加入以下内容
#include
using std::endl;
using std::cout;
以下就是一个空类,
class Date {};
1. 函数名与类名相同。2. 无返回值。3. 对象实例化时编译器 自动调用 对应的构造函数。4. 构造函数可以重载。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//构造函数是可以写多个的
//Date() //因为构造函数支持函数重载,所以可以合并到一起
//{
// _year = 1;
// _month = 1;
// _day = 1;
//}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 10, 8); // 调用带参的构造函数
Date d2(2022, 10, 9);
Date d3; //不可以加 () // 调用无参构造函数
//无参的不要像如下这样子写
//Date d4(); //这样子在VS里面会报警告,但是在别的编译器中可能会直接报错
//Date func(); //上面这样子写就类似于函数的声明了,会出错的
d1.Print();
d2.Print();
d3.Print();
return 0;
}
构造函数的使用
在使用的时候编译器会自动调用构造函数,用以做某些工作,这样子可以避免很多因为忘记而导致的出错,比如忘记 Inint(初始化) 、忘记 destroy(释放内存)
内置类型:int / char / double ... 指针(自定义类型的指针也算)
自定义类型:class / struct ... Stack / queue / Person
内置类型不处理,自定义类型会调用它的默认构造函数
分析下面的代码
class A
{
public:
A()
{
_a = 0;
cout << "A()构造函数" << endl;
}
private:
int _a;
};
class Date
{
public:
// 没有写构造函数
//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; // 日
// 自定义类型
A _aa;
};
int main()
{
Date d1;
d1.Print();
return 0 ;
}
通过上面的代码发现内置类型是会调用默认构造函数,但是并不会做处理(初始化),因为自定义类型会调用构造函数
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack() //这里是一个析构函数, 函数名与类名一致,但是前面需要加上 ~(C语言中的取反)
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
void Push(int x)
{
// ....
// 扩容
_a[_top++] = x;
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue {
public:
void push(int x)
{
_pushST.Push(x);
}
Stack _pushST; //自动调用了 Stack 里面的构造函数
Stack _popST;
//size_t _size;
};
int main()
{
MyQueue mq;
mq.push(1);
mq.push(2);
return 0;
}
当有默认构造函数并且满足需求的时候就不需要再写一个函数了,可以直接调用(下面再介绍析构函数)
基于以上种种不方便,后面在C++11中又对其进行了优化,打了个补丁,可以直接给默认值(缺省值)
那么有这个补丁的话上面的代码可以如下这样写
注意这里都是缺省值,不是初始化,它并没有为其开辟空间,上述将其描述为蓝图,空有方法,但是并没有实体。
同样的符合缺省值的特性,在没有给定值的时候,会直接使用缺省值进行初始化,而当有值的时候就不会使用缺省值了,如下就不会被初始化为 0,而全部初始化为 1
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
析构函数: 与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会 自动 调用析构函数,完成对象中资源的清理工作
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~2. 无参数无返回值类型。3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载4. 对象生命周期结束时, C++ 编译系统系统自动调用析构函数
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
_array[_size] = data;
_size++;
}
~Stack() //析构函数,在对象生命周期结束时自动调用
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity = 4;
int _size = 0;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
int main()
{
TestStack();
return 0;
}
//析构函数 补
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
上面的输出如下
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。
但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
例题案例(出至题目 232. 用栈实现队列 - 力扣(LeetCode))
当然有时候有些是不需要写析构函数的,因为编译器会在没有析构函数的时候自动生成,比如上面的日期类就没必要写,一切看需要使用.
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
构造是初始化的意思,那么拷贝构造就是拷贝初始化
有些地方叫做复制构造函数
拷贝构造函数也是特殊的成员函数,其 特征 如下:1. 拷贝构造函数 是构造函数的一个重载形式 。2. 拷贝构造函数的 参数只有一个 且 必须是类类型对象的引用 ,使用 传值方式编译器直接报错, 因为会引发无穷递归调用。
内置类型(编译器提供的,编译器对内置类型了如指掌)所以没有拷贝构造函数,但是自定义类型(比较复制)就需要一个拷贝构造函数来帮助完成
如下代码
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d) //使用 &(无限循环访问构造函数) 并且要加上 const (防止写反,导致被反向赋值)
{
cout << "Date 拷贝构造" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
// 形参加const,防止写反了,下面问题就可以检查出来
//d._day = _day;
}
private:
int _year;
int _month;
int _day;
};
void Func1(Date d)
{
cout << "Func1()" << endl;
}
void Func2(Date& d)
{
cout << "Func2()" << endl;
}
int main()
{
Date d1(2022, 9, 22); // 构造 - 初始化
Func1(d1);
Func2(d1);
// 拷贝一份d1
//Date d2(d1); // 拷贝构造 -- 拷贝初始化 会调用拷贝构造函数
return 0;
}
不加 & 会无限调用构造函数
不加 const 容易出现以下错误
还有一点需要注意的是,这里的 & 理论上是可以使用指针来替代完成的,但是用起来会使得代码看起来不整洁,更重要的一点是,使用指针完成该操作已经不算是拷贝构造了,应该叫它为构造函数
// 这里会发现下面的程序会崩溃掉?这里就需要写一个深拷贝去解决。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size = 0;
size_t _capacity = 0;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
Stack s2(s1); //浅拷贝
return 0;
}
会发现程序崩溃了(析构函数析构同一块空间两次),这里会根据栈的性质,后定义的先析构(后进先出),先析构了 s2,然后又析构了 s1,但是由于编译器自动生成的析构函数是浅拷贝,导致两个指针指向了同一块空间,所以会直接崩溃掉程序
深拷贝
//深拷贝
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
_array[_size] = data;
_size++;
}
Stack(const Stack& st)
{
_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
memcpy(_array, st._array, sizeof(DataType) * st._size); //利用memcpy进行拷贝,利用 st._size(数据的个数)更好点
_size = st._size;
_capacity = st._capacity;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size = 0;
size_t _capacity = 0;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
Stack s2(s1); //深拷贝
s2.Push(10);
return 0;
}
监视发现,两者不在指向同一空间,并且两者互不干扰
1.使用已存在对象创建新对象2.函数参数类型为类类型对象3.函数返回值类型为类类型对象
下篇会提及赋值运算符重载
C++为了 增强代码的可读性 引入了运算符重载 , 运算符重载是具有特殊函数名的函数 ,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字operator 后面接需要重载的运算符符号 。函数原型:返回值类型 operator 操作符 ( 参数列表 )
注意:
1.不能通过连接其他符号来创建新的操作符:比如 operator@2.重载操作符必须有一个 类类型参数(不能改变内置类型的运算符)3.用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义4.作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐藏的 this.* :: sizeof ?: . 注意以上 5 个运算符不能重载。这个经常在笔试选择题中出现。第一个比较少见,是古老的运算符了,不过仍需要注意一下
如下是一个操作符重载
//运算符的重载
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 我们不写,编译器生成的默认拷贝构造函数
//private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2022, 9, 22);
Date d2(2023, 1, 1);
//d1 > d2;
cout << (d1 == d2) << endl; // 转换成operator==(d1, d2);
d1 == d2;
// 也可以显示调用,不过一般不这样 ,上面的与下面的写法是一样的,相同的意思
operator==(d1, d2);
return 0;
}
不过还是不好,为了使用操作符重载而让私有变量转化为共有的是不好的行为。
为此我们需要把函数放入类里面,不过我们需要进行一点修改,因为有 this 指针的存在
假如直接放进去不加以修正的话,编译器会报下面的错误
修改过后如下
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d)
{
return _year == d._year //_year 前面默认有一个 this指针,下面同理
&& _month == d._month
&& _day == d._day;
}
// 我们不写,编译器生成的默认拷贝构造函数
private: //可以被类里面的成员函数访问了
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 9, 22);
Date d2(2023, 1, 1);
cout << (d1 == d2) << endl; // 编译器会转换成 d1.operator == (d2),所以在使用的时候要注意运算符的优先级(这里用括号括起来)
d1 == d2;
// 也可以显示调用,不过一般不这样写 ,上面的与下面的写法是一样的,相同的意思
cout << (d1.operator == (d2)) << endl; //
return 0;
}
综上所述一定不能忘记 this指针,可以说任何的成员函数(包括构造函数)都含有隐藏的this指针,除了后面的 静态成员函数
有点太长了,于是就分篇写了