目录
2.静态成员函数(static member function)
一.类中默认的成员函数
1.构造函数(constructor)
构造函数是一个成员函数,它在创建对象时被调用,并执行初始化成员的操作。类定义中数据成员的声明不会初始化数据成员,声明只给出数据成员的名称和类型。
特征:(1)函数名与类名相同
(2)无返回值——甚至不能是void
(3)对象实例化时编译器自动调用对应的构造函数——用来进行初始化,尽管我们也可以执行一些其他任务,例如值的验证,但这些任务也被认为是初始化的一部分
(4)构造函数可以接收参数——这意味着构造函数可以被重载,确切的说是有参数的构造函数——一般被称为参数构造函数(parameter constructor)
(5)如果类中没有显式定义构造函数,编译器会自动生成一个无参的构造函数——一般被称为默认构造函数(default constructor)。一旦用户显式定义,编译器将不再生成。
注:默认生成的无参构造函数(synthetic default constructor)不会对内置类型的成员变量进行处理,但会对自定义类型的成员变量(如成员对象)进行初始化处理,所以C++11针对这个问题进行优化,即内置类型成员变量在类中声明时可以给默认值——声明时给缺省值。为了帮助理解,代码如下:
#include<iostream>
using namespace std;
class Calendar {
public:
Calendar()
{
cout << "1" << endl;
n = 0;
}
private:
int n;
};
class Date {
private:
int _year;
int _month;
int _day;
Calendar A;
};
int main(void)
{
Date d1;
return 0;
}
输出结果为:1
(6)无参的构造函数和全缺省的构造函数都被称为默认构造函数,两个函数同时存在且构成函数重载,但依旧推荐只保留一个,原因如下,先看代码
#include<iostream>
using namespace std;
class Date {
public:
Date()
:_year(2020),_month(1),_day(1)
{
cout << "1" << endl;
}
Date(int year = 1,int month = 1 ,int day = 1)
{
_year = year;
_month = month;
_day = day;
cout << "2" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(1,1,1);
Date d2;
return 0;
}
这段代码编译是无法通过的,原因在于Date d2;具体的报错为:
'Date::Date':ambigous call to overloaded function.
class"Date" has more than one default constructor.
把Date d2;删掉,输出结果为2。
(7)在上述代码中,输出为1的构造函数下的这串语句被称为初始化列表,也是用于初始化数据成员。格式为:dataMemember(parameter),...,dataMemember(parameter)
严格来讲,构造函数体中的赋值语句只能称其为赋初值,部分无法通过赋值的方式初始化成员可以使用初始化列表来进行初始化操作(e.g.const修饰的成员,引用类型成员,成员对象(无默认构造函数))。
一般情况下推荐使用初始化列表进行初始化。然而,有时我们必须使用构造函数的主体,通过赋值来初始化复杂的数据成员,这些成员不能在初始化列表中简单地被初始化。列如验证参数,根据需要打开文件夹,甚至打印信息以验证是否调用该构造函数
注意:成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
2.拷贝构造函数(copy constructor)
有些时候,我们希望将对象的每个数据成员初始化为与先前创建的对象的相应数据成员相同的值。在这种情况下,我们可以使用拷贝构造函数,其作用就是将给定对象的数据成员值复制到刚刚创建的新对象中。
特征如下:
(1)拷贝构造函数是构造函数的一个重载形式,因其参数列表是固定的,所以无法重载拷贝构造函数。
(2)拷贝构造函数只有一个参数,按引用接收源对象(一般用const修饰),使用传值方式编译器会直接报错(因为会引发死递归,个人理解——创建新对象时需要传参给拷贝构造函数,如果拷贝构造函数不按引用接收源对象,转而创建一个临时对象接收源对象,此时又需要调用拷贝构造函数用来将源对象拷贝给临时对象,进而陷入一个无穷递归的处境)
(3)若未显式定义,编译器自动生成的拷贝构造函数(synthetic copy constructor)将按照内存存储字节完成拷贝,这种拷贝被称为浅拷贝,或者是值拷贝,大多数情况下,最好创建自己的拷贝构造函数,因为在一些应用场景下会触发浅拷贝问题(后续深入)
3.析构函数(destructor)
与构造函数类似,但析构函数用于对象的清理和回收。当实例化对象超出其作用域时,系统保证自动调用和执行析构函数,完成对象中资源的清理工作
注意:并非是完成对象本身的销毁,局部对象的销毁工作是由编译器完成的。
特征:
(1)析构函数名是在类名前加上~
(2)无参数,无返回值类型,自然也就无法被重载
(3)若未显示定义,系统会自动生成synthetic destructor。但在大多数时候,synthetic destructor并不能满足我们的需求,最好创建自己的析构函数。
4.赋值运算符重载
注:若不了解运算符重载,建议先看运算符重载。
左操作数和右操作数的性质不同。左操作数是一个接收操作副作用的左值对象,右操作数是一个不应在处理过程中更改的右值对象。
不同于拷贝构造函数,以现有对象创建新对象;赋值运算符重载,两个对象都必须存在,我们只更改左对象,使左对象是右对象的精确副本。
构建函数时的部分注意事项:
(1)由于副作用,宿主对象(host object——this指针指向的对象)也就是左操作数不能是常量
(2)返回的对象不推荐是常量,因为我们应当考虑级联运算符(x = y = z)
(3)应该验证宿主对象和参数对象不是同一对象(地址不同)。如果对象是在堆中创建的,这一点尤其重要。
由于在复制参数对象的内容之前必须删除宿主对象,如果两个对象相同,则参数对象(与宿主对象的物理地址相同)也会被删除,从而没有了要复制的内容。
(4)众所周知,赋值对象是从右到左结合的,换言之,z = y = x,这被解释为z = ( y = x )。但是,C++要求z被看作对y的引用。所以返回的对象必须通过引用返回,如果你要考虑级联运算的话
(5)参数类型推荐设为const修饰的引用,这是处于安全和效率考虑
(6)推荐返回*this指针,符合级联运算的含义
区分:
现有日期类Date
Date d2(d1) //在d1已经实例化的情况下 ;
Date d2 = d1 //同上 ;
d2 = d1 //在d2 和 d1 已经实例化的情况下;
区分三个语句:
Date d2 = d1 等同于Date d2(d1),都是拷贝构造函数
d2 = d1 应该是 d2 operator= d1
5.普通对象和const修饰对象的取地址&符号的重载
这个很好理解,像这样
Date* operator&()
{
return this;
}
二.运算符重载(operator overloading)
重载原理:
运算符重载是使用同一运算符的两个或者多个操作的定义。
C++使用一组名为运算符的符号来处理基本数据类型。这些符号大多数都被重载以处理多种数据类型。这意味这,C++中两个表达式使用相同的符号,但可能会有两种不同的解释,编译器也会有不同的处理过程。
着重记忆不可重载符号即可:
作用域范围::
成员选择.
指针成员选择.*
条件?:
不推荐重载:
取地址&(C++对这个运算符定义了一个非常特殊的含义,不能保证这个特殊的含义可由用户来实现)
按位与&
按位或|
逻辑与&&
条件或||
逗号,
重载的注意事项:
(1)优先级 ,重载无法更改运算符的优先级
(2) 结合性,重载无法更改运算符的结合性,大多数运算符具有从左到右的结合性,但也有一些具有从右到左的结合性
(3)交换性,重载不能更改运算符的交换性。如( a - b ) 与(b - a )不同
(4)元数,重载无法更改运算符的元数。我们不需要担心唯一的三元运算符(?:),因为它不可重载
(5)不允许新的运算符,我们不能发明新的运算符,只能重载现有的可重载运算符
(6)不允许组合运算符,我们不能组合两个运算符来创建一个新的运算符符号
运算符函数
要为用户定义的数据类型重载运算符,必须编写运算符函数(operator function),该函数用作运算符。此函数名以保留字operator开始,后跟需要重载的运算符的符号。
格式:
返回类型 需要重载的运算符的符——即函数名称 (参数列表)
大多数可重载运算符可以定义为成员函数或者非成员函数。少数可重载运算符只能作为成员函数重载;少数可重载函数只能作为非成员函数重载。
重载为成员函数
对于一元运算符而言,宿主对象是其唯一的操作数(参数)
将二元运算符重载为成员函数时,其第一个参数一定是宿主对象
重载为非成员函数——重载为全局函数或者是友元函数
使用全局函数来重载二元运算符没有任何限制,但该方法存在缺点。运算符函数的定义代码会个冗长且更复杂,因为需要使用类的访问器函数和更改器函数来访问类的数据成员,不推荐开发这类型的代码
友元函数我们在后续会提到
三.static成员
1.静态数据成员
静态数据成员(static data member)是不属于所有实例的数据成员,但为所有类对象所共享。同时,它也属于类本身。数据成员属于类,它们的声明必须包含在类的定义中,静态成员须以关键字static修饰限定。
初始化
我们知道实例数据成员通常是在构造函数中初始化的,但静态数据成员不属于所有任何实例,这意味着静态数据成员不能在构造函数中初始化。
前文提到的C++11优化的声明时给缺省值也不行,报错为:
a static data member with an in-class initializer must have non-volatile const integral type or be specified as 'inline'
从善如流,加上const修饰确实可行,而加上inline则会报错:
inline variables require at least '/std:C++17'
最推荐的方法是,静态成员变量在类中声明,在类后的全局区域中定义(也就是初始化)
2.静态成员函数(static member function)
在声明和初始化静态数据成员后,我们必须寻求访问静态数据成员的。因为静态数据成员通常是私有的,所以我们需要一个公共成员函数来实现这一点,尽管这可以由实例成员函数完成,但我们通常为此使用静态成员函数。静态成员函数可以通过对象访问静态数据成员,也可以在不存在实例对象时通过类的名称访问静态数据成员
注:静态数据成员没有宿主对象,因为静态成员不与仍和实例关联。所以也理所应当的无法访问任何非静态成员。
定义
同上,类中以static关键字限定声明,类外定义,与成员函数别无二致
如:
int Date::Getcount()
{
return count;
}
四.friend友元
1.友元函数
C++允许函数被声明为类的友元函数。友元函数没有宿主对象,但它被授予权限(友元),这样友元函数就可以在不调用公共成员函数的情况下访问类的私有数据成员和成员函数,友元函数定义在类外部,不属于任何类,但需要在类内部声明(函数前以friend修饰)
注:
(1)友元函数不能用const修饰
(2)友元函数可以在类定义的任何地方声明,不受类的访问限定符限制
(3)一个函数可以是多个类的友元函数
2.友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
注:
(1)友元关系是单向的,不具有交换性
(2)友元关系无法传递
(3)友元关系无法继承
友元提供了一种突破封装的方式,有时候提供了便利。但友元会增加耦合度,突破了封装,所有推荐常用。