目录
1.类的六个默认成员函数
如果一个类中什么成员都没有,简称为空类。
当一个类中没有自定义成员时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数;
2.构造函数
2.1 概念
构造函数是用来初始化对象的;
2.2 特征
构造函数是特殊的成员函数,主要任务并不是开辟空间创建对象,而是初始化对象。
其特征如下:
1.函数名与类名相同;
2.无返回值;(无需标识void,void为空返回值)
3.对象实例化时编译器自动调用对应的构造函数;
4.构造函数可以重载;
class Date
{
public:
Date( )
{
_year = 2022;
_month = 1;
_day = 1;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
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(2022, 7, 17);
Date d2;
//调用Init初始化,可能因为忘记而导致程序崩溃出现随机值
//d1.Init(2022, 7, 5);
d1.Print();
d2.Print();
return 0;
}
5.如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义则编译器不再生成。
typedef int DataType;
class Stack
{
public:
Stack(int capacity = 4)
{
_array =(DataType*) malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc fail");
return;
}
_size = 0;
_capacity = capacity;
}
private:
DataType* _array;
int _capacity;
int _size;
};
class MyQueue
{
private:
Stack _st1;
Stack _st2;
};
int main()
{
//默认生成构造函数对内置类型成员不作处理,只对自定义类型成员会去调用它的默认构造函数
Stack st;
MyQueue q;
return 0;
}
6. C++早期设计的默认生成构造函数对内置类型成员不作处理,这是早期设计的缺陷,在C++11里,增添内容:允许为内置类型成员进行赋值,但这不是初始化,而是给了一个缺省值。
比如:
class Date
{
public:
void Print()
{
cout<<_year<<"-"<<_month<<"-"<<_day<<"_"<<endl;
}
private:
int _year=1;
int _month=1;
int _day=1;
};
int main()
{
Date d;
return 0;
}
在没有进行赋值时,内置类型会被默认赋予缺省值,若进行赋值操作,则该操作不起作用;
再举一例:
typedef int DataType;
class Stack
{
private:
DataType* _array;
int _capacity;
int _size;
};
class MyQueue
{
private:
int _size;
Stack _st1;
Stack _st2;
};
int main()
{
Stack st;
MyQueue q;
return 0;
}
调试监视:
对Queue类没有自定义构造,故而采取默认构造:size为内置类型,不作处理,为随机值,st1与st2为自定义类型,调用Stack的构造函数,Stack也未进行自定义构造,调用默认构造函数,其三个值都为内置类型,不作处理,故而从监视内可见,st,size,st1,st2均为随机值;
PS:C++类型分类:
内置类型(基本类型):int、double、char、指针等等;
自定义类型:struct、class等类类型;
7. 默认构造函数包括:无参构造函数,全缺省构造函数以及我们没有写编译器默认生成的构造函数,并且默认构造函数只能有一个。
即默认构造函数的特点:默认构造函数不传参数就可以调用;
总结:一般情况下都不会让编译器默认生成类的构造函数,可以写一个全缺省构造函数,特殊情况下才会令其默认生成。
3.析构函数
3.1 概念
对象在销毁时会自动调用析构函数,完成对象中资源的清理工作;
3.2 特征
析构函数也是特殊的成员函数,不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的
其特征如下:
1.析构函数名是在类名前加上字符~;
2.无参数无返回值类型;
3.一个类只能有一个析构函数,若未显示定义,系统会自动生成默认析构函数。
注意:析构函数不能重载;
4.对象生命周期结束时,C++编译系统系统自动调用析构函数;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//无需写,没有需要进行的清理工作
//~Date()
//{
// //清理工作
// cout << "~Date()" << endl;
//}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc fail");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
_array[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack()->" << _array<<endl;
free(_array);
//可写可不写
_capacity = _size = 0;
_array = nullptr;
}
private:
DataType* _array;
int _capacity;
int _size;
};
class MyQueue
{
public:
void Push(int x)
{
//...
}
private:
size_t _size = 0;
Stack _st1;
Stack _st2;
};
void func()
{
Date d;
Stack st;
MyQueue q;
}
typedef int DataType;
int main()
{
func();
return 0;
}
5.默认生成析构函数特点与构造函数类似:内置类型不处理,自定义类型调用它的默认析构函数;
6.析构函数分为两种:
(1)需要显式书写:
动态开辟内存空间,如队列、链表、二叉树、顺序表栈等类,不显式书写构造函数会造成内存泄漏;
(2)不需要显式书写:
① 内置类型变量,虽默认生成,实际也并未进行什么清理工作,编译器会自行处理;
② 自定义类型,默认生成对于其中包括的自定义类型成员调用其析构函数进行清理工作;
PS:试运行以下代码:
class A
{
public:
A(int a = 0)
{
_a=a;
cout << "A(int a=0)->"<<_a << endl;
}
~A()
{
cout << "~A->"<<_a << endl;
}
private:
int _a;
};
A aa3(3);
//全局最先初始化,在main函数之前初始化
int main()
{
static A aa0(0);
//局部静态变量存储在静态区,但第一次运行时就会初始化
A aa1(1);
A aa2(2);
static A aa4(4);
return 0;
}
运行结果如下:
可知:
① aa0、aa3、aa4都处于静态区,其生命周期为整个程序,只有整个程序结束后才会销毁,但aa1、aa2处于栈区main函数的栈帧,调用完即销毁;
② 对于栈区的局部变量,先调用的后析构:先aa2再aa1;
栈帧和栈里面的对象都要符合后进先出,也就是后定义先销毁,后定义先析构的规则。
③ 静态区也遵循先定义后析构的原则,定义顺序依次为aa0、aa3、aa4,故而析构顺序依次为aa4、aa3、aa0;
4. 拷贝构造函数
4.1 概念
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用;
4.2 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
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)
{
cout << "Date(Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1(1997,8,5);
Date d2(d1);
return 0;
}
3.若未显式定义,编译器会生成默认的拷贝构造函数。
默认的拷贝构造函数对象按内存存储按字节完成拷贝,这种拷贝叫做浅拷贝,或值拷贝;
注意:在编译器默认生成的拷贝构造函数中,内置类型按字节方式直接拷贝,自定义类型是调用其拷贝构造函数完成拷贝的;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//Date(Date d)
/*Date(const Date& d)
{
cout << "Date(Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}*/
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc fail");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
_array[_size] = data;
_size++;
}
private:
DataType* _array;
int _capacity;
int _size;
};
class MyQueue
{
public:
void Push(int x)
{
//...
}
private:
size_t _size = 0;
Stack _st1;
Stack _st2;
};
void func()
{
Date d1(1997, 8, 5);
Date d2(d1);
/*Date d3 = d1; */
Stack st1;
Stack st2(st1);
//默认拷贝构造函数虽然内置与自定义类型都会处理,但存在问题:
//其余内置类型正常浅拷贝,但指针浅拷贝会导致两个指针指向同一块区域,修改时会彼此影响,
//同时free时会析构两次,程序崩溃;
//故而需要自己实现深拷贝;
}
typedef int DataType;
int main()
{
func();
return 0;
}
5.赋值运算符重载
5.1 运算符重载
内置类型可以直接使用运算符运算,编译器知道要如何运算;
自定义类型无法直接使用运算符,编译器也不知道要如何运算,故而自定义实现运算符重载;
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通函数类似;
函数名:关键词operator后面接需要重载的运算符符号;
函数原型:返回值类型operator操作符(参数列表);
注意:1.不能通过连接其他符号来创建新的操作符:如operator@;
2.重载操作符必须有一个类类型参数(至少有一个自定义类型参数);
3.用于内置类型的运算符,其含义不能改变,如内置的整型+不能改变其含义;
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this;
5. .* :: sizeof ?: . 这是五个不能重载的运算符。
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 DateEqual(const Date& x1, const Date& x2)
{
return x1._year == x2._year
&& x1._month == x2._month
&& x1._day == x2._day;
}
bool operator==(const Date& x1, const Date& x2)
{
//const修饰传引用,可以避免赋值与比较符号的混淆导致的错误
return x1._year == x2._year
&& x1._month == x2._month
&& x1._day == x2._day;
}
//int operator-(Date x1, Date x2)
//{
//
//}
int main()
{
Date d1(1997, 8, 5);
Date d2(1997, 8, 5);
cout <<( d1 == d2) << endl;
cout << operator==(d1, d2) << endl;
//底层调用函数逻辑如上行代码所示
cout << DateEqual(d1, d2) << endl;
//DateEqual函数可以实现相同作用,但运算符重载函数可以像内置类型一样直接表示运算符操作,可读性增强;
return 0;
}
为了能直接访问成员变量,
第一种方法为类内定义GetYear,GetMonth,GetDay函数将成员变量公有化,但这样操作非常麻烦,访问变量还需要调用函数,不采用;
第二种方法为将变量去私有化,直接定义为public,但这样便失去了class定义类降低错误风险的意义,也不采纳;
第三种方法是将运算符重载函数放置于类内设置为内联函数,但是当我们直接将类外函数放置于类内时,编译器会提示参数太多,除了本身由操作数决定的参数个数外,还有一个隐藏的this指针参数,所以写内联的运算符重载函数时,我们需要进行修改:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& x)
{
return _year == x._year
&& _month == x._month
&& _day == x._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1997, 8, 5);
Date d2(1997, 8, 5);
cout <<( d1 == d2) << endl;
//实际上编译器会将上行代码处理为:
cout << d1.operator==(d2) << endl;
return 0;
}
注意此处不能使用域限定符进行限制,需要区分private(封装)限定与域限定符:域限定符是不知道去哪里访问故而采取域限定指明类,但private是设定私有,使类外不能访问;
5.2 赋值运算符重载
#include<iostream>
using namespace std;
class Date
{
public:
//构造函数会被频繁调用,故而放置在类体内定义作为inline
Date(int year = 1, int month = 1, int day = 1)
{
_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;
}
//d1=d3
//法一:只支持单次赋值(空返回值)
/*void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}*/
//法二:支持连续赋值(具有返回值)
Date& operator=(const Date& d)
//同时采取传引用调用,避免多次调用拷贝构造函数
{
if (this != &d)
//没有采用*this!=d的值比较,而是直接进行指针比较
//直接根据判断指针是否相同进行是否为自身赋值操作的判断
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
}
private:
int _year;
int _month;
int _day;
};
void TestDate1()
{
Date d1(1997, 9, 5);
//拷贝构造
Date d2(d1);
Date d3(1998, 3, 25);
//赋值运算符重载
d2 = d1 = d3;
//即:d1.operator=(&d1,d3);
}
int main()
{
TestDate1();
return 0;
}
PS:
(1)赋值运算符重载格式:
① 参数类型:const T& ,传递引用可以提高传参效率;
② 返回值类型:T& ,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值;
③ 检测是否自己给自己赋值;
④ 返回*this,要符合连续赋值的含义;
(2)赋值运算符重载只能重载成类的成员函数,不能重载为全局函数,因为重载为全局函数就没有this指针了,该函数的实现需要两个参数;
但是当我们重载Date&operator=(Date& left,const Date&d)函数时,编译器仍然会报错,因为赋值运算符如果不显式实现,编译器会生成一个默认的,此时若再自定义实现一个全局的赋值函数,就会出现冲突;
(3)当没有显式实现时,编译器会生成一个默认的赋值运算符重载,以值的方式逐字节拷贝。
内置类型成员变量时直接赋值的,自定义类型成员变量需要调用对应类的运算符重载完成赋值;
(4)参考拷贝构造函数与深浅拷贝的相关知识,赋值运算符重载也分为显式写赋值函数与不需显式写赋值函数;