C++类和对象(下)详细笔记
-
- 一、拷贝构造函数
- 二、赋值运算符重载
-
-
- [2.1 运算符重载](#2.1 运算符重载)
- [2.2 赋值运算符重载。](#2.2 赋值运算符重载。)
-
- 三、取地址运算符重载
-
-
- [3.1 const成员函数](#3.1 const成员函数)
- [3.2 取地址运算符重载](#3.2 取地址运算符重载)
-
- 四、再探构造函数
- 五、友元
- 六、内部类
- 七、匿名对象
- 八、static成员
一、拷贝构造函数
拷贝构造是一个特殊的构造函数,是构造函数的一个重载。当构造函数的第一个参数是对自身类类型对象的引用 ,且其他参数都有默认值,那么这个构造函数就是拷贝构造函数(调用拷贝构造又分为两种情况,一种是传值传参,另一种是传值返回)。
拷贝构造有以下特点:
- 是构造函数的一个重载(名字相同,参数不同)。
- 拷贝构造可以有多个参数,但是第一个参数必须是对类类型对象的引用,第一个参数使用传值方式会造成编译报错,语法层面上会引发无穷递归:
C++规定传值传参必须调用拷贝构造
调用函数时要先调用拷贝构造,而拷贝构造是以传值的方式进行传参,所以传值时又会调用拷贝构造,调用拷贝构造又传值,又调用拷贝构造...因此程序会无穷递归下去,无穷无尽,所以会引发编译报错:
cpp
#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)//拷贝构造
{
_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(2024, 12, 21);
d1.Print();
Date d2(d1);//将d1的日期拷贝给d2
//上一行代码(拷贝构造)也可以写成:Date d2 = d1;
d2.Print();
return 0;
}
- 未定义拷贝构造函数,编译器会自动生成对应的拷贝构造。编译器自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(也就是一个一个字节拷贝),对于自定义类型成员变量会去调用他的拷贝构造。
- 内置类型 如Date类没有指向资源,不需要显示地写拷贝构造,编译器自动生成的就可以完成拷贝功能;而像Stack这样的类虽然也是内置类型,但是它动态开辟了资源,编译器自动生成的拷贝构造只能完成值拷贝/浅拷贝,不符合我们的需求。所以我们要自己去写拷贝构造实现深拷贝。自定义类型 如MyQueue不需要自己实现拷贝构造,编译器自动生成的就可以使用,因为编译器自动生成的拷贝构造会去调用Stack的拷贝构造。如果一个类显示地实现了析构函数去释放资源,说明有空间需要清理释放,这时就需要显示地实现拷贝构造。
二、赋值运算符重载
2.1 运算符重载
当运算符被用于类类型的对象时,被允许通过运算符重载对其重新定义为另一种含义。C++规定,运算符被用于类类型对象时必须转换成调用对应的运算符重载,若没有对应的运算符重载则会编译报错。运算符重载是具有特殊形式的函数,其基本形式为operator+要定义的运算符,如operator==()。和其他函数一样,它也有对应的返回值和参数、函数体。
cpp
type operator运算符(参数)
{}
- 重载运算符函数的参数个数必须和该运算符运算作用对象的个数相等。一元运算符就只有一个参数,二元运算符有两个参数(操作符左运算对象指向第一个参数,右运算对象指向第二个参数)。
- 若重载运算符函数是成员函数,那么第一个参数默认传给隐含的this指针,表面上参数个数和运算符运算作用对象数量不一致,但其实还是一致的,只是this指针不显示而已。
cpp
#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& d2)//两个日期相比较
{
return this->_year == d2._year
&& this->_month == d2._month
&& this->_day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
/*operator==(d1, d2);*/
d1 == d2;// 编译器会自动转换成 operator==(d1, d2);
return 0;
}
- 不能重载的五个运算符有:.* :: ?: sizeof .
- 运算符重载时要看重载后的运算符是否有意义,比如说Date类重载operator-就有意义,而operator+就没有意义(日期-日期等于天数,但是日期+日期就没有意义了)。
2.2 赋值运算符重载。
赋值运算符重载是默认成员函数 ,他用于完成两个已经存在的对象的赋值拷贝。而拷贝构造是一个已经存在的对象去初始化拷贝另一个将要创建的对象。
cpp
#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)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& operator=(const Date& d)
{
// 不要出现⾃⼰给⾃⼰赋值的情况
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;// d1 = d2表达式的返回对象应该为d1,也就是*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;
}
赋值运算符重载的特点:
- 赋值运算符重载是一个运算符重载,规定必须重载成成员函数。为了减少拷贝,加快程序运行效率,建议写成const加当前类类型的引用的形式。
- 有返回值,所以为了减少拷贝,建议返回也写成传引用返回的形式。
- 没有自己显示实现赋值重载时,编译器会动自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。
- 如果需要显示实现析构函数并释放资源,就需要自己去实现赋值重载,否则不需要。
三、取地址运算符重载
3.1 const成员函数
将函数用const来修饰的函数称为const成员函数,其形式为type函数名(参数)const :
cpp
type name(参数)const
Date* this指针的实际形式为Date* const this,表明不能修改类里的任何一个成员变量。const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this 。成员函数不修改成员变量的建议都加上const。
cpp
#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) const
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 5);
d1.Print();
const Date d2(2024, 8, 5);
d2.Print();
return 0;
}
3.2 取地址运算符重载
取地址运算符重载可以分为普通取地址运算符重载和const运算符重载,这个重载函数不需要我们自己显示地去实现,编译器默认生成的就可以使用了。但是在一些极特殊的情况下需要自己去写(不想让别人访问到我的地址):
cpp
class Date
{
public:
Date * operator&()
{
return this;
// return nullptr;//写成这样别人就访问不到了
}
const Date * operator&()const
{
return this;
// return nullptr;//写成这样别人就访问不到了
}
private:
int _year;
int _month;
int _day;
};
四、再探构造函数
构造函数初始化有两种方式,一种是直接在函数体内初始化,另一种则是初始化列表。初始化列表的基本形式:以冒号开始,逗号分割成员变量,成员变量后面跟着一个括号,括号内可以是初始值或表达式。
cpp
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 12, 23);
d1.Print();
return 0;
}
-
每个成员变量只能在初始化列表出现一次,语法逻辑上初始化列表是每个成员初始化定义的地方 。
成员变量可以在初始化列表初始化,也可以在函数体内初始化,但是有三种成员变量必须在初始化列表初始化:
cpp
private:
//可在函数体或初始化列表初始化
int _year;
int _month;
int _day;
//必须在初始化列表初始化
const int _a;
int& _ret;
前面说了,初始化列表是成员初始化定义的地方,而const成员和引用成员只有一次初始化的机会,那就是定义的时候。所以这两个成员必须在初始化列表初始化。还有一类必须在初始化列表初始化的成员就是自定义成员变量,当该成员变量没有默认构造函数时,就必须在初始化列表进行初始化。
- 往后初始化成员变量时尽可能地用初始化列表,因为你没有在初始化列表初始化的成员他也会走初始化列表。如果成员变量在声明的时候给了缺省值,编译器会用缺省值进行初始化。而没有给缺省值的对于内置类型成员变量是不确定的,是否初始化取决于编译器;对于自定义类型成员变量,如果没有默认构造这个时候就会报错,有默认构造就会调用他的默认构造。
- 初始化列表初始化成员变量的顺序跟成员变量定义的顺序是一致的,a1先声明那就先初始化a1。当然还是建议声明和初始化列表中成员变量的顺序是一致的
五、友元
友元提供了能突破访问限定符封装的方式。友元有友元函数和友元类,其基本形式为:在函数声明或类声明前+friend,并且把友元声明放到类里面。友元函数仅仅是一个声明,不是类的成员函数。
cpp
#include<iostream>
using namespace std;
class B;// 前置声明,否则A的友元函数声明编译器不认识B(默认往上找B类,找不到就报错,所以要前置声明)
class A
{
friend void func(const A & aa, const B & bb);// 友元声明
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
friend void func(const A& aa, const B& bb);// 友元声明
private:
int _b1 = 3;
int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
func(aa, bb);
return 0;
}
func函数做了友元声明,因此可以访问A、B类的成员,突破了访问限定符的限制。
友元的特点:
- 一个函数可以是多个类的友元。
- 友元类中的成员函数都可以是另一个类的友元函数,都可以访问领一个类的私有成员:
cpp
class A
{
// 友元声明
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
B类就是A类的友元类,B就可以访问A类的成员变量及成员函数
-
友元类的关系是单向的,A是B的友元,但是B不是A的友元。
-
友元类的关系不能传递,A是B的友元,B是C的友元,但是A不是C的友元。
友元会破坏封装,增加耦合度,不宜多用。
六、内部类
一个类定义在另一个类的里面,那这个类就叫做内部类。内部类不是什么很特殊的、很神奇的东西,跟定义在全局的类相比,他也只是受类域和访问限定符的影响,本质上和全局定义的类没什么区别。
- 内部类默认是外部类的友元类。
- 外部类的对象中不包含内部类:
cpp
#include<iostream>
using namespace std;
class A
{
private:
static int _k;
int _h = 1;
public:
class B // B默认就是A的友元
{
public:
void foo(const A& a)
{
cout << _k << endl;
cout << a._h << endl;
}
};
};
int A::_k = 1;
int main()
{
cout << sizeof(A) << endl;
return 0;
}
- 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
七、匿名对象
匿名对象就是没有名字的对象(定义了类型对象名的叫做有名对象,而只用类型定义出来的叫做匿名对象)。
cpp
int main()
{
A aa1;//正常定义
A aa2();// 不能这么定义对象,因为编译器⽆法识别下⾯是⼀个函数声明,还是对象定义
A();//匿名对象
A(1);//匿名对象
A aa3(2);
return 0;
}
-
匿名对象的生命周期只在当前一行,离开当前行就会调用析构清理资源,临时定义使用一下就可以定义一个匿名对象。
虽然匿名对象的内容只有一点点且很简单,但是在后面的学习中会经常用到匿名对象,其作用就是在某些场景下会很方便。
八、static成员
用static修饰的成员变量叫作静态成员变量,他的初始化要在类外面进行。
静态成员变量的特点:
- 静态成员变量不存在于对象中,他存放在静态区中,生命周期为全局,他为所有类对象所共享。
cpp
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << sizeof(A) << endl;
}
private:
static int ptr;
};
int main()
{
A a;
a.Print();
return 0;
}
从运行结果可以看出静态成员变量不存在对象中
- 用static修饰的函数叫作静态成员函数,静态成员函数没有this指针。
- 静态成员函数可以访问静态成员,但是不能访问非静态成员,因为没有this指针。
- 静态成员也是类的成员,所以也受访问限定符的限制。
- 静态成员不走初始化列表初始化,所以不能在声明的地方给缺省值去初始化。