C入门四式类和对象中
C++入门四式——类和对象(中)
this指针
我们的date类中有int跟print()两个成员,函数体中没有关于不同对象的区分,那当d1调用int和print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢? 这里C++给出了一个隐含的this指针解决这里的问题。 //.h #include using namespace std; class date { public: //void init(date* const this,int year…… void init(int year,int month,int day) { //this->_year=year; _year = year; //…… _month = month; //this->_day=day; _day = day; } //void print(date* const this) void print() { cout « _year « “/” « _month « “/” « _day< using namespace std; class A { public: void Print() { cout « “A::Print()” « endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; } 答案是C。 可能有的朋友注意到了void指针的问题,我们知道void指针不能进行指针的+-整数和解引用操作 。会感觉这题应该是运行崩溃。其实不然,我们使用了指针就一定解引用了吗? 可以看出,我们调用Print函数是没有解引用的,所以运行正确。 2.下⾯程序编译运⾏结果是() A、编译报错 B、运⾏崩溃 C、正常运⾏ #include using namespace std; class A { public: void Print() { cout « “A::Print()” « endl; cout « _a « endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; } 这题的答案则是B。 _原因是调用print函数时,打印_a,this- >a需要解引用。 空指针解引用不会报错,而是运行崩溃,大家可以试试。
类的默认成员函数
默认成员函数是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。一个类我们不写的情况下默认会生成以下6个默认成员函数,我们着重讲的是前4个。 我们学习时,从两个方面去学习:
- 我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
- 编译器默认生成的函数不满足我们的要求时,我们要自己实现,如何实现?
构造函数
构造函数是特殊的函数成员 ,需要注意的是,构造函数虽然叫构造,但跟开空间创造对象没关系,而是对象实例化时初始化对象。 构造函数的本质是要替代我们之前所写的类中的Init函数的功能,构造函数自动调用的特点就完美的代替了Init. 特点:
- 函数名与类名相同
- 无返回值(不是void,不用写返回值)
- 对象实例化时系统会自动调用对应的构造函数
- 构造函数可以重载
- 如果类中没有显式定义,则编译器会生成一个无参的默认构造函数,一旦显示定义,编译器不再生成。
- 编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是否初始化不确定;对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数就会报错。
我们要初始化这个成员变量就要用初始化列表才能解决(类和对象(下)会讲 )。
#include using namespace std; class date { public: //无参 /date() { _year = 2024; _month = 2; _day = 1; }/ //全缺省 date(int year = 2025, int month = 3, int day = 14) { _year = year; _month = month; _day = day; } void print() { cout « _year « “/” « _month « “/” « _day< using namespace std; class date { public: //含参 /date(int year, int month, int day) { _year = year; _month = month; _day = day; }/ void print() { cout « _year « “/” « _month « “/” « _day< 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; }; class MyQueue { public: //编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源 // 显⽰写析构,也会⾃动调⽤Stack的析构 /~MyQueue() {}/ private: Stack pushst; Stack popst; }; 而如果我们对stack使用默认析构函数会发生内存泄漏的原因:
- 默认析构函数只会调用类成员的析构函数(如果成员是类类型),但不会释放动态分配的内存。
- stack类内部使用了动态内存分配,默认析构函数无法释放这些内存,从而导致内存泄漏。
同时,通过显示写myquene的析构 ,我们可以看出他也会自动调用stack的析构。
- 一个局部域的多个成员,后定义的先析构
拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,从中我们可以看出拷贝构造函数是一个特殊的构造函数。 拷贝构造函数的特点:
- 拷贝构造函数是构造函数的一个重载。
- 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器会直接报错。因为语法逻辑上会引发无穷递归调用。
#include
using namespace std;
class date
{
public:
date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//正确形式
//使用引用,同时d不期望被改变,加const
date(const date& d)
{
_year =d. _year;
_month =d. _month;
_day =d. _day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d1(2025, 3, 14);
d1.print();
return 0;
}
- c++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用构造拷贝完成。
- 若未显示定义构造拷贝,编译器会自动生成拷贝构造函数,自动生成的构造拷贝对内置类型会完成值拷贝/浅拷贝(即一个字节一个字节的拷贝),对自定义类型变量会调用他的构造拷贝。
- 一个类如果显式实现了析构并释放资源,那么他就需要显示写拷贝构造,否则不需要。例如stack,必须显示写析构且释放资源,不然就报错。stack是必须显示写拷贝构造的,因为:
- 传值返回会产生一个临时对象调用拷贝构造,传值引用返回 ,返回的是返回对象的别名,没有产生拷贝。但如果返回的是当前函数域的局部对象,函数结束就销毁了,是不能传引用返回的,这时候的引用是一个野引用。 #include using namespace std; class date { public: date(int year, int month, int day) { _year = year; _month = month; _day = day; } //不是拷贝构造,虽然也可以拷贝,但是只是普通的构造函数 date(const date* d) { _year = d->_year; _month = d->_month; _day = d->_day; } //拷贝构造 date(const date& d) { _year =d. _year; _month =d. _month; _day =d. _day; } private: int _year; int _month; int _day; }; int main() { date d1(2025, 3, 14); //普通构造 date d2(&d1); //拷贝构造 date d3(d1); date d4 = d1;//这样也是拷贝构造哦 return 0; }
赋值运算符重载
运算符重载
- 当运算符被用于类类型的对象时,我们可以通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转化成对应运算符重载 ,若无,则报错。
- 运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同组成。它也有返回类型和参数列表,函数体 eg: int operator+=(const date& d)
- 重载运算符的参数个数和该运算符作用的运算对象数量一样多。 一元运算符一个参数,二元两个。左侧运算对象传给第一个参数,右侧传给第二个。
- 如果一个重载运算符成员是函数成员,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
- 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
- 不能通过连接语法中没有的符号来创建新的操作符,eg:operator@.
- .* :: sizeof ?:(三目操作符) . 这五个运算符不能重载。
- 重载操作符至少有一个类类型参数
- 一个类需要重载哪些运算符,是看那些运算符重载后有意义,比如date类重载-就有意义(看两个日期相差多少天),但是重载operator+就没有意义。
- 重载++运算符时,后置++增加一个int形参,跟前置++构成区分。 operator++();//为前置++ operator++(int x);//为后置++
- _重载 «和»时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象< using namespace std; class date { public: date(int year, int month, int day) { _year = year; _month = month; _day = day; } date(const date& d) { _year = d._year; _month = d._month; _day =d. _day; } bool operator==(const date& d) { if ((_year == d._year) && (_month == d._month) && (_day == d._day)) return true; else return false; } date operator++() { cout « “前置++” « endl; ++_day; return *this; } date operator++(int a) { cout « “后置++” « endl; date tmp(*this); ++_day; return tmp; } private: int _year; int _month; int _day; }; //.cpp #include"test.h" int main() { date d1(2025, 3, 16); date d2(2025, 3, 16); d1.operator==(d2);//运算符重载可以显式调用 d1 == d2; ++d1; d2++; return 0; }
赋值运算符重载
赋值运算符重载是一个默认成员函数,用于完成两个 已经存在的 对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。 赋值运算符重载的特点:
- 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const当前类类型引用,否则会传值传参有拷贝。
- 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值。
- 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认运算符重载行为跟默认拷贝构造函数类似,对内置类型成员会完成浅拷贝,对自定义类型成员变量会调用它的赋值重载函数。
- 跟拷贝重载一样,一个类如果显式实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则不需要。例如stack,必须显示写析构且释放资源,不然就报错。
取地址运算符重载
const成员函数
- 将const修饰的成员函数称之为const成员函数,const修饰成员放到成员函数参数列表的后面。 eg: void print()const; == void print(const date*const this);
- const实际修饰该成员函数隐含的this指针,表明该成员函数中不能对类的任何成员进行修改。const修饰date类的print成员函数,print隐含的this指针由date*const this 变为const date* const this.
取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就够了,不需要去显示实现。除非一些特殊的场景,比如:我们不想让别人取到我们当前的地址,就可以自己实现一份,胡乱返回一个地址。 date* operator&() { return this; } private: int _year; int _month; int _day; __
__