本文继续介绍与C++中与面向对象相关的内容,介绍了构造函数中的初始化列表、隐式类型转换、类的静态成员、友元、内部类、匿名对象以及编译器对拷贝构造的优化等概念。
在创建对象时,编译器通过调用构造函数给该对象中各个成员变量一个合适的初值。
class Data
{
public:
Data(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
不能将这一过程称为初识化,只能称为赋初值,因为初始化只能初始化一次,而构造函数的函数体内可以进行多次赋值。那么对象是在什么时候进行初始化的呢?
初始化对象是由初始化列表完成的。
以一个冒号开始,接着是以逗号为分割的数据成员列表,每个成员变量后面跟着一个放在括号中的初始值或者表达式。
class Data
{
public:
Data(int year, int month, int day)
: _year(year),//初始化列表
_month(month),
_day(day)
{
}
private:
int _year;
int _month;
int _day;
};
1.每个成员变量只能在初始化列表中出现一次(初始化只能初始化一次) 2.类中包含以下成员必须包含在初始化列表中:
const
成员变量
原因:const
变量只有一次初始化的机会,不能再改变class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int& ref)
:_n(10),
_ref(ref),
_aobj(a)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
3.尽量用初始化列表,因为不管是否显示使用初始化列表,对于自定义的成员变量,一定会先使用初始化列表进行初始化。
class Time
{
public:
Time(int hour = 0)//缺省值
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int day)
{}
private:
int _day;
Time _t;
};
int main()
{
Date d(1);
}
4.成员变量在类中的初始化顺序与初始化列表中的顺序无关,而是与该成员变量在类中的声明顺序有关。 观察下列代码,大家认为输出结果会是什么呢? A. 输出1 1 B.程序崩溃 C.编译不通过 D.输出1 随机值
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();
}
运行结果:
可以看到输出结果是D.1 随机值
为什么会出现这样的情况呢?
答:因为成员变量初始化的顺序是由它们在类中的声明顺序决定的,而不是初始化列表中的顺序。在未进行初始化之前_a1
和_a2
都是随机值,但是先初始化了_a2
,因此_a2
就被初始化为_a1
的随机值,然后初始化_a1
为1。
explicit
关键字(隐式类型转换)对于单个参数或者除第一个参数外其他参数都有缺省值的构造函数,有隐式类型转换的功能。 观察下面的代码:
class Date
{
public:
Date(int year, int month = 1, int day = 1)
:_year(year),
_month(month),
_day(day)
{}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2002);
Date d2 = 2023;//是否可以这样创建对象并进行初始化呢?
d1.Print();
d2.Print();
}
运行结果:
从运行结果我们可以看出,可以像上面的代码中Date d2 = 2023;
一样进行创建对象并进行初始化,为什么这样的代码可以实现呢?
这是因为在这个过程中发生了隐式类型转换:
当然,先进行直接构造再进行拷贝构造是之前的编译器对这种情况进行函数调用的顺序。现在的编译器会省略拷贝构造这一步优化为用2023进行直接构造。但是如果是下面这种情况就无法进行优化:
int main()
{
const Date& d2 = 2023;//引用的是中间的临时变量,因为临时变量具有常性,所以该对象为const对象(指针和引用的权限不能放大)
d2.Print();
}
但是这样代码的可读性会降低,如果我们不希望构造函数中存在隐式类型转换的情况,可以使用explicit
关键字禁止构造函数的隐式类型转换。
class Date
{
public:
// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
explicit Date(int year)//单参数构造函数
:_year(year)
{}
//explicit Date(int year, int month = 1, int day = 1)// 2. 多个参数构造函数,创建对象时后面的两个参数可以不传递
//: _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;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2022);//正常进行初始化
d1 = 2023;//使用explicit修饰构造函数,会禁止单参数的构造函数类型转换的功能
}
运行错误:
声明为static
的类成员称为静态类成员,用static
修饰的类成员变量称为静态成员变量,用static
修饰的类成员函数称为静态成员函数。
静态成员变量一定在类外进行初始化,静态成员函数中没有this指针
。
static
,类中只是声明;类名::类静态成员
或者类对象名.类静态成员
的方式来访问;public
、protect
、private
的访问限定符限制。友元提供了一种突破封装的方式,有时可以提供便利。但是友元会增加耦合度,破坏了封装,所有友元不宜多用。
友元分为友元函数和友元类。
问题:现在尝试去重载operator<<
,然后发现没办法将operator<<
重载成成员函数。因为cout
的输出流对象和隐含的this指针
在抢占第一个参数的位置。this指针
默认是第一个参数也就是左操作数了。但是实际使用中只有cout
是第一个形参对象时,才能正常使用。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2002,03,27);
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
cout << d1 << endl;//不能这样调用
d1 << cout << endl;// 但是这样调用d1 << cout;->d1.operator<<(&d1, cout); 不符合常规调用
}
为了可以常规调用<<
,则需要将operator<<
重载成全局函数,可以避免this指针
抢占第一个参数位。但这样又会导致无法访问类成员,此时就需要使用友元函数。(operator>>
同理)。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend
关键字。
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
const
修饰
因为友元函数没有this指针
,它是一个的类外的函数,不过可以访问类内的成员。如果想要在一个类中访问另一个类的成员,就需要将这个类声明为另一个类的友元类。 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
Time类
和Date类
,在Time类
中声明Date类
为其友元类,那么可以在Date类
中直接访问Time类
的私有成员变量,但想在Time类
中访问Date类
中私有的成员变量则不行。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;
};
如果一个类定义在另一个类里面。则这个类就叫做另一个类的内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问它,外部类对内部类没有任何特殊的访问权限(即,和其他类或对象的访问限制没有区别)。 但是,内部类天生就是外部类的友元类。友元类的概念参照上文,内部类可以通过外部类的对象参数访问外部类的所有成员,但是外部类不是内部类的友元。
public
、protect
、private
的限制中;sizeof(外部类)=外部类
,和内部类没有关系。
观察下面代码:class A
{
private:
static int k;
int h = 2;
public:
class B // B天生就是A的友元,但是A不是B的友元
{
public:
void func(const A& a)
{
cout << k << endl;//可以访问
cout << a.h << endl;//可以访问
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.func(A());
return 0;
}
运行结果:
一般情况下,我们定义一个对象都会给它一个对应的一个名字。特殊的,我们可以定义一个没有名字的对象——匿名对象。 这种对象的生命周期只有它定义所在的那一行,运行到下一行就会销毁,属于一次性的对象,所以不需要命名。 观察以下代码:
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;
// 传值返回
f2();
cout << endl;
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << endl;
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
cout << endl;
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
cout << endl;
return 0;
}
类是对某一类实体(对象)来进行描述的,描述该对象具有那 些属性,那些方法,描述完成后就形成了一种新的自定义类型,采用该自定义类型就可以实例化出具体的对象。
以上就是今天要讲的内容,本文介绍了构造函数中的初始化列表、隐式类型转换、类的静态成员、友元、内部类、匿名对象以及编译器对拷贝构造的优化等相关概念。本文作者目前也是正在学习C++相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。 最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!