【C++初阶】第四站:类和对象(下)(理解+详解)-2

简介: 【C++初阶】第四站:类和对象(下)(理解+详解)-2

【C++初阶】第四站:类和对象(下)(理解+详解)-1

https://developer.aliyun.com/article/1457033?spm=a2c6h.13148508.setting.17.2e124f0eLcFjXO



面试题

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

断语句: 求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)

#include<iostream>
using namespace std;
class Sum//定义一个名为Sum的类
{
public:
    Sum()//构造函数,当创建Sum对象时自动调用
    {
        _ret +=_i;// 每次构造函数被调用时,将静态成员变量_i的当前值累加到静态成员变量_ret上
        _i++; //紧接着递增静态成员变量_i的值
    }
    static int GetRet()//定义一个静态成员函数GetRet,用于获取静态成员变量_ret的值
    {
        return _ret;//直接返回静态变量_ret的值
    }
private:
//定义两个静态私有成员变量
// 静态成员变量属于类,不是某个对象所有,而是所有对象共享,并且在整个程序生命周期内只初始化一次
    static int _i;//初始化为1,每次构造函数调用时递增
    static int _ret;// 初始化为0,用于累计构造函数调用次数
};
//对静态成员变量进行初始化(定义)
int Sum::_i = 1;
int Sum::_ret = 0;
//定义另一个名为Solution的类
class Solution {
public:
    // 定义成员函数Sum_Solution,接收一个整数参数n
    int Sum_Solution(int n) {
        Sum a[n];
        return Sum::GetRet();
    }
};

友元

 

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

友元函数

说明 :

友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同

我们之前在:【C++初阶】第一站:C++入门基础(上) -- 良心详解-CSDN博客


简单了解过关于cout(流插入),cin(流提取)的知识


现在我们来回顾一下:


在之前的印象中,当我们遇到关于内置类型(int,float,double等),可以直接使用cout和cin进行输出和输入:


91d95d5c01774fa5b1c65051c1ccb5cd.png


原因是什么呢,通过查阅资料可以发现:i 和 j 通过操作符重载间接实现了类似成员函数的功能。



520b879e560643e9bfc9a170ca10bf1d.png

cin是istream类型的对象,cout是ostream类型的对象


在C++中,内置类型是直接支持cout流插入<<和cin流提取>>,这并不是什么自动识别类型,是运算符重载和函数重载罢了,库里面支持把内置类型作为成员函数,重载了



057490cbb5744d91858014ca9860443f.png

这时候,我们创建Date类的一个自定义类型的对象,使用cout和cin输出和输入会发现编译错误:


9a03fa7aafa74e6398e33603a0c6fd55.png


我们可以看到,隐含的this指针,占据着这个流插入成员函数的第一个参数的位置,与main函数内调用的位置不相符,cout是ostream类型的对象,但是到了成员函数,第一个位置是Date*类型

d4fc050b047b44f8a3e61b7478f890b4.png

既然它的位置不相符,那么我们可以这样写吗:

65a36ee85bf14928aec40cde8ad8ce4f.png



       可以是可以,但是流插入的本质是:应该是对象流入到console里面去,而不是console流入到对象里 ,对于流提取同理


4c01961fc78349ccb49ed659d25e7887.png


这时候我们把位于Date.h里原本成员函数的声明注释掉:


448945d3e796429d961cf9ad606afb80.png


我们在全局定义一个<<重载的函数,定义成全局的声明,此时经过编译后,又引发了一个新问题:


5d50556e69e74564b906160e17d1c2b9.png


面对这样的情况,该如何去纠正:


d26aa54c999f412e9af87f31299cb621.png


在类的外部要想访问内部私有成员,用友元声明:在类的公有和私有声明都可以


e4cab4a6c7fb4c4291c61d458bc0d661.png


我们发现就可以编译通过了:


ff5110ba824e44e19d49b7513c08ec2e.png


并且类型的顺序也是匹配的:


662f752b7ca14a5f8f130ac3a134a5ac.png


但是当咱们连续输出两个自定义对象的时候,编译就不会通过了,看下面解析:


3d51b08899fd41b9b2a41232b7b4a3ea.png


这时候我们把.h里面的友元的返回值改成ostream&、全局声明和.cpp里面的返回值也改成一样:


e6b7928ddc0e46e4b41bb28a125e27af.png


对于流提取,并不能给声明加const:

4a09bafb44624524846ccf3df67ff629.png



总结:


内置类型,可以使用<<>>是因为函数重载加运算符重载
自定义类型使用的方式是重载这个流插入和流提取的运算符


问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。

因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。

