系列文章目录
一、初始化列表
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;
};
3、初始化列的特性
(1)初始化列表是用来定义初始化所有成员变量的
成员变量即使没有在初始化列表中显示,也会在初始化列表中定义初始化,只不过被初始化为随机值(也可能是缺省值)
如下述代码中,_year在初始化列表中定义并初始化为year,_month、_day虽未显示,也会经过初始化列表定义初始化为随机值。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
{}
private:
int _year;
int _month;
int _day;
};
在C++学习(3)——类和对象(中)六大默认成员函数中我们曾介绍,c++为了弥补编译器自动生成的构造函数对内置类型不处理,而导致内置类型被初始化为随机值,引入了在类内声明变量时给缺省值。语法如下:
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
{}
private:
int _year=0;
int _month=0;
int _day;
};
其实所给的缺省值就是在初始化列表中使用的。例如上述代码中,初始化列表中只显示_year。所以_year被初始化为year,**而未显示的_month此时使用缺省值初始化,而不是随机值。**未显示且没有缺省值的_day被初始化为随机值。
注意:优先使用初始化列表中所给参数进行初始化。如_year虽然有缺省值0,但仍被初始化为year。
(2)初始化列表对内置类型成员变量和自定义类型成员变量做不同处理
对于内置类型成员变量(以上述代码为例):
将显示调用的成员定义初始化为参数值,如_year被定义初始化为year;
将未显示调用但拥有缺省值的成员初始化为缺省值,如_month被定义初始化为0;
将未显示调用且无缺省值的成员初始化为随机值,如_day被定义初始化为随机值;
含有自定义类型的类:
//无默认构造函数
class A
{
public:
A(int testa)
:_testa(testa)
{
}
private:
int _testa;
};
//有默认构造函数
class B
{
public:
B(int testb = 0)
:_testb(testb)
{
}
private:
int _testb;
};
class Date
{
public:
Date(int test)
:_test(test),_c(10)
{}
private:
int _test;
A _a;
B _b;
A _c;
};
对于自定义类型成员变量(以上述代码为例):
如果没有显式调用,初始化列表会自动调用该自定义类型对象的默认构造函数,如果有默认构造函数就执行,如果没有默认构造函数将报错。如_a没有默认构造会直接报错,而_b会调用自己的默认构造进行初始化。
如果显示调用,初始化列表会将参数传给该自定义类型对象的构造函数。如_c会将10传给自己的构造函数进行初始化。
重点在于有无默认构造函数
(3)默认构造函数
根据前面博客所述,默认构造函数即不需要传参的构造函数,主要包括三种:
1.无参构造函数
2.有参但全缺省构造函数
3.没有显示定义构造函数时,编译器自行生成的构造函数
(4)必须在初始化列表中显式调用的成员变量
前文所言,初始化列表实际是定义并初始化成员变量。而有的成员变量只能在定义时初始化,无法使用构造函数进行初始化(也可以称之为赋值)
主要包括三种成员变量:
1.const修饰的常变量(常变量只有定义时可以初始化,其他时候不可赋值)
2.引用型变量(原因:引用一旦指向某一实体便不可以更改)
3.自定义类型成员且没有默认构造(原因:没有默认构造函数的自定义类型成员,如果不在初始化列表中显示调用,会报错)
没有这三种成员变量,我们也要尽量使用初始化列表。
因为即使我们不显示写初始化列表,编译器在执行构造函数时也是先使用初始化列表创建并初始化成员变量,再使用构造函数完成赋值。
(5) 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后无关
如下述代码的输出是: 随机值 1
#include<iostream>
using namespace std;
class test
{
public:
test(int b)
:_b(b), _a(_b)
{
}
void Show()
{
cout << _a << ' ' << _b << endl;
}
private:
int _a;
int _b;
};
int main()
{
test t(1);
t.Show();
return 0;
}
运行结果:
原因:类的private中是成员变量的声明,_a声明在_b前。所以在初始化列表中,我们先用_b给_a初始化,再用b给_b初始化。
二、构造的隐式类型转换和explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
如下述代码:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month = 1, int day = 1)
:_year(year), _month(month), _day(day)
{
}
//拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Show() const
{
cout << _year << ' ' << _month << ' ' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1 = 2022;
d1.Show();
return 0;
}
在上述代码中,编译器先根据2022构造临时变量,再利用该临时变量进行拷贝构造。即构造加拷贝构造
在较老的编译器中,编译器按照构造加拷贝构造的方式执行该语法。而在新编译器中,该语法直接被简化为构造
注意在C++学习(1)——命名空间、函数重载、缺省参数、引用、内联函数中我们已经知道临时变量具有常性,因此对于引用和指针注意保持权限的正确。此时我们的Show函数也要调整为const修饰
下述代码就展示了如何正确使用引用:
int main()
{
Date d1 = 2022;
d1.Show();
const Date d2 = 2019;
d2.Show();
return 0;
}
而如果我们在构造函数前使用explicit关键字进行修饰,那么该函数就会禁止类型转换。这种构建临时变量的行为就不被支持。
此时下述代码将报错!
class Date
{
public:
explicit Date(int year = 1999, int month = 1, int day = 1)
:_year(year), _month(month), _day(day)
{
}
//拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Show()const
{
cout << _year << ' ' << _month << ' ' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
其实在C++11中该语法得到扩展,不再仅适用于单个参数或者除第一个参数无默认值其余均有默认值的构造函数
三、static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用
static修饰的成员函数,称之为静态成员函数。
在上面对初始化列表的学习,我们知道所有成员变量都是在初始化列表中定义初始化。然而,static修饰的静态成员变量是在类外定义的,它并不经过初始化列表处理。
在类外定义时,为了与普通的全局变量区分,我们在变量名前加上类域限定
具体形式如下:
class test
{
public:
test()
:a(0), b(0)
{
}
private:
int a;
int b;
static int c;
};
int test::c = 0;
特性:
(1)静态成员为同类型的所有对象共享。
它不存储于对象内,而是存于静态区。
因此类似与C++学习(2)——类和对象(上)中对成员函数和this指针的操作。即使不存在该类型的对象,我们仍然可以访问静态成员。
如下述代码的输出结果为0。即空指针仍可访问位于静态区的静态成员
class test
{
public:
test()
:a(0), b(0)
{
}
void Show_c()
{
cout << c << endl;
}
private:
int a;
int b;
static int c;
};
int test::c = 0;
int main()
{
test* t = nullptr;
t->Show_c();
return 0;
}
(2)静态成员没有this指针
静态成员不含有隐藏的this指针,所以它无法访问非静态成员
(3)static改变成员的生命周期,但成员的作用域仍受限制
静态成员的作用域仍然受到类域和类内三大限定访问符的限定。
例如位于private内的静态成员仍无法从类外访问(下述代码报错)
class test
{
public:
test()
:a(0), b(0)
{
}
private:
int a;
int b;
static int c;
};
int test::c = 0;
int main()
{
test t;
cout<<t.c<<endl;
return 0;
}
(4)类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
我们知道常规有三种位于不同地方的对象:
1.使用static修饰的构造函数创建对象(static test a),该对象位于静态区(生命周期全局)
2.动态开辟的对象,(tset* pb=new test),该对象位于堆区(生命周期全局,除非主动释放)
3.正常调用构造函数创建的对象,(test c),该对象位于栈区
例如我们要求只能在栈区创建对象,也就是禁止使用前两种方法创建对象。解决思路是:先显示定义一个构造函数,但是使用private限制该构造函数,使得上述三种构建方法均无效(因为类外无法访问私有成员)。然后在public中定义一个GetObj函数,在该函数内调用private限制的构造函数来创建对象,最后传值返回创建出来的对象(这样就限制了创建构造函数的方法只有一种)。
难点:要想在主函数内使用GetObj函数来创建一个对象,需要一个已存在的对象来调用该函数,这就形成了“先有蛋还是先有鸡”的谬论。
此时,静态成员可以由类名直接调用的优势就展现出来了。我们可以使用static修饰GetObj函数,因为静态函数存在于静态区,可以直接通过类名访问该成员,而无需使用已存在的对象。
代码如下:
class test
{
public:
test static GetObj(int a)
{
test ret(a);
return ret;
}
private:
test(int a = 10)
:_a(a)
{
}
private:
int _a;
};
int main()
{
test a = test::GetObj(1);
return 0;
}
四、友元
1.友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数(不含this指针),不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
以前面博客所实现的日期类流插入和流提取重载为例:
注意:友元函数要在类内声明,函数原型前加friend关键字;友元函数要在类外定义,定义时无须加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.友元函数可访问类的私有和保护成员,但不是类的成员函数
2.友元函数不能用const修饰(因为const修饰this指针,而友元函数是普通函数不含this指针)
3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4.一个函数可以是多个类的友元函数
5.友元函数的调用与普通函数的调用原理相同
2.友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
下述代码展现友元类与外类的关系:
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
特性:
1.友元关系是单向的,不具有交换性
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接
访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
2.友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
3.友元关系不能继承
五、内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的任何地方,但内部类的访问受到外部类类域以及外部类访问限定符的限制
受到外部类域的限定:
class A
{
public:
A(int a=10)
:_a(a)
{}
//内部类
class B
{
public:
B(int b=0)
:_b(b)
{}
private:
int _b;
};
private:
int _a;
};
int main()
{
//直接访问B,错误
//B b;
//通过外部类类域访问
A::B b;
return 0;
}
受到外部类访问限定符的限制:
class A
{
public:
A(int a=10)
:_a(a)
{}
private:
int _a;
//内部类
class B
{
public:
B(int b = 0)
:_b(b)
{}
private:
int _b;
};
};
int main()
{
//报错
//此时无法在A类外访问B,B是A类的私有类
A::B b;
return 0;
}
- 内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
class A
{
public:
A(int a=10)
:_a(a)
{}
//内部类
class B
{
public:
void Show(const A& a)
{
cout << a._a << ' ' << num << endl;
}
};
private:
//静态成员变量
static int num;
int _a;
};
int A::num = 0;
int main()
{
//B是A的友元,并且B可以直接访问A的静态成员
A a;
A::B b;
b.Show(a);
return 0;
}
- sizeof(外部类)=外部类,和内部类没有任何关系。
输出结果是4而不是8
class A
{
public:
A(int a=10)
:_a(a)
{}
//内部类
class B
{
int _b;
};
private:
int _a;
};
int main()
{
cout << sizeof(A) << endl;
return 0;
}
六、匿名对象
匿名对象的生命周期只有当前一行。
class test
{
public:
test(int a)
:_a(a)
{
cout << "构造" << endl;
}
~test()
{
cout << "析构" << endl;
}
private:
int _a = 10;
};
int main()
{
test(0);
return 0;
}
运行结果: