目录
- 1. 构造函数与初始化列表
- 1.1 对象的创建与构造函数的初始化
- 1.2 初始化列表及构造函数存在的意义
- 1.3 explicit关键字与构造函数的类型转换
- 2. static成员变量与static成员函数
- 2.1 static成员变量
- 2.2 static成员函数
- 3. 日期类流插入操作符的重载与友元
- 3.1 友元
- 3.2 友元函数
- 3.3 友元类
- 4. 内部类
- 5. 匿名对象
- 6. 拷贝对象时编译器可能会进行的一些优化
1. 构造函数与初始化列表
1.1 对象的创建与构造函数的初始化
- 在前面的学习中,我们尝试了对简单类(日期类)进行了实现,而后在使用中我们通过定义类模板然后实例化的方式,创建我们所需要的对象。
- 在这一过程中,编译器按照所指定的类型去相应的内存区域中申请空间,在已经创建好变量后,再调用构造函数对生成的对象进行初始化。
- 大多数情况下,这种初始化的方式都不会出现问题,可是当类的成员变量中有一些特定的类型比如,const修饰的变量,引用类型的变量,没有默认构造函数的自定义类型,此时,这种初始化方式就行不通了。(默认构造函数:编译器自动生成,无参数,有缺省参数)
1.2 初始化列表及构造函数存在的意义
- 内置类型可以在创建变量申请空间时就进行变量的初始化,而自定义类型是否也可以在创建变量的同时就进行初始化,它的初始化方式是什么,接下来,我们引出类与对象中的初始化列表。
- 类实例化生成对象时并不是只开辟空间,对空间中的内容不做处理,而是会调用初始化列表对开辟出的空间进行初始化。前面之所以无法对特殊类型无法进行初始化,是因为我们没有向初始化列表中添加内容。
- 初始化列表的定义方式:
//构造函数
class A
{
private:
int _a;
int _b;
int _c
//构造函数,函数体之前,语法如下:
A(int a, int b ,int c)
:_a(a)
,_b(b)
,_c(c)
{}
}
- 初始化列表的调用方式:(三种必须用初始化列表进行初始化的成员变量)
//没有缺省参数
class A
{
public:
A(int d)
{
_d = d;
}
int _d;
};
class B
{
public:
const int _a;
int& _b;
A _c;
//构造函数
B(int b, int c)
:_a(10)
,_b(b)
,_c(c)
{
cout << " _a = " << _a << " _b = " << _b << " _c._d = " << _c._d << endl;
}
};
int main()
{
int b = 20;
int c = 30;
B a(b, c);
return 0
}
- 初始化列表初始化成员变量的顺序:
初始化列表进行初始化的顺序是根据成员变量的声明顺序决定的
class A
{
public:
int _b;
int _a;
//先初始化_a,再初始化_b
A(int a = 0)
:_a(a)
,_b(_a)
{}
void Print()
{
cout << _a << ' ' << _b << ' ' << endl;
}
};
int main()
{
A a(10);
a.Print();
return 0;
}
执行结果:使用成员变量_a初始化成员变量_b时,_a还没有被初始化
- <1> 既然初始化列表可以进行初始化,并且初始化列表能做到构造函数无法做到的特殊类型成员的声明,那么,为什么还要有构造函数呢?
<2> 初始化列表能做的只有初始化,无法对初始化后的变量做检查与合法性判断
class A
{
public:
int* _a;
A(int n)
:_a((int*)malloc(n * sizeof(int)))
{
if (_a == nullptr)
{
perror("malloc failed");
exit(-1);
}
}
};
1.3 explicit关键字与构造函数的类型转换
- 类的默认成员函数
operator=重载
,其构造函数在只有单参数或拥有缺省参数,支持用与成员变量类型相同的数据,变量直接进行赋值操作。
class Date
{
public:
int _year;
int _month;
int _day;
Date(int year, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
};
int main()
{
Date d1(2024);
d1 = 2025;
int year = 2026;
d1 = year;
cout << d1._year << '-' << d1._month << '-' << d1._day << endl;
return 0;
}
支持上述操作的原因,是因为数据或者变量会进行类型转换构造临时对象,然后再用临时构造出的对象进行赋值操作。
explicit
关键字,修饰构造函数,使得这个构造函数所在的类其,实例化的对象不会发生类型转换的操作。
class Date
{
public:
int _year;
int _month;
int _day;
explicit Date(int year, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
};
2. static成员变量与static成员函数
2.1 static成员变量
- 静态成员变量也是类的成员之一,它不独属于某个对象,而是属于这个类,为所有实例化对象所共有,存放在静态区。
- 静态成员变量在类中声明,在类外定义,在类外定义时不需要加static关键字。
- 静态成员变量的调用方式为,
对象.静态成员变量
,类区域::静态成员变量
- 静态成员变量也受访问限定符的限制
class A
{
public:
static int _count;
A()
{
++_count;
}
A(const A& a)
{
++_count;
}
~A()
{
--_count;
}
};
int A::_count = 0;
int main()
{
A a1;
cout << a1._count << endl;
a1.~A();
cout << A::_count << endl;
return 0;
}
2.2 static成员函数
- 与静态成员变量类似,静态成员函数也是属于全体实例化对象,而不是属于某个对象。
- 静态成员函数的定义方式,也是类内声明类外定义。
- 静态成员函数没有this指针,不能调用非静态成员变量。
- 静态成员函数的调用方式,
对象.静态成员函数
,类域::静态成员函数
- 同样的静态成员函数也受访问限定符限制。
class A
{
//public:
static int _count;
static int GetCount();
public:
//非静态成员函数可以调用静态成员函数
void Print()
{
cout << (*this).GetCount() << endl;
cout << "hello" << endl;
}
A()
{
++_count;
}
A(const A& a)
{
++_count;
}
~A()
{
--_count;
}
};
int A::_count = 0;
int A::GetCount()
{
//静态成员函数没有this指针
//无法调用非静态成员函数
return A::_count;
}
3. 日期类流插入操作符的重载与友元
- 当我们尝试对实现过的日期类进行流插入运算的重载时,当把它作为成员函数时,我们发现无法实现。
- 操作数的次序为操作符重载函数的参数从左往右,分别是操作符的第一个,第二个…操作数。
- 成员函数的一个参数都为隐藏的默认参数this指针,而流插入操作符的需要的第一个参数是ostream类型的变量。可是,当我们不使用成员函数的方式实现,那么函数就无法访问private访问限定符修饰的成员变量。
- 那么,流插入操作符的重载就无法实现吗,这里我们引入C++新的内容,友元。
3.1 友元
- 友元关系的实际应用分为友元类与友元函数,这是一种突破类访问限定符封装的方式,它在提供了这种功能的同时,也不可避免地增加了代码的耦合性,不建议多用。
3.2 友元函数
- 友元函数是普通函数,它定义在类外。而它达成友元的方式为,在类中对其进行友元声明。
- 友元函数可以在类中的任何地方声明,不受访问限定符影响。
- 友元函数的声明方式为,在普通的函数声明前加关键字
friend
。- 一个函数可以是多个类的友元函数。
class Date
{
public:
int _year;
int _month;
int _day;
Date(int year, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& out, const Date& d);
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << '-' << d._month << '-' << d._day;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
3.3 友元类
- 友元类的所有成员函数都是另一个类的友元函数,可以访问另一个类的所有私有成员。
- 友元类关系是单向的。
- 友元关系不可传递,A类是B类友元,B类是C类的友元,A类不是C类的友元,不可以访问C类。
- 友元关系不能被继承
- 友元类的声明方式为,在需要被访问的类中声明其的友元类,
friend + 类名
。
class Time
{
public:
Time(int hour = 0, int minute = 0, int seconds = 0)
:_hour(hour)
, _minute(minute)
, _seconds(seconds)
{}
friend class Date;
private:
int _hour;
int _minute;
int _seconds;
};
class Date
{
public:
int _year;
int _month;
int _day;
Time _t;
Date(int year, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
void SetTime()
{
_t._hour = 21;
_t._minute = 25;
_t._seconds = 0;
cout << _t._hour << '/' << _t._minute << '/' << _t._seconds << endl;
}
};
4. 内部类
- 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
- 外部类对内部类没有访问权限。
- 内部类只是在外部类中声明,外部类计算大小时不包含内部。(sizeof(外部类)单纯只是外部类的大小)
- 内部类对外部类天生就是友元,并且可以直接访问外部类的静态成员变量与函数,无需指定类域。
- 内部类可以在任意访问限定符的区域声明,在private中时,无法进行内部类的调用实例化对象。
class A
{
private:
int _a;
static int _c;
public:
class B
{
public:
void test(A& x)
{
x._a = 10;
_c = 20;
cout << x._a << endl;
cout << x._c << endl;
}
};
};
int A::_c = 0;
int main()
{
A x;
//內部类对象的声明
A::B h;
h.test(x);
return 0;
}
5. 匿名对象
- 在实例化对象时,省略对象名的创建方式。
- 匿名对象的声明周期只有一行,紧接着下一行时,就会调用析构函数将其销毁。
class A
{
private:
int _ a;
public:
A(int a = 0)
{
cout << _a << endl;
}
void Print()
{
cout << "hello world" << endl;
}
};
//不支持此种调用构造函数的方式,因为无法识别其为函数的声明还是析构函数的调用
A a1();
//创建匿名对象的方式
A();
//匿名对象调用成员函数
A().Print();
6. 拷贝对象时编译器可能会进行的一些优化
- 在成员函数传参和传返回值的过程中,一般编译器会做一些优化,减少不必要对象的拷贝
- 在同一行中,连续的构造,拷贝构造操作编译器会进行优化:
<1> 构造 + 拷贝构造 优化为 构造
<2> 连续的构造 + 拷贝构造 优化为 直接构造
<3> 连续的拷贝构造 优化为 直接进行拷贝构造
class C
{
private:
int _c;
public:
C(int c)
{
cout << "C()" << endl;
}
C(const C& tmp)
{
cout << "C(const C&)" << endl;
}
};
int main()
{
//构造 + 拷贝构造,类型转换,生成临时对象
C c1(2);
return 0;
}