C++——类和对象(初始化列表、匿名对象、static成员、类的隐式类型转换和explicit关键字、内部类)

简介: C++——类和对象(初始化列表、匿名对象、static成员、类的隐式类型转换和explicit关键字、内部类)

初始化列表、匿名对象、static成员、类的隐式类型转换和explicit关键字、内部类

本章思维导图:

注:本章思维导图对应的xmind文件和.png文件都已同步导入至资源


1. 初始化列表

1.1 再谈构造函数

众所周知,每个变量只能被初始化一次,我们之前一直认为成员变量的初始化是在构造函数的函数体中,但是,成员变量是可以在构造函数的函数体出现多次的

class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
    //出现多次,且可以编译通过
    _year = 100;
    _month = 200;
  }
private:
  int _year;
  int _month;
  int _day;
};

因此,我们只能认为在构造函数函数体内执行的是赋值操作,而不是初始化

这就说明,构造函数的函数体并不是类的成员变量真正初始化的地方,那么成员变量到底是在哪里初始化的呢?

1.2 初始化列表

初始化列表是成员变量真正初始化的地方

1.2.1 初始化列表的语法

初始化列表以分号:开始,以逗号,分割,每个成员变量后面带上放在括号()里的初始值或者表达式

例如,对于上面的构造函数:

Date(int year = 1, int month = 1, int day = 1)
    //初始化列表
    : _year(year)
    , _month(month)
    , _day(day)
{
}

1.2.2 初始化列表的意义

初始化列表解决了三类不能在构造函数的函数体内初始化的问题

  • &修饰的引用成员变量——引用成员在定义时就必须初始化
  • const修饰的const成员变量——const变量在定义时就必须初始化
  • 没有默认构造的自定义类型——在函数体内不能初始化自定义类型

也就是说,上面所说三类成员变量必须在初始列表里面进行初始化

例如;

class Stack
{
public:
    //这不是默认构造,因为要传参数
  Stack(int capacity)
  {
  }
private:
  int* _a;
  int _capacity;
  int _top;
};
class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
    : num1(2)
    , num2(_year)
    , st(3)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
  const int num1;
  int& num2;
  Stack st;
};

1.3 注意事项

  • 因为初始化列表是成员变量初始化的地方,而每个变量又只能初始化一次,因此成员变量只能在初始化列表出现一次
  • 因为初始化列表是真正初始化成员变量的地方,因此无论有没有显示的写出初始化列表,成员变量都会经过初始化列表的初始化
  • 如果没有显示的写出初始化列表,那么:
  • 对于内置类型,那就赋予其初始值
  • 对于自定义类型,就调用它的默认构造
  • 能使用初始化列表就使用初始化列表。但也不是说初始化列表就能完全替代函数体。因为有时候函数体需要进行检查等操作。
  • 初始化列表的初始化顺序是成员变量声明的顺序,而不是在初始化列表里出现的顺序。
class A
{
public:
  A()
    : a1(1)
    , a2(a1)
  {
  }
  void Print()
  {
    cout << a1 << endl << a2 << endl;
  }
private:
  int a2;
  int a1;
};
int main()
{
  A a;
  a.Print();
  return 0;
}
/*output:
  1
  -858993460
*/
//a2声明在a1之前,因此,在初始化时,先执行a2(a1),此时a1为随机值
//因此建议成员变量的初始化顺序和声明顺序一致

2. 匿名对象

class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
    : _year(year)
    , _month(month)
    , _day(day)
  {
    myCount++;
  }
  void Print()
  {
    cout << "Date" << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};

如果我们想不实例化对象,但想调用Date类里的Print()函数来知道这是个什么类,该如何做到呢?

这里就可以用到我们的匿名对象来解决:

int main()
{
  Date().Print(); //Date()创建出一个匿名对象,再用这个匿名对象来调用成员函数Print()
  return 0;
}

创建匿名对象的方式:

