本篇文章是C++类和对象讲解的第三篇,将对前两篇未提及的知识进行收尾。如果你还没有看过前两篇文章的话,请点击这里(第一篇、第二篇)。如果你已经看完了这两篇文章,你应该会觉得,某种意义上来讲,类和对象的知识也许称不上难,或者说难在杂乱。而本篇文章的知识似乎使杂乱度更上一层楼了。不过希望我对这些知识的整理能帮助你更好的理解这部分知识。
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class date
{
public:
date(int year, int month, int day)
{
//给成员变量赋值
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象已经拥有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体内的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次(定义时),而构造函数体内可以多次赋值。
那么一个类对象真正的定义初始化是在什么时候的呢?就在接下来提到的初始化列表当中。
初始化列表使用格式:在构造函数函数名与函数体(**{}
**)之间,以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式。
class date
{
public:
date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
//初始化成员变量
{}
private:
int _year;
int _month;
int _day;
};
在讲初始化列表之前,我提到过,类对象缺少一个真正的定义初始化的地方,构造函数并不是初始化的地方,而是给成员变量赋值,或者做一些其他处理的地方。
int n;//定义,但并未初始化,此时是随机值
n = 0;//赋值,不是初始化
int x = 0;//定义并初始化为0
而对于由**const
**修饰的类型以及引用类型,定义时初始化是其唯一的赋值机会,所以需要初始化列表来解决像这样的问题(也许设计C++的大佬一开始在设计构造函数时并没有考虑到这)。
class date
{
public:
date(int year, int month, int day)
{
//在构造函数体内给const修饰的成员变量赋值会导致编译不通过
_a = 0;//error C2789: “date::_a”: 必须初始化常量限定类型的对象
}
private:
int _year;
int _month;
int _day;
const int _a;
};
除此之外,在上一篇文章中,我提到过内置类型成员在类中声明时可以给默认值,这个默认值相当于与函数的缺省参数,只不过上一篇没讲初始化列表,我没提。因为叫“缺省”,意味着有“传参“的地方,而初始化列表就是这个“传参”的地方。所以那里的默认值可以给的那么的“花哨随意”,可以调用函数使用返回值。本质其实是初始化列表的缺省,在初始化时,成员变量也和普通内置类型的变量一样,可以使用值初始化,也可以调用函数并使用其返回值初始化。
int* Create()
{
return new int;
}
class date
{
public:
date()
:_year((int*)malloc(sizeof(int)))
,_month(new int)
,_day(Create())
{}
private:
int* _year;
int* _month;
int* _day;
};
同样是在上一篇文章中,提到过编译器生成的构造函数,拷贝构造函数对于自定义类型成员的处理是调用其对应的构造函数和拷贝构造函数,其实这也是通过初始化列表调用的。而在这里也留着一个坑。
这个坑就是,当编译器默认生成的构造函数处理自定义类型成员变量时,如果该类没有默认构造函数(无参或者全缺省的构造函数)时,会编译不通过。而你想自己写一个构造函数处理这个问题时,发现如果你想解决这个问题,你就必须手动调用这个自定义类型成员的构造函数,而你想调用这个构造函数就必须要在这个自定义类型成员定义初始化时调用。而你是无法在构造函数函数体内解决这个问题的。
class Time
{
public:
Time(int t)
:_t(t)
{}
private:
int _t;
};
class date
{
public:
private:
int _year;
int _month;
int _day;
Time _T;
};
int main()
{
date d;
//error C2280: “date::date(void)”: 尝试引用已删除的函数
//message : 编译器已在此处生成“date::date”
//message : “date::date(void)”: 由于 数据成员“date::_T”不具备相应的 默认构造函数 或重载解决不明确,因此已隐式删除函数
return 0;
}
所以,在这里,初始化列表又派上用场了。直接在初始化列表对自定义类型成员赋值就可以调用其构造函数。
class Time
{
public:
Time(int t)
:_t(t)
{}
private:
int _t;
};
class date
{
public:
date()
:_T(0)
{}
private:
int _year;
int _month;
int _day;
Time _T;
};
int main()
{
date d;
return 0;
}
当然,编译器生成的拷贝构造函数,对自定义类型调用其拷贝构造函数也是通过初始化列表调用的。
到这里你会发现,其实有些情况几乎是必定只能通过初始化列表来解决的。而在之前的两篇文章中均没有使用过初始化列表,但是照样编译通过,运行正常。难道编译器能自动识别什么时候需要走初始化列表,什么时候不需要,还是说是根据程序员自己的实现来检查?来实验一下。
class Time
{
public:
Time(int t = 0)
:_t(t)
{
cout << "Time(int t = 0)" << endl;
}
private:
int _t;
};
class date
{
public:
date()
{}//没有显式写初始化列表
private:
int _year;
int _month;
int _day;
Time _T;
};
int main()
{
date d;//创建对象
return 0;
}
运行截图:
事实上,无论你使不使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。事实上,初始化列表全称叫做构造函数初始化列表,也就是说初始化列表是构造函数的一部分,无论时显式还是隐式,成员变量总是需要初始化的,这是一个类对象创建必经的步骤。所以不使用初始化列表初始化自定义类型成员变量,有时会造成构造函数对该成员变量既初始化又重新赋值覆盖这样的低效的场景。当然,比起效率更重要的是,在像以上的场景中,必须使用到初始化列表。所以,建议尽量使用构造函数初始化列表。
以上就是对于初始化列表的基本介绍以及为什么要有初始化列表,接下来总结一下初始化列表的注意事项:
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
class date
{
public:
date()
:_year(0)
,_year(1)//error C2437: “_year”: 已初始化
{}
private:
int _year;
int _month;
int _day;
};
类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const
**成员变量**
自定义类型成员变量(且该类型没有默认构造函数时)
尽量使用初始化列表初始化。
从概念上讲,可以认为构造函数分两个阶段执行:
(1)初始化阶段(函数体之前);(2)普通的计算阶段。(函数体内)
常规地使用初始化列表,可以避免使用只能在初始化列表初始化的类成员时出现编译错误。有时也可避免一些效率问题。当然,抛开这些不谈,无论是类成员,还是普通地使用内置类型变量,尽量对变量初始化是一个良好的编程习惯。
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表的先后次序无关。
class date
{
public:
date()
:_day(1),_month(_day),_year(1970)
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d;
return 0;
}
运行截图:
所以,建议按照与成员声明一致的次序编写构造函数初始化列表,以及尽可能避免使用成员来初始化其他成员。
构造函数不仅可以构造与初始化,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
class date
{
public:
//1.单参构造函数,没有使用explicit修饰,具有类型转换作用
date(int year)
:_year(year)
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d(2022);
d = 2023;
return 0;
}
运行截图:
class date
{
public:
//1.单参构造函数,没有使用explicit修饰,具有类型转换作用
//date(int year)
//explicit修饰构造函数,禁止类型转换——explicit去掉之后,代码可以通过编译
explicit date(int year)
:_year(year)
{
cout << _year << "/" << _month << "/" << _day << endl;
}
//error C2679 : 二元“ = ” : 没有找到接受“int”类型的右操作数的运算符(或没有可接受的转换)
private:
int _year;
int _month;
int _day;
};
int main()
{
date d(2022);
d = 2023;
return 0;
}
class date
{
public:
//2.虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用
date(int year,int month = 1,int day = 1)
:_year(year),_month(month),_day(day)
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d(2022);
d = 2023;
return 0;
}
运行截图:
class date
{
public:
//2.虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用
//date(int year,int month = 1,int day = 1)
//使用explicit关键字修饰构造函数,禁止类型转换,代码无法通过
explicit date(int year,int month = 1,int day = 1)
:_year(year),_month(month),_day(day)
{
cout << _year << "/" << _month << "/" << _day << endl;
}
//error C2679 : 二元“ = ” : 没有找到接受“int”类型的右操作数的运算符(或没有可接受的转换)
private:
int _year;
int _month;
int _day;
};
int main()
{
date d(2022);
d = 2023;
return 0;
}
上述代码中编译器瞒着我们做了很多事,有时候我们并不希望这样,并且这样代码可读性也不是很好,那就使用**explicit
**修饰构造函数,禁止构造函数的隐式转换。
声明为**static
的类成员称为类的静态成员**。用**static
修饰的成员变量**,称之为静态成员变量,用**static
修饰的成员函数**,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
类名::静态成员
或者对象.静态成员
来访问。const static int成员变量可以缺省,仅此此类型可以在类中声明时给默认值(缺省值)。
//大佬设计想的用法
const static int N = 10;
int a[N];
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
先来个例子引入,以class date
为例,假设我要为这个类重载operator<<
成员函数,用于打印我想要的数据。但是因为**cout
**的输出流对象和隐含的**this
**指针在抢占第一个参数的位置。this
指针默认是类成员函数的第一个参数,也就是<<
左操作数固定为date
类对象了。但是实际上我们正常使用中cout
是左操作数,也就是需要作为重载函数的第一个形参,所以需要重载成全局函数,但是这又会导致类外没办法访问成员,此时就需要友元来解决。operator>>
同理。
硬要重载成类成员就会这样:
class date
{
public:
date(int year = 1970, int month = 1, int day = 1)
:_year(year), _month(month), _day(day)
{}
ostream& operator<<(ostream& out)
{
out << _year << "/" << _month << "/" << _day << endl;
return out;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d;
d << cout;//可读性何在?
//这样的用法非常怪,非常不合常规不是吗?
return 0;
}
运行截图:
而友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明式需要加friend关键字。
class date
{
friend ostream& operator<<(ostream& out, 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;
};
ostream& operator<<(ostream& out,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;
}
int main()
{
date d;
cin >> d;
cout << d << endl;
//两个操作符重载返回istream/ostream引用是为了能链式调用
return 0;
}
运行截图:
说明:
const
**修饰。友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
class Time
{
friend class date;
public:
private:
int _h;
int _m;
int _s;
};
class date
{
public:
date(int year = 1970, int month = 1, int day = 1)
:_year(year), _month(month), _day(day)
{
_t._h = _t._m = _t._s = 0;
//直接访问Time类对象的私有成员
}
private:
int _year;
int _month;
int _day;
Time _t;
};
友元类的几个特性:
date
类和Time
类,在Time
类中声明date
类为其友元类,那么可以在date
类中直接访问Time
类的私有成员,但无法在Time
类中访问date
类的私有成员。概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象来访问外部类中的所有成员。但是外部类不是内部类的友元。
class A
{
public:
A(int a1)
:_a1(a1)
{}
class B
{
public:
void func(const A& a)
{
cout << a._a1 << endl;
cout << _a2 << endl;
}
};
private:
int _a1;
static int _a2;
};
int A::_a2 = 0;
int main()
{
A a(10);
A::B b;//B类在A类域中,所以只能通过A类域找B
b.func(a);
return 0;
}
运行截图:
特性:
public
、protected
、private
都是可以的。static
成员,不需要外部类的对象/类名。sizeof(外部类)=外部类
,和内部类没有任何关系。class A
{
public:
A(int a1 = 0)
:_a1(a1)
{
cout << "A(int a1 = 0)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a1;
};
class solution
{
public:
void funtion(int a)
{
//...
}
private:
};
int main()
{
//A a1();
//不能这么定义对象,因为编译器无法识别下面是一个函数声明还是对象定义
//但是可以这样定义匿名对象,匿名对象的特点是不用取名字
//且声明周期只有这一行,我们可以看到下一行他就会自动调用析构函数
A();
cout << "i'm flag.Destructor was called before the end of program." << endl;
//匿名对象在以下场景很好用,其他也有,详细看我之后的文章
solution().funtion(100);
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);
cout << endl;
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << endl;
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
cout << endl;
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
cout << endl;
return 0;
}
运行截图:
以上就是类和对象最后一部分知识的讲解了,希望能帮助到你的C++学习。如果你觉得做的还不错的话请点赞收藏加分享,当然如果发现我写的有误或者有建议给我的话欢迎在评论区或者私信告诉我。