目录
*一.构造函数
*二.析构函数
*三.拷贝构造函数
四.赋值运算符重载
*运算符重载:
*赋值运算符重载:
*前置++和后置++重载:
*<< 和 >>重载(友元)
取地址(&)和(const &)操作符不需要重载
五.const成员
在学习这些默认成员函数的时候要切记不能以普通函数的定义和调用规则去理解它们。
虽然叫做“构造”函数,但是它并不会定义,只能够初始化(个人认为其实叫做初始化函数更为合理一点)。
特征:
1.函数名就是类名
2.无返回值(不用写void,void其实算作“空返回值”)
3.对象实例化的时候编译器会自动调用对应的构造函数
4.构造函数是可以重载的。
举个例子:
struct Stack
{
public:
Stack(Stack* _a,int top,int capacity)//构造函数的显式实现
{
_a = a;
_top =top;
_capacity = capacity;
}
private:
Stack * _a;
int _top;
int _capacity;
}
int main()
{
Stack st;
Stack (&st,0,4);
return 0;
}
构造函数的应用价值:上面这个栈要传参还要初始化一个st就很烦,可以直接全缺省构造函数。
5.如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义,编译器就不再生成。
6.默认构造函数有什么用呢?既然默认构造函数无参,那它不就是一点用都没有吗?
不是的,C++把类型分为内置类型和自定义类型:
而默认构造函数的作用是:
不管内置类型,调用类型体内的自定义类型的构造函数
例如:
class Time
{
public:
Time ()
{
cout<<"构造函数Time调用"<
IncludeTime 这个类中包含了自定义类型Time,又因为IncludeTime没有显式实现构造函数,那么就会调用IncludeTime()这个默认构造函数进而--->调用Time(),所以最后程序会输出对应的结果。
其实这设计得有点缺陷,在这里他只处理自定义类型却不处理内置类型,就导致了编译器自动生成的这个默认构造函数很鸡肋(内置类型还是得我们自己来,而我们自己实现构造函数的时候还是得调用自定义类型的构造函数)
相应的解决方法就是:在C++11之后,他打上了补丁:可以在类内部给内置类型赋值初始化
7.所有不传参数就可以初始化的构造函数都可以称之为默认构造函数
这里面包括:编译器默认生成的 自己写的无参的 自己写的全缺省的
注意:默认构造函数只能存在一个 (否则编译器会报错——它不知道该调用哪个)
出了域直接调用析构
析构函数和构造函数功能相反(但是析构函数不是对对象本身的销毁,局部对象销毁工作都是由编译器完成的)。对象在销毁时会自动调用析构函数完成对象中资源的清理工作。
特征:
1.析构函数名就是在类名前面加上字符“~”
2.析构函数 无参 无返回值类型(void算作“空返回值”,不能写void,什么都不写即可)
3.一个类只能有一个析构函数,如果未显示定义系统会自动生成默认的析构函数
注意:析构函数不能重载!
析构的顺序:
后定义的先析构(栈区中也是“后进先出”)
栈在堆前面销毁
全局最先初始化,也是最后出栈(最后析构)
4.对象生命周期结束时,C++编译系统会自动调用析构函数
(有些类的析构函数需要我们自己写,有些不用)例如栈、队列、堆、树这些申请了资源的都需要们显式实现析构函数。
5.关于编译器自动生成的析构函数,他什么也不干,只能调用自定义类型成员的析构函数(这一点和编译器自动生成的默认构造函数很像,但是内置类型出了域会自动销毁)
6. 如果类中没有申请资源,那析构函数可以不写,直接使用默认析构函数。
但是如果有资源申请的话,那默认函数必写
例如:
class Date
{
private:
int _year;
int _month;
int _day;
}
这种类完全不用写析构函数。直接用默认的就好了。
class Stack
{
public:
Stack (int *a = nullptr, int top = 0 , int capacity = 4)
{
_a = (int *)malloc(sizeof(int)*capacity);
_top = top;
_capacity = capacity;
}
~Stack()
{
if(_a)
{
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
}
private:
int * _a;
int _top;
int _capacity;
}
类中含有资源申请,这种必须要写析构函数(不然的话会造成内存泄漏)。
拷贝构造函数:只有单个形参,该形参是对本类型对象的引用(一般用const修饰),再用已存在的类型对象创建新对象时由编译器自动调用。
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个并且必须是类类型对象的引用,如果使用传值方式,编译器会直接报错(无穷递归调用了)
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
Date(const Date d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
传值调用会拷贝一份对象,开辟个新空间,它的内部也有拷贝构造函数;只有用传引用才能问题才能解决。
3.若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数把对象按照内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
注意:默认拷贝构造函数不仅会调用自定义类的拷贝构造函数,还会对内置类型逐字节直接拷贝
4.编译器生成的默认拷贝构造函数可以完成字节序的拷贝了,那还要我们自己去实现干嘛?
有些类型用浅拷贝是不合适的!
例如下面这个程序就会崩溃:
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;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
(例如_a=malloc ......),这些指针如果用值拷贝的话,那两个指针值就会相同,这就意味他们指向同一片地址,当调用完了之后要退出时,析构函数会free两次这个地址,程序就会崩溃。
技巧:涉及到资源申请的,一定要拷贝构造函数
5.拷贝构造函数的使用场景:
a.使用已存在对象创建新对象
b.函数参数类型为类类型对象(传参时拷贝)
c.函数返回值为类类型对象(返回时拷贝)
C++为了增强代码可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,但跟前面的几个不一样,他不算是特殊函数,和普通函数类似(有返回值有参数)。有了运算符重载就可以增强代码可读性,让自定义类型之间可以用运算符来计算。
运算符重载函数的函数名为: operator 后面接需要重载的运算符符号
例如: Date& operator +=(Date& a,int b)
注意:
1.不能通过连接其他符号来创建新的操作符(比如 operator@ 这种运算符肯定是重载不了的)
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变
4.作为类成员函数重载时,其形参看起来会比操作数数目少一个,因为所有成员函数得的第一个参数(左参数)都是隐藏的this。5.《.* 》、《::》、《sizeof》、《?:》、《.》这5个运算符不能重载
另外,如果在类外面定义,有可能会出现无法访问private的问题,此时可以放到类里面,注意上面的第四点。
operator=
赋值运算符重载如果要显示实现,只能是成员函数(否则会与默认赋值运算符重载冲突导致编译不了)和默认拷贝构造函数的情况类似。
格式:
1、参数类型:const类名& (传递引用的目的是提高效率,否则会调用拷贝函数很烦)
2、返回值类型: 类& (返回引用可以提高效率,否则会调用拷贝函数很烦),对于=,有返回值是为了支持连续赋值。
3、记得检查是否为自赋值(避免不必要的动作)
4、返回*this即可
注意:当我们没有显示实现时,编译器会生成一个默认赋值运算符重载,进行的是浅拷贝(值拷贝)。对于内置成员变量直接赋值,对于自定义类型成员变量调用对应的赋值运算符重载完成赋值。
技巧: 未涉及资源管理的,赋值运算符可实现可不实现,一但涉及资源管理则必须实现。
(如果涉及到资源管理的类用默认赋值运算符重载进行赋值了,那会出现两个问题:a、赋值给另一个类对象的时候原来地址所对应空间就丢失了,内存泄漏。b、销毁时(析构时)两个指针指向同一块空间,free两次程序会崩溃)这点和拷贝构造函数类似。
前置++:以Date为类名为例
Date& operator ++()
{
return *this+=1;
}
后置++:
Date operator ++(1//随便写个int)
{
Date tmp =*this;
*this += 1;
return tmp;
}
后置++重载的格式 为 类名 operator ++(int)
1.这里的int是一个标志,代表后置++,随便来一个就好
2.后置++因为要传+1之前的类,所以只能用上图这串代码中的方式来实现(先在函数中记录(拷贝)下来,再在返回的时候(拷贝下来)。用不了引用因为局部变量出了域会销毁)
虽然前面我们提到赋值运算符重载必须是成员函数,但是这两个运算符重载作为成员函数时几乎是有弊端的,例如 <<
如果在类中定义,那左参数永远是this指针了,就无法重载。
就只能这样去写
ostream& operator <<(*/左*/ostream& cout ,*/右*/const Date& d)
{
cout<
那么问题又来了:要是这里的d._year 是private,不给你访问呢?
这里只有一个办法就是用友元了
class Date
{
//友元声明
friend ostream& operator <<(ostream& cout,const Date&d)
//...
};
这样子就可以突破封装,但是友元会增加耦合,不宜多用。
(关于友元,我在下一篇博客做更多详细介绍)
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
上面就是编译器默认生成的取地址重载,用它们就够了
格式就是在后面加个 const
将const修饰的成员函数成为const成员函数,const修饰成员函数实际上是修饰该成员函数的 this指针,表明该函数中不能对类的任何成员进行修改
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022,1,13);
d1.Print();
const Date d2(2022,1,13);
d2.Print();
}
(权限可以缩小但是不能放大)
1.const对象不能调用非const成员函数
2.非const对象可以调用const成员函数
3.const成员函数内不可以调用其他的非const成员函数
4.非const成员函数可以调用const成员函数