回顾一下之前的常规初始化方式
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
{
//赋值的方式进行“初始化”
_year = year;
_month = month;
_day = day;
}
private:
int _year;//此处仅仅是变量的声明,而非定义
int _month;
int _day;
};
这种“初始化”的方式严格来讲并不算初始化,只是使用了赋值的形式对已经初始化的变量进行赋值。因此变量在被赋值之前应该已经初始化完成了(这个初始化的过程是编译器给变量置以随机值实现初始化)。
这个过程对int类型好像没什么影响。但对const成员变量
和引用类型
就会出现问题。
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1,const int a = 10)
{
//赋值的方式进行“初始化”
_year = year;
_month = month;
_day = day;
_a = a;//此处会产生报错
}
private:
int _year;//此处仅仅是变量的声明,而非定义
int _month;
int _day;
const int _a;
};
对于const成员变量和引用类型,他们只能初始化一次,因为他们需要在定义的地方就需要完成初始化。而这种赋值相当于在其初始化之后进行二次赋值,当然会发生报错。
因此,初始化列表就诞生了。
在C++中,初始化列表提供了一种在构造函数体执行之前初始化成员变量的方式,他是类对象的所有成员变量定义及初始化的位置。这对于const成员变量
和引用类型
尤为重要,因为它们一旦被创建就必须被初始化。初始化列表确保了这些变量在对象生命周期开始时就已经被正确初始化。
效率更高
:与传统的赋值初始化相比,初始化列表直接初始化成员变量,减少了一次赋值操作。
必要性
:对于const成员变量和引用类型,初始化列表不是选择,而是必须,因为这些类型的成员变量不能在构造函数体内被赋值。
初始化列表基本形式:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date {
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
#include
#include
#include
using namespace std;
class Time
{
public:
Time(int hour = 0)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
private:
int _day = 1;
Time _t;
};
int main()
{
Date d;
}
见上面代码,即便没有写构造函数,也会在系统生成的默认构造函数中完成初始化,即day使用缺省值1(缺省值会在初始化列表中完成初始化),_t对象调用他的默认构造函数来完成初始化。
所以,尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,
一定会先使用初始化列表初始化。
类中声明次序
,与其在初始化列表中的先后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();
}
A. 输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
因此,_a2先声明,_a2先初始化为随机值。
#include
#include
#include
using namespace std;
class A
{
public:
//默认构造函数
A(int a = 0,int b = 0)
:_a(a)
,_b(b)
{
//表示默认构造函数被调用过
cout << "A(int a = 0)" << endl;
}
//默认析构函数
~A()
{
_a = 0;
//表示默认析构函数已被调用
cout << "~A" << endl;
}
//拷贝构造函数
A(const A& a)
{
_a = a._a;
cout << "A(const A& a)" << endl;
}
//赋值重载函数
A& operator=(const A& a)
{
if (this != &a)
{
_a = a._a;
}
cout << "A& operator=(const A& a)" << endl;
return *this;
}
private:
int _a;
int _b;
};
int main()
{
A aa1 = 100;
return 0;
}
上述代码中,A aa1 = 100看起来比较奇怪,其实他是合法的。
这其中的过程涉及到了隐式类型转化
。过程如下:
不过上述过程往往会得到编译器的优化(如果编译器比较新的话)
与其先生成临时变量,再拷贝,不如直接对目标进行构造
因此,构造+拷贝构造的过程就被优化为了构造。
此外,对于类中多成员变量的隐式类型转化,c++11也进行了支持。
int main()
{
A aa1 = {1,2};
return 0;
}
如果我们不想让隐式类型转换的发生,就可以使用explicit关键字
,作用于构造函数。
class A {
public:
explicit A(int a) : _a(a)
{}
private:
int _a;
};
因此,explicit关键字用于防止构造函数的隐式调用,这意味着必须显式地使用构造函数来创建对象。这样做可以避免因隐式类型转换而引入的错误。
实现一个类,计算程序中创建出了多少个类对象
考虑下面的代码:
class A
{
public:
A(int a = 0)
{
++count;
}
A(const A& aa)
{
++count;
}
int GetCount()
{
return count;
}
private:
// 不属于某个对象,所于所有对象,属于整个类
static int count; // 声明
int _a = 0;
};
int A::count = 0; // 定义初始化
void func()
{
A aa1;
A aa2(aa1);
A aa3 = 1;
A aa4[10];
}
int main()
{
func();//对象的创建
//访问静态成员变量
//cout << A::count << endl;
//cout << aa2.count << endl;
//cout << aa3.count << endl;
//A* ptr = nullptr;
//cout << ptr->count << endl;
A aa;
cout << aa.GetCount()-1 << endl;//显示最终结果
return 0;
}
其中:
静态成员变量在类的所有对象中是共享的。它们不属于任何一个对象实例,而是属于类本身。静态成员变量在程序开始时被创建,在程序结束时被销毁。
静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
int A::count = 0; // 定义初始化
类名::静态成员
或者对象.静态成员
来访问cout << A::count << endl;
cout << aa2.count << endl;
此外,下面代码也没有出错。因为没有发生解引用的操作
A* ptr = nullptr;
cout << ptr->count << endl;
上面调用GetCount需要专门再创建一个对象,非常麻烦。可以使用static来修饰成员函数使其成为静态成员函数
class A
{
public:
A(int a = 0)
{
++count;
}
A(const A& aa)
{
++count;
}
// 静态成员函数 -- 没有this指针
static int GetCount()
{
// _a++; // 不能直接访问非静态成员
return count;
}
private:
// 不属于某个对象,所于所有对象,属于整个类
static int count; // 声明
int _a = 0;
};
int A::count = 0; // 定义初始化
void func()
{
A aa1;
A aa2(aa1);
A aa3 = 1;
A aa4[10];
}
int main()
{
func();//对象的创建
cout << A::GetCount()<< endl;
return 0;
}
其中:
静态成员函数可以在没有类的对象实例的情况下被调用
cout << A::GetCount()<< endl;
它们只能访问静态成员变量和其他静态成员函数
。
// 静态成员函数 -- 没有this指针
static int GetCount()
{
// _a++; // 不能直接访问非静态成员
return count;
}
匿名对象是没有名称的对象实例,通常用于一次性的操作,如函数调用的参数或返回值。它们的生命周期非常短,仅存在于创建它们的表达式中
。
//假设存在类A
int main()
{
A(); //此处就是一个匿名对象
return 0;
}
//1.// 创建一个匿名A对象并立即调用其Print方法
A().Print();
//2.
A retA(ret);
return retA;//创建一个对象再返回
return A(ret);//可以直接返回一个匿名对象
友元函数可以访问类的私有成员,即使它们不是类的成员函数
。这对于实现某些操作符重载非常有用。
friend
修饰函数时,称为友元函数.
见下面代码:
class A
{
public:
//声明外部函数 Print 为友元函数
friend void Print(const A&a);
A(int val = 100)
:_val(val)
{}
private:
int _val;
};
void Print(const A& a)
{
cout << a._val << endl;
}
int main()
{
A aa;
Print(aa);
}
在类中声明友元函数后,函数即可访问类中的私有成员。
注意:
友元类的所有成员函数都可以访问另一个类的私有成员。这在类之间的紧密协作中非常有用。
但是
单向的,不具有交换性
。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;
};
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
Time的声明好像就是在说:Date是我的好朋友,不要阻止他访问我的成员
内部类是定义在另一个类内部的类。这种结构用于隐藏实现细节,使得外部代码不能直接访问内部类的成员。
class A
{
public:
//B 称作 A 的内部类
class B
{
private:
int _b;
}
private:
int _a;
}
内部类两个重要特性:
类域限制
class A
{
private:
//B 称作 A 的内部类
class B
{
private:
int _b;
}
private:
int _a;
}
int main()
{
A::B bb;//无法创建
return 0;
}
上述代码中,就无法创建内部类对象,因为受A类private的限制。
友元类
可以在B中访问A的私有成员。
class A
{
private:
static int k;
int h = 1;
public:
// B天生就是A的友元
class B
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
private:
int b = 2;
};
};
int A::k = 1;
int main()
{
A::B bb;
return 0;
}
见下面代码
void func1(A aa)
{
}
int main()
{
A aa1 = 1; // 构造+拷贝构造 -》 优化为直接构造
func1(aa1); // 无优化
func1(2); // 构造+拷贝构造 -》 优化为直接构造
func1(A(3)); // 构造+拷贝构造 -》 优化为直接构造
}
编译器都将构造+拷贝构造优化为了直接构造。
注意区分引用传参,这是没有进行优化的。
void func2(const A& aa)
{
}
func2(aa1); // 无优化
func2(2); // 无优化 调用一次构造
func2(A(3)); // 无优化 调用一次构造
由以上例子,可以总结得出:
传值返回
例一:
A func3()
{
A aa;
return aa;
}
int main()
{
func3();//构造+拷贝构造
A aa1 = func3(); // 拷贝构造+拷贝构造 -- 优化为一个拷贝构造
}
过程见下图:
A func3()
{
A aa;
return aa;
}
int main()
{
func3();//构造+拷贝构造
A aa1;//构造
aa1 = func3();//构造+拷贝构造+赋值
}
对比详见下图:
例二:
A func4()
{
return A();
}
int main()
{
func4(); // 构造+拷贝构造 -- 优化为构造
A aa3 = func4(); // 构造+拷贝构造+拷贝构造 -- 优化为构造
}
由以上例子,可以总结得出: