【C++】 | 类和对象完结篇

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 【C++】 | 类和对象完结篇

前言:

之前,我们已经通过经历了类和对象(上)和类和对象(中)的学习,使我们对类和对象这一概念打下了坚实的基础,今天我们要做的便是对类和对象进行收尾工作,本篇之后关于类和对象的全部知识便讲解完毕了。接下来,我们正式进入今天的学习。


(一)再谈构造函数 (⑉・̆-・̆⑉)

通过之前的学习,我们不难发现对于构造函数,不知道小伙伴有没有觉得对于这个构造函数的设计,祖师爷在开始设计的时候是不是设计太复杂了呀!

  • 其实吧,这也不能全怪我们的祖师爷。在刚开始设计时,全是凭借自己的理解对其进行设计,不像现在,设计都有类似的模板进行对照,完全是“盲人摸象”那种状态。
  • 结果就造成了,设计后发现有点小坑,但是又不能把之前的全部给换掉,所以为了弥补这个短板又进行“修补”,因此,便造成了今天这样特性很多的场景!!

通过之前,我们已经知道构造函数对于内置类型不会处理,而对于自定义类型则会去调用它的默认构造。原因是什么大家现在还知道吗?

  • 因为呀!对于对象来说是可以整体定义的,但是此时又有一个问题那就是我们还要给每个成员找一个定义的地方呀!

那这又是为什么呢?(我们不可以不管它们吗)

  • 有些成员必须在定义的时候进行初始化。举个简单的例子,对于【const】引用,没有默认构造的函数,没有默认构造的成员变量,因此它必须在定义的时候进行初始化,不然就会出错。

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

class Date
{
public:
Date(int year, int month, int day)
 {
     _year = year;
     _month = month;
     _day = day;
 }
private:
int _year;
int _month;
int _day;
};
  • 虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始 化一次,而构造函数体内可以多次赋值。

👉因此,基于上述原因,【C++】就引入了初始化列表这个概念!!!


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

  1. 引用成员变量
  2. const成员变量
  3. 自定义类型成员(且该类没有默认构造函数时)
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 
};

因此,大家记住一点,那就是尽量使用初始化列表!!!

大家在看以下代码就可以清晰的看出了:

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);
  return 0;
}

输出结果为:

接要来要给大家说的一点便是,这点很容易迷糊大家:

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

具体什么意思呢?我们通过代码来给大家具体的说明,具体如下,大家看如下代码输出的结果是什么:

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();
    return 0;
}

好,废话不多说我们直接给出结果,具体如下,大家是否能够想得明白呢?:

上述结果不难发现这一点,这个大家稍微记住就可以了。做题时遇到要会即可。


2、explicit关键字

在学习这个关键字之前,我先通过代码来带大家先了解为什么会引入这个关键字,它的作用是什么呢?

class A
{
public:
  A(int a)
    :_a1(a)
  {
    cout << "A(int a)" << endl;
  }
  A(const A& aa)
    :_a1(aa._a1) //注意拷贝构造也有初始化列表
  {
    cout << "A(const A & a)" << endl;
  }
private:
  int _a1;
};
int main()
{
  A aa1(1);  //构造函数
    A aa2 = 1;  //隐式类型转换  构造+拷贝构造+优化 ——>构造
  return 0;
}

解析:

对于上述的两种初始化的方法,我相信大家应该都知道,但是各位懂得其中的原理吗?像【aa2】那样写的意义是什么呢?

  • 对于【aa1】是直接调用的构造函数,
  • 对于【aa2】来说,大家是否就感到奇怪,我们用【1】去初始化,而【1】又是整形,问题就是整形怎么能够初始化【aa2】呢?其实这个地方本质就是类型转换,那这个又怎么理解呢?

大家结合这个先来理解:

int i = 1;
double j = i; //隐式类型转换
  • 这里的【i】是整形,而【j】是double类型,那这个两个为什么能够转换呢?其实对于二者,本质就是表示数据大小的,只是一个带了精度而一个没有罢了。进行转换只是发生精度丢失的问题。
  • 大家在想想这里的【i】是直接给【d】的吗?其实并不是这样的。在【i】转换的过程中会产生一个临时变量,这个临时变量还具有常性,【i】呢是给这个临时变量,通过这个临时变量再把值给【d】,此时这个临时变量的类型为【double】

