目录
再谈构造函数
在创建对象时,编译器会通过构造函数给对象中各个成员变量一个合适的初始值:
class Date
{
public:
// 构造函数
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量 的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始 化一次,而构造函数体内可以多次赋值。
初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
// 构造函数
Date(int year = 0, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
注意事项:
一、每个成员变量在初始化列表中只能出现一次
因为初始化只能进行一次,所以同一个成员变量在初始化列表中不能多次出现。
二、类中包含以下成员,必须放在初始化列表进行初始化:
1.引用成员变量
引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化。
int a = 10;
int& b = a;// 创建时就初始化
2.const成员变量
被const修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化。
#include<iostream>
using namespace std;
class Data
{
public:
Data(int a = 1, int b = 1,int x=2)
{
_a = a;
_b = b;
_x = x;//无法完成赋值,因为const修饰必须在定义时给初始值,所以需要用初始化列表
}
void Print()
{
cout << _a << " " << _b << endl;
}
private:
int _a;
int _b;
const int _x;
};
int main()
{
Data d1;
d1.Print();
return 0;
}
3.自定义类型成员(该类没有默认构造函数)
若一个类没有默认构造函数,那么我们在实例化该类对象时就需要传参对其进行初始化,所以实例化没有默认构造函数的类对象时必须使用初始化列表对其进行初始化。
在这里再声明一下,默认构造函数是指不用传参就可以调用的构造函数:
1.我们不写,编译器自动生成的构造函数。
2.无参的构造函数。
3.全缺省的构造函数。
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_aobj(a)
, _ref(ref)
, _n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
2.对于自定义类型,使用初始化列表可以提高代码的效率
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Test
{
public:
// 使用初始化列表
Test(int hour)
:_t(12)// 调用一次Time类的构造函数
{}
private:
Time _t;
};
对于以上代码,当我们要实例化一个Test类的对象时,我们使用了初始化列表,在实例化过程中只调用了一次Time类的构造函数。
我们若是想在不使用初始化列表的情况下,达到我们想要的效果,就不得不这样写了:
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Test
{
public:
// 在构造函数体内初始化(不使用初始化列表)
Test(int hour)
{ //初始化列表调用一次Time类的构造函数(不使用初始化列表但也会走这个过程)
Time t(hour);// 调用一次Time类的构造函数
_t = t;// 调用一次Time类的赋值运算符重载函数
}
private:
Time _t;
};
这时,当我们要实例化一个Test类的对象时,在实例化过程中会先在初始化列表时调用一次Time类的构造函数,然后在实例化t对象时调用一次Time类的构造函数,最后还需要调用了一次Time类的赋值运算符重载函数,效率就降下来了。
四、成员变量在类中声明的次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后顺序无关
#include <iostream>
using namespace std;
int i = 0;
class Test
{
public:
Test()
:_b(i++)
,_a(i++)
{}
void Print()
{
cout << "_a:" << _a << endl;
cout << "_b:" << _b << endl;
}
private:
int _a;
int _b;
};
int main()
{
Test test;
test.Print(); //打印结果test._a为0,test._b为1
return 0;
}
代码中,Test类构造函数的初始化列表中成员变量_b先初始化,成员变量_a后初始化,按道理打印结果test._a为1,test._b为0,但是初始化列表的初始化顺序是成员变量在类中声明次序,所以最终test._a为0,test._b为1。
explicit关键字
构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还支持隐式类型转换。
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0) //单个参数的构造函数
:_year(year)
{}
void Print()
{
cout << _year << endl;
}
private:
int _year;
};
int main()
{
Date d1 = 2021; //支持该操作
d1.Print();
return 0;
}
上述代码Date d1=2021等价于:
Date tmp(2021); //先构造
Date d1(tmp); //再拷贝构造
所以在早期的编译器中,当编译器遇到Date d1 = 2021这句代码时,会先构造一个临时对象,再用临时对象拷贝构造d1;但是现在的编译器已经做了优化,当遇到Date d1 = 2021这句代码时,会按照Date d1(2021)这句代码处理,这就叫做隐式类型转换。
内置类型的隐式转换:
int a = 10;
double b = a; //隐式类型转换
在这个过程中,编译器会先构建一个double类型的临时变量接收a的值,然后再将该临时变量的值赋值给b。这就是为什么函数可以返回局部变量的值,因为当函数被销毁后,虽然作为返回值的变量也被销毁了,但是隐式类型转换过程中所产生的临时变量并没有被销毁,所以该值仍然存在。
但是,对于单参数的自定义类型来说,Date d1 = 2021这种代码的可读性不是很好,我们若是想禁止单参数构造函数的隐式转换,可以用关键字explicit来修饰构造函数。
static静态成员
概念
声明为static的类成员成为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外初始化。
特性
一.静态成员为所有类对象共享,不属于某个类共享。
class test
{
public:
test(int a = 10)
:_a(a)
{
cout << sizeof(test) << endl;
}
private:
static int _n;
int _a;
};
int main()
{
test t;
}
运行结果:
为什么计算结果是4而不是8?
answer:因为静态成员_n存储在静态区,它属于整个类,而不是属于某个对象。
二.静态成员变量必须在类外定义,定义时不添加static
class Test
{
private:
static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;
注意:这里静态成员变量_n虽然是私有,但是我们在类外突破类域直接对其进行了访问。这是一个特例,不受访问限定符的限制,否则就没办法对静态成员变量进行定义和初始化了。
三.静态成员函数没有this指针,不能访问任何非静态成员
class Test
{
public:
static void Fun()
{
cout << _a << endl; //error不能访问非静态成员
cout << _n << endl; //correct
}
private:
int _a; //非静态成员
static int _n; //静态成员
};
注意:含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量。
四.访问静态成员变量的方法
1.当静态成员变量为公有时:
#include <iostream>
using namespace std;
class Test
{
public:
static int _n; //公有
};
// 静态成员变量的定义初始化
int Test::_n = 0;
int main()
{
Test test;
cout << test._n << endl; //1.通过类对象突破类域进行访问
cout << Test()._n << endl; //3.通过匿名对象突破类域进行访问
cout << Test::_n << endl; //2.通过类名突破类域进行访问
return 0;
}
2.当静态成员为私有时:
#include <iostream>
using namespace std;
class Test
{
public:
static int GetN()
{
return _n;
}
private:
static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;
int main()
{
Test test;
cout << test.GetN() << endl; //1.通过对象调用成员函数进行访问
cout << Test().GetN() << endl; //2.通过匿名对象调用成员函数进行访问
cout << Test::GetN() << endl; //3.通过类名调用静态成员函数进行访问
return 0;
}
五、静态成员和类的普通成员一样,也有public、private和protected这三种访问级别
所以当静态成员变量设置为private时,尽管我们突破了类域,也不能对其进行访问。
注意区分两个问题:
1、静态成员函数可以调用非静态成员函数吗?
2、非静态成员函数可以调用静态成员函数吗?
问题1:不可以。因为非静态成员函数的第一个形参默认为this指针,而静态成员函数中没有this指针,故静态成员函数不可调用非静态成员函数。
问题2:可以。因为静态成员函数和非静态成员函数都在类中,在类中不受访问限定符的限制。
友元
友元分为友元函数和友元类。友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
我们都知道C++的<<和>>很神奇,因为它们能够自动识别输入和输出变量的类型,我们使用它们时不必像C语言一样增加数据格式的控制。实际上,这一点也不神奇,内置类型的对象能直接使用cout和cin输入输出,是因为库里面已经将它们的<<和>>重载好了,<<和>>能够自动识别类型,是因为它们之间构成了函数重载。
所以,我们若是想让<<和>>也自动识别我们的日期类,就需要我们自己写出对应的运算符重载函数
class Date
{
// 友元函数的声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// <<运算符重载
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day<< endl;
return out;
}
// >>运算符重载
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
注意:其中cout是ostream类的一个全局对象,cin是istream类的一个全局变量,<<和>>运算符的重载函数具有返回值是为了实现连续的输入和输出操作。
友元函数说明:
1、友元函数可以访问类是私有和保护成员,但不是类的成员函数。
2、友元函数不能用const修饰。
3、友元函数可以在类定义的任何地方声明,不受访问限定符的限制。
4、一个函数可以是多个类的友元函数。
5、友元函数的调用与普通函数的调用原理相同。
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中非公有成员。
class A
{
// 声明B是A的友元类
friend class B;
public:
A(int n = 0)
:_n(n)
{}
private:
int _n;
};
class B
{
public:
void Test(A& a)
{
// B类可以直接访问A类中的私有成员变量
cout << a._n << endl;
}
};
友元类说明:
1、友元关系是单向的,不具有交换性。
例如上述代码中,B是A的友元,所以在B类中可以直接访问A类的私有成员变量,但是在A类中不能访问B类中的私有成员变量。
2、友元关系不能传递。
如果A是B的友元,B是C的友元,不能推出A是C的友元。
内部类
概念
如果一个类定义在另一个类的内部,则这个类被称之为内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何的访问权限。
注意:内部类就是外部类的友元类,内部类可以通过外部类的对象来访问外部类的成员,但是外部类不是内部类的友元。
特性
1、内部类可以定义在外部类的public、private以及protected这三个区域中的任一区域。
2、内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public: //内部类——跟A是独立的,只是受A的类域限制
void func(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;//静态成员初始化
int main()
{
A::B b;
b.func(A());//A()为匿名对象
return 0;
}
3、外部类的大小与内部类的大小无关。
#include <iostream>
using namespace std;
class A //外部类
{
public:
class B //内部类
{
private:
int _b;
};
private:
int _a;
};
int main()
{
cout << sizeof(A) << endl; //外部类的大小
return 0;
}
这里的大小为4字节。
匿名对象
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
int main()
{
A aa1;
// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
//A aa1();
// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
A();
A aa2(2);
// 匿名对象在这样场景下就很好用
Solution().Sum_Solution(10);
return 0;
}
拷贝对象时的一些编译优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝。
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;
};
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
int main()
{
// 传值传参
A aa1;
f1(aa1);
cout << endl;
// 传值返回
aa1 = f2();
cout << endl;
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
cout << endl;
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << endl;
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
cout << endl;
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
cout << endl;
return 0;
}