目录
1. 运算符重载
1.1 "==" 的重载
1.2 前置 "++" 和后置 "++" 重载
1.3 流插入 "<<" 和流提取 ">>" 重载
1.4 运算符重载注意事项
2. const成员和static成员
2.1 const成员
2.2 static成员
3. 友元
3.1 友元函数
3.2 友元类
4. 内部类
1. 运算符重载
对于 + - * / 这类运算符,内置类型如int char等都可以支持运算,但一个类直接使用这些运算符的行为是未定义的,C++为了增强代码的可读性引入了运算符重载,使得我们的自定义类型可以像内置类型一样使用这些运算符
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
1.1 "==" 的重载
运算符重载函数可以写在类的外面(麻烦,不推荐),也可以写在类内部作为成员函数,比如要重载运算符 == ,可以写成
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// == 的运算符重载
// 下面的实现等价于 bool operator==(Date* this, Date d2)
bool operator==(Date d) // 这里第一个参数是隐含了的this指针,指向d1,传进来是d2是第二个参数
{
return (_year == d._year)
&& (_month == d._month)
&& (_day == d._day);
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2000, 1, 2);
Date d2(2008, 8, 8);
d1 == d2; // == 用于判断两个日期是否相等,但需要我们对运算符 == 重载后才能使用
// d1.operator==(d2) 实际上是这种形式
return 0;
}
1.2 前置 "++" 和后置 "++" 重载
“++” 是一个很特殊的运算符,它分为前置 ++ 和后置 ++ ,对于内置类型完成 +1的操作,区别在于它的返回值不同,如对于int类型: i++ 和 ++i 是不一样的,前者返回 i ,然后 i 自增1;后者 i 先自增1,然后返回自增之后的值(i + 1)
根据运算符重载的规则,前置++和后置++的重载形式
前置++:自定义类& operator++()
后置++:自定义类 operator++()
// 以Date类为例,可写为
Date& operator++()
Date operator++()
但上面的形式无法区分,我们可以对其进行处理,使用重载将这两个函数区分开来
前置++
Date& operator++()
后置++ (增加一个int参数跟前置++构成重载进行区分)
Date operator++(int) // 这里规定用int ,也可用形参int i接收
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator+=(int day)
{
// +=函数重载的逻辑
}
// 前置++
Date& operator++()
{
*this += 1; // 对象++
return *this; // 返回++后的对象
}
// 后置++
Date operator++(int)
{
Date ret = *this; // 将原对象保存一份
*this += 1; // 对象++
return ret; // 返回++前的对象
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2000, 1, 2);
Date d2(2008, 8, 8);
++d1; // d1.operator++(&d1);
d2++; // d2.operator++(&d2,0)
return 0;
}
在Linux下,通过反汇编查看后置++重载中默认传入int值,发现call函数<_ZN4DateppEi>时,的第二个参数寄存器里存的是 0 ,因此默认传过去的值为int 0
400508: 48 89 c7 mov %rax,%rdi
40050b: e8 24 00 00 00 callq 400534 <_ZN4DateC1Eiii> // call默认构造
400510: 48 8d 45 e0 lea -0x20(%rbp),%rax
400514: be 00 00 00 00 mov $0x0,%esi // 第一个参数 int 0
400519: 48 89 c7 mov %rax,%rdi // 第二个参数 &d
40051c: e8 51 00 00 00 callq 400572 <_ZN4DateppEi> // call后置++
1.3 流插入 "<<" 和流提取 ">>" 重载
C++的流插入运算符 “<<” 和流提取运算符 “>>” 是C++在类库中提供的,所有C++编译系统都在类库中提供输入流类istream和输出流类ostream
C++类库中,istream类和ostream类对于内置类型的操作以成员函数的形式封装在类内部,能用来输出和输入C++标准类型的数据,我们的自定义类型是无法直接使用 “<<” 和 “>>” 进行输入输出的,因此必须进行重载才能使用
🔶 流插入运算符 “<<” 重载
C++库中流插入
arithmetic types (1)
ostream& operator<< (bool val);
ostream& operator<< (short val);
ostream& operator<< (unsigned short val);
ostream& operator<< (int val);
ostream& operator<< (unsigned int val);
ostream& operator<< (long val);
ostream& operator<< (unsigned long val);
ostream& operator<< (long long val);
ostream& operator<< (unsigned long long val);
ostream& operator<< (float val);
ostream& operator<< (double val);
ostream& operator<< (long double val);
ostream& operator<< (void* val);
stream buffers (2)
ostream& operator<< (streambuf* sb );
manipulators (3)
ostream& operator<< (ostream& (*pf)(ostream&));
ostream& operator<< (ios& (*pf)(ios&));
ostream& operator<< (ios_base& (*pf)(ios_base&));
先来看看对于内置类型,流插入运算符 “<<” 和流提取运算符 “>>”是怎么使用的,首先要了解std::cout在C++中是一个 ostream 类型的对象,因此实际上 “<<” 和 “+”一样,都存在左右操作数,<< 的左操作数是一个ostream类型的对象,右操作数可以是其他类型对象
int a = 10;
int b = 20;
左操作数 运算符 右操作数
std::cout << a;
a + b;
由于 “<<” 运算符是作为输出数据使用的,传入的参数是输入型参数,无需对自定义类型的对象做修改,可以使用const传参,对 “<<” 重载的函数形式如下
ostream& operator<<(ostream &, const 自定义类 &)
因此,对于一个自定义类型的Date类,可以试着写一下流插入运算符的重载,将重载函数定义在类内作为成员函数,第一个参数是this指针,指向一个Date类型的对象,第二个参数是一个ostream类型的对象
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
ostream& operator<<(ostream& cout) // << 返回值仍是ostream类型,以便连续输出
{
return cout << _year << "年" << _month << "月" << _day << "日";
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2000, 1, 2);
d1 << std::cout; // 重载 << ,<<的左操作数是Date类型对象d1,右操作数是ostream类型对象std::cout
// 正确输出 2000年1月2日
return 0;
}
在上面的重载中,重载函数内部的 “<<” 是内置类型操作数,可以调用库中的 “<<” 做标准输出,我们的工作只是按照我们的设计对其进行了包装
d1 << std::cout;
等价于
d1.operator<<(std::cout);
但 d1 << std::cout; 这样的使用形式和平时的用法不同,我们渴望追求类似 std::cout << d1; 的形式,由于this指针总是作为成员函数的第一个参数存在,我们无法对左操作数进行调整,因此虽然建议运算符重载时定义成成员函数,但也可以在类外重载,作为全局函数
这里会遇到一个问题,在类外面定义的函数无法访问到类中的私有成员,可以使用自定义接口或者友元解决这个问题
最终实现的流插入运算符 “<<”重载为
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 将 << 的重载声明成友元函数,这样外部的ostream& operator<<可以访问该类的私有成员变量
friend ostream& operator<<(ostream& cout, const Date& d); // 声明
private:
int _year;
int _month;
int _day;
};
inline ostream& operator<<(ostream& cout, const Date& d)
{
return cout << d._year << "年" << d._month << "月" << d._day << "日";
}
int main()
{
Date d1(2000, 1, 2);
Date d2(2008, 8, 8);
std::cout << d1;
return 0;
}
🔶 流提取运算符 “>>” 重载
C++库中流提取
arithmetic types (1)
istream& operator>> (bool& val);
istream& operator>> (short& val);
istream& operator>> (unsigned short& val);
istream& operator>> (int& val);
istream& operator>> (unsigned int& val);
istream& operator>> (long& val);
istream& operator>> (unsigned long& val);
istream& operator>> (long long& val);
istream& operator>> (unsigned long long& val);
istream& operator>> (float& val);
istream& operator>> (double& val);
istream& operator>> (long double& val);
istream& operator>> (void*& val);
stream buffers (2)
istream& operator>> (streambuf* sb );
manipulators (3)
istream& operator>> (istream& (*pf)(istream&));
istream& operator>> (ios& (*pf)(ios&));
istream& operator>> (ios_base& (*pf)(ios_base&));
同样的,对于流提取运算符 “>>”,同样也有两个操作数,与 “>>” 的重载不同的是,“<<” 重载中传入的自定义类型对象要作为输出型参数,以键盘的输入对对象进行修改,因此不能传入const类型对象,要传入普通对象的引用,对 “<<” 重载的函数形式如下
istream& operator>>(istream &, 自定义类 &)
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 同样作为友元
friend istream& operator>>(istream& cin, Date& d);
private:
int _year;
int _month;
int _day;
};
inline istream& operator>>(istream& cin, Date& d) // 不能传const
{
return cin >> d._year >> d._month >> d._day;
}
int main()
{
Date d1(2000, 1, 2);
Date d2(2008, 8, 8);
std::cin >> d1;
return 0;
}
1.4 运算符重载注意事项
- 不能通过连接其他符号来创建新的操作符:比如operator@(因为@原来就不是一个运算符)
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载
2. const成员和static成员
一个类中可以存在成员函数和成员变量,这些成员函数和变量可以有很多的关键字进行修饰,其中包括 const 和 static 这两个在C语言中用到过的关键字
2.1 const成员
在C语言中,const修饰的变量具有常量属性,不允许被修改,这一特性在C++中同样适用,但C语言中const不能用来修饰函数,C++中const可以用来修饰成员函数,也可以用来修饰对象
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
// void changeYear(Date* this)
void changeYear() // 成员函数没有被修饰
{
_year = 2023;
}
// void changeMonth(const Date* this) // 成员函数被const修饰
void changeMonth() const
{
_month = 2; // 修改成员变量出错
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d.changeYear();
d.changeMonth();
return 0;
}
问题一:const对象能调用非const成员函数吗
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
void changeYear() // 非const成员函数
{
_year = 2023;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
const Date d; // const对象
d.changeYear();
return 0;
}
不能
const对象的this指针为:const Date* this
非const成员函数中,第一个隐藏的参数接收的this指针类型为:Date* this
将const类型的指针作为参数传给非const类型的指针,会导致权限的放大,类型不匹配
问题二:非const对象能调用const函数吗
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
const void changeYear() // const成员函数
{
_year = 2023;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d; // 非const对象
d.changeYear();
return 0;
}
可以
非const对象的this指针为:Date* this
const成员函数中,第一个隐藏的参数接收的this指针类型为:cosnt Date* this
将非const类型的指针作为参数传给const类型的指针,会导致权限的缩小,这是被允许的
问题三:const成员函数内可以调用其它的非const成员函数吗
不可以
在const成员函数内部调用其它的非const成员函数,需要将const成员函数的this传参给非const成员函数的this,相当于cosnt Date* this传给Date* this,导致权限的放大
问题四:非const成员函数内可以调用其它的const成员函数吗
可以
在非const成员函数内部调用其它的const成员函数,需要将非const成员函数的this传参给const成员函数的this,相当于 Date* this传给cosnt Date* this
2.2 static成员
C++中,static关键字也可也用来修饰成员函数和成员变量
用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化,生命周期是整个程序运行期间
class A
{
public:
A()
:_sval(1) // 报错,无法通过构造函数初始化静态类数据
{}
private:
static int _sval; // 仅仅是声明
};
正确的定义方式应该是
class A
{
public:
A(){}
private:
static int _sval; // 仅仅是声明
};
int A::_sval = 1; // 类外定义,每个对象可见
注意写法,不用带static,直接 类型 + 类名::变量名 = 值
static成员变量不能在初始化列表进行初始化,因此也不能给它一个缺省值,为什么static成员变量不能在类内初始化呢?
静态成员并不是属于某一个对象,而是属于整个类,如果在类内初始化,会导致每个对象都包含该静态成员,造成错误,因而类外定义和初始化是保证static成员变量只被定义一次的好方法
一个例外是,静态常量成员可以在类内初始化
class A
{
public:
A()
{}
void showStatic()
{
cout << _sval << endl;
}
private:
static const int _sval = 1; // 可以通过这种方式初始化,但不能写进构造函数
};
因为加上const修饰之后,静态变量_sval已经不允许被修改,所有对象可以访问这个公有的变量但不能修改,也就是说这样的写法,也能保证_sval不属于某一个对象,而属于整个类
当静态成员为变量公有时,如果想访问静态成员变量,我们既可以用 <类型::静态成员变量> 访问,也可以用 <类对象::静态成员变量> 访问
class A
{
public:
A(){}
//private:
static int _sval; // 仅仅是声明
};
int A::_sval = 1;
int main()
{
A a;
cout << A::_sval << endl; // 使用类型A或者A类的对象去访问_sval都可以
cout << a._sval << endl; // 前提是_sval在类里面是公有的
return 0;
}
当静态成员变量是私有时,会受到访问限定符的限制,如何访问私有的静态成员变量呢?
一种很容易想到的方法是通过调用接口来访问,因为每个对象都可以访问到这个静态成员变量,另一种方法是 静态成员函数
静态成员函数属于整个类,不属于某一个对象,因此没有this指针,只能访问静态的成员变量
class A
{
public:
A()
:_a(1)
{}
static int showMember()
{
cout << _a << endl; // 报错:对非静态成员“A::_a”的非法引用
return _sval;
}
private:
int _a;
static int _sval; // 仅仅是声明
};
int A::_sval = 1; // 不在类外初始化会报链接错误
int main()
{
A a;
cout << A::showMember() << endl; // 不需要通过对象,直接调用
cout << a.showMember() << endl; // 也可也通过对象调用
return 0;
}
静态成员变量和普通的静态变量都存在静态区,生命周期也类似,那为什么要在类里定义一个静态成员变量而不去全局定义一个静态变量呢?
静态成员变量相比普通的静态变量的区别在于:只允许类内成员对变量做修改,可以用来统计一个类构造了多少对象(这里只是一种使用场景),静态成员函数不可以调用非静态成员函数, 非静态成员函数可以调用类的静态成员函数
static成员的特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,它属于这个类,也属于这个类的所有对象
- 静态成员变量存放在静态区,必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
3. 友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用
友元分为:友元函数,友元类
3.1 友元函数
在上面我们重定义了流插入和流提取两个运算符,为了使类外函数也能够访问到类内的私有成员,将类外的函数声明成这个类的友元函数,其形式是在类内给出一个函数声明,并用friend关键字修饰
class Date
{
friend ostream& operator<<(ostream& cout, const Date& d); // 声明
// ...
};
inline ostream& operator<<(ostream& cout, const Date& d)
{
return cout << d._year << "年" << d._month << "月" << d._day << "日";
}
当一个函数被声明成某个类的友元函数时,并不意味着它变成了这个类的成员函数,因为没有this指针,所有也不能用const修饰,但它可以直接访问类的私有成员
🔶对于友元函数
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
3.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
class A
{
friend class B; // 在A类内声明B类是我的友元
// 此时B类可以访问A类中的私有
private:
int _a;
};
class B
{ // B类中没有把A类声明为友元
public: // A类无法访问B类私有
void getClassA()
{
cout << aa._a << endl;
}
private:
int _b;
A aa;
};
🔶对于友元类
- 友元关系是单向的,不具有交换性
- 友元关系不能传递,如果B是A的友元,C是B的友元,则不能说明C时A的友元
4. 内部类
内部类:如果一个类定义在另一个类的内部,这个在内部的类就叫做内部类
此时具有两种关系:
- 内部类对外部类而言:内部类是一个独立的类,它不属于外部类,外部类不能通过对象去调用内部类,外部类对内部类没有任何优越的访问权限
- 外部类对内部类而言:内部类是外部类的友元类,可以通过外部类的对象参数来访问外部类中的所有成员
简单来说就是内部类是外部类的友元,能访问外部类的私有,但受到外部类的类域限制
外部类不是内部类的友元,不能访问内部类成员
class A
{
public:
A(int a = 1)
:_a(a)
{}
class B // B是A的内部类,在A内定义
{
public:
void getClassA(const A& a) // 通过A类型对象之间访问私有
{
cout << a._a << endl;
}
private:
int _b;
};
private:
int _a;
};
int main()
{
A a;
cout << sizeof(a) << endl; // 4 内部类与外部类无关
A::B b; // B收到A类域的限制,需要指定A::
b.getClassA(a);
return 0;
}
🔶对于内部类
- 内部类可以定义在外部类的public、protected、private都是可以的
- 内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名
- sizeof(外部类)=外部类,和内部类没有任何关系