类与对象(下)
类与对象(下)
文章目录
- 类与对象(下)
- 1.`const`修饰对象
- `const`修饰成员函数
- `const`与非`const`对象与成员函数之间的相互调用关系
- 2.六大默认成员函数的另外两个
- 实现:
- 用途--可以不让别人访问其地址
- 3.友元
- 友元函数
- 友元函数的特性:
- 友元函数的实现:
- 友元类
- 友元类的特性:
- 友元类的实现:
- 4.实现自定义类型的`cout`<<,`cin`>>
- 自定义类型<<运算符重载的实现
- 运算符重载操作数位置的规则:
- 自定义类型>>运算符重载的实现
- 5.再谈构造函数
- 初始化方式的多样化--初始化列表
- 1.首先是咱们原来的初始化方式--**函数体内初始化**
- 2.新方法--初始化列表初始化
- 初始化列表的特性
- 初始化列表的本质--重点
- explicit用法
- 6.匿名对象
- 匿名对象的特性
- 匿名对象代码实现
- 7.`static`成员
- `static`成员变量
- static成员函数
- 静态成员特性总结:
- 实战例子
- 8.c++11初始化的新玩儿法--了解即可
- 9.内部类
- 10.资料分享:
1.const
修饰对象
const
修饰成员函数
我们知道在成员函数中如何保护参数的值不被修改,只需要在**参数的前面写上const
**修饰即可。
但是,如何保护this的成员变量不被修改呢,可能有同学说我在this指针前面加一个const
不就可以了吗,可是this指针是一个隐藏的指针,你没法办像之前那样加const
,c++规定了一个语法规则,这样加const
即可保护this里面的成员变量不被修改
代码演示:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 2003, int month = 6, int day = 10)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
printf("%d-%d-%d\n", _year, _month, _day);
}
void Print() const
{
printf("%d-%d-%d\n", _year, _month, _day);
}
void f() const
{
//_year = 10;//error--const修饰成员函数无法改动成员变量
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
const Date d2(2022, 9, 5);
//这个就是说d2这个对象里面的成员变量是不允许通过成员函数被修改的
d2.Print();
return 0;
}
const Date d2(2022, 9, 5);
->这个就是说d2
这个对象里面的成员变量是不允许通过成员函数被修改的
语法规则:在成员函数后面加上const
即可,上面两个Print成员函数也属于函数重载,d1
对象调用的是非const
的print成员函数,d2
对象调用的是const
的成员函数。
const
修饰成员函数无法改动成员变量
图解演示:
const
与非const
对象与成员函数之间的相互调用关系
对象与函数之间
-
const
对象可以调用非const
成员函数吗? F–权限放大 -
非
const
对象可以调用const
成员函数吗? T–权限缩小 -
const
成员函数内可以调用其它的非const
成员函数吗?F–权限缩小
-
非
const
成员函数内可以调用其它的const
成员函数吗?T–权限放大
大家可以自己用程序去测试上面的关系!
2.六大默认成员函数的另外两个
非const
&运算符重载,const
运算符重载
实现:
代码演示:
#include<iostream>
using namespace std;
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
编译器生成的是完全够用的
用途–可以不让别人访问其地址
#include<iostream>
using namespace std;
Date* operator&()
{
return nullptr;
}
const Date* operator&() const
{
return nullptr;
}
3.友元
友元函数
由于类的封装性(访问限定符),类外部的函数是无法访问类的私有数据成员,但是有些时候我们又不得不让某个外部函数可以访问类的私有成员,友元函数就来了。就像是你配了一把你家钥匙给外面的人。
所以建议不到万不得已是不要用友元的。
友元函数的特性:
-
友元函数可以直接访问类的私有成员
-
它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声 明,声明时需要加friend关键字。
友元函数的实现:
#include<iostream>
using namespace std;
class Date
{
friend void F(Date d);
public:
Date(int year = 2003, int month = 6, int day = 10)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void F(Date d)
{
d._year = 0;
d._month = 0;
d._day = 0;
}
当然了,上面的代码没有什么实际意义,只是为了大家可以理解以下友元函数的语法规则。
友元类
友元类的特性:
-
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
-
友元关系是单向的,不具有交换性。 比如下述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
-
友元关系不能传递 如果B是A的友元,C是B的友元,则不能说明C时A的友元。
友元类的实现:
代码演示:
#include<iostream>
using namespace std;
class Time
{
friend class Date; // 声明日期类为时间类的友元类,那么日期类的成员函数全是时间类的友元函数,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
{
_hour = hour;
_minute = minute;
_second = second;
}
void f(Date& d)
{
//d._year = 1;//error,Date类是Time类的友元类,而Time类不是Date类的友元类,无法访问其私有成员
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
_t._hour = 1;//在Date类中访问Time类的私有成员
}
void SetTimeOfDate(int hour, int minute, int second)//在Date类中访问Time类的私有成员
{
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
return 0;
}
4.实现自定义类型的cout
<<,cin
>>
那么首先我们得理解内置类型的cout
<<,cin
>>的实现原理是什么?
####内置类型的cout
<<,cin
>>的实现原理
1.首先**istream
,与ostream
是两个类**,而**cout
与cin
是两个类定义出来的对象**(当然了,这些对象在using namespace std
里面就已经被定义出来了),这就是为什么你不写using namespace std
他不让你使用cout
的原因。
2.其次呢,#include<iostream>
,是包含了std里面的一些运算符重载,其中就包括了,>>
<<
,当然了里面的实现就比较复杂,咱们不做深的讨论。
自定义类型<<运算符重载的实现
代码实现:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 2003, int month = 6, int day = 10)
{
_year = year;
_month = month;
_day = day;
}
void operator<<(ostream& out)//void operator<< (Date* this,ostream& out)
{
out << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2022,9,5);
d1 << cout;
return 0;
}
运算符重载操作数位置的规则:
可是这跟我们的使用习惯不一样,我们的习惯是将cout
放在前面,d1
放在后面
运算符重载,操作数的位置是这样规定的:根据实参的位置而定,例如:假如有两个操作数,第一个操作数是第一个形参,第二个操作数是第二个实参。
可是如果我们将<<运算符重载写成成员函数的话,第一个参数就会一直被this指针所占居,无法满足我们的需求。
所以我们要将 <<运算符重载写成全局的,这样就可以让cout
去占到第一个操作数的位置了。
但是写成全局的是无法访问Date类里面的成员变量的,所以咱们要将这个函数声明成日期类的友元函数,这样才可以。
代码实现:
#include<iostream>
using namespace std;
class Date
{
friend ostream& Date::operator<<(ostream& out, const Date& d);
public:
Date(int year = 2003, int month = 6, int day = 10)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out,const Date& d)//为了访问Date,必须还要将其声明成是Date的友元函数
{
out <<d. _year << "-" << d._month << "-" << d._day << endl;
return out;
}
int main()
{
Date d1;
Date d2(2022,9,5);
cout << d1 << d2 << endl;
return 0;
}
其中out
是cout
的别名,只是为了在函数中进行区分,写成cout
也是可以的
自定义类型>>运算符重载的实现
代码演示:
#include<iostream>
using namespace std;
class Date
{
friend ostream& Date::operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
//友元并不是说,operator是Date类里面的,可以理解成好朋友吧,然后可以访问朋友的成员变量
public:
Date(int year = 2003, int month = 6, int day = 10)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
ostream& operator<<(ostream& out,const Date& d)
{
out <<d. _year << "-" << d._month << "-" << d._day << endl;
return out;
}
int main()
{
Date d1;
Date d2(2022,9,5);
cout << "输入年份d1,d2的值" << endl;
cin >> d1 >> d2;
cout << d1 << d2 << endl;
return 0;
}
其中in是
cin
的别名,只是为了在函数中进行区分,写成cin
也是可以的
5.再谈构造函数
初始化方式的多样化–初始化列表
咱们先看看两种初始化的语法规则:
1.首先是咱们原来的初始化方式–函数体内初始化
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
2.新方法–初始化列表初始化
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A()" << endl;
}
private:
int _a;
};
class Date
{
public:
Date(int year = 2022, int month = 9, int day = 6)
//初始化列表为成员变量定义的地方,创建的地方
:_year(year)
, _month(month)
, _day(day)
, _ref(_year)
, _aa(10)//引用必须在定义的时候就初始化,所以只能在初始化列表定义
, _n(10)
{
A aaa(20);
_aa = aaa;
_year = 2;
_month = 1;
_day = 1;
}
private:
int _year;
int _month;
int _day;
//那些必须在定义的时候就初始化的成员变量,必须在在初始化列表中,初始化
int& _ref;
A _aa;
const int _n;
};
int main()
{
Date d1(2003, 6, 10);
return 0;
}
初始化列表的特性
- 语法:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括 号中的初始值或表达式。
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始:
-
引用成员变量
-
const
成员变量 -
自定义类型成员(该类没有默认构造函数)–
-
有默认构造函数的时候,自定义类型是可以在函数体内初始化的,但是这样就麻烦了,不如直接在初始化列表初始化好。
初始化列表的本质–重点
初始化列表为成员变量定义的地方,所以严格点来讲,函数体内初始化并不是初始化,而是对成员变量的再处理。
所以就会有引用成员变量,const
成员变量只能在初始化列表初始化,因为他们的语法规定是:在定义的同时就必须完成初始化,后面是无法再对他们的值进行处理的。
而_year
_month
_day
他们在初始化列表初始化完成之后,还可以在函数体内进行在处理,所以他们可以不在初始化列表初始化。
总结:建议都是用初始化列表初始化
初始化列表初始化成员变量的顺序:
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
下面有一个题,大家可以做一做:
代码演示:
#include<iostream>
using namespace std;
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
}
A.输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
explicit用法
我们先来看看内置类型的隐式类型转换
#include<iostream>
using namespace std;
int main()
{
int i = 4;
double j = 4.4;
j = i;
return 0;
}
自定义类型也有隐式类型转换:
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A()" << endl;
}
private:
int _a;
};
class Date
{
public:
Date(int year = 2022, int month = 9, int day = 6)
:_year(year)
, _month(month)
, _day(day)
, _aa(10)
{
A aaa(20);
_aa = aaa;//这个运用赋值运算符重载对_aa进行再处理
_aa = 30;//这个语法c++是支持的,这个是隐式类型转换,原理是 A tmp(30) + _aa = tmp
//构造+赋值重载
//编译器进行优化后把这句代码优化成了直接调用构造函数了
}
private:
int _year;
int _month;
int _day;
A _aa;
};
int main()
{
Date d1(2003, 6, 10);
return 0;
}
隐式类型转换的实质是 A tmp(30) + _aa = tmp
,先构造,再赋值
但是编译器优化之后,将隐式类型转换优化成了直接调用构造函数了,这个是可以用程序去验证的。
当我们不想支持这种隐式类型转换的时候,可以在函数的前面加上explicit
这个关键字
explicit A(int a = 0)
:_a(a)
{
cout << "A()" << endl;
}
6.匿名对象
我们来回顾以下,我们平时是如何来调用成员函数的?
是创建一个对象,让对象取调用成员函数,而且不同的对象调用的是同一个函数。
但是有时候的需求是:是需要调用一下函数,不需要使用对象,这个时候匿名对象就起作用了。
匿名对象的特性
- 匿名对象的生命周期是这一行。
- 匿名对象也是调用构造函数来创建的。
- 当这一行结束之后,即对象销毁之后,就会调用其析构函数。
匿名对象代码实现
class Date
{
public:
Date(int year = 2022, int month = 9, int day = 6)
:_year(year)
, _month(month)
, _day(day)
{}
void Print()const
{
printf("%d-%d-%d\n", _year, _month, _day);
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
//匿名对象的生命周期在这一行
int main()
{
Date d1;
d1.Print();
Date d2(2003, 6, 10);
d2.Print();
Date().Print();//这里创建的是一个匿名变量,可以写一个析构函数看一下其生命周期
return 0;
}
7.static
成员
static
成员变量
声明为static
的类成员称为类的静态成员,用static
修饰的成员变量,称之为静态成员变量;
静态成员变量是在类外面进行初始化的
大家可以猜想以下这个A类的大小:
#include<iostream>
using namespace std;
class A
{
public:
void func()
{
//…………
}
private:
static int _n;
};
int _n = 0;
int main()
{
cout << sizeof(A) << endl;
return 0;
}
屏幕输出了一个1,1是用来占位的,说明静态成员变量不是定义在对象中的,而是定义在静态区的。
所以构造函数中的初始化列表并没有定义静态成员变量。
并且这个静态成员变量是所有权是类的所有成员,所有对象都可以访问,所有对象访问的都是一致的。
static成员函数
用static修饰的成员函数,称之为静态成员函数。
静态成员特性总结:
- 静态成员为所有类对象所共享,不属于某个具体的实例
- **静态成员变量必须在类外定义,定义时不添加static关键字 **
- 类静态成员即可用类名::静态成员或者对象.静态成员来访问 –类名::静态成员这是静态成员特有的访问方式
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员和类的普通成员一样,也有
public
、protected
、private
3种访问级别,也可以具有返回值
【问题】
- 静态成员函数可以调用非静态成员函数吗? F-权限放大,静态无this,非静态有this
- 非静态成员函数可以调用类的静态成员函数吗? T
实战例子
面试题:实现一个类,计算中程序中创建出了多少个类对象。
需要一个计数器,为了保证这个计数器是类专有的,我们定义成静态成员变量
class Date
{
public:
Date(int year = 2022, int month = 9, int day = 6)
:_year(year)
, _month(month)
, _day(day)//创建对象必然会调用构造函数,所以在构造函数中,对静态成员进行处理
{
++_count;
}
void Print()const
{
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
static int _count;//静态成员变量
};
//int _count = 0;//error
//注意,这样写,这个_count就毫无意义了,由于访问限定符的存在,只可以是Date类访问
int Date::_count = 0;//初始化_count只有这一个后路
int main()
{
Date d1;
Date d2;
Date d3;
Date d4;
Date d5;
Date().Print();
cout << d5.Get_Count() << endl;
cout << d1.Get_Count() << endl;
cout << d2.Get_Count() << endl;
Date();
cout << Date::Get_Count() << endl;//静态成员函数特有的调用方式,直接用类名访问
cout << Date().Get_Count() << endl;//这里由于创建了一个匿名变量_count的值又多了一次
return 0;
}
8.c++11初始化的新玩儿法–了解即可
#include<iostream>
using namespace std;
class B
{
public:
B(int x = 0)
:_x(x)
{
}
private:
int _x;
};
class A
{
public:
A(int a = 10, double b = 3.14)
, _p(nullptr)
, __b(3)
{
_a = 100;
}
private://这里在生命成员变量的时候给了缺省值,当初始化列表里面没有初始化处理时,就会使用缺省值
//顺序:先走初始化列表(当初始化列表没有对应的初始化,就会去走缺省值),再走函数体内
int _a = 9;
double _b = 5.5;
int* _p = nullptr;
int* a = (int*)malloc(sizeof(int)* 4);
B __b = 5;//隐式类型转换,编译器优化后是直接调用构造函数
static int n ;//但是静态成员变量不可以这样用,因为静态成员不在初始化列表定义,而是在全局区
};
int A::n = 1;
int main()
{
A aa;
return 0;
}
上面的写法与之前不同的地方是:在成员变量声明时给了缺省值,这个缺省值会在初始化列表中发挥作用。上述例子,初始化列表没有对成员a,成员b进行初始化处理,那么就会按照声明处给的缺省值,对成员a,b进行处理,并且指针类型,动态内存,自定义类型都可以这样用。
大家可以对上述程序进行调式,了解其用法。
9.内部类
理解这句话即可–内部类天生外部类的友元类,而外部类不是内部类的友元类
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 2022 ,int month = 9 ,int day = 8)
:_year(year)
, _month(month)
, _day(day)
{}
class Time
{
public:
void f(const Date& d)
{
_hour = 10;
_minute = 10;
_second = 10;
cout << d._year <<"-"<< d._month<< "-" << d._day << endl;
cout << _count << endl;//同样也可以访问静态的成员变量
}
private:
int _hour;
int _minute;
int _second;
};
void f2(Time t)
{
//cout << t._hour;//外部类不是内部类的友元
}
private:
int _year;
int _month;
int _day;
static int _count ;
};
int Date::_count = 100;
int main()
{
Date d;//用Date创建一个对象
Date::Time t;//用Time创建一个对象,Time这个类是Date的内部类,访问方式是::
t.f(d);
return 0;
}
10.资料分享:
截至到此,咱们的类与对象就彻底地结束了,博主一共总结了
-
类与对象(上)–介绍类,并且怎样写类
(147条消息) c++入门语法收尾,类与对象(上)_月暂未满西楼的博客-CSDN博客
(147条消息) c++类与对象(上)_月暂未满西楼的博客-CSDN博客
-
类与对象(中)–六大默认成员函数,以及运算符重载 重点
(146条消息) 类与对象(中)–六大默认函数–重点_月暂未满西楼的博客-CSDN博客
-
类与对象(下)–类的一些其他语法
下面给出几道对应的OJ题目,大家可以用来巩固知识点,如果你实现了日期类的各功能实现,那么这几个题目其实还好,没写的可以参考我的gitee
:2.3日期类各功能实现/2.3日期类各功能实现 · small_sheep/cplusplus0study - 码云 - 开源中国 (gitee.com)
但是不要陷入到日期类里面去了,不要做每个题都把自己的日期类粘贴过来,想想其他方法。
1.求1+2+3+…+n_牛客题霸_牛客网 (nowcoder.com)
2.计算日期到天数转换_牛客题霸_牛客网 (nowcoder.com)
3.日期差值_牛客题霸_牛客网 (nowcoder.com)
4.打印日期_牛客题霸_牛客网 (nowcoder.com)
5.日期累加_牛客题霸_牛客网 (nowcoder.com)
OJ题目代码也已经放入gitee仓库中:
类与对象OJ题源码.png · small_sheep/cplusplus0study - 码云 - 开源中国 (gitee.com)