类和对象(三)

简介: 今天小编给大家介绍的就是类最后的相关内容,希望大家好好学习理解,以加深大家对类的理解。

前言

今天小编给大家介绍的就是类最后的相关内容,希望大家好好学习理解,以加深大家对类的理解。


1.再谈构造函数

之前给大家介绍了构造函数的相关内容,对于变量的初始化我们都是放在函数体内进行直接赋值的,但是函数体内赋值并不是成员变量定义的地方,我们可以将其理解成为成员变量赋值的位置,比如:

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

我们这里构造函数进行的就是对成员变量进行赋值的操作,那我们进行定义的地方是在哪里呢?这就需要我们引入一个新的概念,也就是初始化列表。


1.1 初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟

一个放在括号中的初始值或表达式。

这里我给大家简单的演示一下:

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. 类中包含以下成员,必须放在初始化列表位置进行初始化:

引用成员变量

const成员变量

对于引用和const修饰的成员必须在定义的时候初始化,所以我们对于该变量是必须放在初始化列表初始化的。

自定义类型成员(且该类没有默认构造函数时)

但对于没有默认构造的自定义类型,我们一定是需要写出(需要在括号后加上非默认构造的参数),因为系统并不知道该如何调用,如果该自定义类型具有自己的默认构造,我们仍然要将其写入初始化列表,我们就需要更据该参数情况进行传参。


这里还有一点就是:我们不写初始化列表,每个成员变量也会自己走初始化列表。


3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,

一定会先使用初始化列表初始化。

在讲解第四点需要注意的要点之前,这里我需要给大家介绍一道题目:

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  随机值


对于上面这道题目,可能大家都会选择 A答案,这里我们运行一下看看结果如何:



我们可以看到输出的是我们的D答案,那为什么该是选择D答案的呢?这就和我们的第四点有关了。


4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关(这里小编建议,以后大家应该尽可能得将声明和初始化的顺序安排成一致)


1.2 explicit关键字

介绍这个关键字之前,小编先给大家介绍一段程序:

class Date
{
public:
  Date(int year)
  :_year(year)
  {
  cout << "Date(int year, int month, int day)" << endl;
  }
  Date(Date& d1)
  {
  cout << "Date(Date& d1)" << endl;
  _year = d1._year;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1=1;
}


这里我们看到主函数,这里我们看到了这里有个语句是Date d1=1,那么这个语句会发生什么过程呢?之前的话如果=后接的是一个对象,那么我们这里就会调用拷贝构造去初始化这个对象,但是我们这里后面接的是一个常数,那么实际上这里会发生一个隐式转换的过程,这里发生隐式转换有个前提的要求是构造函数中对应的形参要和等号接的值保持一致,那么隐式转换这个过程会发生什么呢?


这里首先会调用构造函数,将1这个值构造成一个对象,再调用拷贝构造函数,去对这个对象进行初始化,那么这里是否会发生小编上面所说的这个过程呢,这里我们看一下运行结果



这里我们发现我们只是调用了构造函数,那为什么没有拷贝构造函数呢?这就和编译器的优化有关,具体的小编在文章末会给大家具体介绍。


那么这里和我们的这个关键字又有什么关系呢?


从上面我们可以知道:


构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值

的构造函数,还具有类型转换的作用。

而用我们的explicit关键字去修饰构造函数,就会避免改发生类型转换。这里我给大家演示一下:


2.static成员

2.1 概念

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。


这里小编就给大家介绍一道面试题,以便大家更好的理解这个static修饰符,这里的题目要求是:实现一个类,计算程序中创建出了多少个类对象


题目分析:这里我们可以根据每当我们要创建一个对象,要么利用构造函数,要么就是利用拷贝构造函数,而我们销毁一个对象势必就会调用其析构函数,因此我们可以利用一个变量来记录这个变化,但是这个变量不能是局部的,否则当出了局部作用域就会被销毁而无法完全记录这个过程,因此这里势必我们需要利用一个静态变量去记录这个过程,对于静态变量,我们有全局和static这两种,但是对于全局变量我们在程序内的任何地方都可以使用,这就导致了该变量会在其他位置被改变,导致结果不准确的风险,依次这里我们这里使用static修饰类内变量,且该变量是被private修饰的,这就导致我们只可以通过调用类内相关函数才可以去改变其值。那么下面我们直接看代码。

#include<iostream>
using namespace std;
class A
{
public:
  A() { ++_scount; }
  A(const A& t) { ++_scount; }
  ~A() { --_scount; }
  static int GetACount() { return _scount; }
private:
  int _a = 1;//(成员变量————属于每个一个类对象,存储在对象里面)
  static int _scount; //(静态成员变量————属于类,属于类每个对象共享,存储在静态区)
};
int A::_scount = 0;
void TestA()
{
  cout << A::GetACount() << endl;
  A a1, a2;
  A a3(a1);
  cout << A::GetACount() << endl;
}
int main()
{
  TestA();
}


这里我们运行一下,查看该值是否正确:



2.2 特性

1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区


2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明


3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问


4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员(指定类域和访问限定符就可以访问)


5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制


这里根据上面我们还需要补充一点的是:非静态可以调用静态函数,但是静态函数不能调用非静态函数,因为非静态函数需要this指针,但是静态函数没有this指针。(仅限于类内)。


这里小编再给大家讲解一道题目,以加深大家对static这个修饰符的理解:


题目要求:设计一个类,在类外面只能在栈上创建对象, 设计一个类,在类外面只能在堆上创建对象


题目分析:首先我们需要想到,我们的构造函数是支持在栈上也支持在堆上创建的,而且我们每次构造一个对象势必会使用到构造函数,因此这里首先我们不能让类外对象直接访问到类内构造函数,而我们创建在不同存储空间对象就需要我们在类内进行处理,而如何让我们的类外去调用到我们处理过的函数呢?通过对象调用显得不太可能,那么这里我们只能通过类::函数名,对该函数进行访问,因此这里我们就需要使用到static修饰我们特殊处理的函数,代码如下:

class A
{
public:
//栈上创建
static A GetStackObj()
{
A aa;
return aa;
}
//堆上创建
static A* GetHeapObj()
{
return new A;
}
private:
A()
{}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
A::GetStackObj();
A::GetHeapObj();
return 0;
}

3.友元

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


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


3.1. 友元函数

在介绍友元函数之前,小编这里要给大家介绍两个操作符重载函数,首先是 . <<流插入运算符,>>流提取运算符,之前为了访问到类内成员变量,我们基本上是将操作符的重载写在类内的,这里我们以日期类为例,看看我们的这两个操作符写在类内重载会发生什么情况:


这里需要和大家提前说明的是我们的cout是存在于ostream类中的,而我们的cin是存在于istream类中的,因为我们的流插入和流提取都是支持连续的,根据操作符的性质该都是从左往右执行的,所以我们执行完后需要返回。

#include<iostream>
using namespace std;
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;
  }
  istream& operator >>  (istream& in)
  {
  in >> _year >> _month >> _day;
  return in;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2002, 2, 2);
  d1 << cout;
  d1 >> cin;
  d1 << cout;
  return 0;
}


这里我们运行一下:



以上我们可以看到的是我们重载该运算符后,该调用方式和我们的习惯是不一致的,原因是由于这里的对象会被默认成为第一个参数,如果我们按我们平时的习惯去调用,就会出现:



这里我们的解决方式就是将其写全局,但是我们该如何访问到类内私有成员呢?


1.写一个公共函数去获得


这里虽然可以实现,但是过于繁琐,我们不推荐,大家可以自己尝试一下。


2.友元函数


友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字,这里我们直接看我们的改造过程