有了这个做铺垫,那上面的【1】和【aa2】之间是怎么发生转换的呢?其实是一样的道理,此时这里的这个临时变量就是【A】类型的,这个临时对象再去拷贝构造:

接下来我们验证一下,按照我上面说的,【aa1】是有一个构造,【aa2】应该是有一个构造再加一个拷贝构造,接下来我进行编译看结果是不是那样。

咦......两个构造是有,但是为什么拷贝构造没有呢?这是什么原因呢?🤔

  • 其实这是【C++】给出的优化效果导致的,【aa2】是经过构造加拷贝构造,在编译器这么做太费事了吧,他就直接合二为一,等价于直接去进行构造(但是并不是所有的编译器都会优化)

此时可能有好奇的小伙伴就会问了,你是怎么知道这里有临时对象的呢?不要急,接下来我就给大家验证一下。🤥

接下来,我们这样做,看如下代码:

A& ref = 10;

此时会不会编译成功呢?

  • 我猜大家应该都知道,对这个铁铁的不行呀!!!我们编译看下结果

  • 对的,确实是错误的,但是当我们加上【const】后行不行呢?我们在编译看看

此时我们发现加上【const】之后就可以了,那么为什么呢?

  • 因为啊,正如前面讲到的,因为这里产生了临时对象,临时对象具有常性,而且我们我们左边为引用之后,这里就不会发生优化了,所以现在大家怎么了吗?

因此,上述我们不难得出一个结论,对于单参数的构造函数,此时这里发生的是隐式类型转换。构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值 的构造函数,还具有类型转换的作用。

但是此时我们并不想它发生这样的转换有什么方法吗?——这时就引出了【explicit】这个关键字。

  • explicit关键字的作用就是防止类构造函数的隐式自动转换.

当我们加上这个关键字之后,在当我们去编译时就会报错:

 

  • 上面也已经说过了, 【explicit】关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以【explicit】关键字也就无效了.

例如:

class A
{
public:
  A(int a, int a2)
    :_a1(a)
    , _a2(a2)
  {
    cout << "A(int a)" << endl;
  }
  A(const A& aa)
    :_a1(aa._a1)
  {
    cout << "A(const A & a)" << endl;
  }
private:
  int _a1;
  int _a2;
};
int main()
{
  // 多参数构造函数 C++11
  A aa3(1, 1);
  A aa4 = { 2,2 }; //注意这里是花括号
  const A &ref = { 2, 2 };
  return 0;
}
  • 此时,当我们去对其进行编译时,我们会发现这个程序是能够编译通过的,具体如下:

  • 当我们加上之后,我们就会发现,编译时就会报错:

总结:

  • explicit关键字只需用于类内的单参数构造函数前面。由于无参数的构造函数和多参数的构造函数总是显示调用,这种情况在构造函数前加explicit无意义。

(二)Static成员

  • 在类中,static 除了可以声明静态成员变量,还可以声明静态成员函数。
  • 普通成员函数可以访问所有成员(包括成员变量和成员函数),静态成员函数只能访问静态成员。
  • 当编译器在编译一个普通成员函数时,会隐式地增加一个形参 【this】,并把当前对象的地址赋值给 【this】,所以普通成员函数只能在创建对象后通过对象来调用,因为它需要当前对象的地址。
  • 而静态成员函数可以通过类来直接调用,编译器不会为它增加形参 【this】,它不需要当前对象的地址,所以不管有没有创建对象,都可以调用静态成员函数。

接下来,我们通过一个来具体看看

  • 面试题:实现一个类,计算程序中创建出了多少个类对象。
using std::cout; //解决命名冲突
using std::endl;
int count = 0; //传统方式定义一个全局变量
class A
{
public:
  A(int a = 0)
  {
    ++count;
  }
  A(const A& aa)
  {
    ++count;
  }
};
void func(A a)
{}
int main()
{
  A aa1;
  A aa2(aa1);
  func(aa1);
  A aa3 = 1;
  cout << count << endl;
  return 0;
}

