【类和对象(下)】

简介: 🍕前言本文章继自类和对象(中),完成收尾工作。一、🍕再谈构造函数1.1构造函数体赋值在学习过的类和对象的基础知识中,构造函数内部通常是给成员变量一个初始值。虽然调用完构造函数后,变量有了初始值,但是不能称其为对对象的初始化,只能称其为对变量的赋值。因为初始化只能初始化一次,而赋值可以多次赋值。所以下面给出一个真正的初始化操作:初始化列表。

🍕前言

本文章继自类和对象(中),完成收尾工作。

一、🍕再谈构造函数

1.1构造函数体赋值

在学习过的类和对象的基础知识中,构造函数内部通常是给成员变量一个初始值。虽然调用完构造函数后,变量有了初始值,但是不能称其为对对象的初始化,只能称其为对变量的赋值。

因为初始化只能初始化一次,而赋值可以多次赋值。

所以下面给出一个真正的初始化操作:初始化列表

1.2初始化列表

初始化列表是构造函数的一部分,与构造函数体赋值并没有冲突,可以共存,只是初始化列表的每个变量都只能出现一次(因为初始化只初始化一次)。

初始化列表格式:

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

比如:

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

构造函数的大括号内部仍然可以像构造函数一样写其他的东西。

注意:

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
  1. (1)引用成员变量
    (2)const成员变量
    (3)自定义类型成员(且该类没有默认构造函数时)

以下面的代码为例:

class B
{
public:
  class A
  {
  public:
    A(int a = 1)
      :_a(a)
    {}
  private:
    int _a;
  };
  B(int a, int ref,int n)
    : _ref(ref)
    , _n(10)
  {}
private:
  A _aobj; //没有默认构造函数
  int& _ref;   // 引用
  const int _n; // const
};

不同于其他内置类型,其他内置类型可以只定义不初始化,比如:

int a;
double b;
char c;

对于引用成员变量和const成员变量来说,它们有一个共同点:

在定义的时候必须初始化。

所以在类的构造函数的初始化列表中必须有这两个成员变量的存在。

而对于自定义类型,前面我们说过,编译器自己生成的默认构造函数对内置类型不做处理,对自定义类型调用它的默认构造函数。


在初始化列表中,如果该自定义类型没有默认构造函数,(默认构造函数包括:无参构造函数,全缺省构造函数和编译器自己生成的构造函数。其中如果我们不写构造函数,编译器才会自己生成)那么在初始化列表必须要对自定义类型初始化。


如果不初始化,会报错,比如:

e6dde2c5830c4fa0a77ad12299089459.png

在这个例子中,A类的构造函数并不是默认构造函数,而是一个普通的构造函数,并且在B类的构造函数的初始化列表中并没有对A类进行初始化,这就会产生报错。


总结:


每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)


类中包含以下成员,必须放在初始化列表位置进行初始化:

(1)引用成员变量
(2)const成员变量
(3)自定义类型成员(且该类没有默认构造函数时)

  1. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
  2. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

第四点是非常重要的一点,下面给一道题:

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();
}
A.输出1  1
B.程序崩溃
C.编译不通过
D.输出1  随机值

这道题选择D项。首先排除程序会崩溃。

程序崩溃的原因不多,主要就是动态申请的资源泄露了或者产生死循环等等。


明显本题并没有以上情况。这道题既然能放在这里,说明该题可能会有坑,可以排除A,那就从CD之间选择。认真看代码,发现没有什么语法错误,不会编译不通过,只能选一个看似最离谱的答案D。

解析:类A实例化一个对象aa并显式地传递一个1给形参a,调用它的构造函数,此时在初始化列表中

看似先初始化a1,再初始化a2,实际上是先初始化a2,再初始化a1,因为初始化的顺序取决于成员变量的声明顺序的!

所以:成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

1.3explicit关键字

explicit关键字的作用在于禁止将会禁止构造函数的隐式转换。

对于什么是隐式转换,请看下面的代码:

class Date
{
public:
  explicit Date(int year)
    :_year(year)
  {}
  //赋值运算符重载
  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;
};
int main()
{
  Date d2 = 2;
}

实例化的类对象中,看似是将变量2赋值给对象d2,实际上的情况是:对于不同类型的赋值,会在中间生成一个临时空间,整型2发生了隐式类型转换,升级成了类对象,这个类对象2调用构造函数,此时这块临时空间就是一个对象,然后该临时空间再调用拷贝构造函数拷贝给d2。


这个过程是:构造–>拷贝构造。


但是编译器会自动优化,优化成直接进行构造。


