C++ 类与对象(三)
一、深度刨析构造函数
1.1 编译器对构造函数的优化
比较新的C++编译器在一行语句里面,若存在连续的构造函数(拷贝构造函数)调用,编译器对其优化为一次调用。注意:C++标准并没有规定必须要进行优化
下面测试用例的编译器进行了优化
#include <iostream>
using namespace std;
class Person
{
private:
char _name[10];
int _age;
public:
Person(const char* name,int age)
{
strcpy(_name, name);
_age = age;
cout << "调用构造函数" << endl;
}
Person(const Person& obj)
{
strcpy(_name, obj._name);
_age = obj._age;
cout << "调用拷贝构造函数" << endl;
}
Person& operator=(const Person& obj)
{
strcpy(_name, obj._name);
_age = obj._age;
cout << "赋值运算符重载" << endl;
return *this;
}
};
Person test(Person obj)
{
return obj;
}
int main()
{
Person p1("张三", 18);
Person p2 = test(p1);
return 0;
}
输出
调用构造函数
调用拷贝构造函数
调用拷贝构造函数
下面测试用例不是连续的构造函数(拷贝构造函数)调用,编译器不能优化
#include <iostream>
using namespace std;
class Person
{
private:
char _name[10];
int _age;
public:
Person(const char* name,int age)
{
strcpy(_name, name);
_age = age;
cout << "调用构造函数" << endl;
}
Person(const Person& obj)
{
strcpy(_name, obj._name);
_age = obj._age;
cout << "调用拷贝构造函数" << endl;
}
Person& operator=(const Person& obj)
{
strcpy(_name, obj._name);
_age = obj._age;
cout << "赋值运算符重载" << endl;
return *this;
}
};
Person test(Person obj)
{
return obj;
}
int main()
{
Person p1("张三", 18);
Person p2("李四",20);
p2 = test(p1);
return 0;
}
输出
调用构造函数
调用构造函数
调用拷贝构造函数
调用拷贝构造函数
赋值运算符重载
1.2 初始化列表
1.2.1 什么是初始化列表
如果问大家类的对象是在什么时候定义初始化的,大家都能回答上来,是在调用构造函数(拷贝构造函数)时初始化的
class Person
{
private:
char _name[10];
int _age;
};
Person p1; // 调用默认构造函数对p1初始化
Person p2 = p1; // 调用拷贝构造函数对p2初始化
那么对象的成员变量在什么时候定义初始化呢,我们一般习惯认为在构造函数(拷贝构造函数)体内初始化成员变量,然而这种想法是错误的,这只是对成员变量赋值而不是初始化。成员变量真正的初始化是在初始化列表进行的。初始化列表位于形参列表之后,函数体{ }之前,这也说明初始化列表进行的工作发生在函数体内任何代码执行之前。
1.2.2 初始化列表定义
初始化列表定义语法:以冒号开始,每个成员变量初始化由变量名+括号,括号内是初始值,成员变量间用逗号隔开
● 没有显式定义在初始化列表中成员变量,编译器对内置类型初始化为随机值,自定义类型调用其默认构造函数
● 每个成员变量在初始化列表中只能出现一次(初始化只能一次)
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
}
Date(const Date& obj)
: _year(obj._year)
, _month(obj._month)
, _day(obj._day)
{
}
};
1.2.3 必须使用初始化列表场景
成员变量为以下情况,成员变量必须放在初始化列表中进行初始化:
● 成员变量为引用(引用必须在定义时初始化)
● 成员变量为const(C++中const变量必须在定义时初始化)
● 成员变量为自定义类型且没有默认构造函数(不显式在初始化列表中定义,编译器会自动调用其默认构造函数)
class A
{
private:
int _x;
public:
A(int val)
{
_x = val;
}
};
class B
{
private:
int& _a;
const int _b;
A _c;
public:
B(int& a, int b, int c)
:_a(a)
,_b(b)
,_c(c)
{
}
};
1.2.4 推荐使用初始化列表场景
成员变量类型为自定义类型推荐使用初始化列表,如果不使用初始化列表:编译器调用成员变量的默认构造函数,并在函数体内调用赋值运算符重载进行赋值,开销2次。使用初始化列表:在初始化列表中调用成员变量的构造函数,函数体内不需要赋值,开销1次
测试:不使用初始化列表
class A
{
private:
int _x;
public:
A(int x = 0)
{
_x = x;
cout << "调用类A构造函数" << endl;
}
A(const A& obj)
{
_x = obj._x;
cout << "调用类A拷贝构造函数" << endl;
}
A& operator=(const A& obj)
{
_x = obj._x;
cout << "调用类A赋值运算符重载" << endl;
return *this;
}
};
class Date
{
private:
int _year;
int _month;
int _day;
A _a;
public:
Date(int year, int month, int day,const A& a)
{
_year = year;
_month = month;
_day = day;
_a = a;
}
};
int main()
{
A a(10);
Date d(2020, 01, 02, a);
return 0;
}
输出
调用类A构造函数
调用类A构造函数
调用类A赋值运算符重载
测试:使用初始化列表
class A
{
private:
int _x;
public:
A(int x = 0)
{
_x = x;
cout << "调用类A构造函数" << endl;
}
A(const A& obj)
{
_x = obj._x;
cout << "调用类A拷贝构造函数" << endl;
}
A& operator=(const A& obj)
{
_x = obj._x;
cout << "调用类A赋值运算符重载" << endl;
return *this;
}
};
class Date
{
private:
int _year;
int _month;
int _day;
A _a;
public:
Date(int year, int month, int day,const A& a)
: _a(a)
{
_year = year;
_month = month;
_day = day;
}
};
int main()
{
A a(10);
Date d(2020, 01, 02, a);
return 0;
}
输出
调用类A构造函数
调用类A拷贝构造函数
1.2.5 成员变量的初始化顺序
成员变量的初始化顺序由类中成员变量声明次序决定,与其在初始化列表中的先后次序无关。所以推荐初始化列表中成员变量次序与类中声明次序一致
class A
{
private:
int _x;
int _y;
public:
A(int val)
: _y(val)
, _x(_y)
{
}
void Print()
{
cout << "_x = " << _x << " _y = " << _y << endl;
}
};
int main()
{
A a(10);
a.Print();
}
输出
_x = -858993460 _y = 10
在初始化列表中_y的次序在_x前,如果成员变量初始化次序由初始化列表中次序决定,先将形参val值初始化给_y,然后将_y值初始化给_x,所以_x=_y=val。但观察输出结果发现_x为随机值,_y=val,因为类中_x声明次序在_y前面,所以先将_y的值初始化给_x,此时_y为随机值,所以_x也为随机值,然后将val值初始化给_y
1.3 explicit关键字
使用explicit关键字修饰的构造函数将禁止隐式类型转换
1.3.1 隐式类型转换
c++支持隐式类型转换,即根据类型A变量的值创建一个类型B的临时变量,并将该临时变量赋值给某个类型B变量
#include <iostream>
using namespace std;
class A
{
private:
int _a;
public:
A(int a = 10)
{
_a = a;
cout << "构造函数" << endl;
}
A(const A& obj)
{
_a = obj._a;
cout << "拷贝构造函数" << endl;
}
};
int main()
{
A a = {3}; // 隐式类型转换
A b = 4; // 隐式类型转换
return 0;
}
1.3.2 禁止构造函数隐式类型转换
explicit虽然禁止构造函数隐式类型转换,但可以强制类型转换
#include <iostream>
using namespace std;
class A
{
private:
int _a;
public:
explicit A(int a = 10)
{
_a = a;
cout << "构造函数" << endl;
}
A(const A& obj)
{
_a = obj._a;
cout << "拷贝构造函数" << endl;
}
};
int main()
{
A a = {3}; // 错误,不能隐式类型转换
A b = 4; // 错误,不能隐式类型转换
A c = (A)3; // 正确,强制类型转换
A D = (A){4}; // 正确,强制类型转换
return 0;
}
二、static
类中用static修饰的成员变量称为静态成员变量,用static修饰的成员函数称为静态成员函数
2.1 静态成员变量
● 静态成员变量为类中所有对象共享,对象中不存储静态成员变量
● 静态成员变量必须在类外定义并初始化且不需要加static关键字
● 静态成员变量可用对象或类名访问,前提是静态成员变量控制权限为public
下面程序为统计创建出多少个类对象
class A
{
public:
static int _sCount; // 静态成员变量的声明,为了测试对象或类访问变量,控制权限给的public,一般不建议给public
A()
{
++_sCount;
}
A(A& obj)
{
++_sCount;
}
};
int A::_sCount = 0; // 静态成员变量必须在类外定义并初始化
int main()
{
cout << "已创建:" << A::_sCount << "个对象" << endl; // 通过类名访问
A a1;
A a2;
A a3(a1);
cout << "已创建:" << a3._sCount << "个对象" << endl; // 通过对象访问
return 0;
}
输出
已创建:0个对象
已创建:3个对象
2.2 静态成员函数
● 静态成员函数没有隐藏的this形参,不能访问任何非静态成员变量
● 静态成员函数和类的普通成员函数一样,也有public、protected、private3种访问级别,也可以具有返回值
● 静态成员函数可用对象或类名访问,前提是静态成员函数控制权限为public
● 静态成员函数内不能调用非静态成员函数(静态成员函数没有隐藏this形参)
● 非静态成员函数内可以调用静态成员函数(非静态成员函数有隐藏this形参)
上面统计创建出多少个类对象的程序,_sCount给的public控制权限封装性不好,程序修改为_sCount控制权限为private,通过静态成员函数去访问
class A
{
private:
static int _sCount; // 静态成员变量的声明,为了测试对象或类访问变量,控制权限给的public,一般不建议给public
public:
A()
{
++_sCount;
}
A(A& obj)
{
++_sCount;
}
static int GetACount()
{
return _sCount;
}
};
int A::_sCount = 0; // 静态成员变量必须在类外定义并初始化
int main()
{
cout << "已创建:" << A::GetACount() << "个对象" << endl; // 通过类名访问
A a1;
A a2;
A a3(a1);
cout << "已创建:" << a3.GetACount() << "个对象" << endl; // 通过对象访问
return 0;
}
输出
已创建:0个对象
已创建:3个对象
三、C++11 成员变量初始化新玩法
C++11 之前令人诟病的是编译器生成的默认构造函数对于类型为内置类型成员变量不进行初始化处理,若需对内置类型成员变量初始化需要手动定义构造函数,在构造函数的初始化列表对成员变量初始化(构造函数体内对成员变量赋值)。C++11 引入非静态成员变量在类中声明时给缺省值的玩法,当在构造函数中没有显式对成员变量初始化(赋值)则用缺省值初始化
class A
{
private:
static int _sCount;
int _x = 1; // 在声明时给缺省值
int _y = 2;
public:
void Print()
{
cout << "_sCout = " << _sCount << " _x = " << _x << " _y = " << _y << endl;
}
};
int A::_sCount = 0; // 静态成员变量必须在类外定义并初始化
int main()
{
A a;
a.Print();
return 0;
}
输出
_sCout = 0 _x = 1 _y = 2
四、友元
友元可以让函数突破类的封装性,能访问任何控制权限(public、protected、private)的成员,在某些场景下提供了便利,但破坏了封装性,一般不推荐使用
4.1 友元函数
我们可以用<<
运算符配合cout
打印内置类型变量,但如果要打印自定义类型,需要对<<
进行运算符重载。当重载为成员函数时会带来问题,因为非静态成员函数默认第一个形参为隐藏this指针,调用时编译器会将左操作数地址传给隐藏的形参this指针,this指针类型为 类名*
,而左操作数cout
对象类型为ostream
,类型不匹配,只能通过函数名,或交换左右操作数位置方式调用
class A
{
private:
int _x;
public:
A(int x = 10)
{
_x = x;
}
ostream& operator<<(ostream& out)
{
out << "_x = " << _x << endl;
return out;
}
};
int main()
{
A a(1);
//cout << a << endl; 错误,左操作数地址类型为ostream* , 隐藏形参this类型为 A*
a.operator<<(cout); // 正确
a << cout; // 正确,调用时将左操作数地址传给隐藏形参this,右操作数传给形参out
return 0;
}
由于运算符重载为非静态成员函数都有隐藏this形参,而<<
运算符左操作数为cout
。类型不匹配,只能将运算符重载为全局函数,但由于成员变量访问权限为非public
,类外不能访问成员变量。这种场景可以使用友元函数,友元函数:能访问任何控制权限(public、protected、private)的成员,它是定义在类外部的普通函数,不属于任何类,但需要在对应类里面进行函数声明,声明时需要加friend关键字
● 友元函数可以在类里面任何地方声明,不受类访问限定符限制
● 一个函数可以是多个类的友元函数
● 友元函数不能用const修饰,因为没有this指针
友元函数写法一:在类中声明友元函数,类外定义友元函数
class A
{
friend ostream& operator<<(ostream& out, A& obj); // 在类中声明友元函数
private:
int _x;
public:
A(int x = 10)
{
_x = x;
}
};
ostream& operator<<(ostream& out,A& obj) // 友元函数定义
{
out << "_x = " << obj._x << endl;
return out;
}
int main()
{
A a(1);
cout << a << endl;
return 0;
}
输出
_x = 1
友元函数写法二:在类中声明并定义友元函数
class A
{
friend ostream& operator<<(ostream& out,A& obj) // 友元函数定义
{
out << "_x = " << obj._x << endl;
return out;
}
private:
int _x;
public:
A(int x = 10)
{
_x = x;
}
};
4.2 友元类
类A是类B的友元,则称类A为类B的友元类。友元类所有成员函数可以访问另一个类的任何控制权限(public、protected、private)的成员
● 友元关系是单向的(A是B的友元类,A可以访问B,但B不能访问A的非public的成员)
● 友元关系不能传递(A是B的友元,B是C的友元,不能说明A是C的友元)
class B
{
friend class A; // 声明类A是类B的友元类
private:
int _b;
};
class A
{
private:
int _a;
B b;
void Print()
{
cout << "_b = " << b._b << endl; // 可以访问类B的任何权限的成员
}
};
五、内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类只是定义在外部类里面,它不属于外部类,不能通过外部类的对象去访问内部类。
● 内部类就是外部类的友元类,外部类不是内部类的友元类
● 外部类可以使用访问限定符public、protected、private限定内部类
● 内部类可以直接访问外部类的静态成员,不需要用外部类的对象/类名
● 内部类只是定义在外部类里面而已,并不占用外部类的空间
class A
{
private:
int _a;
static int _sCount;
public:
class B
{
private:
int _b;
public:
void Print(A& obj)
{
cout << _sCount << endl; // 直接访问外部类静态成员变量
cout << obj._a << endl; // 通过对象访问成员变量
}
};
};
int main()
{
A::B b; // 正确,类B访问权限为public,通过作用域限定符::访问类A中的类B并初始化对象b
// A::C c 错误,类C访问权限为private
}