这里写目录标题
- const成员
- 再谈构造函数
- 为什么会有初始化列表
- 第一个问题
- 第二个问题
- 第三个问题
- 初始化列表的使用方式即注意事项
- explicit关键字
- static成员
- static修饰类中的变量
- 一些性质
- static修饰成员函数
- 友元
- 友元函数
- 友元类
- 内部类
- 匿名对象
- 拷贝对象时的一些编译器优化
const成员
在介绍const成员之前我们先来看一段代码:
#include<stdio.h>
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
printf("year = %d\n", _year);
printf("month = %d\n", _month);
printf("day = %d\n", _day);
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 12, 9);
d1.print();
return 0;
}
我们这里简单的创建了一个日期的类,这个类中含有三个成员变量并且还有一个构造函数和一个打印函数,然后我们就在main函数中创建了一个对象,并且调用构造函数进行初始化,然后调用类里面的打印函数进行打印,那么我们这里来看看这段代码的打印结果:
我们可以看到这里正常的打印了类中的三个数据的值,我们这里实例化出来的对象是普通的类型,但是在我们的生活中有些对象它是const修饰的,比如说我们下面的代码:
int main()
{
Date d1(2022, 12, 9);
d1.print();
const Date d2(2022, 12, 9);
d2.print();
return 0;
}
这里的d2就是const修饰的,那这样我们再调用类中的print函数,他还能正常的运行吗?我们来看看这段代码运行的结果:
我们发现这里报出了错误,而且这个错误还和this指针有关,我们知道this指针的类型是类类型*const,并且指针在进行赋值的时候是会出现权限放大这个错误的,比如说下面的代码在编译的时候就会报出错误:
int main()
{
const int a = 0;
int* pa = &a;
return 0;
}
错误的原因就是因为权限的放大:
那将const类型的地址赋值给一个const类型的指针的话会出现权限放大的问题吗?我们可以通过下面的代码来验证一下:
int main()
{
const int a = 0;
int* const pa = &a;
return 0;
}
这里报错的原因也是因为权限的放大,那通过这两个权限放大的例子我们知道了调用print函数报错的原因是权限的放大,那我们解决这个问题的方法就是将this指针的类型由类类型*const改成修改成const类类型*const,这样就可以解决权限放大的问题,但是这里有个问题就是this指针的定义和传递都是编译器自己完成的,不由我们操作者来完成,那我们如何来对其进行修改呢?所以为了解决这个问题c++就引入了const成员这个内容:将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。其形式就是在函数的括号后面加个const,比如说上面的print函数在括号后面加个const就不会在报错啦:
void print() const
{
printf("year = %d\n", _year);
printf("month = %d\n", _month);
printf("day = %d\n", _day);
}
再将上面的代码运行一下就可以发现这里并没有报错了:
那这就是const成员的作用以及介绍,这里大家要注意的一点就是我们在写成员函数的时候,能加const成员就加const成员,因为权限放大很容易被我们疏忽,所以我们在写函数的时候能加就加,比如说我们上篇文章中写的>操作符的重载,==的操作符重载这些不改变this指针指向的数据的函数都可以加const成员,那这就是const成员的内容。
再谈构造函数
为什么会有初始化列表
在解释为什么之前,我们首先来想几个问题,
第一个问题
首先第一个就是const修饰的变量我们能修改他的值吗?答案是可以的,但是修改的机会只有一次,就是在该变量定义的时候我们可以修改它的值,比如说我们下面的代码:
int main()
{
const int a = 1;
return 0;
}
我们在定义a的时候,可以对这个const修饰的变量进行赋值,但是在其他的地方我们是不能对a的值进行任何的修改,如果修改的话我们的编译器就会报错:
那看到这里我们就得想一个问题:类中的构造函数可以初始化const修饰的变量吗?那我们来看看下面的代码:
#include<stdio.h>
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void print() const
{
printf("year = %d\n", _year);
printf("month = %d\n", _month);
printf("day = %d\n", _day);
}
private:
const int _year;
const int _month;
const int _day;
};
int main()
{
Date d1(2022, 12, 9);
d1.print();;
return 0;
}
我们运行的时候就可以看到这里报出了错误:
那么这就说明我们之前学的构造函数是无法初始化const修饰的变量,所以遇到这种情况我们是得用到初始化列表,那这时有小伙伴就问我们这里可以使用缺省值来对初始化这里的const变量吗?答案是可以的,比如说我们下面的代码:
#include<stdio.h>
class Date
{
public:
Date()
{
;
}
void print() const
{
printf("year = %d\n", _year);
printf("month = %d\n", _month);
printf("day = %d\n", _day);
}
private:
const int _year = 1;
const int _month = 1;
const int _day = 1;
};
int main()
{
Date d1;
d1.print();;
return 0;
}
我们将这个代码运行一下就可以看到这里正常的运行下去:
但是这种方法比较单调,只能初始化为一种值,如果我们想将这里的数据全部都初始化为2的话,我们就只能在类中对这里的缺省值进行修改,而不是在实例化对象的时候通过传参来进行修改,所以要想真正的符合我们的日常所需,我们还是得用初始化列表来进行初始化,因为
这里只是我们声明的过程,变量真正定义的过程是在初始化列表,而唯一一次修改值得机会是在变量定义得时候。
第二个问题
我们知道引用只能在定义的时候来指明他想要指向的对象,所以当类中存在引用变量的时候我们就得用初始化列表来对其进行初始化,所以这也是为什么得有初始化列表的原因。
第三个问题
当我们在一个类中实例化了另外一个类的时候,我们编译器自动生成的默认构造函数会自动调用该类的默认构造函数来对这个对象进行初始化,比如我们下面的代码:
class Date
{
public:
void print() const
{
printf("year = %d\n", _year);
printf("month = %d\n", _month);
printf("day = %d\n", _day);
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
class double_date
{
public:
void print() const
{
d1.print();
d2.print();
}
private:
Date d1;
Date d2;
};
int main()
{
double_date date1;
date1.print();
return 0;
}
我们这里又创建了一个类将这个类的名字称为double_date,然后在这个类里面创建了两个date类型的对象,然后我们这里没有写构造函数,编译器自己生成的默认构造函数会自动的调用date类里面的默认构造构造函数来初始化对象d1和d2,但是我们date类里面我们也没有写构造函数,所以这里编译器又会自动生成默认构造函数,但是好在我们这里给了缺省值所以d1和d2这两个类中的数据都不会是随机值,我们运行一下这里的代码就可以看到屏幕上打印出来的值都是1:
但是这里有个问题就是,如果date类里面没有默认构造函数呢?我们自己写了一个构造函数,并且必须要传参呢?比如说我们下面的代码:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void print() const
{
printf("year = %d\n", _year);
printf("month = %d\n", _month);
printf("day = %d\n", _day);
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
首先我们知道这种情况编译器自己生成的默认构造函数已经无法使用了,我们运行一下就可以看到这里的编译器报错了:
因为编译器自己生成的默认构造函数只能调用其他类中的默认构造函数,而这里date类中已经没有默认构造函数了所以就报错了,所以现在的问题就是如何在double_date这个类中调用date这个类中构造函数呢?要想解决这个问题我们就必须得使用初始化列表。
初始化列表的使用方式即注意事项
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。比如说Date类中的初始化列表就是这么写的:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
double_date类中的初始化列表就可以这么写:
double_date(int year1, int month1, int day1,
int year2, int month2, int day2)
:d1(year1, month1, day1)
, d2(year2, month2, day2)
{}
那么为了检验我们这里的double_date是否写的正确,我们可以通过下面的代码来检验一下:
int main()
{
double_date date1(2022,11,11,2022,12,12);
date1.print();
return 0;
}
我们运行一下就可以看到这里的代码写的是真确的:
注意事项1:
大家观察这里的形势就可以发现在初始化列表的下面还有一个大括号,那么这个大括号就是构造函数的函数体,虽然初始化列表能够干很多函数体无法做到的实行,但是很多时候初始化列表和函数体是相辅相成的存在,比如说我们之前写的stack类中的构造函数就得初始化列表和函数体相符相承:
#include<iostream>
#include<stdlib.h>
using namespace std;
class Stack
{
public:
Stack(int capacity = 4)
:_a((int*)malloc(sizeof(int)* capacity))
, _top(0)
,_capacity(capacity)
{
cout << "Stack(int capacity = )" <<capacity<<endl;
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
}
private:
int* _a;
int _top;
int _capacity;
};
初始化列表负责开辟空间,而在函数体中则负责对这个新开的空间进行检查,检查其空间是否开辟成功。
注意事项2:
类中的每个成员都要走初始化列表,就算我们不显示在初始化列表中写,它也会走,显示写了就会采用你给他的值,如果没有显示写对于内置类型它就会初始化为随机值,对于自定义类型它就会调用它的默认构造函数,所以大家以后在写构造函数的时候能在初始化列表中进行初始化就在初始化列表中进行初始化,因为你不写它还是会走,如果你写了你还能够提高你的效率,以免浪费。
注意事项3:
尽量在类中提供默认构造函数,因为在其他类中可能会用到,并且构造函数尽量都是全缺省的。
注意事项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再初始化_a1,而在初始化_a2的时候_a1还是随机值,所以这就是为什么a2的值是一个随机值的原因,我们来看看这个代码的执行结果:
explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。我们将上面日期类的成员变量进行简化,将三个成员变量简化为一个成员变量,其代码如下:
class Date
{
public:
Date(int year)
:_year(year)
{}
void print()
{
printf("year = %d", _year);
}
private:
int _year = 1;
};
那么这时我们在对这个变量进行实例化的时候就只用给一个参数,比如说我们下面的代码:
int main()
{
Date d1(2022);
d1.print();
return 0;
}
代码执行的结果就如下:
但是大家在学习的时候一定看到过这样的实例化方式:
int main()
{
Date d1(2022);
d1.print();
Date d2 = 2023;
d2.print();
return 0;
}
他这里直接采用等于号的方式来实例化一个对象,并且我们运行一下发现这里竟然还能运行成功:
那么之所以可以这样的原因是因为这里并不是将2023这个值赋值给这个对象,而是用这个2023构造出来一个临时对象,再用这个临时对象来拷贝构造这里的d2,所以这一个=的背后其实是有两个过程,一个是构造另外一个就是拷贝构造,但是这是对于一些古董编译器是这么进行的,对于很多现在的编译器里都会将这两个步骤优化为一个步骤直接进行构造,那么这里为了验证这一过程,我们就可以看看我们这个编译器(vs2022)是否使用了这样的优化,我们在构造函数里面加一个打印的语句来作为标记,并且自己再写一个拷贝构造函数在里面也加一个打印语句用来作为标记,其代码如下:
class Date
{
public:
Date(int year)
:_year(year)
{
cout << "构造函数" << endl;
}
Date(const Date& d1)
{
cout << "拷贝构造" << endl;
_year = d1._year;
}
void print() const
{
printf("year = %d\n", _year);
}
private:
int _year = 1;
};
int main()
{
Date d2 = 2023;
d2.print();
return 0;
}
我们运行一下就可以看到这里的结果就是这样:
并没有打印拷贝构造这几个字,说明我们的编译器这里是有优化的,那么看到这里大家应该能够想到这个过程其实是和变量的赋值的过程是类似的,比如说将一个double类型的变量强制类型转换为int类型在赋值给另外一个变量,那么这个过程他也是会创建一个临时变量出来的,用的这个临时变量来进行接下来的赋值,但是这个临时变量是有常性的,如果我们这里用引用的话是得加const来进行修饰的,我们之前讲过,那这里类跟上面的类似那我们在用引用的时候是不是也得加const来进行修饰呢?答案是肯定的比如说我们下面的代码:
int main()
{
const Date& d2 = 2023;
d2.print();
return 0;
}
那么我们就称上述过程为隐式类型转换,如果大家以后在写代码的时候不想出现这种现象的话我们就可以采取在构造函数的前面加上一个explicit这个关键字来修饰这样他就不会出现上诉隐式类型转换的这种情况,那么这就是explicit关键字的作用,当然对于单参数的类型可以实现这样的隐式类型转换,在后序的语法中(c++11就支持了)对于多参数的情况也是可以的,但是我们得把多个参数用一个大括号括起来这样就可以实现多个参数的隐式类型转换,比如我们下面的代码:
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{
cout << "构造函数" << endl;
}
Date(const Date& d1)
{
cout << "拷贝构造" << endl;
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
void print() const
{
printf("year = %d\n", _year);
printf("month = %d\n", _month);
printf("day = %d\n", _day);
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1 = { 2022,12,12 };
d1.print();
return 0;
}
将其运行一下就可以发现这里确实是可以实现多个参数的隐式类型转换的:
那么这就是explicit的用法希望大家能够理解。
static成员
static修饰类中的变量
在讲这个之前我们首先来想一个问题就是如何实现统计调用了多少次构造函数和拷贝构造函数的次数这个功能,那这里有小伙伴就要说了这还不简单,我们直接创建一个全局变量N将其值初始化为0,在构造函数和拷贝构造函数里面添加一个代码N++,这样我们每次调用构造函数或者拷贝构造的时候全局变量N的值就会加一,这样在程序的末尾我们就可以通过观察变量N的值来判断一共调用了多少次拷贝构造和构造函数,比如说我们下面的代码:
int N = 0;
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{
cout << "构造函数" << endl;
N++;
}
Date(const Date & d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "拷贝构造" << endl;
N++;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 11, 11);
Date d2(d1);
printf("%d", N);
return 0;
}
我们这里创建出来了两个对象,一个采用的是拷贝构造一个采用的是构造,所以我们这里打印出来的值应该就是2 :
如果这里可以统计出来调用的构造函数和拷贝构造的次数的话,那么我们这里就可以用这个功能来看我们在传参的时候是否使用了这两个函数:
void test(Date d)
{
;
}
int main()
{
Date d1(2022, 11, 11);
test(d1);
printf("%d", N);
return 0;
}
我们将这个代码运行一下就可以看到这里打印出来的结果就是N等于2,分别调用一次构造函数,一次拷贝构造,那么这就说明我们的函数在传参的时候是通过拷贝构造来进行传参的,而不是先构造出来一个临时对象再用这个临时对象来进行拷贝构造,那么传参过程有了优化,那传值返回也是这样的吗?我们再来看看下面的这个代码:
Date test()
{
Date d1(2022, 11, 11);
return d1;
}
int main()
{
test();
printf("%d", N);
return 0;
}
我们来看看这个代码打印的结果:
我们发现依然是一个构造函数一个拷贝构造,这就说明函数在结束返回的时候也会对其进行相应的优化,那么看到这里我们就基本上实现了这个计数的功能,但是这里有个问题就是我们是通过全局变量来实现的这个功能,但是在c++当中我们是非常的不建议使用全局变量的,因为当我们定义了一个全局变量的时候我们整个程序都可以使用这个变量,这就导致稳定性降低,容易在其他的地方使用和修改这个变量的值,而导致程序错误,所以我们这里不采用全局变量的形式,我们使用static修饰的变量来记录这里的次数,但是这里的修饰又要分为好几个类型,一个是static修饰全局变量,一个是static修饰的局部变量,一个是static修饰的类中的变量,这三种不同类型的变量的生命周期都是整个程序,他们唯一的不同的就是生命周期不同,全局的static整个程序都可以使用,而局部的static只能在函数中使用,类中的static只能在类作用域使用,而我们这里的N他是用来记录类中调用了多少次构造函数和拷贝构造函数,所以我们这里只希望在类中能够使用到他,所以我们这里采用的就是在类中使用static修饰的N来记录这里的次数,而且这里还有个问题就是如果我们在类中创建static类型的变量的话我们得在类的外面实现初始化,并且还得指明类域和该变量的类型,那么这里我们的代码就如下:
class Date
{
public:
static int N;
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{
cout << "构造函数" << endl;
N++;
}
Date(const Date & d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "拷贝构造" << endl;
N++;
}
private:
int _year;
int _month;
int _day;
};
int Date::N = 0;
那么看到这里我们的该功能就算完成了,我们运行一下下面的代码就可以看到我们这里的实现没有啥问题:
Date test()
{
Date d1(2022, 11, 11);
return d1;
}
int main()
{
test();
cout << Date::N << endl;
return 0;
}
一些性质
第一个:
大家通过上面的例子就可以知道,static修饰的变量所有由该类实例化出来的对象都可以使用,比如上面的这个代码我们明明是创建了两个对象使用的是两个不同的函数,但是加1却加到同一个N上去了。
第二个:
我们在构造函数里面不要初始化静态变量的值,因为我们每实例化出来一个对象都会调用构造函数,而类中的static变量又是所有对象中共享的,如果每次都改变必定会影响这里N的正确性。
第三个:
我们在main函数里面实例化出来的一些对象,这些对象里面是没有静态变量的,你想:这些变量创建的地方是在栈上,而static修饰的变量是在堆上,所以该对象中是肯定没有静态变量的,我们还可以通过计算类的大小来验证这里的结果,比如说下面的代码:
class A
{
public:
static int a;
private:
int b;
};
int A::a = 0;
int main()
{
A a1;
cout << sizeof(a1) << endl;
return 0;
}
我们将这个代码运行一下就可以看到这里打印出来的值为:4
那这就说明对象a1中确实没有静态变量a。
第四点:
我们在类的外面访问这里的静态变量的时候,不仅可以通过类名访问到这里的变量,还可以通过该类实例化出来的对象来访问这里的静态变量,因为就算对象中没有静态变量,但是这个对象他也可以间接的代表类域,所以依然可以通过对象来访问到静态变量,比如说下面的代码:
int main()
{
Date d1(2022, 11, 11);
cout << Date::N << endl;
cout << d1.N << endl;
return 0;
}
我们就可以看到这里正常的打印出来了1和1
static修饰成员函数
通过上面的例子我们知道了static修饰的成员变量会有什么样的性质,那么在c++当中static不仅可以修饰变量,还可以修饰类中的成员函数,这些函数在修饰之后是没有this指针的,而我们知道编译器之所以能够区分是哪个对象调用的成员函数就是通过this指针的来实现,所以当一个类中的函数没有this指针的话,那么这也就说明我们在调用这个函数的时候可以无需通过对象来进行调用,比如说我们在Date的类中加一个函数,这个函数的作用就是获取这里的静态变量N的值,那么这里我们的代码就如下:
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{
cout << "构造函数" << endl;
N++;
}
Date(const Date & d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "拷贝构造" << endl;
N++;
}
static int get_number_N()
{
return N;
}
private:
static int N;
int _year;
int _month;
int _day;
};
int Date::N = 0;
那么这时我们想要调用这个该函数的话,我们就无需实例化对象出来而是直接的调用,比如说我们下面的代码:
int main()
{
cout << Date::get_number_N() << endl;
return 0;
}
但是如果我们这么做的话,对于该函数来说还是有一定的缺陷的就是static修饰的函数,它就只能访问到静态的数据,而不能访问到非静态的数据,比如说我们上面的静态函数get_number_N
它就无法访问到非静态数据_year,_month等等。
友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以
友元不宜多用。
友元函数
之前我们尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的
输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作
数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成
全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字,比如说我们下面的代码:
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;
}
我们将其运行一下来看看我们这里的代码写的是否是真确:
很显然我们这里的代码写的是真确的,然后我们再来看看友元类又是什么?那么这里有几个点需要大家注意一下的就是:
1.友元函数可访问类的私有和保护成员,但不是类的成员函数
2.友元函数不能用const修饰
3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4.一个函数可以是多个类的友元函数
5.友元函数的调用与普通函数的调用原理相同
友元类
既然友元函数可以在类的外面访问到类中的私有数据,那么同样的道理友元类也可以在一个类的里面访问到另外一个类中的私有数据,比如说我们下面的代码:
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 = 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;
};
但是这里大家要注意几点就是:
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
但是友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。其次友元关系不能传递如果C是B的友元, B是A的友元,则不能说明C时A的友元。
友元关系不能继承,在继承位置再给大家详细介绍。
内部类
内部类的概念是概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。也就是说内部类他是一个白眼狼,他可以得到外部类的数据,但是外部类却无法得到他的任何数据,注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
内部类的特性为:
- 内部类可以定义在外部类的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;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}
我们将其运行一下就可以看到这里的b确实能够访问的A的私有数据,当然这里使用了匿名对象,我们来看看这段代码运行的结果为:
匿名对象
何为匿名对象?通过名字我们就知道匿名对象就是实例化出来的一个对象,但是这个对象却没有名字,比如说我们上面写的日期类,我们就可以这样创建对象:
int main()
{
Date(2022, 11, 11);
Date d1(2022, 12, 12);
return 0;
}
这里的d1就是有名对象,而d1上面的就是匿名对象,对于匿名对象他的生命周期就只为一行,出了这一行他的生命周期就结束了,比如说上面的代码,当编译器执行到Date d1(2022, 12, 12);
的时候,上面的匿名对象的生命周期就已经结束了编译器便会自动的调用析构函数来结束这个匿名对象,那么这个就是匿名对象的介绍,没啥复杂的东西。
拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还
是非常有用的。
场景一:
这里的A是一个类的类型
A aa1 = 1;
那么根据上面的学习,我们知道这里是两个步骤,先是用1来构造出来一个临时对象,再用这个临时对象来拷贝构造这个aa1,但是这里就很明显这个构造出来的临时对象就是一个多余的步骤,所以编译器在这里就会做出一些简化,直接拿这个1来构造这个aa1.
场景二:
void f1(A aa)
{}
f1(A(1)); // 构造 + 拷贝构造 -> 优化 构造
f1(1);
我们直接用匿名对象来进行传参,那么这样的话,这里的一步就会变成两个过程:先是构造,再是拷贝构造赋值给这里的形参aa,那么这样的话我们的编译器就会对其进行优化将其变成一步就是构造
场景三:
A f2()
{
A aa;
return aa;
}
f2(); // 构造+拷贝构造
A ret = f2(); // 构造+拷贝构造+拷贝构造 ->优化 构造+拷贝构造
这里是传值返回,我们现在函数里面创建实例化了一个对象,再将这个对象进行传值返回,那么再传值返回的过程中,我们的编译器会拷贝构造出来一个临时对象,所以f2()实际上就是两个过程:构造+拷贝构造,当我们用一个变量来接收这里的函数返回值的时候,这里就会又多出一个步骤就是将函数的返回的那个临时对象拷贝构造给这里的ret,这里编译器就会发现创建出来的那个临时对象似乎没有什么用,所以就会直接优化为构造+拷贝构造。
场景四:
f3()
{
return A(10);
}
A ret = f3(); // 构造+拷贝构造+拷贝构造 -> 优化 -> 构造
当一个函数传值返回返回的是一个匿名对象的时候,编译器会将构造+拷贝构造+拷贝构造直接优化为构造。