您现在的位置是:首页 >技术杂谈 >从C语言到C++⑦(第二章_类和对象_下篇)初始化列表+explicit+static成员+友元+内部类+匿名对象网站首页技术杂谈
从C语言到C++⑦(第二章_类和对象_下篇)初始化列表+explicit+static成员+友元+内部类+匿名对象
目录
1. 构造函数的初始化列表
我们知道,引用在定义时必须初始化,常量也必须在定义时初始化,
因为常量只有一次初始化的机会,就是在定义的时候。
类里面哪里是初始化的地方?
我们之前学习创建对象时,编译器通过调用构造函数,给对象赋初值。
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;
};
类里面给成员变量提供了一个初始化的地方:初始化列表
1.1 初始化列表概念
#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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(2023, 5, 7);
d.Print();
return 0;
}
1.2 初始化列表注意事项
① 每个成员变量再初始化列表中只能出现一次,即 初始化只能初始化一次。
② 类中包含以下成员,必须放在初始化列表位置进行初始化:
(编译器会把前两个在其它位置的“初始化”当做声明,即给初始化列表缺省值,
前面提到的C++11打的补丁时给内置类型的缺省值也是给初始化列表的)
1. const成员变量 const int _N;
2. 引用成员变量 int& ref;
3. 没有默认构造函数的自定义类型成员变量 A _aa;
③ 尽量显示使用初始化列表初始化,因为不管你是否使用初始化列表,编译器都会默认生成。
使用示例:
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int year, int hour, int& x)
:_year(year)
,_t(hour)
, _N(10)
, _ref(x)
{
}
private:
int _year;
Time _t;
const int _N;
int& _ref;
};
④ 成员变量在类中的声明顺序就是在初始化列表中的初始化顺序,
与其在初始化列表中出现的顺序无关。
下面的程序输出什么?
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();
}
因为我们先声明的是 _a2,所以在初始化列表里我们先初始化的是 _a2,
因为这里是 _a2(_a1), _a1 此时还是没有得到传过去的 1,
此时还是随机值,所以 _a2 就被初始化成随机值了。
按照声明顺序然后是 _a1, _a1 接收到了1,自然会初始化成 1。
最后按顺序打印 —— 1 和 随机值。
如果先声明_a1的话就会打印两个1。
2. 构造函数的explicit关键字
2.1 C语言的隐式类型转换
这里只是报了一个警告,为什么会支持隐式类型转换呢?
因为他们是意义相同的类型,比如 char、int、double 这些类型都是可以互相转,
因为它们都是表示数据大小的。这里 d 也不是直接转给 i,我们之前讲过,中间会生成一个临时变量。我们在讲引用的时候详细讲过这一点。
2.2 explicit 关键字使用
explicit 关键字只能用于类内部的构造函数声明上。
看一段代码:
class Date
{
public:
Date(int year = 1)
: _year(year)
{
}
void Print()
{
cout << _year << endl;
}
private:
int _year;
};
int main()
{
Date d1(2022);
Date d2 = 2023; // 隐式类型转换
d1.Print();
d2.Print();
return 0;
}
这里是隐式类型的转换,为什么支持一个整型转换成日期类相关的类型呢?
整型和日期类本来是没有关系的,但是你支持一个单参数的构造函数后,
整型就可以去构造一个日期类的对象,这个日期类的对象自然可以赋值给他了。
本来用 2023 构造成一个临时对象 Date(2023) ,在用这个对象拷贝构造 d2,
但是 C++ 编译器在连续的一个过程中,编译器为了提高效率,多个构造会被优化,合二为一。
所以这里被优化成,直接就是一个构造了。并不是所有的编译器都会这么做,
C++标准并没有规定,但是新一点的编译器一般都会这么做。
如果你不想让这种 "转换" 发生,C++提供了一种关键字:explicit
构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还具有类型转换的作用。
用 explicit 关键字修饰构造函数,可以禁止单参构造函数的隐式类型转换:
3. static成员
如果我们要计算一个类中创建了多少个类对象,我们可以用全局变量计算一下。
int n = 0; // 全局变量
class A
{
public:
A(int a = 0)
: _a(a)
{
n++;
}
A(const A& aa)
: _a(aa._a)
{
n++;
}
private:
int _a;
};
void f(A a)
{
;
}
int main()
{
A a1;
A a2 = 1;
f(a1);
cout << n << endl;
return 0;
}
输出了3,如果我不想让这个 n 可以被人在外面随便改呢?
有没有办法可以把 n 和类贴合起来呢?让这个 n 专门用来计算我 A 这个类的。
我们先试着把它定义成 —— 成员变量:
class A
{
public:
A(int a = 0)
: _a(a)
{
_n++;
}
A(const A& aa)
: _a(aa._a)
{
_n++;
}
private:
int _a;
int _n = 0; // 定义成成员变量
};
是这样还是不行!这样的话每个对象里面都有一个 n,
我们是希望的是每个对象创建的时候去++的是同一个变量,而不是每个对象里面都有一个。
那该怎么办呢?
类里面可以定义静态成员,在成员变量前面加一个 static,就是静态成员。
3.1 static的概念
声明为 static 的类成员称为类的静态成员,用 static 修饰的成员变量,称为静态成员变量。
用 static 修饰的成员函数,称为静态成员函数,静态的成员变量一定要在类外进行初始化。
class A
{
public:
A(int a = 0)
: _a(a)
{
_sn++;
}
A(const A& aa)
: _a(aa._a)
{
_sn++;
}
private:
int _a;
// 静态成员变量属于整个类,所有对象,生命周期在整个程序运行期间。
static int _sn; // 这里以 _s 为前缀,是为了一眼就看出它是静态成员变量。
};
int A::_sn = 0;//静态的成员变量一定要在类外进行初始化。
3.2 static成员特性
① 静态成员为所有类对象所共享,不属于某个具体的实例。
② 静态成员变量必须在类外定义,定义时不添加 static 关键字。
③ 类静态成员即可用类名 :: 静态成员变量或者对象 . 来访问。
如果它是公有的,我们就可以在类外对它进行访问:
class A
{
public:
A(int a = 0)
: _a(a)
{
_sn++;
}
A(const A& aa)
: _a(aa._a)
{
_sn++;
}
//private:
int _a;
static int _sn;
};
int A::_sn = 0;//静态的成员变量一定要在类外进行初始化。
void f(A a)
{
;
}
int main()
{
A a1;
A a2 = 1;
f(a1);
cout << A::_sn << endl; // 使用类域对它进行访问
// 这里不是说是在 a1 里面找,这里只是帮助他突破类域
cout << a1._sn << endl;
cout << a2._sn << endl;
return 0;
}
但是如果它是私有的,我们可以提供一个公有的成员函数。
我们写一个公有的 Get_sn成员函数,让它返回 _sn 的值,
这样我们就可以在类外调用该函数,就可以访问到它了。
还有没有更好的方式?让我不用对象就可以访问到它呢?静态成员函数:
class A
{
public:
A(int a = 0)
: _a(a)
{
_sn++;
}
A(const A& aa)
: _a(aa._a)
{
_sn++;
}
static int Get_sn()
{
return _sn;
}
private:
int _a;
static int _sn;
};
int A::_sn = 0;//静态的成员变量一定要在类外进行初始化。
void f(A a)
{
;
}
int main()
{
A a1;
A a2 = 1;
f(a1);
cout << A::Get_sn() << endl; // 使用类域对它进行访问
// 这里不是说是在 a1 里面找,这里只是帮助他突破类域
cout << a1.Get_sn() << endl;
cout << a2.Get_sn() << endl;
return 0;
}
④ 静态成员函数没有隐藏的 this 指针,不能访问任何非静态成员。
⑤ 静态成员和类的普通成员一样,也有 public、protected、private 三种访问级别,
静态成员函数也可以具有返回值。
3.3 static成员使用场景
如果有这么一个要求:设计一个只能在栈上定义对象的类。
class StackOnly
{
public:
StackOnly(int x = 0, int y = 0)
:_x(x)
, _y(0)
{
}
private:
int _x = 0;
int _y = 0;
};
int main()
{
StackOnly so1; // 栈
static StackOnly so2; // 静态区
return 0;
}
怎么设计一个只能在栈上定义对象的类?
应该不让类外面的人随便调用构造函数,所以我们把构造函数设置成私有,
那就要再设计一个类内的成员函数获取在栈上定义对象的函数:
class StackOnly
{
public:
StackOnly CreateObj()
{
StackOnly so;
return so;
}
private:
StackOnly(int x = 0, int y = 0)
:_x(x)
, _y(0)
{
}
int _x = 0;
int _y = 0;
};
int main()
{
//StackOnly so1; // 栈
//static StackOnly so2; // 静态区
CreateObj();
return 0;
}
现在这里的代码是过不了的,CreateObj(); 需要对象调,创造对象又要调用CreateObj();
这就是一个先有鸡还是先有蛋的问题了。
这时我们的静态成员函数就能上场了:(因为静态成员用类域也能调)
class StackOnly
{
public:
static StackOnly CreateObj()
{
StackOnly so;
return so;
}
private:
StackOnly(int x = 0, int y = 0)
:_x(x)
, _y(0)
{
}
int _x = 0;
int _y = 0;
};
int main()
{
//StackOnly so1; // 栈
//static StackOnly so2; // 静态区
StackOnly so3 = StackOnly::CreateObj();
return 0;
}
(类和对象后面的OJ题还会有使用静态成员的场景)
这里有两个问题:
4. 友元(friend)
4.1 引入:日期类的流提取
下面这个日期类,我们是调用 Print 成员函数来打印的:
#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
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 5, 7);
d1.Print();
return 0;
}
我们此时思考一个问题,我们能不能用 cout 输出一下 d1 呢? cout << d1;
这样当然是不行的,主要的原因还是这个是一个操作符。
是C++里面的 流插入 ,这里的意思就是要像流里面插入一个 d1。
我们说过,内置类型是支持运算符的,而自定义类型是不支持的,
它是不知道该怎么输出的,输入也是一样的道理,也是不知道该怎么去输入。
那怎样才能向我们内置类型一样去用 流插入 和 流提取 呢?
依然可以使用重载这个运算符的方法来解决!
cout 其实是一个全局类型的对象,这个对象的类型是 ostream :
内置类型之所以能直接支持你用,是因为 ostream 已经帮你写好了。
所谓的 "自动识别类型" ,不过只是函数重载而已……
你是 int 它就匹配 int ,你是 char 它就匹配 char 。
我们现在知道了, cout 是一个 ostream 类型的对象了,我们来重载一下:
第一想法是这样吗?:
#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
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
void operator<<(ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 5, 7);
//d1.Print();
cout << d1;
return 0;
}
这时我们发现 cout << d1 还是识别不了,调不动。
这里不识别的原因是因为它是按参数走的,第一个参数是左操作数,第二个参数是右操作数。
双操作数的运算符重载时,规定第一个参数是左操作数,第二个参数是右操作数。
我们这里是成员函数,那第一个参数是隐含的this
所以,我们在调用这个流插入重载时就需要:
d1.operator<<(cout);
我们要直接写就会成这样:
#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
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
void operator<<(ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 5, 7);
//d1.Print();
//cout << d1;
d1 << cout;
return 0;
}
可以打印出来了,但是这样看起来就变扭了:
这不符合我们对 "流" 的理解,我们正常理解流插入,是对象流到 cout 里面去。
因为被隐含的 this 指针参数给占据了,所以就一定会是左操作数,
这时如果写成成员函数,双操作数的左操作数一定是对象。
基于这样的原因,我们如果还是想让 cout 到左边去,就不能把他重载成成员函数了。
可以直接把它重载成全局的,在类外面,不是成员函数了就没有这些隐含的东西了!
这样的话就可以让第一个参数变为左操作数,即 out 在第一个位置,Date& d 在第二个位置:
void operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
这个时候调用是肯定能调的动了,调的是全局函数。
但我们现在面临的问题是,不能访问私有的问题。
能访问私有的问题改如何解决?把 private 改为 public ?
这种方式肯定是不好的,当然我们可以写个 getYear getMonth getDay 去获取它们。
这样也可以,但是输入的时候怎么办?我们再实现 cin 流体去的时候是要 "写" 的。
这时候就麻烦了,你还得写一个 set,属实是麻烦,有没有更好地办法可以解决这种问题呢?
铺垫了这么久,终于来辣:C++ 引入了一个东西叫做 —— 友元。
4.2 友元的概念
一个全局函数想用对象去访问 private 或者 public ,就可以用友元来解决。
友元分为 友元函数 和 友元类 。
比如刚才我们想访问 Date 类,我就可以把它定义为 友元函数 ,友元的声明要放到类里面。
需要注意的是:友元破坏了封装,能不用就不用。
4.3 友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数。
它不属于任何类,但需要在类的内部进行声明,声明时要加 friend 关键字。
我们现在就可以去解决刚才的问题了:
#include <iostream>
using namespace std;
class Date
{
friend void operator<<(ostream& out, const Date& d);// 友元的声明
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
void operator<<(ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
void operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
int main()
{
Date d1(2023, 5, 7);
//d1.Print();
cout << d1;
//d1 << cout;
return 0;
}
如果我们想连续地输出呢?我想在这又输出 d1 又输出 d2。
cout << d1 << d2;
现在实现的不支持。这和连续赋值很像,只是连续赋值是从右往左,这里是从左往右。
连续插入 d1 和 d2 实际上就是两次函数的调用,这里先执行的是 cout << d1,
因为调用函数后返回值是 void,void 会做这里的左操作数,
所以当然不支持连续输出了,我们可以改一下,
我们把返回值改为 ostream 就行,把 out 返回回去。
解决了流插入,我们再来顺便实现一下流提取。
这样我们上一篇的大练习:日期类,基本上就完整了。
流提取因为要把输入的东西写到对象里去,会改变,所以这里当然不能加 const 。
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
4.3.1 完整流插入流提取重载:
#include <iostream>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);// 友元的声明
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
void operator<<(ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
Date d1(2023, 5, 7);
Date d2(2023, 5, 8);
//d1.Print();
cout << d1 << d2;
//d1 << cout;
Date d3;
Date d4;
cin >> d3 >> d4;
cout << d3 << d4 << endl;
return 0;
}
4.3.2 友元函数注意事项:
① 友元函数可以访问类的 private 和 protected 成员,但并不代表能访问类的成员函数。
② 友元函数不能用 const 修饰。
③ 友元函数可以在类定义的任何地方申明,可以不受类访问限定符的控制。
④ 一个函数可以是多个类的友元函数。
⑤ 友元函数的调用和普通函数的调用原理相同。
4.4 友元类
友元类的所有成员函数都可以是另一个类的友元函数,
都可以访问另一个类中的非公有成员。
friend class 类名;
① 友元关系是单向的,不具有交换性。
② 友元关系不具有传递性(朋友的朋友不一定是朋友)。
如果 C 是 B 的友元,B 是 A 的友元,则不能说明 C 是 A 的友元。
定义一个友元类:
#include<iostream>
using namespace std;
class Date; // 前置声明
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0, 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 = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void GetTime()
{
// 直接访问Time类私有的成员变量
cout << _t._hour << ":" << _t._minute << ":" << _t._second << endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d;
d.GetTime();
return 0;
}
这里 Date 是 Time 的友元,我们在日期类里就可以访问时间类的私有成员了。
但是时间类里不能访问日期类,因为这是 "单向好友" ,
如果想在时间类里访问日期类,我们可以在日期类里声明:
class Date
{
friend class Time;
// ...
}
这样,它们之间就是 "双向好友" 了 —— 互相成为对方的友元。
5. 内部类(了解)
C++中不常用内部类,Java中用得多一点,所以我们了解一下就行。
5.1 内部类的概念
如果在 A 类中定义 B 类,我们称 B 是 A 的内部类。
class A
{
class B {
;
};
};
#include<iostream>
using namespace std;
class A
{
public:
class B // B天生就是A的友元
{
public:
void fuc(const A& a)
{
cout << g << endl;
cout << a.r << endl;
}
};
private:
static int g;
int r = 19;
};
int A::g = 1;
int main()
{
A a;
A::B b;
b.fuc(a);
return 0;
}
5.2 内部类的特性
#include<iostream>
using namespace std;
class A
{
private:
static int _s_a1;
int _a2;
public:
class B
{
private:
int _b1;
};
};
int A::_s_a1 = 1;
int main()
{
cout << "A的大小为: " << sizeof(A) << endl;
return 0;
}
sizeof(外部类)=外部类,和内部类没有任何关系。
内部类 B 天生就是外部类 A 的友元,也就是 B 中可以访问 A 的私有(或保护),
A 不能访问 B 的私有(或保护)。
所以,A 类型的对象里没有 B,跟 B 没什么关系,计算 sizeof 当然也不会带上B。
加上静态成员属于整个类,是放在静态区的,所以这里只计算了int _a2的大小。
6. 匿名对象
匿名对象是指创建对象时,只有创建对象的语句,却没有把对象地址值赋值给某个变量。
产生匿名对象的三种情况:
① 以值的方式给函数传参;
A(); —> 生成了一个匿名对象,执行完Cat( )代码后,此匿名对象就此消失。这就是匿名对象的生命周期。
A aa = A(); —>首先生成了一个匿名对象,然后将此匿名对象变为了aa对象,其生命周期就变成了aa对象的生命周期。
② 类型转换;
③ 函数需要返回一个对象时;return temp;
#include<iostream>
using namespace std;
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 aa1(); 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
A();
A aa2;
return 0;
}
7. 拷贝对象时的一些编译器优化
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
int main()
{
// 传值传参
A aa1;
f1(aa1);
cout << endl;
// 传值返回
f2();
cout << endl;
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
cout << endl;
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << endl;
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
cout << endl;
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
cout << endl;
return 0;
}
拷贝对象时的一些编译器优化就提醒我们能在一行写的就在一行写,
尽量往编译器的优化方面靠拢。关于匿名对象和这方面的题就放在下一篇了。
本篇完。
下一篇更一篇类和对象的笔试题和OJ题,类和对象就结束了。
然后更C++的动态内存管理,new和delete。