若想了解什么是类、封装的意义可以移步 【C++】类与对象(引入)
若对六大成员函数或const成员函数有疑问的这篇文章可能可以帮到你 【C++】类与对象(上)
目录
系列文章
前言
1.初始化列表
1.1概念
1.2特性
1.2.1必须使用初始化列表的情况
1.2.2初始化的顺序
2.explicit关键字
3.Static成员
3.1静态成员变量
3.2静态成员函数
3.3功能实现
4.友元
4.1友元函数
4.2友元类
5.内部类
6.匿名对象
6.1使用
6.2证明生命周期
7.拷贝对象时编译器的优化
总结
这一期博客算是给整个类与对象的系列做个收尾,补充类的一些其他功能,还有对类与对象更多的细节理解。
虽然说我们已经学会了使用构造函数对函数进行初始化,但下面这个情况是否出乎你的意料呢?const 的成员变量,因其特性一开始我们便需要对其初始化,但想在函数之中修改却又无法修改,这是为什么呢?
有同学说,在定义变量的时候使用缺省不就好了吗?这个方式确实能够解决问题,但缺省的这个功能是 C++11 才出现的,在那之前难道就无法解决这个问题吗?究其本质,在构造函数体内的这种操作已经算是赋值了。真正初始化变量的地方并不在这,而是我们接下来要讲的初始化列表。
初始化列表:以 :开头,接以 , 分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。
位置位于构造函数名与函数内容之间。
class A
{
public:
A()
:_a(5) //初始化列表初始化_a和_b
,_b(3)
{}
private:
int _a;
const int _b;
};
int main()
{
A a;
return 0;
}
每个成员变量只能在初始化列表中出现一次。就像外部定义变量那样,同名的变量也不可以定义两次!
正如上文所说,构造函数的函数体的本质只是给变量赋值,而初始化的这个步骤则交给了初始化列表,因此:
不管是否显式写在初始化列表里,都会在初始化列表里初始化 。
以下三种情况必须使用初始化列表进行初始化:
- 引用成员变量
- const成员变量
- 没有默认构造函数的自定义类型成员
前面两种成员是因为其本身特性而导致不得不在初始化的时候就得赋值,但第三种不太一样,需要单独拎出来讲一讲。
没有默认的构造函数即指类中至少有一个非全缺省的构造函数,这才满足类中没有默认构造函数的情况。我们知道对于自定义类型,编译器会自动调用目标类的默认构造函数,因此当前情况便会出现没有函数能够调用的情况。
使用初始化列表为该类的构造函数传入一个初始值,去调用该类的构造函数,便可完成该成员的初始化。
class B
{
public:
B(int b)
{
_b = b;
}
private:
int _b;
};
class A
{
public:
A()
:_a(5) //初始化列表初始化_a
,_bb(8)
{}
private:
int _a;
B _bb;
};
int main()
{
A a;
return 0;
}
先看以下代码:
class A
{
public:
A()
:a1(2)
,a2(a1)
{}
void print()
{
printf("%d %d", a1, a2);
}
private:
int a2;
int a1;
};
int main()
{
A a;
a.print();
return 0;
}
我们发现最后输出的结果竟然一个是 2 另一个是随机值。这是因为变量初始化的顺序是根据声明的顺序进行的。由于 a2 先声明,所以先初始化 a2 ,但由于此时 a1 还未被初始化,因此 a2 就被初始化成了随机值。
为了避免编译错误,尽量使用初始化列表初始化。
我们都知道定义对象时还有这样的一种写法,并知道他是通过调用构造函数来进行初始化的。但真的是这样吗?其实在这个过程之中会产生一个隐式类型转换,用 1 来构建一个 A 的临时变量,之后用这个变量对 a 进行拷贝构造。之后编译器对这个过程进行了优化,才变成只出现调用一次构造函数。
A a = 1;
如何证明这其中生成了临时变量呢?看下图,为什么使用普通的引用就会报错,而使用常引用没有出现错误了呢?
总所周知,临时变量具有常性,若用一个常变量给一个普通变量赋值,则会出现权限放大的情况,这时只要定义一个常变量便不会出现权限放大的情况。由此便可以证明我们上面所说的隐式类型转换是存在的。
若我们不想要让这种类型转换发生,就需要引入这个关键字 explict 。
在构造函数前增加这个关键字,上面的代码便无法通过编译。其针对的不仅仅是单参数的构造函数。
而多参数的构造函数需要这样使用才能够初始化(需在C++11的环境下) 。
class A
{
public:
A(int a,int b)
:_a(a)
,_b(b)
{}
private:
int _a;
int _b;
};
int main()
{
A a = { 1,1 };
return 0;
}
且这也同样能用 explicit 禁止类型转换。
用 static 修饰的类成员称为类的静态成员,用 static 修饰的成员变量,被称为静态成员变量。
而静态成员变量不属于某个对象,而是属于整个类。因此静态成员变量一定要在类外进行初始化。
根据如下代码就完成了对静态成员变量的定义:
class A
{
public:
A()
{}
private:
static int _a; //声明静态成员变量
};
int A::_a = 5; //初始化静态成员变量
int main()
{
A a;
return 0;
}
静态成员函数的本质还是静态成员,同样,他也是所有类中共享的,一般用来访问静态成员变量。
class A
{
public:
A()
{}
static int get_a() //静态成员函数
{
return _a; //返回静态成员变量
}
private:
static int _a; //声明静态成员变量
};
int A::_a = 5; //初始化静态成员变量
int main()
{
A a;
cout << a.get_a();
return 0;
}
认识了静态成员后,我们就可以根据他们的这个性质实现一个类的小功能。我们能够实现统计当前类的实例化个数。
class A
{
public:
A()
{
_cnt++; //实例化则增加1计数
}
A(const A& a)
{
_cnt++; //实例化则增加1计数
}
~A()
{
_cnt--; //析构则减少1计数
}
static int get_memnum() //使用静态成员函数获取静态成员变量
{
return _cnt;
}
private:
static int _cnt; //声明静态成员变量
};
int A::_cnt = 0; //初始化静态成员变量为0
int main()
{
A a;
cout << a.get_memnum();
return 0;
}
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
其中分作友元函数和友元类。
当我们对<<运算符进行重载时,会发现若在类中定义,则第一个操作数便固定为this指针,便与我们平时使用流提取的写法相反。
class A
{
public:
A(int a,int b)
:_a(a)
,_b(b)
{}
ostream& operator<<(ostream& _cout)
{
_cout << _a << _b << endl;
return _cout;
}
private:
int _a;
int _b;
};
int main()
{
A a = { 1,3 };
a << cout;
return 0;
}
因此,这个重载的函数便不能写在类中,但写在类外又访问不到类中的私有成员变量。因此便可以将这个函数定义成友元。就像是一种信任关系,对这个函数去掉访问限定符的限制。这样一来,外部的函数便可以访问到内部的的保护成员和私有成员。
使用时需要在类的内部声明,声明时需要加 friend 关键字。
class A
{
public:
A(int a,int b)
:_a(a)
,_b(b)
{}
friend ostream& operator<<(ostream& _cout, const A& a);
private:
int _a;
int _b;
};
ostream& operator<<(ostream& _cout, const A& a)
{
_cout << a._a << a._b << endl;
return _cout;
}
int main()
{
A a = { 1,3 };
cout << a;
return 0;
}
[注意]
- 友元函数可访问类的私有和保护成员,但不是类的成员函数。
- 友元函数不能用 const 修饰。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 一个函数可以是多个类的友元函数。
- 友元函数的调用与普通函数的调用原理相同。
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性。如下面的代码,在B中声明A为B的友元,在A中能够便能够访问B的私有成员,但B却无法访问A中的私有成员。
友元关系不能传递,如果C是B的友元, B是A的友元,但C不是A的友元。友元关系也不能继承。
class B
{
public:
friend class A; //声明A为B的友元
B(int b)
:_b(b)
{}
private:
int _b;
};
class A
{
public:
A(int a)
:_a(a)
,b(2)
{}
void print()
{
cout << b._b << endl; //在A中就能够访问b中的私有成员
}
private:
int _a;
B b;
};
int main()
{
A a = 1;
a.print();
return 0;
}
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,不能用外部类的对象去访问内部类的成员。即内部类是外部类的友元类,内部类可以访问外部类的所有成员,但外部类却无法访问内部类的私有成员。
[注意]
内部类受外部类类域的限制。
内部类可以定义在外部类的任意位置。
内部类可以直接访问外部类中的 static 成员,不需要外部类的对象/类名。
sizeof (外部类) = 外部类的大小,和内部类没有任何关系。
class A
{
public:
A(int a)
:_a(a)
{}
class B //B天生是A的友元
{
public:
B(int b)
:_b(b)
{}
void print(const A& a)
{
cout << a._a << " " << _b << endl; //b能够访问到a中的私有成员变量
}
private:
int _b;
};
private:
int _a;
};
int main()
{
A a = 1;
A::B b = 5;
b.print(a);
return 0;
}
当我们在某个时刻想要使用一个类,且仅使用一次以后便不再使用的情况下,可以试试匿名对象。
直接在类名后加上括号便可以使用,其生命周期只在定义的这一行内,这行结束便会自动销毁。
class A
{
public:
A(int a)
:_a(a)
{}
void print()
{
cout << _a << endl; //输出成员变量的值
}
private:
int _a;
};
int main()
{
A(3).print(); //使用匿名对象
return 0;
}
当我们调试到匿名对象的下一行语句时,该类的构造函数已经被调用了,即此时上一行的匿名对象已经被销毁了。
新的编译器在传参和传返回值时,会做一些优化,减少对对象的拷贝。当一行中满足以下条件便会进行优化。
构造 + 拷贝构造 -> 优化为直接构造
拷贝构造 + 拷贝构造 -> 1个拷贝构造
构造 + 构造 -> 1个直接构造
由于传引用传参本质就是传一个别名,因此无优化。
因为是否优化的判断区间为当前一行,因此正确使用匿名对象有利于编译器的优化。
对象返回的总结
接收返回值对象,尽量以拷贝构造的方式接收,不要赋值接收。
函数中返回对象时,尽量返回匿名对象 。
函数传参的总结
尽量使用 const& 传参 。
今天,我们在原来类的基础上补充了一些细节,讲述了初始化列表,explicit 关键字,静态变量,友元,匿名对象,以及编译器在拷贝对象时的优化。每个知识点的内容都比较零散,因此要在自己理解后再用代码进行实现,才能真正的掌握。掌握编译器的规律来优化代码的效率,才能对语言的理解更近一步。
好了,今天类与对象下半部分讲解到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。