class Date
{
public:
    Date(int year, int month, int day)
     : _year(year)
     , _month(month)
     , _day(day)
     {}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
    ostream& operator<<(ostream& _cout)
    {
     _cout << _year << "-" << _month << "-" << _day << endl;
     return _cout;
    }
private:
    int _year;
    int _month;
    int _day;
};


所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在

类的内部声明,声明时需要加friend关键字。

adfa21300c334a6fa96de96d5c53b121.png


友元类

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

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

       比如下面的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;
};

内部类

内部类概念

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,

它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越

的访问权限

注意:内部类就是外部类的友元类, 参见友元类的定义,内部类可以通过外部类的对象参数来访

问外部类中的所有成员。但是外部类不是内部类的友元。


64fef9aa300b45f89022bdec3c066325.png


从下面这张图看出来什么:


98a7bd9c3b9a453abf4a02081d35ec8e.png


总结:

1.B类 受A类域和访问限定符的限制,其实它们是两个独立的类
2.内部类默认就是外部类的友元 -- 内部类可以访问外部类,外部类不能访问类部类

特性:

1. 内部类可以定义在外部类的 public 、 protected 、 private 都是可以的。
2. 注意内部类可以直接访问外部类中的 static 成员,不需要外部类的对象 / 类名。
3. 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::B b;
    b.foo(A());
   
    return 0;
}


执行:

306ceacfee5342eb809169af48036ab5.png



优化面试题

求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)
class Solution {
    class Sum {
    public:
        Sum()
        {
            _ret += _i;
            _i++;
        }
         };
  public:
    int Sum_Solution(int n) {
        Sum a[n];
        return _ret;
    }
    private:
        static int _i;
        static int _ret;
};
int Solution::_i = 1;
int Solution::_ret = 0;


匿名对象

#include<iostream>
using namespace std;
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;
}

执行:


9a2b2f1c760647c2ac745e2df15e6e5a.png


匿名对象和有名对象


7c303986825b435aabb59b7e13232e23.png

例子



796e44e557944c7b9f402cd2cb939f6c.png

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

知识点回顾:

   

C++默认兼容C语言,默认生成的拷贝构造函数对内置类型会完成值拷贝(跟结构体拷贝一样)但是又规定自定义类型传值传参过程中,符合拷贝构造:拿一个以及存在的对象去初始化另一个对象
        为什么要规定调拷贝构造?因为直接搞值拷贝,会有很大的问题,比如说像栈、顺序表、链表这样的类(析构两次),对象里面可能还有一个指针指向一块空间,这时候就要完成深拷贝,那想让这个拷贝正确该怎么办呢,就要自己去写那个深拷贝
        所以c++在这一块完成了完美的兼容,对于日期类就算要浅拷贝(不写,编译器默认生成的拷贝构造),编译器有着个性化处理,对于栈要自己写深拷贝,对于日期类写不写都行


示例(包含讲解):

比如下面这个例子中:传值传参引发了对象的拷贝,拷贝要调用拷贝构造,拷贝出aa1的副本aa,然后出了作用域aa先析构,回到main函数之后,出了作用域aa1再调析构


90e64138529440a89f95b539c801c6d8.png


可以想象一下,假设我仅仅只想调用一下Print()有没有必要使用拷贝构造?没有吧。


3155ce5b5fd54521b62f2ea36cb11835.png


我们要给这个形参加上引用,同时加上const,这样的话就不会引发拷贝,也保护了对象不可修改:


5a20f94f5b4c468eb3be7a8784d41d31.png


同时插播一下,这两者是有着显著区别的:

6f007f09821946c48b51e35345030335.png




其实对于void f1的(A& aa)这个地方可以不加const,这属于权限的缩小,但是对于匿名对象来说可不行

4b0d7a66379046568d7535e721554005.png

因为:f1(A())这一行试图将一个匿名临时对象传递给需要非const引用参数的函数f1(A& aa)。

匿名临时对象不能绑定到非const引用上,因为匿名临时对象生命周期结束后会自动销毁,

而非const引用可能会尝试修改临时对象,这是不允许的。

所以要给这个函数加上 const-->void f1(const A& aa)

另外, 我们知道匿名对象的生命周期只在这一行,但是const引用会延迟匿名对象的生命周期:

87806b338a92465f8690756ea4c41d58.png

传值传参和传值返回

对于编译器处理 传值传参和传值返回的总结:

传值返回 -- 不能带引用返回,因为aa出了这个作用域调析构了。 如果返回了aa的引用,意味着返回的引用指向了一个已经销毁的对象,在实际运行时可能导致各种难以预料的问题:


a0409e9ea0774c07b1da9e7f7bc855d5.png

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