对于编译器的优化,我们会在下面的篇幅详细地讲到。


对于explicit关键字,就是禁止编译器对于这种隐式类型转化。


当我们加上exiplict关键字后,编译器会直接报错。

85019078bdae476b9733fc38c62ed3ff.png

二、🍕static成员

被static修饰的成员变量叫做静态成员变量,被修饰的成员函数叫做静态成员函数。普通成员变量属于对象自己所有,静态成员变量属于每个类对象所共有的。

注意:1.静态成员变量一定要在类外面进行初始化。

2.静态成员变量或静态成员函数可以直接通过类域访问。(因为它属于整个类)

对于静态成员变量, 如果设置成公有,那就跟全局变量几乎一样,都可以被类外面进行访问。

下面有一道面试题:实现一个类,计算程序中出现了多少类对象。


为了完成这道题目,我们实现的类不能在外面计算出现的类的次数。只能在类内计算,如果调用了构造函数或者拷贝构造函数,就让一个计数变量++,如果调用了析构函数,就让计数变量–。很明显需要用静态成员函数才能实现,为了获得该成员变量,我们需要在对象内实现一个静态成员函数,来跟静态成员变量进行配套使用。


静态成员函数的特点:

1.没有this指针

2.指定类域和访问限定符即可访问。


所以可以通过下面的代码来计算类的对象的个数。

class A
{
public:
  A() 
  {
    ++_scount; 
  }
  A(const A& t) 
  { 
    ++_scount; 
  }
  ~A() 
  { 
    --_scount;
  }
  static int GetACount() 
  {
    return _scount;
  }
private:
  static int _scount;
};

另一道题目:

设计一个类,在类外面只能在栈/堆上创建一个对象。

class A
{
public:
  static A GetStackObj()
  {
    A a;
    return a;
  }
  static A* GetHeapObj()
  {
    return new A;
  }
private:
  A()
  {}
private:
  int _a1;
  int _a2;
};
int main()
{
  A::GetStackObj();
  A::GetHeapObj();
  return 0;
}

解析:将构造函数设置成私有的,那么所有的类型的对象都不能在类外面实例化

但是又将栈/堆的成员函数设置成静态公有的,使得我们可以在类外面直接通过类作用限定符

//直接调用该静态成员函数,达到题目要求。

再补充几点:

1.不能通过在类中给静态成员变量缺省值,因为这个地方只是静态成员变量的声明,并且缺省值实际上是给初始化列表的,对于普通成员变量来说,可以在构造函数的初始化列表中进行初始化,而静态成员变量则无法实现。

2.可以通过普通成员函数访问静态成员函数,因为对于静态成员函数来说,只要给定类域和访问限定符,就可以访问静态成员函数,在类中是不受访问限定符的限制的,也不受类域的限制,因为对于类来说这是一个整体;但不能通过静态成员函数访问普通成员函数,因为静态成员函数没有this指针,无法访问普通成员函数和成员变量。

三、🍕友元

友元的出现突破了类的封装机制,友元就像是类的朋友,可以随意访问类的非公有成员变量和成员函数,所以友元不适合多用。

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

友元函数:

我们知道,一般内置类型的输出可以使用

cout << 内置类型

的方式进行输出。但是对于一个自定义类型,却不能定义成类的成员函数:operator<<。因为在成员函数中有一个隐含的this指针,该指针会一直占用第一个位置,导致左操作数只能是cout。

所以如果要定义成成员函数,只能实现成 d1 << cout。(假设d1是一个日期类对象)。


如果定义成全局函数,就不再有this指针,左操作数就可以正常地中作为cout。但是重载成全局函数又导致我们无法访问到类地成员变量,这就需要友元函数的出现。

友元函数关键字: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;
}

注意:


1.友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

2.友元函数可访问类的私有和保护成员,但不是类的成员函数

3.友元函数不能用const修饰

4.友元函数可以在类定义的任何地方声明,不受类访问限定符限制

5.一个函数可以是多个类的友元函数

6.友元函数的调用与普通函数的调用原理相同

四、🍕内部类

如果一个类定义在外部类的内部,那么这个类就叫做内部类。内部类不属于外部类,外部类不能通过任何途径访问内部类的成员。

注意:内部类天生就是外部类的友元,但外部类不是内部类的友元。

特性

sizeof(外部类) == 外部类,计算一个类的大小,与内部类完全没有任何关系,结果是外部类的大小。

比如:

