目录
一.类的6个默认成员函数
默认成员函数:用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。
1.构造函数
1.什么是构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证
每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.构造函数特性
1. 函数名与类名相同。(函数前面不加 void )
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。5.调用无参构造函数时不能写成 Date d1(); 分不清是否为函数声明
注意:funt(int a=1)和funt()都是无参函数,且构成函数重载。
3.编译器生成的默认构造函数
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。
如果我们没有自己定义构造函数,那么编译器会帮我们自动生成一个默认构造函数且自动调用。
(编译器生成的默认构造函数不能传参)
这里我们没有定义构造函数,按理来说编译器会给我们自动生成默认的构造函数。
但为什么会出现乱码呢?
这是因为C++把类型分成内置类型(基本类型)和自定义类型。
内置类型就是语言提供的数据类型,如:int/char...
自定义类型就是我们使用class/struct/union等自己定义的类型。
而编译器定义的构造函数只会对自定义类型进行处理(调用该自定义类型的无参构造函数)
注:1.如果要调用的构造函数没有无参的,会编译报错。
2.如果没有用户没有定义构造函数,编译器则会自动生成无参的构造函数,同理只会对当前类的自定义类型进行处理。
针对内置类型成员不初始化的问题,我们可以通过这种方式解决:
声明内置类型成员变量时给默认值。
2.析构函数
1.什么是析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
2.析构函数特性
1.析构函数的函数名为 '~类名'
2.析构函数无返回类型,无参数
3.析构函数只能有一个,不能构成重载。
4.对象生命周期结束时自动调用
3.编译器生成的析构函数
编译器生成的析构函数同样只对自定义变量处理(调用它自己的析构函数)
(内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可)
注意:1.有资源需要显示清理,需要写析构函数。比如Stack中会malloc空间
2.没有资源需要清理,编译器自动生成的析构函数就可以。
基本类型成员没有资源需要清理,自定义类型成员调用自身的析构就可以。
3.拷贝构造函数
1.什么是拷贝构造函数
只有单个形参,该形参是对本类类型对象的引用(const Date& d),在用已存
在的类类型对象创建新对象时由编译器自动调用。(自定义类型传值传参时)
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;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1,2,3);
Date d2=d1;//Date d2(d1)等价
return 0;
}
2.拷贝构造函数特点
1.拷贝构造函数是构造函数的重构函数。
2.拷贝构造函数只有对本类类型变量引用变量一个参数,如果不是引用变量,则不是拷贝构造函数。(如果通过传值传参的方式 const Data d 会发生无穷递归)
3.编译器生成的拷贝构造函数
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定
义类型是调用其拷贝构造函数完成拷贝的。
为什么通过传值传参的方式 const Data d 会发生无穷递归?
我们先看下面一段代码会发生什么
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d) //拷贝构造函数
{
cout << "调用拷贝构造函数"<<endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void funt(Date d1)
{
Date d2 = d1;
d2.Print();
}
int main()
{
Date d1(2024,1,1), d2;
d1.Print();
funt(d1);
return 0;
}
我们可以看到,这里调用了两次拷贝构造函数。第一次是d1向funt 传值传参时,第二次是d2复制d1时。由此我们可以得知自定义类型传值传参时要进行拷贝构造函数的调用。
Date(const Date d)
{
cout << "调用拷贝构造函数"<<endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
如果写成这种形式,我们在调用拷贝构造函数时就会发生自定义类型的传值传参,而自定义类型传值传参会调用拷贝构造函数,一直死循环。
如果我们写拷贝构造函数采用指针传参的方式会发生什么?
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)
{
cout << "指针拷贝构造函数"<<endl;
_year = d->_year;
_month = d->_month;
_day = d->_day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void funt(Date d1)
{
Date d2(&d1);
d2.Print();
}
int main()
{
Date d1(2024,1,1);
d1.Print();
funt(d1);
return 0;
}
为什么这里只调用了一次用我们写的拷贝函数呢?这是因为向funt传参时调用的是编译器自动生成的拷贝构造函数,在函数内实现的拷贝调用的是我们自己写的拷贝。
由此看来,我们写的拷贝函数只能说是构造函数的重载函数,并不是拷贝调用函数。
什么情况下需要我们自己写拷贝构造函数?
先看下面的代码会发生什么
class Stack
{
public:
Stack(int n = 10)//构造函数
{
_array = (int*)malloc(sizeof(int) * n);
if (_array == nullptr)
{
perror("malloc fail");
return;
}
_capacity = n;
_size = 0;
}
void Push(const int x)
{
_array[_size] = x;
_size++;
return;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_size = 0;
_capacity = 0;
}
}
private:
int* _array;
int _size;
int _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
为什么会出现代码崩溃的情况呢?
这是因为在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,自定
义类型是调用其拷贝构造函数完成拷贝的。
所以s1和s2用的是同一块内存空间,程序结束时s2先销毁,s1再销毁时就会对同一块空间free两次,自然会报错。(先创建的变量,后销毁)
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
class Stack
{
public:
Stack(int n = 10)//构造函数
{
_array = (int*)malloc(sizeof(int) * n);
if (_array == nullptr)
{
perror("malloc fail");
return;
}
_capacity = n;
_size = 0;
}
Stack(const Stack& s1)//拷贝构造函数
{
_array = (int*)malloc(sizeof(int) * s1._capacity);
if (_array == nullptr)
{
perror("malloc fail");
return;
}
memcpy(_array, s1._array, s1._size*sizeof(int));
_size = s1._size;
_capacity = s1._capacity;
}
void Push(const int x)
{
_array[_size] = x;
_size++;
return;
}
bool StackEmpty()
{
return _size == 0;
}
int StackTop()
{
return _array[_size-1];
}
void Pop()
{
_size--;
return;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_size = 0;
_capacity = 0;
}
}
private:
int* _array;
int _size;
int _capacity;
};
void PrintStack(Stack& s)
{
while (!s.StackEmpty())
{
cout << s.StackTop() << " ";
s.Pop();
}
return;
}
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
PrintStack(s1);
return 0;
}
二.运算符重载
1.运算符重载定义
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
2.运算符重载的使用
运算符重载可以在全局定义,也可以在类中定义(可以直接访问成员变量)。
在全局定义:有两个操作对象就传2个参数。
在类中:有两个操作对象就传1个参数(右边的),另一个由this 指针来传。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(Date& d2)
{
return _year == d2._year && _month == d2._month && _day == d2._day;
}
//private: 为了使全局的 operator> 能访问
int _year;
int _month;
int _day;
};
bool operator>(Date& d1, Date& d2)
{
if (d1._year > d2._year) return true;
else if (d1._year == d2._year && d1._month > d2._month) return true;
else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day) return true;
else return false;
}
int main()
{
Date d1(2024, 1, 1);
Date d2(2024, 1, 2);
cout << (d1 == d2) << endl;//相当于operator(d2);
cout << (d1 > d2) << endl;//相当于operator(d1,d2);
return 0;
}
3.运算符重载的特点
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this
5. .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
4.赋值运算符重载
赋值运算符如果用户不显式实现,编译器自动会生成一个默认的。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(Date& d2)
{
return _year == d2._year && _month == d2._month && _day == d2._day;
}
Date& operator=(Date& d2)//赋值运算符
{
if (&d2 != this)
{
_year = d2._year;
_month = d2._month;
_day = d2._day;
}
return *this;
}
//private: 为了使全局的operator>能访问
int _year;
int _month;
int _day;
};
bool operator>(Date& d1, Date& d2)
{
if (d1._year > d2._year) return true;
else if (d1._year == d2._year && d1._month > d2._month) return true;
else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day) return true;
else return false;
}
int main()
{
Date d1(2024, 1, 1);
Date d2(2024, 1, 2);
d1 = d2;
cout << (d1 == d2) << endl;
//cout << (d1 > d2) << endl;
return 0;
}
如果我们不显示写,编译器也会自动生成一个默认的。
1.赋值运算符格式
参数类型:const T&,传递引用可以提高传参效率。
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值。
返回*this :要复合连续赋值的含义。
2.赋值运算符不能重载成全局函数
我们知道如果在类中不显示写赋值运算符,编译器会自动生成一个默认的。而这个默认的赋值运算符重载会和我们自己写的冲突。所以只能把赋值运算符定义成成员函数。
3.编译器生成的赋值运算符重载
编译器生成的赋值运算符重载和我们之前学的编译器生成的拷贝构造函数有些类似。
都是以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
如果我们用编译器生成的赋值运算符给栈赋值会发生什么呢?
会出现两个错误:
1.s2会和s1同用同一块空间,销毁的时候会free两次。2.s2之前申请的空间找不到了,发生内存泄漏。
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;
s2 = s1;
return 0;
}
所以像这种涉及到资源管理就要自己显示写赋值运算符重载。
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
bool StackEmpty()
{
return _size == 0;
}
int StackTop()
{
return _array[_size - 1];
}
void Pop()
{
_size--;
return;
}
Stack& operator=(Stack& s)
{
if (this != &s)
{
memcpy(_array, s._array, sizeof(DataType) * s._size);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
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;
};
void PrintStack(Stack& s)
{
while (!s.StackEmpty())
{
cout << s.StackTop() << " ";
s.Pop();
}
return;
}
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2;
s2 = s1;
PrintStack(s1);
cout << endl;
PrintStack(s2);
return 0;
}
注意:s2=s1 是调用的运算符重载函数,而Stack s2=s1 是调用的拷贝构造函数。
5.前置++ 后置++重载
C++为了区分前置++和后置++,规定后置++重载函数要多一个int 形参。(int参数值是什么不重要,只是为了区分)
前置++:*this在mian函数栈上建立,出了前置++函数仍存在,可以直接引用。
后置++:要先用一个临时变量d保存*this的当前值,*this值加1后,把之前用d保存*this原本的值传过去。
因为d的作用域仅在当前函数,出了函数就会销毁,所以采用传值的方式返回。
多了拷贝构造函数的调用,相比于前置++效率更低。
.cpp
// 前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
// 后置++
Date Date::operator++(int)
{
Date d = *this;
*this += 1;
return d;
}
.test
int main()
{
Date d1(2024, 5, 1);
Date d2(2024, 5, 1);
d1++;
d2.operator++(1);//显试调用后置++
cout << (d1 == d2);
}
6.cout<< 流插入重载
1.cout<<实现本质
cout<< 为什么可以自动识别变量类型呢?
自动识别内置类型,本质上是流插入重载构成函数重载。
2.cout<< 流插入重载函数实现
//.h
// cout<< 插入流
ostream& operator<<(ostream& out);
//.cpp
// cout<< 插入流
ostream& Date::operator<<(ostream& out)
{
cout << _year <<' '<< _month <<' '<< _day;
return out;
}
//.test
int main()
{
Date d1(2024, 5, 1);
cout << d1; //错误
d1 << cout;
}
为什么我们写成cout<<d1会出错呢?
因为ostream& operator<<(&ostream)是成员函数,它的第一个参数是隐含的this 指针,第二的参数才是ostream。
cout<<d1; 因为传参顺序是从左到右的,所以它和成员函数规定的传参顺序正好是是反过来的。
怎么才能按我们习惯 写成cout<<正常输出呢?
我们只要把ostream放到第一个参数的位置就可以解决了。
我们可以把cout<<用全局函数的形式写就可以了。
但设定为全局函数的话,我们就不能在类外直接访问Date类的私有的成员变量。
我们有两种解决方法:
1.我们可以在Daet类中设 返回成员变量的Get函数,全局函数通过使用Get函数间接访问成员变量。2.我们也可以用 友元 让该全局函数自由访问Date类。
//.h
class Date
{
// 告诉编译器 poerator<<全局函数是 Date类的好朋友,可以访问Date对象的私有成员
friend ostream& operator<<(ostream& out, const Date& d);
public:
....
private:
....
}
//全局函数声明
ostream& operator<<(ostream& out, const Date& d);
//.cpp
// cout<< 插入流
ostream& operator<<(ostream& out, const Date& d)
{
cout << d._year << ' ' << d._month << ' ' << d._day;
return out;
}
//.test
int main()
{
Date d1(2024, 5, 1);
cout << d1;
}
函数实现的细节:
1.ostream& operator<<(ostream& out, const Date& d); 因为d不应该被改变所以应该加const。
2.ostream& operator<<(ostream& out, const Date& d); out采用引用的方式传 是因为out不支持拷贝,d引用的目的是提高效率。
3.ostream& operator<<(ostream& out, const Date& d); 返回值类型为ostream& 目的是实现连续输出。 eg. cout<<d1<<d2;
3.cin>> 流提取重载函数实现
同理cin>>函数实现也要设为全局函数。
// cin>> 提取流
istream& operator>>(istream& in, Date& d)
{
cin >> d._year;
while (d._year<0)
{
cout << "非法 请重新输入"<<endl;
cin >> d._year;
}
cin >> d._month;
while (d._month > 12 || d._month < 1)
{
cout << "非法 请重新输入" << endl;
cin >> d._month;
}
cin >> d._day;
while (d._day> d.GetMonthDay(d._year, d._month) || d._day < 1)
{
cout << "非法 请重新输入" << endl;
cin >> d._day;
}
return in;
}
7.const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数 隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
下面这段代码为什么编译不过呢?
.h
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << _month << _day;
}
private:
int _year;
int _month;
int _day;
};
因为隐含的参数类型是 Date* const this ,是可以被改变的。
而传过来的this指针类型是 const Date* const this ,是不能改变的。
也就是发生了权限的放大。
怎么样才能让隐含的this指针被const修饰呢?
C++规定在函数()右侧加上const表示*this不可被修改。
.h
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print() const//在函数右侧
{
cout << _year << _month << _day;
}
private:
int _year;
int _month;
int _day;
};
8.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
第一个函数*this没有被const修饰,第二个被const修饰,函数参数不一致,构成函数重载。
class Date { public: Date* operator&() { return this; } const Date* operator&()const { return this; } private: int _year; // 年 int _month; // 月 int _day; // 日 };
虽然可以自己定义&重载,但一般不需要。
编译器会自动生成,调用时会把该变量的正确地址返回。
自己写可以修改返回值。比如说不返沪正确地址,返回nullptr。