对于析构的分析


a2b9774787e74e8f9c04b797873450ce.png


对于f2(),仅此于f2(),我们分析一下析构:


第一次析构:在 f2 函数内部,局部变量 aa 在 return aa; 语句处会触发一次析构。这是因为 aa 是 f2 函数的局部对象,当函数执行完毕时,局部对象的生命周期结束,因此会调用析构函数。
第二次析构:f2 函数返回的是 A 类的一个对象,但由于它是通过值返回的,所以在 f2() 调用处会创建一个临时对象接收返回值。然而,由于这个临时对象在表达式结束之后没有被存储到任何地方,因此它也会在表达式(也就是f2() )结束时立即被销毁,从而触发第二次析构。


构造+拷贝构造

一个表达式,连续的步骤里面,连续的构造会被合并


f1(1):隐式类型,连续构造(构造函数)+拷贝构造->优化为直接构造
f1(A(2)):一个表达式中, 连续构造(构造函数)+拷贝构造->优化为一个构造

3b6f1e07ff894043b20375955b4e61b1.png


连续的拷贝构造

一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造

d0f4e09a77194688bd280eaafa96e2ba.png


拷贝构造+赋值重载(无法优化)

一个表达式中,连续拷贝构造+赋值重载->无法优化

7f7529c68d1640ab90426d0300b92a90.png


//上文有A类的定义
A f2()
{
  A aa;
  return aa;
}
int main()
{
  // 一个表达式中,连续拷贝构造+赋值重载->无法优化
  A aa1;
  aa1 = f2();
  cout << endl;
  return 0;
}

执行:


775aa6085f3349b9a51425c84fce259a.png


       拷贝构造的aa,返回的临时拷贝,也就是回到main函数之后的那个临时对象(黄色字),要等到赋值运算符重载完毕之后,才调的析构


再次理解类和对象

fd425c6bfa15454598ea1c621840855f.png


类和对象篇就此结束,接下来是内存管理。


相关文章
|
8天前
|
C++
C++(十一)对象数组
本文介绍了C++中对象数组的使用方法及其注意事项。通过示例展示了如何定义和初始化对象数组,并解释了栈对象数组与堆对象数组在初始化时的区别。重点强调了构造器设计时应考虑无参构造器的重要性,以及在需要进一步初始化的情况下采用二段式初始化策略的应用场景。
|
8天前
|
存储 编译器 C++
C ++初阶:类和对象(中)
C ++初阶:类和对象(中)
|
8天前
|
C++
C++(十六)类之间转化
在C++中,类之间的转换可以通过转换构造函数和操作符函数实现。转换构造函数是一种单参数构造函数,用于将其他类型转换为本类类型。为了防止不必要的隐式转换,可以使用`explicit`关键字来禁止这种自动转换。此外,还可以通过定义`operator`函数来进行类型转换,该函数无参数且无返回值。下面展示了如何使用这两种方式实现自定义类型的相互转换,并通过示例代码说明了`explicit`关键字的作用。
|
8天前
|
存储 设计模式 编译器
C++(十三) 类的扩展
本文详细介绍了C++中类的各种扩展特性,包括类成员存储、`sizeof`操作符的应用、类成员函数的存储方式及其背后的`this`指针机制。此外,还探讨了`const`修饰符在成员变量和函数中的作用,以及如何通过`static`关键字实现类中的资源共享。文章还介绍了单例模式的设计思路,并讨论了指向类成员(数据成员和函数成员)的指针的使用方法。最后,还讲解了指向静态成员的指针的相关概念和应用示例。通过这些内容,帮助读者更好地理解和掌握C++面向对象编程的核心概念和技术细节。
|
21天前
|
存储 算法 编译器
c++--类(上)
c++--类(上)
|
27天前
|
编译器 C++
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
|
27天前
|
编译器 C++
virtual类的使用方法问题之静态和非静态函数成员在C++对象模型中存放如何解决
virtual类的使用方法问题之静态和非静态函数成员在C++对象模型中存放如何解决
|
27天前
|
编译器 C++
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
|
8天前
|
存储 C++
C++(五)String 字符串类
本文档详细介绍了C++中的`string`类,包括定义、初始化、字符串比较及数值与字符串之间的转换方法。`string`类简化了字符串处理,提供了丰富的功能如字符串查找、比较、拼接和替换等。文档通过示例代码展示了如何使用这些功能,并介绍了如何将数值转换为字符串以及反之亦然的方法。此外,还展示了如何使用`string`数组存储和遍历多个字符串。
|
16天前
|
存储 C++
C++ dll 传 string 类 问题
C++ dll 传 string 类 问题
15 0