作者主页:lightqjx
本文专栏:C++
目录
前言
本章是基于前两章:认识类和对象和类中的6个默认成员函数后续的关于类和对象的知识认识
一、构造函数
构造函数来初始化成员有两种方式:1.构造函数体赋值;2. 初始化列表
1. 构造函数体赋值
函数体赋值即:在创建对象时,编译器自动调用(默认)构造函数来给对象中各个成员变量一个合适的初始值。如以下代码:
class Date
{
public:
Date(int year, int month, int day)
{
//函数体内赋值
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
2. 初始化列表
(1)基本概念
当我们写了一个类,实际上是对其中的成员变量的声明(C++11中可以加缺省值,但还是声明,缺省值其实是给构造函数中的初始化列表用的),当进行实例化时,其实是对对象的整体定义,而其中对象的成员定义的位置就是在初始化列表中的。
初始化列表也是用来初始化的,它的使用格式是:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。代码使用如以下所示:
class Date
{
public:
//初始化列表
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{ }
private:
int _year;
int _month;
int _day;
};
(2)使用特性
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下三种成员,必须放在初始化列表位置进行初始化:
(1)引用成员变量;
(2)const成员变量;
(3)自定义类型成员(且该类没有默认构造函数时)- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
对各个特性的解释:
- 每个成员变量在初始化列表中只能出现一次
只能出现一次,否则会报错,如以下代码所示:
- 类中包含以下三种成员,必须放在初始化列表位置进行初始化:
(1)引用成员变量;
(2)const成员变量;
(3)自定义类型成员(且该类没有默认构造函数时)
因为对于引用成员变量和const成员变量这两种成员是必须在定义的时候就初始化,否则会报错,如果是在函数体内赋值进行初始化也是会报错的;而对于没有默认构造函数的自定义类型,如果自定义类型的类没有默认构造函数若也不在初始化列表中初始化时,也会导致编译错误。
#include <iostream>
using namespace std;
class A
{
public:
A(int a)
:_a(a)
{ } //这不是默认的构造函数
private:
int _a;
};
class B
{
public:
// 初始化列表
B(int a, int& ref)
: _ref(ref)
, _n(10)
,_aobj(a)
{ }
private:
int& _ref; // 引用
const int _n; // const
A _aobj; // 没有默认构造函数的自定义类型
};
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
不写初始化列表,对于内置类型和自定义类型的处理如下:
- 如果不写初始化列表,则对于内置类型,如果有缺省值,则会使用缺省值进行初始化,如果没有缺省值,则就会不做处理,为随机值;
- 如果不写初始化列表,则对于自定义类型成员,若有默认构造函数,则会调用它的默认构造函数;如果没有默认构造函数,或者默认构造函数不可访问,则会导致编译错误。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序
如图所示:
3. explicit关键字
构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。接收单个参数的构造函数具体表现:
- 构造函数只有一个参数
- 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
- 全缺省构造函数
补充知识点:隐式类型转换:
在学习C语言时,我们知道当数据丛一个类型转化从另一个类型时,就会发生隐式类型转换:
int i = 10; double d = i; // 从int类型转换成double类型
而在类类型中也可以进行隐式类型转换的。
#include <iostream> using namespace std; class A { public: //构造函数只有一个参数 - 支持隐式类型转换 A(int year) :_year(year) { cout << "A(int year)" << endl; } private: int _year; }; int main() { // 我们这里只有一个成员 // 这是我们常用的形式来初始化一个对象,调用的构造函数 A aa1(2025); // 另一种形式 --- 隐式类型转换 // 本来这里是调用了两次构造函数的:首先用2026来构造一个A的临时对象,临时对象再拷贝构造给aa2 // 但是在C++编译器中,多个构造会被优化,所以这里经过优化,直接就是一个构造了 // 即:用2026直接构造了一个aa2对象 A aa2 = 2026; return 0; }
编译器的优化可以通过运行观察调用构造函数的次数看验证。
为了验证产生了临时对象,可以看以下代码:
#include <iostream> using namespace std; class A { public: //构造函数只有一个参数 - 支持隐式类型转换 A(int year = 1) :_year(year) { cout << "A(int year = 1)" << endl; } private: int _year; }; int main() { // A& aa3 = 2026; //这条语句编译器会报错:“初始化”: 无法从“int”转换为“A &” //但是加上const就没有问题了 const A& aa3 = 2026; return 0; }
这里思考为什么加上const就可以了?因为隐式类型转换产生的临时对象具有常性,需要用const修饰才能进行引用,这也反映了隐式类型转换中是产生临时变量的,将 int 类型的 2026 隐式转换为一个临时的 A 对象,只有通过常引用才能绑定这个临时对象。
如果不想要这个隐式类型转换,就可以在构造函数的前面加上关键字 explicit ,这样就无法进行隐式类型转换了。需要注意隐式类型转换只适用于是接收单个参数的构造函数,有三种情况,如以下代码:
情况1:构造函数只有一个参数
#include <iostream>
using namespace std;
class Date
{
public:
//构造函数只有一个参数 --- 支持隐式类型转换
Date(int year)
:_year(year)
{
cout << "A(int year)" << endl;
}
/*//explicit修饰构造函数, 不支持隐式类型转换
explicit Date(int year)
:_year(year)
{
cout << "A(int year)" << endl;
}*/
private:
int _year;
};
int main()
{
Date d1 = 2025;
return 0;
}
情况2:构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
#include <iostream>
using namespace std;
class Date
{
public:
//构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值 --- 支持隐式类型转换
Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{ }
/*//explicit修饰构造函数, 不支持隐式类型转换
explicit Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{ }*/
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1 = 2025;
return 0;
}
情况3:全缺省构造函数
#include <iostream>
using namespace std;
class Date
{
public:
//全缺省构造函数 --- 支持隐式类型转换
Date(int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{ }
/*//explicit修饰构造函数, 不支持隐式类型转换
explicit Date(int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{ }*/
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1 = 2025;
return 0;
}
二、static成员
1. 概念
声明为static的类成员称为类的静态成员,存储在静态区。
- 用static修饰的成员变量,称之为静态成员变量;
- 用static修饰的成员函数,称之为静态成员函数。
静态成员变量一定要在类外进行初始化。
2. 特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区(全局数据区),不会被 sizeof 计算在内。
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
其初始化代码如下:
class MyClass { public: //...... private: static int a; //静态成员变量 - 声明 }; //静态成员变量必须在类外面初始化 int MyClass::a = 2025;
- 类的静态成员在类外可用 类名::静态成员 或者 对象.静态成员 来访问;在类内部直接通过 成员名 访问
代码示例:#include <iostream> using namespace std; class MyClass { public: void PrintA() { cout << a << endl; // 直接访问,无需类名::或对象. } static void Print() { cout << a << endl; // 静态成员函数也可以直接访问静态变量 cout << "static void Print()" << endl; } private: static int a; //静态成员变量 }; //静态成员变量必须在类外面定义 int MyClass::a = 2025; int main() { MyClass d1; d1.PrintA(); // 调用静态函数的两种方式 MyClass::Print(); //类名::静态成员 d1.Print(); //对象.静态成员 return 0; }
- 静态成员函数 没有隐藏的this指针,不能访问任何非静态成员。
所以静态成员函数可以调用非静态成员函数,但是非静态成员函数却不可以调用类的静态成员函数。
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
3. 应用
记录计算程序中创建出了多少个类对象。如以下代码所示:
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass()
{
_count++; // 每创建一个对象,计数加1
}
~MyClass()
{
_count--; // 每销毁一个对象,计数减1
}
static int GetACount() // 受访问限定符限制,只能这样访问静态成员变量
{
return _count;
}
private:
static int _count; // 静态成员变量,记录对象数量
};
int MyClass::_count = 0; // 初始化静态成员变量
void Func()
{
MyClass d2;
cout << "当前对象个数: " << MyClass::GetACount() << endl;
}
int main()
{
cout << "当前对象个数: " << MyClass::GetACount() << endl;
MyClass d1;
Func();
cout << "当前对象个数: " << MyClass::GetACount() << endl;
MyClass d2;
MyClass d3;
cout << "当前对象个数: " << MyClass::GetACount() << endl;
return 0;
}
三、友元
友元提供了一种突破封装的方式,有时提供了便利,但是友元会增加耦合度,也破坏了类的封装,所以友元不宜多用。友元分为:友元函数和友元类。
1. 友元函数
在C++中,友元函数是一种特殊的函数,它不是该类的成员函数,但它能够访问类的私有和保护成员。下面是关于友元函数的说明:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
使用方法:
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加 friend 关键字。
应用场景:类中的流插入运算符(<< ),流提取运算符(>> )重载
C++中,实际上 cout 和 cin 分别是 ostream 和 istream 类的对象
如果对于一个类,想要直接使用流插入和流提取运算符来输入输出一个类的对象中的数据,通常可以时需要再类中写一个这样的运算符重载的成员函数来实现,但是这样做的话就会有一些问题,由于成员函数有一个在一个为形参位置隐含了一个this指针,那么我们在写类的输出代码时,对象的名称必须放在<<的左侧,会看起来比较别扭,如以下代码:
#include <iostream>
using namespace std;
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(2025, 8, 30);
// <<的左边是第一个参数,<<的右边是第二个参数
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
d1 << cout; // 相当于 d1.operator<<(&d1, cout);
return 0;
}
所以,如果不想这样,就不能将这个运算符重载函数设为成员函数。可以将它设为一个全局函数,这样就可以控制参数传递的顺序了。但是全局函数无法访问类中的私有成员,即成员变量无法访问。因此只要将这个函数设为这个类的友元函数就可以了,使用方法如下:
#include <iostream>
using namespace std;
class Date
{
// 使用friend来声明一下这个就可以了
friend ostream& operator<<(ostream& _cout, const Date& d);
public:
Date(int year, int month, int day)
: _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;
}
int main()
{
Date d1(2025, 8, 30);
cout << d1; // 相当于 operator<<(cout, &d1);
return 0;
}
其中的函数 “ ostream& operator<<(ostream& _cout, const Date& d) ”就称为Date类的友元函数。operator>>同理。
2. 友元类
在C++中,友元类是一种特殊的类关系机制,通过friend关键字声明后,允许一个类的所有成员函数访问另一个类的私有和保护成员。下面是一些公关与友元类的说明:
- 友元关系是单向的,不具有交换性。
比如下面的Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time中的私有成员class Time { friend class Date; //声明Date类为Time类的友元类,则在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 = 2025, int month = 8, int day = 30) : _year(year) , _month(month) , _day(day) { } void SetTimeOfDate(int hour, int minute, int second) { //直接访问Time类中私有的成员变量 _t._hour = hour; _t._minute = minute; _t._second = second; } private: int _year; int _month; int _day; Time _t; };
- 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递。
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
- 友元关系不能继承。
四、内部类
在C++中,内部类,也称为嵌套类,是指定义在另一个类内部的类。它能够访问外部类的成员(包括私有成员),但外部类无法直接访问内部类的私有成员(除非通过内部类的对象或友元关系)。
注意:内部类天生就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象或类名。
- sizeof(外部类) = 外部类,和内部类没有任何关系。但是如果在外部类中定义的内部类的对象,则就要加上B内部类的大小。
#include <iostream>
using namespace std;
class A // 外部类
{
public:
class B // B天生就是A的友元 --- 内部类
{
public:
void fun(const A& a)
{
cout << k << endl; // OK
cout << a.h << endl; // OK
}
private:
int a;
};
private:
static int k;
int h;
B _aaa;
};
int A::k = 1;
int main()
{
A::B b;
b.fun(A());
cout << sizeof(A) << endl;// 输出:8
return 0;
}
其中,sizeof()不会计算静态成员的大小
五、匿名对象
在 C++ 中,匿名对象是指没有显式命名的临时对象,通常在表达式中直接创建并使用,生命周期短暂(通常限于当前语句)。需要注意的有以下几点:
定义方式
class MyClass
{
public:
MyClass(int a = 1)
:_a(a)
{ }
private:
int _a;
};
int main()
{
MyClass aa1(2025); // 有名对象定义
MyClass(2025); // 匿名对象定义
//MyClass aa2(); // 有名对象时不能这么使用,因为会和函数声明冲突
MyClass(); // 但匿名对象这样是可以的
return 0;
}
匿名对象调用函数
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass(int a = 1)
:_a(a)
{ }
void Print()
{
cout << "void Print()" << endl;
}
private:
int _a;
};
int main()
{
MyClass().Print(); // 必须加括号
return 0;
}
生命周期规则
- 默认情况:匿名对象在当前语句结束时销毁。
class MyClass { public: MyClass(int a = 1) :_a(a) { } private: int _a; }; int main() { MyClass(40); // 构造后立即析构,生命周期只有当前这一行 return 0; }
- 延长生命周期:匿名对象具有常性,如果用
const
引用接住,会活到引用作用域结束。class MyClass { public: MyClass(int a = 1) :_a(a) { } private: int _a; }; int main() { const MyClass& ref = MyClass(50); // 匿名对象活到 ref 失效 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;
};
基于上述类的实现。
对于函数传值
调用传值函数:
调用传引用函数:
可以发现这里没有拷贝构造了,即传引用可以减少拷贝构造,提供效率。
需要注意它们两个是不构成函数重载的
这里还看不出优化。但基本知道了拷贝构造的使用时机。
对于函数传返回值。
如图运行结果对于不同的编译器会有不同的结果,优化程度也不同。这里使用的是VS2022,优化程度比较大,原本应该是Fun函数在返回时,会调用拷贝构造产生一个临时对象,然后临时对象再拷贝构造给ra的,而这里编译器做了优化,优化为一个拷贝构造了,即:
除了这种优化,还有以下情况:
void Fun1(A aa)
{ }
A Fun3()
{
A aa;
return aa;
}
int main()
{
A a1 = 1; // 隐式类型转换(构造+拷贝构造)-->优化为直接构造
Fun1(1); //隐式类型转换-->优化为直接构造
Fun1(A(1));//构造匿名对象+拷贝构造给形参-->优化为直接构造
A ra = Fun3();//aa先拷贝构造为临时对象,在拷贝构造给ra--->优化合二为一
return 0;
}
但是如以下的情况:
因为这样分开写了之后,对象已经有了,这里就变成了赋值,并不是构造,所以不会优化。
所以都建议构造、拷贝构造合在一起写,利于编译器优化。
感谢各位观看!希望能多多支持!