你好,我是安然无虞。

文章目录

写在前面

前面我们已经学习过了关于类和对象的很多知识,今天我们就对类和对象部分进行收尾工作。

再谈构造函数

构造函数体赋值

在创建对象的时候,编译器通过调用构造函数,给对象中的各个成员变量一个合适的初始值。

class Date
{
public:
//构造函数体内初始化
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}

private:
int _year;
int _month;
int _day;
};

虽然上面的构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,因为构造函数体内的语句只能将其称作为赋初值,而不能称之为初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值
还有一个问题需要强调一下:

class Date
{
public:
...

private:
//这里的成员变量是定义吗?
int _year;
int _month;
int _day;
}

注意哦,类中的成员变量只是声明,并不是定义。定义的标志是为其开辟空间。
那成员变量在哪里定义的呢?
答案是:初始化列表可认为是对象的成员变量定义的地方

初始化列表

初始化列表:一个以冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或者表达式。
还是日期类,用初始化列表初始化:

//初始化列表可认为是对象的成员变量定义的地方
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}

当然了,函数体内初始化和初始化列表初始化二者也可以混着用:

Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
{
_day = day;
}

我们知道定义对象是这样的:

//对象定义,属于对象整体的定义,那对象里的成员什么时候定义呢?
Date d1(2022, 9,30);

上面一行代码属于对象整体的定义,那对象里的成员什么时候定义呢?我们可以认为初始化列表就是对象的成员变量定义的地方。
不过问题来了,有的成员变量必须在初始化列表初始化,而有的则不必,这是为什么呢,可以具体谈谈吗?
好的,请看:

class Date
{
public:
...

private:
int _year;
int _month;
int _day;
}

我们知道,日期类里的_year, _month, _day都是 int 类型,它们可以在定义的时候初始化,也可以在定义的时候不初始化,后面再赋值修改。
但是,有些成员变量必须在定义的时候初始化,如果把日期类改成下面这样:

class Date
{
public:
Date(int year, int n, int ref)
:_n(n)
,_ref(ref)
{
_year = year;
}

private:
int _year;
const int _n;
int& ref;
}

这个应该很好理解吧,我们之前不常说嘛,const类型和引用类型的变量必须在定义的时候初始化。OK,那除了这两种类型的变量需要在初始化列表初始化,还有没有其他的情况呢?答案是有的,还有第三种情况:自定义类型成员(且该类没有默认构造函数)。
比如:

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
};

下面我们总结一下:
1.每个成员变量在初始化列表中只能出现一次,因为初始化列表是成员变量定义的地方,只能定义一次;
2.以下成员必须放在初始化列表初始化:

  • 引用成员变量;
  • const 成员变量(准确来说是非静态的const成员变量,后面说);
  • 自定义类型成员(且该类没有默认构造函数)

除了这三种情况之外,其余的变量既可以在初始化列表初始化,也可以在函数体内赋初值,这里的建议是:所有成员变量尽量都在初始化列表初始化

对于初始化列表,还有一点需要注意的就是成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表的先后次序无关
例如下面这道题:

class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}

void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
//先声明的是_a2,后声明的是_a1
int _a2;
int _a1;
};

int main()
{
A aa(1);
aa.Print();
return 0;
}

A. 输出 1 1
B. 程序崩溃
C.编译不通过
D.输出1 随机值
解析:很明显,先声明的是_a2, 后声明的是_a1,所以先初始化_a2,再初始化_a1,答案选最后一个。

explicit关键字

构造函数不仅可以初始化对象,对于单个参数的构造函数,还具有隐式类型转换的作用
比如下面一段代码:

class Date
{
public:
Date(int year)
:_year(year)
{}

private:
int _year;
};

int main()
{
Date d1(2022);

//用一个整数给一个日期类对象赋值
//编译器实际上会在背后用2018构造出一个Date类型的临时对象
//再用这个临时对象去拷贝构造d1
d1 = 2018;
return 0;
}

类和对象·再谈构造函数_c++


本质其实就是:构造+拷贝构造,不过现在有很多编译器都会对其进行优化,将其合二为一,一次构造即可

但是需要注意哦,临时对象具有常性,所以下面需要注意:

Date& d2 = 2022;//错误,权限放大
const Date& d3 = 2022;//可以,权限不变

OK,说了半天还没有提到explicit,那它到底是用来干什么的呢?我们发现,上面代码的可读性并不是很好,所以用explicit修饰构造函数,将会禁止单参数的构造函数隐式类型转换

class Date
{
public:
//用explicit修饰构造函数,将会禁止单参数的构造函数隐式类型转换
explicit Date(int year)
:_year(year)
{}

private:
int _year;
};

int main()
{
Date d1(2022);

d1 = 2018;//此时编译报错
return 0;
}

static 成员

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
注意:静态成员变量一定要在类体外定义

面试题

题目:实现一个类,计算程序中创建出了多少个对象?
解题思路:
无非通过两种方式创建对象——构造函数和拷贝构造函数,所以我们只需要统计调用了多少次构造函数和拷贝构造即可,那怎么统计呢,这时候就体现出了static的作用。

class A
{
public:
A()
{
++count;
}
A(const A& t)
{
++count;
}
//成员函数也可以是静态的,静态的成员函数没有this指针
static int GetACount()
{
return count;
}

private:
//静态成员变量属于整个类,属于类的所有成员
static int count;//声明
};

int A::count = 0;

int main()
{
A a;
A aa(a);
//静态成员函数可以只指定类域调用,也可用对象调用
cout << A::GetACount() << endl;
return 0;
}

