C++之面向对象(下)(一)

简介: C++之面向对象(下)(一)

前言

本文继续介绍与C++中与面向对象相关的内容,介绍了构造函数中的初始化列表、隐式类型转换、类的静态成员、友元、内部类、匿名对象以及编译器对拷贝构造的优化等概念。


一、再谈构造函数

1.构造函数体赋值

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

class Data
{
public:
  Data(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};

不能将这一过程称为初识化,只能称为赋初值,因为初始化只能初始化一次,而构造函数的函数体内可以进行多次赋值。那么对象是在什么时候进行初始化的呢?

2.初始化列表

初始化对象是由初始化列表完成的。

1.初始化列表的格式

以一个冒号开始,接着是以逗号为分割的数据成员列表,每个成员变量后面跟着一个放在括号中的初始值或者表达式

class Data
{
public:
  Data(int year, int month, int day)
    : _year(year),//初始化列表
      _month(month),
      _day(day)
  {
  }
private:
  int _year;
  int _month;
  int _day;
};

2.注意的要点

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。

小总结

  1. 尽量使用初始化列表;
  2. 一个类尽量提供默认构造函数(最好提供全缺省)。

2.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成员

1.概念

声明为static的类成员称为静态类成员,用static修饰的类成员变量称为静态成员变量,用static修饰的类成员函数称为静态成员函数

静态成员变量一定在类外进行初始化,静态成员函数中没有this指针

2.特性

  1. 静态类成员为所有类对象所共享,不在某个具体的类对象中,而是存放在静态区。
  2. 静态成员变量必须在类外定义,不用加static,类中只是声明;
  3. 类静态成员可用类名::类静态成员或者类对象名.类静态成员的方式来访问;
  4. 静态成员函数没有this指针,不能访问任何非静态成员;
  5. 静态成员也是类的成员,受publicprotectprivate的访问限定符限制。

三、友元

友元提供了一种突破封装的方式,有时可以提供便利。但是友元会增加耦合度,破坏了封装,所有友元不宜多用。

1.分类分类

友元分为友元函数和友元类。

2.友元函数

1.友元函数的引入

问题:现在尝试去重载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;
}

2.友元函数的说明

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰
    因为友元函数没有this指针,它是一个的类外的函数,不过可以访问类内的成员。
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同
相关文章
|
2月前
|
安全 程序员 编译器
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
92 11
|
3月前
|
存储 安全 编译器
【C++核心】一文理解C++面向对象(超级详细!)
这篇文章详细讲解了C++面向对象的核心概念,包括类和对象、封装、继承、多态等。
32 2
|
2月前
|
存储 编译器 C语言
【C++】初识面向对象:类与对象详解
【C++】初识面向对象:类与对象详解
|
7月前
|
算法 Java 程序员
【C++专栏】C++入门 | 类和对象 | 面向过程与面向对象的初步认识
【C++专栏】C++入门 | 类和对象 | 面向过程与面向对象的初步认识
67 0
|
4月前
|
存储 安全 数据处理
【C++】C++ 超市会员卡管理系统(面向对象)(源码+数据)【独一无二】
【C++】C++ 超市会员卡管理系统(面向对象)(源码+数据)【独一无二】
118 1
|
4月前
|
算法 数据可视化 C++
【C++】C++ 学生信息管理系统(源码+面向对象)【独一无二】
【C++】C++ 学生信息管理系统(源码+面向对象)【独一无二】
|
5月前
|
存储 开发框架 Java
|
6月前
|
算法 编译器 C语言
C++进阶之路:深入理解编程范式,从面向过程到面向对象(类与对象_上篇)
C++进阶之路:深入理解编程范式,从面向过程到面向对象(类与对象_上篇)
80 3
|
5月前
|
Java C++ iOS开发
|
6月前
|
C++
C++ 是一种面向对象的编程语言,它支持对象、类、继承、多态等面向对象的特性
C++ 是一种面向对象的编程语言,它支持对象、类、继承、多态等面向对象的特性