#include<iostream>
using namespace std;
class Date
{
  //函数友元
  friend ostream& operator<<(ostream& out, const Date& d);
  friend istream& operator >>  (istream& in, 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;
}
istream& operator >>  (istream& in,Date&d)
{
  in >> d._year >>d._month >> d._day;
  return in;
}
int main()
{
  Date d1(2002, 2, 2);
  cout << d1;
  cin >> d1;
  cout<< d1;
  return 0;
}


这里我们调用一下,看看其过程:



这里我们还需要给大家说明几个要点:


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


友元函数不能用const修饰


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


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


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



3.2 友元类

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

友元关系是单向的,不具有交换性。

比如 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接

访问 Time 类的私有成员变量,但想在 Time 类中访问 Date 类中私有的成员变量则不行。

友元关系不能传递

如果 C 是 B 的友元, B 是 A 的友元,则不能说明 C 时 A 的友元。

友元关系不能继承,在继承位置再给大家详细介绍。

这里就给大家简单的实现一下类的友元

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


4.内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限


注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。(内部类在没有创建对象的时候是不占空间的,因为内部类在某个类中只是一个声明)


特性:


1. 内部类可以定义在外部类的public、protected、private都是可以的。


2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。


3. sizeof(外部类)=外部类,和内部类没有任何关系。


4.内部类也受访问限定符限制


#include<iostream>
using namespace std;
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::B b;
  b.foo(A());
  return 0;
}


5.匿名对象

对于匿名对象的介绍,小编这里就直接在代码中给大家说明一下:


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

return 0;

}


这里大家配合上面的讲解,小编这里在给大家说明归纳这几个要点:


1.首先匿名对象的构造方式是:类名(对应构造函数的参数)


2.匿名对象的生命周期只有一行,这里我们运行一下,这个程序让大家观察一下此过程




我们可以看到第二次构造函数执行后,后续马上就执行了析构函数,这就说明了该匿名对象的生命周期就只有一行。


3.匿名对象具有常性


这里小编给大家演示一下,这里的类和上面一致,小编只是改了一下主函数的部分内容


int main()
{
  A& b = A();
  return 0;
}

这里我们会发现编译器会给我们报一个这样的错误:



但是当我们把程序改为:


int main()
{
  const A& b = A();
  return 0;
}

可以发现我们的程序是没有任何错误的:



4.对于const A& ra=A()此时的const引用会延长匿名对象的生命周期,生命周期就会延长至当前函数的作用域


这里我们写几个变量给大家观察一下:


int main()
{
  A a(1);
  const A& b = A();
  A d(5);
  return 0;
}

调用可以发现:



这里我们第二次调用构造函数并没有马上销毁该匿名对象。


6.拷贝对象时的一些编译器优化

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。这里我们直接看代码,看其的具体现象:


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


这里我们运行一下,看该是否进行了优化



这里很明显就是我们在连续的构造和拷贝构造构造会省略拷贝构造,这也就告诉我们在传参和返回值时最后将会出现构造和拷贝构造的情况写在同一行。


相关文章
|
5天前
|
编译器 C语言 C++
c++类和对象
c++类和对象
26 1
|
5月前
|
编译器 C++
【C++】类和对象(下)
【C++】类和对象(下)
35 0
|
5月前
|
存储 编译器 C语言
【C++】类和对象(上)
【C++】类和对象(上)
42 0
|
7月前
|
存储 程序员 数据安全/隐私保护
C++ 类和对象(一)
C++ 类和对象(一)
28 0
|
7月前
|
Java 编译器 C++
【C++】类和对象(四)下
4.匿名对象: 实验代码:在这里插入图片描述
21 0
|
5天前
|
存储 编译器 C语言
类和对象(1)
类和对象(1)
10 1
|
5天前
|
存储 编译器 C语言
【c++】类和对象(一)
朋友们,大家好,本篇内容我们来对类和对象进行初步的认识
|
5天前
|
Java C++ Python
【c++】理解类和对象
【c++】理解类和对象
【c++】理解类和对象
|
5天前
|
存储 安全 编译器
|
5天前
|
存储 Java 编译器
C嘎嘎之类和对象中
C嘎嘎之类和对象中
24 0