第四章:类和对象下
4.1再谈构造函数
4.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;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
4.1.2初始化列表
1.引入:
看下面一段代码:
class A
{
public:
A(int a, int& b)
{
_a = a; // 报错
_b = b; // 报错
}
void func()
{
++_b;
++_b;
}
private:
// 成员函数
const int _a;
int& _b;
};
int main()
{
int n = 0;
A a1(1, n);
return 0;
}
为什么会报错?
const
类型和引用类型的变量只能在定义的时候初始化,我们已经知道了,在构造函数内所写的_a = a; _b = b;
是赋值而不是初始化,所以自然就编不过了。那怎么办?引入一个新概念:初始化列表。
2.初始化列表的写法:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
上面的问题代码就可以解决了:
class A
{
public:
A(int a, int& b)
:_a(a)
,_b(b)
{}
void func()
{
++_b;
++_b;
}
private:
// 成员函数
// 必须在定义的地方初始化
const int _a;
int& _b; // 引用也必须在定义的地方初始化
};
int main()
{
int n = 0;
A a1(1, n);
a1.func();
cout << n << endl; // n变成了2
return 0;
}
3.注意(总结):
1)每一个成员在初始化列表中只能出现一次(初始化只能初始化一次)。
2)类中包含以下成员,必须在初始化列表位置初始化:
- const成员变量
- 引用成员变量
- 自定义类型成员,且该类没有默认构造函数时(但必须有构造函数)
class A
{
public:
A(int a) // 有构造函数,但不是默认的
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_aobj(a) // 不写这句代码直接报错
,_ref(ref)
,_n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
int mian()
{
B b(1, 2);
return 0;
}
如果没有初始化_aobj,没有:_aobj(a)
这句代码,程序直接报错。因为_aobj是自定义类型的成员,且它没有默认构造函数,只有构造函数,只能使用这个构造函数来进行初始化。
3)初始化列表是每个成员定义的地方,不管你写不写,每个成员都要走初始化列表,所以每个成员最好都用初始化列表初始化。
class Time
{
public:
Time(int hour = 0)
:_hour(hour)
{}
private:
int _hour;
};
class Date
{
public:
Date(int day)
{}
private:
int _day;
Time _t;
};
int main()
{
Date d1(1);
return 0;
}
执行构造函数时,先执行初始化列表中的内容,如果有的成员没有写进初始化列表,就会被编译器自行处理:内置类型成员不做处理,自定义类型的变量用默认的构造函数初始化。
执行
Date d1(1);
时,相当于执行Date类的构造函数。发现Date的构造函数中没有定义初始化列表,根据默认情况,内置类型成员_day
不做处理,自定义类型成员_t
会去调用自己的默认构造函数初始化。其中Time类型的构造函数有自定义的初始化列表,所以先执行这个初始化列表,让hour的值给_hour。
4)成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
// 下面代码运行的结果是什么?
class C
{
public:
C(int a)
: _a1(a)
, _a2(_a1)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
C aa(1);
aa.Print();
return 0;
}
上面代码运行的一个结果:1 -858993460
其中_a2是随机值。为什么?
执行C的构造函数时,先走初始化列表。而初始化列表中,成员变量初始化的执行顺序和成员变量在初始化列表中的位置无关,只和成员变量声明的顺序有关。
对于上面的这段代码:执行C的初始化列表时,先执行的代码是:_a2(a1)
,因为_a2最先声明。而_a1此时是随机值,把_a1的值赋给_a2,_a2就是随机值。后执行的代码是:_a1(a)
,我们传给a的值是1,所以把a的值赋给_a1,_a1自然也是1。上面的输出结果就是这么来的。
4.初始化列表一般会和构造函数体混合使用:
写一个栈的构造函数:
class Stack
{
//成员函数
public:
//构造函数
Stack(int capacity = 4) //给缺省值,如果什么也不传,就默认开辟4个数据的空间
: _a((STDataType*)malloc(sizeof(STDataType) * capacity))
, _top(0)
, _capacity(capacity)
{
if (_a == nullptr)
{
perror("malloc fail");
return;
}
memset(_a, 0, sizeof(DateType) * _capacity);
}
//成员变量
private:
STDataType* _a;
int _top;
int _capacity;
};
4.1.3explicit关键字
1.单参数构造函数的隐式类型转换:
class A
{
public:
A(int i)
:_a(i)
{}
A(const A& aa)
:_a(aa._a)
{}
private:
int _a;
};
int main()
{
A aa1(1);
A aa2 = 2; // 这句代码是单参数的隐式类型转换
A& ref = 2; // 报错,权限放大
const A& reftt = 2; // 可以
return 0;
}
编译器在执行A aa2 = 2;
这句代码时,会先用2调用A的构造函数,去生成一个临时对象,再用这个对象去拷贝构造aa2。有的编译器会优化,直接用2调用构造函数去构造aa2。验证:
上述代码,执行
A& ref = 2;
时,会报错。因为编译器先用2调用构造函数去生成了一个临时对象,而临时对象是具有常性的,传给没有const
修饰的引用ret属于权限的放大,所以编不过;而传给用const
修饰的rett就不会报错,因为这属于权限的平移。
2.浅看一下隐式类型转化有什么用:
class A
{
public:
A(int i)
:_a(i)
{}
A(const A& aa)
:_a(aa._a)
{}
private:
int _a;
};
typedef A DataType;
// 顺序表中存的是A类型的数据
class SeqList
{
public:
// 成员函数
SeqList(size_t capacity = 10)
: _array((DataType*)malloc(sizeof(DataType)* capacity))
, _capacity(capacity)
, _size(0)
{
memset(_array, 0, sizeof(DataType) * _capacity);
}
void Push(const DataType& x) { _array[_size++] = x; }
private:
// 成员变量
DataType* _array;
size_t _capacity;
size_t _size;
};
int main()
{
SeqList s;
A aa1(1); // 没有隐式类型转换时需要先创建一个A类型的变量,再将这个变量传入s
s.Push(aa1);
s.Push(2); // 有了隐式类型转换就可以这样写,不用再创建一个A类型的变量了
return 0;
}
C++11还支持多参数隐式类型转换:
class A
{
public:
A(int a, int b)
:_a(a)
,_b(b)
{}
A(const A& aa)
:_a(aa._a)
,_b(aa._b)
{}
private:
int _a;
int _b;
};
typedef A DataType;
class SeqList
{
public:
// 成员函数
SeqList(size_t capacity = 10)
: _array((DataType*)malloc(sizeof(DataType)* capacity))
, _capacity(capacity)
, _size(0)
{
memset(_array, 0, sizeof(DataType) * _capacity);
}
void Push(const DataType& x) { _array[_size++] = x; }
private:
// 成员变量
DataType* _array;
size_t _capacity;
size_t _size;
};
int main()
{
SeqList s;
A aa1 = { 2, 3 }; // 使用一个大括号,来实现多参数的隐式类型转换
s.Push({1, 2});
return 0;
}
使用一个{},来实现隐式类型的转换。
3.explicit的作用:
禁止隐式类型转换的发生:
class A
{
public:
explicit A(int i) // 在构造函数前加了explicit,就不能再使用隐式类型转换了
:_a(i)
{}
A(const A& aa)
:_a(aa._a)
{}
private:
int _a;
};
int main()
{
A aa1(1);
// 下面两句代码全都编不过
A aa2 = 2;
const A& reftt = 2;
return 0;
}
4.2static成员
1.引入:
如何知道创建了多少个某类类型的变量?以及正在使用中的某类类型的变量?
1)方法1:
// 正在使用的
int m = 0;
// 累计创建了多少个对象
int n = 0;
class A
{
public:
A()
{
++m;
++n;
}
A(const A& t)
{
++m;
++n;
}
~A()
{
--m;
}
};
const A& Func(const A& aa)
{
return aa;
}
int main()
{
A aa1;
A aa2;
cout << "n:" << n << " m:" << m << endl;
A();
cout << "n:" << n << " m:" << m << endl;
Func(aa1);
cout << "n:" << n << " m:" << m << endl;
return 0;
}
输出结果:
n:2 m:2
n:3 m:2
n:3 m:2
使用全局变量m
来记录正在使用的A类型变量个数,n
来记录累计创建的A类型变量个数。创建新变量时,肯定要用到构造函数或拷贝构造函数,那么我们可以在这两个函数中,让m
和n++
,此时m
和n
都记录的是累计创建的A类型变量个数。如何记录正在使用的A类型变量个数呢?因为自定义类型变量出了作用域就会销毁,所以在析构函数中对m--
就可以了。
思考:上面这种写法有什么缺陷?
容易被动手脚
// 类还是上面的类,只写了主函数
int main()
{
A aa1;
A aa2;
cout << "n:" << n << " m:" << m << endl;
// 动手脚
--n;
--m;
A();
cout << "n:" << n << " m:" << m << endl;
Func(aa1);
cout << "n:" << n << " m:" << m << endl;
return 0;
}
输出结果改变:
n:2 m:2
n:2 m:1
n:2 m:1
2)方法2:(n和m是公有的)
class A
{
public:
A()
{
++m;
++n;
}
A(const A& t)
{
++m;
++n;
}
~A()
{
--m;
}
// m和n用public修饰
// 声明
// 正在使用的
static int m;
// 累计创建了多少个对象
static int n;
};
// 定义
int A::n = 0;
int A::m = 0;
// 注意,在类中定义的static修饰的变量,不能在声明处给缺省值
// 因为声明处给的缺省值是在初始化列表中使用的,而静态成员变量不会走初始化列表,所以不能在声明处给缺省值
int main()
{
A aa1;
A aa2;
// 直接访问n和m不可以
// cout << "n:" << n << " m:" << m << endl;
// 可以这样访问
cout << "n:" << A::n << " m:" << A::m << endl;
// 也可以这样访问(因为静态成员变量属于所有A对象,属于整个类)
cout << "n:" << aa1.n << " m:" << aa2.m << endl;
// 不能像之前一样动手脚了
/*
--m;
--n;
*/
// 报错
// 可以这样动手脚
--A::n;
--aa2.m;
cout << "n:" << aa1.n << " m:" << aa2.m << endl;
return 0;
}
输出结果:
n:2 m:2
n:2 m:2
n:1 m:1
注意:
1)静态成员变量属于所有A对象,属于整个类。
2)虽然不能像之前一样那么明目张胆的改变n和m,但是也是有方法可以改变的。
3)通过aa1或aa2改变n和m,结果是一样的,改变的是同一个n,同一个m,因为静态成员变量属于所有A对象,是大家共同的成员变量。
4)在类中定义的static修饰的变量,不能在声明处给缺省值:不能在声明处直接写static int n = 0;
。因为声明处给的缺省值是在初始化列表中使用的,而静态成员变量不会走初始化列表,所以不能在声明处给缺省值。
思考:创建一个A*类型的指针变量,赋值为空,可以像下面这样访问n和m吗?
int main()
{
A aa1;
A aa2;
cout << "n:" << aa1.n << " m:" << aa2.m << endl;
// 像下面这样去访问n和m
A* ptr = nullptr;
ptr->m++;
ptr->n++;
cout << "n:" << aa1.n << " m:" << aa2.m << endl;
return 0;
}
输出结果:
n:2 m:2
n:3 m:3
结论:访问成功,不报错(和用空指针去访问成员函数是同样的道理,这是为什么?)
在这段代码中,看似发生了控制指针的解引用,实则没有,编译器没有那样做。因为
static
修饰的变量是存储在静态区的,你写下ptr->n
这样的代码,其实从底层来看,和A::n
是同样的作用,都是用来突破类域的写法,告诉编译器要去哪个类中去找这个成员。
当然,如何n和m用private
修饰,上面所有的访问方式就都行不通了。但是,用private
修饰才是正确的方法,这样就可以彻底防止被动小动作(人为改变n和m),我们要查看n和m的值,只需要写一个公有的函数就可以了。
class A
{
public:
A()
{
++_m;
++_n;
}
A(const A& t)
{
++_m;
++_n;
}
~A()
{
--_m;
}
void Print()
{
cout << "n:" << _n << " m:" << _m << endl;
}
private:
static int _m;
static int _n;
};
// 静态成员变量一定要在类外初始化
int A::_m = 0;
int A::_n = 0;
int main()
{
A aa1;
A aa2;
// A::Print(); 报错
aa1.Print();
return 0;
}
思考:能不能匿名成员来使用Print()
函数?
代码:A().Print();
不行,因为为了使用这个打印功能,就多创建了一个对象,干扰了我们正常的逻辑。
要想用A::Print()
这样的方式使用打印函数怎么办?
使用静态成员函数:
static void Print() { cout << "n:" << _n << " m:" << _m << endl; }
这样就可以使用
A::Print()
了。
静态成员函数的特点:没有this指针
class A
{
public:
A()
{
++_m;
++_n;
}
A(const A& t)
{
++_m;
++_n;
}
~A()
{
--_m;
}
// 静态成员函数的特点是没有this指针
static void Print()
{
_x++; // 报错,因为没有this指针
cout << "n:" << _n << " m:" << _m << endl; // 可以访问非静态成员变量
}
private:
static int _m;
static int _n;
int _x = 0;
};
int A::_m = 0;
int A::_n = 0;
int main()
{
A aa1;
A aa2;
A::Print();
aa1.Print();
return 0;
}
非静态成员变量,需要使用this指针来访问,而静态成员函数没有this指针,自然无法访问非静态成员变量。
2.概念:
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
3.特性:
1)静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
2)静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明。
3)类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问。
4)静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
5)静态成员也是类的成员,受public、protected、private 访问限定符的限制。
4.看一道例题:
题目:
求1+2+3+…+n,要求不能使用乘除法,不能使用
for
,while
,if
,else
,switch
,case
等关键字及条件判断语句(A?B:C
)。
分析问题:
限制了任何形式的循环,也因为不能使用
for
,while
;限制了递归,因为递归中很重要的一步就是确定递归的边界,而这种边界条件的设置,需要借助if
等条件判断语句来完成。
代码:
class Sum
{
public:
Sum()
{
_ret += _i;
_i++;
}
static int Getret()
{
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Soution
{
public:
int Sum_Solution(int n)
{
Sum a[n]; // 变长数组
return Sum::Getret();
}
};
int main()
{
cout<< Soultion().Sum_Soution(10) << endl;
return 0;
}
分析代码:
我们可以利用静态成员,通过重复调用构造函数的方式,来完成该题目。
1)首先,先创建一个自定义类型Sum
,里面包含两个静态成员变量_i
和_ret
,_i
记录从1到n的变化,每走一次构造函数,就++一次;_ret
记录1+2+…+n的结果。_i
的初始值为1,_ret
的初始值为0。
2)其次,我们怎么实现构造函数的多次调用呢?现在绝大多数编译器都支持变长数组,我们可以通过创建一个Sum
类型数组的方式,实现构造函数的多次调用。Sum a[n];
3)最后,我们要返回_ret
的值,而_ret
是私有的,我们需要写一个成员函数来,通过调用这个成员函数来访问它。之所以选择静态成员函数,是因为这样就可以用类名加::加函数名的方式(Sum::Getret()
)来使用这个函数了。
4.3友元
友元提供了一种突破封装的方式,有时,提供了便利。但友元提高了耦合度,破坏了封装,所以不宜多用。
4.3.1友元函数
1.问题:
现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
2.解决:
使用友元关键字friend
,在类外定义函数:
class Time
{
// 友元函数
friend ostream& operator<<(ostream& out, const Time& t);
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
ostream& operator<<(ostream& out, const Time& t)
{
out << t._hour << "/" << t._minute << "/" << t._second << endl;
return out;
}
int main()
{
Time t(12, 30, 6);
cout << t << endl;
return 0;
}
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
3.说明:
1)友元函数可访问类的私有和保护成员,但不是类的成员函数。
2)友元函数不能用const修饰。
3)友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
4)一个函数可以是多个类的友元函数。
5)友元函数的调用与普通函数的调用原理相同。
4.3.2友元类
1.介绍:
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
class Time
{
// 声明Date类为Time类的友元,这样就可以在Date中直接访问Time的私有成员了
friend class Date;
// 友元函数
friend ostream& operator<<(ostream& out, const Time& t);
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
{
friend ostream& operator<<(ostream& out, const Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
// 直接访问了Time类的私有成员
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;
};
ostream& operator<<(ostream& out, const Time& t)
{
out << t._hour << "/" << t._minute << "/" << t._second << endl;
return out;
}
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
out << d._t << endl;
return out;
}
int main()
{
Date d1(2023, 9, 23);
cout << d1 << endl;
d1.SetTimeofDate(12, 2, 34);
cout << d1 << endl;
return 0;
}
2.说明:
1)友元关系是单向的,不具有交换性。(我把你当朋友,你不一定把我当朋友)
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
2)友元关系不能传递。(你的朋友不是我的朋友)
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
3)友元关系不能继承,在继承位置再给大家详细介绍。
3.总结:
不到万不得已,不要使用友元。
4.4内部类
1.概念:
在一个类内部定义的类,就叫内部类:
class A
{
public:
// B是内部类
class B
{
private:
int _b;
};
// B类受A类域和访问限定符的限制,其实他们是两个独立的类
private:
int _a;
};
如何理解“B类受A类域和访问限定符的限制,其实他们是两个独立的类”?
1)如果要创建一个B类型的变量,需要这样写:A::B b;
,而且B类必须在A类中用public
修饰。如果用private
或protected
修饰,那么就不能像上面那么写了,因为没有对B类的访问权限。
2)用siezof
查看A类的大小:
class A
{
public:
class B
{
private:
int _b;
};
private:
int _a;
};
int main()
{
cout << sizeof(A) << endl; // 输出结果是4,只有_a的大小
return 0;
}
发现输出结果是4,说明B在存储这一概念上并没有存在A类中,A类和B类是两个独立的类。
2.内部类就是外部类的友元
class A
{
public:
class B
{
public:
void func(A a, int b)
{
a._a = 10; // 可以直接访问A的私有成员
_b = b;
}
private:
int _b;
};
private:
int _a;
};
内部类可以访问外部类,但外部类不可以访问内部类。
3.用内部类改进之前static的练习题
class Solution
{
class Sum
{
public:
Sum()
{
_ret += _i;
_i++;
}
};
public:
int Sum_solution(int n)
{
Sum a[n];
return _ret;
}
private:
static int _i;
static int _ret;
};
int Solution::_i = 1;
int Solution::_ret = 0;
内部类的使用使代码更加简洁了。
4.5匿名对象
1.匿名对象的定义:
class A
{
public:
A(int a = 0)
:_a(a)
{}
private:
int _a;
};
int main()
{
// 有名对象
// 特点:生命周期在当前局部域
A a1;
// 匿名对象
// 特点:生命周期只在这一行
A(7);
return 0;
}
不要认为匿名对象没有用,事实上,匿名对象的使用非常广泛。
2.匿名对象的使用场景:
class Solution
{
public:
int sum(int x, int y)
{
return x + y;
}
};
int main()
{
// 正常情况下调用这个Solution
Solution sl;
sl.sum(10, 20);
// 利用匿名对象去调用sum
Solution().sum(10, 29);
return 0;
}
上面的代码中,Solution()
就是定义了一个匿名对象。有了匿名对象,我们就不用为了使用Soultion
类里的功能,再多写一行代码去创建一个有名函数sl
了。
3.临时对象和匿名对象都具有常属性,无法更改
class A
{
public:
A(int a = 0)
:_a(a)
{}
private:
int _a;
};
void f1(A& aa)
{
}
int main()
{
f1(A()); // 报错
return 0;
}
当临时对象作为引用传给f1后,是权限的放大,导致这个临时对象可以被修改了,直接报错。要想正常传参,要这样写void f1(const A& aa)
。
4.匿名对象的生命周期只在当前这一行,而const加引用的方式会延长匿名对象的生命周期
class A
{
public:
A(int a = 0)
:_a(a)
{}
private:
int _a;
};
int mian()
{
// const引用会延长匿名对象生命周期
// ret出了作用域,匿名对象才销毁
const A& ret = A();
return 0;
}
const A& ret = A();
本质上是给匿名对象又取了一个名字,叫ret
。
4.6拷贝对象时的一些编译器优化(了解学习)
在传参和传返回值的过程中,编译器一般会做一些优化(大部分编译器都会优化),减少拷贝构造函数的调用,来看一些例子:
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;
}
以下的例子都是根据以上代码运行的。
1.来复习一下拷贝构造函数调用的场景:
int main()
{
// 传值传参
A aa1;
f1(aa1);
cout << "————————————————————" << endl;
// 传值返回
f2();
cout << "————————————————————" << endl;
}
输出结果:
A(int a)
A(const A& aa)
~A()
————————————————————
A(int a)
~A()
————————————————————
~A()
分析:
1)代码
A aa1;
先调用了一次构造函数A(int a)
,初始化变量aa1。接着将aa1传给形参aa时,是一个传值调用,调用拷贝构造函数A(const A& aa)
。出函数f1时,调用析构函数~A()
,析构形参aa。然后打印一条横线。
2)执行函数f2时,在函数内,代码A aa;
先调用了一次构造函数A(int a)
。return
aa时,实际上会把aa的值用拷贝构造函数先储存在一个临时变量中,这里没有显示的打印出来。然后aa出作用域,调用析构函数~A()
销毁。
3)最后,主函数结束,aa1出作用域,调用析构~A()
销毁。
2.几种编译器优化的情况分析:
1)情况一:
int main()
{
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << endl;
return 0;
}
输出结果:
A(int a)
~A()
A(int a)
~A()
分析:
1)没有优化的情况下,执行代码
f1(1);
,会先调用一次构造函数A(int a)
,进行隐式类型转化,生成一个临时变量。再调用一次拷贝构造A(const A& aa)
,将临时变量拷贝给aa。但是打印结果却并非如此,经过优化,编译器只调用了一次构造函数A(int a)
,将连续构造+拷贝构造优化成了直接构造。
2)没有优化的情况下,执行代码f1(A(2));
,会先调用一次构造函数A(int a)
,给匿名对象初始化。然后再调用一次拷贝构造A(const A& aa)
,将匿名对象拷贝给aa。但是打印结果并非如此,经过优化,编译器只调用了一次构造函数A(int a)
,将连续构造+拷贝构造优化为了一个构造。
2)情况二:
int main()
{
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
cout << endl;
// 多个表达式中,连续拷贝构造+赋值重载->无法优化
A aa1;
aa1 = f2();
cout << endl;
return 0;
}
输出:
A(int a)
A(int a)
A(int a)
A& operator=(const A& aa)
~A()
~A()
~A()
分析:
1)在没有优化的情况下,执行代码
A aa2 = f2();
,会先进入f2函数,调用一次构造函数A(int a)
,初始化aa。然后return
aa时,会调用拷贝构造A(const A& aa)
,生成一个临时变量。然后再调用一次拷贝构造A(const A& aa)
,将临时变量的值给aa2。出函数f2时,aa出作用域,调用析构~A()
销毁。在有优化的情况下,编译器将A aa2 = f2();
这段代码直接看作给aa2初始化,仅调用了一个构造函数。通过调试可以发现,编译器进入了f2函数,仅执行了一次初始化aa的构造函数,甚至都没有析构,然后就这样直接完成了aa2的初始化。
2)下面这段代码相当于把1)中的代码拆开,分成了两个步骤去写,这样写是不会触发编译器的优化的。编译器先是执行了一次构造A(int a)
,初始化aa1。然后又进入f2,调用构造A(int a)
,初始化aa。return
aa时,创建临时变量的这一步没有打印出来,但确实是存在的。之后又调用了一次赋值重载A& operator=(const A& aa)
,将临时变量的值给了aa1。再然后就是aa出了作用域,调用了析构~A()
,至此,第二部分代码结束。
3)最后,aa1和aa2也出了作用域,再调用两次析构函数~A()
。
4.7再次理解类和对象
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
- 用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程。
- 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中。
- 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能知道洗衣机是什么东西。
- 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有哪些属性,哪些方法,描述完成后就形成了一种新的自定义类型,采用该自定义类型就可以实例化具体的对象。