初始化列表
在前面我们学习了构造函数,得知了构造函数会对类的成员变量进行初始化。 除此之外,C++又提供出了另一种初始化成员变量的方式,也就是初始化列表。 注意:1.只有构造函数才有初始化列表这一功能,因为只有构造函数才是完成初始化工作的。 2.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关 具体语法: 我们以日期类Date为例
这时通过类创建对象的时候,初始化列表就会自动完成成员变量的初始化。
但是明明C++已经有了构造函数这一初始化方式了,为何又要引入初始化列表呢?C++这一做法是否是脱裤子放屁? 原来初始化列表是为了满足初始化过程中的其他场景,而这种场景是构造函数无法做到的。 在此之前,我们先回忆下C++中类成员变量的定义以及声明。 日期类Date中有三个成员变量,分别是年 m_year,月 m_month,日 m_day。 这里我们需要区别一个概念,这里的三个成员变量究竟是定义还是声明。通过前面的学习,我们知道这里的变量只是一种声明。
那么究竟成员变量是在什么时候才被定义呢?
当我们创建日期类的对象的时候,整个对象被定义出来了,类的数据成员这时也会被定义出来,但是其在定义时是被赋予随机值的,接着再被构造函数进行初始化 这就像下面的场景: 这里的变量i是先被定义出来,这时i中存储的是系统分配的随机值,接着再给i初始化为10。 也就是说:构造函数不是在成员变量定义的时候就对其进行初始化,而是在成员变量定义出以后,再对其进行初始化
这时就会出现三种构造函数无法对成员变量进行初始化的场景
1.const类型成员变量: const类型的成员变量必须在定义的时候就进行初始化。否则就会产生编译错误。 就如下图所示。在构造函数中对const类型的m_year进行初始化,这时系统产生报错。产生这一报错的原因也正说明了构造函数不会在成员变量定义的时候就对其进行初始化。
正是因为这一场景,C++才引入了初始化列表。 接着我们通过初始化列表的方式去对m_year进行初始化,看看能否成功。 编译成功通过! 这里我们可以大胆进行推测:初始化列表是在成员变量进行定义的同时进行初始化:如 const int m_year=10; 事实也确实是如此。初始化列表的确是在成员变量定义的时候就对其完成了初始化操作。 初始化列表究竟是何许人也?
这里为了方便记忆,我们可以将初始化操作看作是附加在构造函数上的一块地方。成员变量在定义的时候初始化列表会对其进行初始化工作。
接着我们继续学习初始化列表的第二种应用场景 2.引用做成员变量 通过构造函数依旧不能完成这种初始化功能。 继续使用初始化列表就可以正常运行了。 最后一种初始化列表的应用场景 3.没有默认构造函数的自定义类型成员
通过初始化列表调用其构造函数就可以解决。 这里类AA没有无参构造函数,因此构造函数无法对其进行初始化,因此我们通过初始化列表调用了其有参构造函数。
总结:C++建议我们对于变量的初始化尽量使用初始化列表的方式,一来满足了多种情况的需求,二来也提高了程序的运行速度。
如果我们不写初始化列表,是不是就没有初始化列表了
其实不是,通过之前的学习,我们知道,系统默认的构造函数对于自定义类型会调用其构造函数,其实这一功能也是初始化列表实现的。
class AA
{
public:
AA()
{
cout << "类AA的无参构造函数" << endl;
}
};
class Date
{
public:
AA aa;
};
int main()
{
AA a;
Date d1;
}
面试题练习
看一下这道题该选什么? A:输出 1 1 B:程序崩溃 C:编译不通过 D:输出1 随机值
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();
}
这里我想大家可能都被坑了,这里的知识点涉及到了初始化列表的顺序,在前面我们说:初始化列表的顺序和变量的声明顺序有关,与初始化列表中的书写顺序无关,在该类中,_a2变量先声明,所以先执行_a2(_a1),这时a1还是随机值,所以_a2也被赋予随机值。接着对_a1进行初始化,将其初始化为1。
正确答案是D:
explicit关键字
对于下图的日期类,第一种创建对象的方式我们已经很熟悉了,这种创建对象的方式是调用其有参构造函数。 但是对于第二种方式:这里为什么能成立呢,两者的类型明显是不同的? 在揭晓答案之前,我们需要先知道强制类型转换和隐式类型转换的区别:
隐式类型转换: 隐式类型转换是一种数据类型相似的转换,如浮点型的double转换成int类型的整数。这里两者的类型十分相似。对于类型区别较大时,就不能发生隐式类型转换,如: 这里b的类型时 int*,c的类型是 int,两者一个是指针,一个是整形,区别较大,因此无法发生隐式类型转换。 强制类型转换: 正如其名,强制类型转换有点类似强买强卖,也就是我强制你转换成这种类型。 上面的 int* 转换成 int 通过强制类型转换就能转换成功。 接着我们返回来看日期类第二种创建对象的方式,这里其实就是发生了隐式类型转换,通过学习我们知道隐式类型转换会产生临时变量,这里的30会先构造出成一个Date类型的临时对象,接着再用这个对象去拷贝构造d2。 --------------------------------------我是分割线------------------------------------------
但是对于编译器而言,vs下对其进行了优化,这种连续的两次构造被编译器优化成了一次,也就是说这里其实只会发生一次有参构造。 我们可以通过运行代码进行验证: 这里第一次是d1的有参构造,而第二次则只运行了有参构造,说明这里的拷贝构造被编译器进行了优化。 这时引入explicit,explicit用于修饰构造函数。 格式如下: 作用:被explicit修饰以后的构造函数无法发生隐式类型转换 将前面的日期类Date的有参构造函数+了explicit以后,这里的隐式类型就无法发生了,30无法通过隐式类型转换构造出一个临时对象。这也就是explicit的作用。
static成员
静态成员变量不能给缺省值,因为缺省值给构造函数使用的,而构造函数无法初始化静态成员变量。
static修饰的成员变量,称之为静态成员变量,静态成员不属于某一个对象,其作用域同样也是在类中,但是生命周期是全局的。 静态成员变量特征: 静态成员变量必须类内声明,类外初始化
- 静态成员为所有类对象所共享,不属于某个对象
- 静态成员变量必须在类外定义,定义时不添加static关键字
如日期类:
但是为什么静态成员变量不能在类内进行初始化呢? 因为静态成员变量是所有对象所共有的,假设能够在构造函数中进行初始化,就导致每次创建对象的时候都会初始化一次静态成员变量,这种每次都重新初始化的操作也失去了定义静态成员变量的定义,因此C++规定静态成员变量必须类内声明,类外初始化。 3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问 如: 需要注意,这里通过对象访问静态成员的意思并不是说去对象里找这个静态成员,静态成员不属于某个对象,而是说这个静态成员变量属于这个类。
静态成员函数
在成员函数前加上static关键字的函数就称作静态成员函数。 静态成员函数的特征: 1.静态成员函数不属于某个对象,同时其不具有this指针。 2.静态成员函数只能访问静态成员变量 关于静态成员函数只能访问静态成员变量? 因为静态成员函数内不含有this指针,假设其能访问非静态成员变量,在对象调用这个静态成员函数的时候,静态成员函数无法识别是哪个对象调用了他,因此也无法确认将这个成员变量赋值给哪个对象。 3.静态成员函数和静态成员变量一样,都具有两种访问方式 1.类名::静态成员函数名() 2.对象.静态成员函数名()(第二种方式同样也不是说去对象里找,成员函数不属于对象!) 如
C++11初始化新玩法
在一些类中,我们常常能看到这样的操作: 这其实是C++打的一种补丁,这里的m_year=2020,m_day=1,并不是说这两个变量定义的时候被初始化为2020和1,而是有点类似函数参数的缺省值。 如果构造函数阶段没有对这两个变量进行初始化,这两个变量就会被初始化为缺省值。 需要注意的是: 静态成员变量没有缺省值! 因为缺省值是给构造函数使用的,只有能够在构造函数中初始化成员变量才能赋予缺省值,而静态成员变量不能在类内进行初始化,所以自然也就不能赋予缺省值。
C++的这个补丁导致多了很多骚操作: 如: 当然这里还有一种非常灵活的应用场景,那就是给成员对象赋予缺省值
内部类
概念:如果一个类定义在另一个类内部,该类就属于内部类。在C++中我们不常使用内部类,有个大致了解即可。
内部类的特征:
??
(
1
)
(1)
(1) 内部类天生就是其外部类的友元类,但是这种关系依旧是单向的,外部类并不是内部类的友元类。 ??
(
2
)
(2)
(2)内部类和定义在全局的类功能基本一致,只不过其受到外部类类域的限制 ??
(
3
)
(3)
(3)内部类并不属于外部类 对于特征一和特征二我们逐一验证:
内部类可以随意访问外部类,这里内部类student可以访问person外部类的私有数据。
class person
{
public:
person(int a, int b)
:m_a(a), m_b(b)
{};
class student {
public:
student(int c)
:m_c(c)
{};
void showperson(person&p)
{
cout << p.m_a << endl;
}
private:
int m_c;
};
private:
int m_a;
int m_b;
};
int main()
{
person p1(10, 20);
person::student s1(30);
s1.showperson(p1);
}
外部类不可随意访问内部类的数据
class person
{
public:
person(int a, int b)
:m_a(a), m_b(b)
{};
class student {
public:
student(int c)
:m_c(c)
{};
void showperson(person&p)
{
cout << p.m_a << endl;
}
private:
int m_c;
};
void showstudent(student s1)
{
cout << s1.m_c << endl;
}
private:
int m_a;
int m_b;
};
int main()
{
person p1(10, 20);
person::student s1(30);
p1.showstudent(s1);
}
特征三:内部类不属于外部类
class person
{
public:
person(int a, int b)
:m_a(a), m_b(b)
{};
class student {
public:
student(int c)
:m_c(c)
{};
void showperson(person&p)
{
cout << p.m_a << endl;
}
private:
int m_c;
};
private:
int m_a;
int m_b;
};
int main()
{
cout<<"外部类person的大小为"<< sizeof(person) << endl;
}
const修饰对象
??常对象
const修饰以后的对象又称作常对象。 对于常对象该如何理解? 常对象的数据不能修改。 如下面的person类
class person {
public:
int m_a;
};
int main()
{
const person p1;
p1.m_a = 50;
}
并且常对象只能调用常函数,因为常函数不会对成员变量的值进行修改。
这里有一种情况容易迷惑人: 如果这个类的成员变量是一个指针呢? 如下面的代码: 这时const修饰的是p指针还是p指针指向的那块空间呢?
class person {
public:
int *p;
};
int main()
{
const person p1;
}
const修饰对象的本质究竟是什么:我们需要认真思考一下. const修饰对象的本质:对象的数据不能被修改 对于一个对象而言,其存储了很多个成员变量,成员变量其实也就是一块块空间。const修饰对象,其实本质也是对这一块块空间进行了修饰,空间里存储的数据不能修改。
那么指针的空间里存储的又是什么呢? 指针空间里存储的是另一块空间的地址,所以常对象里的指针指向不能修改。 这里可能有点绕:我们画图进行理解。
|