本文主要讲解类和对象的一些其他小知识。
文章目录
- 前情回顾
- 一、用运算符重载写一个输入流和输出流
- ①流插入
- ②流提取
- ③流提取和流插入的优化
- 二、const成员
- 三、用运算符重载改变数组
- 1、再谈构造函数
- 1.1 构造函数体赋值(不相当于初始化)
- 1.2 初始化列表
- ①引出初始化列表
- ②怎么用初始化列表
- ③初始化列表的规定
- 2、 explicit关键字
- 3、 static成员
- 面试题:
- ①方法一:创建一个全局变量。
- ②方法二:static成员
- ③方法三:static成员函数
- 静态成员变量小语法
- 4、 匿名对象
- 5、 友元
- 3.1 友元函数
- 3.2 友元类
- 6、 内部类
- 7、 编译器的优化
前情回顾
关于上节的日期类,我们使用了运算符重载实现了流插入和流提取,这里我们详细实现以下。
一、用运算符重载写一个输入流和输出流
①流插入
我们C++中我们输入流和输出流就是用
cin
和cout。
他们最大的特点就是能够自动识别类型。
我们平常用的
cin>>d
和cout<<d
,这个其实就是运算符重载,<<
和>>
就是运算符。
我们要想看明白下面的内容,理解这个是必要的。
cin
和cout
其实就是类的对象。
同时大家需要知道ostream
是cout
对象的类。
istream
是cin
对象的类
为什们能够支持自动识别类型?
支持自动识别,是因为库里面已经帮我们写了,并且支持 函数重载。当我们写不同类型的时候我们就会调用不同的函数。
所以我们写的cout << d
其实就是cout.operator <<(int)
,通过你传的参,去识别类型。
如果我们想要输入输出一个自定义类型,但是对于自定义类型来说,编译器并不能自动识别,所以需要我们自己用运算符重载一个。
//输出流,放到我们上面的日期类中。
void Date::operator<<(ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日"<< endl;
}
但是我们使用的时候,对于我们写的这种类型,不能像
cout << d
这样使用,这样会报错。(d是我们日期类的对象)
利用我们现有的知识,对于运算符重载规定
第一个参数是左操作数,第二个参数是右操作数,
如果是在类中,那么传的时候会将第一个参数省略,传的是this
指针,
对于我们这个函数,this
指的是d
,第二个参数才是cout
。
但是如果cout << d
这样写,传第一个参数就是cout
,第二个参数是d
。
所以我们应该这样写。
d.operator>>(cout);
,也可以这样d1 << cout
;
但是这样写跟我们想的完全不一样。
所以我们这时可以不用将我们的运算符重载放到我们的类中。
可以将我们的运算符重载放到全局。
按照需要的参数顺序传参,就没有this
指针了。
具体传参为operator<<(cout, d1);或 cout << d1;
但是可能面对的问题就是不能访问私有的成员变量。这个其实有办法解决。
//加返回值是为了实现连续输出
//为了实现这种场景 cout << d1 <<d2 << endl;
//定义
ostream& operator<<(ostream& out,const Date& d)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
return out;
}
访问私有成员变量的方法:
友元函数
具体做法就是将我们这个函数的声明放到我们的类中,在前面加上friend。
下面会详细介绍,如果不懂可直接跳到下面的友元函数。
流提取的运算符重载
//声明
class Date
{
//友元函数的声明
friend ostream& operator<<(ostream& out, const Date& d);
public:
//成员函数
private:
int _year;
int _month;
int _day;
};
//在类外面定义
ostream& operator<<(ostream& out,const Date& d)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
return out;
}
在类里面声明前面加上friend就可以访问类内成员变量了
②流提取
跟上面一样,将运算符重载定义,放到全局中。
并且有返回值,是为了能够多重打印。
cout << d1 <<d2 << endl;
流提取在类中声明,友元函数的声明
friend istream& operator>>(istream& in,Date& d);
流提取的运算符重载
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
③流提取和流插入的优化
因为流插入和流提取的运算符重载都很简单,所以我们可以将他们变成内联函数,然后都放到头文件中。
内联函数,声明和定义都必须放在同文件中,不能将声明和定义分开。
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
private:
int _year;
int _month;
int _day;
};
inline ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
二、const成员
我们先看一个例子
class A
{
public:
void Print()
{
cout << _a << endl;
}
private:
int _a = 10;
};
int main()
{
const A aa;
aa.Print();
return 0;
}
我们的程序出现错误。其实这涉及到一个问题,权限的放大。
我们定义了一个对象aa
,但是我们把他设置为了const
此时由于对象在调用函数的时候会将对象的地址传递给this指针
&aa
的类型为const A*
,(*this不能改
)
但是this
指针的默认类型为A* const this
,虽然this
指针不能改,但是*this
可以改
所以要解决这个问题,就是将
this
设置为const,
void Print() const
,规定const
修饰*this
.注意修饰的并不是this
class A
{
public:
void Print() const
{
cout << _a << endl;
}
private:
int _a = 10;
};
int main()
{
const A aa;
aa.Print();
return 0;
}
所以我们以后内部不改变成员变量的成员函数就用const修饰,const对象和普通对象都可以调用。
三、用运算符重载改变数组
//用运算符重载改变数组
//静态顺序表
class A
{
public:
//1:运算符重载[],使其可以对自定义类型使用--可以写
int& operator[](int i)
{
assert(i < 10);
return _a[i];
}
//2:类对象是const定义的不能改变的--重载一个
const int& operator[](int i) const
{
assert(i < 10);
return _a[i];
}
private:
int _a[10];
};
void Fun(const A& aa)
{
for (int i = 0; i < 10; i++)
{
cout << aa[i] << " ";
}
}
int main()
{
A x;
for(int i = 0; i < 10; i++)
{
x[i] = i * 2;
}
for (int i = 0; i < 10; i++)
{
cout << x[i] << " ";
}
cout << endl;
Fun(x); //传的是const
return 0;
}
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;
};
int main()
{
Date a;//对象整体的定义
}
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量
的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始
化一次,而构造函数体内可以多次赋值
1.2 初始化列表
①引出初始化列表
我们知道对象实例化的时候
(Date a;)
,是对象整体的定义。
这时就会有一个问题,每个成员什们时候定义的呢?
这时有人就会说,不就是在整体定义的时候就将其成员定义了吗?
其实话说是这样说,但是类好像并不是。
我举个例子
int main()
{
//判断是否正确
const int i;
return 0;
}
我们看上面的代码,我们看他是典型的错误。
i被const修饰所以它具有常性,在定义完之后就不可在修改了,如果你不在定义的时候初始化,那么以后他将没有机会初始化了。
结论:const修饰的变量必须在定义的时候初始化。
那我们看下面的类
class A
{
public:
private:
//这个只是声明,并不是定义
const int _x;
};
int main()
{
A a;//对象整体的定义
return 0;
}
首先上面这个程序是错误的。
对于这个类,成员变量是常变量,我们必须在定义的时候初始化,但是声明也不能初始化。对象整体的定义的时候也没有初始化。
所以就抛出了下面这个问题。
每个成员什们时候定义的呢?
- 第一种,其实我们在上面默认构造函数的时候说了,他对内置类型不做处理,其实是一个
bug
,在C++11
,这时打了一个补丁。
用在声明的时候用缺省参数可以很好的解决问题。在调用默认构造函数的时候就用这个缺省参数定义了。具体看下面代码。
class A
{
public:
private:
//用缺省参数
const int _x = 1;
};
int main()
{
A a;//对象整体的定义
return 0;
}
- 但是上面这种方法是在2011年,才打的补丁,C++在98年就出来,并且就有这个问题,那么在这段期间是这么解决的呢?
其实就是用到了初始化列表。
②怎么用初始化列表
因为上面的问题,所以必须给每个成员变量找一个定义的位置,否则就会像
const
的变量就没有办法初始化。
对于对象来说,初始化列表是成员变量定义的地方。
初始化列表:
- 初始化列表的位置,在构造函数名和函数体之间
- 初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
③初始化列表的规定
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
①引用成员变量
②const成员变量
③自定义类型成员(且该类没有默认构造函数时)- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,
一定会先使用初始化列表初始化。- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后
次序无关
接下来我们一个一个解释
🐯特点1:每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
class A
{
public:
A()
:_x(1)
,_a2(1)
,_a2(2)//错误的代码不能出现两次,只能出现一次。
{
_a1++;
_a2++;
}
private:
//用缺省参数
int _a1 = 1;
int _a2 = 2;
const int _x;
};
🐯特点2:
类中包含以下成员,必须放在初始化列表位置进行初始化:
①引用成员变量
②const成员变量
③自定义类型成员(且该类没有默认构造函数时)
对于前两个还比较好理解,引用如果不初始化,就会产生一个问题,他是谁的别名,这个就非常奇怪。
下来我们详细看一下为什们第三个不可以。
class B
{
public:
B()
:b(1)
{
}
private:
int b;
};
class A
{
public:
A()
:_x(1)
,_a2(1)
//引用的初始化
,ret(_a1) //表示ret是_a1的引用。
{
_a1++;
_a2++;
}
private:
//用缺省参数
int _a1 = 1;
int _a2 = 2;
const int _x;
int& ret;
B _bb;
};
首先上面这个程序没有任何问题
这时有人会问,为什们没有在初始化列表中让自定义类型初始化,怎么他还是对着呢?
我们第三点,规定的是没有默认构造的自定义类型,我们必须初始化。
但是类B,它有默认构造函数(上面将默认构造函数写了:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数),我们对于自定义类型,会调用他的默认构造函数,将其初始化,这是默认构造函数对于自定义类型的特点。
所以这是没有问题的,B类的对象_bb调用自己的默认构造函数初始化了。
再看下面例子,判断是否正确。
class B
{
public:
B(int)
:b(1)
{
}
private:
int b;
};
class A
{
public:
A()
:_x(1)
,_a2(1)
//引用的初始化
,ret(_a1) //表示ret是_a1的引用。
{
_a1++;
_a2++;
}
private:
//用缺省参数
int _a1 = 1;
int _a2 = 2;
const int _x;
int& ret;
B _bb;
};
int main()
{
A a;//对象整体的定义
return 0;
}
先说答案:上面的代码是错的,
对于自定义类型B,他没有默认构造函数,但有构造函数。
但是这个构造函数它既不是全缺省,也不是无参构造函数,并且他写了,系统也不会自动生成默认构造函数。
因此,这个类对象B既无法调用默认的构造函数(因为显示定义了一个有参的),也无法调用显示定义的构造函数(因为类型不匹配,没有给参数),无法完成初始化
所以我们在初始化列表中,我们必须将无默认构造函数的自定义类型初始化。
初始化过程看下面代码
class B
{
public:
B(int)
:b(1)
{}
private:
int b;
};
class A
{
public:
A()
:_bb(2) //可以随意给值,有了参数就可以调用它的构造函数。
{}
private:
B _bb;
};
int main()
{
A a;//对象整体的定义
return 0;
}
🐯特点3:不管是否在初始化列表写,编译器的每个变量都会走初始化列表,在初始化列表定义和初始化
class A
{
public:
A()
:_x(1)
,_a2(1)
{
_a1++;
_a2++;
}
private:
//用缺省参数
int _a1 = 1;
int _a2 = 2;
const int _x;
};
int main()
{
A a;//对象整体的定义
return 0;
}
答案是:
a1=2,a2=2,x=1;
。
虽然我们利用缺省参数对a2初始化为2了,
但是不管你是不是在初始化列表初始化,我们的编译器的每个变量都会走一遍初始化列表,
初始化列表里面我们又将a2重新初始化为1了*
再进入函数体后,a2++后为2
🐯特点4:成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
利用这个知识点做下面这个题
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();
}
答案:
a1 = 1,a2 = 随机值
解释:
我们变量初始化的顺序是按照成员变量声明的顺序来的
可以看到,a2先声明,所以走初始化列表时候先初始化a2,不管你初始化列表前面有多少
但是我们的a2是用a1初始化的,我们的a1由于还没有初始化,所以是随机值,所以我们的a2=随机值
然后再使用a = 1来初始化a1
2、 explicit关键字
总结:用explicit修饰构造函数,将会禁止构造函数的隐式转换
构造函数不仅可以构造与初始化对象,
对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
class Date
{
public:
// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
explicit Date(int year)
:_year(year)
{}
/*
// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具
有类型转换作用
// explicit修饰构造函数,禁止类型转换
explicit Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
*/
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2022);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2023构造一个无名的Date类对象,最后用无名对象给d1对象进行赋值(调用拷贝构造)
d1 = 2023;
// 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转
换的作用
}
3、 static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;
用static修饰的成员函数,称之为静态成员函数。
注意:静态成员变量一定要在类外进行初始化
初始化的方式:
类型+ 类名::变量 = 0;
int A::count = 0;
面试题:
实现一个类,计算程序中创建出了多少个类对象。**
//这是我们的类,计算主函数中,我们一共创建了几个类。
class A
{
public:
//构造函数
A(int a = 0)
{}
//拷贝构造
A(const A&aa)
{}
};
void Func(A a)
{}
int main()
{
A aa1;
A aa2(aa1);
Func(aa1);
A aa3 = 1;
return 0;
}
分析:我们创造键一个类。
他只有两条路,一条是走构造函数,一条是走拷贝构造函数。
所以我们只要计算我们进行了几次构造函数和拷贝构造函数即可。
①方法一:创建一个全局变量。
//全局变量
int n = 0;
class A
{
public:
//构造函数
A(int a = 0)
{
n++;
}
//拷贝构造
A(const A&aa)
{
n++;
}
};
void Func(A a)
{}
int main()
{
//构造函数一次
A aa1;
//拷贝构造一次
A aa2(aa1);
//进行了一次拷贝构造
Func(aa1);
//隐式类型转换优化为一次构造函数
A aa3 = 1;
cout << n << endl;
return 0;
}
这种方式可行,但是这种方法的缺陷还是太明显,我们n是全局变量,我们在主函数里随便可以随便修改,一改就变了,结果就会不对。
②方法二:static成员
什么是 static
成员?
在类中用
static
修饰的成员变量,称之为静态成员变量;
静态成员变量一定要在类外进行初始化
注意:静态成员变量 ,不属于某个对象,属于所有对象,属于整个类.
为什们静态成员变量一定要在类外进行初始化?
我们在类中初始化的必须是类的对象,但是静态成员变量不属于某个对象,所以必须类外初始化。
class A
{
public:
private:
//不属于某个对象,属于所有对象,属于整个类
static int count;//声明
};
int A::count = 0;//定义初始化
有了这个知识,我们还需要一个
GetCount()
函数获取count,
具体代码如下
class A
{
public:
//构造函数
A(int a = 0)
{
count++;
}
//拷贝构造
A(const A&aa)
{
count++;
}
int GetCount()
{
//在这个函数中不受访问限定符是限制
//如果在外面直接调用,就可以拿到count
//只可以读,不可以写
return count;
}
private:
//不属于某个对象,属于所有对象,属于整个类
static int count;//声明
};
int A::count = 0;//定义初始化
void Func(A a)
{}
int main()
{
//构造函数一次
A aa1;
//拷贝构造一次
A aa2(aa1);
//进行了一次拷贝构造
Func(aa1);
//隐式类型转换优化为一次构造函数
A aa3 = 1;
A aa4[10];
cout << aa3.GetCount() << endl;
return 0;
}
但是有一个缺陷,他在最后调用的时候,只用用我们的创建的对象才能进行调用(因为我们把他定义为了成员函数),这样是不是感觉很难受。
如果我们没有创建对象,想去调用好像没有办法
这是就有了静态成员函数。
③方法三:static成员函数
我们想要调用我们的成员函数,必须先创建一个对象,再通过Get函数得到私有 的成员变量。
我们不想要我们的通过创建一个对象来调用函数从而得到数据,那我们有没有什么方法呢?
其实我们的静态成员函数就可以很好的帮我们解决这个问题。
static
成员函数怎么用?
就是在成员函数前加一个
static
,就变成了静态成员函数。
static
成员函数的特点
就是没有
this
指针,我们知道了域名,就可以直接调用。
为什们没有this指针,就可以直接调用?
我们知道,类中的成员函数都有一个隐藏的
this
指针,可以帮我们指向对应的成员变量。
所以我们在传参的时候,我们必须传一个this
指针,就像aa3.GetCount()
,它的this
指针就是aa3对象的指针。
所以静态成员函数突破了这个限制,在传参的时候不传this
,就可以用域名找到。
static成员函数能不能调用我们的非静态成员变量?
答案肯定是不可以,
因为调用我们的成员变量,都是用this
指针进行调用,但是我们上面说了静态成员函数没有this
,肯定就不可以调用呀
什么时候用 static成员函数?
当我们需要访问静态成员变量时,并且可以用域名直接找到静态成员函数。
class A
{
public:
//构造函数
A(int a = 0)
{
count++;
}
//拷贝构造
A(const A&aa)
{
count++;
}
//静态成员函数
static int GetCount()
{
return count;
}
private:
//不属于某个对象,属于所有对象,属于整个类
static int count;//声明
};
int A::count = 0;//定义初始化
void Func(A a)
{}
int main()
{
//构造函数一次
A aa1;
//拷贝构造一次
A aa2(aa1);
//进行了一次拷贝构造
Func(aa1);
//隐式类型转换优化为一次构造函数
A aa3 = 1;
A aa4[10];
cout << A::GetCount() << endl;
return 0;
}
静态成员变量小语法
我们知道静态成员变量不能在定义的时候给缺省值,只能在函数外面定义。
因为静态成员变量属于整个对象,在初始化列表中并不定义静态成员变量。
但是这个小语法:如果是const整型的静态成员变量 就可以在定义用缺省值。而其他的不可以
class A
{
private:
static const int count = 1;
};
4、 匿名对象
匿名对象的样式
类名直接加括号。例如:上面类的匿名对象是
A()。
匿名对象的特征
他没有名字,并且声明周期只在它这一行,一旦跳过这一行,直接析构。
举个例子:
对于上面的那个面试题,我们还可以对方法2改进一下,使用匿名对象调用成员函数,然后-1
class A
{
public:
//构造函数
A(int a = 0)
{
count++;
}
//拷贝构造
A(const A&aa)
{
count++;
}
//
int GetCount()
{
return count;
}
private:
//不属于某个对象,属于所有对象,属于整个类
static int count;//声明
};
int A::count = 0;//定义初始化
void Func()
{
A aa4[10];
}
int main()
{
Func();
//这个就是匿名对象。
cout << A().GetCount() -1 << endl;
return 0;
}
5、 友元
友元:
友元提供了一种 突破封装的方式,有时提供了便利。
但是友元会增加耦合度,破坏了封装,所以 友元不宜多用。
友元分为:友元函数和友元类
3.1 友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
说明:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
之前日期类的问题:重载operator<<,然后发现没办法将operator<<重载成成员函数。
因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。
但是实际使用中cout需要是第一个形参对象,才能正常使用。
所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
将<<写成成员函数的形式(不符合常规调用)
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
解决办法:
将函数定义写在类外,然后使用frend关键字将函数在类内的任意位置声明,使其成为类的友元函数。
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
//定义成普通函数,传两个参数
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
3.2 友元类
他是一个但单向关系,你的是我的,我的还是我的。
友元类的特征
- 友元关系是单向的,不具有交换性。
- 友元关系不能传递
- 如果C是B的友元, B是A的友元,则不能说明C时A的友元。
- 友元关系不能继承,在继承位置再给大家详细介绍。
对于下面这个
Date
是Time
的友元,所以Date
可以访问Time
,但是Time
不能访问Date
class Time
{
// 声明日期类为时间类的友元类,则在日期类中就直接访问Time类 中的私有成员变量
friend class Date;
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 = 1900, 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;
};
对于特点:其实很好理解
class A
{
friend class B; //B是A友元
}
class B
{
friend class C;//C是B的友元
}
class C
{
}
这里我们可以显而易见的看到,B可以访问A的所有成员,因为B在A里面声明了友元类,B是A的友元
C可以访问B的所有成员,因为C在B里面声明了友元类,C是B的友元
但是ABC是都是独立的类,不写友元类根本没办法访问
6、 内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。
外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//可以直接访问static成员
cout << a.h << endl;//可以访问所有成员
}
};
};
int A::k = 1;
int main()
{
A::B b; //创建对象时候要写清楚域,因为B在A里面
b.foo(A());
return 0;
}
拥有内部类的类的大小
计算上面A类的大小。
答案是:4.
所以我们可以初步判断一下,A类的大小只有他自己,并没有计算B。
内部类,其实和将类定义到全局没有区别。他是独立的
用上面举个例子,类B是类A的内部类,但是类B是独立的,只是受域的限制,必须再A域中才能找到B。
7、 编译器的优化
优化的过程都在代码中的注释,大家好好研究
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 fun(A aa)
{
}
void func(const A& aa)
{
}
int main()
{
//隐式类型转化
//构造+拷贝构造+优化为==>构造
A aa1 = 1;
//传值传参
//拷贝构造,没有优化
fun(aa1);
//构造+拷贝构造+优化为==>构造
fun(2);
//构造+拷贝构造+优化为==>构造
fun(A(3));
//引用传参
//没有优化,啥都不调用
func(aa1);
//无优化,隐式转换为直接引用
func(2);
//无优化 就是构造完直接引用
func(A(3));
return 0;
}
再来一种形式
A function()
{
//进行构造函数
A aa;
//在返回的时候进行拷贝构造
return aa;
}
int main()
{
function();
//function()函数中一个构造,一个拷贝构造,
//然后再一个拷贝构造
//被优化为一个构造+拷贝构造。
A aa1 = function();
}
因为上面的函数function()是两个步骤,编译器没办法优化
再来一种形式
A function2()
{
//直接返回一个匿名对象。
//进行一个构造函数+拷贝构造
//被优化为一个构造
return A();
}
int main()
{
function2();
//上面说函数被优化为一个构造
//在下面那就进行一个构造+拷贝构造
//被优化为==>一个构造
A aa1 = function2();
}
优化总结:
对象返回总结
- 接受返回值对象,尽量拷贝构造方式接收,不要赋值接收
- 函数中返回对象时,尽量返回匿名对象函数传参优化(例如第二个例子,两步没办法优化)
- 尽量使用
const &
传参。也就是引用传参