特性

  • 静态成员被所有类对象共有,不属于某个具体的实例;
  • 静态成员变量必须在类外定义,定义时不添加static关键字
  • 类静态成员既可用类名::静态成员又可用对象.静态成员来访问;
  • 静态的成员函数没有隐藏的this指针,所以不能访问任何非静态成员
  • 静态成员和类的普通成员一样,也有public、protected、private这三种访问级别,也可以具有返回值。

需要注意的问题:
1.静态成员函数可以调用非静态的成员函数吗?
不可以,因为静态成员函数没有隐藏的this指针,不可以调用任何非静态成员

2.非静态成员函数可以调用静态的成员函数吗?
可以,非静态成员函数和静态的成员函数都是在类中定义的,所以在类内不受访问限定符的限制

面试题

题目:不能用乘除法、循环、关键字及条件判断来做1+2+3+…+n

class Solution
{
public:
int Sum_Solution(int n)
{
Sum a[n];//变长数组,定义n个对象的数组,要调用n次构造函数
return Sum::GetSum();
}
};
class Sum
{
public:
Sum()
{
_sum += _i;
_i++;
}
static int GetSum()
{
return _sum;
}
private:
static int _sum;
static int _i;
};
int Sum::_sum = 0;
int Sum::_i = 1;

友元

友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利,但是破坏了封装,所以友元不适合多用。

友元函数

之前我们在实现Date类,尝试重载operator<<,我们发现没办法将其重载为成员函数,因为cout的ostream对象和隐藏的this指针在抢占第一个参数的位置。this指针默认是第一个形参,但是实际使用中cout需要是第一个形参对象。所以之前只能将其重载成全局函数,由于在类外定义,不能访问类中成员,所以需要使用友元解决。

若重载成成员函数,是下面这样的:

class Date
{
public:
Date(int year, int month, int day)
:_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 d1(2022, 1, 2);
d1 << cout;
return 0;
}

很显然,上面的使用方式很怪,为了方便我们正常使用,友元需要派上用场了:
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但是需要在类的内部声明,声明时需要加上friend关键字。

class Date
{
friend ostream& operator<<(ostream& out, 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& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day << endl;
return out;
}

int main()
{
Date d1(2022, 1, 2);
cout << d1;
return 0;
}

注意:

  • 友元函数可以访问类的私有成员和保护成员,但是友元函数不是类的成员函数;
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数;
  • 友元函数不能用const修饰。

友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类的非公有成员。

class Date;//前置声明
class Time
{
//声明日期类为时间类的友元类
friend class Date;
public:
Time(int hour = 1, int minute = 1, int second = 1)
:_hour(hour)
,_minute(minute)
,_second(second)
{}

private:
int _hour;
int _minute;
int _second;
};

class Date
{
public:
Date(int year = 2022, 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;
};

注意:

  • 友元关系是单向的,不具有交换性;比如上面的Time类和Date类,在Time类中声明Date类为其友元类,那么在Date类中可以直接访问Time类中的私有成员,但是却不能在Time类中访问Date类中的私有成员;
  • 友元关系不能传递;比如B是A的友元,C是B的友元,不能说明C是A的友元。

内部类

如果一个类定义在另一个类的内部,这个内部的类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类,外部类对内部类没有一个优越的访问权限。
注意哦,内部类就是外部类的友元类,内部类可以通过外部类的对象来访问外部类中的所有成员,但是外部类不是内部类的友元
特性:

  • 内部类定义在外部类的public、protected、private都是可以的;
  • 内部类可以直接访问外部类的static、枚举成员,不需要外部类的对象或类名;
  • sizeof(外部类) = 外部类,和内部类没有关系
class A
{
private:
static int _i;
int h;

public:
//B天生就是A的友元类,但A不是B的友元
class B
{
public:
void foo(const A& a)
{
cout << _i << endl;
cout << a.h << endl;
}
private:
int _b;
};
//B不是A的友元,所以不能访问A的私有成员变量
//void Print(const B& b)
//{
// cout << b._b << endl;
//}
};

int A::_i = 1;

int main()
{
A::B b;
b.foo(A());
return 0;
}

练习题

1、求共调用了几次拷贝构造函数?

Widget f(Widget u)
{
Widget v(u);
Widget w = v;
return w;
}

int main()
{
Widget x;
Widget ret = f(x);
}

以往我们会认为调用了5次拷贝构造:

类和对象·再谈构造函数_初始化_02


但是现在的编译器会做优化,因为在一个表达式中,连续步骤的构造+拷贝构造或者拷贝构造+拷贝构造,胆大一点的编译器可能会做优化,合二为一

类和对象·再谈构造函数_成员变量_03


好,我们加大难度,问下面调用了多少次拷贝构造?

Widget f(Widget u)
{
Widget v(u);
Widget w = v;
return w;
}

int main()
{
Widget x;
Widget y = f(f(x));
}

类和对象·再谈构造函数_c++_04


看似是9次拷贝构造,但是实际上经过编译器优化后只调用了7次拷贝构造(4和5合二为一,8和9合二为一)

2、有一个类A,其数据成员如下: 则构造函数中,成员变量一定要通过初始化列表来初始化的是:

class A {
...
private:
int a;

public:
const int b;
float* &c;
static const char* d;
static double* e;
};

解析:
以下成员必须放在初始化列表初始化:

  • 引用成员变量;
  • const 成员变量(准确来说是非静态的const成员变量,后面说);
  • 自定义类型成员(且该类没有默认构造函数)

注意哦,const 静态成员也是只能在类外进行初始化的。