· CSDN的uu们,大家好。这里是C++入门的第十一讲。
· 座右铭:前路坎坷,披荆斩棘,扶摇直上。
· 博客主页: @姬如祎
· 收录专栏:C++专题
目录
1. 初始化列表
1.1 引入
还记得我们之前在学习类和对象的时候是怎么给类的成员变量 "初始化" 的吗?我们是通过构造函数在函数体内部给成员变量赋值的。为什么说是赋值而不是说初始化呢?因为成员变量的初始化就不是在函数体内部完成的!而是一个叫初始化列表的东西中完成的!
我们定义了一个类A,在构造函数中我们对成员变量进行了多次赋值!以此来证明构造函数的函数体不是对成员变量进行初始化的地方。
class A
{
public:
A(int x = 0, int y = 0)
{
_x = x;
_x = 5;
_y = y;
}
private:
int _x;
int _y;
};
int main()
{
A a(2, 3);
return 0;
}
1.2 初始化列表
我们先来看看初始化列表的语法吧:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
比如下面的代码:
class A
{
public:
A(int x = 0, int y = 0)
:_x(x)
,_y(y + x)
{}
private:
int _x;
int _y;
};
在括号里面也是可以写表达式的哦!
我们的C++祖师爷为什么要设计出初始化列表呢?那肯定是因为函数体内的赋值无法对所有类型的成员变量进行赋值。我们来看下面的代码:
我们看到类的成员变量如果有const 修饰的变量或者引用,在构造函数的函数体内部赋值就会直接报错了!根据报错提示,我们也能够隐隐猜到构造函数的函数体不是成员变量初始化的地方!
对于const修饰的变量 和引用都有一个共同的特性:在定义的时候必须初始化!我们又知道了初始化列表才是成员变量真正定义的地方!所以const 修饰的变量和引用就必须在初始化列表中初始化:这样的代码就没有什么问题啦!
class A
{
public:
A(int x = 0, int y = 0)
:_x(x)
,_y(y)
{}
private:
const int _x;
int& _y;
};
1.3 初始化列表的注意事项
我们来看看初始化列表的注意事项:
1:每个成员变量在初始化列表中最多出现一次。
2:类中包含以下成员,必须放在初始化列表位置进行初始化: 引用成员变量 const成员变量 自定义类型成员,且该类没有默认构造函数时。
我们来看第一点:每个成员变量在初始化列表中最多出现一次。我们现在知道了初始化列表是成员变量定义的地方。如果一个成员变量在初始化列表中出现两次,那么这个变量不就是被定义了两次嘛!这当然是不被允许的!
第二点: 引用变量 与 const 修饰的成员变量为什么只能在初始化列表中初始化你应该已经明白了!那么没有默认构造函数的自定义类型为什么也必须要在初始化列表中初始化呢?
你还记得默认构造函数的定义嘛:默认构造函数就是不用传参就可以直接调用的构造函数。
来看下面的代码:我们定义了类A,自己写了带参的构造函数,那么编译器则不会为类A提供默认构造函数。我们又定义了类B,成员变量是自定义类型。我们尝试在B的构造函数中给成员变量赋值。
class A
{
public:
A(int x, int y)
:_x(x)
,_y(y)
{}
private:
int _x;
int _y;
};
class B
{
public:
B(A a)
{
_a = a;
}
private:
A _a;
};
我们编译器直接报错了:类 A 不存在默认构造函数!为什么会报这个错误呢?我们知道初始化列表是成员变量定义的地方,那么类B的成员变量会在初始化列表中有类似这样的定义:A _a。即在定义的过程中他会去调用A的默认构造函数,但是因为 A 没有默认构造函数自然就会报错了!
也就是说:对于自定义类型的成员变量,如果该成员变量没有显示在初始化列表中初始化, 就会调用该自定义类型的默认构造函数。
那如果我们在初始化列表中显示初始化,就会调用对应的构造函数对自定义类型的成员变量进行初始化。
1.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();
return 0;
}
A:输出 1 1
B:程序崩溃
C:编译不通过
D:输出 1 随机值
答案是D哦!初始化列表的初始化顺序并不是按照初始化列表中的书写顺序来的!而是按照成员变量的声明顺序来的!_a2先声明,那么就会先初始化_a2,因此_a2用还未初始化的_a1初始化就是随机值。所以说为了避免出现这样的错误,初始化列表的书写顺序最好与成员变量的声明顺序一致。
最后一点:还记得我们在类中给成员变量缺省值的操作嘛!那个缺省值其实就是给初始化列表初始化的!
2. 同一表达式连续构造的优化
大家觉得下面的代码有没有问题呢?
class A
{
public:
A(int x)
:_x(x)
{}
A(const A& a)
{
_x = a._x;
}
private:
int _x;
};
int main()
{
A a = 2;
return 0;
}
你变异之后发现变异通过并且运行良好!这是为啥呢?整形可以直接赋值给自定义类型?其实不是的啦!这其中发生了隐式类型转化,C++98的语法:单参数的构造函数支持隐士类型转换。所以说 A a = 2;其实是用 2 这个整形调用了A的构造函数,构造了一个A的对象,然后在通过拷贝构造来构造 a 这个对象!
如果你不能理解我们可以拿一个内置类型来举例:
我们有一个整形变量直接将他赋值给一个double变量,看看编译器是怎么做的。
这个道理和 A a = 2;是一样的撒!所以在这一个表达式中调用了构造函数和拷贝构造函数!于是编译器看不下去了!他将这个表达式优化成了直接调用构造函数来构造 a 这个对象。编译器是容忍不了一个表达式出现两个构造函数的调用的!
那你可能不相信我的解释,你说:有没有可能并没有一个临时对象被构造出来,而本来就是用 2 直接构造a这个对象。那我们来看看这个代码:A& a = 2; 报错:非常量引用的初始值必须为左值,也就是说,在给 a 赋值的时候,a引用的是一个右值,自然就报错了。
可是我们如果用的是const的引用呢?发现就没有报错了!我们知道临时对象具有常性,加上const才能对其进行引用。但是这也并不能说明通过2构造了一个A的对象哇!你可能会说a就是引用的2这个右值嘛!
OK,那很巧,C++中有一个关键字能够废除这种隐式类型转换:explicit。如果真如你所有说 a 能够引用 2 这个右值,那么废除 这种隐式类型转换也不会报错吧!
很遗憾他报错了!那么说明这种隐式类型转换是真实存在的!
3. 静态成员变量与静态成员函数
假设我们现在有这样一个需求,需要你统计出一个类实例化出了多少个对象!你激动的说到,这个我会呀!对象实例化不是都要调用构造函数吗,对象销毁不是都要调用析构函数嘛,那么我只要维护一个全局变量count,然后再构造函数里对count++,析构函数里面对count--,不就大功告成了吗?是的你很厉害,也很聪明!
int _count;
class A
{
public:
A(int x = 0)
:_x(x)
{
_count++;
}
A(const A& a)
{
_x = a._x;
_count++;
}
~A()
{
_count--;
}
private:
int _x;
};
可是有一天,来了一个程序员,看到了这个count,这是干什么用的?一下就给你把count给改了!显然不行!
原因还是在与全局变量无法得到有效的封转保护!于是我们的静态成员变量他来了!静态成员变量可以理解为全局变量的封转,使得变量更好被维护。静态成员变量应该怎么初始化呢?很简单:
class A
{
public:
A(int x = 0)
:_x(x)
{
_count++;
}
A(const A& a)
{
_x = a._x;
_count++;
}
~A()
{
_count--;
}
private:
int _x;
static int _count;
};
静态成员变量属于一个类,不单独属于任何一个对象!静态成员变量是所有对象共享的。访问静态变量的时候只要突破类域和访问权限就可以直接访问。可以通过类域,也可以通过对象访问。
但是吧静态成员变量设置为共有又有一点不太好!因此我们需要提供一个函数来访问静态成员变量。
我们提供了一个普通的成员函数,发现必须需要有对象才能访问这个函数!但是我们的需求是随时访问这个函数,因此我们将这个函数变为静态函数!静态函数没有this指针,不能访问非静态的成员变量(因为函数访问普通的成员变量就是通过this指针访问的嘛)。通过对象或者类名都可以直接访问。前提是有权限哦!
4. 内部类
内部类 概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的。
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3. sizeof(外部类) = 外部类,和内部类没有任何关系。
内部类C++用的不多,Java喜欢用内部类。内部类供大家了解一下吧!