解析:

  • 以上为我们传统的方案,就是先创建,在定义一个全局变量用来进行记录即可。但是此时如果去 编译的话就会出现一个问题,我们打印看看

此时,就会出现一个命名冲突的问题,跟库中的发生了冲突,那么如何解决呢?

  • 此时我们就不展开命名空间,一旦我们展开命名空间就把其中的【cout】展开出来了,因此我们只需展开部分,即可解决:
using std::cout;
using std::endl;

打印结果为:

但是大家上述写法是否存在问题呢?因为全局变量在哪个地方都可以改,没有封装起来,而 C++ 是很注重数据的封装的。假如我们“手欠”,加上了这样几行代码,此时结果是什么呢?

此时,我们就会发现,这时的结果显然就出错了呀!!那怎么解决呢?我们就可以通过 【static 】成员变量来解决这个问题。

class A
{
public:
  A(int a = 0)
  {
    ++count;
  }
  A(const A& aa)
  {
    ++count;
  }
//private:
  static int count; //属于所有对象,属于整个类
};
int A::count = 0;// 定义初始化
void func(A a)
{}
int main()
{
  A aa1;
  A aa2(aa1);
  func(aa1);
  A aa3 = 1;
  cout << A::count << endl;  //注意这里的访问方式
  cout << aa2.count << endl;
  cout << aa3.count << endl;
  return 0;
}

注意:

  • static 成员变量不在类对象里,static 成员变量在静态区中,其生命周期是全局的,作用域受类域的限制。
  • 和静态成员变量类似,静态成员函数在声明时要加 【static】,在定义时不能加 【static】。静态成员函数可以通过类来调用(一般都是这样做),也可以通过对象来调用。

上述虽然解决了全局变量的问题,又带来了另一个问题:【static 】成员变量是公有的,没有将其封装起来。那有没有更好的方式呢?

class A
{
public:
  A(int a = 0)
  {
    ++count;
  }
  A(const A& aa)
  {
    ++count;
  }
  static int GetACount()
  { 
    return count; 
  }
private:
  static int count; //属于所有对象,属于整个类
};
int A::count = 0;// 定义初始化
void func(A a)
{}
int main()
{
  A aa1;
  A aa2(aa1);
  func(aa1);
  A aa3 = 1;
  cout << aa3.GetACount() << endl;
  return 0;
}

此时,我们打印一下结果,看是否正确:

  • C++中,静态成员函数的主要目的是访问静态成员。GetACount 当然也可以声明为普通成员函数,但是它们都只对静态成员进行操作,加上 [static ] 语义更加明确。因为它不需要传【this】指针

特性

  • 1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
  • 2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  • 3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
  • 4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  • 5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

😮‍💨问题

1. 静态成员函数可以调用非静态成员函数吗?

  • 普通成员变量占用对象的内存,静态成员函数没有 this 指针,不知道指向哪个对象,无法访问对象的成员变量,也就是说静态成员函数不能访问普通成员变量,只能访问静态成员变量。

2. 非静态成员函数可以调用类的静态成员函数吗?

  • 普通成员函数必须通过对象才能调用,而静态成员函数没有 this 指针,无法在函数体内部访问某个对象,所以不能调用普通成员函数,只能调用静态成员函数。

3.静态成员函数与普通成员函数的根本区别

  • 普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)

这里给大家提供一个题,大家有兴趣可以按照上述思想去进行解答:

1.求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判 断语句


(三)友元

首先解答为什么引入友元🧐:

  • 友元提供了一种突破封装的方式,有时提供了便利。在实现类之间数据共享时,减少系统开销,提高效率
  • 但是友元会增加耦合度,破坏了封装,尽量使用成员函数,除非不得已的情况下才使用友元函数。
  • 友元分为:友元函数和友元类

3.1友元函数

这个在前面我们已经提过,当我们尝试去重载【operator<<】,然而发现没办法将【operator<<】重载成成员函数。因为【cout】的 输出流对象和隐含的this指针在抢占第一个参数的位置。【this】指针默认是第一个参数也就是左操作 数了。但是实际使用中【cout】需要是第一个形参对象,才能正常使用。所以要将【operator<<】重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。

  • 友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend,其格式如下:   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;
}

