关于类和对象依旧有许多难点,这篇博客将会讲解关于类的构造函数的初始化列表,静态成员,友元,内部类,以及匿名对象等一些比较复杂的东西。
我们之前就已经学过类和对象的构造函数,但是实际上那并不算是对象的初始化,其实算是给成员变量赋值。
而我们用c语言和c++时都知道,变量初始化只能初始化一次,而赋值可以在函数内赋值无数次。
既然说构造函数只能算是给成员变量赋值,那么怎样才能证明?
根据之前学的知识,我们都知道用const修饰的对象都只能在初始化的时候确定值;
那么我们用默认构造函数初始化const成员变量不就可以证明了吗?
我们直接看看:
发现用默认构造函数实际上不能初始化用const修饰的变量。
这就变相的证明了默认构造函数内部实际上不是初始化,而是赋值。
那么类的对象在哪里才算是初始化呢?
这就轮到初始化列表出场了。
不过在了解初始化列表之前需要先了解下初始化列表的使用场景。
初始化列表使用场景:
1.用来初始化const成员变量
2.用来初始化无默认构造函数的自定义类型成员变量
3.用来初始化引用成员变量
所谓初始化列表,实际上就是在构造函数下,以一个冒号为开始,成员之间以逗号隔开的列表,每一个成员变量后面用括号跟上赋的值。
初始化列表的使用方式:
我们可以看到,确实是成功的初始化了成员变量。
而上面也说了,初始化列表不仅能够初始化const成员变量;
也可以修饰其它两种变量,其中,自定义类型的变量需要好好了解一下。
- class B {
- public :
- B(int _b)
- {
- b = _b;
- }
- private:
- int b;
- };
-
- class A {
- public:
-
- A()
- :a(10)
- ,_b(5)
- {
- }
- private:
- const int a;
- B _b;
- };
-
- int main()
- {
- A _a;
-
- return 0;
- }
通过这里我们可以发现,实际上初始化列表在初始化自定义类型的变量的时候;
会调用对应的构造函数,并且将括号里的数据用来初始化成员变量,而若是初始化列表没有显示初始化自定义类型的成员变量时,就会调用默认构造函数,无则报错;
此外,还有引用的成员变量需要使用初始化列表才能用。
这里我们可以看到,成功的初始化了rc这个引用类型的成员变量。
初始化列表的规则
对象的所有成员都会走一套初始化列表,面对自定义类型的变量,若是初始化列表没有显示初始化就会调用对应的默认构造函数,无则报错,而面对内置类型,有显示初始化就用显示的值,无则用随机值或者构造函数内部的数值。
此外,还有一个规则
初始化列表的初始化顺序是根据成员变量的声明顺序决定的。
这样我们发现,成员变量的声明顺序是 先a2后a1,而初始化列表则是先a1后a2,这就说明,初始化列表的初始化顺序由变量的声明顺序决定。
之前我们写过Date类,而Date类中的构造函数其实还有其他用处。
比如有一个构造函数只有一个参数,或者说只有第一个参数是没有缺省值的时候,会出现隐式类型转换。
我们先来看看代码。
我们发现,Date类型的对象d1居然能够直接用int类型的常量来初始化。
实际上这里涉及到类的隐式类型转换。
首先编译器会将用构造函数创建一个Date类型的中间变量,再用拷贝构造将中间变量的值给d1。
当然,实际上这只是便于理解的说法,现在的编译器都对这个过程进行了优化。
过程变成了直接用构造函数来将2022作为参数来构造d1。
下图可证:
这样实际上用处并不大,因为大部分的类的成员变量都不只有一个,比如Date类就有三个成员变量。
当然,我们也可以这样初始化一个变量,不过格式需要注意,如下:
就好像是初始化数组一样,这样也是可以的,但是这样会影响可读性,因此c++针对这个出现了一个关键字——explicit。
将explicit关键字放于构造函数之前,就可以禁止这样的隐式类型转换。
我们可以看到,对象实例化的地方出现了报错。
因此这样就可以避免这种错误出现。
在类中,有一种特殊的成员变量——static成员变量,这种被称为静态成员变量,那么这种变量有何妙用呢?接下来我们就来深入了解一下吧。
在之前我们学习过,static修饰的变量和普通变量不一样,普通变量都是在栈区,除非你是动态开辟的那么就在堆区,而static修饰的则在静态区。
而类中的成员若是用static修饰会怎样呢?
static修饰的成员特性
1.静态成员为所有对象共享,存放在静态区。
2.静态成员变量必须在类外定义,定义时不用加static关键字。
3.类的静态成员可以直接用类名::静态成员或者对象.静态成员来访问。
4.静态成员函数没有隐藏的this指针。
5.静态成员也受限定符限制。
了解了静态成员的特性后,我先来直接看看实现。
- #include
-
- using namespace std;
-
- class Date {
- private:
- int _year;
- int _month;
- int _day;
-
- static int time;
-
- public:
- Date(int year = 0, int month = 0, int day = 0)
- {
- _year = year;
- _month = month;
- _day = day;
- }
-
- static void Print1()
- {
- cout << "静态成员函数" << endl;
- // Print2();静态成员函数不能调用非静态成员函数,因为非静态成员函数必须在对象初始化后才能使用
- }
- void Print2()
- {
- cout << "非静态成员函数" << endl;
- Print1();//而非静态成员函数可以调用静态成员函数
- }
- };
-
- int Date::time = 1;
-
- int main()
- {
- Date::Print1();
- Date d1;
- cout << endl << endl;
- d1.Print2();
-
-
- return 0;
- }
我们发现,静态成员函数不能调用非静态成员函数,而非静态成员函数则 能调用静态成员函数。
此外,定义的time的static类型的变量只能在外面才能初始化,并且构造成员函数无法初始化静态成员变量,但是限定符依旧能够限制外部直接访问time这个静态成员变量。
友元
之前我们实现了Date类,但是我们还有几个方法没有实现。
比如用istream直接输入Date变量,而不是通过构造函数来创建。
当然,我们可以在类里面实现,但是这样我们就会有隐藏的this指针,我们的输入就会变成这样:
- Date d1;
- d1>>cin;
这样就和cin不同了,因此 为了可读性,我们只能在类外面实现这种函数。
但是我们Date类的成员变量又有访问限定符private来防止外部直接访问,那么我们该怎么办呢?
这里就轮到友元出场了。
- class Date {
- friend istream& operator>>(istream& in, Date& d);
- friend ostream& operator<<(ostream& out, const Date& d);
-
- private:
- int _year;
- int _month;
- int _day;
-
-
- public:
- static int time;
-
-
- Date(int year = 0, int month = 0, int day = 0)
- {
- _year = year;
- _month = month;
- _day = day;
- }
-
- static void Print1()
- {
- cout << "静态成员函数" << endl;
- // Print2();静态成员函数不能调用非静态成员函数,因为非静态成员函数必须在对象初始化后才能使用
- }
- void Print2()
- {
- cout << "非静态成员函数" << endl;
- Print1();//而非静态成员函数可以调用静态成员函数
- }
- };
-
- int Date::time = 1;
-
- istream& operator>>(istream& in, Date& d)
- {
- in >> d._year >> d._month >> d._day;
- return in;
- }
-
- ostream& operator<<(ostream& out, const Date& d)
- {
- out << d._year << '/' << d._month << '/' << d._day << endl;
- return out;
- }
-
- int main()
- {
- Date d;
- cin >> d;
- cout << d;
- return 0;
- }
看过实现后,再来看看友元函数的特性:
友元类的特性
1.友元函数可以直接访问类的私有和保护成员,但不是类的成员函数
2.友元函数不能用const修饰
3.友元函数可以在类的任意位置定义,不受类的访问限定符限制
4.一个函数可以是多个类的友元
5.友元函数的调用和普通函数的调用原理相同
友元可不止只有友元函数可以使用,实际上,也有友元类存在。
而友元类实际上差不多。
- class Time {
- friend class Date;
-
- private:
- int _hour;
- int _minte;
- public:
- Time()
- {
-
- }
- void Print()
- {
- cout << _hour << _minte << endl;
- }
- };
-
-
- class Date {
- private:
- int _year;
- int _month;
- int _day;
- Time t;
- public:
- Date()
- {
-
- }
- void Print()
- {
- cout << _year << _month << _day << t._hour << t._minte << endl;
- }
- };
我们在Date类创建了一个Time类型的成员变量t,我们就能够直接访问 t 的成员变量。
友元类的特性
1.友元关系是单向的,不具有交换性。2.友元关系不能传递3.友元类不能继承
当我们在一个类的内部定义了另一个类,那么这个类就是内部类。
而这个内部类实际上就是相当于外部类的友元函数,因此内部类可以直接使用外部类的变量。
但是这个友元只是单向的,外部类并不能用内部类的成员。
-
- class A {
- private:
- int a;
- static int k;
- public:
- class B {
- private:
-
- public :
-
- void Print(const A& d)
- {
- cout << d._a << endl;
- cout << k << endl;
- }
- };
-
- };
此外,内部类受外部类的访问限定符和类域限制。
内部类的特性:
1. 内部类可以定义在外部类的 public、protected、private 都是可以的。2. 注意内部类可以直接访问外部类中的 static成员,不需要外部类的对象/ 类名。3. sizeof( 外部类)= 外部类,和内部类没有任何关系。
在c++中有这样一种奇怪的对象,它没有名字,被称为匿名对象,我们直接看看如何实现。
- class A {
- private:
- int _a;
- public:
- A(int a)
- :_a(a)
- {
-
- }
- ~A()
- {
- cout << "这是一个析构函数" << endl;
- }
- };
-
- int main()
- {
- A a1(1);
-
- A(1);
- return 0;
- }
这个匿名对象十分神奇,它的生命周期只有这一行。
那么匿名对象有什么用呢?
比如,我们需要用类的方法返回一个值。
比如这样的类:
- class Solution {
- private:
- int n;
- public:
- Solution(int _n)
- :n(_n)
- {
-
- }
-
- int addFromTo(int from,int to)
- {
- int ret = 0;
- for (int i = from; i <= to; i++)
- {
- ret += i;
- }
- return ret;
- }
- };
-
- int main()
- {
- Solution d(5);
- cout << d.addFromTo(0, 100) << endl;
- return 0;
- }
我们为了计算从0到100的和而创造了一个对象。
但是这个对象可能之后都用不着了;
这时候就可以用匿名对象了。
在一些新一点的编译器中,对象的创建会被编译器优化。
我们先创建一个这样的类。
-
- class A {
- private:
- int _a;
- public:
- A(int a = 0)
- :_a(a)
- {
- cout << "这是一个构造函数" << endl;
- }
-
-
- ~A()
- {
- cout << "这是一个析构函数" << endl;
- }
-
- A(const A& a)
- {
- cout << "这是拷贝构造" << endl;
- }
- };
1.优化场景1
- int main()
- {
- A a = 1;
-
- return 0;
- }
在以前的编译器中,这里的对象应该是先构造一个中间变量再用中间变量拷贝构造出a。
顺序本来是这样的:
但是现在是这样了:
我们可以看到这里只有一次构造函数。
2.优化场景2
- int f(A a)
- {
-
- }
-
- int main()
- {
- f(A());
-
- return 0;
- }
像这里,如果是用匿名对象来传参,就会直接优化,
变成一次构造函数:
3.优化场景3
- A f2()
- {
- A a;
- return a;
- }
-
- int main()
- {
- A ret = f2();
-
-
- return 0;
- }
像这样的场景,本来应该是构造加拷贝构造再拷贝构造的。
而这里编译器会做一个优化,免去中间的拷贝构造,化为一个拷贝构造。
这就是编译器的优化。
以上就是类的剩余知识了,谢谢大家。