className()

匿名对象的特点:

  • 匿名对象是一种临时对象,它没有分配给任何命名变量,而是在需要时被创建并使用
  • 其生命周期仅存在于当前行,执行完后立即销毁
  • 匿名对象一般是常量对象,不可被修改

3. static成员

如果我们想记录一个类究竟被构造了多少次

我们不难写出这样的代码:

//定义一个全局变量来记录类A构造的次数
int myCount = 0;
class A
{
public:
  //构造函数
  A()
  {
    myCount++;
  }
  //拷贝构造
  A(A& a)
  {
    myCount++;
  }
};
int main()
{
  A a[10];
  cout << myCount << endl;
  return 0;
}

但是这就出现了一个问题:我们可以在全局随意修改变量myCount的值:

int main()
{
  A a[10];
  myCount++;
  cout << myCount << endl;
  return 0;
}

这样也就不能保证类被构造次数的正确性了。

3.1 static成员变量

为了解决这个问题,我们可以在类里面声明一个static成员,并用这个成员来记录类被构造的次数

class A
{
public:
  //构造函数
  A()
  {
    myCount++;
  }
  //拷贝构造
  A(A& a)
  {
    myCount++;
  }
private:
  static int myCount;
};
int A::myCount = 0;

这个static修饰的静态成员变量有如下特点

  • 实际上也是一个全局变量,只是受类域和访问限定符所限制
  • 静态成员变量只能在类里面声明在类外面定义。
  • 静态成员变量在声明时不能和非静态成员变量一样给缺省值,因为这个缺省值是给初始化列表里用的,而静态成员变量不用初始化列表初始化。
  • static修饰的静态成员变量是这个类所属的,而不是由这个类实例化的某个对象所独有

3.2 static成员函数

知道如何利用static成员变量之后,针对最开始的问题,我们不难写出下面的代码:

class A
{
public:
  //构造函数
  A()
  {
    myCount++;
  }
  //拷贝构造
  A(A& a)
  {
    myCount++;
  }
  //因为myCount被private修饰,在类外面无法访问
  //因此要用成员函数访问myCount
  int GetCount()
  {
    return myCount;
  }
private:
  static int myCount;
};
int A::myCount = 0;
int main()
{
  A a[10];
  //为了调用GetCount成员函数,必须要实例化一个对象,而这个对象是没有意义的,因此最终结果要减一
  cout << A().GetCount() - 1 << endl;
  return 0;
}

但是又有一个问题出现了:

我们只是想知道A类到底被调用了多少次,但是要知道这个结果又必须新实例化一个对象,有没有什么方法不实例化对象就可以直接得到myCount的值呢?

为了解决上述问题,就需要用到static成员函数

class A
{
public:
  //构造函数
  A()
  {
    myCount++;
  }
  //拷贝构造
  A(A& a)
  {
    myCount++;
  }
  //static静态成员函数
  static int GetCount()
  {
    return myCount;
  }
private:
  static int myCount;
};
int A::myCount = 0;
int main()
{
  A a[10];
  cout << A::GetCount() << endl;
  return 0;
}

static修饰的静态成员函数有如下特点

  • 和静态成员变量一样,静态成员函数实际上也是一个全局函数,只是受类域和访问限定符限制
  • 静态成员函数在类里面声明,但既可以在类外面定义也可以在类里面定义
  • 和非静态成员函数不同,静态成员函数没有this指针,因此静态成员函数无法访问非静态成员变量和非静态成员函数,但也因如此,它可以直接通过类名和域作用限定符::调用

4. 类的隐式类型转换和explicit关键字

4.1 类的隐式类型转换

以前我们一般是这么实例化一个对象的:

class Date
{
public:
private:
};
int main()
{
  Date d1;  //利用构造函数实例化对象
  Date d2(d1);  //利用拷贝构造实例化对象
  return 0;
}