注意事项:

  • 友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函数。
  • 一个函数可以是多个类的友元函数,只需要在各个类中分别声明。
  • 可以直接调用友元函数,不需要通过对象或指针
  • 友元函数不能用const修饰
  • 友元函数的调用与普通函数的调用原理相同

说明:

  • 在函数声明中加入【operator< < < >】:是将operator<<函数定义为函数模板,将函数模板申明为类模板的友员时,是一对一绑定的
  • 实际的声明函数:这里模板参数可以省略,但是尖括号不可以省略

除此之外,两个类要共享数据的时候也可以使用友元函数!!!


3.2友元类

  • 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)      
  • 当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。

定义友元类的语句格式如下:

  •  friend class 类名
  • 其中:friend和class是关键字,类名必须是程序中的一个已定义过的类。
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;
};

注意事项:

  • 友元关系是单向的,不具有交换性
  • 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接 访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递 如果C是B的友元, B是A的友元,则不能说明C时A的友元。这点大家结合生活应该不难理解😯
  • 友元关系不能继承,在继承位置再给大家详细介绍。

(四)内部类

概念:

  • 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类, 它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越 的访问权限。
  • 也就是说:内部类相当于外部类的友元类。注意友元类的定义,内部类中的方法可以通过外部类的对象参数来访问外部类中的所有成员(包括private)。但是外部类不是内部类的友元。

接下来,我举个例子,大家看以下代码最终的结果是多少:

class A
{
private:
    int m;
public:
    class B // B天生就是A的友元
    {
    public:
        void inner(const A& a)
        {
            cout << a.m << endl;//OK
        }
         
    };
};
int main()
{
    A aa;
   
    cout << sizeof(aa) << endl;
    return 0;
}

直接打印看最终的结果:

  • 此时,大家有没有发现一个问题呀!最终打印出来为【4】,即为我们计算外部类的大小时,此时我们的大小与内部类无关。

外部类和内部类是相当于两个独立的类,只是内部类的访问受外部类的类域和访问限定符的限制。那么我们要对内部域进行操作时应该怎么办呢?

具体可以像以下这种方式:

class A
{
private:
    
    int m;
public:
    class B // B天生就是A的友元
    {
    public:
        void inner(const A& a)
        {
            cout << a.m << endl;//OK
        }
    };
};
int main()
{
    A aa;
    cout << sizeof(aa) << endl;
    A::B b; //
    b.inner(A());
    
    return 0;
}

此时有小伙伴就会说内部类是不是没什么用呢这样看来?其实吧,不能说没用,只能说功能比较小而已对于C++来说,对于java使用这个就很频。

  • 因为B天生就是A的友元,因此在B里可以直接访问A的私有

我们举个例子来看看:

class A
{
private:
    static int n;
    int m;
public:
    class B // B天生就是A的友元
    {
    public:
        void inner(const A& a)
        {
            cout << n << endl;//OK
            cout << a.m << endl;//OK
        }
    };
};
int A::n = 10;
int main()
{
    A aa;
    cout << sizeof(aa) << endl;
    A::B b;
    b.inner(A());
    
    return 0;
}

解析:

我们在A里面定义了私有的n,此时B是可以直接访问的,我们编译看看是不是像我说的这样

但是如果B是一个全局的类,此时再去访问还可以吗?结果是报错的

总结:

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

  • 如果内部类定义在public,则可通过 外部类名::内部类名 来定义内部类的对象。
  • 如果定义在private,则外部不可定义内部类的对象,这可实现“实现一个不能被继承的类”问题。

2.sizeof(外部类)=仅仅是外部类的大小,和内部类没有任何关系。内部类的大小也和外部类没有关系

3.内部类可以直接访问外部类中的任何权限的static成员,不需要通过外部类的对象或类名来访问。


(五)匿名对象

定义:

  • 匿名对象可以理解为是一个临时对象,一般系统自动生成的,如你的函数返回一个对象,这个对象在返回时会生成一个临时对象。

其实这个我们在上述讲解生成临时对象的时候就该给大家提一下,我在这里单独放在了一部分来给大家讲解。接下来我们还是通过代码来进行认识:

当我们定义对象时,最常见的就是下面这种:

但是当我们这样定义时还可以吗?

  • 答案是不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义