class A
{
private:
  static int k;
  int h;
public:
  class B // B天生就是A的友元
  {
  public:
    void foo(const A& a)
    {
      cout << k << endl;//OK
      cout << a.h << endl;//OK
    }
  };
};
int A::k = 1;
int main()
{
  A a;
  cout << sizeof(a) << endl;
  return 0;
}

结果是4;

解析:对于外部类来说,内部类和外部类没有声明关系,因为内部类只是一个声明,并没有真正意义地创建内部类对象。相当于A类是一个图纸,通过A类这个图纸建造了a这所房子,但是B是A类的图纸里面的一张图纸,并没有通过B这个图纸真正创造一所房子。

特性2:

内部类受外部类的访问限定符的限制。

特性3:

建议内部类的成员变量声明在外部类,这样内部类既可以使用它的成员变量,外部类也可以使用内部类的成员变量。

例题:求和

class Solution {
public:
    int Sum_Solution(int n) 
    {
        Sum a[n];
        return _sum;       
    }
    class Sum
    {
    public:
        Sum()
        {
            _sum+=_i;
            ++_i;
        }
    };
    private:
        static int _i;
        static int _sum;
};
int Solution::_i = 1;
int Solution::_sum = 0;

五、🍕匿名对象

请看下面的代码以及注释:

class A
{
public:
  A(int a = 0)
  :_a(a)
  {
  cout << "A(int a)" << endl;
  }
  ~A()
  {
  cout << "~A()" << endl;
  }
private:
  int _a;
};
class Solution {
public:
  int Sum_Solution(int n) 
  {
  //...
  return n;
  }
};
int main()
{
  A aa1;
// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
//  A aa1();
// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
  A();
  A aa2(2);
//不能这样调用:
  //Solution.Sum_Solution(10);
  //Solution::.Sum_Solution(10);
  //对于第1种情况,不能直接使用类型调用它的成员函数
  //对于第二种情况,只有static成员函数或者成员变量可以这样调用,因为静态成员函数没有this指针,而普通成员函数需要传递this指针,如果这样调用,就无法传递this指针。
  Solution().Sum_Solution(10);
  return 0;
}

匿名对象特性:

1.匿名对象的生命周期在当前行

有名对象的生命周期在当前的局部作用域

2.匿名对象类似于临时对象,都具有常性

3const引用延长了匿名对象的生命周期

1.匿名对象的生命周期只有1行
  A(2);
2.匿名对象具有常性,如果这样定义结果是权限放大了,不行
  A& pa = A(3);
  const A& ra = A(3);//这样就可以,const修饰的变量具有常性,这样是权限平移。
3.const引用修饰的匿名对象延长了它的生命周期,使其生命周期延长到当前函数作用域。

六、🍕拷贝对象时编译器的一些优化

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝。

几种优化情况:

1.同一行一个表达式中连续的构造+构造会优化成为1个构造。
2.同一行一个表达式中连续的构造+拷贝构造会优化成为1个构造
3.不同行的构造+赋值重载不会优化

 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()
{
  // 隐式类型,连续构造+拷贝构造->优化为直接构造
  f1(1);
  // 一个表达式中,连续构造+拷贝构造->优化为一个构造
  f1(A(2));
  // 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
  A aa2 = f2();
  // 一个表达式中,连续拷贝构造+赋值重载->无法优化
  A aa2;
  aa2 = f2();
  return 0;
}

七、🍕再谈类和对象

类是对某一类实体(对象)来进行描述的,描述该对象具有那

些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。

🍕总结

类和对象收尾工作到此结束。

相关文章
|
12天前
|
编译器 程序员 C语言
C++ --> 类和对象(一)
C++ --> 类和对象(一)
20 5
|
2月前
|
存储 编译器 C语言
【C++】:类和对象(上)
【C++】:类和对象(上)
15 0
|
3月前
|
存储 Java 数据安全/隐私保护
类和对象是什么?(上)
类和对象是什么?
38 4
|
2月前
|
Java 编译器 C++
4. C++类和对象(下)
4. C++类和对象(下)
|
3月前
|
存储 编译器 C语言
【C++】类和对象(上)
【C++】类和对象(上)
|
3月前
|
存储 编译器 C语言
C++-类和对象(2)
C++-类和对象(2)
33 0
|
3月前
|
存储 Java 编译器
C嘎嘎之类和对象中
C嘎嘎之类和对象中
31 0
|
8月前
|
存储 编译器 C语言
类和对象(上)
类和对象(上)
60 0
|
8月前
|
存储 编译器 C++
类和对象(中)
类和对象(中)
35 0
|
10月前
|
存储 编译器 C语言