okk我们终于来到了C++类和对象的最后一节,大多都是对之前学习的内容做的补充
所以加油继续冲啦!
∧_∧::
(´・ω・`)::
/⌒ ⌒)::
/へ__ / /::
(_\\ ミ)/::
| `-イ::
/ y )::
// /::
/ /::
( く:::
|\ ヽ:::
【本节目标】
- 再谈构造函数
- Static成员
- 友元
- 内部类
- 匿名对象
目录
【本节目标】
1.再谈构造函数
1.1构造函数体赋值
1.2 初始化列表
初始化列表的特性
2. static 成员
2.1概念
2.2特性
3.友元
3.1友元函数
友元函数的特点:
3.2 友元类
4.内部类
4.1 概念
5.匿名对象
1.再谈构造函数
1.1构造函数体赋值
再创建对象是,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
这里需要注意:
上述代码再构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对象成员变量的初始化,
构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
理由如下代码:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year; // 第一次赋值
_year = 2022; // 第二次赋值
//...还可以赋值很多次
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
这段代码中,构造函数对_year的多次复制,其编译过程是被允许的。
那么既然构造函数里面进行的是赋值的话,什么时候才是成员变量的初始化?
这就要引出我们真正的初始方式:初始化列表
1.2 初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year),_month(month),_day(day)
{
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;// 类对象的整体初始化
//(初始化了类对象,但没有初始化成员变量),成员变量放到了初始化列表里面解决
return 0;
}
初始化列表是对成员函数的初始化,他直接跟在析构函数参数括号的后面
初始化列表的特性
特性一:
解决一些必须在声明时就要初始化的成员变量
(1)const修饰的成员变量
class Date { private: const int _year; // const修饰的成员变量只能在定义时初始化 };
被 const 修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化。
(2)引用成员变量
class Date { private: int& _year; // 引用成员变量只能在定义时初始化 };
引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化。
(3)自定义类型成员(该类没有默认构造函数)
class A { public: A(int val) //注:这个不叫默认构造函数(因为需要传参调用) { _val = val; } private: int _val; }; class B { public: B() :_a(2023) //所以必须使用初始化列表对其进行初始化 {} private: A _a; //自定义类型成员(该类没有默认构造函数)⬆ };
在初始化列表:_a(2023)也就是对A类的构造函数进行的一次调用
肯定有人会有这样的疑惑: 为什么不直接在类中声明时就给其初始化?
像这样:,但这样的写法,放在现在是可以的,但在C11发布之前是不行的
class A { public: A() {} private: int a; const int a1 = 1; int& b = a; }; int main() { A d1; return 0; }
要注意:
在类里面声明时给值,不叫做初始化,而是缺省参数,在C11发布之前不允许在类里面直接定义,所以才会有初始化列表的应用
特性二:
每个成员变量在初始化列表中,只能出现一次,(只能初始化一次)
因为初始化只能进行一次,所以同一个成员变量在初始化列表中不能多次出现
特性三:
在你不写初始化列表时,系统自动生成的初始化列表,对内置类型不处理,对自定义类型也是去调用它的初始化类别,
和构造函数、析构函数的特点很像
因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
所以,在我们写构造函数的时候,尽量使用初始化列表进行初始化,能提高效率
如下:
// Date类
class Date
{
public:
Date(int day = 0)
{
_day = day;
}
private:
int _day;
};
// Test类
class Test
{
public:
Test(int day)
:_d(12)//调用Date的构造函数
{}
//这样用初始化列表,只用调用一次Date类的构造函数
Test(int day)
{
Date t(day);
_d = t;
}
//这样写的话,会调用两次Date类的构造函数
//因为系统在自动生成初始化列表时就会调用一次,之后在{ }里面又会调用一次
private:
Date _d;
};
特性四:
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,
与其在初始化列表中的先后次序无关
int i = 0;
class Test
{
public:
Test()
:_b(i++)
, _a(i++)
{}
void Print()
{
cout << "_a:" << _a << endl;
cout << "_b:" << _b << endl;
}
private:
int _a;
int _b;
};
int main()
{
Test test;
test.Print();
return 0;
}
结果:
可以看到代码中,在初始化列表里面,我们先初始化的_b,然后是_a,但是结果却不按这个顺序走,因为初始化顺序是按照声明时的顺序走的
2. static 成员
2.1概念
声明为static的类成员称为类的静态成员
用static修饰的成员变量,称之为静态成员变量;
用static修饰的成员函数,称之为静态成员函数。
静态成员变量一定要在类外进行初始化
2.2特性
特性一
(1)静态成员为所有类对象所共享,不属于某个具体的实例,存放在静态区
先看下面的代码
我们发现 Test2 的类里面比 Test1 类多定义了一个静态变量 _aaa 但是其空间并没有增加
因为静态成员 _aaa 是存储在静态区的,属于整个类,也属于类的所有对象。所以计算类的大小或是类对象的大小时,静态成员并不计入其总大小之和。
特性二
(2)静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
class Date { private: //静态成员变量的声明 static int _year; static int _month; static int _day; }; // 静态成员变量的定义初始化 int Date::_year = 2023; int Date::_month = 5; int Date::_day = 8;
特性三
(3)类静态成员即可用 类名::静态成员函数 或者 对象.静态成员函数 来访问
class Date { public: static void Print() { cout << _year << "-" << _month << endl; } private: static int _year; static int _month; }; int Date::_year = 2022; int Date::_month = 10; int main() { Date d1; d1.Print(); // 对象.静态成员函数 Date::Print(); // 类名::静态成员函数 return 0; }
应为static定义的成员函数是所有类对象所共享的,所以它既可以用类域::来定位,也可以用对象.来定位
特性四
(4)静态成员函数没有隐藏的this指针,不能访问任何非静态成员
class Date { public: static void Print() { cout << _year << endl; //静态的成员可以访问 cout << _month << endl; //不能访问非静态成员 } private: static int _year; int _month; }; int Date::_year = 2022; int main() { Date d1; d1.Print(); // 对象.静态成员 return 0; }
含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量。
特性五
(5)静态成员也是类的成员,受public、protected、private 访问限定符的限制
在静态成员变量设为private时,经过我们通过类域进行访问,也是不行的
特性六
(6)访问静态成员变量的方法:
class Date { public: static int GetMonth() { return _month; } static int _year; private: static int _month; }; //静态成员变量的定义初始化 int Date::_year = 2023; int Date::_month = 5; int main() { Date d1; //对公共属性的静态成员进行访问 cout << d1._year << endl; // 1.通过类对象突破类域进行访问 cout << Date()._year << endl; // 2.通过匿名对象突破类域进行访问 cout << Date::_year << endl; // 3.通过类名突破类域进行访问 //对私有属性的静态成员进行访问 cout << d1.GetMonth() << endl; // 1.通过类对象突破类域进行访问 cout << Date().GetMonth() << endl; // 2.通过匿名对象突破类域进行访问 cout << Date::GetMonth() << endl; // 3.通过类名突破类域进行访问 return 0; }
3.友元
友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
3.1友元函数
问题:现在尝试去重载operator<<,然后会发现没办法将operator<<重载成成员函数。
如下展示:
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
std::ostream& operator<<(std::ostream& out)
{
out << _year << "-" << _month << "-" << _day;
return out;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(2017, 12, 24);
cout << d;
return 0;
}
因为 cout 的输出流对象和隐含的this指针在抢占第一个参数的位置。
this指针默认是第一个参数也就是左操作数了。
但是实际使用中cout需要是第一个形参对象,才能正常使用。
所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
用友元来解决如下:
class Date
{
//友元函数
// 标准流输出 --> printf
friend std::ostream& operator<<(std::ostream& out, const Date& d);
// 标准流插入 --> scanf
friend std::istream& operator>>(std::istream& in, Date& d);
public:
Date(int year = 2023, int month = 5, int day = 8)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
// <<运算符重载---输入
ostream& operator<<(std::ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
// >>运算符重载---输出
istream& operator>>(std::istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
注意:其中 cout 是 ostream 类的一个全局对象,cin 是 istream 类的一个全局变量,
<< 和 >> 运算符的重载函数具有返回值是为了实现连续的输入和输出操作。
友元函数的特点:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
3.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 12, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 2023, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
// 内置类型成员
int _year;
int _month;
int _day;
// 自定义类型成员
Time _t;
};
- 友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
- 友元关系不能继承
4.内部类
4.1 概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类
如下:
class A
{
public:
// 内部类
class B // B天生就是A的友元,在B里面可以直接访问A的私有成员
{
public:
void Print(const A& a)
{
cout << y << endl; // 可以直接访问静态成员
cout << a.h << endl; // 也可以访问普通成员
}
private:
int _b;
};
private:
static int y;
int h;
};
int A::y = 1;
int main()
{
A a; // 定义a对象
A::B bb; // 定义bb对象
bb.Print(a); // 把a对象传给bb对象,打印
return 0;
}
上述代码中,我们在A类里面定义了一个B类,B就叫做A的内部类
B天生就是A的友元,B可以访问A的私有,但不能访问B的私有
注意:
- 1. 内部类可以定义在外部类的public、protected、private都是可以的。
- 2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- 3. sizeof(外部类)=外部类,和内部类没有任何关系。
5.匿名对象
对于一个类 class A{ };
通过前面的学习,我们知道,在定义类对象的时候不能这样写:A aa1();
但是我们可以写成这样: A();
匿名对象的特点:
(1)匿名对象的声明周期只有写匿名周期的那一行
class A { public: A(int a = 0) :_a(a) { cout << "A(int a)" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; }; int main() { A aa1; A(); return; }
通过调试我们可以看到,匿名对象在离开他那一行的时候,他的生命周期就结束了
这样写的方式我们就称为匿名对象,那么匿名对象有什么用呢?
看看下面这段代码:
class Solution {
public:
int Sum_Solution(int n)
{
//...
return n;
}
};
int main()
{
Solution aaa;
cout << aaa.Sum_Solution(10) << endl;//一般调用
cout << Solution().Sum_Solution(10) << endl;//匿名对象调用
return 0;
}
如果我们要用一个类里面的某一个成员函数,我们可能需要真没为其定义一个实例对象 aaa 出来,然后再去调用该类里面的成员函数
比较麻烦,
所以我们可以直接用过匿名对象来进行解决,匿名对象再其调用完成后会直接销毁,不会占用资源