那这样呢?

  • 答案是可以的,我们可以这么定义匿名对象,匿名对象的特点不用取名字, 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数


(六)  拷贝对象时的一些编译器优化

  • 在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还 是非常有用的。

接下来我们一个一个看。

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 func1(A aa)
{  
}
void func2(const A& aa) //注意临时变量具有常性
{
  
}

首先对于传值传参来说:

  • 以下我上述是不是已经讲过啦!这里不再重复

  • 这里大家是否明白为什么还有析构呢?

  • 注意这里本来是构造,加拷贝构造,最后两个析构,结果直接优化为了构造,声明周期在【func1】里面,结束后再去析构

  • 最后这个就不用说了,只是形式不同罢了

接下来对于传引用传参来说呢:

  • 对于【func2(aa1);  】 是无优化的我们可以看到。因为这是引用传参,【aa】是【aa1】的别名,没有任何东西

  • 对于【func2(20);  】 也是无优化的我们可以看到

  • 对于【func2(A(30)); 】 也是无优化的我们可以看到

结论:

  • 因此我们不难得出一个结论那就是传参的话尽量使用引用传参

那对于这两个呢?

A func3()
{
  A aa;
  return aa;
}
int main()
{
  
  func3();
  A aa2 = func3();
  return 0;
}

我给大家画图进行理解:

  • 所以,我们不难看出对于【aa2】来说,经过连续的拷贝构造,在之前我们已经知道,对连续的拷贝构造,编译器就会对其进行优化。因此,上述两者其实结果是一样的,都是一个构造加一个拷贝构造。

接下来,给大家看一组代码,大家觉得哪种好些呢?

int main()
{
  
  func3();
  A aa2 = func3();
  cout << "---------------------------------------" << endl;
  A aa3;
  aa3 = func3();
  return 0;
}

解析 :

我们先直接打印看结果:

此时大家发现没有,二者存在的区别是多么的巨大。

紧接着还有这种情况

A func4()
{
  return A();
}
int main()
{
  func4();
    A aa2 = func4();
  return 0;
}

这个样例的最终结果是什么呢?我们先直接打印看最终的结果:

解析:

  • 对于【func4()】,其实是构造加拷贝构造,最终直接优化为构造;
  • 对于【A aa2 = func4();】,其实是构造+拷贝构造+拷贝构造,最终优化为了构造

结论:

  • 因此,我们在日后在返回的时候,能用这个命名对象就用命名对象进行返回,可以加速编译器的优化!!!
  • 函数中返回对象时,尽量使用匿名对象!

具体的还可以参考一本书:《深度探索C++ 对象模型》


(七)再次理解封装

现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现 实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创 建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:

  • 1. 用户先要对现实中洗衣机实体进行抽象---即在人为思想层面对洗衣机进行认识,洗衣机有什 么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
  • 2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清 楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、 Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
  • 3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣 机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才 能洗衣机是什么东西。
  • 4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。


(八)总结

到此,关于类和对象的所有的知识便全部讲完了!!大家都掌握了吗?😉

接下来我们总结一下类和对象都学到了什么:

  • 在类和对象(上)篇,我们首选先引出了关于C++面向对象编程的思想,紧接着对类的定义啊进行了相关的学习,还包括类的访问限定符及封装,作用域,类的实例化 对象大小的计算 ,成员函数的this指针等相关的学习;
  • 紧接着在类和对象(中),我们对类和对象的六大默认成员函数进行了具体讲解
  • 最后,在本期博客,我们对之前学习过的知识进行了深入的思考与理解,并在此基础上学习了关于静态成员函数,友元函数,内部类,以及匿名对象进行了学习,最后对编译器的优化进行了探讨。
  • 至此,便是关于类和对象的全部知识点总结了!

在类和对象阶段,大家一定要体会到这样的观点:

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

最后,如果本文对您有帮助的话,记得点赞三连哟!!!

相关文章
|
1月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
51 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
106 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
97 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
114 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
32 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
29 1
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
32 4
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
2月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
2月前
|
存储 编译器 C语言
【C++类和对象(上)】—— 我与C++的不解之缘(三)
【C++类和对象(上)】—— 我与C++的不解之缘(三)