现在又有一个新的实例化对象的方法——类的隐式类型转换

class A
{
public:
  A(int a = 1)
    : _a(a)
  {
  }
private:
  int _a;
};
int main()
{
  A A1 = 10;
  return 0;
}

可以看出,整形10确实被转换为了A类型。

根据当隐式类型转换发生时会产生临时变量的知识点,我们可以推导出A A1 = 10这行代码的具体实现逻辑:

应该清楚,要支持这种隐式类型转换,该类的构造函数应该支持只传一个内置类型就可以实现构造

例如对于下面几种情况,就不支持内置类型隐式转换为类类型:

//Error_1
class A
{
public:
  A()
    : _a(a)
  {
  }
private:
  int _a;
};
int main()
{
  A A1 = 10;
  return 0;
}
/*
报错:
  error C2065: “a”: 未声明的标识符
  error C2440: “初始化”: 无法从“int”转换为“A”
  message : 无构造函数可以接受源类型,或构造函数重载决策不明确
*/
//Error_2
class A
{
public:
  A(int a, int b)
    : _a(a)
  {
  }
private:
  int _a;
  int _b;
};
int main()
{
  A A1 = 10;
  return 0;
}
/*
报错:
  error C2440: “初始化”: 无法从“int”转换为“A”
  message : 无构造函数可以接受源类型,或构造函数重载决策不明确
*/

类似的,对于有多个形参的构造函数,我们也可以传入多个内置类型进行构造:

class Date
{
public:
  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 d3 = {2023, 11, 7};  //传入三个内置类型进行构造
  return 0;
}

4.1 explicit关键字

有些时候,如果我们不想让上面所说的隐式类型转换发生,我们可以在构造函数的声明前加上explicit关键字:

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

加上explicit关键字后,如果继续进行隐式类型转换,就会报错:

error C3445: "Date" 的复制列表初始化不能使用显式构造函数

5. 内部类

C++支持在类的内部继续创建类,例如:

class A
{
public:
  class B
  {
  };
private:
  int _a;
  int _b;
};

内部类有如下的特点:

  • 内部类是一个独立的类,它不属于外部类,不能通过外部类的对象来访问内部类的成员
  • 内部类天生就是外部类的友元类,可以直接访问外部类的成员变量和成员函数
class A
{
public:
  class B
  {
    void Print(A& a)
    {
      a._a = 1;
    }
  public:
    int _b;
  };
private:
  int _a;
};
  • sizeof(外部类)的结果和内部类无关
class A
{
public:
  class B
  {
  public:
    int _b;
  };
private:
  int _a;
};
int main()
{
  cout << sizeof(A) << endl;
  return 0;
}
//output:4

  • C++类和对象的知识到这里就学习完毕了,之后博主会发布C++类和对象的总结篇
  • 下一篇,博主将介绍C++的内存管理,感兴趣的小伙伴可以来看看哦~

相关文章
|
4月前
|
存储 安全 编译器
【C++入门 四】学习C++内联函数 | auto关键字 | 基于范围的for循环(C++11) | 指针空值nullptr(C++11)
【C++入门 四】学习C++内联函数 | auto关键字 | 基于范围的for循环(C++11) | 指针空值nullptr(C++11)
|
5月前
|
存储 安全 编译器
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
64 5
|
4月前
|
存储 编译器 C++
C++从遗忘到入门问题之float、double 和 long double 之间的主要区别是什么
C++从遗忘到入门问题之float、double 和 long double 之间的主要区别是什么
|
5月前
|
Unix 编译器 C语言
【C++航海王:追寻罗杰的编程之路】关键字、命名空间、输入输出、缺省、重载汇总
【C++航海王:追寻罗杰的编程之路】关键字、命名空间、输入输出、缺省、重载汇总
26 0
|
17天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
20 4
|
17天前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
18 4
|
17天前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
17 1
|
27天前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
27天前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
29天前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
51 1