在类与对象上篇我们讲解了类的基础框架,中篇我们讲解了类的基本内容,下篇我们将补充类的一些零散知识点。
构造函数:创建类对象时自动调用,给对象中各个成员变量一个合适的初始值。
我们都知道,有一些对象,在定义时就必须初始化,如:
代码示例:
class A
{
public:
A(int a)
{
_a = a;
}
private:
int _a;
};
class B
{
public:
B(int a,int &ref,int n)
{
_aobj(a);
_ref = ref;
_n = n;
}
private:
A _aobj;//没有默认成员函数的自定义类型
int& _ref;//引用变量
const int _n;//const变量
};
这就需要知道构造函数的初始化列表,初始化列表可以理解成对象成员变量定义的位置,引用变量、const变量、没有默认构造函数的自定义类型变量都必须在定义时就初始化,在构造函数体中的赋值时变量已经创建好了,这时的赋值只能将其称为赋初值,而不能称为初始化,所以编译报错。
初始化与默认初始化:
构造函数的函数体:
初始化列表:是成员变量定义的地方,以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每一个“成员变量”后面跟一个放在括号中的初始值或表达式。
代码示例:
class A
{
public:
A(int a)
{
_a = a;
}
private:
int _a;
};
class B
{
public:
//初始化列表:成员变量定义的地方
B(int a, int& ref, int n) :_aobj(a), _ref(ref), _n(n)
{}
private:
//这里只是声明
//以下三个成员变量都有一个特征:在成员定义时就必须初始化!
A _aobj;//没有默认成员函数的自定义类型
int& _ref;//引用变量
const int _n;//const变量
};
int main()
{
//B b(1, 2, 3);//编译报错,因为第二个实参涉及引用权限放大
int a = 2;
//对象整体定义的地方,在创建时自动调用构造函数给成员变量赋初值
B b(1, a, 3);
return 0;
}
tip:
class A
{
public:
A(int a = 0)
{
_a = a;
}
private:
int _a;
};
class B
{
public:
//初始化列表:不显示初始化,如果成员有缺省值,使用缺省值初始化
B()
{}
//初始化列表:显示初始化,使用指定的值初始化,不使用缺省值
B(int a,int n):_aobj(a),_n(n)
{}
private:
A _aobj;//没有默认成员函数的自定义类型
const int _n = 1;//const变量,给该成员变量提供一个缺省值,在初始化列表没有显示初始化时,就会使用该缺省值
};
int main()
{
B b1;
B b2(10, 10);
return 0;
}
class B
{
public:
//初始化列表:先使用_a1给_a2初始化,再使用a给_a1初始化
B(int a)
:_a1(a), _a2(_a1)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
B b(1);
b.Print();
return 0;
}
//代码分析:
//A、输出1 1
//B、程序崩溃——数组越界,野指针等严重情况才会
//C、编译不通过——语法错误
//D、输出1 随机值
//正确选项:D
回顾给成员变量内置类型给定缺省值:
代码示例:在堆区开辟一个二维数组
class A
{
public:
A(int row = 10, int col = 10)
:_row(row), _col(col)
{
//指针数组
_a = (int**)malloc(sizeof(int*) * row);
//判断是否开辟失败
if (nullptr == _a)
{
//开辟失败,提示并退出
perror("malloc");
exit(-1);
}
//循环,指针数组的每一个元素指向一维数组
for (int i = 0; i < row; i++)
{
_a[i] = (int*)malloc(sizeof(int) * col);
if (nullptr == _a[i])
{
//开辟失败,提示并退出
perror("malloc");
exit(-1);
}
}
}
private:
int** _a;
int _row;//行
int _col;//列
};
构造函数不仅可以初始化对象,如果构造函数只接受一个参数,还具有隐式类型转换的作用。
代码示例:
class Date
{
public:
//1、单参构造函数,具有类型转换作用
//Date(int year = 2024)
// :_year(year)//建议使用初始化列表初始化,虽然内置类型在函数体也可以完成赋初值的操作
//{}
//2、对于多个参数的构造函数,
//(1)所有参数都设缺省值——全缺省
//(2)除第一个参数无默认值其余参数均有默认值
//具有类型转换作用,建议使用第一种
//注意:两者不构成重载,只能存在一种
Date(int year = 2001, int month = 1, int day = 1)
:_year(year),_month(month),_day(day)
{
//观察是否调用构造函数
cout << "Date(int year = 2001, int month = 1, int day = 1)" << endl;
}
//拷贝构造
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;//观察是否调用拷贝构造函数
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1 = 2001;//隐式类型转换,整形转换成自定义类型
//语法:①2001先构造一个Date的临时对象;②临时对象再拷贝构造d1
//tip:同一个表达式连续的构造+拷贝构造,一般都会优化成直接构造
//编译器会优化成调用直接构造,怎么证明隐式转化的发生呢?
//答案是:利用临时对象具有常性与引用权限的特点证明会发生隐式转换
//Date& d2 = 2001;//error C2440: “初始化”: 无法从“int”转换为“Date &”
const Date& d2 = 2001;
return 0;
}
tip:
//编译器每次只能执行一种类类型的转换
class A
{
public:
//支持字符、整形、浮点型、布尔型隐式转换为A类型
A(double d)
:_d(d)
{
cout << "A(double d)" << endl;
}
private:
double _d;
};
class B
{
public:
//支持A类型隐式转换为B类型
B(A a)
:_a(a)
{
cout << "B(A a)" << endl;
}
private:
A _a;
};
void func(const B& a)
{}
int main()
{
//错误示例:
//func(1);// error C2664: “void func(const B &)”: 无法将参数 1 从“int”转换为“const B &”
//因为编译器每次只能执行一种类类型的转换,所以需要我们定义;两种转换
//1、把整形1转换成A
//2、再把这个A转换成B
//正确示例1:显示地转换成A,隐式转换成B
func(A(1));
//正确示例2:隐式转换成A,显示转换成B
func(B(1));
return 0;
}
explicit修饰构造函数,禁止隐式类型转换。
tip:
代码示例:
class Date
{
public:
//explicit修饰构造函数,禁止隐式类型转换
explicit Date(int year = 2001, int month = 1, int day = 1)
:_year(year), _month(month), _day(day)
{
//观察是否调用构造函数
cout << "Date(int year = 2001, int month = 1, int day = 1)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//Date d1 = 2001;//报错,因为explicit修饰构造函数,禁止隐式类型转换
Date d1 = (Date)2001;//强制类型转换
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;
// 传值返回
A ra;
ra = f2();
cout << endl;
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(1);
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << endl;
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
cout << endl;
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
cout << endl;
return 0;
}
tip:
面试题:实现一个类,计算程序中创建出了多少个类对象。
思路:
综上我们可以使用全局变量来计算程序创建出了多少个类对象,但是全局变量有一个缺点——没有封装,任何地方都可以随意改变,不安全。
所以这个时候就得引入静态成员了。
声明为static的类成员称为类的静态成员
class A
{
public:
A()
{
++_scount;
}
A(const A & t)
{
++_scount;
}
~A()
{
--_scount;
}
//静态成员函数
static int GetACount()
{
//tip:1、静态成员函数没有隐藏的this指针,不能访问任何非静态成员
//2、因为静态成员变量一般定义在private下,类外无法访问,只能通过静态成员函数访问,所以静态成员函数常与静态成员变量配套使用,
return _scount;
}
private:
//成员变量属于类对象,存储在对象里面
int _a = 1;//缺省值给初始化列表使用
//静态成员变量属于所有类对象所共享,不属于某个具体的对象。存放在静态区
//tip:
// 1、一般情况下,静态成员变量不能给缺省值,因为缺省值是给初始化列表用的,初始化列表是初始化对象的成员,静态成员变量不属于类的任何一个对象。
// 2、静态成员也是类的成员,受访问限定符的限制
// (1)静态成员变量建议定义在private下,将其封装在类中,不能在类外随意改变,安全
// (2)静态成员函数建议定义在public下,与静态成员变量配套使用
static int _scount;
};
//静态成员必须在类的外部定义和初始化
//tip:
// 1、当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句
// 2、定义静态成员变量的方式与在类外部定义成员函数类似,需要指定对象的类型名,然后是类名,作用域运算符以及成员自己的名字
// 3、一个静态成员变量只能定义一次,为了确保只定义一次,建议把静态成员的定义与其他非内联函数的定义放在同一个文件
int A::_scount = 0;
int main()
{
//静态成员可以通过类名::静态成员或者对象.静态成员来两种方式访问
//静态成员只要能突破类域和访问限定符就可以访问
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
return 0;
}
tip:
问题:
1. 求1+2+……+你,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句
思路:每创建一个对象都需要调用构造函数,所以可以在构造函数体中累加计算。
代码示例:
class Sum
{
public:
Sum()
{
_ret += _i;
_i++;
}
static int GetRet()
{
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
//创建n个对象的数组(支持变长数组),就会调用n次构造
Sum a[n];
return Sum::GetRet();
}
};
思路:先将构造函数定义在private下,在设计两个函数分别在栈与堆上创建对象,在类外通过调用这两个函数来创建栈或者堆的对象
问题:在类外调用成员函数时,需要通过对象来调用,所以这个时候产生了先有鸡还是先有蛋的问题——为了解决该问题将其设计为静态成员函数
代码示例:
class A
{
public:
static A GetStackObj()
{
//创建一个栈上的对象
A aa;
//出了函数体aa销毁,所以只能值返回
return aa;
}
static A* GetHeapObj()
{
return new A;//new在堆区创建对象,后面我们会讲解
}
private:
//将构造函数定义在private下,类外不能随意创建对象
A()
{}
//成员变量
int _a = 1;
};
int main()
{
A::GetStackObj();
A::GetHeapObj();
return 0;
}
在开发过程中,在类外有些时候需要我们能访问到类中的私有成员,比如输入输出的重载函数。
输入输出运算符必须是非成员函数: 因为输入输出运算符的左操作数分别是istream和ostream,而成员函数的左操作数是隐含的this指针,所以输入输出运算符必须是非成员函数。
问题: 在类外想要访问类中的私有成员,就需要突破类的封装,突破类封装的方式有如下两种:
tip: 虽然友元可以突破封装,提供了便利。但是友元会增加耦合度(即两者关系更紧密了,例如:类中的成员改变,类外也要随之改变),破坏封装,所以友元不建议多用。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但是需要在类的内部声明,声明时需要加friend关键字。
代码示例:
class Date
{
//声明operator<<和operator>>这两个函数为Date类的友元类
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
//tip:输入输出运算符重载,不能是类的成员函数,需要定义在类外
/*ostream & operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}*/
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
//<<运算符从左向右结合,可以连续打印,所以要返回ostream的形参
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
//输入运算符必须处理输入可能失败的情况
int year, month, day;
_cin >> year >> month >> day;
if (_cin)
{
//输入成功
d._year = year;
d._month = month;
d._day = day;
}
else
{
//输入失败,提示并断言
cout << "输入失败" << endl;
assert(false);
}
return _cin;
}
int main()
{
Date d1;
cout << d1 << endl;
cin >> d1;
cout << d1 << endl;
return 0;
}
tip:
补充:
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
代码示例:
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;
};
tip:
友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。
所以为了使友元对类的用户可见,建议把友元的声明与类本身放置在同一个头文件中(类的外部)。
**tip:**一些编译器允许在尚未友元函数的初始声明的情况下就调用它。但是建议还是提供一个独立的函数声明,提高可移植性。
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
代码示例:
class A
{
private:
static int k;
int h;
public:
class B // 内部类是外部类的天生友元
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
//1、sizeof(外部类) = 外部类,大小和内部类无关,因为类实例化之后才占空间
cout << sizeof(A) << endl;
//要创建B的对象,必须要突破外部域和访问限定符
A::B b;
b.foo(A());
return 0;
}
tip:
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
void func()
{}
private:
int _a;
};
int main()
{
//1、匿名对象的生命周期在当前行
A aa1;//有名对象——生命周期在当前函数局部域
A();//匿名对象——生命周期在当前行
aa1.func();
//匿名对象不传参也要带括号,因为类型不能调用函数,需要对象来调用函数
A().func();
//2、匿名对象具有常性
//A& ra = A();//报错
//3、const引用延长匿名对象的生命周期,生命周期在当前函数局部域
const A& ra = A();
aa1.func();
return 0;
}
tip: