目录
前言
前两章主要理解了类与对象的核心知识点,本章将介绍类与对象一些细微收尾的知识点;
一、再次深入理解构造函数
构造函数除了上一章节我们提到的七个特点以外,还有一个初始化的知识点,我们都知道类是一个模板,而我们可以通过这个模板构造出无数的对象,只有在我们构造处对象时,类才会有自己的实体,下面介绍关于类成员实体的定义;
1、构造结构体的赋值与初始化
class Date
{
public:
Date(int year, int month, int day)
{
// 成员对象的赋值
_year = year;
_month = month;
_day = day;
}
private:
// 成员对象的声明
int _year;
int _month;
int _day;
};
我们之前是通过上述代码对整个类进行初始化的,实际上,对于每个成员对象来说,他们并不是在函数体内完成初始化动作,他们在函数体内完成的是赋值动作;实际上,真正初始化一个对象是在其构造函数的圆括号与花括号之间进行初始化,格式为以冒号开始(:),用成员变量名+(初始化的值)的格式,其中每个初始化用逗号分割开来,如果看不懂,可看以下代码;
Date(int year, int month, int day)
//初始化列表
:_year(year)
,_month(month)
,_day(day)
{}
以上两个构造函数,在初始化类对象时,一个是赋值(函数体内),一个是初始化(在初始化列表内),如果不懂初始化和赋值的区别,可看以下代码;
int main()
{
int a; // 定义对象,没有初始化
a = 10; // 给对象赋值
int b = 10; // 定义对象并对其初始化
return 0;
}
这一部分一定要区分好 声明,定义,初始化,赋值的概念,切记不可混淆;可能有小伙伴就纳闷了,那这两种方式有什么区别呢?最终实现的效果不是一样的吗?
确实,在很多时候,这两种可能区别不大,但是推荐还是使用初始化列表的方式对成员变量进行初始化。因为有一些成员变量只能在初始化列表中进行初始化;如下
(1)当有const类型的成员变量时
class A
{
public:
A(int a)
:_a(a)
{}
private:
const int _a;
};
我们都知道const变量只有一次初始化的机会,就是定义的时候,如果我们在函数体中赋值,很明显时错误的,因为不能给const变量赋值,只能在定义时进行初始化,而成员变量的初始化在初始化列表中;
(2)引用成员变量
class A
{
public:
A(int& a)
:_ra(a)
{}
private:
int _ra;
};
通过前面的学习,我们发现引用也只能在定义时进行初始化,故也只能在初始化列表中进行初始化;
(3)无默认构造函数的自定义类型成员(无默认 -> 无参/全缺省构造)
class B
{
public:
B(int b)
:_b(b)
{}
private:
int _b;
};
class A
{
public:
A(int b)
:_bb(b)
{}
private:
B _bb;
};
补充知识:当我们在用一个类实例化出对象时,我们如果都会调用其对应的构造函数,而在执行函数体内容之前,都会先在初始化列表中定义其成员变量,定义时,如果我们不在初始化列表中显示定义,编译器会对其执行默认行为,即对内置类型不做处理,对自定义类型调用其默认构造函数;也就是说不管我们是否显示定义成员变量,构造函数都会走一遍初始化列表;
根据以上补充内容,A类中有B类型的成员,而B类型无默认构造函数,如果我们不在初始化列表对B类型进行显示调用其构造函数,编译器则会自动调用B类型的默认构造函数,而B类型没有默认构造函数,只有一个单参数的构造函数,编译器就会报错,找不到合适的默认构造函数;
2、初始化列表初始成员变量的顺序
观察以下代码,判断程序结果;
// 判断成员变量 _a 与 _b的结果
class A
{
public:
A(int a)
:_a(a)
,_b(_a)
{}
private:
int _b;
int _a;
};
int main()
{
A a(1);
return 0;
}
其结果如下图所示;
很多人就不解了,_b不是用_a进行初始化了么,在初始化列表中,我们首先将_a初始化成了1,然后再用_a对_b进行初始化,那为什么_b是随机值呢?这就引出了构造函数初始化列表的另一条特性;
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关;
也就是说刚才我们在类中先声明_b,因此先初始化_b,而我们用_a对_b进行初始化,而_a也是随机值,因此初始化的值也是随机值,接着我们用a对_a进行初始化,故_a的值为1;
3、explicit关键字
(1)对于单参数的隐式类型转换
当我们在构造一个单参数的对象时,经常有以下两种写法;
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
int main()
{
// 写法一
A a1(1);
// 写法二
A a2 = 1;
return 0;
}
其中写法一实际上是调用了单参构造,而写法二实际上调用了单参构造+拷贝构造,编译器首先将 1 隐式类型转换调用单参构造成一个临时对象,接着用这个临时对象进行拷贝构造给我们的a2;
补充知识1:编译器调用单参构造生成的临时变量具有常性;
补充知识2:编译器会将调用单参构造+拷贝构造优化为直接调用单参构造
口说无凭,如何论证以上两个补充知识呢?看如下代码;
int main()
{
A& ra1 = 1; // err
const A& ra2 = 1; // 正常运行
return 0;
}
当我们用1初始化ra1时会报错,而初始化ra2时却可以正常运行,两者区别仅仅一个const,ra1是一个普通引用,ra2是一个const常量引用,当我们用1分别进行初始化时,首先会用1进行隐式类型转换,用单参构造函数构造一个临时对象,因为该临时对象具有常性,故可以用ra2引用,不可用ra1引用;证实了补充知识1;
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& a)
:_a(a._a)
{
cout << "A(const A& a)" << endl;
}
private:
int _a;
};
int main()
{
A a1 = 1;
return 0;
}
我们再对以上类进行改造,分别再单参构造和拷贝构造处加上打印,我们再运行程序,会发现以下结果;
并未打印拷贝构造的打印内容,故证实了我们的补充知识2;
以上知识实际上都是为了我们的explicit关键字进行铺垫;当我们不想一个类进行隐式类型转换时,我们用explicit关键字修饰其构造函数,这样如果我们用隐式类型转换的方法初始化该类的对象时会发生编译报错;
(2)对于多参数的隐式类型转换
实际上,在C++11以后,我们以支持对多参数的隐式类型转换,我们只需用花括号将其参数括起来即可;
class A
{
public:
A(int a, int b)
:_a(a)
,_b(b)
{}
private:
int _a;
int _b;
};
int main()
{
// 多参数的隐式类型转换
A a = { 2, 4 };
return 0;
}
同样,我们使用explicit关键字可以禁止构造函数的隐式类型转换;
二、static成员
1、C语言中static的使用
首先,我们回忆一下static关键字在C语言中的使用;
(1)修饰局部变量,经常用于函数中,可以修饰局部变量使其出作用域不被销毁,但是只能初始化一次;
int func()
{
static int i = 0;
i++;
return i;
}
int main()
{
cout << func() << endl;
cout << func() << endl;
cout << func() << endl;
return 0;
}
由于C++支持C语言,所以static的特性也保留了下来;
(2)修饰全局变量,使其只能作用于本文件内,即使在其他文件重新声明也无法调用;
(3)修饰普通函数,使该函数只能作用于本文件内,即使在其他文件声明,也无法调用;
2、C++中static的新用法
在C++中,static也可以修饰类内的成员变量,使其属于整个类,而不属于某个具体对象;
class A
{
public:
int _a;
static int _i;
};
// 只能在类外初始化
int A::_i = 0;
int main()
{
cout << A::_i << endl;
return 0;
}
静态类成员的特性:
(1)静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
(2)静态成员必须定义在类外,定义时可不加static关键词,类内只是声明
(3)静态成员也会受到类内的public、protected与private等访问限定符的限制
(4)静态成员可通过 类名::静态成员名 或 对象名::静态成员名 的方式访问(前提是可以访问到,也就是修饰符允许)
(5)静态成员函数没有this指针,因此不能访问非静态成员
面试题:统计某个类创建的次数
class A
{
public:
A()
{
_count++;
}
A(const A& a)
{
_count++;
}
static int GetCount()
{
return _count;
}
private:
static int _count;
};
int A::_count = 0;
A func(A a)
{
return a;
}
int main()
{
A a;
func(a);
// 以下两种方法都可以,但是上面的方法无需实例化对象
cout << A::GetCount() << endl;
cout << a.GetCount() << endl;
return 0;
}
若想要统计一个类出现多少次,可以把问题转换为一个类调用了多少次构造函数(普通构造与拷贝构造),我们可以利用性质一与性质二,创建一个类内静态变量,并在类外初始化为0,然后设计一个静态成员函数,返回静态成员变量的值,静态成员的好处是不用专门初始化一个对象来调用这个静态成员函数,我们只需类名即可调用(利用了性质四);我们可以把静态成员函数理解为专门服务于静态成员变量;
下面还有两个问题值得深思:
1、静态成员函数可以调用非静态成员函数吗?
不可以,因为静态成员函数没有this指针,无法调用非静态成员;
2、非静态成员函数可以调用静态成员函数吗?
可以,因为非静态成员函数有this指针,且静态成员函数属于该类实例化出来的每一个对象;故可以调用
三、友元
友元是一种突破类的封装的一种方式,有时可以提供便利性,但会破坏类的封装;增加程序耦合度;
1、友元函数
在上一章节中,我们实现日期类时,为了实现流插入和流提取的运算符重载,我们使用了友元,使其可以访问类内私有成员;
class Date
{
// 友元声明 -- 允许该函数访问该类的私有成员
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1970, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
// 想要实现 cout << d; 的效果(cout在前,d在后),又想访问类内私有成员
ostream& operator<<(ostream& out, const Date& d)
{
cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
Date d;
cin >> d;
cout << d;
return 0;
}
友元函数的性质如下:
1、友元函数可以声明在类内的任何地方,且不受类的访问限定符的限制,以关键字friend开始;
2、友元函数可以访问类的任何成员,包括private与protected修饰的成员;
3、友元函数不能用const修饰,因为const修饰this指针,而友元函数不属于类内成员
4、一个函数可以是多个类的友元
5、友元函数的调用与普通函数的调用相同
2、友元类
友元类的所有成员函数都是另一个类的友元,也就是说,友元类可以访问另一个类的所有成员(包括私有成员);
class Time
{
// 设置成Date的友元类
friend class Date;
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 = 1970, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日 ";
cout << _t._hour << "时" << _t._minute << "分" << _t._second << "秒" << endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d;
d.Print();
return 0;
}
当我们打印Date类时,我们也想打印其成员Time类中的私有成员,此时,我们便可以将Date类设为Time类的友元,是Date类可以随意访问Time类的所有成员;实现打印;
友元类的性质:
1、友元类的关系是单向的,A类是B类的友元,但是B类不一定是A类的友元
2、友元关系不能传递,即A类是B类的友元,B类是C类的友元,而A类并不是C类的友元
3、友元关系不能继承
四、内部类
内部类即定义在类内部的类,他与外部类有什么关系呢?看如下代码
class A
{
public:
class B
{
public:
private:
int _b;
};
private:
int _a;
};
int main()
{
A a;
A::B b;
cout << sizeof(a) << endl; // ?
cout << sizeof(b) << endl; // ?
return 0;
}
以上A类的大小为4,B类的大小也为4,也就是说,内部类与外部类都是独立的两个类,并没有什么包含关系;
上图验证了我们的想法,并且在A类中并不包含B类;那内部类有什么作用呢?
内部类的性质:
1、内部类天生是外部类的友元
2、内部类访问外部类的静态成员不需要指定外部类名
3、外部类的大小等于外部类大大小,并不会将内部类的大小计入其中,可以把外部类看作给内部类加了一层类域;
五、匿名对象
在我们实例化对象时,我们可以实例化出一个无名对象,该对象就便被称为匿名对象;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
// 定义匿名对象,生命周期仅在当前行
A();
return 0;
}
通过上面代码,我们可以看出我们的匿名对象的生命周期仅在当前行,一旦结束当前行后,自动调用析构函数;当然,有一种特殊用法,我们可以通过const 引用延长匿名对象的声明周期,使与const 引用同声明周期;
六、拷贝对象时编译器的优化
首先我们定义如下类,方便我们研究编译器的优化;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A & aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A a1(1); // 构造函数
A a2 = 2; // 构造+拷贝构造 优化-> 构造
return 0;
}
a2对象会先隐式类型转换然后再调用拷贝构造,这个会直接优化成一个构造,这里我们在前面验证过,此处略过;
// 传值传参
void func1(A aa)
{}
int main()
{
A a1(1);
func1(a1); // 拷贝构造
cout << "--------------------" << endl;
func1(A(1)); // 构造+拷贝构造 -> 构造
cout << "--------------------" << endl;
func1(1); // 构造+拷贝构造 -> 构造
return 0;
}
// 引用传参
void func2(const A& aa)
{}
int main()
{
A a1(1);
func2(a1); // 无优化
cout << "--------------------" << endl;
func2(A(1)); // 无优化 - 构造
cout << "--------------------" << endl;
func2(1); // 无优化 - 构造
return 0;
}
总结:引用传参可以大大减少拷贝构造函数调用次数;
// 传值返回
A func3()
{
A aa;
return aa;
}
int main()
{
// 1、
A a1;
a1 = func3();
// 2、
A a2 = func3();
return 0;
}
上面两种传值返回的方式中,明显方法一消耗大于方法二,方法一不做任何优化,而方法二中将传值返回时调用的拷贝构造与拷贝构造a2对象的拷贝构造结合成了一步;所以,这种情况下,能调用拷贝构造就调用拷贝构造,尽量不调用赋值重载;
A func4()
{
return A();
}
int main()
{
func4(); // 构造+拷贝 -> 构造
cout << "--------------------" << endl;
// 1、
A a1; // 构造
a1 = func4(); // 构造+拷贝+赋值 优化为-> 构造+赋值
cout << "--------------------" << endl;
// 2、
A a2 = func4(); // 构造+拷贝+拷贝 优化为-> 构造
return 0;
}
当我们返回匿名对象时,编译器也会对其做优化,将构造+拷贝构造合成了一步;因此在最后一个表达式中,只调用了构造函数;