文章目录
- 🍕前言
- 一、🍕再谈构造函数
- 1.1构造函数体赋值
- 1.2初始化列表
- 1.3explicit关键字
- 二、🍕static成员
- 三、🍕友元
- 四、🍕内部类
- 五、🍕匿名对象
- 六、🍕拷贝对象时编译器的一些优化
- 七、🍕再谈类和对象
- 🍕总结
🍕前言
本文章继自类和对象(中),完成收尾工作。
一、🍕再谈构造函数
1.1构造函数体赋值
在学习过的类和对象的基础知识中,构造函数内部通常是给成员变量一个初始值。虽然调用完构造函数后,变量有了初始值,但是不能称其为对对象的初始化,只能称其为对变量的赋值。
因为初始化只能初始化一次,而赋值可以多次赋值。
所以下面给出一个真正的初始化操作:初始化列表。
1.2初始化列表
初始化列表是构造函数的一部分,与构造函数体赋值并没有冲突,可以共存,只是初始化列表的每个变量都只能出现一次(因为初始化只初始化一次)。
初始化列表格式:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
比如:
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
构造函数的大括号内部仍然可以像构造函数一样写其他的东西。
注意:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
(1)引用成员变量
(2)const成员变量
(3)自定义类型成员(且该类没有默认构造函数时)
以下面的代码为例:
class B
{
public:
class A
{
public:
A(int a = 1)
:_a(a)
{}
private:
int _a;
};
B(int a, int ref,int n)
: _ref(ref)
, _n(10)
{}
private:
A _aobj; //没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
不同于其他内置类型,其他内置类型可以只定义不初始化,比如:
int a;
double b;
char c;
对于引用成员变量和const成员变量来说,它们有一个共同点:
在定义的时候必须初始化。
所以在类的构造函数的初始化列表中必须有这两个成员变量的存在。
而对于自定义类型,前面我们说过,编译器自己生成的默认构造函数对内置类型不做处理,对自定义类型调用它的默认构造函数。
在初始化列表中,如果该自定义类型没有默认构造函数,(默认构造函数包括:无参构造函数,全缺省构造函数和编译器自己生成的构造函数。其中如果我们不写构造函数,编译器才会自己生成)那么在初始化列表必须要对自定义类型初始化。
如果不初始化,会报错,比如:
在这个例子中,A类的构造函数并不是默认构造函数,而是一个普通的构造函数,并且在B类的构造函数的初始化列表中并没有对A类进行初始化,这就会产生报错。
总结:
-
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
-
类中包含以下成员,必须放在初始化列表位置进行初始化:
(1)引用成员变量
(2)const成员变量
(3)自定义类型成员(且该类没有默认构造函数时) -
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
-
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
第四点是非常重要的一点,下面给一道题:
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();
}
A.输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
这道题选择D项。首先排除程序会崩溃。
程序崩溃的原因不多,主要就是动态申请的资源泄露了或者产生死循环等等。
明显本题并没有以上情况。这道题既然能放在这里,说明该题可能会有坑,可以排除A,那就从CD之间选择。认真看代码,发现没有什么语法错误,不会编译不通过,只能选一个看似最离谱的答案D。
解析:类A实例化一个对象aa并显式地传递一个1给形参a,调用它的构造函数,此时在初始化列表中看似先初始化a1,再初始化a2,实际上是先初始化a2,再初始化a1,因为初始化的顺序取决于成员变量的声明顺序的!
所以:成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
1.3explicit关键字
explicit关键字的作用在于禁止将会禁止构造函数的隐式转换。
对于什么是隐式转换,请看下面的代码:
class Date
{
public:
explicit Date(int year)
:_year(year)
{}
//赋值运算符重载
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;
};
int main()
{
Date d2 = 2;
}
实例化的类对象中,看似是将变量2赋值给对象d2,实际上的情况是:对于不同类型的赋值,会在中间生成一个临时空间,整型2发生了隐式类型转换,升级成了类对象,这个类对象2调用构造函数,此时这块临时空间就是一个对象,然后该临时空间再调用拷贝构造函数拷贝给d2。
这个过程是:构造–>拷贝构造。
但是编译器会自动优化,优化成直接进行构造。
对于编译器的优化,我们会在下面的篇幅详细地讲到。
对于explicit关键字,就是禁止编译器对于这种隐式类型转化。
当我们加上exiplict关键字后,编译器会直接报错。
二、🍕static成员
被static修饰的成员变量叫做静态成员变量,被修饰的成员函数叫做静态成员函数。普通成员变量属于对象自己所有,静态成员变量属于每个类对象所共有的。
注意:1.静态成员变量一定要在类外面进行初始化。
2.静态成员变量或静态成员函数可以直接通过类域访问。(因为它属于整个类)对于静态成员变量, 如果设置成公有,那就跟全局变量几乎一样,都可以被类外面进行访问。
下面有一道面试题:实现一个类,计算程序中出现了多少类对象。
为了完成这道题目,我们实现的类不能在外面计算出现的类的次数。只能在类内计算,如果调用了构造函数或者拷贝构造函数,就让一个计数变量++,如果调用了析构函数,就让计数变量–。很明显需要用静态成员函数才能实现,为了获得该成员变量,我们需要在对象内实现一个静态成员函数,来跟静态成员变量进行配套使用。
静态成员函数的特点:
1.没有this指针
2.指定类域和访问限定符即可访问。
所以可以通过下面的代码来计算类的对象的个数。
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A()
{
--_scount;
}
static int GetACount()
{
return _scount;
}
private:
static int _scount;
};
另一道题目:
设计一个类,在类外面只能在栈/堆上创建一个对象。
class A
{
public:
static A GetStackObj()
{
A a;
return a;
}
static A* GetHeapObj()
{
return new A;
}
private:
A()
{}
private:
int _a1;
int _a2;
};
int main()
{
A::GetStackObj();
A::GetHeapObj();
return 0;
}
解析:将构造函数设置成私有的,那么所有的类型的对象都不能在类外面实例化
但是又将栈/堆的成员函数设置成静态公有的,使得我们可以在类外面直接通过类作用限定符
//直接调用该静态成员函数,达到题目要求。
再补充几点:
1.不能通过在类中给静态成员变量缺省值,因为这个地方只是静态成员变量的声明,并且缺省值实际上是给初始化列表的,对于普通成员变量来说,可以在构造函数的初始化列表中进行初始化,而静态成员变量则无法实现。
2.可以通过普通成员函数访问静态成员函数,因为对于静态成员函数来说,只要给定类域和访问限定符,就可以访问静态成员函数,在类中是不受访问限定符的限制的,也不受类域的限制,因为对于类来说这是一个整体;但不能通过静态成员函数访问普通成员函数,因为静态成员函数没有this指针,无法访问普通成员函数和成员变量。
三、🍕友元
友元的出现突破了类的封装机制,友元就像是类的朋友,可以随意访问类的非公有成员变量和成员函数,所以友元不适合多用。
友元分为友元函数和友元类。
友元函数:
我们知道,一般内置类型的输出可以使用
cout << 内置类型
的方式进行输出。但是对于一个自定义类型,却不能定义成类的成员函数:operator<<。因为在成员函数中有一个隐含的this指针,该指针会一直占用第一个位置,导致左操作数只能是cout。
所以如果要定义成成员函数,只能实现成 d1 << cout。(假设d1是一个日期类对象)。
如果定义成全局函数,就不再有this指针,左操作数就可以正常地中作为cout。但是重载成全局函数又导致我们无法访问到类地成员变量,这就需要友元函数的出现。
友元函数关键字: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.友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
2.友元函数可访问类的私有和保护成员,但不是类的成员函数
3.友元函数不能用const修饰
4.友元函数可以在类定义的任何地方声明,不受类访问限定符限制
5.一个函数可以是多个类的友元函数
6.友元函数的调用与普通函数的调用原理相同
四、🍕内部类
如果一个类定义在外部类的内部,那么这个类就叫做内部类。内部类不属于外部类,外部类不能通过任何途径访问内部类的成员。
注意:内部类天生就是外部类的友元,但外部类不是内部类的友元。
特性1:
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 a;
cout << sizeof(a) << endl;
return 0;
}
结果是4;
解析:对于外部类来说,内部类和外部类没有声明关系,因为内部类只是一个声明,并没有真正意义地创建内部类对象。相当于A类是一个图纸,通过A类这个图纸建造了a这所房子,但是B是A类的图纸里面的一张图纸,并没有通过B这个图纸真正创造一所房子。
特性2:
内部类受外部类的访问限定符的限制。
特性3:
建议内部类的成员变量声明在外部类,这样内部类既可以使用它的成员变量,外部类也可以使用内部类的成员变量。
例题:求和
class Solution {
public:
int Sum_Solution(int n)
{
Sum a[n];
return _sum;
}
class Sum
{
public:
Sum()
{
_sum+=_i;
++_i;
}
};
private:
static int _i;
static int _sum;
};
int Solution::_i = 1;
int Solution::_sum = 0;
五、🍕匿名对象
请看下面的代码以及注释:
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n)
{
//...
return n;
}
};
int main()
{
A aa1;
// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
// A aa1();
// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
A();
A aa2(2);
//不能这样调用:
//Solution.Sum_Solution(10);
//Solution::.Sum_Solution(10);
//对于第1种情况,不能直接使用类型调用它的成员函数
//对于第二种情况,只有static成员函数或者成员变量可以这样调用,因为静态成员函数没有this指针,而普通成员函数需要传递this指针,如果这样调用,就无法传递this指针。
Solution().Sum_Solution(10);
return 0;
}
匿名对象特性:
1.匿名对象的生命周期在当前行
有名对象的生命周期在当前的局部作用域
2.匿名对象类似于临时对象,都具有常性
3const引用延长了匿名对象的生命周期
1.匿名对象的生命周期只有1行
A(2);
2.匿名对象具有常性,如果这样定义结果是权限放大了,不行
A& pa = A(3);
const A& ra = A(3);//这样就可以,const修饰的变量具有常性,这样是权限平移。
3.const引用修饰的匿名对象延长了它的生命周期,使其生命周期延长到当前函数作用域。
六、🍕拷贝对象时编译器的一些优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝。
几种优化情况:
1.同一行一个表达式中连续的构造+构造会优化成为1个构造。
2.同一行一个表达式中连续的构造+拷贝构造会优化成为1个构造
3.不同行的构造+赋值重载不会优化
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()
{
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
A aa2;
aa2 = f2();
return 0;
}
七、🍕再谈类和对象
类是对某一类实体(对象)来进行描述的,描述该对象具有那
些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
🍕总结
类和对象收尾工作到此结束。