C++初阶——类和对象(中)
目录
1、类的默认成员函数
2、构造函数
3、析构函数
4、拷贝构造函数
5、赋值运算符重载函数
5.1 运算符重载
5.2 赋值运算符重载函数
6、取地址运算符重载函数
6.1 const成员函数
6.2 取地址运算符重载函数
1、类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。
一个类,没有显式声明任何一个构造函数情况下,编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,稍微了解一下即可。其次就是C++11以后还会增加两个默认成员函数, 移动构造和移动赋值,这个后再讲解。
默认成员函数很重要,也比较复杂,我们要从两个方面去学习:
• 第一:我们不写时,编译器默认生成的成员函数,能否满足我们的需求。
• 第二:编译器默认生成的成员函数,不满足我们的需求,那我们如何自己实现?
2、构造函数
构造函数的主要任务是对象实例化时初始化对象,并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了)。
构造函数分为
默认构造函数(不用传参,无参,全缺省,无构造函数时编译器自动生成)、
带参的构造函数(要传参)
构造函数的特点:
1. 构造函数名 = 类名。
2. 无返回值。(不需要写void,C++规定)
3. 构造函数可以重载。默认构造函数和带参构造函数。
4. 对象实例化时系统会自动调用 对应的构造函数。无参调默认构造函数,有参调带参构造函数
5. 如果类中没有显示定义构造函数,那么C++编译器会自动生成一个无参的构造函数,即一个默认构造函数
注意:如果类中显示定义了构造函数,那么编译器不会自动生成一个无参的构造函数
6. 无参 构造函数(A)、全缺省 构造函数(B)、我们不写构造函数时编译器默认生成的 构造函数(C)
三个都叫做默认构造函数(不用传参)。
一个类中,默认构造函数,如果有的话,只能有一个:
有A,不会有B(虽然AB构成函数重载,但调用时存在歧义),也不会有C(已经有了构造函数A)
有B,不会有A(虽然AB构成函数重载,但调用时存在歧义),也不会有C(已经有了构造函数C)
有C,那肯定没有显示定义构造函数,也就没有A,B
一个类,也可以没有默认构造函数,但当对象无参初始化时,因没有默认构造函数,就会报错:
如果有了带参的构造函数,也就没有C(有了构造函数),也不写A或B,就没有默认构造函数了
一个类中,默认构造函数和带参的构造函数不会同时没有,可以同时有:
没有带参的构造函数,有A,没有B,C,有B,没有A,C,有C,没有A,B,一定存在一个默认构造函数
没有默认构造函数,说明没有C,那一定有带参的构造函数
同时有,A+带参的构造函数,或者B+带参的构造函数
全缺省构造函数用的比较多(在知道这个缺省值应该设什么的情况下)因为可以传参,也可以不传
有了全缺省构造函数,那么带参数的构造函数就不需要了
7. 我们不写构造函数时编译器默认生成的构造函数,对内置类型成员变量的初始化不确定,取决于编译器。对于自定义类型成员变量,会调用这个成员变量自己的默认构造函数初始化。
如果这个成员变量没有默认构造函数(结合第6点),那么就会报错,这时我们要初始化这个成员变量,可以写个默认构造函数,也可以用初始化列表,初始化列表,在C++初阶——类和对象(下)再细细讲解。
说明:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型, 如:int/char/double/指针等,自定义类型就是我们使用class/struct等关键字自己定义的类型。
8.全局对象先于局部对象进行构造,
局部对象按照出现的顺序进行构造,无论是否为static
以下构造顺序为c,a,b,d
C c;
int main()
{A a;B b;static D d;return 0;
}
实现:大多数情况下,构造函数都要自己实现,少数情况下如MyQueue(两个Stack实现)且Stack有默认构造函数,MyQueue自动生成的默认构造函数会调用Stack的默认构造函数,完成了两个成员的初始化。
#include<iostream>
using namespace std;class Date
{
public:// 1.无参构造函数Date(){_year = 1;_month = 1;_day = 1;}// 2.带参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}// 3.全缺省构造函数/*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()
{// 如果留下三个构造中的第⼆个带参构造,第⼀个和第三个注释掉// 编译报错:error C2512 : “Date”:没有合适的默认构造函数可用Date d1;// 调用默认构造函数Date d2(2025, 1, 1); // 调用带参的构造函数// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则编译器无法// 区分这里是函数声明还是实例化对象// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的? )Date d3();d1.Print();d2.Print();return 0;
}
#include<iostream>
using namespace std;
typedef int STDataType;class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}// ...
private:STDataType* _a;size_t _capacity;size_t _top;
};// 两个Stack实现队列
class MyQueue
{
public:// 编译器默认生成MyQueue的构造函数调用了Stack的构造函数,完成了两个成员的初始化private:Stack pushst;Stack popst;
};int main()
{MyQueue mq;return 0;
}
3、析构函数
析构函数与构造函数功能相反
析构函数的主要任务是对象销毁时完成对象中资源的清理释放工作,并不是完成对对象本身的销毁,(比如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管)
析构函数的特点:
1. 析构函数名 = ~类名。
2. 无参数无返回值。(不需要加void,C++规定)
3. 一个类只能有一个析构函数(因为无参,构不成函数重载)。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,系统会自动调用析构函数。
5. 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。
注意:如果显示写了析构函数,对于自定义类型成员也会调用他的析构函数,
也就是说自定义类型成员无论什么情况都会自动调用自己的析构函数。
6. 如果类中没有申请资源时,可以不显示写析构函数,直接使用编译器生成的默认析构函数,如Date;
如果默认生成的析构就可以用,也可以不显示写析构函数,如MyQueue;
但是有资源申请时,一定要自己写析构,否则会造成资源泄漏,如Stack。
7. C++规定后构造的先析构。类似栈(后进先出)。
需注意static改变对象的生存作用域之后,会在局部对象之后进行析构
以下构造顺序为c,a,b,d,析构顺序为b,a,d,c
C c;
int main()
{A a;B b;static D d;return 0;
}
实现: 需要释放资源时,析构函数需要自己实现。
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};// 两个Stack实现队列
class MyQueue
{
public:// 编译器默认生成MyQueue的析构函数调用了Stack的析构,释放的Stack内部的资源// 显示写析构,也会自动调用Stack的析构/*~MyQueue(){}*/
private:Stack pushst;Stack popst;
};int main()
{Stack st;MyQueue mq;return 0;
}
有了构造函数和析构函数,系统会自动初始化和释放对象中的资源,方便了不少,也不用担心内存泄漏
4、拷贝构造函数
我认为拷贝构造函数的出现,主要是为了进行深拷贝
拷贝构造函数的主要任务是
一个已经存在的同类对象初始化给另一个要创建的对象及
自定义类型对象出现传值行为时被调用,进行拷贝
拷贝构造的特点:
1. 拷贝构造函数是构造函数的一个重载。
注意:若显示定义拷贝构造函数,编译器就不会自动生成构造函数(C),因为已经有了构造函数,当无参初始化对象时,得自己写默认构造函数A或B。
2. 拷贝构造函数的第一个参数必须是类类型对象的引用。
拷贝构造函数的第一个参数使用传值方式编译器直接报错:
C++规定,自定义类型对象进行传值行为(传值传参,传值返回)必须调用拷贝构造函数
如果拷贝构造函数的第一个参数使用传值方式,当一个函数对自定义类型对象进行传值行为(传值传参,传值返回)时,要调用拷贝构造函数,对拷贝构造函数进行的是传值传参,又要调用拷贝构造函数……不断的调用拷贝构造函数,相当于只有“递”,没有“归”,然后报错。
3. 拷贝构造函数也可以多个参数,第一个参数必须是类类型对象的引用,后面必须是缺省参数
4. 若未显式定义拷贝构造函数,编译器会生成自动生成拷贝构造函数。
对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),
对自定义类型成员变量会调用他的拷贝构造函数。
5. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。
像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。
像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现 MyQueue的拷贝构造。
注意:如果一个类显示实现了释放资源的析构函数,那么他就需要显示写 拷贝构造函数(深拷贝),否则就不需要。
6. 传值返回会调用拷贝构造函数,
传引用返回,返回的是返回对象的别名(引用),不会调用拷贝构造函数。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。
注意:传引用返回可以减少拷贝,但是一定要确保返回的对象,在当前函数结束后还存在,才能传引用返回。
实现:需要进行深拷贝时,拷贝构造函数需要自己实现。
#include<iostream>
using namespace std;class Date
{
public:Date(int year = 1, int month = 1, int day = 1)// 默认构造函数{_year = year;_month = month;_day = day;}// 编译报错:error C2652 : “Date”:非法的复制构造函数:第一个参数不应是“Date”//Date(Date d)Date(const Date& d)// 拷贝构造函数{_year = d._year;_month = d._month;_day = d._day;}Date(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;
};void Func1(Date d)
{cout << &d << endl;d.Print();
}// Date Func2()
Date& Func2()
{Date tmp(2024, 7, 5);tmp.Print();return tmp;
}int main()
{Date d1(2024, 7, 5);// C++规定自定义类型对象进行传值行为(传值传参,传值返回)必须调用拷贝构造,所以这里传值传参要调用拷贝构造// 所以这里的d1传值传参给d要调用拷贝构造函数完成拷贝,引用传参可以减少这里的拷贝Func1(d1);cout << &d1 << endl;// 这里可以完成拷贝,但是调用的不是拷贝构造函数,而是一个带参的构造函数Date d2(&d1);d1.Print();d2.Print();//这样写调用的才是拷贝构造函数,通过同类型的对象初始化,而不是指针Date d3(d1);d2.Print();// 也可以这样写,这里调用的也是拷贝构造函数Date d4 = d1;// 用的较多d2.Print();// Func2返回了一个局部对象tmp的引用作为返回值// Func2函数结束,tmp对象就销毁了,相当于了一个野引用Date ret = Func2();ret.Print();return 0;
}
#include<iostream>
using namespace std;
typedef int STDataType;class Stack
{
public:Stack(int n = 4)// 默认构造函数{_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}Stack(const Stack& st)// 拷贝构造函数{// 需要对 _a指向资源创建同样大的资源再拷贝值,即深拷贝_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (nullptr == _a){perror("malloc申请空间失败!!!");return;}memcpy(_a, st._a, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;}void Push(STDataType x){if (_top == _capacity){int newcapacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, newcapacity *sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}~Stack()// 析构函数{cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}private:STDataType* _a;size_t _capacity;size_t _top;
};// 两个Stack实现队列
class MyQueue
{
public:
private:Stack pushst;Stack popst;
};int main()
{Stack st1;st1.Push(1);st1.Push(2);// Stack不显示实现拷贝构造,用自动生成的拷贝构造完成浅拷贝// 会导致st1和st2里面的_a指针指向同一块资源,并且会析构两次,程序崩溃Stack st2 = st1;MyQueue mq1;// MyQueue自动生成的拷贝构造函数,会自动调用Stack拷贝构造完成pushst / popst// 的拷贝,只要Stack拷贝构造函数自己实现了深拷贝,他就没问题MyQueue mq2 = mq1;return 0;
}
5、赋值运算符重载函数
先说说什么是运算符重载,运算符重载,是对运算符重定义,可以构成函数重载
5.1 运算符重载
1. C++规定,类类型对象使用运算符时,必须转换成调用对应的重载运算符函数,若没有对应的运算符重载,则会编译报错。
2. 重载运算符函数的名字 = operator + 要定义的运算符。具有返回类型和参数列表以及函数体。
3. 重载运算符函数的参数个数 = 该运算符作用的运算对象个数。
一元运算符有一个参数,二元运算符有两个参数,
二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
4. 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,
因此运算符重载作为成员函数时,参数比运算对象少一个。
5. 重载操作符函数,至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:int operator+(int x, int y)
6. 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
7. 不能创建一些新的操作符:比如operator@。
8. .* :: sizeof ?:三目运算符 . 注意以上5个运算符不能重载。(选择题常考,记一下)
解释一下这个 .* 不怎么用,但是要解释一下
#include<iostream>
using namespace std;// 编译报错:“operator + ”必须至少有一个类类型的形参
int operator+(int x, int y)
{return x - y;
}class A
{
public:void func(){cout << "A::func()" << endl;}
};
typedef void(A::* PF)(); //成员函数指针类型
// 本来是typedef void(*)() PF; 但函数指针和数组指针typedef的别名,要写在()里面
// 然后是成员函数,就加个A::
// 此时 PF = void(A::*)()int main()
{// C++规定成员函数要加&才能取到函数指针PF pf = &(A::func);A obj;// obj对象调用成员函数指针时,使用.*运算符(obj.*pf)();return 0;
}
9. 一个类重载的运算符要有意义,比如Date类重载operator-就有意义,但是重载operator+就没有意义。
10. 重载++运算符时,有前置++和后置++,
C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。
11. 重载 << 和 >> 时,需要重载为全局函数,
因为重载为成员函数,this指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了 对象 << cout,平时是cout << 对象,不符合使用习惯和可读性。
重载为全局函数把ostream / istream放到第一个形参位置就可以了,第二个形参接受类类型对象。
#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 Print(){cout << _year << "-" << _month << "-" << _day << endl;}//private:int _year;int _month;int _day;
};// 重载为全局函数时访问对象私有成员变量存在问题
// 有几种方法可以解决:
// 1、成员放公有
// 2、Date提供getxxx函数
// 3、友元函数,C++初阶——类和对象(下)
// 4、重载为成员函数,但参数会少一个,因为第一个运算对象传给了隐藏的this指针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(2024, 7, 5);Date d2(2024, 7, 6);// 运算符重载函数可以显示调用operator==(d1, d2);d1 == d2;// 编译器会转换成operator==(d1, d2);return 0;
}
#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 Print(){cout << _year << "-" << _month << "-" << _day << endl;}bool operator==(const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}Date& operator++(){cout << "前置++" << endl;//...return *this;}Date operator++(int)//也可以写成Date operator++(int i),但不会使用i{Date tmp;cout << "后置++" << endl;//...return tmp;}private:int _year;int _month;int _day;
};
int main()
{Date d1(2024, 7, 5);Date d2(2024, 7, 6);// 运算符重载函数可以显⽰调用d1.operator==(d2);d1 == d2;// 编译器会转换成d1.operator==(d2);++d1;// 编译器会转换成d1.operator++();d1++;// 编译器会转换成d1.operator++(0);()里随便一个整数return 0;
}
5.2 赋值运算符重载函数
赋值运算符重载函数的主要任务是完成两个已经存在的对象直接的拷贝赋值,
注意:跟拷贝构造区分,拷贝构造用于一个已经存在的同类对象初始化给另一个要创建的对象。
赋值运算符重载的特点:
1. 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。
赋值运算符重载函数的参数 建议写成 const 当前类类型引用,
引用为了减少拷贝,const为了不改变对象里的值。
2. 有返回值,且建议写成当前类类型引用,
传引用返回减少拷贝,有返回值目的是为了支持连续赋值。
3. 没有显式实现时,编译器会自动生成一个默认赋值运算符重载函数,
对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),
对自定义类型成员变量会调用他的赋值运算符重载函数。
4.像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载函数。
像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。
像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载函数会调用Stack的赋值运算符重载函数, 也不需要我们显示实现MyQueue的赋值运算符重载函数。
注意:如果一个类显示实现了释放资源的析构函数,那么他就需要显示写 赋值运算符重载函数(深拷贝),否则就不需要。
实现:需要进行深拷贝时,赋值运算符函数需要自己实现。
#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(const Date& d){cout << " Date(const Date& d)" << endl;_year = d._year;_month = d._month;_day = d._day;}// 传引用返回减少拷贝// d1 = d2;Date& operator=(const Date& d){// 不要检查自己给自己赋值的情况if (this != &d){_year = d._year;_month = d._month;_day = d._day;}// d1 = d2表达式的返回对象应该为d1,也就是* thisreturn *this;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};
int main()
{Date d1(2024, 7, 5);Date d2(d1);Date d3(2024, 7, 6);d1 = d3;// 需要注意这里是拷贝构造,不是赋值重载// 请牢牢记住赋值重载完成两个已经存在的对象直接的拷贝赋值// 而拷贝构造函数用于一个已经存在的同类对象初始化给另一个要创建的对象Date d4 = d1;return 0;
}
6、取地址运算符重载函数
先说说什么是const成员函数
6.1 const成员函数
1. 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。
2. const修饰成员函数,函数声明和定义一致
3. const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
如:const 修饰Date类的Print成员函数,
Print隐含的this指针由 Date* const this (this指针默认的类型,防止this被改变)变为 const Date* const this
注意:没有const修饰成员函数,那么const对象就不能调用成员函数,因为权限不能放大
优点:
1. const对象,和非const对象都可以调用const成员函数,因为权限可以平移和缩小。
2. const一般加在不修改对象的函数参数列表后面,防止修改对象
#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 Print(const Date* const this) constvoid Print() const{cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{// 这里非const对象也可以调用const成员函数是一种权限的缩小Date d1(2024, 7, 5);d1.Print();const Date d2(2024, 8, 5);d2.Print();return 0;
}
6.2 取地址运算符重载函数
取地址运算符重载函数分为普通取地址运算符重载和const取地址运算符重载函数,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。
除非一些很特殊的场景,如:我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。
class Date
{
public:Date* operator&(){return this;// return nullptr;}const Date* operator&()const{return this;// return nullptr;}private:int _year; // 年int _month; // 月int _day; // 日
};